create-gametau 0.1.0-alpha.2
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/dist/cli.d.ts +17 -0
- package/dist/cli.js +151 -0
- package/dist/cli.js.map +1 -0
- package/package.json +23 -0
- package/templates/base/index.html +32 -0
- package/templates/base/package.json +23 -0
- package/templates/base/src/game/loop.ts +28 -0
- package/templates/base/src/index.ts +55 -0
- package/templates/base/src/services/backend.ts +18 -0
- package/templates/base/src-tauri/Cargo.toml +7 -0
- package/templates/base/src-tauri/app/Cargo.toml +19 -0
- package/templates/base/src-tauri/app/build.rs +3 -0
- package/templates/base/src-tauri/app/src/lib.rs +14 -0
- package/templates/base/src-tauri/app/src/main.rs +6 -0
- package/templates/base/src-tauri/app/tauri.conf.json +28 -0
- package/templates/base/src-tauri/commands/Cargo.toml +16 -0
- package/templates/base/src-tauri/commands/src/commands.rs +20 -0
- package/templates/base/src-tauri/commands/src/lib.rs +7 -0
- package/templates/base/src-tauri/core/Cargo.toml +8 -0
- package/templates/base/src-tauri/core/src/lib.rs +45 -0
- package/templates/base/src-tauri/wasm/Cargo.toml +21 -0
- package/templates/base/src-tauri/wasm/src/lib.rs +2 -0
- package/templates/base/tsconfig.json +13 -0
- package/templates/base/vite.config.ts +22 -0
- package/templates/pixi/package.json +24 -0
- package/templates/pixi/src/game/scene.ts +46 -0
- package/templates/three/package.json +25 -0
- package/templates/three/src/game/scene.ts +54 -0
- package/templates/vanilla/src/game/scene.ts +43 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-gametau — Scaffold a Tauri game with web + desktop deployment.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx create-gametau my-game
|
|
7
|
+
* bunx create-gametau my-game --template pixi
|
|
8
|
+
* bunx create-gametau my-game --template vanilla
|
|
9
|
+
*/
|
|
10
|
+
declare const TEMPLATES: readonly ["three", "pixi", "vanilla"];
|
|
11
|
+
type Template = (typeof TEMPLATES)[number];
|
|
12
|
+
interface Options {
|
|
13
|
+
projectName: string;
|
|
14
|
+
template: Template;
|
|
15
|
+
}
|
|
16
|
+
export declare function scaffold(options: Options, cwd?: string): void;
|
|
17
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-gametau — Scaffold a Tauri game with web + desktop deployment.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx create-gametau my-game
|
|
7
|
+
* bunx create-gametau my-game --template pixi
|
|
8
|
+
* bunx create-gametau my-game --template vanilla
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, cpSync } from "fs";
|
|
11
|
+
import { join, resolve, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
const TEMPLATES = ["three", "pixi", "vanilla"];
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const positional = [];
|
|
16
|
+
let template = "three";
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg === "--template" || arg === "-t") {
|
|
20
|
+
const next = args[++i];
|
|
21
|
+
if (!next || !TEMPLATES.includes(next)) {
|
|
22
|
+
console.error(`Invalid template: ${next}`);
|
|
23
|
+
console.error(`Available: ${TEMPLATES.join(", ")}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
template = next;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === "--help" || arg === "-h") {
|
|
29
|
+
printHelp();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
else if (!arg.startsWith("-")) {
|
|
33
|
+
positional.push(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (positional.length === 0) {
|
|
37
|
+
console.error("Error: project name required.\n");
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return { projectName: positional[0], template };
|
|
42
|
+
}
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`
|
|
45
|
+
create-gametau — Scaffold a Tauri game with web + desktop deployment
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
bunx create-gametau <project-name> [options]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--template, -t Template to use: three (default), pixi, vanilla
|
|
52
|
+
--help, -h Show this help message
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
bunx create-gametau my-game
|
|
56
|
+
bunx create-gametau my-game --template pixi
|
|
57
|
+
bun create gametau my-game
|
|
58
|
+
`.trim());
|
|
59
|
+
}
|
|
60
|
+
function getTemplatesDir() {
|
|
61
|
+
// Works both when running from source (dev) and from dist (published)
|
|
62
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
63
|
+
const packageRoot = resolve(dirname(thisFile), "..");
|
|
64
|
+
return join(packageRoot, "templates");
|
|
65
|
+
}
|
|
66
|
+
export function scaffold(options, cwd) {
|
|
67
|
+
const { projectName, template } = options;
|
|
68
|
+
const targetDir = resolve(cwd || process.cwd(), projectName);
|
|
69
|
+
if (existsSync(targetDir)) {
|
|
70
|
+
const contents = readdirSync(targetDir);
|
|
71
|
+
if (contents.length > 0) {
|
|
72
|
+
throw new Error(`directory "${projectName}" already exists and is not empty.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const templatesDir = getTemplatesDir();
|
|
76
|
+
const baseDir = join(templatesDir, "base");
|
|
77
|
+
const overlayDir = join(templatesDir, template);
|
|
78
|
+
if (!existsSync(baseDir)) {
|
|
79
|
+
throw new Error(`base template not found at ${baseDir}`);
|
|
80
|
+
}
|
|
81
|
+
// Copy base template
|
|
82
|
+
mkdirSync(targetDir, { recursive: true });
|
|
83
|
+
cpSync(baseDir, targetDir, { recursive: true });
|
|
84
|
+
// Copy template overlay (overwrites base files where applicable)
|
|
85
|
+
if (existsSync(overlayDir)) {
|
|
86
|
+
cpSync(overlayDir, targetDir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
// Replace {{PROJECT_NAME}} placeholders
|
|
89
|
+
replaceInDir(targetDir, "{{PROJECT_NAME}}", projectName);
|
|
90
|
+
}
|
|
91
|
+
function replaceInDir(dir, search, replace) {
|
|
92
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const fullPath = join(dir, entry.name);
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
replaceInDir(fullPath, search, replace);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
try {
|
|
100
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
101
|
+
if (content.includes(search)) {
|
|
102
|
+
writeFileSync(fullPath, content.replaceAll(search, replace));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Skip binary files
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isDirectExecution() {
|
|
112
|
+
// Bun exposes import.meta.main directly.
|
|
113
|
+
if (typeof Bun !== "undefined") {
|
|
114
|
+
return import.meta.main;
|
|
115
|
+
}
|
|
116
|
+
// Node 20 does not expose import.meta.main; compare current module to argv[1].
|
|
117
|
+
const entry = process.argv[1];
|
|
118
|
+
if (!entry)
|
|
119
|
+
return false;
|
|
120
|
+
try {
|
|
121
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
122
|
+
return resolve(entry) === currentFile;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Main — only runs when executed directly (not when imported by tests)
|
|
129
|
+
if (isDirectExecution()) {
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const options = parseArgs(args);
|
|
132
|
+
try {
|
|
133
|
+
console.log(`Creating ${options.projectName} with ${options.template} template...`);
|
|
134
|
+
scaffold(options);
|
|
135
|
+
console.log(`
|
|
136
|
+
Done! Your game is ready.
|
|
137
|
+
|
|
138
|
+
cd ${options.projectName}
|
|
139
|
+
bun install
|
|
140
|
+
bun run dev # web dev server
|
|
141
|
+
bun run dev:tauri # desktop dev (requires Tauri CLI)
|
|
142
|
+
bun run build:web # build for web deployment
|
|
143
|
+
bun run build:desktop # build desktop app
|
|
144
|
+
`);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-gametau",
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "Scaffold a Tauri game that deploys to web + desktop",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-gametau": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "^1.2.0",
|
|
21
|
+
"typescript": "^5.8.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{PROJECT_NAME}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
|
|
10
|
+
canvas { display: block; }
|
|
11
|
+
#app { width: 100%; height: 100%; }
|
|
12
|
+
#hud {
|
|
13
|
+
position: fixed;
|
|
14
|
+
top: 16px;
|
|
15
|
+
left: 16px;
|
|
16
|
+
color: #fff;
|
|
17
|
+
font-family: monospace;
|
|
18
|
+
font-size: 14px;
|
|
19
|
+
z-index: 10;
|
|
20
|
+
pointer-events: none;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div id="hud">
|
|
26
|
+
<div>Score: <span id="score">0</span></div>
|
|
27
|
+
<div>Tick: <span id="tick">0</span></div>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="app"></div>
|
|
30
|
+
<script type="module" src="/src/index.ts"></script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:tauri": "tauri dev",
|
|
9
|
+
"build:web": "vite build",
|
|
10
|
+
"build:desktop": "tauri build",
|
|
11
|
+
"preview": "vite preview"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"webtau": "^0.1.0-alpha.2"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.8.0",
|
|
18
|
+
"vite": "^6.0.0",
|
|
19
|
+
"webtau-vite": "^0.1.0-alpha.2",
|
|
20
|
+
"@tauri-apps/cli": "^2.0.0",
|
|
21
|
+
"@tauri-apps/api": "^2.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type TickFn = (dt: number) => void;
|
|
2
|
+
type RenderFn = () => void;
|
|
3
|
+
|
|
4
|
+
let running = false;
|
|
5
|
+
let lastTime = 0;
|
|
6
|
+
|
|
7
|
+
export function startGameLoop(tick: TickFn, render: RenderFn): void {
|
|
8
|
+
running = true;
|
|
9
|
+
lastTime = performance.now();
|
|
10
|
+
|
|
11
|
+
function frame(now: number) {
|
|
12
|
+
if (!running) return;
|
|
13
|
+
|
|
14
|
+
const dt = (now - lastTime) / 1000; // seconds
|
|
15
|
+
lastTime = now;
|
|
16
|
+
|
|
17
|
+
tick(dt);
|
|
18
|
+
render();
|
|
19
|
+
|
|
20
|
+
requestAnimationFrame(frame);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
requestAnimationFrame(frame);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stopGameLoop(): void {
|
|
27
|
+
running = false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { configure, isTauri } from "webtau";
|
|
2
|
+
import { getWorldView, tickWorld } from "./services/backend";
|
|
3
|
+
import { startGameLoop } from "./game/loop";
|
|
4
|
+
import { initScene, updateScene } from "./game/scene";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
// Configure webtau for web mode (no-op in Tauri)
|
|
8
|
+
if (!isTauri()) {
|
|
9
|
+
configure({
|
|
10
|
+
loadWasm: async () => {
|
|
11
|
+
const wasm = await import("./wasm/{{PROJECT_NAME}}_wasm");
|
|
12
|
+
await wasm.default(); // Initialize WASM
|
|
13
|
+
wasm.init(42); // Initialize game state
|
|
14
|
+
return wasm;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Set up the renderer
|
|
20
|
+
const app = document.getElementById("app")!;
|
|
21
|
+
await initScene(app);
|
|
22
|
+
|
|
23
|
+
// Get initial state
|
|
24
|
+
const view = await getWorldView();
|
|
25
|
+
document.getElementById("score")!.textContent = String(view.score);
|
|
26
|
+
document.getElementById("tick")!.textContent = String(view.tick_count);
|
|
27
|
+
|
|
28
|
+
// Start game loop
|
|
29
|
+
let tickAccumulator = 0;
|
|
30
|
+
let tickInFlight = false;
|
|
31
|
+
const TICK_RATE = 1 / 10; // 10 ticks per second
|
|
32
|
+
|
|
33
|
+
startGameLoop(
|
|
34
|
+
(dt) => {
|
|
35
|
+
tickAccumulator += dt;
|
|
36
|
+
if (!tickInFlight && tickAccumulator >= TICK_RATE) {
|
|
37
|
+
tickAccumulator -= TICK_RATE;
|
|
38
|
+
tickInFlight = true;
|
|
39
|
+
tickWorld()
|
|
40
|
+
.then(() => getWorldView())
|
|
41
|
+
.then((view) => {
|
|
42
|
+
document.getElementById("score")!.textContent = String(view.score);
|
|
43
|
+
document.getElementById("tick")!.textContent = String(view.tick_count);
|
|
44
|
+
})
|
|
45
|
+
.catch(console.error)
|
|
46
|
+
.finally(() => { tickInFlight = false; });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
() => {
|
|
50
|
+
updateScene();
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { invoke } from "webtau";
|
|
2
|
+
|
|
3
|
+
export interface WorldView {
|
|
4
|
+
score: number;
|
|
5
|
+
tick_count: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TickResult {
|
|
9
|
+
score_delta: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getWorldView(): Promise<WorldView> {
|
|
13
|
+
return invoke<WorldView>("get_world_view");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function tickWorld(): Promise<TickResult> {
|
|
17
|
+
return invoke<TickResult>("tick_world");
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "{{PROJECT_NAME}}-app"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "app_lib"
|
|
8
|
+
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
9
|
+
|
|
10
|
+
[build-dependencies]
|
|
11
|
+
tauri-build = { version = "2", features = [] }
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
tauri = { version = "2", features = [] }
|
|
15
|
+
tauri-plugin-opener = "2"
|
|
16
|
+
serde = { version = "1", features = ["derive"] }
|
|
17
|
+
serde_json = "1"
|
|
18
|
+
{{PROJECT_NAME}}-core = { path = "../core" }
|
|
19
|
+
{{PROJECT_NAME}}-commands = { path = "../commands" }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
use std::sync::Mutex;
|
|
2
|
+
|
|
3
|
+
use {{PROJECT_NAME}}_core::GameWorld;
|
|
4
|
+
use {{PROJECT_NAME}}_commands::{get_world_view, tick_world};
|
|
5
|
+
|
|
6
|
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
7
|
+
pub fn run() {
|
|
8
|
+
tauri::Builder::default()
|
|
9
|
+
.plugin(tauri_plugin_opener::init())
|
|
10
|
+
.manage(Mutex::new(GameWorld::new(42)))
|
|
11
|
+
.invoke_handler(tauri::generate_handler![get_world_view, tick_world])
|
|
12
|
+
.run(tauri::generate_context!())
|
|
13
|
+
.expect("error while running tauri application");
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/nicholasgasior/tauri/tauri-v2/crates/tauri-config-schema/schema.json",
|
|
3
|
+
"productName": "{{PROJECT_NAME}}",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"identifier": "com.gametau.{{PROJECT_NAME}}",
|
|
6
|
+
"build": {
|
|
7
|
+
"frontendDist": "../../../dist",
|
|
8
|
+
"devUrl": "http://localhost:1420",
|
|
9
|
+
"beforeDevCommand": "bun run dev",
|
|
10
|
+
"beforeBuildCommand": "bun run build:web"
|
|
11
|
+
},
|
|
12
|
+
"app": {
|
|
13
|
+
"windows": [
|
|
14
|
+
{
|
|
15
|
+
"title": "{{PROJECT_NAME}}",
|
|
16
|
+
"width": 1280,
|
|
17
|
+
"height": 720,
|
|
18
|
+
"resizable": true,
|
|
19
|
+
"fullscreen": false
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"bundle": {
|
|
24
|
+
"active": true,
|
|
25
|
+
"targets": "all",
|
|
26
|
+
"icon": []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "{{PROJECT_NAME}}-commands"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
{{PROJECT_NAME}}-core = { path = "../core" }
|
|
8
|
+
webtau = "0.1.0-alpha.2"
|
|
9
|
+
|
|
10
|
+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
11
|
+
tauri = { version = "2", features = [] }
|
|
12
|
+
|
|
13
|
+
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
|
14
|
+
wasm-bindgen = "0.2"
|
|
15
|
+
serde = { version = "1", features = ["derive"] }
|
|
16
|
+
serde-wasm-bindgen = "0.6"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
use {{PROJECT_NAME}}_core::{GameWorld, WorldView, TickResult};
|
|
2
|
+
|
|
3
|
+
#[cfg(target_arch = "wasm32")]
|
|
4
|
+
webtau::wasm_state!(GameWorld);
|
|
5
|
+
|
|
6
|
+
#[cfg(target_arch = "wasm32")]
|
|
7
|
+
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
8
|
+
pub fn init(seed: u32) {
|
|
9
|
+
set_state(GameWorld::new(seed as u64));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[webtau::command]
|
|
13
|
+
pub fn get_world_view(state: &GameWorld) -> WorldView {
|
|
14
|
+
state.view()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[webtau::command]
|
|
18
|
+
pub fn tick_world(state: &mut GameWorld) -> TickResult {
|
|
19
|
+
state.tick()
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use rand::rngs::StdRng;
|
|
2
|
+
use rand::SeedableRng;
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
#[derive(Serialize, Clone)]
|
|
6
|
+
pub struct WorldView {
|
|
7
|
+
pub score: i32,
|
|
8
|
+
pub tick_count: u32,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[derive(Serialize, Clone)]
|
|
12
|
+
pub struct TickResult {
|
|
13
|
+
pub score_delta: i32,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub struct GameWorld {
|
|
17
|
+
score: i32,
|
|
18
|
+
tick_count: u32,
|
|
19
|
+
rng: StdRng,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl GameWorld {
|
|
23
|
+
pub fn new(seed: u64) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
score: 0,
|
|
26
|
+
tick_count: 0,
|
|
27
|
+
rng: StdRng::seed_from_u64(seed),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn view(&self) -> WorldView {
|
|
32
|
+
WorldView {
|
|
33
|
+
score: self.score,
|
|
34
|
+
tick_count: self.tick_count,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn tick(&mut self) -> TickResult {
|
|
39
|
+
use rand::Rng;
|
|
40
|
+
let delta = self.rng.gen_range(-1..=3);
|
|
41
|
+
self.score += delta;
|
|
42
|
+
self.tick_count += 1;
|
|
43
|
+
TickResult { score_delta: delta }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "{{PROJECT_NAME}}-wasm"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
crate-type = ["cdylib"]
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
wasm-bindgen = "0.2"
|
|
11
|
+
serde = { version = "1", features = ["derive"] }
|
|
12
|
+
serde-wasm-bindgen = "0.6"
|
|
13
|
+
webtau = "0.1.0-alpha.2"
|
|
14
|
+
{{PROJECT_NAME}}-core = { path = "../core" }
|
|
15
|
+
{{PROJECT_NAME}}-commands = { path = "../commands" }
|
|
16
|
+
|
|
17
|
+
[profile.release]
|
|
18
|
+
lto = true
|
|
19
|
+
opt-level = "z"
|
|
20
|
+
codegen-units = 1
|
|
21
|
+
strip = true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"isolatedModules": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import webtauVite from "webtau-vite";
|
|
3
|
+
|
|
4
|
+
const host = process.env.TAURI_DEV_HOST;
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
// webtauVite() auto-detects the standard layout (src-tauri/wasm,
|
|
8
|
+
// src-tauri/core, etc.) — no config needed for scaffolded projects.
|
|
9
|
+
plugins: [webtauVite()],
|
|
10
|
+
clearScreen: false,
|
|
11
|
+
server: {
|
|
12
|
+
port: 1420,
|
|
13
|
+
strictPort: true,
|
|
14
|
+
host: host || false,
|
|
15
|
+
hmr: host
|
|
16
|
+
? { protocol: "ws", host, port: 1421 }
|
|
17
|
+
: { protocol: "ws", host: "localhost", port: 1421 },
|
|
18
|
+
watch: {
|
|
19
|
+
ignored: ["**/src-tauri/**"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:tauri": "tauri dev",
|
|
9
|
+
"build:web": "vite build",
|
|
10
|
+
"build:desktop": "tauri build",
|
|
11
|
+
"preview": "vite preview"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"pixi.js": "^8.0.0",
|
|
15
|
+
"webtau": "^0.1.0-alpha.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.8.0",
|
|
19
|
+
"vite": "^6.0.0",
|
|
20
|
+
"webtau-vite": "^0.1.0-alpha.2",
|
|
21
|
+
"@tauri-apps/cli": "^2.0.0",
|
|
22
|
+
"@tauri-apps/api": "^2.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Application, Graphics, Text, TextStyle } from "pixi.js";
|
|
2
|
+
|
|
3
|
+
let app: Application;
|
|
4
|
+
let rect: Graphics;
|
|
5
|
+
let angle = 0;
|
|
6
|
+
|
|
7
|
+
export async function initScene(container: HTMLElement): Promise<void> {
|
|
8
|
+
app = new Application();
|
|
9
|
+
await app.init({
|
|
10
|
+
resizeTo: window,
|
|
11
|
+
background: 0x1a1a2e,
|
|
12
|
+
antialias: true,
|
|
13
|
+
});
|
|
14
|
+
container.appendChild(app.canvas);
|
|
15
|
+
|
|
16
|
+
// Spinning rectangle
|
|
17
|
+
rect = new Graphics();
|
|
18
|
+
drawRect();
|
|
19
|
+
rect.x = window.innerWidth / 2;
|
|
20
|
+
rect.y = window.innerHeight / 2;
|
|
21
|
+
rect.pivot.set(50, 50);
|
|
22
|
+
app.stage.addChild(rect);
|
|
23
|
+
|
|
24
|
+
// Title text
|
|
25
|
+
const style = new TextStyle({
|
|
26
|
+
fontFamily: "monospace",
|
|
27
|
+
fontSize: 16,
|
|
28
|
+
fill: 0x00d4aa,
|
|
29
|
+
});
|
|
30
|
+
const text = new Text({ text: "gametau + PixiJS", style });
|
|
31
|
+
text.x = window.innerWidth / 2;
|
|
32
|
+
text.y = window.innerHeight - 40;
|
|
33
|
+
text.anchor.set(0.5);
|
|
34
|
+
app.stage.addChild(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function drawRect(): void {
|
|
38
|
+
rect.clear();
|
|
39
|
+
rect.rect(0, 0, 100, 100);
|
|
40
|
+
rect.fill(0x00d4aa);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function updateScene(): void {
|
|
44
|
+
angle += 0.02;
|
|
45
|
+
rect.rotation = angle;
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:tauri": "tauri dev",
|
|
9
|
+
"build:web": "vite build",
|
|
10
|
+
"build:desktop": "tauri build",
|
|
11
|
+
"preview": "vite preview"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"three": "^0.172.0",
|
|
15
|
+
"webtau": "^0.1.0-alpha.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/three": "^0.172.0",
|
|
19
|
+
"typescript": "^5.8.0",
|
|
20
|
+
"vite": "^6.0.0",
|
|
21
|
+
"webtau-vite": "^0.1.0-alpha.2",
|
|
22
|
+
"@tauri-apps/cli": "^2.0.0",
|
|
23
|
+
"@tauri-apps/api": "^2.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
let renderer: THREE.WebGLRenderer;
|
|
4
|
+
let scene: THREE.Scene;
|
|
5
|
+
let camera: THREE.PerspectiveCamera;
|
|
6
|
+
let cube: THREE.Mesh;
|
|
7
|
+
|
|
8
|
+
export async function initScene(container: HTMLElement): Promise<void> {
|
|
9
|
+
// Renderer
|
|
10
|
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
11
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
12
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
13
|
+
container.appendChild(renderer.domElement);
|
|
14
|
+
|
|
15
|
+
// Scene
|
|
16
|
+
scene = new THREE.Scene();
|
|
17
|
+
scene.background = new THREE.Color(0x1a1a2e);
|
|
18
|
+
|
|
19
|
+
// Camera
|
|
20
|
+
camera = new THREE.PerspectiveCamera(
|
|
21
|
+
75,
|
|
22
|
+
window.innerWidth / window.innerHeight,
|
|
23
|
+
0.1,
|
|
24
|
+
1000,
|
|
25
|
+
);
|
|
26
|
+
camera.position.z = 3;
|
|
27
|
+
|
|
28
|
+
// Lighting
|
|
29
|
+
const ambient = new THREE.AmbientLight(0x404040, 2);
|
|
30
|
+
scene.add(ambient);
|
|
31
|
+
|
|
32
|
+
const directional = new THREE.DirectionalLight(0xffffff, 1);
|
|
33
|
+
directional.position.set(5, 5, 5);
|
|
34
|
+
scene.add(directional);
|
|
35
|
+
|
|
36
|
+
// Spinning cube
|
|
37
|
+
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
38
|
+
const material = new THREE.MeshStandardMaterial({ color: 0x00d4aa });
|
|
39
|
+
cube = new THREE.Mesh(geometry, material);
|
|
40
|
+
scene.add(cube);
|
|
41
|
+
|
|
42
|
+
// Handle resize
|
|
43
|
+
window.addEventListener("resize", () => {
|
|
44
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
45
|
+
camera.updateProjectionMatrix();
|
|
46
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function updateScene(): void {
|
|
51
|
+
cube.rotation.x += 0.01;
|
|
52
|
+
cube.rotation.y += 0.01;
|
|
53
|
+
renderer.render(scene, camera);
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
let canvas: HTMLCanvasElement;
|
|
2
|
+
let ctx: CanvasRenderingContext2D;
|
|
3
|
+
let angle = 0;
|
|
4
|
+
|
|
5
|
+
export async function initScene(container: HTMLElement): Promise<void> {
|
|
6
|
+
canvas = document.createElement("canvas");
|
|
7
|
+
canvas.width = window.innerWidth;
|
|
8
|
+
canvas.height = window.innerHeight;
|
|
9
|
+
container.appendChild(canvas);
|
|
10
|
+
|
|
11
|
+
ctx = canvas.getContext("2d")!;
|
|
12
|
+
|
|
13
|
+
window.addEventListener("resize", () => {
|
|
14
|
+
canvas.width = window.innerWidth;
|
|
15
|
+
canvas.height = window.innerHeight;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function updateScene(): void {
|
|
20
|
+
const w = canvas.width;
|
|
21
|
+
const h = canvas.height;
|
|
22
|
+
|
|
23
|
+
// Clear
|
|
24
|
+
ctx.fillStyle = "#1a1a2e";
|
|
25
|
+
ctx.fillRect(0, 0, w, h);
|
|
26
|
+
|
|
27
|
+
// Spinning square
|
|
28
|
+
const size = 80;
|
|
29
|
+
ctx.save();
|
|
30
|
+
ctx.translate(w / 2, h / 2);
|
|
31
|
+
ctx.rotate(angle);
|
|
32
|
+
ctx.fillStyle = "#00d4aa";
|
|
33
|
+
ctx.fillRect(-size / 2, -size / 2, size, size);
|
|
34
|
+
ctx.restore();
|
|
35
|
+
|
|
36
|
+
angle += 0.02;
|
|
37
|
+
|
|
38
|
+
// Label
|
|
39
|
+
ctx.fillStyle = "#00d4aa";
|
|
40
|
+
ctx.font = "16px monospace";
|
|
41
|
+
ctx.textAlign = "center";
|
|
42
|
+
ctx.fillText("gametau + Canvas2D", w / 2, h - 24);
|
|
43
|
+
}
|