Compare commits
No commits in common. "23575dd5165c25936b2bda50f1c3f00d796d893c" and "f7a6154c685cfb825cc4386328c98b9ec7896c05" have entirely different histories.
23575dd516
...
f7a6154c68
|
|
@ -2,10 +2,6 @@
|
||||||
export { DisposableBag } from './utils';
|
export { DisposableBag } from './utils';
|
||||||
export type { IDisposable, DisposableItem } from './utils';
|
export type { IDisposable, DisposableItem } from './utils';
|
||||||
|
|
||||||
// Drag & drop utilities
|
|
||||||
export { dragDropEventEffect, DragDropEventType } from './utils';
|
|
||||||
export type { DragDropEvent } from './utils';
|
|
||||||
|
|
||||||
// Data-driven object spawning
|
// Data-driven object spawning
|
||||||
export { spawnEffect } from './spawner';
|
export { spawnEffect } from './spawner';
|
||||||
export type { Spawner } from './spawner';
|
export type { Spawner } from './spawner';
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
type PointerRecord = {
|
|
||||||
id: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DragDropEventType {
|
|
||||||
DOWN,
|
|
||||||
UP,
|
|
||||||
MOVE,
|
|
||||||
}
|
|
||||||
export type DragDropEvent = {
|
|
||||||
type: DragDropEventType,
|
|
||||||
relativeX: number;
|
|
||||||
relativeY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dragDropEventEffect(
|
|
||||||
gameObject: Phaser.GameObjects.GameObject,
|
|
||||||
) {
|
|
||||||
let down: PointerRecord | null;
|
|
||||||
let up: PointerRecord | null;
|
|
||||||
|
|
||||||
function onPointerDown(pointer: Phaser.Input.Pointer) {
|
|
||||||
down = {
|
|
||||||
id: pointer.id,
|
|
||||||
x: pointer.x,
|
|
||||||
y: pointer.y
|
|
||||||
}
|
|
||||||
up = null;
|
|
||||||
|
|
||||||
const type = DragDropEventType.DOWN;
|
|
||||||
const relativeX = pointer.x - down.x;
|
|
||||||
const relativeY = pointer.y - down.y;
|
|
||||||
gameObject.emit('drag', {type, relativeX, relativeY});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerUp(pointer: Phaser.Input.Pointer) {
|
|
||||||
if(!down) return;
|
|
||||||
up = {
|
|
||||||
id: pointer.id,
|
|
||||||
x: pointer.x,
|
|
||||||
y: pointer.y
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = DragDropEventType.UP;
|
|
||||||
const relativeX = pointer.x - down.x;
|
|
||||||
const relativeY = pointer.y - down.y;
|
|
||||||
gameObject.emit('drag', {type, relativeX, relativeY});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerMove(pointer: Phaser.Input.Pointer) {
|
|
||||||
if(!down || up) return;
|
|
||||||
if(down.id !== pointer.id) return;
|
|
||||||
|
|
||||||
const type = DragDropEventType.MOVE;
|
|
||||||
const relativeX = pointer.x - down.x;
|
|
||||||
const relativeY = pointer.y - down.y;
|
|
||||||
gameObject.emit('drag', {type, relativeX, relativeY});
|
|
||||||
}
|
|
||||||
|
|
||||||
gameObject.on('pointerdown', onPointerDown);
|
|
||||||
gameObject.on('pointerup', onPointerUp);
|
|
||||||
gameObject.scene.input.on('pointermove', onPointerMove);
|
|
||||||
return function () {
|
|
||||||
gameObject.off('pointerdown', onPointerDown);
|
|
||||||
gameObject.off('pointerup', onPointerUp);
|
|
||||||
gameObject.scene.input.off('pointermove', onPointerMove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export { DisposableBag } from './disposable';
|
export { DisposableBag } from './disposable';
|
||||||
export type { IDisposable, DisposableItem } from './disposable';
|
export type { IDisposable, DisposableItem } from './disposable';
|
||||||
export { dragDropEventEffect, DragDropEventType, type DragDropEvent } from './dnd';
|
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,26 @@ import { ReactiveScene } from 'boardgame-phaser';
|
||||||
import { MutableSignal } from 'boardgame-core';
|
import { MutableSignal } from 'boardgame-core';
|
||||||
import {
|
import {
|
||||||
resolveEncounter,
|
resolveEncounter,
|
||||||
removeItem,
|
|
||||||
type RunState,
|
type RunState,
|
||||||
type EncounterResult,
|
type EncounterResult,
|
||||||
type MapNodeType,
|
type MapNodeType,
|
||||||
|
type InventoryItem,
|
||||||
|
type GameItemMeta,
|
||||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
} from 'boardgame-core/samples/slay-the-spire-like';
|
||||||
import { InventoryWidget } from '@/widgets/InventoryWidget';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 占位符遭遇场景
|
* 占位符遭遇场景
|
||||||
*
|
*
|
||||||
* 左侧显示背包网格(使用 InventoryWidget),右侧显示遭遇信息。
|
* 左侧显示背包网格(80x80 每格),右侧显示遭遇信息。
|
||||||
*/
|
*/
|
||||||
export class PlaceholderEncounterScene extends ReactiveScene {
|
export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
private gameState: MutableSignal<RunState>;
|
private gameState: MutableSignal<RunState>;
|
||||||
private inventoryWidget: InventoryWidget | null = null;
|
|
||||||
|
// Grid constants
|
||||||
|
private readonly CELL_SIZE = 80;
|
||||||
|
private readonly GRID_GAP = 2;
|
||||||
|
private gridX = 0;
|
||||||
|
private gridY = 0;
|
||||||
|
|
||||||
constructor(gameState: MutableSignal<RunState>) {
|
constructor(gameState: MutableSignal<RunState>) {
|
||||||
super('PlaceholderEncounterScene');
|
super('PlaceholderEncounterScene');
|
||||||
|
|
@ -29,37 +34,24 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
const state = this.gameState.value;
|
const state = this.gameState.value;
|
||||||
|
|
||||||
const gridCols = state.inventory.width;
|
// ── Layout: split screen into left (grid) and right (encounter) ──
|
||||||
const gridRows = state.inventory.height;
|
const gridCols = state.inventory.width; // 6
|
||||||
const cellSize = 80;
|
const gridRows = state.inventory.height; // 4
|
||||||
const gridW = gridCols * cellSize + (gridCols - 1) * 2;
|
const gridW = gridCols * this.CELL_SIZE;
|
||||||
const gridH = gridRows * cellSize + (gridRows - 1) * 2;
|
const gridH = gridRows * this.CELL_SIZE;
|
||||||
const leftPanelW = gridW + 40;
|
const leftPanelW = gridW + 40; // panel padding
|
||||||
|
|
||||||
this.inventoryWidget = new InventoryWidget({
|
this.gridX = 60;
|
||||||
scene: this,
|
this.gridY = (height - gridH) / 2 + 20;
|
||||||
gameState: this.gameState,
|
|
||||||
x: 60,
|
|
||||||
y: (height - gridH) / 2 + 20,
|
|
||||||
cellSize,
|
|
||||||
gridGap: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Ensure camera shows the full grid
|
||||||
this.cameras.main.setBounds(0, 0, width, height);
|
this.cameras.main.setBounds(0, 0, width, height);
|
||||||
this.cameras.main.setScroll(0, 0);
|
this.cameras.main.setScroll(0, 0);
|
||||||
|
|
||||||
// Panel background
|
// ── LEFT PANEL: inventory grid ──
|
||||||
this.add.rectangle(
|
this.drawLeftPanel(leftPanelW, gridW, gridH);
|
||||||
60 + leftPanelW / 2, this.inventoryWidgetY(gridH),
|
|
||||||
leftPanelW + 10, gridH + 50,
|
|
||||||
0x111122, 0.9
|
|
||||||
).setStrokeStyle(2, 0x5555aa);
|
|
||||||
|
|
||||||
// "背包" title
|
|
||||||
this.add.text(60 + gridW / 2, (height - gridH) / 2, '背包', {
|
|
||||||
fontSize: '22px', color: '#ffffff', fontStyle: 'bold',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
|
|
||||||
|
// ── RIGHT PANEL: encounter info ──
|
||||||
const node = state.map.nodes.get(state.currentNodeId);
|
const node = state.map.nodes.get(state.currentNodeId);
|
||||||
if (!node || !node.encounter) {
|
if (!node || !node.encounter) {
|
||||||
const rightX = leftPanelW + 80;
|
const rightX = leftPanelW + 80;
|
||||||
|
|
@ -72,10 +64,79 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
this.drawRightPanel(node, leftPanelW, width, height);
|
this.drawRightPanel(node, leftPanelW, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private inventoryWidgetY(gridH: number): number {
|
// ───────────────────── LEFT PANEL ─────────────────────
|
||||||
const { height } = this.scale;
|
|
||||||
return (height - gridH) / 2 + 20 + gridH / 2;
|
private drawLeftPanel(panelW: number, gridW: number, gridH: number): void {
|
||||||
|
// Panel background
|
||||||
|
this.add.rectangle(
|
||||||
|
this.gridX + panelW / 2, this.gridY + gridH / 2,
|
||||||
|
panelW + 10, gridH + 50,
|
||||||
|
0x111122, 0.9
|
||||||
|
).setStrokeStyle(2, 0x5555aa);
|
||||||
|
|
||||||
|
// "背包" title
|
||||||
|
this.add.text(this.gridX + gridW / 2, this.gridY - 20, '背包', {
|
||||||
|
fontSize: '22px', color: '#ffffff', fontStyle: 'bold',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
const graphics = this.add.graphics();
|
||||||
|
|
||||||
|
// Draw empty cell backgrounds
|
||||||
|
for (let y = 0; y < 4; y++) {
|
||||||
|
for (let x = 0; x < 6; x++) {
|
||||||
|
const px = this.gridX + x * this.CELL_SIZE;
|
||||||
|
const py = this.gridY + y * this.CELL_SIZE;
|
||||||
|
|
||||||
|
// Dark cell fill
|
||||||
|
graphics.fillStyle(0x1a1a2e);
|
||||||
|
graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
|
||||||
|
|
||||||
|
// Cell border
|
||||||
|
graphics.lineStyle(2, 0x444477);
|
||||||
|
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw items on top
|
||||||
|
this.drawItems(graphics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawItems(graphics: Phaser.GameObjects.Graphics): void {
|
||||||
|
const state = this.gameState.value;
|
||||||
|
const palette = [0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, 0xff6633];
|
||||||
|
const colorMap = new Map<string, number>();
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
for (const [id, item] of state.inventory.items) {
|
||||||
|
const color = colorMap.get(id) ?? palette[idx++ % palette.length];
|
||||||
|
colorMap.set(id, color);
|
||||||
|
|
||||||
|
const cells = this.getItemCells(item);
|
||||||
|
if (cells.length === 0) continue;
|
||||||
|
|
||||||
|
// Filled cells
|
||||||
|
for (const c of cells) {
|
||||||
|
const px = this.gridX + c.x * this.CELL_SIZE;
|
||||||
|
const py = this.gridY + c.y * this.CELL_SIZE;
|
||||||
|
|
||||||
|
graphics.fillStyle(color);
|
||||||
|
graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
|
||||||
|
graphics.lineStyle(2, 0xffffff);
|
||||||
|
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item name
|
||||||
|
const first = cells[0];
|
||||||
|
const name = item.meta?.itemData.name ?? item.id;
|
||||||
|
this.add.text(
|
||||||
|
this.gridX + first.x * this.CELL_SIZE + this.CELL_SIZE / 2,
|
||||||
|
this.gridY + first.y * this.CELL_SIZE + this.CELL_SIZE / 2,
|
||||||
|
name, { fontSize: '12px', color: '#fff', fontStyle: 'bold' }
|
||||||
|
).setOrigin(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────── RIGHT PANEL ─────────────────────
|
||||||
|
|
||||||
private drawRightPanel(node: any, leftPanelW: number, width: number, height: number): void {
|
private drawRightPanel(node: any, leftPanelW: number, width: number, height: number): void {
|
||||||
const encounter = {
|
const encounter = {
|
||||||
|
|
@ -90,10 +151,12 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
const cx = rightX + rightW / 2;
|
const cx = rightX + rightW / 2;
|
||||||
const cy = height / 2;
|
const cy = height / 2;
|
||||||
|
|
||||||
|
// Title
|
||||||
this.add.text(cx, cy - 180, '遭遇', {
|
this.add.text(cx, cy - 180, '遭遇', {
|
||||||
fontSize: '36px', color: '#fff', fontStyle: 'bold',
|
fontSize: '36px', color: '#fff', fontStyle: 'bold',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Type badge
|
||||||
const typeLabel = this.getTypeLabel(encounter.type);
|
const typeLabel = this.getTypeLabel(encounter.type);
|
||||||
const badgeColor = this.getTypeColor(encounter.type);
|
const badgeColor = this.getTypeColor(encounter.type);
|
||||||
this.add.rectangle(cx, cy - 110, 140, 40, badgeColor);
|
this.add.rectangle(cx, cy - 110, 140, 40, badgeColor);
|
||||||
|
|
@ -101,23 +164,28 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
fontSize: '18px', color: '#fff', fontStyle: 'bold',
|
fontSize: '18px', color: '#fff', fontStyle: 'bold',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Name
|
||||||
this.add.text(cx, cy - 50, encounter.name, {
|
this.add.text(cx, cy - 50, encounter.name, {
|
||||||
fontSize: '28px', color: '#fff',
|
fontSize: '28px', color: '#fff',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Description
|
||||||
this.add.text(cx, cy + 10, encounter.description || '(暂无描述)', {
|
this.add.text(cx, cy + 10, encounter.description || '(暂无描述)', {
|
||||||
fontSize: '18px', color: '#bbb',
|
fontSize: '18px', color: '#bbb',
|
||||||
wordWrap: { width: rightW - 40 }, align: 'center',
|
wordWrap: { width: rightW - 40 }, align: 'center',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Node id
|
||||||
this.add.text(cx, cy + 80, `节点: ${nodeId}`, {
|
this.add.text(cx, cy + 80, `节点: ${nodeId}`, {
|
||||||
fontSize: '14px', color: '#666',
|
fontSize: '14px', color: '#666',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Placeholder notice
|
||||||
this.add.text(cx, cy + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
|
this.add.text(cx, cy + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
|
||||||
fontSize: '14px', color: '#ff8844', fontStyle: 'italic',
|
fontSize: '14px', color: '#ff8844', fontStyle: 'italic',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
this.createButton('完成遭遇', cx, cy + 200, 220, 50, async () => {
|
this.createButton('完成遭遇', cx, cy + 200, 220, 50, async () => {
|
||||||
await this.completeEncounter();
|
await this.completeEncounter();
|
||||||
});
|
});
|
||||||
|
|
@ -126,6 +194,21 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ───────────────────── Helpers ─────────────────────
|
||||||
|
|
||||||
|
private getItemCells(item: InventoryItem<GameItemMeta>): { x: number; y: number }[] {
|
||||||
|
const cells: { x: number; y: number }[] = [];
|
||||||
|
const { offset } = item.transform;
|
||||||
|
for (let y = 0; y < item.shape.height; y++) {
|
||||||
|
for (let x = 0; x < item.shape.width; x++) {
|
||||||
|
if (item.shape.grid[y]?.[x]) {
|
||||||
|
cells.push({ x: x + offset.x, y: y + offset.y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
private getTypeLabel(type: MapNodeType): string {
|
private getTypeLabel(type: MapNodeType): string {
|
||||||
const m: Record<MapNodeType, string> = {
|
const m: Record<MapNodeType, string> = {
|
||||||
start: '起点', end: '终点', minion: '战斗', elite: '精英战斗',
|
start: '起点', end: '终点', minion: '战斗', elite: '精英战斗',
|
||||||
|
|
@ -157,15 +240,6 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
const node = state.map.nodes.get(state.currentNodeId);
|
const node = state.map.nodes.get(state.currentNodeId);
|
||||||
if (!node || !node.encounter) return;
|
if (!node || !node.encounter) return;
|
||||||
|
|
||||||
// Clear lost items from inventory
|
|
||||||
if (this.inventoryWidget) {
|
|
||||||
const lostIds = this.inventoryWidget.getLostItems();
|
|
||||||
for (const lostId of lostIds) {
|
|
||||||
removeItem(state, lostId);
|
|
||||||
}
|
|
||||||
this.inventoryWidget.clearLostItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
||||||
resolveEncounter(state, result);
|
resolveEncounter(state, result);
|
||||||
await this.sceneController.launch('GameFlowScene');
|
await this.sceneController.launch('GameFlowScene');
|
||||||
|
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
import Phaser from 'phaser';
|
|
||||||
import type { CombatEntity, EnemyEntity, EffectTable } from 'boardgame-core/samples/slay-the-spire-like';
|
|
||||||
|
|
||||||
export interface CombatUnitWidgetOptions {
|
|
||||||
scene: Phaser.Scene;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
entity: CombatEntity;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HP_BAR_WIDTH = 180;
|
|
||||||
const HP_BAR_HEIGHT = 16;
|
|
||||||
const BUFF_ICON_SIZE = 28;
|
|
||||||
const BUFF_ICON_GAP = 6;
|
|
||||||
|
|
||||||
const POSITIVE_EFFECTS = new Set(['block', 'strength', 'dexterity', 'regen']);
|
|
||||||
const NEGATIVE_EFFECTS = new Set(['weak', 'vulnerable', 'frail', 'poison']);
|
|
||||||
|
|
||||||
export class CombatUnitWidget {
|
|
||||||
private scene: Phaser.Scene;
|
|
||||||
private container: Phaser.GameObjects.Container;
|
|
||||||
private entity: CombatEntity;
|
|
||||||
private width: number;
|
|
||||||
private height: number;
|
|
||||||
|
|
||||||
private nameText!: Phaser.GameObjects.Text;
|
|
||||||
private hpBarBg!: Phaser.GameObjects.Graphics;
|
|
||||||
private hpBarFill!: Phaser.GameObjects.Graphics;
|
|
||||||
private hpText!: Phaser.GameObjects.Text;
|
|
||||||
private buffContainer!: Phaser.GameObjects.Container;
|
|
||||||
private buffIcons: Phaser.GameObjects.Container[] = [];
|
|
||||||
|
|
||||||
constructor(options: CombatUnitWidgetOptions) {
|
|
||||||
this.scene = options.scene;
|
|
||||||
this.entity = options.entity;
|
|
||||||
this.width = options.width ?? 240;
|
|
||||||
this.height = options.height ?? 120;
|
|
||||||
|
|
||||||
this.container = this.scene.add.container(options.x, options.y);
|
|
||||||
this.container.setSize(this.width, this.height);
|
|
||||||
|
|
||||||
this.drawBackground();
|
|
||||||
this.drawName();
|
|
||||||
this.drawHpBar();
|
|
||||||
this.drawBuffs();
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawBackground(): void {
|
|
||||||
const bg = this.scene.add.rectangle(0, 0, this.width, this.height, 0x1a1a2e, 0.9)
|
|
||||||
.setStrokeStyle(2, 0x444477)
|
|
||||||
.setOrigin(0, 0);
|
|
||||||
this.container.add(bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawName(): void {
|
|
||||||
const entityName = 'enemy' in this.entity
|
|
||||||
? (this.entity as EnemyEntity).enemy.name
|
|
||||||
: 'Player';
|
|
||||||
|
|
||||||
this.nameText = this.scene.add.text(this.width / 2, 12, entityName, {
|
|
||||||
fontSize: '16px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontStyle: 'bold',
|
|
||||||
}).setOrigin(0.5, 0);
|
|
||||||
this.container.add(this.nameText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawHpBar(): void {
|
|
||||||
const barX = (this.width - HP_BAR_WIDTH) / 2;
|
|
||||||
const barY = 36;
|
|
||||||
|
|
||||||
this.hpBarBg = this.scene.add.graphics();
|
|
||||||
this.hpBarBg.fillStyle(0x333333);
|
|
||||||
this.hpBarBg.fillRoundedRect(barX, barY, HP_BAR_WIDTH, HP_BAR_HEIGHT, 4);
|
|
||||||
this.hpBarBg.lineStyle(1, 0x666666);
|
|
||||||
this.hpBarBg.strokeRoundedRect(barX, barY, HP_BAR_WIDTH, HP_BAR_HEIGHT, 4);
|
|
||||||
this.container.add(this.hpBarBg);
|
|
||||||
|
|
||||||
this.hpBarFill = this.scene.add.graphics();
|
|
||||||
this.container.add(this.hpBarFill);
|
|
||||||
|
|
||||||
this.hpText = this.scene.add.text(this.width / 2, barY + HP_BAR_HEIGHT / 2, '', {
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontStyle: 'bold',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
this.container.add(this.hpText);
|
|
||||||
|
|
||||||
this.updateHpBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateHpBar(): void {
|
|
||||||
const barX = (this.width - HP_BAR_WIDTH) / 2;
|
|
||||||
const barY = 36;
|
|
||||||
const ratio = Math.max(0, this.entity.hp / this.entity.maxHp);
|
|
||||||
|
|
||||||
this.hpBarFill.clear();
|
|
||||||
|
|
||||||
let fillColor: number;
|
|
||||||
if (ratio > 0.6) fillColor = 0x44aa44;
|
|
||||||
else if (ratio > 0.3) fillColor = 0xccaa44;
|
|
||||||
else fillColor = 0xcc4444;
|
|
||||||
|
|
||||||
const fillWidth = Math.max(0, (HP_BAR_WIDTH - 2) * ratio);
|
|
||||||
if (fillWidth > 0) {
|
|
||||||
this.hpBarFill.fillStyle(fillColor);
|
|
||||||
this.hpBarFill.fillRoundedRect(barX + 1, barY + 1, fillWidth, HP_BAR_HEIGHT - 2, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hpText.setText(`${this.entity.hp}/${this.entity.maxHp}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawBuffs(): void {
|
|
||||||
this.buffContainer = this.scene.add.container(10, 62);
|
|
||||||
this.container.add(this.buffContainer);
|
|
||||||
this.refreshBuffs();
|
|
||||||
}
|
|
||||||
|
|
||||||
private refreshBuffs(): void {
|
|
||||||
for (const icon of this.buffIcons) {
|
|
||||||
icon.destroy();
|
|
||||||
}
|
|
||||||
this.buffIcons = [];
|
|
||||||
|
|
||||||
const effects = this.entity.effects;
|
|
||||||
let x = 0;
|
|
||||||
const y = 0;
|
|
||||||
|
|
||||||
for (const [effectId, entry] of Object.entries(effects)) {
|
|
||||||
if (entry.stacks <= 0) continue;
|
|
||||||
|
|
||||||
const icon = this.createBuffIcon(effectId, entry);
|
|
||||||
icon.setPosition(x, y);
|
|
||||||
this.buffContainer.add(icon);
|
|
||||||
this.buffIcons.push(icon);
|
|
||||||
|
|
||||||
x += BUFF_ICON_SIZE + BUFF_ICON_GAP;
|
|
||||||
|
|
||||||
if (x + BUFF_ICON_SIZE > this.width - 20) {
|
|
||||||
x = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBuffIcon(effectId: string, entry: { data: { name: string; description: string }; stacks: number }): Phaser.GameObjects.Container {
|
|
||||||
const icon = this.scene.add.container(0, 0);
|
|
||||||
|
|
||||||
const isPositive = POSITIVE_EFFECTS.has(effectId.toLowerCase());
|
|
||||||
const isNegative = NEGATIVE_EFFECTS.has(effectId.toLowerCase());
|
|
||||||
const bgColor = isPositive ? 0x226644 : isNegative ? 0x662222 : 0x444466;
|
|
||||||
const borderColor = isPositive ? 0x44aa88 : isNegative ? 0xaa4444 : 0x7777aa;
|
|
||||||
|
|
||||||
const bg = this.scene.add.rectangle(0, 0, BUFF_ICON_SIZE, BUFF_ICON_SIZE, bgColor, 1)
|
|
||||||
.setStrokeStyle(2, borderColor)
|
|
||||||
.setOrigin(0, 0);
|
|
||||||
icon.add(bg);
|
|
||||||
|
|
||||||
const label = this.getEffectLabel(effectId);
|
|
||||||
const text = this.scene.add.text(BUFF_ICON_SIZE / 2, 2, label, {
|
|
||||||
fontSize: '9px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontStyle: 'bold',
|
|
||||||
}).setOrigin(0.5, 0);
|
|
||||||
icon.add(text);
|
|
||||||
|
|
||||||
const stackText = this.scene.add.text(BUFF_ICON_SIZE / 2, BUFF_ICON_SIZE - 2, `${entry.stacks}`, {
|
|
||||||
fontSize: '10px',
|
|
||||||
color: '#ffcc44',
|
|
||||||
fontStyle: 'bold',
|
|
||||||
}).setOrigin(0.5, 1);
|
|
||||||
icon.add(stackText);
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getEffectLabel(effectId: string): string {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
block: '🛡',
|
|
||||||
strength: '💪',
|
|
||||||
dexterity: '🤸',
|
|
||||||
regen: '💚',
|
|
||||||
weak: '⚡',
|
|
||||||
vulnerable: '🔥',
|
|
||||||
frail: '🩹',
|
|
||||||
poison: '☠',
|
|
||||||
};
|
|
||||||
return labels[effectId.toLowerCase()] ?? effectId.substring(0, 3).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(entity: CombatEntity): void {
|
|
||||||
this.entity = entity;
|
|
||||||
this.updateHpBar();
|
|
||||||
this.refreshBuffs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.container.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getContainer(): Phaser.GameObjects.Container {
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,513 +0,0 @@
|
||||||
import Phaser from 'phaser';
|
|
||||||
import { MutableSignal } from 'boardgame-core';
|
|
||||||
import {
|
|
||||||
type GridInventory,
|
|
||||||
type InventoryItem,
|
|
||||||
type GameItemMeta,
|
|
||||||
type RunState,
|
|
||||||
type CellKey,
|
|
||||||
validatePlacement,
|
|
||||||
removeItemFromGrid,
|
|
||||||
placeItem,
|
|
||||||
moveItem,
|
|
||||||
rotateItem,
|
|
||||||
transformShape,
|
|
||||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
|
||||||
|
|
||||||
const ITEM_COLORS = [0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, 0xff6633];
|
|
||||||
|
|
||||||
export interface InventoryWidgetOptions {
|
|
||||||
scene: Phaser.Scene;
|
|
||||||
gameState: MutableSignal<RunState>;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
cellSize: number;
|
|
||||||
gridGap?: number;
|
|
||||||
isLocked?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DragState {
|
|
||||||
itemId: string;
|
|
||||||
itemShape: InventoryItem<GameItemMeta>['shape'];
|
|
||||||
itemTransform: InventoryItem<GameItemMeta>['transform'];
|
|
||||||
itemMeta: InventoryItem<GameItemMeta>['meta'];
|
|
||||||
ghostContainer: Phaser.GameObjects.Container;
|
|
||||||
previewGraphics: Phaser.GameObjects.Graphics;
|
|
||||||
dragOffsetX: number;
|
|
||||||
dragOffsetY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LostItem {
|
|
||||||
id: string;
|
|
||||||
container: Phaser.GameObjects.Container;
|
|
||||||
shape: InventoryItem<GameItemMeta>['shape'];
|
|
||||||
transform: InventoryItem<GameItemMeta>['transform'];
|
|
||||||
meta: InventoryItem<GameItemMeta>['meta'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InventoryWidget {
|
|
||||||
private scene: Phaser.Scene;
|
|
||||||
private gameState: MutableSignal<RunState>;
|
|
||||||
private container: Phaser.GameObjects.Container;
|
|
||||||
private cellSize: number;
|
|
||||||
private gridGap: number;
|
|
||||||
private gridX = 0;
|
|
||||||
private gridY = 0;
|
|
||||||
private isLocked: boolean;
|
|
||||||
|
|
||||||
private itemContainers = new Map<string, Phaser.GameObjects.Container>();
|
|
||||||
private itemGraphics = new Map<string, Phaser.GameObjects.Graphics>();
|
|
||||||
private itemTexts = new Map<string, Phaser.GameObjects.Text>();
|
|
||||||
private colorMap = new Map<string, number>();
|
|
||||||
private colorIdx = 0;
|
|
||||||
|
|
||||||
private gridGraphics!: Phaser.GameObjects.Graphics;
|
|
||||||
private dragState: DragState | null = null;
|
|
||||||
private lostItems = new Map<string, LostItem>();
|
|
||||||
|
|
||||||
private pointerMoveHandler: (pointer: Phaser.Input.Pointer) => void;
|
|
||||||
private pointerUpHandler: (pointer: Phaser.Input.Pointer) => void;
|
|
||||||
|
|
||||||
constructor(options: InventoryWidgetOptions) {
|
|
||||||
this.scene = options.scene;
|
|
||||||
this.gameState = options.gameState;
|
|
||||||
this.cellSize = options.cellSize;
|
|
||||||
this.gridGap = options.gridGap ?? 2;
|
|
||||||
this.isLocked = options.isLocked ?? false;
|
|
||||||
|
|
||||||
const inventory = this.gameState.value.inventory;
|
|
||||||
const gridW = inventory.width * this.cellSize + (inventory.width - 1) * this.gridGap;
|
|
||||||
const gridH = inventory.height * this.cellSize + (inventory.height - 1) * this.gridGap;
|
|
||||||
|
|
||||||
this.container = this.scene.add.container(options.x, options.y);
|
|
||||||
|
|
||||||
this.pointerMoveHandler = this.onPointerMove.bind(this);
|
|
||||||
this.pointerUpHandler = this.onPointerUp.bind(this);
|
|
||||||
|
|
||||||
this.drawGridBackground(inventory.width, inventory.height, gridW, gridH);
|
|
||||||
this.drawItems();
|
|
||||||
this.setupInput();
|
|
||||||
|
|
||||||
this.scene.events.once('shutdown', () => this.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInventory(): GridInventory<GameItemMeta> {
|
|
||||||
return this.gameState.value.inventory as unknown as GridInventory<GameItemMeta>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawGridBackground(width: number, height: number, gridW: number, gridH: number): void {
|
|
||||||
this.gridGraphics = this.scene.add.graphics();
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
const px = this.gridX + x * (this.cellSize + this.gridGap);
|
|
||||||
const py = this.gridY + y * (this.cellSize + this.gridGap);
|
|
||||||
|
|
||||||
this.gridGraphics.fillStyle(0x1a1a2e);
|
|
||||||
this.gridGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
this.gridGraphics.lineStyle(2, 0x444477);
|
|
||||||
this.gridGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.container.add(this.gridGraphics);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawItems(): void {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
|
|
||||||
for (const [itemId, item] of inventory.items) {
|
|
||||||
if (this.itemContainers.has(itemId)) continue;
|
|
||||||
this.createItemVisuals(itemId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createItemVisuals(itemId: string, item: InventoryItem<GameItemMeta>): void {
|
|
||||||
const color = this.colorMap.get(itemId) ?? ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length];
|
|
||||||
this.colorMap.set(itemId, color);
|
|
||||||
|
|
||||||
const graphics = this.scene.add.graphics();
|
|
||||||
this.itemGraphics.set(itemId, graphics);
|
|
||||||
|
|
||||||
const cells = this.getItemCells(item);
|
|
||||||
for (const cell of cells) {
|
|
||||||
const px = this.gridX + cell.x * (this.cellSize + this.gridGap);
|
|
||||||
const py = this.gridY + cell.y * (this.cellSize + this.gridGap);
|
|
||||||
|
|
||||||
graphics.fillStyle(color);
|
|
||||||
graphics.fillRect(px + 1, py + 1, this.cellSize - 2, this.cellSize - 2);
|
|
||||||
graphics.lineStyle(2, 0xffffff);
|
|
||||||
graphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstCell = cells[0];
|
|
||||||
const name = item.meta?.itemData.name ?? item.id;
|
|
||||||
const fontSize = Math.max(10, Math.floor(this.cellSize / 5));
|
|
||||||
const text = this.scene.add.text(
|
|
||||||
this.gridX + firstCell.x * (this.cellSize + this.gridGap) + this.cellSize / 2,
|
|
||||||
this.gridY + firstCell.y * (this.cellSize + this.gridGap) + this.cellSize / 2,
|
|
||||||
name,
|
|
||||||
{ fontSize: `${fontSize}px`, color: '#fff', fontStyle: 'bold' }
|
|
||||||
).setOrigin(0.5);
|
|
||||||
this.itemTexts.set(itemId, text);
|
|
||||||
|
|
||||||
const hitRect = new Phaser.Geom.Rectangle(
|
|
||||||
this.gridX + firstCell.x * (this.cellSize + this.gridGap),
|
|
||||||
this.gridY + firstCell.y * (this.cellSize + this.gridGap),
|
|
||||||
this.cellSize, this.cellSize
|
|
||||||
);
|
|
||||||
|
|
||||||
const container = this.scene.add.container(0, 0);
|
|
||||||
container.add(graphics);
|
|
||||||
container.add(text);
|
|
||||||
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
||||||
|
|
||||||
container.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
||||||
if (this.isLocked) return;
|
|
||||||
if (this.dragState) return;
|
|
||||||
if (pointer.button === 0) {
|
|
||||||
this.startDrag(itemId, pointer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.itemContainers.set(itemId, container);
|
|
||||||
this.container.add(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getItemCells(item: InventoryItem<GameItemMeta>): { x: number; y: number }[] {
|
|
||||||
const cells: { x: number; y: number }[] = [];
|
|
||||||
const { offset } = item.transform;
|
|
||||||
for (let y = 0; y < item.shape.height; y++) {
|
|
||||||
for (let x = 0; x < item.shape.width; x++) {
|
|
||||||
if (item.shape.grid[y]?.[x]) {
|
|
||||||
cells.push({ x: x + offset.x, y: y + offset.y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cells;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupInput(): void {
|
|
||||||
this.scene.input.on('pointermove', this.pointerMoveHandler);
|
|
||||||
this.scene.input.on('pointerup', this.pointerUpHandler);
|
|
||||||
this.scene.input.on('pointerdown', this.onPointerDown.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPointerDown(pointer: Phaser.Input.Pointer): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
if (pointer.button === 1) {
|
|
||||||
this.rotateDraggedItem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startDrag(itemId: string, pointer: Phaser.Input.Pointer): void {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
const item = inventory.items.get(itemId);
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
this.gameState.produce(state => {
|
|
||||||
removeItemFromGrid(state.inventory, itemId);
|
|
||||||
});
|
|
||||||
this.removeItemVisuals(itemId);
|
|
||||||
|
|
||||||
const cells = this.getItemCells(item);
|
|
||||||
const firstCell = cells[0];
|
|
||||||
const itemWorldX = this.container.x + this.gridX + firstCell.x * (this.cellSize + this.gridGap);
|
|
||||||
const itemWorldY = this.container.y + this.gridY + firstCell.y * (this.cellSize + this.gridGap);
|
|
||||||
const dragOffsetX = pointer.x - itemWorldX;
|
|
||||||
const dragOffsetY = pointer.y - itemWorldY;
|
|
||||||
|
|
||||||
const ghostContainer = this.scene.add.container(itemWorldX, itemWorldY).setDepth(1000);
|
|
||||||
const ghostGraphics = this.scene.add.graphics();
|
|
||||||
const color = this.colorMap.get(itemId) ?? 0x888888;
|
|
||||||
|
|
||||||
for (let y = 0; y < item.shape.height; y++) {
|
|
||||||
for (let x = 0; x < item.shape.width; x++) {
|
|
||||||
if (item.shape.grid[y]?.[x]) {
|
|
||||||
ghostGraphics.fillStyle(color, 0.7);
|
|
||||||
ghostGraphics.fillRect(x * (this.cellSize + this.gridGap), y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
||||||
ghostGraphics.lineStyle(2, 0xffffff);
|
|
||||||
ghostGraphics.strokeRect(x * (this.cellSize + this.gridGap), y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ghostContainer.add(ghostGraphics);
|
|
||||||
|
|
||||||
const previewGraphics = this.scene.add.graphics().setDepth(999).setAlpha(0.5);
|
|
||||||
|
|
||||||
this.dragState = {
|
|
||||||
itemId,
|
|
||||||
itemShape: item.shape,
|
|
||||||
itemTransform: { ...item.transform, offset: { ...item.transform.offset } },
|
|
||||||
itemMeta: item.meta,
|
|
||||||
ghostContainer,
|
|
||||||
previewGraphics,
|
|
||||||
dragOffsetX,
|
|
||||||
dragOffsetY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private startLostItemDrag(itemId: string, pointer: Phaser.Input.Pointer): void {
|
|
||||||
const lost = this.lostItems.get(itemId);
|
|
||||||
if (!lost) return;
|
|
||||||
|
|
||||||
lost.container.destroy();
|
|
||||||
this.lostItems.delete(itemId);
|
|
||||||
|
|
||||||
const ghostContainer = this.scene.add.container(pointer.x, pointer.y).setDepth(1000);
|
|
||||||
const ghostGraphics = this.scene.add.graphics();
|
|
||||||
const color = this.colorMap.get(itemId) ?? 0x888888;
|
|
||||||
|
|
||||||
const cells = transformShape(lost.shape, lost.transform);
|
|
||||||
for (const cell of cells) {
|
|
||||||
ghostGraphics.fillStyle(color, 0.7);
|
|
||||||
ghostGraphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
||||||
ghostGraphics.lineStyle(2, 0xffffff);
|
|
||||||
ghostGraphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
ghostContainer.add(ghostGraphics);
|
|
||||||
|
|
||||||
const previewGraphics = this.scene.add.graphics().setDepth(999).setAlpha(0.5);
|
|
||||||
|
|
||||||
this.dragState = {
|
|
||||||
itemId,
|
|
||||||
itemShape: lost.shape,
|
|
||||||
itemTransform: { ...lost.transform, offset: { ...lost.transform.offset } },
|
|
||||||
itemMeta: lost.meta,
|
|
||||||
ghostContainer,
|
|
||||||
previewGraphics,
|
|
||||||
dragOffsetX: 0,
|
|
||||||
dragOffsetY: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private rotateDraggedItem(): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
const currentRotation = (this.dragState.itemTransform.rotation + 90) % 360;
|
|
||||||
this.dragState.itemTransform = {
|
|
||||||
...this.dragState.itemTransform,
|
|
||||||
rotation: currentRotation,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateGhostVisuals();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateGhostVisuals(): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
this.dragState.ghostContainer.removeAll(true);
|
|
||||||
const ghostGraphics = this.scene.add.graphics();
|
|
||||||
const color = this.colorMap.get(this.dragState.itemId) ?? 0x888888;
|
|
||||||
|
|
||||||
const cells = transformShape(this.dragState.itemShape, this.dragState.itemTransform);
|
|
||||||
for (const cell of cells) {
|
|
||||||
ghostGraphics.fillStyle(color, 0.7);
|
|
||||||
ghostGraphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
||||||
ghostGraphics.lineStyle(2, 0xffffff);
|
|
||||||
ghostGraphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
this.dragState.ghostContainer.add(ghostGraphics);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPointerMove(pointer: Phaser.Input.Pointer): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
this.dragState.ghostContainer.setPosition(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
|
||||||
|
|
||||||
const gridCell = this.getWorldGridCell(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
|
||||||
this.dragState.previewGraphics.clear();
|
|
||||||
|
|
||||||
if (gridCell) {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
const testTransform = { ...this.dragState.itemTransform, offset: { x: gridCell.x, y: gridCell.y } };
|
|
||||||
const validation = validatePlacement(inventory, this.dragState.itemShape, testTransform);
|
|
||||||
|
|
||||||
const cells = transformShape(this.dragState.itemShape, testTransform);
|
|
||||||
for (const cell of cells) {
|
|
||||||
const px = this.gridX + cell.x * (this.cellSize + this.gridGap);
|
|
||||||
const py = this.gridY + cell.y * (this.cellSize + this.gridGap);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
this.dragState.previewGraphics.fillStyle(0x33ff33, 0.3);
|
|
||||||
this.dragState.previewGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
this.dragState.previewGraphics.lineStyle(2, 0x33ff33);
|
|
||||||
this.dragState.previewGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
} else {
|
|
||||||
this.dragState.previewGraphics.fillStyle(0xff3333, 0.3);
|
|
||||||
this.dragState.previewGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
this.dragState.previewGraphics.lineStyle(2, 0xff3333);
|
|
||||||
this.dragState.previewGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPointerUp(pointer: Phaser.Input.Pointer): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
const gridCell = this.getWorldGridCell(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
|
|
||||||
this.dragState.ghostContainer.destroy();
|
|
||||||
this.dragState.previewGraphics.destroy();
|
|
||||||
|
|
||||||
if (gridCell) {
|
|
||||||
const testTransform = { ...this.dragState.itemTransform, offset: { x: gridCell.x, y: gridCell.y } };
|
|
||||||
const validation = validatePlacement(inventory, this.dragState.itemShape, testTransform);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
this.gameState.produce(state => {
|
|
||||||
const item: InventoryItem<GameItemMeta> = {
|
|
||||||
id: this.dragState!.itemId,
|
|
||||||
shape: this.dragState!.itemShape,
|
|
||||||
transform: testTransform,
|
|
||||||
meta: this.dragState!.itemMeta,
|
|
||||||
};
|
|
||||||
placeItem(state.inventory, item);
|
|
||||||
});
|
|
||||||
this.createItemVisualsFromDrag();
|
|
||||||
} else {
|
|
||||||
this.createLostItem();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.createLostItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createItemVisualsFromDrag(): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
const item = inventory.items.get(this.dragState.itemId);
|
|
||||||
if (item) {
|
|
||||||
this.createItemVisuals(this.dragState.itemId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getWorldGridCell(worldX: number, worldY: number): { x: number; y: number } | null {
|
|
||||||
const localX = worldX - this.container.x - this.gridX;
|
|
||||||
const localY = worldY - this.container.y - this.gridY;
|
|
||||||
|
|
||||||
const cellX = Math.floor(localX / (this.cellSize + this.gridGap));
|
|
||||||
const cellY = Math.floor(localY / (this.cellSize + this.gridGap));
|
|
||||||
|
|
||||||
if (cellX < 0 || cellY < 0 || cellX >= this.getInventory().width || cellY >= this.getInventory().height) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { x: cellX, y: cellY };
|
|
||||||
}
|
|
||||||
|
|
||||||
private createLostItem(): void {
|
|
||||||
if (!this.dragState) return;
|
|
||||||
|
|
||||||
const container = this.scene.add.container(
|
|
||||||
this.dragState.ghostContainer.x,
|
|
||||||
this.dragState.ghostContainer.y
|
|
||||||
).setDepth(500);
|
|
||||||
|
|
||||||
const graphics = this.scene.add.graphics();
|
|
||||||
const color = this.colorMap.get(this.dragState.itemId) ?? 0x888888;
|
|
||||||
|
|
||||||
const cells = transformShape(this.dragState.itemShape, this.dragState.itemTransform);
|
|
||||||
for (const cell of cells) {
|
|
||||||
graphics.fillStyle(color, 0.5);
|
|
||||||
graphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
||||||
graphics.lineStyle(2, 0xff4444);
|
|
||||||
graphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
||||||
}
|
|
||||||
container.add(graphics);
|
|
||||||
|
|
||||||
const name = this.dragState.itemMeta?.itemData.name ?? this.dragState.itemId;
|
|
||||||
const text = this.scene.add.text(0, -20, `${name} (lost)`, {
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#ff4444',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
}).setOrigin(0.5);
|
|
||||||
container.add(text);
|
|
||||||
|
|
||||||
const hitRect = new Phaser.Geom.Rectangle(0, 0, this.cellSize, this.cellSize);
|
|
||||||
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
||||||
|
|
||||||
container.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
||||||
if (this.isLocked) return;
|
|
||||||
if (this.dragState) return;
|
|
||||||
if (pointer.button === 0) {
|
|
||||||
this.startLostItemDrag(this.dragState!.itemId, pointer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lostItems.set(this.dragState.itemId, {
|
|
||||||
id: this.dragState.itemId,
|
|
||||||
container,
|
|
||||||
shape: this.dragState.itemShape,
|
|
||||||
transform: { ...this.dragState.itemTransform },
|
|
||||||
meta: this.dragState.itemMeta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeItemVisuals(itemId: string): void {
|
|
||||||
this.itemContainers.get(itemId)?.destroy();
|
|
||||||
this.itemGraphics.get(itemId)?.destroy();
|
|
||||||
this.itemTexts.get(itemId)?.destroy();
|
|
||||||
this.itemContainers.delete(itemId);
|
|
||||||
this.itemGraphics.delete(itemId);
|
|
||||||
this.itemTexts.delete(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setLocked(locked: boolean): void {
|
|
||||||
this.isLocked = locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLostItems(): string[] {
|
|
||||||
return Array.from(this.lostItems.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearLostItems(): void {
|
|
||||||
for (const lost of this.lostItems.values()) {
|
|
||||||
lost.container.destroy();
|
|
||||||
}
|
|
||||||
this.lostItems.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public refresh(): void {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
|
|
||||||
for (const itemId of this.itemContainers.keys()) {
|
|
||||||
if (!inventory.items.has(itemId)) {
|
|
||||||
this.removeItemVisuals(itemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [itemId, item] of inventory.items) {
|
|
||||||
if (!this.itemContainers.has(itemId)) {
|
|
||||||
this.createItemVisuals(itemId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.scene.input.off('pointermove', this.pointerMoveHandler);
|
|
||||||
this.scene.input.off('pointerup', this.pointerUpHandler);
|
|
||||||
|
|
||||||
if (this.dragState) {
|
|
||||||
this.dragState.ghostContainer.destroy();
|
|
||||||
this.dragState.previewGraphics.destroy();
|
|
||||||
this.dragState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearLostItems();
|
|
||||||
|
|
||||||
for (const container of this.itemContainers.values()) {
|
|
||||||
container.destroy();
|
|
||||||
}
|
|
||||||
this.itemContainers.clear();
|
|
||||||
this.itemGraphics.clear();
|
|
||||||
this.itemTexts.clear();
|
|
||||||
|
|
||||||
this.gridGraphics.destroy();
|
|
||||||
this.container.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue