class_name WeaponComponent extends Node @export var weapon_data: WeaponShot = null @export var available_weapons: Array[WeaponShot] = [] @export var weapon_change_flash: BaseEffect = null @export var test_mode: bool = false var _current_weapon_index = 0 var _current_power_level = 0 var _cycled_weapon: WeaponShot = null @onready var effects_component: EffectComponent = get_node("../EffectsComponent") func _ready() -> void: # Set the default weapon to be stock, i.e. index 1 if weapon_data and not available_weapons.is_empty(): for i in range(available_weapons.size()): if available_weapons[i] == weapon_data: _current_weapon_index = i # Initialize the power level from the default weapon so # shooting works immediately without needing to cycle. _current_power_level = weapon_data.power_level return # If available_weapons was populated from the scene but no matching # weapon_data was found (e.g. all weapons share the same power level), # cap the power level to the minimum power level across all weapons. # This ensures initial shooting works even when weapon_data is null or # mismatches are found. if not available_weapons.is_empty(): var min_level: int = 999 for w: WeaponShot in available_weapons: if w != null and w.power_level < min_level: min_level = w.power_level _current_power_level = min_level func _compute_active_weapons() -> Array[WeaponShot]: # Filter available weapons by current power level. var active: Array[WeaponShot] = [] for weapon: WeaponShot in available_weapons: if weapon != null and weapon.power_level <= _current_power_level: active.append(weapon) return active func get_active_weapons(power_level: int = -1) -> Array[WeaponShot]: # Return all weapons active at the given power level. # # If power_level is negative, uses the internally tracked power level. # var _level: int = power_level if power_level >= 0 else _current_power_level # (unused - power_level parameter is ignored by design) # If the cycled weapon is set, return it exclusively — # cycling overrides all additive logic. if _cycled_weapon != null: return [_cycled_weapon] var base: Array[WeaponShot] = _compute_active_weapons() if not base.is_empty(): var additive: Array[WeaponShot] = base.filter( func(w: WeaponShot): return not w.force_replace_all ) if not additive.is_empty(): return additive # If no non-force-replace weapons are active, return only the current # default weapon (weapon_data). This ensures the starting weapon (stock) # fires as a single shot rather than all force_replace_all weapons firing # simultaneously. if weapon_data != null and base.has(weapon_data): return [weapon_data] return base.filter( func(w: WeaponShot): return w.force_replace_all ) func _get_force_replace_weapons() -> Array[WeaponShot]: # Return weapons that override all others (force_replace_all = true). return _compute_active_weapons().filter( func(w: WeaponShot): return w.force_replace_all ) func collect_weapon(new_weapon: WeaponShot) -> void: # Handle adding a weapon, applying replacement logic as needed. # # Replacement rules (applied in order): # 1. force_replace_all: Discard all weapons, add only this one. # Rule 2: replaces_at_same_level: Remove weapons at matching power level. # 3. Normal: Add additive weapon (no removal). # # Sets the power level to this weapon's if higher than current. # Emits weapon_changed with the new active set. # # Clear any active cycle — collecting a weapon during cycling restores # normal additive behavior. if _cycled_weapon != null and not test_mode: reset_weapon_cycle() # Rule 1: Force replace — discard everything, keep only this weapon if new_weapon.force_replace_all: available_weapons = [new_weapon] _current_power_level = new_weapon.power_level _update_data_from_active_weapons() _emit_weapon_changed() _trigger_change_flash() return # Rule 2: Replace if same level — remove weapons at matching power level if new_weapon.replaces_at_same_level: var was_replaced: bool = false for i in range(available_weapons.size()): if available_weapons[i] != null and \ available_weapons[i].replaces_at_same_level == false and \ available_weapons[i].power_level == new_weapon.power_level: available_weapons.remove_at(i) was_replaced = true break # Only replace the highest (first match at this level) if was_replaced: # Also remove any other weapons with the same power_level that may # have sneaked in via previous power level upgrades var filtered: Array[WeaponShot] = [] for w: WeaponShot in available_weapons: if w != null and w.power_level == new_weapon.power_level: continue filtered.append(w) available_weapons = filtered # Rule 3: Additive — just add it (no removal) var already_has: bool = false for w: WeaponShot in available_weapons: if w != null and w.shot_name == new_weapon.shot_name: already_has = true break if not already_has: available_weapons.append(new_weapon) # Update power level if this weapon is at a higher tier if new_weapon.power_level > _current_power_level: _current_power_level = new_weapon.power_level _update_data_from_active_weapons() _emit_weapon_changed() _trigger_change_flash() func _update_data_from_active_weapons() -> void: # Update weapon_data to point to the highest-priority active weapon. # # This is called after any weapon collection or power level change to # keep the legacy `weapon_data` property in sync for backward compatibility. # Force_replace_all weapons take priority over additive weapons. # Check for force_replace weapons first (they override everything) var force_replaces: Array[WeaponShot] = _get_force_replace_weapons() if not force_replaces.is_empty(): weapon_data = force_replaces[-1] return # Otherwise, point to the highest-priority additive weapon var active: Array[WeaponShot] = get_active_weapons() if not active.is_empty(): weapon_data = active[-1] else: weapon_data = null func _emit_weapon_changed() -> void: if weapon_data == null: return var ship: Sprite2D = get_parent().get_node("Ship") var sprite_size = ship.get_rect().size var origin_offset: float = weapon_data.origin EventBus.weapon_changed.emit(weapon_data.shot_name, get_parent(), sprite_size, origin_offset) func get_bullet_scene() -> PackedScene: return weapon_data.bullet_scene if weapon_data else null func get_weapon_resource() -> Resource: return weapon_data func select_weapon_by_name(weapon_name: String) -> bool: for i in range(available_weapons.size()): var candidate: WeaponShot = available_weapons[i] if candidate != null and candidate.shot_name == weapon_name: _cycled_weapon = candidate # override additive behavior print("Switched to: ", weapon_name) _trigger_change_flash() return true return false func _trigger_change_flash() -> void: if weapon_change_flash and effects_component: effects_component.apply_effect(weapon_change_flash) func cycle_weapon() -> void: # Used for testing weapon cycling if available_weapons.is_empty(): return _current_weapon_index = (_current_weapon_index + 1) % available_weapons.size() var new_weapon: WeaponShot = available_weapons[_current_weapon_index] if new_weapon == null: return print("Switched to: ", new_weapon.shot_name) # Remove the previous weapon from the active set before engaging the next. # Clearing _cycled_weapon forces get_active_weapons() to evaluate the # additive state (which temporarily re-includes the old weapon). Then # re-setting _cycled_weapon to the new weapon re-establishes the cycle # override. This two-step process ensures the old weapon is fully removed # from the active set and its power level doesn't leak into the new cycle. _cycled_weapon = null # 1) Remove previous weapon _cycled_weapon = new_weapon # 2) Engage next weapon _current_power_level = new_weapon.power_level # Update weapon_data to reflect the cycled weapon directly, # so _emit_weapon_changed() reports the correct name and origin. weapon_data = new_weapon _emit_weapon_changed() _trigger_change_flash() func reset_weapon_cycle() -> void: # Restore normal additive behavior after cycling. _cycled_weapon = null _update_data_from_active_weapons() _emit_weapon_changed() _trigger_change_flash()