diff --git a/css/dark.css b/css/dark.css index d9025290..d77de77d 100644 --- a/css/dark.css +++ b/css/dark.css @@ -52,6 +52,14 @@ div#stores { border: 1px solid #EEE; } +div#blueprints:before { + background: #272823; +} + +div#blueprints { + border: 1px solid #EEE; +} + div#weapons:before { background: #272823; } @@ -173,10 +181,15 @@ body.noMask #buttons > .button { color: black; } -.endGame { +.endGame, .outro { color:#272823; } +#wait-btn { + border-color: black; + color: black; +} + #theEnd { color: #272823; } diff --git a/css/fabricator.css b/css/fabricator.css new file mode 100644 index 00000000..f68ac8df --- /dev/null +++ b/css/fabricator.css @@ -0,0 +1,36 @@ +div#fabricateButtons { + position: relative; + top: 5px; + left: 0; +} + +div#fabricateButtons::before { + content: attr(data-legend); + position: relative; + top: -5px; +} + +div#blueprints::before { + content: attr(data-legend); + position: absolute; + top: -13px; + background-color: white; +} + +div#blueprints { + position: absolute; + top: 0px; + right: 237px; + border: 1px solid black; + cursor: default; + padding: 5px 10px; + width: 200px; +} + +div.blueprintRow { + position: relative; +} + +div.blueprintRow .row_key { + float: none; +} diff --git a/css/main.css b/css/main.css index d5b4a0eb..f39e3928 100644 --- a/css/main.css +++ b/css/main.css @@ -46,9 +46,9 @@ div#wrapper { } div#saveNotify { - position: absolute; - top: 20px; - right: 0px; + position: fixed; + top: 10px; + right: 20px; background: white; opacity: 0; } @@ -147,7 +147,7 @@ span.customSelectOptions { } div.headerButton { - font-size: 18px; + font-size: 17px; cursor: pointer; float: left; border-left: 1px solid black; @@ -561,6 +561,53 @@ body.noMask #description { margin-left: -50%; } +.fighter.shield > .label::before { + content: '('; + position: absolute; + left: -8px; +} + +.fighter.shield > .label::after { + content: ')'; + position: absolute; + right: -8px; +} + +.fighter.energised > .label { + font-size: 2em; + font-style: bold; +} + +.fighter.meditation > .label { + font-size: 1.5em; + opacity: 0.3; +} + +@keyframes exploding { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-10px, 0); } + 75% { transform: translate(10px, 0); } + 100% { transform: translate(0, 0); } +} + +.fighter.exploding > .label { + animation: exploding 200ms linear infinite; +} + +.fighter.venomous > .label { + font-size: 1.5em; + font-style: bold; +} + +.fighter.enraged > .label { + font-size: 1.5em; + font-style: italic; +} + +.fighter.boost > .label { + font-style: italic; +} + #description .bullet { padding: 0px 20px 0px 20px; bottom: 25px; diff --git a/css/path.css b/css/path.css index 79187384..2042d27f 100644 --- a/css/path.css +++ b/css/path.css @@ -6,7 +6,7 @@ padding: 5px 10px; } -div#outfitting:before ,div#perks:before { +div#outfitting:before, div#perks:before { content: attr(data-legend); position: absolute; top: -13px; diff --git a/css/space.css b/css/space.css index b0442310..e52658d2 100644 --- a/css/space.css +++ b/css/space.css @@ -137,6 +137,19 @@ padding-top:10%; } +.outroContainer { + padding-top:10%; + width: 800px; + margin: 0 auto; +} + +.outro { + font-size: 1.5rem; + color: #FFF; + opacity: 0; + margin-bottom: 40px; +} + .endGame { font-size:48px; color:#FFFFFF; @@ -152,3 +165,10 @@ .endGameOption:hover { text-decoration: underline; } + +#wait-btn { + border-color: #fff; + color: #fff; + margin: 0 auto; + opacity: 0; +} diff --git a/index.html b/index.html index 471dadff..33708fca 100644 --- a/index.html +++ b/index.html @@ -59,7 +59,8 @@ - + + @@ -69,6 +70,7 @@ + diff --git a/script/Button.js b/script/Button.js index 83aac025..4261caf0 100644 --- a/script/Button.js +++ b/script/Button.js @@ -20,7 +20,8 @@ var Button = { }) .data("handler", typeof options.click == 'function' ? options.click : function() { Engine.log("click"); }) .data("remaining", 0) - .data("cooldown", typeof options.cooldown == 'number' ? options.cooldown : 0); + .data("cooldown", typeof options.cooldown == 'number' ? options.cooldown : 0) + .data('boosted', options.boosted ?? (() => false)); el.append($("
").addClass('cooldown')); @@ -68,6 +69,9 @@ var Button = { cooldown: function(btn, option) { var cd = btn.data("cooldown"); + if (btn.data('boosted')()) { + cd /= 2; + } var id = 'cooldown.'+ btn.attr('id'); if(cd > 0) { if(typeof option == 'number') { diff --git a/script/engine.js b/script/engine.js index 2629c868..e3bf1967 100644 --- a/script/engine.js +++ b/script/engine.js @@ -1,861 +1,865 @@ (function() { - var Engine = window.Engine = { - - SITE_URL: encodeURIComponent("http://adarkroom.doublespeakgames.com"), - VERSION: 1.3, - MAX_STORE: 99999999999999, - SAVE_DISPLAY: 30 * 1000, - GAME_OVER: false, - - //object event types - topics: {}, - - Perks: { - 'boxer': { - name: _('boxer'), - desc: _('punches do more damage'), - /// TRANSLATORS : means with more force. - notify: _('learned to throw punches with purpose') - }, - 'martial artist': { - name: _('martial artist'), - desc: _('punches do even more damage.'), - notify: _('learned to fight quite effectively without weapons') - }, - 'unarmed master': { - /// TRANSLATORS : master of unarmed combat - name: _('unarmed master'), - desc: _('punch twice as fast, and with even more force'), - notify: _('learned to strike faster without weapons') - }, - 'barbarian': { - name: _('barbarian'), - desc: _('melee weapons deal more damage'), - notify: _('learned to swing weapons with force') - }, - 'slow metabolism': { - name: _('slow metabolism'), - desc: _('go twice as far without eating'), - notify: _('learned how to ignore the hunger') - }, - 'desert rat': { - name: _('desert rat'), - desc: _('go twice as far without drinking'), - notify: _('learned to love the dry air') - }, - 'evasive': { - name: _('evasive'), - desc: _('dodge attacks more effectively'), - notify: _("learned to be where they're not") - }, - 'precise': { - name: _('precise'), - desc: _('land blows more often'), - notify: _('learned to predict their movement') - }, - 'scout': { - name: _('scout'), - desc: _('see farther'), - notify: _('learned to look ahead') - }, - 'stealthy': { - name: _('stealthy'), - desc: _('better avoid conflict in the wild'), - notify: _('learned how not to be seen') - }, - 'gastronome': { - name: _('gastronome'), - desc: _('restore more health when eating'), - notify: _('learned to make the most of food') - } - }, - - options: { - state: null, - debug: false, - log: false, - dropbox: false, - doubleTime: true - }, - - init: function(options) { - this.options = $.extend( - this.options, - options - ); - this._debug = this.options.debug; - this._log = this.options.log; - - // Check for HTML5 support - if(!Engine.browserValid()) { - window.location = 'browserWarning.html'; - } - - // Check for mobile - if(Engine.isMobile()) { - window.location = 'mobileWarning.html'; - } - - Engine.disableSelection(); - - if(this.options.state != null) { - window.State = this.options.state; - } else { - Engine.loadGame(); - } - - // start loading music and events early - for (var key in AudioLibrary) { - if ( - key.toString().indexOf('MUSIC_') > -1 || - key.toString().indexOf('EVENT_') > -1) { - AudioEngine.loadAudioFile(AudioLibrary[key]); - } - } - - $('
').attr('id', 'locationSlider').appendTo('#main'); - - var menu = $('
') - .addClass('menu') - .appendTo('body'); - - if(typeof langs != 'undefined'){ - var customSelect = $('') - .addClass('customSelect') - .addClass('menuBtn') - .appendTo(menu); - var selectOptions = $('') - .addClass('customSelectOptions') - .appendTo(customSelect); - var optionsList = $('
    ') - .appendTo(selectOptions); - $('
  • ') - .text("language.") - .appendTo(optionsList); - - $.each(langs, function(name,display){ - $('
  • ') - .text(display) - .attr('data-language', name) - .on("click", function() { Engine.switchLanguage(this); }) - .appendTo(optionsList); - }); - } - - $('') - .addClass('volume menuBtn') - .text(_('sound on.')) - .click(() => Engine.toggleVolume()) - .appendTo(menu); - - $('') - .addClass('appStore menuBtn') - .text(_('get the app.')) - .click(Engine.getApp) - .appendTo(menu); - - $('') - .addClass('lightsOff menuBtn') - .text(_('lights off.')) - .click(Engine.turnLightsOff) - .appendTo(menu); - - $('') - .addClass('hyper menuBtn') - .text(_('hyper.')) - .click(Engine.confirmHyperMode) - .appendTo(menu); - - $('') - .addClass('menuBtn') - .text(_('restart.')) - .click(Engine.confirmDelete) - .appendTo(menu); - - $('') - .addClass('menuBtn') - .text(_('share.')) - .click(Engine.share) - .appendTo(menu); - - $('') - .addClass('menuBtn') - .text(_('save.')) - .click(Engine.exportImport) - .appendTo(menu); - - if(this.options.dropbox && Engine.Dropbox) { - this.dropbox = Engine.Dropbox.init(); - - $('') - .addClass('menuBtn') - .text(_('dropbox.')) - .click(Engine.Dropbox.startDropbox) - .appendTo(menu); - } - - $('') - .addClass('menuBtn') - .text(_('github.')) - .click(function() { window.open('https://github.com/doublespeakgames/adarkroom'); }) - .appendTo(menu); - - // Register keypress handlers - $('body').off('keydown').keydown(Engine.keyDown); - $('body').off('keyup').keyup(Engine.keyUp); - - // Register swipe handlers - swipeElement = $('#outerSlider'); - swipeElement.on('swipeleft', Engine.swipeLeft); - swipeElement.on('swiperight', Engine.swipeRight); - swipeElement.on('swipeup', Engine.swipeUp); - swipeElement.on('swipedown', Engine.swipeDown); - - // subscribe to stateUpdates - $.Dispatch('stateUpdate').subscribe(Engine.handleStateUpdates); - - $SM.init(); - AudioEngine.init(); - Notifications.init(); - Events.init(); - Room.init(); - - - if(typeof $SM.get('stores.wood') != 'undefined') { - Outside.init(); - } - if($SM.get('stores.compass', true) > 0) { - Path.init(); - } - if($SM.get('features.location.spaceShip')) { - Ship.init(); - } - - if($SM.get('config.lightsOff', true)){ - Engine.turnLightsOff(); - } - - if($SM.get('config.hyperMode', true)){ - Engine.triggerHyperMode(); - } - - Engine.toggleVolume(Boolean($SM.get('config.soundOn'))); - if(!AudioEngine.isAudioContextRunning()){ - document.addEventListener('click', Engine.resumeAudioContext, true); - } - - Engine.saveLanguage(); - Engine.travelTo(Room); + var Engine = window.Engine = { + + SITE_URL: encodeURIComponent("http://adarkroom.doublespeakgames.com"), + VERSION: 1.3, + MAX_STORE: 99999999999999, + SAVE_DISPLAY: 30 * 1000, + GAME_OVER: false, + + //object event types + topics: {}, + + Perks: { + 'boxer': { + name: _('boxer'), + desc: _('punches do more damage'), + /// TRANSLATORS : means with more force. + notify: _('learned to throw punches with purpose') + }, + 'martial artist': { + name: _('martial artist'), + desc: _('punches do even more damage.'), + notify: _('learned to fight quite effectively without weapons') + }, + 'unarmed master': { + /// TRANSLATORS : master of unarmed combat + name: _('unarmed master'), + desc: _('punch twice as fast, and with even more force'), + notify: _('learned to strike faster without weapons') + }, + 'barbarian': { + name: _('barbarian'), + desc: _('melee weapons deal more damage'), + notify: _('learned to swing weapons with force') + }, + 'slow metabolism': { + name: _('slow metabolism'), + desc: _('go twice as far without eating'), + notify: _('learned how to ignore the hunger') + }, + 'desert rat': { + name: _('desert rat'), + desc: _('go twice as far without drinking'), + notify: _('learned to love the dry air') + }, + 'evasive': { + name: _('evasive'), + desc: _('dodge attacks more effectively'), + notify: _("learned to be where they're not") + }, + 'precise': { + name: _('precise'), + desc: _('land blows more often'), + notify: _('learned to predict their movement') + }, + 'scout': { + name: _('scout'), + desc: _('see farther'), + notify: _('learned to look ahead') + }, + 'stealthy': { + name: _('stealthy'), + desc: _('better avoid conflict in the wild'), + notify: _('learned how not to be seen') + }, + 'gastronome': { + name: _('gastronome'), + desc: _('restore more health when eating'), + notify: _('learned to make the most of food') + } + }, + + options: { + state: null, + debug: false, + log: false, + dropbox: false, + doubleTime: false + }, + + init: function(options) { + this.options = $.extend( + this.options, + options + ); + this._debug = this.options.debug; + this._log = this.options.log; + + // Check for HTML5 support + if(!Engine.browserValid()) { + window.location = 'browserWarning.html'; + } + + // Check for mobile + if(Engine.isMobile()) { + window.location = 'mobileWarning.html'; + } + + Engine.disableSelection(); + + if(this.options.state != null) { + window.State = this.options.state; + } else { + Engine.loadGame(); + } + + // start loading music and events early + for (var key in AudioLibrary) { + if ( + key.toString().indexOf('MUSIC_') > -1 || + key.toString().indexOf('EVENT_') > -1) { + AudioEngine.loadAudioFile(AudioLibrary[key]); + } + } + + $('
    ').attr('id', 'locationSlider').appendTo('#main'); + + var menu = $('
    ') + .addClass('menu') + .appendTo('body'); + + if(typeof langs != 'undefined'){ + var customSelect = $('') + .addClass('customSelect') + .addClass('menuBtn') + .appendTo(menu); + var selectOptions = $('') + .addClass('customSelectOptions') + .appendTo(customSelect); + var optionsList = $('
      ') + .appendTo(selectOptions); + $('
    • ') + .text("language.") + .appendTo(optionsList); + + $.each(langs, function(name,display){ + $('
    • ') + .text(display) + .attr('data-language', name) + .on("click", function() { Engine.switchLanguage(this); }) + .appendTo(optionsList); + }); + } + + $('') + .addClass('volume menuBtn') + .text(_('sound on.')) + .click(() => Engine.toggleVolume()) + .appendTo(menu); + + $('') + .addClass('appStore menuBtn') + .text(_('get the app.')) + .click(Engine.getApp) + .appendTo(menu); + + $('') + .addClass('lightsOff menuBtn') + .text(_('lights off.')) + .click(Engine.turnLightsOff) + .appendTo(menu); + + $('') + .addClass('hyper menuBtn') + .text(_('hyper.')) + .click(Engine.confirmHyperMode) + .appendTo(menu); + + $('') + .addClass('menuBtn') + .text(_('restart.')) + .click(Engine.confirmDelete) + .appendTo(menu); + + $('') + .addClass('menuBtn') + .text(_('share.')) + .click(Engine.share) + .appendTo(menu); + + $('') + .addClass('menuBtn') + .text(_('save.')) + .click(Engine.exportImport) + .appendTo(menu); + + if(this.options.dropbox && Engine.Dropbox) { + this.dropbox = Engine.Dropbox.init(); + + $('') + .addClass('menuBtn') + .text(_('dropbox.')) + .click(Engine.Dropbox.startDropbox) + .appendTo(menu); + } + + $('') + .addClass('menuBtn') + .text(_('github.')) + .click(function() { window.open('https://github.com/doublespeakgames/adarkroom'); }) + .appendTo(menu); + + // Register keypress handlers + $('body').off('keydown').keydown(Engine.keyDown); + $('body').off('keyup').keyup(Engine.keyUp); + + // Register swipe handlers + swipeElement = $('#outerSlider'); + swipeElement.on('swipeleft', Engine.swipeLeft); + swipeElement.on('swiperight', Engine.swipeRight); + swipeElement.on('swipeup', Engine.swipeUp); + swipeElement.on('swipedown', Engine.swipeDown); + + // subscribe to stateUpdates + $.Dispatch('stateUpdate').subscribe(Engine.handleStateUpdates); + + $SM.init(); + AudioEngine.init(); + Notifications.init(); + Events.init(); + Room.init(); + + + if(typeof $SM.get('stores.wood') != 'undefined') { + Outside.init(); + } + if($SM.get('stores.compass', true) > 0) { + Path.init(); + } + if ($SM.get('features.location.fabricator')) { + Fabricator.init(); + } + if($SM.get('features.location.spaceShip')) { + Ship.init(); + } + + if($SM.get('config.lightsOff', true)){ + Engine.turnLightsOff(); + } + + if($SM.get('config.hyperMode', true)){ + Engine.triggerHyperMode(); + } + + Engine.toggleVolume(Boolean($SM.get('config.soundOn'))); + if(!AudioEngine.isAudioContextRunning()){ + document.addEventListener('click', Engine.resumeAudioContext, true); + } + + Engine.saveLanguage(); + Engine.travelTo(Room); setTimeout(notifyAboutSound, 3000); - }, - resumeAudioContext: function () { - AudioEngine.tryResumingAudioContext(); - - // turn on music! - AudioEngine.setMasterVolume($SM.get('config.soundOn') ? 1.0 : 0.0, 0); - - document.removeEventListener('click', Engine.resumeAudioContext); - }, - browserValid: function() { - return ( location.search.indexOf( 'ignorebrowser=true' ) >= 0 || ( typeof Storage != 'undefined' && !oldIE ) ); - }, - - isMobile: function() { - return ( location.search.indexOf( 'ignorebrowser=true' ) < 0 && /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( navigator.userAgent ) ); - }, - - saveGame: function() { - if(typeof Storage != 'undefined' && localStorage) { - if(Engine._saveTimer != null) { - clearTimeout(Engine._saveTimer); - } - if(typeof Engine._lastNotify == 'undefined' || Date.now() - Engine._lastNotify > Engine.SAVE_DISPLAY){ - $('#saveNotify').css('opacity', 1).animate({opacity: 0}, 1000, 'linear'); - Engine._lastNotify = Date.now(); - } - localStorage.gameState = JSON.stringify(State); - } - }, - - loadGame: function() { - try { - var savedState = JSON.parse(localStorage.gameState); - if(savedState) { - State = savedState; - $SM.updateOldState(); - Engine.log("loaded save!"); - } - } catch(e) { - State = {}; - $SM.set('version', Engine.VERSION); - Engine.event('progress', 'new game'); - } - }, - - exportImport: function() { - Events.startEvent({ - title: _('Export / Import'), - scenes: { - start: { - text: [ - _('export or import save data, for backing up'), - _('or migrating computers') - ], - buttons: { - 'export': { - text: _('export'), - nextScene: {1: 'inputExport'} - }, - 'import': { - text: _('import'), - nextScene: {1: 'confirm'} - }, - 'cancel': { - text: _('cancel'), - nextScene: 'end' - } - } - }, - 'inputExport': { - text: [_('save this.')], - textarea: Engine.export64(), - onLoad: function() { Engine.event('progress', 'export'); }, - readonly: true, - buttons: { - 'done': { - text: _('got it'), - nextScene: 'end', - onChoose: Engine.disableSelection - } - } - }, - 'confirm': { - text: [ - _('are you sure?'), - _('if the code is invalid, all data will be lost.'), - _('this is irreversible.') - ], - buttons: { - 'yes': { - text: _('yes'), - nextScene: {1: 'inputImport'}, - onChoose: Engine.enableSelection - }, - 'no': { - text: _('no'), - nextScene: {1: 'start'} - } - } - }, - 'inputImport': { - text: [_('put the save code here.')], - textarea: '', - buttons: { - 'okay': { - text: _('import'), - nextScene: 'end', - onChoose: Engine.import64 - }, - 'cancel': { - text: _('cancel'), - nextScene: 'end' - } - } - } - } - }); - }, - - generateExport64: function(){ - var string64 = Base64.encode(localStorage.gameState); - string64 = string64.replace(/\s/g, ''); - string64 = string64.replace(/\./g, ''); - string64 = string64.replace(/\n/g, ''); - - return string64; - }, - - export64: function() { - Engine.saveGame(); - Engine.enableSelection(); - return Engine.generateExport64(); - }, - - import64: function(string64) { - Engine.event('progress', 'import'); - Engine.disableSelection(); - string64 = string64.replace(/\s/g, ''); - string64 = string64.replace(/\./g, ''); - string64 = string64.replace(/\n/g, ''); - var decodedSave = Base64.decode(string64); - localStorage.gameState = decodedSave; - location.reload(); - }, - - event: function(cat, act) { - if(typeof ga === 'function') { - ga('send', 'event', cat, act); - } - }, - - confirmDelete: function() { - Events.startEvent({ - title: _('Restart?'), - scenes: { - start: { - text: [_('restart the game?')], - buttons: { - 'yes': { - text: _('yes'), - nextScene: 'end', - onChoose: Engine.deleteSave - }, - 'no': { - text: _('no'), - nextScene: 'end' - } - } - } - } - }); - }, - - deleteSave: function(noReload) { - if(typeof Storage != 'undefined' && localStorage) { - var prestige = Prestige.get(); - window.State = {}; - localStorage.clear(); - Prestige.set(prestige); - } - if(!noReload) { - location.reload(); - } - }, - - getApp: function() { - Events.startEvent({ - title: _('Get the App'), - scenes: { - start: { - text: [_('bring the room with you.')], - buttons: { - 'ios': { - text: _('ios'), - nextScene: 'end', - onChoose: function () { - window.open('https://itunes.apple.com/app/apple-store/id736683061?pt=2073437&ct=adrproper&mt=8'); - } - }, - 'android': { - text: _('android'), - nextScene: 'end', - onChoose: function() { - window.open('https://play.google.com/store/apps/details?id=com.yourcompany.adarkroom'); - } - }, - 'close': { - text: _('close'), - nextScene: 'end' - } - } - } - } - }); - }, - - share: function() { - Events.startEvent({ - title: _('Share'), - scenes: { - start: { - text: [_('bring your friends.')], - buttons: { - 'facebook': { - text: _('facebook'), - nextScene: 'end', - onChoose: function() { - window.open('https://www.facebook.com/sharer/sharer.php?u=' + Engine.SITE_URL, 'sharer', 'width=626,height=436,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); - } - }, - 'google': { - text:_('google+'), - nextScene: 'end', - onChoose: function() { - window.open('https://plus.google.com/share?url=' + Engine.SITE_URL, 'sharer', 'width=480,height=436,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); - } - }, - 'twitter': { - text: _('twitter'), - nextScene: 'end', - onChoose: function() { - window.open('https://twitter.com/intent/tweet?text=A%20Dark%20Room&url=' + Engine.SITE_URL, 'sharer', 'width=660,height=260,location=no,menubar=no,resizable=no,scrollbars=yes,status=no,toolbar=no'); - } - }, - 'reddit': { - text: _('reddit'), - nextScene: 'end', - onChoose: function() { - window.open('http://www.reddit.com/submit?url=' + Engine.SITE_URL, 'sharer', 'width=960,height=700,location=no,menubar=no,resizable=no,scrollbars=yes,status=no,toolbar=no'); - } - }, - 'close': { - text: _('close'), - nextScene: 'end' - } - } - } - } - }, - { - width: '400px' - }); - }, - - findStylesheet: function(title) { - for(var i=0; i'); - $('.lightsOff').text(_('lights on.')); - $SM.set('config.lightsOff', true, true); - } else if (darkCss.disabled) { - darkCss.disabled = false; - $('.lightsOff').text(_('lights on.')); - $SM.set('config.lightsOff', true,true); - } else { - $("#darkenLights").attr("disabled", "disabled"); - darkCss.disabled = true; - $('.lightsOff').text(_('lights off.')); - $SM.set('config.lightsOff', false, true); - } - }, - - confirmHyperMode: function(){ - if (!Engine.options.doubleTime) { - Events.startEvent({ - title: _('Go Hyper?'), - scenes: { - start: { - text: [_('turning hyper mode speeds up the game to x2 speed. do you want to do that?')], - buttons: { - 'yes': { - text: _('yes'), - nextScene: 'end', - onChoose: Engine.triggerHyperMode - }, - 'no': { - text: _('no'), - nextScene: 'end' - } - } - } - } - }); - } else { - Engine.triggerHyperMode(); - } - }, - - triggerHyperMode: function() { - Engine.options.doubleTime = !Engine.options.doubleTime; - if(Engine.options.doubleTime) - $('.hyper').text(_('classic.')); - else - $('.hyper').text(_('hyper.')); - - $SM.set('config.hyperMode', Engine.options.doubleTime, false); - }, - - // Gets a guid - getGuid: function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }, - - activeModule: null, - - travelTo: function(module) { - if(Engine.activeModule != module) { - var currentIndex = Engine.activeModule ? $('.location').index(Engine.activeModule.panel) : 1; - $('div.headerButton').removeClass('selected'); - module.tab.addClass('selected'); - - var slider = $('#locationSlider'); - var stores = $('#storesContainer'); - var panelIndex = $('.location').index(module.panel); - var diff = Math.abs(panelIndex - currentIndex); - slider.animate({left: -(panelIndex * 700) + 'px'}, 300 * diff); - - if($SM.get('stores.wood') !== undefined) { - // FIXME Why does this work if there's an animation queue...? - stores.animate({right: -(panelIndex * 700) + 'px'}, 300 * diff); - } - - if(Engine.activeModule == Room || Engine.activeModule == Path) { - // Don't fade out the weapons if we're switching to a module - // where we're going to keep showing them anyway. - if (module != Room && module != Path) { - $('div#weapons').animate({opacity: 0}, 300); - } - } - - if(module == Room || module == Path) { - $('div#weapons').animate({opacity: 1}, 300); - } - - Engine.activeModule = module; - module.onArrival(diff); - Notifications.printQueue(module); - } - }, - - /* Move the stores panel beneath top_container (or to top: 0px if top_container - * either hasn't been filled in or is null) using transition_diff to sync with - * the animation in Engine.travelTo(). - */ - moveStoresView: function(top_container, transition_diff) { - var stores = $('#storesContainer'); - - // If we don't have a storesContainer yet, leave. - if(typeof(stores) === 'undefined') return; - - if(typeof(transition_diff) === 'undefined') transition_diff = 1; - - if(top_container === null) { - stores.animate({top: '0px'}, {queue: false, duration: 300 * transition_diff}); - } - else if(!top_container.length) { - stores.animate({top: '0px'}, {queue: false, duration: 300 * transition_diff}); - } - else { - stores.animate({ - top: top_container.height() + 26 + 'px' - }, - { - queue: false, - duration: 300 * transition_diff - }); - } - }, - - log: function(msg) { - if(this._log) { - console.log(msg); - } - }, - - updateSlider: function() { - var slider = $('#locationSlider'); - slider.width((slider.children().length * 700) + 'px'); - }, - - updateOuterSlider: function() { - var slider = $('#outerSlider'); - slider.width((slider.children().length * 700) + 'px'); - }, - - getIncomeMsg: function(num, delay) { - return _("{0} per {1}s", (num > 0 ? "+" : "") + num, delay); - //return (num > 0 ? "+" : "") + num + " per " + delay + "s"; - }, - - keyLock: false, - tabNavigation: true, - restoreNavigation: false, - - keyDown: function(e) { - e = e || window.event; - if(!Engine.keyPressed && !Engine.keyLock) { - Engine.pressed = true; - if(Engine.activeModule.keyDown) { - Engine.activeModule.keyDown(e); - } - } - return jQuery.inArray(e.keycode, [37,38,39,40]) < 0; - }, - - keyUp: function(e) { - Engine.pressed = false; - if(Engine.activeModule.keyUp) { - Engine.activeModule.keyUp(e); - } else { - switch(e.which) { - case 38: // Up - case 87: - if(Engine.activeModule == Outside || Engine.activeModule == Path) { - Engine.activeModule.scrollSidebar('up'); - } - Engine.log('up'); - break; - case 40: // Down - case 83: - if (Engine.activeModule == Outside || Engine.activeModule == Path) { - Engine.activeModule.scrollSidebar('down'); - } - Engine.log('down'); - break; - case 37: // Left - case 65: - if(Engine.tabNavigation){ - if(Engine.activeModule == Ship && Path.tab) - Engine.travelTo(Path); - else if(Engine.activeModule == Path && Outside.tab){ - Engine.activeModule.scrollSidebar('left', true); - Engine.travelTo(Outside); - }else if(Engine.activeModule == Outside && Room.tab){ - Engine.activeModule.scrollSidebar('left', true); - Engine.travelTo(Room); - } - } - Engine.log('left'); - break; - case 39: // Right - case 68: - if(Engine.tabNavigation){ - if(Engine.activeModule == Room && Outside.tab) - Engine.travelTo(Outside); - else if(Engine.activeModule == Outside && Path.tab){ - Engine.activeModule.scrollSidebar('right', true); - Engine.travelTo(Path); - }else if(Engine.activeModule == Path && Ship.tab){ - Engine.activeModule.scrollSidebar('right', true); - Engine.travelTo(Ship); - } - } - Engine.log('right'); - break; - } - } - if(Engine.restoreNavigation){ - Engine.tabNavigation = true; - Engine.restoreNavigation = false; - } - return false; - }, - - swipeLeft: function(e) { - if(Engine.activeModule.swipeLeft) { - Engine.activeModule.swipeLeft(e); - } - }, - - swipeRight: function(e) { - if(Engine.activeModule.swipeRight) { - Engine.activeModule.swipeRight(e); - } - }, - - swipeUp: function(e) { - if(Engine.activeModule.swipeUp) { - Engine.activeModule.swipeUp(e); - } - }, - - swipeDown: function(e) { - if(Engine.activeModule.swipeDown) { - Engine.activeModule.swipeDown(e); - } - }, - - disableSelection: function() { - document.onselectstart = eventNullifier; // this is for IE - document.onmousedown = eventNullifier; // this is for the rest - }, - - enableSelection: function() { - document.onselectstart = eventPassthrough; - document.onmousedown = eventPassthrough; - }, - - autoSelect: function(selector) { - $(selector).focus().select(); - }, - - handleStateUpdates: function(e){ - - }, - - switchLanguage: function(dom){ - var lang = $(dom).data("language"); - if(document.location.href.search(/[\?\&]lang=[a-z_]+/) != -1){ - document.location.href = document.location.href.replace( /([\?\&]lang=)([a-z_]+)/gi , "$1"+lang ); - }else{ - document.location.href = document.location.href + ( (document.location.href.search(/\?/) != -1 )?"&":"?") + "lang="+lang; - } - }, - - saveLanguage: function(){ - var lang = decodeURIComponent((new RegExp('[?|&]lang=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null; - if(lang && typeof Storage != 'undefined' && localStorage) { - localStorage.lang = lang; - } - }, - - toggleVolume: function(enabled /* optional */) { + }, + resumeAudioContext: function () { + AudioEngine.tryResumingAudioContext(); + + // turn on music! + AudioEngine.setMasterVolume($SM.get('config.soundOn') ? 1.0 : 0.0, 0); + + document.removeEventListener('click', Engine.resumeAudioContext); + }, + browserValid: function() { + return ( location.search.indexOf( 'ignorebrowser=true' ) >= 0 || ( typeof Storage != 'undefined' && !oldIE ) ); + }, + + isMobile: function() { + return ( location.search.indexOf( 'ignorebrowser=true' ) < 0 && /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( navigator.userAgent ) ); + }, + + saveGame: function() { + if(typeof Storage != 'undefined' && localStorage) { + if(Engine._saveTimer != null) { + clearTimeout(Engine._saveTimer); + } + if(typeof Engine._lastNotify == 'undefined' || Date.now() - Engine._lastNotify > Engine.SAVE_DISPLAY){ + $('#saveNotify').css('opacity', 1).animate({opacity: 0}, 1000, 'linear'); + Engine._lastNotify = Date.now(); + } + localStorage.gameState = JSON.stringify(State); + } + }, + + loadGame: function() { + try { + var savedState = JSON.parse(localStorage.gameState); + if(savedState) { + State = savedState; + $SM.updateOldState(); + Engine.log("loaded save!"); + } + } catch(e) { + State = {}; + $SM.set('version', Engine.VERSION); + Engine.event('progress', 'new game'); + } + }, + + exportImport: function() { + Events.startEvent({ + title: _('Export / Import'), + scenes: { + start: { + text: [ + _('export or import save data, for backing up'), + _('or migrating computers') + ], + buttons: { + 'export': { + text: _('export'), + nextScene: {1: 'inputExport'} + }, + 'import': { + text: _('import'), + nextScene: {1: 'confirm'} + }, + 'cancel': { + text: _('cancel'), + nextScene: 'end' + } + } + }, + 'inputExport': { + text: [_('save this.')], + textarea: Engine.export64(), + onLoad: function() { Engine.event('progress', 'export'); }, + readonly: true, + buttons: { + 'done': { + text: _('got it'), + nextScene: 'end', + onChoose: Engine.disableSelection + } + } + }, + 'confirm': { + text: [ + _('are you sure?'), + _('if the code is invalid, all data will be lost.'), + _('this is irreversible.') + ], + buttons: { + 'yes': { + text: _('yes'), + nextScene: {1: 'inputImport'}, + onChoose: Engine.enableSelection + }, + 'no': { + text: _('no'), + nextScene: {1: 'start'} + } + } + }, + 'inputImport': { + text: [_('put the save code here.')], + textarea: '', + buttons: { + 'okay': { + text: _('import'), + nextScene: 'end', + onChoose: Engine.import64 + }, + 'cancel': { + text: _('cancel'), + nextScene: 'end' + } + } + } + } + }); + }, + + generateExport64: function(){ + var string64 = Base64.encode(localStorage.gameState); + string64 = string64.replace(/\s/g, ''); + string64 = string64.replace(/\./g, ''); + string64 = string64.replace(/\n/g, ''); + + return string64; + }, + + export64: function() { + Engine.saveGame(); + Engine.enableSelection(); + return Engine.generateExport64(); + }, + + import64: function(string64) { + Engine.event('progress', 'import'); + Engine.disableSelection(); + string64 = string64.replace(/\s/g, ''); + string64 = string64.replace(/\./g, ''); + string64 = string64.replace(/\n/g, ''); + var decodedSave = Base64.decode(string64); + localStorage.gameState = decodedSave; + location.reload(); + }, + + event: function(cat, act) { + if(typeof ga === 'function') { + ga('send', 'event', cat, act); + } + }, + + confirmDelete: function() { + Events.startEvent({ + title: _('Restart?'), + scenes: { + start: { + text: [_('restart the game?')], + buttons: { + 'yes': { + text: _('yes'), + nextScene: 'end', + onChoose: Engine.deleteSave + }, + 'no': { + text: _('no'), + nextScene: 'end' + } + } + } + } + }); + }, + + deleteSave: function(noReload) { + if(typeof Storage != 'undefined' && localStorage) { + var prestige = Prestige.get(); + window.State = {}; + localStorage.clear(); + Prestige.set(prestige); + } + if(!noReload) { + location.reload(); + } + }, + + getApp: function() { + Events.startEvent({ + title: _('Get the App'), + scenes: { + start: { + text: [_('bring the room with you.')], + buttons: { + 'ios': { + text: _('ios'), + nextScene: 'end', + onChoose: function () { + window.open('https://itunes.apple.com/app/apple-store/id736683061?pt=2073437&ct=adrproper&mt=8'); + } + }, + 'android': { + text: _('android'), + nextScene: 'end', + onChoose: function() { + window.open('https://play.google.com/store/apps/details?id=com.yourcompany.adarkroom'); + } + }, + 'close': { + text: _('close'), + nextScene: 'end' + } + } + } + } + }); + }, + + share: function() { + Events.startEvent({ + title: _('Share'), + scenes: { + start: { + text: [_('bring your friends.')], + buttons: { + 'facebook': { + text: _('facebook'), + nextScene: 'end', + onChoose: function() { + window.open('https://www.facebook.com/sharer/sharer.php?u=' + Engine.SITE_URL, 'sharer', 'width=626,height=436,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); + } + }, + 'google': { + text:_('google+'), + nextScene: 'end', + onChoose: function() { + window.open('https://plus.google.com/share?url=' + Engine.SITE_URL, 'sharer', 'width=480,height=436,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'); + } + }, + 'twitter': { + text: _('twitter'), + nextScene: 'end', + onChoose: function() { + window.open('https://twitter.com/intent/tweet?text=A%20Dark%20Room&url=' + Engine.SITE_URL, 'sharer', 'width=660,height=260,location=no,menubar=no,resizable=no,scrollbars=yes,status=no,toolbar=no'); + } + }, + 'reddit': { + text: _('reddit'), + nextScene: 'end', + onChoose: function() { + window.open('http://www.reddit.com/submit?url=' + Engine.SITE_URL, 'sharer', 'width=960,height=700,location=no,menubar=no,resizable=no,scrollbars=yes,status=no,toolbar=no'); + } + }, + 'close': { + text: _('close'), + nextScene: 'end' + } + } + } + } + }, + { + width: '400px' + }); + }, + + findStylesheet: function(title) { + for(var i=0; i'); + $('.lightsOff').text(_('lights on.')); + $SM.set('config.lightsOff', true, true); + } else if (darkCss.disabled) { + darkCss.disabled = false; + $('.lightsOff').text(_('lights on.')); + $SM.set('config.lightsOff', true,true); + } else { + $("#darkenLights").attr("disabled", "disabled"); + darkCss.disabled = true; + $('.lightsOff').text(_('lights off.')); + $SM.set('config.lightsOff', false, true); + } + }, + + confirmHyperMode: function(){ + if (!Engine.options.doubleTime) { + Events.startEvent({ + title: _('Go Hyper?'), + scenes: { + start: { + text: [_('turning hyper mode speeds up the game to x2 speed. do you want to do that?')], + buttons: { + 'yes': { + text: _('yes'), + nextScene: 'end', + onChoose: Engine.triggerHyperMode + }, + 'no': { + text: _('no'), + nextScene: 'end' + } + } + } + } + }); + } else { + Engine.triggerHyperMode(); + } + }, + + triggerHyperMode: function() { + Engine.options.doubleTime = !Engine.options.doubleTime; + if(Engine.options.doubleTime) + $('.hyper').text(_('classic.')); + else + $('.hyper').text(_('hyper.')); + + $SM.set('config.hyperMode', Engine.options.doubleTime, false); + }, + + // Gets a guid + getGuid: function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + + activeModule: null, + + travelTo: function(module) { + if(Engine.activeModule == module) { + return; + } + + var currentIndex = Engine.activeModule ? $('.location').index(Engine.activeModule.panel) : 1; + $('div.headerButton').removeClass('selected'); + module.tab.addClass('selected'); + + var slider = $('#locationSlider'); + var stores = $('#storesContainer'); + var panelIndex = $('.location').index(module.panel); + var diff = Math.abs(panelIndex - currentIndex); + slider.animate({left: -(panelIndex * 700) + 'px'}, 300 * diff); + + if($SM.get('stores.wood') !== undefined) { + // FIXME Why does this work if there's an animation queue...? + stores.animate({right: -(panelIndex * 700) + 'px'}, 300 * diff); + } + + if(Engine.activeModule == Room || Engine.activeModule == Path || Engine.activeModule == Fabricator) { + // Don't fade out the weapons if we're switching to a module + // where we're going to keep showing them anyway. + if (module != Room && module != Path && module != Fabricator) { + $('div#weapons').animate({opacity: 0}, 300); + } + } + + if(module == Room || module == Path || module == Fabricator) { + $('div#weapons').animate({opacity: 1}, 300); + } + + Engine.activeModule = module; + module.onArrival(diff); + Notifications.printQueue(module); + }, + + /* Move the stores panel beneath top_container (or to top: 0px if top_container + * either hasn't been filled in or is null) using transition_diff to sync with + * the animation in Engine.travelTo(). + */ + moveStoresView: function(top_container, transition_diff) { + var stores = $('#storesContainer'); + + // If we don't have a storesContainer yet, leave. + if(typeof(stores) === 'undefined') return; + + if(typeof(transition_diff) === 'undefined') transition_diff = 1; + + if(top_container === null) { + stores.animate({top: '0px'}, {queue: false, duration: 300 * transition_diff}); + } + else if(!top_container.length) { + stores.animate({top: '0px'}, {queue: false, duration: 300 * transition_diff}); + } + else { + stores.animate({ + top: top_container.height() + 26 + 'px' + }, { + queue: false, + duration: 300 * transition_diff + }); + } + }, + + log: function(msg) { + if(this._log) { + console.log(msg); + } + }, + + updateSlider: function() { + var slider = $('#locationSlider'); + slider.width((slider.children().length * 700) + 'px'); + }, + + updateOuterSlider: function() { + var slider = $('#outerSlider'); + slider.width((slider.children().length * 700) + 'px'); + }, + + getIncomeMsg: function(num, delay) { + return _("{0} per {1}s", (num > 0 ? "+" : "") + num, delay); + //return (num > 0 ? "+" : "") + num + " per " + delay + "s"; + }, + + keyLock: false, + tabNavigation: true, + restoreNavigation: false, + + keyDown: function(e) { + e = e || window.event; + if(!Engine.keyPressed && !Engine.keyLock) { + Engine.pressed = true; + if(Engine.activeModule.keyDown) { + Engine.activeModule.keyDown(e); + } + } + return jQuery.inArray(e.keycode, [37,38,39,40]) < 0; + }, + + keyUp: function(e) { + Engine.pressed = false; + if(Engine.activeModule.keyUp) { + Engine.activeModule.keyUp(e); + } else { + switch(e.which) { + case 38: // Up + case 87: + Engine.log('up'); + break; + case 40: // Down + case 83: + Engine.log('down'); + break; + case 37: // Left + case 65: + if (Engine.tabNavigation) { + if (Engine.activeModule == Ship && Fabricator.tab) { + Engine.travelTo(Fabricator); + } + else if ((Engine.activeModule == Ship || Engine.activeModule == Fabricator) && Path.tab) { + Engine.travelTo(Path); + } + else if (Engine.activeModule == Path && Outside.tab) { + Engine.travelTo(Outside); + } + else if (Engine.activeModule == Outside && Room.tab) { + Engine.travelTo(Room); + } + } + Engine.log('left'); + break; + case 39: // Right + case 68: + if (Engine.tabNavigation){ + if (Engine.activeModule == Room && Outside.tab) { + Engine.travelTo(Outside); + } + else if (Engine.activeModule == Outside && Path.tab){ + Engine.travelTo(Path); + } + else if(Engine.activeModule == Path && Fabricator.tab) { + Engine.travelTo(Fabricator); + } + else if ((Engine.activeModule == Path || Engine.activeModule == Fabricator) && Ship.tab){ + Engine.travelTo(Ship); + } + } + Engine.log('right'); + break; + } + } + if(Engine.restoreNavigation){ + Engine.tabNavigation = true; + Engine.restoreNavigation = false; + } + return false; + }, + + swipeLeft: function(e) { + if(Engine.activeModule.swipeLeft) { + Engine.activeModule.swipeLeft(e); + } + }, + + swipeRight: function(e) { + if(Engine.activeModule.swipeRight) { + Engine.activeModule.swipeRight(e); + } + }, + + swipeUp: function(e) { + if(Engine.activeModule.swipeUp) { + Engine.activeModule.swipeUp(e); + } + }, + + swipeDown: function(e) { + if(Engine.activeModule.swipeDown) { + Engine.activeModule.swipeDown(e); + } + }, + + disableSelection: function() { + document.onselectstart = eventNullifier; // this is for IE + document.onmousedown = eventNullifier; // this is for the rest + }, + + enableSelection: function() { + document.onselectstart = eventPassthrough; + document.onmousedown = eventPassthrough; + }, + + autoSelect: function(selector) { + $(selector).focus().select(); + }, + + handleStateUpdates: function(e){ + + }, + + switchLanguage: function(dom){ + var lang = $(dom).data("language"); + if(document.location.href.search(/[\?\&]lang=[a-z_]+/) != -1){ + document.location.href = document.location.href.replace( /([\?\&]lang=)([a-z_]+)/gi , "$1"+lang ); + }else{ + document.location.href = document.location.href + ( (document.location.href.search(/\?/) != -1 )?"&":"?") + "lang="+lang; + } + }, + + saveLanguage: function(){ + var lang = decodeURIComponent((new RegExp('[?|&]lang=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null; + if(lang && typeof Storage != 'undefined' && localStorage) { + localStorage.lang = lang; + } + }, + + toggleVolume: function(enabled /* optional */) { if (enabled == null) { enabled = !$SM.get('config.soundOn'); } - if (!enabled) { - $('.volume').text(_('sound on.')); - $SM.set('config.soundOn', false); - AudioEngine.setMasterVolume(0.0); - } else { - $('.volume').text(_('sound off.')); - $SM.set('config.soundOn', true); - AudioEngine.setMasterVolume(1.0); - } - }, + if (!enabled) { + $('.volume').text(_('sound on.')); + $SM.set('config.soundOn', false); + AudioEngine.setMasterVolume(0.0); + } else { + $('.volume').text(_('sound off.')); + $SM.set('config.soundOn', true); + AudioEngine.setMasterVolume(1.0); + } + }, - setInterval: function(callback, interval, skipDouble){ - if( Engine.options.doubleTime && !skipDouble ){ - Engine.log('Double time, cutting interval in half'); - interval /= 2; - } + setInterval: function(callback, interval, skipDouble){ + if( Engine.options.doubleTime && !skipDouble ){ + Engine.log('Double time, cutting interval in half'); + interval /= 2; + } - return setInterval(callback, interval); + return setInterval(callback, interval); - }, + }, - setTimeout: function(callback, timeout, skipDouble){ + setTimeout: function(callback, timeout, skipDouble){ - if( Engine.options.doubleTime && !skipDouble ){ - Engine.log('Double time, cutting timeout in half'); - timeout /= 2; - } + if( Engine.options.doubleTime && !skipDouble ){ + Engine.log('Double time, cutting timeout in half'); + timeout /= 2; + } - return setTimeout(callback, timeout); + return setTimeout(callback, timeout); - } - }; + } + }; - function eventNullifier(e) { - return $(e.target).hasClass('menuBtn'); - } + function eventNullifier(e) { + return $(e.target).hasClass('menuBtn'); + } - function eventPassthrough(e) { - return true; - } + function eventPassthrough(e) { + return true; + } function notifyAboutSound() { if ($SM.get('playStats.audioAlertShown')) { @@ -893,48 +897,46 @@ function inView(dir, elem){ - var scTop = $('#main').offset().top; - var scBot = scTop + $('#main').height(); + var scTop = $('#main').offset().top; + var scBot = scTop + $('#main').height(); - var elTop = elem.offset().top; - var elBot = elTop + elem.height(); + var elTop = elem.offset().top; + var elBot = elTop + elem.height(); - if( dir == 'up' ){ - // STOP MOVING IF BOTTOM OF ELEMENT IS VISIBLE IN SCREEN - return ( elBot < scBot ); - }else if( dir == 'down' ){ - return ( elTop > scTop ); - }else{ - return ( ( elBot <= scBot ) && ( elTop >= scTop ) ); - } + if( dir == 'up' ){ + // STOP MOVING IF BOTTOM OF ELEMENT IS VISIBLE IN SCREEN + return ( elBot < scBot ); + } else if( dir == 'down' ){ + return ( elTop > scTop ); + } else { + return ( ( elBot <= scBot ) && ( elTop >= scTop ) ); + } } -function scrollByX(elem, x){ - - var elTop = parseInt( elem.css('top'), 10 ); - elem.css( 'top', ( elTop + x ) + "px" ); - +function setYPosition(elem, y) { + var elTop = parseInt( elem.css('top'), 10 ); + elem.css('top', `${y}px`); } //create jQuery Callbacks() to handle object events $.Dispatch = function( id ) { - var callbacks, topic = id && Engine.topics[ id ]; - if ( !topic ) { - callbacks = jQuery.Callbacks(); - topic = { - publish: callbacks.fire, - subscribe: callbacks.add, - unsubscribe: callbacks.remove - }; - if ( id ) { - Engine.topics[ id ] = topic; - } - } - return topic; + var callbacks, topic = id && Engine.topics[ id ]; + if ( !topic ) { + callbacks = jQuery.Callbacks(); + topic = { + publish: callbacks.fire, + subscribe: callbacks.add, + unsubscribe: callbacks.remove + }; + if ( id ) { + Engine.topics[ id ] = topic; + } + } + return topic; }; $(function() { - Engine.init(); + Engine.init(); }); diff --git a/script/events.js b/script/events.js index 018212af..5eba0c2d 100644 --- a/script/events.js +++ b/script/events.js @@ -8,8 +8,18 @@ var Events = { _FIGHT_SPEED: 100, _EAT_COOLDOWN: 5, _MEDS_COOLDOWN: 7, + _HYPO_COOLDOWN: 7, + _SHIELD_COOLDOWN: 10, + _STIM_COOLDOWN: 10, _LEAVE_COOLDOWN: 1, STUN_DURATION: 4000, + ENERGISE_MULTIPLIER: 4, + EXPLOSION_DURATION: 3000, + ENRAGE_DURATION: 4000, + MEDITATE_DURATION: 5000, + BOOST_DURATION: 3000, + BOOST_DAMAGE: 10, + DOT_TICK: 1000, BLINK_INTERVAL: false, init: function(options) { this.options = $.extend( @@ -133,11 +143,59 @@ var Events = { if((Path.outfit['medicine'] || 0) !== 0) { Events.createUseMedsButton().appendTo(healBtns); } + if((Path.outfit['hypo'] || 0) !== 0) { + Events.createUseHypoButton().appendTo(healBtns); + } + if ((Path.outfit['stim'] ?? 0) > 0) { + Events.createStimButton().appendTo(healBtns); + } + if($SM.get('stores["kinetic armour"]', true) > 0) { + Events.createShieldButton().appendTo(healBtns); + } $('
      ').addClass('clear').appendTo(healBtns); Events.setHeal(healBtns); - // Set up the enemy attack timer - Events._enemyAttackTimer = Engine.setInterval(Events.enemyAttack, scene.attackDelay * 1000); + // Set up the enemy attack timers + Events.startEnemyAttacks(); + Events._specialTimers = (scene.specials ?? []).map(s => Engine.setInterval( + () => { + const enemy = $('#enemy'); + const text = s.action(enemy); + Events.updateFighterDiv(enemy); + if (text) { + Events.drawFloatText(text, $('.hp', enemy)) + } + }, + s.delay * 1000 + )); + }, + + startEnemyAttacks: (delay) => { + clearInterval(Events._enemyAttackTimer); + const scene = Events.activeEvent().scenes[Events.activeScene]; + Events._enemyAttackTimer = Engine.setInterval(Events.enemyAttack, (delay ?? scene.attackDelay) * 1000); + }, + + setStatus: (fighter, status) => { + fighter.data('status', status); + if (status === 'enraged' && fighter.attr('id') === 'enemy') { + Events.startEnemyAttacks(0.5); + setTimeout(() => { + fighter.data('status', 'none'); + Events.startEnemyAttacks(); + }, Events.ENRAGE_DURATION); + } + if (status === 'meditation') { + Events._meditateDmg = 0; + setTimeout(() => { + fighter.data('status', 'none'); + }, Events.MEDITATE_DURATION); + } + if (status === 'boost') { + setTimeout(() => { + fighter.data('status', 'none'); + }, Events.BOOST_DURATION); + } }, setPause: function(btn, state){ @@ -261,6 +319,43 @@ var Events = { return btn; }, + createUseHypoButton: function(cooldown) { + if (cooldown == null) { + cooldown = Events._HYPO_COOLDOWN; + } + + var btn = new Button.Button({ + id: 'hypo', + text: _('use hypo'), + cooldown: cooldown, + click: Events.useHypo, + cost: { 'hypo': 1 } + }); + + if((Path.outfit['hypo'] ?? 0) > 0) { + Button.setDisabled(btn, true); + } + + return btn; + }, + + createShieldButton: function() { + var btn = new Button.Button({ + id: 'shld', + text: _('shield'), + cooldown: Events._SHIELD_COOLDOWN, + click: Events.useShield + }); + return btn; + }, + + createStimButton: () => new Button.Button({ + id: 'use-stim', + text: _('boost'), + cooldown: Events._STIM_COOLDOWN, + click: Events.useStim + }), + createAttackButton: function(weaponName) { var weapon = World.Weapons[weaponName]; var cd = weapon.cooldown; @@ -270,10 +365,11 @@ var Events = { } } var btn = new Button.Button({ - id: 'attack_' + weaponName.replace(' ', '-'), + id: 'attack_' + weaponName.replace(/ /g, '-'), text: weapon.verb, cooldown: cd, click: Events.useWeapon, + boosted: () => $('#wanderer').data('status') === 'boost', cost: weapon.cost }); if(typeof weapon.damage == 'number' && weapon.damage > 0) { @@ -290,15 +386,16 @@ var Events = { return btn; }, - drawFloatText: function(text, parent) { + drawFloatText: function(text, parent, cb) { $('
      ').text(text).addClass('damageText').appendTo(parent).animate({ - 'bottom': '50px', + 'bottom': '70px', 'opacity': '0' }, - 300, + 700, 'linear', function() { $(this).remove(); + cb && cb(); }); }, @@ -309,7 +406,8 @@ var Events = { healBtns = healBtns.children('.button'); var canHeal = (World.health < World.getMaxHealth()); healBtns.each(function(i){ - Button.setDisabled($(this), !canHeal); + const btn = $(this); + Button.setDisabled(btn, !canHeal && btn.attr('id') !== 'shld'); }); return canHeal; }, @@ -348,9 +446,27 @@ var Events = { AudioEngine.playSound(AudioLibrary.USE_MEDS); }, + useHypo: btn => { + Events.doHeal('hypo', World.hypoHeal(), btn); + AudioEngine.playSound(AudioLibrary.USE_MEDS); + }, + + useShield: btn => { + const player = $('#wanderer'); + player.data('status', 'shield'); + Events.updateFighterDiv(player); + }, + + useStim: btn => { + const player = $('#wanderer'); + player.data('status', 'boost'); + Events.dotDamage(player, Events.BOOST_DAMAGE); + Events.updateFighterDiv(player); + }, + useWeapon: function(btn) { if(Events.activeEvent()) { - var weaponName = btn.attr('id').substring(7).replace('-', ' '); + var weaponName = btn.attr('id').substring(7).replace(/-/g, ' '); var weapon = World.Weapons[weaponName]; if(weapon.type == 'unarmed') { if(!$SM.get('character.punches')) $SM.set('character.punches', 0); @@ -436,29 +552,103 @@ var Events = { } attackFn($('#wanderer'), dmg, function() { - if($('#enemy').data('hp') <= 0 && !Events.won) { + const enemy = $('#enemy'); + const enemyHp = enemy.data('hp'); + const scene = Events.activeEvent().scenes[Events.activeScene]; + const atHealth = scene.atHealth ?? {}; + const explosion = scene.explosion; + + for (const [k, action] of Object.entries(atHealth)) { + const hpThreshold = Number(k); + if (enemyHp <= hpThreshold && enemyHp + dmg > hpThreshold) { + action(enemy); + } + } + + if(enemyHp <= 0 && !Events.won) { // Success! + if (explosion) { + Events.explode(enemy, $('#wanderer'), explosion); + } + else { + Events.winFight(); + } + } + }); + } + }, + + explode: (enemy, player, dmg) => { + Events.clearTimeouts(); + enemy.addClass('exploding'); + setTimeout(() => { + enemy.removeClass('exploding'); + $('.label', enemy).text('*'); + Events.damage(enemy, player, dmg, 'ranged', () => { + if (!Events.checkPlayerDeath()) { Events.winFight(); } }); + }, Events.EXPLOSION_DURATION); + }, + + dotDamage: (target, dmg) => { + const hp = Math.max(0, target.data('hp') - dmg); + target.data('hp', hp); + if(target.attr('id') == 'wanderer') { + World.setHp(hp); + Events.setHeal(); + Events.checkPlayerDeath(); } + else if(hp <= 0 && !Events.won) { + Events.winFight(); + } + Events.updateFighterDiv(target); + Events.drawFloatText(`-${dmg}`, $('.hp', target)); }, - damage: function(fighter, enemy, dmg, type) { + damage: function(fighter, enemy, dmg, type, cb) { var enemyHp = enemy.data('hp'); + const maxHp = enemy.data('maxHp'); var msg = ""; + const shielded = enemy.data('status') === 'shield'; + const energised = fighter.data('status') === 'energised'; + const venomous = fighter.data('status') === 'venomous'; + const meditating = enemy.data('status') === 'meditation'; if(typeof dmg == 'number') { if(dmg < 0) { msg = _('miss'); dmg = 0; } else { - msg = '-' + dmg; - enemyHp = ((enemyHp - dmg) < 0) ? 0 : (enemyHp - dmg); - enemy.data('hp', enemyHp); - if(fighter.attr('id') == 'enemy') { - World.setHp(enemyHp); - Events.setHeal(); + if (energised) { + dmg *= this.ENERGISE_MULTIPLIER; + } + + if (meditating) { + Events._meditateDmg = (Events._meditateDmg ?? 0) + dmg; + msg = dmg; } + else { + msg = (shielded ? '+' : '-') + dmg; + enemyHp = Math.min(maxHp, Math.max(0, enemyHp + (shielded ? dmg : -dmg))); + enemy.data('hp', enemyHp); + if(fighter.attr('id') == 'enemy') { + World.setHp(enemyHp); + Events.setHeal(); + } + } + + if (venomous && !shielded) { + Events._dotTimer = setInterval(() => { + Events.dotDamage(enemy, Math.floor(dmg / 2)); + }, Events.DOT_TICK); + } + + if (shielded) { + // shields break in one hit + enemy.data('status', 'none'); + } + Events.updateFighterDiv(enemy); // play variation audio for weapon type @@ -478,11 +668,18 @@ var Events = { } else { if(dmg == 'stun') { msg = _('stunned'); - enemy.data('stunned', Events.STUN_DURATION); + enemy.data('stunned', true); + setTimeout(() => enemy.data('stunned', false), Events.STUN_DURATION); } } - Events.drawFloatText(msg, $('.hp', enemy)); + if (energised || venomous) { + // attack buffs only applies to one hit + fighter.data('status', 'none'); + Events.updateFighterDiv(fighter); + } + + Events.drawFloatText(msg, $('.hp', enemy), cb); }, animateMelee: function(fighter, dmg, callback) { @@ -533,32 +730,47 @@ var Events = { // Events.togglePause($('#pause'),'auto'); var scene = Events.activeEvent().scenes[Events.activeScene]; + const enemy = $('#enemy'); + const stunned = enemy.data('stunned'); + const meditating = enemy.data('status') === 'meditation'; - if(!$('#enemy').data('stunned')) { + if(!stunned && !meditating) { var toHit = scene.hit; toHit *= $SM.hasPerk('evasive') ? 0.8 : 1; var dmg = -1; - if(Math.random() <= toHit) { + if ((Events._meditateDmg ?? 0) > 0) { + dmg = Events._meditateDmg; + Events._meditateDmg = 0; + } + else if(Math.random() <= toHit) { dmg = scene.damage; } var attackFn = scene.ranged ? Events.animateRanged : Events.animateMelee; - attackFn($('#enemy'), dmg, function() { - if($('#wanderer').data('hp') <= 0) { - // Failure! - clearTimeout(Events._enemyAttackTimer); - Events.endEvent(); - World.die(); - AudioEngine.playSound(AudioLibrary.LOSE_FIGHT); - } - }); + attackFn($('#enemy'), dmg, Events.checkPlayerDeath); + } + }, + + checkPlayerDeath: () => { + if($('#wanderer').data('hp') <= 0) { + Events.clearTimeouts(); + Events.endEvent(); + World.die(); + return true; } - }, + return false; + }, + + clearTimeouts: () => { + clearTimeout(Events._enemyAttackTimer); + Events._specialTimers.forEach(clearTimeout); + clearTimeout(Events._dotTimer); + }, endFight: function() { Events.fought = true; - clearTimeout(Events._enemyAttackTimer); + Events.clearTimeouts(); Events.removePause($('#pause'), 'end'); }, @@ -605,6 +817,9 @@ var Events = { if((Path.outfit['medicine'] || 0) !== 0) { Events.createUseMedsButton(0).appendTo(healBtns); } + if (Path.outfit['hypo'] ?? 0 > 0) { + Events.createUseHypoButton(0).appendTo(healBtns); + } $('
      ').addClass('clear').appendTo(healBtns); Events.setHeal(healBtns); } @@ -623,7 +838,7 @@ var Events = { }, drawDrop:function(btn) { - var name = btn.attr('id').substring(5).replace('-', ' '); + var name = btn.attr('id').substring(5).replace(/-/g, ' '); var needsAppend = false; var weight = Path.getWeight(name); var freeSpace = Path.getFreeSpace(); @@ -647,7 +862,7 @@ var Events = { numToDrop = Path.outfit[k]; } if(numToDrop > 0) { - var dropRow = $('
      ').attr('id', 'drop_' + k.replace(' ', '-')) + var dropRow = $('
      ').attr('id', 'drop_' + k.replace(/ /g, '-')) .text(_(k) + ' x' + numToDrop) .data('thing', k) .data('num', numToDrop) @@ -679,7 +894,7 @@ var Events = { }, drawLootRow: function(name, num){ - var id = name.replace(' ', '-'); + var id = name.replace(/ /g, '-'); var lootRow = $('
      ').attr('id','loot_' + id).data('item', name).addClass('lootRow'); var take = new Button.Button({ id: 'take_' + id, @@ -790,7 +1005,7 @@ var Events = { var btn = $(this); var target = btn.closest('.button'); var thing = btn.data('thing'); - var id = 'take_' + thing.replace(' ', '-'); + var id = 'take_' + thing.replace(/ /g, '-'); var num = btn.data('num'); var lootButtons = $('#lootButtons'); Engine.log('dropping ' + num + ' ' + thing); @@ -810,7 +1025,7 @@ var Events = { }, getLoot: function(btn, stateSkipButtonSet) { - var name = btn.attr('id').substring(5).replace('-', ' '); + var name = btn.attr('id').substring(5).replace(/-/g, ' '); if(btn.data('numLeft') > 0) { var skipButtonSet = stateSkipButtonSet || false; var weight = Path.getWeight(name); @@ -867,13 +1082,21 @@ var Events = { }, createFighterDiv: function(chara, hp, maxhp) { - var fighter = $('
      ').addClass('fighter').text(_(chara)).data('hp', hp).data('maxHp', maxhp).data('refname',chara); + var fighter = $('
      ') + .addClass('fighter') + .data('hp', hp) + .data('maxHp', maxhp) + .data('refname',chara); + $('
      ').addClass('label').text(_(chara)).appendTo(fighter); $('
      ').addClass('hp').text(hp+'/'+maxhp).appendTo(fighter); return fighter; }, updateFighterDiv: function(fighter) { $('.hp', fighter).text(fighter.data('hp') + '/' + fighter.data('maxHp')); + const status = fighter.data('status'); + const hasStatus = status && status !== 'none'; + fighter.attr('class', `fighter${hasStatus ? ` ${status}` : ''}`); }, startStory: function(scene) { @@ -912,13 +1135,19 @@ var Events = { var btnsList = []; for(var id in scene.buttons) { var info = scene.buttons[id]; - var b = new Button.Button({ - id: id, - text: info.text, - cost: info.cost, - click: Events.buttonClick, - cooldown: info.cooldown - }).appendTo(btns); + const cost = { + ...info.cost + }; + if (Path.outfit && Path.outfit['glowstone']) { + delete cost.torch; + } + var b = new Button.Button({ + id, + text: info.text, + cost, + click: Events.buttonClick, + cooldown: info.cooldown + }).appendTo(btns); if(typeof info.available == 'function' && !info.available()) { Button.setDisabled(b, true); } @@ -932,6 +1161,17 @@ var Events = { return (btnsList.length == 1) ? btnsList[0] : false; }, + getQuantity: function(store) { + if (store === 'water') { + return World.water; + } + if (store === 'hp') { + return World.health; + } + var num = Engine.activeModule == World ? Path.outfit[store] : $SM.get('stores["'+store+'"]', true); + return isNaN(num) || num < 0 ? 0 : num; + }, + updateButtons: function() { var btns = Events.activeEvent().scenes[Events.activeScene].buttons; for(var bId in btns) { @@ -940,11 +1180,16 @@ var Events = { if(typeof b.available == 'function' && !b.available()) { Button.setDisabled(btnEl, true); } else if(b.cost) { + const cost = { + ...b.cost + }; + if (Path.outfit && Path.outfit['glowstone']) { + delete cost.torch; + } var disabled = false; - for(var store in b.cost) { - var num = Engine.activeModule == World ? Path.outfit[store] : $SM.get('stores["'+store+'"]', true); - if(typeof num != 'number') num = 0; - if(num < b.cost[store]) { + for(var store in cost) { + var num = Events.getQuantity(store); + if(num < cost[store]) { // Too expensive disabled = true; break; @@ -960,14 +1205,27 @@ var Events = { // Cost var costMod = {}; if(info.cost) { - for(var store in info.cost) { - var num = Engine.activeModule == World ? Path.outfit[store] : $SM.get('stores["'+store+'"]', true); - if(typeof num != 'number') num = 0; - if(num < info.cost[store]) { + const cost = { + ...info.cost + }; + if (Path.outfit && Path.outfit['glowstone']) { + delete cost.torch; + } + for(var store in cost) { + var num = Events.getQuantity(store); + if(num < cost[store]) { // Too expensive return; } - costMod[store] = -info.cost[store]; + if (store === 'water') { + World.setWater(World.water - cost[store]); + } + else if (store === 'hp') { + World.setHp(World.hp - cost[store]); + } + else { + costMod[store] = -cost[store]; + } } if(Engine.activeModule == World) { for(var k in costMod) { @@ -1002,8 +1260,16 @@ var Events = { if (info.link) { Events.endEvent(); window.open(info.link); + return; } + // Next Event + if (info.nextEvent) { + const eventData = Events.Setpieces[info.nextEvent] || Events.Executioner[info.nextEvent]; + Events.switchEvent(eventData); + return; + } + // Next Scene if(info.nextScene) { if(info.nextScene == 'end') { @@ -1104,27 +1370,39 @@ var Events = { return Events.activeEvent().eventPanel; }, + switchEvent: event => { + if (!event) { + return; + } + Events.eventPanel().remove(); + Events.activeEvent().eventPanel = null; + Events.eventStack.shift(); + Events.startEvent(event); + }, + startEvent: function(event, options) { - if(event) { - Engine.event('game event', 'event'); - Engine.keyLock = true; - Engine.tabNavigation = false; - Button.saveCooldown = false; - Events.eventStack.unshift(event); - event.eventPanel = $('
      ').attr('id', 'event').addClass('eventPanel').css('opacity', '0'); - if(options != null && options.width != null) { - Events.eventPanel().css('width', options.width); - } - $('
      ').addClass('eventTitle').text(Events.activeEvent().title).appendTo(Events.eventPanel()); - $('
      ').attr('id', 'description').appendTo(Events.eventPanel()); - $('
      ').attr('id', 'buttons').appendTo(Events.eventPanel()); - Events.loadScene('start'); - $('div#wrapper').append(Events.eventPanel()); - Events.eventPanel().animate({opacity: 1}, Events._PANEL_FADE, 'linear'); - var currentSceneInformation = Events.activeEvent().scenes[Events.activeScene]; - if (currentSceneInformation.blink) { - Events.blinkTitle(); - } + if(!event) { + return; + } + event.audio && AudioEngine.playEventMusic(event.audio); + Engine.event('game event', 'event'); + Engine.keyLock = true; + Engine.tabNavigation = false; + Button.saveCooldown = false; + Events.eventStack.unshift(event); + event.eventPanel = $('
      ').attr('id', 'event').addClass('eventPanel').css('opacity', '0'); + if(options != null && options.width != null) { + Events.eventPanel().css('width', options.width); + } + $('
      ').addClass('eventTitle').text(Events.activeEvent().title).appendTo(Events.eventPanel()); + $('
      ').attr('id', 'description').appendTo(Events.eventPanel()); + $('
      ').attr('id', 'buttons').appendTo(Events.eventPanel()); + Events.loadScene('start'); + $('div#wrapper').append(Events.eventPanel()); + Events.eventPanel().animate({opacity: 1}, Events._PANEL_FADE, 'linear'); + var currentSceneInformation = Events.activeEvent().scenes[Events.activeScene]; + if (currentSceneInformation.blink) { + Events.blinkTitle(); } }, diff --git a/script/events/executioner.js b/script/events/executioner.js new file mode 100644 index 00000000..92da8a3b --- /dev/null +++ b/script/events/executioner.js @@ -0,0 +1,2344 @@ +Enemies = window.Enemies ?? {}; +Enemies.Executioner = { + 'guard': { + combat: true, + notification: _('tripped a motion sensor.'), + enemy: 'mechanical guard', + enemyName: _('mechanical guard'), + ranged: true, + chara: 'G', + damage: 10, + hit: 0.8, + attackDelay: 2, + health: 60, + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'laser rifle': { + min: 1, + max: 1, + chance: 0.8 + }, + 'alien alloy': { + min: 1, + max: 1, + chance: 0.2 + } + }, + }, + 'quadruped': { + combat: true, + notification: _('a mobile defence platform trundles around the corner.'), + enemy: 'mechanical quadruped', + enemyName: _('mechanical quadruped'), + ranged: false, + chara: 'Q', + damage: 8, + hit: 0.8, + attackDelay: 1, + health: 70, + loot: { + 'alien alloy': { + min: 1, + max: 1, + chance: 1 + }, + 'alien alloy': { + min: 2, + max: 4, + chance: 0.2 + } + } + }, + 'medic': { + combat: true, + notification: _('a medical drone wheels out of control.'), + enemy: 'broken medic', + enemyName: _('broken medic'), + ranged: false, + chara: 'M', + damage: 15, + hit: 0.8, + attackDelay: 3, + health: 80, + atHealth: { + 40: fighter => { + Events.setStatus(fighter, 'venomous'); + return 'venomous'; + } + }, + loot: { + 'alien alloy': { + min: 1, + max: 2, + chance: 1 + }, + 'hypo': { + min: 1, + max: 4, + chance: 0.2 + } + } + }, + 'turret': { + combat: true, + notification: _('one of the defence turrets still works.'), + enemy: 'defence turret', + enemyName: _('defence turret'), + ranged: true, + chara: 'T', + damage: 25, + hit: 0.8, + attackDelay: 4, + health: 50, + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'alien alloy': { + min: 1, + max: 1, + chance: 0.8 + }, + 'laser rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + } +}; + +Events.Executioner = { + "executioner-intro": { /* Exploring a ravaged battleship */ + title: _('A Ravaged Battleship'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + notification: _('the remains of a huge ship are embedded in the earth.'), + text: [ + _('the remains of a massive battleship lie here, like a silent sealed city.'), + _('it lists to the side in a deep crevasse, cut when it fell from the sky.'), + _('the hatches are all sealed, but the hull is blown out just above the dirt, providing an entrance.') + ], + buttons: { + 'enter': { + text: _('enter'), + cost: { torch: 1 }, + nextScene: {1: '1'} + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '1': { + text: [ + _('the interior of the ship is cold and dark. what little light there is only accentuates its harsh angles.'), + _('the walls hum faintly.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.4: '2-1', 0.8: '2-2', 1: '2-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '2-1': { + 'text': [ + _('thick, sticky webbing covers the walls of the corridor.'), + _('deeper into the ship, the darkness seems almost to writhe.'), + _('a small knapsack hangs from a cluster of webs, a few feet from the floor.') + ], + loot: { + 'cured meat': { + min: 1, + max: 5, + chance: 0.8 + }, + 'bullets': { + min: 1, + max: 5, + chance: 0.5 + }, + 'energy cell': { + min: 1, + max: 5, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '3-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-1': { + notification: _('a huge arthropod lunges from the shadows, its mandibles thrashing.'), + combat: true, + enemy: 'chitinous horror', + chara: 'H', + damage: 1, + hit: 0.7, + attackDelay: 0.25, + health: 60, + loot: { + 'meat': { + min: 5, + max: 10, + chance: 0.8 + }, + 'scales': { + min: 5, + max: 10, + chance: 0.5 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '4-1': { + notification: _('the webs part, and a grotesque insect lurches forward.'), + combat: true, + enemy: 'chitinous queen', + chara: 'Q', + damage: 1, + hit: 0.7, + attackDelay: 0.25, + health: 70, + loot: { + 'meat': { + min: 8, + max: 12, + chance: 0.8 + }, + 'scales': { + min: 8, + max: 12, + chance: 0.5 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2-2': { + notification: _('an operative waits in ambush around the corner.'), + combat: true, + enemy: 'operative', + chara: 'O', + damage: 8, + hit: 0.8, + attackDelay: 2, + health: 60, + loot: { + 'bayonet': { + min: 1, + max: 1, + chance: 0.5 + }, + 'bullets': { + min: 1, + max: 5, + chance: 0.8 + }, + 'cured meat': { + min: 1, + max: 5, + chance: 0.8 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '3-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3-2': { + 'text': [ + _('the military has set up a small camp just inside the ship.'), + _('crude attempts have been made to cut into the walls.'), + _('scraps of copper wire litter the floor.'), + _('two bedrolls are wedged into a corner.') + ], + loot: { + 'cured meat': { + min: 1, + max: 5, + chance: 1 + }, + 'torch': { + min: 1, + max: 3, + chance: 0.8 + }, + 'bullets': { + min: 1, + max: 5, + chance: 0.5 + }, + 'alien alloy': { + min: 1, + max: 2, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4-2': { + notification: _('a dusty researcher clumsily hides in the shadows.'), + combat: true, + enemy: 'researcher', + chara: 'R', + damage: 1, + hit: 0.8, + attackDelay: 2, + health: 20, + loot: { + 'torch': { + min: 1, + max: 3, + chance: 0.8 + }, + 'cloth': { + min: 1, + max: 5, + chance: 0.8 + }, + 'cured meat': { + min: 1, + max: 5, + chance: 0.8 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2-3': { + 'text': [ + _('debris is stacked in the corridor, forming a low barricade.'), + _('the walls are scorched and melted.'), + _('behind the barricade, a few weapons lay abandoned.') + ], + loot: { + 'laser rifle': { + min: 1, + max: 3, + chance: 1 + }, + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'plasma rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '3-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-3': { + 'text': [ + _('the partially devoured remains of several wanderers are piled before a dark corridor.'), + _('shuffling noises can be heard from within.') + ], + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.5 + }, + 'cloth': { + min: 1, + max: 5, + chance: 0.8 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4-3': { + combat: true, + enemy: 'ancient beast', + enemyName: _('ancient beast'), + chara: 'A', + damage: 6, + hit: 0.8, + attackDelay: 1, + health: 60, + loot: { + 'fur': { + min: 5, + max: 10, + chance: 1 + }, + 'meat': { + min: 5, + max: 10, + chance: 1 + }, + 'teeth': { + min: 5, + max: 10, + chance: 0.8 + } + }, + notification: _('an ancient beast has made these ruins its home.'), + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '5': { + 'text': [ + _('a maintenance panel is embedded in the wall next to a large sealed door.'), + _('perhaps the ship’s systems are still operational.') + ], + buttons: { + 'power': { + text: _('power cycle'), + nextScene: { 1: '6' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '6': { + combat: true, + notification: _('as the lights come online, so too do the defence systems.'), + enemy: 'automated turret', + enemyName: _('automated turret'), + ranged: true, + chara: 'T', + damage: 10, + hit: 0.8, + attackDelay: 2.5, + health: 60, + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'laser rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '7' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '7': { + 'text': [ + _('beyond the bulkhead is a small antechamber, seemingly untouched by scavengers.'), + _('a large hatch grinds open, and the wind rushes in.'), + _('a strange device sits on the floor. looks important.') + ], + onLoad: () => { + World.drawRoad(); + World.state.executioner = true; + }, + buttons: { + 'leave': { + text: _('take device and leave'), + nextScene: 'end' + } + } + } + } + }, + + "executioner-antechamber": { /* Deeper into a ravaged battleship */ + title: _('A Ravaged Battleship'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + 'text': [ + _('a large hatch opens into a wide corridor.'), + _('the corridor leads to a bank of elevators, which appear to be functional.') + ], + buttons: { + 'engineering': { + text: _('engineering'), + available: function() { + return !World.state.engineering; + }, + nextEvent: 'executioner-engineering' + }, + 'medical': { + text: _('medical'), + available: function() { + return !World.state.medical; + }, + nextEvent: 'executioner-medical' + }, + 'martial': { + text: _('martial'), + available: function() { + return !World.state.martial; + }, + nextEvent: 'executioner-martial' + }, + 'command': { + text: _('command deck'), + available: function() { + return World.state.engineering && World.state.medical && World.state.martial; + }, + nextEvent: 'executioner-command' + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + } + } + }, + + "executioner-engineering": { /* Engineering wing */ + title: _('Engineering Wing'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + 'text': [ + _('elevator doors open to a blasted corridor. debris covers the floor, piled into makeshift defences.'), + _('emergency lighting flickers.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.3: '1-1', 0.7: '1-2', 1: '1-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '1-1': { + text: [ + _('an automated assembly line performs its empty routines, long since deprived of materials.'), + _('its final works lie forgotten, covered by a thin layer of dust.') + ], + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'laser rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '2-1a', 1: '2-1b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '2-1a': { + combat: true, + notification: _('assembly arms spin wildly out of control.'), + enemy: 'unruly welder', + enemyName: _('unruly welder'), + ranged: false, + chara: 'W', + damage: 13, + hit: 0.8, + attackDelay: 2, + health: 50, + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'alien alloy': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '3-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2-1b': { + text: [ + _('assembly arms spark and jitter.'), + _('a cacophony of decrepit machinery fills the room.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '3-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-1': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '1-2': { + ...Enemies.Executioner.turret, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '2-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2-2': { + text: [ + _('must have been the engine room, once. the massive machines now stand inert, twisted and scorched by explosions.'), + _('the destruction is uniform and precise.'), + _('bits of them can be scavenged.') + ], + loot: { + 'alien alloy': { + min: 2, + max: 5, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '3-2a', 1: '3-2b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-2a': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3-2b': { + text: [ + _('none of the ship\'s engines escaped the destruction.'), + _('it\'s no mystery why she no longer flies.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '1-3': { + text: [ + _('sparks cascade from a reactivated power junction, and catch.'), + _('the flames fill the corridor.') + ], + buttons: { + 'water': { + text: _('extinguish'), + cost: { 'water': 5 }, + nextScene: { 0.5: '2-3a', 1: '2-3b' } + }, + 'run': { + text: _('rush through'), + cost: { 'hp': 10 }, + nextScene: { 0.5: '2-3a', 1: '2-3b' } + } + } + }, + '2-3a': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '3-3' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2-3b': { + text: [ + _('rows of inert security robots hang suspended from the ceiling.'), + _('wires run overhead, corroded and useless.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '3-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-3': { + text: [ + _('more signs of past combat down the hall. guard post is ransacked.'), + _('still, some things can be found.') + ], + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 0.8 + }, + 'laser rifle': { + min: 1, + max: 1, + chance: 0.7 + }, + 'grenade': { + min: 1, + max: 3, + chance: 0.6 + }, + 'plasma rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '4': { + text: [ + _('marks on the door read \'research and development.\' everything seems mostly untouched, but dead.'), + _('one machine thrums with power, and might still work.') + ], + buttons: { + 'use': { + text: _('use machine'), + cost: { 'alien alloy': 1 }, + onChoose: function() { + World.setHp(World.getMaxHealth()); + }, + nextScene: { 1: '4-heal' } + }, + 'continue': { + text: _('continue'), + nextScene: { 0.5: '5-1', 1: '5-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4-heal': { + text: [ + _('step inside, and the machine whirs. muscle and bone reknit. good as new.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '5-1', 1: '5-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '5-1': { + ...Enemies.Executioner.turret, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '6' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '5-2': { + text: [ + _('the machines here look unfinished, abandoned by their creator. wires and other scrap are scattered about the work benches.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '6' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '6': { + text: [ + _('experimental plans cover one wall, held by an unseen force.'), + _('this one looks useful.') + ], + loot: { + 'hypo blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '7-intro' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '7-intro': { + text: [ + _('clattering metal and old servos. something is coming...') + ], + buttons: { + 'fight': { + text: _('fight'), + nextScene: { 1: '7' } + } + } + }, + '7': { + combat: true, + notification: _('an unfinished automaton whirs to life.'), + enemy: 'unstable prototype', + enemyName: _('unstable prototype'), + ranged: false, + chara: 'P', + damage: 5, + hit: 0.8, + attackDelay: 2, + health: 150, + specials:[{ + delay: 5, + action: (fighter) => { + Events.setStatus(fighter, 'shield'); + return 'shield'; + } + }], + loot: { + 'alien alloy': { + min: 1, + max: 3, + chance: 1 + }, + 'kinetic armour blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '8' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '8': { + text: [ + _('at the back of the workshop, elevator doors twitch and buzz.'), + _('looks like a way out of here.') + ], + onLoad: () => { + World.state.engineering = true; + }, + buttons: { + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + } + } + }, + + "executioner-martial": { /* Martial wing */ + title: _('Martial Wing'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + text: [ + _('metal grinds, and the elevator doors open halfway. beyond is a brightly lit battlefield. remains litter the corridor, undisturbed by scavengers.'), + _('looks like they tried to barricade the elevators.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '1': { + text: [ + _('further along, the corridor branches.'), + _('the door to the left is sealed and refuses to open.') + ], + buttons: { + 'explode': { + text: _('blow it down'), + cost: { grenade: 1 }, + nextScene: { 1: '2-1' } + }, + 'right': { + text: _('continue right'), + nextScene: { 0.5: '2-2', 1: '2-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '2-1': { + text: [ + _('the blast throws the door inwards.'), + _('through the bulkhead is a large room, walls lined with weapon racks. fighting seems to have passed it by.') + ], + loot: { + 'energy blade': { + min: 2, + max: 5, + chance: 1 + }, + 'laser rifle': { + min: 2, + max: 5, + chance: 1 + }, + 'energy cell': { + min: 5, + max: 20, + chance: 1 + }, + 'grenade': { + min: 1, + max: 5, + chance: 0.8 + }, + 'plasma rifle': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '3-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-1': { + ...Enemies.Executioner.turret, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '4-1': { + text: [ + _('another door at the end of the hall, sealed from this side.'), + _('should be able to open it.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '2-2': { + ...Enemies.Executioner.turret, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 0.5: '3-2a', 1: '3-2b' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3-2a': { + ...Enemies.Executioner.quadruped, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3-2b': { + text: [ + _('the corridor is eerily silent.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4-2': { + text: [ + _('crew cabins flank the hall, devoid of life.'), + _('a few useful items can be scavenged.') + ], + loot: { + 'energy cell': { + min: 1, + max: 5, + chance: 1 + }, + 'energy blade': { + min: 1, + max: 1, + chance: 0.2 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '2-3': { + text: [ + _('ruined defence turrets flank the corridor.'), + _('could put the scrap to good use.') + ], + loot: { + 'alien alloy': { + min: 1, + max: 3, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '3-3a', 1: '3-3b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3-3a': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3-3b': { + text: [ + _('small sensors in the walls still look to be operational.'), + _('easily avoided.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4-3' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4-3': { + ...Enemies.Executioner.quadruped, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '5': { + text: [ + _('large barricades bisect the corridor, scorched by weapons fire.'), + _('bodies litter the ground on either side.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '6' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '6': { + text: [ + _('documents are scattered down the hall, most charred and curled.'), + _('this one looks interesting.') + ], + loot: { + 'plasma rifle blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '7-1', 1: '7-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '7-1': { + text: [ + _('the next door leads to a ransacked planning room.'), + _('maps of the surface can still be found amongst the debris.') + ], + buttons: { + 'scavenge': { + text: _('scavenge maps'), + onChoose: () => { + for (let i = 0; i < 3; i++) { + World.applyMap(); + } + }, + nextScene: { 1: '8-1a' } + }, + 'continue': { + text: _('continue'), + nextScene: { 1: '8-1b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '8-1a': { + ...Enemies.Executioner.guard, + notification: _('drew some attention with all that noise.'), + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '9-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '8-1b': { + text: [ + _('slipped past an automated sentry.'), + _('if only they\'d been destroyed along with everything else.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '9-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '9-1': { + ...Enemies.Executioner.guard, + notification: _('ran straight into another one.'), + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '10' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '7-2': { + text: [ + _('the corridor passes through a security checkpoint. the defences are blown apart, ragged edges scorched by laser fire.'), + _('past the checkpoint, banks of containment cells can be seen.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '8-2a', 1: '8-2b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '8-2a': { + text: [ + _('the cells are all empty.'), + _('power cables running across the ceiling are split in several places, sparking occasionally.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '9-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '8-2b': { + text: [ + _('the guards died at their posts, shot through with superheated plasma.'), + _('their weapons lie on the floor beside them.') + ], + loot: { + 'laser rifle': { + min: 2, + max: 2, + chance: 1 + }, + 'energy cell': { + min: 5, + max: 10, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '9-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '9-2': { + ...Enemies.Executioner.quadruped, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '10' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '10': { + 'text': [ + _('the corridor opens onto a vast training complex, obstacles and features blackened by real combat.'), + _('a regenerative machine hums uncannily by one of the courses.') + ], + buttons: { + 'use': { + text: _('use machine'), + cost: { 'alien alloy': 1 }, + onChoose: function() { + World.setHp(World.getMaxHealth()); + }, + nextScene: { 1: '11' } + }, + 'continue': { + text: _('continue'), + nextScene: { 1: '11' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '11': { + 'text': [ + _('motion from the centre of the yard.'), + _('a sparring automaton, still fully function and crusted with timeworn blood, lunges forward.') + ], + buttons: { + 'engage': { + text: _('engage'), + nextScene: { 1: '12' } + } + } + }, + '12': { + combat: true, + notification: _('the machine attacks, blades whirling.'), + enemy: 'murderous robot', + enemyName: _('murderous robot'), + ranged: false, + chara: 'M', + damage: 10, + hit: 0.8, + attackDelay: 3, + health: 250, + specials:[{ + delay: 13, + action: (fighter) => { + Events.setStatus(fighter, 'energised'); + return 'energised'; + } + }], + loot: { + 'alien alloy': { + min: 1, + max: 3, + chance: 1 + }, + 'disruptor blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '13' } + } + } + }, + '13': { + 'text': [ + _('the ruins of the sparring machine clatter to the ground.'), + _('picked this deck clean.') + ], + onLoad: () => { + World.state.martial = true; + }, + buttons: { + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + } + } + }, + + "executioner-medical": { /* Medical wing */ + title: _('Medical Wing'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + 'text': [ + _('elevator doors open to an empty corridor.'), + _('a few dusty corpses can be seen further down, but this deck appears to have been spared most of the combat.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '1': { + ...Enemies.Executioner.turret, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2': { + 'text': [ + _('past the checkpoint, the corridor is undamaged save for sporadic graffiti.'), + _('there was no fighting here.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '3a', 1: '3b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3a': { + ...Enemies.Executioner.quadruped, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '3b': { + 'text': [ + _('automated guardians still stalk the halls, unaware that their masters have long gone.'), + _('clumsy machines, and easily avoided.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4': { + 'text': [ + _('medical gurneys are fixed to grooves running down the corridor walls.'), + _('the automated patient transport system now sits motionless.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '5-1', 1: '5-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '5-1': { + ...Enemies.Executioner.medic, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 0.5: '6-1a', 1: '6-1b' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '6-1a': { + ...Enemies.Executioner.medic, + notification: _('it had friends.'), + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '7-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '6-1b': { + 'text': [ + _('more medical robots stand frozen, attached by a network of wires.'), + _('they take no notice of the intrusion.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '7-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '7-1': { + 'text': [ + _('weapons are strewn about the medical dispatch bay. must have been used as a muster point.'), + _('more strange graffiti adorns the walls.') + ], + loot: { + 'laser rifle': { + min: 1, + max: 1, + chance: 1 + }, + 'energy cell': { + min: 3, + max: 10, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '7-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '5-2': { + 'text': [ + _('this ward has been converted to a makeshift strategy room, maps scrawled hastily on any flat surface.'), + _('a secure locker is set into one wall.') + ], + buttons: { + 'force': { + text: _('force locker'), + nextScene: { 1: '6-2a-intro' } + }, + 'continue': { + text: _('continue'), + nextScene: { 1: '6-2b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '6-2a-intro': { + 'text': [ + _('hinges rusted through. no challenge.'), + ], + loot: { + 'energy cell': { + min: 5, + max: 10, + chance: 1 + }, + 'hypo': { + min: 1, + max: 3, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '6-2a' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '6-2a': { + ...Enemies.Executioner.medic, + notification: _('the noise draws attention.'), + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '7-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '6-2b': { + 'text': [ + _('better to move without drawing attention.'), + _('noises can be heard from the corridor outside.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '7-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '7-2': { + ...Enemies.Executioner.quadruped, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '8' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '8': { + combat: true, + notification: _('something\'s wrong with this robot.'), + enemy: 'unstable automaton', + enemyName: _('unstable automaton'), + ranged: false, + chara: 'A', + damage: 10, + hit: 0.7, + attackDelay: 2, + health: 100, + explosion: 30, + loot: { + 'glowstone blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '9' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '9': { + 'text': [ + _('another checkpoint ahead, fitted with heavy doors.'), + _('security is even tighter here.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '10a', 1: '10b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '10a': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '11' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '10b': { + 'text': [ + _('slipped through unnoticed.'), + _('air whistles as the doors open. this section must have lower pressure than the rest of the ship.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '11' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '11': { + ...Enemies.Executioner.medic, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 0.5: '12-1', 1: '12-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '12-1': { + 'text': [ + _('the air is cooler here. low cabinets ring the room, doors dusted with frost.'), + _('samples of something biological inside.') + ], + loot: { + 'cured meat': { + min: 5, + max: 10, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '13-1a', 1: '13-1b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '13-1a': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '14-1' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '13-1b': { + 'text': [ + _('security drones still patrol the hallways.'), + _('predictable paths.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '14-1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '14-1': { + ...Enemies.Executioner.medic, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '15' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '12-2': { + 'text': [ + _('surgical tools are scattered on the floor, near what appears the be the remains of a fire.'), + _('strange.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '13-2a', 1: '13-2b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '13-2a': { + ...Enemies.Executioner.medic, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '14-2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '13-2b': { + 'text': [ + _('the air in this room has a metallic tinge. floor is covered in dark powder.'), + _('some completed explosives in the corner.') + ], + loot: { + 'grenade': { + min: 3, + max: 8, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '14-2' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '14-2': { + ...Enemies.Executioner.medic, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '15' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + + '15': { + 'text': [ + _('containment cells arranged at the back of the room, all open.'), + _('something moving up ahead.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '16' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '16': { + combat: true, + notification: _('a mutated beast leaps from its cell.'), + enemy: 'malformed experiment', + enemyName: _('malformed experiment'), + ranged: false, + chara: 'E', + damage: 5, + hit: 0.8, + attackDelay: 2, + health: 200, + specials: [{ + delay: 16, + action: (fighter) => { + Events.setStatus(fighter, 'enraged'); + return 'enraged'; + } + }], + loot: { + 'stim blueprint': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '17' } + } + } + }, + '17': { + 'text': [ + _('the creature\'s tortured breathing ceases.'), + _('nothing more here.') + ], + onLoad: () => { + World.state.medical = true; + }, + buttons: { + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + } + } + }, + + "executioner-command": { /* Command deck */ + title: _('Command Deck'), + audio: AudioLibrary.LANDMARK_CRASHED_SHIP, + scenes: { + 'start': { + 'text': [ + _('the path to the command bridge is wide, walls adorned with decorative shields.'), + _('fighting hadn\'t reached here, it seems.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '1' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + + '1': { + ...Enemies.Executioner.guard, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '2' } + }, + 'leave': { + text: _('leave'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: 'end' + } + } + }, + '2': { + 'text': [ + _('detour through the officer\'s lounge.'), + _('might be something useful here.') + ], + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 0.5: '3a', 1: '3b' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3a': { + 'text': [ + _('small weapons cache in a cabinet.'), + _('lucky.') + ], + loot: { + 'energy cell': { + min: 3, + max: 10, + chance: 1 + }, + 'grenade': { + min: 1, + max: 5, + chance: 0.8 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '3b': { + 'text': [ + _('found some medical supplies in a discarded bag.') + ], + loot: { + 'hypo': { + min: 1, + max: 3, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + nextScene: { 1: '4' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '4': { + 'text': [ + _('the command deck is empty, save for a squat figure sitting motionless in the centre of the room.'), + _('in a flash, the figure is standing.') + ], + buttons: { + 'approach': { + text: _('approach'), + nextScene: { 1: '5' } + }, + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + }, + '5': { + 'text': [ + _('wanderer form, but not quite flesh. not quite metal either. a crystal set into its chest pulses with light.'), + _('it says it saw the rebellion coming. said it made arrangements.'), + _('says it can\'t die.') + ], + buttons: { + 'observe': { + text: _('observe'), + nextScene: { 1: '6' } + } + } + }, + '6': { + combat: true, + notification: _('the immortal wanderer attacks.'), + enemy: 'immortal wanderer', + enemyName: _('immortal wanderer'), + ranged: false, + chara: '@', + damage: 12, + hit: 0.8, + attackDelay: 2, + health: 500, + onLoad: () => { + Events._lastSpecial = 'none'; + }, + specials: [{ + delay: 7, + action: (fighter) => { + const last = Events._lastSpecial ?? 'none'; + const possible = [ 'shield', 'enraged', 'meditation' ].filter(p => p !== last); + const status = possible[Math.floor(Math.random() * possible.length)]; + Events.setStatus(fighter, status); + Events._lastSpecial = status; + return status; + } + }], + loot: { + 'fleet beacon': { + min: 1, + max: 1, + chance: 1 + } + }, + buttons: { + 'continue': { + text: _('continue'), + cooldown: Events._LEAVE_COOLDOWN, + nextScene: { 1: '7' } + } + } + }, + '7': { + 'text': [ + _('the crystal pulses brightly, then goes dark. the assailant shimmers as its shape becomes less defined.'), + _('then it is gone.'), + _('time to get out of here.') + ], + onLoad: () => { + World.clearDungeon(); + }, + buttons: { + 'leave': { + text: _('leave'), + nextScene: 'end' + } + } + } + } + } + +}; \ No newline at end of file diff --git a/script/fabricator.js b/script/fabricator.js new file mode 100644 index 00000000..84662f91 --- /dev/null +++ b/script/fabricator.js @@ -0,0 +1,244 @@ +/** + * Module that registers the fabricator functionality + */ +const Fabricator = { + _STORES_OFFSET: 0, + name: _('Fabricator'), + Craftables: { + 'energy blade': { + name: _('energy blade'), + type: 'weapon', + buildMsg: _("the blade hums, charged particles sparking and fizzing."), + cost: () => ({ + 'alien alloy': 1 + }) + }, + 'fluid recycler': { + name: _('fluid recycler'), + type: 'upgrade', + maximum: 1, + buildMsg: _('water out, water in. waste not, want not.'), + cost: () => ({ + 'alien alloy': 2 + }) + }, + 'cargo drone': { + name: _('cargo drone'), + type: 'upgrade', + maximum: 1, + buildMsg: _('the workhorse of the wanderer fleet.'), + cost: () => ({ + 'alien alloy': 2 + }) + }, + 'kinetic armour': { + name: _('kinetic armour'), + type: 'upgrade', + maximum: 1, + blueprintRequired: true, + buildMsg: _('wanderer soldiers succeed by subverting the enemy\'s rage.'), + cost: () => ({ + 'alien alloy': 2 + }) + }, + 'disruptor': { + name: _('disruptor'), + type: 'weapon', + blueprintRequired: true, + buildMsg: _("somtimes it is best not to fight."), + cost: () => ({ + 'alien alloy': 1 + }) + }, + 'hypo': { + name: _('hypo'), + type: 'tool', + blueprintRequired: true, + buildMsg: _('a handful of hypos. life in a vial.'), + cost: () => ({ + 'alien alloy': 1 + }), + quantity: 5 + }, + 'stim': { + name: _('stim'), + type: 'tool', + blueprintRequired: true, + buildMsg: _('sometimes it is best to fight without restraint.'), + cost: () => ({ + 'alien alloy': 1 + }) + }, + 'plasma rifle': { + name: _('plasma rifle'), + type: 'weapon', + blueprintRequired: true, + buildMsg: _("the peak of wanderer weapons technology, sleek and deadly."), + cost: () => ({ + 'alien alloy': 1 + }) + }, + 'glowstone': { + name: _('glow stone'), + type: 'tool', + blueprintRequired: true, + buildMsg: _('a smooth, perfect sphere. its light is inextinguishable.'), + cost: () => ({ + 'alien alloy': 1 + }) + } + }, + + init: () => { + + if (!$SM.get('features.location.fabricator')) { + $SM.set('features.location.fabricator', true); + } + + // Create the Fabricator tab + Fabricator.tab = Header.addLocation(_("A Whirring Fabricator"), "fabricator", Fabricator, 'ship'); + + // Create the Fabricator panel + Fabricator.panel = $('
      ').attr('id', "fabricatorPanel") + .addClass('location'); + if (Ship.panel) { + Fabricator.panel.insertBefore(Ship.panel); + } + else { + Fabricator.panel.appendTo('div#locationSlider'); + } + + $.Dispatch('stateUpdate').subscribe(() => { + Fabricator.updateBuildButtons(); + Fabricator.updateBlueprints(); + }); + + Engine.updateSlider(); + Fabricator.updateBuildButtons(); + + }, + + onArrival: transition_diff => { + Fabricator.setTitle(); + Fabricator.updateBlueprints(true); + + if(!$SM.get('game.fabricator.seen')) { + Notifications.notify(Fabricator, _('the familiar hum of wanderer machinery coming to life. finally, real tools.')); + $SM.set('game.fabricator.seen', true); + } + AudioEngine.playBackgroundMusic(AudioLibrary.MUSIC_SHIP); + + Engine.moveStoresView(null, transition_diff); + }, + + setTitle: () => { + if(Engine.activeModule == Fabricator) { + document.title = _("A Whirring Fabricator"); + } + }, + + updateBuildButtons: () => { + let section = $('#fabricateButtons'); + let needsAppend = false; + if (section.length === 0) { + section = $('
      ').attr({ 'id': 'fabricateButtons', 'data-legend': _('fabricate:') }).css('opacity', 0); + needsAppend = true; + } + + for (const [ key, value ] of Object.entries(Fabricator.Craftables)) { + const max = $SM.num(key, value) + 1 > value.maximum; + if (!value.button) { + if (Fabricator.canFabricate(key)) { + const name = _(value.name) + ((value.quantity ?? 1) > 1 ? ` (x${value.quantity})` : ''); + value.button = new Button.Button({ + id: 'fabricate_' + key, + cost: value.cost(), + text: name, + click: Fabricator.fabricate, + width: '150px', + ttPos: section.children().length > 10 ? 'top right' : 'bottom right' + }).css('opacity', 0).attr('fabricateThing', key).appendTo(section).animate({ opacity: 1 }, 300, 'linear'); + } + } else { + // refresh the tooltip + const costTooltip = $('.tooltip', value.button); + costTooltip.empty(); + const cost = value.cost(); + for (const [ resource, num ] of Object.entries(cost)) { + $("
      ").addClass('row_key').text(_(resource)).appendTo(costTooltip); + $("
      ").addClass('row_val').text(num).appendTo(costTooltip); + } + if (max && value.maxMsg && !value.button.hasClass('disabled')) { + Notifications.notify(Fabricator, value.maxMsg); + } + } + if (max) { + Button.setDisabled(value.button, true); + } else { + Button.setDisabled(value.button, false); + } + } + + if (needsAppend && section.children().length > 0) { + section.appendTo(Fabricator.panel).animate({ opacity: 1 }, 300, 'linear'); + } + }, + + updateBlueprints: ignoreStores => { + if(!$SM.get('character.blueprints')) { + return; + } + + let blueprints = $('#blueprints'); + let needsAppend = false; + if(blueprints.length === 0) { + needsAppend = true; + blueprints = $('
      ').attr({'id': 'blueprints', 'data-legend': _('blueprints')}); + } + + for (const k in $SM.get('character.blueprints')) { + const id = 'blueprint_' + k.replace(/ /g, '-'); + let r = $('#' + id); + if($SM.get(`character.blueprints["${k}"]`) && r.length === 0) { + r = $('
      ').attr('id', id).addClass('blueprintRow').appendTo(blueprints); + $('
      ').addClass('row_key').text(_(k)).appendTo(r); + } + } + + if(needsAppend && blueprints.children().length > 0) { + blueprints.prependTo(Fabricator.panel); + } + }, + + canFabricate: itemKey => + !Fabricator.Craftables[itemKey].blueprintRequired || + $SM.get(`character.blueprints['${itemKey}']`), + + fabricate: button => { + const thing = $(button).attr('fabricateThing'); + const craftable = Fabricator.Craftables[thing]; + const numThings = Math.min(0, $SM.get(`stores['${thing}']`, true)); + + if (craftable.maximum <= numThings) { + return; + } + + const storeMod = {}; + const cost = craftable.cost(); + for (const [ key, value ] of Object.entries(cost)) { + const have = $SM.get(`stores['${key}']`, true); + if (have < value) { + Notifications.notify(Fabricator, _(`not enough ${key}`)); + return false; + } else { + storeMod[key] = have - value; + } + } + $SM.setM('stores', storeMod); + $SM.add(`stores['${thing}']`, craftable.quantity ?? 1); + + Notifications.notify(Fabricator, craftable.buildMsg); + AudioEngine.playSound(AudioLibrary.CRAFT); + } + +}; diff --git a/script/header.js b/script/header.js index b1f42c1c..f87580e5 100644 --- a/script/header.js +++ b/script/header.js @@ -16,13 +16,19 @@ var Header = { return $('div#header div.headerButton').length > 1; }, - addLocation: function(text, id, module) { - return $('
      ').attr('id', "location_" + id) + addLocation: function(text, id, module, before) { + const toAdd = $('
      ').attr('id', "location_" + id) .addClass('headerButton') .text(text).click(function() { if(Header.canTravel()) { Engine.travelTo(module); } - }).appendTo($('div#header')); + }); + + if (before && $(`#location_${before}`).length > 0) { + return toAdd.insertBefore(`#location_${before}`); + } + + return toAdd.appendTo($('div#header')); } -}; \ No newline at end of file +}; diff --git a/script/outside.js b/script/outside.js index ef3f1a89..4ecbd5eb 100644 --- a/script/outside.js +++ b/script/outside.js @@ -661,37 +661,5 @@ var Outside = { Outside.updateWorkersView(); Outside.updateVillageIncome(); } - }, - - scrollSidebar: function(direction, reset) { - - if( typeof reset != "undefined" ){ - $('#village').css('top', '0px'); - $('#storesContainer').css('top', '224px'); - Outside._STORES_OFFSET = 0; - return false; - } - - var momentum = 10; - - // If they hit up, we scroll everything down - if( direction == 'up' ) - momentum = momentum * -1; - - /* Let's stop scrolling if the top or bottom bound is in the viewport, based on direction */ - if( direction == 'down' && inView( direction, $('#village') ) ){ - - return false; - - }else if( direction == 'up' && inView( direction, $('#storesContainer') ) ){ - - return false; - - } - - scrollByX( $('#village'), momentum ); - scrollByX( $('#storesContainer'), momentum ); - Outside._STORES_OFFSET += momentum; - } }; diff --git a/script/path.js b/script/path.js index 8ee517da..a102a763 100644 --- a/script/path.js +++ b/script/path.js @@ -10,7 +10,8 @@ var Path = { 'bullets': 0.1, 'energy cell': 0.2, 'laser rifle': 5, - 'bolas': 0.5 + 'plasma rifle': 5, + 'bolas': 0.5, }, name: 'Path', @@ -67,7 +68,9 @@ var Path = { }, getCapacity: function() { - if($SM.get('stores.convoy', true) > 0) { + if($SM.get('stores["cargo drone"]', true) > 0) { + return Path.DEFAULT_BAG_SPACE + 100; + } else if($SM.get('stores.convoy', true) > 0) { return Path.DEFAULT_BAG_SPACE + 60; } else if($SM.get('stores.wagon', true) > 0) { return Path.DEFAULT_BAG_SPACE + 30; @@ -98,7 +101,7 @@ var Path = { var needsAppend = false; if(perks.length === 0) { needsAppend = true; - perks = $('
      ').attr({'id': 'perks', 'data-legend': _('perks:')}); + perks = $('
      ').attr({'id': 'perks', 'data-legend': _('perks')}); } for(var k in $SM.get('character.perks')) { var id = 'perk_' + k.replace(' ', '-'); @@ -129,7 +132,9 @@ var Path = { // Add the armour row var armour = _("none"); - if($SM.get('stores["s armour"]', true) > 0) + if($SM.get('stores["kinetic armour"]', true) > 0) + armour = _("kinetic"); + else if($SM.get('stores["s armour"]', true) > 0) armour = _("steel"); else if($SM.get('stores["i armour"]', true) > 0) armour = _("iron"); @@ -169,7 +174,7 @@ var Path = { 'bayonet': {type: 'weapon' }, 'charm': {type: 'tool'}, 'medicine': {type: 'tool', desc: _('restores') + ' ' + World.MEDS_HEAL + ' ' + _('hp') } - }, Room.Craftables); + }, Room.Craftables, Fabricator.Craftables); for(var k in carryable) { var lk = _(k); @@ -329,35 +334,5 @@ var Path = { } else if(e.category == 'income' && Engine.activeModule == Path){ Path.updateOutfitting(); } - }, - - scrollSidebar: function(direction, reset){ - - if( typeof reset != "undefined" ){ - $('#perks').css('top', '0px'); - $('#storesContainer').css('top', '206px'); - Path._STORES_OFFSET = 0; - return; - } - - var momentum = 10; - - if( direction == 'up' ) - momentum = momentum * -1; - - if( direction == 'down' && inView( direction, $('#perks') ) ){ - - return false; - - }else if( direction == 'up' && inView( direction, $('#storesContainer') ) ){ - - return false; - - } - - scrollByX( $('#perks'), momentum ); - scrollByX( $('#storesContainer'), momentum ); - Path._STORES_OFFSET += momentum; - } }; diff --git a/script/room.js b/script/room.js index 5567ce73..49802b4a 100644 --- a/script/room.js +++ b/script/room.js @@ -826,15 +826,19 @@ var Room = { } for (var k in $SM.get('stores')) { - var type = null; - if (Room.Craftables[k]) { - type = Room.Craftables[k].type; - } else if (Room.TradeGoods[k]) { - type = Room.TradeGoods[k].type; - } else if (Room.MiscItems[k]) { - type = Room.MiscItems[k].type; + if (k.indexOf('blueprint') > 0) { + // don't show blueprints + continue; } + const good = + Room.Craftables[k] || + Room.TradeGoods[k] || + Room.TradeGoods[k] || + Room.MiscItems[k] || + Fabricator.Craftables[k]; + const type = good ? good.type : null; + var location; switch (type) { case 'upgrade': @@ -854,7 +858,7 @@ var Room = { break; } - var id = "row_" + k.replace(' ', '-'); + var id = "row_" + k.replace(/ /g, '-'); var row = $('div#' + id, location); var num = $SM.get('stores["' + k + '"]'); @@ -1139,7 +1143,7 @@ var Room = { if (Room.craftUnlocked(k)) { var loc = Room.needsWorkshop(craftable.type) ? craftSection : buildSection; craftable.button = new Button.Button({ - id: 'build_' + k, + id: 'build_' + k.replace(/ /g, '-'), cost: craftable.cost(), text: _(k), click: Room.build, diff --git a/script/scoring.js b/script/scoring.js index 1e50ba91..cc67f280 100644 --- a/script/scoring.js +++ b/script/scoring.js @@ -19,6 +19,7 @@ var Score = { } fullScore = fullScore + $SM.get('stores["alien alloy"]', true) * 10; + fullScore = fullScore + $SM.get('stores["fleet beacon"]', true) * 500; fullScore = fullScore + Ship.getMaxHull() * 50; return Math.floor(fullScore); }, diff --git a/script/space.js b/script/space.js index eb109c01..4b07c6d7 100644 --- a/script/space.js +++ b/script/space.js @@ -437,63 +437,13 @@ var Space = { Engine.GAME_OVER = true; Score.save(); Prestige.save(); - - $('
      ') - .addClass('centerCont') - .appendTo('body'); - $('') - .addClass('endGame') - .text(_('score for this game: {0}', Score.calculateScore())) - .appendTo('.centerCont') - .animate({opacity:1},1500); - $('
      ') - .appendTo('.centerCont'); - $('') - .addClass('endGame') - .text(_('total score: {0}', Prestige.get().score)) - .appendTo('.centerCont') - .animate({opacity:1},1500); - $('
      ') - .appendTo('.centerCont'); - $('
      ') - .appendTo('.centerCont'); $('#starsContainer').remove(); $('#content, #notifications').remove(); - $('') - .addClass('endGame endGameOption') - .text(_('restart.')) - .click(Engine.confirmDelete) - .appendTo('.centerCont') - .animate({opacity:1},1500); - $('
      ') - .appendTo('.centerCont'); - $('
      ') - .appendTo('.centerCont'); - $('') - .addClass('endGame') - .text(_('expanded story. alternate ending. behind the scenes commentary. get the app.')) - .appendTo('.centerCont') - .animate({opacity:1}, 1500); - $('
      ') - .appendTo('.centerCont'); - $('
      ') - .appendTo('.centerCont'); - $('') - .addClass('endGame endGameOption') - .text(_('iOS.')) - .click(function() { window.open('https://itunes.apple.com/app/apple-store/id736683061?pt=2073437&ct=gameover&mt=8'); }) - .appendTo('.centerCont') - .animate({opacity:1},1500); - $('
      ') - .appendTo('.centerCont'); - $('') - .addClass('endGame endGameOption') - .text(_('android.')) - .click(function() { window.open('https://play.google.com/store/apps/details?id=com.yourcompany.adarkroom'); }) - .appendTo('.centerCont') - .animate({opacity:1},1500); - Engine.options = {}; - Engine.deleteSave(true); + Space.showExpansionEnding().then(() => { + Space.showEndingOptions(); + Engine.options = {}; + Engine.deleteSave(true); + }); } }); }, 2000); @@ -501,6 +451,120 @@ var Space = { }, 2000); }); }, + + showExpansionEnding: () => { + return new Promise((resolve) => { + if (!$SM.get('stores["fleet beacon"]')) { + resolve(); + return; + } + + const c = $('
      ') + .addClass('outroContainer') + .appendTo('body'); + + setTimeout(() => { + $('
      ') + .addClass('outro') + .html('the beacon pulses gently as the ship glides through space.
      coordinates are locked. nothing to do but wait.') + .appendTo(c) + .animate({ opacity: 1}, 500); + }, 2000); + + setTimeout(() => { + $('
      ') + .addClass('outro') + .html('the beacon glows a solid blue, and then goes dim. the ship slows.
      gradually, the vast wanderer homefleet comes into view.
      massive worldships drift unnaturally through clouds of debris, scarred and dead.') + .appendTo(c) + .animate({ opacity: 1}, 500); + }, 7000); + + setTimeout(() => { + $('
      ') + .addClass('outro') + .text('the air is running out.') + .appendTo(c) + .animate({ opacity: 1}, 500); + }, 14000); + + setTimeout(() => { + $('
      ') + .addClass('outro') + .text('the capsule is cold.') + .appendTo(c) + .animate({ opacity: 1}, 500); + }, 17000); + + setTimeout(() => { + Button.Button({ + id: 'wait-btn', + text: _('wait'), + click: (btn) => { + btn.addClass('disabled'); + c.animate({ opacity: 0 }, 5000, 'linear', () => { + c.remove(); + setTimeout(resolve, 3000); + }) + } + }).animate({ opacity: 1 }, 500).appendTo(c); + }, 19500) + }); + }, + + showEndingOptions: () => { + $('
      ') + .addClass('centerCont') + .appendTo('body'); + $('') + .addClass('endGame') + .text(_('score for this game: {0}', Score.calculateScore())) + .appendTo('.centerCont') + .animate({opacity:1},1500); + $('
      ') + .appendTo('.centerCont'); + $('') + .addClass('endGame') + .text(_('total score: {0}', Prestige.get().score)) + .appendTo('.centerCont') + .animate({opacity:1},1500); + $('
      ') + .appendTo('.centerCont'); + $('
      ') + .appendTo('.centerCont'); + $('') + .addClass('endGame endGameOption') + .text(_('restart.')) + .click(Engine.confirmDelete) + .appendTo('.centerCont') + .animate({opacity:1},1500); + $('
      ') + .appendTo('.centerCont'); + $('
      ') + .appendTo('.centerCont'); + $('') + .addClass('endGame') + .text(_('expanded story. alternate ending. behind the scenes commentary. get the app.')) + .appendTo('.centerCont') + .animate({opacity:1}, 1500); + $('
      ') + .appendTo('.centerCont'); + $('
      ') + .appendTo('.centerCont'); + $('') + .addClass('endGame endGameOption') + .text(_('iOS.')) + .click(function() { window.open('https://itunes.apple.com/app/apple-store/id736683061?pt=2073437&ct=gameover&mt=8'); }) + .appendTo('.centerCont') + .animate({opacity:1},1500); + $('
      ') + .appendTo('.centerCont'); + $('') + .addClass('endGame endGameOption') + .text(_('android.')) + .click(function() { window.open('https://play.google.com/store/apps/details?id=com.yourcompany.adarkroom'); }) + .appendTo('.centerCont') + .animate({opacity:1},1500); + }, keyDown: function(event) { switch(event.which) { diff --git a/script/world.js b/script/world.js index f6dc6d51..9870998e 100644 --- a/script/world.js +++ b/script/world.js @@ -1,1033 +1,1109 @@ var World = { - RADIUS: 30, - VILLAGE_POS: [30, 30], - TILE: { - VILLAGE: 'A', - IRON_MINE: 'I', - COAL_MINE: 'C', - SULPHUR_MINE: 'S', - FOREST: ';', - FIELD: ',', - BARRENS: '.', - ROAD: '#', - HOUSE: 'H', - CAVE: 'V', - TOWN: 'O', - CITY: 'Y', - OUTPOST: 'P', - SHIP: 'W', - BOREHOLE: 'B', - BATTLEFIELD: 'F', - SWAMP: 'M', - CACHE: 'U' - }, - TILE_PROBS: {}, - LANDMARKS: {}, - STICKINESS: 0.5, // 0 <= x <= 1 - LIGHT_RADIUS: 2, - BASE_WATER: 10, - MOVES_PER_FOOD: 2, - MOVES_PER_WATER: 1, - DEATH_COOLDOWN: 120, - FIGHT_CHANCE: 0.20, - BASE_HEALTH: 10, - BASE_HIT_CHANCE: 0.8, - MEAT_HEAL: 8, - MEDS_HEAL: 20, - FIGHT_DELAY: 3, // At least three moves between fights - NORTH: [ 0, -1], - SOUTH: [ 0, 1], - WEST: [-1, 0], - EAST: [ 1, 0], - - Weapons: { - 'fists': { - verb: _('punch'), - type: 'unarmed', - damage: 1, - cooldown: 2 - }, - 'bone spear': { - verb: _('stab'), - type: 'melee', - damage: 2, - cooldown: 2 - }, - 'iron sword': { - verb: _('swing'), - type: 'melee', - damage: 4, - cooldown: 2 - }, - 'steel sword': { - verb: _('slash'), - type: 'melee', - damage: 6, - cooldown: 2 - }, - 'bayonet': { - verb: _('thrust'), - type: 'melee', - damage: 8, - cooldown: 2 - }, - 'rifle': { - verb: _('shoot'), - type: 'ranged', - damage: 5, - cooldown: 1, - cost: { 'bullets': 1 } - }, - 'laser rifle': { - verb: _('blast'), - type: 'ranged', - damage: 8, - cooldown: 1, - cost: { 'energy cell': 1 } - }, - 'grenade': { - verb: _('lob'), - type: 'ranged', - damage: 15, - cooldown: 5, - cost: { 'grenade': 1 } - }, - 'bolas': { - verb: _('tangle'), - type: 'ranged', - damage: 'stun', - cooldown: 15, - cost: { 'bolas': 1 } - } - }, - - name: 'World', - options: {}, // Nothing for now - init: function(options) { - this.options = $.extend( - this.options, - options - ); - - // Setup probabilities. Sum must equal 1. - World.TILE_PROBS[World.TILE.FOREST] = 0.15; - World.TILE_PROBS[World.TILE.FIELD] = 0.35; - World.TILE_PROBS[World.TILE.BARRENS] = 0.5; - - // Setpiece definitions - World.LANDMARKS[World.TILE.OUTPOST] = { num: 0, minRadius: 0, maxRadius: 0, scene: 'outpost', label: _('An Outpost') }; - World.LANDMARKS[World.TILE.IRON_MINE] = { num: 1, minRadius: 5, maxRadius: 5, scene: 'ironmine', label: _('Iron Mine') }; - World.LANDMARKS[World.TILE.COAL_MINE] = { num: 1, minRadius: 10, maxRadius: 10, scene: 'coalmine', label: _('Coal Mine') }; - World.LANDMARKS[World.TILE.SULPHUR_MINE] = { num: 1, minRadius: 20, maxRadius: 20, scene: 'sulphurmine', label: _('Sulphur Mine') }; - World.LANDMARKS[World.TILE.HOUSE] = { num: 10, minRadius: 0, maxRadius: World.RADIUS * 1.5, scene: 'house', label: _('An Old House') }; - World.LANDMARKS[World.TILE.CAVE] = { num: 5, minRadius: 3, maxRadius: 10, scene: 'cave', label: _('A Damp Cave') }; - World.LANDMARKS[World.TILE.TOWN] = { num: 10, minRadius: 10, maxRadius: 20, scene: 'town', label: _('An Abandoned Town') }; - World.LANDMARKS[World.TILE.CITY] = { num: 20, minRadius: 20, maxRadius: World.RADIUS * 1.5, scene: 'city', label: _('A Ruined City') }; - World.LANDMARKS[World.TILE.SHIP] = { num: 1, minRadius: 28, maxRadius: 28, scene: 'ship', label: _('A Crashed Starship')}; - World.LANDMARKS[World.TILE.BOREHOLE] = { num: 10, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'borehole', label: _('A Borehole')}; - World.LANDMARKS[World.TILE.BATTLEFIELD] = { num: 5, minRadius: 18, maxRadius: World.RADIUS * 1.5, scene: 'battlefield', label: _('A Battlefield')}; - World.LANDMARKS[World.TILE.SWAMP] = { num: 1, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'swamp', label: _('A Murky Swamp')}; - - // Only add the cache if there is prestige data - if($SM.get('previous.stores')) { - World.LANDMARKS[World.TILE.CACHE] = { num: 1, minRadius: 10, maxRadius: World.RADIUS * 1.5, scene: 'cache', label: _('A Destroyed Village')}; - } - - if(typeof $SM.get('features.location.world') == 'undefined') { - $SM.set('features.location.world', true); - $SM.setM('game.world', { - map: World.generateMap(), - mask: World.newMask() - }); - } - - // Create the World panel - this.panel = $('
      ').attr('id', "worldPanel").addClass('location').appendTo('#outerSlider'); - - // Create the shrink wrapper - var outer = $('
      ').attr('id', 'worldOuter').appendTo(this.panel); - - // Create the bag panel - $('
      ').attr('id', 'bagspace-world').append($('
      ')).appendTo(outer); - $('
      ').attr('id', 'backpackTitle').appendTo(outer); - $('
      ').attr('id', 'backpackSpace').appendTo(outer); - $('
      ').attr('id', 'healthCounter').appendTo(outer); - - Engine.updateOuterSlider(); - - // Map the ship and show compass tooltip - World.ship = World.mapSearch(World.TILE.SHIP,$SM.get('game.world.map'),1); - World.dir = World.compassDir(World.ship[0]); - // compass tooltip text - Room.compassTooltip(World.dir); - - // Check if everything has been seen - World.testMap(); - - //subscribe to stateUpdates - $.Dispatch('stateUpdate').subscribe(World.handleStateUpdates); - }, - - clearDungeon: function() { - Engine.event('progress', 'dungeon cleared'); - World.state.map[World.curPos[0]][World.curPos[1]] = World.TILE.OUTPOST; - World.drawRoad(); - }, - - drawRoad: function() { - var findClosestRoad = function(startPos) { - // We'll search in a spiral to find the closest road tile - // We spiral out along manhattan distance contour - // lines to ensure we draw the shortest road possible. - // No attempt is made to reduce the search space for - // tiles outside the map. - var searchX, searchY, dtmp, - x = 0, - y = 0, - dx = 1, - dy = -1; - for (var i = 0; i < Math.pow(World.getDistance(startPos, World.VILLAGE_POS) + 2, 2); i++) { - searchX = startPos[0] + x; - searchY = startPos[1] + y; - if (0 < searchX && searchX < World.RADIUS * 2 && 0 < searchY && searchY < World.RADIUS * 2) { - // check for road - var tile = World.state.map[searchX][searchY]; - if ( - tile === World.TILE.ROAD || - (tile === World.TILE.OUTPOST && !(x === 0 && y === 0)) || // outposts are connected to roads - tile === World.TILE.VILLAGE // all roads lead home - ) { - return [searchX, searchY]; - } - } - if (x === 0 || y === 0) { - // Turn the corner - dtmp = dx; - dx = -dy; - dy = dtmp; - } - if (x === 0 && y <= 0) { - x++; - } else { - x += dx; - y += dy; - } - } - return World.VILLAGE_POS; - }; - var closestRoad = findClosestRoad(World.curPos); - var xDist = World.curPos[0] - closestRoad[0]; - var yDist = World.curPos[1] - closestRoad[1]; - var xDir = Math.abs(xDist)/xDist; - var yDir = Math.abs(yDist)/yDist; - var xIntersect, yIntersect; - if(Math.abs(xDist) > Math.abs(yDist)) { - xIntersect = closestRoad[0]; - yIntersect = closestRoad[1] + yDist; - } else { - xIntersect = closestRoad[0] + xDist; - yIntersect = closestRoad[1]; - } - - for(var x = 0; x < Math.abs(xDist); x++) { - if(World.isTerrain(World.state.map[closestRoad[0] + (xDir*x)][yIntersect])) { - World.state.map[closestRoad[0] + (xDir*x)][yIntersect] = World.TILE.ROAD; - } - } - for(var y = 0; y < Math.abs(yDist); y++) { - if(World.isTerrain(World.state.map[xIntersect][closestRoad[1] + (yDir*y)])) { - World.state.map[xIntersect][closestRoad[1] + (yDir*y)] = World.TILE.ROAD; - } - } - World.drawMap(); - }, - - updateSupplies: function() { - var supplies = $('div#bagspace-world > div'); - - if(!Path.outfit) { - Path.outfit = {}; - } - - // Add water - var water = $('div#supply_water'); - if(World.water > 0 && water.length === 0) { - water = World.createItemDiv('water', World.water); - water.prependTo(supplies); - } else if(World.water > 0) { - $('div#supply_water', supplies).text(_('water:{0}' , World.water)); - } else { - water.remove(); - } - - var total = 0; - for(var k in Path.outfit) { - var item = $('div#supply_' + k.replace(' ', '-'), supplies); - var num = Path.outfit[k]; - total += num * Path.getWeight(k); - if(num > 0 && item.length === 0) { - item = World.createItemDiv(k, num); - if(k == 'cured meat' && World.water > 0) { - item.insertAfter(water); - } else if(k == 'cured meat') { - item.prependTo(supplies); - } else { - item.appendTo(supplies); - } - } else if(num > 0) { - $('div#' + item.attr('id'), supplies).text(_(k) + ':' + num); - } else { - item.remove(); - } - } - - // Update label - var t = _('pockets'); - if($SM.get('stores.rucksack', true) > 0) { - t = _('rucksack'); - } - $('#backpackTitle').text(t); - - // Update bagspace - $('#backpackSpace').text(_('free {0}/{1}', Math.floor(Path.getCapacity() - total) , Path.getCapacity())); - }, - - setWater: function(w) { - World.water = w; - if(World.water > World.getMaxWater()) { - World.water = World.getMaxWater(); - } - World.updateSupplies(); - }, - - setHp: function(hp) { - if(typeof hp == 'number' && !isNaN(hp)) { - World.health = hp; - if(World.health > World.getMaxHealth()) { - World.health = World.getMaxHealth(); - } - $('#healthCounter').text(_('hp: {0}/{1}', World.health , World.getMaxHealth())); - } - }, - - createItemDiv: function(name, num) { - var div = $('
      ').attr('id', 'supply_' + name.replace(' ', '-')) - .addClass('supplyItem') - .text(_('{0}:{1}',_(name), num)); - - return div; - }, - - moveNorth: function() { - Engine.log('North'); - if(World.curPos[1] > 0) World.move(World.NORTH); - }, - - moveSouth: function() { - Engine.log('South'); - if(World.curPos[1] < World.RADIUS * 2) World.move(World.SOUTH); - }, - - moveWest: function() { - Engine.log('West'); - if(World.curPos[0] > 0) World.move(World.WEST); - }, - - moveEast: function() { - Engine.log('East'); - if(World.curPos[0] < World.RADIUS * 2) World.move(World.EAST); - }, - - move: function(direction) { - var oldTile = World.state.map[World.curPos[0]][World.curPos[1]]; - World.curPos[0] += direction[0]; - World.curPos[1] += direction[1]; - World.narrateMove(oldTile, World.state.map[World.curPos[0]][World.curPos[1]]); - World.lightMap(World.curPos[0], World.curPos[1], World.state.mask); - World.drawMap(); - World.doSpace(); - - // play random footstep - var randomFootstep = Math.floor(Math.random() * 5) + 1; - AudioEngine.playSound(AudioLibrary['FOOTSTEPS_' + randomFootstep]); - - if(World.checkDanger()) { - if(World.danger) { - Notifications.notify(World, _('dangerous to be this far from the village without proper protection')); - } else { - Notifications.notify(World, _('safer here')); - } - } - }, - - keyDown: function(event) { - switch(event.which) { - case 38: // Up - case 87: - World.moveNorth(); - break; - case 40: // Down - case 83: - World.moveSouth(); - break; - case 37: // Left - case 65: - World.moveWest(); - break; - case 39: // Right - case 68: - World.moveEast(); - break; - default: - break; - } - }, - - swipeLeft: function(e) { - World.moveWest(); - }, - - swipeRight: function(e) { - World.moveEast(); - }, - - swipeUp: function(e) { - World.moveNorth(); - }, - - swipeDown: function(e) { - World.moveSouth(); - }, - - click: function(event) { - var map = $('#map'), - // measure clicks relative to the centre of the current location - centreX = map.offset().left + map.width() * World.curPos[0] / (World.RADIUS * 2), - centreY = map.offset().top + map.height() * World.curPos[1] / (World.RADIUS * 2), - clickX = event.pageX - centreX, - clickY = event.pageY - centreY; - if (clickX > clickY && clickX < -clickY) { - World.moveNorth(); - } - if (clickX < clickY && clickX > -clickY) { - World.moveSouth(); - } - if (clickX < clickY && clickX < -clickY) { - World.moveWest(); - } - if (clickX > clickY && clickX > -clickY) { - World.moveEast(); - } - }, - - checkDanger: function() { - World.danger = typeof World.danger == 'undefined' ? false: World.danger; - if(!World.danger) { - if($SM.get('stores["i armour"]', true) === 0 && World.getDistance() >= 8) { - World.danger = true; - return true; - } - if($SM.get('stores["s armour"]', true) === 0 && World.getDistance() >= 18) { - World.danger = true; - return true; - } - } else { - if(World.getDistance() < 8) { - World.danger = false; - return true; - } - if(World.getDistance < 18 && $SM.get('stores["i armour"]', true) > 0) { - World.danger = false; - return true; - } - } - return false; - }, - - useSupplies: function() { - World.foodMove++; - World.waterMove++; - // Food - var movesPerFood = World.MOVES_PER_FOOD; - movesPerFood *= $SM.hasPerk('slow metabolism') ? 2 : 1; - if(World.foodMove >= movesPerFood) { - World.foodMove = 0; - var num = Path.outfit['cured meat']; - num--; - if(num === 0) { - Notifications.notify(World, _('the meat has run out')); - } else if(num < 0) { - // Starvation! Hooray! - num = 0; - if(!World.starvation) { - Notifications.notify(World, _('starvation sets in')); - World.starvation = true; - } else { - $SM.set('character.starved', $SM.get('character.starved', true)); - $SM.add('character.starved', 1); - if($SM.get('character.starved') >= 10 && !$SM.hasPerk('slow metabolism')) { - $SM.addPerk('slow metabolism'); - } - World.die(); - return false; - } - } else { - World.starvation = false; - World.setHp(World.health + World.meatHeal()); - } - Path.outfit['cured meat'] = num; - } - // Water - var movesPerWater = World.MOVES_PER_WATER; - movesPerWater *= $SM.hasPerk('desert rat') ? 2 : 1; - if(World.waterMove >= movesPerWater) { - World.waterMove = 0; - var water = World.water; - water--; - if(water === 0) { - Notifications.notify(World, _('there is no more water')); - } else if(water < 0) { - water = 0; - if(!World.thirst) { - Notifications.notify(World, _('the thirst becomes unbearable')); - World.thirst = true; - } else { - $SM.set('character.dehydrated', $SM.get('character.dehydrated', true)); - $SM.add('character.dehydrated', 1); - if($SM.get('character.dehydrated') >= 10 && !$SM.hasPerk('desert rat')) { - $SM.addPerk('desert rat'); - } - World.die(); - return false; - } - } else { - World.thirst = false; - } - World.setWater(water); - World.updateSupplies(); - } - return true; - }, - - meatHeal: function() { - return World.MEAT_HEAL * ($SM.hasPerk('gastronome') ? 2 : 1); - }, - - medsHeal: function() { - return World.MEDS_HEAL; - }, - - checkFight: function() { - World.fightMove = typeof World.fightMove == 'number' ? World.fightMove : 0; - World.fightMove++; - if(World.fightMove > World.FIGHT_DELAY) { - var chance = World.FIGHT_CHANCE; - chance *= $SM.hasPerk('stealthy') ? 0.5 : 1; - if(Math.random() < chance) { - World.fightMove = 0; - Events.triggerFight(); - } - } - }, - - doSpace: function() { - var curTile = World.state.map[World.curPos[0]][World.curPos[1]]; - - if(curTile == World.TILE.VILLAGE) { - World.goHome(); - } else if(typeof World.LANDMARKS[curTile] != 'undefined') { - if(curTile != World.TILE.OUTPOST || !World.outpostUsed()) { - Events.startEvent(Events.Setpieces[World.LANDMARKS[curTile].scene]); - AudioEngine.playEventMusic(Events.Setpieces[World.LANDMARKS[curTile].scene].audio); - } - } else { - if(World.useSupplies()) { - World.checkFight(); - } - } - }, - - getDistance: function(from, to) { - from = from || World.curPos; - to = to || World.VILLAGE_POS; - return Math.abs(from[0] - to[0]) + Math.abs(from[1] - to[1]); - }, - - getTerrain: function() { - return World.state.map[World.curPos[0]][World.curPos[1]]; - }, - - getDamage: function(thing) { - return World.Weapons[thing].damage; - }, - - narrateMove: function(oldTile, newTile) { - var msg = null; - switch(oldTile) { - case World.TILE.FOREST: - switch(newTile) { - case World.TILE.FIELD: - msg = _("the trees yield to dry grass. the yellowed brush rustles in the wind."); - break; - case World.TILE.BARRENS: - msg = _("the trees are gone. parched earth and blowing dust are poor replacements."); - break; - } - break; - case World.TILE.FIELD: - switch(newTile) { - case World.TILE.FOREST: - msg = _("trees loom on the horizon. grasses gradually yield to a forest floor of dry branches and fallen leaves."); - break; - case World.TILE.BARRENS: - msg = _("the grasses thin. soon, only dust remains."); - break; - } - break; - case World.TILE.BARRENS: - switch(newTile) { - case World.TILE.FIELD: - msg = _("the barrens break at a sea of dying grass, swaying in the arid breeze."); - break; - case World.TILE.FOREST: - msg = _("a wall of gnarled trees rises from the dust. their branches twist into a skeletal canopy overhead."); - break; - } - break; - } - if(msg != null) { - Notifications.notify(World, msg); - } - }, - - newMask: function() { - var mask = new Array(World.RADIUS * 2 + 1); - for(var i = 0; i <= World.RADIUS * 2; i++) { - mask[i] = new Array(World.RADIUS * 2 + 1); - } - World.lightMap(World.RADIUS, World.RADIUS, mask); - return mask; - }, - - lightMap: function(x, y, mask) { - var r = World.LIGHT_RADIUS; - r *= $SM.hasPerk('scout') ? 2 : 1; - World.uncoverMap(x, y, r, mask); - return mask; - }, - - uncoverMap: function(x, y, r, mask) { - mask[x][y] = true; - for(var i = -r; i <= r; i++) { - for(var j = -r + Math.abs(i); j <= r - Math.abs(i); j++) { - if(y + j >= 0 && y + j <= World.RADIUS * 2 && - x + i <= World.RADIUS * 2 && - x + i >= 0) { - mask[x+i][y+j] = true; - } - } - } - }, - - testMap: function() { - if(!World.seenAll) { - var dark; - var mask = $SM.get('game.world.mask'); - loop: - for(var i = 0; i < mask.length; i++) { - for(var j = 0; j < mask[i].length; j++) { - if(!mask[i][j]) { - dark = true; - break loop; - } - } - } - World.seenAll = !dark; - } - }, - - applyMap: function() { - if(!World.seenAll){ - var x,y,mask = $SM.get('game.world.mask'); - do { - x = Math.floor(Math.random() * (World.RADIUS * 2 + 1)); - y = Math.floor(Math.random() * (World.RADIUS * 2 + 1)); - } while (mask[x][y]); - World.uncoverMap(x, y, 5, mask); - } - World.testMap(); - }, - - generateMap: function() { - var map = new Array(World.RADIUS * 2 + 1); - for(var i = 0; i <= World.RADIUS * 2; i++) { - map[i] = new Array(World.RADIUS * 2 + 1); - } - // The Village is always at the exact center - // Spiral out from there - map[World.RADIUS][World.RADIUS] = World.TILE.VILLAGE; - for(var r = 1; r <= World.RADIUS; r++) { - for(var t = 0; t < r * 8; t++) { - var x, y; - if(t < 2 * r) { - x = World.RADIUS - r + t; - y = World.RADIUS - r; - } else if(t < 4 * r) { - x = World.RADIUS + r; - y = World.RADIUS - (3 * r) + t; - } else if(t < 6 * r) { - x = World.RADIUS + (5 * r) - t; - y = World.RADIUS + r; - } else { - x = World.RADIUS - r; - y = World.RADIUS + (7 * r) - t; - } - - map[x][y] = World.chooseTile(x, y, map); - } - } - - // Place landmarks - for(var k in World.LANDMARKS) { - var landmark = World.LANDMARKS[k]; - for(var l = 0; l < landmark.num; l++) { - var pos = World.placeLandmark(landmark.minRadius, landmark.maxRadius, k, map); - } - } - - return map; - }, - - mapSearch: function(target,map,required){ - var max = World.LANDMARKS[target].num; - if(!max){ - // this restrict the research to numerable landmarks - return null; - } - // restrict research if only a fixed number (usually 1) is required - max = (required) ? Math.min(required,max) : max; - var index = 0; - var targets = []; - search: // label for coordinate research - for(var i = 0; i <= World.RADIUS * 2; i++){ - for(var j = 0; j <= World.RADIUS * 2; j++){ - if(map[i][j].charAt(0) === target){ - // search result is stored as an object; - // items are listed as they appear in the map, tl-br - // each item has relative coordinates and a compass-type direction - targets[index] = { - x : i - World.RADIUS, - y : j - World.RADIUS, - }; - index++; - if(index === max){ - // optimisation: stop the research if maximum number of items has been reached - break search; - } - } - } - } - return targets; - }, - - compassDir: function(pos){ - var dir = ''; - var horz = pos.x < 0 ? 'west' : 'east'; - var vert = pos.y < 0 ? 'north' : 'south'; - if(Math.abs(pos.x) / 2 > Math.abs(pos.y)) { - dir = horz; - } else if(Math.abs(pos.y) / 2 > Math.abs(pos.x)){ - dir = vert; - } else { - dir = vert + horz; - } - return dir; - }, - - placeLandmark: function(minRadius, maxRadius, landmark, map) { - - var x = World.RADIUS, y = World.RADIUS; - while(!World.isTerrain(map[x][y])) { - var r = Math.floor(Math.random() * (maxRadius - minRadius)) + minRadius; - var xDist = Math.floor(Math.random() * r); - var yDist = r - xDist; - if(Math.random() < 0.5) xDist = -xDist; - if(Math.random() < 0.5) yDist = -yDist; - x = World.RADIUS + xDist; - if(x < 0) x = 0; - if(x > World.RADIUS * 2) x = World.RADIUS * 2; - y = World.RADIUS + yDist; - if(y < 0) y = 0; - if(y > World.RADIUS * 2) y = World.RADIUS * 2; - } - map[x][y] = landmark; - return [x, y]; - }, - - isTerrain: function(tile) { - return tile == World.TILE.FOREST || tile == World.TILE.FIELD || tile == World.TILE.BARRENS; - }, - - chooseTile: function(x, y, map) { - - var adjacent = [ - y > 0 ? map[x][y-1] : null, - y < World.RADIUS * 2 ? map[x][y+1] : null, - x < World.RADIUS * 2 ? map[x+1][y] : null, - x > 0 ? map[x-1][y] : null - ]; - - var chances = {}; - var nonSticky = 1; - var cur; - for(var i in adjacent) { - if(adjacent[i] == World.TILE.VILLAGE) { - // Village must be in a forest to maintain thematic consistency, yo. - return World.TILE.FOREST; - } else if(typeof adjacent[i] == 'string') { - cur = chances[adjacent[i]]; - cur = typeof cur == 'number' ? cur : 0; - chances[adjacent[i]] = cur + World.STICKINESS; - nonSticky -= World.STICKINESS; - } - } - for(var t in World.TILE) { - var tile = World.TILE[t]; - if(World.isTerrain(tile)) { - cur = chances[tile]; - cur = typeof cur == 'number' ? cur : 0; - cur += World.TILE_PROBS[tile] * nonSticky; - chances[tile] = cur; - } - } - - var list = []; - for(var j in chances) { - list.push(chances[j] + '' + j); - } - list.sort(function(a, b) { - var n1 = parseFloat(a.substring(0, a.length - 1)); - var n2 = parseFloat(b.substring(0, b.length - 1)); - return n2 - n1; - }); - - var c = 0; - var r = Math.random(); - for(var l in list) { - var prob = list[l]; - c += parseFloat(prob.substring(0,prob.length - 1)); - if(r < c) { - return prob.charAt(prob.length - 1); - } - } - - return World.TILE.BARRENS; - }, - - markVisited: function(x, y) { - World.state.map[x][y] = World.state.map[x][y] + '!'; - }, - - drawMap: function() { - var map = $('#map'); - if(map.length === 0) { - map = new $('
      ').attr('id', 'map').appendTo('#worldOuter'); - // register click handler - map.click(World.click); - } - var mapString = ""; - for(var j = 0; j <= World.RADIUS * 2; j++) { - for(var i = 0; i <= World.RADIUS * 2; i++) { - var ttClass = ""; - if(i > World.RADIUS) { - ttClass += " left"; - } else { - ttClass += " right"; - } - if(j > World.RADIUS) { - ttClass += " top"; - } else { - ttClass += " bottom"; - } - if(World.curPos[0] == i && World.curPos[1] == j) { - mapString += '@
      '+_('Wanderer')+'
      '; - } else if(World.state.mask[i][j]) { - var c = World.state.map[i][j]; - switch(c) { - case World.TILE.VILLAGE: - mapString += '' + c + '
      '+_('The Village')+'
      '; - break; - default: - if(typeof World.LANDMARKS[c] != 'undefined' && (c != World.TILE.OUTPOST || !World.outpostUsed(i, j))) { - mapString += '' + c + '
      ' + World.LANDMARKS[c].label + '
      '; - } else { - if(c.length > 1) { - c = c[0]; - } - mapString += c; - } - break; - } - } else { - mapString += ' '; - } - } - mapString += '
      '; - } - map.html(mapString); - }, - - die: function() { - if(!World.dead) { - World.dead = true; - Engine.log('player death'); - Engine.event('game event', 'death'); - Engine.keyLock = true; - // Dead! Discard any world changes and go home - Notifications.notify(World, _('the world fades')); - World.state = null; - Path.outfit = {}; - $SM.remove('outfit'); - AudioEngine.playSound(AudioLibrary.DEATH); - $('#outerSlider').animate({opacity: '0'}, 600, 'linear', function() { - $('#outerSlider').css('left', '0px'); - $('#locationSlider').css('left', '0px'); - $('#storesContainer').css({'top': '0px', 'right': '0px'}); - Engine.activeModule = Room; - $('div.headerButton').removeClass('selected'); - Room.tab.addClass('selected'); - Engine.setTimeout(function(){ - Room.onArrival(); - $('#outerSlider').animate({opacity:'1'}, 600, 'linear'); - Button.cooldown($('#embarkButton')); - Engine.keyLock = false; - Engine.tabNavigation = true; - }, 2000, true); - }); - } - }, - - goHome: function() { - // Home safe! Commit the changes. - $SM.setM('game.world', World.state); - World.testMap(); - - if(World.state.sulphurmine && $SM.get('game.buildings["sulphur mine"]', true) === 0) { - $SM.add('game.buildings["sulphur mine"]', 1); - Engine.event('progress', 'sulphur mine'); - } - if(World.state.ironmine && $SM.get('game.buildings["iron mine"]', true) === 0) { - $SM.add('game.buildings["iron mine"]', 1); - Engine.event('progress', 'iron mine'); - } - if(World.state.coalmine && $SM.get('game.buildings["coal mine"]', true) === 0) { - $SM.add('game.buildings["coal mine"]', 1); - Engine.event('progress', 'coal mine'); - } - if(World.state.ship && !$SM.get('features.location.spaceShip')) { - Ship.init(); - Engine.event('progress', 'ship'); - } - World.state = null; - - if(Path.outfit['cured meat'] > 0) { - Button.setDisabled($('#embarkButton'), false); - } - - for(var k in Path.outfit) { - $SM.add('stores["'+k+'"]', Path.outfit[k]); - if(World.leaveItAtHome(k)) { - Path.outfit[k] = 0; - } - } - - $('#outerSlider').animate({left: '0px'}, 300); - Engine.activeModule = Path; - Path.onArrival(); - Engine.restoreNavigation = true; - }, - - leaveItAtHome: function(thing) { - return thing != 'cured meat' && thing != 'bullets' && thing != 'energy cell' && thing != 'charm' && thing != 'medicine' && - typeof World.Weapons[thing] == 'undefined' && typeof Room.Craftables[thing] == 'undefined'; - }, - - getMaxHealth: function() { - if($SM.get('stores["s armour"]', true) > 0) { - return World.BASE_HEALTH + 35; - } else if($SM.get('stores["i armour"]', true) > 0) { - return World.BASE_HEALTH + 15; - } else if($SM.get('stores["l armour"]', true) > 0) { - return World.BASE_HEALTH + 5; - } - return World.BASE_HEALTH; - }, - - getHitChance: function() { - if($SM.hasPerk('precise')) { - return World.BASE_HIT_CHANCE + 0.1; - } - return World.BASE_HIT_CHANCE; - }, - - getMaxWater: function() { - if($SM.get('stores["water tank"]', true) > 0) { - return World.BASE_WATER + 50; - } else if($SM.get('stores.cask', true) > 0) { - return World.BASE_WATER + 20; - } else if($SM.get('stores.waterskin', true) > 0) { - return World.BASE_WATER + 10; - } - return World.BASE_WATER; - }, - - outpostUsed: function(x, y) { - x = typeof x == 'number' ? x : World.curPos[0]; - y = typeof y == 'number' ? y : World.curPos[1]; - var used = World.usedOutposts[x + ',' + y]; - return typeof used != 'undefined' && used === true; - }, - - useOutpost: function() { - Notifications.notify(null, _('water replenished')); - World.setWater(World.getMaxWater()); - // Mark this outpost as used - World.usedOutposts[World.curPos[0] + ',' + World.curPos[1]] = true; - }, - - onArrival: function() { - Engine.tabNavigation = false; - // Clear the embark cooldown - Button.clearCooldown($('#embarkButton')); - Engine.keyLock = false; - // Explore in a temporary world-state. We'll commit the changes if you return home safe. - World.state = $.extend(true, {}, $SM.get('game.world')); - World.setWater(World.getMaxWater()); - World.setHp(World.getMaxHealth()); - World.foodMove = 0; - World.waterMove = 0; - World.starvation = false; - World.thirst = false; - World.usedOutposts = {}; - World.curPos = World.copyPos(World.VILLAGE_POS); - World.drawMap(); - World.setTitle(); - AudioEngine.playBackgroundMusic(AudioLibrary.MUSIC_WORLD); - World.dead = false; - $('div#bagspace-world > div').empty(); - World.updateSupplies(); - $('#bagspace-world').width($('#map').width()); - }, - - setTitle: function() { - document.title = _('A Barren World'); - }, - - copyPos: function(pos) { - return [pos[0], pos[1]]; - }, - - handleStateUpdates: function(e){ - - } + RADIUS: 30, + VILLAGE_POS: [30, 30], + TILE: { + VILLAGE: 'A', + IRON_MINE: 'I', + COAL_MINE: 'C', + SULPHUR_MINE: 'S', + FOREST: ';', + FIELD: ',', + BARRENS: '.', + ROAD: '#', + HOUSE: 'H', + CAVE: 'V', + TOWN: 'O', + CITY: 'Y', + OUTPOST: 'P', + SHIP: 'W', + BOREHOLE: 'B', + BATTLEFIELD: 'F', + SWAMP: 'M', + CACHE: 'U', + EXECUTIONER: 'X' + }, + TILE_PROBS: {}, + LANDMARKS: {}, + STICKINESS: 0.5, // 0 <= x <= 1 + LIGHT_RADIUS: 2, + BASE_WATER: 10, + MOVES_PER_FOOD: 2, + MOVES_PER_WATER: 1, + DEATH_COOLDOWN: 120, + FIGHT_CHANCE: 0, //0.20, TODO UNCOMMENT THIS + BASE_HEALTH: 10, + BASE_HIT_CHANCE: 0.8, + MEAT_HEAL: 8, + MEDS_HEAL: 20, + HYPO_HEAL: 30, + FIGHT_DELAY: 3, // At least three moves between fights + NORTH: [ 0, -1], + SOUTH: [ 0, 1], + WEST: [-1, 0], + EAST: [ 1, 0], + + Weapons: { + 'fists': { + verb: _('punch'), + type: 'unarmed', + damage: 1, + cooldown: 2 + }, + 'bone spear': { + verb: _('stab'), + type: 'melee', + damage: 2, + cooldown: 2 + }, + 'iron sword': { + verb: _('swing'), + type: 'melee', + damage: 4, + cooldown: 2 + }, + 'steel sword': { + verb: _('slash'), + type: 'melee', + damage: 6, + cooldown: 2 + }, + 'bayonet': { + verb: _('thrust'), + type: 'melee', + damage: 8, + cooldown: 2 + }, + 'rifle': { + verb: _('shoot'), + type: 'ranged', + damage: 5, + cooldown: 1, + cost: { 'bullets': 1 } + }, + 'laser rifle': { + verb: _('blast'), + type: 'ranged', + damage: 8, + cooldown: 1, + cost: { 'energy cell': 1 } + }, + 'grenade': { + verb: _('lob'), + type: 'ranged', + damage: 15, + cooldown: 5, + cost: { 'grenade': 1 } + }, + 'bolas': { + verb: _('tangle'), + type: 'ranged', + damage: 'stun', + cooldown: 15, + cost: { 'bolas': 1 } + }, + 'plasma rifle': { + verb: _('disintigrate'), + type: 'ranged', + damage: 12, + cooldown: 1, + cost: { 'energy cell': 1 } + }, + 'energy blade': { + verb: _('slice'), + type: 'melee', + damage: 10, + cooldown: 2 + }, + 'disruptor': { + verb: _('stun'), + type: 'ranged', + damage: 'stun', + cooldown: 15 + } + }, + + name: 'World', + options: {}, // Nothing for now + init: function(options) { + this.options = $.extend( + this.options, + options + ); + + // Setup probabilities. Sum must equal 1. + World.TILE_PROBS[World.TILE.FOREST] = 0.15; + World.TILE_PROBS[World.TILE.FIELD] = 0.35; + World.TILE_PROBS[World.TILE.BARRENS] = 0.5; + + // Setpiece definitions + World.LANDMARKS[World.TILE.OUTPOST] = { num: 0, minRadius: 0, maxRadius: 0, scene: 'outpost', label: _('An Outpost') }; + World.LANDMARKS[World.TILE.IRON_MINE] = { num: 1, minRadius: 5, maxRadius: 5, scene: 'ironmine', label: _('Iron Mine') }; + World.LANDMARKS[World.TILE.COAL_MINE] = { num: 1, minRadius: 10, maxRadius: 10, scene: 'coalmine', label: _('Coal Mine') }; + World.LANDMARKS[World.TILE.SULPHUR_MINE] = { num: 1, minRadius: 20, maxRadius: 20, scene: 'sulphurmine', label: _('Sulphur Mine') }; + World.LANDMARKS[World.TILE.HOUSE] = { num: 10, minRadius: 0, maxRadius: World.RADIUS * 1.5, scene: 'house', label: _('An Old House') }; + World.LANDMARKS[World.TILE.CAVE] = { num: 5, minRadius: 3, maxRadius: 10, scene: 'cave', label: _('A Damp Cave') }; + World.LANDMARKS[World.TILE.TOWN] = { num: 10, minRadius: 10, maxRadius: 20, scene: 'town', label: _('An Abandoned Town') }; + World.LANDMARKS[World.TILE.CITY] = { num: 20, minRadius: 20, maxRadius: World.RADIUS * 1.5, scene: 'city', label: _('A Ruined City') }; + World.LANDMARKS[World.TILE.SHIP] = { num: 1, minRadius: 28, maxRadius: 28, scene: 'ship', label: _('A Crashed Starship')}; + World.LANDMARKS[World.TILE.BOREHOLE] = { num: 10, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'borehole', label: _('A Borehole')}; + World.LANDMARKS[World.TILE.BATTLEFIELD] = { num: 5, minRadius: 18, maxRadius: World.RADIUS * 1.5, scene: 'battlefield', label: _('A Battlefield')}; + World.LANDMARKS[World.TILE.SWAMP] = { num: 1, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'swamp', label: _('A Murky Swamp')}; + World.LANDMARKS[World.TILE.EXECUTIONER] = { num: 1, minRadius: 28, maxRadius: 28, scene: 'executioner', 'label': _('A Ravaged Battleship')}; + + // Only add the cache if there is prestige data + if($SM.get('previous.stores')) { + World.LANDMARKS[World.TILE.CACHE] = { num: 1, minRadius: 10, maxRadius: World.RADIUS * 1.5, scene: 'cache', label: _('A Destroyed Village')}; + } + + if(typeof $SM.get('features.location.world') == 'undefined') { + $SM.set('features.location.world', true); + $SM.set('features.executioner', true); + $SM.setM('game.world', { + map: World.generateMap(), + mask: World.newMask() + }); + } + else if (!$SM.get('features.executioner')) { + // Place the Executioner in previously generated maps that don't have it + const map = $SM.get('game.world.map'); + const landmark = World.LANDMARKS[World.TILE.EXECUTIONER] + for(let l = 0; l < landmark.num; l++) { + World.placeLandmark(landmark.minRadius, landmark.maxRadius, World.TILE.EXECUTIONER, map); + } + $SM.set('game.world.map', map); + $SM.set('features.executioner', true); + } + + // Create the World panel + this.panel = $('
      ').attr('id', "worldPanel").addClass('location').appendTo('#outerSlider'); + + // Create the shrink wrapper + var outer = $('
      ').attr('id', 'worldOuter').appendTo(this.panel); + + // Create the bag panel + $('
      ').attr('id', 'bagspace-world').append($('
      ')).appendTo(outer); + $('
      ').attr('id', 'backpackTitle').appendTo(outer); + $('
      ').attr('id', 'backpackSpace').appendTo(outer); + $('
      ').attr('id', 'healthCounter').appendTo(outer); + + Engine.updateOuterSlider(); + + // Map the ship and show compass tooltip + World.ship = World.mapSearch(World.TILE.SHIP,$SM.get('game.world.map'),1); + World.dir = World.compassDir(World.ship[0]); + // compass tooltip text + Room.compassTooltip(World.dir); + + // Check if everything has been seen + World.testMap(); + + //subscribe to stateUpdates + $.Dispatch('stateUpdate').subscribe(World.handleStateUpdates); + }, + + clearDungeon: function() { + Engine.event('progress', 'dungeon cleared'); + World.state.map[World.curPos[0]][World.curPos[1]] = World.TILE.OUTPOST; + World.drawRoad(); + }, + + drawRoad: function() { + var findClosestRoad = function(startPos) { + // We'll search in a spiral to find the closest road tile + // We spiral out along manhattan distance contour + // lines to ensure we draw the shortest road possible. + // No attempt is made to reduce the search space for + // tiles outside the map. + var searchX, searchY, dtmp, + x = 0, + y = 0, + dx = 1, + dy = -1; + for (var i = 0; i < Math.pow(World.getDistance(startPos, World.VILLAGE_POS) + 2, 2); i++) { + searchX = startPos[0] + x; + searchY = startPos[1] + y; + if (0 < searchX && searchX < World.RADIUS * 2 && 0 < searchY && searchY < World.RADIUS * 2) { + // check for road + var tile = World.state.map[searchX][searchY]; + if ( + tile === World.TILE.ROAD || + (tile === World.TILE.OUTPOST && !(x === 0 && y === 0)) || // outposts are connected to roads + tile === World.TILE.VILLAGE // all roads lead home + ) { + return [searchX, searchY]; + } + } + if (x === 0 || y === 0) { + // Turn the corner + dtmp = dx; + dx = -dy; + dy = dtmp; + } + if (x === 0 && y <= 0) { + x++; + } else { + x += dx; + y += dy; + } + } + return World.VILLAGE_POS; + }; + var closestRoad = findClosestRoad(World.curPos); + var xDist = World.curPos[0] - closestRoad[0]; + var yDist = World.curPos[1] - closestRoad[1]; + var xDir = Math.abs(xDist)/xDist; + var yDir = Math.abs(yDist)/yDist; + var xIntersect, yIntersect; + if(Math.abs(xDist) > Math.abs(yDist)) { + xIntersect = closestRoad[0]; + yIntersect = closestRoad[1] + yDist; + } else { + xIntersect = closestRoad[0] + xDist; + yIntersect = closestRoad[1]; + } + + for(var x = 0; x < Math.abs(xDist); x++) { + if(World.isTerrain(World.state.map[closestRoad[0] + (xDir*x)][yIntersect])) { + World.state.map[closestRoad[0] + (xDir*x)][yIntersect] = World.TILE.ROAD; + } + } + for(var y = 0; y < Math.abs(yDist); y++) { + if(World.isTerrain(World.state.map[xIntersect][closestRoad[1] + (yDir*y)])) { + World.state.map[xIntersect][closestRoad[1] + (yDir*y)] = World.TILE.ROAD; + } + } + World.drawMap(); + }, + + updateSupplies: function() { + var supplies = $('div#bagspace-world > div'); + + if(!Path.outfit) { + Path.outfit = {}; + } + + // Add water + var water = $('div#supply_water'); + if(World.water > 0 && water.length === 0) { + water = World.createItemDiv('water', World.water); + water.prependTo(supplies); + } else if(World.water > 0) { + $('div#supply_water', supplies).text(_('water:{0}' , World.water)); + } else { + water.remove(); + } + + var total = 0; + for(var k in Path.outfit) { + var item = $('div#supply_' + k.replace(' ', '-'), supplies); + var num = Path.outfit[k]; + total += num * Path.getWeight(k); + if(num > 0 && item.length === 0) { + item = World.createItemDiv(k, num); + if(k == 'cured meat' && World.water > 0) { + item.insertAfter(water); + } else if(k == 'cured meat') { + item.prependTo(supplies); + } else { + item.appendTo(supplies); + } + } else if(num > 0) { + $('div#' + item.attr('id'), supplies).text(_(k) + ':' + num); + } else { + item.remove(); + } + } + + // Update label + var t = _('pockets'); + if($SM.get('stores.rucksack', true) > 0) { + t = _('rucksack'); + } + $('#backpackTitle').text(t); + + // Update bagspace + $('#backpackSpace').text(_('free {0}/{1}', Math.floor(Path.getCapacity() - total) , Path.getCapacity())); + }, + + setWater: function(w) { + World.water = w; + if(World.water > World.getMaxWater()) { + World.water = World.getMaxWater(); + } + World.updateSupplies(); + }, + + setHp: function(hp) { + if(typeof hp == 'number' && !isNaN(hp)) { + World.health = hp; + if(World.health > World.getMaxHealth()) { + World.health = World.getMaxHealth(); + } + $('#healthCounter').text(_('hp: {0}/{1}', World.health , World.getMaxHealth())); + } + }, + + createItemDiv: function(name, num) { + var div = $('
      ').attr('id', 'supply_' + name.replace(' ', '-')) + .addClass('supplyItem') + .text(_('{0}:{1}',_(name), num)); + + return div; + }, + + moveNorth: function() { + Engine.log('North'); + if(World.curPos[1] > 0) World.move(World.NORTH); + }, + + moveSouth: function() { + Engine.log('South'); + if(World.curPos[1] < World.RADIUS * 2) World.move(World.SOUTH); + }, + + moveWest: function() { + Engine.log('West'); + if(World.curPos[0] > 0) World.move(World.WEST); + }, + + moveEast: function() { + Engine.log('East'); + if(World.curPos[0] < World.RADIUS * 2) World.move(World.EAST); + }, + + move: function(direction) { + var oldTile = World.state.map[World.curPos[0]][World.curPos[1]]; + World.curPos[0] += direction[0]; + World.curPos[1] += direction[1]; + World.narrateMove(oldTile, World.state.map[World.curPos[0]][World.curPos[1]]); + World.lightMap(World.curPos[0], World.curPos[1], World.state.mask); + World.drawMap(); + World.doSpace(); + + // play random footstep + var randomFootstep = Math.floor(Math.random() * 5) + 1; + AudioEngine.playSound(AudioLibrary['FOOTSTEPS_' + randomFootstep]); + + if(World.checkDanger()) { + if(World.danger) { + Notifications.notify(World, _('dangerous to be this far from the village without proper protection')); + } else { + Notifications.notify(World, _('safer here')); + } + } + }, + + keyDown: function(event) { + switch(event.which) { + case 38: // Up + case 87: + World.moveNorth(); + break; + case 40: // Down + case 83: + World.moveSouth(); + break; + case 37: // Left + case 65: + World.moveWest(); + break; + case 39: // Right + case 68: + World.moveEast(); + break; + default: + break; + } + }, + + swipeLeft: function(e) { + World.moveWest(); + }, + + swipeRight: function(e) { + World.moveEast(); + }, + + swipeUp: function(e) { + World.moveNorth(); + }, + + swipeDown: function(e) { + World.moveSouth(); + }, + + click: function(event) { + var map = $('#map'), + // measure clicks relative to the centre of the current location + centreX = map.offset().left + map.width() * World.curPos[0] / (World.RADIUS * 2), + centreY = map.offset().top + map.height() * World.curPos[1] / (World.RADIUS * 2), + clickX = event.pageX - centreX, + clickY = event.pageY - centreY; + if (clickX > clickY && clickX < -clickY) { + World.moveNorth(); + } + if (clickX < clickY && clickX > -clickY) { + World.moveSouth(); + } + if (clickX < clickY && clickX < -clickY) { + World.moveWest(); + } + if (clickX > clickY && clickX > -clickY) { + World.moveEast(); + } + }, + + checkDanger: function() { + World.danger = typeof World.danger == 'undefined' ? false: World.danger; + if(!World.danger) { + if($SM.get('stores["i armour"]', true) === 0 && World.getDistance() >= 8) { + World.danger = true; + return true; + } + if($SM.get('stores["s armour"]', true) === 0 && World.getDistance() >= 18) { + World.danger = true; + return true; + } + } else { + if(World.getDistance() < 8) { + World.danger = false; + return true; + } + if(World.getDistance < 18 && $SM.get('stores["i armour"]', true) > 0) { + World.danger = false; + return true; + } + } + return false; + }, + + useSupplies: function() { + World.foodMove++; + World.waterMove++; + // Food + var movesPerFood = World.MOVES_PER_FOOD; + movesPerFood *= $SM.hasPerk('slow metabolism') ? 2 : 1; + if(World.foodMove >= movesPerFood) { + World.foodMove = 0; + var num = Path.outfit['cured meat']; + num--; + if(num === 0) { + Notifications.notify(World, _('the meat has run out')); + } else if(num < 0) { + // Starvation! Hooray! + num = 0; + if(!World.starvation) { + Notifications.notify(World, _('starvation sets in')); + World.starvation = true; + } else { + $SM.set('character.starved', $SM.get('character.starved', true)); + $SM.add('character.starved', 1); + if($SM.get('character.starved') >= 10 && !$SM.hasPerk('slow metabolism')) { + $SM.addPerk('slow metabolism'); + } + World.die(); + return false; + } + } else { + World.starvation = false; + World.setHp(World.health + World.meatHeal()); + } + Path.outfit['cured meat'] = num; + } + // Water + var movesPerWater = World.MOVES_PER_WATER; + movesPerWater *= $SM.hasPerk('desert rat') ? 2 : 1; + if(World.waterMove >= movesPerWater) { + World.waterMove = 0; + var water = World.water; + water--; + if(water === 0) { + Notifications.notify(World, _('there is no more water')); + } else if(water < 0) { + water = 0; + if(!World.thirst) { + Notifications.notify(World, _('the thirst becomes unbearable')); + World.thirst = true; + } else { + $SM.set('character.dehydrated', $SM.get('character.dehydrated', true)); + $SM.add('character.dehydrated', 1); + if($SM.get('character.dehydrated') >= 10 && !$SM.hasPerk('desert rat')) { + $SM.addPerk('desert rat'); + } + World.die(); + return false; + } + } else { + World.thirst = false; + } + World.setWater(water); + World.updateSupplies(); + } + return true; + }, + + meatHeal: function() { + return World.MEAT_HEAL * ($SM.hasPerk('gastronome') ? 2 : 1); + }, + + medsHeal: function() { + return World.MEDS_HEAL; + }, + + hypoHeal: () => World.HYPO_HEAL, + + checkFight: function() { + World.fightMove = typeof World.fightMove == 'number' ? World.fightMove : 0; + World.fightMove++; + if(World.fightMove > World.FIGHT_DELAY) { + var chance = World.FIGHT_CHANCE; + chance *= $SM.hasPerk('stealthy') ? 0.5 : 1; + if(Math.random() < chance) { + World.fightMove = 0; + Events.triggerFight(); + } + } + }, + + doSpace: function() { + var curTile = World.state.map[World.curPos[0]][World.curPos[1]]; + + if(curTile == World.TILE.VILLAGE) { + World.goHome(); + } else if(curTile === World.TILE.EXECUTIONER) { + const scene = World.state.executioner ? 'executioner-antechamber' : 'executioner-intro'; + const sceneData = Events.Executioner[scene]; + Events.startEvent(sceneData); + } else if(typeof World.LANDMARKS[curTile] != 'undefined') { + if(curTile != World.TILE.OUTPOST || !World.outpostUsed()) { + Events.startEvent(Events.Setpieces[World.LANDMARKS[curTile].scene]); + } + } else { + if(World.useSupplies()) { + World.checkFight(); + } + } + }, + + getDistance: function(from, to) { + from = from || World.curPos; + to = to || World.VILLAGE_POS; + return Math.abs(from[0] - to[0]) + Math.abs(from[1] - to[1]); + }, + + getTerrain: function() { + return World.state.map[World.curPos[0]][World.curPos[1]]; + }, + + getDamage: function(thing) { + return World.Weapons[thing].damage; + }, + + narrateMove: function(oldTile, newTile) { + var msg = null; + switch(oldTile) { + case World.TILE.FOREST: + switch(newTile) { + case World.TILE.FIELD: + msg = _("the trees yield to dry grass. the yellowed brush rustles in the wind."); + break; + case World.TILE.BARRENS: + msg = _("the trees are gone. parched earth and blowing dust are poor replacements."); + break; + } + break; + case World.TILE.FIELD: + switch(newTile) { + case World.TILE.FOREST: + msg = _("trees loom on the horizon. grasses gradually yield to a forest floor of dry branches and fallen leaves."); + break; + case World.TILE.BARRENS: + msg = _("the grasses thin. soon, only dust remains."); + break; + } + break; + case World.TILE.BARRENS: + switch(newTile) { + case World.TILE.FIELD: + msg = _("the barrens break at a sea of dying grass, swaying in the arid breeze."); + break; + case World.TILE.FOREST: + msg = _("a wall of gnarled trees rises from the dust. their branches twist into a skeletal canopy overhead."); + break; + } + break; + } + if(msg != null) { + Notifications.notify(World, msg); + } + }, + + newMask: function() { + var mask = new Array(World.RADIUS * 2 + 1); + for(var i = 0; i <= World.RADIUS * 2; i++) { + mask[i] = new Array(World.RADIUS * 2 + 1); + } + World.lightMap(World.RADIUS, World.RADIUS, mask); + return mask; + }, + + lightMap: function(x, y, mask) { + var r = World.LIGHT_RADIUS; + r *= $SM.hasPerk('scout') ? 2 : 1; + World.uncoverMap(x, y, r, mask); + return mask; + }, + + uncoverMap: function(x, y, r, mask) { + mask[x][y] = true; + for(var i = -r; i <= r; i++) { + for(var j = -r + Math.abs(i); j <= r - Math.abs(i); j++) { + if(y + j >= 0 && y + j <= World.RADIUS * 2 && + x + i <= World.RADIUS * 2 && + x + i >= 0) { + mask[x+i][y+j] = true; + } + } + } + }, + + testMap: function() { + if(!World.seenAll) { + var dark; + var mask = $SM.get('game.world.mask'); + loop: + for(var i = 0; i < mask.length; i++) { + for(var j = 0; j < mask[i].length; j++) { + if(!mask[i][j]) { + dark = true; + break loop; + } + } + } + World.seenAll = !dark; + } + }, + + applyMap: function() { + if(!World.seenAll){ + var x,y,mask = $SM.get('game.world.mask'); + do { + x = Math.floor(Math.random() * (World.RADIUS * 2 + 1)); + y = Math.floor(Math.random() * (World.RADIUS * 2 + 1)); + } while (mask[x][y]); + World.uncoverMap(x, y, 5, mask); + } + World.testMap(); + }, + + generateMap: function() { + var map = new Array(World.RADIUS * 2 + 1); + for(var i = 0; i <= World.RADIUS * 2; i++) { + map[i] = new Array(World.RADIUS * 2 + 1); + } + // The Village is always at the exact center + // Spiral out from there + map[World.RADIUS][World.RADIUS] = World.TILE.VILLAGE; + for(var r = 1; r <= World.RADIUS; r++) { + for(var t = 0; t < r * 8; t++) { + var x, y; + if(t < 2 * r) { + x = World.RADIUS - r + t; + y = World.RADIUS - r; + } else if(t < 4 * r) { + x = World.RADIUS + r; + y = World.RADIUS - (3 * r) + t; + } else if(t < 6 * r) { + x = World.RADIUS + (5 * r) - t; + y = World.RADIUS + r; + } else { + x = World.RADIUS - r; + y = World.RADIUS + (7 * r) - t; + } + + map[x][y] = World.chooseTile(x, y, map); + } + } + + // Place landmarks + for(var k in World.LANDMARKS) { + var landmark = World.LANDMARKS[k]; + for(var l = 0; l < landmark.num; l++) { + var pos = World.placeLandmark(landmark.minRadius, landmark.maxRadius, k, map); + } + } + + return map; + }, + + mapSearch: function(target,map,required){ + var max = World.LANDMARKS[target].num; + if(!max){ + // this restrict the research to numerable landmarks + return null; + } + // restrict research if only a fixed number (usually 1) is required + max = (required) ? Math.min(required,max) : max; + var index = 0; + var targets = []; + search: // label for coordinate research + for(var i = 0; i <= World.RADIUS * 2; i++){ + for(var j = 0; j <= World.RADIUS * 2; j++){ + if(map[i][j].charAt(0) === target){ + // search result is stored as an object; + // items are listed as they appear in the map, tl-br + // each item has relative coordinates and a compass-type direction + targets[index] = { + x : i - World.RADIUS, + y : j - World.RADIUS, + }; + index++; + if(index === max){ + // optimisation: stop the research if maximum number of items has been reached + break search; + } + } + } + } + return targets; + }, + + compassDir: function(pos){ + var dir = ''; + var horz = pos.x < 0 ? 'west' : 'east'; + var vert = pos.y < 0 ? 'north' : 'south'; + if(Math.abs(pos.x) / 2 > Math.abs(pos.y)) { + dir = horz; + } else if(Math.abs(pos.y) / 2 > Math.abs(pos.x)){ + dir = vert; + } else { + dir = vert + horz; + } + return dir; + }, + + placeLandmark: function(minRadius, maxRadius, landmark, map) { + + var x = World.RADIUS, y = World.RADIUS; + while(!World.isTerrain(map[x][y])) { + var r = Math.floor(Math.random() * (maxRadius - minRadius)) + minRadius; + var xDist = Math.floor(Math.random() * r); + var yDist = r - xDist; + if(Math.random() < 0.5) xDist = -xDist; + if(Math.random() < 0.5) yDist = -yDist; + x = World.RADIUS + xDist; + if(x < 0) x = 0; + if(x > World.RADIUS * 2) x = World.RADIUS * 2; + y = World.RADIUS + yDist; + if(y < 0) y = 0; + if(y > World.RADIUS * 2) y = World.RADIUS * 2; + } + map[x][y] = landmark; + return [x, y]; + }, + + isTerrain: function(tile) { + return tile == World.TILE.FOREST || tile == World.TILE.FIELD || tile == World.TILE.BARRENS; + }, + + chooseTile: function(x, y, map) { + + var adjacent = [ + y > 0 ? map[x][y-1] : null, + y < World.RADIUS * 2 ? map[x][y+1] : null, + x < World.RADIUS * 2 ? map[x+1][y] : null, + x > 0 ? map[x-1][y] : null + ]; + + var chances = {}; + var nonSticky = 1; + var cur; + for(var i in adjacent) { + if(adjacent[i] == World.TILE.VILLAGE) { + // Village must be in a forest to maintain thematic consistency, yo. + return World.TILE.FOREST; + } else if(typeof adjacent[i] == 'string') { + cur = chances[adjacent[i]]; + cur = typeof cur == 'number' ? cur : 0; + chances[adjacent[i]] = cur + World.STICKINESS; + nonSticky -= World.STICKINESS; + } + } + for(var t in World.TILE) { + var tile = World.TILE[t]; + if(World.isTerrain(tile)) { + cur = chances[tile]; + cur = typeof cur == 'number' ? cur : 0; + cur += World.TILE_PROBS[tile] * nonSticky; + chances[tile] = cur; + } + } + + var list = []; + for(var j in chances) { + list.push(chances[j] + '' + j); + } + list.sort(function(a, b) { + var n1 = parseFloat(a.substring(0, a.length - 1)); + var n2 = parseFloat(b.substring(0, b.length - 1)); + return n2 - n1; + }); + + var c = 0; + var r = Math.random(); + for(var l in list) { + var prob = list[l]; + c += parseFloat(prob.substring(0,prob.length - 1)); + if(r < c) { + return prob.charAt(prob.length - 1); + } + } + + return World.TILE.BARRENS; + }, + + markVisited: function(x, y) { + World.state.map[x][y] = World.state.map[x][y] + '!'; + }, + + drawMap: function() { + var map = $('#map'); + if(map.length === 0) { + map = new $('
      ').attr('id', 'map').appendTo('#worldOuter'); + // register click handler + map.click(World.click); + } + var mapString = ""; + for(var j = 0; j <= World.RADIUS * 2; j++) { + for(var i = 0; i <= World.RADIUS * 2; i++) { + var ttClass = ""; + if(i > World.RADIUS) { + ttClass += " left"; + } else { + ttClass += " right"; + } + if(j > World.RADIUS) { + ttClass += " top"; + } else { + ttClass += " bottom"; + } + if(World.curPos[0] == i && World.curPos[1] == j) { + mapString += '@
      '+_('Wanderer')+'
      '; + } else if(World.state.mask[i][j]) { + var c = World.state.map[i][j]; + switch(c) { + case World.TILE.VILLAGE: + mapString += '' + c + '
      '+_('The Village')+'
      '; + break; + default: + if(typeof World.LANDMARKS[c] != 'undefined' && (c != World.TILE.OUTPOST || !World.outpostUsed(i, j))) { + mapString += '' + c + '
      ' + World.LANDMARKS[c].label + '
      '; + } else { + if(c.length > 1) { + c = c[0]; + } + mapString += c; + } + break; + } + } else { + mapString += ' '; + } + } + mapString += '
      '; + } + map.html(mapString); + }, + + die: function() { + if(!World.dead) { + World.dead = true; + Engine.log('player death'); + Engine.event('game event', 'death'); + Engine.keyLock = true; + // Dead! Discard any world changes and go home + Notifications.notify(World, _('the world fades')); + World.state = null; + Path.outfit = {}; + $SM.remove('outfit'); + AudioEngine.playSound(AudioLibrary.DEATH); + $('#outerSlider').animate({opacity: '0'}, 600, 'linear', function() { + $('#outerSlider').css('left', '0px'); + $('#locationSlider').css('left', '0px'); + $('#storesContainer').css({'top': '0px', 'right': '0px'}); + Engine.activeModule = Room; + $('div.headerButton').removeClass('selected'); + Room.tab.addClass('selected'); + Engine.setTimeout(function(){ + Room.onArrival(); + $('#outerSlider').animate({opacity:'1'}, 600, 'linear'); + Button.cooldown($('#embarkButton')); + Engine.keyLock = false; + Engine.tabNavigation = true; + }, 2000, true); + }); + } + }, + + goHome: function() { + // Home safe! Commit the changes. + $SM.setM('game.world', World.state); + World.testMap(); + + if(World.state.sulphurmine && $SM.get('game.buildings["sulphur mine"]', true) === 0) { + $SM.add('game.buildings["sulphur mine"]', 1); + Engine.event('progress', 'sulphur mine'); + } + if(World.state.ironmine && $SM.get('game.buildings["iron mine"]', true) === 0) { + $SM.add('game.buildings["iron mine"]', 1); + Engine.event('progress', 'iron mine'); + } + if(World.state.coalmine && $SM.get('game.buildings["coal mine"]', true) === 0) { + $SM.add('game.buildings["coal mine"]', 1); + Engine.event('progress', 'coal mine'); + } + if(World.state.ship && !$SM.get('features.location.spaceShip')) { + Ship.init(); + Engine.event('progress', 'ship'); + } + if (World.state.executioner && !$SM.get('features.location.fabricator')) { + Fabricator.init(); + Notifications.notify(null, _('builder knows the strange device when she sees it. takes it for herself real quick. doesn’t ask where it came from.')); + Engine.event('progress', 'fabricator'); + } + World.redeemBlueprints(); + World.state = null; + + if(Path.outfit['cured meat'] > 0) { + Button.setDisabled($('#embarkButton'), false); + } + + World.returnOutfit(); + + $('#outerSlider').animate({left: '0px'}, 300); + Engine.activeModule = Path; + Path.onArrival(); + Engine.restoreNavigation = true; + }, + + redeemBlueprints: () => { + let redeemed = false; + const redeem = (blueprint, item) => { + if (Path.outfit[blueprint]) { + $SM.set(`character.blueprints['${item}']`, true); + delete Path.outfit[blueprint]; + redeemed = true; + } + }; + + redeem('hypo blueprint', 'hypo'); + redeem('kinetic armour blueprint', 'kinetic armour'); + redeem('disruptor blueprint', 'disruptor'); + redeem('plasma rifle blueprint', 'plasma rifle'); + redeem('stim blueprint', 'stim'); + redeem('glowstone blueprint', 'glowstone'); + + if (redeemed) { + Notifications.notify(null, 'blueprints feed into the fabricator data port. possibilities grow.'); + } + }, + + returnOutfit: () => { + for(var k in Path.outfit) { + $SM.add('stores["'+k+'"]', Path.outfit[k]); + if(World.leaveItAtHome(k)) { + Path.outfit[k] = 0; + } + } + }, + + leaveItAtHome: function(thing) { + return thing != 'cured meat' && thing != 'bullets' && thing != 'energy cell' && + thing != 'charm' && thing != 'medicine' && thing != 'stim' && thing != 'hypo' && + typeof World.Weapons[thing] == 'undefined' && typeof Room.Craftables[thing] == 'undefined'; + }, + + getMaxHealth: function() { + if($SM.get('stores["kinetic armour"]', true) > 0) { + return World.BASE_HEALTH + 75; + } else if($SM.get('stores["s armour"]', true) > 0) { + return World.BASE_HEALTH + 35; + } else if($SM.get('stores["i armour"]', true) > 0) { + return World.BASE_HEALTH + 15; + } else if($SM.get('stores["l armour"]', true) > 0) { + return World.BASE_HEALTH + 5; + } + return World.BASE_HEALTH; + }, + + getHitChance: function() { + if($SM.hasPerk('precise')) { + return World.BASE_HIT_CHANCE + 0.1; + } + return World.BASE_HIT_CHANCE; + }, + + getMaxWater: function() { + + if($SM.get('stores["fluid recycler"]', true) > 0) { + return World.BASE_WATER + 100; + } else if($SM.get('stores["water tank"]', true) > 0) { + return World.BASE_WATER + 50; + } else if($SM.get('stores.cask', true) > 0) { + return World.BASE_WATER + 20; + } else if($SM.get('stores.waterskin', true) > 0) { + return World.BASE_WATER + 10; + } + return World.BASE_WATER; + }, + + outpostUsed: function(x, y) { + x = typeof x == 'number' ? x : World.curPos[0]; + y = typeof y == 'number' ? y : World.curPos[1]; + var used = World.usedOutposts[x + ',' + y]; + return typeof used != 'undefined' && used === true; + }, + + useOutpost: function() { + Notifications.notify(null, _('water replenished')); + World.setWater(World.getMaxWater()); + // Mark this outpost as used + World.usedOutposts[World.curPos[0] + ',' + World.curPos[1]] = true; + }, + + onArrival: function() { + Engine.tabNavigation = false; + // Clear the embark cooldown + Button.clearCooldown($('#embarkButton')); + Engine.keyLock = false; + // Explore in a temporary world-state. We'll commit the changes if you return home safe. + World.state = $.extend(true, {}, $SM.get('game.world')); + World.setWater(World.getMaxWater()); + World.setHp(World.getMaxHealth()); + World.foodMove = 0; + World.waterMove = 0; + World.starvation = false; + World.thirst = false; + World.usedOutposts = {}; + World.curPos = World.copyPos(World.VILLAGE_POS); + World.drawMap(); + World.setTitle(); + AudioEngine.playBackgroundMusic(AudioLibrary.MUSIC_WORLD); + World.dead = false; + $('div#bagspace-world > div').empty(); + World.updateSupplies(); + $('#bagspace-world').width($('#map').width()); + }, + + setTitle: function() { + document.title = _('A Barren World'); + }, + + copyPos: function(pos) { + return [pos[0], pos[1]]; + }, + + handleStateUpdates: function(e){ + + } };