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_IMPORT(name) __attribute__((import_module("env"), import_name(#name)))
|
||||||
#define JS_EXPORT_AS(name) __attribute__((export_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 {
|
interface CompilerWithFlags {
|
||||||
cc: string;
|
cc: string;
|
||||||
|
nm: string;
|
||||||
flags: 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 WASI_VERSION = '32.0';
|
||||||
const rtURL = 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/libclang_rt.builtins-wasm32-wasi-25.0.tar.gz';
|
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 getCompiler = async (): Promise<CompilerWithFlags> => {
|
||||||
const wasiDir = path.resolve(import.meta.dir, '..', '..', 'dist', 'wasi');
|
const wasiDir = path.resolve(import.meta.dir, '..', '..', 'dist', 'wasi');
|
||||||
const cc: CompilerWithFlags = {
|
const cc: CompilerWithFlags = {
|
||||||
cc: 'clang',
|
cc: 'clang',
|
||||||
|
nm: 'nm',
|
||||||
flags: [
|
flags: [
|
||||||
'--target=wasm32',
|
'--target=wasm32',
|
||||||
'--no-standard-libraries',
|
'--no-standard-libraries',
|
||||||
|
|
@ -28,7 +32,10 @@ const getCompiler = async (): Promise<CompilerWithFlags> => {
|
||||||
|
|
||||||
await fs.mkdir(wasiDir, { recursive: true });
|
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);
|
const response = await fetch(wasiArchiveURL);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -37,8 +44,10 @@ const getCompiler = async (): Promise<CompilerWithFlags> => {
|
||||||
|
|
||||||
const bytes = await response.bytes();
|
const bytes = await response.bytes();
|
||||||
|
|
||||||
|
console.log(`Extracting WASI SDK...`);
|
||||||
await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${bytes}`;
|
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);
|
const rtResponse = await fetch(rtURL);
|
||||||
|
|
||||||
if (!rtResponse.ok) {
|
if (!rtResponse.ok) {
|
||||||
|
|
@ -47,12 +56,14 @@ const getCompiler = async (): Promise<CompilerWithFlags> => {
|
||||||
|
|
||||||
const rtBytes = await rtResponse.bytes();
|
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}`;
|
await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${rtBytes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cc.cc = `${path.resolve(wasiDir, 'bin', 'clang')}`;
|
cc.cc = `${path.resolve(wasiDir, 'bin', 'clang')}`;
|
||||||
|
cc.nm = `${path.resolve(wasiDir, 'bin', 'nm')}`;
|
||||||
cc.flags = [
|
cc.flags = [
|
||||||
'--target=wasm32-wasi',
|
'--target=wasm32-wasip1',
|
||||||
`--sysroot=${path.resolve(wasiDir, 'share', 'wasi-sysroot')}`,
|
`--sysroot=${path.resolve(wasiDir, 'share', 'wasi-sysroot')}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -106,12 +117,11 @@ async function instantiate(url: string) {
|
||||||
const memory = new WebAssembly.Memory({
|
const memory = new WebAssembly.Memory({
|
||||||
initial: 32,
|
initial: 32,
|
||||||
});
|
});
|
||||||
const table = new WebAssembly.Table({ initial: 16, element: 'anyfunc' });
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buf = '';
|
let buf = '';
|
||||||
let errBuf = '';
|
let errBuf = '';
|
||||||
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {
|
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {
|
||||||
env: { memory, __indirect_function_table: table },
|
env: { memory },
|
||||||
wasi_snapshot_preview1: new Proxy({
|
wasi_snapshot_preview1: new Proxy({
|
||||||
random_get: (ptr: number, length: number) => {
|
random_get: (ptr: number, length: number) => {
|
||||||
const data = new DataView(memory.buffer);
|
const data = new DataView(memory.buffer);
|
||||||
|
|
@ -164,9 +174,12 @@ async function instantiate(url: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...instance.exports,
|
...Object.fromEntries(
|
||||||
|
Object.entries(instance.exports)
|
||||||
|
.filter(([k]) => !k.startsWith('_'))
|
||||||
|
),
|
||||||
memory,
|
memory,
|
||||||
table,
|
table: instance.exports.__indirect_function_table,
|
||||||
get data() { return new DataView(memory.buffer); },
|
get data() { return new DataView(memory.buffer); },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -227,13 +240,24 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => {
|
||||||
throw new Error('Compile failed, check output');
|
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 = [
|
const linkFlags = [
|
||||||
'--strip-all',
|
'--strip-all',
|
||||||
'--lto-O3',
|
'--lto-O3',
|
||||||
'--no-entry',
|
'--no-entry',
|
||||||
'--import-memory',
|
'--import-memory',
|
||||||
'--import-table',
|
'--export-table',
|
||||||
'--export-dynamic',
|
...exports.flatMap(e => ['--export', e]),
|
||||||
].map(f => `-Wl,${f}`);
|
].map(f => `-Wl,${f}`);
|
||||||
|
|
||||||
const linkResult = await $`${cc.cc} ${flags} -std=gnu23 ${linkFlags} -lstdc++ -nostartfiles -o ${wasmPath} ${objPath} ${stdlib}`;
|
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>({
|
Bun.serve<ClientData>({
|
||||||
|
idleTimeout: 0,
|
||||||
websocket: {
|
websocket: {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
const game = ws.data.game;
|
const game = ws.data.game;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
--target=wasm32-wasi
|
--target=wasm32-wasip1
|
||||||
--sysroot=dist/wasi/share/wasi-sysroot
|
--sysroot=dist/wasi/share/wasi-sysroot
|
||||||
-std=c++26
|
-std=gnu++26
|
||||||
-fno-exceptions
|
-fno-exceptions
|
||||||
-I
|
-I
|
||||||
build/assets/include
|
build/assets/include
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,6 @@ export class Stat<T = {}> extends Component<StatState & T> {
|
||||||
removeModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
|
removeModifier(delta: number, field: 'value' | 'max' | 'min' = 'value'): void {
|
||||||
this.applyModifier(-delta, field);
|
this.applyModifier(-delta, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
get current(): number { return this.value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@component
|
@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 { gameLoop } from "@common/game";
|
||||||
import type { Point } from "@common/geometry";
|
|
||||||
import Input from "@common/input";
|
import Input from "@common/input";
|
||||||
import { BSP } from "@common/level/bsp";
|
import { Attacked, Damage } from "@common/rpg/components/combat";
|
||||||
import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
import { Equipment } from "@common/rpg/components/equipment";
|
||||||
import { SeededRandom } from "@common/random";
|
import { Experience, type LevelUpEvent } from "@common/rpg/components/experience";
|
||||||
import { Factions } from "@common/rpg/components/faction";
|
import { Inventory } from "@common/rpg/components/inventory";
|
||||||
import { getPosition, move, Position } from "@common/rpg/components/position";
|
import { Item, Items } from "@common/rpg/components/item";
|
||||||
import { createViewport } from "@common/rpg/components/render/viewport";
|
import { Name } from "@common/rpg/components/name";
|
||||||
import { Sprite } from "@common/rpg/components/sprite";
|
import { getWorldRandom } from "@common/rpg/components/random";
|
||||||
import { Serialization } from "@common/rpg/core/serialization";
|
import { Health } from "@common/rpg/components/stat";
|
||||||
import { World } from "@common/rpg/core/world";
|
import { Entity, World } from "@common/rpg/core/world";
|
||||||
import { TextDisplaySystem } from "@common/rpg/systems/render/text";
|
import { CombatSystem, type HitEvent } from "@common/rpg/systems/combat";
|
||||||
import { Resources } from "@common/rpg/utils/resources";
|
import { capitalize } from "@common/utils";
|
||||||
import { resolveVariables } from "@common/rpg/utils/variables";
|
|
||||||
|
|
||||||
const WALL = '#';
|
const createPlayer = (world: World, weapon: Entity): Entity => {
|
||||||
const PLAYER = '@';
|
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) {
|
inv.add(weapon);
|
||||||
const MAP_SIZE = 100;
|
inv.equip(weapon);
|
||||||
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');
|
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEnemy(world: World, x: number, y: number, sprite: string, ...factions: string[]) {
|
const createEnemy = (world: World, level: number): Entity => {
|
||||||
const enemy = world.createEntity('enemy_*');
|
const enemy = world.createEntity();
|
||||||
enemy.add(new Position(x, y, 9));
|
const inv = enemy.add(new Inventory());
|
||||||
enemy.add(new Sprite(sprite));
|
enemy.add(new Equipment('weapon'));
|
||||||
factions.forEach(faction => Factions.join(enemy, faction));
|
|
||||||
|
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;
|
return enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMovement() {
|
const createWeapon = (world: World, id: string, damage: number, variance: number): Entity => {
|
||||||
let dx = 0;
|
const name = id.split(/[-_ ]+/).map(capitalize).join(' ');
|
||||||
let dy = 0;
|
const weapon = Items.register(world, `${id}_*`, name, { equippable: { slotType: 'weapon' } });
|
||||||
|
|
||||||
const isReleasedOrRepeated = (...keys: Input.KeyCode[]) =>
|
weapon.add(new Damage({ value: damage, variance }));
|
||||||
keys.some(key => Input.isReleased(key) || Input.isRepeated(key));
|
|
||||||
|
|
||||||
const left = isReleasedOrRepeated(Input.KeyCode.LEFT, Input.KeyCode.H);
|
return weapon;
|
||||||
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 drawEntity = (display: TextDisplay, entity: Entity, x: number, y: number, anchorH: AlignHorizontal) => {
|
||||||
const upRight = isReleasedOrRepeated(Input.KeyCode.U);
|
const { value: hp, max: maxHp } = entity.get(Health)!;
|
||||||
const downLeft = isReleasedOrRepeated(Input.KeyCode.B);
|
let text = `HP: ${hp}/${maxHp}`;
|
||||||
const downRight = isReleasedOrRepeated(Input.KeyCode.N);
|
|
||||||
|
|
||||||
if (left) dx -= 1;
|
const xp = entity.get(Experience);
|
||||||
if (right) dx += 1;
|
if (xp) {
|
||||||
if (up) dy -= 1;
|
text += `\nLevel: ${xp.level}`;
|
||||||
if (down) dy += 1;
|
text += `\nXP: ${xp.xp}/${xp.xpForNext}`;
|
||||||
|
|
||||||
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 { 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(() => {
|
export default gameLoop(() => {
|
||||||
Input.setRepeatInterval(50);
|
|
||||||
const world = new World();
|
const world = new World();
|
||||||
const display = world.addSystem(new TextDisplaySystem(100, 25)).display;
|
|
||||||
|
|
||||||
const random = new SeededRandom();
|
world.addSystem(new CombatSystem());
|
||||||
const { map, mask } = createMap(world, random);
|
const display = new TextDisplay();
|
||||||
|
|
||||||
const mapData = Resources.get(TextRegion, map.get(Sprite)!.image)!;
|
const fists = createWeapon(world, 'fists', 7, 2);
|
||||||
const emptyCells: Point[] = [];
|
const player = createPlayer(world, fists);
|
||||||
for (let x = 0; x < mapData.width; x++) {
|
const enemy = createEnemy(world, 1);
|
||||||
for (let y = 0; y < mapData.height; y++) {
|
|
||||||
if (mapData.get(x, y)[0] === '.') {
|
|
||||||
emptyCells.push({ x, y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskData = Resources.get(TextRegion, mask.get(Sprite)!.image)!;
|
const log: TextRegion[] = [];
|
||||||
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 state = {
|
const state = {
|
||||||
display,
|
|
||||||
world,
|
world,
|
||||||
map, mapData,
|
display,
|
||||||
mask, maskData, maskDirty: true,
|
|
||||||
random,
|
|
||||||
viewport,
|
|
||||||
player,
|
player,
|
||||||
enemy,
|
enemy,
|
||||||
isWall: (x: number, y: number): boolean => {
|
needUpdate: true,
|
||||||
const [ch] = state.mapData.get(x, y);
|
blocked: false,
|
||||||
return ch === WALL || isVertical(ch) || isHorizontal(ch) || isCorner(ch);
|
log,
|
||||||
},
|
addLog(text: string, color?: ColorLike) {
|
||||||
updateMask: () => {
|
log.push(new TextRegion(text, color));
|
||||||
if (!state.maskDirty) return;
|
if (log.length > 10) {
|
||||||
|
log.shift();
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const { x, y } of bresenhamCircleGen(playerPos.x, playerPos.y, 10, { fill: 'shadow', breaker: isWall })) {
|
state.needUpdate = true;
|
||||||
maskData.set(x + mapData.width, y + mapData.height, '\0');
|
|
||||||
}
|
|
||||||
state.maskDirty = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Factions.setReputationIn(player, 'monsters', 5);
|
world.on<HitEvent>('Combat.hit', ({ target, data }) => {
|
||||||
console.log(Factions.getReputation(enemy, player));
|
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;
|
return state;
|
||||||
}, (dt, state) => {
|
}, (_dt, state) => {
|
||||||
const {
|
if (Input.isReleased(Input.KeyCode.SPACE, Input.KeyCode.MOUSE_LEFT) && !state.blocked) {
|
||||||
world,
|
const weapon = state.player.get(Equipment)!.getItem('weapon')!;
|
||||||
viewport,
|
state.enemy.add(new Attacked({ attackerId: state.player.id, sourceId: weapon.id }));
|
||||||
player,
|
state.blocked = true;
|
||||||
updateMask,
|
|
||||||
isWall,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const hasInput = Input.hasReleased() || Input.hasRepeated();
|
setTimeout(() => {
|
||||||
const playerPos = getPosition(player)!;
|
if (state.enemy.destroyed) {
|
||||||
|
state.enemy = createEnemy(state.world, state.player.get(Experience)!.level);
|
||||||
if (hasInput) {
|
} else {
|
||||||
if (Input.isHeld(Input.KeyCode.SHIFT)) {
|
const enemyWeapon = state.enemy.get(Equipment)!.getItem('weapon')!;
|
||||||
} else {
|
state.player.add(new Attacked({ attackerId: state.enemy.id, sourceId: enemyWeapon.id }));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
state.blocked = false;
|
||||||
|
state.needUpdate = true;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
if (dx || dy) {
|
state.needUpdate = true;
|
||||||
move(player, dx, dy);
|
|
||||||
move(viewport, dx, dy);
|
|
||||||
|
|
||||||
playerPos.x + dx;
|
|
||||||
playerPos.y + dy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO environment step
|
|
||||||
|
|
||||||
state.maskDirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMask();
|
if (state.needUpdate) {
|
||||||
world.update(dt);
|
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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <js.h>
|
#include <js.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,183 +1,5 @@
|
||||||
import { AlignHorizontal, TextDisplay, TextRegion, type ColorLike } from "@common/display/text";
|
import awoo from './awoo.cpp';
|
||||||
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";
|
|
||||||
|
|
||||||
const createPlayer = (world: World, weapon: Entity): Entity => {
|
export default function main() {
|
||||||
const player = world.createEntity();
|
console.log(awoo);
|
||||||
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<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