feat: adding widgets to the game
This commit is contained in:
parent
f7a6154c68
commit
28a2623bd1
|
|
@ -3,26 +3,21 @@ 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 占位符遭遇场景
|
* 占位符遭遇场景
|
||||||
*
|
*
|
||||||
* 左侧显示背包网格(80x80 每格),右侧显示遭遇信息。
|
* 左侧显示背包网格(使用 InventoryWidget),右侧显示遭遇信息。
|
||||||
*/
|
*/
|
||||||
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');
|
||||||
|
|
@ -34,24 +29,37 @@ 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;
|
||||||
|
|
||||||
// ── Layout: split screen into left (grid) and right (encounter) ──
|
const gridCols = state.inventory.width;
|
||||||
const gridCols = state.inventory.width; // 6
|
const gridRows = state.inventory.height;
|
||||||
const gridRows = state.inventory.height; // 4
|
const cellSize = 80;
|
||||||
const gridW = gridCols * this.CELL_SIZE;
|
const gridW = gridCols * cellSize + (gridCols - 1) * 2;
|
||||||
const gridH = gridRows * this.CELL_SIZE;
|
const gridH = gridRows * cellSize + (gridRows - 1) * 2;
|
||||||
const leftPanelW = gridW + 40; // panel padding
|
const leftPanelW = gridW + 40;
|
||||||
|
|
||||||
this.gridX = 60;
|
this.inventoryWidget = new InventoryWidget({
|
||||||
this.gridY = (height - gridH) / 2 + 20;
|
scene: this,
|
||||||
|
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);
|
||||||
|
|
||||||
// ── LEFT PANEL: inventory grid ──
|
// Panel background
|
||||||
this.drawLeftPanel(leftPanelW, gridW, gridH);
|
this.add.rectangle(
|
||||||
|
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;
|
||||||
|
|
@ -64,80 +72,11 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
this.drawRightPanel(node, leftPanelW, width, height);
|
this.drawRightPanel(node, leftPanelW, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────── LEFT PANEL ─────────────────────
|
private inventoryWidgetY(gridH: number): number {
|
||||||
|
const { height } = this.scale;
|
||||||
private drawLeftPanel(panelW: number, gridW: number, gridH: number): void {
|
return (height - gridH) / 2 + 20 + gridH / 2;
|
||||||
// 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 = {
|
||||||
type: node.type as MapNodeType,
|
type: node.type as MapNodeType,
|
||||||
|
|
@ -151,12 +90,10 @@ 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);
|
||||||
|
|
@ -164,28 +101,23 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
@ -194,21 +126,6 @@ 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: '精英战斗',
|
||||||
|
|
@ -240,6 +157,15 @@ 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');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,444 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LostItem {
|
||||||
|
id: string;
|
||||||
|
container: Phaser.GameObjects.Container;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.drawGridBackground(inventory.width, inventory.height, gridW, gridH);
|
||||||
|
this.drawItems();
|
||||||
|
this.setupInput();
|
||||||
|
|
||||||
|
this.pointerMoveHandler = this.onPointerMove.bind(this);
|
||||||
|
this.pointerUpHandler = this.onPointerUp.bind(this);
|
||||||
|
|
||||||
|
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 ghostContainer = this.scene.add.container(pointer.x, pointer.y).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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, pointer.y);
|
||||||
|
|
||||||
|
const gridCell = this.getWorldGridCell(pointer.x, pointer.y);
|
||||||
|
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, pointer.y);
|
||||||
|
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));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.lostItems.set(this.dragState.itemId, { id: this.dragState.itemId, container });
|
||||||
|
}
|
||||||
|
|
||||||
|
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