junkerstock
 vrz132ー2 

<!DOCTYPE html>
<html>
<head>
<title>VRテスト環境 Ver13.2 動作調整</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/MozillaReality/ammo.js@8bbc0ea/builds/ammo.wasm.js"></script>
<script src="./js2/aframe-physics-system.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>
// --- Component: Player Controls ---
AFRAME.registerComponent('camera-relative-controls', {
schema: {
targetSpeed: { type: 'number', default: 2 },
acceleration: { type: 'number', default: 3 },
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: 2 },
groundY: { type: 'number', default: 0 },
ceilingY: { type: 'number', default: 50 },
gravity: { type: 'number', default: 0 }
},
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;

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)); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); }
});

this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
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){} }
},
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; }

const data = this.data;
const dt = timeDelta / 1000;
const position = this.rigEl.object3D.position;

if (this.rigEl.sceneEl.is('vr-mode') || this.rigEl.sceneEl.is('ar-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) {
this.rigEl.object3D.rotation.y += -this.rightThumbstickInput.x * data.rotationSpeed * dt;
}
if (Math.abs(this.rightThumbstickInput.y) > 0.1) {
const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt;
this.rigEl.object3D.position.y -= verticalMovement;
}
}

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;
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);
position.add(deltaPosition);
}

let newY = position.y;
if (this.keys['KeyE']) { newY += data.verticalSpeed * dt; }
if (this.keys['KeyC']) { newY -= data.verticalSpeed * dt; }

if (position.y > data.groundY && data.gravity > 0) {
newY -= data.gravity * dt;
}
position.y = Math.max(data.groundY, Math.min(newY, data.ceilingY));
},
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight', 'KeyE', 'KeyC'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});

AFRAME.registerComponent('accelerator', {
schema: {
type: { type: 'string', default: 'up' },
force: { type: 'number', default: 18 }
},
init: function () {
this.collidingEls = new Set();
this.objectContainer = document.getElementById('object-container');

this.bbox = new THREE.Box3();
this.el.object3D.updateWorldMatrix(true, false);
this.bbox.setFromObject(this.el.object3D);
},
tick: function () {
const dynamicEls = Array.from(this.objectContainer.children)
.filter(el => el.dataset.physicsType === 'dynamic' && el.id !== window.grabbedElId);

if (dynamicEls.length === 0) return;

this.el.object3D.updateWorldMatrix(true, false);
this.bbox.setFromObject(this.el.object3D);

dynamicEls.forEach(otherEl => {
if (!otherEl.object3D) return;
const otherBbox = new THREE.Box3().setFromObject(otherEl.object3D);

const isColliding = this.bbox.intersectsBox(otherBbox);

if (isColliding) {
if (!this.collidingEls.has(otherEl)) {
this.collidingEls.add(otherEl);
this.applyEffect(otherEl);
}
} else {
if (this.collidingEls.has(otherEl)) {
this.collidingEls.delete(otherEl);
}
}
});
},

applyEffect: function(otherEl) {
if (!otherEl || !otherEl.body) return;

const velocity = otherEl.body.getLinearVelocity();

switch (this.data.type) {
case 'up':
otherEl.body.setLinearVelocity(new Ammo.btVector3(velocity.x(), 0, velocity.z()));
const impulse = new THREE.Vector3(0, 1, 0).multiplyScalar(this.data.force);
otherEl.body.applyCentralImpulse(new Ammo.btVector3(impulse.x, impulse.y, impulse.z));
break;

case 'straight':
const straightDir = new THREE.Vector3(velocity.x(), 0, velocity.z()).normalize();
const straightForce = straightDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(straightForce.x, 0, straightForce.z));
break;

case 'reverse':
const reverseDir = new THREE.Vector3(-velocity.x(), 0, -velocity.z()).normalize();
const reverseForce = reverseDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(reverseForce.x, 0, reverseForce.z));
break;

default: // 'left' と 'right'
const travelDir = new THREE.Vector3(velocity.x(), 0, velocity.z()).normalize();
if (travelDir.lengthSq() > 0.01) {
const angle = this.data.type === 'left' ? Math.PI / 2 : -Math.PI / 2;
travelDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
const forceVec = travelDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(forceVec.x, forceVec.y, forceVec.z));
}
break;
}
}
});

AFRAME.registerComponent('hand-grab', {
schema: {
enabled: { type: 'boolean', default: false },
grabRadius: { type: 'number', default: 0.1 }
},
init: function () {
this.grabbedEl = null;
this.objectContainer = document.getElementById('object-container');
this.handPos = new THREE.Vector3();
this.objPos = new THREE.Vector3();
this.isAButtonPressed = false;
this.isBButtonPressed = false;

this.onAButtonDown = this.onAButtonDown.bind(this);
this.onAButtonUp = this.onAButtonUp.bind(this);
this.onBButtonDown = this.onBButtonDown.bind(this);
this.onBButtonUp = this.onBButtonUp.bind(this);
this.onTriggerDown = this.onTriggerDown.bind(this);
this.onTriggerUp = this.onTriggerUp.bind(this);
this.el.addEventListener('triggerdown', this.onTriggerDown);
this.el.addEventListener('triggerup', this.onTriggerUp);
this.el.addEventListener('abuttondown', this.onAButtonDown);
this.el.addEventListener('abuttonup', this.onAButtonUp);
this.el.addEventListener('bbuttondown', this.onBButtonDown);
this.el.addEventListener('bbuttonup', this.onBButtonUp);
},
remove: function () {
this.el.removeEventListener('triggerdown', this.onTriggerDown);
this.el.removeEventListener('triggerup', this.onTriggerUp);
this.el.removeEventListener('abuttondown', this.onAButtonDown);
this.el.removeEventListener('abuttonup', this.onAButtonUp);
this.el.removeEventListener('bbuttondown', this.onBButtonDown);
this.el.removeEventListener('bbuttonup', this.onBButtonUp);
},
onAButtonDown: function() { this.isAButtonPressed = true; },
onAButtonUp: function() { this.isAButtonPressed = false; },
onBButtonDown: function() { this.isBButtonPressed = true; },
onBButtonUp: function() { this.isBButtonPressed = false; },

onTriggerDown: function () {
if (!this.data.enabled || this.grabbedEl) return;

this.el.object3D.getWorldPosition(this.handPos);
const grabbableEls = this.objectContainer.children;
let closestEl = null;
let closestDistSq = this.data.grabRadius * this.data.grabRadius;

for (let i = 0; i < grabbableEls.length; i++) {
const el = grabbableEls[i];
el.object3D.getWorldPosition(this.objPos);
const distSq = this.handPos.distanceToSquared(this.objPos);
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestEl = el;
}
}

if (closestEl) {
if (this.isBButtonPressed) {
closestEl.parentNode.removeChild(closestEl);
return;
}
if (this.isAButtonPressed) { // --- コピー操作 ---ハンドモード
const worldPos = new THREE.Vector3();
const worldQuat = new THREE.Quaternion();
closestEl.object3D.getWorldPosition(worldPos);
closestEl.object3D.getWorldQuaternion(worldQuat);

const copyEl = closestEl.cloneNode(true);
const originalType = copyEl.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
copyEl.setAttribute('id', `${originalType}-${window.createdObjectCounter++}`);

this.objectContainer.appendChild(copyEl);

const originalMaterial = closestEl.getAttribute('material');
if (originalMaterial) {
copyEl.setAttribute('material', originalMaterial);
}
const originalAccelerator = closestEl.getAttribute('accelerator');
if (originalAccelerator) {
copyEl.setAttribute('accelerator', originalAccelerator);
}
if (closestEl.hasAttribute('accelerator')) {
copyEl.removeAttribute('ammo-body');
copyEl.dataset.physicsType = 'static';
} else {
const isGravityEnabled = closestEl.dataset.physicsType === 'dynamic';
if (copyEl.hasAttribute('ammo-body')) {
if (isGravityEnabled) {
copyEl.setAttribute('ammo-body', 'type: dynamic; mass: 2;');
copyEl.dataset.physicsType = 'dynamic';
} else {
copyEl.setAttribute('ammo-body', 'type: static;');
copyEl.dataset.physicsType = 'static';
}
}
}
const euler = new THREE.Euler().setFromQuaternion(worldQuat, 'YXZ');
const rotationDegrees = {
x: THREE.MathUtils.radToDeg(euler.x),
y: THREE.MathUtils.radToDeg(euler.y),
z: THREE.MathUtils.radToDeg(euler.z)
};
copyEl.setAttribute('position', worldPos);
copyEl.setAttribute('rotation', rotationDegrees);
this.grabbedEl = closestEl;
} else {
this.grabbedEl = closestEl;
}
this.el.object3D.attach(this.grabbedEl.object3D);
if (this.grabbedEl.hasAttribute('ammo-body')) {
this.grabbedEl.setAttribute('ammo-body', 'type', 'kinematic');
}
}
},
onTriggerUp: function () {
if (!this.data.enabled || !this.grabbedEl) return;

this.objectContainer.object3D.attach(this.grabbedEl.object3D);

if (this.grabbedEl.hasAttribute('ammo-body')) {const physicsType = this.grabbedEl.dataset.physicsType || 'dynamic';
this.grabbedEl.setAttribute('ammo-body', 'type', physicsType);
}
window.alignSingleObject(this.grabbedEl);
this.grabbedEl = null;
}
});
</script>

