prosperon/source/scripts/engine.js
2023-05-03 18:50:37 +00:00

1770 lines
37 KiB
JavaScript

"use strict";
var Log = {
set level(x) { cmd(92,x); },
get level() { return cmd(93); },
print(msg, lvl) {
var lg;
if (typeof msg === 'object') {
lg = JSON.stringify(msg, null, 2);
} else {
lg = msg;
}
var stack = (new Error()).stack;
var n = stack.next('\n',0)+1;
n = stack.next('\n', n)+1;
var nnn = stack.slice(n);
var fmatch = nnn.match(/\(.*\:/);
var file = fmatch ? fmatch[0].shift(1).shift(-1) : "nofile";
var lmatch = nnn.match(/\:\d*\)/);
var line = lmatch ? lmatch[0].shift(1).shift(-1) : "0";
yughlog(lvl, lg, file, line);
},
info(msg) {
this.print(msg, 0);
},
warn(msg) {
this.print(msg, 1);
},
error(msg) {
this.print(msg, 2);
this.stack(1);
},
critical(msg) {
this.print(msg,3);
this.stack(1);
},
write(msg) {
cmd(91,msg);
},
stack(skip = 0) {
Log.warn("Printing stack");
var stack = (new Error()).stack;
var n = stack.next('\n',0)+1;
for (var i = 0; i < skip; i++)
n = stack.next('\n', n)+1;
this.write(stack.slice(n));
},
};
var files = {};
function load(file) {
var modtime = cmd(0, file);
if (modtime === 0) {
Log.stack();
return false;
}
files[file] = modtime;
}
load("scripts/base.js");
load("scripts/diff.js");
load("scripts/debug.js");
function win_icon(str) {
cmd(90, str);
};
function sim_start() {
sys_cmd(1);
/*
Game.objects.forEach(function(x) {
if (x.start) x.start(); });
Level.levels.forEach(function(lvl) {
lvl.run();
});
*/
}
function sim_stop() { sys_cmd(2); }
function sim_pause() { sys_cmd(3); }
function sim_step() { sys_cmd(4); }
function sim_playing() { return sys_cmd(5); }
function sim_paused() { return sys_cmd(6); }
function phys_stepping() { return cmd(79); }
function quit() { sys_cmd(0); };
function set_cam(id) {
cmd(61, id);
};
var Window = {
get width() {
return cmd(48);
},
get height() {
return cmd(49);
},
get dimensions() {
return [this.width, this.height];
}
};
var Color = {
white: [255,255,255],
blue: [84,110,255],
green: [120,255,10],
yellow: [251,255,43],
red: [255,36,20],
teal: [96, 252, 237],
gray: [181, 181,181],
};
var GUI = {
text(str, pos, size, color, wrap) {
size = size ? size : 1;
color = color ? color : [255,255,255];
wrap = wrap ? wrap : 500;
var h = ui_text(str, pos, size, color, wrap);
return [wrap,h];
},
text_cursor(str, pos, size, cursor) {
cursor_text(str,pos,size,[255,255,255],cursor);
},
};
function listbox(pos, item) {
pos.y += (item[1] - 20);
};
var Yugine = {
get dt() {
return cmd(63);
},
wait_fns: [],
wait_exec(fn) {
if (!phys_stepping())
fn();
else
this.wait_fns.push(fn);
},
exec() {
this.wait_fns.forEach(function(x) { x(); });
this.wait_fns = [];
},
};
var timer = {
make(fn, secs,obj,loop) {
if (secs === 0) {
fn.call(obj);
return;
}
var t = clone(this);
t.id = make_timer(fn, secs, obj);
return t;
},
oneshot(fn, secs,obj) {
var t = this.make(() => {
fn.call();
t.kill();
},secs);
t.loop = 0;
t.start();
},
get remain() { return cmd(32, this.id); },
get on() { return cmd(33, this.id); },
get loop() { return cmd(34, this.id); },
set loop(x) { cmd(35, this.id, x); },
start() { cmd(26, this.id); },
stop() { cmd(25, this.id); },
pause() { cmd(24, this.id); },
kill() { cmd(27, this.id); },
set time(x) { cmd(28, this.id, x); },
get time() { return cmd(29, this.id); },
};
var animation = {
time: 0,
loop: false,
playtime: 0,
playing: false,
keyframes: [],
create() {
var anim = Object.create(animation);
register_update(anim.update, anim);
return anim;
},
start() {
this.playing = true;
this.time = this.keyframes.last[1];
this.playtime = 0;
},
interval(a, b, t) {
return (t - a) / (b - a);
},
near_val(t) {
for (var i = 0; i < this.keyframes.length-1; i++) {
if (t > this.keyframes[i+1][1]) continue;
return this.interval(this.keyframes[i][1], this.keyframes[i+1][1], t) >= 0.5 ? this.keyframes[i+1][0] : this.keyframes[i][0];
}
return this.keyframes.last[0];
},
lerp_val(t) {
for (var i = 0; i < this.keyframes.length-1; i++) {
if (t > this.keyframes[i+1][1]) continue;
var intv = this.interval(this.keyframes[i][1], this.keyframes[i+1][1], t);
return ((1 - intv) * this.keyframes[i][0]) + (intv * this.keyframes[i+1][0]);
}
return this.keyframes.last[0];
},
cubic_val(t) {
},
mirror() {
if (this.keyframes.length <= 1) return;
for (var i = this.keyframes.length-1; i >= 1; i--) {
this.keyframes.push(this.keyframes[i-1]);
this.keyframes.last[1] = this.keyframes[i][1] + (this.keyframes[i][1] - this.keyframes[i-1][1]);
}
},
update(dt) {
if (!this.playing) return;
this.playtime += dt;
if (this.playtime >= this.time) {
if (this.loop)
this.playtime = 0;
else {
this.playing = false;
return;
}
}
this.fn(this.lerp_val(this.playtime));
},
};
var sound = {
play() {
this.id = cmd(14,this.path);
},
stop() {
},
};
var Music = {
play(path) {
Log.info("Playing " + path);
cmd(87,path);
},
stop() {
cmd(89);
},
pause() {
cmd(88);
},
set volume(x) {
},
};
var Sound = {
play(file) {
var s = Object.create(sound);
s.path = file;
s.play();
return s;
},
music(midi, sf) {
cmd(13, midi, sf);
},
musicstop() {
cmd(15);
},
/* Between 0 and 100 */
set volume(x) { cmd(19, x); },
killall() {
Music.stop();
this.musicstop();
/* TODO: Kill all sound effects that may still be running */
},
};
var Render = {
normal() {
cmd(67);
},
wireframe() {
cmd(68);
},
};
var Mouse = {
get pos() {
return cmd(45);
},
get worldpos() {
return screen2world(cmd(45));
},
disabled() {
cmd(46, 212995);
},
hidden() {
cmd(46, 212994);
},
normal() {
cmd(46, 212993);
},
};
var Keys = {
shift() {
return cmd(50, 340) || cmd(50, 344);
},
ctrl() {
return cmd(50, 341) || cmd(50, 344);
},
alt() {
return cmd(50, 342) || cmd(50, 346);
},
};
var Input = {
setgame() { cmd(77); },
setnuke() { cmd(78); },
};
function screen2world(screenpos) { return editor.camera.view2world(screenpos); }
function world2screen(worldpos) { return editor.camera.world2view(worldpos); }
var physics = {
set gravity(x) { cmd(8, x); },
get gravity() { return cmd(72); },
set damping(x) { cmd(73,Math.clamp(x,0,1)); },
get damping() { return cmd(74); },
pos_query(pos) {
return cmd(44, pos);
},
/* Returns a list of body ids that a box collides with */
box_query(box) {
var pts = cmd(52,box.pos,box.wh);
return cmd(52, box.pos, box.wh);
},
box_point_query(box, points) {
if (!box || !points)
return [];
return cmd(86, box.pos, box.wh, points, points.length);
},
};
var Action = {
add_new(name) {
var action = Object.create(Action);
action.name = name;
action.inputs = [];
this.actions.push(action);
return action;
},
actions: [],
};
/* May be a human player; may be an AI player */
var Player = {
players: [],
input(fn, ...args) {
this.pawns.forEach(x => x[fn]?.(...args));
},
control(pawn) {
this.pawns.push_unique(pawn);
},
uncontrol(pawn) {
this.pawns = this.pawns.filter(x => x !== pawn);
},
};
for (var i = 0; i < 4; i++) {
var player1 = Object.create(Player);
player1.pawns = [];
player1.gamepads = [];
Player.players.push(player1);
}
function state2str(state) {
if (typeof state === 'string') return state;
switch (state) {
case 0:
return "down";
case 1:
return "pressed";
case 2:
return "released";
}
}
var Register = {
updates: [],
update(dt) {
this.updates.forEach(x => x[0].call(x[1], dt));
},
physupdates: [],
physupdate(dt) {
this.physupdates.forEach(x => x[0].call(x[1], dt));
},
guis: [],
gui() {
this.guis.forEach(x => x[0].call(x[1]));
},
nk_guis: [],
nk_gui() {
this.nk_guis.forEach(x => x[0].call(x[1]));
},
kbm_input(src, btn, state, ...args) {
var input = `${src}_${btn}_${state}`;
Player.players[0].input(input, ...args);
},
gamepad_playermap: [],
gamepad_input(pad, btn, state, ...args) {
var player = this.gamepad_playermap[pad];
if (!player) return;
var statestr = state2str(state);
var rawfn = `gamepad_${btn}_${statestr}`;
player.input(rawfn, ...args);
Action.actions.forEach(x => {
if (x.inputs.includes(btn))
player.input(`action_${x.name}_${statestr}`, ...args);
});
},
debugs: [],
debug() {
this.debugs.forEach(x => x[0].call(x[1]));
},
unregister_obj(obj) {
this.updates = this.updates.filter(x => x[1] !== obj);
this.guis = this.guis.filter(x => x[1] !== obj);
this.nk_guis = this.nk_guis.filter(x => x[1] !== obj);
this.debugs = this.debugs.filter(x => x[1] !== obj);
this.physupdates = this.physupdates.filter(x => x[1] !== obj);
Player.players.forEach(x => x.uncontrol(obj));
},
};
Register.unregister_obj(null);
register(0, Register.update, Register);
register(1, Register.physupdate, Register);
register(2, Register.gui, Register);
register(3, Register.nk_gui, Register);
register(6, Register.debug, Register);
register(7, Register.kbm_input, Register);
register(8, Register.gamepad_input, Register);
register(9, Log.stack, this);
Register.gamepad_playermap[0] = Player.players[0];
function register_update(fn, obj) {
Register.updates.push([fn, obj ? obj : null]);
};
function register_physupdate(fn, obj) {
Register.physupdates.push([fn, obj ? obj : null]);
};
function register_gui(fn, obj) {
Register.guis.push([fn, obj ? obj : this]);
};
function register_debug(fn, obj) {
Register.debugs.push([fn, obj ? obj : this]);
};
function unregister_gui(fn, obj) {
Register.guis = Register.guis.filter(x => x[0] !== fn && x[1] !== obj);
};
function register_nk_gui(fn, obj) {
Register.nk_guis.push([fn, obj ? obj : this]);
};
function unregister_nk_gui(fn, obj) {
Register.nk_guis = Register.nk_guis.filter(x => x[0] !== fn && x[1] !== obj);
};
register_update(Yugine.exec, Yugine);
/* These functions are the "defaults", and give control to player0 */
function set_pawn(obj, player = Player.players[0]) {
player.control(obj);
}
function unset_pawn(obj, player = Player.players[0]) {
player.uncontrol(obj);
}
var Signal = {
signals: [],
obj_begin(fn, obj, go) {
this.signals.push([fn, obj]);
register_collide(0, fn, obj, go.body);
},
obj_separate(fn, obj, go) {
this.signals.push([fn,obj]);
register_collide(3,fn,obj,go.body);
},
clear_obj(obj) {
this.signals.filter(function(x) { return x[1] !== obj; });
},
};
var IO = {
exists(file) { return cmd(65, file);},
slurp(file) {
if (!this.exists(file)) {
Log.warn(`File ${file} does not exist; can't slurp.`);
return "";
}
return cmd(38,file);
},
slurpwrite(str, file) { return cmd(39, str, file); },
extensions(ext) { return cmd(66, "." + ext); },
};
function slurp(file) { return IO.slurp(file);}
function slurpwrite(str, file) { return IO.slurpwrite(str, file); }
function reloadfiles() {
Object.keys(files).forEach(function (x) { load(x); });
}
function Color(from) {
var color = Object.create(Array);
Object.defineProperty(color, 'r', setelem(0));
Object.defineProperty(color, 'g', setelem(1));
Object.defineProperty(color, 'b', setelem(2));
Object.defineProperty(color, 'a', setelem(3));
color.a = color.g = color.b = color.a = 1;
Object.assign(color, from);
return color;
};
load("scripts/components.js");
function replacer_empty_nil(key, val) {
if (typeof val === 'object' && JSON.stringify(val) === '{}')
return undefined;
// if (typeof val === 'number')
// return parseFloat(val.toFixed(4));
return val;
};
function clean_object(obj) {
Object.keys(obj).forEach(function(x) {
if (!(x in obj.__proto__)) return;
switch(typeof obj[x]) {
case 'object':
if (Array.isArray(obj[x])) {
if (obj[x].equal(obj.__proto__[x])) {
delete obj[x];
}
} else
clean_object(obj[x]);
break;
case 'function':
return;
default:
if (obj[x] === obj.__proto__[x])
delete obj[x];
break;
}
});
};
function find_com(objects)
{
if (!objects || objects.length === 0)
return [0,0];
var com = [0,0];
com[0] = objects.reduce(function(acc, val) {
return acc + val.pos[0];
}, 0);
com[0] /= objects.length;
com[1] = objects.reduce(function(acc, val) {
return acc + val.pos[1];
}, 0);
com[1] /= objects.length;
return com;
};
var Game = {
objects: [],
resolution: [1200,720],
name: "Untitled",
register_obj(obj) {
this.objects[obj.body] = obj;
},
/* Returns an object given an id */
object(id) {
return this.objects[id];
},
/* Returns a list of objects by name */
find(name) {
},
/* Return a list of objects derived from a specific prototype */
find_proto(proto) {
},
/* List of all objects spawned that have a specific tag */
find_tag(tag) {
},
groupify(objects, spec) {
var newgroup = {
locked: true,
breakable: true,
objs: objects,
// get pos() { return find_com(objects); },
// set pos(x) { this.objs.forEach(function(obj) { obj.pos = x; }) },
};
Object.assign(newgroup, spec);
objects.forEach(function(x) {
x.defn('group', newgroup);
});
var bb = bb_from_objects(newgroup.objs);
newgroup.startbb = bb2cwh(bb);
newgroup.bboffset = newgroup.startbb.c.sub(newgroup.objs[0].pos);
newgroup.boundingbox = function() {
newgroup.startbb.c = newgroup.objs[0].pos.add(newgroup.bboffset);
return cwh2bb(newgroup.startbb.c, newgroup.startbb.wh);
};
if (newgroup.file)
newgroup.color = [120,255,10];
return newgroup;
},
};
var Level = {
levels: [],
objects: [],
alive: true,
selectable: true,
toString() {
if (this.file)
return this.file;
return "Loose level";
},
get boundingbox() {
return bb_from_objects(this.objects);
},
varname2obj(varname) {
for (var i = 0; i < this.objects.length; i++)
if (this.objects[i].varname === varname)
return this.objects[i];
return null;
},
run() {
var objs = this.objects.slice();
var scene = {};
var self = this;
objs.forEach(function(x) {
if (x.hasOwn('varname')) {
scene[x.varname] = x;
this[x.varname] = x;
}
},this);
eval(this.script);
if (typeof extern === 'object')
Object.assign(this, extern);
if (typeof update === 'function')
register_update(update, this);
if (typeof gui === 'function')
register_gui(gui, this);
if (typeof nk_gui === 'function')
register_nk_gui(nk_gui, this);
},
revert() {
delete this.unique;
this.load(this.filelvl);
},
/* Returns how many objects this level created are still alive */
object_count() {
return objects.length();
},
/* Save a list of objects into file, with pos acting as the relative placement */
saveas(objects, file, pos) {
if (!pos) pos = find_com(objects);
objects.forEach(function(obj) {
obj.pos = obj.pos.sub(pos);
});
var newlvl = Level.create();
objects.forEach(function(x) { newlvl.register(x); });
var save = newlvl.save();
slurpwrite(save, file);
},
clean() {
for (var key in this.objects)
clean_object(this.objects[key]);
for (var key in gameobjects)
clean_object(gameobjects[key]);
},
sync_file(file) {
var openlvls = this.levels.filter(function(x) { return x.file === file && x !== editor.edit_level; });
openlvls.forEach(function(x) {
x.clear();
x.load(IO.slurp(x.file));
x.flipdirty = true;
x.sync();
x.flipdirty = false;
x.check_dirty();
});
},
save() {
this.clean();
var pos = this.pos;
var angle = this.angle;
this.pos = [0,0];
this.angle = 0;
if (this.flipx) {
this.objects.forEach(function(obj) {
this.mirror_x_obj(obj);
}, this);
}
if (this.flipy) {
this.objects.forEach(function(obj) {
this.mirror_y_obj(obj);
}, this);
}
var savereturn = JSON.stringify(this.objects, replacer_empty_nil, 1);
if (this.flipx) {
this.objects.forEach(function(obj) {
this.mirror_x_obj(obj);
}, this);
}
if (this.flipy) {
this.objects.forEach(function(obj) {
this.mirror_y_obj(obj);
}, this);
}
this.pos = pos;
this.angle = angle;
return savereturn;
},
mirror_x_obj(obj) {
obj.flipx = !obj.flipx;
var rp = obj.relpos;
obj.pos = [-rp.x, rp.y].add(this.pos);
obj.angle = -obj.angle;
},
mirror_y_obj(obj) {
var rp = obj.relpos;
obj.pos = [rp.x, -rp.y].add(this.pos);
obj.angle = -obj.angle;
},
toJSON() {
var obj = {};
obj.file = this.file;
obj.pos = this._pos;
obj.angle = this._angle;
obj.from = "group";
obj.flipx = this.flipx;
obj.flipy = this.flipy;
obj.scale = this.scale;
if (this.varname)
obj.varname = this.varname;
if (!this.unique)
return obj;
obj.objects = {};
this.objects.forEach(function(x,i) {
obj.objects[i] = {};
var adiff = Math.abs(x.relangle - this.filelvl[i]._angle) > 1e-5;
if (adiff)
obj.objects[i].angle = x.relangle;
var pdiff = Vector.equal(x.relpos, this.filelvl[i]._pos, 1e-5);
if (!pdiff)
obj.objects[i].pos = x._pos.sub(this.pos);
if (obj.objects[i].empty)
delete obj.objects[i];
}, this);
return obj;
},
register(obj) {
if (obj.level)
obj.level.unregister(obj);
this.objects.push(obj);
},
make() {
return Level.loadfile(this.file, this.pos);
},
spawn(prefab) {
var newobj = prefab.make();
newobj.defn('level', this);
this.objects.push(newobj);
Game.register_obj(newobj);
newobj.setup?.();
newobj.start?.();
return newobj;
},
create() {
var newlevel = Object.create(this);
newlevel.objects = [];
newlevel._pos = [0,0];
newlevel._angle = 0;
newlevel.color = Color.green;
newlevel.toString = function() {
return (newlevel.unique ? "#" : "") + newlevel.file;
};
newlevel.filejson = newlevel.save();
return newlevel;
},
addfile(file) {
var lvl = this.loadfile(file);
this.objects.push(lvl);
lvl.level = this;
return lvl;
},
check_dirty() {
this.dirty = this.save() !== this.filejson;
},
start() {
this.objects.forEach(function(x) { if ('start' in x) x.start(); });
},
loadlevel(file) {
var lvl = Level.loadfile(file);
if (lvl && sim_playing())
lvl.start();
return lvl;
},
loadfile(file) {
if (!file.endsWith(".lvl")) file = file + ".lvl";
var newlevel = Level.create();
if (IO.exists(file)) {
newlevel.filejson = IO.slurp(file);
try {
newlevel.filelvl = JSON.parse(newlevel.filejson);
newlevel.load(newlevel.filelvl);
} catch (e) {
newlevel.ed_gizmo = function() { GUI.text("Invalid level file: " + newlevel.file, world2screen(newlevel.pos), 1, Color.red); };
newlevel.selectable = false;
}
newlevel.file = file;
newlevel.dirty = false;
}
var scriptfile = file.replace('.lvl', '.js');
if (IO.exists(scriptfile))
newlevel.script = IO.slurp(scriptfile);
newlevel.run();
return newlevel;
},
/* Spawns all objects specified in the lvl json object */
load(lvl) {
this.clear();
this.levels.push_unique(this);
if (!lvl) {
Log.warn("Level is " + lvl + ". Need a better formed one.");
return;
}
var opos = this.pos;
var oangle = this.angle;
this.pos = [0,0];
this.angle = 0;
var objs;
var created = [];
if (typeof lvl === 'string')
objs = JSON.parse(lvl);
else
objs = lvl;
if (typeof objs === 'object')
objs = objs.array();
objs.forEach(function(x) {
if (x.from === 'group') {
var loadedlevel = Level.loadfile(x.file);
if (!loadedlevel) {
Log.error("Error loading level: file " + x.file + " not found.");
return;
}
if (!IO.exists(x.file)) {
loadedlevel.ed_gizmo = function() { GUI.text("MISSING LEVEL " + x.file, world2screen(loadedlevel.pos) ,1, Color.red) };
}
var objs = x.objects;
delete x.objects;
Object.assign(loadedlevel, x);
if (objs) {
objs.array().forEach(function(x, i) {
if (x.pos)
loadedlevel.objects[i].pos = x.pos.add(loadedlevel.pos);
if (x.angle)
loadedlevel.objects[i].angle = x.angle + loadedlevel.angle;
});
loadedlevel.unique = true;
}
loadedlevel.level = this;
loadedlevel.sync();
created.push(loadedlevel);
this.objects.push(loadedlevel);
return;
}
var newobj = this.spawn(gameobjects[x.from]);
dainty_assign(newobj, x);
if (x._pos)
newobj.pos = x._pos;
if (x._angle)
newobj.angle = x._angle;
for (var key in newobj.components)
if ('sync' in newobj.components[key]) newobj.components[key].sync();
newobj.sync();
created.push(newobj);
}, this);
created.forEach(function(x) {
if (x.varname)
this[x.varname] = x;
},this);
this.pos = opos;
this.angle = oangle;
return created;
},
clear() {
for (var i = this.objects.length-1; i >= 0; i--)
if (this.objects[i].alive)
this.objects[i].kill();
this.levels.remove(this);
},
clear_all() {
this.levels.forEach(function(x) { x.kill(); });
},
kill() {
if (this.level)
this.level.unregister(this);
Register.unregister_obj(this);
this.clear();
},
unregister(obj) {
var removed = this.objects.remove(obj);
if (removed && obj.varname)
delete this[obj.varname];
},
get pos() { return this._pos; },
set pos(x) {
var diff = x.sub(this._pos);
this.objects.forEach(function(x) { x.pos = x.pos.add(diff); });
this._pos = x;
},
get angle() { return this._angle; },
set angle(x) {
var diff = x - this._angle;
this.objects.forEach(function(x) {
x.angle = x.angle + diff;
var pos = x.pos.sub(this.pos);
var r = Vector.length(pos);
var p = Math.rad2deg(Math.atan2(pos.y, pos.x));
p += diff;
p = Math.deg2rad(p);
x.pos = this.pos.add([r*Math.cos(p), r*Math.sin(p)]);
},this);
this._angle = x;
},
flipdirty: false,
sync() {
this.flipx = this.flipx;
this.flipy = this.flipy;
},
_flipx: false,
get flipx() { return this._flipx; },
set flipx(x) {
if (this._flipx === x && (!x || !this.flipdirty)) return;
this._flipx = x;
this.objects.forEach(function(obj) {
obj.flipx = !obj.flipx;
var rp = obj.relpos;
obj.pos = [-rp.x, rp.y].add(this.pos);
obj.angle = -obj.angle;
},this);
},
_flipy: false,
get flipy() { return this._flipy; },
set flipy(x) {
if (this._flipy === x && (!x || !this.flipdirty)) return;
this._flipy = x;
this.objects.forEach(function(obj) {
var rp = obj.relpos;
obj.pos = [rp.x, -rp.y].add(this.pos);
obj.angle = -obj.angle;
},this);
},
_scale: 1.0,
get scale() { return this._scale; },
set scale(x) {
var diff = (x - this._scale) + 1;
this._scale = x;
this.objects.forEach(function(obj) {
obj.scale *= diff;
obj.relpos = obj.relpos.scale(diff);
}, this);
},
get up() {
return [0,1].rotate(Math.deg2rad(this.angle));
},
get down() {
return [0,-1].rotate(Math.deg2rad(this.angle));
},
get right() {
return [1,0].rotate(Math.deg2rad(this.angle));
},
get left() {
return [-1,0].rotate(Math.deg2rad(this.angle));
},
};
var World = Level.create();
World.name = "World";
var gameobjects = {};
/* Returns the index of the smallest element in array, defined by a function that returns a number */
Object.defineProperty(Array.prototype, 'min', {
value: function(fn) {
},
});
function grab_from_points(pos, points, slop) {
var shortest = slop;
var idx = -1;
points.forEach(function(x,i) {
if (Vector.length(pos.sub(x)) < shortest) {
shortest = Vector.length(pos.sub(x));
idx = i;
}
});
return idx;
};
var gameobject = {
get scale() { return this._scale; },
set scale(x) {
this._scale = Math.max(0,x);
if (this.body > -1)
cmd(36, this.body, this._scale);
this.sync();
},
_scale: 1.0,
save: true,
selectable: true,
layer: 0, /* Collision layer; should probably have been called "mask" */
layer_nuke() {
Nuke.label("Collision layer");
Nuke.newline(Collision.num);
for (var i = 0; i < Collision.num; i++)
this.layer = Nuke.radio(i, this.layer, i);
},
_draw_layer: 1,
set draw_layer(x) {
if (x < 0) x = 0;
if (x > 4) x = 4;
this._draw_layer = x;
},
_draw_layer_nuke() {
Nuke.label("Draw layer");
Nuke.newline(5);
for (var i = 0; i < 5; i++)
this.draw_layer = Nuke.radio(i, this.draw_layer, i);
},
in_air() {
return q_body(7, this.body);
},
get draw_layer() { return this._draw_layer; },
name: "gameobject",
toString() { return this.name; },
clone(name, ext) {
var obj = Object.create(this);
complete_assign(obj, ext);
gameobjects[name] = obj;
obj.defc('name', name);
obj.from = this.name;
obj.defn('instances', []);
return obj;
},
ed_locked: false,
_visible: true,
get visible(){ return this._visible; },
set visible(x) {
this._visible = x;
for (var key in this.components) {
if ('visible' in this.components[key]) {
this.components[key].visible = x;
}
}
},
_mass: 1,
set mass(x) { this._mass = Math.max(0,x); },
get mass() { return this._mass; },
bodytype: {
dynamic: 0,
kinematic: 1,
static: 2
},
get moi() { return q_body(6, this.body); },
phys: 2,
phys_nuke() {
Nuke.newline(1);
Nuke.label("phys");
Nuke.newline(3);
this.phys = Nuke.radio("dynamic", this.phys, 0);
this.phys = Nuke.radio("kinematic", this.phys, 1);
this.phys = Nuke.radio("static", this.phys, 2);
},
_friction: 0,
set friction(x) { this._friction = Math.max(0,x); },
get friction() { return this._friction; },
_elasticity: 0,
set elasticity(x) { this._elasticity = Math.max(0, x); },
get elasticity() { return this._elasticity; },
_flipx: false,
_flipy: false,
get flipx() { return this._flipx; },
set flipx(x) { this._flipx = x; if (this.alive) cmd(55, this.body, x); this.sync(); },
get flipy() { return this._flipy; },
set flipy(x) { this._flipy = x; if (this.alive) cmd(56, this.body, x); this.sync(); },
body: -1,
controlled: false,
get properties() {
var keys = [];
for (var key of Object.keys(this)) {
if (key.startsWith("_"))
keys.push(key.substring(1));
else
keys.push(key);
}
return keys;
},
toJSON() {
var obj = {};
for (var key of this.properties)
obj[key] = this[key];
return obj;
},
set_center(pos) {
var change = pos.sub(this.pos);
this.pos = pos;
for (var key in this.components) {
this.components[key].finish_center(change);
}
},
varname: "",
_pos: [0,0],
set pos(x) { this._pos = x; set_body(2, this.body, x); this.sync(); },
get pos() {
if (this.body !== -1)
return q_body(1, this.body);
else
return this._pos;
},
set relpos(x) {
if (!this.level) {
this.pos = x;
return;
}
this.pos = Vector.rotate(x, Math.deg2rad(this.level.angle)).add(this.level.pos);
},
get relpos() {
if (!this.level) return this.pos;
var offset = this.pos.sub(this.level.pos);
return Vector.rotate(offset, -Math.deg2rad(this.level.angle));
},
_angle: 0,
set angle(x) { this._angle = x; set_body(0, this.body, Math.deg2rad(x)); this.sync(); },
get angle() {
if (this.body !== -1)
return Math.rad2deg(q_body(2, this.body)) % 360;
else
return this._angle;
},
get relangle() {
if (!this.level) return this.angle;
return this.angle - this.level.angle;
},
get velocity() { return q_body(3, this.body); },
set velocity(x) { set_body(9, this.body, x); },
get angularvelocity() { return Math.rad2deg(q_body(4, this.body)); },
set angularvelocity(x) { if (this.alive) set_body(8, this.body, Math.deg2rad(x)); },
get alive() { return this.body >= 0; },
disable() {
this.components.forEach(function(x) { x.disable(); });
},
enable() {
this.components.forEach(function(x) { x.enable(); });
},
sync() {
if (this.body === -1) return;
cmd(55, this.body, this.flipx);
cmd(56, this.body, this.flipy);
set_body(2, this.body, this.pos);
set_body(0, this.body, Math.deg2rad(this.angle));
cmd(36, this.body, this.scale);
set_body(10,this.body,this.elasticity);
set_body(11,this.body,this.friction);
set_body(1, this.body, this.phys);
cmd(75,this.body,this.layer);
cmd(54, this.body);
if (this.components)
for (var key in this.components)
this.components[key].sync();
},
syncall() {
this.instances.forEach(function(x) { x.sync(); });
},
pulse(vec) {
set_body(4, this.body, vec);
},
push(vec) {
set_body(12,this.body,vec);
},
gizmo: "", /* Path to an image to draw for this gameobject */
set_pawn() {
this.controlled = true;
set_pawn(this);
},
uncontrol() {
if (!this.controlled) return;
unset_pawn(this);
},
/* Bounding box of the object in world dimensions */
get boundingbox() {
var boxes = [];
boxes.push({t:0, r:0,b:0,l:0});
for (var key in this.components) {
if ('boundingbox' in this.components[key])
boxes.push(this.components[key].boundingbox);
}
if (boxes.empty) return;
var bb = boxes[0];
boxes.forEach(function(x) {
bb = bb_expand(bb, x);
});
var cwh = bb2cwh(bb);
if (!bb) return;
if (this.flipx) cwh.c.x *= -1;
if (this.flipy) cwh.c.y *= -1;
cwh.c = cwh.c.add(this.pos);
bb = cwh2bb(cwh.c, cwh.wh);
return bb ? bb : cwh2bb([0,0], [0,0]);
},
kill() {
Log.warn(`Killing ${this.toString()}`);
cmd(2, this.body);
delete Game.objects[this.body];
if (this.level)
this.level.unregister(this);
this.uncontrol();
this.instances.remove(this);
Register.unregister_obj(this);
Signal.clear_obj(this);
this.body = -1;
for (var key in this.components) {
Register.unregister_obj(this.components[key]);
this.components[key].kill();
}
},
prop_obj() {
var obj = JSON.parse(JSON.stringify(this));
delete obj.name;
delete obj._pos;
delete obj._angle;
delete obj.from;
return obj;
},
get up() {
return [0,1].rotate(Math.deg2rad(this.angle));
},
get down() {
return [0,-1].rotate(Math.deg2rad(this.angle));
},
get right() {
return [1,0].rotate(Math.deg2rad(this.angle));
},
get left() {
return [-1,0].rotate(Math.deg2rad(this.angle));
},
/* Make a unique object the same as its prototype */
revert() {
unmerge(this, this.prop_obj());
this.sync();
},
gui() {
var go_guis = walk_up_get_prop(this, 'go_gui');
Nuke.newline();
go_guis.forEach(function(x) { x.call(this); }, this);
for (var key in this) {
if (typeof this[key] === 'object' && 'gui' in this[key]) this[key].gui();
}
},
world2this(pos) { return cmd(70, this.body, pos); },
this2world(pos) { return cmd(71, this.body,pos); },
make(props, level) {
level ??= World;
var obj = Object.create(this);
this.instances.push(obj);
obj.toString = function() {
var props = obj.prop_obj();
for (var key in props)
if (typeof props[key] === 'object' && props[key].empty)
delete props[key];
var edited = !props.empty;
return (edited ? "#" : "") + obj.name + " object " + obj.body + ", layer " + obj.draw_layer + ", phys " + obj.layer;
};
obj.deflock('toString');
obj.defc('from', this.name);
obj.defn('body', make_gameobject(this.scale,
this.phys,
this.mass,
this.friction,
this.elasticity) );
complete_assign(obj, props);
obj.sync();
obj.defn('components', {});
for (var prop in obj) {
if (typeof obj[prop] === 'object' && 'make' in obj[prop]) {
if (prop === 'flipper') return;
obj[prop] = obj[prop].make(obj.body);
obj[prop].defn('gameobject', obj);
obj.components[prop] = obj[prop];
}
};
if (typeof obj.update !== 'undefined')
register_update(obj.update, obj);
if (typeof obj.physupdate === 'function')
register_physupdate(obj.physupdate, obj);
if (typeof obj.collide === 'function')
obj.register_hit(obj.collide, obj);
if (typeof obj.separate === 'function')
obj.register_separate(obj.separate, obj);
obj.components.forEach(function(x) {
if (typeof x.collide === 'function')
register_collide(1, x.collide, x, obj.body, x.shape);
});
if ('begin' in obj) obj.begin();
return obj;
},
register_hit(fn, obj) {
if (!obj)
obj = this;
Signal.obj_begin(fn, obj, this);
},
register_separate(fn, obj) {
if (!obj)
obj = this;
Signal.obj_separate(fn,obj,this);
},
}
var locks = ['draw_layer', 'friction','elasticity', 'visible', 'body', 'flipx', 'flipy', 'controlled', 'selectable', 'save', 'velocity', 'angularvelocity', 'alive', 'boundingbox', 'name', 'scale', 'angle', 'properties', 'moi', 'relpos', 'relangle', 'up', 'down', 'right', 'left', 'bodytype', 'gizmo', 'pos'];
locks.forEach(function(x) {
Object.defineProperty(gameobject, x, {enumerable:false});
});
/* Load configs */
function load_configs(file) {
var configs = JSON.parse(IO.slurp(file));
for (var key in configs) {
Object.assign(globalThis[key], configs[key]);
}
Collision.sync();
Game.objects.forEach(function(x) { x.sync(); });
if (!local_conf.mouse) {
Log.info("disabling mouse features");
Mouse.disabled = function() {};
Mouse.hidden = function() {};
};
};
var local_conf = {
mouse: true,
};
/* Save configs */
function save_configs() {
Log.info("saving configs");
var configs = {};
configs.editor_config = editor_config;
configs.Nuke = Nuke;
configs.local_conf = local_conf;
IO.slurpwrite(JSON.stringify(configs, null, 1), "editor.config");
save_game_configs();
};
function save_game_configs() {
var configs = {};
configs.physics = physics;
configs.Collision = Collision;
Log.info(configs);
IO.slurpwrite(JSON.stringify(configs,null,1), "game.config");
Collision.sync();
Game.objects.forEach(function(x) { x.sync(); });
};
var Collision = {
types: {},
num: 10,
set_collide(a, b, x) {
this.types[a][b] = x;
this.types[b][a] = x;
},
sync() {
for (var i = 0; i < this.num; i++)
cmd(76,i,this.types[i]);
},
types_nuke() {
Nuke.newline(this.num+1);
Nuke.label("");
for (var i = 0; i < this.num; i++) Nuke.label(i);
for (var i = 0; i < this.num; i++) {
Nuke.label(i);
for (var j = 0; j < this.num; j++) {
if (j < i)
Nuke.label("");
else {
this.types[i][j] = Nuke.checkbox(this.types[i][j]);
this.types[j][i] = this.types[i][j];
}
}
}
},
};
for (var i = 0; i < Collision.num; i++) {
Collision.types[i] = [];
for (var j = 0; j < Collision.num; j++)
Collision.types[i][j] = false;
};
if (IO.exists("game.config"))
load_configs("game.config");
var camera2d = gameobject.clone("camera2d", {
phys: gameobject.bodytype.kinematic,
speed: 300,
get zoom() { return this._zoom; },
set zoom(x) {
if (x <= 0) return;
this._zoom = x;
cmd(62, this._zoom);
},
_zoom: 1.0,
speedmult: 1.0,
selectable: false,
view2world(pos) {
return pos.mapc(mult, [1,-1]).add([-Window.width,Window.height].scale(0.5)).scale(this.zoom).add(this.pos);
},
world2view(pos) {
return pos.sub(this.pos).scale(1/this.zoom).add(Window.dimensions.scale(0.5));
},
});
win_make(Game.title, Game.resolution[0], Game.resolution[1]);
win_icon("icon.png");
/* Default objects */
gameobject.clone("polygon2d", {
polygon2d: polygon2d.clone(),
});
gameobject.clone("edge2d", {
edge2d: bucket.clone(),
});
gameobject.clone("sprite", {
sprite: sprite.clone(),
});
load("config.js");
var prototypes = JSON.parse(slurp("proto.json"));
for (var key in prototypes) {
if (key in gameobjects)
dainty_assign(gameobjects[key], prototypes[key]);
else {
/* Create this gameobject fresh */
Log.info("Making new prototype: " + key + " from " + prototypes[key].from);
var newproto = gameobjects[prototypes[key].from].clone(key);
gameobjects[key] = newproto;
for (var pkey in newproto)
if (typeof newproto[pkey] === 'object' && newproto[pkey] && 'clone' in newproto[pkey])
newproto[pkey] = newproto[pkey].clone();
dainty_assign(gameobjects[key], prototypes[key]);
}
}
function save_gameobjects_as_prototypes() { slurpwrite(JSON.stringify(gameobjects,null,2), "proto.json"); };