Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dade03e
create lambda condition prototype
CodeTriangle Jul 30, 2025
653a008
add support for new conditions
CodeTriangle May 14, 2026
84cf980
pre-cache some f-strings for speed
CodeTriangle May 14, 2026
301a818
Merge branch 'crosscode-dev' into crosscode-lambda-conditions-2
CodeTriangle Jun 18, 2026
709ca16
add enemy types
CodeTriangle Jun 19, 2026
8b7a1b9
read enemy data (but do not do anything with it yet)
CodeTriangle Jun 19, 2026
3cc3980
do not emit non-`init` fields
CodeTriangle Jun 19, 2026
e9b7a0c
add special handling of and/or conditions
CodeTriangle Jun 19, 2026
de3c0c4
Merge branch 'crosscode-lambda-conditions-2' into combat-logic
CodeTriangle Jun 19, 2026
d10adcc
condense dataclass encoding operation
CodeTriangle Jul 1, 2026
9e6f7bc
add codegen for enemies (and do codegen)
CodeTriangle Jul 1, 2026
90e7bd6
fix enemy event generation
CodeTriangle Jul 4, 2026
503191a
add killsanity to codegen
CodeTriangle Jul 4, 2026
b2ac929
add killsanity support to generation
CodeTriangle Jul 4, 2026
e42d09d
add killsanity to pool locations
CodeTriangle Jul 4, 2026
d4fd053
fix some dlc boss details
CodeTriangle Jul 4, 2026
c65a385
add enemies to randoData
CodeTriangle Jul 4, 2026
a05debc
add killsanity item groups
CodeTriangle Jul 5, 2026
8d6d486
finish implementing level condition
CodeTriangle Jul 25, 2026
5b5a751
add more combat-related options
CodeTriangle Aug 2, 2026
18f0019
start hooking options and functionality together
CodeTriangle Aug 23, 2026
de411e8
remove logicdict; replace with world reference
CodeTriangle Aug 23, 2026
0ddd633
Merge branch 'crosscode-dev' into crosscode-lambda-conditions-2
CodeTriangle Aug 23, 2026
5e48b16
Merge branch 'crosscode-lambda-conditions-2' into logicdict-refactor
CodeTriangle Aug 23, 2026
8cb7079
rename killsanity -> monster fibula
CodeTriangle Aug 23, 2026
aa15e95
Merge branch 'logicdict-refactor' into combat-logic
CodeTriangle Aug 23, 2026
ac2cd56
add equipment data to items in codegen
CodeTriangle Aug 24, 2026
a82561f
create a very rough draft of the equipment level check function
CodeTriangle Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions worlds/crosscode/codegen/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,47 @@
import typing
import ast

from ..types.condition import Condition
from ..types.condition import AndCondition, Condition, OrCondition, QuestCondition
from ..types.locations import AccessInfo, LocationData
from ..types.regions import Goal, RegionConnection
from ..types.items import ItemData, ItemPoolEntry, ProgressiveChainEntry, SingleItemData
from ..types.shops import ShopData


def create_expression_dataclass(cls) -> ast.Call:
result = ast.Call(
func=ast.Name(cls.__class__.__name__),
args=[],
keywords=[
ast.keyword(arg=key, value=ast.Constant(value))
for key, value in cls.__dict__.items()
if cls.__dataclass_fields__[key].init
# and cls.__dataclass_fields__[key].default != value
],
)
ast.fix_missing_locations(result)

return result

def create_expression_condition(condition: Condition) -> ast.Call:
"""
Create an expression representing a singular condition.
"""
result = ast.Call(
func=ast.Name(condition.__class__.__name__),
args=[],
keywords=[ast.keyword(arg=key, value=ast.Constant(value)) for key, value in condition.__dict__.items()],
)
if isinstance(condition, OrCondition) or isinstance(condition, AndCondition):
# we handle these conditions in a special way, since their lists cannot be encoded by ast.Constant
result = ast.Call(
func=ast.Name(condition.__class__.__name__),
args=[],
keywords=[
ast.keyword(
arg="subconditions",
value=create_expression_condition_list(condition.subconditions)
)
],
)
else:
# this block should handle most cases, unless you make a condition that has a complex type
result = create_expression_dataclass(condition)
ast.fix_missing_locations(result)

