create-pixifact 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/createPixifactProject.d.ts +3 -2
- package/dist/index.js +34 -14
- package/dist/skills/pixifact/SKILL.md +27 -4
- package/dist/skills/pixifact/agents/openai.yaml +2 -2
- package/dist/skills/pixifact/references/scene-authoring-decisions.md +2 -0
- package/dist/skills/pixifact/references/scene-hierarchy-patterns.md +38 -0
- package/dist/skills/pixifact/references/scene-objects.md +40 -2
- package/package.json +5 -2
- package/templates/game/.env +1 -0
- package/templates/game/README.md +119 -0
- package/templates/game/assets/ui/button.png +0 -0
- package/templates/game/assets/ui/panel.png +0 -0
- package/templates/game/bunfig.toml +1 -0
- package/templates/game/index.html +12 -0
- package/templates/game/package.json +24 -0
- package/templates/game/pixifact.project.json +11 -0
- package/templates/game/src/GameApp.ts +118 -0
- package/templates/game/src/core/GameSession.ts +43 -0
- package/templates/game/src/core/ScreenManager.ts +26 -0
- package/templates/game/src/core/WebRecordStore.ts +14 -0
- package/templates/game/src/main.ts +64 -0
- package/templates/game/src/screens/MainMenu.scene +57 -0
- package/templates/game/src/screens/MainMenu.ts +101 -0
- package/templates/game/src/screens/PlayScreen.scene +13 -0
- package/templates/game/src/screens/PlayScreen.ts +31 -0
- package/templates/game/src/screens/ResultScreen.scene +13 -0
- package/templates/game/src/screens/ResultScreen.ts +25 -0
- package/templates/game/src/styles.css +21 -0
- package/templates/game/src/ui/Button.scene +10 -0
- package/templates/game/src/ui/Button.ts +84 -0
- package/templates/game/src/ui/Dialog.scene +10 -0
- package/templates/game/src/ui/Dialog.ts +31 -0
- package/templates/game/src/ui/Panel.scene +7 -0
- package/templates/game/src/ui/Panel.ts +11 -0
- package/templates/game/src/ui/ProgressBar.scene +4 -0
- package/templates/game/src/ui/ProgressBar.ts +25 -0
- package/templates/game/src/ui/nine/ButtonBackground.scene +3 -0
- package/templates/game/src/ui/nine/ButtonBackground.ts +5 -0
- package/templates/game/src/ui/nine/PanelBackground.scene +3 -0
- package/templates/game/src/ui/nine/PanelBackground.ts +5 -0
- package/templates/game/src/vite-env.d.ts +2 -0
- package/templates/game/tsconfig.json +15 -0
- package/templates/game/vite.config.ts +9 -0
- package/templates/minimal/package.json +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Group, setFrameLayout } from 'pixifact/runtime';
|
|
2
|
+
import project from '../pixifact.project.json';
|
|
3
|
+
import { GameSession } from './core/GameSession';
|
|
4
|
+
import { ScreenManager } from './core/ScreenManager';
|
|
5
|
+
import { WebRecordStore } from './core/WebRecordStore';
|
|
6
|
+
import { MainMenu } from './screens/MainMenu';
|
|
7
|
+
import { PlayScreen } from './screens/PlayScreen';
|
|
8
|
+
import { ResultScreen } from './screens/ResultScreen';
|
|
9
|
+
import { Dialog } from './ui/Dialog';
|
|
10
|
+
|
|
11
|
+
export class GameApp extends Group {
|
|
12
|
+
private readonly session: GameSession;
|
|
13
|
+
private readonly screens = new ScreenManager();
|
|
14
|
+
private readonly modals = new ScreenManager();
|
|
15
|
+
private modalOpen = false;
|
|
16
|
+
|
|
17
|
+
constructor(private readonly records: WebRecordStore) {
|
|
18
|
+
super(project.resolution);
|
|
19
|
+
this.session = new GameSession(records.read());
|
|
20
|
+
for (const layer of [this.screens, this.modals]) {
|
|
21
|
+
setFrameLayout(layer, { left: 0, right: 0, top: 0, bottom: 0 });
|
|
22
|
+
}
|
|
23
|
+
this.addChild(this.screens, this.modals);
|
|
24
|
+
this.showMenu();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
snapshot() {
|
|
28
|
+
const screen = this.screens.children[0];
|
|
29
|
+
return {
|
|
30
|
+
...this.session.snapshot(),
|
|
31
|
+
modalOpen: this.modalOpen,
|
|
32
|
+
showcase: screen instanceof MainMenu ? screen.snapshot() : null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pause() {
|
|
37
|
+
if (this.session.snapshot().phase !== 'playing') return;
|
|
38
|
+
this.session.pause();
|
|
39
|
+
this.screens.interactiveChildren = false;
|
|
40
|
+
this.modalOpen = true;
|
|
41
|
+
const dialog = new Dialog();
|
|
42
|
+
dialog.title = '休息一下';
|
|
43
|
+
dialog.message = '星光会等你回来。';
|
|
44
|
+
dialog.confirmText = '继续游戏';
|
|
45
|
+
dialog.cancelText = '返回组件展台';
|
|
46
|
+
this.modals.replace(dialog);
|
|
47
|
+
this.modals.track(dialog.confirmed.connect(() => {
|
|
48
|
+
this.session.resume();
|
|
49
|
+
this.closeModal();
|
|
50
|
+
}));
|
|
51
|
+
this.modals.track(dialog.cancelled.connect(() => this.showMenu()));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private start() {
|
|
55
|
+
this.closeModal();
|
|
56
|
+
this.session.start();
|
|
57
|
+
const screen = new PlayScreen();
|
|
58
|
+
this.screens.replace(screen);
|
|
59
|
+
screen.renderProgress(0, this.session.snapshot().goal);
|
|
60
|
+
this.screens.track(screen.paused.connect(() => this.pause()));
|
|
61
|
+
this.screens.track(screen.collected.connect(() => {
|
|
62
|
+
const previousBest = this.session.snapshot().bestScore;
|
|
63
|
+
this.session.collect();
|
|
64
|
+
const state = this.session.snapshot();
|
|
65
|
+
if (state.bestScore > previousBest) this.records.write(state.bestScore);
|
|
66
|
+
if (state.phase === 'finished') {
|
|
67
|
+
this.showResult();
|
|
68
|
+
} else {
|
|
69
|
+
screen.renderProgress(state.score, state.goal);
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private showMenu() {
|
|
75
|
+
this.closeModal();
|
|
76
|
+
this.session.menu();
|
|
77
|
+
const screen = new MainMenu();
|
|
78
|
+
screen.bestText = `最高记录 · ${this.session.snapshot().bestScore}`;
|
|
79
|
+
this.screens.replace(screen);
|
|
80
|
+
this.screens.track(screen.started.connect(() => this.start()));
|
|
81
|
+
this.screens.track(screen.dialogRequested.connect(() => this.previewDialog(screen)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private previewDialog(screen: MainMenu) {
|
|
85
|
+
this.screens.interactiveChildren = false;
|
|
86
|
+
this.modalOpen = true;
|
|
87
|
+
const dialog = new Dialog();
|
|
88
|
+
dialog.title = 'Dialog · 弹窗预览';
|
|
89
|
+
dialog.message = '确认或取消后,按钮会显示结果。';
|
|
90
|
+
dialog.confirmText = '确认';
|
|
91
|
+
dialog.cancelText = '取消';
|
|
92
|
+
this.modals.replace(dialog);
|
|
93
|
+
this.modals.track(dialog.confirmed.connect(() => {
|
|
94
|
+
this.closeModal();
|
|
95
|
+
screen.reportDialog(true);
|
|
96
|
+
}));
|
|
97
|
+
this.modals.track(dialog.cancelled.connect(() => {
|
|
98
|
+
this.closeModal();
|
|
99
|
+
screen.reportDialog(false);
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private showResult() {
|
|
104
|
+
const state = this.session.snapshot();
|
|
105
|
+
const screen = new ResultScreen();
|
|
106
|
+
screen.scoreText = `本局 · ${state.score} 份星光`;
|
|
107
|
+
screen.bestText = `最高记录 · ${state.bestScore}`;
|
|
108
|
+
this.screens.replace(screen);
|
|
109
|
+
this.screens.track(screen.restarted.connect(() => this.start()));
|
|
110
|
+
this.screens.track(screen.returned.connect(() => this.showMenu()));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private closeModal() {
|
|
114
|
+
this.modals.clear();
|
|
115
|
+
this.modalOpen = false;
|
|
116
|
+
this.screens.interactiveChildren = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface GameSnapshot {
|
|
2
|
+
phase: 'menu' | 'playing' | 'paused' | 'finished';
|
|
3
|
+
score: number;
|
|
4
|
+
goal: number;
|
|
5
|
+
bestScore: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class GameSession {
|
|
9
|
+
private state: GameSnapshot;
|
|
10
|
+
|
|
11
|
+
constructor(bestScore: number, goal = 8) {
|
|
12
|
+
this.state = { phase: 'menu', score: 0, goal, bestScore };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start() {
|
|
16
|
+
this.state.phase = 'playing';
|
|
17
|
+
this.state.score = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
collect() {
|
|
21
|
+
if (this.state.phase !== 'playing') return;
|
|
22
|
+
this.state.score += 1;
|
|
23
|
+
this.state.bestScore = Math.max(this.state.bestScore, this.state.score);
|
|
24
|
+
if (this.state.score === this.state.goal) this.state.phase = 'finished';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pause() {
|
|
28
|
+
if (this.state.phase === 'playing') this.state.phase = 'paused';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resume() {
|
|
32
|
+
if (this.state.phase === 'paused') this.state.phase = 'playing';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
menu() {
|
|
36
|
+
this.state.phase = 'menu';
|
|
37
|
+
this.state.score = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
snapshot(): GameSnapshot {
|
|
41
|
+
return { ...this.state };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DestroyOptions } from 'pixi.js';
|
|
2
|
+
import { Group, setFrameLayout } from 'pixifact/runtime';
|
|
3
|
+
|
|
4
|
+
export class ScreenManager extends Group {
|
|
5
|
+
private disconnects: (() => void)[] = [];
|
|
6
|
+
|
|
7
|
+
replace(screen: Group) {
|
|
8
|
+
this.clear();
|
|
9
|
+
setFrameLayout(screen, { left: 0, right: 0, top: 0, bottom: 0 });
|
|
10
|
+
this.addChild(screen);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
track(disconnect: () => void) {
|
|
14
|
+
this.disconnects.push(disconnect);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
clear() {
|
|
18
|
+
for (const disconnect of this.disconnects.splice(0)) disconnect();
|
|
19
|
+
for (const screen of this.removeChildren()) screen.destroy({ children: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override destroy(options?: DestroyOptions) {
|
|
23
|
+
this.clear();
|
|
24
|
+
super.destroy(options);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const recordKey = 'pixifact-game-starter.best-score';
|
|
2
|
+
|
|
3
|
+
export class WebRecordStore {
|
|
4
|
+
constructor(private readonly storage: Pick<Storage, 'getItem' | 'setItem'>) {}
|
|
5
|
+
|
|
6
|
+
read(): number {
|
|
7
|
+
const saved = this.storage.getItem(recordKey);
|
|
8
|
+
return saved === null ? 0 : JSON.parse(saved) as number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
write(bestScore: number) {
|
|
12
|
+
this.storage.setItem(recordKey, JSON.stringify(bestScore));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import './styles.css';
|
|
2
|
+
import 'pixifact:scenes';
|
|
3
|
+
import { Assets } from 'pixi.js';
|
|
4
|
+
import { createApplication } from 'pixifact:platform';
|
|
5
|
+
import manifest from 'pixifact:assets';
|
|
6
|
+
import type { PixifactProjectConfig } from 'pixifact';
|
|
7
|
+
import { applyPixifactViewportLayout, calculatePixifactViewportLayout } from 'pixifact/runtime';
|
|
8
|
+
import { prepareSceneClass } from 'pixifact/scene';
|
|
9
|
+
import projectJson from '../pixifact.project.json';
|
|
10
|
+
import { GameApp } from './GameApp';
|
|
11
|
+
import { WebRecordStore } from './core/WebRecordStore';
|
|
12
|
+
import { MainMenu } from './screens/MainMenu';
|
|
13
|
+
import { PlayScreen } from './screens/PlayScreen';
|
|
14
|
+
import { ResultScreen } from './screens/ResultScreen';
|
|
15
|
+
import { Dialog } from './ui/Dialog';
|
|
16
|
+
|
|
17
|
+
const root = document.querySelector<HTMLElement>('#game')!;
|
|
18
|
+
const screen = root.getBoundingClientRect();
|
|
19
|
+
const app = await createApplication({
|
|
20
|
+
width: screen.width,
|
|
21
|
+
height: screen.height,
|
|
22
|
+
backgroundColor: 0xe5e9e3,
|
|
23
|
+
antialias: true,
|
|
24
|
+
autoDensity: true,
|
|
25
|
+
resolution: Math.min(window.devicePixelRatio, 2),
|
|
26
|
+
});
|
|
27
|
+
root.append(app.canvas);
|
|
28
|
+
|
|
29
|
+
await Assets.init({ manifest });
|
|
30
|
+
await Promise.all([MainMenu, PlayScreen, ResultScreen, Dialog].map((scene) => prepareSceneClass(scene)));
|
|
31
|
+
const game = new GameApp(new WebRecordStore(window.localStorage));
|
|
32
|
+
app.stage.addChild(game);
|
|
33
|
+
|
|
34
|
+
const project = projectJson as PixifactProjectConfig;
|
|
35
|
+
function resize() {
|
|
36
|
+
const bounds = root.getBoundingClientRect();
|
|
37
|
+
app.renderer.resize(bounds.width, bounds.height);
|
|
38
|
+
applyPixifactViewportLayout({ root: game, stage: app.stage }, calculatePixifactViewportLayout({
|
|
39
|
+
resolution: project.resolution,
|
|
40
|
+
screen: { width: bounds.width, height: bounds.height },
|
|
41
|
+
mode: project.viewport.mode,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
resize();
|
|
45
|
+
const observer = new ResizeObserver(resize);
|
|
46
|
+
observer.observe(root);
|
|
47
|
+
|
|
48
|
+
function onVisibilityChange() {
|
|
49
|
+
if (document.hidden) game.pause();
|
|
50
|
+
}
|
|
51
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
52
|
+
|
|
53
|
+
if (import.meta.env.DEV) {
|
|
54
|
+
const { registerPixiRuntime } = await import('pixifact/runtime-dev');
|
|
55
|
+
registerPixiRuntime(app, { getState: () => game.snapshot() });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (import.meta.hot) {
|
|
59
|
+
import.meta.hot.dispose(() => {
|
|
60
|
+
observer.disconnect();
|
|
61
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
62
|
+
app.destroy({ removeView: true }, { children: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<Scene name="MainMenu" width="1080" height="1920">
|
|
2
|
+
<Panel id="page" scene="../ui/Panel.scene" left="0" right="0" top="0" bottom="0" title="Pixifact / UI 组件库">
|
|
3
|
+
<ScrollContainer id="menuScroll" slot="content" left="0" right="0" top="0" bottom="0" direction="vertical">
|
|
4
|
+
<VBoxContainer id="menuContent" width="1016" height="1424" gap="24" alignX="center">
|
|
5
|
+
<Group id="intro" width="1016" height="144">
|
|
6
|
+
<Label id="headline" left="8" right="8" top="0" height="72" text="基础组件,直接试用" fontSize="54" fontWeight="700" fill="#25322d" alignY="center" />
|
|
7
|
+
<Label id="description" left="8" right="8" top="88" height="48" text="四个基础组件,点击查看状态。" fontSize="32" fill="#516158" alignY="center" />
|
|
8
|
+
</Group>
|
|
9
|
+
|
|
10
|
+
<Panel id="buttonsPanel" scene="../ui/Panel.scene" width="1016" height="384" title="01 / Button 按钮">
|
|
11
|
+
<Group id="buttonsContent" slot="content" left="0" right="0" top="0" bottom="0">
|
|
12
|
+
<Group id="sizeRow" horizontal="0" top="0" width="888" height="96">
|
|
13
|
+
<Button id="sampleButton" scene="../ui/Button.scene" left="0" top="0" width="360" height="96" size="large" text="大号" @clicked="click">
|
|
14
|
+
<Group id="sampleIcon" slot="icon" left="0" right="0" top="0" bottom="0" eventMode="none">
|
|
15
|
+
<Label id="star" left="0" right="0" top="0" bottom="0" text="★" fontSize="36" fill="#25322d" alignX="center" alignY="center" />
|
|
16
|
+
</Group>
|
|
17
|
+
</Button>
|
|
18
|
+
<Button id="mediumButton" scene="../ui/Button.scene" left="384" top="8" width="280" height="80" size="medium" text="中号" @clicked="click" />
|
|
19
|
+
<Button id="smallButton" scene="../ui/Button.scene" right="0" top="16" width="200" height="64" size="small" text="小号" @clicked="click" />
|
|
20
|
+
</Group>
|
|
21
|
+
<Group id="stateRow" horizontal="0" top="120" width="584" height="80">
|
|
22
|
+
<Button id="disabledButton" scene="../ui/Button.scene" left="0" top="0" width="280" height="80" text="{previewText}" disabled="{previewDisabled}" @clicked="click" />
|
|
23
|
+
<Button id="toggleButton" scene="../ui/Button.scene" right="0" top="0" width="280" height="80" text="{toggleText}" @clicked="togglePreview" />
|
|
24
|
+
</Group>
|
|
25
|
+
<Label id="clickCount" left="0" right="0" top="216" height="40" text="{clickText}" fontSize="32" fill="#516158" alignX="center" alignY="center" />
|
|
26
|
+
</Group>
|
|
27
|
+
</Panel>
|
|
28
|
+
|
|
29
|
+
<Panel id="progressPanel" scene="../ui/Panel.scene" width="1016" height="288" title="02 / ProgressBar 进度条">
|
|
30
|
+
<Group id="progressContent" slot="content" left="0" right="0" top="0" bottom="0">
|
|
31
|
+
<Label id="progressLabel" left="0" right="0" top="0" height="40" text="{progressText}" fontSize="30" fill="#516158" alignY="center" />
|
|
32
|
+
<ProgressBar id="progressBar" scene="../ui/ProgressBar.scene" left="0" right="0" top="44" height="20" />
|
|
33
|
+
<Group id="progressActions" horizontal="0" top="96" width="424" height="64">
|
|
34
|
+
<Button id="decreaseButton" scene="../ui/Button.scene" left="0" top="0" width="200" height="64" size="small" text="− 25%" @clicked="decrease" />
|
|
35
|
+
<Button id="increaseButton" scene="../ui/Button.scene" right="0" top="0" width="200" height="64" size="small" text="+ 25%" @clicked="increase" />
|
|
36
|
+
</Group>
|
|
37
|
+
</Group>
|
|
38
|
+
</Panel>
|
|
39
|
+
|
|
40
|
+
<Panel id="panelSample" scene="../ui/Panel.scene" width="1016" height="184" title="03 / Panel 面板">
|
|
41
|
+
<Group id="panelContent" slot="content" left="0" right="0" top="0" bottom="0">
|
|
42
|
+
<Label id="slotDescription" left="0" top="0" width="550" height="48" text="内容区可组合任意组件" fontSize="30" fill="#516158" alignY="center" />
|
|
43
|
+
<Label id="record" right="0" top="0" width="300" height="48" text="{bestText}" fontSize="30" fill="#516158" alignX="end" alignY="center" />
|
|
44
|
+
</Group>
|
|
45
|
+
</Panel>
|
|
46
|
+
|
|
47
|
+
<Panel id="dialogPanel" scene="../ui/Panel.scene" width="1016" height="224" title="04 / Dialog 弹窗">
|
|
48
|
+
<Group id="dialogContent" slot="content" left="0" right="0" top="0" bottom="0">
|
|
49
|
+
<Button id="dialogButton" scene="../ui/Button.scene" horizontal="0" top="8" width="360" height="80" text="{dialogText}" @clicked="openDialog" />
|
|
50
|
+
</Group>
|
|
51
|
+
</Panel>
|
|
52
|
+
|
|
53
|
+
<Button id="startButton" scene="../ui/Button.scene" width="360" height="80" text="体验游戏流程" @clicked="start" />
|
|
54
|
+
</VBoxContainer>
|
|
55
|
+
</ScrollContainer>
|
|
56
|
+
</Panel>
|
|
57
|
+
</Scene>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Group } from 'pixifact/runtime';
|
|
2
|
+
import { createEvent, event, part, prop, scene } from 'pixifact/scene';
|
|
3
|
+
import { Button } from '../ui/Button';
|
|
4
|
+
import { ProgressBar } from '../ui/ProgressBar';
|
|
5
|
+
|
|
6
|
+
@scene()
|
|
7
|
+
export class MainMenu extends Group {
|
|
8
|
+
@prop({ default: '最高记录 · 0' })
|
|
9
|
+
declare bestText: string;
|
|
10
|
+
|
|
11
|
+
@prop({ default: '已点击 · 0 次' })
|
|
12
|
+
declare clickText: string;
|
|
13
|
+
|
|
14
|
+
@prop({ default: true })
|
|
15
|
+
declare previewDisabled: boolean;
|
|
16
|
+
|
|
17
|
+
@prop({ default: '已禁用' })
|
|
18
|
+
declare previewText: string;
|
|
19
|
+
|
|
20
|
+
@prop({ default: '启用按钮' })
|
|
21
|
+
declare toggleText: string;
|
|
22
|
+
|
|
23
|
+
@prop({ default: '当前进度 · 50%' })
|
|
24
|
+
declare progressText: string;
|
|
25
|
+
|
|
26
|
+
@prop({ default: '打开弹窗' })
|
|
27
|
+
declare dialogText: string;
|
|
28
|
+
|
|
29
|
+
@part()
|
|
30
|
+
protected declare progressBar: ProgressBar;
|
|
31
|
+
|
|
32
|
+
@part()
|
|
33
|
+
protected declare decreaseButton: Button;
|
|
34
|
+
|
|
35
|
+
@part()
|
|
36
|
+
protected declare increaseButton: Button;
|
|
37
|
+
|
|
38
|
+
@event()
|
|
39
|
+
readonly started = createEvent();
|
|
40
|
+
|
|
41
|
+
@event()
|
|
42
|
+
readonly dialogRequested = createEvent();
|
|
43
|
+
|
|
44
|
+
private buttonClicks = 0;
|
|
45
|
+
private progress = 50;
|
|
46
|
+
private dialogResult: 'confirmed' | 'cancelled' | null = null;
|
|
47
|
+
|
|
48
|
+
onMounted() {
|
|
49
|
+
this.renderProgress();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
snapshot() {
|
|
53
|
+
return {
|
|
54
|
+
buttonClicks: this.buttonClicks,
|
|
55
|
+
previewDisabled: this.previewDisabled,
|
|
56
|
+
progress: this.progress,
|
|
57
|
+
dialogResult: this.dialogResult,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
click() {
|
|
62
|
+
this.buttonClicks += 1;
|
|
63
|
+
this.clickText = `已点击 · ${this.buttonClicks} 次`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
togglePreview() {
|
|
67
|
+
this.previewDisabled = !this.previewDisabled;
|
|
68
|
+
this.previewText = this.previewDisabled ? '已禁用' : '可点击';
|
|
69
|
+
this.toggleText = this.previewDisabled ? '启用按钮' : '禁用按钮';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
decrease() {
|
|
73
|
+
this.progress = Math.max(0, this.progress - 25);
|
|
74
|
+
this.renderProgress();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
increase() {
|
|
78
|
+
this.progress = Math.min(100, this.progress + 25);
|
|
79
|
+
this.renderProgress();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
openDialog() {
|
|
83
|
+
this.dialogRequested.emit();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
reportDialog(confirmed: boolean) {
|
|
87
|
+
this.dialogResult = confirmed ? 'confirmed' : 'cancelled';
|
|
88
|
+
this.dialogText = confirmed ? '已确认 · 再试' : '已取消 · 再试';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start() {
|
|
92
|
+
this.started.emit();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private renderProgress() {
|
|
96
|
+
this.progressBar.setProgress(this.progress, 100);
|
|
97
|
+
this.progressText = `当前进度 · ${this.progress}%`;
|
|
98
|
+
this.decreaseButton.disabled = this.progress === 0;
|
|
99
|
+
this.increaseButton.disabled = this.progress === 100;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<Scene name="PlayScreen" width="1080" height="1920">
|
|
2
|
+
<Panel id="page" scene="../ui/Panel.scene" left="0" right="0" top="0" bottom="0" title="收集星光">
|
|
3
|
+
<ScrollContainer id="playScroll" slot="content" left="0" right="0" top="0" bottom="0" direction="vertical">
|
|
4
|
+
<Group id="playContent" width="1016" height="840">
|
|
5
|
+
<Label id="score" left="48" right="48" top="100" height="72" text="{scoreText}" fontSize="42" fontWeight="700" fill="#25322d" alignX="center" alignY="center" />
|
|
6
|
+
<ProgressBar id="progress" scene="../ui/ProgressBar.scene" left="80" right="80" top="218" height="24" />
|
|
7
|
+
<Label id="instruction" left="48" right="48" top="330" height="80" text="点击下方按钮,收集一份星光。" fontSize="26" fill="#516158" alignX="center" alignY="center" />
|
|
8
|
+
<Button id="collectButton" scene="../ui/Button.scene" horizontal="0" top="520" width="420" height="96" size="large" text="✦ 收集星光" @clicked="collect" />
|
|
9
|
+
<Button id="pauseButton" scene="../ui/Button.scene" horizontal="0" bottom="60" width="280" height="80" text="暂停" @clicked="pause" />
|
|
10
|
+
</Group>
|
|
11
|
+
</ScrollContainer>
|
|
12
|
+
</Panel>
|
|
13
|
+
</Scene>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Group } from 'pixifact/runtime';
|
|
2
|
+
import { createEvent, event, part, prop, scene } from 'pixifact/scene';
|
|
3
|
+
import { ProgressBar } from '../ui/ProgressBar';
|
|
4
|
+
|
|
5
|
+
@scene()
|
|
6
|
+
export class PlayScreen extends Group {
|
|
7
|
+
@prop({ default: '星光 · 0 / 8' })
|
|
8
|
+
declare scoreText: string;
|
|
9
|
+
|
|
10
|
+
@part()
|
|
11
|
+
protected declare progress: ProgressBar;
|
|
12
|
+
|
|
13
|
+
@event()
|
|
14
|
+
readonly collected = createEvent();
|
|
15
|
+
|
|
16
|
+
@event()
|
|
17
|
+
readonly paused = createEvent();
|
|
18
|
+
|
|
19
|
+
collect() {
|
|
20
|
+
this.collected.emit();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pause() {
|
|
24
|
+
this.paused.emit();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
renderProgress(score: number, goal: number) {
|
|
28
|
+
this.scoreText = `星光 · ${score} / ${goal}`;
|
|
29
|
+
this.progress.setProgress(score, goal);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<Scene name="ResultScreen" width="1080" height="1920">
|
|
2
|
+
<Panel id="page" scene="../ui/Panel.scene" left="0" right="0" top="0" bottom="0" title="本局完成">
|
|
3
|
+
<ScrollContainer id="resultScroll" slot="content" left="0" right="0" top="0" bottom="0" direction="vertical">
|
|
4
|
+
<Group id="resultContent" width="1016" height="840">
|
|
5
|
+
<Label id="headline" left="44" right="44" top="174" height="100" text="星光已收集" fontSize="50" fontWeight="700" fill="#25322d" alignX="center" alignY="center" />
|
|
6
|
+
<Label id="score" left="48" right="48" top="348" height="64" text="{scoreText}" fontSize="32" fill="#25322d" alignX="center" />
|
|
7
|
+
<Label id="record" left="48" right="48" top="438" height="56" text="{bestText}" fontSize="26" fill="#516158" alignX="center" />
|
|
8
|
+
<Button id="restartButton" scene="../ui/Button.scene" horizontal="0" top="598" width="280" height="80" text="再玩一次" @clicked="restart" />
|
|
9
|
+
<Button id="menuButton" scene="../ui/Button.scene" horizontal="0" top="694" width="360" height="80" text="返回组件展台" @clicked="menu" />
|
|
10
|
+
</Group>
|
|
11
|
+
</ScrollContainer>
|
|
12
|
+
</Panel>
|
|
13
|
+
</Scene>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Group } from 'pixifact/runtime';
|
|
2
|
+
import { createEvent, event, prop, scene } from 'pixifact/scene';
|
|
3
|
+
|
|
4
|
+
@scene()
|
|
5
|
+
export class ResultScreen extends Group {
|
|
6
|
+
@prop({ default: '本局 · 8 份星光' })
|
|
7
|
+
declare scoreText: string;
|
|
8
|
+
|
|
9
|
+
@prop({ default: '最高记录 · 8' })
|
|
10
|
+
declare bestText: string;
|
|
11
|
+
|
|
12
|
+
@event()
|
|
13
|
+
readonly restarted = createEvent();
|
|
14
|
+
|
|
15
|
+
@event()
|
|
16
|
+
readonly returned = createEvent();
|
|
17
|
+
|
|
18
|
+
restart() {
|
|
19
|
+
this.restarted.emit();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
menu() {
|
|
23
|
+
this.returned.emit();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<Scene name="Button" width="280" height="80" eventMode="static" cursor="pointer" on:pointerdown="press" on:pointerup="release" on:pointerupoutside="release" on:pointerout="release" on:pointertap="activate">
|
|
2
|
+
<Group id="visual" left="0" right="0" top="0" bottom="0" eventMode="none">
|
|
3
|
+
<ButtonBackground id="background" scene="./nine/ButtonBackground.scene" left="0" right="0" top="0" bottom="0" />
|
|
4
|
+
<Label id="label" text="{text}" left="{size.labelPadding}" right="{size.labelPadding}" top="0" bottom="0" fontSize="{size.fontSize}" fontFamily="sans-serif" fontWeight="700" fill="#25322d" alignX="center" alignY="center" eventMode="none" />
|
|
5
|
+
<Group id="iconHost" left="{size.iconInset}" vertical="0" width="{size.iconSize}" height="{size.iconSize}" eventMode="none">
|
|
6
|
+
<slot name="icon" />
|
|
7
|
+
</Group>
|
|
8
|
+
<Rect id="disabledShade" left="0" right="0" top="0" bottom="0" fillColor="#25322d" fillAlpha="0.18" radius="18" visible="{disabled}" eventMode="none" />
|
|
9
|
+
</Group>
|
|
10
|
+
</Scene>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Container } from 'pixi.js';
|
|
2
|
+
import { Group } from 'pixifact/runtime';
|
|
3
|
+
import { bindSceneNodeProp, createEvent, defineVariants, event, part, prop, scene, slot } from 'pixifact/scene';
|
|
4
|
+
|
|
5
|
+
const buttonSizes = defineVariants({
|
|
6
|
+
large: { width: 360, height: 96, fontSize: 42, labelPadding: 60, iconInset: 28, iconSize: 36 },
|
|
7
|
+
medium: { width: 280, height: 80, fontSize: 36, labelPadding: 48, iconInset: 24, iconSize: 32 },
|
|
8
|
+
small: { width: 200, height: 64, fontSize: 32, labelPadding: 32, iconInset: 16, iconSize: 28 },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
@scene()
|
|
12
|
+
export class Button extends Group {
|
|
13
|
+
@prop({ default: '按钮' })
|
|
14
|
+
declare text: string;
|
|
15
|
+
|
|
16
|
+
@prop({ default: false })
|
|
17
|
+
declare disabled: boolean;
|
|
18
|
+
|
|
19
|
+
@prop({ default: 'medium', variants: buttonSizes })
|
|
20
|
+
declare size: keyof typeof buttonSizes;
|
|
21
|
+
|
|
22
|
+
@part()
|
|
23
|
+
protected declare visual: Group;
|
|
24
|
+
|
|
25
|
+
@event()
|
|
26
|
+
readonly clicked = createEvent();
|
|
27
|
+
|
|
28
|
+
@slot()
|
|
29
|
+
declare readonly icon: Container;
|
|
30
|
+
|
|
31
|
+
private releasedAt: number | null = null;
|
|
32
|
+
|
|
33
|
+
onMounted() {
|
|
34
|
+
bindSceneNodeProp(this, { kind: 'binding', path: ['size', 'width'] }, this, 'width');
|
|
35
|
+
bindSceneNodeProp(this, { kind: 'binding', path: ['size', 'height'] }, this, 'height');
|
|
36
|
+
this.on('pointercancel', this.release, this);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
press() {
|
|
40
|
+
if (this.disabled) return;
|
|
41
|
+
this.releasedAt = null;
|
|
42
|
+
this.setVisualScale(0.97);
|
|
43
|
+
this.onRender = this.updateAnimation;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
release() {
|
|
47
|
+
if (this.visual.scale.x === 1) return;
|
|
48
|
+
this.releasedAt = performance.now();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
activate() {
|
|
52
|
+
if (this.disabled) return;
|
|
53
|
+
this.press();
|
|
54
|
+
this.release();
|
|
55
|
+
this.clicked.emit();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override layout() {
|
|
59
|
+
super.layout();
|
|
60
|
+
// Group 在 Scene 挂载完成前也会布局;@part 在挂载后注入。
|
|
61
|
+
if (this.visual) this.setVisualScale(this.visual.scale.x);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private setVisualScale(scale: number) {
|
|
65
|
+
this.visual.scale.set(scale);
|
|
66
|
+
this.visual.position.set(this.width * (1 - scale) / 2, this.height * (1 - scale) / 2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private updateAnimation = () => {
|
|
70
|
+
if (this.disabled) {
|
|
71
|
+
this.setVisualScale(1);
|
|
72
|
+
this.releasedAt = null;
|
|
73
|
+
this.onRender = null;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (this.releasedAt === null) return;
|
|
77
|
+
const progress = Math.min(1, (performance.now() - this.releasedAt) / 140);
|
|
78
|
+
this.setVisualScale(1 - 0.03 * (1 - progress) ** 3);
|
|
79
|
+
if (progress === 1) {
|
|
80
|
+
this.releasedAt = null;
|
|
81
|
+
this.onRender = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<Scene name="Dialog" width="1080" height="1920" eventMode="static">
|
|
2
|
+
<Rect id="shade" left="0" right="0" top="0" bottom="0" fillColor="#25322d" fillAlpha="0.42" eventMode="none" />
|
|
3
|
+
<Group id="body" horizontal="0" vertical="0" width="800" height="400">
|
|
4
|
+
<PanelBackground id="background" scene="./nine/PanelBackground.scene" left="0" right="0" top="0" bottom="0" />
|
|
5
|
+
<Label id="titleLabel" text="{title}" left="40" right="40" top="24" height="64" fontSize="44" fontFamily="sans-serif" fontWeight="700" fill="#25322d" alignX="center" alignY="center" eventMode="none" />
|
|
6
|
+
<Label id="messageLabel" text="{message}" left="40" right="40" top="112" height="136" fontSize="36" fontFamily="sans-serif" fill="#52635b" alignX="center" alignY="center" wordWrap="true" eventMode="none" />
|
|
7
|
+
<Button id="confirm" scene="./Button.scene" left="40" bottom="32" width="320" height="80" size="medium" text="{confirmText}" @clicked="confirm" />
|
|
8
|
+
<Button id="cancel" scene="./Button.scene" right="40" bottom="32" width="320" height="80" size="medium" text="{cancelText}" @clicked="cancel" />
|
|
9
|
+
</Group>
|
|
10
|
+
</Scene>
|