Compare commits

...

2 Commits

Author SHA1 Message Date
hypercross c639218b53 refactor: viewr 2026-04-13 14:57:00 +08:00
hypercross e91e8d2ab7 feat: sts-like-viewer 2026-04-13 12:38:41 +08:00
12 changed files with 896 additions and 0 deletions

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>STS-Like Viewer</title>
</head>
<body>
<div id="ui-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,26 @@
{
"name": "sts-like-viewer",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@preact/signals-core": "^1.5.1",
"boardgame-core": "link:../../../boardgame-core",
"boardgame-phaser": "workspace:*",
"mutative": "^1.3.0",
"phaser": "^3.80.1",
"preact": "^10.19.3"
},
"devDependencies": {
"@preact/preset-vite": "^2.8.1",
"@preact/signals": "^2.9.0",
"@tailwindcss/vite": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.1.0"
}
}

View File

@ -0,0 +1,11 @@
import { h } from 'preact';
import { GameUI } from 'boardgame-phaser';
import './style.css';
import App from "@/ui/App";
const ui = new GameUI({
container: document.getElementById('ui-root')!,
root: <App/>,
});
ui.mount();

View File

@ -0,0 +1,256 @@
import Phaser from 'phaser';
import {
createGridInventory,
placeItem,
getAdjacentItems,
parseShapeString,
heroItemFighter1Data,
type GridInventory,
type InventoryItem,
} from 'boardgame-core/samples/slay-the-spire-like';
import { ReactiveScene } from 'boardgame-phaser';
export class GridViewerScene extends ReactiveScene {
private inventory: GridInventory;
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(this.GRID_WIDTH, this.GRID_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;
// Place some sample items
this.placeSampleItems();
// Draw grid
this.drawGrid();
// Draw items
this.drawItems();
// Title
this.add.text(width / 2, 30, 'Grid Inventory Viewer (4x6)', {
fontSize: '24px',
color: '#ffffff',
fontStyle: 'bold',
}).setOrigin(0.5);
// Controls
this.createControls();
// Info text
this.add.text(width / 2, height - 40, 'Hover over cells to see item details', {
fontSize: '14px',
color: '#aaaaaa',
}).setOrigin(0.5);
}
private placeSampleItems(): void {
// Place a few items from heroItemFighter1Data
const sampleItems = [
{ index: 0, x: 0, y: 0 }, // 剑
{ index: 3, x: 3, y: 0 }, // 短刀
{ index: 6, x: 0, y: 2 }, // 盾
{ index: 12, x: 3, y: 2 }, // 披风
];
for (const { index, x, y } of sampleItems) {
const data = heroItemFighter1Data[index];
const shape = parseShapeString(data.shape);
const item: InventoryItem = {
id: `item-${index}`,
shape,
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
meta: { name: data.name, data },
};
placeItem(this.inventory, item);
}
}
private drawGrid(): void {
const graphics = this.add.graphics();
// Draw cells
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;
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.lineStyle(1, 0x555577);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
}
}
}
private drawItems(): void {
for (const [itemId, item] of this.inventory.items) {
const shape = item.shape;
const transform = item.transform;
// Get occupied cells
const cells: { x: number; y: number }[] = [];
for (let y = 0; y < shape.height; y++) {
for (let x = 0; x < shape.width; x++) {
if (shape.grid[y]?.[x]) {
const finalX = x + transform.offset.x;
const finalY = y + transform.offset.y;
cells.push({ x: finalX, y: finalY });
}
}
}
// Draw item cells with a unique color per item
const itemColor = this.getItemColor(itemId);
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;
graphics.fillStyle(itemColor);
graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
}
// Item name label (at first cell)
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 itemName = (item.meta?.name as string) ?? 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);
}
// Adjacent items info
const adjacent = getAdjacentItems(this.inventory, itemId);
if (adjacent.size > 0) {
const adjacentNames = Array.from(adjacent.values()).map(i => (i.meta?.name as string) ?? 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;
this.add.text(px + this.CELL_SIZE / 2, py, `邻接: ${adjacentNames}`, {
fontSize: '10px',
color: '#ffff88',
}).setOrigin(0.5);
}
}
}
private getItemColor(itemId: string): number {
// Generate a consistent color based on item ID
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];
}
private createControls(): void {
const { width, height } = this.scale;
// Back button
this.createButton('返回菜单', 100, 40, () => {
this.scene.start('IndexScene');
});
// Clear button
this.createButton('清空', width - 260, 40, () => {
this.inventory = createGridInventory(this.GRID_WIDTH, this.GRID_HEIGHT);
this.scene.restart();
});
// Random fill button
this.createButton('随机填充', width - 130, 40, () => {
this.randomFill();
this.scene.restart();
});
}
private randomFill(): void {
this.inventory = createGridInventory(this.GRID_WIDTH, this.GRID_HEIGHT);
// Try to place random items
let itemIndex = 0;
for (let y = 0; y < this.GRID_HEIGHT && itemIndex < heroItemFighter1Data.length; y++) {
for (let x = 0; x < this.GRID_WIDTH && itemIndex < heroItemFighter1Data.length; x++) {
const data = heroItemFighter1Data[itemIndex];
const shape = parseShapeString(data.shape);
// Check if placement is valid
const occupiedCells = new Set<string>();
for (let sy = 0; sy < shape.height; sy++) {
for (let sx = 0; sx < shape.width; sx++) {
if (shape.grid[sy]?.[sx]) {
occupiedCells.add(`${sx + x},${sy + y}`);
}
}
}
// Simple check if any cell is out of bounds or occupied
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)) {
valid = false;
break;
}
}
if (valid) {
const item: InventoryItem = {
id: `item-${itemIndex}`,
shape,
transform: { offset: { x, y }, rotation: 0, flipX: false, flipY: false },
meta: { name: data.name, data },
};
placeItem(this.inventory, item);
itemIndex++;
}
}
}
}
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);
}
}

