feat: Implement object health system, damage mechanics, inventory management, and enhanced player movement

- Added health bars for objects with visual indicators
- Implemented takeDamage and destroyObject functionality for trees and boulders
- Created resource dropping system when objects are destroyed
- Enhanced player movement with grid-based positioning and collision handling
- Added inventory manager for item collection and storage
- Integrated physics collisions between player, objects, and collectibles
- Improved UI with updated time of day text and font styling
This commit is contained in:
Brian Fertig 2025-08-17 15:40:25 -06:00
parent 67c207e54e
commit 321b491c7b
11 changed files with 346 additions and 50 deletions

BIN
assets/fonts/EraserDust.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/images/backpack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 120 KiB

BIN
raw/eraser-dust.zip Normal file

Binary file not shown.

Binary file not shown.

View File

@ -30,8 +30,12 @@ export class CycleManager {
this.cycleText = this.scene.add.text( this.cycleText = this.scene.add.text(
40, 40,
40, 40,
'Day', 'Time of Day: Day',
{ fontSize: '24px', fill: '#ffffff' } {
fontSize: '36px',
fill: '#ffffff',
fontFamily: 'eraserDust, arial'
}
).setShadow(3,3, '#333', 5).setScrollFactor(0); ).setShadow(3,3, '#333', 5).setScrollFactor(0);
} }
@ -53,7 +57,7 @@ export class CycleManager {
// Update display text // Update display text
if (this.cycleText) { if (this.cycleText) {
this.cycleText.setText(this.currentCycle.charAt(0).toUpperCase() + this.currentCycle.slice(1)); this.cycleText.setText('Time of Day: ' + this.currentCycle.charAt(0).toUpperCase() + this.currentCycle.slice(1));
} }
// Apply tint to the terrain layer // Apply tint to the terrain layer

126
src/inventory.js Normal file
View File

@ -0,0 +1,126 @@
export class InventoryManager {
constructor(scene) {
this.scene = scene;
this.isOpen = false;
this.inventory = {};
this.backpackIcon = null;
this.inventoryUI = null;
this.closeButton = null;
}
init() {
const cam = this.scene.cameras.main;
this.backpackIcon = this.scene.physics.add.image(cam.worldView.width-150, cam.worldView.height-150, 'backpack')
.setOrigin(0.5)
.setScale(.5)
.setScrollFactor(0)
.setDepth(99);
// Create inventory UI (hidden initially)
this.createInventoryUI();
// Add click event to toggle inventory
this.backpackIcon.setInteractive();
this.backpackIcon.on('pointerdown', () => {
console.log('hit');
this.toggleInventory();
});
}
createInventoryUI() {
const scene = this.scene;
// Create inventory background
this.inventoryUI = scene.add.rectangle(
scene.game.config.width / 2,
scene.game.config.height / 2,
400,
300,
0x2F4F4F
).setOrigin(0.5).setAlpha(0.9).setScrollFactor(0).setDepth(150);
// Create close button
this.closeButton = scene.add.rectangle(
scene.game.config.width / 2 + 180,
scene.game.config.height / 2 - 130,
30,
30,
0xFF4444
).setOrigin(0.5).setScrollFactor(0).setDepth(150);
// Add X to close button
this.closeButtonText = scene.add.text(
scene.game.config.width / 2 + 180,
scene.game.config.height / 2 - 130,
'X',
{ fontSize: '24px', fill: '#FFFFFF' }
).setOrigin(0.5).setScrollFactor(0).setDepth(150);
// Hide inventory UI initially
this.inventoryUI.setVisible(false);
this.closeButton.setVisible(false);
this.closeButtonText.setVisible(false);
// Add click event to close button
this.closeButton.setInteractive();
this.closeButton.on('pointerdown', () => {
this.toggleInventory();
});
}
toggleInventory() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.scene.player.stop();
this.scene.player.canMove = false;
// Show inventory UI and close button
this.inventoryUI.setVisible(true);
this.closeButton.setVisible(true);
this.closeButtonText.setVisible(true);
} else {
// Hide inventory UI and close button
this.inventoryUI.setVisible(false);
this.closeButton.setVisible(false);
this.closeButtonText.setVisible(false);
this.scene.time.delayedCall(200, () => {
this.scene.player.canMove = true;
});
}
}
addItem(item) {
const type = item.props.type;
const amount = item.props.amount;
if (type in this.inventory) {
this.inventory[type] += amount;
} else {
this.inventory[type] = amount;
}
this.scene.tweens.add({
targets: this.backpackIcon,
angle: 360,
delay: 500,
duration: 500
});
this.scene.tweens.add({
targets: this.backpackIcon,
scale: 0.8,
delay: 500,
yoyo: true,
ease: 'Bounce',
duration: 500
});
}
removeItem(item) {
const index = this.inventory.indexOf(item);
if (index > -1) {
this.inventory.splice(index, 1);
}
}
update(delta) {
// Inventory update logic can go here
}
}

