"""Rebuild chapter-one candidate board; no image mutation."""
from pathlib import Path
import html
import json
import hashlib
from PIL import Image

ROOT = Path(__file__).resolve().parents[1]
ART = ROOT / 'art/chapter1'
SELECTION = json.loads((ROOT/'content/chapter1/art_selection.json').read_text())
CAST = [
 ('fishergirl','Fishergirl / Sorry','12–13 · same body before and after possession','Revised after your feedback: rounder cheeks, a shorter chin, broader nose and centre-parted smooth hair give her an independent identity. Thick black hair and brown eyes follow the book; exact face shape, complexion and tunic colour are design choices.', ['curious','alarm','grief','possessed blank']),
 ('rigga','Rigga','Old · bony · woollen shawl','No wizard costume. Grey tied-back hair and shawl colour are interpretations. Prophetic urgency and bitter grief matter more than a magical glow.', ['bitter','urgent','injured state']),
 ('ganoes_adult','Lieutenant Ganoes Paran','About 19 · seven years after the prologue','Black hair, clean-shaven, broad back, mail hauberk. Review recognition beside his child portrait. Revised broader shoulders, stronger jaw, visible mail and blacker hair. Blood/grime variants are still pending.', ['neutral','stung','shock','tight smile','exhausted']),
 ('lorn','Adjunct Lorn','Young · lithe · long auburn hair','Hair is long, revealed when she removes her helm. Leather armour. Helmeted and sweat-disordered variants remain pending; no glowing sword.', ['controlled','warning','thoughtful','slight smile']),
 ('topper','Topper','Early-thirties appearance · part Tiste Andii','Long light-grey face, dark angled eyes, white hair; green clothes. Many rings belong on future hand gestures. Clothes are now unmistakably green. White hair is explicit in chapter1; its length is not established in the passages checked. Short and shoulder-length candidates are shown as interpretations, not a claim about canon.', ['courteous smile','appraising','furious']),
 ('tavore','Tavore Paran','About 18 · nearly Ganoes’s height','Plain angular features, short reddish hair, pale eyes. Civilian noble tailoring is interpretation. The first portrait may still be too conventionally attractive for the book’s plain, angular description; this is an identity-review point. Do not age her into her later military role.', ['impassive','narrowed eyes','restrained disdain']),
]
ASSETS=[]
def esc(value): return html.escape(str(value), quote=True)
def fig(path, label, kind='portrait'):
 return f'<figure class="{kind}"><a href="{esc(path)}"><img loading="lazy" src="{esc(path)}" alt="{esc(label)}"></a><figcaption>{esc(label)}</figcaption></figure>'

comparison='<section id="scale"><h2>Shared sprite scale</h2><p>Existing prologue sprites beside the new static studies, all at exact 3×. Feet share the same anchor. Different canvas widths are clipped for this display only; source atlases are untouched.</p><div class="assets">'
for path,label,width in [
 ('art/sprites/ganoes_idle_v4.png','Ganoes, 12 · existing',144),
 ('art/chapter1/sprites/fishergirl_turnaround_v1.png','Fishergirl, 12–13 · 44px',576),
 ('art/chapter1/sprites/ganoes_adult_turnaround_v1.png','Ganoes, ~19 · 56px',576),
 ('art/sprites/commander_idle_v4.png','Commander · existing',144)]:
 if path=='art/chapter1/sprites/ganoes_adult_turnaround_v1.png': path=SELECTION['ganoes_adult']['sprites'][0]['file']
 if path=='art/chapter1/sprites/fishergirl_turnaround_v1.png': path=SELECTION['fishergirl']['sprites'][0]['file']
 if (ROOT/path).exists():
  comparison+=f'<figure><div style="width:144px;height:192px;overflow:hidden"><img src="{path}" alt="{esc(label)}" style="width:{width}px;max-width:none"></div><figcaption>{esc(label)}</figcaption></figure>'
