Browse Source
- Introduced scene discovery utility to streamline scene loading by automatically collecting scene definitions. - Refactored initApp function to utilize the new scene discovery method, improving clarity and maintainability. - Updated Button component to support click cooldowns and state management, enhancing user interaction experience. - Added WelcomeScene to guide users through gameplay instructions with interactive elements. Made-with: Cursormaster
8 changed files with 382 additions and 85 deletions
@ -0,0 +1,44 @@ |
|||
export type SceneModuleMap = Record<string, unknown>; |
|||
|
|||
export type SceneDefinition = { |
|||
name: string; |
|||
path: string; |
|||
SceneCtor: new (...args: unknown[]) => object; |
|||
}; |
|||
|
|||
type CollectSceneDefinitionsOptions = { |
|||
exclude?: (sceneName: string, modulePath: string) => boolean; |
|||
}; |
|||
|
|||
export function collectSceneDefinitions( |
|||
modules: SceneModuleMap, |
|||
options: CollectSceneDefinitionsOptions = {} |
|||
): SceneDefinition[] { |
|||
const definitions: SceneDefinition[] = []; |
|||
const { exclude } = options; |
|||
|
|||
for (const modulePath in modules) { |
|||
const match = modulePath.match(/page_(.*?)\.ts$/); |
|||
if (!match) { |
|||
continue; |
|||
} |
|||
|
|||
const sceneName = match[1]; |
|||
if (exclude?.(sceneName, modulePath)) { |
|||
continue; |
|||
} |
|||
|
|||
const raw = (modules[modulePath] as { default?: unknown }).default; |
|||
if (typeof raw !== "function") { |
|||
continue; |
|||
} |
|||
|
|||
definitions.push({ |
|||
name: sceneName, |
|||
path: modulePath, |
|||
SceneCtor: raw as new (...args: unknown[]) => object, |
|||
}); |
|||
} |
|||
|
|||
return definitions; |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
import { Button } from "@/components/Button"; |
|||
import Game from "@/core/Game"; |
|||
import { SceneType } from "@/enums/SceneType"; |
|||
import { BaseScene } from "@/scene/BaseScene"; |
|||
import position from "@/utils/Position"; |
|||
import { Container, Graphics, Text, TextStyle } from "pixi.js"; |
|||
|
|||
type GuideItem = { |
|||
title: string; |
|||
hint: string; |
|||
}; |
|||
|
|||
const GUIDE_ITEMS: GuideItem[] = [ |
|||
{ title: "1. 宝箱记忆", hint: "翻牌配对,考验短时记忆" }, |
|||
{ title: "2. 节奏连击", hint: "跟随节拍点击,连击越高分越高" }, |
|||
{ title: "3. 躲避训练", hint: "滑动角色闪避障碍,坚持更久" }, |
|||
]; |
|||
|
|||
export default class WelcomeScene extends BaseScene { |
|||
stage = new Container(); |
|||
private game = Game.getInstance(); |
|||
|
|||
private titleText?: Text; |
|||
private descText?: Text; |
|||
private detailText?: Text; |
|||
private backBtn?: Button; |
|||
private itemRows: Container[] = []; |
|||
|
|||
private readonly onResize = (): void => { |
|||
this.relayout(); |
|||
}; |
|||
|
|||
constructor() { |
|||
super("welcome", SceneType.Normal); |
|||
} |
|||
|
|||
protected async onSceneLayout(): Promise<void> { |
|||
this.stage.sortableChildren = true; |
|||
this.stage.eventMode = "passive"; |
|||
|
|||
this.titleText = new Text({ |
|||
text: "子游戏引导", |
|||
style: new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 42, |
|||
fontWeight: "700", |
|||
fill: 0xffffff, |
|||
}), |
|||
}); |
|||
this.titleText.anchor.set(0.5); |
|||
this.stage.addChild(this.titleText); |
|||
|
|||
this.descText = new Text({ |
|||
text: "选择一项查看玩法说明", |
|||
style: new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 20, |
|||
fill: 0xdbeafe, |
|||
}), |
|||
}); |
|||
this.descText.anchor.set(0.5); |
|||
this.stage.addChild(this.descText); |
|||
|
|||
this.detailText = new Text({ |
|||
text: GUIDE_ITEMS[0].hint, |
|||
style: new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 18, |
|||
fill: 0xf8fafc, |
|||
}), |
|||
}); |
|||
this.detailText.anchor.set(0.5); |
|||
this.stage.addChild(this.detailText); |
|||
|
|||
this.itemRows = GUIDE_ITEMS.map((item, index) => this.createGuideRow(item, index)); |
|||
for (const row of this.itemRows) { |
|||
this.stage.addChild(row); |
|||
} |
|||
this.highlightRow(0); |
|||
|
|||
this.backBtn = new Button({ |
|||
text: "返回开始", |
|||
fontSize: 24, |
|||
padding: { x: 64, y: 26 }, |
|||
position: () => position.get("center", "bottom", { y: -56, x: 0 }), |
|||
onClick: () => { |
|||
this.backBtn?.setDisabled(true); |
|||
this.backBtn?.setText("返回中..."); |
|||
void this.changeScene("init"); |
|||
}, |
|||
}); |
|||
this.stage.addChild(this.backBtn.getView()); |
|||
|
|||
this.relayout(); |
|||
} |
|||
|
|||
protected onSceneEnter(): void { |
|||
window.addEventListener("resize", this.onResize); |
|||
} |
|||
|
|||
protected onSceneExit(): void { |
|||
window.removeEventListener("resize", this.onResize); |
|||
} |
|||
|
|||
private createGuideRow(item: GuideItem, index: number): Container { |
|||
const row = new Container(); |
|||
row.eventMode = "static"; |
|||
row.cursor = "pointer"; |
|||
|
|||
const bg = new Graphics(); |
|||
bg.roundRect(0, 0, 560, 68, 14); |
|||
bg.fill({ color: 0x10223f, alpha: 0.92 }); |
|||
row.addChild(bg); |
|||
|
|||
const title = new Text({ |
|||
text: item.title, |
|||
style: new TextStyle({ |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
fontSize: 24, |
|||
fill: 0xffffff, |
|||
}), |
|||
}); |
|||
title.anchor.set(0.5); |
|||
title.position.set(280, 34); |
|||
row.addChild(title); |
|||
|
|||
row.on("pointerdown", () => this.highlightRow(index)); |
|||
return row; |
|||
} |
|||
|
|||
private highlightRow(targetIndex: number): void { |
|||
this.itemRows.forEach((row, index) => { |
|||
const bg = row.children[0] as Graphics | undefined; |
|||
if (!bg) { |
|||
return; |
|||
} |
|||
bg.clear(); |
|||
if (index === targetIndex) { |
|||
bg.roundRect(0, 0, 560, 68, 14); |
|||
bg.fill({ color: 0x1d4ed8, alpha: 0.95 }); |
|||
} else { |
|||
bg.roundRect(0, 0, 560, 68, 14); |
|||
bg.fill({ color: 0x10223f, alpha: 0.92 }); |
|||
} |
|||
}); |
|||
|
|||
if (this.detailText) { |
|||
this.detailText.text = GUIDE_ITEMS[targetIndex].hint; |
|||
} |
|||
} |
|||
|
|||
private relayout(): void { |
|||
const { height: H } = this.game.getInfo(); |
|||
|
|||
this.titleText?.position.copyFrom(position.get("center", H * 0.12)); |
|||
this.descText?.position.copyFrom(position.get("center", H * 0.2)); |
|||
|
|||
this.itemRows.forEach((row, index) => { |
|||
row.position.copyFrom(position.get("center", H * 0.32 + index * 90)); |
|||
row.pivot.set(280, 34); |
|||
}); |
|||
|
|||
this.detailText?.position.copyFrom(position.get("center", H * 0.66)); |
|||
this.backBtn?.updateView(); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { collectSceneDefinitions } from "../src/init/sceneDiscovery"; |
|||
import { SceneType } from "../src/enums/SceneType"; |
|||
|
|||
class DummyScene { |
|||
name = "dummy"; |
|||
type = SceneType.Normal; |
|||
stage = {}; |
|||
} |
|||
|
|||
describe("collectSceneDefinitions", () => { |
|||
it("collects every page_*.ts scene without manual whitelist", () => { |
|||
const modules = { |
|||
"./stages/page_init.ts": { default: DummyScene }, |
|||
"./stages/_global/page_00_global.ts": { default: DummyScene }, |
|||
"./stages/welcome/page_welcome.ts": { default: DummyScene }, |
|||
}; |
|||
|
|||
const scenes = collectSceneDefinitions(modules); |
|||
expect(scenes.map((item) => item.name)).toEqual(["init", "00_global", "welcome"]); |
|||
}); |
|||
|
|||
it("skips files excluded by predicate", () => { |
|||
const modules = { |
|||
"./stages/page_init.ts": { default: DummyScene }, |
|||
"./stages/_global/page_00_global.ts": { default: DummyScene }, |
|||
"./stages/welcome/page_welcome.ts": { default: DummyScene }, |
|||
}; |
|||
|
|||
const scenes = collectSceneDefinitions(modules, { |
|||
exclude: (name) => name.startsWith("00_"), |
|||
}); |
|||
|
|||
expect(scenes.map((item) => item.name)).toEqual(["init", "welcome"]); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue