@guinetik/gcanvas 1.0.4 → 1.0.5
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/dist/CNAME +1 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +517 -0
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +398 -0
- package/dist/isometric.html +34 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +863 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Game,
|
|
3
|
+
GameObject,
|
|
4
|
+
Text,
|
|
5
|
+
Rectangle,
|
|
6
|
+
Circle,
|
|
7
|
+
Keys,
|
|
8
|
+
FPSCounter,
|
|
9
|
+
PlatformerScene,
|
|
10
|
+
Collision,
|
|
11
|
+
Painter,
|
|
12
|
+
Position,
|
|
13
|
+
Group,
|
|
14
|
+
Synth,
|
|
15
|
+
Sprite,
|
|
16
|
+
Camera2D,
|
|
17
|
+
} from "/gcanvas.es.min.js";
|
|
18
|
+
|
|
19
|
+
// ==================== Configuration ====================
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
// Theme - Terminal/Tron aesthetic
|
|
22
|
+
theme: {
|
|
23
|
+
background: "#000000",
|
|
24
|
+
primary: "#00ff00", // Terminal green
|
|
25
|
+
secondary: "#0a0a0a",
|
|
26
|
+
accent: "#00cc00",
|
|
27
|
+
text: "#ffffff",
|
|
28
|
+
textDim: "#666666",
|
|
29
|
+
platform: "#1a1a1a",
|
|
30
|
+
platformLine: "#00ff00",
|
|
31
|
+
player: "#00ff00",
|
|
32
|
+
enemy: "#ff3333", // Red glitches
|
|
33
|
+
orb: "#00ffff", // Cyan data orbs
|
|
34
|
+
goal: "#ffff00", // Yellow end zone
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Physics - mathematically balanced for level design
|
|
38
|
+
// Standing jump: ~57px high, ~97px far
|
|
39
|
+
// Running jump: ~75px high, ~112px far
|
|
40
|
+
gravity: 1400,
|
|
41
|
+
jumpVelocity: -400, // base jump
|
|
42
|
+
jumpSpeedBonus: 0.35, // extra jump power from horizontal speed
|
|
43
|
+
maxSpeed: 170, // max horizontal speed
|
|
44
|
+
acceleration: 1000, // how fast you speed up
|
|
45
|
+
friction: 600, // how fast you slow down on ground
|
|
46
|
+
airFriction: 60, // low friction in air - maintain momentum
|
|
47
|
+
airControl: 1.0, // full control in air
|
|
48
|
+
jumpCooldown: 0.1, // seconds between jumps
|
|
49
|
+
pogoBoost: -500, // bounce velocity when stomping enemies (~89px high)
|
|
50
|
+
|
|
51
|
+
// Player
|
|
52
|
+
playerScale: 1.0,
|
|
53
|
+
|
|
54
|
+
// Camera
|
|
55
|
+
cameraLerp: 0.08,
|
|
56
|
+
cameraDeadzoneWidth: 120,
|
|
57
|
+
cameraDeadzoneHeight: 60,
|
|
58
|
+
|
|
59
|
+
// Sound
|
|
60
|
+
soundEnabled: true,
|
|
61
|
+
masterVolume: 0.3,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ==================== Level Data ====================
|
|
65
|
+
// Level uses relative Y positions (0 = ground level, negative = above ground)
|
|
66
|
+
// These are converted to screen coordinates based on game height
|
|
67
|
+
// Note: All positions are CENTER-based. For platforms, we place the center,
|
|
68
|
+
// and for entities, we need to offset by half their height to stand ON platforms.
|
|
69
|
+
const LEVEL_DATA = {
|
|
70
|
+
bounds: { minX: 0, maxX: 3000 },
|
|
71
|
+
spawn: { x: 80, yOffset: 0 },
|
|
72
|
+
|
|
73
|
+
// Platform design based on jump physics:
|
|
74
|
+
// Standing jump: 57px high, 97px far | Running jump: 75px high, 112px far
|
|
75
|
+
// Easy gap: <80px | Medium gap: 80-100px | Hard gap: 100-110px
|
|
76
|
+
// Easy height: <45px | Medium height: 45-55px | Hard height: 55-70px
|
|
77
|
+
platforms: [
|
|
78
|
+
// === ZONE 1: Tutorial Area ===
|
|
79
|
+
{ type: 'ground', x: 200, yOffset: 0, width: 400, height: 32 },
|
|
80
|
+
// Easy first jump (gap ~50px, same height)
|
|
81
|
+
{ type: 'floating', x: 480, yOffset: 0, width: 80, height: 16 },
|
|
82
|
+
|
|
83
|
+
// === ZONE 2: First Challenge ===
|
|
84
|
+
{ type: 'ground', x: 650, yOffset: 0, width: 200, height: 32 },
|
|
85
|
+
// Medium gap (~70px) with enemy
|
|
86
|
+
{ type: 'ground', x: 920, yOffset: 0, width: 200, height: 32 },
|
|
87
|
+
|
|
88
|
+
// === ZONE 3: Vertical Climb ===
|
|
89
|
+
// Stair stepping stones (gap ~60px, height +40px each)
|
|
90
|
+
{ type: 'floating', x: 1100, yOffset: -40, width: 80, height: 16 },
|
|
91
|
+
{ type: 'floating', x: 1220, yOffset: -40, width: 80, height: 16 },
|
|
92
|
+
// Upper bonus path (harder - need running jump, +65px height)
|
|
93
|
+
{ type: 'floating', x: 1160, yOffset: -105, width: 70, height: 16 },
|
|
94
|
+
|
|
95
|
+
// === ZONE 4: Platforming Gauntlet ===
|
|
96
|
+
{ type: 'ground', x: 1420, yOffset: 0, width: 180, height: 32 },
|
|
97
|
+
// Chain jumps (gap ~70px each, slight height variation)
|
|
98
|
+
{ type: 'floating', x: 1600, yOffset: -30, width: 70, height: 16 },
|
|
99
|
+
{ type: 'floating', x: 1730, yOffset: -30, width: 70, height: 16 },
|
|
100
|
+
|
|
101
|
+
// === ZONE 5: Enemy Gauntlet ===
|
|
102
|
+
{ type: 'ground', x: 1950, yOffset: 0, width: 300, height: 32 },
|
|
103
|
+
|
|
104
|
+
// === ZONE 6: Final Stretch ===
|
|
105
|
+
// Stepping stones (gap ~70px each)
|
|
106
|
+
{ type: 'floating', x: 2200, yOffset: -25, width: 80, height: 16 },
|
|
107
|
+
{ type: 'floating', x: 2330, yOffset: -25, width: 80, height: 16 },
|
|
108
|
+
{ type: 'floating', x: 2460, yOffset: -25, width: 80, height: 16 },
|
|
109
|
+
|
|
110
|
+
// === ZONE 7: Victory Road ===
|
|
111
|
+
{ type: 'ground', x: 2680, yOffset: 0, width: 350, height: 32 },
|
|
112
|
+
],
|
|
113
|
+
|
|
114
|
+
enemies: [
|
|
115
|
+
// Zone 2 - first enemy encounter
|
|
116
|
+
{ x: 920, yOffset: 0, patrolStart: 840, patrolEnd: 1000 },
|
|
117
|
+
// Zone 4 - ground patrol
|
|
118
|
+
{ x: 1420, yOffset: 0, patrolStart: 1350, patrolEnd: 1490 },
|
|
119
|
+
// Zone 5 - enemy gauntlet (2 enemies!)
|
|
120
|
+
{ x: 1900, yOffset: 0, patrolStart: 1820, patrolEnd: 1980 },
|
|
121
|
+
{ x: 2020, yOffset: 0, patrolStart: 1940, patrolEnd: 2080 },
|
|
122
|
+
// Zone 7 - final guard
|
|
123
|
+
{ x: 2680, yOffset: 0, patrolStart: 2530, patrolEnd: 2830 },
|
|
124
|
+
],
|
|
125
|
+
|
|
126
|
+
orbs: [
|
|
127
|
+
// Zone 1 - Easy pickups (above ground)
|
|
128
|
+
{ x: 100, yOffset: -40, value: 10 },
|
|
129
|
+
{ x: 200, yOffset: -40, value: 10 },
|
|
130
|
+
{ x: 300, yOffset: -40, value: 10 },
|
|
131
|
+
{ x: 480, yOffset: -35, value: 15 }, // On first platform
|
|
132
|
+
|
|
133
|
+
// Zone 2
|
|
134
|
+
{ x: 650, yOffset: -40, value: 10 },
|
|
135
|
+
{ x: 780, yOffset: -50, value: 15 }, // In the gap (risky!)
|
|
136
|
+
{ x: 920, yOffset: -40, value: 10 },
|
|
137
|
+
|
|
138
|
+
// Zone 3 - Stair rewards
|
|
139
|
+
{ x: 1100, yOffset: -75, value: 15 },
|
|
140
|
+
{ x: 1220, yOffset: -75, value: 15 },
|
|
141
|
+
// Bonus path reward (on high platform)
|
|
142
|
+
{ x: 1160, yOffset: -140, value: 100 },
|
|
143
|
+
|
|
144
|
+
// Zone 4 - Gauntlet rewards
|
|
145
|
+
{ x: 1420, yOffset: -40, value: 10 },
|
|
146
|
+
{ x: 1600, yOffset: -65, value: 20 },
|
|
147
|
+
{ x: 1730, yOffset: -65, value: 20 },
|
|
148
|
+
|
|
149
|
+
// Zone 5 - Near enemies
|
|
150
|
+
{ x: 1850, yOffset: -40, value: 10 },
|
|
151
|
+
{ x: 1950, yOffset: -40, value: 10 },
|
|
152
|
+
{ x: 2050, yOffset: -40, value: 10 },
|
|
153
|
+
|
|
154
|
+
// Zone 6 - Stepping stones
|
|
155
|
+
{ x: 2200, yOffset: -60, value: 15 },
|
|
156
|
+
{ x: 2330, yOffset: -60, value: 20 },
|
|
157
|
+
{ x: 2460, yOffset: -60, value: 25 },
|
|
158
|
+
|
|
159
|
+
// Zone 7 - Victory orbs
|
|
160
|
+
{ x: 2600, yOffset: -40, value: 10 },
|
|
161
|
+
{ x: 2700, yOffset: -40, value: 10 },
|
|
162
|
+
{ x: 2800, yOffset: -40, value: 10 },
|
|
163
|
+
],
|
|
164
|
+
|
|
165
|
+
endZone: { x: 2900, yOffset: -80, width: 80, height: 160 },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ==================== Sound Effects ====================
|
|
169
|
+
class SFX {
|
|
170
|
+
static initialized = false;
|
|
171
|
+
|
|
172
|
+
static init() {
|
|
173
|
+
if (this.initialized || !CONFIG.soundEnabled) return;
|
|
174
|
+
Synth.init({ masterVolume: CONFIG.masterVolume });
|
|
175
|
+
this.initialized = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static async resume() {
|
|
179
|
+
if (!this.initialized) return;
|
|
180
|
+
await Synth.resume();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static jump() {
|
|
184
|
+
if (!this.initialized) return;
|
|
185
|
+
Synth.osc.sweep(200, 500, 0.12, {
|
|
186
|
+
type: "square",
|
|
187
|
+
volume: 0.2,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static collect() {
|
|
192
|
+
if (!this.initialized) return;
|
|
193
|
+
const now = Synth.now;
|
|
194
|
+
Synth.osc.tone(880, 0.08, {
|
|
195
|
+
type: "sine",
|
|
196
|
+
volume: 0.15,
|
|
197
|
+
attack: 0.01,
|
|
198
|
+
release: 0.05,
|
|
199
|
+
startTime: now,
|
|
200
|
+
});
|
|
201
|
+
Synth.osc.tone(1320, 0.1, {
|
|
202
|
+
type: "sine",
|
|
203
|
+
volume: 0.15,
|
|
204
|
+
attack: 0.01,
|
|
205
|
+
release: 0.08,
|
|
206
|
+
startTime: now + 0.06,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static stomp() {
|
|
211
|
+
if (!this.initialized) return;
|
|
212
|
+
Synth.osc.sweep(300, 100, 0.15, {
|
|
213
|
+
type: "square",
|
|
214
|
+
volume: 0.25,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static death() {
|
|
219
|
+
if (!this.initialized) return;
|
|
220
|
+
const now = Synth.now;
|
|
221
|
+
const notes = [400, 300, 200, 100];
|
|
222
|
+
notes.forEach((freq, i) => {
|
|
223
|
+
Synth.osc.tone(freq, 0.2, {
|
|
224
|
+
type: "sawtooth",
|
|
225
|
+
volume: 0.2,
|
|
226
|
+
attack: 0.01,
|
|
227
|
+
release: 0.15,
|
|
228
|
+
startTime: now + i * 0.1,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static complete() {
|
|
234
|
+
if (!this.initialized) return;
|
|
235
|
+
const now = Synth.now;
|
|
236
|
+
const notes = [523, 659, 784, 1047];
|
|
237
|
+
notes.forEach((freq, i) => {
|
|
238
|
+
Synth.osc.tone(freq, 0.2, {
|
|
239
|
+
type: "square",
|
|
240
|
+
volume: 0.2,
|
|
241
|
+
attack: 0.01,
|
|
242
|
+
sustain: 0.7,
|
|
243
|
+
release: 0.1,
|
|
244
|
+
startTime: now + i * 0.12,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static start() {
|
|
250
|
+
if (!this.initialized) return;
|
|
251
|
+
Synth.osc.sweep(150, 500, 0.2, {
|
|
252
|
+
type: "square",
|
|
253
|
+
volume: 0.15,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ==================== HackerShapeFactory ====================
|
|
259
|
+
class HackerShapeFactory {
|
|
260
|
+
static createFrame(options = {}) {
|
|
261
|
+
const color = options.color || CONFIG.theme.player;
|
|
262
|
+
const scale = options.scale || 1;
|
|
263
|
+
const pose = options.pose || 'stand';
|
|
264
|
+
const px = 2 * scale;
|
|
265
|
+
|
|
266
|
+
const group = new Group({});
|
|
267
|
+
|
|
268
|
+
// Head (hood/helmet) - 8px wide
|
|
269
|
+
const headPixels = [
|
|
270
|
+
{ x: 1, y: 0, w: 6, h: 1 }, // top of hood
|
|
271
|
+
{ x: 0, y: 1, w: 8, h: 1 }, // hood wider
|
|
272
|
+
{ x: 0, y: 2, w: 8, h: 1 }, // hood
|
|
273
|
+
// Visor/face gap at y:3
|
|
274
|
+
{ x: 0, y: 3, w: 2, h: 1 }, // left hood
|
|
275
|
+
{ x: 6, y: 3, w: 2, h: 1 }, // right hood
|
|
276
|
+
{ x: 0, y: 4, w: 8, h: 1 }, // chin
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// Visor (eyes) - glowing accent
|
|
280
|
+
const visorPixels = [
|
|
281
|
+
{ x: 2, y: 3, w: 4, h: 1, isVisor: true },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
// Body (coat/jacket)
|
|
285
|
+
const bodyPixels = [
|
|
286
|
+
{ x: 1, y: 5, w: 6, h: 1 }, // shoulders
|
|
287
|
+
{ x: 0, y: 6, w: 8, h: 1 }, // torso
|
|
288
|
+
{ x: 0, y: 7, w: 8, h: 1 }, // torso
|
|
289
|
+
{ x: 0, y: 8, w: 8, h: 1 }, // waist
|
|
290
|
+
{ x: 0, y: 9, w: 8, h: 1 }, // coat bottom
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
// Legs based on pose
|
|
294
|
+
let legPixels = [];
|
|
295
|
+
switch (pose) {
|
|
296
|
+
case 'walk1':
|
|
297
|
+
// Left leg forward, right back
|
|
298
|
+
legPixels = [
|
|
299
|
+
{ x: 0, y: 10, w: 3, h: 1 },
|
|
300
|
+
{ x: 5, y: 10, w: 3, h: 1 },
|
|
301
|
+
{ x: -1, y: 11, w: 3, h: 1 },
|
|
302
|
+
{ x: 6, y: 11, w: 2, h: 1 },
|
|
303
|
+
{ x: -2, y: 12, w: 3, h: 1 },
|
|
304
|
+
{ x: 6, y: 12, w: 2, h: 1 },
|
|
305
|
+
];
|
|
306
|
+
break;
|
|
307
|
+
case 'walk2':
|
|
308
|
+
// Right leg forward, left back
|
|
309
|
+
legPixels = [
|
|
310
|
+
{ x: 0, y: 10, w: 3, h: 1 },
|
|
311
|
+
{ x: 5, y: 10, w: 3, h: 1 },
|
|
312
|
+
{ x: 0, y: 11, w: 2, h: 1 },
|
|
313
|
+
{ x: 6, y: 11, w: 3, h: 1 },
|
|
314
|
+
{ x: 0, y: 12, w: 2, h: 1 },
|
|
315
|
+
{ x: 7, y: 12, w: 3, h: 1 },
|
|
316
|
+
];
|
|
317
|
+
break;
|
|
318
|
+
case 'jump':
|
|
319
|
+
// Legs tucked
|
|
320
|
+
legPixels = [
|
|
321
|
+
{ x: 1, y: 10, w: 6, h: 1 },
|
|
322
|
+
{ x: 2, y: 11, w: 4, h: 1 },
|
|
323
|
+
{ x: 2, y: 12, w: 4, h: 1 },
|
|
324
|
+
];
|
|
325
|
+
break;
|
|
326
|
+
case 'stand':
|
|
327
|
+
default:
|
|
328
|
+
// Standing straight
|
|
329
|
+
legPixels = [
|
|
330
|
+
{ x: 1, y: 10, w: 2, h: 1 },
|
|
331
|
+
{ x: 5, y: 10, w: 2, h: 1 },
|
|
332
|
+
{ x: 1, y: 11, w: 2, h: 1 },
|
|
333
|
+
{ x: 5, y: 11, w: 2, h: 1 },
|
|
334
|
+
{ x: 0, y: 12, w: 3, h: 1 },
|
|
335
|
+
{ x: 5, y: 12, w: 3, h: 1 },
|
|
336
|
+
];
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Centering offset
|
|
341
|
+
const offsetX = 4 * px;
|
|
342
|
+
const offsetY = 6 * px;
|
|
343
|
+
|
|
344
|
+
// Draw all body parts
|
|
345
|
+
const allPixels = [...headPixels, ...bodyPixels, ...legPixels];
|
|
346
|
+
allPixels.forEach(p => {
|
|
347
|
+
group.add(new Rectangle({
|
|
348
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
349
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
350
|
+
width: p.w * px,
|
|
351
|
+
height: p.h * px,
|
|
352
|
+
color: color,
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Draw visor (accent color)
|
|
357
|
+
visorPixels.forEach(p => {
|
|
358
|
+
group.add(new Rectangle({
|
|
359
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
360
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
361
|
+
width: p.w * px,
|
|
362
|
+
height: p.h * px,
|
|
363
|
+
color: CONFIG.theme.orb, // Cyan visor
|
|
364
|
+
}));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return group;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
static createWalkFrames(options = {}) {
|
|
371
|
+
return [
|
|
372
|
+
this.createFrame({ ...options, pose: 'walk1' }),
|
|
373
|
+
this.createFrame({ ...options, pose: 'stand' }),
|
|
374
|
+
this.createFrame({ ...options, pose: 'walk2' }),
|
|
375
|
+
this.createFrame({ ...options, pose: 'stand' }),
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
static createIdleFrame(options = {}) {
|
|
380
|
+
return this.createFrame({ ...options, pose: 'stand' });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
static createJumpFrame(options = {}) {
|
|
384
|
+
return this.createFrame({ ...options, pose: 'jump' });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
static createDeathFrame(options = {}) {
|
|
388
|
+
const color = options.color || CONFIG.theme.player;
|
|
389
|
+
const scale = options.scale || 1;
|
|
390
|
+
const px = 2 * scale;
|
|
391
|
+
|
|
392
|
+
const group = new Group({});
|
|
393
|
+
|
|
394
|
+
// Fallen/collapsed pose - lying on side
|
|
395
|
+
const pixels = [
|
|
396
|
+
// Head tilted
|
|
397
|
+
{ x: 0, y: 2, w: 1, h: 6 },
|
|
398
|
+
{ x: 1, y: 1, w: 1, h: 8 },
|
|
399
|
+
{ x: 2, y: 1, w: 1, h: 8 },
|
|
400
|
+
{ x: 3, y: 2, w: 1, h: 6 },
|
|
401
|
+
// Body sprawled
|
|
402
|
+
{ x: 4, y: 3, w: 1, h: 4 },
|
|
403
|
+
{ x: 5, y: 3, w: 1, h: 4 },
|
|
404
|
+
{ x: 6, y: 3, w: 1, h: 4 },
|
|
405
|
+
{ x: 7, y: 4, w: 1, h: 2 },
|
|
406
|
+
// Legs
|
|
407
|
+
{ x: 8, y: 4, w: 2, h: 2 },
|
|
408
|
+
{ x: 10, y: 5, w: 2, h: 1 },
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
// Visor (dim, dying)
|
|
412
|
+
const visorPixels = [
|
|
413
|
+
{ x: 1, y: 4, w: 2, h: 1 },
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const offsetX = 6 * px;
|
|
417
|
+
const offsetY = 3 * px;
|
|
418
|
+
|
|
419
|
+
pixels.forEach(p => {
|
|
420
|
+
group.add(new Rectangle({
|
|
421
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
422
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
423
|
+
width: p.w * px,
|
|
424
|
+
height: p.h * px,
|
|
425
|
+
color: color,
|
|
426
|
+
}));
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Dim visor
|
|
430
|
+
visorPixels.forEach(p => {
|
|
431
|
+
group.add(new Rectangle({
|
|
432
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
433
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
434
|
+
width: p.w * px,
|
|
435
|
+
height: p.h * px,
|
|
436
|
+
color: CONFIG.theme.enemy, // Red when dying
|
|
437
|
+
}));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return group;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ==================== Player ====================
|
|
445
|
+
class Player extends Sprite {
|
|
446
|
+
constructor(game, options = {}) {
|
|
447
|
+
super(game, {
|
|
448
|
+
...options,
|
|
449
|
+
frameRate: 10,
|
|
450
|
+
loop: true,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Actual pixel art dimensions: 8 cols x 13 rows at 2px each = 16x26
|
|
454
|
+
this.width = 16;
|
|
455
|
+
this.height = 26;
|
|
456
|
+
this.vx = 0;
|
|
457
|
+
this.vy = 0;
|
|
458
|
+
this._grounded = false;
|
|
459
|
+
this.facingRight = true;
|
|
460
|
+
this._isMoving = false;
|
|
461
|
+
this.isDead = false;
|
|
462
|
+
this.jumpCooldownTimer = 0;
|
|
463
|
+
|
|
464
|
+
this.buildAnimations();
|
|
465
|
+
this.stopAnimation('idle');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
canJump() {
|
|
469
|
+
return this._grounded && this.jumpCooldownTimer <= 0 && !this.isDead;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
jump() {
|
|
473
|
+
if (!this.canJump()) return false;
|
|
474
|
+
// Base jump + bonus from horizontal speed (running jumps go higher!)
|
|
475
|
+
const speedBonus = Math.abs(this.vx) * CONFIG.jumpSpeedBonus;
|
|
476
|
+
this.vy = CONFIG.jumpVelocity - speedBonus;
|
|
477
|
+
this._grounded = false;
|
|
478
|
+
this.jumpCooldownTimer = CONFIG.jumpCooldown;
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Pogo bounce when stomping enemies
|
|
483
|
+
pogo() {
|
|
484
|
+
this.vy = CONFIG.pogoBoost;
|
|
485
|
+
this._grounded = false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
buildAnimations() {
|
|
489
|
+
this._animations.clear();
|
|
490
|
+
|
|
491
|
+
const frameOptions = {
|
|
492
|
+
color: CONFIG.theme.player,
|
|
493
|
+
scale: CONFIG.playerScale,
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
this.addAnimation('walk', HackerShapeFactory.createWalkFrames(frameOptions), {
|
|
497
|
+
frameRate: 10,
|
|
498
|
+
});
|
|
499
|
+
this.addAnimation('idle', [HackerShapeFactory.createIdleFrame(frameOptions)], {
|
|
500
|
+
loop: false,
|
|
501
|
+
});
|
|
502
|
+
this.addAnimation('jump', [HackerShapeFactory.createJumpFrame(frameOptions)], {
|
|
503
|
+
loop: false,
|
|
504
|
+
});
|
|
505
|
+
this.addAnimation('death', [HackerShapeFactory.createDeathFrame(frameOptions)], {
|
|
506
|
+
loop: false,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
die() {
|
|
511
|
+
if (this.isDead) return;
|
|
512
|
+
this.isDead = true;
|
|
513
|
+
this.vx = 0;
|
|
514
|
+
this.vy = 0;
|
|
515
|
+
this.playAnimation('death');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
update(dt) {
|
|
519
|
+
super.update(dt);
|
|
520
|
+
|
|
521
|
+
// Tick jump cooldown
|
|
522
|
+
if (this.jumpCooldownTimer > 0) {
|
|
523
|
+
this.jumpCooldownTimer -= dt;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Don't update animation state machine if dead
|
|
527
|
+
if (this.isDead) return;
|
|
528
|
+
|
|
529
|
+
// Flip sprite based on direction
|
|
530
|
+
this.scaleX = this.facingRight ? 1 : -1;
|
|
531
|
+
|
|
532
|
+
// Animation state machine
|
|
533
|
+
if (!this._grounded) {
|
|
534
|
+
if (this.currentAnimationName !== 'jump') {
|
|
535
|
+
this.playAnimation('jump');
|
|
536
|
+
}
|
|
537
|
+
} else if (this._isMoving) {
|
|
538
|
+
if (this.currentAnimationName !== 'walk') {
|
|
539
|
+
this.playAnimation('walk');
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
if (this.currentAnimationName !== 'idle') {
|
|
543
|
+
this.stopAnimation('idle');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
getBounds() {
|
|
549
|
+
const shrink = 2;
|
|
550
|
+
return {
|
|
551
|
+
x: this.x - this.width / 2 + shrink,
|
|
552
|
+
y: this.y - this.height / 2 + shrink,
|
|
553
|
+
width: this.width - shrink * 2,
|
|
554
|
+
height: this.height - shrink * 2,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ==================== Platform ====================
|
|
560
|
+
class Platform extends GameObject {
|
|
561
|
+
constructor(game, options = {}) {
|
|
562
|
+
super(game, options);
|
|
563
|
+
this.width = options.width || 100;
|
|
564
|
+
this.height = options.height || 16;
|
|
565
|
+
this.type = options.type || 'floating';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
draw() {
|
|
569
|
+
super.draw();
|
|
570
|
+
|
|
571
|
+
Painter.useCtx((ctx) => {
|
|
572
|
+
const w = this.width;
|
|
573
|
+
const h = this.height;
|
|
574
|
+
|
|
575
|
+
// Main platform body
|
|
576
|
+
ctx.fillStyle = CONFIG.theme.platform;
|
|
577
|
+
ctx.fillRect(-w / 2, -h / 2, w, h);
|
|
578
|
+
|
|
579
|
+
// Top glow line
|
|
580
|
+
ctx.fillStyle = CONFIG.theme.platformLine;
|
|
581
|
+
ctx.fillRect(-w / 2, -h / 2, w, 2);
|
|
582
|
+
|
|
583
|
+
// Grid lines for Tron effect
|
|
584
|
+
ctx.strokeStyle = CONFIG.theme.platformLine;
|
|
585
|
+
ctx.globalAlpha = 0.2;
|
|
586
|
+
ctx.lineWidth = 1;
|
|
587
|
+
|
|
588
|
+
// Vertical grid
|
|
589
|
+
const gridSpacing = 20;
|
|
590
|
+
for (let x = -w / 2 + gridSpacing; x < w / 2; x += gridSpacing) {
|
|
591
|
+
ctx.beginPath();
|
|
592
|
+
ctx.moveTo(x, -h / 2 + 2);
|
|
593
|
+
ctx.lineTo(x, h / 2);
|
|
594
|
+
ctx.stroke();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
ctx.globalAlpha = 1;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
getBounds() {
|
|
602
|
+
return {
|
|
603
|
+
x: this.x - this.width / 2,
|
|
604
|
+
y: this.y - this.height / 2,
|
|
605
|
+
width: this.width,
|
|
606
|
+
height: this.height,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ==================== GlitchShapeFactory ====================
|
|
612
|
+
class GlitchShapeFactory {
|
|
613
|
+
static createFrame(options = {}) {
|
|
614
|
+
const color = options.color || CONFIG.theme.enemy;
|
|
615
|
+
const scale = options.scale || 1;
|
|
616
|
+
const pose = options.pose || 'walk1';
|
|
617
|
+
const px = 2 * scale;
|
|
618
|
+
|
|
619
|
+
const group = new Group({});
|
|
620
|
+
|
|
621
|
+
// Glitchy corrupted creature body
|
|
622
|
+
const bodyPixels = [
|
|
623
|
+
// Head (glitchy/corrupted)
|
|
624
|
+
{ x: 2, y: 0, w: 4, h: 1 },
|
|
625
|
+
{ x: 1, y: 1, w: 6, h: 1 },
|
|
626
|
+
{ x: 0, y: 2, w: 8, h: 1 },
|
|
627
|
+
{ x: 0, y: 3, w: 8, h: 1 },
|
|
628
|
+
// Body
|
|
629
|
+
{ x: 1, y: 4, w: 6, h: 1 },
|
|
630
|
+
{ x: 1, y: 5, w: 6, h: 1 },
|
|
631
|
+
{ x: 2, y: 6, w: 4, h: 1 },
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
// Evil eyes
|
|
635
|
+
const eyePixels = [
|
|
636
|
+
{ x: 2, y: 2, w: 1, h: 1, isEye: true },
|
|
637
|
+
{ x: 5, y: 2, w: 1, h: 1, isEye: true },
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
// Legs based on pose
|
|
641
|
+
let legPixels = [];
|
|
642
|
+
if (pose === 'walk1') {
|
|
643
|
+
legPixels = [
|
|
644
|
+
{ x: 1, y: 7, w: 2, h: 1 },
|
|
645
|
+
{ x: 5, y: 7, w: 2, h: 1 },
|
|
646
|
+
{ x: 0, y: 8, w: 2, h: 1 },
|
|
647
|
+
{ x: 6, y: 8, w: 2, h: 1 },
|
|
648
|
+
];
|
|
649
|
+
} else {
|
|
650
|
+
legPixels = [
|
|
651
|
+
{ x: 2, y: 7, w: 2, h: 1 },
|
|
652
|
+
{ x: 4, y: 7, w: 2, h: 1 },
|
|
653
|
+
{ x: 2, y: 8, w: 2, h: 1 },
|
|
654
|
+
{ x: 4, y: 8, w: 2, h: 1 },
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const offsetX = 4 * px;
|
|
659
|
+
const offsetY = 4 * px;
|
|
660
|
+
|
|
661
|
+
[...bodyPixels, ...legPixels].forEach(p => {
|
|
662
|
+
group.add(new Rectangle({
|
|
663
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
664
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
665
|
+
width: p.w * px,
|
|
666
|
+
height: p.h * px,
|
|
667
|
+
color: color,
|
|
668
|
+
}));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Eyes (white/glowing)
|
|
672
|
+
eyePixels.forEach(p => {
|
|
673
|
+
group.add(new Rectangle({
|
|
674
|
+
x: p.x * px - offsetX + (p.w * px) / 2,
|
|
675
|
+
y: p.y * px - offsetY + (p.h * px) / 2,
|
|
676
|
+
width: p.w * px,
|
|
677
|
+
height: p.h * px,
|
|
678
|
+
color: "#ffffff",
|
|
679
|
+
}));
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
return group;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
static createWalkFrames(options = {}) {
|
|
686
|
+
return [
|
|
687
|
+
this.createFrame({ ...options, pose: 'walk1' }),
|
|
688
|
+
this.createFrame({ ...options, pose: 'walk2' }),
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ==================== GlitchEnemy ====================
|
|
694
|
+
class GlitchEnemy extends Sprite {
|
|
695
|
+
constructor(game, options = {}) {
|
|
696
|
+
super(game, {
|
|
697
|
+
...options,
|
|
698
|
+
frameRate: 6,
|
|
699
|
+
loop: true,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Actual pixel art dimensions: 8 cols x 9 rows at 2px each = 16x18
|
|
703
|
+
this.width = 16;
|
|
704
|
+
this.height = 18;
|
|
705
|
+
this.patrolStart = options.patrolStart || this.x - 50;
|
|
706
|
+
this.patrolEnd = options.patrolEnd || this.x + 50;
|
|
707
|
+
this.patrolSpeed = options.patrolSpeed || 60;
|
|
708
|
+
this.direction = 1;
|
|
709
|
+
this.isDead = false;
|
|
710
|
+
|
|
711
|
+
this.buildAnimations();
|
|
712
|
+
this.playAnimation('walk');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
buildAnimations() {
|
|
716
|
+
this._animations.clear();
|
|
717
|
+
|
|
718
|
+
const frameOptions = {
|
|
719
|
+
color: CONFIG.theme.enemy,
|
|
720
|
+
scale: 1,
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
this.addAnimation('walk', GlitchShapeFactory.createWalkFrames(frameOptions), {
|
|
724
|
+
frameRate: 6,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
update(dt) {
|
|
729
|
+
if (this.isDead) return;
|
|
730
|
+
|
|
731
|
+
super.update(dt);
|
|
732
|
+
|
|
733
|
+
// Patrol movement
|
|
734
|
+
this.x += this.direction * this.patrolSpeed * dt;
|
|
735
|
+
|
|
736
|
+
// Reverse at patrol bounds
|
|
737
|
+
if (this.x >= this.patrolEnd) {
|
|
738
|
+
this.x = this.patrolEnd;
|
|
739
|
+
this.direction = -1;
|
|
740
|
+
this.scaleX = -1;
|
|
741
|
+
} else if (this.x <= this.patrolStart) {
|
|
742
|
+
this.x = this.patrolStart;
|
|
743
|
+
this.direction = 1;
|
|
744
|
+
this.scaleX = 1;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
die() {
|
|
749
|
+
this.isDead = true;
|
|
750
|
+
this.visible = false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
getBounds() {
|
|
754
|
+
const shrink = 2;
|
|
755
|
+
return {
|
|
756
|
+
x: this.x - this.width / 2 + shrink,
|
|
757
|
+
y: this.y - this.height / 2 + shrink,
|
|
758
|
+
width: this.width - shrink * 2,
|
|
759
|
+
height: this.height - shrink * 2,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ==================== DataOrb ====================
|
|
765
|
+
class DataOrb extends GameObject {
|
|
766
|
+
constructor(game, options = {}) {
|
|
767
|
+
super(game, options);
|
|
768
|
+
this.radius = 10;
|
|
769
|
+
this.value = options.value || 10;
|
|
770
|
+
this.collected = false;
|
|
771
|
+
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
update(dt) {
|
|
775
|
+
super.update(dt);
|
|
776
|
+
this.pulsePhase += dt * 4;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
draw() {
|
|
780
|
+
if (this.collected) return;
|
|
781
|
+
super.draw();
|
|
782
|
+
|
|
783
|
+
const pulse = 1 + Math.sin(this.pulsePhase) * 0.2;
|
|
784
|
+
const r = this.radius * pulse;
|
|
785
|
+
|
|
786
|
+
Painter.useCtx((ctx) => {
|
|
787
|
+
// Glow effect
|
|
788
|
+
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, r * 2);
|
|
789
|
+
gradient.addColorStop(0, CONFIG.theme.orb);
|
|
790
|
+
gradient.addColorStop(0.5, CONFIG.theme.orb + "44");
|
|
791
|
+
gradient.addColorStop(1, "transparent");
|
|
792
|
+
|
|
793
|
+
ctx.fillStyle = gradient;
|
|
794
|
+
ctx.beginPath();
|
|
795
|
+
ctx.arc(0, 0, r * 2, 0, Math.PI * 2);
|
|
796
|
+
ctx.fill();
|
|
797
|
+
|
|
798
|
+
// Core
|
|
799
|
+
ctx.fillStyle = CONFIG.theme.orb;
|
|
800
|
+
ctx.beginPath();
|
|
801
|
+
ctx.arc(0, 0, r * 0.6, 0, Math.PI * 2);
|
|
802
|
+
ctx.fill();
|
|
803
|
+
|
|
804
|
+
// Inner highlight
|
|
805
|
+
ctx.fillStyle = "#ffffff";
|
|
806
|
+
ctx.globalAlpha = 0.7;
|
|
807
|
+
ctx.beginPath();
|
|
808
|
+
ctx.arc(-r * 0.2, -r * 0.2, r * 0.2, 0, Math.PI * 2);
|
|
809
|
+
ctx.fill();
|
|
810
|
+
ctx.globalAlpha = 1;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
collect() {
|
|
815
|
+
if (this.collected) return false;
|
|
816
|
+
this.collected = true;
|
|
817
|
+
this.visible = false;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
getBounds() {
|
|
822
|
+
return {
|
|
823
|
+
x: this.x - this.radius,
|
|
824
|
+
y: this.y - this.radius,
|
|
825
|
+
width: this.radius * 2,
|
|
826
|
+
height: this.radius * 2,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ==================== EndZone ====================
|
|
832
|
+
class EndZone extends GameObject {
|
|
833
|
+
constructor(game, options = {}) {
|
|
834
|
+
super(game, options);
|
|
835
|
+
this.width = options.width || 80;
|
|
836
|
+
this.height = options.height || 160;
|
|
837
|
+
this.animPhase = 0;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
update(dt) {
|
|
841
|
+
super.update(dt);
|
|
842
|
+
this.animPhase += dt * 2;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
draw() {
|
|
846
|
+
super.draw();
|
|
847
|
+
|
|
848
|
+
const w = this.width;
|
|
849
|
+
const h = this.height;
|
|
850
|
+
|
|
851
|
+
Painter.useCtx((ctx) => {
|
|
852
|
+
// Portal glow
|
|
853
|
+
const gradient = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
|
|
854
|
+
gradient.addColorStop(0, CONFIG.theme.goal + "00");
|
|
855
|
+
gradient.addColorStop(0.3, CONFIG.theme.goal + "44");
|
|
856
|
+
gradient.addColorStop(0.5, CONFIG.theme.goal + "88");
|
|
857
|
+
gradient.addColorStop(0.7, CONFIG.theme.goal + "44");
|
|
858
|
+
gradient.addColorStop(1, CONFIG.theme.goal + "00");
|
|
859
|
+
|
|
860
|
+
ctx.fillStyle = gradient;
|
|
861
|
+
ctx.fillRect(-w / 2, -h / 2, w, h);
|
|
862
|
+
|
|
863
|
+
// Animated scan lines
|
|
864
|
+
ctx.strokeStyle = CONFIG.theme.goal;
|
|
865
|
+
ctx.lineWidth = 2;
|
|
866
|
+
const numLines = 8;
|
|
867
|
+
for (let i = 0; i < numLines; i++) {
|
|
868
|
+
const lineY = ((i / numLines + this.animPhase * 0.1) % 1) * h - h / 2;
|
|
869
|
+
ctx.globalAlpha = 0.3 + Math.sin(i + this.animPhase) * 0.2;
|
|
870
|
+
ctx.beginPath();
|
|
871
|
+
ctx.moveTo(-w / 2, lineY);
|
|
872
|
+
ctx.lineTo(w / 2, lineY);
|
|
873
|
+
ctx.stroke();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Border
|
|
877
|
+
ctx.globalAlpha = 0.8;
|
|
878
|
+
ctx.strokeStyle = CONFIG.theme.goal;
|
|
879
|
+
ctx.lineWidth = 3;
|
|
880
|
+
ctx.strokeRect(-w / 2, -h / 2, w, h);
|
|
881
|
+
|
|
882
|
+
// Arrow pointing up
|
|
883
|
+
ctx.globalAlpha = 0.6 + Math.sin(this.animPhase * 3) * 0.4;
|
|
884
|
+
ctx.fillStyle = CONFIG.theme.goal;
|
|
885
|
+
ctx.beginPath();
|
|
886
|
+
ctx.moveTo(0, -30);
|
|
887
|
+
ctx.lineTo(15, 0);
|
|
888
|
+
ctx.lineTo(5, 0);
|
|
889
|
+
ctx.lineTo(5, 20);
|
|
890
|
+
ctx.lineTo(-5, 20);
|
|
891
|
+
ctx.lineTo(-5, 0);
|
|
892
|
+
ctx.lineTo(-15, 0);
|
|
893
|
+
ctx.closePath();
|
|
894
|
+
ctx.fill();
|
|
895
|
+
|
|
896
|
+
ctx.globalAlpha = 1;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
getBounds() {
|
|
901
|
+
return {
|
|
902
|
+
x: this.x - this.width / 2,
|
|
903
|
+
y: this.y - this.height / 2,
|
|
904
|
+
width: this.width,
|
|
905
|
+
height: this.height,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
isPlayerInside(playerBounds) {
|
|
910
|
+
const zoneBounds = this.getBounds();
|
|
911
|
+
const centerX = playerBounds.x + playerBounds.width / 2;
|
|
912
|
+
return centerX > zoneBounds.x && centerX < zoneBounds.x + zoneBounds.width;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ==================== SkyLayer ====================
|
|
917
|
+
class SkyLayer extends GameObject {
|
|
918
|
+
constructor(game, options = {}) {
|
|
919
|
+
super(game, options);
|
|
920
|
+
this.gridWidth = 4000;
|
|
921
|
+
this.scrollOffset = 0;
|
|
922
|
+
|
|
923
|
+
// Generate grid lines
|
|
924
|
+
this.verticalLines = [];
|
|
925
|
+
this.horizontalLines = [];
|
|
926
|
+
|
|
927
|
+
const spacing = 60;
|
|
928
|
+
for (let x = 0; x < this.gridWidth; x += spacing) {
|
|
929
|
+
this.verticalLines.push({
|
|
930
|
+
x,
|
|
931
|
+
height: 150 + Math.random() * 100,
|
|
932
|
+
opacity: 0.1 + Math.random() * 0.2,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
for (let y = 0; y < 300; y += spacing / 2) {
|
|
937
|
+
this.horizontalLines.push({
|
|
938
|
+
y: -250 + y,
|
|
939
|
+
opacity: 0.05 + Math.random() * 0.1,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
setScrollOffset(offset) {
|
|
945
|
+
this.scrollOffset = offset % this.gridWidth;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
draw() {
|
|
949
|
+
super.draw();
|
|
950
|
+
|
|
951
|
+
Painter.useCtx((ctx) => {
|
|
952
|
+
ctx.strokeStyle = CONFIG.theme.primary;
|
|
953
|
+
ctx.lineWidth = 1;
|
|
954
|
+
|
|
955
|
+
// Draw horizontal lines
|
|
956
|
+
this.horizontalLines.forEach(line => {
|
|
957
|
+
ctx.globalAlpha = line.opacity;
|
|
958
|
+
ctx.beginPath();
|
|
959
|
+
ctx.moveTo(-this.gridWidth, line.y);
|
|
960
|
+
ctx.lineTo(this.gridWidth, line.y);
|
|
961
|
+
ctx.stroke();
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Draw vertical lines
|
|
965
|
+
this.verticalLines.forEach(line => {
|
|
966
|
+
let x = line.x - this.scrollOffset;
|
|
967
|
+
while (x < -this.gridWidth / 2) x += this.gridWidth;
|
|
968
|
+
while (x > this.gridWidth / 2) x -= this.gridWidth;
|
|
969
|
+
|
|
970
|
+
ctx.globalAlpha = line.opacity;
|
|
971
|
+
ctx.beginPath();
|
|
972
|
+
ctx.moveTo(x, -300);
|
|
973
|
+
ctx.lineTo(x, -300 + line.height);
|
|
974
|
+
ctx.stroke();
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
ctx.globalAlpha = 1;
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ==================== Level ====================
|
|
983
|
+
class Level extends PlatformerScene {
|
|
984
|
+
constructor(game, options = {}) {
|
|
985
|
+
super(game, {
|
|
986
|
+
...options,
|
|
987
|
+
player: options.player,
|
|
988
|
+
gravity: CONFIG.gravity,
|
|
989
|
+
jumpVelocity: CONFIG.jumpVelocity,
|
|
990
|
+
moveSpeed: CONFIG.moveSpeed,
|
|
991
|
+
autoInput: false, // We'll handle input manually to respect game state
|
|
992
|
+
autoGravity: false, // We'll handle gravity manually to respect game state
|
|
993
|
+
groundY: null, // No default ground - use platforms
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
this.platforms = [];
|
|
997
|
+
this.enemies = [];
|
|
998
|
+
this.orbs = [];
|
|
999
|
+
this.endZone = null;
|
|
1000
|
+
this.score = 0;
|
|
1001
|
+
this.totalOrbs = 0;
|
|
1002
|
+
this.collectedOrbs = 0;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
buildLevel(levelData, groundY) {
|
|
1006
|
+
this.groundY = groundY;
|
|
1007
|
+
|
|
1008
|
+
// Create platforms
|
|
1009
|
+
// yOffset: 0 means platform TOP is at groundY
|
|
1010
|
+
// Platform center = groundY + yOffset + height/2
|
|
1011
|
+
levelData.platforms.forEach(p => {
|
|
1012
|
+
const platform = new Platform(this.game, {
|
|
1013
|
+
x: p.x,
|
|
1014
|
+
y: groundY + p.yOffset + p.height / 2,
|
|
1015
|
+
width: p.width,
|
|
1016
|
+
height: p.height,
|
|
1017
|
+
type: p.type,
|
|
1018
|
+
});
|
|
1019
|
+
this.platforms.push(platform);
|
|
1020
|
+
this.add(platform);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// Create enemies
|
|
1024
|
+
// Enemy sprite feet are 10 pixels below center (based on pixel art)
|
|
1025
|
+
const enemyFeetOffset = 10;
|
|
1026
|
+
levelData.enemies.forEach(e => {
|
|
1027
|
+
const enemy = new GlitchEnemy(this.game, {
|
|
1028
|
+
x: e.x,
|
|
1029
|
+
y: groundY + e.yOffset - enemyFeetOffset,
|
|
1030
|
+
patrolStart: e.patrolStart,
|
|
1031
|
+
patrolEnd: e.patrolEnd,
|
|
1032
|
+
});
|
|
1033
|
+
this.enemies.push(enemy);
|
|
1034
|
+
this.add(enemy);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Create orbs - these float, so yOffset is from ground level
|
|
1038
|
+
levelData.orbs.forEach(o => {
|
|
1039
|
+
const orb = new DataOrb(this.game, {
|
|
1040
|
+
x: o.x,
|
|
1041
|
+
y: groundY + o.yOffset,
|
|
1042
|
+
value: o.value,
|
|
1043
|
+
});
|
|
1044
|
+
this.orbs.push(orb);
|
|
1045
|
+
this.add(orb);
|
|
1046
|
+
});
|
|
1047
|
+
this.totalOrbs = this.orbs.length;
|
|
1048
|
+
|
|
1049
|
+
// Create end zone - center-based
|
|
1050
|
+
const ez = levelData.endZone;
|
|
1051
|
+
this.endZone = new EndZone(this.game, {
|
|
1052
|
+
x: ez.x,
|
|
1053
|
+
y: groundY + ez.yOffset,
|
|
1054
|
+
width: ez.width,
|
|
1055
|
+
height: ez.height,
|
|
1056
|
+
});
|
|
1057
|
+
this.add(this.endZone);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Override applyInput to handle horizontal movement with acceleration
|
|
1061
|
+
applyInput(player, dt) {
|
|
1062
|
+
const accel = CONFIG.acceleration;
|
|
1063
|
+
const maxSpeed = CONFIG.maxSpeed;
|
|
1064
|
+
const grounded = player._grounded;
|
|
1065
|
+
// Use different friction for ground vs air
|
|
1066
|
+
const friction = grounded ? CONFIG.friction : CONFIG.airFriction;
|
|
1067
|
+
const controlMult = grounded ? 1.0 : CONFIG.airControl;
|
|
1068
|
+
|
|
1069
|
+
let inputDir = 0;
|
|
1070
|
+
if (Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.A)) {
|
|
1071
|
+
inputDir = -1;
|
|
1072
|
+
}
|
|
1073
|
+
if (Keys.isDown(Keys.RIGHT) || Keys.isDown(Keys.D)) {
|
|
1074
|
+
inputDir = 1;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (inputDir !== 0) {
|
|
1078
|
+
// Accelerate in input direction
|
|
1079
|
+
player.vx += inputDir * accel * controlMult * dt;
|
|
1080
|
+
// Clamp to max speed
|
|
1081
|
+
if (player.vx > maxSpeed) player.vx = maxSpeed;
|
|
1082
|
+
if (player.vx < -maxSpeed) player.vx = -maxSpeed;
|
|
1083
|
+
} else {
|
|
1084
|
+
// Apply friction when no input (much less in air)
|
|
1085
|
+
if (player.vx > 0) {
|
|
1086
|
+
player.vx -= friction * dt;
|
|
1087
|
+
if (player.vx < 0) player.vx = 0;
|
|
1088
|
+
} else if (player.vx < 0) {
|
|
1089
|
+
player.vx += friction * dt;
|
|
1090
|
+
if (player.vx > 0) player.vx = 0;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// Jump is handled by player.jump() via handleAction - NOT here
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
isPlayerGrounded() {
|
|
1097
|
+
if (!this.player) return false;
|
|
1098
|
+
|
|
1099
|
+
const pb = this.player.getBounds();
|
|
1100
|
+
const feetY = pb.y + pb.height;
|
|
1101
|
+
const tolerance = 12; // Increased tolerance for better landing detection
|
|
1102
|
+
|
|
1103
|
+
for (const platform of this.platforms) {
|
|
1104
|
+
const platB = platform.getBounds();
|
|
1105
|
+
|
|
1106
|
+
// Check if player feet are at or slightly below platform top level
|
|
1107
|
+
if (feetY >= platB.y - tolerance && feetY <= platB.y + tolerance) {
|
|
1108
|
+
// Check horizontal overlap
|
|
1109
|
+
if (pb.x + pb.width > platB.x && pb.x < platB.x + platB.width) {
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
handleGroundCollision(player) {
|
|
1118
|
+
let grounded = false;
|
|
1119
|
+
const pb = player.getBounds();
|
|
1120
|
+
|
|
1121
|
+
for (const platform of this.platforms) {
|
|
1122
|
+
const platB = platform.getBounds();
|
|
1123
|
+
|
|
1124
|
+
if (Collision.rectRect(pb, platB)) {
|
|
1125
|
+
const mtv = Collision.getMTV(pb, platB);
|
|
1126
|
+
|
|
1127
|
+
if (mtv) {
|
|
1128
|
+
// Determine collision direction based on overlap
|
|
1129
|
+
const overlapX = Math.abs(mtv.x);
|
|
1130
|
+
const overlapY = Math.abs(mtv.y);
|
|
1131
|
+
|
|
1132
|
+
if (overlapY <= overlapX || player.vy > 0) {
|
|
1133
|
+
// Vertical collision takes priority when falling
|
|
1134
|
+
if (mtv.y < 0 && player.vy >= 0) {
|
|
1135
|
+
// Landing from above
|
|
1136
|
+
player.y += mtv.y;
|
|
1137
|
+
player.vy = 0;
|
|
1138
|
+
grounded = true;
|
|
1139
|
+
} else if (mtv.y > 0 && player.vy < 0) {
|
|
1140
|
+
// Hit ceiling from below
|
|
1141
|
+
player.y += mtv.y;
|
|
1142
|
+
player.vy = 0;
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
// Horizontal collision
|
|
1146
|
+
player.x += mtv.x;
|
|
1147
|
+
player.vx = 0;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Also check if standing on platform surface (for walking off edges)
|
|
1154
|
+
if (!grounded && player.vy >= 0) {
|
|
1155
|
+
const feetY = player.y + player.height / 2;
|
|
1156
|
+
for (const platform of this.platforms) {
|
|
1157
|
+
const platB = platform.getBounds();
|
|
1158
|
+
// Check if feet are just above or at platform and horizontally aligned
|
|
1159
|
+
if (feetY >= platB.y - 4 && feetY <= platB.y + 8) {
|
|
1160
|
+
if (pb.x + pb.width > platB.x && pb.x < platB.x + platB.width) {
|
|
1161
|
+
// Snap feet to platform surface
|
|
1162
|
+
player.y = platB.y - player.height / 2;
|
|
1163
|
+
player.vy = 0;
|
|
1164
|
+
grounded = true;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
player._grounded = grounded;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
checkEnemyCollisions() {
|
|
1175
|
+
if (!this.player || this.game.gameState !== 'playing') return;
|
|
1176
|
+
|
|
1177
|
+
const pb = this.player.getBounds();
|
|
1178
|
+
|
|
1179
|
+
for (const enemy of this.enemies) {
|
|
1180
|
+
if (enemy.isDead) continue;
|
|
1181
|
+
|
|
1182
|
+
const eb = enemy.getBounds();
|
|
1183
|
+
|
|
1184
|
+
if (Collision.rectRect(pb, eb)) {
|
|
1185
|
+
const playerBottom = pb.y + pb.height;
|
|
1186
|
+
const enemyTop = eb.y;
|
|
1187
|
+
|
|
1188
|
+
// Check if stomping (player falling and above enemy)
|
|
1189
|
+
if (this.player.vy > 0 && playerBottom < enemyTop + 20) {
|
|
1190
|
+
// Stomp! Pogo bounce off enemy
|
|
1191
|
+
enemy.die();
|
|
1192
|
+
this.player.pogo(); // Big bounce!
|
|
1193
|
+
this.score += 100;
|
|
1194
|
+
SFX.stomp();
|
|
1195
|
+
this.game.shakeCamera(5, 0.15);
|
|
1196
|
+
} else {
|
|
1197
|
+
// Death
|
|
1198
|
+
this.game.handleDeath();
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
checkOrbCollisions() {
|
|
1206
|
+
if (!this.player || this.game.gameState !== 'playing') return;
|
|
1207
|
+
|
|
1208
|
+
const pb = this.player.getBounds();
|
|
1209
|
+
|
|
1210
|
+
for (const orb of this.orbs) {
|
|
1211
|
+
if (orb.collected) continue;
|
|
1212
|
+
|
|
1213
|
+
const ob = orb.getBounds();
|
|
1214
|
+
|
|
1215
|
+
if (Collision.rectRect(pb, ob)) {
|
|
1216
|
+
if (orb.collect()) {
|
|
1217
|
+
this.score += orb.value;
|
|
1218
|
+
this.collectedOrbs++;
|
|
1219
|
+
SFX.collect();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
checkEndZone() {
|
|
1226
|
+
if (!this.player || !this.endZone || this.game.gameState !== 'playing') return;
|
|
1227
|
+
|
|
1228
|
+
if (this.endZone.isPlayerInside(this.player.getBounds())) {
|
|
1229
|
+
this.game.handleComplete();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
checkFallDeath() {
|
|
1234
|
+
if (!this.player || this.game.gameState !== 'playing') return;
|
|
1235
|
+
|
|
1236
|
+
if (this.player.y > this.game.height + 100) {
|
|
1237
|
+
this.game.handleDeath();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
update(dt) {
|
|
1242
|
+
// Only apply physics when playing
|
|
1243
|
+
if (this.game.gameState === 'playing' && this.player) {
|
|
1244
|
+
// Apply gravity
|
|
1245
|
+
this.applyGravity(this.player, dt);
|
|
1246
|
+
|
|
1247
|
+
// Apply input
|
|
1248
|
+
this.applyInput(this.player, dt);
|
|
1249
|
+
|
|
1250
|
+
// Apply velocity
|
|
1251
|
+
this.applyVelocity(this.player, dt);
|
|
1252
|
+
|
|
1253
|
+
// Handle platform collisions
|
|
1254
|
+
this.handleGroundCollision(this.player);
|
|
1255
|
+
|
|
1256
|
+
// Update player movement state for animation
|
|
1257
|
+
this.player._isMoving = Math.abs(this.player.vx) > 10;
|
|
1258
|
+
if (this.player.vx > 0) this.player.facingRight = true;
|
|
1259
|
+
else if (this.player.vx < 0) this.player.facingRight = false;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Update camera
|
|
1263
|
+
this.updateCamera(dt);
|
|
1264
|
+
|
|
1265
|
+
// Update all children (orbs, enemies, etc.)
|
|
1266
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
1267
|
+
const child = this.children[i];
|
|
1268
|
+
if (child.active !== false && child.update) {
|
|
1269
|
+
child.update(dt);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Re-resolve ground collision after all updates
|
|
1274
|
+
// This ensures player can't fall through platforms even if something moved them
|
|
1275
|
+
if (this.game.gameState === 'playing' && this.player) {
|
|
1276
|
+
this.handleGroundCollision(this.player);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Collision checks (only when playing)
|
|
1280
|
+
if (this.game.gameState === 'playing') {
|
|
1281
|
+
this.checkEnemyCollisions();
|
|
1282
|
+
this.checkOrbCollisions();
|
|
1283
|
+
this.checkEndZone();
|
|
1284
|
+
this.checkFallDeath();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ==================== PlatformerGame ====================
|
|
1290
|
+
class PlatformerGame extends Game {
|
|
1291
|
+
constructor(canvas) {
|
|
1292
|
+
super(canvas);
|
|
1293
|
+
this.backgroundColor = CONFIG.theme.background;
|
|
1294
|
+
this.enableFluidSize();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
init() {
|
|
1298
|
+
super.init();
|
|
1299
|
+
SFX.init();
|
|
1300
|
+
this.setupGame();
|
|
1301
|
+
this.setupInput();
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
setupGame() {
|
|
1305
|
+
this.score = 0;
|
|
1306
|
+
this.gameState = 'start'; // 'start', 'playing', 'dead', 'complete'
|
|
1307
|
+
|
|
1308
|
+
// Calculate ground Y - vertically centered, slightly below center
|
|
1309
|
+
this.groundY = this.height * 0.7;
|
|
1310
|
+
|
|
1311
|
+
// Create player - spawn position relative to ground
|
|
1312
|
+
// Player sprite feet are 13 pixels below center (based on pixel art)
|
|
1313
|
+
const playerFeetOffset = 13;
|
|
1314
|
+
this.player = new Player(this, {
|
|
1315
|
+
x: LEVEL_DATA.spawn.x,
|
|
1316
|
+
y: this.groundY + LEVEL_DATA.spawn.yOffset - playerFeetOffset,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// Create camera with bounds relative to ground
|
|
1320
|
+
this.camera = new Camera2D({
|
|
1321
|
+
target: this.player,
|
|
1322
|
+
viewportWidth: this.width,
|
|
1323
|
+
viewportHeight: this.height,
|
|
1324
|
+
lerp: CONFIG.cameraLerp,
|
|
1325
|
+
deadzone: {
|
|
1326
|
+
width: CONFIG.cameraDeadzoneWidth,
|
|
1327
|
+
height: CONFIG.cameraDeadzoneHeight,
|
|
1328
|
+
},
|
|
1329
|
+
bounds: {
|
|
1330
|
+
minX: LEVEL_DATA.bounds.minX,
|
|
1331
|
+
maxX: LEVEL_DATA.bounds.maxX,
|
|
1332
|
+
minY: 0,
|
|
1333
|
+
maxY: this.height,
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
// Create level
|
|
1338
|
+
this.level = new Level(this, {
|
|
1339
|
+
player: this.player,
|
|
1340
|
+
camera: this.camera,
|
|
1341
|
+
viewportWidth: this.width,
|
|
1342
|
+
viewportHeight: this.height,
|
|
1343
|
+
x: 0,
|
|
1344
|
+
y: 0,
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// Add sky layer (slow parallax) - centered vertically
|
|
1348
|
+
this.sky = new SkyLayer(this, {
|
|
1349
|
+
x: this.width / 2,
|
|
1350
|
+
y: this.height / 2,
|
|
1351
|
+
});
|
|
1352
|
+
this.level.addLayer(this.sky, { speed: 0.2 });
|
|
1353
|
+
|
|
1354
|
+
// Build level content with groundY
|
|
1355
|
+
this.level.buildLevel(LEVEL_DATA, this.groundY);
|
|
1356
|
+
|
|
1357
|
+
// Add player to level (moves with camera)
|
|
1358
|
+
this.level.add(this.player);
|
|
1359
|
+
|
|
1360
|
+
this.pipeline.add(this.level);
|
|
1361
|
+
|
|
1362
|
+
// Create UI
|
|
1363
|
+
this.createUI();
|
|
1364
|
+
|
|
1365
|
+
// FPS counter
|
|
1366
|
+
this.pipeline.add(
|
|
1367
|
+
new FPSCounter(this, {
|
|
1368
|
+
color: CONFIG.theme.textDim,
|
|
1369
|
+
anchor: Position.BOTTOM_RIGHT,
|
|
1370
|
+
})
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
createUI() {
|
|
1375
|
+
// Score display
|
|
1376
|
+
this.scoreText = new Text(this, "SCORE: 0", {
|
|
1377
|
+
font: "bold 20px 'Courier New', monospace",
|
|
1378
|
+
color: CONFIG.theme.primary,
|
|
1379
|
+
anchor: Position.TOP_LEFT,
|
|
1380
|
+
anchorOffsetX: 20,
|
|
1381
|
+
anchorOffsetY: 20,
|
|
1382
|
+
});
|
|
1383
|
+
this.pipeline.add(this.scoreText);
|
|
1384
|
+
|
|
1385
|
+
// Orb counter
|
|
1386
|
+
this.orbText = new Text(this, "DATA: 0/0", {
|
|
1387
|
+
font: "16px 'Courier New', monospace",
|
|
1388
|
+
color: CONFIG.theme.orb,
|
|
1389
|
+
anchor: Position.TOP_LEFT,
|
|
1390
|
+
anchorOffsetX: 20,
|
|
1391
|
+
anchorOffsetY: 50,
|
|
1392
|
+
});
|
|
1393
|
+
this.pipeline.add(this.orbText);
|
|
1394
|
+
|
|
1395
|
+
// Start message
|
|
1396
|
+
this.startText = new Text(this, "[ PRESS SPACE TO START ]", {
|
|
1397
|
+
font: "18px 'Courier New', monospace",
|
|
1398
|
+
color: CONFIG.theme.primary,
|
|
1399
|
+
anchor: Position.CENTER,
|
|
1400
|
+
});
|
|
1401
|
+
this.pipeline.add(this.startText);
|
|
1402
|
+
|
|
1403
|
+
this.subtitleText = new Text(this, "collect data orbs and reach the portal", {
|
|
1404
|
+
font: "14px 'Courier New', monospace",
|
|
1405
|
+
color: CONFIG.theme.textDim,
|
|
1406
|
+
anchor: Position.CENTER,
|
|
1407
|
+
anchorOffsetY: 30,
|
|
1408
|
+
});
|
|
1409
|
+
this.pipeline.add(this.subtitleText);
|
|
1410
|
+
|
|
1411
|
+
// Game over text
|
|
1412
|
+
this.gameOverText = new Text(this, "SYSTEM CRASH", {
|
|
1413
|
+
font: "bold 36px 'Courier New', monospace",
|
|
1414
|
+
color: CONFIG.theme.enemy,
|
|
1415
|
+
anchor: Position.CENTER,
|
|
1416
|
+
anchorOffsetY: -30,
|
|
1417
|
+
visible: false,
|
|
1418
|
+
});
|
|
1419
|
+
this.pipeline.add(this.gameOverText);
|
|
1420
|
+
|
|
1421
|
+
this.restartText = new Text(this, "[ PRESS SPACE TO RESTART ]", {
|
|
1422
|
+
font: "16px 'Courier New', monospace",
|
|
1423
|
+
color: CONFIG.theme.textDim,
|
|
1424
|
+
anchor: Position.CENTER,
|
|
1425
|
+
anchorOffsetY: 60,
|
|
1426
|
+
visible: false,
|
|
1427
|
+
});
|
|
1428
|
+
this.pipeline.add(this.restartText);
|
|
1429
|
+
|
|
1430
|
+
// Complete text
|
|
1431
|
+
this.completeText = new Text(this, "UPLOAD COMPLETE", {
|
|
1432
|
+
font: "bold 36px 'Courier New', monospace",
|
|
1433
|
+
color: CONFIG.theme.goal,
|
|
1434
|
+
anchor: Position.CENTER,
|
|
1435
|
+
anchorOffsetY: -20,
|
|
1436
|
+
visible: false,
|
|
1437
|
+
});
|
|
1438
|
+
this.pipeline.add(this.completeText);
|
|
1439
|
+
|
|
1440
|
+
this.finalScoreText = new Text(this, "", {
|
|
1441
|
+
font: "20px 'Courier New', monospace",
|
|
1442
|
+
color: CONFIG.theme.text,
|
|
1443
|
+
anchor: Position.CENTER,
|
|
1444
|
+
anchorOffsetY: 20,
|
|
1445
|
+
visible: false,
|
|
1446
|
+
});
|
|
1447
|
+
this.pipeline.add(this.finalScoreText);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
setupInput() {
|
|
1451
|
+
this.events.on(Keys.SPACE, () => this.handleAction());
|
|
1452
|
+
this.events.on(Keys.UP, () => this.handleAction());
|
|
1453
|
+
this.events.on(Keys.W, () => this.handleAction());
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async handleAction() {
|
|
1457
|
+
await SFX.resume();
|
|
1458
|
+
|
|
1459
|
+
if (this.gameState === 'start') {
|
|
1460
|
+
this.startGame();
|
|
1461
|
+
} else if (this.gameState === 'dead' || this.gameState === 'complete') {
|
|
1462
|
+
this.restartGame();
|
|
1463
|
+
} else if (this.gameState === 'playing') {
|
|
1464
|
+
// Use player's jump method with cooldown
|
|
1465
|
+
if (this.player && this.player.jump()) {
|
|
1466
|
+
SFX.jump();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
startGame() {
|
|
1472
|
+
this.gameState = 'playing';
|
|
1473
|
+
this.startText.visible = false;
|
|
1474
|
+
this.subtitleText.visible = false;
|
|
1475
|
+
SFX.start();
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
handleDeath() {
|
|
1479
|
+
if (this.gameState !== 'playing') return;
|
|
1480
|
+
|
|
1481
|
+
this.gameState = 'dead';
|
|
1482
|
+
|
|
1483
|
+
// Trigger player death animation
|
|
1484
|
+
if (this.player) {
|
|
1485
|
+
this.player.die();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
SFX.death();
|
|
1489
|
+
this.shakeCamera(10, 0.3);
|
|
1490
|
+
|
|
1491
|
+
// Show game over UI after a short delay for death animation
|
|
1492
|
+
setTimeout(() => {
|
|
1493
|
+
// Only show if still in dead state (user might have restarted)
|
|
1494
|
+
if (this.gameState === 'dead') {
|
|
1495
|
+
this.gameOverText.visible = true;
|
|
1496
|
+
this.restartText.visible = true;
|
|
1497
|
+
}
|
|
1498
|
+
}, 500);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
handleComplete() {
|
|
1502
|
+
if (this.gameState !== 'playing') return;
|
|
1503
|
+
|
|
1504
|
+
this.gameState = 'complete';
|
|
1505
|
+
this.completeText.visible = true;
|
|
1506
|
+
this.finalScoreText.visible = true;
|
|
1507
|
+
this.finalScoreText.text = `FINAL SCORE: ${this.level.score}`;
|
|
1508
|
+
this.restartText.visible = true;
|
|
1509
|
+
SFX.complete();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
restartGame() {
|
|
1513
|
+
this.pipeline.clear();
|
|
1514
|
+
this.setupGame();
|
|
1515
|
+
|
|
1516
|
+
// Auto-start
|
|
1517
|
+
this.gameState = 'playing';
|
|
1518
|
+
this.startText.visible = false;
|
|
1519
|
+
this.subtitleText.visible = false;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
shakeCamera(intensity, duration) {
|
|
1523
|
+
if (this.level && this.level.camera) {
|
|
1524
|
+
this.level.shakeCamera(intensity, duration);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
update(dt) {
|
|
1529
|
+
// Blinking start text
|
|
1530
|
+
if (this.gameState === 'start' && this.startText) {
|
|
1531
|
+
const blink = Math.sin(Date.now() / 500) > 0;
|
|
1532
|
+
this.startText.opacity = blink ? 1 : 0.5;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Update UI
|
|
1536
|
+
if (this.level) {
|
|
1537
|
+
this.scoreText.text = `SCORE: ${this.level.score}`;
|
|
1538
|
+
this.orbText.text = `DATA: ${this.level.collectedOrbs}/${this.level.totalOrbs}`;
|
|
1539
|
+
|
|
1540
|
+
// Update sky parallax
|
|
1541
|
+
if (this.sky && this.camera) {
|
|
1542
|
+
const offset = this.camera.getOffset();
|
|
1543
|
+
this.sky.setScrollOffset(offset.x);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Pause updates when not playing
|
|
1548
|
+
if (this.gameState !== 'playing') {
|
|
1549
|
+
// Still update camera and rendering but not game logic
|
|
1550
|
+
if (this.camera) {
|
|
1551
|
+
this.camera.update(dt);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
super.update(dt);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
onResize() {
|
|
1559
|
+
if (this.camera) {
|
|
1560
|
+
this.camera.viewportWidth = this.width;
|
|
1561
|
+
this.camera.viewportHeight = this.height;
|
|
1562
|
+
this.camera.bounds.maxY = this.height;
|
|
1563
|
+
}
|
|
1564
|
+
if (this.level) {
|
|
1565
|
+
this.level.setViewport(this.width, this.height);
|
|
1566
|
+
}
|
|
1567
|
+
if (this.sky) {
|
|
1568
|
+
this.sky.x = this.width / 2;
|
|
1569
|
+
this.sky.y = this.height / 2;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// ==================== Initialize ====================
|
|
1575
|
+
window.addEventListener("load", () => {
|
|
1576
|
+
const canvas = document.getElementById("game");
|
|
1577
|
+
const game = new PlatformerGame(canvas);
|
|
1578
|
+
game.start();
|
|
1579
|
+
});
|