return result
Expand Down Expand Up @@ -151,6 +176,12 @@ def create_expression_single_item(data: SingleItemData):
value=ast.Constant(True)
))

if data.equip_data is not None:
ast_item.keywords.append(ast.keyword(
arg="equip_data",
value=create_expression_dataclass(data.equip_data)
))

ast.fix_missing_locations(ast_item)
return ast_item

Expand Down
19 changes: 19 additions & 0 deletions worlds/crosscode/codegen/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ def generate_python_file_shops(self):
with open(os.path.join(self.world_dir, "shops.py"), "w", encoding="utf8") as f:
f.write(locations_complete)

def generate_python_file_enemies(self):
"""
Generates enemies.py, which provides a list of enemies.
"""
template = self.environment.get_template("enemies.template.py")

locations_complete = template.render(
enemies=self.lists.enemies.items(),
**self.common_args
)

with open(os.path.join(self.world_dir, "enemies.py"), "w", encoding="utf8") as f:
f.write(locations_complete)


def generate_python_files(self) -> None:
"""
Expand All @@ -228,6 +242,7 @@ def generate_python_files(self) -> None:
self.generate_python_file_regions()
self.generate_python_file_vars()
self.generate_python_file_shops()
self.generate_python_file_enemies()

def generate_mod_files(self):
"""
Expand Down Expand Up @@ -256,6 +271,10 @@ def generate_mod_files(self):
"byShopAndId": defaultdict(dict)
},
},
"enemies": {
enemy.internal_name: { "kill": self.lists.locations_data[f"Monster Fibula: {enemy.name}"].code }
for enemy in self.lists.enemies.values()
},
"botanics": self.lists.botanics_internal_names_to_ids,
"descriptions": self.lists.descriptions,
"markers": self.lists.markers
Expand Down
99 changes: 97 additions & 2 deletions worlds/crosscode/codegen/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

from BaseClasses import ItemClassification

from .parse import JsonParser
from .parse import JsonParser, JsonParserError
from .context import Context
from .util import BASE_ID, DYNAMIC_ITEM_AREA_OFFSET, RESERVED_ITEM_IDS
from .markers import Marker, MarkerGenerator

from ..types.items import ItemData, ProgressiveItemChainSingle, SingleItemData, ItemPoolEntry, ProgressiveItemChain
from ..types.enemies import Enemy
from ..types.locations import AccessInfo, LocationData
from ..types.condition import Condition, NeverCondition, RegionCondition, OrCondition, AndCondition, ShopSlotCondition
from ..types.condition import Condition, ItemCondition, NeverCondition, RegionCondition, OrCondition, AndCondition, ShopSlotCondition
from ..types.shops import ShopData

class LocationCategory(StrEnum):
Expand Down Expand Up @@ -69,6 +70,8 @@ class ListInfo:
region_botanics_amounts: dict[str, dict[str, int]] # { mode => { region => number of plants } }
botanics_internal_names_to_ids: dict[str, int]

enemies: dict[str, Enemy]

progressive_chains: dict[str, ProgressiveItemChain]
progressive_items: dict[str, ItemData]

Expand Down Expand Up @@ -115,6 +118,8 @@ def __init__(self, ctx: Context):
self.region_botanics_amounts = defaultdict(lambda: defaultdict(lambda: 0))
self.botanics_internal_names_to_ids = {}

self.enemies = {}

self.json_parser = JsonParser(self.ctx)
self.json_parser.single_items_dict = self.single_items_dict
self.json_parser.items_dict = self.items_dict
Expand Down Expand Up @@ -165,6 +170,8 @@ def build(self):

self.__add_botanics(file["botanics"])

self.__add_enemies(file["enemies"])

self.__add_vars(self.ctx.rando_data["vars"])

def __get_cached_location_id(self, name: str) -> typing.Optional[int]:
Expand Down Expand Up @@ -544,6 +551,94 @@ def __add_botanics(self, raw: dict[str, dict[str, typing.Any]]):
for name, plant in raw.items():
self.__add_plant(name, plant)

def __add_enemy(self, raw: dict[str, typing.Any]):
try:
name = raw["name"]
area = raw["area"]
area_name = self.ctx.area_names[area]
internal_name = raw["id"]
level = raw["level"]
except KeyError:
raise JsonParserError(raw, raw, "", f"Enemy specification lacks an essential parameter: {raw}")

