Files
EscapistsReintroduced/tools/generate_map_icons.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

71 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Build original vector site symbols, review PNGs and the game's single atlas.
Requires rsvg-convert and Pillow. Runtime texture import is done in Workbench.
"""
from pathlib import Path
import io
import subprocess
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'art/map-markers'
RUNTIME = ROOT / 'addons/core/UI/Textures/ERR'
VIOLET = '#B277F0'
# All symbols use a 64-unit grid and a generous quiet border for small map sizes.
SYMBOLS = [
('unknown', 'Unknown location', 'mark-question', '<path d="M24 25c0-11 19-11 19 0 0 7-11 7-11 14"/><circle cx="32" cy="47" r="2.6" fill="black" stroke="none"/>'),
('radio', 'Radio station', 'radio', '<path d="M25 49l7-25 7 25M23 49h18M28 40h8M26 46h12M23 20q-6 7 0 14M41 20q6 7 0 14M17 15q-11 12 0 24M47 15q11 12 0 24"/><circle cx="32" cy="22" r="3" fill="black" stroke="none"/>'),
('extraction', 'Extraction pickup', 'helicopters', '<path d="M13 19h34M31 19v8M18 45h30M25 38v7M41 38v7M20 33l-9-3v-7M10 25h6"/><path d="M21 29h19q9 0 11 9H25q-6 0-4-9Z" fill="black" stroke="none"/><path d="M39 30v5h7" stroke="#B277F0" stroke-width="2.5"/>'),
('camp', 'Camp / outpost', 'building', '<path d="M12 48l20-32 20 32ZM29 20l-4-6M35 20l4-6"/><path d="M24 48l8-20 8 20Z" fill="black" stroke="none"/>'),
('supplies', 'Supply depot', 'supplies', '<rect x="16" y="24" width="32" height="25" rx="2"/><path d="M23 24v25M41 24v25M15 29h34M26 24v-7h12v7"/><path d="M29 35h6v7h-6Z" fill="black" stroke="none"/>'),
('mortar', 'Mortar position', 'artilleryFireMission', '<path d="M19 16l6-4 19 30-6 4Z" fill="black" stroke="none"/><path d="M30 29L19 48M31 32l6 16M14 49h31M39 47h11"/>'),
('hospital', 'Field hospital', 'medical', '<path d="M26 15h12v11h11v12H38v11H26V38H15V26h11Z" fill="black" stroke="none"/>'),
('prison', 'Prison', 'prison', '<rect x="17" y="16" width="30" height="33" rx="2"/><path d="M24 17v31M32 17v31M40 17v31M18 27h28M18 39h28"/>'),
('roadblock', 'Roadblock', 'roadblock', '<path d="M16 36v14M48 36v14"/><rect x="12" y="23" width="40" height="13" rx="1"/><path d="M15 34l10-10M29 34l10-10M43 34l7-7"/>'),
('factory', 'Industrial site', 'factory', '<path d="M15 48V31l11-8v8l11-8v25ZM38 48V16h9l2 32ZM15 48h34"/><path d="M20 39h2M29 39h2"/>'),
('minefield', 'Minefield', 'minefield', '<path d="M31 14L11 49h42L33 14Z"/><path d="M32 26v11"/><circle cx="32" cy="43" r="2" fill="black" stroke="none"/>'),
('crash', 'Aircraft wreck', 'crash', '<path d="M34 15l-3 15-16 6v5l16-3 1 10-5 4M38 19l-1 11 12 11M37 41l2 9M19 17l7 6M46 15l-3 7M48 49l5 3"/>'),
]
def svg(symbol):
return f'<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="{VIOLET}"/><g fill="none" stroke="#000000" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">{symbol}</g></svg>\n'
def main():
OUT.mkdir(parents=True, exist_ok=True)
RUNTIME.mkdir(parents=True, exist_ok=True)
atlas = Image.new('RGBA', (512, 512))
sheet = Image.new('RGB', (1200, 1050), '#f2f0f5')
draw = ImageDraw.Draw(sheet)
font_path = '/usr/share/fonts/TTF/DejaVuSans.ttf'
title = ImageFont.truetype(font_path, 32)
label = ImageFont.truetype(font_path, 19)
note = ImageFont.truetype(font_path, 16)
draw.text((40, 26), 'CO10 ESCAPE / MAP MARKERS', font=title, fill='#21152f')
draw.text((40, 74), 'Violet circles · black symbols · transparent outside · actual 36 px samples below', font=note, fill='#544662')
definitions = []
for i, (name, caption, image_name, symbol) in enumerate(SYMBOLS):
source = OUT / (name + '.svg')
source.write_text(svg(symbol))
png = subprocess.check_output(['rsvg-convert', str(source)])
(OUT / (name + '.png')).write_bytes(png)
icon = Image.open(io.BytesIO(png)).convert('RGBA')
x, y = i % 4 * 128, i // 4 * 128
atlas.alpha_composite(icon.resize((128, 128), Image.Resampling.LANCZOS), (x, y))
definitions.append(f' ImageSetDefClass "{image_name}" {{\n Pos {x} {y}\n Size 128 128\n Flags 0 0\n }}')
sx, sy = 40 + i % 4 * 290, 135 + i // 4 * 295
sheet.paste(icon.resize((160, 160), Image.Resampling.LANCZOS), (sx + 50, sy), icon.resize((160, 160), Image.Resampling.LANCZOS))
draw.text((sx + 130, sy + 178), caption, font=label, fill='#21152f', anchor='mt')
small = icon.resize((36, 36), Image.Resampling.LANCZOS)
for j, bg in enumerate(('#d5d3b2', '#303b32')):
px = sx + 82 + j * 55
draw.rectangle((px-4, sy+215, px+39, sy+258), fill=bg)
sheet.paste(small, (px, sy + 219), small)
atlas.save(RUNTIME / 'SiteMarkers.png')
sheet.save(OUT / 'preview.png')
(RUNTIME / 'SiteMarkers.imageset').write_text('ImageSetClass {\n RefSize 512 512\n Textures {\n ImageSetTextureClass {\n screenSize FHD\n size 512 512\n path "{ADEE022D9F06C05A}UI/Textures/ERR/SiteMarkers.edds"\n }\n }\n Images {\n' + '\n'.join(definitions) + '\n }\n}\n')
print(OUT / 'preview.png')
if __name__ == '__main__':
main()