Upgrade wasi
This commit is contained in:
parent
ec8cff15ac
commit
e22d3b7c6f
|
|
@ -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")))
|
||||
|
|
|
|||
|
|
@ -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<CompilerWithFlags> => {
|
||||
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<CompilerWithFlags> => {
|
|||
|
||||
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<CompilerWithFlags> => {
|
|||
|
||||
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<CompilerWithFlags> => {
|
|||
|
||||
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}`;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ watcher.on('change', async (_, filename) => {
|
|||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Bun.serve<ClientData>({
|
||||
idleTimeout: 0,
|
||||
websocket: {
|
||||
open(ws) {
|
||||
const game = ws.data.game;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -52,8 +52,6 @@ export class Stat<T = {}> extends Component<StatState & T> {
|
|||
removeModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
|
||||
this.applyModifier(-delta, field);
|
||||
}
|
||||
|
||||
get current(): number { return this.value; }
|
||||
}
|
||||
|
||||
@component
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
const upLeft = isReleasedOrRepeated(Input.KeyCode.Y);
|
||||
const upRight = isReleasedOrRepeated(Input.KeyCode.U);
|
||||
const downLeft = isReleasedOrRepeated(Input.KeyCode.B);
|
||||
const downRight = isReleasedOrRepeated(Input.KeyCode.N);
|
||||
|
||||
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;
|
||||
return weapon;
|
||||
}
|
||||
|
||||
return { dx, dy };
|
||||
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(() => {
|
||||
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<HitEvent>('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<LevelUpEvent>('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)) {
|
||||
setTimeout(() => {
|
||||
if (state.enemy.destroyed) {
|
||||
state.enemy = createEnemy(state.world, state.player.get(Experience)!.level);
|
||||
} else {
|
||||
let { dx, dy } = handleMovement();
|
||||
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 (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;
|
||||
state.needUpdate = true;
|
||||
}
|
||||
|
||||
if (dx || dy) {
|
||||
move(player, dx, dy);
|
||||
move(viewport, dx, dy);
|
||||
if (state.needUpdate) {
|
||||
state.world.update(1);
|
||||
|
||||
playerPos.x + dx;
|
||||
playerPos.y + dy;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO environment step
|
||||
|
||||
state.maskDirty = true;
|
||||
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);
|
||||
}
|
||||
|
||||
updateMask();
|
||||
world.update(dt);
|
||||
state.needUpdate = false;
|
||||
}
|
||||
|
||||
state.display.update();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
#include <format>
|
||||
#include <iostream>
|
||||
#include <js.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
export default function main() {
|
||||
console.log(awoo);
|
||||
}
|
||||
|
||||
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<HitEvent>('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<LevelUpEvent>('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();
|
||||
});
|
||||
Loading…
Reference in New Issue