View File

@ -2,6 +2,7 @@ export class ObjectManager {
constructor(scene) { constructor(scene) {
this.scene = scene; this.scene = scene;
this.objects = []; this.objects = [];
this.healthBars = new Map(); // Store references to health bars for cleanup
} }
init() { init() {
@ -37,58 +38,182 @@ export class ObjectManager {
const palmTree = this.scene.physics.add.sprite(x, y, 'objects', 0); const palmTree = this.scene.physics.add.sprite(x, y, 'objects', 0);
this.scene.objects.add(palmTree); this.scene.objects.add(palmTree);
palmTree.setImmovable(true).setSize(60,100); palmTree.setImmovable(true).setSize(60,100);
palmTree.props = {
type: 'palmTree',
health: 100,
fullHealth: 100,
yield: 'wood',
amount: [0,1],
doneSprite: 10
};
this.createHealthBar(palmTree);
} else if (key === 'forestTree' && rand <= objectType[key]) { } else if (key === 'forestTree' && rand <= objectType[key]) {
const forestRand = Phaser.Math.Between(1,4); const forestRand = Phaser.Math.Between(1,4);
const forestTree = this.scene.physics.add.sprite(x, y, 'objects', forestRand); const forestTree = this.scene.physics.add.sprite(x, y, 'objects', forestRand);
this.scene.objects.add(forestTree); this.scene.objects.add(forestTree);
forestTree.setImmovable(true).setSize(50,100); forestTree.setImmovable(true).setSize(50,100);
forestTree.props = {
type: 'forestTree',
health: 200,
fullHealth: 200,
yield: 'wood',
amount: [0,3],
doneSprite: 11
};
this.createHealthBar(forestTree);
} else if (key === 'boulder' && rand <= objectType[key]) { } else if (key === 'boulder' && rand <= objectType[key]) {
const boulder = this.scene.physics.add.sprite(x, y, 'objects', 20); const boulder = this.scene.physics.add.sprite(x, y, 'objects', 20);
this.scene.objects.add(boulder); this.scene.objects.add(boulder);
boulder.setImmovable(true); boulder.setImmovable(true);
boulder.props = {
type: 'boulder',
health: 200,
fullHealth: 200,
yield: 'rock',
amount: [1,3],
doneSprite: null
};
this.createHealthBar(boulder);
} }
}); });
} }
createEnemy(x, y, properties) { createHealthBar(object) {
// Placeholder for enemy creation logic // Create a graphics object for the health bar
console.log(`Creating enemy at (${x}, ${y}) with properties:`, properties); const healthBar = this.scene.add.graphics();
// Example implementation: // Position the health bar above the object (adjust offset as needed)
// const enemy = this.scene.physics.add.sprite( const offsetX = 0;
// x * tileWidth + tileWidth / 2, const offsetY = -50; // Adjust to position above the object
// y * tileHeight + tileHeight / 2,
// 'enemy-sprite' // Store reference to health bar for updates and cleanup
// ); object.healthBar = healthBar;
// this.objects.push(enemy);
// Add to our map for cleanup
this.healthBars.set(object, healthBar);
// Draw initial health bar (will be updated in update loop)
this.updateHealthBar(object, true);
} }
createCollectible(x, y, properties) { updateHealthBar(object, justCreated) {
// Placeholder for collectible creation logic if (!object.healthBar || !object.props) return;
console.log(`Creating collectible at (${x}, ${y}) with properties:`, properties);
// Example implementation: const health = object.props.health;
// const collectible = this.scene.physics.add.sprite( const fullHealth = object.props.fullHealth;
// x * tileWidth + tileWidth / 2,
// y * tileHeight + tileHeight / 2, // Calculate health percentage
// 'collectible-sprite' const healthPercentage = Math.max(0, Math.min(1, health / fullHealth));
// );
// this.objects.push(collectible); // Clear previous graphics
object.healthBar.clear();
// Health bar dimensions
const barWidth = 60;
const barHeight = 8;
const offsetX = -barWidth / 2; // Center the bar on object
const offsetY = -50; // Position above object
// Draw background (red) - full health bar
object.healthBar.fillStyle(0xff0000, 1);
object.healthBar.fillRect(object.x + offsetX, object.y + offsetY, barWidth, barHeight).setAlpha(1);
// Draw health fill (green) - current health
const greenWidth = barWidth * healthPercentage;
if (greenWidth > 0) {
object.healthBar.fillStyle(0x00ff00, 1);
object.healthBar.fillRect(object.x + offsetX, object.y + offsetY, greenWidth, barHeight).setAlpha(1);
}
if (justCreated) {
object.healthBar.setAlpha(0);
}
if (object.alpha > 0) {
this.scene.tweens.add({
targets: object.healthBar,
alpha: 0,
delay: 10000
});
}
if (healthPercentage <= 0) {
object.healthBar.destroy();
}
} }
createPlatform(x, y, properties) { takeDamage(object, player) {
// Placeholder for platform creation logic const dmg = Phaser.Math.Between(player.dmgMin,player.dmgMax);
console.log(`Creating platform at (${x}, ${y}) with properties:`, properties); if (player.targetX === object.x && player.targetY === object.y) {
switch (object.props.type){
case 'palmTree':
object.props.health -= dmg;
break;
case 'forestTree':
object.props.health -= dmg;
break;
default:
}
this.updateHealthBar(object);
if (object.props.health <= 0) {
this.destroyObject(object);
}
}
}
destroyObject(object) {
// Replace object with doneSprite
if (object.props.doneSprite !== null) {
const doneSprite = this.scene.physics.add.sprite(object.x, object.y, 'objects', object.props.doneSprite);
}
// Drop Resources
if (object.props.yield) {
const amount = Phaser.Math.Between(object.props.amount[0], object.props.amount[1]);
if (amount > 0) {
let sprite = null;
switch (object.props.yield) {
case 'wood':
if (amount === 3) {
sprite = 14;
} else if (amount === 2) {
sprite = 13;
} else {
sprite = 12;
}
break;
default:
}
const drop = this.scene.physics.add.sprite(object.x-5, object.y-50, 'objects', sprite).setScale(.8);
this.scene.tweens.add({
targets: drop,
scale: 1,
y: object.y,
x: object.x,
ease: 'Bounce',
duration: 800,
onComplete: () => {
drop.postFX.addGlow(0x00ffc6);
this.scene.items.add(drop);
drop.props = {
type: object.props.yield,
amount: amount
};
drop.setInteractive();
}
});
}
}
// Remove from scene and cleanup
object.destroy();
// Example implementation: // Remove health bar reference
// const platform = this.scene.physics.add.sprite( if (object.healthBar && this.healthBars.has(object)) {
// x * tileWidth + tileWidth / 2, this.healthBars.delete(object);
// y * tileHeight + tileHeight / 2, }
// 'platform-sprite'
// );
// platform.setImmovable(true);
// this.objects.push(platform);
} }
update(delta) { update(delta) {
@ -97,6 +222,11 @@ export class ObjectManager {
if (object.update) { if (object.update) {
object.update(delta); object.update(delta);
} }
// Update health bars for objects that have them
if (object.healthBar && object.props) {
this.updateHealthBar(object);
}
}); });
} }
} }

