246 lines
11 KiB
Python
246 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from engine.character import AilmentDef, BodyPart, BodyPartDef, ClothingPiece, Organ, OrganDef
|
|
from engine.item import ItemDef
|
|
from engine.species import AbilityDef, SpeciesDef
|
|
from engine.tile import TILE_LAYERS, TileLayerDef
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EntityDef:
|
|
"""A spawn template: who to make (species, for characters), starting clothing, and starting inventory size."""
|
|
|
|
id: str
|
|
name: str
|
|
species_id: str | None = None
|
|
default_clothing: list[ClothingPiece] = field(default_factory=list)
|
|
inventory_size: tuple[int, int] | None = None
|
|
|
|
|
|
class DefRegistry:
|
|
def __init__(self):
|
|
self.tile_layer_defs: dict[tuple[str, str], TileLayerDef] = {}
|
|
self.item_defs: dict[str, ItemDef] = {}
|
|
self.body_part_defs: dict[str, BodyPartDef] = {}
|
|
self.organ_defs: dict[str, OrganDef] = {}
|
|
self.ailment_defs: dict[str, AilmentDef] = {}
|
|
self.species_defs: dict[str, SpeciesDef] = {}
|
|
self.entity_defs: dict[str, EntityDef] = {}
|
|
|
|
@classmethod
|
|
def load(cls, defs_dir: Path) -> "DefRegistry":
|
|
registry = cls()
|
|
registry._load_tiles(defs_dir / "tiles.json")
|
|
registry._load_items(defs_dir / "items.json")
|
|
registry._load_body_parts(defs_dir / "body_parts.json")
|
|
registry._load_organs(defs_dir / "organs.json")
|
|
registry._load_ailments(defs_dir / "ailments.json")
|
|
registry._load_species(defs_dir / "species.json")
|
|
registry._load_entities(defs_dir / "entities.json")
|
|
return registry
|
|
|
|
def _load_tiles(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for layer in TILE_LAYERS:
|
|
for def_id, fields in data.get(layer, {}).items():
|
|
self.tile_layer_defs[(layer, def_id)] = TileLayerDef(
|
|
id=def_id,
|
|
layer=layer,
|
|
texture=fields.get("texture"),
|
|
color=tuple(fields.get("color", (255, 0, 255))),
|
|
walkable=fields.get("walkable", True),
|
|
blocks_los=fields.get("blocks_los", False),
|
|
deconstruct_item_id=fields.get("deconstruct_item_id"),
|
|
is_helm=fields.get("is_helm", False),
|
|
staircase_direction=fields.get("staircase_direction"),
|
|
speed_multiplier=fields.get("speed_multiplier", 1.0),
|
|
)
|
|
|
|
def _load_items(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
self.item_defs[def_id] = ItemDef(
|
|
id=def_id,
|
|
name=fields["name"],
|
|
width=fields["width"],
|
|
height=fields["height"],
|
|
texture=fields.get("texture"),
|
|
color=tuple(fields.get("color", (255, 0, 255))),
|
|
stackable=fields.get("stackable", False),
|
|
max_stack=fields.get("max_stack", 1),
|
|
clothing_slot=fields.get("clothing_slot"),
|
|
grants_inventory_size=(
|
|
tuple(fields["grants_inventory_size"]) if "grants_inventory_size" in fields else None
|
|
),
|
|
armor_reduction=fields.get("armor_reduction", 0.0),
|
|
arm_slots_required=fields.get("arm_slots_required", 0),
|
|
compatible_body_types=tuple(fields.get("compatible_body_types", ())),
|
|
weapon_skill=fields.get("weapon_skill"),
|
|
weapon_damage=fields.get("weapon_damage", 0.0),
|
|
weapon_range=fields.get("weapon_range", 1),
|
|
projectile_type=fields.get("projectile_type"),
|
|
hands_required=fields.get("hands_required", 1),
|
|
shield_block_bonus=fields.get("shield_block_bonus", 0.0),
|
|
blockable_projectile_types=tuple(fields.get("blockable_projectile_types", ())),
|
|
tool_kind=fields.get("tool_kind"),
|
|
builds_tile_layer=fields.get("builds_tile_layer"),
|
|
builds_tile_layer_def_id=fields.get("builds_tile_layer_def_id"),
|
|
ammo_type=fields.get("ammo_type"),
|
|
ranged_knockback=fields.get("ranged_knockback", 0.0),
|
|
compatible_ammo_types=tuple(fields.get("compatible_ammo_types", ())),
|
|
magazine_size=fields.get("magazine_size", 0),
|
|
aim_cone_degrees=fields.get("aim_cone_degrees", 0.0),
|
|
pellet_count=fields.get("pellet_count", 1),
|
|
blast_radius=fields.get("blast_radius", 0),
|
|
attachment_kind=fields.get("attachment_kind"),
|
|
aimcone_reduction_degrees=fields.get("aimcone_reduction_degrees", 0.0),
|
|
magazine_capacity_bonus=fields.get("magazine_capacity_bonus", 0),
|
|
melee_knockback=fields.get("melee_knockback", 0.0),
|
|
grants_ability=fields.get("grants_ability"),
|
|
is_implant=fields.get("is_implant", False),
|
|
)
|
|
|
|
def _load_body_parts(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
self.body_part_defs[def_id] = BodyPartDef(
|
|
id=def_id,
|
|
slot=fields["slot"],
|
|
texture=fields.get("texture"),
|
|
color=tuple(fields.get("color", (255, 0, 255))),
|
|
bleeding_weight=fields.get("bleeding_weight", 0.0),
|
|
bleeding_speed=fields.get("bleeding_speed", 0.0),
|
|
skill_weights=dict(fields.get("skill_weights", {})),
|
|
speed_factor=fields.get("speed_factor", 0.0),
|
|
)
|
|
|
|
def _load_organs(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
self.organ_defs[def_id] = OrganDef(
|
|
id=def_id,
|
|
name=fields.get("name", def_id),
|
|
host_slot=fields["host_slot"],
|
|
check_weights=dict(fields.get("check_weights", {})),
|
|
affects_organs=dict(fields.get("affects_organs", {})),
|
|
role=fields.get("role"),
|
|
)
|
|
|
|
def _load_ailments(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
self.ailment_defs[def_id] = AilmentDef(
|
|
id=def_id,
|
|
name=fields.get("name", def_id),
|
|
check_weights=dict(fields.get("check_weights", {})),
|
|
)
|
|
|
|
def _load_species(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
default_parts = [
|
|
BodyPart(slot=self.body_part_defs[part_id].slot, part_def_id=part_id)
|
|
for part_id in fields.get("default_body_parts", [])
|
|
]
|
|
abilities = [
|
|
AbilityDef(
|
|
id=ability["id"],
|
|
name=ability.get("name", ability["id"]),
|
|
required_slots=tuple(ability.get("required_slots", ())),
|
|
)
|
|
for ability in fields.get("abilities", [])
|
|
]
|
|
self.species_defs[def_id] = SpeciesDef(
|
|
id=def_id,
|
|
name=fields.get("name", def_id),
|
|
genders=list(fields.get("genders", [])),
|
|
default_body_parts=default_parts,
|
|
default_organs=list(fields.get("default_organs", [])),
|
|
body_type=fields.get("body_type", "biped"),
|
|
body_type_options=list(fields.get("body_type_options", [])),
|
|
hair_colors=list(fields.get("hair_colors", [])),
|
|
is_synthetic=fields.get("is_synthetic", False),
|
|
abilities=abilities,
|
|
blood_danger_threshold=fields.get("blood_danger_threshold", 50.0),
|
|
blood_critical_threshold=fields.get("blood_critical_threshold", 25.0),
|
|
health_item_compatibility=list(fields.get("health_item_compatibility", [])),
|
|
)
|
|
|
|
def _load_entities(self, path: Path) -> None:
|
|
data = json.loads(path.read_text())
|
|
for def_id, fields in data.items():
|
|
inventory_size = fields.get("inventory_size")
|
|
default_clothing = [
|
|
ClothingPiece(slot=self.item_defs[item_id].clothing_slot, item_def_id=item_id)
|
|
for item_id in fields.get("default_clothing", [])
|
|
]
|
|
self.entity_defs[def_id] = EntityDef(
|
|
id=def_id,
|
|
name=fields.get("name", def_id),
|
|
species_id=fields.get("species_id"),
|
|
default_clothing=default_clothing,
|
|
inventory_size=tuple(inventory_size) if inventory_size else None,
|
|
)
|
|
|
|
def get_tile_layer_def(self, layer: str, def_id: str | None) -> TileLayerDef | None:
|
|
if def_id is None:
|
|
return None
|
|
return self.tile_layer_defs.get((layer, def_id))
|
|
|
|
def get_item_def(self, def_id: str) -> ItemDef:
|
|
return self.item_defs[def_id]
|
|
|
|
def get_body_part_def(self, def_id: str) -> BodyPartDef:
|
|
return self.body_part_defs[def_id]
|
|
|
|
def get_organ_def(self, def_id: str) -> OrganDef:
|
|
return self.organ_defs[def_id]
|
|
|
|
def get_ailment_def(self, def_id: str) -> AilmentDef | None:
|
|
"""None (not KeyError) for an unrecognized ailment id - not every Ailment instance
|
|
needs a matching def (e.g. ad-hoc/test-only ailments), see Character.ailment_penalty.
|
|
"""
|
|
return self.ailment_defs.get(def_id)
|
|
|
|
def get_species_def(self, def_id: str) -> SpeciesDef:
|
|
return self.species_defs[def_id]
|
|
|
|
def get_entity_def(self, def_id: str) -> EntityDef:
|
|
return self.entity_defs[def_id]
|
|
|
|
def new_default_body_parts(self, species_id: str) -> list[BodyPart]:
|
|
"""Fresh BodyPart instances copied from a species' defaults, safe to mutate per-character.
|
|
|
|
Organs are placed per the species' own explicit default_organs list (not by scanning
|
|
every loaded organ def for a host_slot match) - a species declares exactly which organs
|
|
it has, same as it declares exactly which body parts it has. This matters once species
|
|
stop sharing anatomy: e.g. a robotic species with a "torso" slot must NOT automatically
|
|
inherit an organic heart/lungs/liver/kidneys just because they happen to host there.
|
|
Each declared organ is still placed into whichever body part its own OrganDef.host_slot
|
|
names.
|
|
"""
|
|
species = self.get_species_def(species_id)
|
|
organs_by_slot: dict[str, list[Organ]] = {}
|
|
for organ_def_id in species.default_organs:
|
|
organ_def = self.organ_defs[organ_def_id]
|
|
organs_by_slot.setdefault(organ_def.host_slot, []).append(Organ(organ_def_id=organ_def_id))
|
|
return [
|
|
BodyPart(
|
|
slot=p.slot,
|
|
part_def_id=p.part_def_id,
|
|
organs=organs_by_slot.get(p.slot, []),
|
|
)
|
|
for p in species.default_body_parts
|
|
]
|
|
|
|
def new_default_clothing(self, entity_def_id: str) -> list[ClothingPiece]:
|
|
"""Fresh ClothingPiece instances copied from an entity template's starting outfit, safe to mutate per-character."""
|
|
return [
|
|
ClothingPiece(slot=p.slot, item_def_id=p.item_def_id)
|
|
for p in self.get_entity_def(entity_def_id).default_clothing
|
|
]
|