pax_global_header00006660000000000000000000000064135650020220014505gustar00rootroot0000000000000052 comment=f64c774c59d0d063ae2d560f87959ae7649964de boohu-0.13.0/000077500000000000000000000000001356500202200127025ustar00rootroot00000000000000boohu-0.13.0/.gitignore000066400000000000000000000001421356500202200146670ustar00rootroot00000000000000*.[oa] *~ *.swp *.tar.gz *.nogit *.zip *.bz *.pdf *.bak boohu *.prof *.js *.map *.html boohu.wasm boohu-0.13.0/Changes000066400000000000000000000404661356500202200142070ustar00rootroot00000000000000v0.13 2019-11-19 + Some improvements for the browser version based on Harmonist code. + Fix rare crash in Tk version at startup. + use simpler math/rand, because we don't need crypto secure random numbers, and this works faster, specially on some platforms. + Fix unintuitive interaction between dancing rapier and tiny harpies blinking. + New tile for doors, backported from Harmonist. + New tile for “magical stairs”, now more like a “real” monolith, backported from Harmonist. + Update wasm version to latest API. + Do not print rod of lightning message when no targets. + You can now use arrow keys in menus too. Backported from Harmonist. + Fix interaction between har-kar gauntlets and tiny harpies in a pathological case. + Improvements in character dump. Mainly ideas from Harmonist. + Allow (x) to close/continue as well as an alternative to space and esc, and hint to that first. Backported from Harmonist. More portable for browsers. ----------------------------------------------------------------------------- v0.12.0 2018-12-19 Bugfix and minor improvements release: + Winged monsters produce wing flapping noise instead of footsteps noise, and there's a distinction between heavy and normal footsteps. This way you still cannot be sure what kind of monster it is, but you can have an idea. + Add a message when a monster falls asleep because of rod of sleeping or night magara. + Make fireball do a little more damage to account for the fact that it is somewhat more difficult to use than firebolt. + Fix colour scheme when there is no config file. Fix windows 8-colour scheme. + Fix spelling of “cyclops”. + Allow Page Up/Down as alternative scroll keys in log messages window. + Allow “z” and some other keys as alternatives to “.” to select target. + Make layout toggle automatically resize the window in Tk backend. ----------------------------------------------------------------------------- v0.11.1 2018-11-29 Bugfix release: + Fix colour handling issue in replay for the termbox-go backend. + Get the starting log message back (it was removed during the dungeon structure overhaul). + Make replay exit properly with the termbox-go backend. ----------------------------------------------------------------------------- v0.11.0 2018-11-28 Gameplay: + Overhaul of dungeon structure. The main dungeon is shortened to 8 levels, from Depth 1 to 8, and there are 3 optional levels as before, now from 9 to 11. This is to make the game more coffee-break (as intended), and also more dense: each level now should feel more special. In particular, weapon/rod/armour distribution is a little less random than before, and excluding some occasional permutations because of some sane randomisation, one will in particular find a rod in Depth 1, a weapon/shield in Depth 2, and an armour in Depth 3. Levels that do not get a weapon/rod/armour will get extra consumables. Monster band distribution and special levels have had various adjustments too. + Made confused monsters smarter. + New tree stone: any creature hurt while standing on the stone is lignified. + New aptitude: you occasionally teleport your foes when hurt. + New aptitude: you occasionally lignify your foes when hurt. + New aptitude: light-footed when hurt. + New rod of lignification - it works on monsters in a similar way to the player potion. + New rod of last hope - the damage it does is inversely proportional to your health. + New projectile: teleport magara - it teleports monsters in a square area. + New projectile: slowing magara - it releases a bolt slowing monsters. + Darts of confusion always hit. Less of them generated as a result, and removed “unusually accurate” aptitude. + New potion of accuracy that makes you never miss for a few turns. + Berserk status now forbids rod usage instead of potion usage. + New monster: mind celmist - uses a smitting attack and avoids melee. + New monster: tiny harpy - can blink when hurt, appears sometimes in place of goblins. + New monster: vampire that has a nauseous spit and can regenerate by drinking your blood. + New monster: tree mushrooms - slow, big clunky creatures that can lignify you by releasing spores. + New weapon: vampiric dagger, that heals you a little when hitting living monsters. + New weapon: hopeful sword - it hurts harder the more you're injured; two-handed. + New weapon: final blade - it can kill in one shot ennemies with less than half HP; two-handed; your maximum HP is reduced. + New weapon: dragon sabre - it hits harder against big monsters with lots of HP such as dragons or hydras. + Renamed sabre into assassin sabre. + Removed berserk sword. + Removed plain chain mail, leather armour and buckler. + Make shields a little less useful when surrounded: you can only block once per turn. + Reworked stealth: now hunting monsters may return to wandering mode when not seing you. The effect is much increased by wearing the harmonist robe. This gives more depth to stealth (the wandering/hunting difference matters now more). Other: + A new native Tk backend with graphical tiles. + A new replay feature: automatic recording of playthroughs. You can play ASCII and then replay with tiles (and vice-versa). + Many other minor gameplay and user interface fixes and improvements. ----------------------------------------------------------------------------- v0.10.0 2018-09-21 Gameplay: + New map feature: 4 distinct magical stones, static objects on the ground that have a special effect when a creature is hurt while standing on them. Magical stones have only 1 charge. Some special levels can get more stones. + New instable level: temporal walls can appear inadvertently. + New fire shield: it can burn nearby foliage after a successful block. + New potion of confusion: it confuses any monster in your line of sight. + New potion of torment that acts similarly to an explosive magara, but in the whole LOS area, including the player. + Rename ponderousness plates into turtle plates, and scintillating plates into shiny plates (shorter). User Interface: + Many grammar fixes (by kilobyte). + No more need to refresh the page to play again in the browser version. + Change the color of some statuses when they are about to expire. + Avoid clash between browser and Boohu keys when the canvas is focused. ----------------------------------------------------------------------------- v0.9.0 2018-07-25 Items and Monsters: + New monster: winged milfids that can make you swap positions. + New monster: mad nixe, that can throw an attracting magic bolt to you, moving you to a square adjacent to the monster (the one corresponding to LOS ray). + Now hounds can smell you in a short range: this means that fog and dense foliage are not always enough to flee from them. + New weapon: har-kar gauntlets which you used “unarmed”: it is a (weak) two-handed weapon that has a particular attack patern: if there is a free cell after monsters in a given direction, you hit every monster in the path, move after them, and hit another monster (if any), which can be very useful in corridors, as well as for escaping. + Double sword got renamed into berserk sword, which occasionally makes you berserk when attacking while severely injured. + Sword got renamed into sabre, and its mechanics were changed too: now this weapon has better accuracy the more injured the monster is. + New dancing rapier weapon: it makes you swap positions with monsters you attack, attempting a hit to any monster behind in the same direction, with extra damage. + Axes now deal 1-2 extra damage in open areas - actually when neighbor cells common to you and the monster do not have any walls. The idea is to make risky non-corridor fights potentially optimal with axes. + New defender flail weapon: it hits harder the more you keep attacking without moving; moreover, moving toward a monster moves the monster to you instead. + New robes: speed robe, celmist robe and harmonist robe. These armours give you no physical protection. The first make you speedy. The celmist robe makes you better at using rods (better recharge rate, more mana and an extra charge), and harmonist robe makes you better at being stealthy (slightly reduced LOS range, stealthy movement and a less good version of Frundis staff noise mitigation). There has been some work for rebalancing stealth. + Old plate armour got renamed into chain mail (which is gone). + New ponderousness plates armour appeared that gives great protection at a movement speed cost. + New armour: smoking scales that leaves a short-lived fog behind you each time you move. It really is useful to escape from monsters in corridors, less so in open areas. + New armour: scintillating plates, good protection but +1 LOS range. + New shields: bashing, confusing and earth shields that occasionally on block have a special effect. Bashing moves the monsters several squares away in a direction. Confusing just confuses the monster. The earth shield gives much more protection than the others, but impacts produce a noisy sound that can destruct nearby walls, which sometimes can be bad (for example in a corridor) or good (due to the resulting fog). + New rod: rod of lightning, an electricity attack to every monster connected to you (similar to lightning whip, but for a rod). A little less strong per monster than fireball, but probably stronger overall. + New rod: rod of sleeping, that makes monsters sleep in a 1-radius square area. + New potion of shadows: LOS range of 1 for a short time. + New projectile: night magara that produces sleep inducing clouds + Reduced duration for potion of swapping, it was OP. Other gameplay changes: + Three new optional levels up to Depth:15! Each of those has normal downstairs, and magical stairs leading you out (except Depth:14). This way on good runs you can chose to go for more challenge. + New aptitude: “you have good ears”. It makes you hear monster footsteps with higher probability and with range improved by 1. + One or two levels per game get special fauna, with monster bands related to a particular monster or theme. + Sometimes some lonely out of depth monsters can appear in early levels, for more diversity early on. + Now there is always at least two weapons generated per game, so that you have a choice before the last levels. + More game statistics. User Interface: + New web version with 16x24 tiles, and much improved mouse support. + There is now a setting in-game to change default LOS color to a dark one, for less contrast. + Now in compact 80x24 layout you see a log line even in target mode. + Improved the settings menu: now you can toggle normal/compact layout and tiles/ascii mode. ----------------------------------------------------------------------------- v0.8.0 2018-06-02 Game related changes: + No more automatic HP and MP regeneration: now you have to rest. This is inspired from TGGW, but adapted to Boohu's fast-paced flow and DCSS-like short temporal effects: you have to wait for your status to wear off before resting (the first attempt waits for this), and then you attempt to rest and are successful at it if you are not interrumpted by monsters during a few turns. You replenish HP and MP, monsters do too, and some monsters might awake when you rest, so that you want to avoid doing it too often. + Reworked cyclops: less accurate, rocks do never confuse, but the rocks create temporal obstacles (walls) for the player. The placement of the rock depends on whether the player dodged (rock behind the player), blocked with the shield (rock in front of the player), or got hit (the rock takes player cell if possible, and the player gets 1 cell moved backwards). + Renamed worms into farmer worms: they now furrow and help foliage grow as they move. + New potion of digging that lets you walk into walls for a few turns. + New potion of dreams: you get the current location of sleeping monsters. + New potion of swapping that makes you dance with monsters (swap positions) when you move or are hit by them. + Explosive magara and nadre explosions now can occasionally destroy walls. + Removed javelines: some of their damage integrated in darts of confusion, which are more funny and versatile. + Improvements in dungeon generation: new town-like map generator, special rooms and/or foliage in most maps. + Improved morgue files. They now show some miscellaneous statistics at the end, including some per-depth statistics. User interface: + Improved mouse support: now there are several buttons for the more common actions, instead of one huge menu. + New compact alternative style for 80x24 terminals. + The game tries to display several log messages in one line when there is enough room for it (the algorithm could still be improved). + Improved animations. Animations can be disabled with `-n` option. A few bug fixes and many other little improvements. ----------------------------------------------------------------------------- v0.7.1 2018-05-09 Bugfix release. Fix a crash with “G” when no safe path to stairs. ----------------------------------------------------------------------------- v0.7 2018-05-08 User interface: + Animations: combat, explosions, magic mapping, menu selections … + Better usage of screen for menus (try to show as much of the map at any time). + Rebindable key bindings. + Many little look improvements. Game: + New narrative: you're searching medicinal simella Underground plants for your village. + Gold replaced by simella plants. + Foliage and doors can be burned by fire explosions and lightning. Fire spreads. As a result, terrain is now fully destructible. + New projectile: explosive magara. Similar to a nadre explosion. + Simpler and more correct system for zone exclusion. + Miscellaneous bug fixes and improvements. ----------------------------------------------------------------------------- v0.6.1 2018-04-28 Bugfix release : fix an information leak in position description. ----------------------------------------------------------------------------- v0.6 2018-04-27 Game: + New rod of swapping that makes you swap positions with a monster. + New weapon: lightning whip which hits a monster and every monster connected to it (inspired from minmay's TOME4 Harbinger addon class). + New potion of swiftness: replaces potions of running and evasion. + Some minor tweaks in generation of weapons. + Aptitude “regenerate quickly” is no more. Dungeon generation: + Sometimes special rooms with colums appear in cave-like maps. + Add sometimes cave-like vegetation in ruin and tree-like cave maps. + Add special rooms with columns in maps with rooms. User interface: + Better behaved auto-exploration, in particular with respect to excluded areas. + Various improvements in log messages and timeline of character dump. In particular, the timeline keeps track of dangerous monster you see in addition to dangerous you kill. ----------------------------------------------------------------------------- v0.5 2018-04-14 Highlights: + new unique monster: Marevor, which can teleport people around + new unique weapon: staff Frundis, which can confuse, generate fog and reduce noise + new monster: oklob plant, doesn't move but can throw confusing acid projectiles + actions which destoy walls produce some fog now + improvements in auto-exploration (smarter and faster) + little improvements to monster hunting + new command line option -c: use centered camera + new experimental javascript backend for the browser (still lacking some features) + miscellaneous improvements, bug fixes and optimizations ----------------------------------------------------------------------------- v0.4 2017-10-29 New features highlight: + New terrain feature: dense foliage reducing your line of sight. + New terrain feature: doors that automatically open and close. + New monsters: explosive nadre, acid mound, brizzias, blinking frog, mirror specter. + Several new potions: potion of walls, potion of controlled blink. + New feature: you hear footsteps. + New aptitudes: smoke, confusing gas. User interface: + Mouse support for movement and targetting. + Colored console messages. And many other small improvements and fixes. ----------------------------------------------------------------------------- v0.3 2017-10-01 ----------------------------------------------------------------------------- v0.2 2017-09-22 ----------------------------------------------------------------------------- v0.1 2017-09-16 boohu-0.13.0/LICENSE000066400000000000000000000013661356500202200137150ustar00rootroot00000000000000Copyright (c) 2017 Yon Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. boohu-0.13.0/README.md000066400000000000000000000065771356500202200142000ustar00rootroot00000000000000Break Out Of Hareka's Underground (Boohu) is a roguelike game mainly inspired from DCSS and its tavern, with some ideas from Brogue, but aiming for very short games, almost no character building, and a simplified inventory. *Every year, the elders send someone to collect medicinal simella plants in the Underground. This year, the honor fell upon you, and so here you are. According to the elders, deep in the Underground, magical stairs will lead you back to your village. Along the way, you will collect simellas, as well as various items that will help you deal with monsters, which you may fight or flee...* ![Boohu introduction screen](https://download.tuxfamily.org/boohu/intro-screen-tiles.png) Screenshot and Website ---------------------- [![Introduction Screeshot](https://download.tuxfamily.org/boohu/screenshot.png)](https://download.tuxfamily.org/boohu/index.html) You can visit the [game's website](https://download.tuxfamily.org/boohu/index.html) for more informations, tips, screenshots and asciicasts. You will also be able to play in the browser and download pre-built binaries for the latest release. Install from Sources -------------------- In all cases, you need first to perform the following preliminaries: + Install the [go compiler](https://golang.org/). + Set `$GOPATH` variable (for example `export GOPATH=$HOME/go`, the default value in recent Go versions). + Add `$GOPATH/bin` to your `$PATH` (for example `export PATH="$PATH:$GOPATH/bin"`). ### ASCII You can build a native ASCII version from source by using this command: + `go get -u git.tuxfamily.org/boohu/boohu.git`. The `boohu` command should now be available (you may have to rename it to remove the `.git` suffix). The only dependency outside of the go standard library is the lightweight curses-like library [termbox-go](https://github.com/nsf/termbox-go), which is installed automatically by the previous `go get` command. *Portability note.* If you happen to experience input problems, try adding option `--tags tcell` or `--tags ansi` to the `go get` command. The first will use [tcell](https://github.com/gdamore/tcell) instead of termbox-go, which is more portable (works on OpenBSD). The second will work on POSIX systems with a `stty` command. ### Tiles You can build a graphical version depending on Tcl/Tk (8.6) using this command: go get -u --tags tk git.tuxfamily.org/boohu/boohu.git This will install the [gothic](https://github.com/nsf/gothic) Go bindings for Tcl/Tk. You need to install Tcl/Tk first. With Go 1.11 or later, you can also build the WebAssembly version with: GOOS=js GOARCH=wasm go build --tags js -o boohu.wasm You can then play by serving a directory containing the wasm file via http. The directory should contain some other files that you can find in the main website instance. Colors ------ If the default colors do not display nicely on your terminal emulator, you can use the `-s` option: `boohu -s` to use the 16-color palette, which will display nicely if the [solarized](http://ethanschoonover.com/solarized) palette is used. Configurations are available for most terminal emulators, otherwise, colors may have to be configured manually to one's liking in the terminal emulator options. Documentation ------------- See the man page boohu(6) for more information on command line options and use of the replay file. For example: boohu -r _ launches an auto-replay of your last game. boohu-0.13.0/animation.go000066400000000000000000000232501356500202200152120ustar00rootroot00000000000000package main import "time" func (ui *gameui) SwappingAnimation(mpos, ppos position) { if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) _, fgm, bgColorm := ui.PositionDrawing(mpos) _, _, bgColorp := ui.PositionDrawing(ppos) ui.DrawAtPosition(mpos, true, 'Φ', fgm, bgColorp) ui.DrawAtPosition(ppos, true, 'Φ', ColorFgPlayer, bgColorm) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawAtPosition(mpos, true, 'Φ', ColorFgPlayer, bgColorp) ui.DrawAtPosition(ppos, true, 'Φ', fgm, bgColorm) ui.Flush() time.Sleep(75 * time.Millisecond) } func (ui *gameui) TeleportAnimation(from, to position, showto bool) { if DisableAnimations { return } _, _, bgColorf := ui.PositionDrawing(from) _, _, bgColort := ui.PositionDrawing(to) ui.DrawAtPosition(from, true, 'Φ', ColorCyan, bgColorf) ui.Flush() time.Sleep(75 * time.Millisecond) if showto { ui.DrawAtPosition(from, true, 'Φ', ColorBlue, bgColorf) ui.DrawAtPosition(to, true, 'Φ', ColorCyan, bgColort) ui.Flush() time.Sleep(75 * time.Millisecond) } } type explosionStyle int const ( FireExplosion explosionStyle = iota WallExplosion AroundWallExplosion ) func (ui *gameui) ProjectileTrajectoryAnimation(ray []position, fg uicolor) { if DisableAnimations { return } for i := len(ray) - 1; i >= 0; i-- { pos := ray[i] r, fgColor, bgColor := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, true, '•', fg, bgColor) ui.Flush() time.Sleep(30 * time.Millisecond) ui.DrawAtPosition(pos, true, r, fgColor, bgColor) } } func (ui *gameui) MonsterProjectileAnimation(ray []position, r rune, fg uicolor) { if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) for i := 0; i < len(ray); i++ { pos := ray[i] or, fgColor, bgColor := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, true, r, fg, bgColor) ui.Flush() time.Sleep(30 * time.Millisecond) ui.DrawAtPosition(pos, true, or, fgColor, bgColor) } } func (ui *gameui) ExplosionAnimationAt(pos position, fg uicolor) { g := ui.g _, _, bgColor := ui.PositionDrawing(pos) mons := g.MonsterAt(pos) r := ';' switch RandInt(9) { case 0, 6: r = ',' case 1: r = '}' case 2: r = '%' case 3, 7: r = ':' case 4: r = '\\' case 5: r = '~' } if mons.Exists() || g.Player.Pos == pos { r = '√' } //ui.DrawAtPosition(pos, true, r, fg, bgColor) ui.DrawAtPosition(pos, true, r, bgColor, fg) } func (ui *gameui) ExplosionAnimation(es explosionStyle, pos position) { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(20 * time.Millisecond) colors := [2]uicolor{ColorFgExplosionStart, ColorFgExplosionEnd} if es == WallExplosion || es == AroundWallExplosion { colors[0] = ColorFgExplosionWallStart colors[1] = ColorFgExplosionWallEnd } for i := 0; i < 3; i++ { nb := g.Dungeon.FreeNeighbors(pos) if es != AroundWallExplosion { nb = append(nb, pos) } for _, npos := range nb { fg := colors[RandInt(2)] if !g.Player.LOS[npos] { continue } ui.ExplosionAnimationAt(npos, fg) } ui.Flush() time.Sleep(100 * time.Millisecond) } time.Sleep(20 * time.Millisecond) } func (ui *gameui) TormentExplosionAnimation() { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(20 * time.Millisecond) colors := [3]uicolor{ColorFgExplosionStart, ColorFgExplosionEnd, ColorFgMagicPlace} for i := 0; i < 3; i++ { for npos, b := range g.Player.LOS { if !b { continue } fg := colors[RandInt(3)] ui.ExplosionAnimationAt(npos, fg) } ui.Flush() time.Sleep(100 * time.Millisecond) } time.Sleep(20 * time.Millisecond) } func (ui *gameui) WallExplosionAnimation(pos position) { if DisableAnimations { return } colors := [2]uicolor{ColorFgExplosionWallStart, ColorFgExplosionWallEnd} for _, fg := range colors { _, _, bgColor := ui.PositionDrawing(pos) //ui.DrawAtPosition(pos, true, '☼', fg, bgColor) ui.DrawAtPosition(pos, true, '☼', bgColor, fg) ui.Flush() time.Sleep(25 * time.Millisecond) } } func (ui *gameui) FireBoltAnimation(ray []position) { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) colors := [2]uicolor{ColorFgExplosionStart, ColorFgExplosionEnd} for j := 0; j < 3; j++ { for i := len(ray) - 1; i >= 0; i-- { fg := colors[RandInt(2)] pos := ray[i] _, _, bgColor := ui.PositionDrawing(pos) mons := g.MonsterAt(pos) r := '*' if RandInt(2) == 0 { r = '×' } if mons.Exists() { r = '√' } //ui.DrawAtPosition(pos, true, r, fg, bgColor) ui.DrawAtPosition(pos, true, r, bgColor, fg) } ui.Flush() time.Sleep(100 * time.Millisecond) } time.Sleep(25 * time.Millisecond) } func (ui *gameui) SlowingMagaraAnimation(ray []position) { if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) colors := [2]uicolor{ColorFgConfusedMonster, ColorFgMagicPlace} for j := 0; j < 3; j++ { for i := len(ray) - 1; i >= 0; i-- { fg := colors[RandInt(2)] pos := ray[i] _, _, bgColor := ui.PositionDrawing(pos) r := '*' if RandInt(2) == 0 { r = '×' } ui.DrawAtPosition(pos, true, r, bgColor, fg) } ui.Flush() time.Sleep(100 * time.Millisecond) } time.Sleep(25 * time.Millisecond) } func (ui *gameui) ProjectileSymbol(dir direction) (r rune) { switch dir { case E, ENE, ESE, WNW, W, WSW: r = '—' case NE, SW: r = '/' case NNE, N, NNW, SSW, S, SSE: r = '|' case NW, SE: r = '\\' } return r } func (ui *gameui) ThrowAnimation(ray []position, hit bool) { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) for i := len(ray) - 1; i >= 0; i-- { pos := ray[i] r, fgColor, bgColor := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, true, ui.ProjectileSymbol(pos.Dir(g.Player.Pos)), ColorFgProjectile, bgColor) ui.Flush() time.Sleep(30 * time.Millisecond) ui.DrawAtPosition(pos, true, r, fgColor, bgColor) } if hit { pos := ray[0] ui.HitAnimation(pos, true) } time.Sleep(30 * time.Millisecond) } func (ui *gameui) MonsterJavelinAnimation(ray []position, hit bool) { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) for i := 0; i < len(ray); i++ { pos := ray[i] r, fgColor, bgColor := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, true, ui.ProjectileSymbol(pos.Dir(g.Player.Pos)), ColorFgMonster, bgColor) ui.Flush() time.Sleep(30 * time.Millisecond) ui.DrawAtPosition(pos, true, r, fgColor, bgColor) } time.Sleep(30 * time.Millisecond) } func (ui *gameui) HitAnimation(pos position, targeting bool) { g := ui.g if DisableAnimations { return } if !g.Player.LOS[pos] { return } ui.DrawDungeonView(NoFlushMode) _, _, bgColor := ui.PositionDrawing(pos) mons := g.MonsterAt(pos) if mons.Exists() || pos == g.Player.Pos { ui.DrawAtPosition(pos, targeting, '√', ColorFgAnimationHit, bgColor) } else { ui.DrawAtPosition(pos, targeting, '∞', ColorFgAnimationHit, bgColor) } ui.Flush() time.Sleep(50 * time.Millisecond) } func (ui *gameui) LightningHitAnimation(targets []position) { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(25 * time.Millisecond) colors := [2]uicolor{ColorFgExplosionStart, ColorFgExplosionEnd} for j := 0; j < 2; j++ { for _, pos := range targets { _, _, bgColor := ui.PositionDrawing(pos) mons := g.MonsterAt(pos) if mons.Exists() || pos == g.Player.Pos { ui.DrawAtPosition(pos, false, '√', bgColor, colors[RandInt(2)]) } else { ui.DrawAtPosition(pos, false, '∞', bgColor, colors[RandInt(2)]) } } ui.Flush() time.Sleep(100 * time.Millisecond) } } func (ui *gameui) WoundedAnimation() { g := ui.g if DisableAnimations { return } r, _, bg := ui.PositionDrawing(g.Player.Pos) ui.DrawAtPosition(g.Player.Pos, false, r, ColorFgHPwounded, bg) ui.Flush() time.Sleep(50 * time.Millisecond) if g.Player.HP <= 15 { ui.DrawAtPosition(g.Player.Pos, false, r, ColorFgHPcritical, bg) ui.Flush() time.Sleep(50 * time.Millisecond) } } func (ui *gameui) DrinkingPotionAnimation() { g := ui.g if DisableAnimations { return } ui.DrawDungeonView(NormalMode) time.Sleep(50 * time.Millisecond) r, fg, bg := ui.PositionDrawing(g.Player.Pos) ui.DrawAtPosition(g.Player.Pos, false, r, ColorGreen, bg) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawAtPosition(g.Player.Pos, false, r, ColorYellow, bg) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawAtPosition(g.Player.Pos, false, r, fg, bg) ui.Flush() } func (ui *gameui) StatusEndAnimation() { g := ui.g if DisableAnimations { return } r, fg, bg := ui.PositionDrawing(g.Player.Pos) ui.DrawAtPosition(g.Player.Pos, false, r, ColorViolet, bg) ui.Flush() time.Sleep(100 * time.Millisecond) ui.DrawAtPosition(g.Player.Pos, false, r, fg, bg) ui.Flush() } func (ui *gameui) MenuSelectedAnimation(m menu, ok bool) { if DisableAnimations { return } if !ui.Small() { var message string if m == MenuInteract { message = ui.UpdateInteractButton() } else { message = m.String() } if message == "" { return } if ok { ui.DrawColoredText(message, MenuCols[m][0], DungeonHeight, ColorCyan) } else { ui.DrawColoredText(message, MenuCols[m][0], DungeonHeight, ColorMagenta) } ui.Flush() time.Sleep(25 * time.Millisecond) ui.DrawColoredText(m.String(), MenuCols[m][0], DungeonHeight, ColorViolet) } } func (ui *gameui) MagicMappingAnimation(border []int) { if DisableAnimations { return } for _, i := range border { pos := idxtopos(i) r, fg, bg := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, false, r, fg, bg) } ui.Flush() time.Sleep(12 * time.Millisecond) } boohu-0.13.0/ansi.go000066400000000000000000000053051356500202200141660ustar00rootroot00000000000000// +build ansi package main import ( "bufio" "fmt" "os" "os/exec" ) var ch chan uiInput var interrupt chan bool func init() { ch = make(chan uiInput, 100) interrupt = make(chan bool) } type gameui struct { g *game bStdin *bufio.Reader bStdout *bufio.Writer cursor position stty string // below unused for this backend menuHover menu itemHover int } func (ui *gameui) Init() error { ui.bStdin = bufio.NewReader(os.Stdin) ui.bStdout = bufio.NewWriter(os.Stdout) fmt.Fprint(ui.bStdout, "\x1b[2J") ui.HideCursor() fmt.Fprintf(ui.bStdout, "\x1b[?25l") cmd := exec.Command("stty", "-g") cmd.Stdin = os.Stdin save, err := cmd.Output() if err != nil { save = []byte("sane") } ui.stty = string(save) cmd = exec.Command("stty", "raw", "-echo") cmd.Stdin = os.Stdin cmd.Run() ui.menuHover = -1 go func() { for { r, _, err := ui.bStdin.ReadRune() if err == nil { ch <- uiInput{key: string(r)} } } }() return nil } func (ui *gameui) Close() { fmt.Fprint(ui.bStdout, "\x1b[2J") fmt.Fprintf(ui.bStdout, "\x1b[?25h") ui.bStdout.Flush() cmd := exec.Command("stty", ui.stty) cmd.Stdin = os.Stdin err := cmd.Run() if err != nil { cmd = exec.Command("stty", "sane") cmd.Stdin = os.Stdin cmd.Run() } } func (ui *gameui) MoveTo(x, y int) { fmt.Fprintf(ui.bStdout, "\x1b[%d;%dH", y+1, x+1) } func (ui *gameui) Flush() { ui.DrawLogFrame() var prevfg, prevbg uicolor first := true var prevx, prevy int for _, cdraw := range ui.g.DrawLog[len(ui.g.DrawLog)-1].Draws { cell := cdraw.Cell x, y := cdraw.X, cdraw.Y pfg := true pbg := true pxy := true if first { prevfg = cell.Fg prevbg = cell.Bg prevx = x prevy = y first = false } else { if prevfg == cell.Fg { pfg = false } else { prevfg = cell.Fg } if prevbg == cell.Bg { pbg = false } else { prevbg = cell.Bg } if x == prevx+1 && y == prevy { pxy = false } } if pxy { ui.MoveTo(x, y) } if pfg { fmt.Fprintf(ui.bStdout, "\x1b[38;5;%dm", cell.Fg) } if pbg { fmt.Fprintf(ui.bStdout, "\x1b[48;5;%dm", cell.Bg) } ui.bStdout.WriteRune(cell.R) } ui.MoveTo(ui.cursor.X, ui.cursor.Y) fmt.Fprintf(ui.bStdout, "\x1b[0m") ui.bStdout.Flush() } func (ui *gameui) ApplyToggleLayout() { GameConfig.Small = !GameConfig.Small if GameConfig.Small { ui.Clear() ui.Flush() UIHeight = 24 UIWidth = 80 } else { UIHeight = 26 UIWidth = 100 } ui.g.DrawBuffer = make([]UICell, UIWidth*UIHeight) ui.Clear() } func (ui *gameui) Small() bool { return GameConfig.Small } func (ui *gameui) Interrupt() { interrupt <- true } func (ui *gameui) PollEvent() (in uiInput) { select { case in = <-ch: case in.interrupt = <-interrupt: } return in } boohu-0.13.0/aptitude.go000066400000000000000000000034101356500202200150460ustar00rootroot00000000000000package main type aptitude int const ( AptObstruction aptitude = iota AptAgile AptFast AptHealthy AptStealthyMovement AptScales AptStealthyLOS AptMagic AptConfusingGas AptSmoke AptHear AptTeleport AptLignification AptStrong ) const NumApts = int(AptStrong) + 1 func (ap aptitude) String() string { var text string switch ap { case AptObstruction: text = "The earth occasionally blows monsters away when hurt." case AptAgile: text = "You are agile." case AptFast: text = "You move fast." case AptHealthy: text = "You are healthy." case AptStealthyMovement: text = "You move stealthily." case AptScales: text = "You are covered by scales." case AptHear: text = "You have good ears." case AptStrong: text = "You are strong." case AptMagic: text = "You have big magic reserves." case AptStealthyLOS: text = "The shadows follow you. (reduced LOS)" case AptConfusingGas: text = "You occasionally release some confusing gas when hurt." case AptSmoke: text = "You occasionally get energetic and emit smoke clouds when hurt." case AptLignification: text = "Nature occasionally lignifies your foes when hurt." case AptTeleport: text = "You occasionally teleport your foes when hurt." } return text } func (g *game) RandomApt() (aptitude, bool) { count := 0 var apt aptitude for { count++ if count > 1000 { break } r := RandInt(NumApts) apt = aptitude(r) if g.Player.Aptitudes[apt] { continue } return apt, true } return apt, false } func (g *game) ApplyAptitude(ap aptitude) { if g.Player.Aptitudes[ap] { // should not happen g.PrintStyled("Hm… You already have that aptitude. "+ap.String(), logError) return } g.Player.Aptitudes[ap] = true g.PrintStyled("You feel different. "+ap.String(), logSpecial) } boohu-0.13.0/assets/000077500000000000000000000000001356500202200142045ustar00rootroot00000000000000boohu-0.13.0/assets/style.css000066400000000000000000000026321356500202200160610ustar00rootroot00000000000000/* Colors from http://ethanschoonover.com/solarized */ html{ background-color:#fdf6e3; color:#657b83; font-size:16px; } canvas:focus { outline: black 5px solid; } canvas:hover { outline: black 5px solid; } body { margin-left:2%; } p{ max-width:80ch; text-align:justify; } details{ background-color:#eee8d5 } span.fg0 { color: #073642 } span.fg1 { color: #dc322f } span.fg2 { color: #859900 } span.fg3 { color: #b58900 } span.fg4 { color: #268bd2 } span.fg5 { color: #d33682 } span.fg6 { color: #2aa198 } span.fg7 { color: #eee8d5 } span.fg8 { color: #002b36 } span.fg9 { color: #cb4b16 } span.fg10 { color: #586e75 } span.fg11 { color: #657b83 } span.fg12 { color: #839496 } span.fg13 { color: #6c71c4 } span.fg14 { color: #93a1a1 } span.fg15 { color: #fdf6e3 } span.bg0 { background-color: #073642 } span.bg1 { background-color: #dc322f } span.bg2 { background-color: #859900 } span.bg3 { background-color: #b58900 } span.bg4 { background-color: #b58900 } span.bg5 { background-color: #d33682 } span.bg6 { background-color: #2aa198 } span.bg7 { background-color: #eee8d5 } span.bg8 { background-color: #002b36 } span.bg9 { background-color: #cb4b16 } span.bg10 { background-color: #586e75 } span.bg11 { background-color: #657b83 } span.bg12 { background-color: #839496 } span.bg13 { background-color: #6c71c4 } span.bg14 { background-color: #93a1a1 } span.bg15 { background-color: #fdf6e3 } boohu-0.13.0/astar.go000066400000000000000000000074521356500202200143530ustar00rootroot00000000000000// code of this file is a modified version of code from // github.com/beefsack/go-astar, which has the following license: // // Copyright (c) 2014 Michael Charles Alexander // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package main import ( "container/heap" ) type node struct { Pos position Cost int Rank int Parent *position Open bool Closed bool Index int Num int } type nodeMap map[position]*node var nodeCache []node func init() { nodeCache = make([]node, 0, DungeonNCells) } func (nm nodeMap) get(p position) *node { n, ok := nm[p] if !ok { nodeCache = append(nodeCache, node{Pos: p}) n = &nodeCache[len(nodeCache)-1] nm[p] = n } return n } type Astar interface { Neighbors(position) []position Cost(position, position) int Estimation(position, position) int } func AstarPath(ast Astar, from, to position) (path []position, length int, found bool) { nodeCache = nodeCache[:0] nm := nodeMap{} nq := &priorityQueue{} heap.Init(nq) fromNode := nm.get(from) fromNode.Open = true num := 0 fromNode.Num = num heap.Push(nq, fromNode) for { if nq.Len() == 0 { // There's no path, return found false. return } current := heap.Pop(nq).(*node) current.Open = false current.Closed = true if current.Pos == to { // Found a path to the goal. p := []position{} curr := current for { p = append(p, curr.Pos) if curr.Parent == nil { break } curr = nm[*curr.Parent] } return p, current.Cost, true } for _, neighbor := range ast.Neighbors(current.Pos) { cost := current.Cost + ast.Cost(current.Pos, neighbor) neighborNode := nm.get(neighbor) if cost < neighborNode.Cost { if neighborNode.Open { heap.Remove(nq, neighborNode.Index) } neighborNode.Open = false neighborNode.Closed = false } if !neighborNode.Open && !neighborNode.Closed { neighborNode.Cost = cost neighborNode.Open = true neighborNode.Rank = cost + ast.Estimation(neighbor, to) neighborNode.Parent = ¤t.Pos num++ neighborNode.Num = num heap.Push(nq, neighborNode) } } } } // A priorityQueue implements heap.Interface and holds Nodes. The // priorityQueue is used to track open nodes by rank. type priorityQueue []*node func (pq priorityQueue) Len() int { return len(pq) } func (pq priorityQueue) Less(i, j int) bool { //return pq[i].Rank < pq[j].Rank return pq[i].Rank < pq[j].Rank || pq[i].Rank == pq[j].Rank && pq[i].Num < pq[j].Num } func (pq priorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].Index = i pq[j].Index = j } func (pq *priorityQueue) Push(x interface{}) { n := len(*pq) no := x.(*node) no.Index = n *pq = append(*pq, no) } func (pq *priorityQueue) Pop() interface{} { old := *pq n := len(old) no := old[n-1] no.Index = -1 *pq = old[0 : n-1] return no } boohu-0.13.0/autoexplore.go000066400000000000000000000044701356500202200156050ustar00rootroot00000000000000package main import "errors" var DijkstraMapCache [DungeonNCells]int func (g *game) Autoexplore(ev event) error { if mons := g.MonsterInLOS(); mons.Exists() { return errors.New("You cannot auto-explore while there are monsters in view.") } if g.ExclusionsMap[g.Player.Pos] { return errors.New("You cannot auto-explore while in an excluded area.") } if g.AllExplored() { return errors.New("Nothing left to explore.") } sources := g.AutoexploreSources() if len(sources) == 0 { return errors.New("Some excluded places remain unexplored.") } g.BuildAutoexploreMap(sources) n, finished := g.NextAuto() if finished || n == nil { return errors.New("You cannot reach some places safely.") } g.Autoexploring = true g.AutoHalt = false return g.MovePlayer(*n, ev) } func (g *game) AllExplored() bool { np := &normalPath{game: g} for i, c := range g.Dungeon.Cells { pos := idxtopos(i) if c.T == WallCell { if len(np.Neighbors(pos)) == 0 { continue } } _, okc := g.Collectables[pos] if !c.Explored || g.Simellas[pos] > 0 || okc { return false } else if _, ok := g.Rods[pos]; ok { return false } } return true } func (g *game) AutoexploreSources() []int { sources := []int{} np := &normalPath{game: g} for i, c := range g.Dungeon.Cells { pos := idxtopos(i) if c.T == WallCell { if len(np.Neighbors(pos)) == 0 { continue } } if g.ExclusionsMap[pos] { continue } _, okc := g.Collectables[pos] if !c.Explored || g.Simellas[pos] > 0 || okc { sources = append(sources, i) } else if _, ok := g.Rods[pos]; ok { sources = append(sources, i) } } return sources } func (g *game) BuildAutoexploreMap(sources []int) { ap := &autoexplorePath{game: g} g.AutoExploreDijkstra(ap, sources) g.DijkstraMapRebuild = false } func (g *game) NextAuto() (next *position, finished bool) { ap := &autoexplorePath{game: g} if DijkstraMapCache[g.Player.Pos.idx()] == unreachable { return nil, false } neighbors := ap.Neighbors(g.Player.Pos) if len(neighbors) == 0 { return nil, false } n := neighbors[0] ncost := DijkstraMapCache[n.idx()] for _, pos := range neighbors[1:] { cost := DijkstraMapCache[pos.idx()] if cost < ncost { n = pos ncost = cost } } if ncost >= DijkstraMapCache[g.Player.Pos.idx()] { finished = true } next = &n return next, finished } boohu-0.13.0/boohu.6000066400000000000000000000051041356500202200141050ustar00rootroot00000000000000.\" Copyright (c) 2018 Yon .\" .\" Permission to use, copy, modify, and distribute this software for any .\" purpose with or without fee is hereby granted, provided that the above .\" copyright notice and this permission notice appear in all copies. .\" .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .Dd Nov 28, 2018 .Dt BOOHU 6 .Os .Sh NAME .Nm boohu .Nd coffee-break roguelike game .Sh SYNOPSIS .Nm .Op Fl c .Op Fl n .Op Fl o .Op Fl s .Op Fl v .Op Fl x .Op Fl r Ar file .Sh DESCRIPTION Break Out Of Hareka's Underground (Boohu) is a turn-based coffee-break roguelike game with a heavy focus on tactical positioning mechanisms. This focus strongly influenced its weapon attack patterns, consumables and terrain features. Aiming for a replayable streamlined experience, the game avoids manual inventory management and complex character building, relying on items and player adaptability for character progression. .Pp “Every year, the elders send someone to collect medicinal simella plants in the Underground. This year, the honor fell upon you, and so here you are. According to the elders, deep in the Underground, magical stairs will lead you back to your village.” .Pp The options are as follows: .Bl -tag -width Ds .It Fl c Use a centered camera. .It Fl n No animations. .It Fl o Use 8-color palette. .It Fl r Ar file Watch replay file .Ar file instead of launching a normal game. If .Ar file is .Sq _ , the last game replay is used. The following key bindings are available: .Cm + and .Cm - for changing speed, the arrow keys for going to next or previous frame, .Cm space and .Cm p for pausing/resuming the video, and .Cm Q for exiting the program. .It Fl s Use the 16-color solarized palette. .It Fl v Print version number. .It Fl x Use xterm 256-color palette (solarized approximation). This is the default. .El .Sh FILES .Bl -tag -width Ds -compact .It Pa "$XDG_DATA_HOME/boohu/save" Last saved game. .It Pa "$XDG_DATA_HOME/boohu/dump" Last game character and statistics. .It Pa "$XDG_DATA_HOME/boohu/config.gob" Key bindings configuration. .It Pa "$XDG_DATA_HOME/boohu/replay" Last game replay file. .El boohu-0.13.0/combat.go000066400000000000000000000344111356500202200145010ustar00rootroot00000000000000// combat utility functions package main func (g *game) Absorb(armor int) int { absorb := 0 for i := 0; i <= 2; i++ { absorb += RandInt(armor + 1) } q := absorb / 3 r := absorb % 3 if r == 2 { q++ } return q } func (g *game) HitDamage(dt dmgType, base int, armor int) (attack int, clang bool) { min := base / 2 attack = min + RandInt(base-min+1) absorb := g.Absorb(armor) if dt == DmgMagical { absorb = 2 * absorb / 3 } attack -= absorb if absorb > 0 && absorb >= 2*armor/3 && RandInt(2) == 0 { clang = true } if attack < 0 { attack = 0 } return attack, clang } func (m *monster) InflictDamage(g *game, damage, max int) { g.Stats.ReceivedHits++ g.Stats.Damage += damage oldHP := g.Player.HP g.Player.HP -= damage g.ui.WoundedAnimation() if oldHP > max && g.Player.HP <= max { g.StoryPrintf("Critical HP: %d (hit by %s)", g.Player.HP, m.Kind.Indefinite(false)) g.ui.CriticalHPWarning() } if g.Player.HP <= 0 { return } stn, ok := g.MagicalStones[g.Player.Pos] if !ok { return } switch stn { case TeleStone: g.UseStone(g.Player.Pos) g.Teleportation(g.Ev) case FogStone: g.Fog(g.Player.Pos, 3, g.Ev) g.UseStone(g.Player.Pos) case QueenStone: g.MakeNoise(QueenStoneNoise, g.Player.Pos) dij := &normalPath{game: g} nm := Dijkstra(dij, []position{g.Player.Pos}, 2) for _, m := range g.Monsters { if !m.Exists() { continue } if m.State == Resting { continue } _, ok := nm[m.Pos] if !ok { continue } m.EnterConfusion(g, g.Ev) } //g.Confusion(g.Ev) g.UseStone(g.Player.Pos) case TreeStone: if !g.Player.HasStatus(StatusLignification) { g.UseStone(g.Player.Pos) g.EnterLignification(g.Ev) g.Print("You feel rooted to the ground.") } case ObstructionStone: neighbors := g.Dungeon.FreeNeighbors(g.Player.Pos) for _, pos := range neighbors { mons := g.MonsterAt(pos) if mons.Exists() { continue } g.CreateTemporalWallAt(pos, g.Ev) } g.Printf("You see walls appear out of thin air around the stone.") g.UseStone(g.Player.Pos) g.ComputeLOS() } } func (g *game) MakeMonstersAware() { for _, m := range g.Monsters { if m.HP <= 0 { continue } if g.Player.LOS[m.Pos] { m.MakeAware(g) if m.State != Resting { m.GatherBand(g) } } } } func (g *game) MakeNoise(noise int, at position) { dij := &normalPath{game: g} nm := Dijkstra(dij, []position{at}, noise) for _, m := range g.Monsters { if !m.Exists() { continue } if m.State == Hunting { continue } n, ok := nm[m.Pos] if !ok { continue } d := n.Cost v := noise - d if v <= 0 { continue } if v > 25 { v = 25 } r := RandInt(30) if m.State == Resting { v /= 2 } if m.Status(MonsExhausted) { v = 2 * v / 3 } if v > r { if g.Player.LOS[m.Pos] { m.MakeHunt(g) } else { m.Target = at m.State = Wandering } m.GatherBand(g) } } } func (g *game) InOpenMons(mons *monster) bool { neighbors := g.Dungeon.FreeNeighbors(g.Player.Pos) for _, pos := range neighbors { if pos.Distance(mons.Pos) > 1 { continue } if g.Dungeon.Cell(pos).T == WallCell { return false } } return true } func (g *game) AttackMonster(mons *monster, ev event) { switch { case g.Player.HasStatus(StatusSwap) && !g.Player.HasStatus(StatusLignification) && !mons.Status(MonsLignified): g.SwapWithMonster(mons) case g.Player.Weapon == Frundis: if !g.HitMonster(DmgPhysical, g.Player.Attack(), mons, ev) { break } if RandInt(2) == 0 { mons.EnterConfusion(g, ev) g.PrintfStyled("Frundis glows… %s appears confused.", logPlayerHit, mons.Kind.Definite(false)) } case g.Player.Weapon.Cleave(): var neighbors []position if g.Player.HasStatus(StatusConfusion) { neighbors = g.Dungeon.CardinalFreeNeighbors(g.Player.Pos) } else { neighbors = g.Dungeon.FreeNeighbors(g.Player.Pos) } for _, pos := range neighbors { m := g.MonsterAt(pos) if m.Exists() { g.HitMonster(DmgPhysical, g.Player.Attack(), m, ev) } } case g.Player.Weapon.Pierce(): g.HitMonster(DmgPhysical, g.Player.Attack(), mons, ev) dir := mons.Pos.Dir(g.Player.Pos) behind := g.Player.Pos.To(dir).To(dir) if behind.valid() { m := g.MonsterAt(behind) if m.Exists() { g.HitMonster(DmgPhysical, g.Player.Attack(), m, ev) } } case g.Player.Weapon == ElecWhip: g.HitConnected(mons.Pos, DmgMagical, ev) case g.Player.Weapon == DancingRapier: ompos := mons.Pos g.HitMonster(DmgPhysical, g.Player.Attack(), mons, ev) if g.Player.HasStatus(StatusLignification) || mons.Status(MonsLignified) || mons.Kind == MonsTinyHarpy { break } dir := ompos.Dir(g.Player.Pos) behind := g.Player.Pos.To(dir).To(dir) if behind.valid() { m := g.MonsterAt(behind) if m.Exists() { g.HitMonster(DmgPhysical, g.Player.Attack()+3, m, ev) } } if mons.Exists() { mons.MoveTo(g, g.Player.Pos) } g.PlacePlayerAt(ompos) case g.Player.Weapon == HarKarGauntlets: g.HarKarAttack(mons, ev) case g.Player.Weapon == HopeSword: attack := g.Player.Attack() fact := -60 + 100*DefaultHealth/g.Player.HP if fact < 100 { fact = 100 } if fact > 250 { fact = 250 } attack *= fact attack /= 100 g.HitMonster(DmgPhysical, attack, mons, ev) case g.Player.Weapon == DragonSabre: mfact := 100 * (mons.HPmax * mons.HPmax) / (45 * 45) bonus := -1 + 13*mfact/100 g.HitMonster(DmgPhysical, g.Player.Attack()+bonus, mons, ev) case g.Player.Weapon == DefenderFlail: bonus := g.Player.Statuses[StatusSlay] g.HitMonster(DmgPhysical, g.Player.Attack()+bonus, mons, ev) g.Player.Statuses[StatusSlay]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 60, EAction: SlayEnd}) default: g.HitMonster(DmgPhysical, g.Player.Attack(), mons, ev) } } func (g *game) AttractMonster(pos position) *monster { dir := pos.Dir(g.Player.Pos) for cpos := pos.To(dir); g.Player.LOS[cpos]; cpos = cpos.To(dir) { mons := g.MonsterAt(cpos) if mons.Exists() { mons.MoveTo(g, pos) g.ui.TeleportAnimation(cpos, pos, false) return mons } } return nil } func (g *game) HarKarAttack(mons *monster, ev event) { dir := mons.Pos.Dir(g.Player.Pos) pos := g.Player.Pos for { pos = pos.To(dir) if !pos.valid() || g.Dungeon.Cell(pos).T != FreeCell { break } m := g.MonsterAt(pos) if !m.Exists() { break } } if pos.valid() && g.Dungeon.Cell(pos).T == FreeCell && !g.Player.HasStatus(StatusLignification) { pos = g.Player.Pos for { pos = pos.To(dir) if !pos.valid() || g.Dungeon.Cell(pos).T != FreeCell { break } m := g.MonsterAt(pos) if !m.Exists() { break } g.HitMonster(DmgPhysical, g.Player.Attack(), m, ev) } if !pos.valid() || g.Dungeon.Cell(pos).T != FreeCell { return } g.PlacePlayerAt(pos) behind := pos.To(dir) m := g.MonsterAt(behind) if m.Exists() { g.HitMonster(DmgPhysical, g.Player.Attack(), m, ev) } } else { g.HitMonster(DmgPhysical, g.Player.Attack(), mons, ev) } } func (g *game) HitConnected(pos position, dt dmgType, ev event) { d := g.Dungeon conn := map[position]bool{} stack := []position{pos} conn[pos] = true nb := make([]position, 0, 8) for len(stack) > 0 { pos = stack[len(stack)-1] stack = stack[:len(stack)-1] mons := g.MonsterAt(pos) if !mons.Exists() { continue } g.HitMonster(dt, g.Player.Attack(), mons, ev) nb = pos.Neighbors(nb, func(npos position) bool { return npos.valid() && d.Cell(npos).T != WallCell }) for _, npos := range nb { if !conn[npos] { conn[npos] = true stack = append(stack, npos) } } } } func (g *game) HitNoise(clang bool) int { noise := BaseHitNoise if g.Player.Weapon == Frundis { noise -= 5 } if g.Player.Armour == HarmonistRobe { noise -= 3 } if g.Player.Armour == Robe { noise -= 1 } if clang { arnoise := g.Player.Armor() if arnoise > 7 { arnoise = 7 } noise += arnoise } return noise } type dmgType int const ( DmgPhysical dmgType = iota DmgMagical ) func (g *game) HitMonster(dt dmgType, dmg int, mons *monster, ev event) (hit bool) { maxacc := g.Player.Accuracy() if g.Player.Weapon == AssassinSabre && mons.HP > 0 { adjust := 6 * (-100 + 100*mons.HPmax/mons.HP) / 100 if adjust > 25 { adjust = 25 } maxacc += adjust } else if g.Player.Weapon == FinalBlade { maxacc += 10 } acc := RandInt(maxacc) if g.Player.AccScore == 1 && acc >= maxacc/2 { acc -= RandInt(1 + maxacc/2) } else if g.Player.AccScore == -1 && acc < maxacc/2 { acc += RandInt(1 + maxacc/2) } if acc >= maxacc/2 { g.Player.AccScore = 1 } else { g.Player.AccScore = -1 } evasion := RandInt(mons.Evasion) if mons.State == Resting { evasion /= 2 + 1 } if acc > evasion || g.Player.HasStatus(StatusAccurate) { hit = true noise := BaseHitNoise if g.Player.Weapon == Dagger || g.Player.Weapon == VampDagger { noise -= 2 } if g.Player.Armour == HarmonistRobe { noise -= 3 } if g.Player.Weapon == Frundis { noise -= 5 } bonus := 0 if g.Player.HasStatus(StatusBerserk) { bonus += 2 + RandInt(4) } pa := dmg + bonus if g.Player.Weapon.Cleave() && g.InOpenMons(mons) { if g.Player.Attack() >= 15 { pa += 1 + RandInt(3) } else { pa += 1 + RandInt(2) } } marmor := mons.Armor marmor = 6 + marmor/2 attack, clang := g.HitDamage(dt, pa, marmor) if clang { noise += marmor } g.MakeNoise(noise, mons.Pos) if mons.State == Resting { if g.Player.Weapon == Dagger || g.Player.Weapon == VampDagger { attack *= 4 } else { attack *= 2 } } var sclang string if clang { if marmor > 3 { sclang = " ♫ Clang!" } else { sclang = " ♪ Clang!" } } oldHP := mons.HP if g.Player.Weapon == FinalBlade { if mons.HP <= mons.HPmax/2 { attack = mons.HP } } mons.HP -= attack if g.Player.Weapon == VampDagger && mons.Kind.Living() { healing := attack if healing > 2*pa/3 { healing = 2 * pa / 3 } if g.Player.HP+healing > g.Player.HPMax() { g.Player.HP = g.Player.HPMax() } else { g.Player.HP += healing } } g.ui.HitAnimation(mons.Pos, false) if mons.HP > 0 { g.PrintfStyled("You hit %s (%d dmg).%s", logPlayerHit, mons.Kind.Definite(false), attack, sclang) } else if oldHP > 0 { // test oldHP > 0 because of sword special attack g.PrintfStyled("You kill %s (%d dmg).%s", logPlayerHit, mons.Kind.Definite(false), attack, sclang) g.HandleKill(mons, ev) } if mons.Kind == MonsBrizzia && RandInt(4) == 0 && !g.Player.HasStatus(StatusNausea) && mons.Pos.Distance(g.Player.Pos) == 1 { g.Player.Statuses[StatusNausea]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 30 + RandInt(20), EAction: NauseaEnd}) g.Print("The brizzia's corpse releases some nauseating gas. You feel sick.") } if mons.Kind == MonsTinyHarpy && mons.HP > 0 { mons.Blink(g) } g.HandleStone(mons) g.Stats.Hits++ } else { g.Printf("You miss %s.", mons.Kind.Definite(false)) g.Stats.Misses++ } mons.MakeHuntIfHurt(g) return hit } func (g *game) HandleStone(mons *monster) { stn, ok := g.MagicalStones[mons.Pos] if !ok { return } switch stn { case TeleStone: if mons.Exists() { g.UseStone(mons.Pos) mons.TeleportAway(g) } case FogStone: g.Fog(mons.Pos, 3, g.Ev) g.UseStone(mons.Pos) case QueenStone: g.MakeNoise(QueenStoneNoise, mons.Pos) dij := &normalPath{game: g} nm := Dijkstra(dij, []position{mons.Pos}, 2) for _, m := range g.Monsters { if !m.Exists() { continue } if m.State == Resting { continue } _, ok := nm[m.Pos] if !ok { continue } m.EnterConfusion(g, g.Ev) } // _, ok := nm[g.Player.Pos] // if ok { // g.Confusion(g.Ev) // } g.UseStone(mons.Pos) case TreeStone: if mons.Exists() { g.UseStone(mons.Pos) mons.EnterLignification(g, g.Ev) } case ObstructionStone: if !mons.Exists() { g.CreateTemporalWallAt(mons.Pos, g.Ev) } neighbors := g.Dungeon.FreeNeighbors(mons.Pos) for _, pos := range neighbors { if pos == g.Player.Pos { continue } m := g.MonsterAt(pos) if m.Exists() { continue } g.CreateTemporalWallAt(pos, g.Ev) } g.Printf("You see walls appear out of thin air around the stone.") g.UseStone(mons.Pos) g.ComputeLOS() } } func (g *game) HandleKill(mons *monster, ev event) { g.Stats.Killed++ g.Stats.KilledMons[mons.Kind]++ if mons.Kind == MonsExplosiveNadre { mons.Explode(g, ev) } if g.Doors[mons.Pos] { g.ComputeLOS() } if mons.Kind.Dangerousness() > 10 { g.StoryPrintf("Killed %s.", mons.Kind.Indefinite(false)) } } const ( WallNoise = 18 TemporalWallNoise = 13 ExplosionHitNoise = 13 ExplosionNoise = 18 MagicHitNoise = 15 BarkNoise = 13 MagicExplosionNoise = 16 MagicCastNoise = 16 BaseHitNoise = 11 ShieldBlockNoise = 17 QueenStoneNoise = 19 ) func (g *game) ArmourClang() (sclang string) { if g.Player.Armor() > 3 { sclang = " Clang!" } else { sclang = " Smash!" } return sclang } func (g *game) BlockEffects(m *monster) { g.Stats.Blocks++ // only one shield block per turn g.Player.Blocked = true g.PushEvent(&simpleEvent{ERank: g.Ev.Rank() + 10, EAction: BlockEnd}) switch g.Player.Shield { case EarthShield: dir := m.Pos.Dir(g.Player.Pos) lat := g.Player.Pos.Laterals(dir) for _, pos := range lat { if !pos.valid() { continue } if RandInt(3) == 0 && g.Dungeon.Cell(pos).T == WallCell { g.Dungeon.SetCell(pos, FreeCell) g.Stats.Digs++ g.MakeNoise(WallNoise+3, pos) g.Fog(pos, 1, g.Ev) g.Printf("%s The sound of blocking breaks a wall.", g.CrackSound()) } } case BashingShield: if m.Kind == MonsSatowalgaPlant || m.Pos.Distance(g.Player.Pos) > 1 { break } if RandInt(5) == 0 { break } dir := m.Pos.Dir(g.Player.Pos) pos := m.Pos npos := pos i := 0 for { i++ npos = npos.To(dir) if !npos.valid() || g.Dungeon.Cell(npos).T == WallCell { break } mons := g.MonsterAt(npos) if mons.Exists() { continue } pos = npos if i >= 3 { break } } m.Exhaust(g) if pos != m.Pos { m.MoveTo(g, pos) g.Printf("%s is repelled.", m.Kind.Definite(true)) } case ConfusingShield: if m.Pos.Distance(g.Player.Pos) > 1 { break } if RandInt(4) == 0 { m.EnterConfusion(g, g.Ev) g.Printf("%s appears confused.", m.Kind.Definite(true)) } case FireShield: dir := m.Pos.Dir(g.Player.Pos) burnpos := g.Player.Pos.To(dir) if RandInt(4) == 0 { g.Print("Sparks emerge out of the shield.") g.Burn(burnpos, g.Ev) } } } boohu-0.13.0/credits.txt000066400000000000000000000031241356500202200151000ustar00rootroot00000000000000This file lists some great influences for many Boohu features. ----------------------------------------------------------------------- DCSS is a source of inspiration for many things. Statuses: berserk, swift, agile, slow, exhaustion, lignification, corrosion, teletransportation, as well as a deeply positional confusion status from this tavern thread: https://crawl.develz.org/tavern/viewtopic.php?f=17&t=24108&sid=cb465fe78aba3b9074a32efc2a835d80#p318813 Many potions also come from there (sometimes replacing a scroll). ----------------------------------------------------------------------- Brogue inspired spear/halberd attack pattern, as well as the axe/battle axe one (similar to DCSS, but no distinction between primary and secondary targets). Acid mounds also come from there. Dense foliage was inspired by Brogue's. It shares with Boohu a non-XP system, though Boohu does not have potions of strengh/life nor enchant scrolls: only aptitudes and immutable items. ----------------------------------------------------------------------- There are two features inspired from Tome4, more precisely from minmay's Harbinger class (ligthning whip uses the ligthning arc idea), and from nsrr's white monk class (har-kar guantlets make a swift strike passively). ----------------------------------------------------------------------- Automatic doors are inspired from Cogmind, but are open only if there is someone right on the door. ----------------------------------------------------------------------- Magical stones are inspired from TGGW's pillars, even though the mechanics and effects are very different. boohu-0.13.0/dijkstra.go000066400000000000000000000033541356500202200150510ustar00rootroot00000000000000package main import ( "container/heap" ) type Dijkstrer interface { Neighbors(position) []position Cost(position, position) int } func Dijkstra(dij Dijkstrer, sources []position, maxCost int) nodeMap { nodeCache = nodeCache[:0] nm := nodeMap{} nq := &priorityQueue{} heap.Init(nq) for _, f := range sources { n := nm.get(f) n.Open = true heap.Push(nq, n) } for { if nq.Len() == 0 { return nm } current := heap.Pop(nq).(*node) current.Open = false current.Closed = true for _, neighbor := range dij.Neighbors(current.Pos) { cost := current.Cost + dij.Cost(current.Pos, neighbor) neighborNode := nm.get(neighbor) if cost < neighborNode.Cost { if neighborNode.Open { heap.Remove(nq, neighborNode.Index) } neighborNode.Open = false neighborNode.Closed = false } if !neighborNode.Open && !neighborNode.Closed { neighborNode.Cost = cost if cost < maxCost { neighborNode.Open = true neighborNode.Rank = cost heap.Push(nq, neighborNode) } } } } } const unreachable = 9999 func (g *game) AutoExploreDijkstra(dij Dijkstrer, sources []int) { d := g.Dungeon dmap := DijkstraMapCache[:] var visited [DungeonNCells]bool var queue [DungeonNCells]int var qstart, qend int for i := 0; i < DungeonNCells; i++ { dmap[i] = unreachable } for _, s := range sources { dmap[s] = 0 queue[qend] = s qend++ visited[s] = true } for qstart < qend { cidx := queue[qstart] qstart++ cpos := idxtopos(cidx) for _, npos := range dij.Neighbors(cpos) { nidx := npos.idx() if !npos.valid() || d.Cells[nidx].T == WallCell { continue } if !visited[nidx] { queue[qend] = nidx qend++ visited[nidx] = true dmap[nidx] = 1 + dmap[cidx] } } } } boohu-0.13.0/draw.go000066400000000000000000001402641356500202200141750ustar00rootroot00000000000000package main import ( "bytes" "fmt" "runtime" "sort" "strings" "time" "unicode/utf8" ) var ( UIWidth = 100 UIHeight = 26 DisableAnimations bool = false ) type uicolor int const ( Color256Base03 uicolor = 234 Color256Base02 uicolor = 235 Color256Base01 uicolor = 240 Color256Base00 uicolor = 241 // for dark on light background Color256Base0 uicolor = 244 Color256Base1 uicolor = 245 Color256Base2 uicolor = 254 Color256Base3 uicolor = 230 Color256Yellow uicolor = 136 Color256Orange uicolor = 166 Color256Red uicolor = 160 Color256Magenta uicolor = 125 Color256Violet uicolor = 61 Color256Blue uicolor = 33 Color256Cyan uicolor = 37 Color256Green uicolor = 64 Color16Base03 uicolor = 8 Color16Base02 uicolor = 0 Color16Base01 uicolor = 10 Color16Base00 uicolor = 11 Color16Base0 uicolor = 12 Color16Base1 uicolor = 14 Color16Base2 uicolor = 7 Color16Base3 uicolor = 15 Color16Yellow uicolor = 3 Color16Orange uicolor = 9 Color16Red uicolor = 1 Color16Magenta uicolor = 5 Color16Violet uicolor = 13 Color16Blue uicolor = 4 Color16Cyan uicolor = 6 Color16Green uicolor = 2 ) // uicolors: http://ethanschoonover.com/solarized var ( ColorBase03 uicolor = Color256Base03 ColorBase02 uicolor = Color256Base02 ColorBase01 uicolor = Color256Base01 ColorBase00 uicolor = Color256Base00 // for dark on light background ColorBase0 uicolor = Color256Base0 ColorBase1 uicolor = Color256Base1 ColorBase2 uicolor = Color256Base2 ColorBase3 uicolor = Color256Base3 ColorYellow uicolor = Color256Yellow ColorOrange uicolor = Color256Orange ColorRed uicolor = Color256Red ColorMagenta uicolor = Color256Magenta ColorViolet uicolor = Color256Violet ColorBlue uicolor = Color256Blue ColorCyan uicolor = Color256Cyan ColorGreen uicolor = Color256Green ) func (ui *gameui) Map256ColorTo16(c uicolor) uicolor { switch c { case Color256Base03: return Color16Base03 case Color256Base02: return Color16Base02 case Color256Base01: return Color16Base01 case Color256Base00: return Color16Base00 case Color256Base0: return Color16Base0 case Color256Base1: return Color16Base1 case Color256Base2: return Color16Base2 case Color256Base3: return Color16Base3 case Color256Yellow: return Color16Yellow case Color256Orange: return Color16Orange case Color256Red: return Color16Red case Color256Magenta: return Color16Magenta case Color256Violet: return Color16Violet case Color256Blue: return Color16Blue case Color256Cyan: return Color16Cyan case Color256Green: return Color16Green default: return c } } func (ui *gameui) Map16ColorTo256(c uicolor) uicolor { switch c { case Color16Base03: return Color256Base03 case Color16Base02: return Color256Base02 case Color16Base01: return Color256Base01 case Color16Base00: return Color256Base00 case Color16Base0: return Color256Base0 case Color16Base1: return Color256Base1 case Color16Base2: return Color256Base2 case Color16Base3: return Color256Base3 case Color16Yellow: return Color256Yellow case Color16Orange: return Color256Orange case Color16Red: return Color256Red case Color16Magenta: return Color256Magenta case Color16Violet: return Color256Violet case Color16Blue: return Color256Blue case Color16Cyan: return Color256Cyan case Color16Green: return Color256Green default: return c } } var ( ColorBg, ColorBgBorder, ColorBgDark, ColorBgLOS, ColorFg, ColorFgAnimationHit, ColorFgCollectable, ColorFgConfusedMonster, ColorFgLignifiedMonster, ColorFgSlowedMonster, ColorFgDark, ColorFgExcluded, ColorFgExplosionEnd, ColorFgExplosionStart, ColorFgExplosionWallEnd, ColorFgExplosionWallStart, ColorFgHPcritical, ColorFgHPok, ColorFgHPwounded, ColorFgLOS, ColorFgMPcritical, ColorFgMPok, ColorFgMPpartial, ColorFgMagicPlace, ColorFgMonster, ColorFgPlace, ColorFgPlayer, ColorFgProjectile, ColorFgSimellas, ColorFgSleepingMonster, ColorFgStatusBad, ColorFgStatusGood, ColorFgStatusExpire, ColorFgStatusOther, ColorFgTargetMode, ColorFgWanderingMonster uicolor ) func LinkColors() { ColorBg = ColorBase03 ColorBgBorder = ColorBase02 ColorBgDark = ColorBase03 ColorBgLOS = ColorBase3 ColorFg = ColorBase0 ColorFgDark = ColorBase01 ColorFgLOS = ColorBase0 ColorFgAnimationHit = ColorMagenta ColorFgCollectable = ColorYellow ColorFgConfusedMonster = ColorGreen ColorFgLignifiedMonster = ColorYellow ColorFgSlowedMonster = ColorCyan ColorFgExcluded = ColorRed ColorFgExplosionEnd = ColorOrange ColorFgExplosionStart = ColorYellow ColorFgExplosionWallEnd = ColorMagenta ColorFgExplosionWallStart = ColorViolet ColorFgHPcritical = ColorRed ColorFgHPok = ColorGreen ColorFgHPwounded = ColorYellow ColorFgMPcritical = ColorMagenta ColorFgMPok = ColorBlue ColorFgMPpartial = ColorViolet ColorFgMagicPlace = ColorCyan ColorFgMonster = ColorRed ColorFgPlace = ColorMagenta ColorFgPlayer = ColorBlue ColorFgProjectile = ColorBlue ColorFgSimellas = ColorYellow ColorFgSleepingMonster = ColorViolet ColorFgStatusBad = ColorRed ColorFgStatusGood = ColorBlue ColorFgStatusExpire = ColorViolet ColorFgStatusOther = ColorYellow ColorFgTargetMode = ColorCyan ColorFgWanderingMonster = ColorOrange } func ApplyDarkLOS() { ColorBg = ColorBase03 ColorBgBorder = ColorBase02 ColorBgDark = ColorBase03 ColorBgLOS = ColorBase02 ColorFgDark = ColorBase01 ColorFg = ColorBase0 if Only8Colors { ColorFgLOS = ColorGreen } else { ColorFgLOS = ColorBase0 } } func ApplyLightLOS() { if Only8Colors { ApplyDarkLOS() ColorBgLOS = ColorBase2 ColorFgLOS = ColorBase00 } else { ColorBg = ColorBase3 ColorBgBorder = ColorBase2 ColorBgDark = ColorBase3 ColorBgLOS = ColorBase2 ColorFgDark = ColorBase1 ColorFgLOS = ColorBase00 ColorFg = ColorBase00 } } func SolarizedPalette() { ColorBase03 = Color16Base03 ColorBase02 = Color16Base02 ColorBase01 = Color16Base01 ColorBase00 = Color16Base00 ColorBase0 = Color16Base0 ColorBase1 = Color16Base1 ColorBase2 = Color16Base2 ColorBase3 = Color16Base3 ColorYellow = Color16Yellow ColorOrange = Color16Orange ColorRed = Color16Red ColorMagenta = Color16Magenta ColorViolet = Color16Violet ColorBlue = Color16Blue ColorCyan = Color16Cyan ColorGreen = Color16Green } const ( Black uicolor = iota Maroon Green Olive Navy Purple Teal Silver ) func Map16ColorTo8Color(c uicolor) uicolor { switch c { case Color16Base03: return Black case Color16Base02: return Black case Color16Base01: return Silver case Color16Base00: return Black case Color16Base0: return Silver case Color16Base1: return Silver case Color16Base2: return Silver case Color16Base3: return Silver case Color16Yellow: return Olive case Color16Orange: return Purple case Color16Red: return Maroon case Color16Magenta: return Purple case Color16Violet: return Teal case Color16Blue: return Navy case Color16Cyan: return Teal case Color16Green: return Green default: return c } } var Only8Colors bool func Simple8ColorPalette() { Only8Colors = true } type drawFrame struct { Draws []cellDraw Time time.Time } type cellDraw struct { Cell UICell X int Y int } func (ui *gameui) SetCell(x, y int, r rune, fg, bg uicolor) { ui.SetGenCell(x, y, r, fg, bg, false) } func (ui *gameui) SetGenCell(x, y int, r rune, fg, bg uicolor, inmap bool) { i := ui.GetIndex(x, y) if i >= UIHeight*UIWidth { return } c := UICell{R: r, Fg: fg, Bg: bg, InMap: inmap} ui.g.DrawBuffer[i] = c } func (ui *gameui) SetMapCell(x, y int, r rune, fg, bg uicolor) { ui.SetGenCell(x, y, r, fg, bg, true) } func (ui *gameui) DrawLogFrame() { if len(ui.g.drawBackBuffer) != len(ui.g.DrawBuffer) { ui.g.drawBackBuffer = make([]UICell, len(ui.g.DrawBuffer)) } ui.g.DrawLog = append(ui.g.DrawLog, drawFrame{Time: time.Now()}) for i := 0; i < len(ui.g.DrawBuffer); i++ { if ui.g.DrawBuffer[i] == ui.g.drawBackBuffer[i] { continue } c := ui.g.DrawBuffer[i] x, y := ui.GetPos(i) cdraw := cellDraw{Cell: c, X: x, Y: y} last := len(ui.g.DrawLog) - 1 ui.g.DrawLog[last].Draws = append(ui.g.DrawLog[last].Draws, cdraw) ui.g.drawBackBuffer[i] = c } } func (ui *gameui) DrawWelcomeCommon() int { ui.DrawBufferInit() ui.Clear() col := 10 line := 5 rcol := col + 20 ColorText := ColorFgHPok ui.DrawDark(fmt.Sprintf(" Boohu %s", Version), col, line-2, ColorText, false) ui.DrawDark("────│\\/\\/\\/\\/\\/\\/\\/│────", col, line, ColorText, false) line++ ui.DrawDark("##", col, line, ColorFgDark, true) ui.DrawLOS("#", col+2, line, ColorFgLOS, true) ui.DrawLOS("#", col+3, line, ColorFgLOS, true) ui.DrawDark("│ │", col+4, line, ColorText, false) ui.DrawDark("####", rcol, line, ColorFgDark, true) line++ ui.DrawDark("#.", col, line, ColorFgDark, true) ui.DrawLOS(".", col+2, line, ColorFgLOS, true) ui.DrawLOS(".", col+3, line, ColorFgLOS, true) ui.DrawDark("│ │", col+4, line, ColorText, false) ui.DrawDark(".", rcol, line, ColorFgDark, true) ui.DrawDark("♣", rcol+1, line, ColorFgSimellas, true) ui.DrawDark(".#", rcol+2, line, ColorFgDark, true) line++ ui.DrawDark("##", col, line, ColorFgDark, true) ui.DrawLOS("!", col+2, line, ColorFgCollectable, true) ui.DrawLOS(".", col+3, line, ColorFgLOS, true) ui.DrawDark("│ │", col+4, line, ColorText, false) ui.DrawDark("│ BREAK │", col+4, line, ColorText, false) ui.DrawDark(".###", rcol, line, ColorFgDark, true) line++ ui.DrawDark(" #", col, line, ColorFgDark, true) ui.DrawLOS("g", col+2, line, ColorFgMonster, true) ui.DrawLOS("G", col+3, line, ColorFgMonster, true) ui.DrawDark("│ OUT OF │", col+4, line, ColorText, false) ui.DrawDark("## ", rcol, line, ColorFgDark, true) line++ ui.DrawLOS("#", col, line, ColorFgLOS, true) ui.DrawLOS("#", col+1, line, ColorFgLOS, true) ui.DrawLOS("D", col+2, line, ColorFgMonster, true) ui.DrawLOS("g", col+3, line, ColorFgMonster, true) ui.DrawDark("│ HAREKA'S │", col+4, line, ColorText, false) ui.DrawDark(".## ", rcol, line, ColorFgDark, true) line++ ui.DrawLOS("#", col, line, ColorFgLOS, true) ui.DrawLOS("@", col+1, line, ColorFgPlayer, true) ui.DrawLOS("#", col+2, line, ColorFgLOS, true) ui.DrawDark("#", col+3, line, ColorFgDark, true) ui.DrawDark("│ UNDERGROUND │", col+4, line, ColorText, false) ui.DrawDark("\".##", rcol, line, ColorFgDark, true) line++ ui.DrawLOS("#", col, line, ColorFgLOS, true) ui.DrawLOS(".", col+1, line, ColorFgLOS, true) ui.DrawLOS("#", col+2, line, ColorFgLOS, true) ui.DrawDark("#", col+3, line, ColorFgDark, true) ui.DrawDark("│ │", col+4, line, ColorText, false) ui.DrawDark("#.", rcol, line, ColorFgDark, true) ui.DrawDark(">", rcol+2, line, ColorFgPlace, true) ui.DrawDark("#", rcol+3, line, ColorFgDark, true) line++ ui.DrawLOS("#", col, line, ColorFgLOS, true) ui.DrawLOS("[", col+1, line, ColorFgCollectable, true) ui.DrawLOS(".", col+2, line, ColorFgLOS, true) ui.DrawDark("##", col+3, line, ColorFgDark, true) ui.DrawDark("│ │", col+4, line, ColorFgHPok, false) ui.DrawDark("\"\"##", rcol, line, ColorFgDark, true) line++ ui.DrawDark("────│/\\/\\/\\/\\/\\/\\/\\│────", col, line, ColorText, false) line++ line++ if runtime.GOARCH == "wasm" { ui.DrawDark("- (P)lay", col-3, line, ColorFg, false) ui.DrawDark("- (W)atch replay", col-3, line+1, ColorFg, false) } else { ui.DrawDark("───Press any key to continue───", col-3, line, ColorFg, false) } ui.Flush() return line } func (ui *gameui) DrawWelcome() { ui.DrawWelcomeCommon() ui.PressAnyKey() } func (ui *gameui) RestartDrawBuffers() { g := ui.g g.DrawBuffer = nil g.drawBackBuffer = nil ui.DrawBufferInit() } func (ui *gameui) DrawColored(text string, x, y int, fg, bg uicolor) { col := 0 for _, r := range text { ui.SetCell(x+col, y, r, fg, bg) col++ } } func (ui *gameui) DrawDark(text string, x, y int, fg uicolor, inmap bool) { col := 0 for _, r := range text { if inmap { ui.SetMapCell(x+col, y, r, fg, ColorBgDark) } else { ui.SetCell(x+col, y, r, fg, ColorBgDark) } col++ } } func (ui *gameui) DrawLOS(text string, x, y int, fg uicolor, inmap bool) { col := 0 for _, r := range text { if inmap { ui.SetMapCell(x+col, y, r, fg, ColorBgLOS) } else { ui.SetCell(x+col, y, r, fg, ColorBgLOS) } col++ } } func (ui *gameui) DrawKeysDescription(title string, actions []string) { ui.DrawDungeonView(NoFlushMode) if CustomKeys { ui.DrawStyledTextLine(fmt.Sprintf(" Default %s ", title), 0, HeaderLine) } else { ui.DrawStyledTextLine(fmt.Sprintf(" %s ", title), 0, HeaderLine) } for i := 0; i < len(actions)-1; i += 2 { bg := ui.ListItemBG(i / 2) ui.ClearLineWithColor(i/2+1, bg) ui.DrawColoredTextOnBG(fmt.Sprintf(" %-36s %s", actions[i], actions[i+1]), 0, i/2+1, ColorFg, bg) } lines := 1 + len(actions)/2 ui.DrawTextLine(" press (x) to continue ", lines) ui.Flush() ui.WaitForContinue(lines) } func (ui *gameui) KeysHelp() { ui.DrawKeysDescription("Commands", []string{ "Movement", "h/j/k/l/y/u/b/n or numpad or mouse left", "Wait a turn", "“.” or 5 or mouse left on @", "Rest (until status free or regen)", "r", "Descend stairs", "> or D", "Go to nearest stairs", "G", "Autoexplore", "o", "Examine", "x or mouse left", "Equip/Get weapon/armour/...", "e or g", "Quaff/Drink potion", "q or d", "Throw/Fire item", "t or f", "Evoke/Zap rod", "v or z", "View Character and Quest Information", `% or C`, "View previous messages", "m", "Write game statistics to file", "#", "Save and Quit", "S", "Quit without saving", "Q", "Change settings and key bindings", "=", }) } func (ui *gameui) ExamineHelp() { ui.DrawKeysDescription("Examine/Travel/Targeting Commands", []string{ "Move cursor", "h/j/k/l/y/u/b/n or numpad or mouse left", "Cycle through monsters", "+", "Cycle through stairs", ">", "Cycle through objects", "o", "Go to/select target", "“.” or enter or mouse left", "View target description", "v or d or mouse right", "Toggle exclude area from auto-travel", "e or mouse middle", }) } const TextWidth = DungeonWidth - 2 func (ui *gameui) CharacterInfo() { g := ui.g ui.DrawDungeonView(NoFlushMode) b := bytes.Buffer{} b.WriteString(formatText("Every year, the elders send someone to collect medicinal simella plants in the Underground. This year, the honor fell upon you, and so here you are. According to the elders, deep in the Underground, magical stairs will lead you back to your village.", TextWidth)) b.WriteString("\n\n") b.WriteString(formatText( fmt.Sprintf("You are wielding %s. %s", Indefinite(g.Player.Weapon.String(), false), g.Player.Weapon.Desc()), TextWidth)) b.WriteString("\n\n") b.WriteString(formatText(fmt.Sprintf("You are wearing %s. %s", g.Player.Armour.StringIndefinite(), g.Player.Armour.Desc()), TextWidth)) b.WriteString("\n\n") if g.Player.Shield != NoShield { b.WriteString(formatText(fmt.Sprintf("You are wearing a %s. %s", g.Player.Shield, g.Player.Shield.Desc()), TextWidth)) b.WriteString("\n\n") } b.WriteString(ui.AptitudesText()) desc := b.String() lines := strings.Count(desc, "\n") for i := 0; i <= lines+2; i++ { if i >= DungeonWidth { ui.SetCell(DungeonWidth, i, '│', ColorFg, ColorBg) } ui.ClearLine(i) } ui.DrawText(desc, 0, 0) escspace := " press (x) to continue " if lines+2 >= DungeonHeight { ui.DrawTextLine(escspace, lines+2) ui.SetCell(DungeonWidth, lines+2, '┘', ColorFg, ColorBg) } else { ui.DrawTextLine(escspace, lines+2) } ui.Flush() ui.WaitForContinue(lines + 2) } func (ui *gameui) WizardInfo() { g := ui.g ui.Clear() b := &bytes.Buffer{} fmt.Fprintf(b, "Monsters: %d (%d)\n", len(g.Monsters), g.MaxMonsters()) fmt.Fprintf(b, "Danger: %d (%d)\n", g.Danger(), g.MaxDanger()) ui.DrawText(b.String(), 0, 0) ui.Flush() ui.WaitForContinue(-1) } func (ui *gameui) AptitudesText() string { g := ui.g apts := []string{} for apt, b := range g.Player.Aptitudes { if b { apts = append(apts, apt.String()) } } sort.Strings(apts) var text string if len(apts) > 0 { text = "Aptitudes:\n" + strings.Join(apts, "\n") } else { text = "You do not have any special aptitudes." } return text } func (ui *gameui) AddComma(see, s string) string { if len(s) > 0 { return s + ", " } return fmt.Sprintf("You %s %s", see, s) } func (ui *gameui) DescribePosition(pos position, targ Targeter) { g := ui.g var desc string switch { case !g.Dungeon.Cell(pos).Explored: desc = "You do not know what is in there." g.InfoEntry = desc return case !targ.Reachable(g, pos): desc = "This is out of reach." g.InfoEntry = desc return } mons := g.MonsterAt(pos) c, okCollectable := g.Collectables[pos] eq, okEq := g.Equipables[pos] rod, okRod := g.Rods[pos] if pos == g.Player.Pos { desc = "This is you" } see := "see" if !g.Player.LOS[pos] { see = "saw" } if g.Dungeon.Cell(pos).T == WallCell && !g.WrongWall[pos] || g.Dungeon.Cell(pos).T == FreeCell && g.WrongWall[pos] { desc = ui.AddComma(see, "") desc += fmt.Sprintf("a wall") g.InfoEntry = desc + "." return } if mons.Exists() && g.Player.LOS[pos] { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("%s (%s)", mons.Kind.Indefinite(false), ui.MonsterInfo(mons)) } strt, okStair := g.Stairs[pos] stn, okStone := g.MagicalStones[pos] switch { case g.Simellas[pos] > 0: desc = ui.AddComma(see, desc) desc += fmt.Sprintf("some simellas (%d)", g.Simellas[pos]) case okCollectable: if c.Quantity > 1 { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("%d %s", c.Quantity, c.Consumable) } else { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("%s", Indefinite(c.Consumable.String(), false)) } case okEq: desc = ui.AddComma(see, desc) desc += fmt.Sprintf("%s", Indefinite(eq.String(), false)) case okRod: desc = ui.AddComma(see, desc) desc += fmt.Sprintf("a %v", rod) case okStair: if strt == WinStair { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("glowing monolith") } else { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("stairs downwards") } case okStone: desc = ui.AddComma(see, desc) desc += fmt.Sprint(Indefinite(stn.String(), false)) case g.Doors[pos] || g.WrongDoor[pos]: desc = ui.AddComma(see, desc) desc += fmt.Sprintf("a door") } if cld, ok := g.Clouds[pos]; ok && g.Player.LOS[pos] { if cld == CloudFire { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("burning flames") } else if cld == CloudNight { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("night clouds") } else { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("a dense fog") } } else if _, ok := g.Fungus[pos]; ok && !g.WrongFoliage[pos] || !ok && g.WrongFoliage[pos] { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("foliage") } else if desc == "" { desc = ui.AddComma(see, desc) desc += fmt.Sprintf("the ground") } g.InfoEntry = desc + "." } func (ui *gameui) ViewPositionDescription(pos position) { g := ui.g if !g.Dungeon.Cell(pos).Explored { ui.DrawDescription("This place is unknown to you.") return } mons := g.MonsterAt(pos) if mons.Exists() && g.Player.LOS[mons.Pos] { ui.HideCursor() ui.DrawMonsterDescription(mons) ui.SetCursor(pos) } else if c, ok := g.Collectables[pos]; ok { ui.DrawDescription(c.Consumable.Desc()) } else if r, ok := g.Rods[pos]; ok { ui.DrawDescription(r.Desc()) } else if eq, ok := g.Equipables[pos]; ok { ui.DrawDescription(eq.Desc()) } else if strt, ok := g.Stairs[pos]; ok { if strt == WinStair { desc := "This magical monolith will teleport you back to your village. It is said such monoliths were made some centuries ago by Marevor Helith. You can use it like stairs." if g.Depth < MaxDepth { desc += " Note that this is not the last floor, so you may want to find a stair and continue collecting simellas, if you're courageous enough." } ui.DrawDescription(desc) } else { desc := "Stairs lead to the next level of the Underground. There's no way back. Monsters do not follow you." if g.Depth == WinDepth { desc += " If you're afraid, you could instead just win by taking the magical stairs somewhere in the same map." } ui.DrawDescription(desc) } } else if stn, ok := g.MagicalStones[pos]; ok { ui.DrawDescription(stn.Description()) } else if g.Doors[pos] { ui.DrawDescription("A closed door blocks your line of sight. Doors open automatically when you or a monster stand on them. Doors are flammable.") } else if g.Simellas[pos] > 0 { ui.DrawDescription("A simella is a plant with big white flowers which are used in the Underground for their medicinal properties. They can also make tasty infusions. You were actually sent here by your village to collect as many as possible of those plants.") } else if _, ok := g.Fungus[pos]; ok && g.Dungeon.Cell(pos).T == FreeCell { ui.DrawDescription("Blue dense foliage grows in the Underground. It is difficult to see through, and is flammable.") } else if g.Dungeon.Cell(pos).T == WallCell { ui.DrawDescription("A wall is an impassable pile of rocks. It can be destructed by using some items.") } else { ui.DrawDescription("This is just plain ground.") } } func (ui *gameui) MonsterInfo(m *monster) string { infos := []string{} state := m.State.String() if m.Kind == MonsSatowalgaPlant && m.State == Wandering { state = "awaken" } infos = append(infos, state) for st, i := range m.Statuses { if i > 0 { infos = append(infos, monsterStatus(st).String()) } } p := (m.HP * 100) / m.HPmax health := fmt.Sprintf("%d %% HP", p) infos = append(infos, health) return strings.Join(infos, ", ") } var CenteredCamera bool func (ui *gameui) InView(pos position, targeting bool) bool { g := ui.g if targeting { return pos.DistanceY(ui.cursor) <= 10 && pos.DistanceX(ui.cursor) <= 39 } return pos.DistanceY(g.Player.Pos) <= 10 && pos.DistanceX(g.Player.Pos) <= 39 } func (ui *gameui) CameraOffset(pos position, targeting bool) (int, int) { g := ui.g if targeting { return pos.X + 39 - ui.cursor.X, pos.Y + 10 - ui.cursor.Y } return pos.X + 39 - g.Player.Pos.X, pos.Y + 10 - g.Player.Pos.Y } func (ui *gameui) InViewBorder(pos position, targeting bool) bool { g := ui.g if targeting { return pos.DistanceY(ui.cursor) != 10 && pos.DistanceX(ui.cursor) != 39 } return pos.DistanceY(g.Player.Pos) != 10 && pos.DistanceX(g.Player.Pos) != 39 } func (ui *gameui) DrawAtPosition(pos position, targeting bool, r rune, fg, bg uicolor) { g := ui.g if g.Highlight[pos] || pos == ui.cursor { bg, fg = fg, bg } if CenteredCamera { if !ui.InView(pos, targeting) { return } x, y := ui.CameraOffset(pos, targeting) ui.SetMapCell(x, y, r, fg, bg) if ui.InViewBorder(pos, targeting) && g.Dungeon.Border(pos) { for _, opos := range pos.OutsideNeighbors() { xo, yo := ui.CameraOffset(opos, targeting) ui.SetMapCell(xo, yo, '#', ColorFg, ColorBgBorder) } } return } ui.SetMapCell(pos.X, pos.Y, r, fg, bg) } const BarCol = DungeonWidth + 2 func (ui *gameui) DrawDungeonView(m uiMode) { g := ui.g ui.Clear() d := g.Dungeon for i := 0; i < DungeonWidth; i++ { ui.SetCell(i, DungeonHeight, '─', ColorFg, ColorBg) } for i := 0; i < DungeonHeight; i++ { ui.SetCell(DungeonWidth, i, '│', ColorFg, ColorBg) } ui.SetCell(DungeonWidth, DungeonHeight, '┘', ColorFg, ColorBg) for i := range d.Cells { pos := idxtopos(i) r, fgColor, bgColor := ui.PositionDrawing(pos) ui.DrawAtPosition(pos, m == TargetingMode, r, fgColor, bgColor) } line := 0 if !ui.Small() { ui.SetMapCell(BarCol, line, '[', ColorFg, ColorBg) ui.DrawText(fmt.Sprintf(" %v", g.Player.Armour), BarCol+1, line) line++ ui.SetMapCell(BarCol, line, ')', ColorFg, ColorBg) ui.DrawText(fmt.Sprintf(" %v", g.Player.Weapon), BarCol+1, line) line++ if g.Player.Shield != NoShield { if g.Player.Weapon.TwoHanded() { ui.SetMapCell(BarCol, line, ']', ColorFg, ColorBg) ui.DrawText(" (unusable)", BarCol+1, line) } else { ui.SetMapCell(BarCol, line, ']', ColorFg, ColorBg) ui.DrawText(fmt.Sprintf(" %v", g.Player.Shield), BarCol+1, line) } } line++ line++ } if ui.Small() { ui.DrawStatusLine() } else { ui.DrawStatusBar(line) ui.DrawMenus() } if ui.Small() { ui.DrawLog(2) } else { ui.DrawLog(4) } if m != TargetingMode && m != NoFlushMode { ui.Flush() } } func (ui *gameui) PositionDrawing(pos position) (r rune, fgColor, bgColor uicolor) { g := ui.g m := g.Dungeon c := m.Cell(pos) fgColor = ColorFg bgColor = ColorBg if !c.Explored && !g.Wizard { r = ' ' bgColor = ColorBgDark if g.HasFreeExploredNeighbor(pos) { r = '¤' fgColor = ColorFgDark } if g.DreamingMonster[pos] { r = '☻' fgColor = ColorFgSleepingMonster } if g.Noise[pos] { r = '♫' fgColor = ColorFgWanderingMonster } return } if g.Wizard { if !c.Explored && g.HasFreeExploredNeighbor(pos) && !g.WizardMap { r = '¤' fgColor = ColorFgDark bgColor = ColorBgDark return } if c.T == WallCell { if len(g.Dungeon.FreeNeighbors(pos)) == 0 { r = ' ' return } } } if g.Player.LOS[pos] && !g.WizardMap { fgColor = ColorFgLOS bgColor = ColorBgLOS } else { fgColor = ColorFgDark bgColor = ColorBgDark } if g.ExclusionsMap[pos] && c.T != WallCell { fgColor = ColorFgExcluded } switch { case c.T == WallCell && (!g.WrongWall[pos] || g.Wizard) || c.T == FreeCell && g.WrongWall[pos] && !g.Wizard: r = '#' if g.TemporalWalls[pos] { fgColor = ColorFgMagicPlace } case pos == g.Player.Pos && !g.WizardMap: r = '@' fgColor = ColorFgPlayer default: r = '.' if _, ok := g.Fungus[pos]; ok && !g.WrongFoliage[pos] || !ok && g.WrongFoliage[pos] { r = '"' } if cld, ok := g.Clouds[pos]; ok && g.Player.LOS[pos] { r = '§' if cld == CloudFire { fgColor = ColorFgWanderingMonster } else if cld == CloudNight { fgColor = ColorFgSleepingMonster } } if c, ok := g.Collectables[pos]; ok { r = c.Consumable.Letter() fgColor = ColorFgCollectable } else if eq, ok := g.Equipables[pos]; ok { r = eq.Letter() fgColor = ColorFgCollectable } else if rod, ok := g.Rods[pos]; ok { r = rod.Letter() fgColor = ColorFgCollectable } else if strt, ok := g.Stairs[pos]; ok { r = '>' if strt == WinStair { fgColor = ColorFgMagicPlace r = 'Δ' } else { fgColor = ColorFgPlace } } else if stn, ok := g.MagicalStones[pos]; ok { r = '_' if stn == InertStone { fgColor = ColorFgPlace } else { fgColor = ColorFgMagicPlace } } else if _, ok := g.Simellas[pos]; ok { r = '♣' fgColor = ColorFgSimellas } else if _, ok := g.Doors[pos]; ok { r = '+' fgColor = ColorFgPlace } if (g.Player.LOS[pos] || g.Wizard) && !g.WizardMap { m := g.MonsterAt(pos) if m.Exists() { r = m.Kind.Letter() if m.Status(MonsLignified) { fgColor = ColorFgLignifiedMonster } else if m.Status(MonsConfused) { fgColor = ColorFgConfusedMonster } else if m.Status(MonsSlow) { fgColor = ColorFgSlowedMonster } else if m.State == Resting { fgColor = ColorFgSleepingMonster } else if m.State == Wandering { fgColor = ColorFgWanderingMonster } else { fgColor = ColorFgMonster } } } else if !g.Wizard && g.Noise[pos] { r = '♫' fgColor = ColorFgWanderingMonster } else if !g.Wizard && g.DreamingMonster[pos] { r = '☻' fgColor = ColorFgSleepingMonster } } return } func (ui *gameui) DrawStatusBar(line int) { g := ui.g sts := statusSlice{} if cld, ok := g.Clouds[g.Player.Pos]; ok && cld == CloudFire { g.Player.Statuses[StatusFlames] = 1 defer func() { g.Player.Statuses[StatusFlames] = 0 }() } for st, c := range g.Player.Statuses { if c > 0 { sts = append(sts, st) } } sort.Sort(sts) hpColor := ColorFgHPok switch { case g.Player.HP*100/g.Player.HPMax() < 30: hpColor = ColorFgHPcritical case g.Player.HP*100/g.Player.HPMax() < 70: hpColor = ColorFgHPwounded } mpColor := ColorFgMPok switch { case g.Player.MP*100/g.Player.MPMax() < 30: mpColor = ColorFgMPcritical case g.Player.MP*100/g.Player.MPMax() < 70: mpColor = ColorFgMPpartial } ui.DrawColoredText(fmt.Sprintf("HP: %d", g.Player.HP), BarCol, line, hpColor) line++ ui.DrawColoredText(fmt.Sprintf("MP: %d", g.Player.MP), BarCol, line, mpColor) line++ line++ ui.DrawText(fmt.Sprintf("Simellas: %d", g.Player.Simellas), BarCol, line) line++ if g.Depth == -1 { ui.DrawText("Depth: Out!", BarCol, line) } else { ui.DrawText(fmt.Sprintf("Depth: %d", g.Depth), BarCol, line) } line++ ui.DrawText(fmt.Sprintf("Turns: %.1f", float64(g.Turn)/10), BarCol, line) line++ for _, st := range sts { fg := ColorFgStatusOther if st.Good() { fg = ColorFgStatusGood t := 13 if g.Player.Statuses[StatusBerserk] > 0 { t -= 3 } if g.Player.Statuses[StatusSlow] > 0 { t += 3 } if g.Player.Expire[st] >= g.Ev.Rank() && g.Player.Expire[st]-g.Ev.Rank() <= t { fg = ColorFgStatusExpire } } else if st.Bad() { fg = ColorFgStatusBad } if g.Player.Statuses[st] > 1 { ui.DrawColoredText(fmt.Sprintf("%s(%d)", st, g.Player.Statuses[st]), BarCol, line, fg) } else { ui.DrawColoredText(st.String(), BarCol, line, fg) } line++ } } func (ui *gameui) DrawStatusLine() { g := ui.g sts := statusSlice{} if cld, ok := g.Clouds[g.Player.Pos]; ok && cld == CloudFire { g.Player.Statuses[StatusFlames] = 1 defer func() { g.Player.Statuses[StatusFlames] = 0 }() } for st, c := range g.Player.Statuses { if c > 0 { sts = append(sts, st) } } sort.Sort(sts) hpColor := ColorFgHPok switch { case g.Player.HP*100/g.Player.HPMax() < 30: hpColor = ColorFgHPcritical case g.Player.HP*100/g.Player.HPMax() < 70: hpColor = ColorFgHPwounded } mpColor := ColorFgMPok switch { case g.Player.MP*100/g.Player.MPMax() < 30: mpColor = ColorFgMPcritical case g.Player.MP*100/g.Player.MPMax() < 70: mpColor = ColorFgMPpartial } line := DungeonHeight col := 2 ui.DrawText(" ", col, line) col++ ui.SetMapCell(col, line, ')', ColorFg, ColorBg) col++ weapon := fmt.Sprintf("%s ", g.Player.Weapon.Short()) ui.DrawText(weapon, col, line) col += utf8.RuneCountInString(weapon) ui.SetMapCell(col, line, '[', ColorFg, ColorBg) col++ armour := fmt.Sprintf("%s ", g.Player.Armour.Short()) ui.DrawText(armour, col, line) col += utf8.RuneCountInString(armour) if g.Player.Shield != NoShield { ui.SetMapCell(col, line, ']', ColorFg, ColorBg) col++ shield := fmt.Sprintf("%s ", g.Player.Shield.Short()) ui.DrawText(shield, col, line) col += utf8.RuneCountInString(shield) } ui.SetMapCell(col, line, '♣', ColorFg, ColorBg) col++ simellas := fmt.Sprintf(":%d ", g.Player.Simellas) ui.DrawText(simellas, col, line) col += utf8.RuneCountInString(simellas) var depth string if g.Depth == -1 { depth = "D: Out! " } else { depth = fmt.Sprintf("D:%d ", g.Depth) } ui.DrawText(depth, col, line) col += utf8.RuneCountInString(depth) turns := fmt.Sprintf("T:%.1f ", float64(g.Turn)/10) ui.DrawText(turns, col, line) col += utf8.RuneCountInString(turns) hp := fmt.Sprintf("HP:%2d ", g.Player.HP) ui.DrawColoredText(hp, col, line, hpColor) col += utf8.RuneCountInString(hp) mp := fmt.Sprintf("MP:%d ", g.Player.MP) ui.DrawColoredText(mp, col, line, mpColor) col += utf8.RuneCountInString(mp) if len(sts) > 0 { ui.DrawText("| ", col, line) col += 2 } for _, st := range sts { fg := ColorFgStatusOther if st.Good() { fg = ColorFgStatusGood t := 13 if g.Player.Statuses[StatusBerserk] > 0 { t -= 3 } if g.Player.Statuses[StatusSlow] > 0 { t += 3 } if g.Player.Expire[st] >= g.Ev.Rank() && g.Player.Expire[st]-g.Ev.Rank() <= t { fg = ColorFgStatusExpire } } else if st.Bad() { fg = ColorFgStatusBad } var sttext string if g.Player.Statuses[st] > 1 { sttext = fmt.Sprintf("%s(%d) ", st.Short(), g.Player.Statuses[st]) } else { sttext = fmt.Sprintf("%s ", st.Short()) } ui.DrawColoredText(sttext, col, line, fg) col += utf8.RuneCountInString(sttext) } } func (ui *gameui) LogColor(e logEntry) uicolor { fg := ColorFg switch e.Style { case logCritic: fg = ColorRed case logPlayerHit: fg = ColorGreen case logMonsterHit: fg = ColorOrange case logSpecial: fg = ColorMagenta case logStatusEnd: fg = ColorViolet case logError: fg = ColorRed } return fg } func (ui *gameui) DrawLog(lines int) { g := ui.g min := len(g.Log) - lines if min < 0 { min = 0 } l := len(g.Log) - 1 if l < lines { lines = l + 1 } for i := lines; i > 0 && l >= 0; i-- { cols := 0 first := true to := l for l >= 0 { e := g.Log[l] el := utf8.RuneCountInString(e.String()) if e.Tick { el += 2 } cols += el + 1 if !first && cols > DungeonWidth { l++ break } if e.Tick || l <= i { break } first = false l-- } if l < 0 { l = 0 } col := 0 for ln := l; ln <= to; ln++ { e := g.Log[ln] fguicolor := ui.LogColor(e) if e.Tick { ui.DrawColoredText("•", 0, DungeonHeight+i, ColorYellow) col += 2 } ui.DrawColoredText(e.String(), col, DungeonHeight+i, fguicolor) col += utf8.RuneCountInString(e.String()) + 1 } l-- } } func InRuneSlice(r rune, s []rune) bool { for _, rr := range s { if r == rr { return true } } return false } func (ui *gameui) RunesForKeyAction(k keyAction) string { runes := []rune{} for r, ka := range GameConfig.RuneNormalModeKeys { if k == ka && !InRuneSlice(r, runes) { runes = append(runes, r) } } for r, ka := range GameConfig.RuneTargetModeKeys { if k == ka && !InRuneSlice(r, runes) { runes = append(runes, r) } } chars := strings.Split(string(runes), "") sort.Strings(chars) text := strings.Join(chars, " or ") return text } type keyConfigAction int const ( NavigateKeys keyConfigAction = iota ChangeKeys ResetKeys QuitKeyConfig ) func (ui *gameui) ChangeKeys() { g := ui.g lines := DungeonHeight nmax := len(configurableKeyActions) - lines n := 0 s := 0 loop: for { ui.DrawDungeonView(NoFlushMode) if n >= nmax { n = nmax } if n < 0 { n = 0 } to := n + lines if to >= len(configurableKeyActions) { to = len(configurableKeyActions) } for i := n; i < to; i++ { ka := configurableKeyActions[i] desc := ka.NormalModeDescription() if !ka.NormalModeKey() { desc = ka.TargetingModeDescription() } bg := ui.ListItemBG(i) ui.ClearLineWithColor(i-n, bg) desc = fmt.Sprintf(" %-36s %s", desc, ui.RunesForKeyAction(ka)) if i == s { ui.DrawColoredTextOnBG(desc, 0, i-n, ColorYellow, bg) } else { ui.DrawColoredTextOnBG(desc, 0, i-n, ColorFg, bg) } } ui.ClearLine(lines) ui.DrawStyledTextLine(" add key (a) up/down (arrows/u/d) reset (R) quit (x) ", lines, FooterLine) ui.Flush() var action keyConfigAction s, action = ui.KeyMenuAction(s) if s >= len(configurableKeyActions) { s = len(configurableKeyActions) - 1 } if s < 0 { s = 0 } if s < n+1 { n -= 12 } if s > n+lines-2 { n += 12 } switch action { case ChangeKeys: ui.DrawStyledTextLine(" insert new key ", lines, FooterLine) ui.Flush() r := ui.ReadRuneKey() if r == 0 { continue loop } if FixedRuneKey(r) { g.Printf("You cannot rebind “%c”.", r) continue loop } CustomKeys = true ka := configurableKeyActions[s] if ka.NormalModeKey() { GameConfig.RuneNormalModeKeys[r] = ka } else { delete(GameConfig.RuneNormalModeKeys, r) } if ka.TargetingModeKey() { GameConfig.RuneTargetModeKeys[r] = ka } else { delete(GameConfig.RuneTargetModeKeys, r) } err := g.SaveConfig() if err != nil { g.Print(err.Error()) } case QuitKeyConfig: break loop case ResetKeys: ApplyDefaultKeyBindings() err := g.SaveConfig() //err := g.RemoveDataFile("config.gob") if err != nil { g.Print(err.Error()) } } } } func (ui *gameui) DrawPreviousLogs() { g := ui.g bottom := 4 if ui.Small() { bottom = 2 } lines := DungeonHeight + bottom nmax := len(g.Log) - lines n := nmax loop: for { ui.DrawDungeonView(NoFlushMode) if n >= nmax { n = nmax } if n < 0 { n = 0 } to := n + lines if to >= len(g.Log) { to = len(g.Log) } for i := 0; i < bottom; i++ { ui.SetCell(DungeonWidth, DungeonHeight+i, '│', ColorFg, ColorBg) } for i := n; i < to; i++ { e := g.Log[i] fguicolor := ui.LogColor(e) ui.ClearLine(i - n) rc := utf8.RuneCountInString(e.String()) if e.Tick { rc += 2 } if rc >= DungeonWidth { for j := DungeonWidth; j < 103; j++ { ui.SetCell(j, i-n, ' ', ColorFg, ColorBg) } } if e.Tick { ui.DrawColoredText("•", 0, i-n, ColorYellow) ui.DrawColoredText(e.String(), 2, i-n, fguicolor) } else { ui.DrawColoredText(e.String(), 0, i-n, fguicolor) } } for i := len(g.Log); i < DungeonHeight+bottom; i++ { ui.ClearLine(i - n) } ui.ClearLine(lines) s := fmt.Sprintf(" half-page up/down (u/d) quit (x) — (%d/%d) \n", len(g.Log)-to, len(g.Log)) ui.DrawStyledTextLine(s, lines, FooterLine) ui.Flush() var quit bool n, quit = ui.Scroll(n) if quit { break loop } } } func (ui *gameui) DrawMonsterDescription(mons *monster) { s := mons.Kind.Desc() s += " " + fmt.Sprintf("They can hit for up to %d damage.", mons.Kind.BaseAttack()) s += " " + fmt.Sprintf("They have around %d HP.", mons.Kind.MaxHP()) ui.DrawDescription(s) } func (ui *gameui) DrawConsumableDescription(c consumable) { ui.DrawDescription(c.Desc()) } func (ui *gameui) DrawDescription(desc string) { ui.DrawDungeonView(NoFlushMode) desc = formatText(desc, TextWidth) lines := strings.Count(desc, "\n") for i := 0; i <= lines+2; i++ { ui.ClearLine(i) } ui.DrawText(desc, 0, 0) ui.DrawTextLine(" press (x) to continue ", lines+2) ui.Flush() ui.WaitForContinue(lines + 2) ui.DrawDungeonView(NoFlushMode) } func (ui *gameui) DrawText(text string, x, y int) { ui.DrawColoredText(text, x, y, ColorFg) } func (ui *gameui) DrawColoredText(text string, x, y int, fg uicolor) { ui.DrawColoredTextOnBG(text, x, y, fg, ColorBg) } func (ui *gameui) DrawColoredTextOnBG(text string, x, y int, fg, bg uicolor) { col := 0 for _, r := range text { if r == '\n' { y++ col = 0 continue } if x+col >= UIWidth { break } ui.SetCell(x+col, y, r, fg, bg) col++ } } func (ui *gameui) DrawLine(lnum int) { for i := 0; i < DungeonWidth; i++ { ui.SetCell(i, lnum, '─', ColorFg, ColorBg) } ui.SetCell(DungeonWidth, lnum, '┤', ColorFg, ColorBg) } func (ui *gameui) DrawTextLine(text string, lnum int) { ui.DrawStyledTextLine(text, lnum, NormalLine) } type linestyle int const ( NormalLine linestyle = iota HeaderLine FooterLine ) func (ui *gameui) DrawInfoLine(text string) { ui.ClearLineWithColor(DungeonHeight+1, ColorBgBorder) ui.DrawColoredTextOnBG(text, 0, DungeonHeight+1, ColorBlue, ColorBgBorder) } func (ui *gameui) DrawStyledTextLine(text string, lnum int, st linestyle) { nchars := utf8.RuneCountInString(text) dist := (DungeonWidth - nchars) / 2 for i := 0; i < dist; i++ { ui.SetCell(i, lnum, '─', ColorFg, ColorBg) } switch st { case HeaderLine: ui.DrawColoredText(text, dist, lnum, ColorYellow) case FooterLine: ui.DrawColoredText(text, dist, lnum, ColorCyan) default: ui.DrawColoredText(text, dist, lnum, ColorFg) } for i := dist + nchars; i < DungeonWidth; i++ { ui.SetCell(i, lnum, '─', ColorFg, ColorBg) } switch st { case HeaderLine: ui.SetCell(DungeonWidth, lnum, '┐', ColorFg, ColorBg) case FooterLine: ui.SetCell(DungeonWidth, lnum, '┘', ColorFg, ColorBg) default: ui.SetCell(DungeonWidth, lnum, '┤', ColorFg, ColorBg) } } func (ui *gameui) ClearLine(lnum int) { for i := 0; i < DungeonWidth; i++ { ui.SetCell(i, lnum, ' ', ColorFg, ColorBg) } ui.SetCell(DungeonWidth, lnum, '│', ColorFg, ColorBg) } func (ui *gameui) ClearLineWithColor(lnum int, bg uicolor) { for i := 0; i < DungeonWidth; i++ { ui.SetCell(i, lnum, ' ', ColorFg, bg) } ui.SetCell(DungeonWidth, lnum, '│', ColorFg, ColorBg) } func (ui *gameui) ListItemBG(i int) uicolor { bg := ColorBg if i%2 == 1 { bg = ColorBgBorder } return bg } func (ui *gameui) ConsumableItem(i, lnum int, c consumable, fg uicolor) { g := ui.g bg := ui.ListItemBG(i) ui.ClearLineWithColor(lnum, bg) ui.DrawColoredTextOnBG(fmt.Sprintf("%c - %s (%d available)", rune(i+97), c, g.Player.Consumables[c]), 0, lnum, fg, bg) } func (ui *gameui) SelectProjectile(ev event) error { g := ui.g desc := false for { cs := g.SortedProjectiles() ui.ClearLine(0) if !ui.Small() { ui.DrawColoredText(MenuThrow.String(), MenuCols[MenuThrow][0], DungeonHeight, ColorCyan) } if desc { ui.DrawColoredText("Describe", 0, 0, ColorBlue) col := utf8.RuneCountInString("Describe") ui.DrawText(" which projectile? (press ? or click here for throwing menu)", col, 0) } else { ui.DrawColoredText("Throw", 0, 0, ColorOrange) col := utf8.RuneCountInString("Throw") ui.DrawText(" which projectile? (press ? or click here for describe menu)", col, 0) } for i, c := range cs { ui.ConsumableItem(i, i+1, c, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(cs)+1) ui.Flush() index, alt, err := ui.Select(len(cs)) if alt { desc = !desc continue } if err == nil { ui.ConsumableItem(index, index+1, cs[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) if desc { ui.DrawDescription(cs[index].Desc()) continue } err = cs[index].Use(g, ev) } return err } } func (ui *gameui) SelectPotion(ev event) error { g := ui.g desc := false for { cs := g.SortedPotions() ui.ClearLine(0) if !ui.Small() { ui.DrawColoredText(MenuDrink.String(), MenuCols[MenuDrink][0], DungeonHeight, ColorCyan) } if desc { ui.DrawColoredText("Describe", 0, 0, ColorBlue) col := utf8.RuneCountInString("Describe") ui.DrawText(" which potion? (press ? or click here for quaff menu)", col, 0) } else { ui.DrawColoredText("Drink", 0, 0, ColorGreen) col := utf8.RuneCountInString("Drink") ui.DrawText(" which potion? (press ? or click here for description menu)", col, 0) } for i, c := range cs { ui.ConsumableItem(i, i+1, c, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(cs)+1) ui.Flush() index, alt, err := ui.Select(len(cs)) if alt { desc = !desc continue } if err == nil { ui.ConsumableItem(index, index+1, cs[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) if desc { ui.DrawDescription(cs[index].Desc()) continue } err = cs[index].Use(g, ev) } return err } } func (ui *gameui) RodItem(i, lnum int, r rod, fg uicolor) { g := ui.g bg := ui.ListItemBG(i) ui.ClearLineWithColor(lnum, bg) mc := r.MaxCharge() if g.Player.Armour == CelmistRobe { mc += 2 } ui.DrawColoredTextOnBG(fmt.Sprintf("%c - %s (%d/%d charges, %d mana cost)", rune(i+97), r, g.Player.Rods[r].Charge, mc, r.MPCost()), 0, lnum, fg, bg) } func (ui *gameui) SelectRod(ev event) error { g := ui.g desc := false for { rs := g.SortedRods() ui.ClearLine(0) if !ui.Small() { ui.DrawColoredText(MenuEvoke.String(), MenuCols[MenuEvoke][0], DungeonHeight, ColorCyan) } if desc { ui.DrawColoredText("Describe", 0, 0, ColorBlue) col := utf8.RuneCountInString("Describe") ui.DrawText(" which rod? (press ? or click here for evocation menu)", col, 0) } else { ui.DrawColoredText("Evoke", 0, 0, ColorCyan) col := utf8.RuneCountInString("Evoke") ui.DrawText(" which rod? (press ? or click here for description menu)", col, 0) } for i, r := range rs { ui.RodItem(i, i+1, r, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(rs)+1) ui.Flush() index, alt, err := ui.Select(len(rs)) if alt { desc = !desc continue } if err == nil { ui.RodItem(index, index+1, rs[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) if desc { ui.DrawDescription(rs[index].Desc()) continue } err = rs[index].Use(g, ev) } return err } } func (ui *gameui) ActionItem(i, lnum int, ka keyAction, fg uicolor) { bg := ui.ListItemBG(i) ui.ClearLineWithColor(lnum, bg) desc := ka.NormalModeDescription() if !ka.NormalModeKey() { desc = ka.TargetingModeDescription() } ui.DrawColoredTextOnBG(fmt.Sprintf("%c - %s", rune(i+97), desc), 0, lnum, fg, bg) } var menuActions = []keyAction{ KeyCharacterInfo, KeyLogs, KeyMenuCommandHelp, KeyMenuTargetingHelp, KeyConfigure, KeySave, KeyQuit, } func (ui *gameui) SelectAction(actions []keyAction, ev event) (keyAction, error) { for { ui.ClearLine(0) if !ui.Small() { ui.DrawColoredText(MenuOther.String(), MenuCols[MenuOther][0], DungeonHeight, ColorCyan) } ui.DrawColoredText("Choose", 0, 0, ColorCyan) col := utf8.RuneCountInString("Choose") ui.DrawText(" which action?", col, 0) for i, r := range actions { ui.ActionItem(i, i+1, r, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(actions)+1) ui.Flush() index, alt, err := ui.Select(len(actions)) if alt { continue } if err != nil { ui.DrawDungeonView(NoFlushMode) return KeyExamine, err } ui.ActionItem(index, index+1, actions[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawDungeonView(NoFlushMode) return actions[index], nil } } type setting int const ( setKeys setting = iota invertLOS toggleLayout toggleTiles ) func (s setting) String() (text string) { switch s { case setKeys: text = "Change key bindings" case invertLOS: text = "Toggle dark/light LOS" case toggleLayout: text = "Toggle normal/compact layout" case toggleTiles: text = "Toggle Tiles/Ascii display" } return text } var settingsActions = []setting{ setKeys, invertLOS, toggleLayout, } func (ui *gameui) ConfItem(i, lnum int, s setting, fg uicolor) { bg := ui.ListItemBG(i) ui.ClearLineWithColor(lnum, bg) ui.DrawColoredTextOnBG(fmt.Sprintf("%c - %s", rune(i+97), s), 0, lnum, fg, bg) } func (ui *gameui) SelectConfigure(actions []setting) (setting, error) { for { ui.ClearLine(0) ui.DrawColoredText("Perform", 0, 0, ColorCyan) col := utf8.RuneCountInString("Perform") ui.DrawText(" which change?", col, 0) for i, r := range actions { ui.ConfItem(i, i+1, r, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(actions)+1) ui.Flush() index, alt, err := ui.Select(len(actions)) if alt { continue } if err != nil { ui.DrawDungeonView(NoFlushMode) return setKeys, err } ui.ConfItem(index, index+1, actions[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawDungeonView(NoFlushMode) return actions[index], nil } } func (ui *gameui) HandleSettingAction() error { g := ui.g s, err := ui.SelectConfigure(settingsActions) if err != nil { return err } switch s { case setKeys: ui.ChangeKeys() case invertLOS: GameConfig.DarkLOS = !GameConfig.DarkLOS err := g.SaveConfig() if err != nil { g.Print(err.Error()) } if GameConfig.DarkLOS { ApplyDarkLOS() } else { ApplyLightLOS() } case toggleLayout: ui.ApplyToggleLayout() err := g.SaveConfig() if err != nil { g.Print(err.Error()) } case toggleTiles: ui.ApplyToggleTiles() err := g.SaveConfig() if err != nil { g.Print(err.Error()) } } return nil } func (ui *gameui) WizardItem(i, lnum int, s wizardAction, fg uicolor) { bg := ui.ListItemBG(i) ui.ClearLineWithColor(lnum, bg) ui.DrawColoredTextOnBG(fmt.Sprintf("%c - %s", rune(i+97), s), 0, lnum, fg, bg) } func (ui *gameui) SelectWizardMagic(actions []wizardAction) (wizardAction, error) { for { ui.ClearLine(0) ui.DrawColoredText("Evoke", 0, 0, ColorCyan) col := utf8.RuneCountInString("Evoke") ui.DrawText(" which magic?", col, 0) for i, r := range actions { ui.WizardItem(i, i+1, r, ColorFg) } ui.DrawTextLine(" press (x) to cancel ", len(actions)+1) ui.Flush() index, alt, err := ui.Select(len(actions)) if alt { continue } if err != nil { ui.DrawDungeonView(NoFlushMode) return WizardInfoAction, err } ui.WizardItem(index, index+1, actions[index], ColorYellow) ui.Flush() time.Sleep(75 * time.Millisecond) ui.DrawDungeonView(NoFlushMode) return actions[index], nil } } func (ui *gameui) DrawMenus() { line := DungeonHeight for i, cols := range MenuCols[0 : len(MenuCols)-1] { if cols[0] >= 0 { if menu(i) == ui.menuHover { ui.DrawColoredText(menu(i).String(), cols[0], line, ColorBlue) } else { ui.DrawColoredText(menu(i).String(), cols[0], line, ColorViolet) } } } interactMenu := ui.UpdateInteractButton() if interactMenu == "" { return } i := len(MenuCols) - 1 cols := MenuCols[i] if menu(i) == ui.menuHover { ui.DrawColoredText(interactMenu, cols[0], line, ColorBlue) } else { ui.DrawColoredText(interactMenu, cols[0], line, ColorViolet) } } boohu-0.13.0/dump.go000066400000000000000000000265251356500202200142100ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io" "path/filepath" "sort" "strings" ) type rodSlice []rod func (rs rodSlice) Len() int { return len(rs) } func (rs rodSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } func (rs rodSlice) Less(i, j int) bool { return int(rs[i]) < int(rs[j]) } type consumableSlice []consumable func (cs consumableSlice) Len() int { return len(cs) } func (cs consumableSlice) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] } func (cs consumableSlice) Less(i, j int) bool { return cs[i].Int() < cs[j].Int() } type statusSlice []status func (sts statusSlice) Len() int { return len(sts) } func (sts statusSlice) Swap(i, j int) { sts[i], sts[j] = sts[j], sts[i] } func (sts statusSlice) Less(i, j int) bool { return sts[i] < sts[j] } type monsSlice []monsterKind func (ms monsSlice) Len() int { return len(ms) } func (ms monsSlice) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] } func (ms monsSlice) Less(i, j int) bool { return ms[i].Dangerousness() > ms[j].Dangerousness() } func (g *game) DumpAptitudes() string { apts := []string{} for apt, b := range g.Player.Aptitudes { if b { apts = append(apts, apt.String()) } } sort.Strings(apts) if len(apts) == 0 { return "You do not have any special aptitudes." } return "Aptitudes:\n" + strings.Join(apts, "\n") } func (g *game) DumpStatuses() string { sts := sort.StringSlice{} for st, c := range g.Player.Statuses { if c > 0 { sts = append(sts, st.String()) } } sort.Sort(sts) if len(sts) == 0 { return "You are free of any status effects." } return "Statuses:\n" + strings.Join(sts, "\n") } func (g *game) SortedRods() rodSlice { var rs rodSlice for k, _ := range g.Player.Rods { rs = append(rs, k) } sort.Sort(rs) return rs } func (g *game) SortedKilledMonsters() monsSlice { var ms monsSlice for mk, p := range g.Stats.KilledMons { if p == 0 { continue } ms = append(ms, mk) } sort.Sort(ms) return ms } func (g *game) SortedPotions() consumableSlice { var cs consumableSlice for k := range g.Player.Consumables { switch k := k.(type) { case potion: cs = append(cs, k) } } sort.Sort(cs) return cs } func (g *game) SortedProjectiles() consumableSlice { var cs consumableSlice for k := range g.Player.Consumables { switch k := k.(type) { case projectile: cs = append(cs, k) } } sort.Sort(cs) return cs } func (g *game) Dump() string { buf := &bytes.Buffer{} fmt.Fprintf(buf, " -- Boohu version %s character file --\n\n", Version) if g.Wizard { fmt.Fprintf(buf, "**WIZARD MODE**\n") } if g.Player.HP > 0 && g.Depth == -1 { fmt.Fprintf(buf, "You escaped from Hareka's Underground alive!\n") } else if g.Player.HP <= 0 { fmt.Fprintf(buf, "You died while exploring depth %d of Hareka's Underground.\n", g.Depth) } else { fmt.Fprintf(buf, "You are exploring depth %d of Hareka's Underground.\n", g.Depth) } fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "You have %d/%d HP, and %d/%d MP.\n", g.Player.HP, g.Player.HPMax(), g.Player.MP, g.Player.MPMax()) fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, g.DumpAptitudes()) fmt.Fprintf(buf, "\n\n") fmt.Fprintf(buf, g.DumpStatuses()) fmt.Fprintf(buf, "\n\n") fmt.Fprintf(buf, "Equipment:\n") fmt.Fprintf(buf, "You are wearing %s.\n", g.Player.Armour.StringIndefinite()) fmt.Fprintf(buf, "You are wielding %s.\n", Indefinite(g.Player.Weapon.String(), false)) if g.Player.Shield != NoShield { if g.Player.Weapon.TwoHanded() { fmt.Fprintf(buf, "You have %s (unused).\n", Indefinite(g.Player.Shield.String(), false)) } else { fmt.Fprintf(buf, "You are wearing %s.\n", Indefinite(g.Player.Shield.String(), false)) } } fmt.Fprintf(buf, "\n") rs := g.SortedRods() if len(rs) > 0 { fmt.Fprintf(buf, "Rods:\n") for _, r := range rs { mc := r.MaxCharge() if g.Player.Armour == CelmistRobe { mc += 2 } fmt.Fprintf(buf, "- %s (%d/%d charges) (used %d times)\n", r, g.Player.Rods[r].Charge, mc, g.Stats.UsedRod[r]) } } else { fmt.Fprintf(buf, "You do not have any rods.\n") } fmt.Fprintf(buf, "\n") ps := g.SortedPotions() if len(ps) > 0 { fmt.Fprintf(buf, "Potions:\n") for _, p := range ps { fmt.Fprintf(buf, "- %s (%d available)\n", p, g.Player.Consumables[p]) } } else { fmt.Fprintf(buf, "You do not have any potions.\n") } fmt.Fprintf(buf, "\n") ps = g.SortedProjectiles() if len(ps) > 0 { fmt.Fprintf(buf, "Projectiles:\n") for _, p := range ps { fmt.Fprintf(buf, "- %s (%d available)\n", p, g.Player.Consumables[p]) } } else { fmt.Fprintf(buf, "You do not have any projectiles.\n") } fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "Miscellaneous:\n") fmt.Fprintf(buf, "You collected %d simellas.\n", g.Player.Simellas) fmt.Fprintf(buf, "You killed %d monsters.\n", g.Stats.Killed) fmt.Fprintf(buf, "You spent %d turns in the Underground.\n", g.Turn/10) maxDepth := Max(g.Depth, g.ExploredLevels) s := "s" if maxDepth == 1 { s = "" } fmt.Fprintf(buf, "You explored %d level%s out of %d.\n", maxDepth, s, MaxDepth) fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "Last messages:\n") for i := len(g.Log) - 10; i < len(g.Log); i++ { if i >= 0 { fmt.Fprintf(buf, "%s\n", g.Log[i]) } } fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "Dungeon:\n") fmt.Fprintf(buf, "┌%s┐\n", strings.Repeat("─", DungeonWidth)) buf.WriteString(g.DumpDungeon()) fmt.Fprintf(buf, "└%s┘\n", strings.Repeat("─", DungeonWidth)) fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, g.DumpedKilledMonsters()) fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "Timeline:\n") fmt.Fprintf(buf, g.DumpStory()) fmt.Fprintf(buf, "\n") g.DetailedStatistics(buf) return buf.String() } func (g *game) DetailedStatistics(w io.Writer) { fmt.Fprintf(w, "\n") fmt.Fprintf(w, "Statistics:\n") fmt.Fprintf(w, "You drank %d potions, throwed %d items, and evoked rods %d times.\n", g.Stats.Drinks, g.Stats.Throws, g.Stats.Evocations) fmt.Fprintf(w, "You had %d hits (%.1f per 100 turns), %d misses (%.1f), and %d moves (%.1f).\n", g.Stats.Hits, float64(g.Stats.Hits)*100/float64(g.Stats.Turns+1), g.Stats.Misses, float64(g.Stats.Misses)*100/float64(g.Stats.Turns+1), g.Stats.Moves, float64(g.Stats.Moves)*100/float64(g.Stats.Turns+1)) fmt.Fprintf(w, "You got hit %d times, blocked %d times, and dodged %d times.\n", g.Stats.ReceivedHits, g.Stats.Blocks, g.Stats.Dodges) fmt.Fprintf(w, "You endured %d damage.\n", g.Stats.Damage) fmt.Fprintf(w, "You were lucky %d times.\n", g.Stats.TimesLucky) fmt.Fprintf(w, "You activated %d stones.\n", g.Stats.UsedStones) fmt.Fprintf(w, "There were %d fires.\n", g.Stats.Burns) fmt.Fprintf(w, "There were %d destroyed walls.\n", g.Stats.Digs) fmt.Fprintf(w, "You rested %d times (%d interruptions).\n", g.Stats.Rest, g.Stats.RestInterrupt) fmt.Fprintf(w, "You spent %d%% turns wounded.\n", g.Stats.TWounded*100/(g.Stats.Turns+1)) fmt.Fprintf(w, "You spent %d%% turns with monsters in sight.\n", g.Stats.TMonsLOS*100/(g.Stats.Turns+1)) fmt.Fprintf(w, "You spent %d%% turns wounded with monsters in sight.\n", g.Stats.TMWounded*100/(g.Stats.Turns+1)) maxDepth := Max(g.Depth-1, g.ExploredLevels) if g.Player.HP <= 0 { maxDepth++ } if maxDepth >= MaxDepth+1 { // should not happen maxDepth = -1 } fmt.Fprintf(w, "\n") hfmt := "%-23s" fmt.Fprintf(w, hfmt, "Quantity/Depth") for i := 1; i <= maxDepth; i++ { fmt.Fprintf(w, " %3d", i) } fmt.Fprintf(w, "\n") fmt.Fprintf(w, hfmt, "Explored (%)") for i, n := range g.Stats.DExplPerc { if i == 0 { continue } if i > maxDepth { break } fmt.Fprintf(w, " %3d", n) } fmt.Fprintf(w, "\n") fmt.Fprintf(w, hfmt, "Sleeping monsters (%)") for i, n := range g.Stats.DSleepingPerc { if i == 0 { continue } if i > maxDepth { break } fmt.Fprintf(w, " %3d", n) } fmt.Fprintf(w, "\n") fmt.Fprintf(w, hfmt, "Dead monsters (%)") for i, n := range g.Stats.DKilledPerc { if i == 0 { continue } if i > maxDepth { break } fmt.Fprintf(w, " %3d", n) } fmt.Fprintf(w, "\n") fmt.Fprintf(w, hfmt, "Dungeon Layout") for i, s := range g.Stats.DLayout { if i == 0 { continue } if i > maxDepth { break } fmt.Fprintf(w, " %3s", s) } fmt.Fprintf(w, "\n") fmt.Fprintf(w, "\n") fmt.Fprintf(w, "Legend:") for i, c := range []dungen{GenCaveMap, GenRoomMap, GenCellularAutomataCaveMap, GenCaveMapTree, GenRuinsMap, GenBSPMap} { if i == 4 { fmt.Fprintf(w, "\n ") } fmt.Fprintf(w, " %s (%s)", c.Description(), c.String()) } } func (g *game) DumpStory() string { return strings.Join(g.Stats.Story, "\n") } func (g *game) DumpDungeon() string { buf := bytes.Buffer{} for i, c := range g.Dungeon.Cells { if i%DungeonWidth == 0 { if i == 0 { buf.WriteRune('│') } else { buf.WriteString("│\n│") } } pos := idxtopos(i) if !c.Explored { buf.WriteRune(' ') if i == len(g.Dungeon.Cells)-1 { buf.WriteString("│\n") } continue } var r rune switch c.T { case WallCell: r = '#' case FreeCell: switch { case pos == g.Player.Pos: r = '@' default: r = '.' if _, ok := g.Fungus[pos]; ok { r = '"' } if _, ok := g.Clouds[pos]; ok && g.Player.LOS[pos] { r = '§' } if c, ok := g.Collectables[pos]; ok { r = c.Consumable.Letter() } else if eq, ok := g.Equipables[pos]; ok { r = eq.Letter() } else if rd, ok := g.Rods[pos]; ok { r = rd.Letter() } else if strt, ok := g.Stairs[pos]; ok { r = '>' if strt == WinStair { r = 'Δ' } } else if _, ok := g.MagicalStones[pos]; ok { r = '_' } else if _, ok := g.Simellas[pos]; ok { r = '♣' } else if _, ok := g.Doors[pos]; ok { r = '+' } m := g.MonsterAt(pos) if m.Exists() && (g.Player.LOS[m.Pos] || g.Wizard) { r = m.Kind.Letter() } } } buf.WriteRune(r) if i == len(g.Dungeon.Cells)-1 { buf.WriteString("│\n") } } return buf.String() } func (g *game) DumpedKilledMonsters() string { buf := &bytes.Buffer{} fmt.Fprint(buf, "Killed Monsters:\n") ms := g.SortedKilledMonsters() for _, mk := range ms { fmt.Fprintf(buf, "- %s: %d\n", mk, g.Stats.KilledMons[mk]) } return buf.String() } func (g *game) SimplifedDump(err error) string { buf := &bytes.Buffer{} fmt.Fprintf(buf, " ♣ Boohu version %s play summary ♣\n\n", Version) if g.Wizard { fmt.Fprintf(buf, "**WIZARD MODE**\n") } if g.Player.HP > 0 && g.Depth == -1 { fmt.Fprintf(buf, "You escaped from Hareka's Underground alive!\n") } else if g.Player.HP <= 0 { fmt.Fprintf(buf, "You died while exploring depth %d of Hareka's Underground.\n", g.Depth) } else { fmt.Fprintf(buf, "You are exploring depth %d of Hareka's Underground.\n", g.Depth) } fmt.Fprintf(buf, "You collected %d simellas.\n", g.Player.Simellas) fmt.Fprintf(buf, "You killed %d monsters.\n", g.Stats.Killed) fmt.Fprintf(buf, "You spent %.0f turns in the Underground.\n", float64(g.Turn)/10) maxDepth := Max(g.Depth, g.ExploredLevels) s := "s" if maxDepth == 1 { s = "" } fmt.Fprintf(buf, "You explored %d level%s out of %d.\n", maxDepth, s, MaxDepth+1) fmt.Fprintf(buf, "\n") if err != nil { fmt.Fprintf(buf, "Error writing dump: %v.\n", err) } else { dataDir, err := g.DataDir() if err == nil { if dataDir == "" { fmt.Fprintf(buf, "Full game statistics written below.\n") } else { fmt.Fprintf(buf, "Full game statistics dump written to %s.\n", filepath.Join(dataDir, "dump")) } } } fmt.Fprintf(buf, "\n\n") fmt.Fprintf(buf, "───Press (x) to quit───") return buf.String() } boohu-0.13.0/dungeon.go000066400000000000000000001004671356500202200147000ustar00rootroot00000000000000// many ideas here from articles found at http://www.roguebasin.com/ package main import ( "sort" ) type dungeon struct { Gen dungen Cells []cell } type cell struct { T terrain Explored bool } type terrain int const ( WallCell terrain = iota FreeCell ) type dungen int const ( GenCaveMap dungen = iota GenRoomMap GenCellularAutomataCaveMap GenCaveMapTree GenRuinsMap GenBSPMap ) func (dg dungen) Use(g *game) { switch dg { case GenCaveMap: g.GenCaveMap(DungeonHeight, DungeonWidth) case GenRoomMap: g.GenRoomMap(DungeonHeight, DungeonWidth) case GenCellularAutomataCaveMap: g.GenCellularAutomataCaveMap(DungeonHeight, DungeonWidth) case GenCaveMapTree: g.GenCaveMapTree(DungeonHeight, DungeonWidth) case GenRuinsMap: g.GenRuinsMap(DungeonHeight, DungeonWidth) case GenBSPMap: g.GenBSPMap(DungeonHeight, DungeonWidth) } g.Dungeon.Gen = dg g.Stats.DLayout[g.Depth] = dg.String() } func (dg dungen) String() (text string) { switch dg { case GenCaveMap: text = "OC" case GenRoomMap: text = "BR" case GenCellularAutomataCaveMap: text = "EC" case GenCaveMapTree: text = "TC" case GenRuinsMap: text = "RR" case GenBSPMap: text = "DT" } return text } func (dg dungen) Description() (text string) { switch dg { case GenCaveMap: text = "open cave" case GenRoomMap: text = "big rooms" case GenCellularAutomataCaveMap: text = "eight cave" case GenCaveMapTree: text = "tree-like cave" case GenRuinsMap: text = "ruined rooms" case GenBSPMap: text = "deserted town" } return text } type room struct { pos position w int h int } func (d *dungeon) Cell(pos position) cell { return d.Cells[pos.idx()] } func (d *dungeon) Border(pos position) bool { return pos.X == DungeonWidth-1 || pos.Y == DungeonHeight-1 || pos.X == 0 || pos.Y == 0 } func (d *dungeon) SetCell(pos position, t terrain) { d.Cells[pos.idx()].T = t } func (d *dungeon) SetExplored(pos position) { d.Cells[pos.idx()].Explored = true } func roomDistance(r1, r2 room) int { return Abs(r1.pos.X-r2.pos.X) + Abs(r1.pos.Y-r2.pos.Y) } func nearRoom(rooms []room, r room) room { closest := rooms[0] d := roomDistance(r, closest) for _, nextRoom := range rooms { nd := roomDistance(r, nextRoom) if nd < d { n := RandInt(10) if n > 3 { d = nd closest = nextRoom } } } return closest } func nearestRoom(rooms []room, r room) room { closest := rooms[0] d := roomDistance(r, closest) for _, nextRoom := range rooms { nd := roomDistance(r, nextRoom) if nd < d { n := RandInt(10) if n > 0 { d = nd closest = nextRoom } } } return closest } func intersectsRoom(rooms []room, r room) bool { for _, rr := range rooms { if (r.pos.X+r.w-1 >= rr.pos.X && rr.pos.X+rr.w-1 >= r.pos.X) && (r.pos.Y+r.h-1 >= rr.pos.Y && rr.pos.Y+rr.h-1 >= r.pos.Y) { return true } } return false } func (d *dungeon) connectRooms(r1, r2 room) { x := r1.pos.X if x < r2.pos.X { x += r1.w - 1 } y := r1.pos.Y if y < r2.pos.Y { y += r1.h - 1 } d.SetCell(position{x, y}, FreeCell) count := 0 for { count++ if count > 1000 { panic("ConnectRooms") } if x < r2.pos.X { x++ d.SetCell(position{x, y}, FreeCell) continue } if x > r2.pos.X { x-- d.SetCell(position{x, y}, FreeCell) continue } if y < r2.pos.Y { y++ d.SetCell(position{x, y}, FreeCell) continue } if y > r2.pos.Y { y-- d.SetCell(position{x, y}, FreeCell) continue } break } d.SetCell(r2.pos, FreeCell) } func (d *dungeon) connectRoomsDiagonally(r1, r2 room) { x := r1.pos.X if x < r2.pos.X { x += r1.w - 1 } y := r1.pos.Y if y < r2.pos.Y { y += r1.h - 1 } d.SetCell(position{x, y}, FreeCell) count := 0 for { count++ if count > 1000 { panic("ConnectRooms") } if x < r2.pos.X && y < r2.pos.Y { x++ d.SetCell(position{x, y}, FreeCell) y++ d.SetCell(position{x, y}, FreeCell) continue } if x > r2.pos.X && y < r2.pos.Y { x-- d.SetCell(position{x, y}, FreeCell) y++ d.SetCell(position{x, y}, FreeCell) continue } if x > r2.pos.X && y > r2.pos.Y { x-- d.SetCell(position{x, y}, FreeCell) y-- d.SetCell(position{x, y}, FreeCell) continue } if x < r2.pos.X && y > r2.pos.Y { x++ d.SetCell(position{x, y}, FreeCell) y-- d.SetCell(position{x, y}, FreeCell) continue } if x < r2.pos.X { x++ d.SetCell(position{x, y}, FreeCell) continue } if x > r2.pos.X { x-- d.SetCell(position{x, y}, FreeCell) continue } if y < r2.pos.Y { y++ d.SetCell(position{x, y}, FreeCell) continue } if y > r2.pos.Y { y-- d.SetCell(position{x, y}, FreeCell) continue } break } d.SetCell(r2.pos, FreeCell) } func (d *dungeon) Area(area []position, pos position, radius int) []position { area = area[:0] for x := pos.X - radius; x <= pos.X+radius; x++ { for y := pos.Y - radius; y <= pos.Y+radius; y++ { pos := position{x, y} if pos.valid() { area = append(area, pos) } } } return area } func (d *dungeon) ConnectRoomsShortestPath(r1, r2 room) { var r1pos, r2pos position r1pos.X = r1.pos.X + RandInt(r1.w) if r1pos.X < r2.pos.X { r1pos.X = r1.pos.X + r1.w - 1 } r1pos.Y = r1.pos.Y + RandInt(r1.h) if r1pos.Y < r2.pos.Y { r1pos.Y = r1.pos.Y + r1.h - 1 } r2pos.X = r2.pos.X + RandInt(r2.w) if r2pos.X < r1.pos.X { r2pos.X = r2.pos.X + r2.w - 1 } r2pos.Y = r2.pos.Y + RandInt(r2.h) if r2pos.Y < r1.pos.Y { r2pos.Y = r2.pos.Y + r2.h - 1 } mp := &dungeonPath{dungeon: d} path, _, _ := AstarPath(mp, r1pos, r2pos) for _, pos := range path { d.SetCell(pos, FreeCell) } } func (d *dungeon) ConnectIsolatedRoom(doorpos position) { for i := 0; i < 200; i++ { pos := d.FreeCell() dp := &dungeonPath{dungeon: d, wcost: unreachable} path, _, _ := AstarPath(dp, pos, doorpos) wall := false for _, pos := range path { if d.Cell(pos).T == WallCell { wall = true break } } if !wall { continue } for _, pos := range path { d.SetCell(pos, FreeCell) } break } } func (d *dungeon) DigRoom(r room) { for i := r.pos.X; i < r.pos.X+r.w; i++ { for j := r.pos.Y; j < r.pos.Y+r.h; j++ { rpos := position{i, j} if rpos.valid() { d.SetCell(rpos, FreeCell) } } } } func (d *dungeon) PutCols(r room) { for i := r.pos.X + 1; i < r.pos.X+r.w-1; i += 2 { for j := r.pos.Y + 1; j < r.pos.Y+r.h-1; j += 2 { rpos := position{i, j} if rpos.valid() { d.SetCell(rpos, WallCell) } } } } func (d *dungeon) PutDiagCols(r room) { n := RandInt(2) for i := r.pos.X + 1; i < r.pos.X+r.w-1; i++ { m := n for j := r.pos.Y + 1; j < r.pos.Y+r.h-1; j++ { rpos := position{i, j} if rpos.valid() && m%2 == 0 { d.SetCell(rpos, WallCell) } m++ } n++ } } func (d *dungeon) IsAreaFree(pos position, h, w int) bool { for i := pos.X; i < pos.X+w; i++ { for j := pos.Y; j < pos.Y+h; j++ { rpos := position{i, j} if !rpos.valid() || d.Cell(rpos).T != FreeCell { return false } } } return true } func (d *dungeon) RoomDigCanditate(pos position, h, w int) (ret bool) { for i := pos.X; i < pos.X+w; i++ { for j := pos.Y; j < pos.Y+h; j++ { rpos := position{i, j} if !rpos.valid() { return false } if d.Cell(rpos).T == FreeCell { ret = true } } } return ret } func (d *dungeon) IsolatedRoomDigCanditate(pos position, h, w int) (ret bool) { for i := pos.X; i < pos.X+w; i++ { for j := pos.Y; j < pos.Y+h; j++ { rpos := position{i, j} if !rpos.valid() { return false } if d.Cell(rpos).T == FreeCell { return false } } } return true } func (d *dungeon) DigArea(pos position, h, w int) { for i := pos.X; i < pos.X+w; i++ { for j := pos.Y; j < pos.Y+h; j++ { rpos := position{i, j} if !rpos.valid() { continue } d.SetCell(rpos, FreeCell) } } } func (d *dungeon) BlockArea(pos position, h, w int) { // not used now for i := pos.X; i < pos.X+w; i++ { for j := pos.Y; j < pos.Y+h; j++ { rpos := position{i, j} if !rpos.valid() { continue } d.SetCell(rpos, WallCell) } } } func (d *dungeon) BuildRoom(pos position, w, h int, outside bool) map[position]bool { spos := position{pos.X - 1, pos.Y - 1} if outside && !d.IsAreaFree(spos, h+2, w+2) { return nil } for i := pos.X; i < pos.X+w; i++ { d.SetCell(position{i, pos.Y}, WallCell) d.SetCell(position{i, pos.Y + h - 1}, WallCell) } for i := pos.Y; i < pos.Y+h; i++ { d.SetCell(position{pos.X, i}, WallCell) d.SetCell(position{pos.X + w - 1, i}, WallCell) } if RandInt(2) == 0 || !outside { n := RandInt(2) for x := pos.X + 1; x < pos.X+w-1; x++ { m := n for y := pos.Y + 1; y < pos.Y+h-1; y++ { if m%2 == 0 { d.SetCell(position{x, y}, WallCell) } m++ } n++ } } else { n := RandInt(2) m := RandInt(2) //if n == 0 && m == 0 { //// round room //d.SetCell(pos, FreeCell) //d.SetCell(position{pos.X, pos.Y + h - 1}, FreeCell) //d.SetCell(position{pos.X + w - 1, pos.Y}, FreeCell) //d.SetCell(position{pos.X + w - 1, pos.Y + h - 1}, FreeCell) //} for x := pos.X + 1 + m; x < pos.X+w-1; x += 2 { for y := pos.Y + 1 + n; y < pos.Y+h-1; y += 2 { d.SetCell(position{x, y}, WallCell) } } } area := make([]position, 9) if outside { for _, p := range [4]position{pos, {pos.X, pos.Y + h - 1}, {pos.X + w - 1, pos.Y}, {pos.X + w - 1, pos.Y + h - 1}} { if d.WallAreaCount(area, p, 1) == 4 { d.SetCell(p, FreeCell) } } } doorsc := [4]position{ position{pos.X + w/2, pos.Y}, position{pos.X + w/2, pos.Y + h - 1}, position{pos.X, pos.Y + h/2}, position{pos.X + w - 1, pos.Y + h/2}, } doors := make(map[position]bool) for i := 0; i < 3+RandInt(2); i++ { dpos := doorsc[RandInt(4)] doors[dpos] = true d.SetCell(dpos, FreeCell) } return doors } func (d *dungeon) BuildSomeRoom(w, h int) map[position]bool { for i := 0; i < 200; i++ { pos := d.FreeCell() doors := d.BuildRoom(pos, w, h, true) if doors != nil { return doors } } return nil } func (d *dungeon) DigSomeRoom(w, h int) map[position]bool { for i := 0; i < 200; i++ { pos := d.FreeCell() dpos := position{pos.X - 1, pos.Y - 1} if !d.RoomDigCanditate(dpos, h+2, w+2) { continue } d.DigArea(dpos, h+2, w+2) doors := d.BuildRoom(pos, w, h, true) if doors != nil { return doors } } return nil } func (d *dungeon) DigIsolatedRoom(w, h int) map[position]bool { i := RandInt(DungeonNCells) for j := 0; j < DungeonNCells; j++ { i = (i + 1) % DungeonNCells pos := idxtopos(i) if d.Cells[i].T == FreeCell { continue } dpos := position{pos.X - 1, pos.Y - 1} if !d.IsolatedRoomDigCanditate(dpos, h+2, w+2) { continue } d.DigArea(pos, h, w) doors := d.BuildRoom(pos, w, h, false) if doors != nil { return doors } } return nil } func (d *dungeon) ResizeRoom(r room) room { if DungeonWidth-r.pos.X < r.w { r.w = DungeonWidth - r.pos.X } if DungeonHeight-r.pos.Y < r.h { r.h = DungeonHeight - r.pos.Y } return r } func (g *game) GenRuinsMap(h, w int) { d := &dungeon{} d.Cells = make([]cell, h*w) rooms := []room{} for i := 0; i < 43; i++ { var ro room count := 100 for count > 0 { count-- ro = room{ pos: position{RandInt(w - 1), RandInt(h - 1)}, w: 3 + RandInt(5), h: 2 + RandInt(3)} ro = d.ResizeRoom(ro) if !intersectsRoom(rooms, ro) { break } } d.DigRoom(ro) if RandInt(60) == 0 { if RandInt(2) == 0 { d.PutCols(ro) } else { d.PutDiagCols(ro) } } if len(rooms) > 0 { r := RandInt(100) if r > 75 { d.connectRooms(nearRoom(rooms, ro), ro) } else if r > 25 { d.ConnectRoomsShortestPath(nearRoom(rooms, ro), ro) } else { d.connectRoomsDiagonally(nearRoom(rooms, ro), ro) } } rooms = append(rooms, ro) } doors := d.DigSomeRooms(5) g.Dungeon = d g.Fungus = make(map[position]vegetation) g.DigFungus(1 + RandInt(2)) g.PutDoors(30) g.PutDoorsList(doors, 20) } func (g *game) DigFungus(n int) { d := g.Dungeon count := 0 fungus := g.Foliage(DungeonHeight, DungeonWidth) loop: for i := 0; i < 100; i++ { if count > 100 { break loop } if n <= 0 { break } pos := d.FreeCell() if _, ok := fungus[pos]; ok { continue } conn, count := d.Connected(pos, func(npos position) bool { _, ok := fungus[npos] //return ok && d.IsFreeCell(npos) return ok }) if count < 3 { continue } if len(conn) > 150 { continue } for cpos := range conn { d.SetCell(cpos, FreeCell) g.Fungus[cpos] = foliage count++ } n-- } } type roomSlice []room func (rs roomSlice) Len() int { return len(rs) } func (rs roomSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } func (rs roomSlice) Less(i, j int) bool { return rs[i].pos.Y < rs[j].pos.Y || rs[i].pos.Y == rs[j].pos.Y && rs[i].pos.X < rs[j].pos.X } func (g *game) GenRoomMap(h, w int) { d := &dungeon{} d.Cells = make([]cell, h*w) rooms := []room{} cols := 0 for i := 0; i < 35; i++ { var ro room count := 100 for count > 0 { count-- ro = room{ pos: position{RandInt(w - 1), RandInt(h - 1)}, w: 5 + RandInt(4), h: 3 + RandInt(3)} ro = d.ResizeRoom(ro) if !intersectsRoom(rooms, ro) { break } } d.DigRoom(ro) if RandInt(10+15*cols) == 0 { if RandInt(2) == 0 { d.PutCols(ro) } else { d.PutDiagCols(ro) } cols++ } rooms = append(rooms, ro) } sort.Sort(roomSlice(rooms)) for i, ro := range rooms { if i == 0 { continue } r := RandInt(100) if r > 50 { d.connectRooms(nearestRoom(rooms[:i], ro), ro) } else if r > 25 { d.ConnectRoomsShortestPath(nearRoom(rooms[:i], ro), ro) } else { d.connectRoomsDiagonally(nearestRoom(rooms[:i], ro), ro) } } g.Dungeon = d doors := d.DigSomeRooms(5) g.PutDoors(90) g.PutDoorsList(doors, 10) } func (g *game) PutDoorsList(doors map[position]bool, threshold int) { for pos := range doors { if g.DoorCandidate(pos) && RandInt(100) > threshold { g.Doors[pos] = true if _, ok := g.Fungus[pos]; ok { delete(g.Fungus, pos) } } } } func (d *dungeon) FreeCell() position { count := 0 for { count++ if count > 1000 { panic("FreeCell") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T == FreeCell { return pos } } } func (d *dungeon) WallCell() position { count := 0 for { count++ if count > 1000 { panic("WallCell") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T == WallCell { return pos } } } func (g *game) GenCaveMap(h, w int) { d := &dungeon{} d.Cells = make([]cell, h*w) pos := position{40, 10} max := 21 * 42 d.SetCell(pos, FreeCell) cells := 1 notValid := 0 lastValid := pos diag := RandInt(4) == 0 for cells < max { npos := pos.RandomNeighbor(diag) if !pos.valid() && npos.valid() && d.Cell(npos).T == WallCell { pos = lastValid continue } pos = npos if pos.valid() { if d.Cell(pos).T != FreeCell { d.SetCell(pos, FreeCell) cells++ } lastValid = pos } else { notValid++ } if notValid > 200 { notValid = 0 pos = lastValid } } cells = 1 max = DungeonHeight * 1 digs := 0 i := 0 block := make([]position, 0, 64) loop: for cells < max { i++ if digs > 3 { break } if i > 1000 { break } diag = RandInt(2) == 0 block = d.DigBlock(block, diag) if len(block) == 0 { continue loop } if len(block) < 4 || len(block) > 10 { continue loop } for _, pos := range block { d.SetCell(pos, FreeCell) cells++ } digs++ } doors := make(map[position]bool) rooms := 0 if RandInt(4) > 0 { w, h := GenCaveRoomSize() rooms++ for pos := range d.BuildSomeRoom(w, h) { doors[pos] = true } if RandInt(7) == 0 { rooms++ w, h := GenCaveRoomSize() for pos := range d.BuildSomeRoom(w, h) { doors[pos] = true } } } if RandInt(1+rooms) == 0 { w, h := GenLittleRoomSize() i := 0 for pos := range d.DigIsolatedRoom(w, h) { doors[pos] = true if i == 0 { d.ConnectIsolatedRoom(pos) } i++ } } g.Dungeon = d g.Fungus = g.Foliage(DungeonHeight, DungeonWidth) g.PutDoors(5) for pos := range doors { if g.DoorCandidate(pos) && RandInt(100) > 20 { g.Doors[pos] = true if _, ok := g.Fungus[pos]; ok { delete(g.Fungus, pos) } } } } func GenCaveRoomSize() (int, int) { return 7 + 2*RandInt(2), 5 + 2*RandInt(2) } func GenLittleRoomSize() (int, int) { return 7, 5 } func (d *dungeon) HasFreeNeighbor(pos position) bool { neighbors := pos.ValidNeighbors() for _, pos := range neighbors { if d.Cell(pos).T == FreeCell { return true } } return false } func (g *game) HasFreeExploredNeighbor(pos position) bool { d := g.Dungeon neighbors := pos.ValidNeighbors() for _, pos := range neighbors { c := d.Cell(pos) if c.T == FreeCell && c.Explored && !g.WrongWall[pos] { return true } } return false } func (d *dungeon) DigBlock(block []position, diag bool) []position { pos := d.WallCell() block = block[:0] for { block = append(block, pos) if d.HasFreeNeighbor(pos) { break } pos = pos.RandomNeighbor(diag) if !pos.valid() { block = block[:0] pos = d.WallCell() continue } if !pos.valid() { return nil } } return block } func (g *game) GenCaveMapTree(h, w int) { d := &dungeon{} d.Cells = make([]cell, h*w) center := position{40, 10} d.SetCell(center, FreeCell) d.SetCell(center.E(), FreeCell) d.SetCell(center.NE(), FreeCell) d.SetCell(center.S(), FreeCell) d.SetCell(center.SE(), FreeCell) d.SetCell(center.N(), FreeCell) d.SetCell(center.NW(), FreeCell) d.SetCell(center.W(), FreeCell) d.SetCell(center.SW(), FreeCell) max := 21 * 23 cells := 1 diag := RandInt(2) == 0 block := make([]position, 0, 64) loop: for cells < max { block = d.DigBlock(block, diag) if len(block) == 0 { continue loop } for _, pos := range block { if d.Cell(pos).T != FreeCell { d.SetCell(pos, FreeCell) cells++ } } } doors := d.DigSomeRooms(5) g.Dungeon = d g.Fungus = make(map[position]vegetation) g.DigFungus(1 + RandInt(2)) g.PutDoors(5) g.PutDoorsList(doors, 20) } func (d *dungeon) DigSomeRooms(chances int) map[position]bool { doors := make(map[position]bool) if RandInt(chances) > 0 { w, h := GenCaveRoomSize() for pos := range d.DigSomeRoom(w, h) { doors[pos] = true } if RandInt(3) == 0 { w, h := GenCaveRoomSize() for pos := range d.DigSomeRoom(w, h) { doors[pos] = true } } } return doors } func (d *dungeon) WallAreaCount(area []position, pos position, radius int) int { area = d.Area(area, pos, radius) count := 0 for _, npos := range area { if d.Cell(npos).T == WallCell { count++ } } switch radius { case 1: count += 9 - len(area) case 2: count += 25 - len(area) } return count } func (d *dungeon) Connected(pos position, nf func(position) bool) (map[position]bool, int) { conn := map[position]bool{} stack := []position{pos} count := 0 conn[pos] = true nb := make([]position, 0, 8) for len(stack) > 0 { pos = stack[len(stack)-1] stack = stack[:len(stack)-1] count++ nb = pos.Neighbors(nb, nf) for _, npos := range nb { if !conn[npos] { conn[npos] = true stack = append(stack, npos) } } } return conn, count } func (d *dungeon) connex() bool { pos := d.FreeCell() conn, _ := d.Connected(pos, d.IsFreeCell) for i, c := range d.Cells { if c.T == FreeCell && !conn[idxtopos(i)] { return false } } return true } func (g *game) RunCellularAutomataCave(h, w int) bool { d := &dungeon{} d.Cells = make([]cell, h*w) for i := range d.Cells { r := RandInt(100) pos := idxtopos(i) if r >= 45 { d.SetCell(pos, FreeCell) } else { d.SetCell(pos, WallCell) } } bufm := &dungeon{} bufm.Cells = make([]cell, h*w) area := make([]position, 0, 25) for i := 0; i < 5; i++ { for j := range bufm.Cells { pos := idxtopos(j) c1 := d.WallAreaCount(area, pos, 1) if c1 >= 5 { bufm.SetCell(pos, WallCell) } else { bufm.SetCell(pos, FreeCell) } if i == 3 { c2 := d.WallAreaCount(area, pos, 2) if c2 <= 2 { bufm.SetCell(pos, WallCell) } } } copy(d.Cells, bufm.Cells) } var conn map[position]bool var count int var winner position for i := 0; i < 15; i++ { pos := d.FreeCell() if conn[pos] { continue } var ncount int conn, ncount = d.Connected(pos, d.IsFreeCell) if ncount > count { count = ncount winner = pos } if count >= 37*DungeonHeight*DungeonWidth/100 { break } } conn, count = d.Connected(winner, d.IsFreeCell) if count <= 37*DungeonHeight*DungeonWidth/100 { return false } for i, c := range d.Cells { pos := idxtopos(i) if c.T == FreeCell && !conn[pos] { d.SetCell(pos, WallCell) } } digs := 0 max := DungeonHeight / 2 cells := 1 i := 0 block := make([]position, 0, 64) loop: for cells < max { i++ if digs > 3 { break } if i > 1000 { break } diag := RandInt(2) == 0 block = d.DigBlock(block, diag) if len(block) == 0 { continue loop } if len(block) < 4 || len(block) > 10 { continue loop } for _, pos := range block { d.SetCell(pos, FreeCell) cells++ } digs++ } doors := make(map[position]bool) if RandInt(5) > 0 { w, h := GenLittleRoomSize() i := 0 for pos := range d.DigIsolatedRoom(w, h) { doors[pos] = true if i == 0 { d.ConnectIsolatedRoom(pos) } i++ } if RandInt(4) == 0 { w, h := GenCaveRoomSize() i := 0 for pos := range d.DigIsolatedRoom(w, h) { doors[pos] = true if i == 0 { d.ConnectIsolatedRoom(pos) } i++ } } } g.Dungeon = d g.PutDoors(10) for pos := range doors { if g.DoorCandidate(pos) && RandInt(100) > 20 { g.Doors[pos] = true if _, ok := g.Fungus[pos]; ok { delete(g.Fungus, pos) } } } return true } func (g *game) GenCellularAutomataCaveMap(h, w int) { count := 0 for { count++ if count > 100 { panic("genCellularAutomataCaveMap") } if g.RunCellularAutomataCave(h, w) { break } } g.Fungus = g.Foliage(DungeonHeight, DungeonWidth) } func (d *dungeon) SimpleRoom(r room) map[position]bool { for i := r.pos.X; i < r.pos.X+r.w; i++ { d.SetCell(position{i, r.pos.Y}, WallCell) d.SetCell(position{i, r.pos.Y + r.h - 1}, WallCell) } for i := r.pos.Y; i < r.pos.Y+r.h; i++ { d.SetCell(position{r.pos.X, i}, WallCell) d.SetCell(position{r.pos.X + r.w - 1, i}, WallCell) } doorsc := [4]position{ position{r.pos.X + r.w/2, r.pos.Y}, position{r.pos.X + r.w/2, r.pos.Y + r.h - 1}, position{r.pos.X, r.pos.Y + r.h/2}, position{r.pos.X + r.w - 1, r.pos.Y + r.h/2}, } doors := make(map[position]bool) for i := 0; i < 3+RandInt(2); i++ { dpos := doorsc[RandInt(4)] doors[dpos] = true d.SetCell(dpos, FreeCell) } return doors } func (g *game) ExtendEdgeRoom(r room, doors map[position]bool) room { if g.Dungeon.Cell(r.pos).T != WallCell { return r } extend := false if r.pos.X+r.w+1 == DungeonWidth { for i := r.pos.Y + 1; i < r.pos.Y+r.h-1; i++ { g.Dungeon.SetCell(position{DungeonWidth - 2, i}, FreeCell) g.Dungeon.SetCell(position{DungeonWidth - 1, i}, WallCell) } g.Dungeon.SetCell(position{DungeonWidth - 1, r.pos.Y}, WallCell) g.Dungeon.SetCell(position{DungeonWidth - 1, r.pos.Y + r.h - 1}, WallCell) g.Dungeon.SetCell(position{DungeonWidth - 2, r.pos.Y + 1}, WallCell) g.Dungeon.SetCell(position{DungeonWidth - 2, r.pos.Y + r.h - 2}, WallCell) r.w++ extend = true } if r.pos.X == 1 { for i := r.pos.Y + 1; i < r.pos.Y+r.h-1; i++ { g.Dungeon.SetCell(position{1, i}, FreeCell) g.Dungeon.SetCell(position{0, i}, WallCell) } g.Dungeon.SetCell(position{0, r.pos.Y}, WallCell) g.Dungeon.SetCell(position{0, r.pos.Y + r.h - 1}, WallCell) g.Dungeon.SetCell(position{1, r.pos.Y + 1}, WallCell) g.Dungeon.SetCell(position{1, r.pos.Y + r.h - 2}, WallCell) r.w++ r.pos.X-- extend = true } if r.pos.Y+r.h+1 == DungeonHeight { for i := r.pos.X + 1; i < r.pos.X+r.w-1; i++ { g.Dungeon.SetCell(position{i, DungeonHeight - 2}, FreeCell) g.Dungeon.SetCell(position{i, DungeonHeight - 1}, WallCell) } g.Dungeon.SetCell(position{r.pos.X, DungeonHeight - 1}, WallCell) g.Dungeon.SetCell(position{r.pos.X + r.w - 1, DungeonHeight - 1}, WallCell) g.Dungeon.SetCell(position{r.pos.X + 1, DungeonHeight - 2}, WallCell) g.Dungeon.SetCell(position{r.pos.X + r.w - 2, DungeonHeight - 2}, WallCell) r.h++ extend = true } if r.pos.Y == 1 { for i := r.pos.X + 1; i < r.pos.X+r.w-1; i++ { g.Dungeon.SetCell(position{i, 1}, FreeCell) g.Dungeon.SetCell(position{i, 0}, WallCell) } g.Dungeon.SetCell(position{r.pos.X, 0}, WallCell) g.Dungeon.SetCell(position{r.pos.X + r.w - 1, 0}, WallCell) g.Dungeon.SetCell(position{r.pos.X + 1, 1}, WallCell) g.Dungeon.SetCell(position{r.pos.X + r.w - 2, 1}, WallCell) r.h++ r.pos.Y-- extend = true } if !extend { return r } for pos := range doors { if pos.X == 1 || pos.X == DungeonWidth-2 || pos.Y == 1 || pos.Y == DungeonHeight-2 { delete(g.Doors, pos) continue } } doorsc := [4]position{ position{r.pos.X + r.w/2, r.pos.Y}, position{r.pos.X + r.w/2, r.pos.Y + r.h - 1}, position{r.pos.X, r.pos.Y + r.h/2}, position{r.pos.X + r.w - 1, r.pos.Y + r.h/2}, } ndoorsc := []position{} ndoors := 0 for _, pos := range doorsc { if pos.X == 0 || pos.X == DungeonWidth-1 || pos.Y == 0 || pos.Y == DungeonHeight-1 { continue } if g.Doors[pos] { ndoors++ } ndoorsc = append(ndoorsc, pos) } for i := 0; i < 1+RandInt(2-ndoors); i++ { dpos := ndoorsc[RandInt(len(ndoorsc))] g.Doors[dpos] = true g.Dungeon.SetCell(dpos, FreeCell) } return r } func (g *game) DivideRoomVertically(r room) { if g.Dungeon.Cell(r.pos).T != WallCell { return } if r.w <= 6 { return } if r.h < 5 { return } dx := 2 + RandInt(r.w/2-2) if RandInt(2) == 0 { dx = r.w - 3 - RandInt(r.w/2-3) } if dx == 2 && r.pos.X == 0 { return } if dx == r.w-3 && r.pos.X+r.w == DungeonWidth { return } free := true loop: for i := r.pos.Y + 1; i < r.pos.Y+r.h-1; i++ { for j := dx - 1; j <= dx+1; j++ { if g.Dungeon.Cell(position{r.pos.X + j, i}).T == WallCell { free = false break loop } } } if !free { return } for i := r.pos.Y + 1; i < r.pos.Y+r.h-1; i++ { g.Dungeon.SetCell(position{r.pos.X + dx, i}, WallCell) } doorpos := position{r.pos.X + dx, r.pos.Y + r.h/2} g.Doors[doorpos] = true g.Dungeon.SetCell(doorpos, FreeCell) } func (g *game) DivideRoomHorizontally(r room) { if g.Dungeon.Cell(r.pos).T != WallCell { return } if r.h <= 6 { return } if r.w < 5 { return } dy := 2 + RandInt(r.h/2-2) if RandInt(2) == 0 { dy = r.h - 3 - RandInt(r.h/2-3) } if dy == 2 && r.pos.Y == 0 { return } if dy == r.h-3 && r.pos.Y+r.h == DungeonHeight { return } free := true loop: for i := r.pos.X + 1; i < r.pos.X+r.w-1; i++ { for j := dy - 1; j <= dy+1; j++ { if g.Dungeon.Cell(position{i, r.pos.Y + j}).T == WallCell { free = false break loop } } } if !free { return } for i := r.pos.X + 1; i < r.pos.X+r.w-1; i++ { g.Dungeon.SetCell(position{i, r.pos.Y + dy}, WallCell) } doorpos := position{r.pos.X + r.w/2, r.pos.Y + dy} g.Doors[doorpos] = true g.Dungeon.SetCell(doorpos, FreeCell) } func (g *game) GenBSPMap(height, width int) { rooms := []room{} crooms := []room{{pos: position{1, 1}, w: DungeonWidth - 2, h: DungeonHeight - 2}} big := 0 for len(crooms) > 0 { r := crooms[0] crooms = crooms[1:] if r.h <= 8 && r.w <= 12 { switch RandInt(6) { case 0: if r.h >= 6 { r.h-- if RandInt(2) == 0 { r.pos.Y++ } } case 1: if r.w >= 8 { r.w-- if RandInt(2) == 0 { r.pos.X++ } } } if r.h > 2 && r.w > 2 { rooms = append(rooms, r) } continue } if RandInt(2+big) == 0 && (r.h <= 12 && r.w <= 20) { big++ switch RandInt(4) { case 0: r.h-- if RandInt(2) == 0 { r.pos.Y++ } case 1: r.w-- if RandInt(2) == 0 { r.pos.X++ } } if r.h > 2 && r.w > 2 { rooms = append(rooms, r) } continue } horizontal := false if r.h > 8 && r.w > 10 && r.w < 40 && RandInt(4) == 0 { horizontal = true } else if r.h > 8 && r.w <= 10+RandInt(5) { horizontal = true } if horizontal { h := r.h/2 - r.h/4 + RandInt(1+r.h/2) if h <= 3 { h++ } if r.h-h-1 <= 3 { h-- } crooms = append(crooms, room{r.pos, r.w, h}, room{position{r.pos.X, r.pos.Y + 1 + h}, r.w, r.h - h - 1}) } else { w := r.w/2 - r.w/4 + RandInt(1+r.w/2) if w <= 3 { w++ } if r.w-w-1 <= 3 { w-- } crooms = append(crooms, room{r.pos, w, r.h}, room{position{r.pos.X + 1 + w, r.pos.Y}, r.w - w - 1, r.h}) } } d := &dungeon{} d.Cells = make([]cell, height*width) for i := 0; i < DungeonNCells; i++ { d.SetCell(idxtopos(i), FreeCell) } g.Dungeon = d g.Doors = map[position]bool{} special := 0 empty := 0 for i, r := range rooms { var doors map[position]bool if RandInt(2+special/3) == 0 && r.w%2 == 1 && r.h%2 == 1 && r.w >= 5 && r.h >= 5 { doors = d.BuildRoom(r.pos, r.w, r.h, true) special++ } else if empty > 0 || RandInt(20) > 0 { doors = d.SimpleRoom(r) if RandInt(2) == 0 && r.w >= 7 && r.h >= 7 { rn := r rn.pos.X++ rn.pos.Y++ rn.h-- rn.h-- rn.w-- rn.w-- if RandInt(2) == 0 { d.PutCols(rn) } else { d.PutDiagCols(rn) } } else if RandInt(1+special/2) == 0 && r.w >= 11 && r.h >= 9 { sx := (r.w - 11) / 2 sy := (r.h - 9) / 2 doors = d.BuildRoom(position{r.pos.X + 2 + sx, r.pos.Y + 2 + sy}, 7, 5, true) special++ } } else { empty++ } for pos := range doors { if g.DoorCandidate(pos) && RandInt(100) > 10 { g.Doors[pos] = true } } if RandInt(2) == 0 { r = g.ExtendEdgeRoom(r, doors) rooms[i] = r } if RandInt(5) > 0 { if RandInt(2) == 0 { g.DivideRoomVertically(r) } else { g.DivideRoomHorizontally(r) } } } g.Fungus = make(map[position]vegetation) g.DigFungus(RandInt(3)) for i := 0; i <= RandInt(2); i++ { r := rooms[RandInt(len(rooms))] for x := r.pos.X + 1; x < r.pos.X+r.w-1; x++ { for y := r.pos.Y + 1; y < r.pos.Y+r.h-1; y++ { g.Fungus[position{x, y}] = foliage } } } } type vegetation int const ( foliage vegetation = iota ) func (g *game) Foliage(h, w int) map[position]vegetation { // use same structure as for the dungeon // walls will become foliage d := &dungeon{} d.Cells = make([]cell, h*w) for i := range d.Cells { r := RandInt(100) pos := idxtopos(i) if r >= 43 { d.SetCell(pos, WallCell) } else { d.SetCell(pos, FreeCell) } } area := make([]position, 0, 25) for i := 0; i < 6; i++ { bufm := &dungeon{} bufm.Cells = make([]cell, h*w) copy(bufm.Cells, d.Cells) for j := range bufm.Cells { pos := idxtopos(j) c1 := d.WallAreaCount(area, pos, 1) if i < 4 { if c1 <= 4 { bufm.SetCell(pos, FreeCell) } else { bufm.SetCell(pos, WallCell) } } if i == 4 { if c1 > 6 { bufm.SetCell(pos, WallCell) } } if i == 5 { c2 := d.WallAreaCount(area, pos, 2) if c2 < 5 && c1 <= 2 { bufm.SetCell(pos, FreeCell) } } } d.Cells = bufm.Cells } fungus := make(map[position]vegetation) for i, c := range d.Cells { if _, ok := g.Doors[idxtopos(i)]; !ok && c.T == FreeCell { fungus[idxtopos(i)] = foliage } } return fungus } func (g *game) DoorCandidate(pos position) bool { d := g.Dungeon if !pos.valid() || d.Cell(pos).T != FreeCell { return false } return pos.W().valid() && pos.E().valid() && d.Cell(pos.W()).T == FreeCell && d.Cell(pos.E()).T == FreeCell && !g.Doors[pos.W()] && !g.Doors[pos.E()] && (!pos.N().valid() || d.Cell(pos.N()).T == WallCell) && (!pos.S().valid() || d.Cell(pos.S()).T == WallCell) && ((pos.NW().valid() && d.Cell(pos.NW()).T == FreeCell) || (pos.SW().valid() && d.Cell(pos.SW()).T == FreeCell) || (pos.NE().valid() && d.Cell(pos.NE()).T == FreeCell) || (pos.SE().valid() && d.Cell(pos.SE()).T == FreeCell)) || pos.N().valid() && pos.S().valid() && d.Cell(pos.N()).T == FreeCell && d.Cell(pos.S()).T == FreeCell && !g.Doors[pos.N()] && !g.Doors[pos.S()] && (!pos.E().valid() || d.Cell(pos.E()).T == WallCell) && (!pos.W().valid() || d.Cell(pos.W()).T == WallCell) && ((pos.NW().valid() && d.Cell(pos.NW()).T == FreeCell) || (pos.SW().valid() && d.Cell(pos.SW()).T == FreeCell) || (pos.NE().valid() && d.Cell(pos.NE()).T == FreeCell) || (pos.SE().valid() && d.Cell(pos.SE()).T == FreeCell)) } func (g *game) PutDoors(percentage int) { g.Doors = map[position]bool{} for i := range g.Dungeon.Cells { pos := idxtopos(i) if g.DoorCandidate(pos) && RandInt(100) < percentage { g.Doors[pos] = true if _, ok := g.Fungus[pos]; ok { delete(g.Fungus, pos) } } } } boohu-0.13.0/dungeon_test.go000066400000000000000000000034201356500202200157260ustar00rootroot00000000000000package main import ( "bytes" "fmt" "testing" ) var Rounds = 100 func BenchmarkCellularAutomataCaveMap(b *testing.B) { for i := 0; i < b.N; i++ { g := &game{} g.GenCellularAutomataCaveMap(DungeonHeight, DungeonWidth) } } func TestCellularAutomataCaveMap(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenCellularAutomataCaveMap(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } func TestCaveMap(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenCaveMap(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } func TestCaveMapTree(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenCaveMapTree(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } func TestRuinsMap(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenRuinsMap(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } func TestBSPMap(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenBSPMap(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } func (d *dungeon) String() string { b := &bytes.Buffer{} for i, c := range d.Cells { if i > 0 && i%DungeonWidth == 0 { fmt.Fprint(b, "\n") } if c.T == WallCell { fmt.Fprint(b, "#") } else { fmt.Fprint(b, ".") } } return b.String() } func TestRoomMap(t *testing.T) { for i := 0; i < Rounds; i++ { g := &game{} g.GenRoomMap(DungeonHeight, DungeonWidth) if !g.Dungeon.connex() { t.Errorf("Not connex:\n%s\n", g.Dungeon.String()) } } } boohu-0.13.0/encoding.go000066400000000000000000000040121356500202200150140ustar00rootroot00000000000000package main import ( "bytes" "compress/zlib" "encoding/gob" ) func init() { gob.Register(potion(0)) gob.Register(projectile(0)) gob.Register(&simpleEvent{}) gob.Register(&monsterEvent{}) gob.Register(&cloudEvent{}) gob.Register(armour(0)) gob.Register(weapon(0)) gob.Register(shield(0)) } func (g *game) GameSave() ([]byte, error) { data := bytes.Buffer{} enc := gob.NewEncoder(&data) err := enc.Encode(g) if err != nil { return nil, err } var buf bytes.Buffer w := zlib.NewWriter(&buf) w.Write(data.Bytes()) w.Close() return buf.Bytes(), nil } type config struct { RuneNormalModeKeys map[rune]keyAction RuneTargetModeKeys map[rune]keyAction DarkLOS bool Small bool Tiles bool Version string } func (c *config) ConfigSave() ([]byte, error) { data := bytes.Buffer{} enc := gob.NewEncoder(&data) err := enc.Encode(c) if err != nil { return nil, err } return data.Bytes(), nil } func (g *game) DecodeGameSave(data []byte) (*game, error) { buf := bytes.NewReader(data) r, err := zlib.NewReader(buf) if err != nil { return nil, err } dec := gob.NewDecoder(r) lg := &game{} err = dec.Decode(lg) if err != nil { return nil, err } r.Close() return lg, nil } func (g *game) DecodeConfigSave(data []byte) (*config, error) { buf := bytes.NewBuffer(data) dec := gob.NewDecoder(buf) c := &config{} err := dec.Decode(c) if err != nil { return nil, err } return c, nil } func (g *game) EncodeDrawLog() ([]byte, error) { data := bytes.Buffer{} enc := gob.NewEncoder(&data) err := enc.Encode(&g.DrawLog) if err != nil { return nil, err } var buf bytes.Buffer w := zlib.NewWriter(&buf) w.Write(data.Bytes()) w.Close() return buf.Bytes(), nil } func (g *game) DecodeDrawLog(data []byte) ([]drawFrame, error) { buf := bytes.NewReader(data) r, err := zlib.NewReader(buf) if err != nil { return nil, err } dec := gob.NewDecoder(r) dl := []drawFrame{} err = dec.Decode(&dl) if err != nil { return nil, err } r.Close() return dl, nil } boohu-0.13.0/events.go000066400000000000000000000247551356500202200145520ustar00rootroot00000000000000package main import "container/heap" type event interface { Rank() int Action(*game) Renew(*game, int) } type iEvent struct { Event event Index int } type eventQueue []iEvent func (evq eventQueue) Len() int { return len(evq) } func (evq eventQueue) Less(i, j int) bool { return evq[i].Event.Rank() < evq[j].Event.Rank() || evq[i].Event.Rank() == evq[j].Event.Rank() && evq[i].Index < evq[j].Index } func (evq eventQueue) Swap(i, j int) { evq[i], evq[j] = evq[j], evq[i] } func (evq *eventQueue) Push(x interface{}) { no := x.(iEvent) *evq = append(*evq, no) } func (evq *eventQueue) Pop() interface{} { old := *evq n := len(old) no := old[n-1] *evq = old[0 : n-1] return no } type simpleAction int const ( PlayerTurn simpleAction = iota Teleportation BerserkEnd SlowEnd ExhaustionEnd HasteEnd EvasionEnd LignificationEnd ConfusionEnd NauseaEnd DisabledShieldEnd CorrosionEnd DigEnd SwapEnd ShadowsEnd SlayEnd AccurateEnd BlockEnd ) func (g *game) PushEvent(ev event) { iev := iEvent{Event: ev, Index: g.EventIndex} g.EventIndex++ heap.Push(g.Events, iev) } func (g *game) PushAgainEvent(ev event) { iev := iEvent{Event: ev, Index: 0} heap.Push(g.Events, iev) } func (g *game) PopIEvent() iEvent { iev := heap.Pop(g.Events).(iEvent) return iev } type simpleEvent struct { ERank int EAction simpleAction } func (sev *simpleEvent) Rank() int { return sev.ERank } func (sev *simpleEvent) Renew(g *game, delay int) { sev.ERank += delay if delay == 0 { g.PushAgainEvent(sev) } else { g.PushEvent(sev) } } func (sev *simpleEvent) Action(g *game) { switch sev.EAction { case PlayerTurn: g.ComputeNoise() g.LogNextTick = g.LogIndex g.AutoNext = g.AutoPlayer(sev) if g.AutoNext { g.TurnStats() return } g.Quit = g.ui.HandlePlayerTurn(sev) if g.Quit { return } g.TurnStats() case Teleportation: if !g.Player.HasStatus(StatusLignification) { g.Teleportation(sev) } else { g.Print("Lignification has prevented teleportation.") } g.Player.Statuses[StatusTele] = 0 case BerserkEnd: g.Player.Statuses[StatusBerserk] = 0 g.Player.Statuses[StatusSlow]++ g.Player.Statuses[StatusExhausted] = 1 g.Player.HP -= int(10 * g.Player.HP / Max(g.Player.HPMax(), g.Player.HP)) g.PrintStyled("You are no longer berserk.", logStatusEnd) g.PushEvent(&simpleEvent{ERank: sev.Rank() + 90 + RandInt(30), EAction: SlowEnd}) g.PushEvent(&simpleEvent{ERank: sev.Rank() + 270 + RandInt(60), EAction: ExhaustionEnd}) g.ui.StatusEndAnimation() case SlowEnd: g.Player.Statuses[StatusSlow]-- if g.Player.Statuses[StatusSlow] <= 0 { g.PrintStyled("You no longer feel slow.", logStatusEnd) g.ui.StatusEndAnimation() } case ExhaustionEnd: g.PrintStyled("You no longer feel exhausted.", logStatusEnd) g.Player.Statuses[StatusExhausted] = 0 g.ui.StatusEndAnimation() case HasteEnd: g.Player.Statuses[StatusSwift]-- if g.Player.Statuses[StatusSwift] == 0 { g.PrintStyled("You no longer feel speedy.", logStatusEnd) g.ui.StatusEndAnimation() } case EvasionEnd: g.Player.Statuses[StatusAgile]-- if g.Player.Statuses[StatusAgile] == 0 { g.PrintStyled("You no longer feel agile.", logStatusEnd) g.ui.StatusEndAnimation() } case LignificationEnd: g.Player.Statuses[StatusLignification]-- g.Player.HP -= int(10 * g.Player.HP / Max(g.Player.HPMax(), g.Player.HP)) if g.Player.Statuses[StatusLignification] == 0 { g.PrintStyled("You no longer feel attached to the ground.", logStatusEnd) g.ui.StatusEndAnimation() } case ConfusionEnd: g.PrintStyled("You no longer feel confused.", logStatusEnd) g.Player.Statuses[StatusConfusion] = 0 g.ui.StatusEndAnimation() case NauseaEnd: g.PrintStyled("You no longer feel sick.", logStatusEnd) g.Player.Statuses[StatusNausea] = 0 g.ui.StatusEndAnimation() case DisabledShieldEnd: g.PrintStyled("You manage to dislodge the projectile from your shield.", logStatusEnd) g.Player.Statuses[StatusDisabledShield] = 0 g.ui.StatusEndAnimation() case CorrosionEnd: g.Player.Statuses[StatusCorrosion]-- if g.Player.Statuses[StatusCorrosion] == 0 { g.PrintStyled("Your equipment is now free from acid.", logStatusEnd) g.ui.StatusEndAnimation() } case DigEnd: g.Player.Statuses[StatusDig]-- if g.Player.Statuses[StatusDig] == 0 { g.PrintStyled("You no longer feel like an earth dragon.", logStatusEnd) g.ui.StatusEndAnimation() } case SwapEnd: g.Player.Statuses[StatusSwap]-- if g.Player.Statuses[StatusSwap] == 0 { g.PrintStyled("You no longer feel light-footed.", logStatusEnd) g.ui.StatusEndAnimation() } case ShadowsEnd: g.Player.Statuses[StatusShadows]-- if g.Player.Statuses[StatusShadows] == 0 { g.PrintStyled("The shadows leave you.", logStatusEnd) g.ui.StatusEndAnimation() g.ComputeLOS() g.MakeMonstersAware() } case SlayEnd: if g.Player.Statuses[StatusSlay] <= 0 { break } g.Player.Statuses[StatusSlay]-- if g.Player.Statuses[StatusSlay] == 0 { g.PrintStyled("You no longer feel extra slaying power.", logStatusEnd) g.ui.StatusEndAnimation() g.ComputeLOS() g.MakeMonstersAware() } case AccurateEnd: g.Player.Statuses[StatusAccurate]-- if g.Player.Statuses[StatusAccurate] == 0 { g.PrintStyled("You no longer feel accurate.", logStatusEnd) g.ui.StatusEndAnimation() } case BlockEnd: g.Player.Blocked = false } } type monsterAction int const ( MonsterTurn monsterAction = iota MonsConfusionEnd MonsExhaustionEnd MonsSlowEnd MonsLignificationEnd ) type monsterEvent struct { ERank int NMons int EAction monsterAction } func (mev *monsterEvent) Rank() int { return mev.ERank } func (mev *monsterEvent) Action(g *game) { switch mev.EAction { case MonsterTurn: mons := g.Monsters[mev.NMons] if mons.Exists() { mons.HandleTurn(g, mev) } case MonsConfusionEnd: mons := g.Monsters[mev.NMons] if mons.Exists() { mons.Statuses[MonsConfused] = 0 if g.Player.LOS[mons.Pos] { g.Printf("The %s is no longer confused.", mons.Kind) } mons.Path = mons.APath(g, mons.Pos, mons.Target) } case MonsLignificationEnd: mons := g.Monsters[mev.NMons] if mons.Exists() { mons.Statuses[MonsLignified] = 0 if g.Player.LOS[mons.Pos] { g.Printf("%s is no longer lignified.", mons.Kind.Definite(true)) } mons.Path = mons.APath(g, mons.Pos, mons.Target) } case MonsSlowEnd: mons := g.Monsters[mev.NMons] if mons.Exists() { mons.Statuses[MonsSlow]-- if g.Player.LOS[mons.Pos] { g.Printf("%s is no longer slowed.", mons.Kind.Definite(true)) } } case MonsExhaustionEnd: mons := g.Monsters[mev.NMons] if mons.Exists() { mons.Statuses[MonsExhausted]-- //if mons.State != Resting && g.Player.LOS[mons.Pos] && //(mons.Kind.Ranged() || mons.Kind.Smiting()) && mons.Pos.Distance(g.Player.Pos) > 1 { //g.Printf("%s is ready to fire again.", mons.Kind.Definite(true)) //} } } } func (mev *monsterEvent) Renew(g *game, delay int) { mev.ERank += delay g.PushEvent(mev) } type cloudAction int const ( CloudEnd cloudAction = iota ObstructionEnd ObstructionProgression FireProgression NightProgression ) type cloudEvent struct { ERank int Pos position EAction cloudAction } func (cev *cloudEvent) Rank() int { return cev.ERank } func (cev *cloudEvent) Action(g *game) { switch cev.EAction { case CloudEnd: delete(g.Clouds, cev.Pos) g.ComputeLOS() case ObstructionEnd: if !g.Player.LOS[cev.Pos] && g.Dungeon.Cell(cev.Pos).T == WallCell { g.WrongWall[cev.Pos] = !g.WrongWall[cev.Pos] } else { delete(g.TemporalWalls, cev.Pos) } if g.Dungeon.Cell(cev.Pos).T == FreeCell { break } g.Dungeon.SetCell(cev.Pos, FreeCell) g.MakeNoise(TemporalWallNoise, cev.Pos) g.Fog(cev.Pos, 1, &simpleEvent{ERank: cev.Rank()}) g.ComputeLOS() case ObstructionProgression: pos := g.FreeCell() g.TemporalWallAt(pos, cev) if g.Player.LOS[pos] { g.Printf("You see a wall appear out of thin air.") g.StopAuto() } g.PushEvent(&cloudEvent{ERank: cev.Rank() + 200 + RandInt(50), EAction: ObstructionProgression}) case FireProgression: if _, ok := g.Clouds[cev.Pos]; !ok { break } g.BurnCreature(cev.Pos, cev) if RandInt(10) == 0 { delete(g.Clouds, cev.Pos) g.Fog(cev.Pos, 1, &simpleEvent{ERank: cev.Rank()}) g.ComputeLOS() break } for _, pos := range g.Dungeon.FreeNeighbors(cev.Pos) { if RandInt(3) > 0 { continue } g.Burn(pos, cev) } cev.Renew(g, 10) case NightProgression: if _, ok := g.Clouds[cev.Pos]; !ok { break } g.MakeCreatureSleep(cev.Pos, cev) if RandInt(20) == 0 { delete(g.Clouds, cev.Pos) g.ComputeLOS() break } cev.Renew(g, 10) } } func (g *game) MakeCreatureSleep(pos position, ev event) { if pos == g.Player.Pos { g.Player.Statuses[StatusSlow]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 30 + RandInt(10), EAction: SlowEnd}) g.Print("The clouds of night make you sleepy.") return } mons := g.MonsterAt(pos) if !mons.Exists() || (RandInt(2) == 0 && mons.Status(MonsExhausted)) { // do not always make already exhausted monsters sleep (they were probably awaken) return } if mons.State != Resting && g.Player.LOS[mons.Pos] { g.Printf("%s falls asleep.", mons.Kind.Definite(true)) } mons.State = Resting mons.ExhaustTime(g, 40+RandInt(10)) } func (g *game) BurnCreature(pos position, ev event) { mons := g.MonsterAt(pos) if mons.Exists() { mons.HP -= 1 + RandInt(10) if mons.HP <= 0 { if g.Player.LOS[mons.Pos] { g.PrintfStyled("%s is killed by the fire.", logPlayerHit, mons.Kind.Definite(true)) } g.HandleKill(mons, ev) } else { mons.MakeAwareIfHurt(g) } } if pos == g.Player.Pos { damage := 1 + RandInt(10) if damage > g.Player.HP { damage = 1 + RandInt(10) } g.Player.HP -= damage g.PrintfStyled("The fire burns you (%d dmg).", logMonsterHit, damage) if g.Player.HP+damage < 10 { g.Stats.TimesLucky++ } g.StopAuto() } } func (g *game) Burn(pos position, ev event) { if _, ok := g.Clouds[pos]; ok { return } _, okFungus := g.Fungus[pos] _, okDoor := g.Doors[pos] if !okFungus && !okDoor { return } g.Stats.Burns++ foliage := true delete(g.Fungus, pos) if _, ok := g.Doors[pos]; ok { delete(g.Doors, pos) foliage = false g.Print("The door vanishes in flames.") } g.Clouds[pos] = CloudFire if !g.Player.LOS[pos] { if foliage { g.WrongFoliage[pos] = true } else { g.WrongDoor[pos] = true } } else { g.ComputeLOS() } g.PushEvent(&cloudEvent{ERank: ev.Rank() + 10, EAction: FireProgression, Pos: pos}) g.BurnCreature(pos, ev) } func (cev *cloudEvent) Renew(g *game, delay int) { cev.ERank += delay g.PushEvent(cev) } boohu-0.13.0/game.go000066400000000000000000000524551356500202200141550ustar00rootroot00000000000000package main import ( "container/heap" "fmt" ) var Version string = "v0.13" type game struct { Dungeon *dungeon Player *player Monsters []*monster MonstersPosCache []int // monster (dungeon index + 1) / no monster (0) Bands []monsterBand BandData []monsterBandData Events *eventQueue Ev event EventIndex int Depth int ExploredLevels int DepthPlayerTurn int Turn int Highlight map[position]bool // highlighted positions (e.g. targeted ray) Collectables map[position]collectable CollectableScore int LastConsumables []consumable Equipables map[position]equipable Rods map[position]rod Stairs map[position]stair Clouds map[position]cloud Fungus map[position]vegetation Doors map[position]bool TemporalWalls map[position]bool MagicalStones map[position]stone GeneratedUniques map[monsterBand]int GeneratedEquipables map[equipable]bool GeneratedRods map[rod]bool GenPlan [MaxDepth + 1]genFlavour FoundEquipables map[equipable]bool Simellas map[position]int WrongWall map[position]bool WrongFoliage map[position]bool WrongDoor map[position]bool ExclusionsMap map[position]bool Noise map[position]bool DreamingMonster map[position]bool Resting bool RestingTurns int Autoexploring bool DijkstraMapRebuild bool Targeting position AutoTarget position AutoDir direction AutoHalt bool AutoNext bool DrawBuffer []UICell drawBackBuffer []UICell DrawLog []drawFrame Log []logEntry LogIndex int LogNextTick int InfoEntry string Stats stats Boredom int Quit bool Wizard bool WizardMap bool Version string Opts startOpts ui *gameui } type startOpts struct { Alternate monsterKind StoneLevel int SpecialBands map[int][]monsterBandData UnstableLevel int } func (g *game) FreeCell() position { d := g.Dungeon count := 0 for { count++ if count > 1000 { panic("FreeCell") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T != FreeCell { continue } if g.Player != nil && g.Player.Pos == pos { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } return pos } } func (g *game) FreeCellForPlayer() position { center := position{DungeonWidth / 2, DungeonHeight / 2} bestpos := g.FreeCell() for i := 0; i < 2; i++ { pos := g.FreeCell() if pos.Distance(center) > bestpos.Distance(center) { bestpos = pos } } return bestpos } func (g *game) FreeCellForStair(dist int) position { iters := 0 bestpos := g.Player.Pos for { pos := g.FreeCellForStatic() adjust := 0 for i := 0; i < 4; i++ { adjust += RandInt(dist) } adjust /= 4 if pos.Distance(g.Player.Pos) <= 6+adjust { continue } iters++ if pos.Distance(g.Player.Pos) > bestpos.Distance(g.Player.Pos) { bestpos = pos } if iters == 2 { return bestpos } } } func (g *game) FreeCellForStatic() position { d := g.Dungeon count := 0 for { count++ if count > 1000 { panic("FreeCellForStatic") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T != FreeCell { continue } if g.Player != nil && g.Player.Pos == pos { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } if g.Doors[pos] { continue } if g.Simellas[pos] > 0 { continue } if _, ok := g.Collectables[pos]; ok { continue } if _, ok := g.Stairs[pos]; ok { continue } if _, ok := g.Rods[pos]; ok { continue } if _, ok := g.Equipables[pos]; ok { continue } if _, ok := g.MagicalStones[pos]; ok { continue } return pos } } func (g *game) FreeCellForMonster() position { d := g.Dungeon count := 0 for { count++ if count > 1000 { panic("FreeCellForMonster") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T != FreeCell { continue } if g.Player != nil && g.Player.Pos.Distance(pos) < 8 { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } return pos } } func (g *game) FreeCellForBandMonster(pos position) position { count := 0 for { count++ if count > 1000 { return g.FreeCellForMonster() } neighbors := g.Dungeon.FreeNeighbors(pos) r := RandInt(len(neighbors)) pos = neighbors[r] if g.Player != nil && g.Player.Pos.Distance(pos) < 8 { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } return pos } } func (g *game) FreeForStairs() position { d := g.Dungeon count := 0 for { count++ if count > 1000 { panic("FreeForStairs") } x := RandInt(DungeonWidth) y := RandInt(DungeonHeight) pos := position{x, y} c := d.Cell(pos) if c.T != FreeCell { continue } _, ok := g.Collectables[pos] if ok { continue } return pos } } const MaxDepth = 11 const WinDepth = 8 const ( DungeonHeight = 21 DungeonWidth = 79 DungeonNCells = DungeonWidth * DungeonHeight ) func (g *game) GenDungeon() { g.Fungus = make(map[position]vegetation) for { dg := GenRuinsMap switch RandInt(7) { //switch 4 { case 0: dg = GenCaveMap case 1: dg = GenRoomMap case 2: dg = GenCellularAutomataCaveMap case 3: dg = GenCaveMapTree case 4: dg = GenBSPMap } if g.Depth > 1 && dg.String() == g.Stats.DLayout[g.Depth-1] && RandInt(4) > 0 { // avoid too often the same layout in a row continue } dg.Use(g) break } } func (g *game) InitPlayer() { g.Player = &player{ HP: 42, MP: 3, Simellas: 0, Aptitudes: map[aptitude]bool{}, } g.Player.Consumables = map[consumable]int{ HealWoundsPotion: 1, } switch RandInt(7) { case 0: g.Player.Consumables[ExplosiveMagara] = 1 case 1: g.Player.Consumables[NightMagara] = 1 case 2: g.Player.Consumables[TeleportMagara] = 1 case 3: g.Player.Consumables[SlowingMagara] = 1 case 4: g.Player.Consumables[ConfuseMagara] = 1 default: g.Player.Consumables[ConfusingDart] = 2 } switch RandInt(12) { case 0, 1: g.Player.Consumables[TeleportationPotion] = 1 case 2, 3: g.Player.Consumables[BerserkPotion] = 1 case 4: g.Player.Consumables[SwiftnessPotion] = 1 case 5: g.Player.Consumables[LignificationPotion] = 1 case 6: g.Player.Consumables[WallPotion] = 1 case 7: g.Player.Consumables[CBlinkPotion] = 1 case 8: g.Player.Consumables[DigPotion] = 1 case 9: g.Player.Consumables[SwapPotion] = 1 case 10: g.Player.Consumables[ShadowsPotion] = 1 case 11: g.Player.Consumables[AccuracyPotion] = 1 } r := g.RandomRod() items := r.String() for c, n := range g.Player.Consumables { if n == 1 { items += ", " + c.String() } else { items += fmt.Sprintf(", %d %s", n, c.Plural()) } } g.StoryPrintf("Started with %s", items) g.Player.Rods = map[rod]rodProps{r: rodProps{r.MaxCharge() - 1}} g.Player.Statuses = map[status]int{} g.Player.Expire = map[status]int{} // Testing //g.Player.Aptitudes[AptStealthyLOS] = true //g.Player.Aptitudes[AptStealthyMovement] = true //g.Player.Rods[RodSwapping] = rodProps{Charge: 3} //g.Player.Rods[RodFireball] = rodProps{Charge: 3} //g.Player.Rods[RodLightning] = rodProps{Charge: 3} //g.Player.Rods[RodLightningBolt] = rodProps{Charge: 3} //g.Player.Rods[RodShatter] = rodProps{Charge: 3} //g.Player.Rods[RodFog] = rodProps{Charge: 3} //g.Player.Rods[RodSleeping] = rodProps{Charge: 3} //g.Player.Consumables[BerserkPotion] = 5 //g.Player.Consumables[MagicMappingPotion] = 1 //g.Player.Consumables[ExplosiveMagara] = 5 //g.Player.Consumables[NightMagara] = 5 //g.Player.Consumables[SlowingMagara] = 5 //g.Player.Consumables[ConfuseMagara] = 5 //g.Player.Consumables[DigPotion] = 5 //g.Player.Consumables[SwapPotion] = 5 //g.Player.Consumables[DreamPotion] = 5 //g.Player.Consumables[ShadowsPotion] = 5 //g.Player.Consumables[TormentPotion] = 5 //g.Player.Consumables[AccuracyPotion] = 5 //g.Player.Weapon = ElecWhip //g.Player.Weapon = DancingRapier //g.Player.Weapon = Sabre //g.Player.Weapon = HarKarGauntlets //g.Player.Weapon = DefenderFlail //g.Player.Weapon = HopeSword //g.Player.Weapon = DragonSabre //g.Player.Weapon = FinalBlade //g.Player.Weapon = VampDagger //g.Player.Shield = EarthShield //g.Player.Shield = FireShield //g.Player.Shield = BashingShield //g.Player.Armour = TurtlePlates //g.Player.Armour = HarmonistRobe //g.Player.Armour = CelmistRobe //g.Player.Armour = ShinyPlates //g.Player.Armour = SmokingScales } func (g *game) InitSpecialBands() { g.Opts.SpecialBands = map[int][]monsterBandData{} sb := MonsSpecialBands[RandInt(len(MonsSpecialBands))] depth := sb.minDepth + RandInt(sb.maxDepth-sb.minDepth+1) g.Opts.SpecialBands[depth] = sb.bands seb := MonsSpecialEndBands[RandInt(len(MonsSpecialEndBands))] if RandInt(4) == 0 { if RandInt(5) > 1 || depth == WinDepth { g.Opts.SpecialBands[WinDepth+1] = seb.bands } else { g.Opts.SpecialBands[WinDepth] = seb.bands } } else if RandInt(5) > 0 { if RandInt(3) > 0 { g.Opts.SpecialBands[MaxDepth] = seb.bands } else { g.Opts.SpecialBands[MaxDepth-1] = seb.bands } } } type genFlavour int const ( GenRod genFlavour = iota GenWeapon GenArmour GenWpArm GenExtraCollectables ) func (g *game) InitFirstLevel() { g.Depth++ // start at 1 g.InitPlayer() g.AutoTarget = InvalidPos g.Targeting = InvalidPos g.GeneratedRods = map[rod]bool{} g.GeneratedEquipables = map[equipable]bool{} g.FoundEquipables = map[equipable]bool{Robe: true, Dagger: true} g.GeneratedUniques = map[monsterBand]int{} g.Stats.KilledMons = map[monsterKind]int{} g.InitSpecialBands() if RandInt(4) > 0 { g.Opts.UnstableLevel = 1 + RandInt(MaxDepth) } if g.Opts.UnstableLevel >= 1 && g.Opts.UnstableLevel <= 3 { // it should happen less often in the first levels g.Opts.UnstableLevel += RandInt(MaxDepth - 2) } if RandInt(3) > 0 || RandInt(2) == 0 && g.Opts.UnstableLevel == 0 { g.Opts.StoneLevel = 1 + RandInt(MaxDepth) } if g.Opts.StoneLevel >= 1 && g.Opts.StoneLevel <= 3 { g.Opts.StoneLevel += RandInt(MaxDepth - 2) } if RandInt(3) == 0 { g.Opts.Alternate = MonsTinyHarpy if RandInt(10) == 0 { g.Opts.Alternate = MonsWorm } } g.Version = Version g.GenPlan = [MaxDepth + 1]genFlavour{ 1: GenRod, 2: GenWeapon, 3: GenArmour, 4: GenRod, 5: GenExtraCollectables, 6: GenWpArm, 7: GenRod, 8: GenExtraCollectables, 9: GenWeapon, 10: GenExtraCollectables, 11: GenExtraCollectables, } permi := RandInt(7) switch permi { case 0, 1, 2, 3: g.GenPlan[permi+1], g.GenPlan[permi+2] = g.GenPlan[permi+2], g.GenPlan[permi+1] } if RandInt(4) == 0 { g.GenPlan[6], g.GenPlan[7] = g.GenPlan[7], g.GenPlan[6] } } func (g *game) InitLevel() { // Starting data if g.Depth == 0 { g.InitFirstLevel() } // Dungeon terrain g.GenDungeon() g.MonstersPosCache = make([]int, DungeonNCells) g.Player.Pos = g.FreeCellForPlayer() g.WrongWall = map[position]bool{} g.WrongFoliage = map[position]bool{} g.WrongDoor = map[position]bool{} g.ExclusionsMap = map[position]bool{} g.TemporalWalls = map[position]bool{} g.DreamingMonster = map[position]bool{} // Monsters g.BandData = MonsBands if bd, ok := g.Opts.SpecialBands[g.Depth]; ok { g.BandData = bd } g.GenMonsters() // Collectables g.Collectables = make(map[position]collectable) g.GenCollectables() // Equipment g.Equipables = make(map[position]equipable) g.Rods = map[position]rod{} switch g.GenPlan[g.Depth] { case GenWeapon: g.GenWeapon() case GenArmour: g.GenArmour() case GenWpArm: g.GenWeapon() g.GenArmour() case GenRod: g.GenerateRod() case GenExtraCollectables: for i := 0; i < 2; i++ { g.GenCollectable() g.CollectableScore-- // these are extra } } if g.Depth == 1 { // extra collectable g.GenCollectable() g.CollectableScore-- } // Aptitudes/Mutations if g.Depth == 2 || g.Depth == 5 { apt, ok := g.RandomApt() if ok { g.ApplyAptitude(apt) } } // Stairs g.Stairs = make(map[position]stair) nstairs := 2 if RandInt(3) == 0 { if RandInt(2) == 0 { nstairs++ } else { nstairs-- } } if g.Depth >= WinDepth { nstairs = 1 } else if g.Depth == WinDepth-1 && nstairs > 2 { nstairs = 2 } for i := 0; i < nstairs; i++ { var pos position if g.Depth >= WinDepth && g.Depth != MaxDepth-1 { pos = g.FreeCellForStair(60) g.Stairs[pos] = WinStair } if g.Depth < MaxDepth { if g.Depth > 5 { pos = g.FreeCellForStair(50) } else { pos = g.FreeCellForStair(0) } g.Stairs[pos] = NormalStair } } // Magical Stones g.MagicalStones = map[position]stone{} nstones := 1 switch RandInt(8) { case 0: nstones = 0 case 1, 2, 3: nstones = 2 case 4, 5, 6: nstones = 3 } ustone := stone(0) if g.Depth == g.Opts.StoneLevel { ustone = stone(1 + RandInt(NumStones-1)) nstones = 10 + RandInt(3) if RandInt(4) == 0 { g.Opts.StoneLevel = g.Opts.StoneLevel + RandInt(MaxDepth-g.Opts.StoneLevel) + 1 } } for i := 0; i < nstones; i++ { pos := g.FreeCellForStatic() var st stone if ustone != stone(0) { st = ustone } else { st = stone(1 + RandInt(NumStones-1)) } g.MagicalStones[pos] = st } // Simellas g.Simellas = make(map[position]int) for i := 0; i < 5; i++ { pos := g.FreeCellForStatic() const rounds = 5 for j := 0; j < rounds; j++ { g.Simellas[pos] += 1 + RandInt(g.Depth+g.Depth*g.Depth/6) } g.Simellas[pos] /= rounds if g.Simellas[pos] == 0 { g.Simellas[pos] = 1 } } // initialize LOS if g.Depth == 1 { g.Print("You're in Hareka's Underground searching for medicinal simellas. Good luck!") g.PrintStyled("► Press ? for help on keys or use the mouse and [buttons].", logSpecial) } if g.Depth == WinDepth { g.PrintStyled("You feel magic in the air. A first way out is close!", logSpecial) } else if g.Depth == MaxDepth { g.PrintStyled("If rumors are true, you have reached the bottom!", logSpecial) } g.ComputeLOS() g.MakeMonstersAware() // Frundis is somewhere in the level if g.FrundisInLevel() { g.PrintStyled("You hear some faint music… ♫ larilon, larila ♫ ♪", logSpecial) } // recharge rods if g.Depth > 1 { g.RechargeRods() } // clouds g.Clouds = map[position]cloud{} // Events if g.Depth == 1 { g.Events = &eventQueue{} heap.Init(g.Events) g.PushEvent(&simpleEvent{ERank: 0, EAction: PlayerTurn}) } else { g.CleanEvents() } for i := range g.Monsters { g.PushEvent(&monsterEvent{ERank: g.Turn + RandInt(10), EAction: MonsterTurn, NMons: i}) } if g.Depth == g.Opts.UnstableLevel { g.PrintStyled("You sense magic instability on this level.", logSpecial) for i := 0; i < 15; i++ { g.PushEvent(&cloudEvent{ERank: g.Turn + 100 + RandInt(900), EAction: ObstructionProgression}) } if RandInt(4) == 0 { g.Opts.UnstableLevel = g.Opts.UnstableLevel + RandInt(MaxDepth-g.Opts.UnstableLevel) + 1 } } } func (g *game) CleanEvents() { evq := &eventQueue{} for g.Events.Len() > 0 { iev := g.PopIEvent() switch iev.Event.(type) { case *monsterEvent: case *cloudEvent: default: heap.Push(evq, iev) } } g.Events = evq } func (g *game) StairsSlice() []position { stairs := []position{} for stairPos, _ := range g.Stairs { if g.Dungeon.Cell(stairPos).Explored { stairs = append(stairs, stairPos) } } return stairs } func (g *game) GenCollectable() { rounds := 100 if len(g.LastConsumables) > 3 { g.LastConsumables = g.LastConsumables[1:] } for { loopcons: for c, data := range ConsumablesCollectData { r := RandInt(data.rarity * rounds) if r != 0 { continue } // avoid too many of the same for _, co := range g.LastConsumables { if co == c && RandInt(4) > 0 { continue loopcons } } g.LastConsumables = append(g.LastConsumables, c) g.CollectableScore++ pos := g.FreeCellForStatic() g.Collectables[pos] = collectable{Consumable: c, Quantity: data.quantity} return } } } func (g *game) GenCollectables() { score := g.CollectableScore - 2*(g.Depth-1) n := 2 if score >= 0 && RandInt(4) == 0 { n-- } if score <= 0 && RandInt(4) == 0 { n++ } if score > 0 && n >= 2 { n-- } if score < 0 && n <= -2 { n++ } for i := 0; i < n; i++ { g.GenCollectable() } } func (g *game) GenShield() { ars := [4]shield{ConfusingShield, BashingShield, EarthShield, FireShield} for { i := RandInt(len(ars)) if g.GeneratedEquipables[ars[i]] { // do not generate duplicates continue } pos := g.FreeCellForStatic() g.Equipables[pos] = ars[i] g.GeneratedEquipables[ars[i]] = true break } } func (g *game) GenArmour() { ars := [6]armour{SmokingScales, ShinyPlates, TurtlePlates, SpeedRobe, CelmistRobe, HarmonistRobe} for { i := RandInt(len(ars)) if g.GeneratedEquipables[ars[i]] { // do not generate duplicates continue } pos := g.FreeCellForStatic() g.Equipables[pos] = ars[i] g.GeneratedEquipables[ars[i]] = true break } } func (g *game) GenWeapon() { wps := [WeaponNum - 1]weapon{Axe, BattleAxe, Spear, Halberd, AssassinSabre, DancingRapier, HopeSword, Frundis, ElecWhip, HarKarGauntlets, VampDagger, DragonSabre, FinalBlade, DefenderFlail} onehanded := false for { i := RandInt(len(wps)) if g.GeneratedEquipables[wps[i]] { // do not generate duplicates continue } pos := g.FreeCellForStatic() g.Equipables[pos] = wps[i] if !wps[i].TwoHanded() { onehanded = true } g.GeneratedEquipables[wps[i]] = true break } if onehanded { g.GenShield() } } func (g *game) FrundisInLevel() bool { for _, eq := range g.Equipables { if wp, ok := eq.(weapon); ok && wp == Frundis { return true } } return false } func (g *game) Descend() bool { g.LevelStats() if strt, ok := g.Stairs[g.Player.Pos]; ok && strt == WinStair { g.StoryPrint("Escaped!") g.ExploredLevels = g.Depth g.Depth = -1 return true } g.Print("You descend deeper in the dungeon.") g.StoryPrint("Descended deeper in the dungeon.") g.Depth++ g.DepthPlayerTurn = 0 g.Boredom = 0 g.PushEvent(&simpleEvent{ERank: g.Ev.Rank(), EAction: PlayerTurn}) g.InitLevel() g.Save() return false } func (g *game) WizardMode() { g.Wizard = true g.Player.Consumables[DescentPotion] = 15 g.PrintStyled("You are now in wizard mode and cannot obtain winner status.", logSpecial) } func (g *game) ApplyRest() { g.Player.HP = g.Player.HPMax() g.Player.MP = g.Player.MPMax() for _, mons := range g.Monsters { if !mons.Exists() { continue } mons.HP = mons.HPmax } adjust := 0 if g.Player.Armour == HarmonistRobe { // the harmonist robe mitigates the sound of your snorts adjust = 100 } if g.DepthPlayerTurn < 100+adjust && RandInt(5) > 2 || g.DepthPlayerTurn >= 100+adjust && g.DepthPlayerTurn < 250+adjust && RandInt(2) == 0 || g.DepthPlayerTurn >= 250+adjust && RandInt(3) > 0 { rmons := []int{} for i, mons := range g.Monsters { if mons.Exists() && mons.State == Resting { rmons = append(rmons, i) } } if len(rmons) > 0 { g.Monsters[rmons[RandInt(len(rmons))]].NaturalAwake(g) } } g.Stats.Rest++ g.PrintStyled("You feel fresh again. Some monsters might have awoken.", logStatusEnd) } func (g *game) AutoPlayer(ev event) bool { if g.Resting { const enoughRestTurns = 15 mons := g.MonsterInLOS() sr := g.StatusRest() if mons == nil && (sr || g.NeedsRegenRest() && g.RestingTurns >= 0) && g.RestingTurns < enoughRestTurns { g.WaitTurn(ev) if !sr && g.RestingTurns >= 0 { g.RestingTurns++ } return true } if g.RestingTurns >= enoughRestTurns { g.ApplyRest() } else if mons != nil { g.Stats.RestInterrupt++ g.Print("You could not sleep.") } g.Resting = false } else if g.Autoexploring { if g.ui.ExploreStep() { g.AutoHalt = true g.Print("Stopping, then.") } switch { case g.AutoHalt: // stop exploring default: var n *position var finished bool if g.DijkstraMapRebuild { if g.AllExplored() { g.Print("You finished exploring.") break } sources := g.AutoexploreSources() g.BuildAutoexploreMap(sources) } n, finished = g.NextAuto() if finished { n = nil } if finished && g.AllExplored() { g.Print("You finished exploring.") } else if n == nil { g.Print("You could not safely reach some places.") } if n != nil { err := g.MovePlayer(*n, ev) if err != nil { g.Print(err.Error()) break } return true } } g.Autoexploring = false } else if g.AutoTarget.valid() { if !g.ui.ExploreStep() && g.MoveToTarget(ev) { return true } else { g.AutoTarget = InvalidPos } } else if g.AutoDir != NoDir { if !g.ui.ExploreStep() && g.AutoToDir(ev) { return true } else { g.AutoDir = NoDir } } return false } func (g *game) EventLoop() { loop: for { if g.Player.HP <= 0 { if g.Wizard { g.Player.HP = g.Player.HPMax() } else { g.LevelStats() err := g.RemoveSaveFile() if err != nil { g.PrintfStyled("Error removing save file: %v", logError, err.Error()) } g.ui.Death() break loop } } if g.Events.Len() == 0 { break loop } ev := g.PopIEvent().Event g.Turn = ev.Rank() g.Ev = ev ev.Action(g) if g.AutoNext { continue loop } if g.Quit { break loop } } } boohu-0.13.0/game_test.go000066400000000000000000000005021356500202200151760ustar00rootroot00000000000000package main import "testing" func TestInitLevel(t *testing.T) { for i := 0; i < 10; i++ { g := &game{} for depth := 0; depth < 11; depth++ { g.Depth = depth g.InitLevel() for _, m := range g.Monsters { if g.Dungeon.Cell(m.Pos).T != FreeCell { t.Errorf("Not free: %+v", m.Pos) } } } } } boohu-0.13.0/images.go000066400000000000000000002747511356500202200145160ustar00rootroot00000000000000// +build js tk // font used for letters: source code pro package main func init() { TileImgs = map[string][]byte{} TileImgs["letter-0"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAV0lEQVQ4jWNgGHSAEVPo////CGlG dAWMuJTi0YOiAU0PpghhOTRxJpzW4QCUaYD7D4/TsdiA05e4NOALRzQNcLMZGRlxaaN9KKEAYmKa 5LREcmodBcQAAJn8O/F+864QAAAAAElFTkSuQmCC `) TileImgs["letter-1"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMUlEQVQ4jWNgGCLg////////xyrF gqaO+jYwkWrWqIYhqoERzsKfkBgZGcm0YRQQAwCp2hIN7jKm7wAAAABJRU5ErkJggg== `) TileImgs["letter-2"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUElEQVQ4jWNgGHSAEY3///9/FGlG dAUofDTVWPUgOBDVyNJw/ciCTPgMw3APigas0vg0EAlI1oAP/P//H2u4jTzVWBICdnWwaKVqPIwC GAAABrky9E6EMlQAAAAASUVORK5CYII= `) TileImgs["letter-3"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAZklEQVQ4je2SwQ6AMAxCN+P//3I9 mNgOEOvNg9yWPUrJNsbnNOEcEcv1RGA5Ay1tO9AwkkdsfgFWGjo0JoBkJTG1chzrEmSI2/uim/XS VnMeVmKl4e6ZteGk2eNKm4Ra+vVv/dXRAVmVMASCyAPdAAAAAElFTkSuQmCC `) TileImgs["letter-4"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVUlEQVQ4je2SQQrAIAwEd/3/n7cH QVtJYhQ8CJ1jmMEgAa5CkqRhWALbnNtBtUmmgmabj7greYxBsIwRTO1PkLF7kLQB8B1MVBIbvxSx dhoe54OfDA+wXicIiFgWcgAAAABJRU5ErkJggg== `) TileImgs["letter-5"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWklEQVQ4jWNgGHSAEZnz//9/7IoY EcqYSLWBBb95BAAuJyEDajgJ2R5M55EcSggWRDWakXAjSAsJZJtJ9jS5GoiJAQhgRNOA1dP4Qgm7 qVg1YNVGQmiOAiQAACQJJxDX8UETAAAAAElFTkSuQmCC `) TileImgs["letter-6"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAYklEQVQ4je1SOw5AIQzSF+9/5b7B TxSqVl0cZKRAWlLnroNnSkQahW80aAA124KqrlN7EWk2Gu+pP2teRmDK2tJgn9qjGCAy8oXEG0DN WD76wBCXgdO5CdMv6S31PNMaHhg/nrowCtkOm60AAAAASUVORK5CYII= `) TileImgs["letter-7"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAARklEQVQ4jWNgGHSAEc76//8/PnWM UJVM1HcDms0EbMDvTiyqSdaAKYjTSaSZjUcDdhvwGE9yPFAp4khzEv7wGSg/jAJUAACYNSD2YoRs XwAAAABJRU5ErkJggg== `) TileImgs["letter-8"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAZElEQVQ4je1QOQ7AMAiDqP//Mh0S tYkxKMfSod7AB8gin4P6lZm9tKJAI+kg6mxXTkcRjaM07EvyLkWBmR7pszD1MVRRHacM0atDS5CX VSQbLfnrFNjSmsF/RTrI6SY6qfXHDG47T0H7Um9AGAAAAABJRU5ErkJggg== `) TileImgs["letter-9"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAZ0lEQVQ4jeVSQQ7AMAiqZv//Mjss 2SoYPOyyZNwUrGC61ucQVAModLCg1KRuZ57iVu+Kq7l3cnhPLKWn1SpvGFEGNLR23Ib+aF4REXQo F7q9waFPGpMzAJDPNNwQuqXVpPutb/P8GCepbDMQH0KS6AAAAABJRU5ErkJggg== `) TileImgs["letter-A"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXElEQVQ4je2RwQ7AIAxCZdn//zI7 LM6lJaS60xI52lIesbWfiCRJOTrktrklDN6pDQCqCc/V25NDHFIpwfNEQwCQVNNIZ37y/4DqXi8W E3Ljrx3GGYP0nq4mbFldSrMtA7gBVywAAAAASUVORK5CYII= `) TileImgs["letter-B"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVklEQVQ4jWNgGHSAEc76//8/dhWM jMhcJoJGohmEbgOaeXDVcHECNqDpJ8pJpGnAdCftQwlFAqscmjhhG0h2EprDiPUDsRGH6SvCwcqA 6g2SPT0KiAEAha0tCvChWkMAAAAASUVORK5CYII= `) TileImgs["letter-C"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXklEQVQ4je1SQQ4AEAxD/P/LcxCx VRkHiYMet3YbbQjPIdKqiHRGNBwUaKrhNVmmbD0VRpAG3ABIs9W7AhfHgo7Z/6w27GjuvwEF9CqT FNqgTtficZaIrxgez/uPEQW1dycNRNauhwAAAABJRU5ErkJggg== `) TileImgs["letter-D"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWklEQVQ4je1SQQoAIAhz/f/PdghE VNQOQod2CnObsBE9B8iLmYNvwExWrudVrIOWlG09zBwAnFXtU5zkUROMyYDDPMFE0XWQKK6Tzghh 0kX5yPWvOMm39aODDW2SHhymX90sAAAAAElFTkSuQmCC `) TileImgs["letter-E"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOElEQVQ4jWNgGHSAEZnz//9/nOoY oSqZSLWBBY9hWAHJNtBeA1GhhOyrERlKtAlWBkoS3yggBgAAMUUJJrlUpZ8AAAAASUVORK5CYII= `) TileImgs["letter-F"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANElEQVQ4jWNgGHSAEZnz//9/nOoY oSqZSLWBBY9hWAHJNtBeA1GhhOyrERlKg1DDKCAGAADE0wYn9o4BfQAAAABJRU5ErkJggg== `) TileImgs["letter-G"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAYElEQVQ4jWNgGHSAEVPo////KCoY UdSga0BTjakHRQNcNbIKiCCaPVAJrMajASaCKrADIo0nxwYWPHYic+Gepp4NcCPRrGJClqZJsCI0 wC3Bbw9RaYkByUskp9ZRQAwAAHQ1LQu5bs34AAAAAElFTkSuQmCC `) TileImgs["letter-H"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMklEQVQ4jWNgGMTg////////JyjO RKq5w0EDCxofa0BR1QZGRkb8dg7CUBqEGkYBMQAAsgISErMyrJwAAAAASUVORK5CYII= `) TileImgs["letter-I"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAALklEQVQ4jWNgGHSAEc76//8/PnWM UJVM1LH3////uCwk2YZRDUNUw0AlvlGACgDYYQwVeZqOCwAAAABJRU5ErkJggg== `) TileImgs["letter-J"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAQUlEQVQ4jWNgGHSAEZnz//9/7IoY EcqYqOyA////o1lLsg2jGmisAVe6wGcDWrxiNYJw4kNOeegaMPWgqR4FRAIAuD4bCGleye0AAAAA SUVORK5CYII= `) TileImgs["letter-K"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAYElEQVQ4je2RMQ7AMAgDTf//ZzpU QgjseumQoZ6ixIeNAhynqFNmAoiI4Xju6+l6nzfcBthun7BLSkCtxAHl5kBVp5pAn01JuYNiCECr S6C7aYj/B19pB/aQrxNoyC+rGyyoJyHwcT7qAAAAAElFTkSuQmCC `) TileImgs["letter-L"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAALUlEQVQ4jWNgGNzg////////x6+G iVRDRzWMasABGJE5eFIeIyMjmTaMAmIAAGP8CRaVjXzxAAAAAElFTkSuQmCC `) TileImgs["letter-M"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWUlEQVQ4je2RsRIAIAhCs+v/f5mG NkStobuGGMkXZK09LAAASr+Xt5DDgJ8wsxCgM6ms0kqjTAHIp4cAtfIlw0pRjgbWtNyBAPJdFR/n NaSbhBwn3Ae+djQBsEshE8dQQO8AAAAASUVORK5CYII= `) TileImgs["letter-N"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATklEQVQ4jWNgGMTg////////JyjO hCmN31wUDYyMjAQdgqIBYjx+S9CdRJoNcFfhsYRiGwhaQg0b8FtCJRvwWILPBqwRTz0n4bFkFBAE AE8SIRFoIhqWAAAAAElFTkSuQmCC `) TileImgs["letter-O"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXUlEQVQ4je1SsQ0AIAgD4/8v46BB pGrUicFuQksrgSgcGEsi0tvsCbyiDiQjy8i2bRyRNsP0aWVpHwYxOOAX0cRHunMIKJjuqhZ1H00w 3c9RJGfixtPDLV1f68cJChMcJxodPcJCAAAAAElFTkSuQmCC `) TileImgs["letter-P"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASElEQVQ4jWNgGHSAEc76//8/dhWM jChcghrQtLHgNw/TICY8pmIFhDWg2UlYA2lOwgwJokIJ2VUk+4FwsKIBGgTr4NcwCogBABUgEiPV tqjTAAAAAElFTkSuQmCC `) TileImgs["letter-Q"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAa0lEQVQ4jc1TQQ6AMAgD4/+/XA8z E1tBZ7JkvW20UBgzWw6uVwCusDPBM+qNFGS7smNYU2xFsn6MMhZ8BYCsAYoOV1hB0Nz36Z0CHd80 S1SE/NiPXeJlJI1u6zPqhx/TpBWV3ey9WKx/3xwcePI/Ao8j9xcAAAAASUVORK5CYII= `) TileImgs["letter-R"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXklEQVQ4jWNgGHSAEc76//8/dhWM jMhcJoJGohmEbgOaeXDVcHECNqDpJ8pJNNaAGXSEg5UB1SeEnYTmbxZc0lhDGZ8NEKWY7iQ5pvFp wHQPYRswHUZsxOEJ9FGACQBX7iEeyp8VgwAAAABJRU5ErkJggg== `) TileImgs["letter-S"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAX0lEQVQ4je1SORIAIQgL+/8/Y+Os CBF1bCxMKTmYIHAdJD6paseQjiMJlWqIwI5/C5dTZ6MEi2/KyLAS4jezArL3aa0jJY3KZDZzu6Um WDmCT4i1Rpezv8Qttyp6AAAUo1o485e+kI8AAAAASUVORK5CYII= `) TileImgs["letter-T"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAALUlEQVQ4jWNgGHSAEUL9//+fsFJG RgYGBibq2Pv//39cdpJsw6iGUQ2jgAIAAGv1CRbyg0exAAAAAElFTkSuQmCC `) TileImgs["letter-U"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANklEQVQ4jWNgGMTg////////JyjO RKq5oxpGqga05AThMjIywkUY8ahGKMKlAVMPstJRQDwAADJNGAxgBBDNAAAAAElFTkSuQmCC `) TileImgs["letter-V"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXElEQVQ4jWNgGKzg////////J0aW iRizGBgYGBkZUTRA+HgsgQPCNpCjAe4eFA1YXYXpSBo4Cdk96BrQXIU10KjtJDT3YNEAV4ErErHb gCfKSfYDdoA/8Y4CggAA+e0qCWwjtQMAAAAASUVORK5CYII= `) TileImgs["letter-W"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAZklEQVQ4je2SOw7AMAhDH9z/znSI hCp+TaUMHeqNYGOHBD4HAcwMEJGOtAiLo/m0nS0C6Dw7Q58pOwIz6xKecPDZpcmugy9GQ12WrcM9 g2tCsBgptPM1TmxpfvjJoVRuRXr12X4AFyN8IR4XoFjLAAAAAElFTkSuQmCC `) TileImgs["letter-X"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWElEQVQ4jeVSuwoAIBDy+v9/tqGh 6K6saAhyFHwgAg+DJEnJJ+kCwMwCQWHDkNViYUlRycM81bq27Q8TYowmvlHJDy8EnbeYVdrPEiYh 1X7jfPJC2x/7Gxk2V0flHa2atwAAAABJRU5ErkJggg== `) TileImgs["letter-Y"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAQ0lEQVQ4jWNgGKzg////////J00W lx40cSYqOwxThDIb0IzE6iuSbWDEaglCmhFdAT4bMFVj14BVHVE2DFsNo4AYAAAf0TLstDSWXgAA AABJRU5ErkJggg== `) TileImgs["letter-Z"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANUlEQVQ4jWNgGHSAEc76//8/PnWM UJVM1HfD////8Vs+4Kqp5OkB9ShlfqCJ0wdD4hsFDAwA0xk71RqHAUgAAAAASUVORK5CYII= `) TileImgs["letter-a"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVUlEQVQ4je2QsRIAEAxDxfn/X46N 0lAGm0yufbkmUvp6IPgRyYHAwGCDSk9/Ndqu5TAQSXu5rKCVvxxywmBpWUNHCsvl/dqHnC+ENfoF HwbAxfd/GVXY8SQT1EK8RQAAAABJRU5ErkJggg== `) TileImgs["letter-arrow"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASUlEQVQ4jWNgGKHg////////J1Ix I0QDgs/ISJoGgjpxasCljYAGTJ3EaoBrYyFeKYRBQAOmH3BqwBVK6BoIxgNCA0Glo2AwAQCYwBUj NrCdMQAAAABJRU5ErkJggg== `) TileImgs["letter-asterisc"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATklEQVQ4jWNgGK7g////uKSYSNWD UwNhG/A4A1mKBVOCkZERjxGMeAyDqmBEUYOuAU0PmmoGMjyNxXi4DXiCAbt7sOphwi9NrD2jYNAD AJfSLO2WOBYXAAAAAElFTkSuQmCC `) TileImgs["letter-b"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUklEQVQ4je2RwQoAIAhDW/T/v7xu ojaQLlHQO65NcrZ2DpIkS1vfnXthYEjVbw/AP0GaEj6jA+Yw0ZS8A4AwL/5HBEoeCKRy16637yBq LZVPyQTMWRsh/QC8EQAAAABJRU5ErkJggg== `) TileImgs["letter-backslash"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASklEQVQ4jb2SMQ4AIAwCif//M25d VJJSIvvlIC3wLyRJ5pnlVypJ26CZcSXTIJhEJciDhAx4z1CGK5OrZBqch+8ZTiY6ugztJaNsXZ41 4Q5NDtkAAAAASUVORK5CYII= `) TileImgs["letter-boxe"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAALUlEQVQ4jWNgwAb+//////9/rFJM WEXxgFENg0MDC64YpZoN2MFoWhrVQLoGAHeeDyBp6/G3AAAAAElFTkSuQmCC `) TileImgs["letter-boxne"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAKUlEQVQ4jWNgGAUjAzD+//8fpxwj I6YgE3Xs/f//Py6bSbZhVMNI0QAAUjkJFKC9N6EAAAAASUVORK5CYII= `) TileImgs["letter-boxse"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAKklEQVQ4jWNgwAb+//////9/rFJM WEXxgFENg0MDC64YpZoNo2AUUAsAADXMCRDEi5wNAAAAAElFTkSuQmCC `) TileImgs["letter-c"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAARUlEQVQ4jWNgGAU0AIyYQv///0dR wYiiBl0DmmpMbSxYVSObissIhv///+OUQwJMBFUMfg0owYonlOAiJMcDyTE9CogBAPxmHgkhag0h AAAAAElFTkSuQmCC `) TileImgs["letter-colon"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAKUlEQVQ4jWNgGAXkgv///////x+r FCNW1QhpRnQFTDR30vAFo/Ew/AAAdWUd7xfXyb8AAAAASUVORK5CYII= `) TileImgs["letter-comma"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOUlEQVQ4jWNgGAWjYISB//////// H6sUI1bVCGlGdAVMWMzAUERAA3492DWQDPB4mmLVDKihRAUAAAJ9F/ke+NToAAAAAElFTkSuQmCC `) TileImgs["letter-d"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAZElEQVQ4je1SOxaAMAxK8rz/lXFQ Y4TWp0vrIBstkE9rNh8AACSNt/4PGhbidT4zc3cSXDipm86zQqprqkbwDNSDthQ1Sa8Vw95ha+Zm S90K1UPfbo/uqVl37COap0mf7O2HYgX8gSQeOVIZ4gAAAABJRU5ErkJggg== `) TileImgs["letter-dot"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAKElEQVQ4jWNgGAWjYGiD//////// H6sUI1bVCGlGdAVMNHfSKCAGAABHNg740fM+0wAAAABJRU5ErkJggg== `) TileImgs["letter-dots"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMUlEQVQ4jWNgGAWjYNgCRgj1//9/ KJ+REc5FZsO5TJhmwFXAGcgAiwaISciMUUAqAABj+g8G/3WJvwAAAABJRU5ErkJggg== `) TileImgs["letter-dreaming"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAYUlEQVQ4je1SQQrAMAgzZf//cnYZ UqMWStlhsJyqJia0Nfs+IDXJMIYSQi3shexhu6A7m9lY787moxt0W1Qwxy2jX8I+cihNggNJAPUl dg6CnAp55g4z25vbL739l368ghu7TTwDVNiIYAAAAABJRU5ErkJggg== `) TileImgs["letter-e"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVklEQVQ4jWNgGAU0AIyYQv///0dR wYiihhGPUqx6ECy4ajQjIeJwQSY8hmEVYUGTxuUqOEC3gSBAtwHTSZTagK4B0w////9HFiQqHhiQ nEpyTI8CYgAAYc8eE5PHb5UAAAAASUVORK5CYII= `) TileImgs["letter-equal"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAJUlEQVQ4jWNgGAU0AIxw1v////Gp Y4SqZKKte8gBw8EPo2CIAgANWQYH3bjBwAAAAABJRU5ErkJggg== `) TileImgs["letter-exclamation"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAM0lEQVQ4jWNgGCLg////////xyrF RKpZoxpoomGgAJ6kwYhVNUKaEV0BlTyNx0mjgBgAAO6MFQKPyEb8AAAAAElFTkSuQmCC `) TileImgs["letter-f"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAQ0lEQVQ4jWNgoDVgxCP3//9/FKWM jAwMDCzEKEUG2DUgG4lmBBMeDbg0k+YkLJbit4FYJxEAeGwj2YZRDTTRMAqIAQBpihIZqcW/PgAA AABJRU5ErkJggg== `) TileImgs["letter-fog"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAc0lEQVQ4jc1TQQrAMAgz+/+f3WHQ itFYBoN5syaNsdbs6wAfufsuIwNyHtEdJxMiJ6UD+ihGztXROgIYB0AMCoxmnXhYt7RwDzQKKkIZ m7Au01NSq7HcRw/ts5eOTXgYVmjUqRV48wplXebehv/w0snP4waqE1DxUsNqugAAAABJRU5ErkJg gg== `) TileImgs["letter-frontier"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAV0lEQVQ4je2RwQrAIAxDE9n//3J2 EB3WEnoU3LvVNDVV4GokVSSGI5KLPEqSi8FfMrsBPF7eRzTTnfIZ0jy7vxmtFMk8bidZ2nviDiFY L5MRkoo//XMGL4jGMAPFTm3JAAAAAElFTkSuQmCC `) TileImgs["letter-g"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAbElEQVQ4je1RQQ7AMAhSs/9/mR2W OGcoNVl2GzcrKFSzHx/AWw1gSXU3sxiyE8dqUh1RX0L0KEK3yf5aZIZrTyuJwFhuZTLZAPY/NiJV S5qdxnhoobkPJ+5QBz3usL1aF1BL7fFdaCqb+FQ4AfRHPAUIlersAAAAAElFTkSuQmCC `) TileImgs["letter-gt"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAR0lEQVQ4jWNgGMTg////xChjxNTA yMiIQzGqBuL1YHEbHudhNwmPVTitpprzmEjQTZ6TSPY0dtXEBisxBpOcNLAYPwoGAAAA570v86cV AugAAAAASUVORK5CYII= `) TileImgs["letter-h"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASUlEQVQ4jWNgoB/4////////CSpj ItXcQaiBBasosu8ZGRkJ2IAWVmhcRqwScFPhgnARLDYguwHNPVg0YKogoIEgGJEaRgExAAAzTxUd exIgrAAAAABJRU5ErkJggg== `) TileImgs["letter-hash"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAV0lEQVQ4jWNgGArg////yGxkLgMD AxNB/YyMjKRpQAMka0BYh+ZWdHUwh5FsAzpACxZMa7HYALcdqyNp42nkqGDB7yS0WMMC8PuYgdKY xh93ZNowCogBAK8vJwTnOpfAAAAAAElFTkSuQmCC `) TileImgs["letter-hbar"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAHUlEQVQ4jWNgGAUjAzD+//+fJA1M NHLIKBgFBAEACLQDAWnx+3YAAAAASUVORK5CYII= `) TileImgs["letter-hit"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXklEQVQ4jdWQMQ7AMAgDQ9T/f5kO XRA4jlGUoZ4YfIA9hiZ3/4bZcqtAW0cXLgDxH0kJaL/01E1mRoAZ3dW6D5AcFQAZ+NYM8AD4Aj+F AdIYAKRadS2BVVcY2Hb1b72SASoLxAb2+wAAAABJRU5ErkJggg== `) TileImgs["letter-hyphen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIklEQVQ4jWNgGAVDEjDCWf///8en jhGqkom27hkFo4CKAACI8QME8uqXXgAAAABJRU5ErkJggg== `) TileImgs["letter-i"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAPElEQVQ4jWNgoBb4////////McWZ cKnGZRB2DYyMjOS5a0AAwq1YPYrpGeyeJgeQFg94wKgGmmgYBcQAAHArEgyeM0JuAAAAAElFTkSu QmCC `) TileImgs["letter-interrogation"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATUlEQVQ4jWNgoDVgxBT6//8/QpoR XQEjLqW49GDRAFcB149pD07w//9/NGuZiNVKnvEDpZoBR/hCAFU9TR2ALy1hjWCS/UB7DaOAGAAA H88p9IuKJvgAAAAASUVORK5CYII= `) TileImgs["letter-j"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASklEQVQ4jWNgoBb4////////McWZ cKnGZRB2DYyMjOS5a0AAwq1YPYrpGeyeJgeQFg94wKiGoa0BLRXgyXGMeFRgzXc4kzfVcikAU6Ae DvI9I/kAAAAASUVORK5CYII= `) TileImgs["letter-k"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWklEQVQ4je2SMQ4AIAgDxfj/L9fB hBhAi4txsPMdWLWUewEAgGL1dO6DQqOE3oSIcGHQA+VH8vROCOmlsKJjQVuGDy+e09nZDoYwe6ww 09kO3p+X8K/hnR+aDgQAKhx0pOT0AAAAAElFTkSuQmCC `) TileImgs["letter-kill"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUUlEQVQ4je2QOQ4AIAgEd43///Ja EI8g2FmYOI1xQA6BzwVoh6SpeJJ0gREOJYDq7mFV85JIFt+xP1uzV8o+bmaMesgI+6Qfki0dD5pV +bxLAy0ULQ4kMZu1AAAAAElFTkSuQmCC `) TileImgs["letter-l"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOElEQVQ4jWNgoDVghLP+//+PLsfI yIABmKhj7////zEtJNOGUQ3DWQNyMmEhUh0BG7Am7FFAPAAAG1kPF1Y3zLEAAAAASUVORK5CYII= `) TileImgs["letter-lbrace"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVElEQVQ4jWNgIBEw4pL4//8/ijpG qEomYlQjAxZ8tjNisR+7DXjAqAYIwBMJDGgxjawUaySQ4yQsxsDtITamcTmGTCcNBw3YAwQzsglk UfwhS2MAAOcSEiyKk3AcAAAAAElFTkSuQmCC `) TileImgs["letter-lbracket"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMElEQVQ4jWNgIBEwYgr9//8fizpG qEomUm1gwWk1IxbLybFhVMOohmGsgfZZlPYAAIUdBi6L64lcAAAAAElFTkSuQmCC `) TileImgs["letter-longhyphen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIklEQVQ4jWNgGAWjACtgZGBg+P// P7GqGRmZaOmaUTCiAABzmAMEIf36fwAAAABJRU5ErkJggg== `) TileImgs["letter-lparen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAT0lEQVQ4jcVSQQ4AIAjC/v9nO7aV MTVdHBVQnEAtVHWriIctwmiLfdoDGN7lcvZMcGsZK3HvcIZ+gYFMhtiVOL6ERi5D7JfQ/t58SLXm FRPxJjXlQ5S8QQAAAABJRU5ErkJggg== `) TileImgs["letter-lquotes"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANElEQVQ4jWNgoDf4//8/fhEmMvSQ ppQJjxxOg0nSwIgpzcjIiEuEgQqeRpPDFBkFo2DoAACmmjXYy1xXEQAAAABJRU5ErkJggg== `) TileImgs["letter-m"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAP0lEQVQ4je2MMQ4AIAwCD///5zo0 6YAmOujWWwo0AM0HlCciAEkpUlde1guHbQkYa1pjq/WC/cxuCjd0oXnFBBkkDCfYa8CbAAAAAElF TkSuQmCC `) TileImgs["letter-magic"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAX0lEQVQ4je1SSQoAIQwz/v/PmcNA O2osLXgRJidpli7Y2iUgSVJSvZqFNXtWYNAgkEpbn9QAjPu+DcMOK21FSywv7QYZHxnipYWhPNKu yVv00yVHEoadLXWMk5/vRwYPsHonFL9lilYAAAAASUVORK5CYII= `) TileImgs["letter-music1"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAT0lEQVQ4jWNgGDDw//9/rOJMpOph JEY1IyNCGT4b4OqQNePTgGY2URow9RDWQLINlGrACciJOOpoYCHGGdhtIEY1PidhxjE+DbhUjwIi AQC9MRUbP+PYIwAAAABJRU5ErkJggg== `) TileImgs["letter-music2"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAVUlEQVQ4jWNgIBEwElTx//9/hGpG Rhb8KjABE6lOIlkDFichA0ZGqCfh7sSuAa6OCk4akRoIxANmMmHBJYEpCIkcJlyqcQFi/QCPe/Sk gidRjALiAQAafhgllrVjzwAAAABJRU5ErkJggg== `) TileImgs["letter-n"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAARklEQVQ4je2OOwoAMAhDk97/znYp QVSwS6f6JpV8BIYHUJOZASCp+ShIb1g5w6vzGhtCqo66FA3+h/BPYciKxtDypWG4YQPMpxIYUe3D 0QAAAABJRU5ErkJggg== `) TileImgs["letter-o"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAT0lEQVQ4jWNgGAU0AIxo/P///6NI M6IrQOGjqcaqDcGCq0aWxhRkwmMYVicxIZuEKY0J0G2gmQaIY3CFEj4b0PRgGkFBPGDVQ0y4jQJM AAB4jR4S7eFipgAAAABJRU5ErkJggg== `) TileImgs["letter-p"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAU0lEQVQ4jWNgGAU0AIxw1v///xkY GBgZGeFsqApGRnwasJuKpAe7BrgKuCBchAnTMBTzUN2DRQNBMAQ0oAUuZliTHA9YghW/CAt+8zDB EAhW6gMAcuoYKyywTUoAAAAASUVORK5CYII= `) TileImgs["letter-percent"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAbklEQVQ4je1RuxLAMAiSXP//l+nW GpRcMnSrk0/gMOLrwJORfLtAtxwRMep2LXNnTHRAi13vVwwkczkxOOxMO2RWExEJp9IZdWyrHTgI e+BEqktiovtMs+0+o7au8PJBu92yXVluK1I6x7b+sRM3utVW7yRjBK8AAAAASUVORK5CYII= `) TileImgs["letter-pipe"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIUlEQVQ4jWNgoAr4////////sUox kWrWqIZRDaMaRrgGACQhBiczCAYQAAAAAElFTkSuQmCC `) TileImgs["letter-player"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAe0lEQVQ4jc2TQRKAMAgDi9P/f7ke 6iAECD05coSEBapj/C4kptZaTiFOgwZQR48zqNoqdhI4TyFtD9grVZBw6Mi1vXd1xkKVYSPZ7QE7 K3VyFkIg0RtgmWQkESFv0hOqZco7MsKJB0dqPa9BZwUP/h6HBG2XXyB+cx/GDXe0RQgQbFivAAAA AElFTkSuQmCC `) TileImgs["letter-plus"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMElEQVQ4jWNgGAVQ8P///////2OV YiLVLNprYISzcDkaqo6RkUwbsIOhFUqjYIgCACGbDwS8lF+4AAAAAElFTkSuQmCC `) TileImgs["letter-portal"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAWElEQVQ4je2SMQ4AIAgDxf//uQ4O Ji1iVBYTGaG1R6SURwoAAHdkrnqMjQU1h0ejpgkkUs82kmPQRacGAuhOal4g9ZdinnWCUp1+XHA8 lGDaivEybumXVANG7Sz8egYphAAAAABJRU5ErkJggg== `) TileImgs["letter-q"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAX0lEQVQ4je1RQQ7AIAizxv9/uTuQ 6FZQ4wFP68kCLaSW8iMBEE7y0wasAiAQyLQo7dH8dO+FFnXmFNIhkEMX0A1pAjtmkdJ0w1tD0lsc /0MNq5363JrwbbLXYk0UHOMB6oshKB0gbnMAAAAASUVORK5CYII= `) TileImgs["letter-quote"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIElEQVQ4jWNgGBjw////////Y5Vi ItWsUQ2jYBQMLwAAzosGCaECG94AAAAASUVORK5CYII= `) TileImgs["letter-quotes"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIElEQVQ4jWNgoCv4////////8Ysw kWroqIZRMAqGFwAAaFEMAyr1H9UAAAAASUVORK5CYII= `) TileImgs["letter-r"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAP0lEQVQ4jWNgGAU0AIzInP///zMw MDAyMiJzoepggixYjUFWStgGTFORAXYbsCqFACaSVGPXgB+MSA2jgBgAAKdKDBzeBS9nAAAAAElF TkSuQmCC `) TileImgs["letter-rbrace"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUElEQVQ4je2TwQoAIAhDZ/T/v7xu UeYoiW69k6BTcQgksR6RnBJmSzEAFNXJ6TeQVAI5QfEFjvCysZ2uenQ9vZJs/52+EdQeHb6YnKB+ +j0NMF4bFZFcrnoAAAAASUVORK5CYII= `) TileImgs["letter-rbracket"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANklEQVQ4jWNgIBEwwln////HIs3I iCbCRKoNOMH///+x2kmyDaMaRjUMYw0scBbW7EIFG2gPAAKXDCGXzNPFAAAAAElFTkSuQmCC `) TileImgs["letter-rock"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAQElEQVQ4jWNgGIGAEVPo////CGlG dAWMuJSiKELSxkRQNRpgIqwE1SyiNJBsA+01YA8lzCDHrhlTCH/EjYIhCgDqYQ8SXsVRJwAAAABJ RU5ErkJggg== `) TileImgs["letter-rparen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAPUlEQVQ4jWNgoAT8//+foBomMvSg axhoPeh+IB+QYwNWPSQ7aURqwAlIiwc8aYkaTiIttdJSNQMZ+ZMmAACTqjjeE0bFngAAAABJRU5E rkJggg== `) TileImgs["letter-rquotes"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAN0lEQVQ4jWNgoCv4////////8Ysw IsshRBkZsYowMDAw0dxVlNlAjKfJVc2AGia4REbBKBhSAAA0wjvQeMkoywAAAABJRU5ErkJggg== `) TileImgs["letter-s"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUElEQVQ4je1PRw4AIAgD4v+/jBdj mI4Yb/TWdFAACh+AhjOzktEaFDfuMNa8W8pZxdBW8naSXx8EwhkmGdf48IxRVpwNI0mens7uXD9d OEEH4O4nBElOWW4AAAAASUVORK5CYII= `) TileImgs["letter-semicolon"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOElEQVQ4jWNgGAXkgv///////x+r FCNW1QhpRnQFTDR30iiAAxpHHKYiAhrw6xmQpEFyQqJysgMAMKwm8Ngar/8AAAAASUVORK5CYII= `) TileImgs["letter-simella"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAUElEQVQ4je1SQQ4AIAiS/v9nulVT c+XyFjcVUZwi1YBNkZxlaIKOV7bb025Xqm/ImrZeHW0gu9KJ/Bjy7kqOXUASpn2Q3EnUPx8CajDn I0YHe/QhFvreLOwAAAAASUVORK5CYII= `) TileImgs["letter-slash"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATUlEQVQ4jWNgGHjw////////k6+a ifruQRPBZwMJTsejAacNuIynnqdJswGPd6nhJPxpgUo24JFFt4Fg7FLsJNJsICa1UeAk0rIiDQEA yzM14X/tLloAAAAASUVORK5CYII= `) TileImgs["letter-space"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAEklEQVQ4jWNgGAWjYBSMgsEFAASY AAHKGPyHAAAAAElFTkSuQmCC `) TileImgs["letter-stone"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAIklEQVQ4jWNgGAWjYBQMYsAIof7/ /09AHSNUJRNt3UMXAADN0AMEvr/DMAAAAABJRU5ErkJggg== `) TileImgs["letter-sun"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAaklEQVQ4je1RywrAMAgzo///y+5g WW18QNlpsJyqibEYkY9AVVU1pa5TrzVQWRIFagHYaFcCWAP9EpMaRk9HC/AXAVIQe3yl0qxqvsgh 9Ys7B3GUQ7nhUct+KHv3FnPei3yZBBcRw/nR4wZrPkT7ECoX6wAAAABJRU5ErkJggg== `) TileImgs["letter-t"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAQ0lEQVQ4jWNgGArg////////xyXL RKpxtNfACGfhcTcDAwMjIyOZNmABQy6UBokG5EBjIUYRMmDEKopVAzyyRwFBAADJZRgJl37VyQAA AABJRU5ErkJggg== `) TileImgs["letter-tick"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAN0lEQVQ4jWNgGAWDATBiCv3//x8h zYiuAJ2PrBqrHiZSnYSiAdN4AhowXUxAAzGA5FAaBUMUAABNRgwMbG6kRQAAAABJRU5ErkJggg== `) TileImgs["letter-tilde"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANElEQVQ4jWNgGAWDDvyHAUxxOJsJ j05M1QwMDIxoxjAyMmJRxIhQxoRVAlkFMnsUjAIqAgAtMBr6apZkYwAAAABJRU5ErkJggg== `) TileImgs["letter-times"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAPklEQVQ4jWNgGAUkgv///xMUZEKT QJPGagS6eXBFyGw4YMTvBkZGLAoI2ENtGwj6gYBL8OnBJUc4ZEfBUAAA5vJHw8EB1xcAAAAASUVO RK5CYII= `) TileImgs["letter-u"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOElEQVQ4jWNgGAU0Bf///////z9B cSZSzR3VgAkwAxqfBqzRwoJfESMjI5oIE7IcmmZMkVFAJAAAthQYCKM54loAAAAASUVORK5CYII= `) TileImgs["letter-v"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAARUlEQVQ4jWNgGAW0A////////z9p srj0oIkzUdlhmCKU2QAxEg8Xuw1wRVjDgICTGBkZ0UXwuwpTA8WehluCJ+JHAUEAAMxjONnsXb+d AAAAAElFTkSuQmCC `) TileImgs["letter-vbar"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAJElEQVQ4jWNgwAb+//////9/rFJM WEXxgFENoxpGNYxqIA0AAFHYBil6UycHAAAAAElFTkSuQmCC `) TileImgs["letter-w"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATUlEQVQ4je2PMQ4AIAgDhfj/L9fN KAWMiW7cBvQItFZ8AgCAu4wReMUsNXLc9Cbw2F3hCDm+ICJRR48JQ18LPpo7L36IjrFC8ka+omAG g0kqA48OK3IAAAAASUVORK5CYII= `) TileImgs["letter-x"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAASUlEQVQ4je1QOQ4AIAij/v/PdWAx SComutGRXoBZ4ytIkqzPc06pd0WqHnpPAHGSlgjDebHXDXdHO61T1ZfcGUqwZqfxldpGwAR3ykfX q5fnOwAAAABJRU5ErkJggg== `) TileImgs["letter-y"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAXElEQVQ4je1SQQ4AIAjS/v9nOrRV K7Q565a3FBQxkR/vAgCAWNXiLPlyWdieyU1oLZ0nn9BB1IOVoKq+BL5D773zzaWdO3K0RQjbmr6D nD6izriRtc0lko6niEUFvNpE231dSPwAAAAASUVORK5CYII= `) TileImgs["letter-z"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAMElEQVQ4jWNgGAU0AIxw1v////Gp Y4SqZKKyA/7//4/f5lHVWMGAR9woYGBgYGAAAEc+L93Qbp4XAAAAAElFTkSuQmCC `) TileImgs["map-B"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAATUlEQVQYlZWPAQrA IAwDc///tKK2Lt2YWBCaMwkqXQ3YQmgYq+su5pl+0jQcEYLsWlo8O6LBkQOpkvqSJLwtQiWzg/75 AvQFDvr3ek8Dg3sAm2sbCqsAAAAASUVORK5CYII= `) TileImgs["map-C"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASGNyTmcAAAACMD8A AAACQE8AAAACUF8AAAACYG8AAAACcH8AAAACgI8AAAACkJ8AAAACoK8AAAACsL8AAAACwM8AAAAC 0N8AAAAC4O+6mu6bAAAAVElEQVQYlY2PSw4AIQxC4f6XnqhtKaymiT/EhwV+FW/ts+a6B98iASHQ KW+7qfCUw7xgJqIUplIvyPnheDpOAjpfloB69+NAMkZQwyu1hR5ywBxWH4Y7AL/6VD6kAAAAAElF TkSuQmCC `) TileImgs["map-D"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAT0lEQVQYlY3QQRLA MAgCQPj/p9PWxALJId7cUZwRuC0yu01oUiNU4BFMDN72A/ZChU6Y0wJVEFhW8CdKBGhC+I4EN5zu IkLySz2aofnqpwZyOQCksgmj8AAAAABJRU5ErkJggg== `) TileImgs["map-F"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAS0lEQVQYla2QMQ4A IAgDuf9/2kisFF0c7CQVLpSIXwLO+jCmZ235yJJmhAyWLkNoHCaa/kXrM8CzUVC0aHW0zD7r8f0G AtRR2KFvDW0UAKSPOsYmAAAAAElFTkSuQmCC `) TileImgs["map-G"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASklEQVQYlZWQ0QoA IAgDd///00GoLSmoPVhcak7pQdiVCEwFmBnEsQAcgSwgA1Bdd0Bmf4D6Kt87sDn6YO5pL9EdlJtH EOuz5ZoGfIsAnhiI8MEAAAAASUVORK5CYII= `) TileImgs["map-H"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASElEQVQYlZWOMQ4A IAwCuf9/2qFKW3VQBkMuSJE+BHSPJgiPEyxvIBLWRIRcWFsK0A6OBDkKdxaQw/CzgPUGniK+6j9t 6QU0DYojAMXtnf3uAAAAAElFTkSuQmCC `) TileImgs["map-L"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAS0lEQVQYla1Qyw4A IAiC///p5mviPNQh5mySCQa8ggatK5CZ1qKEZ0II1D3WmTPYMhzooVFNAhfCrVLlhixkt1+EGJf/ GE/M1zIUOFYwAHGB7EweAAAAAElFTkSuQmCC `) TileImgs["map-M"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAUUlEQVQYlY2QwQ4A IAhCff//082mKdYhVgcBh2r2C2DWwoAwFMoSv2pX6US8dwvRNATUmZnIkG2QHYrGymSEoy8jF+A4 6B0mxJUz1zfXxlUTC1Y6AHYOGVuXAAAAAElFTkSuQmCC `) TileImgs["map-N"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASElEQVQYla2QUQoA IAxC9f6XboxqZgV9JBEoe5EDHsV5SSKejBPSJD3FI4ecKIZj6IJ0BhL4Gx+C+hi1yVrPyu7BEbAN wVZaamTvAIkJdcwOAAAAAElFTkSuQmCC `) TileImgs["map-O"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASGNyTmcAAAACMD8A AAACQE8AAAACUF8AAAACYG8AAAACcH8AAAACgI8AAAACkJ8AAAACoK8AAAACsL8AAAACwM8AAAAC 0N8AAAAC4O+6mu6bAAAAUklEQVQYlY2QSxZAIQhCYf+b7vXxgDh5DsquQhnwM7gjzyIn5dsP+NJa msJddnW6evkZsztUVwJeQ2kEWAApsYeNDl0CAYRFAzmsv9wG7NL53QuM6ADJ7WnNNwAAAABJRU5E rkJggg== `) TileImgs["map-P"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASklEQVQYlZWPSQ4A IAgDmf9/2hhZCpzkILSxYTD7KZj9DbhztQ/Vka+AZNkGYrhImZlJtBMNMJwgMl1cTyDqgbL40+jg +xaYrK0ObFIAjWTC5T4AAAAASUVORK5CYII= `) TileImgs["map-S"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASGNyTmcAAAACMD8A AAACQE8AAAACUF8AAAACYG8AAAACcH8AAAACgI8AAAACkJ8AAAACoK8AAAACsL8AAAACwM8AAAAC 0N8AAAAC4O+6mu6bAAAAVklEQVQYlYVRAQ4AIQiCl/X/X1U2Uq5u2WyJSGbAsIa3kfQ4VmYjYInh hLJLT2ZFdtM8hqtSMpsjRmqz+O7NgZMREvYcOIA7gOxDZZ9OfyewiDbV8g8dsjIA3dTkXRUAAAAA SUVORK5CYII= `) TileImgs["map-T"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAABvSURBVHicrVFBDsAwCOr/P82SJetMQbTd ONUoCHWMHwFgo40b6cBsx4dTjAUeLK13Rm7nhY4w9aK8zlAa0x4iMgtilJmr3SJrXxt8mQ4nJZi/ SuULoU9+WpzMA+cRySRhewP4dh1LblrY8NpcnuMC9YMaA7fAzegAAAAASUVORK5CYII= `) TileImgs["map-V"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAABWSURBVHicY2CgHPxHBYRVExTBIo1JUsMG ZAlMs7Frg4uiacDnKpI9TZRLkKXRwp5AbCDLITNopgEeVgQ0oEUzgRSFaSqxSWNUAx7VuGKaKG1E FQIEAQC8fwEO7XQC3AAAAABJRU5ErkJggg== `) TileImgs["map-W"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAUUlEQVQYlZWQwQ3A MAgDffsvXYmCoeSTOh84R5aFdC3WBCYxEe91w8uFBBQ2IH3ntfbujApyBQprRm+g7lFl9KHI1bO2 gQbQBH2R+fUHOEKsB3lPAI4e7OcHAAAAAElFTkSuQmCC `) TileImgs["map-a"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAPElEQVQYlb2P0QoA IAwCd///01HUWCKjp3zz3AQjvooH0qX+GkkgPb4BQVA6pmGR/c1R9t8AhKCkB2bVACV2AGPFSnxk AAAAAElFTkSuQmCC `) TileImgs["map-asterisc"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAATklEQVQ4jWNgGK7g////uKSYSNWD UwNhG/A4A1mKBVOCkZERjxGMeAyDqmBEUYOuAU0PmmoGMjyNxXi4DXiCAbt7sOphwi9NrD2jYNAD AJfSLO2WOBYXAAAAAElFTkSuQmCC `) TileImgs["map-backslash"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAFElEQVQYlWNgGCyA kXHEiKALYAEAEiMAHHOq/sAAAAAASUVORK5CYII= `) TileImgs["map-c"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAABlSURBVHictZLBCgAgCEP7/5+uQxCBm0+h dogyp244xhPMC71s5sRszdkhmWGbnA93ZlNhhGUATSrOlEjR2itZUpubzGAJePlAAEPvBYlPi5hX XY0SwdkKtdsd3P5xH5ArZZQ4CRZQvlq04ec1mwAAAABJRU5ErkJggg== `) TileImgs["map-colon"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAKUlEQVQ4jWNgGAXkgv///////x+r FCNW1QhpRnQFTDR30vAFo/Ew/AAAdWUd7xfXyb8AAAAASUVORK5CYII= `) TileImgs["map-comma"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAOUlEQVQ4jWNgGAWjYISB//////// H6sUI1bVCGlGdAVMWMzAUERAA3492DWQDPB4mmLVDKihRAUAAAJ9F/ke+NToAAAAAElFTkSuQmCC `) TileImgs["map-door"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAAA3SURBVHicY2AYZuA/bkCaaix6MEXR2Oh6 MM3A5A53DVgDDYsGEqKCTBvo6OkhrIHkeCBBA/EAAN/jk3veGNYsAAAAAElFTkSuQmCC `) TileImgs["map-dreaming"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAALklEQVQYlWNgGHaA EQTQuMhC6AJgFiOUxiWAqgdIQ3jIWmAYyRAGPNZiuhQZAAAmvwBXc8jX/AAAAABJRU5ErkJggg== `) TileImgs["map-fog"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAE0lEQVQYlWNgGMKA EQUMbTPQAAAZTQAr7fn/aAAAAABJRU5ErkJggg== `) TileImgs["map-foliage"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAQ0lEQVQYldWOyREA IAgDs/03rcONHcjwcGMISB8V3vjTlUvGtAWac0iD0TZYyEyenR+2J4aIFLS3Qp3h0EJhO15l1QEo hwBcyClAXwAAAABJRU5ErkJggg== `) TileImgs["map-footsteps"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAALElEQVQYlWNgGLyA EQjQaRALRpMjwIAmgTCdAYkGq8DlHGQzGNAEkBRT5ncAQvAAYmbxeFUAAAAASUVORK5CYII= `) TileImgs["map-frontier"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAHElEQVQYlWNgGFyA kRGND0YYYiTxsdhCoqvoDwAIsgAJrHNYDAAAAABJRU5ErkJggg== `) TileImgs["map-g"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAATklEQVQYlZ2QSQ4A IAgDmf9/2ogLrXqyJgZHoGLEt4AK+5ZLAQGWLCUjVc4dkCoARuAGCXefafIAy7tAiNG6MCOf9QT+ +Dfw+W6Af4moAVkPAHV0XLSkAAAAAElFTkSuQmCC `) TileImgs["map-ground"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAEklEQVQYlWNgGImA kZGQwEADAAP8AAV3EZSrAAAAAElFTkSuQmCC `) TileImgs["map-h"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASklEQVQYlbWP0QoA IAgDd///01GxWkmPCYKeU6b0KeDuC3j0ONkTDH3LYFZdsQYMPQfwRhIViU6FPWMWTlFu2EH8dHt2 nW9TyIoGT1YAfBSNWpAAAAAASUVORK5CYII= `) TileImgs["map-hbar"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAE0lEQVQYlWNgGE6A EQVgExgqAAAWDAAdNVYEsQAAAABJRU5ErkJggg== `) TileImgs["map-hit"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAK0lEQVQYlWNgGADA iM5nJJ7PiMJnhCBGVKUo+hkJmA/joFmCooURm6tpCgAZZAAg0/s5EwAAAABJRU5ErkJggg== `) TileImgs["map-kill"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASUlEQVQYlZ2QSQoA MAgDk/9/uuCu2Is5FDu4RIGjKBrfJGQn5CALUOrhD6AA6WKPAq8qJTbBnEUPt5qWcxfJYAdY1p33 uBxy1wN1jQCCDA1T3gAAAABJRU5ErkJggg== `) TileImgs["map-lbracket"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAANElEQVQYlWNgGFqA EQKQ+XCSAYmFRMNUQEQYkQEuAahyZMOQ+UiaGHAIoJmJzRYkYzH9CQA8vwCAGeZqKAAAAABJRU5E rkJggg== `) TileImgs["map-lparen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAK0lEQVQYlWNgGJaA kZERQwSNz8CIxkeXJ4LPiM5H2ALjw0RgFBof02AIAAAMAQAdmIcn/gAAAABJRU5ErkJggg== `) TileImgs["map-m"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAPklEQVQYlWNgoB5g BAJUPgQjBOAE8QKMqKZC2EgijDCAzkcIQMTgxjIyoArAFCAEGNAEEKYS4uNUALOTWAAASU0AcjXl 29kAAAAASUVORK5CYII= `) TileImgs["map-magic"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAK0lEQVQYlWNgGMSA ERuDkRGJD2UyouhiZESRZ0SisAlgaME0FN1aTAa1AQAW2AAhCnx2aAAAAABJRU5ErkJggg== `) TileImgs["map-n"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASklEQVQYlaVPWwoA IAzS+186aE9X9NMgNp3iAr6K3rJTmI0ZK8MhsZ29Ymw+iMqbVGoouF9Cj1LHzSIKzBAhMK+AMP4N qgJPT68FbZkAka5pbUAAAAAASUVORK5CYII= `) TileImgs["map-notile"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAALklEQVQYlWNgIBIw AgE6H1kEzEESgTIRIjAGI4JBrAya1STzGdD5dBLAAMRrAQBBhwBFq/1ziwAAAABJRU5ErkJggg== `) TileImgs["map-percent"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAKUlEQVQYlWNgGJSA ERefEZ3PyIimHp2P3zw8fEZ0PiMjmnp0Pm7zSAYAEBQAGODrJooAAAAASUVORK5CYII= `) TileImgs["map-player"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAM0lEQVQYlWNgoBNg RALofLAIRBimmHgBqH64AMxEJAEIgzIBdJei+gXDs3h8j+l/LMEFAFrWAI/HkedyAAAAAElFTkSu QmCC `) TileImgs["map-portal"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAABSSURBVHic7VFBDgAgCPL/n65DzS1lRHaV E1MRKbNGCePEfVQp7gbfxUqL5wqYVghez3l0zCdhAd+NHbznr8kyVBzeMui8+g85upoBIgoUfAh0 TAyfEvz9iiM2AAAAAElFTkSuQmCC `) TileImgs["map-potion"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAP0lEQVQYlcWOQQ4A IQwCmf9/eqNmE0DjVS60k9JWeimGrkDdb6AjkASSQBIo8pcFtJBHFBPTlVslu+t2+tT1AUTpAH7I /GBjAAAAAElFTkSuQmCC `) TileImgs["map-rbrace"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAI0lEQVQYlWNgoCFg ZGTEFMIQoYYAuggRAiTbgmYEFs9REQAAE6QAGGXJnV8AAAAASUVORK5CYII= `) TileImgs["map-rbracket"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAJElEQVQYlWNgGMSA kRGNwYgAOAVABKlaIJpobAsjCo2kBJvXAUHNAHzsf7j3AAAAAElFTkSuQmCC `) TileImgs["map-rock"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAK0lEQVQYlWNgGNyA EQjQ+cgijIxoImAOmgADCKFqYUA1hIEB0xqq+YEcAAAjrQAv/A1zzAAAAABJRU5ErkJggg== `) TileImgs["map-rparen"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAN0lEQVQYlc2OsQ0A MAjD8P9Pd6kq4jxQJmwRkZmfBjOYw5hX4mnzXSNGvaarVFUKqHMsVh+ZmAMaNQArAzxTjgAAAABJ RU5ErkJggg== `) TileImgs["map-s"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAT0lEQVQYlY2QQQ4A IAjD6P8/bdDIQKKRi1DJNjX7KFrrB6QrfHYiwKwbIAOkIBUQmF5ayTmqbdKo5JxFzqQsZFu1Beu2 a49o40Fv0L83agByFgCNKWZ45gAAAABJRU5ErkJggg== `) TileImgs["map-semicolon"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAHklEQVQYlWNgGGSA kZGRVAEyzAAKkSyAZgZhZ1ACABIoABhHN9hGAAAAAElFTkSuQmCC `) TileImgs["map-simella"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAKUlEQVQYlWNgGGqA kZERnY8qQlgA1QwQB4bhymGAOAEMM4hyKbIB6AAALEcAXscWfmcAAAAASUVORK5CYII= `) TileImgs["map-slash"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAEklEQVQYlWNgGJqA kXF48dEBAA6LABvcG2Q2AAAAAElFTkSuQmCC `) TileImgs["map-space"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAADklEQVQYlWNgGAWD EQAAAZgAAYLRFi4AAAAASUVORK5CYII= `) TileImgs["map-stairs"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAALUlEQVQYlWNgIAwY UQBWATCCqcYlAFGLogJTAKwIVQWmAIq1WFUMNgGCQYgGAHjfAJ9Wi0gEAAAAAElFTkSuQmCC `) TileImgs["map-stone"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAJ0lEQVQYlWNgGAVk AEYgABFwPlgEJAaXh4gxQNVA+FCIpANiEiYAAArLACpwD1v5AAAAAElFTkSuQmCC `) TileImgs["map-sun"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAATElEQVQYlZ2QUQoA IAhD9+5/6Qgxl/aViKBuuCl9B6d4pS3paJxPpJMcsldgbfQHTw1IDRdiUHxQVxOASkxXhOtCbzPT 7njIfJn7zVhFQQBdElvN0gAAAABJRU5ErkJggg== `) TileImgs["map-t"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAA3NCSVQICAjb4U/gAAAAGnRFWHRT b2Z0d2FyZQBUayBUb29sa2l0IHY4LjYuOK3Fod8AAABRSURBVHic7ZArDgAgDEO5/6WLwiyvdCQk GCow/bIxPt5C0pn6nkELVl248kIE5lk1KjA+e7J6A3uZLG39Eg3YZg1umy3BJaEEOXuiMqxDdTEB XEkY9sn7bvgAAAAASUVORK5CYII= `) TileImgs["map-tick"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAN0lEQVQ4jWNgGAWDATBiCv3//x8h zYiuAJ2PrBqrHiZSnYSiAdN4AhowXUxAAzGA5FAaBUMUAABNRgwMbG6kRQAAAABJRU5ErkJggg== `) TileImgs["map-tilde"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAANElEQVQ4jWNgGAWDDvyHAUxxOJsJ j05M1QwMDIxoxjAyMmJRxIhQxoRVAlkFMnsUjAIqAgAtMBr6apZkYwAAAABJRU5ErkJggg== `) TileImgs["map-times"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAIAAAB8wupbAAAAPklEQVQ4jWNgGAUkgv///xMUZEKT QJPGagS6eXBFyGw4YMTvBkZGLAoI2ENtGwj6gYBL8OnBJUc4ZEfBUAAA5vJHw8EB1xcAAAAASUVO RK5CYII= `) TileImgs["map-vbar"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAEklEQVQYlWNgoCdg ZBwVIA4AABbYACF/17hFAAAAAElFTkSuQmCC `) TileImgs["map-w"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAQklEQVQYlY2OAQoA IAgDt/9/uihMcxmOIO+aENANKaJjPg2u1IyEws8Cw20DmV7O8cakuOMrNm6Jy5QCIpA5fksyAFxp AJlAkBHgAAAAAElFTkSuQmCC `) TileImgs["map-wall"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAVElEQVQYlY2QUQoA IAhD5/0vHanp1IL8kHy4tQI+SnZZl5gPEAe5yjLteg4ZjoSM2jbI1ARpCnfLIB6F5gvAAEDN7Kax S4+jyzgpgySvDypg/GmrBZ1IANWOroY0AAAAAElFTkSuQmCC `) TileImgs["map-wall2"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAPElEQVQYlWNgIAIw IgF0PlgEKs4AZwIJqAgjA0wAIsLIgBBggAoiBJDJQSWA7lJ0v6D7FiM88IUhliAHAHczAJvEmIc9 AAAAAElFTkSuQmCC `) TileImgs["map-wall3"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASUlEQVQYlY2PUQ4A IAhC5f6Xbs0ywLbio+StFCP+hCn3hAAlgBIpzo2s1pei1jbfcd+WA+47QHkOtCfuo0oJ8pji297X Z9KAaQCQBADMjKNixQAAAABJRU5ErkJggg== `) TileImgs["map-y"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8A/wD/+wCC/wAA /wAA/30A//8Agv8AAP95AP/PAP//ANf/AIL/AAD/fQDLmkWWPBhhAAD/94LD/4KC/4KC/76C//+C w/+Cgv+mgv/Pgv/7gv//gsP/goL/noKCAACCHACCPACCUQCCZQCCeQBBggAAggAAgjwAgoIAQYIA AIIoAIJNAIJ5AIKCAEEAAAAQEBAgICAwMDBFRUVVVVVlZWV1dXWGhoaampqqqqq6urrLy8vf39/v 7+////9NAABZAABxAACGAACeAAC2AADPAADnAAD/AAD/HBz/NDT/UVH/bW3/ior/oqL/vr5NJABV KABtNACGPACeSQC2WQDPZQDncQD/fQD/jhz/mjT/plH/sm3/vob/z6L/375NSQBZUQBxaQCGggCe lgC2rgDPxwDn4wD//wD//xz/+zT/+1H/923/+4b/+6L/+74ATQAAYQAAeQAAjgAApgAAugAA0wAA 6wAA/wAc/xw4/zRV/1Fx/22K/4am/6LD/74AQUEAWVkAcXEAhoYAnp4AtrYAz88A5+cA//9Z//t1 //uK//+e//u6///L///b//8AIEEALFkAOHEARYYAUZ4AXbYAac8AdecAgv8cjv80nv9Rqv9tuv+K y/+i1/++4/8AAE0AAGUABHkABI4ABKYAAL4AANMAAOsAAP8cJP80PP9RXf9tef+Kkv+iqv++x/8k AE0wAGVBAIJNAJpZALJlAMtxAOd5AP+CAP+OHP+WNP+mUf+ubf++hv/Lov/bvv9JAE1fAGN1AHqL AJChAKe3AL3NANTjAOvmF+3qL/DtR/LxX/X0dvf4jvr7pvz/vv8gAAAsAAA4BARJDAhVFBBhIBhx KCR9OCyGRTiaWU2qbV26gnXLmorfsqLvz77/698gIAA8PABRTQBlWQh5ZQyObRSieRy2fSi+gjjH jk3PlmHbpnXjso7rw6b308P/69//HBz/HBz/HBz/HBz/HBz/HBz/HBysfHz/HBz/HBz/HBz/HBwA AABXYnq0tLRtbW1fGku6AAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAAT0lEQVQYla2P2xKA QAhCOf//001pCtVjjLMXBNyV/gL0fl+IBqZgFq1nVQxxnilcFkdlTJ8OehLpyIxvonJzrJQe09j/ 6z1DdBGCF+EZiwNuTgClUAh0wwAAAABJRU5ErkJggg== `) TileImgs["map-z"] = []byte(`iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAADAFBMVEUAAAD///8AAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABXYnq0tLT///9dIegXAAAAD3RFWHRTb2Z0d2FyZQBHcmFmeDKgolNqAAAASklEQVQYlY2O2wrA MAxC9f9/umOkzphS5kPAY27AD5EDsEXuA6gjhqu8ZgOWOsAHrEfjD6pIAAK6oI9oSm87bgADID0O OeyfCUILi8AAzR92UQUAAAAASUVORK5CYII= `) } boohu-0.13.0/img/000077500000000000000000000000001356500202200134565ustar00rootroot00000000000000boohu-0.13.0/img/dragons.png000066400000000000000000001303021356500202200156200ustar00rootroot00000000000000PNG  IHDR5 pHYs  tIME  xKO IDATx{X׹FtI$.0c6c;Ď4'ubN'ilKJJ.]Zr墤I)e>}!t7oy!g7ʀD&ヮL&Ӂ8l6766+7eeejðᦦ"a+H^x2t…/vtt59|wO?tٲe7olnnFeddTUUegg9\tl6#Qm۶XlԂbϝ;7<}ҥ===!Bhllle333 wU]׫jp8p/,,X,d]QQ! 1 Caa~n9tEh/~k~rxBaqW^)%}_3q6ʘ(ш??hXv 6еZ: h慅7n_Pڵ+;;;hs֮][UUt-HTSSdɒ:644LMM\/rdddӦMSSSd}ھ맞zl6_>99NOOw;00@[njGW.x衇-[_rff&|zz]Bٳg;;;~ag}xf吝k׮K.]v-2{yyyWWn'LMM˻~t: ].i,mFx+e#vkO|:.7,++7tfbîbw'M VY]q|TnFFwk׮k3BdICCh`.˟y晞>Nݻ^"ܜ>77W @Hl6r- tdpp00_v K. wĄV]2CX<===BرcGkk{ӓsWe2ͻ+|r[ͧ7oj49.=Iݏpd{_}RJaSЙ_^weͻ[nÆ s+344tرS|u urȑ#O>FN=g?Y|CdJKKkkk ɓ'" vANV`08N FGG޲>ժ299B:,Eo~?ѻ>ZfM#^ߞ=U)%' o2 dIRWb!`a>.uk4??rCvg-?x2L{>''fQ R);Rh4&IUVV(JP(JB:e[[iO(syC~.ghhkh~ 忯]| 'M V"X 3333^lYff뭩q8 Xzuaau---6lؾ}ŋ=ڵkr搟zܹ=gI q;y"4t ˪wEaJ'J{DŇ#].WWWץKf-G9|mmO>9===::zUйs~<033c6Ϟ=sJjׯk4;v۷oSy ݾvں:rih[[ۂ ].|vv|o޼… 5rIknf|>PHv;uFdݶm[{{ ⛰ÃZArgT*dzyzw= X' ؂w0+@;@"UxOZZS8}+**bqZ&mذ$tFCKc]K"V\)Op7<()Wh~$m$Z_l޳gOFFF̙V^^^}}Z]vϞ=w_А}>iLkbu)U/\YlY{ hu~V㽽uuu "J---'NZҺݻw+YIBKcZ \=bX(:NYmhJ OOMss3tR,6Ѭ=I H:ssy<{'VJ(n߾}hhˁz}KKˡCΞ=;7Uhij#ð"٬RD"ؘF÷$"99Y,8+)GWQJJNLLeӗref7OlFՙC]̴VVVI"JKKsrrJT*|f|r??LVc6<<4kKq?wܬ}YTL8]׫jp8p/,,X,V***B!a! 0 06Nqf{ C~X BaqW^ !bCv {? a\ WuT k׮K.]v7n_l߾=??wfCmܸQ$Qk׮jjjuH$YdO䡇Zl/˙ihh0LG CXm@>.NwVYXXr8B?/_yNo>J Jo3qec7OlFՙy cwD /ܾ}̙3tOJJJJJr݁ѰhO)z!XGt+W8q \.gzzzpJO777777)x<q }H辫*^$C$EJ9N@R, 0}INxo説,/boذD3纚Fd2|>Nݻ^"n0|۶mk֬9y$#rssAOOu197OwXLV%z o]T=ZmP͛75 '83wǎ]8y2tŏj!ݽ9У,+ž&OlW=U#U.y}}}Ǐs+**:;;_>>>zdTzȑ#f:nݺ 6shhرctJ߷onC\d}Hh^gZ q`ttYV^/I$'Rw}z͚#Gy={TBʞ3eW^$[֞hu扮R)B(pCh.uij6}.555===555^/y,?oood@Ȟ1,VJRFN'<όffIBn;+#aHBT*%_P(CCWR˹!4W_} aH].(cWжmeIBK`AI4koD eڹ+ヮ*3 v `7@pwtt޽O{{{=OAAAرիr9K{-VAtttvH{D" ORBv^@ q|nMHk/a+HHi8.fGfZ JY;~#‟)\YtzܹC~^jM&VzmmoB_|Ů]l_"\.Wcc;jkkoܸabqffZr ;::V^]XX jb#e8{9|PZqZ/boذD3h4_=wffffٳgwܙ]YYLNn O VZj*rE`k׮n{ttfSSS}Bفʇ2F@]U wUG$jjj꣏>:>>~q 1X,0O>o1!pEqqO?wĉ UncFGG닊\Q# T>}Z.6 vsn[D0Ԏm 6-3;07@d wJ 455A~ƍ᭗c5_~ :_A{DD}ͥLGe^K'ÆE .Ljժq-..V(|M|nbwO|YהLLLƬ,ZFJ rǔ?d'JЃ;~n,=EZMOOS1tRDXTz뭰ەwƎdffƍ|>_OOŋ].%5EEEVOfs:ebOɃwwA}$`_:+aϮb#(;p@Df#V[ZZN8aZɓs8ǬGy$pj=~xooo]]BH]PUAГD?.BDY&R>\iz0+aϮb#(BpCCC/_yZ.--ܹs{WZ3aXQQlVT"hzzzllLtvv@4CлBp:A$''Y2JЃ1=~\Y{voJ6lXlT*|Nh4^x\vERlْiZϜ9c߯co1z~~~YYZ0lxxiddNRSS7nܨj'''ۮbϝ;7%Kv!(((riff255u||ORͽ2??ڵk #;;;`n ^p9A477wVYXXr HHB $߈rreeeMNNrn߾jժYѭ\.O%V53־Xr|ׯ_Gy</>ӥ͡Ӯ[N":u()_'4nuj BR$SSSYq9x龫*^$C$T*LJX,+Hd(W*v뮻Ҧ> BhdddllBL.rҝwqZ$xP}t:V%Ƀk"n޼h–Nz!ʚ\ ܸeO.+++..޺u+Bp]v-h8($ B6N;::yX,O%Þo<ӧryJJ $ Ģۨ0 l6D vbwU@!TUUU]]aBhgٖ|&Z\:nIKKۿ?bwRRRtRPVVʕ+&z @|>L0R^ض::㈽*\_n2bxu)ȱ;u~ƆFJ4^VGg3^˭QCP(  X, N!$gaٖHa78{9U+e;aEEEfYRD鱱1Fٙ6N dXRkokkk>6t\i#l2_[N`aw{yyyWWn'LMM˻~zZ[[Ngaabwap5RXm/COm$MW._[@ԠH$"x8HȊS*>t JeX#)|)aיd\D12ywR:NݼySѰ$dE0o=F {흜 dž.+m$M3Cۀs 3̻ )쵗 ?hޛI19"x^bw :::vhNͽR"'n vT8>w+QhKCWHa}lPDLIU0,x@"^vGG 0RLm=)Ic3pUg^$=Cj?7رoyYu@ DzgL[Ѹ~ICPRKc\>]]!/щݩw6m=)Ic3pUgqS u<&7R66g}X{#g#R6)gkb})wnnf`}M=)CdO >z!q|{i,zj{pQJ[: 1 S>e=cKs.)M"jnkb Dk"٬RD"ؘF 0IDrrX,v8X.@.N$ˎNNÒM'8=4=WufaƇ=I.wOA֢rӭ`}7j+|VG;*&!?S1`hEBHk }^Wp8^XXhXz{{S)VQQ! V0 0oBH[[[c܁a$/nG  ~P(,.^ksNνq6ؓ29 \iآnqS?dיS=䚙u.)cbweƮ|*ӌ#s#g|ؒ3 F^^^e?SSS_ϪVYXXrhJc\?/_yNo>J g&ٓρ^H^˙ۢc'I"jh\CƌoFI_YhRIsDžԱ>'htX>+[Ɠz)3ZE7k 慠D"rDRN@ PT4'tYƍ їǷr")|z9un|YS~t[6+pG] YwP=㣽b jךc ĪcSI_) ?y޼RϏywNjn޼hBH'''c\wϱcNl*񏿧Ն?yw&6ɞǟρ^o^JEaL:OxP[ !&;) PĿi]׫T*t:q/(($eee}}}VU n*bk9rkf${R9 `9s] d W{@MS9Ф$/H͞.Zgp}fJF1:N&y<rFBn;(1$ R|\P ݳMh)"R.m!F꫏4 Bvu mV dO >zCΫEEaw0 <9!XiY& >#;Avh4R'^)HBCRBvpU)HDVvw32p\̷;rCQabI@/pyտbHl;^5Hu@ֹw9{!α@|$^^vӗb}/1DVw{${R3Bv~XNr&םfm yƃZjU:uw,iU^D=dʞy!&]1ޝy;@ֻ bw vb`;;D0@d@j BWJ@l3#姯cܴ,]!rJ^%!32?uM]E/5,öŲ'ebd3?.tޝ{(WQmomJULt=?ݶ ~HJ556nI~C--`}7֭?۰-)ٝ `؝6G˨ut'eR+~*U?mN?p΋{;)~5-bw,=)e/g {giu+t\Cu< .^!fU`<-B/Pcˍ/75WNn990/Z\Vr? XJ$R@pBP(tNX, N!$Îg 3&R&b~֊ .?pi'eu=q@R!ǝx#kIX,{9s5k/?O3]wʕgT@3jJZ!"vBxvac'nm3$:aVTTd6U*H$h4p:A$''bA?֡ :Ö2B<`佺UIY_H{n[x}wm)?Y;{˞Ų3Wc8{w:\:~F>s+&&B*Uiෳܓ۶| jB"._ D1!}(uz^V ÁxaabeZ/)*څ 5uycרcrV~}P Hu.0x0qwuuvԼׯGp:B500s{wTiҟwgT^bB1wm{Rjʕ&c6@Woo,C$ÎoptB*Պ{R$55 wxL6l!B_,{c7hHz8_n|F@۶D/!"U RN@ PTZFW彝(Q_p?ML"|~$K#3( $`mI٫?WE]񿽱xJf2%Zrl}۶iӿ?l2]#._~7! )9YZgfcII҈ `޼Rg>ZmP͛7'''9QJՊ$#m|?ͩ__eʴ|??ͳV#+ryMλ FÒ419̕4tw+JZ TYOccYY/DMAIJooVk͛?8bw^R q`ttzw>ժAww7T853IDꭿy,_nW'K(Ʊ}/^|Lh`O^9Bd,{(W3ؽ.'gÁ:K5ߗ2~DINnFGmi|>3JRFN'<όffIBn;K!ABP*/k+Os84ke"_?hR_}1>H}zՁeee໪f|wN& $Oy":҈{Rjq^{Ygέ.At"4>:-t̂}xڊ] #l6aۍF#urlll$D>A v38>ߎKs84ke"a+_=t|'Eu="'wn`̛ٛ.ٳ|nMim=){߈b3V EkWBlLPJM22r5;⮻mofɲ s:FE!vH}2^v@8!jn=wÇZRmT$I߫'m=){XJh+7V\i;BH&˜@LM]ڰTPN mnGQ"q_߹٬`Bw;ĻqT;@, q"+bw`_'Boqdܪj BWJ4ЁI(b)v/))Yt)B(++kʕL$X$qӧ".\7F0mPm!YSO~A5 dbCRl\z @|> M)-'-<FG0WR}In/^N}x:TZ&9"񧍮.c~RH *-&Bw O=0_o͐BJd(!t䝃:z_OgˉEmQ.Q^^2Ljv{/BP( Bt"r9eae3{(6x<>5 #v1,Iዪx~_ch)5-Kp\BN~YwYRZN,ZlXK2eщU+ (l)Z^It.0l6T*H4===66h:;;gEN dXp8e3{Lq-V<˨e%% '&FG#~i/~w|S$chG8.*BzWBHyk9h7cQ,p]+ (l)Z^ZQڇB_j`p88Z,^2ҭ !Aav{kkki9tJ f܁Yj_ߏA~PX\;+-mD܋Pyv/,\Pɹ7,I+7 Ooiz!m4ts眖V__[˻v;gjjj^^gYvkk,,,t\2ə=i|ba(~+u:]o~YVVnHҹ{o7ZYΕ0i2Ѵ߶^reQ"\Ep8D"QJ9N@R,KD2ə=) 'tYƍʒH]t~?&`ĢEm@/ķm׿\$D9ywROCt:V4͛7'''Nh9tJY6.yc.<TOՄy{WV\oCɼ;WäVcp &˞ϡ3̻[#c0߶^reMw׻t`XD vbwH0@W ox7yw *DV MMMA^Z.߸qcjjtkU=WeI?zg~{zxcovI^C=ϐh^FsHHo>Ot.~1 [2bw %%%F1++KV+<; B> l{&=ûKu޾}{^^޹s纺*++WZ`!)))˖-op8d2ƍ7mD_M^jժ-[\v/t\999wk2v333?梢DrU iA, BӉh)*R5!\}^)`gkF@3\.&r K"Kh2:XU2G\,yV ]:/_^ruwwlh IDAT6>ߗgffgff&&&Ν;Zl)UT7oq˗!/F/ | \!M`ޝdIDrrX,v8HzRKYiႳ\scwG$RR&-ZL+)?`,9jv';KU¤y`%%%AQg>LQUU2L.]243OMMݸqVMNN z f#GPd2$j*Ь%4yP\\?1 KJJ 7n܀ZQڇ@P7UTTBr  0 v-ZqgyC*"ʜ(늽XWR=mW+~je̞oCawޙ *MOOw;00@moݺU9rjΊ羄*vmZϝ;711!J}ٹSj@p1˅ ӟ֯_d Æ?u޻w#Cy vJKK;;;O:0Uhnn&@kk,,,t\VǕ4P~Zu̞oŜBdttt>iee@ hll$9{޽{+**脹֭H$N"+>grss~al68q Br^xɓ۷oߵk>^"X,@ ǯ\RZZ*H`dֻsR|NS T*҈iJAW`?ѷ:D\ hgl#HW3K.jtrtv}=.>|Ǐ/}}}x?̮]LoX??y`&A߿l6G/cNJJs=7>>!ϻ?݋3͗/_FI$^xD޲>ժd)%mhti)~iVi,j#W/=9#L&SAAL& tvE"QXL G(a!Yc?- uLBLn?22IO`nC(JRrٙB/(//'tЗoH_BHI}Ǣ) Z+`.h3cb:HٳD3kŕe{84JDF&johZ[[ ׬Y4W:88d2B52;7LZjZ Rz} #&&&/YzD"O?SRR cK.5rr_o<W"Eb="Nc̒(@fH8!D"5`HD p  ` vHJ;{^|g?S@7oVUUUWWcZzuuuT*Hsf"eR.Wuf+7"KZZ_OvvYrg2io[[h#"`\&RzJ\$rNKK_RtQ_kVUU555ݺuK$,Y'l6tjtg׮]A`ۧxǏ3C7w*A477^t\PNQ,umDDIj2|;?d,<. q\._~}wwBŋO?tiiiss3yBӟ466rW\y%:O?j"Vkff iX+/΀wU9@T|>)T*bSb3h#:-bR.WugsD \0 A YtFFul6;Bh||!F*ų.8|ÇgMkfNjAE7oޜVf&R}e^Fڈ6+^$Wz?ɷ#ff޽_n݆ &:vܴ`1y&! X3f&YV^/wJ8XumDDD+Mrg𓱞sPd2䋪6y{{;9  BP*(x#nwkؓ!F꫏-V&u檏"6bynaԙ1):Gyeϱgh${Vs{^f Adx h> Q$`#&&&` Bp[8ϷÔD" {akR&03W}I&sk쵈I\9"+{E=sE'ٳ:ڳL&[bw}5rxظcǎ7nvXV\H.)I"xWL4O90fgg744Xƶ6`׮][WW'nhiii{.ҥKAPׯk4;v۷o*wn}ŋ.44LFw #mLPH80LQy07E9=:3r+c!vkD:ǟodo=]E|w5L555V"s8ǬGyZZ&i!vObP(t:)?bw)5.Kp\B B(tsT쾨:3r+c!FkD:ǟodo=]E%"?4hE.3=:3r+c!FkD:ǟodo=]EiIvq 0606B#e$aZb15ͷ`2o xSeOx7ߔY3gHD8P,RUWWGtww'|d!mmmw!W~}ϟ{'D.-T#dkniZ-EQED"D4)FJZajjt}+#JU8k#}JHQo[r%>ottL:u޽{7(oN?| _ܜCDbssܹsvW_{[oEǽ{ݫy~mnn*\Ko~3'?K/̜={ԩS4M?~ff_f@\UrG?5*Mɶ5%Ą8 D"zzz"j2+Zs Z*ze6_6\ 7RTV\/CB(<oX,Fb{_ommMRKKK 7o> /W#F !6_~˗sdz_fv_m8!drjj?=3?}/}K,$s_|qvvw7駇\.G}422B5*Mɶ5{fjj~~~~~~pppaa!aͶ1jySTQzK^ժJHR2RV\r(ʕ+W\a& >SGt钪}AgǧpBVWW !όNMM=3B_~uz333#3*MɶXMn[p:˲!$ɔxzQjDk$]f%QsU+s,TU鱺*v#`UFJσZJfK7 k_q͛7ܹo;lQBFnqFgg֫/Y-wNg!===PGt5-Ųl&0NO?HHJJ{ڵ(tdnuyn5OD9F,z=V2?)m^Tm9[z;ŋ_z饯|+.\x^o_|;!d޽_ٻw߷oߺn{wVlycǎ8·~ᇥǨ_reccСC_׋b,{K(ӧ'&& P!R6BHkkk < BJUrjSTΞj%el[=o~VѶFFWIk}re^X\/i+P{+ʢrjSTΞj%el[=o~VѶFFWIk}re^X,Ni!G5gZAI+V6s*Ȉ5_*)z^.Tp'nw:d->o|34eW~GwDt=k J\񶪶{dVѰFFWIk}re^F65<Ĭ5g}+0V#̢Ej}r%gXC ~w x|>qfh5g}+0V#̢F? s;>R( eCTd5 :ze6_FjdZmU2O*i}r%XB, 6mnn5g}+0V#̢F?JΞvsfji-~8e{ʿk"d2QqdWZճ^R_ׯ5Mt>ʴEuՂW\ ˲LF|0;=arIDI5Y_iUφzeJ}գ^BT=45(jU W_re^ƅϪj0G0 L5g}+0V#̢F? s;w5XVvԺʆ&-m6o0'Nttt,..BxժTF̆=k0'w鉉 T|h_#< |>/"3^}>z٨F9 *zΆuږJݡJ_V6gev;EիU/HQ0ǼYE0bT}NR鳾;t4q!hrv)ʲ,˲N^Q9.޲F *͆: K!Z]8Nt:Z|ހghʮF)]]]#ͮ]r+]*eCy IDAT:Ut #F74uJkni(B EQU,D$gpssl2d'5h}ׂ`ycjjR}RI6nA>gBJ%>otttll `nsO$8'NOiЭ[n% 񟍍W^5b}ׂ`yj}RI6ggӶXC ~w x|>qfhTv8}d2p8dFX_ ~ްZT #F7V_\wP( ˆ&''S>ʺ֨Ak@R}RI6nA>gBJ%ϸXB, 6mnnN~vv8aį_zDԈ5_ I%0bT}CNb5Mn[p:˲444Dd2e?^$^o X^^B \N|L<O$QlTF^Uu:Ut #F79Z;˲LF|0;=arIDIq 0==H$K/ˉʯ4dZ5'¨tl1ϾϙjݡF3m hm0F)N>=11TBZ[[+ZyWdFիU#b9dt73>S)o v#JݡJ_VA~TjsٳNQqX+ ԾF[󂶽݈g}kwwYewzQIt4q!ljVTv]_Rl.3ֲ JU23hmѤF yAnR鳾`5uH(]]]#ͮѭ[AN3Lʏ*Y>CSS6v}Kڽl 7_%3&QMj4F,> [ed20LOOO4W(jxxi! PEQTXL$SSSH$blHWޮdf*k9|e6_󍎎aJkmhh֭[DBgcccggիWGK@$8'N%5\6Y#UWv%3VQ#>kd2Tr;w/p7ɤJzǯX|g\w5~,q È_X,m6CKG }@%JzA[A52_W*9{jϙ)P(АgD"$JivNgY޽z$-"d22k$MI} }(d*h^ yA^cJAN$˥וG1dG1 S$%.KZfC~)ouo#E_%3VQ}kTA}5?/kL\鳾;Yo[($J|0=11Ti}}}mmm~yA yQ9g&j_}:-[F9KZsݍUjZ*q/\v~)*T̤@}C\Yv:gIkW8N9#x<RJm6}KGw->3R65wPu3WjAj5Y҂SR 8AvL&&.Oؔ%JT)o:~+KnY,i w/i(B EQU,D$AdjjjJe ceC\gݳC'5"q===tZ$>Jg}X*WhAYRv;z|8(JUYYf@0V6ZpDo}muGB`0X6499JP* :,3o+Z -8K>qvB, 6mnnT,3o+Z -8K3PxLy4Mn#,޻W!$ɔLգ5(UsL HߨJЂzV}Rs> XXd2ó9P.Kb'U֠TUϕ>3^6"}*B a[FZI/X>sa3 JUYYf@0V6ZpDoUݡpkwjp;1;ZWٰuѤf-VwĉEB@wwr`E>}zbb"!VyA yQ}}ckTIoWf}fs6b0 vW~T^ ST5BUҾz9Yq'<7x-t4q!lVk~6AKGY# cn>{zeg6p\Iݎn# #| `۝Ng2T^M\"ol*i_ }ʬl:FF<[BQ4MSE(b"a4,WZ}2b}ˬޞիՎk|Po߳R vlnD"iz/W֩V{VV;g3eC3P}G^7sg~4|B@߰N}ڳzq8/j }ی B`lhrr20JL+ja\eVoj5lfl7kw- .,,bp8l5_77S_^}v\#fˆzǕgݡhvGYw%CCCL&S9Q]*5z}"Ȉ}{qjg#o Fckw˲LF|<0>J%Q5騮rUU=jaqdľQ=׸}ʳ۷Q]n#I}α`M_axd2/W֩V{VV;g3eCZ}Ap;@~w\wv` ;U6@]4ii#Y|ˀ8qcqq200ݽ(o`tcK|O@"JBZ[[u^ZyWdFծʳR+o(9JWy猤g̯糆>$zW_~.; U{*JJe(i_}Y3VWcPJo3_,˲N/3Zt:i8x v6JGw\IΤVyVR*EzNUWy֊VejW:UZ}@:mOQToow8lv}}=6uq vt&zД%;Wfv} 'IT*>Mʬ՞ׯt8: 3c,5{8nnnM& DyF)i(B EQU,D$1[٭ҙxSSSYIg&K6jk5ԛ k_}+ 5 w\{|cccX_CCCnJ$?;;;^hi$G"zzzY{vk*޳yVR*#FI#GejW:kr;ԀUu8cd2p8t%x|>qfhE=kg%2bT4bpZfl~eAk1uwյo=DCP0,[T*e.k}3Y&yVR*#FIR鳍Z#fBWʂg Wq,v~vv8a/wz߹X,m6ܜ;D}wU{0JJeĨ>3i6UZ|Rg WΞv93^7,//B\.'>$' }F !4Mn,޻W!$ɔV Hw\Ug TFGy&k$ LOO'ҋz6ɈbfD\.kD}wU{0JJeĨzk_gƣ[W/Z{NsQOXh^{ J/3J|0KOgC>ַZjfPx\͢p^I(wuu;l6fffO~f.Q#%Qq vәL&kU KOgC>ַZjfPx\͢p^qpssl2d'ϣm]0MEBA(bH$TF"HR Z|̠^VEz8/|ѱ1,Kk[n% 񟍍W^ETkw0?p8ěDdpa*z$ ^uvyyA!|^|kkk#$j>φtT0(C6+#Sp0ڽ:/x+>}RI CxNxkweY~qZ( I4qdzmf*j>dCO*n!zȕ)8TNSۻG6]__333[:O~hò,q vәL&5M!'D 7ʐ =ʈ*\otttll y>44t֭D"!ի&#q===tzqqQYϦV ÜcsYtXC hYU!޲&J&iz|8Ѩlh;ʐ =ʈ*뫷^˸CP0, DS:g&144Dd2e?R(Mn[;˲"Sl(i}?#jh ƑiU4?̚AN$˥w?.K?Qe3(@a}&QJ6zt՟r4*WStUf{eO_P7axd2zrlh8ʐ =ʈ~wP6kwbp;1;ZWٰjĉ'O(200pI-3 ^uvׯ6BHkkk?b6)< B̨*+`QFɳV-hĹkw+}[0VW祟ˮw*9 Ve`qfh5265q* h}YLj6Roݽ^oWWRB`0X6499J W#n@ ݂WITIg(C0ׂZZuw=Y^[[Ⴣ X,l9|$hרVժF3gmg3VjFx@  rTx<^n-"d2Uqi-~e{UP#PDU)~Ƃnˌ(Z߂ylU} Z 0==H$K/?.Kb?5,d2 )Qkh]Z*eXmE[0Uʹ>[Bkk/;(}0믿<𹾾6BHkkkţVkAIg|cj#v-ޯ􃕣VkAIg|cj#vaYV΋Ն+3oAUt:i8x95"jo`1h S)]]]#ͮݦ̈e-^Zo<8n;d2iA:Vk}AP02F织d20LOOO4s֌Xf߂Ei(B EQU,ԔHĈMV_VT/5|ccc8ςCCCnJ$?;;;^ ՋVD8IӋw}`Z67XC hYU!:&J&CY3bA~ Uyl6FVkַX0suw IDATe(3oA B`lhrr2X*J 唴>U*eCFXa?;;q0kkkpe-^TX,m6ܜFdZ67XC hCPCCC.KW.ECCCL&S#2/3t UXlnXeݻgg]櫯Z_c7k0B̿vazz:H,//^\__7].~j_f'T/˲Ly0̣O.TT3îQjPu5_^ف [P+axd2VkַX0~wP6kwbp;1;ZWٰt`N!}}}mmm~ADk3_F̳Ơf*}YsADk3_F̳ƠfYeYvN'MB<OR,34 U+X1=3A(wuu;l6fff9n;d2YWf@1hQZj1h@{8nnnM& Dyq<<q0}hh֭[DBgcccggիWH$q\OOO:^\\UT>VTVZc j~vּ}vCLL&_z|8hm &q] AD+U+X1=3Mޮ{ޮ.v( eLR*iJ@QQ%VF̳ƠVgg=/%pr0Lww{bX8lsss)qw-3:}V`4̀;ϙz@`yy9 544r9-x\MӴ?Ͳ{c B2LT8e>*̈́kkAD"\zq}}3˲Ly0NOkr\G *q] I}DGU "1 XBo=b[y>l鸻T>VTVZc {fw05`C v,``%vIk`X`X`X@|>_"("(nU(hfY+K}ttx,x\(@uQaBH0q3;`;`X`XX@if{ !+++/EǷ?Nx㍪07M5*.EQFIN׿^\\4,XvڿQ|YX#l[O %Nl6;U?$pҞR}{3Prl.#$Is:l#XF(5}~??FGGcҋkkkt׿52SNݻL:Hd2T㵻t+$ ^g7ZPK_w8ʕ+{yW/^45mlllnn w}Z|eeŋ'OܳgO6V^# >l0TjkHJFV>ĉ`pv;11111!7n-K(L\mqǏoooqx>55n讹Ru,7|s+++bKMjzݣUضf]{tdZnFxViѤfA>|O,O>yꫯ>|޺~zGGDZcn߾NA8z(qSSS;w{<q G?{9r͛7)DTaկ677_x>vZP,O?w-:866VZ,nQ0ppccc~ᑑׯ={6BijiۆÇW\Y__?rH}}۷XȣG4>>>~Eei^^^VX#WUϜ9sҥA1JFee23gΌOMM:theeޓ<ϋݵFHd[/Bg^pOSP8zkx55n <Ϸd2{x%}~\.W(Eݶ3g,//ٵ55vux<^(vwlktq.^, {le5jmmMRKKKB!N߼y3 g[mT=7Ձ@I|DBq$mccΝ;------ل:tUf ƑSY}*2G(DgD^xP(\~b[[xT* Uҙ^u+RBT*bX:uE˹\w)UJr9q@lw)X"C|\.ضDч?/--)P(H$v]-tNc+ݻw;;;O:uƍGo * gB4333훘ڽ6"|?w\{{/DtCC|TNzۻw655A;::N:IvZ}}P}}}www__mk455uG~طoߙ3g*144$>n8pVk$vJYձ ֶHY\\lkkvWוH$ l6STeV);>,&(0!$'{ݷ[tV}{饗VWW?^|.弄 NoݺGm㶥E<-eٵ+W.U}}ÇgZy?_{5b{ޗ_~g2 .Wݳ͑---~'?֯MMMm]^Hi[sutt W^yt+{_VP#9/߼yҥKwsRIרlGZ^^կ~kURgyf߾}MMMnx@ȭ퓷WRڸ7Wٻ %j*_[>hL[?}o$>=dJ7le9tC?F9rhcGمMؘ<P+d>O#Оo'i;{ۗ<ʦrb':mBNkKdEʾOLǼӆi{!ےK6֑ы[{{m˖Jҙn?dk$j$nwKڲubiSvu;?UP#BHk旅)WS۷j֧5.\ߞi1/ 3) Խ #Ƽ4cIK!&`&l ?yAJ'8ro{`;b"44h:QoK1@\aod8zF!dmմ/\us A|}fno^VoK\acAPD!g/]g"7ݛb ض9 =w ;1CCɺza펫Jߞu;F'o#eIp4Z-iw'6Ǯ!Z島4Ȼrap~꿛{]F >AFB7]|A}@)!D:bK$ٖ\lm5-LzdR~-'RMs+/C{rec3z؟>||n-=t"|dfn\e9:7/luBpZup2|}A}*fg*٩TNOaDkaғ2={Kۅ})m$sbvBsԃ[nnͮjCCIRbpST5`}͛OU 6V+|nt:!]FrzSSM+7O$ȒҹӾ;HQ&eXB"E6k=U|ֶp⺊VL}ϦhB7UU>۶s|yIWof9VWakzXd>(u6 o\)i_%c+j=%˛w/鎆o[#1MGE(Peeۮ; 9k#JokI&Igܺ$;I.PDm]Ku]7v+c:sԻ [%2۾YBHlɡ0WJWI&XO'<\)s?9@k;{yqSp0K\(P[cxPij-& Wm&!Md|vB=7ohmh(Ç7n44}_2;YC$u)JhFÝG6畦`泳u tYR6bƶ,]'ٞ/9ψΕt6N kw\Sp4f+Τt+|U6vhV369a`'yrLC.XȺtzsM)Y|Qb#nٗɤ1?úOEMؐ0(χ'ٖxG׉ m*>cpk֞Mfw/8i@UFɅY$I?񬕻ZmNOdL64&>v3;``9ao~󛄐_&vk~(3%GoajzvoL&pll70M9VZt&ڽF':uj޽ccca5ZPiПw!J=.e_ut:ӟ~s=gϞX,{Rkpp_t2˱Ӷ ܷou|~uuܹs.+++/^Z]]%[zZYY9Hϛovm{{L>z3(%IDAT>~x{{X㖗Q֗t6>S&m6믿/“O>IgMZ!l6Yz'x|뭷_qر۷oiŃVV#1KvPKK޽{x≫WNѣGjnK_R(:{ ? G^v]ҭnܸ100F[TJ" l;w<:(jxΟ?%}~\.W(ݶ3g,//ٵ55vux<^(d_+[*?~n Bb|͆f;wqǝ?nxxXyDsssb!y&qNs׭Z[[SRPH7oތ ȉJ666ܹ" !}9Л?r9q1Dlw)xFi$Az|>].Mӄm[h4C祥%5 -tN1hkk(7JmllA5ϋ?$ KdBݻw;;;O:uƍx_3>3 VroK4qe~$bb-m-MF׮]s݇݇]ZZz ?X]^^ֶk-.ݼy7 !$ȉJrLOOg7d6~FGGGGG/c#s_|Ŧ&/޽{) 4qԩ-[" ttt8zvZ}}P}}}www__mk455uG~طoߙ3gF/_GFFafddP(\|Yn444$>n8pv=Gzϻy>dojj)qdX<A' !=׏h+%??nii9|Oz_~eߟd/\ ^xo޼yҥ|;?tko~+MMM'OO[E--LPZF3"*4x11DcⅉA L )-Fr*P-=}Bٟ~U;3kֻɚ⸸8avJfާ$>>~hh(##cVIIIrp8>|`5gg?RTwܙy\$.8nwttL>~۷o;~7hv.111::Ǐݻ###---HΡL`I_Jlll^^NcFT^p-$8eSSS>5|HvY/++KOO'Ay:nuu͛7#pAZZZVVVlllXXbww!DP={6))IVrk[[[766"##߿O^ږׯ__t`0\Ή`fXa6$tJxiq͛7Fcaa@aBԔ` }󣣣unndljjcrrr~~>ydnnnGGbZ+++yyyᓓ^b1 ]]]fYR1 ҵkגۇ|>_AA_ExdB.:;;srrZ\|||BBBģ gϞYAϊG%~އF^SSfCxp^orrj}m\\\^^fٽGDjjjYYٻw\.WH$ &J*եUN8^XX811v777d2ٳg !uuu<ϻn799IQջnظzǗ'..nkkk~~9Np8~٘)J&˲(Q2wfJjooH$! cHٜ766~Jnmmmllciiiee~v~6d绘t-V%x޴ӧOk4Ha!H|F?ljjjeeᨭ=P6B!U(3( qnfTLLL}ssv;+Ne_I)/////y<""RXX?66аxj欬3gΔҜ | # xT̠x6B5Z! p,j=\]˲5A!---###ߟt:G",,jZVR333 RiwwwQ(ģ %flx"*~< W ~U(*j2~h4!vݧN,0  w\~Tyy]!f{%!ĉAf# xTLyKeCAK _BHUUUUUՍ7 !L&RT*M& \HKvL&Ӯt:[[[ Cyy9q (,qzaXYYy2̙3}qB<`b^__'Fe322•}!6JRBBBŋy_BHLLLLLJ3;ntsW\\0{TTTpg;::JKK}x} ]٬jsss2ᱱ˗/Fg߿Ue6;#IIIrp8>|`ق``l6JJJ㇆DFd~49?KQW\h4.knn>M7"m)VWW{^òSOO.3~ipjw3\W@)-;L"k;o kfPjwo||IENDB`boohu-0.13.0/img/intro-screen.png000066400000000000000000000322671356500202200166060ustar00rootroot00000000000000PNG  IHDRbKGD pHYs  tIME 1&D IDATxyt\u{k*LqFRyG|Wĕ3\d 4.¸:b4򐗸w"0*99lQ[Ѵ3 #WW%q,ȰJӲ5fwq\8%$ĝ?̵JC1uq6~O3ߕw@w;6'#IxܐNrU:n/)D#v$Hq7=⠘^IgIi%q%^)@$9d3y&0 wSWgvZ E5 jډ s&ص#AJ+ڝ Ww[X^;HgئAO.'UyǐHҺ"rOԔf\kk0@oE$^mUp{pG6$&Nj`|Ss o-ybI\ȸ"0.}&RfVV\4VKE(#96NV vM댺*ũ8u5Th`ű/%ʷR]!Y( K|QNYDFװ!- LTm/joAt eZ(@ut09zc-~ ԐJ+teZ䕎ˡ\</?PʀdTm_`ph:Coڄ~z?'GMT>C*'H+E />&|(! j/EݵWD`+0%ʷҵD2ɦR$Sd#`DL&F[͠*@]`5hPs>WέU ªDV˵㬢:bl,B㍍-Jplϖ?ѳÌ44ɷ)QbH֛u} eɵ,LPV}Pdۃi$:%E%[N6ndb&NWpUʑn]mJl00S_<񥩫w0vo&5)pd?{7Fi {8l|Wӗfٺ حF$4oUdfK#gqkyy'5EŘSrԴg8[o1x&PR'0%Ͳr8cmmg *Vw]1~u"PCS}gՍ9fQݞ%]I|t/?zʰM18X~L|E4iʺ X⫑*p8`o,J0 n`6*N[8ɵ$85c3z6 sr#}: )j+ϥ&wEd!8vf_irsIylPar!ʭ;cxKf.%r<-ʡ9ߜolN{93^XucOQ.KW{iUIBy9|.H%xd<^ ㋩F tUwuz>@<i@ f=/='e:?@9q=/O!u^2O[9F8q v_bByR,/WNFo(l tj8W FidciH=op1:{%N: [b[Ic. w5@2Ԯ qώ)Z0EmmAmV'^x[׏f'"/i\fةo'7 XT#7n`˯} ]ThdPuJ?Dcs52uT2^LyB&n7))G~ >EmpG],Gh=ʦ:6c7@D\b]"Y4/312H<$eC $TfYɱNFO>Bjl|wg㙱U6u G0 58䋛չ27A028HE/b]^"y 7nN8hƤ1E^V^_|<x9. ̚LޫA6r8RluF!F[Ɖ|Jܶ=d;ARz;Su g;GCgeCrs)y,F" "0 crEWA1)3eA\"0  ưbv\Œ~+OI94kp؁ÿM{LMvNж%M‚331Ygųs\ j9=2H{U8Li;eE_( VINz)shW,@~iqW_5yr#ǚGԬ1J%Gh-F9kONBh5nٓ"Yˡ.r P}ôtwæ8fO?|_̑,TaGtpF45Bm,H$.Wj&MoBas|\7;{L<#ɤ5VF'&kIldn(Ԍhdd)82%% 0 LXلAƺ4y!/gV>5V\F~P5_5T>Lw+gMSe#cubSϨOS|N:34oϢ%;>`97ΖMU>X-fU.L^u{O|,V,]cyas|g |s z&Bo1hƻނ8^[jӃ&MY73k(h՚jmopUe %CV#q8@r _U"2}?xi1XPQ9SCkYc|q됉9mcJe]COyō`rȖSձDQ%,T~x˛t 44gUY(dTN4TSeߩ3*ƍ+nш!VdAQ!sʂʵ8f>PԴ8HIej3[HH1bBLT+EۥWExp(h-ةr~sдf'Y~D][׆PN5m>~TlF> k+`ìegi­PB!J~nS !eADc_rM.0v+pýŀȜ1 lHLkJGoǿ ďy@-zyͪac8 * /cS3VḥYz ɨ%u  ."7U E F, V_uK$[&*]D~;ϐy[ >)51ұ'03{)w#.,M}-`mE;Hdٜ&4w[P:V*LjӪQ\_}fW4m. uLJƃ_ztQoŷ>9 ] 0Fx[72M˭T%lMol/I%4PɩWct 2t,S#%FW[膉7q[` r吽WتFn4;t;s||2~ih(cGD2t`O)RZjI횠}oW!^90lUCfu H;>맢oCǎnj<+3"FM6,RUQ/حdGٻ X]Ng~* =*Bo.'d¸`6+ΨS?f6~6Yr O%wKD`V+TZ?%}=w`4Чi̯Y}WW˔ʁ@_yR* *p!IAD`A!@Xy,rm  !Lgp 7M:$溅|Gݬ }AMƚF:&1?ڛc4ddu2N^H{ā` K$@:\dEG{xZOc|o1^кŪg׍zh3Z;ƽ9Lm{bMt0P[2u )0U*TCilm:p*ZIS";m|[ Ukǁvcs=NƬu6Ž/,κ1קY=+(~4(Ks޹7Om9O4s{UPD TbI >wNж#i; ̇A9:,jF%F86gK aF[Z/R:z8hρ=_~)5{Q.CKbRy^iKS1SA:YKOIYP~\ByF8Bl/1#%XK;xZϑXc{a"3ڗޟόa*lN| ,$"|GOמ/=ڂʜ:׶64^pS&sغ:3Ԯ qώ)OhDg\jhgϥczGNs ]> !](5&^x[׏f'".FhNW0΍oxs zsXY[y#drٟ6>+u8s|{ݼm;l]xmyӧQs X8S(@|J8ƹ49N^v(['1NL%CyB&n7m @0,^pnE츏d:R[i ζ(`ͺ 5YB1-JX~7kv4*06U A1mWl7X{'iX_\ߙ+ms|OrQ \>S_5';o0tk c ɳH$ǧ"jY8^ ~C'ґbǨ3re؏ :>ya190#VasKr[x(G;.2kf"v,R[ )=C۝):]D"*96%Eq^6y C❕e@`U햃* =*PB" fU@ "0%a* ݓR\ʊ*iwk>U@(7U*B\"0  ư!>R%?b hIssEZ(LJY}9*Xa^c#W~nvU$w=Bϻf-=(\`Wq]S(PJ#/T?0Hv\ ˏ)[҄OF]),*I ڶ VXr0~&~&s}W;t X\t X7)Mirs [Fy4k:=+9A-" X"z')je#ܲ'EC]n;iK&sgpm'6{9-+NZ-P/}yB ZwDٱ!Ej8sAtA9A =}a{9um~2(uI:83<\zFa;Zi*inrn\#ǚGԬ1J%Gh-F9kON]틘s~U 3Pn_nM9_o=y0.5/EĽ_N&а2:605Ȥ50  pFYzYRlNCe c A/UOY !v\67I :HE?eFb+";6E^;clJ?$Q`=IZ\U=})A ] ~CI5+۳;ێ;N7BI\Z\URܙnzWݘJ#z#W3+3C,Z˹ Cz5\m1V- LiJ*wϙi܉ > PT8Zv?ٿ*=cS;|^&e0DRѐG;7*0e:ӭ@L r^9fEĭIuD~wZsT=([Rd愛fU+&TKj\|焇'j;T|Ue%c\6-Q7aoWvl7[AW+x-6|qv{?v{71YJqlφc&iKJ\ 8U`VИۚRMyD_zӊUD_وPhsȹOҚ._k+==y4hUJnR *B9|RktGu xI.i$ex}#u (J'p3wy6qȞgc:*>nz+/TD'0ј!)\>EnhWiZ E}LBUsr1[Bs cQ*RԄ*O&J>/TƆ },*hMr$ i%s@[&HZS< NU-xgyQ[qW"}*&gsu;MUBv` *N4+]AE•\ǭrr8EڜqzI7M7XB ֶX亃􏭒,+@PRWк!oWH˧+pi;JUD_6rnz;]Q][( 'wt 򞀊9uЅ^d&@_m[G$p_jU[9'wEhِ6KekT79;Gغ3MeMʎ׽e?*F_UJ9U,na0} Y2iO&;qW"rNo52.N@xW%_qqvEmO IJojr[q07\<+i;6"rQT}K2w9*Ǹ]Źræ ur]滓t85rb'*9|On\'}OՒ5A1v߮PICrYrr Xu B! "0 crEq\S7S\2)* ȘAFAFU(03xW2&>!K씝Z޳vܳZS(nK5ӫєMr>V>\s % \ay75s 9=)/W{ BIǺdjC{4[Z`oͮ1.Uq94wR*0OgWD7B!]GW SYsW9ÎBИ_t܏Ӫ+SM tea*5sY" h4AXNUwE[ٕq4ŷ iKC\fWa:ي*]a1EnM)7s|#sqz4ijViZ޳:|K!E\Qÿ7WkkeӤAt&z'hf*BP|FJXXת|ui9~3@y隃ԓmo]}Z9**Ӫ ,Ruͪ}C۹JYwciMދ75PBZ:;q/V9$hBrvQXJpaqYjU@HU@AF7qdL(* 2&e, cRFAY=E7y}Svoy̍-WG?,w~ݲ P 8x x1yWAX\lI ŠU*`K|U@V,osUw{0.wwh-hb*n2osAQ)>v fO`w~n*ɰ@UAXZe-mIVWAX\l!r9|DiO| bE cf*~e.W#̡NFoXRp CN U6rB W[ÅU@6"ʡ%=7M_dW[zش*qRl\ r9Teㇿ溥}-U  ,rbdnK0eܳGpoߚ>uԯPneQ crEWA1)# ]<ۇcì zW.c\k#K, 1rƱ>FcPJe+smtwsɒ%` ygvQJ\k hڙ)?y}͉gS:ĕK+smh!/3'ViZliPJ\kͤ~WGށ9LC1uqW.2ӸjsO1 VĻLqt1w.ؑ id%q%͸sYUoǼK;)ڂCb=K͢vgh#eqv$ )FsGĕEǝ{U"3N0/sjvWGfgP Ia?bJgTufKJܢ\8ᜏoYz-w%jW^)̵7M˖<qT>Mq++q+smV{i#|oQ])\ĕqiޞ#}"Pro9@[ĕ6qoq{b沧\X3};WN+sMJm2O[BEa 1WΈ{axmMdnxGdL K$AAD`AAAFAAD`AA}6 IENDB`boohu-0.13.0/io.go000066400000000000000000000101111356500202200136320ustar00rootroot00000000000000// +build !js package main import ( "fmt" "io/ioutil" "os" "path/filepath" ) func Replay(file string) error { ui := &gameui{} g := &game{} ui.g = g g.ui = ui err := g.LoadReplay(file) if err != nil { return fmt.Errorf("loading replay: %v", err) } err = ui.Init() if err != nil { fmt.Fprintf(os.Stderr, "boohu: %v\n", err) os.Exit(1) } defer ui.Close() ui.DrawBufferInit() ui.Replay() return nil } func (g *game) DataDir() (string, error) { var xdg string if os.Getenv("GOOS") == "windows" { xdg = os.Getenv("LOCALAPPDATA") } else { xdg = os.Getenv("XDG_DATA_HOME") } if xdg == "" { xdg = filepath.Join(os.Getenv("HOME"), ".local", "share") } dataDir := filepath.Join(xdg, "boohu") _, err := os.Stat(dataDir) if err != nil { err = os.MkdirAll(dataDir, 0755) if err != nil { return "", fmt.Errorf("%v\n", err) } } return dataDir, nil } func (g *game) Save() error { dataDir, err := g.DataDir() if err != nil { g.Print(err.Error()) return err } saveFile := filepath.Join(dataDir, "save") data, err := g.GameSave() if err != nil { g.Print(err.Error()) return err } err = ioutil.WriteFile(saveFile, data, 0644) if err != nil { g.Print(err.Error()) return err } return nil } func (g *game) RemoveSaveFile() error { return g.RemoveDataFile("save") } func (g *game) Load() (bool, error) { dataDir, err := g.DataDir() if err != nil { return false, err } saveFile := filepath.Join(dataDir, "save") _, err = os.Stat(saveFile) if err != nil { // no save file, new game return false, err } data, err := ioutil.ReadFile(saveFile) if err != nil { return true, err } lg, err := g.DecodeGameSave(data) if err != nil { return true, err } if lg.Version != Version { return true, fmt.Errorf("saved game for previous version %s.", lg.Version) } *g = *lg return true, nil } func (g *game) SaveConfig() error { dataDir, err := g.DataDir() if err != nil { g.Print(err.Error()) return err } saveFile := filepath.Join(dataDir, "config.gob") data, err := GameConfig.ConfigSave() if err != nil { g.Print(err.Error()) return err } err = ioutil.WriteFile(saveFile, data, 0644) if err != nil { g.Print(err.Error()) return err } return nil } func (g *game) LoadConfig() (bool, error) { dataDir, err := g.DataDir() if err != nil { return false, err } saveFile := filepath.Join(dataDir, "config.gob") _, err = os.Stat(saveFile) if err != nil { // no save file, new game return false, err } data, err := ioutil.ReadFile(saveFile) if err != nil { return true, err } c, err := g.DecodeConfigSave(data) if err != nil { return true, err } GameConfig = *c return true, nil } func (g *game) RemoveDataFile(file string) error { dataDir, err := g.DataDir() if err != nil { return err } dataFile := filepath.Join(dataDir, file) _, err = os.Stat(dataFile) if err == nil { err := os.Remove(dataFile) if err != nil { return err } } return nil } func (g *game) SaveReplay() error { dataDir, err := g.DataDir() if err != nil { g.Print(err.Error()) return err } saveFile := filepath.Join(dataDir, "replay") data, err := g.EncodeDrawLog() if err != nil { g.Print(err.Error()) return err } err = ioutil.WriteFile(saveFile, data, 0644) if err != nil { g.Print(err.Error()) return err } return nil } func (g *game) LoadReplay(file string) error { dataDir, err := g.DataDir() if err != nil { return err } replayFile := filepath.Join(dataDir, "replay") if file != "_" { replayFile = file } _, err = os.Stat(replayFile) if err != nil { // no save file, new game return err } data, err := ioutil.ReadFile(replayFile) if err != nil { return err } dl, err := g.DecodeDrawLog(data) if err != nil { return err } g.DrawLog = dl return nil } func (g *game) WriteDump() error { dataDir, err := g.DataDir() if err != nil { return err } err = ioutil.WriteFile(filepath.Join(dataDir, "dump"), []byte(g.Dump()), 0644) if err != nil { return fmt.Errorf("writing game statistics: %v", err) } err = g.SaveReplay() if err != nil { return fmt.Errorf("writing replay: %v", err) } return nil } boohu-0.13.0/items.go000066400000000000000000000701471356500202200143630ustar00rootroot00000000000000package main import ( "errors" "fmt" "sort" ) type consumable interface { Use(*game, event) error String() string Plural() string Desc() string Letter() rune Int() int } func (g *game) UseConsumable(c consumable) { g.Player.Consumables[c]-- g.StoryPrintf("Used %s.", Indefinite(c.String(), false)) if g.Player.Consumables[c] <= 0 { delete(g.Player.Consumables, c) } g.FunAction() } type potion int const ( HealWoundsPotion potion = iota TeleportationPotion BerserkPotion DescentPotion SwiftnessPotion LignificationPotion MagicMappingPotion MagicPotion WallPotion CBlinkPotion DigPotion SwapPotion ShadowsPotion TormentPotion AccuracyPotion DreamPotion ) const NumPotions = int(DreamPotion) + 1 func (p potion) String() (text string) { text = "potion" switch p { case HealWoundsPotion: text += " of heal wounds" case TeleportationPotion: text += " of teleportation" case DescentPotion: text += " of descent" case MagicMappingPotion: text += " of magic mapping" case MagicPotion: text += " of refill magic" case BerserkPotion: text += " of berserk" case SwiftnessPotion: text += " of swiftness" case LignificationPotion: text += " of lignification" case WallPotion: text += " of walls" case CBlinkPotion: text += " of controlled blink" case DigPotion: text += " of digging" case SwapPotion: text += " of swapping" case ShadowsPotion: text += " of shadows" case TormentPotion: text += " of torment explosion" case AccuracyPotion: text += " of accuracy" case DreamPotion: text += " of dreams" } return text } func (p potion) Plural() (text string) { // never used for potions return p.String() } func (p potion) Desc() (text string) { switch p { case HealWoundsPotion: text = "heals you a good deal." case TeleportationPotion: text = "teleports you away after a few turns." case DescentPotion: text = "makes you go deeper in the Underground." case MagicMappingPotion: text = "shows you the map layout and item locations." case MagicPotion: text = "replenishes your magical reserves." case BerserkPotion: text = "makes you enter a crazy rage, temporarily making you faster, stronger and healthier. You cannot use rods while berserk, and afterwards it leaves you slow and exhausted." case SwiftnessPotion: text = "makes you move faster and better at avoiding blows for a short time." case LignificationPotion: text = "increases your armour against physical blows, but you are attached to the ground while the effect lasts (you can still descend)." case WallPotion: text = "replaces free cells around you with temporary walls." case CBlinkPotion: text = "makes you blink to a targeted cell in your line of sight." case DigPotion: text = "makes you dig walls by walking into them like an earth dragon." case SwapPotion: text = "makes you swap positions with monsters instead of attacking. Ranged monsters can still damage you." case ShadowsPotion: text = "reduces your line of sight range to 1. Because monsters only can see you if you see them, this makes it easier to get out of sight of monsters so that they eventually stop chasing you." case TormentPotion: text = "halves HP of every creature in sight, including the player, and destroys visible walls. Extremely noisy. It can burn foliage and doors." case AccuracyPotion: text = "makes you never miss for a few turns." case DreamPotion: text = "shows you the position in the map of monsters sleeping at drink time." } return fmt.Sprintf("The %s %s", p, text) } func (p potion) Letter() rune { return '!' } func (p potion) Int() int { return int(p) } func (p potion) Use(g *game, ev event) error { quant, ok := g.Player.Consumables[p] if !ok || quant <= 0 { // should not happen return errors.New("no such consumable: " + p.String()) } if g.Player.HasStatus(StatusNausea) { return errors.New("You cannot drink potions while sick.") } var err error switch p { case HealWoundsPotion: err = g.QuaffHealWounds(ev) case TeleportationPotion: err = g.QuaffTeleportation(ev) case BerserkPotion: err = g.QuaffBerserk(ev) case DescentPotion: err = g.QuaffDescent(ev) case SwiftnessPotion: err = g.QuaffSwiftness(ev) case LignificationPotion: err = g.QuaffLignification(ev) case MagicMappingPotion: err = g.QuaffMagicMapping(ev) case MagicPotion: err = g.QuaffMagic(ev) case WallPotion: err = g.QuaffWallPotion(ev) case CBlinkPotion: err = g.QuaffCBlinkPotion(ev) case DigPotion: err = g.QuaffDigPotion(ev) case SwapPotion: err = g.QuaffSwapPotion(ev) case ShadowsPotion: err = g.QuaffShadowsPotion(ev) case TormentPotion: err = g.QuaffTormentPotion(ev) case AccuracyPotion: err = g.QuaffAccuracyPotion(ev) case DreamPotion: err = g.QuaffDreamPotion(ev) } if err != nil { return err } ev.Renew(g, 5) g.UseConsumable(p) g.Stats.Drinks++ g.ui.DrinkingPotionAnimation() return nil } func (g *game) QuaffTeleportation(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot teleport while lignified.") } if g.Player.HasStatus(StatusTele) { return errors.New("You already quaffed a potion of teleportation.") } delay := 20 + RandInt(30) g.Player.Statuses[StatusTele] = 1 g.PushEvent(&simpleEvent{ERank: ev.Rank() + delay, EAction: Teleportation}) g.Printf("You quaff the %s. You feel unstable.", TeleportationPotion) return nil } func (g *game) QuaffBerserk(ev event) error { if g.Player.HasStatus(StatusExhausted) { return errors.New("You are too exhausted to berserk.") } if g.Player.HasStatus(StatusBerserk) { return errors.New("You are already berserk.") } g.Player.Statuses[StatusBerserk] = 1 end := ev.Rank() + 65 + RandInt(20) g.PushEvent(&simpleEvent{ERank: end, EAction: BerserkEnd}) g.Player.Expire[StatusBerserk] = end g.Printf("You quaff the %s. You feel a sudden urge to kill things.", BerserkPotion) g.Player.HP += 10 return nil } func (g *game) QuaffHealWounds(ev event) error { hp := g.Player.HP g.Player.HP += 2 * DefaultHealth / 3 if g.Player.HP > g.Player.HPMax() { g.Player.HP = g.Player.HPMax() } g.Printf("You quaff the %s (%d -> %d).", HealWoundsPotion, hp, g.Player.HP) return nil } func (g *game) QuaffMagic(ev event) error { mp := g.Player.MP g.Player.MP += 2 * g.Player.MPMax() / 3 if g.Player.MP > g.Player.MPMax() { g.Player.MP = g.Player.MPMax() } g.Printf("You quaff the %s (%d -> %d).", MagicPotion, mp, g.Player.MP) return nil } func (g *game) QuaffDescent(ev event) error { // why not? //if g.Player.HasStatus(StatusLignification) { //return errors.New("You cannot descend while lignified.") //} if g.Depth >= MaxDepth { return errors.New("You cannot descend any deeper!") } g.Printf("You quaff the %s. You fall through the ground.", DescentPotion) g.LevelStats() g.StoryPrint("Descended deeper into the dungeon.") g.Depth++ g.DepthPlayerTurn = 0 g.InitLevel() g.Save() return nil } func (g *game) QuaffSwiftness(ev event) error { g.Player.Statuses[StatusSwift]++ end := ev.Rank() + 85 + RandInt(20) g.PushEvent(&simpleEvent{ERank: end, EAction: HasteEnd}) g.Player.Expire[StatusSwift] = end g.Player.Statuses[StatusAgile]++ g.PushEvent(&simpleEvent{ERank: end, EAction: EvasionEnd}) g.Player.Expire[StatusAgile] = end g.Printf("You quaff the %s. You feel speedy and agile.", SwiftnessPotion) return nil } func (g *game) QuaffDigPotion(ev event) error { g.Player.Statuses[StatusDig] = 1 end := ev.Rank() + 75 + RandInt(20) g.PushEvent(&simpleEvent{ERank: end, EAction: DigEnd}) g.Player.Expire[StatusDig] = end g.Printf("You quaff the %s. You feel like an earth dragon.", DigPotion) return nil } func (g *game) QuaffSwapPotion(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot drink this potion while lignified.") } g.Player.Statuses[StatusSwap] = 1 end := ev.Rank() + 130 + RandInt(41) g.PushEvent(&simpleEvent{ERank: end, EAction: SwapEnd}) g.Player.Expire[StatusSwap] = end g.Printf("You quaff the %s. You feel light-footed.", SwapPotion) return nil } func (g *game) QuaffShadowsPotion(ev event) error { if g.Player.HasStatus(StatusShadows) { return errors.New("You are already surrounded by shadows.") } g.Player.Statuses[StatusShadows] = 1 end := ev.Rank() + 130 + RandInt(41) g.PushEvent(&simpleEvent{ERank: end, EAction: ShadowsEnd}) g.Player.Expire[StatusShadows] = end g.Printf("You quaff the %s. You feel surrounded by shadows.", ShadowsPotion) g.ComputeLOS() return nil } func (g *game) QuaffLignification(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You are already lignified.") } g.EnterLignification(ev) g.Printf("You quaff the %s. You feel rooted to the ground.", LignificationPotion) return nil } func (g *game) QuaffMagicMapping(ev event) error { dp := &dungeonPath{dungeon: g.Dungeon} g.AutoExploreDijkstra(dp, []int{g.Player.Pos.idx()}) cdists := make(map[int][]int) for i, dist := range DijkstraMapCache { cdists[dist] = append(cdists[dist], i) } var dists []int for dist, _ := range cdists { dists = append(dists, dist) } sort.Ints(dists) g.ui.DrawDungeonView(NormalMode) for _, d := range dists { draw := false for _, i := range cdists[d] { pos := idxtopos(i) c := g.Dungeon.Cell(pos) if (c.T == FreeCell || g.Dungeon.HasFreeNeighbor(pos)) && !c.Explored { g.Dungeon.SetExplored(pos) draw = true } } if draw { g.ui.MagicMappingAnimation(cdists[d]) } } g.Printf("You quaff the %s. You feel aware of your surroundings..", MagicMappingPotion) return nil } func (g *game) QuaffTormentPotion(ev event) error { g.Printf("You quaff the %s. %s It hurts!", TormentPotion, g.ExplosionSound()) damage := g.Player.HP / 2 g.Player.HP = g.Player.HP - damage g.Stats.Damage += damage g.ui.WoundedAnimation() g.MakeNoise(ExplosionNoise+10, g.Player.Pos) g.ui.TormentExplosionAnimation() for pos, b := range g.Player.LOS { if !b { continue } g.ExplosionAt(ev, pos) } return nil } func (g *game) QuaffAccuracyPotion(ev event) error { g.Player.Statuses[StatusAccurate]++ end := ev.Rank() + 85 + RandInt(20) g.PushEvent(&simpleEvent{ERank: end, EAction: AccurateEnd}) g.Player.Expire[StatusAccurate] = end g.Printf("You quaff the %s. You feel accurate.", SwiftnessPotion) return nil } func (g *game) QuaffDreamPotion(ev event) error { for _, mons := range g.Monsters { if mons.Exists() && mons.State == Resting && !g.Player.LOS[mons.Pos] { g.DreamingMonster[mons.Pos] = true } } g.Printf("You quaff the %s. You perceive monsters' dreams.", DreamPotion) return nil } func (g *game) QuaffWallPotion(ev event) error { neighbors := g.Dungeon.FreeNeighbors(g.Player.Pos) for _, pos := range neighbors { mons := g.MonsterAt(pos) if mons.Exists() { continue } g.CreateTemporalWallAt(pos, ev) } g.Printf("You quaff the %s. You feel surrounded by temporary walls.", WallPotion) g.ComputeLOS() return nil } func (g *game) QuaffCBlinkPotion(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot blink while lignified.") } if err := g.ui.ChooseTarget(&chooser{free: true}); err != nil { return err } g.Printf("You quaff the %s. You blink.", CBlinkPotion) g.PlacePlayerAt(g.Player.Target) return nil } type projectile int const ( ConfusingDart projectile = iota ExplosiveMagara TeleportMagara SlowingMagara ConfuseMagara NightMagara ) const NumProjectiles = int(NightMagara) + 1 func (p projectile) String() (text string) { switch p { case ConfusingDart: text = "dart of confusion" case ExplosiveMagara: text = "explosive magara" case TeleportMagara: text = "teleport magara" case SlowingMagara: text = "slowing magara" case ConfuseMagara: text = "confusion magara" case NightMagara: text = "night magara" } return text } func (p projectile) Plural() (text string) { switch p { case ConfusingDart: text = "darts of confusion" case ExplosiveMagara: text = "explosive magaras" case TeleportMagara: text = "teleport magaras" case SlowingMagara: text = "slowing magaras" case ConfuseMagara: text = "confusion magaras" case NightMagara: text = "night magaras" } return text } func (p projectile) Desc() (text string) { switch p { case ConfusingDart: text = "can be silently thrown to confuse foes, dealing up to 7 damage. Confused monsters cannot move diagonally." case ExplosiveMagara: text = "can be thrown to cause a fire explosion halving HP of monsters in a square area. It can occasionally destruct walls. It can burn doors and foliage." case TeleportMagara: text = "can be thrown to make monsters in a square area teleport." case SlowingMagara: text = "can be activated to release a slowing bolt inducing slow movement and attack in one or more foes." case ConfuseMagara: text = "generates a harmonic light that confuses all the monsters in your line of sight." case NightMagara: text = "can be thrown at a monster to produce sleep inducing clouds in a 2-radius area. You are affected too by the clouds, but they will slow your actions instead." } return fmt.Sprintf("The %s %s", p, text) } func (p projectile) Letter() rune { return '(' } func (p projectile) Int() int { return int(p) } func (p projectile) Use(g *game, ev event) error { quant, ok := g.Player.Consumables[p] if !ok || quant <= 0 { // should not happen return errors.New("no such consumable: " + p.String()) } var err error switch p { case ConfusingDart: err = g.ThrowConfusingDart(ev) case ExplosiveMagara: err = g.ThrowExplosiveMagara(ev) case TeleportMagara: err = g.ThrowTeleportMagara(ev) case SlowingMagara: err = g.ThrowSlowingMagara(ev) case ConfuseMagara: err = g.ThrowConfuseMagara(ev) case NightMagara: err = g.ThrowNightMagara(ev) } if err != nil { return err } g.UseConsumable(p) g.Stats.Throws++ return nil } func (g *game) ThrowConfusingDart(ev event) error { if err := g.ui.ChooseTarget(&chooser{needsFreeWay: true}); err != nil { return err } mons := g.MonsterAt(g.Player.Target) bonus := 0 if g.Player.HasStatus(StatusBerserk) { bonus += RandInt(5) } if g.Player.Aptitudes[AptStrong] { bonus += 2 } attack, _ := g.HitDamage(DmgPhysical, 7+bonus, mons.Armor) // no clang with darts mons.HP -= attack if mons.HP > 0 { mons.EnterConfusion(g, ev) g.PrintfStyled("Your %s hits the %s (%d dmg), who appears confused.", logPlayerHit, ConfusingDart, mons.Kind, attack) g.ui.ThrowAnimation(g.Ray(mons.Pos), true) mons.MakeHuntIfHurt(g) } else { g.PrintfStyled("Your %s kills the %s.", logPlayerHit, ConfusingDart, mons.Kind) g.ui.ThrowAnimation(g.Ray(mons.Pos), true) g.HandleKill(mons, ev) } g.HandleStone(mons) ev.Renew(g, 7) return nil } func (g *game) ExplosionAt(ev event, pos position) { g.Burn(pos, ev) mons := g.MonsterAt(pos) if mons.Exists() { mons.HP /= 2 if mons.HP == 0 { mons.HP = 1 } g.MakeNoise(ExplosionHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) } else if g.Dungeon.Cell(pos).T == WallCell && RandInt(2) == 0 { g.Dungeon.SetCell(pos, FreeCell) g.Stats.Digs++ if !g.Player.LOS[pos] { g.WrongWall[pos] = true } else { g.ui.WallExplosionAnimation(pos) } g.MakeNoise(WallNoise, pos) g.Fog(pos, 1, ev) } } func (g *game) ThrowExplosiveMagara(ev event) error { if err := g.ui.ChooseTarget(&chooser{area: true, minDist: true, flammable: true, wall: true}); err != nil { return err } neighbors := g.Player.Target.ValidNeighbors() g.Printf("You throw the explosive magara... %s", g.ExplosionSound()) g.MakeNoise(ExplosionNoise, g.Player.Target) g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgPlayer) g.ui.ExplosionAnimation(FireExplosion, g.Player.Target) for _, pos := range append(neighbors, g.Player.Target) { g.ExplosionAt(ev, pos) } ev.Renew(g, 7) return nil } func (g *game) ThrowTeleportMagara(ev event) error { if err := g.ui.ChooseTarget(&chooser{area: true, minDist: true}); err != nil { return err } neighbors := g.Player.Target.ValidNeighbors() g.Print("You throw the teleport magara.") g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgPlayer) for _, pos := range append(neighbors, g.Player.Target) { mons := g.MonsterAt(pos) if mons.Exists() { mons.TeleportAway(g) } } ev.Renew(g, 7) return nil } func (g *game) ThrowSlowingMagara(ev event) error { if err := g.ui.ChooseTarget(&chooser{}); err != nil { return err } ray := g.Ray(g.Player.Target) g.MakeNoise(MagicCastNoise, g.Player.Pos) g.Print("Whoosh! A bolt of slowing emerges out of the magara.") g.ui.SlowingMagaraAnimation(ray) for _, pos := range ray { mons := g.MonsterAt(pos) if !mons.Exists() { continue } mons.Statuses[MonsSlow]++ g.PushEvent(&monsterEvent{ERank: g.Ev.Rank() + 130 + RandInt(40), NMons: mons.Index, EAction: MonsSlowEnd}) } ev.Renew(g, 7) return nil } func (g *game) ThrowConfuseMagara(ev event) error { g.Printf("You activate the %s. A harmonic light confuses monsters.", ConfuseMagara) for pos, b := range g.Player.LOS { if !b { continue } mons := g.MonsterAt(pos) if mons.Exists() { mons.EnterConfusion(g, ev) } } ev.Renew(g, 7) return nil } func (g *game) NightFog(at position, radius int, ev event) { dij := &normalPath{game: g} nm := Dijkstra(dij, []position{at}, radius) for pos := range nm { _, ok := g.Clouds[pos] if !ok { g.Clouds[pos] = CloudNight g.PushEvent(&cloudEvent{ERank: ev.Rank() + 10, EAction: NightProgression, Pos: pos}) g.MakeCreatureSleep(pos, ev) } } g.ComputeLOS() } func (g *game) ThrowNightMagara(ev event) error { if err := g.ui.ChooseTarget(&chooser{needsFreeWay: true}); err != nil { return err } g.Print("You throw the night magara… Clouds come out of it.") g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgSleepingMonster) g.NightFog(g.Player.Target, 2, ev) ev.Renew(g, 7) return nil } type collectable struct { Consumable consumable Quantity int } type collectData struct { rarity int quantity int } var ConsumablesCollectData = map[consumable]collectData{ ConfusingDart: {rarity: 4, quantity: 2}, ExplosiveMagara: {rarity: 6, quantity: 1}, NightMagara: {rarity: 9, quantity: 1}, TeleportMagara: {rarity: 12, quantity: 1}, SlowingMagara: {rarity: 12, quantity: 1}, ConfuseMagara: {rarity: 15, quantity: 1}, TeleportationPotion: {rarity: 6, quantity: 1}, BerserkPotion: {rarity: 6, quantity: 1}, HealWoundsPotion: {rarity: 6, quantity: 1}, SwiftnessPotion: {rarity: 6, quantity: 1}, LignificationPotion: {rarity: 9, quantity: 1}, MagicPotion: {rarity: 9, quantity: 1}, WallPotion: {rarity: 12, quantity: 1}, CBlinkPotion: {rarity: 12, quantity: 1}, DigPotion: {rarity: 12, quantity: 1}, SwapPotion: {rarity: 12, quantity: 1}, ShadowsPotion: {rarity: 15, quantity: 1}, DescentPotion: {rarity: 18, quantity: 1}, MagicMappingPotion: {rarity: 18, quantity: 1}, DreamPotion: {rarity: 18, quantity: 1}, TormentPotion: {rarity: 30, quantity: 1}, AccuracyPotion: {rarity: 18, quantity: 1}, } type equipable interface { Equip(g *game) String() string Letter() rune Desc() string } type armour int const ( Robe armour = iota SmokingScales ShinyPlates TurtlePlates SpeedRobe CelmistRobe HarmonistRobe ) func (ar armour) Equip(g *game) { oar := g.Player.Armour g.Player.Armour = ar if !g.FoundEquipables[ar] { g.StoryPrintf("Found and put on %s.", ar.StringIndefinite()) g.FoundEquipables[ar] = true } g.Printf("You put the %s on and leave your %s.", ar, oar) g.Equipables[g.Player.Pos] = oar if oar == CelmistRobe && g.Player.MP > g.Player.MPMax() { g.Player.MP = g.Player.MPMax() } } func (ar armour) String() string { switch ar { case Robe: return "robe" case SmokingScales: return "smoking scales" case ShinyPlates: return "shiny plates" case TurtlePlates: return "turtle plates" case SpeedRobe: return "robe of speed" case CelmistRobe: return "celmist robe" case HarmonistRobe: return "harmonist robe" default: // should not happen return "?" } } func (ar armour) StringIndefinite() string { switch ar { case ShinyPlates, TurtlePlates, SmokingScales: return ar.String() default: return "a " + ar.String() } } func (ar armour) Short() string { switch ar { case Robe: return "Rb" case SmokingScales: return "Sm" case ShinyPlates: return "Sh" case TurtlePlates: return "Tr" case SpeedRobe: return "Sp" case CelmistRobe: return "Cl" case HarmonistRobe: return "Hr" default: // should not happen return "?" } } func (ar armour) Desc() string { var text string switch ar { case Robe: text = "A robe provides no special protection, and will not help you much in your journey." case SmokingScales: text = "Smoking scales provide protection against blows. They leave short-lived fog behind as you move." case ShinyPlates: text = "Shiny plates provide good protection against blows, but increase your line of sight range." case TurtlePlates: text = "Turtle plates provide great protection against blows, but make you move slower and a little less good at evading blows." case SpeedRobe: text = "The speed robe makes you move faster, with a minor evasion bonus." case CelmistRobe: text = "The celmist robe improves your magic reserves, rod recharge rate, and rods can gain two extra charges. In Hareka, celmists are what most people would call mages." case HarmonistRobe: text = "The harmonist robe makes you harder to detect (reduced LOS range, stealthy movement, noise mitigation). Harmonists are mages specialized in manipulation of light and noise." } return text } func (ar armour) Letter() rune { return '[' } type weapon int const ( Dagger weapon = iota Axe BattleAxe Spear Halberd AssassinSabre DancingRapier HopeSword Frundis ElecWhip HarKarGauntlets VampDagger DragonSabre FinalBlade DefenderFlail ) const WeaponNum = int(DefenderFlail) + 1 func (wp weapon) Equip(g *game) { owp := g.Player.Weapon g.Player.Weapon = wp if !g.FoundEquipables[wp] { g.StoryPrintf("Found and took %s.", Indefinite(wp.String(), false)) g.FoundEquipables[wp] = true } g.Printf("You take the %s and leave your %s.", wp, owp) if wp == Frundis { g.PrintfStyled("♫ ♪ … Oh, you're there, let's fight our way out!", logSpecial) } g.Equipables[g.Player.Pos] = owp } func (wp weapon) String() string { switch wp { case Dagger: return "dagger" case Axe: return "axe" case BattleAxe: return "battle axe" case Spear: return "spear" case Halberd: return "halberd" case AssassinSabre: return "assassin sabre" case DancingRapier: return "dancing rapier" case HopeSword: return "hopeful sword" case Frundis: return "staff Frundis" case ElecWhip: return "lightning whip" case HarKarGauntlets: return "har-kar gauntlets" case VampDagger: return "vampiric dagger" case DragonSabre: return "dragon sabre" case FinalBlade: return "final blade" case DefenderFlail: return "defender flail" default: // should not happen return "some weapon" } } func (wp weapon) Short() string { switch wp { case Dagger: return "Dg" case Axe: return "Ax" case BattleAxe: return "Bt" case Spear: return "Sp" case Halberd: return "Hl" case AssassinSabre: return "Sb" case DancingRapier: return "Dn" case HopeSword: return "Ds" case Frundis: return "Fr" case ElecWhip: return "Wh" case HarKarGauntlets: return "Hk" case VampDagger: return "Vm" case DragonSabre: return "Dr" case FinalBlade: return "Fn" case DefenderFlail: return "Fl" default: // should not happen return "?" } } func (wp weapon) Desc() string { var text string switch wp { case Dagger: text = "A dagger is the most basic weapon. Great against sleeping monsters, but that's all." case Axe: text = "An axe is a one-handed weapon that can hit at once any foes adjacent to you, dealing extra damage in the open." case BattleAxe: text = "A battle axe is a big two-handed weapon that can hit at once any foes adjacent to you, dealing extra damage in the open." case Spear: text = "A spear is a one-handed weapon that can hit two opponents in a row at once. Useful in corridors." case Halberd: text = "An halberd is a big two-handed weapon that can hit two opponents in a row at once. Useful in corridors." case AssassinSabre: text = "The assassin sabre is a one-handed weapon. It is more accurate against injured opponents." case DancingRapier: text = "The dancing rapier is a one-handed weapon. It makes you swap positions with your foe and can hit another monster behind with extra damage." case HopeSword: text = "The hopeful sword is a big two-handed weapon. The more injured you are, the more damage increases." case Frundis: text = "Frundis is a musician and harmonist, which happens to be a two-handed staff too. It may occasionally confuse monsters on hit. It magically helps reducing noise in combat too, and reduces your line of sight range by 1." case ElecWhip: text = "The lightning whip is a one-handed weapon that inflicts electrical damage to a monster and any foes connected to it." case HarKarGauntlets: text = "Har-kar gauntlets are an unarmed combat weapon. They allow you to make a wind attack, passing over foes in a direction." case VampDagger: text = "The vampiric dagger is a one-handed weapon that gives you some healing when you hit living monsters." case DragonSabre: text = "The dragon sabre is a one-handed weapon that inflicts extra damage on healthy big monsters." case FinalBlade: text = "The final blade is an accurate two-handed weapon that instantly kills monsters at less than half full health. Wielding this weapon will reduce your maximum health by a third." case DefenderFlail: text = "The defender flail is a one-handed weapon that moves foes toward you, and hits harder as you keep attacking without moving." } return fmt.Sprintf("%s It can hit for up to %d damage.", text, wp.Attack()) } func (wp weapon) Attack() int { switch wp { case Axe, Spear, AssassinSabre, DancingRapier, DragonSabre: return 11 case BattleAxe, Halberd, HopeSword, FinalBlade: return 15 case Frundis: return 13 case HarKarGauntlets: return 14 case DefenderFlail: return 10 case Dagger, VampDagger: return 9 case ElecWhip: return 8 default: return 0 } } func (wp weapon) TwoHanded() bool { switch wp { case BattleAxe, Halberd, HopeSword, Frundis, HarKarGauntlets, FinalBlade: return true default: return false } } func (wp weapon) Letter() rune { return ')' } func (wp weapon) Cleave() bool { switch wp { case Axe, BattleAxe: return true default: return false } } func (wp weapon) Pierce() bool { switch wp { case Spear, Halberd: return true default: return false } } type shield int const ( NoShield shield = iota ConfusingShield EarthShield BashingShield FireShield ) func (sh shield) Equip(g *game) { osh := g.Player.Shield g.Player.Shield = sh if !g.FoundEquipables[sh] { g.StoryPrintf("Found and put on %s.", Indefinite(sh.String(), false)) g.FoundEquipables[sh] = true } if osh != NoShield { g.Equipables[g.Player.Pos] = osh g.Printf("You put the %s on and leave your %s.", sh, osh) } else { delete(g.Equipables, g.Player.Pos) g.Printf("You put the %s on.", sh) } } func (sh shield) String() (text string) { switch sh { case ConfusingShield: text = "confusing shield" case EarthShield: text = "earth shield" case BashingShield: text = "bashing shield" case FireShield: text = "fire shield" } return text } func (sh shield) Short() (text string) { switch sh { case ConfusingShield: text = "Cn" case EarthShield: text = "Er" case BashingShield: text = "Bs" case FireShield: text = "Fr" } return text } func (sh shield) Desc() (text string) { switch sh { case ConfusingShield: text = "A confusing shield can block an attack, sometimes confusing the monster." case EarthShield: text = "An earth shield offers great protection, but impact sound can disintegrate nearby walls." case BashingShield: text = "A bashing shield can block an attack and push the ennemy away." case FireShield: text = "A fire shield can block an attack, sometimes burning nearby foliage." } return text } func (sh shield) Letter() rune { return ']' } func (sh shield) Block() (block int) { switch sh { case ConfusingShield, BashingShield, FireShield: block += 10 case EarthShield: block += 15 } return block } boohu-0.13.0/js.go000066400000000000000000000257551356500202200136630ustar00rootroot00000000000000// +build js package main import ( "encoding/base64" "errors" "fmt" "log" "runtime" "time" "unicode/utf8" "syscall/js" ) func main() { ui := &gameui{} err := ui.Init() if err != nil { log.Fatalf("boohu: %v\n", err) } defer ui.Close() GameConfig.Tiles = true GameConfig.Version = Version LinkColors() GameConfig.DarkLOS = true ApplyDarkLOS() go func() { for { ui.ReqAnimFrame() } }() for { newGame(ui) } } func newGame(ui *gameui) { g := &game{} ui.g = g load, err := g.LoadConfig() if load && err != nil { log.Printf("Error loading config: %v\n", err) } else if load { CustomKeys = true } ApplyConfig() ui.PostConfig() if runtime.GOARCH != "wasm" { ui.DrawWelcome() } else { again := ui.HandleStartMenu() if again { return } } load, err = g.Load() if !load { g.InitLevel() } else if err != nil { g.InitLevel() g.Printf("Error loading saved game… starting new game. (%v)", err) } else { ui.DrawBufferInit() } g.ui = ui g.EventLoop() ui.Clear() ui.DrawColoredText("Do you want to collect some more simellas today?\n\n───Click or press any key to play again───", 7, 5, ColorFg) ui.DrawText(SaveError, 0, 10) ui.Flush() ui.PressAnyKey() } func (ui *gameui) HandleStartMenu() (again bool) { l := ui.DrawWelcomeCommon() g := ui.g for { a := ui.StartMenu(l) switch a { case StartWatchReplay: err := g.LoadReplay() if err != nil { ui.ColorLine(l+1, ColorRed) ui.Flush() time.Sleep(25 * time.Millisecond) log.Printf("Load replay: %v", err) return true } small := GameConfig.Small GameConfig.Small = true ui.ApplyToggleLayoutWithClear(false) ui.RestartDrawBuffers() ui.Replay() if small { GameConfig.Small = false ui.ApplyToggleLayoutWithClear(false) } return true default: return false } } } var SaveError string type gameui struct { g *game cursor position display js.Value cache map[UICell]js.Value ctx js.Value width int height int mousepos position menuHover menu itemHover int } func (ui *gameui) InitElements() error { canvas := js.Global().Get("document").Call("getElementById", "gamecanvas") canvas.Call("addEventListener", "contextmenu", js.FuncOf(func(this js.Value, args []js.Value) interface{} { e := args[0] e.Call("preventDefault") return nil }), false) canvas.Call("setAttribute", "tabindex", "1") ui.ctx = canvas.Call("getContext", "2d") ui.ctx.Set("imageSmoothingEnabled", false) ui.width = 16 ui.height = 24 canvas.Set("height", 24*UIHeight) canvas.Set("width", 16*UIWidth) ui.cache = make(map[UICell]js.Value) return nil } func (ui *gameui) Draw(cell UICell, x, y int) { var canvas js.Value if cv, ok := ui.cache[cell]; ok { canvas = cv } else { canvas = js.Global().Get("document").Call("createElement", "canvas") canvas.Set("width", 16) canvas.Set("height", 24) ctx := canvas.Call("getContext", "2d") ctx.Set("imageSmoothingEnabled", false) buf := getImage(cell).Pix ua := js.Global().Get("Uint8Array").New(js.ValueOf(len(buf))) js.CopyBytesToJS(ua, buf) ca := js.Global().Get("Uint8ClampedArray").New(ua) imgdata := js.Global().Get("ImageData").New(ca, 16, 24) ctx.Call("putImageData", imgdata, 0, 0) ui.cache[cell] = canvas } ui.ctx.Call("drawImage", canvas, x*ui.width, ui.height*y) } func (ui *gameui) GetMousePos(evt js.Value) (int, int) { canvas := js.Global().Get("document").Call("getElementById", "gamecanvas") rect := canvas.Call("getBoundingClientRect") scaleX := canvas.Get("width").Float() / rect.Get("width").Float() scaleY := canvas.Get("height").Float() / rect.Get("height").Float() x := (evt.Get("clientX").Float() - rect.Get("left").Float()) * scaleX y := (evt.Get("clientY").Float() - rect.Get("top").Float()) * scaleY return (int(x) - 1) / ui.width, (int(y) - 1) / ui.height } // io compatibility functions func (g *game) DataDir() (string, error) { return "", nil } func (g *game) Save() error { if runtime.GOARCH != "wasm" { return errors.New("Saving games is not available in the web html version.") // TODO remove when it works } save, err := g.GameSave() if err != nil { SaveError = err.Error() return err } storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { SaveError = "localStorage not found" return errors.New("localStorage not found") } s := base64.StdEncoding.EncodeToString(save) storage.Call("setItem", "boohusave", s) SaveError = "" return nil } func (g *game) SaveConfig() error { if runtime.GOARCH != "wasm" { return nil } conf, err := GameConfig.ConfigSave() if err != nil { SaveError = err.Error() return err } storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { SaveError = "localStorage not found" return errors.New("localStorage not found") } s := base64.StdEncoding.EncodeToString(conf) storage.Call("setItem", "boohuconfig", s) SaveError = "" return nil } func (g *game) SaveReplay() error { if runtime.GOARCH != "wasm" { return nil } storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { SaveError = "localStorage not found" return errors.New("localStorage not found") } data, err := g.EncodeDrawLog() if err != nil { return err } s := base64.StdEncoding.EncodeToString(data) storage.Call("setItem", "boohureplay", s) SaveError = "" return nil } func (g *game) RemoveSaveFile() error { storage := js.Global().Get("localStorage") storage.Call("removeItem", "boohusave") return nil } func (g *game) RemoveDataFile(file string) error { storage := js.Global().Get("localStorage") storage.Call("removeItem", file) return nil } func (g *game) Load() (bool, error) { storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { return true, errors.New("localStorage not found") } save := storage.Call("getItem", "boohusave") if save.Type() != js.TypeString || runtime.GOARCH != "wasm" { return false, nil } s, err := base64.StdEncoding.DecodeString(save.String()) if err != nil { return true, err } lg, err := g.DecodeGameSave(s) if err != nil { return true, err } *g = *lg // // XXX: gob encoding works badly with gopherjs, it seems, some maps get broken return true, nil } func (g *game) LoadConfig() (bool, error) { storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { return true, errors.New("localStorage not found") } conf := storage.Call("getItem", "boohuconfig") if conf.Type() != js.TypeString || runtime.GOARCH != "wasm" { return false, nil } s, err := base64.StdEncoding.DecodeString(conf.String()) if err != nil { return true, err } c, err := g.DecodeConfigSave(s) if err != nil { return true, err } if c.Version != GameConfig.Version { return true, errors.New("Version mismatch, could not load old custom configuration.") } GameConfig = *c return true, nil } func (g *game) LoadReplay() error { storage := js.Global().Get("localStorage") if storage.Type() != js.TypeObject { return errors.New("localStorage not found") } save := storage.Call("getItem", "boohureplay") if save.Type() != js.TypeString || runtime.GOARCH != "wasm" { return errors.New("invalid storage") } data, err := base64.StdEncoding.DecodeString(save.String()) if err != nil { return err } dl, err := g.DecodeDrawLog(data) if err != nil { return err } g.DrawLog = dl return nil } func (g *game) WriteDump() error { pre := js.Global().Get("document").Call("getElementById", "dump") pre.Set("innerHTML", g.Dump()) err := g.SaveReplay() if err != nil { return fmt.Errorf("writing replay: %v", err) } return nil } // End of io compatibility functions func (ui *gameui) Init() error { canvas := js.Global().Get("document").Call("getElementById", "gamecanvas") gamediv := js.Global().Get("document").Call("getElementById", "gamediv") js.Global().Get("document").Call( "addEventListener", "keydown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { e := args[0] if !e.Get("ctrlKey").Bool() && !e.Get("metaKey").Bool() { e.Call("preventDefault") } else { return nil } s := e.Get("key").String() if s == "F11" { screenfull := js.Global().Get("screenfull") if screenfull.Get("enabled").Bool() { screenfull.Call("toggle", gamediv) } return nil } if s == "Unidentified" { s = e.Get("code").String() } ch <- uiInput{key: s} return nil })) canvas.Call( "addEventListener", "mousedown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { e := args[0] x, y := ui.GetMousePos(e) ch <- uiInput{mouse: true, mouseX: x, mouseY: y, button: e.Get("button").Int()} return nil })) canvas.Call( "addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) interface{} { if CenteredCamera { return nil } e := args[0] x, y := ui.GetMousePos(e) if x != ui.mousepos.X || y != ui.mousepos.Y { ui.mousepos.X = x ui.mousepos.Y = y ch <- uiInput{mouse: true, mouseX: x, mouseY: y, button: -1} } return nil })) ui.menuHover = -1 ui.InitElements() SolarizedPalette() ui.HideCursor() settingsActions = append(settingsActions, toggleTiles) return nil } var ch chan uiInput var interrupt chan bool func init() { ch = make(chan uiInput, 100) interrupt = make(chan bool) Flushdone = make(chan bool) ReqFrame = make(chan bool) } func (ui *gameui) Close() { // TODO } func (ui *gameui) Flush() { ReqFrame <- true <-Flushdone } func (ui *gameui) ReqAnimFrame() { <-ReqFrame js.Global().Get("window").Call("requestAnimationFrame", js.FuncOf(func(this js.Value, args []js.Value) interface{} { ui.FlushCallback(args[0]); return nil })) } func (ui *gameui) ApplyToggleLayoutWithClear(clear bool) { GameConfig.Small = !GameConfig.Small if GameConfig.Small { if clear { ui.Clear() ui.Flush() } UIHeight = 24 UIWidth = 80 } else { UIHeight = 26 UIWidth = 100 } canvas := js.Global().Get("document").Call("getElementById", "gamecanvas") canvas.Set("height", 24*UIHeight) canvas.Set("width", 16*UIWidth) ui.g.DrawBuffer = make([]UICell, UIWidth*UIHeight) ui.cache = make(map[UICell]js.Value) if clear { ui.Clear() } } func (ui *gameui) ApplyToggleLayout() { ui.ApplyToggleLayoutWithClear(true) } var Flushdone chan bool var ReqFrame chan bool func (ui *gameui) FlushCallback(t js.Value) { ui.DrawLogFrame() for _, cdraw := range ui.g.DrawLog[len(ui.g.DrawLog)-1].Draws { cell := cdraw.Cell ui.Draw(cell, cdraw.X, cdraw.Y) } Flushdone <- true } func (ui *gameui) PollEvent() (in uiInput) { select { case in = <-ch: case in.interrupt = <-interrupt: } switch in.key { case "Escape", "Space": in.key = "\x1b" case "Enter", "\r", "\n": in.key = "." case "ArrowLeft": in.key = "4" case "ArrowRight": in.key = "6" case "ArrowUp", "BackSpace": in.key = "8" case "ArrowDown": in.key = "2" case "Home": in.key = "7" case "End": in.key = "1" case "PageUp": in.key = "9" case "PageDown": in.key = "3" case "Numpad5", "Delete": in.key = "5" default: if utf8.RuneCountInString(in.key) != 1 { in.key = "" } } return in } boohu-0.13.0/log.go000066400000000000000000000036331356500202200140170ustar00rootroot00000000000000package main import "fmt" type logStyle int const ( logNormal logStyle = iota logCritic logPlayerHit logMonsterHit logSpecial logStatusEnd logError ) type logEntry struct { Text string Index int Tick bool Style logStyle Dups int } func (e logEntry) String() string { if e.Dups > 0 { return fmt.Sprintf("%s (%d×)", e.Text, e.Dups+1) } return e.Text } func (g *game) Print(s string) { e := logEntry{Text: s, Index: g.LogIndex} g.PrintEntry(e) } func (g *game) PrintStyled(s string, style logStyle) { e := logEntry{Text: s, Index: g.LogIndex, Style: style} g.PrintEntry(e) } func (g *game) Printf(format string, a ...interface{}) { e := logEntry{Text: fmt.Sprintf(format, a...), Index: g.LogIndex} g.PrintEntry(e) } func (g *game) PrintfStyled(format string, style logStyle, a ...interface{}) { e := logEntry{Text: fmt.Sprintf(format, a...), Index: g.LogIndex, Style: style} g.PrintEntry(e) } func (g *game) PrintEntry(e logEntry) { if e.Index == g.LogNextTick { e.Tick = true } if !e.Tick && len(g.Log) > 0 { le := g.Log[len(g.Log)-1] if le.Text == e.Text { le.Dups++ g.Log[len(g.Log)-1] = le return } } g.Log = append(g.Log, e) g.LogIndex++ if len(g.Log) > 100000 { g.Log = g.Log[10000:] } } func (g *game) StoryPrint(s string) { g.Stats.Story = append(g.Stats.Story, fmt.Sprintf("Depth %2d|Turn %5d| %s", g.Depth, g.Turn/10, s)) } func (g *game) StoryPrintf(format string, a ...interface{}) { g.Stats.Story = append(g.Stats.Story, fmt.Sprintf("Depth %2d|Turn %5d| %s", g.Depth, g.Turn/10, fmt.Sprintf(format, a...))) } func (g *game) CrackSound() (text string) { switch RandInt(4) { case 0: text = "Crack!" case 1: text = "Crash!" case 2: text = "Crunch!" case 3: text = "Creak!" } return text } func (g *game) ExplosionSound() (text string) { switch RandInt(3) { case 0: text = "Bang!" case 1: text = "Pop!" case 2: text = "Boom!" } return text } boohu-0.13.0/los.go000066400000000000000000000144001356500202200140250ustar00rootroot00000000000000package main type raynode struct { Cost int } type rayMap map[position]raynode func (g *game) bestParent(rm rayMap, from, pos position) (position, int) { p := pos.Parents(from) b := p[0] if len(p) > 1 && rm[p[1]].Cost+g.losCost(p[1]) < rm[b].Cost+g.losCost(b) { b = p[1] } return b, rm[b].Cost + g.losCost(b) } func (g *game) losCost(pos position) int { if g.Player.Pos == pos { return 0 } c := g.Dungeon.Cell(pos) if c.T == WallCell { return g.LosRange() } if _, ok := g.Clouds[pos]; ok { return g.LosRange() } if _, ok := g.Doors[pos]; ok { if pos != g.Player.Pos { mons := g.MonsterAt(pos) if !mons.Exists() { return g.LosRange() } } } if _, ok := g.Fungus[pos]; ok { return g.LosRange() - 1 } return 1 } func (g *game) buildRayMap(from position, distance int) rayMap { rm := rayMap{} rm[from] = raynode{Cost: 0} for d := 1; d <= distance; d++ { for x := -d + from.X; x <= d+from.X; x++ { for _, pos := range []position{{x, from.Y + d}, {x, from.Y - d}} { if !pos.valid() { continue } _, c := g.bestParent(rm, from, pos) rm[pos] = raynode{Cost: c} } } for y := -d + 1 + from.Y; y <= d-1+from.Y; y++ { for _, pos := range []position{{from.X + d, y}, {from.X - d, y}} { if !pos.valid() { continue } _, c := g.bestParent(rm, from, pos) rm[pos] = raynode{Cost: c} } } } return rm } func (g *game) LosRange() int { losRange := 6 if g.Player.Armour == ShinyPlates { losRange++ } if g.Player.Aptitudes[AptStealthyLOS] { losRange -= 2 } if g.Player.Armour == HarmonistRobe { losRange -= 1 } if g.Player.Weapon == Frundis { losRange -= 1 } if g.Player.HasStatus(StatusShadows) { losRange = 1 } if losRange < 1 { losRange = 1 } return losRange } func (g *game) StopAuto() { if g.Autoexploring && !g.AutoHalt { g.Print("You stop exploring.") } else if g.AutoDir != NoDir { g.Print("You stop.") } else if g.AutoTarget != InvalidPos { g.Print("You stop.") } g.AutoHalt = true g.AutoDir = NoDir g.AutoTarget = InvalidPos if g.Resting { g.Stats.RestInterrupt++ g.Resting = false g.Print("You could not sleep.") } } func (g *game) ComputeLOS() { m := map[position]bool{} losRange := g.LosRange() g.Player.Rays = g.buildRayMap(g.Player.Pos, losRange) for pos, n := range g.Player.Rays { if n.Cost < g.LosRange() { m[pos] = true g.SeePosition(pos) } } g.Player.LOS = m for _, mons := range g.Monsters { if mons.Exists() && g.Player.LOS[mons.Pos] { if mons.Seen { g.StopAuto() continue } mons.Seen = true g.Printf("You see %s (%v).", mons.Kind.Indefinite(false), mons.State) if mons.Kind.Dangerousness() > 10 { g.StoryPrint(mons.Kind.SeenStoryText()) } g.StopAuto() } } } func (g *game) SeePosition(pos position) { if !g.Dungeon.Cell(pos).Explored { see := "see" if c, ok := g.Collectables[pos]; ok { if c.Quantity > 1 { g.Printf("You %s %d %s.", see, c.Quantity, c.Consumable.Plural()) } else { g.Printf("You %s %s.", see, Indefinite(c.Consumable.String(), false)) } g.StopAuto() } else if _, ok := g.Stairs[pos]; ok { g.Printf("You %s stairs.", see) g.StopAuto() } else if eq, ok := g.Equipables[pos]; ok { g.Printf("You %s %s.", see, Indefinite(eq.String(), false)) g.StopAuto() } else if rd, ok := g.Rods[pos]; ok { g.Printf("You %s %s.", see, Indefinite(rd.String(), false)) g.StopAuto() } else if stn, ok := g.MagicalStones[pos]; ok { g.Printf("You %s %s.", see, Indefinite(stn.String(), false)) g.StopAuto() } g.FunAction() g.Dungeon.SetExplored(pos) g.DijkstraMapRebuild = true } else { if g.WrongWall[pos] { g.Printf("There is no longer a wall there.") g.StopAuto() g.DijkstraMapRebuild = true } if cld, ok := g.Clouds[pos]; ok && cld == CloudFire && (g.WrongDoor[pos] || g.WrongFoliage[pos]) { g.Printf("There are flames there.") g.StopAuto() g.DijkstraMapRebuild = true } } if g.WrongWall[pos] { delete(g.WrongWall, pos) if g.Dungeon.Cell(pos).T == FreeCell { delete(g.TemporalWalls, pos) } } if _, ok := g.WrongDoor[pos]; ok { delete(g.WrongDoor, pos) } if _, ok := g.WrongFoliage[pos]; ok { delete(g.WrongFoliage, pos) } if _, ok := g.DreamingMonster[pos]; ok { delete(g.DreamingMonster, pos) } } func (g *game) ComputeExclusion(pos position, toggle bool) { exclusionRange := g.LosRange() g.ExclusionsMap[pos] = toggle for d := 1; d <= exclusionRange; d++ { for x := -d + pos.X; x <= d+pos.X; x++ { for _, pos := range []position{{x, pos.Y + d}, {x, pos.Y - d}} { if !pos.valid() { continue } g.ExclusionsMap[pos] = toggle } } for y := -d + 1 + pos.Y; y <= d-1+pos.Y; y++ { for _, pos := range []position{{pos.X + d, y}, {pos.X - d, y}} { if !pos.valid() { continue } g.ExclusionsMap[pos] = toggle } } } } func (g *game) Ray(pos position) []position { if !g.Player.LOS[pos] { return nil } ray := []position{} for pos != g.Player.Pos { ray = append(ray, pos) pos, _ = g.bestParent(g.Player.Rays, g.Player.Pos, pos) } return ray } func (g *game) ComputeRayHighlight(pos position) { g.Highlight = map[position]bool{} ray := g.Ray(pos) for _, p := range ray { g.Highlight[p] = true } } func (g *game) ComputeNoise() { dij := &noisePath{game: g} rg := g.LosRange() + 2 if rg <= 5 { rg++ } if g.Player.Aptitudes[AptHear] { rg++ } nm := Dijkstra(dij, []position{g.Player.Pos}, rg) count := 0 noise := map[position]bool{} rmax := 3 if g.Player.Aptitudes[AptHear] { rmax-- } for pos := range nm { if g.Player.LOS[pos] { continue } mons := g.MonsterAt(pos) if mons.Exists() && mons.State != Resting && RandInt(rmax) == 0 { switch mons.Kind { case MonsMirrorSpecter, MonsSatowalgaPlant: // no footsteps case MonsTinyHarpy, MonsWingedMilfid, MonsGiantBee: noise[pos] = true g.Print("You hear the flapping of wings.") count++ case MonsOgre, MonsCyclop, MonsBrizzia, MonsHydra, MonsEarthDragon, MonsTreeMushroom: noise[pos] = true g.Print("You hear heavy footsteps.") count++ case MonsWorm, MonsAcidMound: noise[pos] = true g.Print("You hear a creep noise.") count++ default: noise[pos] = true g.Print("You hear footsteps.") count++ } } } if count > 0 { g.StopAuto() } g.Noise = noise } boohu-0.13.0/main.go000066400000000000000000000033061356500202200141570ustar00rootroot00000000000000// +build !js package main import ( "flag" "fmt" "log" "os" "runtime" ) func main() { optSolarized := flag.Bool("s", false, "Use true 16-color solarized palette") optVersion := flag.Bool("v", false, "print version number") optCenteredCamera := flag.Bool("c", false, "centered camera") color8 := false if runtime.GOOS == "windows" { color8 = true } opt8colors := flag.Bool("o", color8, "use only 8-color palette") opt256colors := flag.Bool("x", !color8, "use xterm 256-color palette (solarized approximation)") optNoAnim := flag.Bool("n", false, "no animations") optReplay := flag.String("r", "", "path to replay file") flag.Parse() if *optSolarized { SolarizedPalette() } else if color8 && !*opt256colors || !color8 && *opt8colors { SolarizedPalette() Simple8ColorPalette() } if *optVersion { fmt.Println(Version) os.Exit(0) } if *optReplay != "" { err := Replay(*optReplay) if err != nil { log.Printf("boohu: replay: %v\n", err) os.Exit(1) } os.Exit(0) } if *optCenteredCamera { CenteredCamera = true } if *optNoAnim { DisableAnimations = true } ui := &gameui{} g := &game{} ui.g = g err := ui.Init() if err != nil { fmt.Fprintf(os.Stderr, "boohu: %v\n", err) os.Exit(1) } defer ui.Close() LinkColors() GameConfig.DarkLOS = true load, err := g.LoadConfig() if load && err != nil { g.Print("Error loading config file.") } else if load { CustomKeys = true } ApplyConfig() ui.PostConfig() ui.DrawWelcome() load, err = g.Load() if !load { g.InitLevel() } else if err != nil { g.InitLevel() g.PrintfStyled("Error: %v", logError, err) g.PrintStyled("Could not load saved game… starting new game.", logError) } g.ui = ui g.EventLoop() } boohu-0.13.0/monster.go000066400000000000000000002627301356500202200147320ustar00rootroot00000000000000package main import "fmt" type monsterState int const ( Resting monsterState = iota Hunting Wandering ) func (m monsterState) String() string { var st string switch m { case Resting: st = "resting" case Wandering: st = "wandering" case Hunting: st = "hunting" } return st } type monsterStatus int const ( MonsConfused monsterStatus = iota MonsExhausted MonsSlow MonsLignified ) const NMonsStatus = int(MonsLignified) + 1 func (st monsterStatus) String() (text string) { switch st { case MonsConfused: text = "confused" case MonsExhausted: text = "exhausted" case MonsSlow: text = "slowed" case MonsLignified: text = "lignified" } return text } type monsterKind int const ( MonsGoblin monsterKind = iota MonsTinyHarpy MonsOgre MonsCyclop MonsWorm MonsBrizzia MonsHound MonsYack MonsGiantBee MonsGoblinWarrior MonsHydra MonsSkeletonWarrior MonsSpider MonsWingedMilfid MonsBlinkingFrog MonsLich MonsEarthDragon MonsMirrorSpecter MonsAcidMound MonsExplosiveNadre MonsSatowalgaPlant MonsMadNixe MonsMindCelmist MonsVampire MonsTreeMushroom MonsMarevorHelith ) func (mk monsterKind) String() string { return MonsData[mk].name } func (mk monsterKind) MovementDelay() int { return MonsData[mk].movementDelay } func (mk monsterKind) Letter() rune { return MonsData[mk].letter } func (mk monsterKind) AttackDelay() int { return MonsData[mk].attackDelay } func (mk monsterKind) BaseAttack() int { return MonsData[mk].baseAttack } func (mk monsterKind) MaxHP() int { return MonsData[mk].maxHP } func (mk monsterKind) Dangerousness() int { return MonsData[mk].dangerousness } func (mk monsterKind) Ranged() bool { switch mk { case MonsLich, MonsCyclop, MonsGoblinWarrior, MonsSatowalgaPlant, MonsMadNixe, MonsVampire, MonsTreeMushroom: return true default: return false } } func (mk monsterKind) Smiting() bool { switch mk { case MonsMirrorSpecter, MonsMindCelmist: return true default: return false } } func (mk monsterKind) Desc() string { return monsDesc[mk] } func (mk monsterKind) SeenStoryText() (text string) { switch mk { case MonsMarevorHelith: text = "Saw Marevor." default: text = fmt.Sprintf("Saw %s.", Indefinite(mk.String(), false)) } return text } func (mk monsterKind) Indefinite(capital bool) (text string) { switch mk { case MonsMarevorHelith: text = mk.String() default: text = Indefinite(mk.String(), capital) } return text } func (mk monsterKind) Definite(capital bool) (text string) { switch mk { case MonsMarevorHelith: text = mk.String() default: if capital { text = fmt.Sprintf("The %s", mk.String()) } else { text = fmt.Sprintf("the %s", mk.String()) } } return text } func (mk monsterKind) Living() bool { switch mk { case MonsLich, MonsSkeletonWarrior, MonsMarevorHelith: return false default: return true } } type monsterData struct { movementDelay int baseAttack int attackDelay int maxHP int accuracy int armor int evasion int letter rune name string dangerousness int } var MonsData = []monsterData{ MonsGoblin: {10, 7, 10, 15, 14, 0, 12, 'g', "goblin", 2}, MonsTinyHarpy: {10, 8, 10, 14, 14, 0, 14, 't', "tiny harpy", 3}, MonsOgre: {10, 15, 12, 28, 13, 0, 8, 'O', "ogre", 6}, MonsCyclop: {10, 12, 12, 28, 13, 0, 8, 'C', "cyclops", 9}, MonsWorm: {12, 9, 10, 25, 13, 0, 10, 'w', "farmer worm", 3}, MonsBrizzia: {12, 10, 10, 30, 13, 0, 10, 'z', "brizzia", 7}, MonsAcidMound: {10, 9, 10, 19, 16, 0, 8, 'a', "acid mound", 7}, MonsHound: {8, 9, 10, 15, 14, 0, 12, 'h', "hound", 4}, MonsYack: {10, 11, 10, 21, 14, 0, 10, 'y', "yack", 6}, MonsGiantBee: {6, 10, 10, 11, 15, 0, 15, 'B', "giant bee", 6}, MonsGoblinWarrior: {10, 11, 10, 22, 15, 3, 12, 'G', "goblin warrior", 8}, MonsHydra: {10, 9, 10, 45, 13, 0, 6, 'H', "hydra", 15}, MonsSkeletonWarrior: {10, 12, 10, 25, 15, 4, 12, 'S', "skeleton warrior", 10}, MonsSpider: {8, 7, 10, 13, 17, 0, 15, 's', "spider", 6}, MonsWingedMilfid: {8, 9, 10, 17, 15, 0, 13, 'W', "winged milfid", 7}, MonsBlinkingFrog: {10, 10, 10, 20, 15, 0, 12, 'F', "blinking frog", 7}, MonsLich: {10, 10, 10, 23, 15, 3, 12, 'L', "lich", 16}, MonsEarthDragon: {10, 14, 10, 40, 14, 6, 8, 'D', "earth dragon", 20}, MonsMirrorSpecter: {10, 10, 10, 18, 15, 0, 17, 'm', "mirror specter", 11}, MonsExplosiveNadre: {10, 6, 10, 3, 14, 0, 10, 'n', "explosive nadre", 6}, MonsSatowalgaPlant: {10, 12, 12, 30, 15, 0, 4, 'P', "satowalga plant", 7}, MonsMadNixe: {10, 11, 10, 20, 15, 0, 15, 'N', "mad nixe", 12}, MonsMindCelmist: {10, 9, 20, 18, 99, 0, 14, 'c', "mind celmist", 14}, MonsVampire: {10, 9, 10, 21, 17, 0, 15, 'V', "vampire", 13}, MonsTreeMushroom: {12, 15, 12, 38, 14, 4, 6, 'T', "tree mushroom", 17}, MonsMarevorHelith: {10, 0, 10, 97, 18, 10, 15, 'M', "Marevor Helith", 18}, } var monsDesc = []string{ MonsGoblin: "Goblins are little humanoid creatures. They often appear in a group.", MonsTinyHarpy: "Tiny harpies are little humanoid flying creatures. They blink away when hurt. They often appear in a group.", MonsOgre: "Ogres are big clunky humanoids that can hit really hard.", MonsCyclop: "Cyclopes are very similar to ogres, but they also like to throw rocks at their foes (for up to 15 damage). The rocks can block your way for a while.", MonsWorm: "Farmer worms are ugly slow moving creatures, but surprisingly hardy at times, and they furrow as they move, helping new foliage to grow.", MonsBrizzia: "Brizzias are big slow moving biped creatures. They are quite hardy, and when hurt they can cause nausea, impeding the use of potions.", MonsAcidMound: "Acid mounds are acidic creatures. They can temporarily corrode your equipment.", MonsHound: "Hounds are fast moving carnivore quadrupeds. They can bark, and smell you.", MonsYack: "Yacks are quite large herbivorous quadrupeds. They tend to form large groups, and can push you one cell away.", MonsGiantBee: "Giant bees are fragile but extremely fast moving creatures. Their bite can sometimes enrage you.", MonsGoblinWarrior: "Goblin warriors are goblins that learned to fight, and got equipped with leather armour. They can throw javelins.", MonsHydra: "Hydras are enormous creatures with four heads that can hit you each at once.", MonsSkeletonWarrior: "Skeleton warriors are good fighters, clad in chain mail.", MonsSpider: "Spiders are fast moving fragile creatures, whose bite can confuse you.", MonsWingedMilfid: "Winged milfids are fast moving humanoids that can fly over you and make you swap positions. They tend to be very agressive creatures.", MonsBlinkingFrog: "Blinking frogs are big frog-like creatures, whose bite can make you blink away.", MonsLich: "Liches are non-living mages wearing a leather armour. They can throw a bolt of torment at you, halving your HP.", MonsEarthDragon: "Earth dragons are big and hardy creatures that wander in the Underground. It is said they can be credited for many of the tunnels.", MonsMirrorSpecter: "Mirror specters are very insubstantial creatures, which can absorb your mana.", MonsExplosiveNadre: "Explosive nadres are very frail creatures that explode upon dying, halving HP of any adjacent creatures and occasionally destroying walls.", MonsSatowalgaPlant: "Satowalga Plants are immobile bushes that throw acidic projectiles at you, sometimes corroding and confusing you.", MonsMadNixe: "Mad nixes are magical humanoids that can attract you to them.", MonsMindCelmist: "Mind celmists are mages that use magical smitting mind attacks that bypass armour. They can occasionally confuse or slow you. They try to avoid melee.", MonsVampire: "Vampires are humanoids that drink blood to survive. Their spitting can cause nausea, impeding the use of potions.", MonsTreeMushroom: "Tree mushrooms are big clunky slow-moving creatures. They can throw lignifying spores at you.", MonsMarevorHelith: "Marevor Helith is an ancient undead nakrus very fond of teleporting people away. He is a well-known expert in the field of magaras - items that many people simply call magical objects. His current research focus is monolith creation. Marevor, a repentant necromancer, is now searching for his old disciple Jaixel in the Underground to help him overcome the past.", } type monsterBand int const ( LoneGoblin monsterBand = iota LoneOgre LoneWorm LoneRareWorm LoneBrizzia LoneHound LoneHydra LoneSpider LoneMilfid LoneBlinkingFrog LoneCyclop LoneLich LoneEarthDragon LoneSpecter LoneAcidMound LoneExplosiveNadre LoneSatowalgaPlant LoneMindCelmist LoneVampire LoneTreeMushroom LoneEarlyNixe LoneEarlyAcidMound LoneEarlyBrizzia LoneEarlySpecter LoneEarlySatowalgaPlant LoneEarlyEarthDragon LoneEarlyHydra LoneEarlyLich LoneEarlyMindCelmist LoneEarlyVampire LoneEarlyTreeMushroom BandGoblins BandGoblinsMany BandGoblinsHound BandGoblinsOgre BandGoblinsWithWarriors BandGoblinsWithWarriorsMilfid BandGoblinsWithWarriorsHound BandGoblinsWithWarriorsOgre BandGoblinWarriors BandGoblinWarriorsMilfid BandHounds BandHoundsMany BandYacksGoblin BandYacksMilfid BandYacksMany BandSpiders BandSpidersMilfid BandWingedMilfids BandSatowalga BandBlinkingFrogs BandExplosiveFrog BandExplosiveBrizzia BandGiantBees BandGiantBeesMany BandSkeletonWarrior BandTreeMushroomWorms BandTreeMushrooms BandMindCelmists BandMindCelmistsLich BandMindCelmistsMadNixe BandMadNixes BandMadNixesDragon BandMadNixesHydra BandMadNixesFrogs BandVampires BandVampireNixe BandVampireCelmist UBandTinyHarpy UBandWorms UBandGoblinsEasy UBandFrogs UBandOgres UBandGoblins UBandBeeYacks UBandMadNixes UBandMindCelmist UHydras UExplosiveNadres ULich UVampires UBrizzias UAcidMounds USatowalga UDragon UMarevorHelith UXCyclops UXLiches UXFrogRanged UXExplosive UXWarriors UXSatowalgaNixe UXSpecters UXDisabling UXMadNixeSpecter UXMadNixeCyclop UXMadNixeHydra UXMadNixes UXVampires UXTreeMushrooms UXMindCelmists UXMilfidYack UXYacks UXVariedWarriors ) type monsInterval struct { Min int Max int } type monsterBandData struct { Distribution map[monsterKind]monsInterval Rarity int MinDepth int MaxDepth int Band bool Monster monsterKind Unique bool } func (g *game) GenBand(mbd monsterBandData, band monsterBand) []monsterKind { if g.GeneratedUniques[band] > 0 && mbd.Unique { return nil } if g.Depth > mbd.MaxDepth { return nil } if g.Depth < mbd.MinDepth { return nil } if !mbd.Band { return []monsterKind{mbd.Monster} } bandMonsters := []monsterKind{} for m, interval := range mbd.Distribution { for i := 0; i < interval.Min+RandInt(interval.Max-interval.Min+1); i++ { bandMonsters = append(bandMonsters, m) } } return bandMonsters } var MonsBands = []monsterBandData{ LoneGoblin: {Rarity: 2, MinDepth: 1, MaxDepth: 2, Monster: MonsGoblin}, LoneOgre: {Rarity: 4, MinDepth: 2, MaxDepth: 7, Monster: MonsOgre}, LoneWorm: {Rarity: 2, MinDepth: 1, MaxDepth: 3, Monster: MonsWorm}, LoneRareWorm: {Rarity: 13, MinDepth: 4, MaxDepth: WinDepth + 1, Monster: MonsWorm}, LoneBrizzia: {Rarity: 13, MinDepth: 4, MaxDepth: WinDepth + 1, Monster: MonsBrizzia}, LoneHound: {Rarity: 5, MinDepth: 1, MaxDepth: 5, Monster: MonsHound}, LoneHydra: {Rarity: 10, MinDepth: 5, MaxDepth: WinDepth + 1, Monster: MonsHydra}, LoneSpider: {Rarity: 3, MinDepth: 3, MaxDepth: WinDepth + 1, Monster: MonsSpider}, LoneMilfid: {Rarity: 13, MinDepth: 3, MaxDepth: WinDepth + 1, Monster: MonsWingedMilfid}, LoneBlinkingFrog: {Rarity: 7, MinDepth: 3, MaxDepth: WinDepth + 1, Monster: MonsBlinkingFrog}, LoneCyclop: {Rarity: 4, MinDepth: 3, MaxDepth: WinDepth + 1, Monster: MonsCyclop}, LoneLich: {Rarity: 8, MinDepth: 5, MaxDepth: WinDepth + 1, Monster: MonsLich}, LoneEarthDragon: {Rarity: 9, MinDepth: 6, MaxDepth: WinDepth + 1, Monster: MonsEarthDragon}, LoneSpecter: {Rarity: 7, MinDepth: 4, MaxDepth: WinDepth + 1, Monster: MonsMirrorSpecter}, LoneAcidMound: {Rarity: 7, MinDepth: 4, MaxDepth: WinDepth + 1, Monster: MonsAcidMound}, LoneExplosiveNadre: {Rarity: 5, MinDepth: 2, MaxDepth: 4, Monster: MonsExplosiveNadre}, LoneSatowalgaPlant: {Rarity: 9, MinDepth: 4, MaxDepth: WinDepth + 1, Monster: MonsSatowalgaPlant}, LoneMindCelmist: {Rarity: 12, MinDepth: 5, MaxDepth: WinDepth + 1, Monster: MonsMindCelmist}, LoneVampire: {Rarity: 12, MinDepth: 5, MaxDepth: WinDepth + 1, Monster: MonsVampire}, LoneTreeMushroom: {Rarity: 15, MinDepth: 6, MaxDepth: WinDepth + 1, Monster: MonsTreeMushroom}, LoneEarlyNixe: {Rarity: 20, MinDepth: 1, MaxDepth: 4, Monster: MonsMadNixe, Unique: true}, LoneEarlyVampire: {Rarity: 30, MinDepth: 2, MaxDepth: 4, Monster: MonsVampire, Unique: true}, LoneEarlyAcidMound: {Rarity: 20, MinDepth: 1, MaxDepth: 3, Monster: MonsAcidMound, Unique: true}, LoneEarlyBrizzia: {Rarity: 20, MinDepth: 1, MaxDepth: 3, Monster: MonsBrizzia, Unique: true}, LoneEarlySpecter: {Rarity: 20, MinDepth: 1, MaxDepth: 3, Monster: MonsMirrorSpecter, Unique: true}, LoneEarlySatowalgaPlant: {Rarity: 20, MinDepth: 1, MaxDepth: 3, Monster: MonsSatowalgaPlant, Unique: true}, LoneEarlyEarthDragon: {Rarity: 30, MinDepth: 4, MaxDepth: 5, Monster: MonsEarthDragon, Unique: true}, LoneEarlyHydra: {Rarity: 30, MinDepth: 3, MaxDepth: 4, Monster: MonsHydra, Unique: true}, LoneEarlyLich: {Rarity: 30, MinDepth: 3, MaxDepth: 4, Monster: MonsLich, Unique: true}, LoneEarlyMindCelmist: {Rarity: 30, MinDepth: 3, MaxDepth: 4, Monster: MonsMindCelmist, Unique: true}, LoneEarlyTreeMushroom: {Rarity: 30, MinDepth: 4, MaxDepth: 5, Monster: MonsTreeMushroom, Unique: true}, BandGoblins: { Distribution: map[monsterKind]monsInterval{MonsGoblin: {2, 3}}, Rarity: 2, MinDepth: 1, MaxDepth: 3, Band: true, }, BandGoblinsMany: { Distribution: map[monsterKind]monsInterval{MonsGoblin: {4, 4}}, Rarity: 7, MinDepth: 2, MaxDepth: 3, Band: true, }, BandGoblinsHound: { Distribution: map[monsterKind]monsInterval{MonsGoblin: {2, 2}, MonsHound: {1, 1}}, Rarity: 4, MinDepth: 1, MaxDepth: 3, Band: true, }, BandGoblinsOgre: { Distribution: map[monsterKind]monsInterval{MonsGoblin: {1, 1}, MonsOgre: {1, 1}}, Rarity: 7, MinDepth: 2, MaxDepth: 3, Band: true, }, BandGoblinsWithWarriors: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsGoblinWarrior: {2, 2}}, Rarity: 7, MinDepth: 4, MaxDepth: 5, Band: true, }, BandGoblinsWithWarriorsMilfid: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsGoblinWarrior: {1, 1}, MonsWingedMilfid: {1, 1}}, Rarity: 8, MinDepth: 4, MaxDepth: 5, Band: true, }, BandGoblinsWithWarriorsHound: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsGoblinWarrior: {1, 1}, MonsHound: {1, 1}}, Rarity: 7, MinDepth: 4, MaxDepth: 5, Band: true, }, BandGoblinsWithWarriorsOgre: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsGoblinWarrior: {1, 1}, MonsOgre: {1, 1}}, Rarity: 7, MinDepth: 4, MaxDepth: 5, Band: true, }, BandGoblinWarriors: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {1, 1}, MonsGoblinWarrior: {3, 3}}, Rarity: 10, MinDepth: 6, MaxDepth: WinDepth + 1, Band: true, }, BandGoblinWarriorsMilfid: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {1, 1}, MonsGoblinWarrior: {2, 2}, MonsWingedMilfid: {1, 1}}, Rarity: 10, MinDepth: 6, MaxDepth: WinDepth + 1, Band: true, }, BandHounds: { Distribution: map[monsterKind]monsInterval{MonsHound: {2, 2}, MonsGoblin: {1, 1}}, Rarity: 6, MinDepth: 2, MaxDepth: 6, Band: true, }, BandHoundsMany: { Distribution: map[monsterKind]monsInterval{MonsHound: {3, 3}}, Rarity: 10, MinDepth: 2, MaxDepth: 6, Band: true, }, BandSpiders: { Distribution: map[monsterKind]monsInterval{MonsSpider: {2, 3}}, Rarity: 4, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandSpidersMilfid: { Distribution: map[monsterKind]monsInterval{MonsSpider: {2, 2}, MonsWingedMilfid: {1, 1}}, Rarity: 7, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandWingedMilfids: { Distribution: map[monsterKind]monsInterval{MonsWingedMilfid: {2, 3}}, Rarity: 9, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandBlinkingFrogs: { Distribution: map[monsterKind]monsInterval{MonsBlinkingFrog: {2, 4}}, Rarity: 7, MinDepth: 5, MaxDepth: WinDepth + 1, Band: true, }, BandSatowalga: { Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, }, Rarity: 10, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandExplosiveFrog: { Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {1, 1}, MonsExplosiveNadre: {2, 2}, MonsGiantBee: {1, 1}, }, Rarity: 10, MinDepth: 5, MaxDepth: WinDepth + 1, Band: true, }, BandExplosiveBrizzia: { Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsGiantBee: {1, 1}, MonsBrizzia: {1, 1}, }, Rarity: 10, MinDepth: 5, MaxDepth: WinDepth + 1, Band: true, }, BandYacksGoblin: { Distribution: map[monsterKind]monsInterval{MonsYack: {2, 2}, MonsGoblin: {1, 1}}, Rarity: 5, MinDepth: 3, MaxDepth: WinDepth - 1, Band: true, }, BandYacksMilfid: { Distribution: map[monsterKind]monsInterval{MonsYack: {2, 2}, MonsWingedMilfid: {1, 1}}, Rarity: 8, MinDepth: 3, MaxDepth: WinDepth - 1, Band: true, }, BandYacksMany: { Distribution: map[monsterKind]monsInterval{MonsYack: {4, 5}}, Rarity: 5, MinDepth: 4, MaxDepth: WinDepth - 1, Band: true, }, BandGiantBees: { Distribution: map[monsterKind]monsInterval{MonsGiantBee: {2, 3}}, Rarity: 6, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandGiantBeesMany: { Distribution: map[monsterKind]monsInterval{MonsGiantBee: {4, 5}}, Rarity: 9, MinDepth: 4, MaxDepth: WinDepth + 1, Band: true, }, BandSkeletonWarrior: { Distribution: map[monsterKind]monsInterval{MonsSkeletonWarrior: {2, 3}}, Rarity: 7, MinDepth: 5, MaxDepth: WinDepth + 1, Band: true, }, BandTreeMushroomWorms: { Distribution: map[monsterKind]monsInterval{ MonsTreeMushroom: {1, 1}, MonsWorm: {2, 2}, }, Rarity: 10, MinDepth: 6, MaxDepth: WinDepth, Band: true, }, BandVampires: { Distribution: map[monsterKind]monsInterval{ MonsVampire: {2, 2}, }, Rarity: 10, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandVampireNixe: { Distribution: map[monsterKind]monsInterval{ MonsVampire: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 10, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandVampireCelmist: { Distribution: map[monsterKind]monsInterval{ MonsVampire: {1, 1}, MonsMindCelmist: {1, 1}, }, Rarity: 10, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandTreeMushrooms: { Distribution: map[monsterKind]monsInterval{ MonsTreeMushroom: {2, 2}, MonsWorm: {1, 1}, }, Rarity: 10, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMindCelmists: { Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsGoblinWarrior: {1, 1}, }, Rarity: 8, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMindCelmistsLich: { Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {2, 2}, }, Rarity: 8, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMindCelmistsMadNixe: { Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 8, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMadNixes: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsSpider: {1, 1}, MonsHound: {1, 1}, }, Rarity: 4, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMadNixesDragon: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsEarthDragon: {1, 1}, }, Rarity: 4, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMadNixesHydra: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsHydra: {1, 1}, }, Rarity: 4, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, BandMadNixesFrogs: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 4, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, }, UBandTinyHarpy: { Distribution: map[monsterKind]monsInterval{ MonsTinyHarpy: {3, 3}, MonsWingedMilfid: {1, 1}, }, Rarity: 6, MinDepth: 2, MaxDepth: 2, Band: true, Unique: true, }, UBandWorms: { Distribution: map[monsterKind]monsInterval{MonsWorm: {3, 4}, MonsSpider: {1, 1}}, Rarity: 8, MinDepth: 2, MaxDepth: 3, Band: true, Unique: true, }, UBandGoblinsEasy: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsHound: {2, 2}, }, Rarity: 4, MinDepth: 3, MaxDepth: 3, Band: true, Unique: true, }, UBandFrogs: { Distribution: map[monsterKind]monsInterval{MonsBlinkingFrog: {2, 3}}, Rarity: 7, MinDepth: 4, MaxDepth: 4, Band: true, Unique: true, }, UBandOgres: { Distribution: map[monsterKind]monsInterval{MonsOgre: {2, 3}, MonsCyclop: {1, 1}}, Rarity: 4, MinDepth: 4, MaxDepth: 4, Band: true, Unique: true, }, UBandGoblins: { Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsGoblinWarrior: {2, 2}, MonsHound: {1, 1}, }, Rarity: 4, MinDepth: 5, MaxDepth: 5, Band: true, Unique: true, }, UBandBeeYacks: { Distribution: map[monsterKind]monsInterval{ MonsYack: {3, 4}, MonsGiantBee: {2, 2}, }, Rarity: 5, MinDepth: 5, MaxDepth: 5, Band: true, Unique: true, }, UBandMadNixes: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {2, 2}, MonsSpider: {1, 1}, }, Rarity: 5, MinDepth: 5, MaxDepth: 5, Band: true, Unique: true, }, UVampires: { Distribution: map[monsterKind]monsInterval{ MonsVampire: {2, 2}, MonsWingedMilfid: {1, 1}, }, Rarity: 10, MinDepth: 5, MaxDepth: 5, Band: true, Unique: true, }, UHydras: { Distribution: map[monsterKind]monsInterval{ MonsHydra: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 5, MinDepth: 6, MaxDepth: 6, Band: true, Unique: true, }, UExplosiveNadres: { Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 3}, MonsBrizzia: {1, 2}, }, Rarity: 6, MinDepth: 6, MaxDepth: 6, Band: true, Unique: true, }, ULich: { Distribution: map[monsterKind]monsInterval{ MonsSkeletonWarrior: {2, 2}, MonsLich: {1, 1}, MonsMirrorSpecter: {0, 1}, }, Rarity: 6, MinDepth: WinDepth - 1, MaxDepth: WinDepth - 1, Band: true, Unique: true, }, UBrizzias: { Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {3, 4}, }, Rarity: 8, MinDepth: WinDepth - 1, MaxDepth: WinDepth - 1, Band: true, Unique: true, }, UBandMindCelmist: { Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {2, 2}, MonsHound: {1, 1}, }, Rarity: 10, MinDepth: WinDepth - 1, MaxDepth: WinDepth - 1, Band: true, Unique: true, }, UAcidMounds: { Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {3, 4}, }, Rarity: 8, MinDepth: WinDepth, MaxDepth: WinDepth, Band: true, Unique: true, }, USatowalga: { Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {3, 3}, }, Rarity: 8, MinDepth: WinDepth, MaxDepth: WinDepth, Band: true, Unique: true, }, UDragon: { Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {2, 2}, }, Rarity: 6, MinDepth: WinDepth, MaxDepth: WinDepth, Band: true, Unique: true, }, UMarevorHelith: { Distribution: map[monsterKind]monsInterval{ MonsMarevorHelith: {1, 1}, MonsLich: {0, 1}, MonsVampire: {0, 1}, }, Rarity: 13, MinDepth: 2, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXCyclops: { Distribution: map[monsterKind]monsInterval{ MonsCyclop: {3, 3}, }, Rarity: 6, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXLiches: { Distribution: map[monsterKind]monsInterval{ MonsLich: {2, 2}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXFrogRanged: { Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsCyclop: {1, 1}, MonsLich: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXExplosive: { Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {5, 5}, }, Rarity: 6, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXWarriors: { Distribution: map[monsterKind]monsInterval{ MonsHound: {2, 2}, MonsGoblinWarrior: {3, 3}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXSatowalgaNixe: { Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXSpecters: { Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {3, 3}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXDisabling: { Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {1, 1}, MonsSpider: {1, 1}, MonsBrizzia: {1, 1}, MonsGiantBee: {1, 1}, MonsMirrorSpecter: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMadNixeSpecter: { Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMadNixeCyclop: { Distribution: map[monsterKind]monsInterval{ MonsCyclop: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMadNixeHydra: { Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 6, MinDepth: MaxDepth, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXVampires: { Distribution: map[monsterKind]monsInterval{ MonsVampire: {3, 3}, }, Rarity: 10, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMadNixes: { Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {3, 3}, }, Rarity: 10, MinDepth: MaxDepth - 2, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMindCelmists: { Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {2, 2}, MonsCyclop: {1, 1}, }, Rarity: 8, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXTreeMushrooms: { Distribution: map[monsterKind]monsInterval{ MonsTreeMushroom: {3, 3}, }, Rarity: 10, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXMilfidYack: { Distribution: map[monsterKind]monsInterval{ MonsWingedMilfid: {2, 2}, MonsYack: {3, 3}, }, Rarity: 6, MinDepth: MaxDepth - 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXYacks: { Distribution: map[monsterKind]monsInterval{ MonsYack: {7, 7}, }, Rarity: 8, MinDepth: MaxDepth - 2, MaxDepth: MaxDepth, Band: true, Unique: true, }, UXVariedWarriors: { Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {1, 1}, MonsWingedMilfid: {1, 1}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 6, MinDepth: WinDepth + 1, MaxDepth: MaxDepth, Band: true, Unique: true, }, } type specialBands struct { bands []monsterBandData minDepth int maxDepth int } var MonsSpecialBands []specialBands var MonsSpecialEndBands []specialBands func init() { MonsSpecialBands = []specialBands{ {bands: []monsterBandData{ // ogres easy {Monster: MonsOgre, Rarity: 20}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsOgre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {1, 1}, MonsOgre: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 4, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // spiders {Monster: MonsSpider, Rarity: 40}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSpider: {4, 4}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsYack: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSpider: {2, 2}, MonsBrizzia: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSpider: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 8, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // milfids {Monster: MonsWingedMilfid, Rarity: 50}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsWingedMilfid: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsWingedMilfid: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsWingedMilfid: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsWingedMilfid: {1, 1}, MonsYack: {3, 3}, }, Rarity: 4, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // Bees {Monster: MonsGiantBee, Rarity: 50}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsGiantBee: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsCyclop: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsGiantBee: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsYack: {3, 3}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsBrizzia: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsHydra: {1, 1}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 8, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // goblins {Monster: MonsGoblin, Rarity: 4}, {Monster: MonsGoblinWarrior, Rarity: 5}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsGoblin: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {3, 3}, MonsExplosiveNadre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {2, 2}, MonsGoblinWarrior: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsGoblin: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblin: {2, 2}, MonsYack: {3, 3}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 10, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // explosive nadres {Monster: MonsExplosiveNadre, Rarity: 4}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsExplosiveNadre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsGiantBee: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsGoblinWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsExplosiveNadre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsYack: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 7, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsEarthDragon: {1, 1}, }, Rarity: 10, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // plants {Monster: MonsSatowalgaPlant, Rarity: 4}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsWorm: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {3, 3}, MonsSatowalgaPlant: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {1, 1}, MonsGiantBee: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsSatowalgaPlant: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsSpider: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsSatowalgaPlant: {2, 2}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsWingedMilfid: {1, 1}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsBlinkingFrog: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {1, 1}, MonsTreeMushroom: {1, 1}, }, Rarity: 10, Band: true}, }, minDepth: 4, maxDepth: 7, }, {bands: []monsterBandData{ // acid mounds {Monster: MonsAcidMound, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsAcidMound: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {3, 3}, MonsExplosiveNadre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsGoblinWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsAcidMound: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsYack: {2, 2}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 8, Band: true}, }, minDepth: 4, maxDepth: WinDepth, }, {bands: []monsterBandData{ // blinking frogs {Monster: MonsBlinkingFrog, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {3, 3}, MonsExplosiveNadre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsGoblinWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsYack: {2, 2}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBlinkingFrog: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 10, Band: true}, }, minDepth: 4, maxDepth: WinDepth, }, {bands: []monsterBandData{ // hydras {Monster: MonsHydra, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsWorm: {3, 3}, MonsSpider: {2, 2}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsGoblin: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {2, 2}, MonsMirrorSpecter: {1, 1}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsTreeMushroom: {1, 1}, }, Rarity: 8, Band: true}, }, minDepth: 5, maxDepth: WinDepth, }, {bands: []monsterBandData{ // liches {Monster: MonsLich, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsSkeletonWarrior: {1, 2}, MonsHound: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSkeletonWarrior: {1, 2}, MonsAcidMound: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsGoblin: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsVampire: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsBlinkingFrog: {1, 1}, MonsWingedMilfid: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsMirrorSpecter: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {2, 2}, MonsSkeletonWarrior: {2, 2}, }, Rarity: 8, Band: true}, }, minDepth: 6, maxDepth: WinDepth, }, {bands: []monsterBandData{ // dragons {Monster: MonsEarthDragon, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {3, 3}, MonsHound: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {3, 3}, MonsAcidMound: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsSpider: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsWingedMilfid: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsExplosiveNadre: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsGoblin: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsBlinkingFrog: {1, 1}, MonsWingedMilfid: {1, 1}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsMadNixe: {1, 1}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {2, 2}, MonsExplosiveNadre: {1, 1}, }, Rarity: 10, Band: true}, }, minDepth: 6, maxDepth: WinDepth, }, } for _, sb := range MonsSpecialBands { for i, _ := range sb.bands { sb.bands[i].MaxDepth = MaxDepth } } MonsSpecialEndBands = []specialBands{ {bands: []monsterBandData{ // ogres terrible {Monster: MonsOgre, Rarity: 5}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsOgre: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {1, 1}, MonsOgre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsCyclop: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsEarthDragon: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsHydra: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsAcidMound: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsMirrorSpecter: {1, 1}, MonsExplosiveNadre: {1, 1}, }, Rarity: 3, Band: true}, }}, {bands: []monsterBandData{ // ranged terrible {Monster: MonsCyclop, Rarity: 5}, {Monster: MonsLich, Rarity: 5}, {Distribution: map[monsterKind]monsInterval{ MonsCyclop: {2, 2}, MonsOgre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsCyclop: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {2, 2}, MonsWingedMilfid: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {2, 2}, MonsGoblinWarrior: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsCyclop: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsEarthDragon: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {3, 3}, MonsWingedMilfid: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {2, 2}, MonsTreeMushroom: {1, 1}, }, Rarity: 5, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsCyclop: {2, 2}, MonsLich: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {2, 2}, MonsLich: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsCyclop: {1, 1}, }, Rarity: 3, Band: true}, }}, {bands: []monsterBandData{ // mind celmists {Monster: MonsMindCelmist, Rarity: 5}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {2, 2}, MonsHound: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsMadNixe: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsLich: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsOgre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsCyclop: {1, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsYack: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsVampire: {1, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {3, 3}, }, Rarity: 10, Band: true}, }}, {bands: []monsterBandData{ // nixe trap {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {2, 2}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsSatowalgaPlant: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsAcidMound: {3, 3}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsOgre: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsEarthDragon: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsHydra: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsHydra: {1, 1}, MonsEarthDragon: {1, 1}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsGiantBee: {3, 3}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {4, 4}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {2, 2}, MonsMindCelmist: {1, 1}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {2, 2}, MonsVampire: {1, 1}, }, Rarity: 6, Band: true}, }}, {bands: []monsterBandData{ // blinking frogs terrible {Distribution: map[monsterKind]monsInterval{ MonsMadNixe: {1, 1}, MonsBlinkingFrog: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSatowalgaPlant: {1, 1}, MonsBlinkingFrog: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSpider: {2, 2}, MonsBlinkingFrog: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {1, 1}, MonsBlinkingFrog: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsBlinkingFrog: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsCyclop: {1, 1}, MonsBlinkingFrog: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsWingedMilfid: {2, 2}, MonsBlinkingFrog: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsYack: {2, 2}, MonsBlinkingFrog: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGiantBee: {2, 2}, MonsBlinkingFrog: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMindCelmist: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsTreeMushroom: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 8, Band: true}, }}, {bands: []monsterBandData{ // yacks and brizzias terrible {Distribution: map[monsterKind]monsInterval{ MonsYack: {4, 4}, MonsExplosiveNadre: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsYack: {4, 4}, MonsSpider: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {3, 3}, MonsSpider: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {3, 3}, MonsAcidMound: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {2, 2}, MonsExplosiveNadre: {1, 1}, MonsMirrorSpecter: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {1, 1}, MonsHydra: {1, 1}, MonsYack: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {3, 3}, MonsWorm: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsYack: {3, 3}, MonsBrizzia: {3, 3}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsYack: {1, 1}, MonsBrizzia: {1, 1}, MonsBlinkingFrog: {1, 1}, MonsHound: {1, 1}, }, Rarity: 2, Band: true}, }}, {bands: []monsterBandData{ // terrible undead {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsSkeletonWarrior: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsMadNixe: {1, 1}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {2, 2}, MonsSkeletonWarrior: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSkeletonWarrior: {3, 3}, MonsMadNixe: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsSkeletonWarrior: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {1, 1}, MonsSkeletonWarrior: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsVampire: {2, 2}, }, Rarity: 6, Band: true}, }}, {bands: []monsterBandData{ // terrible vampires {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsVampire: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsLich: {1, 1}, MonsMadNixe: {1, 1}, MonsVampire: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsVampire: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsVampire: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsVampire: {2, 2}, MonsMindCelmist: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsVampire: {4, 4}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsMirrorSpecter: {1, 1}, MonsVampire: {1, 1}, }, Rarity: 2, Band: true}, }}, {bands: []monsterBandData{ // terrible dragon and hydras {Distribution: map[monsterKind]monsInterval{ MonsBrizzia: {2, 2}, MonsExplosiveNadre: {2, 2}, }, Rarity: 10, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsHydra: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {2, 2}, MonsSpider: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsBlinkingFrog: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsExplosiveNadre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsBrizzia: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsMirrorSpecter: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsEarthDragon: {1, 1}, MonsMindCelmist: {1, 1}, }, Rarity: 8, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsHydra: {1, 1}, MonsTreeMushroom: {1, 1}, }, Rarity: 8, Band: true}, }}, {bands: []monsterBandData{ // terrible goblin warriors {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsHound: {4, 4}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsHydra: {1, 1}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsBrizzia: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsSpider: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsMadNixe: {1, 1}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsWingedMilfid: {2, 2}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsGoblinWarrior: {2, 2}, MonsYack: {3, 3}, }, Rarity: 3, Band: true}, }}, {bands: []monsterBandData{ // terrible acid mounds {Monster: MonsAcidMound, Rarity: 2}, {Distribution: map[monsterKind]monsInterval{ MonsHound: {1, 1}, MonsAcidMound: {3, 3}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {3, 3}, MonsExplosiveNadre: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsHydra: {1, 1}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsOgre: {2, 2}, MonsAcidMound: {2, 2}, }, Rarity: 2, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsSpider: {3, 3}, }, Rarity: 3, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {3, 3}, MonsWingedMilfid: {2, 2}, }, Rarity: 6, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsBrizzia: {2, 2}, }, Rarity: 4, Band: true}, {Distribution: map[monsterKind]monsInterval{ MonsAcidMound: {2, 2}, MonsMadNixe: {1, 1}, MonsSatowalgaPlant: {1, 1}, }, Rarity: 8, Band: true}, }}, } for _, sb := range MonsSpecialEndBands { for i, _ := range sb.bands { sb.bands[i].MaxDepth = MaxDepth } } } type monster struct { Kind monsterKind Band int Index int Attack int Accuracy int Armor int Evasion int HPmax int HP int State monsterState Statuses [NMonsStatus]int Pos position Target position Path []position // cache Obstructing bool FireReady bool Seen bool } func (m *monster) Init() { m.HPmax = MonsData[m.Kind].maxHP - 1 + RandInt(3) m.Attack = MonsData[m.Kind].baseAttack m.HP = m.HPmax m.Accuracy = MonsData[m.Kind].accuracy m.Armor = MonsData[m.Kind].armor m.Evasion = MonsData[m.Kind].evasion if m.Kind == MonsMarevorHelith { m.State = Wandering } } func (m *monster) Status(st monsterStatus) bool { return m.Statuses[st] > 0 } func (m *monster) Exists() bool { return m != nil && m.HP > 0 } func (m *monster) AlternatePlacement(g *game) *position { if m.Status(MonsLignified) { return nil } var neighbors []position if m.Status(MonsConfused) { neighbors = g.Dungeon.CardinalFreeNeighbors(m.Pos) } else { neighbors = g.Dungeon.FreeNeighbors(m.Pos) } for _, pos := range neighbors { if pos.Distance(g.Player.Pos) != 1 { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } return &pos } return nil } func (m *monster) AlternateConfusedPlacement(g *game) *position { var neighbors []position neighbors = g.Dungeon.CardinalFreeNeighbors(m.Pos) npos := InvalidPos for _, pos := range neighbors { mons := g.MonsterAt(pos) if mons.Exists() || g.Player.Pos == pos { continue } npos = pos if npos.Distance(g.Player.Pos) == 1 { return &npos } } if npos.valid() { return &npos } return nil } func (m *monster) SafePlacement(g *game) *position { var neighbors []position if m.Status(MonsConfused) { neighbors = g.Dungeon.CardinalFreeNeighbors(m.Pos) } else { neighbors = g.Dungeon.FreeNeighbors(m.Pos) } spos := InvalidPos sbest := 9 area := make([]position, 9) for _, pos := range neighbors { if pos.Distance(g.Player.Pos) <= 1 { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } // simple heuristic nsbest := g.Dungeon.WallAreaCount(area, pos, 1) if nsbest < sbest { sbest = nsbest spos = pos } else if nsbest == sbest { switch pos.Dir(g.Player.Pos) { case N, W, E, S: default: sbest = nsbest spos = pos } } } if spos.valid() { return &spos } return nil } func (m *monster) TeleportPlayer(g *game, ev event) { evasion := RandInt(g.Player.Evasion()) acc := RandInt(m.Accuracy) if acc > evasion { g.Print("Marevor pushes you through a monolith.") g.StoryPrint("Pushed by Marevor through a monolith.") g.Teleportation(ev) } else if RandInt(2) == 0 { g.Print("Marevor inadvertently goes into a monolith.") m.TeleportAway(g) } } func (m *monster) TeleportAway(g *game) { pos := m.Pos i := 0 count := 0 for { count++ if count > 1000 { panic("TeleportOther") } pos = g.FreeCell() if pos.Distance(m.Pos) < 15 && i < 1000 { i++ continue } break } switch m.State { case Hunting: m.State = Wandering // TODO: change the target? case Resting, Wandering: m.State = Wandering m.Target = m.Pos } if g.Player.LOS[m.Pos] { g.Printf("%s teleports away.", m.Kind.Definite(true)) } opos := m.Pos m.MoveTo(g, pos) if g.Player.LOS[opos] { g.ui.TeleportAnimation(opos, pos, false) } } func (m *monster) MoveTo(g *game, pos position) { if !g.Player.LOS[m.Pos] && g.Player.LOS[pos] { if !m.Seen { m.Seen = true g.Printf("%s (%v) comes into view.", m.Kind.Indefinite(true), m.State) } g.StopAuto() } recomputeLOS := g.Player.LOS[m.Pos] && g.Doors[m.Pos] || g.Player.LOS[pos] && g.Doors[pos] m.PlaceAt(g, pos) if recomputeLOS { g.ComputeLOS() } } func (m *monster) PlaceAt(g *game, pos position) { g.MonstersPosCache[m.Pos.idx()] = 0 m.Pos = pos g.MonstersPosCache[m.Pos.idx()] = m.Index + 1 } func (m *monster) TeleportMonsterAway(g *game) bool { neighbors := g.Dungeon.FreeNeighbors(m.Pos) for _, pos := range neighbors { if pos == m.Pos || RandInt(3) != 0 { continue } mons := g.MonsterAt(pos) if mons.Exists() { if g.Player.LOS[m.Pos] { g.Print("Marevor makes some strange gestures.") } mons.TeleportAway(g) return true } } return false } func (m *monster) AttackAction(g *game, ev event) { switch { case m.Obstructing: m.Obstructing = false pos := m.AlternatePlacement(g) if pos != nil { m.MoveTo(g, *pos) ev.Renew(g, m.Kind.MovementDelay()) return } fallthrough default: if m.Kind == MonsHydra { for i := 0; i <= 3; i++ { m.HitPlayer(g, ev) } } else if m.Kind == MonsMarevorHelith { m.TeleportPlayer(g, ev) } else { m.HitPlayer(g, ev) } adelay := m.Kind.AttackDelay() if m.Status(MonsSlow) { adelay += 3 } ev.Renew(g, adelay) } } func (m *monster) NaturalAwake(g *game) { m.Target = g.FreeCell() m.State = Wandering m.GatherBand(g) } func (m *monster) HandleTurn(g *game, ev event) { ppos := g.Player.Pos mpos := m.Pos m.MakeAware(g) if !g.Player.LOS[m.Pos] && m.State == Hunting { if g.Player.Armour == HarmonistRobe && RandInt(2) == 0 || g.Player.Aptitudes[AptStealthyMovement] && RandInt(4) == 0 || RandInt(10) == 0 { m.State = Wandering } } movedelay := m.Kind.MovementDelay() if m.Status(MonsSlow) { movedelay += 3 } if m.State == Resting { wander := RandInt(100 + 6*Max(800-(g.DepthPlayerTurn+1), 0)) if wander == 0 { m.NaturalAwake(g) } ev.Renew(g, m.Kind.MovementDelay()) return } if m.State == Hunting && m.RangedAttack(g, ev) { return } if m.State == Hunting && m.SmitingAttack(g, ev) { return } switch m.Kind { case MonsSatowalgaPlant: ev.Renew(g, movedelay) // oklob plants are static ranged-only return case MonsMindCelmist: if m.State == Hunting && !g.Player.LOS[m.Pos] && m.Pos.Distance(g.Player.Pos) <= 2 { // “smart” wait at short distance ev.Renew(g, movedelay) return } } if mpos.Distance(ppos) == 1 { attack := true if m.Status(MonsConfused) { switch m.Pos.Dir(g.Player.Pos) { case E, N, W, S: default: attack = false m.Path = nil safepos := m.AlternateConfusedPlacement(g) if safepos != nil { m.Target = *safepos } } } else if m.Kind == MonsMindCelmist { // we can avoid melee safepos := m.SafePlacement(g) m.Path = nil attack = false if safepos != nil { m.Target = *safepos } } if attack { m.AttackAction(g, ev) return } } if m.Status(MonsLignified) { ev.Renew(g, 10) // wait return } if m.Kind == MonsMarevorHelith { if m.TeleportMonsterAway(g) { ev.Renew(g, movedelay) return } } m.Obstructing = false if !(len(m.Path) > 0 && m.Path[0] == m.Target && m.Path[len(m.Path)-1] == mpos) { m.Path = m.APath(g, mpos, m.Target) if len(m.Path) == 0 && !m.Status(MonsConfused) { // if target is not accessible, try free neighbor cells for _, npos := range g.Dungeon.FreeNeighbors(m.Target) { m.Path = m.APath(g, mpos, npos) if len(m.Path) > 0 { m.Target = npos break } } } } if len(m.Path) < 2 { switch m.State { case Wandering: keepWandering := RandInt(100) if keepWandering > 75 && g.BandData[g.Bands[m.Band]].Band { for _, mons := range g.Monsters { m.Target = mons.Pos } } else { m.Target = g.FreeCell() } m.GatherBand(g) case Hunting: // pick a random cell: more escape strategies for the player if m.Kind == MonsHound && m.Pos.Distance(g.Player.Pos) <= 6 && !(g.Player.Aptitudes[AptStealthyMovement] && RandInt(2) == 0) { m.Target = g.Player.Pos } else { m.Target = g.FreeCell() } m.State = Wandering m.GatherBand(g) } ev.Renew(g, movedelay) return } target := m.Path[len(m.Path)-2] mons := g.MonsterAt(target) switch { case !mons.Exists(): if m.Kind == MonsEarthDragon && g.Dungeon.Cell(target).T == WallCell { g.Dungeon.SetCell(target, FreeCell) g.Stats.Digs++ if !g.Player.LOS[target] { g.WrongWall[m.Pos] = true } g.MakeNoise(WallNoise, m.Pos) g.Fog(m.Pos, 1, ev) if g.Player.Pos.Distance(target) < 12 { // XXX use dijkstra distance ? g.Printf("%s You hear an earth-splitting noise.", g.CrackSound()) g.StopAuto() } m.MoveTo(g, target) m.Path = m.Path[:len(m.Path)-1] } else if g.Dungeon.Cell(target).T == WallCell { m.Path = m.APath(g, mpos, m.Target) } else { m.InvertFoliage(g) m.MoveTo(g, target) if (m.Kind.Ranged() || m.Kind.Smiting()) && !m.FireReady && g.Player.LOS[m.Pos] { m.FireReady = true } m.Path = m.Path[:len(m.Path)-1] } case m.State == Hunting && mons.State != Hunting: r := RandInt(5) if r == 0 { mons.Target = m.Target mons.State = Wandering mons.GatherBand(g) } else if (r == 1 || r == 2) && g.Player.Pos.Distance(mons.Target) > 2 { mons.Target = g.FreeCell() mons.State = Wandering mons.GatherBand(g) } else { m.Path = m.APath(g, mpos, m.Target) } case !g.Player.LOS[mons.Pos] && g.Player.Pos.Distance(mons.Target) > 2 && mons.State != Hunting: r := RandInt(5) if r == 0 { m.Target = g.FreeCell() m.GatherBand(g) } else if (r == 1 || r == 2) && mons.State == Resting { mons.Target = g.FreeCell() mons.State = Wandering mons.GatherBand(g) } else { m.Path = m.APath(g, mpos, m.Target) } case mons.Pos.Distance(g.Player.Pos) == 1: m.Path = m.APath(g, mpos, m.Target) if len(m.Path) < 2 || m.Path[len(m.Path)-2] == mons.Pos { mons.Obstructing = true } case mons.State == Hunting && m.State == Hunting || !g.Player.LOS[m.Target]: if RandInt(4) == 0 { m.Target = mons.Target m.Path = m.APath(g, mpos, m.Target) } else { m.Path = m.APath(g, mpos, m.Target) } default: m.Path = m.APath(g, mpos, m.Target) } ev.Renew(g, movedelay) } func (m *monster) InvertFoliage(g *game) { if m.Kind != MonsWorm { return } invert := false if _, ok := g.Fungus[m.Pos]; !ok { if _, ok := g.Doors[m.Pos]; !ok { g.Fungus[m.Pos] = foliage invert = true } } else { delete(g.Fungus, m.Pos) invert = true } if !g.Player.LOS[m.Pos] && invert { g.WrongFoliage[m.Pos] = !g.WrongFoliage[m.Pos] } else if invert { g.ComputeLOS() } } func (m *monster) DramaticAdjustment(g *game, baseAttack, attack, evasion, acc int, clang bool) (int, int, bool) { if attack >= g.Player.HP { // a little dramatic effect if RandInt(2) == 0 { attack, clang = g.HitDamage(DmgPhysical, baseAttack, g.Player.Armor()) } if attack >= g.Player.HP { n := RandInt(g.Player.Evasion()) if n > evasion { evasion = n } } } if baseAttack >= g.Player.HP && (acc <= evasion || attack < g.Player.HP) { g.Stats.TimesLucky++ } return attack, evasion, clang } func (m *monster) Exhaust(g *game) { m.ExhaustTime(g, 100+RandInt(50)) } func (m *monster) ExhaustTime(g *game, t int) { m.Statuses[MonsExhausted]++ g.PushEvent(&monsterEvent{ERank: g.Ev.Rank() + t, NMons: m.Index, EAction: MonsExhaustionEnd}) } func (m *monster) HitPlayer(g *game, ev event) { if g.Player.HP <= 0 || g.Player.Pos.Distance(m.Pos) > 1 { return } evasion := RandInt(g.Player.Evasion()) acc := RandInt(m.Accuracy) attack, clang := g.HitDamage(DmgPhysical, m.Attack, g.Player.Armor()) attack, evasion, clang = m.DramaticAdjustment(g, m.Attack, attack, evasion, acc, clang) if acc > evasion { if m.Blocked(g) { g.Printf("Clang! You block %s's attack.", m.Kind.Definite(false)) g.MakeNoise(ShieldBlockNoise, g.Player.Pos) g.BlockEffects(m) return } if g.Player.HasStatus(StatusSwap) && !g.Player.HasStatus(StatusLignification) && !m.Status(MonsLignified) { g.SwapWithMonster(m) return } noise := g.HitNoise(clang) g.MakeNoise(noise, g.Player.Pos) var sclang string if clang { sclang = g.ArmourClang() } g.PrintfStyled("%s hits you (%d dmg).%s", logMonsterHit, m.Kind.Definite(true), attack, sclang) m.InflictDamage(g, attack, m.Attack) if m.Kind == MonsVampire { healing := attack if healing > 2*m.Attack/3 { healing = 2 * m.Attack / 3 } m.HP += healing if m.HP > m.HPmax { m.HP = m.HPmax } } if g.Player.HP <= 0 { return } m.HitSideEffects(g, ev) const HeavyWoundHP = 18 if g.Player.Aptitudes[AptConfusingGas] && g.Player.HP < HeavyWoundHP && RandInt(2) == 0 { m.EnterConfusion(g, ev) g.Printf("You release some confusing gas against the %s.", m.Kind) } if g.Player.Aptitudes[AptSmoke] && g.Player.HP < HeavyWoundHP && RandInt(2) == 0 { g.Smoke(ev) } if g.Player.Aptitudes[AptObstruction] && g.Player.HP <= HeavyWoundHP && RandInt(2) == 0 { opos := m.Pos m.Blink(g) if opos != m.Pos { g.TemporalWallAt(opos, ev) g.Print("A temporal wall emerges.") m.Exhaust(g) } } if g.Player.Aptitudes[AptTeleport] && g.Player.HP < HeavyWoundHP && RandInt(2) == 0 { m.TeleportAway(g) } if g.Player.Aptitudes[AptLignification] && g.Player.HP < HeavyWoundHP && RandInt(2) == 0 { m.EnterLignification(g, ev) } } else { g.Stats.Dodges++ g.Printf("%s misses you.", m.Kind.Definite(true)) } } func (m *monster) EnterConfusion(g *game, ev event) { if !m.Status(MonsConfused) { m.Statuses[MonsConfused] = 1 m.Path = m.Path[:0] g.PushEvent(&monsterEvent{ ERank: ev.Rank() + 50 + RandInt(100), NMons: m.Index, EAction: MonsConfusionEnd}) } } func (m *monster) EnterLignification(g *game, ev event) { if !m.Status(MonsLignified) { m.Statuses[MonsLignified] = 1 m.Path = m.Path[:0] g.PushEvent(&monsterEvent{ ERank: ev.Rank() + 150 + RandInt(100), NMons: m.Index, EAction: MonsLignificationEnd}) if g.Player.LOS[m.Pos] { g.Printf("%s is rooted to the ground.", m.Kind.Definite(true)) } } } func (m *monster) HitSideEffects(g *game, ev event) { switch m.Kind { case MonsSpider: if RandInt(2) == 0 { g.Confusion(ev) } case MonsGiantBee: if RandInt(5) == 0 && !g.Player.HasStatus(StatusBerserk) && !g.Player.HasStatus(StatusExhausted) { g.Player.Statuses[StatusBerserk] = 1 g.Player.HP += 10 end := ev.Rank() + 25 + RandInt(30) g.PushEvent(&simpleEvent{ERank: end, EAction: BerserkEnd}) g.Player.Expire[StatusBerserk] = end g.Print("You feel a sudden urge to kill things.") } case MonsBlinkingFrog: if RandInt(2) == 0 { g.Blink(ev) } case MonsAcidMound: g.Corrosion(ev) case MonsYack: if RandInt(2) == 0 && m.PushPlayer(g) { g.Print("The yack pushes you.") } case MonsWingedMilfid: if m.Status(MonsExhausted) || g.Player.HasStatus(StatusLignification) { break } ompos := m.Pos m.MoveTo(g, g.Player.Pos) g.PlacePlayerAt(ompos) g.Print("The flying milfid makes you swap positions.") m.ExhaustTime(g, 50+RandInt(50)) } } func (m *monster) PushPlayer(g *game) (pushed bool) { dir := g.Player.Pos.Dir(m.Pos) pos := g.Player.Pos.To(dir) if !g.Player.HasStatus(StatusLignification) && pos.valid() && g.Dungeon.Cell(pos).T == FreeCell { mons := g.MonsterAt(pos) if !mons.Exists() { g.PlacePlayerAt(pos) pushed = true } } return pushed } func (m *monster) RangedAttack(g *game, ev event) bool { if !m.Kind.Ranged() { return false } if m.Pos.Distance(g.Player.Pos) <= 1 && m.Kind != MonsSatowalgaPlant { return false } if !g.Player.LOS[m.Pos] { m.FireReady = false return false } if !m.FireReady { m.FireReady = true if m.Pos.Distance(g.Player.Pos) <= 3 { ev.Renew(g, m.Kind.AttackDelay()) return true } else { return false } } if m.Status(MonsExhausted) { return false } switch m.Kind { case MonsLich: return m.TormentBolt(g, ev) case MonsCyclop: return m.ThrowRock(g, ev) case MonsGoblinWarrior: return m.ThrowJavelin(g, ev) case MonsSatowalgaPlant: return m.ThrowAcid(g, ev) case MonsMadNixe: return m.NixeAttraction(g, ev) case MonsVampire: return m.VampireSpit(g, ev) case MonsTreeMushroom: return m.ThrowSpores(g, ev) } return false } func (m *monster) RangeBlocked(g *game) bool { ray := g.Ray(m.Pos) blocked := false for _, pos := range ray[1:] { mons := g.MonsterAt(pos) if mons == nil { continue } blocked = true break } return blocked } func (m *monster) TormentBolt(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked { return false } hit := !m.Blocked(g) g.MakeNoise(9, m.Pos) if hit { g.MakeNoise(MagicHitNoise, g.Player.Pos) damage := g.Player.HP - g.Player.HP/2 g.PrintfStyled("%s throws a bolt of torment at you.", logMonsterHit, m.Kind.Definite(true)) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '*', ColorCyan) m.InflictDamage(g, damage, 15) } else { g.Printf("You block the %s's bolt of torment.", m.Kind) g.BlockEffects(m) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '*', ColorCyan) } m.Exhaust(g) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) Blocked(g *game) bool { blocked := false if g.Player.Shield != NoShield && !g.Player.Weapon.TwoHanded() && !g.Player.Blocked { block := RandInt(g.Player.Block()) acc := RandInt(m.Accuracy) if block >= acc { blocked = true } } return blocked } func (m *monster) ThrowRock(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked { return false } block := false hit := true evasion := RandInt(g.Player.Evasion()) acc := RandInt(m.Accuracy) const rockdmg = 15 attack, clang := g.HitDamage(DmgPhysical, rockdmg, g.Player.Armor()) attack, evasion, clang = m.DramaticAdjustment(g, rockdmg, attack, evasion, acc, clang) if 4*acc/3 <= evasion { // rocks are big and do not miss so often hit = false } else { block = m.Blocked(g) hit = !block } if hit { noise := g.HitNoise(clang) g.MakeNoise(noise, g.Player.Pos) var sclang string if clang { sclang = g.ArmourClang() } g.PrintfStyled("%s throws a rock at you (%d dmg).%s", logMonsterHit, m.Kind.Definite(true), attack, sclang) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '●', ColorMagenta) oppos := g.Player.Pos if m.PushPlayer(g) { g.TemporalWallAt(oppos, ev) } else { ray := g.Ray(m.Pos) if len(ray) > 0 { g.TemporalWallAt(ray[len(ray)-1], ev) } } m.InflictDamage(g, attack, rockdmg) } else if block { g.Printf("You block %s's rock. Clang!", m.Kind.Indefinite(false)) g.MakeNoise(ShieldBlockNoise, g.Player.Pos) g.BlockEffects(m) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '●', ColorMagenta) ray := g.Ray(m.Pos) if len(ray) > 0 { g.TemporalWallAt(ray[len(ray)-1], ev) } } else { g.Stats.Dodges++ g.Printf("You dodge %s's rock.", m.Kind.Indefinite(false)) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '●', ColorMagenta) dir := g.Player.Pos.Dir(m.Pos) pos := g.Player.Pos.To(dir) if pos.valid() { mons := g.MonsterAt(pos) if mons.Exists() { mons.HP -= RandInt(15) if mons.HP <= 0 { g.HandleKill(mons, ev) } else { mons.Blink(g) if mons.Pos != pos { g.TemporalWallAt(pos, ev) } } } else { g.TemporalWallAt(pos, ev) } } } ev.Renew(g, 2*m.Kind.AttackDelay()) return true } func (m *monster) VampireSpit(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked || g.Player.HasStatus(StatusNausea) { return false } g.Player.Statuses[StatusNausea]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 30 + RandInt(20), EAction: NauseaEnd}) g.Print("The vampire spits at you. You feel sick.") m.Exhaust(g) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) ThrowSpores(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked || g.Player.HasStatus(StatusLignification) { return false } g.EnterLignification(ev) g.Print("The tree mushroom releases spores. You feel rooted to the ground.") m.Exhaust(g) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) ThrowJavelin(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked { return false } block := false hit := true evasion := RandInt(g.Player.Evasion()) acc := RandInt(m.Accuracy) const jdmg = 11 attack, clang := g.HitDamage(DmgPhysical, jdmg, g.Player.Armor()) attack, evasion, clang = m.DramaticAdjustment(g, jdmg, attack, evasion, acc, clang) if acc <= evasion { hit = false } else { block = m.Blocked(g) hit = !block } if hit { noise := g.HitNoise(clang) g.MakeNoise(noise, g.Player.Pos) var sclang string if clang { sclang = g.ArmourClang() } g.Printf("%s throws %s at you (%d dmg).%s", m.Kind.Definite(true), Indefinite("javelin", false), attack, sclang) g.ui.MonsterJavelinAnimation(g.Ray(m.Pos), true) m.InflictDamage(g, attack, jdmg) } else if block { if RandInt(3) == 0 { g.Printf("You block %s's %s. Clang!", m.Kind.Indefinite(false), "javelin") g.MakeNoise(ShieldBlockNoise, g.Player.Pos) g.BlockEffects(m) g.ui.MonsterJavelinAnimation(g.Ray(m.Pos), false) } else if !g.Player.HasStatus(StatusDisabledShield) { g.Player.Statuses[StatusDisabledShield] = 1 g.PushEvent(&simpleEvent{ERank: ev.Rank() + 100 + RandInt(100), EAction: DisabledShieldEnd}) g.Printf("%s's %s gets embedded in your shield.", m.Kind.Indefinite(true), "javelin") g.MakeNoise(ShieldBlockNoise, g.Player.Pos) g.ui.MonsterJavelinAnimation(g.Ray(m.Pos), false) } } else { g.Stats.Dodges++ g.Printf("You dodge %s's %s.", m.Kind.Indefinite(false), "javelin") g.ui.MonsterJavelinAnimation(g.Ray(m.Pos), false) } m.ExhaustTime(g, 50+RandInt(50)) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) ThrowAcid(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked { return false } block := false hit := true evasion := RandInt(g.Player.Evasion()) acc := RandInt(m.Accuracy) acdmg := 12 attack, clang := g.HitDamage(DmgPhysical, acdmg, g.Player.Armor()) attack, evasion, clang = m.DramaticAdjustment(g, acdmg, attack, evasion, acc, clang) if acc <= evasion { hit = false } else { block = m.Blocked(g) hit = !block } if hit { noise := g.HitNoise(false) // no clang with acid projectiles g.MakeNoise(noise, g.Player.Pos) g.Printf("%s throws acid at you (%d dmg).", m.Kind.Definite(true), attack) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '*', ColorGreen) m.InflictDamage(g, attack, acdmg) if RandInt(2) == 0 { g.Corrosion(ev) if RandInt(2) == 0 { g.Confusion(ev) } } } else if block { g.Printf("You block %s's acid projectile.", m.Kind.Indefinite(false)) g.MakeNoise(BaseHitNoise, g.Player.Pos) // no real clang g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '*', ColorGreen) if RandInt(2) == 0 { g.Corrosion(ev) } } else { g.Stats.Dodges++ g.Printf("You dodge %s's acid projectile.", m.Kind.Indefinite(false)) g.ui.MonsterProjectileAnimation(g.Ray(m.Pos), '*', ColorGreen) } ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) NixeAttraction(g *game, ev event) bool { blocked := m.RangeBlocked(g) if blocked { return false } g.MakeNoise(9, m.Pos) g.PrintfStyled("%s lures you to her.", logMonsterHit, m.Kind.Definite(true)) ray := g.Ray(m.Pos) g.ui.MonsterProjectileAnimation(ray, 'θ', ColorCyan) // TODO: improve if len(ray) > 1 { // should always be the case g.ui.TeleportAnimation(g.Player.Pos, ray[1], true) g.PlacePlayerAt(ray[1]) } m.Exhaust(g) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) SmitingAttack(g *game, ev event) bool { if !m.Kind.Smiting() { return false } if !g.Player.LOS[m.Pos] { m.FireReady = false return false } if !m.FireReady { m.FireReady = true if m.Pos.Distance(g.Player.Pos) <= 3 { ev.Renew(g, m.Kind.AttackDelay()) return true } else { return false } } if m.Status(MonsExhausted) { return false } switch m.Kind { case MonsMirrorSpecter: return m.AbsorbMana(g, ev) case MonsMindCelmist: return m.MindAttack(g, ev) } return false } func (m *monster) AbsorbMana(g *game, ev event) bool { if g.Player.MP == 0 { return false } g.Player.MP -= 1 g.Printf("%s absorbs your mana.", m.Kind.Definite(true)) m.ExhaustTime(g, 10+RandInt(10)) ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) MindAttack(g *game, ev event) bool { if g.Player.Pos.Distance(m.Pos) == 1 && (m.HP < m.HPmax || RandInt(2) == 0) { // try to avoid melee safepos := m.SafePlacement(g) if safepos != nil { return false } } dmg := 3 + RandInt(m.Attack) + RandInt(m.Attack) + RandInt(m.Attack) dmg /= 3 m.InflictDamage(g, dmg, m.Attack) g.Printf("The celmist mage hurts your mind (%d dmg).", dmg) if RandInt(2) == 0 { if RandInt(2) == 0 { g.Player.Statuses[StatusSlow]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 30 + RandInt(10), EAction: SlowEnd}) } else { g.Confusion(ev) } } ev.Renew(g, m.Kind.AttackDelay()) return true } func (m *monster) Explode(g *game, ev event) { neighbors := m.Pos.ValidNeighbors() g.MakeNoise(WallNoise, m.Pos) g.Printf("%s %s explodes with a loud boom.", g.ExplosionSound(), m.Kind.Definite(true)) g.ui.ExplosionAnimation(FireExplosion, m.Pos) for _, pos := range append(neighbors, m.Pos) { c := g.Dungeon.Cell(pos) if c.T == FreeCell { g.Burn(pos, ev) } mons := g.MonsterAt(pos) if mons.Exists() { mons.HP /= 2 if mons.HP == 0 { mons.HP = 1 } g.MakeNoise(ExplosionHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) } else if g.Player.Pos == pos { dmg := g.Player.HP / 2 m.InflictDamage(g, dmg, 15) } else if c.T == WallCell && RandInt(2) == 0 { g.Dungeon.SetCell(pos, FreeCell) g.Stats.Digs++ if !g.Player.LOS[pos] { g.WrongWall[pos] = true } else { g.ui.WallExplosionAnimation(pos) } g.MakeNoise(WallNoise, pos) g.Fog(pos, 1, ev) } } } func (m *monster) Blink(g *game) { npos := g.BlinkPos() if !npos.valid() || npos == g.Player.Pos || npos == m.Pos { return } opos := m.Pos g.Printf("The %s blinks away.", m.Kind) g.ui.TeleportAnimation(opos, npos, true) m.MoveTo(g, npos) } func (m *monster) MakeHunt(g *game) { m.State = Hunting m.Target = g.Player.Pos } func (m *monster) MakeHuntIfHurt(g *game) { if m.Exists() && m.State != Hunting { m.MakeHunt(g) if m.State == Resting { g.Printf("%s awakens.", m.Kind.Definite(true)) } if m.Kind == MonsHound { g.Printf("%s barks.", m.Kind.Definite(true)) g.MakeNoise(BarkNoise, m.Pos) } } } func (m *monster) MakeAwareIfHurt(g *game) { if g.Player.LOS[m.Pos] && m.State != Hunting { m.MakeHuntIfHurt(g) return } if m.State != Resting { return } m.State = Wandering m.Target = g.FreeCell() } func (m *monster) MakeAware(g *game) { if !g.Player.LOS[m.Pos] { return } if m.State == Resting { if m.Status(MonsExhausted) && (m.Pos.Distance(g.Player.Pos) > 1 || RandInt(3) > 0) { return } adjust := g.LosRange() - m.Pos.Distance(g.Player.Pos) max := 28 if g.Player.Aptitudes[AptStealthyMovement] { max += 3 } if g.Player.Armour == HarmonistRobe { max += 10 } stealth := max - 4*adjust fact := 2 if m.Pos.Distance(g.Player.Pos) > 1 { fact = 3 } else if stealth > 15 { stealth = 15 } r := RandInt(stealth) if g.Player.Aptitudes[AptStealthyMovement] { r *= fact } if g.Player.Armour == HarmonistRobe { r *= fact } if r >= 5 { return } } if m.State == Wandering { adjust := g.LosRange() - m.Pos.Distance(g.Player.Pos) max := 37 if g.Player.Aptitudes[AptStealthyMovement] { max += 5 } if g.Player.Armour == HarmonistRobe { max += 10 } stealth := max - 4*adjust r := RandInt(stealth) if g.Player.Aptitudes[AptStealthyMovement] { r *= 2 } if g.Player.Armour == HarmonistRobe { r *= 2 r += 5 } if r >= 25 && m.Pos.Distance(g.Player.Pos) > 1 { return } } if m.State == Resting { g.Printf("%s awakens.", m.Kind.Definite(true)) } if m.State == Wandering { g.Printf("%s notices you.", m.Kind.Definite(true)) } if m.State != Hunting && m.Kind == MonsHound { g.Printf("%s barks.", m.Kind.Definite(true)) g.MakeNoise(BarkNoise, m.Pos) } m.MakeHunt(g) } func (m *monster) Heal(g *game, ev event) { if m.HP < m.HPmax { m.HP++ } ev.Renew(g, 50) } func (m *monster) GatherBand(g *game) { if !g.BandData[g.Bands[m.Band]].Band { return } dij := &normalPath{game: g} nm := Dijkstra(dij, []position{m.Pos}, 4) for _, mons := range g.Monsters { if mons.Band == m.Band { if mons.State == Hunting && m.State != Hunting { continue } n, ok := nm[mons.Pos] if !ok || n.Cost > 4 || mons.State == Resting && mons.Status(MonsExhausted) && RandInt(2) == 0 { continue } r := RandInt(100) if r > 50 || mons.State == Wandering && r > 10 { mons.Target = m.Target if mons.State == Resting { mons.State = Wandering } } } } } func (g *game) MonsterAt(pos position) *monster { if !pos.valid() { return nil } i := g.MonstersPosCache[pos.idx()] if i <= 0 { return nil } return g.Monsters[i-1] } func (g *game) Danger() int { danger := 0 for _, mons := range g.Monsters { danger += mons.Kind.Dangerousness() } return danger } func (g *game) MaxDanger() int { danger := [MaxDepth + 1]int{ 1: 20, 2: 42, 3: 65, 4: 90, 5: 115, 6: 140, 7: 165, 8: 190, 9: 215, 10: 245, 11: 285, } max := danger[g.Depth] adjust := -2 * g.Depth for c, q := range g.Player.Consumables { switch c { case HealWoundsPotion, CBlinkPotion: adjust += Min(5, g.Depth) * Min(q, Min(5, g.Depth)) case TeleportationPotion, DigPotion, WallPotion: adjust += Min(3, g.Depth) * Min(q, 3) case SwiftnessPotion, LignificationPotion, MagicPotion, BerserkPotion, ExplosiveMagara, ShadowsPotion, AccuracyPotion, TormentPotion, TeleportMagara, NightMagara: adjust += Min(2, g.Depth) * Min(q, 3) case ConfusingDart: adjust += Min(1, g.Depth) * Min(q, 7) } } for _, props := range g.Player.Rods { adjust += Min(props.Charge, 2) * Min(2, g.Depth-1) } if g.Depth < MaxDepth && g.Player.Consumables[DescentPotion] > 0 { adjust += g.Depth } if max+adjust < max-max/3 { max = max - max/3 } else if max+adjust > max+max/3 { max = max + max/3 } else { max = max + adjust } if g.Depth > 3 && g.Player.Weapon == Dagger { max -= 3 * g.Depth } if g.Depth > 4 && g.Player.Armour == Robe { max -= 2 * g.Depth } if g.Player.Consumables[MagicMappingPotion] > 0 && WinDepth-g.Depth < g.Player.Consumables[MagicMappingPotion] { max = max * 110 / 100 } if g.Player.Consumables[DreamPotion] > 0 && WinDepth-g.Depth < g.Player.Consumables[DreamPotion] { max = max * 105 / 100 } switch g.Dungeon.Gen { case GenCaveMapTree: max = max * 90 / 100 case GenCaveMap: max = max * 95 / 100 case GenRoomMap: max = max * 105 / 100 case GenRuinsMap: max = max * 108 / 100 case GenBSPMap: max = max * 115 / 100 } return max } func (g *game) MaxMonsters() int { nmons := [MaxDepth + 1]int{ 1: 13, 2: 17, 3: 22, 4: 28, 5: 33, 6: 33, 7: 33, 8: 36, 9: 36, 10: 39, 11: 42, } max := nmons[g.Depth] switch g.Dungeon.Gen { case GenCaveMapTree, GenCaveMap: max = max * 90 / 100 case GenBSPMap: max = max * 110 / 100 } return max } func (g *game) GenMonsters() { g.Monsters = []*monster{} g.Bands = []monsterBand{} danger := g.MaxDanger() nmons := g.MaxMonsters() nband := 0 i := 0 repeat := 0 loop: for danger > 0 && nmons > 0 { for band, data := range g.BandData { if RandInt(data.Rarity*50) != 0 { continue } monsters := g.GenBand(data, monsterBand(band)) if monsters == nil { continue } if data.Unique { g.GeneratedUniques[monsterBand(band)]++ } g.Bands = append(g.Bands, monsterBand(band)) pos := g.FreeCellForMonster() for _, mk := range monsters { if mk == MonsGoblin { mk = g.Opts.Alternate } if nmons-1 <= 0 { return } if danger-mk.Dangerousness() <= 0 { if repeat > 15 { return } repeat++ continue loop } danger -= mk.Dangerousness() nmons-- mons := &monster{Kind: mk} mons.Init() mons.Index = i mons.Band = nband mons.PlaceAt(g, pos) g.Monsters = append(g.Monsters, mons) i++ pos = g.FreeCellForBandMonster(pos) } nband++ } } } func (g *game) MonsterInLOS() *monster { for _, mons := range g.Monsters { if mons.Exists() && g.Player.LOS[mons.Pos] { return mons } } return nil } boohu-0.13.0/neighbors.go000066400000000000000000000024031356500202200152100ustar00rootroot00000000000000package main func (pos position) Neighbors(nb []position, keep func(position) bool) []position { neighbors := [8]position{pos.E(), pos.W(), pos.N(), pos.S(), pos.NE(), pos.NW(), pos.SE(), pos.SW()} nb = nb[:0] for _, npos := range neighbors { if keep(npos) { nb = append(nb, npos) } } return nb } func (pos position) CardinalNeighbors(nb []position, keep func(position) bool) []position { neighbors := [4]position{pos.E(), pos.W(), pos.N(), pos.S()} nb = nb[:0] for _, npos := range neighbors { if keep(npos) { nb = append(nb, npos) } } return nb } func (pos position) OutsideNeighbors() []position { nb := make([]position, 0, 8) nb = pos.Neighbors(nb, func(npos position) bool { return !npos.valid() }) return nb } func (pos position) ValidNeighbors() []position { nb := make([]position, 0, 8) nb = pos.Neighbors(nb, position.valid) return nb } func (d *dungeon) IsFreeCell(pos position) bool { return pos.valid() && d.Cell(pos).T != WallCell } func (d *dungeon) FreeNeighbors(pos position) []position { nb := make([]position, 0, 8) nb = pos.Neighbors(nb, d.IsFreeCell) return nb } func (d *dungeon) CardinalFreeNeighbors(pos position) []position { nb := make([]position, 0, 4) nb = pos.CardinalNeighbors(nb, d.IsFreeCell) return nb } boohu-0.13.0/path.go000066400000000000000000000115621356500202200141720ustar00rootroot00000000000000package main import "sort" type dungeonPath struct { dungeon *dungeon neighbors [8]position wcost int } func (dp *dungeonPath) Neighbors(pos position) []position { nb := dp.neighbors[:0] return pos.Neighbors(nb, position.valid) } func (dp *dungeonPath) Cost(from, to position) int { if dp.dungeon.Cell(to).T == WallCell { if dp.wcost > 0 { return dp.wcost } return 4 } return 1 } func (dp *dungeonPath) Estimation(from, to position) int { return from.Distance(to) } type playerPath struct { game *game neighbors [8]position } func (pp *playerPath) Neighbors(pos position) []position { d := pp.game.Dungeon nb := pp.neighbors[:0] keep := func(npos position) bool { if cld, ok := pp.game.Clouds[npos]; ok && cld == CloudFire && !(pp.game.WrongDoor[npos] || pp.game.WrongFoliage[npos]) { return false } return npos.valid() && ((d.Cell(npos).T == FreeCell && !pp.game.WrongWall[npos] || d.Cell(npos).T == WallCell && pp.game.WrongWall[npos]) || pp.game.Player.HasStatus(StatusDig)) && d.Cell(npos).Explored } if pp.game.Player.HasStatus(StatusConfusion) { nb = pos.CardinalNeighbors(nb, keep) } else { nb = pos.Neighbors(nb, keep) } return nb } func (pp *playerPath) Cost(from, to position) int { if !pp.game.ExclusionsMap[from] && pp.game.ExclusionsMap[to] { return unreachable } return 1 } func (pp *playerPath) Estimation(from, to position) int { return from.Distance(to) } type noisePath struct { game *game neighbors [8]position } func (fp *noisePath) Neighbors(pos position) []position { nb := fp.neighbors[:0] d := fp.game.Dungeon keep := func(npos position) bool { return npos.valid() && d.Cell(npos).T != WallCell } return pos.Neighbors(nb, keep) } func (fp *noisePath) Cost(from, to position) int { return 1 } type normalPath struct { game *game neighbors [8]position } func (np *normalPath) Neighbors(pos position) []position { nb := np.neighbors[:0] d := np.game.Dungeon keep := func(npos position) bool { return npos.valid() && d.Cell(npos).T != WallCell } if np.game.Player.HasStatus(StatusConfusion) { return pos.CardinalNeighbors(nb, keep) } return pos.Neighbors(nb, keep) } func (np *normalPath) Cost(from, to position) int { return 1 } type autoexplorePath struct { game *game neighbors [8]position } func (ap *autoexplorePath) Neighbors(pos position) []position { if ap.game.ExclusionsMap[pos] { return nil } d := ap.game.Dungeon nb := ap.neighbors[:0] keep := func(npos position) bool { if cld, ok := ap.game.Clouds[npos]; ok && cld == CloudFire && !(ap.game.WrongDoor[npos] || ap.game.WrongFoliage[npos]) { // XXX little info leak return false } return npos.valid() && (d.Cell(npos).T == FreeCell && !ap.game.WrongWall[npos] || d.Cell(npos).T == WallCell && ap.game.WrongWall[npos]) && !ap.game.ExclusionsMap[npos] } if ap.game.Player.HasStatus(StatusConfusion) { nb = pos.CardinalNeighbors(nb, keep) } else { nb = pos.Neighbors(nb, keep) } return nb } func (ap *autoexplorePath) Cost(from, to position) int { return 1 } type monPath struct { game *game monster *monster wall bool neighbors [8]position } func (mp *monPath) Neighbors(pos position) []position { nb := mp.neighbors[:0] d := mp.game.Dungeon keep := func(npos position) bool { return npos.valid() && (d.Cell(npos).T != WallCell || mp.wall) } if mp.monster.Status(MonsConfused) { return pos.CardinalNeighbors(nb, keep) } return pos.Neighbors(nb, keep) } func (mp *monPath) Cost(from, to position) int { g := mp.game mons := g.MonsterAt(to) if !mons.Exists() { if mp.wall && g.Dungeon.Cell(to).T == WallCell && mp.monster.State != Hunting { return 6 } return 1 } if mons.Status(MonsLignified) { return 8 } return 4 } func (mp *monPath) Estimation(from, to position) int { return from.Distance(to) } func (m *monster) APath(g *game, from, to position) []position { mp := &monPath{game: g, monster: m} if m.Kind == MonsEarthDragon { mp.wall = true } path, _, found := AstarPath(mp, from, to) if !found { return nil } return path } func (g *game) PlayerPath(from, to position) []position { pp := &playerPath{game: g} path, _, found := AstarPath(pp, from, to) if !found { return nil } return path } func (g *game) SortedNearestTo(cells []position, to position) []position { ps := posSlice{} for _, pos := range cells { pp := &dungeonPath{dungeon: g.Dungeon, wcost: unreachable} _, cost, found := AstarPath(pp, pos, to) if found { ps = append(ps, posCost{pos, cost}) } } sort.Sort(ps) sorted := []position{} for _, pc := range ps { sorted = append(sorted, pc.pos) } return sorted } type posCost struct { pos position cost int } type posSlice []posCost func (ps posSlice) Len() int { return len(ps) } func (ps posSlice) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } func (ps posSlice) Less(i, j int) bool { return ps[i].cost < ps[j].cost } boohu-0.13.0/player.go000066400000000000000000000247751356500202200145440ustar00rootroot00000000000000package main import ( "errors" "fmt" ) type player struct { HP int MP int Simellas int Armour armour Weapon weapon Shield shield Consumables map[consumable]int Rods map[rod]rodProps Aptitudes map[aptitude]bool Statuses map[status]int Expire map[status]int Pos position Target position LOS map[position]bool Rays rayMap Bored int AccScore int Blocked bool } const DefaultHealth = 42 func (p *player) HPMax() int { hpmax := DefaultHealth if p.Aptitudes[AptHealthy] { hpmax += 10 } hpmax -= 3 * p.Bored if p.Weapon == FinalBlade { hpmax = 2 * hpmax / 3 } if hpmax < 21 { hpmax = 21 } return hpmax } func (p *player) MPMax() int { mpmax := 3 if p.Aptitudes[AptMagic] { mpmax += 2 } if p.Armour == CelmistRobe { mpmax += 2 } return mpmax } func (p *player) Accuracy() int { acc := 15 return acc } func (p *player) RangedAccuracy() int { acc := 15 return acc } func (p *player) Armor() int { ar := 0 switch p.Armour { case SmokingScales: ar += 4 case ShinyPlates: ar += 6 case TurtlePlates: ar += 9 } if p.Aptitudes[AptScales] { ar += 2 } if p.HasStatus(StatusLignification) { ar = 9 + ar/2 } if p.HasStatus(StatusCorrosion) { ar -= 2 * p.Statuses[StatusCorrosion] if ar < 0 { ar = 0 } } return ar } func (p *player) Attack() int { attack := p.Weapon.Attack() if p.Aptitudes[AptStrong] { attack += attack / 5 } if p.HasStatus(StatusCorrosion) { penalty := p.Statuses[StatusCorrosion] if penalty > 5 { penalty = 5 } attack -= penalty } return attack } func (p *player) Block() int { block := p.Shield.Block() if p.HasStatus(StatusDisabledShield) { block /= 3 } return block } func (p *player) Evasion() int { ev := 15 if p.Aptitudes[AptAgile] { ev += 3 } switch p.Armour { case TurtlePlates: ev -= 2 case HarmonistRobe, CelmistRobe, Robe: ev += 1 case SpeedRobe: ev += 3 } if p.HasStatus(StatusAgile) { ev += 7 } return ev } func (p *player) HasStatus(st status) bool { return p.Statuses[st] > 0 } func (p *player) AptitudeCount() int { count := 0 for _, b := range p.Aptitudes { if b { count++ } } return count } func (g *game) AutoToDir(ev event) bool { if g.MonsterInLOS() == nil { err := g.MovePlayer(g.Player.Pos.To(g.AutoDir), ev) if err != nil { g.Print(err.Error()) g.AutoDir = NoDir return false } return true } g.AutoDir = NoDir return false } func (g *game) GoToDir(dir direction, ev event) error { if g.MonsterInLOS() != nil { g.AutoDir = NoDir return errors.New("You cannot travel while there are monsters in view.") } err := g.MovePlayer(g.Player.Pos.To(dir), ev) if err != nil { return err } g.AutoDir = dir return nil } func (g *game) MoveToTarget(ev event) bool { if !g.AutoTarget.valid() { return false } path := g.PlayerPath(g.Player.Pos, g.AutoTarget) if g.MonsterInLOS() != nil { g.AutoTarget = InvalidPos } if len(path) < 1 { g.AutoTarget = InvalidPos return false } var err error if len(path) > 1 { err = g.MovePlayer(path[len(path)-2], ev) if g.ExclusionsMap[path[len(path)-2]] { g.AutoTarget = InvalidPos } } else { g.WaitTurn(ev) } if err != nil { g.Print(err.Error()) g.AutoTarget = InvalidPos return false } if g.AutoTarget.valid() && g.Player.Pos == g.AutoTarget { g.AutoTarget = InvalidPos } return true } func (g *game) WaitTurn(ev event) { grade := 1 if len(g.Noise) > 0 || g.StatusRest() { grade = 1 } g.BoredomAction(ev, grade) ev.Renew(g, 10) } func (g *game) MonsterCount() (count int) { for _, mons := range g.Monsters { if mons.Exists() { count++ } } return count } func (g *game) BoredomAction(ev event, grade int) { obor := g.Boredom if g.MonsterInLOS() == nil { g.Boredom += grade } else { g.Boredom-- if g.Boredom < 0 { g.Boredom = 0 g.Player.Bored = 0 } return } if g.Boredom >= 120 && obor < 120 { if g.MonsterCount() > 4 { g.PrintStyled("You feel a little bored, your health may decline.", logCritic) g.StopAuto() } } if g.Boredom >= 130 && (obor/10 != g.Boredom/10) { if g.MonsterCount() > 4 { g.Player.Bored++ g.PrintStyled("You feel unhealthy.", logCritic) g.StopAuto() if g.Player.HP > g.Player.HPMax() { g.Player.HP -= 3 } } } } func (g *game) FunAction() { g.Boredom -= 15 if g.Boredom < 0 { g.Boredom = 0 g.Player.Bored = 0 } } func (g *game) Rest(ev event) error { if g.MonsterInLOS() != nil { return fmt.Errorf("You cannot sleep while monsters are in view.") } if cld, ok := g.Clouds[g.Player.Pos]; ok && cld == CloudFire { return errors.New("You cannot rest on flames.") } if !g.NeedsRegenRest() && !g.StatusRest() { return errors.New("You do not need to rest.") } g.WaitTurn(ev) g.Resting = true g.RestingTurns = 0 if g.StatusRest() { g.RestingTurns = -1 // not true resting, just waiting for status end } g.FunAction() return nil } func (g *game) StatusRest() bool { for _, q := range g.Player.Statuses { if q > 0 { return true } } return false } func (g *game) NeedsRegenRest() bool { return g.Player.HP < g.Player.HPMax() || g.Player.MP < g.Player.MPMax() } func (g *game) Equip(ev event) error { if eq, ok := g.Equipables[g.Player.Pos]; ok { eq.Equip(g) ev.Renew(g, 10) g.BoredomAction(ev, 1) return nil } return errors.New("Found nothing to equip here.") } func (g *game) Teleportation(ev event) { var pos position i := 0 count := 0 for { count++ if count > 1000 { panic("Teleportation") } pos = g.FreeCell() if pos.Distance(g.Player.Pos) < 15 && i < 1000 { i++ continue } break } if pos.valid() { // should always happen opos := g.Player.Pos g.Print("You teleport away.") g.ui.TeleportAnimation(opos, pos, true) g.PlacePlayerAt(pos) } else { // should not happen g.Print("Something went wrong with the teleportation.") } } func (g *game) CollectGround() { pos := g.Player.Pos if g.Simellas[pos] > 0 { g.Player.Simellas += g.Simellas[pos] if g.Simellas[pos] == 1 { g.Print("You pick up a simella.") } else { g.Printf("You pick up %d simellas.", g.Simellas[pos]) } g.DijkstraMapRebuild = true delete(g.Simellas, pos) } if c, ok := g.Collectables[pos]; ok { g.Player.Consumables[c.Consumable] += c.Quantity g.DijkstraMapRebuild = true delete(g.Collectables, pos) if c.Quantity > 1 { g.Printf("You take %d %s.", c.Quantity, c.Consumable.Plural()) g.StoryPrintf("Took %d %s.", c.Quantity, c.Consumable.Plural()) } else { g.Printf("You take %s.", Indefinite(c.Consumable.String(), false)) g.StoryPrintf("Took %s.", Indefinite(c.Consumable.String(), false)) } } if r, ok := g.Rods[pos]; ok { g.Player.Rods[r] = rodProps{Charge: r.MaxCharge() - 1} g.DijkstraMapRebuild = true delete(g.Rods, pos) g.Printf("You take a %s.", r) g.StoryPrintf("Found and took a %s.", r) } if eq, ok := g.Equipables[pos]; ok { g.Printf("You are standing over %s.", Indefinite(eq.String(), false)) } else if _, ok := g.Stairs[pos]; ok { g.Print("You are standing on a staircase.") } else if stn, ok := g.MagicalStones[pos]; ok { g.Printf("You are standing on %s.", Indefinite(stn.String(), false)) } else if g.Doors[pos] { g.Print("You stand at the door.") } } func (g *game) MovePlayer(pos position, ev event) error { if !pos.valid() { return errors.New("You cannot move there.") } c := g.Dungeon.Cell(pos) if c.T == WallCell && !g.Player.HasStatus(StatusDig) { return errors.New("You cannot move into a wall.") } if g.Player.HasStatus(StatusConfusion) { switch pos.Dir(g.Player.Pos) { case E, N, W, S: default: return errors.New("You cannot use diagonal movements while confused.") } } delay := 10 mons := g.MonsterAt(pos) if g.Player.Weapon == DefenderFlail && !mons.Exists() { mons = g.AttractMonster(pos) } if !mons.Exists() { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot move while lignified") } if c.T == WallCell { g.Dungeon.SetCell(pos, FreeCell) g.MakeNoise(WallNoise, pos) g.Print(g.CrackSound()) g.Fog(pos, 1, ev) g.Stats.Digs++ } if g.Player.Aptitudes[AptFast] { // only fast for movement delay -= 2 } switch g.Player.Armour { case TurtlePlates: delay += 3 case SpeedRobe: delay -= 3 case SmokingScales: _, ok := g.Clouds[g.Player.Pos] if !ok { g.Clouds[g.Player.Pos] = CloudFog g.PushEvent(&cloudEvent{ERank: ev.Rank() + 15 + RandInt(10), EAction: CloudEnd, Pos: g.Player.Pos}) } } if g.Player.HasStatus(StatusSwift) { // only fast for movement delay -= 3 } g.Stats.Moves++ g.PlacePlayerAt(pos) if !g.Autoexploring { g.BoredomAction(ev, 1) } if g.Player.Statuses[StatusSlay] > 0 { g.Player.Statuses[StatusSlay] /= 2 } } else { g.FunAction() g.AttackMonster(mons, ev) } if g.Player.HasStatus(StatusBerserk) { delay -= 3 } if g.Player.HasStatus(StatusSlow) { delay += 3 * g.Player.Statuses[StatusSlow] } if delay < 3 { delay = 3 } ev.Renew(g, delay) return nil } func (g *game) HealPlayer(ev event) { if g.Player.HP < g.Player.HPMax() { g.Player.HP++ } delay := 50 ev.Renew(g, delay) } func (g *game) MPRegen(ev event) { if g.Player.MP < g.Player.MPMax() { g.Player.MP++ } delay := 100 ev.Renew(g, delay) } func (g *game) Smoke(ev event) { dij := &normalPath{game: g} nm := Dijkstra(dij, []position{g.Player.Pos}, 2) for pos := range nm { _, ok := g.Clouds[pos] if !ok { g.Clouds[pos] = CloudFog g.PushEvent(&cloudEvent{ERank: ev.Rank() + 100 + RandInt(100), EAction: CloudEnd, Pos: pos}) } } g.Player.Statuses[StatusSwift]++ end := ev.Rank() + 20 + RandInt(10) g.PushEvent(&simpleEvent{ERank: end, EAction: HasteEnd}) g.Player.Expire[StatusSwift] = end g.ComputeLOS() g.Print("You feel an energy burst and smoke comes out from you.") } func (g *game) Corrosion(ev event) { g.Player.Statuses[StatusCorrosion]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 80 + RandInt(40), EAction: CorrosionEnd}) g.Print("Your equipment gets corroded.") } func (g *game) Confusion(ev event) { if !g.Player.HasStatus(StatusConfusion) { g.Player.Statuses[StatusConfusion]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 100 + RandInt(100), EAction: ConfusionEnd}) g.Print("You feel confused.") } } func (g *game) PlacePlayerAt(pos position) { g.Player.Pos = pos g.CollectGround() g.ComputeLOS() g.MakeMonstersAware() } func (g *game) EnterLignification(ev event) { g.Player.Statuses[StatusLignification]++ g.PushEvent(&simpleEvent{ERank: ev.Rank() + 150 + RandInt(100), EAction: LignificationEnd}) g.Player.HP += 10 } boohu-0.13.0/pos.go000066400000000000000000000122761356500202200140420ustar00rootroot00000000000000package main import "fmt" type position struct { X int Y int } func (pos position) E() position { return position{pos.X + 1, pos.Y} } func (pos position) SE() position { return position{pos.X + 1, pos.Y + 1} } func (pos position) NE() position { return position{pos.X + 1, pos.Y - 1} } func (pos position) N() position { return position{pos.X, pos.Y - 1} } func (pos position) S() position { return position{pos.X, pos.Y + 1} } func (pos position) W() position { return position{pos.X - 1, pos.Y} } func (pos position) SW() position { return position{pos.X - 1, pos.Y + 1} } func (pos position) NW() position { return position{pos.X - 1, pos.Y - 1} } func (pos position) Distance(to position) int { deltaX := Abs(to.X - pos.X) deltaY := Abs(to.Y - pos.Y) if deltaX > deltaY { return deltaX } return deltaY } func (pos position) DistanceX(to position) int { deltaX := Abs(to.X - pos.X) return deltaX } func (pos position) DistanceY(to position) int { deltaY := Abs(to.Y - pos.Y) return deltaY } type direction int const ( NoDir direction = iota E ENE NE NNE N NNW NW WNW W WSW SW SSW S SSE SE ESE ) func KeyToDir(k keyAction) (dir direction) { switch k { case KeyW, KeyRunW: dir = W case KeyE, KeyRunE: dir = E case KeyS, KeyRunS: dir = S case KeyN, KeyRunN: dir = N case KeyNW, KeyRunNW: dir = NW case KeySW, KeyRunSW: dir = SW case KeyNE, KeyRunNE: dir = NE case KeySE, KeyRunSE: dir = SE } return dir } func (pos position) To(dir direction) position { to := pos switch dir { case E, ENE, ESE: to = pos.E() case NE: to = pos.NE() case NNE, N, NNW: to = pos.N() case NW: to = pos.NW() case WNW, W, WSW: to = pos.W() case SW: to = pos.SW() case SSW, S, SSE: to = pos.S() case SE: to = pos.SE() } return to } func (pos position) Dir(from position) direction { deltaX := Abs(pos.X - from.X) deltaY := Abs(pos.Y - from.Y) switch { case pos.X > from.X && pos.Y == from.Y: return E case pos.X > from.X && pos.Y < from.Y: switch { case deltaX > deltaY: return ENE case deltaX == deltaY: return NE default: return NNE } case pos.X == from.X && pos.Y < from.Y: return N case pos.X < from.X && pos.Y < from.Y: switch { case deltaY > deltaX: return NNW case deltaX == deltaY: return NW default: return WNW } case pos.X < from.X && pos.Y == from.Y: return W case pos.X < from.X && pos.Y > from.Y: switch { case deltaX > deltaY: return WSW case deltaX == deltaY: return SW default: return SSW } case pos.X == from.X && pos.Y > from.Y: return S case pos.X > from.X && pos.Y > from.Y: switch { case deltaY > deltaX: return SSE case deltaX == deltaY: return SE default: return ESE } default: panic(fmt.Sprintf("internal error: invalid position:%+v-%+v", pos, from)) } } func (pos position) Parents(from position) []position { p := []position{} switch pos.Dir(from) { case E: p = append(p, pos.W()) case ENE: p = append(p, pos.W(), pos.SW()) case NE: p = append(p, pos.SW()) case NNE: p = append(p, pos.S(), pos.SW()) case N: p = append(p, pos.S()) case NNW: p = append(p, pos.S(), pos.SE()) case NW: p = append(p, pos.SE()) case WNW: p = append(p, pos.E(), pos.SE()) case W: p = append(p, pos.E()) case WSW: p = append(p, pos.E(), pos.NE()) case SW: p = append(p, pos.NE()) case SSW: p = append(p, pos.N(), pos.NE()) case S: p = append(p, pos.N()) case SSE: p = append(p, pos.N(), pos.NW()) case SE: p = append(p, pos.NW()) case ESE: p = append(p, pos.W(), pos.NW()) } return p } func (pos position) RandomNeighbor(diag bool) position { if diag { return pos.RandomNeighborDiagonals() } return pos.RandomNeighborCardinal() } func (pos position) RandomNeighborDiagonals() position { neighbors := [8]position{pos.E(), pos.W(), pos.N(), pos.S(), pos.NE(), pos.NW(), pos.SE(), pos.SW()} var r int switch RandInt(8) { case 0: r = RandInt(len(neighbors[0:4])) case 1: r = RandInt(len(neighbors[0:2])) default: r = RandInt(len(neighbors[4:])) } return neighbors[r] } func (pos position) RandomNeighborCardinal() position { neighbors := [8]position{pos.E(), pos.W(), pos.N(), pos.S(), pos.NE(), pos.NW(), pos.SE(), pos.SW()} var r int switch RandInt(6) { case 0: r = RandInt(len(neighbors[0:4])) case 1: r = RandInt(len(neighbors)) default: r = RandInt(len(neighbors[0:2])) } return neighbors[r] } func idxtopos(i int) position { return position{i % DungeonWidth, i / DungeonWidth} } func (pos position) idx() int { return pos.Y*DungeonWidth + pos.X } func (pos position) valid() bool { return pos.Y >= 0 && pos.Y < DungeonHeight && pos.X >= 0 && pos.X < DungeonWidth } func (pos position) Laterals(dir direction) []position { switch dir { case E, ENE, ESE: return []position{pos.NE(), pos.SE()} case NE: return []position{pos.E(), pos.N()} case N, NNE, NNW: return []position{pos.NW(), pos.NE()} case NW: return []position{pos.W(), pos.N()} case W, WNW, WSW: return []position{pos.SW(), pos.NW()} case SW: return []position{pos.W(), pos.S()} case S, SSW, SSE: return []position{pos.SW(), pos.SE()} case SE: return []position{pos.S(), pos.E()} default: // should not happen return []position{} } } boohu-0.13.0/pos_test.go000066400000000000000000000012321356500202200150670ustar00rootroot00000000000000package main import "testing" func TestDir(t *testing.T) { type tableTest struct { pos position dir direction } table := []tableTest{ {position{3, 2}, E}, {position{4, 1}, ENE}, {position{3, 1}, NE}, {position{3, 0}, NNE}, {position{2, 1}, N}, {position{1, 0}, NNW}, {position{1, 1}, NW}, {position{0, 1}, WNW}, {position{1, 2}, W}, {position{0, 3}, WSW}, {position{1, 3}, SW}, {position{1, 4}, SSW}, {position{2, 3}, S}, {position{3, 4}, SSE}, {position{3, 3}, SE}, {position{4, 3}, ESE}, } for _, test := range table { if test.pos.Dir(position{2, 2}) != test.dir { t.Errorf("Bad direction for %+v\n", test) } } } boohu-0.13.0/replay.go000066400000000000000000000067001356500202200145300ustar00rootroot00000000000000package main import ( "time" ) func (ui *gameui) Replay() { g := ui.g dl := g.DrawLog if len(dl) == 0 { return } g.DrawLog = nil rep := &replay{ui: ui, frames: dl, frame: 0} if ColorBase03 == Color256Base03 { rep.color256 = true } rep.Run() } type replay struct { ui *gameui frames []drawFrame undo [][]cellDraw frame int auto bool speed time.Duration evch chan repEvent color256 bool } type repEvent int const ( ReplayNext repEvent = iota ReplayPrevious ReplayTogglePause ReplayQuit ReplaySpeedMore ReplaySpeedLess ) func (rep *replay) Run() { rep.auto = true rep.speed = 1 rep.evch = make(chan repEvent, 100) rep.undo = [][]cellDraw{} go func(r *replay) { r.PollKeyboardEvents() }(rep) for { e := rep.PollEvent() switch e { case ReplayNext: if rep.frame >= len(rep.frames) { break } else if rep.frame < 0 { rep.frame = 0 } rep.DrawFrame() rep.frame++ case ReplayPrevious: if rep.frame <= 1 { break } else if rep.frame >= len(rep.frames) { rep.frame = len(rep.frames) } rep.frame-- rep.UndoFrame() case ReplayQuit: return case ReplayTogglePause: rep.auto = !rep.auto case ReplaySpeedMore: rep.speed *= 2 if rep.speed > 16 { rep.speed = 16 } case ReplaySpeedLess: rep.speed /= 2 if rep.speed < 1 { rep.speed = 1 } } } } func (rep *replay) DrawFrame() { ui := rep.ui df := rep.frames[rep.frame] rep.undo = append(rep.undo, []cellDraw{}) j := len(rep.undo) - 1 for _, dr := range df.Draws { i := ui.GetIndex(dr.X, dr.Y) c := ui.g.DrawBuffer[i] if rep.color256 { dr.Cell.Fg = rep.ui.Map16ColorTo256(dr.Cell.Fg) dr.Cell.Bg = rep.ui.Map16ColorTo256(dr.Cell.Bg) } else { dr.Cell.Bg = ui.Map256ColorTo16(dr.Cell.Bg) dr.Cell.Fg = ui.Map256ColorTo16(dr.Cell.Fg) } rep.undo[j] = append(rep.undo[j], cellDraw{Cell: c, X: dr.X, Y: dr.Y}) ui.SetGenCell(dr.X, dr.Y, dr.Cell.R, dr.Cell.Fg, dr.Cell.Bg, dr.Cell.InMap) } ui.Flush() ui.g.DrawLog = nil } func (rep *replay) UndoFrame() { ui := rep.ui df := rep.undo[len(rep.undo)-1] for _, dr := range df { ui.SetGenCell(dr.X, dr.Y, dr.Cell.R, dr.Cell.Fg, dr.Cell.Bg, dr.Cell.InMap) } rep.undo = rep.undo[:len(rep.undo)-1] ui.Flush() ui.g.DrawLog = nil } func (rep *replay) PollEvent() (in repEvent) { if rep.auto && rep.frame <= len(rep.frames)-1 && rep.frame >= 0 { var d time.Duration if rep.frame > 0 { d = rep.frames[rep.frame].Time.Sub(rep.frames[rep.frame-1].Time) } else { d = 0 } if d >= 2*time.Second { d = 2 * time.Second } d = d / rep.speed if d <= 10*time.Millisecond { d = 10 * time.Millisecond } t := time.NewTimer(d) select { case in = <-rep.evch: case <-t.C: in = ReplayNext } t.Stop() } else { in = <-rep.evch } return in } func (rep *replay) PollKeyboardEvents() { for { e := rep.ui.PollEvent() if e.interrupt { rep.evch <- ReplayNext continue } switch e.key { case "Q", "q", "\x1b": rep.evch <- ReplayQuit return case "p", "P", " ": rep.evch <- ReplayTogglePause case "+", ">": rep.evch <- ReplaySpeedMore case "-", "<": rep.evch <- ReplaySpeedLess case ".", "6", "j", "n", "f": rep.evch <- ReplayNext case "4", "k", "N", "b": rep.evch <- ReplayPrevious default: if !e.mouse { break } switch e.button { case 0: rep.evch <- ReplayNext case 1: rep.evch <- ReplayTogglePause case 2: rep.evch <- ReplayPrevious } } } } boohu-0.13.0/rods.go000066400000000000000000000357111356500202200142070ustar00rootroot00000000000000package main import ( "errors" "fmt" ) type rod int const ( RodDigging rod = iota RodBlink RodTeleportOther RodFireBolt RodFireBall RodLightning RodFog RodObstruction RodShatter RodSleeping RodLignification RodHope RodSwapping ) const NumRods = int(RodSwapping) + 1 func (r rod) Letter() rune { return '/' } func (r rod) String() string { var text string switch r { case RodDigging: text = "rod of digging" case RodBlink: text = "rod of blinking" case RodTeleportOther: text = "rod of teleport other" case RodFog: text = "rod of fog" case RodFireBall: text = "rod of fireball" case RodFireBolt: text = "rod of fire bolt" case RodLightning: text = "rod of lightning" case RodObstruction: text = "rod of obstruction" case RodShatter: text = "rod of shatter" case RodSleeping: text = "rod of sleeping" case RodLignification: text = "rod of lignification" case RodHope: text = "rod of last hope" case RodSwapping: text = "rod of swapping" } return text } func (r rod) Desc() string { var text string switch r { case RodDigging: text = "digs through up to 3 walls in a given direction." case RodBlink: text = "makes you blink away within your line of sight. The rod is more susceptible to send you to the cells thar are most far from you." case RodTeleportOther: text = "teleports away one of your foes. Note that the monster remembers where it saw you last time." case RodFog: text = "creates a dense fog that reduces your line of sight. Monsters at more than 1 cell away from you will not be able to see you." case RodFireBall: text = "throws a 1-radius fireball at your foes. You cannot use it against yourself. It can burn foliage and doors." case RodFireBolt: text = "throws a fire bolt through one or more enemies. It can burn foliage and doors." case RodLightning: text = "deals electrical damage to foes connected to you. It can burn foliage and doors." case RodObstruction: text = "creates a temporary wall at targeted location." case RodShatter: text = "induces an explosion around a wall, hurting adjacent monsters. The wall can disintegrate. You cannot use against yourself." case RodSleeping: text = "induces deep sleeping and exhaustion for monsters in the targeted area. You cannot use it against yourself." case RodLignification: text = "lignifies a monster, so that it cannot move, but can still fight with improved resistance." case RodHope: text = "creates an energy channel against a targeted monster. The damage done is inversely proportional to your health. It can burn foliage and doors." case RodSwapping: text = "makes you swap positions with a targeted monster." } return fmt.Sprintf("The %s %s Rods sometimes regain charges as you go deeper. This rod can have up to %d charges.", r, text, r.MaxCharge()) } type rodProps struct { Charge int } func (r rod) MaxCharge() (charges int) { switch r { case RodBlink: charges = 5 case RodDigging, RodShatter: charges = 3 default: charges = 4 } return charges } func (r rod) Rate() int { rate := r.MaxCharge() - 2 if rate < 1 { rate = 1 } return rate } func (r rod) MPCost() (mp int) { return 1 //switch r { //case RodBlink: //mp = 3 //case RodTeleportOther, RodDigging, RodShatter: //mp = 5 //default: //mp = 4 //} //return mp } func (r rod) Use(g *game, ev event) error { rods := g.Player.Rods if rods[r].Charge <= 0 { return errors.New("No charges remaining on this rod.") } if r.MPCost() > g.Player.MP { return errors.New("Not enough magic points for using this rod.") } if g.Player.HasStatus(StatusBerserk) { return errors.New("You cannot use rods while berserk.") } var err error switch r { case RodBlink: err = g.EvokeRodBlink(ev) case RodTeleportOther: err = g.EvokeRodTeleportOther(ev) case RodFireBolt: err = g.EvokeRodFireBolt(ev) case RodFireBall: err = g.EvokeRodFireball(ev) case RodLightning: err = g.EvokeRodLightning(ev) case RodFog: err = g.EvokeRodFog(ev) case RodDigging: err = g.EvokeRodDigging(ev) case RodObstruction: err = g.EvokeRodObstruction(ev) case RodShatter: err = g.EvokeRodShatter(ev) case RodSleeping: err = g.EvokeRodSleeping(ev) case RodLignification: err = g.EvokeRodLignification(ev) case RodHope: err = g.EvokeRodHope(ev) case RodSwapping: err = g.EvokeRodSwapping(ev) } if err != nil { return err } rp := rods[r] rp.Charge-- rods[r] = rp g.Player.MP -= r.MPCost() g.StoryPrintf("Evoked your %s.", r) g.Stats.UsedRod[r]++ g.Stats.Evocations++ g.FunAction() ev.Renew(g, 7) return nil } func (g *game) EvokeRodBlink(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot blink while lignified.") } g.Blink(ev) return nil } func (g *game) BlinkPos() position { losPos := []position{} for pos, b := range g.Player.LOS { if !b { continue } if g.Dungeon.Cell(pos).T != FreeCell { continue } mons := g.MonsterAt(pos) if mons.Exists() { continue } losPos = append(losPos, pos) } if len(losPos) == 0 { return InvalidPos } npos := losPos[RandInt(len(losPos))] for i := 0; i < 4; i++ { pos := losPos[RandInt(len(losPos))] if npos.Distance(g.Player.Pos) < pos.Distance(g.Player.Pos) { npos = pos } } return npos } func (g *game) Blink(ev event) { if g.Player.HasStatus(StatusLignification) { return } npos := g.BlinkPos() if !npos.valid() { // should not happen g.Print("You could not blink.") return } opos := g.Player.Pos g.Print("You blink away.") g.ui.TeleportAnimation(opos, npos, true) g.PlacePlayerAt(npos) } func (g *game) EvokeRodTeleportOther(ev event) error { if err := g.ui.ChooseTarget(&chooser{}); err != nil { return err } mons := g.MonsterAt(g.Player.Target) // mons not nil (check done in the targeter) mons.TeleportAway(g) return nil } func (g *game) EvokeRodSleeping(ev event) error { if err := g.ui.ChooseTarget(&chooser{area: true, minDist: true}); err != nil { return err } neighbors := g.Dungeon.FreeNeighbors(g.Player.Target) g.Print("A sleeping ball emerges straight out of the rod.") g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgSleepingMonster) for _, pos := range append(neighbors, g.Player.Target) { mons := g.MonsterAt(pos) if !mons.Exists() { continue } if mons.State != Resting { g.Printf("%s falls asleep.", mons.Kind.Definite(true)) } mons.State = Resting mons.ExhaustTime(g, 40+RandInt(10)) } return nil } func (g *game) EvokeRodFireBolt(ev event) error { if err := g.ui.ChooseTarget(&chooser{flammable: true}); err != nil { return err } ray := g.Ray(g.Player.Target) g.MakeNoise(MagicCastNoise, g.Player.Pos) g.Print("Whoosh! A fire bolt emerges straight out of the rod.") g.ui.FireBoltAnimation(ray) for _, pos := range ray { g.Burn(pos, ev) mons := g.MonsterAt(pos) if !mons.Exists() { continue } dmg := 0 for i := 0; i < 2; i++ { dmg += RandInt(21) } dmg /= 2 mons.HP -= dmg if mons.HP <= 0 { g.Printf("%s is killed by the bolt.", mons.Kind.Indefinite(true)) g.HandleKill(mons, ev) } g.MakeNoise(MagicHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) } return nil } func (g *game) EvokeRodFireball(ev event) error { if err := g.ui.ChooseTarget(&chooser{area: true, minDist: true, flammable: true}); err != nil { return err } neighbors := g.Dungeon.FreeNeighbors(g.Player.Target) g.MakeNoise(MagicExplosionNoise, g.Player.Target) g.Printf("A fireball emerges straight out of the rod... %s", g.ExplosionSound()) g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgExplosionStart) g.ui.ExplosionAnimation(FireExplosion, g.Player.Target) for _, pos := range append(neighbors, g.Player.Target) { g.Burn(pos, ev) mons := g.MonsterAt(pos) if mons == nil { continue } dmg := 0 for i := 0; i < 2; i++ { dmg += RandInt(24) } dmg /= 2 mons.HP -= dmg if mons.HP <= 0 { g.Printf("%s is killed by the fireball.", mons.Kind.Indefinite(true)) g.HandleKill(mons, ev) } g.MakeNoise(MagicHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) } return nil } func (g *game) EvokeRodLightning(ev event) error { d := g.Dungeon conn := map[position]bool{} nb := make([]position, 0, 8) nb = g.Player.Pos.Neighbors(nb, func(npos position) bool { return npos.valid() && d.Cell(npos).T != WallCell }) stack := []position{} for _, pos := range nb { mons := g.MonsterAt(pos) if !mons.Exists() { continue } stack = append(stack, pos) conn[pos] = true } if len(stack) == 0 { return errors.New("There are no adjacent monsters.") } g.MakeNoise(MagicCastNoise, g.Player.Pos) g.Print("Whoosh! Lightning emerges straight out of the rod.") var pos position targets := []position{} for len(stack) > 0 { pos = stack[len(stack)-1] stack = stack[:len(stack)-1] g.Burn(pos, ev) mons := g.MonsterAt(pos) if !mons.Exists() { continue } targets = append(targets, pos) dmg := 0 for i := 0; i < 2; i++ { dmg += RandInt(17) } dmg /= 2 mons.HP -= dmg if mons.HP <= 0 { g.Printf("%s is killed by lightning.", mons.Kind.Indefinite(true)) g.HandleKill(mons, ev) } g.MakeNoise(MagicHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) nb = pos.Neighbors(nb, func(npos position) bool { return npos.valid() && d.Cell(npos).T != WallCell }) for _, npos := range nb { if !conn[npos] { conn[npos] = true stack = append(stack, npos) } } } g.ui.LightningHitAnimation(targets) return nil } type cloud int const ( CloudFog cloud = iota CloudFire CloudNight ) func (g *game) EvokeRodFog(ev event) error { g.Fog(g.Player.Pos, 3, ev) g.Print("You are surrounded by a dense fog.") return nil } func (g *game) Fog(at position, radius int, ev event) { dij := &normalPath{game: g} nm := Dijkstra(dij, []position{at}, radius) for pos := range nm { _, ok := g.Clouds[pos] if !ok { g.Clouds[pos] = CloudFog g.PushEvent(&cloudEvent{ERank: ev.Rank() + 100 + RandInt(100), EAction: CloudEnd, Pos: pos}) } } g.ComputeLOS() } func (g *game) EvokeRodDigging(ev event) error { if err := g.ui.ChooseTarget(&wallChooser{}); err != nil { return err } pos := g.Player.Target for i := 0; i < 3; i++ { g.Dungeon.SetCell(pos, FreeCell) g.Stats.Digs++ g.MakeNoise(WallNoise, pos) g.Fog(pos, 1, ev) pos = pos.To(pos.Dir(g.Player.Pos)) if !g.Player.LOS[pos] { g.WrongWall[pos] = true } if !pos.valid() || g.Dungeon.Cell(pos).T != WallCell { break } } g.Print("You see the wall disintegrate with a crash.") g.ComputeLOS() g.MakeMonstersAware() return nil } func (g *game) EvokeRodShatter(ev event) error { if err := g.ui.ChooseTarget(&wallChooser{minDist: true}); err != nil { return err } neighbors := g.Dungeon.FreeNeighbors(g.Player.Target) g.Dungeon.SetCell(g.Player.Target, FreeCell) g.Stats.Digs++ g.ComputeLOS() g.MakeMonstersAware() g.MakeNoise(WallNoise, g.Player.Target) g.Printf("%s The wall disappeared.", g.CrackSound()) g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgExplosionWallStart) g.ui.ExplosionAnimation(WallExplosion, g.Player.Target) g.Fog(g.Player.Target, 2, ev) for _, pos := range neighbors { mons := g.MonsterAt(pos) if !mons.Exists() { continue } dmg := 0 for i := 0; i < 3; i++ { dmg += RandInt(30) } dmg /= 3 mons.HP -= dmg if mons.HP <= 0 { g.Printf("%s is killed by the explosion.", mons.Kind.Indefinite(true)) g.HandleKill(mons, ev) } g.MakeNoise(ExplosionHitNoise, mons.Pos) g.HandleStone(mons) mons.MakeHuntIfHurt(g) } return nil } func (g *game) EvokeRodObstruction(ev event) error { if err := g.ui.ChooseTarget(&chooser{free: true}); err != nil { return err } g.TemporalWallAt(g.Player.Target, ev) g.Printf("You see a wall appear out of thin air.") return nil } func (g *game) EvokeRodLignification(ev event) error { if err := g.ui.ChooseTarget(&chooser{}); err != nil { return err } mons := g.MonsterAt(g.Player.Target) // mons not nil (check done in targeter) if mons.Status(MonsLignified) { return errors.New("You cannot target a lignified monster.") } mons.EnterLignification(g, ev) return nil } func (g *game) TemporalWallAt(pos position, ev event) { if g.Dungeon.Cell(pos).T == WallCell { return } if !g.Player.LOS[pos] { g.WrongWall[pos] = true } g.CreateTemporalWallAt(pos, ev) g.ComputeLOS() } func (g *game) CreateTemporalWallAt(pos position, ev event) { g.Dungeon.SetCell(pos, WallCell) delete(g.Clouds, pos) g.TemporalWalls[pos] = true g.PushEvent(&cloudEvent{ERank: ev.Rank() + 200 + RandInt(50), Pos: pos, EAction: ObstructionEnd}) } func (g *game) EvokeRodHope(ev event) error { if err := g.ui.ChooseTarget(&chooser{needsFreeWay: true}); err != nil { return err } g.MakeNoise(MagicCastNoise, g.Player.Pos) g.ui.ProjectileTrajectoryAnimation(g.Ray(g.Player.Target), ColorFgExplosionStart) mons := g.MonsterAt(g.Player.Target) // mons not nil (check done in the targeter) attack := -20 + 30*DefaultHealth/g.Player.HP if attack > 130 { attack = 130 } dmg := 0 for i := 0; i < 5; i++ { dmg += RandInt(attack) } dmg /= 5 if dmg < 0 { // should not happen dmg = 0 } mons.HP -= dmg g.Burn(g.Player.Target, ev) g.ui.HitAnimation(g.Player.Target, true) g.Printf("An energy channel hits %s (%d dmg).", mons.Kind.Definite(false), dmg) if mons.HP <= 0 { g.Printf("%s dies.", mons.Kind.Indefinite(true)) g.HandleKill(mons, ev) } return nil } func (g *game) EvokeRodSwapping(ev event) error { if g.Player.HasStatus(StatusLignification) { return errors.New("You cannot use this rod while lignified.") } if err := g.ui.ChooseTarget(&chooser{}); err != nil { return err } mons := g.MonsterAt(g.Player.Target) // mons not nil (check done in the targeter) if mons.Status(MonsLignified) { return errors.New("You cannot target a lignified monster.") } g.SwapWithMonster(mons) return nil } func (g *game) SwapWithMonster(mons *monster) { ompos := mons.Pos g.Printf("You swap positions with the %s.", mons.Kind) g.ui.SwappingAnimation(mons.Pos, g.Player.Pos) mons.MoveTo(g, g.Player.Pos) g.PlacePlayerAt(ompos) mons.MakeAware(g) } func (g *game) GeneratedRodsCount() int { count := 0 for _, b := range g.GeneratedRods { if b { count++ } } return count } func (g *game) RandomRod() rod { r := rod(RandInt(NumRods)) return r } func (g *game) GenerateRod() { count := 0 for { count++ if count > 1000 { panic("GenerateRod") } pos := g.FreeCellForStatic() r := g.RandomRod() if _, ok := g.Player.Rods[r]; !ok && !g.GeneratedRods[r] { g.GeneratedRods[r] = true g.Rods[pos] = r return } } } func (g *game) RechargeRods() { for r, props := range g.Player.Rods { max := r.MaxCharge() if g.Player.Armour == CelmistRobe { max += 2 } if props.Charge < max { rchg := RandInt(1 + r.Rate()) if rchg == 0 && RandInt(2) == 0 { rchg++ } if g.Player.Armour == CelmistRobe { if RandInt(10) > 0 { rchg++ } if RandInt(3) == 0 { rchg++ } } props.Charge += rchg g.Player.Rods[r] = props } if props.Charge > max { props.Charge = max g.Player.Rods[r] = props } } } boohu-0.13.0/stairs.go000066400000000000000000000001141356500202200145320ustar00rootroot00000000000000package main type stair int const ( NormalStair stair = iota WinStair ) boohu-0.13.0/stats.go000066400000000000000000000027321356500202200143730ustar00rootroot00000000000000package main type stats struct { Story []string Killed int KilledMons map[monsterKind]int Moves int Hits int Misses int ReceivedHits int Dodges int Blocks int Drinks int Evocations int UsedStones int Throws int TimesLucky int Damage int DExplPerc [MaxDepth + 1]int DSleepingPerc [MaxDepth + 1]int DKilledPerc [MaxDepth + 1]int DLayout [MaxDepth + 1]string Burns int Digs int Rest int RestInterrupt int Turns int TWounded int TMWounded int TMonsLOS int UsedRod [NumRods]int } func (g *game) TurnStats() { g.Stats.Turns++ g.DepthPlayerTurn++ if g.Player.HP < g.Player.HPMax() { g.Stats.TWounded++ } if g.MonsterInLOS() != nil { g.Stats.TMonsLOS++ if g.Player.HP < g.Player.HPMax() { g.Stats.TMWounded++ } } } func (g *game) LevelStats() { free := 0 exp := 0 for _, c := range g.Dungeon.Cells { if c.T != FreeCell { continue } free++ if c.Explored { exp++ } } g.Stats.DExplPerc[g.Depth] = exp * 100 / free //g.Stats.DBurns[g.Depth] = g.Stats.CurBurns // XXX to avoid little dump info leak nmons := len(g.Monsters) kmons := 0 smons := 0 for _, mons := range g.Monsters { if !mons.Exists() { kmons++ continue } if mons.State == Resting { smons++ } } g.Stats.DSleepingPerc[g.Depth] = smons * 100 / nmons g.Stats.DKilledPerc[g.Depth] = kmons * 100 / nmons } boohu-0.13.0/status.go000066400000000000000000000042001356500202200145500ustar00rootroot00000000000000package main type status int const ( StatusBerserk status = iota StatusSlow StatusExhausted StatusSwift StatusAgile StatusLignification StatusConfusion StatusTele StatusNausea StatusDisabledShield StatusCorrosion StatusFlames // fake status StatusDig StatusSwap StatusShadows StatusSlay StatusAccurate ) func (st status) Good() bool { switch st { case StatusBerserk, StatusSwift, StatusAgile, StatusDig, StatusSwap, StatusShadows, StatusSlay, StatusAccurate: return true default: return false } } func (st status) Bad() bool { switch st { case StatusSlow, StatusConfusion, StatusNausea, StatusDisabledShield, StatusFlames, StatusCorrosion: return true default: return false } } func (st status) String() string { switch st { case StatusBerserk: return "Berserk" case StatusSlow: return "Slow" case StatusExhausted: return "Exhausted" case StatusSwift: return "Swift" case StatusLignification: return "Lignified" case StatusAgile: return "Agile" case StatusConfusion: return "Confused" case StatusTele: return "Tele" case StatusNausea: return "Nausea" case StatusDisabledShield: return "-Shield" case StatusCorrosion: return "Corroded" case StatusFlames: return "Flames" case StatusDig: return "Dig" case StatusSwap: return "Swap" case StatusShadows: return "Shadows" case StatusSlay: return "Slay" case StatusAccurate: return "Accurate" default: // should not happen return "unknown" } } func (st status) Short() string { switch st { case StatusBerserk: return "Be" case StatusSlow: return "Sl" case StatusExhausted: return "Ex" case StatusSwift: return "Sw" case StatusLignification: return "Li" case StatusAgile: return "Ag" case StatusConfusion: return "Co" case StatusTele: return "Te" case StatusNausea: return "Na" case StatusDisabledShield: return "-S" case StatusCorrosion: return "Co" case StatusFlames: return "Fl" case StatusDig: return "Di" case StatusSwap: return "Sw" case StatusShadows: return "Sh" case StatusSlay: return "Sl" case StatusAccurate: return "Ac" default: // should not happen return "?" } } boohu-0.13.0/stones.go000066400000000000000000000027211356500202200145460ustar00rootroot00000000000000package main type stone int const ( InertStone stone = iota TeleStone FogStone QueenStone TreeStone ObstructionStone ) const NumStones = int(ObstructionStone) + 1 func (s stone) String() (text string) { switch s { case InertStone: text = "inert stone" case TeleStone: text = "teleport stone" case FogStone: text = "fog stone" case QueenStone: text = "queenstone" case TreeStone: text = "tree stone" case ObstructionStone: text = "obstruction stone" } return text } func (s stone) Description() (text string) { switch s { case InertStone: text = "This stone has been depleted of magical energies." case TeleStone: text = "Any creature standing on the teleport stone will teleport away when hit in combat." case FogStone: text = "Fog will appear if a creature is hurt while standing on the fog stone." case QueenStone: text = "If a creature is hurt while standing on queenstone, a loud boom will resonate, leaving nearby monsters in a 2-range distance confused. You know how to avoid the effect yourself." case TreeStone: text = "Any creature hurt while standing on a tree stone will be lignified." case ObstructionStone: text = "When a creature is hurt while standing on the obstruction stone, temporal walls appear around it." } return text } func (g *game) UseStone(pos position) { g.StoryPrintf("Activated a %s.", g.MagicalStones[pos]) g.MagicalStones[pos] = InertStone g.Stats.UsedStones++ g.Print("The stone becomes inert.") } boohu-0.13.0/target.go000066400000000000000000000117421356500202200145240ustar00rootroot00000000000000package main import "errors" type Targeter interface { ComputeHighlight(*game, position) Action(*game, position) error Reachable(*game, position) bool Done() bool } type examiner struct { done bool stairs bool } func (ex *examiner) ComputeHighlight(g *game, pos position) { g.ComputePathHighlight(pos) } func (g *game) ComputePathHighlight(pos position) { path := g.PlayerPath(g.Player.Pos, pos) g.Highlight = map[position]bool{} for _, p := range path { g.Highlight[p] = true } } func (ex *examiner) Action(g *game, pos position) error { if !g.Dungeon.Cell(pos).Explored { return errors.New("You do not know this place.") } if g.Dungeon.Cell(pos).T == WallCell && !g.Player.HasStatus(StatusDig) { return errors.New("You cannot travel into a wall.") } path := g.PlayerPath(g.Player.Pos, pos) if len(path) == 0 { if ex.stairs { return errors.New("There is no safe path to the nearest stairs.") } return errors.New("There is no safe path to this place.") } if c := g.Dungeon.Cell(pos); c.Explored && c.T == FreeCell { g.AutoTarget = pos g.Targeting = pos ex.done = true return nil } return errors.New("Invalid destination.") } func (ex *examiner) Reachable(g *game, pos position) bool { return true } func (ex *examiner) Done() bool { return ex.done } type chooser struct { done bool area bool minDist bool needsFreeWay bool free bool flammable bool wall bool } func (ch *chooser) ComputeHighlight(g *game, pos position) { g.ComputeRayHighlight(pos) if !ch.area { return } neighbors := g.Dungeon.FreeNeighbors(pos) for _, pos := range neighbors { g.Highlight[pos] = true } } func (ch *chooser) Reachable(g *game, pos position) bool { return g.Player.LOS[pos] } func (ch *chooser) Action(g *game, pos position) error { if !ch.Reachable(g, pos) { return errors.New("You cannot target that place.") } if ch.minDist && pos.Distance(g.Player.Pos) <= 1 { return errors.New("Invalid target: too close.") } c := g.Dungeon.Cell(pos) if c.T == WallCell { return errors.New("You cannot target a wall.") } if (ch.area || ch.needsFreeWay) && !ch.freeWay(g, pos) { return errors.New("Invalid target: there are monsters in the way.") } mons := g.MonsterAt(pos) if ch.free { if mons.Exists() { return errors.New("Invalid target: there is a monster there.") } if g.Player.Pos == pos { return errors.New("Invalid target: you are here.") } g.Player.Target = pos ch.done = true return nil } if mons.Exists() || ch.flammable && ch.flammableInWay(g, pos) { g.Player.Target = pos ch.done = true return nil } if ch.flammable && ch.flammableInWay(g, pos) { g.Player.Target = pos ch.done = true return nil } if !ch.area { return errors.New("You must target a monster.") } neighbors := pos.ValidNeighbors() for _, npos := range neighbors { nc := g.Dungeon.Cell(npos) if !ch.wall && nc.T == WallCell { continue } mons := g.MonsterAt(npos) _, okFungus := g.Fungus[pos] _, okDoors := g.Doors[pos] if ch.flammable && (okFungus || okDoors) || mons.Exists() || nc.T == WallCell { g.Player.Target = pos ch.done = true return nil } } if ch.flammable && ch.wall { return errors.New("Invalid target: no monsters, walls nor flammable terrain in the area.") } if ch.flammable { return errors.New("Invalid target: no monsters nor flammable terrain in the area.") } if ch.wall { return errors.New("Invalid target: no monsters nor walls in the area.") } return errors.New("Invalid target: no monsters in the area.") } func (ch *chooser) Done() bool { return ch.done } func (ch *chooser) freeWay(g *game, pos position) bool { ray := g.Ray(pos) tpos := pos for _, rpos := range ray { mons := g.MonsterAt(rpos) if !mons.Exists() { continue } tpos = mons.Pos } return tpos == pos } func (ch *chooser) flammableInWay(g *game, pos position) bool { ray := g.Ray(pos) for _, rpos := range ray { if rpos == g.Player.Pos { continue } if _, ok := g.Fungus[rpos]; ok { return true } if _, ok := g.Doors[rpos]; ok { return true } } return false } type wallChooser struct { done bool minDist bool } func (ch *wallChooser) ComputeHighlight(g *game, pos position) { g.ComputeRayHighlight(pos) } func (ch *wallChooser) Reachable(g *game, pos position) bool { return g.Player.LOS[pos] } func (ch *wallChooser) Action(g *game, pos position) error { if !ch.Reachable(g, pos) { return errors.New("You cannot target that place.") } ray := g.Ray(pos) if len(ray) == 0 { return errors.New("You are not a wall.") } if g.Dungeon.Cell(ray[0]).T != WallCell { return errors.New("You must target a wall.") } if ch.minDist && g.Player.Pos.Distance(pos) <= 1 { return errors.New("You cannot target an adjacent wall.") } for _, pos := range ray[1:] { mons := g.MonsterAt(pos) if mons.Exists() { return errors.New("There are monsters in the way.") } } g.Player.Target = pos ch.done = true return nil } func (ch *wallChooser) Done() bool { return ch.done } boohu-0.13.0/tcell.go000066400000000000000000000053741356500202200143450ustar00rootroot00000000000000// +build tcell package main import ( "runtime" "github.com/gdamore/tcell" ) type gameui struct { g *game tcell.Screen cursor position small bool // below unused for this backend menuHover menu itemHover int } func (ui *gameui) Init() error { screen, err := tcell.NewScreen() ui.Screen = screen if err != nil { return err } err = ui.Screen.Init() if err != nil { return err } ui.Screen.SetStyle(tcell.StyleDefault) if runtime.GOOS != "openbsd" { ui.Screen.EnableMouse() } ui.Screen.HideCursor() ui.HideCursor() ui.menuHover = -1 return nil } func (ui *gameui) Close() { ui.Screen.Fini() } var SmallScreen = false func (ui *gameui) Flush() { ui.DrawLogFrame() for _, cdraw := range ui.g.DrawLog[len(ui.g.DrawLog)-1].Draws { cell := cdraw.Cell st := tcell.StyleDefault fg := cell.Fg bg := cell.Bg if Only8Colors { fg = Map16ColorTo8Color(fg) bg = Map16ColorTo8Color(bg) } st = st.Foreground(tcell.Color(fg)).Background(tcell.Color(bg)) ui.Screen.SetContent(cdraw.X, cdraw.Y, cell.R, nil, st) } //ui.g.Printf("%d %d %d", ui.g.DrawFrame, ui.g.DrawFrameStart, len(ui.g.DrawLog)) ui.Screen.Show() w, h := ui.Screen.Size() if w <= UIWidth-8 || h <= UIHeight-2 { SmallScreen = true } else { SmallScreen = false } } func (ui *gameui) ApplyToggleLayout() { GameConfig.Small = !GameConfig.Small if GameConfig.Small { ui.Clear() ui.Flush() UIHeight = 24 UIWidth = 80 } else { UIHeight = 26 UIWidth = 100 } ui.g.DrawBuffer = make([]UICell, UIWidth*UIHeight) ui.Clear() } func (ui *gameui) Small() bool { return GameConfig.Small || SmallScreen } func (ui *gameui) Interrupt() { ui.Screen.PostEvent(tcell.NewEventInterrupt(nil)) } func (ui *gameui) PollEvent() (in uiInput) { switch tev := ui.Screen.PollEvent().(type) { case *tcell.EventKey: switch tev.Key() { case tcell.KeyEsc: in.key = " " case tcell.KeyLeft: // TODO: will not work if user changes keybindings in.key = "4" case tcell.KeyDown: in.key = "2" case tcell.KeyUp: in.key = "8" case tcell.KeyRight: in.key = "6" case tcell.KeyHome: in.key = "7" case tcell.KeyEnd: in.key = "1" case tcell.KeyPgUp: in.key = "9" case tcell.KeyPgDn: in.key = "3" case tcell.KeyDelete: in.key = "5" case tcell.KeyCtrlW: in.key = "W" case tcell.KeyCtrlQ: in.key = "Q" case tcell.KeyCtrlP: in.key = "m" } if tev.Rune() != 0 && in.key == "" { in.key = string(tev.Rune()) } case *tcell.EventMouse: in.mouseX, in.mouseY = tev.Position() switch tev.Buttons() { case tcell.Button1: in.mouse = true in.button = 0 case tcell.Button2: in.mouse = true in.button = 1 case tcell.Button3: in.mouse = true in.button = 2 } case *tcell.EventInterrupt: in.interrupt = true } return in } boohu-0.13.0/termbox.go000066400000000000000000000050161356500202200147130ustar00rootroot00000000000000// +build !tcell,!ansi,!js,!tk package main import ( termbox "github.com/nsf/termbox-go" ) type gameui struct { g *game cursor position small bool // below unused for this backend menuHover menu itemHover int } func (ui *gameui) Init() error { err := termbox.Init() if err != nil { return err } termbox.SetOutputMode(termbox.Output256) termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse) termbox.HideCursor() ui.HideCursor() ui.menuHover = -1 return nil } func (ui *gameui) Close() { termbox.Close() } var SmallScreen = false func (ui *gameui) Flush() { ui.DrawLogFrame() for _, cdraw := range ui.g.DrawLog[len(ui.g.DrawLog)-1].Draws { cell := cdraw.Cell fg := cell.Fg bg := cell.Bg if Only8Colors { fg = Map16ColorTo8Color(fg) bg = Map16ColorTo8Color(bg) } termbox.SetCell(cdraw.X, cdraw.Y, cell.R, termbox.Attribute(fg)+1, termbox.Attribute(bg)+1) } termbox.Flush() w, h := termbox.Size() if w <= UIWidth-8 || h <= UIHeight-2 { SmallScreen = true } else { SmallScreen = false } } func (ui *gameui) ApplyToggleLayout() { GameConfig.Small = !GameConfig.Small if GameConfig.Small { ui.Clear() ui.Flush() UIHeight = 24 UIWidth = 80 } else { UIHeight = 26 UIWidth = 100 } ui.g.DrawBuffer = make([]UICell, UIWidth*UIHeight) ui.Clear() } func (ui *gameui) Small() bool { return GameConfig.Small || SmallScreen } func (ui *gameui) Interrupt() { termbox.Interrupt() } func (ui *gameui) PollEvent() (in uiInput) { switch tev := termbox.PollEvent(); tev.Type { case termbox.EventKey: if tev.Ch == 0 { switch tev.Key { case termbox.KeyArrowLeft: in.key = "4" case termbox.KeyArrowDown: in.key = "2" case termbox.KeyArrowUp: in.key = "8" case termbox.KeyArrowRight: in.key = "6" case termbox.KeyHome: in.key = "7" case termbox.KeyEnd: in.key = "1" case termbox.KeyPgup: in.key = "9" case termbox.KeyPgdn: in.key = "3" case termbox.KeyDelete: in.key = "5" case termbox.KeyEsc, termbox.KeySpace: in.key = " " case termbox.KeyEnter: in.key = "." } } if tev.Ch != 0 && in.key == "" { in.key = string(tev.Ch) } case termbox.EventMouse: if tev.Ch == 0 { in.mouseX, in.mouseY = tev.MouseX, tev.MouseY switch tev.Key { case termbox.MouseLeft: in.mouse = true in.button = 0 case termbox.MouseMiddle: in.mouse = true in.button = 1 case termbox.MouseRight: in.mouse = true in.button = 2 } } case termbox.EventInterrupt: in.interrupt = true } return in } boohu-0.13.0/terminal.go000066400000000000000000000003331356500202200150430ustar00rootroot00000000000000// +build !js,!tk package main func (ui *gameui) ApplyToggleTiles() { } func (ui *gameui) PostConfig() { if GameConfig.Small { UIHeight = 24 UIWidth = 80 } } func (ui *gameui) ColorLine(y int, fg uicolor) { } boohu-0.13.0/tiles.go000066400000000000000000000116031356500202200143520ustar00rootroot00000000000000// +build js tk package main import ( "bytes" "encoding/base64" "image" "image/color" "image/draw" "image/png" "log" ) func (ui *gameui) ApplyToggleTiles() { GameConfig.Tiles = !GameConfig.Tiles for c, _ := range ui.cache { if c.InMap { delete(ui.cache, c) } } for i := 0; i < len(ui.g.drawBackBuffer); i++ { ui.g.drawBackBuffer[i] = UICell{} } } func (c uicolor) String() string { color := "#002b36" switch c { case 0: color = "#073642" case 1: color = "#dc322f" case 2: color = "#859900" case 3: color = "#b58900" case 4: color = "#268bd2" case 5: color = "#d33682" case 6: color = "#2aa198" case 7: color = "#eee8d5" case 8: color = "#002b36" case 9: color = "#cb4b16" case 10: color = "#586e75" case 11: color = "#657b83" case 12: color = "#839496" case 13: color = "#6c71c4" case 14: color = "#93a1a1" case 15: color = "#fdf6e3" } return color } func (c uicolor) Color() color.Color { cl := color.RGBA{} opaque := uint8(255) switch c { case 0: cl = color.RGBA{7, 54, 66, opaque} case 1: cl = color.RGBA{220, 50, 47, opaque} case 2: cl = color.RGBA{133, 153, 0, opaque} case 3: cl = color.RGBA{181, 137, 0, opaque} case 4: cl = color.RGBA{38, 139, 210, opaque} case 5: cl = color.RGBA{211, 54, 130, opaque} case 6: cl = color.RGBA{42, 161, 152, opaque} case 7: cl = color.RGBA{238, 232, 213, opaque} case 8: cl = color.RGBA{0, 43, 54, opaque} case 9: cl = color.RGBA{203, 75, 22, opaque} case 10: cl = color.RGBA{88, 110, 117, opaque} case 11: cl = color.RGBA{101, 123, 131, opaque} case 12: cl = color.RGBA{131, 148, 150, opaque} case 13: cl = color.RGBA{108, 113, 196, opaque} case 14: cl = color.RGBA{147, 161, 161, opaque} case 15: cl = color.RGBA{253, 246, 227, opaque} } return cl } var TileImgs map[string][]byte var MapNames = map[rune]string{ '¤': "frontier", '√': "hit", 'Φ': "magic", '☻': "dreaming", '♫': "footsteps", '#': "wall", '@': "player", '§': "fog", '♣': "simella", '+': "door", '.': "ground", '"': "foliage", '•': "tick", '●': "rock", '×': "times", ',': "comma", '}': "rbrace", '%': "percent", ':': "colon", '\\': "backslash", '~': "tilde", '☼': "sun", '*': "asterisc", '—': "hbar", '/': "slash", '|': "vbar", '∞': "kill", ' ': "space", '[': "lbracket", ']': "rbracket", ')': "rparen", '(': "lparen", '>': "stairs", 'Δ': "portal", '!': "potion", ';': "semicolon", '_': "stone", } var LetterNames = map[rune]string{ '(': "lparen", ')': "rparen", '@': "player", '{': "lbrace", '}': "rbrace", '[': "lbracket", ']': "rbracket", '♪': "music1", '♫': "music2", '•': "tick", '♣': "simella", ' ': "space", '!': "exclamation", '?': "interrogation", ',': "comma", ':': "colon", ';': "semicolon", '\'': "quote", '—': "longhyphen", '-': "hyphen", '|': "pipe", '/': "slash", '\\': "backslash", '%': "percent", '┐': "boxne", '┤': "boxe", '│': "vbar", '┘': "boxse", '─': "hbar", '►': "arrow", '×': "times", '.': "dot", '#': "hash", '"': "quotes", '+': "plus", '“': "lquotes", '”': "rquotes", '=': "equal", '>': "gt", 'Δ': "portal", '¤': "frontier", '√': "hit", 'Φ': "magic", '§': "fog", '●': "rock", '~': "tilde", '☼': "sun", '*': "asterisc", '∞': "kill", '☻': "dreaming", '…': "dots", '_': "stone", } func (ui *gameui) Interrupt() { interrupt <- true } func (ui *gameui) Small() bool { return GameConfig.Small } func (ui *gameui) ColorLine(y int, fg uicolor) { for x := 0; x < DungeonWidth; x++ { i := ui.GetIndex(x, y) c := ui.g.DrawBuffer[i] ui.SetCell(x, y, c.R, fg, c.Bg) } } func getImage(cell UICell) *image.RGBA { var pngImg []byte if cell.InMap && GameConfig.Tiles { pngImg = TileImgs["map-notile"] if im, ok := TileImgs["map-"+string(cell.R)]; ok { pngImg = im } else if im, ok := TileImgs["map-"+MapNames[cell.R]]; ok { pngImg = im } } else { pngImg = TileImgs["map-notile"] if im, ok := TileImgs["letter-"+string(cell.R)]; ok { pngImg = im } else if im, ok := TileImgs["letter-"+LetterNames[cell.R]]; ok { pngImg = im } } buf := make([]byte, len(pngImg)) base64.StdEncoding.Decode(buf, pngImg) // TODO: check error br := bytes.NewReader(buf) img, err := png.Decode(br) if err != nil { log.Printf("Could not decode png: %v", err) } rect := img.Bounds() rgbaimg := image.NewRGBA(rect) draw.Draw(rgbaimg, rect, img, rect.Min, draw.Src) bgc := cell.Bg.Color() fgc := cell.Fg.Color() for y := 0; y < rect.Max.Y; y++ { for x := 0; x < rect.Max.X; x++ { c := rgbaimg.At(x, y) r, _, _, _ := c.RGBA() if r == 0 { rgbaimg.Set(x, y, bgc) } else { rgbaimg.Set(x, y, fgc) } } } return rgbaimg } func (ui *gameui) PostConfig() { if GameConfig.Small { GameConfig.Small = false ui.ApplyToggleLayoutWithClear(false) } } boohu-0.13.0/tk.go000066400000000000000000000122701356500202200136510ustar00rootroot00000000000000// +build tk package main import ( "bytes" "encoding/base64" "image" "image/draw" "image/png" "unicode/utf8" "github.com/nsf/gothic" ) type gameui struct { g *game ir *gothic.Interpreter cursor position stty string cache map[UICell]*image.RGBA width int height int mousepos position menuHover menu itemHover int canvas *image.RGBA } func (ui *gameui) Init() error { ui.canvas = image.NewRGBA(image.Rect(0, 0, UIWidth*16, UIHeight*24)) ui.ir = gothic.NewInterpreter(` wm title . "Boohu Tk" wm resizable . 0 0 set width [expr {16 * 100}] set height [expr {24 * 26}] wm geometry . =${width}x$height set can [canvas .c -width $width -height $height -background #002b36] grid $can -row 0 -column 0 focus $can image create photo gamescreen -width $width -height $height -palette 256/256/256 image create photo bufscreen -width $width -height $height -palette 256/256/256 $can create image 0 0 -anchor nw -image gamescreen `) ui.InitElements() ui.ir.RegisterCommand("GetKey", func(c, keysym string) { var s string if c != "" { s = c } else { s = keysym } ch <- uiInput{key: s} }) ui.ir.RegisterCommand("MouseDown", func(x, y, b int) { ch <- uiInput{mouse: true, mouseX: (x - 1) / ui.width, mouseY: (y - 1) / ui.height, button: b - 1} }) ui.ir.RegisterCommand("MouseMotion", func(x, y int) { nx := (x - 1) / ui.width ny := (y - 1) / ui.height if nx != ui.mousepos.X || ny != ui.mousepos.Y { ui.mousepos.X = nx ui.mousepos.Y = ny ch <- uiInput{mouse: true, mouseX: nx, mouseY: ny, button: -1} } }) ui.ir.Eval(` bind .c { GetKey %A %K } bind .c { MouseMotion %x %y } bind .c { MouseDown %x %y %b } `) ui.menuHover = -1 SolarizedPalette() ui.HideCursor() settingsActions = append(settingsActions, toggleTiles) GameConfig.Tiles = true return nil } func (ui *gameui) InitElements() error { ui.width = 16 ui.height = 24 ui.cache = make(map[UICell]*image.RGBA) return nil } var ch chan uiInput var interrupt chan bool func init() { ch = make(chan uiInput, 100) interrupt = make(chan bool) } func (ui *gameui) Close() { } func (ui *gameui) Flush() { ui.DrawLogFrame() // very ugly optimisation xdgnmin := UIWidth - 1 xdgnmax := 0 ydgnmin := UIHeight - 1 ydgnmax := 0 xlogmin := UIWidth - 1 xlogmax := 0 ylogmin := UIHeight - 1 ylogmax := 0 xbarmin := UIWidth - 1 xbarmax := 0 ybarmin := UIHeight - 1 ybarmax := 0 for _, cdraw := range ui.g.DrawLog[len(ui.g.DrawLog)-1].Draws { cell := cdraw.Cell x, y := cdraw.X, cdraw.Y ui.Draw(cell, x, y) switch { case x < DungeonWidth && y < DungeonHeight: if x < xdgnmin { xdgnmin = x } if x > xdgnmax { xdgnmax = x } if y < ydgnmin { ydgnmin = y } if y > ydgnmax { ydgnmax = y } case x > DungeonWidth: if x < xbarmin { xbarmin = x } if x > xbarmax { xbarmax = x } if y < ybarmin { ybarmin = y } if y > ybarmax { ybarmax = y } default: if x < xlogmin { xlogmin = x } if x > xlogmax { xlogmax = x } if y < ylogmin { ylogmin = y } if y > ylogmax { ylogmax = y } } } ui.UpdateRectangle(xdgnmin, ydgnmin, xdgnmax, ydgnmax) ui.UpdateRectangle(xbarmin, ybarmin, xbarmax, ybarmax) ui.UpdateRectangle(xlogmin, ylogmin, xlogmax, ylogmax) } func (ui *gameui) UpdateRectangle(xmin, ymin, xmax, ymax int) { if xmin > xmax || ymin > ymax { return } pngbuf := &bytes.Buffer{} subimg := ui.canvas.SubImage(image.Rect(xmin*16, ymin*24, (xmax+1)*16, (ymax+1)*24)) png.Encode(pngbuf, subimg) png := base64.StdEncoding.EncodeToString(pngbuf.Bytes()) ui.ir.Eval("gamescreen put %{0%s} -format png -to %{1%d} %{2%d} %{3%d} %{4%d}", png, xmin*16, ymin*24, (xmax+1)*16, (ymax+1)*24) // TODO: optimize this more } func (ui *gameui) ApplyToggleLayout() { ui.ApplyToggleLayoutWithClear(true) } func (ui *gameui) ApplyToggleLayoutWithClear(clear bool) { GameConfig.Small = !GameConfig.Small if GameConfig.Small { ui.ir.Eval("wm geometry . =1280x576") if clear { ui.Clear() ui.Flush() } UIHeight = 24 UIWidth = 80 } else { ui.ir.Eval("wm geometry . =${width}x$height") UIHeight = 26 UIWidth = 100 } ui.cache = make(map[UICell]*image.RGBA) ui.g.DrawBuffer = make([]UICell, UIWidth*UIHeight) if clear { ui.Clear() } } func (ui *gameui) Draw(cell UICell, x, y int) { var img *image.RGBA if im, ok := ui.cache[cell]; ok { img = im } else { img = getImage(cell) ui.cache[cell] = img } draw.Draw(ui.canvas, image.Rect(x*ui.width, ui.height*y, (x+1)*ui.width, (y+1)*ui.height), img, image.Point{0, 0}, draw.Over) } func (ui *gameui) PollEvent() (in uiInput) { select { case in = <-ch: case in.interrupt = <-interrupt: } switch in.key { case "KP_Enter", "Return", "\r", "\n": in.key = "." case "Left", "KP_Left": in.key = "4" case "Right", "KP_Right": in.key = "6" case "Up", "KP_Up", "BackSpace": in.key = "8" case "Down", "KP_Down": in.key = "2" case "KP_Home": in.key = "7" case "KP_End": in.key = "1" case "KP_Prior", "Prior": in.key = "9" case "KP_Next", "Next": in.key = "3" case "KP_Begin", "KP_Delete": in.key = "5" default: if utf8.RuneCountInString(in.key) != 1 { in.key = "" } } return in } boohu-0.13.0/ui.go000066400000000000000000001041771356500202200136600ustar00rootroot00000000000000package main import ( "errors" "fmt" "path/filepath" "strings" "time" "unicode" "unicode/utf8" ) type UICell struct { Fg uicolor Bg uicolor R rune InMap bool } type uiInput struct { key string mouse bool mouseX int mouseY int button int interrupt bool } func (ui *gameui) HideCursor() { ui.cursor = InvalidPos } func (ui *gameui) SetCursor(pos position) { ui.cursor = pos } func (ui *gameui) KeyToRuneKeyAction(in uiInput) rune { if utf8.RuneCountInString(in.key) != 1 { return 0 } return ui.ReadKey(in.key) } func (ui *gameui) WaitForContinue(line int) { loop: for { in := ui.PollEvent() r := ui.KeyToRuneKeyAction(in) switch r { case '\x1b', ' ', 'x', 'X': break loop } if in.mouse && in.button == -1 { continue } if in.mouse && line >= 0 { if in.mouseY > line || in.mouseX > DungeonWidth { break loop } } else if in.mouse { break loop } } } func (ui *gameui) PromptConfirmation() bool { for { in := ui.PollEvent() switch in.key { case "Y", "y": return true default: return false } } } func (ui *gameui) PressAnyKey() error { for { e := ui.PollEvent() if e.interrupt { return errors.New("interrupted") } if e.key != "" || (e.mouse && e.button != -1) { return nil } } } type startAction int const ( StartPlay startAction = iota StartWatchReplay ) func (ui *gameui) StartMenu(l int) startAction { for { in := ui.PollEvent() switch in.key { case "P", "p": ui.ColorLine(l, ColorYellow) ui.Flush() time.Sleep(10 * time.Millisecond) return StartPlay case "W", "w": ui.ColorLine(l+1, ColorYellow) ui.Flush() time.Sleep(10 * time.Millisecond) return StartWatchReplay } if in.key != "" && !in.mouse { continue } y := in.mouseY switch in.button { case -1: oih := ui.itemHover if y < l || y >= l+2 { ui.itemHover = -1 if oih != -1 { ui.ColorLine(oih, ColorFg) ui.Flush() } break } if y == oih { break } ui.itemHover = y ui.ColorLine(y, ColorYellow) if oih != -1 { ui.ColorLine(oih, ColorFg) } ui.Flush() case 0: if y < l || y >= l+2 { ui.itemHover = -1 break } ui.itemHover = -1 switch y - l { case 0: return StartPlay case 1: return StartWatchReplay } } } } func (ui *gameui) PlayerTurnEvent(ev event) (err error, again, quit bool) { g := ui.g again = true in := ui.PollEvent() switch in.key { case "": if in.mouse { pos := position{X: in.mouseX, Y: in.mouseY} switch in.button { case -1: if in.mouseY == DungeonHeight { m, ok := ui.WhichButton(in.mouseX) omh := ui.menuHover if ok { ui.menuHover = m } else { ui.menuHover = -1 } if ui.menuHover != omh { ui.DrawMenus() ui.Flush() } break } ui.menuHover = -1 if in.mouseX >= DungeonWidth || in.mouseY >= DungeonHeight { again = true break } fallthrough case 0: if in.mouseY == DungeonHeight { m, ok := ui.WhichButton(in.mouseX) if !ok { again = true break } err, again, quit = ui.HandleKeyAction(runeKeyAction{k: m.Key(g)}) if err != nil { again = true } return err, again, quit } else if in.mouseX >= DungeonWidth || in.mouseY >= DungeonHeight { again = true } else { err, again, quit = ui.ExaminePos(ev, pos) } case 2: err, again, quit = ui.HandleKeyAction(runeKeyAction{k: KeyMenu}) if err != nil { again = true } return err, again, quit } } default: r := ui.KeyToRuneKeyAction(in) if r == 0 { again = true } else { err, again, quit = ui.HandleKeyAction(runeKeyAction{r: r}) } } if err != nil { again = true } return err, again, quit } func (ui *gameui) Scroll(n int) (m int, quit bool) { in := ui.PollEvent() switch in.key { case "Escape", "\x1b", " ", "x", "X": quit = true case "u", "9", "b": n -= 12 case "d", "3", "f": n += 12 case "j", "2", ".": n++ case "k", "8": n-- case "": if in.mouse { switch in.button { case 0: y := in.mouseY x := in.mouseX if x >= DungeonWidth { quit = true break } if y > UIHeight { break } n += y - (DungeonHeight+3)/2 } } } return n, quit } func (ui *gameui) GetIndex(x, y int) int { return y*UIWidth + x } func (ui *gameui) GetPos(i int) (int, int) { return i - (i/UIWidth)*UIWidth, i / UIWidth } func (ui *gameui) Select(l int) (index int, alternate bool, err error) { if ui.itemHover >= 1 && ui.itemHover <= l { ui.ColorLine(ui.itemHover, ColorYellow) ui.Flush() } else { ui.itemHover = -1 } for { in := ui.PollEvent() r := ui.ReadKey(in.key) switch { case in.key == "\x1b" || in.key == "Escape" || in.key == " " || in.key == "x" || in.key == "X": return -1, false, errors.New(DoNothing) case in.key == "?": return -1, true, nil case 97 <= r && int(r) < 97+l: if ui.itemHover >= 1 && ui.itemHover <= l { ui.ColorLine(ui.itemHover, ColorFg) } ui.itemHover = int(r-97) + 1 return int(r - 97), false, nil case in.key == "2": oih := ui.itemHover ui.itemHover++ if ui.itemHover < 1 || ui.itemHover > l { ui.itemHover = 1 } if oih > 0 && oih <= l { ui.ColorLine(oih, ColorFg) } ui.ColorLine(ui.itemHover, ColorYellow) ui.Flush() case in.key == "8": oih := ui.itemHover ui.itemHover-- if ui.itemHover < 1 { ui.itemHover = l } if oih > 0 && oih <= l { ui.ColorLine(oih, ColorFg) } ui.ColorLine(ui.itemHover, ColorYellow) ui.Flush() case in.key == "." && ui.itemHover >= 1 && ui.itemHover <= l: if ui.itemHover >= 1 && ui.itemHover <= l { ui.ColorLine(ui.itemHover, ColorFg) } return ui.itemHover - 1, false, nil case in.key == "" && in.mouse: y := in.mouseY x := in.mouseX switch in.button { case -1: oih := ui.itemHover if y <= 0 || y > l || x >= DungeonWidth { ui.itemHover = -1 if oih > 0 { ui.ColorLine(oih, ColorFg) ui.Flush() } break } if y == oih { break } ui.itemHover = y ui.ColorLine(y, ColorYellow) if oih > 0 { ui.ColorLine(oih, ColorFg) } ui.Flush() case 0: if y < 0 || y > l || x >= DungeonWidth { ui.itemHover = -1 return -1, false, errors.New(DoNothing) } if y == 0 { ui.itemHover = -1 return -1, true, nil } ui.itemHover = -1 return y - 1, false, nil case 2: ui.itemHover = -1 return -1, true, nil case 1: ui.itemHover = -1 return -1, false, errors.New(DoNothing) } } } } func (ui *gameui) KeyMenuAction(n int) (m int, action keyConfigAction) { in := ui.PollEvent() r := ui.KeyToRuneKeyAction(in) switch string(r) { case "a": action = ChangeKeys case "\x1b", " ", "x", "X": action = QuitKeyConfig case "u": n -= DungeonHeight / 2 case "d": n += DungeonHeight / 2 case "j", "2", "ArrowDown": n++ case "k", "8", "ArrowUp": n-- case "R": action = ResetKeys default: if r == 0 && in.mouse { y := in.mouseY x := in.mouseX switch in.button { case 0: if x > DungeonWidth || y > DungeonHeight { action = QuitKeyConfig } case 1: action = QuitKeyConfig } } } return n, action } func (ui *gameui) TargetModeEvent(targ Targeter, data *examineData) (err error, again, quit, notarg bool) { g := ui.g again = true in := ui.PollEvent() switch in.key { case "\x1b", "Escape", " ", "x", "X": g.Targeting = InvalidPos notarg = true case "": if !in.mouse { return } switch in.button { case -1: if in.mouseY == DungeonHeight { m, ok := ui.WhichButton(in.mouseX) omh := ui.menuHover if ok { ui.menuHover = m } else { ui.menuHover = -1 } if ui.menuHover != omh { ui.DrawMenus() ui.Flush() } g.Targeting = InvalidPos notarg = true err = errors.New(DoNothing) break } ui.menuHover = -1 if in.mouseY >= DungeonHeight || in.mouseX >= DungeonWidth { g.Targeting = InvalidPos notarg = true err = errors.New(DoNothing) break } mpos := position{in.mouseX, in.mouseY} if g.Targeting == mpos { break } g.Targeting = InvalidPos fallthrough case 0: if in.mouseY == DungeonHeight { m, ok := ui.WhichButton(in.mouseX) if !ok { g.Targeting = InvalidPos notarg = true err = errors.New(DoNothing) break } err, again, quit, notarg = ui.CursorKeyAction(targ, runeKeyAction{k: m.Key(g)}, data) } else if in.mouseX >= DungeonWidth || in.mouseY >= DungeonHeight { g.Targeting = InvalidPos notarg = true err = errors.New(DoNothing) } else { again, notarg = ui.CursorMouseLeft(targ, position{X: in.mouseX, Y: in.mouseY}, data) } case 2: if in.mouseY >= DungeonHeight || in.mouseX >= DungeonWidth { err, again, quit, notarg = ui.CursorKeyAction(targ, runeKeyAction{k: KeyMenu}, data) } else { err, again, quit, notarg = ui.CursorKeyAction(targ, runeKeyAction{k: KeyDescription}, data) } case 1: err, again, quit, notarg = ui.CursorKeyAction(targ, runeKeyAction{k: KeyExclude}, data) } default: r := ui.KeyToRuneKeyAction(in) if r != 0 { return ui.CursorKeyAction(targ, runeKeyAction{r: r}, data) } again = true } return } func (ui *gameui) ReadRuneKey() rune { for { in := ui.PollEvent() switch in.key { case "\x1b", "Escape", " ", "x", "X": return 0 case "Enter": return '.' } r := ui.ReadKey(in.key) if unicode.IsPrint(r) { return r } } } func (ui *gameui) ReadKey(s string) (r rune) { bs := strings.NewReader(s) r, _, _ = bs.ReadRune() return r } type uiMode int const ( NormalMode uiMode = iota TargetingMode NoFlushMode ) const DoNothing = "Do nothing, then." func (ui *gameui) EnterWizard() { g := ui.g if ui.Wizard() { g.WizardMode() ui.DrawDungeonView(NoFlushMode) } else { g.Print(DoNothing) } } func (ui *gameui) CleanError(err error) error { if err != nil && err.Error() == DoNothing { err = errors.New("") } return err } type keyAction int const ( KeyNothing keyAction = iota KeyW KeyS KeyN KeyE KeyNW KeyNE KeySW KeySE KeyRunW KeyRunS KeyRunN KeyRunE KeyRunNW KeyRunNE KeyRunSW KeyRunSE KeyRest KeyWaitTurn KeyDescend KeyGoToStairs KeyExplore KeyExamine KeyEquip KeyDrink KeyThrow KeyEvoke KeyCharacterInfo KeyLogs KeyDump KeyHelp KeySave KeyQuit KeyWizard KeyWizardInfo KeyPreviousMonster KeyNextMonster KeyNextObject KeyDescription KeyTarget KeyExclude KeyEscape KeyConfigure KeyMenu KeyNextStairs KeyMenuCommandHelp KeyMenuTargetingHelp ) var configurableKeyActions = [...]keyAction{ KeyW, KeyS, KeyN, KeyE, KeyNW, KeyNE, KeySW, KeySE, KeyRunW, KeyRunS, KeyRunN, KeyRunE, KeyRunNW, KeyRunNE, KeyRunSW, KeyRunSE, KeyRest, KeyWaitTurn, KeyDescend, KeyGoToStairs, KeyExplore, KeyExamine, KeyEquip, KeyDrink, KeyThrow, KeyEvoke, KeyCharacterInfo, KeyLogs, KeyDump, KeySave, KeyQuit, KeyPreviousMonster, KeyNextMonster, KeyNextObject, KeyNextStairs, KeyDescription, KeyTarget, KeyExclude} var CustomKeys bool func FixedRuneKey(r rune) bool { switch r { case ' ', '?', '=', '.', '\x1b', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'x', 'X': return true default: return false } } func (k keyAction) NormalModeKey() bool { switch k { case KeyW, KeyS, KeyN, KeyE, KeyNW, KeyNE, KeySW, KeySE, KeyRunW, KeyRunS, KeyRunN, KeyRunE, KeyRunNW, KeyRunNE, KeyRunSW, KeyRunSE, KeyRest, KeyWaitTurn, KeyDescend, KeyGoToStairs, KeyExplore, KeyExamine, KeyEquip, KeyDrink, KeyThrow, KeyEvoke, KeyCharacterInfo, KeyLogs, KeyDump, KeyHelp, KeyMenuCommandHelp, KeyMenuTargetingHelp, KeySave, KeyQuit, KeyConfigure, KeyWizard, KeyWizardInfo: return true default: return false } } func (k keyAction) NormalModeDescription() (text string) { switch k { case KeyW: text = "Move west" case KeyS: text = "Move south" case KeyN: text = "Move north" case KeyE: text = "Move east" case KeyNW: text = "Move north west" case KeyNE: text = "Move north east" case KeySW: text = "Move south west" case KeySE: text = "Move south east" case KeyRunW: text = "Travel west" case KeyRunS: text = "Travel south" case KeyRunN: text = "Travel north" case KeyRunE: text = "Travel east" case KeyRunNW: text = "Travel north west" case KeyRunNE: text = "Travel north east" case KeyRunSW: text = "Travel south west" case KeyRunSE: text = "Travel south east" case KeyRest: text = "Rest (until status free or regen)" case KeyWaitTurn: text = "Wait a turn" case KeyDescend: text = "Descend stairs" case KeyGoToStairs: text = "Go to nearest stairs" case KeyExplore: text = "Autoexplore" case KeyExamine: text = "Examine" case KeyEquip: text = "Equip weapon/armour/..." case KeyDrink: text = "Quaff potion" case KeyThrow: text = "Throw/Fire item" case KeyEvoke: text = "Evoke rod" case KeyCharacterInfo: text = "View Character and Quest Information" case KeyLogs: text = "View previous messages" case KeyDump: text = "Write game statistics to file" case KeySave: text = "Save and Quit" case KeyQuit: text = "Quit without saving" case KeyHelp: text = "Help (keys and mouse)" case KeyMenuCommandHelp: text = "Help (general commands)" case KeyMenuTargetingHelp: text = "Help (targeting commands)" case KeyConfigure: text = "Settings and key bindings" case KeyWizard: text = "Wizard (debug) mode" case KeyWizardInfo: text = "Wizard (debug) mode information" case KeyMenu: text = "Action Menu" } return text } func (k keyAction) TargetingModeDescription() (text string) { switch k { case KeyW: text = "Move cursor west" case KeyS: text = "Move cursor south" case KeyN: text = "Move cursor north" case KeyE: text = "Move cursor east" case KeyNW: text = "Move cursor north west" case KeyNE: text = "Move cursor north east" case KeySW: text = "Move cursor south west" case KeySE: text = "Move cursor south east" case KeyRunW: text = "Big move cursor west" case KeyRunS: text = "Big move cursor south" case KeyRunN: text = "Big move north" case KeyRunE: text = "Big move east" case KeyRunNW: text = "Big move north west" case KeyRunNE: text = "Big move north east" case KeyRunSW: text = "Big move south west" case KeyRunSE: text = "Big move south east" case KeyDescend: text = "Target next stair" case KeyPreviousMonster: text = "Target previous monster" case KeyNextMonster: text = "Target next monster" case KeyNextObject: text = "Target next object" case KeyNextStairs: text = "Target next stairs" case KeyDescription: text = "View target description" case KeyTarget: text = "Go to/select target" case KeyExclude: text = "Toggle exclude area from auto-travel" case KeyEscape: text = "Quit targeting mode" case KeyMenu: text = "Action Menu" } return text } func (k keyAction) TargetingModeKey() bool { switch k { case KeyW, KeyS, KeyN, KeyE, KeyNW, KeyNE, KeySW, KeySE, KeyRunW, KeyRunS, KeyRunN, KeyRunE, KeyRunNW, KeyRunNE, KeyRunSW, KeyRunSE, KeyDescend, KeyPreviousMonster, KeyNextMonster, KeyNextObject, KeyNextStairs, KeyDescription, KeyTarget, KeyExclude, KeyEscape: return true default: return false } } var GameConfig config func ApplyDefaultKeyBindings() { GameConfig.RuneNormalModeKeys = map[rune]keyAction{ 'h': KeyW, 'j': KeyS, 'k': KeyN, 'l': KeyE, 'y': KeyNW, 'u': KeyNE, 'b': KeySW, 'n': KeySE, '4': KeyW, '2': KeyS, '8': KeyN, '6': KeyE, '7': KeyNW, '9': KeyNE, '1': KeySW, '3': KeySE, 'H': KeyRunW, 'J': KeyRunS, 'K': KeyRunN, 'L': KeyRunE, 'Y': KeyRunNW, 'U': KeyRunNE, 'B': KeyRunSW, 'N': KeyRunSE, '.': KeyWaitTurn, '5': KeyWaitTurn, 'r': KeyRest, '>': KeyDescend, 'D': KeyDescend, 'G': KeyGoToStairs, 'o': KeyExplore, 'x': KeyExamine, 'e': KeyEquip, 'g': KeyEquip, ',': KeyEquip, 'q': KeyDrink, 'd': KeyDrink, 't': KeyThrow, 'f': KeyThrow, 'v': KeyEvoke, 'z': KeyEvoke, '%': KeyCharacterInfo, 'C': KeyCharacterInfo, 'm': KeyLogs, '#': KeyDump, '?': KeyHelp, 'S': KeySave, 'Q': KeyQuit, 'W': KeyWizard, '@': KeyWizardInfo, '=': KeyConfigure, } GameConfig.RuneTargetModeKeys = map[rune]keyAction{ 'h': KeyW, 'j': KeyS, 'k': KeyN, 'l': KeyE, 'y': KeyNW, 'u': KeyNE, 'b': KeySW, 'n': KeySE, '4': KeyW, '2': KeyS, '8': KeyN, '6': KeyE, '7': KeyNW, '9': KeyNE, '1': KeySW, '3': KeySE, 'H': KeyRunW, 'J': KeyRunS, 'K': KeyRunN, 'L': KeyRunE, 'Y': KeyRunNW, 'U': KeyRunNE, 'B': KeyRunSW, 'N': KeyRunSE, '>': KeyNextStairs, '-': KeyPreviousMonster, '+': KeyNextMonster, 'o': KeyNextObject, ']': KeyNextObject, ')': KeyNextObject, '(': KeyNextObject, '[': KeyNextObject, '_': KeyNextObject, 'v': KeyDescription, 'd': KeyDescription, '.': KeyTarget, 'z': KeyTarget, 't': KeyTarget, 'f': KeyTarget, 'e': KeyExclude, ' ': KeyEscape, '\x1b': KeyEscape, 'x': KeyEscape, 'X': KeyEscape, '?': KeyHelp, } CustomKeys = false } type runeKeyAction struct { r rune k keyAction } func (ui *gameui) HandleKeyAction(rka runeKeyAction) (err error, again bool, quit bool) { g := ui.g if rka.r != 0 { var ok bool rka.k, ok = GameConfig.RuneNormalModeKeys[rka.r] if !ok { switch rka.r { case 's': err = errors.New("Unknown key. Did you mean capital S for save and quit?") default: err = fmt.Errorf("Unknown key '%c'. Type ? for help.", rka.r) } return err, again, quit } } if rka.k == KeyMenu { rka.k, err = ui.SelectAction(menuActions, g.Ev) if err != nil { err = ui.CleanError(err) return err, again, quit } } return ui.HandleKey(rka) } func (ui *gameui) OptionalDescendConfirmation(st stair) (err error) { g := ui.g if g.Depth == WinDepth && st == NormalStair { g.Print("Do you really want to dive into optional depths? [y/N]") ui.DrawDungeonView(NormalMode) dive := ui.PromptConfirmation() if !dive { err = errors.New("Keep going in the current level, then.") } } return err } func (ui *gameui) HandleKey(rka runeKeyAction) (err error, again bool, quit bool) { g := ui.g switch rka.k { case KeyW, KeyS, KeyN, KeyE, KeyNW, KeyNE, KeySW, KeySE: err = g.MovePlayer(g.Player.Pos.To(KeyToDir(rka.k)), g.Ev) case KeyRunW, KeyRunS, KeyRunN, KeyRunE, KeyRunNW, KeyRunNE, KeyRunSW, KeyRunSE: err = g.GoToDir(KeyToDir(rka.k), g.Ev) case KeyWaitTurn: g.WaitTurn(g.Ev) case KeyRest: err = g.Rest(g.Ev) ui.MenuSelectedAnimation(MenuRest, err == nil) case KeyDescend: if st, ok := g.Stairs[g.Player.Pos]; ok { ui.MenuSelectedAnimation(MenuInteract, true) err = ui.OptionalDescendConfirmation(st) if err != nil { break } if g.Descend() { ui.Win() quit = true return err, again, quit } ui.DrawDungeonView(NormalMode) } else { err = errors.New("No stairs here.") } case KeyGoToStairs: stairs := g.StairsSlice() sortedStairs := g.SortedNearestTo(stairs, g.Player.Pos) if len(sortedStairs) > 0 { stair := sortedStairs[0] if g.Player.Pos == stair { err = errors.New("You are already on the stairs.") break } ex := &examiner{stairs: true} err = ex.Action(g, stair) if err == nil && !g.MoveToTarget(g.Ev) { err = errors.New("You could not move toward stairs.") } if ex.Done() { g.Targeting = InvalidPos } } else { err = errors.New("You cannot go to any stairs.") } case KeyEquip: err = g.Equip(g.Ev) ui.MenuSelectedAnimation(MenuInteract, err == nil) case KeyDrink: err = ui.SelectPotion(g.Ev) err = ui.CleanError(err) case KeyThrow: err = ui.SelectProjectile(g.Ev) err = ui.CleanError(err) case KeyEvoke: err = ui.SelectRod(g.Ev) err = ui.CleanError(err) case KeyExplore: err = g.Autoexplore(g.Ev) ui.MenuSelectedAnimation(MenuExplore, err == nil) case KeyExamine: err, again, quit = ui.Examine(nil) case KeyHelp, KeyMenuCommandHelp: ui.KeysHelp() again = true case KeyMenuTargetingHelp: ui.ExamineHelp() again = true case KeyCharacterInfo: ui.CharacterInfo() again = true case KeyLogs: ui.DrawPreviousLogs() again = true case KeySave: g.Ev.Renew(g, 0) errsave := g.Save() if errsave != nil { g.PrintfStyled("Error: %v", logError, errsave) g.PrintStyled("Could not save game.", logError) } else { quit = true } case KeyDump: errdump := g.WriteDump() if errdump != nil { g.PrintfStyled("Error: %v", logError, errdump) } else { dataDir, _ := g.DataDir() if dataDir != "" { g.Printf("Game statistics written to %s.", filepath.Join(dataDir, "dump")) } else { g.Print("Game statistics written.") } } again = true case KeyWizardInfo: if g.Wizard { err = ui.HandleWizardAction() again = true } else { err = errors.New("Unknown key. Type ? for help.") } case KeyWizard: ui.EnterWizard() return nil, true, false case KeyQuit: if ui.Quit() { return nil, false, true } return nil, true, false case KeyConfigure: err = ui.HandleSettingAction() again = true case KeyDescription: //ui.MenuSelectedAnimation(MenuView, false) err = fmt.Errorf("You must choose a target to describe.") case KeyExclude: err = fmt.Errorf("You must choose a target for exclusion.") default: err = fmt.Errorf("Unknown key '%c'. Type ? for help.", rka.r) } if err != nil { again = true } return err, again, quit } func (ui *gameui) ExaminePos(ev event, pos position) (err error, again, quit bool) { var start *position if pos.valid() { start = &pos } err, again, quit = ui.Examine(start) return err, again, quit } func (ui *gameui) Examine(start *position) (err error, again, quit bool) { ex := &examiner{} err, again, quit = ui.CursorAction(ex, start) return err, again, quit } func (ui *gameui) ChooseTarget(targ Targeter) error { err, _, _ := ui.CursorAction(targ, nil) if err != nil { return err } if !targ.Done() { return errors.New(DoNothing) } return nil } func (ui *gameui) NextMonster(r rune, pos position, data *examineData) { g := ui.g nmonster := data.nmonster for i := 0; i < len(g.Monsters); i++ { if r == '+' { nmonster++ } else { nmonster-- } if nmonster > len(g.Monsters)-1 { nmonster = 0 } else if nmonster < 0 { nmonster = len(g.Monsters) - 1 } mons := g.Monsters[nmonster] if mons.Exists() && g.Player.LOS[mons.Pos] && pos != mons.Pos { pos = mons.Pos break } } data.npos = pos data.nmonster = nmonster } func (ui *gameui) NextStair(data *examineData) { g := ui.g if data.sortedStairs == nil { stairs := g.StairsSlice() data.sortedStairs = g.SortedNearestTo(stairs, g.Player.Pos) } if data.stairIndex >= len(data.sortedStairs) { data.stairIndex = 0 } if len(data.sortedStairs) > 0 { data.npos = data.sortedStairs[data.stairIndex] data.stairIndex++ } } func (ui *gameui) NextObject(pos position, data *examineData) { g := ui.g nobject := data.nobject if len(data.objects) == 0 { for p := range g.Collectables { data.objects = append(data.objects, p) } for p := range g.Rods { data.objects = append(data.objects, p) } for p := range g.Equipables { data.objects = append(data.objects, p) } for p := range g.Simellas { data.objects = append(data.objects, p) } for p := range g.MagicalStones { data.objects = append(data.objects, p) } data.objects = g.SortedNearestTo(data.objects, g.Player.Pos) } for i := 0; i < len(data.objects); i++ { p := data.objects[nobject] nobject++ if nobject > len(data.objects)-1 { nobject = 0 } if g.Dungeon.Cell(p).Explored { pos = p break } } data.npos = pos data.nobject = nobject } func (ui *gameui) ExcludeZone(pos position) { g := ui.g if !g.Dungeon.Cell(pos).Explored { g.Print("You cannot choose an unexplored cell for exclusion.") } else { toggle := !g.ExclusionsMap[pos] g.ComputeExclusion(pos, toggle) } } func (ui *gameui) CursorMouseLeft(targ Targeter, pos position, data *examineData) (again, notarg bool) { g := ui.g again = true if data.npos == pos { err := targ.Action(g, pos) if err != nil { g.Print(err.Error()) } else { if g.MoveToTarget(g.Ev) { again = false } if targ.Done() { notarg = true } } } else { data.npos = pos } return again, notarg } func (ui *gameui) CursorKeyAction(targ Targeter, rka runeKeyAction, data *examineData) (err error, again, quit, notarg bool) { g := ui.g pos := data.npos again = true if rka.r != 0 { var ok bool rka.k, ok = GameConfig.RuneTargetModeKeys[rka.r] if !ok { err = fmt.Errorf("Invalid targeting mode key '%c'. Type ? for help.", rka.r) return err, again, quit, notarg } } if rka.k == KeyMenu { rka.k, err = ui.SelectAction(menuActions, g.Ev) if err != nil { err = ui.CleanError(err) return err, again, quit, notarg } } switch rka.k { case KeyW, KeyS, KeyN, KeyE, KeyNW, KeyNE, KeySW, KeySE: data.npos = pos.To(KeyToDir(rka.k)) case KeyRunW, KeyRunS, KeyRunN, KeyRunE, KeyRunNW, KeyRunNE, KeyRunSW, KeyRunSE: for i := 0; i < 5; i++ { p := data.npos.To(KeyToDir(rka.k)) if !p.valid() { break } data.npos = p } case KeyNextStairs: ui.NextStair(data) case KeyDescend: if strt, ok := g.Stairs[g.Player.Pos]; ok { ui.MenuSelectedAnimation(MenuInteract, true) err = ui.OptionalDescendConfirmation(strt) if err != nil { break } again = false g.Targeting = InvalidPos notarg = true if g.Descend() { ui.Win() quit = true return err, again, quit, notarg } } else { err = errors.New("No stairs here.") } case KeyPreviousMonster, KeyNextMonster: ui.NextMonster(rka.r, pos, data) case KeyNextObject: ui.NextObject(pos, data) case KeyHelp, KeyMenuTargetingHelp: ui.HideCursor() ui.ExamineHelp() ui.SetCursor(pos) case KeyMenuCommandHelp: ui.HideCursor() ui.KeysHelp() ui.SetCursor(pos) case KeyTarget: err = targ.Action(g, pos) if err != nil { break } g.Targeting = InvalidPos if g.MoveToTarget(g.Ev) { again = false } if targ.Done() { notarg = true } case KeyDescription: ui.HideCursor() ui.ViewPositionDescription(pos) ui.SetCursor(pos) case KeyExclude: ui.ExcludeZone(pos) case KeyEscape: g.Targeting = InvalidPos notarg = true err = errors.New(DoNothing) case KeyExplore, KeyRest, KeyThrow, KeyDrink, KeyEvoke, KeyLogs, KeyEquip, KeyCharacterInfo: if _, ok := targ.(*examiner); !ok { break } err, again, quit = ui.HandleKey(rka) if err != nil { notarg = true } g.Targeting = InvalidPos case KeyConfigure: err = ui.HandleSettingAction() case KeySave: g.Ev.Renew(g, 0) g.Highlight = nil g.Targeting = InvalidPos errsave := g.Save() if errsave != nil { g.PrintfStyled("Error: %v", logError, errsave) g.PrintStyled("Could not save game.", logError) } else { notarg = true again = false quit = true } case KeyQuit: if ui.Quit() { quit = true again = false } default: err = fmt.Errorf("Invalid targeting mode key '%c'. Type ? for help.", rka.r) } return err, again, quit, notarg } type examineData struct { npos position nmonster int objects []position nobject int sortedStairs []position stairIndex int } var InvalidPos = position{-1, -1} func (ui *gameui) CursorAction(targ Targeter, start *position) (err error, again, quit bool) { g := ui.g pos := g.Player.Pos if start != nil { pos = *start } else { minDist := 999 for _, mons := range g.Monsters { if mons.Exists() && g.Player.LOS[mons.Pos] { dist := mons.Pos.Distance(g.Player.Pos) if minDist > dist { minDist = dist pos = mons.Pos } } } } data := &examineData{ npos: pos, objects: []position{}, } if _, ok := targ.(*examiner); ok && pos == g.Player.Pos && start == nil { ui.NextObject(InvalidPos, data) if !data.npos.valid() { ui.NextStair(data) } if data.npos.valid() { pos = data.npos } } opos := InvalidPos loop: for { err = nil if pos != opos { ui.DescribePosition(pos, targ) } opos = pos targ.ComputeHighlight(g, pos) ui.SetCursor(pos) ui.DrawDungeonView(TargetingMode) ui.DrawInfoLine(g.InfoEntry) if !ui.Small() { st := " Examine/Travel (? for help) " if _, ok := targ.(*examiner); !ok { st = " Targeting (? for help) " } ui.DrawStyledTextLine(st, DungeonHeight+2, FooterLine) } ui.SetCell(DungeonWidth, DungeonHeight, '┤', ColorFg, ColorBg) ui.Flush() data.npos = pos var notarg bool err, again, quit, notarg = ui.TargetModeEvent(targ, data) if err != nil { err = ui.CleanError(err) } if !again || notarg { break loop } if err != nil { g.Print(err.Error()) } if data.npos.valid() { pos = data.npos } } g.Highlight = nil ui.HideCursor() return err, again, quit } type menu int const ( MenuRest menu = iota MenuExplore MenuThrow MenuDrink MenuEvoke MenuOther MenuInteract ) func (m menu) String() (text string) { switch m { case MenuRest: text = "rest" case MenuExplore: text = "explore" case MenuThrow: text = "throw" case MenuDrink: text = "drink" case MenuEvoke: text = "evoke" case MenuOther: text = "menu" case MenuInteract: text = "interact" } return "[" + text + "]" } func (m menu) Key(g *game) (key keyAction) { switch m { case MenuRest: key = KeyRest case MenuExplore: key = KeyExplore case MenuThrow: key = KeyThrow case MenuDrink: key = KeyDrink case MenuEvoke: key = KeyEvoke case MenuOther: key = KeyMenu case MenuInteract: if _, ok := g.Equipables[g.Player.Pos]; ok { key = KeyEquip } else if _, ok := g.Stairs[g.Player.Pos]; ok { key = KeyDescend } } return key } var MenuCols = [][2]int{ MenuRest: {0, 0}, MenuExplore: {0, 0}, MenuThrow: {0, 0}, MenuDrink: {0, 0}, MenuEvoke: {0, 0}, MenuOther: {0, 0}, MenuInteract: {0, 0}} func init() { for i := range MenuCols { runes := utf8.RuneCountInString(menu(i).String()) if i == 0 { MenuCols[0] = [2]int{7, 7 + runes} continue } MenuCols[i] = [2]int{MenuCols[i-1][1] + 2, MenuCols[i-1][1] + 2 + runes} } } func (ui *gameui) WhichButton(col int) (menu, bool) { g := ui.g if ui.Small() { return MenuOther, false } end := len(MenuCols) - 1 if _, ok := g.Equipables[g.Player.Pos]; ok { end++ } else if _, ok := g.Stairs[g.Player.Pos]; ok { end++ } for i, cols := range MenuCols[0:end] { if cols[0] >= 0 && col >= cols[0] && col < cols[1] { return menu(i), true } } return MenuOther, false } func (ui *gameui) UpdateInteractButton() string { g := ui.g var interactMenu string var show bool if _, ok := g.Equipables[g.Player.Pos]; ok { interactMenu = "[equip]" show = true } else if _, ok := g.Stairs[g.Player.Pos]; ok { interactMenu = "[descend]" show = true } if !show { return "" } i := len(MenuCols) - 1 runes := utf8.RuneCountInString(interactMenu) MenuCols[i][1] = MenuCols[i][0] + runes return interactMenu } type wizardAction int const ( WizardInfoAction wizardAction = iota WizardToggleMap ) func (a wizardAction) String() (text string) { switch a { case WizardInfoAction: text = "Info" case WizardToggleMap: text = "toggle see/hide monsters" } return text } var wizardActions = []wizardAction{ WizardInfoAction, WizardToggleMap, } func (ui *gameui) HandleWizardAction() error { g := ui.g s, err := ui.SelectWizardMagic(wizardActions) if err != nil { return err } switch s { case WizardInfoAction: ui.WizardInfo() case WizardToggleMap: g.WizardMap = !g.WizardMap ui.DrawDungeonView(NoFlushMode) } return nil } func (ui *gameui) Death() { g := ui.g g.Print("You die... [(x) to continue]") ui.DrawDungeonView(NormalMode) ui.WaitForContinue(-1) err := g.WriteDump() ui.Dump(err) ui.WaitForContinue(-1) } func (ui *gameui) Win() { g := ui.g err := g.RemoveSaveFile() if err != nil { g.PrintfStyled("Error removing save file: %v", logError, err) } if g.Wizard { g.Print("You escape by the magic portal! **WIZARD** [(x) to continue]") } else { g.Print("You escape by the magic portal! You win. [(x) to continue]") } ui.DrawDungeonView(NormalMode) ui.WaitForContinue(-1) err = g.WriteDump() ui.Dump(err) ui.WaitForContinue(-1) } func (ui *gameui) Dump(err error) { g := ui.g ui.Clear() ui.DrawText(g.SimplifedDump(err), 0, 0) ui.Flush() } func (ui *gameui) CriticalHPWarning() { g := ui.g g.PrintStyled("*** CRITICAL HP WARNING *** [(x) to continue]", logCritic) ui.DrawDungeonView(NormalMode) ui.WaitForContinue(DungeonHeight) g.Print("Ok. Be careful, then.") } func (ui *gameui) Quit() bool { g := ui.g g.Print("Do you really want to quit without saving? [y/N]") ui.DrawDungeonView(NormalMode) quit := ui.PromptConfirmation() if quit { err := g.RemoveSaveFile() if err != nil { g.PrintfStyled("Error removing save file: %v [press any key to quit]", logError, err) ui.DrawDungeonView(NormalMode) ui.PressAnyKey() } } else { g.Print(DoNothing) } return quit } func (ui *gameui) Wizard() bool { g := ui.g g.Print("Do you really want to enter wizard mode (no return)? [y/N]") ui.DrawDungeonView(NormalMode) return ui.PromptConfirmation() } func (ui *gameui) HandlePlayerTurn(ev event) bool { g := ui.g getKey: for { var err error var again, quit bool if g.Targeting.valid() { err, again, quit = ui.ExaminePos(ev, g.Targeting) } else { ui.DrawDungeonView(NormalMode) err, again, quit = ui.PlayerTurnEvent(ev) } if err != nil && err.Error() != "" { g.Print(err.Error()) } if again { continue getKey } return quit } } func (ui *gameui) ExploreStep() bool { next := make(chan bool) var stop bool go func() { time.Sleep(10 * time.Millisecond) ui.Interrupt() }() go func() { err := ui.PressAnyKey() interrupted := err != nil next <- !interrupted }() stop = <-next ui.DrawDungeonView(NormalMode) return stop } func (ui *gameui) Clear() { for i := 0; i < UIHeight*UIWidth; i++ { x, y := ui.GetPos(i) ui.SetCell(x, y, ' ', ColorFg, ColorBg) } } func (ui *gameui) DrawBufferInit() { if len(ui.g.DrawBuffer) == 0 { ui.g.DrawBuffer = make([]UICell, UIHeight*UIWidth) } else if len(ui.g.DrawBuffer) != UIHeight*UIWidth { ui.g.DrawBuffer = make([]UICell, UIHeight*UIWidth) } } func ApplyConfig() { if GameConfig.RuneNormalModeKeys == nil || GameConfig.RuneTargetModeKeys == nil { ApplyDefaultKeyBindings() } if GameConfig.DarkLOS { ApplyDarkLOS() } else { ApplyLightLOS() } } boohu-0.13.0/utils.go000066400000000000000000000027561356500202200144030ustar00rootroot00000000000000package main import ( "bytes" "math/rand" "strings" "time" ) func Abs(x int) int { if x < 0 { return -x } return x } func init() { rand.Seed(time.Now().UnixNano()) } func RandInt(n int) int { if n <= 0 { return 0 } x := rand.Intn(n) return x } func Min(x, y int) int { if x < y { return x } return y } func Max(x, y int) int { if x > y { return x } return y } func Indefinite(s string, upper bool) (text string) { if len(s) > 0 { if s == HarKarGauntlets.String() { if upper { return strings.ToUpper(s[0:1]) + s[1:] } return s } switch s[0] { case 'a', 'e', 'i', 'o', 'u': if upper { text = "An " + s } else { text = "an " + s } default: if upper { text = "A " + s } else { text = "a " + s } } } return text } func formatText(text string, width int) string { pbuf := bytes.Buffer{} wordbuf := bytes.Buffer{} col := 0 wantspace := false wlen := 0 for _, c := range text { if c == ' ' { if wlen == 0 { continue } if col+wlen > width { if wantspace { pbuf.WriteRune('\n') col = 0 } } else if wantspace { pbuf.WriteRune(' ') col++ } pbuf.Write(wordbuf.Bytes()) col += wlen wordbuf.Reset() wlen = 0 wantspace = true continue } wordbuf.WriteRune(c) wlen++ } if wordbuf.Len() > 0 { if wantspace { if wlen+col > width { pbuf.WriteRune('\n') } else { pbuf.WriteRune(' ') } } pbuf.Write(wordbuf.Bytes()) } return pbuf.String() }