feat: expand campaign and refine puzzle visuals and controls
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,52 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="590" viewBox="0 0 1000 590">
|
||||
<rect width="1000" height="590" rx="18" fill="#f6f7f9"/>
|
||||
<text x="32" y="42" font-family="sans-serif" font-size="20" fill="#273b50">HEX VIDEO / DECODED PIXEL PALETTE</text>
|
||||
<rect x="32" y="68" width="216" height="68" rx="9" fill="#4787ED" stroke="#273b5022"/>
|
||||
<text x="32" y="156" font-family="monospace" font-size="13" fill="#273b50">tile.blue #4787ED</text>
|
||||
<text x="32" y="175" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="272" y="68" width="216" height="68" rx="9" fill="#7BE637" stroke="#273b5022"/>
|
||||
<text x="272" y="156" font-family="monospace" font-size="13" fill="#273b50">tile.green #7BE637</text>
|
||||
<text x="272" y="175" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="512" y="68" width="216" height="68" rx="9" fill="#E4413F" stroke="#273b5022"/>
|
||||
<text x="512" y="156" font-family="monospace" font-size="13" fill="#273b50">tile.red #E4413F</text>
|
||||
<text x="512" y="175" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="752" y="68" width="216" height="68" rx="9" fill="#F9BE02" stroke="#273b5022"/>
|
||||
<text x="752" y="156" font-family="monospace" font-size="13" fill="#273b50">tile.yellow #F9BE02</text>
|
||||
<text x="752" y="175" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="32" y="193" width="216" height="68" rx="9" fill="#C460FD" stroke="#273b5022"/>
|
||||
<text x="32" y="281" font-family="monospace" font-size="13" fill="#273b50">tile.purple #C460FD</text>
|
||||
<text x="32" y="300" font-family="monospace" font-size="11" fill="#667789">t=114s / decoded ROI median</text>
|
||||
<rect x="272" y="193" width="216" height="68" rx="9" fill="#00C6FE" stroke="#273b5022"/>
|
||||
<text x="272" y="281" font-family="monospace" font-size="13" fill="#273b50">tile.cyan #00C6FE</text>
|
||||
<text x="272" y="300" font-family="monospace" font-size="11" fill="#667789">t=114s / decoded ROI median</text>
|
||||
<rect x="512" y="193" width="216" height="68" rx="9" fill="#FE8A16" stroke="#273b5022"/>
|
||||
<text x="512" y="281" font-family="monospace" font-size="13" fill="#273b50">tile.orange #FE8A16</text>
|
||||
<text x="512" y="300" font-family="monospace" font-size="11" fill="#667789">t=170s / decoded ROI median</text>
|
||||
<rect x="752" y="193" width="216" height="68" rx="9" fill="#F2F2F4" stroke="#273b5022"/>
|
||||
<text x="752" y="281" font-family="monospace" font-size="13" fill="#273b50">play.background #F2F2F4</text>
|
||||
<text x="752" y="300" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="32" y="318" width="216" height="68" rx="9" fill="#A6AAB9" stroke="#273b5022"/>
|
||||
<text x="32" y="406" font-family="monospace" font-size="13" fill="#273b50">board.face #A6AAB9</text>
|
||||
<text x="32" y="425" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="272" y="318" width="216" height="68" rx="9" fill="#B2B4B8" stroke="#273b5022"/>
|
||||
<text x="272" y="406" font-family="monospace" font-size="13" fill="#273b50">hud.surface #B2B4B8</text>
|
||||
<text x="272" y="425" font-family="monospace" font-size="11" fill="#667789">t=130s / decoded ROI median</text>
|
||||
<rect x="512" y="318" width="216" height="68" rx="9" fill="#0C61E8" stroke="#273b5022"/>
|
||||
<text x="512" y="406" font-family="monospace" font-size="13" fill="#273b50">map.top #0C61E8</text>
|
||||
<text x="512" y="425" font-family="monospace" font-size="11" fill="#667789">t=0s / decoded ROI median</text>
|
||||
<rect x="752" y="318" width="216" height="68" rx="9" fill="#40C8FE" stroke="#273b5022"/>
|
||||
<text x="752" y="406" font-family="monospace" font-size="13" fill="#273b50">map.center #40C8FE</text>
|
||||
<text x="752" y="425" font-family="monospace" font-size="11" fill="#667789">t=0s / decoded ROI median</text>
|
||||
<rect x="32" y="443" width="216" height="68" rx="9" fill="#39DB2E" stroke="#273b5022"/>
|
||||
<text x="32" y="531" font-family="monospace" font-size="13" fill="#273b50">action.green #39DB2E</text>
|
||||
<text x="32" y="550" font-family="monospace" font-size="11" fill="#667789">t=0s / decoded ROI median</text>
|
||||
<rect x="272" y="443" width="216" height="68" rx="9" fill="#191D2A" stroke="#273b5022"/>
|
||||
<text x="272" y="531" font-family="monospace" font-size="13" fill="#273b50">modal.scrimComposite #191D2A</text>
|
||||
<text x="272" y="550" font-family="monospace" font-size="11" fill="#667789">t=110s / decoded ROI median</text>
|
||||
<rect x="512" y="443" width="216" height="68" rx="9" fill="#8FB1FD" stroke="#273b5022"/>
|
||||
<text x="512" y="531" font-family="monospace" font-size="13" fill="#273b50">modal.header #8FB1FD</text>
|
||||
<text x="512" y="550" font-family="monospace" font-size="11" fill="#667789">t=110s / decoded ROI median</text>
|
||||
<rect x="752" y="443" width="216" height="68" rx="9" fill="#FFC80C" stroke="#273b5022"/>
|
||||
<text x="752" y="531" font-family="monospace" font-size="13" fill="#273b50">reward.button #FFC80C</text>
|
||||
<text x="752" y="550" font-family="monospace" font-size="11" fill="#667789">t=42s / decoded ROI median</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,107 @@
|
||||
"""Sample decoded reference pixels, not source-game material colors.
|
||||
Run from any directory: python3 sample-colors.py (requires Pillow).
|
||||
Frames are ffmpeg -ss TIME -i examples/hex.mp4 -frames:v 1 TIME.png.
|
||||
ROI uses [left, top, right, bottom], right/bottom exclusive, original 416x960.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from statistics import median
|
||||
import colorsys, json
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
# Saturated face sampling rejects white arrows and deep sidewalls.
|
||||
SAMPLES = [
|
||||
('play.background',130,(25,270,75,295),False),
|
||||
('play.backgroundTop',130,(150,150,250,180),False),
|
||||
('play.ice',130,(210,20,250,40),False),
|
||||
('board.face',130,(108,320,141,341),False),
|
||||
('hud.surface',130,(285,85,325,110),False),
|
||||
('hud.counter',130,(155,80,181,114),False),
|
||||
('hud.number',130,(202,100,213,110),'dark'),
|
||||
('settings.ink',130,(359,88,368,98),'dark'),
|
||||
('tile.blue',130,(216,368,256,415),True),
|
||||
('tile.green',130,(269,464,310,505),True),
|
||||
('tile.red',130,(158,333,201,379),True),
|
||||
('tile.yellow',130,(103,365,147,407),True),
|
||||
('tile.purple',114,(179,602,235,645),True),
|
||||
('tile.cyan',114,(179,309,236,355),True),
|
||||
('tile.orange',170,(345,371,383,415),True),
|
||||
('tile.arrow',114,(201,319,215,350),'light'),
|
||||
('tile.blueSide',130,(224,545,244,551),False),
|
||||
('tile.greenSide',130,(278,518,296,523),False),
|
||||
('tile.redSide',130,(168,390,191,394),False),
|
||||
('tile.purpleSide',114,(189,664,225,669),False),
|
||||
('map.edge',0,(0,300,12,600),False),
|
||||
('map.top',0,(100,10,300,35),False),
|
||||
('map.center',0,(148,350,173,480),False),
|
||||
('map.path',0,(200,340,214,390),False),
|
||||
('map.node',0,(180,425,196,448),False),
|
||||
('map.nodeInk',0,(203,432,214,444),'dark'),
|
||||
('action.green',0,(122,815,143,850),True),
|
||||
('progress.green',0,(181,725,195,745),True),
|
||||
('currency.surface',0,(174,83,196,106),False),
|
||||
('modal.scrimComposite',110,(40,140,100,180),False),
|
||||
('modal.header',110,(40,260,130,283),False),
|
||||
('modal.body',110,(45,361,365,390),False),
|
||||
('modal.footer',110,(42,765,101,806),False),
|
||||
('modal.confirm',110,(136,779,151,795),True),
|
||||
('reward.button',42,(270,752,281,777),True),
|
||||
('reward.gold',42,(191,446,210,458),True),
|
||||
]
|
||||
def pixels_of(image):
|
||||
return image.get_flattened_data() if hasattr(image, 'get_flattened_data') else image.getdata()
|
||||
def hexcolor(rgb):return '#'+''.join(f'{c:02X}' for c in rgb)
|
||||
def lum(rgb):
|
||||
v=[x/255 for x in rgb];v=[x/12.92 if x<=.04045 else ((x+.055)/1.055)**2.4 for x in v]
|
||||
return sum(x*w for x,w in zip(v,[.2126,.7152,.0722]))
|
||||
def contrast(a,b):
|
||||
a,b=sorted([lum(a),lum(b)]);return round((b+.05)/(a+.05),2)
|
||||
results=[]
|
||||
for name,second,roi,mode in SAMPLES:
|
||||
image=Image.open(ROOT/f'{second:03}.png').convert('RGB');pixels=list(pixels_of(image.crop(roi)));selected=[]
|
||||
for p in pixels:
|
||||
h,s,v=colorsys.rgb_to_hsv(*(c/255 for c in p))
|
||||
if mode is True and not(s>.55 and v>.6):continue
|
||||
if mode=='dark' and not(v<.55):continue
|
||||
if mode=='light' and not(min(p)>225):continue
|
||||
selected.append(p)
|
||||
if not selected:raise ValueError(name)
|
||||
rgb=tuple(round(median([p[i] for p in selected])) for i in range(3))
|
||||
h,s,v=colorsys.rgb_to_hsv(*(c/255 for c in rgb))
|
||||
results.append(dict(role=name,second=second,roi=roi,filter=mode,pixels=len(selected),hex=hexcolor(rgb),rgb=rgb,hsv=[round(h*360,1),round(s*100,1),round(v*100,1)],whiteContrast=contrast(rgb,(255,255,255)),darkContrast=contrast(rgb,(39,59,80))))
|
||||
# Area measurements are image-wide HSV pixel counts, not object segmentation.
|
||||
areas=[]
|
||||
for second in [0,14,130,42,110]:
|
||||
im=Image.open(ROOT/f'{second:03}.png').convert('RGB');px=list(pixels_of(im));n=len(px)
|
||||
high=sum(colorsys.rgb_to_hsv(*(c/255 for c in p))[1]>.55 for p in px)
|
||||
gray=sum(colorsys.rgb_to_hsv(*(c/255 for c in p))[1]<.15 for p in px)
|
||||
areas.append(dict(second=second,highSaturationPercent=round(high/n*100,1),lowSaturationPercent=round(gray/n*100,1)))
|
||||
(ROOT/'samples.json').write_text(json.dumps(dict(method='Median of decoded RGB ROI; HSV filters; not original game tokens.',samples=results,areaMeasurements=areas),ensure_ascii=False,indent=2)+'\n')
|
||||
for r in results:print(r['role'],r['hex'],r['hsv'],'white',r['whiteContrast'],'dark',r['darkContrast'])
|
||||
print(areas)
|
||||
# Cross-frame check: coarse board ROI, excludes white arrows / neutral board.
|
||||
# It still includes bevels, moving slabs and occlusions; not a material estimate.
|
||||
hue_bands={'red':[(0,13),(345,360)],'orange':[(16,38)],'yellow':[(40,65)],'green':[(75,135)],'cyan':[(178,202)],'blue':[(205,239)],'purple':[(260,300)]}
|
||||
cross=[]
|
||||
for second in [55,85,114,130,170]:
|
||||
im=Image.open(ROOT/f'{second:03}.png').convert('RGB').crop((20,280,396,690))
|
||||
groups={k:[] for k in hue_bands}
|
||||
for p in pixels_of(im):
|
||||
h,s,v=colorsys.rgb_to_hsv(*(c/255 for c in p));h*=360
|
||||
if s<.5 or v<.65:continue
|
||||
for k,bands in hue_bands.items():
|
||||
if any(lo<=h<hi for lo,hi in bands):groups[k].append(p)
|
||||
for k,px in groups.items():
|
||||
if len(px)<500:continue
|
||||
rgb=tuple(round(median([p[i] for p in px])) for i in range(3))
|
||||
cross.append(dict(second=second,family=k,pixels=len(px),hex=hexcolor(rgb)))
|
||||
data=json.loads((ROOT/'samples.json').read_text());data['crossFrameHueGroups']=cross
|
||||
(ROOT/'samples.json').write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
|
||||
print('Cross-frame hue groups:',cross)
|
||||
# A small source-derived palette illustration; no screenshot is used as game art.
|
||||
from html import escape
|
||||
svg=['<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="590" viewBox="0 0 1000 590">','<rect width="1000" height="590" rx="18" fill="#f6f7f9"/>','<text x="32" y="42" font-family="sans-serif" font-size="20" fill="#273b50">HEX VIDEO / DECODED PIXEL PALETTE</text>']
|
||||
palette=[r for r in results if r['role'].startswith('tile.') and 'Side' not in r['role'] and r['role']!='tile.arrow']+[r for r in results if r['role'] in ['play.background','board.face','hud.surface','map.top','map.center','action.green','reward.button','modal.header','modal.scrimComposite']]
|
||||
for i,r in enumerate(palette):
|
||||
x=32+(i%4)*240;y=68+(i//4)*125
|
||||
svg += [f'<rect x="{x}" y="{y}" width="216" height="68" rx="9" fill="{r["hex"]}" stroke="#273b5022"/>',f'<text x="{x}" y="{y+88}" font-family="monospace" font-size="13" fill="#273b50">{escape(r["role"])} {r["hex"]}</text>',f'<text x="{x}" y="{y+107}" font-family="monospace" font-size="11" fill="#667789">t={r["second"]}s / decoded ROI median</text>']
|
||||
svg.append('</svg>');(ROOT/'palette.svg').write_text('\n'.join(svg))
|
||||