354 lines
8.9 KiB
Plaintext
354 lines
8.9 KiB
Plaintext
/**
|
||
* 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 input = use('input')
|
||
|
||
input.watch($_)
|
||
|
||
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 ];
|
||
}
|
||
|
||
function screenToWorldPoint([px, py], camera, winW, winH) {
|
||
// 1) undo pixel→NDC within the camera’s viewport
|
||
const ndcX = (px - camera.viewport.x * winW)
|
||
/ (camera.viewport.width * winW)
|
||
const ndcY = 1 - (py - camera.viewport.y * winH)
|
||
/ (camera.viewport.height * winH)
|
||
|
||
// 2) compute the 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]
|
||
|
||
// 3) map NDC back to world coords
|
||
return [
|
||
ndcX * camera.size[0] + worldX0,
|
||
ndcY * camera.size[1] + worldY0
|
||
]
|
||
}
|
||
|
||
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]}`)
|
||
|
||
log.spam('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) {
|
||
log.error(e.error)
|
||
os.exit(1)
|
||
}
|
||
|
||
window = e.id
|
||
|
||
send(video_actor,{
|
||
kind:"window",
|
||
op:"makeRenderer",
|
||
id:window
|
||
}, e => {
|
||
if (e.error) {
|
||
log.error(e.error)
|
||
os.exit(1)
|
||
}
|
||
|
||
render = e.id
|
||
graphics = use('graphics', video_actor, e.id)
|
||
})
|
||
})
|
||
|
||
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()
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
$_.receiver(e => {
|
||
if (e.type === 'quit')
|
||
$_.stop()
|
||
|
||
if (e.type.includes('mouse')) {
|
||
if (e.pos)
|
||
e.pos = screenToWorldPoint(e.pos, camera, 500, 500)
|
||
|
||
if (e.d_pos)
|
||
e.d_pos.y *= -1
|
||
}
|
||
|
||
send(gameactor, e)
|
||
}) |