JSFiddle - React, Tailwind, and code Playground
by Gwyn Milcote
HTML
<h2>CYOA Editor</h2>
<p>Although making the game <i>work</i> is pretty simple, I soon got tired of organising the different scenes and choices by ID numbers. Even the short demo was a chore to get right!</p>
<p>This editor lets you arrange a game more easily, and export the <b>db</b> object ready to use.</p>
<div id='editor_container'>
<button class='tab_button active_button' data-tab='scenes'>Scenes</button>
<button class='tab_button' data-tab='stats'>Stats</button>
<button class='tab_button' data-tab='load'>Import/Export</button>
<button class='tab_button' data-tab='test'>Test</button>
<div class='tab_content' id='tab_content_scenes'>
<div id='scene_editor_side'>
<h2>Your Scenes</h2>
<div id='scene_list'>No scenes yet</div>
</div>
<div id='scene_editor_main'>
<h2>Editing Scene #<span id='edit_scene_id'>0</span></h2>
<div id='scene_options'>
<button id='delete_scene'>Delete</button>
<button id='save_scene'>Save</button>
<button id='create_scene'>Save as new scene</button>
</div>
Text:<br>
<textarea id='edit_scene_text'></textarea>
<p>The properties below are optional. Summary will be seen only by you, not the player.</p>
<div class='scene_input'>
<span>Title:</span>
<input type='text' id='edit_scene_title'>
</div>
<div class='scene_input'>
<span>Summary:</span>
<input type='text' id='edit_scene_summary'>
</div>
<div class='scene_input'>
<span>Image URL:</span>
<input type='text' id='edit_scene_image'>
</div>
<p>Choices that lead <b>to this scene</b> from others:</p>
<div id='choicesTo_list'>No choices yet</div>
<p>Choices that lead away <b>from this scene</b>:</p>
<div id='choicesIn_list'>No choices yet</div>
</div>
</div>
<div...
CSS
div, img, p, a, button, input, textarea {
box-sizing: border-box;
}
#editor-container {
width: 1000px;
}
.tab_button {
position: relative;
top: 1px;
left: 20px;
padding: 5px 10px;
border: 1px solid grey;
border-radius: 5px 5px 0px 0px;
cursor: pointer;
background-color: lightpink;
transition: 0.3s;
}
.tab_button:hover {
cursor: pointer;
background-color: ivory;
transition: 0.3s;
}
.active_button {
background-color: ivory;
border-bottom: 1px solid ivory;
}
.tab_content {
border: 1px solid grey;
padding: 10px;
background-color: ivory;
border-radius: 5px;
}
#editor_container {
max-width: 860px;
margin: 20px auto;
}
button {
cursor: pointer;
}
#import_data, #export_data {
width: 100%;
min-height: 200px;
}
/* Middle of page */
#popup_holder {
position: fixed;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -100px;
background-color: #fff;
padding: 10px 20px;
text-align: center;
border-radius: 10px;
box-shadow: 0 0 20px 10px rgba(0,0,0,0.4);
}
.stat_thumb,
.choice_thumb {
border: 1px solid silver;
background-color: #fff;
padding: 5px;
margin-bottom: 10px;
border-radius: 5px;
transition: 0.3s;
}
.scene_thumb:hover,
.stat_thumb:hover,
.choice_thumb:hover {
cursor: pointer;
border: 1px solid grey;
background-color: #eee;
transition: 0.3s;
}
/* For .scene_thumb, .stat_thumb etc */
.selected {
border: 1px solid grey;
background-color: silver;
}
#tab_content_scenes {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
.tab_content h2 {
text-align: center;
margin: 10px auto;
}
/* SCENES */
/* Panel that contains #scene_list */
#scene_editor_side {
width: 250px;
border: 1px dotted blue;
text-align: center;
}
/* Scrollable. Contains many .scene_thumb */
#scene_list {
max-height: 500px;
overflow: auto;
padding:...
JavaScript
// Default, demo.
let db = {
scenes: [
{
id: 1, title: "The Beginning", summary: "Intro scene",
text: "This is a demo. Here's the first scene. Choose a colour.",
},{
id: 2, text: "Second scene. No title or summary."
},{
id: 3, text: "Third scene. No title or summary."
}
],
choices: [
{
id: 1, sceneId: 1, toScene: 2,
text: "This is a choice.",
},{
id: 2, sceneId: 1, toScene: 2,
text: "This is another choice.",
},{
id: 3, sceneId: 2, toScene: 3,
text: "Going from scene 2 to scene 3.",
}
],
stats: [
{id: 1, name: "hp", displayName: "Health", type: "int", default: 0, description: "About this stat..."}
]
};
/*
// when loading/starting new game
this.stats = {};
for(let s of db.stats){
this.stats[s.name] = s.default;
}
*/
class CyoaEditor {
constructor(){
$("#tab_content_stats, #tab_content_load").hide();
this.listScenes();
this.listStats();
}
popup(msg){
$("#popup_message").html(msg);
$("#popup_holder").fadeIn();
console.log(msg);
}
export(){
let html = JSON.stringify(db);
$("#export_data").val(html);
}
import(){
let val = $("#import_data").val();
if(val == ""){
this.popup("Can't import, the textarea is empty.");
}
console.log(val);
// Hopefully it's the right kinda object structure.
// Try to correct/build it, if not.
try{
val = JSON.parse(val);
}catch(err){
this.popup("Can't parse this into an object. Check your code structure.");
}
if(!val.scenes){
val.scenes = [];
}else if(!val.scenes.isArray){
this.popup("Your <b>db.scenes</b> is not an array.");
}
...