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.
This commit is contained in:
hypercross 2026-04-20 16:04:29 +08:00
parent 22817945cc
commit 368d9942d2
9 changed files with 102 additions and 19 deletions

View File

@ -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,

View File

@ -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);
}

View File

@ -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,
);

View File

@ -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

View File

@ -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)

View File

@ -10,6 +10,8 @@ import {
COLORS,
FONTS,
MENU_BUTTON,
TEXT_POSITION,
VISUAL,
getBoardCenter,
getCardLabelPosition,
colorToStr,
@ -79,7 +81,12 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
});
// 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<OnitamaState> {
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<OnitamaState> {
boardWidth,
boardHeight,
COLORS.overlayBg,
0.6,
VISUAL.overlayAlpha,
)
.setInteractive({ useHandCursor: true });

View File

@ -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<CardSpawnData, CardContainer> {
// 设置悬停效果
container.on("pointerover", () => {
if (this.scene.uiState.value.selectedCard !== data.cardId) {
container.setAlpha(0.8);
container.setAlpha(VISUAL.cardDisabledAlpha);
}
});

View File

@ -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";

View File

@ -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 {