Class: Doom::Game::World

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(map, sprites:, sound: nil, random: Random.new, skill_hidden: {}, mode: :coop, frag_limit: nil) ⇒ World

Returns a new instance of World.

Raises:

  • (ArgumentError)


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

#combatObject (readonly)

Returns the value of attribute combat.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def combat
  @combat
end

#damage_multiplierObject

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_limitObject (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_pickupObject (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

#leveltimeObject (readonly)

Returns the value of attribute leveltime.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def leveltime
  @leveltime
end

#mapObject (readonly)

Returns the value of attribute map.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def map
  @map
end

#modeObject (readonly)

Returns the value of attribute mode.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def mode
  @mode
end

#monster_aiObject (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_idObject (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

#playersObject (readonly)

Returns the value of attribute players.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def players
  @players
end

#randomObject (readonly)

Returns the value of attribute random.



27
28
29
# File 'lib/doom/game/world.rb', line 27

def random
  @random
end

#sector_actionsObject (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_effectsObject (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_hiddenObject

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.

Raises:

  • (ArgumentError)


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_triggeredObject



174
175
176
# File 'lib/doom/game/world.rb', line 174

def exit_triggered
  @sector_actions.exit_triggered
end

#frag_leaderObject

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

#fragsObject

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_thingsObject



214
215
216
# File 'lib/doom/game/world.rb', line 214

def hidden_things
  @skill_hidden.merge(@item_pickup.picked_up)
end

#match_winnerObject

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_hashObject

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_sectionsObject

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