/** * Moth Game Framework * Higher-level game development framework built on top of Prosperon */ var os = use('os'); var io = use('io'); var transform = use('transform'); var rasterize = use('rasterize'); var video_actor = use('sdl_video') var geometry = use('geometry') function worldToScreenRect({x,y,width,height}, camera, winW, winH) { var bl = worldToScreenPoint([x,y], camera, winW, winH) var tr = worldToScreenPoint([x+width, y+height], camera, winW, winH) return { x: Math.min(bl.x, tr.x), y: Math.min(bl.y, tr.y), width: Math.abs(tr.x - bl.x), height: Math.abs(tr.y - bl.y) } } function worldToScreenPoint([wx, wy], camera, winW, winH) { // 1) world‐window origin (bottom‐left) const worldX0 = camera.pos[0] - camera.size[0] * camera.anchor[0]; const worldY0 = camera.pos[1] - camera.size[1] * camera.anchor[1]; // 2) normalized device coords [0..1] const ndcX = (wx - worldX0) / camera.size[0]; const ndcY = (wy - worldY0) / camera.size[1]; // 3) map into pixel‐space via the fractional viewport const px = camera.viewport.x * winW + ndcX * (camera.viewport.width * winW); const py = camera.viewport.y * winH + (1 - ndcY) * (camera.viewport.height * winH); return [ px, py ]; } var camera = { size: [500,500],//{width:500,height:500}, // pixel size the camera "sees", like its resolution pos: [250,250],//{x:0,y:0}, // where it is fov:50, near_z:0, far_z:1000, viewport: {x:0,y:0,width:1,height:1}, // viewport it appears on screen ortho:true, anchor:[0.5,0.5],//{x:0.5,y:0.5}, surface: undefined } var util = use('util') var cammy = util.camera_globals(camera) var graphics var window var render var gameactor var dir = args[0] if (!io.exists(args[0] + '/main.js')) throw Error(`No main.js found in ${args[0]}`) console.log('starting game in ' + dir) io.mount(dir) $_.start(e => { if (gameactor) return gameactor = e.actor loop() }, args[0] + "/main.js") send(video_actor, { kind: "window", op:"create", data: { title: "Moth Test", width: 500, height: 500 } }, e => { if (e.error) { console.error(e.error) os.exit(1) } window = e.id send(video_actor,{ kind:"window", op:"makeRenderer", id:window }, e => { if (e.error) { console.error(e.error) os.exit(1) } render = e.id graphics = use('graphics', video_actor, e.id) console.log(`Created window and renderer id ${render}`) }) }) var last = os.now() // FPS tracking var fps_samples = [] var fps_sample_count = 60 var fps_sum = 0 var images = {} // Convert high-level draw commands to low-level renderer commands function translate_draw_commands(commands) { if (!graphics) return var renderer_commands = [] commands.forEach(function(cmd) { if (cmd.material && cmd.material.color) { renderer_commands.push({ op: "set", prop: "drawColor", value: cmd.material.color }) } switch(cmd.cmd) { case "draw_rect": cmd.rect = worldToScreenRect(cmd.rect, camera,500, 500) // Handle rectangles with optional rounding and thickness if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) { // Rounded rectangle var thickness = (cmd.opt.thickness === 0) ? 0 : (cmd.opt.thickness || 1) var raster_result = rasterize.round_rect(cmd.rect, cmd.opt.radius, thickness) if (raster_result.type === 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type === 'rects') { raster_result.data.forEach(function(rect) { renderer_commands.push({ op: "fillRect", data: {rect: rect} }) }) } } else if (cmd.opt && cmd.opt.thickness && cmd.opt.thickness > 0) { // Outlined rectangle var raster_result = rasterize.outline_rect(cmd.rect, cmd.opt.thickness) if (raster_result.type === 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type === 'rects') { renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } } else { renderer_commands.push({ op: "fillRect", data: {rect: cmd.rect} }) } break case "draw_circle": case "draw_ellipse": cmd.pos = worldToScreenPoint(cmd.pos, camera, 500, 500) // Rasterize ellipse to points or rects var radii = cmd.radii || [cmd.radius, cmd.radius] var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {}) if (raster_result.type === 'points') { renderer_commands.push({ op: "point", data: {points: raster_result.data} }) } else if (raster_result.type === 'rects') { // Use 'rects' operation for multiple rectangles renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } break case "draw_line": renderer_commands.push({ op: "line", data: {points: cmd.points.map(p => worldToScreenPoint(p, camera, 500, 500))} }) break case "draw_point": cmd.pos = worldToScreenPoint(cmd.pos, camera, 500, 500) renderer_commands.push({ op: "point", data: {points: [cmd.pos]} }) break case "draw_image": var img = graphics.texture(cmd.image) if (!img.gpu) break cmd.rect.width ??= img.width cmd.rect.height ??= img.height cmd.rect = worldToScreenRect(cmd.rect, camera, 500, 500) renderer_commands.push({ op: "texture", data: { texture_id: img.gpu.id, dst: cmd.rect, src: {x:0,y:0,width:img.width,height:img.height}, } }) break case "draw_text": if (!cmd.text) break if (!cmd.pos) break var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera, 500,500) var pos = {x: rect.x, y: rect.y} renderer_commands.push({ op: "debugText", data: { pos, text: cmd.text } }) break } }) return renderer_commands } function loop() { os.frame() var now = os.now() var dt = now - last last = now // Update the game send(gameactor, {kind:'update', dt:dt}, e => { // Get draw commands from game send(gameactor, {kind:'draw'}, draw_commands => { var batch_commands = [] batch_commands.push({ op: "set", prop: "drawColor", value: [0.1,0.1,0.15,1] }) // Clear the screen batch_commands.push({ op: "clear" }) if (draw_commands && draw_commands.length > 0) { var renderer_commands = translate_draw_commands(draw_commands) batch_commands = batch_commands.concat(renderer_commands) } batch_commands.push({ op: "present" }) send(video_actor, { kind: "renderer", id: render, op: "batch", data: batch_commands }, _ => { var diff = os.now() - now // Calculate and track FPS var frame_time = os.now() - last if (frame_time > 0) { var current_fps = 1 / frame_time // Add to samples fps_samples.push(current_fps) fps_sum += current_fps // Keep only the last N samples if (fps_samples.length > fps_sample_count) { fps_sum -= fps_samples.shift() } // Calculate average FPS var avg_fps = fps_sum / fps_samples.length } loop() }) }) }) }