diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ec312ae..4e39de0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -77,7 +77,7 @@ jobs: shell: bash - name: run pytest - run: python -m pytest + run: python -m pytest --skip-if-missing full_test: runs-on: self-hosted diff --git a/pyproject.toml b/pyproject.toml index e0aaca6..94dcf11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ requires-python = ">=3.10" dynamic = ["version"] dependencies = [ - "retro-data-structures>=0.20.1", + "retro-data-structures>=0.21.0", "jsonschema>=4.0.0", "ppc-asm", "py_randomprime", # for Prime 1 symbols diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst.TXTR index 3243e0e..263ac04 100644 Binary files a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst.TXTR and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst.TXTR differ diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst_emissive.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst_emissive.TXTR new file mode 100644 index 0000000..b16f542 Binary files /dev/null and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_darkburst_emissive.TXTR differ diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor.TXTR new file mode 100644 index 0000000..660acd4 Binary files /dev/null and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor.TXTR differ diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor_emissive.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor_emissive.TXTR new file mode 100644 index 0000000..71d2da9 Binary files /dev/null and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_echo_visor_emissive.TXTR differ diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack.TXTR new file mode 100644 index 0000000..38e43d5 Binary files /dev/null and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack.TXTR differ diff --git a/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack_emissive.TXTR b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack_emissive.TXTR new file mode 100644 index 0000000..36e1c44 Binary files /dev/null and b/src/open_prime_rando/echoes/custom_assets/doors/custom_door_lock_screwattack_emissive.TXTR differ diff --git a/src/open_prime_rando/echoes/dock_lock_rando/__init__.py b/src/open_prime_rando/echoes/dock_lock_rando/__init__.py index e3ed8e3..eda0bff 100644 --- a/src/open_prime_rando/echoes/dock_lock_rando/__init__.py +++ b/src/open_prime_rando/echoes/dock_lock_rando/__init__.py @@ -12,18 +12,25 @@ def add_custom_models(editor: PatcherEditor): assets = Path(__file__).parent.parent.joinpath("custom_assets", "doors") - def get_txtr(n: str) -> AssetId: + def get_txtr(n: str, must_exist: bool = True) -> AssetId: + f = assets.joinpath(n) + if not must_exist and not f.exists(): + return None res = RawResource( type="TXTR", - data=assets.joinpath(f"{n}.TXTR").read_bytes() + data=f.read_bytes() ) - return editor.add_file(f"{n}.TXTR", res, []) + return editor.add_file(n, res) - emissive = get_txtr("custom_door_lock_greyscale_emissive") + greyscale_emissive = get_txtr("custom_door_lock_greyscale_emissive.TXTR") template = editor.get_parsed_asset(0xF115F575, type_hint=Cmdl) - for name in ("darkburst", "sunburst", "sonicboom"): - txtr = get_txtr(f"custom_door_lock_{name}") + for name in ("darkburst", "sunburst", "sonicboom", "screwattack", "echo_visor"): + txtr = get_txtr(f"custom_door_lock_{name}.TXTR") + emissive = get_txtr(f"custom_door_lock_{name}_emissive.TXTR", must_exist=False) + if emissive is None: + emissive = greyscale_emissive + cmdl = Container(template.raw) cmdl.material_sets[0].texture_file_ids[0] = txtr cmdl.material_sets[0].texture_file_ids[1] = emissive diff --git a/src/open_prime_rando/echoes/dock_lock_rando/dock_type.py b/src/open_prime_rando/echoes/dock_lock_rando/dock_type.py index cfa1264..8c67d03 100644 --- a/src/open_prime_rando/echoes/dock_lock_rando/dock_type.py +++ b/src/open_prime_rando/echoes/dock_lock_rando/dock_type.py @@ -1,5 +1,4 @@ import dataclasses -from typing import NamedTuple from retro_data_structures.asset_manager import NameOrAssetId from retro_data_structures.base_resource import AssetId @@ -30,7 +29,9 @@ from retro_data_structures.properties.echoes.objects.Dock import Dock from retro_data_structures.properties.echoes.objects.Door import Door from retro_data_structures.properties.echoes.objects.Effect import Effect +from retro_data_structures.properties.echoes.objects.HUDHint import HUDHint from retro_data_structures.properties.echoes.objects.MemoryRelay import MemoryRelay +from retro_data_structures.properties.echoes.objects.PlayerController import PlayerController from retro_data_structures.properties.echoes.objects.Relay import Relay from retro_data_structures.properties.echoes.objects.ScannableObjectInfo import ScannableObjectInfo from retro_data_structures.properties.echoes.objects.Sound import Sound @@ -153,8 +154,8 @@ def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, doc with door.edit_properties(Door) as door_props: door_props.vulnerability = self.vulnerability - -class BlastShieldActors(NamedTuple): +@dataclasses.dataclass +class BlastShieldActors: door: ScriptInstance sound: ScriptInstance streamed: ScriptInstance @@ -187,6 +188,40 @@ def get_spline(self) -> Spline: b'\x02A \x00\x00?\x80\x00\x00\x02\x02\x01\x00\x00\x00\x00?\x80\x00\x00' ) + def create_trigger(self, + name: str, + door_xfm: Transform, + health: float, + visor_flags: VisorFlags = VisorFlags.Combat | VisorFlags.Dark, + active: bool = True, + seeker_lock_on: bool = True, + orbitable: bool = False, + ): + pos = Vector( + door_xfm.position.x, + door_xfm.position.y, + door_xfm.position.z + 1.8 + ) + + return DamageableTriggerOrientated( + editor_properties=EditorProperties( + name=name, + transform=Transform( + position=pos, + rotation=door_xfm.rotation, + scale=Vector(4.0, 4.0, 1.5) + ), + active=active + ), + health=HealthInfo(health=health), + vulnerability=self.vulnerability, + enable_seeker_lock_on=seeker_lock_on, + orbitable=orbitable, + visor=VisorParameters( + visor_flags=visor_flags + ) + ) + def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, dock_name: str, low_memory: bool): """ blast shield connections: @@ -321,30 +356,6 @@ def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, doc door_transform = actors.door.get_properties_as(Door).editor_properties.transform - def create_trigger(name: str, health: float, lock_on: bool = True) -> ScriptInstance: - pos = Vector( - door_transform.position.x, - door_transform.position.y, - door_transform.position.z + 1.8 - ) - - return default.add_instance_with(DamageableTriggerOrientated( - editor_properties=EditorProperties( - name=name, - transform=Transform( - position=pos, - rotation=door_transform.rotation, - scale=Vector(4.0, 4.0, 1.5) - ), - ), - health=HealthInfo(health=health), - vulnerability=self.vulnerability, - enable_seeker_lock_on=lock_on, - visor=VisorParameters( - visor_flags=VisorFlags.Combat | VisorFlags.Dark - ) - )) - timer = default.add_instance_with(Timer( editor_properties=EditorProperties( name="Button Control", @@ -364,9 +375,19 @@ def create_trigger(name: str, health: float, lock_on: bool = True) -> ScriptInst # create 5 triggers so that you can have 5 lock-ons # 30.01 health because splash damage is inconsistent. missiles do 30 damage # so this guarantees you need at least 2 missiles at once to break it - triggers = [create_trigger(f"Bridge Button {i}", 30.01) for i in range(5)] + triggers = [ + default.add_instance_with(self.create_trigger( + f"Bridge Button {i}", + door_transform, 30.01) + ) for i in range(5) + ] main_trigger = triggers[0] - mini_trigger = create_trigger("Bridge Button Mini", 1.0, False) + mini_trigger = default.add_instance_with(self.create_trigger( + "Bridge Button Mini", + door_transform, + 1.0, + seeker_lock_on=False + )) # start a timer when the tiny trigger dies. stop it if the main trigger dies mini_trigger.add_connection(State.Dead, Message.ResetAndStart, timer) @@ -386,7 +407,7 @@ def create_trigger(name: str, health: float, lock_on: bool = True) -> ScriptInst main_trigger.add_connection( connection.state, connection.message, - default.get_instance(connection.target) + connection.target ) for trigger in triggers: @@ -426,16 +447,183 @@ def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, doc raise NotImplementedError() +@dataclasses.dataclass +class VisorBlastShieldActors(BlastShieldActors): + trigger: ScriptInstance + visor_detector: ScriptInstance + + @dataclasses.dataclass(kw_only=True) -class DarkVisorDoorType(BlastShieldDoorType): +class VisorDoorType(BlastShieldDoorType): + visor_flags: VisorFlags + player_controller_proxy: int + def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, dock_name: str, low_memory: bool): - raise NotImplementedError() + actors = super().patch_door(editor, world_name, area_name, dock_name, low_memory) + area = self.get_area(editor, world_name, area_name) + default = area.get_layer("Default") + + with actors.door.edit_properties(Door) as door: + door.vulnerability = resist_all_vuln + door_xfm = door.editor_properties.transform + + + with actors.lock.edit_properties(Actor) as lock: + lock.vulnerability = resist_all_vuln + + trigger = default.add_instance_with(self.create_trigger( + "Door Button", door_xfm, 1.0, self.visor_flags, + active=False, seeker_lock_on=False, orbitable=True + )) + + visor_detector = default.add_instance_with(PlayerController( + editor_properties=EditorProperties( + name="Detect Visor", + transform=door_xfm + ), + unknown_0xe71de331=1, + proxy_type=self.player_controller_proxy + )) + + visor_detector.add_connection(State.Entered, Message.Activate, trigger) + visor_detector.add_connection(State.Exited, Message.Deactivate, trigger) + + for connection in actors.lock.connections: + actors.lock.remove_connection(connection) + trigger.add_connection( + connection.state, + connection.message, + connection.target + ) + + actors.relay.add_connection(State.Active, Message.Deactivate, trigger) + actors.relay.add_connection(State.Active, Message.Deactivate, visor_detector) + + return VisorBlastShieldActors( + actors.door, actors.sound, actors.streamed, + actors.lock, actors.relay, actors.gibs, + trigger, visor_detector + ) @dataclasses.dataclass(kw_only=True) -class EchoVisorDoorType(BlastShieldDoorType): +class DarkVisorDoorType(VisorDoorType): def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, dock_name: str, low_memory: bool): - raise NotImplementedError() + actors = super().patch_door(editor, world_name, area_name, dock_name, low_memory) + + with actors.lock.edit_properties(Actor) as lock: + # TODO: update the template + # this property makes the actor appear red in dark visor + lock.actor_information.unknown_0xcd4c81a1 = True + + +@dataclasses.dataclass(kw_only=True) +class EchoVisorDoorType(VisorDoorType): + def patch_door(self, editor: PatcherEditor, world_name: str, area_name: str, dock_name: str, low_memory: bool): + actors = super().patch_door(editor, world_name, area_name, dock_name, low_memory) + area = self.get_area(editor, world_name, area_name) + default = area.get_layer("Default") + + door_xfm = actors.door.get_properties_as(Door).editor_properties.transform + + center_pos = Vector( + door_xfm.position.x, + door_xfm.position.y, + door_xfm.position.z + 1.8 + ) + + hud_hint = default.add_instance_with(HUDHint( + editor_properties=EditorProperties( + name="Echo Target Icon", + transform=Transform( + center_pos, + door_xfm.rotation, + ), + active=True + ), + hud_texture=0x36B1CB06, + unknown_0x6078a651=24.0, + unknown_0xf00bb6bb=128.0, + animation_time=0.5, + animation_frames=4, + unknown_0xd993f97b=2 + )) + + beacon_loop = default.add_instance_with(Sound( + editor_properties=EditorProperties( + name="Echo Beacon Sound Loop", + transform=Transform( + position=center_pos + ), + ), + sound=1003, + max_audible_distance=75.0, + min_volume=0, + max_volume=60, + surround_pan=SurroundPan( + pan=0.0, + surround_pan=1.0 + ), + loop=True, + play_always=True, + echo_visor_max_volume=127 + )) + + disrupted = default.add_instance_with(Sound( + editor_properties=EditorProperties( + name="Echo Particle Disrupted", + transform=Transform( + position=center_pos + ), + ), + sound=1004, + max_audible_distance=75.0, + min_volume=20, + max_volume=120, + surround_pan=SurroundPan( + pan=0.0, + surround_pan=1.0 + ), + )) + + timer = default.add_instance_with(Timer( + editor_properties=EditorProperties( + name="Open Echo Door", + transform=door_xfm, + ), + time=1.0 + )) + for connection in actors.trigger.connections: + actors.trigger.remove_connection(connection) + timer.add_connection( + State.Zero, + connection.message, + connection.target + ) + + with actors.lock.edit_properties(Actor) as lock: + lock.echo_information.is_echo_emitter = True + + actors.trigger.add_connection(State.Dead, Message.ResetAndStart, timer) + actors.trigger.add_connection(State.Dead, Message.InternalMessage00, hud_hint) + actors.trigger.add_connection(State.Dead, Message.Stop, beacon_loop) + actors.trigger.add_connection(State.Dead, Message.Play, disrupted) + + actors.visor_detector.add_connection(State.Entered, Message.Play, beacon_loop) + actors.visor_detector.add_connection(State.Exited, Message.Stop, beacon_loop) + + actors.relay.add_connection(State.Active, Message.Deactivate, hud_hint) + actors.relay.add_connection(State.Active, Message.Deactivate, beacon_loop) + actors.relay.add_connection(State.Active, Message.Deactivate, timer) + actors.relay.add_connection(State.Active, Message.Deactivate, disrupted) + + dependencies = ( + 0x36B1CB06, # hud hint TXTR + 0x4FBCAC73 # DigitalGuardianBeacon.AGSC + ) + for pak in self.get_paks(editor, world_name, area_name): + for asset in dependencies: + editor.ensure_present(pak, asset) @dataclasses.dataclass(kw_only=True) diff --git a/src/open_prime_rando/echoes/dock_lock_rando/dock_type_database.py b/src/open_prime_rando/echoes/dock_lock_rando/dock_type_database.py index 1823c89..0f5d67d 100644 --- a/src/open_prime_rando/echoes/dock_lock_rando/dock_type_database.py +++ b/src/open_prime_rando/echoes/dock_lock_rando/dock_type_database.py @@ -1,11 +1,12 @@ import dataclasses +from retro_data_structures.enums.echoes import VisorFlags from retro_data_structures.properties.echoes.core.Color import Color from retro_data_structures.properties.echoes.core.Vector import Vector from open_prime_rando.echoes.dock_lock_rando import dock_type from open_prime_rando.echoes.dock_lock_rando.map_icons import DoorMapIcon -from open_prime_rando.echoes.vulnerabilities import resist_all_vuln, vulnerable +from open_prime_rando.echoes.vulnerabilities import normal_vuln, resist_all_vuln, vulnerable, vulnerable_no_splash normal_door_model = 0x6B78FD92 dark_door_model = 0xbbcf134d @@ -17,16 +18,8 @@ DOCK_TYPES: dict[str, dock_type.DoorType] = { "Normal": dock_type.NormalDoorType( name="Normal", - vulnerability=dataclasses.replace( - resist_all_vuln, - power=vulnerable, dark=vulnerable, light=vulnerable, annihilator=vulnerable, - power_charge=vulnerable, entangler=vulnerable, light_blast=vulnerable, sonic_boom=vulnerable, - super_missle=vulnerable, black_hole=vulnerable, sunburst=vulnerable, imploder=vulnerable, - - missile=vulnerable, bomb=vulnerable, power_bomb=vulnerable, - - ), - shell_color=Color(r=0, g=1, b=1, a=1), + vulnerability=normal_vuln, + shell_color=Color(0, 1, 1, 1), map_icon=DoorMapIcon.Normal, ), "Dark": dock_type.NormalDoorType( @@ -34,7 +27,7 @@ vulnerability=dataclasses.replace(resist_all_vuln, dark=vulnerable, entangler=vulnerable, black_hole=vulnerable), shell_model=dark_door_model, - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. Dark energy may damage it." @@ -45,7 +38,7 @@ name="Light", vulnerability=dataclasses.replace(resist_all_vuln, light=vulnerable, light_blast=vulnerable, sunburst=vulnerable), - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. Light energy may damage it." @@ -57,7 +50,7 @@ vulnerability=dataclasses.replace(resist_all_vuln, annihilator=vulnerable, sonic_boom=vulnerable, imploder=vulnerable), shell_model=annihilator_door_model, - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -68,7 +61,7 @@ "Disabled": dock_type.NormalDoorType( name="Disabled", vulnerability=resist_all_vuln, - shell_color=Color(r=0, g=0, b=0, a=0), + shell_color=Color(0, 0, 0, 0), scan_text=( "Door system access denied.", "Unable to bypass security codes. Seek an alternate exit." @@ -78,7 +71,7 @@ "Missile": dock_type.VanillaBlastShieldDoorType( name="Missile", vulnerability=dataclasses.replace(resist_all_vuln, missile=vulnerable), - shell_color=Color(r=1, g=0, b=0, a=1), + shell_color=Color(1, 0, 0, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. A Missile blast may damage it." @@ -89,7 +82,7 @@ "SuperMissile": dock_type.VanillaBlastShieldDoorType( name="SuperMissile", vulnerability=dataclasses.replace(resist_all_vuln, super_missle=vulnerable), - shell_color=Color(r=0, g=1, b=0, a=1), + shell_color=Color(0, 1, 0, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -101,7 +94,7 @@ "PowerBomb": dock_type.VanillaBlastShieldDoorType( name="PowerBomb", vulnerability=dataclasses.replace(resist_all_vuln, power_bomb=vulnerable), - shell_color=Color(r=1, g=0.94, b=0, a=1), + shell_color=Color(1, 0.94, 0, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. A Power Bomb may damage it." @@ -112,7 +105,7 @@ "SeekerMissile": dock_type.SeekerBlastShieldDoorType( name="SeekerMissile", vulnerability=dataclasses.replace(resist_all_vuln, missile=vulnerable), - shell_color=Color(r=0.5, g=0, b=0.64, a=1), + shell_color=Color(0.5, 0, 0.64, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -124,45 +117,49 @@ "ScrewAttack": dock_type.BlastShieldDoorType( name="ScrewAttack", vulnerability=dataclasses.replace(resist_all_vuln, screw_attack=vulnerable), - shell_model=annihilator_door_model, - shell_color=Color(r=0, g=0, b=1, a=1), + shell_model=normal_door_model, + shell_color=Color(0.93, 0.58, 0.83, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. The Screw Attack may damage it." ), map_icon=DoorMapIcon.ScrewAttack, - shield_model=0x56F4208B, + shield_model="custom_door_lock_screwattack.CMDL", ), "Bomb": dock_type.BlastShieldDoorType( name="Bomb", vulnerability=dataclasses.replace(resist_all_vuln, bomb=vulnerable), - shell_model=annihilator_door_model, - shell_color=Color(r=0, g=1, b=1, a=1), + shell_model=normal_door_model, + shell_color=Color(1/3, 1/3, 0.5, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " "A Morph Ball Bomb blast may damage it." ), map_icon=DoorMapIcon.Bomb, - shield_model=0x56F4208B, + shield_model="custom_door_lock_sonicboom.CMDL", ), "Boost": dock_type.BlastShieldDoorType( name="Boost", - vulnerability=dataclasses.replace(resist_all_vuln, boost_ball=vulnerable, cannon_ball=vulnerable), - shell_model=annihilator_door_model, - shell_color=Color(r=1, g=1, b=0, a=1), + vulnerability=dataclasses.replace( + resist_all_vuln, + boost_ball=vulnerable, + cannon_ball=vulnerable_no_splash + ), + shell_model=normal_door_model, + shell_color=Color(1, 1/3, 0, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. The Boost Ball may damage it." ), map_icon=DoorMapIcon.Boost, - shield_model=0x56F4208B, + shield_model=0xBFB4A8EE, ), "Grapple": dock_type.GrappleDoorType( name="Grapple", vulnerability=resist_all_vuln, shell_model=annihilator_door_model, - shell_color=Color(r=1, g=0, b=0, a=1), + shell_color=Color(1, 0, 0, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to weapon fire, but is weakly secured. " @@ -175,7 +172,7 @@ name="Darkburst", vulnerability=dataclasses.replace(resist_all_vuln, black_hole=vulnerable), shell_model=dark_door_model, - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -188,7 +185,7 @@ name="Sunburst", vulnerability=dataclasses.replace(resist_all_vuln, sunburst=vulnerable), shell_model=normal_door_model, - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -201,7 +198,7 @@ name="SonicBoom", vulnerability=dataclasses.replace(resist_all_vuln, imploder=vulnerable), shell_model=annihilator_door_model, - shell_color=Color(r=1, g=1, b=1, a=1), + shell_color=Color(1, 1, 1, 1), scan_text=( "There is a Blast Shield on the door blocking access. ", "Analysis indicates that the Blast Shield is invulnerable to most weapons. " @@ -213,7 +210,7 @@ "AgonEnergy": dock_type.PlanetaryEnergyDoorType( name="AgonEnergy", vulnerability=resist_all_vuln, - shell_color=Color(r=0.64, g=0.34, b=0, a=1), + shell_color=Color(0.64, 0.34, 0, 1), scan_text=( "There is a Luminoth barrier on the door blocking access. ", "Analysis indicates that the barrier is linked to the energy of Agon. Return the energy to the Agon Temple." @@ -224,7 +221,7 @@ "TorvusEnergy": dock_type.PlanetaryEnergyDoorType( name="TorvusEnergy", vulnerability=resist_all_vuln, - shell_color=Color(r=0.31, g=0.59, b=38, a=1), + shell_color=Color(0.31, 0.59, 38, 1), scan_text=( "There is a Luminoth barrier on the door blocking access. ", "Analysis indicates that the barrier is linked to the energy of Torvus. " @@ -236,7 +233,7 @@ "SanctuaryEnergy": dock_type.PlanetaryEnergyDoorType( name="SanctuaryEnergy", vulnerability=resist_all_vuln, - shell_color=Color(r=0.64, g=0.34, b=0, a=1), + shell_color=Color(0.64, 0.34, 0, 1), scan_text=( "There is a Luminoth barrier on the door blocking access. ", "Analysis indicates that the barrier is linked to the energy of Sanctuary. " @@ -245,4 +242,34 @@ map_icon=DoorMapIcon.AgonEnergy, planetary_energy_item_id=-1 # TODO ), + "EchoVisor": dock_type.EchoVisorDoorType( + name="EchoVisor", + vulnerability=normal_vuln, + shell_model=dark_door_model, + shell_color=Color(1/16, 1/16, 1/16, 1), + scan_text=( + "There is a Blast Shield on the door blocking access. ", + "Sonic detection gear needed to interface with this system." + "Neutralizing the control emitter may disable it." + ), + map_icon=DoorMapIcon.EchoVisor, + shield_model="custom_door_lock_echo_visor.CMDL", + visor_flags=VisorFlags.Echo, + player_controller_proxy=8, + ), + "DarkVisor": dock_type.DarkVisorDoorType( + name="DarkVisor", + vulnerability=normal_vuln, + shell_model=dark_door_model, + shell_color=Color(1, 0, 0, 1), + scan_text=( + "There is a Blast Shield on the door blocking access. ", + "Scans indicate presence of a control system." + "Interface method unknown. Control units not present in the visible spectrum or current timespace. " + ), + map_icon=DoorMapIcon.DarkVisor, + shield_model=0xBFB4A8EE, + visor_flags=VisorFlags.Dark, + player_controller_proxy=7, + ) } diff --git a/src/open_prime_rando/echoes/dock_lock_rando/map_icons.py b/src/open_prime_rando/echoes/dock_lock_rando/map_icons.py index 4721721..3524121 100644 --- a/src/open_prime_rando/echoes/dock_lock_rando/map_icons.py +++ b/src/open_prime_rando/echoes/dock_lock_rando/map_icons.py @@ -66,11 +66,11 @@ def get_door_index_bounds() -> tuple[int, int]: DoorMapIcon.ScanVisor: DoorIconColors(0x007f7fff), DoorMapIcon.DarkVisor: DoorIconColors(0x660000ff), - # DoorMapIcon.EchoVisor: None, + DoorMapIcon.EchoVisor: DoorIconColors(0xcc7a00ff), - # DoorMapIcon.ScrewAttack: None, - # DoorMapIcon.Bomb: None, - # DoorMapIcon.Boost: None, + DoorMapIcon.ScrewAttack: DoorIconColors(0xed94d4ff), + DoorMapIcon.Bomb: DoorIconColors(0x55557fff), + DoorMapIcon.Boost: DoorIconColors(0xff5500ff), # DoorMapIcon.Grapple: None, DoorMapIcon.Disabled: DoorIconColors(0x202020ff), diff --git a/src/open_prime_rando/echoes/schema.json b/src/open_prime_rando/echoes/schema.json index 74d40fb..b886ffe 100644 --- a/src/open_prime_rando/echoes/schema.json +++ b/src/open_prime_rando/echoes/schema.json @@ -201,7 +201,9 @@ "SonicBoom", "AgonEnergy", "TorvusEnergy", - "SanctuaryEnergy" + "SanctuaryEnergy", + "DarkVisor", + "EchoVisor" ] }, "area_identifier": { diff --git a/src/open_prime_rando/echoes/vulnerabilities.py b/src/open_prime_rando/echoes/vulnerabilities.py index 0399458..0625dd6 100644 --- a/src/open_prime_rando/echoes/vulnerabilities.py +++ b/src/open_prime_rando/echoes/vulnerabilities.py @@ -1,3 +1,5 @@ +import dataclasses + from retro_data_structures.enums import echoes from retro_data_structures.properties.echoes.archetypes.DamageVulnerability import DamageVulnerability from retro_data_structures.properties.echoes.archetypes.WeaponVulnerability import WeaponVulnerability @@ -5,6 +7,7 @@ reflect = WeaponVulnerability(damage_multiplier=0, effect=echoes.Effect.Reflect) vulnerable = WeaponVulnerability(damage_multiplier=100, effect=echoes.Effect.Normal) immune = WeaponVulnerability(damage_multiplier=0, effect=echoes.Effect.Normal, ignore_radius=True) +vulnerable_no_splash = WeaponVulnerability(damage_multiplier=100.0, effect=echoes.Effect.Normal, ignore_radius=True) resist_all_vuln = DamageVulnerability( @@ -13,7 +16,16 @@ super_missle=reflect, black_hole=reflect, sunburst=reflect, imploder=reflect, boost_ball=immune, cannon_ball=immune, screw_attack=immune, bomb=immune, power_bomb=immune, - missile=immune, phazon=reflect, ai=immune, poison_water=immune, dark_water=immune, lava=immune, + missile=reflect, phazon=reflect, ai=immune, poison_water=immune, dark_water=immune, lava=immune, area_damage_hot=immune, area_damage_cold=immune, area_damage_dark=immune, area_damage_light=immune, weapon_vulnerability=immune, normal_safe_zone=immune, ) + +normal_vuln = dataclasses.replace( + resist_all_vuln, + power=vulnerable, dark=vulnerable, light=vulnerable, annihilator=vulnerable, + power_charge=vulnerable, entangler=vulnerable, light_blast=vulnerable, sonic_boom=vulnerable, + super_missle=vulnerable, black_hole=vulnerable, sunburst=vulnerable, imploder=vulnerable, + + missile=vulnerable, bomb=vulnerable, power_bomb=vulnerable, +) diff --git a/src/open_prime_rando/echoes_patcher.py b/src/open_prime_rando/echoes_patcher.py index 3e607f3..fe49b35 100644 --- a/src/open_prime_rando/echoes_patcher.py +++ b/src/open_prime_rando/echoes_patcher.py @@ -95,7 +95,7 @@ def apply_area_modifications(editor: PatcherEditor, configuration: dict[str, dic new_strg = editor.add_file(f"custom_name_for_{area.internal_name}.STRG", strg, paks) area._raw.area_name_id = new_strg - area.build_mlvl_dependencies(only_modified=True) + area.update_all_dependencies(only_modified=True) def apply_corrupted_memory_card_change(editor: PatcherEditor): diff --git a/tests/conftest.py b/tests/conftest.py index 15f55d5..70cf37e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ from open_prime_rando.patcher_editor import PatcherEditor -_FAIL_INSTEAD_OF_SKIP = False +_FAIL_INSTEAD_OF_SKIP = True def get_env_or_skip(env_name): @@ -25,13 +25,24 @@ def prime2_iso_provider(): @pytest.fixture(scope="module") -def prime2_editor(prime2_iso_provider): +def raw_prime2_editor(prime2_iso_provider): return PatcherEditor(prime2_iso_provider, game=Game.ECHOES) +@pytest.fixture() +def prime2_editor(raw_prime2_editor): + editor = raw_prime2_editor + yield editor + editor.memory_files = {} + for custom_asset, asset_id in editor._custom_asset_ids.items(): + editor._paks_for_asset_id.pop(asset_id) + editor._custom_asset_ids = {} + editor._modified_resources = {} + + def pytest_addoption(parser): - parser.addoption('--fail-if-missing', action='store_true', dest="fail_if_missing", - default=False, help="Fails tests instead of skipping, in case any asset is missing") + parser.addoption('--skip-if-missing', action='store_false', dest="fail_if_missing", + default=True, help="Skip tests instead of missing, in case any asset is missing") def pytest_configure(config: pytest.Config): diff --git a/tests/echoes/test_echoes_dock_lock_rando.py b/tests/echoes/test_echoes_dock_lock_rando.py new file mode 100644 index 0000000..2472163 --- /dev/null +++ b/tests/echoes/test_echoes_dock_lock_rando.py @@ -0,0 +1,61 @@ +import contextlib +from unittest.mock import MagicMock + +import pytest + +from open_prime_rando.echoes import dock_lock_rando +from open_prime_rando.echoes.dock_lock_rando import DOCK_TYPES +from open_prime_rando.patcher_editor import PatcherEditor + +_custom_asset_ids = { + 'custom_door_lock_darkburst.CMDL': 3171862285, + 'custom_door_lock_darkburst.TXTR': 3672172162, + 'custom_door_lock_darkburst_emissive.TXTR': 234173793, + 'custom_door_lock_echo_visor.CMDL': 2853277507, + 'custom_door_lock_echo_visor.TXTR': 3456085708, + 'custom_door_lock_echo_visor_emissive.TXTR': 1855515304, + 'custom_door_lock_greyscale_emissive.TXTR': 3597936245, + 'custom_door_lock_screwattack.CMDL': 1232841653, + 'custom_door_lock_screwattack.TXTR': 781552186, + 'custom_door_lock_screwattack_emissive.TXTR': 1230915219, + 'custom_door_lock_sonicboom.CMDL': 1368621368, + 'custom_door_lock_sonicboom.TXTR': 914202807, + 'custom_door_lock_sunburst.CMDL': 987291923, + 'custom_door_lock_sunburst.TXTR': 1563869340, +} + + +def test_add_custom_models(prime2_editor: PatcherEditor): + dock_lock_rando.add_custom_models(prime2_editor) + + assert prime2_editor._custom_asset_ids == _custom_asset_ids + + +vanilla_doors = { + "Normal": ("Torvus Bog", "Gathering Hall", "North"), + "Dark": ("Torvus Bog", "Torvus Temple", "EastTop"), + "Light": ("Torvus Bog", "Underground Tunnel", "North"), + "Annihilator": ("Torvus Bog", "Gathering Hall", "East_0P"), + + "Missile": ("Temple Grounds", "GFMC Compound", "WestTop"), + "SuperMissile": ("Torvus Bog", "Torvus Temple", "WestGenerator"), + "SeekerMissile": ("Temple Grounds", "Path of Honor", "North"), + "PowerBomb": ("Temple Grounds", "GFMC Compound", "West"), +} + + +@pytest.mark.parametrize("new_door_type", DOCK_TYPES.keys()) +@pytest.mark.parametrize("old_door_type", vanilla_doors.keys()) +@pytest.mark.parametrize("low_memory", [False]) # too slow to run twice for now +def test_apply_door_rando(prime2_editor, new_door_type, old_door_type, low_memory): + prime2_editor.ensure_present = MagicMock() + + if new_door_type in {"Grapple", "AgonEnergy", "TorvusEnergy", "SanctuaryEnergy"}: + expectation = pytest.raises(NotImplementedError) + else: + expectation = contextlib.nullcontext() + + world_name, area_name, dock_name = vanilla_doors[old_door_type] + with expectation: + dock_lock_rando.apply_door_rando(prime2_editor, world_name, area_name, dock_name, + new_door_type, old_door_type, low_memory)