Mi2dRPGamEng/engine/defs.py

243 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"),
)
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", {})),
)
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", [])),
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
]