├── .gitignore ├── README.md ├── art ├── characters_1.json ├── characters_1.png ├── circle_bg.png ├── dungeoncarpet.json ├── dungeoncarpet.png ├── particle.png ├── roguelikecreatures.png ├── roguelikeitems.json ├── roguelikeitems.png ├── rogueliketiles.json ├── rogueliketiles.png ├── walls.json └── walls.png ├── game.html ├── gulpfile.js ├── package.json ├── src ├── Actors │ ├── Actor.ts │ ├── Character │ │ ├── Chaser.ts │ │ ├── Enemies │ │ │ ├── Ghost.ts │ │ │ ├── GreenBlob.ts │ │ │ └── Skeleton.ts │ │ ├── Player.ts │ │ └── RunStats.ts │ ├── EmitsLight.ts │ └── Environment │ │ ├── Bookshelf.ts │ │ ├── Carpet.ts │ │ ├── Door.ts │ │ ├── Floor.ts │ │ ├── Graves.ts │ │ ├── Pillar.ts │ │ ├── Special │ │ ├── OutOfBounds.ts │ │ ├── StairsDown.ts │ │ └── StairsUp.ts │ │ ├── Torch.ts │ │ └── Wall.ts ├── Behaviour │ ├── Action.ts │ ├── Actions │ │ ├── AttackFacingTile.ts │ │ ├── AttackFirstInLine.ts │ │ ├── Move.ts │ │ └── RadialAttack.ts │ ├── Command.ts │ └── Commands │ │ ├── DirectAttack.ts │ │ ├── MoveTo.ts │ │ └── ProjectileAttack.ts ├── Buff │ ├── Base │ │ └── Buff.ts │ └── Buffs │ │ ├── InvisibilityBuff.ts │ │ ├── PetrifiedDebuff.ts │ │ └── WallBreakerBuff.ts ├── Enums.ts ├── Game.ts ├── GameSettingsProvider.ts ├── Helpers │ ├── Battle.ts │ ├── BuffHelpers.ts │ ├── Color.ts │ ├── Extensions.ts │ ├── Falloff.ts │ ├── Generation │ │ ├── GenerationHelpers.ts │ │ ├── WorldDecorator.ts │ │ ├── WorldDecoratorHelpers.ts │ │ └── WorldGenerator.ts │ ├── Generic.ts │ ├── Geometry.ts │ ├── Movement.ts │ ├── Numbers.ts │ ├── Random.ts │ ├── Rendering.ts │ └── XP.ts ├── Inventory │ ├── Base │ │ └── InventoryItem.ts │ ├── Consumables │ │ └── Potion.ts │ └── Equippables │ │ ├── Ammo │ │ └── InventoryArrow.ts │ │ ├── Armor.ts │ │ └── Weapon.ts ├── Layer.ts ├── Menu │ ├── Menu.ts │ ├── Menus │ │ ├── InventoryMenu.ts │ │ └── MainMenu.ts │ └── SelectableActorGroup.ts ├── Point.ts ├── Renderers │ ├── Particles │ │ ├── NumberSmoke.ts │ │ └── ParticleEmitters.ts │ └── PixiRenderer.ts ├── Room.ts ├── Sprites │ ├── Sprite.ts │ ├── SpriteSet.ts │ └── Sprites.ts ├── UI │ └── LogMessage.ts ├── World.ts └── WorldItems │ ├── Base │ └── WorldItem.ts │ ├── Chest.ts │ ├── DroppedArrow.ts │ └── GoldPile.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore tag files generated by the IDE for hinting and linting javascript 2 | *.tags 3 | *.tags1 4 | 5 | # Ignore any npm modules 6 | node_modules 7 | 8 | # Ignore any server software locally installed 9 | *.app 10 | *.exe 11 | 12 | # Ignore anything built to the build directory 13 | build/ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roguelike # 2 | ![preview](http://i.imgur.com/l29YGJY.png) 3 | Play at http://www.torrobinson.com/roguelike/build/game.html 4 | 5 | This is an experiment to familiarize myself with game mechanics, TypeScript, and Pixi.js 6 | 7 | It is a procedurally generated rogulike-esque game with armor, a gui, monsters, and fake "lighting". 8 | 9 | The game "engine" is written entirely from scratch, with the exception of a pathfinder (for resolving paths) and Pixi for drawing images and shapes to the screen. 10 | 11 | ### To Play: ### 12 | - Install Node.js 13 | - This should include NPM 14 | - In your project directory root, run `npm install` to install required packages 15 | - Run `gulp` to build 16 | - Run the built`./build/game.html` 17 | 18 | ### Instructions ### 19 | - `Up`, `Down`, `Left`, `Right` keys move 20 | - `I` opens the inventory 21 | - `Esc` pauses the game 22 | - Moving into an enemy performs an attack 23 | - With a ranged weapon equipped, use `[` and `]` to cycle through enemies and press `|` to fire the ranged weapon 24 | - Missed arrow shots can be picked up again 25 | - Clicking the "Random Dungeon" button generate a random dungeon for debugging purposes 26 | 27 | ## Attribution ## 28 | 29 | ### Art provided by ### 30 | - https://opengameart.org/content/roguelike-monsters 31 | - Joe Williamson 32 | - https://opengameart.org/content/roguelike-dungeonworld-tiles 33 | - https://opengameart.org/content/roguelikerpg-items 34 | - DiegoJP 35 | - https://opengameart.org/content/castledungeon-tileset 36 | 37 | ### Tools ### 38 | - [Pixi.js](http://pixijs.com) 39 | - [Pathfinding](https://www.npmjs.com/package/pathfinding) by [imor](https://github.com/imor) 40 | 41 | ### Lessons Learned ### 42 | - Start off using TypeScript, don't try to convert later (ugh) 43 | - Abstract everything 44 | - Separate gameclock from frameclock - game should be playable blind without a renderer attached 45 | - Finding sprite assets and mapping them to actors is hard 46 | -------------------------------------------------------------------------------- /art/characters_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/characters_1.png -------------------------------------------------------------------------------- /art/circle_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/circle_bg.png -------------------------------------------------------------------------------- /art/dungeoncarpet.json: -------------------------------------------------------------------------------- 1 | {"frames":{"sprite1":{"frame":{"x":0,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite2":{"frame":{"x":16,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite3":{"frame":{"x":32,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite4":{"frame":{"x":48,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite5":{"frame":{"x":64,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite6":{"frame":{"x":0,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetDownRight":{"frame":{"x":16,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetDownLeftRight":{"frame":{"x":32,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetDownLeft":{"frame":{"x":48,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite10":{"frame":{"x":64,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite11":{"frame":{"x":0,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpDownRight":{"frame":{"x":16,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpDownLeftRight":{"frame":{"x":32,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpDownLeft":{"frame":{"x":48,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite15":{"frame":{"x":64,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite16":{"frame":{"x":0,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpRight":{"frame":{"x":16,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpLeftRight":{"frame":{"x":32,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpLeft":{"frame":{"x":48,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite20":{"frame":{"x":64,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite21":{"frame":{"x":0,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite22":{"frame":{"x":16,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite23":{"frame":{"x":32,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite24":{"frame":{"x":48,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite25":{"frame":{"x":64,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite26":{"frame":{"x":0,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite27":{"frame":{"x":16,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite28":{"frame":{"x":32,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite29":{"frame":{"x":48,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite30":{"frame":{"x":0,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite31":{"frame":{"x":16,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite32":{"frame":{"x":32,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite33":{"frame":{"x":48,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite34":{"frame":{"x":0,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite35":{"frame":{"x":16,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetDown":{"frame":{"x":32,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUp":{"frame":{"x":48,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite38":{"frame":{"x":64,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite39":{"frame":{"x":0,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite40":{"frame":{"x":16,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetRight":{"frame":{"x":32,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetLeft":{"frame":{"x":48,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite43":{"frame":{"x":64,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetUpDown":{"frame":{"x":0,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"CarpetLeftRight":{"frame":{"x":16,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite46":{"frame":{"x":32,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite47":{"frame":{"x":64,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite48":{"frame":{"x":0,"y":160,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite49":{"frame":{"x":16,"y":160,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite50":{"frame":{"x":0,"y":176,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite51":{"frame":{"x":16,"y":176,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite52":{"frame":{"x":32,"y":176,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}}},"meta":{"app":"https://www.leshylabs.com/apps/sstool/","version":"Leshy SpriteSheet Tool v0.8.4","image":"dungeoncarpet.png","size":{"w":80,"h":192},"scale":1}} -------------------------------------------------------------------------------- /art/dungeoncarpet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/dungeoncarpet.png -------------------------------------------------------------------------------- /art/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/particle.png -------------------------------------------------------------------------------- /art/roguelikecreatures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/roguelikecreatures.png -------------------------------------------------------------------------------- /art/roguelikeitems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/roguelikeitems.png -------------------------------------------------------------------------------- /art/rogueliketiles.json: -------------------------------------------------------------------------------- 1 | {"frames":{"TreeDown":{"frame":{"x":0,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"GrassDown":{"frame":{"x":16,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"DoorDownClosed":{"frame":{"x":32,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"DoorDownOpen":{"frame":{"x":48,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"LadderBottomDown":{"frame":{"x":64,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ChairRight":{"frame":{"x":80,"y":0,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"DirtDown":{"frame":{"x":0,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallLightDown":{"frame":{"x":16,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallMediumDown":{"frame":{"x":32,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallDarkDown":{"frame":{"x":48,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"LadderTopDown":{"frame":{"x":64,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ChairLeft":{"frame":{"x":80,"y":16,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"StoneDown":{"frame":{"x":0,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"BricksDown":{"frame":{"x":16,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"Cobblestone1Down":{"frame":{"x":32,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"Cobblestone2Down":{"frame":{"x":48,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"TorchDown":{"frame":{"x":64,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ChairDown":{"frame":{"x":80,"y":32,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ChestClosedDown":{"frame":{"x":0,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ChestOpenDown":{"frame":{"x":16,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"Cobblestone3Down":{"frame":{"x":32,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"Cobblestone4Down":{"frame":{"x":48,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"LavalDown":{"frame":{"x":64,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"BedDown":{"frame":{"x":80,"y":48,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallLightHalfDown":{"frame":{"x":0,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallMediumHalfDown":{"frame":{"x":16,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WallDarkHalfDown":{"frame":{"x":32,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"SpikeDown":{"frame":{"x":48,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"HoleDown":{"frame":{"x":64,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"TableDown":{"frame":{"x":80,"y":64,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite31":{"frame":{"x":0,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite32":{"frame":{"x":16,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite33":{"frame":{"x":32,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite34":{"frame":{"x":48,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"RockDOwn":{"frame":{"x":64,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ShelvesDown":{"frame":{"x":80,"y":80,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite37":{"frame":{"x":0,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite38":{"frame":{"x":16,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite39":{"frame":{"x":32,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite40":{"frame":{"x":48,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"Bones":{"frame":{"x":64,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"ShelvesOpenDown":{"frame":{"x":80,"y":96,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WellDown":{"frame":{"x":0,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"PillarDown":{"frame":{"x":32,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"TombstoneDown":{"frame":{"x":48,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"GraveCrossDown":{"frame":{"x":64,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"BookshelfDown":{"frame":{"x":80,"y":112,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite48":{"frame":{"x":0,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite49":{"frame":{"x":16,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"SpecialChestDown":{"frame":{"x":32,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"KitchenTableDown":{"frame":{"x":48,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"FurnaceDown":{"frame":{"x":64,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"AlchemyTableDown":{"frame":{"x":80,"y":128,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite54":{"frame":{"x":0,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite55":{"frame":{"x":16,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"SpecialChestOpenedDown":{"frame":{"x":32,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"sprite57":{"frame":{"x":48,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"AnvilDown":{"frame":{"x":64,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}},"WorkbenchDown":{"frame":{"x":80,"y":144,"w":16,"h":16},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":16,"h":16},"sourceSize":{"w":16,"h":16}}},"meta":{"app":"https://www.leshylabs.com/apps/sstool/","version":"Leshy SpriteSheet Tool v0.8.4","image":"rogueliketiles.png","size":{"w":96,"h":160},"scale":1}} -------------------------------------------------------------------------------- /art/rogueliketiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/rogueliketiles.png -------------------------------------------------------------------------------- /art/walls.json: -------------------------------------------------------------------------------- 1 | { 2 | "frames":{ 3 | "WallUp":{ 4 | "frame":{ 5 | "x":0, 6 | "y":0, 7 | "w":16, 8 | "h":16 9 | }, 10 | "rotated":false, 11 | "trimmed":false, 12 | "spriteSourceSize":{ 13 | "x":0, 14 | "y":0, 15 | "w":16, 16 | "h":16 17 | }, 18 | "sourceSize":{ 19 | "w":16, 20 | "h":16 21 | } 22 | }, 23 | "WallDown":{ 24 | "frame":{ 25 | "x":16, 26 | "y":0, 27 | "w":16, 28 | "h":16 29 | }, 30 | "rotated":false, 31 | "trimmed":false, 32 | "spriteSourceSize":{ 33 | "x":0, 34 | "y":0, 35 | "w":16, 36 | "h":16 37 | }, 38 | "sourceSize":{ 39 | "w":16, 40 | "h":16 41 | } 42 | }, 43 | "WallLeft":{ 44 | "frame":{ 45 | "x":32, 46 | "y":0, 47 | "w":16, 48 | "h":16 49 | }, 50 | "rotated":false, 51 | "trimmed":false, 52 | "spriteSourceSize":{ 53 | "x":0, 54 | "y":0, 55 | "w":16, 56 | "h":16 57 | }, 58 | "sourceSize":{ 59 | "w":16, 60 | "h":16 61 | } 62 | }, 63 | "WallRight":{ 64 | "frame":{ 65 | "x":48, 66 | "y":0, 67 | "w":16, 68 | "h":16 69 | }, 70 | "rotated":false, 71 | "trimmed":false, 72 | "spriteSourceSize":{ 73 | "x":0, 74 | "y":0, 75 | "w":16, 76 | "h":16 77 | }, 78 | "sourceSize":{ 79 | "w":16, 80 | "h":16 81 | } 82 | }, 83 | "WallLeftRight":{ 84 | "frame":{ 85 | "x":0, 86 | "y":16, 87 | "w":16, 88 | "h":16 89 | }, 90 | "rotated":false, 91 | "trimmed":false, 92 | "spriteSourceSize":{ 93 | "x":0, 94 | "y":0, 95 | "w":16, 96 | "h":16 97 | }, 98 | "sourceSize":{ 99 | "w":16, 100 | "h":16 101 | } 102 | }, 103 | "WallUpDown":{ 104 | "frame":{ 105 | "x":16, 106 | "y":16, 107 | "w":16, 108 | "h":16 109 | }, 110 | "rotated":false, 111 | "trimmed":false, 112 | "spriteSourceSize":{ 113 | "x":0, 114 | "y":0, 115 | "w":16, 116 | "h":16 117 | }, 118 | "sourceSize":{ 119 | "w":16, 120 | "h":16 121 | } 122 | }, 123 | "WallUpLeftRight":{ 124 | "frame":{ 125 | "x":32, 126 | "y":16, 127 | "w":16, 128 | "h":16 129 | }, 130 | "rotated":false, 131 | "trimmed":false, 132 | "spriteSourceSize":{ 133 | "x":0, 134 | "y":0, 135 | "w":16, 136 | "h":16 137 | }, 138 | "sourceSize":{ 139 | "w":16, 140 | "h":16 141 | } 142 | }, 143 | "WallDownLeftRight":{ 144 | "frame":{ 145 | "x":48, 146 | "y":16, 147 | "w":16, 148 | "h":16 149 | }, 150 | "rotated":false, 151 | "trimmed":false, 152 | "spriteSourceSize":{ 153 | "x":0, 154 | "y":0, 155 | "w":16, 156 | "h":16 157 | }, 158 | "sourceSize":{ 159 | "w":16, 160 | "h":16 161 | } 162 | }, 163 | "WallUpDownLeft":{ 164 | "frame":{ 165 | "x":0, 166 | "y":32, 167 | "w":16, 168 | "h":16 169 | }, 170 | "rotated":false, 171 | "trimmed":false, 172 | "spriteSourceSize":{ 173 | "x":0, 174 | "y":0, 175 | "w":16, 176 | "h":16 177 | }, 178 | "sourceSize":{ 179 | "w":16, 180 | "h":16 181 | } 182 | }, 183 | "WallUpDownRight":{ 184 | "frame":{ 185 | "x":16, 186 | "y":32, 187 | "w":16, 188 | "h":16 189 | }, 190 | "rotated":false, 191 | "trimmed":false, 192 | "spriteSourceSize":{ 193 | "x":0, 194 | "y":0, 195 | "w":16, 196 | "h":16 197 | }, 198 | "sourceSize":{ 199 | "w":16, 200 | "h":16 201 | } 202 | }, 203 | "WallUpDownLeftRight":{ 204 | "frame":{ 205 | "x":32, 206 | "y":32, 207 | "w":16, 208 | "h":16 209 | }, 210 | "rotated":false, 211 | "trimmed":false, 212 | "spriteSourceSize":{ 213 | "x":0, 214 | "y":0, 215 | "w":16, 216 | "h":16 217 | }, 218 | "sourceSize":{ 219 | "w":16, 220 | "h":16 221 | } 222 | }, 223 | "WallUpLeft":{ 224 | "frame":{ 225 | "x":48, 226 | "y":32, 227 | "w":16, 228 | "h":16 229 | }, 230 | "rotated":false, 231 | "trimmed":false, 232 | "spriteSourceSize":{ 233 | "x":0, 234 | "y":0, 235 | "w":16, 236 | "h":16 237 | }, 238 | "sourceSize":{ 239 | "w":16, 240 | "h":16 241 | } 242 | }, 243 | "WallUpRight":{ 244 | "frame":{ 245 | "x":0, 246 | "y":48, 247 | "w":16, 248 | "h":16 249 | }, 250 | "rotated":false, 251 | "trimmed":false, 252 | "spriteSourceSize":{ 253 | "x":0, 254 | "y":0, 255 | "w":16, 256 | "h":16 257 | }, 258 | "sourceSize":{ 259 | "w":16, 260 | "h":16 261 | } 262 | }, 263 | "WallDownLeft":{ 264 | "frame":{ 265 | "x":16, 266 | "y":48, 267 | "w":16, 268 | "h":16 269 | }, 270 | "rotated":false, 271 | "trimmed":false, 272 | "spriteSourceSize":{ 273 | "x":0, 274 | "y":0, 275 | "w":16, 276 | "h":16 277 | }, 278 | "sourceSize":{ 279 | "w":16, 280 | "h":16 281 | } 282 | }, 283 | "WallDownRight":{ 284 | "frame":{ 285 | "x":32, 286 | "y":48, 287 | "w":16, 288 | "h":16 289 | }, 290 | "rotated":false, 291 | "trimmed":false, 292 | "spriteSourceSize":{ 293 | "x":0, 294 | "y":0, 295 | "w":16, 296 | "h":16 297 | }, 298 | "sourceSize":{ 299 | "w":16, 300 | "h":16 301 | } 302 | }, 303 | "WallNone":{ 304 | "frame":{ 305 | "x":48, 306 | "y":48, 307 | "w":16, 308 | "h":16 309 | }, 310 | "rotated":false, 311 | "trimmed":false, 312 | "spriteSourceSize":{ 313 | "x":0, 314 | "y":0, 315 | "w":16, 316 | "h":16 317 | }, 318 | "sourceSize":{ 319 | "w":16, 320 | "h":16 321 | } 322 | }, 323 | "DoorDownClosed":{ 324 | "frame":{ 325 | "x":0, 326 | "y":64, 327 | "w":16, 328 | "h":16 329 | }, 330 | "rotated":false, 331 | "trimmed":false, 332 | "spriteSourceSize":{ 333 | "x":0, 334 | "y":0, 335 | "w":16, 336 | "h":16 337 | }, 338 | "sourceSize":{ 339 | "w":16, 340 | "h":16 341 | } 342 | }, 343 | "DoorDownOpen":{ 344 | "frame":{ 345 | "x":16, 346 | "y":64, 347 | "w":16, 348 | "h":16 349 | }, 350 | "rotated":false, 351 | "trimmed":false, 352 | "spriteSourceSize":{ 353 | "x":0, 354 | "y":0, 355 | "w":16, 356 | "h":16 357 | }, 358 | "sourceSize":{ 359 | "w":16, 360 | "h":16 361 | } 362 | }, 363 | "DoorLeftClosed":{ 364 | "frame":{ 365 | "x":64, 366 | "y":32, 367 | "w":32, 368 | "h":32 369 | }, 370 | "rotated":false, 371 | "trimmed":false, 372 | "sourceSize":{ 373 | "w":32, 374 | "h":32 375 | } 376 | }, 377 | "DoorLeftOpen":{ 378 | "frame":{ 379 | "x":64, 380 | "y":0, 381 | "w":32, 382 | "h":32 383 | }, 384 | "rotated":false, 385 | "trimmed":false, 386 | "sourceSize":{ 387 | "w":32, 388 | "h":32 389 | } 390 | } 391 | }, 392 | "meta":{ 393 | "app":"https://www.leshylabs.com/apps/sstool/", 394 | "version":"Leshy SpriteSheet Tool v0.8.4", 395 | "image":"walls.png", 396 | "size":{ 397 | "w":64, 398 | "h":80 399 | }, 400 | "scale":1 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /art/walls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torrobinson/roguelike/331e3c613ed97d209a545ab32134271f1b7301dc/art/walls.png -------------------------------------------------------------------------------- /game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 125 | 126 | 127 | 128 |
129 | 130 |
131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | //*********** IMPORTS ***************** 2 | var gulp = require('gulp'); 3 | var sass = require('gulp-ruby-sass'); 4 | var gutil = require('gulp-util'); 5 | var rename = require("gulp-rename"); 6 | var map = require("map-stream"); 7 | var livereload = require("gulp-livereload"); 8 | var concat = require("gulp-concat"); 9 | var babel = require('gulp-babel'); 10 | var uglify = require('gulp-uglify'); 11 | var include = require("gulp-include"); 12 | var run = require('gulp-run'); 13 | var open = require('gulp-open'); 14 | var notify = require("gulp-notify"); 15 | 16 | var buildFolder = './build'; 17 | 18 | gulp.task('default', ['build']); 19 | 20 | gulp.task('buildAndLaunch',['build'], function(){ 21 | return gulp.src([buildFolder + '/game.html']).pipe(open()); 22 | }); 23 | 24 | gulp.task('art', function(){ 25 | gulp.src(['art/**/*']) 26 | .pipe(gulp.dest(buildFolder + '/art')); 27 | }); 28 | 29 | gulp.task('build', function(){ 30 | // Include third party scripts 31 | 32 | // Pixi 33 | gulp.src("./node_modules/pixi.js/dist/pixi.min.js") 34 | .pipe(include()) 35 | .pipe(gulp.dest(buildFolder + "/js/")); 36 | gulp.src("./node_modules/pixi.js/dist/pixi.min.js.map") 37 | .pipe(include()) 38 | .pipe(gulp.dest(buildFolder + "/js/")); 39 | 40 | // Pixi particles 41 | gulp.src("./node_modules/pixi-particles/dist/pixi-particles.min.js") 42 | .pipe(include()) 43 | .pipe(gulp.dest(buildFolder + "/js/")); 44 | gulp.src("./node_modules/pixi-particles/dist/pixi-particles.min.js.map") 45 | .pipe(include()) 46 | .pipe(gulp.dest(buildFolder + "/js/")); 47 | 48 | 49 | // Pathfinding 50 | gulp.src("./node_modules/pathfinding/visual/lib/pathfinding-browser.min.js") 51 | .pipe(include()) 52 | .pipe(gulp.dest(buildFolder + "/js/")); 53 | 54 | // Copy over game client 55 | gulp.src(['game.html']) 56 | .pipe(gulp.dest(buildFolder)); 57 | 58 | // Copy over game resources 59 | gulp.src(['art/**/*']) 60 | .pipe(gulp.dest(buildFolder + '/art')); 61 | 62 | // Compile typescript 63 | run('tsc').exec(function(){ 64 | gulp.src([buildFolder + '/game.html']) 65 | .pipe(notify({title: 'Gulp', message: 'Build finished', wait: false })); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roguelike", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "pixi.js": "4.5.2", 6 | "pathfinding": "0.4.18" 7 | }, 8 | "devDependencies": { 9 | "babel-preset-es2015": "*", 10 | "gulp": "*", 11 | "gulp-babel": "*", 12 | "gulp-concat": "*", 13 | "gulp-include": "^2.3.1", 14 | "gulp-livereload": "*", 15 | "gulp-notify": "^3.0.0", 16 | "gulp-open": "^2.0.0", 17 | "gulp-rename": "*", 18 | "gulp-ruby-sass": "*", 19 | "gulp-run": "^1.7.1", 20 | "gulp-uglify": "*", 21 | "gulp-util": "*", 22 | "gulp-watch": "*", 23 | "map-stream": "*", 24 | "node-notifier": "^5.1.2", 25 | "pixi-particles": "^2.1.5", 26 | "typescript": "^2.3.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Actors/Character/Chaser.ts: -------------------------------------------------------------------------------- 1 | class Chaser extends Actor { 2 | // Movement/pathing helpers 3 | target: Actor = null; 4 | targetKnownLocation: Point = null; 5 | stuckFor: number = 0; 6 | abandonPathAfterStuckFor: number = 2; 7 | 8 | constructor(game: Game) { 9 | super(game); 10 | this.doesSubscribeToTicks = true; 11 | this.takesCommands = true; 12 | } 13 | 14 | // When run into the player, perform the attack 15 | collidedInto(actor: Actor) { 16 | // Call base Actor collision 17 | super.collidedInto(actor); 18 | 19 | if (actor instanceof Player) { 20 | this.attack(actor); 21 | } 22 | } 23 | 24 | // When bumped by anything else (or bumping into anything else, retarget) 25 | collided(actorInvolved: Actor) { 26 | super.collided(actorInvolved); 27 | // If we hit something that wasn't our target, re-evaluate the path 28 | if (!(actorInvolved instanceof Player) && this.target !== null && actorInvolved !== this.target && this.targetKnownLocation !== null) { 29 | this.setCourseForPoint(this.targetKnownLocation); 30 | } 31 | } 32 | 33 | defaultAttackPower(): number { 34 | return 1 + Math.floor(this.level * 0.5); 35 | } 36 | 37 | tick() { 38 | super.tick(); 39 | 40 | var player = this.game.player; 41 | 42 | // If we can see the player, 43 | if (this.canSeeActor(player)) { 44 | // Then try to attack them 45 | if (this.canAttack(player)) { 46 | this.interruptWithCommand( 47 | new DirectAttack( 48 | this, 49 | // a chargup duration, if greater than 1 50 | ) 51 | ); 52 | } 53 | // Otherwise, try get closer 54 | this.setCourseFor(player); 55 | } 56 | else { 57 | // Can't see the player. 58 | // They'll finish their current moves here to where the last saw you 59 | 60 | 61 | // If they ran out of commands, then go back 'home' and forget who we were chasing 62 | if (this.currentCommand === null && this.home !== null && !this.location.equals(this.home)) { 63 | this.setCourseForHome(); 64 | } 65 | 66 | } 67 | 68 | } 69 | 70 | move(direction: Direction) { 71 | super.move(direction); 72 | 73 | // After every movement attempt, check if we actually moved 74 | if (this.movedLastTurn) { 75 | this.stuckFor = 0; 76 | } 77 | else if (this.target != null || this.currentCommand instanceof MoveTo) { 78 | // If we didnt and we meant to (had a target or have a move command), count up 79 | this.stuckFor++; 80 | 81 | // And if we were stuck for our maximum allowable turns, then abandon the target and try return home 82 | if (this.stuckFor >= this.abandonPathAfterStuckFor) { 83 | this.stuckFor = 0; 84 | this.setCourseForHome(); 85 | } 86 | } 87 | 88 | } 89 | 90 | setCourseFor(actor: Actor) { 91 | this.target = actor; 92 | this.targetKnownLocation = actor.location.clone(); 93 | this.setCourseForPoint(this.targetKnownLocation); 94 | } 95 | 96 | setCourseForPoint(point: Point) { 97 | var command = new MoveTo( 98 | this, 99 | point 100 | ); 101 | this.interruptWithCommand(command); 102 | } 103 | 104 | setCourseForHome() { 105 | this.forgetAboutTarget(); 106 | this.interruptWithCommand( 107 | new MoveTo( 108 | this, 109 | this.home, 110 | true 111 | ) 112 | ); 113 | } 114 | 115 | forgetAboutTarget() { 116 | this.target = null; 117 | this.targetKnownLocation = null; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Actors/Character/Enemies/Ghost.ts: -------------------------------------------------------------------------------- 1 | class Ghost extends Chaser{ 2 | startingHealth: number = 1; 3 | health: number = this.startingHealth; 4 | name: string = 'Ghost'; 5 | moveTickDuration: number = 3; 6 | viewRadius: number = 25; 7 | 8 | constructor(game: Game){ 9 | super(game); 10 | 11 | this.blocksSight = false; // it's short and we can see over it 12 | this.spritesets = Sprites.GhostSprites(); 13 | 14 | // Initialize level as the same as the player 15 | this.level = Battle.getLevelModifierForActor(game.player); 16 | this.xpBounty = 1 + this.level * 2; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Actors/Character/Enemies/GreenBlob.ts: -------------------------------------------------------------------------------- 1 | class GreenBlob extends Chaser{ 2 | startingHealth: number = 2; 3 | health: number = this.startingHealth; 4 | name: string = 'Green Blob'; 5 | moveTickDuration: number = 2; 6 | viewRadius: number = 15; 7 | 8 | constructor(game: Game){ 9 | super(game); 10 | 11 | this.blocksSight = false; // it's short and we can see over it 12 | this.spritesets = Sprites.GreenBlobSprites(); 13 | 14 | // Initialize level as the same as the player 15 | this.level = Battle.getLevelModifierForActor(game.player); 16 | this.xpBounty = 1 + this.level * 2; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Actors/Character/Enemies/Skeleton.ts: -------------------------------------------------------------------------------- 1 | class Skeleton extends Chaser{ 2 | startingHealth: number = 4; 3 | health: number = this.startingHealth; 4 | name: string = 'Skeleton'; 5 | moveTickDuration: number = 1; 6 | viewRadius: number = 10; 7 | 8 | constructor(game: Game){ 9 | super(game); 10 | 11 | this.blocksSight = false; // it's short and we can see over it 12 | this.spritesets = Sprites.SkeletonSprites(); 13 | 14 | this.attackRange = 2; 15 | 16 | // Initialize level as the same as the player 17 | this.level = Battle.getLevelModifierForActor(game.player); 18 | this.xpBounty = 1 + this.level * 2; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Actors/Character/Player.ts: -------------------------------------------------------------------------------- 1 | class Player extends Actor { 2 | runStats: RunStats; 3 | startingHealth: number = 10; 4 | health: number = this.startingHealth; 5 | moveTickDuration: number = 1; 6 | name: string = 'You'; 7 | viewRadius: number = 12; 8 | totalXP: number = 0; 9 | constructor(game: Game) { 10 | super(game); 11 | this.fogged = false; 12 | this.takesCommands = true; 13 | this.doesSubscribeToTicks = true; 14 | this.spritesets = Sprites.PlayerSprites(); 15 | this.reset(); 16 | } 17 | 18 | move(direction: Direction) { 19 | super.move(direction); 20 | // When we move, we want to start the animation over the next turn 21 | this.restartSpriteNextFrame = true; 22 | } 23 | 24 | initStats() { 25 | this.runStats = new RunStats(); 26 | } 27 | 28 | // Fully reset the player to a clean state 29 | reset() { 30 | this.health = this.startingHealth; 31 | this.clearCommands(); 32 | this.equippedWeapon = null; 33 | this.inventory = []; 34 | this.level = 0; 35 | this.xpNeeded = XP.getExperiencePointsRequired(this.level); 36 | this.totalXP = 0; 37 | this.currentLevelXP = 0; 38 | this.initStats(); 39 | } 40 | 41 | collidedInto(actor: Actor) { 42 | // Call base Actor collision 43 | super.collidedInto(actor); 44 | // When the player touches the stairs, generate the next dungeon 45 | if (actor instanceof StairsDown) { 46 | // push the current state of the world to the stack 47 | this.game.generateNextDungeon(); 48 | } 49 | else if (actor instanceof Chaser) { 50 | this.attack(actor); 51 | } 52 | } 53 | 54 | tick() { 55 | super.tick(); 56 | this.revealWorld(); 57 | } 58 | 59 | tryUseInventory(consumable) { 60 | // Get and use the first instance of the consumable type passed in 61 | var item: Consumable = this.inventory.where((inv) => { return inv instanceof consumable }).first(); 62 | if (item !== undefined && item !== null) { 63 | this.useItem(item); 64 | } 65 | } 66 | 67 | useItem(item: Consumable) { 68 | 69 | // Don't let the player waste potions if they're at max health 70 | if (item instanceof Potion && this.health === this.maxHealth()) { 71 | return; 72 | } 73 | 74 | item.use(); 75 | } 76 | 77 | equip(equipment: Equipment) { 78 | equipment.equip(); 79 | } 80 | 81 | giveGold(goldCount: number) { 82 | this.gold += goldCount; 83 | this.game.log( 84 | new LogMessage( 85 | 'You received ' + goldCount + ' gold', 86 | LogMessageType.ObtainedGold 87 | ) 88 | ); 89 | 90 | this.game.renderer.renderGoldPickupEffect(this, goldCount); 91 | } 92 | takeGold(goldCount: number) { 93 | this.gold -= goldCount; 94 | this.game.log( 95 | new LogMessage( 96 | 'You lost ' + goldCount + ' gold', 97 | LogMessageType.LostGold 98 | ) 99 | ); 100 | } 101 | 102 | 103 | attackedBy(attacker: Actor, damage: number) { 104 | super.attackedBy(attacker, damage); 105 | this.game.log( 106 | new LogMessage( 107 | 'You were damaged by ' + attacker.name + ' for ' + damage + ' HP', 108 | LogMessageType.Damaged 109 | ) 110 | ); 111 | } 112 | 113 | attack(otherActor: Actor) { 114 | super.attack(otherActor); 115 | this.game.log( 116 | new LogMessage( 117 | 'You damaged ' + otherActor.name + ' for ' + this.getDamage() + ' HP', 118 | LogMessageType.LandedAttack 119 | ) 120 | ); 121 | } 122 | 123 | die() { 124 | this.game.reset(); 125 | this.game.log( 126 | new LogMessage( 127 | "You died.", 128 | LogMessageType.Damaged 129 | ) 130 | ); 131 | } 132 | 133 | madeKill(killedActor: Actor) { 134 | super.madeKill(killedActor); 135 | this.runStats.kills++; 136 | 137 | this.game.log( 138 | new LogMessage( 139 | 'You killed ' + killedActor.name, 140 | LogMessageType.Informational 141 | ) 142 | ); 143 | 144 | this.giveXP(killedActor.xpBounty); 145 | 146 | } 147 | 148 | giveXP(xp: number, announce: boolean = true) { 149 | this.currentLevelXP += xp; 150 | var overflow = 0; 151 | if (this.currentLevelXP > this.xpNeeded) { 152 | overflow = this.currentLevelXP - this.xpNeeded; 153 | } 154 | 155 | this.totalXP += xp - overflow; 156 | 157 | if (announce) { 158 | this.game.log( 159 | new LogMessage( 160 | 'You gained ' + xp + ' XP', 161 | LogMessageType.GainedXP 162 | ) 163 | ); 164 | } 165 | 166 | if (this.currentLevelXP >= this.xpNeeded) { 167 | this.level++; 168 | this.currentLevelXP = 0; 169 | this.xpNeeded = Math.floor(XP.getExperiencePointsRequired(this.level)); 170 | 171 | this.game.log( 172 | new LogMessage( 173 | 'You levelled up to level ' + this.level, 174 | LogMessageType.LevelledUp 175 | ) 176 | ); 177 | 178 | if (overflow > 0) { 179 | this.giveXP(overflow, false); 180 | } 181 | else { 182 | this.currentLevelXP = 0; 183 | } 184 | 185 | } 186 | } 187 | 188 | // Unfog the world as it's explored 189 | revealWorld() { 190 | 191 | // Keep track of actors in range to use for the selectable actors group 192 | var enemiesInRange: Actor[] = []; 193 | 194 | // If we're placed 195 | if (this.location !== null) { 196 | 197 | // Visibility is based on line-of-site and radius around the player that's not obscured 198 | // on the main collision/wall layer. 199 | 200 | var wallLayer = this.world.getWallLayer(); 201 | var floorLayer = this.world.getLayersOfType(LayerType.Floor).first(); 202 | var floorDecorLayer = this.world.getLayersOfType(LayerType.FloorDecor).first(); 203 | 204 | // Based on the radius around the player and line-of-sight with walls and other 205 | // wall-layered objects, see if they can see other tiles 206 | for (var y = this.location.y - this.viewRadius; y < this.location.y + this.viewRadius; y++) { 207 | for (var x = this.location.x - this.viewRadius; x < this.location.x + this.viewRadius; x++) { 208 | if (y >= 0 && y < wallLayer.height && x >= 0 && x < wallLayer.width) { // If it's in-bounds 209 | // The point to trace TO 210 | var point = new Point(x, y); 211 | 212 | var actor = wallLayer.getTile(point.x, point.y); 213 | var floor = floorLayer.getTile(point.x, point.y); 214 | var floorDecor = floorDecorLayer.getTile(point.x, point.y); 215 | 216 | if (actor instanceof Chaser && actor !== undefined && !actor.fogged && Geometry.IsPointInCircle(this.location, this.viewRadius, point)) { 217 | enemiesInRange.push(actor); 218 | } 219 | 220 | // If we can see this point in the world 221 | if (this.canSeePoint(point, this.viewRadius)) { 222 | 223 | // Unfog wall/collision pieces 224 | 225 | if (actor !== null && actor.fogged) { 226 | actor.fogged = false; 227 | } 228 | 229 | // Unfog floor pieces 230 | if (floor !== null && floor.fogged) { 231 | floor.fogged = false; 232 | } 233 | 234 | // Unfor floor decor the same way as the floor 235 | if (floorDecor !== null && floorDecor.fogged) { 236 | floorDecor.fogged = false; 237 | } 238 | } 239 | else if (Geometry.IsPointInCircle(this.location, this.viewRadius, point)) { 240 | // Regardless if we can see it, clear pieces by radius alone, 241 | // ignoring line of sight 242 | 243 | // Unfog blocked surrounded wall pieces OR side pieces within view radius 244 | var surroundedWall = wallLayer.getTile(point.x, point.y); 245 | if (surroundedWall !== null && surroundedWall.location !== null) { 246 | if (surroundedWall.fogged 247 | && ( 248 | surroundedWall.facing === Direction.UpDownLeftRight || 249 | surroundedWall.facing === Direction.UpDownRight || 250 | surroundedWall.facing === Direction.UpDownLeft || 251 | surroundedWall.facing === Direction.UpLeftRight || 252 | surroundedWall.facing === Direction.DownLeftRight || 253 | ( 254 | surroundedWall.location.x === 0 255 | || surroundedWall.location.y === 0 256 | || surroundedWall.location.y === this.layer.tiles.length - 1 257 | || surroundedWall.location.x === this.layer.tiles[0].length - 1 258 | ) 259 | ) 260 | ) { 261 | surroundedWall.fogged = false; 262 | } 263 | } 264 | } 265 | 266 | } 267 | } 268 | } 269 | } 270 | this.game.selectableActorGroup.setGroup(enemiesInRange); 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /src/Actors/Character/RunStats.ts: -------------------------------------------------------------------------------- 1 | class RunStats { 2 | kills: number = 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/Actors/EmitsLight.ts: -------------------------------------------------------------------------------- 1 | interface EmitsLight { 2 | emitRadius: number; 3 | emitColor: number; // 0xFFFFFF is white 4 | emitIntensity: number; // 1.0 is max 5 | } 6 | -------------------------------------------------------------------------------- /src/Actors/Environment/Bookshelf.ts: -------------------------------------------------------------------------------- 1 | class Bookshelf extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.BookshelfSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | this.blocksSight = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Actors/Environment/Carpet.ts: -------------------------------------------------------------------------------- 1 | class Carpet extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.CarpetSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Actors/Environment/Door.ts: -------------------------------------------------------------------------------- 1 | class Door extends Actor { 2 | tryToClose: boolean = false; 3 | doesSubscribeToTicks: boolean = true; 4 | defaultOpenTickDuration: number = 3; 5 | openTickDuration: number; 6 | 7 | constructor(game: Game, orientation: Orientation) { 8 | super(game); 9 | this.spritesets = Sprites.DoorSprites(); 10 | this.fogStyle = FogStyle.Darken; 11 | this.blocksSight = true; 12 | this.openTickDuration = this.defaultOpenTickDuration; 13 | 14 | this.status = ActorStatus.Closed; 15 | 16 | if (orientation === Orientation.Horizontal) { 17 | this.facing = Direction.Left; 18 | } 19 | else if (orientation === Orientation.Vertical) { 20 | this.facing = Direction.Down; 21 | } 22 | } 23 | 24 | tryOpen() { 25 | if (this.status === ActorStatus.Closed) { 26 | this.open(); 27 | } 28 | } 29 | 30 | open() { 31 | this.tryToClose = false; 32 | this.status = ActorStatus.Open; 33 | var wallDecorLayer: Layer = this.game.world.getLayersOfType(LayerType.WallDecor).first(); 34 | this.jumpToLayer(wallDecorLayer); 35 | this.blocksSight = false; 36 | this.openTickDuration = this.defaultOpenTickDuration; 37 | } 38 | 39 | close() { 40 | // Try to close 41 | this.tryToClose = true; 42 | this.tryClose(); 43 | } 44 | 45 | // try close when the doorway is free and open 46 | tryClose() { 47 | var mainlayer: Layer = this.game.world.getWallLayer(); 48 | if (mainlayer.getTile(this.location.x, this.location.y) === null) { 49 | this.status = ActorStatus.Closed; 50 | this.jumpToLayer(mainlayer); 51 | this.blocksSight = true; 52 | this.tryToClose = false; 53 | } 54 | } 55 | 56 | tick() { 57 | super.tick(); 58 | 59 | // Every tick while open, count down 60 | if (this.status === ActorStatus.Open) { 61 | this.openTickDuration--; 62 | 63 | // And if we reached below 0, then close 64 | if (this.openTickDuration < 0) { 65 | this.close(); 66 | } 67 | } 68 | 69 | // If we're waiting on a close, try every tick 70 | if (this.tryToClose) { 71 | this.tryClose(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Actors/Environment/Floor.ts: -------------------------------------------------------------------------------- 1 | class Floor extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.FloorSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Actors/Environment/Graves.ts: -------------------------------------------------------------------------------- 1 | class CrossGrave extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.CrossGraveSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | this.blocksSight = false; 7 | } 8 | } 9 | 10 | class Tombstone extends Actor { 11 | constructor(game: Game) { 12 | super(game); 13 | this.spritesets = Sprites.TombstoneSprites(); 14 | this.fogStyle = FogStyle.Darken; 15 | this.blocksSight = false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Actors/Environment/Pillar.ts: -------------------------------------------------------------------------------- 1 | class Pillar extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.PillarSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | this.blocksSight = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Actors/Environment/Special/OutOfBounds.ts: -------------------------------------------------------------------------------- 1 | class OutOfBounds extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.OutOfBoundsSprites(); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Actors/Environment/Special/StairsDown.ts: -------------------------------------------------------------------------------- 1 | class StairsDown extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.StairsDownSprites(); 5 | this.fogStyle = FogStyle.Hide; 6 | this.blocksSight = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Actors/Environment/Special/StairsUp.ts: -------------------------------------------------------------------------------- 1 | class StairsUp extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.StairsUpSprites(); 5 | this.fogStyle = FogStyle.Hide; 6 | this.blocksSight = false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Actors/Environment/Torch.ts: -------------------------------------------------------------------------------- 1 | class Torch extends Actor implements EmitsLight { 2 | emitRadius: number = 8; 3 | emitColor: number = LightColorCode.White; 4 | emitIntensity: number = 1; 5 | 6 | 7 | constructor(game: Game, color?: number) { 8 | super(game); 9 | 10 | if (color) { 11 | this.emitColor = color; 12 | } 13 | 14 | this.spritesets = Sprites.TorchSprites(); 15 | this.fogStyle = FogStyle.Darken; 16 | this.blocksSight = false; 17 | this.fullBright = true; // it's a torch, it cant be darkened 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Actors/Environment/Wall.ts: -------------------------------------------------------------------------------- 1 | class Wall extends Actor { 2 | constructor(game: Game) { 3 | super(game); 4 | this.spritesets = Sprites.WallSprites(); 5 | this.fogStyle = FogStyle.Darken; 6 | } 7 | 8 | die() { 9 | // Before a wall is removed from the world, ensure there's a floor piece under it 10 | var floorLayer = this.game.world.getLayersOfType(LayerType.Floor).first(); 11 | if (floorLayer.getTile(this.location.x, this.location.y) === null) { 12 | floorLayer.placeActor( 13 | new Floor(this.game), 14 | this.location 15 | ); 16 | } 17 | super.die(); 18 | } 19 | 20 | attackedBy(attacker: Actor, damage: number){ 21 | // Override to nothing. We don't want to apply damage effects to a wall 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Behaviour/Action.ts: -------------------------------------------------------------------------------- 1 | // Action represents an atomic instruction such as a single movement or attack or action 2 | class Action { 3 | command: Command; 4 | tickDuration: number = 1; 5 | executionType: ExecutionType = ExecutionType.WaitAndThenExecute; 6 | 7 | constructor(command: Command) { 8 | this.command = command; 9 | } 10 | 11 | getActor() { 12 | return this.command.actor; 13 | } 14 | 15 | execute() { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Behaviour/Actions/AttackFacingTile.ts: -------------------------------------------------------------------------------- 1 | class AttackFacingTile extends Action { 2 | 3 | tickDuration: number; 4 | executionType: ExecutionType; 5 | direction: Direction; 6 | 7 | constructor(command: Command, chargeTicks: number = 1) { 8 | super(command); 9 | this.tickDuration = chargeTicks; 10 | this.executionType = ExecutionType.WaitAndThenExecute; 11 | } 12 | execute() { 13 | super.execute(); 14 | var me: Actor = this.getActor(); 15 | this.direction = me.facing; 16 | 17 | var affectedPoint: Point = Movement.AddPoints( 18 | me.location, 19 | Movement.DirectionToOffset( 20 | this.direction 21 | ) 22 | ); 23 | 24 | var actorAtPoint: Actor = me.layer.getTile(affectedPoint.x, affectedPoint.y); 25 | if (actorAtPoint !== null) { 26 | me.attack(actorAtPoint); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Behaviour/Actions/AttackFirstInLine.ts: -------------------------------------------------------------------------------- 1 | class AttackFirstInLine extends Action { 2 | 3 | tickDuration: number; 4 | executionType: ExecutionType; 5 | targetTile: Point; 6 | weaponUsed: Projectile; 7 | 8 | constructor(command: Command, targetTile: Point, weaponUsed: Projectile, chargeTicks: number = 1) { 9 | super(command); 10 | this.tickDuration = chargeTicks; 11 | this.targetTile = targetTile; 12 | this.weaponUsed = weaponUsed; 13 | this.executionType = ExecutionType.WaitAndThenExecute; 14 | } 15 | execute() { 16 | super.execute(); 17 | 18 | // Draw a line from me to the targetTile and damage whatever actor we hit first 19 | var start: Actor = this.getActor(); 20 | var end = this.targetTile; 21 | 22 | var actorHit: Actor = null; 23 | // In in view range 24 | if (start.getDistanceFromPoint(end) <= start.viewRadius) { 25 | 26 | if (Geometry.PointCanSeePoint(start.location, end, start.layer, 27 | (intermediaryActorHit) => { 28 | // If it can't see, but it did hit an intermediary actor 29 | actorHit = intermediaryActorHit; 30 | } 31 | )) { 32 | // Can see and nothing obstructed 33 | actorHit = start.layer.getTile(end.x, end.y); 34 | } 35 | 36 | } 37 | 38 | // We have an actor to attempt the shot on 39 | if (actorHit !== null) { 40 | 41 | // First consume the arrow no matter whar 42 | var ammoToUse = this.getActor().getInventoryOfType(this.weaponUsed.ammoType).first(); 43 | this.getActor().inventory.remove(ammoToUse); 44 | 45 | // Roll to see whether it was a success or not 46 | // We don't have to use the base game seed here because we want gameplay to vary 47 | var random = new Random(Date.now()); 48 | if (random.wasLuckyPercent(this.weaponUsed.successRatePercent)) { 49 | // If they made the shot, damage the target 50 | // Damage the actor hit 51 | actorHit.attack(actorHit, start.getDamage()); 52 | } 53 | else { 54 | // They missed, so drop it nearby as the worlditem 55 | var layer: Layer = start.layer 56 | var emptySpaceToDropAt: Point = Geometry.GetNearestFreePointTo(end, layer, 10); 57 | layer.setTile( 58 | emptySpaceToDropAt.x, 59 | emptySpaceToDropAt.y, 60 | new DroppedArrow( 61 | start.game, 62 | start.game.random, 63 | ) 64 | ); 65 | 66 | // Display a 'missed' message 67 | start.game.renderer.renderMessageAboveActor(actorHit, 'missed', ColorCode.Red); 68 | 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Behaviour/Actions/Move.ts: -------------------------------------------------------------------------------- 1 | class Move extends Action { 2 | 3 | direction: Direction; 4 | tickDuration: number; 5 | executionType: ExecutionType; 6 | 7 | constructor(command: Command, direction: Direction) { 8 | super(command); 9 | this.direction = direction; 10 | this.tickDuration = this.command.actor.moveTickDuration; 11 | this.executionType = ExecutionType.ExecuteAndThenWait; 12 | } 13 | execute() { 14 | super.execute(); 15 | this.getActor().move(this.direction); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Behaviour/Actions/RadialAttack.ts: -------------------------------------------------------------------------------- 1 | class RadialAttack extends Action { 2 | 3 | tickDuration: number; 4 | range: number; 5 | executionType: ExecutionType; 6 | 7 | constructor(command: Command, range: number, chargeTicks: number) { 8 | super(command); 9 | this.range = range; 10 | this.tickDuration = chargeTicks; 11 | this.executionType = ExecutionType.WaitAndThenExecute; 12 | } 13 | execute() { 14 | super.execute(); 15 | var me = this.getActor(); 16 | 17 | // Look for all surrounding valid points by the actor attacking 18 | var affectedPoints: Point[] = Geometry.GetPointsInCircle( 19 | me.location, 20 | this.range, 21 | me.layer 22 | ); 23 | 24 | // For each point, if there's an actor there, damage them 25 | for(let p=0; p 0) { 47 | var nextAction = this.actions[0]; 48 | this.actions.shift(); // pop off the next action from the stack 49 | return nextAction; 50 | } 51 | else { 52 | return null; 53 | } 54 | } 55 | 56 | execute() { 57 | if (this.currentAction !== null) { 58 | this.currentAction.execute(); 59 | this.currentAction = this.popAction(); 60 | } 61 | } 62 | 63 | peekNextAction() { 64 | if (this.actions.length > 0) { 65 | return this.actions[0]; 66 | } 67 | else { 68 | return null; 69 | } 70 | } 71 | 72 | hasActionsRemaining() { 73 | return this.currentAction !== null || this.actions.length > 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Behaviour/Commands/DirectAttack.ts: -------------------------------------------------------------------------------- 1 | class DirectAttack extends Command { 2 | constructor(actor: Actor, chargeUpDuration: number = 1) { 3 | super(actor); 4 | 5 | this.addAction( 6 | new AttackFacingTile( 7 | this, 8 | chargeUpDuration 9 | ) 10 | ); 11 | } 12 | 13 | execute() { 14 | super.execute(); 15 | 16 | // Set status based on actions happening now 17 | this.actor.status = ActorStatus.Attacking; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Behaviour/Commands/MoveTo.ts: -------------------------------------------------------------------------------- 1 | declare var PF: any; 2 | 3 | class MoveTo extends Command { 4 | /** 5 | * constructor 6 | * @param {Actor} actor [description] 7 | * @param {Point} endPoint [description] 8 | * @param {boolean = false} canBeNear if true, actor will try move as close as possible 9 | * @param {number = 5} canBeNearRadius is the radius around endPoint to check for free spots to override with, if endPoint is taken 10 | */ 11 | constructor(actor: Actor, endPoint: Point, canBeNear: boolean = false, canBeNearRadius: number = 5) { 12 | super(actor); 13 | 14 | var startPoint = actor.location; 15 | // Override the endPoint to be as close as possible, if endPoint is current blocked 16 | if (canBeNear && actor.layer.getTile(endPoint.x, endPoint.y) !== null) { 17 | var nearestPoint = Geometry.GetNearestFreePointTo(endPoint, actor.layer, canBeNearRadius) 18 | if (nearestPoint != null) { 19 | endPoint = nearestPoint; 20 | } 21 | } 22 | 23 | if (Point.getDistanceBetweenPoints(startPoint, endPoint) === 1) { 24 | // If we're only 1 away, just add a single simple move actions 25 | this.addAction( 26 | new Move(this, Movement.AdjacentPointsToDirection(startPoint, endPoint)) 27 | ); 28 | } 29 | else { 30 | var collisionGrid = this.actor.layer.getCollisionGrid( 31 | startPoint, // consider the start and destination to be points that are walkable for the pathfinder to run 32 | endPoint // consider the start and destination to be points that are walkable for the pathfinder to run 33 | ); 34 | 35 | // Perform a Pathfind if we're more than 1 away 36 | 37 | var grid = new PF.Grid(collisionGrid.length, collisionGrid[0].length, collisionGrid); 38 | var finder = new PF.AStarFinder(); 39 | var path = finder.findPath(startPoint.x, startPoint.y, endPoint.x, endPoint.y, grid); 40 | if (path.length > 0) { 41 | for (var p = 1; p < path.length; p++) { 42 | var lastStep = new Point(path[p - 1][0], path[p - 1][1]); 43 | var step = new Point(path[p][0], path[p][1]); 44 | this.addAction( 45 | new Move( 46 | this, 47 | Movement.AdjacentPointsToDirection( 48 | lastStep, 49 | step 50 | ) 51 | ) 52 | ); 53 | } 54 | } 55 | } 56 | } 57 | 58 | execute() { 59 | super.execute(); 60 | 61 | // Set status based on actions happening now 62 | this.actor.status = ActorStatus.Moving; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Behaviour/Commands/ProjectileAttack.ts: -------------------------------------------------------------------------------- 1 | class ProjectileAttack extends Command { 2 | constructor(actor: Actor, weaponUsed: Projectile, remoteActor: Actor, chargeUpDuration: number = 1) { 3 | super(actor); 4 | 5 | this.addAction( 6 | new AttackFirstInLine( 7 | this, 8 | remoteActor.location, 9 | weaponUsed, 10 | chargeUpDuration 11 | ) 12 | ); 13 | } 14 | 15 | execute() { 16 | super.execute(); 17 | 18 | // Set status based on actions happening now 19 | this.actor.status = ActorStatus.Attacking; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Buff/Base/Buff.ts: -------------------------------------------------------------------------------- 1 | class Buff { 2 | owner: Actor = null; 3 | granter: any = null; 4 | maxUses: number = Infinity; 5 | uses: number = 0; 6 | namePart: string; 7 | color: number = ColorCode.Grey; 8 | overridesExistingBehaviour: boolean = false; 9 | 10 | constructor() { 11 | 12 | } 13 | 14 | // Basic Helpers 15 | applyTo(actor: Actor, granter = null): void { 16 | actor.buffs.push(this); 17 | this.owner = actor; 18 | if (granter) { 19 | this.granter = granter; 20 | } 21 | } 22 | 23 | remove(): void { 24 | this.owner.removeBuff(this); 25 | this.owner = null; 26 | } 27 | 28 | used(): void { 29 | this.uses++; 30 | if (this.uses >= this.maxUses) { 31 | this.remove(); 32 | } 33 | } 34 | 35 | getUsesRemaining(): number { 36 | return this.maxUses - this.uses; 37 | } 38 | 39 | // Describes the state of the buff 40 | getDescription(): string { 41 | return ''; 42 | } 43 | 44 | 45 | 46 | // Event handlers 47 | // BEFORE handlers should return TRUE or FALSE for if they should override and cancell out the normally 48 | // proceeding behaviour 49 | onAttackBefore(attacked: Actor): boolean { 50 | return false; 51 | } 52 | onAttackAfter(attacked: Actor) { 53 | 54 | } 55 | 56 | onAttackedByBefore(attackedBy: Actor): boolean { 57 | return false; 58 | } 59 | onAttackedByAfter(attackedBy: Actor) { 60 | 61 | } 62 | 63 | onMovedBefore(): boolean { 64 | return false; 65 | } 66 | onMovedAfter() { 67 | 68 | } 69 | 70 | onCollideBefore(bumped: Actor): boolean { 71 | return false; 72 | } 73 | onCollideAfter(bumped: Actor) { 74 | 75 | } 76 | 77 | 78 | onCollidedIntoByBefore(bumper: Actor): boolean { 79 | return false; 80 | } 81 | onCollidedIntoByAfter(bumper: Actor) { 82 | 83 | } 84 | 85 | tickedBefore(): boolean { 86 | return false; 87 | } 88 | tickedAfter() { 89 | 90 | } 91 | 92 | onBuffEquippedBefore(user: Actor, buff: Buff): boolean { 93 | return false; 94 | } 95 | onBuffEquippedAfter(user: Actor, buff: Buff) { 96 | 97 | } 98 | 99 | onBuffUnequippedBefore(user: Actor, buff: Buff): boolean { 100 | return false; 101 | } 102 | onBuffUnequippedAfter(user: Actor, buff: Buff) { 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/Buff/Buffs/InvisibilityBuff.ts: -------------------------------------------------------------------------------- 1 | // 2 | class InvisibilityBuff extends Buff { 3 | maxUses: number = 30; 4 | namePart: string = 'Invisibility'; 5 | color: number = ColorCode.DarkGrey; 6 | 7 | getDescription() { 8 | return "Invisible to enemies for the next " + this.getUsesRemaining() + ' actions'; 9 | } 10 | 11 | // After this buff is added, flip the actor to invisible 12 | onBuffEquippedAfter(user: Actor, buff: Buff) { 13 | if (buff === this) user.isVisible = false; 14 | } 15 | 16 | // Right before we remove this buff, flip the actor to visible again 17 | onBuffUnequippedBefore(user: Actor, buff: Buff): boolean { 18 | if (buff === this) user.isVisible = true; 19 | return false; // don't skip other normal behaiour 20 | } 21 | 22 | // Every tick while on, count as a use 23 | tickedAfter() { 24 | this.used(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Buff/Buffs/PetrifiedDebuff.ts: -------------------------------------------------------------------------------- 1 | // 2 | class PetrifiedDebuff extends Buff { 3 | maxUses: number = 30; 4 | namePart: string = 'Petrified'; 5 | color: number = ColorCode.DarkGrey; 6 | overridesExistingBehaviour: boolean = true; 7 | 8 | getDescription() { 9 | return "You cannot perform the next " + this.getUsesRemaining() + ' actions'; 10 | } 11 | 12 | // After this buff is added, flip the actor to invisible 13 | onBuffEquippedAfter(user: Actor, buff: Buff) { 14 | if (buff === this) user.isStone = true; 15 | } 16 | 17 | // Right before we remove this buff, flip the actor to visible again 18 | onBuffUnequippedBefore(user: Actor, buff: Buff): boolean { 19 | if (buff === this) user.isStone = false; 20 | return false; // don't skip other normal behaiour 21 | } 22 | 23 | // Prevent movements 24 | onMovedBefore(): boolean { 25 | return true; // skip normal movement 26 | } 27 | 28 | // Prevent attacks 29 | onAttackBefore(attacked: Actor): boolean { 30 | return true; // skip normal attack attempts 31 | } 32 | 33 | // Every tick while on, count as a use as it goes away 34 | tickedAfter() { 35 | this.used(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Buff/Buffs/WallBreakerBuff.ts: -------------------------------------------------------------------------------- 1 | // 2 | class WallBreakerBuff extends Buff { 3 | maxUses: number = 10; 4 | namePart: string = 'Wall Breaking'; 5 | color: number = ColorCode.Purple; 6 | 7 | getDescription() { 8 | return "Destroy the next " + this.getUsesRemaining() + ' walls you touch'; 9 | } 10 | 11 | // The wall breaker buff causes walls the actor bumps into to be destroyed immediately 12 | onCollideBefore(bumped: Actor): boolean { 13 | // If we hit a wall 14 | if (bumped instanceof Wall) { 15 | // And the wall isn't the border of the map 16 | // (we dont want the player to try leave the map) 17 | if ( 18 | bumped.location.x !== 0 && 19 | bumped.location.x !== bumped.layer.width - 1 && 20 | bumped.location.y !== 0 && 21 | bumped.location.y !== bumped.layer.height - 1 22 | ) { 23 | bumped.die(); 24 | this.used(); 25 | } 26 | } 27 | return false; // don't skip other normal behaiour 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Enums.ts: -------------------------------------------------------------------------------- 1 | // Bitmasks of 2 | // 1 3 | // 8 0 2 4 | // 4 5 | enum Direction { 6 | None = 0, 7 | Up = 1, 8 | Right = 2, 9 | UpRight = 3, 10 | Down = 4, 11 | UpDown = 5, 12 | DownRight = 6, 13 | UpDownRight = 7, 14 | Left = 8, 15 | UpLeft = 9, 16 | LeftRight = 10, 17 | UpLeftRight = 11, 18 | DownLeft = 12, 19 | UpDownLeft = 13, 20 | DownLeftRight = 14, 21 | UpDownLeftRight = 15 22 | } 23 | 24 | enum LightColorCode { 25 | White = 0xffedb2, // red shifted white 26 | Black = 0x121723 // blue shifted black 27 | } 28 | 29 | enum ColorCode { 30 | White = 0xFFFFFF, 31 | Black = 0x000000, 32 | Grey = 0x7a7a7a, 33 | Red = 0xFF0000, 34 | DarkRed = 0x820000, 35 | Green = 0x00FF00, 36 | Yellow = 0xffff00, 37 | Purple = 0x7f00ff, 38 | DarkPurple = 0x26004c, 39 | Pink = 0xff00ee, 40 | DarkGrey = 0x2D2D2D, 41 | DarkerGrey = 0x1C1C1C, 42 | DarkestGrey = 0x070707 43 | } 44 | 45 | enum EquipPoint { 46 | None, 47 | Head, 48 | Torso, 49 | Legs, 50 | Hands, 51 | Feet, 52 | Weapon 53 | } 54 | 55 | enum Corner { 56 | TopLeft = 0, 57 | TopRight = 1, 58 | BottomLeft = 2, 59 | BottomRight = 3 60 | } 61 | 62 | // Areas of rooms and their category 63 | enum SizeCategory { 64 | Tiny = 36, 65 | Small = 81, 66 | Medium = 100, 67 | Large = 200, 68 | Huge = 9999 69 | } 70 | 71 | enum RoomDecorationType { 72 | Nothing = 0, // Do not decorate 73 | Atrium = 1, // Room with columns down the left and right sides 74 | Library = 2, // Room with bookshelves down the side walls 75 | Graveyard = 3 // Room with graves everywhere 76 | } 77 | 78 | enum LayerType { 79 | WallDecor, 80 | Wall, 81 | FloorDecor, 82 | Floor 83 | } 84 | 85 | enum Control { 86 | UpArrow = 38, 87 | DownArrow = 40, 88 | LeftArrow = 37, 89 | RightArrow = 39, 90 | Space = 32, 91 | Enter = 13, 92 | Tab = 9, 93 | LeftBrace = 219, 94 | RightBrace = 221, 95 | Backspace = 8, 96 | Backslash = 220, 97 | Escape = 27, 98 | P = 80, 99 | I = 73 100 | } 101 | 102 | enum ExecutionType { 103 | WaitAndThenExecute = 0, 104 | ExecuteAndThenWait = 1 105 | } 106 | 107 | enum PathfinderTile { 108 | Walkable = 0, 109 | Unwalkable = 1 110 | } 111 | 112 | enum ActorStatus { 113 | Idle = 0, 114 | Moving = 1, 115 | Attacking = 2, 116 | 117 | Open = 3, 118 | Closed = 4 119 | } 120 | 121 | enum GameState { 122 | NotStarted = 0, 123 | Playing = 1, 124 | Paused = 2 125 | } 126 | 127 | enum FogStyle { 128 | Hide = 0, 129 | Darken = 1 130 | } 131 | 132 | enum AnimationLoopStyle { 133 | Static = 0, 134 | Loop = 1, 135 | Once = 2, 136 | PingPong = 3, 137 | RandomStatic = 4 138 | } 139 | 140 | enum GameDefault { 141 | FramesPerSecond = 30, 142 | FrameWaitDuration = 15, 143 | TicksPerSecond = 20 144 | } 145 | 146 | enum Orientation { 147 | Horizontal = 0, 148 | Vertical = 1 149 | } 150 | 151 | enum LogMessageType { 152 | LandedAttack, 153 | Damaged, 154 | GainedXP, 155 | LevelledUp, 156 | ObtainedItem, 157 | ObtainedGold, 158 | LostGold, 159 | Informational 160 | } 161 | 162 | class Enumeration { 163 | // Picks a random property from an object or "enum" 164 | static GetRandomEnumValue(obj: any, random: Random) { 165 | var result; 166 | var count = 0; 167 | for (var prop in obj) { 168 | if (obj.hasOwnProperty(prop) && parseInt(prop) != NaN) { //ensure it's not inherited 169 | if (random.go() < 1 / ++count) { 170 | result = prop; 171 | } 172 | } 173 | } 174 | return parseInt(result); 175 | } 176 | 177 | static GetEnumValuesAsArray(obj: any): number[] { 178 | return Object.keys(obj).filter(key => !isNaN(Number(obj[key]))).select(key => Number(key)); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Game.ts: -------------------------------------------------------------------------------- 1 | class Game { 2 | 3 | frameClock: any; 4 | framesPerSecond: number; 5 | ticksPerSecond: number; 6 | renderer: PixiRenderer; 7 | seed: number; 8 | player: Player; 9 | world: World; 10 | state: GameState; 11 | settings: GameSettings; 12 | gameLog: LogMessage[]; 13 | random: Random; 14 | dungeonNumber: number; 15 | 16 | pauseMenu: Menu; 17 | inventoryMenu: Menu; 18 | activeMenu: Menu = null; 19 | 20 | selectableActorGroup: SelectableActorGroup 21 | 22 | constructor(renderer: PixiRenderer, seed: number, settings: GameSettings, dungeonNumber: number = 1) { 23 | this.renderer = renderer; 24 | this.renderer.game = this; // set up a reference 25 | this.seed = seed; 26 | this.dungeonNumber = dungeonNumber; 27 | this.random = new Random(seed); 28 | this.settings = settings; 29 | 30 | this.frameClock = null; 31 | 32 | this.framesPerSecond = GameDefault.FramesPerSecond; 33 | this.ticksPerSecond = GameDefault.TicksPerSecond; 34 | 35 | 36 | 37 | // Add a Player to the first room with a reference back to this game 38 | this.player = new Player(this); 39 | 40 | this.world = null; 41 | 42 | this.state = GameState.NotStarted; 43 | 44 | // Initialize the renderer 45 | this.renderer.init(); 46 | 47 | this.gameLog = []; 48 | 49 | 50 | // Menus 51 | // Main 52 | this.pauseMenu = MainMenu; 53 | this.pauseMenu.linkToGame(this); 54 | // Inventory 55 | this.inventoryMenu = InventoryMenu; 56 | this.inventoryMenu.linkToGame(this); 57 | 58 | this.selectableActorGroup = new SelectableActorGroup(this); 59 | } 60 | 61 | saveSettings() { 62 | GameSettingsProvider.saveSettings(this.settings); 63 | } 64 | 65 | start() { 66 | this.state = GameState.Playing; 67 | //Tick once 68 | this.gameTick(); 69 | this.renderer.startFrameLoop(); 70 | } 71 | 72 | pause() { 73 | this.state = GameState.Paused; 74 | this.activeMenu = this.pauseMenu; 75 | } 76 | 77 | reset() { 78 | // Reset the game 79 | this.dungeonNumber = 0; 80 | this.player.reset(); 81 | this.generateNextDungeon(); 82 | this.gameLog = []; 83 | this.gameTick(); 84 | this.selectableActorGroup.clearGroup(); 85 | } 86 | 87 | openInventory() { 88 | this.state = GameState.Paused; 89 | this.activeMenu = this.inventoryMenu; 90 | } 91 | 92 | killActiveMenu() { 93 | this.state = GameState.Playing; 94 | this.activeMenu = null; 95 | } 96 | 97 | 98 | log(message: LogMessage) { 99 | this.gameLog.push(message); 100 | } 101 | 102 | getLastLog(count: number): LogMessage[] { 103 | return this.gameLog.clone().reverse().slice(0, count); 104 | } 105 | 106 | gameTick() { 107 | if (this.state !== GameState.Paused) { 108 | var actorsToTick = this.getTickableActors(); 109 | for (var a = 0; a < actorsToTick.length; a++) { 110 | actorsToTick[a].tick(); 111 | } 112 | } 113 | } 114 | 115 | getTickableActors() { 116 | var tickableActors = []; 117 | if (this.world !== null) { 118 | var actor = null; 119 | for (var l = 0; l < this.world.layers.length; l++) { 120 | for (var y = 0; y < this.world.layers[l].tiles.length; y++) { 121 | for (var x = 0; x < this.world.layers[l].tiles[y].length; x++) { 122 | actor = this.world.layers[l].getTile(x, y); 123 | if (actor instanceof Actor && actor.doesSubscribeToTicks) { 124 | tickableActors.push(actor); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | // Place player first 132 | var player = null; 133 | for (var actor of tickableActors) { 134 | if (actor instanceof Player) { 135 | player = actor; 136 | tickableActors.remove(player); 137 | } 138 | } 139 | if (player !== null) { 140 | tickableActors.unshift(player); 141 | } 142 | 143 | return tickableActors; 144 | } 145 | 146 | controlPressed(control: Control) { 147 | // PAUSED 148 | if (this.activeMenu !== null) { 149 | if (control === Control.UpArrow) { 150 | this.activeMenu.navUp(); 151 | } 152 | 153 | if (control === Control.DownArrow) { 154 | this.activeMenu.navDown(); 155 | } 156 | 157 | if (control === Control.Enter || control === Control.Space) { 158 | this.activeMenu.executeCurrentOption(); 159 | } 160 | 161 | if (control === Control.Backspace) { 162 | this.activeMenu.goBackAPage(); 163 | } 164 | 165 | if (control === Control.Escape) { 166 | this.activeMenu.resetNavStack(); 167 | this.killActiveMenu(); 168 | } 169 | 170 | // Let the Inventory key toggle itself 171 | if (control === Control.I && this.activeMenu === this.inventoryMenu) { 172 | this.activeMenu.resetNavStack(); 173 | this.killActiveMenu(); 174 | } 175 | 176 | return; 177 | } 178 | 179 | // PLAYING 180 | if (this.state === GameState.Playing) { 181 | // Arrows 182 | if ([Control.UpArrow, Control.DownArrow, Control.LeftArrow, Control.RightArrow].contains(control)) { 183 | 184 | // If we're not moving, issue a new move 185 | if (!this.player.isMoving()) { 186 | var directionToMove = Movement.ControlArrowToDirection(control); 187 | var offset = Movement.DirectionToOffset(directionToMove); 188 | var resultLocation = Movement.AddPoints(this.player.location, offset); 189 | this.player.addCommand( 190 | new MoveTo(this.player, resultLocation) 191 | ); 192 | } 193 | 194 | // Regardless, tick once 195 | this.gameTick(); 196 | } 197 | 198 | if (control === Control.Escape) { 199 | this.pause(); 200 | } 201 | 202 | if (control === Control.P) { 203 | this.player.tryUseInventory(Potion); 204 | this.gameTick(); 205 | } 206 | 207 | if (control === Control.I) { 208 | this.activeMenu = this.inventoryMenu; 209 | } 210 | 211 | if (control === Control.LeftBrace) { 212 | this.selectableActorGroup.previous(); 213 | } 214 | 215 | if (control === Control.RightBrace) { 216 | this.selectableActorGroup.next(); 217 | } 218 | 219 | if (control === Control.Backslash) { 220 | // If we hit Backslash (shoot projectile button) 221 | var actorToAttack = this.selectableActorGroup.selectedActor; 222 | // and we have an actor selected 223 | if (actorToAttack !== null) { 224 | 225 | var weapon: Weapon = this.player.getWeapon(); 226 | 227 | // And we have a projectile weapon equipped 228 | if (weapon !== null && weapon instanceof Projectile) { 229 | // And they have the ammo for the weapon 230 | if (this.player.getInventoryOfType( 231 | (weapon).ammoType 232 | ).length > 0 233 | ) { 234 | // Then set up the shot 235 | this.player.addCommand( 236 | new ProjectileAttack( 237 | this.player, 238 | weapon, 239 | actorToAttack 240 | ) 241 | ); 242 | this.gameTick(); 243 | } 244 | else { 245 | this.renderer.renderWarningAbovePlayer('No ammo'); 246 | } 247 | } 248 | else { 249 | this.renderer.renderWarningAbovePlayer('No bow equipped'); 250 | } 251 | 252 | } 253 | } 254 | 255 | return; 256 | } 257 | } 258 | 259 | getWorldSettingsForDungeonNumber(dungeonNumber: number): WorldGeneratorSettings { 260 | var settings = new WorldGeneratorSettings(); 261 | var incrementConstant = 2; 262 | 263 | var startingWidth = 25; 264 | var startingMinNumRooms = 3; 265 | 266 | 267 | settings.totalWidth = Math.ceil(startingWidth + (dungeonNumber * incrementConstant * 0.5)); 268 | settings.totalHeight = settings.totalWidth; // mirror the width for always a square 269 | settings.minRoomWidth = 3; 270 | settings.maxRoomWidth = Math.min(10, settings.minRoomWidth + (dungeonNumber * incrementConstant * 0.15)); // allow 1*constant tile bigger each floor 271 | settings.minRoomHeight = 3; 272 | settings.maxRoomHeight = Math.min(10, settings.minRoomHeight + (dungeonNumber * incrementConstant * 0.15)); 273 | settings.minNumRooms = Math.floor(startingMinNumRooms + (dungeonNumber * 0.75)); 274 | settings.maxNumRooms = settings.minNumRooms * 2; 275 | 276 | settings.minHallThickness = 1; 277 | settings.maxHallThickness = settings.maxRoomWidth > 25 ? 3 : 1; 278 | settings.retryAttempts = 1000; 279 | settings.floorActorType = Floor; 280 | this.world = WorldGenerator.GenerateCarvedWorld( 281 | this.seed, // seed, 282 | settings, // settings, 283 | this // forward on the reference to this game instance 284 | ); 285 | 286 | return settings; 287 | } 288 | 289 | generateNextDungeon() { 290 | console.log('Generating dungeon with seed "' + this.seed + '"'); 291 | this.log( 292 | new LogMessage("You've entered dungeon #" + this.dungeonNumber, LogMessageType.Informational) 293 | ); 294 | this.seed++; 295 | 296 | // Generate the dungeon 297 | var settings: WorldGeneratorSettings = this.getWorldSettingsForDungeonNumber(this.dungeonNumber); 298 | this.world = WorldGenerator.GenerateCarvedWorld( 299 | this.seed, // seed, 300 | settings, // settings, 301 | this // forward on the reference to this game instance 302 | ); 303 | this.dungeonNumber++; 304 | 305 | // Decorate it 306 | var decoratorSettings = new WorldDecoratorSettings(); 307 | var decorator = new WorldDecorator(decoratorSettings, this.seed); 308 | decorator.decorate(this.world); 309 | 310 | // Pass a reference to the world so the player can navigate it 311 | this.player.world = this.world; 312 | 313 | var mainLayer = this.world.getWallLayer(); 314 | 315 | var startRoom: Room = this.world.rooms.first(); 316 | var endRoom: Room = this.world.rooms.last(); 317 | 318 | var spawnLocation = startRoom.getCenter(); 319 | var exitLocation = endRoom.getCenter(); 320 | 321 | 322 | // Drop the stairs we just took down into the center of the rooms 323 | var stairsUp = new StairsUp(this); 324 | mainLayer.placeActor(stairsUp, spawnLocation); 325 | mainLayer.placeActor(this.player, Movement.AddPoints(spawnLocation, new Point(0, 1))); 326 | 327 | var exit = new StairsDown(this); 328 | mainLayer.placeActor(exit, exitLocation); 329 | 330 | // Throw in some demo enemies protecting the exit 331 | var chaser = new GreenBlob(this); 332 | mainLayer.placeActor(chaser, exitLocation.offsetBy(1, 1)); 333 | var chaser2 = new GreenBlob(this); 334 | mainLayer.placeActor(chaser2, exitLocation.offsetBy(0, 1)); 335 | var chaser3 = new GreenBlob(this); 336 | mainLayer.placeActor(chaser3, exitLocation.offsetBy(1, 0)); 337 | var chaser4 = new Skeleton(this); 338 | mainLayer.placeActor(chaser4, exitLocation.offsetBy(-1, -1)); 339 | var chaser5 = new Skeleton(this); 340 | mainLayer.placeActor(chaser5, exitLocation.offsetBy(-1, 0)); 341 | var chaser6 = new Skeleton(this); 342 | mainLayer.placeActor(chaser6, exitLocation.offsetBy(0, -1)); 343 | var chaser7 = new Ghost(this); 344 | mainLayer.placeActor(chaser7, exitLocation.offsetBy(1, -1)); 345 | var chaser8 = new Ghost(this); 346 | mainLayer.placeActor(chaser8, exitLocation.offsetBy(-1, 1)); 347 | chaser8.addBuff( 348 | new PetrifiedDebuff() 349 | ); 350 | 351 | var demoChest = new Chest(this, [new Potion(this.random)]); 352 | 353 | var buffedSteelBoots = new SteelBoots(5, this.random); 354 | buffedSteelBoots.addBuff( 355 | new WallBreakerBuff() 356 | ); 357 | 358 | var buffedShirt = new Shirt(2, this.random); 359 | buffedShirt.addBuff( 360 | new InvisibilityBuff() 361 | ); 362 | 363 | var dagger = new Dagger(this.random, 2); 364 | 365 | var demoChest2 = new Chest(this, [ 366 | new Potion(this.random), 367 | new Potion(this.random), 368 | new Potion(this.random), 369 | buffedShirt, 370 | new Chestplate(5, this.random), 371 | new LeatherBoots(4, this.random), 372 | new SteelBoots(5, this.random), 373 | buffedSteelBoots, 374 | dagger, 375 | new Bow(this.random, 3), 376 | new InventoryArrow(this.random), 377 | new InventoryArrow(this.random), 378 | new InventoryArrow(this.random), 379 | new InventoryArrow(this.random), 380 | new InventoryArrow(this.random), 381 | new InventoryArrow(this.random), 382 | new InventoryArrow(this.random), 383 | new InventoryArrow(this.random), 384 | new InventoryArrow(this.random), 385 | new InventoryArrow(this.random), 386 | new InventoryArrow(this.random) 387 | ]); 388 | 389 | mainLayer.placeActor(demoChest, this.world.rooms.second().getCenter()); 390 | mainLayer.placeActor(demoChest2, Movement.AddPoints(spawnLocation, new Point(1, 0))); 391 | 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/GameSettingsProvider.ts: -------------------------------------------------------------------------------- 1 | // Default game settings 2 | class GameSettings { 3 | graphic: GraphicSettings = new GraphicSettings(); 4 | minimap: MinimapSettings = new MinimapSettings(); 5 | } 6 | 7 | class GraphicSettings { 8 | showHealth: boolean = true; 9 | showLighting : boolean = true; 10 | showColoredLighting : boolean = true; 11 | } 12 | 13 | class MinimapSettings { 14 | visible: boolean = true; 15 | position: Corner = Corner.TopLeft; 16 | size: number = 1.0; 17 | opacity: number = 1.0; 18 | } 19 | 20 | // Provider which fetches and saves game settings, or provides the default 21 | var localStorageName: string = 'gameSettings'; 22 | class GameSettingsProvider { 23 | static getSettings() { 24 | var settings: GameSettings = JSON.parse(localStorage.getItem(localStorageName)); 25 | if (settings !== undefined && settings !== null) { 26 | return settings; 27 | } 28 | return new GameSettings(); 29 | } 30 | 31 | static saveSettings(gameSettings: GameSettings) { 32 | if (gameSettings !== undefined && gameSettings !== null) { 33 | localStorage.setItem(localStorageName, JSON.stringify(gameSettings)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Helpers/Battle.ts: -------------------------------------------------------------------------------- 1 | class Battle { 2 | static getLevelModifierForActor(actor: Actor): number { 3 | return actor.level; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Helpers/BuffHelpers.ts: -------------------------------------------------------------------------------- 1 | class BuffHelpers { 2 | 3 | // Attack 4 | static handleOnAttackBuffsBefore(actor: Actor, attacked: Actor) { 5 | var skipExistingBehaviour = false; 6 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onAttackBefore(attacked); }); 7 | return skipExistingBehaviour; 8 | } 9 | static handleOnAttackBuffsAfter(actor: Actor, attacked: Actor) { 10 | actor.buffs.forEach((buff) => { buff.onAttackAfter(attacked) }); 11 | } 12 | 13 | // Attacked By 14 | static handleonAttackedBuffsBefore(actor: Actor, attackedBy: Actor) { 15 | var skipExistingBehaviour = false; 16 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onAttackedByBefore(attackedBy); }); 17 | return skipExistingBehaviour; 18 | } 19 | static handleonAttackedBuffsAfter(actor: Actor, attackedBy: Actor) { 20 | actor.buffs.forEach((buff) => { buff.onAttackedByAfter(attackedBy) }); 21 | } 22 | 23 | 24 | // Moved 25 | static handleonMovedBuffsBefore(actor: Actor) { 26 | var skipExistingBehaviour = false; 27 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onMovedBefore(); }); 28 | return skipExistingBehaviour; 29 | } 30 | static handleonMovedBuffsAfter(actor: Actor) { 31 | actor.buffs.forEach((buff) => { buff.onMovedAfter() }); 32 | } 33 | 34 | 35 | // Collided 36 | static handleonCollideBuffsBefore(actor: Actor, bumped: Actor) { 37 | var skipExistingBehaviour = false; 38 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onCollideBefore(bumped); }); 39 | return skipExistingBehaviour; 40 | } 41 | static handleonCollideBuffsAfter(actor: Actor, bumped: Actor) { 42 | actor.buffs.forEach((buff) => { buff.onCollideAfter(bumped) }); 43 | } 44 | 45 | // Collided Into 46 | static handleonCollidedIntoBuffsBefore(actor: Actor, bumper: Actor) { 47 | var skipExistingBehaviour = false; 48 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onCollidedIntoByBefore(bumper); }); 49 | return skipExistingBehaviour; 50 | } 51 | static handleonCollidedIntoBuffsAfer(actor: Actor, bumper: Actor) { 52 | actor.buffs.forEach((buff) => { buff.onCollidedIntoByAfter(bumper) }); 53 | } 54 | 55 | // Ticked 56 | static handleTickBuffsBefore(actor: Actor) { 57 | var skipExistingBehaviour = false; 58 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.tickedBefore(); }); 59 | return skipExistingBehaviour; 60 | } 61 | static handleTickBuffsAfter(actor: Actor) { 62 | actor.buffs.forEach((buff) => { buff.tickedAfter() }); 63 | } 64 | 65 | // Equipped Buffs 66 | static handleOnBuffEquippedBefore(actor: Actor, buff: Buff) { 67 | var skipExistingBehaviour = false; 68 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onBuffEquippedBefore(actor, buff); }); 69 | return skipExistingBehaviour; 70 | } 71 | static handleOnBuffEquippedAfter(actor: Actor, buff: Buff) { 72 | actor.buffs.forEach((buff) => { buff.onBuffEquippedAfter(actor, buff) }); 73 | } 74 | static handleOnBuffUnequippedBefore(actor: Actor, buff: Buff) { 75 | var skipExistingBehaviour = false; 76 | actor.buffs.forEach((buff) => { skipExistingBehaviour = buff.onBuffUnequippedBefore(actor, buff); }); 77 | return skipExistingBehaviour; 78 | } 79 | static handleOnBuffUnequippedAfter(actor: Actor, buff: Buff) { 80 | actor.buffs.forEach((buff) => { buff.onBuffUnequippedAfter(actor, buff) }); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Helpers/Color.ts: -------------------------------------------------------------------------------- 1 | class Color { 2 | 3 | // 0xFFFFFF -> 'FFFFFF' 4 | static intToHexString(hex) { 5 | return '#' + hex.toString(16); 6 | } 7 | 8 | // 'FFFFFF' -> 0xFFFFFF 9 | static hexStringToInt(string) { 10 | return parseInt('0x' + string.replace('#', '')); 11 | } 12 | 13 | // produce a new color from (sharePercent 0.0->1.0, color1, optional color 2) 14 | // Accepts '#FFFFFF' color string values 15 | static shadeBlend(p: number, c0: any, c1: any) { 16 | var n = p < 0 ? p * -1 : p, u = Math.round, w = parseInt; 17 | var R, G, B, R1, G1, B1, t; 18 | var f: any; 19 | if (c0.length > 7) { 20 | f = c0.split(","), t = (c1 ? c1 : p < 0 ? "rgb(0,0,0)" : "rgb(255,255,255)").split(","), R = w(f[0].slice(4)), G = w(f[1]), B = w(f[2]); 21 | return "rgb(" + (u((w(t[0].slice(4)) - R) * n) + R) + "," + (u((w(t[1]) - G) * n) + G) + "," + (u((w(t[2]) - B) * n) + B) + ")"; 22 | } else { 23 | f = w(c0.slice(1), 16), t = w((c1 ? c1 : p < 0 ? "#000000" : "#FFFFFF").slice(1), 16), R1 = f >> 16, G1 = f >> 8 & 0x00FF, B1 = f & 0x0000FF; 24 | return "#" + (0x1000000 + (u(((t >> 16) - R1) * n) + R1) * 0x10000 + (u(((t >> 8 & 0x00FF) - G1) * n) + G1) * 0x100 + (u(((t & 0x0000FF) - B1) * n) + B1)).toString(16).slice(1); 25 | } 26 | } 27 | 28 | // Same as above but accepts 0xFFFFFF int hex values 29 | static shadeBlendInt(percent: number, color1: number, color2?: number) { 30 | var color1Str: string = this.intToHexString(color1); 31 | var color2Str: string = undefined; 32 | if (color2) { 33 | color2Str = this.intToHexString(color2); 34 | } 35 | return this.hexStringToInt(this.shadeBlend(percent, color1Str, color2Str)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Helpers/Extensions.ts: -------------------------------------------------------------------------------- 1 | interface Array { 2 | first(): T; 3 | second(): T; 4 | last(): T; 5 | secondLast(): T; 6 | remove(obj: T): void; 7 | shuffle(random: Random): Array; 8 | pickRandom(random: Random): T; 9 | contains(needle: T): boolean; 10 | onlyOdd(): Array; 11 | onlyEven(): Array; 12 | insert(obj: Object, index: number): Array; 13 | whereNotNull(): Array; 14 | where(condition): Array; 15 | select(what): Array; 16 | not(object: any): Array; 17 | sum(): number; 18 | average(): number; 19 | any(): boolean; 20 | } 21 | interface Object { 22 | clone(): any; 23 | } 24 | interface String { 25 | repeat(times: number): string; 26 | padLeft(char: string, maxLength: number): string; 27 | padRight(char: string, maxLength: number): string; 28 | } 29 | 30 | // Array extensions 31 | Array.prototype.first = function() { 32 | if (this.length) { 33 | return this[0]; 34 | } 35 | else { 36 | return null; 37 | } 38 | }; 39 | Array.prototype.second = function() { 40 | if (this.length > 1) { 41 | return this[1]; 42 | } 43 | else { 44 | return null; 45 | } 46 | }; 47 | 48 | Array.prototype.last = function() { 49 | if (this.length) { 50 | return this[this.length - 1]; 51 | } 52 | else { 53 | return null; 54 | } 55 | }; 56 | 57 | Array.prototype.secondLast = function() { 58 | if (this.length > 1) { 59 | return this[this.length - 2]; 60 | } 61 | else { 62 | return null; 63 | } 64 | }; 65 | 66 | Array.prototype.remove = function(obj) { 67 | var index = this.indexOf(obj); 68 | if (index > -1) { 69 | return this.splice(index, 1); 70 | } 71 | else { 72 | return this; 73 | } 74 | }; 75 | 76 | 77 | Array.prototype.shuffle = function(random: Random) { 78 | var a = this; 79 | for (let i = a.length; i; i--) { 80 | let j = Math.floor(random.go() * i); 81 | [a[i - 1], a[j]] = [a[j], a[i - 1]]; 82 | } 83 | return a; 84 | }; 85 | 86 | Array.prototype.pickRandom = function(random) { 87 | return this[random.next(0, this.length - 1)]; 88 | }; 89 | 90 | Array.prototype.contains = function(needle) { 91 | return this.indexOf(needle) > -1; 92 | }; 93 | 94 | Array.prototype.onlyOdd = function() { 95 | return this.filter(Numbers.isOdd); 96 | }; 97 | 98 | Array.prototype.onlyEven = function() { 99 | return this.filter(Numbers.isEven); 100 | }; 101 | 102 | Array.prototype.insert = function(obj, index) { 103 | return this.splice(index, 0, obj); 104 | }; 105 | 106 | Array.prototype.whereNotNull = function() { 107 | return this.filter((val) => { return val !== null }); 108 | }; 109 | 110 | 111 | // LINQ-esque extensions 112 | Array.prototype.where = function(condition) { 113 | return this.filter(condition); 114 | }; 115 | 116 | Array.prototype.select = function(attributes) { 117 | return this.map(attributes); 118 | }; 119 | 120 | Array.prototype.sum = function(): number { 121 | return this.reduce((a, b) => a + b, 0); 122 | }; 123 | 124 | Array.prototype.average = function(): number { 125 | return this.sum() / this.length; 126 | }; 127 | 128 | Array.prototype.any = function(): boolean { 129 | return this.length > 0; 130 | } 131 | 132 | Array.prototype.not = function(object) { 133 | return this.filter((thing) => { return thing != object }); 134 | } 135 | 136 | 137 | // Object extensions 138 | 139 | Object.prototype.clone = function() { 140 | return JSON.parse(JSON.stringify(this)); 141 | }; 142 | 143 | 144 | // String extensions 145 | 146 | String.prototype.repeat = function(times) { 147 | return (new Array(times + 1)).join(this); 148 | }; 149 | 150 | String.prototype.padLeft = function(char: string, maxLength: number): string { 151 | return char.repeat(maxLength - this.length) + this; 152 | } 153 | 154 | String.prototype.padRight = function(char: string, maxLength: number): string { 155 | return this + char.repeat(maxLength - this.length); 156 | } 157 | -------------------------------------------------------------------------------- /src/Helpers/Falloff.ts: -------------------------------------------------------------------------------- 1 | class Falloff { 2 | 3 | //y=-(x/5)^2+1 4 | static Quadratic(distanceAway: number, maxViewDistance: number, maxReturnValue: number) { 5 | return Math.max(0, -Math.pow((distanceAway / maxViewDistance), 2) + maxReturnValue); 6 | } 7 | 8 | //y=( (1/sqr(x+1)) - (1/sqr(5)) ) * 3 9 | static QuadraticInverse(distanceAway: number, maxViewDistance: number, maxReturnValue: number) { 10 | return Math.min(1, 11 | Math.max(0, 12 | ((1 / Math.sqrt(distanceAway + maxReturnValue)) - (1 / Math.sqrt(maxViewDistance))) * 3 13 | ) 14 | ); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Helpers/Generation/GenerationHelpers.ts: -------------------------------------------------------------------------------- 1 | class GenerationHelpers { 2 | // One-off Helpers Functions 3 | static canPlace(room: Room, rooms: Room[], totalWidth: number, totalHeight: number) { 4 | // Check if it goes out of bounds 5 | // the 1 and -1s are to ensure that they dont also touch the exact edge, but stay 1 away 6 | if (room.left() < 1 || room.right() > totalWidth - 1 || room.top() < 1 || room.bottom() > totalHeight - 1) { 7 | return false; 8 | } 9 | 10 | // Check for intersections with any other room 11 | for (var i = 0; i < rooms.length; i++) { 12 | var otherRoom = rooms[i]; 13 | if (Room.Intersects(room, otherRoom)) { 14 | return false; 15 | } 16 | } 17 | return true; 18 | } 19 | 20 | // Carve the Room out of Actor on a Layer 21 | static carveRoom(room: Room, wallLayer: Layer, floorLayer: Layer, floorActorType: any, gameReference: Game) { 22 | for (var y = room.top(); y < room.bottom(); y++) { 23 | for (var x = room.left(); x < room.right(); x++) { 24 | // Carve out the walls 25 | wallLayer.placeActor(null, new Point(x, y)); 26 | 27 | // Place a floor 28 | var actor = new floorActorType(gameReference); 29 | floorLayer.placeActor(actor, new Point(x, y)); 30 | } 31 | } 32 | } 33 | 34 | // Given 2 Rooms, create a hallway made of Actor at given thicknesses on a Layer 35 | static carveHallway(room1: Room, room2: Room, wallLayer: Layer, floorLayer: Layer, floorActorType: any, minHallThickness: number, maxHallThickness: number, random: Random, gameReference: Game, doorsToPlace: Door[]) { 36 | var prevCenter = room1.getCenter(); 37 | var newCenter = room2.getCenter(); 38 | 39 | // We want to get a random number between 40 | var hallThickness = Numbers.roundToOdd( 41 | random.next(minHallThickness, maxHallThickness) 42 | ); 43 | 44 | // Draw a corridor between me and the last room 45 | var horizontalFirst = random.next(0, 2); 46 | 47 | if (horizontalFirst) { 48 | this.carveHorizontalHallway(prevCenter.x, newCenter.x, prevCenter.y, hallThickness, wallLayer, floorLayer, floorActorType, gameReference, doorsToPlace, room1, room2, true); 49 | this.carveVerticalHallway(prevCenter.y, newCenter.y, newCenter.x, hallThickness, wallLayer, floorLayer, floorActorType, gameReference, doorsToPlace, room1, room2, false); 50 | } 51 | else { 52 | //vertical first 53 | this.carveVerticalHallway(prevCenter.y, newCenter.y, prevCenter.x, hallThickness, wallLayer, floorLayer, floorActorType, gameReference, doorsToPlace, room1, room2, true); 54 | this.carveHorizontalHallway(prevCenter.x, newCenter.x, newCenter.y, hallThickness, wallLayer, floorLayer, floorActorType, gameReference, doorsToPlace, room1, room2, false); 55 | } 56 | } 57 | 58 | private static newDoorHere(gameReference: Game, x: number, y: number, orientation: Orientation, doorsToPlace: Door[]): void { 59 | // roll a die to decide whether to even put one down or not 60 | if (gameReference.random.wasLuckyPercent(50)) { 61 | var newDoor = new Door(gameReference, orientation); 62 | newDoor.location = new Point(x, y); 63 | doorsToPlace.push(newDoor); 64 | } 65 | } 66 | 67 | // Carve a horizontal hallway at a given Y, from a given X to X2, on a Layer, and fill with an Actor 68 | static carveHorizontalHallway(x1: number, x2: number, y: number, thickness: number, wallLayer: Layer, floorLayer: Layer, floorActorType: any, gameReference: Game, doorsToPlace: Door[], room1: Room, room2: Room, startingWithThis: boolean) { 69 | // bulk to add on either side of hallway if thickness > 1 70 | var bulk = thickness == 1 ? 0 : (thickness - 1) / 2; 71 | 72 | // figure out room order (for door dropping) 73 | var room1_orig: Room = room1; 74 | var room2_orig: Room = room2; 75 | room1 = null; 76 | room2 = null; 77 | if (room1_orig.getCenter().x > room2_orig.getCenter().x) { 78 | room1 = room2_orig; 79 | room2 = room1_orig; 80 | } 81 | else { 82 | room1 = room1_orig; 83 | room2 = room2_orig; 84 | } 85 | 86 | for (var x = Math.min(x1, x2); x < Math.max(x1, x2) + 1 + bulk; x++) { 87 | if (thickness == 1) { 88 | // Carve to null from the walls 89 | wallLayer.placeActor(null, new Point(x, y)); 90 | 91 | // Add the floor tile 92 | var actor = new floorActorType(gameReference); 93 | floorLayer.placeActor(actor, new Point(x, y)); 94 | 95 | // And mark this space as needing a door if it's at the boundry of a room 96 | if ( 97 | (!startingWithThis && x === room1.right() + 1) || 98 | (startingWithThis && x === room2.left() - 1) 99 | ) { 100 | this.newDoorHere(gameReference, x, y, Orientation.Horizontal, doorsToPlace); 101 | } 102 | } 103 | else { 104 | for (var o = bulk; o > -bulk; o--) { 105 | // Carve to null from the walls 106 | wallLayer.placeActor(null, new Point(x, y + o)); 107 | 108 | // Add the floor tile 109 | var actor = new floorActorType(gameReference); 110 | floorLayer.placeActor(actor, new Point(x, y + o)); 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Carve a horizontal hallway at a given X, from a given Y to Y2, on a Layer, and fill with an Actor 117 | static carveVerticalHallway(y1: number, y2: number, x: number, thickness: number, wallLayer: Layer, floorLayer: Layer, floorActorType: any, gameReference: Game, doorsToPlace: Door[], room1: Room, room2: Room, startingWithThis: boolean) { 118 | // bulk to add on either side of hallway if thickness > 1 119 | var bulk = thickness == 1 ? 0 : (thickness - 1) / 2; 120 | 121 | // figure out room order (for door dropping) 122 | var room1_orig: Room = room1; 123 | var room2_orig: Room = room2; 124 | room1 = null; 125 | room2 = null; 126 | if (room1_orig.getCenter().y > room2_orig.getCenter().y) { 127 | room1 = room2_orig; 128 | room2 = room1_orig; 129 | } 130 | else { 131 | room1 = room1_orig; 132 | room2 = room2_orig; 133 | } 134 | 135 | for (var y = Math.min(y1, y2); y < Math.max(y1, y2) + 1 + bulk; y++) { 136 | if (thickness == 1) { 137 | // Carve to null from the walls 138 | wallLayer.placeActor(null, new Point(x, y)); 139 | 140 | // Add the floor tile 141 | var actor = new floorActorType(gameReference); 142 | floorLayer.placeActor(actor, new Point(x, y)); 143 | 144 | // And mark this space as needing a door if it's at the boundry of a room 145 | if ( 146 | (startingWithThis && y === room1.bottom() + 1) || 147 | (!startingWithThis && y === room2.top() - 1) 148 | ) { 149 | this.newDoorHere(gameReference, x, y, Orientation.Vertical, doorsToPlace); 150 | } 151 | } 152 | else { 153 | for (var o = bulk; o > -bulk; o--) { 154 | // Carve to null from the walls 155 | wallLayer.placeActor(null, new Point(x + o, y)); 156 | 157 | // Add the floor tile 158 | var actor = new floorActorType(gameReference); 159 | floorLayer.placeActor(actor, new Point(x + o, y)); 160 | } 161 | } 162 | } 163 | } 164 | 165 | // Given a world and a list of doors with their locations and orientations set but not placed, place them 166 | // on the WallDecor layer 167 | static placeDoors(world: World, doorsToPlace: Door[]) { 168 | for (let d = 0; d < doorsToPlace.length; d++) { 169 | var door = doorsToPlace[d]; 170 | var layer: Layer = world.getWallLayer(); 171 | layer.placeActor(door, door.location); 172 | } 173 | } 174 | 175 | static removeStandaloneDoors(world: World) { 176 | // For any doors, check if the door isn't connected to a wall 177 | // and delete it if so 178 | var doorLayer = world.getWallLayer(); 179 | for (let y = 0; y < doorLayer.tiles.length; y++) { 180 | for (let x = 0; x < doorLayer.tiles[y].length; x++) { 181 | var actor = doorLayer.getTile(x, y); 182 | if (actor && actor instanceof Door) { 183 | if (actor.facing === Direction.Down) { // vertical 184 | if ( 185 | actor.getAdjacentActor(Direction.Left) === null || 186 | actor.getAdjacentActor(Direction.Right) === null 187 | ) { 188 | doorLayer.destroyTile(actor.location.x, actor.location.y); 189 | } 190 | } 191 | else if (actor.facing === Direction.Left) { // horizontal 192 | if ( 193 | actor.getAdjacentActor(Direction.Up) === null || 194 | actor.getAdjacentActor(Direction.Down) === null 195 | ) { 196 | doorLayer.destroyTile(actor.location.x, actor.location.y); 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Helpers/Generation/WorldDecorator.ts: -------------------------------------------------------------------------------- 1 | class WorldDecoratorSettings { 2 | minNumberOfChests: number = 0; 3 | maxNumberOfChests: number = 2; 4 | minNumberOfChestContents: number = 1; 5 | maxNumberOfChestContents: number = 3; 6 | } 7 | 8 | class WorldDecorator { 9 | settings: WorldDecoratorSettings; 10 | random: Random; 11 | constructor(settings: WorldDecoratorSettings, seed: number) { 12 | this.settings = settings; 13 | this.random = new Random(seed); 14 | } 15 | 16 | decorate(world: World) { 17 | // Connect walls 18 | this.setAjdacentActorStatuses(world, LayerType.Wall, Wall); 19 | 20 | // Decorate with objects 21 | this.decorateAllRooms(world); 22 | 23 | // Drop gold 24 | this.dropGold(world); 25 | 26 | // Connect carpets 27 | this.setAjdacentActorStatuses(world, LayerType.FloorDecor, Carpet); 28 | } 29 | 30 | setAjdacentActorStatuses(world: World, layerType: any, actorType: any) { 31 | var layer = world.getLayersOfType(layerType).first(); 32 | if (layer !== undefined && layer !== null) { 33 | for (var y = 0; y < layer.tiles.length; y++) { 34 | for (var x = 0; x < layer.tiles[y].length; x++) { 35 | var tile = layer.getTile(x, y); 36 | if (tile instanceof actorType) { 37 | tile.facing = WorldDecoratorHelpers.getTileAdjacencyBitmask(layer, tile.location, actorType); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | decorateAllRooms(world: World) { 45 | // Shuffle the rooms 46 | var rooms = world.rooms.shuffle(this.random); 47 | 48 | // For each room 49 | for (var r = 0; r < rooms.length; r++) { 50 | // Decorate it 51 | this.decorateRoom(world, rooms[r]); 52 | } 53 | } 54 | 55 | decorateRoom(world: World, room: Room) { 56 | // Pick a random room type 57 | var roomType = Enumeration.GetRandomEnumValue(RoomDecorationType, this.random); 58 | var wallLayer = world.getWallLayer(); 59 | var floorDecorLayer = world.getLayersOfType(LayerType.FloorDecor).first(); 60 | 61 | // NOTHING 62 | if (roomType === RoomDecorationType.Nothing) { 63 | // Empty rooms get a 1/3 chance of having torches in the corners 64 | if (this.random.wasLucky(1, 3)) { 65 | WorldDecoratorHelpers.addTorchesToCorners( 66 | world.game, 67 | wallLayer, 68 | room, 69 | [ColorCode.Green, ColorCode.Red, ColorCode.Pink, ColorCode.Yellow, LightColorCode.White].pickRandom(this.random) 70 | ); 71 | } 72 | } 73 | 74 | // ATRIUM 75 | else if (roomType === RoomDecorationType.Atrium) { 76 | var orientation = Enumeration.GetRandomEnumValue(Orientation, this.random); 77 | // Build columns down the sides, padded by 1 78 | WorldDecoratorHelpers.decorateDownWalls( 79 | world.game, 80 | wallLayer, 81 | room, 82 | 1, // ensures what is placed always has 1 space free around it 83 | Pillar, 84 | orientation 85 | ); 86 | 87 | // Half of all atriums get torches 88 | if (this.random.go() > 0.5) { 89 | WorldDecoratorHelpers.addTorchesToCorners( 90 | world.game, 91 | wallLayer, 92 | room, 93 | LightColorCode.White 94 | ); 95 | } 96 | } 97 | 98 | // LIBRARY 99 | else if (roomType === RoomDecorationType.Library) { 100 | 101 | // Put a carpet down the middle 102 | var carpetPadding = this.random.next(1, (Math.min(room.height, room.width) / 2) - 1); 103 | WorldDecoratorHelpers.decorateWithCenteredRectangle( 104 | world.game, 105 | floorDecorLayer, 106 | room, 107 | carpetPadding, 108 | Carpet 109 | ); 110 | 111 | // Build bookshelves down the sides, against the walls (padded by 0) 112 | var orientation = Enumeration.GetRandomEnumValue(Orientation, this.random); 113 | WorldDecoratorHelpers.decorateDownWalls( 114 | world.game, 115 | wallLayer, 116 | room, 117 | 0, 118 | Bookshelf, 119 | orientation 120 | ); 121 | } 122 | 123 | // GRAVEYARD 124 | else if (roomType === RoomDecorationType.Graveyard) { 125 | var min: number = 1, max: number; 126 | 127 | switch (room.getSizeCategory()) { 128 | case SizeCategory.Tiny: 129 | max = 2; 130 | break; 131 | 132 | case SizeCategory.Small: 133 | max = 4; 134 | break; 135 | 136 | case SizeCategory.Medium: 137 | max = 8; 138 | break; 139 | 140 | case SizeCategory.Large: 141 | max = 16; 142 | break; 143 | 144 | case SizeCategory.Huge: 145 | max = 32; 146 | break; 147 | } 148 | 149 | WorldDecoratorHelpers.populateWithActor( 150 | world.game, 151 | wallLayer, 152 | room, 153 | [Tombstone, CrossGrave], 154 | this.random, 155 | min, 156 | max, 157 | false 158 | ); 159 | } 160 | 161 | } 162 | 163 | dropGold(world: World) { 164 | var wallLayer = world.getWallLayer(); 165 | for (var r = 0; r < world.rooms.length; r++) { 166 | var room = world.rooms[r]; 167 | // Rooms get a 1/7 chance of having gold in them anywhere 168 | if (this.random.wasLucky(1, 8)) { 169 | WorldDecoratorHelpers.populateWithActor( 170 | world.game, 171 | wallLayer, 172 | room, 173 | [GoldPile], 174 | this.random, 175 | 1, //between 1 and 4 of them will be dropped 176 | 3 177 | ); 178 | } 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/Helpers/Generation/WorldDecoratorHelpers.ts: -------------------------------------------------------------------------------- 1 | class WorldDecoratorHelpers { 2 | // Given a layer and the tile's location, return the bitmask representing the adjacent tiles 3 | // Numbers will align to the ActorStatus enumeration for wall status directions 4 | static getTileAdjacencyBitmask(layer: Layer, tileLocation: Point, adjacentType: any) { 5 | var up = tileLocation.y > 0 && layer.getTile(tileLocation.x, tileLocation.y - 1) instanceof adjacentType ? 1 : 0; 6 | var down = tileLocation.y < layer.tiles.length - 1 && layer.getTile(tileLocation.x, tileLocation.y + 1) instanceof adjacentType ? 4 : 0; 7 | var left = tileLocation.x > 0 && layer.getTile(tileLocation.x - 1, tileLocation.y) instanceof adjacentType ? 8 : 0; 8 | var right = tileLocation.x < layer.tiles[tileLocation.y].length - 1 && layer.getTile(tileLocation.x + 1, tileLocation.y) instanceof adjacentType ? 2 : 0; 9 | return up + down + left + right; 10 | } 11 | 12 | static decorateDownWalls(game: Game, layer: Layer, room: Room, padding: number, actorType: any, orientation: Orientation) { 13 | 14 | // VERTICAL 15 | if (orientation === Orientation.Vertical) { 16 | var leftX = padding; 17 | var rightX = room.width - (padding + 1); 18 | for (var y = (padding > 0 ? padding : 1); y + (padding > 0 ? padding : 1) < room.height; y += (padding + 1)) { 19 | var leftLocation = room.position.offsetBy(leftX, y); 20 | if (layer.getTile(leftLocation.x, leftLocation.y) === null) { 21 | if (padding > 0 || (layer.getTile(leftLocation.x - 1, leftLocation.y) instanceof Wall)) { 22 | layer.placeActor(new actorType(game), leftLocation); 23 | } 24 | } 25 | var rightLocation = room.position.offsetBy(rightX, y); 26 | if (layer.getTile(rightLocation.x, rightLocation.y) === null) { 27 | if (padding > 0 || (layer.getTile(rightLocation.x + 1, rightLocation.y) instanceof Wall)) { 28 | layer.placeActor(new actorType(game), rightLocation); 29 | } 30 | } 31 | } 32 | } 33 | 34 | 35 | // HORIZONTAL 36 | else if (orientation === Orientation.Horizontal) { 37 | var topY = padding; 38 | var bottomY = room.height - (padding + 1); 39 | for (var x = (padding > 0 ? padding : 1); x + (padding > 0 ? padding : 1) < room.width; x += (padding + 1)) { 40 | var topLocation = room.position.offsetBy(x, topY); 41 | if (layer.getTile(topLocation.x, topLocation.y) === null) { 42 | if (padding > 0 || (layer.getTile(topLocation.x, topLocation.y - 1) instanceof Wall)) { 43 | layer.placeActor(new actorType(game), topLocation); 44 | } 45 | } 46 | var bottomLocation = room.position.offsetBy(x, bottomY); 47 | if (layer.getTile(bottomLocation.x, bottomLocation.y) === null) { 48 | if (padding > 0 || (layer.getTile(bottomLocation.x, bottomLocation.y + 1) instanceof Wall)) { 49 | layer.placeActor(new actorType(game), bottomLocation); 50 | } 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | 58 | // Given a layer, a room, and the padding to ignore around the room's border, draw a rectangle of 59 | // actorytpe in the center of the room 60 | static decorateWithCenteredRectangle(game: Game, layer: Layer, room: Room, padding: number, actorType: any) { 61 | for (var y = padding; y < room.height - padding; y++) { 62 | for (var x = padding; x < room.width - padding; x++) { 63 | var location = Movement.AddPoints(room.position, new Point(x, y)); 64 | if (layer.getTile(location.x, location.y) === null) { 65 | layer.placeActor(new actorType(game), location); 66 | } 67 | } 68 | } 69 | } 70 | 71 | static addTorchesToCorners(game: Game, layer: Layer, room: Room, color: number) { 72 | var topLeft = new Point(room.position.x, room.position.y); 73 | var topRight = new Point(room.position.x + room.width - 1, room.position.y); 74 | var bottomLeft = new Point(room.position.x, room.height - 1 + room.position.y); 75 | var bottomRight = new Point(room.position.x + room.width - 1, room.height - 1 + room.position.y); 76 | 77 | // Top Left 78 | var possibleBlock1: Point = Movement.AddPoints(topLeft, Movement.DirectionToOffset(Direction.Left)); 79 | var possibleBlock2: Point = Movement.AddPoints(topLeft, Movement.DirectionToOffset(Direction.Up)); 80 | var possibleBlock3: Point = Movement.AddPoints(topLeft, new Point(1, 1)); // diagonal 81 | if (layer.getTile(topLeft.x, topLeft.y) === null && layer.getTile(possibleBlock1.x, possibleBlock1.y) instanceof Wall && layer.getTile(possibleBlock2.x, possibleBlock2.y) instanceof Wall) { 82 | if (layer.getTile(possibleBlock3.x, possibleBlock3.y) === null) 83 | layer.placeActor( 84 | new Torch(game, color), 85 | topLeft 86 | ); 87 | } 88 | 89 | // Top Right 90 | possibleBlock1 = Movement.AddPoints(topRight, Movement.DirectionToOffset(Direction.Right)); 91 | possibleBlock2 = Movement.AddPoints(topRight, Movement.DirectionToOffset(Direction.Up)); 92 | possibleBlock3 = Movement.AddPoints(topRight, new Point(-1, 1)); // diagonal 93 | if (layer.getTile(topRight.x, topRight.y) === null && layer.getTile(possibleBlock1.x, possibleBlock1.y) instanceof Wall && layer.getTile(possibleBlock2.x, possibleBlock2.y) instanceof Wall) { 94 | if (layer.getTile(possibleBlock3.x, possibleBlock3.y) === null) 95 | layer.placeActor( 96 | new Torch(game, color), 97 | topRight 98 | ); 99 | } 100 | 101 | // Bottom Left 102 | possibleBlock1 = Movement.AddPoints(bottomLeft, Movement.DirectionToOffset(Direction.Left)); 103 | possibleBlock2 = Movement.AddPoints(bottomLeft, Movement.DirectionToOffset(Direction.Down)); 104 | possibleBlock3 = Movement.AddPoints(bottomLeft, new Point(1, -1)); // diagonal 105 | if (layer.getTile(bottomLeft.x, bottomLeft.y) === null && layer.getTile(possibleBlock1.x, possibleBlock1.y) instanceof Wall && layer.getTile(possibleBlock2.x, possibleBlock2.y) instanceof Wall) { 106 | if (layer.getTile(possibleBlock3.x, possibleBlock3.y) === null) 107 | layer.placeActor( 108 | new Torch(game, color), 109 | bottomLeft 110 | ); 111 | } 112 | 113 | // Bottom Right 114 | possibleBlock1 = Movement.AddPoints(bottomRight, Movement.DirectionToOffset(Direction.Right)); 115 | possibleBlock2 = Movement.AddPoints(bottomRight, Movement.DirectionToOffset(Direction.Down)); 116 | possibleBlock3 = Movement.AddPoints(bottomRight, new Point(-1, -1)); // diagonal 117 | if (layer.getTile(bottomRight.x, bottomRight.y) === null && layer.getTile(possibleBlock1.x, possibleBlock1.y) instanceof Wall && layer.getTile(possibleBlock2.x, possibleBlock2.y) instanceof Wall) { 118 | if (layer.getTile(possibleBlock3.x, possibleBlock3.y) === null) 119 | layer.placeActor( 120 | new Torch(game, color), 121 | bottomRight 122 | ); 123 | } 124 | 125 | } 126 | 127 | static populateWithActor(game: Game, layer: Layer, room: Room, actorsToPlace: any[], random: Random, mininum: number, maximum: number, placeOnBorders: boolean = true): void { 128 | var numberToDrop = random.nextWeighted(mininum, maximum); 129 | var attemptsToMake = 20; 130 | var totalAttempts = 0; 131 | var placed = 0; 132 | 133 | while (placed < numberToDrop && totalAttempts < attemptsToMake) { 134 | for (let i = 0; i < numberToDrop; i++) { 135 | var randomX = random.next(room.position.x + (placeOnBorders ? 0 : 1), room.position.x + room.width - (placeOnBorders ? 0 : 1)); 136 | var randomY = random.next(room.position.y + (placeOnBorders ? 0 : 1), room.position.y + room.height - (placeOnBorders ? 0 : 1)); 137 | var actorToPlace = actorsToPlace.pickRandom(random); 138 | try { 139 | if (layer.getTile(randomX, randomY) === null) { 140 | layer.placeActor(new actorToPlace(game), new Point(randomX, randomY)); 141 | placed++; 142 | } 143 | } 144 | finally { 145 | totalAttempts++; 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Helpers/Generation/WorldGenerator.ts: -------------------------------------------------------------------------------- 1 | class WorldGeneratorSettings { 2 | totalWidth: number = 0; 3 | totalHeight: number = 0; 4 | minRoomWidth: number = 0; 5 | maxRoomWidth: number = 0; 6 | minRoomHeight: number = 0; 7 | maxRoomHeight: number = 0; 8 | minNumRooms: number = 0; 9 | maxNumRooms: number = 0; 10 | minHallThickness: number = 1; 11 | maxHallThickness: number = 1; 12 | retryAttempts: number = 100; 13 | floorActorType: any = null; 14 | } 15 | 16 | class WorldGenerator { 17 | static GenerateCarvedWorld( 18 | seed: number, 19 | settings: WorldGeneratorSettings, 20 | game: Game 21 | ) { 22 | // The world to return 23 | var world = new World(settings.totalWidth, settings.totalHeight, game); 24 | 25 | 26 | var wallDecor = new Layer(settings.totalHeight, settings.totalWidth, 1, 'Wall Decorations', LayerType.WallDecor); 27 | 28 | // Set up the main collision layer as ALL walls 29 | var wallLayer = new Layer(settings.totalHeight, settings.totalWidth, 0, 'Main', LayerType.Wall); 30 | wallLayer.fillWith(Wall, game); 31 | 32 | // Create a new empty floor layer. 33 | // As we carve away the walls to create rooms and hallways, we'll add floor tiles here 34 | var floorLayer = new Layer(settings.totalHeight, settings.totalWidth, -2, 'Floors', LayerType.Floor); 35 | 36 | // Add a layer for things that can be stepped on, or floor decorations, 37 | // in-between the floor and the wall layer 38 | var floorDecorLayer = new Layer(settings.totalHeight, settings.totalWidth, -1, 'Floor Decorations', LayerType.FloorDecor); 39 | 40 | // The doors we'll have to place, as conceived during hallway carving 41 | var doorsToPlace: Door[] = []; 42 | 43 | // The rooms we're creating 44 | var rooms = []; 45 | // Our RNG 46 | var random = new Random(seed); 47 | // The number of rooms to place 48 | var randomRoomsToPlace = random.next(settings.minNumRooms, settings.maxNumRooms); 49 | 50 | var failedAttempts = 0; 51 | var preferSquareRooms = 1; 52 | 53 | // While we still have rooms to carve 54 | while (rooms.length < randomRoomsToPlace && failedAttempts < settings.retryAttempts) { 55 | // Create a new place to put it 56 | var randomPosition = new Point( 57 | random.next(0, settings.totalWidth), 58 | random.next(0, settings.totalHeight) 59 | ); 60 | // Create a random size 61 | var randomWidth; 62 | var randomHeight; 63 | if (preferSquareRooms) { 64 | randomWidth = random.nextWeighted(settings.minRoomWidth, settings.maxRoomWidth); 65 | randomHeight = random.nextWeighted(settings.minRoomHeight, settings.maxRoomHeight); 66 | } 67 | else { 68 | randomWidth = random.next(settings.minRoomWidth, settings.maxRoomWidth); 69 | randomHeight = random.next(settings.minRoomHeight, settings.maxRoomHeight); 70 | } 71 | 72 | 73 | // Instantiate the room 74 | var newRoom = new Room( 75 | randomWidth, 76 | randomHeight, 77 | randomPosition 78 | ); 79 | 80 | // If we can place it, then place it 81 | if (GenerationHelpers.canPlace(newRoom, rooms, settings.totalWidth, settings.totalHeight)) { 82 | // We can place this room, so draw it out 83 | GenerationHelpers.carveRoom(newRoom, wallLayer, floorLayer, settings.floorActorType, game); 84 | rooms.push(newRoom); 85 | } 86 | else { 87 | failedAttempts++; 88 | if (preferSquareRooms && failedAttempts >= settings.retryAttempts / 2) { 89 | // If we're halfway through looking for spaces, stop preferring the bigger rooms 90 | // and give smaller rooms a shot 91 | preferSquareRooms = 0; 92 | } 93 | } 94 | } 95 | 96 | // The rooms are now built. Start carving out the hallways 97 | 98 | // First lets order the rooms in somewhat in order of distance and before they're chained together 99 | var roomsOrdered = []; 100 | var roomBag = rooms.slice(); 101 | var firstRoom = rooms.pickRandom(random); 102 | 103 | // Start with the first room at random 104 | var currentRoom = firstRoom; 105 | function distanceFromCurrentRoom(x, y) { 106 | return Point.getDistanceBetweenPoints( 107 | x.getCenter(), currentRoom.getCenter()) - Point.getDistanceBetweenPoints(y.getCenter(), currentRoom.getCenter()); 108 | } 109 | // And then find the next closest one 110 | while (roomBag.length > 0) { 111 | // While we have rooms to add, search through the bag for the closest room to prevRoom 112 | currentRoom = roomBag.sort(distanceFromCurrentRoom).first(); 113 | roomBag.remove(currentRoom); 114 | roomsOrdered.push(currentRoom); 115 | } 116 | rooms = roomsOrdered.slice(); 117 | 118 | // Then carve each hallway out 119 | for (var i = 1; i < rooms.length; i++) { 120 | var room = rooms[i]; 121 | var previousRoom = rooms[i - 1]; 122 | 123 | GenerationHelpers.carveHallway(previousRoom, room, wallLayer, floorLayer, settings.floorActorType, settings.minHallThickness, settings.maxHallThickness, random, game, doorsToPlace); 124 | } 125 | 126 | // Then to keep it from being too linear, connect the second and second last rooms 127 | GenerationHelpers.carveHallway(rooms.second(), rooms.secondLast(), wallLayer, floorLayer, settings.floorActorType, settings.minHallThickness, settings.maxHallThickness, random, game, doorsToPlace); 128 | 129 | // Set and return the World so far 130 | world.addLayer(wallDecor); 131 | world.addLayer(wallLayer); 132 | world.addLayer(floorDecorLayer); 133 | world.addLayer(floorLayer); 134 | world.rooms = rooms; 135 | 136 | // Place doors 137 | GenerationHelpers.placeDoors(world, doorsToPlace); 138 | // Remove any doors that aren't entirely embedded in walls 139 | GenerationHelpers.removeStandaloneDoors(world); 140 | 141 | return world; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Helpers/Generic.ts: -------------------------------------------------------------------------------- 1 | class Generic { 2 | 3 | /** If we have an property on a non-classes Object that 4 | can either be a static attribute OR a function/callback 5 | that makes it dynamic, then find out which and return. 6 | 7 | This essentially takes in a property and, if it's actually 8 | a function, resolves the function and returns the static 9 | result*/ 10 | static ResolveIfDynamic(property){ 11 | return typeof property == 'function' ? property() : property; 12 | } 13 | 14 | 15 | static GetDateTimeStamp(date?: Date){ 16 | if(!date){ 17 | date = new Date(); 18 | } 19 | return (date.getMonth()+1).toString().padLeft('0',2) + '/' + date.getDate().toString().padLeft('0',2) + '/' + date.getFullYear().toString().padLeft('0',2) + ' ' + date.getHours().toString().padLeft('0',2) + ':' + date.getMinutes().toString().padLeft('0',2) + ':' + date.getSeconds().toString().padLeft('0',2); 20 | } 21 | 22 | static GetTimeStamp(date?: Date){ 23 | if(!date){ 24 | date = new Date(); 25 | } 26 | return date.getHours().toString().padLeft('0',2) + ':' + date.getMinutes().toString().padLeft('0',2) + ':' + date.getSeconds().toString().padLeft('0',2); 27 | } 28 | 29 | static NewLine() : string{ 30 | return '\r\n'; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Helpers/Geometry.ts: -------------------------------------------------------------------------------- 1 | // The callback type to call upon an actor that was collided into while projecting a bresenham line 2 | type BresenhamCallback = (actor: Actor) => void; 3 | 4 | class Geometry { 5 | 6 | // Use Pythagoras to check if a point is a certain distance away 7 | static IsPointInCircle(circleLocation: Point, circleRadius: number, point: Point) { 8 | if (circleLocation !== null && point !== null) { 9 | return Math.pow((point.x - circleLocation.x), 2) + Math.pow((point.y - circleLocation.y), 2) <= Math.pow(circleRadius, 2); 10 | } 11 | } 12 | 13 | static GetPointsInCircle(circleLocation: Point, circleRadius: number, layer: Layer): Point[] { 14 | var points: Point[] = []; 15 | 16 | for (let y = circleLocation.y - circleRadius; y < circleLocation.y + circleRadius; y++) { 17 | for (let x = circleLocation.x - circleRadius; x < circleLocation.x + circleRadius; x++) { 18 | if (y > 0 && y < layer.tiles.length && x > 0 && x < layer.tiles[y].length) { 19 | var possiblePoint = new Point(x, y); 20 | if (this.IsPointInCircle(circleLocation, circleRadius, possiblePoint)) { 21 | points.push(possiblePoint); 22 | } 23 | } 24 | } 25 | } 26 | 27 | return points; 28 | } 29 | 30 | static GetNearestFreePointTo(idealPoint: Point, onLayer: Layer, maxDistance: number) { 31 | var nearestPoint: Point = null; 32 | 33 | var checkRadius = 1; 34 | while (nearestPoint === null && checkRadius <= maxDistance) { 35 | var possiblyFreePoints: Point[] = this.GetPointsInCircle(idealPoint, checkRadius, onLayer); 36 | for (let p = 0; p < possiblyFreePoints.length; p++) { 37 | var possiblyFreePoint = possiblyFreePoints[p]; 38 | if (onLayer.getTile(possiblyFreePoint.x, possiblyFreePoint.y) === null) { 39 | nearestPoint = possiblyFreePoint; 40 | } 41 | } 42 | checkRadius++; 43 | } 44 | 45 | return nearestPoint; 46 | } 47 | 48 | static IsAdjacent(point1: Point, point2: Point) { 49 | return Math.abs(point2.x - point1.x) === 1 && Math.abs(point2.y - point1.y) === 1; 50 | } 51 | 52 | 53 | // Use Bresenham's Algorithm to check if a point can project onto another point 54 | // in a straight line 55 | // This essentially keeps plotting along from point1 to point2 and if it encounters an 56 | // occlusion, returns false 57 | static PointCanSeePoint(point1: Point, point2: Point, layer: Layer, callback: BresenhamCallback = null) { 58 | // Clone these to compare against the initial input 59 | var initialStart = point1.clone(); 60 | var initialEnd = point2.clone(); 61 | 62 | // Clone these, as the alorithm modifies them during the loop 63 | var point1Clone = point1.clone(); 64 | var point2Clone = point2.clone(); 65 | 66 | var dx = Math.abs(point2Clone.x - point1Clone.x); 67 | var dy = Math.abs(point2Clone.y - point1Clone.y); 68 | var sx = (point1Clone.x < point2Clone.x) ? 1 : -1; 69 | var sy = (point1Clone.y < point2Clone.y) ? 1 : -1; 70 | var err = dx - dy; 71 | while (true) { 72 | // Only check for occulusion if we're not on the first point or last point 73 | if ((point1Clone.x === initialStart.x && point1Clone.y === initialStart.y) === false && (point1Clone.x === initialEnd.x && point1Clone.y === initialEnd.y) === false) { 74 | var objectHit = layer.tiles[point1Clone.y][point1Clone.x]; 75 | if (objectHit !== null && objectHit instanceof Actor && objectHit.blocksSight) { // we hit something not empty on the layer 76 | if(callback !== null){ 77 | callback(objectHit); 78 | } 79 | return false; 80 | } 81 | } 82 | if ((point1Clone.x == point2Clone.x) && (point1Clone.y == point2Clone.y)) break; 83 | var e2 = 2 * err; 84 | if (e2 > -dy) { err -= dy; point1Clone.x += sx; } 85 | if (e2 < dx) { err += dx; point1Clone.y += sy; } 86 | } 87 | return true; // the line was fully drawn 88 | } 89 | 90 | 91 | static getBrightnessForPoint(point: Point, lightsource: Point, maxViewDistance: number, maxReturnValue: number, fallOffFunction: any) { 92 | return fallOffFunction( 93 | Math.hypot(point.x - lightsource.x, point.y - lightsource.y), 94 | maxViewDistance, 95 | maxReturnValue); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Helpers/Movement.ts: -------------------------------------------------------------------------------- 1 | class Movement { 2 | static ControlArrowToDirection(control) { 3 | if (control == Control.UpArrow) { 4 | return Direction.Up; 5 | } 6 | if (control == Control.DownArrow) { 7 | return Direction.Down; 8 | } 9 | if (control == Control.LeftArrow) { 10 | return Direction.Left; 11 | } 12 | if (control == Control.RightArrow) { 13 | return Direction.Right; 14 | } 15 | } 16 | 17 | static DirectionToOffset(direction) { 18 | if (direction == Direction.Up) { 19 | return new Point(0, -1); 20 | } 21 | if (direction == Direction.Down) { 22 | return new Point(0, 1); 23 | } 24 | if (direction == Direction.Left) { 25 | return new Point(-1, 0); 26 | } 27 | if (direction == Direction.Right) { 28 | return new Point(1, 0); 29 | } 30 | if (direction == Direction.UpLeft) { 31 | return new Point(-1, -1); 32 | } 33 | if (direction == Direction.UpRight) { 34 | return new Point(1, -1); 35 | } 36 | if (direction == Direction.DownLeft) { 37 | return new Point(-1, 1); 38 | } 39 | if (direction == Direction.DownRight) { 40 | return new Point(1, 1); 41 | } 42 | } 43 | 44 | static AdjacentPointsToDirection(point1, point2) { 45 | var x = point2.x - point1.x; 46 | var y = point2.y - point1.y; 47 | if (x == 0 && y == 1) { 48 | return Direction.Down; 49 | } 50 | if (x == 0 && y == -1) { 51 | return Direction.Up; 52 | } 53 | if (x == 1 && y == 0) { 54 | return Direction.Right; 55 | } 56 | if (x == -1 && y == 0) { 57 | return Direction.Left; 58 | } 59 | if (x == 1 && y == 1) { 60 | return Direction.DownRight; 61 | } 62 | if (x == -1 && y == 1) { 63 | return Direction.DownLeft; 64 | } 65 | if (x == 1 && y == -1) { 66 | return Direction.UpRight; 67 | } 68 | if (x == -1 && y == -1) { 69 | return Direction.UpLeft; 70 | } 71 | }; 72 | 73 | static AddPoints(point1, point2) { 74 | return new Point(point1.x + point2.x, point1.y + point2.y); 75 | } 76 | 77 | static doMove(actor, layer, desiredLocation) { 78 | // Remove it from the current location 79 | layer.destroyTile(actor.location.x, actor.location.y); 80 | 81 | // Drop it in the new location 82 | layer.placeActor(actor, new Point(desiredLocation.x, desiredLocation.y)); 83 | 84 | return true; 85 | } 86 | 87 | static TryMove(actor, layer, desiredLocation) { 88 | 89 | var movingInto = layer.getTile(desiredLocation.x, desiredLocation.y); 90 | 91 | // If nothing is there, then move 92 | if (movingInto === null) { 93 | return this.doMove(actor, layer, desiredLocation); 94 | } 95 | // Else, collide 96 | else { 97 | 98 | // If we move into an Chest on the world, pick up the items inside (open it) and dont do the movement 99 | if (movingInto instanceof Chest) { 100 | movingInto.pickedUpBy(actor); 101 | return false; 102 | } 103 | 104 | // If we move into any generic Item on the world, pick up the item and allow the movement 105 | if (movingInto instanceof WorldItem) { 106 | movingInto.pickedUpBy(actor); 107 | return this.doMove(actor, layer, desiredLocation); 108 | } 109 | return false; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Helpers/Numbers.ts: -------------------------------------------------------------------------------- 1 | class Numbers{ 2 | static roundToOdd(i: number){ 3 | return 2* Math.floor(i/2) + 1; 4 | } 5 | static isNumber(obj: Object) { 6 | return obj!== undefined && typeof(obj) === 'number' && !isNaN(obj); 7 | } 8 | static isOdd(obj: number){ 9 | return Numbers.isNumber(obj) && obj % 2; 10 | } 11 | static isEven(obj: number){ 12 | return Numbers.isNumber(obj) && ! (obj%2); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Helpers/Random.ts: -------------------------------------------------------------------------------- 1 | class Random { 2 | seed: number; 3 | 4 | constructor(seed: number) { 5 | this.seed = seed; 6 | } 7 | 8 | go() { 9 | // Fetches the next random number in the sequence 10 | let x = Math.sin(this.seed++) * 10000; 11 | return x - Math.floor(x); 12 | } 13 | 14 | next(min: number, max: number) { 15 | // Fetches the next random number in the sequence, with a minimum and maximum 16 | return Math.floor(this.go() * (max - min + 1)) + min; 17 | } 18 | 19 | nextWeighted(min: number, max: number) { 20 | // Fetches the next random number in the sequence, with a minimum and maximum, and weighted 21 | // towards the center. 22 | // This is done by generating only two numbers at half the maximum and adding them together 23 | return Math.floor((this.next(min, max) + this.next(min, max)) / 2); 24 | } 25 | 26 | // wasLucky(2/3) return if something was true with a 66% chance of being true 27 | wasLucky(numerator: number, denominator: number) { 28 | var percent = Math.ceil((numerator / denominator) * 100); 29 | return this.next(1, 100) <= percent; 30 | } 31 | 32 | wasLuckyPercent(percent: number) { 33 | return this.next(1, 100) <= percent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Helpers/Rendering.ts: -------------------------------------------------------------------------------- 1 | class Rendering { 2 | static SliceLayersToSize(game: Game, layers: Layer[], centerPoint: Point, width: number, height: number) { 3 | var slicedLayers = []; 4 | 5 | for (var i = 0; i < layers.length; i++) { 6 | // For reference 7 | var layer = layers[i]; 8 | 9 | // Our new layer 10 | var slicedLayer = new Layer(width, height, layer.zIndex, layer.name, layer.type); 11 | 12 | // Draw out the actors in-view from the layer 13 | var trimmedXToWrite = 0; 14 | var trimmedYToWrite = 0; 15 | 16 | var topPosition = centerPoint.y - Math.floor(height / 2); 17 | var bottomPosition = centerPoint.y + Math.ceil(height / 2); 18 | var leftPosition = centerPoint.x - Math.floor(width / 2); 19 | var rightPosition = centerPoint.x + Math.ceil(width / 2); 20 | 21 | for (var y = topPosition; y < bottomPosition; y++) { 22 | for (var x = leftPosition; x < rightPosition; x++) { 23 | if (x >= 0 && x < layer.width && y >= 0 && y < layer.height) { 24 | slicedLayer.setTile(trimmedXToWrite, trimmedYToWrite, layer.getTile(x, y)); 25 | } 26 | else { 27 | slicedLayer.setTile(trimmedXToWrite, trimmedYToWrite, new OutOfBounds(game)); 28 | } 29 | trimmedXToWrite++; 30 | } 31 | 32 | trimmedXToWrite = 0; 33 | trimmedYToWrite++; 34 | } 35 | 36 | slicedLayers.push(slicedLayer); 37 | } 38 | 39 | return slicedLayers; 40 | } 41 | 42 | static fogSprite(sprite: any, fogged: boolean, fogStyle: FogStyle) { 43 | if (fogStyle === FogStyle.Hide) { 44 | sprite.visible = !fogged; 45 | } 46 | if (fogStyle === FogStyle.Darken) { 47 | sprite.tint = fogged ? LightColorCode.Black : LightColorCode.White; 48 | } 49 | } 50 | 51 | static darkenSpriteByDistanceFromLightSource(sprite: Sprite, spriteActor: Actor, lightSourceActor: Actor, fallOffFunction) { 52 | var darkColor = LightColorCode.Black; 53 | if (spriteActor !== null && lightSourceActor !== null && spriteActor.location !== null && lightSourceActor.location !== null) { 54 | var currentTint = sprite.tint; 55 | var darkenAmount = 1 - Geometry.getBrightnessForPoint(spriteActor.location, lightSourceActor.location, lightSourceActor.viewRadius, 1, fallOffFunction); 56 | var newTint = Color.shadeBlendInt(darkenAmount, currentTint, darkColor); // blend that much blackness into it to darken it 57 | 58 | sprite.tint = newTint; 59 | } 60 | } 61 | 62 | static lightSpriteByDistanceFromLightSource(sprite: Sprite, spriteActor: Actor, lightSourceActor: Actor, color: number, fallOffFunction, intensity?: number) { 63 | if (spriteActor !== null && lightSourceActor !== null && spriteActor.location !== null && lightSourceActor.location !== null) { 64 | if (!intensity) { 65 | intensity = 1.0; 66 | } 67 | var currentTint = sprite.tint; 68 | var darkenAmount = Geometry.getBrightnessForPoint(spriteActor.location, lightSourceActor.location, lightSourceActor.viewRadius, 1, fallOffFunction) * intensity; 69 | if (darkenAmount > 0.0) { 70 | var newTint = Color.shadeBlendInt(darkenAmount, currentTint, color); // blend that much blackness into it to darken it 71 | sprite.tint = newTint; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Helpers/XP.ts: -------------------------------------------------------------------------------- 1 | class XP { 2 | // Return the XP needed for the next level progression 3 | static getExperiencePointsRequired(currentLevel: number, starterXP: number = 4) { 4 | return starterXP + Math.pow(currentLevel, 2) / 2; 5 | //return starterXP + currentLevel / 4; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Inventory/Base/InventoryItem.ts: -------------------------------------------------------------------------------- 1 | class InventoryItem { 2 | holder: Actor = null; 3 | protected name: string = ''; 4 | spritesets: SpriteSet[] = null; 5 | randomSpriteIndex: number = 0; 6 | random: Random; 7 | 8 | constructor(random: Random) { 9 | this.random = random; 10 | } 11 | 12 | getName() { 13 | return this.name; 14 | } 15 | 16 | setSprite() { 17 | // Set the random sprite to use on creation 18 | this.randomSpriteIndex = this.random.next(0, this.spritesets.first().sprites.length - 1); 19 | } 20 | 21 | getSprite() { 22 | if (this.spritesets !== null) { 23 | return this.spritesets.first().sprites[this.randomSpriteIndex]; 24 | } 25 | else { 26 | return null; 27 | } 28 | } 29 | } 30 | 31 | class Ammo extends InventoryItem { 32 | static friendlyName: string = 'Ammo'; 33 | constructor(random: Random) { 34 | super(random); 35 | } 36 | } 37 | 38 | 39 | class Consumable extends InventoryItem { 40 | usesRemaining: number = 1; 41 | 42 | constructor(random: Random) { 43 | super(random); 44 | } 45 | 46 | use() { 47 | this.usesRemaining--; 48 | if (this.usesRemaining <= 0) { 49 | this.holder.inventory.remove(this); 50 | } 51 | } 52 | } 53 | 54 | class Equipment extends InventoryItem { 55 | equipPoint: EquipPoint; 56 | buffs: Buff[] = []; 57 | isEquipped: boolean = false; 58 | constructor(random: Random) { 59 | super(random); 60 | } 61 | equip() { 62 | this.isEquipped = true; 63 | } 64 | 65 | giveHolderBuffs() { 66 | // Apply any buffs with remaining uses 67 | this.buffs 68 | .where((buff) => { 69 | return buff.getUsesRemaining() > 0 70 | }) 71 | .forEach((buff) => { 72 | this.holder.addBuff( 73 | buff, // the buff 74 | this // who granted.caused it 75 | ) 76 | }); 77 | this.cleanEmptyBuffs(); 78 | } 79 | 80 | removeHolderBuffs() { 81 | // Remove any buffs 82 | this.buffs.forEach((buff) => { this.holder.removeBuff(buff) }); 83 | this.cleanEmptyBuffs(); 84 | } 85 | 86 | unequip() { 87 | this.isEquipped = false; 88 | this.removeHolderBuffs(); 89 | } 90 | 91 | cleanEmptyBuffs() { 92 | this.buffs 93 | .where((buff) => { 94 | return buff.getUsesRemaining() <= 0 95 | }) 96 | .forEach((buff) => { 97 | this.removeBuff(buff) 98 | }); 99 | } 100 | 101 | // Override to include buff name 102 | getName() { 103 | if (this.buffs.any() && this.buffs.first().getUsesRemaining() > 0) { 104 | return this.name + ' of ' + this.buffs.first().namePart; 105 | } 106 | else { 107 | return this.name; 108 | } 109 | } 110 | 111 | addBuff(buff: Buff) { 112 | this.buffs.push(buff); 113 | } 114 | 115 | removeBuff(buff: Buff) { 116 | this.buffs.remove(buff); 117 | } 118 | } 119 | 120 | class Weapon extends Equipment { 121 | static friendlyName: string = 'Weapon'; 122 | attackPower: number = 0; 123 | equipPoint: EquipPoint = EquipPoint.Weapon; 124 | constructor(random: Random) { 125 | super(random); 126 | } 127 | 128 | equip() { 129 | // Toggle if this is what is equipped 130 | if (this.holder.equippedWeapon === this) { 131 | this.unequip(); 132 | return; 133 | } 134 | else { 135 | // Unequip anything already in this spot 136 | if (this.holder.equippedWeapon !== null) { 137 | this.holder.equippedWeapon.unequip(); 138 | } 139 | } 140 | this.holder.equippedWeapon = this; 141 | this.giveHolderBuffs(); 142 | this.isEquipped = true; 143 | } 144 | 145 | unequip() { 146 | this.holder.equippedWeapon = null; 147 | super.unequip(); 148 | } 149 | } 150 | 151 | class Armor extends Equipment { 152 | maxHealthBuff: number = 0; 153 | constructor(healthBuff: number, random: Random) { 154 | super(random); 155 | this.maxHealthBuff = healthBuff; 156 | } 157 | 158 | equip() { 159 | // Toggle off is this item is already equipped 160 | if (this.equipPoint === EquipPoint.Feet && this.holder.equippedFeet === this) { 161 | this.holder.equippedFeet.unequip(); 162 | return; 163 | } 164 | if (this.equipPoint === EquipPoint.Hands && this.holder.equippedHands === this) { 165 | this.holder.equippedHands.unequip(); 166 | return; 167 | } 168 | if (this.equipPoint === EquipPoint.Head && this.holder.equippedHead === this) { 169 | this.holder.equippedHead.unequip(); 170 | return; 171 | } 172 | if (this.equipPoint === EquipPoint.Legs && this.holder.equippedLegs === this) { 173 | this.holder.equippedLegs.unequip(); 174 | return; 175 | } 176 | if (this.equipPoint === EquipPoint.Torso && this.holder.equippedTorso === this) { 177 | this.holder.equippedTorso.unequip(); 178 | return; 179 | } 180 | 181 | // Otherwise unequip anything already in this spot 182 | if (this.equipPoint === EquipPoint.Feet && this.holder.equippedFeet !== null) { 183 | this.holder.equippedFeet.unequip(); 184 | } 185 | if (this.equipPoint === EquipPoint.Hands && this.holder.equippedHands !== null) { 186 | this.holder.equippedHands.unequip(); 187 | } 188 | if (this.equipPoint === EquipPoint.Head && this.holder.equippedHead !== null) { 189 | this.holder.equippedHead.unequip(); 190 | } 191 | if (this.equipPoint === EquipPoint.Legs && this.holder.equippedLegs !== null) { 192 | this.holder.equippedLegs.unequip(); 193 | } 194 | if (this.equipPoint === EquipPoint.Torso && this.holder.equippedTorso !== null) { 195 | this.holder.equippedTorso.unequip(); 196 | } 197 | 198 | // Then equip this 199 | this.updateHolder(this.equipPoint, this, true); 200 | this.isEquipped = true; 201 | this.giveHolderBuffs(); 202 | } 203 | 204 | unequip() { 205 | super.unequip(); 206 | this.updateHolder(this.equipPoint, null, false); 207 | 208 | // Lower the heatlh down if this is buffing it beyond max 209 | if (this.holder.health > this.holder.maxHealth()) { 210 | this.holder.health = this.holder.maxHealth(); 211 | } 212 | } 213 | 214 | updateHolder(point: EquipPoint, armor: Armor, equipped: boolean) { 215 | switch (point) { 216 | case EquipPoint.Head: 217 | this.holder.equippedHead = equipped ? armor : null; 218 | break; 219 | case EquipPoint.Torso: 220 | this.holder.equippedTorso = equipped ? armor : null; 221 | break; 222 | case EquipPoint.Legs: 223 | this.holder.equippedLegs = equipped ? armor : null; 224 | break; 225 | case EquipPoint.Hands: 226 | this.holder.equippedHands = equipped ? armor : null; 227 | break; 228 | case EquipPoint.Feet: 229 | this.holder.equippedFeet = equipped ? armor : null; 230 | break; 231 | } 232 | } 233 | } 234 | 235 | class HandArmor extends Armor { //Gauntlets 236 | static friendlyName: string = 'Hand Armor'; 237 | equipPoint: EquipPoint = EquipPoint.Hands; 238 | 239 | constructor(healthBuff: number, random: Random) { 240 | super(healthBuff, random); 241 | } 242 | } 243 | 244 | class LegArmor extends Armor { //Pants 245 | static friendlyName: string = 'Leg Armor'; 246 | equipPoint: EquipPoint = EquipPoint.Legs; 247 | 248 | constructor(healthBuff: number, random: Random) { 249 | super(healthBuff, random); 250 | } 251 | } 252 | 253 | class HeadArmor extends Armor { //Helmet 254 | static friendlyName: string = 'Head Armor'; 255 | equipPoint: EquipPoint = EquipPoint.Head; 256 | 257 | constructor(healthBuff: number, random: Random) { 258 | super(healthBuff, random); 259 | } 260 | } 261 | 262 | class TorsoArmor extends Armor { //Shirt 263 | static friendlyName: string = 'Torso Armor'; 264 | equipPoint: EquipPoint = EquipPoint.Torso; 265 | 266 | constructor(healthBuff: number, random: Random) { 267 | super(healthBuff, random); 268 | } 269 | } 270 | 271 | class FootArmor extends Armor { //Boots 272 | static friendlyName: string = 'Foot Armor'; 273 | equipPoint: EquipPoint = EquipPoint.Feet; 274 | 275 | constructor(healthBuff: number, random: Random) { 276 | super(healthBuff, random); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Inventory/Consumables/Potion.ts: -------------------------------------------------------------------------------- 1 | /// 2 | class Potion extends Consumable { 3 | healAmount: number; 4 | spritesets: SpriteSet[] = Sprites.PotionSprites(); 5 | constructor(random: Random) { 6 | super(random); 7 | this.name = 'Potion'; 8 | this.healAmount = 10; 9 | this.setSprite(); 10 | } 11 | 12 | use() { 13 | super.use(); 14 | if (this.holder !== null) { 15 | this.holder.heal(this.healAmount); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Inventory/Equippables/Ammo/InventoryArrow.ts: -------------------------------------------------------------------------------- 1 | /// 2 | class InventoryArrow extends Ammo { 3 | spritesets: SpriteSet[] = Sprites.ArrowSprites(); 4 | constructor(random: Random) { 5 | super(random); 6 | this.name = 'Arrow'; 7 | this.setSprite(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Inventory/Equippables/Armor.ts: -------------------------------------------------------------------------------- 1 | /// 2 | class Shirt extends TorsoArmor { 3 | name: string = "Shirt"; 4 | spritesets: SpriteSet[] = Sprites.ShirtSprites(); 5 | 6 | constructor(healthBuff: number, random: Random) { 7 | super(healthBuff, random); 8 | this.setSprite(); 9 | } 10 | } 11 | 12 | class Chestplate extends TorsoArmor { 13 | name: string = "Chestplate"; 14 | spritesets: SpriteSet[] = Sprites.ChestplaceSprites(); 15 | 16 | constructor(healthBuff: number, random: Random) { 17 | super(healthBuff, random); 18 | this.setSprite(); 19 | } 20 | } 21 | 22 | class LeatherBoots extends FootArmor { 23 | name: string = "Leather Boots"; 24 | spritesets: SpriteSet[] = Sprites.LeatherBootsSprites(); 25 | 26 | constructor(healthBuff: number, random: Random) { 27 | super(healthBuff, random); 28 | this.setSprite(); 29 | } 30 | } 31 | 32 | class SteelBoots extends FootArmor { 33 | name: string = "Steel Boots"; 34 | spritesets: SpriteSet[] = Sprites.SteelBootsSprites(); 35 | 36 | constructor(healthBuff: number, random: Random) { 37 | super(healthBuff, random); 38 | this.setSprite(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Inventory/Equippables/Weapon.ts: -------------------------------------------------------------------------------- 1 | /// 2 | class Dagger extends Weapon { 3 | name: string = "Dagger"; 4 | attackPower: number = 2; 5 | spritesets: SpriteSet[] = Sprites.DaggerSprites(); 6 | 7 | constructor(random: Random, attackPower: number) { 8 | super(random); 9 | this.attackPower = attackPower; 10 | } 11 | } 12 | 13 | class Projectile extends Weapon { 14 | successRatePercent: number = 0; 15 | ammoType: any; 16 | 17 | constructor(random: Random, attackPower: number) { 18 | super(random); 19 | this.attackPower = attackPower; 20 | } 21 | } 22 | 23 | class Bow extends Projectile { 24 | name: string = "Bow"; 25 | attackPower: number = 2; 26 | successRatePercent: number = 75; 27 | spritesets: SpriteSet[] = Sprites.BowSprites(); 28 | ammoType: any = InventoryArrow; 29 | 30 | constructor(random: Random, attackPower: number) { 31 | super(random, attackPower); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Layer.ts: -------------------------------------------------------------------------------- 1 | class Layer { 2 | width: number; 3 | height: number; 4 | zIndex: number; 5 | name: string; 6 | type: LayerType; 7 | tiles: any[]; 8 | 9 | constructor(width: number, height: number, zIndex: number, name: string, type: LayerType) { 10 | this.width = width; 11 | this.height = height; 12 | this.zIndex = zIndex; 13 | this.name = name; 14 | this.type = type; 15 | this.tiles = []; 16 | 17 | // Clear it initially (initialize with all nulls) 18 | this.clear(); 19 | } 20 | 21 | // Don't use this externally, as any time we place an actor down, we want to 22 | // make the actor aware of it's layer and location. Use placeActor instead. 23 | setTile(x: number, y: number, actor: Actor) { 24 | this.tiles[y][x] = actor; 25 | } 26 | 27 | destroyTile(x: number, y: number) { 28 | this.setTile(x, y, null); 29 | } 30 | 31 | getTile(x: number, y: number): Actor { 32 | var vert = this.tiles[y]; 33 | if (vert) { 34 | return vert[x]; 35 | } 36 | else { 37 | return null; 38 | } 39 | } 40 | 41 | // Will fill with unaware actors (no layer or world or game contexts) 42 | fillWith(actorType: any, gameReference: Game) { 43 | this.tiles = []; 44 | for (var y = 0; y < this.height; y++) { 45 | var newRow = []; 46 | for (var x = 0; x < this.width; x++) { 47 | if (actorType !== null) { 48 | var actor = new actorType(gameReference); 49 | actor.location = new Point(x, y); 50 | actor.layer = this; 51 | newRow.push(actor); 52 | } 53 | else { 54 | newRow.push(null); 55 | } 56 | } 57 | this.tiles.push(newRow); 58 | } 59 | } 60 | 61 | placeActor(actor: Actor, location: Point) { 62 | this.setTile(location.x, location.y, actor); 63 | if (actor !== null) { 64 | // If we're placing for the first time, amke note of it as the actor's home 65 | if (actor.location === null) { 66 | actor.home = location; 67 | } 68 | actor.location = location; 69 | actor.layer = this; 70 | } 71 | } 72 | 73 | clear() { 74 | this.fillWith(null, null); 75 | } 76 | 77 | // Generate a collision grid of 0s and 1s for pathfinding through 78 | getCollisionGrid(startPoint: Point, endPoint: Point) { 79 | var grid = []; 80 | for (var y = 0; y < this.tiles.length; y++) { 81 | var row = []; 82 | for (var x = 0; x < this.tiles[y].length; x++) { 83 | var actor = this.getTile(x, y); 84 | if ( // Consider this space temporarily free IF 85 | actor === null || // There's nothing here 86 | actor instanceof Door || // Or there is and it's a door (it can be opened) 87 | (actor.status === ActorStatus.Moving) || // Or there is and it's moving and might be free by the time we get there 88 | (x == startPoint.x && y == startPoint.y) || // Or there is and it's the start/end point to ignore 89 | (x == endPoint.x && y == endPoint.y)) // Or there is and it's the start/end point to ignore 90 | { 91 | row.push(PathfinderTile.Walkable); 92 | } 93 | else { 94 | row.push(PathfinderTile.Unwalkable); 95 | } 96 | } 97 | grid.push(row); 98 | } 99 | return grid; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Menu/Menu.ts: -------------------------------------------------------------------------------- 1 | class Menu { 2 | pages: any[]; 3 | game: Game; 4 | navStack: any[] = []; 5 | previousSelectedOptionIndexStack: any[] = []; 6 | selectedOptionIndex: number = 0; 7 | charWidth: number = 50; 8 | 9 | constructor(pages: any[]) { 10 | this.pages = this.init(pages); 11 | this.game = null; // null reference initially 12 | this.resetNavStack(); 13 | } 14 | 15 | // Attributes 16 | currentPage() { 17 | return this.navStack[0]; 18 | } 19 | currentOption() { 20 | var options = Generic.ResolveIfDynamic(this.currentPage().options); 21 | return options[this.selectedOptionIndex]; 22 | } 23 | getPreviousPageSelectedIndex() { 24 | if (this.previousSelectedOptionIndexStack.length > 0) { 25 | return this.previousSelectedOptionIndexStack[0]; 26 | } 27 | else { 28 | return 0; 29 | } 30 | } 31 | 32 | 33 | // Helpers 34 | resetNavStack() { 35 | this.selectedOptionIndex = 0; 36 | this.navStack = [this.pages[0]]; 37 | this.previousSelectedOptionIndexStack = []; 38 | } 39 | containCursor() { 40 | var options = Generic.ResolveIfDynamic(this.currentPage().options); 41 | 42 | if (this.selectedOptionIndex < 0) { 43 | this.selectedOptionIndex = options.length - 1; 44 | } 45 | else if (this.selectedOptionIndex >= options.length) { 46 | this.selectedOptionIndex = 0; 47 | } 48 | } 49 | 50 | // because we define in a JSON format, we can't pass a reference. 51 | // Initialize the options in the menu by setting their menu refrence 52 | init(pages: any[]) { 53 | var self = this; 54 | for (var p = 0; p < pages.length; p++) { 55 | var page = pages[p]; 56 | for (var o = 0; o < page.options.length; o++) { 57 | var option = page.options[o]; 58 | option.menu = self; 59 | } 60 | } 61 | return pages; 62 | } 63 | 64 | linkToGame(game: Game) { 65 | this.game = game; 66 | } 67 | 68 | // Option navigation 69 | navUp() { 70 | this.selectedOptionIndex--; 71 | this.containCursor(); 72 | if (this.currentOption().execute === undefined) { 73 | // If there's no action, dont let it be selected 74 | this.navUp(); 75 | } 76 | } 77 | navDown() { 78 | this.selectedOptionIndex++; 79 | this.containCursor(); 80 | if (this.currentOption().execute === undefined) { 81 | // If there's no action, dont let it be selected 82 | this.navDown(); 83 | } 84 | } 85 | executeCurrentOption() { 86 | var option = this.currentOption(); 87 | if (option !== undefined && option !== null) { 88 | option.execute(); 89 | } 90 | } 91 | 92 | // Page navigation 93 | navToPage(pageId: string) { 94 | var page = this.pages.filter(function(page) { return page.id === pageId })[0]; 95 | 96 | this.navStack.unshift(page); 97 | this.previousSelectedOptionIndexStack.unshift(this.selectedOptionIndex); 98 | 99 | this.selectedOptionIndex = 0; 100 | if (this.currentOption().execute === undefined) { 101 | // If there's no action, dont let it be selected and go down to the first it can 102 | this.navDown(); 103 | } 104 | } 105 | goBackAPage() { 106 | if (this.navStack.length > 1) { 107 | this.selectedOptionIndex = this.getPreviousPageSelectedIndex(); 108 | 109 | this.navStack.shift(); 110 | this.previousSelectedOptionIndexStack.shift(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Menu/Menus/InventoryMenu.ts: -------------------------------------------------------------------------------- 1 | // 2 | var InventoryMenu = new Menu([ 3 | { 4 | id: "inventory", 5 | name: "Inventory", 6 | options: [ 7 | { 8 | label: function() { return "Equipment" + "(" + InventoryMenu.game.player.getInventoryOfType(Equipment).length + ")" }, 9 | execute: function() { 10 | InventoryMenu.navToPage("equipmentMenu"); 11 | } 12 | }, 13 | { 14 | label: function() { return "Consumables" + "(" + InventoryMenu.game.player.getInventoryOfType(Consumable).length + ")" }, 15 | execute: function() { 16 | InventoryMenu.navToPage("consumablesMenu"); 17 | } 18 | }, 19 | ] 20 | }, 21 | { 22 | id: "equipmentMenu", 23 | name: "Equipment", 24 | options: function() { 25 | var options = []; 26 | 27 | var equipmentTypesToShow = [ 28 | Weapon, 29 | HeadArmor, 30 | TorsoArmor, 31 | LegArmor, 32 | FootArmor, 33 | HandArmor, 34 | Ammo 35 | ]; 36 | 37 | for (let t = 0; t < equipmentTypesToShow.length; t++) { 38 | var type = equipmentTypesToShow[t]; 39 | 40 | // Options 41 | var equipment = InventoryMenu.game.player.getInventoryOfType(type); 42 | 43 | // HEADER 44 | if (equipment.any()) { 45 | // Type Header 46 | options.push( 47 | { 48 | menu: InventoryMenu, // set up reference live 49 | label: type.friendlyName, 50 | } 51 | ); 52 | } 53 | 54 | // If it's ammo, then aggregate together 55 | if (type === Ammo) { 56 | var text = ''; 57 | var equipmentNames = equipment.select((eq) => { return eq.getName() }); 58 | 59 | var counts = equipmentNames.reduce((countMap, word) => { countMap[word] = ++countMap[word] || 1; return countMap }, {}); 60 | for (var item in counts) { 61 | if (counts.hasOwnProperty(item)) { 62 | options.push({ 63 | menu: InventoryMenu, 64 | label: ' └──' + item + '(x' + counts[item] + ')', 65 | execute: function() { } 66 | }); 67 | } 68 | } 69 | } 70 | else { 71 | for (let i = 0; i < equipment.length; i++) { 72 | let inventoryItem: Equipment = equipment[i]; 73 | options.push( 74 | { 75 | menu: InventoryMenu, // set up reference live 76 | label: function() { 77 | if (inventoryItem instanceof Armor) { 78 | return ' └──' + inventoryItem.getName() + // Name 79 | '(+' + inventoryItem.maxHealthBuff + ' HP)' + // Buff 80 | (inventoryItem.isEquipped ? ' (equipped)' : ''); // Equipped status 81 | } 82 | 83 | else if (inventoryItem instanceof Weapon) { 84 | return ' └──' + inventoryItem.getName() + // Name 85 | '(+' + inventoryItem.attackPower + ' AP)' + // AP 86 | (inventoryItem.isEquipped ? ' (equipped)' : ''); // Equipped status 87 | } 88 | 89 | else if (inventoryItem instanceof Ammo) { 90 | return ' └──' + inventoryItem.getName() // Name 91 | } 92 | 93 | }, 94 | execute: function() { 95 | inventoryItem.equip(); 96 | this.menu.containCursor(); 97 | } 98 | } 99 | ); 100 | } 101 | } 102 | } 103 | 104 | return options; 105 | } 106 | }, 107 | 108 | { 109 | id: "consumablesMenu", 110 | name: "Consumables", 111 | options: function() { 112 | var options = []; 113 | var items = InventoryMenu.game.player.getInventoryOfType(Consumable); 114 | for (let i = 0; i < items.length; i++) { 115 | let inventoryItem: Consumable = items[i]; 116 | options.push( 117 | { 118 | menu: InventoryMenu, // set up reference live 119 | label: function() { 120 | return inventoryItem.getName(); 121 | }, 122 | execute: function() { 123 | inventoryItem.use(); 124 | this.menu.containCursor(); 125 | } 126 | } 127 | ); 128 | } 129 | return options; 130 | } 131 | } 132 | ]); 133 | -------------------------------------------------------------------------------- /src/Menu/Menus/MainMenu.ts: -------------------------------------------------------------------------------- 1 | // 2 | var MainMenu = new Menu([ 3 | { 4 | id: "mainmenu", 5 | name: "Main Menu", 6 | options: [ 7 | { 8 | label: "Resume", 9 | execute: function() { 10 | MainMenu.game.state = GameState.Playing; 11 | } 12 | }, 13 | { 14 | label: "Options...", 15 | execute: function() { 16 | MainMenu.navToPage("options"); 17 | } 18 | }, 19 | { 20 | label: "Start Over", 21 | execute: function() { 22 | MainMenu.navToPage("reset"); 23 | } 24 | } 25 | ] 26 | }, 27 | 28 | { 29 | id: "reset", 30 | name: "Start Over?", 31 | options: [ 32 | { 33 | label: "No", 34 | execute: function() { 35 | MainMenu.goBackAPage(); 36 | } 37 | }, 38 | { 39 | label: "Yes", 40 | execute: function() { 41 | MainMenu.game.player.reset(); 42 | MainMenu.game.generateNextDungeon(); 43 | MainMenu.game.unpause(); 44 | MainMenu.game.gameTick(); 45 | MainMenu.resetNavStack(); 46 | } 47 | } 48 | ] 49 | }, 50 | 51 | { 52 | id: "options", 53 | name: "Options", 54 | options: [ 55 | { 56 | label: "Minimap...", 57 | execute: function() { 58 | MainMenu.navToPage("minimapOptions"); 59 | } 60 | }, 61 | 62 | { 63 | label: "Graphics...", 64 | execute: function() { 65 | MainMenu.navToPage("graphicOptions"); 66 | } 67 | }, 68 | 69 | 70 | { 71 | label: "(back)", 72 | execute: function() { 73 | MainMenu.goBackAPage(); 74 | } 75 | }, 76 | 77 | ] 78 | }, 79 | 80 | { 81 | id: "graphicOptions", 82 | name: "Graphics", 83 | options: [ 84 | 85 | { 86 | label: function() { 87 | return ( 88 | (MainMenu.game.settings.graphic.showHealth ? "Hide" : "Show") + 89 | " health pips" 90 | ); 91 | }, 92 | execute: function() { 93 | MainMenu.game.settings.graphic.showHealth = !MainMenu.game.settings.graphic.showHealth; 94 | MainMenu.game.saveSettings(); 95 | } 96 | }, 97 | 98 | { 99 | label: function() { 100 | return ( 101 | (MainMenu.game.settings.graphic.showLighting ? "Hide" : "Show") + 102 | " dynamic lighting" 103 | ); 104 | }, 105 | execute: function() { 106 | MainMenu.game.settings.graphic.showLighting = !MainMenu.game.settings.graphic.showLighting; 107 | MainMenu.game.saveSettings(); 108 | } 109 | }, 110 | 111 | { 112 | label: function() { 113 | return ( 114 | "└──" + 115 | (MainMenu.game.settings.graphic.showColoredLighting ? "Hide" : "Show") + 116 | " colored lighting" 117 | ); 118 | }, 119 | execute: function() { 120 | MainMenu.game.settings.graphic.showColoredLighting = !MainMenu.game.settings.graphic.showColoredLighting; 121 | MainMenu.game.saveSettings(); 122 | }, 123 | visible: function() { return MainMenu.game.settings.graphic.showLighting } 124 | }, 125 | 126 | { 127 | label: "(back)", 128 | execute: function() { 129 | MainMenu.goBackAPage(); 130 | } 131 | }, 132 | 133 | ] 134 | }, 135 | 136 | { 137 | id: "minimapOptions", 138 | name: "Minimap", 139 | options: [ 140 | { 141 | label: function() { 142 | return ( 143 | (MainMenu.game.settings.minimap.visible ? "Hide" : "Show") + 144 | " minimap" 145 | ); 146 | }, 147 | execute: function() { 148 | MainMenu.game.settings.minimap.visible = !MainMenu.game.settings.minimap.visible; 149 | MainMenu.game.saveSettings(); 150 | } 151 | }, 152 | 153 | { 154 | label: function() { 155 | return ( 156 | "├──Minimap size: " + MainMenu.game.settings.minimap.size 157 | ); 158 | }, 159 | execute: function() { 160 | MainMenu.game.settings.minimap.size += 0.5; 161 | if (MainMenu.game.settings.minimap.size > 3) MainMenu.game.settings.minimap.size = 0.5; 162 | MainMenu.game.saveSettings(); 163 | }, 164 | visible: function() { return MainMenu.game.settings.minimap.visible } 165 | }, 166 | 167 | { 168 | label: function() { 169 | return ( 170 | "├──Minimap opacity: " + MainMenu.game.settings.minimap.opacity 171 | ); 172 | }, 173 | execute: function() { 174 | MainMenu.game.settings.minimap.opacity += 0.1; 175 | MainMenu.game.settings.minimap.opacity = Math.round(MainMenu.game.settings.minimap.opacity * 10) / 10 // nearest 1 decimal place 176 | if (MainMenu.game.settings.minimap.opacity > 1) MainMenu.game.settings.minimap.opacity = 0.1; 177 | MainMenu.game.saveSettings(); 178 | }, 179 | visible: function() { return MainMenu.game.settings.minimap.visible } 180 | }, 181 | 182 | { 183 | label: function() { 184 | var cornerEnglish; 185 | switch (MainMenu.game.settings.minimap.position) { 186 | case Corner.TopLeft: 187 | cornerEnglish = 'top-left'; 188 | break; 189 | case Corner.TopRight: 190 | cornerEnglish = 'top-right'; 191 | break; 192 | case Corner.BottomLeft: 193 | cornerEnglish = 'bottom-left'; 194 | break; 195 | case Corner.BottomRight: 196 | cornerEnglish = 'bottom-right'; 197 | break; 198 | } 199 | return ( 200 | "└──Minimap position: " + cornerEnglish 201 | ); 202 | }, 203 | execute: function() { 204 | MainMenu.game.settings.minimap.position++; 205 | if (MainMenu.game.settings.minimap.position > 3) MainMenu.game.settings.minimap.position = 0; 206 | MainMenu.game.saveSettings(); 207 | }, 208 | visible: function() { return MainMenu.game.settings.minimap.visible } 209 | }, 210 | 211 | { 212 | label: "(back)", 213 | execute: function() { 214 | MainMenu.goBackAPage(); 215 | } 216 | }, 217 | ] 218 | } 219 | ]); 220 | -------------------------------------------------------------------------------- /src/Menu/SelectableActorGroup.ts: -------------------------------------------------------------------------------- 1 | class SelectableActorGroup { 2 | game: Game; 3 | selectableActors: Actor[]; 4 | selectedIndex: number = null; 5 | selectedActor: Actor = null; 6 | 7 | constructor(game: Game) { 8 | this.game = game; 9 | this.selectableActors = []; 10 | } 11 | 12 | private wrapIndex() { 13 | if (this.selectedIndex != null) { 14 | if (this.selectedIndex >= this.selectableActors.length) { 15 | this.selectedIndex = 0; 16 | } 17 | if (this.selectedIndex < 0) { 18 | this.selectedIndex = this.selectableActors.length - 1; 19 | } 20 | } 21 | } 22 | 23 | next(): void { 24 | if (this.selectedIndex === null) { 25 | this.selectedIndex = 0; 26 | } 27 | else { 28 | this.selectedIndex++; 29 | } 30 | 31 | this.wrapIndex(); 32 | this.selectedActor = this.selectableActors[this.selectedIndex]; 33 | } 34 | 35 | previous(): void { 36 | if (this.selectedIndex === null) { 37 | this.selectedIndex = 0; 38 | } 39 | else { 40 | this.selectedIndex--; 41 | } 42 | this.wrapIndex(); 43 | this.selectedActor = this.selectableActors[this.selectedIndex]; 44 | } 45 | 46 | clear(): void { 47 | this.selectedIndex = null; 48 | this.selectedActor = null; 49 | } 50 | 51 | setGroup(newGroup: Actor[]): void { 52 | if (this.selectableActors.length === 0) { 53 | this.selectableActors = newGroup; 54 | } 55 | else if (newGroup.length === 0) { 56 | this.selectableActors = []; 57 | this.selectedActor = null; 58 | this.selectedIndex = null; 59 | } 60 | else { 61 | // Remove any ones in here but not newGroup 62 | var toRemove: Actor[] = []; 63 | toRemove = this.selectableActors.where( 64 | (localActor) => { return !newGroup.contains(localActor) } 65 | ); 66 | for (let r = 0; r <= toRemove.length; r++) { 67 | this.selectableActors.remove(toRemove[r]); 68 | } 69 | 70 | // Add ones in newGroup not here 71 | var toAdd: Actor[] = []; 72 | toAdd = newGroup.where( 73 | (newActor) => { return !this.selectableActors.contains(newActor) } 74 | ); 75 | for (let r = 0; r <= toAdd.length; r++) { 76 | if (toAdd[r] != undefined && toAdd[r] != null) { 77 | this.selectableActors.push(toAdd[r]); 78 | } 79 | } 80 | } 81 | } 82 | 83 | clearGroup(): void { 84 | this.selectableActors = []; 85 | this.selectedActor = null; 86 | this.selectedIndex = null; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Point.ts: -------------------------------------------------------------------------------- 1 | class Point { 2 | 3 | x: number; 4 | y: number; 5 | 6 | constructor(x: number, y: number) { 7 | this.x = x; 8 | this.y = y; 9 | } 10 | 11 | static getDistanceBetweenPoints(point1: Point, point2: Point) { 12 | return Math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)); 13 | } 14 | 15 | offsetBy(x: number, y: number) { 16 | return new Point(this.x + x, this.y + y); 17 | } 18 | 19 | reverse() { 20 | var y = this.y; 21 | this.y = this.x; 22 | this.x = y; 23 | } 24 | 25 | equals(otherPoint: Point) { 26 | return this.x === otherPoint.x && this.y === otherPoint.y; 27 | } 28 | 29 | toString() { 30 | return "{x=" + this.x + ", y=" + this.y + "}" 31 | } 32 | 33 | clone(): Point{ 34 | return new Point(this.x, this.y); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Renderers/Particles/NumberSmoke.ts: -------------------------------------------------------------------------------- 1 | declare var PIXI: any; 2 | 3 | class NumberSmoke { 4 | // Attributes 5 | text: string; 6 | color: number; 7 | screenLocation: Point; 8 | pixiContainer: any; 9 | 10 | startAlpha: number = 1.0; 11 | endAlpha: number = 0.0; 12 | msDuration: number = 1500; 13 | verticalOffset: number = 32; 14 | 15 | private age: number = 0; 16 | 17 | // Sprite 18 | pixiText: any = null; 19 | 20 | // For calculations 21 | private heightPerMillisecond: number; 22 | private alphaDeltaPerMillisecond: number; 23 | 24 | constructor(pixiContainer: any, text: string = '', screenLocation: Point, color: number = ColorCode.White) { 25 | this.text = text; 26 | this.color = color; 27 | this.screenLocation = screenLocation; 28 | this.pixiContainer = pixiContainer; 29 | 30 | this.heightPerMillisecond = -this.verticalOffset / this.msDuration; 31 | this.alphaDeltaPerMillisecond = (this.endAlpha - this.startAlpha) / this.msDuration; 32 | 33 | this.pixiText = new PIXI.Text( 34 | text, 35 | new PIXI.TextStyle({ 36 | fontFamily: 'Courier', 37 | fontSize: 14, 38 | fill: color, 39 | align: 'center', 40 | stroke: ColorCode.Black, 41 | strokeThickness: 4 42 | }) 43 | ); 44 | this.pixiText.x = this.screenLocation.x; 45 | this.pixiText.y = this.screenLocation.y; 46 | this.pixiContainer.addChild(this.pixiText); 47 | } 48 | 49 | update(secondsElapsed: number) { 50 | let millisecondsElapsed = secondsElapsed * 1000; 51 | this.age += millisecondsElapsed; 52 | this.pixiText.x = this.screenLocation.x; 53 | this.pixiText.y = Math.ceil(this.getVerticalScreenLocationAt(this.age)); 54 | this.pixiText.alpha = this.getAlphaAt(this.age); 55 | } 56 | 57 | private getVerticalScreenLocationAt(millisecondsElapsed: number): number { 58 | return this.screenLocation.offsetBy( 59 | 0, 60 | millisecondsElapsed * this.heightPerMillisecond 61 | ).y; 62 | } 63 | 64 | private getAlphaAt(millisecondsElapsed: number): number { 65 | return this.startAlpha + (millisecondsElapsed * this.alphaDeltaPerMillisecond); 66 | } 67 | 68 | isFinished(): boolean { 69 | return this.age >= this.msDuration; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Renderers/Particles/ParticleEmitters.ts: -------------------------------------------------------------------------------- 1 | class ParticleEmitters { 2 | 3 | static DamageEmitter = { 4 | "alpha": { 5 | "start": 1, 6 | "end": 0.2 7 | }, 8 | "scale": { 9 | "start": 0.25, 10 | "end": 0.2, 11 | "minimumScaleMultiplier": 0.001 12 | }, 13 | "color": { 14 | "start": "#ff0000", 15 | "end": "#d67e7e" 16 | }, 17 | "speed": { 18 | "start": 100, 19 | "end": 0, 20 | "minimumSpeedMultiplier": 1 21 | }, 22 | "acceleration": { 23 | "x": 15, 24 | "y": 0 25 | }, 26 | "maxSpeed": 0, 27 | "startRotation": { 28 | "min": 0, 29 | "max": 360 30 | }, 31 | "noRotation": true, 32 | "rotationSpeed": { 33 | "min": 0, 34 | "max": 0 35 | }, 36 | "lifetime": { 37 | "min": 0.12, 38 | "max": 0.21 39 | }, 40 | "blendMode": "normal", 41 | "frequency": 0.001, 42 | "emitterLifetime": 0.25, 43 | "maxParticles": 20, 44 | "pos": { 45 | "x": 0, 46 | "y": 0 47 | }, 48 | "addAtBack": false, 49 | "spawnType": "point" 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Room.ts: -------------------------------------------------------------------------------- 1 | class Room { 2 | width: number; 3 | height: number; 4 | position: Point; // position is the position of the top-left corner 5 | 6 | constructor(width: number, height: number, position: Point) { 7 | this.width = width; 8 | this.height = height; 9 | this.position = position; 10 | } 11 | 12 | getArea(): number { 13 | return this.width * this.height; 14 | } 15 | 16 | getCenter(): Point { 17 | return new Point(Math.floor((this.left() + this.right()) / 2), 18 | Math.floor((this.top() + this.bottom()) / 2)); 19 | } 20 | 21 | getSizeCategory(): SizeCategory { 22 | var area = this.getArea(); 23 | if (area < SizeCategory.Tiny) 24 | return SizeCategory.Tiny; 25 | else if (area < SizeCategory.Small) 26 | return SizeCategory.Small; 27 | else if (area < SizeCategory.Medium) 28 | return SizeCategory.Medium; 29 | else if (area < SizeCategory.Large) 30 | return SizeCategory.Large; 31 | else 32 | return SizeCategory.Huge; 33 | } 34 | 35 | left(): number { 36 | return this.position.x; 37 | } 38 | 39 | right(): number { 40 | return this.position.x + this.width; 41 | } 42 | 43 | top(): number { 44 | return this.position.y; 45 | } 46 | 47 | bottom(): number { 48 | return this.position.y + this.height; 49 | } 50 | 51 | topLeft(): Point { 52 | return new Point(this.position.x, this.position.y); 53 | } 54 | 55 | topRight(): Point { 56 | return new Point(this.position.x + this.width, this.position.y); 57 | } 58 | 59 | bottomLeft(): Point { 60 | return new Point(this.position.x, this.position.y + this.height); 61 | } 62 | 63 | bottomRight(): Point { 64 | return new Point(this.position.x + this.width, this.position.y + this.height); 65 | } 66 | 67 | static Intersects(a: Room, b: Room): boolean { 68 | return (a.left() <= b.right() && 69 | b.left() <= a.right() && 70 | a.top() <= b.bottom() && 71 | b.top() <= a.bottom()); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Sprites/Sprite.ts: -------------------------------------------------------------------------------- 1 | // Sprite defines a single, static image or text representation of something 2 | class Sprite { 3 | spriteName: string; 4 | tint: number; 5 | visible: boolean = true; 6 | originOffset: Point; 7 | 8 | constructor(spriteName: string, originOffset = new Point(0, 0)) { 9 | this.spriteName = spriteName; 10 | this.originOffset = originOffset 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Sprites/SpriteSet.ts: -------------------------------------------------------------------------------- 1 | // A Sprite Set represents a set of sprites (or frames) representing an actor in a given status and facing a given direction. 2 | // Its animationLoopStyle defines how, if at all, it animates 3 | class SpriteSet { 4 | status: ActorStatus; 5 | direction: Direction; 6 | sprites: Sprite[]; 7 | animationLoopStyle: AnimationLoopStyle; 8 | frameWaitDuration: number; 9 | currentFrame: number = 0; 10 | waitFramesUntilNextFrame: number = 0; 11 | playDirection: Direction = Direction.Right; 12 | restart: boolean = false; 13 | 14 | constructor( 15 | status: ActorStatus, 16 | direction: Direction, 17 | sprites: Sprite[], 18 | animationLoopStyle: AnimationLoopStyle, 19 | frameWaitDuration?: number) { 20 | 21 | // Default the frame wait duration of not provided 22 | if (!frameWaitDuration) { 23 | frameWaitDuration = GameDefault.FrameWaitDuration; 24 | } 25 | 26 | // Given an actor's status and direction 27 | this.status = status; 28 | this.direction = direction; 29 | 30 | // Play these sprites 31 | this.sprites = sprites; 32 | this.animationLoopStyle = animationLoopStyle; 33 | this.frameWaitDuration = frameWaitDuration; 34 | 35 | // If we have many sprites and just want to pick 1 random one, then shuffle 36 | // and pick the first always later 37 | if (this.animationLoopStyle === AnimationLoopStyle.RandomStatic) { 38 | this.sprites = this.sprites.shuffle(new Random(this.sprites.length)); 39 | } 40 | } 41 | 42 | reset() { 43 | this.waitFramesUntilNextFrame = 0; 44 | this.currentFrame = 0; 45 | this.restart = false; 46 | } 47 | 48 | getSprite(restart: boolean) { 49 | this.restart = restart !== undefined && restart === true; 50 | 51 | // Static sprites are always just the first sprite/frame 52 | if (this.animationLoopStyle === AnimationLoopStyle.Static || this.animationLoopStyle === AnimationLoopStyle.RandomStatic) { 53 | return this.sprites.first(); 54 | } 55 | 56 | // One-time animations reach their last frame and then stop 57 | if (this.animationLoopStyle === AnimationLoopStyle.Once) { 58 | this.waitFramesUntilNextFrame--; 59 | // Advance if there's still frames to play 60 | if (this.waitFramesUntilNextFrame <= 0) { 61 | if (this.currentFrame < this.sprites.length - 1) { 62 | this.currentFrame++; 63 | this.waitFramesUntilNextFrame = this.frameWaitDuration; 64 | } 65 | 66 | if (this.restart) this.currentFrame = 0; 67 | } 68 | return this.sprites[this.currentFrame]; 69 | } 70 | 71 | // Looping animations advance back to frame 1 when finished 72 | if (this.animationLoopStyle === AnimationLoopStyle.Loop) { 73 | this.waitFramesUntilNextFrame--; 74 | if (this.waitFramesUntilNextFrame <= 0) { 75 | // Advance 76 | this.currentFrame++; 77 | this.waitFramesUntilNextFrame = this.frameWaitDuration; 78 | // Wrap back to start if needed 79 | if (this.currentFrame >= this.sprites.length) { 80 | this.currentFrame = 0; 81 | } 82 | } 83 | if (this.restart) this.reset(); 84 | return this.sprites[this.currentFrame]; 85 | } 86 | 87 | // Ping-pong animation play forwards, backwards, and then repeat 88 | if (this.animationLoopStyle === AnimationLoopStyle.PingPong) { 89 | 90 | this.waitFramesUntilNextFrame--; 91 | 92 | if (this.waitFramesUntilNextFrame <= 0) { 93 | if (this.playDirection === Direction.Right) { 94 | this.currentFrame++; 95 | this.waitFramesUntilNextFrame = this.frameWaitDuration; 96 | if (this.currentFrame = this.sprites.length) { 97 | this.currentFrame--; 98 | this.playDirection = Direction.Left; 99 | } 100 | } 101 | else if (this.playDirection === Direction.Left) { 102 | this.currentFrame--; 103 | this.waitFramesUntilNextFrame = this.frameWaitDuration; 104 | if (this.currentFrame < 0) { 105 | this.currentFrame++; 106 | this.playDirection = Direction.Right; 107 | } 108 | } 109 | } 110 | if (this.restart && this.playDirection === Direction.Right) this.reset(); 111 | if (this.restart && this.playDirection === Direction.Left) this.currentFrame = this.sprites.length - 1; 112 | return this.sprites[this.currentFrame]; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/UI/LogMessage.ts: -------------------------------------------------------------------------------- 1 | class LogMessage { 2 | message: string; 3 | color: number = ColorCode.White; 4 | constructor(message: string, logMessageType: LogMessageType = LogMessageType.Informational) { 5 | this.message = '[' +Generic.GetTimeStamp() + '] ' + message; 6 | var color = ColorCode.White; 7 | switch (logMessageType) { 8 | case LogMessageType.LandedAttack: 9 | color = ColorCode.White; 10 | break; 11 | 12 | case LogMessageType.Damaged: 13 | color = ColorCode.Red; 14 | break; 15 | 16 | case LogMessageType.ObtainedItem: 17 | color = ColorCode.Green; 18 | break; 19 | 20 | case LogMessageType.ObtainedGold: 21 | color = ColorCode.Yellow; 22 | break; 23 | 24 | case LogMessageType.GainedXP: 25 | color = ColorCode.Purple; 26 | break; 27 | 28 | case LogMessageType.LevelledUp: 29 | color = ColorCode.Pink; 30 | break; 31 | 32 | case LogMessageType.Informational: 33 | color = ColorCode.White; 34 | break; 35 | 36 | case LogMessageType.LostGold: 37 | color = ColorCode.Red; 38 | break; 39 | } 40 | this.color = color; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/World.ts: -------------------------------------------------------------------------------- 1 | 2 | class World { 3 | width: number; 4 | height: number; 5 | layers: Layer[]; 6 | rooms: Room[]; 7 | game: Game; 8 | 9 | constructor(width: number, height: number, game: Game) { 10 | this.width = width; 11 | this.height = height; 12 | this.layers = []; 13 | this.rooms = []; 14 | 15 | // References 16 | this.game = game; 17 | } 18 | 19 | addLayer(layer: Layer) { 20 | this.layers.push(layer); 21 | } 22 | 23 | getLayersOfType(layerType: LayerType) { 24 | return this.layers.filter(function(layer) { 25 | return layer.type === layerType 26 | }); 27 | } 28 | 29 | getWallLayer(): Layer { 30 | return this.getLayersOfType(LayerType.Wall).first(); 31 | } 32 | 33 | getLayersNotOfType(layerType: LayerType) { 34 | return this.layers.filter(function(layer) { 35 | return layer.type !== layerType 36 | }); 37 | } 38 | 39 | static MoveActorToLayer(actor: Actor, layer: Layer): void { 40 | // Remove from the current layer 41 | actor.layer.setTile(actor.location.x, actor.location.y, null); 42 | 43 | // Add to the new layer 44 | layer.placeActor(actor, actor.location); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/WorldItems/Base/WorldItem.ts: -------------------------------------------------------------------------------- 1 | class WorldItem extends Actor { 2 | inventoryItem: InventoryItem; 3 | random: Random = null; 4 | useRandomSprite: boolean; 5 | randomSpriteIndex: number = 0; 6 | 7 | constructor(game: Game, random?: Random) { 8 | super(game); 9 | 10 | if (random) { 11 | // We want random sprites from the set 12 | this.random = random; 13 | this.useRandomSprite = true; 14 | } 15 | 16 | this.blocksSight = false; 17 | 18 | this.inventoryItem = null; 19 | } 20 | 21 | setSprite() { 22 | // Set the random sprite to use on creation 23 | if (this.useRandomSprite) { 24 | this.randomSpriteIndex = this.random.next(0, this.spritesets.first().sprites.length); 25 | } 26 | } 27 | 28 | getSprite() { 29 | if (this.useRandomSprite) { 30 | return this.spritesets.first().sprites[this.randomSpriteIndex]; 31 | } 32 | else { 33 | return super.getSprite(); 34 | } 35 | } 36 | 37 | pickedUpBy(actor: Actor) { 38 | if (this.inventoryItem !== null) { 39 | actor.obtainInventoryItem(this.inventoryItem); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WorldItems/Chest.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | class Chest extends WorldItem { 5 | contents: InventoryItem[]; 6 | 7 | constructor(game: Game, contents?: InventoryItem[]) { 8 | if (!contents) { 9 | var contents: InventoryItem[] = []; 10 | } 11 | super(game); 12 | 13 | this.spritesets = Sprites.ChestSprites(); 14 | this.contents = contents; //array of contents 15 | this.inventoryItem = null; // rather than 1 item, a chest picks up many 16 | this.status = ActorStatus.Closed; 17 | } 18 | 19 | openedBy(actor: Actor) { 20 | if (this.status === ActorStatus.Closed) { 21 | for (var i = 0; i < this.contents.length; i++) { 22 | var item = this.contents[i]; 23 | actor.obtainInventoryItem(item); 24 | } 25 | this.contents = null; 26 | this.status = ActorStatus.Open; 27 | } 28 | } 29 | 30 | pickedUpBy(actor: Actor) { 31 | //super(by); 32 | // Override pickedUp with method to deliver the contents 33 | this.openedBy(actor); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/WorldItems/DroppedArrow.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | class DroppedArrow extends WorldItem { 4 | constructor(game: Game, random?: Random) { 5 | super(game, random); 6 | this.spritesets = Sprites.ArrowSprites(); 7 | this.status = ActorStatus.Idle; 8 | this.setSprite(); 9 | this.inventoryItem = new InventoryArrow(random); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/WorldItems/GoldPile.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | class GoldPile extends WorldItem { 4 | 5 | goldCount: number = 1; 6 | 7 | constructor(game: Game, goldCount?: number, random?: Random) { 8 | super(game, random); 9 | this.spritesets = Sprites.GoldPileSprites(); 10 | this.status = ActorStatus.Idle; 11 | this.setSprite(); 12 | 13 | if (goldCount === undefined && this.game.random !== null) { 14 | goldCount = this.game.random.next(1, 10); 15 | } 16 | else { 17 | goldCount = 1; 18 | } 19 | 20 | this.goldCount = goldCount; //array of contents 21 | this.inventoryItem = null; // rather than 1 item, a chest picks up many 22 | } 23 | 24 | pickedUpBy(actor: Actor) { 25 | //super(by); 26 | // Override pickedUp with method to deliver the contents 27 | if (actor instanceof Player) { 28 | (actor).giveGold(this.goldCount); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "removeComments": true, 5 | "preserveConstEnums": true, 6 | "outFile": "build/js/game.js", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "include": [ 11 | "src/**/*.ts" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------