<style>
#hud-pc {
position: fixed; bottom: 20px; left: 20px; color: white;
background-color: rgba(0, 0, 0, 0.4); padding: 10px; border-radius: 10px;
font-family: sans-serif; font-size: 16px; font-weight: bold; z-index: 10;
}
#hud-pc #hud-main-content-pc > div:not(:last-child) { margin-bottom: 12px; }
#post-start-panel > div:not(:last-child) { margin-bottom: 12px; }
.hud-buttons { display: flex; gap: 10px; }
.hud-buttons > div {
flex-grow: 1; cursor: pointer; padding: 10px; border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
}

/* --- ボタンの配色 --- */
#replay-button-pc { background-color: #C0392B; }
#replay-button-pc:hover { background-color: #A93226; }
#help-button-pc { background-color: #3498DB; }
#credit-button-pc { background-color: #9B59B6; }
#create-gravity-box-pc { background-color: #8E44AD; }
#create-gravity-sphere-pc { background-color: #D35400; }
#create-grid-box-pc { background-color: #2980B9; }
#create-grid-sphere-pc { background-color: #27AE60; }
#create-accel-up-pc { background-color: #e74c3c; }
#create-accel-right-pc { background-color: #f1c40f; }
#create-accel-left-pc { background-color: #e67e22; }
#create-accel-straight-pc { background-color: #3498db; }
#create-accel-reverse-pc { background-color: #2ecc71; }
#change-background-pc { background-color: #7f8c8d; }
#change-background-pc:hover { background-color: #95a5a6; }

/* --- カラーパレット --- */
.color-palette-pc {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
}
.color-swatch-pc {
width: 100%;
height: 30px;
border-radius: 4px;
cursor: pointer;
border: 2px solid rgba(255, 255, 255, 0.5);
box-sizing: border-box;
}
.color-swatch-pc:hover { border-color: white; }

#xr-buttons {
position: fixed; bottom: 21px; right: 100px; display: flex; flex-direction: row;
gap: 10px; z-index: 1000;
}
#xr-buttons button {
padding: 6px 12px; font-size: 14px; border: 2px solid white;
background-color: rgba(0, 0, 0, 0.5); color: white; cursor: pointer;
border-radius: 8px; width: 60px; text-align: center;
}
#xr-buttons button:hover { background-color: rgba(255, 50, 100, 0.5); }

.a-dom-overlay:not(.a-no-style) {
top: auto !important; left: auto !important; padding: 0em;
}

#change-background-pc:hover { background-color: #95a5a6; }
#tidy-up-pc { background-color: #16A085; }

#post-start-panel {
display: none;
}

#replay-button-pc.pre-start {
font-size: 2em;
padding: 20px;
}

#info-overlay-pc {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 20;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
#info-content-pc {
background-color: white;
color: black;
padding: 30px;
border-radius: 10px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
white-space: pre-wrap;
font-family: sans-serif;
font-size: 16px;
}
#info-close-button-pc {
margin-top: 15px;
padding: 10px 25px;
background-color: #E74C3C;
color: white;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
}
</style>

</head>
<body>

<div id="hud-pc">
<div id="hud-main-content-pc">
<div class="hud-buttons">
<div id="replay-button-pc" onclick="handleStartReplayClick()">スタート</div>
<div id="help-button-pc">説明</div>
<div id="credit-button-pc">Credit</div>
<div id="change-background-pc">背景</div>
</div>

<div id="post-start-panel">
<div class="hud-buttons">
<div id="save-button-pc" style="background-color: #1abc9c;">セーブ</div>
<div id="load-button-pc" style="background-color: #9b59b6;">ロード</div>
<div id="tidy-up-pc">整理</div>
</div>

<div id="color-palette-pc" class="color-palette-pc"></div>

<div class="hud-buttons">
<div id="create-gravity-box-pc">■</div>
<div id="create-gravity-sphere-pc">●</div>
<div id="create-grid-box-pc">□</div>
<div id="create-grid-sphere-pc">〇</div>
</div>

<div class="hud-buttons">
<div id="create-accel-up-pc">上</div>
<div id="create-accel-right-pc">右</div>
<div id="create-accel-left-pc">左</div>
<div id="create-accel-straight-pc">順</div>
<div id="create-accel-reverse-pc">反</div>
</div>
</div>
</div>
</div>

<div id="info-overlay-pc">
<div id="info-content-pc"></div>
<div id="info-close-button-pc">閉じる</div>
</div>


<div id="xr-buttons">
<button id="ar-button">AR</button>
</div>

