feat: add 10-level progression for all games and new Vortex game
- Expand all existing games (ColoradoDefense, CodeBugInvaders, DotDude, SmashOut) from 2 levels to 10 levels with progressive difficulty scaling - Add new game "Vortex" with 10 levels of increasing difficulty - Update game config to include Vortex in the game pool - Add Vortex music asset and configuration - Improve GameManager robustness with duplicate event guards - Clean up JSON formatting in all game config files - Remove version query parameters from config loading where appropriate
This commit is contained in:
parent
3527a1e3ef
commit
d044b1f319
Binary file not shown.
|
|
@ -2,7 +2,15 @@
|
||||||
"lives": 3,
|
"lives": 3,
|
||||||
"levels": {
|
"levels": {
|
||||||
"1": { "gameCount": 3 },
|
"1": { "gameCount": 3 },
|
||||||
"2": { "gameCount": 4 }
|
"2": { "gameCount": 4 },
|
||||||
|
"3": { "gameCount": 4 },
|
||||||
|
"4": { "gameCount": 4 },
|
||||||
|
"5": { "gameCount": 5 },
|
||||||
|
"6": { "gameCount": 5 },
|
||||||
|
"7": { "gameCount": 5 },
|
||||||
|
"8": { "gameCount": 5 },
|
||||||
|
"9": { "gameCount": 5 },
|
||||||
|
"10": { "gameCount": 5 }
|
||||||
},
|
},
|
||||||
"gamePool": ["ColoradoDefense", "CodeBugInvaders", "DotDude", "SmashOut"]
|
"gamePool": ["ColoradoDefense", "CodeBugInvaders", "DotDude", "SmashOut", "Vortex"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ export default class CodeBugInvaders extends Phaser.Scene {
|
||||||
constructor() { super({ key: 'CodeBugInvaders' }); }
|
constructor() { super({ key: 'CodeBugInvaders' }); }
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('cbiConfig', 'src/games/code-bug-invaders/config.json?v=2');
|
this.load.json('cbiConfig', 'src/games/code-bug-invaders/config.json');
|
||||||
this.load.audio('cbiMusic', 'assets/music/02-code-bug-invaders.mp3');
|
this.load.audio('cbiMusic', 'assets/music/02-code-bug-invaders.mp3');
|
||||||
this.load.audio('cbiComplete','assets/fx/complete.mp3');
|
this.load.audio('cbiComplete', 'assets/fx/complete.mp3');
|
||||||
this.load.audio('cbifire', 'assets/fx/fire.mp3');
|
this.load.audio('cbifire', 'assets/fx/fire.mp3');
|
||||||
this.load.audio('cbilaser', 'assets/fx/laser.mp3');
|
this.load.audio('cbilaser', 'assets/fx/laser.mp3');
|
||||||
this.load.audio('cbideath', 'assets/fx/death.mp3');
|
this.load.audio('cbideath', 'assets/fx/death.mp3');
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,184 @@
|
||||||
{
|
{
|
||||||
"levels": {
|
"levels": {
|
||||||
"1": {
|
"1": {
|
||||||
"cols": 10, "rows": 4,
|
"cols": 10,
|
||||||
"bugSpacingX": 120, "bugSpacingY": 70,
|
"rows": 4,
|
||||||
"startX": 180, "startY": 100,
|
"bugSpacingX": 120,
|
||||||
"formationSpeed": 60, "formationStepDown": 30,
|
"bugSpacingY": 70,
|
||||||
"rightBound": 1540, "leftBound": 60,
|
"startX": 180,
|
||||||
"bugFireInterval": 1800, "bugBulletSpeed": 280,
|
"startY": 100,
|
||||||
"playerSpeed": 420, "playerBulletSpeed": 700,
|
"formationSpeed": 60,
|
||||||
"dangerY": 720, "bugsToPass": 15
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 1800,
|
||||||
|
"bugBulletSpeed": 280,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 15
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"cols": 10, "rows": 4,
|
"cols": 10,
|
||||||
"bugSpacingX": 120, "bugSpacingY": 70,
|
"rows": 4,
|
||||||
"startX": 180, "startY": 100,
|
"bugSpacingX": 120,
|
||||||
"formationSpeed": 85, "formationStepDown": 30,
|
"bugSpacingY": 70,
|
||||||
"rightBound": 1540, "leftBound": 60,
|
"startX": 180,
|
||||||
"bugFireInterval": 1300, "bugBulletSpeed": 350,
|
"startY": 100,
|
||||||
"playerSpeed": 420, "playerBulletSpeed": 700,
|
"formationSpeed": 85,
|
||||||
"dangerY": 720, "bugsToPass": 28
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 1300,
|
||||||
|
"bugBulletSpeed": 350,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 20
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 105,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 1010,
|
||||||
|
"bugBulletSpeed": 400,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 23
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 120,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 795,
|
||||||
|
"bugBulletSpeed": 440,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 26
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 133,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 642,
|
||||||
|
"bugBulletSpeed": 473,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 29
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 144,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 530,
|
||||||
|
"bugBulletSpeed": 499,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 31
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 152,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 447,
|
||||||
|
"bugBulletSpeed": 519,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 33
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 159,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 384,
|
||||||
|
"bugBulletSpeed": 535,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 35
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 164,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 336,
|
||||||
|
"bugBulletSpeed": 547,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 37
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"cols": 10,
|
||||||
|
"rows": 4,
|
||||||
|
"bugSpacingX": 120,
|
||||||
|
"bugSpacingY": 70,
|
||||||
|
"startX": 180,
|
||||||
|
"startY": 100,
|
||||||
|
"formationSpeed": 168,
|
||||||
|
"formationStepDown": 30,
|
||||||
|
"rightBound": 1540,
|
||||||
|
"leftBound": 60,
|
||||||
|
"bugFireInterval": 298,
|
||||||
|
"bugBulletSpeed": 557,
|
||||||
|
"playerSpeed": 420,
|
||||||
|
"playerBulletSpeed": 700,
|
||||||
|
"dangerY": 720,
|
||||||
|
"bugsToPass": 38
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ export default class ColoradoDefense extends Phaser.Scene {
|
||||||
constructor() { super({ key: 'ColoradoDefense' }); }
|
constructor() { super({ key: 'ColoradoDefense' }); }
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('cdConfig', 'src/games/colorado-defense/config.json?v=2');
|
this.load.json('cdConfig', 'src/games/colorado-defense/config.json');
|
||||||
this.load.json('cdCities', 'src/games/colorado-defense/cities.json');
|
this.load.json('cdCities', 'src/games/colorado-defense/cities.json');
|
||||||
this.load.audio('cdMusic', 'assets/music/01-colorado-defense.mp3');
|
this.load.audio('cdMusic', 'assets/music/01-colorado-defense.mp3');
|
||||||
this.load.audio('cdComplete','assets/fx/complete.mp3');
|
this.load.audio('cdComplete', 'assets/fx/complete.mp3');
|
||||||
this.load.audio('cdShoot', 'assets/fx/shoot.mp3');
|
this.load.audio('cdShoot', 'assets/fx/shoot.mp3');
|
||||||
this.load.audio('cdFireball','assets/fx/fireball.mp3');
|
this.load.audio('cdFireball', 'assets/fx/fireball.mp3');
|
||||||
this.load.audio('cdDestroy', 'assets/fx/destroy.mp3');
|
this.load.audio('cdDestroy', 'assets/fx/destroy.mp3');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
{ "count": 9, "speed": 80, "spawnInterval": 2000 },
|
{ "count": 9, "speed": 80, "spawnInterval": 2000 },
|
||||||
{ "count": 14, "speed": 110, "spawnInterval": 1400 }
|
{ "count": 14, "speed": 110, "spawnInterval": 1400 }
|
||||||
],
|
],
|
||||||
"interceptorSpeed": 420, "explosionRadius": 75,
|
"interceptorSpeed": 420,
|
||||||
"maxCityDeaths": 3, "wavesToPass": 2
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 3,
|
||||||
|
"wavesToPass": 1
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"waves": [
|
"waves": [
|
||||||
|
|
@ -15,8 +17,98 @@
|
||||||
{ "count": 12, "speed": 100, "spawnInterval": 1600 },
|
{ "count": 12, "speed": 100, "spawnInterval": 1600 },
|
||||||
{ "count": 16, "speed": 130, "spawnInterval": 1100 }
|
{ "count": 16, "speed": 130, "spawnInterval": 1100 }
|
||||||
],
|
],
|
||||||
"interceptorSpeed": 420, "explosionRadius": 75,
|
"interceptorSpeed": 420,
|
||||||
"maxCityDeaths": 2, "wavesToPass": 3
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 2,
|
||||||
|
"wavesToPass": 2
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 10, "speed": 82, "spawnInterval": 1900 },
|
||||||
|
{ "count": 14, "speed": 118, "spawnInterval": 1350 },
|
||||||
|
{ "count": 18, "speed": 148, "spawnInterval": 980 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 2,
|
||||||
|
"wavesToPass": 2
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 11, "speed": 93, "spawnInterval": 1650 },
|
||||||
|
{ "count": 16, "speed": 132, "spawnInterval": 1175 },
|
||||||
|
{ "count": 21, "speed": 163, "spawnInterval": 875 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 2,
|
||||||
|
"wavesToPass": 2
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 12, "speed": 102, "spawnInterval": 1455 },
|
||||||
|
{ "count": 17, "speed": 144, "spawnInterval": 1040 },
|
||||||
|
{ "count": 23, "speed": 176, "spawnInterval": 790 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 2,
|
||||||
|
"wavesToPass": 3
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 13, "speed": 110, "spawnInterval": 1290 },
|
||||||
|
{ "count": 19, "speed": 154, "spawnInterval": 932 },
|
||||||
|
{ "count": 25, "speed": 187, "spawnInterval": 717 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 2,
|
||||||
|
"wavesToPass": 3
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 14, "speed": 117, "spawnInterval": 1155 },
|
||||||
|
{ "count": 20, "speed": 163, "spawnInterval": 840 },
|
||||||
|
{ "count": 27, "speed": 197, "spawnInterval": 653 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 1,
|
||||||
|
"wavesToPass": 3
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 15, "speed": 122, "spawnInterval": 1042 },
|
||||||
|
{ "count": 22, "speed": 170, "spawnInterval": 763 },
|
||||||
|
{ "count": 29, "speed": 205, "spawnInterval": 597 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 1,
|
||||||
|
"wavesToPass": 3
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 16, "speed": 127, "spawnInterval": 946 },
|
||||||
|
{ "count": 23, "speed": 177, "spawnInterval": 696 },
|
||||||
|
{ "count": 31, "speed": 213, "spawnInterval": 548 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 1,
|
||||||
|
"wavesToPass": 3
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"waves": [
|
||||||
|
{ "count": 18, "speed": 130, "spawnInterval": 862 },
|
||||||
|
{ "count": 25, "speed": 183, "spawnInterval": 638 },
|
||||||
|
{ "count": 34, "speed": 220, "spawnInterval": 505 }
|
||||||
|
],
|
||||||
|
"interceptorSpeed": 420,
|
||||||
|
"explosionRadius": 75,
|
||||||
|
"maxCityDeaths": 1,
|
||||||
|
"wavesToPass": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,21 @@ const TH = 60;
|
||||||
const OX = (1600 - COLS * TW) / 2; // left edge of maze = 500
|
const OX = (1600 - COLS * TW) / 2; // left edge of maze = 500
|
||||||
|
|
||||||
const LAYOUT = [
|
const LAYOUT = [
|
||||||
[1,1,1,1,1,1,1,1,1,1], // 0 top wall
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 0 top wall
|
||||||
[1,0,0,0,0,0,0,0,0,1], // 1 open corridor
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 1 open corridor
|
||||||
[1,0,1,1,0,0,1,1,0,1], // 2 block rooms
|
[1, 0, 1, 1, 0, 0, 1, 1, 0, 1], // 2 block rooms
|
||||||
[1,0,1,1,0,0,1,1,0,1], // 3 (thick walls, same as row 2)
|
[1, 0, 1, 1, 0, 0, 1, 1, 0, 1], // 3 (thick walls, same as row 2)
|
||||||
[1,0,0,0,0,0,0,0,0,1], // 4 open corridor
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 4 open corridor
|
||||||
[1,0,1,0,1,1,0,1,0,1], // 5 pre-ghost-house
|
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1], // 5 pre-ghost-house
|
||||||
[1,0,1,0,0,0,0,1,0,1], // 6 ghost house (open centre)
|
[1, 0, 1, 0, 0, 0, 0, 1, 0, 1], // 6 ghost house (open centre)
|
||||||
[1,0,1,0,1,1,0,1,0,1], // 7 post-ghost-house (mirror of 5)
|
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1], // 7 post-ghost-house (mirror of 5)
|
||||||
[1,0,0,0,0,0,0,0,0,1], // 8 open corridor
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 8 open corridor
|
||||||
[1,0,1,1,0,0,1,1,0,1], // 9 block rooms (mirror of 2)
|
[1, 0, 1, 1, 0, 0, 1, 1, 0, 1], // 9 block rooms (mirror of 2)
|
||||||
[1,0,1,1,0,0,1,1,0,1], // 10 (mirror of 3)
|
[1, 0, 1, 1, 0, 0, 1, 1, 0, 1], // 10 (mirror of 3)
|
||||||
[1,0,0,0,0,0,0,0,0,1], // 11 open corridor
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 11 open corridor
|
||||||
[1,1,0,1,0,0,1,0,1,1], // 12 narrow passage
|
[1, 1, 0, 1, 0, 0, 1, 0, 1, 1], // 12 narrow passage
|
||||||
[1,0,0,0,0,0,0,0,0,1], // 13 open corridor (player spawn)
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 13 open corridor (player spawn)
|
||||||
[1,1,1,1,1,1,1,1,1,1], // 14 bottom wall
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 14 bottom wall
|
||||||
];
|
];
|
||||||
|
|
||||||
import { playMusic, stopMusic } from '../../audio.js';
|
import { playMusic, stopMusic } from '../../audio.js';
|
||||||
|
|
@ -30,7 +30,7 @@ export default class DotDude extends Phaser.Scene {
|
||||||
constructor() { super({ key: 'DotDude' }); }
|
constructor() { super({ key: 'DotDude' }); }
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('ddConfig', 'src/games/dot-dude/config.json?v=2');
|
this.load.json('ddConfig', 'src/games/dot-dude/config.json');
|
||||||
this.load.audio('ddMusic', 'assets/music/03-dot-dude.mp3');
|
this.load.audio('ddMusic', 'assets/music/03-dot-dude.mp3');
|
||||||
this.load.audio('ddPellet', 'assets/fx/pellet.mp3');
|
this.load.audio('ddPellet', 'assets/fx/pellet.mp3');
|
||||||
this.load.audio('ddComplete', 'assets/fx/complete.mp3');
|
this.load.audio('ddComplete', 'assets/fx/complete.mp3');
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,74 @@
|
||||||
{
|
{
|
||||||
"levels": {
|
"levels": {
|
||||||
"1": {
|
"1": {
|
||||||
"playerSpeed": 155, "ghostSpeed": 90,
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 90,
|
||||||
"ghostStartDelay": [2000, 4000, 6500],
|
"ghostStartDelay": [2000, 4000, 6500],
|
||||||
"dotRadius": 5, "pelletsToPass": 20
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 20
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"playerSpeed": 155, "ghostSpeed": 120,
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 120,
|
||||||
"ghostStartDelay": [1500, 3000, 4500],
|
"ghostStartDelay": [1500, 3000, 4500],
|
||||||
"dotRadius": 5, "pelletsToPass": 40
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 25
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 143,
|
||||||
|
"ghostStartDelay": [1100, 2200, 3500],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 28
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 160,
|
||||||
|
"ghostStartDelay": [850, 1700, 2700],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 31
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 175,
|
||||||
|
"ghostStartDelay": [650, 1300, 2100],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 34
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 188,
|
||||||
|
"ghostStartDelay": [500, 1000, 1650],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 37
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 199,
|
||||||
|
"ghostStartDelay": [380, 760, 1280],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 40
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 208,
|
||||||
|
"ghostStartDelay": [290, 580, 980],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 43
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 215,
|
||||||
|
"ghostStartDelay": [220, 440, 750],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 46
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"playerSpeed": 155,
|
||||||
|
"ghostSpeed": 220,
|
||||||
|
"ghostStartDelay": [160, 320, 540],
|
||||||
|
"dotRadius": 5,
|
||||||
|
"pelletsToPass": 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ export default class SmashOut extends Phaser.Scene {
|
||||||
constructor() { super({ key: 'SmashOut' }); }
|
constructor() { super({ key: 'SmashOut' }); }
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('soConfig', 'src/games/smash-out/config.json?v=2');
|
this.load.json('soConfig', 'src/games/smash-out/config.json');
|
||||||
this.load.audio('soMusic', 'assets/music/04-smash-out.mp3');
|
this.load.audio('soMusic', 'assets/music/04-smash-out.mp3');
|
||||||
this.load.audio('soComplete','assets/fx/complete.mp3');
|
this.load.audio('soComplete', 'assets/fx/complete.mp3');
|
||||||
this.load.audio('soLaunch', 'assets/fx/launch.mp3');
|
this.load.audio('soLaunch', 'assets/fx/launch.mp3');
|
||||||
this.load.audio('soBounce', 'assets/fx/bounce.mp3');
|
this.load.audio('soBounce', 'assets/fx/bounce.mp3');
|
||||||
this.load.audio('soDestroy', 'assets/fx/short_death.mp3');
|
this.load.audio('soDestroy', 'assets/fx/short_death.mp3');
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,173 @@
|
||||||
{
|
{
|
||||||
"levels": {
|
"levels": {
|
||||||
"1": {
|
"1": {
|
||||||
"paddleWidth": 200, "paddleHeight": 18, "paddleY": 855,
|
"paddleWidth": 200,
|
||||||
"paddleSpeed": 480, "ballRadius": 12, "ballSpeed": 530,
|
"paddleHeight": 18,
|
||||||
"blockCols": 8, "blockRows": 5, "blockWidth": 140, "blockHeight": 30,
|
"paddleY": 855,
|
||||||
"blockGapX": 20, "blockGapY": 12, "blocksStartX": 170, "blocksStartY": 70,
|
"paddleSpeed": 480,
|
||||||
"bricksToPass": 15
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 530,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 8
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"paddleWidth": 170, "paddleHeight": 18, "paddleY": 855,
|
"paddleWidth": 170,
|
||||||
"paddleSpeed": 520, "ballRadius": 12, "ballSpeed": 640,
|
"paddleHeight": 18,
|
||||||
"blockCols": 8, "blockRows": 5, "blockWidth": 140, "blockHeight": 30,
|
"paddleY": 855,
|
||||||
"blockGapX": 20, "blockGapY": 12, "blocksStartX": 170, "blocksStartY": 70,
|
"paddleSpeed": 520,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 640,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 10
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"paddleWidth": 150,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 555,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 725,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 12
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"paddleWidth": 134,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 583,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 800,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 15
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"paddleWidth": 121,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 607,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 865,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 18
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"paddleWidth": 111,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 627,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 920,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 20
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"paddleWidth": 103,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 643,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 968,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 23
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"paddleWidth": 97,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 655,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 1008,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 25
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"paddleWidth": 92,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 665,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 1042,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
|
"bricksToPass": 27
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"paddleWidth": 88,
|
||||||
|
"paddleHeight": 18,
|
||||||
|
"paddleY": 855,
|
||||||
|
"paddleSpeed": 672,
|
||||||
|
"ballRadius": 12,
|
||||||
|
"ballSpeed": 1070,
|
||||||
|
"blockCols": 8,
|
||||||
|
"blockRows": 5,
|
||||||
|
"blockWidth": 140,
|
||||||
|
"blockHeight": 30,
|
||||||
|
"blockGapX": 20,
|
||||||
|
"blockGapY": 12,
|
||||||
|
"blocksStartX": 170,
|
||||||
|
"blocksStartY": 70,
|
||||||
"bricksToPass": 30
|
"bricksToPass": 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
import { playMusic, stopMusic } from '../../audio.js';
|
||||||
|
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
|
||||||
|
export default class Vortex extends Phaser.Scene {
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'Vortex' });
|
||||||
|
}
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
this.load.json('vortexConfig', 'src/games/vortex/config.json?v=1');
|
||||||
|
this.load.audio('vortexMusic', 'assets/music/05-vortex.mp3');
|
||||||
|
this.load.audio('vortexComplete', 'assets/fx/complete.mp3');
|
||||||
|
this.load.audio('vortexFire', 'assets/fx/laser.mp3');
|
||||||
|
this.load.audio('vortexHit', 'assets/fx/destroy.mp3');
|
||||||
|
this.load.audio('vortexBreach', 'assets/fx/short_death.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data) {
|
||||||
|
this.lives = data.lives ?? 3;
|
||||||
|
this.level = String(data.level ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
const all = this.cache.json.get('vortexConfig');
|
||||||
|
const cfg = all.levels[this.level] ?? all.levels['1'];
|
||||||
|
this.cfg = cfg;
|
||||||
|
|
||||||
|
this.cx = 800;
|
||||||
|
this.cy = 470;
|
||||||
|
this.TWO_PI_N = TAU / cfg.numLanes;
|
||||||
|
|
||||||
|
this.playerLane = 0;
|
||||||
|
this.moveCooldown = 0;
|
||||||
|
this.keyAPressed = false;
|
||||||
|
|
||||||
|
this.bullets = [];
|
||||||
|
this.enemies = [];
|
||||||
|
this.kills = 0;
|
||||||
|
this.misses = 0;
|
||||||
|
this.spawnTimer = cfg.spawnInterval * 0.5; // first spawn after half interval
|
||||||
|
this.gameActive = true;
|
||||||
|
|
||||||
|
this.cursors = this.input.keyboard.createCursorKeys();
|
||||||
|
this.keyA = this.input.keyboard.addKey('A');
|
||||||
|
|
||||||
|
this.bgGfx = this.add.graphics();
|
||||||
|
this.mainGfx = this.add.graphics();
|
||||||
|
|
||||||
|
this._drawBackground();
|
||||||
|
|
||||||
|
this.killsText = this.add.text(800, 16, '', {
|
||||||
|
fontSize: '26px', fontFamily: 'monospace',
|
||||||
|
color: '#00ffff', stroke: '#000000', strokeThickness: 4,
|
||||||
|
}).setOrigin(0.5, 0).setDepth(10);
|
||||||
|
|
||||||
|
this.shieldsText = this.add.text(20, 16, '', {
|
||||||
|
fontSize: '26px', fontFamily: 'monospace',
|
||||||
|
color: '#ffaa00', stroke: '#000000', strokeThickness: 4,
|
||||||
|
}).setDepth(10);
|
||||||
|
|
||||||
|
this.livesText = this.add.text(1580, 16, '', {
|
||||||
|
fontSize: '26px', fontFamily: 'monospace',
|
||||||
|
color: '#ff4444', stroke: '#000000', strokeThickness: 4,
|
||||||
|
}).setOrigin(1, 0).setDepth(10);
|
||||||
|
|
||||||
|
this.levelText = this.add.text(800, 860, `LEVEL ${this.level} | Arrow Keys: Move A: Fire`, {
|
||||||
|
fontSize: '20px', fontFamily: 'monospace',
|
||||||
|
color: '#446688', stroke: '#000000', strokeThickness: 3,
|
||||||
|
}).setOrigin(0.5, 1).setDepth(10);
|
||||||
|
|
||||||
|
playMusic(this, 'vortexMusic');
|
||||||
|
this._updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(time, delta) {
|
||||||
|
if (!this.gameActive) return;
|
||||||
|
const dt = delta / 1000;
|
||||||
|
|
||||||
|
this._handleInput(dt);
|
||||||
|
this._spawnEnemies(dt);
|
||||||
|
this._updateBullets(dt);
|
||||||
|
this._updateEnemies(dt);
|
||||||
|
this._draw();
|
||||||
|
this._updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Input ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_handleInput(dt) {
|
||||||
|
this.moveCooldown = Math.max(0, this.moveCooldown - dt * 1000);
|
||||||
|
|
||||||
|
if (this.moveCooldown <= 0) {
|
||||||
|
if (this.cursors.left.isDown) {
|
||||||
|
this.playerLane = (this.playerLane - 1 + this.cfg.numLanes) % this.cfg.numLanes;
|
||||||
|
this.moveCooldown = this.cfg.moveCooldown;
|
||||||
|
} else if (this.cursors.right.isDown) {
|
||||||
|
this.playerLane = (this.playerLane + 1) % this.cfg.numLanes;
|
||||||
|
this.moveCooldown = this.cfg.moveCooldown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyA.isDown && !this.keyAPressed) {
|
||||||
|
this.keyAPressed = true;
|
||||||
|
this._shoot();
|
||||||
|
}
|
||||||
|
if (!this.keyA.isDown) this.keyAPressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_shoot() {
|
||||||
|
this.bullets.push({ lane: this.playerLane, t: 0.98 });
|
||||||
|
this._playFx('vortexFire', 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Spawn ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_spawnEnemies(dt) {
|
||||||
|
this.spawnTimer += dt;
|
||||||
|
if (this.spawnTimer < this.cfg.spawnInterval) return;
|
||||||
|
if (this.enemies.length >= this.cfg.maxActiveEnemies) return;
|
||||||
|
this.spawnTimer = 0;
|
||||||
|
|
||||||
|
const lane = Phaser.Math.Between(0, this.cfg.numLanes - 1);
|
||||||
|
this.enemies.push({ lane, t: 0.04, alive: true, flash: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Update ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_updateBullets(dt) {
|
||||||
|
const speed = this.cfg.bulletSpeed;
|
||||||
|
for (const b of this.bullets) {
|
||||||
|
b.t -= speed * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bullet–enemy collision
|
||||||
|
for (const b of this.bullets) {
|
||||||
|
if (b.t <= 0) continue;
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (!e.alive || e.lane !== b.lane) continue;
|
||||||
|
if (Math.abs(b.t - e.t) < 0.07) {
|
||||||
|
e.alive = false;
|
||||||
|
b.t = -1;
|
||||||
|
this.kills++;
|
||||||
|
this._playFx('vortexHit', 0.6);
|
||||||
|
if (this.kills >= this.cfg.killsToWin) {
|
||||||
|
this._endGame(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bullets = this.bullets.filter(b => b.t > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateEnemies(dt) {
|
||||||
|
const speed = this.cfg.enemySpeed;
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (!e.alive) continue;
|
||||||
|
e.t += speed * dt;
|
||||||
|
if (e.flash > 0) e.flash -= dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rim breaches
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (!e.alive || e.t < 1.0) continue;
|
||||||
|
e.alive = false;
|
||||||
|
this.misses++;
|
||||||
|
this._playFx('vortexBreach', 0.7);
|
||||||
|
if (this.misses >= this.cfg.maxMisses) {
|
||||||
|
this._endGame(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enemies = this.enemies.filter(e => e.alive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drawing ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_drawBackground() {
|
||||||
|
this.bgGfx.fillStyle(0x000814);
|
||||||
|
this.bgGfx.fillRect(0, 0, 1600, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
_draw() {
|
||||||
|
const g = this.mainGfx;
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
this._drawTube(g);
|
||||||
|
this._drawPlayerLaneHighlight(g);
|
||||||
|
this._drawEnemies(g);
|
||||||
|
this._drawBullets(g);
|
||||||
|
this._drawPlayer(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawTube(g) {
|
||||||
|
const { numLanes, rimRadius } = this.cfg;
|
||||||
|
const cx = this.cx, cy = this.cy;
|
||||||
|
const step = this.TWO_PI_N;
|
||||||
|
const RINGS = 7;
|
||||||
|
|
||||||
|
// Depth rings (concentric polygons shrinking toward center)
|
||||||
|
for (let r = 1; r <= RINGS; r++) {
|
||||||
|
const t = r / RINGS;
|
||||||
|
const depth = t * t; // non-linear for stronger perspective
|
||||||
|
const radius = rimRadius * depth;
|
||||||
|
const alpha = 0.15 + t * 0.25;
|
||||||
|
const blue = Math.floor(60 + t * 140);
|
||||||
|
g.lineStyle(1, Phaser.Display.Color.GetColor(0, blue / 3, blue), alpha);
|
||||||
|
g.beginPath();
|
||||||
|
for (let i = 0; i <= numLanes; i++) {
|
||||||
|
const angle = i * step - Math.PI / 2;
|
||||||
|
const x = cx + radius * Math.cos(angle);
|
||||||
|
const y = cy + radius * Math.sin(angle);
|
||||||
|
if (i === 0) g.moveTo(x, y); else g.lineTo(x, y);
|
||||||
|
}
|
||||||
|
g.closePath();
|
||||||
|
g.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lane dividers: lines from each rim vertex to center
|
||||||
|
for (let i = 0; i < numLanes; i++) {
|
||||||
|
const angle = i * step - Math.PI / 2;
|
||||||
|
const rx = cx + rimRadius * Math.cos(angle);
|
||||||
|
const ry = cy + rimRadius * Math.sin(angle);
|
||||||
|
const isEdge = i === this.playerLane || i === (this.playerLane + 1) % numLanes;
|
||||||
|
if (isEdge) {
|
||||||
|
g.lineStyle(2, 0x00eeff, 0.75);
|
||||||
|
} else {
|
||||||
|
g.lineStyle(1, 0x003355, 0.55);
|
||||||
|
}
|
||||||
|
g.lineBetween(cx, cy, rx, ry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer rim
|
||||||
|
g.lineStyle(3, 0x00ccff, 1.0);
|
||||||
|
g.beginPath();
|
||||||
|
for (let i = 0; i <= numLanes; i++) {
|
||||||
|
const angle = i * step - Math.PI / 2;
|
||||||
|
const x = cx + rimRadius * Math.cos(angle);
|
||||||
|
const y = cy + rimRadius * Math.sin(angle);
|
||||||
|
if (i === 0) g.moveTo(x, y); else g.lineTo(x, y);
|
||||||
|
}
|
||||||
|
g.closePath();
|
||||||
|
g.strokePath();
|
||||||
|
|
||||||
|
// Center vanishing point glow
|
||||||
|
g.fillStyle(0x00aaff, 0.6);
|
||||||
|
g.fillCircle(cx, cy, 5);
|
||||||
|
g.fillStyle(0x002244, 0.4);
|
||||||
|
g.fillCircle(cx, cy, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawPlayerLaneHighlight(g) {
|
||||||
|
const { numLanes, rimRadius } = this.cfg;
|
||||||
|
const cx = this.cx, cy = this.cy;
|
||||||
|
const step = this.TWO_PI_N;
|
||||||
|
const pl = this.playerLane;
|
||||||
|
const a0 = pl * step - Math.PI / 2;
|
||||||
|
const a1 = (pl + 1) * step - Math.PI / 2;
|
||||||
|
|
||||||
|
g.fillStyle(0x00ffff, 0.06);
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(cx, cy);
|
||||||
|
const SEGS = 6;
|
||||||
|
for (let i = 0; i <= SEGS; i++) {
|
||||||
|
const a = a0 + (a1 - a0) * (i / SEGS);
|
||||||
|
g.lineTo(cx + rimRadius * Math.cos(a), cy + rimRadius * Math.sin(a));
|
||||||
|
}
|
||||||
|
g.closePath();
|
||||||
|
g.fillPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawEnemies(g) {
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (!e.alive) continue;
|
||||||
|
const pos = this._lanePos(e.lane, e.t);
|
||||||
|
const angle = this._laneAngle(e.lane);
|
||||||
|
const size = 13;
|
||||||
|
const perp = angle + Math.PI / 2;
|
||||||
|
|
||||||
|
// Triangle pointing outward (toward rim)
|
||||||
|
const tx = pos.x + Math.cos(angle) * size;
|
||||||
|
const ty = pos.y + Math.sin(angle) * size;
|
||||||
|
const lx = pos.x - Math.cos(angle) * (size * 0.4) + Math.cos(perp) * (size * 0.65);
|
||||||
|
const ly = pos.y - Math.sin(angle) * (size * 0.4) + Math.sin(perp) * (size * 0.65);
|
||||||
|
const rx = pos.x - Math.cos(angle) * (size * 0.4) - Math.cos(perp) * (size * 0.65);
|
||||||
|
const ry = pos.y - Math.sin(angle) * (size * 0.4) - Math.sin(perp) * (size * 0.65);
|
||||||
|
|
||||||
|
// Pulse red as they get closer to rim
|
||||||
|
const danger = e.t;
|
||||||
|
const r = Math.floor(200 + danger * 55);
|
||||||
|
const gv = Math.floor(80 - danger * 70);
|
||||||
|
const color = Phaser.Display.Color.GetColor(r, Math.max(0, gv), 0);
|
||||||
|
|
||||||
|
g.fillStyle(color, 1.0);
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(tx, ty);
|
||||||
|
g.lineTo(lx, ly);
|
||||||
|
g.lineTo(rx, ry);
|
||||||
|
g.closePath();
|
||||||
|
g.fillPath();
|
||||||
|
|
||||||
|
g.lineStyle(1.5, 0xff8800, 0.7);
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(tx, ty);
|
||||||
|
g.lineTo(lx, ly);
|
||||||
|
g.lineTo(rx, ry);
|
||||||
|
g.closePath();
|
||||||
|
g.strokePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawBullets(g) {
|
||||||
|
for (const b of this.bullets) {
|
||||||
|
const pos = this._lanePos(b.lane, b.t);
|
||||||
|
const trailPos = this._lanePos(b.lane, Math.min(1, b.t + 0.08));
|
||||||
|
|
||||||
|
g.lineStyle(2, 0xffff00, 0.55);
|
||||||
|
g.lineBetween(pos.x, pos.y, trailPos.x, trailPos.y);
|
||||||
|
|
||||||
|
g.fillStyle(0xffffff, 1.0);
|
||||||
|
g.fillCircle(pos.x, pos.y, 5);
|
||||||
|
g.fillStyle(0xffff00, 0.9);
|
||||||
|
g.fillCircle(pos.x, pos.y, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawPlayer(g) {
|
||||||
|
const { rimRadius } = this.cfg;
|
||||||
|
const cx = this.cx, cy = this.cy;
|
||||||
|
const pl = this.playerLane;
|
||||||
|
const angle = this._laneAngle(pl);
|
||||||
|
const perp = angle + Math.PI / 2;
|
||||||
|
|
||||||
|
// Position slightly outside rim
|
||||||
|
const px = cx + (rimRadius + 22) * Math.cos(angle);
|
||||||
|
const py = cy + (rimRadius + 22) * Math.sin(angle);
|
||||||
|
|
||||||
|
const arm = 18;
|
||||||
|
const depth = 14;
|
||||||
|
|
||||||
|
// Claw: two arms that open toward center, connected at the back
|
||||||
|
const tipX = px + Math.cos(angle) * 6;
|
||||||
|
const tipY = py + Math.sin(angle) * 6;
|
||||||
|
const backX = px - Math.cos(angle) * depth;
|
||||||
|
const backY = py - Math.sin(angle) * depth;
|
||||||
|
|
||||||
|
g.lineStyle(2.5, 0x00ffff, 1.0);
|
||||||
|
|
||||||
|
// Left arm
|
||||||
|
g.lineBetween(tipX, tipY,
|
||||||
|
backX + Math.cos(perp) * arm, backY + Math.sin(perp) * arm);
|
||||||
|
// Right arm
|
||||||
|
g.lineBetween(tipX, tipY,
|
||||||
|
backX - Math.cos(perp) * arm, backY - Math.sin(perp) * arm);
|
||||||
|
// Back connector
|
||||||
|
g.lineBetween(
|
||||||
|
backX + Math.cos(perp) * arm, backY + Math.sin(perp) * arm,
|
||||||
|
backX - Math.cos(perp) * arm, backY - Math.sin(perp) * arm);
|
||||||
|
|
||||||
|
// Tip dot
|
||||||
|
g.fillStyle(0x00ffff, 1.0);
|
||||||
|
g.fillCircle(tipX, tipY, 4);
|
||||||
|
|
||||||
|
// Glow ring
|
||||||
|
g.lineStyle(1, 0x00ffff, 0.3);
|
||||||
|
g.strokeCircle(px, py, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Geometry helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_laneAngle(lane) {
|
||||||
|
return (lane + 0.5) * this.TWO_PI_N - Math.PI / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lanePos(lane, t) {
|
||||||
|
const angle = this._laneAngle(lane);
|
||||||
|
const r = t * this.cfg.rimRadius;
|
||||||
|
return {
|
||||||
|
x: this.cx + r * Math.cos(angle),
|
||||||
|
y: this.cy + r * Math.sin(angle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HUD ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_updateHUD() {
|
||||||
|
const cfg = this.cfg;
|
||||||
|
this.killsText.setText(`KILLS ${this.kills} / ${cfg.killsToWin}`);
|
||||||
|
const shieldsLeft = cfg.maxMisses - this.misses;
|
||||||
|
this.shieldsText.setText('■ '.repeat(shieldsLeft).trim() + '□ '.repeat(this.misses).trim());
|
||||||
|
this.livesText.setText('♥ '.repeat(this.lives).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── End game ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_endGame(success) {
|
||||||
|
if (!this.gameActive) return;
|
||||||
|
this.gameActive = false;
|
||||||
|
|
||||||
|
if (success) this._playFx('vortexComplete', 0.8);
|
||||||
|
|
||||||
|
const msg = success ? 'VICTORY!' : 'BREACHED!';
|
||||||
|
const color = success ? '#00ff44' : '#ff2222';
|
||||||
|
this.add.text(800, 440, msg, {
|
||||||
|
fontSize: '80px', fontFamily: 'monospace',
|
||||||
|
color, stroke: '#000000', strokeThickness: 6,
|
||||||
|
}).setOrigin(0.5).setDepth(20);
|
||||||
|
|
||||||
|
this.time.delayedCall(2200, () => {
|
||||||
|
this.game.events.emit('miniGameResult', { success });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
stopMusic(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_playFx(key, volume = 0.6) {
|
||||||
|
const snd = this.sound.add(key, { volume });
|
||||||
|
snd.once('complete', () => snd.destroy());
|
||||||
|
snd.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"levels": {
|
||||||
|
"1": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.18,
|
||||||
|
"spawnInterval": 2.2,
|
||||||
|
"maxActiveEnemies": 4,
|
||||||
|
"killsToWin": 5,
|
||||||
|
"bulletSpeed": 1.4,
|
||||||
|
"moveCooldown": 120,
|
||||||
|
"maxMisses": 3
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.27,
|
||||||
|
"spawnInterval": 1.5,
|
||||||
|
"maxActiveEnemies": 6,
|
||||||
|
"killsToWin": 7,
|
||||||
|
"bulletSpeed": 1.7,
|
||||||
|
"moveCooldown": 100,
|
||||||
|
"maxMisses": 3
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.33,
|
||||||
|
"spawnInterval": 1.25,
|
||||||
|
"maxActiveEnemies": 6,
|
||||||
|
"killsToWin": 9,
|
||||||
|
"bulletSpeed": 1.85,
|
||||||
|
"moveCooldown": 88,
|
||||||
|
"maxMisses": 3
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.38,
|
||||||
|
"spawnInterval": 1.1,
|
||||||
|
"maxActiveEnemies": 7,
|
||||||
|
"killsToWin": 11,
|
||||||
|
"bulletSpeed": 1.95,
|
||||||
|
"moveCooldown": 78,
|
||||||
|
"maxMisses": 3
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.43,
|
||||||
|
"spawnInterval": 0.98,
|
||||||
|
"maxActiveEnemies": 7,
|
||||||
|
"killsToWin": 13,
|
||||||
|
"bulletSpeed": 2.05,
|
||||||
|
"moveCooldown": 70,
|
||||||
|
"maxMisses": 3
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.47,
|
||||||
|
"spawnInterval": 0.88,
|
||||||
|
"maxActiveEnemies": 8,
|
||||||
|
"killsToWin": 15,
|
||||||
|
"bulletSpeed": 2.1,
|
||||||
|
"moveCooldown": 64,
|
||||||
|
"maxMisses": 2
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.51,
|
||||||
|
"spawnInterval": 0.80,
|
||||||
|
"maxActiveEnemies": 8,
|
||||||
|
"killsToWin": 19,
|
||||||
|
"bulletSpeed": 2.15,
|
||||||
|
"moveCooldown": 58,
|
||||||
|
"maxMisses": 2
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.54,
|
||||||
|
"spawnInterval": 0.73,
|
||||||
|
"maxActiveEnemies": 9,
|
||||||
|
"killsToWin": 23,
|
||||||
|
"bulletSpeed": 2.2,
|
||||||
|
"moveCooldown": 54,
|
||||||
|
"maxMisses": 2
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.57,
|
||||||
|
"spawnInterval": 0.67,
|
||||||
|
"maxActiveEnemies": 9,
|
||||||
|
"killsToWin": 27,
|
||||||
|
"bulletSpeed": 2.25,
|
||||||
|
"moveCooldown": 50,
|
||||||
|
"maxMisses": 2
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"numLanes": 16,
|
||||||
|
"rimRadius": 340,
|
||||||
|
"enemySpeed": 0.60,
|
||||||
|
"spawnInterval": 0.62,
|
||||||
|
"maxActiveEnemies": 10,
|
||||||
|
"killsToWin": 33,
|
||||||
|
"bulletSpeed": 2.3,
|
||||||
|
"moveCooldown": 46,
|
||||||
|
"maxMisses": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import ColoradoDefense from './games/colorado-defense/ColoradoDefense.js';
|
||||||
import CodeBugInvaders from './games/code-bug-invaders/CodeBugInvaders.js';
|
import CodeBugInvaders from './games/code-bug-invaders/CodeBugInvaders.js';
|
||||||
import DotDude from './games/dot-dude/DotDude.js';
|
import DotDude from './games/dot-dude/DotDude.js';
|
||||||
import SmashOut from './games/smash-out/SmashOut.js';
|
import SmashOut from './games/smash-out/SmashOut.js';
|
||||||
|
import Vortex from './games/vortex/Vortex.js';
|
||||||
|
|
||||||
new Phaser.Game({
|
new Phaser.Game({
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
|
|
@ -16,5 +17,5 @@ new Phaser.Game({
|
||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
},
|
},
|
||||||
scene: [MainMenu, GameManager, ColoradoDefense, CodeBugInvaders, DotDude, SmashOut, GameOver, LevelComplete],
|
scene: [MainMenu, GameManager, ColoradoDefense, CodeBugInvaders, DotDude, SmashOut, Vortex, GameOver, LevelComplete],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export default class GameManager extends Phaser.Scene {
|
||||||
init(data) {
|
init(data) {
|
||||||
this.config = data.config;
|
this.config = data.config;
|
||||||
this.level = data.level || 1;
|
this.level = data.level || 1;
|
||||||
this.lives = data.lives !== undefined ? data.lives : this.config.lives;
|
this.lives = data.lives ?? this.config.lives ?? 3;
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.queueIndex = 0;
|
this.queueIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -14,10 +14,14 @@ export default class GameManager extends Phaser.Scene {
|
||||||
create() {
|
create() {
|
||||||
this.announceTween = null;
|
this.announceTween = null;
|
||||||
this.announceTimer = null;
|
this.announceTimer = null;
|
||||||
|
this.resultHandled = false;
|
||||||
|
|
||||||
|
// Remove any stale listener from a previous session before adding a fresh one
|
||||||
|
this.game.events.off('miniGameResult', this._onResult, this);
|
||||||
|
this.game.events.on('miniGameResult', this._onResult, this);
|
||||||
|
|
||||||
this._buildQueue();
|
this._buildQueue();
|
||||||
this._showAnnouncement();
|
this._showAnnouncement();
|
||||||
|
|
||||||
this.game.events.on('miniGameResult', this._onResult, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
|
|
@ -86,12 +90,16 @@ export default class GameManager extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
this.cameras.main.setBackgroundColor('#000000');
|
this.cameras.main.setBackgroundColor('#000000');
|
||||||
|
|
||||||
|
this.resultHandled = false; // ready to accept exactly one result per launch
|
||||||
const key = this.queue[this.queueIndex];
|
const key = this.queue[this.queueIndex];
|
||||||
this.scene.launch(key, { lives: this.lives, level: this.level });
|
this.scene.launch(key, { lives: this.lives, level: this.level });
|
||||||
this.scene.bringToTop(key);
|
this.scene.bringToTop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResult({ success }) {
|
_onResult({ success }) {
|
||||||
|
if (this.resultHandled) return; // guard against duplicate events
|
||||||
|
this.resultHandled = true;
|
||||||
|
|
||||||
const key = this.queue[this.queueIndex];
|
const key = this.queue[this.queueIndex];
|
||||||
if (this.scene.isActive(key)) {
|
if (this.scene.isActive(key)) {
|
||||||
this.scene.stop(key);
|
this.scene.stop(key);
|
||||||
|
|
@ -131,6 +139,7 @@ export default class GameManager extends Phaser.Scene {
|
||||||
CodeBugInvaders: 'CODE BUG INVADERS',
|
CodeBugInvaders: 'CODE BUG INVADERS',
|
||||||
DotDude: 'DOT DUDE',
|
DotDude: 'DOT DUDE',
|
||||||
SmashOut: 'SMASH OUT',
|
SmashOut: 'SMASH OUT',
|
||||||
|
Vortex: 'VORTEX',
|
||||||
};
|
};
|
||||||
return names[key] || key;
|
return names[key] || key;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export default class MainMenu extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('gameConfig', 'config/game.json');
|
this.load.json('gameConfig', 'config/game.json?v=2');
|
||||||
this.load.audio('mainTheme', 'assets/music/00-main-theme.mp3');
|
this.load.audio('mainTheme', 'assets/music/00-main-theme.mp3');
|
||||||
this.load.image('logo-title', 'assets/images/logo-title.png');
|
this.load.image('logo-title', 'assets/images/logo-title.png');
|
||||||
this.load.image('logo-people', 'assets/images/logo-people.png');
|
this.load.image('logo-people', 'assets/images/logo-people.png');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue