junkerstock
 vrz1331 

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>VRテスト環境 Ver13.3 背景調整</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="./pic2/h308.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!
`;


const backgroundImages = [];
for (let i = 1; i <= 500; i++) {
const formattedNumber = String(i).padStart(3, '0'); // 例: 1 -> "001"
backgroundImages.push(`./pic2/h${formattedNumber}.png`); // ディレクトリも変更
}

let backgroundIndex = 308;
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) {
backgroundIndex = savedBackgroundIndex;
const formattedNumber = String(backgroundIndex + 1).padStart(3, '0'); // 3桁ゼロ埋め
if (skyEl) skyEl.setAttribute('src', `./pic2/h${formattedNumber}.png`); // ディレクトリも変更
} else {
backgroundIndex = 125;
if (skyEl) skyEl.setAttribute('src', '#skyTexture');
}
console.log('Scene loaded!');
}

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



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


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; }
const img = new Image();
const formattedNumber = String(index + 1).padStart(3, '0'); // 3桁ゼロ埋め
const imageUrl = `./pic2/h${formattedNumber}.png`; // ディレクトリも変更
img.onload = function() {
backgroundIndex = index;
const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('src', imageUrl); }
};
img.onerror = function() {
findNextAvailableImage(index + 1);
};
img.src = imageUrl;
}

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) {
// 正解: ARに入る時は「表示」し「クリック可能」にする
                    toggleGroundVR.setAttribute('visible', true);
                    toggleGroundVR.classList.add('clickable');
                }
                if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', true);
                
                const changeBackgroundVR = document.getElementById('change-background-vr');
                const changeBackgroundTextVR = document.getElementById('change-background-text-vr');
                if (changeBackgroundVR) {
// 正解: 通常のVR用ボタンは「非表示」にし「クリック不可」にする
                    changeBackgroundVR.setAttribute('visible', false);
                    changeBackgroundVR.classList.remove('clickable');
                }
                if (changeBackgroundTextVR) changeBackgroundTextVR.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);
toggleGroundVR.classList.remove('clickable'); // クリック不可に戻す
}
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', false);

const changeBackgroundVR = document.getElementById('change-background-vr');
const changeBackgroundTextVR = document.getElementById('change-background-text-vr');
if (changeBackgroundVR) {
changeBackgroundVR.setAttribute('visible', true);
changeBackgroundVR.classList.add('clickable'); // クリック可能に戻す
}
if (changeBackgroundTextVR) changeBackgroundTextVR.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
arButton
ateGravitySpherePC
background
backgroundColor
backgroundImages
backgroundIndex
bbox
block
blueprint
body
box
bp
camera
cameraDirection
cameraEl
cameraObject
cameraRight
ccupiedCoordinates
changeBackground -------( Function )
changeBackgroundVR
charset
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
currentVisibility
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
formattedNumber
geBackgroundTextVR
geometry
ghtThumbstickInput
grabbableEls
grabbedEl
grabbedObject
groundEl
handleStartReplayClick -------( Function )
handModelVisual
handPointer
handPos
height
HELP_TEXT
hideInfoPanelPC -------( Function )
hideInfoPanelVR -------( Function )
hitEl
hue
i
id
imageUrl
img
impulse
index
innerHTML
intersectedEl
intersectedEls
intersection
isAButtonPressed
isBButtonPressed
isColliding
isCopyCancelled
isCtrlKeyPressed
isGameStarted
isGravity
isGravityEnabled
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
key
keys
lang
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
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
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