Skip to content

Commit 8bf3244

Browse files
committed
Add spirit exercise scene
1 parent 60a4b6c commit 8bf3244

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed

js/scenes/spirit_exercise.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import * as cg from "../render/core/cg.js";
2+
import { loadSound, playSoundAtPosition } from "../util/positional-audio.js";
3+
4+
const TARGET_COUNT = 36;
5+
const TRAIL_COUNT = 24;
6+
const STRIKE_Z = -0.55;
7+
const DESPAWN_Z = -0.15;
8+
const SPAWN_Z = -5.6;
9+
10+
const lanes = [-1.0, -0.35, 0.35, 1.0];
11+
const heights = [1.15, 1.5, 1.85];
12+
13+
const leftColor = [0.22, 0.9, 1.0];
14+
const rightColor = [1.0, 0.35, 0.25];
15+
const neutralColor = [0.95, 0.9, 0.35];
16+
const pulseColor = [0.2, 0.9, 0.5];
17+
18+
let soundBuffer = [], loadSounds = [];
19+
for (let i = 0; i < 6; i++)
20+
loadSounds.push(loadSound("../../media/sound/bounce/" + i + ".wav", buffer => soundBuffer[i] = buffer));
21+
Promise.all(loadSounds);
22+
23+
const makeTarget = () => ({
24+
active: false,
25+
hand: "any",
26+
lane: 0,
27+
level: 1,
28+
spawnTime: 0,
29+
life: 0,
30+
speed: 0,
31+
hitFlash: 0,
32+
pos: [0, 1.5, SPAWN_Z],
33+
});
34+
35+
const targetColor = hand => hand == "left" ? leftColor : hand == "right" ? rightColor : neutralColor;
36+
37+
const choosePattern = beat => {
38+
const phase = beat % 16;
39+
40+
if (phase == 12 || phase == 13 || phase == 14)
41+
return { step: 0.23, count: 1, speed: 3.4, mode: "burst" };
42+
43+
if (phase >= 8)
44+
return { step: 0.36, count: 1, speed: 3.0, mode: "cross" };
45+
46+
return { step: 0.42, count: 1, speed: 2.7, mode: "flow" };
47+
};
48+
49+
export const init = async model => {
50+
let beat = 0;
51+
let nextSpawnTime = 0;
52+
let combo = 0;
53+
let bestCombo = 0;
54+
let score = 0;
55+
let hits = 0;
56+
let misses = 0;
57+
let coachPulse = 0;
58+
59+
let targets = [];
60+
let trails = [];
61+
62+
for (let i = 0; i < TARGET_COUNT; i++) {
63+
targets.push(makeTarget());
64+
model.add("sphere");
65+
}
66+
67+
for (let i = 0; i < TRAIL_COUNT; i++) {
68+
trails.push({ pos: [0, 1.5, -1], life: 0, color: [1, 1, 1] });
69+
model.add("sphere");
70+
}
71+
72+
const leftGuide = model.add("ringZ");
73+
const rightGuide = model.add("ringZ");
74+
const pulseRing = model.add("ringY");
75+
76+
const horizon = model.add("square");
77+
const floor = model.add("square");
78+
const leftPeak = model.add("coneY");
79+
const rightPeak = model.add("coneY");
80+
const moon = model.add("sphere");
81+
82+
let addTrail = (pos, color) => {
83+
trails.unshift({ pos: [...pos], life: 1, color: [...color] });
84+
trails.pop();
85+
};
86+
87+
let spawnTarget = (spawnAt, patternMode, speed) => {
88+
for (let i = 0; i < TARGET_COUNT; i++) {
89+
if (targets[i].active)
90+
continue;
91+
92+
const lane = patternMode == "cross"
93+
? (beat % 2 == 0 ? 0 : 3)
94+
: 4 * Math.random() >> 0;
95+
96+
const level = patternMode == "burst"
97+
? ((beat + i) % 3)
98+
: (3 * Math.random() >> 0);
99+
100+
let hand = "any";
101+
if (lane < 2)
102+
hand = "left";
103+
if (lane > 1)
104+
hand = "right";
105+
if (patternMode == "flow" && beat % 4 == 3)
106+
hand = "any";
107+
108+
targets[i].active = true;
109+
targets[i].hand = hand;
110+
targets[i].lane = lane;
111+
targets[i].level = level;
112+
targets[i].spawnTime = spawnAt;
113+
targets[i].life = 1;
114+
targets[i].speed = speed;
115+
targets[i].hitFlash = 0;
116+
targets[i].pos = [lanes[lane], heights[level], SPAWN_Z];
117+
return;
118+
}
119+
};
120+
121+
let playHit = index => {
122+
if (soundBuffer.length == 0)
123+
return;
124+
playSoundAtPosition(soundBuffer[index % soundBuffer.length], targets[index].pos);
125+
};
126+
127+
model.animate(() => {
128+
const t = model.time;
129+
const dt = model.deltaTime;
130+
const bpm = 132 + 16 * Math.sin(0.05 * t);
131+
const beatDuration = 60 / bpm;
132+
133+
while (t >= nextSpawnTime) {
134+
const pattern = choosePattern(beat);
135+
for (let i = 0; i < pattern.count; i++)
136+
spawnTarget(nextSpawnTime + i * pattern.step, pattern.mode, pattern.speed);
137+
138+
nextSpawnTime += beatDuration;
139+
beat++;
140+
coachPulse = 1;
141+
}
142+
143+
const leftHand = clientState.finger(clientID, "left", 1);
144+
const rightHand = clientState.finger(clientID, "right", 1);
145+
146+
if (Array.isArray(leftHand))
147+
addTrail(leftHand, leftColor);
148+
if (Array.isArray(rightHand))
149+
addTrail(rightHand, rightColor);
150+
151+
coachPulse *= 0.93;
152+
153+
for (let i = 0; i < TARGET_COUNT; i++) {
154+
const targetNode = model.child(i);
155+
const target = targets[i];
156+
157+
if (!target.active) {
158+
targetNode.identity().scale(0);
159+
continue;
160+
}
161+
162+
target.pos[2] += target.speed * dt;
163+
target.hitFlash *= 0.86;
164+
165+
let gotHit = false;
166+
const hitRadius = 0.28;
167+
168+
if (Array.isArray(leftHand) && target.hand != "right" && cg.distance(leftHand, target.pos) < hitRadius)
169+
gotHit = true;
170+
171+
if (Array.isArray(rightHand) && target.hand != "left" && cg.distance(rightHand, target.pos) < hitRadius)
172+
gotHit = true;
173+
174+
if (gotHit) {
175+
target.hitFlash = 1;
176+
target.active = false;
177+
combo++;
178+
hits++;
179+
score += 10 + combo;
180+
bestCombo = Math.max(bestCombo, combo);
181+
coachPulse = 1;
182+
playHit(i);
183+
continue;
184+
}
185+
186+
if (target.pos[2] > DESPAWN_Z) {
187+
target.active = false;
188+
misses++;
189+
combo = 0;
190+
continue;
191+
}
192+
193+
const c = targetColor(target.hand);
194+
const glow = 0.2 + 0.25 * Math.sin(14 * t + i);
195+
const depthScale = 1.05 + 0.8 * (target.pos[2] - STRIKE_Z) / (SPAWN_Z - STRIKE_Z);
196+
197+
targetNode.identity()
198+
.move(target.pos)
199+
.scale(0.13 * depthScale)
200+
.color(c[0] + glow, c[1] + glow, c[2] + glow);
201+
}
202+
203+
for (let i = 0; i < TRAIL_COUNT; i++) {
204+
const n = model.child(TARGET_COUNT + i);
205+
const tr = trails[i];
206+
tr.life *= 0.9;
207+
208+
if (tr.life < 0.06) {
209+
n.identity().scale(0);
210+
continue;
211+
}
212+
213+
n.identity().move(tr.pos).scale(0.04 * tr.life)
214+
.color(tr.color[0] * tr.life, tr.color[1] * tr.life, tr.color[2] * tr.life);
215+
}
216+
217+
const leftGuidePos = Array.isArray(leftHand) ? leftHand : [-0.45, 1.4, -0.55];
218+
const rightGuidePos = Array.isArray(rightHand) ? rightHand : [0.45, 1.4, -0.55];
219+
220+
leftGuide.identity().move(leftGuidePos).scale(0.1 + 0.03 * Math.sin(9 * t)).color(...leftColor);
221+
rightGuide.identity().move(rightGuidePos).scale(0.1 + 0.03 * Math.sin(9 * t + 1)).color(...rightColor);
222+
223+
pulseRing.identity().move(0, 1.5, STRIKE_Z - 0.02)
224+
.turnY(2 * t)
225+
.scale(1.0 + 0.45 * coachPulse)
226+
.color(pulseColor[0], pulseColor[1], pulseColor[2]);
227+
228+
horizon.identity().move(0, 2.0, -7.5).scale(9.5, 4.8, 1).color(0.06, 0.1, 0.2);
229+
floor.identity().move(0, 0.72, -2.8).turnX(-Math.PI / 2).scale(3.2, 3.2, 1).color(0.04, 0.06, 0.12);
230+
leftPeak.identity().move(-4.8, 0.8, -8.0).scale(1.8, 3.2, 1.8).color(0.08, 0.14, 0.2);
231+
rightPeak.identity().move(4.8, 0.82, -8.1).scale(2.1, 3.4, 2.1).color(0.09, 0.12, 0.22);
232+
moon.identity().move(2.4, 3.1, -7.0).scale(0.32).color(0.85, 0.95, 1.0);
233+
234+
const total = hits + misses;
235+
const accuracy = total > 0 ? Math.floor(100 * hits / total) : 100;
236+
237+
while (model.nChildren() > TARGET_COUNT + TRAIL_COUNT + 8)
238+
model.remove(TARGET_COUNT + TRAIL_COUNT + 8);
239+
240+
model.add(clay.text("RHYTHM CARDIO")).move(-1.05, 2.52, -1.85).scale(1.95).color(0.9, 1.0, 1.0);
241+
model.add(clay.text("SCORE " + score)).move(-1.05, 2.30, -1.85).scale(1.2).color(0.85, 0.95, 1.0);
242+
model.add(clay.text("COMBO " + combo + " BEST " + bestCombo)).move(-1.05, 2.13, -1.85).scale(1.08).color(1.0, 0.95, 0.5);
243+
model.add(clay.text("ACCURACY " + accuracy + "%"))
244+
.move(-1.05, 1.96, -1.85).scale(1.08).color(0.5, 1.0, 0.8);
245+
model.add(clay.text("STRIKE IN TIME WITH THE BEAT"))
246+
.move(-1.05, 1.73, -1.85).scale(0.92).color(0.65, 0.85, 1.0);
247+
});
248+
};

0 commit comments

Comments
 (0)