<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 3D巨大迷路 v25D</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script src="./js/aframe-vrm.js"></script>
<script>
// --- SCRIPT BLOCK 1: Component Definitions ---
AFRAME.registerComponent('camera-relative-controls', {
schema: {
targetSpeed: { type: 'number', default: 5 },
acceleration: { type: 'number', default: 10 },
damping: { type: 'number', default: 8 },
brakingDeceleration: { type: 'number', default: 20 },
enabled: { type: 'boolean', default: true },
rotationSpeed: { type: 'number', default: 1.5 },
pitchLimit: { type: 'number', default: 85 },
verticalSpeed: { type: 'number', default: 30 },
groundY: { type: 'number', default: 0 },
ceilingY: { type: 'number', default: 6.4 }
},
init: function () {
this.keys = {};
this.leftThumbstickInput = { x: 0, y: 0 };
this.rightThumbstickInput = { x: 0, y: 0 };
this.currentVelocity = new THREE.Vector3();
this.ZERO_VECTOR = new THREE.Vector3();
this.cameraDirection = new THREE.Vector3();
this.cameraRight = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.desiredVelocity = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.rigEl = this.el;
this.cameraEl = this.el.querySelector('[camera]');
this.isReady = false;
this.mazeData = null;
this.mazeCellSize = 0;
this.mazeOffset = 0;
this.goldenWallPositions = [];
this.validWarpDestinations = [];
this.health = 100;
this.healthElementPC = document.getElementById('health-display-pc');
this.healthElementVR = null;
if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); }
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand');
if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); }
else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); }
else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); }
});
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
setMazeInfo: function(mazeData, cellSize, offset, goldenWalls, warpDestinations) {
this.mazeData = mazeData;
this.mazeCellSize = cellSize;
this.mazeOffset = offset;
this.goldenWallPositions = goldenWalls;
this.validWarpDestinations = warpDestinations;
console.log(`[Controls] Maze info received. Golden walls: ${goldenWalls.length}, Warp destinations: ${warpDestinations.length}`);
},
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} }
if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} }
},
resetHealth: function() {
this.health = 100;
},
worldToMazeCoords: function(worldX, worldZ) {
const i = Math.round(worldX / this.mazeCellSize + this.mazeOffset);
const j = Math.round(worldZ / this.mazeCellSize + this.mazeOffset);
return { i, j };
},
isNearGoldenWall: function() {
if (!this.goldenWallPositions) return false;
const playerPos = this.rigEl.object3D.position;
const detectionRange = (this.mazeCellSize / 2) + 1;
for (const wallPos of this.goldenWallPositions) {
const dx = Math.abs(playerPos.x - wallPos.x);
const dz = Math.abs(playerPos.z - wallPos.z);
if (dx <= detectionRange && dz <= detectionRange) {
return true;
}
}
return false;
},
warpPlayer: function(fromCoords) {
if (this.validWarpDestinations.length === 0) {
console.warn("No valid warp destinations available.");
return;
}
const destinationIndex = Math.floor(Math.random() * this.validWarpDestinations.length);
const destinationCoords = this.validWarpDestinations[destinationIndex];
const destWorldX = (destinationCoords.i - this.mazeOffset) * this.mazeCellSize;
const destWorldZ = (destinationCoords.j - this.mazeOffset) * this.mazeCellSize;
this.rigEl.object3D.position.set(destWorldX, this.data.groundY, destWorldZ);
if (fromCoords) {
this.mazeData[fromCoords.i][fromCoords.j] = 0;
const warpFloorEl = document.getElementById(`warp-floor-${fromCoords.i}-${fromCoords.j}`);
if (warpFloorEl) {
warpFloorEl.parentNode.removeChild(warpFloorEl);
}
}
console.log(`Warped to: (${destinationCoords.i}, ${destinationCoords.j})`);
},
onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; },
onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; }
else { return; }
}
if (!this.cameraEl || !this.cameraEl.object3D || !this.rigEl || !this.rigEl.object3D) { return; }
if (!this.healthElementVR) {
this.healthElementVR = document.getElementById('health-display-vr');
}
const data = this.data;
const dt = timeDelta / 1000;
const position = this.rigEl.object3D.position;
if (this.rigEl.sceneEl.is('vr-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) { this.rigEl.object3D.rotation.y += -this.rightThumbstickInput.x * data.rotationSpeed * dt; }
}
const cameraObject = this.cameraEl.object3D;
cameraObject.getWorldQuaternion(this.cameraWorldQuaternion);
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); this.cameraDirection.normalize();
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; this.cameraRight.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); }
if (Math.abs(this.leftThumbstickInput.y) > 0.1) { this.moveDirection.add(this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y)); }
if (Math.abs(this.leftThumbstickInput.x) > 0.1) { this.moveDirection.add(this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.x)); }
const isInputting = this.moveDirection.lengthSq() > 0.0001;
if (isInputting) { this.moveDirection.normalize(); }
let lerpFactor = data.damping;
if (isInputting) {
if (this.currentVelocity.dot(this.moveDirection) < -0.1) { this.desiredVelocity.set(0,0,0); lerpFactor = data.brakingDeceleration;
} else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; }
} else { this.desiredVelocity.set(0,0,0); lerpFactor = data.damping; }
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.set(0,0,0); }
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
if (this.mazeData) {
const margin = 0.2;
const nextX = position.x + deltaPosition.x;
const signX = Math.sign(deltaPosition.x);
const { i: nextI_X, j: nextJ_X } = this.worldToMazeCoords(nextX + (margin * signX), position.z);
const tileTypeX = this.mazeData[nextI_X] ? this.mazeData[nextI_X][nextJ_X] : 0;
if (tileTypeX === 1 || tileTypeX === 2) {
deltaPosition.x = 0;
if (!isInputting) { this.currentVelocity.x = 0; }
}
const nextZ = position.z + deltaPosition.z;
const signZ = Math.sign(deltaPosition.z);
const { i: nextI_Z, j: nextJ_Z } = this.worldToMazeCoords(position.x, nextZ + (margin * signZ));
const tileTypeZ = this.mazeData[nextI_Z] ? this.mazeData[nextI_Z][nextJ_Z] : 0;
if (tileTypeZ === 1 || tileTypeZ === 2) {
deltaPosition.z = 0;
if (!isInputting) { this.currentVelocity.z = 0; }
}
}
position.add(deltaPosition);
}
let verticalMovement = 0;
if (this.rigEl.sceneEl.is('vr-mode') && Math.abs(this.rightThumbstickInput.y) > 0.1) {
verticalMovement = -this.rightThumbstickInput.y * data.verticalSpeed * dt;
}
const nextY = position.y + verticalMovement;
const isNearGold = this.isNearGoldenWall();
const ceiling = isNearGold ? Infinity : data.ceilingY;
if (nextY > ceiling) {
position.y = ceiling;
} else if (nextY < data.groundY) {
position.y = data.groundY;
} else {
position.y = nextY;
}
if (this.mazeData) {
const playerCoords = this.worldToMazeCoords(position.x, position.z);
if (this.mazeData[playerCoords.i] && this.mazeData[playerCoords.i][playerCoords.j] === 3) {
this.warpPlayer(playerCoords);
}
}
const enemies = document.querySelectorAll('.enemy');
const playerPos = this.rigEl.object3D.position;
const damageDistance = 4.0;
const damagePerSecond = 20;
let isTakingDamage = false;
const playerPos2D = new THREE.Vector2(playerPos.x, playerPos.z);
enemies.forEach(enemy => {
if (enemy.getAttribute('enemy-ai')?.enabled) {
const enemyPos = new THREE.Vector3();
enemy.object3D.getWorldPosition(enemyPos);
const enemyPos2D = new THREE.Vector2(enemyPos.x, enemyPos.z);
if (playerPos2D.distanceTo(enemyPos2D) < damageDistance) {
isTakingDamage = true;
this.health -= damagePerSecond * dt;
}
}
});
if (this.health < 0) {
this.health = 0;
}
if (this.healthElementPC) { this.healthElementPC.textContent = `体力: ${Math.floor(this.health)}`; }
if (this.healthElementVR) { this.healthElementVR.setAttribute('troika-text', 'value', `体力: ${Math.floor(this.health)}`); }
if (this.health <= 0) {
if (window.triggerGameOver) {
window.triggerGameOver("体力切れ");
}
}
if (this.mazeData) {
const playerCoords = this.worldToMazeCoords(position.x, position.z);
const goalI = this.mazeData.lengt
使用変数
-------( Function ) | |
) { if -------( Function ) | |
) { this.health = 100; }, worldToMazeCoords: function -------( Function ) | |
cameraDirection | |
cameraEl | |
cameraObject | |
cameraRight | |
ceiling | |
currentVelocity | |
damageDistance | |
damagePerSecond | |
data | |
deltaPosition | |
desiredVelocity | |
destinationCoords | |
destinationIndex | |
destWorldX | |
destWorldZ | |
detectionRange | |
dt | |
dx | |
dz | |
eftThumbstickInput | |
enemies | |
enemy | |
enemyPos2D | |
enemyPos | |
eraWorldQuaternion | |
ffectiveLerpFactor | |
fromCoords) { if -------( Function ) | |
ghtThumbstickInput | |
goalI | |
health | |
healthElementPC | |
healthElementVR | |
i | |
idWarpDestinations | |
isInputting | |
isNearGold | |
isReady | |
isTakingDamage | |
j | |
keys | |
leftHand | |
length | |
lerpFactor | |
margin | |
mazeCellSize | |
mazeData, cellSize, offset, goldenWalls, warpDestinations) { this.mazeData = mazeData; this.mazeCellSize = cellSize; this.mazeOffset = offset; this.goldenWallPositions = goldenWalls; this.validWarpDestinations = warpDestinations; console.log -------( Function ) | |
mazeData | |
mazeOffset | |
moveDirection | |
nextX | |
nextY | |
nextZ | |
oldenWallPositions | |
onKeyDown | |
onKeyUp | |
playerCoords | |
playerPos2D | |
playerPos | |
position | |
rigEl | |
rightHand | |
signX | |
signZ | |
src | |
textContent | |
tileTypeX | |
tileTypeZ | |
verticalMovement | |
warpFloorEl | |
x | |
y | |
z | |
ZERO_VECTOR |