a94bccc57b
Capital ships & support art: - New Flagship capital ships (world/flagship.gd) spawn a small point-defense formation at each team's spawn marker, independently targeting the nearest enemy in range with a slow unmissable-looking missile and a faster bullet stream -- keeps a team from parking on the enemy spawn and farming respawns. - Per-race/role bullet sprites for turret fire (assets/images/effects/ bullets/), flagship art for both factions, minimap now draws flagship blips, bot_ai awareness tuned to notice them. Settings screen (items 20-21 in CLAUDE.md) -- was a stub, now five real sections shared between the main menu and the in-game pause menu: - Diagnosed and fixed a "movement looks blurry/laggy" report down to three independent causes: physics-tick vs. display-refresh judder (fixed via Godot's built-in physics interpolation, plus a frame-rate-cap + VSync dropdown so each player can match their own monitor), missing mipmaps on every minified ship texture (real GPU sampling shimmer, unrelated to frame timing), and a periodic hitch from the matchmaking heartbeat spinning up a new HTTPRequest thread every 8 seconds instead of reusing one. - Nameplates get their own manual per-frame interpolation, since Godot's physics interpolation only covers Node2D/Node3D, not the Control-based Label they're built from. - Audio: Master/Music/SFX volume sliders backed by a real bus layout (default_bus_layout.tres) -- the project had no volume control at all before this. - Window mode (Fullscreen/Exclusive Fullscreen/Windowed), a colorblind-friendly enemy-color toggle (also fixes the minimap's own separate, inconsistent yellow enemy color), and keyboard rebinding for every action with a live capture UI. - New GameConfig.settings_focused flag (same pattern as chat_focused/ team_select_focused) so a key-rebind capture can't also move the ship or toggle the pause menu underneath the panel. Spawn intro: a ship's first-ever appearance now eases in from a random direction (fast off the start, decelerating into its landing spot) instead of popping into place -- purely a cosmetic sprite offset, so collision/camera/networking are untouched and every peer (including bots) sees the same warp-in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
402 lines
20 KiB
GDScript
402 lines
20 KiB
GDScript
extends Node
|
|
|
|
# Render frame-rate cap (Engine.max_fps) -- 0 mirrors Engine.max_fps's own
|
|
# "no cap" meaning. Persisted to user://settings.cfg so each player's choice
|
|
# survives restarts; changed live via menu/settings_panel.gd (reachable from
|
|
# both the main menu's OPTIONS and the in-game pause menu's SETTINGS, see
|
|
# main_menu.gd/pause_menu.gd), not hardcoded here, since the right value
|
|
# depends on each player's own monitor refresh rate -- physics only ever
|
|
# ticks at a fixed 60Hz (see ships/ship_movement.gd), so a render rate that
|
|
# isn't a clean multiple of that (e.g. an uncapped rate on a high-refresh
|
|
# monitor) is what reads as stuttery/blurry motion, especially against
|
|
# world/starfield.gdshader's pixel-crisp stars.
|
|
const SETTINGS_PATH := "user://settings.cfg"
|
|
var max_fps: int = 0
|
|
|
|
# DisplayServer.VSyncMode int value (VSYNC_ENABLED=1 is both Godot's and this
|
|
# project's prior default -- project.godot never overrode it, so this keeps
|
|
# existing behavior unchanged until a player picks something else). Separate
|
|
# knob from max_fps -- capping frame rate doesn't help if the cap itself
|
|
# doesn't match what vsync will actually let through (see menu/settings_panel.gd's
|
|
# vsync dropdown, added after a 165 FPS cap on a display vsync wouldn't
|
|
# cleanly deliver at that rate caused a repeating smooth/stutter cycle that
|
|
# capping to exactly 60 fixed -- Adaptive/Disabled here are the other two
|
|
# standard ways to resolve that same class of mismatch without guessing at
|
|
# the "right" cap).
|
|
var vsync_mode: int = DisplayServer.VSYNC_ENABLED
|
|
|
|
# DisplayServer.WindowMode int value -- project.godot's window/size/mode=3
|
|
# already boots into WINDOW_MODE_FULLSCREEN (Godot's non-exclusive
|
|
# borderless-style fullscreen, see item 10 in CLAUDE.md); this just makes
|
|
# that switchable at runtime instead of fixed. Exclusive Fullscreen can
|
|
# perform better on some GPU/driver combos at the cost of slower alt-tabbing;
|
|
# Windowed is for players who want to multitask or stream.
|
|
var window_mode: int = DisplayServer.WINDOW_MODE_FULLSCREEN
|
|
|
|
# Master/Music/SFX, linear 0..1 (matches HSlider's natural range in
|
|
# menu/settings_panel.gd) — converted to dB only when actually applied to a
|
|
# bus. "Music"/"SFX" are real AudioServer buses (default_bus_layout.tres,
|
|
# both routed to Master) that MusicManager and world.gd's _play_shoot_sound()
|
|
# route their players onto; Master is the built-in bus every sound already
|
|
# passes through regardless. 0.0 explicitly mutes the bus (AudioServer.
|
|
# set_bus_mute()) rather than relying on linear_to_db(0)'s -inf dB edge case.
|
|
var master_volume: float = 1.0
|
|
var music_volume: float = 1.0
|
|
var sfx_volume: float = 1.0
|
|
|
|
# Key rebinding (menu/settings_panel.gd's CONTROLS section) — keyboard-only
|
|
# (this project's only non-keyboard binding is toggle_pause's joypad Start
|
|
# button, which _apply_single_keybind() below always leaves untouched, so
|
|
# there's nothing to rebind there yet). InputMap resets to project.godot's
|
|
# compiled-in defaults on every engine boot, so DEFAULT_KEYCODES mirrors that
|
|
# file's [input] section (needed both to reapply a persisted override in
|
|
# _apply_keybinds() and to know what "reset to default" means once an
|
|
# action's original binding has already been erased from InputMap).
|
|
# keybinds only holds entries that override a default; an action absent from
|
|
# it just means "still on its default", not "unbound".
|
|
const KEYBIND_ACTIONS := ["move_up", "move_down", "move_left", "move_right", "shoot", "toggle_pause", "chat_all", "chat_team"]
|
|
const DEFAULT_KEYCODES := {
|
|
"move_up": KEY_W, "move_down": KEY_S, "move_left": KEY_A, "move_right": KEY_D,
|
|
"shoot": KEY_SPACE, "toggle_pause": KEY_ESCAPE, "chat_all": KEY_T, "chat_team": KEY_Y,
|
|
}
|
|
var keybinds: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
_load_settings()
|
|
Engine.max_fps = max_fps
|
|
DisplayServer.window_set_vsync_mode(vsync_mode as DisplayServer.VSyncMode)
|
|
DisplayServer.window_set_mode(window_mode as DisplayServer.WindowMode)
|
|
_apply_bus_volume("Master", master_volume)
|
|
_apply_bus_volume("Music", music_volume)
|
|
_apply_bus_volume("SFX", sfx_volume)
|
|
enemy_color = ENEMY_COLOR_COLORBLIND if colorblind_mode else ENEMY_COLOR_DEFAULT
|
|
for action in KEYBIND_ACTIONS:
|
|
_apply_single_keybind(action, get_keycode(action))
|
|
|
|
|
|
func set_max_fps(value: int) -> void:
|
|
max_fps = value
|
|
Engine.max_fps = value
|
|
save_settings()
|
|
|
|
|
|
func set_vsync_mode(value: int) -> void:
|
|
vsync_mode = value
|
|
DisplayServer.window_set_vsync_mode(value as DisplayServer.VSyncMode)
|
|
save_settings()
|
|
|
|
|
|
func set_window_mode(value: int) -> void:
|
|
window_mode = value
|
|
DisplayServer.window_set_mode(value as DisplayServer.WindowMode)
|
|
save_settings()
|
|
|
|
|
|
func set_colorblind_mode(value: bool) -> void:
|
|
colorblind_mode = value
|
|
enemy_color = ENEMY_COLOR_COLORBLIND if value else ENEMY_COLOR_DEFAULT
|
|
save_settings()
|
|
|
|
|
|
func get_keycode(action: String) -> int:
|
|
return keybinds.get(action, DEFAULT_KEYCODES.get(action, 0))
|
|
|
|
|
|
func rebind_action(action: String, keycode: int) -> void:
|
|
keybinds[action] = keycode
|
|
_apply_single_keybind(action, keycode)
|
|
save_settings()
|
|
|
|
|
|
func reset_keybind(action: String) -> void:
|
|
keybinds.erase(action)
|
|
_apply_single_keybind(action, DEFAULT_KEYCODES.get(action, 0))
|
|
save_settings()
|
|
|
|
|
|
# Only touches this action's InputEventKey entries -- toggle_pause also has a
|
|
# joypad Start-button event in project.godot's default map (Steam Deck), and
|
|
# wiping every event on the action (instead of just the keyboard ones) would
|
|
# silently break that controller binding the first time a player rebinds its
|
|
# keyboard key.
|
|
func _apply_single_keybind(action: String, keycode: int) -> void:
|
|
if not InputMap.has_action(action):
|
|
return
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventKey:
|
|
InputMap.action_erase_event(action, event)
|
|
var new_event := InputEventKey.new()
|
|
new_event.physical_keycode = keycode
|
|
InputMap.action_add_event(action, new_event)
|
|
|
|
|
|
# Volume setters deliberately don't call save_settings() themselves -- an
|
|
# HSlider fires value_changed continuously through a drag, and writing
|
|
# user://settings.cfg to disk on every one of those (unlike a dropdown's one
|
|
# discrete selection) would reintroduce exactly the kind of per-event hitch
|
|
# this session already hunted down once (see the matchmaking heartbeat fix in
|
|
# CLAUDE.md item 20). menu/settings_panel.gd instead applies live on every
|
|
# value_changed and calls save_settings() once on the slider's drag_ended.
|
|
func set_master_volume(value: float) -> void:
|
|
master_volume = value
|
|
_apply_bus_volume("Master", value)
|
|
|
|
|
|
func set_music_volume(value: float) -> void:
|
|
music_volume = value
|
|
_apply_bus_volume("Music", value)
|
|
|
|
|
|
func set_sfx_volume(value: float) -> void:
|
|
sfx_volume = value
|
|
_apply_bus_volume("SFX", value)
|
|
|
|
|
|
func _apply_bus_volume(bus_name: String, value: float) -> void:
|
|
var idx := AudioServer.get_bus_index(bus_name)
|
|
if idx == -1:
|
|
return
|
|
AudioServer.set_bus_mute(idx, value <= 0.0)
|
|
AudioServer.set_bus_volume_db(idx, linear_to_db(maxf(value, 0.001)))
|
|
|
|
|
|
func save_settings() -> void:
|
|
var cfg := ConfigFile.new()
|
|
cfg.set_value("display", "max_fps", max_fps)
|
|
cfg.set_value("display", "vsync_mode", vsync_mode)
|
|
cfg.set_value("display", "window_mode", window_mode)
|
|
cfg.set_value("audio", "master_volume", master_volume)
|
|
cfg.set_value("audio", "music_volume", music_volume)
|
|
cfg.set_value("audio", "sfx_volume", sfx_volume)
|
|
cfg.set_value("accessibility", "colorblind_mode", colorblind_mode)
|
|
cfg.set_value("input", "keybinds", keybinds)
|
|
cfg.save(SETTINGS_PATH)
|
|
|
|
|
|
func _load_settings() -> void:
|
|
var cfg := ConfigFile.new()
|
|
if cfg.load(SETTINGS_PATH) == OK:
|
|
max_fps = cfg.get_value("display", "max_fps", max_fps)
|
|
vsync_mode = cfg.get_value("display", "vsync_mode", vsync_mode)
|
|
window_mode = cfg.get_value("display", "window_mode", window_mode)
|
|
master_volume = cfg.get_value("audio", "master_volume", master_volume)
|
|
music_volume = cfg.get_value("audio", "music_volume", music_volume)
|
|
sfx_volume = cfg.get_value("audio", "sfx_volume", sfx_volume)
|
|
colorblind_mode = cfg.get_value("accessibility", "colorblind_mode", colorblind_mode)
|
|
keybinds = cfg.get_value("input", "keybinds", keybinds)
|
|
|
|
|
|
# Ship movement
|
|
var ship_thrust: float = 250.0
|
|
var ship_max_speed: float = 300.0
|
|
var ship_rotation_speed: float = 1.0
|
|
# Continuing to hold left/right past this many seconds ramps to
|
|
# ship_rotation_speed_held (double the tap rate) — a quick tap stays precise,
|
|
# a held turn goes faster. See ship_movement.gd's _simulate_step().
|
|
var ship_rotation_ramp_delay: float = 0.15
|
|
var ship_rotation_speed_held: float = 2.0
|
|
|
|
# Shooting — damage/energy cost/bullet count all scale per-role (Fighter/
|
|
# Gunner/Tank, see menu/team_select.gd's RACES "role" field); Gunner and Tank
|
|
# fire their bullets in a side-by-side pair instead of Fighter's single shot
|
|
# (see ship_movement.gd's _server_process_shoot()).
|
|
var ship_fire_rate: float = 0.15
|
|
var bullet_speed: float = 750.0
|
|
var ship_bullet_damage_by_role: Dictionary = {"Fighter": 100, "Gunner": 25, "Tank": 50}
|
|
var ship_bullet_count_by_role: Dictionary = {"Fighter": 1, "Gunner": 2, "Tank": 2}
|
|
var bullet_side_spacing: float = 14.0
|
|
var bullet_max_range: float = 1875.0 # 1500 * 1.25
|
|
|
|
# Per-shot sound effect (menu/team_select.gd's RACES "sound_path" field, one
|
|
# shared clip per faction) — max_distance is in world/pixel units, same as
|
|
# AudioStreamPlayer2D expects, so only ships within this radius of the shot
|
|
# actually hear it (see world.gd's _play_shoot_sound()).
|
|
var ship_shoot_sound_max_distance: float = 500.0
|
|
|
|
# Health & respawn
|
|
var ship_max_health: int = 100
|
|
var ship_respawn_delay: float = 3.0
|
|
var ship_invincibility_time: float = 2.0
|
|
|
|
# Energy (see ships/ship_movement.gd) — max energy scales by ship role
|
|
# (Fighter/Gunner/Tank, see menu/team_select.gd's RACES ship "role" field);
|
|
# regen is a flat rate regardless of role, so bigger ships take longer to
|
|
# top off from empty than smaller ones.
|
|
var ship_max_energy_by_role: Dictionary = {"Fighter": 100.0, "Gunner": 150.0, "Tank": 250.0}
|
|
var ship_bullet_energy_cost_by_role: Dictionary = {"Fighter": 75.0, "Gunner": 25.0, "Tank": 50.0}
|
|
var ship_energy_regen_rate: float = 100.0 / 2.5 # 100 energy per 2.5s
|
|
|
|
# Environment collision (asteroids only) — bounce-back and impact damage scale with impact speed
|
|
var ship_bounce_restitution: float = 0.45
|
|
var ship_collision_damage_scale: float = 0.064
|
|
|
|
# One shared knob to resize every ship's in-world sprite at once, applied on
|
|
# top of each ship's own per-role "scale" in team_select.gd's RACES (see
|
|
# ship_movement.gd's _init_player()) — doesn't touch those individually
|
|
# balanced per-role values, just scales the end result uniformly.
|
|
var ship_scale_factor: float = 0.75
|
|
|
|
# How much of the sprite's own texture bounds its collision capsule covers
|
|
# (see ship_movement.gd's _update_collision_shape()) — under 1.0 to stay
|
|
# inside the hull's soft/anti-aliased edge instead of the full texture rect.
|
|
var ship_hitbox_scale: float = 0.85
|
|
|
|
# Explosion VFX (effects/explosion.gd) — one shared knob to resize every
|
|
# explosion in the game at once; explosion_fps controls playback speed of the
|
|
# 12-frame animation (see assets/images/effects/explosion/). Each frame is
|
|
# held for 5 ticks (explosion.tscn's SpriteFrames "duration": 5.0 per frame,
|
|
# so mostly-transparent/dark frames like the opening spark and the frame 6
|
|
# ember cloud have time to register instead of reading as a flicker-to-black
|
|
# gap between the brighter frames) — total time is (12 * 5) / explosion_fps.
|
|
var explosion_scale: float = 0.75
|
|
var explosion_fps: float = 24.0 # 60 ticks / 24.0 fps = 2.5s total
|
|
|
|
# Damage-number VFX (effects/damage_number.gd) — floating "(amount)" combat
|
|
# text shown only to the attacker when their shot lands, not broadcast to
|
|
# every peer like the kill feed (see ship_movement.gd's
|
|
# _notify_damage_dealt()/_show_damage_number()).
|
|
var damage_number_rise_distance: float = 36.0
|
|
var damage_number_duration: float = 1.4
|
|
var damage_number_font_size: int = 34
|
|
var damage_number_color: Color = Color(0.95, 0.15, 0.15)
|
|
|
|
# Shared white/teammate vs. red/enemy color scheme -- used by the top-left
|
|
# roster (hud/player_list.gd), in-world ship nameplates (ships/ship_movement.gd),
|
|
# and the minimap (hud/mini_map.gd) so all three always agree. Was a yellow
|
|
# enemy_color that read too close to white/teammate at a glance; red is
|
|
# unambiguous -- for most people. team_color is white in both palettes below
|
|
# (already colorblind-safe, nothing to swap); only enemy_color changes.
|
|
const ENEMY_COLOR_DEFAULT := Color(0.95, 0.25, 0.25)
|
|
# Red can still read as muddy/brown for red-green color vision deficiency
|
|
# (deuteranopia/protanopia, the most common forms). Orange stays distinct
|
|
# from white/team and from the minimap's own blue self-marker
|
|
# (hud/mini_map.gd's SELF_COLOR) across effectively all forms of CVD,
|
|
# including tritanopia -- see set_colorblind_mode().
|
|
const ENEMY_COLOR_COLORBLIND := Color(1.0, 0.55, 0.0)
|
|
|
|
var team_color: Color = Color(0.92, 0.95, 1.0)
|
|
var enemy_color: Color = ENEMY_COLOR_DEFAULT
|
|
var colorblind_mode: bool = false
|
|
|
|
# In-world ship nameplate (ships/ship_movement.gd) -- offset from ship center
|
|
# so it sits at the bottom-right of the sprite rather than directly under it
|
|
# (keeps it clear of the engine flame and doesn't read as a health-bar-like
|
|
# element stacked straight below).
|
|
var ship_nameplate_offset: Vector2 = Vector2(26, 24)
|
|
var ship_nameplate_font_size: int = 18
|
|
|
|
# One shared knob for the local player's view distance -- Camera2D.zoom below
|
|
# 1.0 shows more world per screen pixel (zoomed out), above 1.0 shows less
|
|
# (zoomed in). Applied once in ship_movement.gd's _ready() to the owner's own
|
|
# Camera2D only; doesn't affect any other peer's view.
|
|
var camera_zoom: float = 0.85
|
|
|
|
# Locally-remembered callsign, pre-fills the name field on the menu.
|
|
# Race/ship/speed are per-peer now — see PlayerRegistry.
|
|
var player_name: String = ""
|
|
|
|
# Current map's play area, in world coordinates. Set by world.gd on load.
|
|
var world_bounds: Rect2 = Rect2(0, 0, 1152, 648)
|
|
|
|
# Ships spawn at a random point in an annulus (ring) around a chosen
|
|
# team_a_spawn/team_b_spawn marker (see ship_movement.gd's
|
|
# _get_spawn_position()) rather than right on top of it -- a plain small-radius
|
|
# square jitter left casual's up-to-25-per-team fill stacking ships nearly on
|
|
# top of each other. The inner radius also keeps ships mostly clear of the
|
|
# flagship formation now sitting on/near the marker (see world.gd's
|
|
# _spawn_flagships()) instead of spawning inside a hull.
|
|
var spawn_area_inner_radius: float = 420.0
|
|
var spawn_area_outer_radius: float = 1200.0
|
|
|
|
# Capital-ship backdrop on each team's spawn marker (world.gd's
|
|
# _spawn_flagships()) -- a small line-abreast formation instead of one hull,
|
|
# spaced along the axis perpendicular to the two teams' spawn line so the
|
|
# ships read as a fleet rather than overlapping each other.
|
|
# flagship_spawn_jitter is a per-ship random offset (both axes, see
|
|
# _spawn_flagship()) on top of that even spacing -- large relative to the
|
|
# spacing itself (deliberately: a small wobble still reads as "a line", this
|
|
# needs to read as scattered) so ships can end up well out of their nominal
|
|
# slot order, and flagship_spawn_rotation_jitter_deg randomizes each ship's
|
|
# facing too, so the formation reads as a loose defensive cluster instead of
|
|
# 3 hulls snapped to a ruler and all facing the same way.
|
|
var flagship_count_per_spawn: int = 3
|
|
var flagship_spawn_spacing: float = 950.0
|
|
var flagship_spawn_jitter: float = 750.0
|
|
var flagship_spawn_rotation_jitter_deg: float = 35.0
|
|
|
|
# Point-defense turret behavior (flagship.gd) -- each formation ship
|
|
# independently scans for the nearest enemy within flagship_defense_radius
|
|
# and fires two independent weapons at it, each on its own cooldown: a big,
|
|
# slow, unmissable-looking Missile and a faster-cadence stream of ordinary
|
|
# per-race bullets (the "Fighter" role sprite -- see BULLET_SPRITES in
|
|
# bullet.gd), both slower than a player's own shots so a target has a real
|
|
# chance to dodge despite a guaranteed-lethal hit. This is what keeps a team
|
|
# from parking on the enemy's spawn marker and farming respawns: get within
|
|
# range and multiple capital ships start shooting back with both.
|
|
# flagship_missile_damage/flagship_bullet_damage are set far above
|
|
# ship_max_health so a hit from either is always a kill regardless of the
|
|
# target's current health -- the only counterplay is staying out of range or
|
|
# dodging the (non-homing, fired straight at a lead-predicted point) shot
|
|
# before it arrives. flagship_defense_radius is kept comfortably under
|
|
# bullet_max_range (1875, shared with every other bullet) so a shot fired
|
|
# right at the edge of detection range still has enough travel budget left
|
|
# to reach a target that keeps moving away after it's launched.
|
|
var flagship_defense_radius: float = 1200.0
|
|
var flagship_fire_rate: float = 1.4
|
|
var flagship_missile_speed: float = 950.0
|
|
var flagship_missile_damage: int = 999
|
|
var flagship_missile_visual_scale: float = 0.8
|
|
|
|
var flagship_bullet_fire_rate: float = 0.9
|
|
var flagship_bullet_speed: float = 500.0
|
|
var flagship_bullet_damage: int = 999
|
|
|
|
# Random offset applied to both weapons' lead-predicted aim point (see
|
|
# flagship.gd's _lead_aim_dir()) -- keeps the turret feeling sharp without
|
|
# being an unavoidable guarantee at range; the farther out a target engages
|
|
# from, the more this same pixel offset translates into a wider angular
|
|
# miss, so distance is already its own accuracy penalty on top of this.
|
|
var flagship_aim_jitter_px: float = 90.0
|
|
|
|
# True while the chat input box has keyboard focus — gates ship movement/fire
|
|
# input so typing (e.g. the letter "w") doesn't also move the ship.
|
|
var chat_focused: bool = false
|
|
|
|
# True while TeamSelect is reopened mid-match (pause menu "SELECT TEAM") —
|
|
# same purpose as chat_focused, so clicking ship rows doesn't also fly the
|
|
# ship underneath. Not needed for the initial pre-spawn pick since the ship
|
|
# has no physics processing yet at that point.
|
|
var team_select_focused: bool = false
|
|
|
|
# True while SettingsPanel is open (see menu/settings_panel.gd's open()/
|
|
# close()) — same purpose as chat_focused/team_select_focused, but more
|
|
# load-bearing here: the CONTROLS section's key-rebind capture listens for
|
|
# literally any physical key, including WASD/Space, so without this a rebind
|
|
# click would also thrust/turn/fire the ship (if unpaused behind the panel)
|
|
# and toggle_pause's own Escape-to-cancel-a-rebind would also close the pause
|
|
# menu underneath it (pause_menu.gd's _input() checks this too).
|
|
var settings_focused: bool = false
|
|
|
|
# Bots (casual fill) — see bots/bot_manager.gd and overview/bots.md
|
|
var bot_min_team_size: int = 7 # mirrors matchmaking-api's CASUAL_TEAM_SIZE default
|
|
var bot_engage_range: float = 500.0
|
|
|
|
# Personality trait ranges (bots/bot_personality.gd) — each bot rolls all 5
|
|
# traits 0..1 independently once at spawn; these ranges convert that roll
|
|
# into the concrete numbers BotAI actually flies with.
|
|
var bot_stop_distance_min: float = 70.0 # aggression 1: dogfights up close
|
|
var bot_stop_distance_max: float = 260.0 # aggression 0: hangs back at range
|
|
var bot_retreat_health_frac_min: float = 0.10 # caution 0: fights to the death
|
|
var bot_retreat_health_frac_max: float = 0.65 # caution 1: breaks off early
|
|
var bot_aim_tolerance_best_deg: float = 3.0 # accuracy 1: pinpoint gate
|
|
var bot_aim_tolerance_worst_deg: float = 14.0 # accuracy 0: sloppy gate
|
|
var bot_aim_jitter_max_px: float = 220.0 # accuracy 0: aims this far off the real spot
|
|
var bot_reaction_update_best_sec: float = 0.05 # reaction 1: re-aims almost every tick
|
|
var bot_reaction_update_worst_sec: float = 0.6 # reaction 0: laggy, stale tracking
|
|
var bot_awareness_range_min: float = 900.0 # awareness 0: near-sighted
|
|
var bot_awareness_range_max: float = 16000.0 # awareness 1: sees across the whole map
|
|
# map_01 spawns the two teams ~10608 units apart (see world/maps/map_01.tscn),
|
|
# on a ~14016x6000 map (diagonal ~15247) — the max must clear that or every
|
|
# bot spawns permanently unable to detect the enemy team and never advances.
|