diff --git a/assets/sprites/player.png b/assets/sprites/player.png new file mode 100644 index 0000000..d90a8fe Binary files /dev/null and b/assets/sprites/player.png differ diff --git a/assets/sprites/player.psd b/assets/sprites/player.psd new file mode 100644 index 0000000..fdc7262 Binary files /dev/null and b/assets/sprites/player.psd differ diff --git a/js/entities/Player.js b/js/entities/Player.js index c184029..ef9f722 100644 --- a/js/entities/Player.js +++ b/js/entities/Player.js @@ -2,10 +2,11 @@ const TURN_RATE_DEG = 4; // degrees per frame at 60fps const BULLET_SPEED = 700; const BULLET_DAMAGE = 20; const BULLET_SIZE = 5; -const INVINCIBILITY_MS = 1500; const PLAYER_RADIUS = 16; -const PLAYER_COLOR = 0x00ccff; -const BARREL_LENGTH = 22; +// Sprite faces DOWN at rotation=0; Phaser's 0° is RIGHT, so offset by -90° +const SPRITE_ROTATION_OFFSET = -Math.PI / 2; +// Gun tip distance from sprite center (in pixels) +const GUN_TIP_DIST = 24; export class Player { constructor(scene, x, y) { @@ -26,18 +27,32 @@ export class Player { this.invincible = false; this._fireCooldown = 0; this._baseFireInterval = 250; // ms between shots + this._isMoving = false; + this._isShooting = false; - this._buildGraphics(x, y); + this._createAnims(); + this._buildSprite(x, y); this._setupKeys(); this.bullets = scene.add.group(); } - _buildGraphics(x, y) { - this.body = this.scene.add.circle(x, y, PLAYER_RADIUS, PLAYER_COLOR); - this.barrel = this.scene.add.rectangle(x + BARREL_LENGTH / 2, y, BARREL_LENGTH, 5, 0x0088cc); - this.scene.physics.add.existing(this.body); - this.body.body.setCollideWorldBounds(true); - this.body.body.setCircle(PLAYER_RADIUS); + _createAnims() { + const anims = this.scene.anims; + if (anims.exists('player-idle')) return; // already created + + anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 }); + anims.create({ key: 'player-walk', frames: anims.generateFrameNumbers('player', { frames: [1, 2] }), frameRate: 8, repeat: -1 }); + anims.create({ key: 'player-idle-gun', frames: [{ key: 'player', frame: 3 }], frameRate: 1, repeat: -1 }); + anims.create({ key: 'player-walk-gun', frames: anims.generateFrameNumbers('player', { frames: [4, 5] }), frameRate: 8, repeat: -1 }); + } + + _buildSprite(x, y) { + this.sprite = this.scene.add.sprite(x, y, 'player'); + this.scene.physics.add.existing(this.sprite); + this.sprite.body.setCollideWorldBounds(true); + // Circle hitbox centered in the 48×48 sprite: radius 16, offset 8 from top-left + this.sprite.body.setCircle(PLAYER_RADIUS, 8, 8); + this.sprite.play('player-idle'); } _setupKeys() { @@ -49,15 +64,15 @@ export class Player { }); } - get x() { return this.body.x; } - get y() { return this.body.y; } - get active() { return this.body.active; } + get x() { return this.sprite.x; } + get y() { return this.sprite.y; } + get active() { return this.sprite?.active ?? false; } update(delta) { this._move(); this._rotateFacing(delta); - this._updateBarrel(); this._handleFire(delta); + this._updateAnimation(); this._updateBullets(); } @@ -68,8 +83,9 @@ export class Player { if (this.keys.up.isDown) dy -= 1; if (this.keys.down.isDown) dy += 1; + this._isMoving = dx !== 0 || dy !== 0; if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; } - this.body.body.setVelocity(dx * this.stats.speed, dy * this.stats.speed); + this.sprite.body.setVelocity(dx * this.stats.speed, dy * this.stats.speed); } _rotateFacing(delta) { @@ -77,27 +93,35 @@ export class Player { const targetAngle = Phaser.Math.Angle.Between(this.x, this.y, ptr.worldX, ptr.worldY); const maxTurn = Phaser.Math.DegToRad(TURN_RATE_DEG) * (delta / (1000 / 60)); this.facing = Phaser.Math.Angle.RotateTo(this.facing, targetAngle, maxTurn); - } - - _updateBarrel() { - const bx = this.x + Math.cos(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH / 2); - const by = this.y + Math.sin(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH / 2); - this.barrel.setPosition(bx, by); - this.barrel.setRotation(this.facing); + // Sprite art faces DOWN; offset by -90° so it faces RIGHT at facing=0 + this.sprite.setRotation(this.facing + SPRITE_ROTATION_OFFSET); } _handleFire(delta) { this._fireCooldown -= delta; const ptr = this.scene.input.activePointer; + this._isShooting = ptr.isDown; if (ptr.isDown && this._fireCooldown <= 0) { this._fireCooldown = this._baseFireInterval / this.stats.fireRate; this._spawnBullet(); } } + _updateAnimation() { + let key; + if (this._isMoving && this._isShooting) key = 'player-walk-gun'; + else if (this._isMoving) key = 'player-walk'; + else if (this._isShooting) key = 'player-idle-gun'; + else key = 'player-idle'; + + if (this.sprite.anims.currentAnim?.key !== key) { + this.sprite.play(key); + } + } + _spawnBullet() { - const bx = this.x + Math.cos(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH); - const by = this.y + Math.sin(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH); + const bx = this.x + Math.cos(this.facing) * GUN_TIP_DIST; + const by = this.y + Math.sin(this.facing) * GUN_TIP_DIST; const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00); this.scene.physics.add.existing(bullet); bullet.body.setVelocity( @@ -141,31 +165,27 @@ export class Player { _startInvincibility() { this.invincible = true; - // Flash effect this.scene.tweens.add({ - targets: [this.body, this.barrel], + targets: this.sprite, alpha: 0, duration: 150, yoyo: true, repeat: 4, onComplete: () => { - this.body.setAlpha(1); - this.barrel.setAlpha(1); + this.sprite.setAlpha(1); this.invincible = false; } }); } respawn(x, y) { - this.body.setPosition(x, y); - this.barrel.setPosition(x, y); + this.sprite.setPosition(x, y); this.hp = this.stats.maxHp; this._startInvincibility(); } destroy() { - this.body.destroy(); - this.barrel.destroy(); + this.sprite.destroy(); this.bullets.clear(true, true); } } diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 8194fa9..fb12937 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -13,6 +13,7 @@ export class GameScene extends Phaser.Scene { preload() { this.load.json('zones', './js/data/zones.json'); this.load.json('skillTree', './js/data/skillTree.json'); + this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 }); } create() {