├── .gitignore ├── roguelike_oo ├── 10 │ ├── ascii │ │ └── app │ │ │ ├── dice │ │ │ ├── d20.rb │ │ │ └── dice.rb │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── static_entity.rb │ │ │ ├── wall.rb │ │ │ ├── zombie.rb │ │ │ ├── floor.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── base.rb │ │ │ ├── enemy.rb │ │ │ └── player.rb │ │ │ ├── behaviour │ │ │ ├── defender.rb │ │ │ ├── occupant.rb │ │ │ └── attacker.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── event_logs_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ ├── game_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ ├── screenshots │ │ └── logs_and_hp_color.png │ └── tutorial.md ├── 09 │ ├── ascii │ │ └── app │ │ │ ├── dice │ │ │ ├── d20.rb │ │ │ └── dice.rb │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── static_entity.rb │ │ │ ├── zombie.rb │ │ │ ├── wall.rb │ │ │ ├── floor.rb │ │ │ ├── base.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── player.rb │ │ │ └── enemy.rb │ │ │ ├── behaviour │ │ │ ├── defender.rb │ │ │ ├── occupant.rb │ │ │ └── attacker.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ ├── game_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ └── tutorial.md ├── 02 │ ├── ascii │ │ └── app │ │ │ ├── entities │ │ │ ├── static_entity.rb │ │ │ ├── floor.rb │ │ │ ├── wall.rb │ │ │ └── base.rb │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── controllers │ │ │ ├── game_controller.rb │ │ │ ├── title_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ ├── screenshots │ │ └── map.png │ └── tutorial.md ├── 03 │ ├── ascii │ │ └── app │ │ │ ├── entities │ │ │ ├── static_entity.rb │ │ │ ├── floor.rb │ │ │ ├── wall.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── player.rb │ │ │ └── base.rb │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── game_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ ├── screenshots │ │ └── player.png │ └── tutorial.md ├── 04 │ ├── screenshots │ │ └── map.png │ └── ascii │ │ └── app │ │ ├── sprites │ │ ├── wall.png │ │ ├── floor.png │ │ ├── player.png │ │ ├── dragonruby.png │ │ └── null_sprite.png │ │ ├── entities │ │ ├── floor.rb │ │ ├── static_entity.rb │ │ ├── wall.rb │ │ ├── mobile_entity.rb │ │ ├── base.rb │ │ └── player.rb │ │ ├── controllers │ │ ├── title_controller.rb │ │ ├── game_controller.rb │ │ └── map_controller.rb │ │ └── main.rb ├── 05 │ ├── screenshots │ │ └── zombies.png │ ├── ascii │ │ └── app │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── floor.rb │ │ │ ├── zombie.rb │ │ │ ├── static_entity.rb │ │ │ ├── wall.rb │ │ │ ├── enemy.rb │ │ │ ├── base.rb │ │ │ ├── mobile_entity.rb │ │ │ └── player.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── game_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ └── tutorial.md ├── 06 │ ├── ascii │ │ └── app │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── zombie.rb │ │ │ ├── static_entity.rb │ │ │ ├── wall.rb │ │ │ ├── floor.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── player.rb │ │ │ ├── base.rb │ │ │ └── enemy.rb │ │ │ ├── behaviour │ │ │ └── occupant.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── game_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ └── tutorial.md ├── 07 │ ├── ascii │ │ └── app │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── zombie.rb │ │ │ ├── static_entity.rb │ │ │ ├── wall.rb │ │ │ ├── floor.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── base.rb │ │ │ ├── player.rb │ │ │ └── enemy.rb │ │ │ ├── behaviour │ │ │ └── occupant.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ ├── game_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ └── screenshots │ │ └── render_targets.png ├── 08 │ ├── ascii │ │ └── app │ │ │ ├── sprites │ │ │ ├── wall.png │ │ │ ├── floor.png │ │ │ ├── player.png │ │ │ ├── zombie.png │ │ │ ├── dragonruby.png │ │ │ └── null_sprite.png │ │ │ ├── entities │ │ │ ├── static_entity.rb │ │ │ ├── zombie.rb │ │ │ ├── wall.rb │ │ │ ├── floor.rb │ │ │ ├── base.rb │ │ │ ├── mobile_entity.rb │ │ │ ├── player.rb │ │ │ └── enemy.rb │ │ │ ├── behaviour │ │ │ ├── attacker.rb │ │ │ ├── occupant.rb │ │ │ └── defender.rb │ │ │ ├── controllers │ │ │ ├── title_controller.rb │ │ │ ├── enemy_controller.rb │ │ │ ├── game_controller.rb │ │ │ └── map_controller.rb │ │ │ └── main.rb │ └── tutorial.md ├── 01 │ ├── screenshots │ │ └── menu_screen.png │ ├── ascii │ │ └── app │ │ │ ├── sprites │ │ │ └── dragonruby.png │ │ │ ├── controllers │ │ │ ├── game_controller.rb │ │ │ └── title_controller.rb │ │ │ └── main.rb │ └── tutorial.md └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/dice/d20.rb: -------------------------------------------------------------------------------- 1 | class D20 < Dice 2 | def self.max_value 3 | 20 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/dice/d20.rb: -------------------------------------------------------------------------------- 1 | class D20 < Dice 2 | def self.max_value 3 | 20 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /roguelike_oo/02/screenshots/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/02/screenshots/map.png -------------------------------------------------------------------------------- /roguelike_oo/04/screenshots/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/screenshots/map.png -------------------------------------------------------------------------------- /roguelike_oo/03/screenshots/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/screenshots/player.png -------------------------------------------------------------------------------- /roguelike_oo/05/screenshots/zombies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/screenshots/zombies.png -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/02/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/wall.png -------------------------------------------------------------------------------- /roguelike_oo/01/screenshots/menu_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/01/screenshots/menu_screen.png -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/02/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/floor.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/player.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/zombie.png -------------------------------------------------------------------------------- /roguelike_oo/07/screenshots/render_targets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/screenshots/render_targets.png -------------------------------------------------------------------------------- /roguelike_oo/01/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/01/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/02/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/02/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/03/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/04/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/05/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/06/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/07/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/08/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/09/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/dragonruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/dragonruby.png -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/sprites/null_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/ascii/app/sprites/null_sprite.png -------------------------------------------------------------------------------- /roguelike_oo/10/screenshots/logs_and_hp_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Craggar/dragonruby_tutorials/HEAD/roguelike_oo/10/screenshots/logs_and_hp_color.png -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/floor.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/floor.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/floor.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/floor.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/static_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class StaticEntity < Base 3 | def tick(args) 4 | @x = map_x - args.state.map.x 5 | @y = map_y - args.state.map.y 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | @defense = 4 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | @defense = 4 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/wall.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Wall < StaticEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/wall.png' 6 | end 7 | 8 | def blocking? 9 | true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | def self.spawn(tile_x, tile_y) 4 | new( 5 | x: tile_x * SPRITE_WIDTH, 6 | y: tile_y * SPRITE_HEIGHT 7 | ) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /roguelike_oo/01/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | end 5 | 6 | def self.render(state, sprites, labels) 7 | end 8 | 9 | def self.reset(state) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/behaviour/attacker.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Attacker 3 | attr_reader :attack 4 | 5 | def deal_damage(other) 6 | return unless other.respond_to?(:take_damage) 7 | 8 | other.take_damage(attack) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/zombie.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Zombie < Enemy 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/zombie.png' 6 | @defense = 4 7 | end 8 | 9 | def name 10 | 'Zombie' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/behaviour/occupant.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Occupant 3 | attr_reader :tile 4 | 5 | def update_tile(args) 6 | tile.occupant = nil if tile 7 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 8 | tile.occupant = self 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/behaviour/occupant.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Occupant 3 | attr_reader :tile 4 | 5 | def update_tile(args) 6 | tile.occupant = nil if tile 7 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 8 | tile.occupant = self 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/behaviour/occupant.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Occupant 3 | attr_reader :tile 4 | 5 | def update_tile(args) 6 | tile.occupant = nil if tile 7 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 8 | tile.occupant = self 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/dice/dice.rb: -------------------------------------------------------------------------------- 1 | class Dice 2 | def self.roll(count) 3 | total = 0 4 | count.times do 5 | total += (min_value..max_value).to_a.sample 6 | end 7 | total 8 | end 9 | 10 | def self.min_value 11 | 1 12 | end 13 | 14 | def self.max_value 15 | 6 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/dice/dice.rb: -------------------------------------------------------------------------------- 1 | class Dice 2 | def self.roll(count) 3 | total = 0 4 | count.times do 5 | total += (min_value..max_value).to_a.sample 6 | end 7 | total 8 | end 9 | 10 | def self.min_value 11 | 1 12 | end 13 | 14 | def self.max_value 15 | 6 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/behaviour/defender.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Defender 3 | attr_reader :max_hp, :hp, :defense 4 | 5 | def alive? 6 | hp > 0 7 | end 8 | 9 | def take_damage(damage) 10 | @hp = [ 11 | 0, 12 | hp - damage 13 | ].max 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | attr_accessor :occupant 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/floor.png' 8 | end 9 | 10 | def blocking? 11 | occupant&.blocking? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | attr_accessor :occupant 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/floor.png' 8 | end 9 | 10 | def blocking? 11 | occupant&.blocking? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | attr_accessor :occupant 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/floor.png' 8 | end 9 | 10 | def blocking? 11 | occupant&.blocking? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | attr_accessor :occupant 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/floor.png' 8 | end 9 | 10 | def blocking? 11 | occupant&.blocking? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/floor.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Floor < StaticEntity 3 | attr_accessor :occupant 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/floor.png' 8 | end 9 | 10 | def blocking? 11 | occupant&.blocking? || super 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | end 5 | 6 | def self.render(state, sprites, labels) 7 | sprites << state.map.tiles 8 | end 9 | 10 | def self.reset(state) 11 | ::Controllers::MapController.load_map(state) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/behaviour/defender.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Defender 3 | attr_reader :hp, :defense 4 | 5 | def alive? 6 | hp > 0 7 | end 8 | 9 | def take_damage(damage) 10 | @hp = [ 11 | 0, 12 | hp - damage 13 | ].max 14 | puts "#{self.class} took #{damage} damage -> #{hp} remaining" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/behaviour/defender.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Defender 3 | attr_reader :hp, :defense 4 | 5 | def alive? 6 | hp > 0 7 | end 8 | 9 | def take_damage(damage) 10 | @hp = [ 11 | 0, 12 | hp - damage 13 | ].max 14 | puts "#{self.class} took #{damage} damage -> #{hp} remaining" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/behaviour/occupant.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Occupant 3 | attr_reader :tile 4 | 5 | def update_tile(args) 6 | tile.occupant = nil if tile 7 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 8 | tile.occupant = self 9 | end 10 | 11 | def free_tile_on_death(args) 12 | tile.occupant = nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/behaviour/occupant.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Occupant 3 | attr_reader :tile 4 | 5 | def update_tile(args) 6 | tile.occupant = nil if tile 7 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 8 | tile.occupant = self 9 | end 10 | 11 | def free_tile_on_death(args) 12 | tile.occupant = nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DragonRuby Object Oriented, Sprite-based Roguelike Tutorial - Work In Progress 2 | This is a set of tutorials to create a top-down 'roguelike' 3 | 4 | [DragonRuby Object Oriented, Sprite-based Roguelike Tutorial](./roguelike_oo/README.md) 5 | 6 | ![A screenshot showing the player's HP color-coded, along with a log of events from most recent to oldest, fading from white to black based on their recency](./roguelike_oo/10/screenshots/logs_and_hp_color.png) 7 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(args, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(args, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(args, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(args, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/01/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/controllers/title_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class TitleController 3 | def self.tick(args) 4 | $game.goto_game(args) if args.inputs.keyboard.space 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | labels << {x: 620, y: 300, text: 'ASCII'} 9 | labels << {x: 550, y: 100, text: 'Press space to start'} 10 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | args.state.player.tick(args) 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | sprites << state.map.tiles 9 | sprites << state.player 10 | end 11 | 12 | def self.reset(state) 13 | ::Controllers::MapController.load_map(state) 14 | state.player = ::Entities::Player.spawn(2, 2) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | args.state.player.tick(args) 5 | end 6 | 7 | def self.render(state, sprites, labels) 8 | sprites << state.map.tiles 9 | sprites << state.player 10 | end 11 | 12 | def self.reset(state) 13 | ::Controllers::MapController.load_map(state) 14 | state.player = ::Entities::Player.spawn(20, 11) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | args.state.player.tick(args) 5 | ::Controllers::EnemyController.tick(args) 6 | end 7 | 8 | def self.render(state, sprites, labels) 9 | sprites << state.map.tiles 10 | sprites << state.enemies 11 | sprites << state.player 12 | end 13 | 14 | def self.reset(state) 15 | ::Controllers::MapController.load_map(state) 16 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 17 | ::Controllers::EnemyController.spawn_enemies(state) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | def self.tick(args) 4 | args.state.player.tick(args) 5 | ::Controllers::EnemyController.tick(args) 6 | end 7 | 8 | def self.render(state, sprites, labels) 9 | sprites << state.map.tiles 10 | sprites << state.enemies 11 | sprites << state.player 12 | end 13 | 14 | def self.reset(state) 15 | ::Controllers::MapController.load_map(state) 16 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 17 | ::Controllers::EnemyController.spawn_enemies(state) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/behaviour/attacker.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Attacker 3 | attr_reader :attack, :crit_bonus 4 | 5 | def deal_damage(other) 6 | return unless other.respond_to?(:take_damage) 7 | 8 | roll = ::D20.roll(1) 9 | puts "Rolled: #{roll == 20 ? 'CRIT!' : roll} against #{other.class}'s DEF: #{other.defense}" 10 | total_attack = if roll == 20 11 | attack + crit_bonus 12 | else 13 | attack 14 | end 15 | if roll >= other.defense 16 | other.take_damage(total_attack) 17 | else 18 | puts 'miss!' 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/controllers/event_logs_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EventLogsController 3 | LOG_TOP = 650 4 | 5 | def self.events_as_labels(args) 6 | args.state.event_logs.map.with_index do |event, index| 7 | alpha = 255 - (index * 15) 8 | {x: 16, y: LOG_TOP - (index * 40), text: event, r: 230, g: 230, b: 230, a: alpha} 9 | end 10 | end 11 | 12 | def self.log_event(event) 13 | $gtk.args.state.event_logs.unshift(event) 14 | $gtk.args.state.logged_event_this_tick = true 15 | end 16 | 17 | def self.reset(state) 18 | state.event_logs = [] 19 | state.logged_event_this_tick = false 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | def self.spawn(tile_x, tile_y) 4 | new( 5 | map_x: tile_x * SPRITE_WIDTH, 6 | map_y: tile_y * SPRITE_HEIGHT 7 | ) 8 | end 9 | 10 | def attempt_move(args, target_x, target_y) 11 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 12 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 13 | return if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 14 | 15 | @map_x = target_x 16 | @map_y = target_y 17 | yield if block_given? 18 | @x = map_x - args.state.map.x 19 | @y = map_y - args.state.map.y 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/player.png' 6 | end 7 | 8 | def tick(args) 9 | @y += ::Controllers::MapController::TILE_HEIGHT if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 10 | @y -= ::Controllers::MapController::TILE_HEIGHT if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 11 | @x += ::Controllers::MapController::TILE_WIDTH if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 12 | @x -= ::Controllers::MapController::TILE_WIDTH if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | def tick(args) 4 | patrol(args) 5 | @x = map_x - args.state.map.x 6 | @y = map_y - args.state.map.y 7 | end 8 | 9 | def patrol(args) 10 | direction = [:up, :down, :left, :right].sample 11 | case direction 12 | when :up 13 | attempt_move(args, map_x, map_y + ::Controllers::MapController::TILE_HEIGHT) 14 | when :down 15 | attempt_move(args, map_x, map_y - ::Controllers::MapController::TILE_HEIGHT) 16 | when :left 17 | attempt_move(args, map_x - ::Controllers::MapController::TILE_WIDTH, map_y) 18 | when :right 19 | attempt_move(args, map_x + ::Controllers::MapController::TILE_WIDTH, map_y) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | 5 | SPRITE_WIDTH = 32 6 | SPRITE_HEIGHT = 32 7 | 8 | def initialize(opts = {}) 9 | @x = opts[:x] || 0 10 | @y = opts[:y] || 0 11 | @w = opts[:w] || SPRITE_WIDTH 12 | @h = opts[:h] || SPRITE_HEIGHT 13 | @path = opts[:path] || 'app/sprites/null_sprite.png' 14 | end 15 | 16 | def serialize 17 | { 18 | x: x, 19 | y: y, 20 | w: w, 21 | h: h, 22 | path: path 23 | } 24 | end 25 | 26 | def inspect 27 | # Override the inspect method and return ~serialize.to_s~. 28 | serialize.to_s 29 | end 30 | 31 | def to_s 32 | # Override to_s and return ~serialize.to_s~. 33 | serialize.to_s 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | 5 | SPRITE_WIDTH = 32 6 | SPRITE_HEIGHT = 32 7 | 8 | def initialize(opts = {}) 9 | @x = opts[:x] || 0 10 | @y = opts[:y] || 0 11 | @w = opts[:w] || SPRITE_WIDTH 12 | @h = opts[:h] || SPRITE_HEIGHT 13 | @path = opts[:path] || 'app/sprites/null_sprite.png' 14 | end 15 | 16 | def serialize 17 | { 18 | x: x, 19 | y: y, 20 | w: w, 21 | h: h, 22 | path: path 23 | } 24 | end 25 | 26 | def inspect 27 | # Override the inspect method and return ~serialize.to_s~. 28 | serialize.to_s 29 | end 30 | 31 | def to_s 32 | # Override to_s and return ~serialize.to_s~. 33 | serialize.to_s 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/behaviour/attacker.rb: -------------------------------------------------------------------------------- 1 | module Behaviour 2 | module Attacker 3 | attr_reader :attack, :crit_bonus 4 | 5 | def deal_damage(other) 6 | return unless other.respond_to?(:take_damage) 7 | 8 | roll = ::D20.roll(1) 9 | total_attack = if roll == 20 10 | attack + crit_bonus 11 | else 12 | attack 13 | end 14 | if roll >= other.defense 15 | other.take_damage(total_attack) 16 | ::Controllers::EventLogsController.log_event( 17 | "#{roll == 20 ? 'CRIT! ' : ''}#{name} hit #{other.name} for #{total_attack} damage" 18 | ) 19 | else 20 | ::Controllers::EventLogsController.log_event( 21 | "#{name} missed #{other.name}!" 22 | ) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /roguelike_oo/01/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | 4 | class Game 5 | attr_reader :active_controller 6 | 7 | def goto_title 8 | @active_controller = ::Controllers::TitleController 9 | end 10 | 11 | def goto_game(args) 12 | ::Controllers::GameController.reset(args.state) 13 | @active_controller = ::Controllers::GameController 14 | end 15 | 16 | def tick(args) 17 | goto_title unless active_controller 18 | sprites = [] 19 | labels = [] 20 | active_controller.tick(args) 21 | active_controller.render(args.state, sprites, labels) 22 | render(args, sprites, labels) 23 | end 24 | 25 | def render(args, sprites, labels) 26 | args.outputs.sprites << sprites 27 | args.outputs.labels << labels 28 | end 29 | end 30 | 31 | $game ||= Game.new 32 | def tick(args) 33 | $game.tick(args) 34 | end 35 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | enemies = args.state.enemies 6 | enemies.each { |enemy| enemy.tick(args) } 7 | end 8 | 9 | def self.spawn_enemies(state) 10 | state.enemies ||= [] 11 | 30.times do 12 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 13 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 14 | spawn_enemy( 15 | state, 16 | tile_x, 17 | tile_y, 18 | ::Entities::Zombie 19 | ) 20 | end 21 | end 22 | 23 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 24 | state.enemies << enemy_type.spawn_near( 25 | state, 26 | tile_x, 27 | tile_y 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | enemies = args.state.enemies 6 | enemies.each { |enemy| enemy.tick(args) } 7 | end 8 | 9 | def self.spawn_enemies(state) 10 | state.enemies ||= [] 11 | 30.times do 12 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 13 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 14 | spawn_enemy( 15 | state, 16 | tile_x, 17 | tile_y, 18 | ::Entities::Zombie 19 | ) 20 | end 21 | end 22 | 23 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 24 | state.enemies << enemy_type.spawn_near( 25 | state, 26 | tile_x, 27 | tile_y 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | enemies = args.state.enemies 6 | enemies.each { |enemy| enemy.tick(args) } 7 | end 8 | 9 | def self.spawn_enemies(state) 10 | state.enemies ||= [] 11 | 30.times do 12 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 13 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 14 | spawn_enemy( 15 | state, 16 | tile_x, 17 | tile_y, 18 | ::Entities::Zombie 19 | ) 20 | end 21 | end 22 | 23 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 24 | state.enemies << enemy_type.spawn_near( 25 | state, 26 | tile_x, 27 | tile_y 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | enemies = args.state.enemies 6 | enemies.each { |enemy| enemy.tick(args) } 7 | end 8 | 9 | def self.spawn_enemies(state) 10 | state.enemies ||= [] 11 | 30.times do 12 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 13 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 14 | spawn_enemy( 15 | state, 16 | tile_x, 17 | tile_y, 18 | ::Entities::Zombie 19 | ) 20 | end 21 | end 22 | 23 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 24 | state.enemies << enemy_type.spawn_near( 25 | state, 26 | tile_x, 27 | tile_y 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | 8 | def self.load_map(state) 9 | state.map.tiles = map_tiles 10 | end 11 | 12 | def self.map_tiles 13 | MAP_WIDTH.times.map do |tile_x| 14 | MAP_HEIGHT.times.map do |tile_y| 15 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 16 | tile_x == 0 || tile_x == MAP_WIDTH - 1 17 | tile_for tile_x, tile_y, Entities::Wall 18 | else 19 | tile_for tile_x, tile_y, Entities::Floor 20 | end 21 | end 22 | end 23 | end 24 | 25 | def self.tile_for(tile_x, tile_y, tile_type) 26 | tile_type.new( 27 | x: tile_x * TILE_WIDTH, 28 | y: tile_y * TILE_HEIGHT, 29 | w: TILE_WIDTH, 30 | h: TILE_HEIGHT 31 | ) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | 8 | def self.load_map(state) 9 | state.map.tiles = map_tiles 10 | end 11 | 12 | def self.map_tiles 13 | MAP_WIDTH.times.map do |tile_x| 14 | MAP_HEIGHT.times.map do |tile_y| 15 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 16 | tile_x == 0 || tile_x == MAP_WIDTH - 1 17 | tile_for tile_x, tile_y, Entities::Wall 18 | else 19 | tile_for tile_x, tile_y, Entities::Floor 20 | end 21 | end 22 | end 23 | end 24 | 25 | def self.tile_for(tile_x, tile_y, tile_type) 26 | tile_type.new( 27 | x: tile_x * TILE_WIDTH, 28 | y: tile_y * TILE_HEIGHT, 29 | w: TILE_WIDTH, 30 | h: TILE_HEIGHT 31 | ) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | 6 | args.state.enemies.each { |enemy| enemy.tick(args) } 7 | args.state.enemies = args.state.enemies.select(&:alive?) 8 | end 9 | 10 | def self.spawn_enemies(state) 11 | state.enemies ||= [] 12 | 30.times do 13 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 14 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 15 | spawn_enemy( 16 | state, 17 | tile_x, 18 | tile_y, 19 | ::Entities::Zombie 20 | ) 21 | end 22 | end 23 | 24 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 25 | state.enemies << enemy_type.spawn_near( 26 | state, 27 | tile_x, 28 | tile_y 29 | ) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/controllers/enemy_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class EnemyController 3 | def self.tick(args) 4 | return unless args.state.player.took_action 5 | 6 | args.state.enemies.each { |enemy| enemy.tick(args) } 7 | args.state.enemies = args.state.enemies.select(&:alive?) 8 | end 9 | 10 | def self.spawn_enemies(state) 11 | state.enemies ||= [] 12 | 30.times do 13 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 14 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 15 | spawn_enemy( 16 | state, 17 | tile_x, 18 | tile_y, 19 | ::Entities::Zombie 20 | ) 21 | end 22 | end 23 | 24 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 25 | state.enemies << enemy_type.spawn_near( 26 | state, 27 | tile_x, 28 | tile_y 29 | ) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def serialize 24 | { 25 | x: x, 26 | y: y, 27 | w: w, 28 | h: h, 29 | path: path, 30 | blocking: blocking? 31 | } 32 | end 33 | 34 | def inspect 35 | # Override the inspect method and return ~serialize.to_s~. 36 | serialize.to_s 37 | end 38 | 39 | def to_s 40 | # Override to_s and return ~serialize.to_s~. 41 | serialize.to_s 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def serialize 24 | { 25 | x: x, 26 | y: y, 27 | w: w, 28 | h: h, 29 | path: path, 30 | blocking: blocking? 31 | } 32 | end 33 | 34 | def inspect 35 | # Override the inspect method and return ~serialize.to_s~. 36 | serialize.to_s 37 | end 38 | 39 | def to_s 40 | # Override to_s and return ~serialize.to_s~. 41 | serialize.to_s 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /roguelike_oo/02/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | 5 | require 'app/entities/base.rb' 6 | require 'app/entities/static_entity.rb' 7 | require 'app/entities/wall.rb' 8 | require 'app/entities/floor.rb' 9 | 10 | class Game 11 | attr_reader :active_controller 12 | 13 | def goto_title 14 | @active_controller = ::Controllers::TitleController 15 | end 16 | 17 | def goto_game(args) 18 | ::Controllers::GameController.reset(args.state) 19 | @active_controller = ::Controllers::GameController 20 | end 21 | 22 | def tick(args) 23 | goto_title unless active_controller 24 | sprites = [] 25 | labels = [] 26 | active_controller.tick(args) 27 | active_controller.render(args.state, sprites, labels) 28 | render(args, sprites, labels) 29 | end 30 | 31 | def render(args, sprites, labels) 32 | args.outputs.sprites << sprites 33 | args.outputs.labels << labels 34 | end 35 | end 36 | 37 | $game ||= Game.new 38 | def tick(args) 39 | $game.tick(args) 40 | end 41 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | def initialize(opts = {}) 4 | super 5 | @path = 'app/sprites/player.png' 6 | end 7 | 8 | def tick(args) 9 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 10 | map_x + ::Controllers::MapController::TILE_WIDTH 11 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 12 | map_x - ::Controllers::MapController::TILE_WIDTH 13 | else 14 | map_x 15 | end 16 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 17 | map_y + ::Controllers::MapController::TILE_HEIGHT 18 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 19 | map_y - ::Controllers::MapController::TILE_HEIGHT 20 | else 21 | map_y 22 | end 23 | attempt_move(args, target_x, target_y) do 24 | ::Controllers::MapController.tick(args) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /roguelike_oo/03/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | 5 | require 'app/entities/base.rb' 6 | require 'app/entities/static_entity.rb' 7 | require 'app/entities/wall.rb' 8 | require 'app/entities/floor.rb' 9 | require 'app/entities/mobile_entity.rb' 10 | require 'app/entities/player.rb' 11 | 12 | class Game 13 | attr_reader :active_controller 14 | 15 | def goto_title 16 | @active_controller = ::Controllers::TitleController 17 | end 18 | 19 | def goto_game(args) 20 | ::Controllers::GameController.reset(args.state) 21 | @active_controller = ::Controllers::GameController 22 | end 23 | 24 | def tick(args) 25 | goto_title unless active_controller 26 | sprites = [] 27 | labels = [] 28 | active_controller.tick(args) 29 | active_controller.render(args.state, sprites, labels) 30 | render(args, sprites, labels) 31 | end 32 | 33 | def render(args, sprites, labels) 34 | args.outputs.sprites << sprites 35 | args.outputs.labels << labels 36 | end 37 | end 38 | 39 | $game ||= Game.new 40 | def tick(args) 41 | $game.tick(args) 42 | end 43 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | 5 | require 'app/entities/base.rb' 6 | require 'app/entities/static_entity.rb' 7 | require 'app/entities/wall.rb' 8 | require 'app/entities/floor.rb' 9 | require 'app/entities/mobile_entity.rb' 10 | require 'app/entities/player.rb' 11 | 12 | class Game 13 | attr_reader :active_controller 14 | 15 | def goto_title 16 | @active_controller = ::Controllers::TitleController 17 | end 18 | 19 | def goto_game(args) 20 | ::Controllers::GameController.reset(args.state) 21 | @active_controller = ::Controllers::GameController 22 | end 23 | 24 | def tick(args) 25 | goto_title unless active_controller 26 | sprites = [] 27 | labels = [] 28 | active_controller.tick(args) 29 | active_controller.render(args.state, sprites, labels) 30 | render(args, sprites, labels) 31 | end 32 | 33 | def render(args, sprites, labels) 34 | args.outputs.sprites << sprites 35 | args.outputs.labels << labels 36 | end 37 | end 38 | 39 | $game ||= Game.new 40 | def tick(args) 41 | $game.tick(args) 42 | end 43 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | def self.spawn_near(state, spawn_x, spawn_y) 4 | radius = 1 5 | attempt = 0 6 | tile = state.map.tiles[spawn_x][spawn_y] 7 | while tile.nil? || tile.blocking? 8 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 9 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 10 | tile = state.map.tiles[spawn_x][spawn_y] 11 | attempt += 1 12 | next unless attempt >= radius * 8 13 | 14 | radius += 1 15 | attempt = 0 16 | end 17 | new( 18 | map_x: spawn_x * SPRITE_WIDTH, 19 | map_y: spawn_y * SPRITE_HEIGHT 20 | ) 21 | end 22 | 23 | def attempt_move(args, target_x, target_y) 24 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 25 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 26 | return if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 27 | 28 | @map_x = target_x 29 | @map_y = target_y 30 | yield if block_given? 31 | @x = map_x - args.state.map.x 32 | @y = map_y - args.state.map.y 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | 6 | require 'app/entities/base.rb' 7 | require 'app/entities/static_entity.rb' 8 | require 'app/entities/wall.rb' 9 | require 'app/entities/floor.rb' 10 | require 'app/entities/mobile_entity.rb' 11 | require 'app/entities/player.rb' 12 | require 'app/entities/enemy.rb' 13 | require 'app/entities/zombie.rb' 14 | 15 | class Game 16 | attr_reader :active_controller 17 | 18 | def goto_title 19 | @active_controller = ::Controllers::TitleController 20 | end 21 | 22 | def goto_game(args) 23 | ::Controllers::GameController.reset(args.state) 24 | @active_controller = ::Controllers::GameController 25 | end 26 | 27 | def tick(args) 28 | goto_title unless active_controller 29 | sprites = [] 30 | labels = [] 31 | active_controller.tick(args) 32 | active_controller.render(args.state, sprites, labels) 33 | render(args, sprites, labels) 34 | end 35 | 36 | def render(args, sprites, labels) 37 | args.outputs.sprites << sprites 38 | args.outputs.labels << labels 39 | end 40 | end 41 | 42 | $game ||= Game.new 43 | def tick(args) 44 | $game.tick(args) 45 | end 46 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | include ::Behaviour::Occupant 4 | 5 | def self.spawn_near(state, spawn_x, spawn_y) 6 | radius = 1 7 | attempt = 0 8 | tile = state.map.tiles[spawn_x][spawn_y] 9 | while tile.nil? || tile.blocking? 10 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 11 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 12 | tile = state.map.tiles[spawn_x][spawn_y] 13 | attempt += 1 14 | next unless attempt >= radius * 8 15 | 16 | radius += 1 17 | attempt = 0 18 | end 19 | new( 20 | map_x: spawn_x * SPRITE_WIDTH, 21 | map_y: spawn_y * SPRITE_HEIGHT 22 | ) 23 | end 24 | 25 | def attempt_move(args, target_x, target_y) 26 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 27 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 28 | return if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 29 | 30 | @map_x = target_x 31 | @map_y = target_y 32 | yield if block_given? 33 | @x = map_x - args.state.map.x 34 | @y = map_y - args.state.map.y 35 | end 36 | 37 | def blocking? 38 | true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | include ::Behaviour::Occupant 4 | 5 | def self.spawn_near(state, spawn_x, spawn_y) 6 | radius = 1 7 | attempt = 0 8 | tile = state.map.tiles[spawn_x][spawn_y] 9 | while tile.nil? || tile.blocking? 10 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 11 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 12 | tile = state.map.tiles[spawn_x][spawn_y] 13 | attempt += 1 14 | next unless attempt >= radius * 8 15 | 16 | radius += 1 17 | attempt = 0 18 | end 19 | new( 20 | map_x: spawn_x * SPRITE_WIDTH, 21 | map_y: spawn_y * SPRITE_HEIGHT 22 | ) 23 | end 24 | 25 | def attempt_move(args, target_x, target_y) 26 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 27 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 28 | return if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 29 | 30 | @map_x = target_x 31 | @map_y = target_y 32 | yield if block_given? 33 | @x = map_x - args.state.map.x 34 | @y = map_y - args.state.map.y 35 | end 36 | 37 | def blocking? 38 | true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | attr_reader :took_action 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/player.png' 8 | end 9 | 10 | def tick(args) 11 | @took_action = false 12 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 13 | map_x + ::Controllers::MapController::TILE_WIDTH 14 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 15 | map_x - ::Controllers::MapController::TILE_WIDTH 16 | else 17 | map_x 18 | end 19 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 20 | map_y + ::Controllers::MapController::TILE_HEIGHT 21 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 22 | map_y - ::Controllers::MapController::TILE_HEIGHT 23 | else 24 | map_y 25 | end 26 | return unless target_x != map_x || target_y != map_y 27 | 28 | attempt_move(args, target_x, target_y) do 29 | ::Controllers::MapController.tick(args) 30 | @took_action = true 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | 6 | require 'app/behaviour/occupant.rb' 7 | 8 | require 'app/entities/base.rb' 9 | require 'app/entities/static_entity.rb' 10 | require 'app/entities/wall.rb' 11 | require 'app/entities/floor.rb' 12 | require 'app/entities/mobile_entity.rb' 13 | require 'app/entities/player.rb' 14 | require 'app/entities/enemy.rb' 15 | require 'app/entities/zombie.rb' 16 | 17 | class Game 18 | attr_reader :active_controller 19 | 20 | def goto_title 21 | @active_controller = ::Controllers::TitleController 22 | end 23 | 24 | def goto_game(args) 25 | ::Controllers::GameController.reset(args.state) 26 | @active_controller = ::Controllers::GameController 27 | end 28 | 29 | def tick(args) 30 | goto_title unless active_controller 31 | sprites = [] 32 | labels = [] 33 | active_controller.tick(args) 34 | active_controller.render(args.state, sprites, labels) 35 | render(args, sprites, labels) 36 | end 37 | 38 | def render(args, sprites, labels) 39 | args.outputs.sprites << sprites 40 | args.outputs.labels << labels 41 | end 42 | end 43 | 44 | $game ||= Game.new 45 | def tick(args) 46 | $game.tick(args) 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | 6 | require 'app/behaviour/occupant.rb' 7 | 8 | require 'app/entities/base.rb' 9 | require 'app/entities/static_entity.rb' 10 | require 'app/entities/wall.rb' 11 | require 'app/entities/floor.rb' 12 | require 'app/entities/mobile_entity.rb' 13 | require 'app/entities/player.rb' 14 | require 'app/entities/enemy.rb' 15 | require 'app/entities/zombie.rb' 16 | 17 | class Game 18 | attr_reader :active_controller 19 | 20 | def goto_title 21 | @active_controller = ::Controllers::TitleController 22 | end 23 | 24 | def goto_game(args) 25 | ::Controllers::GameController.reset(args.state) 26 | @active_controller = ::Controllers::GameController 27 | end 28 | 29 | def tick(args) 30 | goto_title unless active_controller 31 | sprites = [] 32 | labels = [] 33 | active_controller.tick(args) 34 | active_controller.render(args, sprites, labels) 35 | render(args, sprites, labels) 36 | end 37 | 38 | def render(args, sprites, labels) 39 | args.outputs.sprites << sprites 40 | args.outputs.labels << labels 41 | end 42 | end 43 | 44 | 45 | $game ||= Game.new 46 | def tick(args) 47 | $game.tick(args) 48 | end 49 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | attr_reader :took_action 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/player.png' 8 | end 9 | 10 | def tick(args) 11 | @took_action = false 12 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 13 | map_x + ::Controllers::MapController::TILE_WIDTH 14 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 15 | map_x - ::Controllers::MapController::TILE_WIDTH 16 | else 17 | map_x 18 | end 19 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 20 | map_y + ::Controllers::MapController::TILE_HEIGHT 21 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 22 | map_y - ::Controllers::MapController::TILE_HEIGHT 23 | else 24 | map_y 25 | end 26 | return unless target_x != map_x || target_y != map_y 27 | 28 | attempt_move(args, target_x, target_y) do 29 | ::Controllers::MapController.tick(args) 30 | @took_action = true 31 | update_tile(args) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def linear_distance_to(other) 24 | x_diff = other.map_x - map_x 25 | y_diff = other.map_y - map_y 26 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 27 | end 28 | 29 | def map_tile_x 30 | ::Controllers::MapController.map_x_to_tile_x(map_x) 31 | end 32 | 33 | def map_tile_y 34 | ::Controllers::MapController.map_x_to_tile_x(map_y) 35 | end 36 | 37 | def serialize 38 | { 39 | x: x, 40 | y: y, 41 | w: w, 42 | h: h, 43 | path: path, 44 | blocking: blocking? 45 | } 46 | end 47 | 48 | def inspect 49 | # Override the inspect method and return ~serialize.to_s~. 50 | serialize.to_s 51 | end 52 | 53 | def to_s 54 | # Override to_s and return ~serialize.to_s~. 55 | serialize.to_s 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def linear_distance_to(other) 24 | x_diff = other.map_x - map_x 25 | y_diff = other.map_y - map_y 26 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 27 | end 28 | 29 | def map_tile_x 30 | ::Controllers::MapController.map_x_to_tile_x(map_x) 31 | end 32 | 33 | def map_tile_y 34 | ::Controllers::MapController.map_x_to_tile_x(map_y) 35 | end 36 | 37 | def serialize 38 | { 39 | x: x, 40 | y: y, 41 | w: w, 42 | h: h, 43 | path: path, 44 | blocking: blocking? 45 | } 46 | end 47 | 48 | def inspect 49 | # Override the inspect method and return ~serialize.to_s~. 50 | serialize.to_s 51 | end 52 | 53 | def to_s 54 | # Override to_s and return ~serialize.to_s~. 55 | serialize.to_s 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | 6 | require 'app/behaviour/occupant.rb' 7 | require 'app/behaviour/defender.rb' 8 | require 'app/behaviour/attacker.rb' 9 | 10 | require 'app/entities/base.rb' 11 | require 'app/entities/static_entity.rb' 12 | require 'app/entities/wall.rb' 13 | require 'app/entities/floor.rb' 14 | require 'app/entities/mobile_entity.rb' 15 | require 'app/entities/player.rb' 16 | require 'app/entities/enemy.rb' 17 | require 'app/entities/zombie.rb' 18 | 19 | class Game 20 | attr_reader :active_controller 21 | 22 | def goto_title 23 | @active_controller = ::Controllers::TitleController 24 | end 25 | 26 | def goto_game(args) 27 | ::Controllers::GameController.reset(args.state) 28 | @active_controller = ::Controllers::GameController 29 | end 30 | 31 | def tick(args) 32 | goto_title unless active_controller 33 | sprites = [] 34 | labels = [] 35 | active_controller.tick(args) 36 | active_controller.render(args, sprites, labels) 37 | render(args, sprites, labels) 38 | end 39 | 40 | def render(args, sprites, labels) 41 | args.outputs.sprites << sprites 42 | args.outputs.labels << labels 43 | end 44 | end 45 | 46 | 47 | $game ||= Game.new 48 | def tick(args) 49 | $game.tick(args) 50 | end 51 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | attr_reader :took_action 4 | 5 | def initialize(opts = {}) 6 | super 7 | @path = 'app/sprites/player.png' 8 | end 9 | 10 | def tick(args) 11 | @took_action = false 12 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 13 | map_x + ::Controllers::MapController::TILE_WIDTH 14 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 15 | map_x - ::Controllers::MapController::TILE_WIDTH 16 | else 17 | map_x 18 | end 19 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 20 | map_y + ::Controllers::MapController::TILE_HEIGHT 21 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 22 | map_y - ::Controllers::MapController::TILE_HEIGHT 23 | else 24 | map_y 25 | end 26 | return unless target_x != map_x || target_y != map_y 27 | 28 | attempt_move(args, target_x, target_y) do 29 | ::Controllers::MapController.tick(args) 30 | @took_action = true 31 | args.state.redraw_entities = true 32 | args.state.redraw_play_area = true 33 | update_tile(args) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def linear_distance_to(other) 24 | x_diff = other.map_x - map_x 25 | y_diff = other.map_y - map_y 26 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 27 | end 28 | 29 | def map_tile_x 30 | ::Controllers::MapController.map_x_to_tile_x(map_x) 31 | end 32 | 33 | def map_tile_y 34 | ::Controllers::MapController.map_x_to_tile_x(map_y) 35 | end 36 | 37 | def serialize 38 | { 39 | x: x, 40 | y: y, 41 | w: w, 42 | h: h, 43 | path: path, 44 | blocking: blocking? 45 | } 46 | end 47 | 48 | def inspect 49 | # Override the inspect method and return ~serialize.to_s~. 50 | serialize.to_s 51 | end 52 | 53 | def to_s 54 | # Override to_s and return ~serialize.to_s~. 55 | serialize.to_s 56 | end 57 | 58 | def respond_to?(method) 59 | self.class.method_defined?(method.to_sym) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | 6 | require 'app/behaviour/occupant.rb' 7 | require 'app/behaviour/defender.rb' 8 | require 'app/behaviour/attacker.rb' 9 | 10 | require 'app/dice/dice.rb' 11 | require 'app/dice/d20.rb' 12 | 13 | require 'app/entities/base.rb' 14 | require 'app/entities/static_entity.rb' 15 | require 'app/entities/wall.rb' 16 | require 'app/entities/floor.rb' 17 | require 'app/entities/mobile_entity.rb' 18 | require 'app/entities/player.rb' 19 | require 'app/entities/enemy.rb' 20 | require 'app/entities/zombie.rb' 21 | 22 | class Game 23 | attr_reader :active_controller 24 | 25 | def goto_title 26 | @active_controller = ::Controllers::TitleController 27 | end 28 | 29 | def goto_game(args) 30 | ::Controllers::GameController.reset(args.state) 31 | @active_controller = ::Controllers::GameController 32 | end 33 | 34 | def tick(args) 35 | goto_title unless active_controller 36 | sprites = [] 37 | labels = [] 38 | active_controller.tick(args) 39 | active_controller.render(args, sprites, labels) 40 | render(args, sprites, labels) 41 | end 42 | 43 | def render(args, sprites, labels) 44 | args.outputs.sprites << sprites 45 | args.outputs.labels << labels 46 | end 47 | end 48 | 49 | 50 | $game ||= Game.new 51 | def tick(args) 52 | $game.tick(args) 53 | end 54 | -------------------------------------------------------------------------------- /roguelike_oo/README.md: -------------------------------------------------------------------------------- 1 | ## DragonRuby Sprite-based Roguelike Tutorial - Work In Progress 2 | This is a set of tutorials to create a top-down 'roguelike' 3 | 4 | ## Tutorial 01 5 | Sets up a basic project structure, with a title screen and game state that transitions from title screen to game 6 | 7 | [Tutorial 01](./01/tutorial.md) 8 | 9 | ## Tutorial 02 10 | Adds a Map Controller, and static entities to display floor/wall tiles 11 | 12 | [Tutorial 02](./02/tutorial.md) 13 | 14 | ## Tutorial 03 15 | Adds a moving Player Entity 16 | 17 | [Tutorial 03](./03/tutorial.md) 18 | 19 | ## Tutorial 04 20 | Extends Map, Player, and static entity behaviour to make the 'camera' follow the player, enable tile-based collisions between the player and the map. 21 | 22 | [Tutorial 04](./04/tutorial.md) 23 | 24 | ## Tutorial 05 25 | Adds Enemy Entities, and Enemy Controller, and a very primitive random movement (subject to tile collisions) for the enemies 26 | 27 | [Tutorial 05](./05/tutorial.md) 28 | 29 | ## Tutorial 06 30 | Adds Enemy behaviour to hunt players, and adds entity-to-entity collisions 31 | 32 | [Tutorial 06](./06/tutorial.md) 33 | 34 | ## Tutorial 07 35 | Adds Render Targets to split the screen into a play area and a test area. 36 | 37 | [Tutorial 07](./07/tutorial.md) 38 | 39 | ## Tutorial 08 40 | Combat between Players and enemies 41 | 42 | [Tutorial 08](./08/tutorial.md) 43 | 44 | ## Tutorial 09 45 | Dice Rolls & Death 46 | 47 | [Tutorial 09](./09/tutorial.md) 48 | 49 | ## Tutorial 10 50 | Logging Events 51 | 52 | [Tutorial 10](./10/tutorial.md) 53 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | include ::Behaviour::Occupant 4 | 5 | def self.spawn_near(state, spawn_x, spawn_y) 6 | radius = 1 7 | attempt = 0 8 | tile = state.map.tiles[spawn_x][spawn_y] 9 | while tile.nil? || tile.blocking? 10 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 11 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 12 | tile = state.map.tiles[spawn_x][spawn_y] 13 | attempt += 1 14 | next unless attempt >= radius * 8 15 | 16 | radius += 1 17 | attempt = 0 18 | end 19 | new( 20 | map_x: spawn_x * SPRITE_WIDTH, 21 | map_y: spawn_y * SPRITE_HEIGHT 22 | ) 23 | end 24 | 25 | def move_or_attack(args, target_x, target_y) 26 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 27 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 28 | if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 29 | other = ::Controllers::MapController.tile_occupant(args, tile_x, tile_y) 30 | if respond_to?(:deal_damage) && other 31 | deal_damage(other) 32 | yield 33 | end 34 | else 35 | @map_x = target_x 36 | @map_y = target_y 37 | yield if block_given? 38 | end 39 | @x = map_x - args.state.map.x 40 | @y = map_y - args.state.map.y 41 | end 42 | 43 | def blocking? 44 | true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/main.rb: -------------------------------------------------------------------------------- 1 | require 'app/controllers/title_controller.rb' 2 | require 'app/controllers/game_controller.rb' 3 | require 'app/controllers/map_controller.rb' 4 | require 'app/controllers/enemy_controller.rb' 5 | require 'app/controllers/event_logs_controller.rb' 6 | 7 | require 'app/behaviour/occupant.rb' 8 | require 'app/behaviour/defender.rb' 9 | require 'app/behaviour/attacker.rb' 10 | 11 | require 'app/dice/dice.rb' 12 | require 'app/dice/d20.rb' 13 | 14 | require 'app/entities/base.rb' 15 | require 'app/entities/static_entity.rb' 16 | require 'app/entities/wall.rb' 17 | require 'app/entities/floor.rb' 18 | require 'app/entities/mobile_entity.rb' 19 | require 'app/entities/player.rb' 20 | require 'app/entities/enemy.rb' 21 | require 'app/entities/zombie.rb' 22 | 23 | class Game 24 | attr_reader :active_controller 25 | 26 | def goto_title 27 | @active_controller = ::Controllers::TitleController 28 | end 29 | 30 | def goto_game(args) 31 | ::Controllers::GameController.reset(args.state) 32 | @active_controller = ::Controllers::GameController 33 | end 34 | 35 | def tick(args) 36 | goto_title unless active_controller 37 | sprites = [] 38 | labels = [] 39 | active_controller.tick(args) 40 | active_controller.render(args, sprites, labels) 41 | render(args, sprites, labels) 42 | end 43 | 44 | def render(args, sprites, labels) 45 | args.outputs.sprites << sprites 46 | args.outputs.labels << labels 47 | end 48 | end 49 | 50 | 51 | $game ||= Game.new 52 | def tick(args) 53 | $game.tick(args) 54 | end 55 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def linear_distance_to(other) 24 | x_diff = other.map_x - map_x 25 | y_diff = other.map_y - map_y 26 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 27 | end 28 | 29 | def map_tile_x 30 | ::Controllers::MapController.map_x_to_tile_x(map_x) 31 | end 32 | 33 | def map_tile_y 34 | ::Controllers::MapController.map_x_to_tile_x(map_y) 35 | end 36 | 37 | def faction 38 | 'neutral' 39 | end 40 | 41 | def serialize 42 | { 43 | x: x, 44 | y: y, 45 | w: w, 46 | h: h, 47 | path: path, 48 | blocking: blocking? 49 | } 50 | end 51 | 52 | def inspect 53 | # Override the inspect method and return ~serialize.to_s~. 54 | serialize.to_s 55 | end 56 | 57 | def to_s 58 | # Override to_s and return ~serialize.to_s~. 59 | serialize.to_s 60 | end 61 | 62 | def respond_to?(method) 63 | self.class.method_defined?(method.to_sym) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | VISIBLE_RANGE = 300 4 | 5 | def tick(args) 6 | act(args) 7 | @x = map_x - args.state.map.x 8 | @y = map_y - args.state.map.y 9 | end 10 | 11 | def act(args) 12 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 13 | seek_player(args) 14 | else 15 | patrol(args) 16 | end 17 | end 18 | 19 | def seek_player(args) 20 | directions = [] 21 | player = args.state.player 22 | directions << :left if player.map_x < map_x 23 | directions << :right if player.map_x > map_x 24 | directions << :up if player.map_y > map_y 25 | directions << :down if player.map_y < map_y 26 | direction = directions.sample 27 | move_towards(args, direction) 28 | end 29 | 30 | def patrol(args) 31 | direction = [:up, :down, :left, :right].sample 32 | move_towards(args, direction) 33 | end 34 | 35 | def move_towards(args, direction) 36 | target_x = map_x 37 | target_y = map_y 38 | case direction 39 | when :up 40 | target_y += ::Controllers::MapController::TILE_HEIGHT 41 | when :down 42 | target_y -= ::Controllers::MapController::TILE_HEIGHT 43 | when :left 44 | target_x -= ::Controllers::MapController::TILE_WIDTH 45 | when :right 46 | target_x += ::Controllers::MapController::TILE_WIDTH 47 | end 48 | attempt_move(args, target_x, target_y) do 49 | update_tile(args) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | VISIBLE_RANGE = 300 4 | 5 | def tick(args) 6 | act(args) 7 | @x = map_x - args.state.map.x 8 | @y = map_y - args.state.map.y 9 | end 10 | 11 | def act(args) 12 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 13 | seek_player(args) 14 | else 15 | patrol(args) 16 | end 17 | end 18 | 19 | def seek_player(args) 20 | directions = [] 21 | player = args.state.player 22 | directions << :left if player.map_x < map_x 23 | directions << :right if player.map_x > map_x 24 | directions << :up if player.map_y > map_y 25 | directions << :down if player.map_y < map_y 26 | direction = directions.sample 27 | move_towards(args, direction) 28 | end 29 | 30 | def patrol(args) 31 | direction = [:up, :down, :left, :right].sample 32 | move_towards(args, direction) 33 | end 34 | 35 | def move_towards(args, direction) 36 | target_x = map_x 37 | target_y = map_y 38 | case direction 39 | when :up 40 | target_y += ::Controllers::MapController::TILE_HEIGHT 41 | when :down 42 | target_y -= ::Controllers::MapController::TILE_HEIGHT 43 | when :left 44 | target_x -= ::Controllers::MapController::TILE_WIDTH 45 | when :right 46 | target_x += ::Controllers::MapController::TILE_WIDTH 47 | end 48 | attempt_move(args, target_x, target_y) do 49 | update_tile(args) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | include ::Behaviour::Occupant 4 | 5 | def self.spawn_near(state, spawn_x, spawn_y) 6 | radius = 1 7 | attempt = 0 8 | tile = state.map.tiles[spawn_x][spawn_y] 9 | while tile.nil? || tile.blocking? 10 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 11 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 12 | tile = state.map.tiles[spawn_x][spawn_y] 13 | attempt += 1 14 | next unless attempt >= radius * 8 15 | 16 | radius += 1 17 | attempt = 0 18 | end 19 | new( 20 | map_x: spawn_x * SPRITE_WIDTH, 21 | map_y: spawn_y * SPRITE_HEIGHT 22 | ) 23 | end 24 | 25 | def move_or_attack(args, target_x, target_y) 26 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 27 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 28 | if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 29 | other = ::Controllers::MapController.tile_occupant(args, tile_x, tile_y) 30 | if respond_to?(:deal_damage) && other && (other.faction != 'neutral' && other.faction != faction) 31 | deal_damage(other) 32 | yield 33 | end 34 | else 35 | @map_x = target_x 36 | @map_y = target_y 37 | yield if block_given? 38 | end 39 | @x = map_x - args.state.map.x 40 | @y = map_y - args.state.map.y 41 | end 42 | 43 | def blocking? 44 | true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/mobile_entity.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class MobileEntity < Base 3 | include ::Behaviour::Occupant 4 | 5 | def self.spawn_near(state, spawn_x, spawn_y) 6 | radius = 1 7 | attempt = 0 8 | tile = state.map.tiles[spawn_x][spawn_y] 9 | while tile.nil? || tile.blocking? 10 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 11 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 12 | tile = state.map.tiles[spawn_x][spawn_y] 13 | attempt += 1 14 | next unless attempt >= radius * 8 15 | 16 | radius += 1 17 | attempt = 0 18 | end 19 | new( 20 | map_x: spawn_x * SPRITE_WIDTH, 21 | map_y: spawn_y * SPRITE_HEIGHT 22 | ) 23 | end 24 | 25 | def move_or_attack(args, target_x, target_y) 26 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 27 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 28 | if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 29 | other = ::Controllers::MapController.tile_occupant(args, tile_x, tile_y) 30 | if respond_to?(:deal_damage) && other && (other.faction != 'neutral' && other.faction != faction) 31 | deal_damage(other) 32 | yield 33 | end 34 | else 35 | @map_x = target_x 36 | @map_y = target_y 37 | yield if block_given? 38 | end 39 | @x = map_x - args.state.map.x 40 | @y = map_y - args.state.map.y 41 | end 42 | 43 | def blocking? 44 | true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | attr_reader :took_action 6 | 7 | def initialize(opts = {}) 8 | super 9 | @path = 'app/sprites/player.png' 10 | @hp = 50 11 | @defense = 10 12 | @attack = 3 13 | end 14 | 15 | def tick(args) 16 | @took_action = false 17 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 18 | map_x + ::Controllers::MapController::TILE_WIDTH 19 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 20 | map_x - ::Controllers::MapController::TILE_WIDTH 21 | else 22 | map_x 23 | end 24 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 25 | map_y + ::Controllers::MapController::TILE_HEIGHT 26 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 27 | map_y - ::Controllers::MapController::TILE_HEIGHT 28 | else 29 | map_y 30 | end 31 | return unless target_x != map_x || target_y != map_y 32 | 33 | move_or_attack(args, target_x, target_y) do 34 | ::Controllers::MapController.tick(args) 35 | @took_action = true 36 | args.state.redraw_entities = true 37 | args.state.redraw_play_area = true 38 | update_tile(args) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/base.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Base 3 | attr_sprite 4 | attr_reader :map_x, :map_y 5 | 6 | SPRITE_WIDTH = 32 7 | SPRITE_HEIGHT = 32 8 | 9 | def initialize(opts = {}) 10 | @map_x = opts[:map_x] || 0 11 | @map_y = opts[:map_y] || 0 12 | @x = map_x 13 | @y = map_y 14 | @w = opts[:w] || SPRITE_WIDTH 15 | @h = opts[:h] || SPRITE_HEIGHT 16 | @path = opts[:path] || 'app/sprites/null_sprite.png' 17 | end 18 | 19 | def blocking? 20 | false 21 | end 22 | 23 | def linear_distance_to(other) 24 | x_diff = other.map_x - map_x 25 | y_diff = other.map_y - map_y 26 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 27 | end 28 | 29 | def map_tile_x 30 | ::Controllers::MapController.map_x_to_tile_x(map_x) 31 | end 32 | 33 | def map_tile_y 34 | ::Controllers::MapController.map_x_to_tile_x(map_y) 35 | end 36 | 37 | def faction 38 | 'neutral' 39 | end 40 | 41 | def name 42 | '' 43 | end 44 | 45 | def serialize 46 | { 47 | x: x, 48 | y: y, 49 | w: w, 50 | h: h, 51 | path: path, 52 | blocking: blocking? 53 | } 54 | end 55 | 56 | def inspect 57 | # Override the inspect method and return ~serialize.to_s~. 58 | serialize.to_s 59 | end 60 | 61 | def to_s 62 | # Override to_s and return ~serialize.to_s~. 63 | serialize.to_s 64 | end 65 | 66 | def respond_to?(method) 67 | self.class.method_defined?(method.to_sym) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | attr_reader :took_action 6 | 7 | def initialize(opts = {}) 8 | super 9 | @path = 'app/sprites/player.png' 10 | @hp = 50 11 | @defense = 10 12 | @attack = 3 13 | @crit_bonus = 1 14 | end 15 | 16 | def faction 17 | 'player' 18 | end 19 | 20 | def tick(args) 21 | @took_action = false 22 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 23 | map_x + ::Controllers::MapController::TILE_WIDTH 24 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 25 | map_x - ::Controllers::MapController::TILE_WIDTH 26 | else 27 | map_x 28 | end 29 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 30 | map_y + ::Controllers::MapController::TILE_HEIGHT 31 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 32 | map_y - ::Controllers::MapController::TILE_HEIGHT 33 | else 34 | map_y 35 | end 36 | return unless target_x != map_x || target_y != map_y 37 | 38 | move_or_attack(args, target_x, target_y) do 39 | ::Controllers::MapController.tick(args) 40 | @took_action = true 41 | args.state.redraw_entities = true 42 | args.state.redraw_play_area = true 43 | update_tile(args) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | VISIBLE_RANGE = 300 6 | 7 | def initialize(opts = {}) 8 | super 9 | @hp = 10 10 | @defense = 0 11 | @attack = 1 12 | end 13 | 14 | def tick(args) 15 | act(args) 16 | @x = map_x - args.state.map.x 17 | @y = map_y - args.state.map.y 18 | end 19 | 20 | def act(args) 21 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 22 | seek_player(args) 23 | else 24 | patrol(args) 25 | end 26 | end 27 | 28 | def seek_player(args) 29 | directions = [] 30 | player = args.state.player 31 | directions << :left if player.map_x < map_x 32 | directions << :right if player.map_x > map_x 33 | directions << :up if player.map_y > map_y 34 | directions << :down if player.map_y < map_y 35 | direction = directions.sample 36 | move_towards(args, direction) 37 | end 38 | 39 | def patrol(args) 40 | direction = [:up, :down, :left, :right].sample 41 | move_towards(args, direction) 42 | end 43 | 44 | def move_towards(args, direction) 45 | target_x = map_x 46 | target_y = map_y 47 | case direction 48 | when :up 49 | target_y += ::Controllers::MapController::TILE_HEIGHT 50 | when :down 51 | target_y -= ::Controllers::MapController::TILE_HEIGHT 52 | when :left 53 | target_x -= ::Controllers::MapController::TILE_WIDTH 54 | when :right 55 | target_x += ::Controllers::MapController::TILE_WIDTH 56 | end 57 | move_or_attack(args, target_x, target_y) do 58 | update_tile(args) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | VISIBLE_RANGE = 300 6 | 7 | def initialize(opts = {}) 8 | super 9 | @hp = 10 10 | @defense = 0 11 | @attack = 1 12 | @crit_bonus = 1 13 | end 14 | 15 | def tick(args) 16 | if alive? 17 | act(args) 18 | @x = map_x - args.state.map.x 19 | @y = map_y - args.state.map.y 20 | else 21 | free_tile_on_death(args) 22 | end 23 | end 24 | 25 | def act(args) 26 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 27 | seek_player(args) 28 | else 29 | patrol(args) 30 | end 31 | end 32 | 33 | def seek_player(args) 34 | directions = [] 35 | player = args.state.player 36 | directions << :left if player.map_x < map_x 37 | directions << :right if player.map_x > map_x 38 | directions << :up if player.map_y > map_y 39 | directions << :down if player.map_y < map_y 40 | direction = directions.sample 41 | move_towards(args, direction) 42 | end 43 | 44 | def patrol(args) 45 | direction = [:up, :down, :left, :right].sample 46 | move_towards(args, direction) 47 | end 48 | 49 | def move_towards(args, direction) 50 | target_x = map_x 51 | target_y = map_y 52 | case direction 53 | when :up 54 | target_y += ::Controllers::MapController::TILE_HEIGHT 55 | when :down 56 | target_y -= ::Controllers::MapController::TILE_HEIGHT 57 | when :left 58 | target_x -= ::Controllers::MapController::TILE_WIDTH 59 | when :right 60 | target_x += ::Controllers::MapController::TILE_WIDTH 61 | end 62 | move_or_attack(args, target_x, target_y) do 63 | update_tile(args) 64 | end 65 | end 66 | 67 | def faction 68 | 'enemy' 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/enemy.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Enemy < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | VISIBLE_RANGE = 300 6 | 7 | def initialize(opts = {}) 8 | super 9 | @max_hp = 10 10 | @hp = max_hp 11 | @defense = 0 12 | @attack = 1 13 | @crit_bonus = 1 14 | end 15 | 16 | def tick(args) 17 | if alive? 18 | act(args) 19 | @x = map_x - args.state.map.x 20 | @y = map_y - args.state.map.y 21 | else 22 | free_tile_on_death(args) 23 | end 24 | end 25 | 26 | def act(args) 27 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 28 | seek_player(args) 29 | else 30 | patrol(args) 31 | end 32 | end 33 | 34 | def seek_player(args) 35 | directions = [] 36 | player = args.state.player 37 | directions << :left if player.map_x < map_x 38 | directions << :right if player.map_x > map_x 39 | directions << :up if player.map_y > map_y 40 | directions << :down if player.map_y < map_y 41 | direction = directions.sample 42 | move_towards(args, direction) 43 | end 44 | 45 | def patrol(args) 46 | direction = [:up, :down, :left, :right].sample 47 | move_towards(args, direction) 48 | end 49 | 50 | def move_towards(args, direction) 51 | target_x = map_x 52 | target_y = map_y 53 | case direction 54 | when :up 55 | target_y += ::Controllers::MapController::TILE_HEIGHT 56 | when :down 57 | target_y -= ::Controllers::MapController::TILE_HEIGHT 58 | when :left 59 | target_x -= ::Controllers::MapController::TILE_WIDTH 60 | when :right 61 | target_x += ::Controllers::MapController::TILE_WIDTH 62 | end 63 | move_or_attack(args, target_x, target_y) do 64 | update_tile(args) 65 | end 66 | end 67 | 68 | def faction 69 | 'enemy' 70 | end 71 | 72 | def name 73 | 'Enemy' 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | PLAY_AREA_WIDTH = 832 4 | PLAY_AREA_HEIGHT = 720 5 | TEXT_AREA_WIDTH = 1280 - PLAY_AREA_WIDTH 6 | TEXT_AREA_HEIGHT = 720 7 | 8 | def self.tick(args) 9 | args.state.player.tick(args) 10 | ::Controllers::EnemyController.tick(args) 11 | end 12 | 13 | def self.render(args, sprites, labels) 14 | render_play_area(args) if args.state.redraw_play_area 15 | render_entities(args) if args.state.redraw_entities 16 | render_text_area(args) if args.state.redraw_text_area 17 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :play_area} 18 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :entities} 19 | sprites << {x: PLAY_AREA_WIDTH, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: TEXT_AREA_WIDTH, source_h: TEXT_AREA_HEIGHT, path: :text_area} 20 | end 21 | 22 | def self.render_play_area(args) 23 | args.state.redraw_play_area = false 24 | args.render_target(:play_area).sprites << args.state.map.tiles 25 | end 26 | 27 | def self.render_entities(args) 28 | args.state.redraw_entities = false 29 | args.render_target(:entities).sprites << args.state.enemies 30 | args.render_target(:entities).sprites << args.state.player 31 | end 32 | 33 | def self.render_text_area(args) 34 | args.state.redraw_text_area = false 35 | args.render_target(:text_area).solids << {x: 0, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, r: 10, g: 21, b: 33} 36 | end 37 | 38 | def self.reset(state) 39 | ::Controllers::MapController.load_map(state) 40 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 41 | ::Controllers::EnemyController.spawn_enemies(state) 42 | state.redraw_play_area = true 43 | state.redraw_entities = true 44 | state.redraw_text_area = true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | PLAY_AREA_WIDTH = 832 4 | PLAY_AREA_HEIGHT = 720 5 | TEXT_AREA_WIDTH = 1280 - PLAY_AREA_WIDTH 6 | TEXT_AREA_HEIGHT = 720 7 | 8 | def self.tick(args) 9 | args.state.player.tick(args) 10 | ::Controllers::EnemyController.tick(args) 11 | end 12 | 13 | def self.render(args, sprites, labels) 14 | render_play_area(args) if args.state.redraw_play_area 15 | render_entities(args) if args.state.redraw_entities 16 | render_text_area(args) if args.state.redraw_text_area 17 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :play_area} 18 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :entities} 19 | sprites << {x: PLAY_AREA_WIDTH, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: TEXT_AREA_WIDTH, source_h: TEXT_AREA_HEIGHT, path: :text_area} 20 | end 21 | 22 | def self.render_play_area(args) 23 | args.state.redraw_play_area = false 24 | args.render_target(:play_area).sprites << args.state.map.tiles 25 | end 26 | 27 | def self.render_entities(args) 28 | args.state.redraw_entities = false 29 | args.render_target(:entities).sprites << args.state.enemies 30 | args.render_target(:entities).sprites << args.state.player 31 | end 32 | 33 | def self.render_text_area(args) 34 | args.state.redraw_text_area = false 35 | args.render_target(:text_area).solids << {x: 0, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, r: 10, g: 21, b: 33} 36 | end 37 | 38 | def self.reset(state) 39 | ::Controllers::MapController.load_map(state) 40 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 41 | ::Controllers::EnemyController.spawn_enemies(state) 42 | state.redraw_play_area = true 43 | state.redraw_entities = true 44 | state.redraw_text_area = true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | PLAY_AREA_WIDTH = 832 4 | PLAY_AREA_HEIGHT = 720 5 | TEXT_AREA_WIDTH = 1280 - PLAY_AREA_WIDTH 6 | TEXT_AREA_HEIGHT = 720 7 | 8 | def self.tick(args) 9 | args.state.player.tick(args) 10 | ::Controllers::EnemyController.tick(args) 11 | end 12 | 13 | def self.render(args, sprites, labels) 14 | render_play_area(args) if args.state.redraw_play_area 15 | render_entities(args) if args.state.redraw_entities 16 | render_text_area(args) if args.state.redraw_text_area 17 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :play_area} 18 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :entities} 19 | sprites << {x: PLAY_AREA_WIDTH, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: TEXT_AREA_WIDTH, source_h: TEXT_AREA_HEIGHT, path: :text_area} 20 | end 21 | 22 | def self.render_play_area(args) 23 | args.state.redraw_play_area = false 24 | args.render_target(:play_area).sprites << args.state.map.tiles 25 | end 26 | 27 | def self.render_entities(args) 28 | args.state.redraw_entities = false 29 | args.render_target(:entities).sprites << args.state.enemies 30 | args.render_target(:entities).sprites << args.state.player 31 | end 32 | 33 | def self.render_text_area(args) 34 | args.state.redraw_text_area = false 35 | args.render_target(:text_area).solids << {x: 0, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, r: 10, g: 21, b: 33} 36 | end 37 | 38 | def self.reset(state) 39 | ::Controllers::MapController.load_map(state) 40 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 41 | ::Controllers::EnemyController.spawn_enemies(state) 42 | state.redraw_play_area = true 43 | state.redraw_entities = true 44 | state.redraw_text_area = true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/entities/player.rb: -------------------------------------------------------------------------------- 1 | module Entities 2 | class Player < MobileEntity 3 | include ::Behaviour::Defender 4 | include ::Behaviour::Attacker 5 | attr_reader :took_action, :took_damage 6 | 7 | def initialize(opts = {}) 8 | super 9 | @path = 'app/sprites/player.png' 10 | @max_hp = 50 11 | @hp = max_hp 12 | @defense = 10 13 | @attack = 3 14 | @crit_bonus = 1 15 | end 16 | 17 | def faction 18 | 'player' 19 | end 20 | 21 | def name 22 | 'Player' 23 | end 24 | 25 | def tick(args) 26 | @took_action = false 27 | @took_damange = false 28 | target_x = if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 29 | map_x + ::Controllers::MapController::TILE_WIDTH 30 | elsif args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 31 | map_x - ::Controllers::MapController::TILE_WIDTH 32 | else 33 | map_x 34 | end 35 | target_y = if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 36 | map_y + ::Controllers::MapController::TILE_HEIGHT 37 | elsif args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 38 | map_y - ::Controllers::MapController::TILE_HEIGHT 39 | else 40 | map_y 41 | end 42 | return unless target_x != map_x || target_y != map_y 43 | 44 | move_or_attack(args, target_x, target_y) do 45 | ::Controllers::MapController.tick(args) 46 | @took_action = true 47 | args.state.redraw_entities = true 48 | args.state.redraw_play_area = true 49 | update_tile(args) 50 | end 51 | end 52 | 53 | def take_damage(damage) 54 | super 55 | @took_damage = true 56 | end 57 | 58 | def stats_labels 59 | [ 60 | {x: 16, y: 700, text: hp_string}.merge(hp_string_color) 61 | ] 62 | end 63 | 64 | def hp_string 65 | hp_label = (hp < 10) ? "0#{hp}" : hp.to_s 66 | max_hp_label = (max_hp < 10) ? "0#{max_hp}" : max_hp.to_s 67 | "HP: #{hp_label} / #{max_hp_label}" 68 | end 69 | 70 | def hp_string_color 71 | if hp / max_hp >= 0.5 72 | {r: 10, g: 200, b: 10, a: 255} 73 | elsif hp / max_hp >= 0.2 74 | {r: 255, g: 165, b: 0, a: 255} 75 | else 76 | {r: 220, g: 0, b: 0, a: 255} 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/controllers/game_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class GameController 3 | PLAY_AREA_WIDTH = 832 4 | PLAY_AREA_HEIGHT = 720 5 | TEXT_AREA_WIDTH = 1280 - PLAY_AREA_WIDTH 6 | TEXT_AREA_HEIGHT = 720 7 | 8 | def self.tick(args) 9 | args.state.logged_event_this_tick = false 10 | args.state.player.tick(args) 11 | if args.state.player.took_damage || args.state.logged_event_this_tick 12 | args.state.redraw_text_area = true 13 | end 14 | ::Controllers::EnemyController.tick(args) 15 | end 16 | 17 | def self.render(args, sprites, labels) 18 | render_play_area(args) if args.state.redraw_play_area 19 | render_entities(args) if args.state.redraw_entities 20 | render_text_area(args) if args.state.redraw_text_area 21 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :play_area} 22 | sprites << {x: 0, y: 0, w: PLAY_AREA_WIDTH, h: PLAY_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: PLAY_AREA_WIDTH, source_h: PLAY_AREA_HEIGHT, path: :entities} 23 | sprites << {x: PLAY_AREA_WIDTH, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, source_x: 0, source_y: 0, source_w: TEXT_AREA_WIDTH, source_h: TEXT_AREA_HEIGHT, path: :text_area} 24 | end 25 | 26 | def self.render_play_area(args) 27 | args.state.redraw_play_area = false 28 | args.render_target(:play_area).sprites << args.state.map.tiles 29 | end 30 | 31 | def self.render_entities(args) 32 | args.state.redraw_entities = false 33 | args.render_target(:entities).sprites << args.state.enemies 34 | args.render_target(:entities).sprites << args.state.player 35 | end 36 | 37 | def self.render_text_area(args) 38 | args.state.redraw_text_area = false 39 | args.render_target(:text_area).solids << {x: 0, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, r: 10, g: 21, b: 33} 40 | args.render_target(:text_area).labels << args.state.player.stats_labels 41 | args.render_target(:text_area).labels << ::Controllers::EventLogsController.events_as_labels(args) 42 | end 43 | 44 | def self.reset(state) 45 | ::Controllers::EventLogsController.reset(state) 46 | ::Controllers::MapController.load_map(state) 47 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 48 | ::Controllers::EnemyController.spawn_enemies(state) 49 | state.redraw_play_area = true 50 | state.redraw_entities = true 51 | state.redraw_text_area = true 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /roguelike_oo/04/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (1280 - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (720 - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | return true if tile_x < 0 || tile_x > MAP_WIDTH - 1 31 | return true if tile_y < 0 || tile_y > MAP_HEIGHT - 1 32 | 33 | tile = args.state.map.tiles[tile_x][tile_y] 34 | tile.blocking? 35 | end 36 | 37 | def self.map_x_to_tile_x(map_x) 38 | (map_x / TILE_WIDTH).floor 39 | end 40 | 41 | def self.map_y_to_tile_y(map_y) 42 | (map_y / TILE_HEIGHT).floor 43 | end 44 | 45 | def self.load_map(state) 46 | state.map.tiles = map_tiles 47 | state.map.x = 0 48 | state.map.y = 0 49 | end 50 | 51 | def self.map_tiles 52 | MAP_WIDTH.times.map do |tile_x| 53 | MAP_HEIGHT.times.map do |tile_y| 54 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 55 | tile_x == 0 || tile_x == MAP_WIDTH - 1 56 | tile_for tile_x, tile_y, Entities::Wall 57 | else 58 | if (0..8).to_a.sample == 0 59 | tile_for tile_x, tile_y, Entities::Wall 60 | else 61 | tile_for tile_x, tile_y, Entities::Floor 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | def self.tile_for(tile_x, tile_y, tile_type) 69 | tile_type.new( 70 | map_x: tile_x * TILE_WIDTH, 71 | map_y: tile_y * TILE_HEIGHT, 72 | w: TILE_WIDTH, 73 | h: TILE_HEIGHT 74 | ) 75 | end 76 | 77 | def self.min_x 78 | 0 79 | end 80 | 81 | def self.min_y 82 | 0 83 | end 84 | 85 | def self.max_x 86 | MAP_WIDTH * TILE_WIDTH - 1280 87 | end 88 | 89 | def self.max_y 90 | MAP_HEIGHT * TILE_HEIGHT - 720 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /roguelike_oo/05/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (1280 - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (720 - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | return true if tile_x < 0 || tile_x > MAP_WIDTH - 1 31 | return true if tile_y < 0 || tile_y > MAP_HEIGHT - 1 32 | 33 | tile = args.state.map.tiles[tile_x][tile_y] 34 | tile.blocking? 35 | end 36 | 37 | def self.map_x_to_tile_x(map_x) 38 | (map_x / TILE_WIDTH).floor 39 | end 40 | 41 | def self.map_y_to_tile_y(map_y) 42 | (map_y / TILE_HEIGHT).floor 43 | end 44 | 45 | def self.load_map(state) 46 | state.map.tiles = map_tiles 47 | state.map.x = 0 48 | state.map.y = 0 49 | end 50 | 51 | def self.map_tiles 52 | MAP_WIDTH.times.map do |tile_x| 53 | MAP_HEIGHT.times.map do |tile_y| 54 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 55 | tile_x == 0 || tile_x == MAP_WIDTH - 1 56 | tile_for tile_x, tile_y, Entities::Wall 57 | else 58 | if (0..8).to_a.sample == 0 59 | tile_for tile_x, tile_y, Entities::Wall 60 | else 61 | tile_for tile_x, tile_y, Entities::Floor 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | def self.tile_for(tile_x, tile_y, tile_type) 69 | tile_type.new( 70 | map_x: tile_x * TILE_WIDTH, 71 | map_y: tile_y * TILE_HEIGHT, 72 | w: TILE_WIDTH, 73 | h: TILE_HEIGHT 74 | ) 75 | end 76 | 77 | def self.min_x 78 | 0 79 | end 80 | 81 | def self.min_y 82 | 0 83 | end 84 | 85 | def self.max_x 86 | MAP_WIDTH * TILE_WIDTH - 1280 87 | end 88 | 89 | def self.max_y 90 | MAP_HEIGHT * TILE_HEIGHT - 720 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /roguelike_oo/06/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (1280 - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (720 - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | return true if tile_x < 0 || tile_x > MAP_WIDTH - 1 31 | return true if tile_y < 0 || tile_y > MAP_HEIGHT - 1 32 | 33 | tile = args.state.map.tiles[tile_x][tile_y] 34 | tile.blocking? 35 | end 36 | 37 | def self.map_x_to_tile_x(map_x) 38 | (map_x / TILE_WIDTH).floor 39 | end 40 | 41 | def self.map_y_to_tile_y(map_y) 42 | (map_y / TILE_HEIGHT).floor 43 | end 44 | 45 | def self.load_map(state) 46 | state.map.tiles = map_tiles 47 | state.map.x = 0 48 | state.map.y = 0 49 | end 50 | 51 | def self.map_tiles 52 | MAP_WIDTH.times.map do |tile_x| 53 | MAP_HEIGHT.times.map do |tile_y| 54 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 55 | tile_x == 0 || tile_x == MAP_WIDTH - 1 56 | tile_for tile_x, tile_y, Entities::Wall 57 | else 58 | if (0..8).to_a.sample == 0 59 | tile_for tile_x, tile_y, Entities::Wall 60 | else 61 | tile_for tile_x, tile_y, Entities::Floor 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | def self.tile_for(tile_x, tile_y, tile_type) 69 | tile_type.new( 70 | map_x: tile_x * TILE_WIDTH, 71 | map_y: tile_y * TILE_HEIGHT, 72 | w: TILE_WIDTH, 73 | h: TILE_HEIGHT 74 | ) 75 | end 76 | 77 | def self.min_x 78 | 0 79 | end 80 | 81 | def self.min_y 82 | 0 83 | end 84 | 85 | def self.max_x 86 | MAP_WIDTH * TILE_WIDTH - 1280 87 | end 88 | 89 | def self.max_y 90 | MAP_HEIGHT * TILE_HEIGHT - 720 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /roguelike_oo/07/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (::Controllers::GameController::PLAY_AREA_WIDTH - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (::Controllers::GameController::PLAY_AREA_HEIGHT - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | return true if tile_x < 0 || tile_x > MAP_WIDTH - 1 31 | return true if tile_y < 0 || tile_y > MAP_HEIGHT - 1 32 | 33 | tile = args.state.map.tiles[tile_x][tile_y] 34 | tile.blocking? 35 | end 36 | 37 | def self.map_x_to_tile_x(map_x) 38 | (map_x / TILE_WIDTH).floor 39 | end 40 | 41 | def self.map_y_to_tile_y(map_y) 42 | (map_y / TILE_HEIGHT).floor 43 | end 44 | 45 | def self.load_map(state) 46 | state.map.tiles = map_tiles 47 | state.map.x = 0 48 | state.map.y = 0 49 | end 50 | 51 | def self.map_tiles 52 | MAP_WIDTH.times.map do |tile_x| 53 | MAP_HEIGHT.times.map do |tile_y| 54 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 55 | tile_x == 0 || tile_x == MAP_WIDTH - 1 56 | tile_for tile_x, tile_y, Entities::Wall 57 | else 58 | if (0..8).to_a.sample == 0 59 | tile_for tile_x, tile_y, Entities::Wall 60 | else 61 | tile_for tile_x, tile_y, Entities::Floor 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | def self.tile_for(tile_x, tile_y, tile_type) 69 | tile_type.new( 70 | map_x: tile_x * TILE_WIDTH, 71 | map_y: tile_y * TILE_HEIGHT, 72 | w: TILE_WIDTH, 73 | h: TILE_HEIGHT 74 | ) 75 | end 76 | 77 | def self.min_x 78 | 0 79 | end 80 | 81 | def self.min_y 82 | 0 83 | end 84 | 85 | def self.max_x 86 | MAP_WIDTH * TILE_WIDTH - ::Controllers::GameController::PLAY_AREA_WIDTH 87 | end 88 | 89 | def self.max_y 90 | MAP_HEIGHT * TILE_HEIGHT - ::Controllers::GameController::PLAY_AREA_HEIGHT 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /roguelike_oo/08/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (::Controllers::GameController::PLAY_AREA_WIDTH - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (::Controllers::GameController::PLAY_AREA_HEIGHT - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | tile = tile_at(args, tile_x, tile_y) 31 | return true unless tile 32 | 33 | tile.blocking? 34 | end 35 | 36 | def self.tile_at(args, tile_x, tile_y) 37 | return nil if tile_x < 0 || tile_x > MAP_WIDTH - 1 38 | return nil if tile_y < 0 || tile_y > MAP_HEIGHT - 1 39 | 40 | args.state.map.tiles[tile_x][tile_y] 41 | end 42 | 43 | def self.tile_occupant(args, tile_x, tile_y) 44 | tile = tile_at(args, tile_x, tile_y) 45 | return nil unless tile&.respond_to?(:occupant) 46 | 47 | tile.occupant 48 | end 49 | 50 | def self.map_x_to_tile_x(map_x) 51 | (map_x / TILE_WIDTH).floor 52 | end 53 | 54 | def self.map_y_to_tile_y(map_y) 55 | (map_y / TILE_HEIGHT).floor 56 | end 57 | 58 | def self.load_map(state) 59 | state.map.tiles = map_tiles 60 | state.map.x = 0 61 | state.map.y = 0 62 | end 63 | 64 | def self.map_tiles 65 | MAP_WIDTH.times.map do |tile_x| 66 | MAP_HEIGHT.times.map do |tile_y| 67 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 68 | tile_x == 0 || tile_x == MAP_WIDTH - 1 69 | tile_for tile_x, tile_y, Entities::Wall 70 | else 71 | if (0..8).to_a.sample == 0 72 | tile_for tile_x, tile_y, Entities::Wall 73 | else 74 | tile_for tile_x, tile_y, Entities::Floor 75 | end 76 | end 77 | end 78 | end 79 | end 80 | 81 | def self.tile_for(tile_x, tile_y, tile_type) 82 | tile_type.new( 83 | map_x: tile_x * TILE_WIDTH, 84 | map_y: tile_y * TILE_HEIGHT, 85 | w: TILE_WIDTH, 86 | h: TILE_HEIGHT 87 | ) 88 | end 89 | 90 | def self.min_x 91 | 0 92 | end 93 | 94 | def self.min_y 95 | 0 96 | end 97 | 98 | def self.max_x 99 | MAP_WIDTH * TILE_WIDTH - ::Controllers::GameController::PLAY_AREA_WIDTH 100 | end 101 | 102 | def self.max_y 103 | MAP_HEIGHT * TILE_HEIGHT - ::Controllers::GameController::PLAY_AREA_HEIGHT 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /roguelike_oo/09/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (::Controllers::GameController::PLAY_AREA_WIDTH - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (::Controllers::GameController::PLAY_AREA_HEIGHT - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | tile = tile_at(args, tile_x, tile_y) 31 | return true unless tile 32 | 33 | tile.blocking? 34 | end 35 | 36 | def self.tile_at(args, tile_x, tile_y) 37 | return nil if tile_x < 0 || tile_x > MAP_WIDTH - 1 38 | return nil if tile_y < 0 || tile_y > MAP_HEIGHT - 1 39 | 40 | args.state.map.tiles[tile_x][tile_y] 41 | end 42 | 43 | def self.tile_occupant(args, tile_x, tile_y) 44 | tile = tile_at(args, tile_x, tile_y) 45 | return nil unless tile&.respond_to?(:occupant) 46 | 47 | tile.occupant 48 | end 49 | 50 | def self.map_x_to_tile_x(map_x) 51 | (map_x / TILE_WIDTH).floor 52 | end 53 | 54 | def self.map_y_to_tile_y(map_y) 55 | (map_y / TILE_HEIGHT).floor 56 | end 57 | 58 | def self.load_map(state) 59 | state.map.tiles = map_tiles 60 | state.map.x = 0 61 | state.map.y = 0 62 | end 63 | 64 | def self.map_tiles 65 | MAP_WIDTH.times.map do |tile_x| 66 | MAP_HEIGHT.times.map do |tile_y| 67 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 68 | tile_x == 0 || tile_x == MAP_WIDTH - 1 69 | tile_for tile_x, tile_y, Entities::Wall 70 | else 71 | if (0..8).to_a.sample == 0 72 | tile_for tile_x, tile_y, Entities::Wall 73 | else 74 | tile_for tile_x, tile_y, Entities::Floor 75 | end 76 | end 77 | end 78 | end 79 | end 80 | 81 | def self.tile_for(tile_x, tile_y, tile_type) 82 | tile_type.new( 83 | map_x: tile_x * TILE_WIDTH, 84 | map_y: tile_y * TILE_HEIGHT, 85 | w: TILE_WIDTH, 86 | h: TILE_HEIGHT 87 | ) 88 | end 89 | 90 | def self.min_x 91 | 0 92 | end 93 | 94 | def self.min_y 95 | 0 96 | end 97 | 98 | def self.max_x 99 | MAP_WIDTH * TILE_WIDTH - ::Controllers::GameController::PLAY_AREA_WIDTH 100 | end 101 | 102 | def self.max_y 103 | MAP_HEIGHT * TILE_HEIGHT - ::Controllers::GameController::PLAY_AREA_HEIGHT 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /roguelike_oo/10/ascii/app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | module Controllers 2 | class MapController 3 | MAP_WIDTH = 80 4 | MAP_HEIGHT = 45 5 | TILE_WIDTH = 32 6 | TILE_HEIGHT = 32 7 | MOVEMENT_ZONE_BUFFER_X = 8 * TILE_WIDTH 8 | MOVEMENT_ZONE_BUFFER_Y = 6 * TILE_HEIGHT 9 | 10 | def self.tick(args) 11 | player = args.state.player 12 | map = args.state.map 13 | player_x_offset = player.map_x - map.x 14 | player_y_offset = player.map_y - map.y 15 | if player_x_offset < MOVEMENT_ZONE_BUFFER_X 16 | map.x = [min_x, map.x - TILE_WIDTH].max 17 | elsif player_x_offset > (::Controllers::GameController::PLAY_AREA_WIDTH - MOVEMENT_ZONE_BUFFER_X) 18 | map.x = [map.x + TILE_WIDTH, max_x].min 19 | end 20 | if player_y_offset < MOVEMENT_ZONE_BUFFER_Y 21 | map.y = [min_y, map.y - TILE_HEIGHT].max 22 | elsif player_y_offset > (::Controllers::GameController::PLAY_AREA_HEIGHT - MOVEMENT_ZONE_BUFFER_Y) 23 | map.y = [map.y + TILE_HEIGHT, max_y].min 24 | end 25 | 26 | args.state.map.tiles.flatten.each { |tile| tile.tick(args) } 27 | end 28 | 29 | def self.blocked?(args, tile_x, tile_y) 30 | tile = tile_at(args, tile_x, tile_y) 31 | return true unless tile 32 | 33 | tile.blocking? 34 | end 35 | 36 | def self.tile_at(args, tile_x, tile_y) 37 | return nil if tile_x < 0 || tile_x > MAP_WIDTH - 1 38 | return nil if tile_y < 0 || tile_y > MAP_HEIGHT - 1 39 | 40 | args.state.map.tiles[tile_x][tile_y] 41 | end 42 | 43 | def self.tile_occupant(args, tile_x, tile_y) 44 | tile = tile_at(args, tile_x, tile_y) 45 | return nil unless tile&.respond_to?(:occupant) 46 | 47 | tile.occupant 48 | end 49 | 50 | def self.map_x_to_tile_x(map_x) 51 | (map_x / TILE_WIDTH).floor 52 | end 53 | 54 | def self.map_y_to_tile_y(map_y) 55 | (map_y / TILE_HEIGHT).floor 56 | end 57 | 58 | def self.load_map(state) 59 | state.map.tiles = map_tiles 60 | state.map.x = 0 61 | state.map.y = 0 62 | end 63 | 64 | def self.map_tiles 65 | MAP_WIDTH.times.map do |tile_x| 66 | MAP_HEIGHT.times.map do |tile_y| 67 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || 68 | tile_x == 0 || tile_x == MAP_WIDTH - 1 69 | tile_for tile_x, tile_y, Entities::Wall 70 | else 71 | if (0..8).to_a.sample == 0 72 | tile_for tile_x, tile_y, Entities::Wall 73 | else 74 | tile_for tile_x, tile_y, Entities::Floor 75 | end 76 | end 77 | end 78 | end 79 | end 80 | 81 | def self.tile_for(tile_x, tile_y, tile_type) 82 | tile_type.new( 83 | map_x: tile_x * TILE_WIDTH, 84 | map_y: tile_y * TILE_HEIGHT, 85 | w: TILE_WIDTH, 86 | h: TILE_HEIGHT 87 | ) 88 | end 89 | 90 | def self.min_x 91 | 0 92 | end 93 | 94 | def self.min_y 95 | 0 96 | end 97 | 98 | def self.max_x 99 | MAP_WIDTH * TILE_WIDTH - ::Controllers::GameController::PLAY_AREA_WIDTH 100 | end 101 | 102 | def self.max_y 103 | MAP_HEIGHT * TILE_HEIGHT - ::Controllers::GameController::PLAY_AREA_HEIGHT 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /roguelike_oo/03/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | This is the third part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen. 3 | 4 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 5 | 6 | This tutorial will create our first mobile entity - those that can move around - our Player. 7 | 8 | ## The Player 9 | The simplest place to start is a player entity, but first lets set up our `MobileEntity` base class. Create a new file `/app/entities/mobile_entity.rb`. Make sure to include this file within `main.rb` with: 10 | ```ruby 11 | # /ascii/app/main.rb 12 | require 'app/entities/mobile_entity.rb' 13 | ``` 14 | 15 | This will, for now, be the same as our `Entities::StaticEntity` class, but serves as a place to hang any common behaviours or attributes that mobile entities share. Put this code in the file: 16 | ```ruby 17 | # /ascii/app/entities/mobile_entity.rb 18 | module Entities 19 | class MobileEntity < Base 20 | end 21 | end 22 | ``` 23 | Next create `/app/entities/player.rb`, and fill in with this: 24 | ```ruby 25 | # /ascii/app/entities/player.rb 26 | module Entities 27 | class Player < MobileEntity 28 | def initialize(opts = {}) 29 | super 30 | @path = 'app/sprites/player.png' 31 | end 32 | end 33 | end 34 | ``` 35 | Just like with the `Wall` and `Floor` tiles, for now the only thing special about the player is that we load a player sprite (as ever, found in the example folder). 36 | 37 | Then, of course, include it in `main.rb`: 38 | ```ruby 39 | # /ascii/app/main.rb 40 | require 'app/entities/player.rb' 41 | ``` 42 | 43 | To create a new player, we want a class-level (rather than instance level) spawn method. This is likely to be shared by most mobile entities, so within `app/entities/mobile_entity.rb` add a `spawn` method: 44 | ```ruby 45 | # /ascii/app/entities/mobile_entity.rb 46 | def self.spawn(tile_x, tile_y) 47 | new( 48 | x: tile_x * SPRITE_WIDTH, 49 | y: tile_y * SPRITE_HEIGHT 50 | ) 51 | end 52 | ``` 53 | 54 | Let's check this in action. First, call the player `spawn` method from within the `GameController`'s reset method: 55 | ```ruby 56 | # /ascii/app/controller/game_controller.rb 57 | def self.reset(state) 58 | ::Controllers::MapController.load_map(state) 59 | state.player = ::Entities::Player.spawn(2, 2) 60 | end 61 | ``` 62 | And make sure we are rendering the player sprite; again within the `GameController`, but this time in the `render` method: 63 | ```ruby 64 | # /ascii/app/controller/game_controller.rb 65 | def self.render(state, sprites, labels) 66 | sprites << state.map.tiles 67 | sprites << state.player 68 | end 69 | ``` 70 | Run the game now, and you should see your player sprite showing near the bottom left corner. If you're not seeing anything, make sure you've copied the `player.png` from the examples folder into your `app/sprites` directory. 71 | 72 | ![screenshot showing a map with wall tiles around the perimeter](screenshots/player.png) 73 | 74 | ## Processing Inputs 75 | Next up, we need to make the player move based on keyboard input - either cursor keys or WASD. Add a tick method to the `player.rb`: 76 | ```ruby 77 | # /ascii/app/entities/player.rb 78 | def tick(args) 79 | @y += ::Controllers::MapController::TILE_HEIGHT if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_down.w 80 | @y -= ::Controllers::MapController::TILE_HEIGHT if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_down.s 81 | @x += ::Controllers::MapController::TILE_WIDTH if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_down.d 82 | @x -= ::Controllers::MapController::TILE_WIDTH if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_down.a 83 | end 84 | ``` 85 | This handles 4-directional movement via the up/down/left/right/WASD keys, moving the player by a single tile each time the key is pressed. This code isn't being called anywhere yet, so let's do so now. Within the `GameController`, replace the empty `tick` method with: 86 | ```ruby 87 | # /ascii/app/controllers/game_controller.rb 88 | def self.tick(args) 89 | args.state.player.tick(args) 90 | end 91 | ``` 92 | 93 | Run the game now, and you can move the player around the map with the keyboard. But if you play around you'll see that we can pass through walls and off the screen and so on. In the next tutorial, we'll map static entities either blocking or non-blocking, and we'll make the 'camera' follow the player. 94 | 95 | Continue in [Part 4](../04/tutorial.md) 96 | -------------------------------------------------------------------------------- /roguelike_oo/09/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the ninth part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen. We have both a Player entity and Enemy entities which are capable of movement, tile-based and entity-to-entity collisions, and can attack one another and take damage. The camera follows the player as the traverse the map. We also took a brief aside to look at some Render Targets. 4 | 5 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 6 | 7 | Next up we're going to handle death, move to dice rolls for attack, and add 'factions' so that players, enemies, etc don't accidentally kill people on their own 'team'. 8 | 9 | ## Of Dice and Death 10 | This would be a good title for our little game... Anyway. 11 | 12 | ## Dice 13 | We want to move to attack rolls, that have to overwhelm an enemies 'defense' stat to deal damage. So, first up let's create a base 'dice' class: 14 | ```ruby 15 | # /ascii/app/dice/dice.rb 16 | class Dice 17 | def self.roll(count) 18 | total = 0 19 | count.times do 20 | total += (min_value..max_value).to_a.sample 21 | end 22 | total 23 | end 24 | 25 | def self.min_value 26 | 1 27 | end 28 | 29 | def self.max_value 30 | 6 31 | end 32 | end 33 | ``` 34 | Each type of Die will have a min and max value, and use this 'roll' method to return the total value of 'rolling' itself `count` times. 35 | 36 | Let's add our first 'proper' Die the D20 (20-sided dice): 37 | ```ruby 38 | # /ascii/app/dice/d20.rb 39 | class D20 < Dice 40 | def self.max_value 41 | 20 42 | end 43 | end 44 | ``` 45 | 46 | Include the `Dice` and `D20` in `main.rb`, ensuring you again include it before any of the `Behaviour` includes: 47 | ```ruby 48 | # /ascii/app/main.rb 49 | require 'app/dice/dice.rb' 50 | require 'app/dice/d20.rb' 51 | ``` 52 | 53 | We want to use these dice in the `Attacker` behaviour, so open that up, and let's refactor the `deal_damage` method: 54 | ```ruby 55 | # /ascii/app/behaviour/attacker.rb 56 | def deal_damage(other) 57 | return unless other.respond_to?(:take_damage) 58 | 59 | roll = ::D20.roll(1) 60 | puts "Rolled: #{roll} against #{other.class}'s DEF: #{other.defense}'" 61 | if roll >= other.defense 62 | other.take_damage(attack) 63 | else 64 | puts 'miss!' 65 | end 66 | end 67 | ``` 68 | Instead of just dealing damage to the `other`, we now roll a D20. If the value matches or beats their defense, they take damage, otherwise the attack misses. 69 | 70 | Let's also add 'critical' rolls. Within the `Attacker` behaviour, add another `attr_reader` attribute: 71 | ```ruby 72 | # /ascii/app/behaviour/attacker.rb 73 | attr_reader :attack, :crit_bonus 74 | ``` 75 | 76 | And set that value for both `Player` and `Enemy`: 77 | ```ruby 78 | # /ascii/app/entities/player.rb 79 | def initialize(opts = {}) 80 | # ...etc 81 | @crit_bonus = 1 82 | end 83 | ``` 84 | 85 | ```ruby 86 | # /ascii/app/entities/enemy.rb 87 | def initialize(opts = {}) 88 | # ...etc 89 | @crit_bonus = 1 90 | end 91 | ``` 92 | 93 | And let's make use of this new value, on a 20 roll: 94 | ```ruby 95 | # /ascii/app/behaviour/attacker.rb 96 | def deal_damage(other) 97 | return unless other.respond_to?(:take_damage) 98 | 99 | roll = ::D20.roll(1) 100 | puts "Rolled: #{roll == 20 ? 'CRIT!' : roll} against #{other.class}'s DEF: #{other.defense}" 101 | total_attack = if roll == 20 102 | attack + crit_bonus 103 | else 104 | attack 105 | end 106 | if roll >= other.defense 107 | other.take_damage(total_attack) 108 | else 109 | puts 'miss!' 110 | end 111 | end 112 | ``` 113 | 114 | ## Death on 0 HP (for Enemies) 115 | You'll notice that although you can knock the enemy HP down, they never die. So let's fix that. 116 | 117 | I think we can all agree that enemies should probably only be able to do anything if they're alive, so update the `Enemy`'s `tick` method: 118 | ```ruby 119 | # /ascii/app/entities/enemy.rb 120 | def tick(args) 121 | if alive? 122 | act(args) 123 | @x = map_x - args.state.map.x 124 | @y = map_y - args.state.map.y 125 | else 126 | free_tile_on_death(args) 127 | end 128 | end 129 | ``` 130 | 131 | As you can see here, we want to free up the tile an enemy occupies on it's death. We'll add that under the `Occupant` behaviour: 132 | ```ruby 133 | # /ascii/app/behaviour/occupant.rb 134 | def free_tile_on_death(args) 135 | tile.occupant = nil 136 | end 137 | ``` 138 | 139 | And finally, we want to clear out the list of 'dead' enemies. We can do this in the `EnemyController`'s `tick` method: 140 | ```ruby 141 | # /ascii/app/controllers/enemy_controller.rb 142 | def self.tick(args) 143 | return unless args.state.player.took_action 144 | 145 | args.state.enemies.each { |enemy| enemy.tick(args) } 146 | args.state.enemies = args.state.enemies.select(&:alive?) 147 | end 148 | ``` 149 | After the enemies have acted (so a dead enemy can clear it's occupied tile), we whittle the list of enemies down to just those alive, so the dead will not longer be drawn, or move, or occupy tiles, etc. 150 | 151 | Give the game a run now, and bump into an enemy a few times. Watch the Logs for "CRIT!" and "miss!" messages, and see the enemies disappear when they hit 0 HP! 152 | 153 | 154 | ## Factions 155 | Watching the logging you might spot zombies hitting zombies. We probably don't want that to happen, broadly speaking - though it might be a nice idea to have 'lawless' Entities that will attack anything they see. But for a start, we want to basically create a `player` faction and an `enemy` faction, so they don't hurt each other. 156 | 157 | In `Entities::Base` add a basic faction definiton: 158 | ```ruby 159 | # /ascii/app/entities/base.rb 160 | def faction 161 | 'neutral' 162 | end 163 | ``` 164 | 165 | Override this within `Player`: 166 | ```ruby 167 | # /ascii/app/entities/player.rb 168 | def faction 169 | 'player' 170 | end 171 | ``` 172 | 173 | And override it within `Enemy`: 174 | ```ruby 175 | # /ascii/app/entities/player.rb 176 | def faction 177 | 'enemy' 178 | end 179 | ``` 180 | 181 | Then, to stop factions attacking each other (and we'll make them leave the neutrals alone), we change the `MobileEntity`'s `move_or_attack` to be wary of the factions: 182 | ```ruby 183 | # /ascii/app/behaviour/mobile_entity.rb 184 | if respond_to?(:deal_damage) && other && (other.faction != 'neutral' && other.faction != faction) 185 | deal_damage(other) 186 | yield 187 | end 188 | ``` 189 | 190 | Run the game again, you can still attack the enemies and they can still attack you, but they won't attack each other. 191 | 192 | Continue in [Part 10](../10/tutorial.md) 193 | -------------------------------------------------------------------------------- /roguelike_oo/05/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the fifth part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen, and got our player entity rendered and moving around the screen, making the map/camera follow the player, and enabling tile-based collisions with the map. 4 | 5 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 6 | 7 | Next up we're going to add more entities, in the form of enemies. 8 | 9 | ## A wild Appears! 10 | Create a new file at `/app/entities/enemy.rb`, which will serve as our core `Enemy` class that all enemy types will inherit from. Populate the file with: 11 | ```ruby 12 | # /ascii/app/entities/enemy.rb 13 | module Entities 14 | class Enemy < MobileEntity 15 | end 16 | end 17 | 18 | ``` 19 | 20 | And create our first enemy at `/app/entities/zombie.rb`, and add this code: 21 | ```ruby 22 | # /ascii/app/entities/zombie.rb 23 | module Entities 24 | class Zombie < Enemy 25 | def initialize(opts = {}) 26 | super 27 | @path = 'app/sprites/zombie.png' 28 | end 29 | end 30 | end 31 | ``` 32 | 33 | Remember as always to include those in `main.rb`: 34 | ```ruby 35 | # /ascii/app/main.rb 36 | require 'app/entities/enemy.rb' 37 | require 'app/entities/zombie.rb' 38 | ``` 39 | 40 | Before we go creating a pile of enemies, I think an `EnemyController` would be a good idea. This can be responsible for spawning enemies, looping through them to execute movements, removing dead enemies, etc. So go ahead and create a file at `app/controllers/enemy_controller.rb`, and put this placeholder code in for now: 41 | ```ruby 42 | # /ascii/app/controllers/enemy_controller.rb 43 | module Controllers 44 | class EnemyController 45 | def self.tick(args) 46 | end 47 | end 48 | end 49 | ``` 50 | 51 | And include it in `main.rb`: 52 | ```ruby 53 | # /ascii/app/main.rb 54 | require 'app/controllers/enemy_controller.rb' 55 | ``` 56 | Next we want to make it spawn some enemies. So let's add a `spawn_enemies` and `spawn_enemy` method: 57 | ```ruby 58 | # /ascii/app/controllers/enemy_controller.rb 59 | def self.spawn_enemies(state) 60 | state.enemies ||= [] 61 | 30.times do 62 | tile_x = (::Controllers::MapController::MAP_WIDTH * rand).floor 63 | tile_y = (::Controllers::MapController::MAP_HEIGHT * rand).floor 64 | spawn_enemy( 65 | state, 66 | tile_x, 67 | tile_y, 68 | ::Entities::Zombie 69 | ) 70 | end 71 | end 72 | 73 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 74 | state.enemies << enemy_type.spawn( 75 | tile_x, 76 | tile_y 77 | ) 78 | end 79 | ``` 80 | The `spawn_enemies` makes sure the `state.enemies` array is initialized, then just loops 30 times, each time creating a random `tile_x`/`tile_y` somewhere on the map, and passes those to the `spawn_enemy` method which creates a Zombie, and shifts it into the `state.enemies` array. 81 | 82 | To get the enemies showing up, go over to the `GameController`, and in `reset` add: 83 | ```ruby 84 | # /ascii/app/controllers/game_controller.rb#reset 85 | ::Controllers::EnemyController.spawn_enemies(state) 86 | ``` 87 | And change the contends of `render` to: 88 | ```ruby 89 | # /ascii/app/controllers/game_controller.rb 90 | def self.render(state, sprites, labels) 91 | sprites << state.map.tiles 92 | sprites << state.enemies 93 | sprites << state.player 94 | end 95 | ``` 96 | Run the game and you should see your Zombies showing up on the map (it's random, so try a couple of times if you don't see anything - and make sure you've copied the `zombie.png` from the examples folder into `app/sprites`). 97 | 98 | ![A screenshot showing the map populated with the player and zombies!](./screenshots/zombies.png) 99 | 100 | ## Spawning Safely 101 | At the moment, players, enemies, etc can spawn anywhere, even on top of a wall piece. So we want to check whether a tile is `blocked` before spawning. 102 | 103 | In `MobileEntity`, let's replace the `spawn` method with a `spawn_near` method, and pass the `state` to it. It will _try_ to spawn on the exact spot, but if that fails, we'll gradually increase the radius we're searching in and sample a few spots, until we find a space nearby to spawn. 104 | ```ruby 105 | # /ascii/app/entities/mobile_entity.rb 106 | def self.spawn_near(state, spawn_x, spawn_y) 107 | radius = 1 108 | attempt = 0 109 | tile = state.map.tiles[spawn_x][spawn_y] 110 | while tile.nil? || tile.blocking? 111 | spawn_x = (spawn_x - radius..spawn_x + radius).to_a.sample 112 | spawn_y = (spawn_y - radius..spawn_y + radius).to_a.sample 113 | tile = state.map.tiles[spawn_x][spawn_y] 114 | attempt += 1 115 | next unless attempt >= radius * 8 116 | 117 | radius += 1 118 | attempt = 0 119 | end 120 | new( 121 | map_x: spawn_x * SPRITE_WIDTH, 122 | map_y: spawn_y * SPRITE_HEIGHT 123 | ) 124 | end 125 | ``` 126 | 127 | Update the `GameController` to call `spawn_near` and pass the state: 128 | ```ruby 129 | # /ascii/app/controllers/game_controller.rb#reset 130 | state.player = ::Entities::Player.spawn_near(state, 10, 11) 131 | ``` 132 | 133 | And the EnemyController `spawn_enemy` method: 134 | ```ruby 135 | # /ascii/app/controllers/EnemyController.rb 136 | def self.spawn_enemy(state, tile_x, tile_y, enemy_type) 137 | state.enemies << enemy_type.spawn_near( 138 | state, 139 | tile_x, 140 | tile_y 141 | ) 142 | end 143 | ``` 144 | 145 | ## Shamble? 146 | Zombies are neat, but they're a bit _static_ like this, so lets get them moving. We'll get them to `patrol` shortly, which will just move the zombies around in random directions when the player takes a turn. First, though, we need a way to track if the player has actually moved this `tick`. 147 | 148 | Within `player.rb`, at the top of the class definition, add an `attr_reader` called `took_action`: 149 | ```ruby 150 | # /ascii/app/entities/player.rb 151 | module Entities 152 | class Player < MobileEntity 153 | attr_reader :took_action 154 | # ...etc 155 | ``` 156 | 157 | and in the player's tick, set it to false at the start of the tick, and set it to true in our `attempt_move` block: 158 | ```ruby 159 | # /ascii/app/entities/player.rb 160 | def tick(args) 161 | @took_action = false 162 | # ... etc 163 | 164 | attempt_move(args, target_x, target_y) do 165 | ::Controllers::MapController.tick(args) 166 | @took_action = true 167 | end 168 | end 169 | ``` 170 | 171 | In the `GameController`'s `tick` method, add a conditional call to the enemy controller's tick: 172 | ```ruby 173 | # /ascii/app/controllers/game_controller.rb#tick 174 | ::Controllers::EnemyController.tick(args) 175 | ``` 176 | 177 | And change the `EnemyController`'s tick to this: 178 | ```ruby 179 | # /ascii/app/controllers/enemy_controller.rb 180 | def self.tick(args) 181 | return unless args.state.player.took_action 182 | 183 | enemies = args.state.enemies 184 | enemies.each { |enemy| enemy.patrol(state) } 185 | end 186 | ``` 187 | 188 | And let's add base `patrol` and `tick` methods to the core `Enemy` class: 189 | ```ruby 190 | # /ascii/app/entities/enemy.rb 191 | def tick(args) 192 | patrol(args) 193 | @x = map_x - args.state.map.x 194 | @y = map_y - args.state.map.y 195 | end 196 | 197 | def patrol(args) 198 | end 199 | ``` 200 | 201 | As a basic 'patrolling' behaviour, we want them to choose a random direction to move in, and just shuffle that direction on their turn, using the same `attempt_move` method we used for our player (so they are subject to the same tile-based collisions the human player is): 202 | 203 | ```ruby 204 | # /ascii/app/entities/enemy.rb 205 | def patrol(args) 206 | direction = [:up, :down, :left, :right].sample 207 | case direction 208 | when :up 209 | attempt_move(args, map_x, map_y + ::Controllers::MapController::TILE_HEIGHT) 210 | when :down 211 | attempt_move(args, map_x, map_y - ::Controllers::MapController::TILE_HEIGHT) 212 | when :left 213 | attempt_move(args, map_x - ::Controllers::MapController::TILE_WIDTH, tile_y) 214 | when :right 215 | attempt_move(args, map_x + ::Controllers::MapController::TILE_WIDTH, tile_y) 216 | else 217 | end 218 | end 219 | ``` 220 | 221 | Continue in [Part 6](../06/tutorial.md) 222 | -------------------------------------------------------------------------------- /roguelike_oo/06/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the sixth part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen, and got our player entity rendered and moving around the screen, making the map/camera follow the player, and enabling tile-based collisions with the map. We also added our first enemies, and gave them a basic random movement/patrol behaviour 4 | 5 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 6 | 7 | Next up we're going to add more enemy behaviours (moving towards player when in range), and adding collisions between Entities. 8 | 9 | ## (Slightly) Smarter Enemies 10 | Rather than just shuffling back and forth in random directions, enemies should seek the player when they get close enough. The first step to this is that entities need to know how far away another entity is. Time for some linear algebra. In `Entities::Base`, add this: 11 | ```ruby 12 | # /ascii/app/entities/base.rb 13 | def linear_distance_to(other) 14 | x_diff = other.map_x - map_x 15 | y_diff = other.map_y - map_y 16 | Math.sqrt((x_diff * x_diff) + (y_diff * y_diff)) 17 | end 18 | ``` 19 | 20 | Now, enemies need to leverage this information do decide whether to 'patrol' randomly, or whether to attack the player. Change the `Enemy`'s `tick` method to: 21 | ```ruby 22 | # /ascii/app/entities/enemy.rb 23 | def tick(args) 24 | act(args) 25 | @x = map_x - args.state.map.x 26 | @y = map_y - args.state.map.y 27 | end 28 | ``` 29 | This `act` method is new. Populate it as: 30 | ```ruby 31 | # /ascii/app/entities/enemy.rb 32 | def act(args) 33 | if linear_distance_to(args.state.player) < VISIBLE_RANGE 34 | seek_player(args) 35 | else 36 | patrol(args) 37 | end 38 | end 39 | ``` 40 | We need to add the `VISIBLE_RANGE` constant: 41 | ```ruby 42 | # /ascii/app/entities/enemy.rb 43 | VISIBLE_RANGE = 300 44 | ``` 45 | And both the 'movement' methods risk some code duplication. Extract some of the code from the `patrol` method into a new movement method: 46 | ```ruby 47 | # /ascii/app/entities/enemy.rb 48 | def patrol(args) 49 | direction = [:up, :down, :left, :right].sample 50 | move_towards(args, direction) 51 | end 52 | 53 | def move_towards(args, direction) 54 | target_x = map_x 55 | target_y = map_y 56 | case direction 57 | when :up 58 | target_y += ::Controllers::MapController::TILE_HEIGHT 59 | when :down 60 | target_y -= ::Controllers::MapController::TILE_HEIGHT 61 | when :left 62 | target_x -= ::Controllers::MapController::TILE_WIDTH 63 | when :right 64 | target_x += ::Controllers::MapController::TILE_WIDTH 65 | end 66 | attempt_move(args, target_x, target_y) 67 | end 68 | ``` 69 | Now we have the `move_towards` method, the `seek_player` method becomes quite simple to implement: 70 | ```ruby 71 | # /ascii/app/entities/enemy.rb 72 | def seek_player(args) 73 | directions = [] 74 | player = args.state.player 75 | directions << :left if player.map_x < map_x 76 | directions << :right if player.map_x > map_x 77 | directions << :up if player.map_y > map_y 78 | directions << :down if player.map_y < map_y 79 | direction = directions.sample 80 | move_towards(args, direction) 81 | end 82 | ``` 83 | This method works out which directions of movement would close the gap on the player, then chooses from those directions at random (if there's more than one). 84 | Run the code now, and you'll see the distant zombies shuffling around, but get to close and they will charge you. 85 | 86 | ## Entity-to-Entity collisions 87 | There are loads of ways we could attempt collision between entities, and there are some that are better suited to different types of games - entities could have one or more hit boxes, which we combine with `intersects_rect?` to see if they overlap with any other entities hit boxes, which is great for things like platform games, or checking whether a projectile hits an entity in a bullet-hell game, and so on. 88 | 89 | Because Roguelike's (I'm taking this in the traditional sense of "Being like the game 'Rogue'") tend to be grid-based, we can leverage the fact we already have a grid of tiles that can be blocking or non-blocking, by making a tile blocking, if it has an occupant that is also a 'blocking' entity. 90 | 91 | So to start with, I'm going to treat our `Floor` object, going forward, as the prototypical "Tile that entities can walk on", and extend it to have an `occupant` field. 92 | ```ruby 93 | # /ascii/app/entities/floor.rb 94 | module Entities 95 | class Floor < StaticEntity 96 | attr_accessor :occupant 97 | ``` 98 | > We're using `attr_accessor` here because we want to be able to both 'get' and 'set' this value from an outside entity. We _could_ make this an `attr_reader`, but then we'd need a method like `def occupy(other)` to set the `@occupant` attribute. 99 | 100 | Because `Floor` inherits from the base entity, it already has a `blocking?` method. We don't want to completely override that, but we _do_ want to extend it. Again within `Floor`, add this `blocking?` method: 101 | ```ruby 102 | # /ascii/app/entities/floor.rb 103 | def blocking? 104 | occupant&.blocking? || super 105 | end 106 | ``` 107 | > There's some _ruby_ going on here. The `&.` is generally known as 'safe navigation' and means "try to access a method or attribute on this thing that might not exist", and lets that conditional fail early if there is no occupant on the tile, and revert to the `|| super` to call the parent `blocking?` method. This line is the equivalent of the perhaps more readable: 108 | ```ruby 109 | if occupant.nil? 110 | super 111 | else 112 | occupant.blocking? 113 | end 114 | ``` 115 | 116 | We also want to set moving entities to be blocking, so set this on the `MobileEntity`: 117 | ```ruby 118 | # /ascii/app/entities/mobile_entity.rb 119 | def blocking? 120 | true 121 | end 122 | ``` 123 | 124 | Finally, for this section, it makes sense that our Entities be broadly aware of what tile they're on. So within `Entities::Base` add this: 125 | ```ruby 126 | # /ascii/app/entities/base.rb 127 | def map_tile_x 128 | ::Controllers::MapController.map_x_to_tile_x(map_x) 129 | end 130 | 131 | def map_tile_y 132 | ::Controllers::MapController.map_x_to_tile_x(map_y) 133 | end 134 | ``` 135 | 136 | ## The first (of our) Mixins 137 | There are behaviours that will be shared across classes that might not make sense existing at the base class, or might have to exist in multiple base classes. For example, we want entities to be able to occupy a tile, but the `Floor` tile itself is an `Entity`, so it doesn't make sense at `Entity::Base`. 138 | 139 | So we're going to create a library that can be included into any Class so it can make use of the behaviours. Create a folder at `/app/behaviour`, and create a file in there called `occupant.rb`: 140 | ```ruby 141 | # /ascii/app/behaviour/occupant.rb 142 | module Behaviour 143 | module Occupant 144 | attr_reader :tile 145 | 146 | def update_tile(args) 147 | tile.occupant = nil if tile 148 | @tile = args.state.map.tiles[map_tile_x][map_tile_y] 149 | tile.occupant = self 150 | end 151 | end 152 | end 153 | ``` 154 | All this adds is a readable attribute of `tile` (to any instance of a class that includes it), and adds an `update_tile` method, which clears the `occupant` attribute of the current tile, and sets the `occupant` tile of the new tile. 155 | 156 | Remember to include this file in `main.rb`. Make sure you do so after the `Controllers`, and before any of the `Entities` that will be including this behaviour 157 | ```ruby 158 | # /ascii/app/main.rb 159 | require 'app/behaviour/occupant.rb' 160 | ``` 161 | 162 | Include this 'behaviour' into the `MobileEntity`: 163 | ```ruby 164 | # /ascii/app/entities/mobile_entity.rb 165 | module Entities 166 | class MobileEntity < Base 167 | include ::Behaviour::Occupant 168 | ``` 169 | 170 | Now we need both our Player and our Enemies to update their occupied tile every time they move. Within the `Player`'s `tick` method, update the `attempt_move` block: 171 | ```ruby 172 | # /ascii/app/entities/player.rb 173 | attempt_move(args, target_x, target_y) do 174 | ::Controllers::MapController.tick(args) 175 | @took_action = true 176 | update_tile(args) 177 | end 178 | ``` 179 | And within the `Enemy`'s `move_towards`, add a block to the `attempt_move` call: 180 | ```ruby 181 | # /ascii/app/entities/enemy.rb 182 | attempt_move(args, target_x, target_y) do 183 | update_tile(args) 184 | end 185 | ``` 186 | 187 | Continue in [Part 7](../07/tutorial.md) 188 | -------------------------------------------------------------------------------- /roguelike_oo/08/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the eighth part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen, and got our player entity rendered and moving around the screen, making the map/camera follow the player, and enabling tile-based collisions with the map. We also added our first enemies, and gave them a basic random movement/patrol behaviour and a 'seek player' behaviour, as well as introducing the concept of entities 'occupying' a tile, preventing other entities moving into them, to form our basic entity-to-entity collision. We also took a brief aside to look at some Render Targets. 4 | 5 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 6 | 7 | Next up we're going to build out some of the 'Behaviours' to give our players and enemies Hit Points/Health, enable them to deal and take damage. 8 | 9 | ## Defensive Behaviour 10 | We want to add Hit Points to enemies, players, and perhaps beyond (destructible walls?). While we're at it, we probably want to add a concept of a defense attribute, in the classic D&D style. So, let's create a `Defender` behaviour at `/app/behaviour/defender.rb`, give anything that includes it an `hp` and `defense` attribute, as well as a simple helper `alive?` that returns false once HP hits `0` 11 | ```ruby 12 | # /ascii/app/behaviour/defender.rb 13 | module Behaviour 14 | module Defender 15 | attr_reader :hp, :defense 16 | 17 | def alive? 18 | hp > 0 19 | end 20 | end 21 | end 22 | ``` 23 | 24 | Defenders also need to be able to take damage, so let's add a method to take a given amount of damage, and log the damage via `puts` (this will later go in our logger pane on the right): 25 | ```ruby 26 | # /ascii/app/behaviour/defender.rb 27 | def take_damage(damage) 28 | @hp = [ 29 | 0, 30 | hp - damage 31 | ].max 32 | puts "#{self.class} took #{damage} damage -> #{hp} remaining" 33 | end 34 | ``` 35 | 36 | Include the `defender` behaviour in `main.rb`, ensuring you again include it before any of the `Entity` includes: 37 | ```ruby 38 | # /ascii/app/main.rb 39 | require 'app/behaviour/defender.rb' 40 | ``` 41 | 42 | We want our enemies and our Player to have `defender` behaviour, so they have HP and can take damage. So within `Enemy` include the behaviour, and set these new attributes in a new `initialize` method 43 | ```ruby 44 | # /ascii/app/entities/enemy.rb 45 | module Entities 46 | class Enemy < MobileEntity 47 | include ::Behaviour::Defender 48 | VISIBLE_RANGE = 300 49 | 50 | def initialize(opts = {}) 51 | super 52 | @hp = 10 53 | @defense = 0 54 | end 55 | # ...etc 56 | ``` 57 | 58 | I'm going to give our Zombie slightly better defense: 59 | ```ruby 60 | # /ascii/app/entities/zombie.rb 61 | def initialize(opts = {}) 62 | super 63 | @path = 'app/sprites/zombie.png' 64 | @defense = 4 65 | end 66 | ``` 67 | 68 | And finally our player, where we need to add the 'include', then I'm giving us 50 HP and 10 DEF: 69 | ```ruby 70 | # /ascii/app/entities/player.rb 71 | module Entities 72 | class Player < MobileEntity 73 | include ::Behaviour::Defender 74 | 75 | attr_reader :took_action 76 | 77 | def initialize(opts = {}) 78 | super 79 | @path = 'app/sprites/player.png' 80 | @hp = 50 81 | @defense = 10 82 | end 83 | # ...etc 84 | ``` 85 | ## Attacking behaviour 86 | Next we want to be able to actually deal damage. So let's create an attacker behaviour at `/app/behaviour/attacker.rb`, give anything that includes it an `attack` attribute: 87 | ```ruby 88 | # /ascii/app/behaviour/attacker.rb 89 | module Behaviour 90 | module Attacker 91 | attr_reader :attack 92 | end 93 | end 94 | ``` 95 | As an attacker, we need to be able to deal damage to an `other`, so add a basic `deal_damage` method, which takes another entity as a parameter. 96 | ```ruby 97 | # /ascii/app/behaviour/attacker.rb 98 | def deal_damage(other) 99 | other.take_damage(attack) 100 | end 101 | ``` 102 | 103 | Include the `attacker` behaviour in `main.rb`, ensuring you again include it before any of the `Entity` includes: 104 | ```ruby 105 | # /ascii/app/main.rb 106 | require 'app/behaviour/attacker.rb' 107 | ``` 108 | 109 | Then include the Attacker behaviour in `Player`: 110 | ```ruby 111 | # /ascii/app/entities/player.rb 112 | module Entities 113 | class Player < MobileEntity 114 | include ::Behaviour::Defender 115 | include ::Behaviour::Attacker 116 | ``` 117 | 118 | In initialize, set the new attribute `attack` on the `Player` entity: 119 | ```ruby 120 | # /ascii/app/entities/player.rb 121 | def initialize(opts = {}) 122 | super 123 | @path = 'app/sprites/player.png' 124 | @hp = 50 125 | @defense = 10 126 | @attack = 3 127 | end 128 | ``` 129 | 130 | We also want enemies to be able to attack, so add the behaviour in `Enemy`: 131 | ```ruby 132 | # /ascii/app/entities/enemy.rb 133 | module Entities 134 | class Player < MobileEntity 135 | include ::Behaviour::Defender 136 | include ::Behaviour::Attacker 137 | ``` 138 | 139 | And in initialize, set the new attribute `attack` on the base `Enemy` entity: 140 | ```ruby 141 | # /ascii/app/entities/enemy.rb 142 | def initialize(opts = {}) 143 | super 144 | @hp = 10 145 | @defense = 0 146 | @attack = 1 147 | end 148 | ``` 149 | 150 | ## Can I hurt it? Can it hurt me? 151 | We want a way to know whether an entity can deal and/or take damage - when we try to move and are obstructed, it could be by a wall, or an NPC, or something else that blocks our way. If it's an enemy, though, we want to attack it. So we need to know if it can `take_damage`. 152 | 153 | Ruby provides a neat helper `respond_to?`, which let's you ask if an object has a method on it. Sadly, that's not available in DR GTK right now, but we can implement it ourselves. Within `Entities::Base`, add this: 154 | ```ruby 155 | # /ascii/app/entities/base.rb 156 | def respond_to?(method) 157 | self.class.method_defined?(method.to_sym) 158 | end 159 | ``` 160 | > Hopefully fairly self explanatory, but this lets any instance of a class that inherits from `Entities::Base` ask it's `Class` whether the given method exists. 161 | 162 | To use this, we're going to have to be able to get the occupant of the tile we are trying to move towards, so we can then check if we can hurt whatever is blocking us. We're going to split the `MapController`'s `blocked?` method out so we have a separate `tile_at` method that we can re-use: 163 | ```ruby 164 | # /ascii/app/controllers/map_controller.rb 165 | def self.blocked?(args, tile_x, tile_y) 166 | tile = tile_at(args, tile_x, tile_y) 167 | return true unless tile 168 | 169 | tile.blocking? 170 | end 171 | 172 | def self.tile_at(args, tile_x, tile_y) 173 | return nil if tile_x < 0 || tile_x > MAP_WIDTH - 1 174 | return nil if tile_y < 0 || tile_y > MAP_HEIGHT - 1 175 | 176 | args.state.map.tiles[tile_x][tile_y] 177 | end 178 | ``` 179 | 180 | With that in place, we want to leverage this new `tile_at` method, as well as the `respond_to?` to see if the tile at that location has the `Occupant` behaviour, by testing if it responds to `.occupant` (to see if it's a floor tile, rather than a wall): 181 | 182 | ```ruby 183 | # /ascii/app/controllers/map_controller.rb 184 | def self.tile_occupant(args, tile_x, tile_y) 185 | tile = tile_at(args, tile_x, tile_y) 186 | return nil unless tile&.respond_to?(:occupant) 187 | 188 | tile.occupant 189 | end 190 | ``` 191 | 192 | And we want to make sure that when an attacker attacks something, we can actually attack it: 193 | ```ruby 194 | # /ascii/app/behaviour/attacker.rb 195 | def deal_damage(other) 196 | return unless other.respond_to?(:take_damage) 197 | 198 | other.take_damage(attack) 199 | end 200 | ``` 201 | 202 | ## (Actually) Attack! 203 | 204 | Now we can check if things can be hurt, and we can get the occupant of a blocking tile, we want to hurt them. We're going to extend the `attempt_move` method, and give it a better name as it will be attacking too. So we're going to rename it to `move_or_attack` within the `app/entities/mobile_entity.rb`, and make sure you update the calls to this method in both `player.rb` and `enemy.rb`. 205 | 206 | And now we expand this renamed `move_or_attack` method: 207 | ```ruby 208 | # /ascii/app/entities/mobile_entity.rb 209 | def move_or_attack(args, target_x, target_y) 210 | tile_x = ::Controllers::MapController.map_x_to_tile_x(target_x) 211 | tile_y = ::Controllers::MapController.map_y_to_tile_y(target_y) 212 | if ::Controllers::MapController.blocked?(args, tile_x, tile_y) 213 | other = ::Controllers::MapController.tile_occupant(args, tile_x, tile_y) 214 | if respond_to?(:deal_damage) && other 215 | deal_damage(other) 216 | yield 217 | end 218 | else 219 | @map_x = target_x 220 | @map_y = target_y 221 | yield if block_given? 222 | end 223 | @x = map_x - args.state.map.x 224 | @y = map_y - args.state.map.y 225 | end 226 | ``` 227 | Here we've removed the 'return' if the tile is blocked, and instead branched to an `if blocked {attack} else {move}`, but we're using `respond_to?` to check that the entity moving can actually attack (`respond_to?(:deal_damage)`) before doing so, and the `deal_damage` method makes sure the `other` can `take_damage`. 228 | 229 | Have a play, watch the console output, and you'll see our logs that player and zombies are taking and dealing damage. Nothing is dying yet, so let's get to it. 230 | 231 | Continue in [Part 9](../09/tutorial.md) 232 | -------------------------------------------------------------------------------- /roguelike_oo/01/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | We're going to be building a top-down 'Roguelike' game where movement is on a tile-by-tile, turn-by-turn basis. I'm going to assume you are familiar enough with DragonRuby GTK (GTK from here on out) that you know how to launch a project from the command line. What I'm not going to assume is a deep knowledge of Ruby, so I'll try and explain some core concepts and language features as they are introduced. 3 | 4 | > I'll do any Ruby/GTK explainers in these inline sections, like this. 5 | 6 | Finally, a disclaimer: This is only _a_ way of doing things - it's not even _my_ only way doing things, let alone _the_ only way of doing things. There may be best/better practices elsewhere, but I'm hoping to help you build something quickly, and learn some of the core concepts of both Ruby and GTK. 7 | 8 | ## Getting Started 9 | To start with, we want a new game folder. Create a new folder called `ascii` and within it create the `app` folder. Then create a file called: `main.rb`. We need a `Game` class to instantiate, so add the following skeleton code: 10 | 11 | Within `main.rb` add the following: 12 | ```ruby 13 | # /ascii/app/main.rb 14 | 15 | class Game 16 | def tick(args) 17 | sprites = [] 18 | labels = [] 19 | render(args, sprites, labels) 20 | end 21 | 22 | def render(args, sprites, labels) 23 | args.outputs.sprites << sprites 24 | args.outputs.labels << labels 25 | end 26 | end 27 | 28 | $game ||= Game.new 29 | def tick(args) 30 | $game.tick(args) 31 | end 32 | ``` 33 | 34 | > `Class Game ... end` declares our `Game` class. In this particular case this is a class we will 'instantiate' (create an Object of type `Game`) by calling `.new` in `main.rb`. We then call it's `tick` method from further in `main`, letting us make the `Game` class instance the core for all of our game logic. 35 | 36 | > At the start of tick we create two arrays - sprites and labels for now (there are others primitives like `borders`, `lines`, etc in GTK, but we won't be using them for now), then pass these to the `render` method, which 'shifts' (`<<`) our sprites and labels into the `args.outputs` 37 | 38 | This is a very basic framework for the game to handle all of the other game logic we're going to create. You can run it now, but it won't do anything. We'll get to that when we add the first pieces of our framework. Give it a try from the DragonRuby directory with: 39 | ``` 40 | ./dragonruby ascii 41 | ``` 42 | 43 | ## Controllers 44 | First up, to make sure we're laying a framework we can expand, I like to use a range of separate 'controllers' depending on where we are in the game lifecycle - title screen, in game, post-game, pause menu, etc. So the first thing I want us to add is a title screen/controller. 45 | 46 | Create a new folder at `/app/controllers`. 47 | 48 | Create a new file at `/app/controllers/title_controller.rb`, and fill it with the code below. For _these_ controllers we will be using them as Classes, rather than _instances_ of classes as the intention is to use use the args.state to manage all of the game state. 49 | 50 | > Ruby Classes have 'class methods' and 'instance methods'. Instance methods operate on an object, and are typically used to update its variables, or perform actions specific to the object. Class methods don't operate on the object, but can perhaps take input, process it and produce an output. 51 | 52 | So their tick and render functions will be class methods `self.tick` and `self.render` 53 | ```ruby 54 | # /ascii/app/controllers/title_controller.rb 55 | module Controllers 56 | class TitleController 57 | def self.tick(args) 58 | end 59 | 60 | def self.render(state, sprites, labels) 61 | end 62 | end 63 | end 64 | ``` 65 | 66 | > the `Module` keyword lets you 'namespace' your code. Namespacing in a module like this can be used to, for example, encapsulate a bunch of useful code into a single library - making it easier to re-use code in other projects - or, in this case, just to namespace core 'concepts', such as Controllers, Entities (players, enemies, etc), and other things. 67 | 68 | Now go into `main.rb` and make sure to include this file (some people like to create a `require.rb` file in the root of the project, and require all of the files from there, meaning you just need to `require 'app/require.rb'` in `main.rb`): 69 | ```ruby 70 | # /ascii/app/main.rb 71 | require 'app/controllers/title_controller.rb' 72 | # ... etc 73 | ``` 74 | 75 | > In ruby, we include the code from other files by using the `require` keyword. In GTK, it is necessary that all files other than `main.rb` are "required" at the top of `main.rb`. We're using a relative path here, relative to the 'root' of the Project folder. 76 | 77 | > We're assigning a global variable `$game` using the ruby "double pipe"/"or equals". This basically says "get the value of $game, but if it's not already set, set it to `Game.new`". It's a way of executing the `new` code only once to assign the value, thereafter we'll grab the already assigned value (in this case an instance of "Game"). 78 | 79 | We then need to add a means of tracking the currently active controller. Within `main.rb`, add the following: 80 | 81 | ```ruby 82 | # /ascii/app/main.rb 83 | class Game 84 | attr_reader :active_controller 85 | 86 | def goto_title 87 | @active_controller = ::Controllers::TitleController 88 | end 89 | 90 | # The existing code is here 91 | end 92 | ``` 93 | 94 | > the `attr_reader` keyword sets `active_controller` as a "Getable" attribute on the Controller. This means that any code in the project can ask an instance of `Game` what its `active_controller` is via `game.active_controller`, and it will return the value. Using `attr_reader` makes it only a 'getter' - so other code can ask what the value is, but cannot set it. Only the Game instance itself can set the value via `@active_controller = ...` 95 | 96 | Within the `Game#tick` method, add this as the first line: 97 | ```ruby 98 | # /ascii/app/main.rb#tick 99 | goto_title unless active_controller 100 | ``` 101 | 102 | > `unless` is a nice example of Ruby's dedication to producing readable code. It is basically the opposite of an `if` statement, lets you avoid things like `if !true`, to instead say `do this, unless some condition is true`. In this case we're saying "call to `goto_title` method unless we already have an active controller" 103 | 104 | After the `labels = []` add this, right before the render call: 105 | ```ruby 106 | # /ascii/app/main.rb#tick 107 | active_controller.tick(args) 108 | active_controller.render(args.state, sprites, labels) 109 | ``` 110 | It may not look like much, but the active controller is now being set and its tick/render methods being called. Next up we need to get the active controller to do something with these methods. 111 | 112 | ## The First Output - Labels 113 | Within the `title_controller.rb`, in the render method add the following lines: 114 | ```ruby 115 | # /ascii/app/controllers/title_controller.rb#render 116 | labels << {x: 640, y: 500, text: 'ASCII'} 117 | labels << {x: 640, y: 400, text: 'Press space to start'} 118 | ``` 119 | Give the game a run from the command line, and we'll take a look at the "Hot Reloading" feature of GTK. With the game running, change these values to: 120 | ```ruby 121 | # /ascii/app/controllers/title_controller.rb#render 122 | labels << {x: 620, y: 300, text: 'ASCII'} 123 | labels << {x: 550, y: 100, text: 'Press space to start'} 124 | ``` 125 | >Hot Reloading lets you make changes to your code while your project is running, letting you instantly see the result of those changes. 126 | 127 | They're sitting about central now, and looking a bit like a title screen. We need a neat graphic though. 128 | 129 | ## The Second Output - Sprites 130 | Create a new folder in `/app/sprites` and copy the `dragonruby.png` file from the GTK distribution into that directory. 131 | 132 | Within the `TitleController`'s `render` method, add the following: 133 | ```ruby 134 | # /ascii/app/controllers/title_controller.rb#render 135 | sprites << {x: 576, y: 500, w: 128, h: 101, path: 'dragonruby.png'} 136 | ``` 137 | 138 | Run the game again, and you should see a nice title screen with text and a sprite. 139 | 140 | ![A screenshot showing the game menu with a 'press space to start' prompt](screenshots/menu_screen.png) 141 | 142 | ## Input 143 | We want to actually launch the game when space is pressed, but to do that we need a couple of things: 1) we need a 'GameController', and 2) we need to handle some keyboard input. Let's start with the controller. 144 | 145 | Create a new file at `./app/controllers/game_controller.rb`, and fill in this skeleton code, similar to when we created the TitleController, except we hav a `reset` method in here this time to allow the game to reset anything it needs to within the `$args.state`: 146 | ```ruby 147 | # /ascii/app/controllers/game_controller.rb 148 | module Controllers 149 | class GameController 150 | def self.tick(args) 151 | end 152 | 153 | def self.render(state, sprites, labels) 154 | end 155 | 156 | def self.reset(state) 157 | end 158 | end 159 | end 160 | ``` 161 | And again go into `main.rb` and make sure to include this file: 162 | 163 | ```ruby 164 | # /ascii/app/main.rb 165 | require 'app/controllers/title_controller.rb' 166 | require 'app/controllers/game_controller.rb' 167 | # ... etc 168 | ``` 169 | 170 | Within the `Game` class, add a new method, `goto_game`: 171 | ```ruby 172 | # /ascii/app/main.rb 173 | def goto_game(args) 174 | ::Controllers::GameController.reset(args.state) 175 | @active_controller = ::Controllers::GameController 176 | end 177 | ``` 178 | 179 | The `reset` method wont do anything just yet, but soon it will be padded out to make sure that the game data is in a 'known good' state before the game portion runs. 180 | 181 | The `GameController` is ready to take action now, so we need to switch to it. This is done by watching for an input within the `TitleController`'s `tick` method. So, change it to: 182 | ```ruby 183 | # /ascii/app/controllers/title_controller.rb 184 | def self.tick(args) 185 | $game.goto_game(args) if args.inputs.keyboard.space 186 | end 187 | ``` 188 | This is a good stopping point - we have some label and sprite rendering, and the game is transitioning between controllers based on a state. 189 | 190 | Example code is available in the `ascii` folder. 191 | 192 | Continue in [Part 2](../02/tutorial.md) 193 | -------------------------------------------------------------------------------- /roguelike_oo/10/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the tenth part of a series of tutorials building a top-down 'Roguelike' game. In the previous installments we created a basic framework for our classes, controllers, entities, etc, and got some 'Static Entities' in the form of map tiles drawn on the screen. We have both a Player entity and Enemey entities which are capable of movement, tile-based and entity-to-entity collisions, and can attack one another and take damage. The camera follows the player as the traverse the map. We also took a brief aside to look at some Render Targets. 4 | 5 | I recommend you familiarise yourself with the previous parts, and we'll be using the 'final code' from the previous tutorial as our starting point here. 6 | 7 | Next up we're going to start using our right-hand panel for displaying events, stats etc. 8 | 9 | ## Player Stats 10 | First up, let's extend the `Defender` behaviour to set a `max_hp` attribute, as we'll want to show HP remaining, and let us do some neat colouring. 11 | 12 | ```ruby 13 | # /ascii/app/behaviour/defender.rb 14 | attr_reader :max_hp, :hp, :defense 15 | ``` 16 | 17 | Then in `Player` we need to set the `max_hp` within the `initialize` method: 18 | ```ruby 19 | # /ascii/app/entities/player.rb 20 | def initialize(opts = {}) 21 | # ...etc 22 | @max_hp = 50 23 | @hp = max_hp 24 | # ...etc 25 | end 26 | ``` 27 | 28 | And likewise in `Enemy`: 29 | ```ruby 30 | # /ascii/app/entities/enemy.rb 31 | def initialize(opts = {}) 32 | # ...etc 33 | @max_hp = 10 34 | @hp = max_hp 35 | # ...etc 36 | end 37 | ``` 38 | 39 | For now we just want the HP stats, but we may want to expand this further, so add a `stats_labels` method to the `Player`: 40 | ```ruby 41 | # /ascii/app/entities/player.rb 42 | def stats_labels 43 | [ 44 | {x: 16, y: 700, text: hp_string, r: 255, g: 255, b: 255, a: 255} 45 | ] 46 | end 47 | ``` 48 | > The attributes here are `[x, y, string, red, green, blue, alpha]` 49 | 50 | Next we need to populate the HP string. I'm choosing to 'pad' the HP to two digits, to produce an HP like `HP: 07/50` 51 | 52 | ```ruby 53 | # /ascii/app/entities/player.rb 54 | def hp_string 55 | hp_label = (hp < 10) ? "0#{hp}" : hp.to_s 56 | max_hp_label = (max_hp < 10) ? "0#{max_hp}" : max_hp.to_s 57 | "HP: #{hp_label} / #{max_hp_label}" 58 | end 59 | ``` 60 | 61 | Render the logs to the panel within the `GameController`'s `render_text_area` method: 62 | ```ruby 63 | # /ascii/app/controllers/game_controller.rb 64 | def self.render_text_area(args) 65 | args.state.redraw_text_area = false 66 | args.render_target(:text_area).solids << [0, 0, TEXT_AREA_WIDTH, TEXT_AREA_HEIGHT, 10, 21, 33] 67 | args.render_target(:text_area).labels << args.state.player.stats_labels 68 | end 69 | ``` 70 | 71 | Finally, for a nice touch, let's color-code the HP string. Create an `hp_string` method, which will return Green if HP is above 50%, orange between 20% and 50%, and red under 20%. 72 | ```ruby 73 | # /ascii/app/entities/player.rb 74 | def hp_string_color 75 | if hp / max_hp >= 0.5 76 | {r: 10, g: 200, b: 10, a: 255} 77 | elsif hp / max_hp >= 0.2 78 | {r: 255, g: 165, b: 0, a: 255} 79 | else 80 | {r: 220, g: 0, b: 0, a: 255} 81 | end 82 | end 83 | ``` 84 | 85 | Then Change the `stats_labels` method to: 86 | ```ruby 87 | # /ascii/app/entities/player.rb 88 | def stats_labels 89 | [ 90 | {x: 16, y: 700, text: hp_string}.merge(hp_string_color) 91 | ] 92 | end 93 | ``` 94 | 95 | 96 | Run the game, get a little beaten up. Wait, why is the label just saying "50/50" despite the logs saying the Player is taking damage? It's because we're being economical and using render targets, and the text area is only redrawn when we set `args.state.redraw_text_area = true`. So let's set that to true whenever the player takes damage. 97 | 98 | First, let's track whether the player took damage, in the same way we track whether they 'took_action', by adding a `took_damage` attribute to the `Player` class: 99 | ```ruby 100 | # /ascii/app/entities/player.rb 101 | attr_reader :took_action, :took_damage 102 | ``` 103 | 104 | Make sure we set `took_damage` to `false` at the start of each tick: 105 | ```ruby 106 | # /ascii/app/entities/player.rb 107 | def tick(args) 108 | @took_action = false 109 | @took_damange = false 110 | # ...etc 111 | end 112 | ``` 113 | 114 | Then set `took_damage` to `true` whenever the player takes damage by extending the `take_damage` method inherited from `Defender`: 115 | ```ruby 116 | # /ascii/app/entities/player.rb 117 | def take_damage(damage) 118 | super 119 | @took_damage = true 120 | end 121 | ``` 122 | 123 | Finally, set `args.state.redraw_text_area` to `true` whenever the player takes damage by checking this attribute in the GameController: 124 | ```ruby 125 | # /ascii/app/controllers/game_controller.rb 126 | def self.tick(args) 127 | args.state.player.tick(args) 128 | if args.state.player.took_damage 129 | args.state.redraw_text_area = true 130 | end 131 | ::Controllers::EnemyController.tick(args) 132 | end 133 | ``` 134 | 135 | (we could also do that with a one-liner as `args.state.redraw_text_area ||= args.state.player.took_damage`, but I'll leave the clearer version in place for now). 136 | 137 | Now give the game a run, get beaten up, and see the HP change text _and_ colour. If it's not dramatic enough for you, you can always adjust the damage dealt by the enemies to move the HP bar more quickly, change the ratios at which the color changes happen, or lower the player's max HP. 138 | 139 | ## Events 140 | We want to fill the panel with a history of the last X events. This will require storing the events on the `state`, drawing them to the panel, and perhaps occasionally pruning them. We'll start with an `EventLogsController` which will be responsible for initializing the state, for logging new events, and for producing a rendering of the X most recent events. 141 | 142 | Create a new file at `app/controllers/event_logs_controller.rb`: 143 | ```ruby 144 | # /ascii/app/controllers/event_logs_controller.rb 145 | module Controllers 146 | class EventLogsController 147 | def self.render(args, sprites, labels) 148 | end 149 | 150 | def self.reset(state) 151 | state.event_logs = [] 152 | state.logged_event_this_tick = false 153 | end 154 | end 155 | end 156 | ``` 157 | 158 | Include the `EventLogsController` in `main.rb`, along with the other `Controller` includes: 159 | ```ruby 160 | # /ascii/app/main.rb 161 | require 'app/controllers/event_logs_controller.rb' 162 | ``` 163 | 164 | Call the `reset` method from within the `GameController`'s `reset` method: 165 | ```ruby 166 | # /ascii/app/controllers/game_controller.rb 167 | def self.reset(state) 168 | ::Controllers::EventLogsController.reset(state) 169 | # ...etc 170 | end 171 | ``` 172 | 173 | Add a `log_event` method to the `EventLogsController`, which just puts any new log into position 0 in the array (so the array is naturally a list of newest to oldest), and sets our flag to say there was an event logged this tick: 174 | 175 | ```ruby 176 | # /ascii/app/controllers/event_logs_controller.rb 177 | def self.log_event(event) 178 | $gtk.args.state.event_logs.unshift(event) 179 | $gtk.args.state.logged_event_this_tick = true 180 | end 181 | ``` 182 | 183 | We want a way of turning all event logs into labels, so they can be displayed in our panel on render. So add an `events_as_labels` method to the `EventLogsController`: 184 | ```ruby 185 | # /ascii/app/controllers/event_logs_controller.rb 186 | LOG_TOP = 650 187 | 188 | def self.events_as_labels(args) 189 | args.state.event_logs.map.with_index do |event, index| 190 | alpha = 255 - (index * 15) 191 | {x: 16, y: LOG_TOP - (index * 40), text: event, r: 230, g: 230, b: 230, a: alpha} 192 | end 193 | end 194 | ``` 195 | 196 | The changing alpha just makes the most recent events appear a white, and the older events appear as dark grey. 197 | 198 | We don't have any logs yet, but before we do let's get the last mechanism in place: calling the `EventLogsController`'s `events_as_labels` method in `GameController#render_text_area` to pull through all those lovely labels: 199 | ```ruby 200 | # /ascii/app/controllers/game_controller.rb 201 | def self.render_text_area(args) 202 | args.state.redraw_text_area = false 203 | args.render_target(:text_area).solids << {x: 0, y: 0, w: TEXT_AREA_WIDTH, h: TEXT_AREA_HEIGHT, r: 10, g: 21, b: 33} 204 | args.render_target(:text_area).labels << args.state.player.stats_labels 205 | args.render_target(:text_area).labels << ::Controllers::EventLogsController.events_as_labels(args) 206 | end 207 | ``` 208 | 209 | While we're in here, here we need to clear the `logged_event_this_tick` flag at the start of the tick: 210 | ```ruby 211 | # /ascii/app/controllers/game_controller.rb 212 | def self.tick(args) 213 | args.state.logged_event_this_tick = false 214 | # ...etc 215 | end 216 | ``` 217 | 218 | ## Attack Events 219 | 220 | For our `puts` that we've been using so far, we've been using the class name, but this isn't going to look great in the logs when we show `Entities::Zombie` or whatever. So let's add a `name` Class Method to the `Base`, `Enemy`, `Zombie` and `Player` classes: 221 | ```ruby 222 | # /ascii/app/entities/base.rb 223 | def self.name 224 | '' 225 | end 226 | ``` 227 | 228 | ```ruby 229 | # /ascii/app/entities/enemy.rb 230 | def self.name 231 | 'Enemy' 232 | end 233 | ``` 234 | 235 | ```ruby 236 | # /ascii/app/entities/zombie.rb 237 | def self.name 238 | 'Zombie' 239 | end 240 | ``` 241 | 242 | ```ruby 243 | # /ascii/app/entities/Player.rb 244 | def self.name 245 | 'Player' 246 | end 247 | ``` 248 | 249 | To create Event Logs for out attacks, head over to the `Attacker` behaviour, and add the following two calls to `log_event` from the `deal_damage` method. 250 | ```ruby 251 | # /ascii/app/behaviour/attacker.rb 252 | if roll >= other.defense 253 | other.take_damage(total_attack) 254 | ::Controllers::EventLogsController.log_event( 255 | "#{roll == 20 ? 'CRIT! ' : ''}#{name} hit #{other.name} for #{total_attack} damage" 256 | ) 257 | else 258 | ::Controllers::EventLogsController.log_event( 259 | "#{name} missed #{other.name}!" 260 | ) 261 | end 262 | ``` 263 | 264 | Remove the `puts` from the `Attacker` behaviour, and the one from the `Defender` behaviour. 265 | 266 | ![A screenshot showing the player's HP color-coded, along with a log of events from most recent to oldest, fading from white to black based on their recency](./screenshots/logs_and_hp_color.png) 267 | -------------------------------------------------------------------------------- /roguelike_oo/02/tutorial.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | This is the second part of a series of tutorials building a top-down 'Roguelike' game. In the first part where the basic project structure was established, this tutorial continues from there. I recommend you familiarise yourself with the first part, and we'll be using the 'final code' from the previous tutorial as our starting point here. 3 | 4 | This tutorial will start with the first entities, starting with a core entity that can be built on for other entities. 5 | 6 | ## Enter the Entities 7 | So, create a new folder `/app/entities` and create a file called `base.rb`. Within that file, add the following: 8 | ```ruby 9 | # /ascii/app/entities/base.rb 10 | module Entities 11 | class Base 12 | attr_sprite 13 | end 14 | end 15 | ``` 16 | >Once again, namespacing our entities, to make a clear distinction between these types of objects and our Controllers. 17 | 18 | > We're using the `attr_sprite` mixin which lets us treat our entities as sprites, without having to do much transforming, or manually producing a hash/array. It automatically gives you x, y, w (width), h (height), path (sprite image path) attributes on your model 19 | 20 | There will be plenty of entity types, but I want to start with two core distinctions: static entities (walls, floor, etc) and mobile entities (players, enemies, projectiles). Using these base classes for future inheritance lets us group behaviours that are common to their inheritors. So let's start with a static entity, and two types of static entities - walls and floors. 21 | 22 | Create a file at `/app/entities/static_entity.rb`, and fill it with the following code. 23 | 24 | ```ruby 25 | # /ascii/app/entities/static_entity.rb 26 | module Entities 27 | class StaticEntity < Base 28 | end 29 | end 30 | 31 | ``` 32 | > `class StaticEntity < Base` inherits any behaviours or attributes from the `Base` entity (like that handy `attr_sprite` mixin), but also lets us override or extend them, which we'll get to later. 33 | 34 | Next, we should include these in `main.rb`. Just after the block of controller includes, add the entities: 35 | ```ruby 36 | # /ascii/app/main.rb 37 | require 'app/entities/base.rb' 38 | require 'app/entities/static_entity.rb' 39 | ``` 40 | 41 | As we will be drawing these entities as sprites, we want to set `SPRITE_WIDTH` and `SPRITE_HEIGHT` constants for all entities. In this project every sprite will take up a full tile height/width, so it makes sense to define them at the `Base` level, but in other projects you might have different sized sprites, and local defines might make more sense. Add them near the top of the class, like this: 42 | ```ruby 43 | # /ascii/app/entities/base.rb 44 | module Entities 45 | class Base 46 | attr_sprite 47 | 48 | SPRITE_WIDTH = 32 49 | SPRITE_HEIGHT = 32 50 | 51 | # the rest of the code follows here 52 | ``` 53 | 54 | We're going to be using all these entities to create the map, but of course we want to be able to create everything at specific locations, and set certain attributes. We'll create a core `initialize` method in the Entities::Base object, and we can override/extend this in other classes. So, open `entities/base.rb` and add an `initialize` method: 55 | 56 | ```ruby 57 | # /ascii/app/entities/base.rb 58 | def initialize(opts = {}) 59 | @x = opts[:x] || 0 60 | @y = opts[:y] || 0 61 | @w = opts[:w] || SPRITE_WIDTH 62 | @h = opts[:h] || SPRITE_HEIGHT 63 | @path = opts[:path] || 'app/sprites/null_sprite.png' 64 | end 65 | ``` 66 | 67 | Note the `app/sprites/null_sprite.png` - you'll find this in the example project folder - it's just an invisible sprite. 68 | 69 | We can now create base entities with `Entities::Base.new(...)`, but we don't want to do that - we want to use specific entities. Let's expand our static entities with a `Floor` and a `Wall` type. 70 | 71 | Create `/app/entities/wall.rb` with the following code in it. 72 | ```ruby 73 | # /ascii/app/entities/wall.rb 74 | module Entities 75 | class Wall < StaticEntity 76 | end 77 | end 78 | 79 | ``` 80 | 81 | And similarly for `/app/entities/static/floor.rb`: 82 | ```ruby 83 | # /ascii/app/entities/floor.rb 84 | module Entities 85 | class Floor < StaticEntity 86 | end 87 | end 88 | 89 | ``` 90 | 91 | Make sure to include these in `main.rb`: 92 | ```ruby 93 | # /ascii/app/main.rb 94 | require 'app/entities/wall.rb' 95 | require 'app/entities/floor.rb' 96 | ``` 97 | 98 | The main way these differ from their 'parent' (`Entities::Base`) - for now at least - is that they show a different sprite. In `floor.rb` add the following `initialize` method: 99 | ```ruby 100 | # /ascii/app/entities/floor.rb 101 | def initialize(opts = {}) 102 | super 103 | @path = 'app/sprites/floor.png' 104 | end 105 | ``` 106 | And similar for the `wall.rb`: 107 | ```ruby 108 | # /ascii/app/entities/wall.rb 109 | def initialize(opts = {}) 110 | super 111 | @path = 'app/sprites/wall.png' 112 | end 113 | ``` 114 | > When inheriting from another class - as we do here with `class Wall < Base` and `class Floor < Base`, if we don't define an `initialize` in `Floor` or `Wall`, they will call the method in `Base`. We can completely override the parent `initialize` method by declaring one locally wihin `Wall` or `Floor`, but we actually want to use _most_ of the `Base#initialize` method (to set x, y, etc) - we only want to change the `@path`. These two classes, as shown above, leverage the `Entities::Base#initialize` method by first calling `super`, which calls the parent's initialize method, then overrides the sprite `@path` to set their own image. 115 | 116 | Note: The `floor.png` and `wall.png` can be found in the example code, again. 117 | 118 | ## Creating a Map 119 | Next up, to draw our map, we want a Map Controller. This will be used to populate the `state` with tiles, and to do some helper methods for navigating the map. Add a file at `app/controllers/map_controller.rb`, and make sure to add `require` line in `main.rb` again: 120 | ```ruby 121 | # /ascii/app/main.rb 122 | require 'app/controllers/map_controller.rb' 123 | ``` 124 | 125 | Fill the MapController with this: 126 | ```ruby 127 | # /ascii/app/controllers/map_controller.rb 128 | module Controllers 129 | class MapController 130 | MAP_WIDTH = 80 131 | MAP_HEIGHT = 45 132 | TILE_WIDTH = 32 133 | TILE_HEIGHT = 32 134 | 135 | def self.load_map(state) 136 | end 137 | end 138 | end 139 | ``` 140 | The `MAP_WIDTH` and `MAP_HEIGHT` are the map size in _tiles_. The `TILE_WIDTH` and `TILE_HEIGHT` are the size of _each tile_ in _pixels_. 141 | 142 | The `MAP_WIDTH` and `MAP_HEIGHT` for this example just equate to 2 screens wide and 2 screens high, as we'll ultimately make this map scroll about as the player moves. 143 | 144 | Add a method in the MapController: 145 | ```ruby 146 | # /ascii/app/controllers/map_controller.rb 147 | def self.map_tiles 148 | MAP_WIDTH.times.map do |tile_x| 149 | MAP_HEIGHT.times.map do |tile_y| 150 | end 151 | end 152 | end 153 | ``` 154 | > The ruby `times` keyword is another nice example of the ruby's language/grammar. You can basically say "Do this thing `8.times`". Here we're doing both `MAP_WIDTH` and `MAP_HEIGHT` times, so the outer loop runs `80.times` and the inner loop runs `45.times`. 155 | 156 | > We're coupling that with ruby's `.map` method, which gathers the return value of the loop into an Array. Because we have a nested loop, we're creating a two-dimentional array. 157 | 158 | This nested loop loops through the number of horizonal tiles, and then the number of vertical tiles. By doing a couple of `.map` here we can basically produce a grid of tiles, something like (if this helps). To understand how this translates to on-screen, rotate it 90 degees counter-clockwise, and imagine each number as a tile on the map. 159 | ```ruby 160 | [ 161 | [0,0,0,0,0,0,0,0], 162 | [0,1,1,1,1,1,1,0], 163 | [0,1,1,1,1,1,1,0], 164 | [0,1,1,1,1,1,1,0], 165 | [0,1,1,1,1,1,1,0], 166 | [0,1,1,1,1,1,1,0], 167 | [0,1,1,1,1,1,1,0], 168 | [0,0,0,0,0,0,0,0] 169 | ] 170 | ``` 171 | Where a `0` is a wall and a `1` is the floor. To put a wall all around this floor area, we want any edge tile (where `x = 0`, or `x = MAP_WIDTH -1`, or where `y = 0` or `y = MAP_HEIGHT - 1`). Extend the `map_tiles` method as follows: 172 | ```ruby 173 | # /ascii/app/controllers/map_controller.rb 174 | def self.map_tiles 175 | MAP_WIDTH.times.map do |tile_x| 176 | MAP_HEIGHT.times.map do |tile_y| 177 | if tile_y == 0 || tile_y == MAP_HEIGHT - 1 || tile_x == 0 || tile_x == MAP_WIDTH - 1 178 | # This is the left, right, top or bottom edge, so make a wall 179 | tile_for(tile_x, tile_y, ::Entities::Wall) 180 | else 181 | # it's not the edge, so make it floor 182 | tile_for(tile_x, tile_y, ::Entities::Floor) 183 | end 184 | end 185 | end 186 | end 187 | ``` 188 | 189 | and add a `tile_for` method to the `MapController`. This takes a tile x/y co-ordinate, and a tile tile (wall or floor), and returns an instance of the appropriate Static Entity. 190 | ```ruby 191 | # /ascii/app/controllers/map_controller.rb 192 | def self.tile_for(tile_x, tile_y, tile_type) 193 | tile_type.new( 194 | x: tile_x * TILE_WIDTH, 195 | y: tile_y * TILE_HEIGHT, 196 | w: TILE_WIDTH, 197 | h: TILE_HEIGHT 198 | ) 199 | end 200 | ``` 201 | With that, the last thing to do to get our tiles showing is to 1) initialize the map, and 2) render the sprites. 202 | 203 | Change the `MapController`'s' `load_map` method to pull in the map tiles to the state: 204 | ```ruby 205 | # /ascii/app/controllers/map_controller.rb 206 | def self.load_map(state) 207 | state.map.tiles = map_tiles 208 | end 209 | ``` 210 | and call `load_map` from the `GameController`'s `reset` method: 211 | ```ruby 212 | # /ascii/app/controllers/game_controller.rb 213 | def self.reset(state) 214 | ::Controllers::MapController.load_map(state) 215 | end 216 | ``` 217 | 218 | Finally, render the tile sprites within the `GameController`: 219 | ```ruby 220 | # /ascii/app/controllers/game_controller.rb 221 | def self.render(state, sprites, labels) 222 | sprites << state.map.tiles 223 | end 224 | ``` 225 | It might not be terribly exciting to look at, but we have a collection of wall tiles round the left and bottom edge, and floor tiles everywhere else. If you're not seeing anything here, make sure you've copied the `floor.png` and `wall.png` from the example folder into your `app/sprites` directory. 226 | 227 | ![screenshot showing a map with wall tiles around the perimeter](screenshots/map.png) 228 | 229 | ## Helping Out the Debugger 230 | When something goes wrong - and it will at some point, either in following this tutorial, or in your own code - DR GTK will attempt to dump the state as a string. In order to do this it expects objects to have three methods to assist with the serialization. These are: 231 | ```ruby 232 | def serialize 233 | { } 234 | end 235 | 236 | def inspect 237 | # Override the inspect method and return ~serialize.to_s~. 238 | serialize.to_s 239 | end 240 | 241 | def to_s 242 | # Override to_s and return ~serialize.to_s~. 243 | serialize.to_s 244 | end 245 | ``` 246 | 247 | We actually want something useful in our serialization, though, so let's pad out the serialize method. Add those methods within `Entities::Base`. We want to know the basic sprite attributes: x, y, w (width), h (height), path: 248 | ```ruby 249 | # /ascii/app/entities/base.rb 250 | def serialize 251 | { 252 | x: x, 253 | y: y, 254 | w: w, 255 | h: h, 256 | path: path 257 | } 258 | end 259 | 260 | def inspect 261 | # Override the inspect method and return ~serialize.to_s~. 262 | serialize.to_s 263 | end 264 | 265 | def to_s 266 | # Override to_s and return ~serialize.to_s~. 267 | serialize.to_s 268 | end 269 | ``` 270 | 271 | That wraps it up for Part 2 - in Part 3 we'll start adding a player entity. 272 | 273 | Continue in [Part 3](../03/tutorial.md) 274 | --------------------------------------------------------------------------------