398 lines
10 KiB
JavaScript
398 lines
10 KiB
JavaScript
var ex = this
|
||
|
||
var input = use('input')
|
||
|
||
var DEAD = Symbol()
|
||
var GARBAGE = Symbol()
|
||
var FILE = Symbol()
|
||
var TIMERS = Symbol()
|
||
var REGGIES = Symbol()
|
||
var UNDERLINGS = Symbol()
|
||
var OVERLING = Symbol()
|
||
|
||
function add_timer(obj, fn, seconds) {
|
||
var timers = obj[TIMERS]
|
||
|
||
var stop = function () {
|
||
if (!timer) return
|
||
timers.delete(stop)
|
||
timer.fn = undefined
|
||
timer = undefined
|
||
}
|
||
|
||
function execute() {
|
||
if (fn) timer.remain = fn(stop.seconds)
|
||
if (!timer) return
|
||
if (!timer.remain) stop()
|
||
else stop.seconds = timer.remain
|
||
}
|
||
|
||
// var timer = os.make_timer(execute)
|
||
timer.remain = seconds
|
||
|
||
stop.remain = seconds
|
||
stop.seconds = seconds
|
||
|
||
timers.push(stop)
|
||
return stop
|
||
}
|
||
|
||
globalThis.Register = {
|
||
registries: [],
|
||
|
||
add_cb(name) {
|
||
var n = {}
|
||
var fns = []
|
||
|
||
n.register = function (fn, oname) {
|
||
if (typeof fn !== 'function') return
|
||
|
||
var dofn = function (...args) {
|
||
fn(...args)
|
||
}
|
||
Object.defineProperty(dofn, 'name', {value:`do_${oname}`})
|
||
|
||
var left = 0
|
||
var right = fns.length - 1
|
||
dofn.layer = fn.layer
|
||
dofn.layer ??= 0
|
||
|
||
while (left <= right) {
|
||
var mid = Math.floor((left + right) / 2)
|
||
if (fns[mid] === dofn.layer) {
|
||
left = mid
|
||
break
|
||
} else if (fns[mid].layer < dofn.layer) left = mid + 1
|
||
else right = mid - 1
|
||
}
|
||
|
||
fns.splice(left, 0, dofn)
|
||
return function () {
|
||
fns.delete(dofn)
|
||
}
|
||
}
|
||
|
||
prosperon[name] = function (...args) {
|
||
fns.forEach(fn => {
|
||
fn(...args)
|
||
})
|
||
}
|
||
Object.defineProperty(prosperon[name], 'name', {value:name})
|
||
prosperon[name].fns = fns
|
||
|
||
n.clear = function () {
|
||
fns = []
|
||
}
|
||
|
||
Register[name] = n
|
||
Register.registries[name] = n
|
||
return n
|
||
},
|
||
}
|
||
|
||
Register.pull_registers = function pull_registers(obj) {
|
||
var reggies = []
|
||
for (var reg in Register.registries) {
|
||
if (typeof obj[reg] === "function")
|
||
reggies.push(reg)
|
||
}
|
||
return reggies
|
||
}
|
||
|
||
Register.register_obj = function register_obj(obj, reg) {
|
||
var fn = obj[reg].bind(obj)
|
||
fn.layer = obj[reg].layer
|
||
var name = obj.ur ? obj.ur.name : obj.toString()
|
||
obj[TIMERS].push(Register.registries[reg].register(fn, name))
|
||
if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`})
|
||
}
|
||
|
||
Register.check_registers = function check_registers(obj) {
|
||
if (obj[REGGIES]) {
|
||
if (obj[REGGIES].length == 0) return
|
||
for (var reg of obj[REGGIES])
|
||
Register.register_obj(obj,reg)
|
||
return
|
||
}
|
||
for (var reg in Register.registries) {
|
||
if (typeof obj[reg] === "function")
|
||
Register.register_obj(obj,reg)
|
||
}
|
||
}
|
||
|
||
Register.add_cb("appupdate")
|
||
Register.add_cb("update").doc = "Called once per frame."
|
||
Register.add_cb("physupdate")
|
||
Register.add_cb("gui")
|
||
Register.add_cb("hud")
|
||
Register.add_cb("draw")
|
||
Register.add_cb("imgui")
|
||
Register.add_cb("app")
|
||
|
||
|
||
var actor = {}
|
||
|
||
actor.toString = function() { return this[FILE] }
|
||
|
||
actor.spawn = function spawn(script, config, actor_context) {
|
||
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
||
var prog
|
||
if (!script) {
|
||
prog = {}
|
||
prog.module_ret = {}
|
||
prog.prog_fn = function() {}
|
||
} else {
|
||
prog = script_fn(script)
|
||
if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
|
||
}
|
||
|
||
var underling
|
||
prog.module_ret.__proto__ = actor
|
||
underling = Object.create(prog.module_ret)
|
||
underling[OVERLING] = this
|
||
underling[FILE] = script
|
||
underling[TIMERS] = []
|
||
underling[UNDERLINGS] = new Set()
|
||
|
||
// Make $_ available to the actor (either passed context or the engine's $_)
|
||
var actor_dollar = actor_context || $_
|
||
Object.defineProperty(underling, '$_', {
|
||
value: actor_dollar,
|
||
writable:false,
|
||
enumerable:false,
|
||
configurable:false
|
||
})
|
||
|
||
Object.defineProperty(underling, 'overling', {
|
||
get() { return this[OVERLING] },
|
||
enumerable:true,
|
||
configurable:false
|
||
})
|
||
|
||
Object.defineProperty(underling, 'underlings', {
|
||
get() { return new Set(this[UNDERLINGS]) },
|
||
enumerable:true,
|
||
configurable:false
|
||
})
|
||
|
||
Object.defineProperty(underling, 'spawn', {
|
||
value: function(script, config) {
|
||
return actor.spawn.call(this, script, config, actor_dollar)
|
||
},
|
||
writable:false,
|
||
enumerable:true,
|
||
configurable:false
|
||
})
|
||
|
||
Object.defineProperty(underling, 'kill', {
|
||
value: actor.kill,
|
||
writable:false,
|
||
enumerable:true,
|
||
configurable:false
|
||
})
|
||
|
||
Object.defineProperty(underling, 'delay', {
|
||
value: actor.delay,
|
||
writable:false,
|
||
enumerable:true,
|
||
configurable:false
|
||
})
|
||
|
||
try {
|
||
// Pass $_ as a parameter to actor scripts
|
||
prog.prog_fn.call(underling, actor_dollar)
|
||
} catch(e) { throw e; }
|
||
|
||
if (underling[DEAD]) return undefined;
|
||
|
||
if (typeof config === 'object') Object.assign(underling, config)
|
||
|
||
if (!underling[REGGIES])
|
||
underling.__proto__[REGGIES] = Register.pull_registers(underling)
|
||
|
||
Register.check_registers(underling)
|
||
if (underling.awake) underling.awake()
|
||
|
||
this[UNDERLINGS].add(underling)
|
||
if (underling.tag) act.tag_add(underling.tag, underling)
|
||
|
||
underling[GARBAGE] = underling.garbage
|
||
return underling
|
||
}
|
||
|
||
actor.clear = function actor_clear() {
|
||
this[UNDERLINGS].forEach(p => {
|
||
p.kill()
|
||
})
|
||
this[UNDERLINGS].clear()
|
||
}
|
||
|
||
actor.kill = function kill() {
|
||
if (this[DEAD]) return
|
||
this[DEAD] = true
|
||
this[TIMERS].slice().forEach(t => t())
|
||
delete this[TIMERS]
|
||
input.do_uncontrol(this)
|
||
|
||
this.clear()
|
||
this[OVERLING][UNDERLINGS].delete(this)
|
||
delete this[UNDERLINGS]
|
||
|
||
if (typeof this.garbage === "function") this.garbage()
|
||
if (typeof this.then === "function") this.then()
|
||
|
||
act.tag_clear_guid(this)
|
||
}
|
||
actor.kill.doc = `Remove this actor and all its underlings from existence.`
|
||
|
||
actor.delay = function(fn, seconds) {
|
||
if (this[DEAD]) return
|
||
add_timer(this, fn, seconds)
|
||
}
|
||
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`
|
||
|
||
actor[UNDERLINGS] = new Set()
|
||
|
||
|
||
ex[cell.DOC] = `
|
||
A set of utilities for iterating over a hierarchy of actor-like objects, as well
|
||
as managing tag-based lookups. Objects are assumed to have a "objects" property,
|
||
pointing to children or sub-objects, forming a tree.
|
||
`
|
||
|
||
function eachobj(obj, fn) {
|
||
var val = fn(obj)
|
||
if (val) return val
|
||
for (var o in obj.objects) {
|
||
if (obj.objects[o] === obj) log.error(`Object ${obj.toString()} is referenced by itself.`)
|
||
val = eachobj(obj.objects[o], fn)
|
||
if (val) return val
|
||
}
|
||
}
|
||
|
||
ex.all_objects = function (fn, startobj = world) {
|
||
return eachobj(startobj, fn)
|
||
}
|
||
ex.all_objects[cell.DOC] = `
|
||
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
|
||
:param startobj: The root object at which iteration begins, default is the global "world".
|
||
:return: The first truthy value returned by fn, or undefined if none.
|
||
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
|
||
`
|
||
|
||
ex.find_object = function (fn, startobj = world) {}
|
||
ex.find_object[cell.DOC] = `
|
||
:param fn: A callback or criteria to locate a particular object.
|
||
:param startobj: The root object at which search begins, default "world".
|
||
:return: Not yet implemented.
|
||
Intended to find a matching object within the hierarchy.
|
||
`
|
||
|
||
var gtags = {}
|
||
|
||
ex.tag_add = function (tag, obj) {
|
||
gtags[tag] ??= new Set()
|
||
gtags[tag].add(obj)
|
||
}
|
||
ex.tag_add[cell.DOC] = `
|
||
:param tag: A string tag to associate with the object.
|
||
:param obj: The object to add under this tag.
|
||
:return: None
|
||
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
|
||
`
|
||
|
||
ex.tag_rm = function (tag, obj) {
|
||
delete gtags[tag].delete(obj)
|
||
}
|
||
ex.tag_rm[cell.DOC] = `
|
||
:param tag: The tag to remove the object from.
|
||
:param obj: The object to remove from the tag set.
|
||
:return: None
|
||
Remove the given object from the specified tag’s set, if it exists.
|
||
`
|
||
|
||
ex.tag_clear_guid = function (obj) {
|
||
for (var tag in gtags) gtags[tag].delete(obj)
|
||
}
|
||
ex.tag_clear_guid[cell.DOC] = `
|
||
:param obj: The object whose tags should be cleared.
|
||
:return: None
|
||
Remove the object from all tag sets.
|
||
`
|
||
|
||
ex.objects_with_tag = function (tag) {
|
||
if (!gtags[tag]) return []
|
||
return Array.from(gtags[tag])
|
||
}
|
||
ex.objects_with_tag[cell.DOC] = `
|
||
:param tag: A string tag to look up.
|
||
:return: An array of objects associated with the given tag.
|
||
Retrieve all objects currently tagged with the specified tag.
|
||
`
|
||
|
||
function parse_file(content, file) {
|
||
if (!content) return {}
|
||
if (content.match()
|
||
if (!/^\s*---\s*$/m.test(content)) {
|
||
var part = content.trim()
|
||
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
||
return { module: part }
|
||
}
|
||
return { program: part }
|
||
}
|
||
var parts = content.split(/\n\s*---\s*\n/)
|
||
var module = parts[0]
|
||
if (!/\breturn\b/.test(module))
|
||
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`)
|
||
|
||
try {
|
||
new Function(module)()
|
||
} catch (e) {
|
||
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message)
|
||
}
|
||
|
||
var pad = '\n'.repeat(module.split('\n').length + 4)
|
||
return {
|
||
module,
|
||
program: pad + parts[1]
|
||
}
|
||
}
|
||
|
||
// path is the path of a module or script to resolve
|
||
var script_fn = function script_fn(path, args) {
|
||
var parsed = {}
|
||
var file = resources.find_script(path)
|
||
|
||
if (!file) {
|
||
parsed.module_ret = bare_load(path)
|
||
if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
|
||
return parsed
|
||
}
|
||
|
||
var content = io.slurp(file)
|
||
var parsed = parse_file(content, file)
|
||
var module_name = file.name()
|
||
parsed.module_ret = bare_load(path)
|
||
parsed.module_ret ??= {}
|
||
|
||
if (parsed.module) {
|
||
// Create a context object with args
|
||
var context = Object.create(parsed.module_ret)
|
||
context.__args__ = args || []
|
||
var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports: exports}; var define = undefined; var arg = this.__args__; ${parsed.module}})`
|
||
var module_fn = js.eval(file, mod_script)
|
||
parsed.module_ret = module_fn.call(context)
|
||
if (parsed.module_ret === undefined || parsed.module_ret === null)
|
||
throw new Error(`Module ${module_name} must return a value`)
|
||
parsed.module_fn = module_fn
|
||
}
|
||
|
||
parsed.program ??= ""
|
||
var prog_script = `(function use_${module_name}($_) { var self = this; var $ = this.__proto__; ${parsed.program}})`
|
||
parsed.prog_fn = js.eval(file, prog_script)
|
||
|
||
return parsed
|
||
}.hashify()
|
||
|
||
return ex
|