1
0
Fork 0

Async imports support, aggressive uglifying

This commit is contained in:
Pabloader 2024-07-08 14:32:19 +00:00
parent 5e4af4d923
commit 576e191631
12 changed files with 266 additions and 51 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,9 +14,11 @@
"@types/bun": "latest", "@types/bun": "latest",
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7", "@types/inquirer": "9.0.7",
"assemblyscript": "0.27.29",
"bun-lightningcss": "0.2.0", "bun-lightningcss": "0.2.0",
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
"inquirer": "9.3.4", "inquirer": "9.3.4",
"typescript": "5.5.2" "typescript": "5.5.2",
"uglify-js": "3"
} }
} }

View File

@ -1,21 +1,20 @@
import path from 'path'; import path from 'path';
import { minify } from 'html-minifier'; import { minify } from 'html-minifier';
import UglifyJS from 'uglify-js';
import wasmPlugin from './wasmPlugin';
import imagePlugin from './imagePlugin'; import imagePlugin from './imagePlugin';
import fontPlugin from './fontPlugin'; import fontPlugin from './fontPlugin';
import lightningcss from 'bun-lightningcss'; import lightningcss from 'bun-lightningcss';
import { getGames } from './isGame'; import { getGames } from './isGame';
const transpiler = new Bun.Transpiler();
export async function buildHTML(game: string, production = false) { export async function buildHTML(game: string, production = false) {
const html = await Bun.file(path.resolve(import.meta.dir, '..', 'assets', 'index.html')).text(); const html = await Bun.file(path.resolve(import.meta.dir, '..', 'assets', 'index.html')).text();
const bundle = await Bun.build({ const bundle = await Bun.build({
outdir: '/tmp', outdir: '/tmp',
entrypoints: [path.resolve(import.meta.dir, '..', 'index.ts')], entrypoints: [path.resolve(import.meta.dir, '..', 'index.ts')],
sourcemap: production ? 'none' : 'inline', sourcemap: production ? 'none' : 'inline',
minify: production,
define: { define: {
global: 'window', global: 'window',
GAME: `"${game}"`, GAME: `"${game}"`,
@ -24,6 +23,7 @@ export async function buildHTML(game: string, production = false) {
plugins: [ plugins: [
imagePlugin, imagePlugin,
fontPlugin, fontPlugin,
wasmPlugin,
lightningcss(), lightningcss(),
] ]
}); });
@ -38,17 +38,39 @@ export async function buildHTML(game: string, production = false) {
if (await iconFile.exists()) { if (await iconFile.exists()) {
icon = `<link rel="icon" href="data:;base64,${Buffer.from(await iconFile.arrayBuffer()).toString('base64')}" />`; icon = `<link rel="icon" href="data:;base64,${Buffer.from(await iconFile.arrayBuffer()).toString('base64')}" />`;
} }
const script = await bundle.outputs[0].text(); let script = await bundle.outputs[0].text();
const inits = new Set<string>();
script = script.replace(/var (init_[^ ]+) = __esm\(\(\)/g, (_, $1) => {
inits.add($1);
return `var ${$1} = __esm(async ()`;
});
for (const init of inits) {
script = script.replaceAll(`${init}()`, `await ${init}()`);
}
script = script.replaceAll('await Promise.resolve().then(() =>', '(');
if (production) {
const minifyResult = UglifyJS.minify(script, {
module: true,
});
if (minifyResult.error) {
console.warn(`Minify error: ${minifyResult.error}`);
} else {
script = minifyResult.code;
}
}
const resultHTML = html const resultHTML = html
.replace('$SCRIPT$', `<script>${script}</script>`) .replace('$SCRIPT$', `<script type="module">${script}</script>`)
.replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase()) .replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase())
.replace('$ICON$', icon); .replace('$ICON$', icon);
return minify(resultHTML, { return minify(resultHTML, {
collapseWhitespace: true, collapseWhitespace: production,
decodeEntities: true, decodeEntities: true,
minifyCSS: true, minifyCSS: production,
minifyJS: production,
}); });
} else { } else {
console.error('Failed: ', !bundle.success, bundle); console.error('Failed: ', !bundle.success, bundle);

View File

@ -10,7 +10,14 @@ const imagePlugin: BunPlugin = {
return { return {
contents: ` contents: `
const img = new Image(); const img = new Image();
const promise = new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
img.src = (${JSON.stringify(src)}); img.src = (${JSON.stringify(src)});
await promise;
export default img; export default img;
`, `,
loader: 'js', loader: 'js',

39
src/build/wasmPlugin.ts Normal file
View File

@ -0,0 +1,39 @@
import { plugin, type BunPlugin } from "bun";
import path from 'path';
import asc from 'assemblyscript/asc';
const wasmPlugin: BunPlugin = {
name: "WASM loader",
async setup(build) {
build.onLoad({ filter: /\.wasm\.ts$/ }, async (args) => {
const wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm');
const jsPath = wasmPath.replace(/\.wasm$/, '.js');
const { error, stderr } = await asc.main([
args.path,
'--outFile', wasmPath,
'--textFile', wasmPath.replace(/\.wasm$/, '.wat'),
'--optimize', '--bindings', 'esm',
]);
if (error) {
console.error(stderr.toString(), error.message);
throw error;
} else {
const jsContent = await Bun.file(jsPath).text();
const wasmContent = await Bun.file(wasmPath).arrayBuffer();
const wasmBuffer = Buffer.from(wasmContent).toString('base64');
const wasmURL = `data:application/wasm;base64,${wasmBuffer}`;
return {
loader: 'js',
contents: jsContent
.replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`),
};
}
});
}
};
plugin(wasmPlugin);
export default wasmPlugin;

View File

@ -36,7 +36,7 @@ export class BrickDisplay implements Display {
} }
set score(value) { set score(value) {
this.#score = (value | 0) % 1000000000; this.#score = Math.max(0, (value | 0) % 1000000000);
} }
get speed() { get speed() {

View File

@ -3,7 +3,7 @@ export const nextFrame = async (): Promise<number> => new Promise((resolve) => r
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random()); export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
export const randBool = () => Math.random() < 0.5; export const randBool = () => Math.random() < 0.5;
export const choice = (array: any[]) => array[randInt(0, array.length)]; export const choice = <T>(array: T[]): T => array[randInt(0, array.length)];
export const weightedChoice = <T extends string | number>(options: [T, number][] | Partial<Record<T, number>>): T | null => { export const weightedChoice = <T extends string | number>(options: [T, number][] | Partial<Record<T, number>>): T | null => {
if (!Array.isArray(options)) { if (!Array.isArray(options)) {
options = Object.entries(options) as [T, number][]; options = Object.entries(options) as [T, number][];
@ -22,6 +22,16 @@ export const weightedChoice = <T extends string | number>(options: [T, number][]
return null; return null;
} }
export const shuffle = <T>(array: T[]): T[] => {
const shuffledArray = [...array];
for (let i = shuffledArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
}
return shuffledArray;
}
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

After

Width:  |  Height:  |  Size: 241 B

View File

@ -1,4 +1,5 @@
import { BrickDisplay, type BrickDisplayImage } from "@common/display"; import { BrickDisplay, type BrickDisplayImage } from "@common/display";
import { choice, shuffle } from "@common/utils";
const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 }; const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 };
@ -54,26 +55,26 @@ export const ITEMS: Item[] = [
}, },
{ // SWORD { // SWORD
sprite: emptySprite, sprite: emptySprite,
accuracy: 1, accuracy: 0.8,
damage: 5, damage: 5,
ranged: false, ranged: false,
}, },
{ // BIG_SWORD { // BIG_SWORD
sprite: emptySprite, sprite: emptySprite,
accuracy: 0.9, accuracy: 0.7,
damage: 10, damage: 10,
ranged: false, ranged: false,
}, },
{ // BOW { // BOW
sprite: emptySprite, sprite: emptySprite,
accuracy: 0.7, accuracy: 0.6,
damage: 5, damage: 4,
ranged: true, ranged: true,
}, },
{ // GRENADE { // GRENADE
sprite: emptySprite, sprite: emptySprite,
accuracy: 0.8, accuracy: 0.8,
damage: 10, damage: 8,
ranged: true, ranged: true,
consumable: true, consumable: true,
}, },
@ -155,6 +156,102 @@ export const MONSTERS: Monster[] = [
ranged: true, ranged: true,
secondLootChance: 0, secondLootChance: 0,
}, },
{ // SLIME
sprite: emptySprite,
health: 5,
maxHealth: 5,
damage: 3,
accuracy: 0.6,
lootTable: {
[Items.HEAL]: 0.5,
[Items.SWORD]: 0.3,
[Items.POTION]: 0.15,
[Items.BOW]: 0.05,
[Items.HEAVY_SWORD]: 0.01,
},
ranged: false,
secondLootChance: 0.3,
},
{ // DEMON
sprite: emptySprite,
health: 6,
maxHealth: 6,
damage: 3,
accuracy: 0.8,
lootTable: {
[Items.HEAL]: 0.5,
[Items.SWORD]: 0.3,
[Items.POTION]: 0.15,
[Items.BOW]: 0.1,
[Items.HEAVY_SWORD]: 0.05,
},
ranged: false,
secondLootChance: 0.3,
},
{ // ELEMENTAL
sprite: emptySprite,
health: 7,
maxHealth: 7,
damage: 4,
accuracy: 0.5,
lootTable: {
[Items.HEAL]: 0.2,
[Items.POTION]: 0.2,
[Items.BOW]: 0.3,
[Items.GRENADE]: 0.2,
},
ranged: true,
secondLootChance: 0.3,
},
{ // BIG_SLIME
sprite: emptySprite,
health: 10,
maxHealth: 10,
damage: 5,
accuracy: 0.6,
lootTable: {
[Items.HEAL]: 0.5,
[Items.SWORD]: 0.3,
[Items.POTION]: 0.2,
[Items.BOW]: 0.3,
[Items.GRENADE]: 0.2,
[Items.HEAVY_SWORD]: 0.1,
},
ranged: false,
secondLootChance: 0.7,
},
{ // BIG_ELEMENTAL
sprite: emptySprite,
health: 20,
maxHealth: 20,
damage: 6,
accuracy: 0.9,
lootTable: {},
ranged: true,
secondLootChance: 0,
},
];
export const MONSTERS_ORDER = [
...shuffle([Monsters.SMALL_SLIME, Monsters.SMALL_DEMON, Monsters.SNAKE]),
Monsters.SMALL_ELEMENTAL,
choice([Monsters.SMALL_SLIME, Monsters.SMALL_DEMON, Monsters.SNAKE]),
...shuffle([Monsters.SLIME, Monsters.DEMON]),
Monsters.ELEMENTAL,
choice([Monsters.SLIME, Monsters.DEMON]),
Monsters.BIG_SLIME,
choice([
Monsters.SMALL_SLIME,
Monsters.SMALL_DEMON,
Monsters.SNAKE,
Monsters.SMALL_ELEMENTAL,
Monsters.SLIME,
Monsters.DEMON,
Monsters.ELEMENTAL,
]),
Monsters.BIG_ELEMENTAL,
]; ];
export function loadData(spritesheet: BrickDisplayImage) { export function loadData(spritesheet: BrickDisplayImage) {
@ -164,7 +261,7 @@ export function loadData(spritesheet: BrickDisplayImage) {
for (let i = 0; i < MONSTERS.length; i++) { for (let i = 0; i < MONSTERS.length; i++) {
const x = (i % 8) << 2; const x = (i % 8) << 2;
const y = (3 + i / 8) << 2; const y = (2 + i / 8) << 2;
MONSTERS[i].sprite = BrickDisplay.extractSprite(spritesheet, x, y, 4, 4); MONSTERS[i].sprite = BrickDisplay.extractSprite(spritesheet, x, y, 4, 4);
} }
} }

View File

@ -1,14 +1,16 @@
import { BrickDisplay, type BrickDisplayImage } from "@common/display"; import { BrickDisplay, type BrickDisplayImage } from "@common/display";
import { isPressed, updateKeys } from "@common/input"; import { isPressed, updateKeys } from "@common/input";
import backgroundImage from './assets/background.png';
import spritesheetImage from './assets/spritesheet.png'; import spritesheetImage from './assets/spritesheet.png';
import { ITEMS, Items, MONSTERS, loadData, type Item, type Monster } from "./data"; import { ITEMS, Items, MONSTERS, MONSTERS_ORDER, loadData, type Item, type Monster } from "./data";
import { delay, randBool, weightedChoice } from "@common/utils"; import { randBool, weightedChoice } from "@common/utils";
let display: BrickDisplay; let display: BrickDisplay;
let background: BrickDisplayImage; const spritesheet = BrickDisplay.convertImage(spritesheetImage);
let playerSprite: BrickDisplayImage; const background = BrickDisplay.extractSprite(spritesheet, 12, 12, 10, 20);
const winScreen = BrickDisplay.extractSprite(spritesheet, 24, 12, 6, 20);
const playerSprite = BrickDisplay.extractSprite(spritesheet, 0, 4, 4, 4);
const playerDeadSprite = BrickDisplay.extractSprite(spritesheet, 4, 4, 4, 4);
const playerY = 2; const playerY = 2;
@ -16,6 +18,9 @@ let y = 0;
let targetY = 0; let targetY = 0;
let playerHealth = 10; let playerHealth = 10;
let playerBlink = 0; let playerBlink = 0;
let missBlink = 0;
let win = false;
let winBlink = false;
let playerTurn = true; let playerTurn = true;
let lootToConfirm: Items | null = null; let lootToConfirm: Items | null = null;
@ -33,6 +38,7 @@ let monsterBlink = 0;
let bulletY = -1; let bulletY = -1;
let bulletBlink = false; let bulletBlink = false;
let bulletTargetY = -1; let bulletTargetY = -1;
let bulletItem: Item | null = null;
let lootY = -1; let lootY = -1;
let lootItem: Items | null = null; let lootItem: Items | null = null;
@ -50,9 +56,19 @@ async function loop(time: number) {
lootBlink = !lootBlink; lootBlink = !lootBlink;
} }
if (win && frames % 16 == 0) {
winBlink = !winBlink;
}
if (playerHealth <= 0) {
playerHealth = 0;
playerTurn = false;
}
let lootConfirmed = false; let lootConfirmed = false;
const item = ITEMS[inventory[selectedSlot]]; const item = ITEMS[inventory[selectedSlot]];
const monster = MONSTERS[currentMonster]; const monster = MONSTERS[MONSTERS_ORDER[currentMonster]];
if (playerBlink) { if (playerBlink) {
if (frames % 4 == 0) { if (frames % 4 == 0) {
playerBlink--; playerBlink--;
@ -61,6 +77,10 @@ async function loop(time: number) {
if (frames % 4 == 0) { if (frames % 4 == 0) {
monsterBlink--; monsterBlink--;
} }
} else if (missBlink) {
if (frames % 4 == 0) {
missBlink--;
}
} else if (bulletTargetY !== bulletY) { } else if (bulletTargetY !== bulletY) {
console.log('Bullet animation'); console.log('Bullet animation');
if (frames % 2 == 0) { if (frames % 2 == 0) {
@ -77,7 +97,7 @@ async function loop(time: number) {
if (frames % 3 == 0) { if (frames % 3 == 0) {
y += Math.sign(targetY - y); y += Math.sign(targetY - y);
} }
} else if (playerTurn && playerHealth > 0) { } else if (playerTurn) {
lootToConfirm = null; lootToConfirm = null;
if (y === lootY) { if (y === lootY) {
lootToConfirm = lootItem; lootToConfirm = lootItem;
@ -104,6 +124,7 @@ async function loop(time: number) {
console.log('Ranged attack') console.log('Ranged attack')
bulletY = y + playerY + playerSprite.height; bulletY = y + playerY + playerSprite.height;
bulletTargetY = monsterY + monster.sprite.height; bulletTargetY = monsterY + monster.sprite.height;
bulletItem = item;
} else if (item.heal) { } else if (item.heal) {
console.log('Heal' + item.heal); console.log('Heal' + item.heal);
playerHealth += item.heal; playerHealth += item.heal;
@ -123,6 +144,9 @@ async function loop(time: number) {
playerHealth += i.heal; playerHealth += i.heal;
} else { } else {
inventory.push(lootToConfirm); inventory.push(lootToConfirm);
if (!i.heal) {
selectedSlot = inventory.length - 1;
}
} }
lootConfirmed = true; lootConfirmed = true;
@ -147,7 +171,9 @@ async function loop(time: number) {
lootY = monsterY - 1; lootY = monsterY - 1;
lootItem = drop; lootItem = drop;
if (drop != null) { if (drop == null) {
spawnNextMonster();
} else {
lootTable[drop] = 0; lootTable[drop] = 0;
const rnd = Math.random(); const rnd = Math.random();
if (rnd < monster.secondLootChance) { if (rnd < monster.secondLootChance) {
@ -158,11 +184,11 @@ async function loop(time: number) {
playerTurn = true; playerTurn = true;
} }
} else if (monster.ranged) { } else if (monster.ranged) {
const retreat = randBool(); const retreat = randBool() && monsterY - y < 12;
if (retreat) { if (retreat) {
monsterTargetY = monsterY + 4; monsterTargetY = monsterY + 4;
playerTurn = true; playerTurn = true;
} else { } else {
bulletY = monsterY + monster.sprite.height; bulletY = monsterY + monster.sprite.height;
bulletTargetY = y + playerY + playerSprite.height; bulletTargetY = y + playerY + playerSprite.height;
} }
@ -193,7 +219,7 @@ async function loop(time: number) {
bulletTargetY = bulletY = -1; bulletTargetY = bulletY = -1;
bulletBlink = false; bulletBlink = false;
if (playerTurn) { if (playerTurn) {
damageMonster(item); damageMonster(bulletItem ?? item);
playerTurn = false; playerTurn = false;
} else { } else {
damagePlayer(monster); damagePlayer(monster);
@ -204,16 +230,23 @@ async function loop(time: number) {
display.clear(); display.clear();
display.clear(true); display.clear(true);
display.speed = item.damage; if (lootToConfirm) {
const i = ITEMS[lootToConfirm];
display.speed = i.heal ?? i.damage;
} else {
display.speed = item.heal ?? item.damage;
}
display.score = playerHealth; display.score = playerHealth;
display.gameOver = playerHealth === 0; display.gameOver = playerHealth <= 0;
const bgY = y % display.height; const bgY = y % display.height;
display.drawImage(background, 0, bgY); if (missBlink % 2 == 0 || playerHealth <= 0) {
display.drawImage(background, 0, bgY - display.height); display.drawImage(background, 0, bgY);
display.drawImage(background, 0, bgY - display.height);
}
if (playerBlink % 2 == 0) { if (playerBlink % 2 == 0) {
display.drawImage(playerSprite, 3, display.height - playerSprite.height - playerY); display.drawImage(playerHealth > 0 ? playerSprite : playerDeadSprite, 3, display.height - playerSprite.height - playerY);
} }
if (monsterAlive && monsterBlink % 2 == 0 && monster) { if (monsterAlive && monsterBlink % 2 == 0 && monster) {
@ -240,6 +273,11 @@ async function loop(time: number) {
display.drawImage(item.sprite, 0, 0, true); display.drawImage(item.sprite, 0, 0, true);
} }
if (winBlink) {
display.fillRect(2, 0, winScreen.width, winScreen.height, false);
display.drawImage(winScreen, 2, 0);
}
display.update(); display.update();
updateKeys(); updateKeys();
@ -248,21 +286,26 @@ async function loop(time: number) {
function spawnNextMonster() { function spawnNextMonster() {
currentMonster++; currentMonster++;
monsterAlive = MONSTERS[currentMonster] != null; monsterAlive = MONSTERS[MONSTERS_ORDER[currentMonster]] != null;
if (monsterAlive) { if (monsterAlive) {
MONSTERS[currentMonster].health = MONSTERS[currentMonster].maxHealth; MONSTERS[MONSTERS_ORDER[currentMonster]].health = MONSTERS[MONSTERS_ORDER[currentMonster]].maxHealth;
monsterY = y + 20;
monsterTargetY = y + 13;
} else {
win = true;
} }
monsterY = y + 20;
monsterTargetY = y + 13;
} }
function damageMonster(item: Item) { function damageMonster(item: Item) {
if (!monsterAlive) return;
const rnd = Math.random(); const rnd = Math.random();
console.log(`Attack for ${item.damage}, success: ${rnd.toFixed(1)} < ${item.accuracy}`); console.log(`Attack for ${item.damage}, success: ${rnd.toFixed(1)} < ${item.accuracy}`);
if (monsterAlive && rnd < item.accuracy) { if (rnd < item.accuracy) {
MONSTERS[currentMonster].health -= item.damage; MONSTERS[MONSTERS_ORDER[currentMonster]].health -= item.damage;
monsterBlink = 10; monsterBlink = 6;
console.log(`Monster HP: ${MONSTERS[currentMonster].health}`); console.log(`Monster HP: ${MONSTERS[MONSTERS_ORDER[currentMonster]].health}`);
} else {
missBlink = 6;
} }
} }
@ -272,10 +315,9 @@ function damagePlayer(monster: Monster) {
if (rnd < monster.accuracy) { if (rnd < monster.accuracy) {
playerHealth -= monster.damage; playerHealth -= monster.damage;
playerBlink = 10; playerBlink = 6;
if (playerHealth <= 0) { } else {
playerHealth = 0; missBlink = 6;
}
} }
} }
@ -283,12 +325,7 @@ export default function main() {
display = new BrickDisplay(); display = new BrickDisplay();
display.init(); display.init();
background = BrickDisplay.convertImage(backgroundImage);
const spritesheet = BrickDisplay.convertImage(spritesheetImage);
loadData(spritesheet); loadData(spritesheet);
playerSprite = BrickDisplay.extractSprite(spritesheet, 0, 8, 4, 4);
spawnNextMonster(); spawnNextMonster();
requestAnimationFrame(loop); requestAnimationFrame(loop);
} }

View File

@ -27,5 +27,6 @@
"paths": { "paths": {
"@common/*": ["./src/common/*"] "@common/*": ["./src/common/*"]
} }
} },
"include": ["./**/*.ts", "./**/*.tsx", "./node_modules/assemblyscript/std/portable/index.d.ts"],
} }