<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>アゲハ蝶の一生 - Real 3D Adventure</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Three.jsの読み込み -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
user-select: none;
touch-action: none;
}
#canvas-container {
position: absolute;
top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 1;
}
#ui-layer {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
}
.pointer-events-auto { pointer-events: auto; }
@keyframes pop {
0% { transform: scale(0.8); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.animate-pop { animation: pop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }
.glass-panel { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.4); }
.dpad-btn { transition: all 0.1s; }
.dpad-btn:active, .dpad-btn.active-key { background-color: rgba(255, 255, 255, 0.4); transform: scale(0.9); }
#flash-screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-color: white; z-index: 9999; pointer-events: none;
opacity: 0; transition: opacity 1s; display: none;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="flash-screen"></div>
<div id="ui-layer" class="p-4 sm:p-8">
<div id="stats-bar" class="w-full max-w-3xl flex justify-center mt-6 text-2xl font-bold text-white drop-shadow-lg hidden">
<div id="score-display" class="glass-panel px-8 py-3 rounded-2xl text-yellow-300 tracking-widest">🌸 0 / 20</div>
</div>
<div id="progress-container" class="w-full max-w-md mt-8 hidden">
<div class="h-4 w-full bg-black/50 rounded-full overflow-hidden border border-white/50 shadow-inner">
<div id="progress-bar" class="h-full bg-gradient-to-r from-green-400 to-emerald-600 w-0 transition-all duration-300 relative">
<div class="absolute inset-0 bg-white/20 w-full animate-pulse"></div>
</div>
</div>
</div>
<!-- 文字をなくし、再生アイコンのみのスタートボタン -->
<button id="start-btn" class="pointer-events-auto bg-gradient-to-b from-green-500 to-green-700 hover:from-green-400 hover:to-green-600 text-white py-6 px-10 rounded-full shadow-[0_10px_25px_rgba(0,0,0,0.5)] transform transition hover:scale-105 active:scale-95 text-4xl mt-auto mb-20 animate-pop border-2 border-green-300">
▶
</button>
<!-- 糸を吐く攻撃ボタン(アイコンのみ) -->
<button id="attack-btn" class="pointer-events-auto absolute bottom-32 right-8 sm:bottom-40 sm:right-12 w-24 h-24 bg-white/30 backdrop-blur-md rounded-full shadow-[0_0_20px_rgba(255,255,255,0.5)] text-white hidden z-50 border-2 border-white/80 flex items-center justify-center transition-transform active:scale-90 animate-pop">
<span class="text-5xl drop-shadow-lg">🕸️</span>
</button>
<!-- 蛹になるボタン(アイコンのみ) -->
<button id="action-btn" class="pointer-events-auto absolute bottom-32 right-8 sm:bottom-40 sm:right-12 w-24 h-24 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full shadow-[0_0_20px_rgba(255,165,0,0.8)] text-white hidden z-50 border-4 border-white/80 flex items-center justify-center transition-transform active:scale-90">
<span class="text-5xl drop-shadow-lg">🧶</span>
</button>
<!-- クリアボタン(アイコンのみ) -->
<button id="finish-btn" class="pointer-events-auto absolute bottom-32 right-8 sm:bottom-40 sm:right-12 w-24 h-24 bg-gradient-to-br from-pink-400 to-purple-500 rounded-full shadow-[0_0_20px_rgba(255,105,180,0.8)] text-white hidden z-50 border-4 border-white/80 flex items-center justify-center transition-transform active:scale-90 animate-pulse">
<span class="text-5xl drop-shadow-lg">✨</span>
</button>
<!-- 文字なしのシンプルな十字キー -->
<div id="dpad-container" class="absolute bottom-8 left-8 sm:bottom-12 sm:left-12 w-48 h-48 hidden z-50 pointer-events-auto">
<div class="relative w-full h-full">
<button id="btn-up" class="dpad-btn absolute top-0 left-16 w-16 h-16 bg-white/20 rounded-xl backdrop-blur-sm border border-white/40 shadow-lg"></button>
<button id="btn-down" class="dpad-btn absolute bottom-0 left-16 w-16 h-16 bg-white/20 rounded-xl backdrop-blur-sm border border-white/40 shadow-lg"></button>
<button id="btn-left" class="dpad-btn absolute top-16 left-0 w-16 h-16 bg-white/20 rounded-xl backdrop-blur-sm border border-white/40 shadow-lg"></button>
<button id="btn-right" class="dpad-btn absolute top-16 right-0 w-16 h-16 bg-white/20 rounded-xl backdrop-blur-sm border border-white/40 shadow-lg"></button>
</div>
</div>
</div>
<script>
const gameState = {
phase: 0,
score: 0,
progress: 0,
targetProgress: 5,
pointer: new THREE.Vector2(),
flySpeed: 0,
isReadyToPupate: false,
hasStormed: false,
isStorming: false,
stormIntensity: 0, // 0.0 ~ 1.0 で嵐の激しさを管理
waterWall: null, // 押し寄せる鉄砲水のオブジェクト
startZ: 0,
goalZ: -800,
silks: [],
pupateTimer: 0,
hasPupated: false
};
const inputState = { up: false, down: false, left: false, right: false };
// --- 音声生成(Web Audio API) ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let windOsc = null;
let windGain = null;
function playStormSound() {
if (audioCtx.state === 'suspended') audioCtx.resume();
// 風の音(ノイズ)
const bufferSize = audioCtx.sampleRate * 2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
windOsc = audioCtx.createBufferSource();
windOsc.buffer = buffer;
windOsc.loop = true;
// フィルターで「ゴー」という低い音にする
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 100; // 初期はかなり低い
windGain = audioCtx.createGain();
windGain.gain.value = 0; // 初期は無音
windOsc.connect(filter);
filter.connect(windGain);
windGain.connect(audioCtx.destination);
windOsc.start();
// 嵐の激しさに合わせて音量と周波数を上げるアニメーションはメインループで行う
gameState.windFilter = filter;
}
function stopStormSound() {
if (windGain) {
windGain.gain.setTargetAtTime(0, audioCtx.currentTime, 1.0);
setTimeout(() => { if(windOsc) windOsc.stop(); }, 2000);
}
}
const uiLayer = document.getElementById('ui-layer');
const startBtn = document.getElementById('start-btn');
const actionBtn = document.getElementById('action-btn');
const attackBtn = document.getElementById('attack-btn');
const finishBtn = document.getElementById('finish-btn');
const statsBar = document.getElementById('stats-bar');
const scoreDisplay = document.getElementById('score-display');
const progressBar = document.getElementById('progress-bar');
const progressContainer = document.getElementById('progress-container');
const dpadContainer = document.getElementById('dpad-container');
const flashScreen = document.getElementById('flash-screen');
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEFA, 0.003);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
container.appendChild(renderer.domElement);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9);
hemiLight.position.set(0, 200, 0);
scene.add(hemiLight);
const dirLight = new THREE.DirectionalLight(0xffffee, 1.5);
dirLight.position.set(150, 200, 100);
dirLight.castShadow = true;
dirLight.shadow.camera.top = 150;
dirLight.shadow.camera.bottom = -150;
dirLight.shadow.camera.left = -150;
dirLight.shadow.camera.right = 150;
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 1000;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
let gameObjects = [];
let playerObj = null;
let environmentGroup = new THREE.Group();
let particleSystem = null;
let npcButterflies = [];
let enemies = [];
scene.add(environmentGroup);
const raycaster = new THREE.Raycaster();
const clock = new THREE.Clock();
function setupInput() {
const bindBtn = (id, key) => {
const btn = document.getElementById(id);
const press = (e) => { e.preventDefault(); e.stopPropagation(); inputState[key] = true; btn.classList.add('active-key'); };
const release = (e) => { e.preventDefault(); e.stopPropagation(); inputState[key] = false; btn.classList.remove('active-key'); };
btn.addEventListener('pointerdown', press);
btn.addEventListener('pointerup', release);
btn.addEventListener('pointerleave', release);
};
bindBtn('btn-up', 'up'); bindBtn('btn-down', 'down');
bindBtn('btn-left', 'left'); bindBtn('btn-right', 'right');
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { inputState.up = true; }
if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') { inputState.down = true; }
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { inputState.left = true; }
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { inputState.right = true; }
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { inputState.up = false; }
if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') { inputState.down = false; }
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { inputState.left = false; }
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { inputState.right = false; }
});
let isDragging = false;
window.addEventListener('pointerdown', (e) => {
if(e.target.tagName === 'BUTTON' || e.target.tagName === 'SPAN') return;
if(audioCtx.state === 'suspended') audioCtx.resume(); // オーディオコンテキストの初期化
isDragging = true;
gameState.pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
gameState.pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;
handleTapInteraction();
});
window.addEventListener('pointermove', (e) => {
if (!isDragging) return;
gameState.pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
gameState.pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('pointerup', () => { isDragging = false; gameState.pointer.y = 0; });
actionBtn.addEventListener('click', () => {
if (gameState.phase === 2.2 && gameState.isReadyToPupate) {
actionBtn.classList.add('hidden');
dpadContainer.classList.add('hidden');
attackBtn.classList.add('hidden');
startPupating();
}
});
finishBtn.addEventListener('click', () => {
finishBtn.classList.add('hidden');
showResult();
});
attackBtn.addEventListener('click', () => {
if ((gameState.phase === 2 || gameState.phase === 2.1 || gameState.phase === 2.2) && !gameState.isStorming && playerObj) {
const netGroup = new THREE.Group();
const netMat = new THREE.MeshBasicMaterial({color: 0xffffff, transparent: true, opacity: 0.8, wireframe: true});
const netGeo = new THREE.SphereGeometry(0.6, 8, 8);
const net = new THREE.Mesh(netGeo, netMat);
for(let i=0; i<6; i++){
const lGeo = new THREE.CylinderGeometry(0.01, 0.01, 1.2);
const l = new THREE.Mesh(lGeo, netMat);
l.rotation.x = Math.random() * Math.PI;
l.rotation.y = Math.random() * Math.PI;
netGroup.add(l);
}
netGroup.add(net);
let fw = new THREE.Vector3(0, 0, -1).applyQuaternion(playerObj.quaternion).normalize();
netGroup.position.copy(playerObj.position).add(new THREE.Vector3(0, 0.5, 0)).add(fw.clone().multiplyScalar(1.5));
netGroup.quaternion.copy(playerObj.quaternion);
scene.add(netGroup);
gameState.silks.push({mesh: netGroup, velocity: fw.multiplyScalar(30), life: 1.5, scaleSpeed: 4.0});
}
});
}
setupInput();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function createSkyTexture(type = 'forest') {
const canvas = document.createElement('canvas');
canvas.width = 2; canvas.height = 512;
const ctx = canvas.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 0, 512);
if (type === 'forest') {
grad.addColorStop(0, '#4aa8ff'); grad.addColorStop(0.5, '#87CEFA'); grad.addColorStop(1, '#e0f7fa');
} else if (type === 'storm') {
grad.addColorStop(0, '#222233'); grad.addColorStop(0.5, '#333344'); grad.addColorStop(1, '#444455');
} else if (type === 'wetland') {
grad.addColorStop(0, '#445544'); grad.addColorStop(0.5, '#667766'); grad.addColorStop(1, '#99aa99');
}
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 2, 512);
return new THREE.CanvasTexture(canvas);
}
scene.background = createSkyTexture();
function createWingTexture(isNPC = false) {
const canvas = document.createElement('canvas');
canvas.width = 1024; canvas.height = 1024;
const ctx = canvas.getContext('2d');
ctx.fillStyle = isNPC ? '#ffffff' : '#fceb3b';
ctx.fillRect(0, 0, 1024, 1024);
ctx.lineWidth = 100;
ctx.strokeStyle = '#111111';
ctx.strokeRect(0, 0, 1024, 1024);
ctx.lineCap = 'round';
for(let i = 0; i < 12; i++) {
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(0, 512);
let angle = -Math.PI/2.5 + (i / 11) * Math.PI*0.8;
let endX = 1024 * Math.cos(angle) * 1.5;
let endY = 512 + 1024 * Math.sin(angle) * 1.5;
ctx.lineTo(endX, endY);
ctx.stroke();
}
ctx.fillStyle = '#111111';
ctx.fillRect(0, 750, 1024, 274);
ctx.fillStyle = '#e53935';
ctx.beginPath(); ctx.arc(800, 120, 45, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#1e88e5';
for(let i=0; i<5; i++) {
ctx.beginPath(); ctx.arc(780 - i*70, 250 + i*30, 25, 0, Math.PI*2); ctx.fill();
}
const tex = new THREE.CanvasTexture(canvas);
tex.encoding = THREE.sRGBEncoding;
return tex;
}
function createButterflyModel(colorHex = 0xffffff, isNPC = false) {
const group = new THREE.Group();
const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 12), new THREE.MeshStandardMaterial({ color: 0x111111 }));
head.position.z = 0.8;
group.add(head);
const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.25, 16, 16), new THREE.MeshStandardMaterial({ color: 0x222222 }));
thorax.scale.set(1, 1, 1.5);
group.add(thorax);
const abdomen = new THREE.Mesh(new THREE.SphereGeometry(0.15, 12, 12), new THREE.MeshStandardMaterial({ color: 0x333333 }));
abdomen.scale.set(1, 1, 5);
abdomen.position.z = -0.7;
group.add(abdomen);
const antGeo = new THREE.CylinderGeometry(0.01, 0.01, 0.8);
const lAnt = new THREE.Mesh(antGeo, head.material);
lAnt.position.set(0.1, 0.1, 1.1); lAnt.rotation.x = Math.PI/2.5; lAnt.rotation.z = 0.2;
group.add(lAnt);
const rAnt = lAnt.clone();
rAnt.position.x = -0.1; rAnt.rotation.z = -0.2;
group.add(rAnt);
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.bezierCurveTo(0.5, 3, 3, 5, 6, 4);
wingShape.bezierCurveTo(5.5, 2, 4.5, 1, 3.5, 0.5);
wingShape.bezierCurveTo(4.5, -2, 4, -4, 4.2, -6);
wingShape.lineTo(3.5, -6);
wingShape.bezierCurveTo(3, -4, 1.5, -5, 0, -2);
wingShape.lineTo(0, 0);
const wingGeo = new THREE.ShapeGeometry(wingShape);
wingGeo.rotateX(Math.PI / 2);
wingGeo.translate(0.4, 0, 0);
const wingMat = new THREE.MeshStandardMaterial({
map: createWingTexture(isNPC),
side: THREE.DoubleSide,
roughness: 0.6,
alphaTest: 0.5,
color: colorHex
});
const rWing = new THREE.Mesh(wingGeo, wingMat);
group.add(rWing);
const lWingGeo = wingGeo.clone();
const posAttr = lWingGeo.attributes.position;
for(let i=0; i<posAttr.count; i++) {
posAttr.setX(i, -posAttr.getX(i));
}
lWingGeo.computeVertexNormals();
const lWing = new THREE.Mesh(lWingGeo, wingMat);
group.add(lWing);
group.userData = { lWing, rWing, lAnt, rAnt, baseRotX: 0, baseRotY: 0, baseRotZ: 0 };
return group;
}
// --- 敵キャラクター群 ---
function createAnt() {
const ant = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({color: 0x111111, roughness: 0.5, clearcoat: 0.8});
const head = new THREE.Mesh(new THREE.SphereGeometry(0.6, 16, 16), mat); head.position.set(0, 0.6, 1.2);
const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), mat); thorax.position.set(0, 0.5, 0);
const abdomen = new THREE.Mesh(new THREE.SphereGeometry(0.8, 16, 16), mat); abdomen.position.set(0, 0.8, -1.2);
ant.add(head, thorax, abdomen);
ant.userData = { legs: [] };
const legMat = new THREE.MeshStandardMaterial({color: 0x111111});
for(let i=0; i<3; i++) {
let zPos = 0.5 - i * 0.5;
for(let side of [-1, 1]) {
const leg = new THREE.Group();
const upperLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.8), legMat);
upperLeg.position.set(side * 0.4, 0.4, 0); upperLeg.rotation.z = side * -Math.PI/4;
const lowerLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.01, 0.8), legMat);
lowerLeg.position.set(side * 0.8, 0, 0); lowerLeg.rotation.z = side * Math.PI/8;
leg.add(upperLeg, lowerLeg); leg.position.set(0, 0, zPos);
ant.add(leg);
ant.userData.legs.push({ mesh: leg, side: side, offset: i });
}
}
return ant;
}
function createAntSwarm() {
const swarm = new THREE.Group();
swarm.userData = { type: 'antSwarm', seed: Math.random() * 100, isEnemy: true, childrenData: [], stunTimer: 0 };
const numAnts = 5 + Math.floor(Math.random() * 5);
for(let i=0; i<numAnts; i++) {
let ant = createAnt();
let offsetX = (Math.random() - 0.5) * 8;
let offsetZ = (Math.random() - 0.5) * 8;
ant.position.set(offsetX, 0, offsetZ);
ant.rotation.y = Math.random() * Math.PI * 2;
swarm.add(ant);
swarm.userData.childrenData.push({ speed: 0.8 + Math.random(), phase: Math.random() * Math.PI * 2 });
}
return swarm;
}
function createSpider() {
const spider = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({color: 0x1a0a0a, roughness: 0.8});
const body = new THREE.Mesh(new THREE.SphereGeometry(1.0, 16, 16), mat);
const head = new THREE.Mesh(new THREE.SphereGeometry(0.6, 16, 16), mat); head.position.z = 1.0;
spider.add(body, head);
spider.userData = { type: 'spider', seed: Math.random() * 100, isEnemy: true, stunTimer: 0, legs: [] };
for(let i=0; i<4; i++) {
let zPos = 0.6 - i * 0.4;
for(let side of [-1, 1]) {
const leg = new THREE.Group();
const upper = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 1.5), mat);
upper.position.set(side * 0.7, 0.5, 0); upper.rotation.z = side * -Math.PI/3;
const lower = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.01, 2.0), mat);
lower.position.set(side * 1.8, -0.2, 0); lower.rotation.z = side * Math.PI/6;
leg.add(upper, lower); leg.position.set(0, 0, zPos);
spider.add(leg);
spider.userData.legs.push({mesh: leg, offset: i, side: side});
}
}
const silk = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 40), new THREE.MeshBasicMaterial({color: 0xffffff, transparent: true, opacity: 0.5}));
silk.position.y = 20; spider.add(silk);
return spider;
}
function createPitcherPlant() {
const plant = new THREE.Group();
const matOut = new THREE.MeshStandardMaterial({color: 0x228B22, side: THREE.DoubleSide});
const matIn = new THREE.MeshStandardMaterial({color: 0xcc2222, side: THREE.DoubleSide});
const jaw1 = new THREE.Mesh(new THREE.SphereGeometry(2, 16, 16, 0, Math.PI), matOut);
jaw1.rotation.x = Math.PI / 4;
const jawIn1 = new THREE.Mesh(new THREE.SphereGeometry(1.9, 16, 16, 0, Math.PI), matIn);
jawIn1.rotation.x = Math.PI / 4;
const jaw2 = new THREE.Mesh(new THREE.SphereGeometry(2, 16, 16, 0, Math.PI), matOut);
jaw2.rotation.x = -Math.PI / 4; jaw2.rotation.z = Math.PI;
const jawIn2 = new THREE.Mesh(new THREE.SphereGeometry(1.9, 16, 16, 0, Math.PI), matIn);
jawIn2.rotation.x = -Math.PI / 4; jawIn2.rotation.z = Math.PI;
plant.add(jaw1, jawIn1, jaw2, jawIn2);
plant.position.y = 2;
const wrapper = new THREE.Group();
wrapper.add(plant);
wrapper.userData = { type: 'plant', seed: Math.random() * 100, isEnemy: true, stunTimer: 0 };
return wrapper;
}
function updateProgress() {
if (gameState.phase === 2.1) {
let totalDist = Math.abs(gameState.startZ - gameState.goalZ);
let currentDist = gameState.startZ - playerObj.position.z;
let p = Math.max(0, Math.min(100, (currentDist / totalDist) * 100));
progressBar.style.width = p + '%';
} else {
const percentage = Math.min((gameState.progress / gameState.targetProgress) * 100, 100);
progressBar.style.width = `${percentage}%`;
}
}
function clearScene() {
if (camera.parent) camera.parent.remove(camera);
gameObjects.forEach(obj => { if (obj.mesh) scene.remove(obj.mesh); });
gameObjects = [];
npcButterflies.forEach(npc => scene.remove(npc));
npcButterflies = [];
enemies.forEach(e => scene.remove(e));
enemies = [];
gameState.silks.forEach(s => scene.remove(s.mesh));
gameState.silks = [];
if(gameState.waterWall) { scene.remove(gameState.waterWall); gameState.waterWall = null; }
while(environmentGroup.children.length > 0) environmentGroup.remove(environmentGroup.children[0]);
if(playerObj) { scene.remove(playerObj); playerObj = null; }
if(particleSystem) { scene.remove(particleSystem); particleSystem = null; }
}
function getTerrainHeight(x, z) {
let y = Math.sin(x * 0.02) * Math.cos(z * 0.02) * 15;
y += Math.sin(x * 0.05 + z * 0.03) * 5;
y += Math.sin(x * 0.005) * Math.sin(z * 0.005) * 40;
return y;
}
function handleTapInteraction() {
if (!playerObj) return;
if (gameState.phase === 1) {
raycaster.setFromCamera(gameState.pointer, camera);
const intersects = raycaster.intersectObject(playerObj, true);
if (intersects.length > 0) {
gameState.progress++; updateProgress();
playerObj.userData.shakeTime = 0.2;
if (gameState.progress >= gameState.targetProgress) setTimeout(startPhase2, 500);
}
} else if (gameState.phase === 3) {
gameState.progress++; updateProgress();
playerObj.userData.shakeTime = 0.2;
if (playerObj.children) {
playerObj.children.forEach(child => {
if(child.userData && child.userData.isSegment) {
child.children.forEach(mesh => {
if(mesh.material && mesh.material.color) {
mesh.material.color.lerp(new THREE.Color(0xb8860b), 0.05);
}
});
}
});
}
if (gameState.progress >= gameState.targetProgress) setTimeout(startPhase4, 500);
}
}
function createDustParticles() {
if(particleSystem) scene.remove(particleSystem);
const particleGeo = new THREE.BufferGeometry();
const particleCount = 1500;
const posArray = new Float32Array(particleCount * 3);
for(let i=0; i<particleCount*3; i++) posArray[i] = (Math.random() - 0.5) * 300;
particleGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const particleMat = new THREE.PointsMaterial({
size: 0.6, color: 0xffffff, transparent: true, opacity: 0.6,
blending: THREE.AdditiveBlending, depthWrite: false
});
particleSystem = new THREE.Points(particleGeo, particleMat);
scene.add(particleSystem);
}
function buildEnvironment(type) {
while(environmentGroup.children.length > 0) environmentGroup.remove(environmentGroup.children[0]);
gameObjects.forEach(obj => { if (obj.mesh) scene.remove(obj.mesh); });
gameObjects = [];
enemies.forEach(e => scene.remove(e));
enemies = [];
const isWetland = type === 'wetland';
const size = isWetland ? 2000 : 250;
const segs = isWetland ? 256 : 128;
const groundGeo = new THREE.PlaneGeometry(size, size, segs, segs);
const pos = groundGeo.attributes.position;
for(let i=0; i<pos.count; i++) {
const px = pos.getX(i); const pz = -pos.getY(i);
pos.setZ(i, getTerrainHeight(px, pz));
}
groundGeo.computeVertexNormals();
let groundColor = isWetland ? 0x2a3322 : 0x3cb371;
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({ color: groundColor, roughness: 0.9, side: THREE.DoubleSide }));
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true;
ground.userData.isGround = true;
environmentGroup.add(ground);
if (isWetland) {
const waterGeo = new THREE.PlaneGeometry(size, size);
const waterMat = new THREE.MeshStandardMaterial({ color: 0x334455, transparent: true, opacity: 0.8, roughness: 0.1 });
const water = new THREE.Mesh(waterGeo, waterMat);
water.rotation.x = -Math.PI / 2;
water.position.y = -5;
environmentGroup.add(water);
const grassGeo = new THREE.CylinderGeometry(0.2, 2.5, 60, 8);
const grassMat = new THREE.MeshStandardMaterial({color: 0x2a7a2a, roughness: 0.8});
for(let i=0; i<800; i++) {
const grass = new THREE.Mesh(grassGeo, grassMat);
let px = (Math.random() - 0.5) * size; let pz = (Math.random() - 0.5) * size;
if(Math.abs(px) < 20 && Math.abs(pz - gameState.startZ) < 30) continue;
grass.position.set(px, getTerrainHeight(px, pz) + 28, pz);
grass.rotation.x = (Math.random() - 0.5) * 0.3;
grass.rotation.z = (Math.random() - 0.5) * 0.3;
grass.castShadow = true;
grass.userData.isGrass = true;
environmentGroup.add(grass);
}
for(let i=0; i<25; i++) {
let antSwarm = createAntSwarm();
antSwarm.position.set((Math.random()-0.5)*size, 0, (Math.random()-0.5)*size);
scene.add(antSwarm); enemies.push(antSwarm);
let spider = createSpider();
spider.position.set((Math.random()-0.5)*size, 20, (Math.random()-0.5)*size);
scene.add(spider); enemies.push(spider);
let plant = createPitcherPlant();
plant.position.set((Math.random()-0.5)*size, 0, (Math.random()-0.5)*size);
scene.add(plant); enemies.push(plant);
}
} else {
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5c4033, roughness: 1.0 });
const leavesMat = new THREE.MeshStandardMaterial({ color: 0x1e5622, roughness: 0.8 });
for(let i=0; i<60; i++) {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 4.0, 80, 32), trunkMat);
trunk.position.y = 30; trunk.castShadow = true; trunk.receiveShadow = true;
tree.add(trunk);
const leaves = new THREE.Mesh(new THREE.DodecahedronGeometry(18, 1), leavesMat);
leaves.position.y = 65; leaves.castShadow = true; leaves.userData.isLeaves = true;
tree.add(leaves);
let px = (Math.random() - 0.5) * 200; let pz = (Math.random() - 0.5) * 200;
if(Math.abs(px) < 15 && Math.abs(pz) < 15) px += 20;
let py = getTerrainHeight(px, pz);
tree.position.set(px, py - 5, pz);
environmentGroup.add(tree);
}
for(let i=0; i<15; i++) spawnFood();
}
}
function startPhase1() {
gameState.phase = 1;
gameState.progress = 0;
gameState.targetProgress = 5;
clearScene();
scene.fog.density = 0.05;
dpadContainer.classList.add('hidden');
actionBtn.classList.add('hidden');
finishBtn.classList.add('hidden');
attackBtn.classList.add('hidden');
progressContainer.classList.remove('hidden');
updateProgress();
const leafGeo = new THREE.PlaneGeometry(30, 30, 10, 10);
const leafMat = new THREE.MeshStandardMaterial({ color: 0x2e8b57, roughness: 0.6, metalness: 0.1 });
const leaf = new THREE.Mesh(leafGeo, leafMat);
leaf.rotation.x = -Math.PI / 2; leaf.receiveShadow = true;
environmentGroup.add(leaf);
const eggGeo = new THREE.SphereGeometry(1, 64, 64); eggGeo.scale(1, 1.15, 1);
playerObj = new THREE.Mesh(eggGeo, new THREE.MeshStandardMaterial({ color: 0xfffae6, roughness: 0.3, metalness: 0.1, clearcoat: 0.5 }));
playerObj.position.y = 1; playerObj.castShadow = true;
playerObj.userData = { shakeTime: 0, baseY: 1 };
scene.add(playerObj);
camera.position.set(0, 4, 6); camera.lookAt(0, 0.5, 0);
createDustParticles();
}
function createPlayerLarva() {
const player = new THREE.Group();
const segmentsCount = 12;
const bodyColor = 0x55c533;
for(let i=0; i<segmentsCount; i++) {
let scaleFactor = 1.0;
if (i === 0) scaleFactor = 0.4;
else if (i === 1) scaleFactor = 0.6;
else if (i === 2) scaleFactor = 0.9;
else if (i === 3) scaleFactor = 1.2;
else scaleFactor = 1.2 - (i - 3) * 0.08;
const segGroup = new THREE.Group();
const segGeo = new THREE.SphereGeometry(0.55 * scaleFactor, 32, 32);
segGeo.scale(1.1, 0.9, 1.0);
const segMat = new THREE.MeshPhysicalMaterial({ color: bodyColor, roughness: 0.4, metalness: 0.05, clearcoat: 0.6, clearcoatRoughness: 0.2 });
const seg = new THREE.Mesh(segGeo, segMat);
seg.castShadow = true;
segGroup.add(seg);
const sideLineGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.45 * scaleFactor, 8);
const sideLineMat = new THREE.MeshBasicMaterial({color: 0xffffff});
const lLine = new THREE.Mesh(sideLineGeo, sideLineMat);
lLine.rotation.z = Math.PI / 2; lLine.position.set(0.58 * scaleFactor, -0.15 * scaleFactor, 0);
segGroup.add(lLine);
const rLine = lLine.clone();
rLine.position.set(-0.58 * scaleFactor, -0.15 * scaleFactor, 0);
segGroup.add(rLine);
if (i === 3) {
const addEye = (side) => {
const eyeGroup = new THREE.Group();
const eyeBase = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.05, 32), new THREE.MeshBasicMaterial({color: 0x111111}));
eyeBase.rotation.x = Math.PI / 2;
const eyeInner = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.06, 32), new THREE.MeshBasicMaterial({color: 0xddddaa}));
eyeInner.rotation.x = Math.PI / 2;
const pupil = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.07, 32), new THREE.MeshBasicMaterial({color: 0x111111}));
pupil.rotation.x = Math.PI / 2; pupil.position.set(0.02 * side, 0.02, 0.01);
eyeGroup.add(eyeBase); eyeGroup.add(eyeInner); eyeGroup.add(pupil);
eyeGroup.position.set(0.35 * side, 0.25, 0.35);
eyeGroup.rotation.y = (Math.PI / 3) * side; eyeGroup.rotation.x = -0.2;
segGroup.add(eyeGroup);
};
addEye(1); addEye(-1);
const stripe = new THREE.Mesh(new THREE.TorusGeometry(0.56 * scaleFactor, 0.04, 16, 32, Math.PI), new THREE.MeshBasicMaterial({color: 0x111111}));
stripe.rotation.x = -Math.PI / 2; stripe.position.y = 0.05;
segGroup.add(stripe);
}
if (i === 4 || i === 6 || i === 8 || i === 10) {
const stripe = new THREE.Mesh(new THREE.TorusGeometry(0.54 * scaleFactor, 0.03, 16, 32, Math.PI), new THREE.MeshBasicMaterial({color: 0x111111}));
stripe.rotation.x = -Math.PI / 2; stripe.position.y = 0.05;
segGroup.add(stripe);
const dot = new THREE.Mesh(new THREE.SphereGeometry(0.06, 16, 16), new THREE.MeshBasicMaterial({color: 0xff6600}));
dot.position.set(0, 0.52 * scaleFactor, 0);
segGroup.add(dot);
}
if ((i >= 1 && i <= 3) || (i >= 6 && i <= 9) || i === 11) {
const isThoracic = i <= 3;
const legGeo = new THREE.CylinderGeometry(isThoracic ? 0.03 : 0.08, isThoracic ? 0.01 : 0.1, isThoracic ? 0.3 : 0.25);
const legMat = new THREE.MeshStandardMaterial({color: isThoracic ? 0x111111 : bodyColor});
const lLeg = new THREE.Mesh(legGeo, legMat);
lLeg.position.set(0.2 * scaleFactor, -0.45 * scaleFactor, 0); lLeg.rotation.z = -0.2;
lLeg.userData = { isLeg: true, baseRotX: 0, baseRotZ: -0.2, side: 1 };
segGroup.add(lLeg);
const rLeg = new THREE.Mesh(legGeo, legMat);
rLeg.position.set(-0.2 * scaleFactor, -0.45 * scaleFactor, 0); rLeg.rotation.z = 0.2;
rLeg.userData = { isLeg: true, baseRotX: 0, baseRotZ: 0.2, side: -1 };
segGroup.add(rLeg);
}
if (i === 0) {
const face = new THREE.Mesh(new THREE.SphereGeometry(0.25, 16, 16), new THREE.MeshStandardMaterial({color: 0x8b5a2b}));
face.position.set(0, -0.1, 0.2);
segGroup.add(face);
}
segGroup.position.z = i * 0.45;
segGroup.userData.isSegment = true;
segGroup.userData.baseY = 0.6 * scaleFactor;
segGroup.position.y = segGroup.userData.baseY;
player.add(segGroup);
}
player.userData.surfaceNormal = new THREE.Vector3(0, 1, 0);
return player;
}
function startPhase2() {
gameState.phase = 2;
gameState.progress = 0;
gameState.targetProgress = 5;
gameState.isReadyToPupate = false;
gameState.hasStormed = false;
gameState.isStorming = false;
clearScene();
scene.fog.density = 0.02;
dpadContainer.classList.remove('hidden');
attackBtn.classList.remove('hidden');
updateProgress();
buildEnvironment('forest');
playerObj = createPlayerLarva();
playerObj.position.set(0, getTerrainHeight(0,0) + 1, 0);
scene.add(playerObj);
camera.position.set(0, 10, 15); camera.lookAt(0, 0, 0);
createDustParticles();
}
function spawnFood() {
let placed = false;
for(let attempt=0; attempt<20; attempt++) {
let basePos = playerObj ? playerObj.position : new THREE.Vector3(0,0,0);
let px = THREE.MathUtils.clamp(basePos.x + (Math.random()-0.5)*120, -120, 120);
let pz = THREE.MathUtils.clamp(basePos.z + (Math.random()-0.5)*120, -120, 120);
let targetY = getTerrainHeight(px, pz) + 0.5;
let eps = 0.1;
let hx = getTerrainHeight(px + eps, pz);
let hz = getTerrainHeight(px, pz + eps);
let dx = new THREE.Vector3(eps, hx - (targetY - 0.5), 0);
let dz = new THREE.Vector3(0, hz - (targetY - 0.5), eps);
let targetNormal = new THREE.Vector3().crossVectors(dz, dx).normalize();
const foodGroup = new THREE.Group();
const foodMesh = new THREE.Mesh(
new THREE.SphereGeometry(0.8, 16, 16),
new THREE.MeshStandardMaterial({ color: 0xadff2f, roughness: 0.2, emissive: 0x33aa00 })
);
foodMesh.castShadow = true; foodGroup.add(foodMesh);
const aura = new THREE.Mesh(
new THREE.SphereGeometry(1.5, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending })
);
foodGroup.add(aura);
const pillar = new THREE.Mesh(
new THREE.CylinderGeometry(0.1, 0.8, 30, 8),
new THREE.MeshBasicMaterial({ color: 0xadff2f, transparent: true, opacity: 0.3, blending: THREE.AdditiveBlending })
);
pillar.position.y = 15; foodGroup.add(pillar);
foodGroup.position.set(px, targetY, pz);
let q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), targetNormal);
foodGroup.quaternion.copy(q);
scene.add(foodGroup);
gameObjects.push({ mesh: foodGroup, type: 'food' });
placed = true;
break;
}
if(!placed) setTimeout(spawnFood, 100);
}
// --- 嵐と鉄砲水の演出 ---
function triggerStorm() {
gameState.isStorming = true;
gameState.hasStormed = true;
gameState.stormIntensity = 0;
playStormSound();
// 鉄砲水(濁流の壁)を生成
const waterGeo = new THREE.PlaneGeometry(300, 100);
const waterMat = new THREE.MeshStandardMaterial({color: 0x8b7355, transparent: true, opacity: 0.9, roughness: 0.2});
gameState.waterWall = new THREE.Mesh(waterGeo, waterMat);
let fw = new THREE.Vector3(0, 0, -1).applyQuaternion(playerObj.quaternion);
gameState.waterWall.position.copy(playerObj.position).add(fw.multiplyScalar(150));
gameState.waterWall.position.y = getTerrainHeight(gameState.waterWall.position.x, gameState.waterWall.position.z) + 10;
gameState.waterWall.rotation.x = -Math.PI / 10;
gameState.waterWall.lookAt(playerObj.position);
scene.add(gameState.waterWall);
setTimeout(startWetlandPhase, 5000);
}
function startWetlandPhase() {
flashScreen.style.display = 'block';
flashScreen.style.backgroundColor = '#8b7355'; // 泥水の色でフラッシュ
flashScreen.style.opacity = '1';
stopStormSound();
setTimeout(() => {
gameState.phase = 2.1;
gameState.startZ = 800;
gameState.goalZ = -800;
buildEnvironment('wetland');
playerObj.position.set(0, getTerrainHeight(0, gameState.startZ) + 1, gameState.startZ);
playerObj.rotation.set(0, Math.PI, 0);
flashScreen.style.opacity = '0';
setTimeout(() => { flashScreen.style.display = 'none'; flashScreen.style.backgroundColor = 'white'; }, 1000);
gameState.isStorming = false;
if(gameState.waterWall) { scene.remove(gameState.waterWall); gameState.waterWall = null; }
scene.background = createSkyTexture('wetland');
scene.fog.color.setHex(0x667777);
scene.fog.density = 0.005;
hemiLight.intensity = 0.6;
dirLight.intensity = 0.8;
updateProgress();
}, 500);
}
function transitionToHomeForest() {
flashScreen.style.display = 'block';
flashScreen.style.backgroundColor = '#ffffff';
flashScreen.style.opacity = '1';
setTimeout(() => {
gameState.phase = 2.2;
buildEnvironment('forest');
playerObj.position.set(0, getTerrainHeight(0,0) + 1, 0);
playerObj.rotation.set(0, 0, 0);
flashScreen.style.opacity = '0';
setTimeout(() => { flashScreen.style.display = 'none'; }, 1000);
scene.background = createSkyTexture('forest');
scene.fog.color.setHex(0x87CEFA);
scene.fog.density = 0.002;
hemiLight.intensity = 0.9;
dirLight.intensity = 1.5;
progressBar.style.width = '100%';
}, 500);
}
function startPupating() {
gameState.phase = 2.5;
gameState.pupateTimer = 0;
gameState.hasPupated = false;
let localUp = playerObj.userData.surfaceNormal || new THREE.Vector3(0, 1, 0);
let forward = new THREE.Vector3(0, 1, 0);
if (Math.abs(localUp.dot(forward)) > 0.99) {
forward.set(1, 0, 0);
}
let right = new THREE.Vector3().crossVectors(localUp, forward).normalize();
forward.crossVectors(right, localUp).normalize();
let m = new THREE.Matrix4().makeBasis(right, localUp, forward.multiplyScalar(-1));
gameState.pupateQuat = new THREE.Quaternion().setFromRotationMatrix(m);
gameState.silkGroup = new THREE.Group();
playerObj.add(gameState.silkGroup);
}
function startPhase4() {
gameState.phase = 4;
gameState.score = 0;
gameState.progress = 0;
gameState.targetProgress = 20;
gameState.flySpeed = 0;
clearScene();
scene.background = createSkyTexture('forest');
scene.fog.color.setHex(0x87CEFA);
scene.fog.density = 0.002;
hemiLight.intensity = 0.9;
dirLight.intensity = 1.5;
dpadContainer.classList.remove('hidden');
attackBtn.classList.add('hidden');
progressContainer.classList.add('hidden');
statsBar.classList.remove('hidden');
scoreDisplay.innerText = `🌸 0 / ${gameState.targetProgress}`;
const groundGeo = new THREE.PlaneGeometry(2500, 2500, 100, 100);
const pos = groundGeo.attributes.position;
for(let i=0; i<pos.count; i++) {
const px = pos.getX(i); const pz = -pos.getY(i);
pos.setZ(i, getTerrainHeight(px, pz));
}
groundGeo.computeVertexNormals();
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({ color: 0x2e8b57, roughness: 1.0, side: THREE.DoubleSide }));
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true;
environmentGroup.add(ground);
const trunkGeo = new THREE.CylinderGeometry(1, 1.5, 10, 8);
const leavesGeo = new THREE.DodecahedronGeometry(6, 1);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5c4033, roughness: 1.0 });
const leavesMat = new THREE.MeshStandardMaterial({ color: 0x1e5622, roughness: 0.8 });
for(let i=0; i<400; i++) {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = 5; trunk.castShadow = true;
const leaves1 = new THREE.Mesh(leavesGeo, leavesMat); leaves1.position.set(0, 12, 0); leaves1.castShadow = true;
const leaves2 = new THREE.Mesh(leavesGeo, leavesMat); leaves2.position.set(3, 10, 2); leaves2.scale.set(0.8, 0.8, 0.8); leaves2.castShadow = true;
tree.add(trunk); tree.add(leaves1); tree.add(leaves2);
const px = (Math.random() - 0.5) * 1800; const pz = (Math.random() - 0.5) * 1800;
const py = getTerrainHeight(px, pz);
tree.position.set(px, py - 5, pz);
if(px*px + pz*pz > 2500) environmentGroup.add(tree);
}
playerObj = createButterflyModel(0xffffff);
playerObj.position.set(0, getTerrainHeight(0,0) + 10, 0);
scene.add(playerObj);
const npcColors = [0xffffff, 0xffff00, 0xffa500, 0x87cefa, 0xff69b4];
for(let i=0; i<15; i++) {
let npc = createButterflyModel(npcColors[Math.floor(Math.random() * npcColors.length)], true);
let px = (Math.random() - 0.5) * 800; let pz = (Math.random() - 0.5) * 800;
let py = getTerrainHeight(px, pz) + 15 + Math.random() * 30;
npc.position.set(px, py, pz); npc.rotation.y = Math.random() * Math.PI * 2;
scene.add(npc); npcButterflies.push(npc);
}
camera.position.set(0, 15, -15); camera.lookAt(0, 10, 0);
for(let i=0; i<30; i++) spawnRealFlower();
createDustParticles();
}
function spawnRealFlower() {
const colors = [0xff1493, 0xffff00, 0xff4500, 0x00ffff, 0xffffff];
const color = colors[Math.floor(Math.random() * colors.length)];
const flGroup = new THREE.Group();
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.2, 5), new THREE.MeshStandardMaterial({color: 0x228b22}));
stem.position.y = 2.5; flGroup.add(stem);
const flower = new THREE.Mesh(new THREE.DodecahedronGeometry(2.5, 1), new THREE.MeshStandardMaterial({ color: color, roughness: 0.3, emissive: color, emissiveIntensity: 0.3 }));
flower.position.y = 5; flower.castShadow = true; flGroup.add(flower);
let px = (playerObj ? playerObj.position.x : 0) + (Math.random() - 0.5) * 300;
let pz = (playerObj ? playerObj.position.z : 0) + (Math.random() - 0.5) * 300;
let py = getTerrainHeight(px, pz);
flGroup.position.set(px, py, pz);
scene.add(flGroup);
gameObjects.push({ mesh: flGroup, type: 'flower' });
}
function showResult() {
gameState.phase = 5;
clearScene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.005);
scene.background = createSkyTexture('forest');
dpadContainer.classList.add('hidden');
statsBar.classList.add('hidden');
progressContainer.classList.add('hidden');
startBtn.innerText = "▶";
startBtn.classList.remove('hidden');
camera.position.set(0, 0, 10); camera.lookAt(0, 0, 0);
createDustParticles();
}
// --- メインループ ---
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1);
const time = clock.getElapsedTime();
if (gameState.isStorming) {
// 嵐が徐々に激しくなる演出
gameState.stormIntensity = Math.min(1.0, gameState.stormIntensity + delta * 0.2);
// 空が暗くなり、霧が濃くなる
scene.fog.density = 0.015 + gameState.stormIntensity * 0.05;
hemiLight.intensity = Math.max(0.1, 0.9 - gameState.stormIntensity);
dirLight.intensity = Math.max(0.1, 1.5 - gameState.stormIntensity * 1.5);
// 音の激しさ(周波数と音量)を上げる
if (windGain && gameState.windFilter) {
windGain.gain.value = gameState.stormIntensity;
gameState.windFilter.frequency.value = 100 + gameState.stormIntensity * 2000;
}
if (particleSystem) {
const pos = particleSystem.geometry.attributes.position.array;
let speed = 15 + gameState.stormIntensity * 30;
for(let i=0; i<pos.length; i+=3) {
pos[i] += speed * delta; pos[i+1] -= (speed * 3) * delta; pos[i+2] += speed * delta;
if (pos[i+1] < -50) { pos[i] = (Math.random() - 0.5) * 300; pos[i+1] = 150; pos[i+2] = (Math.random() - 0.5) * 300; }
}
particleSystem.geometry.attributes.position.needsUpdate = true;
}
// 鉄砲水がものすごい勢いで迫ってくる
if (gameState.waterWall && playerObj) {
let dir = new THREE.Vector3().subVectors(playerObj.position, gameState.waterWall.position).normalize();
gameState.waterWall.position.add(dir.multiplyScalar(60 * delta));
// 水に飲み込まれたらカメラを揺らす
if (gameState.waterWall.position.distanceTo(playerObj.position) < 10) {
camera.position.x += (Math.random() - 0.5) * 2;
camera.position.y += (Math.random() - 0.5) * 2;
}
}
} else {
if (particleSystem) {
particleSystem.rotation.y += 0.01 * delta;
const pos = particleSystem.geometry.attributes.position.array;
for(let i=1; i<pos.length; i+=3) { pos[i] -= 2 * delta; if (pos[i] < -50) pos[i] = 150; }
particleSystem.geometry.attributes.position.needsUpdate = true;
}
}
enemies.forEach(e => {
if (e.userData.stunTimer > 0) {
e.userData.stunTimer -= delta;
return;
}
let th = getTerrainHeight(e.position.x, e.position.z);
if (e.userData.type === 'antSwarm') {
e.position.x += Math.sin(time * 0.3 + e.userData.seed) * 6 * delta;
e.position.z += Math.cos(time * 0.2 + e.userData.seed) * 6 * delta;
e.position.y = th;
e.children.forEach((ant, idx) => {
let data = e.userData.childrenData[idx];
ant.rotation.y += Math.sin(time * data.speed + data.phase) * 0.1;
let fw = new THREE.Vector3(0, 0, 1).applyQuaternion(ant.quaternion);
ant.position.add(fw.multiplyScalar(4 * delta));
if (ant.position.lengthSq() > 40) {
let targetRot = Math.atan2(ant.position.x, ant.position.z) + Math.PI;
ant.quaternion.slerp(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), targetRot), 0.1);
}
let antTh = getTerrainHeight(e.position.x + ant.position.x, e.position.z + ant.position.z);
antTh = Math.max(antTh, -5);
ant.position.y = antTh - th;
if (ant.userData.legs) {
ant.userData.legs.forEach(leg => {
leg.mesh.rotation.x = Math.sin(time * 20 * data.speed + leg.offset * Math.PI + leg.side * Math.PI/2) * 0.5;
});
}
});
}
else if (e.userData.type === 'spider') {
let moveY = Math.sin(time * 1.5 + e.userData.seed);
e.position.y = th + 10 + moveY * 8;
e.rotation.y += delta * 0.5;
e.userData.legs.forEach(leg => {
leg.mesh.rotation.z = Math.sin(time * 10 + leg.offset) * 0.2 * moveY;
});
}
else if (e.userData.type === 'plant') {
e.position.y = th;
let chomp = Math.max(0, Math.sin(time*3 + e.userData.seed));
e.children[0].children[0].rotation.x = Math.PI/4 - chomp * 0.6;
e.children[0].children[1].rotation.x = Math.PI/4 - chomp * 0.6;
e.children[0].children[2].rotation.x = -Math.PI/4 + chomp * 0.6;
e.children[0].children[3].rotation.x = -Math.PI/4 + chomp * 0.6;
}
});
for (let i = gameState.silks.length - 1; i >= 0; i--) {
let s = gameState.silks[i];
s.life -= delta;
s.mesh.position.add(s.velocity.clone().multiplyScalar(delta));
s.mesh.scale.addScalar(s.scaleSpeed * delta);
s.mesh.rotation.z += delta * 5;
let hitEnemy = null;
for(let e of enemies) {
if(e.userData.stunTimer && e.userData.stunTimer > 0) continue;
let threshold = (e.userData.type === 'antSwarm' ? 8.0 : 4.0) * s.mesh.scale.x;
if(e.position.distanceTo(s.mesh.position) < threshold) {
hitEnemy = e;
break;
}
}
if (hitEnemy) {
hitEnemy.userData.stunTimer = 5.0;
const bindMesh = s.mesh.clone();
bindMesh.position.set(0, 0, 0);
bindMesh.rotation.set(0, 0, 0);
hitEnemy.add(bindMesh);
setTimeout(() => { if(hitEnemy) hitEnemy.remove(bindMesh); }, 5000);
scene.remove(s.mesh);
gameState.silks.splice(i, 1);
continue;
}
if(s.life <= 0) {
scene.remove(s.mesh);
gameState.silks.splice(i, 1);
}
}
if ((gameState.phase === 2 || gameState.phase === 2.1 || gameState.phase === 2.2) && playerObj && !gameState.isStorming) {
let hitEnemy = null;
for(let e of enemies) {
if (e.userData.stunTimer && e.userData.stunTimer > 0) continue;
let dist = playerObj.position.distanceTo(e.position);
let threshold = e.userData.type === 'antSwarm' ? 6.0 : 3.5;
if (dist < threshold) { hitEnemy = e; break; }
}
if (hitEnemy) {
let backward = new THREE.Vector3(0, 0, 1).applyQuaternion(playerObj.quaternion);
playerObj.position.add(backward.multiplyScalar(5.0));
flashScreen.style.display = 'block';
flashScreen.style.backgroundColor = 'red';
flashScreen.style.opacity = '0.4';
setTimeout(() => { flashScreen.style.opacity = '0'; }, 200);
setTimeout(() => { flashScreen.style.display = 'none'; flashScreen.style.backgroundColor = 'white'; }, 1000);
}
let oldPos = playerObj.position.clone();
let oldQuat = playerObj.quaternion.clone();
let isMoving = false;
const moveSpeed = 6.0 * delta;
const rotSpeed = 2.0 * delta;
if (inputState.up) { playerObj.translateZ(-moveSpeed); isMoving = true; }
if (inputState.down) { playerObj.translateZ(moveSpeed); isMoving = true; }
if (inputState.left) { playerObj.rotateY(rotSpeed); if(!inputState.up && !inputState.down) playerObj.translateZ(-moveSpeed); isMoving = true; }
if (inputState.right) { playerObj.rotateY(-rotSpeed); if(!inputState.up && !inputState.down) playerObj.translateZ(-moveSpeed); isMoving = true; }
const isSolid = (o) => !o.object.userData.isLeaves && !o.object.userData.isEnemy;
let localUp = new THREE.Vector3(0, 1, 0).applyQuaternion(playerObj.quaternion).normalize();
let localForward = new THREE.Vector3(0, 0, -1).applyQuaternion(playerObj.quaternion).normalize();
let hitObj = null; let hitNormal = new THREE.Vector3(0, 1, 0); let hitPoint = playerObj.position.clone();
let footOrigin = playerObj.position.clone().add(localUp.clone().multiplyScalar(2.0));
raycaster.set(footOrigin, localUp.clone().multiplyScalar(-1));
let footIntersects = raycaster.intersectObjects(environmentGroup.children, true).filter(isSolid);
if (footIntersects.length > 0 && footIntersects[0].distance < 4.0) {
hitObj = footIntersects[0];
if (hitObj.face) hitNormal.copy(hitObj.face.normal).transformDirection(hitObj.object.matrixWorld).normalize();
hitPoint.copy(hitObj.point);
}
if (isMoving) {
let fwdOrigin = playerObj.position.clone().add(localUp.clone().multiplyScalar(0.8));
raycaster.set(fwdOrigin, localForward);
let fwdIntersects = raycaster.intersectObjects(environmentGroup.children, true).filter(isSolid);
if (fwdIntersects.length > 0 && fwdIntersects[0].distance < 1.5) {
hitObj = fwdIntersects[0];
if (hitObj.face) hitNormal.copy(hitObj.face.normal).transformDirection(hitObj.object.matrixWorld).normalize();
hitPoint.copy(hitObj.point);
}
}
if (!hitObj) {
let fallOrigin = playerObj.position.clone(); fallOrigin.y += 20.0;
raycaster.set(fallOrigin, new THREE.Vector3(0, -1, 0));
let fallbacks = raycaster.intersectObjects(environmentGroup.children, true).filter(isSolid);
if (fallbacks.length > 0) {
hitObj = fallbacks[0];
if (hitObj.face) hitNormal.copy(hitObj.face.normal).transformDirection(hitObj.object.matrixWorld).normalize();
hitPoint.copy(hitObj.point);
}
}
if (hitObj) {
let targetUp = hitNormal; playerObj.userData.surfaceNormal = targetUp.clone();
let targetPos = hitPoint.clone().add(targetUp.clone().multiplyScalar(0.8));
playerObj.position.lerp(targetPos, isMoving ? 0.4 : 0.8);
let alignQuat = new THREE.Quaternion().setFromUnitVectors(localUp, targetUp);
let targetQuat = playerObj.quaternion.clone().premultiply(alignQuat);
playerObj.quaternion.slerp(targetQuat, isMoving ? 0.3 : 0.6);
} else {
playerObj.position.copy(oldPos); playerObj.quaternion.copy(oldQuat);
}
const mapLimit = gameState.phase === 2.1 ? 1000 : 120;
if(playerObj.position.x > mapLimit) playerObj.position.x = mapLimit;
if(playerObj.position.x < -mapLimit) playerObj.position.x = -mapLimit;
if(gameState.phase !== 2.1) {
if(playerObj.position.z > mapLimit) playerObj.position.z = mapLimit;
if(playerObj.position.z < -mapLimit) playerObj.position.z = -mapLimit;
}
const terrainY = getTerrainHeight(playerObj.position.x, playerObj.position.z);
if (playerObj.position.y < terrainY + 0.8) {
playerObj.position.y = terrainY + 0.8;
if (!hitObj) playerObj.userData.surfaceNormal = new THREE.Vector3(0, 1, 0);
}
playerObj.children.forEach((seg, i) => {
if (seg.userData && seg.userData.baseY !== undefined) {
if (isMoving) {
seg.position.y = seg.userData.baseY + Math.max(0, Math.sin(time * 15 - i * 0.8)) * 0.15;
} else {
seg.position.y = seg.userData.baseY + Math.sin(time * 2 - i * 0.3) * 0.02;
}
seg.children.forEach(child => {
if (child.userData && child.userData.isLeg) {
child.rotation.x = child.userData.baseRotX + (isMoving ? Math.sin(time * 20 - i) * 0.4 : 0);
let grabAngle = playerObj.userData.surfaceNormal.y < 0.8 ? 0.5 : 0;
child.rotation.z = child.userData.baseRotZ - (child.userData.side * grabAngle);
}
});
}
});
let flatForward = localForward.clone(); flatForward.y = 0;
if (flatForward.lengthSq() < 0.001) {
flatForward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
flatForward.y = 0;
if (flatForward.lengthSq() < 0.001) flatForward.set(0, 0, -1);
}
flatForward.normalize();
const targetCamPos = playerObj.position.clone().add(flatForward.multiplyScalar(-10)).add(new THREE.Vector3(0, 7, 0));
let terrainHeightAtCam = getTerrainHeight(targetCamPos.x, targetCamPos.z);
if (targetCamPos.y < terrainHeightAtCam + 3) targetCamPos.y = terrainHeightAtCam + 3;
camera.position.lerp(targetCamPos, 0.08);
camera.up.set(0, 1, 0);
camera.lookAt(playerObj.position);
if (gameState.phase === 2) {
for (let i = gameObjects.length - 1; i >= 0; i--) {
if (!gameObjects[i]) continue;
const foodGroup = gameObjects[i].mesh;
if (foodGroup.children.length >= 3) {
foodGroup.children[0].rotation.y += delta * 2;
foodGroup.children[1].scale.setScalar(1 + Math.sin(time * 5 + i) * 0.2);
foodGroup.children[2].rotation.y -= delta;
}
const dx = playerObj.position.x - foodGroup.position.x;
const dy = playerObj.position.y - foodGroup.position.y;
const dz = playerObj.position.z - foodGroup.position.z;
const distSqXZ = dx * dx + dz * dz;
if (distSqXZ < 25.0 && Math.abs(dy) < 10.0) {
scene.remove(foodGroup); gameObjects.splice(i, 1);
if (gameState.progress < gameState.targetProgress) {
gameState.progress++; updateProgress();
playerObj.scale.addScalar(0.06);
if (gameState.progress >= gameState.targetProgress && !gameState.hasStormed) {
triggerStorm();
} else {
spawnFood('forest');
}
}
}
}
}
else if (gameState.phase === 2.1) {
updateProgress();
if (playerObj.position.z <= gameState.goalZ) {
transitionToHomeForest();
}
}
else if (gameState.phase === 2.2) {
if (playerObj.position.y > terrainY + 8) {
if (!gameState.isReadyToPupate) {
gameState.isReadyToPupate = true;
actionBtn.classList.remove('hidden');
actionBtn.classList.add('animate-pulse');
attackBtn.classList.add('hidden');
}
} else {
if (gameState.isReadyToPupate) {
gameState.isReadyToPupate = false;
actionBtn.classList.add('hidden');
actionBtn.classList.remove('animate-pulse');
attackBtn.classList.remove('hidden');
}
}
}
}
else if (gameState.phase === 2.5 && playerObj) {
gameState.pupateTimer += delta;
if (gameState.pupateQuat) {
playerObj.quaternion.slerp(gameState.pupateQuat, 0.05);
}
if (playerObj.children[0]) {
playerObj.children[0].rotation.x = Math.max(-0.6, -gameState.pupateTimer * 0.3);
}
if (gameState.pupateTimer < 3.5) {
if (Math.random() < 0.4) {
const sGeo = new THREE.CylinderGeometry(0.01, 0.01, 1.5 + Math.random(), 4);
const sMat = new THREE.MeshBasicMaterial({color: 0xffffff, transparent: true, opacity: 0.6});
const s = new THREE.Mesh(sGeo, sMat);
s.position.set((Math.random()-0.5)*1.5, (Math.random()-0.5)*2.0, -0.5);
s.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI);
gameState.silkGroup.add(s);
}
}
else if (gameState.pupateTimer > 4.5 && !gameState.hasPupated) {
gameState.hasPupated = true;
let savedPos = playerObj.position.clone();
let savedQuat = playerObj.quaternion.clone();
let silkGroupClone = gameState.silkGroup.clone();
scene.remove(playerObj);
playerObj = new THREE.Group();
const chrys = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.1, 3.5, 32), new THREE.MeshStandardMaterial({ color: 0x556b2f, roughness: 0.6, metalness: 0.2, bumpScale: 0.05 }));
chrys.position.y = -1.5; chrys.rotation.x = 0.2; chrys.castShadow = true;
const mainSilk = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 2.0, 8), new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }));
mainSilk.position.set(0, 0.5, 0);
playerObj.add(mainSilk);
playerObj.add(chrys);
playerObj.add(silkGroupClone);
playerObj.position.copy(savedPos);
playerObj.quaternion.copy(savedQuat);
playerObj.userData = { shakeTime: 0 };
scene.add(playerObj);
setTimeout(() => {
gameState.phase = 3;
gameState.progress = 0;
gameState.targetProgress = 20;
}, 1000);
}
const offset = new THREE.Vector3(0, 4, 12);
const rotationMatrix = new THREE.Matrix4().extractRotation(playerObj.matrix);
offset.applyMatrix4(rotationMatrix);
const targetCamPos = playerObj.position.clone().add(offset);
camera.position.lerp(targetCamPos, 0.02);
camera.up.set(0, 1, 0); camera.lookAt(playerObj.position);
}
else if (gameState.phase === 3 && playerObj) {
if (playerObj.userData.shakeTime > 0) {
playerObj.userData.shakeTime -= delta;
playerObj.rotation.z = Math.sin(time * 60) * 0.1;
} else {
playerObj.rotation.z = 0;
}
const offset = new THREE.Vector3(Math.sin(time*0.5)*10, 2, Math.cos(time*0.5)*10);
camera.position.lerp(playerObj.position.clone().add(offset), 0.05);
camera.up.set(0, 1, 0); camera.lookAt(playerObj.position);
}
else if (gameState.phase === 4 && playerObj) {
let targetSpeed = 0;
if (inputState.up) targetSpeed = 50;
if (inputState.down) targetSpeed = -20;
gameState.flySpeed = THREE.MathUtils.lerp(gameState.flySpeed, targetSpeed, 0.05);
const turnSpeed = 2.0 * delta;
if (inputState.left) playerObj.userData.baseRotY += turnSpeed;
if (inputState.right) playerObj.userData.baseRotY -= turnSpeed;
let targetPitch = (gameState.pointer.y || 0) * (Math.PI / 2.5);
playerObj.userData.baseRotX = THREE.MathUtils.lerp(playerObj.userData.baseRotX, targetPitch, 0.08);
let targetRoll = 0;
if (inputState.left) targetRoll = Math.PI / 4;
if (inputState.right) targetRoll = -Math.PI / 4;
playerObj.userData.baseRotZ = THREE.MathUtils.lerp(playerObj.userData.baseRotZ, targetRoll, 0.08);
playerObj.rotation.set(playerObj.userData.baseRotX, playerObj.userData.baseRotY, playerObj.userData.baseRotZ, 'YXZ');
let localForward = new THREE.Vector3(0, 0, 1).applyQuaternion(playerObj.quaternion).normalize();
let rayOrigin = playerObj.position.clone();
raycaster.set(rayOrigin, localForward);
let intersects = raycaster.intersectObjects(environmentGroup.children, true);
let isLanded = false;
if (intersects.length > 0 && intersects[0].distance < 3.0) {
let hitPoint = intersects[0].point.clone();
let hitNorm = new THREE.Vector3(0,1,0);
if(intersects[0].face) hitNorm.copy(intersects[0].face.normal).transformDirection(intersects[0].object.matrixWorld).normalize();
playerObj.position.copy(hitPoint.add(hitNorm.multiplyScalar(2.0)));
gameState.flySpeed = 0; isLanded = true;
}
let currentHeight = getTerrainHeight(playerObj.position.x, playerObj.position.z);
if (playerObj.position.y < currentHeight + 3.0) {
playerObj.position.y = currentHeight + 3.0;
if (gameState.flySpeed < 10 && !inputState.up) isLanded = true;
}
if (playerObj.position.y > 200) playerObj.position.y = 200;
if (!isLanded) {
playerObj.translateZ(gameState.flySpeed * delta);
playerObj.position.y += Math.sin(time * 10) * 0.05;
}
let flapSpeed = isLanded ? 2 : (15 + Math.abs(gameState.flySpeed) * 0.2);
let flapAmp = isLanded ? 0.1 : 0.8;
const flapTime = time * flapSpeed;
const flap = Math.sin(flapTime) * flapAmp - (isLanded ? 0 : 0.2);
if (playerObj.userData.lWing) {
playerObj.userData.lWing.rotation.z = flap;
playerObj.userData.rWing.rotation.z = -flap;
}
if (playerObj.userData.lAnt) {
playerObj.userData.lAnt.rotation.x = Math.PI/2.5 + Math.sin(time * 5) * 0.1;
playerObj.userData.rAnt.rotation.x = Math.PI/2.5 + Math.sin(time * 5.2) * 0.1;
}
for (let i = gameObjects.length - 1; i >= 0; i--) {
if (!gameObjects[i]) continue;
const flGroup = gameObjects[i].mesh;
if(flGroup.children.length > 1) flGroup.children[1].rotation.y += delta;
if (playerObj.position.distanceTo(flGroup.position) < 8) {
scene.remove(flGroup); gameObjects.splice(i, 1);
gameState.score += 100;
if(gameState.progress < gameState.targetProgress) {
gameState.progress++; updateProgress();
scoreDisplay.innerText = `🌸 ${gameState.progress} / ${gameState.targetProgress}`;
if (gameState.progress >= gameState.targetProgress) {
finishBtn.classList.remove('hidden');
}
}
spawnRealFlower(); continue;
}
if (playerObj.position.distanceTo(flGroup.position) > 400) {
scene.remove(flGroup); gameObjects.splice(i, 1); spawnRealFlower();
}
}
npcButterflies.forEach((npc, i) => {
npc.translateZ(25 * delta);
npc.rotation.y += Math.sin(time * 0.5 + i) * delta;
npc.position.y += Math.cos(time + i) * 0.1;
let nth = getTerrainHeight(npc.position.x, npc.position.z);
if(npc.position.y < nth + 5) npc.position.y = nth + 5;
if(npc.position.y > 150) npc.position.y = 150;
if(npc.position.distanceTo(playerObj.position) > 600) {
npc.position.copy(playerObj.position).add(new THREE.Vector3((Math.random()-0.5)*400, (Math.random()-0.5)*100, (Math.random()-0.5)*400));
}
let nFlap = Math.sin(time * 15 + i) * 0.8 - 0.2;
if (npc.userData.lWing) {
npc.userData.lWing.rotation.z = nFlap;
npc.userData.rWing.rotation.z = -nFlap;
}
});
let flatForward = new THREE.Vector3(0, 0, 1).applyQuaternion(playerObj.quaternion);
flatForward.y = 0;
if (flatForward.lengthSq() < 0.001) {
flatForward = new THREE.Vector3(0, 0, 1).applyQuaternion(camera.quaternion);
flatForward.y = 0;
if (flatForward.lengthSq() < 0.001) flatForward.set(0, 0, 1);
}
flatForward.normalize();
let targetCamPos = playerObj.position.clone().add(flatForward.multiplyScalar(-15)).add(new THREE.Vector3(0, 6, 0));
let tcHeight = getTerrainHeight(targetCamPos.x, targetCamPos.z);
if(targetCamPos.y < tcHeight + 2) targetCamPos.y = tcHeight + 2;
camera.position.lerp(targetCamPos, 0.1);
camera.up.set(0, 1, 0);
camera.lookAt(playerObj.position);
}
else if (gameState.phase === 5) {
if (camera.parent) camera.parent.remove(camera);
camera.position.x = Math.sin(time * 0.3) * 30;
camera.position.z = Math.cos(time * 0.3) * 30;
camera.position.y = 20 + Math.sin(time * 0.5) * 10;
camera.up.set(0, 1, 0);
camera.lookAt(0, 10, 0);
}
renderer.render(scene, camera);
}
startBtn.addEventListener('click', () => {
startBtn.classList.add('hidden');
startPhase1();
});
animate();
</script>
</body>
</html>