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/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7",
"assemblyscript": "0.27.29",
"bun-lightningcss": "0.2.0",
"html-minifier": "4.0.0",
"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 { minify } from 'html-minifier';
import UglifyJS from 'uglify-js';
import wasmPlugin from './wasmPlugin';
import imagePlugin from './imagePlugin';
import fontPlugin from './fontPlugin';
import lightningcss from 'bun-lightningcss';
import { getGames } from './isGame';
const transpiler = new Bun.Transpiler();
export async function buildHTML(game: string, production = false) {
const html = await Bun.file(path.resolve(import.meta.dir, '..', 'assets', 'index.html')).text();
const bundle = await Bun.build({
outdir: '/tmp',
entrypoints: [path.resolve(import.meta.dir, '..', 'index.ts')],
sourcemap: production ? 'none' : 'inline',
minify: production,
define: {
global: 'window',
GAME: `"${game}"`,
@ -24,6 +23,7 @@ export async function buildHTML(game: string, production = false) {
plugins: [
imagePlugin,
fontPlugin,
wasmPlugin,
lightningcss(),
]
});
@ -38,17 +38,39 @@ export async function buildHTML(game: string, production = false) {
if (await iconFile.exists()) {
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
.replace('$SCRIPT$', `<script>${script}</script>`)
.replace('$SCRIPT$', `<script type="module">${script}</script>`)
.replace('$TITLE$', game[0].toUpperCase() + game.slice(1).toLowerCase())
.replace('$ICON$', icon);
return minify(resultHTML, {
collapseWhitespace: true,
collapseWhitespace: production,
decodeEntities: true,
minifyCSS: true,
minifyCSS: production,
minifyJS: production,
});
} else {
console.error('Failed: ', !bundle.success, bundle);

View File

@ -10,7 +10,14 @@ const imagePlugin: BunPlugin = {
return {
contents: `
const img = new Image();
const promise = new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
img.src = (${JSON.stringify(src)});
await promise;
export default img;
`,
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) {
this.#score = (value | 0) % 1000000000;
this.#score = Math.max(0, (value | 0) % 1000000000);
}
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 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 => {
if (!Array.isArray(options)) {
options = Object.entries(options) as [T, number][];
@ -22,6 +22,16 @@ export const weightedChoice = <T extends string | number>(options: [T, number][]
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 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 { choice, shuffle } from "@common/utils";
const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 };
@ -54,26 +55,26 @@ export const ITEMS: Item[] = [
},
{ // SWORD
sprite: emptySprite,
accuracy: 1,
accuracy: 0.8,
damage: 5,
ranged: false,
},
{ // BIG_SWORD
sprite: emptySprite,
accuracy: 0.9,
accuracy: 0.7,
damage: 10,
ranged: false,
},
{ // BOW
sprite: emptySprite,
accuracy: 0.7,
damage: 5,
accuracy: 0.6,
damage: 4,
ranged: true,
},
{ // GRENADE
sprite: emptySprite,
accuracy: 0.8,
damage: 10,
damage: 8,
ranged: true,
consumable: true,
},
@ -155,6 +156,102 @@ export const MONSTERS: Monster[] = [
ranged: true,
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) {
@ -164,7 +261,7 @@ export function loadData(spritesheet: BrickDisplayImage) {
for (let i = 0; i < MONSTERS.length; i++) {
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);
}
}

View File

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

View File

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