if "region" in raw:
first_encounter_access = grind_access = self.json_parser.parse_location_access_info(raw)
else:
if "firstEncounter" in raw:
first_encounter_access = self.json_parser.parse_location_access_info(raw["firstEncounter"])
else:
raise JsonParserError(raw, raw, "first encounter", f"Enemy must at least have a first encounter")

if "grind" in raw:
grind_access = self.json_parser.parse_location_access_info(raw["grind"])
else:
grind_access = None

metadata = raw.get("metadata", {})
first_encounter_event = LocationData(
name=f"First Encounter: {name} (Event)",
code=None,
access=first_encounter_access,
area=area,
metadata=metadata | { "kill": True },
)

self.events_data[first_encounter_event.name] = first_encounter_event

if grind_access is not None:
grind_event = LocationData(
name=f"Grind: {name} (Event)",
code=None,
access=grind_access,
area=area,
metadata=metadata | { "combat": True },
)

self.events_data[grind_event.name] = grind_event
else:
grind_event = None

kill_loc_name = f"Monster Fibula: {name}"
kill_loc_id = self.__get_or_allocate_location_id(kill_loc_name)
kill_location = LocationData(
name=kill_loc_name,
code=kill_loc_id,
access=AccessInfo(
region={ name: "Menu" for name in first_encounter_access.region },
cond=[
ItemCondition(first_encounter_event.name)
],
),
area=raw["area"],
metadata=metadata | { "kill": True },
)

enemy = Enemy(
name=name,
area=area,
internal_name=internal_name,
level=level,
first_encounter_event_name=first_encounter_event.name,
grind_event_name=grind_event.name if grind_event is not None else None
)

self.locations_data[kill_loc_name] = kill_location
self.pool_locations.append(kill_location)
self.location_groups["Monster Defeats"].append(kill_location)

if area != None:
try:
self.location_groups[area_name].append(kill_location)
self.location_groups[f"{area_name} Monster Defeats"].append(kill_location)
except KeyError:
print(f"Cannot add location '{name}' in area '{area}'")

self.enemies[name] = enemy

def __add_enemies(self, raw: dict[str, dict[str, typing.Any]]):
for enemy in raw.values():
self.__add_enemy(enemy)

def __add_reward(self, reward: list[dict[str, typing.Any]]) -> ItemData:
"""
Ensure an item reward is in the list of items.
Expand Down
8 changes: 7 additions & 1 deletion worlds/crosscode/codegen/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .context import Context
from .util import BASE_ID, RESERVED_ITEM_IDS, get_item_classification

from ..types.items import ItemData, ProgressiveChainEntry, ProgressiveItemChain, ProgressiveItemChainSingle, ProgressiveItemChainMulti, ProgressiveItemSubchain, SingleItemData
from ..types.items import ItemData, ProgressiveChainEntry, ProgressiveItemChain, ProgressiveItemChainSingle, ProgressiveItemChainMulti, ProgressiveItemSubchain, SingleItemData, EquipmentData
from ..types.locations import AccessInfo, Condition
from ..types.regions import Goal, RegionConnection, RegionsData
from ..types.condition import *
Expand Down Expand Up @@ -228,6 +228,12 @@ def parse_item_data(self, name: str, raw: dict[str, typing.Any]) -> tuple[Single
amount=1
)

if db_entry.get("type", None) == "EQUIP" and raw.get("providesLevels", True):
single_item.equip_data = EquipmentData(
equip_type=db_entry["equipType"],
level=self.ctx.item_data[single_item.item_id]["level"],
)

return single_item, item

def parse_item_reward(self, raw: list[typing.Any]) -> ItemData:
Expand Down
4 changes: 2 additions & 2 deletions worlds/crosscode/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
NAME: str = "CrossCode"
BASE_ID: int = 3235824000
DATA_VERSION: str = "0.5"
APWORLD_VERSION_STRING = "0.9.7"
APWORLD_VERSION: Version = Version(0, 9, 7)
APWORLD_VERSION_STRING = "0.9.3"
APWORLD_VERSION: Version = Version(0, 9, 3)
Loading
Loading