<a-scene id="myScene"
vr-mode-ui="enabled: false"
webxr="optionalFeatures: dom-overlay; overlayElement: #xr-buttons;"
background="color: #87CEEB"
renderer="colorManagement: true"
cursor="rayOrigin: mouse"
physics="driver: ammo; debug: false; gravity: -9.8;">

<a-assets>
<img id="skyTexture" src="./pic/h400.png">
<img id="gridTexture" src="./pic/grid3.png">
</a-assets>

<a-entity id="rig" position="0 1.6 2"
camera-relative-controls="targetSpeed: 2; acceleration: 3; verticalSpeed: 2;">
<a-entity id="player" position="0 0 0">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 0.1 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse;" raycaster="objects: .clickable, .grabbable;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"></a-entity>
</a-entity>

<a-entity id="leftHand" hand-controls="hand: left;">

<a-entity id="vr-hud" position="0.02 0.04 -0.35" rotation="-60 0 0">
<a-plane id="hud-background" width="0.4" height="0.40" color="#FAFAFA" opacity="0.8" side="double"></a-plane>


<a-box id="replay-button-vr" class="clickable" position="0 0 0.011"
width="0.4" height="0.4" depth="0.2" material="color: #2ECC71; shader: flat"></a-box>
<a-entity id="replay-text-vr" troika-text="value: スタート; color: black; fontSize: 0.04; align: center; anchor: center;"
position="0 0 0.12"></a-entity>


<a-box id="change-background-vr" class="clickable" position="0.15 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #bdc3c7; shader: flat"></a-box>
<a-entity id="change-background-text-vr" troika-text="value: 背景; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.15 0.17 0.016"></a-entity>

<a-box id="help-button-vr" class="clickable" position="-0.05 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #3498DB; shader: flat"></a-box>
<a-entity troika-text="value: 説明; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.05 0.17 0.016"></a-entity>

<a-box id="credit-button-vr" class="clickable" position="0.05 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #9B59B6; shader: flat"></a-box>
<a-entity troika-text="value: Credit; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.05 0.17 0.016"></a-entity>

<a-box id="tidy-up-vr" class="clickable" position="-0.05 -0.13 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #16A085; shader: flat"></a-box>
<a-entity id="tidy-up-text-vr" troika-text="value: 整理; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.05 -0.13 0.016"></a-entity>
<a-box id="toggle-grab-mode-vr" class="clickable" position="0.05 -0.13 0.01"
width="0.07" height="0.03" depth="0.01" material="color: yellow; shader: flat"></a-box>
<a-entity id="grab-mode-text-vr" troika-text="value: ライン; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.05 -0.13 0.016"></a-entity>

<a-box id="toggle-ground-vr" class="clickable" visible="false"
width="0.07" height="0.03" depth="0.01" position="0.15 0.17 0.01" material="color: #95a5a6; shader: flat"></a-box>
<a-entity id="toggle-ground-text-vr" troika-text="value: 地面ON/OFF; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.15 0.17 0.016" visible="false"></a-entity>

<a-entity id="color-palette-vr" position="0 0.03 0.01"></a-entity>



<a-box id="create-gravity-box-vr" class="clickable" width="0.06" height="0.06" depth="0.06" position="-0.125 -0.065 0.01" material="color: #8E44AD; shader: flat"></a-box>
<a-entity troika-text="value: 重; color: black; fontSize: 0.012;" position="-0.125 -0.065 0.042"></a-entity>

<a-sphere id="create-gravity-sphere-vr" class="clickable" radius="0.03" position="-0.045 -0.065 0.01" material="color: #D35400; shader: flat"></a-sphere>
<a-entity troika-text="value: 重; color: black; fontSize: 0.012;" position="-0.045 -0.065 0.042"></a-entity>

<a-box id="create-grid-box-vr" class="clickable" width="0.06" height="0.06" depth="0.06" position="0.045 -0.065 0.01" material="color: #2980B9; shader: flat"></a-box>
<a-entity troika-text="value: 固; color: black; fontSize: 0.012;" position="0.045 -0.065 0.042"></a-entity>

<a-sphere id="create-grid-sphere-vr" class="clickable" radius="0.03" position="0.125 -0.065 0.01" material="color: #27AE60; shader: flat"></a-sphere>
<a-entity troika-text="value: 固; color: black; fontSize: 0.012;" position="0.125 -0.065 0.042"></a-entity>


<a-box id="create-accel-up-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.16 -0.17 0.01" material="color: #e74c3c; shader: flat"></a-box>
<a-entity troika-text="value: 上; color: black; fontSize: 0.012;" position="-0.16 -0.17 0.016"></a-entity>
<a-box id="create-accel-right-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.08 -0.17 0.01" material="color: #f1c40f; shader: flat"></a-box>
<a-entity troika-text="value: 右; color: black; fontSize: 0.012;" position="-0.08 -0.17 0.016"></a-entity>
<a-box id="create-accel-left-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0 -0.17 0.01" material="color: #e67e22; shader: flat"></a-box>
<a-entity troika-text="value: 左; color: black; fontSize: 0.012;" position="0 -0.17 0.016"></a-entity>
<a-box id="create-accel-straight-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.08 -0.17 0.01" material="color: #3498db; shader: flat"></a-box>
<a-entity troika-text="value: 順; color: black; fontSize: 0.012;" position="0.08 -0.17 0.016"></a-entity>
<a-box id="create-accel-reverse-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.16 -0.17 0.01" material="color: #2ecc71; shader: flat"></a-box>
<a-entity troika-text="value: 反; color: black; fontSize: 0.012;" position="0.16 -0.17 0.016"></a-entity>

<a-box id="save-button-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.05 0.13 0.01" material="color: #1abc9c; shader: flat"></a-box>
<a-entity troika-text="value: セーブ; color: black; fontSize: 0.012; align: center; anchor: center;" position="-0.05 0.13 0.016"></a-entity>
<a-box id="load-button-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.05 0.13 0.01" material="color: #9b59b6; shader: flat"></a-box>
<a-entity troika-text="value: ロード; color: black; fontSize: 0.012; align: center; anchor: center;" position="0.05 0.13 0.016"></a-entity>

<a-entity id="info-panel-vr" position="0 0 0.05" visible="false">
<a-plane width="0.4" height="0.52" color="#333" opacity="0.9"></a-plane>
<a-entity id="info-text-vr" troika-text="value: Text here; color: white; fontSize: 0.015; anchor: center; align: left; maxWidth: 0.38; wrapCount: 40;"
position="0 0.03 0.013"></a-entity>

<a-box id="info-close-button-vr" class="clickable" width="0.1" height="0.03" depth="0.01" position="0 -0.25 0.01"
material="color: #E74C3C; shader: flat"></a-box>

<a-entity troika-text="value: 閉じる; color: white; fontSize: 0.012; anchor: center; align: center;"
position="0 -0.25 0.016"></a-entity>
</a-entity>


</a-entity>
</a-entity>

<a-entity id="rightHand"
hand-controls="hand: right;"
laser-controls="hand: right; model: false;"
raycaster="objects: .clickable, .grabbable; lineColor: white; lineOpacity: 0.75"
hand-grab>
<a-cylinder id="hand-pointer-visual" radius="0.005" height="0.25" rotation="90 0 0"
position="0 0 -0.125" material="color: lightblue; shader: flat" visible="false">
</a-cylinder>
</a-entity>

