Refactor Player entity to use sprite-based rendering with animations
- Replaced simple circle/rectangle graphics with a spritesheet-based player sprite. - Added animation definitions for idle, walk, idle-gun, and walk-gun states. - Implemented state tracking (`_isMoving`, `_isShooting`) to drive animation switching. - Adjusted barrel position calculation to use `GUN_TIP_DIST` for bullet spawning. - Updated invincibility tween and cleanup logic to target the new sprite object. - Added sprite loading in GameScene preload phase.
This commit is contained in:
parent
8dc7762aba
commit
ea0a95c4e5
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
|
@ -2,10 +2,11 @@ const TURN_RATE_DEG = 4; // degrees per frame at 60fps
|
||||||
const BULLET_SPEED = 700;
|
const BULLET_SPEED = 700;
|
||||||
const BULLET_DAMAGE = 20;
|
const BULLET_DAMAGE = 20;
|
||||||
const BULLET_SIZE = 5;
|
const BULLET_SIZE = 5;
|
||||||
const INVINCIBILITY_MS = 1500;
|
|
||||||
const PLAYER_RADIUS = 16;
|
const PLAYER_RADIUS = 16;
|
||||||
const PLAYER_COLOR = 0x00ccff;
|
// Sprite faces DOWN at rotation=0; Phaser's 0° is RIGHT, so offset by -90°
|
||||||
const BARREL_LENGTH = 22;
|
const SPRITE_ROTATION_OFFSET = -Math.PI / 2;
|
||||||
|
// Gun tip distance from sprite center (in pixels)
|
||||||
|
const GUN_TIP_DIST = 24;
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(scene, x, y) {
|
constructor(scene, x, y) {
|
||||||
|
|
@ -26,18 +27,32 @@ export class Player {
|
||||||
this.invincible = false;
|
this.invincible = false;
|
||||||
this._fireCooldown = 0;
|
this._fireCooldown = 0;
|
||||||
this._baseFireInterval = 250; // ms between shots
|
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._setupKeys();
|
||||||
this.bullets = scene.add.group();
|
this.bullets = scene.add.group();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildGraphics(x, y) {
|
_createAnims() {
|
||||||
this.body = this.scene.add.circle(x, y, PLAYER_RADIUS, PLAYER_COLOR);
|
const anims = this.scene.anims;
|
||||||
this.barrel = this.scene.add.rectangle(x + BARREL_LENGTH / 2, y, BARREL_LENGTH, 5, 0x0088cc);
|
if (anims.exists('player-idle')) return; // already created
|
||||||
this.scene.physics.add.existing(this.body);
|
|
||||||
this.body.body.setCollideWorldBounds(true);
|
anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 });
|
||||||
this.body.body.setCircle(PLAYER_RADIUS);
|
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() {
|
_setupKeys() {
|
||||||
|
|
@ -49,15 +64,15 @@ export class Player {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get x() { return this.body.x; }
|
get x() { return this.sprite.x; }
|
||||||
get y() { return this.body.y; }
|
get y() { return this.sprite.y; }
|
||||||
get active() { return this.body.active; }
|
get active() { return this.sprite?.active ?? false; }
|
||||||
|
|
||||||
update(delta) {
|
update(delta) {
|
||||||
this._move();
|
this._move();
|
||||||
this._rotateFacing(delta);
|
this._rotateFacing(delta);
|
||||||
this._updateBarrel();
|
|
||||||
this._handleFire(delta);
|
this._handleFire(delta);
|
||||||
|
this._updateAnimation();
|
||||||
this._updateBullets();
|
this._updateBullets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,8 +83,9 @@ export class Player {
|
||||||
if (this.keys.up.isDown) dy -= 1;
|
if (this.keys.up.isDown) dy -= 1;
|
||||||
if (this.keys.down.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; }
|
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) {
|
_rotateFacing(delta) {
|
||||||
|
|
@ -77,27 +93,35 @@ export class Player {
|
||||||
const targetAngle = Phaser.Math.Angle.Between(this.x, this.y, ptr.worldX, ptr.worldY);
|
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));
|
const maxTurn = Phaser.Math.DegToRad(TURN_RATE_DEG) * (delta / (1000 / 60));
|
||||||
this.facing = Phaser.Math.Angle.RotateTo(this.facing, targetAngle, maxTurn);
|
this.facing = Phaser.Math.Angle.RotateTo(this.facing, targetAngle, maxTurn);
|
||||||
}
|
// Sprite art faces DOWN; offset by -90° so it faces RIGHT at facing=0
|
||||||
|
this.sprite.setRotation(this.facing + SPRITE_ROTATION_OFFSET);
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleFire(delta) {
|
_handleFire(delta) {
|
||||||
this._fireCooldown -= delta;
|
this._fireCooldown -= delta;
|
||||||
const ptr = this.scene.input.activePointer;
|
const ptr = this.scene.input.activePointer;
|
||||||
|
this._isShooting = ptr.isDown;
|
||||||
if (ptr.isDown && this._fireCooldown <= 0) {
|
if (ptr.isDown && this._fireCooldown <= 0) {
|
||||||
this._fireCooldown = this._baseFireInterval / this.stats.fireRate;
|
this._fireCooldown = this._baseFireInterval / this.stats.fireRate;
|
||||||
this._spawnBullet();
|
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() {
|
_spawnBullet() {
|
||||||
const bx = this.x + Math.cos(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) * (PLAYER_RADIUS + BARREL_LENGTH);
|
const by = this.y + Math.sin(this.facing) * GUN_TIP_DIST;
|
||||||
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
|
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
|
||||||
this.scene.physics.add.existing(bullet);
|
this.scene.physics.add.existing(bullet);
|
||||||
bullet.body.setVelocity(
|
bullet.body.setVelocity(
|
||||||
|
|
@ -141,31 +165,27 @@ export class Player {
|
||||||
|
|
||||||
_startInvincibility() {
|
_startInvincibility() {
|
||||||
this.invincible = true;
|
this.invincible = true;
|
||||||
// Flash effect
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: [this.body, this.barrel],
|
targets: this.sprite,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
duration: 150,
|
duration: 150,
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
repeat: 4,
|
repeat: 4,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.body.setAlpha(1);
|
this.sprite.setAlpha(1);
|
||||||
this.barrel.setAlpha(1);
|
|
||||||
this.invincible = false;
|
this.invincible = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
respawn(x, y) {
|
respawn(x, y) {
|
||||||
this.body.setPosition(x, y);
|
this.sprite.setPosition(x, y);
|
||||||
this.barrel.setPosition(x, y);
|
|
||||||
this.hp = this.stats.maxHp;
|
this.hp = this.stats.maxHp;
|
||||||
this._startInvincibility();
|
this._startInvincibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.body.destroy();
|
this.sprite.destroy();
|
||||||
this.barrel.destroy();
|
|
||||||
this.bullets.clear(true, true);
|
this.bullets.clear(true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export class GameScene extends Phaser.Scene {
|
||||||
preload() {
|
preload() {
|
||||||
this.load.json('zones', './js/data/zones.json');
|
this.load.json('zones', './js/data/zones.json');
|
||||||
this.load.json('skillTree', './js/data/skillTree.json');
|
this.load.json('skillTree', './js/data/skillTree.json');
|
||||||
|
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue