흔들 때 나뭇잎이 팔랑거리며 떨어지고 낮은 확률로 다람쥐가 나타나 도망가는 연출 추가
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
194
index.html
194
index.html
@@ -270,6 +270,8 @@ const GROUND_Y = 0.16;
|
|||||||
const GRAVITY = -9.2;
|
const GRAVITY = -9.2;
|
||||||
const RESPAWN_AFTER = 2.6; // how long a branch stays empty before a new apple grows back
|
const RESPAWN_AFTER = 2.6; // how long a branch stays empty before a new apple grows back
|
||||||
const MISS_LINGER = 1.4; // how long a missed apple stays on the grass before fading away
|
const MISS_LINGER = 1.4; // how long a missed apple stays on the grass before fading away
|
||||||
|
const LEAF_LINGER = 1.8; // how long a fallen leaf rests before fading away
|
||||||
|
const SQUIRREL_COOLDOWN = 6; // min seconds between squirrel appearances
|
||||||
|
|
||||||
// a tree "slot" holds at most one apple at a time and regrows independently of
|
// a tree "slot" holds at most one apple at a time and regrows independently of
|
||||||
// whatever happens to the apple once it falls (caught in the basket or not)
|
// whatever happens to the apple once it falls (caught in the basket or not)
|
||||||
@@ -284,6 +286,114 @@ const fallingApples = []; // independent apples in flight: falling | resting | v
|
|||||||
const basketApples = []; // apples caught and resting in the basket
|
const basketApples = []; // apples caught and resting in the basket
|
||||||
let basketCount = 0;
|
let basketCount = 0;
|
||||||
|
|
||||||
|
// ---------- leaves ----------
|
||||||
|
const leafGeo = new THREE.PlaneGeometry(0.14, 0.18);
|
||||||
|
const leafMats = [0x4f9d6e, 0x3f8a5d, 0x5aab77].map(
|
||||||
|
(c) => new THREE.MeshStandardMaterial({ color: c, roughness: 0.85, side: THREE.DoubleSide })
|
||||||
|
);
|
||||||
|
function createLeafMesh(){
|
||||||
|
return new THREE.Mesh(leafGeo, leafMats[Math.floor(Math.random() * leafMats.length)]);
|
||||||
|
}
|
||||||
|
const fallingLeaves = []; // falling | resting | vanishing
|
||||||
|
|
||||||
|
function randomFoliageWorldPoint(){
|
||||||
|
const spec = foliageSpecs[Math.floor(Math.random() * foliageSpecs.length)];
|
||||||
|
const dir = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
|
||||||
|
const local = new THREE.Vector3(spec.pos[0], spec.pos[1], spec.pos[2]).addScaledVector(dir, spec.r * 0.9);
|
||||||
|
return treeGroup.localToWorld(local);
|
||||||
|
}
|
||||||
|
|
||||||
|
function knockOffLeaf(){
|
||||||
|
if (fallingLeaves.length > 30) return;
|
||||||
|
const world = randomFoliageWorldPoint();
|
||||||
|
const mesh = createLeafMesh();
|
||||||
|
mesh.position.copy(world);
|
||||||
|
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
fallingLeaves.push({
|
||||||
|
mesh,
|
||||||
|
velocity: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 0.8 + angVel.z * 0.04,
|
||||||
|
0.3 + Math.random() * 0.3,
|
||||||
|
(Math.random() - 0.5) * 0.8
|
||||||
|
),
|
||||||
|
spin: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 4,
|
||||||
|
(Math.random() - 0.5) * 4,
|
||||||
|
(Math.random() - 0.5) * 4
|
||||||
|
),
|
||||||
|
driftPhase: Math.random() * Math.PI * 2,
|
||||||
|
state: 'falling',
|
||||||
|
timer: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- squirrel ----------
|
||||||
|
function createSquirrelMesh(){
|
||||||
|
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8a6642, roughness: 0.9, flatShading: true });
|
||||||
|
const bellyMat = new THREE.MeshStandardMaterial({ color: 0xe4c9a0, roughness: 0.9, flatShading: true });
|
||||||
|
|
||||||
|
const g = new THREE.Group();
|
||||||
|
|
||||||
|
const body = new THREE.Mesh(new THREE.SphereGeometry(0.16, 8, 6), bodyMat);
|
||||||
|
body.scale.set(1, 0.85, 1.3);
|
||||||
|
g.add(body);
|
||||||
|
|
||||||
|
const belly = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 6), bellyMat);
|
||||||
|
belly.scale.set(0.8, 0.7, 1);
|
||||||
|
belly.position.set(0, -0.04, 0.05);
|
||||||
|
g.add(belly);
|
||||||
|
|
||||||
|
const head = new THREE.Mesh(new THREE.SphereGeometry(0.11, 8, 6), bodyMat);
|
||||||
|
head.position.set(0, 0.06, 0.22);
|
||||||
|
g.add(head);
|
||||||
|
|
||||||
|
const earGeo = new THREE.ConeGeometry(0.035, 0.07, 6);
|
||||||
|
const earL = new THREE.Mesh(earGeo, bodyMat);
|
||||||
|
earL.position.set(-0.06, 0.15, 0.26);
|
||||||
|
earL.rotation.x = -0.3;
|
||||||
|
g.add(earL);
|
||||||
|
const earR = earL.clone();
|
||||||
|
earR.position.x = 0.06;
|
||||||
|
g.add(earR);
|
||||||
|
|
||||||
|
const tail = new THREE.Mesh(new THREE.SphereGeometry(0.14, 8, 6), bodyMat);
|
||||||
|
tail.scale.set(0.7, 1.4, 0.7);
|
||||||
|
tail.position.set(0, 0.2, -0.24);
|
||||||
|
tail.rotation.x = 0.7;
|
||||||
|
g.add(tail);
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallingSquirrels = []; // falling | fleeing
|
||||||
|
|
||||||
|
function knockOffSquirrel(){
|
||||||
|
if (fallingSquirrels.length) return;
|
||||||
|
const world = randomFoliageWorldPoint();
|
||||||
|
const mesh = createSquirrelMesh();
|
||||||
|
mesh.position.copy(world);
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
const shadow = makeShadowBlob(0.55);
|
||||||
|
scene.add(shadow);
|
||||||
|
|
||||||
|
const fleeAngle = Math.random() * Math.PI * 2;
|
||||||
|
fallingSquirrels.push({
|
||||||
|
mesh,
|
||||||
|
shadow,
|
||||||
|
velocity: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * 1.2 + angVel.z * 0.05,
|
||||||
|
0.6 + Math.random() * 0.4,
|
||||||
|
(Math.random() - 0.5) * 1.2
|
||||||
|
),
|
||||||
|
fleeDir: new THREE.Vector3(Math.sin(fleeAngle), 0, Math.cos(fleeAngle)),
|
||||||
|
state: 'falling',
|
||||||
|
timer: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateBasketCounter(){
|
function updateBasketCounter(){
|
||||||
basketCountEl.textContent = `${basketCount} / ${BASKET_FULL}`;
|
basketCountEl.textContent = `${basketCount} / ${BASKET_FULL}`;
|
||||||
}
|
}
|
||||||
@@ -300,6 +410,8 @@ const angVel = { x: 0, z: 0 };
|
|||||||
const SPRING_K = 42, SPRING_C = 7;
|
const SPRING_K = 42, SPRING_C = 7;
|
||||||
let shakeEnergy = 0;
|
let shakeEnergy = 0;
|
||||||
let lastDetachAt = -10;
|
let lastDetachAt = -10;
|
||||||
|
let lastLeafAt = -10;
|
||||||
|
let lastSquirrelAt = -999;
|
||||||
|
|
||||||
function toNdc(clientX, clientY){
|
function toNdc(clientX, clientY){
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
@@ -470,6 +582,21 @@ function tick(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shakeEnergy > 0.35 && now - lastLeafAt > 0.05){
|
||||||
|
const chance = 1 - Math.exp(-shakeEnergy * dt * 1.6);
|
||||||
|
if (Math.random() < chance){
|
||||||
|
knockOffLeaf();
|
||||||
|
lastLeafAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shakeEnergy > 1.1 && fallingSquirrels.length === 0 && now - lastSquirrelAt > SQUIRREL_COOLDOWN){
|
||||||
|
if (Math.random() < shakeEnergy * dt * 0.03){
|
||||||
|
knockOffSquirrel();
|
||||||
|
lastSquirrelAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = fallingApples.length - 1; i >= 0; i--){
|
for (let i = fallingApples.length - 1; i >= 0; i--){
|
||||||
const apple = fallingApples[i];
|
const apple = fallingApples[i];
|
||||||
if (apple.state === 'falling'){
|
if (apple.state === 'falling'){
|
||||||
@@ -522,6 +649,73 @@ function tick(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = fallingLeaves.length - 1; i >= 0; i--){
|
||||||
|
const leaf = fallingLeaves[i];
|
||||||
|
if (leaf.state === 'falling'){
|
||||||
|
leaf.timer += dt;
|
||||||
|
leaf.velocity.y += GRAVITY * 0.28 * dt;
|
||||||
|
leaf.mesh.position.x += leaf.velocity.x * dt + Math.sin(leaf.timer * 4 + leaf.driftPhase) * 0.4 * dt;
|
||||||
|
leaf.mesh.position.z += leaf.velocity.z * dt + Math.cos(leaf.timer * 3.3 + leaf.driftPhase) * 0.4 * dt;
|
||||||
|
leaf.mesh.position.y += leaf.velocity.y * dt;
|
||||||
|
leaf.mesh.rotation.x += leaf.spin.x * dt;
|
||||||
|
leaf.mesh.rotation.y += leaf.spin.y * dt;
|
||||||
|
leaf.mesh.rotation.z += leaf.spin.z * dt;
|
||||||
|
|
||||||
|
if (leaf.mesh.position.y <= 0.01){
|
||||||
|
leaf.mesh.position.y = 0.01;
|
||||||
|
leaf.state = 'resting';
|
||||||
|
leaf.timer = 0;
|
||||||
|
}
|
||||||
|
} else if (leaf.state === 'resting'){
|
||||||
|
leaf.timer += dt;
|
||||||
|
if (leaf.timer > LEAF_LINGER){
|
||||||
|
leaf.state = 'vanishing';
|
||||||
|
leaf.timer = 0;
|
||||||
|
}
|
||||||
|
} else if (leaf.state === 'vanishing'){
|
||||||
|
leaf.timer += dt;
|
||||||
|
const t = Math.min(leaf.timer / 0.4, 1);
|
||||||
|
leaf.mesh.scale.setScalar(1 - t);
|
||||||
|
if (t >= 1){
|
||||||
|
scene.remove(leaf.mesh);
|
||||||
|
fallingLeaves.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = fallingSquirrels.length - 1; i >= 0; i--){
|
||||||
|
const sq = fallingSquirrels[i];
|
||||||
|
if (sq.state === 'falling'){
|
||||||
|
sq.velocity.y += GRAVITY * dt;
|
||||||
|
sq.mesh.position.addScaledVector(sq.velocity, dt);
|
||||||
|
sq.mesh.rotation.x += sq.velocity.z * dt;
|
||||||
|
sq.mesh.rotation.z -= sq.velocity.x * dt;
|
||||||
|
|
||||||
|
if (sq.mesh.position.y <= GROUND_Y){
|
||||||
|
sq.mesh.position.y = GROUND_Y;
|
||||||
|
sq.mesh.rotation.set(0, Math.atan2(sq.fleeDir.x, sq.fleeDir.z), 0);
|
||||||
|
sq.state = 'fleeing';
|
||||||
|
sq.timer = 0;
|
||||||
|
}
|
||||||
|
sq.shadow.position.set(sq.mesh.position.x, 0.01, sq.mesh.position.z);
|
||||||
|
const h = Math.max(sq.mesh.position.y - GROUND_Y, 0);
|
||||||
|
sq.shadow.material.opacity = THREE.MathUtils.clamp(1 - h * 0.6, 0.15, 1);
|
||||||
|
} else if (sq.state === 'fleeing'){
|
||||||
|
sq.timer += dt;
|
||||||
|
const speed = 3.4;
|
||||||
|
sq.mesh.position.addScaledVector(sq.fleeDir, speed * dt);
|
||||||
|
sq.mesh.position.y = GROUND_Y + Math.abs(Math.sin(sq.timer * 14)) * 0.05;
|
||||||
|
sq.shadow.position.set(sq.mesh.position.x, 0.01, sq.mesh.position.z);
|
||||||
|
|
||||||
|
const distFromCenter = Math.hypot(sq.mesh.position.x, sq.mesh.position.z);
|
||||||
|
if (distFromCenter > 6.2 || sq.timer > 3){
|
||||||
|
scene.remove(sq.mesh);
|
||||||
|
scene.remove(sq.shadow);
|
||||||
|
fallingSquirrels.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const slot of treeSlots){
|
for (const slot of treeSlots){
|
||||||
if (slot.state === 'empty'){
|
if (slot.state === 'empty'){
|
||||||
slot.timer += dt;
|
slot.timer += dt;
|
||||||
|
|||||||
Reference in New Issue
Block a user