Add bonus system with medpacks, shotgun, and rocket launcher power-ups
This commit is contained in:
parent
840f6788ab
commit
145341d992
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
|
@ -30,11 +30,14 @@ export class Player {
|
|||
this._baseFireInterval = 250; // ms between shots
|
||||
this._isMoving = false;
|
||||
this._isShooting = false;
|
||||
this.weaponMode = 'default'; // 'default' | 'shotgun' | 'rocket'
|
||||
this.weaponTimeLeft = 0; // ms remaining for timed weapon
|
||||
|
||||
this._createAnims();
|
||||
this._buildSprite(x, y);
|
||||
this._setupKeys();
|
||||
this.bullets = scene.add.group();
|
||||
this.rockets = scene.add.group();
|
||||
}
|
||||
|
||||
_createAnims() {
|
||||
|
|
@ -76,6 +79,14 @@ export class Player {
|
|||
this._handleFire(delta);
|
||||
this._updateAnimation();
|
||||
this._updateBullets();
|
||||
this._updateRockets();
|
||||
if (this.weaponTimeLeft > 0) {
|
||||
this.weaponTimeLeft -= delta;
|
||||
if (this.weaponTimeLeft <= 0) {
|
||||
this.weaponTimeLeft = 0;
|
||||
this.weaponMode = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_move() {
|
||||
|
|
@ -105,7 +116,9 @@ export class Player {
|
|||
this._isShooting = ptr.isDown;
|
||||
if (ptr.isDown && this._fireCooldown <= 0) {
|
||||
this._fireCooldown = this._baseFireInterval / this.stats.fireRate;
|
||||
this._spawnBullet();
|
||||
if (this.weaponMode === 'shotgun') this._spawnShotgunBurst();
|
||||
else if (this.weaponMode === 'rocket') this._spawnRocket();
|
||||
else this._spawnBullet();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +158,53 @@ export class Player {
|
|||
});
|
||||
}
|
||||
|
||||
_spawnShotgunBurst() {
|
||||
this.scene.sound.play('sfx-shoot', { volume: 0.4 });
|
||||
[-20, -10, 0, 10, 20].forEach(deg => {
|
||||
const angle = this.facing + Phaser.Math.DegToRad(deg);
|
||||
const bx = this.x + Math.cos(angle) * GUN_TIP_DIST;
|
||||
const by = this.y + Math.sin(angle) * GUN_TIP_DIST;
|
||||
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
|
||||
this.scene.physics.add.existing(bullet);
|
||||
bullet.body.setVelocity(Math.cos(angle) * BULLET_SPEED, Math.sin(angle) * BULLET_SPEED);
|
||||
bullet.damage = this.stats.damage;
|
||||
this.bullets.add(bullet);
|
||||
});
|
||||
}
|
||||
|
||||
_spawnRocket() {
|
||||
this.scene.sound.play('sfx-shoot', { volume: 0.5 });
|
||||
const bx = this.x + Math.cos(this.facing) * GUN_TIP_DIST;
|
||||
const by = this.y + Math.sin(this.facing) * GUN_TIP_DIST;
|
||||
const rocket = this.scene.add.circle(bx, by, 8, 0xff4400);
|
||||
this.scene.physics.add.existing(rocket);
|
||||
rocket.body.setVelocity(Math.cos(this.facing) * 400, Math.sin(this.facing) * 400);
|
||||
rocket.damage = 80;
|
||||
this.rockets.add(rocket);
|
||||
}
|
||||
|
||||
_updateRockets() {
|
||||
const W = this.scene.scale.width;
|
||||
const H = this.scene.scale.height;
|
||||
this.rockets.getChildren().forEach(r => {
|
||||
if (r.x < -20 || r.x > W + 20 || r.y < -20 || r.y > H + 20) {
|
||||
r.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyBonus(type) {
|
||||
if (type === 'medpack') {
|
||||
this.hp = Math.min(this.hp + this.stats.maxHp * 0.5, this.stats.maxHp);
|
||||
} else if (type === 'shotgun') {
|
||||
this.weaponMode = 'shotgun';
|
||||
this.weaponTimeLeft = 20000;
|
||||
} else if (type === 'rocket') {
|
||||
this.weaponMode = 'rocket';
|
||||
this.weaponTimeLeft = 20000;
|
||||
}
|
||||
}
|
||||
|
||||
takeDamage(amount) {
|
||||
if (this.invincible) return;
|
||||
const reduced = amount * (1 - Math.min(this.stats.damageReduction, 0.9));
|
||||
|
|
@ -300,5 +360,6 @@ export class Player {
|
|||
destroy() {
|
||||
this.sprite.destroy();
|
||||
this.bullets.clear(true, true);
|
||||
this.rockets.clear(true, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { HUD } from '../ui/HUD.js';
|
|||
import { SkillTreeUI } from '../ui/SkillTreeUI.js';
|
||||
import { Reticle } from '../ui/Reticle.js';
|
||||
import { BarrierManager } from '../systems/BarrierManager.js';
|
||||
import { BonusManager } from '../systems/BonusManager.js';
|
||||
|
||||
const ZONE_PALETTE = [
|
||||
null, // index 0 unused
|
||||
|
|
@ -42,6 +43,7 @@ export class GameScene extends Phaser.Scene {
|
|||
this.load.audio('sfx-death-shooter', './assets/fx/EnemyShooterDeath.mp3');
|
||||
this.load.audio('sfx-death-sprayer', './assets/fx/EnemySprayerDeath.mp3');
|
||||
this.load.audio('sfx-death-bomber', './assets/fx/EnemyBomberDeath.mp3');
|
||||
this.load.spritesheet('bonus', './assets/sprites/bonus.png', { frameWidth: 32, frameHeight: 32 });
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -64,6 +66,7 @@ export class GameScene extends Phaser.Scene {
|
|||
this.waveManager.load(this.cache.json.get('zones'));
|
||||
|
||||
this.hud = new HUD(this, this.player, this.xpSystem);
|
||||
this.bonusManager = new BonusManager(this, this.player);
|
||||
|
||||
// Enemy projectile group for collision
|
||||
this._enemyProjectiles = [];
|
||||
|
|
@ -238,7 +241,9 @@ export class GameScene extends Phaser.Scene {
|
|||
this.waveManager.update(delta);
|
||||
this.hud.update();
|
||||
|
||||
this.bonusManager.update(delta);
|
||||
this._checkBulletHits();
|
||||
this._checkRocketHits();
|
||||
this._checkEnemyProjectileHits();
|
||||
this._pruneEnemyProjectiles();
|
||||
this._updatePulseEffects(delta);
|
||||
|
|
@ -317,6 +322,41 @@ export class GameScene extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
_checkRocketHits() {
|
||||
const rockets = this.player.rockets.getChildren();
|
||||
for (const rocket of rockets) {
|
||||
if (!rocket.active) continue;
|
||||
if (this.barrierManager.isPointBlocked(rocket.x, rocket.y)) {
|
||||
this._explodeRocket(rocket.x, rocket.y, rocket.damage);
|
||||
rocket.destroy();
|
||||
continue;
|
||||
}
|
||||
for (const enemy of this.waveManager.enemies) {
|
||||
if (!enemy.active) continue;
|
||||
if (Phaser.Math.Distance.Between(rocket.x, rocket.y, enemy.x, enemy.y) < enemy.radius + 8) {
|
||||
this._explodeRocket(rocket.x, rocket.y, rocket.damage);
|
||||
rocket.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_explodeRocket(x, y, damage) {
|
||||
const flash = this.add.circle(x, y, 10, 0xff6600, 0.9).setDepth(5);
|
||||
this.tweens.add({ targets: flash, scaleX: 14, scaleY: 14, alpha: 0, duration: 320, onComplete: () => flash.destroy() });
|
||||
const ring = this.add.circle(x, y, 10, 0x000000, 0).setDepth(5);
|
||||
ring.setStrokeStyle(4, 0xffcc00, 1);
|
||||
this.tweens.add({ targets: ring, scaleX: 13, scaleY: 13, alpha: 0, duration: 400, onComplete: () => ring.destroy() });
|
||||
this.cameras.main.shake(130, 0.009);
|
||||
const RADIUS = 120;
|
||||
this.waveManager.enemies.forEach(e => {
|
||||
if (!e.active) return;
|
||||
const dist = Phaser.Math.Distance.Between(x, y, e.x, e.y);
|
||||
if (dist < RADIUS) e.takeDamage(damage * (1 - dist / RADIUS));
|
||||
});
|
||||
}
|
||||
|
||||
_checkEnemyProjectileHits() {
|
||||
this._enemyProjectiles = this._enemyProjectiles.filter(p => p?.active);
|
||||
this._enemyProjectiles.forEach(proj => {
|
||||
|
|
@ -532,6 +572,7 @@ export class GameScene extends Phaser.Scene {
|
|||
this.player?.destroy();
|
||||
this.waveManager?.reset();
|
||||
this.hud?.destroy();
|
||||
this.bonusManager?.destroy();
|
||||
this._bgMusic?.stop();
|
||||
this._bgMusic = null;
|
||||
this._enemyProjectiles = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
const BONUS_LIFETIME_MS = 15000;
|
||||
const BLINK_START_MS = 10000; // blink when < 5s left (at 10s elapsed of 15s)
|
||||
const COOLDOWN_PICKUP = 20000;
|
||||
const COOLDOWN_EXPIRE = 10000;
|
||||
const FIRST_SPAWN_MS = 20000;
|
||||
const PICKUP_RADIUS = 32;
|
||||
const BONUS_TYPES = ['medpack', 'shotgun', 'rocket'];
|
||||
|
||||
const FRAME_INDEX = { medpack: 0, shotgun: 1, rocket: 2 };
|
||||
const GLOW_COLOR = { medpack: 0x44ff44, shotgun: 0xff9900, rocket: 0xff3300 };
|
||||
|
||||
export class BonusManager {
|
||||
constructor(scene, player) {
|
||||
this._scene = scene;
|
||||
this._player = player;
|
||||
this._countdown = FIRST_SPAWN_MS;
|
||||
this._active = null; // { sprite, glow, glowTween, blinkTween, type, elapsed }
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
if (!this._active) {
|
||||
this._countdown -= delta;
|
||||
if (this._countdown <= 0) this._spawn();
|
||||
return;
|
||||
}
|
||||
|
||||
const a = this._active;
|
||||
a.elapsed += delta;
|
||||
|
||||
// Pickup check
|
||||
const dist = Phaser.Math.Distance.Between(
|
||||
a.sprite.x, a.sprite.y, this._player.x, this._player.y
|
||||
);
|
||||
if (dist < PICKUP_RADIUS) {
|
||||
this._player.applyBonus(a.type);
|
||||
this._clear();
|
||||
this._countdown = COOLDOWN_PICKUP;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start blinking for last 5 seconds
|
||||
if (a.elapsed >= BLINK_START_MS && !a.blinkTween) {
|
||||
a.blinkTween = this._scene.tweens.add({
|
||||
targets: [a.sprite, a.glow],
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
// Expiry
|
||||
if (a.elapsed >= BONUS_LIFETIME_MS) {
|
||||
this._clear();
|
||||
this._countdown = COOLDOWN_EXPIRE;
|
||||
}
|
||||
}
|
||||
|
||||
_spawn() {
|
||||
const type = BONUS_TYPES[Phaser.Math.Between(0, 2)];
|
||||
const cell = this._findSpawnCell();
|
||||
if (!cell) {
|
||||
// No valid cell found — retry in 2 seconds
|
||||
this._countdown = 2000;
|
||||
return;
|
||||
}
|
||||
|
||||
const { cx, cy } = cell;
|
||||
const glow = this._scene.add.circle(cx, cy, 24, GLOW_COLOR[type], 0.4).setDepth(4);
|
||||
const glowTween = this._scene.tweens.add({
|
||||
targets: glow,
|
||||
scaleX: 1.4, scaleY: 1.4,
|
||||
alpha: 0.1,
|
||||
duration: 700,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
});
|
||||
|
||||
const sprite = this._scene.add.image(cx, cy, 'bonus', FRAME_INDEX[type]).setDepth(5);
|
||||
|
||||
this._active = { sprite, glow, glowTween, blinkTween: null, type, elapsed: 0 };
|
||||
}
|
||||
|
||||
_findSpawnCell() {
|
||||
const W = this._scene.scale.width;
|
||||
const H = this._scene.scale.height;
|
||||
const COLS = Math.floor(W / 80);
|
||||
const ROWS = Math.floor(H / 80);
|
||||
const candidates = [];
|
||||
|
||||
for (let col = 1; col < COLS - 1; col++) {
|
||||
for (let row = 1; row < ROWS - 1; row++) {
|
||||
const cx = col * 80 + 40;
|
||||
const cy = row * 80 + 40;
|
||||
if (this._scene.barrierManager?.isPointBlocked(cx, cy)) continue;
|
||||
if (Phaser.Math.Distance.Between(cx, cy, this._player.x, this._player.y) < 300) continue;
|
||||
candidates.push({ cx, cy });
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.length ? Phaser.Utils.Array.GetRandom(candidates) : null;
|
||||
}
|
||||
|
||||
_clear() {
|
||||
if (!this._active) return;
|
||||
const a = this._active;
|
||||
a.blinkTween?.stop();
|
||||
a.glowTween?.stop();
|
||||
a.sprite?.destroy();
|
||||
a.glow?.destroy();
|
||||
this._active = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._clear();
|
||||
}
|
||||
}
|
||||
24
js/ui/HUD.js
24
js/ui/HUD.js
|
|
@ -39,6 +39,12 @@ export class HUD {
|
|||
this._xpFill = scene.add.rectangle(PAD + XP_W / 2, xpY, XP_W, XP_H, 0x44ffaa).setScrollFactor(0).setDepth(10);
|
||||
this._xpLabel = scene.add.text(PAD, xpY + 8, 'Level 1', { fontSize: '12px', fill: '#aaffcc' }).setScrollFactor(0).setDepth(10);
|
||||
|
||||
// --- Weapon timer bar (top-center, hidden by default) ---
|
||||
const W = scene.scale.width;
|
||||
this._weaponBarBg = scene.add.rectangle(W / 2, 12, 200, 14, 0x333333).setScrollFactor(0).setDepth(10).setVisible(false);
|
||||
this._weaponBarFill = scene.add.rectangle(W / 2 - 100, 12, 0, 14, 0xff9900).setScrollFactor(0).setDepth(11).setOrigin(0, 0.5).setVisible(false);
|
||||
this._weaponLabel = scene.add.text(W / 2, 23, '', { fontSize: '11px', fill: '#ffffff' }).setScrollFactor(0).setDepth(10).setOrigin(0.5, 0).setVisible(false);
|
||||
|
||||
// --- Zone/Wave indicator ---
|
||||
this._zoneText = scene.add.text(0, PAD, 'Zone 1 — Wave 1/3', { fontSize: '14px', fill: '#ffffff' })
|
||||
.setScrollFactor(0).setDepth(10).setOrigin(1, 0);
|
||||
|
|
@ -69,12 +75,28 @@ export class HUD {
|
|||
this._xpFill.width = XP_W * xpRatio;
|
||||
this._xpFill.x = PAD + (XP_W * xpRatio) / 2;
|
||||
this._xpLabel.setText(`Level ${this.xpSystem.level}`);
|
||||
|
||||
// Weapon timer bar
|
||||
const hasWeapon = this.player.weaponMode !== 'default';
|
||||
this._weaponBarBg.setVisible(hasWeapon);
|
||||
this._weaponBarFill.setVisible(hasWeapon);
|
||||
this._weaponLabel.setVisible(hasWeapon);
|
||||
if (hasWeapon) {
|
||||
const ratio = Math.max(0, this.player.weaponTimeLeft / 20000);
|
||||
this._weaponBarFill.width = 200 * ratio;
|
||||
const color = this.player.weaponMode === 'rocket' ? 0xff3300 : 0xff9900;
|
||||
this._weaponBarFill.setFillStyle(color);
|
||||
this._weaponLabel.setText(this.player.weaponMode === 'rocket' ? 'ROCKET LAUNCHER' : 'SHOTGUN');
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.scene.events.off('wave-start');
|
||||
this.scene.events.off('zone-clear');
|
||||
[this._hpLabel, this._hpBg, this._hpFill, this._xpBg, this._xpFill, this._xpLabel, this._zoneText, ...this._lifeIcons]
|
||||
[this._hpLabel, this._hpBg, this._hpFill,
|
||||
this._xpBg, this._xpFill, this._xpLabel,
|
||||
this._weaponBarBg, this._weaponBarFill, this._weaponLabel,
|
||||
this._zoneText, ...this._lifeIcons]
|
||||
.forEach(o => o?.destroy());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue