diff --git a/CLAUDE.md b/CLAUDE.md index c877c2b9..e395c0d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,9 +36,10 @@ Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty sy ### JavaScript Style Guide - Use `use()` function for imports (Misty-style, not ES6 import/export) -- Prefer objects and closures over ES6 classes +- Prefer closures and javascript objects and prototypes over ES6 style classes - Follow existing JavaScript patterns in the codebase - Functions as first-class citizens +- Do not use const or let; only var ### Core Systems 1. **Actor System** (scripts/core/engine.js) @@ -59,8 +60,11 @@ Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty sy ### Engine Entry Points - `source/prosperon.c` - Main C entry point -- `scripts/core/engine.js` - JavaScript engine initialization -- `scripts/core/base.js` - Base prototypes and utilities +- `scripts/core/engine.js` - JavaScript engine initialization for system +- `scripts/core/base.js` has modifications to this Javascript runtime (for example, additions to the base Array, String, etc) + +### Subprojects +- C code has many subprojects, who's source and sometimes documentation can be found in subprojects. subprojects/quickjs/doc has documentation for quickjs ### Resource System - Scripts are bundled into `core.zip` during build @@ -91,6 +95,12 @@ cd examples/chess ./prosperon ``` +### Documentation +- Documentation is found in docs +- Documentation for the JS modules loaded with 'use' is docs/api/modules +- .md files directly in docs gives a high level overview +- docs/dull is what this specific Javascript system is (including alterations from quickjs/es6) + ### Shader Development - Shaders are in `shaders/` directory as HLSL - Compile script: `shaders/compile.sh` @@ -117,4 +127,134 @@ meson test -C build_dbg - Use debug build: `make debug` - Tracy profiler support when enabled - Console logging available via `console.log()`, `console.error()`, etc. -- Log files written to `.prosperon/log.txt` \ No newline at end of file +- Log files written to `.prosperon/log.txt` + +# Project Structure Notes + +## Core JavaScript Modules + +- JavaScript modules are defined using the MISTUSE macro in jsffi.c +- The `js_os_funcs`, `js_io_funcs`, etc. arrays define the available functions for each module +- New functions are added with MIST_FUNC_DEF(module, function, args_count) + +## File I/O + +- `io.slurp(path)` - Reads a file as text +- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer +- `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file +- `io.exists(path)` - Checks if a file exists + +## Script Loading + +- The `use(path)` function in engine.js loads JavaScript modules +- Script loading happens in prosperon.c and the engine.js script +- jsffi.c contains the C hooks for the QuickJS JavaScript engine +- Added functionality for bytecode compilation and loading: + - `os.compile_bytecode(source, filename)` - Compiles JS to bytecode, returns ArrayBuffer + - `os.eval_bytecode(bytecode)` - Evaluates bytecode from an ArrayBuffer + - `compile(scriptPath)` - Compiles a JS file to a .jso bytecode file + - Modified `use()` to check for .jso files before loading .js files + +## QuickJS Bytecode API + +- `JS_Eval` with JS_EVAL_FLAG_COMPILE_ONLY - Compiles without executing +- `JS_WriteObject` with JS_WRITE_OBJ_BYTECODE - Serializes to bytecode +- `JS_ReadObject` with JS_READ_OBJ_BYTECODE - Deserializes and loads bytecode +- Bytecode files use .jso extension alongside .js files + +## Available JavaScript APIs + +### Core APIs +- `actor` - Base prototype for all actor objects +- `$_` - Special global for actor messaging +- `prosperon` - Global engine interface +- `console` - Logging and debugging interface + +### Framework APIs +- `moth` - Higher-level game framework that simplifies Prosperon usage + - Handles window creation, game loop, and event dispatching + - Provides simple configuration via config.js + - Auto-initializes systems like rendering and input + - Manages camera, resolution, and FPS automatically + +### Rendering +- `draw2d` - 2D drawing primitives +- `render` - Low-level rendering operations +- `graphics` - Higher-level graphics utilities +- `camera` - Camera controls and transformations +- `sprite` - Sprite rendering and management + +### Physics and Math +- `math` - Mathematical utilities +- `geometry` - Geometric calculations and shapes +- `transform` - Object transformations + +### Input and Events +- `input` - Mouse, keyboard, and touch handling +- `event` - Event management system + +### Networking +- `enet` - Networking through ENet library +- `http` - HTTP client capabilities + +### Audio +- `sound` - Audio playback using SoLoud + +### Utility Modules +- `time` - Time management and delays +- `io` - File I/O operations +- `json` - JSON parsing and serialization +- `util` - General utilities +- `color` - Color manipulation +- `miniz` - Compression utilities +- `nota` - Structured data format +- `wota` - Serialization format +- `qr` - QR code generation/reading +- `tween` - Animation tweening +- `spline` - Spline calculations +- `imgui` - Immediate mode GUI + +## Game Development Patterns + +### Project Structure +- Game config is typically in `config.js` +- Main entry point is `main.js` +- Resource loading through `resources.js` + +### Actor Pattern Usage +- Create actors with `actor.spawn(script, config)` +- Manage actor hierarchy with overlings and underlings +- Schedule actor tasks with `delay()` method +- Clean up with `kill()` and `garbage()` + +### Game Loop Registration +- Register functions like `update`, `draw`, `gui`, etc. +- Set function.layer property to control execution order +- Use `Register` system to manage callbacks + +### Program vs Module Pattern +- Programs are actor scripts that don't return values, they execute top-to-bottom +- Modules are files that return single values (usually objects) that get frozen +- Programs can spawn other programs as underlings +- Programs have lifecycle hooks: awake, update, draw, garbage, etc. + +## Technical Capabilities + +### Graphics Pipeline +- Supports multiple render backends (Direct3D, Metal, Vulkan via SDL3) +- Custom shader system with cross-platform compilation +- Sprite batching for efficient 2D rendering +- Camera systems for both 2D and 3D + +### Asset Support +- Images: PNG, JPG, QOI, etc. +- Audio: Various formats through SoLoud +- Models: Basic 3D model support +- Custom formats: Aseprite animations, etc. + +### Developer Tools +- Built-in documentation system with `prosperon.DOC` +- Tracy profiler integration for performance monitoring +- Imgui debugging tools +- Console logging with various severity levels +``` \ No newline at end of file diff --git a/examples/chess/main.js b/examples/chess/main.js index 0c372285..e4520575 100644 --- a/examples/chess/main.js +++ b/examples/chess/main.js @@ -1,18 +1,17 @@ /* main.js – runs the demo with your prototype-based grid */ + +var moth = use('moth') +var json = use('json') + var res = 160 var internal_res = 480 -var render = use('render'); -render.initialize({ width:res, height:res, resolution_x:internal_res, resolution_y:internal_res, mode:'letterbox' }); + +moth.initialize({ width:res, height:res, resolution_x:internal_res, resolution_y:internal_res, mode:'letterbox' }); var os = use('os'); var draw2d = use('draw2d'); var gfx = use('graphics'); -/* camera – unchanged */ -var camera = { size:[internal_res,internal_res], transform:os.make_transform(), fov:50, near_z:0, - far_z:1000, surface:undefined, viewport:{x:0,y:0,width:1,height:1}, - ortho:true, anchor:[0,0] }; - /*──── import our pieces + systems ───────────────────────────────────*/ var Grid = use('grid'); // your new ctor var MovementSystem = use('movement').MovementSystem; @@ -21,97 +20,281 @@ var rules = use('rules'); /*──── build board ───────────────────────────────────────────────────*/ var grid = new Grid(8, 8); -grid.width = 8; // (the ctor didn’t store them) +grid.width = 8; // (the ctor didn't store them) grid.height = 8; var mover = new MovementSystem(grid, rules); startingPos(grid); +/*──── networking and game state ─────────────────────────────────────*/ +var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected' +var isServer = false; +var opponent = null; +var myColor = null; // 'white' or 'black' +var isMyTurn = false; + +function updateTitle() { + var title = "Misty Chess - "; + + switch(gameState) { + case 'waiting': + title += "Press S to start server or J to join"; + break; + case 'searching': + title += "Searching for server..."; + break; + case 'server_waiting': + title += "Waiting for player to join..."; + break; + case 'connected': + if (myColor) { + title += (mover.turn === myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")"); + } else { + title += mover.turn + " turn"; + } + break; + } + + prosperon.window.title = title +} + +// Initialize title +updateTitle(); + /*──── mouse → click-to-move ─────────────────────────────────────────*/ var selectPos = null; -prosperon.window.on_mousedown = function (btn, mx, my) { - if (btn !== 0) return; +var hoverPos = null; +var holdingPiece = false; + +prosperon.on('mouse_button_down', function(e) { + if (e.which !== 0) return; + var mx = e.mouse.x; + var my = e.mouse.y; + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; if (!grid.inBounds(c)) return; - + var cell = grid.at(c); - if (selectPos && mover.tryMove(grid.at(selectPos)[0], c)) { - selectPos = null; // made a move - return; - } if (cell.length && cell[0].colour === mover.turn) { - selectPos = c; // pick up piece + selectPos = c; + holdingPiece = true; } else { selectPos = null; } -}; +}) + +prosperon.on('mouse_button_up', function(e) { + if (e.which !== 0 || !holdingPiece || !selectPos) return; + + var mx = e.mouse.x; + var my = e.mouse.y; + + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; + if (!grid.inBounds(c)) { + holdingPiece = false; + return; + } + + // Only allow moves if it's our turn or we're in local mode + if (gameState !== 'connected' || isMyTurn) { + if (mover.tryMove(grid.at(selectPos)[0], c)) { + // Send move to opponent if connected + if (gameState === 'connected' && opponent) { + $_.send(opponent, { + type: 'move', + from: selectPos, + to: c + }); + isMyTurn = false; // It's now opponent's turn + } + selectPos = null; + updateTitle(); + } + } + + holdingPiece = false; +}) + +prosperon.on('mouse_motion', function(e) { + var mx = e.pos.x; + var my = e.pos.y; + + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; + if (!grid.inBounds(c)) { + hoverPos = null; + return; + } + + hoverPos = c; +}) /*──── drawing helpers ───────────────────────────────────────────────*/ /* ── constants ─────────────────────────────────────────────────── */ var S = 60; // square size in px var light = [0.93,0.93,0.93,1]; var dark = [0.25,0.25,0.25,1]; +var hoverColor = [0.8, 1.0, 0.8, 1.0]; +var validMoveColor = [1.0, 0.8, 0.8, 1.0]; /* ── draw one 8×8 chess board ──────────────────────────────────── */ function drawBoard() { for (var y = 0; y < 8; ++y) for (var x = 0; x < 8; ++x) { + var isHovered = hoverPos && hoverPos[0] === x && hoverPos[1] === y; + var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); + + var color = ((x+y)&1) ? dark : light; + + if (isHovered && !isValidMove) { + color = hoverColor; + } else if (isValidMove) { + color = validMoveColor; + } + draw2d.rectangle( { x: x*S, y: y*S, width: S, height: S }, - { thickness: 0, color: ((x+y)&1) ? dark : light } + { thickness: 0, color: color } ); } } +function isValidMoveForTurn(from, to) { + if (!grid.inBounds(to)) return false; + + var piece = grid.at(from)[0]; + if (!piece) return false; + + // Check if the destination has a piece of the same color + var destCell = grid.at(to); + if (destCell.length && destCell[0].colour === piece.colour) { + return false; + } + + return rules.canMove(piece, from, to, grid); +} + /* ── draw every live piece ─────────────────────────────────────── */ function drawPieces() { grid.each(function (piece) { if (piece.captured) return; + + // Skip drawing the piece being held + if (holdingPiece && selectPos && + piece.coord[0] === selectPos[0] && + piece.coord[1] === selectPos[1]) { + return; + } var r = { x: piece.coord[0]*S, y: piece.coord[1]*S, width:S, height:S }; draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"}); }); -} - -/*──── main loop ─────────────────────────────────────────────────────*/ -var last = os.now(), fpsTimer = 0, fpsCount = 0; - -function loop() { - var now = os.now(), dt = now - last; last = now; - - render.clear([22/255, 120/255, 194/255, 1]); - render.camera(camera); - drawBoard(); - drawPieces(); - render.present(); - - fpsTimer += dt; fpsCount++; - if (fpsTimer >= 0.5) { - prosperon.window.title = - 'Chess demo – ' + mover.turn + '\'s move – FPS ' + (fpsCount / fpsTimer).toFixed(1); - fpsTimer = fpsCount = 0; + + // Draw the held piece at the mouse position if we're holding one + if (holdingPiece && selectPos && hoverPos) { + var piece = grid.at(selectPos)[0]; + if (piece) { + var r = { x: hoverPos[0]*S, y: hoverPos[1]*S, + width:S, height:S }; + + draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"}); + } } - $_.delay(loop, Math.max(0, (1 / 60) - (os.now() - now))); } -loop(); + +prosperon.on('draw', function() { + drawBoard() + drawPieces() +}) + +prosperon.on('key_down', function(e) { + // S key - start server + if (e.scancode === 22 && gameState === 'waiting') { // S key + startServer(); + } + // J key - join server + else if (e.scancode === 13 && gameState === 'waiting') { // J key + joinServer(); + } +}) + +function startServer() { + gameState = 'server_waiting'; + isServer = true; + myColor = 'white'; + isMyTurn = true; + updateTitle(); + + $_.portal(e => { + opponent = e; + gameState = 'connected'; + updateTitle(); + + // Tell the joining player they are black + $_.send(opponent, { + type: 'game_start', + your_color: 'black' + }); + }, 5678); +} + +function joinServer() { + gameState = 'searching'; + updateTitle(); + + function contact_fn(actor, reason) { + console.log("CONTACTED!") + if (actor) { + opponent = actor; + gameState = 'connected'; + myColor = 'black'; + isMyTurn = false; + updateTitle(); + } else { + console.log(`Failed to connect: ${json.encode(reason)}`); + gameState = 'waiting'; + updateTitle(); + } + } + + $_.contact(contact_fn, { + address: "localhost", + port: 5678 + }); +} var os = use('os') + + // Set up IO actor subscription var ioguy = { __ACTORDATA__: { id: os.ioactor() } -} +}; $_.send(ioguy, { type: "subscribe", actor: $_ -}) +}); $_.receiver(e => { - if (e.type === 'quit') - os.exit() - else - console.log(json.encode(e)) -}) + if (e.type === 'quit') os.exit() + if (e.type === 'game_start') { + myColor = e.your_color; + isMyTurn = (myColor === 'white'); + updateTitle(); + } else if (e.type === 'move') { + // Apply opponent's move + var fromCell = grid.at(e.from); + if (fromCell.length) { + var piece = fromCell[0]; + if (mover.tryMove(piece, e.to)) { + isMyTurn = true; // It's now our turn + updateTitle(); + } + } + } + + prosperon.dispatch(e.type, e) +}) \ No newline at end of file diff --git a/scripts/core/engine.js b/scripts/core/engine.js index 935d14c3..c071793a 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -1000,8 +1000,6 @@ function enet_check() $_.delay(enet_check, service_delay); } -console.log(`actor ${prosperon.id} online.`) - send_messages(); })() diff --git a/scripts/core/io.js b/scripts/core/io.js index b0cda680..66807baa 100644 --- a/scripts/core/io.js +++ b/scripts/core/io.js @@ -3,6 +3,11 @@ $_.unneeded(_ => { var subscribers = [] +var windows = [] +var renderers = [] + +var os = use('os') + $_.receiver(e => { if (e.type === "subscribe") { if (!e.actor) throw Error('Got a subscribe message with no actor.'); @@ -11,6 +16,150 @@ $_.receiver(e => { return; } + if (e.type === "window") { + var window = windows[e.id] + + switch (e.fn) { + case "create": + window = os.engine_start(e.config) + windows[e.id] = window + break; + + case "fullscreen": + window.fullscreen() + break; + + case "make_renderer": + var renderer = window.make_renderer(e.config || {}) + renderers[e.renderer_id] = renderer + break; + + case "keyboard_shown": + return window.keyboard_shown() + + case "theme": + return window.theme() + + case "safe_area": + return window.safe_area() + + case "bordered": + window.bordered(e.value) + break; + + case "set_icon": + window.set_icon(e.icon) + break; + + case "set_title": + window.title = e.title + break; + + case "get_title": + return window.title + + case "set_size": + window.size = e.size + break; + + case "get_size": + return window.size + + case "mouse_grab": + window.mouse_grab(e.value) + break; + } + } + + if (e.type === "render") { + var renderer = renderers[e.id] + + switch (e.fn) { + case "draw_color": + renderer.draw_color(e.color) + break; + + case "present": + renderer.present() + break; + + case "clear": + renderer.clear() + break; + + case "line": + renderer.line(e.config) + break; + + case "point": + renderer.point(e.config) + break; + + case "texture": + renderer.texture(e.texture, e.src_rect, e.dst_rect, e.angle, e.center) + break; + + case "rects": + renderer.rects(e.rects) + break; + + case "geometry": + renderer.geometry(e.vertices, e.indices) + break; + + case "geometry2": + renderer.geometry2(e.vertices, e.indices) + break; + + case "sprite": + renderer.sprite(e.config) + break; + + case "load_texture": + renderer.load_texture(e.path) + break; + + case "get_image": + renderer.get_image(e.config) + break; + + case "scale": + renderer.scale(e.scale) + break; + + case "logical_size": + renderer.logical_size(e.size) + break; + + case "viewport": + renderer.viewport(e.viewport) + break; + + case "clip": + renderer.clip(e.rect) + break; + + case "vsync": + renderer.vsync(e.enable) + break; + + case "coords": + renderer.coords(e.config) + break; + + case "camera": + renderer.camera(e.cam, e.layer) + break; + + case "screen2world": + return renderer.screen2world(e.point) + + case "target": + renderer.target(e.target) + break; + } + } + for (var a of subscribers) $_.send(a, e); }); diff --git a/scripts/modules/moth.js b/scripts/modules/moth.js index 558b335a..089b8deb 100644 --- a/scripts/modules/moth.js +++ b/scripts/modules/moth.js @@ -6,20 +6,15 @@ var os = use('os'); var io = use('io'); var render = use('render'); -var draw2d = use('draw2d'); var actor = use('actor'); var gameConfig = {}; var gameDir = ""; // Framework initialization -function initialize(dir) { - gameDir = dir; - - // Load configuration if it exists - var configPath = `${dir}/config.js`; - if (io.exists(configPath)) { +function initialize() { + var configPath = `config.js`; + if (io.exists(configPath)) gameConfig = use(configPath); - } // Set up default config values gameConfig.resolution = gameConfig.resolution || { width: 640, height: 480 }; @@ -52,42 +47,8 @@ function initialize(dir) { // Set window title prosperon.window.title = gameConfig.title; - - // Set up IO actor subscription - var ioguy = { - __ACTORDATA__: { - id: os.ioactor() - } - }; - - $_.send(ioguy, { - type: "subscribe", - actor: $_ - }); - - // Set up automatic receiver for input events - $_.receiver(function(e) { - // Handle quit - if (e.type === 'quit') { - os.exit(); - } - - // Forward all events to prosperon's dispatch system - prosperon.dispatch(e.type, e); - }); - - // Start the game loop + startGameLoop(); - - // Load the game's main.js - - var mainPath = `${dir}/main.js`; - if (io.exists(mainPath)) { - // Spawn the main game actor - actor.spawn(mainPath); - } else { - console.error(`No main.js found in ${dir}`); - } } // Main game loop