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:
parent
22817945cc
commit
368d9942d2
|
|
@ -113,6 +113,13 @@ export const MENU_BUTTON = {
|
||||||
height: 40,
|
height: 40,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Text positioning
|
||||||
|
export const TEXT_POSITION = {
|
||||||
|
titleX: 40,
|
||||||
|
titleY: 40,
|
||||||
|
infoX: 40,
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Animation durations (in ms)
|
// Animation durations (in ms)
|
||||||
export const ANIMATIONS = {
|
export const ANIMATIONS = {
|
||||||
pawnSpawn: 300,
|
pawnSpawn: 300,
|
||||||
|
|
@ -139,6 +146,23 @@ export const CARD_GRID = {
|
||||||
gridSize: 5,
|
gridSize: 5,
|
||||||
} as const;
|
} 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
|
// Helper function to convert board coordinates to screen coordinates
|
||||||
export function boardToScreen(
|
export function boardToScreen(
|
||||||
boardX: number,
|
boardX: number,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import type { Card } from "@/game/onitama";
|
import type { Card } from "@/game/onitama";
|
||||||
import type { OnitamaScene } from "@/scenes/OnitamaScene";
|
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 {
|
export interface CardRenderOptions {
|
||||||
card: Card;
|
card: Card;
|
||||||
|
|
@ -28,12 +35,17 @@ export class CardRenderer {
|
||||||
// Create background rectangle
|
// Create background rectangle
|
||||||
const bg = this.scene.add
|
const bg = this.scene.add
|
||||||
.rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, COLORS.cardBg, 1)
|
.rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, COLORS.cardBg, 1)
|
||||||
.setStrokeStyle(2, COLORS.cardStroke);
|
.setStrokeStyle(VISUAL.cardStrokeWidth, COLORS.cardStroke);
|
||||||
container.add(bg);
|
container.add(bg);
|
||||||
|
|
||||||
// Create title text
|
// Create title text
|
||||||
const title = this.scene.add
|
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);
|
.setOrigin(0.5);
|
||||||
container.add(title);
|
container.add(title);
|
||||||
|
|
||||||
|
|
@ -42,7 +54,12 @@ export class CardRenderer {
|
||||||
|
|
||||||
// Create starting player text
|
// Create starting player text
|
||||||
const playerText = this.scene.add
|
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);
|
.setOrigin(0.5);
|
||||||
container.add(playerText);
|
container.add(playerText);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { OnitamaScene } from "@/scenes/OnitamaScene";
|
import type { OnitamaScene } from "@/scenes/OnitamaScene";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CELL_SIZE,
|
|
||||||
COLORS,
|
COLORS,
|
||||||
|
VISUAL,
|
||||||
createHighlightInnerPulseTween,
|
createHighlightInnerPulseTween,
|
||||||
createHighlightOuterPulseTween,
|
createHighlightOuterPulseTween,
|
||||||
} from "@/config";
|
} from "@/config";
|
||||||
|
|
@ -37,7 +37,7 @@ export class HighlightRenderer {
|
||||||
const outerCircle = this.scene.add.circle(
|
const outerCircle = this.scene.add.circle(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
CELL_SIZE / 3,
|
VISUAL.highlightOuterRadius,
|
||||||
COLORS.black,
|
COLORS.black,
|
||||||
0.2,
|
0.2,
|
||||||
);
|
);
|
||||||
|
|
@ -47,7 +47,7 @@ export class HighlightRenderer {
|
||||||
const innerCircle = this.scene.add.circle(
|
const innerCircle = this.scene.add.circle(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
CELL_SIZE / 4,
|
VISUAL.highlightInnerRadius,
|
||||||
COLORS.black,
|
COLORS.black,
|
||||||
0.4,
|
0.4,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { OnitamaScene } from "@/scenes/OnitamaScene";
|
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 PawnType = "master" | "student";
|
||||||
export type PawnOwner = "red" | "black";
|
export type PawnOwner = "red" | "black";
|
||||||
|
|
@ -31,8 +31,8 @@ export class PawnRenderer {
|
||||||
// Create background circle
|
// Create background circle
|
||||||
const bgColor = owner === "red" ? COLORS.red : COLORS.black;
|
const bgColor = owner === "red" ? COLORS.red : COLORS.black;
|
||||||
const circle = this.scene.add
|
const circle = this.scene.add
|
||||||
.circle(0, 0, CELL_SIZE / 3, bgColor, 1)
|
.circle(0, 0, VISUAL.pawnRadius, bgColor, 1)
|
||||||
.setStrokeStyle(2, COLORS.pawnStroke);
|
.setStrokeStyle(VISUAL.pawnStrokeWidth, COLORS.pawnStroke);
|
||||||
container.add(circle);
|
container.add(circle);
|
||||||
|
|
||||||
// Create label text
|
// Create label text
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type { OnitamaScene } from "@/scenes/OnitamaScene";
|
||||||
import {
|
import {
|
||||||
CELL_SIZE,
|
CELL_SIZE,
|
||||||
COLORS,
|
COLORS,
|
||||||
|
VISUAL,
|
||||||
createSelectionShowTween,
|
createSelectionShowTween,
|
||||||
createSelectionRingPulseTween,
|
createSelectionRingPulseTween,
|
||||||
createSelectionHideTween,
|
createSelectionHideTween,
|
||||||
|
|
@ -31,8 +32,21 @@ export class SelectionRenderer {
|
||||||
parent: Phaser.GameObjects.Container | Phaser.GameObjects.GameObject,
|
parent: Phaser.GameObjects.Container | Phaser.GameObjects.GameObject,
|
||||||
): Phaser.GameObjects.Arc {
|
): Phaser.GameObjects.Arc {
|
||||||
const ring = this.scene.add
|
const ring = this.scene.add
|
||||||
.arc(0, 0, CELL_SIZE / 3 + 5, 0, 360, false, COLORS.highlight, 0)
|
.arc(
|
||||||
.setStrokeStyle(3, COLORS.highlightStroke, 1)
|
0,
|
||||||
|
0,
|
||||||
|
VISUAL.pawnRadius + VISUAL.selectionRingOffset,
|
||||||
|
0,
|
||||||
|
360,
|
||||||
|
false,
|
||||||
|
COLORS.highlight,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.setStrokeStyle(
|
||||||
|
VISUAL.selectionRingStrokeWidth,
|
||||||
|
COLORS.highlightStroke,
|
||||||
|
1,
|
||||||
|
)
|
||||||
.setAlpha(0);
|
.setAlpha(0);
|
||||||
|
|
||||||
// Add to parent at index 0 (behind other visuals)
|
// Add to parent at index 0 (behind other visuals)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import {
|
||||||
COLORS,
|
COLORS,
|
||||||
FONTS,
|
FONTS,
|
||||||
MENU_BUTTON,
|
MENU_BUTTON,
|
||||||
|
TEXT_POSITION,
|
||||||
|
VISUAL,
|
||||||
getBoardCenter,
|
getBoardCenter,
|
||||||
getCardLabelPosition,
|
getCardLabelPosition,
|
||||||
colorToStr,
|
colorToStr,
|
||||||
|
|
@ -79,7 +81,12 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Info text
|
// 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
|
// Update info text when UI state changes
|
||||||
this.addEffect(() => {
|
this.addEffect(() => {
|
||||||
|
|
@ -149,7 +156,12 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
|
|
||||||
g.strokePath();
|
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 {
|
private setupInput(): void {
|
||||||
|
|
@ -288,7 +300,7 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
boardWidth,
|
boardWidth,
|
||||||
boardHeight,
|
boardHeight,
|
||||||
COLORS.overlayBg,
|
COLORS.overlayBg,
|
||||||
0.6,
|
VISUAL.overlayAlpha,
|
||||||
)
|
)
|
||||||
.setInteractive({ useHandCursor: true });
|
.setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import type { Spawner } from "boardgame-phaser";
|
||||||
import {
|
import {
|
||||||
CARD_WIDTH,
|
CARD_WIDTH,
|
||||||
CARD_HEIGHT,
|
CARD_HEIGHT,
|
||||||
|
COLORS,
|
||||||
|
VISUAL,
|
||||||
getCardPosition,
|
getCardPosition,
|
||||||
createCardMoveTween,
|
createCardMoveTween,
|
||||||
createCardRotateTween,
|
createCardRotateTween,
|
||||||
|
|
@ -102,7 +104,7 @@ export class CardContainer extends Phaser.GameObjects.Container {
|
||||||
.rectangle(0, 0, CARD_WIDTH + 8, CARD_HEIGHT + 8, color, 0)
|
.rectangle(0, 0, CARD_WIDTH + 8, CARD_HEIGHT + 8, color, 0)
|
||||||
.setStrokeStyle(lineWidth, color)
|
.setStrokeStyle(lineWidth, color)
|
||||||
.setAlpha(0)
|
.setAlpha(0)
|
||||||
.setDepth(-1);
|
.setDepth(VISUAL.cardBackgroundDepth);
|
||||||
this.highlightRect = rect;
|
this.highlightRect = rect;
|
||||||
this.addAt(this.highlightRect, 0);
|
this.addAt(this.highlightRect, 0);
|
||||||
|
|
||||||
|
|
@ -161,7 +163,7 @@ export class CardContainer extends Phaser.GameObjects.Container {
|
||||||
// 创建一个 effect 来持续监听高亮状态变化
|
// 创建一个 effect 来持续监听高亮状态变化
|
||||||
const dispose = effect(() => {
|
const dispose = effect(() => {
|
||||||
if (scene.uiState.value.selectedCard === this._cardId) {
|
if (scene.uiState.value.selectedCard === this._cardId) {
|
||||||
this.highlight(0xfbbf24, 3);
|
this.highlight(COLORS.highlight, VISUAL.selectionRingStrokeWidth);
|
||||||
} else {
|
} else {
|
||||||
this.unhighlight();
|
this.unhighlight();
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +278,7 @@ export class CardSpawner implements Spawner<CardSpawnData, CardContainer> {
|
||||||
// 设置悬停效果
|
// 设置悬停效果
|
||||||
container.on("pointerover", () => {
|
container.on("pointerover", () => {
|
||||||
if (this.scene.uiState.value.selectedCard !== data.cardId) {
|
if (this.scene.uiState.value.selectedCard !== data.cardId) {
|
||||||
container.setAlpha(0.8);
|
container.setAlpha(VISUAL.cardDisabledAlpha);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type { Spawner } from "boardgame-phaser";
|
||||||
import {
|
import {
|
||||||
boardToScreen,
|
boardToScreen,
|
||||||
CELL_SIZE,
|
CELL_SIZE,
|
||||||
|
VISUAL,
|
||||||
createHighlightSpawnTween,
|
createHighlightSpawnTween,
|
||||||
createHighlightDespawnTween,
|
createHighlightDespawnTween,
|
||||||
createHighlightClickFeedbackTween,
|
createHighlightClickFeedbackTween,
|
||||||
|
|
@ -83,7 +84,7 @@ export class HighlightSpawner implements Spawner<
|
||||||
this.renderer.render(container, { x: data.x, y: data.y });
|
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);
|
container.setInteractive(hitArea, Phaser.Geom.Circle.Contains);
|
||||||
if (container.input) {
|
if (container.input) {
|
||||||
container.input.cursor = "pointer";
|
container.input.cursor = "pointer";
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,19 @@ export class ShapeViewerScene extends ReactiveScene {
|
||||||
super.create();
|
super.create();
|
||||||
this.drawShapeViewer();
|
this.drawShapeViewer();
|
||||||
this.createControls();
|
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 {
|
private drawShapeViewer(): void {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue