Files
dockipelago/worlds/duke3d/base_classes.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

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