├── .gitignore ├── README.md ├── build └── modtools │ └── game-data │ ├── README.md │ ├── icons │ └── README.md │ └── json │ └── README.md ├── config.json ├── lib └── jquery-1.11.2.js ├── package.json ├── release ├── clockworacle-mod.zip └── modtools.zip ├── scripts └── create-mod.js ├── spec ├── scripts │ └── objects │ │ ├── clump.spec.js │ │ └── lump.spec.js └── support │ └── jasmine.json └── src ├── scripts ├── api.js ├── io.js ├── library.js ├── objects │ ├── area.js │ ├── availability.js │ ├── clump.js │ ├── combat-attack.js │ ├── event.js │ ├── exchange.js │ ├── interaction.js │ ├── lump.js │ ├── port.js │ ├── quality-effect.js │ ├── quality-requirement.js │ ├── quality.js │ ├── setting.js │ ├── shop.js │ ├── spawned-entity.js │ ├── tile-variant.js │ └── tile.js ├── ui.js └── ui │ ├── dragndrop.js │ ├── query.js │ └── render.js ├── styles └── sunless-sea.css └── templates ├── events.json.handlebars ├── index.html.handlebars ├── objects ├── event.handlebars └── interaction.handlebars └── qualities.json.handlebars /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | game-data 4 | *.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Oracle 2 | 3 | A mod for Sunless Sea by Failbetter Games 4 | 5 | * [Players](#players) 6 | * [Modders/Developers](#sunless-sea-moddersdevelopers) 7 | * [Build Instructions](#build-instructions) 8 | 9 | ## Introduction 10 | 11 | **New players!** Tired of dying needlessly because you didn't know it was a bad idea to poke a flock of harmless-looking blue birds? 12 | 13 | **Veteran Zee-captains!** Bored of sailing endlessly in circles because you can't remember where to find a lump of Blue Scintillack for that one quest you're stuck on? 14 | 15 | Sick of constantly having to slink back to [the wiki](http://sunlesssea.gamepedia.com/) or face hours of grinding and pointless, unnecessary deaths? (Pointy *necessary* deaths, of course, still being very much a part of Sunless Sea, and rightly so). 16 | 17 | *We feel your pain.* 18 | 19 | ##### Introducing 20 | 21 | ### The Clockwork Oracle 22 | 23 | A most wonderful ~~Demonic~~ *Mechanical* Contrivance, fresh from the engineering workshops of Messirs Faust and Fhlanje of Fallen London, Royal Occultarians and Gizmographers to the Gentry. 24 | 25 | It *knows* Things. If you ask it, it will Tell you. 26 | 27 | It will Never Lie to you, but you will Remain Told. Possibly to Death. 28 | 29 | 30 | Messirs Faust and Fhlange disclaim to the fullest extend permissable by Law and Civility (and then about ten percent further again) any and all Responsibility, Inferred or Implied, for any effects on the user's Sanity, Character, Mortal Status, Personality, Psychological Stability, Physical Stability or Vital Statistics incurred as a Consequence of consulting the Aforementioned Product. 31 | 32 | Clockwork Oracle is not to be used for Prognostication, Augury or Panupunitoplasty. Any resemblance to Real Persons, Living or Dead, is probably Significant but wisest to Ignore. Do not Taunt the Clockwork Oracle. Warranty void if Clockwork Oracle consulted. 33 | 34 | ## Players 35 | 36 | ### Installing the mod 37 | 38 | [Download the release version of the mod](https://github.com/jtq/clockworacle/raw/master/release/clockworacle-mod.zip). 39 | 40 | #### Find your Sunless Sea game-data directory 41 | 42 | On Windows this is normally `C:\Users\USERNAME\AppData\LocalLow\Failbetter Games\Sunless Sea` (fill in your installation drive letter and Windows username as appropriate). 43 | 44 | If you can't find it there, try going to your user's home directory and searching in subdirectories for a folder called `Sunless Sea`. 45 | 46 | 47 | #### Install the mod files 48 | 49 | There should be a folder under your game-data directory called `addon` - create it if it doesn't exist already. 50 | 51 | Now unzip the mod files into this folder - it should automatically create a subdirectory called `clockworacle`. 52 | 53 | That's it - you're done. Enjoy! 54 | 55 | #### Run Sunless Sea 56 | 57 | If you look around Fallen London you should see a new event that involves travelling to a mysterious little shop. 58 | 59 | Once you have acquired the Clockwork Oracle you will be able to go into your hold in the game at any time, right-click on the Clockwork Oracle item and click "Use" from the menu to consult the Clockwork Oracle for hints on acquiring a wide variety of qualities, attributes, trade goods, characteristics and quest-items in the Neath. 60 | 61 | The directions are always truthful, but may be misleading, confusing or incomplete. That's what happens when you let clockwork do your thinking for you. Or possibly a bound demon in a box that hates you and wants you to die. Nobody knows exactly which. 62 | 63 | 64 | ## Sunless Sea Modders/Developers 65 | 66 | ### Installation 67 | 68 | * Download and unzip the [precompiled mod-tools](https://github.com/jtq/clockworacle/raw/master/release/modtools.zip). 69 | * Find your Sunless Sea game-data directory (using the instructions for Players above), and copy each of the `.json` files contained in the various subdirectories of your Sunless Sea game-data directory **directly** into the `modtools\game-data\json` directory (ie, the only thing in the `modtools\game-data\json` directory should be `.json` files - no subdirectories allowed). 70 | * Now in your Sunless Sea game-data directory open the `images\sn\icons` folder and copy the entire contents into the `modtools\game-data\icons` folder. 71 | * Open the `modtools\index.html` file in your browser (only tested in Chrome so far, but should probably work in others). Opening it straight from a `file:///...` URL should work fine. 72 | * Select all the json files in your game-data/json folder, and drag-and-drop them **all at once** onto the square in the web UI. 73 | 74 | ### UI 75 | 76 | #### Exploring the basic object-types 77 | 78 | After a few seconds (we're chewing through megabytes of text in a browser here!) your UI should unfreeze. From here you can use the tabs to switch between several of the fundamental types of objects available in the Sunless Sea game - Events (things that can happen in the Gazette view), Qualities (attributes on your character that are set/incremented/decremented to lock/unlock various game events and interactions, SpawnedEntities (monsters in the game-world), etc. 79 | 80 | If an item has "children" (eg, the various interaction choices offered as part of an Event) then clicking on the item will expand it to show these children (and so on for each child), allowing you to explore the game's storylines and event-sequences. 81 | 82 | Mouse-hover tooltips always try to give you a concise description of the item you're looking at, for easy reference when exploring/debugging. 83 | 84 | #### Query tab/Paths to node form 85 | 86 | The last tab is the `Query` tab - this is used for quickly and easily finding all the routes through the various game events/interactions that will lead to a certain quality being modified or event being triggered. 87 | 88 | To demonstrate its use, pick a Quality to search for - let's say "A Casket of Sapphires" (id #113012, according to its tooltip in the Qualities tab). 89 | 90 | Look at the top of the page, and make sure the Qualities/Events dropdown is set to "1. Qualities". Next you can select the type of modification you'd like to search for for the chosen Quality - "additive" means any path that acquires the quality or increments its value, "subtractive" means losing the quality or decremening its value, and "Any/None" means either. In game-terms, "additive" is roughly "how to get an item or attribute", ands "subtractive" is broadly "what it's useful for or how to get rid of it". 91 | 92 | Finally click on the `Paths to node` button, type in the ID of the "A Casket of Sapphires" quality (113012), and click `ok`. 93 | 94 | The `Query` tab should now show you a selection of trees of nodes, each rooted in a QualityEffect node that modifies the A Casket of Sapphires quality, with each leaf node generally something you can intentionally choose to do in the game, like an Area you can visit, a SpawnedEntity you can find, an Event you can trigger or an Exchange you can visit in a Port. 95 | 96 | The way to read these trees is to pick a leaf, then work your way back up the tree - for example if you chose Any/None then the first path will be: 97 | 98 | * A Casket of Sapphires -7 (QualityEffect) 99 | * "Something azure." (Interaction) 100 | * The Venturer's Desires (Event) 101 | * Fallen London (Area) 102 | 103 | This corresponds to a sequence that should (hopefully) be achievable by the player - in this case travel to Fallen London and dock there, Trigger the Venturer's Desires Event, Select the option "Something azure" when prompted, and 7 Caskets of Sapphires will be deducted from your hold. 104 | 105 | Each of these steps may have their own Quality Requirements - look them up in the appropriate tab for the full details required for each step of the chain. 106 | 107 | Finally, below the path you should see an auto-generated hint and flattened/summarised list of Quality Requirements that represents the option the Clockwork Oracle mod will display for this path. 108 | 109 | (Note that presently the Clockwork Oracle mod only offers additive paths in-game - subtractive paths are currently offered only by the UI, for debugging/exploration purposes.) 110 | 111 | ## Build Instructions 112 | 113 | ### Set up 114 | 115 | * Install node.js and npm (the node.js package manager) if you don't already have them installed 116 | * Clone the project code: `git clone https://github.com/jtq/clockworacle.git` 117 | * From inside the root folder of the repo, install the dependencies: `npm install` 118 | * Locate your game folder - usually something like `C:\Users\USERNAME\AppData\LocalLow\Failbetter Games\Sunless Sea` (where `C` is your installation drive and `USERNAME` is your Windows username). 119 | * Copy all the `.json` files (except those with `_import` in the name) under the game folder (including any in subdirectories) into the codebase's `build/modtools/game-data/json` folder (copy all `.json` files *directly* into this folder - do not replicate any sub-folders that are in your game folder) 120 | * (Optional) Copy all your game-images from the `images\sn\icons` subdirectory of your game folder into the codebase's `build/modtools/game-data/icons` folder 121 | 122 | ### Build the mod-tools 123 | 124 | * Build the browser-based UI explorer: `npm run build-ui` 125 | * Open `build/modtools/index.html` in your browser (see [Modders/Developers](#sunless-sea-moddersdevelopers) for usage instructions) 126 | 127 | ### Build the Clockwork Oracle mod 128 | 129 | * Build the mod: `npm run release-mod` 130 | * A `clockworacle-mod.zip` file containing the mod will be placed in the `release` folder (see [Players](#players) for installation instructions) 131 | 132 | 133 | ## Final Notes 134 | 135 | Compiled and up to date as of: 136 | 137 | * Sunless Sea v2.1.2.3064 138 | * Zubmariner update (11th October 2016: http://steamcommunity.com/games/304650/announcements/detail/941641850289522196) 139 | 140 | Fallen London is © 2015 and ™ Failbetter Games Limited: [www.fallenlondon.com](http://www.fallenlondon.com). This is an unofficial fan work. 141 | -------------------------------------------------------------------------------- /build/modtools/game-data/README.md: -------------------------------------------------------------------------------- 1 | # Game data files location 2 | 3 | This is the location the build system (and the HTML UI it generates) expects to find the game config JSON and game icons/artwork (which obviously can't be distributed with the mod source-code for copyright reasons). 4 | 5 | Please see the main README file for more information. 6 | -------------------------------------------------------------------------------- /build/modtools/game-data/icons/README.md: -------------------------------------------------------------------------------- 1 | # Game icons/artwork 2 | 3 | Please copy all the images from the artwork folder under your game-data directory (usually `C:\Users\USERNAME\AppData\LocalLow\Failbetter Games\Sunless Sea\images\sn\icons`, or similar) into this directory. 4 | 5 | This is an optional step, as the icons are only used by the HTML UI and are not required by the mod. However if you want to mess about exploring the game files in the UI it's highly advised as it makes things a lot prettier and easier to recognise. 6 | -------------------------------------------------------------------------------- /build/modtools/game-data/json/README.md: -------------------------------------------------------------------------------- 1 | # Game config JSON 2 | 3 | Please copy all the files from all the folders under your game-data directory (usually `C:\Users\USERNAME\AppData\LocalLow\Failbetter Games\Sunless Sea`, or similar) into this directory. 4 | 5 | ## Important 6 | 7 | Copy all the `.json` files directly into this folder - do not simply copy all the sub-folders across, because the build system is not smart enough yet to search recursively through subfolders looking for the names it expects. The `json` folder should contain nothing but a flat list of `.json` files, with no subfolders under it. 8 | 9 | **Note:** For a cleaner import of JSON data, omit copying any JSON files with "_import" in the name as these are just a cached copy of the latest game content-update. 10 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sunless Sea", 3 | "paths": { 4 | "templates": "src/templates", 5 | "modGameJson": "build/modtools/game-data/json", 6 | "builddir": { 7 | "mod": "build/clockworacle/entities", 8 | "ui": "build/modtools" 9 | } 10 | }, 11 | "locations": { 12 | "imagesPath": "game-data/icons" 13 | }, 14 | "baseGameIds": { 15 | "quality": 500000, 16 | "prelimEvent": 500010, 17 | "buyOracle": 5000020, 18 | "sellOracle": 500030, 19 | "event": 500035, 20 | "acquire": 600000, 21 | "learn": 700000, 22 | "suffer": 800000, 23 | "become": 900000 24 | } 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clockworacle", 3 | "version": "0.0.2", 4 | "description": "Clockwork Oracle mod for Sunless Sea", 5 | "author": { 6 | "name" : "jtq", 7 | "email" : "jtq.github@gmail.com", 8 | "url" : "https://github.com/jtq" 9 | }, 10 | "repository" : { 11 | "type" : "git", 12 | "url" : "https://github.com/jtq/clockworacle.git" 13 | }, 14 | "devDependencies": { 15 | "jquery": "latest", 16 | "jshint": "latest", 17 | "browserify": "latest", 18 | "jasmine": "latest", 19 | "render-cli": "latest", 20 | "handlebars": "latest", 21 | "filereader": "latest", 22 | "File": "latest" 23 | }, 24 | "scripts": { 25 | "build": "npm run build-ui && npm run build-mod", 26 | 27 | "prebuild-ui": "mkdir -p build/modtools", 28 | "build-ui": "npm run compilecss && npm run compilejs && npm run compilehtml", 29 | "compilecss": "cp src/styles/sunless-sea.css build/modtools/sunless-sea.css", 30 | "compilejs": "browserify src/**/*.js -o build/modtools/sunless-sea.js --debug", 31 | "postcompilejs": "cp -r lib build/modtools/", 32 | "compilehtml": "render src/templates/index.html.handlebars --context config.json -o build/modtools/index.html", 33 | "postbuild-ui": "mkdir -p build/modtools/game-data/json && mkdir -p build/modtools/game-data/icons", 34 | 35 | "prebuild-mod": "rm -rf build/clockworacle && mkdir -p build/clockworacle/entities", 36 | "build-mod": "node scripts/create-mod.js", 37 | 38 | "release": "npm run release-ui && npm run release-mod", 39 | 40 | "prerelease-ui": "npm run build-ui && mkdir -p release", 41 | "release-ui": "rm release/modtools.zip; cd build; zip -r ../release/modtools.zip modtools -x modtools/game-data/json/*.json -x modtools/game-data/icons/*.png -x modtools/game-data/icons/*.db", 42 | 43 | "prerelease-mod": "npm run build-mod && mkdir -p release", 44 | "release-mod": "rm release/clockworacle-mod.zip; cd build; zip -r ../release/clockworacle-mod.zip clockworacle", 45 | 46 | "lint": "jshint src/ spec/", 47 | "test": "jasmine", 48 | "env": "env" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /release/clockworacle-mod.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtq/clockworacle/65fcde90a73f1f9dfb95725d3d15d7ce3c92ed9b/release/clockworacle-mod.zip -------------------------------------------------------------------------------- /release/modtools.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtq/clockworacle/65fcde90a73f1f9dfb95725d3d15d7ce3c92ed9b/release/modtools.zip -------------------------------------------------------------------------------- /scripts/create-mod.js: -------------------------------------------------------------------------------- 1 | var config = require('../config.json'); 2 | var api = require('../src/scripts/api'); 3 | var File = require('File'); 4 | var fs = require('fs'); 5 | 6 | var io = require('../src/scripts/io'); 7 | var query = require('../src/scripts/ui/query'); 8 | 9 | var Handlebars = require('handlebars'); 10 | 11 | var templates = loadModTemplates(); 12 | loadGameData(function() { 13 | renderTemplates(generateJSON()); 14 | }); 15 | 16 | function loadGameData(onAllLoaded) { 17 | io.resetFilesToLoad(); 18 | Object.keys(io.fileObjectMap).forEach(function(filename) { 19 | var typeName = io.fileObjectMap[filename]; 20 | var Type = api.types[typeName]; 21 | 22 | var filepath = config.paths.modGameJson+'/'+filename; 23 | var file = new File(filepath); 24 | console.log('Reading game-data', filepath); 25 | 26 | io.incrementFilesToLoad(); 27 | api.readFromFile(Type, file, function(contents, type, clump) { 28 | io.decrementFilesToLoad(); 29 | 30 | if(io.countFilesToLoad() === 0) { 31 | Object.keys(api.library).forEach(function(typeName) { 32 | console.log("Loaded", typeName, api.library[typeName].size()+' items'); 33 | }); 34 | console.log('Loading sub-objects and wiring up object references'); 35 | api.wireUpObjects(); 36 | Object.keys(api.library).forEach(function(typeName) { 37 | console.log("Discovered", typeName, api.library[typeName].size()+' items'); 38 | }); 39 | 40 | onAllLoaded(); 41 | } 42 | }); 43 | 44 | }); 45 | } 46 | 47 | function loadModTemplates() { 48 | return { 49 | events: fs.readFileSync(config.paths.templates+'/events.json.handlebars', { encoding:'utf8' }), 50 | qualities: fs.readFileSync(config.paths.templates+'/qualities.json.handlebars', { encoding:'utf8' }) 51 | }; 52 | } 53 | 54 | 55 | function generateJSON() { 56 | var json = { 57 | buildDateTime: new Date().toISOString(), 58 | baseGameIds: api.config.baseGameIds 59 | }; 60 | 61 | var additionalEvents = []; 62 | 63 | var temp; 64 | 65 | temp = generateObjectListFromClump(api.config.baseGameIds.acquire, api.library.Quality.query('Tag', 'Goods'), json); 66 | json.eventAcquireChildren = temp.interactions; 67 | additionalEvents = additionalEvents.concat(temp.additionalEvents); 68 | 69 | temp = generateObjectListFromClump(api.config.baseGameIds.learn, api.library.Quality.query('Tag', 'Knowledge').query('Category', 'Curiosity'), json); 70 | json.eventLearnChildren = temp.interactions; 71 | additionalEvents = additionalEvents.concat(temp.additionalEvents); 72 | 73 | temp = generateObjectListFromClump(api.config.baseGameIds.suffer, api.library.Quality.query('Tag', 'Menace').query('Nature', 'Status').query('Category', 'Story'), json); 74 | json.eventSufferChildren = temp.interactions; 75 | additionalEvents = additionalEvents.concat(temp.additionalEvents); 76 | 77 | temp = generateObjectListFromClump(api.config.baseGameIds.become, api.library.Quality.query('Tag', 'Abilities').query('Category', 'SidebarAbility'), json); 78 | json.eventBecomeChildren = temp.interactions; 79 | additionalEvents = additionalEvents.concat(temp.additionalEvents); 80 | 81 | json.additionalEvents = additionalEvents; 82 | 83 | return json; 84 | } 85 | 86 | function generateObjectListFromClump(baseId, clump, data) { 87 | var Type = clump.type; 88 | var interactions = []; 89 | var additionalEvents = []; 90 | 91 | var newItemId = baseId; 92 | 93 | var interactionTemplate = Handlebars.compile(fs.readFileSync(config.paths.templates+'/objects/interaction.handlebars', { encoding:'utf8' })); 94 | var eventTemplate = Handlebars.compile(fs.readFileSync(config.paths.templates+'/objects/event.handlebars', { encoding:'utf8' })); 95 | 96 | var chosenQualityEvents = []; 97 | 98 | clump.forEach(function(quality, id, collection) { 99 | 100 | // Quality interaction object 101 | quality.Id = ++newItemId; // Id of this new item, not the Id of the existing game-item it relates to 102 | var defaultEventId = ++newItemId; 103 | var linkToEventId = ++newItemId; 104 | 105 | 106 | var linkToEvent = eventTemplate({ 107 | Id: linkToEventId, 108 | Name: '', 109 | Description: 'LinkTo event for '+quality.Name, 110 | Image: 'null', 111 | buildDateTime: data.buildDateTime, 112 | linkToEvent: "null", 113 | interactionChildren: '' 114 | }); 115 | 116 | // Double-event (Interaction->DefaultEvent->LinktoEvent) for this interaction (to hang the various route interactions off) 117 | quality.defaultEvent = eventTemplate({ 118 | Id: defaultEventId, 119 | Name: '', 120 | Description: prepareString(quality.Description), 121 | Image: '', 122 | buildDateTime: data.buildDateTime, 123 | interactionChildren: '', 124 | linkToEvent: linkToEvent 125 | }); 126 | 127 | quality.Name = prepareString(quality.Name); 128 | quality.Description = ''; 129 | quality.buildDateTime = data.buildDateTime; 130 | quality.qualityRequirements = '[]'; 131 | 132 | interactions.push(interactionTemplate(quality)); 133 | 134 | 135 | // Generate interactions to affect this quality 136 | var interactionChildren = []; 137 | var routesToNode = query.filterPathsToNode(query.pathsToNode(quality, {}), 'additive'); 138 | var hints = []; 139 | getHints(routesToNode, [], hints); 140 | 141 | var routesArray = []; 142 | getAncestryLists(routesToNode, [], routesArray); 143 | 144 | for(var i=0; i
You can\'t help but wonder at what cost, however.', 155 | Image: quality.Image || 'null', 156 | buildDateTime: data.buildDateTime, 157 | interactionChildren: '', 158 | linkToEvent: '{ "Id": '+config.baseGameIds.event+' }' 159 | }); 160 | 161 | var requirements = query.getRouteRequirements(routesArray[i]); 162 | 163 | interactionChildren.push(interactionTemplate({ 164 | Id: interactionId, 165 | Name: prepareString(hint), 166 | Description: '', 167 | buildDateTime: data.buildDateTime, 168 | qualityRequirements: JSON.stringify(requirements 169 | .map(function(req) { 170 | req.attribs.Id = ++newItemId; 171 | req.attribs.Name = prepareString(req.attribs.Name); 172 | req.attribs.VisibleWhenRequirementFailed = true; 173 | return req.attribs; }) 174 | ), 175 | defaultEvent: specificInteractionDefaultEvent 176 | })); 177 | } 178 | 179 | chosenQualityEvents.push(eventTemplate({ 180 | Id: linkToEventId, 181 | Name: prepareString(quality.Name), 182 | Description: 'To acquire '+prepareString(quality.Name)+' you may...', 183 | Image: quality.Image || 'null', 184 | buildDateTime: data.buildDateTime, 185 | linkToEvent: 'null', 186 | interactionChildren: interactionChildren 187 | })); 188 | 189 | }); 190 | 191 | additionalEvents = additionalEvents.concat(chosenQualityEvents); 192 | 193 | return { 194 | interactions: interactions, 195 | additionalEvents: additionalEvents 196 | }; 197 | } 198 | 199 | function getHints(routeNode, ancestry, hints) { 200 | var new_ancestry = ancestry.slice(); 201 | new_ancestry.push(routeNode.node); 202 | routeNode.children.forEach(function(child_route, index, children) { 203 | getHints(child_route, new_ancestry, hints); 204 | }); 205 | 206 | if(!routeNode.children.length) { // Leaf node 207 | var hint = query.describeRoute(new_ancestry); 208 | hints.push(hint); 209 | } 210 | 211 | return hints; 212 | } 213 | 214 | function getAncestryLists(routeNode, ancestry, all) { 215 | var new_ancestry = ancestry.slice(); 216 | new_ancestry.push(routeNode.node); 217 | routeNode.children.forEach(function(child_route, index, children) { 218 | var child_ancestry = new_ancestry.slice(); 219 | getAncestryLists(child_route, child_ancestry, all); 220 | }); 221 | 222 | if(!routeNode.children.length) { // Leaf node 223 | all.push(ancestry); 224 | } 225 | } 226 | 227 | function getDetails(routeNode, ancestry, details) { 228 | var new_ancestry = ancestry.slice(); 229 | new_ancestry.push(routelNode.node); 230 | routeNode.children.forEach(function(child_route, index, children) { 231 | getDetails(child_route, new_ancestry, details); 232 | }); 233 | 234 | if(!routeNode.children.length) { // Leaf node 235 | var detail = query.detailRoute(new_ancestry); 236 | details.push(detail); 237 | } 238 | 239 | return details; 240 | } 241 | 242 | function prepareString(raw) { 243 | return Handlebars.Utils.escapeExpression(raw) 244 | .replace(/\s*\[[^\]]*\]/g, '') 245 | .replace(/'/g, '\'') 246 | .replace(/"/g, '\\"') 247 | .replace(/&/g, '&'); 248 | } 249 | 250 | function renderTemplates(data) { 251 | 252 | var ids = JSON.parse(JSON.stringify(data.baseGameIds)); 253 | 254 | Handlebars.registerHelper('id', function(name) { 255 | ids[name] = (typeof ids[name] === "undefined") ? 0 : ids[name]; 256 | return ids[name]; 257 | }); 258 | 259 | Handlebars.registerHelper('increment', function(name, amount) { 260 | amount = (typeof amount === "undefined" || typeof amount === "object") ? 1 : amount; 261 | ids[name] += amount; 262 | return ids[name]; 263 | }); 264 | 265 | Handlebars.registerHelper('bumpToNext', function(name, amount) { 266 | amount = (typeof amount === "undefined" || typeof amount === "object") ? 100 : amount; 267 | ids[name] = Math.ceil((ids[name]+1)/amount) * amount; 268 | return ids[name]; 269 | }); 270 | 271 | var qualitiesTemplate = Handlebars.compile(templates.qualities); 272 | var eventsTemplate = Handlebars.compile(templates.events); 273 | fs.writeFileSync(config.paths.builddir.mod+'/qualities.json', qualitiesTemplate(data), { encoding:'utf8' }); 274 | fs.writeFileSync(config.paths.builddir.mod+'/events.json', eventsTemplate(data), { encoding:'utf8' }); 275 | } -------------------------------------------------------------------------------- /spec/scripts/objects/clump.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var Clump = require("../../../src/scripts/objects/clump"); 3 | 4 | describe('Clump', function() { 5 | 6 | describe('#constructor()', function () { 7 | 8 | var StubType = function(raw, parent) { 9 | var self = this; 10 | Object.keys(raw).forEach(function(key) { 11 | self[key] = raw[key]; 12 | }); 13 | 14 | this.parents = []; 15 | if(parent) { 16 | this.parents.push(parent); 17 | } 18 | }; 19 | 20 | it('reads array of untyped objects into items', function () { 21 | 22 | var raw = [{ Id:1 }, { Id:2 }, { Id:3 }]; 23 | 24 | var clump = new Clump(raw, StubType); 25 | 26 | expect(Object.keys(clump.items).length).toBe(3); 27 | expect(clump.size()).toBe(3); 28 | }); 29 | 30 | it('reads array of type-objects into items', function () { 31 | 32 | var typeArray = [ 33 | new StubType({ Id:1 }), 34 | new StubType({ Id:2 }), 35 | new StubType({ Id:3 }) 36 | ]; 37 | 38 | var clump = new Clump(typeArray, StubType); 39 | 40 | expect(Object.keys(clump.items).length).toBe(3); 41 | expect(clump.size()).toBe(3); 42 | }); 43 | 44 | it('passes parent param through to new clump items\' constructor', function () { 45 | 46 | var sampleParent = { Id:"sampleParent" }; 47 | var raw = [{ Id:"Plain POJO" }, new StubType({ Id:"Prebuild StubType" }), { Id:"POJO w/ empty parent", parents:[] }, { Id:"POJO w/ sample parent", parents:[sampleParent] } ]; 48 | 49 | var clump = new Clump(raw, StubType, sampleParent); 50 | 51 | expect(clump.items["Plain POJO"].parents[0]).toBe(sampleParent); 52 | expect(clump.items["Prebuild StubType"].parents[0]).toBe(sampleParent); 53 | expect(clump.items["POJO w/ empty parent"].parents[0]).toBe(sampleParent); 54 | expect(clump.items["POJO w/ sample parent"].parents.length).toBe(1); 55 | }); 56 | 57 | }); 58 | 59 | }); -------------------------------------------------------------------------------- /spec/scripts/objects/lump.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var Lump = require("../../../src/scripts/objects/lump"); 3 | 4 | describe('Lump', function() { 5 | 6 | describe('#getStates()', function () { 7 | 8 | it('should split states string into a map of id:description', function () { 9 | var lump = new Lump({ Id:null }); 10 | var result = lump.getStates("0|Zero~1|One~2|Two"); 11 | 12 | expect(result).toEqual({ 13 | '0':'Zero', 14 | '1':'One', 15 | '2':'Two' 16 | }); 17 | }); 18 | 19 | it('should return null for empty strings', function () { 20 | var lump = new Lump({ Id:null }); 21 | var result = lump.getStates(""); 22 | 23 | expect(result).toBe(null); 24 | }); 25 | 26 | it('should return null for non-strings', function () { 27 | var lump = new Lump({ Id:null }); 28 | var result = lump.getStates({}); 29 | 30 | expect(result).toBe(null); 31 | }); 32 | }); 33 | 34 | }); -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/scripts/api.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config.json'); 2 | var Clump = require('./objects/clump'); 3 | var Lump = require('./objects/lump'); 4 | 5 | var io = require('./io'); 6 | 7 | var library = require('./library'); 8 | var loaded = {}; 9 | 10 | var types = { 11 | Quality: require('./objects/quality'), 12 | Event: require('./objects/event'), 13 | Interaction: require('./objects/interaction'), 14 | QualityEffect: require('./objects/quality-effect'), 15 | QualityRequirement: require('./objects/quality-requirement'), 16 | Area: require('./objects/area'), 17 | SpawnedEntity: require('./objects/spawned-entity'), 18 | CombatAttack: require('./objects/combat-attack'), 19 | Exchange: require('./objects/exchange'), 20 | Shop: require('./objects/shop'), 21 | Availability: require('./objects/availability'), 22 | Tile: require('./objects/tile'), 23 | TileVariant: require('./objects/tile-variant'), 24 | Port: require('./objects/port'), 25 | Setting: require('./objects/setting') 26 | }; 27 | 28 | // Prepopulate library with Clumps of each type we know about 29 | Object.keys(types).forEach(function(typeName) { 30 | var Type = types[typeName]; 31 | if(!library[typeName]) { 32 | library[typeName] = new Clump([], Type); 33 | loaded[typeName] = new Clump([], Type); 34 | } 35 | }); 36 | 37 | function get(Type, id, parent) { 38 | var typename = Type.name; // Event, Quality, Interaction, etc 39 | 40 | var existingThingWithThisId = library[typename].id(id); 41 | if(existingThingWithThisId) { 42 | //console.log("Attached existing " + existingThingWithThisId + " to " + this.toString()) 43 | var newParent = true; 44 | existingThingWithThisId.parents.forEach(function(p) { 45 | if(p.Id === parent.Id && p.constructor.name === parent.constructor.name) { 46 | newParent = false; 47 | } 48 | }); 49 | if(newParent){ 50 | existingThingWithThisId.parents.push(parent); 51 | } 52 | 53 | if(!existingThingWithThisId.wired) { 54 | existingThingWithThisId.wireUp(this); // Pass in the api so object can add itself to the master-library 55 | } 56 | return existingThingWithThisId; 57 | } 58 | else { 59 | return null; 60 | } 61 | } 62 | 63 | function getOrCreate(Type, possNewThing, parent) { // If an object already exists with this ID, use that. Otherwise create a new object from the supplied details hash 64 | var typename = Type.name; // Event, Quality, Interaction, etc 65 | if(possNewThing) { 66 | var existingThingWithThisId = this.get(Type, possNewThing.Id, parent); 67 | if(existingThingWithThisId) { 68 | return existingThingWithThisId; 69 | } 70 | else { 71 | var newThing = new Type(possNewThing, parent); 72 | newThing.wireUp(this); 73 | //console.log("Recursively created " + newThing + " for " + this.toString()); 74 | return newThing; 75 | } 76 | } 77 | else { 78 | return null; 79 | } 80 | } 81 | 82 | function wireUpObjects() { 83 | var api = this; 84 | Object.keys(types).forEach(function(type) { 85 | library[type].forEach(function(lump) { 86 | if(lump.wireUp) { 87 | lump.wireUp(api); 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | var whatIs = function(id) { 94 | var possibilities = []; 95 | Object.keys(library).forEach(function(key) { 96 | if(library[key] instanceof Clump && library[key].id(id)) { 97 | possibilities.push(key); 98 | } 99 | }); 100 | return possibilities; 101 | }; 102 | 103 | function describeAdvancedExpression(expr) { 104 | var self = this; 105 | if(expr) { 106 | expr = expr.replace(/\[d:(\d+)\]/gi, "RANDOM[1-$1]"); // [d:x] = random number from 1-x(?) 107 | expr = expr.replace(/\[q:(\d+)\]/gi, function(match, backref, pos, whole_str) { 108 | var quality = self.library.Quality.id(backref); 109 | return "["+(quality ? quality.Name : 'INVALID')+"]"; 110 | }); 111 | 112 | return expr; 113 | } 114 | return null; 115 | } 116 | 117 | function readFromFile(Type, file, callback) { 118 | io.readFile(file, function (e) { 119 | var contents = e.target.result; 120 | 121 | var obj = JSON.parse(contents); 122 | loaded[Type.prototype.constructor.name] = new Clump(obj, Type); 123 | 124 | callback(contents, Type, loaded[Type.prototype.constructor.name]); 125 | }); 126 | } 127 | 128 | 129 | module.exports = { 130 | 'Clump': Clump, 131 | 'Lump': Lump, 132 | 'config': config, 133 | 'types': types, 134 | 'library': library, 135 | 'loaded': loaded, 136 | 'get': get, 137 | 'whatIs': whatIs, 138 | 'wireUpObjects': wireUpObjects, 139 | 'getOrCreate': getOrCreate, 140 | 'describeAdvancedExpression': describeAdvancedExpression, 141 | 'readFromFile': readFromFile 142 | }; -------------------------------------------------------------------------------- /src/scripts/io.js: -------------------------------------------------------------------------------- 1 | 2 | if(typeof FileReader === 'undefined') { // Running in node rather than a browser 3 | FileReader = require('filereader'); 4 | } 5 | 6 | var fileObjectMap = { 7 | 'events.json' : 'Event', 8 | 'qualities.json' : 'Quality', 9 | 'areas.json' : 'Area', 10 | 'SpawnedEntities.json' : 'SpawnedEntity', 11 | 'CombatAttacks.json' : 'CombatAttack', 12 | 'exchanges.json' : 'Exchange', 13 | 'Tiles.json': 'Tile' 14 | }; 15 | 16 | function readFile(file, callback) { 17 | var reader = new FileReader(); 18 | reader.onload = callback; 19 | reader.readAsText(file); 20 | } 21 | 22 | var files_to_load = 0; 23 | function resetFilesToLoad() { 24 | files_to_load = 0; 25 | } 26 | function incrementFilesToLoad() { 27 | files_to_load++; 28 | } 29 | function decrementFilesToLoad() { 30 | files_to_load--; 31 | } 32 | function countFilesToLoad() { 33 | return files_to_load; 34 | } 35 | 36 | 37 | module.exports = { 38 | readFile: readFile, 39 | resetFilesToLoad: resetFilesToLoad, 40 | incrementFilesToLoad: incrementFilesToLoad, 41 | decrementFilesToLoad: decrementFilesToLoad, 42 | countFilesToLoad: countFilesToLoad, 43 | fileObjectMap: fileObjectMap 44 | }; -------------------------------------------------------------------------------- /src/scripts/library.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/scripts/objects/area.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function Area(raw) { 6 | this.straightCopy = ["Name", "Description", "ImageName", "MoveMessage"]; 7 | Lump.call(this, raw); 8 | } 9 | Object.keys(Lump.prototype).forEach(function(member) { Area.prototype[member] = Lump.prototype[member]; }); 10 | 11 | Area.prototype.wireUp = function(theApi) { 12 | api = theApi; 13 | Lump.prototype.wireUp.call(this); 14 | }; 15 | 16 | Area.prototype.toString = function() { 17 | return this.constructor.name + " " + this.Name + " (#" + this.Id + ")"; 18 | }; 19 | 20 | Area.prototype.toDom = function(size) { 21 | 22 | size = size || "normal"; 23 | 24 | var element = document.createElement("li"); 25 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 26 | 27 | if(this.ImageName !== null && this.Image !== "") { 28 | element.innerHTML = ""; 29 | } 30 | 31 | element.innerHTML += "\n

"+this.Name+"

\n

"+this.Description+"

"; 32 | 33 | element.title = this.toString(); 34 | 35 | return element; 36 | }; 37 | 38 | module.exports = Area; -------------------------------------------------------------------------------- /src/scripts/objects/availability.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function Availability(raw, parent) { 6 | this.straightCopy = [ 7 | 'Cost', 8 | 'SellPrice' 9 | ]; 10 | Lump.apply(this, arguments); 11 | 12 | this.quality = null; 13 | this.purchaseQuality = null; 14 | } 15 | Object.keys(Lump.prototype).forEach(function(member) { Availability.prototype[member] = Lump.prototype[member]; }); 16 | 17 | Availability.prototype.wireUp = function(theApi) { 18 | 19 | api = theApi; 20 | 21 | this.quality = api.getOrCreate(api.types.Quality, this.attribs.Quality, this); 22 | this.purchaseQuality = api.getOrCreate(api.types.Quality, this.attribs.PurchaseQuality, this); 23 | 24 | Lump.prototype.wireUp.call(this, api); 25 | }; 26 | 27 | Availability.prototype.isAdditive = function() { 28 | return this.Cost > 0; 29 | }; 30 | 31 | Availability.prototype.isSubtractive = function() { 32 | return this.SellPrice > 0; 33 | }; 34 | 35 | Availability.prototype.toString = function() { 36 | return this.constructor.name + " " + this.quality + " (buy: " + this.Cost + "x" + this.purchaseQuality.Name + " / sell: " + this.SellPrice + "x" + this.purchaseQuality.Name + ")"; 37 | }; 38 | 39 | Availability.prototype.toDom = function(size) { 40 | 41 | size = size || "small"; 42 | 43 | var element = document.createElement("li"); 44 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 45 | 46 | var purchase_quality_element; 47 | 48 | if(!this.quality) { 49 | purchase_quality_element = document.createElement("span"); 50 | purchase_quality_element.innerHTML = "[INVALID]"; 51 | } 52 | else { 53 | purchase_quality_element = this.quality.toDom("small", false, "span"); 54 | } 55 | 56 | var currency_quality_element = this.purchaseQuality.toDom("small", false, "span"); 57 | currency_quality_element.className = "quantity item small"; 58 | var currency_quality_markup = currency_quality_element.outerHTML; 59 | 60 | var currency_buy_amount_element = document.createElement("span"); 61 | currency_buy_amount_element.className = "item quantity"; 62 | currency_buy_amount_element.innerHTML = "Buy: " + (this.Cost ? this.Cost+"x" : "✗"); 63 | currency_buy_amount_element.title = this.toString(); 64 | 65 | var currency_sell_amount_element = document.createElement("span"); 66 | currency_sell_amount_element.className = "item quantity"; 67 | currency_sell_amount_element.innerHTML = "Sell: " + (this.SellPrice ? this.SellPrice+"x" : "✗"); 68 | currency_sell_amount_element.title = this.toString(); 69 | 70 | 71 | element.appendChild(purchase_quality_element); 72 | element.appendChild(currency_buy_amount_element); 73 | if(this.Cost) { 74 | element.appendChild($(currency_quality_markup)[0]); 75 | } 76 | element.appendChild(currency_sell_amount_element); 77 | if(this.SellPrice) { 78 | element.appendChild($(currency_quality_markup)[0]); 79 | } 80 | 81 | return element; 82 | }; 83 | 84 | module.exports = Availability; -------------------------------------------------------------------------------- /src/scripts/objects/clump.js: -------------------------------------------------------------------------------- 1 | 2 | function Clump(raw, Type, parent) { 3 | this.type = Type; 4 | this.items = {}; 5 | var self = this; 6 | raw.forEach(function(item, index, collection) { 7 | if(!(item instanceof Type)) { 8 | item = new Type(item, parent); 9 | } 10 | else if(parent) { 11 | var newParent = true; 12 | item.parents.forEach(function(p) { 13 | if(p.Id === parent.Id && p.constructor.name === parent.constructor.name) { 14 | newParent = false; 15 | } 16 | }); 17 | if(newParent){ 18 | item.parents.push(parent); 19 | } 20 | } 21 | if(self.items[item.Id]) { 22 | console.warn(Type.name+" "+item.Id+" already exists in this clump. Duplicated IDs for different objects in the game-json?") 23 | } 24 | self.items[item.Id] = item; 25 | }); 26 | } 27 | 28 | Clump.prototype.empty = function() { 29 | return !!this.size(); 30 | }; 31 | 32 | Clump.prototype.size = function() { 33 | return Object.keys(this.items).length; 34 | }; 35 | 36 | Clump.prototype.get = function(index) { 37 | for(var id in this.items) { 38 | if(index === 0) { 39 | return this.items[id]; 40 | } 41 | index--; 42 | } 43 | }; 44 | 45 | Clump.prototype.id = function(id) { 46 | return this.items[id]; 47 | }; 48 | 49 | Clump.prototype.each = function() { 50 | var args = Array.prototype.slice.call(arguments); 51 | return this.map(function(item) { 52 | 53 | if(args[0] instanceof Array) { // Passed in array of fields, so return values concatenated with optional separator 54 | var separator = (typeof args[1] === "undefined") ? "-" : args[1]; 55 | return args[0].map(function(f) { return item[f]; }).join(separator); 56 | } 57 | else if(args.length > 1) { // Passed in separate fields, so return array of values 58 | return args.map(function(f) { return item[f]; }); 59 | } 60 | else { 61 | return item[args[0]]; 62 | } 63 | }); 64 | }; 65 | 66 | Clump.prototype.forEach = function(callback) { 67 | for(var id in this.items) { 68 | var item = this.items[id]; 69 | callback(item, id, this.items); 70 | } 71 | }; 72 | 73 | Clump.prototype.map = function(callback) { 74 | var self = this; 75 | var arrayOfItems = Object.keys(this.items).map(function(key) { 76 | return self.items[key]; 77 | }); 78 | return arrayOfItems.map.call(arrayOfItems, callback); 79 | }; 80 | 81 | Clump.prototype.sortBy = function(field, reverse) { 82 | var self = this; 83 | var objs = Object.keys(this.items).map(function(key) { 84 | return self.items[key]; 85 | }).sort(function(a, b) { 86 | if(a[field] < b[field]) { 87 | return -1; 88 | } 89 | if(a[field] === b[field]) { 90 | return 0; 91 | } 92 | if(a[field] > b[field]) { 93 | return 1; 94 | } 95 | }); 96 | 97 | return reverse ? objs.reverse() : objs; 98 | }; 99 | 100 | Clump.prototype.same = function() { 101 | var self = this; 102 | 103 | var clone = function(obj) { 104 | var target = {}; 105 | for (var i in obj) { 106 | if (obj.hasOwnProperty(i)) { 107 | if(typeof obj[i] === "object") { 108 | target[i] = clone(obj[i]); 109 | } 110 | else { 111 | target[i] = obj[i]; 112 | } 113 | } 114 | } 115 | return target; 116 | }; 117 | 118 | var template = clone(this.get(0).attribs); 119 | 120 | for(var id in this.items) { 121 | var otherObj = this.items[id].attribs; 122 | for(var key in template) { 123 | if(template[key] !== otherObj[key]) { 124 | delete(template[key]); 125 | } 126 | } 127 | } 128 | 129 | return template; 130 | }; 131 | 132 | Clump.prototype.distinct = function(field) { 133 | var sampleValues = {}; 134 | this.forEach(function(item) { 135 | var value = item[field]; 136 | sampleValues[value] = value; // Cheap de-duping with a hash 137 | }); 138 | return Object.keys(sampleValues).map(function(key) { return sampleValues[key]; }); 139 | }; 140 | 141 | Clump.prototype.distinctRaw = function(field) { 142 | var sampleValues = {}; 143 | this.forEach(function(item) { 144 | var value = item.attribs[field]; 145 | sampleValues[value] = value; // Cheap de-duping with a hash 146 | }); 147 | return Object.keys(sampleValues).map(function(key) { return sampleValues[key]; }); 148 | }; 149 | 150 | Clump.prototype.query = function(field, value) { 151 | var matches = []; 152 | var test; 153 | 154 | // Work out what sort of comparison to do: 155 | 156 | if(typeof value === "function") { // If value is a function, pass it the candidate and return the result 157 | test = function(candidate) { 158 | return !!value(candidate); 159 | }; 160 | } 161 | else if(typeof value === "object") { 162 | if(value instanceof RegExp) { 163 | test = function(candidate) { 164 | return value.test(candidate); 165 | }; 166 | } 167 | else if(value instanceof Array) { // If value is an array, test for the presence of the candidate value in the array 168 | test = function(candidate) { 169 | return value.indexOf(candidate) !== -1; 170 | }; 171 | } 172 | else { 173 | test = function(candidate) { 174 | return candidate === value; // Handle null, undefined or object-reference comparison 175 | }; 176 | } 177 | } 178 | else { // Else if it's a simple type, try a strict equality comparison 179 | test = function(candidate) { 180 | return candidate === value; 181 | }; 182 | } 183 | 184 | // Now iterate over the items, filtering using the test function we defined 185 | this.forEach(function(item) { 186 | if( 187 | (field !== null && test(item[field])) || 188 | (field === null && test(item)) 189 | ) { 190 | matches.push(item); 191 | } 192 | }); 193 | return new Clump(matches, this.type); // And wrap the resulting array of objects in a new Clump object for sexy method chaining like x.query().forEach() or x.query().query() 194 | }; 195 | 196 | Clump.prototype.queryRaw = function(field, value) { 197 | var matches = []; 198 | var test; 199 | 200 | // Work out what sort of comparison to do: 201 | 202 | if(typeof value === "function") { // If value is a function, pass it the candidate and return the result 203 | test = function(candidate) { 204 | return !!value(candidate); 205 | }; 206 | } 207 | else if(typeof value === "object") { 208 | if(value instanceof RegExp) { 209 | test = function(candidate) { 210 | return value.test(candidate); 211 | }; 212 | } 213 | else if(value instanceof Array) { // If value is an array, test for the presence of the candidate value in the array 214 | test = function(candidate) { 215 | return value.indexOf(candidate) !== -1; 216 | }; 217 | } 218 | else { // If value is a hash... what do we do? 219 | // Check the candidate for each field in the hash in turn, and include the candidate if any/all of them have the same value as the corresponding value-hash field? 220 | throw "No idea what to do with an object as the value"; 221 | } 222 | } 223 | else { // Else if it's a simple type, try a strict equality comparison 224 | test = function(candidate) { 225 | return candidate === value; 226 | }; 227 | } 228 | 229 | // Now iterate over them all, filtering using the test function we defined 230 | this.forEach(function(item) { 231 | if( 232 | (field !== null && test(item.attribs[field])) || 233 | (field === null && test(item.attribs)) 234 | ) { 235 | matches.push(item); 236 | } 237 | }); 238 | return new Clump(matches, this.type); // And wrap the resulting array of objects in a new Clump object for sexy method chaining like x.query().forEach() or x.query().query() 239 | }; 240 | 241 | Clump.prototype.toString = function() { 242 | return this.type.name + " Clump (" + this.size() + " items)"; 243 | }; 244 | 245 | Clump.prototype.toDom = function(size, includeChildren, tag, firstChild) { 246 | 247 | size = size || "normal"; 248 | tag = tag || "ul"; 249 | 250 | var element = document.createElement(tag); 251 | element.className = this.constructor.name.toLowerCase()+"-list "+size; 252 | if(firstChild) { 253 | element.appendChild(firstChild); 254 | } 255 | this.sortBy("Name").forEach(function(i) { 256 | element.appendChild(i.toDom(size, includeChildren)); 257 | }); 258 | return element; 259 | }; 260 | 261 | Clump.prototype.describe = function() { 262 | var self = this; 263 | return Object.keys(this.items).map(function(i) { return self.items[i].toString(); }).join(" and "); 264 | }; 265 | 266 | module.exports = Clump; -------------------------------------------------------------------------------- /src/scripts/objects/combat-attack.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function CombatAttack(raw, parent) { 7 | this.straightCopy = [ 8 | 'Name', 9 | 'Image', 10 | 'RammingAttack', 11 | 'OnlyWhenExposed', 12 | 'Range', 13 | 'Orientation', 14 | 'Arc', 15 | 'BaseHullDamage', 16 | 'BaseLifeDamage', 17 | 'ExposedQualityDamage', // Value to add to the exposedQuality: positive increases quality level (eg Terror), negative decreases it (eg Crew) 18 | 'StaggerAmount', 19 | 'BaseWarmUp', 20 | 'Animation', 21 | 'AnimationNumber' 22 | ]; 23 | raw.Id = raw.Name; 24 | Lump.apply(this, arguments); 25 | 26 | this.qualityRequired = null; 27 | this.qualityCost = null; 28 | this.exposedQuality = null; 29 | } 30 | 31 | Object.keys(Lump.prototype).forEach(function(member) { CombatAttack.prototype[member] = Lump.prototype[member]; }); 32 | 33 | CombatAttack.prototype.wireUp = function(theApi) { 34 | 35 | api = theApi; 36 | 37 | this.qualityRequired = api.get(api.types.Quality, this.attribs.QualityRequiredId, this); 38 | this.qualityCost = api.get(api.types.Quality, this.attribs.QualityCostId, this); 39 | this.exposedQuality = api.get(api.types.Quality, this.attribs.ExposedQualityId, this); 40 | 41 | Lump.prototype.wireUp.call(this, api); 42 | }; 43 | 44 | CombatAttack.prototype.toString = function() { 45 | return this.constructor.name + " " + this.Name + " (#" + this.Id + ")"; 46 | }; 47 | 48 | CombatAttack.prototype.toDom = function(size, includeChildren) { 49 | 50 | size = size || "normal"; 51 | includeChildren = includeChildren === false ? false : true; 52 | 53 | var self = this; 54 | 55 | var html = ""; 56 | 57 | var element = document.createElement("li"); 58 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 59 | 60 | if(this.Image !== null && this.Image !== "") { 61 | html = ""; 62 | } 63 | 64 | html += "\n

"+this.Name+"

"; 65 | 66 | if(this.qualityRequired || this.qualityCost) { 67 | html += ""; 78 | } 79 | 80 | html += "
"; 81 | ['Range', 'Arc', 'BaseHullDamage', 'BaseLifeDamage', 'StaggerAmount', 'BaseWarmUp'].forEach(function(key) { 82 | html += "
"+key+"
"+self[key]+"
"; 83 | }); 84 | html += "
"; 85 | 86 | element.innerHTML = html; 87 | 88 | element.title = this.toString(); 89 | 90 | if(includeChildren) { 91 | element.addEventListener("click", function(e) { 92 | e.stopPropagation(); 93 | 94 | var childList = element.querySelector(".child-list"); 95 | if(childList) { 96 | element.removeChild(childList); 97 | } 98 | else { 99 | var successEvent = self.successEvent; 100 | var defaultEvent = self.defaultEvent; 101 | var qualitiesRequired = self.qualitiesRequired; 102 | var events = []; 103 | if(successEvent && qualitiesRequired && qualitiesRequired.size()) { 104 | events.push(successEvent); 105 | } 106 | if(defaultEvent) { 107 | events.push(defaultEvent); 108 | } 109 | if(events.length) { 110 | var wrapperClump = new Clump(events, api.types.Event); 111 | var child_events = wrapperClump.toDom(size, true); 112 | 113 | child_events.classList.add("child-list"); 114 | element.appendChild(child_events); 115 | } 116 | } 117 | }); 118 | } 119 | 120 | return element; 121 | }; 122 | 123 | module.exports = CombatAttack; -------------------------------------------------------------------------------- /src/scripts/objects/event.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Event(raw, parent) { 7 | this.straightCopy = [ 8 | 'Name', 9 | 'Description', 10 | 'Teaser', 11 | 'Image', 12 | 'Category' 13 | ]; 14 | Lump.apply(this, arguments); 15 | 16 | this.tag = null; 17 | 18 | this.ExoticEffects = this.getExoticEffect(this.attribs.ExoticEffects); 19 | 20 | this.qualitiesRequired = null; 21 | this.qualitiesAffected = null; 22 | this.interactions = null; 23 | this.linkToEvent = null; 24 | 25 | this.limitedToArea = null; 26 | 27 | this.setting = null; 28 | 29 | //Deck 30 | //Stickiness 31 | //Transient 32 | //Urgency 33 | } 34 | Object.keys(Lump.prototype).forEach(function(member) { Event.prototype[member] = Lump.prototype[member]; }); 35 | 36 | Event.prototype.wireUp = function(theApi) { 37 | 38 | api = theApi; 39 | 40 | this.qualitiesRequired = new Clump(this.attribs.QualitiesRequired || [], api.types.QualityRequirement, this); 41 | this.qualitiesAffected = new Clump(this.attribs.QualitiesAffected || [], api.types.QualityEffect, this); 42 | this.interactions = new Clump(this.attribs.ChildBranches|| [], api.types.Interaction, this); 43 | 44 | this.linkToEvent = api.getOrCreate(api.types.Event, this.attribs.LinkToEvent, this); 45 | 46 | this.limitedToArea = api.getOrCreate(api.types.Area, this.attribs.LimitedToArea, this); 47 | 48 | this.setting = api.getOrCreate(api.types.Setting, this.attribs.Setting, this); 49 | 50 | Lump.prototype.wireUp.call(this, api); 51 | }; 52 | 53 | Event.prototype.toString = function(long) { 54 | return this.constructor.name + " " + (long ? " [" + this.Category + "] " : "") + this.Name + " (#" + this.Id + ")"; 55 | }; 56 | 57 | Event.prototype.toDom = function(size, includeChildren) { 58 | 59 | size = size || "normal"; 60 | includeChildren = includeChildren === false ? false : true; 61 | 62 | var html = ""; 63 | 64 | var element = document.createElement("li"); 65 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 66 | 67 | if(this.Image !== null && this.Image !== "") { 68 | html = ""; 69 | } 70 | 71 | html += "\n

"+this.Name+"\n"+(this.tag ? ""+this.tag+"" : "")+"

"; 72 | 73 | if(size != "small" && (this.qualitiesRequired || this.qualitiesAffected)) { 74 | html += ""; 84 | } 85 | 86 | html += "

"+this.Description+"

"; 87 | 88 | element.innerHTML = html; 89 | 90 | element.title = this.toString(true); 91 | 92 | if(includeChildren) { 93 | var self = this; 94 | element.addEventListener("click", function(e) { 95 | e.stopPropagation(); 96 | 97 | var childList = element.querySelector(".child-list"); 98 | if(childList) { 99 | element.removeChild(childList); 100 | } 101 | else { 102 | var interactions = self.interactions; 103 | var linkToEvent = self.linkToEvent; 104 | if(linkToEvent) { 105 | var wrapperClump = new Clump([linkToEvent], api.types.Event); 106 | var linkToEvent_element = wrapperClump.toDom("normal", true); 107 | 108 | linkToEvent_element.classList.add("child-list"); 109 | element.appendChild(linkToEvent_element); 110 | } 111 | else if(interactions && interactions.size() > 0) { 112 | var interactions_element = interactions.toDom("normal", true); 113 | 114 | interactions_element.classList.add("child-list"); 115 | element.appendChild(interactions_element); 116 | } 117 | } 118 | }); 119 | } 120 | 121 | return element; 122 | }; 123 | 124 | module.exports = Event; -------------------------------------------------------------------------------- /src/scripts/objects/exchange.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Exchange(raw, parent) { 7 | this.straightCopy = [ 8 | 'Id', 9 | 'Name', 10 | 'Description', 11 | 'Image', 12 | 'SettingIds' 13 | ]; 14 | Lump.apply(this, arguments); 15 | 16 | this.shops = null; 17 | this.settings = null; 18 | } 19 | Object.keys(Lump.prototype).forEach(function(member) { Exchange.prototype[member] = Lump.prototype[member]; }); 20 | 21 | Exchange.prototype.wireUp = function(theApi) { 22 | 23 | api = theApi; 24 | 25 | var self = this; 26 | 27 | this.shops = new Clump(this.attribs.Shops || [], api.types.Shop, this); 28 | 29 | this.settings = api.library.Setting.query("Id", function(id) { 30 | return self.SettingIds.indexOf(id) !== -1; 31 | }); 32 | this.settings.forEach(function (s) { 33 | self.parents.push(s); 34 | }); 35 | 36 | this.ports = api.library.Port.query("SettingId", function(id) { 37 | return self.SettingIds.indexOf(id) !== -1; 38 | }); 39 | this.ports.forEach(function (p) { 40 | self.parents.push(p); 41 | }); 42 | 43 | Lump.prototype.wireUp.call(this); 44 | }; 45 | 46 | Exchange.prototype.toString = function() { 47 | return this.constructor.name + " " + this.Name + " (#" + this.Id + ")"; 48 | }; 49 | 50 | Exchange.prototype.toDom = function(size, includeChildren, tag) { 51 | 52 | size = size || "normal"; 53 | includeChildren = includeChildren === false ? false : true; 54 | tag = tag || "li"; 55 | 56 | var self = this; 57 | var html = ""; 58 | 59 | var element = document.createElement(tag); 60 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 61 | 62 | html = "\n"; 63 | html += "\n

"+this.Name+"

"; 64 | html += "\n

"+this.Description+"

"; 65 | 66 | element.innerHTML = html; 67 | 68 | element.title = this.toString(); 69 | 70 | if(includeChildren) { 71 | element.addEventListener("click", function(e) { 72 | e.stopPropagation(); 73 | 74 | var childList = element.querySelector(".child-list"); 75 | if(childList) { 76 | element.removeChild(childList); 77 | } 78 | else { 79 | if(self.shops) { 80 | 81 | var child_elements = self.shops.toDom("normal", true); 82 | 83 | child_elements.classList.add("child-list"); 84 | element.appendChild(child_elements); 85 | } 86 | } 87 | }); 88 | } 89 | 90 | return element; 91 | }; 92 | 93 | module.exports = Exchange; -------------------------------------------------------------------------------- /src/scripts/objects/interaction.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Interaction(raw, parent) { 7 | this.straightCopy = [ 8 | 'Name', 9 | 'Description', 10 | 'ButtonText', 11 | 'Image', 12 | 13 | 'Ordering' 14 | ]; 15 | Lump.apply(this, arguments); 16 | 17 | this.qualitiesRequired = null; 18 | this.successEvent = null; 19 | this.defaultEvent = null; 20 | 21 | } 22 | Object.keys(Lump.prototype).forEach(function(member) { Interaction.prototype[member] = Lump.prototype[member]; }); 23 | 24 | Interaction.prototype.wireUp = function(theApi) { 25 | 26 | api = theApi; 27 | 28 | this.qualitiesRequired = new Clump(this.attribs.QualitiesRequired || [], api.types.QualityRequirement, this); 29 | this.successEvent = api.getOrCreate(api.types.Event, this.attribs.SuccessEvent, this); 30 | if(this.successEvent) { 31 | this.successEvent.tag = "success"; 32 | } 33 | this.defaultEvent = api.getOrCreate(api.types.Event, this.attribs.DefaultEvent, this); 34 | var qualitiesRequired = this.qualitiesRequired; 35 | if(this.defaultEvent && this.successEvent && qualitiesRequired && qualitiesRequired.size()) { 36 | this.defaultEvent.tag = "failure"; 37 | } 38 | 39 | Lump.prototype.wireUp.call(this, api); 40 | }; 41 | 42 | Interaction.prototype.toString = function() { 43 | return this.constructor.name + " [" + this.Ordering + "] " + this.Name + " (#" + this.Id + ")"; 44 | }; 45 | 46 | Interaction.prototype.toDom = function(size, includeChildren) { 47 | 48 | size = size || "normal"; 49 | includeChildren = includeChildren === false ? false : true; 50 | 51 | var html = ""; 52 | 53 | var element = document.createElement("li"); 54 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 55 | 56 | if(this.Image !== null && this.Image !== "") { 57 | html = ""; 58 | } 59 | 60 | html += "\n

"+this.Name+"

"; 61 | 62 | if(size != "small" && this.qualitiesRequired) { 63 | html += ""; 67 | } 68 | 69 | html += "

"+this.Description+"

"; 70 | 71 | element.innerHTML = html; 72 | 73 | element.title = this.toString(); 74 | 75 | if(includeChildren) { 76 | var self = this; 77 | element.addEventListener("click", function(e) { 78 | e.stopPropagation(); 79 | 80 | var childList = element.querySelector(".child-list"); 81 | if(childList) { 82 | element.removeChild(childList); 83 | } 84 | else { 85 | var successEvent = self.successEvent; 86 | var defaultEvent = self.defaultEvent; 87 | var qualitiesRequired = self.qualitiesRequired; 88 | var events = []; 89 | if(successEvent && qualitiesRequired && qualitiesRequired.size()) { 90 | events.push(successEvent); 91 | } 92 | if(defaultEvent) { 93 | events.push(defaultEvent); 94 | } 95 | if(events.length) { 96 | var wrapperClump = new Clump(events, api.types.Event); 97 | var child_events = wrapperClump.toDom("normal", true); 98 | 99 | child_events.classList.add("child-list"); 100 | element.appendChild(child_events); 101 | } 102 | } 103 | }); 104 | } 105 | 106 | return element; 107 | }; 108 | 109 | module.exports = Interaction; -------------------------------------------------------------------------------- /src/scripts/objects/lump.js: -------------------------------------------------------------------------------- 1 | var library = require('../library'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Lump(raw, parent) { 7 | if(parent) { 8 | this.parents = parent instanceof Array ? parent : [parent]; 9 | } 10 | else { 11 | this.parents = []; 12 | } 13 | 14 | if(!this.straightCopy) { 15 | this.straightCopy = []; 16 | } 17 | this.straightCopy.unshift('Id'); 18 | 19 | this.attribs = raw; 20 | 21 | var self = this; 22 | this.straightCopy.forEach(function(attrib) { 23 | self[attrib] = raw[attrib]; 24 | if(typeof self[attrib] === "undefined") { 25 | self[attrib] = null; 26 | } 27 | }); 28 | delete(this.straightCopy); 29 | 30 | this.wired = false; 31 | 32 | if(!library[this.constructor.name]) { 33 | library[this.constructor.name] = new Clump([], this); 34 | } 35 | 36 | if(library[this.constructor.name].items[this.Id]) { // Something with this ID already exists! 37 | 38 | var existingObject = library[this.constructor.name].items[this.Id]; 39 | 40 | if(!isFunctionallySame(this.attribs, existingObject.attribs)) { // Was it a functionally-identical redefinition of this object? 41 | console.warn("Duplicate ID", this.constructor.name+" "+this.Id+" already exists in the library - replacing", existingObject, "with redefinition", this); 42 | } 43 | } 44 | library[this.constructor.name].items[this.Id] = this; 45 | } 46 | 47 | var isFunctionallySame = function(obj1, obj2) { 48 | 49 | if(obj1 === obj2) { 50 | return true; 51 | } 52 | 53 | if(obj1 instanceof Object) { 54 | if(!(obj2 instanceof Object) || obj1.constructor !== obj2.constructor) { 55 | return false; 56 | } 57 | 58 | var allKeys = Object.keys(obj1).concat(Object.keys(obj2)).filter(function (value, index, self) { 59 | return self.indexOf(value) === index; 60 | }); 61 | 62 | return allKeys.map(function(key) { 63 | return isFunctionallySame(obj1[key], obj2[key]); 64 | }).reduce(function(previousValue, currentValue) { 65 | return previousValue && currentValue; 66 | }, true); 67 | } 68 | 69 | return false; 70 | 71 | }; 72 | 73 | Lump.prototype = { 74 | wireUp: function(theApi) { 75 | api = theApi; 76 | this.wired = true; 77 | }, 78 | 79 | getStates: function(encoded) { 80 | if(typeof encoded === "string" && encoded !== "") { 81 | var map = {}; 82 | encoded.split("~").forEach(function(state) { 83 | var pair = state.split("|"); 84 | map[pair[0]] = pair[1]; 85 | }); 86 | return map; 87 | } 88 | else { 89 | return null; 90 | } 91 | }, 92 | 93 | getExoticEffect: function(encoded) { 94 | if(typeof encoded === "string") { 95 | var effect={}, fields=["operation", "first", "second"]; 96 | encoded.split(",").forEach(function(val, index) { 97 | effect[fields[index]] = val; 98 | }); 99 | return effect; 100 | } 101 | else { 102 | return null; 103 | } 104 | }, 105 | 106 | evalAdvancedExpression: function(expr) { 107 | expr = expr.replace(/\[d:(\d+)\]/gi, "Math.floor((Math.random()*$1)+1)"); // Replace [d:x] with JS to calculate random number on a Dx die 108 | /*jshint -W061 */ 109 | return eval(expr); 110 | /*jshint +W061 */ 111 | }, 112 | 113 | isA: function(type) { 114 | return this instanceof type; 115 | }, 116 | 117 | isOneOf: function(types) { 118 | var self = this; 119 | return types.map(function(type) { 120 | return self.isA(type); 121 | }).reduce(function(previousValue, currentValue, index, array){ 122 | return previousValue || currentValue; 123 | }, false); 124 | }, 125 | 126 | toString: function() { 127 | return this.constructor.name + " (#" + this.Id + ")"; 128 | }, 129 | 130 | isFunctionallySame: isFunctionallySame // Convenience utility function 131 | }; 132 | 133 | module.exports = Lump; -------------------------------------------------------------------------------- /src/scripts/objects/port.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function Port(raw, parent) { 6 | this.straightCopy = [ 7 | 'Name', 8 | 'Rotation', 9 | 'Position', 10 | 'DiscoveryValue', 11 | 'IsStartingPort' 12 | ]; 13 | 14 | 15 | raw.Id = parent.Name+"/"+raw.Name; 16 | Lump.apply(this, arguments); 17 | 18 | this.SettingId = raw.Setting.Id; 19 | this.setting = null; 20 | 21 | this.area = null; 22 | 23 | this.exchanges = null; 24 | } 25 | Object.keys(Lump.prototype).forEach(function(member) { Port.prototype[member] = Lump.prototype[member]; }); 26 | 27 | Port.prototype.wireUp = function(theApi) { 28 | 29 | api = theApi; 30 | var self = this; 31 | 32 | this.setting = api.getOrCreate(api.types.Setting, this.attribs.Setting, this); 33 | 34 | this.area = api.getOrCreate(api.types.Area, this.attribs.Area, this); 35 | 36 | this.exchanges = api.library.Exchange.query("SettingIds", function(ids) { return ids.indexOf(self.SettingId) !== -1; }); 37 | 38 | Lump.prototype.wireUp.call(this, api); 39 | }; 40 | 41 | Port.prototype.toString = function(long) { 42 | return this.constructor.name + " " + this.Name + " (#" + this.Name + ")"; 43 | }; 44 | 45 | Port.prototype.toDom = function(size, tag) { 46 | 47 | size = size || "normal"; 48 | tag = tag || "li"; 49 | 50 | var html = ""; 51 | 52 | var element = document.createElement(tag); 53 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 54 | 55 | html = "\n

"+this.Name+"

"; 56 | 57 | element.innerHTML = html; 58 | 59 | element.title = this.toString(); 60 | 61 | return element; 62 | }; 63 | 64 | module.exports = Port; -------------------------------------------------------------------------------- /src/scripts/objects/quality-effect.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function QualityEffect(raw, parent) { 6 | this.straightCopy = ["Level", "SetToExactly"]; 7 | Lump.apply(this, arguments); 8 | 9 | // May involve Quality object references, so can't resolve until after all objects are wired up 10 | this.setToExactlyAdvanced = null; 11 | this.changeByAdvanced = null; 12 | 13 | this.associatedQuality = null; 14 | 15 | } 16 | Object.keys(Lump.prototype).forEach(function(member) { QualityEffect.prototype[member] = Lump.prototype[member]; }); 17 | 18 | QualityEffect.prototype.wireUp = function(theApi) { 19 | 20 | api = theApi; 21 | 22 | this.associatedQuality = api.get(api.types.Quality, this.attribs.AssociatedQualityId, this); 23 | this.setToExactlyAdvanced = api.describeAdvancedExpression(this.attribs.SetToExactlyAdvanced); 24 | this.changeByAdvanced = api.describeAdvancedExpression(this.attribs.ChangeByAdvanced); 25 | 26 | Lump.prototype.wireUp.call(this, api); 27 | }; 28 | 29 | QualityEffect.prototype.getQuantity = function() { 30 | var condition = ""; 31 | 32 | if(this.setToExactlyAdvanced !== null) { 33 | condition = "+(" + this.setToExactlyAdvanced + ")"; 34 | } 35 | else if(this.SetToExactly !== null) { 36 | condition = "= " + this.SetToExactly; 37 | } 38 | else if(this.changeByAdvanced !== null) { 39 | condition = "+(" + this.changeByAdvanced + ")"; 40 | } 41 | else if(this.Level !== null) { 42 | if(this.Level < 0) { 43 | condition = this.Level; 44 | } 45 | else if(this.Level > 0) { 46 | condition = "+" + this.Level; 47 | } 48 | } 49 | 50 | return condition; 51 | }; 52 | 53 | QualityEffect.prototype.isAdditive = function() { 54 | return this.setToExactlyAdvanced || this.SetToExactly || this.changeByAdvanced || (this.Level > 0); 55 | }; 56 | 57 | QualityEffect.prototype.isSubtractive = function() { 58 | return !this.setToExactlyAdvanced && !this.SetToExactly && !this.changeByAdvanced && (this.Level <= 0); 59 | }; 60 | 61 | QualityEffect.prototype.toString = function() { 62 | var quality = this.associatedQuality; 63 | return this.constructor.name + " ("+this.Id+") on " + quality + this.getQuantity(); 64 | }; 65 | 66 | QualityEffect.prototype.toDom = function(size) { 67 | 68 | size = size || "small"; 69 | 70 | var element = document.createElement("li"); 71 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 72 | 73 | var quality_element = this.associatedQuality; 74 | 75 | if(!quality_element) { 76 | quality_element = document.createElement("span"); 77 | quality_element.innerHTML = "[INVALID]"; 78 | } 79 | else { 80 | quality_element = this.associatedQuality.toDom(size, false, "span"); 81 | } 82 | 83 | var quantity_element = document.createElement("span"); 84 | quantity_element.className = "item quantity"; 85 | quantity_element.innerHTML = this.getQuantity(); 86 | quantity_element.title = this.toString(); 87 | 88 | element.appendChild(quality_element); 89 | element.appendChild(quantity_element); 90 | 91 | return element; 92 | }; 93 | 94 | module.exports = QualityEffect; -------------------------------------------------------------------------------- /src/scripts/objects/quality-requirement.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function QualityRequirement(raw, parent) { 6 | this.straightCopy = ['MinLevel', 'MaxLevel']; 7 | Lump.apply(this, arguments); 8 | 9 | this.difficultyAdvanced = null; 10 | this.minAdvanced = null; 11 | this.maxAdvanced = null; 12 | 13 | this.associatedQuality = null; 14 | this.chanceQuality = null; 15 | } 16 | Object.keys(Lump.prototype).forEach(function(member) { QualityRequirement.prototype[member] = Lump.prototype[member]; }); 17 | 18 | QualityRequirement.prototype.wireUp = function(theApi) { 19 | 20 | api = theApi; 21 | 22 | this.difficultyAdvanced = api.describeAdvancedExpression(this.attribs.DifficultyAdvanced); 23 | this.minAdvanced = api.describeAdvancedExpression(this.attribs.MinAdvanced); 24 | this.maxAdvanced = api.describeAdvancedExpression(this.attribs.MaxAdvanced); 25 | 26 | this.associatedQuality = api.get(api.types.Quality, this.attribs.AssociatedQualityId, this); 27 | 28 | this.chanceQuality = this.getChanceCap(); 29 | 30 | Lump.prototype.wireUp.call(this, api); 31 | }; 32 | 33 | QualityRequirement.prototype.getChanceCap = function() { 34 | var quality = null; 35 | if(!this.attribs.DifficultyLevel) { 36 | return null; 37 | } 38 | quality = this.associatedQuality; 39 | if(!quality) { 40 | return null; 41 | } 42 | 43 | return Math.round(this.attribs.DifficultyLevel * ((100 + quality.DifficultyScaler + 7)/100)); 44 | }; 45 | 46 | QualityRequirement.prototype.getQuantity = function() { 47 | var condition = ""; 48 | 49 | if(this.difficultyAdvanced !== null) { 50 | condition = this.difficultyAdvanced; 51 | } 52 | else if(this.minAdvanced !== null) { 53 | condition = this.minAdvanced; 54 | } 55 | else if(this.maxAdvanced !== null) { 56 | condition = this.maxAdvanced; 57 | } 58 | else if(this.chanceQuality !== null) { 59 | condition = this.chanceQuality + " for 100%"; 60 | } 61 | else if(this.MaxLevel !== null && this.MinLevel !== null) { 62 | if(this.MaxLevel === this.MinLevel) { 63 | condition = "= " + this.MinLevel; 64 | } 65 | else { 66 | condition = this.MinLevel + "-" + this.MaxLevel; 67 | } 68 | } 69 | else { 70 | if(this.MinLevel !== null) { 71 | condition = "≥ " + this.MinLevel; 72 | } 73 | if(this.MaxLevel !== null) { 74 | condition = "≤ " + this.MaxLevel; 75 | } 76 | } 77 | return condition; 78 | }; 79 | 80 | QualityRequirement.prototype.toString = function() { 81 | var quality = this.associatedQuality; 82 | return this.constructor.name + " ("+this.Id+") on " + quality + " " + this.getQuantity(); 83 | }; 84 | 85 | QualityRequirement.prototype.toDom = function(size) { 86 | 87 | size = size || "small"; 88 | 89 | var element = document.createElement("li"); 90 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 91 | 92 | var quality_element = this.associatedQuality; 93 | 94 | if(!quality_element) { 95 | quality_element = document.createElement("span"); 96 | quality_element.innerHTML = "[INVALID]"; 97 | } 98 | else { 99 | quality_element = this.associatedQuality.toDom(size, false, "span"); 100 | } 101 | 102 | var quantity_element = document.createElement("span"); 103 | quantity_element.className = "item quantity"; 104 | quantity_element.innerHTML = this.getQuantity(); 105 | quantity_element.title = this.toString(); 106 | 107 | element.appendChild(quality_element); 108 | element.appendChild(quantity_element); 109 | 110 | return element; 111 | }; 112 | 113 | module.exports = QualityRequirement; -------------------------------------------------------------------------------- /src/scripts/objects/quality.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Quality(raw, parent) { 7 | this.straightCopy = [ 8 | 'Name', 9 | 'Description', 10 | 'Image', 11 | 12 | 'Category', 13 | 'Nature', 14 | 'Tag', 15 | 16 | "IsSlot", 17 | 18 | 'AllowedOn', 19 | "AvailableAt", 20 | 21 | 'Cap', 22 | 'DifficultyScaler', 23 | 'Enhancements' 24 | ]; 25 | Lump.apply(this, arguments); 26 | 27 | this.States = this.getStates(raw.ChangeDescriptionText); 28 | this.LevelDescriptionText = this.getStates(raw.LevelDescriptionText); 29 | this.LevelImageText = this.getStates(raw.LevelImageText); 30 | 31 | this.useEvent = null; 32 | } 33 | Object.keys(Lump.prototype).forEach(function(member) { Quality.prototype[member] = Lump.prototype[member]; }); 34 | 35 | Quality.prototype.wireUp = function(theApi) { 36 | 37 | api = theApi; 38 | 39 | this.useEvent = api.getOrCreate(api.types.Event, this.attribs.UseEvent, this); 40 | if(this.useEvent) { 41 | this.useEvent.tag = "use"; 42 | } 43 | 44 | Lump.prototype.wireUp.call(this, api); 45 | }; 46 | 47 | Quality.prototype.toString = function(long) { 48 | return this.constructor.name + " " + (long ? " [" + this.Nature + " > " + this.Category + " > " + this.Tag + "] " : "") + this.Name + " (#" + this.Id + ")"; 49 | }; 50 | 51 | Quality.prototype.toDom = function(size, includeChildren, tag) { 52 | 53 | size = size || "normal"; 54 | includeChildren = includeChildren === false ? false : true; 55 | tag = tag || "li"; 56 | 57 | var html = ""; 58 | 59 | var element = document.createElement(tag); 60 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 61 | 62 | html = "\n"; 63 | html += "\n

"+this.Name+"

"; 64 | html += "\n

"+this.Description+"

"; 65 | 66 | element.innerHTML = html; 67 | 68 | element.title = this.toString(); 69 | 70 | if(includeChildren) { 71 | var self = this; 72 | element.addEventListener("click", function(e) { 73 | e.stopPropagation(); 74 | 75 | var childList = element.querySelector(".child-list"); 76 | if(childList) { 77 | element.removeChild(childList); 78 | } 79 | else { 80 | if(self.useEvent) { 81 | 82 | var wrapperClump = new Clump([self.useEvent], api.types.Event); 83 | var child_events = wrapperClump.toDom(size, true); 84 | 85 | child_events.classList.add("child-list"); 86 | element.appendChild(child_events); 87 | } 88 | } 89 | }); 90 | } 91 | 92 | return element; 93 | }; 94 | 95 | module.exports = Quality; -------------------------------------------------------------------------------- /src/scripts/objects/setting.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | 3 | var api; 4 | 5 | function Setting(raw, parent) { 6 | this.straightCopy = [ 7 | 'Id' 8 | ]; 9 | Lump.apply(this, arguments); 10 | 11 | this.shops = null; 12 | } 13 | Object.keys(Lump.prototype).forEach(function(member) { Setting.prototype[member] = Lump.prototype[member]; }); 14 | 15 | Setting.prototype.wireUp = function(theApi) { 16 | 17 | api = theApi; 18 | 19 | Lump.prototype.wireUp.call(this); 20 | }; 21 | 22 | Setting.prototype.toString = function() { 23 | return this.constructor.name + " #" + this.Id; 24 | }; 25 | 26 | Setting.prototype.toDom = function(size, includeChildren, tag) { 27 | 28 | size = size || "normal"; 29 | includeChildren = includeChildren === false ? false : true; 30 | tag = tag || "li"; 31 | 32 | var self = this; 33 | var html = ""; 34 | 35 | var element = document.createElement(tag); 36 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 37 | 38 | html = "\n

"+this.Id+"

"; 39 | 40 | element.innerHTML = html; 41 | 42 | element.title = this.toString(); 43 | 44 | return element; 45 | }; 46 | 47 | module.exports = Setting; -------------------------------------------------------------------------------- /src/scripts/objects/shop.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function Shop(raw, parent) { 7 | this.straightCopy = [ 8 | 'Id', 9 | 'Name', 10 | 'Description', 11 | 'Image', 12 | 'Ordering' 13 | ]; 14 | Lump.apply(this, arguments); 15 | 16 | this.availabilities = null; 17 | this.unlockCost = null; 18 | } 19 | Object.keys(Lump.prototype).forEach(function(member) { Shop.prototype[member] = Lump.prototype[member]; }); 20 | 21 | Shop.prototype.wireUp = function(theApi) { 22 | 23 | api = theApi; 24 | 25 | this.availabilities = new Clump(this.attribs.Availabilities || [], api.types.Availability, this); 26 | 27 | Lump.prototype.wireUp.call(this); 28 | }; 29 | 30 | Shop.prototype.toString = function() { 31 | return this.constructor.name + " " + this.Name + " (#" + this.Id + ")"; 32 | }; 33 | 34 | Shop.prototype.toDom = function(size, includeChildren, tag) { 35 | 36 | size = size || "normal"; 37 | includeChildren = includeChildren === false ? false : true; 38 | tag = tag || "li"; 39 | 40 | var self = this; 41 | var html = ""; 42 | 43 | var element = document.createElement(tag); 44 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 45 | 46 | html = "\n"; 47 | html += "\n

"+this.Name+"

"; 48 | html += "\n

"+this.Description+"

"; 49 | 50 | element.innerHTML = html; 51 | 52 | element.title = this.toString(); 53 | 54 | if(includeChildren) { 55 | element.addEventListener("click", function(e) { 56 | e.stopPropagation(); 57 | 58 | var childList = element.querySelector(".child-list"); 59 | if(childList) { 60 | element.removeChild(childList); 61 | } 62 | else { 63 | if(self.availabilities) { 64 | 65 | var child_elements = self.availabilities.toDom("normal", true); 66 | 67 | child_elements.classList.add("child-list"); 68 | element.appendChild(child_elements); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | return element; 75 | }; 76 | 77 | module.exports = Shop; -------------------------------------------------------------------------------- /src/scripts/objects/spawned-entity.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | 4 | var api; 5 | 6 | function SpawnedEntity(raw, parent) { 7 | this.straightCopy = [ 8 | 'Name', 9 | 'HumanName', 10 | 11 | 'Neutral', 12 | 'PrefabName', 13 | 'DormantBehaviour', 14 | 'AwareBehaviour', 15 | 16 | 'Hull', 17 | 'Crew', 18 | 'Life', 19 | 'MovementSpeed', 20 | 'RotationSpeed', 21 | 'BeastieCharacteristicsName', 22 | 'CombatItems', 23 | 'LootPrefabName', 24 | 'GleamValue' 25 | ]; 26 | raw.Id = raw.Name; 27 | Lump.apply(this, arguments); 28 | 29 | this.pacifyEvent = null; 30 | this.killQualityEvent = null; 31 | this.combatAttackNames = []; 32 | 33 | this.image = null; 34 | } 35 | Object.keys(Lump.prototype).forEach(function(member) { SpawnedEntity.prototype[member] = Lump.prototype[member]; }); 36 | 37 | SpawnedEntity.prototype.wireUp = function(theApi) { 38 | 39 | api = theApi; 40 | 41 | var self = this; 42 | 43 | this.combatAttackNames = (this.attribs.CombatAttackNames || []).map(function(name) { 44 | return api.get(api.types.CombatAttack, name, self); 45 | }).filter(function(attack) { 46 | return typeof attack === "object"; 47 | }); 48 | 49 | this.pacifyEvent = api.get(api.types.Event, this.attribs.PacifyEventId, this); 50 | if(this.pacifyEvent) { 51 | this.pacifyEvent.tag = "pacified"; 52 | } 53 | 54 | this.killQualityEvent = api.get(api.types.Event, this.attribs.KillQualityEventId, this); 55 | if(this.killQualityEvent) { 56 | this.killQualityEvent.tag = "killed"; 57 | } 58 | 59 | this.image = ((this.killQualityEvent && this.killQualityEvent.Image) || (this.pacifyEvent && this.pacifyEvent.Image)); 60 | 61 | Lump.prototype.wireUp.call(this, api); 62 | }; 63 | 64 | SpawnedEntity.prototype.toString = function() { 65 | return this.constructor.name + " " + this.HumanName + " (#" + this.Id + ")"; 66 | }; 67 | 68 | SpawnedEntity.prototype.toDom = function(size, includeChildren) { 69 | 70 | size = size || "normal"; 71 | includeChildren = includeChildren === false ? false : true; 72 | 73 | var self = this; 74 | 75 | var html = ""; 76 | 77 | var element = document.createElement("li"); 78 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 79 | 80 | if(this.Image !== null && this.Image !== "") { 81 | html = ""; 82 | } 83 | 84 | html += "\n

"+this.HumanName+"

"; 85 | 86 | if(size !== "small") { 87 | if(this.qualitiesRequired) { 88 | html += ""; 91 | } 92 | 93 | html += "
"; 94 | 95 | ['Hull', 'Crew', 'Life', 'MovementSpeed', 'RotationSpeed'].forEach(function(key) { 96 | html += "
"+key+"
"+self[key]+"
"; 97 | }); 98 | html += "
"; 99 | } 100 | 101 | element.innerHTML = html; 102 | 103 | element.title = this.toString(); 104 | 105 | if(includeChildren) { 106 | element.addEventListener("click", function(e) { 107 | e.stopPropagation(); 108 | 109 | var childList = element.querySelector(".child-list"); 110 | if(childList) { 111 | element.removeChild(childList); 112 | } 113 | else { 114 | var successEvent = self.successEvent; 115 | var defaultEvent = self.defaultEvent; 116 | var qualitiesRequired = self.qualitiesRequired; 117 | var events = []; 118 | if(successEvent && qualitiesRequired && qualitiesRequired.size()) { 119 | events.push(successEvent); 120 | } 121 | if(defaultEvent) { 122 | events.push(defaultEvent); 123 | } 124 | if(events.length) { 125 | var wrapperClump = new Clump(events, api.types.Event); 126 | var child_events = wrapperClump.toDom(size, true); 127 | 128 | child_events.classList.add("child-list"); 129 | element.appendChild(child_events); 130 | } 131 | } 132 | }); 133 | } 134 | 135 | return element; 136 | }; 137 | 138 | module.exports = SpawnedEntity; -------------------------------------------------------------------------------- /src/scripts/objects/tile-variant.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | var Port = require('./port'); 4 | var Area = require('./area'); 5 | 6 | var api; 7 | 8 | function TileVariant(raw, parent) { 9 | this.straightCopy = [ 10 | 'Name', 11 | 'HumanName', 12 | 'Description', 13 | 14 | 'MaxTilePopulation', 15 | 'MinTilePopulation', 16 | 17 | 'SeaColour', 18 | 'MusicTrackName', 19 | 'ChanceOfWeather', 20 | 'FogRevealThreshold' 21 | ]; 22 | 23 | /* 24 | LabelData: Array[6] 25 | PhenomenaData: Array[1] 26 | SpawnPoints: Array[2] 27 | TerrainData: Array[14] 28 | Weather: Array[1] 29 | */ 30 | 31 | raw.Id = parent.Name+"/"+raw.Name; 32 | Lump.apply(this, arguments); 33 | 34 | this.SettingId = raw.Setting.Id; 35 | this.setting = null; 36 | 37 | this.ports = new Clump(this.attribs.PortData || [], Port, this); 38 | 39 | this.areas = null; 40 | } 41 | Object.keys(Lump.prototype).forEach(function(member) { TileVariant.prototype[member] = Lump.prototype[member]; }); 42 | 43 | TileVariant.prototype.wireUp = function(theApi) { 44 | 45 | api = theApi; 46 | 47 | this.setting = api.getOrCreate(api.types.Setting, this.attribs.Setting, this); 48 | 49 | this.ports.forEach(function(p) { p.wireUp(api); }); 50 | 51 | // Also create a list of all the areas of each of the ports in this object for convenience 52 | this.areas = new Clump(this.ports.map(function(p) { return p.area; }), api.types.Area, this); 53 | 54 | Lump.prototype.wireUp.call(this); 55 | }; 56 | 57 | TileVariant.prototype.toString = function(long) { 58 | return this.constructor.name + " " + this.HumanName + " (#" + this.Name + ")"; 59 | }; 60 | 61 | TileVariant.prototype.toDom = function(size, tag) { 62 | 63 | size = size || "normal"; 64 | tag = tag || "li"; 65 | 66 | var html = ""; 67 | 68 | var element = document.createElement(tag); 69 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 70 | 71 | html = "\n

"+this.HumanName+"

"; 72 | 73 | element.innerHTML = html; 74 | 75 | element.title = this.toString(); 76 | 77 | return element; 78 | }; 79 | 80 | module.exports = TileVariant; -------------------------------------------------------------------------------- /src/scripts/objects/tile.js: -------------------------------------------------------------------------------- 1 | var Lump = require('./lump'); 2 | var Clump = require('./clump'); 3 | var TileVariant = require('./tile-variant'); 4 | var Port = require('./port'); 5 | var Area = require('./area'); 6 | 7 | var api; 8 | 9 | function Tile(raw, parent) { 10 | this.straightCopy = [ 11 | 'Name' 12 | ]; 13 | raw.Id = raw.Name; 14 | Lump.apply(this, arguments); 15 | 16 | this.tileVariants = new Clump(this.attribs.Tiles || [], TileVariant, this); 17 | } 18 | Object.keys(Lump.prototype).forEach(function(member) { Tile.prototype[member] = Lump.prototype[member]; }); 19 | 20 | Tile.prototype.wireUp = function(theApi) { 21 | 22 | api = theApi; 23 | 24 | this.tileVariants.forEach(function(tv) { tv.wireUp(api); }); 25 | 26 | // Also create a list of all the ports and areas of each of the tilevariants in this object for convenience 27 | var all_ports = {}; 28 | var all_areas = {}; 29 | this.tileVariants.forEach(function(tv) { 30 | tv.ports.forEach(function(p) { 31 | all_ports[p.Id] = p; 32 | all_areas[p.area.Id] = p.area; 33 | }); 34 | }); 35 | this.ports = new Clump(Object.keys(all_ports).map(function(p) { return all_ports[p]; }), api.types.Port, this); 36 | this.areas = new Clump(Object.keys(all_areas).map(function(a) { return all_areas[a]; }), api.types.Area, this); 37 | 38 | Lump.prototype.wireUp.call(this); 39 | }; 40 | 41 | Tile.prototype.toString = function(long) { 42 | return this.constructor.name + " " + this.Name + " (#" + this.Name + ")"; 43 | }; 44 | 45 | Tile.prototype.toDom = function(size, tag) { 46 | 47 | size = size || "normal"; 48 | tag = tag || "li"; 49 | 50 | var html = ""; 51 | 52 | var element = document.createElement(tag); 53 | element.className = "item "+this.constructor.name.toLowerCase()+"-item "+size; 54 | 55 | html = "\n

"+this.Name+"

"; 56 | 57 | element.innerHTML = html; 58 | 59 | element.title = this.toString(); 60 | 61 | return element; 62 | }; 63 | 64 | module.exports = Tile; -------------------------------------------------------------------------------- /src/scripts/ui.js: -------------------------------------------------------------------------------- 1 | var api = require('./api'); 2 | var dragndrop = require('./ui/dragndrop'); 3 | var query = require('./ui/query'); 4 | 5 | 6 | $("#tabs .buttons li").on("click", function(e) { 7 | 8 | var type = $(this).attr("data-type"); 9 | 10 | $("#tabs .panes .pane").hide(); // Hide all panes 11 | $("#tabs .buttons li").removeClass("active"); // Deactivate all buttons 12 | 13 | $("#tabs .panes ."+type.toLowerCase()).show(); 14 | $("#tabs .buttons [data-type="+type+"]").addClass("active"); 15 | }); 16 | 17 | // Setup the dnd listeners. 18 | var dropZone = document.getElementById('drop-zone'); 19 | 20 | dropZone.addEventListener('dragenter', dragndrop.handlers.dragOver, false); 21 | dropZone.addEventListener('dragleave', dragndrop.handlers.dragEnd, false); 22 | dropZone.addEventListener('dragover', dragndrop.handlers.dragOver, false); 23 | 24 | dropZone.addEventListener('drop', dragndrop.handlers.dragDrop, false); 25 | 26 | document.getElementById('paths-to-node').addEventListener('click', query.pathsToNodeUI, false); 27 | 28 | // For convenience 29 | window.api = api; 30 | window.api.query = query; -------------------------------------------------------------------------------- /src/scripts/ui/dragndrop.js: -------------------------------------------------------------------------------- 1 | var api = require('../api'); 2 | var Clump = require('../objects/clump'); 3 | var io = require('../io'); 4 | 5 | var render = require('./render'); 6 | 7 | function handleDragOver(evt) { 8 | evt.stopPropagation(); 9 | evt.preventDefault(); 10 | 11 | $("#drop-zone").addClass("drop-target"); 12 | } 13 | 14 | function handleDragEnd(evt) { 15 | evt.stopPropagation(); 16 | evt.preventDefault(); 17 | 18 | $("#drop-zone").removeClass("drop-target"); 19 | } 20 | 21 | function handleDragDrop(evt) { 22 | 23 | $("#drop-zone").removeClass("drop-target"); 24 | 25 | evt.stopPropagation(); 26 | evt.preventDefault(); 27 | 28 | var files = evt.dataTransfer.files; // FileList object. 29 | 30 | // Files is a FileList of File objects. List some properties. 31 | var output = []; 32 | io.resetFilesToLoad(); 33 | for (var i = 0; i < files.length; i++) { 34 | var f = files[i]; 35 | var filename = escape(f.name); 36 | var typeName = io.fileObjectMap[filename]; 37 | var Type = api.types[typeName]; 38 | if(Type) { 39 | io.incrementFilesToLoad(); 40 | api.readFromFile(Type, f, function() { 41 | io.decrementFilesToLoad(); 42 | 43 | if(io.countFilesToLoad() === 0) { 44 | api.wireUpObjects(); 45 | render.lists(); 46 | } 47 | }); 48 | output.push('
  • ', escape(f.name), ' (', f.type || 'n/a', ') - ', 49 | f.size, ' bytes, last modified: ', 50 | f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a', 51 | '
  • '); 52 | } 53 | else { 54 | output.push('
  • ERROR: No handler for file ' , escape(f.name), '
  • '); 55 | } 56 | } 57 | document.getElementById('list').innerHTML = ''; 58 | } 59 | 60 | module.exports = { 61 | handlers: { 62 | dragOver: handleDragOver, 63 | dragEnd: handleDragEnd, 64 | dragDrop: handleDragDrop 65 | } 66 | }; -------------------------------------------------------------------------------- /src/scripts/ui/query.js: -------------------------------------------------------------------------------- 1 | var api = require('../api'); 2 | var Clump = require('../objects/clump'); 3 | 4 | function RouteNode(node) { 5 | this.node = node; 6 | this.children = []; 7 | } 8 | 9 | function pathsToNodeUI() { 10 | 11 | var type = document.getElementById('type'); 12 | type = type.options[type.selectedIndex].value; 13 | 14 | var operation = document.getElementById('operation'); 15 | operation = operation.options[operation.selectedIndex].value; 16 | 17 | var id = prompt("Id of "+type); 18 | 19 | if(!id) { // Cancelled dialogue 20 | return; 21 | } 22 | 23 | var item = api.library[type].id(id); 24 | 25 | if(!item) { 26 | alert("Could not find "+type+" "+id); 27 | return; 28 | } 29 | 30 | var root = document.getElementById("query-tree"); 31 | root.innerHTML = ""; 32 | 33 | var title = $('.pane.query .pane-title').text("Query: "+item.toString()); 34 | 35 | var routes = pathsToNode(item, {}); 36 | 37 | if(routes && routes.children.length) { 38 | 39 | routes = filterPathsToNode(routes, operation); 40 | 41 | var top_children = document.createElement("ul"); 42 | top_children.className += "clump-list small"; 43 | 44 | routes.children.forEach(function(child_route) { 45 | var tree = renderPathsToNode(child_route, []); 46 | top_children.appendChild(tree); 47 | }); 48 | 49 | root.appendChild(top_children); 50 | } 51 | else { 52 | alert("This "+type+" is a root node with no parents that satisfy the conditions"); 53 | } 54 | 55 | } 56 | 57 | function pathsToNode(node, seen, parent) { 58 | 59 | if(seen[node.Id]) { // Don't recurse into nodes we've already seen 60 | return false; 61 | } 62 | 63 | var ancestry = JSON.parse(JSON.stringify(seen)); 64 | ancestry[node.Id] = true; 65 | 66 | var this_node = new RouteNode(/*node.linkToEvent ? node.linkToEvent :*/ node); // If this node is just a link to another one, skip over the useless link 67 | 68 | if(node instanceof api.types.SpawnedEntity) { 69 | return this_node; // Leaf node in tree 70 | } 71 | else if(node instanceof api.types.Event && node.tag === "use") { 72 | return this_node; // Leaf node in tree 73 | } 74 | else if(node instanceof api.types.Event && parent instanceof api.types.Event && (parent.tag === "killed" || parent.tag === "pacified")) { // If this is an event that's reachable by killing a monster, don't recurse any other causes (as they're usually misleading/circular) 75 | return false; 76 | } 77 | else if(node instanceof api.types.Setting) { 78 | return false; 79 | } 80 | else if (node instanceof api.types.Port) { 81 | return new RouteNode(node.area); 82 | } 83 | else if(node.limitedToArea && node.limitedToArea.Id !== 101956) { 84 | var area_name = node.limitedToArea.Name.toLowerCase(); 85 | var event_name = (node.Name && node.Name.toLowerCase()) || ""; 86 | if(area_name.indexOf(event_name) !== -1 || event_name.indexOf(area_name) !== -1) { // If Area has similar name to Event, ignore the event and just substitute the area 87 | return new RouteNode(node.limitedToArea); 88 | } 89 | else { 90 | this_node.children.push(new RouteNode(node.limitedToArea)); // Else include both the Area and the Event 91 | return this_node; 92 | } 93 | 94 | } 95 | else { 96 | for(var i=0; i'; 154 | 155 | var reqsTitle = document.createElement('h5'); 156 | reqsTitle.innerHTML = "Requirements"; 157 | description.appendChild(reqsTitle); 158 | 159 | var total_requirements = getRouteRequirements(new_ancestry); 160 | 161 | description.appendChild(total_requirements.toDom("small", false)); 162 | element.appendChild(description); 163 | } 164 | 165 | return element; 166 | } 167 | 168 | function lower(text) { 169 | return text.slice(0,1).toLowerCase()+text.slice(1); 170 | } 171 | 172 | function describeRoute(ancestry) { 173 | var a = ancestry.slice().reverse(); 174 | 175 | var guide = ""; 176 | if(a[0] instanceof api.types.Area) { 177 | if(a[1] instanceof api.types.Event) { 178 | guide = "Seek "+a[1].Name+" in "+a[0].Name; 179 | if(a[2] instanceof api.types.Interaction) { 180 | guide += " and "; 181 | if("\"'".indexOf(a[2].Name[0]) !== -1) { 182 | guide += "exclaim "; 183 | } 184 | guide += lower(a[2].Name); 185 | } 186 | guide += "."; 187 | } 188 | else { 189 | guide = "Travel to "+a[0].Name; 190 | 191 | if(a[1] instanceof api.types.Interaction) { 192 | guide += " and "+lower(a[1].Name); 193 | } 194 | else if(a[1] instanceof api.types.Exchange && a[2] instanceof api.types.Shop) { 195 | guide += " and look for the "+a[2].Name+" Emporium in "+a[1].Name; 196 | } 197 | 198 | guide += "."; 199 | } 200 | } 201 | else if(a[0] instanceof api.types.SpawnedEntity) { 202 | guide = "Find and best a "+a[0].HumanName; 203 | if(a[2] instanceof api.types.Interaction) { 204 | guide += ", then " + lower(a[2].Name); 205 | } 206 | guide += "."; 207 | } 208 | else if(a[0] instanceof api.types.Event && a[0].tag === "use" && !(a[1] instanceof api.types.QualityRequirement)) { 209 | if(a[0].Name.match(/^\s*Speak/i)) { 210 | guide = a[0].Name; 211 | } 212 | else if(a[0].Name.match(/^\s*A/i)) { 213 | guide = "Acquire "+lower(a[0].Name); 214 | } 215 | else { 216 | guide = "Find a "+lower(a[0].Name); 217 | } 218 | guide += " and " + lower(a[1].Name) + "."; 219 | } 220 | 221 | return guide; 222 | } 223 | 224 | function detailRoute(ancestry) { 225 | var a = ancestry.slice().reverse(); 226 | 227 | var guide = ""; 228 | if(a[0] instanceof api.types.Area) { 229 | if(a[1] instanceof api.types.Event) { 230 | guide = "You must travel to "+a[0].Name+" and look for "+a[1].Name+"."; 231 | if(a[2] instanceof api.types.Interaction) { 232 | guide += " When you find it you should "; 233 | if("\"'".indexOf(a[2].Name[0]) !== -1) { 234 | guide += "say "; 235 | } 236 | guide += lower(a[2].Name); 237 | } 238 | guide += "."; 239 | } 240 | else { 241 | guide = "Make for "+a[0].Name; 242 | 243 | if(a[1] instanceof api.types.Interaction) { 244 | guide += " and "+lower(a[1].Name); 245 | } 246 | else if(a[1] instanceof api.types.Exchange && a[2] instanceof api.types.Shop) { 247 | guide += ". Upon arrival go to "+a[1].Name+", and look for the shop "+a[2].Names; 248 | } 249 | 250 | guide += "."; 251 | } 252 | } 253 | else if(a[0] instanceof api.types.SpawnedEntity) { 254 | guide = "You must hunt the mythical zee-peril known as the "+a[0].HumanName+", engage it in battle and defeat it."; 255 | if(a[2] instanceof api.types.Interaction) { 256 | guide += " Once you have conquered it you must " + lower(a[2].Name) + " to help secure your prize."; 257 | } 258 | } 259 | else if(a[0] instanceof api.types.Event && a[0].tag === "use" && !(a[1] instanceof api.types.QualityRequirement)) { 260 | if(a[0].Name.match(/^\s*Speak/i)) { 261 | guide = "First you must "+lower(a[0].Name); 262 | } 263 | else if(a[0].Name.match(/^\s*A/i)) { 264 | guide = "Source "+lower(a[0].Name); 265 | } 266 | else { 267 | guide = "Try to locate a "+lower(a[0].Name); 268 | } 269 | guide += ", and then " + lower(a[1].Name) + "."; 270 | } 271 | 272 | return guide; 273 | } 274 | 275 | function getRouteRequirements(ancestry) { 276 | 277 | var reqs = {}; 278 | 279 | // Ancestry is ordered from last->first, so iterate backwards from final effect -> initial cause 280 | ancestry.forEach(function(step) { 281 | /* Simplification: if an event modifies a quality then assume that later requirements 282 | on the same quality are probably satisfied by that modification (eg, when qualities 283 | are incremented/decremented to control story-quest progress). */ 284 | if(step.qualitiesAffected) { 285 | step.qualitiesAffected.forEach(function(effect) { 286 | delete(reqs[effect.associatedQuality.Id]); 287 | }); 288 | } 289 | // Now add any requirements for the current stage (earlier requirements overwrite later ones on the same quality) 290 | if(step.qualitiesRequired) { 291 | step.qualitiesRequired.forEach(function(req) { 292 | if(req.associatedQuality) { // Check this is a valid QualityRequirement, and not one of the half-finished debug elements referring to anon-existant Quality 293 | reqs[req.associatedQuality.Id] = req; 294 | } 295 | }); 296 | } 297 | }); 298 | 299 | var result = Object.keys(reqs).map(function(key) { return reqs[key]; }); 300 | 301 | return new Clump(result, api.types.QualityRequirement); 302 | } 303 | 304 | module.exports = { 305 | RouteNode: RouteNode, 306 | pathsToNodeUI: pathsToNodeUI, 307 | pathsToNode: pathsToNode, 308 | filterPathsToNode: filterPathsToNode, 309 | renderPathsToNode: renderPathsToNode, 310 | describeRoute: describeRoute, 311 | detailRoute: detailRoute, 312 | getRouteRequirements: getRouteRequirements 313 | }; -------------------------------------------------------------------------------- /src/scripts/ui/render.js: -------------------------------------------------------------------------------- 1 | var api = require('../api'); 2 | 3 | function renderLists() { 4 | Object.keys(api.loaded).forEach(function(type) { 5 | renderList(api.loaded[type]); // Only display directly loaded (root-level) Lumps, to prevent the list becoming unwieldy 6 | }); 7 | } 8 | 9 | function renderList(clump) { 10 | var root = document.getElementById(clump.type.name.toLowerCase()+"-list"); 11 | if(root) { 12 | root.appendChild(clump.toDom()); 13 | } 14 | } 15 | 16 | module.exports = { 17 | list: renderList, 18 | lists: renderLists 19 | }; -------------------------------------------------------------------------------- /src/styles/sunless-sea.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | } 4 | 5 | .loading-controls { 6 | 7 | } 8 | 9 | .loading-controls:after { 10 | content: ''; 11 | clear: both; 12 | } 13 | 14 | #drop-zone { 15 | display: inline-block; 16 | border: 10px dotted #e0e0e0; 17 | color: #e0e0e0; 18 | padding: 1em; 19 | } 20 | 21 | #drop-zone.drop-target { 22 | color: #000000; 23 | border-color: #000000; 24 | } 25 | 26 | #tabs { 27 | overflow: auto; 28 | } 29 | 30 | #tabs .buttons { 31 | list-style-type: none; 32 | padding: 0; 33 | margin: 1em 0 0 0; 34 | overflow: auto; 35 | } 36 | 37 | #tabs .buttons li { 38 | float: left; 39 | padding: 1em 1em 0.5em 1em; 40 | cursor: pointer; 41 | border: 1px solid transparent; 42 | } 43 | 44 | #tabs .buttons li.active { 45 | border: 1px solid #c0c0c0; 46 | border-bottom-color: #ffffff; 47 | } 48 | 49 | #tabs .panes { 50 | overflow: auto; 51 | border-top: 1px solid #c0c0c0; 52 | } 53 | 54 | #tabs .panes .pane { 55 | float: left; 56 | display: none; 57 | } 58 | 59 | #tabs .panes .pane.file { 60 | display: block; 61 | } 62 | 63 | .clump-list { 64 | 65 | } 66 | 67 | ul.clump-list, 68 | ol.clump-list { 69 | clear: both; 70 | list-style-type: none; 71 | padding: 1em 0 1em 1em; 72 | } 73 | ul.clump-list.normal, 74 | ol.clump-list.normal { 75 | padding: 1em 0 1em 1em; 76 | } 77 | ul.clump-list.small, 78 | ol.clump-list.small { 79 | padding: 0 0 0 1em; 80 | } 81 | 82 | dl.clump-list { 83 | overflow: auto; 84 | } 85 | dl.clump-list dt { 86 | clear: left; 87 | padding: 0; 88 | float: left; 89 | } 90 | dl.clump-list dd { 91 | float: left; 92 | margin: 0; 93 | } 94 | 95 | dl.inline .item { 96 | margin: 0; 97 | } 98 | 99 | span.item, span.item.small { 100 | display: inline-block; 101 | margin: 0; 102 | } 103 | 104 | span.item.quantity { 105 | padding: 0 0.25em; 106 | line-height: 26px; 107 | } 108 | 109 | 110 | .item { 111 | overflow: auto; 112 | margin: 0.25em 0; 113 | padding: 0.25em; 114 | } 115 | .item.normal { 116 | border-left: 1px solid #c0c0c0; 117 | border-bottom: 1px solid #c0c0c0; 118 | } 119 | .item.small { 120 | margin: 0 0 0.25em 0; 121 | padding: 0 0.25em; 122 | } 123 | 124 | .item .icon { 125 | float: left; 126 | margin: 0 0.5em 0 -0.25em; 127 | } 128 | .item.normal .icon { 129 | width: 2.5em; 130 | } 131 | .item.small .icon { 132 | margin-left: 0; 133 | width: 1.25em; 134 | } 135 | 136 | .item .title { 137 | margin: 0; 138 | white-space: nowrap; 139 | } 140 | .item.normal .title { 141 | } 142 | .item.small .title { 143 | font-weight: normal; 144 | padding-left: 1.8em; 145 | line-height: 1.65; 146 | font-size: medium; 147 | } 148 | 149 | .item .title .tag { 150 | padding: 0 0.25em; 151 | border-radius: 3px; 152 | font-size: medium; 153 | font-weight: normal; 154 | margin-left: 0.25em; 155 | color: #c0c0c0; 156 | background-color: #f0f0f0; 157 | border: 1px solid #c0c0c0; 158 | } 159 | 160 | .item .title .tag.success { 161 | color: #00c000; 162 | background-color: #f0fff0; 163 | border-color: #00c000; 164 | } 165 | 166 | .item .title .tag.failure { 167 | color: #c00000; 168 | background-color: #fff0f0; 169 | border-color: #c00000; 170 | } 171 | 172 | .item .description { 173 | margin: 0; 174 | } 175 | .item.normal .description { 176 | } 177 | .item.small .description { 178 | display: none; 179 | } 180 | 181 | .sidebar { 182 | float: right; 183 | margin: 0 0 0.25em 0.25em; 184 | border-left: 1px solid #c0c0c0; 185 | padding-left: 0.5em; 186 | } 187 | .sidebar h4 { 188 | margin: 0 0 0.5em 0; 189 | } 190 | .sidebar .clump-list { 191 | margin: 0; 192 | } 193 | 194 | .item .clump-list.small { 195 | } 196 | 197 | .clump-list.small .quantity { 198 | margin: 0; 199 | line-height: 1.65; 200 | 201 | } 202 | 203 | .item .clump-list { 204 | } 205 | 206 | .hidden { 207 | display: none !important; 208 | } 209 | 210 | 211 | .ancestry-list, .ancestry-list ul, .ancestry-list ol { 212 | padding: 0 0 0 1em; 213 | } 214 | 215 | .ancestry-list ul, .ancestry-list ol { 216 | list-style: none; 217 | border-left: 1px solid #e0e0e0; 218 | } 219 | 220 | .ancestry-list li { 221 | clear: both; 222 | } 223 | 224 | .ancestry-list ul li:before, .ancestry-list ol li:before { 225 | content: '\2196'; 226 | margin: 0 1em 0 0; 227 | float:left; 228 | } 229 | 230 | #query-tree .clump-list { 231 | margin: 0; 232 | } 233 | 234 | #query-tree .item .route-description { 235 | display: inline-block; 236 | font-weight: bold; 237 | margin: 0.5em 0; 238 | } 239 | 240 | #query-tree .item h5 { 241 | margin: 0 0 0.5em 0; 242 | font-weight: normal; 243 | font-size: medium; 244 | } -------------------------------------------------------------------------------- /src/templates/events.json.handlebars: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": {{baseGameIds.prelimEvent}}, 4 | "Name": "You've heard tell of a mysterious little shop", 5 | "Teaser": "Dockside legend says it sells items of use to a fledgeling zee-captain", 6 | "Image": "shopfancy", 7 | "Description": "Following dockside talk, you make your way through the city... to a small, nondescript shop tucked away on the corner of an alleyway. The sign above the door says


    Messirs Faust and Fhlanje

    Purveyors of wonderful mechanical contrivances


    Suitably forewarned, you enter the shop. The opening door knocks a bell attached to the frame, of the type commonly used to summon shopkeepers. You are momentarly surprised when no sound is emitted; instead a sense of profound foreboding peals out across the room.

    A wizened artificer bustles out of a back room, and greets you with an encouraging nod.", 8 | "ParentBranch": null, 9 | "QualitiesAffected": [], 10 | "QualitiesRequired": [], 11 | "Tag": null, 12 | "ExoticEffects": null, 13 | "Note": null, 14 | "ChallengeLevel": 0, 15 | "UnclearedEditAt": null, 16 | "LastEditedBy": null, 17 | "Ordering": 0, 18 | "ShowAsMessage": false, 19 | "LivingStory": null, 20 | "LinkToEvent": null, 21 | "Category": "Unspecialised", 22 | "LimitedToArea": { 23 | "Description": null, 24 | "ImageName": null, 25 | "World": null, 26 | "MarketAccessPermitted": false, 27 | "MoveMessage": null, 28 | "HideName": false, 29 | "RandomPostcard": false, 30 | "MapX": 0, 31 | "MapY": 0, 32 | "UnlocksWithQuality": null, 33 | "ShowOps": false, 34 | "PremiumSubRequired": false, 35 | "Name": null, 36 | "Id": 100321 37 | }, 38 | "World": null, 39 | "Transient": false, 40 | "Stickiness": 0, 41 | "MoveToAreaId": 0, 42 | "MoveToArea": null, 43 | "MoveToDomicile": null, 44 | "SwitchToSetting": null, 45 | "FatePointsChange": 0, 46 | "BootyValue": 0, 47 | "LogInJournalAgainstQuality": null, 48 | "Setting": null, 49 | "Urgency": "Normal", 50 | "OwnerName": null, 51 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 52 | "Distribution": 0, 53 | "Autofire": false, 54 | "CanGoBack": false, 55 | "Deck": { 56 | "World": null, 57 | "Name": "Always", 58 | "ImageName": "100x130", 59 | "Ordering": 1, 60 | "Description": "Always", 61 | "Availability": "Always", 62 | "DrawSize": 3, 63 | "MaxCards": 3, 64 | "Id": 8557 65 | }, 66 | "ChildBranches": [ 67 | { 68 | "Id": {{increment "prelimEvent"}}, 69 | "Name": "\"I... uh... I need a little help...\"", 70 | "Description": "", 71 | "SuccessEvent": null, 72 | "RareDefaultEvent": null, 73 | "RareDefaultEventChance": 0, 74 | "RareSuccessEvent": null, 75 | "RareSuccessEventChance": 0, 76 | "ParentEvent": null, 77 | "QualitiesRequired": [ 78 | { 79 | "DifficultyLevel": null, 80 | "DifficultyAdvanced": null, 81 | "VisibleWhenRequirementFailed": false, 82 | "MinLevel": null, 83 | "MaxLevel": 0, 84 | "MinAdvanced": null, 85 | "MaxAdvanced": null, 86 | "AssociatedQuality": null, 87 | "AssociatedQualityId": {{baseGameIds.quality}}, 88 | "QualityName": null, 89 | "QualityDescription": null, 90 | "QualityImage": null, 91 | "QualityNature": null, 92 | "QualityCategory": null, 93 | "QualityAllowedOn": null, 94 | "Id": {{increment "prelimEvent"}} 95 | } 96 | ], 97 | "Image": null, 98 | "OwnerName": null, 99 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 100 | "CurrencyCost": 0, 101 | "ActionCost": 1, 102 | "RenameQualityCategory": null, 103 | "ButtonText": "", 104 | "Ordering": 3, 105 | "Act": null, 106 | "Archived": false, 107 | "DefaultEvent": { 108 | "Id": {{increment "prelimEvent"}}, 109 | "Name": "\"Ah. New zee-captain, eh?\"", 110 | "Teaser": "", 111 | "Description": "\"Let me guess - you've inherited or otherwise acquired a ship, but know little of the Neath? You've heard all about the dangers of a life at zee, and were hoping to avoid some of the more senseless or boring ways to die?\"

    You nod, dumbly. Clearly the artificer has been here before, so to speak.

    \"Capital!\" he grins at you, \"I have just what you need. The price is 100 Echoes, and the acceptance of our standard terms of sale.\"", 112 | "ChildBranches": [], 113 | "ParentBranch": null, 114 | "QualitiesAffected": [], 115 | "QualitiesRequired": [], 116 | "Image": null, 117 | "Tag": null, 118 | "ExoticEffects": "", 119 | "Note": null, 120 | "ChallengeLevel": 0, 121 | "UnclearedEditAt": null, 122 | "LastEditedBy": null, 123 | "Ordering": 0, 124 | "ShowAsMessage": false, 125 | "LivingStory": null, 126 | "Deck": null, 127 | "Category": "Unspecialised", 128 | "LimitedToArea": null, 129 | "World": null, 130 | "Transient": false, 131 | "Stickiness": 0, 132 | "MoveToAreaId": 0, 133 | "MoveToArea": null, 134 | "MoveToDomicile": null, 135 | "SwitchToSetting": null, 136 | "FatePointsChange": 0, 137 | "BootyValue": 0, 138 | "LogInJournalAgainstQuality": null, 139 | "Setting": null, 140 | "Urgency": "Normal", 141 | "OwnerName": null, 142 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 143 | "Distribution": 0, 144 | "Autofire": true, 145 | "CanGoBack": false, 146 | "LinkToEvent": { 147 | "Id": {{baseGameIds.buyOracle}} 148 | } 149 | } 150 | }, 151 | { 152 | "Id": {{increment "prelimEvent"}}, 153 | "Name": "\"Rid me of this wretched thing\"", 154 | "Description": "", 155 | "SuccessEvent": null, 156 | "RareDefaultEvent": null, 157 | "RareDefaultEventChance": 0, 158 | "RareSuccessEvent": null, 159 | "RareSuccessEventChance": 0, 160 | "ParentEvent": null, 161 | "QualitiesRequired": [ 162 | { 163 | "DifficultyLevel": null, 164 | "DifficultyAdvanced": null, 165 | "VisibleWhenRequirementFailed": false, 166 | "MinLevel": 1, 167 | "MaxLevel": null, 168 | "MinAdvanced": null, 169 | "MaxAdvanced": null, 170 | "AssociatedQuality": null, 171 | "AssociatedQualityId": {{baseGameIds.quality}}, 172 | "QualityName": null, 173 | "QualityDescription": null, 174 | "QualityImage": null, 175 | "QualityNature": null, 176 | "QualityCategory": null, 177 | "QualityAllowedOn": null, 178 | "Id": {{increment "prelimEvent"}} 179 | } 180 | ], 181 | "Image": null, 182 | "OwnerName": null, 183 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 184 | "CurrencyCost": 0, 185 | "ActionCost": 1, 186 | "RenameQualityCategory": null, 187 | "ButtonText": "", 188 | "Ordering": 3, 189 | "Act": null, 190 | "Archived": false, 191 | "DefaultEvent": { 192 | "Id": {{increment "prelimEvent"}}, 193 | "Name": "\"No refunds\"", 194 | "Teaser": "", 195 | "Description": "\"I'm afraid we have a strict policy against refunds, sir.\"

    \"However, if you were desperate enough we would consider... disposing of the device for you. No charge.\"", 196 | "ChildBranches": [], 197 | "ParentBranch": null, 198 | "QualitiesAffected": [], 199 | "QualitiesRequired": [], 200 | "Image": null, 201 | "Tag": null, 202 | "ExoticEffects": "", 203 | "Note": null, 204 | "ChallengeLevel": 0, 205 | "UnclearedEditAt": null, 206 | "LastEditedBy": null, 207 | "Ordering": 0, 208 | "ShowAsMessage": false, 209 | "LivingStory": null, 210 | "Deck": null, 211 | "Category": "Unspecialised", 212 | "LimitedToArea": null, 213 | "World": null, 214 | "Transient": false, 215 | "Stickiness": 0, 216 | "MoveToAreaId": 0, 217 | "MoveToArea": null, 218 | "MoveToDomicile": null, 219 | "SwitchToSetting": null, 220 | "FatePointsChange": 0, 221 | "BootyValue": 0, 222 | "LogInJournalAgainstQuality": null, 223 | "Setting": null, 224 | "Urgency": "Normal", 225 | "OwnerName": null, 226 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 227 | "Distribution": 0, 228 | "Autofire": true, 229 | "CanGoBack": false, 230 | "LinkToEvent": { 231 | "Id": {{baseGameIds.sellOracle}} 232 | } 233 | } 234 | } 235 | 236 | ] 237 | }, 238 | { 239 | "Id": {{baseGameIds.buyOracle}}, 240 | "Name": "The Terms", 241 | "Teaser": "", 242 | "Image": "paperstack", 243 | "Description": "At this he reaches under the counter and unrolls a fat scroll of parchment, spilling across the desk, onto the floor and half-way to the door.

    The text is scribbled in a crabbed hand that begins at a fraction of an inch tall, and shrinks as the document goes on. As you skim across it random phrases jump out at you - \"disclaim all responsibility, express or implied\"... \"aforeheretomentioned party in the third part\"... \"horribly lingering spiky death\"... \"loss or vivisection of your immortal soul\" - before you resolve to just stop reading.", 244 | "ParentBranch": null, 245 | "QualitiesAffected": [], 246 | "QualitiesRequired": [], 247 | "Tag": null, 248 | "ExoticEffects": null, 249 | "Note": null, 250 | "ChallengeLevel": 0, 251 | "UnclearedEditAt": null, 252 | "LastEditedBy": null, 253 | "Ordering": 0, 254 | "ShowAsMessage": false, 255 | "LivingStory": null, 256 | "LinkToEvent": null, 257 | "Category": "Unspecialised", 258 | "LimitedToArea": { 259 | "Description": null, 260 | "ImageName": null, 261 | "World": null, 262 | "MarketAccessPermitted": false, 263 | "MoveMessage": null, 264 | "HideName": false, 265 | "RandomPostcard": false, 266 | "MapX": 0, 267 | "MapY": 0, 268 | "UnlocksWithQuality": null, 269 | "ShowOps": false, 270 | "PremiumSubRequired": false, 271 | "Name": null, 272 | "Id": 101956 273 | }, 274 | "World": null, 275 | "Transient": false, 276 | "Stickiness": 0, 277 | "MoveToAreaId": 0, 278 | "MoveToArea": null, 279 | "MoveToDomicile": null, 280 | "SwitchToSetting": null, 281 | "FatePointsChange": 0, 282 | "BootyValue": 0, 283 | "LogInJournalAgainstQuality": null, 284 | "Setting": null, 285 | "Urgency": "Normal", 286 | "OwnerName": null, 287 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 288 | "Distribution": 0, 289 | "Autofire": false, 290 | "CanGoBack": false, 291 | "Deck": { 292 | "World": null, 293 | "Name": "Always", 294 | "ImageName": "100x130", 295 | "Ordering": 1, 296 | "Description": "Always", 297 | "Availability": "Always", 298 | "DrawSize": 3, 299 | "MaxCards": 3, 300 | "Id": 8557 301 | }, 302 | "ChildBranches": [ 303 | { 304 | "Id": {{increment "buyOracle"}}, 305 | "Name": "Sign the terms of sale", 306 | "Description": "[This will purchase a Clockwork Oracle at a cost of 100 Echoes]", 307 | "SuccessEvent": null, 308 | "RareDefaultEvent": null, 309 | "RareDefaultEventChance": 0, 310 | "RareSuccessEvent": null, 311 | "RareSuccessEventChance": 0, 312 | "ParentEvent": null, 313 | "QualitiesRequired": [], 314 | "Image": null, 315 | "OwnerName": null, 316 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 317 | "CurrencyCost": 0, 318 | "ActionCost": 1, 319 | "RenameQualityCategory": null, 320 | "ButtonText": "", 321 | "Ordering": 3, 322 | "Act": null, 323 | "Archived": false, 324 | "DefaultEvent": { 325 | "Id": {{increment "buyOracle"}}, 326 | "Name": "\"Very good, sir\"", 327 | "Teaser": "", 328 | "Description": "\"I'll have the device delivered to your hold forthwith. It should be installed before you get back to your ship.\"

    You hand over the money and turn to leave, nonplussed.

    \"One last thing sir!\" calls the artificer, as you pause, hand on doorknob.

    \"The mechanism will never lie to you sir, but it's not always... strictly advisable to take it at its word, either. Clockwork can't dissemble, but it may omit... and, well...\"

    He pauses, and takes a deap breath. \"While I would never suggest that a mere mechanical contrivance could harbour an opinion about its owner, I would be lying if I said sometimes at night... when the lights are low in the workshop and there's no-one else about... Well sir, I wouldn't bet my life it couldn't harbour a deep and abiding malevolence towards its keeper, if you know what I mean.\"

    \"Good luck sir, and I mean that in the most sincere way possible.\"

    As you step out onto the street, thoroughly unsettled, the door slams behind you, and you hear the sound of bolts being hurriedly thrown.", 329 | "ChildBranches": [], 330 | "ParentBranch": null, 331 | "QualitiesAffected": [ 332 | { 333 | "ForceEquip": false, 334 | "OnlyIfAtLeast": null, 335 | "OnlyIfNoMoreThan": null, 336 | "SetToExactlyAdvanced": null, 337 | "ChangeByAdvanced": null, 338 | "SetToExactly": null, 339 | "TargetQuality": null, 340 | "TargetLevel": null, 341 | "CompletionMessage": null, 342 | "Level": 1, 343 | "AssociatedQuality": null, 344 | "AssociatedQualityId": {{baseGameIds.quality}}, 345 | "QualityName": null, 346 | "QualityDescription": null, 347 | "QualityImage": null, 348 | "QualityNature": null, 349 | "QualityCategory": null, 350 | "QualityAllowedOn": null, 351 | "Id": {{increment "buyOracle"}} 352 | }, 353 | { 354 | "ForceEquip": false, 355 | "OnlyIfAtLeast": null, 356 | "OnlyIfNoMoreThan": null, 357 | "SetToExactlyAdvanced": null, 358 | "ChangeByAdvanced": null, 359 | "SetToExactly": null, 360 | "TargetQuality": null, 361 | "TargetLevel": null, 362 | "CompletionMessage": null, 363 | "Level": -100, 364 | "AssociatedQuality": null, 365 | "AssociatedQualityId": 102028, 366 | "QualityName": null, 367 | "QualityDescription": null, 368 | "QualityImage": null, 369 | "QualityNature": null, 370 | "QualityCategory": null, 371 | "QualityAllowedOn": null, 372 | "Id": {{increment "buyOracle"}} 373 | } 374 | ], 375 | "QualitiesRequired": [], 376 | "Image": null, 377 | "Tag": null, 378 | "ExoticEffects": "", 379 | "Note": null, 380 | "ChallengeLevel": 0, 381 | "UnclearedEditAt": null, 382 | "LastEditedBy": null, 383 | "Ordering": 0, 384 | "ShowAsMessage": false, 385 | "LivingStory": null, 386 | "Deck": null, 387 | "Category": "Unspecialised", 388 | "LimitedToArea": null, 389 | "World": null, 390 | "Transient": false, 391 | "Stickiness": 0, 392 | "MoveToAreaId": 0, 393 | "MoveToArea": null, 394 | "MoveToDomicile": null, 395 | "SwitchToSetting": null, 396 | "FatePointsChange": 0, 397 | "BootyValue": 0, 398 | "LogInJournalAgainstQuality": null, 399 | "Setting": null, 400 | "Urgency": "Normal", 401 | "OwnerName": null, 402 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 403 | "Distribution": 0, 404 | "Autofire": true, 405 | "CanGoBack": false, 406 | "LinkToEvent": null 407 | } 408 | } 409 | ] 410 | }, 411 | { 412 | "Id": {{baseGameIds.sellOracle}}, 413 | "Name": "Deliver me of this infernal contraption", 414 | "Teaser": "", 415 | "Image": "paperstack", 416 | "Description": "", 417 | "ParentBranch": null, 418 | "QualitiesAffected": [], 419 | "QualitiesRequired": [], 420 | "Tag": null, 421 | "ExoticEffects": null, 422 | "Note": null, 423 | "ChallengeLevel": 0, 424 | "UnclearedEditAt": null, 425 | "LastEditedBy": null, 426 | "Ordering": 0, 427 | "ShowAsMessage": false, 428 | "LivingStory": null, 429 | "LinkToEvent": null, 430 | "Category": "Unspecialised", 431 | "LimitedToArea": { 432 | "Description": null, 433 | "ImageName": null, 434 | "World": null, 435 | "MarketAccessPermitted": false, 436 | "MoveMessage": null, 437 | "HideName": false, 438 | "RandomPostcard": false, 439 | "MapX": 0, 440 | "MapY": 0, 441 | "UnlocksWithQuality": null, 442 | "ShowOps": false, 443 | "PremiumSubRequired": false, 444 | "Name": null, 445 | "Id": 101956 446 | }, 447 | "World": null, 448 | "Transient": false, 449 | "Stickiness": 0, 450 | "MoveToAreaId": 0, 451 | "MoveToArea": null, 452 | "MoveToDomicile": null, 453 | "SwitchToSetting": null, 454 | "FatePointsChange": 0, 455 | "BootyValue": 0, 456 | "LogInJournalAgainstQuality": null, 457 | "Setting": null, 458 | "Urgency": "Normal", 459 | "OwnerName": null, 460 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 461 | "Distribution": 0, 462 | "Autofire": false, 463 | "CanGoBack": false, 464 | "Deck": { 465 | "World": null, 466 | "Name": "Always", 467 | "ImageName": "100x130", 468 | "Ordering": 1, 469 | "Description": "Always", 470 | "Availability": "Always", 471 | "DrawSize": 3, 472 | "MaxCards": 3, 473 | "Id": 8557 474 | }, 475 | "ChildBranches": [ 476 | { 477 | "Id": {{increment "sellOracle"}}, 478 | "Name": "\"Yes! Please! Take this device from my hold so I am no longer bound to it!\"", 479 | "Description": "[This will remove the Clockwork Oracle from your hold]", 480 | "SuccessEvent": null, 481 | "RareDefaultEvent": null, 482 | "RareDefaultEventChance": 0, 483 | "RareSuccessEvent": null, 484 | "RareSuccessEventChance": 0, 485 | "ParentEvent": null, 486 | "QualitiesRequired": [], 487 | "Image": null, 488 | "OwnerName": null, 489 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 490 | "CurrencyCost": 0, 491 | "ActionCost": 1, 492 | "RenameQualityCategory": null, 493 | "ButtonText": "", 494 | "Ordering": 3, 495 | "Act": null, 496 | "Archived": false, 497 | "DefaultEvent": { 498 | "Id": {{increment "sellOracle"}}, 499 | "Name": "\"Very good, sir\"", 500 | "Teaser": "", 501 | "Description": "\"Very good, sir. I'll send the rattus faber to remove and dispose of it forthwith.\"

    \"Thanks once again for your business, and should you find yourself in need of unearthly clockwork eldritch abominations in the future, we hope you'll think of us.\"", 502 | "ChildBranches": [], 503 | "ParentBranch": null, 504 | "QualitiesAffected": [ 505 | { 506 | "ForceEquip": false, 507 | "OnlyIfAtLeast": null, 508 | "OnlyIfNoMoreThan": null, 509 | "SetToExactlyAdvanced": null, 510 | "ChangeByAdvanced": null, 511 | "SetToExactly": null, 512 | "TargetQuality": null, 513 | "TargetLevel": null, 514 | "CompletionMessage": null, 515 | "Level": -1, 516 | "AssociatedQuality": null, 517 | "AssociatedQualityId": {{baseGameIds.quality}}, 518 | "QualityName": null, 519 | "QualityDescription": null, 520 | "QualityImage": null, 521 | "QualityNature": null, 522 | "QualityCategory": null, 523 | "QualityAllowedOn": null, 524 | "Id": {{increment "sellOracle"}} 525 | } 526 | ], 527 | "QualitiesRequired": [], 528 | "Image": null, 529 | "Tag": null, 530 | "ExoticEffects": "", 531 | "Note": null, 532 | "ChallengeLevel": 0, 533 | "UnclearedEditAt": null, 534 | "LastEditedBy": null, 535 | "Ordering": 0, 536 | "ShowAsMessage": false, 537 | "LivingStory": null, 538 | "Deck": null, 539 | "Category": "Unspecialised", 540 | "LimitedToArea": null, 541 | "World": null, 542 | "Transient": false, 543 | "Stickiness": 0, 544 | "MoveToAreaId": 0, 545 | "MoveToArea": null, 546 | "MoveToDomicile": null, 547 | "SwitchToSetting": null, 548 | "FatePointsChange": 0, 549 | "BootyValue": 0, 550 | "LogInJournalAgainstQuality": null, 551 | "Setting": null, 552 | "Urgency": "Normal", 553 | "OwnerName": null, 554 | "DateTimeCreated": "2015-09-23T21:38:49.408Z", 555 | "Distribution": 0, 556 | "Autofire": true, 557 | "CanGoBack": false, 558 | "LinkToEvent": null 559 | } 560 | } 561 | ] 562 | }, 563 | { 564 | "Id": {{baseGameIds.event}}, 565 | "Name": "The Clockwork Oracle", 566 | "Teaser": "Knowledge is power, but ignorance is bliss...", 567 | "Image": "mechanism", 568 | "Description": "The machine squats on your desk, steaming and ticking and wheezing to itself. Covered in disturbing sigils that appear to shift when you blink, it projects an undefinable aura of mystery, temptation... and just a hint of malevolent smugness.", 569 | "ParentBranch": null, 570 | "QualitiesAffected": [], 571 | "QualitiesRequired": [ 572 | { 573 | "DifficultyLevel": null, 574 | "DifficultyAdvanced": null, 575 | "VisibleWhenRequirementFailed": false, 576 | "MinLevel": 1, 577 | "MaxLevel": 1, 578 | "MinAdvanced": null, 579 | "MaxAdvanced": null, 580 | "AssociatedQuality": null, 581 | "AssociatedQualityId": {{baseGameIds.quality}}, 582 | "QualityName": null, 583 | "QualityDescription": null, 584 | "QualityImage": null, 585 | "QualityNature": null, 586 | "QualityCategory": null, 587 | "QualityAllowedOn": null, 588 | "Id": {{increment "event"}} 589 | } 590 | ], 591 | "Tag": null, 592 | "ExoticEffects": null, 593 | "Note": null, 594 | "ChallengeLevel": 0, 595 | "UnclearedEditAt": null, 596 | "LastEditedBy": null, 597 | "Ordering": 0, 598 | "ShowAsMessage": false, 599 | "LivingStory": null, 600 | "LinkToEvent": null, 601 | "Category": "Unspecialised", 602 | "LimitedToArea": { 603 | "Description": null, 604 | "ImageName": null, 605 | "World": null, 606 | "MarketAccessPermitted": false, 607 | "MoveMessage": null, 608 | "HideName": false, 609 | "RandomPostcard": false, 610 | "MapX": 0, 611 | "MapY": 0, 612 | "UnlocksWithQuality": null, 613 | "ShowOps": false, 614 | "PremiumSubRequired": false, 615 | "Name": null, 616 | "Id": 101956 617 | }, 618 | "World": null, 619 | "Transient": false, 620 | "Stickiness": 0, 621 | "MoveToAreaId": 0, 622 | "MoveToArea": null, 623 | "MoveToDomicile": null, 624 | "SwitchToSetting": null, 625 | "FatePointsChange": 0, 626 | "BootyValue": 0, 627 | "LogInJournalAgainstQuality": null, 628 | "Setting": null, 629 | "Urgency": "Normal", 630 | "OwnerName": null, 631 | "DateTimeCreated": "{{buildDateTime}}", 632 | "Distribution": 0, 633 | "Autofire": false, 634 | "CanGoBack": false, 635 | "Deck": { 636 | "World": null, 637 | "Name": "Always", 638 | "ImageName": "100x130", 639 | "Ordering": 1, 640 | "Description": "Always", 641 | "Availability": "Always", 642 | "DrawSize": 3, 643 | "MaxCards": 3, 644 | "Id": 8557 645 | }, 646 | "ChildBranches": [ 647 | { 648 | "Id": {{bumpToNext "event" 10}}, 649 | "Name": "Examine the machine more closely", 650 | "Description": "But not too closely, eh?", 651 | "SuccessEvent": null, 652 | "RareDefaultEvent": null, 653 | "RareDefaultEventChance": 0, 654 | "RareSuccessEvent": null, 655 | "RareSuccessEventChance": 0, 656 | "ParentEvent": null, 657 | "QualitiesRequired": [], 658 | "Image": null, 659 | "OwnerName": null, 660 | "DateTimeCreated": "{{buildDateTime}}", 661 | "CurrencyCost": 0, 662 | "ActionCost": 1, 663 | "RenameQualityCategory": null, 664 | "ButtonText": "", 665 | "Ordering": 3, 666 | "Act": null, 667 | "Archived": false, 668 | "DefaultEvent": { 669 | "Id": {{increment "event"}}, 670 | "Name": "You lean closer, and peer at the device", 671 | "Teaser": "", 672 | "Description": "The casing seems to be mostly made of some unknown metal, but other parts are glass, or crystal. Peering through a dim inspection window some components even look organic, while others appear to refract light wrongly or even flicker in and out of existence altogether. A large brass key projects from the side, covered in delicate carvings of figures doing things you'd really rather not examine too closely. On the front a dented gold plaque bears the legend \"Quod tu nescis potes nocere tibi\".

    Gizmologists and occultarians debate its method of operation; some believe it contains a captured panopticist demon, others that its intricate clockwork encodes a capricious intelligence. Some even assert that it somehow directly reads the Correspondence itself to formulate its answers, with all of the unearthly danger that implies.

    All that is generally known for certain is that its pronouncements (while vague) are almost always completely accurate... and that after taking its advice generations of zee-captains have confidently sailed off to their doom.", 673 | "ChildBranches": [], 674 | "ParentBranch": null, 675 | "QualitiesAffected": [], 676 | "QualitiesRequired": [], 677 | "Image": null, 678 | "Tag": null, 679 | "ExoticEffects": "", 680 | "Note": null, 681 | "ChallengeLevel": 0, 682 | "UnclearedEditAt": null, 683 | "LastEditedBy": null, 684 | "Ordering": 0, 685 | "ShowAsMessage": false, 686 | "LivingStory": null, 687 | "Deck": null, 688 | "Category": "Unspecialised", 689 | "LimitedToArea": null, 690 | "World": null, 691 | "Transient": false, 692 | "Stickiness": 0, 693 | "MoveToAreaId": 0, 694 | "MoveToArea": null, 695 | "MoveToDomicile": null, 696 | "SwitchToSetting": null, 697 | "FatePointsChange": 0, 698 | "BootyValue": 0, 699 | "LogInJournalAgainstQuality": null, 700 | "Setting": null, 701 | "Urgency": "Normal", 702 | "OwnerName": null, 703 | "DateTimeCreated": "{{buildDateTime}}", 704 | "Distribution": 0, 705 | "Autofire": true, 706 | "CanGoBack": false, 707 | "LinkToEvent": { 708 | "Id": {{baseGameIds.event}} 709 | } 710 | } 711 | }, 712 | { 713 | "Id": {{bumpToNext "event" 10}}, 714 | "Name": "Consult the infernal device", 715 | "Description": "It knows many things, but which will it reveal? And why?", 716 | "SuccessEvent": null, 717 | "RareDefaultEvent": null, 718 | "RareDefaultEventChance": 19, 719 | "RareSuccessEvent": null, 720 | "RareSuccessEventChance": 0, 721 | "ParentEvent": null, 722 | "QualitiesRequired": [], 723 | "Image": null, 724 | "OwnerName": null, 725 | "DateTimeCreated": "{{buildDateTime}}", 726 | "CurrencyCost": 0, 727 | "ActionCost": 1, 728 | "RenameQualityCategory": null, 729 | "ButtonText": "", 730 | "Ordering": 3, 731 | "Act": null, 732 | "Archived": false, 733 | "DefaultEvent": { 734 | "Id": {{increment "event"}}, 735 | "Description": "The machine gurgles to itself, reflectively. Relays close deep within its innards, and a smell of ozone and brimstone fills the cabin. With a hiss a tray of neatly-labelled levers emerges from a panel on the front.

    A plaque on the front reads...", 736 | "LinkToEvent": { 737 | "Id": {{bumpToNext "event" 100}} 738 | } 739 | } 740 | } 741 | ] 742 | }, 743 | { 744 | "Id": {{id "event"}}, 745 | "Name": "What would you like to do?", 746 | "Teaser": "", 747 | "Description": "You reach hesitantly for the levers, running your fingers over them. You can sense the potential within, each pregnant with equal parts mortal danger and seductive augury.

    Seized with sudden conviction, you reach forward and grasp the one marked...", 748 | "ParentBranch": null, 749 | "QualitiesAffected": [], 750 | "QualitiesRequired": [], 751 | "Image": "mechanism", 752 | "Tag": null, 753 | "ExoticEffects": "", 754 | "Note": null, 755 | "ChallengeLevel": 0, 756 | "UnclearedEditAt": null, 757 | "LastEditedBy": null, 758 | "Ordering": 0, 759 | "ShowAsMessage": false, 760 | "LivingStory": null, 761 | "Deck": { 762 | "World": null, 763 | "Name": "Always", 764 | "ImageName": "100x130", 765 | "Ordering": 1, 766 | "Description": "Always", 767 | "Availability": "Always", 768 | "DrawSize": 3, 769 | "MaxCards": 3, 770 | "Id": 8557 771 | }, 772 | "Category": "Unspecialised", 773 | "LimitedToArea": { 774 | "Description": null, 775 | "ImageName": null, 776 | "World": null, 777 | "MarketAccessPermitted": false, 778 | "MoveMessage": null, 779 | "HideName": false, 780 | "RandomPostcard": false, 781 | "MapX": 0, 782 | "MapY": 0, 783 | "UnlocksWithQuality": null, 784 | "ShowOps": false, 785 | "PremiumSubRequired": false, 786 | "Name": null, 787 | "Id": 101956 788 | }, 789 | "World": null, 790 | "Transient": false, 791 | "Stickiness": 0, 792 | "MoveToAreaId": 0, 793 | "MoveToArea": null, 794 | "MoveToDomicile": null, 795 | "SwitchToSetting": null, 796 | "FatePointsChange": 0, 797 | "BootyValue": 0, 798 | "LogInJournalAgainstQuality": null, 799 | "Setting": null, 800 | "Urgency": "Normal", 801 | "OwnerName": null, 802 | "DateTimeCreated": "{{buildDateTime}}", 803 | "Distribution": 0, 804 | "Autofire": true, 805 | "CanGoBack": false, 806 | "LinkToEvent": null, 807 | "ChildBranches": [ 808 | { 809 | "Id": {{bumpToNext "event" 10}}, 810 | "Name": "\"To Acquire\"", 811 | "Description": "Desire is the root of all suffering. What could possibly go wrong?", 812 | "SuccessEvent": null, 813 | "RareDefaultEvent": null, 814 | "RareDefaultEventChance": 19, 815 | "RareSuccessEvent": null, 816 | "RareSuccessEventChance": 0, 817 | "ParentEvent": null, 818 | "QualitiesRequired": [], 819 | "Image": null, 820 | "OwnerName": null, 821 | "DateTimeCreated": "{{buildDateTime}}", 822 | "CurrencyCost": 0, 823 | "ActionCost": 1, 824 | "RenameQualityCategory": null, 825 | "ButtonText": "", 826 | "Ordering": 3, 827 | "Act": null, 828 | "Archived": false, 829 | "DefaultEvent": { 830 | "Id": {{increment "event"}}, 831 | "Description": "The lever is bedecked with ornate carvings - a throng of golden humanoid figures, frantically climbing over each other to reach a metal ingot at the end.

    You grasp the lever and pull it towards you. It proves strangely hard to let go.", 832 | "LinkToEvent": { 833 | "Id": {{baseGameIds.acquire}} 834 | } 835 | } 836 | }, 837 | { 838 | "Id": {{bumpToNext "event" 10}}, 839 | "Name": "\"To Learn\"", 840 | "Description": "Knowledge is power, but power corrupts...", 841 | "SuccessEvent": null, 842 | "RareDefaultEvent": null, 843 | "RareDefaultEventChance": 19, 844 | "RareSuccessEvent": null, 845 | "RareSuccessEventChance": 0, 846 | "ParentEvent": null, 847 | "QualitiesRequired": [], 848 | "Image": null, 849 | "OwnerName": null, 850 | "DateTimeCreated": "{{buildDateTime}}", 851 | "CurrencyCost": 0, 852 | "ActionCost": 1, 853 | "RenameQualityCategory": null, 854 | "ButtonText": "", 855 | "Ordering": 3, 856 | "Act": null, 857 | "Archived": false, 858 | "DefaultEvent": { 859 | "Id": {{increment "event"}}, 860 | "Description": "The end of the lever is beautifully sculpted; crisp lines and elegant curves, polished to a nearly mirror-finish.

    As you look down the lever, however, it... changes. A patina develops on the metal finish, the lines waver and become blurred, and the shape becomes increasingly irregular. By the time it joins the body of the machine it is a sickly brown mass; pitted and rusty, with metal flaking onto the desk below.", 861 | "LinkToEvent": { 862 | "Id": {{baseGameIds.learn}} 863 | } 864 | } 865 | }, 866 | { 867 | "Id": {{bumpToNext "event" 10}}, 868 | "Name": "\"To Suffer\"", 869 | "Description": "That which does not kill us makes us stronger. Or merely eternally regretful.", 870 | "SuccessEvent": null, 871 | "RareDefaultEvent": null, 872 | "RareDefaultEventChance": 19, 873 | "RareSuccessEvent": null, 874 | "RareSuccessEventChance": 0, 875 | "ParentEvent": null, 876 | "QualitiesRequired": [], 877 | "Image": null, 878 | "OwnerName": null, 879 | "DateTimeCreated": "{{buildDateTime}}", 880 | "CurrencyCost": 0, 881 | "ActionCost": 1, 882 | "RenameQualityCategory": null, 883 | "ButtonText": "", 884 | "Ordering": 3, 885 | "Act": null, 886 | "Archived": false, 887 | "DefaultEvent": { 888 | "Id": {{increment "event"}}, 889 | "Description": "The lever is a simple metal rod, completely covered in sharp, hypodermic-like metal needles.

    It is exceedingly stiff.", 890 | "LinkToEvent": { 891 | "Id": {{baseGameIds.suffer}} 892 | } 893 | } 894 | }, 895 | { 896 | "Id": {{bumpToNext "event" 10}}, 897 | "Name": "\"To Become\"", 898 | "Description": "We may not evolve without abandoning who we are. Change is death.", 899 | "SuccessEvent": null, 900 | "RareDefaultEvent": null, 901 | "RareDefaultEventChance": 19, 902 | "RareSuccessEvent": null, 903 | "RareSuccessEventChance": 0, 904 | "ParentEvent": null, 905 | "QualitiesRequired": [], 906 | "Image": null, 907 | "OwnerName": null, 908 | "DateTimeCreated": "{{buildDateTime}}", 909 | "CurrencyCost": 0, 910 | "ActionCost": 1, 911 | "RenameQualityCategory": null, 912 | "ButtonText": "", 913 | "Ordering": 3, 914 | "Act": null, 915 | "Archived": false, 916 | "DefaultEvent": { 917 | "Id": {{increment "event"}}, 918 | "Description": "The lever is engraved with a design of caterpillars. Rising up the lever they form pupae, which split down the middle to reveal... more catepillars.

    The artist, it appears, has a sense of humour.", 919 | "LinkToEvent": { 920 | "Id": {{baseGameIds.become}} 921 | } 922 | } 923 | } 924 | ] 925 | }, 926 | { 927 | "Id": {{baseGameIds.acquire}}, 928 | "Name": "\"I desire material goods\"", 929 | "Teaser": "", 930 | "Description": "The Clockwork Oracle whirrs solicitously. A panel rotates, to reveal directions.

    \"Speak aloud your heart's desire.\"", 931 | "ParentBranch": null, 932 | "QualitiesAffected": [], 933 | "QualitiesRequired": [], 934 | "Image": "diamondblue", 935 | "Tag": null, 936 | "ExoticEffects": "", 937 | "Note": null, 938 | "ChallengeLevel": 0, 939 | "UnclearedEditAt": null, 940 | "LastEditedBy": null, 941 | "Ordering": 0, 942 | "ShowAsMessage": false, 943 | "LivingStory": null, 944 | "Deck": { 945 | "World": null, 946 | "Name": "Always", 947 | "ImageName": "100x130", 948 | "Ordering": 1, 949 | "Description": "Always", 950 | "Availability": "Always", 951 | "DrawSize": 3, 952 | "MaxCards": 3, 953 | "Id": 8557 954 | }, 955 | "Category": "Unspecialised", 956 | "LimitedToArea": { 957 | "Description": null, 958 | "ImageName": null, 959 | "World": null, 960 | "MarketAccessPermitted": false, 961 | "MoveMessage": null, 962 | "HideName": false, 963 | "RandomPostcard": false, 964 | "MapX": 0, 965 | "MapY": 0, 966 | "UnlocksWithQuality": null, 967 | "ShowOps": false, 968 | "PremiumSubRequired": false, 969 | "Name": null, 970 | "Id": 101956 971 | }, 972 | "World": null, 973 | "Transient": false, 974 | "Stickiness": 0, 975 | "MoveToAreaId": 0, 976 | "MoveToArea": null, 977 | "MoveToDomicile": null, 978 | "SwitchToSetting": null, 979 | "FatePointsChange": 0, 980 | "BootyValue": 0, 981 | "LogInJournalAgainstQuality": null, 982 | "Setting": null, 983 | "Urgency": "Normal", 984 | "OwnerName": null, 985 | "DateTimeCreated": "{{buildDateTime}}", 986 | "Distribution": 0, 987 | "Autofire": true, 988 | "CanGoBack": false, 989 | "LinkToEvent": null, 990 | "ChildBranches": [ 991 | {{{eventAcquireChildren}}} 992 | ] 993 | }, 994 | { 995 | "Id": {{baseGameIds.learn}}, 996 | "Name": "\"I desire to know\"", 997 | "Teaser": "", 998 | "Description": "There is an sudden silence, as the machine stops dead.

    After a long and pregnant pause, a card slides ominously from the a slot on the front faceplate.

    \"What precisely would you like to know?\"", 999 | "ParentBranch": null, 1000 | "QualitiesAffected": [], 1001 | "QualitiesRequired": [], 1002 | "Image": "booktears", 1003 | "Tag": null, 1004 | "ExoticEffects": "", 1005 | "Note": null, 1006 | "ChallengeLevel": 0, 1007 | "UnclearedEditAt": null, 1008 | "LastEditedBy": null, 1009 | "Ordering": 0, 1010 | "ShowAsMessage": false, 1011 | "LivingStory": null, 1012 | "Deck": { 1013 | "World": null, 1014 | "Name": "Always", 1015 | "ImageName": "100x130", 1016 | "Ordering": 1, 1017 | "Description": "Always", 1018 | "Availability": "Always", 1019 | "DrawSize": 3, 1020 | "MaxCards": 3, 1021 | "Id": 8557 1022 | }, 1023 | "Category": "Unspecialised", 1024 | "LimitedToArea": { 1025 | "Description": null, 1026 | "ImageName": null, 1027 | "World": null, 1028 | "MarketAccessPermitted": false, 1029 | "MoveMessage": null, 1030 | "HideName": false, 1031 | "RandomPostcard": false, 1032 | "MapX": 0, 1033 | "MapY": 0, 1034 | "UnlocksWithQuality": null, 1035 | "ShowOps": false, 1036 | "PremiumSubRequired": false, 1037 | "Name": null, 1038 | "Id": 101956 1039 | }, 1040 | "World": null, 1041 | "Transient": false, 1042 | "Stickiness": 0, 1043 | "MoveToAreaId": 0, 1044 | "MoveToArea": null, 1045 | "MoveToDomicile": null, 1046 | "SwitchToSetting": null, 1047 | "FatePointsChange": 0, 1048 | "BootyValue": 0, 1049 | "LogInJournalAgainstQuality": null, 1050 | "Setting": null, 1051 | "Urgency": "Normal", 1052 | "OwnerName": null, 1053 | "DateTimeCreated": "{{buildDateTime}}", 1054 | "Distribution": 0, 1055 | "Autofire": true, 1056 | "CanGoBack": false, 1057 | "LinkToEvent": null, 1058 | "ChildBranches": [ 1059 | {{{eventLearnChildren}}} 1060 | ] 1061 | }, 1062 | { 1063 | "Id": {{baseGameIds.suffer}}, 1064 | "Name": "\"I desire to suffer\"", 1065 | "Teaser": "", 1066 | "Description": "The clicking increases in tempo. The Oracle exudes an air of lip-licking anticipation. It is evidently pleased at the thought.", 1067 | "ParentBranch": null, 1068 | "QualitiesAffected": [], 1069 | "QualitiesRequired": [], 1070 | "Image": "bloodstain", 1071 | "Tag": null, 1072 | "ExoticEffects": "", 1073 | "Note": null, 1074 | "ChallengeLevel": 0, 1075 | "UnclearedEditAt": null, 1076 | "LastEditedBy": null, 1077 | "Ordering": 0, 1078 | "ShowAsMessage": false, 1079 | "LivingStory": null, 1080 | "Deck": { 1081 | "World": null, 1082 | "Name": "Always", 1083 | "ImageName": "100x130", 1084 | "Ordering": 1, 1085 | "Description": "Always", 1086 | "Availability": "Always", 1087 | "DrawSize": 3, 1088 | "MaxCards": 3, 1089 | "Id": 8557 1090 | }, 1091 | "Category": "Unspecialised", 1092 | "LimitedToArea": { 1093 | "Description": null, 1094 | "ImageName": null, 1095 | "World": null, 1096 | "MarketAccessPermitted": false, 1097 | "MoveMessage": null, 1098 | "HideName": false, 1099 | "RandomPostcard": false, 1100 | "MapX": 0, 1101 | "MapY": 0, 1102 | "UnlocksWithQuality": null, 1103 | "ShowOps": false, 1104 | "PremiumSubRequired": false, 1105 | "Name": null, 1106 | "Id": 101956 1107 | }, 1108 | "World": null, 1109 | "Transient": false, 1110 | "Stickiness": 0, 1111 | "MoveToAreaId": 0, 1112 | "MoveToArea": null, 1113 | "MoveToDomicile": null, 1114 | "SwitchToSetting": null, 1115 | "FatePointsChange": 0, 1116 | "BootyValue": 0, 1117 | "LogInJournalAgainstQuality": null, 1118 | "Setting": null, 1119 | "Urgency": "Normal", 1120 | "OwnerName": null, 1121 | "DateTimeCreated": "{{buildDateTime}}", 1122 | "Distribution": 0, 1123 | "Autofire": true, 1124 | "CanGoBack": false, 1125 | "LinkToEvent": null, 1126 | "ChildBranches": [ 1127 | {{{eventSufferChildren}}} 1128 | ] 1129 | }, 1130 | { 1131 | "Id": {{baseGameIds.become}}, 1132 | "Name": "\"I desire to become\"", 1133 | "Teaser": "", 1134 | "Description": "Change is a constant. Every instant brings a new you, and the death of the old. Who will you be tomorrow? And what will you sacrifice to ensure you reach it?", 1135 | "ParentBranch": null, 1136 | "QualitiesAffected": [], 1137 | "QualitiesRequired": [], 1138 | "Image": "keepermoth", 1139 | "Tag": null, 1140 | "ExoticEffects": "", 1141 | "Note": null, 1142 | "ChallengeLevel": 0, 1143 | "UnclearedEditAt": null, 1144 | "LastEditedBy": null, 1145 | "Ordering": 0, 1146 | "ShowAsMessage": false, 1147 | "LivingStory": null, 1148 | "Deck": { 1149 | "World": null, 1150 | "Name": "Always", 1151 | "ImageName": "100x130", 1152 | "Ordering": 1, 1153 | "Description": "Always", 1154 | "Availability": "Always", 1155 | "DrawSize": 3, 1156 | "MaxCards": 3, 1157 | "Id": 8557 1158 | }, 1159 | "Category": "Unspecialised", 1160 | "LimitedToArea": { 1161 | "Description": null, 1162 | "ImageName": null, 1163 | "World": null, 1164 | "MarketAccessPermitted": false, 1165 | "MoveMessage": null, 1166 | "HideName": false, 1167 | "RandomPostcard": false, 1168 | "MapX": 0, 1169 | "MapY": 0, 1170 | "UnlocksWithQuality": null, 1171 | "ShowOps": false, 1172 | "PremiumSubRequired": false, 1173 | "Name": null, 1174 | "Id": 101956 1175 | }, 1176 | "World": null, 1177 | "Transient": false, 1178 | "Stickiness": 0, 1179 | "MoveToAreaId": 0, 1180 | "MoveToArea": null, 1181 | "MoveToDomicile": null, 1182 | "SwitchToSetting": null, 1183 | "FatePointsChange": 0, 1184 | "BootyValue": 0, 1185 | "LogInJournalAgainstQuality": null, 1186 | "Setting": null, 1187 | "Urgency": "Normal", 1188 | "OwnerName": null, 1189 | "DateTimeCreated": "{{buildDateTime}}", 1190 | "Distribution": 0, 1191 | "Autofire": true, 1192 | "CanGoBack": false, 1193 | "LinkToEvent": null, 1194 | "ChildBranches": [ 1195 | {{{eventBecomeChildren}}} 1196 | ] 1197 | }, 1198 | {{{additionalEvents}}} 1199 | ] 1200 | -------------------------------------------------------------------------------- /src/templates/index.html.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{title}} 8 | 9 | 10 | 11 | 12 |
    13 |
    Drop all Sunless Sea JSON files here
    14 | 15 | 19 | 20 | 25 | 26 | 27 |
    28 | 29 |
    30 |
      31 |
    • Files
    • 32 |
    • Events
    • 33 |
    • Qualities
    • 34 |
    • Areas
    • 35 |
    • Spawned Entities
    • 36 |
    • Combat Attacks
    • 37 |
    • Exchanges
    • 38 |
    • Tiles
    • 39 |
    • Query
    • 40 |
    41 |
    42 |
    43 |

    File list

    44 |
    45 |
    46 |
    47 |

    Events

    48 |
    49 |
    50 |
    51 |

    Qualities

    52 |
    53 |
    54 |
    55 |

    Areas

    56 |
    57 |
    58 |
    59 |

    Spawned Entity

    60 |
    61 |
    62 |
    63 |

    Combat Attacks

    64 |
    65 |
    66 |
    67 |

    Exchanges

    68 |
    69 |
    70 |
    71 |

    Tiles

    72 |
    73 |
    74 |
    75 |

    Query

    76 |
    77 |
    78 |
    79 |
    80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/templates/objects/event.handlebars: -------------------------------------------------------------------------------- 1 | { 2 | "Id": {{Id}}, 3 | "Name": "{{{Name}}}", 4 | "Teaser": "", 5 | "Description": "{{{Description}}}", 6 | "ParentBranch": null, 7 | "QualitiesAffected": [], 8 | "QualitiesRequired": [], 9 | "Image": "{{Image}}", 10 | "Tag": null, 11 | "ExoticEffects": "", 12 | "Note": null, 13 | "ChallengeLevel": 0, 14 | "UnclearedEditAt": null, 15 | "LastEditedBy": null, 16 | "Ordering": 0, 17 | "ShowAsMessage": false, 18 | "LivingStory": null, 19 | "Deck": { 20 | "World": null, 21 | "Name": "Always", 22 | "ImageName": "100x130", 23 | "Ordering": 1, 24 | "Description": "Always", 25 | "Availability": "Always", 26 | "DrawSize": 3, 27 | "MaxCards": 3, 28 | "Id": 8557 29 | }, 30 | "Category": "Unspecialised", 31 | "LimitedToArea": { 32 | "Description": null, 33 | "ImageName": null, 34 | "World": null, 35 | "MarketAccessPermitted": false, 36 | "MoveMessage": null, 37 | "HideName": false, 38 | "RandomPostcard": false, 39 | "MapX": 0, 40 | "MapY": 0, 41 | "UnlocksWithQuality": null, 42 | "ShowOps": false, 43 | "PremiumSubRequired": false, 44 | "Name": null, 45 | "Id": 101956 46 | }, 47 | "World": null, 48 | "Transient": false, 49 | "Stickiness": 0, 50 | "MoveToAreaId": 0, 51 | "MoveToArea": null, 52 | "MoveToDomicile": null, 53 | "SwitchToSetting": null, 54 | "FatePointsChange": 0, 55 | "BootyValue": 0, 56 | "LogInJournalAgainstQuality": null, 57 | "Setting": null, 58 | "Urgency": "Normal", 59 | "OwnerName": null, 60 | "DateTimeCreated": "{{buildDateTime}}", 61 | "Distribution": 0, 62 | "Autofire": true, 63 | "CanGoBack": false, 64 | "LinkToEvent": {{{linkToEvent}}}, 65 | "ChildBranches": [ 66 | {{{interactionChildren}}} 67 | ] 68 | } -------------------------------------------------------------------------------- /src/templates/objects/interaction.handlebars: -------------------------------------------------------------------------------- 1 | { 2 | "Id": {{Id}}, 3 | "Name": "{{{Name}}}", 4 | "Description": "{{{Description}}}", 5 | "SuccessEvent": null, 6 | "RareDefaultEvent": null, 7 | "RareDefaultEventChance": 0, 8 | "RareSuccessEvent": null, 9 | "RareSuccessEventChance": 0, 10 | "ParentEvent": null, 11 | "QualitiesRequired": {{{qualityRequirements}}}, 12 | "Image": "{{Image}}", 13 | "OwnerName": null, 14 | "DateTimeCreated": "{{buildDateTime}}", 15 | "CurrencyCost": 0, 16 | "ActionCost": 1, 17 | "RenameQualityCategory": null, 18 | "ButtonText": "Got it", 19 | "Ordering": 0, 20 | "Act": null, 21 | "Archived": false, 22 | "DefaultEvent": {{{defaultEvent}}} 23 | } -------------------------------------------------------------------------------- /src/templates/qualities.json.handlebars: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Id": {{baseGameIds.quality}}, 4 | "Name": "Clockwork Oracle", 5 | "Description": "A ticking, wheezing eldritch abomination. It knows many things, but what will it divulge?", 6 | "AllowsSecondChancesOnChallengesForQuality": null, 7 | "AssignToSlot": null, 8 | "AvailableAt": null, 9 | "Cap": null, 10 | "CssClasses": null, 11 | "Enhancements": [], 12 | "EnhancementsDescription": null, 13 | "GivesTrophy": null, 14 | "HimbleLevel": 0, 15 | "Image": "clock", 16 | "IsSlot": false, 17 | "LimitedToArea": null, 18 | "Notes": "", 19 | "Ordering": 0, 20 | "OwnerName": "Alexis", 21 | "Persistent": false, 22 | "PreventNaming": false, 23 | "PyramidNumberIncreaseLimit": 50, 24 | "QualitiesWhichAllowSecondChanceOnThis": [], 25 | "RelationshipCapable": false, 26 | "Tag": "Goods", 27 | "UsePyramidNumbers": false, 28 | "Visible": true, 29 | "World": null, 30 | "UseEvent": { 31 | "ChildBranches": [], 32 | "ParentBranch": null, 33 | "QualitiesAffected": [], 34 | "QualitiesRequired": [], 35 | "Image": null, 36 | "Description": null, 37 | "Tag": null, 38 | "ExoticEffects": null, 39 | "Note": null, 40 | "ChallengeLevel": 0, 41 | "UnclearedEditAt": null, 42 | "LastEditedBy": null, 43 | "Ordering": 0, 44 | "ShowAsMessage": false, 45 | "LivingStory": null, 46 | "LinkToEvent": null, 47 | "Deck": null, 48 | "Category": "Unspecialised", 49 | "LimitedToArea": null, 50 | "World": null, 51 | "Transient": false, 52 | "Stickiness": 0, 53 | "MoveToAreaId": 0, 54 | "MoveToArea": null, 55 | "MoveToDomicile": null, 56 | "SwitchToSetting": null, 57 | "FatePointsChange": 0, 58 | "BootyValue": 0, 59 | "LogInJournalAgainstQuality": null, 60 | "Setting": null, 61 | "Urgency": "Normal", 62 | "Teaser": null, 63 | "OwnerName": null, 64 | "DateTimeCreated": "{{buildDateTime}}", 65 | "Distribution": 0, 66 | "Autofire": true, 67 | "CanGoBack": false, 68 | "Name": null, 69 | "Id": {{baseGameIds.event}} 70 | }, 71 | "DifficultyTestType": "Broad", 72 | "DifficultyScaler": 60, 73 | "AllowedOn": "Character", 74 | "Nature": "Thing", 75 | "Category": "Goods", 76 | "LevelDescriptionText": null, 77 | "ChangeDescriptionText": null, 78 | "LevelImageText": null 79 | } 80 | ] --------------------------------------------------------------------------------