commit:14d4702ea0f7bab56146324d7920080dac84b953
author:Chip Black
committer:Chip Black
date:Sun Nov 11 00:27:39 2018 -0600
parents:ab32e317e6140d6f64e428e56c1e0a57b81f0355
Add Object Editor
diff --git a/index.html b/index.html
line changes: +3/-2
index 3590878..582d327
--- a/index.html
+++ b/index.html
@@ -22,8 +22,9 @@
   </div>
 </div>
 
-<div id="cell-editor-container">
-</div>
+<div id="cell-editor-container" class="editor-container hidden"></div>
+
+<div id="object-editor-container" class="editor-container hidden"></div>
 
 </body>
 </html>

diff --git a/src/gamestate.ts b/src/gamestate.ts
line changes: +16/-0
index 7b8d16e..ca9a573
--- a/src/gamestate.ts
+++ b/src/gamestate.ts
@@ -1,5 +1,6 @@
 import GameMap from './map';
 import CellEditor, { CellEditorCallback } from './celleditor';
+import ObjectEditor from './objecteditor';
 import MapView from './mapview';
 import TextView from './textview';
 import Actions from './actions';
@@ -30,6 +31,7 @@ class GameState {
 
     textView: TextView;
     cellEditor: CellEditor;
+    objectEditor: ObjectEditor;
     mapView: MapView;
     actions: Actions;
     editMode: boolean = true;
@@ -44,6 +46,7 @@ class GameState {
 
         this.textView = new TextView();
         this.cellEditor = new CellEditor();
+        this.objectEditor = new ObjectEditor();
         this.mapView = new MapView();
         this.mapView.updateAll();
         this.actions = new Actions();
@@ -90,6 +93,7 @@ class GameState {
         if (this.editMode) {
             this.actions.addAction('Toggle Navigable', () => this.map.toggleNavigable());
             this.actions.addAction('Edit Cell', () => this.map.editDescription());
+            this.actions.addAction('Object Editor', () => this.objectEditor.open());
         }
     }
 
@@ -97,6 +101,18 @@ class GameState {
         this.printCellText();
         this.showCellActions();
     }
+
+    addObject(o: GameObject) {
+        this.objects.set(o.id, o);
+    }
+
+    removeObject(o: GameObject | ID) {
+        if (o instanceof GameObject) {
+            this.objects.delete(o.id);
+        } else {
+            this.objects.delete(o);
+        }
+    }
 }
 
 // Create a default GameState object and export it

diff --git a/src/main.ts b/src/main.ts
line changes: +4/-0
index bbbe81a..0811d9e
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,6 +23,10 @@ function keydown(e: KeyboardEvent) {
             e.preventDefault();
             gs.map.editDescription();
             break;
+        case 'KeyO':
+            e.preventDefault();
+            gs.objectEditor.open();
+            break;
         case 'KeyS':
             localStorage['map'] = JSON.stringify(gs.map.store());
             console.log(localStorage['map']);

diff --git a/src/object.ts b/src/object.ts
line changes: +17/-6
index 936f2fc..974d426
--- a/src/object.ts
+++ b/src/object.ts
@@ -9,23 +9,34 @@ export enum GameObjectType {
 export interface GameObjectProperties {
     id: ID
     type: GameObjectType
+    name: string
+    description: string
+}
+
+interface PropertyBag {
+    [k: string]: number | string
 }
 
 export class GameObject extends Storable {
     id: ID
     type: GameObjectType
-    x: number
-    y: number
-    saveProperties = ['type']
+    name: string
+    description: string
+    properties: PropertyBag
+    saveProperties = ['id', 'type', 'properties']
 
     constructor(t: GameObjectType = GameObjectType.Unknown) {
         super();
         this.id = generateID();
         this.type = t;
+        this.objectProperties = {};
+    }
+
+    set(k: string, v: number | string) {
+        this.properties[k] = v;
     }
 
-    moveTo(x: number, y: number) {
-        this.x = x;
-        this.y = y;
+    get(k: string): number | string {
+        return this.properties[k];
     }
 }

diff --git a/src/objecteditor.ts b/src/objecteditor.ts
line changes: +163/-0
index 0000000..c9fde97
--- /dev/null
+++ b/src/objecteditor.ts
@@ -0,0 +1,163 @@
+import FragmentManager from './fragmentmanager';
+import { GameObject } from './object';
+import { ID } from './id';
+import gs from './gamestate';
+
+export default class ObjectEditor {
+    container: FragmentManager
+    listRef: HTMLSelectElement
+    idRef: HTMLInputElement
+    nameRef: HTMLInputElement
+    descriptionRef: HTMLTextAreaElement
+    originalID: ID
+
+    constructor() {
+        this.container = new FragmentManager(`
+            <div class="box vflex">
+                <h1>Object Editor</h1>
+                <div class="hflex flex-auto">
+                    <div class="vflex" style="width: 200px; margin-right: 8px">
+                        <select multiple name="objectlist" class="flex-auto" style="width: calc(100% - 8px)">
+                        </select>
+                        <div class="hflex">
+                            <button id="object-add" class="flex-auto">+</button>
+                            <button id="object-remove" class="flex-auto">-</button>
+                        </div>
+                    </div>
+                    <div class="vflex flex-auto">
+                        <h2>ID</h2>
+                        <input name="id">
+                        <h2>Name</h2>
+                        <input name="name">
+                        <h2>Description</h2>
+                        <textarea name="description" class="flex-auto"></textarea>
+                    </div>
+                </div>
+                <div class="hflex">
+                    <button id="object-editor-close">Close</button>
+                </div>
+            </div>
+        `);
+        this.container.mountInto('object-editor-container');
+
+        this.container.elem.addEventListener('keydown', function(e: KeyboardEvent) {
+            e.cancelBubble = true;
+        });
+
+        this.listRef = <HTMLSelectElement>this.container.q('[name="objectlist"]');
+        this.idRef = <HTMLInputElement>this.container.q('[name="id"]');
+        this.nameRef = <HTMLInputElement>this.container.q('[name="name"]');
+        this.descriptionRef = <HTMLTextAreaElement>this.container.q('[name=description]');
+
+        const closeButton = this.container.q('#object-editor-close');
+        closeButton.addEventListener('click', this.closeHandler.bind(this));
+
+        const objectAddButton = this.container.q('#object-add');
+        objectAddButton.addEventListener('click', this.createObject.bind(this));
+
+        const objectRemoveButton = this.container.q('#object-remove');
+        objectRemoveButton.addEventListener('click', this.deleteObject.bind(this));
+
+        this.listRef.addEventListener('change', event => {
+            const id = (<HTMLInputElement>event.target).value;
+            this.commitObject();
+            this.selectObject(id);
+        });
+    }
+
+    open(object?: GameObject) {
+        this.refreshObjectList();
+
+        if (object) {
+            this.selectObject(object.id);
+        }
+
+        this.container.elem.classList.add('visible');
+        this.nameRef.focus();
+    }
+
+    refreshObjectList() {
+        while (this.listRef.firstChild)
+            this.listRef.removeChild(this.listRef.firstChild);
+
+        for (let [k, v] of gs.objects) {
+            const option = document.createElement('option');
+            option.value = k;
+            option.innerText = v.name;
+            this.listRef.appendChild(option);
+        }
+    }
+
+    selectObject(id?: ID) {
+        if (id) {
+            if (!gs.objects.has(id)) {
+                throw new Error('Invalid object ID: ' + id);
+            }
+        }
+
+        for (let i = 0; i < this.listRef.children.length; i++) {
+            const child = <HTMLOptionElement>this.listRef.children[i];
+            child.selected = child.value == id;
+        }
+
+        if (id) {
+            const object = gs.objects.get(id);
+
+            this.idRef.value = this.originalID = object.id;
+            this.nameRef.value = object.name || 'Unnamed Object';
+            this.descriptionRef.value = object.description || '';
+        } else {
+            this.originalID = undefined;
+            this.idRef.value = '';
+            this.nameRef.value = '';
+            this.descriptionRef.value = '';
+        }
+    }
+
+    createObject() {
+        this.commitObject();
+        const obj = new GameObject();
+        obj.name = 'Unnamed Object';
+        gs.addObject(obj);
+        this.refreshObjectList();
+        this.selectObject(obj.id);
+    }
+
+    deleteObject() {
+        this.commitObject();
+        const objID = this.originalID;
+        const obj = gs.objects.get(objID);
+        if (window.confirm("Really delete " + obj.name + "?")) {
+            gs.removeObject(objID);
+            this.refreshObjectList();
+            this.selectObject(null);
+        }
+    }
+
+    commitObject() {
+        if (!this.originalID) return;
+        const obj = gs.objects.get(this.originalID);
+        obj.name = this.nameRef.value;
+        obj.description = this.descriptionRef.value;
+
+        const objID = this.idRef.value
+        if (objID != this.originalID) {
+            gs.removeObject(this.originalID);
+            obj.id = objID;
+            gs.addObject(obj);
+            this.refreshObjectList();
+            this.selectObject(objID);
+        } else {
+            this.refreshObjectList();
+        }
+    }
+
+    close() {
+        this.closeHandler();
+    }
+
+    closeHandler() {
+        this.commitObject();
+        this.container.elem.classList.remove('visible');
+    }
+}

diff --git a/structure.css b/structure.css
line changes: +11/-8
index 2ca2403..42921c3
--- a/structure.css
+++ b/structure.css
@@ -30,6 +30,14 @@ body {
 	flex: auto;
 }
 
+.hidden {
+	display: none;
+}
+
+.visible {
+	display: initial;
+}
+
 #map-container {
 	position: relative;
 	border: 1px solid green;
@@ -79,8 +87,7 @@ body {
 	padding: 16px;
 }
 
-#cell-editor-container {
-	display: none;
+.editor-container {
 	position: fixed;
 	left: 0;
 	top: 0;
@@ -88,7 +95,7 @@ body {
 	height: 100vh;
 }
 
-#cell-editor-container .box {
+.editor-container .box {
 	width: 800px;
 	height: 600px;
 	margin-top: calc(50vh - 300px);
@@ -98,11 +105,7 @@ body {
 	background-color: black;
 }
 
-#cell-editor-container.visible {
-	display: initial;
-}
-
-#cell-editor-container textarea[name="description"] {
+.editor-container textarea[name="description"] {
 	height: 80px;
 }