Re-package the aggregate: bundle Ward + ship the policing layer
Fold Ward into the single Institution install as its psychiatric-care layer. package.sh now builds Ward clean and vendors Ward.dll plus its defs and textures alongside the other layers, and the settings shim gains a Ward (psychiatric care) checkbox that writes the shared InstitutionSettings.ward flag. Re-assembling also carries in Justice's now-shipping policing defs (the constable work type and crime thoughts), so the aggregate finally bundles colony crime and the weighed arrest as well. README updated to five layers over one Core with policing shipped and Ward folded in; adds the changelog, the Workshop preview, and the preview generator.
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the Institution suite's Workshop Preview.png images. Pure stdlib -- no Pillow, no CUDA,
|
||||
no AI/diffusion, nothing ripped. Every pixel is drawn by the code below, exactly like the per-repo
|
||||
Tools/make_textures.py sprite generators, so the copyright story stays clean.
|
||||
|
||||
Tools/make_preview.py # run from anywhere; writes into every sibling repo
|
||||
|
||||
Reuses the same from-scratch machinery the texture generators use: a hand-rolled RGBA PNG writer
|
||||
(zlib + struct, see png()) and analytic signed-distance-field shapes supersampled for clean edges
|
||||
(see _raster). Nothing beyond the standard library, so it runs on a box with no image tools at all.
|
||||
|
||||
Writes About/Preview.png (640x360, a safe 16:9 Workshop size) into each of:
|
||||
|
||||
rimworld-institution flagship: a barred cell window over the nature->nurture crime spectrum
|
||||
rimworld-core the invisible engine: a nature x nurture node feeding a 0..1 spectrum bar
|
||||
rimworld-contraband a filed shiv against a brick wall breached by an escape tunnel
|
||||
rimworld-justice a level balance -- the corrections layer weighing what a pawn has done
|
||||
rimworld-gangs a network of pawn tokens, two rival clusters bridged by a contraband line
|
||||
|
||||
They are one SET: shared muted institutional green-grey / tin palette, the same embossed-tin border,
|
||||
the same faint grain, and a shared propensity spectrum ribbon (green -> amber -> red) along the
|
||||
bottom of every image -- the suite's signature, the spectrum every layer reads from.
|
||||
"""
|
||||
|
||||
import os
|
||||
import zlib
|
||||
import struct
|
||||
import math
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # rimworld-institution
|
||||
SUITE = os.path.dirname(REPO) # holds all the sibling repos
|
||||
|
||||
W, H = 640, 360
|
||||
AS = W / H # aspect; everything is normalised by HEIGHT so circles stay round
|
||||
SS = int(os.environ.get("SS", "3"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PNG encoder + supersampled rasteriser (pure stdlib) -- same as make_textures.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def png(path, w, h, pixel_fn):
|
||||
"""Write an 8-bit RGBA PNG from scratch. pixel_fn(x, y) -> (r, g, b, a)."""
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0) # filter byte: 0 = None, once per scanline
|
||||
row = bytearray()
|
||||
for x in range(w):
|
||||
r, g, b, a = pixel_fn(x, y)
|
||||
row += bytes((r & 255, g & 255, b & 255, a & 255))
|
||||
raw += row
|
||||
|
||||
def chunk(typ, data):
|
||||
return (struct.pack(">I", len(data)) + typ + data
|
||||
+ struct.pack(">I", zlib.crc32(typ + data) & 0xffffffff))
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0) # 8-bit depth, colour type 6 = RGBA
|
||||
idat = zlib.compress(bytes(raw), 9)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x89PNG\r\n\x1a\n")
|
||||
f.write(chunk(b"IHDR", ihdr))
|
||||
f.write(chunk(b"IDAT", idat))
|
||||
f.write(chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def _raster(w, h, sample, ss=SS):
|
||||
"""Wrap an analytic sample(fx, fy) -> (r, g, b, a) into a supersampled pixel_fn."""
|
||||
inv = 1.0 / ss
|
||||
n = ss * ss
|
||||
|
||||
def pixel(x, y):
|
||||
pr = pg = pb = pa = 0.0
|
||||
for j in range(ss):
|
||||
fy = y + (j + 0.5) * inv
|
||||
for i in range(ss):
|
||||
r, g, b, a = sample(x + (i + 0.5) * inv, fy)
|
||||
if a:
|
||||
af = a * (1.0 / 255.0)
|
||||
pr += r * af
|
||||
pg += g * af
|
||||
pb += b * af
|
||||
pa += af
|
||||
if pa <= 1e-6:
|
||||
return (0, 0, 0, 0)
|
||||
return (_cb(pr / pa), _cb(pg / pa), _cb(pb / pa), _cb(pa / n * 255.0))
|
||||
|
||||
return pixel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# colour + geometry helpers (normalised coords; u in 0..AS, v in 0..1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cb(x):
|
||||
x = int(x)
|
||||
return 0 if x < 0 else (255 if x > 255 else x)
|
||||
|
||||
|
||||
def _clamp01(x):
|
||||
return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x)
|
||||
|
||||
|
||||
def _shade(c, f):
|
||||
return (_cb(c[0] * f), _cb(c[1] * f), _cb(c[2] * f))
|
||||
|
||||
|
||||
def _mix(a, b, t):
|
||||
return (_cb(a[0] + (b[0] - a[0]) * t),
|
||||
_cb(a[1] + (b[1] - a[1]) * t),
|
||||
_cb(a[2] + (b[2] - a[2]) * t))
|
||||
|
||||
|
||||
def _grain(ix, iy):
|
||||
"""Deterministic faint speckle so fills aren't dead-flat."""
|
||||
n = (ix * 374761393 + iy * 668265263) & 0xffffffff
|
||||
n ^= (n >> 13)
|
||||
n = (n * 1274126177) & 0xffffffff
|
||||
return ((n >> 9) & 7) - 3 # -3..+4
|
||||
|
||||
|
||||
def _grainy(c, u, v):
|
||||
g = _grain(int(u * 300), int(v * 300))
|
||||
return (_cb(c[0] + g), _cb(c[1] + g), _cb(c[2] + g))
|
||||
|
||||
|
||||
def _seg(px, py, ax, ay, bx, by):
|
||||
"""Distance from P to segment AB, and the clamped projection param t in [0, 1]."""
|
||||
dx, dy = bx - ax, by - ay
|
||||
l2 = dx * dx + dy * dy
|
||||
if l2 < 1e-12:
|
||||
return math.hypot(px - ax, py - ay), 0.0
|
||||
t = ((px - ax) * dx + (py - ay) * dy) / l2
|
||||
t = 0.0 if t < 0 else (1.0 if t > 1 else t)
|
||||
cx, cy = ax + dx * t, ay + dy * t
|
||||
return math.hypot(px - cx, py - cy), t
|
||||
|
||||
|
||||
def _ellipse(u, v, cx, cy, rx, ry):
|
||||
"""Approximate signed distance to an ellipse. Negative inside."""
|
||||
k = math.hypot((u - cx) / rx, (v - cy) / ry)
|
||||
return (k - 1.0) * min(rx, ry)
|
||||
|
||||
|
||||
def _rrect(u, v, cx, cy, hw, hh, r):
|
||||
"""Signed distance to a rounded rectangle. Negative inside."""
|
||||
qx = abs(u - cx) - (hw - r)
|
||||
qy = abs(v - cy) - (hh - r)
|
||||
ox = qx if qx > 0 else 0.0
|
||||
oy = qy if qy > 0 else 0.0
|
||||
return math.hypot(ox, oy) + min(max(qx, qy), 0.0) - r
|
||||
|
||||
|
||||
def _convex(u, v, pts):
|
||||
"""Signed distance to a convex polygon (negative inside). Winding-agnostic."""
|
||||
n = len(pts)
|
||||
cxp = sum(p[0] for p in pts) / n
|
||||
cyp = sum(p[1] for p in pts) / n
|
||||
dmax = -1e9
|
||||
for i in range(n):
|
||||
ax, ay = pts[i]
|
||||
bx, by = pts[(i + 1) % n]
|
||||
ex, ey = bx - ax, by - ay
|
||||
L = math.hypot(ex, ey) or 1e-9
|
||||
nx, ny = ey / L, -ex / L # a normal to the edge
|
||||
dp = (u - ax) * nx + (v - ay) * ny
|
||||
dc = (cxp - ax) * nx + (cyp - ay) * ny # which side the interior is on
|
||||
if dc > 0:
|
||||
dp = -dp
|
||||
if dp > dmax:
|
||||
dmax = dp
|
||||
return dmax
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# shared palette -- muted institutional green-grey / tin, dark outline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
OUTLINE = (22, 26, 20)
|
||||
BG_TOP = (56, 64, 55)
|
||||
BG_BOT = (31, 37, 31)
|
||||
|
||||
TIN = (138, 142, 126)
|
||||
TIN_HI = (178, 182, 162)
|
||||
TIN_DK = (92, 96, 82)
|
||||
TIN_DDK = (62, 66, 56)
|
||||
|
||||
STEEL = (180, 184, 178)
|
||||
STEEL_HI = (210, 214, 208)
|
||||
STEEL_DK = (120, 124, 118)
|
||||
|
||||
BRICK = (94, 98, 88)
|
||||
BRICK2 = (110, 114, 102)
|
||||
MORTAR = (54, 58, 50)
|
||||
EARTH = (46, 39, 29)
|
||||
EARTH_DK = (24, 20, 14)
|
||||
|
||||
# the propensity spectrum: calm green (low) -> amber -> dangerous red (high)
|
||||
SPEC_LOW = (104, 150, 100)
|
||||
SPEC_MID = (198, 160, 74)
|
||||
SPEC_HI = (176, 68, 54)
|
||||
|
||||
|
||||
def spec(t):
|
||||
"""Colour along the nature->nurture propensity spectrum, t in 0..1."""
|
||||
t = _clamp01(t)
|
||||
if t < 0.5:
|
||||
return _mix(SPEC_LOW, SPEC_MID, t * 2.0)
|
||||
return _mix(SPEC_MID, SPEC_HI, (t - 0.5) * 2.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# shared frame + background + spectrum ribbon (identical on every preview)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CX, CY = AS / 2.0, 0.5
|
||||
FR_M = 0.050 # outer margin
|
||||
FR_R = 0.038 # corner radius
|
||||
FR_W = 0.026 # frame band width
|
||||
FHW, FHH = CX - FR_M, CY - FR_M
|
||||
|
||||
IL, IR = FR_M + FR_W + 0.004, AS - FR_M - FR_W - 0.004 # inner content bounds
|
||||
IT, IB = FR_M + FR_W + 0.004, 1.0 - FR_M - FR_W - 0.004
|
||||
|
||||
RIB_B = IB - 0.006
|
||||
RIB_T = RIB_B - 0.036
|
||||
RIB_L, RIB_R = IL + 0.004, IR - 0.004
|
||||
|
||||
|
||||
def _bg(u, v):
|
||||
c = _mix(BG_TOP, BG_BOT, _clamp01(v))
|
||||
dx, dy = (u - CX) / CX, (v - CY) / CY
|
||||
c = _shade(c, 1.0 - 0.24 * _clamp01(dx * dx + dy * dy))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def _frame(u, v, d):
|
||||
"""Embossed-tin border colour for a point whose rounded-rect distance is d."""
|
||||
if d > -0.004:
|
||||
return OUTLINE # crisp outer edge line
|
||||
p = (-0.004 - d) / (FR_W - 0.004) # 0 outer .. 1 inner
|
||||
if p > 0.90:
|
||||
return OUTLINE # crisp inner edge line
|
||||
nx, ny = (u - CX), (v - CY)
|
||||
lit = -(nx * 0.55 + ny * 0.85) # light from the top-left
|
||||
if lit > 0:
|
||||
c = _mix(TIN, TIN_HI, _clamp01(lit * 2.4))
|
||||
else:
|
||||
c = _mix(TIN, TIN_DDK, _clamp01(-lit * 2.4))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def _ribbon(u, v):
|
||||
t = (u - RIB_L) / (RIB_R - RIB_L)
|
||||
c = _shade(spec(t), 0.86)
|
||||
if v - RIB_T < 0.004:
|
||||
return OUTLINE # dark lintel over the ribbon
|
||||
if RIB_B - v < 0.004:
|
||||
c = _shade(c, 0.62)
|
||||
if (t * 8.0) % 1.0 < 0.035: # eighth-tick divisions
|
||||
c = _shade(c, 0.68)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def preview(motif):
|
||||
"""Compose background + shared ribbon + motif + border into one supersampled pixel_fn."""
|
||||
def sample(fx, fy):
|
||||
u, v = fx / H, fy / H
|
||||
d = _rrect(u, v, CX, CY, FHW, FHH, FR_R)
|
||||
if d > 0.004:
|
||||
return _grainy(_shade(BG_BOT, 0.72), u, v) + (255,) # rounded corner falloff
|
||||
if d > -FR_W:
|
||||
return _frame(u, v, d) + (255,)
|
||||
if RIB_T <= v <= RIB_B and RIB_L <= u <= RIB_R:
|
||||
return _ribbon(u, v) + (255,)
|
||||
m = motif(u, v)
|
||||
if m is not None:
|
||||
return m if len(m) == 4 else (m + (255,))
|
||||
return _bg(u, v) + (255,)
|
||||
|
||||
return _raster(W, H, sample)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# INSTITUTION -- the flagship. A barred cell window; behind the bars the whole
|
||||
# nature->nurture crime spectrum glows. Prison + a spectrum of crime, in one image.
|
||||
# ===========================================================================
|
||||
|
||||
WIN_CX, WIN_CY = CX, 0.45
|
||||
WIN_HW, WIN_HH = 0.62, 0.30
|
||||
NBARS = 6
|
||||
|
||||
|
||||
def m_institution(u, v):
|
||||
d = _rrect(u, v, WIN_CX, WIN_CY, WIN_HW, WIN_HH, 0.028)
|
||||
if d > 0.030:
|
||||
return None
|
||||
if d > -0.006:
|
||||
return OUTLINE # window outer edge
|
||||
if d > -0.036: # tin window frame, bevelled
|
||||
nx, ny = (u - WIN_CX), (v - WIN_CY)
|
||||
lit = -(nx * 0.5 + ny * 0.9)
|
||||
c = _mix(TIN, TIN_HI, _clamp01(lit * 3.0)) if lit > 0 else _mix(TIN, TIN_DK, _clamp01(-lit * 3.0))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
L, R = WIN_CX - WIN_HW, WIN_CX + WIN_HW
|
||||
t = (u - L) / (R - L)
|
||||
|
||||
# bars: NBARS vertical + two horizontal, in front of the spectrum
|
||||
s = (2 * WIN_HW) / NBARS
|
||||
i = int((u - L) / s)
|
||||
xc = L + s * (i + 0.5)
|
||||
dvb = abs(u - xc)
|
||||
bw = 0.016
|
||||
hb = min(abs(v - (WIN_CY - 0.175)), abs(v - (WIN_CY + 0.175)))
|
||||
hh = 0.014
|
||||
|
||||
is_v = dvb < bw
|
||||
is_h = hb < hh
|
||||
if is_v or is_h:
|
||||
vertical = is_v and (not is_h or (dvb / bw) <= (hb / hh))
|
||||
if vertical:
|
||||
if dvb > bw - 0.0035:
|
||||
return OUTLINE
|
||||
p = (u - (xc - bw)) / (2 * bw) # 0 left .. 1 right
|
||||
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
|
||||
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
|
||||
return _grainy(c, u, v)
|
||||
else:
|
||||
if hb > hh - 0.0035:
|
||||
return OUTLINE
|
||||
near = WIN_CY - 0.175 if abs(v - (WIN_CY - 0.175)) < abs(v - (WIN_CY + 0.175)) else WIN_CY + 0.175
|
||||
p = (v - (near - hh)) / (2 * hh)
|
||||
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
|
||||
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# interior: the spectrum, brighter through the middle so it reads as light behind bars
|
||||
base = spec(t)
|
||||
gy = 1.0 - _clamp01(abs(v - WIN_CY) / WIN_HH)
|
||||
base = _shade(base, 0.58 + 0.55 * gy)
|
||||
if d > -0.020: # inner shadow just inside the frame
|
||||
base = _shade(base, 0.80)
|
||||
return _grainy(base, u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CORE -- the invisible foundation. Two faint axes, NATURE (vertical) x NURTURE
|
||||
# (horizontal), meet at the engine node, which drives the 0..1 spectrum bar below.
|
||||
# Deliberately subdued: on its own it changes nothing you can see.
|
||||
# ===========================================================================
|
||||
|
||||
NODE_X, NODE_Y = CX, 0.38
|
||||
BAR_Y, BAR_HW, BAR_HH = 0.60, 0.62, 0.052
|
||||
|
||||
|
||||
def m_core(u, v):
|
||||
# 0..1 spectrum bar (the hero, low and central)
|
||||
db = _rrect(u, v, CX, BAR_Y, BAR_HW, BAR_HH, 0.024)
|
||||
if db <= 0.005:
|
||||
if db > -0.005:
|
||||
return OUTLINE
|
||||
t = (u - (CX - BAR_HW)) / (2 * BAR_HW)
|
||||
c = spec(t)
|
||||
if (t * 10.0) % 1.0 < 0.028 and abs(v - BAR_Y) > BAR_HH - 0.016:
|
||||
c = _shade(c, 0.6) # scale ticks along the rim
|
||||
if abs(v - (BAR_Y - BAR_HH * 0.45)) < 0.006:
|
||||
c = _mix(c, (255, 255, 255), 0.12) # a faint sheen line
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# engine node at the crossing
|
||||
dn = math.hypot(u - NODE_X, v - NODE_Y)
|
||||
if dn < 0.058:
|
||||
if dn > 0.050:
|
||||
return OUTLINE
|
||||
c = _mix(TIN, STEEL_HI, _clamp01(1.0 - dn / 0.050))
|
||||
return _grainy(c, u, v)
|
||||
|
||||
# faint NATURE x NURTURE axes behind everything
|
||||
if abs(u - NODE_X) < 0.0035 and 0.14 < v < 0.62:
|
||||
return _grainy(TIN_DK, u, v)
|
||||
if abs(v - NODE_Y) < 0.0035 and (CX - 0.54) < u < (CX + 0.54):
|
||||
return _grainy(TIN_DK, u, v)
|
||||
# end caps on the axes -> small pips (nature high/low, nurture low/high)
|
||||
for (px, py) in ((NODE_X, 0.15), (NODE_X, 0.61), (CX - 0.54, NODE_Y), (CX + 0.54, NODE_Y)):
|
||||
if math.hypot(u - px, v - py) < 0.016:
|
||||
return _grainy(TIN, u, v)
|
||||
return None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# CONTRABAND -- a filed shiv lying against a brick wall that an escape tunnel has
|
||||
# breached. The physical smuggling loop: a whittled weapon and a hole under the wall.
|
||||
# ===========================================================================
|
||||
|
||||
BX, BY, BRX, BRY = CX + 0.44, 0.54, 0.28, 0.25 # the tunnel breach
|
||||
|
||||
|
||||
def _brick(u, v):
|
||||
bh, bw = 0.078, 0.180
|
||||
row = int((v - IT) / bh)
|
||||
x = u + (bw * 0.5 if row % 2 else 0.0)
|
||||
col = int(x / bw)
|
||||
ly = (v - IT) - row * bh
|
||||
lx = x - col * bw
|
||||
if ly < 0.012 or lx < 0.012:
|
||||
return MORTAR
|
||||
h = (row * 928371 + col * 123457) & 255
|
||||
return _shade(_mix(BRICK, BRICK2, h / 255.0), 0.94 + 0.12 * (((h >> 3) & 3) / 3.0))
|
||||
|
||||
|
||||
def _breach(u, v):
|
||||
ang = math.atan2(v - BY, u - BX)
|
||||
wob = 0.028 * math.sin(ang * 7.0 + 0.7) + 0.020 * math.sin(ang * 3.0)
|
||||
k = math.hypot((u - BX) / (BRX + wob), (v - BY) / (BRY + wob))
|
||||
return (k - 1.0) * min(BRX, BRY)
|
||||
|
||||
|
||||
def _shiv(u, v):
|
||||
Bx, By = 0.30, 0.80 # cloth grip low-left
|
||||
Jx, Jy = 0.44, 0.60
|
||||
Tx, Ty = 0.78, 0.24 # filed point high-right
|
||||
db, tb = _seg(u, v, Jx, Jy, Tx, Ty)
|
||||
half = 0.030 * (1.0 - tb) + 0.004 * tb
|
||||
d_blade = db - half
|
||||
dg, tg = _seg(u, v, Bx, By, Jx, Jy)
|
||||
d_grip = dg - 0.040
|
||||
d = min(d_blade, d_grip)
|
||||
if d > 0:
|
||||
return None
|
||||
if d > -0.006:
|
||||
return OUTLINE
|
||||
if d_grip <= d_blade:
|
||||
wind = (110, 110, 104) if (tg * 4.3) % 1.0 < 0.34 else (150, 150, 142)
|
||||
return _grainy(_shade(wind, 0.95 + 0.10 * tg), u, v)
|
||||
bevel = 1.0 - _clamp01(db / max(1e-6, half))
|
||||
c = _mix(STEEL_DK, STEEL, _clamp01(bevel * 1.6))
|
||||
c = _mix(c, STEEL_HI, _clamp01(bevel - 0.55) * 1.4)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
def m_contraband(u, v):
|
||||
s = _shiv(u, v) # the shiv sits on top of the wall
|
||||
if s is not None:
|
||||
return s
|
||||
db = _breach(u, v)
|
||||
if db < 0: # tunnel mouth: dark earth, deeper = darker
|
||||
f = _clamp01(-db / BRY)
|
||||
return _grainy(_mix(EARTH, EARTH_DK, f), u, v)
|
||||
if db < 0.020: # broken, crumbling brick edge
|
||||
return _grainy(_shade(MORTAR, 0.7), u, v)
|
||||
return _grainy(_brick(u, v), u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# JUSTICE -- a level balance. Corrections weighing what a pawn has actually done:
|
||||
# classification, deterrence, discipline. A gavel-less balance, held even.
|
||||
# ===========================================================================
|
||||
|
||||
J_BEAM_Y = 0.28
|
||||
J_BEAM_HW = 0.40
|
||||
J_BASE_Y = 0.74
|
||||
J_PAN_Y = 0.54
|
||||
|
||||
|
||||
def _pan(u, v, px):
|
||||
d = _ellipse(u, v, px, J_PAN_Y, 0.15, 0.058)
|
||||
if v < J_PAN_Y: # keep the lower half -> a shallow bowl
|
||||
d = max(d, (J_PAN_Y - v))
|
||||
return d
|
||||
|
||||
|
||||
def m_justice(u, v):
|
||||
OW = 0.006
|
||||
d_post = _rrect(u, v, CX, (J_BASE_Y + J_BEAM_Y) / 2.0, 0.018, (J_BASE_Y - J_BEAM_Y) / 2.0, 0.006)
|
||||
d_base = min(_rrect(u, v, CX, J_BASE_Y, 0.150, 0.026, 0.012),
|
||||
_convex(u, v, [(CX - 0.055, J_BASE_Y - 0.055), (CX + 0.055, J_BASE_Y - 0.055),
|
||||
(CX + 0.100, J_BASE_Y - 0.020), (CX - 0.100, J_BASE_Y - 0.020)]))
|
||||
d_beam = _rrect(u, v, CX, J_BEAM_Y, J_BEAM_HW, 0.015, 0.008)
|
||||
d_fulc = _convex(u, v, [(CX, J_BEAM_Y - 0.055), (CX + 0.052, J_BEAM_Y),
|
||||
(CX - 0.052, J_BEAM_Y)])
|
||||
d_panL = _pan(u, v, CX - J_BEAM_HW)
|
||||
d_panR = _pan(u, v, CX + J_BEAM_HW)
|
||||
|
||||
d_solid = min(d_post, d_base, d_beam, d_fulc, d_panL, d_panR)
|
||||
|
||||
# hangers: a thin line from each beam end down to its pan
|
||||
dcl, _ = _seg(u, v, CX - J_BEAM_HW, J_BEAM_Y, CX - J_BEAM_HW, J_PAN_Y - 0.05)
|
||||
dcr, _ = _seg(u, v, CX + J_BEAM_HW, J_BEAM_Y, CX + J_BEAM_HW, J_PAN_Y - 0.05)
|
||||
d_chain = min(dcl, dcr) - 0.004
|
||||
|
||||
if d_chain <= 0 and d_chain < d_solid:
|
||||
return _grainy(TIN_DK, u, v) if d_chain > -0.004 else _grainy(TIN, u, v)
|
||||
|
||||
if d_solid > 0:
|
||||
return None
|
||||
if d_solid > -OW:
|
||||
return OUTLINE
|
||||
# tin, lit from the top
|
||||
lit = 1.0 - _clamp01((v - J_BEAM_Y) / (J_BASE_Y - J_BEAM_Y))
|
||||
c = _mix(TIN_DK, TIN, 0.55 + 0.45 * lit)
|
||||
if d_solid <= d_panL or d_solid <= d_panR or d_solid <= d_beam:
|
||||
c = _mix(c, TIN_HI, 0.25 * lit)
|
||||
return _grainy(c, u, v)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# GANGS -- a contraband network. Pawn tokens band into two rival clusters (amber
|
||||
# vs red) that a bright contraband line bridges through a broker in the middle.
|
||||
# ===========================================================================
|
||||
|
||||
GANG_A, GANG_A_HI = (190, 150, 72), (216, 180, 98)
|
||||
GANG_B, GANG_B_HI = (176, 70, 56), (208, 100, 84)
|
||||
CONDUIT = (200, 202, 182)
|
||||
|
||||
NODES = [
|
||||
(CX - 0.50, 0.34, 'A'), (CX - 0.62, 0.50, 'A'), (CX - 0.52, 0.66, 'A'), (CX - 0.36, 0.48, 'A'),
|
||||
(CX, 0.44, 'X'),
|
||||
(CX + 0.36, 0.36, 'B'), (CX + 0.54, 0.50, 'B'), (CX + 0.42, 0.66, 'B'), (CX + 0.24, 0.52, 'B'),
|
||||
]
|
||||
EDGES = [
|
||||
(0, 1, 'A'), (1, 2, 'A'), (0, 3, 'A'), (3, 2, 'A'), (1, 3, 'A'),
|
||||
(5, 6, 'B'), (6, 7, 'B'), (5, 8, 'B'), (8, 7, 'B'), (6, 8, 'B'),
|
||||
(3, 4, 'X'), (4, 8, 'X'),
|
||||
]
|
||||
|
||||
|
||||
def _token(u, v, nx, ny, base, hi):
|
||||
d_head = math.hypot(u - nx, v - (ny - 0.024)) - 0.020
|
||||
d_body = _rrect(u, v, nx, ny + 0.014, 0.032, 0.024, 0.012)
|
||||
d = min(d_head, d_body)
|
||||
if d > 0:
|
||||
return None
|
||||
if d > -0.005:
|
||||
return OUTLINE
|
||||
return _grainy(hi if d_head < d_body else base, u, v)
|
||||
|
||||
|
||||
def m_gangs(u, v):
|
||||
for (nx, ny, g) in NODES: # tokens sit on top of the wires
|
||||
if abs(u - nx) < 0.06 and abs(v - ny) < 0.06:
|
||||
if g == 'A':
|
||||
r = _token(u, v, nx, ny, GANG_A, GANG_A_HI)
|
||||
elif g == 'B':
|
||||
r = _token(u, v, nx, ny, GANG_B, GANG_B_HI)
|
||||
else:
|
||||
r = _token(u, v, nx, ny, TIN, TIN_HI)
|
||||
if r is not None:
|
||||
return r
|
||||
|
||||
best, kind, bt = 1e9, None, 0.0
|
||||
for (i, j, k) in EDGES:
|
||||
ax, ay = NODES[i][0], NODES[i][1]
|
||||
bx, by = NODES[j][0], NODES[j][1]
|
||||
dd, tt = _seg(u, v, ax, ay, bx, by)
|
||||
if dd < best:
|
||||
best, kind, bt = dd, k, tt
|
||||
hw = 0.005
|
||||
if best < hw:
|
||||
if kind == 'X': # the contraband conduit: a bright dashed line
|
||||
return _grainy(CONDUIT, u, v) if int(bt * 24) % 2 == 0 else None
|
||||
col = _shade(GANG_A, 0.62) if kind == 'A' else _shade(GANG_B, 0.62)
|
||||
if best > hw - 0.0018:
|
||||
return _shade(col, 0.7)
|
||||
return _grainy(col, u, v)
|
||||
return None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# verify: decode a PNG we wrote and confirm it is non-trivial (varied pixels)
|
||||
# ===========================================================================
|
||||
|
||||
def verify(path):
|
||||
data = open(path, "rb").read()
|
||||
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"
|
||||
i, idat, w, h = 8, b"", 0, 0
|
||||
while i < len(data):
|
||||
ln = struct.unpack(">I", data[i:i + 4])[0]
|
||||
typ = data[i + 4:i + 8]
|
||||
body = data[i + 8:i + 8 + ln]
|
||||
if typ == b"IHDR":
|
||||
w, h = struct.unpack(">II", body[:8])
|
||||
elif typ == b"IDAT":
|
||||
idat += body
|
||||
i += 12 + ln
|
||||
raw = zlib.decompress(idat) # rows: filter byte 0 + w*4 RGBA bytes
|
||||
stride = w * 4 + 1
|
||||
seen = set()
|
||||
lo = [255, 255, 255]
|
||||
hi = [0, 0, 0]
|
||||
for y in range(0, h, 7): # sparse sample is plenty to prove variety
|
||||
base = y * stride + 1
|
||||
for x in range(0, w, 7):
|
||||
o = base + x * 4
|
||||
px = (raw[o], raw[o + 1], raw[o + 2])
|
||||
seen.add(px)
|
||||
for c in range(3):
|
||||
lo[c] = min(lo[c], px[c])
|
||||
hi[c] = max(hi[c], px[c])
|
||||
spread = max(hi[c] - lo[c] for c in range(3))
|
||||
return {"bytes": len(data), "w": w, "h": h, "distinct": len(seen), "spread": spread}
|
||||
|
||||
|
||||
TARGETS = [
|
||||
("institution", m_institution, "barred cell window over the nature->nurture crime spectrum"),
|
||||
("core", m_core, "nature x nurture engine node driving a 0..1 spectrum bar"),
|
||||
("contraband", m_contraband, "filed shiv against a brick wall breached by an escape tunnel"),
|
||||
("justice", m_justice, "a level balance weighing what a pawn has done"),
|
||||
("gangs", m_gangs, "two rival pawn clusters bridged by a contraband line"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
for name, motif, desc in TARGETS:
|
||||
path = os.path.join(SUITE, "rimworld-" + name, "About", "Preview.png")
|
||||
png(path, W, H, preview(motif))
|
||||
info = verify(path)
|
||||
ok = "OK" if info["distinct"] > 200 and info["spread"] > 80 else "?? CHECK"
|
||||
print(f"{ok} {os.path.relpath(path, SUITE)} "
|
||||
f"{info['w']}x{info['h']} {info['bytes']:,}B "
|
||||
f"distinct~{info['distinct']} spread={info['spread']} -- {desc}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+14
-1
@@ -2,7 +2,7 @@
|
||||
# Assemble the shippable, drop-in "Institution" mod into this repo.
|
||||
#
|
||||
# The Institution suite's code lives in SEPARATE sibling repos (rimworld-core, -contraband, -justice,
|
||||
# -gangs) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
|
||||
# -gangs, -ward) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
|
||||
# they are vendored into ONE mod folder (this repo) with one About.xml and a settings shim that toggles
|
||||
# each layer. This script rebuilds the sibling DLLs clean (no self-test) and copies them + Contraband's
|
||||
# Defs in here, so the repo is a complete installable mod.
|
||||
@@ -19,6 +19,7 @@ CORE="$SIB/rimworld-core"
|
||||
CB="$SIB/rimworld-contraband"
|
||||
JUST="$SIB/rimworld-justice"
|
||||
GANGS="$SIB/rimworld-gangs"
|
||||
WARD="$SIB/rimworld-ward"
|
||||
|
||||
command -v dotnet >/dev/null || { echo "dotnet not on PATH" >&2; exit 1; }
|
||||
|
||||
@@ -27,6 +28,7 @@ dotnet build "$CORE/Source/Core/Core.csproj" -c Release -v q --n
|
||||
dotnet build "$JUST/Source/Justice/Justice.csproj" -c Release -v q --nologo
|
||||
dotnet build "$CB/Source/Contraband/Contraband.csproj" -c Release -v q --nologo
|
||||
dotnet build "$GANGS/Source/Gangs/Gangs.csproj" -c Release -v q --nologo
|
||||
dotnet build "$WARD/Source/Ward/Ward.csproj" -c Release -v q --nologo
|
||||
|
||||
echo "==> vendoring DLLs into Assemblies/"
|
||||
mkdir -p "$HERE/Assemblies"
|
||||
@@ -36,6 +38,7 @@ cp "$CORE/Assemblies/InstitutionCore.dll" "$HERE/Assemblies/"
|
||||
cp "$CB/Assemblies/Contraband.dll" "$HERE/Assemblies/"
|
||||
cp "$JUST/Assemblies/InstitutionJustice.dll" "$HERE/Assemblies/"
|
||||
cp "$GANGS/Assemblies/InstitutionGangs.dll" "$HERE/Assemblies/"
|
||||
cp "$WARD/Assemblies/Ward.dll" "$HERE/Assemblies/"
|
||||
|
||||
echo "==> vendoring the feature content"
|
||||
for d in Defs Patches Languages Textures; do
|
||||
@@ -47,6 +50,16 @@ if [[ -e "$JUST/Defs" ]]; then
|
||||
mkdir -p "$HERE/Defs"
|
||||
cp -r "$JUST/Defs/." "$HERE/Defs/"
|
||||
fi
|
||||
# Ward ships defs (interaction mode, hediffs, treatment station, room role) and its own textures.
|
||||
# All its files are *_Ward.xml or under a Ward/ texture subdir, so nothing collides with the others.
|
||||
if [[ -e "$WARD/Defs" ]]; then
|
||||
mkdir -p "$HERE/Defs"
|
||||
cp -r "$WARD/Defs/." "$HERE/Defs/"
|
||||
fi
|
||||
if [[ -e "$WARD/Textures" ]]; then
|
||||
mkdir -p "$HERE/Textures"
|
||||
cp -r "$WARD/Textures/." "$HERE/Textures/"
|
||||
fi
|
||||
|
||||
echo "==> done. Assemblies:"
|
||||
ls -1 "$HERE/Assemblies/" | sed 's/^/ /'
|
||||
|
||||
Reference in New Issue
Block a user