Refactor: centralize config and split inventory
Move magic numbers and style constants to src/config.ts. Decompose InventoryWidget into DragController, ItemRenderer, and LostItemManager for better separation of concerns. Add createButton utility and remove unused CombatUnitWidget. Update scenes to import from centralized config.
This commit is contained in:
@@ -1,48 +1,22 @@
|
||||
import Phaser from 'phaser';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
import { MutableSignal } from 'boardgame-core';
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG, MAP_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
||||
import { MutableSignal } from "boardgame-core";
|
||||
import {
|
||||
canMoveTo,
|
||||
moveToNode,
|
||||
getCurrentNode,
|
||||
getReachableChildren,
|
||||
isAtEndNode,
|
||||
isAtStartNode,
|
||||
type RunState,
|
||||
type MapNode,
|
||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
||||
|
||||
const NODE_COLORS: Record<string, number> = {
|
||||
start: 0x44aa44,
|
||||
end: 0xcc8844,
|
||||
minion: 0xcc4444,
|
||||
elite: 0xcc44cc,
|
||||
event: 0xaaaa44,
|
||||
camp: 0x44cccc,
|
||||
shop: 0x4488cc,
|
||||
curio: 0x8844cc,
|
||||
};
|
||||
|
||||
const NODE_LABELS: Record<string, string> = {
|
||||
start: '起点',
|
||||
end: '终点',
|
||||
minion: '战斗',
|
||||
elite: '精英',
|
||||
event: '事件',
|
||||
camp: '营地',
|
||||
shop: '商店',
|
||||
curio: '奇遇',
|
||||
};
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class GameFlowScene extends ReactiveScene {
|
||||
/** 全局游戏状态(由 App.tsx 注入) */
|
||||
private gameState: MutableSignal<RunState>;
|
||||
|
||||
// Layout constants
|
||||
private readonly LAYER_HEIGHT = 110;
|
||||
private readonly NODE_SPACING = 140;
|
||||
private readonly NODE_RADIUS = 28;
|
||||
|
||||
// UI elements
|
||||
private hudContainer!: Phaser.GameObjects.Container;
|
||||
private hpText!: Phaser.GameObjects.Text;
|
||||
@@ -62,7 +36,7 @@ export class GameFlowScene extends ReactiveScene {
|
||||
private nodeGraphics: Map<string, Phaser.GameObjects.Graphics> = new Map();
|
||||
|
||||
constructor(gameState: MutableSignal<RunState>) {
|
||||
super('GameFlowScene');
|
||||
super("GameFlowScene");
|
||||
this.gameState = gameState;
|
||||
}
|
||||
|
||||
@@ -82,31 +56,46 @@ export class GameFlowScene extends ReactiveScene {
|
||||
this.hudContainer.add(hudBg);
|
||||
|
||||
// HP
|
||||
this.hpText = this.add.text(-150, 0, '', {
|
||||
fontSize: '16px',
|
||||
color: '#ff6666',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0, 0.5);
|
||||
this.hpText = this.add
|
||||
.text(-150, 0, "", {
|
||||
fontSize: "16px",
|
||||
color: "#ff6666",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0, 0.5);
|
||||
this.hudContainer.add(this.hpText);
|
||||
|
||||
// Gold
|
||||
this.goldText = this.add.text(-50, 0, '', {
|
||||
fontSize: '16px',
|
||||
color: '#ffcc44',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0, 0.5);
|
||||
this.goldText = this.add
|
||||
.text(-50, 0, "", {
|
||||
fontSize: "16px",
|
||||
color: "#ffcc44",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0, 0.5);
|
||||
this.hudContainer.add(this.goldText);
|
||||
|
||||
// Current node
|
||||
this.nodeText = this.add.text(50, 0, '', {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0, 0.5);
|
||||
this.nodeText = this.add
|
||||
.text(50, 0, "", {
|
||||
fontSize: "16px",
|
||||
color: "#ffffff",
|
||||
})
|
||||
.setOrigin(0, 0.5);
|
||||
this.hudContainer.add(this.nodeText);
|
||||
|
||||
// Back to menu button
|
||||
this.createButton('返回菜单', width - 100, 25, 140, 36, async () => {
|
||||
await this.sceneController.launch('IndexScene');
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: width - 100,
|
||||
y: 25,
|
||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
},
|
||||
depth: 200,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,18 +117,26 @@ export class GameFlowScene extends ReactiveScene {
|
||||
private drawMap(): void {
|
||||
const { width, height } = this.scale;
|
||||
const state = this.gameState.value;
|
||||
const {
|
||||
LAYER_HEIGHT,
|
||||
NODE_SPACING,
|
||||
NODE_RADIUS,
|
||||
MAX_NODES_PER_LAYER,
|
||||
TOTAL_LAYERS,
|
||||
} = MAP_CONFIG;
|
||||
|
||||
// Calculate map bounds (left-to-right: layers along X, nodes along Y)
|
||||
const maxLayer = 9;
|
||||
const maxNodesInLayer = 5;
|
||||
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
|
||||
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
||||
const maxLayer = TOTAL_LAYERS - 1;
|
||||
const mapWidth = maxLayer * LAYER_HEIGHT + 200;
|
||||
const mapHeight = (MAX_NODES_PER_LAYER - 1) * NODE_SPACING + 200;
|
||||
|
||||
// Create scrollable container
|
||||
this.mapContainer = this.add.container(width / 2, height / 2 + 50);
|
||||
|
||||
// Background panel
|
||||
const bg = this.add.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5).setOrigin(0.5);
|
||||
const bg = this.add
|
||||
.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5)
|
||||
.setOrigin(0.5);
|
||||
this.mapContainer.add(bg);
|
||||
|
||||
const graphics = this.add.graphics();
|
||||
@@ -147,7 +144,7 @@ export class GameFlowScene extends ReactiveScene {
|
||||
|
||||
const { map, currentNodeId } = state;
|
||||
const reachableChildren = getReachableChildren(state);
|
||||
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||
const reachableIds = new Set(reachableChildren.map((n) => n.id));
|
||||
|
||||
// Draw edges
|
||||
graphics.lineStyle(2, 0x666666);
|
||||
@@ -178,9 +175,13 @@ export class GameFlowScene extends ReactiveScene {
|
||||
this.mapContainer.add(nodeGraphics);
|
||||
this.nodeGraphics.set(nodeId, nodeGraphics);
|
||||
|
||||
const color = isCurrent ? 0xffffff : (isReachable ? this.brightenColor(baseColor) : baseColor);
|
||||
const color = isCurrent
|
||||
? 0xffffff
|
||||
: isReachable
|
||||
? this.brightenColor(baseColor)
|
||||
: baseColor;
|
||||
nodeGraphics.fillStyle(color);
|
||||
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.fillCircle(posX, posY, NODE_RADIUS);
|
||||
|
||||
if (isCurrent) {
|
||||
nodeGraphics.lineStyle(3, 0xffff44);
|
||||
@@ -189,86 +190,109 @@ export class GameFlowScene extends ReactiveScene {
|
||||
} else {
|
||||
nodeGraphics.lineStyle(2, 0x888888);
|
||||
}
|
||||
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.strokeCircle(posX, posY, NODE_RADIUS);
|
||||
|
||||
// Node label
|
||||
const label = NODE_LABELS[node.type] ?? node.type;
|
||||
this.mapContainer.add(
|
||||
this.add.text(posX, posY, label, {
|
||||
fontSize: '11px',
|
||||
color: '#ffffff',
|
||||
fontStyle: isCurrent ? 'bold' : 'normal',
|
||||
}).setOrigin(0.5)
|
||||
this.add
|
||||
.text(posX, posY, label, {
|
||||
fontSize: "11px",
|
||||
color: "#ffffff",
|
||||
fontStyle: isCurrent ? "bold" : "normal",
|
||||
})
|
||||
.setOrigin(0.5),
|
||||
);
|
||||
|
||||
// Encounter name
|
||||
if (node.encounter) {
|
||||
this.mapContainer.add(
|
||||
this.add.text(posX, posY + this.NODE_RADIUS + 12, node.encounter.name, {
|
||||
fontSize: '10px',
|
||||
color: '#cccccc',
|
||||
}).setOrigin(0.5)
|
||||
this.add
|
||||
.text(posX, posY + NODE_RADIUS + 12, node.encounter.name, {
|
||||
fontSize: "10px",
|
||||
color: "#cccccc",
|
||||
})
|
||||
.setOrigin(0.5),
|
||||
);
|
||||
}
|
||||
|
||||
// Make reachable nodes interactive (add hitZone to mapContainer so positions match)
|
||||
// Make reachable nodes interactive
|
||||
if (isReachable) {
|
||||
const hitZone = this.add.circle(posX, posY, this.NODE_RADIUS, 0x000000, 0)
|
||||
const hitZone = this.add
|
||||
.circle(posX, posY, NODE_RADIUS, 0x000000, 0)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
this.mapContainer.add(hitZone);
|
||||
|
||||
hitZone.on('pointerover', () => {
|
||||
hitZone.on("pointerover", () => {
|
||||
this.hoveredNode = nodeId;
|
||||
nodeGraphics.clear();
|
||||
nodeGraphics.fillStyle(this.brightenColor(baseColor));
|
||||
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.fillCircle(posX, posY, NODE_RADIUS);
|
||||
nodeGraphics.lineStyle(3, 0xaaddaa);
|
||||
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.strokeCircle(posX, posY, NODE_RADIUS);
|
||||
});
|
||||
|
||||
hitZone.on('pointerout', () => {
|
||||
hitZone.on("pointerout", () => {
|
||||
this.hoveredNode = null;
|
||||
nodeGraphics.clear();
|
||||
nodeGraphics.fillStyle(baseColor);
|
||||
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.fillCircle(posX, posY, NODE_RADIUS);
|
||||
nodeGraphics.lineStyle(2, 0xaaddaa);
|
||||
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||
nodeGraphics.strokeCircle(posX, posY, NODE_RADIUS);
|
||||
});
|
||||
|
||||
hitZone.on('pointerdown', () => {
|
||||
hitZone.on("pointerdown", () => {
|
||||
this.onNodeClick(nodeId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup drag-to-scroll
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
// Setup drag-to-scroll with disposables cleanup
|
||||
const onPointerDown = (pointer: Phaser.Input.Pointer) => {
|
||||
this.isDragging = true;
|
||||
this.dragStartX = pointer.x;
|
||||
this.dragStartY = pointer.y;
|
||||
this.dragStartContainerX = this.mapContainer.x;
|
||||
this.dragStartContainerY = this.mapContainer.y;
|
||||
});
|
||||
};
|
||||
|
||||
this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
|
||||
const onPointerMove = (pointer: Phaser.Input.Pointer) => {
|
||||
if (!this.isDragging) return;
|
||||
this.mapContainer.x = this.dragStartContainerX + (pointer.x - this.dragStartX);
|
||||
this.mapContainer.y = this.dragStartContainerY + (pointer.y - this.dragStartY);
|
||||
});
|
||||
this.mapContainer.x =
|
||||
this.dragStartContainerX + (pointer.x - this.dragStartX);
|
||||
this.mapContainer.y =
|
||||
this.dragStartContainerY + (pointer.y - this.dragStartY);
|
||||
};
|
||||
|
||||
this.input.on('pointerup', () => {
|
||||
const onPointerUp = () => {
|
||||
this.isDragging = false;
|
||||
});
|
||||
};
|
||||
|
||||
this.input.on('pointerout', () => {
|
||||
this.isDragging = false;
|
||||
this.input.on("pointerdown", onPointerDown);
|
||||
this.input.on("pointermove", onPointerMove);
|
||||
this.input.on("pointerup", onPointerUp);
|
||||
this.input.on("pointerout", onPointerUp);
|
||||
|
||||
this.disposables.add(() => {
|
||||
this.input.off("pointerdown", onPointerDown);
|
||||
this.input.off("pointermove", onPointerMove);
|
||||
this.input.off("pointerup", onPointerUp);
|
||||
this.input.off("pointerout", onPointerUp);
|
||||
});
|
||||
|
||||
// Hint text
|
||||
this.add.text(width / 2, this.scale.height - 20, '点击可到达的节点进入遭遇 | 拖拽滚动查看地图', {
|
||||
fontSize: '14px',
|
||||
color: '#888888',
|
||||
}).setOrigin(0.5).setDepth(200);
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
this.scale.height - 20,
|
||||
"点击可到达的节点进入遭遇 | 拖拽滚动查看地图",
|
||||
{
|
||||
fontSize: "14px",
|
||||
color: "#888888",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5)
|
||||
.setDepth(200);
|
||||
}
|
||||
|
||||
private async onNodeClick(nodeId: string): Promise<void> {
|
||||
@@ -297,18 +321,18 @@ export class GameFlowScene extends ReactiveScene {
|
||||
// Launch encounter scene
|
||||
const currentNode = getCurrentNode(state);
|
||||
if (!currentNode || !currentNode.encounter) {
|
||||
console.warn('当前节点没有遭遇数据');
|
||||
console.warn("当前节点没有遭遇数据");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sceneController.launch('PlaceholderEncounterScene');
|
||||
await this.sceneController.launch("PlaceholderEncounterScene");
|
||||
}
|
||||
|
||||
private redrawMapHighlights(): void {
|
||||
const state = this.gameState.value;
|
||||
const { map, currentNodeId } = state;
|
||||
const reachableChildren = getReachableChildren(state);
|
||||
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||
const reachableIds = new Set(reachableChildren.map((n) => n.id));
|
||||
|
||||
for (const [nodeId, nodeGraphics] of this.nodeGraphics) {
|
||||
const node = map.nodes.get(nodeId);
|
||||
@@ -319,9 +343,17 @@ export class GameFlowScene extends ReactiveScene {
|
||||
const baseColor = NODE_COLORS[node.type] ?? 0x888888;
|
||||
|
||||
nodeGraphics.clear();
|
||||
const color = isCurrent ? 0xffffff : (isReachable ? this.brightenColor(baseColor) : baseColor);
|
||||
const color = isCurrent
|
||||
? 0xffffff
|
||||
: isReachable
|
||||
? this.brightenColor(baseColor)
|
||||
: baseColor;
|
||||
nodeGraphics.fillStyle(color);
|
||||
nodeGraphics.fillCircle(this.getNodeX(node), this.getNodeY(node), this.NODE_RADIUS);
|
||||
nodeGraphics.fillCircle(
|
||||
this.getNodeX(node),
|
||||
this.getNodeY(node),
|
||||
MAP_CONFIG.NODE_RADIUS,
|
||||
);
|
||||
|
||||
if (isCurrent) {
|
||||
nodeGraphics.lineStyle(3, 0xffff44);
|
||||
@@ -330,7 +362,11 @@ export class GameFlowScene extends ReactiveScene {
|
||||
} else {
|
||||
nodeGraphics.lineStyle(2, 0x888888);
|
||||
}
|
||||
nodeGraphics.strokeCircle(this.getNodeX(node), this.getNodeY(node), this.NODE_RADIUS);
|
||||
nodeGraphics.strokeCircle(
|
||||
this.getNodeX(node),
|
||||
this.getNodeY(node),
|
||||
MAP_CONFIG.NODE_RADIUS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,79 +375,65 @@ export class GameFlowScene extends ReactiveScene {
|
||||
const state = this.gameState.value;
|
||||
|
||||
// Overlay
|
||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(300);
|
||||
const overlay = this.add
|
||||
.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7)
|
||||
.setDepth(300);
|
||||
|
||||
// End message
|
||||
this.add.text(width / 2, height / 2 - 40, '恭喜通关!', {
|
||||
fontSize: '36px',
|
||||
color: '#ffcc44',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5).setDepth(300);
|
||||
this.add
|
||||
.text(width / 2, height / 2 - 40, "恭喜通关!", {
|
||||
fontSize: "36px",
|
||||
color: "#ffcc44",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(300);
|
||||
|
||||
const { player } = state;
|
||||
this.add.text(width / 2, height / 2 + 20, `剩余 HP: ${player.currentHp}/${player.maxHp}\n剩余金币: ${player.gold}`, {
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
}).setOrigin(0.5).setDepth(300);
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
height / 2 + 20,
|
||||
`剩余 HP: ${player.currentHp}/${player.maxHp}\n剩余金币: ${player.gold}`,
|
||||
{
|
||||
fontSize: "20px",
|
||||
color: "#ffffff",
|
||||
align: "center",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5)
|
||||
.setDepth(300);
|
||||
|
||||
this.createButton('返回菜单', width / 2, height / 2 + 100, 200, 50, async () => {
|
||||
await this.sceneController.launch('IndexScene');
|
||||
}, 300);
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: width / 2,
|
||||
y: height / 2 + 100,
|
||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
},
|
||||
depth: 300,
|
||||
});
|
||||
}
|
||||
|
||||
private getNodeX(node: MapNode): number {
|
||||
// Layers go left-to-right along X axis
|
||||
return -500 + node.layerIndex * this.LAYER_HEIGHT;
|
||||
return -500 + node.layerIndex * MAP_CONFIG.LAYER_HEIGHT;
|
||||
}
|
||||
|
||||
private getNodeY(node: MapNode): number {
|
||||
// Nodes within a layer are spread vertically along Y axis
|
||||
const state = this.gameState.value;
|
||||
const layer = state.map.layers[node.layerIndex];
|
||||
const layer = this.gameState.value.map.layers[node.layerIndex];
|
||||
const nodeIndex = layer.nodeIds.indexOf(node.id);
|
||||
const totalNodes = layer.nodeIds.length;
|
||||
const layerHeight = (totalNodes - 1) * this.NODE_SPACING;
|
||||
return -layerHeight / 2 + nodeIndex * this.NODE_SPACING;
|
||||
const layerHeight = (totalNodes - 1) * MAP_CONFIG.NODE_SPACING;
|
||||
return -layerHeight / 2 + nodeIndex * MAP_CONFIG.NODE_SPACING;
|
||||
}
|
||||
|
||||
private brightenColor(color: number): number {
|
||||
// Simple color brightening
|
||||
const r = Math.min(255, ((color >> 16) & 0xff) + 40);
|
||||
const g = Math.min(255, ((color >> 8) & 0xff) + 40);
|
||||
const b = Math.min(255, (color & 0xff) + 40);
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
private createButton(
|
||||
label: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
onClick: () => void,
|
||||
depth: number = 200
|
||||
): void {
|
||||
const bg = this.add.rectangle(x, y, width, height, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setDepth(depth);
|
||||
|
||||
const text = this.add.text(x, y, label, {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5).setDepth(depth);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x555588);
|
||||
text.setScale(1.05);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x444466);
|
||||
text.setScale(1);
|
||||
});
|
||||
|
||||
bg.on('pointerdown', onClick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG, UI_CONFIG, ITEM_COLORS } from "@/config";
|
||||
import {
|
||||
createGridInventory,
|
||||
placeItem,
|
||||
@@ -8,44 +11,49 @@ import {
|
||||
type GridInventory,
|
||||
type InventoryItem,
|
||||
type GameItemMeta,
|
||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class GridViewerScene extends ReactiveScene {
|
||||
private inventory: GridInventory<GameItemMeta>;
|
||||
private readonly CELL_SIZE = 60;
|
||||
private readonly GRID_WIDTH = 6;
|
||||
private readonly GRID_HEIGHT = 4;
|
||||
private gridOffsetX = 0;
|
||||
private gridOffsetY = 0;
|
||||
|
||||
constructor() {
|
||||
super('GridViewerScene');
|
||||
this.inventory = createGridInventory<GameItemMeta>(this.GRID_WIDTH, this.GRID_HEIGHT);
|
||||
super("GridViewerScene");
|
||||
this.inventory = createGridInventory<GameItemMeta>(
|
||||
GRID_CONFIG.WIDTH,
|
||||
GRID_CONFIG.HEIGHT,
|
||||
);
|
||||
}
|
||||
|
||||
create(): void {
|
||||
super.create();
|
||||
const { width, height } = this.scale;
|
||||
this.gridOffsetX = (width - this.GRID_WIDTH * this.CELL_SIZE) / 2;
|
||||
this.gridOffsetY = (height - this.GRID_HEIGHT * this.CELL_SIZE) / 2 + 20;
|
||||
this.gridOffsetX =
|
||||
(width - GRID_CONFIG.WIDTH * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
||||
this.gridOffsetY =
|
||||
(height - GRID_CONFIG.HEIGHT * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 20;
|
||||
|
||||
this.placeSampleItems();
|
||||
this.drawGrid();
|
||||
this.drawItems();
|
||||
|
||||
this.add.text(width / 2, 30, 'Grid Inventory Viewer (4x6)', {
|
||||
fontSize: '24px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(width / 2, 30, "Grid Inventory Viewer (4x6)", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createControls();
|
||||
|
||||
this.add.text(width / 2, height - 40, 'Hover over cells to see item details', {
|
||||
fontSize: '14px',
|
||||
color: '#aaaaaa',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(width / 2, height - 40, "Hover over cells to see item details", {
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
|
||||
private placeSampleItems(): void {
|
||||
@@ -64,7 +72,12 @@ export class GridViewerScene extends ReactiveScene {
|
||||
const item: InventoryItem<GameItemMeta> = {
|
||||
id: `item-${index}`,
|
||||
shape,
|
||||
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
|
||||
transform: {
|
||||
offset: { x, y },
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
},
|
||||
meta: { itemData, shape },
|
||||
};
|
||||
placeItem(this.inventory, item);
|
||||
@@ -74,18 +87,28 @@ export class GridViewerScene extends ReactiveScene {
|
||||
private drawGrid(): void {
|
||||
const graphics = this.add.graphics();
|
||||
|
||||
for (let y = 0; y < this.GRID_HEIGHT; y++) {
|
||||
for (let x = 0; x < this.GRID_WIDTH; x++) {
|
||||
const px = this.gridOffsetX + x * this.CELL_SIZE;
|
||||
const py = this.gridOffsetY + y * this.CELL_SIZE;
|
||||
for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) {
|
||||
for (let x = 0; x < GRID_CONFIG.WIDTH; x++) {
|
||||
const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
const isOccupied = this.inventory.occupiedCells.has(`${x},${y}`);
|
||||
const color = isOccupied ? 0x334455 : 0x222233;
|
||||
|
||||
graphics.fillStyle(color);
|
||||
graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
|
||||
graphics.fillRect(
|
||||
px + 1,
|
||||
py + 1,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
);
|
||||
graphics.lineStyle(1, 0x555577);
|
||||
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
|
||||
graphics.strokeRect(
|
||||
px,
|
||||
py,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,72 +133,120 @@ export class GridViewerScene extends ReactiveScene {
|
||||
const graphics = this.add.graphics();
|
||||
|
||||
for (const cell of cells) {
|
||||
const px = this.gridOffsetX + cell.x * this.CELL_SIZE;
|
||||
const py = this.gridOffsetY + cell.y * this.CELL_SIZE;
|
||||
const px = this.gridOffsetX + cell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py = this.gridOffsetY + cell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
graphics.fillStyle(itemColor);
|
||||
graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
|
||||
graphics.fillRect(
|
||||
px + 2,
|
||||
py + 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
);
|
||||
}
|
||||
|
||||
if (cells.length > 0) {
|
||||
const firstCell = cells[0];
|
||||
const px = this.gridOffsetX + firstCell.x * this.CELL_SIZE;
|
||||
const py = this.gridOffsetY + firstCell.y * this.CELL_SIZE;
|
||||
const px =
|
||||
this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py =
|
||||
this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const itemName = item.meta?.itemData.name ?? item.id;
|
||||
|
||||
this.add.text(px + this.CELL_SIZE / 2, py + this.CELL_SIZE / 2, itemName, {
|
||||
fontSize: '11px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(
|
||||
px + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
py + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
itemName,
|
||||
{
|
||||
fontSize: "11px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
|
||||
const adjacent = getAdjacentItems(this.inventory, itemId);
|
||||
if (adjacent.size > 0) {
|
||||
const adjacentNames = Array.from(adjacent.values()).map(i => i.meta?.itemData.name ?? i.id).join(', ');
|
||||
const adjacentNames = Array.from(adjacent.values())
|
||||
.map((i) => i.meta?.itemData.name ?? i.id)
|
||||
.join(", ");
|
||||
const firstCell = cells[0];
|
||||
const px = this.gridOffsetX + firstCell.x * this.CELL_SIZE;
|
||||
const py = this.gridOffsetY + firstCell.y * this.CELL_SIZE - 20;
|
||||
const px =
|
||||
this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py =
|
||||
this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE - 20;
|
||||
|
||||
this.add.text(px + this.CELL_SIZE / 2, py, `邻接: ${adjacentNames}`, {
|
||||
fontSize: '10px',
|
||||
color: '#ffff88',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(
|
||||
px + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
py,
|
||||
`邻接: ${adjacentNames}`,
|
||||
{
|
||||
fontSize: "10px",
|
||||
color: "#ffff88",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getItemColor(itemId: string): number {
|
||||
const hash = itemId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
const colors = [0x4488cc, 0xcc8844, 0x44cc88, 0xcc4488, 0x8844cc, 0x44cccc];
|
||||
return colors[hash % colors.length];
|
||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||
}
|
||||
|
||||
private createControls(): void {
|
||||
const { width, height } = this.scale;
|
||||
|
||||
this.createButton('返回菜单', 100, 40, async () => {
|
||||
await this.sceneController.launch('IndexScene');
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
},
|
||||
});
|
||||
|
||||
this.createButton('清空', width - 260, 40, async () => {
|
||||
this.inventory = createGridInventory<GameItemMeta>(this.GRID_WIDTH, this.GRID_HEIGHT);
|
||||
await this.sceneController.restart();
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "清空",
|
||||
x: width - 260,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
this.inventory = createGridInventory<GameItemMeta>(
|
||||
GRID_CONFIG.WIDTH,
|
||||
GRID_CONFIG.HEIGHT,
|
||||
);
|
||||
await this.sceneController.restart();
|
||||
},
|
||||
});
|
||||
|
||||
this.createButton('随机填充', width - 130, 40, async () => {
|
||||
this.randomFill();
|
||||
await this.sceneController.restart();
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "随机填充",
|
||||
x: width - 130,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
this.randomFill();
|
||||
await this.sceneController.restart();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private randomFill(): void {
|
||||
this.inventory = createGridInventory<GameItemMeta>(this.GRID_WIDTH, this.GRID_HEIGHT);
|
||||
this.inventory = createGridInventory<GameItemMeta>(
|
||||
GRID_CONFIG.WIDTH,
|
||||
GRID_CONFIG.HEIGHT,
|
||||
);
|
||||
const items = data.desert.items;
|
||||
|
||||
let itemIndex = 0;
|
||||
for (let y = 0; y < this.GRID_HEIGHT && itemIndex < items.length; y++) {
|
||||
for (let x = 0; x < this.GRID_WIDTH && itemIndex < items.length; x++) {
|
||||
for (let y = 0; y < GRID_CONFIG.HEIGHT && itemIndex < items.length; y++) {
|
||||
for (let x = 0; x < GRID_CONFIG.WIDTH && itemIndex < items.length; x++) {
|
||||
const itemData = items[itemIndex];
|
||||
const shape = parseShapeString(itemData.shape);
|
||||
|
||||
@@ -190,8 +261,12 @@ export class GridViewerScene extends ReactiveScene {
|
||||
|
||||
let valid = true;
|
||||
for (const cell of occupiedCells) {
|
||||
const [cx, cy] = cell.split(',').map(Number);
|
||||
if (cx >= this.GRID_WIDTH || cy >= this.GRID_HEIGHT || this.inventory.occupiedCells.has(cell as `${number},${number}`)) {
|
||||
const [cx, cy] = cell.split(",").map(Number);
|
||||
if (
|
||||
cx >= GRID_CONFIG.WIDTH ||
|
||||
cy >= GRID_CONFIG.HEIGHT ||
|
||||
this.inventory.occupiedCells.has(cell as `${number},${number}`)
|
||||
) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
@@ -201,7 +276,12 @@ export class GridViewerScene extends ReactiveScene {
|
||||
const item: InventoryItem<GameItemMeta> = {
|
||||
id: `item-${itemIndex}`,
|
||||
shape,
|
||||
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
|
||||
transform: {
|
||||
offset: { x, y },
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
},
|
||||
meta: { itemData, shape },
|
||||
};
|
||||
placeItem(this.inventory, item);
|
||||
@@ -210,30 +290,4 @@ export class GridViewerScene extends ReactiveScene {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createButton(label: string, x: number, y: number, onClick: () => void): void {
|
||||
const buttonWidth = 120;
|
||||
const buttonHeight = 36;
|
||||
|
||||
const bg = this.add.rectangle(x, y, buttonWidth, buttonHeight, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
const text = this.add.text(x, y, label, {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x555588);
|
||||
text.setScale(1.05);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x444466);
|
||||
text.setScale(1);
|
||||
});
|
||||
|
||||
bg.on('pointerdown', onClick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Phaser from 'phaser';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG } from "@/config";
|
||||
|
||||
export class IndexScene extends ReactiveScene {
|
||||
constructor() {
|
||||
super('IndexScene');
|
||||
super("IndexScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
@@ -13,24 +15,32 @@ export class IndexScene extends ReactiveScene {
|
||||
const centerY = height / 2;
|
||||
|
||||
// Title
|
||||
this.add.text(centerX, centerY - 150, 'Slay-the-Spire-Like Viewer', {
|
||||
fontSize: '36px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(centerX, centerY - 150, "Slay-the-Spire-Like Viewer", {
|
||||
fontSize: "36px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// Subtitle
|
||||
this.add.text(centerX, centerY - 100, 'Choose a viewer to explore:', {
|
||||
fontSize: '18px',
|
||||
color: '#aaaaaa',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(centerX, centerY - 100, "Choose a viewer to explore:", {
|
||||
fontSize: "18px",
|
||||
color: "#aaaaaa",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
// Buttons
|
||||
const buttons = [
|
||||
{ label: '开始游戏', scene: 'GameFlowScene', y: centerY - 70 },
|
||||
{ label: 'Map Viewer', scene: 'MapViewerScene', y: centerY },
|
||||
{ label: 'Grid Inventory Viewer', scene: 'GridViewerScene', y: centerY + 70 },
|
||||
{ label: 'Shape Viewer', scene: 'ShapeViewerScene', y: centerY + 140 },
|
||||
{ label: "开始游戏", scene: "GameFlowScene", y: centerY - 70 },
|
||||
{ label: "Map Viewer", scene: "MapViewerScene", y: centerY },
|
||||
{
|
||||
label: "Grid Inventory Viewer",
|
||||
scene: "GridViewerScene",
|
||||
y: centerY + 70,
|
||||
},
|
||||
{ label: "Shape Viewer", scene: "ShapeViewerScene", y: centerY + 140 },
|
||||
];
|
||||
|
||||
for (const btn of buttons) {
|
||||
@@ -38,34 +48,22 @@ export class IndexScene extends ReactiveScene {
|
||||
}
|
||||
}
|
||||
|
||||
private createButton(label: string, targetScene: string, x: number, y: number): void {
|
||||
const buttonWidth = 300;
|
||||
const buttonHeight = 50;
|
||||
|
||||
// Background
|
||||
const bg = this.add.rectangle(x, y, buttonWidth, buttonHeight, 0x333355)
|
||||
.setStrokeStyle(2, 0x6666aa)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
// Text
|
||||
const text = this.add.text(x, y, label, {
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Hover effects
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x444477);
|
||||
text.setScale(1.05);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x333355);
|
||||
text.setScale(1);
|
||||
});
|
||||
|
||||
bg.on('pointerdown', async () => {
|
||||
await this.sceneController.launch(targetScene);
|
||||
private createButton(
|
||||
label: string,
|
||||
targetScene: string,
|
||||
x: number,
|
||||
y: number,
|
||||
): void {
|
||||
createButton({
|
||||
scene: this,
|
||||
label,
|
||||
x,
|
||||
y,
|
||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch(targetScene);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
import Phaser from 'phaser';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
import { createRNG } from 'boardgame-core';
|
||||
import { generatePointCrawlMap, data, type PointCrawlMap, type MapNode, MapNodeType } from 'boardgame-core/samples/slay-the-spire-like';
|
||||
|
||||
const NODE_COLORS: Record<MapNodeType, number> = {
|
||||
[MapNodeType.Start]: 0x44aa44,
|
||||
[MapNodeType.End]: 0xcc8844,
|
||||
[MapNodeType.Minion]: 0xcc4444,
|
||||
[MapNodeType.Elite]: 0xcc44cc,
|
||||
[MapNodeType.Event]: 0xaaaa44,
|
||||
[MapNodeType.Camp]: 0x44cccc,
|
||||
[MapNodeType.Shop]: 0x4488cc,
|
||||
[MapNodeType.Curio]: 0x8844cc,
|
||||
};
|
||||
|
||||
const NODE_LABELS: Record<MapNodeType, string> = {
|
||||
[MapNodeType.Start]: '起点',
|
||||
[MapNodeType.End]: '终点',
|
||||
[MapNodeType.Minion]: '战斗',
|
||||
[MapNodeType.Elite]: '精英',
|
||||
[MapNodeType.Event]: '事件',
|
||||
[MapNodeType.Camp]: '篝火',
|
||||
[MapNodeType.Shop]: '商店',
|
||||
[MapNodeType.Curio]: '奇遇',
|
||||
};
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG, MAP_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
||||
import { createRNG } from "boardgame-core";
|
||||
import {
|
||||
generatePointCrawlMap,
|
||||
data,
|
||||
type PointCrawlMap,
|
||||
type MapNode,
|
||||
type MapNodeType,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class MapViewerScene extends ReactiveScene {
|
||||
private map: PointCrawlMap | null = null;
|
||||
private seed: number = Date.now();
|
||||
|
||||
// Layout constants
|
||||
private readonly LAYER_HEIGHT = 110;
|
||||
private readonly NODE_SPACING = 140;
|
||||
private readonly NODE_RADIUS = 28;
|
||||
|
||||
// Scroll state
|
||||
private mapContainer!: Phaser.GameObjects.Container;
|
||||
private isDragging = false;
|
||||
@@ -44,14 +25,10 @@ export class MapViewerScene extends ReactiveScene {
|
||||
|
||||
// Fixed UI (always visible, not scrolled)
|
||||
private titleText!: Phaser.GameObjects.Text;
|
||||
private backButtonBg!: Phaser.GameObjects.Rectangle;
|
||||
private backButtonText!: Phaser.GameObjects.Text;
|
||||
private regenButtonBg!: Phaser.GameObjects.Rectangle;
|
||||
private regenButtonText!: Phaser.GameObjects.Text;
|
||||
private legendContainer!: Phaser.GameObjects.Container;
|
||||
|
||||
constructor() {
|
||||
super('MapViewerScene');
|
||||
super("MapViewerScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
@@ -61,86 +38,77 @@ export class MapViewerScene extends ReactiveScene {
|
||||
}
|
||||
|
||||
private drawFixedUI(): void {
|
||||
const { width } = this.scale;
|
||||
const { width, height } = this.scale;
|
||||
|
||||
// Title
|
||||
this.titleText = this.add.text(width / 2, 30, '', {
|
||||
fontSize: '24px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5).setDepth(100);
|
||||
this.titleText = this.add
|
||||
.text(width / 2, 30, "", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(100);
|
||||
|
||||
// Back button
|
||||
this.backButtonBg = this.add.rectangle(100, 40, 140, 36, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setDepth(100);
|
||||
this.backButtonText = this.add.text(100, 40, '返回菜单', {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5).setDepth(100);
|
||||
|
||||
this.backButtonBg.on('pointerover', () => {
|
||||
this.backButtonBg.setFillStyle(0x555588);
|
||||
this.backButtonText.setScale(1.05);
|
||||
});
|
||||
this.backButtonBg.on('pointerout', () => {
|
||||
this.backButtonBg.setFillStyle(0x444466);
|
||||
this.backButtonText.setScale(1);
|
||||
});
|
||||
this.backButtonBg.on('pointerdown', async () => {
|
||||
await this.sceneController.launch('IndexScene');
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
},
|
||||
depth: 100,
|
||||
});
|
||||
|
||||
// Regenerate button
|
||||
this.regenButtonBg = this.add.rectangle(width - 120, 40, 140, 36, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setDepth(100);
|
||||
this.regenButtonText = this.add.text(width - 120, 40, '重新生成', {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5).setDepth(100);
|
||||
|
||||
this.regenButtonBg.on('pointerover', () => {
|
||||
this.regenButtonBg.setFillStyle(0x555588);
|
||||
this.regenButtonText.setScale(1.05);
|
||||
});
|
||||
this.regenButtonBg.on('pointerout', () => {
|
||||
this.regenButtonBg.setFillStyle(0x444466);
|
||||
this.regenButtonText.setScale(1);
|
||||
});
|
||||
this.regenButtonBg.on('pointerdown', () => {
|
||||
this.seed = Date.now();
|
||||
this.mapContainer.destroy();
|
||||
this.drawMap();
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "重新生成",
|
||||
x: width - 120,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.seed = Date.now();
|
||||
this.mapContainer.destroy();
|
||||
this.drawMap();
|
||||
},
|
||||
depth: 100,
|
||||
});
|
||||
|
||||
// Legend (bottom-left, fixed)
|
||||
this.legendContainer = this.add.container(20, this.scale.height - 180).setDepth(100);
|
||||
this.legendContainer = this.add.container(20, height - 180).setDepth(100);
|
||||
const legendBg = this.add.rectangle(75, 80, 150, 160, 0x222222, 0.8);
|
||||
this.legendContainer.add(legendBg);
|
||||
|
||||
this.legendContainer.add(
|
||||
this.add.text(10, 5, '图例:', { fontSize: '14px', color: '#ffffff', fontStyle: 'bold' })
|
||||
this.add.text(10, 5, "图例:", {
|
||||
fontSize: "14px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
}),
|
||||
);
|
||||
|
||||
let offsetY = 30;
|
||||
for (const [type, color] of Object.entries(NODE_COLORS)) {
|
||||
this.legendContainer.add(this.add.circle(20, offsetY, 8, color));
|
||||
this.legendContainer.add(
|
||||
this.add.circle(20, offsetY, 8, color)
|
||||
);
|
||||
this.legendContainer.add(
|
||||
this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' })
|
||||
this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], {
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
}),
|
||||
);
|
||||
offsetY += 20;
|
||||
}
|
||||
|
||||
// Hint text
|
||||
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图 (从左到右)', {
|
||||
fontSize: '14px',
|
||||
color: '#888888',
|
||||
}).setOrigin(0.5).setDepth(100);
|
||||
this.add
|
||||
.text(width / 2, height - 20, "拖拽滚动查看地图 (从左到右)", {
|
||||
fontSize: "14px",
|
||||
color: "#888888",
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setDepth(100);
|
||||
}
|
||||
|
||||
private drawMap(): void {
|
||||
@@ -148,21 +116,28 @@ export class MapViewerScene extends ReactiveScene {
|
||||
this.map = generatePointCrawlMap(rng, data.desert.encounters);
|
||||
|
||||
const { width, height } = this.scale;
|
||||
const {
|
||||
LAYER_HEIGHT,
|
||||
NODE_SPACING,
|
||||
NODE_RADIUS,
|
||||
TOTAL_LAYERS,
|
||||
MAX_NODES_PER_LAYER,
|
||||
} = MAP_CONFIG;
|
||||
|
||||
// Update title
|
||||
this.titleText.setText(`Map Viewer (Seed: ${this.seed})`);
|
||||
|
||||
// Calculate map bounds (left-to-right: layers along X, nodes along Y)
|
||||
const maxLayer = 9; // TOTAL_LAYERS - 1 (10 layers: 0-9)
|
||||
const maxNodesInLayer = 5; // widest layer (settlement has 4 nodes)
|
||||
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
|
||||
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
||||
// Calculate map bounds
|
||||
const maxLayer = TOTAL_LAYERS - 1;
|
||||
const mapWidth = maxLayer * LAYER_HEIGHT + 200;
|
||||
const mapHeight = (MAX_NODES_PER_LAYER - 1) * NODE_SPACING + 200;
|
||||
|
||||
// Create scrollable container
|
||||
this.mapContainer = this.add.container(width / 2, height / 2);
|
||||
|
||||
// Background panel for the map area
|
||||
const bg = this.add.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5)
|
||||
const bg = this.add
|
||||
.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5)
|
||||
.setOrigin(0.5);
|
||||
this.mapContainer.add(bg);
|
||||
|
||||
@@ -193,65 +168,77 @@ export class MapViewerScene extends ReactiveScene {
|
||||
|
||||
// Node circle
|
||||
graphics.fillStyle(color);
|
||||
graphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||
graphics.fillCircle(posX, posY, NODE_RADIUS);
|
||||
graphics.lineStyle(2, 0xffffff);
|
||||
graphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||
graphics.strokeCircle(posX, posY, NODE_RADIUS);
|
||||
|
||||
// Node label
|
||||
const label = NODE_LABELS[node.type as MapNodeType] ?? node.type;
|
||||
this.mapContainer.add(
|
||||
this.add.text(posX, posY, label, {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5)
|
||||
this.add
|
||||
.text(posX, posY, label, {
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
})
|
||||
.setOrigin(0.5),
|
||||
);
|
||||
|
||||
// Encounter name (if available)
|
||||
if (node.encounter) {
|
||||
this.mapContainer.add(
|
||||
this.add.text(posX, posY + this.NODE_RADIUS + 12, node.encounter.name, {
|
||||
fontSize: '10px',
|
||||
color: '#cccccc',
|
||||
}).setOrigin(0.5)
|
||||
this.add
|
||||
.text(posX, posY + NODE_RADIUS + 12, node.encounter.name, {
|
||||
fontSize: "10px",
|
||||
color: "#cccccc",
|
||||
})
|
||||
.setOrigin(0.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup drag-to-scroll
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
// Setup drag-to-scroll with disposables cleanup
|
||||
const onPointerDown = (pointer: Phaser.Input.Pointer) => {
|
||||
this.isDragging = true;
|
||||
this.dragStartX = pointer.x;
|
||||
this.dragStartY = pointer.y;
|
||||
this.dragStartContainerX = this.mapContainer.x;
|
||||
this.dragStartContainerY = this.mapContainer.y;
|
||||
});
|
||||
};
|
||||
|
||||
this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
|
||||
const onPointerMove = (pointer: Phaser.Input.Pointer) => {
|
||||
if (!this.isDragging) return;
|
||||
this.mapContainer.x = this.dragStartContainerX + (pointer.x - this.dragStartX);
|
||||
this.mapContainer.y = this.dragStartContainerY + (pointer.y - this.dragStartY);
|
||||
});
|
||||
this.mapContainer.x =
|
||||
this.dragStartContainerX + (pointer.x - this.dragStartX);
|
||||
this.mapContainer.y =
|
||||
this.dragStartContainerY + (pointer.y - this.dragStartY);
|
||||
};
|
||||
|
||||
this.input.on('pointerup', () => {
|
||||
const onPointerUp = () => {
|
||||
this.isDragging = false;
|
||||
});
|
||||
};
|
||||
|
||||
this.input.on('pointerout', () => {
|
||||
this.isDragging = false;
|
||||
this.input.on("pointerdown", onPointerDown);
|
||||
this.input.on("pointermove", onPointerMove);
|
||||
this.input.on("pointerup", onPointerUp);
|
||||
this.input.on("pointerout", onPointerUp);
|
||||
|
||||
this.disposables.add(() => {
|
||||
this.input.off("pointerdown", onPointerDown);
|
||||
this.input.off("pointermove", onPointerMove);
|
||||
this.input.off("pointerup", onPointerUp);
|
||||
this.input.off("pointerout", onPointerUp);
|
||||
});
|
||||
}
|
||||
|
||||
private getNodeX(node: MapNode): number {
|
||||
// Layers go left-to-right along X axis
|
||||
return -500 + node.layerIndex * this.LAYER_HEIGHT;
|
||||
return -500 + node.layerIndex * MAP_CONFIG.LAYER_HEIGHT;
|
||||
}
|
||||
|
||||
private getNodeY(node: MapNode): number {
|
||||
// Nodes within a layer are spread vertically along Y axis
|
||||
const layer = this.map!.layers[node.layerIndex];
|
||||
const nodeIndex = layer.nodeIds.indexOf(node.id);
|
||||
const totalNodes = layer.nodeIds.length;
|
||||
const layerHeight = (totalNodes - 1) * this.NODE_SPACING;
|
||||
return -layerHeight / 2 + nodeIndex * this.NODE_SPACING;
|
||||
const layerHeight = (totalNodes - 1) * MAP_CONFIG.NODE_SPACING;
|
||||
return -layerHeight / 2 + nodeIndex * MAP_CONFIG.NODE_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import Phaser from 'phaser';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
import { MutableSignal } from 'boardgame-core';
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG, GRID_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
||||
import { MutableSignal } from "boardgame-core";
|
||||
import {
|
||||
resolveEncounter,
|
||||
removeItem,
|
||||
type RunState,
|
||||
type EncounterResult,
|
||||
type MapNodeType,
|
||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
||||
import { InventoryWidget } from '@/widgets/InventoryWidget';
|
||||
type MapNode,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { InventoryWidget } from "@/widgets/InventoryWidget";
|
||||
|
||||
/**
|
||||
* 占位符遭遇场景
|
||||
@@ -20,7 +23,7 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
private inventoryWidget: InventoryWidget | null = null;
|
||||
|
||||
constructor(gameState: MutableSignal<RunState>) {
|
||||
super('PlaceholderEncounterScene');
|
||||
super("PlaceholderEncounterScene");
|
||||
this.gameState = gameState;
|
||||
}
|
||||
|
||||
@@ -31,9 +34,9 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
|
||||
const gridCols = state.inventory.width;
|
||||
const gridRows = state.inventory.height;
|
||||
const cellSize = 80;
|
||||
const gridW = gridCols * cellSize + (gridCols - 1) * 2;
|
||||
const gridH = gridRows * cellSize + (gridRows - 1) * 2;
|
||||
const cellSize = GRID_CONFIG.WIDGET_CELL_SIZE;
|
||||
const gridW = gridCols * cellSize + (gridCols - 1) * GRID_CONFIG.GRID_GAP;
|
||||
const gridH = gridRows * cellSize + (gridRows - 1) * GRID_CONFIG.GRID_GAP;
|
||||
const leftPanelW = gridW + 40;
|
||||
|
||||
this.inventoryWidget = new InventoryWidget({
|
||||
@@ -42,34 +45,51 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
x: 60,
|
||||
y: (height - gridH) / 2 + 20,
|
||||
cellSize,
|
||||
gridGap: 2,
|
||||
gridGap: GRID_CONFIG.GRID_GAP,
|
||||
});
|
||||
|
||||
this.cameras.main.setBounds(0, 0, width, height);
|
||||
this.cameras.main.setScroll(0, 0);
|
||||
|
||||
// Panel background
|
||||
this.add.rectangle(
|
||||
60 + leftPanelW / 2, this.inventoryWidgetY(gridH),
|
||||
leftPanelW + 10, gridH + 50,
|
||||
0x111122, 0.9
|
||||
).setStrokeStyle(2, 0x5555aa);
|
||||
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);
|
||||
this.add
|
||||
.text(60 + gridW / 2, (height - gridH) / 2, "背包", {
|
||||
fontSize: "22px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
const node = state.map.nodes.get(state.currentNodeId);
|
||||
if (!node || !node.encounter) {
|
||||
const rightX = leftPanelW + 80;
|
||||
this.add.text(rightX + 300, height / 2, '没有遭遇数据', {
|
||||
fontSize: '24px', color: '#ff4444',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(rightX + 300, height / 2, "没有遭遇数据", {
|
||||
fontSize: "24px",
|
||||
color: "#ff4444",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawRightPanel(node, leftPanelW, width, height);
|
||||
this.drawRightPanel(
|
||||
node as MapNode & { encounter: { name: string; description: string } },
|
||||
leftPanelW,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
private inventoryWidgetY(gridH: number): number {
|
||||
@@ -77,11 +97,16 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
return (height - gridH) / 2 + 20 + gridH / 2;
|
||||
}
|
||||
|
||||
private drawRightPanel(node: any, leftPanelW: number, width: number, height: number): void {
|
||||
private drawRightPanel(
|
||||
node: MapNode & { encounter: { name: string; description: string } },
|
||||
leftPanelW: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): void {
|
||||
const encounter = {
|
||||
type: node.type as MapNodeType,
|
||||
name: node.encounter.name as string,
|
||||
description: node.encounter.description as string,
|
||||
name: node.encounter.name,
|
||||
description: node.encounter.description,
|
||||
};
|
||||
const nodeId = node.id as string;
|
||||
|
||||
@@ -90,66 +115,85 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
const cx = rightX + rightW / 2;
|
||||
const cy = height / 2;
|
||||
|
||||
this.add.text(cx, cy - 180, '遭遇', {
|
||||
fontSize: '36px', color: '#fff', fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy - 180, "遭遇", {
|
||||
fontSize: "36px",
|
||||
color: "#fff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
const typeLabel = this.getTypeLabel(encounter.type);
|
||||
const badgeColor = this.getTypeColor(encounter.type);
|
||||
this.add.rectangle(cx, cy - 110, 140, 40, badgeColor);
|
||||
this.add.text(cx, cy - 110, typeLabel, {
|
||||
fontSize: '18px', color: '#fff', fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy - 110, typeLabel, {
|
||||
fontSize: "18px",
|
||||
color: "#fff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, cy - 50, encounter.name, {
|
||||
fontSize: '28px', color: '#fff',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy - 50, encounter.name, {
|
||||
fontSize: "28px",
|
||||
color: "#fff",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, cy + 10, encounter.description || '(暂无描述)', {
|
||||
fontSize: '18px', color: '#bbb',
|
||||
wordWrap: { width: rightW - 40 }, align: 'center',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy + 10, encounter.description || "(暂无描述)", {
|
||||
fontSize: "18px",
|
||||
color: "#bbb",
|
||||
wordWrap: { width: rightW - 40 },
|
||||
align: "center",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, cy + 80, `节点: ${nodeId}`, {
|
||||
fontSize: '14px', color: '#666',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy + 80, `节点: ${nodeId}`, {
|
||||
fontSize: "14px",
|
||||
color: "#666",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, cy + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
|
||||
fontSize: '14px', color: '#ff8844', fontStyle: 'italic',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(cx, cy + 130, "(此为占位符遭遇,后续将替换为真实遭遇场景)", {
|
||||
fontSize: "14px",
|
||||
color: "#ff8844",
|
||||
fontStyle: "italic",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createButton('完成遭遇', cx, cy + 200, 220, 50, async () => {
|
||||
await this.completeEncounter();
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "完成遭遇",
|
||||
x: cx,
|
||||
y: cy + 200,
|
||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
||||
onClick: async () => {
|
||||
await this.completeEncounter();
|
||||
},
|
||||
});
|
||||
this.createButton('暂不处理', cx, cy + 270, 220, 40, async () => {
|
||||
await this.sceneController.launch('GameFlowScene');
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "暂不处理",
|
||||
x: cx,
|
||||
y: cy + 270,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("GameFlowScene");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getTypeLabel(type: MapNodeType): string {
|
||||
const m: Record<MapNodeType, string> = {
|
||||
start: '起点', end: '终点', minion: '战斗', elite: '精英战斗',
|
||||
event: '事件', camp: '营地', shop: '商店', curio: '奇遇',
|
||||
};
|
||||
return m[type] ?? type;
|
||||
return NODE_LABELS[type] ?? type;
|
||||
}
|
||||
|
||||
private getTypeColor(type: MapNodeType): number {
|
||||
const m: Record<MapNodeType, number> = {
|
||||
start: 0x44aa44, end: 0xcc8844, minion: 0xcc4444, elite: 0xcc44cc,
|
||||
event: 0xaaaa44, camp: 0x44cccc, shop: 0x4488cc, curio: 0x8844cc,
|
||||
};
|
||||
return m[type] ?? 0x888888;
|
||||
}
|
||||
|
||||
private createButton(label: string, x: number, y: number, w: number, h: number, onClick: () => void): void {
|
||||
const bg = this.add.rectangle(x, y, w, h, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa).setInteractive({ useHandCursor: true });
|
||||
const txt = this.add.text(x, y, label, { fontSize: '16px', color: '#fff' }).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => { bg.setFillStyle(0x555588); txt.setScale(1.05); });
|
||||
bg.on('pointerout', () => { bg.setFillStyle(0x444466); txt.setScale(1); });
|
||||
bg.on('pointerdown', onClick);
|
||||
return NODE_COLORS[type] ?? 0x888888;
|
||||
}
|
||||
|
||||
private async completeEncounter(): Promise<void> {
|
||||
@@ -168,18 +212,24 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||
|
||||
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
||||
resolveEncounter(state, result);
|
||||
await this.sceneController.launch('GameFlowScene');
|
||||
await this.sceneController.launch("GameFlowScene");
|
||||
}
|
||||
|
||||
private generatePlaceholderResult(type: MapNodeType): EncounterResult {
|
||||
switch (type) {
|
||||
case 'minion': return { hpLost: 8, goldEarned: 15 };
|
||||
case 'elite': return { hpLost: 15, goldEarned: 30 };
|
||||
case 'camp': return { hpGained: 15 };
|
||||
case 'shop': return { goldEarned: 0 };
|
||||
case 'curio':
|
||||
case 'event': return { goldEarned: 20 };
|
||||
default: return {};
|
||||
case "minion":
|
||||
return { hpLost: 8, goldEarned: 15 };
|
||||
case "elite":
|
||||
return { hpLost: 15, goldEarned: 30 };
|
||||
case "camp":
|
||||
return { hpGained: 15 };
|
||||
case "shop":
|
||||
return { goldEarned: 0 };
|
||||
case "curio":
|
||||
case "event":
|
||||
return { goldEarned: 20 };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import Phaser from 'phaser';
|
||||
import { ReactiveScene } from 'boardgame-phaser';
|
||||
import { parseShapeString, data, type ParsedShape } from 'boardgame-core/samples/slay-the-spire-like';
|
||||
import Phaser from "phaser";
|
||||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG, SHAPE_CONFIG, NODE_LABELS, NODE_COLORS } from "@/config";
|
||||
import {
|
||||
parseShapeString,
|
||||
data,
|
||||
type ParsedShape,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
export class ShapeViewerScene extends ReactiveScene {
|
||||
private readonly CELL_SIZE = 40;
|
||||
private readonly ITEMS_PER_ROW = 4;
|
||||
|
||||
constructor() {
|
||||
super('ShapeViewerScene');
|
||||
super("ShapeViewerScene");
|
||||
}
|
||||
|
||||
create(): void {
|
||||
@@ -19,13 +22,15 @@ export class ShapeViewerScene extends ReactiveScene {
|
||||
private drawShapeViewer(): void {
|
||||
this.children.removeAll();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
const { width } = this.scale;
|
||||
|
||||
this.add.text(width / 2, 30, 'Shape Viewer - Item Shapes', {
|
||||
fontSize: '24px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(width / 2, 30, "Shape Viewer - Item Shapes", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.drawAllShapes();
|
||||
}
|
||||
@@ -33,117 +38,127 @@ export class ShapeViewerScene extends ReactiveScene {
|
||||
private drawAllShapes(): void {
|
||||
const { width } = this.scale;
|
||||
const startY = 80;
|
||||
const spacingX = 220;
|
||||
const spacingY = 140;
|
||||
const { SPACING_X, SPACING_Y, ITEMS_PER_ROW, MAX_ITEMS } = SHAPE_CONFIG;
|
||||
|
||||
const itemsToShow = data.desert.items.slice(0, 12);
|
||||
const itemsToShow = data.desert.items.slice(0, MAX_ITEMS);
|
||||
|
||||
for (let i = 0; i < itemsToShow.length; i++) {
|
||||
const itemData = itemsToShow[i];
|
||||
const shape = parseShapeString(itemData.shape);
|
||||
|
||||
const col = i % this.ITEMS_PER_ROW;
|
||||
const row = Math.floor(i / this.ITEMS_PER_ROW);
|
||||
const col = i % ITEMS_PER_ROW;
|
||||
const row = Math.floor(i / ITEMS_PER_ROW);
|
||||
|
||||
const x = 60 + col * spacingX;
|
||||
const y = startY + row * spacingY;
|
||||
const x = 60 + col * SPACING_X;
|
||||
const y = startY + row * SPACING_Y;
|
||||
|
||||
this.drawSingleShape(x, y, itemData, shape);
|
||||
}
|
||||
}
|
||||
|
||||
private drawSingleShape(startX: number, startY: number, itemData: any, shape: ParsedShape): void {
|
||||
private drawSingleShape(
|
||||
startX: number,
|
||||
startY: number,
|
||||
itemData: any,
|
||||
shape: ParsedShape,
|
||||
): void {
|
||||
const graphics = this.add.graphics();
|
||||
const { CELL_SIZE } = SHAPE_CONFIG;
|
||||
|
||||
const shapeWidth = shape.width * this.CELL_SIZE;
|
||||
const shapeHeight = shape.height * this.CELL_SIZE;
|
||||
const shapeWidth = shape.width * CELL_SIZE;
|
||||
const shapeHeight = shape.height * CELL_SIZE;
|
||||
|
||||
this.add.text(startX + shapeWidth / 2, startY - 20, itemData.name, {
|
||||
fontSize: '14px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(startX + shapeWidth / 2, startY - 20, itemData.name, {
|
||||
fontSize: "14px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
for (let y = 0; y < shape.height; y++) {
|
||||
for (let x = 0; x < shape.width; x++) {
|
||||
if (shape.grid[y]?.[x]) {
|
||||
const px = startX + x * this.CELL_SIZE;
|
||||
const py = startY + y * this.CELL_SIZE;
|
||||
const px = startX + x * CELL_SIZE;
|
||||
const py = startY + y * CELL_SIZE;
|
||||
|
||||
const isOrigin = x === shape.originX && y === shape.originY;
|
||||
const color = isOrigin ? 0x88cc44 : 0x4488cc;
|
||||
|
||||
graphics.fillStyle(color);
|
||||
graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
|
||||
graphics.fillRect(px + 1, py + 1, CELL_SIZE - 2, CELL_SIZE - 2);
|
||||
graphics.lineStyle(2, 0xffffff);
|
||||
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
|
||||
graphics.strokeRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
|
||||
if (isOrigin) {
|
||||
this.add.text(px + this.CELL_SIZE / 2, py + this.CELL_SIZE / 2, 'O', {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(px + CELL_SIZE / 2, py + CELL_SIZE / 2, "O", {
|
||||
fontSize: "16px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 10, `形状: ${itemData.shape}`, {
|
||||
fontSize: '11px',
|
||||
color: '#aaaaaa',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(
|
||||
startX + shapeWidth / 2,
|
||||
startY + shapeHeight + 10,
|
||||
`形状: ${itemData.shape}`,
|
||||
{
|
||||
fontSize: "11px",
|
||||
color: "#aaaaaa",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 28,
|
||||
`类型: ${itemData.type} | 费用: ${itemData.costCount} ${itemData.costType}`, {
|
||||
fontSize: '11px',
|
||||
color: '#cccccc',
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(
|
||||
startX + shapeWidth / 2,
|
||||
startY + shapeHeight + 28,
|
||||
`类型: ${itemData.type} | 费用: ${itemData.costCount} ${itemData.costType}`,
|
||||
{
|
||||
fontSize: "11px",
|
||||
color: "#cccccc",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 46, itemData.desc, {
|
||||
fontSize: '10px',
|
||||
color: '#888888',
|
||||
wordWrap: { width: shapeWidth },
|
||||
}).setOrigin(0.5);
|
||||
this.add
|
||||
.text(startX + shapeWidth / 2, startY + shapeHeight + 46, itemData.desc, {
|
||||
fontSize: "10px",
|
||||
color: "#888888",
|
||||
wordWrap: { width: shapeWidth },
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
|
||||
private createControls(): void {
|
||||
const { width, height } = this.scale;
|
||||
|
||||
this.createButton('返回菜单', 100, height - 40, async () => {
|
||||
await this.sceneController.launch('IndexScene');
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: height - 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
},
|
||||
});
|
||||
|
||||
this.add.text(width / 2, height - 40,
|
||||
`Showing first 12 items | Green = Origin | Blue = Normal`, {
|
||||
fontSize: '14px',
|
||||
color: '#aaaaaa',
|
||||
}).setOrigin(0.5);
|
||||
}
|
||||
|
||||
private createButton(label: string, x: number, y: number, onClick: () => void): void {
|
||||
const buttonWidth = 120;
|
||||
const buttonHeight = 36;
|
||||
|
||||
const bg = this.add.rectangle(x, y, buttonWidth, buttonHeight, 0x444466)
|
||||
.setStrokeStyle(2, 0x7777aa)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
const text = this.add.text(x, y, label, {
|
||||
fontSize: '16px',
|
||||
color: '#ffffff',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x555588);
|
||||
text.setScale(1.05);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x444466);
|
||||
text.setScale(1);
|
||||
});
|
||||
|
||||
bg.on('pointerdown', onClick);
|
||||
this.add
|
||||
.text(
|
||||
width / 2,
|
||||
height - 40,
|
||||
`Showing first ${SHAPE_CONFIG.MAX_ITEMS} items | Green = Origin | Blue = Normal`,
|
||||
{
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
},
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user