forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
248 lines
7.6 KiB
Python
248 lines
7.6 KiB
Python
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
|
|
|
from BaseClasses import Entrance, Item, Location, Region
|
|
|
|
from .rules import RULETYPE, LambdaRule, Rule, Rules, RuleTrue
|
|
|
|
if TYPE_CHECKING:
|
|
from ..duke3d import D3DWorld
|
|
|
|
|
|
class D3DItem(Item):
|
|
game = "Duke3D"
|
|
|
|
|
|
class D3DLocation(Location):
|
|
game = "Duke3D"
|
|
|
|
def __init__(
|
|
self,
|
|
player: int,
|
|
name: str = "",
|
|
address: Optional[int] = None,
|
|
parent: Optional[Region] = None,
|
|
):
|
|
super().__init__(player, name, address, parent)
|
|
if address is None:
|
|
self.event = True
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LocationDef:
|
|
name: str
|
|
type: str # "exit", "sprite", "sector
|
|
game_id: int # Sprite number, sector number or exit lotag
|
|
sprite_type: str = "" # additional data for sprites
|
|
density: int = 0 # defines what density settings it appears in. Higher values require more locations enabled
|
|
x: int = 0
|
|
y: int = 0
|
|
z: int = 0
|
|
|
|
|
|
# Density levels:
|
|
# 0: Iconic locations - Always included
|
|
# 1: Balanced with secrets - Default density with location checks at secret areas. Handpicked for interesting places to
|
|
# visit.
|
|
# 2: Balanced without secrets - Default density with no checks at secret areas. Includes additional pickups in (most)
|
|
# secret areas to account for missing checks from the area itself
|
|
# 3: Dense - More checks, including some nearby duplicates
|
|
# 4: All - All single player locations, can sometimes have big clusters in single spot
|
|
# 5: MP Only - Additional items that normally only spawn in MP only deathmatch locations
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ItemDef:
|
|
name: str
|
|
ap_id: int # The id for the item
|
|
type: str # The type of item
|
|
props: Dict[str, Any] # Additional type specific properties
|
|
unique: bool = False # Can only have one of these
|
|
persistent: bool = False # These are persisted in the game save
|
|
progression: bool = False # Marks an item as being a progression item. Persistent non-progression are marked useful
|
|
silent: bool = False # These are acquired but not notified to the player
|
|
|
|
|
|
class D3DLevel(object):
|
|
name: str
|
|
levelnum: int
|
|
volumenum: int
|
|
location_defs: List[dict]
|
|
keys: List[str]
|
|
events: List[str] = []
|
|
must_dive: bool = False # If the level has locations locked behind diving. Determines progression status for Scuba
|
|
has_boss: bool = (
|
|
False # If the level awards a boss token in the appropriate goal settings
|
|
)
|
|
|
|
def __init__(self):
|
|
self.world: Optional["D3DWorld"] = None
|
|
self.prefix = f"E{self.volumenum + 1}L{self.levelnum + 1}"
|
|
self.locations: Dict[str, LocationDef] = self._make_locations()
|
|
self.used_locations: Set[
|
|
str
|
|
] = set() # locations actually filled in make_region
|
|
|
|
def _make_locations(self) -> Dict[str, LocationDef]:
|
|
ret = {}
|
|
for loc_def in self.location_defs:
|
|
loc_name = f'{self.prefix} {loc_def["name"]}'
|
|
ret[loc_name] = LocationDef(
|
|
name=loc_name,
|
|
type=loc_def["type"],
|
|
game_id=loc_def["id"],
|
|
density=loc_def.get("density", 0),
|
|
sprite_type=loc_def.get("sprite_type"),
|
|
)
|
|
return ret
|
|
|
|
def create_region(self, world: "D3DWorld") -> Region:
|
|
self.world = world
|
|
self.used_locations = set()
|
|
ret = self.main_region()
|
|
self.world = None
|
|
return ret
|
|
|
|
def main_region(self) -> Region:
|
|
"""
|
|
To be implemented by each level
|
|
"""
|
|
# Default implementations: everything available in the start region
|
|
# This is wildly incorrect logically, but helps play the levels for configuring the logic
|
|
ret = self.region(self.name)
|
|
self.add_locations([x["name"] for x in self.location_defs], ret)
|
|
return ret
|
|
|
|
def region(
|
|
self,
|
|
name: str,
|
|
locations: Optional[List[str]] = None,
|
|
hint: Optional[str] = None,
|
|
) -> Region:
|
|
ret = Region(
|
|
f"{self.prefix} {name}", self.world.player, self.world.multiworld, hint
|
|
)
|
|
if locations:
|
|
self.add_locations(locations, ret)
|
|
return ret
|
|
|
|
def add_location(self, name: str, region: Region):
|
|
location = f"{self.prefix} {name}"
|
|
if name in self.events:
|
|
region.locations.append(
|
|
D3DLocation(
|
|
self.world.player,
|
|
location,
|
|
None,
|
|
region,
|
|
)
|
|
)
|
|
elif self.world.use_location(self.locations.get(location)):
|
|
region.locations.append(
|
|
D3DLocation(
|
|
self.world.player,
|
|
location,
|
|
self.world.location_name_to_id[location],
|
|
region,
|
|
)
|
|
)
|
|
self.used_locations.add(location)
|
|
|
|
def add_locations(self, locations: List[str], region: Region):
|
|
for loc in locations:
|
|
self.add_location(loc, region)
|
|
|
|
def get_location(self, name) -> Optional[Location]:
|
|
try:
|
|
return self.world.multiworld.get_location(
|
|
f"{self.prefix} {name}", self.world.player
|
|
)
|
|
except KeyError:
|
|
return None
|
|
|
|
@staticmethod
|
|
def _resolve_rule_type(
|
|
rules: Optional[Union[RULETYPE, List[RULETYPE]]] = None
|
|
) -> Optional[Rule]:
|
|
if rules is None:
|
|
return RuleTrue()
|
|
if not isinstance(rules, List):
|
|
rules = [rules]
|
|
if not rules:
|
|
return None
|
|
rule = rules[0]
|
|
if not isinstance(rule, Rule):
|
|
rule = LambdaRule(rule)
|
|
for other_rule in rules[1:]:
|
|
if not isinstance(other_rule, Rule):
|
|
other_rule = LambdaRule(other_rule)
|
|
rule |= other_rule
|
|
return rule
|
|
|
|
def connect(
|
|
self,
|
|
start: Region,
|
|
end: Region,
|
|
rules: Optional[Union[RULETYPE, List[RULETYPE]]] = None,
|
|
):
|
|
start.connect(end, None, self._resolve_rule_type(rules))
|
|
|
|
def restrict(
|
|
self,
|
|
spot: Optional[Union[Location, Entrance, str]],
|
|
rules: Union[RULETYPE, List[RULETYPE]],
|
|
):
|
|
if isinstance(spot, str):
|
|
return self.restrict(self.get_location(spot), rules)
|
|
if spot is not None:
|
|
spot.access_rule = self._resolve_rule_type(rules)
|
|
|
|
@property
|
|
def red_key(self) -> Rule:
|
|
return self.world.rules.can_use & self.world.rules.has(
|
|
f"{self.prefix} Red Key Card"
|
|
)
|
|
|
|
@property
|
|
def blue_key(self) -> Rule:
|
|
return self.world.rules.can_use & self.world.rules.has(
|
|
f"{self.prefix} Blue Key Card"
|
|
)
|
|
|
|
@property
|
|
def yellow_key(self) -> Rule:
|
|
return self.world.rules.can_use & self.world.rules.has(
|
|
f"{self.prefix} Yellow Key Card"
|
|
)
|
|
|
|
@property
|
|
def unlock(self) -> str:
|
|
return f"{self.prefix} Unlock"
|
|
|
|
@property
|
|
def rules(self) -> Rules:
|
|
return self.world.rules
|
|
|
|
@property
|
|
def items(self) -> List[str]:
|
|
ret = []
|
|
for color in ("Red", "Blue", "Yellow"):
|
|
if color in self.keys:
|
|
ret.append(f"{self.prefix} {color} Key Card")
|
|
return ret
|
|
|
|
@property
|
|
def map(self) -> str:
|
|
return f"{self.prefix} Automap"
|
|
|
|
def event(self, name: str) -> Rule:
|
|
return self.world.rules.has(f"{self.prefix} {name}")
|
|
|
|
|
|
class D3DEpisode(object):
|
|
name: str
|
|
volumenum: int
|
|
levels: List[D3DLevel]
|
|
maxlevel: int
|
|
bosslevel: int
|