diff --git a/packages/sts-like-viewer/index.html b/packages/sts-like-viewer/index.html new file mode 100644 index 0000000..9276bf8 --- /dev/null +++ b/packages/sts-like-viewer/index.html @@ -0,0 +1,12 @@ + + + + + + STS-Like Viewer + + +
+ + + diff --git a/packages/sts-like-viewer/package.json b/packages/sts-like-viewer/package.json new file mode 100644 index 0000000..abe68a4 --- /dev/null +++ b/packages/sts-like-viewer/package.json @@ -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" + } +} diff --git a/packages/sts-like-viewer/src/main.tsx b/packages/sts-like-viewer/src/main.tsx new file mode 100644 index 0000000..013e785 --- /dev/null +++ b/packages/sts-like-viewer/src/main.tsx @@ -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: , +}); + +ui.mount(); diff --git a/packages/sts-like-viewer/src/scenes/GridViewerScene.ts b/packages/sts-like-viewer/src/scenes/GridViewerScene.ts new file mode 100644 index 0000000..e332ae4 --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/GridViewerScene.ts @@ -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(); + 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); + } +} diff --git a/packages/sts-like-viewer/src/scenes/IndexScene.ts b/packages/sts-like-viewer/src/scenes/IndexScene.ts new file mode 100644 index 0000000..0f04c57 --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/IndexScene.ts @@ -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); + }); + } +} diff --git a/packages/sts-like-viewer/src/scenes/MapViewerScene.ts b/packages/sts-like-viewer/src/scenes/MapViewerScene.ts new file mode 100644 index 0000000..6fdaa9e --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/MapViewerScene.ts @@ -0,0 +1,184 @@ +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.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.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 = 100; + private readonly NODE_SPACING = 120; + private readonly NODE_RADIUS = 25; + + constructor() { + super('MapViewerScene'); + } + + create(): void { + super.create(); + this.drawMap(); + this.createControls(); + } + + private drawMap(): void { + this.children.removeAll(); + this.map = generatePointCrawlMap(this.seed); + + const { width, height } = this.scale; + const graphics = this.add.graphics(); + + // Draw edges first + graphics.lineStyle(2, 0x666666); + for (const [nodeId, node] of this.map.nodes) { + const posX = this.getNodeX(node, width); + const posY = this.getNodeY(node, height); + + for (const childId of node.childIds) { + const child = this.map.nodes.get(childId); + if (child) { + const childX = this.getNodeX(child, width); + const childY = this.getNodeY(child, height); + graphics.lineBetween(posX, posY, childX, childY); + } + } + } + + // Draw nodes + for (const [nodeId, node] of this.map.nodes) { + const posX = this.getNodeX(node, width); + const posY = this.getNodeY(node, height); + 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.add.text(posX, posY, label, { + fontSize: '12px', + color: '#ffffff', + }).setOrigin(0.5); + + // Encounter name (if available) + if ('encounter' in node && node.encounter) { + this.add.text(posX, posY + this.NODE_RADIUS + 10, (node as any).encounter.name ?? '', { + fontSize: '10px', + color: '#cccccc', + }).setOrigin(0.5); + } + } + + // Title + this.add.text(width / 2, 30, `Map Viewer (Seed: ${this.seed})`, { + fontSize: '24px', + color: '#ffffff', + fontStyle: 'bold', + }).setOrigin(0.5); + + // Legend + this.drawLegend(20, height - 200); + } + + private getNodeX(node: any, sceneWidth: number): 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; + const startX = sceneWidth / 2 - layerWidth / 2; + return startX + nodeIndex * this.NODE_SPACING; + } + + private getNodeY(node: any, sceneHeight: number): number { + return 80 + node.layerIndex * this.LAYER_HEIGHT; + } + + private drawLegend(x: number, y: number): void { + const graphics = this.add.graphics(); + graphics.fillStyle(0x222222, 0.8); + graphics.fillRect(x, y, 150, 180); + + let offsetY = y + 15; + this.add.text(x + 10, offsetY, '图例:', { + fontSize: '14px', + color: '#ffffff', + fontStyle: 'bold', + }); + offsetY += 25; + + for (const [type, color] of Object.entries(NODE_COLORS)) { + graphics.fillStyle(color); + graphics.fillCircle(x + 20, offsetY, 8); + this.add.text(x + 40, offsetY - 5, NODE_LABELS[type as MapNodeType], { + fontSize: '12px', + color: '#ffffff', + }); + offsetY += 22; + } + } + + private createControls(): void { + const { width, height } = this.scale; + + // Back button + this.createButton('返回菜单', 100, 40, () => { + this.scene.start('IndexScene'); + }); + + // Regenerate button + this.createButton('重新生成', width - 120, 40, () => { + this.seed = Date.now(); + this.drawMap(); + this.createControls(); + }); + } + + private createButton(label: string, x: number, y: number, onClick: () => void): void { + const buttonWidth = 140; + 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); + } +} diff --git a/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts b/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts new file mode 100644 index 0000000..2fb51d9 --- /dev/null +++ b/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts @@ -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); + } +} diff --git a/packages/sts-like-viewer/src/style.css b/packages/sts-like-viewer/src/style.css new file mode 100644 index 0000000..4da823c --- /dev/null +++ b/packages/sts-like-viewer/src/style.css @@ -0,0 +1,7 @@ +@import "tailwindcss"; + +body { + margin: 0; + padding: 0; + overflow: hidden; +} diff --git a/packages/sts-like-viewer/src/ui/App.tsx b/packages/sts-like-viewer/src/ui/App.tsx new file mode 100644 index 0000000..994888a --- /dev/null +++ b/packages/sts-like-viewer/src/ui/App.tsx @@ -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 ( +
+
+ + + + + + +
+
+ ); +} diff --git a/packages/sts-like-viewer/tsconfig.json b/packages/sts-like-viewer/tsconfig.json new file mode 100644 index 0000000..d7f3323 --- /dev/null +++ b/packages/sts-like-viewer/tsconfig.json @@ -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/**/*"] +} diff --git a/packages/sts-like-viewer/vite.config.ts b/packages/sts-like-viewer/vite.config.ts new file mode 100644 index 0000000..c75bb07 --- /dev/null +++ b/packages/sts-like-viewer/vite.config.ts @@ -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'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b7dd04..f80ea92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,6 +211,46 @@ importers: specifier: ^5.1.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: '@babel/code-frame@7.29.0':