From 368d9942d22b0c8ac2c5ca41bd26a091b072101c Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Apr 2026 16:04:29 +0800 Subject: [PATCH] refactor(onitama): centralize visual constants in config Introduce `TEXT_POSITION` and `VISUAL` objects to the configuration to manage magic numbers for positioning, radii, stroke widths, and alphas. Update renderers and spawners to use these constants instead of hardcoded values or direct calculations. --- packages/onitama-game/src/config/index.ts | 24 ++++++++++++++++++ .../src/renderers/CardRenderer.ts | 25 ++++++++++++++++--- .../src/renderers/HighlightRenderer.ts | 6 ++--- .../src/renderers/PawnRenderer.ts | 6 ++--- .../src/renderers/SelectionRenderer.ts | 18 +++++++++++-- .../onitama-game/src/scenes/OnitamaScene.ts | 18 ++++++++++--- .../onitama-game/src/spawners/CardSpawner.ts | 8 +++--- .../src/spawners/HighlightSpawner.ts | 3 ++- .../src/scenes/ShapeViewerScene.ts | 13 ++++++++++ 9 files changed, 102 insertions(+), 19 deletions(-) diff --git a/packages/onitama-game/src/config/index.ts b/packages/onitama-game/src/config/index.ts index 7e6a961..9b169b9 100644 --- a/packages/onitama-game/src/config/index.ts +++ b/packages/onitama-game/src/config/index.ts @@ -113,6 +113,13 @@ export const MENU_BUTTON = { height: 40, } as const; +// Text positioning +export const TEXT_POSITION = { + titleX: 40, + titleY: 40, + infoX: 40, +} as const; + // Animation durations (in ms) export const ANIMATIONS = { pawnSpawn: 300, @@ -139,6 +146,23 @@ export const CARD_GRID = { gridSize: 5, } as const; +// Visual style constants +export const VISUAL = { + pawnRadius: CELL_SIZE / 3, + pawnStrokeWidth: 2, + selectionRingOffset: 5, + selectionRingStrokeWidth: 3, + highlightOuterRadius: CELL_SIZE / 3, + highlightInnerRadius: CELL_SIZE / 4, + highlightHitAreaRadius: CELL_SIZE / 3, + cardStrokeWidth: 2, + cardTitleOffset: 16, + cardPlayerOffset: 16, + cardDisabledAlpha: 0.8, + overlayAlpha: 0.6, + cardBackgroundDepth: -1, +} as const; + // Helper function to convert board coordinates to screen coordinates export function boardToScreen( boardX: number, diff --git a/packages/onitama-game/src/renderers/CardRenderer.ts b/packages/onitama-game/src/renderers/CardRenderer.ts index f2a21e8..5d1159b 100644 --- a/packages/onitama-game/src/renderers/CardRenderer.ts +++ b/packages/onitama-game/src/renderers/CardRenderer.ts @@ -1,7 +1,14 @@ import type { Card } from "@/game/onitama"; import type { OnitamaScene } from "@/scenes/OnitamaScene"; -import { CARD_WIDTH, CARD_HEIGHT, COLORS, FONTS, CARD_GRID } from "@/config"; +import { + CARD_WIDTH, + CARD_HEIGHT, + COLORS, + FONTS, + CARD_GRID, + VISUAL, +} from "@/config"; export interface CardRenderOptions { card: Card; @@ -28,12 +35,17 @@ export class CardRenderer { // Create background rectangle const bg = this.scene.add .rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, COLORS.cardBg, 1) - .setStrokeStyle(2, COLORS.cardStroke); + .setStrokeStyle(VISUAL.cardStrokeWidth, COLORS.cardStroke); container.add(bg); // Create title text const title = this.scene.add - .text(0, -CARD_HEIGHT / 2 + 16, card.id, FONTS.cardTitle) + .text( + 0, + -CARD_HEIGHT / 2 + VISUAL.cardTitleOffset, + card.id, + FONTS.cardTitle, + ) .setOrigin(0.5); container.add(title); @@ -42,7 +54,12 @@ export class CardRenderer { // Create starting player text const playerText = this.scene.add - .text(0, CARD_HEIGHT / 2 - 16, card.startingPlayer, FONTS.cardPlayer) + .text( + 0, + CARD_HEIGHT / 2 - VISUAL.cardPlayerOffset, + card.startingPlayer, + FONTS.cardPlayer, + ) .setOrigin(0.5); container.add(playerText); } diff --git a/packages/onitama-game/src/renderers/HighlightRenderer.ts b/packages/onitama-game/src/renderers/HighlightRenderer.ts index 6f2e1db..35b75f8 100644 --- a/packages/onitama-game/src/renderers/HighlightRenderer.ts +++ b/packages/onitama-game/src/renderers/HighlightRenderer.ts @@ -1,8 +1,8 @@ import type { OnitamaScene } from "@/scenes/OnitamaScene"; import { - CELL_SIZE, COLORS, + VISUAL, createHighlightInnerPulseTween, createHighlightOuterPulseTween, } from "@/config"; @@ -37,7 +37,7 @@ export class HighlightRenderer { const outerCircle = this.scene.add.circle( 0, 0, - CELL_SIZE / 3, + VISUAL.highlightOuterRadius, COLORS.black, 0.2, ); @@ -47,7 +47,7 @@ export class HighlightRenderer { const innerCircle = this.scene.add.circle( 0, 0, - CELL_SIZE / 4, + VISUAL.highlightInnerRadius, COLORS.black, 0.4, ); diff --git a/packages/onitama-game/src/renderers/PawnRenderer.ts b/packages/onitama-game/src/renderers/PawnRenderer.ts index c45f4ea..4c98a97 100644 --- a/packages/onitama-game/src/renderers/PawnRenderer.ts +++ b/packages/onitama-game/src/renderers/PawnRenderer.ts @@ -1,6 +1,6 @@ import type { OnitamaScene } from "@/scenes/OnitamaScene"; -import { CELL_SIZE, COLORS, FONTS } from "@/config"; +import { CELL_SIZE, COLORS, FONTS, VISUAL } from "@/config"; export type PawnType = "master" | "student"; export type PawnOwner = "red" | "black"; @@ -31,8 +31,8 @@ export class PawnRenderer { // Create background circle const bgColor = owner === "red" ? COLORS.red : COLORS.black; const circle = this.scene.add - .circle(0, 0, CELL_SIZE / 3, bgColor, 1) - .setStrokeStyle(2, COLORS.pawnStroke); + .circle(0, 0, VISUAL.pawnRadius, bgColor, 1) + .setStrokeStyle(VISUAL.pawnStrokeWidth, COLORS.pawnStroke); container.add(circle); // Create label text diff --git a/packages/onitama-game/src/renderers/SelectionRenderer.ts b/packages/onitama-game/src/renderers/SelectionRenderer.ts index dd1e8d9..3ae90eb 100644 --- a/packages/onitama-game/src/renderers/SelectionRenderer.ts +++ b/packages/onitama-game/src/renderers/SelectionRenderer.ts @@ -5,6 +5,7 @@ import type { OnitamaScene } from "@/scenes/OnitamaScene"; import { CELL_SIZE, COLORS, + VISUAL, createSelectionShowTween, createSelectionRingPulseTween, createSelectionHideTween, @@ -31,8 +32,21 @@ export class SelectionRenderer { parent: Phaser.GameObjects.Container | Phaser.GameObjects.GameObject, ): Phaser.GameObjects.Arc { const ring = this.scene.add - .arc(0, 0, CELL_SIZE / 3 + 5, 0, 360, false, COLORS.highlight, 0) - .setStrokeStyle(3, COLORS.highlightStroke, 1) + .arc( + 0, + 0, + VISUAL.pawnRadius + VISUAL.selectionRingOffset, + 0, + 360, + false, + COLORS.highlight, + 0, + ) + .setStrokeStyle( + VISUAL.selectionRingStrokeWidth, + COLORS.highlightStroke, + 1, + ) .setAlpha(0); // Add to parent at index 0 (behind other visuals) diff --git a/packages/onitama-game/src/scenes/OnitamaScene.ts b/packages/onitama-game/src/scenes/OnitamaScene.ts index 413a265..50513bb 100644 --- a/packages/onitama-game/src/scenes/OnitamaScene.ts +++ b/packages/onitama-game/src/scenes/OnitamaScene.ts @@ -10,6 +10,8 @@ import { COLORS, FONTS, MENU_BUTTON, + TEXT_POSITION, + VISUAL, getBoardCenter, getCardLabelPosition, colorToStr, @@ -79,7 +81,12 @@ export class OnitamaScene extends GameHostScene { }); // Info text - this.infoText = this.add.text(40, BOARD_OFFSET.y, "", FONTS.info); + this.infoText = this.add.text( + TEXT_POSITION.infoX, + BOARD_OFFSET.y, + "", + FONTS.info, + ); // Update info text when UI state changes this.addEffect(() => { @@ -149,7 +156,12 @@ export class OnitamaScene extends GameHostScene { g.strokePath(); - this.add.text(40, 40, "Onitama", FONTS.title); + this.add.text( + TEXT_POSITION.titleX, + TEXT_POSITION.titleY, + "Onitama", + FONTS.title, + ); } private setupInput(): void { @@ -288,7 +300,7 @@ export class OnitamaScene extends GameHostScene { boardWidth, boardHeight, COLORS.overlayBg, - 0.6, + VISUAL.overlayAlpha, ) .setInteractive({ useHandCursor: true }); diff --git a/packages/onitama-game/src/spawners/CardSpawner.ts b/packages/onitama-game/src/spawners/CardSpawner.ts index f8a1684..34814dc 100644 --- a/packages/onitama-game/src/spawners/CardSpawner.ts +++ b/packages/onitama-game/src/spawners/CardSpawner.ts @@ -7,6 +7,8 @@ import type { Spawner } from "boardgame-phaser"; import { CARD_WIDTH, CARD_HEIGHT, + COLORS, + VISUAL, getCardPosition, createCardMoveTween, createCardRotateTween, @@ -102,7 +104,7 @@ export class CardContainer extends Phaser.GameObjects.Container { .rectangle(0, 0, CARD_WIDTH + 8, CARD_HEIGHT + 8, color, 0) .setStrokeStyle(lineWidth, color) .setAlpha(0) - .setDepth(-1); + .setDepth(VISUAL.cardBackgroundDepth); this.highlightRect = rect; this.addAt(this.highlightRect, 0); @@ -161,7 +163,7 @@ export class CardContainer extends Phaser.GameObjects.Container { // 创建一个 effect 来持续监听高亮状态变化 const dispose = effect(() => { if (scene.uiState.value.selectedCard === this._cardId) { - this.highlight(0xfbbf24, 3); + this.highlight(COLORS.highlight, VISUAL.selectionRingStrokeWidth); } else { this.unhighlight(); } @@ -276,7 +278,7 @@ export class CardSpawner implements Spawner { // 设置悬停效果 container.on("pointerover", () => { if (this.scene.uiState.value.selectedCard !== data.cardId) { - container.setAlpha(0.8); + container.setAlpha(VISUAL.cardDisabledAlpha); } }); diff --git a/packages/onitama-game/src/spawners/HighlightSpawner.ts b/packages/onitama-game/src/spawners/HighlightSpawner.ts index 90ab4dd..30edd98 100644 --- a/packages/onitama-game/src/spawners/HighlightSpawner.ts +++ b/packages/onitama-game/src/spawners/HighlightSpawner.ts @@ -7,6 +7,7 @@ import type { Spawner } from "boardgame-phaser"; import { boardToScreen, CELL_SIZE, + VISUAL, createHighlightSpawnTween, createHighlightDespawnTween, createHighlightClickFeedbackTween, @@ -83,7 +84,7 @@ export class HighlightSpawner implements Spawner< this.renderer.render(container, { x: data.x, y: data.y }); // 设置交互区域 - const hitArea = new Geom.Circle(0, 0, CELL_SIZE / 3); + const hitArea = new Geom.Circle(0, 0, VISUAL.highlightHitAreaRadius); container.setInteractive(hitArea, Phaser.Geom.Circle.Contains); if (container.input) { container.input.cursor = "pointer"; diff --git a/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts b/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts index 995f795..c51bdf8 100644 --- a/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts +++ b/packages/sts-like-viewer/src/scenes/ShapeViewerScene.ts @@ -17,6 +17,19 @@ export class ShapeViewerScene extends ReactiveScene { super.create(); this.drawShapeViewer(); this.createControls(); + this.createBackButton(); + } + + private createBackButton(): void { + createButton({ + scene: this, + label: "← Back", + x: 80, + y: 30, + onClick: async () => { + await this.sceneController.launch(SceneKey.IndexScene); + }, + }); } private drawShapeViewer(): void {