├── .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 | 
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 |
--------------------------------------------------------------------------------