-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame.js
More file actions
293 lines (255 loc) · 12.5 KB
/
Copy pathgame.js
File metadata and controls
293 lines (255 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// ECLIPSE RISING — Main Game Controller
// Comprehensive: all animations triggered, canvas fixed, full battle loop
import { saveManager } from "./saveManager.js";
import { audio } from "./audioManager.js";
import { inputManager } from "./inputManager.js";
import { bossManager } from "./bossManager.js";
import { combatManager } from "./combatManager.js";
import { uiManager } from "./uiManager.js";
import { animationManager } from "./animationManager.js";
import { Spells } from "./spells.js";
import { characterRenderer } from "./characterRenderer.js";
class GameController {
constructor() {
this.gameState = "loading";
this.currentBossId = null;
this.currentChar = "knight";
this.rendererReady = false;
this.activeSpellProgress = 0;
// Prevent double win/loss triggers
this._battleEnded = false;
}
start() {
console.log("Eclipse Rising — boot");
// Ambient eclipse background canvas
animationManager.init("bg-canvas");
// UI system (loads save, runs loading bar, wires all buttons)
uiManager.init({
onStartGame: (bossId, charClass) => this.handleStartGame(bossId, charClass),
onSettingsChange: () => { },
onCharacterSelectChange: (charClass) => { this.currentChar = charClass; saveManager.setCharacter(charClass); },
});
// Input — keyboard hold + future WebSocket
inputManager.init(
(spellName, progress) => this.onPoseProgress(spellName, progress),
(packet) => this.onPoseComplete(packet)
);
// Main game loop
requestAnimationFrame(t => this.loop(t));
}
// ─── BATTLE START ─────────────────────────────────────────────────────────
async handleStartGame(bossId, charClass) {
this.currentBossId = bossId;
this.currentChar = charClass || "knight";
this.gameState = "loading_battle";
this.rendererReady = false;
this._battleEnded = false;
// Boss AI + combat systems
bossManager.setupBoss(bossId);
combatManager.setupDuel(bossManager.currentBoss, saveManager.currentData, {
onUpdate: () => uiManager.updateBattleHUD(),
onSpellCompleted: (spell, dmg) => this.onPlayerHit(spell, dmg),
onPlayerHit: (dmg) => this.onBossHitsPlayer(dmg),
onPlayerHealed: (heal) => this.onPlayerHealed(heal),
onLevelUp: (lvl) => animationManager.emitHealBurst("#ffd97d"),
});
// Show battle screen — canvas now visible in DOM
uiManager.showScreen("screen-battle");
uiManager.updateBattleHUD();
// Re-show loading overlay (hidden after previous battle / retry)
const loadingOv = document.getElementById("battle-loading-overlay");
if (loadingOv) loadingOv.classList.remove("hidden");
// Init particle VFX overlay
animationManager.initArena("arena-canvas");
// [FIX Step 5] Always destroy any previous renderer instance before re-init.
// This prevents ghost WebGL contexts on replays / boss transitions.
console.log("[Game] Destroying previous characterRenderer instance.");
characterRenderer.destroy();
// [FIX Step 1+4] Defer renderer init by one rAF frame so the browser has
// committed the DOM paint and the canvas has non-zero layout dimensions.
const gender = charClass === "sorceress" ? "female" : "male";
requestAnimationFrame(() => {
// [FIX Step 2] Verify canvas exists and has real dimensions before init.
const canvas = document.getElementById("arena-canvas-3d");
if (!canvas) {
console.error("[Game] FATAL: arena-canvas-3d not found in DOM after rAF.");
this.gameState = "battle";
const ov = document.getElementById("battle-loading-overlay");
if (ov) ov.classList.add("hidden");
return;
}
const cW = canvas.clientWidth, cH = canvas.clientHeight;
console.log(`[Game] arena-canvas-3d dimensions: ${cW}×${cH} (offsetParent: ${canvas.offsetParent?.id ?? 'null'})`);
if (cW === 0 || cH === 0) {
console.warn("[Game] Canvas is zero-sized — waiting one more frame.");
// Try once more after another rAF if still zero (e.g. CSS transition not committed)
requestAnimationFrame(() => this._bootRenderer(gender, bossId));
return;
}
this._bootRenderer(gender, bossId);
});
}
// ─── RENDERER BOOT (called from rAF defer in handleStartGame) ────────────
_bootRenderer(gender, bossId) {
console.log(`[Game] _bootRenderer() → gender=${gender}, bossId=${bossId}`);
try {
characterRenderer.init("arena-canvas-3d", gender, bossId, () => {
console.log("[Game] characterRenderer.init() onLoaded callback fired ✓");
this.rendererReady = true;
this.gameState = "battle";
const ov = document.getElementById("battle-loading-overlay");
if (ov) ov.classList.add("hidden");
});
} catch (err) {
console.error("[Game] characterRenderer.init() threw:", err);
this.gameState = "battle";
this.rendererReady = false;
const ov = document.getElementById("battle-loading-overlay");
if (ov) ov.classList.add("hidden");
}
}
// ─── INPUT CALLBACKS ──────────────────────────────────────────────────────
onPoseProgress(spellName, progress) {
this.activeSpellProgress = progress;
if (!this.rendererReady) return;
// While the player is charging (holding the key), play the Hold animation
if (progress > 0.05) characterRenderer.setPlayerHolding();
else characterRenderer.setPlayerIdle();
}
onPoseComplete(packet) {
if (this.gameState !== "battle") return;
if (bossManager.state === "death" || bossManager.state === "spawning") return;
// Resolve the spell through combatManager — it will call back onSpellCompleted
// which triggers the cast animation + damage
combatManager.handlePoseInput(packet, bossManager.currentBoss.spellsAllowed);
}
// ─── PLAYER CASTS → BOSS STAGGERS ─────────────────────────────────────────
// Called by combatManager when a spell is confirmed (after hold duration met).
// The cast animation plays here; damage fires at the impact frame inside characterRenderer.
onPlayerHit(spellName, damage) {
if (this.rendererReady) {
// Play cast animation — damage fires at PLAYER_IMPACT_TIME inside the render loop
characterRenderer.startPlayerAttack(spellName, damage);
// Boss hitstagger always plays on every player spell
characterRenderer.triggerBossHitstagger();
characterRenderer.triggerShake(12, 0.22);
characterRenderer.triggerHitStop(0.08);
}
const spell = Spells[spellName];
animationManager.emitSpellImpact(spell ? spell.color : "#e8c99b", damage);
}
// ─── BOSS ATTACKS → PLAYER STAGGERS ──────────────────────────────────────
onBossHitsPlayer(damage) {
animationManager.emitBloodBurst("#ef4444");
uiManager.flashScreenHit();
if (this.rendererReady) {
characterRenderer.setPlayerHit();
characterRenderer.triggerShake(18, 0.30);
characterRenderer.triggerHitStop(0.12);
}
}
onPlayerHealed(heal) {
animationManager.emitHealBurst("#22c55e");
uiManager.flashScreenHeal();
}
// ─── MAIN GAME LOOP ───────────────────────────────────────────────────────
loop(timestamp) {
// Background eclipse canvas — always draws
animationManager.updateBg(timestamp);
// Battle tick — runs whether or not 3D renderer is ready
if (this.gameState === "battle") {
// Sync boss windup animation
if (this.rendererReady) {
const bState = bossManager.state;
if (bState === "windup") {
characterRenderer.startBossWindup();
characterRenderer.setBossWindupProgress(bossManager.actionProgress);
}
}
// Boss AI — always ticks
bossManager.update(timestamp, combatManager.playerLevel);
// VFX particles
animationManager.updateVFX(timestamp);
// Spell hold progress bar
const pf = document.getElementById("spell-hold-progress");
if (pf) pf.style.width = `${this.activeSpellProgress * 100}%`;
// Rune key pulse
this._pulseRune();
// Spell guide mini canvas
uiManager.tickSpellGuide(timestamp);
// Win / loss check
if (!this._battleEnded) this._checkBattleEnd();
}
requestAnimationFrame(t => this.loop(t));
}
_pulseRune() {
const spell = Spells[combatManager.currentRequestedSpell];
if (!spell) return;
document.querySelectorAll(".rune-slot").forEach(r => r.classList.remove("pulsing"));
const slot = document.getElementById(`rune-${spell.key}`);
if (slot) slot.classList.add("pulsing");
}
// ─── WIN / LOSS ───────────────────────────────────────────────────────────
_checkBattleEnd() {
// Defeat
if (combatManager.playerHp <= 0) {
this._battleEnded = true;
this.gameState = "defeat";
if (this.rendererReady && characterRenderer) {
characterRenderer.setPlayerState("death");
characterRenderer.setBossState("idle");
}
saveManager.updateStats({ highestCombo: combatManager.highestComboThisDuel });
setTimeout(() => uiManager.showDefeat(), 2200);
return;
}
// Victory
if (bossManager.hp <= 0) {
this._battleEnded = true;
this.gameState = "victory";
if (this.rendererReady) {
characterRenderer.triggerBossDeath();
characterRenderer.triggerShake(25, 0.5);
setTimeout(() => characterRenderer.setPlayerVictory(), 900);
}
animationManager.emitSpellImpact("#ffd97d", null);
// Victory screen after death animation completes (≈2.5s)
setTimeout(() => {
if (this.currentBossId === 4) {
this.gameState = "ending";
uiManager.showEnding();
} else {
uiManager.showVictory(this.currentBossId);
}
}, 2800);
}
}
}
const game = new GameController();
window.game = game;
window.triggerVictory = () => {
console.log("FAILSAFE VICTORY ACTIVATED");
if (game._battleEnded) return;
game._battleEnded = true;
game.gameState = "victory";
bossManager.hp = 0;
bossManager.state = 'death';
bossManager.hitbox = null; // Remove boss hitbox
if (game.rendererReady) {
characterRenderer.triggerBossDeath();
characterRenderer.triggerShake(25, 0.5);
setTimeout(() => characterRenderer.setPlayerVictory(), 900);
}
animationManager.emitSpellImpact("#ffd97d", null);
setTimeout(() => {
if (game.currentBossId === 4) {
game.gameState = "ending";
uiManager.showEnding();
} else {
uiManager.showVictory(game.currentBossId);
}
}, 500);
};
window.combatManager = combatManager;
window.bossManager = bossManager;
window.addEventListener("DOMContentLoaded", () => game.start());