View File

@ -0,0 +1,70 @@
import Phaser from 'phaser';
import { ReactiveScene } from 'boardgame-phaser';
export class IndexScene extends ReactiveScene {
constructor() {
super('IndexScene');
}
create(): void {
super.create();
const { width, height } = this.scale;
const centerX = width / 2;
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);
// Subtitle
this.add.text(centerX, centerY - 100, 'Choose a viewer to explore:', {
fontSize: '18px',
color: '#aaaaaa',
}).setOrigin(0.5);
// Buttons
const buttons = [
{ label: 'Map Viewer', scene: 'MapViewerScene', y: centerY - 20 },
{ label: 'Grid Inventory Viewer', scene: 'GridViewerScene', y: centerY + 50 },
{ label: 'Shape Viewer', scene: 'ShapeViewerScene', y: centerY + 120 },
];
for (const btn of buttons) {
this.createButton(btn.label, btn.scene, centerX, btn.y);
}
}
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', () => {
this.scene.start(targetScene);
});
}
}

View File

@ -0,0 +1,251 @@
import Phaser from 'phaser';
import { ReactiveScene } from 'boardgame-phaser';
import { generatePointCrawlMap, type PointCrawlMap, MapNodeType } from 'boardgame-core/samples/slay-the-spire-like';
const NODE_COLORS: Record<MapNodeType, number> = {
[MapNodeType.Start]: 0x44aa44,
[MapNodeType.Combat]: 0xcc4444,
[MapNodeType.Event]: 0xaaaa44,
[MapNodeType.Elite]: 0xcc44cc,
[MapNodeType.Shelter]: 0x44cccc,
[MapNodeType.NPC]: 0x4488cc,
[MapNodeType.Boss]: 0xcc8844,
};
const NODE_LABELS: Record<MapNodeType, string> = {
[MapNodeType.Start]: '起点',
[MapNodeType.Combat]: '战斗',
[MapNodeType.Event]: '事件',
[MapNodeType.Elite]: '精英',
[MapNodeType.Shelter]: '篝火',
[MapNodeType.NPC]: 'NPC',
[MapNodeType.Boss]: 'Boss',
};
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;
private dragStartX = 0;
private dragStartContainerX = 0;
private dragStartY = 0;
private dragStartContainerY = 0;
// 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');
}
create(): void {
super.create();
this.drawFixedUI();
this.drawMap();
}
private drawFixedUI(): void {
const { width } = this.scale;
// Title
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', () => {
this.scene.start('IndexScene');
});
// 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();
});
// Legend (bottom-left, fixed)
this.legendContainer = this.add.container(20, this.scale.height - 200).setDepth(100);
const legendBg = this.add.rectangle(75, 90, 150, 180, 0x222222, 0.8);
this.legendContainer.add(legendBg);
this.legendContainer.add(
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.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' })
);
offsetY += 22;
}
// Hint text
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图', {
fontSize: '14px',
color: '#888888',
}).setOrigin(0.5).setDepth(100);
}
private drawMap(): void {
this.map = generatePointCrawlMap(this.seed);
const { width, height } = this.scale;
// Update title
this.titleText.setText(`Map Viewer (Seed: ${this.seed})`);
// Calculate map bounds
const maxLayer = 12; // TOTAL_LAYERS - 1
const maxNodesInLayer = 6; // widest layer
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
const mapHeight = maxLayer * this.LAYER_HEIGHT + 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)
.setOrigin(0.5);
this.mapContainer.add(bg);
const graphics = this.add.graphics();
this.mapContainer.add(graphics);
// Draw edges
graphics.lineStyle(2, 0x666666);
for (const [nodeId, node] of this.map.nodes) {
const posX = this.getNodeX(node);
const posY = this.getNodeY(node);
for (const childId of node.childIds) {
const child = this.map.nodes.get(childId);
if (child) {
const childX = this.getNodeX(child);
const childY = this.getNodeY(child);
graphics.lineBetween(posX, posY, childX, childY);
}
}
}
// Draw nodes
for (const [nodeId, node] of this.map.nodes) {
const posX = this.getNodeX(node);
const posY = this.getNodeY(node);
const color = NODE_COLORS[node.type as MapNodeType] ?? 0x888888;
// Node circle
graphics.fillStyle(color);
graphics.fillCircle(posX, posY, this.NODE_RADIUS);
graphics.lineStyle(2, 0xffffff);
graphics.strokeCircle(posX, posY, this.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)
);
// Encounter name (if available)
if ('encounter' in node && (node as any).encounter) {
this.mapContainer.add(
this.add.text(posX, posY + this.NODE_RADIUS + 12, (node as any).encounter.name ?? '', {
fontSize: '10px',
color: '#cccccc',
}).setOrigin(0.5)
);
}
}
// Setup drag-to-scroll
this.input.on('pointerdown', (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) => {
if (!this.isDragging) return;
this.mapContainer.x = this.dragStartContainerX + (pointer.x - this.dragStartX);
this.mapContainer.y = this.dragStartContainerY + (pointer.y - this.dragStartY);
});
this.input.on('pointerup', () => {
this.isDragging = false;
});
this.input.on('pointerout', () => {
this.isDragging = false;
});
}
private getNodeX(node: any): number {
const layer = this.map!.layers[node.layerIndex];
const nodeIndex = layer.nodeIds.indexOf(node.id);
const totalNodes = layer.nodeIds.length;
const layerWidth = (totalNodes - 1) * this.NODE_SPACING;
return -layerWidth / 2 + nodeIndex * this.NODE_SPACING;
}
private getNodeY(node: any): number {
return -600 + node.layerIndex * this.LAYER_HEIGHT;
}
}