comparison+='</div><p class="small">Review point: the new turnarounds are more upright than the existing walking poses. Their overhead angle and adult head size need review together with a future stride drawing, before treating them as animation masters.</p></section>'
cards=[]
for ident,name,tag,note,expressions in CAST:
 portraits=[]
 for entry in SELECTION[ident]['portraits']:
  p=ROOT/entry['file']
  with Image.open(p) as im: size=list(im.size); alpha=im.mode=='RGBA' and im.getextrema()[-1][0]==0
  stem=p.stem
  source_stem=entry['source_stem']
  source=ART/'source'/f'{source_stem}.png'
  prompt=ART/'source'/f'{source_stem}.prompt.txt'
  rel=p.relative_to(ROOT).as_posix()
  ASSETS.append({'character':ident,'role':'dialogue portrait candidate','file':rel,'size':size,'transparent':alpha,'source':source.relative_to(ROOT).as_posix(),'prompt':prompt.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'processing':entry.get('processing','Aseprite crop, uniform nearest-neighbour fit and binary alpha; tools/export_portrait.lua'),'status':'unapproved; not in playable timeline'})
  portraits.append(fig(rel,stem.removeprefix(ident+'_').replace('_',' ')))
  if source.exists() and prompt.exists():
   portraits.append(f'<p class="small"><a href="{source.relative_to(ROOT)}">Generated master</a> · <a href="{prompt.relative_to(ROOT)}">Exact prompt</a></p>')
 sprite=[]
 for entry in SELECTION[ident]['sprites']:
  p=ROOT/entry['file']
  with Image.open(p) as im: size=list(im.size)
  rel=p.relative_to(ROOT).as_posix()
  ASSETS.append({'character':ident,'role':'four-direction static identity study; not an animation','file':rel,'size':size,'source':f'art/chapter1/source/{p.stem}.png','prompt':f'art/chapter1/source/{p.stem}.prompt.txt','status':'unapproved; not in playable timeline'})
  sprite.append(fig(rel,'Static directions: south / west / east / north. Native 48×64 cells.','sprite'))
  sprite.append(fig(rel,'Same sheet at exact 3×. Check face, neck and feet before animation.','sprite enlarged'))
 if ident=='fishergirl' and (ART/'portraits/fishergirl_neutral_v2.png').exists():
  portraits.append('<details><summary>Identity comparison: Ganoes / rejected girl / revised girl</summary><div class="assets">'+fig('art/portraits/ganoes/neutral_black_v2.png','Ganoes: reference for comparison only')+fig('art/chapter1/portraits/fishergirl_neutral_v1.png','Previous girl: rejected resemblance')+fig('art/chapter1/portraits/fishergirl_neutral_v2.png','Revised girl: independent face and hair')+'</div></details>')
 if ident=='ganoes_adult': portraits.insert(0,fig('art/portraits/ganoes/neutral_black_v2.png','Prologue: age 12, existing candidate'))
 cards.append(f'<section id="{ident}"><h2>{esc(name)}</h2><p class="tag">{esc(tag)}</p><p>{esc(note)}</p><div class="assets">'+''.join(portraits or ['<p>Portrait not generated yet.</p>'])+'</div><div class="assets">'+''.join(sprite)+'</div><p class="small">Expression coverage planned: '+esc(', '.join(expressions))+'. Only the images shown above exist in this pass.</p></section>')

payload={'status':'All candidates unapproved. Chapter 1 is preproduction, not a playable chapter.','tool':'Built-in ImageGen; Aseprite native export. Python image use read-only.','art_contract':{'logical_view':[640,360],'display':[1920,1080],'portrait':[160,192],'sprite_cell':[48,64],'foot_anchor':[24,61]},'book':'Complete supplied 01-chapter-1.txt, 891 lines; source-grounded planning in CHAPTER1_ART_PLAN.md','assets':ASSETS,'pending':['Ammanas and Cotillion hood silhouettes','Kan captain, Aragan, house guard portraits','Laseen seated variant with existing Surly identity','Outriders, horses, seven Hounds, birds and crowds','Scene tile kits and action poses','Actual drawn walking/riding/galloping animations','Chapter 1 timeline and exact-dialogue compilation']}
(ROOT/'content/chapter1/art_manifest.json').write_text(json.dumps(payload,indent=2)+'\n')
css='''
:root{color-scheme:dark;font:16px/1.6 system-ui;background:#141b24;color:#e6dfd0}body{max-width:1140px;margin:auto;padding:24px}h1,h2{color:#eed4a4;line-height:1.2}h1{font-size:36px}a{color:#9fcbd9}p{max-width:86ch}.tag,.small{color:#b9bfcb;font-size:14px}nav,.assets{display:flex;gap:18px;flex-wrap:wrap;align-items:start}section{border-top:1px solid #3d4a59;margin-top:32px;padding-top:24px}figure{margin:0;padding:10px;background:#202936;border:1px solid #3d4a59;border-radius:6px}img{display:block;image-rendering:pixelated;max-width:100%;height:auto}.portrait img{width:160px;height:192px;object-fit:contain}.portrait.large img{width:320px;height:384px}.sprite img{width:192px;height:64px;max-width:none}.sprite.enlarged{max-width:100%;overflow-x:auto}.sprite.enlarged img{width:576px;height:192px}figcaption{font-size:13px;max-width:48ch;margin-top:8px}.wide img{width:640px}.callout{padding:14px 20px;border-left:3px solid #d2ad6f;background:#202936}button{background:#344859;color:white;border:1px solid #9bb3c1;border-radius:5px;padding:9px 14px;font:inherit;cursor:pointer}li{margin:7px 0}code{background:#283340;padding:2px 4px}.expression{position:relative;width:320px;height:384px}.expression img{position:absolute;inset:0;width:320px;height:384px}.expression .second{opacity:0;transition:opacity .2s linear}.expression.changed .second{opacity:1}@media(prefers-reduced-motion:reduce){.expression .second{transition:none}}
'''
blend=''
if (ROOT/SELECTION['fishergirl']['portraits'][1]['file']).exists():
 blend='''<section id="expression"><h2>Same girl, changed presence</h2><p>Switch between the open, curious gaze and the heavier-lidded, tired possessed stare. Same clothes, framing and age; no magical eyes. This 0.20-second browser dissolve is a review aid; the Godot shader remains the game implementation.</p><div id="blend" class="expression"><img src="art/chapter1/portraits/fishergirl_neutral_v1.png" alt="Curious fishergirl"><img class="second" src="art/chapter1/portraits/fishergirl_possessed_v2.png" alt="Possessed expression"></div><button id="expression-button" type="button" aria-pressed="false" onclick="const b=document.getElementById('blend');const on=b.classList.toggle('changed');this.setAttribute('aria-pressed',on);this.textContent=on?'Show curious expression':'Show possessed expression'">Show possessed expression</button><p class="small">Aseprite cleanup preserves the neutral hair, clothing, jaw and exact alpha silhouette. Only the generated eye/brow and mouth regions change; no head resizing or warping. The raw expression master is retained in the source links.</p></section>'''
