Class: Doom::Platform::GosuWindow
- Inherits:
-
Gosu::Window
- Object
- Gosu::Window
- Doom::Platform::GosuWindow
- Defined in:
- lib/doom/benchmark.rb,
lib/doom/platform/gosu_window.rb
Constant Summary collapse
- SCALE =
3- TURN_SPEED =
Movement now lives in Game::PlayerPhysics and runs per tic, so the old continuous-time thrust/friction constants moved there with it.
3.0- DEFAULT_REFRESH_HZ =
Frame generation is decoupled from presentation. Gosu's main loop is limited by the buffer swap, which blocks on vblank -- so drawing every iteration pins the whole engine to the monitor's refresh rate. Instead we render every iteration (in update) but only ask Gosu to present when a refresh interval has elapsed; needs_redraw? => false skips both the blit and the swap, letting the loop spin freely in between.
Gosu 1.4 exposes no refresh-rate API, so we ask SDL (see SDLDisplayMode) and fall back to 60 Hz if it will not say.
60.0- PRESENT_INTERVAL_SLACK =
Aim slightly early so we don't miss a vblank
0.85- MOUSE_SENSITIVITY =
Mouse look sensitivity
0.15- CLIENT_INPUT_LEAD =
Networked: input is sampled on the local clock but tics only run once every player's command for them has arrived. A stall is normal -- it means someone else's packet is late -- so we keep drawing and say who we are waiting for rather than freezing silently. Authoritative-server client: no local simulation at all. Poll the server's frame stream (Net::Client applies it), send this frame's input tagged a few tics ahead so it lands before that tic is finalized, and follow the world the client holds -- which it rebuilds from a fresh snapshot on a resync, so re-point at it when the object changes.
3- NET_SEND_INTERVAL =
Outbound packets are paced to the tic rate, not the uncapped render loop: drawing runs at hundreds of FPS, and sending one packet per frame would flood the server (and, in P2P, every peer) from every phone in the room.
1.0 / 35.0
- CLIENT_INPUT_REDUNDANCY =
Each input packet repeats the last few tics of commands, so pacing the send rate down does not make a single dropped packet lose that input.
4- MAP_MARGIN =
--- Automap ---
20
Instance Attribute Summary collapse
-
#refresh_hz ⇒ Object
readonly
Returns the value of attribute refresh_hz.
Instance Method Summary collapse
-
#active_palette_index ⇒ Object
Palette for the live game view: red pain flash while taking damage (1-8), yellow flash on item pickup (9), otherwise the normal palette.
- #advance_local ⇒ Object
- #advance_networked ⇒ Object
- #advance_server_client ⇒ Object
- #apply_debug_pose ⇒ Object
- #apply_difficulty(skill) ⇒ Object
- #apply_rubykaigi_mode ⇒ Object
-
#bind_world(world) ⇒ Object
Point this window at a world, refreshing every cached reference.
-
#blit_hud_overlay(overlay, pal_idx, tint) ⇒ Object
Blit an indexed overlay (transparent = -1) over the GL world, coloured by palette
pal_idx;tintmultiplies the whole image (nil = untinted). - #build_sector_colors ⇒ Object
-
#build_ticcmd ⇒ Object
Collect this tic's intent.
- #button_down(id) ⇒ Object
-
#capture_debug_snapshot ⇒ Object
--- Debug Snapshot ---.
- #compute_skill_hidden(skill) ⇒ Object
- #draw ⇒ Object
- #draw_automap ⇒ Object
- #draw_debug_overlay ⇒ Object
-
#draw_hardware_hud(menu: false) ⇒ Object
Hardware world rendering bypasses the indexed framebuffer.
-
#draw_match_status ⇒ Object
Deathmatch scoreboard.
-
#draw_net_status ⇒ Object
A lockstep stall looks exactly like a freeze unless we say otherwise, and a desync means everything on screen is already wrong -- both are worth interrupting the player for.
-
#handle_input(_delta_time) ⇒ Object
Per-frame input sampling.
- #handle_mouse_look ⇒ Object
- #handle_option_toggle(option, value) ⇒ Object
-
#handle_weapon_switch ⇒ Object
Weapon selection from the number keys.
-
#hold_pain_palette ⇒ Object
Death keeps damage_count pinned at max so the draw loop selects the red pain palette every frame while dead.
- #hsv_to_gosu(h, s, v) ⇒ Object
- #hud_tint_byte(value) ⇒ Object
-
#initialize(renderer, palette, world, status_bar = nil, weapon_renderer = nil, animations = nil, menu = nil, sound_engine = nil, session: nil, client: nil) ⇒ GosuWindow
constructor
The simulation now lives in Game::World; this class is input and output only.
- #load_next_map(map_name) ⇒ Object
-
#map_bounds ⇒ Object
Lazily computed and cached on first access; cleared by load_next_map.
- #needs_cursor? ⇒ Boolean
-
#needs_redraw? ⇒ Boolean
Gosu calls this before draw; false skips both the blit and the buffer swap, so the main loop keeps generating frames without waiting on the display.
-
#net_send_due? ⇒ Boolean
True at most once per tic interval, so outbound packets track the 35 Hz simulation rather than the uncapped frame rate.
- #new_hud_overlay ⇒ Object
-
#present(framebuffer, pal_idx = 0) ⇒ Object
Blit one palette-indexed framebuffer to the window.
- #respawn_player ⇒ Object
-
#send_client_input ⇒ Object
Sample this frame's input for a near-future tic and send it, paced to the tic rate, with a short redundancy window against packet loss.
- #setup_yjit_toggle ⇒ Object
- #switch_renderer ⇒ Object
- #sync_renderer_visual_options ⇒ Object
-
#track_frame_rates ⇒ Object
Frames generated vs frames actually shown.
- #trigger_level_exit(exit_type) ⇒ Object
- #update ⇒ Object
-
#weapon_light_tint ⇒ Object
The world light reaching the player, as a Gosu colour to multiply the weapon by.
Constructor Details
#initialize(renderer, palette, world, status_bar = nil, weapon_renderer = nil, animations = nil, menu = nil, sound_engine = nil, session: nil, client: nil) ⇒ GosuWindow
The simulation now lives in Game::World; this class is input and output only. The world's subsystems are cached in ivars because the automap, debug overlay and HUD read them constantly -- bind_world keeps those caches honest whenever the world is rebuilt.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/doom/platform/gosu_window.rb', line 33 def initialize(renderer, palette, world, = nil, weapon_renderer = nil, animations = nil, = nil, sound_engine = nil, session: nil, client: nil) fullscreen = ARGV.include?('--fullscreen') || ARGV.include?('-f') super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, fullscreen) self.caption = 'Doom Ruby' self.update_interval = 0 # Uncap framerate (default 16.67ms = 60 FPS cap) SDLKeyboardGrab.setup @renderer = renderer @palette = palette @status_bar = @weapon_renderer = weapon_renderer @animations = animations @menu = @doom_font = &.font @sound = sound_engine @skill = Game::Menu::SKILL_MEDIUM @session = session @client = client # authoritative-server client, if we joined one @local_player_id = session&.local_id || client&.local_id || 0 @last_tic_at = Time.now # for interpolating between server frames @last_net_send_at = nil # throttles outbound packets to the tic rate @client_input_window = [] # recent [tic, ticcmd] resent against loss .netgame = true if && (session || client) bind_world(world) @pending_turn = 0.0 # Mouse turn accumulated between tics @tic_accumulator = 0.0 @screen_image = nil @mouse_captured = false @last_mouse_x = nil @last_update_time = Time.now @use_pressed = false @show_debug = false @show_map = false @screen_melt = nil @intermission = nil @current_map = 'E1M1' @debug_font = Gosu::Font.new(24) @fps_frames = 0 @fps_time = Time.now @fps_display = 0.0 # Uncapped frame generation. Presented frames are counted separately so # the overlay can show both the render rate and what the display got. @uncapped_fps = !ARGV.include?('--vsync') .[:uncapped_fps] = @uncapped_fps if @present_frames = 0 @present_fps_display = 0.0 @last_present_ms = 0 @refresh_hz = (SDLDisplayMode.refresh_rate || DEFAULT_REFRESH_HZ).to_f @present_interval_ms = 1000.0 / @refresh_hz # Precompute sector colors for automap @sector_colors = build_sector_colors # Pre-build palette RGBA lookups for all 14 palettes (0=normal, 1-8=pain red) @all_palette_rgba = [] wad = renderer.wad 14.times do |pal_idx| pal = Wad::Palette.load(wad, pal_idx) @all_palette_rgba << pal.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') } end @palette_rgba = @all_palette_rgba[0] end |
Instance Attribute Details
#refresh_hz ⇒ Object (readonly)
Returns the value of attribute refresh_hz.
416 417 418 |
# File 'lib/doom/platform/gosu_window.rb', line 416 def refresh_hz @refresh_hz end |
Instance Method Details
#active_palette_index ⇒ Object
Palette for the live game view: red pain flash while taking damage (1-8), yellow flash on item pickup (9), otherwise the normal palette.
579 580 581 582 583 584 585 586 587 |
# File 'lib/doom/platform/gosu_window.rb', line 579 def active_palette_index if @item_pickup && @item_pickup.pickup_flash > 0 9 elsif @player_state @player_state.damage_count.clamp(0, 8) else 0 end end |
#advance_local ⇒ Object
99 100 101 102 103 104 105 |
# File 'lib/doom/platform/gosu_window.rb', line 99 def advance_local while @tic_accumulator >= 1.0 @tic_accumulator -= 1.0 @leveltime = @world.run_tic(@player.id => build_ticcmd) @item_pickup.update_flash end end |
#advance_networked ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/doom/platform/gosu_window.rb', line 141 def advance_networked @session.poll while @tic_accumulator >= 1.0 @tic_accumulator -= 1.0 @session.submit(build_ticcmd) end # Lockstep already resends a redundancy window from each peer's ack, so # pacing transmit to the tic rate loses nothing but the flood. @session.transmit if net_send_due? ran = @session.run(@world, limit: 10) ran.times { @item_pickup.update_flash } @leveltime = @world.leveltime end |
#advance_server_client ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/doom/platform/gosu_window.rb', line 125 def advance_server_client @client.poll bind_world(@client.world) unless @world.equal?(@client.world) send_client_input if @client.synced_tic != @leveltime @leveltime = @client.synced_tic @last_tic_at = Time.now @item_pickup.update_flash end # Fraction into the current tic, so rendering stays smooth above the # server's 35 Hz frame rate. @tic_accumulator = [(Time.now - @last_tic_at) * 35.0, 1.0].min end |
#apply_debug_pose ⇒ Object
207 208 209 210 211 212 213 214 |
# File 'lib/doom/platform/gosu_window.rb', line 207 def apply_debug_pose x, y, angle = ENV.fetch('DOOM_DEBUG_POSE').split(',').map { |value| Float(value) } sector = @map.sector_at(x, y) @player.place(x, y, (sector&.floor_height || 0) + Game::PlayerState::VIEWHEIGHT, angle) @debug_pose_applied = true rescue ArgumentError warn 'DOOM_DEBUG_POSE must be x,y,angle_degrees' end |
#apply_difficulty(skill) ⇒ Object
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 |
# File 'lib/doom/platform/gosu_window.rb', line 941 def apply_difficulty(skill) @skill = skill @damage_multiplier = case skill when Game::Menu::SKILL_BABY then 0.5 when Game::Menu::SKILL_EASY then 0.75 when Game::Menu::SKILL_MEDIUM then 1.0 when Game::Menu::SKILL_HARD then 1.0 when Game::Menu::SKILL_NIGHTMARE then 1.5 else 1.0 end # Push difficulty into the world before respawning: respawn rebuilds the # actor subsystems from the world's settings, so setting them here only # would be discarded. @world.skill_hidden = compute_skill_hidden(skill) @world.damage_multiplier = @damage_multiplier @physics.skill_hidden = @world.skill_hidden # Baby mode: start with some armor @player_state.armor = 50 if skill == Game::Menu::SKILL_BABY respawn_player @monster_ai.aggression = true @monster_ai.damage_multiplier = @damage_multiplier # Baby: double ammo from pickups (matching DOOM skill 1) @item_pickup.ammo_multiplier = skill == Game::Menu::SKILL_BABY ? 2 : 1 end |
#apply_rubykaigi_mode ⇒ Object
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 |
# File 'lib/doom/platform/gosu_window.rb', line 862 def apply_rubykaigi_mode return unless @menu&.&.[](:rubykaigi_mode) # God mode: invincible for stress-free demos @player_state.god_mode = true @player_state.health = 100 # All weapons + full ammo handle_option_toggle(:all_weapons, true) @player_state.infinite_ammo = true # Monsters don't attack (peaceful exploration) @monster_ai.aggression = false if @monster_ai # Force debug overlay on (shows FPS + YJIT status) @show_debug = true end |
#bind_world(world) ⇒ Object
Point this window at a world, refreshing every cached reference. Called on construction and whenever the world is rebuilt (new map, respawn).
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/doom/platform/gosu_window.rb', line 186 def bind_world(world) @world = world @map = world.map @random = world.random @player = world.player(@local_player_id) || world.add_player(id: @local_player_id) apply_debug_pose if ENV['DOOM_DEBUG_POSE'] && !@debug_pose_applied @player_state = @player.state # The HUD holds the player state directly; re-point it, or a map change # or a netgame resync (which builds a fresh world) leaves it on a stale # player showing frozen health and ammo. @status_bar.player = @player_state if @status_bar @weapon_renderer.player = @player_state if @weapon_renderer @physics = world.physics_for(@player) @combat = world.combat @monster_ai = world.monster_ai @item_pickup = world.item_pickup @sector_actions = world.sector_actions @damage_multiplier = world.damage_multiplier @leveltime = world.leveltime end |
#blit_hud_overlay(overlay, pal_idx, tint) ⇒ Object
Blit an indexed overlay (transparent = -1) over the GL world, coloured by
palette pal_idx; tint multiplies the whole image (nil = untinted).
544 545 546 547 548 549 550 551 552 553 |
# File 'lib/doom/platform/gosu_window.rb', line 544 def (, pal_idx, tint) palette = @all_palette_rgba[pal_idx] rgba = .map { |index| index == -1 ? "\0\0\0\0" : palette[index] }.join image = Gosu::Image.from_blob(Render::SCREEN_WIDTH, Render::SCREEN_HEIGHT, rgba) if tint image.draw(0, 0, 10, SCALE, SCALE, tint) else image.draw(0, 0, 10, SCALE, SCALE) end end |
#build_sector_colors ⇒ Object
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 |
# File 'lib/doom/platform/gosu_window.rb', line 1068 def build_sector_colors # Generate distinct colors for each sector using golden ratio hue spacing num_sectors = @map.sectors.size colors = Array.new(num_sectors) phi = (1 + Math.sqrt(5)) / 2.0 num_sectors.times do |i| hue = (i * phi * 360) % 360 colors[i] = hsv_to_gosu(hue, 0.6, 0.85) end colors end |
#build_ticcmd ⇒ Object
Collect this tic's intent. Everything that moves the player must come through here, so that replacing it with a ticcmd off the network is the only change multiplayer needs.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/doom/platform/gosu_window.rb', line 320 def build_ticcmd return Game::Ticcmd.none if @menu&.active? return Game::Ticcmd.none if @player_state&.dead forward = 0.0 side = 0.0 forward += 1.0 if Gosu.(Gosu::KB_UP) || Gosu.(Gosu::KB_W) forward -= 1.0 if Gosu.(Gosu::KB_DOWN) || Gosu.(Gosu::KB_S) side += 1.0 if Gosu.(Gosu::KB_D) side -= 1.0 if Gosu.(Gosu::KB_A) turn = @pending_turn @pending_turn = 0.0 turn += TURN_SPEED if Gosu.(Gosu::KB_LEFT) turn -= TURN_SPEED if Gosu.(Gosu::KB_RIGHT) = 0 if (@mouse_captured && Gosu.(Gosu::MS_LEFT)) || Gosu.(Gosu::KB_LEFT_CONTROL) || Gosu.(Gosu::KB_RIGHT_CONTROL) || Gosu.(Gosu::KB_X) || Gosu.(Gosu::KB_LEFT_SHIFT) || Gosu.(Gosu::KB_RIGHT_SHIFT) |= Game::Ticcmd::BTN_FIRE end |= Game::Ticcmd::BTN_USE if Gosu.(Gosu::KB_SPACE) || Gosu.(Gosu::KB_E) Game::Ticcmd.new(forward, side, turn, ) end |
#button_down(id) ⇒ Object
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 |
# File 'lib/doom/platform/gosu_window.rb', line 666 def (id) # Intermission handles input if @intermission @intermission.handle_key if @intermission.finished next_map = @intermission.next_map @intermission = nil if next_map load_next_map(next_map) else # Episode complete - return to menu @menu&.show end end return end # Menu handles input when active if @menu&.active? key = case id when Gosu::KB_UP then :up when Gosu::KB_DOWN then :down when Gosu::KB_RETURN, Gosu::KB_SPACE then :enter when Gosu::KB_ESCAPE then :escape end if key # Play menu navigation sounds case key when :up, :down @sound&. when :escape @sound&. end # Capture old screen before menu state change (for melt effect) old_state = @menu.state result = @menu.handle_key(key) new_state = @menu.state # Trigger melt when transitioning from title to main menu if old_state == Game::Menu::STATE_TITLE && new_state == Game::Menu::STATE_MAIN && @last_menu_fb if @renderer.respond_to?(:hardware?) && @renderer.hardware? # The OpenGL scene has no palette-indexed snapshot for # ScreenMelt. Draw it live on the next frame instead of # melting toward HardwareRenderer's intentionally black # compatibility framebuffer. @screen_melt = nil else # Build the new screen (main menu with game background) @renderer.render_frame @weapon_renderer&.render(@renderer.framebuffer) unless @player_state&.dead @status_bar&.render(@renderer.framebuffer) new_fb = @renderer.framebuffer.dup @menu.render(new_fb, nil) @screen_melt = Render::ScreenMelt.new(@last_menu_fb, new_fb) end end # Play confirmation sound on select @sound&. if key == :enter case result when :start_game # Restarting the level is local, so it would desync a netgame -- # for a lockstep peer or an authoritative-server client alike. apply_difficulty(@menu.selected_skill) unless @session || @client when :resume @mouse_captured = true SDLKeyboardGrab.grab! when :quit close when Hash handle_option_toggle(result[:option], result[:value]) if result[:action] == :toggle_option end end return end case id when Gosu::KB_ESCAPE SDLKeyboardGrab.release! @mouse_captured = false self.mouse_x = width / 2 self.mouse_y = height / 2 @menu&.show when Gosu::MS_LEFT, Gosu::KB_TAB unless @mouse_captured @mouse_captured = true @last_mouse_x = mouse_x SDLKeyboardGrab.grab! end when Gosu::KB_Z @show_debug = !@show_debug when Gosu::KB_R switch_renderer when Gosu::KB_B @renderer.skip_background_fill = !@renderer.skip_background_fill when Gosu::KB_Y if defined?(RubyVM::YJIT) setup_yjit_toggle if RubyVM::YJIT.enabled? RubyVM::YJIT.disable else RubyVM::YJIT.enable end end when Gosu::KB_C @monster_ai.aggression = !@monster_ai.aggression if @monster_ai when Gosu::KB_M @show_map = !@show_map when Gosu::KB_F12 capture_debug_snapshot end end |
#capture_debug_snapshot ⇒ Object
--- Debug Snapshot ---
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 |
# File 'lib/doom/platform/gosu_window.rb', line 1006 def capture_debug_snapshot dir = File.join(File.('../..', __dir__), '..', 'screenshots') FileUtils.mkdir_p(dir) ts = Time.now.strftime('%Y%m%d_%H%M%S_%L') prefix = File.join(dir, ts) # Save framebuffer as PNG require 'chunky_png' unless defined?(ChunkyPNG) w = Render::SCREEN_WIDTH h = Render::SCREEN_HEIGHT img = ChunkyPNG::Image.new(w, h) fb = @renderer.framebuffer colors = @palette.colors h.times do |y| row = y * w w.times do |x| r, g, b = colors[fb[row + x]] img[x, y] = ChunkyPNG::Color.rgb(r, g, b) end end img.save("#{prefix}.png") # Save player state and sector info sector = @map.sector_at(@player.x, @player.y) sector_idx = sector ? @map.sectors.index(sector) : nil angle_deg = Math.atan2(@player.sin_angle, @player.cos_angle) * 180.0 / Math::PI # Sprite diagnostics sprites_info = @renderer.sprite_diagnostics nearby = sprites_info.select { |s| s[:dist] && s[:dist] < 1500 } .sort_by { |s| s[:dist] } sprite_lines = nearby.map do |s| " #{s[:prefix]} type=#{s[:type]} pos=(#{s[:x]},#{s[:y]}) dist=#{s[:dist]} " \ "screen_x=#{s[:screen_x]} scale=#{s[:sprite_scale]} " \ "range=#{s[:screen_range]} status=#{s[:status]} " \ "clip_segs=#{s[:clipping_segs]}" \ "#{if s[:clipping_detail]&.any? "\n clips: #{s[:clipping_detail].map do |c| "ds[#{c[:x1]}..#{c[:x2]}] scale=#{c[:scale]} sil=#{c[:sil]}" end.join(', ')}" end}" end File.write("#{prefix}.txt", <<~INFO) pos: #{@player.x.round(1)}, #{@player.y.round(1)}, #{@player.z.round(1)} angle: #{angle_deg.round(1)} sector: #{sector_idx} floor: #{sector&.floor_height} (#{sector&.floor_texture}) ceil: #{sector&.ceiling_height} (#{sector&.ceiling_texture}) light: #{sector&.light_level} nearby sprites (#{nearby.size}): #{sprite_lines.join("\n")} INFO end |
#compute_skill_hidden(skill) ⇒ Object
880 881 882 |
# File 'lib/doom/platform/gosu_window.rb', line 880 def compute_skill_hidden(skill) WindowLogic.skill_hidden(skill, @map.things) end |
#draw ⇒ Object
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 |
# File 'lib/doom/platform/gosu_window.rb', line 433 def draw @present_frames += 1 @last_present_ms = Gosu.milliseconds # Aim the camera at the local player. The simulation runs at 35 Hz but # we draw as fast as we can, so interpolate between the previous tic's # pose and the current one by however far into the tic we are. @renderer.apply_view(*@player.view_pose(@tic_accumulator)) # Intermission screen if @intermission fb = Array.new(Render::SCREEN_WIDTH * Render::SCREEN_HEIGHT, 0) @intermission.render(fb) present(fb) return end # Screen melt effect in progress if @screen_melt && !@screen_melt.done? fb = Array.new(Render::SCREEN_WIDTH * Render::SCREEN_HEIGHT, 0) @screen_melt.update(fb) present(fb) @screen_melt = nil if @screen_melt.done? return end if @menu&.active? hardware = @renderer.respond_to?(:hardware?) && @renderer.hardware? if hardware && @menu.needs_background? @renderer.draw_hardware(width, height) draw_hardware_hud(menu: true) return elsif @menu.needs_background? # Render game view + HUD as background, then overlay menu on top @renderer.render_frame @weapon_renderer&.render(@renderer.framebuffer) unless @player_state&.dead @status_bar&.render(@renderer.framebuffer) fb = @renderer.framebuffer.dup else # Title screen: black background fb = Array.new(Render::SCREEN_WIDTH * Render::SCREEN_HEIGHT, 0) end @menu.render(fb, nil) # Capture current menu frame for melt transitions @last_menu_fb = fb.dup present(fb) elsif @show_map draw_automap else if @renderer.respond_to?(:hardware?) && @renderer.hardware? @renderer.draw_hardware(width, height) draw_hardware_hud else present(@renderer.framebuffer, active_palette_index) end if @show_debug draw_net_status if @session draw_match_status if @world.deathmatch? end end |
#draw_automap ⇒ Object
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 |
# File 'lib/doom/platform/gosu_window.rb', line 1098 def draw_automap # Black background Gosu.draw_rect(0, 0, width, height, Gosu::Color::BLACK, 0) bounds = map_bounds return unless bounds verts = @map.vertices min_x = bounds[:min_x] max_x = bounds[:max_x] min_y = bounds[:min_y] max_y = bounds[:max_y] map_w = max_x - min_x map_h = max_y - min_y # Scale to fit screen with margin draw_w = width - (MAP_MARGIN * 2) draw_h = height - (MAP_MARGIN * 2) scale = [draw_w.to_f / map_w, draw_h.to_f / map_h].min # Center the map offset_x = MAP_MARGIN + ((draw_w - (map_w * scale)) / 2.0) offset_y = MAP_MARGIN + ((draw_h - (map_h * scale)) / 2.0) # World to screen coordinate transform (Y flipped: world Y+ is up, screen Y+ is down) to_sx = ->(wx) { offset_x + ((wx - min_x) * scale) } to_sy = ->(wy) { offset_y + ((max_y - wy) * scale) } # Draw linedefs colored by front sector Gosu::Color.new(100, 80, 80, 80) @map.linedefs.each do |linedef| v1 = verts[linedef.v1] v2 = verts[linedef.v2] sx1 = to_sx.call(v1.x) sy1 = to_sy.call(v1.y) sx2 = to_sx.call(v2.x) sy2 = to_sy.call(v2.y) if linedef.two_sided? # Two-sided: dim line, colored by front sector front_sd = @map.sidedefs[linedef.sidedef_right] color = @sector_colors[front_sd.sector] dim = Gosu::Color.new(100, color.red, color.green, color.blue) Gosu.draw_line(sx1, sy1, dim, sx2, sy2, dim, 1) else # One-sided: solid wall, bright sector color front_sd = @map.sidedefs[linedef.sidedef_right] color = @sector_colors[front_sd.sector] Gosu.draw_line(sx1, sy1, color, sx2, sy2, color, 1) end end # Draw player px = to_sx.call(@player.x) py = to_sy.call(@player.y) cos_a = @player.cos_angle sin_a = @player.sin_angle # FOV cone fov_len = 40.0 half_fov = Math::PI / 4.0 # 45 deg half = 90 deg total # Cone edges (in world space, Y+ is up; on screen Y is flipped via to_sy) left_dx = (Math.cos(half_fov) * cos_a) - (Math.sin(half_fov) * sin_a) left_dy = (Math.cos(half_fov) * sin_a) + (Math.sin(half_fov) * cos_a) right_dx = (Math.cos(-half_fov) * cos_a) - (Math.sin(-half_fov) * sin_a) right_dy = (Math.cos(-half_fov) * sin_a) + (Math.sin(-half_fov) * cos_a) # Screen positions for cone tips lx = px + (left_dx * fov_len) ly = py - (left_dy * fov_len) # negate because screen Y is flipped rx = px + (right_dx * fov_len) ry = py - (right_dy * fov_len) cone_color = Gosu::Color.new(60, 0, 255, 0) Gosu.draw_triangle(px, py, cone_color, lx, ly, cone_color, rx, ry, cone_color, 2) # Cone edge lines edge_color = Gosu::Color.new(180, 0, 255, 0) Gosu.draw_line(px, py, edge_color, lx, ly, edge_color, 3) Gosu.draw_line(px, py, edge_color, rx, ry, edge_color, 3) # Player dot dot_size = 4 Gosu.draw_rect(px - dot_size, py - dot_size, dot_size * 2, dot_size * 2, Gosu::Color::GREEN, 3) # Direction line dir_len = 12.0 dx = px + (cos_a * dir_len) dy = py - (sin_a * dir_len) Gosu.draw_line(px, py, Gosu::Color::WHITE, dx, dy, Gosu::Color::WHITE, 3) end |
#draw_debug_overlay ⇒ Object
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 |
# File 'lib/doom/platform/gosu_window.rb', line 626 def yjit_status = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? 'ON' : 'OFF' renderer_name = Render::RendererFactory.type_of(@renderer).to_s ang = (Math.atan2(@player.sin_angle, @player.cos_angle) * 180.0 / Math::PI).round(1) # Shown vs generated: only worth spelling out when they differ. shown = if @uncapped_fps "shown #{@present_fps_display} / #{@refresh_hz.round} Hz" else 'vsync' end lines = if @menu&.&.[](:rubykaigi_mode) [ "#{@fps_display} FPS (#{shown})", "YJIT: #{yjit_status} (Y to toggle)", "Ruby #{RUBY_VERSION}", "Renderer: #{renderer_name}", "Map: #{@current_map}", "Pos: #{@player.x.round}, #{@player.y.round}", "Ang: #{ang}" ] else [ "FPS: #{@fps_display} (#{shown})", "YJIT: #{yjit_status}", "Renderer: #{renderer_name}", "Pos: #{@player.x.round}, #{@player.y.round}", "Ang: #{ang}" ] end y = 4 lines.each do |line| @debug_font.draw_text(line, 8, y + 2, 1, 1, 1, Gosu::Color::BLACK) @debug_font.draw_text(line, 6, y, 1, 1, 1, Gosu::Color::WHITE) y += 26 end end |
#draw_hardware_hud(menu: false) ⇒ Object
Hardware world rendering bypasses the indexed framebuffer. Build a transparent indexed overlay for the existing weapon/status renderers, preserving the gameplay UI while the world is drawn by OpenGL.
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/doom/platform/gosu_window.rb', line 515 def draw_hardware_hud(menu: false) # The weapon hand is a held object, so the world's light colours it: tint # it by the light where the player stands (lamps, the flashlight). Drawn # on its own so only it is lit -- never while a menu is up or the player # is dead. unless || @player_state&.dead weapon = @weapon_renderer&.render(weapon) (weapon, active_palette_index, weapon_light_tint) end # HUD, pickup message and menu: readable UI, never lit by the world. The # menu also drops the pain/pickup tint (normal palette), as the classic # present(fb) path does. hud = @status_bar&.render(hud) if @doom_font && @item_pickup&. && @item_pickup..positive? @doom_font.draw_text(hud, @item_pickup., 2, 2) end @menu.render(hud, nil) if (hud, ? 0 : active_palette_index, nil) end |
#draw_match_status ⇒ Object
Deathmatch scoreboard. One line of frags, and the result once someone has hit the limit -- the world decides who won, this only reports it.
591 592 593 594 595 596 597 598 599 600 601 |
# File 'lib/doom/platform/gosu_window.rb', line 591 def draw_match_status score = @world.frags.map { |id, frags| "P#{id + 1} #{frags}" }.join(' ') @debug_font.draw_text(score, 20, 20, 2, 1, 1, Gosu::Color::YELLOW) winner = @world.match_winner return unless winner line = "PLAYER #{winner.id + 1} WINS -- #{winner.frags} FRAGS" @debug_font.draw_text(line, 22, (height / 3) + 2, 2, 1, 1, Gosu::Color::BLACK) @debug_font.draw_text(line, 20, height / 3, 2, 1, 1, Gosu::Color::YELLOW) end |
#draw_net_status ⇒ Object
A lockstep stall looks exactly like a freeze unless we say otherwise, and a desync means everything on screen is already wrong -- both are worth interrupting the player for.
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 |
# File 'lib/doom/platform/gosu_window.rb', line 606 def draw_net_status lines = WindowLogic.net_status_lines( started: @session.started?, host: @session.host?, waiting: @session.waiting_on, stalled_seconds: @session.stalled_seconds, stall_threshold: Net::Session::STALL_WARNING_SECONDS, desync: @session.desyncs.first ) return if lines.empty? y = height / 3 lines.each do |line| @debug_font.draw_text(line, 22, y + 2, 2, 1, 1, Gosu::Color::BLACK) @debug_font.draw_text(line, 20, y, 2, 1, 1, Gosu::Color::YELLOW) y += 30 end end |
#handle_input(_delta_time) ⇒ Object
Per-frame input sampling. Produces nothing but intent: the simulation itself runs per-tic in Game::World (fed the ticcmd from build_ticcmd). Mouse motion is accumulated here because frames are more frequent than tics and dropping the surplus would lose part of every flick.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/doom/platform/gosu_window.rb', line 301 def handle_input(_delta_time) # Handle respawn when dead. Local play only: respawn rebuilds the level, # which in a networked game would desync us from everyone else, so there # it is the server's job (not yet wired -- dead players stay down). if @player_state&.dead if !@session && !@client && @player_state.death_tic > 35 && (Gosu.(Gosu::KB_SPACE) || Gosu.(Gosu::KB_X) || Gosu.(Gosu::MS_LEFT) || Gosu.(Gosu::KB_LEFT_SHIFT)) respawn_player end return # No other input while dead end handle_mouse_look # accumulates into @pending_turn handle_weapon_switch if @player_state end |
#handle_mouse_look ⇒ Object
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/doom/platform/gosu_window.rb', line 385 def handle_mouse_look return unless @mouse_captured current_x = mouse_x if @last_mouse_x delta_x = current_x - @last_mouse_x # Accumulate rather than turn: the turn is applied by the next ticcmd, # so fast mouse motion between tics is summed instead of dropped. @pending_turn -= delta_x * MOUSE_SENSITIVITY if delta_x != 0 end # Keep mouse centered center_x = width / 2 if (current_x - center_x).abs > 50 self.mouse_x = center_x @last_mouse_x = center_x else @last_mouse_x = current_x end end |
#handle_option_toggle(option, value) ⇒ Object
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 |
# File 'lib/doom/platform/gosu_window.rb', line 822 def handle_option_toggle(option, value) # Belt and braces: the menu already hides these in a netgame, but a # cheat applied on one machine only is a real desync, so refuse it # here as well rather than trusting the menu to have filtered. return if (@session || @client) && Game::Menu::NETGAME_UNSAFE_OPTIONS.include?(option) case option when :god_mode @player_state.god_mode = value @player_state.health = 100 if value when :infinite_ammo @player_state.infinite_ammo = value when :all_weapons if value # Give all weapons that have sprites loaded @gfx_weapons ||= @weapon_renderer&.gfx&.weapons || {} (0..7).each do |w| name = Game::PlayerState::WEAPON_NAMES[w] @player_state.has_weapons[w] = true if @gfx_weapons[name]&.dig(:idle) end @player_state.ammo_bullets = @player_state.max_bullets @player_state.ammo_shells = @player_state.max_shells @player_state.ammo_rockets = @player_state.max_rockets @player_state.ammo_cells = @player_state.max_cells end when :uncapped_fps @uncapped_fps = value # Re-read on re-enable: the window may have moved to a display with a # different refresh rate. if value @refresh_hz = (SDLDisplayMode.refresh_rate || DEFAULT_REFRESH_HZ).to_f @present_interval_ms = 1000.0 / @refresh_hz end when :fullscreen self.fullscreen = value if respond_to?(:fullscreen=) when :rubykaigi_mode apply_rubykaigi_mode if value end end |
#handle_weapon_switch ⇒ Object
Weapon selection from the number keys. Read per-frame like the rest of input; the switch itself only changes which weapon the HUD/firing use.
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/doom/platform/gosu_window.rb', line 350 def handle_weapon_switch if Gosu.(Gosu::KB_1) @player_state.switch_weapon(Game::PlayerState::WEAPON_FIST) elsif Gosu.(Gosu::KB_2) @player_state.switch_weapon(Game::PlayerState::WEAPON_PISTOL) elsif Gosu.(Gosu::KB_3) @player_state.switch_weapon(Game::PlayerState::WEAPON_SHOTGUN) elsif Gosu.(Gosu::KB_4) @player_state.switch_weapon(Game::PlayerState::WEAPON_CHAINGUN) elsif Gosu.(Gosu::KB_5) @player_state.switch_weapon(Game::PlayerState::WEAPON_ROCKET) elsif Gosu.(Gosu::KB_6) @player_state.switch_weapon(Game::PlayerState::WEAPON_PLASMA) elsif Gosu.(Gosu::KB_7) @player_state.switch_weapon(Game::PlayerState::WEAPON_BFG) end end |
#hold_pain_palette ⇒ Object
Death keeps damage_count pinned at max so the draw loop selects the red pain palette every frame while dead. The tint itself is applied by palette selection in #draw, not here.
795 796 797 |
# File 'lib/doom/platform/gosu_window.rb', line 795 def hold_pain_palette @player_state.damage_count = 8 if @player_state&.dead end |
#hsv_to_gosu(h, s, v) ⇒ Object
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'lib/doom/platform/gosu_window.rb', line 1081 def hsv_to_gosu(h, s, v) c = v * s x = c * (1 - (((h / 60.0) % 2) - 1).abs) m = v - c r, g, b = case (h / 60).to_i % 6 when 0 then [c, x, 0] when 1 then [x, c, 0] when 2 then [0, c, x] when 3 then [0, x, c] when 4 then [x, 0, c] when 5 then [c, 0, x] end Gosu::Color.new(255, ((r + m) * 255).to_i, ((g + m) * 255).to_i, ((b + m) * 255).to_i) end |
#hud_tint_byte(value) ⇒ Object
565 566 567 |
# File 'lib/doom/platform/gosu_window.rb', line 565 def hud_tint_byte(value) (value * 255.0).clamp(0.0, 255.0).to_i end |
#load_next_map(map_name) ⇒ Object
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 |
# File 'lib/doom/platform/gosu_window.rb', line 910 def load_next_map(map_name) return unless map_name wad = @renderer.wad @current_map = map_name # Load new map data map = Map::MapData.load(wad, map_name) @map = map @map_bounds = nil # Recompute on next automap draw @sector_colors = build_sector_colors # Rebuild all systems for new map @renderer = Render::RendererFactory.build( Render::RendererFactory.type_of(@renderer), wad, map, @renderer.textures, @palette, @renderer.colormap, @renderer.flats.values, @renderer.sprites, @animations ) # A new map means a new world; the RNG carries over so the run stays # one continuous deterministic sequence across level changes. skill_hidden = compute_skill_hidden(@skill || Game::Menu::SKILL_MEDIUM) world = Game::World.new(map, sprites: @renderer.sprites, sound: @sound, random: @random, skill_hidden: skill_hidden) world.damage_multiplier = @damage_multiplier world.item_pickup.ammo_multiplier = @skill == Game::Menu::SKILL_BABY ? 2 : 1 world.monster_ai.aggression = true world.monster_ai.damage_multiplier = @damage_multiplier world.add_player bind_world(world) end |
#map_bounds ⇒ Object
Lazily computed and cached on first access; cleared by load_next_map.
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/doom/platform/gosu_window.rb', line 369 def map_bounds return @map_bounds if defined?(@map_bounds) && @map_bounds min_x = min_y = Float::INFINITY max_x = max_y = -Float::INFINITY @map.vertices.each do |v| min_x = v.x if v.x < min_x max_x = v.x if v.x > max_x min_y = v.y if v.y < min_y max_y = v.y if v.y > max_y end return nil if max_x == min_x || max_y == min_y @map_bounds = { min_x: min_x, max_x: max_x, min_y: min_y, max_y: max_y } end |
#needs_cursor? ⇒ Boolean
1000 1001 1002 |
# File 'lib/doom/platform/gosu_window.rb', line 1000 def needs_cursor? !@mouse_captured end |
#needs_redraw? ⇒ Boolean
Gosu calls this before draw; false skips both the blit and the buffer swap, so the main loop keeps generating frames without waiting on the display. Returning true unconditionally restores plain vsync behaviour.
409 410 411 412 413 414 |
# File 'lib/doom/platform/gosu_window.rb', line 409 def needs_redraw? return true unless @uncapped_fps WindowLogic.present_due?(Gosu.milliseconds, @last_present_ms, @present_interval_ms, PRESENT_INTERVAL_SLACK) end |
#net_send_due? ⇒ Boolean
True at most once per tic interval, so outbound packets track the 35 Hz simulation rather than the uncapped frame rate. Advances the clock as a side effect when it fires.
176 177 178 179 180 181 182 |
# File 'lib/doom/platform/gosu_window.rb', line 176 def net_send_due? now = Time.now return false if @last_net_send_at && (now - @last_net_send_at) < NET_SEND_INTERVAL @last_net_send_at = now true end |
#new_hud_overlay ⇒ Object
538 539 540 |
# File 'lib/doom/platform/gosu_window.rb', line 538 def Array.new(Render::SCREEN_WIDTH * Render::SCREEN_HEIGHT, -1) end |
#present(framebuffer, pal_idx = 0) ⇒ Object
Blit one palette-indexed framebuffer to the window. pal_idx selects one of the 14 prebuilt RGBA palettes (0 = normal, 1-8 = pain red, 9 = pickup yellow). All four draw paths funnel through here so the blob->image->draw sequence lives in exactly one place.
503 504 505 506 507 508 509 510 |
# File 'lib/doom/platform/gosu_window.rb', line 503 def present(framebuffer, pal_idx = 0) active_pal = @all_palette_rgba[pal_idx] rgba = framebuffer.map { |idx| active_pal[idx] }.join @screen_image = Gosu::Image.from_blob( Render::SCREEN_WIDTH, Render::SCREEN_HEIGHT, rgba ) @screen_image.draw(0, 0, 0, SCALE, SCALE) end |
#respawn_player ⇒ Object
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 |
# File 'lib/doom/platform/gosu_window.rb', line 799 def respawn_player if @world.deathmatch? # Deathmatch death is personal: everyone else is still playing, so # only this player comes back. restart_level would revive the monsters # and restore the items under them. @world.respawn(@player) else # Single player: death restarts the level, so the world rebuilds its # actor subsystems and the cached references must be refreshed. @world.restart_level(@player) end bind_world(@world) # Re-apply active cheats from menu options return unless @menu opts = @menu. @player_state.god_mode = opts[:god_mode] @player_state.infinite_ammo = opts[:infinite_ammo] handle_option_toggle(:all_weapons, true) if opts[:all_weapons] apply_rubykaigi_mode if opts[:rubykaigi_mode] end |
#send_client_input ⇒ Object
Sample this frame's input for a near-future tic and send it, paced to the tic rate, with a short redundancy window against packet loss. Only the newest sample for a given target tic is kept.
160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/doom/platform/gosu_window.rb', line 160 def send_client_input return unless net_send_due? tic = @client.synced_tic + CLIENT_INPUT_LEAD if @client_input_window.last&.first == tic @client_input_window[-1] = [tic, build_ticcmd] else @client_input_window << [tic, build_ticcmd] end @client_input_window = @client_input_window.last(CLIENT_INPUT_REDUNDANCY) @client.send_input(@client_input_window) end |
#setup_yjit_toggle ⇒ Object
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 |
# File 'lib/doom/platform/gosu_window.rb', line 970 def setup_yjit_toggle return if @yjit_toggle_ready || !defined?(RubyVM::YJIT) require 'fiddle' address = Fiddle::Handle::DEFAULT['rb_yjit_enabled_p'] enabled_ptr = Fiddle::Pointer.new(address, Fiddle::SIZEOF_CHAR) RubyVM::YJIT.singleton_class.prepend(Module.new do define_method(:enable) do |**kwargs| return false if enabled? return super(**kwargs) unless RUBY_DESCRIPTION.include?('+YJIT') enabled_ptr[0] = 1 true end define_method(:disable) do return false unless enabled? enabled_ptr[0] = 0 true end end) @yjit_toggle_ready = true rescue StandardError => e warn "YJIT toggle setup failed: #{e.class}: #{e.}" end |
#switch_renderer ⇒ Object
781 782 783 784 785 786 787 788 789 790 |
# File 'lib/doom/platform/gosu_window.rb', line 781 def switch_renderer Render::RendererFactory.type_of(@renderer) target = Render::RendererFactory.next_type(@renderer) replacement = Render::RendererFactory.build_like(@renderer, target) replacement.apply_view(@renderer.player_x, @renderer.player_y, @renderer.player_z, @renderer.player_angle) replacement.skip_background_fill = @renderer.skip_background_fill @renderer = replacement puts "Renderer: #{target}" end |
#sync_renderer_visual_options ⇒ Object
569 570 571 572 573 574 575 |
# File 'lib/doom/platform/gosu_window.rb', line 569 def return unless @menu @renderer.fog_enabled = @menu.[:fog] if @renderer.respond_to?(:fog_enabled=) @renderer.flashlight_enabled = @menu.[:flashlight] if @renderer.respond_to?(:flashlight_enabled=) @renderer.bounces_enabled = @menu.[:rt_bounces] if @renderer.respond_to?(:bounces_enabled=) end |
#track_frame_rates ⇒ Object
Frames generated vs frames actually shown. With uncapped rendering the first number can run well above the second, which is the whole point.
420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/doom/platform/gosu_window.rb', line 420 def track_frame_rates @fps_frames += 1 now = Time.now elapsed = now - @fps_time return if elapsed < 0.5 @fps_display = (@fps_frames / elapsed).round(1) @present_fps_display = (@present_frames / elapsed).round(1) @fps_frames = 0 @present_frames = 0 @fps_time = now end |
#trigger_level_exit(exit_type) ⇒ Object
884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 |
# File 'lib/doom/platform/gosu_window.rb', line 884 def trigger_level_exit(exit_type) # Gather stats total_monsters = @monster_ai ? @monster_ai.monsters.size : 0 killed = @combat ? @combat.dead_things.size : 0 total_items = Game::ItemPickup::ITEMS.keys.count do |t| @map.things.any? { |th| th.type == t } end picked = @item_pickup ? @item_pickup.picked_up.size : 0 # Secret sectors (type 9) tracked by SectorActions total_secrets = @map.sectors.count { |s| s.special == 9 } found_secrets = @sector_actions ? @sector_actions.secrets_found.size : 0 stats = { map: @current_map, kills: killed, total_kills: total_monsters, items: picked, total_items: total_items, secrets: found_secrets, total_secrets: total_secrets, time_tics: @leveltime, exit_type: exit_type } @intermission = Game::Intermission.new(@renderer.wad, @status_bar.gfx, stats) end |
#update ⇒ Object
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/doom/platform/gosu_window.rb', line 216 def update # Calculate delta time for smooth animations now = Time.now delta_time = now - @last_update_time @last_update_time = now # Menu is active -- only update menu animation, skip game logic. # # Except in a networked game, which must never stop simulating. Peers # are waiting on our ticcmds and lockstep cannot skip a tic, so a # paused peer stops the whole session; if the pause outlasts the # commands the others still hold, it wedges permanently. DOOM does not # pause netgames either. Input is neutral while the menu has focus, so # navigating it does not also drive the player. if @menu&.active? @menu.update if @session @tic_accumulator += delta_time * 35.0 advance_networked elsif @client advance_server_client end return end # Intermission screen active if @intermission @intermission.update return end handle_input(delta_time) # Advance the simulation at 35/sec (DOOM's tic rate). Everything that # decides what happens now lives in Game::World; this loop only decides # how many tics are owed and hands over the input for each. @tic_accumulator += delta_time * 35.0 if @session advance_networked elsif @client advance_server_client else advance_local end @animations&.update(@leveltime) @status_bar&.update @renderer.hidden_things = @world.hidden_things trigger_level_exit(@world.exit_triggered) if @world.exit_triggered && !@intermission # Other players are drawn as sprites; the local one never is. @renderer.players = @world.players @renderer.view_player = @player # Pass combat state to renderer for death frame rendering @renderer.combat = @combat @renderer.monster_ai = @monster_ai @renderer.leveltime = @leveltime # Render the 3D world. This is the generated-frame rate: it runs every # loop iteration, whether or not the result gets presented. @renderer.render_frame track_frame_rates # Render HUD on top @weapon_renderer.render(@renderer.framebuffer) if @weapon_renderer && !@player_state&.dead @status_bar.render(@renderer.framebuffer) if @status_bar # Pickup message (drawn into framebuffer with DOOM font, 4 seconds like Chocolate Doom) if @doom_font && @item_pickup&. && @item_pickup. > 0 @doom_font.draw_text(@renderer.framebuffer, @item_pickup., 2, 2) end # Red tint when dead return unless @player_state&.dead hold_pain_palette end |
#weapon_light_tint ⇒ Object
The world light reaching the player, as a Gosu colour to multiply the weapon by. Only the hardware/ray-traced renderer computes this; the rasterizer leaves the weapon at full brightness.
558 559 560 561 562 563 |
# File 'lib/doom/platform/gosu_window.rb', line 558 def weapon_light_tint return nil unless @renderer.respond_to?(:light_color_at) r, g, b = @renderer.light_color_at(@player.x, @player.y, @player.z) Gosu::Color.rgba(hud_tint_byte(r), hud_tint_byte(g), hud_tint_byte(b), 255) end |