View File

@ -0,0 +1,166 @@
import Phaser from 'phaser';
import { ReactiveScene } from 'boardgame-phaser';
import { parseShapeString, heroItemFighter1Data, 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;
private currentIndex = 0;
private currentRotation = 0;
private currentFlipX = false;
private currentFlipY = false;
constructor() {
super('ShapeViewerScene');
}
create(): void {
super.create();
this.drawShapeViewer();
this.createControls();
}
private drawShapeViewer(): void {
this.children.removeAll();
const { width, height } = this.scale;
// Title
this.add.text(width / 2, 30, 'Shape Viewer - Item Shapes', {
fontSize: '24px',
color: '#ffffff',
fontStyle: 'bold',
}).setOrigin(0.5);
// Draw all shapes in a grid
this.drawAllShapes();
}
private drawAllShapes(): void {
const { width } = this.scale;
const startY = 80;
const spacingX = 220;
const spacingY = 140;
// Show first 12 items for clarity
const itemsToShow = heroItemFighter1Data.slice(0, 12);
for (let i = 0; i < itemsToShow.length; i++) {
const data = itemsToShow[i];
const shape = parseShapeString(data.shape);
const col = i % this.ITEMS_PER_ROW;
const row = Math.floor(i / this.ITEMS_PER_ROW);
const x = 60 + col * spacingX;
const y = startY + row * spacingY;
this.drawSingleShape(x, y, data, shape);
}
}
private drawSingleShape(startX: number, startY: number, data: any, shape: ParsedShape): void {
const graphics = this.add.graphics();
// Draw shape background
const shapeWidth = shape.width * this.CELL_SIZE;
const shapeHeight = shape.height * this.CELL_SIZE;
// Title - item name
this.add.text(startX + shapeWidth / 2, startY - 20, data.name, {
fontSize: '14px',
color: '#ffffff',
fontStyle: 'bold',
}).setOrigin(0.5);
// Draw shape cells
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;
// Determine if this is the origin cell
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.lineStyle(2, 0xffffff);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
// Mark origin with 'O'
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);
}
}
}
}
// Shape string
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 10, `形状: ${data.shape}`, {
fontSize: '11px',
color: '#aaaaaa',
}).setOrigin(0.5);
// Type and cost
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 28,
`类型: ${data.type} | 费用: ${data.costCount} ${data.costType}`, {
fontSize: '11px',
color: '#cccccc',
}).setOrigin(0.5);
// Description
this.add.text(startX + shapeWidth / 2, startY + shapeHeight + 46, data.desc, {
fontSize: '10px',
color: '#888888',
wordWrap: { width: shapeWidth },
}).setOrigin(0.5);
}
private createControls(): void {
const { width, height } = this.scale;
// Back button
this.createButton('返回菜单', 100, height - 40, () => {
this.scene.start('IndexScene');
});
// Info text
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);
}
}

