Character sprite switching - directionality - added dragon head

This commit is contained in:
2026-01-31 10:58:12 +11:00
parent 58b3726d21
commit b18e2d08ba
17 changed files with 163 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -153,7 +153,7 @@ export const GAME_CONFIG = {
gameplay: {
energyThreshold: 100,
actionCost: 100,
flamethrower: {
ceramicDragonHead: {
range: 4,
initialDamage: 7,
burnDamage: 3,
@@ -175,8 +175,14 @@ export const GAME_CONFIG = {
],
images: [
{ key: "splash_bg", path: "assets/ui/splash_bg.png" },
{ key: "character_outline", path: "assets/ui/character_outline.png" }
{ key: "character_outline", path: "assets/ui/character_outline.png" },
{ key: "ceramic_dragon_head", path: "assets/sprites/items/ceramic_dragon_head.png" },
{ key: "PriestessNorth", path: "assets/sprites/priestess/PriestessNorth.png" },
{ key: "PriestessSouth", path: "assets/sprites/priestess/PriestessSouth.png" },
{ key: "PriestessEast", path: "assets/sprites/priestess/PriestessEast.png" },
{ key: "PriestessWest", path: "assets/sprites/priestess/PriestessWest.png" }
]
},
animations: [

View File

@@ -4,7 +4,7 @@ import type {
RangedWeaponItem,
ArmourItem,
AmmoItem,
FlamethrowerItem
CeramicDragonHeadItem
} from "../types";
import { GAME_CONFIG } from "../config/GameConfig";
@@ -246,15 +246,15 @@ export function createUpgradeScroll(quantity = 1): ConsumableItem {
};
}
export function createFlamethrower(): FlamethrowerItem {
const config = GAME_CONFIG.gameplay.flamethrower;
export function createCeramicDragonHead(): CeramicDragonHeadItem {
const config = GAME_CONFIG.gameplay.ceramicDragonHead;
return {
id: "flamethrower",
name: "Flamethrower",
id: "ceramic_dragon_head",
name: "Ceramic Dragon Head",
type: "Weapon",
weaponType: "flamethrower",
textureKey: "weapons",
spriteIndex: 5,
weaponType: "ceramic_dragon_head",
textureKey: "ceramic_dragon_head",
spriteIndex: 0,
charges: config.maxCharges,
maxCharges: config.maxCharges,
lastRechargeTurn: 0,

View File

@@ -112,9 +112,9 @@ export interface RangedWeaponItem extends BaseItem {
};
}
export interface FlamethrowerItem extends BaseItem {
export interface CeramicDragonHeadItem extends BaseItem {
type: "Weapon";
weaponType: "flamethrower";
weaponType: "ceramic_dragon_head";
charges: number;
maxCharges: number;
lastRechargeTurn: number;
@@ -124,7 +124,7 @@ export interface FlamethrowerItem extends BaseItem {
};
}
export type WeaponItem = MeleeWeaponItem | RangedWeaponItem | FlamethrowerItem;
export type WeaponItem = MeleeWeaponItem | RangedWeaponItem | CeramicDragonHeadItem;
export interface ArmourItem extends BaseItem {

View File

@@ -216,7 +216,7 @@ export const Prefabs = {
return EntityBuilder.create(world)
.withPosition(x, y)
.withName("Player")
.withSprite("warrior", 0)
.withSprite("PriestessSouth", 0)
.asPlayer()
.withStats(config.initialStats)
.withEnergy(config.speed)

View File

@@ -9,7 +9,7 @@ import {
createArmour,
createUpgradeScroll,
createAmmo,
createFlamethrower
createCeramicDragonHead
} from "../../core/config/Items";
import { seededRandom } from "../../core/math";
import * as ROT from "rot-js";
@@ -63,7 +63,7 @@ export function generateWorld(floor: number, runState: RunState): { world: World
createConsumable("throwing_dagger", 3),
createRangedWeapon("pistol"),
createAmmo("ammo_9mm", 10),
createFlamethrower(),
createCeramicDragonHead(),
createArmour("leather_armor", "heavy"),
createUpgradeScroll(2)
] : [])

View File

@@ -4,7 +4,7 @@ import { TileType } from "../core/terrain";
import { TILE_SIZE } from "../core/constants";
import { idx, isWall } from "../engine/world/world-logic";
import { GAME_CONFIG } from "../core/config/GameConfig";
import { ALL_TEMPLATES } from "../core/config/Items";
import { FovManager } from "./FovManager";
import { MinimapRenderer } from "./MinimapRenderer";
import { FxRenderer } from "./FxRenderer";
@@ -88,9 +88,10 @@ export class DungeonRenderer {
// Ensure player sprite exists
if (!this.playerSprite) {
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
this.playerSprite = this.scene.add.sprite(0, 0, "PriestessSouth");
this.playerSprite.setDepth(100);
this.playerSprite.play('warrior-idle');
this.playerSprite.setDisplaySize(TILE_SIZE, TILE_SIZE); // Ensure it fits in 1 tile
// No animation for simple sprites for now
}
this.minimapRenderer.positionMinimap();
@@ -288,6 +289,18 @@ export class DungeonRenderer {
const ty = a.pos.y * TILE_SIZE + TILE_SIZE / 2;
if (this.playerSprite.x !== tx || this.playerSprite.y !== ty) {
// Determine direction
const dx = tx - this.playerSprite.x;
const dy = ty - this.playerSprite.y;
if (Math.abs(dy) > Math.abs(dx)) {
if (dy < 0) this.playerSprite.setTexture("PriestessNorth");
else this.playerSprite.setTexture("PriestessSouth");
} else if (Math.abs(dx) > 0) {
if (dx > 0) this.playerSprite.setTexture("PriestessEast");
else this.playerSprite.setTexture("PriestessWest");
}
this.scene.tweens.add({
targets: this.playerSprite,
x: tx,
@@ -465,7 +478,7 @@ export class DungeonRenderer {
this.fxRenderer.showFloatingText(x, y, message, color);
}
showProjectile(from: Vec2, to: Vec2, itemId: string, onComplete: () => void) {
showProjectile(from: Vec2, to: Vec2, texture: string, frame: number, onComplete: () => void) {
// World coords
const startX = from.x * TILE_SIZE + TILE_SIZE / 2;
const startY = from.y * TILE_SIZE + TILE_SIZE / 2;
@@ -473,15 +486,22 @@ export class DungeonRenderer {
const endY = to.y * TILE_SIZE + TILE_SIZE / 2;
// Create sprite
// Look up sprite index from config
const itemConfig = ALL_TEMPLATES[itemId as keyof typeof ALL_TEMPLATES];
const texture = itemConfig?.textureKey ?? "items";
const frame = itemConfig?.spriteIndex ?? 0;
const isStandalone = frame === undefined || frame === 0;
const sprite = isStandalone
? this.scene.add.sprite(startX, startY, texture)
: this.scene.add.sprite(startX, startY, texture, frame);
// Scale for standalone 24x24 image should be 1.0 (or matching world scale)
// Other sprites are 16x16.
if (isStandalone) {
sprite.setDisplaySize(16, 16);
} else {
sprite.setScale(1.0);
}
// Use 'items' spritesheet
const sprite = this.scene.add.sprite(startX, startY, texture, frame);
sprite.setDepth(2000);
// Rotate?
const angle = Phaser.Math.Angle.Between(startX, startY, endX, endY);
sprite.setRotation(angle + Math.PI / 4); // Adjust for sprite orientation (diagonal usually)

View File

@@ -47,11 +47,11 @@ export class FxRenderer {
let textStr = amount.toString();
let color = "#ff3333";
let fontSize = "16px";
if (isCrit) {
textStr += "!";
color = "#ffff00";
fontSize = "22px";
textStr += "!";
color = "#ffff00";
fontSize = "22px";
}
const text = this.scene.add.text(screenX, screenY, textStr, {
@@ -63,19 +63,19 @@ export class FxRenderer {
}).setOrigin(0.5, 1).setDepth(200);
if (isBlock) {
const blockText = this.scene.add.text(screenX + 10, screenY - 10, "Blocked", {
fontSize: "10px",
color: "#888888",
fontStyle: "bold"
}).setOrigin(0, 1).setDepth(200);
this.scene.tweens.add({
targets: blockText,
y: screenY - 34,
alpha: 0,
duration: 800,
onComplete: () => blockText.destroy()
});
const blockText = this.scene.add.text(screenX + 10, screenY - 10, "Blocked", {
fontSize: "10px",
color: "#888888",
fontStyle: "bold"
}).setOrigin(0, 1).setDepth(200);
this.scene.tweens.add({
targets: blockText,
y: screenY - 34,
alpha: 0,
duration: 800,
onComplete: () => blockText.destroy()
});
}
this.scene.tweens.add({
@@ -132,7 +132,7 @@ export class FxRenderer {
}
spawnCorpse(x: number, y: number, type: ActorType) {
const textureKey = type === "player" ? "warrior" : type;
const textureKey = type === "player" ? "PriestessSouth" : type;
const corpse = this.scene.add.sprite(
x * TILE_SIZE + TILE_SIZE / 2,
@@ -141,7 +141,23 @@ export class FxRenderer {
0
);
corpse.setDepth(50);
corpse.play(`${textureKey}-die`);
// Use display size for Priestess sprites to match 1 tile
if (textureKey.startsWith("Priestess")) {
corpse.setDisplaySize(TILE_SIZE, TILE_SIZE);
} else {
corpse.setScale(1.0); // Reset to standard scale for spritesheet assets
}
// Only play animation if it's not a priestess sprite
if (!textureKey.startsWith("Priestess")) {
corpse.play(`${textureKey}-die`);
} else {
// Maybe rotate or fade for visual interest since there's no animation
corpse.setAngle(90);
}
this.corpseSprites.push({ sprite: corpse, x, y });
}

View File

@@ -8,7 +8,7 @@ import { ALL_VARIANTS, type ItemVariantId } from "../core/config/ItemVariants";
* inventory, quick slots, and world drops.
*/
export class ItemSpriteFactory {
/**
* Creates an item sprite with optional glow effect for variants.
* Returns a container with the glow (if applicable) and main sprite.
@@ -21,7 +21,7 @@ export class ItemSpriteFactory {
scale: number = 1
): Phaser.GameObjects.Container {
const container = scene.add.container(x, y);
// Create glow effect if item has a variant
if (item.variant) {
const glowColor = this.getGlowColor(item.variant as ItemVariantId);
@@ -30,21 +30,32 @@ export class ItemSpriteFactory {
container.add(glow);
}
}
// Create main item sprite
const sprite = scene.add.sprite(0, 0, item.textureKey, item.spriteIndex);
sprite.setScale(scale);
// Standalone images don't use frame indices
const isStandalone = item.spriteIndex === undefined || item.spriteIndex === 0;
const sprite = isStandalone
? scene.add.sprite(0, 0, item.textureKey)
: scene.add.sprite(0, 0, item.textureKey, item.spriteIndex);
if (isStandalone) {
sprite.setDisplaySize(16 * scale, 16 * scale);
} else {
sprite.setScale(scale);
}
container.add(sprite);
// Add upgrade level badge if item has been upgraded
if (item.upgradeLevel && item.upgradeLevel > 0) {
const badge = this.createUpgradeBadge(scene, item.upgradeLevel, scale);
container.add(badge);
}
return container;
}
/**
* Creates just a sprite (no container) for simpler use cases like drag icons.
* Does not include glow - use createItemSprite for full effect.
@@ -56,11 +67,21 @@ export class ItemSpriteFactory {
y: number,
scale: number = 1
): Phaser.GameObjects.Sprite {
const sprite = scene.add.sprite(x, y, item.textureKey, item.spriteIndex);
sprite.setScale(scale);
const isStandalone = item.spriteIndex === undefined || item.spriteIndex === 0;
const sprite = isStandalone
? scene.add.sprite(x, y, item.textureKey)
: scene.add.sprite(x, y, item.textureKey, item.spriteIndex);
if (isStandalone) {
sprite.setDisplaySize(16 * scale, 16 * scale);
} else {
sprite.setScale(scale);
}
return sprite;
}
/**
* Creates a soft glow effect behind the item using graphics.
* Uses a radial gradient-like effect with multiple circles.
@@ -72,26 +93,26 @@ export class ItemSpriteFactory {
color: number
): Phaser.GameObjects.Graphics {
const glow = scene.add.graphics();
// Base size for the glow (16x16 sprite scaled)
const baseSize = 16 * scale;
const glowRadius = baseSize * 0.8;
// Extract RGB from hex color
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = color & 0xff;
// Draw multiple circles with decreasing alpha for soft glow effect
const layers = 5;
for (let i = layers; i >= 1; i--) {
const layerRadius = glowRadius * (i / layers) * 1.2;
const layerAlpha = 0.15 * (1 - (i - 1) / layers);
glow.fillStyle(Phaser.Display.Color.GetColor(r, g, b), layerAlpha);
glow.fillCircle(0, 0, layerRadius);
}
// Add pulsing animation to the glow
scene.tweens.add({
targets: glow,
@@ -103,10 +124,10 @@ export class ItemSpriteFactory {
repeat: -1,
ease: 'Sine.easeInOut'
});
return glow;
}
/**
* Gets the glow color for a variant.
*/
@@ -114,7 +135,7 @@ export class ItemSpriteFactory {
const variant = ALL_VARIANTS[variantId];
return variant?.glowColor ?? null;
}
/**
* Creates a badge displaying the upgrade level (e.g., "+1").
*/
@@ -125,7 +146,7 @@ export class ItemSpriteFactory {
): Phaser.GameObjects.Text {
// Position at top-right corner, slightly inset
const offset = 5 * scale;
// Level text with strong outline for readability without background
const text = scene.add.text(offset, -offset, `+${level}`, {
fontSize: `${9 * scale}px`,
@@ -136,7 +157,7 @@ export class ItemSpriteFactory {
strokeThickness: 3
});
text.setOrigin(0.5);
return text;
}

View File

@@ -16,6 +16,8 @@ import { generateWorld } from "../engine/world/generator";
import { DungeonRenderer } from "../rendering/DungeonRenderer";
import { Prefabs } from "../engine/ecs/Prefabs";
import { GAME_CONFIG } from "../core/config/GameConfig";
import { ALL_TEMPLATES } from "../core/config/Items";
import { EntityAccessor } from "../engine/EntityAccessor";
import { ProgressionManager } from "../engine/ProgressionManager";
import GameUI from "../ui/GameUI";
@@ -311,13 +313,13 @@ export class GameScene extends Phaser.Scene {
player.stats.mana += regenAmount;
}
// Flamethrower Recharge logic
// Ceramic Dragon Head Recharge logic
if (player && player.inventory) {
for (const item of player.inventory.items) {
if (item.type === "Weapon" && item.weaponType === "flamethrower") {
if (item.type === "Weapon" && item.weaponType === "ceramic_dragon_head") {
if (item.charges < item.maxCharges) {
const turnsSinceLast = this.turnCount - item.lastRechargeTurn;
if (turnsSinceLast >= GAME_CONFIG.gameplay.flamethrower.rechargeTurns) {
if (turnsSinceLast >= GAME_CONFIG.gameplay.ceramicDragonHead.rechargeTurns) {
item.charges++;
item.lastRechargeTurn = this.turnCount;
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "+Charge", "#ff6600");
@@ -470,10 +472,16 @@ export class GameScene extends Phaser.Scene {
const player = this.entityAccessor.getPlayer();
if (!player) return;
// Projectile Visuals
let projectileId = item.id;
let projectileTexture = item.textureKey;
let projectileFrame = item.spriteIndex;
if (item.type === "Weapon" && item.weaponType === "ranged") {
projectileId = `ammo_${item.stats.ammoType}`; // Show ammo sprite
const ammoId = `ammo_${item.stats.ammoType}`;
const ammoTemplate = ALL_TEMPLATES[ammoId as keyof typeof ALL_TEMPLATES];
if (ammoTemplate) {
projectileTexture = ammoTemplate.textureKey;
projectileFrame = ammoTemplate.spriteIndex;
}
// Consume Ammo
if (item.currentAmmo > 0) {
@@ -486,14 +494,16 @@ export class GameScene extends Phaser.Scene {
this.dungeonRenderer.showProjectile(
player.pos,
blockedPos,
projectileId,
projectileTexture,
projectileFrame ?? 0,
() => {
// Handle Flamethrower specific impact
if (item.type === "Weapon" && item.weaponType === "flamethrower") {
// Handle Ceramic Dragon Head specific impact
if (item.type === "Weapon" && item.weaponType === "ceramic_dragon_head") {
item.charges--;
item.lastRechargeTurn = this.turnCount; // Prevent immediate recharge if turn logic is before/after
const config = GAME_CONFIG.gameplay.flamethrower;
const config = GAME_CONFIG.gameplay.ceramicDragonHead;
const targetTiles = getConeTiles(player.pos, blockedPos, config.range);
for (const tile of targetTiles) {

View File

@@ -95,7 +95,7 @@ export class GameEventHandler {
const item = player.inventory.items[itemIdx];
// Ranged Weapon Logic
if (item.type === "Weapon" && (item.weaponType === "ranged" || item.weaponType === "flamethrower")) {
if (item.type === "Weapon" && (item.weaponType === "ranged" || item.weaponType === "ceramic_dragon_head")) {
if (item.weaponType === "ranged") {
// Check Ammo
if (item.currentAmmo <= 0) {
@@ -114,7 +114,7 @@ export class GameEventHandler {
this.scene.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloading...", "#aaaaaa");
return;
}
} else if (item.weaponType === "flamethrower") {
} else if (item.weaponType === "ceramic_dragon_head") {
// Check Charges
if (item.charges <= 0) {
this.scene.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No charges!", "#ff6600");

View File

@@ -112,7 +112,7 @@ export class TargetingSystem {
const start = player.pos;
const end = { x: this.cursor.x, y: this.cursor.y };
if (item.type === "Weapon" && item.weaponType === "flamethrower") {
if (item.type === "Weapon" && item.weaponType === "ceramic_dragon_head") {
if (item.charges <= 0) {
console.log("No charges left!");
return false;
@@ -206,10 +206,10 @@ export class TargetingSystem {
finalEndX = bPos.x * TILE_SIZE + TILE_SIZE / 2;
finalEndY = bPos.y * TILE_SIZE + TILE_SIZE / 2;
// Draw Cone if it's a flamethrower
// Draw Cone if it's a ceramic dragon head
const player = this.accessor.getCombatant(this.playerId);
const item = player?.inventory?.items.find(it => it.id === this.targetingItemId);
if (item?.type === "Weapon" && item.weaponType === "flamethrower") {
if (item?.type === "Weapon" && item.weaponType === "ceramic_dragon_head") {
const range = item.stats.range;
const tiles = getConeTiles(playerPos, this.cursor, range);

View File

@@ -8,7 +8,7 @@ export class QuickSlotComponent {
private container!: Phaser.GameObjects.Container;
private slots: Phaser.GameObjects.Container[] = [];
private itemMap: (Item | null)[] = new Array(10).fill(null);
private assignedIds: string[] = ["health_potion", "pistol", "throwing_dagger", "flamethrower", ...new Array(6).fill("")];
private assignedIds: string[] = ["health_potion", "pistol", "throwing_dagger", "ceramic_dragon_head", ...new Array(6).fill("")];
private draggedSlotIndex: number | null = null;
private dragIcon: Phaser.GameObjects.Sprite | null = null;
private reloadSliderContainer!: Phaser.GameObjects.Container;
@@ -231,11 +231,16 @@ export class QuickSlotComponent {
if (foundItem) {
// Use ItemSpriteFactory for glow effect on variants
// Standalone images (24x24) need less scaling than 16x16 sprites
const isStandalone = foundItem.spriteIndex === undefined || foundItem.spriteIndex === 0;
const itemScale = isStandalone ? 1.5 : 2.5;
const itemContainer = ItemSpriteFactory.createItemSprite(
this.scene, foundItem, slotSize / 2, slotSize / 2, 2.5
this.scene, foundItem, slotSize / 2, slotSize / 2, itemScale
);
slot.add(itemContainer);
// Unified Label (Bottom-Right)
let labelText = "";
if (foundItem.stackable) {
@@ -247,8 +252,8 @@ export class QuickSlotComponent {
} else if (foundItem.type === "Weapon" && foundItem.weaponType === "ranged" && foundItem.stats) {
// Show ammo for non-stackable ranged weapons
labelText = `${foundItem.currentAmmo}/${foundItem.stats.magazineSize}`;
} else if (foundItem.type === "Weapon" && foundItem.weaponType === "flamethrower") {
// Show charges for flamethrower
} else if (foundItem.type === "Weapon" && foundItem.weaponType === "ceramic_dragon_head") {
// Show charges for ceramic dragon head
labelText = `${foundItem.charges}/${foundItem.maxCharges}`;
}