</a-entity>
</a-entity>

<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8;" position="-100 200 100"></a-entity>

<a-box
id="ground"
position="0.035 -0.955 0.035"
width="16"
height="2"
depth="16"
material="src: #gridTexture; repeat: 8 8;"
ammo-body="type: static;"
ammo-shape="type: box; halfExtents: 8 1 8;">
</a-box>

<a-entity id="object-container"></a-entity>

<a-sky id="sky" src="#skyTexture"></a-sky>

</a-scene>

<script>
// --- Main Scene Logic ---
document.addEventListener('DOMContentLoaded', function () {
const sceneEl = document.querySelector('a-scene');
let isGameStarted = false;
window.createdObjectCounter = 0;
let grabbedObject = null;
let draggedObject = null;
let dragDistance = 0;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const worldPosition = new THREE.Vector3();
let isLaserGrabMode = true;
let selectedColor = '#CCCCCC';
let isAButtonPressed = false;
let isShiftKeyPressed = false;
let isCtrlKeyPressed = false;
let isBButtonPressed = false;

window.copyDragData = null;
let vrCopyGrabData = null;
let copyOperationData = null;

const HELP_TEXT = `操作方法
PC:
- 移動: W, A, S, D / 矢印キー
- 上昇/下降: E, C
- オブジェクト掴む: 左クリックでドラッグ
- オブジェクトコピー: Shift + 左クリックでドラッグ
- オブジェクト削除: Ctrl + 左クリック

VR (Quest):
- 移動: 左スティック
- 視点回転/上下移動: 右スティック
- オブジェクト操作 (ライン): 右トリガーで掴む
- オブジェクト操作 (ハンド): 右トリガーで掴む
- コピー: Aボタン + トリガー
- 削除: Bボタン + トリガー
- モード切替: パネルの「ライン/ハンド」ボタン
`;

const CREDIT_TEXT = `VR Test Environment Ver 13.1

開発: setokem(GeminiPro2.5使用)

使用ライブラリ:
- A-Frame
- aframe-physics-system
- aframe-look-at-component
- aframe-troika-text
- Ammo.js

背景画像:
- GeminiPro2.5 で画像作成

Special Thanks!
`;


let backgroundIndex = 400;
window.addEventListener('keydown', function(event) {
if (event.key === 'Shift') { isShiftKeyPressed = true; }
if (event.key === 'Control') { isCtrlKeyPressed = true; }
});
window.addEventListener('keyup', function(event) {
if (event.key === 'Shift') { isShiftKeyPressed = false; }
if (event.key === 'Control') { isCtrlKeyPressed = false; }
});

const rightHand = document.getElementById('rightHand');
const rigEl = document.getElementById('rig');
const objectContainer = document.getElementById('object-container');
const createGravityBoxPC = document.getElementById('create-gravity-box-pc');
const createGravitySpherePC = document.getElementById('create-gravity-sphere-pc');
const createGridBoxPC = document.getElementById('create-grid-box-pc');
const createGridSpherePC = document.getElementById('create-grid-sphere-pc');

function handleStartReplayClick() {
if (!isGameStarted) {
startGame();
} else {
replayGame();
}
}
window.handleStartReplayClick = handleStartReplayClick;

function startGame() {
if (isGameStarted) return;

const startButton = document.getElementById('replay-button-pc');
if (startButton) {
startButton.classList.remove('pre-start');
}
const postStartPanel = document.getElementById('post-start-panel');
if (postStartPanel) {
postStartPanel.style.display = 'block';
}

isGameStarted = true;
console.log("Game started!");
document.getElementById('replay-button-pc').textContent = 'リセット';

const replayButtonVR = document.getElementById('replay-button-vr');
const replayTextVR = document.getElementById('replay-text-vr');

if (replayButtonVR) {
replayButtonVR.setAttribute('width', '0.07');
replayButtonVR.setAttribute('height', '0.03');
replayButtonVR.setAttribute('depth', '0.01');
replayButtonVR.setAttribute('material', 'color', '#C0392B');
replayButtonVR.setAttribute('position', { x: -0.15, y: 0.17, z: 0.01 });
}
if (replayTextVR) {
replayTextVR.setAttribute('troika-text', { value: 'リセット', fontSize: 0.012 });
replayTextVR.setAttribute('position', { x: -0.15, y: 0.17, z: 0.016 });
}

const leftHand = document.getElementById('leftHand');
if (leftHand) {
leftHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}

const rightHand = document.getElementById('rightHand');
if (rightHand) {
rightHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}
const handPointer = document.getElementById('hand-pointer-visual');
if (handPointer) {
handPointer.setAttribute('visible', true);
}
replayGame();
}

function replayGame() {
console.log("Replaying game...");
objectContainer.innerHTML = '';
window.createdObjectCounter = 0;
if (rigEl && rigEl.components['camera-relative-controls']) {
rigEl.setAttribute('camera-relative-controls', 'enabled', true);
}
rigEl.setAttribute('position', '0 1.6 2');
rigEl.setAttribute('rotation', '0 0 0');
}

function createBox(position, isGravity, color) {
const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
const box = document.createElement('a-box');
box.setAttribute('id', `box-${window.createdObjectCounter++}`);
box.classList.add('grabbable');
box.setAttribute('position', snappedPosition);
box.setAttribute('width', 0.1);
box.setAttribute('height', 0.1);
box.setAttribute('depth', 0.1);
box.setAttribute('color', color || selectedColor);

if (isGravity) {
box.setAttribute('ammo-body', 'type: dynamic; mass: 2; friction: 0.05; restitution: 0.9;');
box.dataset.physicsType = 'dynamic';
box.dataset.objectType = 'gravity-box';
} else {
box.setAttribute('ammo-body', 'type: static; friction: 0.05; restitution: 0.9;');
box.dataset.physicsType = 'static';
box.dataset.objectType = 'grid-box';
}
box.setAttribute('ammo-shape', 'type: box;');
objectContainer.appendChild(box);
}

function createSphere(position, isGravity, color) {
const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
const sphere = document.createElement('a-sphere');
sphere.setAttribute('id', `sphere-${window.createdObjectCounter++}`);
sphere.classList.add('grabbable');
sphere.setAttribute('position', snappedPosition);
sphere.setAttribute('radius', 0.045);
sphere.setAttribute('color', color || selectedColor);

if (isGravity) {
sphere.setAttribute('ammo-body', 'type: dynamic; mass: 2; angularDamping: 0.3; friction: 0.05; restitution: 0.9;');
sphere.dataset.physicsType = 'dynamic';
sphere.dataset.objectType = 'gravity-sphere';
} else {
sphere.setAttribute('ammo-body', 'type: static; friction: 0.05; restitution: 0.9;');
sphere.dataset.physicsType = 'static';
sphere.dataset.objectType = 'grid-sphere';
}
sphere.setAttribute('ammo-shape', 'type: sphere;');
objectContainer.appendChild(sphere);
}

function createAccelerator(position, accelType) {
const block = document.createElement('a-box');
block.setAttribute('id', `accel-${window.createdObjectCounter++}`);
block.classList.add('grabbable');

const size = 0.1;
block.setAttribute('width', size);
block.setAttribute('height', size);
block.setAttribute('depth', size);

let color, force;
switch (accelType) {
case 'up': color = '#e74c3c'; force = 5;
block.dataset.objectType = 'accelerator-up';
break;
case 'right': color = '#f1c40f'; force = 50;
block.dataset.objectType = 'accelerator-right';
break;
case 'left': color = '#e67e22'; force = 50;
block.dataset.objectType = 'accelerator-left';
break;
case 'straight': color = '#3498db'; force = 50;
block.dataset.objectType = 'accelerator-straight';
break;
case 'reverse': color = '#2ecc71'; force = 50;
block.dataset.objectType = 'accelerator-reverse';
break;
}

block.setAttribute('material', { color: color, opacity: 0.5 });

const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(size, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
block.setAttribute('position', snappedPosition);

block.dataset.physicsType = 'static';
block.setAttribute('accelerator', { type: accelType, force: force });

objectContainer.appendChild(block);
}

function alignSingleObject(obj) {
if (!obj) return;
const pos = obj.getAttribute('position');
let snapSize = (obj.tagName.toLowerCase() === 'a-box') ? obj.getAttribute('width') : 0.1;
const newPos = {
x: Math.round(pos.x / snapSize) * snapSize,
y: Math.round(pos.y / snapSize) * snapSize,
z: Math.round(pos.z / snapSize) * snapSize
};
obj.setAttribute('position', newPos);
obj.setAttribute('rotation', '0 0 0');
}
window.alignSingleObject = alignSingleObject;

let handModelVisual = null;
function toggleGrabMode() {
isLaserGrabMode = !isLaserGrabMode;
const rightHandEl = document.getElementById('rightHand');
const laserLine = rightHandEl.querySelector('[line]');
const handPointer = document.getElementById('hand-pointer-visual');
const modeTextEl = document.getElementById('grab-mode-text-vr');

if (!handModelVisual && rightHandEl) {
handModelVisual = rightHandEl.object3D.getObjectByProperty('type', 'SkinnedMesh');
}

if (isLaserGrabMode) {
rightHandEl.setAttribute('laser-controls', 'enabled', true);
rightHandEl.setAttribute('hand-grab', 'enabled', false);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ライン');
if (laserLine) laserLine.setAttribute('visible', true);
if (handPointer) handPointer.setAttribute('visible', true);
if (handModelVisual) handModelVisual.visible = false;
} else {
rightHandEl.setAttribute('laser-controls', 'enabled', false);
rightHandEl.setAttribute('hand-grab', 'enabled', true);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ハンド');
if (laserLine) laserLine.setAttribute('visible', false);
if (handPointer) handPointer.setAttribute('visible', false);
if (handModelVisual) handModelVisual.visible = true;
}
}

function selectColor(newColor) {
selectedColor = newColor;
createGravityBoxPC.style.backgroundColor = newColor;
createGravitySpherePC.style.backgroundColor = newColor;
createGridBoxPC.style.backgroundColor = newColor;
createGridSpherePC.style.backgroundColor = newColor;

const materialConfig = { shader: 'flat', color: newColor };
document.getElementById('create-gravity-box-vr').setAttribute('material', materialConfig);
document.getElementById('create-gravity-sphere-vr').setAttribute('material', materialConfig);
document.getElementById('create-grid-box-vr').setAttribute('material', materialConfig);
document.getElementById('create-grid-sphere-vr').setAttribute('material', materialConfig);
}

function saveScene() {
if (!isGameStarted) { return; }
const objectsToSave = Array.from(objectContainer.children).map(el => ({
position: el.getAttribute('position'),
rotation: el.getAttribute('rotation'),
objectType: el.dataset.objectType,
color: el.getAttribute('material')?.color,
physicsType: el.dataset.physicsType
}));
const sceneDataToSave = { objects: objectsToSave, background: backgroundIndex };
localStorage.setItem('aframeSceneData', JSON.stringify(sceneDataToSave));
console.log('Scene saved!');
}

function loadScene() {
if (!isGameStarted) { return; }
const savedDataString = localStorage.getItem('aframeSceneData');
if (!savedDataString) { return; }

const savedSceneData = JSON.parse(savedDataString);
const objectsToLoad = savedSceneData.objects;

objectContainer.innerHTML = '';
window.createdObjectCounter = 0;

if (objectsToLoad) {
for (const data of objectsToLoad) {
const isGravity = data.physicsType === 'dynamic';
if (data.objectType.includes('accelerator')) {
createAccelerator(data.position, data.objectType.split('-')[1]);
} else if (data.objectType.includes('box')) {
createBox(data.position, isGravity, data.color);
} else if (data.objectType.includes('sphere')) {
createSphere(data.position, isGravity, data.color);
}
const newEl = objectContainer.lastChild;
if (newEl && data.rotation) {
newEl.setAttribute('rotation', data.rotation);
}
}
}




const savedBackgroundIndex = savedSceneData.background;
const skyEl = document.getElementById('sky');
if (typeof savedBackgroundIndex !== 'undefined' && savedBackgroundIndex >= 0) {
// ロード時は、その番号に画像があるか(PNG優先)を確認してセットする
// 簡易的に findNextAvailableImage のロジックの一部を利用
const targetIndex = savedBackgroundIndex;
const pngUrl = `./pic/h${targetIndex + 1}.png`;
const jpgUrl = `./pic/h${targetIndex + 1}.jpg`;

const imgCheck = new Image();
imgCheck.onload = function() {
// PNGがあった
backgroundIndex = targetIndex;
if (skyEl) skyEl.setAttribute('src', pngUrl);
};
imgCheck.onerror = function() {
// PNGがダメならJPGを試す
const imgCheckJpg = new Image();
imgCheckJpg.onload = function() {
backgroundIndex = targetIndex;
if (skyEl) skyEl.setAttribute('src', jpgUrl);
};
// JPGもなければ、とりあえずインデックスだけ更新しておく(またはデフォルトに戻す)
imgCheckJpg.src = jpgUrl;
};
imgCheck.src = pngUrl;

} else {
backgroundIndex = 400;
if (skyEl) skyEl.setAttribute('src', '#skyTexture');
}
console.log('Scene loaded!');
}




function changeBackground() {
findNextAvailableImage(backgroundIndex + 1);
}

function toggleGroundVisibility() {
const groundEl = document.getElementById('ground');
if (groundEl) {
groundEl.setAttribute('visible', !groundEl.getAttribute('visible'));
}
}

function tidyUpScene() {
const children = Array.from(objectContainer.children);
const objectBlueprints = [];

children.forEach(el => {
if (el.object3D.position.y < 0) return;
let blueprint = { position: el.object3D.position.clone() };
if (el.hasAttribute('accelerator')) {
blueprint.type = 'accelerator';
blueprint.accelType = el.getAttribute('accelerator').type;
} else {
blueprint.isGravity = el.dataset.physicsType === 'dynamic';
blueprint.color = el.getAttribute('material').color;
blueprint.shape = el.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
}
objectBlueprints.push(blueprint);
});

objectContainer.innerHTML = '';
const occupiedCoordinates = new Set();

const placeObject = (bp) => {
const snapSize = 0.1;
let targetPosition = {
x: Math.round(bp.position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(bp.position.y / snapSize) * snapSize),
z: Math.round(bp.position.z / snapSize) * snapSize
};
while (true) {
const coordKey = `${targetPosition.x.toFixed(2)},${targetPosition.y.toFixed(2)},${targetPosition.z.toFixed(2)}`;
if (occupiedCoordinates.has(coordKey)) {
targetPosition.y += snapSize;
} else {
occupiedCoordinates.add(coordKey);
break;
}
}
if (bp.type === 'accelerator') createAccelerator(targetPosition, bp.accelType);
else if (bp.shape === 'box') createBox(targetPosition, bp.isGravity, bp.color);
else if (bp.shape === 'sphere') createSphere(targetPosition, bp.isGravity, bp.color);
};

objectBlueprints.filter(bp => bp.type === 'accelerator').forEach(placeObject);
objectBlueprints.filter(bp => bp.type !== 'accelerator' && !bp.isGravity).forEach(placeObject);
objectBlueprints.filter(bp => bp.type !== 'accelerator' && bp.isGravity).forEach(placeObject);
console.log('整理完了。');
}





function findNextAvailableImage(index) {
if (index >= 500) { index = 0; }

// PNGとJPGのパスを定義
const pngUrl = `./pic/h${index + 1}.png`;
const jpgUrl = `./pic/h${index + 1}.jpg`;

// まずPNGをチェックする
const imgPng = new Image();

imgPng.onload = function() {
// PNGが存在した場合
applyBackground(index, pngUrl);
};

imgPng.onerror = function() {
// PNGがエラー(存在しない)の場合、次はJPGをチェックする
const imgJpg = new Image();

imgJpg.onload = function() {
// JPGが存在した場合
applyBackground(index, jpgUrl);
};

imgJpg.onerror = function() {
// 両方存在しない場合、次の番号へ(再帰呼び出し)
findNextAvailableImage(index + 1);
};

imgJpg.src = jpgUrl;
};

imgPng.src = pngUrl;
}

// 画像切り替えの共通処理(コードの重複を防ぐため追加)
function applyBackground(index, url) {
backgroundIndex = index;
const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('src', url); }
}







function onMouseMove(event) {
if (!draggedObject) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
const camera = sceneEl.camera;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.at(dragDistance, worldPosition);
draggedObject.object3D.parent.worldToLocal(worldPosition);
draggedObject.object3D.position.copy(worldPosition);
}

function deleteObjectAtPosition(targetPosition) {
const children = Array.from(objectContainer.children);
const tolerance = 0.01;
const elToDelete = children.find(el => el.object3D.position.distanceTo(targetPosition) < tolerance);
if (elToDelete) {
elToDelete.parentNode.removeChild(elToDelete);
}
}

function onMouseUp() {
if (!draggedObject) return;

if (window.copyDragData && !window.copyDragData.isVr) {
const data = window.copyDragData;
alignSingleObject(draggedObject);
const finalPosition = draggedObject.object3D.position.clone();
const originalPosition = data.originalPosition;
const isCopyCancelled = finalPosition.distanceTo(originalPosition) < 0.1;

data.originalEl.parentNode.removeChild(data.originalEl);
draggedObject.parentNode.removeChild(draggedObject);
deleteObjectAtPosition(originalPosition);

if (data.isAccelerator) {
createAccelerator(originalPosition, data.accelType);
if (!isCopyCancelled) {
createAccelerator(finalPosition, data.accelType);
}
} else {
const creatorFn = data.shape === 'box' ? createBox : createSphere;
creatorFn(originalPosition, data.isGravity, data.color);
if (!isCopyCancelled) {
creatorFn(finalPosition, data.isGravity, data.color);
}
}
window.copyDragData = null;
}
else {
const physicsType = draggedObject.dataset.physicsType || 'dynamic';
if (physicsType === 'static' || draggedObject.hasAttribute('accelerator')) {
alignSingleObject(draggedObject);
}
if (draggedObject.hasAttribute('ammo-body')) {
draggedObject.setAttribute('ammo-body', 'type', physicsType);
}
}

draggedObject = null;
dragDistance = 0;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}

function showInfoPanelPC(contentType) {
const overlay = document.getElementById('info-overlay-pc');
const content = document.getElementById('info-content-pc');
if (overlay && content) {
content.textContent = (contentType === 'help') ? HELP_TEXT : CREDIT_TEXT;
overlay.style.display = 'flex';
}
}

function hideInfoPanelPC() {
const overlay = document.getElementById('info-overlay-pc');
if (overlay) {
overlay.style.display = 'none';
}
}

// ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆

function showHelpPanelVR() {
const panel = document.getElementById('info-panel-vr');
const textEl = document.getElementById('info-text-vr');
if (panel && textEl) {
panel.setAttribute('visible', true);
setTimeout(() => {
const currentTextConfig = textEl.getAttribute('troika-text');
textEl.setAttribute('troika-text', {...currentTextConfig,value: HELP_TEXT });
}, 0);
}
}
function showCreditPanelVR() {
const panel = document.getElementById('info-panel-vr');
const textEl = document.getElementById('info-text-vr');
if (panel && textEl) {
panel.setAttribute('visible', true);
setTimeout(() => {
const currentTextConfig = textEl.getAttribute('troika-text');
textEl.setAttribute('troika-text', {...currentTextConfig,value: CREDIT_TEXT });
}, 0);
}
}


function hideInfoPanelVR() {
const panel = document.getElementById('info-panel-vr');
if (panel) {
panel.setAttribute('visible', false);
}
}


function setupGame() {
document.getElementById('replay-button-pc').classList.add('pre-start');

const paletteContainerPC = document.getElementById('color-palette-pc');
const paletteContainerVR = document.getElementById('color-palette-vr');
const PALETTE_COLS = 5;
const PALETTE_ROWS = 3;

for (let i = 0; i < PALETTE_ROWS * PALETTE_COLS; i++) {
const hue = Math.floor((i / (PALETTE_ROWS * PALETTE_COLS)) * 360);
const color = `hsl(${hue}, 90%, 60%)`;

const swatchPC = document.createElement('div');
swatchPC.classList.add('color-swatch-pc');
swatchPC.style.backgroundColor = color;
swatchPC.addEventListener('click', () => selectColor(color));
paletteContainerPC.appendChild(swatchPC);

const swatchVR = document.createElement('a-box');
swatchVR.classList.add('clickable');
swatchVR.setAttribute('color', color);
swatchVR.setAttribute('width', '0.06');
swatchVR.setAttribute('height', '0.03');
swatchVR.setAttribute('depth', '0.01');
const x = (i % PALETTE_COLS - (PALETTE_COLS - 1) / 2) * 0.08;
const y = (Math.floor(i / PALETTE_COLS) - (PALETTE_ROWS - 1) / 2) * -0.04;
swatchVR.setAttribute('position', `${x} ${y} 0`);
swatchVR.dataset.color = color;
paletteContainerVR.appendChild(swatchVR);
}
selectColor(selectedColor);

sceneEl.addEventListener('mousedown', function(evt) {
if (sceneEl.is('vr-mode') || !isGameStarted) return;
const intersectedEl = evt.detail.intersectedEl;
if (intersectedEl && intersectedEl.classList.contains('grabbable')) {
if (isCtrlKeyPressed) {
intersectedEl.parentNode.removeChild(intersectedEl);
return;
}
if (isShiftKeyPressed) {
if (intersectedEl.hasAttribute('accelerator')) {
window.copyDragData = {
originalEl: intersectedEl,
originalPosition: intersectedEl.object3D.position.clone(),
isAccelerator: true,
accelType: intersectedEl.getAttribute('accelerator').type,
isVr: false
};
} else {
window.copyDragData = {
originalEl: intersectedEl,
originalPosition: intersectedEl.object3D.position.clone(),
isAccelerator: false,
isGravity: intersectedEl.dataset.physicsType === 'dynamic',
shape: intersectedEl.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere',
color: intersectedEl.getAttribute('material').color,
isVr: false
};
}
intersectedEl.removeAttribute('ammo-body');
intersectedEl.setAttribute('visible', false);

const dragClone = intersectedEl.cloneNode(true);
dragClone.setAttribute('position', intersectedEl.getAttribute('position'));
objectContainer.appendChild(dragClone);
draggedObject = dragClone;
} else {
draggedObject = intersectedEl;
if (draggedObject.hasAttribute('ammo-body')) {
draggedObject.setAttribute('ammo-body', 'type', 'kinematic');
}
}
const intersection = evt.detail.intersection;
if (!intersection) return;
dragDistance = intersection.distance;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
});

// PC用のボタンイベントリスナー
document.getElementById('save-button-pc').addEventListener('click', saveScene);
document.getElementById('load-button-pc').addEventListener('click', loadScene);
document.getElementById('tidy-up-pc').addEventListener('click', tidyUpScene);
document.getElementById('change-background-pc').addEventListener('click', changeBackground);
document.getElementById('help-button-pc').addEventListener('click', () => showInfoPanelPC('help'));
document.getElementById('credit-button-pc').addEventListener('click', () => showInfoPanelPC('credit'));
document.getElementById('info-close-button-pc').addEventListener('click', hideInfoPanelPC);

if (rightHand) {
rightHand.addEventListener('abuttondown', function() { isAButtonPressed = true; });
rightHand.addEventListener('abuttonup', function() { isAButtonPressed = false; });
rightHand.addEventListener('bbuttondown', function() { isBButtonPressed = true; });
rightHand.addEventListener('bbuttonup', function() { isBButtonPressed = false; });

rightHand.addEventListener('triggerdown', function () {
const raycasterComponent = rightHand.components.raycaster;
if (!raycasterComponent) return;
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length === 0) return;

const hitEl = intersectedEls[0];

// --- VRパネルのボタン処理 ---
if (hitEl.id === 'tidy-up-vr') { tidyUpScene(); return; }
if (hitEl.id === 'toggle-ground-vr') { toggleGroundVisibility(); return; }
if (isBButtonPressed && hitEl.classList.contains('grabbable')) { hitEl.parentNode.removeChild(hitEl); return; }
if (hitEl.id === 'change-background-vr') { changeBackground(); return; }
if (hitEl.dataset.color) { selectColor(hitEl.dataset.color); return; }
if (hitEl.id === 'replay-button-vr') { handleStartReplayClick(); return; }
if (hitEl.id === 'toggle-grab-mode-vr') { toggleGrabMode(); return; }
if (hitEl.id === 'save-button-vr') { saveScene(); return; }
if (hitEl.id === 'load-button-vr') { loadScene(); return; }
if (hitEl.id === 'help-button-vr') { showHelpPanelVR(); return; }
if (hitEl.id === 'credit-button-vr') { showCreditPanelVR(); return; }
if (hitEl.id === 'info-close-button-vr') { hideInfoPanelVR(); return; }

const createActions = {
'create-gravity-box-vr': () => createBox(rightHand.object3D.getWorldPosition(new THREE.Vector3()), true),
'create-gravity-sphere-vr': () => createSphere(rightHand.object3D.getWorldPosition(new THREE.Vector3()), true),
'create-grid-box-vr': () => createBox(rightHand.object3D.getWorldPosition(new THREE.Vector3()), false),
'create-grid-sphere-vr': () => createSphere(rightHand.object3D.getWorldPosition(new THREE.Vector3()), false),
'create-accel-up-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'up'),
'create-accel-right-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'right'),
'create-accel-left-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'left'),
'create-accel-straight-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'straight'),
'create-accel-reverse-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'reverse')
};
if (isGameStarted && createActions[hitEl.id]) {
createActions[hitEl.id]();
return;
}

if (isLaserGrabMode && isGameStarted && hitEl.classList.contains('grabbable') && !grabbedObject) {
if (isAButtonPressed) {
if (hitEl.hasAttribute('accelerator')) {
copyOperationData = {
isAccelerator: true,
originalPosition: hitEl.object3D.position.clone(),
accelType: hitEl.getAttribute('accelerator').type
};
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);
} else {
copyOperationData = {
isAccelerator: false,
originalPosition: hitEl.object3D.position.clone(),
isGravity: hitEl.dataset.physicsType === 'dynamic',
shape: hitEl.tagName.toLowerCase().includes('box') ? 'box' : 'sphere',
color: hitEl.getAttribute('material').color
};
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, false, copyOperationData.color);
}
copyOperationData.placeholderEl = objectContainer.lastChild;
}

grabbedObject = hitEl;
if (grabbedObject.hasAttribute('ammo-body')) {
grabbedObject.setAttribute('ammo-body', 'type', 'kinematic');
}
rightHand.object3D.attach(grabbedObject.object3D);
}
});

rightHand.addEventListener('triggerup', function () {
if (isLaserGrabMode && grabbedObject) {
objectContainer.object3D.attach(grabbedObject.object3D);

if (grabbedObject.hasAttribute('ammo-body')) {
const physicsType = grabbedObject.dataset.physicsType || 'dynamic';
grabbedObject.setAttribute('ammo-body', 'type', physicsType);
}
alignSingleObject(grabbedObject);

if (copyOperationData) {
const placeholder = copyOperationData.placeholderEl;
if (placeholder) {
placeholder.parentNode.removeChild(placeholder);
}
if (copyOperationData.isAccelerator) {
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);
} else {
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, copyOperationData.isGravity, copyOperationData.color);
}
copyOperationData = null;
}
grabbedObject = null;
}
});
}

