kirby-world 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kirby_world-0.1.0/LICENSE +149 -0
- kirby_world-0.1.0/PKG-INFO +56 -0
- kirby_world-0.1.0/README.md +39 -0
- kirby_world-0.1.0/kirby_world/__init__.py +57 -0
- kirby_world-0.1.0/kirby_world/construct.py +275 -0
- kirby_world-0.1.0/kirby_world/cover.py +348 -0
- kirby_world-0.1.0/kirby_world/falling.py +94 -0
- kirby_world-0.1.0/kirby_world/generate.py +256 -0
- kirby_world-0.1.0/kirby_world/geometry.py +246 -0
- kirby_world-0.1.0/kirby_world/hazards.py +114 -0
- kirby_world-0.1.0/kirby_world/movement_legality.py +501 -0
- kirby_world-0.1.0/kirby_world/occupancy.py +48 -0
- kirby_world-0.1.0/kirby_world/scene.py +523 -0
- kirby_world-0.1.0/kirby_world/visibility.py +340 -0
- kirby_world-0.1.0/kirby_world/world.py +29 -0
- kirby_world-0.1.0/kirby_world.egg-info/PKG-INFO +56 -0
- kirby_world-0.1.0/kirby_world.egg-info/SOURCES.txt +40 -0
- kirby_world-0.1.0/kirby_world.egg-info/dependency_links.txt +1 -0
- kirby_world-0.1.0/kirby_world.egg-info/requires.txt +6 -0
- kirby_world-0.1.0/kirby_world.egg-info/top_level.txt +1 -0
- kirby_world-0.1.0/pyproject.toml +33 -0
- kirby_world-0.1.0/setup.cfg +4 -0
- kirby_world-0.1.0/tests/test_a_place_without_a_fight.py +59 -0
- kirby_world-0.1.0/tests/test_climb_difficulty.py +31 -0
- kirby_world-0.1.0/tests/test_construct.py +123 -0
- kirby_world-0.1.0/tests/test_cover.py +329 -0
- kirby_world-0.1.0/tests/test_cover_is_per_threat.py +89 -0
- kirby_world-0.1.0/tests/test_cover_protects_the_man_behind_it.py +100 -0
- kirby_world-0.1.0/tests/test_falling.py +112 -0
- kirby_world-0.1.0/tests/test_flank_candidates.py +112 -0
- kirby_world-0.1.0/tests/test_furnishings.py +333 -0
- kirby_world-0.1.0/tests/test_generate.py +151 -0
- kirby_world-0.1.0/tests/test_geometry.py +142 -0
- kirby_world-0.1.0/tests/test_hazards.py +233 -0
- kirby_world-0.1.0/tests/test_leaf.py +51 -0
- kirby_world-0.1.0/tests/test_los_wall_top.py +31 -0
- kirby_world-0.1.0/tests/test_scene.py +110 -0
- kirby_world-0.1.0/tests/test_vantage_support.py +219 -0
- kirby_world-0.1.0/tests/test_visibility.py +141 -0
- kirby_world-0.1.0/tests/test_vocabulary.py +121 -0
- kirby_world-0.1.0/tests/test_wall_defenses.py +47 -0
- kirby_world-0.1.0/tests/test_wall_top_surface.py +99 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree
|
|
8
|
+
to them as both strict obligations and conditions to all
|
|
9
|
+
your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the
|
|
14
|
+
software to do everything you might do with the software
|
|
15
|
+
that would otherwise infringe the licensor's copyright
|
|
16
|
+
in it for any permitted purpose. However, you may
|
|
17
|
+
only distribute the software according to [Distribution
|
|
18
|
+
License](#distribution-license) and make changes or new works
|
|
19
|
+
based on the software according to [Changes and New Works
|
|
20
|
+
License](#changes-and-new-works-license).
|
|
21
|
+
|
|
22
|
+
## Distribution License
|
|
23
|
+
|
|
24
|
+
The licensor grants you an additional copyright license
|
|
25
|
+
to distribute copies of the software. Your license
|
|
26
|
+
to distribute covers distributing the software with
|
|
27
|
+
changes and new works permitted by [Changes and New Works
|
|
28
|
+
License](#changes-and-new-works-license).
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of
|
|
33
|
+
the software from you also gets a copy of these terms or the
|
|
34
|
+
URL for them above, as well as copies of any plain-text lines
|
|
35
|
+
beginning with `Required Notice:` that the licensor provided
|
|
36
|
+
with the software. For example:
|
|
37
|
+
|
|
38
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
39
|
+
|
|
40
|
+
## Changes and New Works License
|
|
41
|
+
|
|
42
|
+
The licensor grants you an additional copyright license to
|
|
43
|
+
make changes and new works based on the software for any
|
|
44
|
+
permitted purpose.
|
|
45
|
+
|
|
46
|
+
## Patent License
|
|
47
|
+
|
|
48
|
+
The licensor grants you a patent license for the software that
|
|
49
|
+
covers patent claims the licensor can license, or becomes able
|
|
50
|
+
to license, that you would infringe by using the software.
|
|
51
|
+
|
|
52
|
+
## Noncommercial Purposes
|
|
53
|
+
|
|
54
|
+
Any noncommercial purpose is a permitted purpose.
|
|
55
|
+
|
|
56
|
+
## Personal Uses
|
|
57
|
+
|
|
58
|
+
Personal use for research, experiment, and testing for
|
|
59
|
+
the benefit of public knowledge, personal study, private
|
|
60
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
61
|
+
observance, without any anticipated commercial application,
|
|
62
|
+
is use for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
## Noncommercial Organizations
|
|
65
|
+
|
|
66
|
+
Use by any charitable organization, educational institution,
|
|
67
|
+
public research organization, public safety or health
|
|
68
|
+
organization, environmental protection organization,
|
|
69
|
+
or government institution is use for a permitted purpose
|
|
70
|
+
regardless of the source of funding or obligations resulting
|
|
71
|
+
from the funding.
|
|
72
|
+
|
|
73
|
+
## Fair Use
|
|
74
|
+
|
|
75
|
+
You may have "fair use" rights for the software under the
|
|
76
|
+
law. These terms do not limit them.
|
|
77
|
+
|
|
78
|
+
## No Other Rights
|
|
79
|
+
|
|
80
|
+
These terms do not allow you to sublicense or transfer any of
|
|
81
|
+
your licenses to anyone else, or prevent the licensor from
|
|
82
|
+
granting licenses to anyone else. These terms do not imply
|
|
83
|
+
any other licenses.
|
|
84
|
+
|
|
85
|
+
## Patent Defense
|
|
86
|
+
|
|
87
|
+
If you make any written claim that the software infringes or
|
|
88
|
+
contributes to infringement of any patent, your patent license
|
|
89
|
+
for the software granted under these terms ends immediately. If
|
|
90
|
+
your company makes such a claim, your patent license ends
|
|
91
|
+
immediately for work on behalf of your company.
|
|
92
|
+
|
|
93
|
+
## Violations
|
|
94
|
+
|
|
95
|
+
The first time you are notified in writing that you have
|
|
96
|
+
violated any of these terms, or done anything with the software
|
|
97
|
+
not covered by your licenses, your licenses can nonetheless
|
|
98
|
+
continue if you come into full compliance with these terms,
|
|
99
|
+
and take practical steps to correct past violations, within
|
|
100
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
101
|
+
end immediately.
|
|
102
|
+
|
|
103
|
+
## No Liability
|
|
104
|
+
|
|
105
|
+
***As far as the law allows, the software comes as is, without
|
|
106
|
+
any warranty or condition, and the licensor will not be liable
|
|
107
|
+
to you for any damages arising out of these terms or the use
|
|
108
|
+
or nature of the software, under any kind of legal claim.***
|
|
109
|
+
|
|
110
|
+
## Definitions
|
|
111
|
+
|
|
112
|
+
The **licensor** is the individual or entity offering these
|
|
113
|
+
terms, and the **software** is the software the licensor makes
|
|
114
|
+
available under these terms.
|
|
115
|
+
|
|
116
|
+
**You** refers to the individual or entity agreeing to these
|
|
117
|
+
terms.
|
|
118
|
+
|
|
119
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
120
|
+
or other kind of organization that you work for, plus all
|
|
121
|
+
organizations that have control over, are under the control of,
|
|
122
|
+
or are under common control with that organization. **Control**
|
|
123
|
+
means ownership of substantially all the assets of an entity,
|
|
124
|
+
or the power to direct its management and policies by vote,
|
|
125
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
126
|
+
|
|
127
|
+
**Your licenses** are all the licenses granted to you for the
|
|
128
|
+
software under these terms.
|
|
129
|
+
|
|
130
|
+
**Use** means anything you do with the software requiring one
|
|
131
|
+
of your licenses.
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
================================================================================
|
|
135
|
+
NOTES SPECIFIC TO kirby-terrain
|
|
136
|
+
================================================================================
|
|
137
|
+
|
|
138
|
+
Required Notice: Copyright (c) 2026 Peter D Bethke
|
|
139
|
+
|
|
140
|
+
(The line above begins with `Required Notice:` deliberately -- see the
|
|
141
|
+
Notices section of the licence. It must travel with any copy of this
|
|
142
|
+
software or any part of it.)
|
|
143
|
+
|
|
144
|
+
This package reproduces no rules text or prose from the HERO System rulebook.
|
|
145
|
+
The PD, ED and BODY values in `durability.py` are bare game statistics —
|
|
146
|
+
numbers, not the rule or the writing around them — transcribed for use by
|
|
147
|
+
this platform's engines.
|
|
148
|
+
|
|
149
|
+
HERO System and HERO Designer are the property of their respective owners.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kirby-world
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Places for the Kirby HERO System platform: Worlds, Scenes and the geometry inside them
|
|
5
|
+
Author: PeterB
|
|
6
|
+
License: PolyForm Noncommercial License 1.0.0
|
|
7
|
+
Project-URL: Repository, https://github.com/pdbethke/kirby-world
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: kirby-terrain>=0.1.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# kirby-world
|
|
19
|
+
|
|
20
|
+
Places for the Kirby HERO System platform: a `World` of `Scene`s, and the
|
|
21
|
+
geometry inside them — surfaces, walls, furnishings, hazards, constructs,
|
|
22
|
+
cover, falling, line of sight and per-mode movement reachability.
|
|
23
|
+
|
|
24
|
+
A place is not a fight. A house with five people doing chores, and what is
|
|
25
|
+
reachable from the kitchen, needs nothing but this package:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from kirby_world import AmbientConditions, Position, Scene, SceneBounds
|
|
29
|
+
from kirby_world.movement_legality import movement_reach
|
|
30
|
+
|
|
31
|
+
house = Scene(id="house", name="Safehouse", bounds=SceneBounds(0, 0, 0, 20, 20, 4),
|
|
32
|
+
surfaces=[], walls=[], hazards=[], ambient=AmbientConditions())
|
|
33
|
+
house = house.place_combatant("ana", Position(1.0, 0.0, 0.0))
|
|
34
|
+
movement_reach("flight", Position(0, 0, 1), Position(5, 0, 1), 10.0, house).reachable
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Where it sits
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
kirby-terrain what a thing is made of no deps
|
|
41
|
+
kirby-world places -> kirby-terrain
|
|
42
|
+
kirby-combat the fight -> kirby-world, kirby-cost, kirby-dice
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Questions about the person moving rather than the place — is he Stunned,
|
|
46
|
+
what has this building already absorbed — belong to the fight. They arrive
|
|
47
|
+
here as plain values (`mover_can_move`, a `damage` ledger), never as the
|
|
48
|
+
fight itself. `tests/test_a_place_without_a_fight.py` enforces that by
|
|
49
|
+
making the fight engine unimportable and building a house anyway.
|
|
50
|
+
|
|
51
|
+
Split out of kirby-combat at 0.20.0, where `kirby_combat.scene` remains as a
|
|
52
|
+
re-export of this package.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
PolyForm Noncommercial 1.0.0. Not affiliated with or endorsed by Hero Games.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# kirby-world
|
|
2
|
+
|
|
3
|
+
Places for the Kirby HERO System platform: a `World` of `Scene`s, and the
|
|
4
|
+
geometry inside them — surfaces, walls, furnishings, hazards, constructs,
|
|
5
|
+
cover, falling, line of sight and per-mode movement reachability.
|
|
6
|
+
|
|
7
|
+
A place is not a fight. A house with five people doing chores, and what is
|
|
8
|
+
reachable from the kitchen, needs nothing but this package:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from kirby_world import AmbientConditions, Position, Scene, SceneBounds
|
|
12
|
+
from kirby_world.movement_legality import movement_reach
|
|
13
|
+
|
|
14
|
+
house = Scene(id="house", name="Safehouse", bounds=SceneBounds(0, 0, 0, 20, 20, 4),
|
|
15
|
+
surfaces=[], walls=[], hazards=[], ambient=AmbientConditions())
|
|
16
|
+
house = house.place_combatant("ana", Position(1.0, 0.0, 0.0))
|
|
17
|
+
movement_reach("flight", Position(0, 0, 1), Position(5, 0, 1), 10.0, house).reachable
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Where it sits
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
kirby-terrain what a thing is made of no deps
|
|
24
|
+
kirby-world places -> kirby-terrain
|
|
25
|
+
kirby-combat the fight -> kirby-world, kirby-cost, kirby-dice
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Questions about the person moving rather than the place — is he Stunned,
|
|
29
|
+
what has this building already absorbed — belong to the fight. They arrive
|
|
30
|
+
here as plain values (`mover_can_move`, a `damage` ledger), never as the
|
|
31
|
+
fight itself. `tests/test_a_place_without_a_fight.py` enforces that by
|
|
32
|
+
making the fight engine unimportable and building a house anyway.
|
|
33
|
+
|
|
34
|
+
Split out of kirby-combat at 0.20.0, where `kirby_combat.scene` remains as a
|
|
35
|
+
re-export of this package.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
PolyForm Noncommercial 1.0.0. Not affiliated with or endorsed by Hero Games.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""kirby-world — places: a World of Scenes, and the geometry inside them.
|
|
2
|
+
|
|
3
|
+
Pure: no dice, no rulebook engine, no database. A house with five people in
|
|
4
|
+
it, and what is reachable from the kitchen, needs nothing else installed.
|
|
5
|
+
The fight that may happen in a place lives in kirby-combat, which depends
|
|
6
|
+
on this package and never the other way round.
|
|
7
|
+
"""
|
|
8
|
+
from kirby_world.construct import (
|
|
9
|
+
Construct,
|
|
10
|
+
ConstructEffect,
|
|
11
|
+
construct_from_hazard,
|
|
12
|
+
construct_from_spawn_spec,
|
|
13
|
+
construct_from_wall,
|
|
14
|
+
constructs_containing,
|
|
15
|
+
constructs_in,
|
|
16
|
+
)
|
|
17
|
+
from kirby_world.movement_legality import mode_requires_support
|
|
18
|
+
from kirby_world.occupancy import blocked_by_furnishing, position_of
|
|
19
|
+
from kirby_world.scene import (
|
|
20
|
+
AmbientConditions,
|
|
21
|
+
Furnishing,
|
|
22
|
+
Hazard,
|
|
23
|
+
HazardEffect,
|
|
24
|
+
Position,
|
|
25
|
+
Scene,
|
|
26
|
+
SceneBounds,
|
|
27
|
+
Surface,
|
|
28
|
+
Wall,
|
|
29
|
+
is_climbable,
|
|
30
|
+
wall_top_surface,
|
|
31
|
+
)
|
|
32
|
+
from kirby_world.world import World
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AmbientConditions",
|
|
36
|
+
"Construct",
|
|
37
|
+
"ConstructEffect",
|
|
38
|
+
"Furnishing",
|
|
39
|
+
"Hazard",
|
|
40
|
+
"HazardEffect",
|
|
41
|
+
"Position",
|
|
42
|
+
"Scene",
|
|
43
|
+
"SceneBounds",
|
|
44
|
+
"Surface",
|
|
45
|
+
"Wall",
|
|
46
|
+
"World",
|
|
47
|
+
"blocked_by_furnishing",
|
|
48
|
+
"construct_from_hazard",
|
|
49
|
+
"construct_from_spawn_spec",
|
|
50
|
+
"construct_from_wall",
|
|
51
|
+
"constructs_containing",
|
|
52
|
+
"constructs_in",
|
|
53
|
+
"is_climbable",
|
|
54
|
+
"mode_requires_support",
|
|
55
|
+
"position_of",
|
|
56
|
+
"wall_top_surface",
|
|
57
|
+
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Unified placed-entity model. A Construct is a destructible/effect-bearing
|
|
2
|
+
region of the scene — walls, hazard zones, force walls, (reserved) illusion
|
|
3
|
+
zones are all Constructs. Spec 2026-06-09 §1.1.
|
|
4
|
+
|
|
5
|
+
The engine reasons over Constructs for durability + effect resolution; the
|
|
6
|
+
authored Wall/Hazard dataclasses (scene.py) remain the template source and
|
|
7
|
+
project into Constructs via the helpers here. Frozen: a Construct's `body` is
|
|
8
|
+
the CURRENT body for one resolution; damage flows out as events and back via
|
|
9
|
+
driver hydration on the next step.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable, Mapping
|
|
14
|
+
from dataclasses import dataclass, replace
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from kirby_world.geometry import point_in_polygon_xy
|
|
18
|
+
from kirby_world.scene import Hazard, Position, Wall
|
|
19
|
+
|
|
20
|
+
EffectKind = Literal["damage", "suffocation", "status", "mental"] # "mental" reserved (v1: not resolved)
|
|
21
|
+
EffectGating = Literal["physical_def", "breathing_swimming", "mental_disbelief", "none"]
|
|
22
|
+
EffectTrigger = Literal["on_enter", "on_pass", "every_segment", "passive"]
|
|
23
|
+
ConstructKind = Literal["wall", "hazard_zone", "force_wall", "illusion_zone", "darkness_zone"] # illusion_zone reserved
|
|
24
|
+
Permeability = Literal["porous", "impermeable"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ConstructEffect:
|
|
29
|
+
"""A defense-gated consequence for occupants/passers of a Construct."""
|
|
30
|
+
kind: EffectKind
|
|
31
|
+
gating: EffectGating = "none"
|
|
32
|
+
trigger: EffectTrigger = "passive"
|
|
33
|
+
damage_dice: int = 0
|
|
34
|
+
damage_type: str = "normal"
|
|
35
|
+
status_inflicted: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Construct:
|
|
40
|
+
"""An independently placed region with optional durability and effect."""
|
|
41
|
+
obj_id: str
|
|
42
|
+
kind: ConstructKind
|
|
43
|
+
# geometry: a segment (walls/barriers) OR a polygon (zones) — exactly one
|
|
44
|
+
segment: tuple[Position, Position] | None = None
|
|
45
|
+
polygon_xy: list[tuple[float, float]] | None = None
|
|
46
|
+
elevation_range_m: tuple[float, float] = (0.0, 0.0)
|
|
47
|
+
height_m: float = 0.0
|
|
48
|
+
# blocking / cover
|
|
49
|
+
blocks_los: bool = False
|
|
50
|
+
blocks_movement: bool = False
|
|
51
|
+
permeability: Permeability = "porous"
|
|
52
|
+
cover_level: int = 0
|
|
53
|
+
#: Levels of "Cannot Be Escaped With Teleportation" on the source
|
|
54
|
+
#: Barrier (6E1 p175): a teleport path may not pass this construct
|
|
55
|
+
#: unless the Teleportation carries at least as many levels of
|
|
56
|
+
#: Armor Piercing (movement_legality gates on it). 0 = no effect.
|
|
57
|
+
no_teleport_levels: int = 0
|
|
58
|
+
# durability (both None => indestructible). `body` is CURRENT body.
|
|
59
|
+
#: Can somebody strong enough pick this up and throw it?
|
|
60
|
+
#:
|
|
61
|
+
#: PORTABLE IS A PROPERTY OF THE OBJECT, not a kind of object. `pickup`
|
|
62
|
+
#: used to filter on `kind == "debris"` --- a kind that is not in
|
|
63
|
+
#: `ConstructKind` and that nothing in this engine has ever created, so
|
|
64
|
+
#: `pickup` and `throw_object` were two complete action kinds that
|
|
65
|
+
#: could never fire. A whiskey barrel is not rubble and never becomes
|
|
66
|
+
#: any; a wagon is not a special sort of wall. Both are simply things
|
|
67
|
+
#: light enough to lift, which is what the weight gate already asks.
|
|
68
|
+
portable: bool = False
|
|
69
|
+
def_value: int | None = None
|
|
70
|
+
ed_value: int | None = None # ED, when it differs from PD (6E2 p173)
|
|
71
|
+
resistant: bool = True # False = Normal Defense, not applied vs Killing
|
|
72
|
+
body: int | None = None
|
|
73
|
+
#: The obj_id of the structure this is a FACE of. Rides across from
|
|
74
|
+
#: `Wall.part_of` --- and MUST, because the resolvers act on the
|
|
75
|
+
#: projected Construct, so a face that lost this on the way through
|
|
76
|
+
#: would be demolished as a building instead of breached as a wall.
|
|
77
|
+
part_of: str | None = None
|
|
78
|
+
# effect / provenance
|
|
79
|
+
effect: ConstructEffect | None = None
|
|
80
|
+
source_combatant_id: str | None = None
|
|
81
|
+
created_at_seq: int | None = None
|
|
82
|
+
# sense-affecting (darkness_zone): the Sense Group this zone occludes
|
|
83
|
+
# (e.g. "sight", "mental") and whether the creating combatant sees through it.
|
|
84
|
+
sense_group: str | None = None
|
|
85
|
+
creator_immune: bool = False
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def destructible(self) -> bool:
|
|
89
|
+
return self.def_value is not None and self.body is not None
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def pd(self) -> int | None:
|
|
93
|
+
"""Physical defense. `def_value` has always meant PD; this names it."""
|
|
94
|
+
return self.def_value
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def ed(self) -> int | None:
|
|
98
|
+
"""Energy defense, falling back to PD when the construct does not state
|
|
99
|
+
one. The fallback is what keeps every pre-existing construct -- here and
|
|
100
|
+
in kirby-api, which constructs these by keyword -- behaving exactly
|
|
101
|
+
as it did before PD and ED were separated."""
|
|
102
|
+
return self.def_value if self.ed_value is None else self.ed_value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def construct_from_spawn_spec(
|
|
106
|
+
*,
|
|
107
|
+
obj_id: str,
|
|
108
|
+
kind: ConstructKind,
|
|
109
|
+
segment: tuple[Position, Position] | None = None,
|
|
110
|
+
polygon_xy: list[tuple[float, float]] | None = None,
|
|
111
|
+
elevation_range_m: tuple[float, float] = (0.0, 0.0),
|
|
112
|
+
height_m: float = 0.0,
|
|
113
|
+
def_value: int | None = None,
|
|
114
|
+
body: int | None = None,
|
|
115
|
+
effect: ConstructEffect | None = None,
|
|
116
|
+
permeability: Permeability | None = None,
|
|
117
|
+
source_combatant_id: str | None = None,
|
|
118
|
+
created_at_seq: int | None = None,
|
|
119
|
+
sense_group: str | None = None,
|
|
120
|
+
creator_immune: bool = False,
|
|
121
|
+
) -> Construct:
|
|
122
|
+
"""Build a spawned Construct (force wall / spawn-on-destroy hazard). A
|
|
123
|
+
force_wall defaults to a LoS+movement blocking impermeable barrier; a
|
|
124
|
+
hazard_zone defaults to porous. the driver computes def/body/geometry from the
|
|
125
|
+
casting power (Plan 2) and passes them here. Spec §1.7."""
|
|
126
|
+
if permeability is None:
|
|
127
|
+
permeability = "impermeable" if kind in ("wall", "force_wall") else "porous"
|
|
128
|
+
blocks = kind in ("wall", "force_wall")
|
|
129
|
+
return Construct(
|
|
130
|
+
obj_id=obj_id, kind=kind, segment=segment, polygon_xy=polygon_xy,
|
|
131
|
+
elevation_range_m=elevation_range_m, height_m=height_m,
|
|
132
|
+
blocks_los=blocks, blocks_movement=blocks, permeability=permeability,
|
|
133
|
+
cover_level=4 if blocks else 0,
|
|
134
|
+
def_value=def_value, body=body, effect=effect,
|
|
135
|
+
source_combatant_id=source_combatant_id, created_at_seq=created_at_seq,
|
|
136
|
+
sense_group=sense_group, creator_immune=creator_immune,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def constructs_containing(pos: Position, constructs: Iterable[Construct]) -> list[Construct]:
|
|
141
|
+
"""Return constructs whose polygon contains `pos` in xy within their elevation
|
|
142
|
+
range (segment constructs are never 'containing' a point)."""
|
|
143
|
+
out: list[Construct] = []
|
|
144
|
+
for c in constructs:
|
|
145
|
+
if c.polygon_xy is None:
|
|
146
|
+
continue
|
|
147
|
+
lo, hi = c.elevation_range_m
|
|
148
|
+
if not (lo <= pos.z <= hi):
|
|
149
|
+
continue
|
|
150
|
+
if point_in_polygon_xy((pos.x, pos.y), c.polygon_xy):
|
|
151
|
+
out.append(c)
|
|
152
|
+
return out
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def construct_from_wall(wall: Wall) -> Construct:
|
|
156
|
+
"""Project an authored Wall into a Construct. `def_value` rides Wall.def_value
|
|
157
|
+
(None on legacy walls with no authored DEF)."""
|
|
158
|
+
return Construct(
|
|
159
|
+
obj_id=wall.id, kind="wall",
|
|
160
|
+
segment=wall.segment, height_m=wall.height_m,
|
|
161
|
+
blocks_los=wall.blocks_los, blocks_movement=wall.blocks_movement,
|
|
162
|
+
permeability="impermeable" if wall.blocks_movement else "porous",
|
|
163
|
+
cover_level=wall.cover_level,
|
|
164
|
+
def_value=wall.def_value, ed_value=wall.ed_value, resistant=wall.resistant,
|
|
165
|
+
body=wall.body, part_of=wall.part_of,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def construct_from_hazard(hazard: Hazard) -> Construct:
|
|
170
|
+
"""Project a legacy authored Hazard into a hazard_zone Construct (porous,
|
|
171
|
+
physical-DEF-gated damage or a status). Suffocation hazards are authored as
|
|
172
|
+
Constructs directly by the driver, not via the legacy Hazard model."""
|
|
173
|
+
eff = hazard.effect
|
|
174
|
+
if not eff.damage_dice and not eff.status_inflicted:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Hazard {hazard.id!r}: HazardEffect has neither damage_dice nor "
|
|
177
|
+
"status_inflicted — nothing to project into a Construct effect"
|
|
178
|
+
)
|
|
179
|
+
kind: EffectKind = "damage" if eff.damage_dice else "status"
|
|
180
|
+
return Construct(
|
|
181
|
+
obj_id=hazard.id, kind="hazard_zone",
|
|
182
|
+
polygon_xy=list(hazard.polygon_xy), elevation_range_m=hazard.elevation_range_m,
|
|
183
|
+
permeability="porous",
|
|
184
|
+
effect=ConstructEffect(
|
|
185
|
+
kind=kind,
|
|
186
|
+
gating="physical_def" if kind == "damage" else "none",
|
|
187
|
+
trigger=hazard.trigger,
|
|
188
|
+
damage_dice=eff.damage_dice, damage_type=eff.damage_type,
|
|
189
|
+
status_inflicted=eff.status_inflicted,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def constructs_in(scene, damage: Mapping[str, int] | None = None) -> list[Construct]:
|
|
195
|
+
"""Everything in this scene that an attack can be aimed AT.
|
|
196
|
+
|
|
197
|
+
The authored constructs, plus every authored `Wall` projected through
|
|
198
|
+
`construct_from_wall`. THE PROJECTION EXISTED AND NOTHING CALLED IT:
|
|
199
|
+
`attack_construct` iterates constructs, so before this the O.K.
|
|
200
|
+
Corral's buildings, barrels and packing crates were not in the fight
|
|
201
|
+
at all, and `smash_cover` --- a tactic whose whole purpose is
|
|
202
|
+
destroying what somebody is hiding behind --- had nothing to smash.
|
|
203
|
+
|
|
204
|
+
Indestructible walls are left out. `def_value is None` is this
|
|
205
|
+
engine's word for "legacy / indestructible", and a target nothing can
|
|
206
|
+
hurt is an offer that wastes a Phase.
|
|
207
|
+
|
|
208
|
+
Walls stay in `scene.walls` as well: movement and line of sight read
|
|
209
|
+
them there, and this is a second VIEW of the same geometry rather than
|
|
210
|
+
a second copy of it.
|
|
211
|
+
|
|
212
|
+
`damage` is BODY already lost, by `obj_id`. A place does not know what
|
|
213
|
+
happened to it --- the fight that did the damage keeps that ledger
|
|
214
|
+
(kirby-combat folds it from its event log) and hands the totals in.
|
|
215
|
+
Without it these are the constructs as authored.
|
|
216
|
+
"""
|
|
217
|
+
out = list(getattr(scene, "constructs", None) or [])
|
|
218
|
+
seen = {getattr(c, "obj_id", None) for c in out}
|
|
219
|
+
# FURNISHINGS ARE THINGS YOU CAN SHOOT. A wagon nobody can hit is a
|
|
220
|
+
# wagon nobody can shoot the cover out from under, which is precisely
|
|
221
|
+
# what `smash_cover` exists to do. Their durability comes from 6E2
|
|
222
|
+
# p.173's Objects Table via the material they name, so nobody types a
|
|
223
|
+
# BODY into a scene file by hand.
|
|
224
|
+
for f in (getattr(scene, "furnishings", None) or []):
|
|
225
|
+
if f.id in seen:
|
|
226
|
+
continue
|
|
227
|
+
seen.add(f.id)
|
|
228
|
+
out.append(Construct(
|
|
229
|
+
obj_id=f.id, kind="wall",
|
|
230
|
+
polygon_xy=list(f.polygon_xy),
|
|
231
|
+
elevation_range_m=(0.0, f.height_m),
|
|
232
|
+
body=f.body_value, def_value=f.pd_value,
|
|
233
|
+
ed_value=f.ed_value,
|
|
234
|
+
blocks_los=f.blocks_los,
|
|
235
|
+
blocks_movement=f.blocks_movement,
|
|
236
|
+
# A flag that stops at the projection is the same defect one
|
|
237
|
+
# layer down -- see `Furnishing.portable`.
|
|
238
|
+
portable=f.portable,
|
|
239
|
+
))
|
|
240
|
+
for wall in (getattr(scene, "walls", None) or []):
|
|
241
|
+
if getattr(wall, "def_value", None) is None:
|
|
242
|
+
continue
|
|
243
|
+
if wall.id in seen:
|
|
244
|
+
continue
|
|
245
|
+
# A FURNISHING'S EDGES ARE THAT FURNISHING, which is already in
|
|
246
|
+
# this list. Without this a wagon is FIVE targets --- itself and
|
|
247
|
+
# its four sides, each a separate offer with the same DEF and
|
|
248
|
+
# BODY. Measured on the long street: 30 constructs from 5
|
|
249
|
+
# furnishings and 25 walls, 25 of them five objects repeated, and
|
|
250
|
+
# a menu where `attack_construct` and `rapid_fire` were about
|
|
251
|
+
# nine offers in ten.
|
|
252
|
+
if hasattr(scene, "is_furnishing_edge") and scene.is_furnishing_edge(wall):
|
|
253
|
+
continue
|
|
254
|
+
out.append(construct_from_wall(wall))
|
|
255
|
+
|
|
256
|
+
# HYDRATION. Without a damage ledger these are the buildings as
|
|
257
|
+
# authored, which is what every caller that has no fight wants.
|
|
258
|
+
if damage is not None:
|
|
259
|
+
hydrated = []
|
|
260
|
+
for c in out:
|
|
261
|
+
if c.body is None:
|
|
262
|
+
hydrated.append(c)
|
|
263
|
+
continue
|
|
264
|
+
left = max(0, c.body - int(damage.get(c.obj_id, 0)))
|
|
265
|
+
# RUBBLE IS NOT A BUILDING. A destroyed construct used to stay
|
|
266
|
+
# in the scene and be knocked over again --- the Harwood House
|
|
267
|
+
# came down on Tom McLaury, Frank McLaury and Billy Clanton,
|
|
268
|
+
# and then came down on them three more times. Once its BODY
|
|
269
|
+
# is gone it stops being a thing you can shoot, hide behind,
|
|
270
|
+
# or drop on somebody.
|
|
271
|
+
if left <= 0:
|
|
272
|
+
continue
|
|
273
|
+
hydrated.append(replace(c, body=left))
|
|
274
|
+
out = hydrated
|
|
275
|
+
return out
|