Class: Doom::Game::World
- Inherits:
-
Object
- Object
- Doom::Game::World
- Defined in:
- lib/doom/game/world.rb
Overview
The simulation, with no dependency on Gosu or any window.
Everything that decides what happens in the game lives here and advances only through run_tic. The platform layer is reduced to two jobs: turn input into ticcmds, and draw the result. That split is what makes lockstep multiplayer possible -- the netcode drives run_tic directly, with no display attached, and two peers feeding equal ticcmds reach equal worlds.
Sound is emitted but never read back, so it cannot influence state.
Constant Summary collapse
- TIC_SECONDS =
1.0 / 35.0
- SECTOR_DAMAGE_INTERVAL =
Tics between nukage/lava ticks
32- USE_DISTANCE =
Max distance to use a linedef
64.0- SECTOR_DAMAGE =
Sector specials that hurt the player, damage per interval.
{ 4 => 20, 5 => 10, 7 => 5, 16 => 20, 11 => 20 }.freeze
- MODES =
i[coop deathmatch].freeze
Instance Attribute Summary collapse
-
#combat ⇒ Object
readonly
Returns the value of attribute combat.
-
#damage_multiplier ⇒ Object
Returns the value of attribute damage_multiplier.
-
#frag_limit ⇒ Object
readonly
Frags needed to win, or nil for an open-ended match.
-
#item_pickup ⇒ Object
readonly
Returns the value of attribute item_pickup.
-
#leveltime ⇒ Object
readonly
Returns the value of attribute leveltime.
-
#map ⇒ Object
readonly
Returns the value of attribute map.
-
#mode ⇒ Object
readonly
Returns the value of attribute mode.
-
#monster_ai ⇒ Object
readonly
Returns the value of attribute monster_ai.
-
#physics_by_id ⇒ Object
readonly
Returns the value of attribute physics_by_id.
-
#players ⇒ Object
readonly
Returns the value of attribute players.
-
#random ⇒ Object
readonly
Returns the value of attribute random.
-
#sector_actions ⇒ Object
readonly
Returns the value of attribute sector_actions.
-
#sector_effects ⇒ Object
readonly
Returns the value of attribute sector_effects.
-
#skill_hidden ⇒ Object
Returns the value of attribute skill_hidden.
Instance Method Summary collapse
-
#add_player(start_thing = nil, id: @players.size) ⇒ Object
Spawn a player.
- #deathmatch? ⇒ Boolean
- #exit_triggered ⇒ Object
-
#frag_leader ⇒ Object
Who is winning.
- #frag_limit_reached? ⇒ Boolean
-
#frags ⇒ Object
Deathmatch score, player id => frags.
- #hidden_things ⇒ Object
-
#initialize(map, sprites:, sound: nil, random: Random.new, skill_hidden: {}, mode: :coop, frag_limit: nil) ⇒ World
constructor
A new instance of World.
-
#match_winner ⇒ Object
The player who won, or nil while the match is still on.
- #physics_for(player) ⇒ Object
- #player(id = 0) ⇒ Object
-
#remove_player(id) ⇒ Object
Remove a player mid-match: someone left, or the server timed them out.
-
#respawn(player = primary) ⇒ Object
Bring one player back to life at its spawn point, leaving the rest of the world alone.
-
#restart_level(player = primary) ⇒ Object
Single-player death: wipe monster/item/projectile state and start the level over.
-
#run_tic(cmds = {}) ⇒ Object
Advance the whole simulation by one tic.
-
#spawn_point_for(id) ⇒ Object
Where player
identers the map. -
#state_hash ⇒ Object
Fingerprint of the whole simulation, for lockstep desync detection.
-
#state_hash_sections ⇒ Object
Same fingerprint broken down by section, so a desync can say which part of the world drifted rather than only that something did.
Constructor Details
#initialize(map, sprites:, sound: nil, random: Random.new, skill_hidden: {}, mode: :coop, frag_limit: nil) ⇒ World
Returns a new instance of World.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/doom/game/world.rb', line 33 def initialize(map, sprites:, sound: nil, random: Random.new, skill_hidden: {}, mode: :coop, frag_limit: nil) raise ArgumentError, "unknown mode #{mode.inspect}" unless MODES.include?(mode) @map = map @sprites = sprites @sound = sound @random = random @mode = mode @frag_limit = frag_limit @skill_hidden = skill_hidden @damage_multiplier = 1.0 @leveltime = 0 @players = [] @physics_by_id = {} @sector_actions = SectorActions.new(map, sound) @sector_effects = SectorEffects.new(map, random: random) @item_pickup = ItemPickup.new(map, skill_hidden) @combat = Combat.new(map, sprites, skill_hidden, sound, random: random) @monster_ai = MonsterAI.new(map, @combat, sprites, skill_hidden, sound, random: random) end |
Instance Attribute Details
#combat ⇒ Object (readonly)
Returns the value of attribute combat.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def combat @combat end |
#damage_multiplier ⇒ Object
Returns the value of attribute damage_multiplier.
31 32 33 |
# File 'lib/doom/game/world.rb', line 31 def damage_multiplier @damage_multiplier end |
#frag_limit ⇒ Object (readonly)
Frags needed to win, or nil for an open-ended match.
30 31 32 |
# File 'lib/doom/game/world.rb', line 30 def frag_limit @frag_limit end |
#item_pickup ⇒ Object (readonly)
Returns the value of attribute item_pickup.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def item_pickup @item_pickup end |
#leveltime ⇒ Object (readonly)
Returns the value of attribute leveltime.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def leveltime @leveltime end |
#map ⇒ Object (readonly)
Returns the value of attribute map.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def map @map end |
#mode ⇒ Object (readonly)
Returns the value of attribute mode.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def mode @mode end |
#monster_ai ⇒ Object (readonly)
Returns the value of attribute monster_ai.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def monster_ai @monster_ai end |
#physics_by_id ⇒ Object (readonly)
Returns the value of attribute physics_by_id.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def physics_by_id @physics_by_id end |
#players ⇒ Object (readonly)
Returns the value of attribute players.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def players @players end |
#random ⇒ Object (readonly)
Returns the value of attribute random.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def random @random end |
#sector_actions ⇒ Object (readonly)
Returns the value of attribute sector_actions.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def sector_actions @sector_actions end |
#sector_effects ⇒ Object (readonly)
Returns the value of attribute sector_effects.
27 28 29 |
# File 'lib/doom/game/world.rb', line 27 def sector_effects @sector_effects end |
#skill_hidden ⇒ Object
Returns the value of attribute skill_hidden.
31 32 33 |
# File 'lib/doom/game/world.rb', line 31 def skill_hidden @skill_hidden end |
Instance Method Details
#add_player(start_thing = nil, id: @players.size) ⇒ Object
Spawn a player. With no explicit start, the world picks one for this player id: its own co-op start in co-op, a random one in deathmatch.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/doom/game/world.rb', line 63 def add_player(start_thing = nil, id: @players.size) start_thing ||= spawn_point_for(id) raise ArgumentError, 'map has no player start' unless start_thing player = Player.new(id: id) player.place(start_thing.x, start_thing.y, PlayerState::VIEWHEIGHT, start_thing.angle) physics = PlayerPhysics.new(@map, player.state) physics.skill_hidden = @skill_hidden physics.item_pickup = @item_pickup physics.combat = @combat physics.settle(player, player.x, player.y) @players << player @physics_by_id[player.id] = physics @combat.players = @players player end |
#deathmatch? ⇒ Boolean
57 58 59 |
# File 'lib/doom/game/world.rb', line 57 def deathmatch? @mode == :deathmatch end |
#exit_triggered ⇒ Object
174 175 176 |
# File 'lib/doom/game/world.rb', line 174 def exit_triggered @sector_actions.exit_triggered end |
#frag_leader ⇒ Object
Who is winning. Ties break on the lowest player id rather than on
152 153 154 |
# File 'lib/doom/game/world.rb', line 152 def frag_leader @players.min_by { |p| [-p.frags, p.id] } end |
#frag_limit_reached? ⇒ Boolean
156 157 158 159 160 |
# File 'lib/doom/game/world.rb', line 156 def frag_limit_reached? return false unless @frag_limit @players.any? { |p| p.frags >= @frag_limit } end |
#frags ⇒ Object
Deathmatch score, player id => frags.
146 147 148 |
# File 'lib/doom/game/world.rb', line 146 def frags @players.to_h { |p| [p.id, p.frags] } end |
#hidden_things ⇒ Object
214 215 216 |
# File 'lib/doom/game/world.rb', line 214 def hidden_things @skill_hidden.merge(@item_pickup.picked_up) end |
#match_winner ⇒ Object
The player who won, or nil while the match is still on.
Note what this deliberately does not do: end run_tic. The frag limit is local configuration and, unlike the mode and the seed, it does not travel in the handshake, so a peer started with a different --frags would stop simulating at a different tic and desync every other peer. The world answers the question; the presentation layer says so on screen.
170 171 172 |
# File 'lib/doom/game/world.rb', line 170 def match_winner frag_limit_reached? ? frag_leader : nil end |
#physics_for(player) ⇒ Object
141 142 143 |
# File 'lib/doom/game/world.rb', line 141 def physics_for(player) @physics_by_id[player.id] end |
#player(id = 0) ⇒ Object
82 83 84 |
# File 'lib/doom/game/world.rb', line 82 def player(id = 0) @players.find { |p| p.id == id } end |
#remove_player(id) ⇒ Object
Remove a player mid-match: someone left, or the server timed them out. Pure bookkeeping, no RNG, so it stays deterministic across peers as long as they all remove the same id on the same tic. Monsters targeting the departed player re-pick next tic via MonsterAI's own dead/absent check.
90 91 92 93 94 95 96 97 98 99 |
# File 'lib/doom/game/world.rb', line 90 def remove_player(id) player = @players.find { |p| p.id == id } return unless player @players.delete(player) @physics_by_id.delete(id) @combat.players = @players @monster_ai.monsters.each { |m| m.target = nil if m.target&.id == id } player end |
#respawn(player = primary) ⇒ Object
Bring one player back to life at its spawn point, leaving the rest of the world alone. This is what multiplayer death does: the others are still playing, so monsters and items must not reset under them.
115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/doom/game/world.rb', line 115 def respawn(player = primary) return unless player player.state.reset start = spawn_point_for(player.id) return unless start physics = physics_for(player) physics.reset player.place(start.x, start.y, PlayerState::VIEWHEIGHT, start.angle) physics.settle(player, player.x, player.y) end |
#restart_level(player = primary) ⇒ Object
Single-player death: wipe monster/item/projectile state and start the level over. Monsters are rebuilt rather than revived because their HP, death and pain state live in Combat keyed by thing index.
131 132 133 134 135 136 137 138 139 |
# File 'lib/doom/game/world.rb', line 131 def restart_level(player = primary) return unless player rebuild_actor_subsystems physics = physics_for(player) physics.item_pickup = @item_pickup physics.combat = @combat respawn(player) end |
#run_tic(cmds = {}) ⇒ Object
Advance the whole simulation by one tic.
cmds maps player id => Ticcmd; a player with no command coasts on a
neutral one rather than repeating its last input, which would make a
dropped packet look like a stuck key.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/doom/game/world.rb', line 194 def run_tic(cmds = {}) @leveltime += 1 @players.each(&:snapshot!) @sector_effects.update @players.each do |p| run_player_tic(p, cmds[p.id] || Ticcmd.none) end @combat.update @monster_ai.update(@players) @players.each { |p| post_tic_player(p) } update_sector_actions update_item_pickups @leveltime end |
#spawn_point_for(id) ⇒ Object
Where player id enters the map. Deathmatch draws from the map's DM
spawns through the shared RNG, so every peer picks the same one.
103 104 105 106 107 108 109 110 |
# File 'lib/doom/game/world.rb', line 103 def spawn_point_for(id) if @mode == :deathmatch starts = @map.deathmatch_starts return starts[@random.rand(starts.size)] unless starts.empty? end @map.player_start_for(id) end |
#state_hash ⇒ Object
Fingerprint of the whole simulation, for lockstep desync detection.
179 180 181 |
# File 'lib/doom/game/world.rb', line 179 def state_hash StateHash.of(self) end |
#state_hash_sections ⇒ Object
Same fingerprint broken down by section, so a desync can say which part of the world drifted rather than only that something did.
185 186 187 |
# File 'lib/doom/game/world.rb', line 185 def state_hash_sections StateHash.sections(self) end |