const createObjectPC = (creatorFn, isGravityOrType) => {
if (isGameStarted) {
const camera = document.getElementById('camera');
const worldPos = new THREE.Vector3();
const direction = new THREE.Vector3();
camera.object3D.getWorldPosition(worldPos);
camera.object3D.getWorldDirection(direction);
worldPos.add(direction.multiplyScalar(-0.5));
creatorFn(worldPos, isGravityOrType);
}
};

createGravityBoxPC.addEventListener('click', () => createObjectPC(createBox, true));
createGravitySpherePC.addEventListener('click', () => createObjectPC(createSphere, true));
createGridBoxPC.addEventListener('click', () => createObjectPC(createBox, false));
createGridSpherePC.addEventListener('click', () => createObjectPC(createSphere, false));

document.getElementById('create-accel-up-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'up'));
document.getElementById('create-accel-right-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'right'));
document.getElementById('create-accel-left-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'left'));
document.getElementById('create-accel-straight-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'straight'));
document.getElementById('create-accel-reverse-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'reverse'));

const arButton = document.getElementById('ar-button');
if (arButton) {
arButton.addEventListener('click', async () => {
document.getElementById('hud-pc').style.display = 'none';
document.getElementById('xr-buttons').style.display = 'none';

const toggleGroundVR = document.getElementById('toggle-ground-vr');
const toggleGroundTextVR = document.getElementById('toggle-ground-text-vr');
if (toggleGroundVR) toggleGroundVR.setAttribute('visible', true);
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', true);

document.getElementById('change-background-vr').setAttribute('visible', false);
document.getElementById('change-background-text-vr').setAttribute('visible', false);

const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('visible', false); }
try {
await sceneEl.enterAR();
} catch (e) {
console.error("ARモードへの移行に失敗しました", e);
}
});
}
sceneEl.addEventListener('exit-vr', () => {
document.getElementById('hud-pc').style.display = 'block';
document.getElementById('xr-buttons').style.display = 'flex';

const toggleGroundVR = document.getElementById('toggle-ground-vr');
const toggleGroundTextVR = document.getElementById('toggle-ground-text-vr');
if (toggleGroundVR) toggleGroundVR.setAttribute('visible', false);
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', false);

document.getElementById('change-background-vr').setAttribute('visible', true);
document.getElementById('change-background-text-vr').setAttribute('visible', true);

document.getElementById('ground').setAttribute('visible', true);
document.getElementById('sky').setAttribute('visible', true);
});
}

if (sceneEl.hasLoaded) { setupGame(); } else { sceneEl.addEventListener('loaded', setupGame, {once: true}); }
});
</script>
</body>
</html>


使用変数

-------( Function )
) { isAButtonPressed = false; }); rightHand.addEventListener -------( Function )
) { isAButtonPressed = true; }); rightHand.addEventListener -------( Function )
) { isBButtonPressed = false; }); rightHand.addEventListener -------( Function )
) { isBButtonPressed = true; }); rightHand.addEventListener -------( Function )
) { this.isAButtonPressed = true; }, onAButtonUp: function -------( Function )
) { this.isBButtonPressed = true; }, onBButtonUp: function -------( Function )
accelType
alignSingleObject -------( Function )
alignSingleObject
angle
applyBackground -------( Function )
arButton
ateGravitySpherePC
background
backgroundColor
backgroundIndex
bbox
block
blueprint
body
box
bp
camera
cameraDirection
cameraEl
cameraObject
cameraRight
ccupiedCoordinates
changeBackground -------( Function )
children
class
closestDistSq
closestEl
collidingEls
color
content
contentType
controls
coordKey
copyDragData
copyEl
copyOperationData
createAccelerator -------( Function )
createActions
createBox -------( Function )
createGravityBoxPC
createGridBoxPC
createGridSpherePC
createObjectPC
createSphere -------( Function )
creatorFn
CREDIT_TEXT
currentTextConfig
currentVelocity
cursor
data
deleteObjectAtPosition -------( Function )
deltaPosition
depth
desiredVelocity
direction
display
distSq
dragClone
dragDistance
draggedObject
dt
dynamicEls
eatedObjectCounter
eftThumbstickInput
el
elToDelete
eraWorldQuaternion
euler
event) { if -------( Function )
evt) { if -------( Function )
ffectiveLerpFactor
finalPosition
findNextAvailableImage -------( Function )
force
forceVec
geometry
ghtThumbstickInput
grabbableEls
grabbedEl
grabbedObject
groundEl
handleStartReplayClick -------( Function )
handModelVisual
handPointer
handPos
height
HELP_TEXT
hideInfoPanelPC -------( Function )
hideInfoPanelVR -------( Function )
hitEl
hue
i
id
imgCheck
imgCheckJpg
imgJpg
imgPng
impulse
index
innerHTML
intersectedEl
intersectedEls
intersection
isAButtonPressed
isBButtonPressed
isColliding
isCopyCancelled
isCtrlKeyPressed
isGameStarted
isGravity
isGravityEnabled
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
jpgUrl
key
keys
laserLine
leftHand
length
lerpFactor
leStartReplayClick
light
loadScene -------( Function )
material
materialConfig
modeTextEl
mouse
moveDirection
newEl
newPos
newY
objectBlueprints
objectContainer
objectsToLoad
objectsToSave
objectType
objPos
onAButtonDown
onAButtonUp
onBButtonDown
onBButtonUp
onclick
onerror
onKeyDown
onKeyUp
onload
onMouseMove -------( Function )
onMouseUp -------( Function )
onTriggerDown
onTriggerUp
opacity
originalMaterial
originalPosition
originalType
otherBbox
otherEl) { if -------( Function )
otherEl
overlay
paletteContainerPC
paletteContainerVR
PALETTE_COLS
PALETTE_ROWS
panel
physics
physicsType
placeholder
placeholderEl
placeObject
pngUrl
pos
position
postStartPanel
radius
raycaster
raycasterComponent
renderer
replayButtonVR
replayGame -------( Function )
replayTextVR
reverseDir
reverseForce
rigEl
rightHand
rightHandEl
riginalAccelerator
rotation
rotationDegrees
savedDataString
savedSceneData
saveScene -------( Function )
sceneDataToSave
sceneEl
selectColor -------( Function )
selectedColor
setupGame -------( Function )
shape
showCreditPanelVR -------( Function )
showHelpPanelVR -------( Function )
showInfoPanelPC -------( Function )
side
size
skyEl
snappedPosition
snapSize
sphere
src
startButton
startGame -------( Function )
straightDir
straightForce
style
swatchPC
swatchVR
targetIndex
targetPosition
text
textContent
textEl
tidyUpScene -------( Function )
toggleGrabMode -------( Function )
toggleGroundTextVR
toggleGroundVisibility -------( Function )
toggleGroundVR
tolerance
travelDir
type
ui
vedBackgroundIndex
velocity
verticalMovement
visible
vrCopyGrabData
webxr
width
worldPos
worldPosition
worldQuat
x
y
ZERO_VECTOR