Skip to content

Instantly share code, notes, and snippets.

@bferguson3
Created October 21, 2025 21:28
Show Gist options
  • Select an option

  • Save bferguson3/69692b08e4875beb6f605ed975b412c0 to your computer and use it in GitHub Desktop.

Select an option

Save bferguson3/69692b08e4875beb6f605ed975b412c0 to your computer and use it in GitHub Desktop.
Visual Novel JSON Scene Editor (for Floating Petals)
<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