Files
cell/prosperon/prosperon.ce
2025-06-23 22:58:25 -05:00

415 lines
10 KiB
Plaintext

var os = use('os');
var io = use('io');
var transform = use('transform');
var rasterize = use('rasterize');
var time = use('time')
var tilemap = use('tilemap')
// Frame timing variables
var frame_times = []
var frame_time_index = 0
var max_frame_samples = 60
var frame_start_time = 0
var average_frame_time = 0
var game = args[0]
var video
//var cnf = use('accio/config')
$_.start(e => {
if (e.type != 'greet') return
video = e.actor
graphics = use('graphics', video)
send(video, {kind:"window", op:"makeRenderer"}, e => {
$_.start(e => {
if (gameactor) return
gameactor = e.actor
$_.couple(gameactor)
start_pipeline()
}, args[0], $_)
})
}, 'prosperon/sdl_video', {})
var geometry = use('geometry')
function updateCameraMatrix(camera, winW, winH) {
// world→NDC
def sx = 1 / camera.size[0];
def sy = 1 / camera.size[1];
def ox = camera.pos[0] - camera.size[0] * camera.anchor[0];
def oy = camera.pos[1] - camera.size[1] * camera.anchor[1];
// NDC→pixels
def vx = camera.viewport.x * winW;
def vy = camera.viewport.y * winH;
def vw = camera.viewport.width * winW;
def vh = camera.viewport.height * winH;
// final “mat” coefficients
// [ a 0 c ]
// [ 0 e f ]
// [ 0 0 1 ]
camera.a = sx * vw;
camera.c = vx - camera.a * ox;
camera.e = -sy * vh;
camera.f = vy + vh + sy * vh * oy;
// and store the inverses so we can go back cheaply
camera.ia = 1 / camera.a;
camera.ic = -camera.c * camera.ia;
camera.ie = 1 / camera.e;
camera.if = -camera.f * camera.ie;
}
//---- forward transform ----
function worldToScreenPoint(pos, camera) {
return {
x: camera.a * pos[0] + camera.c,
y: camera.e * pos[1] + camera.f
};
}
//---- inverse transform ----
function screenToWorldPoint(pos, camera) {
return {
x: camera.ia * pos[0] + camera.ic,
y: camera.ie * pos[1] + camera.if
};
}
//---- rectangle (two corner) ----
function worldToScreenRect(rect, camera) {
// map bottom-left and top-right
def x1 = camera.a * rect.x + camera.c;
def y1 = camera.e * rect.y + camera.f;
def x2 = camera.a * (rect.x + rect.width) + camera.c;
def y2 = camera.e * (rect.y + rect.height) + camera.f;
// pick mins and abs deltas
def x0 = x1 < x2 ? x1 : x2;
def y0 = y1 < y2 ? y1 : y2;
return {
x: x0,
y: y0,
width: x2 > x1 ? x2 - x1 : x1 - x2,
height: y2 > y1 ? y2 - y1 : y1 - y2
};
}
var camera = {
size: [640,480],//{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},
rotation:[0,0,0,1],
surface: null
}
var util = use('util')
var cammy = util.camera_globals(camera)
var graphics
var gameactor
var images = {}
var renderer_commands = []
// Convert high-level draw commands to low-level renderer commands
function translate_draw_commands(commands) {
if (!graphics) return
updateCameraMatrix(camera,500,500)
renderer_commands.length = 0
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)
// 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)
// 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))}
})
break
case "draw_point":
cmd.pos = worldToScreenPoint(cmd.pos, camera)
renderer_commands.push({
op: "point",
data: {points: [cmd.pos]}
})
break
case "draw_image":
var img = graphics.texture(cmd.image)
var gpu = img.gpu
if (!gpu) break
cmd.rect.width ??= img.width
cmd.rect.height ??= img.height
cmd.rect = worldToScreenRect(cmd.rect, camera)
renderer_commands.push({
op: "texture",
data: {
texture_id: gpu.id,
dst: cmd.rect,
src: img.rect
}
})
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
case "tilemap":
var texid
tilemap.for(cmd.tilemap, (tile,{x,y}) => {
if (!texid) texid = graphics.texture(tile)
return graphics.texture(tile)
})
var geom = geometry.tilemap_to_data(cmd.tilemap)
if (!texid) break
if (texid.gpu)
geom.texture_id = texid.gpu.id
renderer_commands.push({
op: "geometry_raw",
data: geom
})
break
}
})
return renderer_commands
}
var parseq = use('parseq', $_.delay)
// Wrap `send(actor,msg,cb)` into a parseq “requestor”
// • on success: cb(data) → value=data, reason=null
// • on failure: cb(null,err)
function rpc_req(actor, msg) {
return (cb, _) => {
send(actor, msg, data => {
if (data.error)
cb(null, data)
else
cb(data)
})
}
}
var game_rec = parseq.sequence([
rpc_req(gameactor, {kind:'update', dt:1/60}),
rpc_req(gameactor, {kind:'draw'})
])
var pending_draw = null
var pending_next = null
var last_time = time.number()
var frames = []
var frame_avg = 0
var input = use('input')
var input_state = {
poll: 1/60
}
// 1) input runs completely independently
function poll_input() {
send(video, {kind:'input', op:'get'}, evs => {
for (var ev of evs) {
if (ev.type == 'quit')
$_.stop()
if (ev.type.includes('mouse')) {
if (ev.pos)
ev.pos = screenToWorldPoint(ev.pos, camera, 500,500)
if (ev.d_pos)
ev.d_pos.y *= -1
}
if (ev.type.includes('key')) {
if (ev.key)
ev.key = input.keyname(ev.key)
}
}
send(gameactor, evs)
})
$_.delay(poll_input, input_state.poll)
}
// 2) helper to build & send a batch, then call done()
function create_batch(draw_cmds, done) {
def batch = [
{op:'set', prop:'drawColor', value:[0.1,0.1,0.15,1]},
{op:'clear'}
]
if (draw_cmds && draw_cmds.length)
batch.push(...translate_draw_commands(draw_cmds))
batch.push(
{op:'set', prop:'drawColor', value:[1,1,1,1]},
{op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}},
{op:'present'}
)
send(video, {kind:'renderer', op:'batch', data:batch}, () => {
def now = time.number()
def dt = now - last_time
last_time = now
frames.push(dt)
if (frames.length > 60) frames.shift()
let sum = 0
for (let f of frames) sum += f
frame_avg = sum / frames.length
done(dt)
})
}
// 3) kick off the very first update→draw
function start_pipeline() {
poll_input()
send(gameactor, {kind:'update', dt:1/60}, () => {
send(gameactor, {kind:'draw'}, cmds => {
pending_draw = cmds
render_step()
})
})
}
function render_step() {
// a) fire off the next update→draw immediately
def dt = time.number() - last_time
send(gameactor, {kind:'update', dt:1/60}, () =>
send(gameactor, {kind:'draw'}, cmds => pending_next = cmds)
)
// c) render the current frame
create_batch(pending_draw, ttr => { // time to render
// only swap in when there's a new set of commands
if (pending_next) {
pending_draw = pending_next
pending_next = null
}
// d) schedule the next render step
def render_dur = time.number() - last_time
def wait = Math.max(0, 1/60 - ttr)
$_.delay(render_step, 0)
})
}
$_.receiver(e => {
switch(e.op) {
case 'resolution':
log.console(json.encode(e))
send(video, {
kind:'renderer',
op:'set',
prop:'logicalPresentation',
value: {...e}
})
break
}
})