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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Background-scene libraries.
|
|
2
|
+
|
|
3
|
+
cam - the camera math: project / decompose / synthesize any FF9 camera + the canvas map
|
|
4
|
+
bgx - the pure-Memoria background scene text format (overlays + camera)
|
|
5
|
+
bgi - the walkmesh codec + flat-floor / obj builder
|
|
6
|
+
guide - author a camera from a spec, frame a floor, emit a paint guide
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import bgi, bgx, cam, guide
|
|
10
|
+
|
|
11
|
+
__all__ = ["cam", "bgx", "bgi", "guide"]
|
ff9mapkit/scene/arena.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""The big flat SCROLLING checkerboard ARENA -- a wide debug stage for staging objects in a row without
|
|
2
|
+
obstruction (huge monster models, the prop/held galleries, the Info Hub in-game preview). A perspective
|
|
3
|
+
checkerboard floor (pure-stdlib placeholder art, projected through the camera so it auto-aligns with the
|
|
4
|
+
walkmesh) + a flat walkmesh + a scrolling camera (window_width 384, range N screens wide).
|
|
5
|
+
|
|
6
|
+
`build_arena()` writes the art + returns the camera/walkmesh meta; `arena_scene_lines()` gives the
|
|
7
|
+
field.toml SCENE that a caller appends `[[npc]]`/`[[prop]]` placements to (a gallery, the preview);
|
|
8
|
+
`arena_toml()` is the standalone debug stage. Lifted here from `tools/build_debug_arena.py` so the package
|
|
9
|
+
(notably the Info Hub spine's `preview_field_toml`) can build a stage without importing a dev script.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from . import guide, placeholder
|
|
17
|
+
|
|
18
|
+
PITCH, FOV = 40.0, 42.2 # downward tilt; horizontal FOV
|
|
19
|
+
DIST = 7000.0 # camera pulled back -> larger world floor = zoomed OUT
|
|
20
|
+
BACK_Y, FRONT_Y = 150.0, 430.0 # painted-canvas rows the floor's back/front edges sit on
|
|
21
|
+
BACK_SPAN = 0.43 # back edge's canvas half-span as a fraction of range_w
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_arena(art_dir: Path, *, screens: int = 3) -> dict:
|
|
25
|
+
"""Generate the arena's checkerboard art (back + floor PNGs) into ``art_dir``; return the camera +
|
|
26
|
+
walkmesh the field.toml needs (range_w, quad, zb, zf, spawn_z)."""
|
|
27
|
+
range_w = 384 * screens
|
|
28
|
+
cam = guide.make_camera(PITCH, DIST, fov_x_deg=FOV, range_wh=(range_w, 448))
|
|
29
|
+
frame = guide.frame_floor(cam, back_canvas_y=BACK_Y, front_canvas_y=FRONT_Y,
|
|
30
|
+
back_span_px=range_w * BACK_SPAN)
|
|
31
|
+
quad = [[int(x), int(z)] for (x, z) in guide.walkmesh_corners(frame)]
|
|
32
|
+
# square-ON-SCREEN checkerboard: at pitch p a world Z step foreshortens to ~sin(p) of a world X step,
|
|
33
|
+
# so world cells must be ~1/sin(p) DEEPER than wide to read square. Pick ~6 rows deep, derive columns.
|
|
34
|
+
sinp = math.sin(math.radians(PITCH))
|
|
35
|
+
width, depth = 2 * frame.half_width, abs(frame.zf - frame.zb)
|
|
36
|
+
nz = 6
|
|
37
|
+
nx = max(4, round(width / (depth / nz * sinp)))
|
|
38
|
+
art_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
placeholder.write_placeholders(cam, frame, art_dir / "back.png", art_dir / "floor.png", nx=nx, nz=nz)
|
|
40
|
+
return {"range_w": range_w, "quad": quad, "zb": frame.zb, "zf": frame.zf,
|
|
41
|
+
"spawn_z": int(round((frame.zb + frame.zf) / 2))}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def arena_scene_lines(meta: dict, *, spawn_z=None, name="ARENA", art_prefix="art") -> list:
|
|
45
|
+
"""The field.toml SCENE lines (field/camera/walkmesh/layers/player) for an arena -- so a caller can
|
|
46
|
+
append its own `[[npc]]`/`[[prop]]` placements. ``spawn_z`` defaults to the arena centre."""
|
|
47
|
+
if spawn_z is None:
|
|
48
|
+
spawn_z = meta["spawn_z"]
|
|
49
|
+
quad = "[" + ", ".join(f"[{x}, {z}]" for x, z in meta["quad"]) + "]"
|
|
50
|
+
return ["[field]", "id = 4003", f'name = "{name}"', "area = 11", "text_block = 1073", "",
|
|
51
|
+
"[camera]", f"pitch = {PITCH}", f"distance = {int(DIST)}", f"fov = {FOV}",
|
|
52
|
+
f"range = [{meta['range_w']}, 448]", "window_width = 384", "[camera.scroll]", "enabled = true", "",
|
|
53
|
+
"[walkmesh]", f"quad = {quad}", 'frame = "world"', "",
|
|
54
|
+
"[[layers]]", f'image = "{art_prefix}/back.png"', "z = 4000",
|
|
55
|
+
"[[layers]]", f'image = "{art_prefix}/floor.png"', "z = 3000", "",
|
|
56
|
+
"[player]", f"spawn = [0, {spawn_z}]", ""]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def arena_toml(meta: dict, *, name="ARENA", art_prefix="art") -> str:
|
|
60
|
+
"""The standalone debug-arena field.toml (player at the centre)."""
|
|
61
|
+
lines = ["# Big flat scrolling checkerboard ARENA -- a debug stage for staging large objects (auto-generated)."]
|
|
62
|
+
lines += arena_scene_lines(meta, name=name, art_prefix=art_prefix)
|
|
63
|
+
return "\n".join(lines)
|
ff9mapkit/scene/bgart.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offline rebuild of a field's per-overlay background PNGs from its atlas + .bgs.
|
|
3
|
+
|
|
4
|
+
This reproduces Memoria's ``[Export] Field=1`` dump (``FieldSceneExporter.ExportOverlay``,
|
|
5
|
+
FieldSceneExporter.cs:177) WITHOUT the in-game step. The engine export is a single blocking
|
|
6
|
+
startup pass over EVERY field (``SceneDirector.MemoriaExport`` -> ``FieldSceneExporter.ExportSafe``
|
|
7
|
+
walks ``mapList.txt``), which is the multi-minute "hang"; here each overlay is composited straight
|
|
8
|
+
from the atlas the kit already reads out of p0data.
|
|
9
|
+
|
|
10
|
+
Byte-exactness (proven on FBG_N00_TSHP_MAP001_TH_CGR_0, overlay 0): cropping the cell this module
|
|
11
|
+
computes out of the engine's OWN dumped ``atlas.png`` reproduces ``Overlay0.png`` with diff == 0 --
|
|
12
|
+
the cell math, the (absent) flip, and the placement are exact. The only delta on a live install
|
|
13
|
+
comes from re-DECODING the p0data atlas offline: a DXT-compressed atlas (Moguri ships the field
|
|
14
|
+
atlas as DXT5 / TextureFormat 12) decodes a hair differently through UnityPy than through Unity's
|
|
15
|
+
runtime, a uniform sub-2/255 per-channel noise that is imperceptible and structurally irrelevant
|
|
16
|
+
(every tile's position/size/index is identical). An uncompressed (vanilla) atlas decodes exactly.
|
|
17
|
+
|
|
18
|
+
The engine packs the upscaled atlas as a grid of ``(TileSize+4)`` cells (a 2px bleed margin each
|
|
19
|
+
side) addressed by a single global sprite index across overlays in order; ``bgs.resolve_sprites``
|
|
20
|
+
already computes that cell (``2 + idx % cpr * (TileSize+4)``, matching ``ExtractSpriteData``,
|
|
21
|
+
BGSCENE_DEF.cs:740). This module is the inverse blit: crop each cell, paste it into the overlay's
|
|
22
|
+
tight canvas at ``(offX*factor, offY*factor)`` (``factor = TileSize/16``), exactly as the engine
|
|
23
|
+
does after its double Y-flip nets out.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .bgs import TILE
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def overlay_size(sprites, tile_size: int):
|
|
31
|
+
"""The ``(w, h)`` :func:`assemble_overlay` produces for these sprites at ``tile_size``.
|
|
32
|
+
|
|
33
|
+
The engine sizes a >1-sprite overlay to its tight tile bbox; a 0/1-sprite overlay is a single
|
|
34
|
+
SPRITE_W x SPRITE_H tile (FieldSceneExporter.cs:184-191). Factored out so the repaint repack can
|
|
35
|
+
validate a hand-edited ``Overlay{i}.png`` against the exact dims its tiles were exported at."""
|
|
36
|
+
factor = tile_size // TILE
|
|
37
|
+
if len(sprites) > 1:
|
|
38
|
+
return (max(s.offX for s in sprites) * factor + tile_size,
|
|
39
|
+
max(s.offY for s in sprites) * factor + tile_size)
|
|
40
|
+
return (tile_size, tile_size)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def assemble_overlay(atlas_img, sprites, tile_size: int):
|
|
44
|
+
"""One overlay's ``Overlay{i}.png`` as a PIL RGBA image, matching ``FieldSceneExporter.ExportOverlay``.
|
|
45
|
+
|
|
46
|
+
``sprites`` are the overlay's resolved tile-sprites (``bgs.resolve_sprites`` against
|
|
47
|
+
``atlas_img.width`` at ``tile_size``), each carrying ``atlasX``/``atlasY`` (the atlas source cell)
|
|
48
|
+
and ``offX``/``offY`` (the tile's 16px-unit position within the overlay). ``tile_size`` is the
|
|
49
|
+
ACTIVE field-map TileSize (vanilla 32 / Moguri 64) -- it MUST match the atlas the cells were
|
|
50
|
+
resolved against, else the grid steps land on the wrong cells (garbled art).
|
|
51
|
+
"""
|
|
52
|
+
from PIL import Image # noqa: PLC0415 - only the art path needs PIL
|
|
53
|
+
|
|
54
|
+
factor = tile_size // TILE
|
|
55
|
+
w, h = overlay_size(sprites, tile_size)
|
|
56
|
+
# the engine inits the canvas to Color(1,1,1,0) == white-under-zero-alpha (FieldSceneExporter.cs:215),
|
|
57
|
+
# NOT black; regions no tile covers keep it. Match it for byte-parity (the RGB is invisible at a=0, but
|
|
58
|
+
# the engine export carries it, so a black init would diff 255 per channel across every uncovered pixel).
|
|
59
|
+
canvas = Image.new("RGBA", (w, h), (255, 255, 255, 0))
|
|
60
|
+
for s in sprites:
|
|
61
|
+
cell = atlas_img.crop((s.atlasX, s.atlasY, s.atlasX + tile_size, s.atlasY + tile_size))
|
|
62
|
+
# paste (overwrite, incl. alpha) == the engine's SetPixels; co-located sprites = last wins.
|
|
63
|
+
canvas.paste(cell, (s.offX * factor, s.offY * factor))
|
|
64
|
+
return canvas
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def assemble_overlays(atlas_img, overlays, tile_size: int) -> dict:
|
|
68
|
+
"""``{overlay_index: PIL RGBA image}`` for every overlay (the offline ``Overlay{i}.png`` set).
|
|
69
|
+
|
|
70
|
+
``overlays`` must already be sprite-resolved (``bgs.resolve_sprites(data, overlays, atlas_img.width,
|
|
71
|
+
tile_size)``) so each carries its atlas cells. Index ``i`` is the overlay list index -- the same
|
|
72
|
+
``i`` the engine names ``Overlay{i}.png`` by and that ``extract.compose_background``/``extract_layers``
|
|
73
|
+
key off.
|
|
74
|
+
"""
|
|
75
|
+
return {i: assemble_overlay(atlas_img, ov.sprites, tile_size) for i, ov in enumerate(overlays)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _bleed_cell(atlas_img, ax, ay, ts: int, pad: int = 2) -> None:
|
|
79
|
+
"""Replicate a freshly-written cell's edge pixels ``pad`` px outward into the surrounding atlas
|
|
80
|
+
margin (clamped to the atlas). The native upscaled render path point-samples with a -0.5 texel UV
|
|
81
|
+
shift (BGSCENE_DEF.cs:1322), so when the background is drawn LARGER than the atlas tile (a hi-res
|
|
82
|
+
display), the first/last screen pixel of a tile reads ONE texel into the 2px margin. The shipped
|
|
83
|
+
atlas reserves that margin with a bled copy of the edge for exactly this reason; a repainted tile
|
|
84
|
+
must re-bleed so its edge -- not the stale old tile -- is what bleeds in (else a 1px seam shows)."""
|
|
85
|
+
W, H = atlas_img.size
|
|
86
|
+
x0, x1 = max(0, ax - pad), min(W, ax + ts + pad) # widened, clamped span for top/bottom
|
|
87
|
+
for d in range(1, pad + 1): # left/right columns within the cell rows
|
|
88
|
+
if ax - d >= 0:
|
|
89
|
+
atlas_img.paste(atlas_img.crop((ax, ay, ax + 1, ay + ts)), (ax - d, ay))
|
|
90
|
+
if ax + ts - 1 + d < W:
|
|
91
|
+
atlas_img.paste(atlas_img.crop((ax + ts - 1, ay, ax + ts, ay + ts)), (ax + ts - 1 + d, ay))
|
|
92
|
+
for d in range(1, pad + 1): # top/bottom rows over the widened span -> corners
|
|
93
|
+
if ay - d >= 0:
|
|
94
|
+
atlas_img.paste(atlas_img.crop((x0, ay, x1, ay + 1)), (x0, ay - d))
|
|
95
|
+
if ay + ts - 1 + d < H:
|
|
96
|
+
atlas_img.paste(atlas_img.crop((x0, ay + ts - 1, x1, ay + ts)), (x0, ay + ts - 1 + d))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def repack_overlay(atlas_img, overlay_png, sprites, tile_size: int, *, bleed: int = 2) -> int:
|
|
100
|
+
"""Blit a (re)painted ``Overlay{i}.png`` back INTO the atlas -- the inverse of :func:`assemble_overlay`.
|
|
101
|
+
``atlas_img`` is BOTH the base and the target (mutated in place); returns the count of CHANGED cells.
|
|
102
|
+
|
|
103
|
+
For each sprite this crops the same ``tile_size`` square the assembler pasted FROM the atlas (at
|
|
104
|
+
``(offX*factor, offY*factor)`` in the overlay PNG) and -- only if it differs from the atlas cell
|
|
105
|
+
already there -- writes it back to that sprite's atlas cell ``(atlasX, atlasY)`` and re-bleeds the
|
|
106
|
+
cell edge into its margin (:func:`_bleed_cell`). Skipping unchanged cells keeps an UNMODIFIED layer
|
|
107
|
+
a byte-exact no-op (crop/paste are pure copies) AND makes a re-pack idempotent, with no separate
|
|
108
|
+
pristine copy to corrupt -- the atlas itself is the base.
|
|
109
|
+
|
|
110
|
+
CO-LOCATED sprites (two tiles sharing one ``(offX, offY)``) are handled like the assembler's
|
|
111
|
+
in-order paste: only the LAST (visible) owner cell is considered; earlier hidden cells -- which the
|
|
112
|
+
engine never samples -- keep their original atlas bytes.
|
|
113
|
+
|
|
114
|
+
``overlay_png`` MUST be sized exactly :func:`overlay_size` for ``sprites`` at ``tile_size`` (the
|
|
115
|
+
caller reconciles a hand-edited PNG first); a mismatch raises ``ValueError`` rather than garble.
|
|
116
|
+
"""
|
|
117
|
+
if not sprites:
|
|
118
|
+
return 0
|
|
119
|
+
exp = overlay_size(sprites, tile_size)
|
|
120
|
+
if tuple(overlay_png.size) != exp:
|
|
121
|
+
raise ValueError(f"repaint overlay is {tuple(overlay_png.size)}, expected {exp} "
|
|
122
|
+
f"(tile_size {tile_size}); re-export or rescale it to match")
|
|
123
|
+
factor = tile_size // TILE
|
|
124
|
+
owner = {} # (offX, offY) -> last sprite index == the visible tile
|
|
125
|
+
for j, s in enumerate(sprites):
|
|
126
|
+
owner[(s.offX, s.offY)] = j
|
|
127
|
+
changed = 0
|
|
128
|
+
for j, s in enumerate(sprites):
|
|
129
|
+
if owner[(s.offX, s.offY)] != j:
|
|
130
|
+
continue
|
|
131
|
+
x0, y0 = s.offX * factor, s.offY * factor
|
|
132
|
+
cell = overlay_png.crop((x0, y0, x0 + tile_size, y0 + tile_size))
|
|
133
|
+
box = (s.atlasX, s.atlasY, s.atlasX + tile_size, s.atlasY + tile_size)
|
|
134
|
+
if cell.tobytes() == atlas_img.crop(box).tobytes():
|
|
135
|
+
continue # unchanged tile -> leave the cell + its margin byte-exact
|
|
136
|
+
atlas_img.paste(cell, (s.atlasX, s.atlasY)) # overwrite incl. alpha == assemble's source cell
|
|
137
|
+
if bleed:
|
|
138
|
+
_bleed_cell(atlas_img, s.atlasX, s.atlasY, tile_size, bleed)
|
|
139
|
+
changed += 1
|
|
140
|
+
return changed
|