├── _config.yml ├── .gitignore ├── jsconfig.json ├── doc ├── youWin.png └── designNote.pdf ├── package.json ├── style.css ├── index.html ├── LICENSE ├── scripts ├── factory.js ├── text.js ├── entity.js ├── component.js ├── system.js ├── game.js └── rot.min.js └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | dev/ 3 | .vscode/ 4 | *.swp 5 | 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "dev", 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /doc/youWin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bozar/runeHunter/HEAD/doc/youWin.png -------------------------------------------------------------------------------- /doc/designNote.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bozar/runeHunter/HEAD/doc/designNote.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard": { 3 | "globals": [ 4 | "Game","ROT" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 10px; 3 | margin: 0; 4 | background-color: #262626; 5 | } 6 | 7 | div#game canvas { 8 | display: block; 9 | margin: 2rem auto; 10 | border: 2px dashed #abb2bf; 11 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rune Hunter 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bozar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Entity factory 4 | // http://vasir.net/blog/game-development/how-to-build-entity-component-system-in-javascript 5 | Game.Factory = function (name) { 6 | this._entityName = name 7 | this._id = createID() 8 | 9 | function createID () { 10 | // 12345678-{repeat}-{repeat}-{repeat} 11 | let randomNumber = '' 12 | 13 | while (randomNumber.length < 32) { 14 | randomNumber += (Math.random() * Math.pow(10, 8) | 0).toString(16) 15 | } 16 | return randomNumber.replace(/.{8}/g, '$&' + '-').slice(0, 35) 17 | } 18 | } 19 | 20 | Game.Factory.prototype.getID = function () { return this._id } 21 | Game.Factory.prototype.getEntityName = function () { return this._entityName } 22 | 23 | Game.Factory.prototype.addComponent = function (component, newName) { 24 | if (newName) { 25 | this[newName] = component 26 | } else { 27 | this[component._name] = component 28 | } 29 | } 30 | Game.Factory.prototype.removeComponent = function (name) { 31 | delete this[name] 32 | } 33 | 34 | Game.Factory.prototype.print = function () { 35 | console.log(JSON.stringify(this, null, 2)) 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rune Hunter 2 | 3 | ![image](https://github.com/Bozar/runeHunter/blob/master/doc/youWin.png) 4 | 5 | Rune Hunter is a coffee break Roguelike game written in Javascript with [rot.js](https://github.com/ondras/rot.js). Explore the dungeon, stay away from ghosts, and collect 3 runes as quickly as possible. 6 | 7 | Play the game [on-line](https://bozar.github.io/runeHunter/). 8 | 9 | ## Your Goal 10 | 11 | * Explore the dungeon and collect treasure. 12 | * Make sacrifice at the altar to get a rune. 13 | * Collect 3 runes to beat the game. 14 | 15 | ## Tips 16 | 17 | * The more you carry, the more turns it takes to move 1 step. 18 | * You cannot drop the rune. 19 | * A satisified ghost needs more time to reappear. 20 | 21 | ## Keybindings 22 | 23 | * Move: arrow keys, hjkl 24 | * Pick up: Space 25 | * Drop the skull/coin/gem: s/c/g, 1/2/3 26 | * Print the seed in the browser console: d 27 | * Switch the wizard mode: ~ 28 | 29 | Wizard mode: 30 | 31 | * Switch the fog of war: f 32 | * Create a/an skull/coin/gem/altar: S/C/G/A 33 | * Print the current turn in the browser console: t 34 | 35 | ## Miscellaneous 36 | 37 | Search `Game._devSeed` in `scripts/game.js` to manually set a seed. 38 | 39 | `doc/designNote.pdf` is the paper prototype I made for Rune Hunter. 40 | -------------------------------------------------------------------------------- /scripts/text.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Game.text = {} 4 | 5 | Game.text.ui = function (id) { 6 | let text = new Map() 7 | 8 | text.set('turn', 'Turn:') 9 | text.set('carry', 'Carry:') 10 | text.set('altar', 'Altar:') 11 | text.set('skull', 'Skull:') 12 | text.set('coin', 'Coin:') 13 | text.set('gem', 'Gem:') 14 | text.set('rune', 'Rune:') 15 | text.set('death', 'Death') 16 | text.set('trick', 'Trick') 17 | text.set('greed', 'Greed') 18 | 19 | return text.get(id) 20 | } 21 | 22 | Game.text.item = function (id) { 23 | let text = new Map() 24 | 25 | text.set('skull', 'Alluring Skull') 26 | text.set('coin', 'Double-Headed Coin') 27 | text.set('gem', 'Red Stone of Aja') 28 | 29 | return text.get(id) 30 | } 31 | 32 | // move, pick up and drop 33 | Game.text.interact = function (id, item) { 34 | let text = new Map() 35 | 36 | text.set('find', 'You find the ' + Game.text.item(item) + '.') 37 | text.set('pick', 'You pick up the ' + Game.text.item(item) + '.') 38 | text.set('drop', 'You drop the ' + Game.text.item(item) + '.') 39 | text.set('emptyFloor', 'There is nothing to pick up here.') 40 | text.set('emptyBag', 'You have nothing to drop.') 41 | text.set('occupiedFloor', 'You cannot drop the item here.') 42 | text.set('fullBag', 'Your bag is full.') 43 | text.set('forbidMove', 'You cannot move there.') 44 | 45 | return text.get(id) 46 | } 47 | 48 | // lure away the ghost 49 | Game.text.encounter = function (id, item) { 50 | let text = new Map() 51 | 52 | text.set('warn', 'Something bad is going to happen.') 53 | text.set('lose', 'You are caught by the ghost.') 54 | text.set('appear', 'The ghost appears from nowhere!') 55 | text.set('invalid', 'You need to drop some treasure.') 56 | text.set('end', '===The End===') 57 | text.set('more', 'You need more treasure.') 58 | text.set('reaction', 'The ghost vanishes' + getReaction(item)) 59 | 60 | return text.get(id) 61 | 62 | function getReaction (item) { 63 | switch (item) { 64 | case 'skull': 65 | return ' with anger.' 66 | case 'gem': 67 | return ' with satisfaction.' 68 | default: 69 | return '.' 70 | } 71 | } 72 | } 73 | 74 | // make sacrifice to the god 75 | Game.text.altar = function (id, item) { 76 | let text = new Map() 77 | 78 | text.set('win', 'You leave the dungeon alive with 3 runes!') 79 | text.set('turn', 'Final turn: ' + getTurn()) 80 | text.set('sacrifice', 'You make a sacrifice.') 81 | text.set('reaction', getReaction(item)) 82 | 83 | function getTurn () { 84 | return Math.floor(Game.getEntity('timer').scheduler.getTime()) 85 | } 86 | 87 | function getReaction (item) { 88 | let god = new Map() 89 | god.set('death', 'The God of Death is indifferent to you.') 90 | god.set('trick', 'The God of Trickery whispers to you.') 91 | god.set('greed', 'The God of Greed smiles to you.') 92 | 93 | return god.get(item) 94 | } 95 | return text.get(id) 96 | } 97 | 98 | Game.text.tutorial = function (id) { 99 | let text = new Map() 100 | 101 | text.set('move', 'Press arrow keys or hjkl to move around.') 102 | text.set('drop', 'Press s/c/g to drop the skull/coin/gem.') 103 | text.set('pick', 'Press Space to pick up.') 104 | text.set('speed', 'Your speed (turn) is slowed down by the carry weight.') 105 | text.set('altar', 'Make sacrifice at the Altar to get a rune.') 106 | 107 | return text.get(id) 108 | } 109 | -------------------------------------------------------------------------------- /scripts/entity.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ----- Store entities +++++ 4 | Game.entities = new Map() 5 | Game.entities.set('message', null) 6 | Game.entities.set('seed', null) 7 | Game.entities.set('dungeon', null) 8 | Game.entities.set('pc', null) 9 | Game.entities.set('timer', null) 10 | Game.entities.set('harbinger', null) 11 | Game.entities.set('item', new Map()) 12 | Game.entities.set('altar', null) 13 | Game.entities.set('fog', null) 14 | 15 | // ----- Create a single entity +++++ 16 | Game.entity = {} 17 | Game.getEntity = function (id) { return Game.entities.get(id) } 18 | 19 | Game.entity.message = function () { 20 | let e = new Game.Factory('message') 21 | 22 | e.addComponent(new Game.Component.Message()) 23 | 24 | Game.entities.set('message', e) 25 | } 26 | 27 | Game.entity.seed = function () { 28 | let e = new Game.Factory('seed') 29 | 30 | e.addComponent(new Game.Component.Seed()) 31 | 32 | Game.entities.set('seed', e) 33 | } 34 | 35 | Game.entity.dungeon = function () { 36 | let e = new Game.Factory('dungeon') 37 | e.addComponent(new Game.Component.Dungeon()) 38 | 39 | cellular() 40 | e.light = function (x, y) { 41 | return e.Dungeon.getTerrain().get(x + ',' + y) === 0 42 | } 43 | e.fov = new ROT.FOV.PreciseShadowcasting(e.light) 44 | 45 | Game.entities.set('dungeon', e) 46 | 47 | // helper functions 48 | function cellular () { 49 | let cell = new ROT.Map.Cellular(e.Dungeon.getWidth(), e.Dungeon.getHeight()) 50 | 51 | cell.randomize(0.5) 52 | for (let i = 0; i < 5; i++) { cell.create() } 53 | cell.connect(function (x, y, wall) { 54 | e.Dungeon.getTerrain().set(x + ',' + y, wall) 55 | }) 56 | } 57 | } 58 | 59 | Game.entity.pc = function () { 60 | let e = new Game.Factory('pc') 61 | 62 | e.addComponent(new Game.Component.Position(5)) 63 | e.addComponent(new Game.Component.Display('@')) 64 | e.addComponent(new Game.Component.Bagpack()) 65 | 66 | e.act = Game.system.pcAct 67 | 68 | Game.entities.set('pc', e) 69 | } 70 | 71 | Game.entity.timer = function () { 72 | let e = new Game.Factory('timer') 73 | 74 | e.scheduler = new ROT.Scheduler.Action() 75 | e.engine = new ROT.Engine(e.scheduler) 76 | 77 | Game.entities.set('timer', e) 78 | } 79 | 80 | Game.entity.harbinger = function () { 81 | let e = new Game.Factory('harbinger') 82 | 83 | e.addComponent(new Game.Component.Display('G')) 84 | e.addComponent(new Game.Component.Position()) 85 | e.addComponent(new Game.Component.Counter()) 86 | e.act = Game.system.harbingerAct 87 | 88 | Game.entities.set('harbinger', e) 89 | } 90 | 91 | Game.entity.skull = function (x, y) { 92 | let e = new Game.Factory('skull') 93 | 94 | e.addComponent(new Game.Component.Position(0, x, y)) 95 | e.addComponent(new Game.Component.Display('?')) 96 | 97 | Game.entities.get('item').set(e.getID(), e) 98 | } 99 | 100 | Game.entity.coin = function (x, y) { 101 | let e = new Game.Factory('coin') 102 | 103 | e.addComponent(new Game.Component.Position(0, x, y)) 104 | e.addComponent(new Game.Component.Display('$')) 105 | 106 | Game.entities.get('item').set(e.getID(), e) 107 | } 108 | 109 | Game.entity.gem = function (x, y) { 110 | let e = new Game.Factory('gem') 111 | 112 | e.addComponent(new Game.Component.Position(0, x, y)) 113 | e.addComponent(new Game.Component.Display('*')) 114 | 115 | Game.entities.get('item').set(e.getID(), e) 116 | } 117 | 118 | Game.entity.altar = function () { 119 | let e = new Game.Factory('altar') 120 | 121 | e.addComponent(new Game.Component.Position()) 122 | e.addComponent(new Game.Component.Display('A', 'orange')) 123 | e.addComponent(new Game.Component.Sacrifice()) 124 | 125 | Game.entities.set('altar', e) 126 | } 127 | 128 | Game.entity.fog = function () { 129 | let e = new Game.Factory('fog') 130 | e.addComponent(new Game.Component.Position(8)) 131 | 132 | Game.entities.set('fog', e) 133 | } 134 | -------------------------------------------------------------------------------- /scripts/component.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Game.Component = {} 4 | 5 | Game.Component.Message = function () { 6 | this._name = 'Message' 7 | 8 | this._message = [] 9 | this._modeline = '' 10 | 11 | this.getMsgList = function () { return this._message } 12 | this.getModeline = function () { 13 | let text = this._modeline 14 | this._modeline = '' 15 | return text 16 | } 17 | 18 | this.setModeline = function (text) { this._modeline = text } 19 | this.pushMsg = function (text) { this._message.push(text) } 20 | } 21 | 22 | Game.Component.Seed = function () { 23 | this._name = 'Seed' 24 | 25 | this._seed = null // to start the RNG engine 26 | this._rawSeed = null // player's input 27 | 28 | this.getSeed = function () { return this._seed } 29 | this.getRawSeed = function () { return this._rawSeed } 30 | this.setSeed = function (seed) { 31 | if (!seed) { 32 | this._seed = 33 | Math.floor((Math.random() * 9 + 1) * Math.pow(10, 9)).toString() 34 | this._rawSeed = this._seed 35 | } else { 36 | this._seed = seed.toString().replace(/^#{0,1}(.+)$/, '$1') 37 | this._rawSeed = seed 38 | } 39 | } 40 | } 41 | 42 | Game.Component.Dungeon = function () { 43 | this._name = 'Dungeon' 44 | 45 | this._width = Game.UI.dungeon.getWidth() - 2 46 | this._height = Game.UI.dungeon.getHeight() - 2 47 | this._padding = 1 // do not draw along the UI border 48 | this._terrain = new Map() // z,x,y: 0(floor) or 1(wall) 49 | this._memory = [] // explored dungeon 50 | this._hasFov = true // only draw whatever the PC can see 51 | 52 | this.getWidth = function () { return this._width } 53 | this.getHeight = function () { return this._height } 54 | this.getPadding = function () { return this._padding } 55 | this.getTerrain = function () { return this._terrain } 56 | this.getMemory = function () { return this._memory } 57 | this.getFov = function () { return this._hasFov } 58 | 59 | this.setFov = function () { this._hasFov = !this._hasFov } 60 | this.setMemory = function (memory) { this._memory = memory } 61 | } 62 | 63 | Game.Component.Display = function (char, color) { 64 | this._name = 'Display' 65 | 66 | this._character = char 67 | this._color = Game.getColor(color || 'white') 68 | 69 | this.getCharacter = function () { return this._character } 70 | this.getColor = function () { return this._color } 71 | } 72 | 73 | Game.Component.Position = function (range, x, y) { 74 | this._name = 'Position' 75 | 76 | this._x = x 77 | this._y = y 78 | this._sight = range || 0 // how far one can see 79 | 80 | this.getX = function () { return this._x } 81 | this.getY = function () { return this._y } 82 | this.getSight = function () { return this._sight } 83 | 84 | this.setX = function (pos) { this._x = pos } 85 | this.setY = function (pos) { this._y = pos } 86 | } 87 | 88 | Game.Component.Counter = function () { 89 | this._name = 'Counter' 90 | 91 | this._start = 50 92 | this._warning = 10 93 | this._count = this._start 94 | 95 | this.hasGhost = function () { return this._count <= 0 } 96 | this.hasWarning = function () { return this._count <= this._warning } 97 | 98 | this.countdown = function () { 99 | this._count -= 1 100 | 101 | return this._count === this._warning 102 | ? 'warning' 103 | : this._count === 0 104 | ? 'ghost' 105 | : null 106 | } 107 | 108 | this.reset = function (item) { 109 | switch (item) { 110 | case 'gem': 111 | this._count = Math.floor(this._start * 1.4) 112 | break 113 | case 'skull': 114 | this._count = Math.floor(this._start * 0.6) 115 | break 116 | default: 117 | this._count = this._start 118 | break 119 | } 120 | } 121 | } 122 | 123 | Game.Component.Bagpack = function () { 124 | this._name = 'Bagpack' 125 | 126 | this._skull = 0 127 | this._coin = 0 128 | this._gem = 0 129 | this._rune = 0 130 | this._max = 9 131 | 132 | this.getSkull = function () { return this._skull } 133 | this.getCoin = function () { return this._coin } 134 | this.getGem = function () { return this._gem } 135 | this.getRune = function () { return this._rune } 136 | 137 | this.getTotal = function () { 138 | return this._skull + this._coin + this._gem + this._rune 139 | } 140 | this.getSpeed = function () { 141 | return Math.max(1, Math.ceil(this.getTotal() / 3)) 142 | } 143 | 144 | this.pickItem = function (item) { 145 | if (this.getTotal() < this._max) { 146 | this['_' + item] += 1 147 | return true 148 | } 149 | return false 150 | } 151 | this.dropItem = function (item, hasGhost) { 152 | if (hasGhost && item === 'coin') { 153 | if (this._coin > 1) { 154 | this._coin -= 2 155 | return true 156 | } 157 | return false 158 | } else { 159 | if (this['_' + item] > 0) { 160 | this['_' + item] -= 1 161 | return true 162 | } 163 | return false 164 | } 165 | } 166 | } 167 | 168 | Game.Component.Sacrifice = function () { 169 | this._name = 'Sacrifice' 170 | 171 | // skull, coin, gem 172 | this._death = [3, 2, 1] 173 | this._trick = [2, 2, 2] 174 | this._greed = [1, 2, 3] 175 | 176 | this._altarList = ['death', 'trick', 'greed'] 177 | this._counter = 0 178 | this._draw = false 179 | 180 | this.getAltarName = function () { return this._altarList[this._counter] } 181 | this.getItemList = function () { return this['_' + this.getAltarName()] } 182 | this.getDrawAltar = function () { return this._draw } 183 | 184 | this.nextAltar = function () { 185 | this._counter = Math.min(this._counter + 1, this._altarList.length - 1) 186 | } 187 | this.drawAlatr = function (status) { this._draw = status } 188 | } 189 | -------------------------------------------------------------------------------- /scripts/system.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Game.system = {} 4 | 5 | Game.system.isFloor = function (x, y) { 6 | return Game.getEntity('dungeon').Dungeon.getTerrain().get(x + ',' + y) === 0 7 | } 8 | 9 | Game.system.placePC = function () { 10 | let x = null 11 | let y = null 12 | let width = Game.getEntity('dungeon').Dungeon.getWidth() 13 | let height = Game.getEntity('dungeon').Dungeon.getHeight() 14 | let border = Game.getEntity('pc').Position.getSight() 15 | 16 | do { 17 | x = Math.floor(width * ROT.RNG.getUniform()) 18 | y = Math.floor(height * ROT.RNG.getUniform()) 19 | } while (!Game.system.isFloor(x, y) || 20 | x < border || x > width - border || 21 | y < border || y > height - border) 22 | 23 | Game.getEntity('pc').Position.setX(x) 24 | Game.getEntity('pc').Position.setY(y) 25 | } 26 | 27 | Game.system.isPC = function (actor) { 28 | return actor.getID() === Game.getEntity('pc').getID() 29 | } 30 | 31 | Game.system.pcHere = function (x, y) { 32 | let pcX = Game.getEntity('pc').Position.getX() 33 | let pcY = Game.getEntity('pc').Position.getY() 34 | 35 | return (x === pcX) && (y === pcY) 36 | } 37 | 38 | Game.system.isAltar = function (x, y) { 39 | return x === Game.getEntity('altar').Position.getX() && 40 | y === Game.getEntity('altar').Position.getY() 41 | } 42 | 43 | Game.system.isItem = function (x, y) { 44 | for (const keyValue of Game.getEntity('item')) { 45 | if (x === keyValue[1].Position.getX() && y === keyValue[1].Position.getY()) { 46 | return keyValue[1] 47 | } 48 | } 49 | return null 50 | } 51 | 52 | Game.system.pcAct = function () { 53 | Game.getEntity('timer').engine.lock() 54 | 55 | if (Game.getEntity('harbinger').Counter.hasGhost()) { 56 | Game.input.listenEvent('add', lure) 57 | } else { 58 | Game.input.listenEvent('add', 'main') 59 | } 60 | 61 | // helper function 62 | function lure (e) { 63 | let item = Game.input.getAction(e, 'drop') 64 | let message = Game.getEntity('message').Message 65 | let bag = Game.getEntity('pc').Bagpack 66 | 67 | if (item) { 68 | if (bag.dropItem(item, true)) { 69 | message.pushMsg(Game.text.interact('drop', item)) 70 | message.pushMsg(Game.text.encounter('reaction', item)) 71 | 72 | Game.input.listenEvent('remove', lure) 73 | Game.system.resetHarbinger(item) 74 | Game.system.unlockEngine(1) 75 | } else { 76 | message.setModeline(Game.text.encounter('more')) 77 | } 78 | } else { 79 | message.setModeline(Game.text.encounter('invalid')) 80 | } 81 | 82 | Game.display.clear() 83 | Game.screens.main.display() 84 | } 85 | } 86 | 87 | Game.system.harbingerAct = function () { 88 | let message = Game.getEntity('message').Message 89 | let unlock = true 90 | 91 | Game.getEntity('timer').engine.lock() 92 | 93 | switch (Game.getEntity('harbinger').Counter.countdown()) { 94 | case 'warning': 95 | message.pushMsg(Game.text.encounter('warn')) 96 | break 97 | case 'ghost': 98 | if (Game.system.isDead()) { 99 | message.pushMsg(Game.text.encounter('lose')) 100 | message.pushMsg(Game.text.encounter('end')) 101 | 102 | unlock = false 103 | } else { 104 | message.pushMsg(Game.text.encounter('appear')) 105 | 106 | if (!Game.getEntity('altar').Sacrifice.getDrawAltar()) { 107 | Game.system.placeAltar(false) 108 | } else { 109 | Game.system.placeAltar(true) 110 | } 111 | Game.system.placeItem(Game.system.placeFog()) 112 | } 113 | break 114 | } 115 | 116 | unlock && Game.system.unlockEngine(1) 117 | } 118 | 119 | Game.system.move = function (direction) { 120 | let actor = Game.getEntity('pc') 121 | let duration = actor.Bagpack.getSpeed() 122 | let x = actor.Position.getX() 123 | let y = actor.Position.getY() 124 | let message = Game.getEntity('message').Message 125 | 126 | switch (direction) { 127 | case 'left': 128 | x -= 1 129 | break 130 | case 'right': 131 | x += 1 132 | break 133 | case 'up': 134 | y -= 1 135 | break 136 | case 'down': 137 | y += 1 138 | break 139 | } 140 | 141 | if (Game.system.isFloor(x, y) && !Game.system.isAltar(x, y)) { 142 | Game.system.isItem(x, y) && 143 | message.pushMsg(Game.text.interact('find', 144 | Game.system.isItem(x, y).getEntityName())) 145 | 146 | actor.Position.setX(x) 147 | actor.Position.setY(y) 148 | 149 | Game.input.listenEvent('remove', 'main') 150 | Game.system.unlockEngine(duration) 151 | } else if (Game.system.isAltar(x, y)) { 152 | if (Game.system.sacrificeItem()) { 153 | message.pushMsg(Game.text.altar('sacrifice')) 154 | message.pushMsg(Game.text.altar('reaction', 155 | Game.getEntity('altar').Sacrifice.getAltarName())) 156 | 157 | actor.Bagpack.pickItem('rune') 158 | Game.getEntity('altar').Sacrifice.nextAltar() 159 | Game.system.resetAltar() 160 | if (actor.Bagpack.getRune() === 3) { 161 | message.pushMsg(Game.text.altar('win')) 162 | message.pushMsg(Game.text.altar('turn')) 163 | message.pushMsg(Game.text.encounter('end')) 164 | 165 | Game.input.listenEvent('remove', 'main') 166 | } else { 167 | Game.input.listenEvent('remove', 'main') 168 | Game.system.unlockEngine(1) 169 | } 170 | } else { 171 | message.setModeline(Game.text.encounter('more')) 172 | } 173 | } else { 174 | message.setModeline(Game.text.interact('forbidMove')) 175 | } 176 | } 177 | 178 | Game.system.unlockEngine = function (duration) { 179 | Game.getEntity('timer').scheduler.setDuration(duration) 180 | Game.getEntity('timer').engine.unlock() 181 | 182 | Game.display.clear() 183 | Game.screens.main.display() 184 | } 185 | 186 | Game.system.pickUp = function (x, y) { 187 | let item = Game.system.isItem(x, y) 188 | 189 | if (!item) { 190 | Game.getEntity('message').Message.setModeline( 191 | Game.text.interact('emptyFloor')) 192 | 193 | return false 194 | } 195 | 196 | if (Game.getEntity('pc').Bagpack.pickItem(item.getEntityName())) { 197 | Game.getEntity('message').Message.pushMsg( 198 | Game.text.interact('pick', item.getEntityName())) 199 | 200 | Game.getEntity('item').delete(item.getID()) 201 | Game.system.unlockEngine(1) 202 | 203 | return true 204 | } else { 205 | Game.getEntity('message').Message.setModeline( 206 | Game.text.interact('fullBag')) 207 | 208 | return false 209 | } 210 | } 211 | 212 | Game.system.drop = function (item) { 213 | let x = Game.getEntity('pc').Position.getX() 214 | let y = Game.getEntity('pc').Position.getY() 215 | 216 | if (Game.system.isItem(x, y)) { 217 | Game.getEntity('message').Message.setModeline( 218 | Game.text.interact('occupiedFloor')) 219 | 220 | return false 221 | } 222 | 223 | if (!Game.getEntity('pc').Bagpack.dropItem(item)) { 224 | Game.getEntity('message').Message.setModeline( 225 | Game.text.interact('emptyBag')) 226 | 227 | return false 228 | } else { 229 | Game.entity[item](x, y) 230 | 231 | Game.getEntity('message').Message.pushMsg( 232 | Game.text.interact('drop', item)) 233 | Game.system.unlockEngine(1) 234 | 235 | return true 236 | } 237 | } 238 | 239 | Game.system.isDead = function () { 240 | let hasSkull = Game.getEntity('pc').Bagpack.getSkull() > 0 241 | let hasCoin = Game.getEntity('pc').Bagpack.getCoin() > 1 242 | let hasGem = Game.getEntity('pc').Bagpack.getGem() > 0 243 | 244 | return !(hasSkull || hasCoin || hasGem) 245 | } 246 | 247 | Game.system.resetHarbinger = function (item) { 248 | Game.getEntity('harbinger').Counter.reset(item) 249 | Game.getEntity('harbinger').Position.setX(null) 250 | Game.getEntity('harbinger').Position.setY(null) 251 | } 252 | 253 | Game.system.placeItem = function (emptyFloor) { 254 | let maxSkull = !emptyFloor ? 18 : skullInFog() 255 | let maxCoin = !emptyFloor 256 | ? 4 + Math.floor(ROT.RNG.getUniform() * 5) 257 | : coinInFog() 258 | let maxGem = !emptyFloor ? 0 : gemInFog() 259 | let x = null 260 | let y = null 261 | 262 | let width = Game.getEntity('dungeon').Dungeon.getWidth() 263 | let height = Game.getEntity('dungeon').Dungeon.getHeight() 264 | 265 | for (let i = 0; i < maxSkull; i++) { 266 | Game.entity.skull.apply(null, findPosition()) 267 | } 268 | for (let i = 0; i < maxCoin; i++) { 269 | Game.entity.coin.apply(null, findPosition()) 270 | } 271 | for (let i = 0; i < maxGem; i++) { 272 | Game.entity.gem.apply(null, findPosition()) 273 | } 274 | 275 | function findPosition () { 276 | let maxTry = 99 277 | do { 278 | if (!emptyFloor) { 279 | x = Math.floor(ROT.RNG.getUniform() * width) 280 | y = Math.floor(ROT.RNG.getUniform() * height) 281 | } else { 282 | x = emptyFloor[Math.floor(ROT.RNG.getUniform() * emptyFloor.length)] 283 | y = Number.parseInt(x.split(',')[1], 10) 284 | x = Number.parseInt(x.split(',')[0], 10) 285 | } 286 | maxTry-- 287 | } while (!Game.system.isFloor(x, y) || Game.system.isAltar(x, y) || 288 | Game.system.isItem(x, y) || Game.system.pcHere(x, y) || maxTry > 0) 289 | 290 | return [x, y] 291 | } 292 | 293 | function gemInFog () { 294 | return Math.max(1, Math.floor(ROT.RNG.getUniform() * 3)) 295 | } 296 | function coinInFog () { 297 | return Math.floor(ROT.RNG.getUniform() * 3 + 2) 298 | } 299 | function skullInFog () { 300 | return Math.floor(ROT.RNG.getUniform() * 5 + 4) 301 | } 302 | } 303 | 304 | Game.system.placeAltar = function (isFog) { 305 | let altar = Game.getEntity('altar') 306 | let fog = Game.getEntity('fog') 307 | let pcX = Game.getEntity('pc').Position.getX() 308 | let width = Game.getEntity('dungeon').Dungeon.getWidth() 309 | let height = Game.getEntity('dungeon').Dungeon.getHeight() 310 | 311 | let x = null 312 | let y = null 313 | 314 | do { 315 | x = Math.floor(width * ROT.RNG.getUniform()) 316 | y = Math.floor(height * ROT.RNG.getUniform()) 317 | } while (!Game.system.isFloor(x, y) || Game.system.isItem(x, y) || 318 | !Game.system.isReachable(x, y) || 319 | x < 0 || x >= width || Math.abs(x - pcX) < Math.floor(width / 3) || 320 | y < 0 || y >= height) 321 | 322 | if (!isFog) { 323 | altar.Position.setX(x) 324 | altar.Position.setY(y) 325 | } 326 | fog.Position.setX(x) 327 | fog.Position.setY(y) 328 | altar.Sacrifice.drawAlatr(true) 329 | } 330 | 331 | Game.system.sacrificeItem = function () { 332 | let needItem = Game.getEntity('altar').Sacrifice.getItemList() 333 | let bag = Game.getEntity('pc').Bagpack 334 | let hasItem = bag.getSkull() >= needItem[0] && 335 | bag.getCoin() >= needItem[1] && 336 | bag.getGem() >= needItem[2] 337 | 338 | if (hasItem) { 339 | for (let i = 0; i < needItem[0]; i++) { 340 | bag.dropItem('skull') 341 | } 342 | for (let i = 0; i < needItem[1]; i++) { 343 | bag.dropItem('coin') 344 | } for (let i = 0; i < needItem[2]; i++) { 345 | bag.dropItem('gem') 346 | } 347 | return true 348 | } 349 | return false 350 | } 351 | 352 | Game.system.isReachable = function (x, y) { 353 | let surround = [[x - 1, y - 1], [x - 1, y], [x - 1, y + 1], 354 | [x, y - 1], [x, y + 1], 355 | [x + 1, y - 1], [x + 1, y], [x + 1, y + 1]] 356 | 357 | for (let i = 0; i < surround.length; i++) { 358 | if (!Game.system.isFloor(...surround[i])) { 359 | return false 360 | } 361 | } 362 | return true 363 | } 364 | 365 | Game.system.resetAltar = function () { 366 | Game.getEntity('altar').Sacrifice.drawAlatr(false) 367 | Game.getEntity('altar').Position.setX(null) 368 | Game.getEntity('altar').Position.setY(null) 369 | } 370 | 371 | Game.system.placeFog = function () { 372 | let centerX = Game.getEntity('fog').Position.getX() 373 | let centerY = Game.getEntity('fog').Position.getY() 374 | let sight = Game.getEntity('fog').Position.getSight() 375 | let dungeon = Game.getEntity('dungeon') 376 | let fog = [] 377 | let memory = null 378 | 379 | dungeon.fov.compute(centerX, centerY, sight, function (x, y) { 380 | Game.system.isItem(x, y) && 381 | Game.getEntity('item').delete(Game.system.isItem(x, y).getID()) 382 | 383 | memory = dungeon.Dungeon.getMemory().filter((i) => { 384 | return i !== x + ',' + y 385 | }) 386 | dungeon.Dungeon.setMemory(memory) 387 | 388 | fog.push(x + ',' + y) 389 | }) 390 | 391 | return fog 392 | } 393 | -------------------------------------------------------------------------------- /scripts/game.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // ----- Version number, development switch, seed & color +++++ 4 | var Game = {} 5 | Game._version = '0.1.0' 6 | Game._develop = false 7 | Game.getVersion = function () { return this._version } 8 | Game.getDevelop = function () { return this._develop } 9 | Game.setDevelop = function () { this._develop = !this._develop } 10 | 11 | // set seed manually for testing, '#' can be omitted 12 | // there are no hyphens ('-') inside numbered seed 13 | // example: 14 | // Game._devSeed = '#12345' 15 | Game.getDevSeed = function () { return this._devSeed } 16 | 17 | Game._color = new Map() 18 | Game._color.set('white', '#ABB2BF') 19 | Game._color.set('black', '#262626') 20 | Game._color.set('grey', '#666666') 21 | Game._color.set('orange', '#FF9900') 22 | 23 | Game.getColor = function (color) { return Game._color.get(color) } 24 | 25 | // ----- Key-bindings +++++ 26 | Game.input = {} 27 | Game.input.keybind = new Map() 28 | // [mode1: [keybind1], mode2: [keybind2], ...] 29 | // keybind1 -> [action1: [key1_1, key1_2, ...], 30 | // action2: [key2_1, key2_2, ...], ...] 31 | 32 | // keys that cannot be remapped by player 33 | Game.input.keybind.set('fixed', new Map()) 34 | Game.input.keybind.get('fixed').set('space', [' ']) 35 | Game.input.keybind.get('fixed').set('enter', ['Enter']) 36 | Game.input.keybind.get('fixed').set('esc', ['Escape']) 37 | Game.input.keybind.get('fixed').set('develop', ['~']) 38 | Game.input.keybind.get('fixed').set('seed', ['d']) 39 | Game.input.keybind.get('fixed').set('turn', ['t']) 40 | Game.input.keybind.get('fixed').set('fog', ['f']) 41 | Game.input.keybind.get('fixed').set('skull', ['S']) 42 | Game.input.keybind.get('fixed').set('coin', ['C']) 43 | Game.input.keybind.get('fixed').set('gem', ['G']) 44 | Game.input.keybind.get('fixed').set('altar', ['A']) 45 | 46 | // movement 47 | Game.input.keybind.set('move', new Map()) 48 | Game.input.keybind.get('move').set('left', ['h', 'ArrowLeft']) 49 | Game.input.keybind.get('move').set('down', ['j', 'ArrowDown']) 50 | Game.input.keybind.get('move').set('up', ['k', 'ArrowUp']) 51 | Game.input.keybind.get('move').set('right', ['l', 'ArrowRight']) 52 | 53 | // drop treasure 54 | Game.input.keybind.set('drop', new Map()) 55 | Game.input.keybind.get('drop').set('skull', ['s', '1']) 56 | Game.input.keybind.get('drop').set('coin', ['c', '2']) 57 | Game.input.keybind.get('drop').set('gem', ['g', '3']) 58 | 59 | Game.input.getAction = function (keyInput, mode) { 60 | if (!mode) { 61 | Game.getDevelop() && console.log('Undefined mode.') 62 | return null 63 | } 64 | 65 | for (const [key, value] of Game.input.keybind.get(mode)) { 66 | if (value.indexOf(keyInput.key) > -1) { 67 | return key 68 | } 69 | } 70 | return null 71 | } 72 | 73 | Game.input.listenEvent = function (event, handler) { 74 | handler = Game.screens[String(handler)] 75 | ? Game.screens[handler].keyInput 76 | : handler 77 | 78 | switch (event) { 79 | case 'add': 80 | window.addEventListener('keydown', handler) 81 | break 82 | case 'remove': 83 | window.removeEventListener('keydown', handler) 84 | break 85 | } 86 | } 87 | 88 | // ----- The position & size of screen elements +++++ 89 | Game.UI = function (width, height) { 90 | this._width = width || null 91 | this._height = height || null 92 | 93 | this._x = null 94 | this._y = null 95 | } 96 | 97 | Game.UI.prototype.getWidth = function () { return this._width } 98 | Game.UI.prototype.getHeight = function () { return this._height } 99 | Game.UI.prototype.getX = function () { return this._x } 100 | Game.UI.prototype.getY = function () { return this._y } 101 | 102 | Game.UI.canvas = new Game.UI(70, 26) 103 | 104 | Game.display = new ROT.Display({ 105 | width: Game.UI.canvas.getWidth(), 106 | height: Game.UI.canvas.getHeight(), 107 | fg: Game.getColor('white'), 108 | bg: Game.getColor('black'), 109 | fontSize: 20, 110 | fontFamily: (function () { 111 | let family = 'dejavu sans mono' 112 | family += ', consolas' 113 | family += ', monospace' 114 | 115 | return family 116 | }()) 117 | }) 118 | 119 | // ``` The main screen +++ 120 | Game.UI.padTopBottom = 0.5 121 | Game.UI.padLeftRight = 1 122 | Game.UI.padModeStatus = 1 123 | Game.UI.padModeMessage = 0 124 | Game.UI.padMessageDungeon = 1 125 | 126 | Game.UI.status = new Game.UI(13, null) 127 | Game.UI.status._height = Game.UI.canvas.getHeight() - Game.UI.padTopBottom * 2 128 | Game.UI.status._x = Game.UI.canvas.getWidth() - 129 | Game.UI.padLeftRight - Game.UI.status.getWidth() 130 | Game.UI.status._y = Game.UI.padTopBottom 131 | 132 | Game.UI.modeline = new Game.UI(null, 1) 133 | Game.UI.modeline._width = Game.UI.canvas.getWidth() - Game.UI.padLeftRight * 2 - 134 | Game.UI.padModeStatus - Game.UI.status.getWidth() 135 | Game.UI.modeline._x = Game.UI.padLeftRight 136 | Game.UI.modeline._y = Game.UI.canvas.getHeight() - Game.UI.padTopBottom - 137 | Game.UI.modeline.getHeight() 138 | 139 | Game.UI.message = new Game.UI(Game.UI.modeline.getWidth(), 5) 140 | Game.UI.message._x = Game.UI.modeline.getX() 141 | Game.UI.message._y = Game.UI.modeline.getY() - Game.UI.padModeMessage - 142 | Game.UI.message.getHeight() 143 | 144 | Game.UI.dungeon = new Game.UI(Game.UI.modeline.getWidth(), null) 145 | Game.UI.dungeon._height = Game.UI.canvas.getHeight() - Game.UI.padTopBottom - 146 | Game.UI.modeline.getHeight() - Game.UI.padModeMessage - 147 | Game.UI.message.getHeight() - Game.UI.padMessageDungeon 148 | // the dungeon size should be an integer 149 | Game.UI.dungeon._height = Math.floor(Game.UI.dungeon._height) 150 | Game.UI.dungeon._x = Game.UI.padLeftRight 151 | Game.UI.dungeon._y = Game.UI.padTopBottom 152 | 153 | // ``` UI blocks +++ 154 | Game.UI.turn = new Game.UI(Game.UI.status.getWidth(), 2) 155 | Game.UI.turn._x = Game.UI.status.getX() 156 | Game.UI.turn._y = Game.UI.status.getY() + 2 157 | 158 | Game.UI.altar = new Game.UI(Game.UI.status.getWidth(), 1) 159 | Game.UI.altar._x = Game.UI.status.getX() 160 | Game.UI.altar._y = Game.UI.turn.getY() + Game.UI.turn.getHeight() + 1 161 | 162 | Game.UI.treasure = new Game.UI(Game.UI.status.getWidth(), 4) 163 | Game.UI.treasure._x = Game.UI.status.getX() 164 | Game.UI.treasure._y = Game.UI.altar.getY() + Game.UI.altar.getHeight() + 1 165 | 166 | // ----- Screen factory: display content, listen keyboard events +++++ 167 | Game.Screen = function (name, mode) { 168 | this._name = name || 'Unnamed Screen' 169 | this._mode = mode || 'main' 170 | this._modeLineText = '' 171 | } 172 | 173 | Game.Screen.prototype.getName = function () { return this._name } 174 | Game.Screen.prototype.getMode = function () { return this._mode } 175 | Game.Screen.prototype.getText = function () { return this._modeLineText } 176 | 177 | Game.Screen.prototype.setMode = function (mode, text) { 178 | this._mode = mode || 'main' 179 | this._modeLineText = Game.text.modeLine(this._mode) + (text || '') 180 | } 181 | 182 | Game.Screen.prototype.enter = function () { 183 | Game.screens._currentName = this.getName() 184 | Game.screens._currentMode = this.getMode() 185 | 186 | this.initialize(this.getName()) 187 | this.display() 188 | } 189 | 190 | Game.Screen.prototype.exit = function () { 191 | Game.screens._currentName = null 192 | Game.screens._currentMode = null 193 | 194 | Game.display.clear() 195 | } 196 | 197 | Game.Screen.prototype.initialize = function (name) { 198 | Game.getDevelop() && console.log('Enter screen: ' + name + '.') 199 | } 200 | 201 | Game.Screen.prototype.display = function () { 202 | Game.display.drawText(1, 1, 'Testing screen') 203 | Game.display.drawText(1, 2, 'Name: ' + Game.screens._currentName) 204 | Game.display.drawText(1, 3, 'Mode: ' + Game.screens._currentMode) 205 | } 206 | 207 | Game.Screen.prototype.keyInput = function (e) { 208 | Game.getDevelop() && console.log('Key pressed: ' + e.key) 209 | } 210 | 211 | // ----- In-game screens & helper functions +++++ 212 | Game.screens = {} 213 | Game.screens._currentName = null 214 | Game.screens._currentMode = null 215 | 216 | // ``` Helper functions +++ 217 | Game.screens.colorfulText = function (text, fgColor, bgColor) { 218 | return bgColor 219 | ? '%c{' + Game.getColor(fgColor) + '}%b{' + 220 | Game.getColor(bgColor) + '}' + text + '%b{}%c{}' 221 | : '%c{' + Game.getColor(fgColor) + '}' + text + '%c{}' 222 | } 223 | 224 | Game.screens.drawAlignRight = function (x, y, width, text, color) { 225 | Game.display.drawText(x + width - text.length, y, 226 | color ? Game.screens.colorfulText(text, color) : text) 227 | } 228 | 229 | Game.screens.drawBorder = function () { 230 | let status = Game.UI.status 231 | let dungeon = Game.UI.dungeon 232 | 233 | for (let i = status.getY(); i < status.getHeight(); i++) { 234 | Game.display.draw(status.getX() - 1, i, '|') 235 | } 236 | for (let i = dungeon.getX(); i < dungeon.getWidth() + 1; i++) { 237 | Game.display.draw(i, dungeon.getY() + dungeon.getHeight(), '-') 238 | } 239 | } 240 | 241 | Game.screens.drawVersion = function () { 242 | let version = Game.getVersion() 243 | 244 | Game.getDevelop() && (version = 'Wiz|' + version) 245 | Game.screens.drawAlignRight(Game.UI.status.getX(), Game.UI.status.getY(), 246 | Game.UI.status.getWidth(), version, 'grey') 247 | } 248 | 249 | Game.screens.drawStatus = function () { 250 | let pc = Game.getEntity('pc').Bagpack 251 | let turn = pc.getSpeed() 252 | let carry = pc.getTotal() 253 | let altar = Game.text.ui(Game.getEntity('altar').Sacrifice.getAltarName()) 254 | 255 | let hasList = [pc.getSkull(), pc.getCoin(), pc.getGem(), pc.getRune()] 256 | let needList = Game.getEntity('altar').Sacrifice.getItemList() 257 | 258 | let uiList = [] 259 | uiList.push(Game.text.ui('skull')) 260 | uiList.push(Game.text.ui('coin')) 261 | uiList.push(Game.text.ui('gem')) 262 | uiList.push(Game.text.ui('rune')) 263 | 264 | let colorList = [] 265 | for (let i = 0; i < needList.length; i++) { 266 | colorList.push(hasList[i] >= needList[i] ? 'orange' : 'white') 267 | } 268 | 269 | let align = 7 270 | let x = Game.UI.status.getX() 271 | let yTrn = Game.UI.turn.getY() 272 | let yAlt = Game.UI.altar.getY() 273 | let yTrs = Game.UI.treasure.getY() 274 | 275 | Game.display.drawText(x, yTrn, Game.text.ui('turn')) 276 | Game.display.drawText(x, yTrn + 1, Game.text.ui('carry')) 277 | Game.display.drawText(x + align, yTrn, turn.toString()) 278 | Game.display.drawText(x + align, yTrn + 1, carry.toString()) 279 | 280 | Game.display.drawText(x, yAlt, Game.text.ui('altar')) 281 | Game.display.drawText(x + align, yAlt, altar) 282 | 283 | for (let i = 0; i < hasList.length; i++) { 284 | Game.display.drawText(x, yTrs + i, uiList[i]) 285 | Game.display.drawText(x + align, yTrs + i, 286 | Game.screens.colorfulText(hasList[i] + (i < 3 ? '/' + needList[i] : ''), 287 | colorList[i])) 288 | } 289 | } 290 | 291 | Game.screens.drawSeed = function () { 292 | let seed = Game.getEntity('seed').Seed.getRawSeed() 293 | seed = seed.replace(/^(#{0,1}\d{5})(\d{5})$/, '$1-$2') 294 | 295 | Game.screens.drawAlignRight( 296 | Game.UI.status.getX(), 297 | Game.UI.status.getY() + Game.UI.status.getHeight() - 1, 298 | Game.UI.status.getWidth(), 299 | seed, 'grey') 300 | } 301 | 302 | Game.screens.drawModeLine = function () { 303 | Game.display.drawText(Game.UI.modeline.getX(), Game.UI.modeline.getY(), 304 | Game.getEntity('message').Message.getModeline()) 305 | } 306 | 307 | // the text cannot be longer than the width of message block 308 | Game.screens.drawMessage = function (text) { 309 | let msgList = Game.getEntity('message').Message.getMsgList() 310 | let x = Game.UI.message.getX() 311 | let y = Game.UI.message.getY() 312 | 313 | text && msgList.push(text) 314 | while (msgList.length > Game.UI.message.getHeight()) { 315 | msgList.shift() 316 | } 317 | y += Game.UI.message.getHeight() - msgList.length 318 | 319 | for (let i = 0; i < msgList.length; i++) { 320 | Game.display.drawText(x, y + i, msgList[i]) 321 | } 322 | } 323 | 324 | Game.screens.drawDungeon = function () { 325 | let dungeon = Game.getEntity('dungeon') 326 | let memory = dungeon.Dungeon.getMemory() 327 | let pcX = Game.getEntity('pc').Position.getX() 328 | let pcY = Game.getEntity('pc').Position.getY() 329 | let sight = Game.getEntity('pc').Position.getSight() 330 | 331 | if (dungeon.Dungeon.getFov()) { 332 | if (memory.length > 0) { 333 | for (let i = 0; i < memory.length; i++) { 334 | drawTerrain( 335 | memory[i].split(',')[0], 336 | memory[i].split(',')[1], 337 | 'grey') 338 | } 339 | } 340 | 341 | dungeon.fov.compute(pcX, pcY, sight, function (x, y) { 342 | memory.indexOf(x + ',' + y) < 0 && memory.push(x + ',' + y) 343 | drawTerrain(x, y, 'white') 344 | }) 345 | } else { 346 | for (const keyValue of dungeon.Dungeon.getTerrain()) { 347 | drawTerrain( 348 | keyValue[0].split(',')[0], 349 | keyValue[0].split(',')[1], 350 | 'white') 351 | } 352 | } 353 | 354 | function drawTerrain (x, y, color) { 355 | x = Number.parseInt(x, 10) 356 | y = Number.parseInt(y, 10) 357 | 358 | Game.display.draw( 359 | x + Game.UI.dungeon.getX() + dungeon.Dungeon.getPadding(), 360 | y + Game.UI.dungeon.getY() + dungeon.Dungeon.getPadding(), 361 | Game.system.isFloor(x, y) ? '.' : '#', 362 | Game.getColor(color)) 363 | } 364 | } 365 | 366 | Game.screens.drawItem = function () { 367 | for (const keyValue of Game.getEntity('item')) { 368 | Game.screens.drawActor(keyValue[1]) 369 | } 370 | } 371 | 372 | Game.screens.drawActor = function (actor, noFov) { 373 | let drawThis = false 374 | 375 | let dungeon = Game.getEntity('dungeon') 376 | let pc = Game.getEntity('pc').Position 377 | let actorX = Number.parseInt(actor.Position.getX(), 10) 378 | let actorY = Number.parseInt(actor.Position.getY(), 10) 379 | 380 | if (!noFov && dungeon.Dungeon.getFov() && !Game.system.isPC(actor)) { 381 | dungeon.fov.compute(pc.getX(), pc.getY(), pc.getSight(), function (x, y) { 382 | if (x === actorX && y === actorY) { 383 | drawThis = true 384 | } 385 | }) 386 | } else { 387 | drawThis = true 388 | } 389 | 390 | drawThis && Game.display.draw( 391 | actorX + Game.UI.dungeon.getX() + dungeon.Dungeon.getPadding(), 392 | actorY + Game.UI.dungeon.getY() + dungeon.Dungeon.getPadding(), 393 | actor.Display.getCharacter(), actor.Display.getColor()) 394 | } 395 | 396 | Game.screens.drawGhost = function () { 397 | let pcX = Game.getEntity('pc').Position.getX() 398 | let pcY = Game.getEntity('pc').Position.getY() 399 | let sight = Game.getEntity('pc').Position.getSight() 400 | let harbinger = Game.getEntity('harbinger') 401 | let dungeon = Game.getEntity('dungeon') 402 | 403 | let drawHere = [] 404 | let position = null 405 | let verify = false 406 | 407 | if (!(harbinger.Position.getX() && harbinger.Position.getY())) { 408 | dungeon.fov.compute(pcX, pcY, sight, function (x, y) { 409 | verify = (x !== pcX && y !== pcY && 410 | Game.system.isFloor(x, y) && !Game.system.isAltar(x, y)) 411 | if (verify) { 412 | drawHere.push(x + ',' + y) 413 | } 414 | }) 415 | position = drawHere[Math.floor(ROT.RNG.getUniform() * drawHere.length)] 416 | harbinger.Position.setX(position.split(',')[0]) 417 | harbinger.Position.setY(position.split(',')[1]) 418 | } 419 | 420 | Game.screens.drawActor(harbinger) 421 | } 422 | 423 | // ``` In-game screens +++ 424 | Game.screens.main = new Game.Screen('main') 425 | 426 | Game.screens.main.initialize = function () { 427 | Game.entity.seed() 428 | Game.getEntity('seed').Seed.setSeed(Game.getDevSeed()) 429 | ROT.RNG.setSeed(Game.getEntity('seed').Seed.getSeed()) 430 | 431 | Game.entity.dungeon() 432 | Game.entity.message() 433 | 434 | Game.entity.pc() 435 | Game.entity.harbinger() 436 | Game.entity.altar() 437 | Game.entity.fog() 438 | 439 | Game.entity.timer() 440 | Game.getEntity('timer').scheduler.add(Game.getEntity('pc'), true) 441 | Game.getEntity('timer').scheduler.add(Game.getEntity('harbinger'), true) 442 | Game.getEntity('timer').engine.start() 443 | 444 | Game.system.placePC() 445 | Game.system.placeItem() 446 | 447 | Game.getEntity('message').Message.getMsgList().push( 448 | Game.text.tutorial('move')) 449 | Game.getEntity('message').Message.getMsgList().push( 450 | Game.text.tutorial('pick')) 451 | Game.getEntity('message').Message.getMsgList().push( 452 | Game.text.tutorial('drop')) 453 | Game.getEntity('message').Message.getMsgList().push( 454 | Game.text.tutorial('speed')) 455 | Game.getEntity('message').Message.getMsgList().push( 456 | Game.text.tutorial('altar')) 457 | } 458 | 459 | Game.screens.main.display = function () { 460 | Game.screens.drawBorder() 461 | Game.screens.drawVersion() 462 | Game.screens.drawStatus() 463 | Game.screens.drawSeed() 464 | 465 | Game.screens.drawDungeon() 466 | Game.screens.drawItem() 467 | Game.screens.drawActor(Game.getEntity('pc')) 468 | 469 | Game.screens.drawMessage() 470 | Game.screens.drawModeLine() 471 | 472 | Game.getEntity('harbinger').Counter.hasGhost() && Game.screens.drawGhost() 473 | Game.getEntity('altar').Sacrifice.getDrawAltar() && 474 | Game.screens.drawActor(Game.getEntity('altar'), true) 475 | } 476 | 477 | Game.screens.main.keyInput = function (e) { 478 | let keyAction = Game.input.getAction 479 | let x = Game.getEntity('pc').Position.getX() 480 | let y = Game.getEntity('pc').Position.getY() 481 | 482 | if (e.shiftKey) { 483 | if (keyAction(e, 'fixed') === 'develop') { 484 | Game.setDevelop() 485 | } 486 | if (Game.getDevelop()) { 487 | switch (keyAction(e, 'fixed')) { 488 | case 'skull': 489 | Game.entity.skull(Game.getEntity('pc').Position.getX() - 1, 490 | Game.getEntity('pc').Position.getY()) 491 | break 492 | case 'coin': 493 | Game.entity.coin(Game.getEntity('pc').Position.getX() - 1, 494 | Game.getEntity('pc').Position.getY()) 495 | break 496 | case 'gem': 497 | Game.entity.gem(Game.getEntity('pc').Position.getX() - 1, 498 | Game.getEntity('pc').Position.getY()) 499 | break 500 | case 'altar': 501 | Game.system.placeAltar() 502 | Game.system.placeItem(Game.system.placeFog()) 503 | break 504 | } 505 | } 506 | } else if (keyAction(e, 'move')) { 507 | Game.system.move(keyAction(e, 'move')) 508 | } else if (keyAction(e, 'fixed') === 'space') { 509 | Game.system.pickUp(x, y) 510 | } else if (keyAction(e, 'drop')) { 511 | Game.system.drop(keyAction(e, 'drop')) 512 | } else if (keyAction(e, 'fixed') === 'seed') { 513 | console.log(Game.getEntity('seed').Seed.getSeed()) 514 | } else if (Game.getDevelop()) { 515 | switch (keyAction(e, 'fixed')) { 516 | case 'turn': 517 | console.log(Game.getEntity('timer').scheduler.getTime()) 518 | break 519 | case 'fog': 520 | Game.getEntity('dungeon').Dungeon.setFov() 521 | break 522 | } 523 | } 524 | 525 | Game.display.clear() 526 | Game.screens.main.display() 527 | } 528 | 529 | // ----- Initialization +++++ 530 | window.onload = function () { 531 | if (!ROT.isSupported()) { 532 | window.alert(Game.text.error('browser')) 533 | return 534 | } 535 | document.getElementById('game').appendChild(Game.display.getContainer()) 536 | 537 | Game.display.clear() 538 | Game.screens.main.enter() 539 | } 540 | -------------------------------------------------------------------------------- /scripts/rot.min.js: -------------------------------------------------------------------------------- 1 | (function(b,a){"function"===typeof define&&define.amd?define([],a):"object"===typeof exports?module.exports=a():b.ROT=a()})(this,function(){var b={isSupported:function(){return!(!document.createElement("canvas").getContext||!Function.prototype.bind)},DEFAULT_WIDTH:80,DEFAULT_HEIGHT:25,DIRS:{4:[[0,-1],[1,0],[0,1],[-1,0]],8:[[0,-1],[1,-1],[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1]],6:[[-1,-1],[1,-1],[2,0],[1,1],[-1,1],[-2,0]]},VK_CANCEL:3,VK_HELP:6,VK_BACK_SPACE:8,VK_TAB:9,VK_CLEAR:12,VK_RETURN:13,VK_ENTER:14, 2 | VK_SHIFT:16,VK_CONTROL:17,VK_ALT:18,VK_PAUSE:19,VK_CAPS_LOCK:20,VK_ESCAPE:27,VK_SPACE:32,VK_PAGE_UP:33,VK_PAGE_DOWN:34,VK_END:35,VK_HOME:36,VK_LEFT:37,VK_UP:38,VK_RIGHT:39,VK_DOWN:40,VK_PRINTSCREEN:44,VK_INSERT:45,VK_DELETE:46,VK_0:48,VK_1:49,VK_2:50,VK_3:51,VK_4:52,VK_5:53,VK_6:54,VK_7:55,VK_8:56,VK_9:57,VK_COLON:58,VK_SEMICOLON:59,VK_LESS_THAN:60,VK_EQUALS:61,VK_GREATER_THAN:62,VK_QUESTION_MARK:63,VK_AT:64,VK_A:65,VK_B:66,VK_C:67,VK_D:68,VK_E:69,VK_F:70,VK_G:71,VK_H:72,VK_I:73,VK_J:74,VK_K:75,VK_L:76, 3 | VK_M:77,VK_N:78,VK_O:79,VK_P:80,VK_Q:81,VK_R:82,VK_S:83,VK_T:84,VK_U:85,VK_V:86,VK_W:87,VK_X:88,VK_Y:89,VK_Z:90,VK_CONTEXT_MENU:93,VK_NUMPAD0:96,VK_NUMPAD1:97,VK_NUMPAD2:98,VK_NUMPAD3:99,VK_NUMPAD4:100,VK_NUMPAD5:101,VK_NUMPAD6:102,VK_NUMPAD7:103,VK_NUMPAD8:104,VK_NUMPAD9:105,VK_MULTIPLY:106,VK_ADD:107,VK_SEPARATOR:108,VK_SUBTRACT:109,VK_DECIMAL:110,VK_DIVIDE:111,VK_F1:112,VK_F2:113,VK_F3:114,VK_F4:115,VK_F5:116,VK_F6:117,VK_F7:118,VK_F8:119,VK_F9:120,VK_F10:121,VK_F11:122,VK_F12:123,VK_F13:124,VK_F14:125, 4 | VK_F15:126,VK_F16:127,VK_F17:128,VK_F18:129,VK_F19:130,VK_F20:131,VK_F21:132,VK_F22:133,VK_F23:134,VK_F24:135,VK_NUM_LOCK:144,VK_SCROLL_LOCK:145,VK_CIRCUMFLEX:160,VK_EXCLAMATION:161,VK_DOUBLE_QUOTE:162,VK_HASH:163,VK_DOLLAR:164,VK_PERCENT:165,VK_AMPERSAND:166,VK_UNDERSCORE:167,VK_OPEN_PAREN:168,VK_CLOSE_PAREN:169,VK_ASTERISK:170,VK_PLUS:171,VK_PIPE:172,VK_HYPHEN_MINUS:173,VK_OPEN_CURLY_BRACKET:174,VK_CLOSE_CURLY_BRACKET:175,VK_TILDE:176,VK_COMMA:188,VK_PERIOD:190,VK_SLASH:191,VK_BACK_QUOTE:192,VK_OPEN_BRACKET:219, 5 | VK_BACK_SLASH:220,VK_CLOSE_BRACKET:221,VK_QUOTE:222,VK_META:224,VK_ALTGR:225,VK_WIN:91,VK_KANA:21,VK_HANGUL:21,VK_EISU:22,VK_JUNJA:23,VK_FINAL:24,VK_HANJA:25,VK_KANJI:25,VK_CONVERT:28,VK_NONCONVERT:29,VK_ACCEPT:30,VK_MODECHANGE:31,VK_SELECT:41,VK_PRINT:42,VK_EXECUTE:43,VK_SLEEP:95,Text:{RE_COLORS:/%([bc]){([^}]*)}/g,TYPE_TEXT:0,TYPE_NEWLINE:1,TYPE_FG:2,TYPE_BG:3,measure:function(a,c){for(var d={width:0,height:1},e=this.tokenize(a,c),f=0,g=0;gc){for(h=-1;;){var l=g.value.indexOf(" ",h+1);if(-1==l)break;if(e+l>c)break;h=l}-1!=h?g.value=this._breakInsideToken(a, 8 | d,h,!0):-1!=f?(g=a[f],d=g.value.lastIndexOf(" "),g.value=this._breakInsideToken(a,f,d,!0),d=f):g.value=this._breakInsideToken(a,d,c-e,!1)}else e+=g.value.length,-1!=g.value.indexOf(" ")&&(f=d);d++}else a.splice(d,1)}}a.push({type:b.Text.TYPE_NEWLINE});e=null;for(d=0;dc||0>a||c>=this._context.canvas.width||a>=this._context.canvas.height?[-1,-1]:this._backend.eventToPosition(c,a)};b.Display.prototype.draw=function(a,c,d,e,f){e||(e=this._options.fg);f||(f=this._options.bg);this._data[a+","+c]=[a,c,d,e,f];!0!==this._dirty&&(this._dirty||(this._dirty= 18 | {}),this._dirty[a+","+c]=!0)};b.Display.prototype.drawText=function(a,c,d,e){var f=null,g=null,h=a,l=1;e||(e=this._options.width-a);for(d=b.Text.tokenize(d,e);d.length;)switch(e=d.shift(),e.type){case b.Text.TYPE_TEXT:for(var m,n=!1,p,r=!1,q=0;qm||65500m||65518a?1/a:a;this._s0=(a>>>0)*this._frac;a=69069*a+1>>> 34 | 0;this._s1=a*this._frac;this._s2=(69069*a+1>>>0)*this._frac;this._c=1;return this},getUniform:function(){var a=2091639*this._s0+this._c*this._frac;this._s0=this._s1;this._s1=this._s2;this._c=a|0;return this._s2=a-this._c},getUniformInt:function(a,c){var d=Math.max(a,c),e=Math.min(a,c);return Math.floor(this.getUniform()*(d-e+1))+e},getNormal:function(a,c){do{var d=2*this.getUniform()-1,e=2*this.getUniform()-1;e=d*d+e*e}while(1this._options.order?a=a.slice(-this._options.order):a.lengthc){d=e;break}this._events.splice(d,0,a);this._eventTimes.splice(d,0,c)};b.EventQueue.prototype.get=function(){if(!this._events.length)return null;var a=this._eventTimes.splice(0, 41 | 1)[0];if(0n;n++){var p=h+2*g[n][0];var r=l+2*g[n][1];if(this._isFree(e,p,r,c,d)){e[p][r]=0;e[h+g[n][0]][l+g[n][1]]=0;h=p;l=r;m=!1;f++;break}}}while(!m)}}while(f+1c;c++)a[c][0]=0,a[c][1]=0;switch(Math.floor(4*b.RNG.getUniform())){case 0:a[0][0]= 53 | -1;a[1][0]=1;a[2][1]=-1;a[3][1]=1;break;case 1:a[3][0]=-1;a[2][0]=1;a[1][1]=-1;a[0][1]=1;break;case 2:a[2][0]=-1;a[3][0]=1;a[0][1]=-1;a[1][1]=1;break;case 3:a[1][0]=-1,a[0][0]=1,a[3][1]=-1,a[2][1]=1}};b.Map.IceyMaze.prototype._isFree=function(a,c,d,e,f){return 1>c||1>d||c>=e||d>=f?!1:a[c][d]};b.Map.EllerMaze=function(a,c){b.Map.call(this,a,c)};b.Map.EllerMaze.extend(b.Map);b.Map.EllerMaze.prototype.create=function(a){for(var c=this._fillMap(1),d=Math.ceil((this._width-2)/2),e=[],f=[],g=0;gb||b>=this._width||0>f||f>=this._height||(d+=1==this._map[b][f]?1:0)}return d};b.Map.Cellular.prototype.connect=function(a,c,d){c|| 58 | (c=0);var e=[],f={},g=1,h=[0,0];6==this._options.topology&&(g=2,h=[0,1]);for(var l=0;ll&&!(g.lengthf);l++);return[d,e]};b.Map.Cellular.prototype._getClosest= 60 | function(a,c){var d=null,e=null;for(k in c){var f=c[k],b=(f[0]-a[0])*(f[0]-a[0])+(f[1]-a[1])*(f[1]-a[1]);if(null==e||bl[1]&&(f--,n=1);m=ml[0]?m-n:l[1]%2?m-n:m+n;this._map[m][f]=b;n=[m,f];var p=this._pointKey(n);d[p]=n;delete e[p]}g&&g(c,a)};b.Map.Cellular.prototype._freeSpace=function(a,c,d){return 0<=a&&athis._options.timeLimit)break;var e=this._findWall();if(!e)break;var b= 65 | e.split(",");e=parseInt(b[0]);b=parseInt(b[1]);var g=this._getDiggingDirection(e,b);if(g){var h=0;do if(h++,this._tryFeature(e,b,g[0],g[1])){this._removeSurroundingWalls(e,b);this._removeSurroundingWalls(e-g[0],b-g[1]);break}while(ha||0>c||a>=this._width||c>=this._height?!1:1==this._map[a][c]};b.Map.Digger.prototype._canBeDugCallback=function(a,c){return 1>a||1>c||a+1>=this._width||c+1>=this._height?!1:1==this._map[a][c]};b.Map.Digger.prototype._priorityWallCallback=function(a,c){this._walls[a+","+c]=2};b.Map.Digger.prototype._firstRoom=function(){var a=b.Map.Feature.Room.createRandomCenter(Math.floor(this._width/ 67 | 2),Math.floor(this._height/2),this._options);this._rooms.push(a);a.create(this._digCallback)};b.Map.Digger.prototype._findWall=function(){var a=[],c=[];for(d in this._walls)2==this._walls[d]?c.push(d):a.push(d);a=c.length?c:a;if(!a.length)return null;var d=a.sort().random();delete this._walls[d];return d};b.Map.Digger.prototype._tryFeature=function(a,c,d,e){var f=b.RNG.getWeightedValue(this._features);f=b.Map.Feature[f].createRandomAt(a,c,d,e,this._options);if(!f.isValid(this._isWallCallback,this._canBeDugCallback))return!1; 68 | f.create(this._digCallback);f instanceof b.Map.Feature.Room&&this._rooms.push(f);f instanceof b.Map.Feature.Corridor&&(f.createPriorityWalls(this._priorityWallCallback),this._corridors.push(f));return!0};b.Map.Digger.prototype._removeSurroundingWalls=function(a,c){for(var d=b.DIRS[4],e=0;e=a||0>=c||a>=this._width- 69 | 1||c>=this._height-1)return null;for(var d=null,e=b.DIRS[4],f=0;fthis._options.timeLimit)return null;this._map=this._fillMap(1);this._dug=0;this._rooms=[];this._unconnected=[];this._generateRooms();if(!(2> 71 | this._rooms.length)&&this._generateCorridors())break}if(a)for(c=0;cthis._options.roomDugPercentage)break}while(d)};b.Map.Uniform.prototype._generateRoom=function(){for(var a=0;a=h&&d[b]<=l){var m=d.slice();e=null;switch(g){case 0:e=c.getTop()-1;break;case 1:e=c.getRight()+1;break;case 2:e=c.getBottom()+1;break;case 3:e=c.getLeft()-1}m[(b+1)%2]=e;this._digLine([d,m])}else if(d[b]l+1){e=d[b]-e[b];switch(g){case 0:case 1:m=0>e?3:1;break;case 2:case 3:m=0>e?1:3}m=this._placeInWall(c,(g+m)%4); 75 | if(!m)return!1;g=[0,0];g[b]=d[b];e=(b+1)%2;g[e]=m[e];this._digLine([d,g,m])}else{e=(b+1)%2;m=this._placeInWall(c,g);if(!m)return!1;g=Math.round((m[e]+d[e])/2);h=[0,0];l=[0,0];h[b]=d[b];h[e]=g;l[b]=m[b];l[e]=g;this._digLine([d,h,l,m])}a.addDoor(d[0],d[1]);c.addDoor(m[0],m[1]);b=this._unconnected.indexOf(a);-1!=b&&(this._unconnected.splice(b,1),this._connected.push(a));b=this._unconnected.indexOf(c);-1!=b&&(this._unconnected.splice(b,1),this._connected.push(c));return!0};b.Map.Uniform.prototype._placeInWall= 76 | function(a,c){var d=[0,0],e=[0,0],b=0;switch(c){case 0:e=[1,0];d=[a.getLeft(),a.getTop()-1];b=a.getRight()-a.getLeft()+1;break;case 1:e=[0,1];d=[a.getRight()+1,a.getTop()];b=a.getBottom()-a.getTop()+1;break;case 2:e=[1,0];d=[a.getLeft(),a.getBottom()+1];b=a.getRight()-a.getLeft()+1;break;case 3:e=[0,1],d=[a.getLeft()-1,a.getTop()],b=a.getBottom()-a.getTop()+1}for(var g=[],h=-2,l=0;la||0>c||a>=this._width||c>=this._height?!1:1==this._map[a][c]};b.Map.Uniform.prototype._canBeDugCallback= 78 | function(a,c){return 1>a||1>c||a+1>=this._width||c+1>=this._height?!1:1==this._map[a][c]};b.Map.Rogue=function(a,c,d){b.Map.call(this,a,c);this._options={cellWidth:3,cellHeight:3};for(var e in d)this._options[e]=d[e];this._options.hasOwnProperty("roomWidth")||(this._options.roomWidth=this._calculateRoomSize(this._width,this._options.cellWidth));this._options.hasOwnProperty("roomHeight")||(this._options.roomHeight=this._calculateRoomSize(this._height,this._options.cellHeight))};b.Map.Rogue.extend(b.Map); 79 | b.Map.Rogue.prototype.create=function(a){this.map=this._fillMap(1);this.rooms=[];this.connectedCells=[];this._initRooms();this._connectRooms();this._connectUnconnectedRooms();this._createRandomRoomConnections();this._createRooms();this._createCorridors();if(a)for(var c=0;ce&&(e=2);2>d&&(d=2);return[e,d]};b.Map.Rogue.prototype._initRooms= 80 | function(){for(var a=0;ag||g>=this._options.cellWidth||0>f||f>= 81 | this._options.cellHeight)){var h=this.rooms[a][c];if(0n||n>=a||0>m||m>=c)){e=this.rooms[n][m];f=!0;if(0==e.connections.length)break;for(n=0;nr-(q.y+q.height);)r++;if(0p-(q.x+ 84 | q.width);)p++;q=Math.round(b.RNG.getUniformInt(0,f-h)/2);for(var v=Math.round(b.RNG.getUniformInt(0,g-l)/2);p+q+h>=a;)q?q--:h--;for(;r+v+l>=c;)v?v--:l--;p+=q;r+=v;this.rooms[t][u].x=p;this.rooms[t][u].y=r;this.rooms[t][u].width=h;this.rooms[t][u].height=l;for(q=p;qd.cellx?(b=2,g=4):e.cellxd.celly?(b=3,g=1):e.cellyt){var u=1;var v=0}else u=0,v=1;var x=q-u+g,y=t-v+g;n=q-1+2*g;g=t-1+2*g;p=p.mod(b);b=r.mod(b);var w=.5-q*q-t*t;0<=w&&(w*=w,r=e[p+d[b]],r=this._gradients[r],h=w*w*(r[0]*q+r[1]*t));q=.5-x*x-y*y;0<=q&&(q*=q,r=e[p+u+d[b+v]],r=this._gradients[r],l=q*q*(r[0]*x+r[1]*y));q=.5-n*n-g*g;0<=q&&(q*=q,r=e[p+1+d[b+1]],r=this._gradients[r],m=q*q*(r[0]*n+r[1]*g));return 70*(h+l+m)};b.FOV=function(a,c){this._lightPasses= 98 | a;this._options={topology:8};for(var d in c)this._options[d]=c[d]};b.FOV.prototype.compute=function(a,c,d,b){};b.FOV.prototype._getCircle=function(a,c,d){var e=[];switch(this._options.topology){case 4:var f=1;var g=[0,1];var h=[b.DIRS[8][7],b.DIRS[8][1],b.DIRS[8][3],b.DIRS[8][5]];break;case 6:h=b.DIRS[6];f=1;g=[-1,1];break;case 8:h=b.DIRS[4],f=2,g=[-1,1]}a+=g[0]*d;c+=g[1]*d;for(g=0;ga)return c=this._visibleCoords(0,c,d,b),a=this._visibleCoords(360+a,360,d,b),c||a;for(var e=0;ec[0])return a=this._checkVisibility(a,[a[1],a[1]], 102 | d,b),c=this._checkVisibility([0,1],c,d,b),(a+c)/2;for(var e=0,g=!1;el&&e%2&&(h=!1);if(!h)return 0;g=l-e+1;if(g%2)e%2?(l=b[e],l=(c[0]*l[1]-l[0]*c[1])/(l[1]*c[1]),d&&b.splice(e,g,c)):(l=b[l],l=(l[0]*a[1]-a[0]*l[1])/(a[1]*l[1]),d&&b.splice(e,g,a));else if(e%2)m=b[e],l=b[l],l=(l[0]*m[1]- 103 | m[0]*l[1])/(m[1]*l[1]),d&&b.splice(e,g);else return d&&b.splice(e,g,a,c),1;return l/((c[0]*a[1]-a[0]*c[1])/(a[1]*c[1]))};b.FOV.RecursiveShadowcasting=function(a,c){b.FOV.call(this,a,c)};b.FOV.RecursiveShadowcasting.extend(b.FOV);b.FOV.RecursiveShadowcasting.OCTANTS=[[-1,0,0,1],[0,-1,1,0],[0,-1,-1,0],[-1,0,0,-1],[1,0,0,-1],[0,1,-1,0],[0,1,1,0],[1,0,0,1]];b.FOV.RecursiveShadowcasting.prototype.compute=function(a,c,d,e){e(a,c,0,1);for(var f=0;f=e;){e+=1;var v=a+e*h+q*l,x=c+e*m+q*n,y=(e-.5)/(q+.5),w=(e+ 106 | .5)/(q-.5);if(!(w>b)){if(yc;c++)d[c+1]+=16*d[c],d.splice(c,1);else d=(c=a.match(/rgb\(([0-9, ]+)\)/i))? 107 | c[1].split(/\s*,\s*/).map(function(a){return parseInt(a)}):[0,0,0];this._cache[a]=d}return d.slice()},add:function(a,c){for(var d=a.slice(),b=0;3>b;b++)for(var f=1;fd;d++)for(var b=1;bb;b++){for(var f=1;fd;d++){for(var b=1;barguments.length&&(d=.5);for(var b=a.slice(),f=0;3>f;f++)b[f]=Math.round(b[f]+d*(c[f]-a[f]));return b},interpolateHSL:function(a,c,b){3>arguments.length&&(b=.5);for(var d=this.rgb2hsl(a),f=this.rgb2hsl(c),g=0;3>g;g++)d[g]+=b*(f[g]-d[g]);return this.hsl2rgb(d)},randomize:function(a,c){c instanceof Array||(c=Math.round(b.RNG.getNormal(0,c)));for(var d=a.slice(),e=0;3> 109 | e;e++)d[e]+=c instanceof Array?Math.round(b.RNG.getNormal(0,c[e])):c;return d},rgb2hsl:function(a){var c=a[0]/255,b=a[1]/255;a=a[2]/255;var e=Math.max(c,b,a),f=Math.min(c,b,a),g=(e+f)/2;if(e==f)var h=f=0;else{var l=e-f;f=.5b&&(b+=1);1b? 110 | c:b<2/3?a+(c-a)*(2/3-b)*6:a},e=a[1];e=.5>c?c*(1+e):c+e-c*e;var f=2*c-e;c=b(f,e,a[0]+1/3);var g=b(f,e,a[0]);a=b(f,e,a[0]-1/3);return[Math.round(255*c),Math.round(255*g),Math.round(255*a)]},toRGB:function(a){return"rgb("+this._clamp(a[0])+","+this._clamp(a[1])+","+this._clamp(a[2])+")"},toHex:function(a){for(var c=[],b=0;3>b;b++)c.push(this._clamp(a[b]).toString(16).lpad("0",2));return"#"+c.join("")},_clamp:function(a){return 0>a?0:255m;m++){var n=Math.round(f[m]*g);h[m]=n;l+=n}l>this._options.emissionThreshold&&(b[e]=h)}}return b};b.Lighting.prototype._emitLightFromCell=function(a,c,b,e){var d=a+","+c;a=d in this._fovCache?this._fovCache[d]:this._updateFOV(a, 121 | c);for(var g in a){c=a[g];g in e?d=e[g]:(d=[0,0,0],e[g]=d);for(var h=0;3>h;h++)d[h]+=Math.round(b[h]*c)}return this};b.Lighting.prototype._updateFOV=function(a,c){var b={};this._fovCache[a+","+c]=b;var e=this._options.range;this._fov.compute(a,c,e,function(a,c,d,l){d=l*(1-d/e);0!=d&&(b[a+","+c]=d)}.bind(this));return b};b.Path=function(a,c,d,e){this._toX=a;this._toY=c;this._fromY=this._fromX=null;this._passableCallback=d;this._options={topology:8};for(var f in e)this._options[f]=e[f];this._dirs=b.DIRS[this._options.topology]; 122 | 8==this._options.topology&&(this._dirs=[this._dirs[0],this._dirs[2],this._dirs[4],this._dirs[6],this._dirs[1],this._dirs[3],this._dirs[5],this._dirs[7]])};b.Path.prototype.compute=function(a,b,d){};b.Path.prototype._getNeighbors=function(a,b){for(var c=[],e=0;e