Files
Jeffie 1aba69c72d Localization: pictures of text per language, with shadows you can set
Some of the game's words are textures, not messages: the area name cards on
the loading screen, the dungeon floor banners and the bosses' names. The disc
has one picture of each per language, and the TrueType font never touched
them.

The port now takes a language's pictures from
lang/textures/<language>/<name>.png (the save folder's before the
executable's), so they follow game.language like the messages do. A picture
is scaled to its texture and left unused if its shape is wrong. The hook sits
at the two texture decode sites and at the loading screen, whose area card is
one texture called "maptitle", so it is named by its file (mt01...).

The pictures are not shipped, since they are made from the disc's art.
tools/textpack/build.py draws them on the player's machine from the words in
tools/textpack/strings/*.json: it erases the old lettering from the disc's
own textures and letters the language's words in the message font (the area
cards and floor labels are that face, squeezed or embossed). The bosses'
names are a serif, for which Cinzel (SIL OFL) stands in.

The pictures carry no shadow; the port casts it, so it follows the Options
sliders: Area Name Shadow, Floor Label Shadow and Boss Name Shadow, next to
Text Shadow and Symbol Shadow. A change shows from the next area that loads
the picture.
2026-10-08 04:29:58 -05:00

54 lines
2.0 KiB
Python

"""Previews the port's picture shadow (LocalizeCastShadow in port/src/localize_texture.cpp) the way the game casts it.
python shadow_preview.py <folder with the pictures> <name> <area|floor|boss> <out.png>
Writes the picture at 0, 25, 50 and 100 percent over a grey backdrop, one under the other.
"""
import os
import sys
import cv2
import numpy as np
from PIL import Image
OFFSET = {'area': 5.6, 'floor': 3.1, 'boss': 3.6} # texture pixels at 50%, as in localize_texture.cpp
NATIVE_W = {'area': 384, 'floor': 384, 'boss': 256}
def cast(rgba, mask, percent, offset):
if percent <= 0:
return rgba
percent = min(percent, 100)
opacity = 1.0 - np.exp(-percent / 30.0)
move = offset * (0.85 + 0.3 * percent / 100)
sigma = max(0.6, 0.42 * move)
h, w = mask.shape
plane = cv2.warpAffine(mask.astype(np.float32), np.float32([[1, 0, move], [0, 1, move]]), (w, h), flags=cv2.INTER_LINEAR)
plane = cv2.GaussianBlur(plane, (0, 0), sigma)
s = np.minimum(1.0, plane * opacity) * (1 - mask)
a = rgba[..., 3] / 255.
out_a = a + s * (1 - a)
k = np.where(out_a > 0, a * (1 - s * a) / np.maximum(out_a, 1e-6), 0)
res = rgba.astype(np.float32)
res[..., :3] *= k[..., None]
res[..., 3] = out_a * 255
return res.clip(0, 255).astype(np.uint8)
def main(root, name, kind, out):
pic = np.asarray(Image.open(os.path.join(root, name + '.png')).convert('RGBA'))
tp = os.path.join(root, name + '_text.png')
mask = (np.asarray(Image.open(tp).convert('L')) / 255.).astype(np.float32) if os.path.exists(tp) else pic[..., 3] / 255.
offset = OFFSET[kind] * pic.shape[1] / NATIVE_W[kind]
rows = []
for pct in (0, 25, 50, 100):
r = cast(pic, mask, pct, offset)
bg = Image.new('RGBA', (r.shape[1], r.shape[0]), (74, 72, 80, 255))
bg.alpha_composite(Image.fromarray(r))
rows.append(np.asarray(bg.convert('RGB')))
Image.fromarray(np.concatenate(rows, 0)).save(out)
if __name__ == '__main__':
main(*sys.argv[1:5])