aurasu 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/assets/Aurasu.note-timings.csv +231 -0
- package/assets/Aurasu.timing.json +48 -0
- package/assets/Aurasu.wav +0 -0
- package/assets/GeneralSans-Bold.otf +0 -0
- package/assets/GeneralSans-BoldItalic.otf +0 -0
- package/assets/approachcircle.png +0 -0
- package/assets/aurasu.jpg +0 -0
- package/assets/aurasu1.jpg +0 -0
- package/assets/combo-break.wav +0 -0
- package/assets/cursor.png +0 -0
- package/assets/cursortrail.png +0 -0
- package/assets/followpoint.png +0 -0
- package/assets/hit-clap.wav +0 -0
- package/assets/hit-finish.wav +0 -0
- package/assets/hit-miss.wav +0 -0
- package/assets/hit-normal.wav +0 -0
- package/assets/hit-whistle.wav +0 -0
- package/assets/hitburst_sheet.png +0 -0
- package/assets/hitcircle.png +0 -0
- package/assets/hitcircleoverlay.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/judgement-100.png +0 -0
- package/assets/judgement-300.png +0 -0
- package/assets/judgement-50.png +0 -0
- package/assets/judgement-miss.png +0 -0
- package/assets/slider-end.wav +0 -0
- package/assets/slider-start.wav +0 -0
- package/assets/slider-tick.wav +0 -0
- package/assets/sliderball.png +0 -0
- package/assets/sliderfollowcircle.png +0 -0
- package/assets/ui-back.wav +0 -0
- package/assets/ui-pause.wav +0 -0
- package/assets/ui-start.wav +0 -0
- package/aura.config.json +16 -0
- package/bin/play.js +16 -0
- package/package.json +24 -0
- package/src/main.js +2235 -0
package/src/main.js
ADDED
|
@@ -0,0 +1,2235 @@
|
|
|
1
|
+
// aurasu
|
|
2
|
+
//
|
|
3
|
+
// A hit-circle rhythm prototype inspired by Aurasu-style gameplay:
|
|
4
|
+
// - timed circles
|
|
5
|
+
// - approach rings
|
|
6
|
+
// - timing windows + combo scoring
|
|
7
|
+
// - health and accuracy-based results
|
|
8
|
+
|
|
9
|
+
const APPROACH_TIME = 1.1;
|
|
10
|
+
const HIT_RADIUS = 38;
|
|
11
|
+
const HIT_PICK_RADIUS = 68;
|
|
12
|
+
const EARLY_ASSIST = 0.03;
|
|
13
|
+
const TIMING_OFFSET = -0.012;
|
|
14
|
+
const PRESS_BUFFER = 0.09;
|
|
15
|
+
const DRAG_PICK_RADIUS = 92;
|
|
16
|
+
const DRAG_CHAIN_GRACE = 0.34;
|
|
17
|
+
const DRAG_CHAIN_BEAT_FACTOR = 0.62;
|
|
18
|
+
const DRAG_EARLY_ASSIST = 0.055;
|
|
19
|
+
const BONK_EARLY_MAX = 0.36;
|
|
20
|
+
const BONK_CURSOR_RADIUS = 86;
|
|
21
|
+
|
|
22
|
+
const WINDOW_PERFECT = 0.045;
|
|
23
|
+
const WINDOW_GOOD = 0.09;
|
|
24
|
+
const WINDOW_OK = 0.14;
|
|
25
|
+
const WINDOW_MISS = 0.18;
|
|
26
|
+
|
|
27
|
+
const HEALTH_MAX = 100;
|
|
28
|
+
|
|
29
|
+
const JUDGEMENT_POINTS = {
|
|
30
|
+
perfect: 300,
|
|
31
|
+
good: 100,
|
|
32
|
+
ok: 50,
|
|
33
|
+
miss: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const BACKGROUND_LAYERS = 18;
|
|
37
|
+
const GRID_COLS = 12;
|
|
38
|
+
const GRID_ROWS = 8;
|
|
39
|
+
const CURSOR_TRAIL_LIFE = 0.18;
|
|
40
|
+
const FEVER_COMBO_THRESHOLD = 24;
|
|
41
|
+
const FEVER_MULTIPLIER = 1.25;
|
|
42
|
+
const BGM_ASSET_PATH = 'Aurasu.wav';
|
|
43
|
+
const BACKGROUND_ASSET_PATH = 'aurasu.jpg';
|
|
44
|
+
const BGM_VOLUME = 1.0;
|
|
45
|
+
const BACKGROUND_IMAGE_ALPHA = 0.13;
|
|
46
|
+
const VIS_OVERLAY_DARKNESS = 0.992;
|
|
47
|
+
const VIS_HIT_FLASH_GAIN = 2.0;
|
|
48
|
+
const VIS_NOTE_POP_SCALE = 0.34;
|
|
49
|
+
const VIS_CIRCLE_SOLID_MIN_ALPHA = 1.0;
|
|
50
|
+
const CIRCLE_SPRITE_INSET = 2;
|
|
51
|
+
const CIRCLE_SPRITE_FRAME = 512 - (CIRCLE_SPRITE_INSET * 2);
|
|
52
|
+
const BACKGROUND_DARK_OVERLAY_ALPHA = VIS_OVERLAY_DARKNESS;
|
|
53
|
+
const BACKGROUND_LAYER_ALPHA_WITH_IMAGE = 0.1;
|
|
54
|
+
const BACKGROUND_VIGNETTE_ALPHA = 0.68;
|
|
55
|
+
const ANALYZED_TRACK_DURATION = 193.747302;
|
|
56
|
+
const ANALYZED_BPM = 130;
|
|
57
|
+
const ANALYZED_FIRST_BEAT = 0.441;
|
|
58
|
+
const ANALYZED_BEAT = 60 / ANALYZED_BPM;
|
|
59
|
+
const MAP_LEAD_IN_BEATS = 4;
|
|
60
|
+
const MAP_START_TIME = ANALYZED_FIRST_BEAT + (ANALYZED_BEAT * MAP_LEAD_IN_BEATS);
|
|
61
|
+
const MAP_MAIN_BEATS = 190;
|
|
62
|
+
const DIFFICULTY_PRESETS = [
|
|
63
|
+
{
|
|
64
|
+
id: 'easy',
|
|
65
|
+
label: 'EASY',
|
|
66
|
+
subtitle: 'relaxed timing',
|
|
67
|
+
timingScale: 1.24,
|
|
68
|
+
approachScale: 1.2,
|
|
69
|
+
beatScale: 0.72,
|
|
70
|
+
dragScale: 0.9,
|
|
71
|
+
pickScale: 1.14,
|
|
72
|
+
accent: [0.38, 0.95, 1.0],
|
|
73
|
+
minJump: 2,
|
|
74
|
+
burstInterval: 16,
|
|
75
|
+
burstChance: 0.4,
|
|
76
|
+
burstSecondary: false,
|
|
77
|
+
tripleStartBeat: 150,
|
|
78
|
+
tripleChance: 0.2,
|
|
79
|
+
tripleCount: 1,
|
|
80
|
+
offbeatChance: 0.03,
|
|
81
|
+
offbeatOffset: 0.5,
|
|
82
|
+
dragLinkChance: 0.52,
|
|
83
|
+
densityLevel: 0,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'normal',
|
|
87
|
+
label: 'NORMAL',
|
|
88
|
+
subtitle: 'balanced',
|
|
89
|
+
timingScale: 1.0,
|
|
90
|
+
approachScale: 1.0,
|
|
91
|
+
beatScale: 1.0,
|
|
92
|
+
dragScale: 1.0,
|
|
93
|
+
pickScale: 1.0,
|
|
94
|
+
accent: [0.76, 1.0, 0.26],
|
|
95
|
+
minJump: 3,
|
|
96
|
+
burstInterval: 12,
|
|
97
|
+
burstChance: 0.72,
|
|
98
|
+
burstSecondary: true,
|
|
99
|
+
tripleStartBeat: 120,
|
|
100
|
+
tripleChance: 0.38,
|
|
101
|
+
tripleCount: 2,
|
|
102
|
+
offbeatChance: 0.12,
|
|
103
|
+
offbeatOffset: 0.5,
|
|
104
|
+
dragLinkChance: 0.72,
|
|
105
|
+
densityLevel: 1,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'hard',
|
|
109
|
+
label: 'HARD',
|
|
110
|
+
subtitle: 'dense chains',
|
|
111
|
+
timingScale: 0.86,
|
|
112
|
+
approachScale: 0.86,
|
|
113
|
+
beatScale: 1.44,
|
|
114
|
+
dragScale: 1.08,
|
|
115
|
+
pickScale: 0.94,
|
|
116
|
+
accent: [1.0, 0.48, 0.72],
|
|
117
|
+
minJump: 4,
|
|
118
|
+
burstInterval: 7,
|
|
119
|
+
burstChance: 1.0,
|
|
120
|
+
burstSecondary: true,
|
|
121
|
+
tripleStartBeat: 70,
|
|
122
|
+
tripleChance: 0.8,
|
|
123
|
+
tripleCount: 3,
|
|
124
|
+
offbeatChance: 0.5,
|
|
125
|
+
offbeatOffset: 0.25,
|
|
126
|
+
dragLinkChance: 0.9,
|
|
127
|
+
densityLevel: 2,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'aura',
|
|
131
|
+
label: 'AURA+',
|
|
132
|
+
subtitle: 'overdrive',
|
|
133
|
+
timingScale: 0.76,
|
|
134
|
+
approachScale: 0.74,
|
|
135
|
+
beatScale: 1.95,
|
|
136
|
+
dragScale: 1.14,
|
|
137
|
+
pickScale: 0.9,
|
|
138
|
+
accent: [0.98, 0.3, 0.96],
|
|
139
|
+
minJump: 5,
|
|
140
|
+
burstInterval: 6,
|
|
141
|
+
burstChance: 1.0,
|
|
142
|
+
burstSecondary: true,
|
|
143
|
+
tripleStartBeat: 40,
|
|
144
|
+
tripleChance: 1.0,
|
|
145
|
+
tripleCount: 4,
|
|
146
|
+
offbeatChance: 0.72,
|
|
147
|
+
offbeatOffset: 0.25,
|
|
148
|
+
dragLinkChance: 0.98,
|
|
149
|
+
densityLevel: 3,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
const SKIN_ASSET_PATHS = {
|
|
153
|
+
approachcircle: 'approachcircle.png',
|
|
154
|
+
hitcircle: 'hitcircle.png',
|
|
155
|
+
hitcircleoverlay: 'hitcircleoverlay.png',
|
|
156
|
+
sliderball: 'sliderball.png',
|
|
157
|
+
sliderfollowcircle: 'sliderfollowcircle.png',
|
|
158
|
+
followpoint: 'followpoint.png',
|
|
159
|
+
cursor: 'cursor.png',
|
|
160
|
+
cursortrail: 'cursortrail.png',
|
|
161
|
+
judgement300: 'judgement-300.png',
|
|
162
|
+
judgement100: 'judgement-100.png',
|
|
163
|
+
judgement50: 'judgement-50.png',
|
|
164
|
+
judgementMiss: 'judgement-miss.png',
|
|
165
|
+
hitburstSheet: 'hitburst_sheet.png',
|
|
166
|
+
};
|
|
167
|
+
const SFX_PATHS = {
|
|
168
|
+
normal: 'hit-normal.wav',
|
|
169
|
+
clap: 'hit-clap.wav',
|
|
170
|
+
finish: 'hit-finish.wav',
|
|
171
|
+
whistle: 'hit-whistle.wav',
|
|
172
|
+
sliderStart: 'slider-start.wav',
|
|
173
|
+
sliderTick: 'slider-tick.wav',
|
|
174
|
+
sliderEnd: 'slider-end.wav',
|
|
175
|
+
miss: 'hit-miss.wav',
|
|
176
|
+
comboBreak: 'combo-break.wav',
|
|
177
|
+
uiStart: 'ui-start.wav',
|
|
178
|
+
uiBack: 'ui-back.wav',
|
|
179
|
+
uiPause: 'ui-pause.wav',
|
|
180
|
+
};
|
|
181
|
+
const SFX_VOLUME = {
|
|
182
|
+
normal: 0.58,
|
|
183
|
+
clap: 0.62,
|
|
184
|
+
finish: 0.72,
|
|
185
|
+
whistle: 0.55,
|
|
186
|
+
sliderStart: 0.52,
|
|
187
|
+
sliderTick: 0.42,
|
|
188
|
+
sliderEnd: 0.58,
|
|
189
|
+
miss: 0.58,
|
|
190
|
+
comboBreak: 0.64,
|
|
191
|
+
uiStart: 0.46,
|
|
192
|
+
uiBack: 0.4,
|
|
193
|
+
uiPause: 0.38,
|
|
194
|
+
};
|
|
195
|
+
const SFX_MIN_INTERVAL = {
|
|
196
|
+
normal: 0.028,
|
|
197
|
+
clap: 0.028,
|
|
198
|
+
finish: 0.04,
|
|
199
|
+
whistle: 0.034,
|
|
200
|
+
sliderStart: 0.03,
|
|
201
|
+
sliderTick: 0.022,
|
|
202
|
+
sliderEnd: 0.034,
|
|
203
|
+
miss: 0.08,
|
|
204
|
+
comboBreak: 0.12,
|
|
205
|
+
uiStart: 0.16,
|
|
206
|
+
uiBack: 0.16,
|
|
207
|
+
uiPause: 0.16,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
let worldWidth = 1280;
|
|
211
|
+
let worldHeight = 720;
|
|
212
|
+
let playX = 130;
|
|
213
|
+
let playY = 76;
|
|
214
|
+
let playW = 1020;
|
|
215
|
+
let playH = 568;
|
|
216
|
+
|
|
217
|
+
let notes = [];
|
|
218
|
+
let currentTime = 0;
|
|
219
|
+
let lastNoteTime = 0;
|
|
220
|
+
|
|
221
|
+
let state = 'title'; // title | playing | results
|
|
222
|
+
let failed = false;
|
|
223
|
+
|
|
224
|
+
let score = 0;
|
|
225
|
+
let combo = 0;
|
|
226
|
+
let maxCombo = 0;
|
|
227
|
+
let hits = 0;
|
|
228
|
+
let misses = 0;
|
|
229
|
+
let totalJudged = 0;
|
|
230
|
+
let totalPointValue = 0;
|
|
231
|
+
let achievedPoints = 0;
|
|
232
|
+
let health = HEALTH_MAX;
|
|
233
|
+
|
|
234
|
+
let perfectCount = 0;
|
|
235
|
+
let goodCount = 0;
|
|
236
|
+
let okCount = 0;
|
|
237
|
+
|
|
238
|
+
let bursts = [];
|
|
239
|
+
let judgementPop = null;
|
|
240
|
+
let pulseTime = 0;
|
|
241
|
+
let frameDt = 1 / 60;
|
|
242
|
+
let beatGlow = 0;
|
|
243
|
+
let comboFlash = 0;
|
|
244
|
+
let screenShake = 0;
|
|
245
|
+
let cursorTrail = [];
|
|
246
|
+
let flowMeter = 0;
|
|
247
|
+
let hitBufferTimer = 0;
|
|
248
|
+
let dragChainTimer = 0;
|
|
249
|
+
let dragExpectedIndex = -1;
|
|
250
|
+
let mouseHoldActive = false;
|
|
251
|
+
let keyHoldActive = false;
|
|
252
|
+
let hitPressedThisFrame = false;
|
|
253
|
+
let k1Down = false;
|
|
254
|
+
let k2Down = false;
|
|
255
|
+
let k1Pulse = 0;
|
|
256
|
+
let k2Pulse = 0;
|
|
257
|
+
let k1Flash = 0;
|
|
258
|
+
let k2Flash = 0;
|
|
259
|
+
let bgmLoaded = false;
|
|
260
|
+
let bgmLoadAttempted = false;
|
|
261
|
+
let bgmHandle = -1;
|
|
262
|
+
let bgmMuted = false;
|
|
263
|
+
let bgmLastError = '';
|
|
264
|
+
let backgroundLoaded = false;
|
|
265
|
+
let backgroundLoadAttempted = false;
|
|
266
|
+
let skinLoadAttempted = false;
|
|
267
|
+
const skinLoaded = new Set();
|
|
268
|
+
let sfxLoadAttempted = false;
|
|
269
|
+
let sfxMuted = false;
|
|
270
|
+
const sfxLoaded = new Set();
|
|
271
|
+
const sfxLastPlay = new Map();
|
|
272
|
+
let selectedDifficultyIndex = 1;
|
|
273
|
+
let titleDifficultyBoxes = [];
|
|
274
|
+
let activeDifficulty = DIFFICULTY_PRESETS[selectedDifficultyIndex];
|
|
275
|
+
let activeApproachTime = APPROACH_TIME;
|
|
276
|
+
let activeWindowPerfect = WINDOW_PERFECT;
|
|
277
|
+
let activeWindowGood = WINDOW_GOOD;
|
|
278
|
+
let activeWindowOk = WINDOW_OK;
|
|
279
|
+
let activeWindowMiss = WINDOW_MISS;
|
|
280
|
+
let activeMainBeats = MAP_MAIN_BEATS;
|
|
281
|
+
let activeDragChainBeatFactor = DRAG_CHAIN_BEAT_FACTOR;
|
|
282
|
+
let activeHitPickRadius = HIT_PICK_RADIUS;
|
|
283
|
+
let activeDragPickRadius = DRAG_PICK_RADIUS;
|
|
284
|
+
let activeEarlyAssist = EARLY_ASSIST;
|
|
285
|
+
let activeDragEarlyAssist = DRAG_EARLY_ASSIST;
|
|
286
|
+
let activePressBuffer = PRESS_BUFFER;
|
|
287
|
+
let activeBonkEarlyMax = BONK_EARLY_MAX;
|
|
288
|
+
|
|
289
|
+
const pressLatch = new Map();
|
|
290
|
+
|
|
291
|
+
const PALETTE = [
|
|
292
|
+
[0.0, 0.95, 1.0],
|
|
293
|
+
[1.0, 0.2, 0.75],
|
|
294
|
+
[0.7, 1.0, 0.15],
|
|
295
|
+
[1.0, 0.55, 0.1],
|
|
296
|
+
[0.62, 0.35, 1.0],
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
function hasMethod(obj, name) {
|
|
300
|
+
return Boolean(obj) && typeof obj[name] === 'function';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function clamp(value, min, max) {
|
|
304
|
+
return Math.max(min, Math.min(max, value));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeDifficultyIndex(index) {
|
|
308
|
+
const len = DIFFICULTY_PRESETS.length;
|
|
309
|
+
if (len <= 0) return 0;
|
|
310
|
+
return ((index % len) + len) % len;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getSelectedDifficulty() {
|
|
314
|
+
return DIFFICULTY_PRESETS[normalizeDifficultyIndex(selectedDifficultyIndex)] || DIFFICULTY_PRESETS[0];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function applyDifficultySettings() {
|
|
318
|
+
const d = getSelectedDifficulty();
|
|
319
|
+
activeDifficulty = d;
|
|
320
|
+
activeApproachTime = APPROACH_TIME * d.approachScale;
|
|
321
|
+
activeWindowPerfect = WINDOW_PERFECT * d.timingScale;
|
|
322
|
+
activeWindowGood = WINDOW_GOOD * d.timingScale;
|
|
323
|
+
activeWindowOk = WINDOW_OK * d.timingScale;
|
|
324
|
+
activeWindowMiss = WINDOW_MISS * d.timingScale;
|
|
325
|
+
activeMainBeats = Math.max(48, Math.floor(MAP_MAIN_BEATS * d.beatScale));
|
|
326
|
+
activeDragChainBeatFactor = DRAG_CHAIN_BEAT_FACTOR * d.dragScale;
|
|
327
|
+
activeHitPickRadius = HIT_PICK_RADIUS * d.pickScale;
|
|
328
|
+
activeDragPickRadius = DRAG_PICK_RADIUS * d.pickScale;
|
|
329
|
+
activeEarlyAssist = EARLY_ASSIST * d.timingScale;
|
|
330
|
+
activeDragEarlyAssist = DRAG_EARLY_ASSIST * d.timingScale;
|
|
331
|
+
activePressBuffer = PRESS_BUFFER * clamp((d.timingScale * 0.86) + (d.pickScale * 0.14), 0.76, 1.35);
|
|
332
|
+
activeBonkEarlyMax = BONK_EARLY_MAX * clamp(d.timingScale, 0.72, 1.3);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function setDifficultyIndex(index) {
|
|
336
|
+
const next = normalizeDifficultyIndex(index);
|
|
337
|
+
if (next === selectedDifficultyIndex) return false;
|
|
338
|
+
selectedDifficultyIndex = next;
|
|
339
|
+
applyDifficultySettings();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function cycleDifficulty(step) {
|
|
344
|
+
return setDifficultyIndex(selectedDifficultyIndex + step);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function rgb(r, g, b) {
|
|
348
|
+
if (typeof aura.rgb === 'function') return aura.rgb(r, g, b);
|
|
349
|
+
return { r, g, b, a: 1 };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function rgba(r, g, b, a) {
|
|
353
|
+
if (typeof aura.rgba === 'function') return aura.rgba(r, g, b, a);
|
|
354
|
+
return { r, g, b, a };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function white() {
|
|
358
|
+
if (aura.Color && aura.Color.WHITE) return aura.Color.WHITE;
|
|
359
|
+
return rgba(1, 1, 1, 1);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function canUseAssetRuntime() {
|
|
363
|
+
return Boolean(aura.assets) && hasMethod(aura.assets, 'load');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function canUseAudioRuntime() {
|
|
367
|
+
return Boolean(aura.audio)
|
|
368
|
+
&& hasMethod(aura.audio, 'play')
|
|
369
|
+
&& hasMethod(aura.audio, 'stop');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function canUseSpriteRuntime() {
|
|
373
|
+
return Boolean(aura.draw2d)
|
|
374
|
+
&& (hasMethod(aura.draw2d, 'sprite') || hasMethod(aura.draw2d, 'image'));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function loadAssetIfPossible(path) {
|
|
378
|
+
if (!canUseAssetRuntime()) return false;
|
|
379
|
+
try {
|
|
380
|
+
await aura.assets.load(path);
|
|
381
|
+
return true;
|
|
382
|
+
} catch {}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await aura.assets.load([path]);
|
|
386
|
+
return true;
|
|
387
|
+
} catch {}
|
|
388
|
+
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function loadBgmAsset() {
|
|
393
|
+
if (bgmLoadAttempted) return bgmLoaded;
|
|
394
|
+
bgmLoadAttempted = true;
|
|
395
|
+
bgmLoaded = await loadAssetIfPossible(BGM_ASSET_PATH);
|
|
396
|
+
return bgmLoaded;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function loadBackgroundAsset() {
|
|
400
|
+
if (backgroundLoadAttempted) return backgroundLoaded;
|
|
401
|
+
backgroundLoadAttempted = true;
|
|
402
|
+
backgroundLoaded = await loadAssetIfPossible(BACKGROUND_ASSET_PATH);
|
|
403
|
+
return backgroundLoaded;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function loadSkinAssets() {
|
|
407
|
+
if (skinLoadAttempted) return skinLoaded.size > 0;
|
|
408
|
+
skinLoadAttempted = true;
|
|
409
|
+
const names = Object.keys(SKIN_ASSET_PATHS);
|
|
410
|
+
const paths = names.map((name) => SKIN_ASSET_PATHS[name]);
|
|
411
|
+
const loaded = await Promise.all(paths.map((path) => loadAssetIfPossible(path)));
|
|
412
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
413
|
+
if (loaded[i]) skinLoaded.add(names[i]);
|
|
414
|
+
}
|
|
415
|
+
return skinLoaded.size > 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function loadSfxAssets() {
|
|
419
|
+
if (sfxLoadAttempted) return sfxLoaded.size > 0;
|
|
420
|
+
sfxLoadAttempted = true;
|
|
421
|
+
const names = Object.keys(SFX_PATHS);
|
|
422
|
+
const files = names.map((name) => SFX_PATHS[name]);
|
|
423
|
+
const loaded = await Promise.all(files.map((path) => loadAssetIfPossible(path)));
|
|
424
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
425
|
+
if (loaded[i]) sfxLoaded.add(names[i]);
|
|
426
|
+
}
|
|
427
|
+
return sfxLoaded.size > 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function playSfx(name, volumeScale = 1) {
|
|
431
|
+
if (sfxMuted) return;
|
|
432
|
+
if (!canUseAudioRuntime()) return;
|
|
433
|
+
if (!sfxLoaded.has(name)) return;
|
|
434
|
+
const now = pulseTime;
|
|
435
|
+
const minInterval = Number.isFinite(SFX_MIN_INTERVAL[name]) ? SFX_MIN_INTERVAL[name] : 0;
|
|
436
|
+
const last = sfxLastPlay.has(name) ? sfxLastPlay.get(name) : -999;
|
|
437
|
+
if ((now - last) < minInterval) return;
|
|
438
|
+
sfxLastPlay.set(name, now);
|
|
439
|
+
|
|
440
|
+
const base = Number.isFinite(SFX_VOLUME[name]) ? SFX_VOLUME[name] : 0.5;
|
|
441
|
+
const volume = clamp(base * volumeScale, 0, 1);
|
|
442
|
+
try {
|
|
443
|
+
aura.audio.play(SFX_PATHS[name], {
|
|
444
|
+
loop: false,
|
|
445
|
+
volume,
|
|
446
|
+
});
|
|
447
|
+
} catch {}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function toggleSfxMute() {
|
|
451
|
+
const next = !sfxMuted;
|
|
452
|
+
if (!next) {
|
|
453
|
+
playSfx('uiPause');
|
|
454
|
+
}
|
|
455
|
+
sfxMuted = next;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function accentForNoteTime(noteTime, mapStart, beat) {
|
|
459
|
+
const beatPos = (noteTime - mapStart) / beat;
|
|
460
|
+
const nearestBeat = Math.round(beatPos);
|
|
461
|
+
const beatErr = Math.abs(beatPos - nearestBeat);
|
|
462
|
+
if (beatErr <= 0.08) {
|
|
463
|
+
const mod16 = ((nearestBeat % 16) + 16) % 16;
|
|
464
|
+
const mod4 = ((nearestBeat % 4) + 4) % 4;
|
|
465
|
+
if (mod16 === 0) return 'finish';
|
|
466
|
+
if (mod4 === 1 || mod4 === 3) return 'clap';
|
|
467
|
+
return 'normal';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const nearestHalf = Math.round(beatPos * 2) / 2;
|
|
471
|
+
const halfErr = Math.abs(beatPos - nearestHalf);
|
|
472
|
+
if (halfErr <= 0.08 && (Math.abs(nearestHalf % 1) > 0.001)) {
|
|
473
|
+
return 'whistle';
|
|
474
|
+
}
|
|
475
|
+
return 'normal';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function playNoteHitsound(note, judgement) {
|
|
479
|
+
if (!note || judgement === 'miss') return;
|
|
480
|
+
|
|
481
|
+
const hasPrev = note.dragPrevIndex !== null && note.dragPrevIndex !== undefined;
|
|
482
|
+
const hasNext = note.dragNextIndex !== null && note.dragNextIndex !== undefined;
|
|
483
|
+
|
|
484
|
+
if (hasPrev && hasNext) {
|
|
485
|
+
playSfx('sliderTick');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!hasPrev && hasNext) {
|
|
489
|
+
playSfx('sliderStart');
|
|
490
|
+
if (note.hitsound === 'clap') playSfx('clap', 0.65);
|
|
491
|
+
else if (note.hitsound === 'finish') playSfx('finish', 0.68);
|
|
492
|
+
else if (note.hitsound === 'whistle') playSfx('whistle', 0.62);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (hasPrev && !hasNext) {
|
|
496
|
+
playSfx('sliderEnd');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const accent = typeof note.hitsound === 'string' ? note.hitsound : 'normal';
|
|
501
|
+
if (accent === 'clap') {
|
|
502
|
+
playSfx('clap');
|
|
503
|
+
} else if (accent === 'finish') {
|
|
504
|
+
playSfx('finish');
|
|
505
|
+
} else if (accent === 'whistle') {
|
|
506
|
+
playSfx('whistle');
|
|
507
|
+
} else {
|
|
508
|
+
playSfx('normal');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function stopBgm() {
|
|
513
|
+
if (bgmHandle < 0) return;
|
|
514
|
+
if (!canUseAudioRuntime()) {
|
|
515
|
+
bgmHandle = -1;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
aura.audio.stop(bgmHandle);
|
|
520
|
+
} catch {}
|
|
521
|
+
bgmHandle = -1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function applyBgmVolume() {
|
|
525
|
+
if (!canUseAudioRuntime()) return;
|
|
526
|
+
const target = bgmMuted ? 0 : 1;
|
|
527
|
+
if (hasMethod(aura.audio, 'setMasterVolume')) {
|
|
528
|
+
try {
|
|
529
|
+
aura.audio.setMasterVolume(1);
|
|
530
|
+
} catch {}
|
|
531
|
+
}
|
|
532
|
+
if (bgmHandle >= 0 && hasMethod(aura.audio, 'setVolume')) {
|
|
533
|
+
try {
|
|
534
|
+
aura.audio.setVolume(bgmHandle, target);
|
|
535
|
+
} catch {}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function startBgm(restart = false) {
|
|
540
|
+
if (!canUseAudioRuntime() || !bgmLoaded) return;
|
|
541
|
+
if (restart) stopBgm();
|
|
542
|
+
if (bgmHandle >= 0) return;
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const handle = aura.audio.play(BGM_ASSET_PATH, {
|
|
546
|
+
loop: true,
|
|
547
|
+
volume: BGM_VOLUME,
|
|
548
|
+
});
|
|
549
|
+
if (Number.isFinite(handle) && handle >= 0) {
|
|
550
|
+
bgmHandle = Math.floor(handle);
|
|
551
|
+
applyBgmVolume();
|
|
552
|
+
bgmLastError = '';
|
|
553
|
+
} else {
|
|
554
|
+
bgmHandle = -1;
|
|
555
|
+
bgmLastError = 'play() returned invalid handle';
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
bgmHandle = -1;
|
|
559
|
+
bgmLastError = String(error);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function beginPlayingRun() {
|
|
564
|
+
playSfx('uiStart');
|
|
565
|
+
applyDifficultySettings();
|
|
566
|
+
resetRun();
|
|
567
|
+
state = 'playing';
|
|
568
|
+
startBgm(false);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function toggleBgmMute() {
|
|
572
|
+
bgmMuted = !bgmMuted;
|
|
573
|
+
applyBgmVolume();
|
|
574
|
+
playSfx('uiPause', 0.9);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function keyPressedLatch(keys) {
|
|
578
|
+
let pressed = false;
|
|
579
|
+
for (const key of keys) {
|
|
580
|
+
const down = aura.input.isKeyDown(key);
|
|
581
|
+
const was = pressLatch.get(key) === true;
|
|
582
|
+
if (down && !was) pressed = true;
|
|
583
|
+
pressLatch.set(key, down);
|
|
584
|
+
}
|
|
585
|
+
return pressed;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function primePressLatch() {
|
|
589
|
+
for (const key of ['enter', 'space', 'escape', 'z', 'x', 'm', 'n', 'h', 'up', 'down', 'w', 's', '1', '2', '3', '4']) {
|
|
590
|
+
pressLatch.set(key, aura.input.isKeyDown(key));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function inputPressed(keys) {
|
|
595
|
+
if (hasMethod(aura.input, 'isKeyPressed')) {
|
|
596
|
+
for (const key of keys) {
|
|
597
|
+
if (aura.input.isKeyPressed(key)) return true;
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return keyPressedLatch(keys);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function mouseHitPressed() {
|
|
605
|
+
if (hasMethod(aura.input, 'isMousePressed') && aura.input.isMousePressed(0)) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function mouseHitDown() {
|
|
612
|
+
if (hasMethod(aura.input, 'isMouseDown')) {
|
|
613
|
+
return aura.input.isMouseDown(0);
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function keyboardHitPressed() {
|
|
619
|
+
return keyboardK1Pressed() || keyboardK2Pressed();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function keyboardHitDown() {
|
|
623
|
+
return keyboardK1Down() || keyboardK2Down();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function keyboardK1Pressed() {
|
|
627
|
+
return inputPressed(['z']);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function keyboardK2Pressed() {
|
|
631
|
+
return inputPressed(['x', 'space']);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function keyboardK1Down() {
|
|
635
|
+
return aura.input.isKeyDown('z');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function keyboardK2Down() {
|
|
639
|
+
return aura.input.isKeyDown('x') || aura.input.isKeyDown('space');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function startPressed() {
|
|
643
|
+
return inputPressed(['enter', 'space']);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function escapePressed() {
|
|
647
|
+
return inputPressed(['escape']);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function pointInRect(px, py, x, y, w, h) {
|
|
651
|
+
return px >= x && py >= y && px <= (x + w) && py <= (y + h);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function handleTitleDifficultyInput() {
|
|
655
|
+
let changed = false;
|
|
656
|
+
|
|
657
|
+
if (inputPressed(['up', 'w'])) changed = cycleDifficulty(-1) || changed;
|
|
658
|
+
if (inputPressed(['down', 's'])) changed = cycleDifficulty(1) || changed;
|
|
659
|
+
|
|
660
|
+
if (inputPressed(['1'])) changed = setDifficultyIndex(0) || changed;
|
|
661
|
+
if (inputPressed(['2'])) changed = setDifficultyIndex(1) || changed;
|
|
662
|
+
if (inputPressed(['3'])) changed = setDifficultyIndex(2) || changed;
|
|
663
|
+
if (inputPressed(['4'])) changed = setDifficultyIndex(3) || changed;
|
|
664
|
+
|
|
665
|
+
if (mouseHitPressed()) {
|
|
666
|
+
const mouse = getMouse();
|
|
667
|
+
for (const chip of titleDifficultyBoxes) {
|
|
668
|
+
if (!pointInRect(mouse.x, mouse.y, chip.x, chip.y, chip.w, chip.h)) continue;
|
|
669
|
+
changed = setDifficultyIndex(chip.index) || changed;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (changed) {
|
|
675
|
+
playSfx('uiPause', 0.82);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function getMouse() {
|
|
680
|
+
if (!hasMethod(aura.input, 'getMousePosition')) {
|
|
681
|
+
return { x: playX + (playW * 0.5), y: playY + (playH * 0.5) };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const pos = aura.input.getMousePosition();
|
|
685
|
+
const x = Number.isFinite(pos?.x) ? pos.x : playX + (playW * 0.5);
|
|
686
|
+
const y = Number.isFinite(pos?.y) ? pos.y : playY + (playH * 0.5);
|
|
687
|
+
return { x, y };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function notePosition(note) {
|
|
691
|
+
return {
|
|
692
|
+
x: playX + (note.nx * playW),
|
|
693
|
+
y: playY + (note.ny * playH),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function evaluateTimingDelta(delta) {
|
|
698
|
+
const adjusted = delta - TIMING_OFFSET;
|
|
699
|
+
const ad = Math.abs(adjusted);
|
|
700
|
+
if (ad <= activeWindowPerfect) return 'perfect';
|
|
701
|
+
if (ad <= activeWindowGood) return 'good';
|
|
702
|
+
if (ad <= activeWindowOk) return 'ok';
|
|
703
|
+
if (ad <= activeWindowMiss) return 'miss';
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function applyJudgement(judgement, hitPos, options = {}) {
|
|
708
|
+
const viaDrag = options.viaDrag === true;
|
|
709
|
+
const comboBefore = combo;
|
|
710
|
+
const base = JUDGEMENT_POINTS[judgement];
|
|
711
|
+
const comboBonus = judgement === 'miss' ? 0 : Math.floor(base * clamp(combo * 0.075, 0, 2.5));
|
|
712
|
+
const feverActive = combo >= FEVER_COMBO_THRESHOLD;
|
|
713
|
+
const feverMult = feverActive ? FEVER_MULTIPLIER : 1;
|
|
714
|
+
|
|
715
|
+
achievedPoints += base;
|
|
716
|
+
totalJudged += 1;
|
|
717
|
+
|
|
718
|
+
if (judgement === 'perfect') {
|
|
719
|
+
combo += 1;
|
|
720
|
+
perfectCount += 1;
|
|
721
|
+
hits += 1;
|
|
722
|
+
health = clamp(health + 3.4, 0, HEALTH_MAX);
|
|
723
|
+
score += Math.floor((base + comboBonus) * feverMult);
|
|
724
|
+
beatGlow = Math.max(beatGlow, 0.18);
|
|
725
|
+
comboFlash = Math.max(comboFlash, 0.24);
|
|
726
|
+
screenShake = Math.max(screenShake, 0.05);
|
|
727
|
+
if (viaDrag) {
|
|
728
|
+
score += 35;
|
|
729
|
+
flowMeter = clamp(flowMeter + 0.13, 0, 1);
|
|
730
|
+
comboFlash = Math.max(comboFlash, 0.28);
|
|
731
|
+
}
|
|
732
|
+
} else if (judgement === 'good') {
|
|
733
|
+
combo += 1;
|
|
734
|
+
goodCount += 1;
|
|
735
|
+
hits += 1;
|
|
736
|
+
health = clamp(health + 1.7, 0, HEALTH_MAX);
|
|
737
|
+
score += Math.floor((base + comboBonus) * feverMult);
|
|
738
|
+
beatGlow = Math.max(beatGlow, 0.11);
|
|
739
|
+
comboFlash = Math.max(comboFlash, 0.18);
|
|
740
|
+
if (viaDrag) {
|
|
741
|
+
score += 20;
|
|
742
|
+
flowMeter = clamp(flowMeter + 0.1, 0, 1);
|
|
743
|
+
comboFlash = Math.max(comboFlash, 0.22);
|
|
744
|
+
}
|
|
745
|
+
} else if (judgement === 'ok') {
|
|
746
|
+
combo += 1;
|
|
747
|
+
okCount += 1;
|
|
748
|
+
hits += 1;
|
|
749
|
+
health = clamp(health + 0.6, 0, HEALTH_MAX);
|
|
750
|
+
score += Math.floor((base + comboBonus) * feverMult);
|
|
751
|
+
comboFlash = Math.max(comboFlash, 0.12);
|
|
752
|
+
if (viaDrag) {
|
|
753
|
+
score += 10;
|
|
754
|
+
flowMeter = clamp(flowMeter + 0.06, 0, 1);
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
combo = 0;
|
|
758
|
+
misses += 1;
|
|
759
|
+
health = clamp(health - 10.5, 0, HEALTH_MAX);
|
|
760
|
+
beatGlow = Math.max(beatGlow, 0.14);
|
|
761
|
+
screenShake = Math.max(screenShake, 0.23);
|
|
762
|
+
flowMeter = Math.max(0, flowMeter - 0.2);
|
|
763
|
+
playSfx('miss');
|
|
764
|
+
if (comboBefore >= 18) {
|
|
765
|
+
playSfx('comboBreak');
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (combo > maxCombo) maxCombo = combo;
|
|
770
|
+
|
|
771
|
+
const color = judgement === 'perfect'
|
|
772
|
+
? rgba(0.43, 1.0, 0.64, 1)
|
|
773
|
+
: judgement === 'good'
|
|
774
|
+
? rgba(0.47, 0.86, 1.0, 1)
|
|
775
|
+
: judgement === 'ok'
|
|
776
|
+
? rgba(1.0, 0.88, 0.42, 1)
|
|
777
|
+
: rgba(1.0, 0.36, 0.38, 1);
|
|
778
|
+
const burstImpact = judgement === 'perfect'
|
|
779
|
+
? 1.06
|
|
780
|
+
: judgement === 'good'
|
|
781
|
+
? 0.92
|
|
782
|
+
: judgement === 'ok'
|
|
783
|
+
? 0.78
|
|
784
|
+
: 0.68;
|
|
785
|
+
|
|
786
|
+
judgementPop = {
|
|
787
|
+
text: viaDrag && judgement !== 'miss' ? `DRAG ${judgement.toUpperCase()}` : judgement.toUpperCase(),
|
|
788
|
+
age: 0,
|
|
789
|
+
x: hitPos.x,
|
|
790
|
+
y: hitPos.y,
|
|
791
|
+
color,
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
bursts.push({
|
|
795
|
+
x: hitPos.x,
|
|
796
|
+
y: hitPos.y,
|
|
797
|
+
age: 0,
|
|
798
|
+
color,
|
|
799
|
+
judgement,
|
|
800
|
+
impact: viaDrag && judgement !== 'miss' ? burstImpact + 0.08 : burstImpact,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
if (combo > 0 && combo % 25 === 0) {
|
|
804
|
+
judgementPop = {
|
|
805
|
+
text: `STREAK x${combo}`,
|
|
806
|
+
age: -0.1,
|
|
807
|
+
x: playX + (playW * 0.5),
|
|
808
|
+
y: playY + 42,
|
|
809
|
+
color: rgba(1.0, 0.9, 0.38, 1),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function mapPoint(idx) {
|
|
815
|
+
// A hand-tuned 4x3-ish distribution that feels close to rhythm-map spacing.
|
|
816
|
+
const points = [
|
|
817
|
+
[0.12, 0.15], [0.35, 0.13], [0.58, 0.14], [0.83, 0.17],
|
|
818
|
+
[0.16, 0.36], [0.39, 0.34], [0.62, 0.33], [0.84, 0.38],
|
|
819
|
+
[0.11, 0.57], [0.34, 0.56], [0.6, 0.55], [0.86, 0.58],
|
|
820
|
+
[0.17, 0.79], [0.41, 0.78], [0.64, 0.77], [0.87, 0.8],
|
|
821
|
+
];
|
|
822
|
+
return points[idx % points.length];
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function createBeatmap() {
|
|
826
|
+
const map = [];
|
|
827
|
+
const beat = ANALYZED_BEAT;
|
|
828
|
+
const start = MAP_START_TIME;
|
|
829
|
+
const mainBeats = activeMainBeats;
|
|
830
|
+
const d = activeDifficulty;
|
|
831
|
+
const minJump = Math.max(1, Math.floor(Number.isFinite(d.minJump) ? d.minJump : 3));
|
|
832
|
+
const burstInterval = Math.max(6, Math.floor(Number.isFinite(d.burstInterval) ? d.burstInterval : 12));
|
|
833
|
+
const burstPrimaryBeat = Math.max(1, Math.floor(burstInterval * 0.42));
|
|
834
|
+
const burstSecondaryBeat = Math.max(burstPrimaryBeat + 1, Math.floor(burstInterval * 0.75));
|
|
835
|
+
const burstChance = clamp(Number.isFinite(d.burstChance) ? d.burstChance : 0.72, 0, 1);
|
|
836
|
+
const allowBurstSecondary = d.burstSecondary !== false;
|
|
837
|
+
const tripleStartBeat = Math.max(8, Math.floor(Number.isFinite(d.tripleStartBeat) ? d.tripleStartBeat : 120));
|
|
838
|
+
const tripleChance = clamp(Number.isFinite(d.tripleChance) ? d.tripleChance : 0.38, 0, 1);
|
|
839
|
+
const tripleCount = Math.max(1, Math.floor(Number.isFinite(d.tripleCount) ? d.tripleCount : 2));
|
|
840
|
+
const offbeatChance = clamp(Number.isFinite(d.offbeatChance) ? d.offbeatChance : 0.12, 0, 1);
|
|
841
|
+
const offbeatOffset = clamp(Number.isFinite(d.offbeatOffset) ? d.offbeatOffset : 0.5, 0.2, 0.75);
|
|
842
|
+
const tripleModulo = Math.max(9, burstInterval + 5);
|
|
843
|
+
const triplePhase = tripleModulo - 2;
|
|
844
|
+
const dragLinkChance = clamp(Number.isFinite(d.dragLinkChance) ? d.dragLinkChance : 0.72, 0, 1);
|
|
845
|
+
const densityLevel = clamp(Math.floor(Number.isFinite(d.densityLevel) ? d.densityLevel : 1), 0, 3);
|
|
846
|
+
const extraAttemptsByDensity = [0, 1, 2, 3];
|
|
847
|
+
const extraChanceByDensity = [0.0, 0.28, 0.62, 0.92];
|
|
848
|
+
const extraOffsetPoolByDensity = [
|
|
849
|
+
[],
|
|
850
|
+
[0.25, 0.75],
|
|
851
|
+
[0.25, 0.5, 0.75, 0.125, 0.625],
|
|
852
|
+
[0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875],
|
|
853
|
+
];
|
|
854
|
+
const extraAttempts = extraAttemptsByDensity[densityLevel];
|
|
855
|
+
const extraChance = extraChanceByDensity[densityLevel];
|
|
856
|
+
const extraOffsetPool = extraOffsetPoolByDensity[densityLevel];
|
|
857
|
+
|
|
858
|
+
let seed = 0xC0FFEE;
|
|
859
|
+
let lastIdx = 5;
|
|
860
|
+
|
|
861
|
+
function rand() {
|
|
862
|
+
seed = ((seed * 1664525) + 1013904223) >>> 0;
|
|
863
|
+
return seed / 4294967296;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function pickIndex() {
|
|
867
|
+
let idx = Math.floor(rand() * 16);
|
|
868
|
+
// Encourage jumpy motion and avoid accidental stacks.
|
|
869
|
+
for (let i = 0; i < 6; i += 1) {
|
|
870
|
+
if (Math.abs(idx - lastIdx) >= minJump) break;
|
|
871
|
+
idx = Math.floor(rand() * 16);
|
|
872
|
+
}
|
|
873
|
+
lastIdx = idx;
|
|
874
|
+
return idx;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
for (let i = 0; i < mainBeats; i += 1) {
|
|
878
|
+
const t = start + (i * beat);
|
|
879
|
+
const idx = pickIndex();
|
|
880
|
+
const p = mapPoint(idx);
|
|
881
|
+
let addedHalfBeat = false;
|
|
882
|
+
const occupiedOffsets = new Set([0]);
|
|
883
|
+
map.push({
|
|
884
|
+
time: t,
|
|
885
|
+
nx: p[0],
|
|
886
|
+
ny: p[1],
|
|
887
|
+
judged: false,
|
|
888
|
+
judgement: null,
|
|
889
|
+
colorIndex: i % PALETTE.length,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Controlled bursts for higher density sections.
|
|
893
|
+
const burstSlot = i % burstInterval;
|
|
894
|
+
if ((burstSlot === burstPrimaryBeat || (allowBurstSecondary && burstSlot === burstSecondaryBeat))
|
|
895
|
+
&& rand() <= burstChance) {
|
|
896
|
+
const burstIdx = (idx + 5 + Math.floor(rand() * 5)) % 16;
|
|
897
|
+
const b = mapPoint(burstIdx);
|
|
898
|
+
map.push({
|
|
899
|
+
time: t + (beat * 0.5),
|
|
900
|
+
nx: b[0],
|
|
901
|
+
ny: b[1],
|
|
902
|
+
judged: false,
|
|
903
|
+
judgement: null,
|
|
904
|
+
colorIndex: (i + 1) % PALETTE.length,
|
|
905
|
+
});
|
|
906
|
+
addedHalfBeat = true;
|
|
907
|
+
occupiedOffsets.add(0.5);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Extra off-beat taps appear earlier on higher difficulties.
|
|
911
|
+
if (offbeatChance > 0 && i > 6 && rand() < offbeatChance) {
|
|
912
|
+
const offIdx = (idx + 3 + Math.floor(rand() * 9)) % 16;
|
|
913
|
+
const off = mapPoint(offIdx);
|
|
914
|
+
const offset = addedHalfBeat && offbeatOffset >= 0.49 ? 0.25 : offbeatOffset;
|
|
915
|
+
occupiedOffsets.add(offset);
|
|
916
|
+
map.push({
|
|
917
|
+
time: t + (beat * offset),
|
|
918
|
+
nx: off[0],
|
|
919
|
+
ny: off[1],
|
|
920
|
+
judged: false,
|
|
921
|
+
judgement: null,
|
|
922
|
+
colorIndex: (i + 2) % PALETTE.length,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Additional density layer for harder difficulties.
|
|
927
|
+
if (extraAttempts > 0 && extraOffsetPool.length > 0 && i > 4) {
|
|
928
|
+
for (let e = 0; e < extraAttempts; e += 1) {
|
|
929
|
+
if (rand() > extraChance) continue;
|
|
930
|
+
const available = extraOffsetPool.filter((offset) => !occupiedOffsets.has(offset));
|
|
931
|
+
if (available.length <= 0) break;
|
|
932
|
+
const offset = available[Math.floor(rand() * available.length)];
|
|
933
|
+
occupiedOffsets.add(offset);
|
|
934
|
+
|
|
935
|
+
const exIdx = (idx + 2 + Math.floor(rand() * 12)) % 16;
|
|
936
|
+
const ex = mapPoint(exIdx);
|
|
937
|
+
map.push({
|
|
938
|
+
time: t + (beat * offset),
|
|
939
|
+
nx: ex[0],
|
|
940
|
+
ny: ex[1],
|
|
941
|
+
judged: false,
|
|
942
|
+
judgement: null,
|
|
943
|
+
colorIndex: (i + 3 + e) % PALETTE.length,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// End sections with short triples.
|
|
949
|
+
if (i > tripleStartBeat && i % tripleModulo === triplePhase && rand() < tripleChance) {
|
|
950
|
+
for (let j = 1; j <= tripleCount; j += 1) {
|
|
951
|
+
const sIdx = (idx + (j * 3) + Math.floor(rand() * 2)) % 16;
|
|
952
|
+
const s = mapPoint(sIdx);
|
|
953
|
+
map.push({
|
|
954
|
+
time: t + (beat * (0.25 * j)),
|
|
955
|
+
nx: s[0],
|
|
956
|
+
ny: s[1],
|
|
957
|
+
judged: false,
|
|
958
|
+
judgement: null,
|
|
959
|
+
colorIndex: (i + j) % PALETTE.length,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
map.sort((a, b) => a.time - b.time);
|
|
966
|
+
|
|
967
|
+
for (let i = 0; i < map.length; i += 1) {
|
|
968
|
+
map[i].index = i;
|
|
969
|
+
map[i].dragPrevIndex = null;
|
|
970
|
+
map[i].dragNextIndex = null;
|
|
971
|
+
map[i].hitsound = accentForNoteTime(map[i].time, start, beat);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
for (let i = 0; i < map.length - 1; i += 1) {
|
|
975
|
+
const curr = map[i];
|
|
976
|
+
const next = map[i + 1];
|
|
977
|
+
const gap = next.time - curr.time;
|
|
978
|
+
if (gap <= (beat * activeDragChainBeatFactor)
|
|
979
|
+
&& rand() <= dragLinkChance
|
|
980
|
+
&& ((i + (i >> 1)) % 5 !== 0)) {
|
|
981
|
+
curr.dragNextIndex = next.index;
|
|
982
|
+
next.dragPrevIndex = curr.index;
|
|
983
|
+
next.colorIndex = curr.colorIndex;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Smooth drag chain positions so sliders follow natural arcs
|
|
988
|
+
for (let i = 0; i < map.length; i += 1) {
|
|
989
|
+
if (map[i].dragNextIndex === null || map[i].dragNextIndex === undefined) continue;
|
|
990
|
+
if (map[i].dragPrevIndex !== null && map[i].dragPrevIndex !== undefined) continue;
|
|
991
|
+
// Start of a drag chain - collect all notes in the chain
|
|
992
|
+
const chain = [map[i]];
|
|
993
|
+
let cur = map[i];
|
|
994
|
+
while (cur.dragNextIndex !== null && cur.dragNextIndex !== undefined) {
|
|
995
|
+
cur = map[cur.dragNextIndex];
|
|
996
|
+
chain.push(cur);
|
|
997
|
+
}
|
|
998
|
+
if (chain.length < 2) continue;
|
|
999
|
+
// Interpolate intermediate notes along a smooth arc
|
|
1000
|
+
const sx = chain[0].nx;
|
|
1001
|
+
const sy = chain[0].ny;
|
|
1002
|
+
const ex = chain[chain.length - 1].nx;
|
|
1003
|
+
const ey = chain[chain.length - 1].ny;
|
|
1004
|
+
const drift = 0.1 + (((chain[0].index * 7) % 11) / 11) * 0.14;
|
|
1005
|
+
const perpX = -(ey - sy);
|
|
1006
|
+
const perpY = ex - sx;
|
|
1007
|
+
const sign = chain[0].index % 2 === 0 ? 1 : -1;
|
|
1008
|
+
for (let j = 1; j < chain.length; j += 1) {
|
|
1009
|
+
const frac = j / (chain.length - 1);
|
|
1010
|
+
const arc = Math.sin(frac * Math.PI) * drift * sign;
|
|
1011
|
+
chain[j].nx = clamp(sx + ((ex - sx) * frac) + (perpX * arc), 0.06, 0.94);
|
|
1012
|
+
chain[j].ny = clamp(sy + ((ey - sy) * frac) + (perpY * arc), 0.06, 0.94);
|
|
1013
|
+
}
|
|
1014
|
+
// Skip to end of chain
|
|
1015
|
+
i = chain[chain.length - 1].index;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return map;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function resetRun() {
|
|
1022
|
+
applyDifficultySettings();
|
|
1023
|
+
notes = createBeatmap();
|
|
1024
|
+
currentTime = 0;
|
|
1025
|
+
lastNoteTime = notes.length > 0 ? notes[notes.length - 1].time : 0;
|
|
1026
|
+
|
|
1027
|
+
score = 0;
|
|
1028
|
+
combo = 0;
|
|
1029
|
+
maxCombo = 0;
|
|
1030
|
+
hits = 0;
|
|
1031
|
+
misses = 0;
|
|
1032
|
+
totalJudged = 0;
|
|
1033
|
+
totalPointValue = notes.length * JUDGEMENT_POINTS.perfect;
|
|
1034
|
+
achievedPoints = 0;
|
|
1035
|
+
health = HEALTH_MAX;
|
|
1036
|
+
|
|
1037
|
+
perfectCount = 0;
|
|
1038
|
+
goodCount = 0;
|
|
1039
|
+
okCount = 0;
|
|
1040
|
+
|
|
1041
|
+
bursts = [];
|
|
1042
|
+
judgementPop = null;
|
|
1043
|
+
failed = false;
|
|
1044
|
+
beatGlow = 0;
|
|
1045
|
+
comboFlash = 0;
|
|
1046
|
+
screenShake = 0;
|
|
1047
|
+
cursorTrail = [];
|
|
1048
|
+
flowMeter = 0;
|
|
1049
|
+
hitBufferTimer = 0;
|
|
1050
|
+
dragChainTimer = 0;
|
|
1051
|
+
dragExpectedIndex = -1;
|
|
1052
|
+
mouseHoldActive = false;
|
|
1053
|
+
keyHoldActive = false;
|
|
1054
|
+
hitPressedThisFrame = false;
|
|
1055
|
+
k1Down = false;
|
|
1056
|
+
k2Down = false;
|
|
1057
|
+
k1Pulse = 0;
|
|
1058
|
+
k2Pulse = 0;
|
|
1059
|
+
k1Flash = 0;
|
|
1060
|
+
k2Flash = 0;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function accuracyPercent() {
|
|
1064
|
+
if (totalPointValue <= 0) return 0;
|
|
1065
|
+
return (achievedPoints / totalPointValue) * 100;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function gradeForResults() {
|
|
1069
|
+
const acc = accuracyPercent();
|
|
1070
|
+
if (failed) return 'F';
|
|
1071
|
+
if (acc >= 98 && misses === 0) return 'SS';
|
|
1072
|
+
if (acc >= 93) return 'S';
|
|
1073
|
+
if (acc >= 86) return 'A';
|
|
1074
|
+
if (acc >= 75) return 'B';
|
|
1075
|
+
if (acc >= 62) return 'C';
|
|
1076
|
+
return 'D';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function assertRuntimeCapabilities() {
|
|
1080
|
+
const missing = [];
|
|
1081
|
+
|
|
1082
|
+
if (!hasMethod(aura.draw2d, 'clear')) missing.push('aura.draw2d.clear');
|
|
1083
|
+
if (!hasMethod(aura.draw2d, 'rectFill')) missing.push('aura.draw2d.rectFill');
|
|
1084
|
+
if (!hasMethod(aura.draw2d, 'circle')) missing.push('aura.draw2d.circle');
|
|
1085
|
+
if (!hasMethod(aura.draw2d, 'circleFill')) missing.push('aura.draw2d.circleFill');
|
|
1086
|
+
if (!hasMethod(aura.draw2d, 'line')) missing.push('aura.draw2d.line');
|
|
1087
|
+
if (!hasMethod(aura.draw2d, 'text')) missing.push('aura.draw2d.text');
|
|
1088
|
+
if (!hasMethod(aura.draw2d, 'measureText')) missing.push('aura.draw2d.measureText');
|
|
1089
|
+
|
|
1090
|
+
if (!hasMethod(aura.input, 'isKeyDown')) missing.push('aura.input.isKeyDown');
|
|
1091
|
+
if (!hasMethod(aura.input, 'getMousePosition')) missing.push('aura.input.getMousePosition');
|
|
1092
|
+
if (!hasMethod(aura.input, 'isMousePressed')) missing.push('aura.input.isMousePressed');
|
|
1093
|
+
if (!hasMethod(aura.window, 'getSize')) missing.push('aura.window.getSize');
|
|
1094
|
+
|
|
1095
|
+
if (missing.length > 0) {
|
|
1096
|
+
throw new Error(`[aurasu] runtime missing required APIs: ${missing.join(', ')}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const small = aura.draw2d.measureText('Probe', { size: 8 });
|
|
1100
|
+
const large = aura.draw2d.measureText('Probe', { size: 24 });
|
|
1101
|
+
if (!Number.isFinite(small.width) || !Number.isFinite(large.width) || large.width <= small.width) {
|
|
1102
|
+
throw new Error('[aurasu] draw2d.measureText appears to be placeholder behavior.');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function syncLayoutFromWindow() {
|
|
1107
|
+
const size = aura.window.getSize();
|
|
1108
|
+
if (!size || !Number.isFinite(size.width) || !Number.isFinite(size.height)) return;
|
|
1109
|
+
|
|
1110
|
+
worldWidth = size.width;
|
|
1111
|
+
worldHeight = size.height;
|
|
1112
|
+
|
|
1113
|
+
const marginX = clamp(worldWidth * 0.09, 70, 180);
|
|
1114
|
+
const marginY = clamp(worldHeight * 0.1, 60, 120);
|
|
1115
|
+
playX = marginX;
|
|
1116
|
+
playY = marginY;
|
|
1117
|
+
playW = worldWidth - (marginX * 2);
|
|
1118
|
+
playH = worldHeight - (marginY * 2);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function drawCentered(text, y, size, color) {
|
|
1122
|
+
const m = aura.draw2d.measureText(text, { size });
|
|
1123
|
+
const x = (worldWidth * 0.5) - (m.width * 0.5);
|
|
1124
|
+
aura.draw2d.text(text, x, y, { size, color });
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function hasSkinAsset(name) {
|
|
1128
|
+
return skinLoaded.has(name) && typeof SKIN_ASSET_PATHS[name] === 'string';
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function drawSkinSprite(name, x, y, options = {}) {
|
|
1132
|
+
if (!canUseSpriteRuntime()) return false;
|
|
1133
|
+
if (!hasSkinAsset(name)) return false;
|
|
1134
|
+
const path = SKIN_ASSET_PATHS[name];
|
|
1135
|
+
const drawOptions = { ...options };
|
|
1136
|
+
const pixelSnap = drawOptions.pixelSnap === true;
|
|
1137
|
+
delete drawOptions.pixelSnap;
|
|
1138
|
+
let width = Number.isFinite(drawOptions.width) ? drawOptions.width : null;
|
|
1139
|
+
let height = Number.isFinite(drawOptions.height) ? drawOptions.height : null;
|
|
1140
|
+
const ox = Number.isFinite(drawOptions.originX) ? drawOptions.originX : 0;
|
|
1141
|
+
const oy = Number.isFinite(drawOptions.originY) ? drawOptions.originY : 0;
|
|
1142
|
+
let drawX = width !== null ? x - (width * ox) : x;
|
|
1143
|
+
let drawY = height !== null ? y - (height * oy) : y;
|
|
1144
|
+
if (pixelSnap) {
|
|
1145
|
+
if (width !== null) {
|
|
1146
|
+
width = Math.max(1, Math.round(width));
|
|
1147
|
+
drawOptions.width = width;
|
|
1148
|
+
}
|
|
1149
|
+
if (height !== null) {
|
|
1150
|
+
height = Math.max(1, Math.round(height));
|
|
1151
|
+
drawOptions.height = height;
|
|
1152
|
+
}
|
|
1153
|
+
drawX = Math.round(drawX);
|
|
1154
|
+
drawY = Math.round(drawY);
|
|
1155
|
+
}
|
|
1156
|
+
delete drawOptions.originX;
|
|
1157
|
+
delete drawOptions.originY;
|
|
1158
|
+
try {
|
|
1159
|
+
if (hasMethod(aura.draw2d, 'sprite')) {
|
|
1160
|
+
aura.draw2d.sprite(path, drawX, drawY, drawOptions);
|
|
1161
|
+
} else {
|
|
1162
|
+
aura.draw2d.image(path, drawX, drawY, drawOptions);
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
} catch {}
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function drawBackground() {
|
|
1170
|
+
aura.draw2d.clear(rgba(0.01, 0.015, 0.05, 1));
|
|
1171
|
+
|
|
1172
|
+
if (backgroundLoaded && canUseSpriteRuntime()) {
|
|
1173
|
+
if (hasMethod(aura.draw2d, 'sprite')) {
|
|
1174
|
+
aura.draw2d.sprite(BACKGROUND_ASSET_PATH, 0, 0, {
|
|
1175
|
+
width: worldWidth,
|
|
1176
|
+
height: worldHeight,
|
|
1177
|
+
alpha: BACKGROUND_IMAGE_ALPHA,
|
|
1178
|
+
});
|
|
1179
|
+
} else {
|
|
1180
|
+
aura.draw2d.image(BACKGROUND_ASSET_PATH, 0, 0, {
|
|
1181
|
+
width: worldWidth,
|
|
1182
|
+
height: worldHeight,
|
|
1183
|
+
alpha: BACKGROUND_IMAGE_ALPHA,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
aura.draw2d.rectFill(0, 0, worldWidth, worldHeight, rgba(0.02, 0.03, 0.08, BACKGROUND_DARK_OVERLAY_ALPHA));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
for (let i = 0; i < BACKGROUND_LAYERS; i += 1) {
|
|
1190
|
+
const t = i / (BACKGROUND_LAYERS - 1);
|
|
1191
|
+
const r = 0.07 + (t * 0.06);
|
|
1192
|
+
const g = 0.08 + (t * 0.08);
|
|
1193
|
+
const b = 0.14 + (t * 0.16);
|
|
1194
|
+
const y = Math.floor((i / BACKGROUND_LAYERS) * worldHeight);
|
|
1195
|
+
const h = Math.ceil(worldHeight / BACKGROUND_LAYERS);
|
|
1196
|
+
aura.draw2d.rectFill(0, y, worldWidth, h, rgba(r, g, b, backgroundLoaded ? BACKGROUND_LAYER_ALPHA_WITH_IMAGE : 1));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
aura.draw2d.rectFill(0, 0, worldWidth, worldHeight, rgba(0, 0, 0, 0.5));
|
|
1200
|
+
|
|
1201
|
+
const vignetteBands = 7;
|
|
1202
|
+
for (let i = 0; i < vignetteBands; i += 1) {
|
|
1203
|
+
const pad = i * 24;
|
|
1204
|
+
const alpha = BACKGROUND_VIGNETTE_ALPHA * (1 - (i / vignetteBands));
|
|
1205
|
+
aura.draw2d.rect(
|
|
1206
|
+
pad,
|
|
1207
|
+
pad,
|
|
1208
|
+
worldWidth - (pad * 2),
|
|
1209
|
+
worldHeight - (pad * 2),
|
|
1210
|
+
rgba(0.01, 0.015, 0.03, alpha)
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
aura.draw2d.rectFill(playX - 6, playY - 6, playW + 12, playH + 12, rgba(0.01, 0.02, 0.05, 0.86));
|
|
1215
|
+
aura.draw2d.rectFill(playX, playY, playW, playH, rgba(0.01, 0.02, 0.05, 0.98));
|
|
1216
|
+
|
|
1217
|
+
for (let c = 0; c <= GRID_COLS; c += 1) {
|
|
1218
|
+
const x = playX + ((c / GRID_COLS) * playW);
|
|
1219
|
+
aura.draw2d.line(x, playY, x, playY + playH, rgba(1, 1, 1, 0.03), 1);
|
|
1220
|
+
}
|
|
1221
|
+
for (let r = 0; r <= GRID_ROWS; r += 1) {
|
|
1222
|
+
const y = playY + ((r / GRID_ROWS) * playH);
|
|
1223
|
+
aura.draw2d.line(playX, y, playX + playW, y, rgba(1, 1, 1, 0.03), 1);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
aura.draw2d.rect(playX - 2, playY - 2, playW + 4, playH + 4, rgba(0.5, 0.76, 1, 0.34));
|
|
1227
|
+
aura.draw2d.rect(playX, playY, playW, playH, rgba(0.88, 0.94, 1, 0.58));
|
|
1228
|
+
|
|
1229
|
+
if (beatGlow > 0) {
|
|
1230
|
+
aura.draw2d.rectFill(0, 0, worldWidth, worldHeight, rgba(0.45, 0.78, 1.0, beatGlow));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function drawHitBursts(dt) {
|
|
1235
|
+
for (const burst of bursts) {
|
|
1236
|
+
burst.age += dt;
|
|
1237
|
+
const impact = Number.isFinite(burst.impact) ? burst.impact : 0.8;
|
|
1238
|
+
const flashGain = VIS_HIT_FLASH_GAIN;
|
|
1239
|
+
const ttl = 0.46 + (impact * 0.1);
|
|
1240
|
+
const life = clamp(1 - (burst.age / ttl), 0, 1);
|
|
1241
|
+
if (life <= 0) continue;
|
|
1242
|
+
|
|
1243
|
+
const r = burst.color.r;
|
|
1244
|
+
const g = burst.color.g;
|
|
1245
|
+
const b = burst.color.b;
|
|
1246
|
+
const shockT = 1 - life;
|
|
1247
|
+
|
|
1248
|
+
// Expanding colored shockwave rings
|
|
1249
|
+
const ringR = (HIT_RADIUS * (0.95 + (impact * 0.08))) + (shockT * (170 + (impact * 100) * flashGain));
|
|
1250
|
+
aura.draw2d.circle(burst.x, burst.y, ringR, rgba(r, g, b, clamp((0.78 + (impact * 0.24)) * life * flashGain, 0, 1)));
|
|
1251
|
+
aura.draw2d.circle(burst.x, burst.y, ringR * 0.78, rgba(1, 1, 1, clamp((0.18 + (impact * 0.28)) * life * flashGain, 0, 1)));
|
|
1252
|
+
aura.draw2d.circle(burst.x, burst.y, ringR * 0.58, rgba(r, g, b, clamp((0.16 + (impact * 0.2)) * life * flashGain, 0, 1)));
|
|
1253
|
+
|
|
1254
|
+
// White additive flash core
|
|
1255
|
+
const flashR = (HIT_RADIUS * (1.35 + (impact * 0.2))) * (1 + ((flashGain - 1) * 0.45)) * clamp(1.5 - (burst.age * 4.5), 0, 1.5);
|
|
1256
|
+
if (flashR > 0.5) {
|
|
1257
|
+
aura.draw2d.circleFill(burst.x, burst.y, flashR, rgba(1, 1, 1, clamp((0.42 + (impact * 0.24)) * life * flashGain, 0, 1)));
|
|
1258
|
+
aura.draw2d.circleFill(burst.x, burst.y, flashR * 0.54, rgba(r, g, b, clamp((0.3 + (impact * 0.25)) * life * flashGain, 0, 1)));
|
|
1259
|
+
}
|
|
1260
|
+
const coreR = HIT_RADIUS * clamp(1.15 - (burst.age * 2.7), 0, 1.15);
|
|
1261
|
+
if (coreR > 0.5) {
|
|
1262
|
+
aura.draw2d.circleFill(
|
|
1263
|
+
burst.x,
|
|
1264
|
+
burst.y,
|
|
1265
|
+
coreR,
|
|
1266
|
+
rgba((r * 0.85) + 0.15, (g * 0.85) + 0.15, (b * 0.85) + 0.15, 0.34 * life)
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Score value popup (300 / 100 / 50)
|
|
1271
|
+
if (burst.judgement !== 'miss' && burst.judgement !== 'bonk') {
|
|
1272
|
+
const pts = burst.judgement === 'perfect' ? '300' : burst.judgement === 'good' ? '100' : '50';
|
|
1273
|
+
const ptsSize = 22 + (life * (6 + (impact * 2)));
|
|
1274
|
+
const ptsY = burst.y - 26 - (burst.age * 65);
|
|
1275
|
+
const m = aura.draw2d.measureText(pts, { size: ptsSize });
|
|
1276
|
+
aura.draw2d.text(pts, burst.x - (m.width * 0.5) + 1, ptsY + 1, { size: ptsSize, color: rgba(0, 0, 0, 0.4 * life) });
|
|
1277
|
+
aura.draw2d.text(pts, burst.x - (m.width * 0.5), ptsY, { size: ptsSize, color: rgba(r, g, b, 0.9 * life) });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
bursts = bursts.filter((burst) => {
|
|
1281
|
+
const impact = Number.isFinite(burst.impact) ? burst.impact : 0.8;
|
|
1282
|
+
return burst.age < (0.46 + (impact * 0.1));
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function drawJudgementPopup(dt) {
|
|
1287
|
+
if (!judgementPop) return;
|
|
1288
|
+
judgementPop.age += dt;
|
|
1289
|
+
const life = clamp(1 - (judgementPop.age / 0.45), 0, 1);
|
|
1290
|
+
if (life <= 0) {
|
|
1291
|
+
judgementPop = null;
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const y = judgementPop.y - 18 - (judgementPop.age * 52);
|
|
1296
|
+
const size = 26 + (life * 8);
|
|
1297
|
+
const col = rgba(judgementPop.color.r, judgementPop.color.g, judgementPop.color.b, 0.95 * life);
|
|
1298
|
+
const textUpper = judgementPop.text.toUpperCase();
|
|
1299
|
+
let judgementSprite = null;
|
|
1300
|
+
if (textUpper.includes('PERFECT')) judgementSprite = 'judgement300';
|
|
1301
|
+
else if (textUpper.includes('GOOD')) judgementSprite = 'judgement100';
|
|
1302
|
+
else if (textUpper.includes('OK')) judgementSprite = 'judgement50';
|
|
1303
|
+
else if (textUpper.includes('MISS')) judgementSprite = 'judgementMiss';
|
|
1304
|
+
|
|
1305
|
+
if (judgementSprite) {
|
|
1306
|
+
const w = 220 + (life * 44);
|
|
1307
|
+
const h = w * 0.5;
|
|
1308
|
+
aura.draw2d.circleFill(judgementPop.x, y + 10, w * 0.28, rgba(col.r, col.g, col.b, 0.08 * life));
|
|
1309
|
+
const drew = drawSkinSprite(judgementSprite, judgementPop.x, y + 10, {
|
|
1310
|
+
width: w,
|
|
1311
|
+
height: h,
|
|
1312
|
+
originX: 0.5,
|
|
1313
|
+
originY: 0.5,
|
|
1314
|
+
alpha: 0.95 * life,
|
|
1315
|
+
});
|
|
1316
|
+
if (drew) return;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const m = aura.draw2d.measureText(judgementPop.text, { size });
|
|
1320
|
+
aura.draw2d.text(judgementPop.text, judgementPop.x - (m.width * 0.5), y, { size, color: col });
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function drawSliderBody(startPos, endPos, activeLink, colorIndex, seed, progress) {
|
|
1324
|
+
const dx = endPos.x - startPos.x;
|
|
1325
|
+
const dy = endPos.y - startPos.y;
|
|
1326
|
+
const len = Math.hypot(dx, dy);
|
|
1327
|
+
if (len <= 0.001) return;
|
|
1328
|
+
|
|
1329
|
+
const perpX = -dy / len;
|
|
1330
|
+
const perpY = dx / len;
|
|
1331
|
+
const bendDir = seed % 2 === 0 ? 1 : -1;
|
|
1332
|
+
const bend = clamp(len * 0.24, 22, 90) * bendDir;
|
|
1333
|
+
const cpx = ((startPos.x + endPos.x) * 0.5) + (perpX * bend);
|
|
1334
|
+
const cpy = ((startPos.y + endPos.y) * 0.5) + (perpY * bend);
|
|
1335
|
+
|
|
1336
|
+
const base = PALETTE[colorIndex % PALETTE.length];
|
|
1337
|
+
const steps = Math.max(22, Math.ceil(len / 6));
|
|
1338
|
+
const bodyR = HIT_RADIUS - 2;
|
|
1339
|
+
|
|
1340
|
+
// Pass 1: white border
|
|
1341
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
1342
|
+
const t = i / steps;
|
|
1343
|
+
const u = 1 - t;
|
|
1344
|
+
const px = (u * u * startPos.x) + (2 * u * t * cpx) + (t * t * endPos.x);
|
|
1345
|
+
const py = (u * u * startPos.y) + (2 * u * t * cpy) + (t * t * endPos.y);
|
|
1346
|
+
aura.draw2d.circleFill(px, py, bodyR + 4, rgba(1, 1, 1, 0.48));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Pass 2: colored body fill
|
|
1350
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
1351
|
+
const t = i / steps;
|
|
1352
|
+
const u = 1 - t;
|
|
1353
|
+
const px = (u * u * startPos.x) + (2 * u * t * cpx) + (t * t * endPos.x);
|
|
1354
|
+
const py = (u * u * startPos.y) + (2 * u * t * cpy) + (t * t * endPos.y);
|
|
1355
|
+
aura.draw2d.circleFill(px, py, bodyR, rgba(base[0] * 0.3, base[1] * 0.3, base[2] * 0.3, 0.9));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Pass 3: inner highlight
|
|
1359
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
1360
|
+
const t = i / steps;
|
|
1361
|
+
const u = 1 - t;
|
|
1362
|
+
const px = (u * u * startPos.x) + (2 * u * t * cpx) + (t * t * endPos.x);
|
|
1363
|
+
const py = (u * u * startPos.y) + (2 * u * t * cpy) + (t * t * endPos.y);
|
|
1364
|
+
aura.draw2d.circleFill(px, py, bodyR * 0.55, rgba(base[0] * 0.15 + 0.06, base[1] * 0.15 + 0.06, base[2] * 0.15 + 0.06, 0.45));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// End circle
|
|
1368
|
+
aura.draw2d.circleFill(endPos.x, endPos.y, bodyR - 4, rgba(base[0] * 0.4, base[1] * 0.4, base[2] * 0.4, 0.72));
|
|
1369
|
+
aura.draw2d.circle(endPos.x, endPos.y, bodyR - 4, rgba(1, 1, 1, 0.58));
|
|
1370
|
+
|
|
1371
|
+
// Tick marks along slider path
|
|
1372
|
+
const tickSpacing = 54;
|
|
1373
|
+
const tickCount = Math.max(1, Math.floor(len / tickSpacing));
|
|
1374
|
+
for (let i = 1; i < tickCount; i += 1) {
|
|
1375
|
+
const t = i / tickCount;
|
|
1376
|
+
const u = 1 - t;
|
|
1377
|
+
const tx = (u * u * startPos.x) + (2 * u * t * cpx) + (t * t * endPos.x);
|
|
1378
|
+
const ty = (u * u * startPos.y) + (2 * u * t * cpy) + (t * t * endPos.y);
|
|
1379
|
+
const tickPulse = 0.65 + (Math.sin((pulseTime * 7.4) + (t * 8.0) + seed) * 0.35);
|
|
1380
|
+
const tickAlpha = 0.22 + (tickPulse * 0.5);
|
|
1381
|
+
if (!drawSkinSprite('followpoint', tx, ty, {
|
|
1382
|
+
width: 10,
|
|
1383
|
+
height: 10,
|
|
1384
|
+
originX: 0.5,
|
|
1385
|
+
originY: 0.5,
|
|
1386
|
+
alpha: tickAlpha,
|
|
1387
|
+
})) {
|
|
1388
|
+
aura.draw2d.circleFill(tx, ty, 4, rgba(1, 1, 1, 0.26 + (tickPulse * 0.3)));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Slider ball motion
|
|
1393
|
+
if (progress >= 0) {
|
|
1394
|
+
const t = clamp(progress, 0, 1);
|
|
1395
|
+
const u = 1 - t;
|
|
1396
|
+
const bx = (u * u * startPos.x) + (2 * u * t * cpx) + (t * t * endPos.x);
|
|
1397
|
+
const by = (u * u * startPos.y) + (2 * u * t * cpy) + (t * t * endPos.y);
|
|
1398
|
+
const motionGlow = 0.35 + (Math.sin((pulseTime * 12) + (seed * 0.9)) * 0.18);
|
|
1399
|
+
|
|
1400
|
+
// Follow circle
|
|
1401
|
+
if (activeLink) {
|
|
1402
|
+
const followDrew = drawSkinSprite('sliderfollowcircle', bx, by, {
|
|
1403
|
+
width: (bodyR + 11) * 2,
|
|
1404
|
+
height: (bodyR + 11) * 2,
|
|
1405
|
+
originX: 0.5,
|
|
1406
|
+
originY: 0.5,
|
|
1407
|
+
alpha: 0.72 + (motionGlow * 0.2),
|
|
1408
|
+
});
|
|
1409
|
+
if (!followDrew) {
|
|
1410
|
+
aura.draw2d.circleFill(bx, by, bodyR + 10, rgba(1, 1, 1, 0.12 + (motionGlow * 0.06)));
|
|
1411
|
+
aura.draw2d.circle(bx, by, bodyR + 10, rgba(1, 1, 1, 0.62 + (motionGlow * 0.1)));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Ball
|
|
1416
|
+
const ballDrew = drawSkinSprite('sliderball', bx, by, {
|
|
1417
|
+
width: 26,
|
|
1418
|
+
height: 26,
|
|
1419
|
+
originX: 0.5,
|
|
1420
|
+
originY: 0.5,
|
|
1421
|
+
alpha: activeLink ? 0.96 : 0.88,
|
|
1422
|
+
tint: rgb(base[0], base[1], base[2]),
|
|
1423
|
+
});
|
|
1424
|
+
if (!ballDrew) {
|
|
1425
|
+
aura.draw2d.circleFill(bx, by, 12, rgba(base[0], base[1], base[2], activeLink ? 0.94 : 0.86));
|
|
1426
|
+
aura.draw2d.circle(bx, by, 12, rgba(1, 1, 1, activeLink ? 0.86 : 0.72));
|
|
1427
|
+
aura.draw2d.circleFill(bx, by, 5, rgba(1, 1, 1, activeLink ? 0.68 : 0.58));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Motion aura
|
|
1431
|
+
aura.draw2d.circle(bx, by, 16 + (motionGlow * 4), rgba(base[0], base[1], base[2], activeLink ? 0.45 : 0.24));
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function drawNotes() {
|
|
1436
|
+
let upcoming = null;
|
|
1437
|
+
let minAhead = Infinity;
|
|
1438
|
+
|
|
1439
|
+
// Pass 1: draw all visible slider bodies (behind circles)
|
|
1440
|
+
for (const note of notes) {
|
|
1441
|
+
if (note.dragNextIndex === null || note.dragNextIndex === undefined) continue;
|
|
1442
|
+
const next = notes[note.dragNextIndex];
|
|
1443
|
+
if (!next || next.judged) continue;
|
|
1444
|
+
|
|
1445
|
+
const tNote = note.time - currentTime;
|
|
1446
|
+
const tNext = next.time - currentTime;
|
|
1447
|
+
if (tNote > activeApproachTime + 0.5 && tNext > activeApproachTime + 0.5) continue;
|
|
1448
|
+
if (tNote < -(activeWindowMiss * 3) && tNext < -(activeWindowMiss * 3)) continue;
|
|
1449
|
+
|
|
1450
|
+
const pos = notePosition(note);
|
|
1451
|
+
const nextPos = notePosition(next);
|
|
1452
|
+
const isActive = dragExpectedIndex === next.index;
|
|
1453
|
+
let sliderProgress = -1;
|
|
1454
|
+
const dur = next.time - note.time;
|
|
1455
|
+
if (dur > 0) {
|
|
1456
|
+
const preRoll = 0.16;
|
|
1457
|
+
const postRoll = 0.08;
|
|
1458
|
+
if (currentTime >= (note.time - preRoll) && currentTime <= (next.time + postRoll)) {
|
|
1459
|
+
sliderProgress = clamp((currentTime - note.time) / dur, 0, 1);
|
|
1460
|
+
}
|
|
1461
|
+
if (!note.judged && isActive) {
|
|
1462
|
+
sliderProgress = clamp((currentTime - note.time) / dur, 0, 1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
drawSliderBody(pos, nextPos, isActive, note.colorIndex, note.index, sliderProgress);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Pass 2: draw hit circles
|
|
1469
|
+
for (const note of notes) {
|
|
1470
|
+
if (note.judged) continue;
|
|
1471
|
+
const timeUntil = note.time - currentTime;
|
|
1472
|
+
if (timeUntil < -activeWindowMiss || timeUntil > activeApproachTime) continue;
|
|
1473
|
+
|
|
1474
|
+
if (timeUntil >= 0 && timeUntil < minAhead) {
|
|
1475
|
+
minAhead = timeUntil;
|
|
1476
|
+
upcoming = note;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const pos = notePosition(note);
|
|
1480
|
+
const pal = PALETTE[note.colorIndex % PALETTE.length];
|
|
1481
|
+
const approachT = clamp(timeUntil / activeApproachTime, 0, 1);
|
|
1482
|
+
const alphaScale = clamp(1 - ((-timeUntil) / activeWindowMiss), 0.3, 1);
|
|
1483
|
+
const fadeIn = clamp(1 - approachT + 0.15, 0, 1);
|
|
1484
|
+
const approachAlpha = fadeIn * alphaScale;
|
|
1485
|
+
const lateFade = clamp(Math.max(0, -timeUntil) / activeWindowMiss, 0, 1);
|
|
1486
|
+
const circleAlpha = clamp(1 - (lateFade * 0.08), VIS_CIRCLE_SOLID_MIN_ALPHA, 1);
|
|
1487
|
+
const spawnAge = activeApproachTime - timeUntil;
|
|
1488
|
+
const spawnPulse = spawnAge >= 0 && spawnAge <= 0.2
|
|
1489
|
+
? 1 - (spawnAge / 0.2)
|
|
1490
|
+
: 0;
|
|
1491
|
+
const noteScale = 1 + (spawnPulse * VIS_NOTE_POP_SCALE);
|
|
1492
|
+
const noteRadius = HIT_RADIUS * noteScale;
|
|
1493
|
+
const noteSpriteRadius = Math.max(8, Math.round(noteRadius));
|
|
1494
|
+
|
|
1495
|
+
// High-contrast approach ring with thicker layered stroke.
|
|
1496
|
+
if (timeUntil > 0) {
|
|
1497
|
+
const aR = noteRadius * (1 + (approachT * 2.35));
|
|
1498
|
+
const approachSpriteRadius = Math.max(2, Math.round(aR));
|
|
1499
|
+
const ringPulse = 0.88 + (Math.sin((pulseTime * 14) + (note.index * 0.72)) * 0.12);
|
|
1500
|
+
const ringAlpha = clamp((0.52 + (approachT * 0.98)) * approachAlpha * ringPulse, 0, 1);
|
|
1501
|
+
const approachColor = rgb(pal[0], pal[1], pal[2]);
|
|
1502
|
+
const drewFar = drawSkinSprite('approachcircle', pos.x, pos.y, {
|
|
1503
|
+
width: (approachSpriteRadius + 8) * 2,
|
|
1504
|
+
height: (approachSpriteRadius + 8) * 2,
|
|
1505
|
+
originX: 0.5,
|
|
1506
|
+
originY: 0.5,
|
|
1507
|
+
frameX: CIRCLE_SPRITE_INSET,
|
|
1508
|
+
frameY: CIRCLE_SPRITE_INSET,
|
|
1509
|
+
frameW: CIRCLE_SPRITE_FRAME,
|
|
1510
|
+
frameH: CIRCLE_SPRITE_FRAME,
|
|
1511
|
+
pixelSnap: true,
|
|
1512
|
+
alpha: 0.2 * ringAlpha,
|
|
1513
|
+
tint: approachColor,
|
|
1514
|
+
});
|
|
1515
|
+
const drewOuter = drawSkinSprite('approachcircle', pos.x, pos.y, {
|
|
1516
|
+
width: (approachSpriteRadius + 3) * 2,
|
|
1517
|
+
height: (approachSpriteRadius + 3) * 2,
|
|
1518
|
+
originX: 0.5,
|
|
1519
|
+
originY: 0.5,
|
|
1520
|
+
frameX: CIRCLE_SPRITE_INSET,
|
|
1521
|
+
frameY: CIRCLE_SPRITE_INSET,
|
|
1522
|
+
frameW: CIRCLE_SPRITE_FRAME,
|
|
1523
|
+
frameH: CIRCLE_SPRITE_FRAME,
|
|
1524
|
+
pixelSnap: true,
|
|
1525
|
+
alpha: 0.42 * ringAlpha,
|
|
1526
|
+
tint: approachColor,
|
|
1527
|
+
});
|
|
1528
|
+
const drewInner = drawSkinSprite('approachcircle', pos.x, pos.y, {
|
|
1529
|
+
width: approachSpriteRadius * 2,
|
|
1530
|
+
height: approachSpriteRadius * 2,
|
|
1531
|
+
originX: 0.5,
|
|
1532
|
+
originY: 0.5,
|
|
1533
|
+
frameX: CIRCLE_SPRITE_INSET,
|
|
1534
|
+
frameY: CIRCLE_SPRITE_INSET,
|
|
1535
|
+
frameW: CIRCLE_SPRITE_FRAME,
|
|
1536
|
+
frameH: CIRCLE_SPRITE_FRAME,
|
|
1537
|
+
pixelSnap: true,
|
|
1538
|
+
alpha: clamp((1.02 * ringAlpha) + 0.08, 0, 1),
|
|
1539
|
+
tint: approachColor,
|
|
1540
|
+
});
|
|
1541
|
+
if (!drewFar && !drewOuter && !drewInner) {
|
|
1542
|
+
aura.draw2d.circle(pos.x, pos.y, aR + 7, rgba(pal[0], pal[1], pal[2], 0.2 * ringAlpha));
|
|
1543
|
+
aura.draw2d.circle(pos.x, pos.y, aR + 2.5, rgba(pal[0], pal[1], pal[2], 0.42 * ringAlpha));
|
|
1544
|
+
aura.draw2d.circle(pos.x, pos.y, aR, rgba(pal[0], pal[1], pal[2], 1.0 * ringAlpha));
|
|
1545
|
+
aura.draw2d.circle(pos.x, pos.y, Math.max(2, aR - 3), rgba(1, 1, 1, 0.68 * ringAlpha));
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Spawn pop + shockwave as notes enter the play window.
|
|
1550
|
+
if (spawnPulse > 0) {
|
|
1551
|
+
const spawnShockR = noteRadius + ((1 - spawnPulse) * 54);
|
|
1552
|
+
const spawnAlpha = 0.42 * spawnPulse * circleAlpha;
|
|
1553
|
+
aura.draw2d.circle(pos.x, pos.y, spawnShockR, rgba(pal[0], pal[1], pal[2], spawnAlpha));
|
|
1554
|
+
aura.draw2d.circle(pos.x, pos.y, Math.max(2, spawnShockR - 3), rgba(1, 1, 1, spawnAlpha * 0.52));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Outer glow behind skinned circle.
|
|
1558
|
+
const glowR = noteRadius + 10 + (spawnPulse * 10);
|
|
1559
|
+
aura.draw2d.circleFill(pos.x, pos.y, glowR, rgba(pal[0], pal[1], pal[2], (0.24 + (spawnPulse * 0.28)) * circleAlpha));
|
|
1560
|
+
|
|
1561
|
+
// Circle body + overlay (skin assets with fallback).
|
|
1562
|
+
const bodyBaseAlpha = clamp(0.97 + (spawnPulse * 0.03), 0, 1);
|
|
1563
|
+
aura.draw2d.circleFill(pos.x, pos.y, noteRadius * 0.98, rgba(pal[0] * 0.62, pal[1] * 0.62, pal[2] * 0.62, bodyBaseAlpha));
|
|
1564
|
+
const drewBase = drawSkinSprite('hitcircle', pos.x, pos.y, {
|
|
1565
|
+
width: noteSpriteRadius * 2,
|
|
1566
|
+
height: noteSpriteRadius * 2,
|
|
1567
|
+
originX: 0.5,
|
|
1568
|
+
originY: 0.5,
|
|
1569
|
+
frameX: CIRCLE_SPRITE_INSET,
|
|
1570
|
+
frameY: CIRCLE_SPRITE_INSET,
|
|
1571
|
+
frameW: CIRCLE_SPRITE_FRAME,
|
|
1572
|
+
frameH: CIRCLE_SPRITE_FRAME,
|
|
1573
|
+
pixelSnap: true,
|
|
1574
|
+
alpha: 0.99 * circleAlpha,
|
|
1575
|
+
});
|
|
1576
|
+
const drewOverlay = drawSkinSprite('hitcircleoverlay', pos.x, pos.y, {
|
|
1577
|
+
width: noteSpriteRadius * 2,
|
|
1578
|
+
height: noteSpriteRadius * 2,
|
|
1579
|
+
originX: 0.5,
|
|
1580
|
+
originY: 0.5,
|
|
1581
|
+
frameX: CIRCLE_SPRITE_INSET,
|
|
1582
|
+
frameY: CIRCLE_SPRITE_INSET,
|
|
1583
|
+
frameW: CIRCLE_SPRITE_FRAME,
|
|
1584
|
+
frameH: CIRCLE_SPRITE_FRAME,
|
|
1585
|
+
pixelSnap: true,
|
|
1586
|
+
alpha: (0.94 + (spawnPulse * 0.06)) * circleAlpha,
|
|
1587
|
+
});
|
|
1588
|
+
if (!drewBase && !drewOverlay) {
|
|
1589
|
+
aura.draw2d.circleFill(pos.x, pos.y, noteRadius, rgba(pal[0] * 0.56, pal[1] * 0.56, pal[2] * 0.56, 0.98 * circleAlpha));
|
|
1590
|
+
aura.draw2d.circle(pos.x, pos.y, noteRadius, rgba(1, 1, 1, (0.95 + (spawnPulse * 0.05)) * circleAlpha));
|
|
1591
|
+
aura.draw2d.circle(pos.x, pos.y, noteRadius - 3, rgba(1, 1, 1, (0.32 + (spawnPulse * 0.18)) * circleAlpha));
|
|
1592
|
+
aura.draw2d.circleFill(pos.x, pos.y, noteRadius * 0.54, rgba(0.02, 0.02, 0.05, 0.72 * circleAlpha));
|
|
1593
|
+
aura.draw2d.circleFill(
|
|
1594
|
+
pos.x - (noteRadius * 0.2),
|
|
1595
|
+
pos.y - (noteRadius * 0.22),
|
|
1596
|
+
noteRadius * 0.22,
|
|
1597
|
+
rgba(1, 1, 1, 0.15 * circleAlpha * (0.5 + (spawnPulse * 0.5)))
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const edgePulse = 0.84 + (Math.sin((pulseTime * 18) + (note.index * 0.37)) * 0.16);
|
|
1602
|
+
aura.draw2d.circle(pos.x, pos.y, noteRadius + 1.5, rgba(1, 1, 1, (0.42 + (spawnPulse * 0.24)) * circleAlpha * edgePulse));
|
|
1603
|
+
aura.draw2d.circle(pos.x, pos.y, noteRadius - 3, rgba(pal[0], pal[1], pal[2], (0.5 + (spawnPulse * 0.3)) * circleAlpha));
|
|
1604
|
+
|
|
1605
|
+
// Combo number
|
|
1606
|
+
const numStr = String((note.index % 9) + 1);
|
|
1607
|
+
const numSz = clamp(32 * noteScale, 28, 44);
|
|
1608
|
+
const numMet = aura.draw2d.measureText(numStr, { size: numSz });
|
|
1609
|
+
const numX = pos.x - (numMet.width * 0.5);
|
|
1610
|
+
const numY = pos.y - (numSz * 0.36);
|
|
1611
|
+
aura.draw2d.text(numStr, numX + 1.5, numY + 1.5, { size: numSz, color: rgba(0, 0, 0, 0.58 * circleAlpha) });
|
|
1612
|
+
aura.draw2d.text(numStr, numX, numY, { size: numSz, color: rgba(1, 1, 1, 0.98 * circleAlpha) });
|
|
1613
|
+
|
|
1614
|
+
// Active drag target indicator
|
|
1615
|
+
if (dragExpectedIndex === note.index) {
|
|
1616
|
+
const s = 1 + (Math.sin(pulseTime * 14) * 0.08);
|
|
1617
|
+
aura.draw2d.circle(pos.x, pos.y, (noteRadius + 12) * s, rgba(0.49, 1.0, 0.72, 0.82));
|
|
1618
|
+
aura.draw2d.circle(pos.x, pos.y, (noteRadius + 18) * s, rgba(1, 1, 1, 0.24));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Subtle highlight on next note
|
|
1623
|
+
if (upcoming) {
|
|
1624
|
+
const p = notePosition(upcoming);
|
|
1625
|
+
aura.draw2d.circle(p.x, p.y, HIT_RADIUS + 16, rgba(1, 1, 1, 0.18));
|
|
1626
|
+
aura.draw2d.circle(p.x, p.y, HIT_RADIUS + 22, rgba(0.45, 0.9, 1.0, 0.14));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function drawCursor() {
|
|
1631
|
+
for (const trail of cursorTrail) {
|
|
1632
|
+
const t = clamp(1 - (trail.age / CURSOR_TRAIL_LIFE), 0, 1);
|
|
1633
|
+
const trailDrew = drawSkinSprite('cursortrail', trail.x, trail.y, {
|
|
1634
|
+
width: 24 + (t * 18),
|
|
1635
|
+
height: 24 + (t * 18),
|
|
1636
|
+
originX: 0.5,
|
|
1637
|
+
originY: 0.5,
|
|
1638
|
+
alpha: 0.36 * t,
|
|
1639
|
+
tint: rgb(0.42, 0.9, 1.0),
|
|
1640
|
+
});
|
|
1641
|
+
if (!trailDrew) {
|
|
1642
|
+
aura.draw2d.circleFill(trail.x, trail.y, 4 + (t * 10), rgba(0.42, 0.9, 1.0, 0.2 * t));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const m = getMouse();
|
|
1647
|
+
if (dragExpectedIndex >= 0 && (mouseHoldActive || keyHoldActive)) {
|
|
1648
|
+
const followDrew = drawSkinSprite('sliderfollowcircle', m.x, m.y, {
|
|
1649
|
+
width: 44,
|
|
1650
|
+
height: 44,
|
|
1651
|
+
originX: 0.5,
|
|
1652
|
+
originY: 0.5,
|
|
1653
|
+
alpha: 0.62,
|
|
1654
|
+
});
|
|
1655
|
+
if (!followDrew) {
|
|
1656
|
+
aura.draw2d.circle(m.x, m.y, 22, rgba(0.52, 1.0, 0.72, 0.75));
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const cursorDrew = drawSkinSprite('cursor', m.x, m.y, {
|
|
1661
|
+
width: 34,
|
|
1662
|
+
height: 34,
|
|
1663
|
+
originX: 0.5,
|
|
1664
|
+
originY: 0.5,
|
|
1665
|
+
alpha: 0.95,
|
|
1666
|
+
});
|
|
1667
|
+
if (!cursorDrew) {
|
|
1668
|
+
aura.draw2d.circle(m.x, m.y, 15, rgba(1, 1, 1, 0.8));
|
|
1669
|
+
aura.draw2d.circle(m.x, m.y, 8, rgba(0.3, 0.95, 1, 0.95));
|
|
1670
|
+
aura.draw2d.line(m.x - 22, m.y, m.x + 22, m.y, rgba(1, 1, 1, 0.32), 1);
|
|
1671
|
+
aura.draw2d.line(m.x, m.y - 22, m.x, m.y + 22, rgba(1, 1, 1, 0.32), 1);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
aura.draw2d.circle(m.x, m.y, 17, rgba(1, 1, 1, 0.3));
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function drawHud() {
|
|
1678
|
+
const acc = accuracyPercent();
|
|
1679
|
+
const timeLeft = Math.max(0, lastNoteTime - currentTime);
|
|
1680
|
+
const feverActive = combo >= FEVER_COMBO_THRESHOLD;
|
|
1681
|
+
const feverFill = feverActive ? rgba(1.0, 0.86, 0.36, 0.16 + (comboFlash * 0.35)) : rgba(0, 0, 0, 0);
|
|
1682
|
+
const diff = getSelectedDifficulty();
|
|
1683
|
+
|
|
1684
|
+
aura.draw2d.rectFill(14, 14, 420, 114, rgba(0.03, 0.04, 0.07, 0.72));
|
|
1685
|
+
if (feverActive) {
|
|
1686
|
+
aura.draw2d.rectFill(14, 14, 420, 114, feverFill);
|
|
1687
|
+
}
|
|
1688
|
+
aura.draw2d.text(`SCORE ${score}`, 28, 32, { size: 26, color: white() });
|
|
1689
|
+
aura.draw2d.text(`COMBO x${combo}`, 28, 60, { size: 18, color: rgba(0.95, 0.97, 1, 1) });
|
|
1690
|
+
aura.draw2d.text(`ACC ${acc.toFixed(2)}%`, 28, 82, { size: 18, color: rgba(0.56, 0.96, 1, 1) });
|
|
1691
|
+
aura.draw2d.text(`LEFT ${timeLeft.toFixed(1)}s`, 28, 103, { size: 14, color: rgba(0.82, 0.88, 1, 0.95) });
|
|
1692
|
+
aura.draw2d.text(`DIFF ${diff.label}`, 250, 82, { size: 14, color: rgba(diff.accent[0], diff.accent[1], diff.accent[2], 0.95) });
|
|
1693
|
+
if (feverActive) {
|
|
1694
|
+
aura.draw2d.text(`FEVER x${FEVER_MULTIPLIER.toFixed(2)}`, 250, 103, { size: 14, color: rgba(1.0, 0.9, 0.36, 1) });
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const flowX = 248;
|
|
1698
|
+
const flowY = 58;
|
|
1699
|
+
const flowW = 166;
|
|
1700
|
+
aura.draw2d.rectFill(flowX, flowY, flowW, 10, rgba(0.02, 0.03, 0.05, 0.65));
|
|
1701
|
+
aura.draw2d.rectFill(flowX + 1, flowY + 1, (flowW - 2) * flowMeter, 8, rgba(0.44, 0.95, 1.0, 0.85));
|
|
1702
|
+
aura.draw2d.text('FLOW', flowX, flowY - 4, { size: 11, color: rgba(0.8, 0.94, 1, 0.92) });
|
|
1703
|
+
|
|
1704
|
+
const barX = worldWidth - 374;
|
|
1705
|
+
const barY = 30;
|
|
1706
|
+
const barW = 340;
|
|
1707
|
+
const barH = 26;
|
|
1708
|
+
const hpT = clamp(health / HEALTH_MAX, 0, 1);
|
|
1709
|
+
const hpColor = hpT > 0.65
|
|
1710
|
+
? rgba(0.32, 1, 0.58, 0.95)
|
|
1711
|
+
: hpT > 0.35
|
|
1712
|
+
? rgba(1, 0.84, 0.34, 0.95)
|
|
1713
|
+
: rgba(1, 0.35, 0.35, 0.95);
|
|
1714
|
+
aura.draw2d.rectFill(barX, barY, barW, barH, rgba(0.02, 0.02, 0.05, 0.75));
|
|
1715
|
+
aura.draw2d.rectFill(barX + 2, barY + 2, (barW - 4) * hpT, barH - 4, hpColor);
|
|
1716
|
+
aura.draw2d.rect(barX, barY, barW, barH, rgba(1, 1, 1, 0.4));
|
|
1717
|
+
aura.draw2d.text('HEALTH', barX + 6, barY + 18, { size: 12, color: rgba(1, 1, 1, 0.75) });
|
|
1718
|
+
|
|
1719
|
+
const progress = clamp(currentTime / Math.max(lastNoteTime, 0.001), 0, 1);
|
|
1720
|
+
const pX = 14;
|
|
1721
|
+
const pY = worldHeight - 26;
|
|
1722
|
+
const pW = worldWidth - 28;
|
|
1723
|
+
aura.draw2d.rectFill(pX, pY, pW, 10, rgba(0.02, 0.03, 0.05, 0.8));
|
|
1724
|
+
aura.draw2d.rectFill(pX + 1, pY + 1, (pW - 2) * progress, 8, rgba(0.45, 0.9, 1.0, 0.8));
|
|
1725
|
+
|
|
1726
|
+
// Round key indicators (osu!-style)
|
|
1727
|
+
const keyR = 30;
|
|
1728
|
+
const keyGap = 22;
|
|
1729
|
+
const keyCy = worldHeight - 78;
|
|
1730
|
+
const k1cx = (worldWidth * 0.5) - keyR - (keyGap * 0.5);
|
|
1731
|
+
const k2cx = (worldWidth * 0.5) + keyR + (keyGap * 0.5);
|
|
1732
|
+
const k1G = clamp((k1Down ? 0.8 : 0) + (k1Pulse * 0.9), 0, 1);
|
|
1733
|
+
const k2G = clamp((k2Down ? 0.8 : 0) + (k2Pulse * 0.9), 0, 1);
|
|
1734
|
+
const k1FT = clamp(k1Flash, 0, 1);
|
|
1735
|
+
const k2FT = clamp(k2Flash, 0, 1);
|
|
1736
|
+
|
|
1737
|
+
// K1 flash ring
|
|
1738
|
+
if (k1FT > 0) {
|
|
1739
|
+
const k1PulseR = keyR + 22 + ((1 - k1FT) * 34);
|
|
1740
|
+
aura.draw2d.circle(k1cx, keyCy, k1PulseR, rgba(0.34, 0.88, 1.0, 0.92 * k1FT));
|
|
1741
|
+
aura.draw2d.circle(k1cx, keyCy, k1PulseR - 6, rgba(1, 1, 1, 0.46 * k1FT));
|
|
1742
|
+
aura.draw2d.circleFill(k1cx, keyCy, keyR + 12 + (k1FT * 9), rgba(0.34, 0.88, 1.0, 0.08 * k1FT));
|
|
1743
|
+
}
|
|
1744
|
+
// K2 flash ring
|
|
1745
|
+
if (k2FT > 0) {
|
|
1746
|
+
const k2PulseR = keyR + 22 + ((1 - k2FT) * 34);
|
|
1747
|
+
aura.draw2d.circle(k2cx, keyCy, k2PulseR, rgba(1.0, 0.48, 0.72, 0.92 * k2FT));
|
|
1748
|
+
aura.draw2d.circle(k2cx, keyCy, k2PulseR - 6, rgba(1, 1, 1, 0.46 * k2FT));
|
|
1749
|
+
aura.draw2d.circleFill(k2cx, keyCy, keyR + 12 + (k2FT * 9), rgba(1.0, 0.48, 0.72, 0.08 * k2FT));
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// K1 glow + body
|
|
1753
|
+
aura.draw2d.circleFill(k1cx, keyCy, keyR + 8 + (k1G * 10), rgba(0.34, 0.88, 1.0, 0.1 + (k1G * 0.28)));
|
|
1754
|
+
aura.draw2d.circleFill(k1cx, keyCy, keyR, rgba(0.08, 0.2, 0.34, 0.66 + (k1G * 0.34)));
|
|
1755
|
+
aura.draw2d.circle(k1cx, keyCy, keyR, rgba(0.34, 0.88, 1.0, 0.56 + (k1G * 0.44)));
|
|
1756
|
+
aura.draw2d.circleFill(k1cx, keyCy, keyR * 0.5, rgba(1, 1, 1, 0.05 + (k1G * 0.2)));
|
|
1757
|
+
const k1m = aura.draw2d.measureText('K1', { size: 18 });
|
|
1758
|
+
aura.draw2d.text('K1', k1cx - (k1m.width * 0.5), keyCy - 5, { size: 18, color: rgba(1, 1, 1, 0.82 + (k1G * 0.18)) });
|
|
1759
|
+
aura.draw2d.text('Z', k1cx - 3, keyCy + 14, { size: 10, color: rgba(0.7, 0.9, 1.0, 0.65) });
|
|
1760
|
+
|
|
1761
|
+
// K2 glow + body
|
|
1762
|
+
aura.draw2d.circleFill(k2cx, keyCy, keyR + 8 + (k2G * 10), rgba(1.0, 0.48, 0.72, 0.1 + (k2G * 0.28)));
|
|
1763
|
+
aura.draw2d.circleFill(k2cx, keyCy, keyR, rgba(0.34, 0.08, 0.2, 0.66 + (k2G * 0.34)));
|
|
1764
|
+
aura.draw2d.circle(k2cx, keyCy, keyR, rgba(1.0, 0.48, 0.72, 0.56 + (k2G * 0.44)));
|
|
1765
|
+
aura.draw2d.circleFill(k2cx, keyCy, keyR * 0.5, rgba(1, 1, 1, 0.05 + (k2G * 0.2)));
|
|
1766
|
+
const k2m = aura.draw2d.measureText('K2', { size: 18 });
|
|
1767
|
+
aura.draw2d.text('K2', k2cx - (k2m.width * 0.5), keyCy - 5, { size: 18, color: rgba(1, 1, 1, 0.82 + (k2G * 0.18)) });
|
|
1768
|
+
aura.draw2d.text('X', k2cx - 3, keyCy + 14, { size: 10, color: rgba(1.0, 0.8, 0.9, 0.65) });
|
|
1769
|
+
|
|
1770
|
+
const bgmState = !bgmLoaded
|
|
1771
|
+
? 'BGM loading...'
|
|
1772
|
+
: bgmHandle >= 0
|
|
1773
|
+
? (bgmMuted ? 'BGM muted (M)' : 'BGM on (M mute, N restart)')
|
|
1774
|
+
: (state === 'title' ? 'BGM ready (press start)' : 'BGM unavailable');
|
|
1775
|
+
const sfxState = sfxMuted ? 'SFX muted (H)' : 'SFX on (H mute)';
|
|
1776
|
+
aura.draw2d.text(`${bgmState} | ${sfxState}`, worldWidth - 430, worldHeight - 38, {
|
|
1777
|
+
size: 13,
|
|
1778
|
+
color: (bgmHandle >= 0 || state === 'title')
|
|
1779
|
+
? rgba(0.66, 0.98, 0.74, 0.96)
|
|
1780
|
+
: rgba(1.0, 0.64, 0.5, 0.96),
|
|
1781
|
+
});
|
|
1782
|
+
if (bgmLastError && bgmHandle < 0 && state !== 'title') {
|
|
1783
|
+
aura.draw2d.text('audio error in terminal logs', worldWidth - 260, worldHeight - 20, {
|
|
1784
|
+
size: 11,
|
|
1785
|
+
color: rgba(1.0, 0.56, 0.5, 0.9),
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function drawTitle() {
|
|
1791
|
+
const pulse = 0.76 + (Math.sin(pulseTime * 4.2) * 0.24);
|
|
1792
|
+
const cx = worldWidth * 0.5;
|
|
1793
|
+
const cy = worldHeight * 0.32;
|
|
1794
|
+
const mouse = getMouse();
|
|
1795
|
+
const selected = getSelectedDifficulty();
|
|
1796
|
+
|
|
1797
|
+
// Decorative pulsing rings
|
|
1798
|
+
const ringPulse = 1 + (Math.sin(pulseTime * 2.0) * 0.04);
|
|
1799
|
+
aura.draw2d.circle(cx, cy, 130 * ringPulse, rgba(0.28, 0.83, 1.0, 0.1));
|
|
1800
|
+
aura.draw2d.circle(cx, cy, 95 * ringPulse, rgba(0.28, 0.83, 1.0, 0.15));
|
|
1801
|
+
aura.draw2d.circleFill(cx, cy, 60, rgba(0.28, 0.83, 1.0, 0.04));
|
|
1802
|
+
|
|
1803
|
+
drawCentered('AURASU', Math.floor(worldHeight * 0.2), 72, rgba(1, 1, 1, 0.98));
|
|
1804
|
+
drawCentered('TAP CIRCLES | FOLLOW SLIDERS | CATCH THE BEAT', Math.floor(worldHeight * 0.32), 19, rgba(0.74, 0.93, 1, 0.92));
|
|
1805
|
+
drawCentered('CLICK / Z / X TO HIT', Math.floor(worldHeight * 0.39), 17, rgba(1, 1, 1, 0.8));
|
|
1806
|
+
drawCentered('PICK DIFFICULTY', Math.floor(worldHeight * 0.47), 18, rgba(0.86, 0.97, 1.0, 0.88));
|
|
1807
|
+
|
|
1808
|
+
const chipCount = DIFFICULTY_PRESETS.length;
|
|
1809
|
+
const rowGap = 7;
|
|
1810
|
+
const rowW = 280;
|
|
1811
|
+
const rowH = 36;
|
|
1812
|
+
const totalH = (rowH * chipCount) + (rowGap * (chipCount - 1));
|
|
1813
|
+
const rowX = Math.floor((worldWidth - rowW) * 0.5);
|
|
1814
|
+
const rowY0 = Math.floor(worldHeight * 0.505);
|
|
1815
|
+
titleDifficultyBoxes = [];
|
|
1816
|
+
|
|
1817
|
+
for (let i = 0; i < chipCount; i += 1) {
|
|
1818
|
+
const diff = DIFFICULTY_PRESETS[i];
|
|
1819
|
+
const x = rowX;
|
|
1820
|
+
const y = rowY0 + (i * (rowH + rowGap));
|
|
1821
|
+
const isSelected = i === selectedDifficultyIndex;
|
|
1822
|
+
const hover = pointInRect(mouse.x, mouse.y, x, y, rowW, rowH);
|
|
1823
|
+
const labelColor = isSelected
|
|
1824
|
+
? rgba(1, 1, 1, 1)
|
|
1825
|
+
: hover
|
|
1826
|
+
? rgba(0.9, 0.94, 1.0, 0.9)
|
|
1827
|
+
: rgba(0.78, 0.84, 0.92, 0.72);
|
|
1828
|
+
if (isSelected) {
|
|
1829
|
+
aura.draw2d.text('▶', x - 22, y + 25, {
|
|
1830
|
+
size: 18,
|
|
1831
|
+
color: rgba(1, 1, 1, 0.98),
|
|
1832
|
+
});
|
|
1833
|
+
aura.draw2d.line(x - 6, y + 5, x - 6, y + rowH - 4, rgba(1, 1, 1, 0.65), 2);
|
|
1834
|
+
}
|
|
1835
|
+
aura.draw2d.text(`${i + 1}. ${diff.label}`, x, y + 24, {
|
|
1836
|
+
size: 28,
|
|
1837
|
+
color: labelColor,
|
|
1838
|
+
});
|
|
1839
|
+
aura.draw2d.text(diff.subtitle, x + 140, y + 24, {
|
|
1840
|
+
size: 12,
|
|
1841
|
+
color: isSelected ? rgba(0.86, 0.9, 0.98, 0.95) : rgba(0.72, 0.8, 0.9, 0.62),
|
|
1842
|
+
});
|
|
1843
|
+
titleDifficultyBoxes.push({ index: i, x: x - 26, y, w: rowW + 32, h: rowH });
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const menuBottom = rowY0 + totalH;
|
|
1847
|
+
const hintY = Math.min(worldHeight - 88, menuBottom + 18);
|
|
1848
|
+
const startY = Math.min(worldHeight - 58, hintY + 30);
|
|
1849
|
+
const timingY = Math.min(worldHeight - 34, startY + 26);
|
|
1850
|
+
const controlsY = Math.min(worldHeight - 16, timingY + 18);
|
|
1851
|
+
drawCentered(`SELECTED: ${selected.label} | UP/DOWN OR W/S OR 1-${chipCount}`, hintY, 14, rgba(0.82, 0.95, 1.0, 0.86));
|
|
1852
|
+
drawCentered('PRESS ENTER OR SPACE TO START', startY, 28, rgba(1, 0.9, 0.42, pulse));
|
|
1853
|
+
drawCentered(`TIMING ${ANALYZED_BPM} BPM | GRID ${ANALYZED_FIRST_BEAT.toFixed(3)}s | LEN ${ANALYZED_TRACK_DURATION.toFixed(1)}s`, timingY, 14, rgba(0.82, 0.95, 1.0, 0.78));
|
|
1854
|
+
drawCentered('M = BGM MUTE | H = SFX MUTE | N = RESTART MUSIC | ESC = QUIT', controlsY, 13, rgba(0.8, 0.94, 1.0, 0.7));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function drawResults() {
|
|
1858
|
+
aura.draw2d.rectFill(0, 0, worldWidth, worldHeight, rgba(0, 0, 0, 0.4));
|
|
1859
|
+
|
|
1860
|
+
const grade = gradeForResults();
|
|
1861
|
+
const acc = accuracyPercent();
|
|
1862
|
+
const diff = getSelectedDifficulty();
|
|
1863
|
+
|
|
1864
|
+
drawCentered(failed ? 'FAILED' : 'RESULTS', Math.floor(worldHeight * 0.16), 56, failed ? rgba(1, 0.35, 0.35, 1) : rgba(0.84, 0.96, 1, 1));
|
|
1865
|
+
drawCentered(`GRADE ${grade}`, Math.floor(worldHeight * 0.26), 52, rgba(1, 0.9, 0.48, 1));
|
|
1866
|
+
drawCentered(`DIFFICULTY ${diff.label}`, Math.floor(worldHeight * 0.32), 18, rgba(0.82, 0.95, 1.0, 0.92));
|
|
1867
|
+
|
|
1868
|
+
const left = Math.floor(worldWidth * 0.28);
|
|
1869
|
+
const top = Math.floor(worldHeight * 0.38);
|
|
1870
|
+
const line = 36;
|
|
1871
|
+
aura.draw2d.text(`SCORE ${score}`, left, top + (line * 0), { size: 28, color: white() });
|
|
1872
|
+
aura.draw2d.text(`ACCURACY ${acc.toFixed(2)}%`, left, top + (line * 1), { size: 28, color: white() });
|
|
1873
|
+
aura.draw2d.text(`MAX COMBO x${maxCombo}`, left, top + (line * 2), { size: 28, color: white() });
|
|
1874
|
+
aura.draw2d.text(`PERFECT ${perfectCount}`, left, top + (line * 3), { size: 24, color: rgba(0.43, 1.0, 0.64, 1) });
|
|
1875
|
+
aura.draw2d.text(`GOOD ${goodCount}`, left, top + (line * 4), { size: 24, color: rgba(0.47, 0.86, 1.0, 1) });
|
|
1876
|
+
aura.draw2d.text(`OK ${okCount}`, left, top + (line * 5), { size: 24, color: rgba(1.0, 0.88, 0.42, 1) });
|
|
1877
|
+
aura.draw2d.text(`MISS ${misses}`, left, top + (line * 6), { size: 24, color: rgba(1.0, 0.36, 0.38, 1) });
|
|
1878
|
+
|
|
1879
|
+
drawCentered('ENTER/SPACE = RESTART | ESC = TITLE', Math.floor(worldHeight * 0.9), 20, rgba(1, 1, 1, 0.92));
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function chooseHitTarget(cursorX, cursorY, options = {}) {
|
|
1883
|
+
const pickRadius = Number.isFinite(options.pickRadius) ? options.pickRadius : activeHitPickRadius;
|
|
1884
|
+
const timingPad = Number.isFinite(options.timingPad) ? options.timingPad : activeEarlyAssist;
|
|
1885
|
+
const onlyIndex = Number.isInteger(options.onlyIndex) ? options.onlyIndex : null;
|
|
1886
|
+
let best = null;
|
|
1887
|
+
let bestScore = Infinity;
|
|
1888
|
+
|
|
1889
|
+
for (const note of notes) {
|
|
1890
|
+
if (note.judged) continue;
|
|
1891
|
+
if (onlyIndex !== null && note.index !== onlyIndex) continue;
|
|
1892
|
+
const rawDt = currentTime - note.time;
|
|
1893
|
+
const adjusted = rawDt - TIMING_OFFSET;
|
|
1894
|
+
if (Math.abs(adjusted) > (activeWindowMiss + timingPad)) continue;
|
|
1895
|
+
|
|
1896
|
+
const pos = notePosition(note);
|
|
1897
|
+
const dx = cursorX - pos.x;
|
|
1898
|
+
const dy = cursorY - pos.y;
|
|
1899
|
+
const dist = Math.hypot(dx, dy);
|
|
1900
|
+
if (dist > pickRadius) continue;
|
|
1901
|
+
|
|
1902
|
+
const rank = (Math.abs(adjusted) * 1000) + (dist * 0.6);
|
|
1903
|
+
if (rank < bestScore) {
|
|
1904
|
+
bestScore = rank;
|
|
1905
|
+
best = { note, pos, dt: rawDt };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
return best;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function chooseKeyboardTarget(options = {}) {
|
|
1913
|
+
const timingPad = Number.isFinite(options.timingPad) ? options.timingPad : activeEarlyAssist;
|
|
1914
|
+
const onlyIndex = Number.isInteger(options.onlyIndex) ? options.onlyIndex : null;
|
|
1915
|
+
let best = null;
|
|
1916
|
+
let bestScore = Infinity;
|
|
1917
|
+
|
|
1918
|
+
for (const note of notes) {
|
|
1919
|
+
if (note.judged) continue;
|
|
1920
|
+
if (onlyIndex !== null && note.index !== onlyIndex) continue;
|
|
1921
|
+
const rawDt = currentTime - note.time;
|
|
1922
|
+
const adjusted = rawDt - TIMING_OFFSET;
|
|
1923
|
+
if (Math.abs(adjusted) > (activeWindowMiss + timingPad)) continue;
|
|
1924
|
+
const rank = Math.abs(adjusted);
|
|
1925
|
+
if (rank < bestScore) {
|
|
1926
|
+
bestScore = rank;
|
|
1927
|
+
best = {
|
|
1928
|
+
note,
|
|
1929
|
+
pos: notePosition(note),
|
|
1930
|
+
dt: rawDt,
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
return best;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function chooseEarlyBonkTarget(cursorX, cursorY, allowDistanceBypass) {
|
|
1939
|
+
let best = null;
|
|
1940
|
+
let bestRank = Infinity;
|
|
1941
|
+
const minEarly = activeWindowMiss + activeEarlyAssist;
|
|
1942
|
+
|
|
1943
|
+
for (const note of notes) {
|
|
1944
|
+
if (note.judged) continue;
|
|
1945
|
+
const rawDt = currentTime - note.time;
|
|
1946
|
+
const adjusted = rawDt - TIMING_OFFSET;
|
|
1947
|
+
|
|
1948
|
+
if (adjusted > -minEarly || adjusted < -activeBonkEarlyMax) continue;
|
|
1949
|
+
|
|
1950
|
+
const pos = notePosition(note);
|
|
1951
|
+
const dist = Math.hypot(cursorX - pos.x, cursorY - pos.y);
|
|
1952
|
+
if (!allowDistanceBypass && dist > BONK_CURSOR_RADIUS) continue;
|
|
1953
|
+
|
|
1954
|
+
const rank = Math.abs(adjusted) + (dist * 0.001);
|
|
1955
|
+
if (rank < bestRank) {
|
|
1956
|
+
bestRank = rank;
|
|
1957
|
+
best = { note, pos, dt: rawDt };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
return best;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function applyEarlyBonk(hitPos) {
|
|
1965
|
+
playSfx('miss', 0.65);
|
|
1966
|
+
judgementPop = {
|
|
1967
|
+
text: 'BONKED EARLY',
|
|
1968
|
+
age: 0,
|
|
1969
|
+
x: hitPos.x,
|
|
1970
|
+
y: hitPos.y,
|
|
1971
|
+
color: rgba(1.0, 0.55, 0.34, 1),
|
|
1972
|
+
};
|
|
1973
|
+
bursts.push({
|
|
1974
|
+
x: hitPos.x,
|
|
1975
|
+
y: hitPos.y,
|
|
1976
|
+
age: 0,
|
|
1977
|
+
color: rgba(1.0, 0.55, 0.34, 1),
|
|
1978
|
+
judgement: 'bonk',
|
|
1979
|
+
impact: 0.64,
|
|
1980
|
+
});
|
|
1981
|
+
screenShake = Math.max(screenShake, 0.08);
|
|
1982
|
+
beatGlow = Math.max(beatGlow, 0.06);
|
|
1983
|
+
flowMeter = Math.max(0, flowMeter - 0.03);
|
|
1984
|
+
health = clamp(health - 0.35, 0, HEALTH_MAX);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function updateInputIntent(dt) {
|
|
1988
|
+
const mousePress = mouseHitPressed();
|
|
1989
|
+
const mouseDown = mouseHitDown();
|
|
1990
|
+
const k1KeyPress = keyboardK1Pressed();
|
|
1991
|
+
const k2KeyPress = keyboardK2Pressed();
|
|
1992
|
+
const k1KeyDown = keyboardK1Down();
|
|
1993
|
+
const k2KeyDown = keyboardK2Down();
|
|
1994
|
+
|
|
1995
|
+
const k1Press = mousePress || k1KeyPress;
|
|
1996
|
+
const k2Press = k2KeyPress;
|
|
1997
|
+
|
|
1998
|
+
hitPressedThisFrame = k1Press || k2Press;
|
|
1999
|
+
mouseHoldActive = mouseDown;
|
|
2000
|
+
keyHoldActive = keyboardHitDown();
|
|
2001
|
+
k1Down = mouseDown || k1KeyDown;
|
|
2002
|
+
k2Down = k2KeyDown;
|
|
2003
|
+
|
|
2004
|
+
if (k1Press) {
|
|
2005
|
+
k1Pulse = 1;
|
|
2006
|
+
k1Flash = 1;
|
|
2007
|
+
}
|
|
2008
|
+
if (k2Press) {
|
|
2009
|
+
k2Pulse = 1;
|
|
2010
|
+
k2Flash = 1;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (hitPressedThisFrame) {
|
|
2014
|
+
hitBufferTimer = activePressBuffer;
|
|
2015
|
+
} else {
|
|
2016
|
+
hitBufferTimer = Math.max(0, hitBufferTimer - dt);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (!mouseHoldActive && !keyHoldActive && dragChainTimer <= 0 && hitBufferTimer <= 0) {
|
|
2020
|
+
dragExpectedIndex = -1;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function processHitInput() {
|
|
2025
|
+
const holdActive = mouseHoldActive || keyHoldActive;
|
|
2026
|
+
const bufferedTap = hitBufferTimer > 0;
|
|
2027
|
+
if (!holdActive && !bufferedTap) return;
|
|
2028
|
+
|
|
2029
|
+
if (holdActive && dragExpectedIndex >= 0 && dragChainTimer <= 0) {
|
|
2030
|
+
dragExpectedIndex = -1;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const mouse = getMouse();
|
|
2034
|
+
let target = null;
|
|
2035
|
+
let viaDrag = false;
|
|
2036
|
+
|
|
2037
|
+
if (holdActive && dragExpectedIndex >= 0 && dragChainTimer > 0) {
|
|
2038
|
+
target = chooseHitTarget(mouse.x, mouse.y, {
|
|
2039
|
+
onlyIndex: dragExpectedIndex,
|
|
2040
|
+
pickRadius: activeDragPickRadius,
|
|
2041
|
+
timingPad: activeDragEarlyAssist,
|
|
2042
|
+
});
|
|
2043
|
+
if (!target && keyHoldActive) {
|
|
2044
|
+
target = chooseKeyboardTarget({
|
|
2045
|
+
onlyIndex: dragExpectedIndex,
|
|
2046
|
+
timingPad: activeDragEarlyAssist,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
viaDrag = Boolean(target);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (!target && bufferedTap) {
|
|
2053
|
+
target = chooseHitTarget(mouse.x, mouse.y, {
|
|
2054
|
+
pickRadius: activeHitPickRadius + 8,
|
|
2055
|
+
timingPad: activeEarlyAssist + 0.018,
|
|
2056
|
+
});
|
|
2057
|
+
if (!target) {
|
|
2058
|
+
target = chooseKeyboardTarget({
|
|
2059
|
+
timingPad: activeEarlyAssist + 0.02,
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (!target) {
|
|
2065
|
+
if (hitPressedThisFrame) {
|
|
2066
|
+
const bonk = chooseEarlyBonkTarget(mouse.x, mouse.y, keyHoldActive);
|
|
2067
|
+
if (bonk) {
|
|
2068
|
+
applyEarlyBonk(bonk.pos);
|
|
2069
|
+
hitBufferTimer = 0;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const judgement = evaluateTimingDelta(target.dt);
|
|
2076
|
+
if (!judgement) return;
|
|
2077
|
+
|
|
2078
|
+
target.note.judged = true;
|
|
2079
|
+
target.note.judgement = judgement;
|
|
2080
|
+
applyJudgement(judgement, target.pos, { viaDrag });
|
|
2081
|
+
playNoteHitsound(target.note, judgement);
|
|
2082
|
+
hitBufferTimer = 0;
|
|
2083
|
+
|
|
2084
|
+
if (judgement === 'miss') {
|
|
2085
|
+
dragExpectedIndex = -1;
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if ((target.note.dragNextIndex !== null && target.note.dragNextIndex !== undefined)
|
|
2090
|
+
&& (holdActive || bufferedTap)) {
|
|
2091
|
+
dragExpectedIndex = target.note.dragNextIndex;
|
|
2092
|
+
dragChainTimer = DRAG_CHAIN_GRACE;
|
|
2093
|
+
} else {
|
|
2094
|
+
dragChainTimer = 0;
|
|
2095
|
+
dragExpectedIndex = -1;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function markExpiredMisses() {
|
|
2100
|
+
for (const note of notes) {
|
|
2101
|
+
if (note.judged) continue;
|
|
2102
|
+
if (currentTime <= note.time + activeWindowMiss) continue;
|
|
2103
|
+
|
|
2104
|
+
note.judged = true;
|
|
2105
|
+
note.judgement = 'miss';
|
|
2106
|
+
const pos = notePosition(note);
|
|
2107
|
+
applyJudgement('miss', pos);
|
|
2108
|
+
if (note.index === dragExpectedIndex) {
|
|
2109
|
+
dragExpectedIndex = -1;
|
|
2110
|
+
dragChainTimer = 0;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
function maybeCompleteRun() {
|
|
2116
|
+
if (health <= 0 && !failed) {
|
|
2117
|
+
failed = true;
|
|
2118
|
+
state = 'results';
|
|
2119
|
+
stopBgm();
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
if (currentTime < lastNoteTime + 1.6) return;
|
|
2124
|
+
for (const note of notes) {
|
|
2125
|
+
if (!note.judged) return;
|
|
2126
|
+
}
|
|
2127
|
+
state = 'results';
|
|
2128
|
+
stopBgm();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function updateEffects(dt) {
|
|
2132
|
+
beatGlow = Math.max(0, beatGlow - (dt * 1.7));
|
|
2133
|
+
comboFlash = Math.max(0, comboFlash - (dt * 2.6));
|
|
2134
|
+
screenShake = Math.max(0, screenShake - (dt * 2.4));
|
|
2135
|
+
flowMeter = Math.max(0, flowMeter - (dt * 0.05));
|
|
2136
|
+
dragChainTimer = Math.max(0, dragChainTimer - dt);
|
|
2137
|
+
k1Pulse = Math.max(0, k1Pulse - (dt * 4.8));
|
|
2138
|
+
k2Pulse = Math.max(0, k2Pulse - (dt * 4.8));
|
|
2139
|
+
k1Flash = Math.max(0, k1Flash - (dt * 8.2));
|
|
2140
|
+
k2Flash = Math.max(0, k2Flash - (dt * 8.2));
|
|
2141
|
+
|
|
2142
|
+
const mouse = getMouse();
|
|
2143
|
+
const last = cursorTrail.length > 0 ? cursorTrail[cursorTrail.length - 1] : null;
|
|
2144
|
+
if (!last || Math.hypot(last.x - mouse.x, last.y - mouse.y) > 2.5) {
|
|
2145
|
+
cursorTrail.push({ x: mouse.x, y: mouse.y, age: 0 });
|
|
2146
|
+
}
|
|
2147
|
+
for (const p of cursorTrail) {
|
|
2148
|
+
p.age += dt;
|
|
2149
|
+
}
|
|
2150
|
+
cursorTrail = cursorTrail.filter((p) => p.age < CURSOR_TRAIL_LIFE);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
aura.setup = async function () {
|
|
2154
|
+
assertRuntimeCapabilities();
|
|
2155
|
+
syncLayoutFromWindow();
|
|
2156
|
+
await loadBackgroundAsset();
|
|
2157
|
+
await loadSkinAssets();
|
|
2158
|
+
await loadBgmAsset();
|
|
2159
|
+
await loadSfxAssets();
|
|
2160
|
+
resetRun();
|
|
2161
|
+
state = 'title';
|
|
2162
|
+
bgmMuted = false;
|
|
2163
|
+
sfxMuted = false;
|
|
2164
|
+
sfxLastPlay.clear();
|
|
2165
|
+
bgmLastError = '';
|
|
2166
|
+
stopBgm();
|
|
2167
|
+
primePressLatch();
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
aura.update = function (dt) {
|
|
2171
|
+
syncLayoutFromWindow();
|
|
2172
|
+
pulseTime += dt;
|
|
2173
|
+
frameDt = dt;
|
|
2174
|
+
updateEffects(dt);
|
|
2175
|
+
if (inputPressed(['m'])) {
|
|
2176
|
+
toggleBgmMute();
|
|
2177
|
+
}
|
|
2178
|
+
if (inputPressed(['h'])) {
|
|
2179
|
+
toggleSfxMute();
|
|
2180
|
+
}
|
|
2181
|
+
if (state === 'playing' && inputPressed(['n'])) {
|
|
2182
|
+
startBgm(true);
|
|
2183
|
+
applyBgmVolume();
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (escapePressed()) {
|
|
2187
|
+
if (state !== 'title') {
|
|
2188
|
+
playSfx('uiBack');
|
|
2189
|
+
}
|
|
2190
|
+
state = 'title';
|
|
2191
|
+
resetRun();
|
|
2192
|
+
stopBgm();
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (state === 'title') {
|
|
2197
|
+
handleTitleDifficultyInput();
|
|
2198
|
+
if (startPressed()) {
|
|
2199
|
+
beginPlayingRun();
|
|
2200
|
+
}
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
if (state === 'results') {
|
|
2205
|
+
if (startPressed()) {
|
|
2206
|
+
beginPlayingRun();
|
|
2207
|
+
}
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
updateInputIntent(dt);
|
|
2212
|
+
currentTime += dt;
|
|
2213
|
+
processHitInput();
|
|
2214
|
+
markExpiredMisses();
|
|
2215
|
+
maybeCompleteRun();
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
aura.draw = function () {
|
|
2219
|
+
drawBackground();
|
|
2220
|
+
|
|
2221
|
+
if (state === 'playing' || state === 'results') {
|
|
2222
|
+
drawNotes();
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
drawHitBursts(frameDt);
|
|
2226
|
+
drawJudgementPopup(frameDt);
|
|
2227
|
+
drawHud();
|
|
2228
|
+
drawCursor();
|
|
2229
|
+
|
|
2230
|
+
if (state === 'title') {
|
|
2231
|
+
drawTitle();
|
|
2232
|
+
} else if (state === 'results') {
|
|
2233
|
+
drawResults();
|
|
2234
|
+
}
|
|
2235
|
+
};
|