ff9mapkit 1.0.0b3__py3-none-any.whl
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.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
ff9mapkit/itemstats.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Item stat / effect catalog -- *what an FF9 item DOES* (weapon power, armor defence, equip stat bonuses,
|
|
2
|
+
consumable use-effect, price, type, who-can-equip). The enrichment layer over :mod:`ff9mapkit.items` (which
|
|
3
|
+
is names-only): it powers the Info Hub item detail + ``ff9mapkit items``.
|
|
4
|
+
|
|
5
|
+
PROVENANCE -- item STATS are game DATA, never committed (docs/PROVENANCE.md: the committed ``_*.py`` tables
|
|
6
|
+
hold names/ids ONLY). So this reads the numbers LIVE from YOUR OWN install and ships/commits nothing:
|
|
7
|
+
|
|
8
|
+
<install>/StreamingAssets/Data/Items/{Items,Weapons,Armors,Stats,ItemEffects}.csv
|
|
9
|
+
|
|
10
|
+
-- Memoria's editable item tables (semicolon-delimited; the very tables the engine loads). They're cached
|
|
11
|
+
in-memory per process. If the install/CSVs aren't reachable, every accessor returns ``None`` and callers
|
|
12
|
+
degrade to id+name only (the Info Hub still works offline, just without the stat lines).
|
|
13
|
+
|
|
14
|
+
Column layout is read from each file's ``# <names...>`` header legend (not hard-coded indices), so it
|
|
15
|
+
survives Memoria's option-driven column toggles (``#! IncludeSellingPrice`` etc.).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field as _dcfield
|
|
20
|
+
|
|
21
|
+
from . import items as _items
|
|
22
|
+
|
|
23
|
+
# Element bitmask -- Stats.csv legend ("1-Fire 2-Ice 4-Thunder 8-Earth / 16-Water 32-Wind 64-Holy 128-Dark"),
|
|
24
|
+
# = the engine's Memoria.Data EffectElement enum (Ice=Cold, Water=Aqua, Dark=Darkness).
|
|
25
|
+
ELEMENTS = [(1, "Fire"), (2, "Ice"), (4, "Thunder"), (8, "Earth"),
|
|
26
|
+
(16, "Water"), (32, "Wind"), (64, "Holy"), (128, "Dark")]
|
|
27
|
+
# WeaponCategory bits (Memoria.Data.WeaponCategory): 128 "Default" is a no-op flag, omitted.
|
|
28
|
+
WEAPON_CATEGORY = [(1, "short-range"), (2, "long-range"), (4, "throw"), (8, "offset")]
|
|
29
|
+
# The 8 Items.csv type-bit columns -> a friendly slot/kind (one item may set several, e.g. Item+Usable).
|
|
30
|
+
TYPE_COLS = [("Weapon", "weapon"), ("Armlet", "wrist"), ("Helmet", "head"), ("Armor", "body"),
|
|
31
|
+
("Accessory", "accessory"), ("Item", "item"), ("Gem", "gem"), ("Usable", "usable")]
|
|
32
|
+
# Items.csv per-character equip-bit columns, in order (= the engine's ItemCharacter mask).
|
|
33
|
+
CHARS = ["Zidane", "Vivi", "Garnet", "Steiner", "Freya", "Quina", "Eiko", "Amarant",
|
|
34
|
+
"Cinna", "Marcus", "Blank", "Beatrix"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def decode_elements(mask) -> list:
|
|
38
|
+
"""An element bitmask -> the list of element names it sets (e.g. ``5`` -> ``['Fire', 'Thunder']``)."""
|
|
39
|
+
try:
|
|
40
|
+
m = int(mask)
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return []
|
|
43
|
+
return [name for bit, name in ELEMENTS if m & bit]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def decode_category(mask) -> list:
|
|
47
|
+
try:
|
|
48
|
+
m = int(mask)
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
return []
|
|
51
|
+
return [name for bit, name in WEAPON_CATEGORY if m & bit]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# BattleStatus bitmask (Memoria.Data.Battle.BattleStatus) -- a consumable's use-effect carries its status
|
|
55
|
+
# set here (a cure item like Phoenix Down/Antidote has Power 0 and acts ENTIRELY via this mask; the add-vs-
|
|
56
|
+
# remove direction is the effect's ScriptId, which we don't decode -- so we name the statuses neutrally).
|
|
57
|
+
STATUSES = [
|
|
58
|
+
(1 << 0, "Petrify"), (1 << 1, "Venom"), (1 << 2, "Virus"), (1 << 3, "Silence"),
|
|
59
|
+
(1 << 4, "Blind"), (1 << 5, "Trouble"), (1 << 6, "Zombie"), (1 << 7, "EasyKill"),
|
|
60
|
+
(1 << 8, "Death"), (1 << 9, "LowHP"), (1 << 10, "Confuse"), (1 << 11, "Berserk"),
|
|
61
|
+
(1 << 12, "Stop"), (1 << 13, "AutoLife"), (1 << 14, "Trance"), (1 << 15, "Defend"),
|
|
62
|
+
(1 << 16, "Poison"), (1 << 17, "Sleep"), (1 << 18, "Regen"), (1 << 19, "Haste"),
|
|
63
|
+
(1 << 20, "Slow"), (1 << 21, "Float"), (1 << 22, "Shell"), (1 << 23, "Protect"),
|
|
64
|
+
(1 << 24, "Heat"), (1 << 25, "Freeze"), (1 << 26, "Vanish"), (1 << 27, "Doom"),
|
|
65
|
+
(1 << 28, "Mini"), (1 << 29, "Reflect"), (1 << 30, "Jump"), (1 << 31, "GradualPetrify"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def decode_status(mask) -> list:
|
|
70
|
+
"""A BattleStatus bitmask -> the list of status names it sets (e.g. ``256`` -> ``['Death']``)."""
|
|
71
|
+
try:
|
|
72
|
+
m = int(mask)
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
return []
|
|
75
|
+
return [name for bit, name in STATUSES if m & bit]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class ItemStat:
|
|
80
|
+
"""The joined stat record for one item id (the fields that apply to its type are populated; the rest
|
|
81
|
+
stay ``None``/empty). ``bonus``/``affinity`` carry only NON-zero entries (so an Empty bonus shows nothing)."""
|
|
82
|
+
id: int
|
|
83
|
+
name: str = ""
|
|
84
|
+
types: list = _dcfield(default_factory=list) # ['weapon'] / ['body'] / ['item','usable'] ...
|
|
85
|
+
price: int = 0
|
|
86
|
+
sell: int = 0
|
|
87
|
+
equip: list = _dcfield(default_factory=list) # character names that can equip it
|
|
88
|
+
abilities: list = _dcfield(default_factory=list) # raw ability tokens taught when equipped (AA:/SA:)
|
|
89
|
+
# weapon (WeaponId >= 0)
|
|
90
|
+
power: "int | None" = None
|
|
91
|
+
elements: list = _dcfield(default_factory=list)
|
|
92
|
+
category: list = _dcfield(default_factory=list)
|
|
93
|
+
# armor (ArmorId >= 0)
|
|
94
|
+
pdef: "int | None" = None
|
|
95
|
+
peva: "int | None" = None
|
|
96
|
+
mdef: "int | None" = None
|
|
97
|
+
meva: "int | None" = None
|
|
98
|
+
# equip stat bonuses + elemental affinity (Stats.csv via BonusId)
|
|
99
|
+
bonus: dict = _dcfield(default_factory=dict) # {'Strength': 3, ...} non-zero only
|
|
100
|
+
affinity: dict = _dcfield(default_factory=dict) # {'absorb': ['Fire'], 'half': [...]} non-empty only
|
|
101
|
+
# consumable use-effect (EffectId >= 0)
|
|
102
|
+
effect_power: "int | None" = None
|
|
103
|
+
effect_elements: list = _dcfield(default_factory=list)
|
|
104
|
+
effect_status: int = 0
|
|
105
|
+
effect_statuses: list = _dcfield(default_factory=list) # decoded status names from effect_status
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_weapon(self) -> bool:
|
|
109
|
+
return self.power is not None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_armor(self) -> bool:
|
|
113
|
+
return self.pdef is not None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_equippable(self) -> bool:
|
|
117
|
+
"""True when the item occupies an equipment slot (weapon/wrist/head/body/accessory/gem) -- so an equip
|
|
118
|
+
stat bonus ([[equip_bonus]]) applies to it. A pure consumable (item/usable only) is not equippable."""
|
|
119
|
+
return any(t in ("weapon", "wrist", "head", "body", "accessory", "gem") for t in self.types)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def is_consumable(self) -> bool:
|
|
123
|
+
"""Has an effect row (an EffectId that joined) -- structural. A row can still be empty: use
|
|
124
|
+
:attr:`has_use_effect` to decide whether there is anything worth SHOWING."""
|
|
125
|
+
return self.effect_power is not None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def has_use_effect(self) -> bool:
|
|
129
|
+
"""True when the use-effect conveys something (non-zero power, an element, or a status) -- so an
|
|
130
|
+
all-zero effect row (e.g. a stat-bonus accessory with a dummy EffectId) shows no use-effect line."""
|
|
131
|
+
return bool(self.effect_power or self.effect_elements or self.effect_statuses)
|
|
132
|
+
|
|
133
|
+
def effect_desc(self) -> str:
|
|
134
|
+
"""A neutral one-phrase description of the use-effect (``"power 10"`` / ``"status Death"`` /
|
|
135
|
+
``"power 20, Fire, status Poison"``); empty when :attr:`has_use_effect` is False."""
|
|
136
|
+
parts = []
|
|
137
|
+
if self.effect_power:
|
|
138
|
+
parts.append(f"power {self.effect_power}")
|
|
139
|
+
if self.effect_elements:
|
|
140
|
+
parts.append("/".join(self.effect_elements))
|
|
141
|
+
if self.effect_statuses:
|
|
142
|
+
parts.append("status " + "/".join(self.effect_statuses))
|
|
143
|
+
return ", ".join(parts)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---- CSV parsing ----------------------------------------------------------------------------------
|
|
147
|
+
def _read_csv(path) -> tuple:
|
|
148
|
+
"""Parse a Memoria item CSV: returns ``(name->index, [row-as-list])``. Column names come from the
|
|
149
|
+
file's ``#``-legend line (the first comment line whose fields include ``Id``); data rows are the
|
|
150
|
+
non-``#``-leading lines split on ``;`` (a trailing ``# nnn - Name`` comment cell is left as an extra
|
|
151
|
+
field and ignored by index access). The first data column may itself be a Comment string containing a
|
|
152
|
+
``#`` (e.g. ``Bonus 0000 # Empty``) -- only a line whose first non-space char is ``#`` is a comment."""
|
|
153
|
+
cols: "dict | None" = None
|
|
154
|
+
rows: list = []
|
|
155
|
+
# utf-8-sig strips a leading BOM if a localized install has one (else a BOM'd first line would fail the
|
|
156
|
+
# `startswith("#")` legend/comment test); splitlines() handles CRLF.
|
|
157
|
+
for raw in path.read_text(encoding="utf-8-sig", errors="replace").splitlines():
|
|
158
|
+
s = raw.strip()
|
|
159
|
+
if not s:
|
|
160
|
+
continue
|
|
161
|
+
if s.startswith("#"):
|
|
162
|
+
if cols is None:
|
|
163
|
+
fields = [f.strip() for f in s.lstrip("#").strip().split(";")]
|
|
164
|
+
if "Id" in fields and len(fields) > 1:
|
|
165
|
+
cols = {name: i for i, name in enumerate(fields)}
|
|
166
|
+
continue
|
|
167
|
+
rows.append([c.strip() for c in raw.split(";")])
|
|
168
|
+
return (cols or {}), rows
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _i(row, cols, name, default=None):
|
|
172
|
+
"""Integer cell ``name`` from ``row`` (None/default on a missing column or non-int)."""
|
|
173
|
+
idx = cols.get(name)
|
|
174
|
+
if idx is None or idx >= len(row):
|
|
175
|
+
return default
|
|
176
|
+
try:
|
|
177
|
+
return int(row[idx])
|
|
178
|
+
except (ValueError, TypeError):
|
|
179
|
+
return default
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---- the in-memory join ---------------------------------------------------------------------------
|
|
183
|
+
_CACHE = None # None = not loaded yet; False = tried + unavailable; dict = {item_id: ItemStat}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _load(game=None):
|
|
187
|
+
"""Read + join the five item CSVs from the install (cached). Returns ``{id: ItemStat}`` or ``None`` if
|
|
188
|
+
the install / a CSV can't be read (cached as unavailable so we don't re-probe the filesystem)."""
|
|
189
|
+
global _CACHE
|
|
190
|
+
if _CACHE is not None:
|
|
191
|
+
return _CACHE or None
|
|
192
|
+
try:
|
|
193
|
+
from .config import find_game_path
|
|
194
|
+
d = find_game_path(game) / "StreamingAssets" / "Data" / "Items"
|
|
195
|
+
icols, irows = _read_csv(d / "Items.csv")
|
|
196
|
+
wcols, wrows = _read_csv(d / "Weapons.csv")
|
|
197
|
+
acols, arows = _read_csv(d / "Armors.csv")
|
|
198
|
+
scols, srows = _read_csv(d / "Stats.csv")
|
|
199
|
+
ecols, erows = _read_csv(d / "ItemEffects.csv")
|
|
200
|
+
if not (icols and irows):
|
|
201
|
+
raise ValueError("Items.csv had no parseable header/rows")
|
|
202
|
+
except (FileNotFoundError, OSError, RuntimeError, ValueError):
|
|
203
|
+
_CACHE = False
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
weap = {_i(r, wcols, "Id"): r for r in wrows if _i(r, wcols, "Id") is not None}
|
|
207
|
+
armor = {_i(r, acols, "Id"): r for r in arows if _i(r, acols, "Id") is not None}
|
|
208
|
+
stat = {_i(r, scols, "Id"): r for r in srows if _i(r, scols, "Id") is not None}
|
|
209
|
+
effect = {_i(r, ecols, "Id"): r for r in erows if _i(r, ecols, "Id") is not None}
|
|
210
|
+
|
|
211
|
+
out: dict = {}
|
|
212
|
+
for r in irows:
|
|
213
|
+
iid = _i(r, icols, "Id")
|
|
214
|
+
if iid is None or iid == 255: # 255 = NoItem (the empty sentinel) -> not a real item
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
out[iid] = _build(iid, r, icols, weap, wcols, armor, acols, stat, scols, effect, ecols)
|
|
218
|
+
except (ValueError, IndexError, KeyError):
|
|
219
|
+
continue # a malformed row -> skip, don't sink the whole load
|
|
220
|
+
_CACHE = out or False
|
|
221
|
+
return out or None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _build(iid, r, ic, weap, wc, armor, ac, stat, sc, effect, ec) -> ItemStat:
|
|
225
|
+
st = ItemStat(id=iid, name=_items.name_of(iid) or "")
|
|
226
|
+
st.types = [friendly for col, friendly in TYPE_COLS if _i(r, ic, col, 0)]
|
|
227
|
+
st.price = _i(r, ic, "Price", 0) or 0
|
|
228
|
+
st.sell = _i(r, ic, "SellingPrice", st.price // 2) or 0
|
|
229
|
+
st.equip = [c for c in CHARS if _i(r, ic, c, 0)]
|
|
230
|
+
raw_ab = r[ic["AbilityIds"]] if "AbilityIds" in ic and ic["AbilityIds"] < len(r) else ""
|
|
231
|
+
st.abilities = [t.strip() for t in raw_ab.split(",") if t.strip() and t.strip() != "0"]
|
|
232
|
+
|
|
233
|
+
wid = _i(r, ic, "WeaponId", -1)
|
|
234
|
+
if wid is not None and wid >= 0 and wid in weap:
|
|
235
|
+
wr = weap[wid]
|
|
236
|
+
st.power = _i(wr, wc, "Power", 0)
|
|
237
|
+
st.elements = decode_elements(_i(wr, wc, "Elements", 0))
|
|
238
|
+
st.category = decode_category(_i(wr, wc, "Category", 0))
|
|
239
|
+
|
|
240
|
+
aid = _i(r, ic, "ArmorId", -1)
|
|
241
|
+
if aid is not None and aid >= 0 and aid in armor:
|
|
242
|
+
ar = armor[aid]
|
|
243
|
+
st.pdef = _i(ar, ac, "P.Def", 0)
|
|
244
|
+
st.peva = _i(ar, ac, "P.Eva", 0)
|
|
245
|
+
st.mdef = _i(ar, ac, "M.Def", 0)
|
|
246
|
+
st.meva = _i(ar, ac, "M.Eva", 0)
|
|
247
|
+
|
|
248
|
+
bid = _i(r, ic, "BonusId", -1)
|
|
249
|
+
if bid is not None and bid in stat:
|
|
250
|
+
sr = stat[bid]
|
|
251
|
+
for col, label in (("Dexterity", "Speed"), ("Strength", "Strength"),
|
|
252
|
+
("Magic", "Magic"), ("Will", "Spirit")):
|
|
253
|
+
v = _i(sr, sc, col, 0) or 0
|
|
254
|
+
if v:
|
|
255
|
+
st.bonus[label] = v
|
|
256
|
+
for col, label in (("AttackElement", "boosts"), ("GuardElement", "nullify"),
|
|
257
|
+
("AbsorbElement", "absorb"), ("HalfElement", "halve"), ("WeakElement", "weak to")):
|
|
258
|
+
els = decode_elements(_i(sr, sc, col, 0))
|
|
259
|
+
if els:
|
|
260
|
+
st.affinity[label] = els
|
|
261
|
+
|
|
262
|
+
eid = _i(r, ic, "EffectId", -1)
|
|
263
|
+
if eid is not None and eid >= 0 and eid in effect:
|
|
264
|
+
er = effect[eid]
|
|
265
|
+
st.effect_power = _i(er, ec, "Power", 0)
|
|
266
|
+
st.effect_elements = decode_elements(_i(er, ec, "Element", 0))
|
|
267
|
+
st.effect_status = _i(er, ec, "Status", 0) or 0
|
|
268
|
+
st.effect_statuses = decode_status(st.effect_status)
|
|
269
|
+
return st
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---- public API -----------------------------------------------------------------------------------
|
|
273
|
+
def available(game=None) -> bool:
|
|
274
|
+
"""True if the install's item CSVs could be read (so stat enrichment is live)."""
|
|
275
|
+
return _load(game) is not None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def for_id(item_id, *, game=None):
|
|
279
|
+
"""The :class:`ItemStat` for an item id, or ``None`` (unknown id, or the install isn't reachable)."""
|
|
280
|
+
table = _load(game)
|
|
281
|
+
if not table:
|
|
282
|
+
return None
|
|
283
|
+
try:
|
|
284
|
+
return table.get(int(item_id))
|
|
285
|
+
except (ValueError, TypeError):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def summary(item_id, *, game=None):
|
|
290
|
+
"""A one-line headline for an item (``"weapon - Atk 12, 320 gil"``), or ``None`` if stats aren't loaded.
|
|
291
|
+
Used for the Info Hub browse row + ``ff9mapkit items``."""
|
|
292
|
+
st = for_id(item_id, game=game)
|
|
293
|
+
if st is None:
|
|
294
|
+
return None
|
|
295
|
+
kind = "/".join(st.types) if st.types else "item"
|
|
296
|
+
bits = []
|
|
297
|
+
if st.is_weapon:
|
|
298
|
+
head = f"Atk {st.power}"
|
|
299
|
+
if st.elements:
|
|
300
|
+
head += " " + "/".join(st.elements)
|
|
301
|
+
bits.append(head)
|
|
302
|
+
if st.is_armor and (st.pdef or st.mdef):
|
|
303
|
+
bits.append(f"Def {st.pdef}/{st.mdef}")
|
|
304
|
+
if st.bonus:
|
|
305
|
+
bits.append(", ".join(f"{k}+{v}" for k, v in st.bonus.items()))
|
|
306
|
+
if st.has_use_effect:
|
|
307
|
+
bits.append("effect " + st.effect_desc())
|
|
308
|
+
if st.price:
|
|
309
|
+
bits.append(f"{st.price} gil")
|
|
310
|
+
return f"{kind} - {', '.join(bits)}" if bits else kind
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def facts(item_id, *, game=None) -> list:
|
|
314
|
+
"""``[(label, value), ...]`` for the Info Hub item-detail pane. Empty list when stats aren't loaded."""
|
|
315
|
+
st = for_id(item_id, game=game)
|
|
316
|
+
if st is None:
|
|
317
|
+
return []
|
|
318
|
+
out = [("type", "/".join(st.types) if st.types else "item"),
|
|
319
|
+
("price", f"{st.price} gil (sell {st.sell})")]
|
|
320
|
+
if st.is_weapon:
|
|
321
|
+
out.append(("attack", str(st.power)))
|
|
322
|
+
if st.elements:
|
|
323
|
+
out.append(("element", "/".join(st.elements)))
|
|
324
|
+
if st.category:
|
|
325
|
+
out.append(("weapon class", "/".join(st.category)))
|
|
326
|
+
if st.is_armor and (st.pdef or st.mdef):
|
|
327
|
+
out.append(("defence", f"P.{st.pdef} M.{st.mdef}"))
|
|
328
|
+
if st.is_armor and (st.peva or st.meva):
|
|
329
|
+
out.append(("evade", f"P.{st.peva} M.{st.meva}"))
|
|
330
|
+
if st.bonus:
|
|
331
|
+
out.append(("stat bonus", ", ".join(f"{k}+{v}" for k, v in st.bonus.items())))
|
|
332
|
+
for label, els in st.affinity.items():
|
|
333
|
+
out.append((label, "/".join(els)))
|
|
334
|
+
if st.has_use_effect:
|
|
335
|
+
out.append(("use-effect", st.effect_desc()))
|
|
336
|
+
if st.equip:
|
|
337
|
+
out.append(("equippable by", ", ".join(st.equip)))
|
|
338
|
+
if st.abilities:
|
|
339
|
+
out.append(("teaches", ", ".join(st.abilities)))
|
|
340
|
+
return out
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _reset_cache():
|
|
344
|
+
"""Test hook: drop the in-memory cache so a later call re-reads (e.g. after pointing at a fixture)."""
|
|
345
|
+
global _CACHE
|
|
346
|
+
_CACHE = None
|