blend=blend.replace('art/chapter1/portraits/fishergirl_neutral_v1.png',SELECTION['fishergirl']['portraits'][0]['file']).replace('art/chapter1/portraits/fishergirl_possessed_v2.png',SELECTION['fishergirl']['portraits'][1]['file'])
arch=''
if (ROOT/'art/previews/prologue/architecture_after_v1.png').exists():
 arch='<section id="prologue"><h2>Prologue: connected tower and quieter stage</h2><p>A separate layout study joins the entrance to the right-hand battlement and removes the isolated stair fragment. Actor positions and the lower wall gaps stay fixed. Floor variation is reduced around the performance area. This layout is now selected in the playable prologue. Doorway routes have been shortened and each departing character disappears independently.</p><div class="assets">'+fig('art/previews/prologue/architecture_before_v1.png','Before: isolated tower prop','wide')+fig('art/previews/prologue/wall_pose_review.png','Current: attached tower, upright soldiers and approved Ganoes tiptoe','wide')+fig('art/previews/prologue/map_coordinates.png','Coordinate review: tower columns 16–19, rows 1–4; actors at current stops','wide')+'</div><p class="small">Built in Godot using existing ImageGen tile/prop assets. The portal joins are approximate: a dedicated tower edge/roof kit is still needed for final environment polish. Chamber B is the selected music direction. The prologue contains dialogue only; narrative beats are staged visually.</p></section>'
page='''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chapter 1 — character candidates</title><style>'''+css+'''</style></head><body><header><p class="tag">PRIVATE LEARNING PROJECT · FEEDBACK REVISION · 18 OCT 2026</p><h1>Chapter 1 — the coast road to Unta</h1><p>New character identities grounded in the supplied book, with the prologue’s pixel-art conventions. Portraits below are actual 160×192 exports. Generated masters are retained for editing, not used as full-resolution gameplay sprites.</p><nav><a href="review.html">Prologue / music review</a><a href="CHAPTER1_ART_PLAN.md">Complete cast, expressions and scene plan</a><a href="content/chapter1/art_manifest.json">Asset manifest</a><a href="#expression">Expression study</a><a href="#prologue">Prologue refinement</a></nav><p class="callout">Chapter 1 is in preproduction. These images are candidates, not an approved cast or a completed animated chapter. The plan covers all eleven scene groups and distinguishes on-screen characters from people merely mentioned.</p><button type="button" onclick="document.querySelectorAll('.portrait').forEach(p=>p.classList.toggle('large'));this.setAttribute('aria-pressed',this.getAttribute('aria-pressed')!=='true')" aria-pressed="false">Toggle portraits 1× / 2×</button></header>'''+comparison+''.join(cards)+blend+arch+'''<section><h2>Next production requirements</h2><ul><li>Approve character identity and native pixel readability before building every direction and emotion.</li><li>Separate baskets, candles, weapons and tack from coherent base bodies; preserve anatomical roots across variants.</li><li>Draw real contact/down/passing/up frames with counter-swing. Static turnarounds and timed offsets are not a completed walk.</li><li>Build coast, village, Gerrom, Warren, palace and estate tile kits; use larger canvases for horses and seven Hounds.</li><li>Reuse Surly’s identity for Laseen; develop hooded gods and supporting cast from the textual plan.</li></ul></section></body></html>'''
(ROOT/'chapter1-review.html').write_text(page)
print(f'Chapter 1: {len(ASSETS)} exported candidate assets indexed.')