View File

@ -0,0 +1,7 @@
@import "tailwindcss";
body {
margin: 0;
padding: 0;
overflow: hidden;
}

View File

@ -0,0 +1,27 @@
import { h } from 'preact';
import { PhaserGame, PhaserScene } from 'boardgame-phaser';
import { useMemo } from 'preact/hooks';
import { IndexScene } from '@/scenes/IndexScene';
import { MapViewerScene } from '@/scenes/MapViewerScene';
import { GridViewerScene } from '@/scenes/GridViewerScene';
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
export default function App() {
const indexScene = useMemo(() => new IndexScene(), []);
const mapViewerScene = useMemo(() => new MapViewerScene(), []);
const gridViewerScene = useMemo(() => new GridViewerScene(), []);
const shapeViewerScene = useMemo(() => new ShapeViewerScene(), []);
return (
<div className="flex flex-col h-screen">
<div className="flex-1 flex relative justify-center items-center">
<PhaserGame initialScene="IndexScene" config={{ width: 1920, height: 1080 }}>
<PhaserScene sceneKey="IndexScene" scene={indexScene} />
<PhaserScene sceneKey="MapViewerScene" scene={mapViewerScene} />
<PhaserScene sceneKey="GridViewerScene" scene={gridViewerScene} />
<PhaserScene sceneKey="ShapeViewerScene" scene={shapeViewerScene} />
</PhaserGame>
</div>
</div>
);
}

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"jsx": "react-jsx",
"jsxImportSource": "preact",
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false
},
"include": ["src/**/*"]
}

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import tailwindcss from '@tailwindcss/vite';
import path from 'path';
export default defineConfig({
plugins: [preact(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'boardgame-phaser': path.resolve(__dirname, '../framework/src/index.ts'),
},
},
});

View File

@ -211,6 +211,46 @@ importers:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.4.21(lightningcss@1.32.0) version: 5.4.21(lightningcss@1.32.0)
packages/sts-like-viewer:
dependencies:
'@preact/signals-core':
specifier: ^1.5.1
version: 1.14.1
boardgame-core:
specifier: link:../../../boardgame-core
version: link:../../../boardgame-core
boardgame-phaser:
specifier: workspace:*
version: link:../framework
mutative:
specifier: ^1.3.0
version: 1.3.0
phaser:
specifier: ^3.80.1
version: 3.90.0
preact:
specifier: ^10.19.3
version: 10.29.0
devDependencies:
'@preact/preset-vite':
specifier: ^2.8.1
version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@5.4.21(lightningcss@1.32.0))
'@preact/signals':
specifier: ^2.9.0
version: 2.9.0(preact@10.29.0)
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.2.2(vite@5.4.21(lightningcss@1.32.0))
tailwindcss:
specifier: ^4.0.0
version: 4.2.2
typescript:
specifier: ^5.3.3
version: 5.9.3
vite:
specifier: ^5.1.0
version: 5.4.21(lightningcss@1.32.0)
packages: packages:
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':