create-gametau 0.2.1 → 0.3.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 +13 -0
- package/package.json +1 -1
- package/templates/base/index.html +4 -0
- package/templates/base/package.json +2 -2
- package/templates/base/src/index.ts +60 -11
- package/templates/base/src/services/comms.ts +32 -0
- package/templates/base/src/services/contracts.ts +48 -0
- package/templates/base/src/services/index.ts +28 -0
- package/templates/base/src/services/session.ts +59 -0
- package/templates/base/src/services/settings.ts +69 -0
- package/templates/base/src-tauri/commands/Cargo.toml +1 -1
- package/templates/base/src-tauri/wasm/Cargo.toml +1 -1
- package/templates/pixi/package.json +2 -2
- package/templates/three/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,6 +38,19 @@ bunx create-gametau my-game --template vanilla
|
|
|
38
38
|
- `src-tauri/app` - Tauri desktop shell
|
|
39
39
|
- `src-tauri/wasm` - WASM entry crate
|
|
40
40
|
- `src` - frontend app wired to `webtau` and `webtau-vite`
|
|
41
|
+
- `src/services` - production-ready service seams for backend invoke calls, persistence/settings, mission session snapshots, and event-driven comms
|
|
42
|
+
|
|
43
|
+
## Service Layer Contract
|
|
44
|
+
|
|
45
|
+
The scaffolded base template now ships a small service architecture instead of a single command wrapper:
|
|
46
|
+
|
|
47
|
+
- `src/services/backend.ts` - typed `invoke()` wrappers for gameplay commands
|
|
48
|
+
- `src/services/settings.ts` - runtime settings persistence via `webtau/path` + `webtau/fs`
|
|
49
|
+
- `src/services/session.ts` - mission/session snapshot persistence via `webtau/path` + `webtau/fs`
|
|
50
|
+
- `src/services/comms.ts` - typed alert/comms channel over `webtau/event`
|
|
51
|
+
- `src/services/contracts.ts` - interfaces/types for settings, session snapshots, and alerts
|
|
52
|
+
|
|
53
|
+
This keeps the generated project lightweight while giving contributors clear extension points for production features.
|
|
41
54
|
|
|
42
55
|
For full docs and package details, see the main repository:
|
|
43
56
|
<https://github.com/devallibus/gametau>
|
package/package.json
CHANGED
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
<div id="hud">
|
|
26
26
|
<div>Score: <span id="score">0</span></div>
|
|
27
27
|
<div>Tick: <span id="tick">0</span></div>
|
|
28
|
+
<div>Tick Rate: <span id="tick-rate">0</span> Hz</div>
|
|
29
|
+
<div>Autosave: every <span id="autosave">0</span> ticks</div>
|
|
30
|
+
<div>Session: <span id="session">fresh</span></div>
|
|
31
|
+
<div>Alert: <span id="alert">ready</span></div>
|
|
28
32
|
</div>
|
|
29
33
|
<div id="app"></div>
|
|
30
34
|
<script type="module" src="/src/index.ts"></script>
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"preview": "vite preview"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"webtau": "^0.
|
|
14
|
+
"webtau": "^0.3.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"typescript": "^5.8.0",
|
|
18
18
|
"vite": "^6.0.0",
|
|
19
|
-
"webtau-vite": "^0.
|
|
19
|
+
"webtau-vite": "^0.3.0",
|
|
20
20
|
"@tauri-apps/cli": "^2.0.0",
|
|
21
21
|
"@tauri-apps/api": "^2.0.0"
|
|
22
22
|
}
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { configure, isTauri } from "webtau";
|
|
2
|
-
import { getWorldView, tickWorld } from "./services/backend";
|
|
3
2
|
import { startGameLoop } from "./game/loop";
|
|
4
3
|
import { initScene, updateScene } from "./game/scene";
|
|
4
|
+
import {
|
|
5
|
+
createServiceLayer,
|
|
6
|
+
getWorldView,
|
|
7
|
+
tickWorld,
|
|
8
|
+
type AlertLevel,
|
|
9
|
+
type RuntimeSettings,
|
|
10
|
+
type WorldView,
|
|
11
|
+
} from "./services";
|
|
12
|
+
|
|
13
|
+
function resolveAlertLevel(scoreDelta: number): AlertLevel {
|
|
14
|
+
if (scoreDelta >= 2) return "critical";
|
|
15
|
+
if (scoreDelta > 0) return "info";
|
|
16
|
+
return "warning";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function updateHud(view: WorldView, settings: RuntimeSettings): void {
|
|
20
|
+
document.getElementById("score")!.textContent = String(view.score);
|
|
21
|
+
document.getElementById("tick")!.textContent = String(view.tick_count);
|
|
22
|
+
document.getElementById("tick-rate")!.textContent = String(settings.tickRateHz);
|
|
23
|
+
document.getElementById("autosave")!.textContent = String(settings.autoSaveEveryTicks);
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
async function main() {
|
|
7
27
|
// Configure webtau for web mode (no-op in Tauri)
|
|
@@ -20,27 +40,56 @@ async function main() {
|
|
|
20
40
|
const app = document.getElementById("app")!;
|
|
21
41
|
await initScene(app);
|
|
22
42
|
|
|
23
|
-
//
|
|
43
|
+
// Build service seams up front: backend invoke wrappers + fs/path + event orchestration.
|
|
44
|
+
const services = await createServiceLayer();
|
|
45
|
+
const settings = await services.settings.load();
|
|
46
|
+
const previousSession = await services.session.loadLastSnapshot();
|
|
47
|
+
document.getElementById("session")!.textContent = previousSession
|
|
48
|
+
? `restored (${previousSession.savedAt})`
|
|
49
|
+
: "fresh";
|
|
50
|
+
document.getElementById("alert")!.textContent = "ready";
|
|
51
|
+
|
|
52
|
+
const unlistenAlerts = await services.comms.subscribe((message) => {
|
|
53
|
+
document.getElementById("alert")!.textContent = `[${message.level}] ${message.message}`;
|
|
54
|
+
});
|
|
55
|
+
window.addEventListener("beforeunload", () => {
|
|
56
|
+
unlistenAlerts();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Persist settings on first run so users have an explicit config seam to extend.
|
|
60
|
+
await services.settings.save(settings);
|
|
61
|
+
|
|
62
|
+
// Get and display initial state.
|
|
24
63
|
const view = await getWorldView();
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
updateHud(view, settings);
|
|
65
|
+
await services.session.saveSnapshot(view);
|
|
27
66
|
|
|
28
67
|
// Start game loop
|
|
29
68
|
let tickAccumulator = 0;
|
|
30
69
|
let tickInFlight = false;
|
|
31
|
-
const
|
|
70
|
+
const tickRate = 1 / settings.tickRateHz;
|
|
32
71
|
|
|
33
72
|
startGameLoop(
|
|
34
73
|
(dt) => {
|
|
35
74
|
tickAccumulator += dt;
|
|
36
|
-
if (!tickInFlight && tickAccumulator >=
|
|
37
|
-
tickAccumulator -=
|
|
75
|
+
if (!tickInFlight && tickAccumulator >= tickRate) {
|
|
76
|
+
tickAccumulator -= tickRate;
|
|
38
77
|
tickInFlight = true;
|
|
39
78
|
tickWorld()
|
|
40
|
-
.then(() =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
.then(async (tickResult) => {
|
|
80
|
+
const nextView = await getWorldView();
|
|
81
|
+
updateHud(nextView, settings);
|
|
82
|
+
|
|
83
|
+
if (nextView.tick_count % settings.autoSaveEveryTicks === 0) {
|
|
84
|
+
await services.session.saveSnapshot(nextView);
|
|
85
|
+
document.getElementById("session")!.textContent = `saved at tick ${nextView.tick_count}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await services.comms.publish({
|
|
89
|
+
level: resolveAlertLevel(tickResult.score_delta),
|
|
90
|
+
source: "engine",
|
|
91
|
+
message: `score delta ${tickResult.score_delta}`,
|
|
92
|
+
});
|
|
44
93
|
})
|
|
45
94
|
.catch(console.error)
|
|
46
95
|
.finally(() => { tickInFlight = false; });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { emit, listen } from "webtau/event";
|
|
2
|
+
import type { AlertMessage, CommsService } from "./contracts";
|
|
3
|
+
|
|
4
|
+
const COMMS_NAMESPACE = "gametau:comms";
|
|
5
|
+
|
|
6
|
+
function eventNameFor(channel: string): string {
|
|
7
|
+
return `${COMMS_NAMESPACE}:${channel}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createCommsService(channel: string): CommsService {
|
|
11
|
+
const eventName = eventNameFor(channel);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
channel,
|
|
15
|
+
|
|
16
|
+
async publish(input): Promise<void> {
|
|
17
|
+
const payload: AlertMessage = {
|
|
18
|
+
level: input.level,
|
|
19
|
+
source: input.source,
|
|
20
|
+
message: input.message,
|
|
21
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
await emit(eventName, payload);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async subscribe(handler): Promise<() => void> {
|
|
27
|
+
return listen<AlertMessage>(eventName, ({ payload }) => {
|
|
28
|
+
handler(payload);
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { WorldView } from "./backend";
|
|
2
|
+
|
|
3
|
+
export type AlertLevel = "info" | "warning" | "critical";
|
|
4
|
+
|
|
5
|
+
export interface AlertMessage {
|
|
6
|
+
level: AlertLevel;
|
|
7
|
+
source: string;
|
|
8
|
+
message: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RuntimeSettings {
|
|
13
|
+
tickRateHz: number;
|
|
14
|
+
autoSaveEveryTicks: number;
|
|
15
|
+
commsChannel: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_RUNTIME_SETTINGS: RuntimeSettings = {
|
|
19
|
+
tickRateHz: 10,
|
|
20
|
+
autoSaveEveryTicks: 10,
|
|
21
|
+
commsChannel: "ops",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface SessionSnapshot extends WorldView {
|
|
25
|
+
savedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SettingsService {
|
|
29
|
+
load(): Promise<RuntimeSettings>;
|
|
30
|
+
save(next: Partial<RuntimeSettings>): Promise<RuntimeSettings>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SessionService {
|
|
34
|
+
loadLastSnapshot(): Promise<SessionSnapshot | null>;
|
|
35
|
+
saveSnapshot(view: WorldView): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CommsService {
|
|
39
|
+
readonly channel: string;
|
|
40
|
+
publish(input: Omit<AlertMessage, "timestamp"> & { timestamp?: string }): Promise<void>;
|
|
41
|
+
subscribe(handler: (message: AlertMessage) => void): Promise<() => void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ServiceLayer {
|
|
45
|
+
settings: SettingsService;
|
|
46
|
+
session: SessionService;
|
|
47
|
+
comms: CommsService;
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createCommsService } from "./comms";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
4
|
+
type AlertLevel,
|
|
5
|
+
type AlertMessage,
|
|
6
|
+
type RuntimeSettings,
|
|
7
|
+
type SessionSnapshot,
|
|
8
|
+
type ServiceLayer,
|
|
9
|
+
} from "./contracts";
|
|
10
|
+
import { createSessionService } from "./session";
|
|
11
|
+
import { createSettingsService } from "./settings";
|
|
12
|
+
|
|
13
|
+
export * from "./backend";
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
16
|
+
type RuntimeSettings,
|
|
17
|
+
type AlertLevel,
|
|
18
|
+
type AlertMessage,
|
|
19
|
+
type SessionSnapshot,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function createServiceLayer(): Promise<ServiceLayer> {
|
|
23
|
+
const settings = createSettingsService();
|
|
24
|
+
const resolvedSettings = await settings.load();
|
|
25
|
+
const comms = createCommsService(resolvedSettings.commsChannel);
|
|
26
|
+
const session = createSessionService();
|
|
27
|
+
return { settings, comms, session };
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createDir, exists, readTextFile, writeTextFile } from "webtau/fs";
|
|
2
|
+
import { appDataDir, join } from "webtau/path";
|
|
3
|
+
import type { SessionService, SessionSnapshot } from "./contracts";
|
|
4
|
+
import type { WorldView } from "./backend";
|
|
5
|
+
|
|
6
|
+
const SESSION_FILENAME = "mission-session.json";
|
|
7
|
+
|
|
8
|
+
function toSnapshot(view: WorldView): SessionSnapshot {
|
|
9
|
+
return {
|
|
10
|
+
...view,
|
|
11
|
+
savedAt: new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseSnapshot(raw: unknown): SessionSnapshot | null {
|
|
16
|
+
if (!raw || typeof raw !== "object") return null;
|
|
17
|
+
const candidate = raw as Partial<SessionSnapshot>;
|
|
18
|
+
if (
|
|
19
|
+
typeof candidate.score !== "number" ||
|
|
20
|
+
typeof candidate.tick_count !== "number" ||
|
|
21
|
+
typeof candidate.savedAt !== "string"
|
|
22
|
+
) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
score: candidate.score,
|
|
27
|
+
tick_count: candidate.tick_count,
|
|
28
|
+
savedAt: candidate.savedAt,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getSessionPath(): Promise<string> {
|
|
33
|
+
const dataDir = await appDataDir();
|
|
34
|
+
await createDir(dataDir, { recursive: true });
|
|
35
|
+
return join(dataDir, SESSION_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createSessionService(): SessionService {
|
|
39
|
+
return {
|
|
40
|
+
async loadLastSnapshot(): Promise<SessionSnapshot | null> {
|
|
41
|
+
const sessionPath = await getSessionPath();
|
|
42
|
+
if (!(await exists(sessionPath))) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readTextFile(sessionPath);
|
|
47
|
+
return parseSnapshot(JSON.parse(raw) as unknown);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async saveSnapshot(view: WorldView): Promise<void> {
|
|
54
|
+
const sessionPath = await getSessionPath();
|
|
55
|
+
const snapshot = toSnapshot(view);
|
|
56
|
+
await writeTextFile(sessionPath, JSON.stringify(snapshot, null, 2));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createDir, exists, readTextFile, writeTextFile } from "webtau/fs";
|
|
2
|
+
import { appConfigDir, join } from "webtau/path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
5
|
+
type RuntimeSettings,
|
|
6
|
+
type SettingsService,
|
|
7
|
+
} from "./contracts";
|
|
8
|
+
|
|
9
|
+
const SETTINGS_FILENAME = "runtime-settings.json";
|
|
10
|
+
|
|
11
|
+
function normalizeSettings(raw: unknown): RuntimeSettings {
|
|
12
|
+
if (!raw || typeof raw !== "object") {
|
|
13
|
+
return DEFAULT_RUNTIME_SETTINGS;
|
|
14
|
+
}
|
|
15
|
+
const candidate = raw as Partial<RuntimeSettings>;
|
|
16
|
+
const tickRateHz =
|
|
17
|
+
typeof candidate.tickRateHz === "number" && Number.isFinite(candidate.tickRateHz)
|
|
18
|
+
? Math.max(1, Math.floor(candidate.tickRateHz))
|
|
19
|
+
: DEFAULT_RUNTIME_SETTINGS.tickRateHz;
|
|
20
|
+
const autoSaveEveryTicks =
|
|
21
|
+
typeof candidate.autoSaveEveryTicks === "number" && Number.isFinite(candidate.autoSaveEveryTicks)
|
|
22
|
+
? Math.max(1, Math.floor(candidate.autoSaveEveryTicks))
|
|
23
|
+
: DEFAULT_RUNTIME_SETTINGS.autoSaveEveryTicks;
|
|
24
|
+
const commsChannel =
|
|
25
|
+
typeof candidate.commsChannel === "string" && candidate.commsChannel.trim().length > 0
|
|
26
|
+
? candidate.commsChannel.trim()
|
|
27
|
+
: DEFAULT_RUNTIME_SETTINGS.commsChannel;
|
|
28
|
+
|
|
29
|
+
return { tickRateHz, autoSaveEveryTicks, commsChannel };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getSettingsPath(): Promise<string> {
|
|
33
|
+
const configDir = await appConfigDir();
|
|
34
|
+
await createDir(configDir, { recursive: true });
|
|
35
|
+
return join(configDir, SETTINGS_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createSettingsService(): SettingsService {
|
|
39
|
+
let cache: RuntimeSettings | null = null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
async load(): Promise<RuntimeSettings> {
|
|
43
|
+
if (cache) return cache;
|
|
44
|
+
|
|
45
|
+
const settingsPath = await getSettingsPath();
|
|
46
|
+
if (!(await exists(settingsPath))) {
|
|
47
|
+
cache = DEFAULT_RUNTIME_SETTINGS;
|
|
48
|
+
return cache;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const raw = await readTextFile(settingsPath);
|
|
53
|
+
cache = normalizeSettings(JSON.parse(raw) as unknown);
|
|
54
|
+
} catch {
|
|
55
|
+
cache = DEFAULT_RUNTIME_SETTINGS;
|
|
56
|
+
}
|
|
57
|
+
return cache;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async save(next: Partial<RuntimeSettings>): Promise<RuntimeSettings> {
|
|
61
|
+
const current = await this.load();
|
|
62
|
+
const merged = normalizeSettings({ ...current, ...next });
|
|
63
|
+
const settingsPath = await getSettingsPath();
|
|
64
|
+
await writeTextFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
65
|
+
cache = merged;
|
|
66
|
+
return merged;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -11,7 +11,7 @@ wasm-bindgen = "0.2"
|
|
|
11
11
|
serde = { version = "1", features = ["derive"] }
|
|
12
12
|
serde-wasm-bindgen = "0.6"
|
|
13
13
|
getrandom = { version = "0.2", features = ["js"] }
|
|
14
|
-
webtau = "0.
|
|
14
|
+
webtau = "0.3.0"
|
|
15
15
|
{{PROJECT_NAME}}-core = { path = "../core" }
|
|
16
16
|
{{PROJECT_NAME}}-commands = { path = "../commands" }
|
|
17
17
|
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"pixi.js": "^8.0.0",
|
|
15
|
-
"webtau": "^0.
|
|
15
|
+
"webtau": "^0.3.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"typescript": "^5.8.0",
|
|
19
19
|
"vite": "^6.0.0",
|
|
20
|
-
"webtau-vite": "^0.
|
|
20
|
+
"webtau-vite": "^0.3.0",
|
|
21
21
|
"@tauri-apps/cli": "^2.0.0",
|
|
22
22
|
"@tauri-apps/api": "^2.0.0"
|
|
23
23
|
}
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"three": "^0.172.0",
|
|
15
|
-
"webtau": "^0.
|
|
15
|
+
"webtau": "^0.3.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/three": "^0.172.0",
|
|
19
19
|
"typescript": "^5.8.0",
|
|
20
20
|
"vite": "^6.0.0",
|
|
21
|
-
"webtau-vite": "^0.
|
|
21
|
+
"webtau-vite": "^0.3.0",
|
|
22
22
|
"@tauri-apps/cli": "^2.0.0",
|
|
23
23
|
"@tauri-apps/api": "^2.0.0"
|
|
24
24
|
}
|