View File

@ -7,16 +7,23 @@ export class Player extends Phaser.GameObjects.Sprite {
scene.physics.world.enable(this); scene.physics.world.enable(this);
this.body.setCollideWorldBounds(true); this.body.setCollideWorldBounds(true);
this.body.setSize(50,100); this.body.setSize(50,100);
this.setDepth(100);
// Set player properties // Set player properties
this.speed = 200; this.speed = 200;
this.isMoving = false; this.isMoving = false;
this.targetX = x; this.targetX = x;
this.targetY = y; this.targetY = y;
this.dmgMin = 5;
this.dmgMax = 25;
this.inventory = {};
this.canMove = true;
// Add input listener for mouse clicks // Add input listener for mouse clicks
scene.input.on('pointerdown', (pointer) => { scene.input.on('pointerdown', (pointer) => {
this.moveToPoint(pointer.worldX, pointer.worldY); if (this.canMove) {
this.moveToPoint(pointer.worldX, pointer.worldY);
}
}); });
// Create animations // Create animations
@ -56,15 +63,15 @@ export class Player extends Phaser.GameObjects.Sprite {
moveToPoint(x, y) { moveToPoint(x, y) {
// Set target position // Set target position
this.targetX = x; this.targetX = Math.floor(x / 100) * 100 + 50;
this.targetY = y; this.targetY = Math.floor(y / 100) * 100 + 50;
// Calculate direction to target // Calculate direction to target
const dx = x - this.x; const dx = this.targetX - this.x;
const dy = y - this.y; const dy = this.targetY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 100) { // Only move if target is far enough if (distance > 50) { // Only move if target is far enough
this.isMoving = true; this.isMoving = true;
// Set velocity towards target // Set velocity towards target
@ -142,7 +149,7 @@ export class Player extends Phaser.GameObjects.Sprite {
// Method to stop player movement // Method to stop player movement
stop() { stop() {
this.setVelocity(0, 0); this.body.setVelocity(0, 0);
this.isMoving = false; this.isMoving = false;
// Play idle animation when stopping // Play idle animation when stopping

View File

@ -1,6 +1,7 @@
import { Player } from '../player.js'; import { Player } from '../player.js';
import { CycleManager } from '../cycle.js'; import { CycleManager } from '../cycle.js';
import { ObjectManager } from '../objects.js'; import { ObjectManager } from '../objects.js';
import { InventoryManager } from '../inventory.js';
export class Game extends Phaser.Scene { export class Game extends Phaser.Scene {
constructor() { constructor() {
@ -11,22 +12,20 @@ export class Game extends Phaser.Scene {
} }
preload() { preload() {
// Load player sprite this.load.tilemapTiledJSON('game-map', 'assets/game-map.json');
this.load.spritesheet('player', 'assets/images/player.png', { this.load.spritesheet('player', 'assets/images/player.png', {
frameWidth: 100, frameWidth: 100,
frameHeight: 100 frameHeight: 100
}); });
// Load objects sprite
this.load.spritesheet('objects', 'assets/images/objects.png', { this.load.spritesheet('objects', 'assets/images/objects.png', {
frameWidth: 100, frameWidth: 100,
frameHeight: 100 frameHeight: 100
}); });
// Load tilemap
this.load.tilemapTiledJSON('game-map', 'assets/game-map.json');
// Load tileset image
this.load.image('terrain-tileset', 'assets/images/terrain.png'); this.load.image('terrain-tileset', 'assets/images/terrain.png');
this.load.image('backpack', 'assets/images/backpack.png');
this.load.font('eraserDust', 'assets/fonts/EraserDust.ttf');
} }
create() { create() {
@ -47,15 +46,41 @@ export class Game extends Phaser.Scene {
// Initialize object manager // Initialize object manager
this.objects = this.physics.add.group(); this.objects = this.physics.add.group();
this.items = this.physics.add.group();
this.objectManager = new ObjectManager(this); this.objectManager = new ObjectManager(this);
this.objectManager.init(); this.objectManager.init();
// Create player at center of screen // Create player at center of screen
this.player = new Player(this, 1600, 3100); this.player = new Player(this, 1600, 3100);
// Initialize inventory manager
this.inventoryManager = new InventoryManager(this);
this.inventoryManager.init();
// Physics Collisions // Physics Collisions
this.physics.add.collider(this.player, this.mainLayer); this.physics.add.collider(this.player, this.mainLayer);
this.physics.add.collider(this.player, this.objects); this.physics.add.collider(this.player, this.objects, (player, object) => {
player.stop();
this.objectManager.takeDamage(object, player);
});
this.physics.add.collider(this.player, this.items, (player, item) => {
if (item.props.collected === true) {
return;
}
item.props.collected = true;
player.stop();
this.inventoryManager.addItem(item);
const cam = this.cameras.main.worldView;
this.tweens.add({
targets: item,
x: cam.x+cam.width-150,
y: cam.y+cam.height-150,
duration: 500,
onComplete: () => {
item.destroy();
}
});
});
// Make camera follow the player // Make camera follow the player
this.cameras.main.startFollow(this.player); this.cameras.main.startFollow(this.player);
@ -86,5 +111,9 @@ export class Game extends Phaser.Scene {
if (this.objectManager) { if (this.objectManager) {
this.objectManager.update(delta); this.objectManager.update(delta);
} }
if (this.inventoryManager) {
this.inventoryManager.update(delta);
}
} }
} }