diff --git a/actors/heroes/bounties/Vex 'Quantum' Cygnus.tres b/actors/heroes/bounties/Vex 'Quantum' Cygnus.tres new file mode 100644 index 00000000..4e7125b1 --- /dev/null +++ b/actors/heroes/bounties/Vex 'Quantum' Cygnus.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="Hero" load_steps=2 format=3 uid="uid://dehs0xqbalevx"] + +[ext_resource type="Script" path="res://actors/heroes/hero.gd" id="1_6aguh"] + +[resource] +script = ExtResource("1_6aguh") +name = "Vex 'Quantum' Cygnus" +bounty_target = true diff --git a/actors/heroes/bounties/Zora 'Starshiv' Blackburn.tres b/actors/heroes/bounties/Zora 'Starshiv' Blackburn.tres new file mode 100644 index 00000000..54a27e6a --- /dev/null +++ b/actors/heroes/bounties/Zora 'Starshiv' Blackburn.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="Hero" load_steps=2 format=3 uid="uid://qlbnowlag2dr"] + +[ext_resource type="Script" path="res://actors/heroes/hero.gd" id="1_5sbmg"] + +[resource] +script = ExtResource("1_5sbmg") +name = "Zora 'Starshiv' Blackburn" +bounty_target = true diff --git a/actors/heroes/hero.gd b/actors/heroes/hero.gd new file mode 100644 index 00000000..89afbed6 --- /dev/null +++ b/actors/heroes/hero.gd @@ -0,0 +1,15 @@ +extends Resource +class_name Hero + +## A "hero" is any named NPC. +## +## These are often used as a target or escort in missions. + +## The name of the hero. +@export var name: String + +## Whether this hero is eligible to be targeted in bounty missions. +@export var bounty_target: bool = false + +## Fires when this hero is killed in combat. +signal killed(hero: Hero) diff --git a/actors/heroes/hero_roster.gd b/actors/heroes/hero_roster.gd new file mode 100644 index 00000000..39e4911c --- /dev/null +++ b/actors/heroes/hero_roster.gd @@ -0,0 +1,46 @@ +extends Node3D +class_name HeroRoster + +## Maintains a list of [Hero]s that are alive in the game. + +## Current heroes. +## +## NOTE: This array must not be mutated at runtime from outside this class! +@export var heroes: Array[Hero] = []: + set(value): + if value == heroes: + return + + self._unsubscribe_from_heroes() + heroes = value.duplicate() + self._subscribe_to_heroes() + +func _subscribe_to_heroes() -> void: + for hero in self.heroes: + hero.killed.connect(_on_hero_killed) + +func _unsubscribe_from_heroes() -> void: + for hero in self.heroes: + hero.killed.disconnect(_on_hero_killed) + +func _on_hero_killed(hero: Hero) -> void: + self.heroes.erase(hero) + +## Randomly picks a hero that is eligible to have a bounty, or returns null if none are available. +func pick_random_bounty() -> Hero: + var eligible := self.heroes.filter(func(hero: Hero) -> bool: return hero.bounty_target) + if not eligible: + return null + + return eligible.pick_random() + +## See [SaveGame]. +func save_to_dict() -> Dictionary: + var result := {} + result["heroes"] = SaveGame.serialize_array_of_resources(self.heroes) + return result + +## See [SaveGame]. +func load_from_dict(dict: Dictionary) -> void: + var heroes_array: Array = dict["heroes"] + self.heroes = SaveGame.deserialize_array_of_resources(heroes_array) diff --git a/actors/player.gd b/actors/player.gd index b77148c1..d4c42a26 100644 --- a/actors/player.gd +++ b/actors/player.gd @@ -12,6 +12,7 @@ class_name Player @export var bank_account: BankAccount @export var calendar: Calendar @export var mission_controller: MissionController +@export var hero_roster: HeroRoster @onready var ship := get_parent() as Ship @@ -44,10 +45,10 @@ var landing_target: PlanetInstance = null: set(value): if landing_target == value: return - + if landing_target: landing_target.targeted_by_player = false - + landing_target = value self.landing_target_changed.emit(self, landing_target) @@ -101,7 +102,7 @@ func _ready() -> void: if self.ship.hyperdrive: self._on_hyperdrive_changed() self.ship.hyperdrive.changed.connect(_on_hyperdrive_changed) - + self.mission_controller.calendar = self.calendar self.mission_controller.cargo_hold = self.ship.cargo_hold self.mission_controller.bank_account = self.bank_account @@ -166,7 +167,7 @@ func _closest_landing_target() -> PlanetInstance: if distance <= nearest_distance: nearest_planet_instance = planet_instance nearest_distance = distance - + return nearest_planet_instance func _unhandled_key_input(event: InputEvent) -> void: @@ -196,15 +197,15 @@ func _on_broadcasted_input_event(receiver: Node, event: InputEvent) -> void: var mouse_button_event := event as InputEventMouseButton if not mouse_button_event: return - + if mouse_button_event.button_index != MOUSE_BUTTON_LEFT or not mouse_button_event.pressed: return - + var combat_object := receiver as CombatObject if combat_object: self.ship.targeting_system.target = combat_object return - + var planet_instance := receiver as PlanetInstance if planet_instance: self.landing_target = planet_instance @@ -213,7 +214,7 @@ func _on_broadcasted_input_event(receiver: Node, event: InputEvent) -> void: func _jump_to_hyperspace() -> void: if not self.ship.hyperdrive_system.jump_destination: return - + if not self.hyperspace_scene_switcher.start_jump(): return @@ -227,7 +228,7 @@ func _land() -> void: self.landing_target = self._closest_landing_target() if not self.landing_target: return - + # Run the following checks only after a target is selected, to avoid spamming the message log. if self.landing_target.global_transform.origin.distance_to(self.ship.global_transform.origin) > MAX_LANDING_DISTANCE: self.message_log.add_message("Too far away to land.") @@ -254,7 +255,7 @@ func _land() -> void: landing.add_sibling(self.ship) landing.queue_free() self._depart_from_planet()) - + self.landed.emit(self, planet) func _depart_from_planet() -> void: diff --git a/galaxy/map/galaxy_map_window.tscn b/galaxy/map/galaxy_map_window.tscn index d53716a8..b2647e2c 100644 --- a/galaxy/map/galaxy_map_window.tscn +++ b/galaxy/map/galaxy_map_window.tscn @@ -60,7 +60,7 @@ own_world_3d = true handle_input_locally = false scaling_3d_scale = 2.0 physics_object_picking = true -size = Vector2i(793, 799) +size = Vector2i(2, 2) render_target_update_mode = 4 [node name="GalaxyMap3D" parent="PanelContainer/HSplitContainer/LeftContainer/SubViewportContainer/SubViewport" instance=ExtResource("1_rlhp4")] diff --git a/galaxy/star_system/scenes/barnard's_star.tscn b/galaxy/star_system/scenes/barnard's_star.tscn index 37cc32cf..1b0f27f2 100644 --- a/galaxy/star_system/scenes/barnard's_star.tscn +++ b/galaxy/star_system/scenes/barnard's_star.tscn @@ -1,18 +1,47 @@ -[gd_scene load_steps=6 format=3 uid="uid://cghdtnx2qen2u"] +[gd_scene load_steps=13 format=3 uid="uid://cghdtnx2qen2u"] [ext_resource type="Resource" uid="uid://shiglva7yxl0" path="res://galaxy/star_system/star_systems/barnard's_star.tres" id="2_y4tl8"] [ext_resource type="PackedScene" uid="uid://d27pdcik2lwf1" path="res://stars/main_sequence/star_class_m.tscn" id="3_cqasn"] +[ext_resource type="Resource" uid="uid://qlbnowlag2dr" path="res://actors/heroes/bounties/Zora 'Starshiv' Blackburn.tres" id="5_lj2ck"] [ext_resource type="PackedScene" uid="uid://ccdkamqw03rk7" path="res://fx/asteroids/multi_asteroid_field.tscn" id="5_x1i1i"] +[ext_resource type="Script" path="res://mechanics/combat/hull.gd" id="6_084xj"] +[ext_resource type="Script" path="res://mechanics/combat/shield.gd" id="7_tt2dl"] +[ext_resource type="Script" path="res://mechanics/power/battery.gd" id="8_qba8t"] [ext_resource type="PackedScene" uid="uid://culoat6jnbwc8" path="res://actors/ai/pirate_frigate.tscn" id="9_xdjbl"] [ext_resource type="PackedScene" uid="uid://fxemun7o6rix" path="res://galaxy/star_system/star_system_instance.tscn" id="star_system_instance"] +[sub_resource type="Resource" id="Resource_nv5hr"] +resource_local_to_scene = true +script = ExtResource("6_084xj") +max_integrity = 100.0 +integrity = 100.0 + +[sub_resource type="Resource" id="Resource_q3lvv"] +resource_local_to_scene = true +script = ExtResource("7_tt2dl") +max_integrity = 100.0 +integrity = 100.0 +recharge_rate = 10.0 +power_efficiency = 1.0 +only_recharge_above = 0.2 + +[sub_resource type="Resource" id="Resource_vl342"] +resource_local_to_scene = true +script = ExtResource("8_qba8t") +max_power = 300.0 +power = 300.0 + [node name="Barnard\'s Star" instance=ExtResource("star_system_instance")] star_system = ExtResource("2_y4tl8") [node name="Star Class M" parent="." index="0" instance=ExtResource("3_cqasn")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.65429, 2.08165e-12, 11.1402) -[node name="PirateFrigate" parent="." index="1" instance=ExtResource("9_xdjbl")] +[node name="Zora" parent="." index="1" instance=ExtResource("9_xdjbl")] transform = Transform3D(0.866897, -1.28496e-16, 0.498488, 3.48787e-16, 1, -3.48787e-16, -0.498488, 4.76228e-16, 0.866897, -8.39, 2.08165e-12, 1.15) +hero = ExtResource("5_lj2ck") +hull = SubResource("Resource_nv5hr") +shield = SubResource("Resource_q3lvv") +battery = SubResource("Resource_vl342") [node name="AsteroidField" parent="." index="2" instance=ExtResource("5_x1i1i")] diff --git a/galaxy/star_system/scenes/wolf_359.tscn b/galaxy/star_system/scenes/wolf_359.tscn index 5444ffd8..77e5e2b8 100644 --- a/galaxy/star_system/scenes/wolf_359.tscn +++ b/galaxy/star_system/scenes/wolf_359.tscn @@ -1,10 +1,56 @@ -[gd_scene load_steps=5 format=3 uid="uid://d3qpe4ne3bgww"] +[gd_scene load_steps=15 format=3 uid="uid://d3qpe4ne3bgww"] [ext_resource type="Resource" uid="uid://di0bekcy5g0ya" path="res://galaxy/star_system/star_systems/wolf_359.tres" id="2_byjpf"] [ext_resource type="PackedScene" uid="uid://d27pdcik2lwf1" path="res://stars/main_sequence/star_class_m.tscn" id="3_wjxxu"] [ext_resource type="PackedScene" uid="uid://culoat6jnbwc8" path="res://actors/ai/pirate_frigate.tscn" id="4_tean3"] +[ext_resource type="Script" path="res://mechanics/combat/hull.gd" id="5_1fi2w"] +[ext_resource type="Script" path="res://mechanics/combat/shield.gd" id="6_fwcxa"] +[ext_resource type="Script" path="res://mechanics/power/battery.gd" id="7_el5w1"] +[ext_resource type="Resource" uid="uid://dehs0xqbalevx" path="res://actors/heroes/bounties/Vex 'Quantum' Cygnus.tres" id="8_25dnf"] [ext_resource type="PackedScene" uid="uid://fxemun7o6rix" path="res://galaxy/star_system/star_system_instance.tscn" id="star_system_instance"] +[sub_resource type="Resource" id="Resource_p4pv3"] +resource_local_to_scene = true +script = ExtResource("5_1fi2w") +max_integrity = 100.0 +integrity = 100.0 + +[sub_resource type="Resource" id="Resource_yxo7w"] +resource_local_to_scene = true +script = ExtResource("6_fwcxa") +max_integrity = 100.0 +integrity = 100.0 +recharge_rate = 10.0 +power_efficiency = 1.0 +only_recharge_above = 0.2 + +[sub_resource type="Resource" id="Resource_0pc4o"] +resource_local_to_scene = true +script = ExtResource("7_el5w1") +max_power = 300.0 +power = 300.0 + +[sub_resource type="Resource" id="Resource_sq0sg"] +resource_local_to_scene = true +script = ExtResource("5_1fi2w") +max_integrity = 100.0 +integrity = 100.0 + +[sub_resource type="Resource" id="Resource_accbt"] +resource_local_to_scene = true +script = ExtResource("6_fwcxa") +max_integrity = 100.0 +integrity = 100.0 +recharge_rate = 10.0 +power_efficiency = 1.0 +only_recharge_above = 0.2 + +[sub_resource type="Resource" id="Resource_cq7l1"] +resource_local_to_scene = true +script = ExtResource("7_el5w1") +max_power = 300.0 +power = 300.0 + [node name="Wolf 359" instance=ExtResource("star_system_instance")] star_system = ExtResource("2_byjpf") @@ -13,6 +59,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.65186, 2.08165e-12, 2.2427 [node name="PirateFrigate" parent="." index="1" instance=ExtResource("4_tean3")] transform = Transform3D(-0.287762, -2.33666e-16, -0.957702, 3.48787e-16, 1, -3.48787e-16, 0.957702, -4.34402e-16, -0.287762, -7.88217, 0, -3.99797) +hull = SubResource("Resource_p4pv3") +shield = SubResource("Resource_yxo7w") +battery = SubResource("Resource_0pc4o") -[node name="PirateFrigate2" parent="." index="2" instance=ExtResource("4_tean3")] +[node name="Vex" parent="." index="2" instance=ExtResource("4_tean3")] transform = Transform3D(-0.69814, 4.9322e-16, 0.715961, 3.48787e-16, 1, -3.48787e-16, -0.715961, 6.21573e-18, -0.69814, 5.40376, 0, 0.771966) +hero = ExtResource("8_25dnf") +hull = SubResource("Resource_sq0sg") +shield = SubResource("Resource_accbt") +battery = SubResource("Resource_cq7l1") diff --git a/mechanics/missions/mission.gd b/mechanics/missions/mission.gd index 98797139..646b7fcf 100644 --- a/mechanics/missions/mission.gd +++ b/mechanics/missions/mission.gd @@ -49,7 +49,9 @@ enum Status { status = value self.emit_changed() -## A dictionary of [Commodity] keys to [int] amounts that the player must deliver to complete the mission. +## A dictionary of [Commodity] keys to [int] amounts that the player can deliver to complete the mission. +## +## If both this and [member assassination_target] are set, the player can choose which to complete the mission with. @export var cargo: Dictionary: set(value): if is_same(value, cargo): @@ -90,6 +92,17 @@ enum Status { starting_cost.make_read_only() self.emit_changed() +## A hero that the player can kill in combat in order to complete the mission. +## +## If both this and [member cargo] are set, the player can choose which to complete the mission with. +@export var assassination_target: Hero: + set(value): + if value == assassination_target: + return + + assassination_target = value + self.emit_changed() + static var _credits: Currency = preload("res://mechanics/economy/currencies/credits.tres") ## The amount to charge in starting cost, relative to the cost of goods, for a randomly generated mission. @@ -157,7 +170,7 @@ static func _randomly_walk_systems(galaxy: Galaxy, path_so_far: Array[StarSystem return not path_so_far.any(func(system: StarSystem) -> bool: return system.name == connection )) - + if allowed_connections.is_empty(): return [] @@ -179,7 +192,7 @@ static func _randomly_walk_systems(galaxy: Galaxy, path_so_far: Array[StarSystem if new_path.size() <= 1: # Back to the starting point, so give up. return [] - + return new_path ## Creates a random rush delivery mission. @@ -197,7 +210,7 @@ static func create_rush_delivery_mission(origin_planet: Planet, calendar: Calend mission.deadline_cycle = calendar.get_current_cycle() for i in path.size() - 1: mission.deadline_cycle += HyperspaceSceneSwitcher.HYPERSPACE_APPROXIMATE_TRAVEL_DAYS * 24 * randf_range(_RUSH_DELIVERY_MIN_DEADLINE_BUFFER, _RUSH_DELIVERY_MAX_DEADLINE_BUFFER) - + var destination_system: StarSystem = path[-1] assert(destination_system != origin_system, "Cannot create rush delivery to the system we started in") @@ -235,18 +248,64 @@ static func create_rush_delivery_mission(origin_planet: Planet, calendar: Calend return mission +const _BOUNTY_MIN_CREDITS_REWARD = 15000 +const _BOUNTY_MAX_CREDITS_REWARD = 40000 + +## Creates a random bounty mission. +## +## Note: this may not succeed every time, so ensure that the return value is checked. +static func create_bounty_mission(hero_roster: HeroRoster) -> Mission: + var mission := Mission.new() + + mission.assassination_target = hero_roster.pick_random_bounty() + if not mission.assassination_target: + return null + + mission.title = "Bounty on %s" % mission.assassination_target.name + mission.description = "%s has been raiding trading vessels in the area. A local trade union has scraped together a reward for whoever can put an end to their piracy." % mission.assassination_target.name + + var reward_credits := randi_range(_BOUNTY_MIN_CREDITS_REWARD, _BOUNTY_MAX_CREDITS_REWARD) + + mission.monetary_reward = { + _credits: reward_credits + } + + return mission + ## Creates a random mission of any type. ## ## Note: this may not succeed every time, so ensure that the return value is checked. -static func create_random_mission(origin_planet: Planet, calendar: Calendar) -> Mission: +static func create_random_mission(origin_planet: Planet, calendar: Calendar, hero_roster: HeroRoster) -> Mission: var generators := [ - func() -> Mission: return Mission.create_delivery_mission(origin_planet), + func() -> Mission: return Mission.create_bounty_mission(hero_roster), func() -> Mission: return Mission.create_rush_delivery_mission(origin_planet, calendar), ] + # Lazy way of weighting the random generation + for i in range(2): + generators.append( + func() -> Mission: return Mission.create_delivery_mission(origin_planet), + ) + var generator: Callable = generators.pick_random() return generator.call() +## Filters out any missions from [param proposed_missions] that are incompatible with [param current_missions] or one of the other proposed missions. +static func filter_incompatible_missions(current_missions: Array[Mission], proposed_missions: Array[Mission]) -> Array[Mission]: + var bounties: Array[Hero] = [] + for mission in current_missions: + if mission.assassination_target: + bounties.append(mission.assassination_target) + + var filtered_missions: Array[Mission] = [] + for mission in proposed_missions: + if mission.assassination_target and bounties.has(mission.assassination_target): + continue + + filtered_missions.append(mission) + + return filtered_missions + # Overridden because dictionaries of resources do not serialize correctly. func save_to_dict() -> Dictionary: var result := {} @@ -255,9 +314,13 @@ func save_to_dict() -> Dictionary: if is_finite(self.deadline_cycle): result["deadline_cycle"] = self.deadline_cycle - + result["status"] = self.status - result["destination_planet"] = self.destination_planet.resource_path + + if self.destination_planet: + result["destination_planet"] = self.destination_planet.resource_path + if self.assassination_target: + result["assassination_target"] = self.assassination_target.resource_path result["cargo"] = SaveGame.serialize_dictionary_with_resource_keys(self.cargo) result["monetary_reward"] = SaveGame.serialize_dictionary_with_resource_keys(self.monetary_reward) @@ -271,8 +334,13 @@ func load_from_dict(dict: Dictionary) -> void: self.deadline_cycle = dict["deadline_cycle"] if "deadline_cycle" in dict else INF self.status = dict["status"] - var destination_planet_path: String = dict["destination_planet"] - self.destination_planet = ResourceUtils.safe_load_resource(destination_planet_path, "tres") + if "destination_planet" in dict: + var path: String = dict["destination_planet"] + self.destination_planet = ResourceUtils.safe_load_resource(path, "tres") + + if "assassination_target" in dict: + var path: String = dict["assassination_target"] + self.assassination_target = ResourceUtils.safe_load_resource(path, "tres") var saved_cargo: Dictionary = dict["cargo"] self.cargo = SaveGame.deserialize_dictionary_with_resource_keys(saved_cargo) @@ -282,5 +350,5 @@ func load_from_dict(dict: Dictionary) -> void: var saved_cost: Dictionary = dict["starting_cost"] self.starting_cost = SaveGame.deserialize_dictionary_with_resource_keys(saved_cost) - + self.emit_changed() diff --git a/mechanics/missions/mission_controller.gd b/mechanics/missions/mission_controller.gd index c4c5bc82..43913942 100644 --- a/mechanics/missions/mission_controller.gd +++ b/mechanics/missions/mission_controller.gd @@ -11,7 +11,7 @@ var calendar: Calendar: set(value): if value == calendar: return - + if calendar: calendar.changed.disconnect(_check_all_missions_failure) calendar = value @@ -23,7 +23,7 @@ var cargo_hold: CargoHold: set(value): if value == cargo_hold: return - + if cargo_hold: cargo_hold.changed.disconnect(_check_all_missions_failure) cargo_hold = value @@ -58,7 +58,7 @@ func start_mission(mission: Mission) -> bool: var required_amount: float = mission.starting_cost[trade_asset] if trade_asset.current_amount(self.cargo_hold, self.bank_account) < required_amount: return false - + var required_volume: float = 0.0 for commodity: Commodity in mission.cargo: required_volume += mission.cargo[commodity] * commodity.volume @@ -71,7 +71,7 @@ func start_mission(mission: Mission) -> bool: var required_amount: float = mission.starting_cost[trade_asset] var result := trade_asset.take_exactly(required_amount, self.cargo_hold, self.bank_account) assert(result, "Withdrawing mission starting cost should succeed after previous check") - + for commodity: Commodity in mission.cargo: var amount: int = mission.cargo[commodity] var result := self.cargo_hold.add_exactly(commodity, amount) @@ -79,6 +79,9 @@ func start_mission(mission: Mission) -> bool: self._missions.push_back(mission) mission.status = Mission.Status.STARTED + if mission.assassination_target: + mission.assassination_target.killed.connect(_on_assassination_target_killed) + self.mission_started.emit(mission) return true @@ -91,10 +94,12 @@ func _fail_mission(mission: Mission, failure_status: Mission.Status = Mission.St assert(mission in self._missions, "Cannot fail a non-current mission") mission.status = failure_status + if mission.assassination_target: + mission.assassination_target.killed.disconnect(_on_assassination_target_killed) # Ordering is important: do this before modifying cargo, so it doesn't participate in the update notification. self._missions.erase(mission) - + # TODO: Don't remove cargo from a forfeited mission(?), maybe the player wants to sell it on. But check this makes sense economically. for commodity: Commodity in mission.cargo: var amount: int = mission.cargo[commodity] @@ -112,6 +117,8 @@ func _succeed_mission(mission: Mission) -> void: assert(mission in self._missions, "Cannot succeed a non-current mission") mission.status = Mission.Status.SUCCEEDED + if mission.assassination_target: + mission.assassination_target.killed.disconnect(_on_assassination_target_killed) # Ordering is important: do this before modifying cargo, so it doesn't participate in the update notification. self._missions.erase(mission) @@ -153,13 +160,18 @@ func _on_player_landed(_player: Player, planet: Planet) -> void: for mission: Mission in self._missions.duplicate(): if mission.destination_planet != planet: continue - + self._check_mission_failure(mission) if mission.status == Mission.Status.FAILED: continue - + self._succeed_mission(mission) +func _on_assassination_target_killed(hero: Hero) -> void: + for mission: Mission in self._missions.duplicate(): + if mission.assassination_target == hero: + self._succeed_mission(mission) + ## See [SaveGame]. func save_to_dict() -> Dictionary: var result := {} @@ -178,3 +190,6 @@ func load_from_dict(dict: Dictionary) -> void: ) self._missions.assign(loaded_missions) + for mission: Mission in loaded_missions: + if mission.assassination_target: + mission.assassination_target.killed.connect(_on_assassination_target_killed) diff --git a/planet/planet_instance.gd b/planet/planet_instance.gd index 8eb9f56b..57969051 100644 --- a/planet/planet_instance.gd +++ b/planet/planet_instance.gd @@ -21,10 +21,11 @@ var targeted_by_player: bool: var _available_missions: Array[Mission] = [] ## Creates or returns the missions currently available to pick up from this planet. -func get_available_missions(calendar: Calendar) -> Array[Mission]: +func get_available_missions(calendar: Calendar, hero_roster: HeroRoster) -> Array[Mission]: if not self._available_missions: - for i in randi_range(3, 5): - var mission := Mission.create_random_mission(self.planet, calendar) + var desired_count := randi_range(3, 5) + while self._available_missions.size() < desired_count: + var mission := Mission.create_random_mission(self.planet, calendar, hero_roster) if mission: self._available_missions.push_back(mission) diff --git a/screens/game/game.tscn b/screens/game/game.tscn index 099d53a6..53b3a337 100644 --- a/screens/game/game.tscn +++ b/screens/game/game.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=71 format=3 uid="uid://sunhu71swcs2"] +[gd_scene load_steps=74 format=3 uid="uid://sunhu71swcs2"] [ext_resource type="Environment" uid="uid://b7t3opwr35xen" path="res://fx/environment.tres" id="1_pqled"] [ext_resource type="Script" path="res://mechanics/hyperspace/hyperspace_scene_switcher.gd" id="4_yyrkm"] @@ -16,11 +16,14 @@ [ext_resource type="Script" path="res://mechanics/combat/hull.gd" id="14_bcf1o"] [ext_resource type="Script" path="res://mechanics/power/battery.gd" id="15_sjw8q"] [ext_resource type="Script" path="res://mechanics/hyperspace/hyperdrive_system.gd" id="16_6olqp"] +[ext_resource type="Script" path="res://actors/heroes/hero_roster.gd" id="16_j7kx1"] [ext_resource type="PackedScene" uid="uid://cxlg0yj8cjbrf" path="res://actors/player.tscn" id="16_lqltb"] [ext_resource type="PackedScene" uid="uid://bksd1ida60ymi" path="res://galaxy/map/galaxy_map_window.tscn" id="17_4w17v"] [ext_resource type="PackedScene" uid="uid://2mdsbbko7baw" path="res://galaxy/star_system/scenes/sol.tscn" id="17_wjgyf"] [ext_resource type="PackedScene" uid="uid://b6tdj2tjqldnt" path="res://screens/game/game_over/game_over.tscn" id="17_yidh7"] +[ext_resource type="Resource" uid="uid://dehs0xqbalevx" path="res://actors/heroes/bounties/Vex 'Quantum' Cygnus.tres" id="17_yop8u"] [ext_resource type="PackedScene" uid="uid://ca3awvk1a7xyi" path="res://screens/game/exit_dialog/exit_dialog.tscn" id="18_hl4w3"] +[ext_resource type="Resource" uid="uid://qlbnowlag2dr" path="res://actors/heroes/bounties/Zora 'Starshiv' Blackburn.tres" id="18_wo2tf"] [ext_resource type="Script" path="res://screens/game/in_game_gui.gd" id="18_y74ml"] [ext_resource type="Texture2D" uid="uid://bphfuhdpceacb" path="res://mechanics/radar/images/radar_bg.png" id="19_vr47x"] [ext_resource type="Shader" path="res://screens/shared_ui/shaders/circular_mask.gdshader" id="20_wa4c1"] @@ -99,7 +102,7 @@ script = ExtResource("11_ag0i7") max_volume = 10.0 commodities = {} -[sub_resource type="Resource" id="Resource_82t4m"] +[sub_resource type="Resource" id="Resource_0rcb3"] resource_local_to_scene = true script = ExtResource("13_q2g24") max_fuel = 6.0 @@ -180,7 +183,7 @@ hull = SubResource("Resource_ffax2") shield = SubResource("Resource_75c8j") battery = SubResource("Resource_xpwk4") cargo_hold = SubResource("Resource_bsv1l") -hyperdrive = SubResource("Resource_82t4m") +hyperdrive = SubResource("Resource_0rcb3") [node name="CombatObject" parent="HyperspaceSceneSwitcher/Sol/PlayerCorvette" index="3" node_paths=PackedStringArray("targeted_sound")] targeted_sound = NodePath("../Player/TargetedSound") @@ -188,11 +191,12 @@ targeted_sound = NodePath("../Player/TargetedSound") [node name="HyperdriveSystem" type="Node3D" parent="HyperspaceSceneSwitcher/Sol/PlayerCorvette"] script = ExtResource("16_6olqp") -[node name="Player" parent="HyperspaceSceneSwitcher/Sol/PlayerCorvette" node_paths=PackedStringArray("hyperspace_scene_switcher", "message_log") groups=["saveable"] instance=ExtResource("16_lqltb")] +[node name="Player" parent="HyperspaceSceneSwitcher/Sol/PlayerCorvette" node_paths=PackedStringArray("hyperspace_scene_switcher", "message_log", "hero_roster") groups=["saveable"] instance=ExtResource("16_lqltb")] hyperspace_scene_switcher = NodePath("../../..") message_log = NodePath("../../../../InGameGUI/MarginContainer/HBoxContainer/MessageLog") bank_account = SubResource("Resource_j1y8s") calendar = SubResource("Resource_jc1pg") +hero_roster = NodePath("../../../../HeroRoster") [node name="MinimapCameraRemoteTransform" parent="HyperspaceSceneSwitcher/Sol/PlayerCorvette/Player" index="0"] remote_path = NodePath("../../../../../MinimapViewport/MinimapCameraTransform") @@ -218,6 +222,10 @@ script = ExtResource("10_w7kut") [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = ExtResource("1_pqled") +[node name="HeroRoster" type="Node3D" parent="." groups=["saveable"]] +script = ExtResource("16_j7kx1") +heroes = Array[Resource("res://actors/heroes/hero.gd")]([ExtResource("17_yop8u"), ExtResource("18_wo2tf")]) + [node name="StarfieldTransform" type="Node3D" parent="."] [node name="Starfield" type="MeshInstance3D" parent="StarfieldTransform"] diff --git a/screens/landing/landing.gd b/screens/landing/landing.gd index c38816b8..64d0bfda 100644 --- a/screens/landing/landing.gd +++ b/screens/landing/landing.gd @@ -66,7 +66,9 @@ func _on_trading_button_pressed() -> void: func _on_missions_button_pressed() -> void: if not self._missions_window: self._missions_window = self.missions_window_scene.instantiate() - self._missions_window.available_missions = self.planet_instance.get_available_missions(self.player.calendar) + self._missions_window.available_missions = Mission.filter_incompatible_missions( + self.player.mission_controller.get_current_missions(), + self.planet_instance.get_available_missions(self.player.calendar, self.player.hero_roster)) self._missions_window.mission_controller = self.player.mission_controller self._missions_window.cargo_hold = self.player.ship.cargo_hold self._missions_window.bank_account = self.player.bank_account @@ -108,7 +110,7 @@ func _on_refuel_button_pressed() -> void: var paid := self.star_system.refueling_money.take_up_to(full_refuel_cost, self.player.ship.cargo_hold, self.player.bank_account) var fuel_paid_for := paid / refueling_price self._hyperdrive.refuel(fuel_paid_for) - + self._update_refuel_button() func _on_depart() -> void: diff --git a/script/claude.py b/script/claude.py index 41d9c13f..b2fae34d 100755 --- a/script/claude.py +++ b/script/claude.py @@ -121,7 +121,7 @@ def sample( "input_schema": { "type": "object", "properties": { - "path": {"type": "string", "description": "File path to write to"}, + "path": {"type": "string", "description": "The relative filesystem path to write to"}, "content": { "type": "string", "description": "The full, new content for the file.", diff --git a/ships/ship.gd b/ships/ship.gd index 8fd33b3f..2ebaa5d3 100644 --- a/ships/ship.gd +++ b/ships/ship.gd @@ -3,6 +3,9 @@ class_name Ship # NODES +## The NPC hero piloting the ship, if any. +@export var hero: Hero + ## The [CombatObject] representing this ship. @export var combat_object: CombatObject @@ -60,6 +63,11 @@ var save_node_path_override: NodePath func _ready() -> void: self.combat_object.hull = self.hull self.combat_object.shield = self.shield + if self.hero: + self.combat_object.combat_name = self.hero.name + self.hull.hull_destroyed.connect(func(_hull: Hull) -> void: + self.hero.killed.emit(self.hero)) + self.rigid_body_thruster.battery = self.battery self.rigid_body_direction.battery = self.battery self.power_management_unit.battery = self.battery diff --git a/utils/save_game.gd b/utils/save_game.gd index 0a10d9a0..0c620f86 100644 --- a/utils/save_game.gd +++ b/utils/save_game.gd @@ -237,7 +237,7 @@ func serialize_dictionary_with_resource_keys(dict: Dictionary) -> Dictionary: var result := {} for resource: Resource in dict: result[resource.resource_path] = dict[resource] - + return result func deserialize_dictionary_with_resource_keys(dict: Dictionary) -> Dictionary: @@ -245,5 +245,13 @@ func deserialize_dictionary_with_resource_keys(dict: Dictionary) -> Dictionary: for resource_path: String in dict: var resource := ResourceUtils.safe_load_resource(resource_path, "tres") result[resource] = dict[resource_path] - + return result + +func serialize_array_of_resources(array: Array) -> Array: + return array.map(func(resource: Resource) -> String: + return resource.resource_path) + +func deserialize_array_of_resources(array: Array) -> Array: + return array.map(func(resource_path: String) -> Resource: + return ResourceUtils.safe_load_resource(resource_path, "tres"))