Created
October 21, 2025 21:28
-
-
Save bferguson3/69692b08e4875beb6f605ed975b412c0 to your computer and use it in GitHub Desktop.
Visual Novel JSON Scene Editor (for Floating Petals)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <html> | |
| <head> | |
| <title>VN Editor</title> | |
| </head> | |
| <body> | |
| <button class="add-button" id="adder" onclick="AddNew()">+</button> | |
| <select id="type_select" class="typeSelector"> | |
| <option value="none">-</option> | |
| <option value="transition">Transition</option> | |
| <option value="play_sfx">Play SFX</option> | |
| <option value="start_combat">Start Combat</option> | |
| <option value="enter_sprite">Enter Sprite</option> | |
| <option value="change_sprite">Change Sprite</option> | |
| <option value="move_sprite">Move Sprite</option> | |
| <option value="speech">Speech</option> | |
| <option value="choice">Choice</option> | |
| <option value="diecheck">Die Check</option> | |
| </select> | |
| <button id="savebtn" onclick="Save()">Save JSON</button> | |
| <input type="file" id="filePicker" accept=".json" onchange="Load()" /> | |
| <div id="main_body"></div> | |
| </body> | |
| </html> | |
| <style> | |
| .add-button { | |
| font-size: larger; | |
| } | |
| .script-item { | |
| border: 2px; | |
| padding: 4px; | |
| border-style: solid; | |
| width: 400px; | |
| } | |
| .dialoguebox { | |
| height: 50px; | |
| text-align: top; | |
| } | |
| textarea { | |
| resize: none; | |
| } | |
| .choice_ct_input { | |
| width: 50px; | |
| } | |
| .typeSelector { | |
| font-size: 16; | |
| } | |
| .choice-item { | |
| background-color: aquamarine; } | |
| .speech-item { | |
| background-color: gold; } | |
| .move-item { | |
| background-color: #c88; } | |
| .change-item { | |
| background-color: lightgreen; } | |
| .enter-item { | |
| background-color:wheat; } | |
| .combat-item { | |
| background-color:violet; } | |
| .sfx-item { | |
| background-color: #5ff; } | |
| .trans-item { | |
| background-color: #fbb; } | |
| .check-item { | |
| background-color: #ff5; | |
| } | |
| </style> | |
| <script> | |
| var mainbody; | |
| var item_ctr = 0; | |
| var fileList = null; | |
| var database = {}; | |
| function Save(){ | |
| var save_objs="{ \"scene\": [\n"; | |
| var b = document.getElementsByClassName("script-item"); | |
| for(var j = 0; j < b.length; j++){ | |
| obj = {} | |
| //obj.index = j; | |
| //console.log(b[j]); | |
| if(b[j].className.indexOf("trans-item")!=-1){ // type, tgt | |
| obj.type = "transition"; | |
| obj.tgt = b[j].lastChild.value; | |
| } | |
| else if (b[j].className.indexOf("sfx-item")!=-1){ // type, sfx | |
| obj.type = "play_sfx"; | |
| obj.sfx = b[j].lastChild.value; | |
| } | |
| else if(b[j].className.indexOf("combat-item")!=-1){ // type | |
| obj.type = "start_combat"; | |
| } | |
| else if(b[j].className.indexOf("enter-item")!=-1){ // type, id, img, direction | |
| obj.type = "enter_sprite"; | |
| obj.id = b[j].querySelector("#idinput").value; | |
| obj.img = b[j].querySelector("#imginput").value; | |
| obj.direction = b[j].querySelector("#dirinput").value; | |
| } | |
| else if(b[j].className.indexOf("change-item")!=-1){ // type, id, img | |
| obj.type = "change_sprite"; | |
| obj.id = b[j].querySelector("#idinput").value; | |
| obj.img = b[j].querySelector("#imginput").value; | |
| } | |
| else if(b[j].className.indexOf("move-item")!=-1){ // type, id, pos | |
| obj.type = "move_sprite"; | |
| obj.id = b[j].querySelector("#idinput").value; | |
| obj.pos = b[j].querySelector("#posinput").value; | |
| } | |
| else if(b[j].className.indexOf("speech-item")!=-1){ // type, name, txt | |
| obj.type = "speech"; | |
| obj.name = b[j].querySelector("#nameinput").value; | |
| obj.txt = b[j].querySelector("#dialogueinput").value; | |
| } | |
| else if(b[j].className.indexOf("choice-item")!=-1){ // type, choices[] | |
| obj.type = "choice"; | |
| obj.choices = [[],[],[],[],[]]; | |
| var css = b[j].querySelector("#inputlist").querySelectorAll("input");; | |
| var ctr = 0; | |
| var ctr2 = 0; | |
| css.forEach(e => { | |
| obj.choices[ctr][ctr2] = e.value; | |
| ctr2++; | |
| if(ctr2==2){ | |
| ctr++; | |
| ctr2=0; | |
| } | |
| }); | |
| } | |
| else if(b[j].className.indexOf("check-item")!=-1){ | |
| obj.type = "diecheck"; | |
| obj.pass = b[j].querySelector("#passinput").value; | |
| obj.fail = b[j].querySelector("#failinput").value; | |
| } | |
| save_objs += JSON.stringify(obj, null, 4); | |
| save_objs +=",\n" | |
| } | |
| save_objs = save_objs.substring(0, save_objs.length - 2); | |
| save_objs += "\n] }"; | |
| let f = new Blob([save_objs], {type: "application/json"}); | |
| saveAs(f, "scenario.json", null); | |
| } | |
| function PopulateScene(){ | |
| if(mainbody != null) mainbody.innerHTML = ""; | |
| database["scene"].forEach(e => { | |
| //console.log(e); | |
| document.getElementById("type_select").value = e.type; | |
| var _n = AddNew(); | |
| if(e.type == "transition"){ | |
| _n.querySelector("#inp").value = e.tgt; | |
| } | |
| else if(e.type == "play_sfx"){ | |
| _n.querySelector("#sfxinput").value = e.sfx; | |
| } | |
| else if(e.type == "enter_sprite"){ | |
| _n.querySelector("#idinput").value = e.id; | |
| _n.querySelector("#imginput").value = e.img; | |
| _n.querySelector("#dirinput").value = e.direction; | |
| } | |
| else if(e.type == "change_sprite"){ | |
| _n.querySelector("#idinput").value = e.id; | |
| _n.querySelector("#imginput").value = e.img; | |
| } | |
| else if(e.type == "move_sprite"){ | |
| _n.querySelector("#idinput").value = e.id; | |
| _n.querySelector("#posinput").value = e.pos; | |
| } | |
| else if(e.type == "speech"){ | |
| _n.querySelector("#nameinput").value = e.name; | |
| _n.querySelector("#dialogueinput").value = e.txt; | |
| } | |
| else if(e.type == "choice"){ | |
| var is = _n.querySelectorAll("input"); | |
| var ctr = 0; | |
| for(var j = 0; j < 10; j+=2){ | |
| is[j].value = e.choices[ctr][0]; | |
| ctr++; | |
| } | |
| ctr = 0; | |
| for(var j = 1; j < 10; j+=2){ | |
| is[j].value = e.choices[ctr][1]; | |
| ctr++; | |
| } | |
| } else if(e.type == "diecheck"){ | |
| _n.querySelector("#passinput").value = e.pass; | |
| _n.querySelector("#failinput").value = e.fail; | |
| } | |
| }); | |
| } | |
| function Load(){ | |
| // Load in json file | |
| fileList = document.getElementById("filePicker").files; /* now you can work with the file list */ | |
| const reader = new FileReader(); // define new reader obj | |
| reader.onload = function() { // define callback (async!!) | |
| //console.log(reader.result); | |
| database = JSON.parse(reader.result); | |
| PopulateScene(); | |
| }; | |
| reader.onerror = function() { // define callback error | |
| console.error("error"); | |
| } | |
| reader.readAsText(fileList[0], 'utf-8'); // perform read | |
| } | |
| function AddNew(){ | |
| var ts = document.getElementById("type_select").value; | |
| var new_a = document.createElement("div"); | |
| mainbody = document.getElementById("main_body"); | |
| if(ts == "transition"){ | |
| new_a.className = "script-item trans-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>" + "\"tgt\": <input id=\"inp\" value=\"001\" />"; | |
| } | |
| else if (ts == "play_sfx"){ | |
| new_a.className = "script-item sfx-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>" + "\"sfx\": <input id=\"sfxinput\" value=\"sfx.wav\" />"; | |
| } | |
| else if (ts == "start_combat"){ | |
| new_a.className = "script-item combat-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>"; | |
| } | |
| else if (ts == "enter_sprite"){ | |
| new_a.className = "script-item enter-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>" + "\"id\": <input id=\"idinput\" value=\"0\" /><br>\"img\": <input id=\"imginput\" value=\"spr01.img\" /><br>\"direction\": <input id=\"dirinput\" value=\"right\" />"; | |
| } | |
| else if (ts == "change_sprite"){ | |
| new_a.className = "script-item change-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>" + "\"id\": <input id=\"idinput\" value=\"0\" /><br>\"img\": <input id=\"imginput\" value=\"spr01.img\" />"; | |
| } | |
| else if (ts == "move_sprite"){ | |
| new_a.className = "script-item move-item"; | |
| new_a.innerHTML += "\"type\": \"" + ts + "\"<br>" + "\"id\": <input id=\"idinput\" value=\"0\" /><br>\"new_pos\": <input id=\"posinput\" value=\"120\" />" | |
| } | |
| else if (ts == "speech"){ | |
| new_a.className = "script-item speech-item"; | |
| new_a.innerHTML = "\"type\": \"" + ts + "\"<br>" + "\"name\": <input id=\"nameinput\" value=\"Yurako\" /><br>\"txt\": <textarea id=\"dialogueinput\" class=\"dialoguebox\">Hello World!</textarea>"; | |
| } | |
| else if (ts == "choice"){ | |
| new_a.className = "script-item choice-item"; | |
| new_a.innerHTML = `"type": "`+ts+`"<br><div id="inputlist"><input value="Option A" /><input value="003" /><br><input/><input/><br><input/><input/><br><input/><input/><br><input/><input/><br></div>`; | |
| } | |
| else if(ts == "diecheck"){ | |
| new_a.className = "script-item check-item"; | |
| new_a.innerHTML = `"type: "`+ts+`"<br>Pass: <input id="passinput" value="001" /> Fail: <input id="failinput" value="002" />`; | |
| } | |
| item_ctr++; | |
| mainbody.appendChild(new_a); | |
| return new_a; | |
| } | |
| /* | |
| * FileSaver.js (MINIFIED) | |
| * source : http://purl.eligrey.com/github/FileSaver.js | |
| */ | |
| var _global="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self? | |
| self:"object"==typeof global&&global.global===global?global:this;function bom(e,t){return(void 0===t? | |
| t={autoBom:!1}:"object"!=typeof t&&(console.warn("Deprecated: Expected third argument to be a object"), | |
| t={autoBom:!t}),t.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test( | |
| e.type))?new Blob(["\uFEFF",e],{type:e.type}):e}function download(e,t,o){var n=new XMLHttpRequest;n.open( | |
| "GET",e),n.responseType="blob",n.onload=function(){saveAs(n.response,t,o)},n.onerror=function(){ | |
| console.error("could not download file")},n.send()}function corsEnabled(e){var t=new XMLHttpRequest;t.open( | |
| "HEAD",e,!1);try{t.send()}catch(o){}return t.status>=200&&t.status<=299}function click(e){try{ | |
| e.dispatchEvent(new MouseEvent("click"))}catch(t){var o=document.createEvent("MouseEvents");o.initMouseEvent( | |
| "click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),e.dispatchEvent(o)}}var isMacOSWebView=_global.navigator | |
| &&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test( | |
| navigator.userAgent),saveAs=_global.saveAs||("object"!=typeof window||window!==_global?function e(){ | |
| }:"download"in HTMLAnchorElement.prototype&&!isMacOSWebView?function e(t,o,n){var a=_global.URL|| | |
| _global.webkitURL,l=document.createElementNS("http://www.w3.org/1999/xhtml","a");o=o||t.name||"download", | |
| l.download=o,l.rel="noopener","string"==typeof t?(l.href=t,l.origin!==location.origin?corsEnabled(l.href)? | |
| download(t,o,n):click(l,l.target="_blank"):click(l)):(l.href=a.createObjectURL(t),setTimeout(function(){ | |
| a.revokeObjectURL(l.href)},4e4),setTimeout(function(){click(l)},0))}:"msSaveOrOpenBlob"in navigator? | |
| function e(t,o,n){if(o=o||t.name||"download","string"==typeof t){if(corsEnabled(t))download(t,o,n);else{ | |
| var a=document.createElement("a");a.href=t,a.target="_blank",setTimeout(function(){click(a)})}}else | |
| navigator.msSaveOrOpenBlob(bom(t,n),o)}:function e(t,o,n,a){if((a=a||open("","_blank"))&&(a.document.title= | |
| a.document.body.innerText="downloading..."),"string"==typeof t)return download(t,o,n);var l= | |
| "application/octet-stream"===t.type,r=/constructor/i.test(_global.HTMLElement)||_global.safari,c= | |
| /CriOS\/[\d]+/.test(navigator.userAgent);if((c||l&&r||isMacOSWebView)&&"undefined"!=typeof FileReader){ | |
| var i=new FileReader;i.onloadend=function(){var e=i.result;e=c?e:e.replace(/^data:[^;]*;/, | |
| "data:attachment/file;"),a?a.location.href=e:location=e,a=null},i.readAsDataURL(t)}else{var s= | |
| _global.URL||_global.webkitURL,d=s.createObjectURL(t);a?a.location=d:location.href=d,a=null,setTimeout( | |
| function(){s.revokeObjectURL(d)},4e4)}});_global.saveAs=saveAs.saveAs=saveAs,"undefined"!=typeof module&&( | |
| module.exports=saveAs); | |
| /* | |
| */ | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment