diff --git a/build/assets/include/js.h b/build/assets/include/js.h index 77a9a92..078e712 100644 --- a/build/assets/include/js.h +++ b/build/assets/include/js.h @@ -2,4 +2,4 @@ #define JS_IMPORT(name) __attribute__((import_module("env"), import_name(#name))) #define JS_EXPORT_AS(name) __attribute__((export_name(#name))) -#define JS_EXPORT __attribute__((visibility("default"))) +#define JS_EXPORT extern "C" __attribute__((visibility("default"))) diff --git a/build/plugins/wasmPlugin.ts b/build/plugins/wasmPlugin.ts index eb4ae89..421766e 100644 --- a/build/plugins/wasmPlugin.ts +++ b/build/plugins/wasmPlugin.ts @@ -9,16 +9,20 @@ interface WasmLoaderConfig { interface CompilerWithFlags { cc: string; + nm: string; flags: string[]; } -const wasiArchiveURL = 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz'; -const rtURL = 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/libclang_rt.builtins-wasm32-wasi-25.0.tar.gz'; +const WASI_VERSION = '32.0'; +const WASI_MAJOR = WASI_VERSION.split('.')[0]; +const wasiArchiveURL = `https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_MAJOR}/wasi-sdk-${WASI_VERSION}-x86_64-linux.tar.gz`; +const rtURL = `https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_MAJOR}/libclang_rt-${WASI_VERSION}.tar.gz`; const getCompiler = async (): Promise => { const wasiDir = path.resolve(import.meta.dir, '..', '..', 'dist', 'wasi'); const cc: CompilerWithFlags = { cc: 'clang', + nm: 'nm', flags: [ '--target=wasm32', '--no-standard-libraries', @@ -28,7 +32,10 @@ const getCompiler = async (): Promise => { await fs.mkdir(wasiDir, { recursive: true }); - if (!await Bun.file(path.resolve(wasiDir, 'VERSION')).exists()) { + const installedVersion = (await Bun.file(path.resolve(wasiDir, 'VERSION')).text().catch(() => '')).slice(0, WASI_VERSION.length); + + if (installedVersion !== WASI_VERSION) { + console.log(`WASI version mismatch. Downloading WASI SDK ${wasiArchiveURL}...`) const response = await fetch(wasiArchiveURL); if (!response.ok) { @@ -37,8 +44,10 @@ const getCompiler = async (): Promise => { const bytes = await response.bytes(); + console.log(`Extracting WASI SDK...`); await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${bytes}`; + console.log(`Downloading libclang_rt.builtins-wasm32-wasi-${WASI_VERSION}.0...`); const rtResponse = await fetch(rtURL); if (!rtResponse.ok) { @@ -47,12 +56,14 @@ const getCompiler = async (): Promise => { const rtBytes = await rtResponse.bytes(); + console.log(`Extracting libclang_rt.builtins-wasm32-wasi-${WASI_VERSION}.0...`); await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${rtBytes}`; } cc.cc = `${path.resolve(wasiDir, 'bin', 'clang')}`; + cc.nm = `${path.resolve(wasiDir, 'bin', 'nm')}`; cc.flags = [ - '--target=wasm32-wasi', + '--target=wasm32-wasip1', `--sysroot=${path.resolve(wasiDir, 'share', 'wasi-sysroot')}`, ]; @@ -106,12 +117,11 @@ async function instantiate(url: string) { const memory = new WebAssembly.Memory({ initial: 32, }); - const table = new WebAssembly.Table({ initial: 16, element: 'anyfunc' }); const decoder = new TextDecoder(); let buf = ''; let errBuf = ''; const { instance } = await WebAssembly.instantiateStreaming(fetch(url), { - env: { memory, __indirect_function_table: table }, + env: { memory }, wasi_snapshot_preview1: new Proxy({ random_get: (ptr: number, length: number) => { const data = new DataView(memory.buffer); @@ -164,9 +174,12 @@ async function instantiate(url: string) { }); return { - ...instance.exports, + ...Object.fromEntries( + Object.entries(instance.exports) + .filter(([k]) => !k.startsWith('_')) + ), memory, - table, + table: instance.exports.__indirect_function_table, get data() { return new DataView(memory.buffer); }, }; } @@ -227,13 +240,24 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { throw new Error('Compile failed, check output'); } + const symbolsResult = await $`${cc.nm} ${objPath}`.quiet(); + + if (symbolsResult.exitCode !== 0) { + throw new Error('nm failed, check output'); + } + + const exports = symbolsResult.text().split('\n') + .map(l => l.match(/^-+ T ([a-z][a-z0-9_]*)/i)) + .filter(m => m != null) + .map(m => m[1]); + const linkFlags = [ '--strip-all', '--lto-O3', '--no-entry', '--import-memory', - '--import-table', - '--export-dynamic', + '--export-table', + ...exports.flatMap(e => ['--export', e]), ].map(f => `-Wl,${f}`); const linkResult = await $`${cc.cc} ${flags} -std=gnu23 ${linkFlags} -lstdc++ -nostartfiles -o ${wasmPath} ${objPath} ${stdlib}`; diff --git a/build/server.ts b/build/server.ts index ccc9caa..34f3f21 100644 --- a/build/server.ts +++ b/build/server.ts @@ -45,6 +45,7 @@ watcher.on('change', async (_, filename) => { // ───────────────────────────────────────────────────────────────────── Bun.serve({ + idleTimeout: 0, websocket: { open(ws) { const game = ws.data.game; diff --git a/compile_flags.txt b/compile_flags.txt index f80d0d1..f1fcefe 100644 --- a/compile_flags.txt +++ b/compile_flags.txt @@ -1,6 +1,6 @@ ---target=wasm32-wasi +--target=wasm32-wasip1 --sysroot=dist/wasi/share/wasi-sysroot --std=c++26 +-std=gnu++26 -fno-exceptions -I build/assets/include diff --git a/src/common/rpg/components/stat.ts b/src/common/rpg/components/stat.ts index a4e7400..28e80b6 100644 --- a/src/common/rpg/components/stat.ts +++ b/src/common/rpg/components/stat.ts @@ -52,8 +52,6 @@ export class Stat extends Component { removeModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void { this.applyModifier(-delta, field); } - - get current(): number { return this.value; } } @component diff --git a/src/games/crawler/index.ts b/src/games/crawler/index.ts index 67d5a61..041b41d 100644 --- a/src/games/crawler/index.ts +++ b/src/games/crawler/index.ts @@ -1,237 +1,183 @@ -import { Color, Glyphs, isCorner, isHorizontal, isVertical, TextRegion } from "@common/display/text"; +import { AlignHorizontal, TextDisplay, TextRegion, type ColorLike } from "@common/display/text"; import { gameLoop } from "@common/game"; -import type { Point } from "@common/geometry"; import Input from "@common/input"; -import { BSP } from "@common/level/bsp"; -import { bresenhamCircleGen } from "@common/navigation/bresenham"; -import { SeededRandom } from "@common/random"; -import { Factions } from "@common/rpg/components/faction"; -import { getPosition, move, Position } from "@common/rpg/components/position"; -import { createViewport } from "@common/rpg/components/render/viewport"; -import { Sprite } from "@common/rpg/components/sprite"; -import { Serialization } from "@common/rpg/core/serialization"; -import { World } from "@common/rpg/core/world"; -import { TextDisplaySystem } from "@common/rpg/systems/render/text"; -import { Resources } from "@common/rpg/utils/resources"; -import { resolveVariables } from "@common/rpg/utils/variables"; +import { Attacked, Damage } from "@common/rpg/components/combat"; +import { Equipment } from "@common/rpg/components/equipment"; +import { Experience, type LevelUpEvent } from "@common/rpg/components/experience"; +import { Inventory } from "@common/rpg/components/inventory"; +import { Item, Items } from "@common/rpg/components/item"; +import { Name } from "@common/rpg/components/name"; +import { getWorldRandom } from "@common/rpg/components/random"; +import { Health } from "@common/rpg/components/stat"; +import { Entity, World } from "@common/rpg/core/world"; +import { CombatSystem, type HitEvent } from "@common/rpg/systems/combat"; +import { capitalize } from "@common/utils"; -const WALL = '#'; -const PLAYER = '@'; +const createPlayer = (world: World, weapon: Entity): Entity => { + const player = world.createEntity(); + player.add(new Health({ value: 100, max: 100 })); + const inv = player.add(new Inventory()); + player.add(new Equipment('weapon')); + player.add(new Experience({ base: 50, factor: 1.5 })); + player.add(new Name("Player")); -function createMap(world: World, random: SeededRandom) { - const MAP_SIZE = 100; - const mapData = new Array(MAP_SIZE * MAP_SIZE).fill(WALL); - - BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, { - minWidth: 10, - minHeight: 8, - depth: 10, - random, - }); - const mapDataString: string[] = []; - const maskDataString: string[] = []; - for (let i = 0; i < MAP_SIZE; i++) { - mapDataString.push(mapData.slice(i * MAP_SIZE, (i + 1) * MAP_SIZE).join('')); - } - for (let i = 0; i < MAP_SIZE * 3; i++) { - maskDataString.push(Array.from({ length: MAP_SIZE * 3 }, () => ' ').join('')); - } - - const map = world.createEntity('map'); - map.add(new Sprite(Resources.add('map', new TextRegion(mapDataString)))); - map.add(new Position(0, 0, -2)); - - const mask = world.createEntity('mask'); - mask.add(new Sprite(Resources.add('mask', new TextRegion(maskDataString)))); - mask.add(new Position(-MAP_SIZE, -MAP_SIZE, 100)); - - return { map, mask }; -} - -function createPlayer(world: World, x: number, y: number) { - const player = world.createEntity('player'); - player.add(new Position(x, y, 10)); - player.add(new Sprite(Resources.add('player', new TextRegion(PLAYER, Color.YELLOW)))); - Factions.join(player, 'players'); + inv.add(weapon); + inv.equip(weapon); return player; } -function createEnemy(world: World, x: number, y: number, sprite: string, ...factions: string[]) { - const enemy = world.createEntity('enemy_*'); - enemy.add(new Position(x, y, 9)); - enemy.add(new Sprite(sprite)); - factions.forEach(faction => Factions.join(enemy, faction)); +const createEnemy = (world: World, level: number): Entity => { + const enemy = world.createEntity(); + const inv = enemy.add(new Inventory()); + enemy.add(new Equipment('weapon')); + + const random = getWorldRandom(world); + enemy.add(new Name(random.nextName({ maxLength: 7 }) + ' The Goblin')); + + const hp = random.nextInt(level * 10, level * 20); + enemy.add(new Health({ value: hp, max: hp })); + + const attack = random.nextInt(level * 3, level * 6); + + const weaponName = random.choice(['sword', 'axe', 'dagger', 'mace']); + const weapon = createWeapon(world, weaponName, attack, level); + + inv.add(weapon); + inv.equip(weapon); return enemy; } -function handleMovement() { - let dx = 0; - let dy = 0; +const createWeapon = (world: World, id: string, damage: number, variance: number): Entity => { + const name = id.split(/[-_ ]+/).map(capitalize).join(' '); + const weapon = Items.register(world, `${id}_*`, name, { equippable: { slotType: 'weapon' } }); - const isReleasedOrRepeated = (...keys: Input.KeyCode[]) => - keys.some(key => Input.isReleased(key) || Input.isRepeated(key)); + weapon.add(new Damage({ value: damage, variance })); - const left = isReleasedOrRepeated(Input.KeyCode.LEFT, Input.KeyCode.H); - const right = isReleasedOrRepeated(Input.KeyCode.RIGHT, Input.KeyCode.L); - const up = isReleasedOrRepeated(Input.KeyCode.UP, Input.KeyCode.K); - const down = isReleasedOrRepeated(Input.KeyCode.DOWN, Input.KeyCode.J); + return weapon; +} - const upLeft = isReleasedOrRepeated(Input.KeyCode.Y); - const upRight = isReleasedOrRepeated(Input.KeyCode.U); - const downLeft = isReleasedOrRepeated(Input.KeyCode.B); - const downRight = isReleasedOrRepeated(Input.KeyCode.N); +const drawEntity = (display: TextDisplay, entity: Entity, x: number, y: number, anchorH: AlignHorizontal) => { + const { value: hp, max: maxHp } = entity.get(Health)!; + let text = `HP: ${hp}/${maxHp}`; - if (left) dx -= 1; - if (right) dx += 1; - if (up) dy -= 1; - if (down) dy += 1; - - if (upLeft) { - dx -= 1; - dy -= 1; - } - if (upRight) { - dx += 1; - dy -= 1; - } - if (downLeft) { - dx -= 1; - dy += 1; - } - if (downRight) { - dx += 1; - dy += 1; + const xp = entity.get(Experience); + if (xp) { + text += `\nLevel: ${xp.level}`; + text += `\nXP: ${xp.xp}/${xp.xpForNext}`; } - return { dx, dy }; + const title = entity.get(Name)?.state ?? entity.id; + + display.drawStringInBox(text, x, y, { title, anchorH }); + + const weapon = entity.get(Equipment)!.getItem('weapon'); + + if (weapon) { + const { value: damage, variance } = weapon.get(Damage)!; + const minDamage = damage - variance; + const maxDamage = damage + variance; + display.drawStringInBox(`Damage: ${minDamage}-${maxDamage}`, x, y + (xp ? 5 : 3), { title: weapon.get(Item)!.name, anchorH }); + } } export default gameLoop(() => { - Input.setRepeatInterval(50); const world = new World(); - const display = world.addSystem(new TextDisplaySystem(100, 25)).display; - const random = new SeededRandom(); - const { map, mask } = createMap(world, random); + world.addSystem(new CombatSystem()); + const display = new TextDisplay(); - const mapData = Resources.get(TextRegion, map.get(Sprite)!.image)!; - const emptyCells: Point[] = []; - for (let x = 0; x < mapData.width; x++) { - for (let y = 0; y < mapData.height; y++) { - if (mapData.get(x, y)[0] === '.') { - emptyCells.push({ x, y }); - } - } - } + const fists = createWeapon(world, 'fists', 7, 2); + const player = createPlayer(world, fists); + const enemy = createEnemy(world, 1); - const maskData = Resources.get(TextRegion, mask.get(Sprite)!.image)!; - const startCell = random.choice(emptyCells); - - const enemyCell = random.choice(emptyCells.filter(c => c !== startCell)); - - const viewport = createViewport(world, { - width: display.width, - height: display.height, - worldX: startCell.x - (display.width >> 1), - worldY: startCell.y - (display.height >> 1), - screenX: 0, - screenY: 0, - }); - const player = createPlayer(world, startCell.x, startCell.y); - - Resources.add('goblin', new TextRegion('g', Color.DARK_GREEN)); - const goblins = Factions.getFaction(world, 'goblins'); - const monsters = Factions.getFaction(world, 'monsters'); - - Factions.setReputationOf(goblins.entity, 'players', -100); - Factions.setReputationOf(monsters.entity, 'players', -100); - - Factions.setReputationOf(goblins.entity, 'monsters', 100); - Factions.setReputationOf(monsters.entity, 'goblins', 100); - - const enemy = createEnemy(world, enemyCell.x, enemyCell.y, 'goblin', 'goblins', 'monsters'); + const log: TextRegion[] = []; const state = { - display, world, - map, mapData, - mask, maskData, maskDirty: true, - random, - viewport, + display, player, enemy, - isWall: (x: number, y: number): boolean => { - const [ch] = state.mapData.get(x, y); - return ch === WALL || isVertical(ch) || isHorizontal(ch) || isCorner(ch); - }, - updateMask: () => { - if (!state.maskDirty) return; - - const { mapData, maskData, player, isWall } = state; - - const playerPos = getPosition(player)!; - for (let x = 0; x < mapData.width; x++) { - for (let y = 0; y < mapData.height; y++) { - const [ch, fg] = maskData.get(x + mapData.width, y + mapData.height); - if (ch !== ' ' && fg !== Color.GRAY) { - maskData.set(x + mapData.width, y + mapData.height, [mapData.get(x, y)[0], Color.GRAY]); - } - } + needUpdate: true, + blocked: false, + log, + addLog(text: string, color?: ColorLike) { + log.push(new TextRegion(text, color)); + if (log.length > 10) { + log.shift(); } - for (const { x, y } of bresenhamCircleGen(playerPos.x, playerPos.y, 10, { fill: 'shadow', breaker: isWall })) { - maskData.set(x + mapData.width, y + mapData.height, '\0'); - } - state.maskDirty = false; + state.needUpdate = true; } }; - Factions.setReputationIn(player, 'monsters', 5); - console.log(Factions.getReputation(enemy, player)); + world.on('Combat.hit', ({ target, data }) => { + if (target instanceof Entity) { + const targetName = target === player ? "Player" : target.get(Name)?.state ?? target.id; + state.addLog(`${targetName} hit by ${data.amount}`); + } + }); + + world.on('Combat.killed', ({ target }) => { + if (target instanceof Entity) { + target.destroy(); + player.get(Experience)!.award(10); + state.addLog("Enemy killed!"); + } + }); + + player.on('Experience.levelup', ({ data }) => { + const health = player.get(Health)!; + + health.applyModifier(Math.round(health.max! * 0.1), 'max'); + health.set(health.max!); + + state.addLog(`Player leveled up to level ${data.level}`); + }) return state; -}, (dt, state) => { - const { - world, - viewport, - player, - updateMask, - isWall, - } = state; +}, (_dt, state) => { + if (Input.isReleased(Input.KeyCode.SPACE, Input.KeyCode.MOUSE_LEFT) && !state.blocked) { + const weapon = state.player.get(Equipment)!.getItem('weapon')!; + state.enemy.add(new Attacked({ attackerId: state.player.id, sourceId: weapon.id })); + state.blocked = true; - const hasInput = Input.hasReleased() || Input.hasRepeated(); - const playerPos = getPosition(player)!; - - if (hasInput) { - if (Input.isHeld(Input.KeyCode.SHIFT)) { - } else { - let { dx, dy } = handleMovement(); - - if (isWall(playerPos.x + dx, playerPos.y)) { - dx = 0; - } - if (isWall(playerPos.x, playerPos.y + dy)) { - dy = 0; - } - if (isWall(playerPos.x + dx, playerPos.y + dy)) { - dx = 0 - dy = 0; + setTimeout(() => { + if (state.enemy.destroyed) { + state.enemy = createEnemy(state.world, state.player.get(Experience)!.level); + } else { + const enemyWeapon = state.enemy.get(Equipment)!.getItem('weapon')!; + state.player.add(new Attacked({ attackerId: state.enemy.id, sourceId: enemyWeapon.id })); } + state.blocked = false; + state.needUpdate = true; + }, 500); - if (dx || dy) { - move(player, dx, dy); - move(viewport, dx, dy); - - playerPos.x + dx; - playerPos.y + dy; - } - } - - // TODO environment step - - state.maskDirty = true; + state.needUpdate = true; } - updateMask(); - world.update(dt); + if (state.needUpdate) { + state.world.update(1); + + state.display.fillBox( + 0, + 0, + state.display.width, + state.display.height, + ' ', + ); + + drawEntity(state.display, state.player, 0, 0, AlignHorizontal.LEFT); + if (!state.enemy.destroyed) { + drawEntity(state.display, state.enemy, state.display.width - 1, 0, AlignHorizontal.RIGHT); + } + + for (let i = 0; i < state.log.length; i++) { + const entry = state.log[i]; + state.display.setRegion(entry, 0, state.display.height - state.log.length + i); + } + + state.needUpdate = false; + } + + state.display.update(); }); diff --git a/src/games/playground/awoo.cpp b/src/games/playground/awoo.cpp index 16b0455..b6d8408 100644 --- a/src/games/playground/awoo.cpp +++ b/src/games/playground/awoo.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include diff --git a/src/games/playground/index.tsx b/src/games/playground/index.tsx index 0b7c0af..f4cc306 100644 --- a/src/games/playground/index.tsx +++ b/src/games/playground/index.tsx @@ -1,183 +1,5 @@ -import { AlignHorizontal, TextDisplay, TextRegion, type ColorLike } from "@common/display/text"; -import { gameLoop } from "@common/game"; -import Input from "@common/input"; -import { Attacked, Damage } from "@common/rpg/components/combat"; -import { Equipment } from "@common/rpg/components/equipment"; -import { Experience, type LevelUpEvent } from "@common/rpg/components/experience"; -import { Inventory } from "@common/rpg/components/inventory"; -import { Item, Items } from "@common/rpg/components/item"; -import { Name } from "@common/rpg/components/name"; -import { getWorldRandom } from "@common/rpg/components/random"; -import { Health } from "@common/rpg/components/stat"; -import { Entity, World } from "@common/rpg/core/world"; -import { CombatSystem, type HitEvent } from "@common/rpg/systems/combat"; -import { capitalize } from "@common/utils"; +import awoo from './awoo.cpp'; -const createPlayer = (world: World, weapon: Entity): Entity => { - const player = world.createEntity(); - player.add(new Health({ value: 100, max: 100 })); - const inv = player.add(new Inventory()); - player.add(new Equipment('weapon')); - player.add(new Experience({ base: 50, factor: 1.5 })); - player.add(new Name("Player")); - - inv.add(weapon); - inv.equip(weapon); - - return player; -} - -const createEnemy = (world: World, level: number): Entity => { - const enemy = world.createEntity(); - const inv = enemy.add(new Inventory()); - enemy.add(new Equipment('weapon')); - - const random = getWorldRandom(world); - enemy.add(new Name(random.nextName({ maxLength: 7 }) + ' The Goblin')); - - const hp = random.nextInt(level * 10, level * 20); - enemy.add(new Health({ value: hp, max: hp })); - - const attack = random.nextInt(level * 3, level * 6); - - const weaponName = random.choice(['sword', 'axe', 'dagger', 'mace']); - const weapon = createWeapon(world, weaponName, attack, level); - - inv.add(weapon); - inv.equip(weapon); - - return enemy; -} - -const createWeapon = (world: World, id: string, damage: number, variance: number): Entity => { - const name = id.split(/[-_ ]+/).map(capitalize).join(' '); - const weapon = Items.register(world, `${id}_*`, name, { equippable: { slotType: 'weapon' } }); - - weapon.add(new Damage({ value: damage, variance })); - - return weapon; -} - -const drawEntity = (display: TextDisplay, entity: Entity, x: number, y: number, anchorH: AlignHorizontal) => { - const { value: hp, max: maxHp } = entity.get(Health)!; - let text = `HP: ${hp}/${maxHp}`; - - const xp = entity.get(Experience); - if (xp) { - text += `\nLevel: ${xp.level}`; - text += `\nXP: ${xp.xp}/${xp.xpForNext}`; - } - - const title = entity.get(Name)?.state ?? entity.id; - - display.drawStringInBox(text, x, y, { title, anchorH }); - - const weapon = entity.get(Equipment)!.getItem('weapon'); - - if (weapon) { - const { value: damage, variance } = weapon.get(Damage)!; - const minDamage = damage - variance; - const maxDamage = damage + variance; - display.drawStringInBox(`Damage: ${minDamage}-${maxDamage}`, x, y + (xp ? 5 : 3), { title: weapon.get(Item)!.name, anchorH }); - } -} - -export default gameLoop(() => { - const world = new World(); - - world.addSystem(new CombatSystem()); - const display = new TextDisplay(); - - const fists = createWeapon(world, 'fists', 7, 2); - const player = createPlayer(world, fists); - const enemy = createEnemy(world, 1); - - const log: TextRegion[] = []; - const state = { - world, - display, - player, - enemy, - needUpdate: true, - blocked: false, - log, - addLog(text: string, color?: ColorLike) { - log.push(new TextRegion(text, color)); - if (log.length > 10) { - log.shift(); - } - state.needUpdate = true; - } - }; - - world.on('Combat.hit', ({ target, data }) => { - if (target instanceof Entity) { - const targetName = target === player ? "Player" : target.get(Name)?.state ?? target.id; - state.addLog(`${targetName} hit by ${data.amount}`); - } - }); - - world.on('Combat.killed', ({ target }) => { - if (target instanceof Entity) { - target.destroy(); - player.get(Experience)!.award(10); - state.addLog("Enemy killed!"); - } - }); - - player.on('Experience.levelup', ({ data }) => { - const health = player.get(Health)!; - - health.applyModifier(Math.round(health.max! * 0.1), 'max'); - health.set(health.max!); - - state.addLog(`Player leveled up to level ${data.level}`); - }) - - return state; -}, (_dt, state) => { - if (Input.isReleased(Input.KeyCode.SPACE, Input.KeyCode.MOUSE_LEFT) && !state.blocked) { - const weapon = state.player.get(Equipment)!.getItem('weapon')!; - state.enemy.add(new Attacked({ attackerId: state.player.id, sourceId: weapon.id })); - state.blocked = true; - - setTimeout(() => { - if (state.enemy.destroyed) { - state.enemy = createEnemy(state.world, state.player.get(Experience)!.level); - } else { - const enemyWeapon = state.enemy.get(Equipment)!.getItem('weapon')!; - state.player.add(new Attacked({ attackerId: state.enemy.id, sourceId: enemyWeapon.id })); - } - state.blocked = false; - state.needUpdate = true; - }, 500); - - state.needUpdate = true; - } - - if (state.needUpdate) { - state.world.update(1); - - state.display.fillBox( - 0, - 0, - state.display.width, - state.display.height, - ' ', - ); - - drawEntity(state.display, state.player, 0, 0, AlignHorizontal.LEFT); - if (!state.enemy.destroyed) { - drawEntity(state.display, state.enemy, state.display.width - 1, 0, AlignHorizontal.RIGHT); - } - - for (let i = 0; i < state.log.length; i++) { - const entry = state.log[i]; - state.display.setRegion(entry, 0, state.display.height - state.log.length + i); - } - - state.needUpdate = false; - } - - state.display.update(); -}); \ No newline at end of file +export default function main() { + console.log(awoo); +} \ No newline at end of file