diff --git a/docs/engine/camera.md b/docs/engine/camera.md index fc8f68ee..93113e8d 100644 --- a/docs/engine/camera.md +++ b/docs/engine/camera.md @@ -195,8 +195,8 @@ launch tube the ship lands in, which the landing keeps, turned as the carrier is The display names view `0x23`, but not `0x26`. **Improvement:** from within the tube, the sound is heard as in a hangar ([Sound](../port/sound.md)). -**Improvement:** a landing in a hangar bay ([Landing](orders.md#landing)) picks one of the same -two views, which stand by the bay instead, in the frame of the launch point of the gate the ship +**Improvement:** a landing in a hangar bay or in the Badanov's bay ([Landing](orders.md#landing)) +picks one of the same two views, which stand by the bay instead, in the frame of the place the ship lands at. They go by the ship and the bay rather than by fixed distances, so that they frame a landing on any carrier. View `0x23` stands in the bay off the launch point, by the landing ship's radius: one radius to the right, a third of one above and one and three quarters out towards the diff --git a/docs/engine/launch.md b/docs/engine/launch.md index 13137026..30ab64ec 100644 --- a/docs/engine/launch.md +++ b/docs/engine/launch.md @@ -129,8 +129,9 @@ fire](orders.md#friendly-fire)) go by it. **Improvement:** the game records the player's carrier only in the Reliant's and the Yamato's launches, the only ones the original's missions give the player. OpenReliant records it as any -launch of the player's ship starts (`launch.init`), so that PERMISSION TO LAND and the bridge's -lines work from any carrier a mission launches the player from, such as a mod's. +launch of the player's ship starts (`launch.init`), so that the bridge's lines work from any +carrier a mission launches the player from, such as a mod's. PERMISSION TO LAND lands the ship +only where that carrier offers a landing ([Landing](orders.md#landing)). The hangar (`reliant_hang.shp`, type `0xD6`) is made in the cutaway slot, colliding with nothing, its hull and its two doors reached by no light of the backdrop's but the first ambient (light mask @@ -315,6 +316,10 @@ From step 2 (`launch_badanov_run`, `0x0041A100`): **Fix:** if the model lacks the bay or its level, OpenReliant leaves the ship in place, riding the carrier's root. The original dereferences the missing part. +A landing in the bay, where a mod or a mission's script lets the player land on the Badanov or the +Krasny, comes to rest where a ship waits at the gate, through doors 1 and 2 +([Landing](orders.md#landing)). + ## The escape pods Both escape pods use `launch_point_init` (`0x0041A4B0`) to wait at the launch point selected diff --git a/docs/engine/orders.md b/docs/engine/orders.md index 1be54019..c09f348a 100644 --- a/docs/engine/orders.md +++ b/docs/engine/orders.md @@ -758,14 +758,30 @@ of a door closing (`0x36`, `doorclos`). PERMISSION TO LAND does nothing where the player's ship launched from no carrier, where the game reads through a null pointer. -**Improvement:** the player's ship lands in a hangar bay of any other carrier that ships launch -from as one ([Hangar bays](launch.md#hangar-bays)), the Victorious or a mod's carrier among them, -so that PERMISSION TO LAND ends a mod's mission on the carrier the mission launched the player -from. The landing aims at a gate: the component of its order's target, which a mission's `SetAI` +**Improvement:** the game lands only on the Reliant and the Yamato, by their types. OpenReliant +lands on a ship that offers a landing (`ailand.Style`): `reliant`, `yamato`, `bay` or `badanov`. +Each ship type's record says which one it offers (`ShipTypeRecord.landing`). The Reliant and the +Yamato offer theirs, as in the game, and no other type offers one. A mod's ship type can give its +own ([Ship types](../guide/modding.md#ship-types)), and a mission's script can change it for a +single ship ([Landing on another carrier](../guide/missions.md#landing-on-another-carrier)). +This holds for every Land order, whether PERMISSION TO LAND, a mission's `SetAI` or the friendly +fire's sending home gives it. A Land order aimed at a ship that offers none ends at once, and so +does one aimed at a ship whose model lacks what its landing needs: the Reliant's first launch tube, +a bay's launch point for the gate, or the Badanov's bay. PERMISSION TO LAND asks nothing where the +player's carrier offers none, and the friendly fire's sending home lets the ship fly on, as where +there is no carrier. A script can make another ship the player's carrier +(`world.set_player_carrier`), and answer PERMISSION TO LAND its own way (the `permission_to_land` +hook). The game's missions launch the player only from the Reliant and the Yamato, so they land as +they do in the game. + +**Improvement:** a ship that offers a bay's landing (`bay`) takes the player's ship into a hangar +bay that ships launch from ([Hangar bays](launch.md#hangar-bays)), such as the Victorious or a +mod's carrier, so that PERMISSION TO LAND can end a mod's mission there. The landing aims at a +gate: the component of its order's target, which a mission's `SetAI` names with `component(carrier, gate)`, or gate 0 when the order aims at the carrier as a whole, as the player's request does. Aimed at the whole carrier, the landing is due 700 ticks on, as the Reliant's is; aimed at a gate, it begins at once, so that a mission's script lands the ship at its -own moment. A carrier whose model has no launch point for the gate is one that nothing lands on. +own moment. A ship other than the player's lets the landing go at once, as on the Yamato. The cutaway is the Reliant's: one of its two views, picked at random, standing by the bay @@ -779,6 +795,11 @@ point, upright, its throttle 0.0001 for each unit it has to go, at most the thro at the Yamato's bay speed of 100, and stops within 200 of the point, sounding its landing. The doors close behind it, and 270 ticks later the mission is over. +**Improvement:** the Badanov's landing (`badanov`) is the same in the Badanov's or the Krasny's bay +([The Badanov and the Krasny](launch.md#the-badanov-and-the-krasny)). The ship comes to rest where +the launch sets a ship at the gate, and the bay's doors, parts 1 and 2, open and close as a hangar +bay's do. + The Yamato's style lands the ship in the Yamato's landing bay, ship type `0xD5` (Jap Yam Landing Bay, `yamhanger.shp`), whose part 6 is the hangar and part 1 the pad. Its init (`land_yamato_init`, `0x0040EB60`) sets the first step, due in 700 ticks, and for the player's ship @@ -809,7 +830,7 @@ leaves the ship turned as it was. **Improvement:** the angles come from `std.math.atan2` rather than the engine's table (`sr_atan2`). OpenReliant has no parent frames, so the ship on the pad is put back on it each frame -(`ailand.hold`), as the launches hold their ships ([Launches](launch.md)). +(`ailand.yamato.hold`), as the launches hold their ships ([Launches](launch.md)). ### Friendly fire diff --git a/docs/engine/radio.md b/docs/engine/radio.md index dec9ec7d..e988f25b 100644 --- a/docs/engine/radio.md +++ b/docs/engine/radio.md @@ -177,6 +177,11 @@ answer 300 ticks on: from `_lnd_017` to `_lnd_024` for any other; refusing it, from `_lnd_den_01` to `_lnd_den_04`. It also writes "DEBUG: Mission is flagged as a" and the rating's name into a line nothing shows. +**Improvement:** outside training, OpenReliant asks only where the player's carrier offers a +landing, and asks nothing otherwise ([Landing](orders.md#landing)). A ship type can't name a bridge +officer of its own yet, so any carrier other than a Yamato answers with the Reliant's officer, a +mod's carrier too ([#1340](https://github.com/OpenReliant/openreliant/issues/1340)). + ## The wingmen's commands ATTACK MY TARGET (`0x00454C20`), BACK OFF (`0x00454FA0`) and HELP ME (`0x00455330`) go to the diff --git a/docs/guide/missions.md b/docs/guide/missions.md index ef19a5f3..cdd12c3f 100644 --- a/docs/guide/missions.md +++ b/docs/guide/missions.md @@ -413,9 +413,36 @@ The objectives' names come from the game's table, which has a row for each of th - `TerminateMission()` ends it too. In the missions numbered below 28, the game then counts the player's ship as destroyed. - `game.landing_cleared = 1` lets the player land on the carrier with PERMISSION TO LAND, which ends - the mission. + the mission. The carrier is the ship the player launched from, and it has to offer a landing: the + Reliant and the Yamato do, as in the game, and any other ship only where a mod or a script gives + it one ([Landing on another carrier](#landing-on-another-carrier)). - The mission also ends once the player's ship is destroyed. +### Landing on another carrier + +A mission that launches the player from another carrier, such as the Victorious or the Badanov, can +let the player land there with a script of its own. A mod lists it under `[Missions]` beside the +mission ([Scripting](scripting.md)), and the script gives the carrier a landing as it joins the +mission: + +```lua +return { + engine_handlers = { + on_object_added = function(object) + if object.type == "victorious" then object.landing = "bay" end + end, + }, +} +``` + +- `landing` takes the values of a ship type's `Landing` ([Ship types](modding.md#ship-types)), such + as `bay` for a hangar bay. Nil keeps the player from landing on the ship, even on the Reliant. +- `world.set_player_carrier(ship)` makes another ship the one PERMISSION TO LAND asks, such as a + carrier that jumps in later. Until then, it is the ship the player launched from, if any. +- A mod's own carrier type gives its landing in its section ([Ship types](modding.md#ship-types)), + and a load script can give every ship of one of the game's types a landing: + `records.ship_types.victorious.landing = "bay"`. + ## Checking a mission `openreliant missions` lists the missions, the mods' among them, and checks that each loads: diff --git a/docs/guide/modding.md b/docs/guide/modding.md index 7c5360df..9794e21d 100644 --- a/docs/guide/modding.md +++ b/docs/guide/modding.md @@ -965,12 +965,31 @@ its own launches them as a hangar bay: each ship waits at the launch point of it `launch_point` object of the model ([Models from OBJ](#models-from-obj)), and flies out once its launch starts. -PERMISSION TO LAND brings the player's ship back into such a carrier the same way, once the -mission's script has cleared it to land (`game.landing_cleared = 1`): the carrier's bridge answers, -and the ship flies in at the doors of gate 0 and down to its launch point, which ends the mission. -A mission can land the ship at another gate, and at a moment of its own, with +The player's ship lands only on a ship that offers a landing. A type's `Landing` says how the +player's ship lands on it: + +- `bay`: the ship flies in at the doors of a gate and down to its launch point, as into a hangar + bay. The model needs `launch_point` objects ([Models from OBJ](#models-from-obj)). +- `reliant`: the Reliant's landing, down its first launch tube, which needs the Reliant's model. +- `yamato`: the Yamato's landing, onto a pad of the Yamato's landing bay. +- `badanov`: the Badanov's landing, into the bay of the Badanov's or the Krasny's model. +- `none`: no landing. + +Where the model lacks what its landing needs, nothing lands on it, and the log says so. + +Without the key, a type that keeps its base's model offers its base's landing, so a type based on +the Reliant lands the player as the Reliant does. A type with a model of its own offers none. + +Where the player's carrier offers a landing, PERMISSION TO LAND brings the player's ship back into +it once the mission's script has cleared it to land (`game.landing_cleared = 1`): the carrier's +bridge answers, and the ship lands, which ends the mission. The Yamato's bridge officer answers for +a Yamato and the Reliant's for any other carrier, a mod's too +([#1340](https://github.com/OpenReliant/openreliant/issues/1340)). In a bay, the ship flies in at +gate 0. A mission can land the ship at another gate, and at a moment of its own, with `SetAI(player, "land", 1, component(carrier, gate))` -([Landing](../engine/orders.md#landing)). +([Landing](../engine/orders.md#landing)). A mission can also let the player land on a ship that +offers no landing, such as the Victorious, or keep the player off one that does +([Landing on another carrier](missions.md#landing-on-another-carrier)). #### Ship types without a base diff --git a/docs/guide/openreliant.d.luau b/docs/guide/openreliant.d.luau index f03f41ae..35449342 100644 --- a/docs/guide/openreliant.d.luau +++ b/docs/guide/openreliant.d.luau @@ -12,6 +12,7 @@ export type PilotNumber = "bandit_tigers_leader" | "diceman_tigers_leader" | "mo export type Side = "friendly" | "hostile" | "neutral" | number export type Maneuver = "defend_dodge1" | "defend_dodge2" | "defend_dodge3" | "out_of_action_sphere" | "defend_runaway" | "attack_pursue" | "attack_massive_object" | "attack_medium_fighter" | "loop_the_loop" | "run_to_ship" | number export type Invulnerability = "none" | "player_can_hit" | "full" | "eject_before_exploding" | number +export type Landing = "yamato" | "reliant" | "bay" | "badanov" export type PartClass = "hull" | "cockpit" | "turret" | "engine" | "shield_generator" | "comms_transmitter" | "gravity_drive" | "laser_turret" | "missile_turret" | "power_core" | "satellite_dish" | "service_door" | "shaft" | "surface_building" | "twin_power_cores" | "vent_hatch" | "ion_cannon" | "armored_plate" | "cap_gun" | "warp_projector" | "fuel_pod" | number export type AttachmentKind = "missile" | "gun" | "engine_glow" | "gun_muzzle" | "light" | "pod" | "eject_point" | "case_ejector" | "launch_point" | "dock_point" | number export type MissileType = "none" | "screamer" | "raptor" | "havoc" | "jack_hammer" | "bandit" | "vagabond" | "solomon" | "imp" | "hawk" | "torpedo" | "fuel_pod" | number | string @@ -479,6 +480,8 @@ declare extern type Object with targetable: boolean --- Whether its lights are on. Setting it is what a mission's DisableLights does. lights: boolean + --- How the player's ship lands on it, as its type's record has it when it's made (`ShipTypeRecord.landing`); nil where the player can't land on it. Setting it lets the player land on this ship, or keeps the player off it, for the rest of the mission. PERMISSION TO LAND and a mission's land order only bring the player's ship down on a ship that offers a landing. + landing: Landing? --- Whether it's left out of the mission's work, as a mission's DisableObject leaves it. disabled: boolean --- Whether its guns don't fire and its turrets rest, as a mission's DisableGuns sets. @@ -587,6 +590,7 @@ declare extern type ShipTypeRecord with class: ShipClass side: Side display: TargetForm + landing: Landing? end declare extern type Gun with name: string @@ -763,6 +767,8 @@ export type World = { player: Object?, --- The mission that runs, with its `number` and its `file`'s name; nil between missions. mission: Mission?, + --- The player's carrier: the ship the player launched from, unless a script sets another (`set_player_carrier`). PERMISSION TO LAND asks it, and its bridge speaks to the player. Nil where there is none, as in a mission the player starts in flight, and between missions. + player_carrier: Object?, --- Every object in the mission, in the order of their slots. objects: () -> { Object }, --- Every missile in flight, newest first. @@ -777,6 +783,8 @@ export type World = { variable: (name: GameVariable) -> number?, --- Sets the game's variable `name` to `value` in the mission that runs, as the mission's script sets it. The campaign keeps the value from one mission to the next where it keeps the variable. Returns false between missions. set_variable: (name: GameVariable, value: number) -> boolean, + --- Makes `ship` the player's carrier (`player_carrier`): PERMISSION TO LAND asks it, its bridge answers, and the player's ship lands on it where it offers a landing (its `landing`). Nil leaves the player without a carrier. Returns false between missions. + set_player_carrier: (ship: Object?) -> boolean, } --- The objects around the script's own object or missile. @@ -1189,6 +1197,7 @@ export type Interfaces = { Missions: { player: Object?, mission: Mission?, + player_carrier: Object?, objects: () -> { Object }, missiles: () -> { Missile }, set_player_target: (target: Object, component: number?) -> boolean, @@ -1196,6 +1205,7 @@ export type Interfaces = { set_objective: (objective: number, state: ObjectiveState) -> boolean, variable: (name: GameVariable) -> number?, set_variable: (name: GameVariable, value: number) -> boolean, + set_player_carrier: (ship: Object?) -> boolean, }, Campaign: { mission: Mission?, @@ -1315,6 +1325,12 @@ declare extern type RadioSay with function original(self): () end +--- The player asks for PERMISSION TO LAND, with its key or the radio's menu, heard at most once in 500 ticks. The pilot asks, and the flight instructor answers in training, or else the player's carrier, where it offers a landing (`world.player_carrier`, the object's `landing`). Once the mission's script has set `landing_cleared`, the answer clears the ship, which lands; otherwise the carrier refuses. A handler that stops it can answer and land the ship its own way, such as with an order of its own. +declare extern type PermissionToLand with + --- Runs the rest of the call now: the handlers after this one, then the function. + function original(self): () +end + --- `launcher` launches a missile from one of its racks at `target`. declare extern type MissileLaunch with launcher: Object @@ -1616,10 +1632,10 @@ declare extern type TriggerFired with end --- The hooks, which `hooks.add` takes. -export type HookName = "object_damage" | "object_armor_damage" | "component_damage" | "damage_by_difficulty" | "object_destroyed" | "bullet_fire" | "radio_say" | "missile_launch" | "missile_launch_turret" | "order_push" | "order_pop" | "fight_choose_maneuver" | "maneuver_run" | "object_orders" | "vm_command" | "restart_screen" | "order_retaliate" | "order_do_nothing" | "order_fly_aimlessly_init" | "order_fly_aimlessly" | "order_launch_missile" | "order_launch_jack_hammer" | "order_warp_in_init" | "order_warp_in" | "order_warp_out_init" | "order_warp_out" | "order_fly_init" | "order_fly" | "order_run_away" | "order_land_init" | "order_land" | "order_escort_init" | "order_escort" | "order_find_new_target" | "order_explode_init" | "order_explode" | "order_ripper_grabs_target_object_init" | "order_ripper_grabs_target_object" | "order_ripper_grabs_target_object_exit" | "order_object_attach_init" | "order_object_attach" | "order_formation_regroup_init" | "order_formation_regroup" | "order_patrol_route_init" | "order_patrol_route" | "order_toggle_cloak" | "order_ship_follow_curve_init" | "order_ship_follow_curve" | "order_ship_follow_curve_exit" | "order_slow_rotate" | "order_jump_in_init" | "order_jump_in" | "order_jump_out_init" | "order_jump_out" | "order_find_scoop_up" | "order_random_spin_slow_init" | "order_random_spin_medium_init" | "order_random_spin_fast_init" | "order_fixed_gate_jump_in_init" | "order_fixed_gate_jump_in" | "order_fixed_gate_jump_out_init" | "order_fixed_gate_jump_out" | "order_formation_init" | "order_formation" | "order_fixed_gate_open_init" | "order_fixed_gate_open" | "order_fixed_gate_close_init" | "order_fixed_gate_close" | "order_eject_init" | "order_eject" | "order_fixed_gate_collapse_init" | "order_fixed_gate_collapse" | "order_match_speed" | "order_dark_reign_shoot" | "order_move_to_spawn_pos" | "order_turns_object_lights_on_init" | "order_turns_object_lights_on" | "order_make_boridin_section_break_away_init" | "order_rotate_boridin_breakaway_warp_projector_init" | "order_start_warp_projection_from_boridin_init" | "order_start_warp_projection_from_boridin" | "order_make_ripper_drop_what_its_carrying_init" | "order_make_ripper_drop_what_its_carrying" | "order_turns_object_lights_off" | "order_huuuuuuuge_explosion" | "order_immediately_set_ship_to_zero_velocity_and_rotation" | "order_fly_ship_backwards" | "player_controls" | "order_multiplayer_control" | "order_avoid_target_init" | "order_avoid_target" | "order_torpedo_init" | "order_torpedo" | "order_launch_init" | "order_launch" | "order_fight_init" | "order_fight" | "order_abandoned_init" | "order_abandoned" | "order_scoop_up_init" | "order_scoop_up" | "order_scoop_up_exit" | "order_eject_spin_init" | "order_eject_spin" | "order_dock_init" | "order_dock" | "order_dock_exit" | "order_fire_ion_cannon_init" | "order_fire_ion_cannon" | "order_fire_ion_cannon_exit" | "order_ripper_end_drop_object_init" | "order_ripper_end_drop_object" | "order_ripper_attach_cargo_pod_to_mammoth_init" | "order_ripper_attach_cargo_pod_to_mammoth" | "order_eject_fighter_attack" | "order_disrupted_init" | "order_disrupted" | "order_disrupted_exit" | "order_make_capship_list_left" | "order_make_capship_list_right" | "order_friendly_fire_init" | "order_friendly_fire" | "order_eject_player_init" | "order_eject_player" | "order_ship_follow_curve_backwards_init" | "order_ship_follow_curve_backwards" | "order_ship_follow_curve_backwards_exit" | "order_mill_init" | "order_mill" | "order_deathmatch_respawn_effect_init" | "order_deathmatch_respawn_effect" | "order_deathmatch_respawn_effect_exit" | "order_first_step_init" | "mission_lost" | "career_over" | "medal_ceremony" | "shot_at" | "destroyed" | "launched" | "ship_reached" | "camera_reached" | "proximity_close" | "proximity_general" | "object_scooped" | "player_ready_to_jump" | "jumped_in" | "fixed_gate_jumped_in" | "player_ready_to_warp" | "player_wants_backup" | "ripper_grabbed_object" | "ripper_dropped_object" | "cloaked" | "decloaked" | "inside_object" | "outside_object" | "undocked" | "docked" | "explosion_ship" | "jumped_through_hoop" | "mission_started" | "mission_ended" | "object_added" | "object_removed" | "missile_added" | "missile_removed" | "order_started" | "order_ended" | "trigger_fired" +export type HookName = "object_damage" | "object_armor_damage" | "component_damage" | "damage_by_difficulty" | "object_destroyed" | "bullet_fire" | "radio_say" | "permission_to_land" | "missile_launch" | "missile_launch_turret" | "order_push" | "order_pop" | "fight_choose_maneuver" | "maneuver_run" | "object_orders" | "vm_command" | "restart_screen" | "order_retaliate" | "order_do_nothing" | "order_fly_aimlessly_init" | "order_fly_aimlessly" | "order_launch_missile" | "order_launch_jack_hammer" | "order_warp_in_init" | "order_warp_in" | "order_warp_out_init" | "order_warp_out" | "order_fly_init" | "order_fly" | "order_run_away" | "order_land_init" | "order_land" | "order_escort_init" | "order_escort" | "order_find_new_target" | "order_explode_init" | "order_explode" | "order_ripper_grabs_target_object_init" | "order_ripper_grabs_target_object" | "order_ripper_grabs_target_object_exit" | "order_object_attach_init" | "order_object_attach" | "order_formation_regroup_init" | "order_formation_regroup" | "order_patrol_route_init" | "order_patrol_route" | "order_toggle_cloak" | "order_ship_follow_curve_init" | "order_ship_follow_curve" | "order_ship_follow_curve_exit" | "order_slow_rotate" | "order_jump_in_init" | "order_jump_in" | "order_jump_out_init" | "order_jump_out" | "order_find_scoop_up" | "order_random_spin_slow_init" | "order_random_spin_medium_init" | "order_random_spin_fast_init" | "order_fixed_gate_jump_in_init" | "order_fixed_gate_jump_in" | "order_fixed_gate_jump_out_init" | "order_fixed_gate_jump_out" | "order_formation_init" | "order_formation" | "order_fixed_gate_open_init" | "order_fixed_gate_open" | "order_fixed_gate_close_init" | "order_fixed_gate_close" | "order_eject_init" | "order_eject" | "order_fixed_gate_collapse_init" | "order_fixed_gate_collapse" | "order_match_speed" | "order_dark_reign_shoot" | "order_move_to_spawn_pos" | "order_turns_object_lights_on_init" | "order_turns_object_lights_on" | "order_make_boridin_section_break_away_init" | "order_rotate_boridin_breakaway_warp_projector_init" | "order_start_warp_projection_from_boridin_init" | "order_start_warp_projection_from_boridin" | "order_make_ripper_drop_what_its_carrying_init" | "order_make_ripper_drop_what_its_carrying" | "order_turns_object_lights_off" | "order_huuuuuuuge_explosion" | "order_immediately_set_ship_to_zero_velocity_and_rotation" | "order_fly_ship_backwards" | "player_controls" | "order_multiplayer_control" | "order_avoid_target_init" | "order_avoid_target" | "order_torpedo_init" | "order_torpedo" | "order_launch_init" | "order_launch" | "order_fight_init" | "order_fight" | "order_abandoned_init" | "order_abandoned" | "order_scoop_up_init" | "order_scoop_up" | "order_scoop_up_exit" | "order_eject_spin_init" | "order_eject_spin" | "order_dock_init" | "order_dock" | "order_dock_exit" | "order_fire_ion_cannon_init" | "order_fire_ion_cannon" | "order_fire_ion_cannon_exit" | "order_ripper_end_drop_object_init" | "order_ripper_end_drop_object" | "order_ripper_attach_cargo_pod_to_mammoth_init" | "order_ripper_attach_cargo_pod_to_mammoth" | "order_eject_fighter_attack" | "order_disrupted_init" | "order_disrupted" | "order_disrupted_exit" | "order_make_capship_list_left" | "order_make_capship_list_right" | "order_friendly_fire_init" | "order_friendly_fire" | "order_eject_player_init" | "order_eject_player" | "order_ship_follow_curve_backwards_init" | "order_ship_follow_curve_backwards" | "order_ship_follow_curve_backwards_exit" | "order_mill_init" | "order_mill" | "order_deathmatch_respawn_effect_init" | "order_deathmatch_respawn_effect" | "order_deathmatch_respawn_effect_exit" | "order_first_step_init" | "mission_lost" | "career_over" | "medal_ceremony" | "shot_at" | "destroyed" | "launched" | "ship_reached" | "camera_reached" | "proximity_close" | "proximity_general" | "object_scooped" | "player_ready_to_jump" | "jumped_in" | "fixed_gate_jumped_in" | "player_ready_to_warp" | "player_wants_backup" | "ripper_grabbed_object" | "ripper_dropped_object" | "cloaked" | "decloaked" | "inside_object" | "outside_object" | "undocked" | "docked" | "explosion_ship" | "jumped_through_hoop" | "mission_started" | "mission_ended" | "object_added" | "object_removed" | "missile_added" | "missile_removed" | "order_started" | "order_ended" | "trigger_fired" --- The hooks on the game's functions, which `hooks.after` takes. -export type FunctionHookName = "object_damage" | "object_armor_damage" | "component_damage" | "damage_by_difficulty" | "object_destroyed" | "bullet_fire" | "radio_say" | "missile_launch" | "missile_launch_turret" | "order_push" | "order_pop" | "fight_choose_maneuver" | "maneuver_run" | "object_orders" | "vm_command" | "restart_screen" | "order_retaliate" | "order_do_nothing" | "order_fly_aimlessly_init" | "order_fly_aimlessly" | "order_launch_missile" | "order_launch_jack_hammer" | "order_warp_in_init" | "order_warp_in" | "order_warp_out_init" | "order_warp_out" | "order_fly_init" | "order_fly" | "order_run_away" | "order_land_init" | "order_land" | "order_escort_init" | "order_escort" | "order_find_new_target" | "order_explode_init" | "order_explode" | "order_ripper_grabs_target_object_init" | "order_ripper_grabs_target_object" | "order_ripper_grabs_target_object_exit" | "order_object_attach_init" | "order_object_attach" | "order_formation_regroup_init" | "order_formation_regroup" | "order_patrol_route_init" | "order_patrol_route" | "order_toggle_cloak" | "order_ship_follow_curve_init" | "order_ship_follow_curve" | "order_ship_follow_curve_exit" | "order_slow_rotate" | "order_jump_in_init" | "order_jump_in" | "order_jump_out_init" | "order_jump_out" | "order_find_scoop_up" | "order_random_spin_slow_init" | "order_random_spin_medium_init" | "order_random_spin_fast_init" | "order_fixed_gate_jump_in_init" | "order_fixed_gate_jump_in" | "order_fixed_gate_jump_out_init" | "order_fixed_gate_jump_out" | "order_formation_init" | "order_formation" | "order_fixed_gate_open_init" | "order_fixed_gate_open" | "order_fixed_gate_close_init" | "order_fixed_gate_close" | "order_eject_init" | "order_eject" | "order_fixed_gate_collapse_init" | "order_fixed_gate_collapse" | "order_match_speed" | "order_dark_reign_shoot" | "order_move_to_spawn_pos" | "order_turns_object_lights_on_init" | "order_turns_object_lights_on" | "order_make_boridin_section_break_away_init" | "order_rotate_boridin_breakaway_warp_projector_init" | "order_start_warp_projection_from_boridin_init" | "order_start_warp_projection_from_boridin" | "order_make_ripper_drop_what_its_carrying_init" | "order_make_ripper_drop_what_its_carrying" | "order_turns_object_lights_off" | "order_huuuuuuuge_explosion" | "order_immediately_set_ship_to_zero_velocity_and_rotation" | "order_fly_ship_backwards" | "player_controls" | "order_multiplayer_control" | "order_avoid_target_init" | "order_avoid_target" | "order_torpedo_init" | "order_torpedo" | "order_launch_init" | "order_launch" | "order_fight_init" | "order_fight" | "order_abandoned_init" | "order_abandoned" | "order_scoop_up_init" | "order_scoop_up" | "order_scoop_up_exit" | "order_eject_spin_init" | "order_eject_spin" | "order_dock_init" | "order_dock" | "order_dock_exit" | "order_fire_ion_cannon_init" | "order_fire_ion_cannon" | "order_fire_ion_cannon_exit" | "order_ripper_end_drop_object_init" | "order_ripper_end_drop_object" | "order_ripper_attach_cargo_pod_to_mammoth_init" | "order_ripper_attach_cargo_pod_to_mammoth" | "order_eject_fighter_attack" | "order_disrupted_init" | "order_disrupted" | "order_disrupted_exit" | "order_make_capship_list_left" | "order_make_capship_list_right" | "order_friendly_fire_init" | "order_friendly_fire" | "order_eject_player_init" | "order_eject_player" | "order_ship_follow_curve_backwards_init" | "order_ship_follow_curve_backwards" | "order_ship_follow_curve_backwards_exit" | "order_mill_init" | "order_mill" | "order_deathmatch_respawn_effect_init" | "order_deathmatch_respawn_effect" | "order_deathmatch_respawn_effect_exit" | "order_first_step_init" | "mission_lost" | "career_over" | "medal_ceremony" +export type FunctionHookName = "object_damage" | "object_armor_damage" | "component_damage" | "damage_by_difficulty" | "object_destroyed" | "bullet_fire" | "radio_say" | "permission_to_land" | "missile_launch" | "missile_launch_turret" | "order_push" | "order_pop" | "fight_choose_maneuver" | "maneuver_run" | "object_orders" | "vm_command" | "restart_screen" | "order_retaliate" | "order_do_nothing" | "order_fly_aimlessly_init" | "order_fly_aimlessly" | "order_launch_missile" | "order_launch_jack_hammer" | "order_warp_in_init" | "order_warp_in" | "order_warp_out_init" | "order_warp_out" | "order_fly_init" | "order_fly" | "order_run_away" | "order_land_init" | "order_land" | "order_escort_init" | "order_escort" | "order_find_new_target" | "order_explode_init" | "order_explode" | "order_ripper_grabs_target_object_init" | "order_ripper_grabs_target_object" | "order_ripper_grabs_target_object_exit" | "order_object_attach_init" | "order_object_attach" | "order_formation_regroup_init" | "order_formation_regroup" | "order_patrol_route_init" | "order_patrol_route" | "order_toggle_cloak" | "order_ship_follow_curve_init" | "order_ship_follow_curve" | "order_ship_follow_curve_exit" | "order_slow_rotate" | "order_jump_in_init" | "order_jump_in" | "order_jump_out_init" | "order_jump_out" | "order_find_scoop_up" | "order_random_spin_slow_init" | "order_random_spin_medium_init" | "order_random_spin_fast_init" | "order_fixed_gate_jump_in_init" | "order_fixed_gate_jump_in" | "order_fixed_gate_jump_out_init" | "order_fixed_gate_jump_out" | "order_formation_init" | "order_formation" | "order_fixed_gate_open_init" | "order_fixed_gate_open" | "order_fixed_gate_close_init" | "order_fixed_gate_close" | "order_eject_init" | "order_eject" | "order_fixed_gate_collapse_init" | "order_fixed_gate_collapse" | "order_match_speed" | "order_dark_reign_shoot" | "order_move_to_spawn_pos" | "order_turns_object_lights_on_init" | "order_turns_object_lights_on" | "order_make_boridin_section_break_away_init" | "order_rotate_boridin_breakaway_warp_projector_init" | "order_start_warp_projection_from_boridin_init" | "order_start_warp_projection_from_boridin" | "order_make_ripper_drop_what_its_carrying_init" | "order_make_ripper_drop_what_its_carrying" | "order_turns_object_lights_off" | "order_huuuuuuuge_explosion" | "order_immediately_set_ship_to_zero_velocity_and_rotation" | "order_fly_ship_backwards" | "player_controls" | "order_multiplayer_control" | "order_avoid_target_init" | "order_avoid_target" | "order_torpedo_init" | "order_torpedo" | "order_launch_init" | "order_launch" | "order_fight_init" | "order_fight" | "order_abandoned_init" | "order_abandoned" | "order_scoop_up_init" | "order_scoop_up" | "order_scoop_up_exit" | "order_eject_spin_init" | "order_eject_spin" | "order_dock_init" | "order_dock" | "order_dock_exit" | "order_fire_ion_cannon_init" | "order_fire_ion_cannon" | "order_fire_ion_cannon_exit" | "order_ripper_end_drop_object_init" | "order_ripper_end_drop_object" | "order_ripper_attach_cargo_pod_to_mammoth_init" | "order_ripper_attach_cargo_pod_to_mammoth" | "order_eject_fighter_attack" | "order_disrupted_init" | "order_disrupted" | "order_disrupted_exit" | "order_make_capship_list_left" | "order_make_capship_list_right" | "order_friendly_fire_init" | "order_friendly_fire" | "order_eject_player_init" | "order_eject_player" | "order_ship_follow_curve_backwards_init" | "order_ship_follow_curve_backwards" | "order_ship_follow_curve_backwards_exit" | "order_mill_init" | "order_mill" | "order_deathmatch_respawn_effect_init" | "order_deathmatch_respawn_effect" | "order_deathmatch_respawn_effect_exit" | "order_first_step_init" | "mission_lost" | "career_over" | "medal_ceremony" --- The hooks that concern an object, which an object's `hook` takes. export type ObjectHookName = "object_damage" | "object_armor_damage" | "component_damage" | "damage_by_difficulty" | "object_destroyed" | "bullet_fire" | "missile_launch" | "missile_launch_turret" | "order_push" | "order_pop" | "fight_choose_maneuver" | "maneuver_run" | "object_orders" | "order_retaliate" | "order_do_nothing" | "order_fly_aimlessly_init" | "order_fly_aimlessly" | "order_launch_missile" | "order_launch_jack_hammer" | "order_warp_in_init" | "order_warp_in" | "order_warp_out_init" | "order_warp_out" | "order_fly_init" | "order_fly" | "order_run_away" | "order_land_init" | "order_land" | "order_escort_init" | "order_escort" | "order_find_new_target" | "order_explode_init" | "order_explode" | "order_ripper_grabs_target_object_init" | "order_ripper_grabs_target_object" | "order_ripper_grabs_target_object_exit" | "order_object_attach_init" | "order_object_attach" | "order_formation_regroup_init" | "order_formation_regroup" | "order_patrol_route_init" | "order_patrol_route" | "order_toggle_cloak" | "order_ship_follow_curve_init" | "order_ship_follow_curve" | "order_ship_follow_curve_exit" | "order_slow_rotate" | "order_jump_in_init" | "order_jump_in" | "order_jump_out_init" | "order_jump_out" | "order_find_scoop_up" | "order_random_spin_slow_init" | "order_random_spin_medium_init" | "order_random_spin_fast_init" | "order_fixed_gate_jump_in_init" | "order_fixed_gate_jump_in" | "order_fixed_gate_jump_out_init" | "order_fixed_gate_jump_out" | "order_formation_init" | "order_formation" | "order_fixed_gate_open_init" | "order_fixed_gate_open" | "order_fixed_gate_close_init" | "order_fixed_gate_close" | "order_eject_init" | "order_eject" | "order_fixed_gate_collapse_init" | "order_fixed_gate_collapse" | "order_match_speed" | "order_dark_reign_shoot" | "order_move_to_spawn_pos" | "order_turns_object_lights_on_init" | "order_turns_object_lights_on" | "order_make_boridin_section_break_away_init" | "order_rotate_boridin_breakaway_warp_projector_init" | "order_start_warp_projection_from_boridin_init" | "order_start_warp_projection_from_boridin" | "order_make_ripper_drop_what_its_carrying_init" | "order_make_ripper_drop_what_its_carrying" | "order_turns_object_lights_off" | "order_huuuuuuuge_explosion" | "order_immediately_set_ship_to_zero_velocity_and_rotation" | "order_fly_ship_backwards" | "player_controls" | "order_multiplayer_control" | "order_avoid_target_init" | "order_avoid_target" | "order_torpedo_init" | "order_torpedo" | "order_launch_init" | "order_launch" | "order_fight_init" | "order_fight" | "order_abandoned_init" | "order_abandoned" | "order_scoop_up_init" | "order_scoop_up" | "order_scoop_up_exit" | "order_eject_spin_init" | "order_eject_spin" | "order_dock_init" | "order_dock" | "order_dock_exit" | "order_fire_ion_cannon_init" | "order_fire_ion_cannon" | "order_fire_ion_cannon_exit" | "order_ripper_end_drop_object_init" | "order_ripper_end_drop_object" | "order_ripper_attach_cargo_pod_to_mammoth_init" | "order_ripper_attach_cargo_pod_to_mammoth" | "order_eject_fighter_attack" | "order_disrupted_init" | "order_disrupted" | "order_disrupted_exit" | "order_make_capship_list_left" | "order_make_capship_list_right" | "order_friendly_fire_init" | "order_friendly_fire" | "order_eject_player_init" | "order_eject_player" | "order_ship_follow_curve_backwards_init" | "order_ship_follow_curve_backwards" | "order_ship_follow_curve_backwards_exit" | "order_mill_init" | "order_mill" | "order_deathmatch_respawn_effect_init" | "order_deathmatch_respawn_effect" | "order_deathmatch_respawn_effect_exit" | "order_first_step_init" | "shot_at" | "destroyed" | "launched" | "ship_reached" | "proximity_close" | "proximity_general" | "object_scooped" | "player_ready_to_jump" | "jumped_in" | "fixed_gate_jumped_in" | "player_ready_to_warp" | "player_wants_backup" | "ripper_grabbed_object" | "ripper_dropped_object" | "cloaked" | "decloaked" | "inside_object" | "outside_object" | "undocked" | "docked" | "explosion_ship" | "jumped_through_hoop" | "object_added" | "object_removed" | "missile_added" | "missile_removed" | "order_started" | "order_ended" | "trigger_fired" diff --git a/docs/guide/reference.md b/docs/guide/reference.md index 8b557695..9969c673 100644 --- a/docs/guide/reference.md +++ b/docs/guide/reference.md @@ -188,6 +188,7 @@ The mission's objects, the player's ship and the mission itself. For global scri |---|---|---| | `player` | [object](#objects), or nil | The player's ship, while a mission runs; nil between missions. | | `mission` | [Mission](#mission), or nil | The mission that runs, with its `number` and its `file`'s name; nil between missions. | +| `player_carrier` | [object](#objects), or nil | The player's carrier: the ship the player launched from, unless a script sets another (`set_player_carrier`). PERMISSION TO LAND asks it, and its bridge speaks to the player. Nil where there is none, as in a mission the player starts in flight, and between missions. | | `objects()` | list of [objects](#objects) | Every object in the mission, in the order of their slots. | | `missiles()` | list of [missile](#missiles) | Every missile in flight, newest first. | | `set_player_target(target: Object, component: number?)` | boolean | Makes `target`, or its component number `component`, the player's target, as a mission's SetPlayerTarget does: the display shows it, and the player stops matching speeds. Returns whether it worked: false for a target the player can't aim at, such as a destroyed component, and between missions. | @@ -195,6 +196,7 @@ The mission's objects, the player's ship and the mission itself. For global scri | `set_objective(objective: number, state: ObjectiveState)` | boolean | Objective `objective` of the mission that runs, numbered from 0 to 9 as a mission's SetObjective numbers them, takes `state`, as SetObjective does: `hidden`, `listed`, or `current`, which the objectives window then shows. Returns whether it changed: false between missions, and in a mission whose objectives nothing names. | | `variable(name: GameVariable)` | number, or nil | The value of the game's variable `name` in the mission that runs, as the mission's script reads it. Nil between missions. | | `set_variable(name: GameVariable, value: number)` | boolean | Sets the game's variable `name` to `value` in the mission that runs, as the mission's script sets it. The campaign keeps the value from one mission to the next where it keeps the variable. Returns false between missions. | +| `set_player_carrier(ship: Object?)` | boolean | Makes `ship` the player's carrier (`player_carrier`): PERMISSION TO LAND asks it, its bridge answers, and the player's ship lands on it where it offers a landing (its `landing`). Nil leaves the player without a carrier. Returns false between missions. | ### `openreliant.self` @@ -485,6 +487,7 @@ scripts on their object. | `cloaked` | boolean | *Changes.* Whether it's cloaked. Setting it cloaks or uncloaks it, as a mission's Cloak does, where its model can. | | `targetable` | boolean | *Changes.* Whether ships can target it. Setting it is what a mission's SetTargetable does: a type that can't be targeted stays so. | | `lights` | boolean | *Changes.* Whether its lights are on. Setting it is what a mission's DisableLights does. | +| `landing` | [Landing](#landing), or nil | *Changes.* How the player's ship lands on it, as its type's record has it when it's made (`ShipTypeRecord.landing`); nil where the player can't land on it. Setting it lets the player land on this ship, or keeps the player off it, for the rest of the mission. PERMISSION TO LAND and a mission's land order only bring the player's ship down on a ship that offers a landing. | | `disabled` | boolean | *Changes.* Whether it's left out of the mission's work, as a mission's DisableObject leaves it. | | `guns_disabled` | boolean | *Changes.* Whether its guns don't fire and its turrets rest, as a mission's DisableGuns sets. | | `missiles_disabled` | boolean | *Changes.* Whether it can't launch missiles, as a mission's DisableMissiles sets. | @@ -679,6 +682,7 @@ turret, and a turret's own scripts on their turret. |---|---|---| | `player` | [object](#objects), or nil | The player's ship, while a mission runs; nil between missions. | | `mission` | [Mission](#mission), or nil | The mission that runs, with its `number` and its `file`'s name; nil between missions. | +| `player_carrier` | [object](#objects), or nil | The player's carrier: the ship the player launched from, unless a script sets another (`set_player_carrier`). PERMISSION TO LAND asks it, and its bridge speaks to the player. Nil where there is none, as in a mission the player starts in flight, and between missions. | | `objects()` | list of [objects](#objects) | Every object in the mission, in the order of their slots. | | `missiles()` | list of [missile](#missiles) | Every missile in flight, newest first. | | `set_player_target(target: Object, component: number?)` | boolean | Makes `target`, or its component number `component`, the player's target, as a mission's SetPlayerTarget does: the display shows it, and the player stops matching speeds. Returns whether it worked: false for a target the player can't aim at, such as a destroyed component, and between missions. | @@ -686,6 +690,7 @@ turret, and a turret's own scripts on their turret. | `set_objective(objective: number, state: ObjectiveState)` | boolean | Objective `objective` of the mission that runs, numbered from 0 to 9 as a mission's SetObjective numbers them, takes `state`, as SetObjective does: `hidden`, `listed`, or `current`, which the objectives window then shows. Returns whether it changed: false between missions, and in a mission whose objectives nothing names. | | `variable(name: GameVariable)` | number, or nil | The value of the game's variable `name` in the mission that runs, as the mission's script reads it. Nil between missions. | | `set_variable(name: GameVariable, value: number)` | boolean | Sets the game's variable `name` to `value` in the mission that runs, as the mission's script sets it. The campaign keeps the value from one mission to the next where it keeps the variable. Returns false between missions. | +| `set_player_carrier(ship: Object?)` | boolean | Makes `ship` the player's carrier (`player_carrier`): PERMISSION TO LAND asks it, its bridge answers, and the player's ship lands on it where it offers a landing (its `landing`). Nil leaves the player without a carrier. Returns false between missions. | ### I.Campaign @@ -802,6 +807,11 @@ The radio says a line: the speech file `speech`, its speaker's face playing the | `film` | string | | `mode` | [RadioMode](#radiomode) | +### permission_to_land + +The player asks for PERMISSION TO LAND, with its key or the radio's menu, heard at most once in 500 ticks. The pilot asks, and the flight instructor answers in training, or else the player's carrier, where it offers a landing (`world.player_carrier`, the object's `landing`). Once the mission's script has set `landing_cleared`, the answer clears the ship, which lands; otherwise the carrier refuses. A handler that stops it can answer and land the ship its own way, such as with an order of its own. + + ### missile_launch `launcher` launches a missile from one of its racks at `target`. @@ -1940,6 +1950,10 @@ number. A script can set a field to either. `none`, `player_can_hit`, `full`, `eject_before_exploding`, or a number. +### Landing + +`yamato`, `reliant`, `bay`, `badanov`. + ### PartClass `hull`, `cockpit`, `turret`, `engine`, `shield_generator`, `comms_transmitter`, `gravity_drive`, `laser_turret`, `missile_turret`, `power_core`, `satellite_dish`, `service_door`, `shaft`, `surface_building`, `twin_power_cores`, `vent_hatch`, `ion_cannon`, `armored_plate`, `cap_gun`, `warp_projector`, `fuel_pod`, or a number. diff --git a/docs/guide/scripting.md b/docs/guide/scripting.md index 1cbdd03f..d58e044d 100644 --- a/docs/guide/scripting.md +++ b/docs/guide/scripting.md @@ -1158,7 +1158,10 @@ A ship type's record (`ShipTypeRecord`) holds what the game keeps in its executa it; `class`, such as `fighter` or `support`; `side`, the side its objects start on; and `display`, how the target display draws it. The class decides more than how the type is targeted: fighters fly with the fighters' flight model, and only fighters turn on a fighter that hits them -([Retaliation](../engine/orders.md#retaliation)). +([Retaliation](../engine/orders.md#retaliation)). OpenReliant adds `landing`, how the player's ship +lands on ships of the type ([Ship types](modding.md#ship-types)): the Reliant's and the Yamato's +landings, as in the game, and nil for the other types. A load script can give a type a landing +([Landing on another carrier](missions.md#landing-on-another-carrier)). A pilot's face (`Face`) has `name`, the id of the string the radio's window shows over the face, and the film the face plays for each way it moves as the pilot speaks: `talking`, `laughing`, diff --git a/src/engine/game/additions.zig b/src/engine/game/additions.zig index 73201969..c9d9171a 100644 --- a/src/engine/game/additions.zig +++ b/src/engine/game/additions.zig @@ -50,6 +50,7 @@ const shp = @import("../../formats/shp.zig"); const stats = @import("../../formats/stats.zig"); const wave = @import("../../formats/wave.zig"); const files = @import("../files.zig"); +const ailand = @import("ailand.zig"); const gameobj = @import("gameobj.zig"); const guns_module = @import("guns.zig"); const missiles_module = @import("missiles.zig"); @@ -444,6 +445,24 @@ pub const ShipExtra = struct { class: ?loadout.Class = null, access: ?loadout.Access = null, crew: ?u8 = null, + /// How the player lands on its ships (`Landing`). Without the key, it offers its base's + /// landing where it keeps its base's model, and none where it has a model of its own + /// (`ailand.Style.of`). + landing: ?Landing = null, + + /// What `Landing=` gives: one of the landings (`ailand.Style`), or `none`. + pub const Landing = union(enum) { + none, + offers: ailand.Style, + + /// The landing it gives; null for none. + pub fn style(given: Landing) ?ailand.Style { + return switch (given) { + .none => null, + .offers => |offered| offered, + }; + } + }; /// What the loadout's panel shows for a ship type without a base, unless its manifest says. pub const default_class: loadout.Class = .light; @@ -468,6 +487,7 @@ fn readShip(context: Context, section: []const u8, base: gameobj.GameType) Alloc if (manifest.value(section, "Class")) |text| made.class = readNamed(loadout.Class, context, "Class", text) orelse return null; if (manifest.value(section, "Access")) |text| made.access = readNamed(loadout.Access, context, "Access", text) orelse return null; if (manifest.value(section, "Crew")) |text| made.crew = readNumber(u8, context, "ship type", "Crew", text) orelse return null; + if (manifest.value(section, "Landing")) |text| made.landing = readLanding(context, text) orelse return null; if (manifest.value(section, "Schematic")) |text| made.schematic = try fileName(context, text); if (manifest.value(section, "GunsModel")) |text| made.guns_model = try fileName(context, text); if (manifest.value(section, "Guns")) |text| { @@ -533,6 +553,13 @@ fn readSwitch(context: Context, comptime noun: []const u8, comptime key: []const return null; } +/// `Landing=`'s `text`: `none`, in any case, or one of the landings (`readNamed`); null where it is +/// neither, which the log says. +fn readLanding(context: Context, text: []const u8) ?ShipExtra.Landing { + if (std.ascii.eqlIgnoreCase(std.mem.trim(u8, text, " \t"), "none")) return .none; + return .{ .offers = readNamed(ailand.Style, context, "Landing", text) orelse return null }; +} + /// `text` as a value of `Named`, by its name in any case or by its number (`gameNamed`); null /// where it is neither, which the log says with the names it takes. fn readNamed(comptime Named: type, context: Context, comptime key: []const u8, text: []const u8) ?Named { @@ -1034,6 +1061,46 @@ test "a type based on a ship the player can't fly, with and without a cockpit" { try std.testing.expectEqualStrings("greywire", list[1].extra.wire_frame.?); } +test "a ship type's Landing" { + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const mod: Mod = .{ .name = "fleet", .source = undefined, .manifest = .{ .text = + \\[ShipTypes] + \\dock= + \\closed= + \\plain= + \\wrong= + \\[ShipType dock] + \\Base=victorious + \\Model=dock.shp + \\Landing=Bay + \\[ShipType closed] + \\Base=reliant + \\Model=RELIANT.SHP + \\Landing=none + \\[ShipType plain] + \\Base=reliant + \\Model=RELIANT.SHP + \\[ShipType wrong] + \\Base=reliant + \\Model=RELIANT.SHP + \\Landing=runway + } }; + try read(arena.allocator(), &.{mod}); + defer reset(); + // A landing in any case, or none; without the key, none given, so the type takes the rule + // (`ailand.Style.of`). A type whose Landing isn't one is left out, which the log says. + const list = ships.all(); + try std.testing.expectEqual(3, list.len); + try std.testing.expectEqual(ailand.Style.bay, list[0].extra.landing.?.style().?); + try std.testing.expectEqual(ShipExtra.Landing.none, list[1].extra.landing.?); + try std.testing.expectEqual(null, list[1].extra.landing.?.style()); + try std.testing.expectEqual(null, list[2].extra.landing); + try std.testing.expectEqual(ailand.Style.bay, ailand.Style.of(@fromBackingInt(ships.first)).?); + try std.testing.expectEqual(null, ailand.Style.of(@fromBackingInt(ships.first + 1))); + try std.testing.expectEqual(ailand.Style.reliant, ailand.Style.of(@fromBackingInt(ships.first + 2)).?); +} + test "a mod's files name the records of the mods it requires" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); diff --git a/src/engine/game/ailand.zig b/src/engine/game/ailand.zig index 9f09c59c..cf8ec569 100644 --- a/src/engine/game/ailand.zig +++ b/src/engine/game/ailand.zig @@ -1,12 +1,18 @@ -//! `C:\lancer\game\ailand.cpp`: Land, order 8, by which the player's ship lands on the carrier it -//! launched from, which ends the mission ([Landing](../../../docs/engine/orders.md#landing)). The -//! player asks for it with PERMISSION TO LAND (`radio.permissionToLand`). Its init picks one -//! of two styles by the carrier; each has its own init and update (`land_styles`, `0x004E1FE8`, -//! `0x18` bytes each: init, update, exit, 0, name and 0): the Reliant's, down into its first -//! launch tube (`reliantInit`, `reliantUpdate`), and the Yamato's, down onto a pad of its landing -//! bay (`yamatoInit`, `yamatoUpdate`). OpenReliant adds a third, for every other carrier it -//! launches from as a hangar bay: in at the doors of a gate and down to its launch point -//! (`bayInit`, `bayUpdate`). +//! `C:\lancer\game\ailand.cpp`: Land, order 8, by which the player's ship lands on its carrier, +//! which ends the mission ([Landing](../../../docs/engine/orders.md#landing)). The player asks for +//! it with PERMISSION TO LAND (`radio.permissionToLand`). Its init picks how the ship lands +//! (`Style`), and each style has its own init and update (`land_styles`, `0x004E1FE8`, `0x18` bytes +//! each: init, update, exit, 0, name and 0): the Reliant's, down into its first launch tube +//! ([`ailand/reliant.zig`](ailand/reliant.zig)), and the Yamato's, down onto a pad of its landing +//! bay ([`ailand/yamato.zig`](ailand/yamato.zig)). OpenReliant adds two, in at the doors of a gate +//! and down to where ships launch from it: into a hangar bay, and into the Badanov's or the +//! Krasny's bay ([`ailand/bay.zig`](ailand/bay.zig)). +//! +//! **Improvement:** the game picks the style by the carrier's type, and lands only on the Reliant +//! and the Yamato. OpenReliant lands on a ship that offers a landing (`create.Slot.landing`). Its +//! type's record says which one (`create.combat_stats.Static.landing`): the Reliant and the Yamato +//! offer theirs, as in the game, and no other type offers one (`Style.of`). Mods and missions' +//! scripts can change it, so that a mission sets its own flow. //! //! **Unverified:** that `order_land` and the styles' routines (`0x0040EB40` to `0x0040FC77`) are //! this file's. They lie after its known code, before `airipper.cpp`'s, and the style table @@ -27,54 +33,60 @@ const Context = aigeneric.Context; const camera = @import("camera.zig"); const create = @import("create.zig"); const gameobj = @import("gameobj.zig"); -const input = @import("../input.zig"); const launch = @import("launch.zig"); -const bay_launch = @import("launch/bay.zig"); const objects = @import("objects.zig"); -const reliant_launch = @import("launch/reliant.zig"); -const sound3d = @import("sound3d.zig"); -/// How a ship lands, by the carrier it lands on (`order_land_init`). +pub const bay = @import("ailand/bay.zig"); +pub const reliant = @import("ailand/reliant.zig"); +pub const yamato = @import("ailand/yamato.zig"); + +/// How the player's ship lands on a carrier: the landing a ship offers (`create.Slot.landing`), +/// and the one a landing runs (`State.landing`). pub const Style = enum(u32) { - /// On the Yamato, down onto a pad of its landing bay (`yamatoInit`, `yamatoUpdate`). + /// On the Yamato, down onto a pad of its landing bay (`yamato.init`, `yamato.update`). yamato = 0, /// On the Reliant, down into its first launch tube through the tube's upper door - /// (`reliantInit`, `reliantUpdate`). + /// (`reliant.init`, `reliant.update`). reliant = 1, - /// **Improvement:** OpenReliant's own, in a hangar bay of any other carrier that ships launch - /// from as one: in at the doors of a gate and down to its launch point (`bayInit`, - /// `bayUpdate`). + /// **Improvement:** OpenReliant's own, in a hangar bay that ships launch from: in at the doors + /// of a gate and down to its launch point (`bay.init`, `bay.update`). bay = 2, - /// OpenReliant's own: the order aims at nothing that can be landed on, and ends. - none = std.math.maxInt(u32), - _, - - /// The style of a landing on a ship of type `carrier`, or null for a ship with a launch style - /// of its own that nothing lands on, such as the Badanov. Any other ship is landed on as a - /// hangar bay, as ships launch from it (`launch.Style.ofCarrier`), where its model has a launch - /// point for the gate (`bayInit`). - pub fn of(carrier: gameobj.Type) ?Style { - return switch (carrier.base()) { + /// **Improvement:** OpenReliant's own, in the Badanov's or the Krasny's bay: in at its doors + /// and down to where a ship waits at the gate to launch, as in a hangar bay + /// (`launch.badanov.gatePlace`). + badanov = 3, + + /// The name scripts know these values by. + pub const script_name = "Landing"; + + /// The landing the game offers on a ship of type `ship_type` (`order_land_init`, `0x0040EAC0`): + /// the Reliant's and the Yamato's. It offers none on any other type, where it stops with + /// "Cannot land on %s". + pub fn original(ship_type: gameobj.GameType) ?Style { + return switch (ship_type) { .reliant => .reliant, .yamato => .yamato, - else => if ((launch.Style.ofCarrier(carrier, 0) orelse .bay) == .bay) .bay else null, + else => null, }; } -}; -/// How the Reliant's landing brings the ship down. -pub const Touchdown = enum { - /// **Improvement:** the ship levels out as it approaches (`flaredAim`) and stops dead, so that - /// it sinks straight down the tube and comes to rest level in it; and once its top is below the - /// tube's upper door, the door closes over it, sounding (`reliantUpdate`). - level, - /// As the game lands it: pitched as it came, coasting as it stops, the door left open. - original, + /// The landing a ship of type `ship_type` offers before scripts change it + /// (`create.typeRecords`). One of the game's types offers the game's (`original`). A type a + /// mod adds offers the one its section gives (`additions.ShipExtra.landing`). Without one, it + /// offers its base's where it keeps its base's model (`launch.launchesAsBase`), whose parts + /// that landing goes by, and none where it has a model of its own. + pub fn of(ship_type: gameobj.Type) ?Style { + const added = ship_type.added() orelse return original(ship_type.base()); + if (added.extra.landing) |given| return given.style(); + return if (launch.launchesAsBase(ship_type)) original(added.base) else null; + } }; /// The order's state (`GameObject.order_state`). pub const State = extern struct { - style: Style, + /// The landing it runs, by its style's number (`landing`), or `no_landing` where the order + /// aims at nothing that offers one. + style: u32, /// The frame's tick the step waits for; in the Yamato's landing, from the cutaway on, the tick /// the view from its bay begins to slide along it (`camera.View.landing_bay`). due: i32, @@ -82,22 +94,37 @@ pub const State = extern struct { pad_due: i32, step: Step, /// The Reliant's: the middle of the tube the ship lands in, where the cutaway has moved the - /// carrier (`cutaway`). + /// carrier (`reliant.cutaway`). tube: [3]f32, /// OpenReliant's own: whether the Reliant's upper door has begun to close over the ship - /// (`Touchdown.level`). + /// (`reliant.Touchdown.level`). door_closing: bool, /// OpenReliant's own: where the ship stands on the Yamato's pad, in the pad's frame, which - /// each frame's pass places it at (`hold`). The game hangs the ship's frame from the pad's. + /// each frame's pass places it at (`yamato.hold`). The game hangs the ship's frame from the + /// pad's. on_pad: [3]f32, pad_orientation: math.Matrix, - /// OpenReliant's own, a bay's: where the launch point of the gate the ship lands at stands and - /// how it is turned, where the cutaway has moved the carrier (`bayCutaway`), and the mouth of - /// the bay, where the way out from the point leaves the carrier's bounds (`bayMouth`). + /// OpenReliant's own, a bay's: where the ship comes to rest at the gate it lands at and how + /// it is turned there, where the cutaway has moved the carrier (`bay.cutaway`), and the mouth + /// of the bay, where the way out from there leaves the carrier's bounds (`bay.mouth`). gate_place: [3]f32, gate_orientation: math.Matrix, mouth: [3]f32, + /// What `style` holds where the order aims at nothing that offers a landing, which the update + /// lets go: OpenReliant's own. + pub const no_landing = std.math.maxInt(u32); + + /// The landing it runs; null where the order aims at nothing that offers one. + pub fn landing(state: *const State) ?Style { + return std.enums.fromInt(Style, state.style); + } + + /// Sets the landing it runs, or none. + pub fn setLanding(state: *State, style: ?Style) void { + state.style = if (style) |runs| @backingInt(runs) else no_landing; + } + comptime { assert(@offsetOf(State, "due") == 0x04); assert(@offsetOf(State, "pad_due") == 0x08); @@ -107,8 +134,8 @@ pub const State = extern struct { } }; -/// The step a landing is at (`State.step`), as each style numbers its own (`ReliantStep`, -/// `YamatoStep`). +/// The step a landing is at (`State.step`), as each style numbers its own (`reliant.Step`, +/// `yamato.Step`, `bay.Step`). pub const Step = enum(u32) { _, @@ -131,898 +158,160 @@ pub const Step = enum(u32) { } }; -/// The Reliant's steps. -pub const ReliantStep = enum(u32) { - /// The player flies on until the landing is due; then the cutaway begins, and the tube's upper - /// door opens. - waiting = 0, - /// The ship flies to `over_tube`, slowing as it nears, and stops within `near`. - approaching = 1, - /// Stopped, it waits `settle` ticks. - settling = 2, - /// It sinks into the tube, slowing as it goes, and is down within `down_at` of its middle. - sinking = 3, - /// Down, it waits `landed_wait` ticks, and the mission is over. - down = 4, - _, -}; - /// How long the player flies on before the landing begins, in ticks: the Reliant's /// (`0x0040F5EA`) and the Yamato's (`0x0040EB7A`). -const wait: i32 = 700; +pub const wait: i32 = 700; -/// Where the cutaway moves the Reliant, or the Yamato's landing bay, turned as the world is, far +/// Where the cutaway moves the carrier, or the Yamato's landing bay, turned as the world is, far /// from what the mission holds (`0x0040F600`, `0x0040EBE4`). -const carrier_away: Vector = .{ 0, -1_000_000, 0 }; - -/// The Reliant's launch tube the ship lands down, its first (`0x0040F600`), halfway between whose -/// doors' middles it lands (`launch.reliant.tubeMiddle`); and that tube's upper door, a part of the -/// Reliant's root's child list (`launch.reliant.Door`), which opens as the landing begins. -const first_tube = 0; -const upper_door = reliant_launch.Door.upper.part(first_tube); - -/// Where the cutaway sets the ship, in the carrier's frame from the tube's middle: ahead of it and -/// above it; and where the ship flies to, over the tube, where it first looks (`0x0040F600`). -const start_offset: Vector = .{ 0, -5000, 20000 }; -const over_tube: Vector = .{ 0, -3000, 0 }; - -/// The upper door's track (`0x004E2050`), and how fast it plays to open the door (`0x0040FA55`). -/// It plays back to close the door as fast as the launch's tube doors open -/// (`launch.reliant.door_speed`), OpenReliant's choice (`Touchdown.level`). -const door_track = "opendoor2"; -const door_speed: f32 = 1.23; - -/// The throttle the ship approaches with for each unit it has to go, in both styles, and the most -/// the Reliant's takes (`0x004DC4B0`, `0x004DC408`); and how near it stops (`0x004DC468`). -const approach_throttle: f32 = 0.0001; -const approach_most: f32 = 0.5; -const near: f32 = 200; +pub const carrier_away: Vector = .{ 0, -1_000_000, 0 }; -/// How long the ship waits over the tube before it sinks, in ticks (`0x0040FB59`). -const settle: i32 = 50; - -/// The throttle the ship sinks with for each unit it has to go (`0x004DC538`), and how far over the -/// tube's middle it is down, along the carrier's Y axis, which points below it (`0x004DC534`). -const sink_throttle: f32 = 1.0 / 15000.0; -const down_at: f32 = -100; +/// The throttle the ship approaches with for each unit it has to go, in the Reliant's and the +/// Yamato's landings, and the most the Reliant's takes (`0x004DC4B0`, `0x004DC408`); and how near +/// it stops (`0x004DC468`). A landing in a bay goes by them too. +pub const approach_throttle: f32 = 0.0001; +pub const approach_most: f32 = 0.5; +pub const near: f32 = 200; /// How long the mission goes on once the ship is down, in ticks (`0x0040FC4A`). -const landed_wait: i32 = 270; - -/// The parts of the Yamato's landing bay its landing goes by: the hangar, whose lights the cutaway -/// masks as the launch's hangar does (`0x0040EC57`), and the pad the ship comes down on, whose -/// track lowers it (`0x0040F0D2`, `0x004E2048`). -const bay_hangar = 6; -const landing_pad = 1; -const bay_light_mask: u32 = 0x3B; -const pad_track = "floor"; - -/// Where the cutaway stands the ship in the bay, as shares of the bay's bounds: across, down and -/// along, this much of its maximum and the rest of its minimum (`0x0040EC95`, `0x004DC3F8`, -/// `0x004DC4C0`). -const stand_share: Vector = .{ 0.2, 0.2, 0.3 }; - -/// The throttle the Yamato's cutaway leaves the ship at (`0x0040EE43`). -const cutaway_throttle: f32 = 0.5; - -/// How long after the cutaway the view from the bay begins to slide along it, in ticks -/// (`0x0040EF1B`). -const slide_after: i32 = 600; - -/// How far above the hangar's middle, along its Y axis, the ship first flies, and how near it gets -/// before it goes on, which the game compares squared (`0x004DC44C`, `0x004DC490`). -const over_bay: f32 = 1000; -const over_bay_near: f32 = 2000; +pub const landed_wait: i32 = 270; -/// The speed the ship flies through the bay at most, its throttle what that is of its cruise speed -/// (`0x004DC440`). -const bay_speed: f32 = 100; - -/// How far beyond the pad's middle along the bay the ship is to stand, and how near it gets before -/// it stops (`0x004DC4A8`); how far ahead along its nose it steers meanwhile (`0x0040F1D1`); and -/// the throttle it approaches with, `approach_throttle` for each unit it has to go less -/// `approach_least` (`0x004DC4AC`). -const pad_ahead: f32 = 500; -const on_pad_near: f32 = 500; -const pad_lead: f32 = 500; -const approach_least: f32 = 0.02; - -/// How the ship turns straight along the bay and level before it stands on the pad -/// (`0x0040F26C`): its yaw and pitch inputs are the angles off the bay's axis less -/// `align_damping` times its rates of turn, times `align_gain` over its type's rates, and it is -/// straight once both are within `aligned` (`0x004DC424`, `0x004DC4C0`, `0x004DC474`). -const align_damping: f32 = 4; -const align_gain: f32 = 0.3; -const aligned: f32 = 0.05; - -/// How long the ship waits on the pad before the pad lowers, and how long the pad lowers before the -/// mission is over, in ticks (`0x0040F3A1`, `0x0040F56D`). -const pad_settle: i32 = 200; -const lowered_wait: i32 = 400; - -/// The Yamato's steps (`land_yamato_update`, `0x0040EE80`). -pub const YamatoStep = enum(u32) { - /// The player flies on until the landing is due; then the cutaway begins (`yamatoCutaway`). - waiting = 0, - /// The ship flies to `over_bay` above the hangar's middle. - descending = 1, - /// It flies on to stand `pad_ahead` beyond the pad's middle, slowing as it nears. - approaching = 2, - /// Stopped, it turns straight along the bay and level, and stands on the pad. - aligning = 3, - /// It waits `pad_settle` ticks on the pad, which it rides from now on (`hold`). - settling = 4, - /// The pad lowers, sounding the ship's landing. - lowering = 5, - /// `lowered_wait` ticks later, the mission is over. - down = 6, - _, -}; - -/// OpenReliant's own, for a landing in a bay: how far out from the mouth of the bay the cutaway -/// sets the ship, along the way it launches: this share of the bay's depth, from the launch point -/// to the mouth, and at least this many times the ship's radius, so that a shallow bay still gives -/// the ship a way to fly in on. They go by the bay and the ship rather than by a fixed distance, -/// so that the landing fits any carrier. -const bay_approach_share: f32 = 0.6; -const bay_approach_least_radii: f32 = 20; - -/// How far out from the mouth of a bay `depth` deep the cutaway sets a ship of radius `radius` -/// (`bay_approach_share`, `bay_approach_least_radii`). -fn bayApproach(depth: f32, radius: f32) f32 { - return @max(bay_approach_share * depth, bay_approach_least_radii * radius); -} - -/// The steps of a landing in a bay (`bayUpdate`), OpenReliant's own. -pub const BayStep = enum(u32) { - /// The player flies on until the landing is due; then the cutaway begins (`bayCutaway`), and - /// the gate's doors open. - waiting = 0, - /// The ship flies in at the doors to the launch point, slowing as it nears, and stops within - /// `near` of it; the doors close behind it. - approaching = 1, - /// It waits `landed_wait` ticks at the launch point, and the mission is over. - landed = 2, - _, -}; - -/// `order_land_init` (`0x0040EAC0`): the style, by the type of the carrier the order aims at, and -/// the style's init. +/// `order_land_init` (`0x0040EAC0`): the landing that the ship the order aims at offers +/// (`create.Slot.landing`), and its style's init. /// -/// **Fix:** the game stops with "Cannot land on %s" for a ship that nothing lands on; OpenReliant +/// **Fix:** the game stops with "Cannot land on %s" for a ship it has no landing for; OpenReliant /// logs it, and the update lets the order go. pub fn init(ctx: Context, index: u16) void { const all = ctx.world.objects; const state = &all.slots[index].state.land; const carrier = all.slots[index].orders[0].target.slotIn(all); - const carrier_type = if (carrier) |at| all.slots[at].object.type else null; - state.style = if (carrier_type) |of| Style.of(of) orelse .none else .none; - switch (state.style) { - .reliant => reliantInit(ctx, index), - .yamato => yamatoInit(ctx, index), - .bay => bayInit(ctx, index), - .none => log.warn("object {d} cannot land on what its order aims at", .{index}), - _ => {}, + const offered = if (carrier) |at| all.slots[at].landing else null; + state.setLanding(offered); + const style = offered orelse { + log.warn("object {d} cannot land on what its order aims at", .{index}); + return; + }; + switch (style) { + .reliant => reliant.init(ctx, index), + .yamato => yamato.init(ctx, index), + .bay, .badanov => bay.init(ctx, index), } } -/// `order_land` (`0x0040EB40`): the style's update. A landing OpenReliant has no style for ends at -/// once. +/// `order_land` (`0x0040EB40`): the style's update. A landing on what offers none ends at once. pub fn update(ctx: Context, index: u16) void { const slot = &ctx.world.objects.slots[index]; - switch (slot.state.land.style) { - .reliant => reliantUpdate(ctx, index), - .yamato => yamatoUpdate(ctx, index), - .bay => bayUpdate(ctx, index), - .none, _ => aigeneric.end(ctx, index), - } -} - -/// `land_reliant_init` (`0x0040F5C0`): the first step, due `wait` ticks on, or at once where the -/// player's ship is being sent home for its friendly fire. -fn reliantInit(ctx: Context, index: u16) void { - const state = &ctx.world.objects.slots[index].state.land; - state.step = .of(ReliantStep.waiting); - state.due = ctx.world.clock.frame_start + if (ctx.world.player.ending.sentHome()) 0 else wait; -} - -/// `land_reliant_update` (`0x0040F940`): a step of the Reliant's landing (`Step`). -/// -/// While the landing is not yet due, the player's ship flies by the player's controls -/// (`input.playerControlOrder`). Then the cutaway begins (`cutaway`): the mission's scene becomes -/// the landing's, every object but the ship and the carrier is disabled and the two others -/// enabled, and the ship flies by its nose (`motion.Motion.plain`), passing through everything. -/// The carrier's first tube's upper door opens, sounding where it is (`playDoor`). -/// -/// The ship steers at `over_tube`, its throttle `approach_throttle` for each unit it has to go, at -/// most `approach_most`, and stops within `near` of it, its turns nothing. It waits `settle` ticks, -/// then sinks along its own Y axis (`motion.Motion.downward`), its throttle `sink_throttle` for -/// each unit it stands from the tube's middle. Once it is less than `down_at` over it, it stops, -/// sounding its landing, and `landed_wait` ticks later the mission is over (`vm.Variables`). -fn reliantUpdate(ctx: Context, index: u16) void { - const world = ctx.world; - const all = world.objects; - const slot = &all.slots[index]; - const object = &slot.object; - const state = &slot.state.land; - const carrier = slot.orders[0].target.slotIn(all) orelse return; - const now = ctx.world.clock.frame_start; - switch (state.step.as(ReliantStep)) { - .waiting => { - if (now < state.due and index == all.player) return input.playerControlOrder(ctx, index); - cutaway(ctx, index, carrier); - world.player.showing = .landing; - for (all.slots[0..all.count], 0..) |*each, at| each.object.flags.disabled = at != index and at != carrier; - state.step = .of(ReliantStep.approaching); - slot.motion = .plain; - object.flags.no_collisions = true; - playDoor(world, &all.slots[carrier], .dooropen, 0, door_speed); - }, - .approaching => { - const frame = tubeFrame(state, &all.slots[carrier]); - const point = switch (world.touchdown) { - .level => flaredAim(frame, object), - .original => frame.point(over_tube), - }; - _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{}); - const distance = math.distance(point, object.nextPosition()); - object.throttle = @min(distance * approach_throttle, approach_most); - if (distance >= near) return; - object.letGo(); - if (world.touchdown == .level) ai.stop(object); - state.step = .of(ReliantStep.settling); - state.due = now + settle; - }, - .settling => { - if (state.due >= now) return; - state.step = .of(ReliantStep.sinking); - slot.motion = .downward; - }, - .sinking => { - const reliant = &all.slots[carrier]; - const frame = tubeFrame(state, reliant); - const off = frame.inverse(object.nextPosition()); - object.throttle = math.length(off) * sink_throttle; - // **Improvement:** the tube's upper door closes over the ship: its opening played back - // from where it stands, as fast as the launch's tube doors open, with the game's sound - // of a door closing (`0x36`, `doorclos`), as the opening had the sound of one opening - // (`Touchdown.level`). - if (world.touchdown == .level and !state.door_closing and belowDoor(reliant, frame, off[1] + object.bounds_min.y)) { - state.door_closing = true; - playDoor(world, reliant, .doorclos, objects.Model.keep_time, -reliant_launch.door_speed); - } - if (off[1] <= down_at) return; - object.letGo(); - sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); - state.step = .of(ReliantStep.down); - state.due = now + landed_wait; - }, - .down => { - if (state.due >= now) return; - if (world.variables) |variables| variables.mission_over = 1; - }, - _ => {}, + const style = slot.state.land.landing() orelse return aigeneric.end(ctx, index); + switch (style) { + .reliant => reliant.update(ctx, index), + .yamato => yamato.update(ctx, index), + .bay, .badanov => bay.update(ctx, index), } } -/// `land_reliant_cutaway` (`0x0040F600`): the cutaway of the Reliant's landing. One of its two -/// views, picked at random, watches the ship, locked. The carrier moves to `carrier_away`, turned -/// as the world is, and the middle of its first launch tube is worked out there: halfway between -/// the middles of the tube's doors, each the middle of the bounds of its part's finest level -/// (`launch.reliant.tubeMiddle`). The ship stands at `start_offset` from it in the carrier's frame, -/// looking at `over_tube`, stopped, its power shared evenly. The carrier's orders are all popped, -/// and it stops. -fn cutaway(ctx: Context, index: u16, carrier: u16) void { - const world = ctx.world; - const all = world.objects; - const slot = &all.slots[index]; - const state = &slot.state.land; - const pick: camera.View = if (world.random.oneIn(2)) .landing_aside else .landing_tube; +/// The start of a landing's cutaway: one of `views`, picked at random, watches the ship in slot +/// `index`, locked (`camera.switchView`). +pub fn watch(world: gameobj.World, index: u16, views: [2]camera.View) void { + const pick = if (world.random.oneIn(2)) views[0] else views[1]; _ = camera.switchView(world, pick, index, true, true); - const reliant = &all.slots[carrier]; - objects.setPlace(&reliant.object, &reliant.drawn, .{ .position = carrier_away, .orientation = math.identity }); - if (reliant_launch.tubeMiddle(reliant, first_tube, 0)) |tube| state.tube = tube; - const frame = tubeFrame(state, reliant); - const object = &slot.object; - objects.setPosition(object, &slot.drawn, frame.point(start_offset)); - const look = frame.point(over_tube); - objects.setOrientation(object, &slot.drawn, math.lookAt(look - object.nextPosition())); - ai.stop(object); - object.gun_factor = 1; - object.speed_factor = 1; - object.shield_factor = 1; - aigeneric.popAll(ctx, carrier); - ai.stop(&reliant.object); -} - -/// `land_yamato_init` (`0x0040EB60`): the first step, due `wait` ticks on; for the player's ship, -/// the landing bay made in the cutaway slot, passing through everything. -fn yamatoInit(ctx: Context, index: u16) void { - const world = ctx.world; - const state = &world.objects.slots[index].state.land; - state.step = .of(YamatoStep.waiting); - state.due = world.clock.frame_start + wait; - if (index != world.objects.player) return; - const bay = create.make(world, create.cutaway_slot, .of(.yamato_landing_bay)) catch |err| { - log.warn("the Yamato's landing bay is left out: {s}", .{@errorName(err)}); - return; - } orelse return; - world.objects.slots[bay].object.flags.no_collisions = true; } -/// `land_yamato_update` (`0x0040EE80`): a step of the Yamato's landing (`YamatoStep`). A ship -/// other than the player's lets its landing go at once (`order_pop`). -/// -/// While the landing is not yet due, the player's ship flies by the player's controls. Then the -/// cutaway begins (`yamatoCutaway`), and the ship flies by its nose (`motion.Motion.plain`), -/// passing through everything. The mission's scene becomes the landing's, and every other object -/// is disabled. The ship steers at `over_bay` above the hangar's middle, upright, at `bay_speed`, -/// until it is within `over_bay_near` of it. Then it steers `pad_lead` ahead along its nose from -/// the place `pad_ahead` beyond the pad's middle, level with its own underside, its throttle -/// `approach_throttle` for each unit it has to go less `approach_least`, at most `bay_speed`, and -/// stops within `on_pad_near` of it. There it turns straight along the bay and level -/// (`alignInputs`), and once it is, it stands on the pad, which it rides (`hold`). `pad_settle` -/// ticks on, the pad lowers, sounding the ship's landing, and `lowered_wait` ticks after that the -/// mission is over. -/// -/// **Improvement:** the angles come from `std.math.atan2` rather than the engine's table -/// (`sr_atan2`), as they do elsewhere in OpenReliant. -fn yamatoUpdate(ctx: Context, index: u16) void { - const world = ctx.world; - const all = world.objects; - if (index != all.player) return aigeneric.end(ctx, index); - const slot = &all.slots[index]; - const object = &slot.object; - const state = &slot.state.land; - const now = world.clock.frame_start; - const bay = &all.slots[create.cutaway_slot]; - switch (state.step.as(YamatoStep)) { - .waiting => { - if (now < state.due) return input.playerControlOrder(ctx, index); - yamatoCutaway(ctx, index); - state.step = .of(YamatoStep.descending); - state.due = now + slide_after; - slot.motion = .plain; - object.flags.no_collisions = true; - }, - .descending => { - world.player.showing = .landing; - for (all.slots[0..all.count], 0..) |*each, at| if (at != index) { - each.object.flags.disabled = true; - }; - const hangar = partMiddle(bay, bay_hangar) orelse return; - const point = hangar.frame.point(hangar.middle - Vector{ 0, over_bay, 0 }); - _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); - object.throttle = bayThrottle(world, slot); - if (math.distance(point, object.nextPosition()) < over_bay_near) state.step = .of(YamatoStep.approaching); - }, - .approaching => { - const pad = partMiddle(bay, landing_pad) orelse return; - const point = pad.frame.point(pad.middle) + Vector{ 0, object.bounds_min.y, pad_ahead }; - const distance = math.distance(point, object.nextPosition()); - const lead = point + math.forward(object.root.next_orientation) * @as(Vector, @splat(pad_lead)); - _ = ai.steer(world, index, lead, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); - object.throttle = @min(distance * approach_throttle - approach_least, bayThrottle(world, slot)); - if (distance < on_pad_near) state.step = .of(YamatoStep.aligning); - }, - .aligning => { - alignInputs(slot); - object.roll_input = 0; - object.throttle = 0; - if (@abs(object.yaw_input) >= aligned or @abs(object.pitch_input) >= aligned) return; - state.step = .of(YamatoStep.settling); - state.pad_due = now + pad_settle; - const model = if (bay.model) |*held| held else return; - if (model.rootChild(landing_pad) == null) return; - const standing: math.Place = .{ .position = object.nextPosition(), .orientation = object.root.next_orientation }; - const on_pad = standing.relativeTo(model.frameAt(landing_pad, bay.drawn)); - state.on_pad = on_pad.position; - state.pad_orientation = on_pad.orientation; - slot.riding = .{ .object = create.cutaway_slot, .part = landing_pad }; - }, - .settling => { - // No turns and no throttle while it waits on the pad (`0x0040F4A9`). - object.letGo(); - if (now <= state.pad_due) return; - state.step = .of(YamatoStep.lowering); - }, - .lowering => { - // And again as the pad starts down (`0x0040F4F5`). - object.letGo(); - if (bay.model) |*model| if (model.rootChild(landing_pad) != null) model.playNamed(landing_pad, pad_track, 0, null, 1); - sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); - state.step = .of(YamatoStep.down); - state.pad_due = now + lowered_wait; - }, - .down => { - if (now <= state.pad_due) return; - if (world.variables) |variables| variables.mission_over = 1; - }, - _ => {}, - } +/// Moves the object in `slot`, the carrier or the Yamato's landing bay, to `carrier_away`, turned +/// as the world is, as a landing's cutaway does. +pub fn moveAway(slot: *create.Slot) void { + objects.setPlace(&slot.object, &slot.drawn, .{ .position = carrier_away }); } -/// `land_yamato_cutaway` (`0x0040EBC0`): the cutaway of the Yamato's landing. The landing bay -/// moves to `carrier_away`, turned as the world is, its hangar lit through `bay_light_mask`. One -/// of its two views, picked at random, watches the ship, locked. The ship stands in the bay at -/// `stand_share` of the bay's bounds, looking at the hangar's middle, stopped, its throttle -/// `cutaway_throttle` and its power shared evenly. -/// -/// **Fix:** the game reads past the bay's list of parts where its model lacks the hangar; then -/// OpenReliant leaves the ship turned as it was. -/// -/// Left out: the view picked, which the game keeps (`0x0051863C`) and nothing reads. -fn yamatoCutaway(ctx: Context, index: u16) void { - const world = ctx.world; - const all = world.objects; - const slot = &all.slots[index]; - const object = &slot.object; - const bay = &all.slots[create.cutaway_slot]; - const away: math.Place = .{ .position = carrier_away }; - objects.setPlace(&bay.object, &bay.drawn, away); - if (bay.model) |*model| if (model.rootChild(bay_hangar) != null) { - model.parts[bay_hangar].object.light_mask = bay_light_mask; - }; - const pick: camera.View = if (world.random.oneIn(2)) .landing_bay else .landing_ship; - _ = camera.switchView(world, pick, index, true, true); - const max = bay.object.bounds_max.vector(); - const min = bay.object.bounds_min.vector(); - objects.setPosition(object, &slot.drawn, away.point(max * stand_share + min * (@as(Vector, @splat(1)) - stand_share))); - if (partMiddle(bay, bay_hangar)) |hangar| { - objects.setOrientation(object, &slot.drawn, math.lookAt(hangar.frame.point(hangar.middle) - object.nextPosition())); - } +/// Stops the ship in `object` and shares its power evenly, as a landing's cutaway does. +pub fn stopShip(object: *gameobj.GameObject) void { ai.stop(object); - object.throttle = cutaway_throttle; object.gun_factor = 1; object.speed_factor = 1; object.shield_factor = 1; } -/// OpenReliant's own: the first step of a landing in a bay. Aimed at the carrier as a whole, as -/// the player's request aims it, the landing is due `wait` ticks on, as the Reliant's is; aimed at -/// a gate, as a mission's `SetAI` aims it at one of the carrier's components, it begins at once, -/// so that the mission lands the ship at its own moment. A ship sent home for its friendly fire -/// lands at once too. A carrier whose model has no launch point for the gate is one that nothing -/// lands on. -fn bayInit(ctx: Context, index: u16) void { - const all = ctx.world.objects; - const slot = &all.slots[index]; - const state = &slot.state.land; - const target = slot.orders[0].target; - const carrier = target.slotIn(all) orelse return; - if (launch.gateOf(all, carrier, bayGate(target)) == null) { - state.style = .none; - log.warn("object {d} cannot land on what its order aims at: it has no launch point {d}", .{ index, bayGate(target) }); - return; - } - state.step = .of(BayStep.waiting); - const at_once = ctx.world.player.ending.sentHome() or !target.isWhole(); - state.due = ctx.world.clock.frame_start + if (at_once) 0 else wait; -} - -/// The gate a landing aims at: the component of its order's target, or gate 0 for the carrier as a -/// whole. -fn bayGate(target: aigeneric.Target) i16 { - return if (target.isWhole()) 0 else target.component; -} - -/// The doors of the gate the landing of the ship in `slot` aims at (`bay_launch.Doors`). -fn bayDoors(all: *const create.Objects, slot: *const create.Slot) bay_launch.Doors { - const target = slot.orders[0].target; - const carrier = target.slotIn(all) orelse return .{}; - return .of(all.slots[carrier].object.type, bayGate(target)); -} - -/// OpenReliant's own: a step of a landing in a bay (`BayStep`). A ship other than the player's -/// lets its landing go at once, as the Yamato's does. -/// -/// While the landing is not yet due, the player's ship flies by the player's controls. Then the -/// cutaway begins (`bayCutaway`): the mission's scene becomes the landing's, every object but the -/// ship and the carrier is disabled, the ship flies by its nose (`motion.Motion.plain`), passing -/// through everything, and the gate's doors open as the launch opens them -/// (`bay_launch.playDoors`). The ship steers at the gate's launch point, upright, its throttle -/// `approach_throttle` for each unit it has to go, at most the one that flies it at `bay_speed`, -/// and stops within `near` of the point, sounding its landing; the doors close behind it, and -/// `landed_wait` ticks later the mission is over. -fn bayUpdate(ctx: Context, index: u16) void { - const world = ctx.world; - const all = world.objects; - if (index != all.player) return aigeneric.end(ctx, index); - const slot = &all.slots[index]; - const object = &slot.object; - const state = &slot.state.land; - const carrier = slot.orders[0].target.slotIn(all) orelse return aigeneric.end(ctx, index); - const now = world.clock.frame_start; - switch (state.step.as(BayStep)) { - .waiting => { - if (now < state.due) return input.playerControlOrder(ctx, index); - bayCutaway(ctx, index, carrier); - world.player.showing = .landing; - for (all.slots[0..all.count], 0..) |*each, at| each.object.flags.disabled = at != index and at != carrier; - state.step = .of(BayStep.approaching); - slot.motion = .plain; - object.flags.no_collisions = true; - bay_launch.playDoors(world, &all.slots[carrier], bayDoors(all, slot), .open); - }, - .approaching => { - const point: Vector = state.gate_place; - _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); - const distance = math.distance(point, object.nextPosition()); - const most = if (ai.slotCruise(slot, world.view())) |cruise| bay_speed / cruise else approach_most; - object.throttle = @min(distance * approach_throttle, most); - if (distance >= near) return; - object.letGo(); - ai.stop(object); - sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); - bay_launch.playDoors(world, &all.slots[carrier], bayDoors(all, slot), .close); - state.step = .of(BayStep.landed); - state.due = now + landed_wait; - log.debug("object {d} is down at its gate's launch point at tick {d}", .{ index, now }); - }, - .landed => { - if (state.due >= now) return; - if (world.variables) |variables| variables.mission_over = 1; - }, - _ => {}, - } -} - -/// OpenReliant's own: the cutaway of a landing in a bay, as the Reliant's (`cutaway`). One of the -/// Reliant's two views, picked at random, watches the ship, locked, standing by the bay -/// (`camera.Landing.bay`). The carrier moves to `carrier_away`, turned as the world is, and the -/// launch point of the gate is worked out there (`launch.gateOf`), with the mouth of the bay -/// (`bayMouth`). The ship stands `bay_approach` out from the mouth along the way it launches, -/// looking in at the launch point, stopped, its power shared evenly. The carrier's orders are all -/// popped, and it stops. -fn bayCutaway(ctx: Context, index: u16, carrier: u16) void { - const world = ctx.world; - const all = world.objects; - const slot = &all.slots[index]; - const state = &slot.state.land; - const pick: camera.View = if (world.random.oneIn(2)) .landing_aside else .landing_tube; - _ = camera.switchView(world, pick, index, true, true); - const holder = &all.slots[carrier]; - objects.setPlace(&holder.object, &holder.drawn, .{ .position = carrier_away, .orientation = math.identity }); - const gate = (launch.gateOf(all, carrier, bayGate(slot.orders[0].target)) orelse return).place; - state.gate_place = gate.position; - state.gate_orientation = gate.orientation; - const mouth = bayMouth(&holder.object, gate); - state.mouth = mouth; - const object = &slot.object; - const approach = bayApproach(math.distance(gate.position, mouth), object.radius); - objects.setPosition(object, &slot.drawn, mouth + math.forward(gate.orientation) * @as(Vector, @splat(approach))); - objects.setOrientation(object, &slot.drawn, math.lookAt(gate.position - object.nextPosition())); - ai.stop(object); - object.gun_factor = 1; - object.speed_factor = 1; - object.shield_factor = 1; +/// Pops all the orders of the carrier in slot `carrier` and stops it, as the Reliant's and a bay's +/// cutaways do. +pub fn stopCarrier(ctx: Context, carrier: u16) void { aigeneric.popAll(ctx, carrier); - ai.stop(&holder.object); - const set = object.nextPosition(); - log.debug("object {d} of radius {d:.0} lands at gate {d} of object {d}: the launch point at ({d:.0}, {d:.0}, {d:.0}), the bay's mouth at ({d:.0}, {d:.0}, {d:.0}), the ship set at ({d:.0}, {d:.0}, {d:.0})", .{ - index, object.radius, bayGate(slot.orders[0].target), carrier, - gate.position[0], gate.position[1], gate.position[2], mouth[0], - mouth[1], mouth[2], set[0], set[1], - set[2], - }); -} - -/// The mouth of the bay a ship launches from the launch point at `gate` of `carrier`: where the -/// way out from the point, along the Z axis of its place, leaves the carrier's bounds -/// (`GameObject.bounds_min`, `bounds_max`), in the world. The point itself where the way out is -/// already outside them, or runs along none of their axes. -fn bayMouth(carrier: *const gameobj.GameObject, gate: math.Place) Vector { - const frame: math.Place = .{ .position = carrier.nextPosition(), .orientation = carrier.root.next_orientation }; - const from = frame.inverse(gate.position); - const way = math.transformTransposed(frame.orientation, math.forward(gate.orientation)); - const min = carrier.bounds_min.vector(); - const max = carrier.bounds_max.vector(); - var reach = std.math.inf(f32); - inline for (0..3) |axis| if (@abs(way[axis]) > std.math.floatEps(f32)) { - const edge = if (way[axis] > 0) max[axis] else min[axis]; - reach = @min(reach, (edge - from[axis]) / way[axis]); - }; - if (!std.math.isFinite(reach) or reach < 0) reach = 0; - return frame.point(from + way * @as(Vector, @splat(reach))); -} - -/// Where part `part` of the object in `slot` stands, as its model places it, and the middle of the -/// bounds of its finest level (`objects.Model.partBounds`), in its own frame; null where the model -/// lacks the part. -fn partMiddle(slot: *create.Slot, part: usize) ?struct { frame: math.Place, middle: Vector } { - const model = if (slot.model) |*held| held else return null; - const middle = model.boundsMiddle(part) orelse return null; - return .{ .frame = model.frameAt(part, slot.drawn), .middle = middle }; -} - -/// The throttle at which the ship in `slot` flies at `bay_speed`, by its cruise speed -/// (`object_cruise_speed`); none for a ship with no flight stats. -fn bayThrottle(world: gameobj.World, slot: *const create.Slot) f32 { - const cruise = ai.slotCruise(slot, world.view()) orelse return 0; - return bay_speed / cruise; + ai.stop(&ctx.world.objects.slots[carrier].object); } -/// The inputs that turn the ship in `slot` straight along the bay and level (`0x0040F26C`): its -/// yaw and its pitch, each the angle off the bay's Z axis less `align_damping` times its rate of -/// turn, times `align_gain` over its type's rate. -/// -/// **Fix:** the game follows a null pointer for a ship with no flight stats; OpenReliant leaves -/// its inputs as they are. -fn alignInputs(slot: *create.Slot) void { - const flight = slot.flight orelse return; - const object = &slot.object; - const forward = math.forward(object.root.next_orientation); - object.yaw_input = (-std.math.atan2(forward[0], forward[2]) - align_damping * object.yaw_rate) * align_gain / flight.yaw_rate; - object.pitch_input = (std.math.atan2(forward[1], forward[2]) - align_damping * object.pitch_rate) * align_gain / flight.pitch_rate; -} - -/// Each frame's placing of a ship on the Yamato's pad (`main.frameObjects`): from -/// `YamatoStep.settling` on, the ship stands where it came to rest on the pad, as the pad lowers. -/// Returns whether it placed the ship. -pub fn hold(all: *create.Objects, index: u16) bool { - const slot = &all.slots[index]; - if (slot.running(.land) == null) return false; - const state = &slot.state.land; - if (state.style != .yamato or !state.step.reached(YamatoStep.settling)) return false; - const pad = (slot.riding orelse return false).place(all) orelse return false; - const on_pad: math.Place = .{ .position = state.on_pad, .orientation = state.pad_orientation }; - objects.setPlace(&slot.object, &slot.drawn, on_pad.within(pad)); - return true; -} - -/// The frame the landing goes by: the middle of the tube the cutaway worked out (`State.tube`), -/// turned as `carrier` will be next. -fn tubeFrame(state: *const State, carrier: *const create.Slot) math.Place { - return .{ .position = state.tube, .orientation = carrier.object.root.next_orientation }; -} - -/// **Improvement:** the point the ship steers at as it approaches, in the tube's frame `frame` -/// (`tubeFrame`), so that it levels out as it comes rather than pitching as it arrives -/// (`Touchdown.level`). It stands over the tube's middle, at first at the game's height, -/// `over_tube`; as the ship nears, eased in and out over the way the cutaway set it to come, it -/// rises to the ship's own height, which it takes once the ship is over the tube. -fn flaredAim(frame: math.Place, object: *const gameobj.GameObject) Vector { - const off = frame.inverse(object.nextPosition()); - const across = @sqrt(off[0] * off[0] + off[2] * off[2]); - const share = std.math.clamp(1 - across / start_offset[2], 0, 1); - const eased = share * share * (3 - 2 * share); - return frame.point(.{ 0, math.lerp(over_tube[1], off[1], eased), 0 }); +/// Makes the mission's scene the landing's, as the Reliant's and a bay's landings begin: every +/// object but the ship in slot `index` and the carrier in slot `carrier` is disabled, and those two +/// are enabled. +pub fn showLanding(world: gameobj.World, index: u16, carrier: u16) void { + world.player.showing = .landing; + const all = world.objects; + for (all.slots[0..all.count], 0..) |*each, at| each.object.flags.disabled = at != index and at != carrier; } -/// Whether a ship whose top stands `top` along the Y axis of the tube's frame `frame` (`tubeFrame`) -/// is below the underside of the tube's upper door, as the door's finest level bounds it. -/// False where the Reliant's model lacks the door. -fn belowDoor(reliant: *const create.Slot, frame: math.Place, top: f32) bool { - const model = if (reliant.model) |*held| held else return false; - const bounds = model.partBounds(upper_door) orelse return false; - const middle = model.boundsMiddle(upper_door) orelse return false; - const underside = model.frameAt(upper_door, reliant.drawn).point(.{ middle[0], bounds[1][1], middle[2] }); - return top > frame.inverse(underside)[1]; +/// Ends the mission (`vm.Variables.mission_over`) once the frame's tick is past `due`, as each +/// landing's last step does. +pub fn endAfter(world: gameobj.World, due: i32) void { + if (world.clock.frame_start <= due) return; + if (world.variables) |variables| variables.mission_over = 1; } -/// Plays the upper door's track (`door_track`) from `from` at `speed`, sounding `which` where the -/// door stands, facing as it does; nothing where the Reliant's model lacks the door. -fn playDoor(world: gameobj.World, reliant: *create.Slot, which: sound3d.sounds.Sound, from: f32, speed: f32) void { - const model = if (reliant.model) |*held| held else return; - if (model.rootChild(upper_door) == null) return; - sound3d.playFrom(world, model.frameAt(upper_door, reliant.drawn), which, .guaranteed); - model.playNamed(upper_door, door_track, from, null, speed); +/// The ship the player's ship can land on: the player's carrier (`gameobj.Player.carrier`), where +/// it offers a landing (`create.Slot.landing`); null where it offers none, or there is no +/// carrier. PERMISSION TO LAND (`radio.permissionToLand`) and the friendly fire's sending home +/// (`friendly_fire.update`) land the ship on it. +pub fn landingCarrier(world: gameobj.World) ?u16 { + const carrier = world.player.carrier orelse return null; + return if (world.objects.slots[carrier].landing != null) carrier else null; } -/// What the landing's views stand by where the object in slot `index` is landing: on the Reliant -/// (`camera.View.landing_tube`, `landing_aside`), the middle of the tube its state keeps and how -/// its carrier is turned as it is drawn; on the Yamato (`camera.View.landing_bay`), where its -/// landing bay stands, the bounds of the bay's hangar, and the tick its view slides from; in a -/// bay (the Reliant's two views again), the launch point's place and the doors' middle its state -/// keeps. +/// What the landing's views stand by where the object in slot `index` is landing, by its style: +/// on the Reliant, the tube its state keeps (`reliant.seen`); on the Yamato, its landing bay +/// (`yamato.seen`); in a bay, the place it lands at and the bay's mouth (`bay.seen`). pub fn seen(all: *const create.Objects, index: u16) ?camera.Landing { if (index >= all.slots.len) return null; const slot = &all.slots[index]; const entry = slot.running(.land) orelse return null; const state = &slot.state.land; - switch (state.style) { - .reliant => { - const carrier = entry.target.slotIn(all) orelse return null; - return .{ .reliant = .{ .tube = state.tube, .carrier = all.slots[carrier].drawn.orientation } }; - }, - .bay => return .{ .bay = .{ - .gate = .{ .position = state.gate_place, .orientation = state.gate_orientation }, - .mouth = state.mouth, - } }, - .yamato => { - const bay = &all.slots[create.cutaway_slot]; - const model = if (bay.model) |*held| held else return null; - const hangar = model.partBounds(bay_hangar) orelse return null; - return .{ .yamato = .{ .bay = bay.drawn, .hangar = hangar, .due = state.due } }; - }, - .none, _ => return null, - } + return switch (state.landing() orelse return null) { + .reliant => reliant.seen(all, entry.target, state), + .yamato => yamato.seen(all, state), + .bay, .badanov => bay.seen(state), + }; +} + +test { + std.testing.refAllDecls(@This()); } test "Step.reached" { - const settling: Step = .of(YamatoStep.settling); - try std.testing.expect(settling.reached(YamatoStep.settling)); - try std.testing.expect(settling.reached(YamatoStep.waiting)); - try std.testing.expect(!Step.of(YamatoStep.waiting).reached(YamatoStep.settling)); + const settling: Step = .of(yamato.Step.settling); + try std.testing.expect(settling.reached(yamato.Step.settling)); + try std.testing.expect(settling.reached(yamato.Step.waiting)); + try std.testing.expect(!Step.of(yamato.Step.waiting).reached(yamato.Step.settling)); } test Style { + // The game lands on the Reliant and the Yamato alone. + try std.testing.expectEqual(Style.reliant, Style.original(.reliant).?); + try std.testing.expectEqual(Style.yamato, Style.original(.yamato).?); + for ([_]gameobj.GameType{ .victorious, .badanov, .krasny, .predator }) |other| { + try std.testing.expectEqual(null, Style.original(other)); + try std.testing.expectEqual(null, Style.of(.of(other))); + } try std.testing.expectEqual(Style.reliant, Style.of(.of(.reliant)).?); - try std.testing.expectEqual(Style.yamato, Style.of(.of(.yamato)).?); - // A ship that ships launch from as a hangar bay is landed on as one, as is any ship without a - // launch style of its own, whose init then looks for a launch point; one with its own style - // is not landed on. - try std.testing.expectEqual(Style.bay, Style.of(.of(.victorious)).?); - try std.testing.expectEqual(Style.bay, Style.of(.of(.predator)).?); - try std.testing.expectEqual(null, Style.of(.of(.badanov))); -} - -test "the player's ship lands on the Reliant, and the mission is over" { - var mission: gameobj.testing.Mission = undefined; - try mission.init(std.testing.allocator); - defer mission.deinit(); - var variables: @import("../vm.zig").Variables = .{}; - var world = mission.world(); - world.variables = &variables; - const ctx: Context = .of(world); - const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); - const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); - const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); - const slot = mission.slot(player); - const state = &slot.state.land; - - // The Reliant's style, due `wait` ticks on, until which the player flies on. - mission.clock.frame_start = 1000; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(Style.reliant, state.style); - try std.testing.expectEqual(ReliantStep.waiting, state.step.as(ReliantStep)); - try std.testing.expectEqual(1000 + wait, state.due); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.waiting, state.step.as(ReliantStep)); - - // Then the cutaway: the Reliant stopped far away, the ship ahead of its tube and above it, - // flying by its nose and passing through everything, and every other object disabled. - mission.clock.frame_start = 1000 + wait; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.approaching, state.step.as(ReliantStep)); - try std.testing.expectEqual(@import("main.zig").Showing.landing, mission.player.showing); - try std.testing.expect(mission.slot(other).object.flags.disabled); - try std.testing.expect(!mission.slot(reliant).object.flags.disabled and !slot.object.flags.disabled); - try std.testing.expectEqual(carrier_away, mission.slot(reliant).object.nextPosition()); - try std.testing.expectEqual(0, mission.slot(reliant).object.order_count); - const tube: Vector = state.tube; - try std.testing.expectEqual(tube + start_offset, slot.object.nextPosition()); - try std.testing.expectEqual(@import("motion.zig").Motion.plain, slot.motion.?); - try std.testing.expect(slot.object.flags.no_collisions); - - // Far from the point over the tube, it approaches at its most; within `near`, it stops. - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(approach_most, slot.object.throttle); - objects.setPosition(&slot.object, &slot.drawn, tube + over_tube + Vector{ 0, 0, 150 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.settling, state.step.as(ReliantStep)); - try std.testing.expectEqual(0, slot.object.throttle); - - // It waits `settle` ticks, and sinks along its Y axis, slowing as it nears the tube's middle. - mission.ordersAfter(ctx, player, settle); - try std.testing.expectEqual(ReliantStep.settling, state.step.as(ReliantStep)); - mission.ordersAfter(ctx, player, 1); - try std.testing.expectEqual(ReliantStep.sinking, state.step.as(ReliantStep)); - try std.testing.expectEqual(@import("motion.zig").Motion.downward, slot.motion.?); - objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -1500, 0 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectApproxEqAbs(1500 * sink_throttle, slot.object.throttle, 1e-6); - objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -50, 0 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.down, state.step.as(ReliantStep)); - try std.testing.expectEqual(0, slot.object.throttle); - - // `landed_wait` ticks after it is down, the mission is over. - mission.ordersAfter(ctx, player, landed_wait); - try std.testing.expectEqual(0, variables.mission_over); - mission.ordersAfter(ctx, player, 1); - try std.testing.expectEqual(1, variables.mission_over); -} - -test flaredAim { - var object = std.mem.zeroes(gameobj.GameObject); - const frame: math.Place = .{ .position = .{ 0, 0, 1000 } }; - const tube = frame.position; - // Where the cutaway sets the ship, the game's point over the tube. - object.root.next_position = .of(tube + start_offset); - try std.testing.expectEqual(tube + over_tube, flaredAim(frame, &object)); - // Half way there, the point has risen half way to the ship's height. - object.root.next_position = .of(tube + Vector{ 0, -4000, start_offset[2] / 2 }); - try std.testing.expectApproxEqAbs(-3500, flaredAim(frame, &object)[1], 1e-2); - // Over the tube's middle, at the ship's own height, so that it flies level. - object.root.next_position = .of(tube + Vector{ 0, -3800, 0 }); - try std.testing.expectApproxEqAbs(-3800, flaredAim(frame, &object)[1], 1e-2); -} - -test "the ship lands in the Reliant's first tube, and its upper door closes over it" { - const gpa = std.testing.allocator; - var mission: gameobj.testing.Mission = undefined; - try mission.init(gpa); - defer mission.deinit(); - var reliant_model: reliant_launch.testing.Reliant = undefined; - try reliant_model.init(gpa); - defer reliant_model.deinit(gpa); - var world = mission.world(); - world.touchdown = .level; - const ctx: Context = .of(world); - const player = try mission.add(.of(.predator), @splat(0)); - const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); - try reliant_model.fit(gpa, mission.slot(reliant)); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); - const slot = mission.slot(player); - const state = &slot.state.land; - aigeneric.objectOrders(ctx, player); - mission.clock.frame_start = wait + 1; - aigeneric.objectOrders(ctx, player); - - // The cutaway works out the tube's middle where it has moved the carrier, turned as the world - // is: halfway between the lower door, part 0, and the upper door, part 6, 500 above it. - const tube: Vector = state.tube; - try std.testing.expectEqual(carrier_away + Vector{ 0, -250, 0 }, tube); - try std.testing.expectEqual(tube + start_offset, slot.object.nextPosition()); - - // Sinking, the door stays open while the ship's top is above the door's underside, 150 over - // the tube's middle; once its top is below it, the door closes over it, and stays closing. - state.step = .of(ReliantStep.sinking); - slot.object.bounds_min.y = -20; - objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -1500, 0 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expect(!state.door_closing); - objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -120, 0 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.sinking, state.step.as(ReliantStep)); - try std.testing.expect(state.door_closing); - aigeneric.objectOrders(ctx, player); - try std.testing.expect(state.door_closing); -} - -test "a ship sent home for its friendly fire lands at once" { - var mission: gameobj.testing.Mission = undefined; - try mission.init(std.testing.allocator); - defer mission.deinit(); - const ctx = mission.orders(); - const player = try mission.add(.of(.predator), @splat(0)); - const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); - mission.player.ending = .friendly_fire; - mission.clock.frame_start = 1000; - aigeneric.objectOrders(ctx, player); - const state = &mission.slot(player).state.land; - try std.testing.expectEqual(1000, state.due); - try std.testing.expectEqual(ReliantStep.approaching, state.step.as(ReliantStep)); } -test "over the tube, a ship stops dead, or coasts as the game lets it" { - for ([_]Touchdown{ .level, .original }) |touchdown| { - var mission: gameobj.testing.Mission = undefined; - try mission.init(std.testing.allocator); - defer mission.deinit(); - var world = mission.world(); - world.touchdown = touchdown; - const ctx: Context = .of(world); - const player = try mission.add(.of(.predator), @splat(0)); - const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); - const slot = mission.slot(player); - const state = &slot.state.land; - aigeneric.objectOrders(ctx, player); - mission.clock.frame_start = wait + 1; - aigeneric.objectOrders(ctx, player); - objects.setPosition(&slot.object, &slot.drawn, @as(Vector, state.tube) + over_tube + Vector{ 0, 0, 150 }); - slot.object.velocity = .{ .x = 0, .y = 0, .z = -5 }; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(ReliantStep.settling, state.step.as(ReliantStep)); - try std.testing.expectEqual(@as(f32, if (touchdown == .level) 0 else -5), slot.object.velocity.z); - } +test State { + var state = std.mem.zeroes(State); + state.setLanding(.badanov); + try std.testing.expectEqual(Style.badanov, state.landing().?); + state.setLanding(null); + try std.testing.expectEqual(State.no_landing, state.style); + try std.testing.expectEqual(null, state.landing()); + // A number of no style, as what another order left in the state, is none. + state.style = 7; + try std.testing.expectEqual(null, state.landing()); } -test "a landing on what nothing lands on, or a ship's but the player's on the Yamato, is let go" { +test "a landing on a ship that offers none, or a ship's but the player's on the Yamato, is let go" { var mission: gameobj.testing.Mission = undefined; try mission.init(std.testing.allocator); defer mission.deinit(); @@ -1030,225 +319,54 @@ test "a landing on what nothing lands on, or a ship's but the player's on the Ya _ = try mission.add(.of(.predator), @splat(0)); const ship = try mission.add(.of(.predator), @splat(0)); try std.testing.expect(try aigeneric.push(ctx, ship, .fly_aimlessly, .none)); - // The Yamato lands the player's ship alone, the Badanov has a launch style of its own, and - // the Predator, landed on as a bay, has no model with a launch point. - for ([_]gameobj.Type{ .of(.yamato), .of(.badanov), .of(.predator) }) |carrier_type| { - const carrier = try mission.add(carrier_type, .{ 0, 0, 1000 }); + // The Yamato lands the player's ship alone, and the Badanov and the Victorious offer no landing + // unless a mod or a script gives them one. A Predator given a bay's landing has no launch point + // to land at, and one given the Reliant's has no launch tube. + const carriers = [_]struct { gameobj.Type, ?Style }{ + .{ .of(.yamato), null }, + .{ .of(.badanov), null }, + .{ .of(.victorious), null }, + .{ .of(.predator), .bay }, + .{ .of(.predator), .reliant }, + }; + for (carriers) |carrier_of| { + const carrier = try mission.add(carrier_of[0], .{ 0, 0, 1000 }); + if (carrier_of[1]) |given| mission.slot(carrier).landing = given; try std.testing.expect(try aigeneric.pushShip(ctx, ship, .land, carrier, null)); aigeneric.objectOrders(ctx, ship); try std.testing.expectEqual(.fly_aimlessly, mission.slot(ship).current().?.order); } } -test "the player's ship lands in a hangar bay of its carrier, and the mission is over" { - const gpa = std.testing.allocator; +test endAfter { var mission: gameobj.testing.Mission = undefined; - try mission.init(gpa); - defer mission.deinit(); - var bremen_model: bay_launch.testing.Bremen = undefined; - bremen_model.init(); - var variables: @import("../vm.zig").Variables = .{}; - var world = mission.world(); - world.variables = &variables; - const ctx: Context = .of(world); - const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); - const bremen = try mission.add(.of(.bremen), .{ 0, 0, 50000 }); - try bremen_model.parts.fit(gpa, mission.slot(bremen)); - const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, bremen, null)); - const slot = mission.slot(player); - const state = &slot.state.land; - - // The bay's style. Aimed at the carrier as a whole, as the player's request aims it, the - // landing is due `wait` ticks on, until which the player flies on. - mission.clock.frame_start = 1000; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(Style.bay, state.style); - try std.testing.expectEqual(BayStep.waiting, state.step.as(BayStep)); - try std.testing.expectEqual(1000 + wait, state.due); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(BayStep.waiting, state.step.as(BayStep)); - - // Then the cutaway: the carrier stopped far away, gate 0's doors opening, the ship out from - // the bay's mouth looking in at the gate's launch point, flying by its nose and passing - // through everything, and every other object disabled. The point lies 100 along the carrier's - // Z axis, and the carrier's bounds reach 400 along it, where the bay's mouth is. - mission.slot(bremen).object.bounds_min = .of(.{ -300, -300, -300 }); - mission.slot(bremen).object.bounds_max = .of(.{ 300, 300, 400 }); - mission.clock.frame_start = 1000 + wait; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(BayStep.approaching, state.step.as(BayStep)); - try std.testing.expectEqual(@import("main.zig").Showing.landing, mission.player.showing); - try std.testing.expect(mission.slot(other).object.flags.disabled); - try std.testing.expect(!mission.slot(bremen).object.flags.disabled and !slot.object.flags.disabled); - try std.testing.expectEqual(carrier_away, mission.slot(bremen).object.nextPosition()); - try std.testing.expectEqual(0, mission.slot(bremen).object.order_count); - const gate = carrier_away + Vector{ 0, 0, 100 }; - try std.testing.expectEqual(gate, @as(Vector, state.gate_place)); - try std.testing.expectEqual(carrier_away + Vector{ 0, 0, 400 }, @as(Vector, state.mouth)); - // The bay is 300 deep, so the ship starts six tenths of that out from the mouth, or twenty of - // its radii where that is more. - const approach = bayApproach(300, slot.object.radius); - try std.testing.expectEqual(carrier_away + Vector{ 0, 0, 400 + approach }, slot.object.nextPosition()); - try std.testing.expectEqual(180, bayApproach(300, 0)); - try std.testing.expectEqual(16000, bayApproach(300, 800)); - const facing = math.forward(slot.object.root.next_orientation); - try std.testing.expectApproxEqAbs(0, facing[0], 1e-5); - try std.testing.expectApproxEqAbs(0, facing[1], 1e-5); - try std.testing.expectApproxEqAbs(-1, facing[2], 1e-5); - try std.testing.expectEqual(@import("motion.zig").Motion.plain, slot.motion.?); - try std.testing.expect(slot.object.flags.no_collisions); - const doors = mission.slot(bremen).model.?.parts; - try std.testing.expectEqual(bay_launch.door_speed, doors[6].animation.speed); - try std.testing.expectEqual(bay_launch.door_speed, doors[7].animation.speed); - - // Far from the point, it flies in at the throttle that flies it at `bay_speed` by its cruise - // speed; nearer, at `approach_throttle` for each unit it has to go; and within `near`, it - // stops, sounding its landing, and the doors close behind it. - objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 50000 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectApproxEqAbs(bay_speed / gameobj.testing.flight.max_speed, slot.object.throttle, 1e-6); - objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 1000 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectApproxEqAbs(1000 * approach_throttle, slot.object.throttle, 1e-6); - objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 150 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(BayStep.landed, state.step.as(BayStep)); - try std.testing.expectEqual(0, slot.object.throttle); - try std.testing.expectEqual(-bay_launch.door_speed, doors[6].animation.speed); - - // `landed_wait` ticks on, the mission is over. - mission.ordersAfter(ctx, player, landed_wait); - try std.testing.expectEqual(0, variables.mission_over); - mission.ordersAfter(ctx, player, 1); - try std.testing.expectEqual(1, variables.mission_over); -} - -test "a landing a mission aims at a gate begins at once, at that gate" { - const gpa = std.testing.allocator; - var mission: gameobj.testing.Mission = undefined; - try mission.init(gpa); - defer mission.deinit(); - var bremen_model: bay_launch.testing.Bremen = undefined; - bremen_model.init(); - const ctx = mission.orders(); - const player = try mission.add(.of(.predator), @splat(0)); - const bremen = try mission.add(.of(.bremen), .{ 0, 0, 50000 }); - try bremen_model.parts.fit(gpa, mission.slot(bremen)); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, bremen, 1)); - mission.clock.frame_start = 1000; - aigeneric.objectOrders(ctx, player); - const state = &mission.slot(player).state.land; - try std.testing.expectEqual(1000, state.due); - try std.testing.expectEqual(BayStep.approaching, state.step.as(BayStep)); - // Gate 1's launch point lies 200 along the carrier's Z axis. - try std.testing.expectEqual(carrier_away + Vector{ 0, 0, 200 }, @as(Vector, state.gate_place)); -} - -test "the player's ship lands on a pad of the Yamato's landing bay, and the mission is over" { - const gpa = std.testing.allocator; - var mission: gameobj.testing.Mission = undefined; - try mission.init(gpa); + try mission.init(std.testing.allocator); defer mission.deinit(); - var bay_model: @import("launch.zig").testing.Bounded(bay_hangar + 1) = undefined; - const hangar_bounds: [2]Vector = .{ .{ -10000, -20000, -30000 }, .{ 10000, 0, 30000 } }; - try bay_model.init(gpa, hangar_bounds); - defer bay_model.deinit(gpa); var variables: @import("../vm.zig").Variables = .{}; var world = mission.world(); world.variables = &variables; - world.spawn = mission.spawn(create.testing.no_models); - const ctx: Context = .of(world); - const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); - const yamato = try mission.add(.of(.yamato), .{ 0, 0, 50000 }); - const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, yamato, null)); - const slot = mission.slot(player); - const state = &slot.state.land; - - // The Yamato's style, due `wait` ticks on, until which the player flies on; and the landing bay - // made in the cutaway slot, passing through everything. mission.clock.frame_start = 1000; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(Style.yamato, state.style); - try std.testing.expectEqual(YamatoStep.waiting, state.step.as(YamatoStep)); - try std.testing.expectEqual(1000 + wait, state.due); - const bay = mission.slot(create.cutaway_slot); - try std.testing.expectEqual(gameobj.Type.of(.yamato_landing_bay), bay.object.type); - try std.testing.expect(bay.object.flags.no_collisions); - try bay_model.parts.fit(gpa, bay); - bay.object.bounds_min = .of(hangar_bounds[0]); - bay.object.bounds_max = .of(hangar_bounds[1]); - - // Then the cutaway: the bay far away, the ship standing in it, flying by its nose through - // everything. - mission.clock.frame_start = 1000 + wait; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(YamatoStep.descending, state.step.as(YamatoStep)); - try std.testing.expectEqual(1000 + wait + slide_after, state.due); - try std.testing.expectEqual(carrier_away, bay.object.nextPosition()); - try std.testing.expectEqual(carrier_away + Vector{ -6000, -16000, -12000 }, slot.object.nextPosition()); - try std.testing.expectEqual(cutaway_throttle, slot.object.throttle); - try std.testing.expectEqual(@import("motion.zig").Motion.plain, slot.motion.?); - try std.testing.expect(slot.object.flags.no_collisions); - - // It flies over the hangar's middle, every other object disabled, and once near it, on. - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(@import("main.zig").Showing.landing, mission.player.showing); - try std.testing.expect(mission.slot(other).object.flags.disabled); - try std.testing.expectEqual(YamatoStep.descending, state.step.as(YamatoStep)); - const middle = carrier_away + Vector{ 0, -10000, 0 }; - objects.setPosition(&slot.object, &slot.drawn, middle - Vector{ 0, over_bay, 0 }); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(YamatoStep.approaching, state.step.as(YamatoStep)); - - // It flies to stand beyond the pad's middle, level with its underside, and stops near it. - objects.setPosition(&slot.object, &slot.drawn, middle + Vector{ 0, 0, pad_ahead }); - objects.setOrientation(&slot.object, &slot.drawn, math.identity); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(YamatoStep.aligning, state.step.as(YamatoStep)); - - // Straight along the bay and level, it stands on the pad, and rides it from then on. - mission.clock.frame_start = 3000; - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(YamatoStep.settling, state.step.as(YamatoStep)); - try std.testing.expectEqual(3000 + pad_settle, state.pad_due); - try std.testing.expectEqual(objects.NodeOf{ .object = create.cutaway_slot, .part = landing_pad }, slot.riding.?); - objects.setPosition(&slot.object, &slot.drawn, @splat(0)); - try std.testing.expect(hold(mission.objects, player)); - try std.testing.expectEqual(middle + Vector{ 0, 0, pad_ahead }, slot.object.nextPosition()); - - // `pad_settle` ticks on, the pad lowers, and `lowered_wait` ticks later the mission is over. - mission.ordersAfter(ctx, player, pad_settle + 1); - try std.testing.expectEqual(YamatoStep.lowering, state.step.as(YamatoStep)); - aigeneric.objectOrders(ctx, player); - try std.testing.expectEqual(YamatoStep.down, state.step.as(YamatoStep)); - mission.ordersAfter(ctx, player, lowered_wait); + endAfter(world, 1000); try std.testing.expectEqual(0, variables.mission_over); - mission.ordersAfter(ctx, player, 1); + mission.clock.frame_start = 1001; + endAfter(world, 1000); try std.testing.expectEqual(1, variables.mission_over); } -test seen { +test landingCarrier { var mission: gameobj.testing.Mission = undefined; try mission.init(std.testing.allocator); defer mission.deinit(); - const ctx = mission.orders(); - const player = try mission.add(.of(.predator), @splat(0)); - const reliant = try mission.add(.of(.reliant), .{ 0, 0, 1000 }); - try std.testing.expectEqual(null, seen(mission.objects, player)); - try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); - mission.slot(player).state.land.style = .reliant; - mission.slot(player).state.land.tube = .{ 1, 2, 3 }; - const landing = seen(mission.objects, player).?.reliant; - try std.testing.expectEqual(Vector{ 1, 2, 3 }, landing.tube); - try std.testing.expectEqual(mission.slot(reliant).drawn.orientation, landing.carrier); - // In a bay, the launch point's place and the bay's mouth the landing keeps. - mission.slot(player).state.land.style = .bay; - mission.slot(player).state.land.gate_place = .{ 4, 5, 6 }; - mission.slot(player).state.land.gate_orientation = math.identity; - mission.slot(player).state.land.mouth = .{ 7, 8, 9 }; - const in_bay = seen(mission.objects, player).?.bay; - try std.testing.expectEqual(Vector{ 4, 5, 6 }, in_bay.gate.position); - try std.testing.expectEqual(Vector{ 7, 8, 9 }, in_bay.mouth); + _ = try mission.add(.of(.predator), @splat(0)); + const reliant_carrier = try mission.add(.of(.reliant), .{ 0, 0, 1000 }); + const victorious = try mission.add(.of(.victorious), .{ 0, 0, 2000 }); + const world = mission.world(); + try std.testing.expectEqual(null, landingCarrier(world)); + mission.player.carrier = reliant_carrier; + try std.testing.expectEqual(reliant_carrier, landingCarrier(world).?); + // The Victorious offers no landing until a script gives it one. + mission.player.carrier = victorious; + try std.testing.expectEqual(null, landingCarrier(world)); + mission.slot(victorious).landing = .bay; + try std.testing.expectEqual(victorious, landingCarrier(world).?); } diff --git a/src/engine/game/ailand/bay.zig b/src/engine/game/ailand/bay.zig new file mode 100644 index 00000000..cfd47469 --- /dev/null +++ b/src/engine/game/ailand/bay.zig @@ -0,0 +1,417 @@ +//! OpenReliant's own landings in a bay: the ship flies in at the doors of a gate and comes to rest +//! where a ship waits to launch through that gate. In a hangar bay (`ailand.Style.bay`) that is the +//! gate's launch point (`launch.gateOf`); in the Badanov's or the Krasny's bay +//! (`ailand.Style.badanov`), the place their launch sets a ship at (`launch.badanov.gatePlace`). +//! +//! **Improvement:** the game has no landing in a bay. + +const std = @import("std"); +const log = std.log.scoped(.orders); + +const math = @import("../../surrender/math.zig"); +const Vector = math.Vector; +const ai = @import("../ai.zig"); +const aigeneric = @import("../aigeneric.zig"); +const Context = aigeneric.Context; +const ailand = @import("../ailand.zig"); +const State = ailand.State; +const camera = @import("../camera.zig"); +const create = @import("../create.zig"); +const gameobj = @import("../gameobj.zig"); +const input = @import("../../input.zig"); +const launch = @import("../launch.zig"); +const badanov_launch = @import("../launch/badanov.zig"); +const bay_launch = @import("../launch/bay.zig"); +const objects = @import("../objects.zig"); +const sound3d = @import("../sound3d.zig"); +const yamato = @import("yamato.zig"); + +/// The steps of a landing in a bay (`update`). +pub const Step = enum(u32) { + /// The player flies on until the landing is due; then the cutaway begins (`cutaway`), and the + /// gate's doors open. + waiting = 0, + /// The ship flies in at the doors to where it lands, slowing as it nears, and stops within + /// `ailand.near` of it; the doors close behind it. + approaching = 1, + /// It waits `ailand.landed_wait` ticks there, and the mission is over. + landed = 2, + _, +}; + +/// The bay a landing goes into, by its style. +const Bay = enum { + /// A hangar bay: to its gate's launch point (`launch.gateOf`), through the gate's doors + /// (`launch.bay.Doors`). + hangar, + /// The Badanov's or the Krasny's: to where a ship waits at the gate to launch + /// (`launch.badanov.gatePlace`), through their two doors (`launch.badanov.landing_doors`). + badanov, + + fn of(state: *const State) Bay { + return if (state.landing() == .badanov) .badanov else .hangar; + } +}; + +/// How far out from the mouth of the bay the cutaway sets the ship, along the way it launches: +/// this share of the bay's depth, from where the ship lands to the mouth, and at least this many +/// times the ship's radius, so that a shallow bay still gives the ship a way to fly in on. They go +/// by the bay and the ship rather than by a fixed distance, so that the landing fits any carrier. +const approach_share: f32 = 0.6; +const approach_least_radii: f32 = 20; + +/// How far out from the mouth of a bay `depth` deep the cutaway sets a ship of radius `radius` +/// (`approach_share`, `approach_least_radii`). +fn approach(depth: f32, radius: f32) f32 { + return @max(approach_share * depth, approach_least_radii * radius); +} + +/// The first step of a landing in a bay. Aimed at the carrier as a whole, as the player's request +/// aims it, the landing is due `ailand.wait` ticks on, as the Reliant's is. Aimed at a gate, as a +/// mission's `SetAI` aims it at one of the carrier's components, it begins at once, so that the +/// mission lands the ship at its own moment. A ship sent home for its friendly fire lands at once +/// too. Where the carrier's model has no place to land at the gate, the landing is none. +pub fn init(ctx: Context, index: u16) void { + const all = ctx.world.objects; + const slot = &all.slots[index]; + const state = &slot.state.land; + const target = slot.orders[0].target; + const carrier = target.slotIn(all) orelse return; + const kind: Bay = .of(state); + if (gatePlace(all, carrier, kind, aimedGate(target)) == null) { + state.setLanding(null); + switch (kind) { + .hangar => log.warn("object {d} cannot land on what its order aims at: it has no launch point {d}", .{ index, aimedGate(target) }), + .badanov => log.warn("object {d} cannot land on what its order aims at: it has no bay", .{index}), + } + return; + } + state.step = .of(Step.waiting); + const at_once = ctx.world.player.ending.sentHome() or !target.isWhole(); + state.due = ctx.world.clock.frame_start + if (at_once) 0 else ailand.wait; +} + +/// The gate a landing aims at: the component of its order's target, or gate 0 for the carrier as a +/// whole. +fn aimedGate(target: aigeneric.Target) i16 { + return if (target.isWhole()) 0 else target.component; +} + +/// Where a ship landing in `kind` of bay at gate `gate` of the carrier in slot `carrier` comes to +/// rest, and how it is turned there, in the world; null where the carrier's model has no such +/// place. +fn gatePlace(all: *const create.Objects, carrier: u16, kind: Bay, gate: i16) ?math.Place { + return switch (kind) { + .hangar => (launch.gateOf(all, carrier, gate) orelse return null).place, + .badanov => badanov_launch.gatePlace(&all.slots[carrier], gate), + }; +} + +/// The doors of the gate the landing of the ship in `slot` aims at. +fn doorsOf(all: *const create.Objects, slot: *const create.Slot) bay_launch.Doors { + const target = slot.orders[0].target; + const carrier = target.slotIn(all) orelse return .{}; + return switch (Bay.of(&slot.state.land)) { + .hangar => .of(all.slots[carrier].object.type, aimedGate(target)), + .badanov => badanov_launch.landing_doors, + }; +} + +/// A step of a landing in a bay (`Step`). A ship other than the player's lets its landing go at +/// once, as on the Yamato. +/// +/// While the landing is not yet due, the player's ship flies by the player's controls. Then the +/// cutaway begins (`cutaway`): the mission's scene becomes the landing's, every object but the +/// ship and the carrier is disabled, the ship flies by its nose (`motion.Motion.plain`), passing +/// through everything, and the gate's doors open as the launch opens them +/// (`launch.bay.playDoors`). The ship steers at where it lands, upright, its throttle +/// `ailand.approach_throttle` for each unit it has to go, at most the one that flies it at +/// `yamato.bay_speed`. It stops within `ailand.near` of the place, sounding its landing, the doors +/// close behind it, and `ailand.landed_wait` ticks later the mission is over. +pub fn update(ctx: Context, index: u16) void { + const world = ctx.world; + const all = world.objects; + if (index != all.player) return aigeneric.end(ctx, index); + const slot = &all.slots[index]; + const object = &slot.object; + const state = &slot.state.land; + const carrier = slot.orders[0].target.slotIn(all) orelse return aigeneric.end(ctx, index); + const now = world.clock.frame_start; + switch (state.step.as(Step)) { + .waiting => { + if (now < state.due) return input.playerControlOrder(ctx, index); + cutaway(ctx, index, carrier); + ailand.showLanding(world, index, carrier); + state.step = .of(Step.approaching); + slot.motion = .plain; + object.flags.no_collisions = true; + bay_launch.playDoors(world, &all.slots[carrier], doorsOf(all, slot), .open); + }, + .approaching => { + const point: Vector = state.gate_place; + _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); + const distance = math.distance(point, object.nextPosition()); + const most = if (ai.slotCruise(slot, world.view())) |cruise| yamato.bay_speed / cruise else ailand.approach_most; + object.throttle = @min(distance * ailand.approach_throttle, most); + if (distance >= ailand.near) return; + object.letGo(); + ai.stop(object); + sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); + bay_launch.playDoors(world, &all.slots[carrier], doorsOf(all, slot), .close); + state.step = .of(Step.landed); + state.due = now + ailand.landed_wait; + log.debug("object {d} is down at its gate at tick {d}", .{ index, now }); + }, + .landed => ailand.endAfter(world, state.due), + _ => {}, + } +} + +/// The cutaway of a landing in a bay, as the Reliant's (`ailand.reliant`). One of the Reliant's two +/// views, picked at random, watches the ship, locked, standing by the bay (`camera.Landing.bay`). +/// The carrier moves to `ailand.carrier_away`, turned as the world is, and where the ship lands is +/// worked out there (`gatePlace`), with the mouth of the bay (`mouth`). The ship stands `approach` +/// out from the mouth along the way a ship launches, looking in at where it lands, stopped, its +/// power shared evenly. The carrier's orders are all popped, and it stops. +fn cutaway(ctx: Context, index: u16, carrier: u16) void { + const world = ctx.world; + const all = world.objects; + const slot = &all.slots[index]; + const state = &slot.state.land; + ailand.watch(world, index, .{ .landing_aside, .landing_tube }); + const holder = &all.slots[carrier]; + ailand.moveAway(holder); + const gate = gatePlace(all, carrier, .of(state), aimedGate(slot.orders[0].target)) orelse return; + state.gate_place = gate.position; + state.gate_orientation = gate.orientation; + const at_mouth = mouth(&holder.object, gate); + state.mouth = at_mouth; + const object = &slot.object; + const out = approach(math.distance(gate.position, at_mouth), object.radius); + objects.setPosition(object, &slot.drawn, at_mouth + math.forward(gate.orientation) * @as(Vector, @splat(out))); + objects.setOrientation(object, &slot.drawn, math.lookAt(gate.position - object.nextPosition())); + ailand.stopShip(object); + ailand.stopCarrier(ctx, carrier); + const set = object.nextPosition(); + log.debug("object {d} of radius {d:.0} lands at gate {d} of object {d}: there at ({d:.0}, {d:.0}, {d:.0}), the bay's mouth at ({d:.0}, {d:.0}, {d:.0}), the ship set at ({d:.0}, {d:.0}, {d:.0})", .{ + index, object.radius, aimedGate(slot.orders[0].target), carrier, + gate.position[0], gate.position[1], gate.position[2], at_mouth[0], + at_mouth[1], at_mouth[2], set[0], set[1], + set[2], + }); +} + +/// The mouth of the bay a ship launches from `gate`, a place in `carrier`: where the way out from +/// it, along the Z axis of the place, leaves the carrier's bounds (`GameObject.bounds_min`, +/// `bounds_max`), in the world. The place itself where the way out is already outside them, or runs +/// along none of their axes. +fn mouth(carrier: *const gameobj.GameObject, gate: math.Place) Vector { + const frame: math.Place = .{ .position = carrier.nextPosition(), .orientation = carrier.root.next_orientation }; + const from = frame.inverse(gate.position); + const way = math.transformTransposed(frame.orientation, math.forward(gate.orientation)); + const min = carrier.bounds_min.vector(); + const max = carrier.bounds_max.vector(); + var reach = std.math.inf(f32); + inline for (0..3) |axis| if (@abs(way[axis]) > std.math.floatEps(f32)) { + const edge = if (way[axis] > 0) max[axis] else min[axis]; + reach = @min(reach, (edge - from[axis]) / way[axis]); + }; + if (!std.math.isFinite(reach) or reach < 0) reach = 0; + return frame.point(from + way * @as(Vector, @splat(reach))); +} + +/// What the landing's views stand by (`camera.Landing.bay`): where the ship lands and how it is +/// turned there, and the bay's mouth, as the landing's state keeps them. +pub fn seen(state: *const State) ?camera.Landing { + return .{ .bay = .{ + .gate = .{ .position = state.gate_place, .orientation = state.gate_orientation }, + .mouth = state.mouth, + } }; +} + +test "the player's ship lands in a hangar bay of its carrier, and the mission is over" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var bremen_model: bay_launch.testing.Bremen = undefined; + bremen_model.init(); + var variables: @import("../../vm.zig").Variables = .{}; + var world = mission.world(); + world.variables = &variables; + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); + const bremen = try mission.add(.of(.bremen), .{ 0, 0, 50000 }); + try bremen_model.parts.fit(gpa, mission.slot(bremen)); + // The Bremen offers no landing unless a mod or a script gives it one. + try std.testing.expectEqual(null, mission.slot(bremen).landing); + mission.slot(bremen).landing = .bay; + const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, bremen, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + + // The bay's style. Aimed at the carrier as a whole, as the player's request aims it, the + // landing is due `wait` ticks on, until which the player flies on. + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(ailand.Style.bay, state.landing().?); + try std.testing.expectEqual(Step.waiting, state.step.as(Step)); + try std.testing.expectEqual(1000 + ailand.wait, state.due); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.waiting, state.step.as(Step)); + + // Then the cutaway: the carrier stopped far away, gate 0's doors opening, the ship out from + // the bay's mouth looking in at the gate's launch point, flying by its nose and passing + // through everything, and every other object disabled. The point lies 100 along the carrier's + // Z axis, and the carrier's bounds reach 400 along it, where the bay's mouth is. + mission.slot(bremen).object.bounds_min = .of(.{ -300, -300, -300 }); + mission.slot(bremen).object.bounds_max = .of(.{ 300, 300, 400 }); + mission.clock.frame_start = 1000 + ailand.wait; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); + try std.testing.expectEqual(@import("../main.zig").Showing.landing, mission.player.showing); + try std.testing.expect(mission.slot(other).object.flags.disabled); + try std.testing.expect(!mission.slot(bremen).object.flags.disabled and !slot.object.flags.disabled); + try std.testing.expectEqual(ailand.carrier_away, mission.slot(bremen).object.nextPosition()); + try std.testing.expectEqual(0, mission.slot(bremen).object.order_count); + const gate = ailand.carrier_away + Vector{ 0, 0, 100 }; + try std.testing.expectEqual(gate, @as(Vector, state.gate_place)); + try std.testing.expectEqual(ailand.carrier_away + Vector{ 0, 0, 400 }, @as(Vector, state.mouth)); + // The bay is 300 deep, so the ship starts six tenths of that out from the mouth, or twenty of + // its radii where that is more. + const out = approach(300, slot.object.radius); + try std.testing.expectEqual(ailand.carrier_away + Vector{ 0, 0, 400 + out }, slot.object.nextPosition()); + try std.testing.expectEqual(180, approach(300, 0)); + try std.testing.expectEqual(16000, approach(300, 800)); + const facing = math.forward(slot.object.root.next_orientation); + try std.testing.expectApproxEqAbs(0, facing[0], 1e-5); + try std.testing.expectApproxEqAbs(0, facing[1], 1e-5); + try std.testing.expectApproxEqAbs(-1, facing[2], 1e-5); + try std.testing.expectEqual(@import("../motion.zig").Motion.plain, slot.motion.?); + try std.testing.expect(slot.object.flags.no_collisions); + const doors = mission.slot(bremen).model.?.parts; + try std.testing.expectEqual(bay_launch.door_speed, doors[6].animation.speed); + try std.testing.expectEqual(bay_launch.door_speed, doors[7].animation.speed); + + // Far from the point, it flies in at the throttle that flies it at `bay_speed` by its cruise + // speed; nearer, at `approach_throttle` for each unit it has to go; and within `near`, it + // stops, sounding its landing, and the doors close behind it. + objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 50000 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectApproxEqAbs(yamato.bay_speed / gameobj.testing.flight.max_speed, slot.object.throttle, 1e-6); + objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 1000 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectApproxEqAbs(1000 * ailand.approach_throttle, slot.object.throttle, 1e-6); + objects.setPosition(&slot.object, &slot.drawn, gate + Vector{ 0, 0, 150 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.landed, state.step.as(Step)); + try std.testing.expectEqual(0, slot.object.throttle); + try std.testing.expectEqual(-bay_launch.door_speed, doors[6].animation.speed); + + // `landed_wait` ticks on, the mission is over. + mission.ordersAfter(ctx, player, ailand.landed_wait); + try std.testing.expectEqual(0, variables.mission_over); + mission.ordersAfter(ctx, player, 1); + try std.testing.expectEqual(1, variables.mission_over); +} + +test "a landing a mission aims at a gate begins at once, at that gate" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var bremen_model: bay_launch.testing.Bremen = undefined; + bremen_model.init(); + const ctx = mission.orders(); + const player = try mission.add(.of(.predator), @splat(0)); + const bremen = try mission.add(.of(.bremen), .{ 0, 0, 50000 }); + try bremen_model.parts.fit(gpa, mission.slot(bremen)); + mission.slot(bremen).landing = .bay; + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, bremen, 1)); + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + const state = &mission.slot(player).state.land; + try std.testing.expectEqual(1000, state.due); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); + // Gate 1's launch point lies 200 along the carrier's Z axis. + try std.testing.expectEqual(ailand.carrier_away + Vector{ 0, 0, 200 }, @as(Vector, state.gate_place)); +} + +test "the player's ship lands in the Badanov's bay where its launch sets ships, and the mission is over" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var badanov_model: badanov_launch.testing.Badanov = undefined; + try badanov_model.init(gpa, badanov_launch.testing.bounds); + defer badanov_model.deinit(gpa); + var variables: @import("../../vm.zig").Variables = .{}; + var world = mission.world(); + world.variables = &variables; + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); + const badanov = try mission.add(.of(.badanov), .{ 0, 0, 50000 }); + try badanov_model.parts.fit(gpa, mission.slot(badanov)); + mission.slot(badanov).landing = .badanov; + mission.slot(badanov).object.bounds_min = .of(.{ -300, -300, -300 }); + mission.slot(badanov).object.bounds_max = .of(.{ 300, 300, 300 }); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, badanov, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(ailand.Style.badanov, state.landing().?); + try std.testing.expectEqual(1000 + ailand.wait, state.due); + + // At the cutaway, the ship is to come to rest where gate 0's launch sets a ship, as it stands + // where the cutaway has moved the carrier: 100 across, 40 up and 240 behind the bay's + // midpoint, facing out of the bay with its nose tilted down. It starts out beyond the mouth, + // looking in, and the Badanov's two doors open. + mission.clock.frame_start = 1000 + ailand.wait; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); + const there = badanov_launch.gatePlace(mission.slot(badanov), 0).?; + try std.testing.expectEqual(there.position, @as(Vector, state.gate_place)); + try std.testing.expectApproxEqAbs(100, state.gate_place[0] - ailand.carrier_away[0], 1e-3); + try std.testing.expectApproxEqAbs(40, state.gate_place[1] - ailand.carrier_away[1], 1e-3); + try std.testing.expectApproxEqAbs(-240, state.gate_place[2] - ailand.carrier_away[2], 1e-3); + // The way out leaves the carrier's bounds at its side, 300 across. + try std.testing.expectApproxEqAbs(300, state.mouth[0] - ailand.carrier_away[0], 1e-3); + const inward = math.forward(slot.object.root.next_orientation); + try std.testing.expect(math.dot(inward, math.forward(there.orientation)) < -0.99); + const doors = mission.slot(badanov).model.?.parts; + try std.testing.expectEqual(bay_launch.door_speed, doors[1].animation.speed); + try std.testing.expectEqual(bay_launch.door_speed, doors[2].animation.speed); + + // Within `near` of it, it stops, and the doors close behind it; the mission is over in time. + objects.setPosition(&slot.object, &slot.drawn, there.position + Vector{ 150, 0, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.landed, state.step.as(Step)); + try std.testing.expectEqual(-bay_launch.door_speed, doors[1].animation.speed); + mission.ordersAfter(ctx, player, ailand.landed_wait + 1); + try std.testing.expectEqual(1, variables.mission_over); +} + +test "a landing in the Badanov's bay is none where its model has no bay" { + var mission: gameobj.testing.Mission = undefined; + try mission.init(std.testing.allocator); + defer mission.deinit(); + const ctx = mission.orders(); + const player = try mission.add(.of(.predator), @splat(0)); + const badanov = try mission.add(.of(.badanov), .{ 0, 0, 1000 }); + mission.slot(badanov).landing = .badanov; + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, badanov, null)); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(null, mission.slot(player).running(.land)); +} + +test seen { + var state = std.mem.zeroes(State); + state.gate_place = .{ 4, 5, 6 }; + state.gate_orientation = math.identity; + state.mouth = .{ 7, 8, 9 }; + const in_bay = seen(&state).?.bay; + try std.testing.expectEqual(Vector{ 4, 5, 6 }, in_bay.gate.position); + try std.testing.expectEqual(Vector{ 7, 8, 9 }, in_bay.mouth); +} diff --git a/src/engine/game/ailand/reliant.zig b/src/engine/game/ailand/reliant.zig new file mode 100644 index 00000000..27fd4109 --- /dev/null +++ b/src/engine/game/ailand/reliant.zig @@ -0,0 +1,434 @@ +//! The Reliant's landing (`land_reliant_init`, `0x0040F5C0`, and `land_reliant_update`, +//! `0x0040F940`): the ship flies over the Reliant's first launch tube and sinks down it through the +//! tube's upper door. + +const std = @import("std"); +const log = std.log.scoped(.orders); + +const math = @import("../../surrender/math.zig"); +const Vector = math.Vector; +const ai = @import("../ai.zig"); +const aigeneric = @import("../aigeneric.zig"); +const Context = aigeneric.Context; +const ailand = @import("../ailand.zig"); +const State = ailand.State; +const camera = @import("../camera.zig"); +const create = @import("../create.zig"); +const gameobj = @import("../gameobj.zig"); +const input = @import("../../input.zig"); +const objects = @import("../objects.zig"); +const reliant_launch = @import("../launch/reliant.zig"); +const sound3d = @import("../sound3d.zig"); + +/// How the Reliant's landing brings the ship down. +pub const Touchdown = enum { + /// **Improvement:** the ship levels out as it approaches (`flaredAim`) and stops dead, so that + /// it sinks straight down the tube and comes to rest level in it; and once its top is below the + /// tube's upper door, the door closes over it, sounding (`update`). + level, + /// As the game lands it: pitched as it came, coasting as it stops, the door left open. + original, +}; + +/// The Reliant's steps. +pub const Step = enum(u32) { + /// The player flies on until the landing is due; then the cutaway begins, and the tube's upper + /// door opens. + waiting = 0, + /// The ship flies to `over_tube`, slowing as it nears, and stops within `ailand.near`. + approaching = 1, + /// Stopped, it waits `settle` ticks. + settling = 2, + /// It sinks into the tube, slowing as it goes, and is down within `down_at` of its middle. + sinking = 3, + /// Down, it waits `ailand.landed_wait` ticks, and the mission is over. + down = 4, + _, +}; + +/// The Reliant's launch tube the ship lands down, its first (`0x0040F600`), halfway between whose +/// doors' middles it lands (`launch.reliant.tubeMiddle`); and that tube's upper door, a part of the +/// Reliant's root's child list (`launch.reliant.Door`), which opens as the landing begins. +const first_tube = 0; +const upper_door = reliant_launch.Door.upper.part(first_tube); + +/// Where the cutaway sets the ship, in the carrier's frame from the tube's middle: ahead of it and +/// above it; and where the ship flies to, over the tube, where it first looks (`0x0040F600`). +const start_offset: Vector = .{ 0, -5000, 20000 }; +const over_tube: Vector = .{ 0, -3000, 0 }; + +/// The upper door's track (`0x004E2050`), and how fast it plays to open the door (`0x0040FA55`). +/// It plays back to close the door as fast as the launch's tube doors open +/// (`launch.reliant.door_speed`), OpenReliant's choice (`Touchdown.level`). +const door_track = "opendoor2"; +const door_speed: f32 = 1.23; + +/// How long the ship waits over the tube before it sinks, in ticks (`0x0040FB59`). +const settle: i32 = 50; + +/// The throttle the ship sinks with for each unit it has to go (`0x004DC538`), and how far over the +/// tube's middle it is down, along the carrier's Y axis, which points below it (`0x004DC534`). +const sink_throttle: f32 = 1.0 / 15000.0; +const down_at: f32 = -100; + +/// `land_reliant_init` (`0x0040F5C0`): the first step, due `ailand.wait` ticks on, or at once where +/// the player's ship is being sent home for its friendly fire. Where the carrier's model lacks the +/// doors of its first launch tube (`launch.reliant.tubeMiddle`), as a ship a mod or a script gives +/// the Reliant's landing can, the landing is none, and the log says so. +pub fn init(ctx: Context, index: u16) void { + const all = ctx.world.objects; + const state = &all.slots[index].state.land; + const carrier = all.slots[index].orders[0].target.slotIn(all) orelse return; + if (reliant_launch.tubeMiddle(&all.slots[carrier], first_tube, 0) == null) { + state.setLanding(null); + log.warn("object {d} cannot land on what its order aims at: it has no launch tube {d}", .{ index, first_tube }); + return; + } + state.step = .of(Step.waiting); + state.due = ctx.world.clock.frame_start + if (ctx.world.player.ending.sentHome()) 0 else ailand.wait; +} + +/// `land_reliant_update` (`0x0040F940`): a step of the Reliant's landing (`Step`). +/// +/// While the landing is not yet due, the player's ship flies by the player's controls +/// (`input.playerControlOrder`). Then the cutaway begins (`cutaway`): the mission's scene becomes +/// the landing's, every object but the ship and the carrier is disabled and those two are enabled +/// (`ailand.showLanding`), and the ship flies by its nose (`motion.Motion.plain`), passing through +/// everything. +/// The carrier's first tube's upper door opens, sounding where it is (`playDoor`). +/// +/// The ship steers at `over_tube`, its throttle `ailand.approach_throttle` for each unit it has to +/// go, at most `ailand.approach_most`, and stops within `ailand.near` of it, its turns nothing. It +/// waits `settle` ticks, then sinks along its own Y axis (`motion.Motion.downward`), its throttle +/// `sink_throttle` for each unit it stands from the tube's middle. Once it is less than `down_at` +/// over it, it stops, sounding its landing, and `ailand.landed_wait` ticks later the mission is +/// over (`vm.Variables`). +pub fn update(ctx: Context, index: u16) void { + const world = ctx.world; + const all = world.objects; + const slot = &all.slots[index]; + const object = &slot.object; + const state = &slot.state.land; + const carrier = slot.orders[0].target.slotIn(all) orelse return; + const now = ctx.world.clock.frame_start; + switch (state.step.as(Step)) { + .waiting => { + if (now < state.due and index == all.player) return input.playerControlOrder(ctx, index); + cutaway(ctx, index, carrier); + ailand.showLanding(world, index, carrier); + state.step = .of(Step.approaching); + slot.motion = .plain; + object.flags.no_collisions = true; + playDoor(world, &all.slots[carrier], .dooropen, 0, door_speed); + }, + .approaching => { + const frame = tubeFrame(state, &all.slots[carrier]); + const point = switch (world.touchdown) { + .level => flaredAim(frame, object), + .original => frame.point(over_tube), + }; + _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{}); + const distance = math.distance(point, object.nextPosition()); + object.throttle = @min(distance * ailand.approach_throttle, ailand.approach_most); + if (distance >= ailand.near) return; + object.letGo(); + if (world.touchdown == .level) ai.stop(object); + state.step = .of(Step.settling); + state.due = now + settle; + }, + .settling => { + if (state.due >= now) return; + state.step = .of(Step.sinking); + slot.motion = .downward; + }, + .sinking => { + const reliant = &all.slots[carrier]; + const frame = tubeFrame(state, reliant); + const off = frame.inverse(object.nextPosition()); + object.throttle = math.length(off) * sink_throttle; + // **Improvement:** the tube's upper door closes over the ship: its opening played back + // from where it stands, as fast as the launch's tube doors open, with the game's sound + // of a door closing (`0x36`, `doorclos`), as the opening had the sound of one opening + // (`Touchdown.level`). + if (world.touchdown == .level and !state.door_closing and belowDoor(reliant, frame, off[1] + object.bounds_min.y)) { + state.door_closing = true; + playDoor(world, reliant, .doorclos, objects.Model.keep_time, -reliant_launch.door_speed); + } + if (off[1] <= down_at) return; + object.letGo(); + sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); + state.step = .of(Step.down); + state.due = now + ailand.landed_wait; + }, + .down => ailand.endAfter(world, state.due), + _ => {}, + } +} + +/// `land_reliant_cutaway` (`0x0040F600`): the cutaway of the Reliant's landing. One of its two +/// views, picked at random, watches the ship, locked. The carrier moves to `ailand.carrier_away`, +/// turned as the world is, and the middle of its first launch tube is worked out there: halfway +/// between the middles of the tube's doors, each the middle of the bounds of its part's finest +/// level (`launch.reliant.tubeMiddle`). The ship stands at `start_offset` from it in the carrier's +/// frame, looking at `over_tube`, stopped, its power shared evenly. The carrier's orders are all +/// popped, and it stops. +fn cutaway(ctx: Context, index: u16, carrier: u16) void { + const world = ctx.world; + const all = world.objects; + const slot = &all.slots[index]; + const state = &slot.state.land; + ailand.watch(world, index, .{ .landing_aside, .landing_tube }); + const reliant = &all.slots[carrier]; + ailand.moveAway(reliant); + if (reliant_launch.tubeMiddle(reliant, first_tube, 0)) |tube| state.tube = tube; + const frame = tubeFrame(state, reliant); + const object = &slot.object; + objects.setPosition(object, &slot.drawn, frame.point(start_offset)); + const look = frame.point(over_tube); + objects.setOrientation(object, &slot.drawn, math.lookAt(look - object.nextPosition())); + ailand.stopShip(object); + ailand.stopCarrier(ctx, carrier); +} + +/// The frame the landing goes by: the middle of the tube the cutaway worked out (`State.tube`), +/// turned as `carrier` will be next. +fn tubeFrame(state: *const State, carrier: *const create.Slot) math.Place { + return .{ .position = state.tube, .orientation = carrier.object.root.next_orientation }; +} + +/// **Improvement:** the point the ship steers at as it approaches, in the tube's frame `frame` +/// (`tubeFrame`), so that it levels out as it comes rather than pitching as it arrives +/// (`Touchdown.level`). It stands over the tube's middle, at first at the game's height, +/// `over_tube`; as the ship nears, eased in and out over the way the cutaway set it to come, it +/// rises to the ship's own height, which it takes once the ship is over the tube. +fn flaredAim(frame: math.Place, object: *const gameobj.GameObject) Vector { + const off = frame.inverse(object.nextPosition()); + const across = @sqrt(off[0] * off[0] + off[2] * off[2]); + const share = std.math.clamp(1 - across / start_offset[2], 0, 1); + const eased = share * share * (3 - 2 * share); + return frame.point(.{ 0, math.lerp(over_tube[1], off[1], eased), 0 }); +} + +/// Whether a ship whose top stands `top` along the Y axis of the tube's frame `frame` (`tubeFrame`) +/// is below the underside of the tube's upper door, as the door's finest level bounds it. +/// False where the Reliant's model lacks the door. +fn belowDoor(reliant: *const create.Slot, frame: math.Place, top: f32) bool { + const model = if (reliant.model) |*held| held else return false; + const bounds = model.partBounds(upper_door) orelse return false; + const middle = model.boundsMiddle(upper_door) orelse return false; + const underside = model.frameAt(upper_door, reliant.drawn).point(.{ middle[0], bounds[1][1], middle[2] }); + return top > frame.inverse(underside)[1]; +} + +/// Plays the upper door's track (`door_track`) from `from` at `speed`, sounding `which` where the +/// door stands, facing as it does; nothing where the Reliant's model lacks the door. +fn playDoor(world: gameobj.World, reliant: *create.Slot, which: sound3d.sounds.Sound, from: f32, speed: f32) void { + const model = if (reliant.model) |*held| held else return; + if (model.rootChild(upper_door) == null) return; + sound3d.playFrom(world, model.frameAt(upper_door, reliant.drawn), which, .guaranteed); + model.playNamed(upper_door, door_track, from, null, speed); +} + +/// What the landing's views stand by (`camera.View.landing_tube`, `landing_aside`): the middle of +/// the tube the landing's state keeps, and how its carrier, which `target` names, is turned as it +/// is drawn. +pub fn seen(all: *const create.Objects, target: aigeneric.Target, state: *const State) ?camera.Landing { + const carrier = target.slotIn(all) orelse return null; + return .{ .reliant = .{ .tube = state.tube, .carrier = all.slots[carrier].drawn.orientation } }; +} + +test "the player's ship lands on the Reliant, and the mission is over" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var reliant_model: reliant_launch.testing.Reliant = undefined; + try reliant_model.init(gpa); + defer reliant_model.deinit(gpa); + var variables: @import("../../vm.zig").Variables = .{}; + var world = mission.world(); + world.variables = &variables; + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); + const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); + try reliant_model.fit(gpa, mission.slot(reliant)); + const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + + // The Reliant's style, due `wait` ticks on, until which the player flies on. + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(ailand.Style.reliant, state.landing().?); + try std.testing.expectEqual(Step.waiting, state.step.as(Step)); + try std.testing.expectEqual(1000 + ailand.wait, state.due); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.waiting, state.step.as(Step)); + + // Then the cutaway: the Reliant stopped far away, the ship ahead of its tube and above it, + // flying by its nose and passing through everything, and every other object disabled. + mission.clock.frame_start = 1000 + ailand.wait; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); + try std.testing.expectEqual(@import("../main.zig").Showing.landing, mission.player.showing); + try std.testing.expect(mission.slot(other).object.flags.disabled); + try std.testing.expect(!mission.slot(reliant).object.flags.disabled and !slot.object.flags.disabled); + try std.testing.expectEqual(ailand.carrier_away, mission.slot(reliant).object.nextPosition()); + try std.testing.expectEqual(0, mission.slot(reliant).object.order_count); + const tube: Vector = state.tube; + try std.testing.expectEqual(tube + start_offset, slot.object.nextPosition()); + try std.testing.expectEqual(@import("../motion.zig").Motion.plain, slot.motion.?); + try std.testing.expect(slot.object.flags.no_collisions); + + // Far from the point over the tube, it approaches at its most; within `near`, it stops. + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(ailand.approach_most, slot.object.throttle); + objects.setPosition(&slot.object, &slot.drawn, tube + over_tube + Vector{ 0, 0, 150 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.settling, state.step.as(Step)); + try std.testing.expectEqual(0, slot.object.throttle); + + // It waits `settle` ticks, and sinks along its Y axis, slowing as it nears the tube's middle. + mission.ordersAfter(ctx, player, settle); + try std.testing.expectEqual(Step.settling, state.step.as(Step)); + mission.ordersAfter(ctx, player, 1); + try std.testing.expectEqual(Step.sinking, state.step.as(Step)); + try std.testing.expectEqual(@import("../motion.zig").Motion.downward, slot.motion.?); + objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -1500, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectApproxEqAbs(1500 * sink_throttle, slot.object.throttle, 1e-6); + objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -50, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.down, state.step.as(Step)); + try std.testing.expectEqual(0, slot.object.throttle); + + // `landed_wait` ticks after it is down, the mission is over. + mission.ordersAfter(ctx, player, ailand.landed_wait); + try std.testing.expectEqual(0, variables.mission_over); + mission.ordersAfter(ctx, player, 1); + try std.testing.expectEqual(1, variables.mission_over); +} + +test flaredAim { + var object = std.mem.zeroes(gameobj.GameObject); + const frame: math.Place = .{ .position = .{ 0, 0, 1000 } }; + const tube = frame.position; + // Where the cutaway sets the ship, the game's point over the tube. + object.root.next_position = .of(tube + start_offset); + try std.testing.expectEqual(tube + over_tube, flaredAim(frame, &object)); + // Half way there, the point has risen half way to the ship's height. + object.root.next_position = .of(tube + Vector{ 0, -4000, start_offset[2] / 2 }); + try std.testing.expectApproxEqAbs(-3500, flaredAim(frame, &object)[1], 1e-2); + // Over the tube's middle, at the ship's own height, so that it flies level. + object.root.next_position = .of(tube + Vector{ 0, -3800, 0 }); + try std.testing.expectApproxEqAbs(-3800, flaredAim(frame, &object)[1], 1e-2); +} + +test "the ship lands in the Reliant's first tube, and its upper door closes over it" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var reliant_model: reliant_launch.testing.Reliant = undefined; + try reliant_model.init(gpa); + defer reliant_model.deinit(gpa); + var world = mission.world(); + world.touchdown = .level; + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), @splat(0)); + const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); + try reliant_model.fit(gpa, mission.slot(reliant)); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + aigeneric.objectOrders(ctx, player); + mission.clock.frame_start = ailand.wait + 1; + aigeneric.objectOrders(ctx, player); + + // The cutaway works out the tube's middle where it has moved the carrier, turned as the world + // is: halfway between the lower door, part 0, and the upper door, part 6, 500 above it. + const tube: Vector = state.tube; + try std.testing.expectEqual(ailand.carrier_away + Vector{ 0, -250, 0 }, tube); + try std.testing.expectEqual(tube + start_offset, slot.object.nextPosition()); + + // Sinking, the door stays open while the ship's top is above the door's underside, 150 over + // the tube's middle; once its top is below it, the door closes over it, and stays closing. + state.step = .of(Step.sinking); + slot.object.bounds_min.y = -20; + objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -1500, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expect(!state.door_closing); + objects.setPosition(&slot.object, &slot.drawn, tube + Vector{ 0, -120, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.sinking, state.step.as(Step)); + try std.testing.expect(state.door_closing); + aigeneric.objectOrders(ctx, player); + try std.testing.expect(state.door_closing); +} + +test "a ship sent home for its friendly fire lands at once" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var reliant_model: reliant_launch.testing.Reliant = undefined; + try reliant_model.init(gpa); + defer reliant_model.deinit(gpa); + const ctx = mission.orders(); + const player = try mission.add(.of(.predator), @splat(0)); + const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); + try reliant_model.fit(gpa, mission.slot(reliant)); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); + mission.player.ending = .friendly_fire; + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + const state = &mission.slot(player).state.land; + try std.testing.expectEqual(1000, state.due); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); +} + +test "over the tube, a ship stops dead, or coasts as the game lets it" { + const gpa = std.testing.allocator; + var reliant_model: reliant_launch.testing.Reliant = undefined; + try reliant_model.init(gpa); + defer reliant_model.deinit(gpa); + for ([_]Touchdown{ .level, .original }) |touchdown| { + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var world = mission.world(); + world.touchdown = touchdown; + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), @splat(0)); + const reliant = try mission.add(.of(.reliant), .{ 0, 0, 50000 }); + try reliant_model.fit(gpa, mission.slot(reliant)); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + aigeneric.objectOrders(ctx, player); + mission.clock.frame_start = ailand.wait + 1; + aigeneric.objectOrders(ctx, player); + objects.setPosition(&slot.object, &slot.drawn, @as(Vector, state.tube) + over_tube + Vector{ 0, 0, 150 }); + slot.object.velocity = .{ .x = 0, .y = 0, .z = -5 }; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.settling, state.step.as(Step)); + try std.testing.expectEqual(@as(f32, if (touchdown == .level) 0 else -5), slot.object.velocity.z); + } +} + +test seen { + var mission: gameobj.testing.Mission = undefined; + try mission.init(std.testing.allocator); + defer mission.deinit(); + const ctx = mission.orders(); + const player = try mission.add(.of(.predator), @splat(0)); + const reliant = try mission.add(.of(.reliant), .{ 0, 0, 1000 }); + try std.testing.expectEqual(null, ailand.seen(mission.objects, player)); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, reliant, null)); + mission.slot(player).state.land.setLanding(.reliant); + mission.slot(player).state.land.tube = .{ 1, 2, 3 }; + const landing = ailand.seen(mission.objects, player).?.reliant; + try std.testing.expectEqual(Vector{ 1, 2, 3 }, landing.tube); + try std.testing.expectEqual(mission.slot(reliant).drawn.orientation, landing.carrier); +} diff --git a/src/engine/game/ailand/yamato.zig b/src/engine/game/ailand/yamato.zig new file mode 100644 index 00000000..1d78094d --- /dev/null +++ b/src/engine/game/ailand/yamato.zig @@ -0,0 +1,363 @@ +//! The Yamato's landing (`land_yamato_init`, `0x0040EB60`, and `land_yamato_update`, +//! `0x0040EE80`): the ship flies into the Yamato's landing bay, a ship type of its own made in the +//! cutaway slot, and comes down onto its pad, which lowers it. + +const std = @import("std"); +const log = std.log.scoped(.orders); + +const math = @import("../../surrender/math.zig"); +const Vector = math.Vector; +const ai = @import("../ai.zig"); +const aigeneric = @import("../aigeneric.zig"); +const Context = aigeneric.Context; +const ailand = @import("../ailand.zig"); +const State = ailand.State; +const camera = @import("../camera.zig"); +const create = @import("../create.zig"); +const gameobj = @import("../gameobj.zig"); +const input = @import("../../input.zig"); +const objects = @import("../objects.zig"); +const sound3d = @import("../sound3d.zig"); + +/// The Yamato's steps (`land_yamato_update`, `0x0040EE80`). +pub const Step = enum(u32) { + /// The player flies on until the landing is due; then the cutaway begins (`cutaway`). + waiting = 0, + /// The ship flies to `over_bay` above the hangar's middle. + descending = 1, + /// It flies on to stand `pad_ahead` beyond the pad's middle, slowing as it nears. + approaching = 2, + /// Stopped, it turns straight along the bay and level, and stands on the pad. + aligning = 3, + /// It waits `pad_settle` ticks on the pad, which it rides from now on (`hold`). + settling = 4, + /// The pad lowers, sounding the ship's landing. + lowering = 5, + /// `lowered_wait` ticks later, the mission is over. + down = 6, + _, +}; + +/// The parts of the Yamato's landing bay its landing goes by: the hangar, whose lights the cutaway +/// masks as the launch's hangar does (`0x0040EC57`), and the pad the ship comes down on, whose +/// track lowers it (`0x0040F0D2`, `0x004E2048`). +const bay_hangar = 6; +const landing_pad = 1; +const bay_light_mask: u32 = 0x3B; +const pad_track = "floor"; + +/// Where the cutaway stands the ship in the bay, as shares of the bay's bounds: across, down and +/// along, this much of its maximum and the rest of its minimum (`0x0040EC95`, `0x004DC3F8`, +/// `0x004DC4C0`). +const stand_share: Vector = .{ 0.2, 0.2, 0.3 }; + +/// The throttle the Yamato's cutaway leaves the ship at (`0x0040EE43`). +const cutaway_throttle: f32 = 0.5; + +/// How long after the cutaway the view from the bay begins to slide along it, in ticks +/// (`0x0040EF1B`). +const slide_after: i32 = 600; + +/// How far above the hangar's middle, along its Y axis, the ship first flies, and how near it gets +/// before it goes on, which the game compares squared (`0x004DC44C`, `0x004DC490`). +const over_bay: f32 = 1000; +const over_bay_near: f32 = 2000; + +/// The speed the ship flies through the bay at most, its throttle what that is of its cruise speed +/// (`0x004DC440`). A landing in a hangar bay flies at most as fast (`ailand.bay`). +pub const bay_speed: f32 = 100; + +/// How far beyond the pad's middle along the bay the ship is to stand, and how near it gets before +/// it stops (`0x004DC4A8`); how far ahead along its nose it steers meanwhile (`0x0040F1D1`); and +/// the throttle it approaches with, `ailand.approach_throttle` for each unit it has to go less +/// `approach_least` (`0x004DC4AC`). +const pad_ahead: f32 = 500; +const on_pad_near: f32 = 500; +const pad_lead: f32 = 500; +const approach_least: f32 = 0.02; + +/// How the ship turns straight along the bay and level before it stands on the pad +/// (`0x0040F26C`): its yaw and pitch inputs are the angles off the bay's axis less +/// `align_damping` times its rates of turn, times `align_gain` over its type's rates, and it is +/// straight once both are within `aligned` (`0x004DC424`, `0x004DC4C0`, `0x004DC474`). +const align_damping: f32 = 4; +const align_gain: f32 = 0.3; +const aligned: f32 = 0.05; + +/// How long the ship waits on the pad before the pad lowers, and how long the pad lowers before the +/// mission is over, in ticks (`0x0040F3A1`, `0x0040F56D`). +const pad_settle: i32 = 200; +const lowered_wait: i32 = 400; + +/// `land_yamato_init` (`0x0040EB60`): the first step, due `ailand.wait` ticks on; for the player's +/// ship, the landing bay made in the cutaway slot, passing through everything. +pub fn init(ctx: Context, index: u16) void { + const world = ctx.world; + const state = &world.objects.slots[index].state.land; + state.step = .of(Step.waiting); + state.due = world.clock.frame_start + ailand.wait; + if (index != world.objects.player) return; + const bay = create.make(world, create.cutaway_slot, .of(.yamato_landing_bay)) catch |err| { + log.warn("the Yamato's landing bay is left out: {s}", .{@errorName(err)}); + return; + } orelse return; + world.objects.slots[bay].object.flags.no_collisions = true; +} + +/// `land_yamato_update` (`0x0040EE80`): a step of the Yamato's landing (`Step`). A ship other than +/// the player's lets its landing go at once (`order_pop`). +/// +/// While the landing is not yet due, the player's ship flies by the player's controls. Then the +/// cutaway begins (`cutaway`), and the ship flies by its nose (`motion.Motion.plain`), passing +/// through everything. The mission's scene becomes the landing's, and every other object is +/// disabled. The ship steers at `over_bay` above the hangar's middle, upright, at `bay_speed`, +/// until it is within `over_bay_near` of it. Then it steers `pad_lead` ahead along its nose from +/// the place `pad_ahead` beyond the pad's middle, level with its own underside, its throttle +/// `ailand.approach_throttle` for each unit it has to go less `approach_least`, at most +/// `bay_speed`, and stops within `on_pad_near` of it. There it turns straight along the bay and +/// level (`alignInputs`), and once it is, it stands on the pad, which it rides (`hold`). +/// `pad_settle` ticks on, the pad lowers, sounding the ship's landing, and `lowered_wait` ticks +/// after that the mission is over. +/// +/// **Improvement:** the angles come from `std.math.atan2` rather than the engine's table +/// (`sr_atan2`), as they do elsewhere in OpenReliant. +pub fn update(ctx: Context, index: u16) void { + const world = ctx.world; + const all = world.objects; + if (index != all.player) return aigeneric.end(ctx, index); + const slot = &all.slots[index]; + const object = &slot.object; + const state = &slot.state.land; + const now = world.clock.frame_start; + const bay = &all.slots[create.cutaway_slot]; + switch (state.step.as(Step)) { + .waiting => { + if (now < state.due) return input.playerControlOrder(ctx, index); + cutaway(ctx, index); + state.step = .of(Step.descending); + state.due = now + slide_after; + slot.motion = .plain; + object.flags.no_collisions = true; + }, + .descending => { + world.player.showing = .landing; + for (all.slots[0..all.count], 0..) |*each, at| if (at != index) { + each.object.flags.disabled = true; + }; + const hangar = partMiddle(bay, bay_hangar) orelse return; + const point = hangar.frame.point(hangar.middle - Vector{ 0, over_bay, 0 }); + _ = ai.steer(world, index, point, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); + object.throttle = bayThrottle(world, slot); + if (math.distance(point, object.nextPosition()) < over_bay_near) state.step = .of(Step.approaching); + }, + .approaching => { + const pad = partMiddle(bay, landing_pad) orelse return; + const point = pad.frame.point(pad.middle) + Vector{ 0, object.bounds_min.y, pad_ahead }; + const distance = math.distance(point, object.nextPosition()); + const lead = point + math.forward(object.root.next_orientation) * @as(Vector, @splat(pad_lead)); + _ = ai.steer(world, index, lead, ai.full_limit, ai.no_ease, .{ .roll_upright = true }); + object.throttle = @min(distance * ailand.approach_throttle - approach_least, bayThrottle(world, slot)); + if (distance < on_pad_near) state.step = .of(Step.aligning); + }, + .aligning => { + alignInputs(slot); + object.roll_input = 0; + object.throttle = 0; + if (@abs(object.yaw_input) >= aligned or @abs(object.pitch_input) >= aligned) return; + state.step = .of(Step.settling); + state.pad_due = now + pad_settle; + const model = if (bay.model) |*held| held else return; + if (model.rootChild(landing_pad) == null) return; + const standing: math.Place = .{ .position = object.nextPosition(), .orientation = object.root.next_orientation }; + const on_pad = standing.relativeTo(model.frameAt(landing_pad, bay.drawn)); + state.on_pad = on_pad.position; + state.pad_orientation = on_pad.orientation; + slot.riding = .{ .object = create.cutaway_slot, .part = landing_pad }; + }, + .settling => { + // No turns and no throttle while it waits on the pad (`0x0040F4A9`). + object.letGo(); + if (now <= state.pad_due) return; + state.step = .of(Step.lowering); + }, + .lowering => { + // And again as the pad starts down (`0x0040F4F5`). + object.letGo(); + if (bay.model) |*model| if (model.rootChild(landing_pad) != null) model.playNamed(landing_pad, pad_track, 0, null, 1); + sound3d.playIn(world, null, null, index, .shipland, 1, .not_reserved); + state.step = .of(Step.down); + state.pad_due = now + lowered_wait; + }, + .down => ailand.endAfter(world, state.pad_due), + _ => {}, + } +} + +/// `land_yamato_cutaway` (`0x0040EBC0`): the cutaway of the Yamato's landing. The landing bay +/// moves to `ailand.carrier_away`, turned as the world is, its hangar lit through +/// `bay_light_mask`. One of its two views, picked at random, watches the ship, locked. The ship +/// stands in the bay at `stand_share` of the bay's bounds, looking at the hangar's middle, stopped, +/// its throttle `cutaway_throttle` and its power shared evenly. +/// +/// **Fix:** the game reads past the bay's list of parts where its model lacks the hangar; then +/// OpenReliant leaves the ship turned as it was. +/// +/// Left out: the view picked, which the game keeps (`0x0051863C`) and nothing reads. +fn cutaway(ctx: Context, index: u16) void { + const world = ctx.world; + const all = world.objects; + const slot = &all.slots[index]; + const object = &slot.object; + const bay = &all.slots[create.cutaway_slot]; + ailand.moveAway(bay); + if (bay.model) |*model| if (model.rootChild(bay_hangar) != null) { + model.parts[bay_hangar].object.light_mask = bay_light_mask; + }; + ailand.watch(world, index, .{ .landing_bay, .landing_ship }); + const max = bay.object.bounds_max.vector(); + const min = bay.object.bounds_min.vector(); + const away: math.Place = .{ .position = ailand.carrier_away }; + objects.setPosition(object, &slot.drawn, away.point(max * stand_share + min * (@as(Vector, @splat(1)) - stand_share))); + if (partMiddle(bay, bay_hangar)) |hangar| { + objects.setOrientation(object, &slot.drawn, math.lookAt(hangar.frame.point(hangar.middle) - object.nextPosition())); + } + ailand.stopShip(object); + object.throttle = cutaway_throttle; +} + +/// Where part `part` of the object in `slot` stands, as its model places it, and the middle of the +/// bounds of its finest level (`objects.Model.partBounds`), in its own frame; null where the model +/// lacks the part. +fn partMiddle(slot: *create.Slot, part: usize) ?struct { frame: math.Place, middle: Vector } { + const model = if (slot.model) |*held| held else return null; + const middle = model.boundsMiddle(part) orelse return null; + return .{ .frame = model.frameAt(part, slot.drawn), .middle = middle }; +} + +/// The throttle at which the ship in `slot` flies at `bay_speed`, by its cruise speed +/// (`object_cruise_speed`); none for a ship with no flight stats. +fn bayThrottle(world: gameobj.World, slot: *const create.Slot) f32 { + const cruise = ai.slotCruise(slot, world.view()) orelse return 0; + return bay_speed / cruise; +} + +/// The inputs that turn the ship in `slot` straight along the bay and level (`0x0040F26C`): its +/// yaw and its pitch, each the angle off the bay's Z axis less `align_damping` times its rate of +/// turn, times `align_gain` over its type's rate. +/// +/// **Fix:** the game follows a null pointer for a ship with no flight stats; OpenReliant leaves +/// its inputs as they are. +fn alignInputs(slot: *create.Slot) void { + const flight = slot.flight orelse return; + const object = &slot.object; + const forward = math.forward(object.root.next_orientation); + object.yaw_input = (-std.math.atan2(forward[0], forward[2]) - align_damping * object.yaw_rate) * align_gain / flight.yaw_rate; + object.pitch_input = (std.math.atan2(forward[1], forward[2]) - align_damping * object.pitch_rate) * align_gain / flight.pitch_rate; +} + +/// Each frame's placing of a ship on the Yamato's pad (`main.frameObjects`): from `Step.settling` +/// on, the ship stands where it came to rest on the pad, as the pad lowers. Returns whether it +/// placed the ship. +pub fn hold(all: *create.Objects, index: u16) bool { + const slot = &all.slots[index]; + if (slot.running(.land) == null) return false; + const state = &slot.state.land; + if (state.landing() != .yamato or !state.step.reached(Step.settling)) return false; + const pad = (slot.riding orelse return false).place(all) orelse return false; + const on_pad: math.Place = .{ .position = state.on_pad, .orientation = state.pad_orientation }; + objects.setPlace(&slot.object, &slot.drawn, on_pad.within(pad)); + return true; +} + +/// What the landing's views stand by (`camera.View.landing_bay`): where the landing bay stands, +/// the bounds of its hangar, and the tick the view slides from. +pub fn seen(all: *const create.Objects, state: *const State) ?camera.Landing { + const bay = &all.slots[create.cutaway_slot]; + const model = if (bay.model) |*held| held else return null; + const hangar = model.partBounds(bay_hangar) orelse return null; + return .{ .yamato = .{ .bay = bay.drawn, .hangar = hangar, .due = state.due } }; +} + +test "the player's ship lands on a pad of the Yamato's landing bay, and the mission is over" { + const gpa = std.testing.allocator; + var mission: gameobj.testing.Mission = undefined; + try mission.init(gpa); + defer mission.deinit(); + var bay_model: @import("../launch.zig").testing.Bounded(bay_hangar + 1) = undefined; + const hangar_bounds: [2]Vector = .{ .{ -10000, -20000, -30000 }, .{ 10000, 0, 30000 } }; + try bay_model.init(gpa, hangar_bounds); + defer bay_model.deinit(gpa); + var variables: @import("../../vm.zig").Variables = .{}; + var world = mission.world(); + world.variables = &variables; + world.spawn = mission.spawn(create.testing.no_models); + const ctx: Context = .of(world); + const player = try mission.add(.of(.predator), .{ 100, 0, 0 }); + const yamato = try mission.add(.of(.yamato), .{ 0, 0, 50000 }); + const other = try mission.add(.of(.predator), .{ 5, 5, 5 }); + try std.testing.expect(try aigeneric.pushShip(ctx, player, .land, yamato, null)); + const slot = mission.slot(player); + const state = &slot.state.land; + + // The Yamato's style, due `wait` ticks on, until which the player flies on; and the landing bay + // made in the cutaway slot, passing through everything. + mission.clock.frame_start = 1000; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(ailand.Style.yamato, state.landing().?); + try std.testing.expectEqual(Step.waiting, state.step.as(Step)); + try std.testing.expectEqual(1000 + ailand.wait, state.due); + const bay = mission.slot(create.cutaway_slot); + try std.testing.expectEqual(gameobj.Type.of(.yamato_landing_bay), bay.object.type); + try std.testing.expect(bay.object.flags.no_collisions); + try bay_model.parts.fit(gpa, bay); + bay.object.bounds_min = .of(hangar_bounds[0]); + bay.object.bounds_max = .of(hangar_bounds[1]); + + // Then the cutaway: the bay far away, the ship standing in it, flying by its nose through + // everything. + mission.clock.frame_start = 1000 + ailand.wait; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.descending, state.step.as(Step)); + try std.testing.expectEqual(1000 + ailand.wait + slide_after, state.due); + try std.testing.expectEqual(ailand.carrier_away, bay.object.nextPosition()); + try std.testing.expectEqual(ailand.carrier_away + Vector{ -6000, -16000, -12000 }, slot.object.nextPosition()); + try std.testing.expectEqual(cutaway_throttle, slot.object.throttle); + try std.testing.expectEqual(@import("../motion.zig").Motion.plain, slot.motion.?); + try std.testing.expect(slot.object.flags.no_collisions); + + // It flies over the hangar's middle, every other object disabled, and once near it, on. + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(@import("../main.zig").Showing.landing, mission.player.showing); + try std.testing.expect(mission.slot(other).object.flags.disabled); + try std.testing.expectEqual(Step.descending, state.step.as(Step)); + const middle = ailand.carrier_away + Vector{ 0, -10000, 0 }; + objects.setPosition(&slot.object, &slot.drawn, middle - Vector{ 0, over_bay, 0 }); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.approaching, state.step.as(Step)); + + // It flies to stand beyond the pad's middle, level with its underside, and stops near it. + objects.setPosition(&slot.object, &slot.drawn, middle + Vector{ 0, 0, pad_ahead }); + objects.setOrientation(&slot.object, &slot.drawn, math.identity); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.aligning, state.step.as(Step)); + + // Straight along the bay and level, it stands on the pad, and rides it from then on. + mission.clock.frame_start = 3000; + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.settling, state.step.as(Step)); + try std.testing.expectEqual(3000 + pad_settle, state.pad_due); + try std.testing.expectEqual(objects.NodeOf{ .object = create.cutaway_slot, .part = landing_pad }, slot.riding.?); + objects.setPosition(&slot.object, &slot.drawn, @splat(0)); + try std.testing.expect(hold(mission.objects, player)); + try std.testing.expectEqual(middle + Vector{ 0, 0, pad_ahead }, slot.object.nextPosition()); + + // `pad_settle` ticks on, the pad lowers, and `lowered_wait` ticks later the mission is over. + mission.ordersAfter(ctx, player, pad_settle + 1); + try std.testing.expectEqual(Step.lowering, state.step.as(Step)); + aigeneric.objectOrders(ctx, player); + try std.testing.expectEqual(Step.down, state.step.as(Step)); + mission.ordersAfter(ctx, player, lowered_wait); + try std.testing.expectEqual(0, variables.mission_over); + mission.ordersAfter(ctx, player, 1); + try std.testing.expectEqual(1, variables.mission_over); +} diff --git a/src/engine/game/create.zig b/src/engine/game/create.zig index 6b7bf3cc..6a2b4d21 100644 --- a/src/engine/game/create.zig +++ b/src/engine/game/create.zig @@ -24,6 +24,7 @@ const Vector = math.Vector; const Pointer = engine.Pointer; const Random = @import("../random.zig").Random; const ai = @import("ai.zig"); +const ailand = @import("ailand.zig"); const camera = @import("camera.zig"); const cloak = @import("cloak.zig"); const aigeneric = @import("aigeneric.zig"); @@ -86,17 +87,23 @@ pub fn shipFiles(number: TypeIndex) models.ShipType { pub const Stats = struct { flight: [max_ship_types]FlightModel, combat: [max_ship_types]ShipCombat, + /// OpenReliant's own: how the player lands on each type's ships, which each new object of the + /// type starts with (`Slot.landing`); null where the player can't land on them. + landing: [max_ship_types]?ailand.Style, /// The tables as the executable holds them before `stats_load_ships` runs: every figure zero, - /// and each record's own words, how the AI turns the type and what its combat record says it is. + /// and each record's own words, how the AI turns the type and what its combat record says it + /// is, with the landing the game offers on it (`ailand.Style.original`). pub const initial: Stats = built: { - @setEvalBranchQuota(ship_type_count * 20); + @setEvalBranchQuota(ship_type_count * 30); var tables: Stats = .{ .flight = @splat(std.mem.zeroes(FlightModel)), .combat = @splat(std.mem.zeroes(ShipCombat)), + .landing = @splat(null), }; for (tables.flight[0..ship_type_count], 0..) |*flight, ship_type| flight.turns = flight_stats.turns(ship_type); for (tables.combat[0..ship_type_count], combat_stats.ship_types) |*record, static| record.take(static); + for (tables.landing[0..ship_type_count], 0..) |*landing, ship_type| landing.* = ailand.Style.original(@fromBackingInt(ship_type)); break :built tables; }; @@ -137,12 +144,15 @@ pub const Stats = struct { } /// OpenReliant's: gives each type the words of its type record (`typeRecords`), which the mods' - /// scripts can change (`records.ship_types`). + /// scripts can change (`records.ship_types`), and the landing the record offers. /// /// **Improvement:** the game keeps them fixed in its executable. pub fn loadTypes(tables: *Stats, types: []const combat_stats.Static) void { const count = @min(types.len, max_ship_types); - for (tables.combat[0..count], types[0..count]) |*record, static| record.take(static); + for (tables.combat[0..count], tables.landing[0..count], types[0..count]) |*record, *landing, static| { + record.take(static); + landing.* = static.landing; + } } /// What `create_object` does for a type that is `from` under another number (`donor`): the @@ -161,11 +171,13 @@ pub const Stats = struct { /// The type records that the mods' scripts can change (`records.ship_types`, `Stats.loadTypes`): /// the words the executable holds for each of the game's types (`combat_stats.Static`), then one /// for each type a mod adds, copied from its base's, with its own name where the mod gives one. +/// Each holds the landing its type offers before scripts change it (`ailand.Style.of`). pub fn typeRecords(arena: Allocator) Allocator.Error![]combat_stats.Static { const made = try additions.ships.records(combat_stats.Static, arena, &combat_stats.ship_types, 0); for (made[additions.ships.first..], additions.ships.all()) |*record, added| { if (added.label_string) |name| record.name = name; } + for (made, 0..) |*record, number| record.landing = ailand.Style.of(@fromBackingInt(@as(u32, @intCast(number)))); return made; } @@ -470,6 +482,10 @@ pub const Slot = struct { /// Its type's stats in `Stats` (`GameObject.combat`, `flight`); null for a stand-in. combat: ?*const ShipCombat = null, flight: ?*const FlightModel = null, + /// OpenReliant's own: how the player's ship lands on it (`ailand`). It starts as its type's + /// (`Stats.landing`), and a mission's script can change it. Null where the player can't land + /// on it, as on a stand-in. + landing: ?ailand.Style = null, /// Its type's model as loaded (`GameObject.model`), and the nodes of the model's parts, which /// hang from its root; null for a stand-in, or a type the game has no model for. type: ?*const Type = null, @@ -1008,25 +1024,49 @@ test "Stats.loadTypes" { tables.* = .initial; var types = combat_stats.ship_types; const shuttle = @backingInt(gameobj.GameType.yakob_shuttle); + const victorious = @backingInt(gameobj.GameType.victorious); try std.testing.expectEqual(.support, tables.combat[shuttle].class); + // Before the records, the game's landings: the Reliant's and the Yamato's alone. + try std.testing.expectEqual(ailand.Style.reliant, tables.landing[@backingInt(gameobj.GameType.reliant)].?); + try std.testing.expectEqual(ailand.Style.yamato, tables.landing[@backingInt(gameobj.GameType.yamato)].?); + try std.testing.expectEqual(null, tables.landing[victorious]); types[shuttle].class = .fighter; types[shuttle].side = .neutral; + types[victorious].landing = .bay; tables.loadTypes(&types); try std.testing.expectEqual(.fighter, tables.combat[shuttle].class); try std.testing.expectEqual(.neutral, tables.combat[shuttle].side); + try std.testing.expectEqual(ailand.Style.bay, tables.landing[victorious].?); try std.testing.expectEqual(combat_stats.ship_types[0].name, tables.combat[0].name); } test typeRecords { // A mod's type takes its base's words, under its own name. - var list = [_]additions.ships.Added{.{ .name = "a:pot", .mod = "a", .base = .predator, .label_string = 900, .extra = .{ .model = "pot.shp" } }}; + var list = [_]additions.ships.Added{ + .{ .name = "a:pot", .mod = "a", .base = .predator, .label_string = 900, .extra = .{ .model = "pot.shp" } }, + // Based on the Reliant: in its model, in a model of its own, and in its model with a bay's + // landing. + .{ .name = "a:refit", .mod = "a", .base = .reliant, .extra = .{ .model = "RELIANT.SHP" } }, + .{ .name = "a:ark", .mod = "a", .base = .reliant, .extra = .{ .model = "ark.shp" } }, + .{ .name = "a:dock", .mod = "a", .base = .reliant, .extra = .{ .model = "RELIANT.SHP", .landing = .{ .offers = .bay } } }, + }; additions.ships.install(&list); defer additions.ships.reset(); const made = try typeRecords(std.testing.allocator); defer std.testing.allocator.free(made); - try std.testing.expectEqual(additions.ships.first + 1, made.len); - try std.testing.expectEqual(combat_stats.ship_types[0].class, made[additions.ships.first].class); - try std.testing.expectEqual(900, made[additions.ships.first].name); + try std.testing.expectEqual(additions.ships.first + list.len, made.len); + const pot = made[additions.ships.first]; + try std.testing.expectEqual(combat_stats.ship_types[0].class, pot.class); + try std.testing.expectEqual(900, pot.name); + // The game's types offer the game's landings, the Reliant's and the Yamato's alone. A mod's type + // offers its base's in its base's model, none in a model of its own, and the one it gives. + try std.testing.expectEqual(ailand.Style.reliant, made[@backingInt(gameobj.GameType.reliant)].landing.?); + try std.testing.expectEqual(ailand.Style.yamato, made[@backingInt(gameobj.GameType.yamato)].landing.?); + try std.testing.expectEqual(null, made[@backingInt(gameobj.GameType.victorious)].landing); + try std.testing.expectEqual(null, pot.landing); + try std.testing.expectEqual(ailand.Style.reliant, made[additions.ships.first + 1].landing.?); + try std.testing.expectEqual(null, made[additions.ships.first + 2].landing); + try std.testing.expectEqual(ailand.Style.bay, made[additions.ships.first + 3].landing.?); } test "ShipCombat.nameString" { @@ -1364,6 +1404,7 @@ pub fn createObject(all: *Objects, tables: *Stats, types: Types, wanted: ?u16, a object.armor = .all(0); object.flags = .standing_in; object.radius = stand_in_radius; + slot.landing = null; hooks.tell(all, .object_added, .{ .object = .of(index) }); return index; }; @@ -1372,6 +1413,7 @@ pub fn createObject(all: *Objects, tables: *Stats, types: Types, wanted: ?u16, a const combat = &tables.combat[stats_type]; slot.combat = combat; slot.flight = &tables.flight[stats_type]; + slot.landing = tables.landing[stats_type]; slot.motion = .forward; object.side = @fromBackingInt(@backingInt(combat.side)); @@ -2574,20 +2616,28 @@ test createObject { try std.testing.expectEqual(.hostile, all.slots[enemy].object.side); try std.testing.expectEqual(coalition_pilot, all.slots[enemy].object.pilot); try std.testing.expect(all.slots[enemy].object.flags.ecm); + // It offers no landing; a ship of a type the tables give one offers that. + try std.testing.expectEqual(null, all.slots[enemy].landing); + mission.tables.landing[@backingInt(gameobj.GameType.sabre)] = .bay; + const carrier = try createObject(all, &mission.tables, model.types(), null, .of(.sabre), 0, .{ 0, 0, 0 }, &mission.random); + try std.testing.expectEqual(ailand.Style.bay, all.slots[carrier].landing.?); // A slot filled once is not filled again, and nothing lies past the last. try std.testing.expectError(error.CreatedTwice, createObject(all, &mission.tables, model.types(), player, .of(.predator), 0, @splat(0), &mission.random)); try std.testing.expectError(error.Overrun, createObject(all, &mission.tables, model.types(), gameobj.max_objects, .of(.predator), 0, @splat(0), &mission.random)); - // Above the last ship type, a stand-in for a marker, at a slot of its own. + // Above the last ship type, a stand-in for a marker, at a slot of its own, which offers no + // landing. + all.slots[20].landing = .reliant; const marker = try createObject(all, &mission.tables, model.types(), 20, .of(.marker), 0, @splat(0), &mission.random); try std.testing.expectEqual(20, marker); - try std.testing.expectEqual(2, all.count); + try std.testing.expectEqual(3, all.count); const stand_in = all.slots[marker]; try std.testing.expectEqual(GameObject.Flags.standing_in, stand_in.object.flags); try std.testing.expectEqual(stand_in_radius, stand_in.object.radius); try std.testing.expectEqual(null, stand_in.model); try std.testing.expectEqual(null, stand_in.combat); + try std.testing.expectEqual(null, stand_in.landing); // Reset, the slot stands in again, and can be filled anew. resetSlot(mission.orders(), player); diff --git a/src/engine/game/create/combat.zig b/src/engine/game/create/combat.zig index c47999cc..8433c9c0 100644 --- a/src/engine/game/create/combat.zig +++ b/src/engine/game/create/combat.zig @@ -5,6 +5,7 @@ //! Generated by `src/tools/tablegen` from the payload executable's `ship_combat_stats` at //! 0x004FC670. Do not edit by hand; run `make combat-tables`. +const ailand = @import("../ailand.zig"); const create = @import("../create.zig"); const gameobj = @import("../gameobj.zig"); @@ -22,6 +23,10 @@ pub const Static = struct { /// The side its objects start on. It has `GameObject.side`'s type, so scripts see one `Side`. side: gameobj.Side(i32), display: create.ShipCombat.TargetDisplay, + /// OpenReliant's own: how the player lands on a ship of the type, which the game keeps in its + /// code rather than in this table. The records start from the game's (`ailand.Style.of`). + /// Null where the player can't land on it. + landing: ?ailand.Style = null, }; /// Every ship type's, by its number. diff --git a/src/engine/game/friendly_fire.zig b/src/engine/game/friendly_fire.zig index c809b237..3641aef0 100644 --- a/src/engine/game/friendly_fire.zig +++ b/src/engine/game/friendly_fire.zig @@ -15,6 +15,7 @@ const assert = std.debug.assert; const math = @import("../surrender/math.zig"); const ai = @import("ai.zig"); const aigeneric = @import("aigeneric.zig"); +const ailand = @import("ailand.zig"); const collision = @import("collision.zig"); const gameobj = @import("gameobj.zig"); const input = @import("../input.zig"); @@ -184,7 +185,9 @@ const jump_beyond: f32 = 1_000_000; /// carrier's stand-in flag. /// /// **Fix:** the game reads the carrier through a null pointer where the player launched from -/// none; OpenReliant lets the ship fly on as the player has it, and then ends the order. +/// none; OpenReliant lets the ship fly on as the player has it, and then ends the order. It does +/// the same where the player's carrier offers no landing (`ailand.landingCarrier`), which only a +/// mod or a mission's script makes so. pub fn update(ctx: aigeneric.Context, index: u16) void { const world = ctx.world; const all = world.objects; @@ -203,13 +206,13 @@ pub fn update(ctx: aigeneric.Context, index: u16) void { return; } input.playerControlOrder(ctx, index); - const carrier = world.player.carrier orelse return; + const carrier = ailand.landingCarrier(world) orelse return; steerHome(world, index, all.slots[carrier].object.nextPosition()); }, .landing => { slot.object.flags.sent_off = false; aigeneric.end(ctx, index); - const carrier = world.player.carrier orelse return; + const carrier = ailand.landingCarrier(world) orelse return; const home = &all.slots[carrier].object; home.flags.stand_in = false; _ = aigeneric.giveShip(ctx, index, .land, carrier, null); @@ -343,3 +346,27 @@ test "a player who destroys a friend is sent home, and lands" { try std.testing.expectEqual(reliant, player.current().?.target.slotIn(all).?); try std.testing.expect(player.object.flags.sent_off); } + +test "a player sent home to a carrier that offers no landing flies on" { + var heard: radio.testing.Heard = undefined; + try heard.init(); + defer heard.deinit(); + const world = heard.world(); + const all = heard.mission.objects; + const ctx: aigeneric.Context = .of(world); + const player = &all.slots[0]; + heard.mission.player.carrier = try heard.mission.add(.of(.victorious), .{ 0, 0, 5000 }); + _ = try aigeneric.pushShip(ctx, 0, .player_control, heard.enemy, null); + friendDestroyed(world); + sendHome(ctx); + heard.mission.clock.frame_start = 1000; + init(ctx, 0); + // Past Moose's words and the steering, the order ends, and the player flies on as before. + heard.mission.clock.frame_start += speaking_ticks + 1; + update(ctx, 0); + heard.mission.clock.frame_start += steering_ticks + 1; + update(ctx, 0); + update(ctx, 0); + try std.testing.expectEqual(.player_control, player.current().?.order); + try std.testing.expect(!player.object.flags.sent_off); +} diff --git a/src/engine/game/gameobj.zig b/src/engine/game/gameobj.zig index 495c3e3a..1a75dd48 100644 --- a/src/engine/game/gameobj.zig +++ b/src/engine/game/gameobj.zig @@ -1955,7 +1955,7 @@ pub const World = struct { /// The Yamato launch steam's brightness, with the original emission behavior. launch_steam: @import("launch/yamato.zig").Steam = .soft, /// How the Reliant's landing brings the ship down. - touchdown: @import("ailand.zig").Touchdown = .level, + touchdown: @import("ailand.zig").reliant.Touchdown = .level, /// When the player hears their own ship jump in. arrival_sound: @import("jump.zig").ArrivalSound = .heard, /// `light_maps` (`0x005D5618`): whether the parts flagged `lightmap` are drawn with their light diff --git a/src/engine/game/launch/badanov.zig b/src/engine/game/launch/badanov.zig index dd68b46b..b8bb3a20 100644 --- a/src/engine/game/launch/badanov.zig +++ b/src/engine/game/launch/badanov.zig @@ -1,7 +1,8 @@ //! The Badanov and Krasny launches (`launch_badanov_init`, `0x00419F60`, and //! `launch_badanov_run`, `0x0041A100`): a ship waits at one of ten positions along the bay, //! facing out with its nose tilted down. The doors open, and the ship flies out with a yaw -//! input based on its gate. Normal flight resumes after 300 ticks. +//! input based on its gate. Normal flight resumes after 300 ticks. A landing in the bay comes to +//! rest where a ship waits, through the same doors (`ailand.bay`). const std = @import("std"); const log = std.log.scoped(.launch); @@ -30,7 +31,11 @@ pub const Step = enum(i32) { /// The part of the carrier's root's child list that the ship waits at (`0x00419F8D`), and the two /// parts that are the bay's doors (`0x0041A1CB`, `0x0041A2FF`). const bay_part = 4; -const doors = [_]usize{ 1, 2 }; +const doors = [_]u8{ 1, 2 }; + +/// The bay's doors as a landing in it opens and closes them, as a hangar bay's +/// (`bay.playDoors`). +pub const landing_doors: bay.Doors = .{ .first = doors[0], .second = doors[1] }; /// Each side has five positions. Gates below this number use one side; the rest use the other /// (`0x00419FFB`). @@ -66,10 +71,8 @@ const out_throttle: f32 = 2; const steer: f32 = 0.2; const steer_middle: f32 = 2.5; -/// `launch_badanov_init` (`0x00419F60`): places the ship in slot `index` at its gate's position -/// in `carrier`'s bay (`bay_part`), riding that part. The position uses the bounds of the last -/// drawn level. The ship turns a quarter turn about the part's Y axis, with opposite signs for -/// the two sides, then tilts down about its X axis. +/// `launch_badanov_init` (`0x00419F60`): places the ship in slot `index` where it waits at its +/// gate in `carrier`'s bay (`gatePlace`), riding the bay's part (`bay_part`). /// /// **Fix:** if the model lacks the bay or its level, the ship stays where it is and rides the /// carrier's root instead of dereferencing the missing part. @@ -78,13 +81,24 @@ pub fn init(ctx: aigeneric.Context, index: u16, carrier: u16) void { const slot = &all.slots[index]; const holder = &all.slots[carrier]; slot.riding = .{ .object = carrier }; - const model = if (holder.model) |*held| held else return; - const bounds = model.partBounds(bay_part) orelse { + if (holder.model == null) return; + const place = gatePlace(holder, slot.orders[0].target.component) orelse { log.warn("the carrier in slot {d} has no bay to launch from", .{carrier}); return; }; slot.riding = .{ .object = carrier, .part = bay_part }; - const gate = slot.orders[0].target.component; + objects.setPlace(&slot.object, &slot.drawn, place); +} + +/// Where a ship waits at gate `gate` of the carrier in `holder` to launch, in the world, and how it +/// is turned there (`init`). It stands in the bay (`bay_part`), by the bounds of the level the bay +/// drew last: on one side for the first `gates_a_side` gates and on the other for the rest, turned +/// a quarter turn about the part's Y axis to face out of its side, then tilted down about its X +/// axis. A landing in the bay comes to rest there (`ailand.bay`). Null where the carrier's model +/// lacks the bay or its level. +pub fn gatePlace(holder: *const create.Slot, gate: i16) ?math.Place { + const model = if (holder.model) |*held| held else return null; + const bounds = model.partBounds(bay_part) orelse return null; const number: f32 = @floatFromInt(gate); const size = bounds[1] - bounds[0]; var at = (bounds[1] + bounds[0]) * @as(math.Vector, @splat(0.5)); @@ -95,7 +109,7 @@ pub fn init(ctx: aigeneric.Context, index: u16, carrier: u16) void { const part = model.frameAt(bay_part, holder.drawn); var turned = math.turned(part.orientation, .y, if (near) quarter_turn else -quarter_turn); turned = math.turned(turned, .x, tilt); - objects.setPlace(&slot.object, &slot.drawn, .{ .position = part.point(at), .orientation = turned }); + return .{ .position = part.point(at), .orientation = turned }; } /// `launch_badanov_run` (`0x0041A100`): opens the doors unless they are already moving, then @@ -148,18 +162,20 @@ fn openDoors(world: gameobj.World, carrier: *create.Slot) void { sound3d.playFrom(world, first.drawn(), .dooropen, .not_reserved); } -/// A Badanov's model for the tests: its bay (part 4) 200 wide and high and 600 long, and its -/// doors (parts 1 and 2) with the doors' track. -const TestBadanov = launch.testing.Bounded(5); -const test_bounds: [2]math.Vector = .{ .{ -100, -100, -300 }, .{ 100, 100, 300 } }; +/// A Badanov's model for the tests, here and of the landings (`ailand.bay`): its bay (part 4) 200 +/// wide and high and 600 long, and its doors (parts 1 and 2) with the doors' track. +pub const testing = struct { + pub const Badanov = launch.testing.Bounded(5); + pub const bounds: [2]math.Vector = .{ .{ -100, -100, -300 }, .{ 100, 100, 300 } }; +}; test openDoors { const gpa = std.testing.allocator; var mission: gameobj.testing.Mission = undefined; try mission.init(gpa); defer mission.deinit(); - var badanov_model: TestBadanov = undefined; - try badanov_model.init(gpa, test_bounds); + var badanov_model: testing.Badanov = undefined; + try badanov_model.init(gpa, testing.bounds); defer badanov_model.deinit(gpa); const badanov = try mission.add(.of(.badanov), .{ 0, 0, 10000 }); try badanov_model.parts.fit(gpa, mission.slot(badanov)); @@ -186,8 +202,8 @@ test "a ship launches from the Badanov's bay" { var mission: gameobj.testing.Mission = undefined; try mission.init(gpa); defer mission.deinit(); - var badanov_model: TestBadanov = undefined; - try badanov_model.init(gpa, test_bounds); + var badanov_model: testing.Badanov = undefined; + try badanov_model.init(gpa, testing.bounds); defer badanov_model.deinit(gpa); _ = try mission.add(.of(.predator), @splat(0)); const badanov = try mission.add(.of(.badanov), .{ 0, 0, 10000 }); diff --git a/src/engine/game/main.zig b/src/engine/game/main.zig index 337d102e..d95bddb3 100644 --- a/src/engine/game/main.zig +++ b/src/engine/game/main.zig @@ -1145,10 +1145,10 @@ fn holdRiders(all: *create.Objects, timing: objects.Timing, now: i32) void { } /// Places the ship in slot `index` on the node it rides, as a launch holds it in its carrier's bay -/// (`launch.hold`) or the Yamato's landing on its bay's pad (`ailand.hold`). Returns whether it -/// placed the ship. +/// (`launch.hold`) or the Yamato's landing on its bay's pad (`ailand.yamato.hold`). Returns +/// whether it placed the ship. fn holdRider(all: *create.Objects, index: u16) bool { - return launch.hold(all, index) or ailand.hold(all, index); + return launch.hold(all, index) or ailand.yamato.hold(all, index); } /// Frames the object in `slot` as far through the simulation's step as `timing` says diff --git a/src/engine/game/objects.zig b/src/engine/game/objects.zig index 0f8d6c4c..58619f28 100644 --- a/src/engine/game/objects.zig +++ b/src/engine/game/objects.zig @@ -2832,7 +2832,7 @@ pub const Timing = struct { }; /// When the frame places a ship riding a node on it (`main.frameObjects`, `launch.hold`, -/// `ailand.hold`). +/// `ailand.yamato.hold`). pub const Riders = enum { /// **Improvement:** once more after every object is framed, so that it keeps with a node /// framed after it, as the player's ship keeps with the hangar's retainer that lowers it. diff --git a/src/engine/game/radio.zig b/src/engine/game/radio.zig index a387fde6..92caf24a 100644 --- a/src/engine/game/radio.zig +++ b/src/engine/game/radio.zig @@ -18,6 +18,7 @@ const log = std.log.scoped(.radio); const hog = @import("../../formats/hog.zig"); const bigfile = @import("bigfile.zig"); const aigeneric = @import("aigeneric.zig"); +const ailand = @import("ailand.zig"); const cbox = @import("cbox.zig"); const create = @import("create.zig"); const gameobj = @import("gameobj.zig"); @@ -64,20 +65,26 @@ pub const permission_every: u32 = 500; /// In training (`create.Objects.training`), where the script's `landing_cleared` is set, the pilot /// asks (`playerSays`, `hud_012`), and the flight instructor answers in `report_delay` ticks /// (`trnglnd_001`) and clears the player's ship to land. Otherwise, unless the ship is landing -/// already, the pilot asks, and the bridge of the carrier it launched from answers in -/// `report_delay` ticks (`bridgeLine`): where `landing_cleared` is set it clears the ship, by a -/// line the script's `mission_success` picks at random from its lines, and otherwise it refuses, -/// by one of the refusals. A ship cleared lands on that carrier (`ailand`). A report +/// already, the pilot asks, and the bridge of the player's carrier, the ship it launched from, +/// answers in `report_delay` ticks (`bridgeLine`): where `landing_cleared` is set it clears the +/// ship, by a line the script's `mission_success` picks at random from its lines, and otherwise it +/// refuses, by one of the refusals. A ship cleared lands on that carrier (`ailand`). A report /// that finds the radio's reports all taken is not said, and a ship refused that way is not -/// cleared either. The radio's menu asks too (`menu.Page.permission_to_land`). +/// cleared either. The radio's menu asks too (`menu.Page.permission_to_land`). Mods' scripts can +/// hook it (`permission_to_land`). /// /// **Fix:** the game reads through a null pointer where the player's ship launched from no /// carrier; OpenReliant asks nothing. /// +/// **Improvement:** the game records the carrier only after a launch from the Reliant or the +/// Yamato. OpenReliant asks only where the player's carrier offers a landing +/// (`ailand.landingCarrier`), and asks nothing otherwise, as where there is no carrier. +/// /// Not ported: the debug line it writes naming the mission's rating, which nothing shows; and a /// multiplayer game's side of it, in which a remote player's ship is cleared whatever the script /// says ([#55](https://github.com/OpenReliant/openreliant/issues/55)). pub fn permissionToLand(world: gameobj.World, game_ticks: u32) void { + if (hooks.enter(.permission_to_land, permissionToLand, .{ world, game_ticks })) |done| return done; const player = world.player; if (game_ticks < player.permission_heard_from) return; player.permission_heard_from = game_ticks + permission_every; @@ -103,7 +110,7 @@ pub fn permissionToLand(world: gameobj.World, game_ticks: u32) void { return; } if (landing(all)) return; - const carrier = player.carrier orelse return; + const carrier = ailand.landingCarrier(world) orelse return; playerSays(world, request_line); const cleared = variables.landing_cleared != 0; if (radio) |heard| { @@ -116,7 +123,9 @@ pub fn permissionToLand(world: gameobj.World, game_ticks: u32) void { /// A report of the bridge of `carrier` to the player, said `report_delay` ticks after /// `game_ticks`: one of `lines`, as its officer says it (`bridgeLine`), under the name of the /// Yamato's officer where the carrier is a Yamato and the Reliant's otherwise. False where the -/// radio's reports are all taken. +/// radio's reports are all taken. A carrier's type can't name an officer of its own yet, so a +/// mod's carrier answers in the Reliant's voice +/// ([#1340](https://github.com/OpenReliant/openreliant/issues/1340)). fn reportBridge(world: gameobj.World, radio: *Radio, carrier: u16, lines: []const []const u8, game_ticks: u32) bool { const place = radio.freeReport() orelse return false; const all = world.objects; @@ -688,7 +697,8 @@ const training_launch_line = "trnlch_001.ut"; /// `radio_launch_line` (`0x00456E50`), as the player's ship's launch from the ship in slot /// `carrier` goes (`launch.update`), while the script lets the remarks be said and the radio is /// free: the flight instructor speaks in training, and in any other mission the bridge officer -/// of the carrier, a Reliant or a Yamato. A launch from anything else goes unremarked. +/// of the carrier, a Reliant or a Yamato. A launch from anything else goes unremarked, a mod's +/// carrier included ([#1340](https://github.com/OpenReliant/openreliant/issues/1340)). pub fn launchLine(world: gameobj.World, carrier: u16) void { if (world.player.remarks.generic_comms_disabled) return; const all = world.objects; @@ -1682,6 +1692,17 @@ test permissionToLand { mission.player.carrier = null; permissionToLand(world, 10000); try std.testing.expectEqual(0, slot.object.order_count); + + // Nor does one whose carrier offers no landing, as the Victorious does until a mod or a script + // gives it one; then the ship lands on it. + const victorious = try mission.add(.of(.victorious), .{ 0, 0, 2000 }); + mission.player.carrier = victorious; + permissionToLand(world, 20000); + try std.testing.expectEqual(0, slot.object.order_count); + mission.slot(victorious).landing = .bay; + permissionToLand(world, 30000); + try std.testing.expectEqual(.land, slot.current().?.order); + try std.testing.expectEqual(victorious, slot.current().?.target.slotIn(mission.objects).?); } /// What the tests of the radio's users share. diff --git a/src/engine/hooks.zig b/src/engine/hooks.zig index a39455c2..bc58b2c5 100644 --- a/src/engine/hooks.zig +++ b/src/engine/hooks.zig @@ -242,6 +242,13 @@ pub const functions = struct { .subject = null, }; + pub const permission_to_land: Declaration = .{ + .address = 0x00453DE0, + .about = "The player asks for PERMISSION TO LAND, with its key or the radio's menu, heard at most once in 500 ticks. The pilot asks, and the flight instructor answers in training, or else the player's carrier, where it offers a landing (`world.player_carrier`, the object's `landing`). Once the mission's script has set `landing_cleared`, the answer clears the ship, which lands; otherwise the carrier refuses. A handler that stops it can answer and land the ship its own way, such as with an order of its own.", + .Fields = struct { _game_ticks: u32 }, + .subject = null, + }; + pub const missile_launch: Declaration = .{ .address = 0x00496290, .about = "`launcher` launches a missile from one of its racks at `target`.", diff --git a/src/openreliant/options.zig b/src/openreliant/options.zig index e9852ab8..27176bf4 100644 --- a/src/openreliant/options.zig +++ b/src/openreliant/options.zig @@ -320,7 +320,7 @@ pub const Options = struct { hangar_beacons: game.objects.HangarBeacons = .to_the_ship, launch_steam: game.launch.yamato.Steam = .soft, /// How the Reliant's landing brings the ship down. - touchdown: game.ailand.Touchdown = .level, + touchdown: game.ailand.reliant.Touchdown = .level, /// When the player hears their own ship jump in. arrival_sound: game.jump.ArrivalSound = .heard, /// How the radio's lines sound. diff --git a/src/scripting/game.zig b/src/scripting/game.zig index 7ed20342..dabc7cea 100644 --- a/src/scripting/game.zig +++ b/src/scripting/game.zig @@ -956,6 +956,40 @@ test "a script stops or changes the radio's lines" { try std.testing.expectEqualStrings("mphud_001.ut", radio.queue[1].speech.slice()); } +test "a script answers PERMISSION TO LAND its own way" { + const radio_module = openreliant.engine.game.radio; + var fixture: Fixture = undefined; + try fixture.init(&.{.{ + "a", + &.{ + .{ "mod.ini", "[Scripts]\nGlobal=a.luau\n" }, + .{ + "a.luau", + \\local hooks = require("openreliant.hooks") + \\local stopped = false + \\hooks.add("permission_to_land", function() + \\ if not stopped then + \\ stopped = true + \\ return false + \\ end + \\end) + }, + }, + }}); + defer fixture.deinit(); + var variables: openreliant.engine.vm.Variables = .{}; + variables.landing_cleared = 1; + var asking = fixture.mission.world(); + asking.variables = &variables; + fixture.mission.player.carrier = try fixture.mission.add(.of(.reliant), .{ 0, 0, 5000 }); + const player = &fixture.mission.objects.slots[fixture.mission.objects.player]; + // Stopped, the request goes unanswered, and the ship lands on nothing; let through, it lands. + radio_module.permissionToLand(asking, 100); + try std.testing.expectEqual(0, player.object.order_count); + radio_module.permissionToLand(asking, 100 + radio_module.permission_every); + try std.testing.expectEqual(.land, player.current().?.order); +} + test "a script says lines on the radio, as the comms commands do" { const radio_module = openreliant.engine.game.radio; const hog_snd = openreliant.engine.game.hog_snd; @@ -1145,6 +1179,38 @@ test "scripts list a ship's parts, destroy it, and set the player's targets" { try std.testing.expect(slot.object.flags.exploding); } +test "a script lets the player land on a ship, and makes it the player's carrier" { + var fixture: Fixture = undefined; + try fixture.init(&.{.{ + "a", + &.{ + .{ "mod.ini", "[Scripts]\nGlobal=a.luau\n" }, + .{ + "a.luau", + \\local world = require("openreliant.world") + \\return { + \\ engine_handlers = { + \\ on_mission_start = function() + \\ local sabre = world.objects()[2] + \\ assert(world.player_carrier == nil and sabre.landing == nil) + \\ sabre.landing = "bay" + \\ assert(sabre.landing == "bay") + \\ assert(not pcall(function() sabre.landing = "runway" end)) + \\ assert(world.set_player_carrier(sabre)) + \\ assert(world.player_carrier == sabre) + \\ end, + \\ }, + \\} + }, + }, + }}); + defer fixture.deinit(); + fixture.begin(); + fixture.game.scripts.started(.{ .number = 5, .file = "mission5.dte" }); + try std.testing.expectEqual(fixture.sabre, fixture.mission.player.carrier.?); + try std.testing.expectEqual(openreliant.engine.game.ailand.Style.bay, fixture.mission.objects.slots[fixture.sabre].landing.?); +} + test "a script chooses the movies of a mission's end, and the restart screen's choice" { const engine_game = openreliant.engine.game; const movie = engine_game.xtrabits.movie; diff --git a/src/scripting/objects.zig b/src/scripting/objects.zig index 22766748..4b5028c4 100644 --- a/src/scripting/objects.zig +++ b/src/scripting/objects.zig @@ -306,6 +306,16 @@ pub const fields = struct { } }); + pub const landing = api.Field(?engine.game.ailand.Style, "How the player's ship lands on it, as its type's record has it when it's made (`ShipTypeRecord.landing`); nil where the player can't land on it. Setting it lets the player land on this ship, or keeps the player off it, for the rest of the mission. PERMISSION TO LAND and a mission's land order only bring the player's ship down on a ship that offers a landing.", struct { + pub fn get(all: *const create.Objects, index: u16) ?engine.game.ailand.Style { + return all.slots[index].landing; + } + + pub fn set(_: Call, all: *create.Objects, index: u16, value: ?engine.game.ailand.Style) void { + all.slots[index].landing = value; + } + }); + pub const disabled = Flag("disabled", "Whether it's left out of the mission's work, as a mission's DisableObject leaves it."); pub const guns_disabled = Flag("guns_disabled", "Whether its guns don't fire and its turrets rest, as a mission's DisableGuns sets."); pub const missiles_disabled = Flag("missiles_disabled", "Whether it can't launch missiles, as a mission's DisableMissiles sets."); diff --git a/src/scripting/records.zig b/src/scripting/records.zig index 6c725cb9..63893703 100644 --- a/src/scripting/records.zig +++ b/src/scripting/records.zig @@ -4,7 +4,8 @@ //! //! The record tables are `ships`, `guns`, `missiles` and `pilots` from the stat files //! ([Stat tables](../../docs/formats/stats.md)); `ship_types`, the class and the other words the -//! game's executable holds for each ship type; `faces`, the pilots' faces from the executable; +//! game's executable holds for each ship type, and how the player lands on its ships; `faces`, the +//! pilots' faces from the executable; //! `text` from `language.dll`; and `itac_text` from the ITAC's `itaclang.dll`. Records are indexed //! by the game's numbers: guns and text from 1, the rest from 0. All but the text can also be //! looked up by OpenReliant's names, such as `records.guns.laser_cannon` or @@ -74,7 +75,8 @@ pub const Records = struct { ships: []stats.Ship, /// Each ship type's words that the game keeps in its executable rather than `shipstats.bin`: /// its class, its side, the string that names it, whether it can be targeted and which form of - /// the target display shows it (`ship_combat_stats`). + /// the target display shows it (`ship_combat_stats`); and OpenReliant's own, how the player + /// lands on its ships (`ailand.Style.of`). ship_types: []game.create.combat_stats.Static, guns: []stats.Gun, missiles: []stats.Missile, @@ -830,9 +832,13 @@ test "the ship types' words the executable holds" { \\assert(shuttle.class == "support" and shuttle.side == "hostile" and tostring(shuttle) == "ShipTypeRecord") \\assert(records.ship_types[0].class == "fighter") \\shuttle.class = "fighter" + \\assert(records.ship_types.victorious.landing == nil) + \\records.ship_types.victorious.landing = "bay" ); try std.testing.expectEqual(.fighter, records.ship_types[@backingInt(game.gameobj.GameType.yakob_shuttle)].class); + try std.testing.expectEqual(game.ailand.Style.bay, records.ship_types[@backingInt(game.gameobj.GameType.victorious)].landing.?); try bind.testing.expectSourceError(thread, "records.ship_types.yakob_shuttle.class = 'gunboat'", "class"); + try bind.testing.expectSourceError(thread, "records.ship_types.victorious.landing = 'runway'", "landing"); } test "the pilots' faces" { @@ -891,7 +897,7 @@ test "the field names scripts see don't change" { .{ stats.Missile, "name,speed,turn_rate,flight_time,damage,lock_time,decoy_chance,lock_range,component_damage" }, .{ stats.Pilot, "name,tier_a,tier_b,tier_c,skill" }, .{ game.pilots.FaceRecord, "name,talking,laughing,squadron,dying" }, - .{ game.create.combat_stats.Static, "targetable,name,class,side,display" }, + .{ game.create.combat_stats.Static, "targetable,name,class,side,display,landing" }, }; inline for (expected) |pinned| { comptime var names: []const u8 = ""; diff --git a/src/scripting/world.zig b/src/scripting/world.zig index 1b92b1c9..48f022c3 100644 --- a/src/scripting/world.zig +++ b/src/scripting/world.zig @@ -24,6 +24,7 @@ pub const package = struct { pub const set_objective = api.Function("Objective `objective` of the mission that runs, numbered from 0 to 9 as a mission's SetObjective numbers them, takes `state`, as SetObjective does: `hidden`, `listed`, or `current`, which the objectives window then shows. Returns whether it changed: false between missions, and in a mission whose objectives nothing names.", &.{ "objective", "state" }, setObjective); pub const variable = api.Function("The value of the game's variable `name` in the mission that runs, as the mission's script reads it. Nil between missions.", &.{"name"}, readVariable); pub const set_variable = api.Function("Sets the game's variable `name` to `value` in the mission that runs, as the mission's script sets it. The campaign keeps the value from one mission to the next where it keeps the variable. Returns false between missions.", &.{ "name", "value" }, setVariable); + pub const set_player_carrier = api.Function("Makes `ship` the player's carrier (`player_carrier`): PERMISSION TO LAND asks it, its bridge answers, and the player's ship lands on it where it offers a landing (its `landing`). Nil leaves the player without a carrier. Returns false between missions.", &.{"ship"}, setPlayerCarrier); pub const player = api.Field(?Object, "The player's ship, while a mission runs; nil between missions.", struct { pub fn get(call: Call) ?Object { @@ -39,8 +40,23 @@ pub const package = struct { return if (held.mission) |*running| running.view() else null; } }); + + pub const player_carrier = api.Field(?Object, "The player's carrier: the ship the player launched from, unless a script sets another (`set_player_carrier`). PERMISSION TO LAND asks it, and its bridge speaks to the player. Nil where there is none, as in a mission the player starts in flight, and between missions.", struct { + pub fn get(call: Call) ?Object { + const orders = call.runtime().orders orelse return null; + return .of(orders.world.player.carrier orelse return null); + } + }); }; +/// `world.set_player_carrier(ship)`: `gameobj.Player.carrier`, which a launch of the player's ship +/// sets (`launch.init`). +fn setPlayerCarrier(call: Call, ship: ?Object) bool { + const orders = call.runtime().orders orelse return false; + orders.world.player.carrier = if (ship) |carrier| carrier.slot() else null; + return true; +} + /// `world.set_objective(objective, state)`: `hud.Objectives.set`, as a mission's `SetObjective` /// calls it. fn setObjective(call: Call, objective: u8, state: hud.Objectives.Status) bool { diff --git a/src/tools/tablegen/combat.zig b/src/tools/tablegen/combat.zig index ab01e0e5..cfb18fd9 100644 --- a/src/tools/tablegen/combat.zig +++ b/src/tools/tablegen/combat.zig @@ -67,6 +67,7 @@ pub fn emit(w: *Io.Writer, types: []const Static) Io.Writer.Error!void { \\//! Generated by `src/tools/tablegen` from the payload executable's `ship_combat_stats` at \\//! 0x{X:0>8}. Do not edit by hand; run `make combat-tables`. \\ + \\const ailand = @import("../ailand.zig"); \\const create = @import("../create.zig"); \\const gameobj = @import("../gameobj.zig"); \\ @@ -84,6 +85,10 @@ pub fn emit(w: *Io.Writer, types: []const Static) Io.Writer.Error!void { \\ /// The side its objects start on. It has `GameObject.side`'s type, so scripts see one `Side`. \\ side: gameobj.Side(i32), \\ display: create.ShipCombat.TargetDisplay, + \\ /// OpenReliant's own: how the player lands on a ship of the type, which the game keeps in its + \\ /// code rather than in this table. The records start from the game's (`ailand.Style.of`). + \\ /// Null where the player can't land on it. + \\ landing: ?ailand.Style = null, \\}}; \\ \\/// Every ship type's, by its number.