diff --git a/Makefile b/Makefile index fa7c181..86d4330 100755 --- a/Makefile +++ b/Makefile @@ -114,12 +114,14 @@ $(BIN)$(NAME): $(objprefix)/source/engine/yugine.o $(ENGINE) $(BIN)libquickjs.a $(CC) $< $(LINK) -o $(BIN)$(NAME) @echo Finished build -$(BIN)$(DIST): $(BIN)$(NAME) source/shaders/* +$(BIN)$(DIST): $(BIN)$(NAME) source/shaders/* source/scripts/* assets/* @echo Creating distribution $(DIST) @mkdir -p $(BIN)dist @cp $(BIN)$(NAME) $(BIN)dist @cp -rf assets/fonts $(BIN)dist + @cp -rf assets/icons $(BIN)dist @cp -rf source/shaders $(BIN)dist + @cp -r source/scripts $(BIN)dist @tar czf $(DIST) --directory $(BIN)dist . @mv $(DIST) $(BIN) diff --git a/assets/fonts/LessPerfectDOSVGA.ttf b/assets/fonts/LessPerfectDOSVGA.ttf new file mode 100644 index 0000000..6aaf28c Binary files /dev/null and b/assets/fonts/LessPerfectDOSVGA.ttf differ diff --git a/assets/fonts/dos.font b/assets/fonts/dos.font new file mode 100644 index 0000000..03f6a9d --- /dev/null +++ b/assets/fonts/dos.font @@ -0,0 +1,4 @@ +(font + :path "LessPerfectDOSVGA.ttf" + :size 16 + ) diff --git a/assets/icons/icons8-bug-16.png b/assets/icons/icons8-bug-16.png new file mode 100644 index 0000000..2e40ca0 Binary files /dev/null and b/assets/icons/icons8-bug-16.png differ diff --git a/assets/icons/icons8-console-16.png b/assets/icons/icons8-console-16.png new file mode 100644 index 0000000..c3fc8c2 Binary files /dev/null and b/assets/icons/icons8-console-16.png differ diff --git a/assets/icons/icons8-factory-16.png b/assets/icons/icons8-factory-16.png new file mode 100644 index 0000000..504762f Binary files /dev/null and b/assets/icons/icons8-factory-16.png differ diff --git a/assets/icons/icons8-gear-16.png b/assets/icons/icons8-gear-16.png new file mode 100644 index 0000000..62503f4 Binary files /dev/null and b/assets/icons/icons8-gear-16.png differ diff --git a/assets/icons/icons8-light-switch-16.png b/assets/icons/icons8-light-switch-16.png new file mode 100644 index 0000000..b2f65ca Binary files /dev/null and b/assets/icons/icons8-light-switch-16.png differ diff --git a/assets/icons/icons8-lock-16.png b/assets/icons/icons8-lock-16.png new file mode 100644 index 0000000..26823fa Binary files /dev/null and b/assets/icons/icons8-lock-16.png differ diff --git a/assets/icons/icons8-music-16.png b/assets/icons/icons8-music-16.png new file mode 100644 index 0000000..87e1d46 Binary files /dev/null and b/assets/icons/icons8-music-16.png differ diff --git a/assets/icons/icons8-pause-16.png b/assets/icons/icons8-pause-16.png new file mode 100644 index 0000000..eaa584e Binary files /dev/null and b/assets/icons/icons8-pause-16.png differ diff --git a/assets/icons/icons8-radio-tower-16.png b/assets/icons/icons8-radio-tower-16.png new file mode 100644 index 0000000..00bf15e Binary files /dev/null and b/assets/icons/icons8-radio-tower-16.png differ diff --git a/assets/icons/icons8-record-16.png b/assets/icons/icons8-record-16.png new file mode 100644 index 0000000..b3547c4 Binary files /dev/null and b/assets/icons/icons8-record-16.png differ diff --git a/assets/icons/icons8-resume-button-16.png b/assets/icons/icons8-resume-button-16.png new file mode 100644 index 0000000..8454c52 Binary files /dev/null and b/assets/icons/icons8-resume-button-16.png differ diff --git a/assets/icons/icons8-shuffle-16.png b/assets/icons/icons8-shuffle-16.png new file mode 100644 index 0000000..73ad01e Binary files /dev/null and b/assets/icons/icons8-shuffle-16.png differ diff --git a/assets/icons/icons8-skip-to-start-16.png b/assets/icons/icons8-skip-to-start-16.png new file mode 100644 index 0000000..2d7437a Binary files /dev/null and b/assets/icons/icons8-skip-to-start-16.png differ diff --git a/assets/icons/icons8-spotlight-16.png b/assets/icons/icons8-spotlight-16.png new file mode 100644 index 0000000..aa0712e Binary files /dev/null and b/assets/icons/icons8-spotlight-16.png differ diff --git a/assets/icons/no_tex.png b/assets/icons/no_tex.png new file mode 100644 index 0000000..bb980b9 Binary files /dev/null and b/assets/icons/no_tex.png differ diff --git a/docs/editor.adoc b/docs/editor.adoc new file mode 100644 index 0000000..c45d05c --- /dev/null +++ b/docs/editor.adoc @@ -0,0 +1,164 @@ += Yugine Editor + +The main editor view is made up of objects. Each object can have a +number of components attached to it. When an object is selected, its +name, position, and list of components are listed. + +.Basic controls +* Ctrl-Z Undo +* Ctrl-Shift-Z Redo +* Ctrl-A Select all +* Ctrl-S Save +* Ctrl-N New +* Ctrl-O Open level +* Ctrl-X Cut +* Ctrl-C Copy +* Ctrl-V Paste +* Alt-O Add level to current level +* Alt-A or Alt-P Add a prefab +* Ctrl-I Objects on the level +* Ctrl-M Asset viewer. When on a component like a sprite, serves to select that sprite's texture +* Ctrl-[ Downsize grid +* Ctrl-] Upsize grid +* Backtick REPL +* Ctrl-[1-9] to set camera positions +* [1-9] to recall camera positions +* 0 Set camera to home view +* ESC quit +* Alt-1 Normal view +* Alt-2 Wireframe view +* Shift-Middle Set editor cursor to mouse position (Cursor affects how objects rotate) +* Shift-Ctrl-Middle Set cursor to object selection +* Shift-Right Remove cursor + +.Editor Mode select +* Alt-F1 Basic mode +* Alt-F2 Brush mode + - Clicking will place what is on clipboard + +.Object controls +* G Translate +* Alt-G Snap objects to cursor +* S Scale +* R Rotate +* Ctrl-P Save object changes to prefab +* Ctrl-shift-P Save object changes as a unique prefab ("Parent") +* Ctrl-shift-T Save object changes to a side prefab ("Type") +* Ctrl-J Bake name to expose to level script +* Alt-J Remove baked name +* Ctrl-Y Show obj chain +* Alt-Y Start prototype explorer +* Ctrl-U Revert object or component to match prototype +* Alt-U Make object unique. If a level, allows setting of internal object position and rotation. +* Ctrl-shift-G Save group as a level +* Arrows Translate 1 px +* Shift-Arrows Translate 10 px +* Tab Select component +* F Zoom to object(s) +* Ctrl-F Focus on a selected sublevel. Edit and save it in place. +* Ctrl-Shift-F Go up one level in the editing chain. +* M Flip horizontally +* Ctrl-M Flip vertically +* Ctrl-D Duplicate +* H Hide +* Ctrl-H Unhide all +* T Lock +* Alt-T Unlock all +* Q Toggle component help +* Ctrl-Shift-Alt-Click Set object center + +.Mouse controls +* Left Select +* Middle Quick grab +* Right Unselect + +.Level controls +* Ctrl-L Open level script + +.Game controls +* F1 Debug draw +* F2 Config menu +* F3 Show bounding boxes +* F4 Show gizmos +* F5 Start +* F6 Pause +* F7 Stop +* F10 Toggle slow motion + +== Components +Components all have their own set of controls. Many act similar to +objects. If a component has a position attribute, it will react as +expected to object grabbing; same with scaling, rotation, and so on. + +If a component uses an asset, the asset viewer will serve to pick new +assets for it. + +.Spline controls +* Ctrl-click Add a point +* Shift-click remove a point +* +,- Increase or decrease spline segments +* Ctrl-+,- Increase or decrease spline degrees. Put this to 1 for the spline to go point to point +* Alt-B,V Increase or decrease spline thickness + +.Collider controls +* Alt-S Toggle sensor + += Yugine Programming + +.Object functions + +* start(): Called when the object is created, before the first update is ran +* update(dt): Called once per frame +* physupdate(dt): Called once per physics calculation +* stop(): Called when the object is killed +* collide(hit): Called when this object collides with another. If on a collider, specific to that collider + - hit.hit: Gameobject ID of what's being hit + - hit.velocity: Velocity of impact + - hit.normal: Normal of impact + +.Input +Input works by adding functions to an object, and then "controlling" +them. The format for a function is "input_[key]_[action]". [Action] +can be any of + +- down: Called once per frame the key is down +- pressed: Called when the key is pressed +- released: called when the key is released + +For example, "input_p_pressed()" will be called when p is pressed, and not again +until it is released and pressed again. + +.Your game + +When the engine runs, it executes config.js, and then game.js. A +window should be created in config.js, and custom code for prototypes +should be executed. + +game.js is the place to open your first level. + +.Levels + +A level is a collection of objects. A level has a script associated +with it. The script is ran when the level is loaded. + +Levels can be added to other levels. Each is independent and unique. +In this way, complicated behavior can easily be added up. For example, +a world might have a door that opens with a lever. The door and lever +can be saved off as their own level, and the level script can contain +the code that causes the door to open when the lever is thrown. Then, +as many door-lever levels can be added to your game as you want. + +The two primary ways to add objects to the game are World.spawn, and +Level.add. World.spawn creates a single object in the world, Level.add +adds an entire level, along with its script. + +Levels also can be checked for "completion". A level can be loaded +over many frames, and only have all of its contents appear once it's +finished loading. World.spawn is immediate. + +Level.clear removes the level from the game world. + +.Level scripting +Each level has a script which is ran when the level is loaded, or the +game is played. A single object declared in it called "scene" can be +used to expose itself to the rest of the game. diff --git a/docs/game.adoc b/docs/game.adoc new file mode 100644 index 0000000..40aabb5 --- /dev/null +++ b/docs/game.adoc @@ -0,0 +1,65 @@ += Yugine Engine + +The yugine essentially is made of a sequence of levels. Levels can be +nested, they can be streamed in, or loaded one at a time. Levels are +made of levels. + +Different "modes" of using the engine has unique sequences of level +loading orders. Each level has an associated script file. Level +loading functions call the script file, as well as the level file. The +level file can be considered to be a container of objects that the +associated script file can access. + +.Game start + +* Engine scripts +* config.js +* game.lvl & game.js + +.Editor + +* Engine scripts +* config.js +* editor.js + +.Editor play + +* F5 debug.lvl + - Used for custom debug level testing. If doesn't exist, game.lvl is loaded. +* F6 game.lvl +* F7 Currently edited level + +While playing ... +* F7 Stop + +.Scripting + +Levels and objects have certain functions you can use that will be +called at particular times during the course of the game running. + +setup + Called once, when the object is created. + +start + Called once when the gameplay is simulating. + +update(dt) + Called once per frame, while the game is simulating + +physupdate(dt) + Called once per physics step + +stop + Called when the object is destroyed, either by being killed or otherwise. + +.Collider functions +Colliders get special functions to help with collision handling. + +collide(hit) + Called when an object collides with the object this function is on. + +"hit" object + normal - A vector in the direction the hit happened + hit - Object ID of colliding object + sensor - True if the colliding object is a sensor + velocity - A vector of the velocity of the collision diff --git a/docs/video.adoc b/docs/video.adoc new file mode 100644 index 0000000..ceea2da --- /dev/null +++ b/docs/video.adoc @@ -0,0 +1,10 @@ +.Yugine video playing + +Yugine plays the open source MPEG-TS muxer, using MPEG1 video and MP2 audio. + +MPEG-1 video works best at about 1.5 Mbit/s, in SD [720x480] video. +For HD [1920x1080] video, use 9 Mbit/s. + +ffmpeg -i "input.video" -vcodec mpeg1video -b:v 1.5M -s 720x480 -acodec mp2 "out.ts" +ffmpeg -i "input.video" -vcodec mpeg1video -b:v 9M -s 1920x1080 -acodec mp2 "out.ts" + diff --git a/source/engine/ffi.c b/source/engine/ffi.c index 9a596db..ebc0644 100644 --- a/source/engine/ffi.c +++ b/source/engine/ffi.c @@ -553,7 +553,8 @@ JSValue dukext2paths(char *ext) JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) { int cmd = js2int(argv[0]); - const char *str; + const char *str = NULL; + const char *str2 = NULL; JSValue ret = JS_NULL; switch(cmd) { @@ -611,16 +612,23 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) return ret; case 12: - sprite_loadtex(id2sprite(js2int(argv[1])), JS_ToCString(js, argv[2]), js2glrect(argv[3])); + str = JS_ToCString(js,argv[2]); + sprite_loadtex(id2sprite(js2int(argv[1])), str, js2glrect(argv[3])); + JS_FreeCString(js,str); break; case 13: - - play_song(JS_ToCString(js, argv[1]), JS_ToCString(js, argv[2])); + str = JS_ToCString(js,argv[1]); + str2 = JS_ToCString(js,argv[2]); + play_song(str,str2); + JS_FreeCString(js,str); + JS_FreeCString(js,str2); break; case 14: - mini_sound(JS_ToCString(js, argv[1])); + str = JS_ToCString(js, argv[1]); + mini_sound(str); + JS_FreeCString(js,str); break; case 15: @@ -712,10 +720,18 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) break; case 38: - return JS_NewString(js, slurp_text(JS_ToCString(js, argv[1]))); + str = JS_ToCString(js,argv[1]); + ret = JS_NewString(js, slurp_text(str)); + JS_FreeCString(js,str); + return ret; case 39: - return JS_NewInt64(js, slurp_write(JS_ToCString(js, argv[1]), JS_ToCString(js, argv[2]))); + str = JS_ToCString(js,argv[1]); + str2 = JS_ToCString(js,argv[2]); + ret = JS_NewInt64(js, slurp_write(str, str2)); + JS_FreeCString(js,str); + JS_FreeCString(js,str2); + return ret; case 40: id2go(js2int(argv[1]))->filter.categories = js2bitmask(argv[2]); @@ -805,13 +821,19 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) return JS_NewFloat64(js, deltaT); case 64: - return vec2js(tex_get_dimensions(texture_pullfromfile(JS_ToCString(js, argv[1])))); + str = JS_ToCString(js,argv[1]); + ret = vec2js(tex_get_dimensions(texture_pullfromfile(str))); + break; case 65: - return JS_NewBool(js, file_exists(JS_ToCString(js, argv[1]))); + str = JS_ToCString(js,argv[1]); + ret = JS_NewBool(js, file_exists(str)); + break; case 66: - return dukext2paths(JS_ToCString(js, argv[1])); + str = JS_ToCString(js,argv[1]); + ret = dukext2paths(str); + break; case 67: opengl_rendermode(LIT); @@ -885,8 +907,9 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) return ints2js(phys2d_query_box_points(js2vec2(argv[1]), js2vec2(argv[2]), js2cpvec2arr(argv[3]), js2int(argv[4]))); case 87: - mini_music_play(JS_ToCString(js, argv[1])); - return JS_NULL; + str = JS_ToCString(js, argv[1]); + mini_music_play(str); + break; case 88: mini_music_pause(); @@ -897,10 +920,20 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) return JS_NULL; case 90: - window_set_icon(JS_ToCString(js, argv[1])); + str = JS_ToCString(js, argv[1]); + window_set_icon(str); break; } + if (str) + JS_FreeCString(js,str); + + if (str2) + JS_FreeCString(js,str2); + + if (!JS_IsNull(ret)) + return ret; + return JS_NULL; } diff --git a/source/engine/script.c b/source/engine/script.c index f10c489..6268859 100644 --- a/source/engine/script.c +++ b/source/engine/script.c @@ -38,7 +38,6 @@ void script_init() { /* Load all prefabs into memory */ script_dofile("scripts/engine.js"); script_dofile("config.js"); - //ftw(".", load_prefab, 10); } void script_run(const char *script) { diff --git a/source/engine/texture.c b/source/engine/texture.c index 2a15b50..5f56a78 100644 --- a/source/engine/texture.c +++ b/source/engine/texture.c @@ -122,7 +122,7 @@ struct Texture *texture_loadfromfile(const char *path) { struct Texture *new = texture_pullfromfile(path); - if (new == NULL) { new = texture_pullfromfile("./ph.png"); } + if (new == NULL) { new = texture_pullfromfile("./icons/no_tex.png"); } if (new->id == 0) { glGenTextures(1, &new->id); diff --git a/source/engine/yugine.c b/source/engine/yugine.c index f1ce285..528e70b 100644 --- a/source/engine/yugine.c +++ b/source/engine/yugine.c @@ -187,7 +187,7 @@ int main(int argc, char **args) { renderMS = 1.0/vidmode->refreshRate; if (ed) - script_dofile("editor.js"); + script_dofile("scripts/editor.js"); else script_dofile("game.js"); diff --git a/source/scripts/base.js b/source/scripts/base.js new file mode 100644 index 0000000..9d44b49 --- /dev/null +++ b/source/scripts/base.js @@ -0,0 +1,609 @@ +/* Prototypes out an object and extends with values */ +function clone(proto, binds) { + var c = Object.create(proto); + complete_assign(c, binds); + return c; +}; + +/* Prototypes out an object and assigns values */ +function copy(proto, binds) { + var c = Object.create(proto); + Object.assign(c, binds); + return c; +}; + +/* OBJECT DEFININTIONS */ +Object.defineProperty(Object.prototype, 'getOwnPropertyDescriptors', { + value: function() { + var obj = {}; + for (var key in this) { + obj[key] = Object.getOwnPropertyDescriptor(this, key); + } + + return obj; + } +}); + +Object.defineProperty(Object.prototype, 'hasOwn', { + value: function(x) { return this.hasOwnProperty(x); } +}); + +Object.defineProperty(Object.prototype, 'array', { + value: function() { + var a = []; + for (var key in this) + a.push(this[key]); + + return a; + }, +}); + +Object.defineProperty(Object.prototype, 'defn', { + value: function(name, val) { + Object.defineProperty(this, name, { value:val, writable:true, configurable:true }); + } +}); + +Object.defineProperty(Object.prototype, 'nulldef', { + value: function(name, val) { + if (!this.hasOwnProperty(name)) this[name] = val; + } +}); + +/* defc 'define constant'. Defines a value that is not writable. */ +Object.defineProperty(Object.prototype, 'defc', { + value: function(name, val) { + Object.defineProperty(this,name, { + value: val, + writable:false, + enumerable:true, + configurable:false, + }); + } +}); + +Object.defineProperty(Object.prototype, 'deflock', { + value: function(prop) { + Object.defineProperty(this,prop, {configurable:false}); + } +}); + +Object.defineProperty(Object.prototype, 'forEach', { + value: function(fn) { + for (var key in this) + fn(this[key]); + } +}); + + +Object.defineProperty(Object.prototype, 'empty', { + get: function() { + return Object.keys(this).empty; + }, +}); + +Object.defineProperty(Object.prototype, 'nth', { + value: function(x) { + if (this.empty || x >= Object.keys(this).length) return null; + + return this[Object.keys(this)[x]]; + }, +}); + +Object.defineProperty(Object.prototype, 'findIndex', { + value: function(x) { + var i = 0; + for (var key in this) { + if (this[key] === x) return i; + i++; + } + + return -1; + } +}); + + +/* STRING DEFS */ + +Object.defineProperty(String.prototype, 'next', { + value: function(char, from) { + if (!Array.isArray(char)) + char = [char]; + if (from > this.length-1) + return -1; + else if (!from) + from = 0; + + var find = this.slice(from).search(char[0]); + + if (find === -1) + return -1; + else + return from + find; + + var i = 0; + var c = this.charAt(from+i); + while (!char.includes(c)) { + i++; + if (from+i >this.length-1) return -1; + c = this.charAt(from+i); + } + + return from+i; + } +}); + +Object.defineProperty(String.prototype, 'prev', { + value: function(char, from, count) { + if (from > this.length-1) + return -1; + else if (!from) + from = this.length-1; + + if (!count) count = 0; + + var find = this.slice(0,from).lastIndexOf(char); + + while (count > 1) { + find = this.slice(0,find).lastIndexOf(char); + count--; + } + + if (find === -1) + return 0; + else + return find; + } +}); + +Object.defineProperty(String.prototype, 'shift', { + value: function(n) { + if (n === 0) return this.slice(); + + if (n > 0) + return this.slice(n); + + if (n < 0) + return this.slice(0, this.length+n); + } +}); + + +/* ARRAY DEFS */ + +Object.defineProperty(Array.prototype, 'copy', { + value: function() { + var c = []; + + this.forEach(function(x, i) { + c[i] = deep_copy(x); + }); + + return c; + } +}); + +Object.defineProperty(Array.prototype, 'rotate', { + value: function(a) { + return Vector.rotate(this, a); + } +}); + + +Object.defineProperty(Array.prototype, '$add', { +value: function(b) { + for (var i = 0; i < this.length; i++) { + this[i] += b[i]; + } +}}); + +function setelem(n) { + return { + get: function() { return this[n]; }, + set: function(x) { this[n] = x; } + } +}; + +function arrsetelem(str, n) +{ + Object.defineProperty(Array.prototype, str, setelem(n)); +} + +Object.defineProperty(Array.prototype, 'x', setelem(0)); +Object.defineProperty(Array.prototype, 'y', setelem(1)); +Object.defineProperty(Array.prototype, 'z', setelem(2)); +Object.defineProperty(Array.prototype, 'w', setelem(3)); +arrsetelem('r', 0); +arrsetelem('g', 1); +arrsetelem('b', 2); +arrsetelem('a', 3); + + +Object.defineProperty(Array.prototype, 'add', { +value: function(b) { + var c = []; + for (var i = 0; i < this.length; i++) { c[i] = this[i] + b[i]; } + return c; +}}); + +Object.defineProperty(Array.prototype, 'newfirst', { + value: function(i) { + var c = this.slice(); + if (i >= c.length) return c; + + do { + c.push(c.shift()); + i--; + } while (i > 0); + + return c; + } +}); + +Object.defineProperty(Array.prototype, 'doubleup', { + value: function(n) { + var c = []; + this.forEach(function(x) { + for (var i = 0; i < n; i++) + c.push(x); + }); + + return c; + } +}); + +Object.defineProperty(Array.prototype, 'sub', { +value: function(b) { + var c = []; + for (var i = 0; i < this.length; i++) { c[i] = this[i] - b[i]; } + return c; +}}); + +Object.defineProperty(Array.prototype, 'apply', { + value: function(fn) { + this.forEach(function(x) { x[fn].apply(x); }); + } +}); + +Object.defineProperty(Array.prototype, 'scale', { + value: function(s) { + return this.map(function(x) { return x*s; }); +}}); + +Object.defineProperty(Array.prototype, 'equal', { +value: function(b) { + if (this.length !== b.length) return false; + if (b == null) return false; + if (this === b) return true; + + return JSON.stringify(this) === JSON.stringify(b); + + for (var i = 0; i < this.length; i++) { + if (!this[i] === b[i]) + return false; + } + + return true; +}}); + +function add(x,y) { return x+y; }; +function mult(x,y) { return x*y; }; + +Object.defineProperty(Array.prototype, 'mapc', { + value: function(fn, arr) { + return this.map(function(x, i) { + return fn(x, arr[i]); + }); + } +}); + +Object.defineProperty(Array.prototype, 'remove', { + value: function(b) { + var idx = this.indexOf(b); + + if (idx === -1) return false; + + this.splice(idx, 1); + + return true; +}}); + +Object.defineProperty(Array.prototype, 'set', { + value: function(b) { + if (this.length !== b.length) return; + + b.forEach(function(val, i) { this[i] = val; }, this); + } +}); + +Object.defineProperty(Array.prototype, 'flat', { + value: function() { + return [].concat.apply([],this); + } +}); + +Object.defineProperty(Array.prototype, 'any', { + value: function(fn) { + var ev = this.every(function(x) { + return !fn(x); + }); + return !ev; + } +}); + +/* Return true if array contains x */ +/*Object.defineProperty(Array.prototype, 'includes', { +value: function(x) { + return this.some(e => e === x); +}}); +*/ +Object.defineProperty(Array.prototype, 'empty', { + get: function() { return this.length === 0; }, +}); + +Object.defineProperty(Array.prototype, 'push_unique', { + value: function(x) { + if (!this.includes(x)) this.push(x); +}}); + +Object.defineProperty(Array.prototype, 'unique', { + value: function() { + var c = []; + this.forEach(function(x) { c.push_unique(x); }); + return c; + } +}); + + +Object.defineProperty(Array.prototype, 'findIndex', { + value: function(fn) { + var idx = -1; + this.every(function(x, i) { + if (fn(x)) { + idx = i; + return false; + } + + return true; + }); + + return idx; +}}); + +Object.defineProperty(Array.prototype, 'find', { + value: function(fn) { + var ret; + + this.every(function(x) { + if (fn(x)) { + ret = x; + return false; + } + + return true; + }); + + return ret; +}}); + +Object.defineProperty(Array.prototype, 'last', { + get: function() { return this[this.length-1]; }, +}); + +Object.defineProperty(Array.prototype, 'at', { +value: function(x) { + return x < 0 ? this[this.length+x] : this[x]; +}}); + +Object.defineProperty(Array.prototype, 'wrapped', { + value: function(x) { + var c = this.slice(0, this.length); + + for (var i = 0; i < x; i++) + c.push(this[i]); + + return c; +}}); + +Object.defineProperty(Array.prototype, 'wrap_idx', { + value: function(x) { + while (x >= this.length) { + x -= this.length; + } + + return x; + } +}); + +Object.defineProperty(Array.prototype, 'mirrored', { + value: function(x) { + var c = this.slice(0); + if (c.length <= 1) return c; + for (var i = c.length-2; i >= 0; i--) + c.push(c[i]); + + return c; + } +}); + + +/* MATH EXTENSIONS */ + +Math.clamp = function (x, l, h) { return x > h ? h : x < l ? l : x; } + +Math.lerp = function (s, f, dt) { + return s + (Math.clamp(dt, 0, 1) * (f - s)); +}; + +Math.snap = function(val, grid) { + if (!grid || grid === 1) return Math.round(val); + + var rem = val%grid; + var d = val - rem; + var i = Math.round(rem/grid)*grid; + return d+i; +} + +Math.angledist = function (a1, a2) { + var dist = a2 - a1; + var wrap = dist >= 0 ? dist+360 : dist-360; + wrap %= 360; + + if (Math.abs(dist) < Math.abs(wrap)) + return dist; + + return wrap; +}; + +Math.deg2rad = function(deg) { return deg * 0.0174533; } +Math.rad2deg = function(rad) { return rad / 0.0174533; } + +/* BOUNDINGBOXES */ +function cwh2bb(c, wh) { + return { + t: c.y+wh.y/2, + b: c.y-wh.y/2, + l: c.x-wh.x/2, + r: c.x+wh.x/2 + }; +}; + +function points2bb(points) { + var b= {t:0,b:0,l:0,r:0}; + + points.forEach(function(x) { + if (x.y > b.t) b.t = x.y; + if (x.y < b.b) b.b = x.y; + if (x.x > b.r) b.r = x.x; + if (x.x < b.l) b.l = x.x; + }); + + return b; +}; + + +function bb2cwh(bb) { + if (!bb) return undefined; + var cwh = {}; + + var w = bb.r - bb.l; + var h = bb.t - bb.b; + cwh.wh = [w, h]; + cwh.c = [bb.l + w/2, bb.b + h/2]; + + return cwh; +}; + +function bb_expand(oldbb, x) { + if (!oldbb || !x) return; + var bb = {}; + Object.assign(bb, oldbb); + + if (bb.t < x.t) bb.t = x.t; + if (bb.r < x.r) bb.r = x.r; + if (bb.b > x.b) bb.b = x.b; + if (bb.l > x.l) bb.l = x.l; + + return bb; +}; + +function bb_draw(bb, color) { + if (!bb) return; + var draw = bb2cwh(bb); + draw.wh[0] /= editor.camera.zoom; + draw.wh[1] /= editor.camera.zoom; + Debug.box(world2screen(draw.c), draw.wh, color); +}; + +function bb_from_objects(objs) { + var bb = objs[0].boundingbox; + objs.forEach(function(obj) { bb = bb_expand(bb, obj.boundingbox); }); + return bb; +}; + + +/* VECTORS */ +var Vector = { + x: 0, + y: 0, + length(v) { + var sum = v.reduce(function(acc, val) { return acc + val**2; }, 0); + return Math.sqrt(sum); + }, + norm(v) { + var len = Vector.length(v); + return [v.x/len, v.y/len]; + + }, + make(x, y) { + var vec = Object.create(this, {x:x, y:y}); + }, + + project(a, b) { + return cmd(85, a, b); + }, + + dot(a, b) { + + }, + + random() { + var vec = [Math.random()-0.5, Math.random()-0.5]; + return Vector.norm(vec); + }, + + angle(v) { + return Math.atan2(v.y, v.x); + }, + + rotate(v,angle) { + var r = Vector.length(v); + var p = Vector.angle(v) + angle; + return [r*Math.cos(p), r*Math.sin(p)]; + }, + + equal(v1, v2, tol) { + if (!tol) + return v1.equal(v2); + + var eql = true; + var c = v1.sub(v2); + + c.forEach(function(x) { + if (!eql) return; + if (Math.abs(x) > tol) + eql = false; + }); + + return eql; + }, + +}; + +/* POINT ASSISTANCE */ + +function points2cm(points) +{ + var x = 0; + var y = 0; + var n = points.length; + points.forEach(function(p) { + x = x + p[0]; + y = y + p[1]; + }); + + return [x/n,y/n]; +}; + +function sortpointsccw(points) +{ + var cm = points2cm(points); + var cmpoints = points.map(function(x) { return x.sub(cm); }); + var ccw = cmpoints.sort(function(a,b) { + aatan = Math.atan2(a.y, a.x); + batan = Math.atan2(b.y, b.x); + return aatan - batan; + }); + + return ccw.map(function(x) { return x.add(cm); }); +} diff --git a/source/scripts/components.js b/source/scripts/components.js new file mode 100644 index 0000000..6417c11 --- /dev/null +++ b/source/scripts/components.js @@ -0,0 +1,657 @@ +var component = { + toString() { + if ('gameobject' in this) + return this.name + " on " + this.gameobject; + else + return this.name; + }, + name: "component", + component: true, + set enabled(x) { }, + get enabled() { }, + enable() { this.enabled = true; }, + disable() { this.enabled = false; }, + + make(go) { }, + kill() { Log.info("Kill not created for this component yet"); }, + gui() { }, + gizmo() { }, + + prepare_center() {}, + finish_center() {}, + clone(spec) { + return clone(this, spec); + }, +}; + +var sprite = clone(component, { + name: "sprite", + path: "", + _pos: [0, 0], + get layer() { + if (!this.gameobject) + return 0; + else + return this.gameobject.draw_layer; + }, + + get pos() { return this._pos; }, + set pos(x) { + this._pos = x; + this.sync(); + }, + + get boundingbox() { + if (!this.gameobject) return null; + var dim = cmd(64, this.path); + dim = dim.scale(this.gameobject.scale); + var realpos = this.pos.copy(); + realpos.x *= dim.x; + realpos.y *= dim.y; + realpos.x += (dim.x/2); + realpos.y += (dim.y/2); + return cwh2bb(realpos, dim); + }, + + set asset(x) { + if (!x) return; + if (!x.endsWith(".png")) { + Log.error("Can't set texture to a non image."); + return; + } + + this.path = x; + Log.info("path is now " + x); + this.sync(); + }, + + _enabled: true, + set enabled(x) { this._enabled = x; cmd(20, this.id, x); }, + get enabled() { return this._enabled; return cmd(21, this.id); }, + get visible() { return this.enabled; }, + set visible(x) { this.enabled = x; }, + + _angle: 0, + get angle() { return this._angle; }, + set angle(x) { + this._angle = x; + sync(); + }, + + make(go) { + var sprite = clone(this); + Object.defineProperty(sprite, 'id', {value:make_sprite(go,this.path,this.pos)}); + sprite.sync(); + return sprite; + }, + + rect: {s0:0, s1: 1, t0: 0, t1: 1}, + + sync() { + if (!this.hasOwn('id')) return; + cmd(60, this.id, this.layer); + cmd(37, this.id, this.pos); + cmd(12, this.id, this.path, this.rect); + }, + + load_img(img) { + cmd(12, this.id, img); + }, + + kill() { + cmd(9, this.id); + }, +}); + +/* Container to play sprites and anim2ds */ +var char2d = clone(sprite, { + clone(anims) { + var char = clone(this); + char.anims = anims; + return char; + }, + + name: "char 2d", + + frame2rect(frames, frame) { + var rect = {s0:0,s1:1,t0:0,t1:1}; + + var frameslice = 1/frames; + rect.s0 = frameslice*frame; + rect.s1 = frameslice*(frame+1); + return rect; + }, + + make(go) { + var char = clone(this); + char.curplaying = char.anims.array()[0]; + Object.defineProperty(char, 'id', {value:make_sprite(go,this.path,this.pos)}); + char.frame = 0; + char.timer = timer.make(char.advance, 1/char.curplaying.fps, char); + char.timer.loop = true; + char.rect = char.frame2rect(char.curplaying.frames, char.frame); + char.setsprite(); + return char; + }, + + frame: 0, + + play(name) { + if (!(name in this.anims)) { + Log.info("Can't find an animation named " + name); + return; + } + + if (this.curplaying === this.anims[name]) return; + + this.curplaying = this.anims[name]; + this.timer.time = 1/this.curplaying.fps; + this.timer.start(); + this.frame = 0; + this.setsprite(); + }, + + setsprite() { + this.path = this.curplaying.path; + this.rect = this.frame2rect(this.curplaying.frames, this.frame); + cmd(12, this.id, this.path, this.rect); + }, + + advance() { + this.frame = (this.frame + 1) % this.curplaying.frames; + this.setsprite(); + }, + + devance() { + this.frame = (this.frame - 1); + if (this.frame === -1) this.frame = this.curplaying.frames-1; + this.setsprite(); + }, + + pause() { + this.timer.pause(); + }, + + stop() { + this.frame = 0; + this.timer.stop(); + this.setsprite(); + }, + + kill() { + this.timer.kill(); + cmd(9, this.id); + }, +}); + +/* For all colliders, "shape" is a pointer to a phys2d_shape, "id" is a pointer to the shape data */ +var collider2d = clone(component, { + name: "collider 2d", + + _sensor: false, + set sensor(x) { + this._sensor = x; + if (this.shape) + cmd(18, this.shape, x); + }, + get sensor() { return this._sensor; }, + + input_s_pressed() { + if (!Keys.alt()) return; + + this.sensor = !this.sensor; + }, + + input_t_pressed() { + if (!Keys.alt()) return; + + this.enabled = !this.enabled; + }, + + coll_sync() { + cmd(18, this.shape, this.sensor); + }, + + _enabled: true, + set enabled(x) {this._enabled = x; if (this.id) cmd(22, this.id, x); }, + get enabled() { return this._enabled; }, + kill() {}, /* No killing is necessary - it is done through the gameobject's kill */ + + register_hit(fn, obj) { + register_collide(1, fn, obj, this.gameobject.body, this.shape); + }, +}); + + +var polygon2d = clone(collider2d, { + name: "polygon 2d", + points: [], + help: "Ctrl-click Add a point\nShift-click Remove a point", + clone(spec) { + var obj = Object.create(this); + obj.points = this.points.copy(); + Object.assign(obj, spec); + return obj; + }, + + make(go) { + var poly = Object.create(this); + Object.assign(poly, make_poly2d(go, this.points)); + Object.defineProperty(poly, 'id', {enumerable:false}); + Object.defineProperty(poly, 'shape', {enumerable:false}); + + return poly; + }, + + get boundingbox() { + if (!this.gameobject) return null; + var scaledpoints = []; + this.points.forEach(function(x) { scaledpoints.push(x.scale(this.gameobject.scale)); }, this); + return points2bb(scaledpoints); + }, + + input_f10_pressed() { + this.points = sortpointsccw(this.points); + }, + + sync() { + if (!this.id) return; + cmd_poly2d(0, this.id, this.spoints); + this.coll_sync(); + }, + + input_b_pressed() { + if (!Keys.ctrl()) return; + + this.points = this.spoints; + this.mirrorx = false; + this.mirrory = false; + }, + + get spoints() { + var spoints = this.points.slice(); + + if (this.mirrorx) { + spoints.forEach(function(x) { + var newpoint = x.slice(); + newpoint.x = -newpoint.x; + spoints.push(newpoint); + }); + } + + if (this.mirrory) { + spoints.forEach(function(x) { + var newpoint = x.slice(); + newpoint.y = -newpoint.y; + spoints.push(newpoint); + }); + } + + return spoints; + }, + + gizmo() { + if (!this.hasOwn('points')) this.points = this.__proto__.points.copy(); + + this.spoints.forEach(function(x) { + Debug.point(world2screen(this.gameobject.this2world(x)), 3, Color.green); + }, this); + + this.points.forEach(function(x, i) { + Debug.numbered_point(this.gameobject.this2world(x), i); + }, this); + + + this.sync(); + }, + + input_lmouse_pressed() { + if (Keys.ctrl()) { + this.points.push(this.gameobject.world2this(Mouse.worldpos)); + } else if (Keys.shift()) { + var idx = grab_from_points(screen2world(Mouse.pos), this.points.map(this.gameobject.this2world,this.gameobject), 25); + if (idx === -1) return; + this.points.splice(idx, 1); + } + }, + + pick(pos) { + return Gizmos.pick_gameobject_points(pos, this.gameobject, this.points); + }, + + query() { + return cmd(80, this.shape); + }, + + mirrorx: false, + mirrory: false, + + input_m_pressed() { + if (Keys.ctrl()) + this.mirrory = !this.mirrory; + else + this.mirrorx = !this.mirrorx; + }, +}); + +var bucket = clone(collider2d, { + name: "bucket", + help: "Ctrl-click Add a point\nShift-click Remove a point\n+,- Increase/decrease spline segs\nCtrl-+,- Inc/dec spline degrees\nCtrl-b,v Inc/dec spline thickness", + clone(spec) { + var obj = Object.create(this); + obj.cpoints = this.cpoints.copy(); + dainty_assign(obj, spec); + return obj; + }, + + cpoints:[], + degrees:2, + dimensions:2, + /* open: 0 + clamped: 1 + beziers: 2 + looped: 3 + */ + type: 3, + + get boundingbox() { + if (!this.gameobject) return null; + var scaledpoints = []; + this.points.forEach(function(x) { scaledpoints.push(x.scale(this.gameobject.scale)); }, this); + return points2bb(scaledpoints); + }, + + mirrorx: false, + mirrory: false, + + input_m_pressed() { + if (Keys.ctrl()) { + this.mirrory = !this.mirrory; + } else { + this.mirrorx = !this.mirrorx; + } + + this.sync(); + }, + + hollow: false, + input_h_pressed() { + this.hollow = !this.hollow; + }, + + get spoints() { + var spoints = this.cpoints.slice(); + + if (this.mirrorx) { + for (var i = spoints.length-1; i >= 0; i--) { + var newpoint = spoints[i].slice(); + newpoint.x = -newpoint.x; + spoints.push(newpoint); + } + } + + if (this.mirrory) { + for (var i = spoints.length-1; i >= 0; i--) { + var newpoint = spoints[i].slice(); + newpoint.y = -newpoint.y; + spoints.push(newpoint); + } + } + + return spoints; + + if (this.hollow) { + var hpoints = []; + var inflatep = inflate_cpv(spoints, spoints.length, this.hollowt); + inflatep[0].slice().reverse().forEach(function(x) { hpoints.push(x); }); + + inflatep[1].forEach(function(x) { hpoints.push(x); }); + return hpoints; + } + + return spoints; + }, + + hollowt: 0, + + input_g_pressed() { + if (!Keys.ctrl()) return; + this.hollowt--; + if (this.hollowt < 0) this.hollowt = 0; + }, + + input_f_pressed() { + if (!Keys.ctrl()) return; + this.hollowt++; + }, + + sample(n) { + var spoints = this.spoints; + + this.degrees = Math.clamp(this.degrees, 1, spoints.length-1); + + if (spoints.length === 2) + return spoints; + if (spoints.length < 2) + return []; + if (this.degrees < 2) { + if (this.type === 3) + return spoints.wrapped(1); + + return spoints; + } + + /* + order = degrees+1 + knots = spoints.length + order + assert knots%order != 0 + */ + + if (this.type === 3) + return spline_cmd(0, this.degrees, this.dimensions, 0, spoints.wrapped(this.degrees), n); + + return spline_cmd(0, this.degrees, this.dimensions, this.type, spoints, n); + }, + + samples: 10, + points:[], + + make(go) { + var edge = Object.create(this); + Object.assign(edge, make_edge2d(go, this.points, this.thickness)); + Object.defineProperty(edge, 'id', {enumerable:false}); + Object.defineProperty(edge, 'shape', {enumerable:false}); + edge.defn('points', []); +// Object.defineProperty(edge, 'points', {enumerable:false}); + edge.sync(); + return edge; + }, + + sync() { + if (!this.gameobject) return; + this.points = this.sample(this.samples); + cmd_edge2d(0, this.id, this.points); + cmd_edge2d(1, this.id, this._thickness * this.gameobject.scale); + this.coll_sync(); + }, + + gizmo() { + if (!this.hasOwn('cpoints')) this.cpoints = this.__proto__.cpoints.copy(); + + this.spoints.forEach(function(x) { + Debug.point(world2screen(this.gameobject.this2world(x)), 3, Color.green); + }, this); + + this.cpoints.forEach(function(x, i) { + Debug.numbered_point(this.gameobject.this2world(x), i); + }, this); + + this.sync(); + }, + + _thickness:0, /* Number of pixels out the edge is */ + get thickness() { return this._thickness; }, + set thickness(x) { + this._thickness = Math.max(x, 0); + cmd_edge2d(1, this.id, this._thickness); + }, + + input_v_pressrep() { + if (!Keys.alt()) return; + this.thickness--; + }, + + input_b_pressrep() { + if (Keys.alt()) { + this.thickness++; + } else if (Keys.ctrl()) { + this.cpoints = this.spoints; + this.mirrorx = false; + this.mirrory = false; + } + }, + + finish_center(change) { + this.cpoints = this.cpoints.map(function(x) { return x.sub(change); }); + this.sync(); + }, + + + input_plus_pressrep() { + if (Keys.ctrl()) + this.degrees++; + else + this.samples += 1; + }, + + input_minus_pressrep() { + if (Keys.ctrl()) + this.degrees--; + else { + this.samples -= 1; + if (this.samples < 1) this.samples = 1; + } + }, + + input_r_pressed() { + if (!Keys.ctrl()) return; + + this.cpoints = this.cpoints.reverse(); + }, + + input_l_pressed() { + if (!Keys.ctrl()) return; + this.type = 3; + }, + + input_c_pressed() { + if (!Keys.ctrl()) return; + this.type = 1; + }, + + input_o_pressed() { + if (!Keys.ctrl()) return; + this.type = 0; + }, + + input_lmouse_pressed() { + if (Keys.ctrl()) { + if (Keys.alt()) { + var idx = grab_from_points(Mouse.worldpos, this.cpoints.map(this.gameobject.world2this,this.gameobject), 25); + if (idx === -1) return; + + this.cpoints = this.cpoints.newfirst(idx); + return; + } + var idx = 0; + + if (this.cpoints.length >= 2) { + idx = cmd(59, screen2world(Mouse.pos).sub(this.gameobject.pos), this.cpoints, 1000); + if (idx === -1) return; + } + + if (idx === this.cpoints.length) + this.cpoints.push(this.gameobject.world2this(screen2world(Mouse.pos))); + else + this.cpoints.splice(idx, 0, this.gameobject.world2this(screen2world(Mouse.pos))); + return; + } else if (Keys.shift()) { + var idx = grab_from_points(screen2world(Mouse.pos), this.cpoints.map(function(x) {return x.add(this.gameobject.pos); }, this), 25); + if (idx === -1) return; + + this.cpoints.splice(idx, 1); + } + }, + + pick(pos) { return Gizmos.pick_gameobject_points(pos, this.gameobject, this.cpoints); }, + + input_lbracket_pressrep() { + var np = []; + + this.cpoints.forEach(function(c) { + np.push(Vector.rotate(c, Math.deg2rad(-1))); + }); + + this.cpoints = np; + }, + + input_rbracket_pressrep() { + var np = []; + + this.cpoints.forEach(function(c) { + np.push(Vector.rotate(c, Math.deg2rad(1))); + }); + + this.cpoints = np; + }, +}); + +var circle2d = clone(collider2d, { + name: "circle 2d", + get radius() { + return this.rradius; + }, + rradius: 10, + set radius(x) { + this.rradius = x; + cmd_circle2d(0, this.id, this.rradius); + }, + + get boundingbox() { + if (!this.gameobject) return null; + var radius = this.radius*2*this.gameobject.scale; + return cwh2bb(this.offset.scale(this.gameobject.scale), [radius, radius]); + }, + + get scale() { return this.radius; }, + set scale(x) { this.radius = x; }, + + ofset: [0,0], + get offset() { return this.ofset; }, + set offset(x) { this.ofset = x; cmd_circle2d(1, this.id, this.ofset); }, + + get pos() { return this.ofset; }, + set pos(x) { this.offset = x; }, + + make(go) { + var circle = clone(this); + var circ = make_circle2d(go, circle.radius, circle.offset); + Object.assign(circle, circ); + Object.defineProperty(circle, 'id', {enumerable:false}); + Object.defineProperty(circle, 'shape', {enumerable:false}); + return circle; + }, + + gui() { + Nuke.newline(); + Nuke.label("circle2d"); + this.radius = Nuke.pprop("Radius", this.radius); + this.offset = Nuke.pprop("offset", this.offset); + }, + + sync() { + cmd_circle2d(0, this.id, this.rradius); + cmd_circle2d(-1, this.id); + this.coll_sync(); + }, +}); diff --git a/source/scripts/debug.js b/source/scripts/debug.js new file mode 100644 index 0000000..79ff674 --- /dev/null +++ b/source/scripts/debug.js @@ -0,0 +1,207 @@ +var Gizmos = { + pick_gameobject_points(worldpos, gameobject, points) { + var idx = grab_from_points(worldpos, points.map(gameobject.this2world,gameobject), 25); + if (idx === -1) return null; + return points[idx]; + }, +}; + + +var Debug = { + draw_grid(width, span) { + cmd(47, width, span); + }, + + point(pos, size, color) { + color = color ? color : Color.blue; + cmd(51, pos, size,color); + }, + + arrow(start, end, color, capsize) { + color = color ? color : Color.red; + if (!capsize) + capsize = 4; + cmd(81, start, end, color, capsize); + }, + + box(pos, wh, color) { + color = color ? color : Color.white; + cmd(53, pos, wh, color); + }, + + numbered_point(pos, n) { + Debug.point(world2screen(pos), 3); + gui_text(n, world2screen(pos).add([0,4]), 1); + }, + + phys_drawing: false, + draw_phys(on) { + this.phys_drawing = on; + cmd(4, this.phys_drawing); + }, + + draw_obj_phys(obj) { + cmd(82, obj.body); + }, + + register_call(fn, obj) { + register_debug(fn,obj); + }, + + print_callstack() { + for (var i = -3;; i--) { + var t = Duktape.act(i); + if (!t) break; + var file = t.function ? t.function.fileName : ""; + var line = t.lineNumber; + Log.info(file + ":" + line); + } + }, + + line(points, color, type) { + if (!type) + type = 0; + + if (!color) + color = Color.white; + + switch (type) { + case 0: + cmd(83, points, color); + } + }, +}; + +var Gizmos = { + pick_gameobject_points(worldpos, gameobject, points) { + var idx = grab_from_points(worldpos, points.map(gameobject.this2world,gameobject), 25); + if (idx === -1) return null; + return points[idx]; + }, +}; + + +var Nuke = { + newline(cols) { nuke(3, cols ? cols : 1); }, + newrow(height) { nuke(13,height); }, + + wins: {}, + curwin:"", + + prop(str, v) { + var ret = nuke(2, str, v); + if (Number.isFinite(ret)) return ret; + return 0; + }, + + treeid: 0, + + tree(str) { var on = nuke(11, str, this.treeid); this.treeid++; return on; }, + tree_pop() { nuke(12);}, + + prop_num(str, num) { return nuke(2, str, num, -1e10, 1e10, 0.01); }, + prop_bool(str, val) { return nuke(4, str, val); }, + checkbox(val) { return nuke(4,"",val); }, + label(str) { nuke(5, str); }, + textbox(str) { return nuke(7, str); }, + scrolltext(str) { nuke(14,str); }, + + defaultrect: { x:10, y:10, w:400, h:600 }, + window(name) { + this.curwin = name; + var rect; + if (name in this.wins) + rect = this.wins[name]; + else + rect = { x:10, y:10, w:400, h:600 }; + + nuke(0, name, rect); + }, + button(name) { return nuke(6, name); }, + radio(name, val, cmp) { return nuke(9, name, val, cmp); }, + img(path) { nuke(8, path); }, + end() { + this.wins[this.curwin] = nuke(10); + this.treeid = 0; + nuke(1); + }, + + pprop(str, p, nonew) { + switch(typeof p) { + case 'number': + if (!nonew) Nuke.newline(); + return Nuke.prop_num(str, p); + break; + + case 'boolean': + if (!nonew) Nuke.newline(); + return Nuke.prop_bool(str, p); + + case 'object': + if (Array.isArray(p)) { + var arr = []; + Nuke.newline(p.length+1); + Nuke.label(str); + arr[0] = Nuke.pprop("#x", p[0], true); + arr[1] = Nuke.pprop("#y", p[1], true); + return arr; + + } else { + if (!nonew)Nuke.newline(2); + Nuke.label(str); + Nuke.label(p); + } + break; + + case 'string': + if (!nonew) Nuke.newline(); + Nuke.label(str); + return Nuke.textbox(p); + + default: + if (!nonew) Nuke.newline(2); + Nuke.label(str); + Nuke.label(p); + } + }, +}; + +Object.defineProperty(Nuke, "curwin", {enumerable:false}); +Object.defineProperty(Nuke, "defaultrect", {enumerable:false}); + + +var Log = { + 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); + }, + +}; + diff --git a/source/scripts/diff.js b/source/scripts/diff.js new file mode 100644 index 0000000..1d1f611 --- /dev/null +++ b/source/scripts/diff.js @@ -0,0 +1,141 @@ +function deep_copy(from) { + if (typeof from !== 'object') + return from; + + if (Array.isArray(from)) { + var c = []; + from.forEach(function(x,i) { c[i] = deep_copy(x); }); + return c; + } + + var obj = {}; + for (var key in from) + obj[key] = deep_copy(from[key]); + + return obj; +}; + + +var walk_up_get_prop = function(obj, prop, endobj) { + var props = []; + var cur = obj; + while (cur !== Object.prototype) { + if (cur.hasOwn(prop)) + props.push(cur[prop]); + + cur = cur.__proto__; + } + + return props; +}; + + +function complete_assign(target, source) { + var descriptors = {}; + var assigns = {}; + if (typeof source === 'undefined') return target; + Object.keys(source).forEach(function (k) { + var desc = Object.getOwnPropertyDescriptor(source, k); + + if (desc.value) { + if (typeof desc.value === 'object' && desc.value.hasOwn('value')) + descriptors[k] = desc.value; + else + assigns[k] = desc.value; + } else + descriptors[k] = desc; + }); + + Object.defineProperties(target, descriptors); + Object.assign(target, assigns); + return target; +}; + +/* Assigns properties from source to target, only if they exist in target */ +function dainty_assign(target, source) +{ + for (var key in source) { + if (typeof source[key] === 'function') { + target[key] = source[key]; + continue; + } + if (!(key in target)) continue; + if (Array.isArray(target[key])) + target[key] = source[key]; + else if (typeof target[key] === 'object') + dainty_assign(target[key], source[key]); + else + target[key] = source[key]; + } +}; + +/* Deeply remove source keys from target, not removing objects */ +function unmerge(target, source) { + for (var key in source) { + if (typeof source[key] === 'object' && !Array.isArray(source[key])) + unmerge(target[key], source[key]); + else + delete target[key]; + } +}; + +/* Deeply merge two objects, not clobbering objects on target with objects on source */ +function deep_merge(target, source) +{ + for (var key in source) { + if (typeof source[key] === 'object' && !Array.isArray(source[key])) + deep_merge(target[key], source[key]); + else + target[key] = source[key]; + } +}; + + +function equal(x,y) { + if (typeof x === 'object') + for (var key in x) + return equal(x[key],y[key]); + + return x === y; +}; + +function diffassign(target, from) { + if (from.empty) return; + + for (var e in from) { + if (typeof from[e] === 'object') { + if (!target.hasOwnProperty(e)) + target[e] = from[e]; + else + diffassign(target[e], from[e]); + } else { + if (from[e] === "DELETE") { + delete target[e]; + } else { + target[e] = from[e]; + } + } + } +}; + +function diff(from, to) { + var obj = {}; + + for (var e in to) { + if (typeof to[e] === 'object' && from.hasOwnProperty(e)) { + obj[e] = diff(from[e], to[e]); + if (obj[e].empty) + delete obj[e]; + } else { + if (from[e] !== to[e]) + obj[e] = to[e]; + } + } + + for (var e in from) { + if (!to.hasOwnProperty(e)) + obj[e] = "DELETE"; + } + + return obj; +}; diff --git a/source/scripts/editor.js b/source/scripts/editor.js new file mode 100644 index 0000000..7c4dd60 --- /dev/null +++ b/source/scripts/editor.js @@ -0,0 +1,2572 @@ +/* + Editor-only variables on objects + selectable +*/ + +var editor_level = Level.create(); +var editor_camera = editor_level.spawn(camera2d); +editor_camera.save = false; +set_cam(editor_camera.body); + +var editor_config = { + grid_size: 100, +}; + +var configs = { + toString() { return "configs"; }, + editor: editor_config, + physics: physics, + local: local_conf, + collision: Collision, +}; + +var editor = { + dbgdraw: false, + selected: null, + selectlist: [], + moveoffset: [0,0], + startrot: 0, + rotoffset: 0, + obj_panel: false, + camera: editor_camera, + edit_level: {}, /* The current level that is being edited */ + working_layer: -1, + cursor: null, + edit_mode: "basic", + + input_f1_pressed() { + if (Keys.ctrl()) { + this.edit_mode = "basic"; + return; + } + + Debug.draw_phys(!Debug.phys_drawing); + }, + + input_f2_pressed() { + if (Keys.ctrl()) { + this.edit_mode = "brush"; + return; + } + + objectexplorer.on_close = save_configs; + objectexplorer.obj = configs; + this.openpanel(objectexplorer); + }, + + input_j_pressed() { + if (Keys.alt()) { + var varmakes = this.selectlist.filter(function(x) { return x.hasOwn('varname'); }); + varmakes.forEach(function(x) { delete x.varname; }); + } else if (Keys.ctrl()) { + var varmakes = this.selectlist.filter(function(x) { return !x.hasOwn('varname'); }); + varmakes.forEach(function(x) { + var allvnames = []; + this.edit_level.objects.forEach(function(x) { + if (x.varname) + allvnames.push(x.varname); + }); + + var vname = x.from.replace(/ /g, '_').replace(/_object/g, '').replace(/\..*$/g, ''); + var tnum = 1; + var testname = vname + "_" + tnum; + while (allvnames.includes(testname)) { + tnum++; + testname = vname + "_" + tnum; + } + x.varname = testname; + },this); + } + }, + + input_i_pressed() { + if (!Keys.ctrl()) return; + + this.openpanel(levellistpanel, true); + }, + + input_y_pressed() { + if (Keys.ctrl()) { + if (this.selectlist.length !== 1) return; + objectexplorer.obj = this.selectlist[0]; + objectexplorer.on_close = this.save_prototypes; + this.openpanel(objectexplorer); + } else if (Keys.alt()) { + this.openpanel(protoexplorer); + } + }, + + try_select() { /* nullify true if it should set selected to null if it doesn't find an object */ + var go = physics.pos_query(screen2world(Mouse.pos)); + return this.do_select(go); + }, + + /* Tries to select id */ + do_select(go) { + var obj = go >= 0 ? Game.object(go) : null; + if (!obj || !obj.selectable) return null; + + if (this.working_layer > -1 && obj.draw_layer !== this.working_layer) return null; + + if (obj.level !== this.edit_level) { + var testlevel = obj.level; + while (testlevel && testlevel.level !== this.edit_level) { + testlevel = testlevel.level; + } + + return testlevel; + } + + return obj; + }, + + curpanel: null, + + input_o_pressed() { + if (this.sel_comp) return; + if (Keys.ctrl() && Keys.alt()) { + if (this.selectlist.length === 1 && this.selectlist[0].file) { + if (this.edit_level.dirty) return; + this.load(this.selectlist[0].file); + } + + return; + } + + if (Keys.ctrl() && Keys.shift()) { + if (!this.edit_level.dirty) + this.load_prev(); + + return; + } + + if (Keys.ctrl()) { + if (this.check_level_nested()) + return; + + if (this.edit_level.dirty) { + this.openpanel(gen_notify("Level is changed. Are you sure you want to close it?", function() { + this.clear_level(); + this.openpanel(openlevelpanel); + }.bind(this))); + return; + } + + this.openpanel(openlevelpanel); + return; + } + + if (Keys.alt()) { + this.openpanel(addlevelpanel); + return; + } + + this.obj_panel = !this.obj_panel; + }, + + check_level_nested() { + if (this.edit_level.level) { + this.openpanel(gen_notify("Can't close a nested level. Save up to the root before continuing.")); + return true; + } + + return false; + }, + + levellist: false, + programmode: false, + + input_l_pressed() { + if (this.sel_comp) return; + if (Keys.ctrl()) { + texteditor.on_close = function() { editor.edit_level.script = texteditor.value;}; + texteditor.input_s_pressed = function() { + if (!Keys.ctrl()) return; + editor.edit_level.script = texteditor.value; + editor.save_current(); + texteditor.startbuffer = texteditor.value.slice(); + }; + + this.openpanel(texteditor); + if (!this.edit_level.script) + this.edit_level.script = ""; + texteditor.value = this.edit_level.script; + texteditor.start(); + } else if (Keys.alt()) { + this.programmode = !this.programmode; + } + }, + + delete_empty_reviver(key, val) { + if (typeof val === 'object' && val.empty) + return undefined; + + return val; + }, + + + input_minus_pressed() { + if (this.sel_comp) return; + + if (!this.selectlist.empty) { + this.selectlist.forEach(function(x) { x.draw_layer--; }); + return; + } + + if (this.working_layer > -1) + this.working_layer--; + }, + + input_plus_pressed() { + if (this.sel_comp) return; + + if (!this.selectlist.empty) { + this.selectlist.forEach(function(x) { x.draw_layer++; }); + return; + } + + if (this.working_layer < 4) + this.working_layer++; + }, + + input_p_pressed() { + if (Keys.ctrl()) { + if (Keys.shift()) { + /* Save prototype as */ + if (this.selectlist.length !== 1) return; + this.openpanel(saveprototypeas); + return; + } + else { + this.save_proto(); + } + } else if (Keys.alt()) + this.openpanel(prefabpanel); + }, + + save_proto() { + if (this.selectlist.length !== 1) return; + var protos = JSON.parse(slurp("proto.json")); + + var tobj = this.selectlist[0].prop_obj(); + var pobj = this.selectlist[0].__proto__.prop_obj(); + + deep_merge(pobj, tobj); + + pobj.from = this.selectlist[0].__proto__.from; + + protos[this.selectlist[0].__proto__.name] = pobj; + slurpwrite(JSON.stringify(protos, null, 2), "proto.json"); + + /* Save object changes to parent */ + dainty_assign(this.selectlist[0].__proto__, tobj); + + /* Remove the local from this object */ + unmerge(this.selectlist[0], tobj); + + /* Now sync all objects */ + Game.objects.forEach(function(x) { x.sync(); }); + }, + + save_prototypes() { + slurpwrite(JSON.stringify(gameobjects,null,2), "proto.json"); + }, + + /* Save the selected object as a new prototype, extending the chain */ + save_proto_as(name) { + if (name in gameobjects) { + Log.info("Already an object with name '" + name + "'. Choose another one."); + return; + } + + var newp = this.selectlist[0].__proto__.clone(name); + + for (var key in newp) + if (typeof newp[key] === 'object' && 'clone' in newp[key]) + newp[key] = newp[key].clone(); + + dainty_assign(newp, this.selectlist[0].prop_obj()); + this.selectlist[0].kill(); + var gopos = this.selectlist[0].pos; + this.unselect(); + var proto = this.edit_level.spawn(gameobjects[name]); + this.selectlist.push(proto); + this.save_proto(); + proto.pos = gopos; + }, + + /* Save selected object as a new prototype, replacing the current prototype */ + save_type_as(name) { + if (name in gameobjects) { + Log.info("Already an object with name '" + name + "'. Choose another one."); + return; + } + + var newp = this.selectlist[0].__proto__.__proto__.clone(name); + + for (var key in newp) + if (typeof newp[key] === 'object' && 'clone' in newp[key]) + newp[key] = newp[key].clone(); + + var tobj = this.selectlist[0].prop_obj(); + var pobj = this.selectlist[0].__proto__.prop_obj(); + deep_merge(pobj, tobj); + + + dainty_assign(newp, pobj); + this.selectlist[0].kill(); + this.unselect(); + var proto = this.edit_level.spawn(gameobjects[name]); + this.selectlist.push(proto); + this.save_proto(); + }, + + /* Makes it so only these objects are editable */ + focus_objects(objs) { + + }, + + dup_objects(x) { + var objs = x.slice(); + var duped = []; + + objs.forEach(function(x,i) { + if (x.file) { + var newlevel = this.edit_level.addfile(x.file); + newlevel.pos = x.pos; + newlevel.angle = x.angle; + duped.push(newlevel); + } else { + var newobj = this.edit_level.spawn(x.__proto__); + duped.push(newobj); + newobj.pos = x.pos; + newobj.angle = x.angle; + dainty_assign(newobj, x.prop_obj()); + } + },this); + + duped.forEach(function(x) { delete duped.varname; }); + + return duped.flat(); + }, + + input_d_pressed() { + if (!Keys.ctrl() || this.selectlist.length === 0) return; + var duped = this.dup_objects(this.selectlist); + this.unselect(); + this.selectlist = duped; + }, + + sel_start: [], + + input_lmouse_pressed() { + if (this.sel_comp) return; + + if (this.edit_mode === "brush") { + this.paste(); + return; + } + + if (this.selectlist.length === 1 && Keys.ctrl() && Keys.shift() && Keys.alt()) { + this.selectlist[0].set_center(screen2world(Mouse.pos)); + return; + } + + this.sel_start = screen2world(Mouse.pos); + }, + + points2cwh(start, end) { + var c = []; + c[0] = (end[0] - start[0]) / 2; + c[0] += start[0]; + c[1] = (end[1] - start[1]) / 2; + c[1] += start[1]; + var wh = []; + wh[0] = Math.abs(end[0] - start[0]); + wh[1] = Math.abs(end[1] - start[1]); + return {c: c, wh: wh}; + }, + + input_lmouse_released() { + Mouse.normal(); + if (!this.sel_start) return; + + if (this.sel_comp) { + this.sel_start = null; + return; + } + + var selects = []; + + /* TODO: selects somehow gets undefined objects in here */ + if (screen2world(Mouse.pos).equal(this.sel_start)) { + var sel = this.try_select(); + if (sel) selects.push(sel); + } else { + var box = this.points2cwh(this.sel_start, screen2world(Mouse.pos)); + + box.pos = box.c; + + var hits = physics.box_query(box); + + hits.forEach(function(x, i) { + var obj = this.do_select(x); + if (obj) + selects.push(obj); + },this); + + var levels = this.edit_level.objects.filter(function(x) { return x.file; }); + var lvlpos = []; + levels.forEach(function(x) { lvlpos.push(x.pos); }); + var lvlhits = physics.box_point_query(box, lvlpos); + + lvlhits.forEach(function(x) { selects.push(levels[x]); }); + } + + this.sel_start = null; + selects = selects.flat(); + selects = selects.unique(); + + if (selects.empty) return; + + if (Keys.shift()) { + selects.forEach(function(x) { + this.selectlist.push_unique(x); + }, this); + + return; + } + + if (Keys.ctrl()) { + selects.forEach(function(x) { + this.selectlist.remove(x); + }, this); + + return; + } + + this.selectlist = []; + selects.forEach(function(x) { + if (x !== null) + this.selectlist.push(x); + }, this); + }, + + mover(amt, snap) { + return function(go) { go.pos = go.pos.add(amt)}; + }, + + input_any_released() { + this.check_snapshot(); + this.edit_level.check_dirty(); + }, + + + step_amt() { return Keys.shift() ? 10 : 1; }, + + on_grid(pos) { + return pos.every(function(x) { return x % editor_config.grid_size === 0; }); + }, + + snapper(dir, grid) { + return function(go) { + go.pos = go.pos.add(dir.scale(grid/2)); + go.pos = go.pos.map(function(x) { return Math.snap(x, grid) }, this); + } + }, + + input_up_pressed() { + this.key_move([0,1]); + }, + + input_up_rep() { + this.input_up_pressed(); + }, + + input_left_pressed() { + this.key_move([-1,0]); + }, + + input_left_rep() { + this.input_left_pressed(); + }, + + key_move(dir) { + if (Keys.ctrl()) + this.selectlist.forEach(this.snapper(dir.scale(1.01), editor_config.grid_size)); + else + this.selectlist.forEach(this.mover(dir.scale(this.step_amt()))); + }, + + input_right_pressed() { + this.key_move([1,0]); + }, + + input_right_rep() { + this.input_right_pressed(); + }, + + input_down_pressed() { + this.key_move([0,-1]); + }, + + input_down_rep() { + this.input_down_pressed(); + }, + + /* Snapmode + 0 No snap + 1 Pixel snap + 2 Grid snap + */ + snapmode: 0, + + snapped_pos(pos) { + switch (this.snapmode) { + case 0: + return pos; + + case 1: + return pos.map(function(x) { return Math.round(x); }); + + case 2: + return pos.map + } + }, + + input_delete_pressed() { + this.selectlist.forEach(function(x) { x.kill(); }); + this.unselect(); + }, + + unselect() { + this.selectlist = []; + this.grabselect = null; + this.sel_comp = null; + }, + + sel_comp: null, + + input_m_pressed() { + if (this.sel_comp) return; + if (Keys.ctrl()) { + this.selectlist.forEach(function(x) { + x.flipy = !x.flipy; + }); + } else + this.selectlist.forEach(function(x) { + x.flipx = !x.flipx; + }); + + }, + + input_u_pressed() { + if (Keys.ctrl()) { + this.selectlist.forEach(function(x) { + x.revert(); + }); + } + + if (Keys.alt()) { + this.selectlist.forEach(function(x) { + x.unique = true; + }); + } + }, + + comp_info: false, + + input_q_pressed() { + this.comp_info = !this.comp_info; + }, + + brush_obj: null, + + input_b_pressed() { + if (this.sel_comp) return; + if (this.brush_obj) { + this.brush_obj = null; + return; + } + + if (this.selectlist.length !== 1) return; + this.brush_obj = this.selectlist[0]; + this.unselect(); + }, + + camera_recalls: {}, + + input_num_pressed(num) { + if (Keys.ctrl()) { + this.camera_recalls[num] = { + pos:this.camera.pos, + zoom:this.camera.zoom + }; + return; + } + + if (Keys.alt()) { + switch(num) { + case 0: + Render.normal(); + break; + + case 2: + Render.wireframe(); + break; + } + + return; + } + + if (num === 0) { + this.camera.pos = [0,0]; + this.camera.zoom = 1; + return; + } + + if (num in this.camera_recalls) + Object.assign(this.camera, this.camera_recalls[num]); + }, + + zoom_to_bb(bb) { + var cwh = bb2cwh(bb); + + var xscale = cwh.wh.x / Window.width; + var yscale = cwh.wh.y / Window.height; + + var zoom = yscale > xscale ? yscale : xscale; + + this.camera.pos = cwh.c; + this.camera.zoom = zoom*1.3; + }, + + input_f_pressed() { + if (!Keys.ctrl()) { + if (this.selectlist.length === 0) return; + var bb = this.selectlist[0].boundingbox; + this.selectlist.forEach(function(obj) { bb = bb_expand(bb, obj.boundingbox); }); + this.zoom_to_bb(bb); + + return; + } + + if (Keys.shift()) { + if (!this.edit_level.level) return; + + this.edit_level = this.edit_level.level; + this.unselect(); + this.reset_undos(); + this.curlvl = this.edit_level.save(); + this.edit_level.filejson = this.edit_level.save(); + this.edit_level.check_dirty(); + } else { + if (this.selectlist.length === 1) { + if (!this.selectlist[0].file) return; + this.edit_level = this.selectlist[0]; + this.unselect(); + this.reset_undos(); + this.curlvl = this.edit_level.save(); + } + } + }, + + input_rmouse_down() { + if (Keys.ctrl() && Keys.alt()) + this.camera.zoom = this.z_start * (1 + (Mouse.pos[1] - this.mousejoy[1])/500); + }, + + z_start: 1, + + input_rmouse_released() { + Mouse.normal(); + }, + + input_rmouse_pressed() { + if (Keys.shift()) { + this.cursor = null; + return; + } + + if (Keys.ctrl() && Keys.alt()) { + this.mousejoy = Mouse.pos; + this.z_start = this.camera.zoom; + Mouse.disabled(); + return; + } + + if (this.brush_obj) + this.brush_obj = null; + + if (this.sel_comp) { + this.sel_comp = null; + return; + } + + this.unselect(); + }, + + grabselect: null, + mousejoy: [0,0], + joystart: [0,0], + + input_mmouse_pressed() { + if (Keys.ctrl() && Keys.alt()) { + this.mousejoy = Mouse.pos; + this.joystart = this.camera.pos; + return; + } + + if (Keys.shift() && Keys.ctrl()) { + this.cursor = find_com(this.selectlist); + return; + } + + if (this.brush_obj) { + this.selectlist = this.dup_objects([this.brush_obj]); + this.selectlist[0].pos = screen2world(Mouse.pos); + this.grabselect = this.selectlist[0]; + return; + } + + if (this.sel_comp && 'pick' in this.sel_comp) { + this.grabselect = this.sel_comp.pick(screen2world(Mouse.pos)); + if (!this.grabselect) return; + + this.moveoffset = this.sel_comp.gameobject.this2world(this.grabselect).sub(screen2world(Mouse.pos)); + return; + } + + var grabobj = this.try_select(); + if (Array.isArray(grabobj)) { + this.selectlist = grabobj; + return; + } + this.grabselect = null; + if (!grabobj) return; + + if (Keys.ctrl()) { + grabobj = this.dup_objects([grabobj])[0]; + } + + this.grabselect = grabobj; + + if (!this.selectlist.includes(grabobj)) { + this.selectlist = []; + this.selectlist.push(grabobj); + } + + this.moveoffset = this.grabselect.pos.sub(screen2world(Mouse.pos)); + }, + + input_mmouse_released() { + Mouse.normal(); + this.grabselect = null; + }, + + input_mmouse_down() { + if (Keys.shift() && !Keys.ctrl()) { + this.cursor = Mouse.worldpos; + return; + } + + if (Keys.alt() && Keys.ctrl()) { + this.camera.pos = this.joystart.add(Mouse.pos.sub(this.mousejoy).mapc(mult, [-1,1]).scale(editor_camera.zoom)); + return; + } + + if (!this.grabselect) return; + + if ('pos' in this.grabselect) + this.grabselect.pos = this.moveoffset.add(screen2world(Mouse.pos)); + else + this.grabselect.set(this.selectlist[0].world2this(this.moveoffset.add(screen2world(Mouse.pos)))); + }, + + + stash: "", + + start_play_ed() { + this.stash = this.edit_level.save(); + this.edit_level.kill(); + load_configs("game.config"); + game.start(); + unset_pawn(this); + set_pawn(limited_editor); + Register.unregister_obj(this); + }, + + input_f5_pressed() { + /* Start debug.lvl */ + if (!sim_playing()) { + this.start_play_ed(); + Level.loadlevel("debug_start.lvl"); + } + }, + + input_f6_pressed() { + /* Start game.lvl */ + if (!sim_playing()) { + this.start_play_ed(); +// Level.loadlevel( + /* Load level of what was being edited */ + } + }, + + input_f7_pressed() { + /* Start current level */ + if (!sim_playing()) { + this.start_play_ed(); + Level.loadlevel("game.lvl"); + } + }, + + input_escape_pressed() { + this.openpanel(quitpanel); + }, + + moveoffsets: [], + + input_g_pressed() { + if (Keys.ctrl() && Keys.shift()) { + Log.info("Saving as level..."); + this.openpanel(groupsaveaspanel); + return; + } + + if (Keys.alt() && this.cursor) { + var com = find_com(this.selectlist); + this.selectlist.forEach(function(x) { + x.pos = x.pos.sub(com).add(this.cursor); + },this); + } + + if (this.sel_comp) { + if ('pos' in this.sel_comp) + this.moveoffset = this.sel_comp.pos.sub(screen2world(Mouse.pos)); + + return; + } + + if (Keys.ctrl()) { + this.selectlist = this.dup_objects(this.selectlist); + } + + this.selectlist.forEach(function(x,i) { + this.moveoffsets[i] = x.pos.sub(screen2world(Mouse.pos)); + }, this); + }, + + input_g_down() { + if (Keys.alt()) return; + if (this.sel_comp) { + this.sel_comp.pos = this.moveoffset.add(screen2world(Mouse.pos)); + return; + } + + if (this.moveoffsets.length === 0) return; + + this.selectlist.forEach(function(x,i) { + x.pos = this.moveoffsets[i].add(screen2world(Mouse.pos)); + }, this); + }, + + input_g_released() { + this.moveoffsets = []; + }, + + scaleoffset: 0, + startscales: [], + selected_com: [0,0], + + openpanel(panel, dontsteal) { + if (this.curpanel) + this.curpanel.close(); + + this.curpanel = panel; + + var stolen = this; + if (dontsteal) + stolen = null; + + this.curpanel.open(stolen); + }, + + curpanels: [], + addpanel(panel) { + this.curpanels.push(panel); + panel.open(); + }, + + cleanpanels(panel) { + this.curpanels = this.curpanels.filter(function(x) { return x.on; }); + }, + + input_s_pressed() { + if (Keys.ctrl()) { + if (Keys.shift()) { + this.openpanel(saveaspanel); + return; + } + + if (this.edit_level.level) { + if (!this.edit_level.unique) + this.save_current(); + + this.selectlist = []; + this.selectlist.push(this.edit_level); + this.edit_level = this.edit_level.level; + + return; + } + + this.save_current(); + return; + } + + var offf = this.cursor ? this.cursor : this.selected_com; + this.scaleoffset = Vector.length(Mouse.worldpos.sub(offf)); + + if (this.sel_comp) { + if (!('scale' in this.sel_comp)) return; + this.startscales = []; + this.startscales.push(this.sel_comp.scale); + return; + } + + this.selectlist.forEach(function(x, i) { + this.startscales[i] = x.scale; + if (this.cursor) + this.startoffs[i] = x.pos.sub(this.cursor); + }, this); + }, + + input_s_down() { + if (!this.scaleoffset) return; + var offf = this.cursor ? this.cursor : this.selected_com; + var dist = Vector.length(screen2world(Mouse.pos).sub(offf)); + var scalediff = dist/this.scaleoffset; + + if (this.sel_comp) { + if (!('scale' in this.sel_comp)) return; + this.sel_comp.scale = this.startscales[0] * scalediff; + return; + } + + this.selectlist.forEach(function(x, i) { + x.scale = this.startscales[i] * scalediff; + if (this.cursor) + x.pos = this.cursor.add(this.startoffs[i].scale(scalediff)); + }, this); + }, + + input_s_released() { + this.scaleoffset = null; + }, + + startrots: [], + startpos: [], + startoffs: [], + + input_r_pressed() { + this.startrots = []; + + if (Keys.ctrl()) { + this.selectlist.forEach(function(x) { + x.angle = -x.angle; + }); + return; + } + + if (this.sel_comp && 'angle' in this.sel_comp) { + var relpos = screen2world(Mouse.pos).sub(this.sel_comp.gameobject.pos); + this.startoffset = Math.atan2(relpos.y, relpos.x); + this.startrot = this.sel_comp.angle; + + return; + } + + var offf = this.cursor ? this.cursor : this.selected_com; + var relpos = screen2world(Mouse.pos).sub(offf); + this.startoffset = Math.atan2(relpos[1], relpos[0]); + + this.selectlist.forEach(function(x, i) { + this.startrots[i] = x.angle; + this.startpos[i] = x.pos; + this.startoffs[i] = x.pos.sub(offf); + }, this); + }, + + input_r_down() { + if (this.sel_comp && 'angle' in this.sel_comp) { + if (!('angle' in this.sel_comp)) return; + var relpos = screen2world(Mouse.pos).sub(this.sel_comp.gameobject.pos); + var anglediff = Math.rad2deg(Math.atan2(relpos.y, relpos.x)) - this.startoffset; + this.sel_comp.angle = this.startrot + anglediff; + return; + } + if (this.startrots.empty) return; + + var offf = this.cursor ? this.cursor : this.selected_com; + var relpos = screen2world(Mouse.pos).sub(offf); + var anglediff = Math.rad2deg(Math.atan2(relpos[1], relpos[0]) - this.startoffset); + + if (this.cursor) { + this.selectlist.forEach(function(x, i) { + x.angle = this.startrots[i] + anglediff; + x.pos = offf.add(this.startoffs[i].rotate(Math.deg2rad(anglediff))); + }, this); + } else { + this.selectlist.forEach(function(x,i) { + x.angle = this.startrots[i]+anglediff; + }, this); + } + }, + + snapshots: [], + curlvl: {}, /* What the level currently looks like on file */ + + reset_undos() { + this.snapshots = []; + this.backshots = []; + }, + + check_snapshot() { + if (!this.selectlist.empty || this.grabselect) this.snapshot(); + }, + + snapshot() { + var cur = this.edit_level.save(); + var dif = diff(cur, this.curlvl); + + if (dif.empty) return; + + this.snapshots.push(this.curlvl); + this.backshots = []; + this.curlvl = cur; + return; + + this.snapshots.push(dif); + + this.backshots = []; + this.backshots.push(diff(this.curlvl, cur)); + this.curlvl = cur; + this.edit_level.check_dirty(); + }, + + backshots: [], /* Redo snapshots */ + + restore_lvl(lvl) { + this.unselect(); + this.edit_level.clear(); + this.edit_level.load(lvl); + this.edit_level.check_dirty(); + }, + + diff_lvl(d) { + this.unselect(); + for (var key in d) { + if (d[key] === "DELETE") + Game.objects[key].kill(); + } + diffassign(Game.objects, d); + Game.objects.forEach(function(x) { x.sync(); }); + this.curlvl = this.edit_level.save(); + }, + + redo() { + if (this.backshots.empty) { + Log.info("Nothing to redo."); + return; + } + + this.snapshots.push(this.edit_level.save()); + var dd = this.backshots.pop(); + this.edit_level.clear(); + this.edit_level.load(dd); + this.edit_level.check_dirty(); + this.curlvl = dd; + return; + + var dd = this.backshots.pop(); + this.snapshots.push(dd); + if (this.was_undoing) { + dd = this.backshots.pop(); + this.snapshots.push(dd); + this.was_undoing = false; + } + + this.diff_lvl(dd); + }, + + undo() { + if (this.snapshots.empty) { + Log.info("Nothing to undo."); + return; + } + this.unselect(); + this.backshots.push(this.edit_level.save()); + var dd = this.snapshots.pop(); + this.edit_level.clear(); + this.edit_level.load(dd); + this.edit_level.check_dirty(); + this.curlvl = dd; + return; + + this.backshots.push(dd); + + if (!this.was_undoing) { + dd = this.snapshots.pop(); + this.backshots.push(dd); + this.was_undoing = true; + } + + this.diff_lvl(dd); + }, + + restore_buffer() { + this.restore_level(this.filesnap); + }, + + input_z_pressed() { + if (Keys.ctrl()) { + if (Keys.shift()) + this.redo(); + else + this.undo(); + } + }, + + save_current() { + if (!this.edit_level.file) { + this.openpanel(saveaspanel); + return; + } + + var lvl = this.edit_level.save(); + + Log.info("Saving level of size " + lvl.length + " bytes."); + + slurpwrite(lvl, this.edit_level.file); + this.edit_level.filejson = lvl; + this.edit_level.check_dirty(); + + if (this.edit_level.script) { + var scriptfile = this.edit_level.file.replace('.lvl', '.js'); + slurpwrite(this.edit_level.script, scriptfile); + } + + Level.sync_file(this.edit_level.file); + }, + + input_t_pressed() { + if (Keys.ctrl()) { + if (Keys.shift()) { + if (this.selectlist.length !== 1) return; + this.openpanel(savetypeas); + return; + } + } + + this.selectlist.forEach(function(x) { x.selectable = false; }); + + if (Keys.alt()) + this.edit_level.objects.forEach(function(x) { x.selectable = true; }); + }, + + clear_level() { + Log.info("Closed level."); + + if (this.edit_level.level) { + this.openpanel(gen_notify("Can't close a nested level. Save up to the root before continuing.")); + return; + } + + this.unselect(); + this.edit_level.kill(); + this.edit_level = Level.create(); + }, + + input_n_pressed() { + if (!Keys.ctrl()) return; + + if (this.edit_level.dirty) { + Log.warn("Level has changed; save before starting a new one."); + this.openpanel(gen_notify("Level is changed. Are you sure you want to close it?", _ => this.clear_level())); + return; + } + + this.clear_level(); + }, + + set timescale(x) { cmd(3, x); }, + set updateMS(x) { cmd(6, x); }, + set physMS(x) { cmd(7, x); }, + set renderMS(x) { cmd(5, x); }, + set dbg_draw_phys(x) { cmd(4, x); }, + set dbg_color(x) { cmd(16, x); }, + set trigger_color(x) { cmd(17, x); }, + get fps() { return sys_cmd(8); }, + input_f10_pressed() { this.timescale = 0.1; }, + input_f10_released() { this.timescale = 1.0; }, + + input_1_pressed() { + if (!Keys.alt()) return; + Render.normal(); + }, + + input_2_pressed() { + if (!Keys.alt()) return; + Render.wireframe(); + }, + + _sel_comp: null, + get sel_comp() { return this._sel_comp; }, + set sel_comp(x) { + if (this._sel_comp) + unset_pawn(this._sel_comp); + + this._sel_comp = x; + + if (this._sel_comp) { + Log.info("sel comp is now " + this._sel_comp); + set_pawn(this._sel_comp); + } + }, + + input_tab_pressed() { + if (!this.selectlist.length === 1) return; + if (!this.selectlist[0].components) return; + + var sel = this.selectlist[0].components; + + if (!this.sel_comp) + this.sel_comp = sel.nth(0); + else { + var idx = sel.findIndex(this.sel_comp) + 1; + if (idx >= Object.keys(sel).length) + this.sel_comp = null; + else + this.sel_comp = sel.nth(idx); + } + + }, + + draw_gizmos: true, + + input_f4_pressed() { this.draw_gizmos = !this.draw_gizmos; }, + time: 0, + + ed_gui() { + + this.time = Date.now(); + /* Clean out killed objects */ + this.selectlist = this.selectlist.filter(function(x) { return x.alive; }); + + GUI.text("WORKING LAYER: " + this.working_layer, [0,520], 1); + GUI.text("MODE: " + this.edit_mode, [0,500],1); + + Debug.point(world2screen(this.edit_level.pos), 5, Color.yellow); + if (this.cursor) { + Debug.point(world2screen(this.cursor), 5, Color.green); + + this.selectlist.forEach(function(x) { + var p = []; + p.push(world2screen(this.cursor)); + p.push(world2screen(x.pos)); + Debug.line(p, Color.green); + },this); + } + + if (this.programmode) { + this.edit_level.objects.forEach(function(x) { + if (x.hasOwn('varname')) GUI.text(x.varname, world2screen(x.pos).add([0,32]), 1, [84,110,255]); + }); + } + + if (this.comp_info && this.sel_comp && 'help' in this.sel_comp) { + GUI.text(this.sel_comp.help, [100,700],1); + } + + gui_text("0,0", world2screen([0,0]), 1); + + if (this.draw_gizmos) + Game.objects.forEach(function(x) { + if (!x.icon) return; + gui_img(x.icon, world2screen(x.pos)); + }); + + gui_text(sim_playing() ? "PLAYING" + : sim_paused() ? + "PAUSED" : + "STOPPED", [0, 0], 1); + + gui_text("FPS " + this.fps, [0, 540], 1); + + var clvl = this.edit_level; + var ypos = 200; + var lvlcolor = Color.white; + while (clvl) { + var lvlstr = clvl.file ? clvl.file : "NEW LEVEL"; + if (clvl.unique) + lvlstr += "#"; + else if (clvl.dirty) + lvlstr += "*"; + GUI.text(lvlstr, [0, ypos], 1, lvlcolor); + + lvlcolor = Color.gray; + drawarrows = true; + clvl = clvl.level; + if (clvl) { + GUI.text("^^^^^^", [0,ypos+5],1); + ypos += 5; + } + ypos += 15; + } + + this.edit_level.objects.forEach(function(x) { + if ('ed_gizmo' in x) + x.ed_gizmo(); + }); + + if (Debug.phys_drawing) + this.edit_level.objects.forEach(function(x) { + Debug.point(world2screen(x.pos), 2, Color.teal); + }); + + this.selectlist.forEach(function(x) { + var color = x.color ? x.color : [255,255,255]; + GUI.text(x.toString(), world2screen(x.pos).add([0, 16]), 1, color); + GUI.text(x.pos.map(function(x) { return Math.round(x); }), world2screen(x.pos), 1, color); + Debug.arrow(world2screen(x.pos), world2screen(x.pos.add(x.up.scale(40))), Color.yellow, 1); + if (x.hasOwn('varname')) GUI.text(x.varname, world2screen(x.pos).add([0,32]), 1, [84,110,255]); + if ('gizmo' in x && typeof x['gizmo'] === 'function' ) + x.gizmo(); + + if (x.hasOwn('file')) + x.objects.forEach(function(x) { + GUI.text(x.toString(), world2screen(x.pos).add([0,16]),1,Color.blue); + }); + }); + + + if (this.selectlist.length === 1) { + var i = 1; + for (var key in this.selectlist[0].components) { + var selected = this.sel_comp === this.selectlist[0].components[key]; + var str = (selected ? ">" : " ") + key + " [" + this.selectlist[0].components[key].name + "]"; + gui_text(str, world2screen(this.selectlist[0].pos).add([0,-16*(i++)]), 1); + } + + if (this.sel_comp) { + if ('gizmo' in this.sel_comp) this.sel_comp.gizmo(); + } + } + + Game.objects.forEach(function(obj) { + if (!obj.selectable) + gui_img("icons/icons8-lock-16.png", world2screen(obj.pos)); + }); + + Debug.draw_grid(1, editor_config.grid_size/editor_camera.zoom); + + var startgrid = screen2world([-20,Window.height]).map(function(x) { return Math.snap(x, editor_config.grid_size); }, this); + var endgrid = screen2world([Window.width, 0]); + + while(startgrid[0] <= endgrid[0]) { + gui_text(startgrid[0], [world2screen([startgrid[0], 0])[0], 1], 1); + startgrid[0] += editor_config.grid_size; + } + + while(startgrid[1] <= endgrid[1]) { + gui_text(startgrid[1], [0, world2screen([0, startgrid[1]])[1]], 1); + startgrid[1] += editor_config.grid_size; + } + + Debug.point(world2screen(this.selected_com), 3); + this.selected_com = find_com(this.selectlist); + + if (this.draw_bb) { + Game.objects.forEach(function(x) { bb_draw(x.boundingbox); }); + } + + /* Draw selection box */ + if (this.sel_start) { + var endpos = screen2world(Mouse.pos); + var c = []; + c[0] = (endpos[0] - this.sel_start[0]) / 2; + c[0] += this.sel_start[0]; + c[1] = (endpos[1] - this.sel_start[1]) / 2; + c[1] += this.sel_start[1]; + var wh = []; + wh[0] = Math.abs(endpos[0] - this.sel_start[0]); + wh[1] = Math.abs(endpos[1] - this.sel_start[1]); + wh[0] /= editor.camera.zoom; + wh[1] /= editor.camera.zoom; + Debug.box(world2screen(c), wh); + } + + if (this.curpanel && this.curpanel.on) + this.curpanel.gui(); + + this.curpanels.forEach(function(x) { + if (x.on) x.gui(); + }); + + if (this.repl) { + Nuke.window("repl"); + Nuke.newrow(500); + var log = cmd(84); + var f = log.prev('\n', 0, 10); + Nuke.scrolltext(log.slice(f)); + + Nuke.end(); + } + + }, + + input_lbracket_pressed() { + if (!Keys.ctrl()) return; + editor_config.grid_size -= Keys.shift() ? 10 : 1; + if (editor_config.grid_size <= 0) editor_config.grid_size = 1; + }, + input_lbracket_rep() { this.input_lbracket_pressed(); }, + + input_rbracket_pressed() { + if (!Keys.ctrl()) return; + editor_config.grid_size += Keys.shift() ? 10 : 1; + }, + + input_rbracket_rep() { this.input_rbracket_pressed(); }, + + grid_size: 100, + + ed_debug() { + if (!Debug.phys_drawing) + this.selectlist.forEach(function(x) { Debug.draw_obj_phys(x); }); + }, + + viewasset(path) { + Log.info(path); + var fn = function(x) { return path.endsWith(x); }; + if (images.any(fn)) { + var newtex = copy(texgui, { path: path }); + this.addpanel(newtex); + } + else if (sounds.any(fn)) + Log.info("selected a sound"); + }, + + killring: [], + killcom: [], + + input_c_pressed() { + if (this.sel_comp) return; + if (!Keys.ctrl()) return; + this.killring = []; + this.killcom = []; + + this.selectlist.forEach(function(x) { + this.killring.push(x); + },this); + + this.killcom = find_com(this.killring); + }, + + input_x_pressed() { + if (!Keys.ctrl()) return; + + this.input_c_pressed(); + this.killring.forEach(function(x) { x.kill(); }); + }, + + paste() { + this.selectlist = this.dup_objects(this.killring); + + var setpos = this.cursor ? this.cursor : Mouse.worldpos; + this.selectlist.forEach(function(x) { + x.pos = x.pos.sub(this.killcom).add(setpos); + },this); + }, + + input_v_pressed() { + if (!Keys.ctrl()) return; + this.paste(); + }, + + input_e_pressed() { + if (Keys.ctrl()) { + this.openpanel(assetexplorer); + return; + } + }, + + lvl_history: [], + + load(file) { + if (this.edit_level) this.lvl_history.push(this.edit_level.file); + this.edit_level.clear(); + this.edit_level = Level.loadfile(file); + this.curlvl = this.edit_level.save(); + this.unselect(); + }, + + load_prev() { + if (this.lvl_history.length === 0) return; + + var file = this.lvl_history.pop(); + this.edit_level = Level.loadfile(file); + this.unselect(); + }, + + addlevel(file, pos) { + Log.info("adding file " + file + " at pos " + pos); + var newlvl = this.edit_level.addfile(file); + newlvl.pos = pos; + this.selectlist = []; + this.selectlist.push(newlvl); + return; + }, + + load_json(json) { + this.edit_level.load(json); + this.curlvl = this.edit_level.save(); + this.unselect(); + }, + + groupsaveas(group, file) { + if (!file) return; + + file = file+".lvl"; + if (IO.exists(file)) { + this.openpanel(gen_notify("Level already exists with that name. Overwrite?", dosave.bind(this,file))); + return; + } else + dosave(file); + + function dosave(file) { + var com = find_com(group); + Level.saveas(group, file); + editor.addlevel(file, com); + + group.forEach(function(x) { x.kill(); }); + } + }, + + saveas_check(file) { + if (!file) return; + + if (!file.endsWith(".lvl")) + file = file + ".lvl"; + + if (IO.exists(file)) { + notifypanel.action = editor.saveas; + this.openpanel(gen_notify("Level already exists with that name. Overwrite?", this.saveas.bind(this, file))); + } else + this.saveas(file); + }, + + saveas(file) { + if (!file) return; + + Log.info("made it"); + + this.edit_level.file = file; + this.save_current(); + }, + + input_h_pressed() { + if (this.sel_comp) return; + if (Keys.ctrl()) { + Game.objects.forEach(function(x) { x.visible = true; }); + } else { + var visible = true; + this.selectlist.forEach(function(x) { if (x.visible) visible = false; }); + this.selectlist.forEach(function(x) { x.visible = visible; }); + } + }, + + input_a_pressed() { + if (Keys.alt()) { + this.openpanel(prefabpanel); + return; + } + if (!Keys.ctrl()) return; + if (!this.selectlist.empty) { + this.unselect(); + return; + } + this.unselect(); + + this.selectlist = this.edit_level.objects.slice(); + }, + + repl: false, + input_backtick_pressed() { this.repl = !this.repl; }, + + draw_bb: false, + input_f3_pressed() { + this.draw_bb = !this.draw_bb; + }, +} + +var inputpanel = { + title: "untitled", + value: "", + on: false, + stolen: {}, + + gui() { + Nuke.window(this.title); + Nuke.newline(); + this.guibody(); + + if (Nuke.button("close")) + this.close(); + + Nuke.end(); + return false; + }, + + guibody() { + this.value = Nuke.textbox(this.value); + + Nuke.newline(2); + if (Nuke.button("submit")) { + this.submit(); + return true; + } + }, + + open(steal) { + this.on = true; + this.value = ""; + if (steal) { + this.stolen = steal; + unset_pawn(this.stolen); + set_pawn(this); + } + this.start(); + this.keycb(); + }, + + start() {}, + + + close() { + unset_pawn(this); + if (this.stolen) { + set_pawn(this.stolen); + this.stolen = null; + } + + this.on = false; + if ('on_close' in this) + this.on_close(); + }, + + action() { + + }, + + closeonsubmit: true, + submit() { + if (!this.submit_check()) return; + this.action(); + if (this.closeonsubmit) + this.close(); + }, + + submit_check() { return true; }, + + input_enter_pressed() { + this.submit(); + }, + + input_text(char) { + this.value += char; + this.keycb(); + }, + + keycb() {}, + + input_backspace_pressrep() { + this.value = this.value.slice(0,-1); + this.keycb(); + }, + + input_escape_pressed() { + this.close(); + }, +}; + +function proto_count_lvls(name) +{ + if (!this.occs) this.occs = {}; + if (name in this.occs) return this.occs[name]; + var lvls = IO.extensions("lvl"); + var occs = {}; + var total = 0; + lvls.forEach(function(lvl) { + var json = JSON.parse(IO.slurp(lvl)); + var count = 0; + json.forEach(function(x) { if (x.from === name) count++; }); + occs[lvl] = count; + total += count; + }); + + this.occs[name] = occs; + this.occs[name].total = total; + + return this.occs[name]; +} +proto_count_lvls = proto_count_lvls.bind(proto_count_lvls); + +function proto_used(name) { + var occs = proto_count_lvls(name); + var used = false; + occs.forEach(function(x) { if (x > 0) used = true; }); + + Log.info(used); +} + +function proto_total_use(name) { + return proto_count_lvls(name).total; +} + +function proto_children(name) { + var children = []; + + for (var key in gameobjects) + if (gameobjects[key].from === name) children.push(gameobjects[key]); + + return children; +} + +var texteditor = clone(inputpanel, { + title: "text editor", + _cursor:0, /* Text cursor: [char,line] */ + get cursor() { return this._cursor; }, + set cursor(x) { + if (x > this.value.length) + x = this.value.length; + if (x < 0) + x = 0; + + this._cursor = x; + this.line = this.get_line(); + }, + + submit() {}, + + line: 0, + killring: [], + undos: [], + startbuffer: "", + + savestate() { + this.undos.push(this.value.slice()); + }, + + popstate() { + if (this.undos.length === 0) return; + this.value = this.undos.pop(); + this.cursor = this.cursor; + }, + + input_s_pressed() { + if (!Keys.ctrl()) return; + Log.info("SAVING"); + editor.save_current(); + }, + + input_u_pressrep() { + if (!Keys.ctrl()) return; + this.popstate(); + }, + + copy(start, end) { + return this.value.slice(start,end); + }, + + input_q_pressed() { + if (!Keys.ctrl()) return; + + var ws = this.prev_word(this.cursor); + var we = this.end_of_word(this.cursor)+1; + var find = this.copy(ws, we); + var obj = editor.edit_level.varname2obj(find); + + if (obj) { + editor.unselect(); + editor.selectlist.push(obj); + } + }, + + delete_line(p) { + var ls = this.line_start(p); + var le = this.line_end(p)+1; + this.cut_span(ls,le); + this.to_line_start(); + }, + + line_blank(p) { + var ls = this.line_start(p); + var le = this.line_end(p); + var line = this.value.slice(ls, le); + if (line.search(/[^\s]/g) === -1) + return true; + else + return false; + }, + + input_o_pressed() { + if (Keys.alt()) { + /* Delete all surrounding blank lines */ + while (this.line_blank(this.next_line(this.cursor))) + this.delete_line(this.next_line(this.cursor)); + + while (this.line_blank(this.prev_line(this.cursor))) + this.delete_line(this.prev_line(this.cursor)); + } else if (Keys.ctrl()) { + this.insert_char('\n'); + this.cursor--; + } + }, + + get_line() { + var line = 0; + for (var i = 0; i < this.cursor; i++) + if (this.value[i] === "\n") + line++; + + return line; + }, + + start() { + this.cursor = 0; + this.startbuffer = this.value.slice(); + }, + + get dirty() { + return this.startbuffer !== this.value; + }, + + gui() { + GUI.text_cursor(this.value, [100,700],1,this.cursor+1); + GUI.text("C" + this.cursor + ":::L" + this.line + ":::" + (this.dirty ? "DIRTY" : "CLEAN"), [100,100], 1); + }, + + insert_char(char) { + this.value = this.value.slice(0,this.cursor) + char + this.value.slice(this.cursor); + this.cursor++; + }, + + input_enter_pressrep() { + var white = this.line_starting_whitespace(this.cursor); + this.insert_char('\n'); + + for (var i = 0; i < white; i++) + this.insert_char(" "); + + }, + + input_text(char) { + if (Keys.ctrl() || Keys.alt()) return; + this.insert_char(char); + this.keycb(); + }, + + input_d_pressrep() { + if (Keys.ctrl()) + this.value = this.value.slice(0,this.cursor) + this.value.slice(this.cursor+1); + else if (Keys.alt()) + this.cut_span(this.cursor, this.end_of_word(this.cursor)+1); + }, + + input_backspace_pressrep() { + this.value = this.value.slice(0,this.cursor-1) + this.value.slice(this.cursor); + this.cursor--; + }, + + input_a_pressed() { + if (Keys.ctrl()) { + this.to_line_start(); + this.desired_inset = this.inset; + } + }, + + input_y_pressed() { + if (!Keys.ctrl()) return; + if (this.killring.length === 0) return; + this.insert_char(this.killring.pop()); + }, + + line_starting_whitespace(p) { + var white = 0; + var l = this.line_start(p); + + while (this.value[l] === " ") { + white++; + l++; + } + + return white; + }, + + input_e_pressed() { + if (Keys.ctrl()) { + this.to_line_end(); + this.desired_inset = this.inset; + } + }, + + input_k_pressrep() { + if (Keys.ctrl()) { + if (this.cursor === this.value.length-1) return; + var killamt = this.value.next('\n', this.cursor) - this.cursor; + var killed = this.cut_span(this.cursor-1, this.cursor+killamt); + this.killring.push(killed); + } else if (Keys.alt()) { + var prevn = this.value.prev('\n', this.cursor); + var killamt = this.cursor - prevn; + var killed = this.cut_span(prevn+1, prevn+killamt); + this.killring.push(killed); + this.to_line_start(); + } + }, + + input_b_pressrep() { + if (Keys.ctrl()) { + this.cursor--; + } else if (Keys.alt()) { + this.cursor = this.prev_word(this.cursor-2); + } + + this.desired_inset = this.inset; + }, + + input_f_pressrep() { + if (Keys.ctrl()) { + this.cursor++; + } else if (Keys.alt()) { + this.cursor = this.next_word(this.cursor); + } + + this.desired_inset = this.inset; + }, + + cut_span(start, end) { + if (end < start) return; + this.savestate(); + var ret = this.value.slice(start,end); + this.value = this.value.slice(0,start) + this.value.slice(end); + if (start > this.cursor) + return ret; + + this.cursor -= ret.length; + return ret; + }, + + next_word(pos) { + var v = this.value.slice(pos+1).search(/[^\w]\w/g); + if (v === -1) return pos; + return pos + v + 2; + }, + + prev_word(pos) { + while (this.value.slice(pos,pos+2).search(/[^\w]\w/g) === -1 && pos > 0) + pos--; + + return pos+1; + }, + + end_of_word(pos) { + var l = this.value.slice(pos).search(/\w[^\w]/g); + return l+pos; + }, + + get inset() { + return this.cursor - this.value.prev('\n', this.cursor) - 1; + }, + + line_start(p) { + return this.value.prev('\n', p)+1; + }, + + line_end(p) { + return this.value.next('\n', p); + }, + + next_line(p) { + return this.value.next('\n',p)+1; + }, + + prev_line(p) { + return this.line_start(this.value.prev('\n', p)); + }, + + to_line_start() { + this.cursor = this.value.prev('\n', this.cursor)+1; + }, + + to_line_end() { + var p = this.value.next('\n', this.cursor); + if (p === -1) + this.to_file_end(); + else + this.cursor = p; + }, + + line_width(pos) { + var start = this.line_start(pos); + var end = this.line_end(pos); + if (end === -1) + end = this.value.length; + + return end-start; + }, + + to_file_end() { this.cursor = this.value.length; }, + + to_file_start() { this.cursor = 0; }, + + desired_inset: 0, + + input_p_pressrep() { + if (Keys.ctrl()) { + if (this.cursor === 0) return; + this.desired_inset = Math.max(this.desired_inset, this.inset); + this.cursor = this.prev_line(this.cursor); + var newlinew = this.line_width(this.cursor); + this.cursor += Math.min(this.desired_inset, newlinew); + } else if (Keys.alt()) { + while (this.line_blank(this.cursor)) + this.cursor = this.prev_line(this.cursor); + + while (!this.line_blank(this.cursor)) + this.cursor = this.prev_line(this.cursor); + } + }, + + input_n_pressrep() { + if (Keys.ctrl()) { + if (this.cursor === this.value.length-1) return; + if (this.value.next('\n', this.cursor) === -1) { + this.to_file_end(); + return; + } + + this.desired_inset = Math.max(this.desired_inset, this.inset); + this.cursor = this.next_line(this.cursor); + var newlinew = this.line_width(this.cursor); + this.cursor += Math.min(this.desired_inset, newlinew); + } else if (Keys.alt()) { + while (this.line_blank(this.cursor)) + this.cursor = this.next_line(this.cursor); + + while (!this.line_blank(this.cursor)) + this.cursor = this.next_line(this.cursor); + } + }, + +}); + +var protoexplorer = copy(inputpanel, { + title: "prototype explorer", + waitclose:false, + + guibody() { + var treeleaf = function(name) { + for (var key in gameobjects) { + if (gameobjects[key].from === name) { + var goname = gameobjects[key].name; + if (Nuke.tree(goname)) { + var lvluses = proto_total_use(goname); + var childcount = proto_children(goname).length; + if (lvluses === 0 && childcount === 0 && gameobjects[key].from !== 'gameobject') { + if (Nuke.button("delete")) { + Log.info("Deleting the prototype " + goname); + delete gameobjects[goname]; + editor.save_prototypes(); + } + } else { + if (Nuke.tree("Occurs " + lvluses + " times in levels.")) { + var occs = proto_count_lvls(goname); + for (var lvl in occs) { + if (occs[lvl] === 0) continue; + Nuke.label(lvl + ": " + occs[lvl]); + } + + Nuke.tree_pop(); + } + Nuke.label("Has " + proto_children(goname).length + " descendents."); + + if (Nuke.button("spawn")) { + var proto = gameobjects[goname]; + var obj = this.edit_level.spawn(proto); + obj.pos = Mouse.worldpos; + editor.unselect(); + editor.selectlist.push(obj); + this.waitclose = true; + } + + if (Nuke.button("view")) { + objectexplorer.obj = gameobjects[goname]; + editor.openpanel(objectexplorer); + } + } + + treeleaf(goname); + Nuke.tree_pop(); + } + } + } + } + + treeleaf = treeleaf.bind(this); + + if (Nuke.tree("gameobject")) { + treeleaf("gameobject"); + Nuke.tree_pop(); + } + + if (this.waitclose) { + this.waitclose = false; + this.close(); + } + }, +}); + +var objectexplorer = copy(inputpanel, { + title: "object explorer", + obj: null, + previous: [], + start() { + this.previous = []; + Input.setnuke(); + }, + + on_close() { Input.setgame(); }, + + input_enter_pressed() {}, + + goto_obj(obj) { + if (obj === this.obj) return; + this.previous.push(this.obj); + this.obj = obj; + }, + + input_lmouse_pressed() { + Mouse.disabled(); + }, + + input_lmouse_released() { + Mouse.normal(); + }, + + guibody() { + Nuke.label("Examining " + this.obj); + + var n = 0; + var curobj = this.obj; + while (curobj) { + n++; + curobj = curobj.__proto__; + } + + n--; + Nuke.newline(n); + curobj = this.obj.__proto__; + while (curobj) { + if (Nuke.button(curobj.toString())) + this.goto_obj(curobj); + + curobj = curobj.__proto__; + } + + Nuke.newline(2); + + if (this.previous.empty) + Nuke.label(""); + else { + if (Nuke.button("prev: " + this.previous.last)) + this.obj = this.previous.pop(); + } + + Object.getOwnPropertyNames(this.obj).forEach(key => { + var descriptor = Object.getOwnPropertyDescriptor(this.obj, key); + if (!descriptor) return; + var hidden = !descriptor.enumerable; + var writable = descriptor.writable; + var configurable = descriptor.configurable; + + if (!descriptor.configurable) return; + if (hidden) return; + + var name = (hidden ? "[hidden] " : "") + key; + var val = this.obj[key]; + + var nuke_str = key + "_nuke"; + if (nuke_str in this.obj) { + this.obj[nuke_str](); + if (key in this.obj.__proto__) { + if (Nuke.button("delete " + key)) { + if (("_" + key) in this.obj) + delete this.obj["_"+key]; + else + delete this.obj[key]; + } + } + } else + switch (typeof val) { + case 'object': + if (val) { + if (Array.isArray(val)) { + this.obj[key] = Nuke.pprop(key,val); + break; + } + + Nuke.newline(2); + Nuke.label(name); + if (Nuke.button(val.toString())) this.goto_obj(val); + } else { + this.obj[key] = Nuke.pprop(key,val); + } + break; + + case 'function': + Nuke.newline(2); + Nuke.label(name); + Nuke.label("function"); + break; + + default: + if (!hidden) {// && Object.getOwnPropertyDescriptor(this.obj, key).writable) { + if (key.startsWith('_')) key = key.slice(1); + + this.obj[key] = Nuke.pprop(key, this.obj[key]); + + if (key in this.obj.__proto__) { + if (Nuke.button("delete " + key)) { + if ("_"+key in this.obj) + delete this.obj["_"+key]; + else + delete this.obj[key]; + } + } + } + else { + Nuke.newline(2); + Nuke.label(name); + Nuke.label(val.toString()); + } + break; + } + }); + + Nuke.newline(); + Nuke.label("Properties that can be pulled in ..."); + Nuke.newline(3); + var pullprops = []; + for (var key in this.obj.__proto__) { + if (key.startsWith('_')) key = key.slice(1); + if (!this.obj.hasOwn(key)) { + if (typeof this.obj[key] === 'object' || typeof this.obj[key] === 'function') continue; + pullprops.push(key); + } + } + + pullprops = pullprops.sort(); + + pullprops.forEach(function(key) { + if (Nuke.button(key)) + this.obj[key] = this.obj[key]; + }, this); + + Game.objects.forEach(function(x) { x.sync(); }); + + Nuke.newline(); + }, + + +}); + +var helppanel = copy(inputpanel, { + title: "help", + + start() { + this.helptext = slurp("editor.adoc"); + }, + + guibody() { + Nuke.label(this.helptext); + }, +}); + +var openlevelpanel = copy(inputpanel, { + title: "open level", + action() { + editor.load(this.value); + }, + + assets: [], + allassets: [], + extensions: ["lvl"], + + submit_check() { + if (this.assets.length === 1) { + this.value = this.assets[0]; + return true; + } else { + return this.assets.includes(this.value); + } + }, + + start() { + this.allassets = []; + + if (this.allassets.empty) { + this.extensions.forEach(function(x) { + this.allassets.push(cmd(66, "." + x)); + }, this); + } + this.allassets = this.allassets.flat().sort(); + this.assets = this.allassets.slice(); + }, + + keycb() { + this.assets = this.allassets.filter(x => x.search(this.value) !== -1); + }, + + input_tab_pressed() { + this.value = tab_complete(this.value, this.assets); + }, + + guibody() { + this.value = Nuke.textbox(this.value); + + this.assets.forEach(function(x) { + if (Nuke.button(x)) { + this.value = x; + this.submit(); + } + }, this); + + Nuke.newline(2); + + if (Nuke.button("submit")) { + this.submit(); + } + }, +}); + +var addlevelpanel = copy(openlevelpanel, { + title: "add level", + action() { editor.addlevel(this.value, Mouse.worldpos); }, +}); + +var saveaspanel = copy(inputpanel, { + title: "save level as", + action() { + editor.saveas_check(this.value); + }, +}); + +var groupsaveaspanel = copy(inputpanel, { + title: "group save as", + action() { editor.groupsaveas(editor.selectlist, this.value); } +}); + +var saveprototypeas = copy(inputpanel, { + title: "save prototype as", + action() { + editor.save_proto_as(this.value); + }, +}); + +var savetypeas = copy(inputpanel, { + title: "save type as", + action() { + editor.save_type_as(this.value); + }, +}); + +var quitpanel = copy(inputpanel, { + title: "really quit?", + action() { + quit(); + }, + + guibody () { + Nuke.label("Really quit?"); + Nuke.newline(2); + if (Nuke.button("yes")) + this.submit(); + }, +}); + +var notifypanel = copy(inputpanel, { + title: "notification", + msg: "Refusing to save. File already exists.", + action() { + this.close(); + }, + + guibody() { + Nuke.label(this.msg); + Nuke.newline(2); + if (Nuke.button("OK")) { + if ('yes' in this) + this.yes(); + this.close(); + } + }, + + input_n_pressed() { + this.close(); + }, +}); + +var gen_notify = function(val, fn) { + var panel = Object.create(notifypanel); + panel.msg = val; + panel.yes = fn; + panel.input_y_pressed = function() { panel.yes(); this.close(); }; + return panel; +}; + +var scripts = ["js"]; +var images = ["png", "jpg", "jpeg"]; +var sounds = ["wav", "mp3"]; +var allfiles = []; +allfiles.push(scripts, images, sounds); +allfiles = allfiles.flat(); + +var assetexplorer = copy(openlevelpanel, { + title: "asset explorer", + extensions: allfiles, + closeonsubmit: false, + allassets:[], + action() { + if (editor.sel_comp && 'asset' in editor.sel_comp) + editor.sel_comp.asset = this.value; + else + editor.viewasset(this.value); + }, +}); + +function tab_complete(val, list) { + var check = list.filter(function(x) { return x.startsWith(val); }, this); + if (check.length === 1) { + list = check; + return check[0]; + } + + var ret = null; + var i = val.length; + while (!ret && !check.empty) { + + var char = check[0][i]; + if (!check.every(function(x) { return x[i] === char; })) + ret = check[0].slice(0, i); + else { + i++; + check = check.filter(function(x) { return x.length-1 > i; }); + } + } + + if (!ret) return val; + + list = check; + return ret; +} + +var texgui = clone(inputpanel, { + get path() { return this._path; }, + set path(x) { + this._path = x; + this.title = "texture " + x; + }, + + guibody() { + Nuke.label("texture"); + Nuke.img(this.path); + }, +}); + +var levellistpanel = copy(inputpanel, { + title: "Level list", + level: {}, + start() { + this.level = editor.edit_level; + }, + + guibody() { + Nuke.newline(4); + Nuke.label("Object"); + Nuke.label("Visible"); + Nuke.label("Selectable"); + Nuke.label("Selected?"); + this.level.objects.forEach(function(x) { + if (Nuke.button(x.toString())) { + editor.selectlist = []; + editor.selectlist.push(x); + } + + x.visible = Nuke.checkbox(x.visible); + x.selectable = Nuke.checkbox(x.selectable); + + if (editor.selectlist.includes(x)) Nuke.label("T"); else Nuke.label("F"); + }); + }, +}); + +var prefabpanel = copy(openlevelpanel, { + title: "prefabs", + allassets: Object.keys(gameobjects), + start() { + this.allassets = Object.keys(gameobjects).sort(); + this.assets = this.allassets.slice(); + }, + action() { + var obj = editor.edit_level.spawn(gameobjects[this.value]); + obj.pos = Mouse.worldpos; + editor.unselect(); + editor.selectlist.push(obj); + }, +}); + +var limited_editor = { + input_f1_pressed() { editor.input_f1_pressed(); }, + + input_f5_pressed() { + /* Pause, and resume editor */ + }, + input_f7_pressed() { + if (sim_playing()) { + sim_stop(); + game.stop(); + Sound.killall(); + unset_pawn(limited_editor); + set_pawn(editor); + register_gui(editor.ed_gui, editor); + Debug.register_call(editor.ed_debug, editor); + Level.kill(); + Level.clear_all(); + editor.load_json(editor.stash); + set_cam(editor_camera.body); + } + }, + input_f8_pressed() { sim_step(); }, + input_f10_pressed() { editor.input_f10_pressed(); }, + input_f10_released() { editor.input_f10_released(); }, +}; + +set_pawn(editor); +register_gui(editor.ed_gui, editor); +Debug.register_call(editor.ed_debug, editor); + +if (IO.exists("editor.config")) + load_configs("editor.config"); +editor.edit_level = Level.create(); diff --git a/source/scripts/engine.js b/source/scripts/engine.js new file mode 100644 index 0000000..f4acab2 --- /dev/null +++ b/source/scripts/engine.js @@ -0,0 +1,1597 @@ +var files = {}; +function load(file) { + var modtime = cmd(0, file); + 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); + + if (loop) + t.loop = loop; + else + t.loop = 0; + Log.info("Made timer " + t.id); + return t; + }, + + oneshot(fn, secs, obj) { + var t = clone(this); + var killfn = function() { + fn.call(this); + t.kill(); + }; + t.id = make_timer(killfn,secs,obj); + t.loop = 0; + }, + + 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 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])); + }, + + pawns: [], + pawn_input(fn, ...args) { + this.pawns.forEach(x => { + if (fn in x) + x[fn].call(x, ...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.pawns = this.pawns.filter(x => x[1] !== obj); + this.debugs = this.debugs.filter(x => x[1] !== obj); + this.physupdates = this.debugs.filter(x => x[1] !== obj); + }, +}; +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.pawn_input, Register); + +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); + +function set_pawn(obj) { + Register.pawns.push(obj); +} + +function unset_pawn(obj) { + Register.pawns = Register.pawns.filter(x => x !== 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) { 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); + 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 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, + + 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() { + 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) { + 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', 'scale', 'controlled', 'selectable', 'save', 'velocity', 'angularvelocity', 'alive', 'boundingbox', 'name']; +locks.forEach(function(x) { + Object.defineProperty(gameobject, x, {enumerable:false}); +}); + +function private_non_enumerable(obj) { + for (var key in obj) { + if (key.startsWith('_')) + Object.defineProperty(obj, key, {enumerable:false}); + } +} + +function add_sync_prop(obj, prop, syncfn) { + var hidden = "_"+prop; + Log.info(hidden); + Object.defineProperty(obj, hidden, { + value: null, + writable: true, + }); + + Object.defineProperty(obj, prop, { + get: function() { return obj[hidden]; }, + set: function(x) { + obj[hidden] = x; + syncfn(obj[hidden]); + }, + enumerable: true, + }); + + return obj; + +}; + +/* Load configs */ +function load_configs(file) { + var configs = JSON.parse(IO.slurp(file)); + for (var key in configs) { + Object.assign(this[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(); }); +}; + +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");