--- a/server/world/hideout_engine.py +++ b/server/world/hideout_engine.py @@ -267,13 +267,18 @@ def enter_map_portal( self, player_id: str, - portal_index: Optional[int] = None + portal_index: Optional[int] = None, + zone_engine: Optional[Any] = None, ) -> Tuple[bool, str, Optional[Dict[str, Any]]]: """ Enters an active Map Device portal. Consumes exactly 1 portal from the remaining 6 charges. When 0 portals remain, the map instance is sealed permanently. """ + if portal_index is not None and not isinstance(portal_index, int) and zone_engine is None: + zone_engine = portal_index + portal_index = None + hideout = self.get_or_create_hideout(player_id) dev = hideout.map_device @@ -310,9 +315,13 @@ "portal_index": target_idx, } - # If exhausted all 6 portals, close device + # If exhausted all 6 portals, close device and purge instance session from ZoneEngine if dev.portals_remaining == 0: + if zone_engine is not None and hasattr(zone_engine, "active_instances"): + if dev.active_instance_id and dev.active_instance_id in zone_engine.active_instances: + zone_engine.active_instances.pop(dev.active_instance_id, None) dev.active_map = None + dev.active_instance_id = None msg = f"Đã bước qua cổng #{target_idx + 1}. Đây là CỔNG CUỐI CÙNG (0/6)! Tinh Đồ Nghi đã khép lại." else: msg = f"Đã bước qua cổng #{target_idx + 1}. Còn lại {dev.portals_remaining}/6 Cổng Tinh Đồ." --- a/tests/unit/test_hideout_engine.py +++ b/tests/unit/test_hideout_engine.py @@ -116,6 +116,10 @@ assert res_last is not None assert res_last["portals_remaining"] == 0 assert "CỔNG CUỐI CÙNG" in msg_last + hideout = engine.get_or_create_hideout(player_id) + assert hideout.map_device.active_map is None + assert hideout.map_device.active_instance_id is None + assert hideout.map_device.portals_remaining == 0 # 7th attempt must be rejected (all 6 portals exhausted) ok_fail, msg_fail, res_fail = engine.enter_map_portal(player_id) @@ -264,6 +268,25 @@ assert zone_engine.can_spawn_hostile_monsters("zone_player_hideout") is False assert zone_engine.can_spawn_hostile_monsters("zone_blood_scale_ruins") is True + # Enter first 5 portals with zone_engine + for _ in range(5): + ok_enter, _, _ = engine.enter_map_portal(player_id, zone_engine=zone_engine) + assert ok_enter is True + assert inst_id in zone_engine.active_instances + + # Enter 6th and final portal + ok_final, msg_final, res_final = engine.enter_map_portal(player_id, zone_engine=zone_engine) + assert ok_final is True + assert res_final['portals_remaining'] == 0 + assert "CỔNG CUỐI CÙNG" in msg_final + + # Assert instance unregistered from ZoneEngine and active_instance_id cleared + assert inst_id not in zone_engine.active_instances + dev = engine.get_or_create_hideout(player_id).map_device + assert dev.active_instance_id is None + assert dev.active_map is None + assert dev.portals_remaining == 0 + def test_client_hideout_gates_dynamic_tier_binding() -> None: """Verifies client hideout_gates.js defines MAP_TIER_CONFIGS and dynamic setMapTier."""