first commmit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { _decorator, Component, director } from 'cc';
|
||||
import { ConfigMgr } from '../data/ConfigMgr';
|
||||
import { CCJsonLoader } from './CCJsonLoader';
|
||||
import { BossController } from '../logic/BossController';
|
||||
import { ensureCanvasSize, createLabel, createButton } from './MainMenuEntry';
|
||||
import { Color } from 'cc';
|
||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Boss scene entry (task 7.3 / 8.2 hookup, req 9.x + 14.x).
|
||||
*
|
||||
* Attach to the root node of `Boss_ShuangHuanFang.scene`. Default bossId
|
||||
* matches the chapter-1 final boss.
|
||||
*
|
||||
* When `autoBuildUI` is enabled, two temporary debug buttons are placed on
|
||||
* screen so the flow can be validated before the combat view layer lands:
|
||||
* - Left side: "Hit Butterfly" → `onButterflyHit`
|
||||
* - Right side: "Hit Body" → `onBodyHit`
|
||||
* These will be removed once the real combat HUD is built.
|
||||
*/
|
||||
@ccclass('BossEntry')
|
||||
export class BossEntry extends Component {
|
||||
@property({ tooltip: '对应 configs/bosses.json 中的 id' })
|
||||
public bossId: string = 'shuang_huan_fang';
|
||||
|
||||
@property({ tooltip: '是否自动创建调试按钮 (战斗 HUD 就绪前使用)' })
|
||||
public autoBuildUI: boolean = true;
|
||||
|
||||
private ctrl: BossController | undefined;
|
||||
|
||||
protected async onLoad(): Promise<void> {
|
||||
if (this.autoBuildUI) this.buildDefaultUI();
|
||||
const cfg = new ConfigMgr(new CCJsonLoader());
|
||||
await cfg.load();
|
||||
this.ctrl = new BossController(cfg.boss(this.bossId));
|
||||
}
|
||||
|
||||
/** Dev-hook: attack landed on the butterfly (req 9.2). */
|
||||
public onButterflyHit(): void {
|
||||
const events = this.ctrl?.onButterflyHit() ?? [];
|
||||
this.processEvents(events);
|
||||
}
|
||||
|
||||
/** Dev-hook: attack landed on boss body (req 9.3). */
|
||||
public onBodyHit(): void {
|
||||
const events = this.ctrl?.onBodyHit() ?? [];
|
||||
this.processEvents(events);
|
||||
}
|
||||
|
||||
private processEvents(events: ReturnType<NonNullable<BossController['onBodyHit']>>): void {
|
||||
for (const ev of events) {
|
||||
if (ev.kind === 'boss_killed') {
|
||||
director.loadScene('Settlement');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildDefaultUI(): void {
|
||||
ensureCanvasSize(this.node);
|
||||
createLabel(this.node, 'BOSS · 双幻坊', 0, DESIGN_HEIGHT / 2 - 60, 32, Color.WHITE);
|
||||
createLabel(
|
||||
this.node,
|
||||
'调试:先击中蝴蝶 → 再击中本体',
|
||||
0,
|
||||
DESIGN_HEIGHT / 2 - 110,
|
||||
18,
|
||||
new Color(200, 200, 200, 255),
|
||||
);
|
||||
// Left / right debug buttons, 180px off center.
|
||||
createButton(this.node, 'Hit Butterfly', -180, -120, 220, 70, () => this.onButterflyHit());
|
||||
createButton(this.node, 'Hit Body', 180, -120, 220, 70, () => this.onBodyHit());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1ac5856f-147f-410c-8b8b-8de267952b40",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
import { IJsonLoader } from '../data/ConfigMgr';
|
||||
|
||||
/**
|
||||
* Cocos Creator backed JSON loader. Used by Scene Entry components to feed
|
||||
* `ConfigMgr`. Production-only: unit tests inject `MapJsonLoader` instead.
|
||||
*
|
||||
* The path parameter matches what `ConfigMgr` requests, e.g. `configs/enemies`.
|
||||
* Cocos resolves it against the `assets/resources/` root.
|
||||
*/
|
||||
export class CCJsonLoader implements IJsonLoader {
|
||||
public load<T>(path: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
resources.load(path, JsonAsset, (err: Error | null, asset: unknown) => {
|
||||
if (err || !asset) {
|
||||
reject(err ?? new Error(`CCJsonLoader: asset not found at ${path}`));
|
||||
return;
|
||||
}
|
||||
resolve((asset as JsonAsset).json as T);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "933a1f2f-13d2-4c18-9a1d-62abd39776b2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { _decorator, Component, director, Color, Label, Node } from 'cc';
|
||||
import { ConfigMgr } from '../data/ConfigMgr';
|
||||
import { CCJsonLoader } from './CCJsonLoader';
|
||||
import { LevelMgr } from '../logic/LevelMgr';
|
||||
import { ensureCanvasSize, createLabel } from './MainMenuEntry';
|
||||
import { DESIGN_HEIGHT } from '../common/Constants';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Generic Level scene entry (task 7.1-7.2 hookup).
|
||||
*
|
||||
* Attach to the root node of `Level_1_1` … `Level_1_5`. Configure the
|
||||
* `levelId` property in the Inspector ("1-1", "1-2", ...).
|
||||
*
|
||||
* For MVP the scene has no real gameplay rendering yet; `autoBuildUI`
|
||||
* simply draws a top-centered label with the current level id so you can
|
||||
* verify scene transitions by eye.
|
||||
*/
|
||||
@ccclass('LevelEntry')
|
||||
export class LevelEntry extends Component {
|
||||
@property({ tooltip: '本关的 levelId (与 configs/levels.json 对应),如 1-1 / 1-2 / 1-3 / 1-4 / 1-5' })
|
||||
public levelId: string = '1-1';
|
||||
|
||||
@property({ tooltip: '胜利后跳转的场景,留空则按 1-1 → 1-2 → ... → 1-5 → Boss 自动推导' })
|
||||
public nextSceneName: string = '';
|
||||
|
||||
@property({ tooltip: '是否自动创建关卡 HUD (顶部显示 Level / 倒计时)' })
|
||||
public autoBuildUI: boolean = true;
|
||||
|
||||
private mgr: LevelMgr | undefined;
|
||||
private hudNode: Node | null = null;
|
||||
|
||||
protected async onLoad(): Promise<void> {
|
||||
if (this.autoBuildUI) this.buildDefaultUI();
|
||||
const cfg = new ConfigMgr(new CCJsonLoader());
|
||||
await cfg.load();
|
||||
this.mgr = new LevelMgr(cfg.level(this.levelId));
|
||||
}
|
||||
|
||||
protected update(dt: number): void {
|
||||
if (!this.mgr) return;
|
||||
const status = this.mgr.tick(dt);
|
||||
this.refreshHud();
|
||||
if (status === 'victory') {
|
||||
director.loadScene(this.nextSceneName || this.deriveNextScene());
|
||||
} else if (status === 'timeout' || status === 'player_dead') {
|
||||
director.loadScene('Settlement');
|
||||
}
|
||||
}
|
||||
|
||||
private deriveNextScene(): string {
|
||||
const map: Record<string, string> = {
|
||||
'1-1': 'Level_1_2',
|
||||
'1-2': 'Level_1_3',
|
||||
'1-3': 'Level_1_4',
|
||||
'1-4': 'Level_1_5',
|
||||
'1-5': 'Boss_ShuangHuanFang',
|
||||
};
|
||||
return map[this.levelId] ?? 'Settlement';
|
||||
}
|
||||
|
||||
private buildDefaultUI(): void {
|
||||
ensureCanvasSize(this.node);
|
||||
createLabel(this.node, `Level ${this.levelId}`, 0, DESIGN_HEIGHT / 2 - 50, 28, Color.WHITE);
|
||||
this.hudNode = createLabel(
|
||||
this.node,
|
||||
'Time: --',
|
||||
0,
|
||||
DESIGN_HEIGHT / 2 - 90,
|
||||
22,
|
||||
new Color(255, 220, 120, 255),
|
||||
);
|
||||
}
|
||||
|
||||
private refreshHud(): void {
|
||||
if (!this.hudNode || !this.mgr) return;
|
||||
const lb = this.hudNode.getComponent(Label);
|
||||
if (lb) {
|
||||
const r = this.mgr.result();
|
||||
lb.string = `Time: ${Math.max(0, Math.ceil(r.remainingSec))}s Kills: ${Object.values(r.kills).reduce((a, b) => a + b, 0)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "621ccf9a-6948-463f-9e06-bbb94027a369",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { _decorator, Component, director, Node, Label, Button, UITransform, Color, Vec3, Graphics } from 'cc';
|
||||
import { UIFlowMgr, ISceneEnter } from '../ui/UIFlowMgr';
|
||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** Maps abstract SceneId → physical Cocos Creator scene name. */
|
||||
const SCENE_MAP: Record<string, string> = {
|
||||
boot: 'Boot',
|
||||
story_intro: 'StoryIntro',
|
||||
main_menu: 'MainMenu',
|
||||
// level_select currently reuses MainMenu until a dedicated LevelSelect scene exists.
|
||||
level_select: 'MainMenu',
|
||||
// `gameplay` is dispatched by levelId — resolved at runtime.
|
||||
gameplay: 'Level_1_1',
|
||||
settlement: 'Settlement',
|
||||
// Settings panel is overlayed on MainMenu for the MVP.
|
||||
settings: 'MainMenu',
|
||||
};
|
||||
|
||||
/** levelId → physical scene name mapping (chapter 1 only). */
|
||||
const LEVEL_SCENE_MAP: Record<string, string> = {
|
||||
'1-1': 'Level_1_1',
|
||||
'1-2': 'Level_1_2',
|
||||
'1-3': 'Level_1_3',
|
||||
'1-4': 'Level_1_4',
|
||||
'1-5': 'Level_1_5',
|
||||
'1-5-boss': 'Boss_ShuangHuanFang',
|
||||
};
|
||||
|
||||
/**
|
||||
* MainMenu scene entry (task 9.2 hookup).
|
||||
*
|
||||
* Owns a `UIFlowMgr` instance and translates each abstract `onSceneEnter`
|
||||
* callback into a concrete `director.loadScene` call. Attach this component
|
||||
* to the root node of `MainMenu.scene`.
|
||||
*
|
||||
* When `autoBuildUI` is enabled (default), two centered buttons (Start /
|
||||
* Settings) and a title label are created programmatically so the scene is
|
||||
* usable out of the box even before any art pass.
|
||||
*/
|
||||
@ccclass('MainMenuEntry')
|
||||
export class MainMenuEntry extends Component {
|
||||
@property({ tooltip: '是否自动生成 Start / Settings 按钮 (方便没美术时就能跑通)' })
|
||||
public autoBuildUI: boolean = true;
|
||||
|
||||
private flow: UIFlowMgr | undefined;
|
||||
|
||||
protected onLoad(): void {
|
||||
this.flow = new UIFlowMgr(undefined, {
|
||||
onSceneEnter: (ev) => this.handleSceneEnter(ev),
|
||||
});
|
||||
if (this.autoBuildUI) this.buildDefaultUI();
|
||||
}
|
||||
|
||||
/** Bind this to the "Start" button's click event in the Inspector. */
|
||||
public onPressStart(): void {
|
||||
this.flow?.onPressStartGame();
|
||||
}
|
||||
|
||||
/** Bind this to the "Settings" button's click event. */
|
||||
public onPressSettings(): void {
|
||||
this.flow?.onOpenSettings();
|
||||
}
|
||||
|
||||
private handleSceneEnter(ev: ISceneEnter): void {
|
||||
const payload = ev.payload ?? {};
|
||||
if (ev.scene === 'gameplay' && typeof payload.levelId === 'string') {
|
||||
const physical = LEVEL_SCENE_MAP[payload.levelId];
|
||||
if (physical) {
|
||||
director.loadScene(physical);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const physical = SCENE_MAP[ev.scene];
|
||||
if (physical) director.loadScene(physical);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Auto-built UI (development affordance; art pass will replace it)
|
||||
// ------------------------------------------------------------------
|
||||
private buildDefaultUI(): void {
|
||||
// Ensure the host node has a UITransform matching the design resolution.
|
||||
ensureCanvasSize(this.node);
|
||||
|
||||
// Title.
|
||||
createLabel(this.node, '影 之 传 说', 0, 140, 44, Color.WHITE);
|
||||
|
||||
// Start button (centered, 40 above origin).
|
||||
createButton(this.node, 'Start', 0, 40, 220, 60, () => this.onPressStart());
|
||||
|
||||
// Settings button (centered, 40 below origin).
|
||||
createButton(this.node, 'Settings', 0, -40, 220, 60, () => this.onPressSettings());
|
||||
|
||||
// Hint line at the bottom.
|
||||
createLabel(this.node, 'Chapter 1 · MVP', 0, -200, 20, new Color(180, 180, 180, 255));
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// Shared UI helpers — intentionally kept inline (no external module) so
|
||||
// each Scene Entry stays self-contained and easy to remove once real UI is
|
||||
// authored in the editor.
|
||||
// ======================================================================
|
||||
|
||||
function ensureCanvasSize(host: Node): void {
|
||||
let ut = host.getComponent(UITransform);
|
||||
if (!ut) ut = host.addComponent(UITransform);
|
||||
ut.setContentSize(DESIGN_WIDTH, DESIGN_HEIGHT);
|
||||
host.setPosition(0, 0, 0);
|
||||
}
|
||||
|
||||
function createLabel(parent: Node, text: string, x: number, y: number, fontSize: number, color: Color): Node {
|
||||
const n = new Node('AutoLabel');
|
||||
n.layer = parent.layer;
|
||||
parent.addChild(n);
|
||||
const ut = n.addComponent(UITransform);
|
||||
ut.setContentSize(DESIGN_WIDTH, fontSize * 1.6);
|
||||
const lb = n.addComponent(Label);
|
||||
lb.useSystemFont = true;
|
||||
lb.string = text;
|
||||
lb.fontSize = fontSize;
|
||||
lb.lineHeight = Math.floor(fontSize * 1.2);
|
||||
lb.color = color;
|
||||
lb.horizontalAlign = 1; // CENTER
|
||||
lb.verticalAlign = 1; // CENTER
|
||||
n.setPosition(new Vec3(x, y, 0));
|
||||
return n;
|
||||
}
|
||||
|
||||
function createButton(
|
||||
parent: Node,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
onClick: () => void
|
||||
): Node {
|
||||
const n = new Node(`Btn_${text}`);
|
||||
n.layer = parent.layer;
|
||||
parent.addChild(n);
|
||||
const ut = n.addComponent(UITransform);
|
||||
ut.setContentSize(w, h);
|
||||
|
||||
// Background drawn via Graphics — avoids needing any texture asset.
|
||||
const g = n.addComponent(Graphics);
|
||||
g.fillColor = new Color(40, 40, 60, 230);
|
||||
g.rect(-w / 2, -h / 2, w, h);
|
||||
g.fill();
|
||||
g.strokeColor = new Color(200, 200, 220, 255);
|
||||
g.lineWidth = 2;
|
||||
g.rect(-w / 2, -h / 2, w, h);
|
||||
g.stroke();
|
||||
|
||||
// Label child for the text.
|
||||
const labelNode = new Node('Label');
|
||||
labelNode.layer = parent.layer;
|
||||
n.addChild(labelNode);
|
||||
const lut = labelNode.addComponent(UITransform);
|
||||
lut.setContentSize(w, h);
|
||||
const lb = labelNode.addComponent(Label);
|
||||
lb.useSystemFont = true;
|
||||
lb.string = text;
|
||||
lb.fontSize = 24;
|
||||
lb.lineHeight = 28;
|
||||
lb.color = Color.WHITE;
|
||||
lb.horizontalAlign = 1;
|
||||
lb.verticalAlign = 1;
|
||||
|
||||
const btn = n.addComponent(Button);
|
||||
btn.transition = Button.Transition.SCALE;
|
||||
btn.target = n;
|
||||
btn.zoomScale = 0.95;
|
||||
n.on(Node.EventType.TOUCH_END, onClick, n);
|
||||
|
||||
n.setPosition(new Vec3(x, y, 0));
|
||||
return n;
|
||||
}
|
||||
|
||||
// Re-export helpers so sibling Scene Entries can reuse them.
|
||||
export { ensureCanvasSize, createLabel, createButton };
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "3dc13e83-a51d-4530-ae5d-1ebceb69550f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { _decorator, Component, director, Label, Node, Color } from 'cc';
|
||||
import { ChapterSettlement, ISettlementStats } from '../logic/ChapterSettlement';
|
||||
import { ensureCanvasSize, createLabel, createButton } from './MainMenuEntry';
|
||||
import { DESIGN_HEIGHT } from '../common/Constants';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* Settlement scene entry (task 8.2 hookup, req 14.x).
|
||||
*
|
||||
* Attach to the root of `Settlement.scene`. All UI is auto-built by default
|
||||
* (title, score, closing line, back-to-menu button). If `autoBuildUI` is
|
||||
* disabled and you wire up `scoreLabelNode` / `closingLabelNode` manually,
|
||||
* those wins.
|
||||
*/
|
||||
@ccclass('SettlementEntry')
|
||||
export class SettlementEntry extends Component {
|
||||
@property({ type: Node, tooltip: '得分 Label 节点 (可留空自动创建)' })
|
||||
public scoreLabelNode: Node | null = null;
|
||||
|
||||
@property({ type: Node, tooltip: '结局旁白 Label 节点 (可留空自动创建)' })
|
||||
public closingLabelNode: Node | null = null;
|
||||
|
||||
@property({ tooltip: '是否自动创建结算界面 (Label + Back 按钮)' })
|
||||
public autoBuildUI: boolean = true;
|
||||
|
||||
protected onLoad(): void {
|
||||
if (this.autoBuildUI) this.buildDefaultUI();
|
||||
|
||||
const defaultStats: ISettlementStats = {
|
||||
totalScore: 0,
|
||||
stageScore: 0,
|
||||
comboCount: 0,
|
||||
flawless: true,
|
||||
remainingTimeSec: 0,
|
||||
};
|
||||
const settlement = new ChapterSettlement(defaultStats);
|
||||
const result = settlement.build();
|
||||
this.setLabel(this.scoreLabelNode, `Stage Score: ${result.stats.stageScore}`);
|
||||
this.setLabel(this.closingLabelNode, result.closingLine);
|
||||
}
|
||||
|
||||
/** Bind to a "Back to Menu" button. */
|
||||
public onReturnToMenu(): void {
|
||||
director.loadScene('MainMenu');
|
||||
}
|
||||
|
||||
private setLabel(node: Node | null, text: string): void {
|
||||
if (!node) return;
|
||||
const label = node.getComponent(Label);
|
||||
if (label) label.string = text;
|
||||
}
|
||||
|
||||
private buildDefaultUI(): void {
|
||||
ensureCanvasSize(this.node);
|
||||
// Title.
|
||||
createLabel(this.node, '章 节 结 算', 0, DESIGN_HEIGHT / 2 - 70, 40, Color.WHITE);
|
||||
// Score label (created only if inspector did not supply one).
|
||||
if (!this.scoreLabelNode) {
|
||||
this.scoreLabelNode = createLabel(this.node, '', 0, 40, 30, new Color(255, 220, 120, 255));
|
||||
}
|
||||
// Closing line label.
|
||||
if (!this.closingLabelNode) {
|
||||
this.closingLabelNode = createLabel(this.node, '', 0, -30, 24, new Color(200, 200, 200, 255));
|
||||
}
|
||||
// Bottom "Back to Menu" button.
|
||||
createButton(this.node, 'Back to Menu', 0, -DESIGN_HEIGHT / 2 + 80, 240, 60, () => this.onReturnToMenu());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "cab3e1e4-d2db-4b2f-bb14-179cfb6243eb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { _decorator, Component, director, Label, Node, Color, EventTouch, UITransform } from 'cc';
|
||||
import { ConfigMgr } from '../data/ConfigMgr';
|
||||
import { CCJsonLoader } from './CCJsonLoader';
|
||||
import { StorySceneCtrl } from '../ui/StorySceneCtrl';
|
||||
import { ensureCanvasSize, createLabel, createButton } from './MainMenuEntry';
|
||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* StoryIntro scene entry (task 9.1 hookup, req 19.x).
|
||||
*
|
||||
* Attach to the root of `StoryIntro.scene`. Inspector:
|
||||
* - labelNode: optional — if left empty and `autoBuildUI` is true, a
|
||||
* centered Label and a bottom-right "Skip" button are auto-created.
|
||||
* - storyId: story id in `configs/stories.json` (default `chapter_1_intro`).
|
||||
*
|
||||
* Tap anywhere → accelerate / advance (req 19.3).
|
||||
* "Skip" button → finish immediately (req 19.4).
|
||||
*/
|
||||
@ccclass('StorySceneEntry')
|
||||
export class StorySceneEntry extends Component {
|
||||
@property({ type: Node, tooltip: '打字机 Label 节点 (可留空,自动创建)' })
|
||||
public labelNode: Node | null = null;
|
||||
|
||||
@property({ tooltip: '对应 configs/stories.json 中的 id' })
|
||||
public storyId: string = 'chapter_1_intro';
|
||||
|
||||
@property({ tooltip: '是否自动创建 Label / Skip 按钮 / 背景遮罩' })
|
||||
public autoBuildUI: boolean = true;
|
||||
|
||||
private ctrl: StorySceneCtrl | undefined;
|
||||
|
||||
protected async onLoad(): Promise<void> {
|
||||
if (this.autoBuildUI && !this.labelNode) {
|
||||
this.buildDefaultUI();
|
||||
}
|
||||
// Full-screen tap accelerator: listen on the host node itself.
|
||||
this.node.on(Node.EventType.TOUCH_END, () => this.onTap(), this);
|
||||
|
||||
const cfg = await this.loadStoryConfig();
|
||||
this.ctrl = new StorySceneCtrl(cfg, undefined, {
|
||||
onTextChanged: (text) => this.updateLabel(text),
|
||||
onFinished: () => director.loadScene('Level_1_1'),
|
||||
});
|
||||
const outcome = this.ctrl.start();
|
||||
if (outcome === 'already_seen') {
|
||||
director.loadScene('Level_1_1');
|
||||
}
|
||||
}
|
||||
|
||||
protected update(dt: number): void {
|
||||
this.ctrl?.tick(dt);
|
||||
}
|
||||
|
||||
/** Tap handler (called by auto-built full-screen listener or external). */
|
||||
public onTap(): void {
|
||||
this.ctrl?.onTap();
|
||||
}
|
||||
|
||||
/** Bound to the "Skip" button. */
|
||||
public onSkip(): void {
|
||||
this.ctrl?.onSkip();
|
||||
}
|
||||
|
||||
private updateLabel(text: string): void {
|
||||
if (!this.labelNode) return;
|
||||
const label = this.labelNode.getComponent(Label);
|
||||
if (label) label.string = text;
|
||||
}
|
||||
|
||||
private async loadStoryConfig() {
|
||||
const mgr = new ConfigMgr(new CCJsonLoader());
|
||||
await mgr.load();
|
||||
return mgr.story(this.storyId);
|
||||
}
|
||||
|
||||
private buildDefaultUI(): void {
|
||||
ensureCanvasSize(this.node);
|
||||
// Ensure the root node can receive touch (size = design resolution).
|
||||
// Central typewriter label.
|
||||
this.labelNode = createLabel(this.node, '', 0, 0, 28, Color.WHITE);
|
||||
const ut = this.labelNode.getComponent(UITransform);
|
||||
if (ut) ut.setContentSize(DESIGN_WIDTH - 80, DESIGN_HEIGHT - 120);
|
||||
// Skip button at bottom-right.
|
||||
const skipX = DESIGN_WIDTH / 2 - 90;
|
||||
const skipY = -DESIGN_HEIGHT / 2 + 50;
|
||||
createButton(this.node, 'Skip >>', skipX, skipY, 140, 50, () => this.onSkip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "2c3b5227-420d-4421-a500-36519776ea9d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user