samengine-build 1.6.3
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 +5 -0
- package/dist/buildconfig.d.ts +9 -0
- package/dist/buildconfig.js +11 -0
- package/dist/buildhelper.d.ts +6 -0
- package/dist/buildhelper.js +108 -0
- package/dist/cli/argparser.d.ts +8 -0
- package/dist/cli/argparser.js +36 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +160 -0
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.js +32 -0
- package/dist/cli/new.d.ts +1 -0
- package/dist/cli/new.js +198 -0
- package/dist/exporthtml.d.ts +3 -0
- package/dist/exporthtml.js +279 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.js +12 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const flog: (...args: any[]) => void;
|
|
2
|
+
export declare function copyFolder(src: string, dest: string): Promise<void>;
|
|
3
|
+
export declare function scanResourcesAsDataURIs(resourceDir: string): Promise<Record<string, string>>;
|
|
4
|
+
export declare function getContentType(path: string): string;
|
|
5
|
+
export declare function getMimeType(filename: string): string;
|
|
6
|
+
export declare function filterResourcesByUsage(bundledJsContent: string, resourcesMap: Record<string, string>): Record<string, string>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readdirSync, mkdirSync, promises as fsPromises, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
export const flog = (...args) => {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
const time = `[${now.getHours().toString().padStart(2, "0")}:` +
|
|
6
|
+
`${now.getMinutes().toString().padStart(2, "0")}:` +
|
|
7
|
+
`${now.getSeconds().toString().padStart(2, "0")}.` +
|
|
8
|
+
`${now.getMilliseconds().toString().padStart(3, "0")}]`;
|
|
9
|
+
console.log(time, ...args);
|
|
10
|
+
};
|
|
11
|
+
export async function copyFolder(src, dest) {
|
|
12
|
+
// Zielordner erstellen
|
|
13
|
+
mkdirSync(dest, { recursive: true });
|
|
14
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const srcPath = join(src, entry.name);
|
|
17
|
+
const destPath = join(dest, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
await copyFolder(srcPath, destPath);
|
|
20
|
+
}
|
|
21
|
+
else if (entry.isFile()) {
|
|
22
|
+
// Datei mit Node.js schreiben
|
|
23
|
+
const data = await fsPromises.readFile(srcPath);
|
|
24
|
+
await fsPromises.writeFile(destPath, data);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Scan resources folder and convert to data URIs (base64)
|
|
29
|
+
export async function scanResourcesAsDataURIs(resourceDir) {
|
|
30
|
+
const resourceMap = {};
|
|
31
|
+
if (!existsSync(resourceDir)) {
|
|
32
|
+
return resourceMap;
|
|
33
|
+
}
|
|
34
|
+
async function scanDir(dir, basePath = "") {
|
|
35
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = join(dir, entry.name);
|
|
38
|
+
const resourcePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
await scanDir(fullPath, resourcePath);
|
|
41
|
+
}
|
|
42
|
+
else if (entry.isFile()) {
|
|
43
|
+
const fileData = await fsPromises.readFile(fullPath);
|
|
44
|
+
const base64Data = fileData.toString("base64");
|
|
45
|
+
const mimeType = getMimeType(entry.name);
|
|
46
|
+
resourceMap[resourcePath] = `data:${mimeType};base64,${base64Data}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await scanDir(resourceDir);
|
|
51
|
+
return resourceMap;
|
|
52
|
+
}
|
|
53
|
+
// === Helper ===
|
|
54
|
+
export function getContentType(path) {
|
|
55
|
+
if (path.endsWith(".js"))
|
|
56
|
+
return "application/javascript";
|
|
57
|
+
if (path.endsWith(".ts"))
|
|
58
|
+
return "application/typescript";
|
|
59
|
+
if (path.endsWith(".html"))
|
|
60
|
+
return "text/html";
|
|
61
|
+
if (path.endsWith(".css"))
|
|
62
|
+
return "text/css";
|
|
63
|
+
if (path.endsWith(".png"))
|
|
64
|
+
return "image/png";
|
|
65
|
+
return "text/plain";
|
|
66
|
+
}
|
|
67
|
+
export function getMimeType(filename) {
|
|
68
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
69
|
+
const mimeTypes = {
|
|
70
|
+
"png": "image/png",
|
|
71
|
+
"jpg": "image/jpeg",
|
|
72
|
+
"jpeg": "image/jpeg",
|
|
73
|
+
"gif": "image/gif",
|
|
74
|
+
"svg": "image/svg+xml",
|
|
75
|
+
"webp": "image/webp",
|
|
76
|
+
"mp3": "audio/mpeg",
|
|
77
|
+
"wav": "audio/wav",
|
|
78
|
+
"ogg": "audio/ogg",
|
|
79
|
+
"m4a": "audio/mp4",
|
|
80
|
+
"json": "application/json",
|
|
81
|
+
"txt": "text/plain",
|
|
82
|
+
"css": "text/css",
|
|
83
|
+
};
|
|
84
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
85
|
+
}
|
|
86
|
+
// Filter resources by checking if they are actually used in the bundled code
|
|
87
|
+
export function filterResourcesByUsage(bundledJsContent, resourcesMap) {
|
|
88
|
+
const filteredResources = {};
|
|
89
|
+
const unusedResources = [];
|
|
90
|
+
for (const resourcePath in resourcesMap) {
|
|
91
|
+
// Extract just the filename (last part after /)
|
|
92
|
+
const filename = resourcePath.split("/").pop() || resourcePath;
|
|
93
|
+
// Check if the resource is referenced in the bundled code
|
|
94
|
+
// Look for string literals containing the resource path or filename
|
|
95
|
+
if (bundledJsContent.includes(filename) || bundledJsContent.includes(resourcePath)) {
|
|
96
|
+
filteredResources[resourcePath] = resourcesMap[resourcePath];
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
unusedResources.push(resourcePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Log unused resources
|
|
103
|
+
if (unusedResources.length > 0) {
|
|
104
|
+
flog(`⚠️ ${unusedResources.length} unused resource(s) excluded from single-file build:`);
|
|
105
|
+
unusedResources.forEach(res => flog(` - ${res}`));
|
|
106
|
+
}
|
|
107
|
+
return filteredResources;
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Arguments for the CLI Tool
|
|
2
|
+
// Function to parse the Args for the CLI Tools
|
|
3
|
+
export function parseArgs() {
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const options = { release: false, singlefile: false, port: 3000, newProject: null, empty: false };
|
|
6
|
+
for (let i = 0; i < args.length; i++) {
|
|
7
|
+
const arg = args[i];
|
|
8
|
+
switch (arg) {
|
|
9
|
+
case "--single-file":
|
|
10
|
+
options.singlefile = true;
|
|
11
|
+
break;
|
|
12
|
+
case "--release":
|
|
13
|
+
case "-r":
|
|
14
|
+
options.release = true;
|
|
15
|
+
break;
|
|
16
|
+
case "--port":
|
|
17
|
+
case "-p":
|
|
18
|
+
options.port = Number(args[++i]);
|
|
19
|
+
break;
|
|
20
|
+
case "--new":
|
|
21
|
+
case "-n":
|
|
22
|
+
options.newProject = args[++i];
|
|
23
|
+
break;
|
|
24
|
+
case "--new-empty":
|
|
25
|
+
options.empty = true;
|
|
26
|
+
break;
|
|
27
|
+
case "-h":
|
|
28
|
+
case "--help":
|
|
29
|
+
console.log("CLI Tools for samengine\nUsage:\n -r, --release\n -p <port>\n -n <project>\n --new-empty\n --single-file to generate the Export into one file");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
default:
|
|
32
|
+
console.warn(`⚠️ Unknown Argument: ${arg}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return options;
|
|
36
|
+
}
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build Cli Tools for samengine
|
|
3
|
+
import { build as esbuild } from "esbuild";
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
6
|
+
import { watch } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { WebSocketServer } from "ws";
|
|
9
|
+
import { createProject } from "./new.js";
|
|
10
|
+
import { copyFolder, flog, getContentType, scanResourcesAsDataURIs, filterResourcesByUsage } from "../buildhelper.js";
|
|
11
|
+
import { GetDefaultHTML, GetSingleFileHTML } from "../exporthtml.js";
|
|
12
|
+
import { loadUserConfig } from "./config.js";
|
|
13
|
+
import { compressHTML } from "../utils/utils.js";
|
|
14
|
+
import { parseArgs } from "./argparser.js";
|
|
15
|
+
// ================= BUILD =================
|
|
16
|
+
function createBuilder(config, isRelease, isSingleFile = false) {
|
|
17
|
+
// Ensure that the Directories are created
|
|
18
|
+
mkdir("resources", { recursive: true });
|
|
19
|
+
mkdir("game", { recursive: true });
|
|
20
|
+
async function build() {
|
|
21
|
+
try {
|
|
22
|
+
flog("🔄 Building project...");
|
|
23
|
+
if (isRelease)
|
|
24
|
+
await rm("./dist", { recursive: true, force: true });
|
|
25
|
+
await esbuild({
|
|
26
|
+
entryPoints: [`./game/${config.entryname}`],
|
|
27
|
+
outdir: `./${config.outdir}`,
|
|
28
|
+
bundle: true,
|
|
29
|
+
platform: "browser",
|
|
30
|
+
minify: isRelease,
|
|
31
|
+
sourcemap: !isRelease,
|
|
32
|
+
define: { "import.meta.env.DEV": JSON.stringify(!isRelease) },
|
|
33
|
+
});
|
|
34
|
+
if (isSingleFile) {
|
|
35
|
+
// Single-file export
|
|
36
|
+
const bundledJsPath = path.join(".", config.outdir, `${config.entryname.replace(/\.[^.]*$/, "")}.js`);
|
|
37
|
+
const bundledJsContent = await readFile(bundledJsPath, "utf-8");
|
|
38
|
+
// Scan resources and convert to data URIs
|
|
39
|
+
let resourcesMap = await scanResourcesAsDataURIs("./resources");
|
|
40
|
+
// Filter resources by usage in the bundled code
|
|
41
|
+
resourcesMap = filterResourcesByUsage(bundledJsContent, resourcesMap);
|
|
42
|
+
let html = GetSingleFileHTML(config, bundledJsContent, resourcesMap);
|
|
43
|
+
if (isRelease)
|
|
44
|
+
html = await compressHTML(html);
|
|
45
|
+
// Add comment at the beginning after minification
|
|
46
|
+
const htmlComment = `<!-- Game made with samengine v${config.version} - https://github.com/Shadowdara/samengine -->\n`;
|
|
47
|
+
html = htmlComment + html;
|
|
48
|
+
await writeFile("./dist/index.html", html);
|
|
49
|
+
// Delete the JS File
|
|
50
|
+
await rm("./dist/main.js", { recursive: true, force: true });
|
|
51
|
+
flog("✅ Single-file export created!");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Multi-file export (original behavior)
|
|
55
|
+
let html = GetDefaultHTML(config);
|
|
56
|
+
if (isRelease)
|
|
57
|
+
html = await compressHTML(html);
|
|
58
|
+
// Add HTML comment at the beginning after minification
|
|
59
|
+
const htmlComment = `<!-- Game made with samengine v${config.version} - https://www.npmjs.com/@shadowdara/samengine -->\n`;
|
|
60
|
+
html = htmlComment + html;
|
|
61
|
+
await writeFile("./dist/index.html", html);
|
|
62
|
+
// Add JS comment at the beginning of JS files
|
|
63
|
+
const jsComment = `// Game made with samengine v${config.version} - https://www.npmjs.com/@shadowdara/samengine\n`;
|
|
64
|
+
const jsPath = path.join(".", config.outdir, `${config.entryname.replace(/\.[^.]*$/, "")}.js`);
|
|
65
|
+
let jsContent = await readFile(jsPath, "utf-8");
|
|
66
|
+
jsContent = jsComment + jsContent;
|
|
67
|
+
await writeFile(jsPath, jsContent);
|
|
68
|
+
await copyFolder("./resources", "./dist/resources");
|
|
69
|
+
flog("✅ Build finished!");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
flog(`❌ Build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { build };
|
|
77
|
+
}
|
|
78
|
+
// ================= SERVER & RELOAD =================
|
|
79
|
+
function createDevServer(port) {
|
|
80
|
+
const sockets = new Set(); // <-- ws WebSocket, nicht DOM
|
|
81
|
+
const server = createServer(async (req, res) => {
|
|
82
|
+
const url = req.url || "/";
|
|
83
|
+
const filePath = path.join(process.cwd(), "dist", url === "/" ? "index.html" : url);
|
|
84
|
+
try {
|
|
85
|
+
const file = await readFile(filePath);
|
|
86
|
+
res.writeHead(200, { "Content-Type": getContentType(filePath) });
|
|
87
|
+
res.end(file);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
res.writeHead(404);
|
|
91
|
+
res.end("Not Found");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const wss = new WebSocketServer({ server });
|
|
95
|
+
wss.on("connection", (ws) => {
|
|
96
|
+
sockets.add(ws);
|
|
97
|
+
ws.on("close", () => sockets.delete(ws));
|
|
98
|
+
});
|
|
99
|
+
server.listen(port, () => flog(`🚀 Dev Server running on http://localhost:${port}`));
|
|
100
|
+
function reloadClients() {
|
|
101
|
+
flog("🔄 Browser reload...");
|
|
102
|
+
sockets.forEach((ws) => ws.send("reload"));
|
|
103
|
+
}
|
|
104
|
+
return { reloadClients };
|
|
105
|
+
}
|
|
106
|
+
// ================= WATCHER =================
|
|
107
|
+
async function startWatcher(onChange) {
|
|
108
|
+
await mkdir("resources", { recursive: true });
|
|
109
|
+
await mkdir("game", { recursive: true });
|
|
110
|
+
["resources", "game"].forEach((dir) => {
|
|
111
|
+
watch(dir, { recursive: true }, async () => {
|
|
112
|
+
flog(`📁 Change noticed in ${dir}`);
|
|
113
|
+
await onChange();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
flog("👀 Watcher active...");
|
|
117
|
+
}
|
|
118
|
+
// ================= CLI APP =================
|
|
119
|
+
async function main() {
|
|
120
|
+
const args = parseArgs();
|
|
121
|
+
if (args.newProject) {
|
|
122
|
+
await createProject(args.newProject, args.empty);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
const config = await loadUserConfig();
|
|
126
|
+
const builder = createBuilder(config, args.release, args.singlefile);
|
|
127
|
+
let isBuilding = false;
|
|
128
|
+
let pendingRestart = false;
|
|
129
|
+
async function restart() {
|
|
130
|
+
if (isBuilding) {
|
|
131
|
+
pendingRestart = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
isBuilding = true;
|
|
135
|
+
do {
|
|
136
|
+
pendingRestart = false;
|
|
137
|
+
try {
|
|
138
|
+
await builder.build();
|
|
139
|
+
devServer?.reloadClients();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
flog(`❌ Rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
|
+
}
|
|
144
|
+
} while (pendingRestart);
|
|
145
|
+
isBuilding = false;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await builder.build();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
flog(`❌ Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
|
+
}
|
|
153
|
+
let devServer = null;
|
|
154
|
+
if (!args.release) {
|
|
155
|
+
devServer = createDevServer(args.port);
|
|
156
|
+
await startWatcher(restart);
|
|
157
|
+
}
|
|
158
|
+
flog(`Build finished! Mode: ${args.release ? "Release" : "Dev"}`);
|
|
159
|
+
}
|
|
160
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadUserConfig(): Promise<any>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
export async function loadUserConfig() {
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const configPath = path.resolve(root, "samengine.config.ts");
|
|
8
|
+
const outDir = path.resolve(root, ".samengine");
|
|
9
|
+
const outfile = path.join(outDir, "config.mjs");
|
|
10
|
+
try {
|
|
11
|
+
// ensure folder exists
|
|
12
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
13
|
+
// 🔥 bundle TS → JS
|
|
14
|
+
await build({
|
|
15
|
+
entryPoints: [configPath],
|
|
16
|
+
outfile,
|
|
17
|
+
bundle: true,
|
|
18
|
+
platform: "node",
|
|
19
|
+
format: "esm",
|
|
20
|
+
});
|
|
21
|
+
// 🔥 import compiled file
|
|
22
|
+
const mod = await import(pathToFileURL(outfile).href + `?t=${Date.now()}`);
|
|
23
|
+
const config = typeof mod.default === "function"
|
|
24
|
+
? await mod.default()
|
|
25
|
+
: mod.default;
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.error(e);
|
|
30
|
+
throw new Error("❌ Could not load webgameengine.config.ts: " + configPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createProject(name: string, empty: boolean): Promise<void>;
|
package/dist/cli/new.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { flog } from "../buildhelper.js";
|
|
5
|
+
// ================= NEW PROJECT =================
|
|
6
|
+
export async function createProject(name, empty) {
|
|
7
|
+
flog(`📦 Erstelle neues Projekt: ${name}`);
|
|
8
|
+
// Create Dirs
|
|
9
|
+
await mkdir("game", { recursive: true });
|
|
10
|
+
await mkdir("resources", { recursive: true });
|
|
11
|
+
await mkdir("dist", { recursive: true });
|
|
12
|
+
let content = `
|
|
13
|
+
// A empty Project with the Web Framework
|
|
14
|
+
|
|
15
|
+
import { createCanvas, enableFullscreen, setupFullscreenButton } from "samengine";
|
|
16
|
+
import { setupInput, resetInput, getMouse } from "samengine";
|
|
17
|
+
import { startEngine } from "samengine";
|
|
18
|
+
|
|
19
|
+
const { canvas, ctx, applyScaling } = createCanvas({fullscreen: true, scaling: "fit", virtualWidth: window.innerWidth, virtualHeight: window.innerHeight});
|
|
20
|
+
setupInput(canvas);
|
|
21
|
+
|
|
22
|
+
enableFullscreen(canvas);
|
|
23
|
+
setupFullscreenButton(canvas);
|
|
24
|
+
|
|
25
|
+
async function gameStart() {
|
|
26
|
+
// Code which runs at the Game Start
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function gameLoop(dt: number) {
|
|
30
|
+
// Code which runs every Frame
|
|
31
|
+
|
|
32
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
33
|
+
|
|
34
|
+
const mouse = getMouse();
|
|
35
|
+
|
|
36
|
+
applyScaling();
|
|
37
|
+
|
|
38
|
+
resetInput();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Because start Game is Async
|
|
42
|
+
startEngine(() => { gameStart().then(() => {/* ready */}) }, gameLoop);
|
|
43
|
+
`;
|
|
44
|
+
if (empty) {
|
|
45
|
+
content = `
|
|
46
|
+
// A mini Snake Clone with my Webframework
|
|
47
|
+
|
|
48
|
+
import { createCanvas } from "samengine";
|
|
49
|
+
import { setupInput, isKeyJustPressed, resetInput } from "samengine";
|
|
50
|
+
import { startEngine } from "samengine";
|
|
51
|
+
import { renderText } from "samengine";
|
|
52
|
+
import { Vector2d } from "samengine/types";
|
|
53
|
+
import { dlog } from "samengine";
|
|
54
|
+
import { Key } from "samengine";
|
|
55
|
+
|
|
56
|
+
const { canvas, ctx } = createCanvas({fullscreen: true, scaling: "fit", virtualWidth: window.innerWidth, virtualHeight: window.innerHeight});
|
|
57
|
+
setupInput(canvas);
|
|
58
|
+
|
|
59
|
+
let snake: Vector2d[] = [{ x: 10, y: 10 }];
|
|
60
|
+
let dir: Vector2d = { x: 1, y: 0 };
|
|
61
|
+
let food: Vector2d = { x: 15, y: 10 };
|
|
62
|
+
let gridSize = 20;
|
|
63
|
+
let lastMove = 0;
|
|
64
|
+
let speed = 0.2; // seconds per cell
|
|
65
|
+
let start = false;
|
|
66
|
+
|
|
67
|
+
async function gameStart() {
|
|
68
|
+
dlog("Snake gestartet");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function gameLoop(dt: number) {
|
|
72
|
+
if (isKeyJustPressed(Key.Escape)) {
|
|
73
|
+
start = !start
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!start) {
|
|
77
|
+
ctx.fillStyle = "white";
|
|
78
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
79
|
+
|
|
80
|
+
renderText(ctx, "Snake", 10, 10, "black", "24px Arial");
|
|
81
|
+
renderText(ctx, "Press ESC to start or Pause the Game!", 10, 50, "black", "24px Arial");
|
|
82
|
+
|
|
83
|
+
// return;
|
|
84
|
+
} else {
|
|
85
|
+
|
|
86
|
+
// Input
|
|
87
|
+
if (isKeyJustPressed(Key.ArrowUp) && dir.y === 0) dir = { x: 0, y: -1 };
|
|
88
|
+
if (isKeyJustPressed(Key.ArrowDown) && dir.y === 0) dir = { x: 0, y: 1 };
|
|
89
|
+
if (isKeyJustPressed(Key.ArrowLeft) && dir.x === 0) dir = { x: -1, y: 0 };
|
|
90
|
+
if (isKeyJustPressed(Key.ArrowRight) && dir.x === 0) dir = { x: 1, y: 0 };
|
|
91
|
+
|
|
92
|
+
lastMove += dt;
|
|
93
|
+
|
|
94
|
+
if (lastMove >= speed) {
|
|
95
|
+
lastMove = 0;
|
|
96
|
+
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
|
|
97
|
+
|
|
98
|
+
// Kollision mit Walls
|
|
99
|
+
if (head.x < 0 || head.y < 0 || head.x >= canvas.width / gridSize || head.y >= canvas.height / gridSize) {
|
|
100
|
+
snake = [{ x: 10, y: 10 }];
|
|
101
|
+
dir = { x: 1, y: 0 };
|
|
102
|
+
dlog("Game Over");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Kollision mit sich selbst
|
|
107
|
+
if (snake.some(s => s.x === head.x && s.y === head.y)) {
|
|
108
|
+
snake = [{ x: 10, y: 10 }];
|
|
109
|
+
dir = { x: 1, y: 0 };
|
|
110
|
+
dlog(\`Game Over! Highscore: \${snake.length - 1}\`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
snake.unshift(head);
|
|
115
|
+
|
|
116
|
+
// Food Check
|
|
117
|
+
if (head.x === food.x && head.y === food.y) {
|
|
118
|
+
food = { x: Math.floor(Math.random() * (canvas.width / gridSize)), y: Math.floor(Math.random() * (canvas.height / gridSize)) };
|
|
119
|
+
} else {
|
|
120
|
+
snake.pop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Zeichnen
|
|
125
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
126
|
+
|
|
127
|
+
ctx.fillStyle = "black";
|
|
128
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
129
|
+
|
|
130
|
+
ctx.fillStyle = "green";
|
|
131
|
+
snake.forEach(s => ctx.fillRect(s.x * gridSize, s.y * gridSize, gridSize, gridSize));
|
|
132
|
+
|
|
133
|
+
ctx.fillStyle = "red";
|
|
134
|
+
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);
|
|
135
|
+
|
|
136
|
+
renderText(ctx, "Score: " + (snake.length - 1), 10, 10, "yellow", "24px Arial");
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Reset Input
|
|
141
|
+
resetInput();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Because start Game is Async
|
|
145
|
+
startEngine(() => { gameStart().then(() => {/* ready */}) }, gameLoop);
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
// Basic game entry
|
|
149
|
+
await writeFile(path.join("game", "main.ts"), content);
|
|
150
|
+
// Config
|
|
151
|
+
await writeFile(path.join("samengine.config.ts"), `
|
|
152
|
+
// Project File for the Game
|
|
153
|
+
|
|
154
|
+
import type { buildconfig } from "samengine-build";
|
|
155
|
+
import { new_buildconfig } from "samengine-build";
|
|
156
|
+
|
|
157
|
+
export default function defineConfig(): buildconfig {
|
|
158
|
+
let config: buildconfig = new_buildconfig();
|
|
159
|
+
return config;
|
|
160
|
+
}
|
|
161
|
+
`);
|
|
162
|
+
// package.json
|
|
163
|
+
// await writeFile(
|
|
164
|
+
// path.join(base, "package.json"),
|
|
165
|
+
// JSON.stringify(
|
|
166
|
+
// {
|
|
167
|
+
// name,
|
|
168
|
+
// version: "1.0.0",
|
|
169
|
+
// type: "module",
|
|
170
|
+
// scripts: {
|
|
171
|
+
// dev: "mycli",
|
|
172
|
+
// build: "mycli --release",
|
|
173
|
+
// },
|
|
174
|
+
// },
|
|
175
|
+
// null,
|
|
176
|
+
// 2
|
|
177
|
+
// )
|
|
178
|
+
// );
|
|
179
|
+
flog("✅ Projekt created!");
|
|
180
|
+
// flog(`👉 cd ${name}`);
|
|
181
|
+
// flog(`👉 npm install`);
|
|
182
|
+
// flog(`👉 npm run dev`);
|
|
183
|
+
console.log(`Add to your package.json file:
|
|
184
|
+
|
|
185
|
+
"scripts": {
|
|
186
|
+
"dev": "npx samengine-build",
|
|
187
|
+
"build": "npx samengine-build --release"
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
Run "npm run dev" to start the Dev Server and play a little Snake Clone
|
|
191
|
+
|
|
192
|
+
Add ".samengine/config.mjs" to your gitignore file if you are using Git.
|
|
193
|
+
|
|
194
|
+
${chalk.red("Dont ignore the complete Folder!")}
|
|
195
|
+
${chalk.red("Some Tools by samengine are saving important configs in this Folder!")}
|
|
196
|
+
`);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Function to create the Export HTML for the Build
|
|
2
|
+
// Function to create the Export HTML for the Build
|
|
3
|
+
import { version } from "samengine/build";
|
|
4
|
+
export function GetSingleFileHTML(config, bundledJsContent, resourcesMap = {}) {
|
|
5
|
+
let frameworkVersion = version();
|
|
6
|
+
let fullscreenbutton = "";
|
|
7
|
+
let fullscreenBtn = "";
|
|
8
|
+
if (config.show_fullscreen_button) {
|
|
9
|
+
fullscreenbutton = `#fullscreenBtn {
|
|
10
|
+
position: fixed;
|
|
11
|
+
top: 10px;
|
|
12
|
+
right: 10px;
|
|
13
|
+
|
|
14
|
+
padding: 10px 15px;
|
|
15
|
+
font-size: 16px;
|
|
16
|
+
|
|
17
|
+
background: rgba(0, 0, 0, 0.6);
|
|
18
|
+
color: white;
|
|
19
|
+
border: none;
|
|
20
|
+
border-radius: 6px;
|
|
21
|
+
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
z-index: 1000;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#fullscreenBtn:hover {
|
|
27
|
+
background: rgba(0, 0, 0, 0.8);
|
|
28
|
+
}`;
|
|
29
|
+
fullscreenBtn = `<!-- Button to make it fullscreen -->
|
|
30
|
+
<button id="fullscreenBtn">⛶ Fullscreen</button>`;
|
|
31
|
+
}
|
|
32
|
+
// Create a resource loader function that will be embedded in the HTML
|
|
33
|
+
const resourceLoaderScript = `window.__resources = ${JSON.stringify(resourcesMap)};
|
|
34
|
+
window.__getResource = function(path) {
|
|
35
|
+
return window.__resources[path] || null;
|
|
36
|
+
};
|
|
37
|
+
window.__loadResource = function(path) {
|
|
38
|
+
const resource = window.__getResource(path);
|
|
39
|
+
if (!resource) {
|
|
40
|
+
console.warn('Resource not found:', path);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return resource;
|
|
44
|
+
};
|
|
45
|
+
window.__samengine__ = {
|
|
46
|
+
version: "${frameworkVersion}"
|
|
47
|
+
};`;
|
|
48
|
+
// Wrap bundled JS in a function to prevent auto-execution
|
|
49
|
+
const wrappedGameCode = `function __initializeGame() {
|
|
50
|
+
${bundledJsContent.split('\n').map(line => ' ' + line).join('\n')}
|
|
51
|
+
}`;
|
|
52
|
+
const defaulthtml = `<!-- HTML Web Game made with samengine by Shadowdara -->
|
|
53
|
+
<!-- DO NOT REMOVE THIS NOTE ! -->
|
|
54
|
+
<!DOCTYPE html>
|
|
55
|
+
<html>
|
|
56
|
+
<head>
|
|
57
|
+
<meta charset="UTF-8" />
|
|
58
|
+
<title>${config.title}</title>
|
|
59
|
+
<!-- Für Mobile Viewports -->
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
61
|
+
<style>
|
|
62
|
+
* {
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
}
|
|
67
|
+
body {
|
|
68
|
+
overflow: hidden; /* 🔥 verhindert Scrollbars */
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
margin: 0;
|
|
72
|
+
background: #0f172a;
|
|
73
|
+
color: white;
|
|
74
|
+
font-family: sans-serif;
|
|
75
|
+
display: flex;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
align-items: center;
|
|
78
|
+
height: 100vh;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#startscreen {
|
|
82
|
+
text-align: center;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
h1 {
|
|
86
|
+
font-size: 3rem;
|
|
87
|
+
margin-bottom: 0.5rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
h2 {
|
|
91
|
+
font-weight: normal;
|
|
92
|
+
opacity: 0.7;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.startbutton {
|
|
96
|
+
margin-top: 2rem;
|
|
97
|
+
padding: 1rem 2rem;
|
|
98
|
+
font-size: 1.2rem;
|
|
99
|
+
background: #22c55e;
|
|
100
|
+
border: none;
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.startbutton:hover {
|
|
106
|
+
background: #16a34a;
|
|
107
|
+
}
|
|
108
|
+
${fullscreenbutton}
|
|
109
|
+
</style>
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<div id="startscreen">
|
|
114
|
+
<h2>made with samengine</h2>
|
|
115
|
+
<h1>${config.title}</h1>
|
|
116
|
+
<p>${config.version}</p>
|
|
117
|
+
|
|
118
|
+
<button class="startbutton" id="startBtn">Start</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
${resourceLoaderScript}
|
|
123
|
+
</script>
|
|
124
|
+
<script>
|
|
125
|
+
${wrappedGameCode}
|
|
126
|
+
</script>
|
|
127
|
+
<script type="module">
|
|
128
|
+
const btn = document.getElementById("startBtn");
|
|
129
|
+
|
|
130
|
+
btn.addEventListener("click", async () => {
|
|
131
|
+
// 🔊 Audio freischalten
|
|
132
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
133
|
+
|
|
134
|
+
const ctx = new AudioContext();
|
|
135
|
+
await ctx.resume();
|
|
136
|
+
|
|
137
|
+
// 👉 HIER rein!
|
|
138
|
+
window.__audioCtx = ctx;
|
|
139
|
+
|
|
140
|
+
// Startscreen entfernen
|
|
141
|
+
document.getElementById("startscreen").remove();
|
|
142
|
+
|
|
143
|
+
// Initialize the game
|
|
144
|
+
window.__initializeGame();
|
|
145
|
+
});
|
|
146
|
+
</script>
|
|
147
|
+
${fullscreenBtn}
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
150
|
+
`;
|
|
151
|
+
return defaulthtml;
|
|
152
|
+
}
|
|
153
|
+
export function GetDefaultHTML(config) {
|
|
154
|
+
let frameworkVersion = version();
|
|
155
|
+
let fullscreenbutton = "";
|
|
156
|
+
let fullscreenBtn = "";
|
|
157
|
+
if (config.show_fullscreen_button) {
|
|
158
|
+
fullscreenbutton = `#fullscreenBtn {
|
|
159
|
+
position: fixed;
|
|
160
|
+
top: 10px;
|
|
161
|
+
right: 10px;
|
|
162
|
+
|
|
163
|
+
padding: 10px 15px;
|
|
164
|
+
font-size: 16px;
|
|
165
|
+
|
|
166
|
+
background: rgba(0, 0, 0, 0.6);
|
|
167
|
+
color: white;
|
|
168
|
+
border: none;
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
z-index: 1000;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#fullscreenBtn:hover {
|
|
176
|
+
background: rgba(0, 0, 0, 0.8);
|
|
177
|
+
}`;
|
|
178
|
+
fullscreenBtn = `<!-- Button to make it fullscreen -->
|
|
179
|
+
<button id="fullscreenBtn">⛶ Fullscreen</button>`;
|
|
180
|
+
}
|
|
181
|
+
const defaulthtml = `<!-- HTML Web Game made with samengine by Shadowdara -->
|
|
182
|
+
<!-- DO NOT REMOVE THIS NOTE ! -->
|
|
183
|
+
<!DOCTYPE html>
|
|
184
|
+
<html>
|
|
185
|
+
<head>
|
|
186
|
+
<meta charset="UTF-8" />
|
|
187
|
+
<title>${config.title}</title>
|
|
188
|
+
<!-- Für Mobile Viewports -->
|
|
189
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
190
|
+
<style>
|
|
191
|
+
* {
|
|
192
|
+
margin: 0;
|
|
193
|
+
padding: 0;
|
|
194
|
+
box-sizing: border-box;
|
|
195
|
+
}
|
|
196
|
+
body {
|
|
197
|
+
overflow: hidden; /* 🔥 verhindert Scrollbars */
|
|
198
|
+
}
|
|
199
|
+
body {
|
|
200
|
+
margin: 0;
|
|
201
|
+
background: #0f172a;
|
|
202
|
+
color: white;
|
|
203
|
+
font-family: sans-serif;
|
|
204
|
+
display: flex;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
align-items: center;
|
|
207
|
+
height: 100vh;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#startscreen {
|
|
211
|
+
text-align: center;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
h1 {
|
|
215
|
+
font-size: 3rem;
|
|
216
|
+
margin-bottom: 0.5rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
h2 {
|
|
220
|
+
font-weight: normal;
|
|
221
|
+
opacity: 0.7;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.startbutton {
|
|
225
|
+
margin-top: 2rem;
|
|
226
|
+
padding: 1rem 2rem;
|
|
227
|
+
font-size: 1.2rem;
|
|
228
|
+
background: #22c55e;
|
|
229
|
+
border: none;
|
|
230
|
+
border-radius: 8px;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.startbutton:hover {
|
|
235
|
+
background: #16a34a;
|
|
236
|
+
}
|
|
237
|
+
${fullscreenbutton}
|
|
238
|
+
</style>
|
|
239
|
+
</style>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
<div id="startscreen">
|
|
243
|
+
<h2>made with samengine</h2>
|
|
244
|
+
<h1>${config.title}</h1>
|
|
245
|
+
<p>${config.version}</p>
|
|
246
|
+
|
|
247
|
+
<button class="startbutton" id="startBtn">Start</button>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<script type="module">
|
|
251
|
+
const btn = document.getElementById("startBtn");
|
|
252
|
+
|
|
253
|
+
btn.addEventListener("click", async () => {
|
|
254
|
+
// 🔊 Audio freischalten
|
|
255
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
256
|
+
|
|
257
|
+
const ctx = new AudioContext();
|
|
258
|
+
await ctx.resume();
|
|
259
|
+
|
|
260
|
+
// 👉 HIER rein!
|
|
261
|
+
window.__audioCtx = ctx;
|
|
262
|
+
|
|
263
|
+
// Startscreen entfernen
|
|
264
|
+
document.getElementById("startscreen").remove();
|
|
265
|
+
|
|
266
|
+
// Game laden
|
|
267
|
+
import("./${config.entryname}.js");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
window.__samengine__ = {
|
|
271
|
+
version: "${frameworkVersion}"
|
|
272
|
+
};
|
|
273
|
+
</script>
|
|
274
|
+
${fullscreenBtn}
|
|
275
|
+
</body>
|
|
276
|
+
</html>
|
|
277
|
+
`;
|
|
278
|
+
return defaulthtml;
|
|
279
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { new_buildconfig } from "./buildconfig.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { compressHTML } from "./utils.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function compressHTML(html: string): Promise<string>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { minify } from "html-minifier-terser";
|
|
2
|
+
// Function to compress HTML
|
|
3
|
+
export async function compressHTML(html) {
|
|
4
|
+
return await minify(html, {
|
|
5
|
+
collapseWhitespace: true,
|
|
6
|
+
removeComments: true,
|
|
7
|
+
removeRedundantAttributes: true,
|
|
8
|
+
removeEmptyAttributes: true,
|
|
9
|
+
minifyCSS: true,
|
|
10
|
+
minifyJS: true,
|
|
11
|
+
});
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "samengine-build",
|
|
3
|
+
"version": "1.6.3",
|
|
4
|
+
"description": "The build and export Tool for samengine.",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"pack": "node ../scripts/clean.js && npm run build && npm pack && git push --follow-tags && node ../scripts/checkversion.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js",
|
|
15
|
+
"./utils": "./dist/utils/index.js"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"samengine-build": "dist/cli/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"author": "Shadowdara",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/html-minifier-terser": "^7.0.2",
|
|
24
|
+
"@types/node": "^25.5.2",
|
|
25
|
+
"@types/ws": "^8.18.1",
|
|
26
|
+
"typescript": "^6.0.2"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^5.6.2",
|
|
30
|
+
"esbuild": "^0.28.0",
|
|
31
|
+
"html-minifier-terser": "^7.2.0",
|
|
32
|
+
"mime": "^4.1.0",
|
|
33
|
+
"samengine": "^1.6.3",
|
|
34
|
+
"ws": "^8.20.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/shadowdara/samengine"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/shadowdara/samengine/issues"
|
|
43
|
+
}
|
|
44
|
+
}
|