life-of-mellow/index.html

2670 lines
222 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>Life of Mellow</title>
<!-- PWA / installable app -->
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#8db26a">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Mellow">
<meta name="application-name" content="Life of Mellow">
<link rel="apple-touch-icon" href="icon-192.png">
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="icon-512.png">
<style>
:root{
--wood:#8a5f38; --wood2:#a9784a; --woodLt:#c99a5a; --woodDk:#4e341f; --plank:#cB9a63;
--parch:#f0e2ba; --parch2:#e6d29e; --ink:#3a2c1a; --ink2:#7a6244;
--green:#6ab04a; --red:#c65a45; --gold:#e0a53f;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif}
html,body{margin:0;height:100%;overflow:hidden;background:#2c2016;color:var(--ink);
user-select:none;-webkit-user-select:none;touch-action:none}
#game{position:fixed;inset:0}
canvas#cv{position:absolute;inset:0;width:100%;height:100%;display:block}
img.ic{width:22px;height:22px;vertical-align:middle;display:inline-block;pointer-events:none}
img.ic.sm{width:16px;height:16px}
img.ic.lg{width:30px;height:30px}
.wood{background:linear-gradient(180deg,#8a5f38,#6b4a2e);border:3px solid var(--woodDk);border-radius:14px;
box-shadow:0 4px 12px rgba(0,0,0,.4), inset 0 0 0 2px rgba(255,255,255,.10);color:#f3e4c4}
.disc{width:54px;height:54px;border-radius:50%;border:3px solid var(--woodDk);
background:radial-gradient(circle at 50% 35%,#e6cd94,#c99a5a 70%,#9c6f3e);
box-shadow:0 4px 10px rgba(0,0,0,.4),inset 0 0 0 3px rgba(120,80,40,.35);
display:flex;align-items:center;justify-content:center;cursor:pointer;color:#4e341f;font-weight:bold;font-size:12px}
.disc:active{transform:scale(.93)}
.parch{background:
radial-gradient(circle at 30% 20%,rgba(255,255,255,.35),transparent 60%),
linear-gradient(180deg,var(--parch),var(--parch2));
border:5px solid var(--wood);border-radius:16px;
box-shadow:0 8px 22px rgba(0,0,0,.45), inset 0 0 0 3px rgba(200,150,90,.5), inset 0 0 34px rgba(139,95,56,.2)}
/* ---------- HUD ---------- */
#hud{position:absolute;top:0;left:0;right:0;padding:8px;padding-top:calc(8px + env(safe-area-inset-top));display:flex;flex-direction:column;gap:6px;
pointer-events:none;z-index:5;align-items:center}
.signwrap{display:flex;gap:8px;align-items:flex-start;pointer-events:auto}
.plank{position:relative;background:linear-gradient(180deg,#d9b678,#c19a5a);border:3px solid #6b4a2e;
border-radius:8px;padding:7px 14px;font-weight:bold;font-size:16px;color:#4a3316;
box-shadow:0 3px 8px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.4);
display:flex;align-items:center;gap:8px;text-shadow:0 1px 0 rgba(255,255,255,.25)}
.plank:before,.plank:after{content:"";position:absolute;top:-8px;width:4px;height:9px;background:#6b4a2e;border-radius:2px}
.plank:before{left:14px}.plank:after{right:14px}
.seasonTag{position:absolute;left:50%;bottom:-19px;transform:translateX(-50%);background:#c19a5a;
border:2px solid #6b4a2e;border-radius:6px;padding:1px 8px;font-size:10px;color:#4a3316;white-space:nowrap}
.tempSign{background:linear-gradient(180deg,#d9b678,#c19a5a);border:3px solid #6b4a2e;border-radius:8px;
padding:7px 10px;font-weight:bold;font-size:14px;color:#4a3316;display:flex;align-items:center;gap:4px;
box-shadow:0 3px 8px rgba(0,0,0,.35)}
.res-strip{display:flex;gap:6px;overflow-x:auto;padding:5px 9px;scrollbar-width:none;max-width:96vw;margin-top:6px}
.res-strip::-webkit-scrollbar{display:none}
.res{display:flex;align-items:center;gap:3px;font-size:14px;font-weight:bold;white-space:nowrap;
color:#f3e4c4;text-shadow:0 1px 1px rgba(0,0,0,.5)}
.res.low{color:#ff9b7b}
/* edge buttons */
#leftCol{position:absolute;left:calc(8px + env(safe-area-inset-left));top:calc(70px + env(safe-area-inset-top));z-index:5;display:flex;flex-direction:column;gap:9px}
#rightCol{position:absolute;right:8px;bottom:calc(14px + env(safe-area-inset-bottom));z-index:5;display:flex;flex-direction:column;gap:9px}
/* ---------- bottom dock ---------- */
#dock{position:absolute;left:0;right:0;bottom:calc(20px + env(safe-area-inset-bottom));z-index:5;display:flex;flex-direction:column;
align-items:center;gap:7px;padding:6px}
#buildRow{display:flex;gap:6px;overflow-x:auto;max-width:min(96vw,540px);padding:7px;scrollbar-width:none}
#buildRow::-webkit-scrollbar{display:none}
.bbtn{flex:0 0 auto;width:64px;height:72px;border:2px solid var(--woodDk);border-radius:11px;
background:linear-gradient(180deg,#f3e6c4,#e6d29e);
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
font-size:9px;font-weight:bold;color:var(--ink);cursor:pointer;padding:3px;
box-shadow:inset 0 0 0 2px rgba(255,255,255,.35)}
.bbtn:active{transform:scale(.94)}
.bbtn.locked{opacity:.45;filter:grayscale(.5)}
.bbtn .cost{font-size:9px;color:var(--ink2);display:flex;align-items:center;gap:3px;flex-wrap:wrap;justify-content:center}
.bbtn .cost b{display:flex;align-items:center;gap:1px}
#placeBanner{position:absolute;top:96px;left:50%;transform:translateX(-50%);z-index:6;
padding:9px 16px;font-weight:bold;display:none;align-items:center;gap:12px;font-size:14px;color:#f3e4c4}
#placeBanner button{background:var(--parch);color:var(--ink);border:2px solid var(--woodDk);border-radius:10px;
padding:5px 12px;font-weight:bold;cursor:pointer}
/* ---------- overlays ---------- */
.overlay{position:absolute;inset:0;background:rgba(30,20,12,.55);z-index:20;display:none;
align-items:center;justify-content:center;padding:16px}
.card{width:100%;max-width:440px;max-height:88vh;overflow-y:auto;padding:20px}
.card h2{margin:0 0 4px;font-size:23px;display:flex;align-items:center;gap:8px}
.card h3{margin:14px 0 6px;font-size:15px;color:var(--wood)}
.row{display:flex;gap:8px;flex-wrap:wrap}
.btn{background:linear-gradient(180deg,var(--woodLt),var(--wood));color:#fff;border:2px solid var(--woodDk);
border-radius:12px;padding:11px 14px;font-weight:bold;font-size:15px;cursor:pointer;flex:1;min-width:110px;
box-shadow:inset 0 1px 0 rgba(255,255,255,.3);text-shadow:0 1px 1px rgba(0,0,0,.3)}
.btn.sec{background:linear-gradient(180deg,#f0e2ba,#e0ca97);color:var(--ink);text-shadow:none}
.btn.danger{background:linear-gradient(180deg,#d5715a,#b84a37)}
.btn:active{transform:scale(.97)}
.btn.small{padding:8px 11px;font-size:13px;flex:0 0 auto;min-width:0}
.islands{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:8px}
.isle{background:linear-gradient(180deg,#fff4d8,#f1dfb2);border-radius:12px;padding:8px;cursor:pointer;
border:3px solid transparent;text-align:left;box-shadow:inset 0 0 0 1px rgba(139,95,56,.2)}
.isle.sel{border-color:var(--gold);box-shadow:0 0 0 2px var(--gold)}
.isle canvas{width:100%;height:58px;border-radius:8px;display:block;margin-bottom:5px}
.isle b{display:block;font-size:13px}
.isle small{color:var(--ink2);font-size:10.5px;line-height:1.25;display:block;margin-top:1px}
.diffs{display:flex;gap:8px;margin-top:6px}
.diff{flex:1;background:linear-gradient(180deg,#fff4d8,#f1dfb2);border-radius:12px;padding:9px;text-align:center;
cursor:pointer;border:3px solid transparent;font-weight:bold}
.diff.sel{border-color:var(--green);box-shadow:0 0 0 2px var(--green)}
.diff small{display:block;font-weight:normal;color:var(--ink2);font-size:10px;margin-top:2px}
.statbar{height:13px;border-radius:7px;background:rgba(74,56,38,.18);overflow:hidden;margin:2px 0 8px;
box-shadow:inset 0 1px 2px rgba(0,0,0,.2)}
.statbar>i{display:block;height:100%;border-radius:7px}
.jobgrid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:4px}
.jobopt{background:linear-gradient(180deg,#fff4d8,#f1dfb2);border:2px solid transparent;border-radius:10px;
padding:7px 3px;font-size:10.5px;cursor:pointer;text-align:center;font-weight:bold;display:flex;
flex-direction:column;align-items:center;gap:3px}
.jobopt.sel{border-color:var(--gold)}
.mini{font-size:12.5px;color:var(--ink2);margin:3px 0}
.toolPick{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin:7px 0}
.tpick{display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px;cursor:pointer;
border:2px solid transparent;border-radius:11px;background:linear-gradient(180deg,#fff4d8,#f1dfb2);font-size:12px;font-weight:bold;color:var(--ink)}
.tpick.sel{border-color:var(--gold);box-shadow:0 0 0 2px var(--gold)}
.tpick .tcost{font-size:10.5px;color:var(--ink2);font-weight:normal;display:flex;align-items:center;gap:3px}
.close{position:absolute;top:10px;right:14px;font-size:28px;line-height:1;cursor:pointer;color:#f3e4c4;z-index:21;
text-shadow:0 1px 3px #000}
.toast{position:absolute;bottom:120px;left:50%;transform:translateX(-50%);z-index:15;
padding:10px 18px;border-radius:20px;font-size:13px;font-weight:bold;color:#f3e4c4;
opacity:0;transition:opacity .3s;pointer-events:none;max-width:90%;text-align:center}
.toast.show{opacity:1}
.settingsRow{display:flex;justify-content:space-between;align-items:center;padding:9px 0;
border-bottom:1px solid rgba(139,95,56,.25)}
.switch{width:50px;height:28px;border-radius:14px;background:rgba(74,56,38,.3);position:relative;cursor:pointer}
.switch.on{background:var(--green)}
.switch i{position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:#fff;
transition:left .15s;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.switch.on i{left:25px}
.hint{font-size:12.5px;color:var(--ink2);line-height:1.45}
.hint p{margin:6px 0}
/* build button + panel */
#btnBuild{border:3px solid var(--woodDk);border-radius:16px;padding:11px 26px;font-size:17px;font-weight:bold;
color:#f3e4c4;display:flex;align-items:center;gap:8px;cursor:pointer;text-shadow:0 1px 1px rgba(0,0,0,.4);
background:linear-gradient(180deg,#a9784a,#6b4a2e);box-shadow:0 4px 12px rgba(0,0,0,.45),inset 0 1px 0 rgba(255,255,255,.2)}
#btnBuild:active{transform:scale(.95)}
.bldTitle{text-align:center;font-size:20px;font-weight:bold;color:#4a3316;margin:-4px 0 6px;
background:linear-gradient(180deg,#d9b678,#c19a5a);border:3px solid #6b4a2e;border-radius:10px;padding:6px}
.vhead{display:flex;align-items:center;gap:8px;justify-content:space-between;background:linear-gradient(180deg,#d9b678,#c19a5a);border:3px solid #6b4a2e;border-radius:10px;padding:7px 12px;margin-bottom:8px}
.vhead b{font-size:19px;color:#4a3316}
.vhead .vsub{font-size:11px;color:#5a4326;font-weight:bold}
.vlvl{display:inline-block;font-size:12px;font-weight:bold;color:#4a3316;background:#e7cf94;border:2px solid #6b4a2e;border-radius:20px;padding:1px 9px;margin-left:6px;vertical-align:middle}
.vlnext{text-align:center;font-size:12px;color:var(--wood);margin:-2px 0 4px}
.bcard.locked{opacity:.62;filter:saturate(.5)}
.catHead{display:flex;align-items:center;gap:8px;color:var(--wood);font-weight:bold;font-size:13px;margin:12px 2px 4px}
.catHead:before,.catHead:after{content:"";flex:1;height:2px;background:rgba(139,95,56,.3)}
.bcard{display:flex;gap:10px;align-items:center;background:linear-gradient(180deg,#fff6e0,#f2e2b8);
border:2px solid #b98a4a;border-radius:12px;padding:9px;margin:7px 0;box-shadow:inset 0 0 0 1px rgba(255,255,255,.4)}
.bcard .prev{width:50px;height:50px;flex:0 0 auto;background:#e7d3a8;border-radius:9px;
display:flex;align-items:center;justify-content:center;border:2px solid #c69a5a}
.bcard .prev img{width:38px;height:38px}
.binfo{flex:1;min-width:0}.binfo b{font-size:14px;color:var(--ink)}
.bdesc{font-size:11px;color:var(--ink2);line-height:1.28;margin:2px 0 3px}
.bcost{font-size:12.5px;font-weight:bold;color:var(--ink);display:flex;align-items:center;gap:5px;flex-wrap:wrap}
.bcost b{display:flex;align-items:center;gap:1px}
.buildbtn{flex:0 0 auto;align-self:stretch;min-width:64px;background:linear-gradient(180deg,#7bc24a,#5a9a34);
color:#fff;border:2px solid #3f7020;border-radius:10px;padding:8px 12px;font-weight:bold;font-size:14px;cursor:pointer;
box-shadow:inset 0 1px 0 rgba(255,255,255,.45);text-shadow:0 1px 1px rgba(0,0,0,.25)}
.buildbtn:active{transform:scale(.95)}
.buildbtn.no{filter:grayscale(.65);opacity:.55}
/* village tabs + notice + portrait */
.vtabs{display:flex;margin:2px 0 10px;border:2px solid var(--woodDk);border-radius:12px;overflow:hidden}
.vtab{flex:1;padding:10px;text-align:center;font-weight:bold;cursor:pointer;background:#e6d29e;color:var(--ink2);font-size:14px}
.vtab.on{background:linear-gradient(180deg,#8a5f38,#6b4a2e);color:#f3e4c4}
.noticeRow{display:flex;align-items:center;gap:10px;padding:9px 2px;border-bottom:1px solid rgba(139,95,56,.25);font-size:13.5px;color:var(--ink)}
.noticeRow img.nb{width:30px;height:30px;flex:0 0 auto}
.noticeRow .num{color:#6a4fb0;font-weight:bold;font-size:15px}
.pinfo{display:flex;gap:14px;align-items:center;margin:4px 0}
.portrait{width:74px;height:82px;flex:0 0 auto;background:#e7d3a8;border:2px solid #c69a5a;border-radius:10px}
.pmeta{font-size:14px;line-height:1.55}.pmeta b{color:var(--ink)}
.resgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}
.ritem{display:flex;align-items:center;gap:8px;background:linear-gradient(180deg,#fff6e0,#f2e2b8);
border:2px solid #c69a5a;border-radius:10px;padding:8px 11px;font-weight:bold;font-size:15px;color:var(--ink)}
.ritem img{width:26px;height:26px}.ritem b{margin-left:auto}
</style>
</head>
<body>
<div id="game">
<canvas id="cv"></canvas>
<div id="hud">
<div class="signwrap">
<div class="plank" id="seasonBox">
<img class="ic sm" id="seasonIco"><span id="seasonName">Spring</span>
<div class="seasonTag" id="dayNum">1st Season</div>
</div>
<div class="tempSign"><span id="tempTxt">18°</span></div>
<div class="tempSign" id="speedBtn" style="cursor:pointer;min-width:44px;text-align:center">1×</div>
</div>
</div>
<div id="leftCol">
<div class="disc" id="btnSettings">Menu</div>
<div class="disc" id="btnJobs">Village</div>
<div class="disc" id="btnRes"><img class="ic" id="resIco"></div>
<div class="disc" id="btnHelp">?</div>
</div>
<div id="placeBanner" class="wood">
<span id="placeTxt">Tap a tile to place</span>
<button id="placeCancel">Cancel</button>
</div>
<div id="dock">
<button id="btnBuild"><img class="ic lg" id="buildIco"> Build</button>
</div>
<div class="toast wood" id="toast"></div>
<div class="overlay" id="startOverlay" style="display:flex">
<div class="card parch">
<h2>Life of Mellow</h2>
<div class="hint">Guide your Mellows to gather, build, feed, and survive the seasons. Choose a home.</div>
<div class="islands" id="isleGrid"></div>
<h3>Difficulty</h3>
<div class="diffs" id="diffRow"></div>
<div class="row" style="margin-top:14px">
<button class="btn" id="btnStart">Begin</button>
<button class="btn sec" id="btnContinue" style="display:none">Continue</button>
</div>
</div>
</div>
<div class="overlay" id="buildOverlay"><span class="close" id="buildClose">×</span><div class="card parch" id="buildCard"></div></div>
<div class="overlay" id="resOverlay"><span class="close" id="resClose">×</span><div class="card parch" id="resCard"></div></div>
<div class="overlay" id="profOverlay"><span class="close" id="profClose">×</span><div class="card parch" id="profCard"></div></div>
<div class="overlay" id="bldOverlay"><span class="close" id="bldClose">×</span><div class="card parch" id="bldCard"></div></div>
<div class="overlay" id="jobsOverlay"><span class="close" id="jobsClose">×</span><div class="card parch" id="jobsCard"></div></div>
<div class="overlay" id="setOverlay"><span class="close" id="setClose">×</span>
<div class="card parch">
<h2>Menu</h2>
<div class="settingsRow"><span>Animations <small style="color:var(--ink2)">(off = smoother on big villages)</small></span>
<div class="switch on" id="swAnim"><i></i></div></div>
<div class="settingsRow"><span>Sound</span><div class="switch" id="swSound"><i></i></div></div>
<h3>Game</h3>
<div class="row"><button class="btn sec small" id="btnSave">Save</button>
<button class="btn danger small" id="btnQuit">Quit to menu</button></div>
<div class="hint" style="margin-top:10px">Autosaves as you play. Everything stays on this device.</div>
</div>
</div>
<div class="overlay" id="helpOverlay"><span class="close" id="helpClose">×</span>
<div class="card parch">
<h2>How to play</h2>
<div class="hint">
<p><b>Assign jobs.</b> Tap a Mellow → pick a job. Gatherers collect resources and drop them at storage. Service jobs work inside a matching building.</p>
<p><b>Build.</b> Tap a building at the bottom, then tap a tile. A Builder constructs it — keep someone on Builder!</p>
<p><b>Feed everyone.</b> Hunger falls over time — keep food in stock. A Cook turns raw food into meals.</p>
<p><b>Survive winter.</b> Plants stop and it gets cold. Stock <b>firewood</b> (Sawmill) and build a <b>Campfire</b>, or Mellows lose health.</p>
<p><b>Need bubbles</b> pop over Mellows that are hungry, idle, or sick — a quick glance tells you what the village needs.</p>
<p><b>Stuck Mellow?</b> Open its profile → <b>Send home</b>.</p>
</div>
<button class="btn" id="helpOk" style="margin-top:8px">Got it</button>
<button class="btn sec" id="helpTut" style="margin-top:8px;margin-left:6px">Replay tutorial</button>
</div>
</div>
<div class="overlay" id="overOverlay">
<div class="card parch" style="text-align:center">
<h2>Your village has faded…</h2>
<div class="hint" id="overStats"></div>
<div class="hint" style="margin-top:6px">Every Mellow has passed on — but hope endures.</div>
<div class="row" style="margin-top:14px;justify-content:center">
<button class="btn" id="overRestart">Try this island again</button>
<button class="btn sec" id="overMenu">Back to menu</button>
</div>
</div>
</div>
</div>
<script>
"use strict";
/* ================= CONFIG ================= */
const GRID=42, TILE_W=64, TILE_H=32, TILE_TH=7; // 42×42 ≈ 3× the old 24×24 area
const SEASONS=["Spring","Summer","Fall","Winter"];
const SEASON_TEMP={Spring:16,Summer:27,Fall:12,Winter:-4};
const DIFFS={Chill:{season:300,cost:0.7,decay:0.7,label:"relaxed"},
Normal:{season:240,cost:1.0,decay:1.0,label:"balanced"},
Harsh:{season:190,cost:1.3,decay:1.35,label:"long winters"}};
const JOBS={
idle:{name:"Idle",cloak:"#9fb0a0",kind:"idle",tool:"none"},
builder:{name:"Builder",cloak:"#4b86b6",kind:"builder",tool:"hammer"},
lumberjack:{name:"Lumberjack",cloak:"#5fa83e",kind:"gather",node:"tree",res:"wood",tool:"axe"},
miner:{name:"Miner",cloak:"#7d8a92",kind:"gather",node:"rock",res:"stone",tool:"pick"},
ironminer:{name:"Iron Miner",cloak:"#a9683c",kind:"gather",node:"ore",res:"iron",tool:"pick"},
hunter:{name:"Hunter",cloak:"#8a5a2e",kind:"gather",node:"deer",res:"meat",tool:"bow"},
fruit:{name:"Fruit Picker",cloak:"#c65a72",kind:"gather",node:"bush",res:"fruit",tool:"none",seasonal:true},
herbalgather:{name:"Herbalist Gatherer",cloak:"#4f9e4a",kind:"gather",node:"herb",res:"herbs",tool:"none",seasonal:true},
fisher:{name:"Fisherman",cloak:"#3a7fb0",kind:"gather",node:"fish",res:"fish",tool:"rod"},
shepherd:{name:"Shepherd",cloak:"#b8b2a2",kind:"gather",node:"sheep",res:"wool",tool:"staff"},
trainer:{name:"Wolf Trainer",cloak:"#7a6a4a",kind:"trainer",bld:"wolfcentre",tool:"none"},
farmer:{name:"Farmer",cloak:"#c9a13a",kind:"gather",node:"crop",res:"crops",tool:"scythe",seasonal:true},
craftsman:{name:"Craftsman",cloak:"#9c6b3a",kind:"produce",bld:"craft",tool:"hammer"},
herbalist:{name:"Apothecary",cloak:"#8e5cc0",kind:"produce",bld:"herbalist",tool:"none"},
brewer:{name:"Brewer",cloak:"#b1567f",kind:"produce",bld:"brewery",tool:"none"},
cook:{name:"Cook",cloak:"#e0913f",kind:"produce",bld:"kitchen",tool:"none"},
sawyer:{name:"Sawyer",cloak:"#7a5230",kind:"produce",bld:"sawmill",tool:"saw"},
doctor:{name:"Doctor",cloak:"#cf5a5a",kind:"doctor",bld:"medical",tool:"none"},
midwife:{name:"Midwife",cloak:"#d98ab4",kind:"midwife",bld:"nursery",tool:"none"},
teacher:{name:"Teacher",cloak:"#c99a3a",kind:"teacher",bld:"school",tool:"none"},
rancher:{name:"Rancher",cloak:"#8a6b3a",kind:"produce",bld:"ranch",tool:"staff"},
butcher:{name:"Butcher",cloak:"#a35a4a",kind:"produce",bld:"butcher",tool:"none"}};
const BUILD={
house:{name:"Wood House",cost:{wood:20,stone:10},house:2,roof:"#b06a44",icon:"house",cat:"Homes",
desc:"Let the Mellows live in a wood house. Shelters 2, and babies are born here."},
stonehouse:{name:"Stone House",cost:{planks:24,stone:30},house:2,warm:true,roof:"#8a8f98",stone:true,icon:"house",cat:"Homes",
desc:"Warmer than a Wood House — keeps Mellows cosy through winter. Needs planks from a Sawmill."},
storage:{name:"Storage",cost:{wood:25},store:true,roof:"#b98a4a",icon:"storage",cat:"Homes",
desc:"A warehouse to hold gathered resources close to your workers."},
craft:{name:"Craft Shop",cost:{planks:20,iron:8},job:"craftsman",craftTools:true,recipe:{t:3.2},roof:"#7a5230",icon:"hammer",cat:"Artisan",
desc:"A Craftsman makes tools — axe, pickaxe, bow & basket. Open the shop to choose which to craft."},
sawmill:{name:"Sawmill",cost:{wood:30},job:"sawyer",recipe:{in:{wood:2},out:{planks:1,firewood:1},t:2.6},roof:"#9c6b3a",icon:"saw",cat:"Artisan",
desc:"A Sawyer processes raw wood into planks (for advanced buildings) and firewood (for winter)."},
kitchen:{name:"Kitchen",cost:{planks:24,stone:14},job:"cook",recipe:{in:{meat:1,fruit:1},out:{meals:2},t:2.6},roof:"#e0913f",icon:"pot",cat:"Artisan",
desc:"A Cook turns raw food into hearty meals. Needs planks to build."},
herbalist:{name:"Apothecary",cost:{planks:24,iron:8},job:"herbalist",recipe:{in:{herbs:1},out:{potion:1},bonus:{res:"contra",chance:0.34},t:3.2},roof:"#8e5cc0",icon:"flask",cat:"Artisan",
desc:"An Apothecary brews potions to cure sickness, and now and then prepares contraceptive herbs. Needs planks."},
brewery:{name:"Brewery",cost:{planks:24,iron:8},job:"brewer",recipe:{in:{fruit:2},out:{wine:1},t:4},roof:"#b1567f",icon:"wine",cat:"Artisan",
desc:"A Brewer makes wine from fruit to keep Mellows happy. Needs planks."},
farm:{name:"Farm",cost:{wood:20},farm:true,size:2,roof:"#c9a13a",icon:"crops",cat:"Food",
desc:"Pick a crop to grow from your seeds. A Farmer sows 10 seeds in spring and harvests 30 in fall. 2×2 plot."},
pasture:{name:"Pasture",cost:{wood:20},pasture:true,size:2,roof:"#8fae63",icon:"sheep",cat:"Food",
desc:"A Shepherd raises a herd here — pick sheep (wool + meat) or deer (meat + leather). The herd grows each season. 2×2 plot."},
ranch:{name:"Ranch",cost:{planks:20,stone:16},job:"rancher",recipe:{in:{crops:1},out:{milk:2,leather:1},t:3.2},roof:"#9a7a4a",icon:"milk",cat:"Food",
desc:"A Rancher raises cattle — feed them crops (from a Farm) for milk and leather."},
butcher:{name:"Butcher",cost:{planks:24,iron:8},job:"butcher",recipe:{in:{meat:2},out:{meals:2,leather:1},t:2.6},roof:"#a35a4a",icon:"cleaver",cat:"Food",
desc:"A Butcher turns raw meat into meals and leather. Needs planks."},
medical:{name:"Medical Tent",cost:{planks:24,iron:8},job:"doctor",tent:true,roof:"#cf5a5a",icon:"cross",cat:"Care",
desc:"A Doctor treats the sick and injured with potions. Needs planks to build."},
nursery:{name:"Nursery",cost:{planks:24,stone:12},job:"midwife",roof:"#d98ab4",icon:"baby",cat:"Care",
desc:"A Midwife raises the birth rate and cares for babies. Needs planks."},
school:{name:"Schoolhouse",cost:{planks:28,iron:10},job:"teacher",roof:"#c99a3a",icon:"school",cat:"Care",
desc:"A Teacher educates your children. Schooled kids grow into far better skilled workers — doctors, farmers, crafters and more."},
cemetery:{name:"Cemetery",cost:{planks:12,stone:40},cemetery:true,roof:"#8a8f88",icon:"grave",cat:"Care",
desc:"A resting place. Mellows who pass are laid to rest here — their grave markers gather neatly on this plot, and you can read the memorial. Without one, the departed are only remembered in the log."},
campfire:{name:"Campfire",cost:{wood:10},warmth:true,small:true,roof:"#e0913f",icon:"fire",cat:"Care",
desc:"Burns firewood to keep nearby Mellows warm in winter."},
wolfcentre:{name:"Wolf Den",cost:{planks:20,iron:10},job:"trainer",wolfcentre:true,roof:"#6b6b72",icon:"wolf",cat:"Care",
desc:"A Wolf Trainer tames nearby wild wolves (8 meat each) and raises pups. Houses up to 10; families can adopt trained wolves here — they guard the family and lift everyone's spirits."},
dock:{name:"Dock",cost:{wood:15},dock:true,roof:"#6b93c0",icon:"boat",cat:"Water",
desc:"Reaches from land onto the water (2 tiles). Tap a water tile beside land — a home for Fishermen to fish the deep."},
bridge:{name:"Bridge",cost:{wood:12},bridge:true,roof:"#a98a5a",icon:"bridge",cat:"Water",
desc:"Bridge a river to reach resources on the far bank."},
boat:{name:"Boat",cost:{planks:20,stone:5,iron:10},boat:true,roof:"#a06a3e",icon:"boat",cat:"Water",needsCraft:true,
desc:"A little boat, made at the Craft Shop. Place it on interior water to cross rivers & ponds — ferries up to 3 Mellows across."},
flowerbed:{name:"Flower Bed",cost:{wood:3},decor:true,small:true,roof:"#e08aa0",icon:"flower",cat:"Decor",
desc:"A cheerful bed of flowers. A prettier village lifts everyone's spirits a little."},
lantern:{name:"Lantern",cost:{wood:4,stone:2},decor:true,small:true,roof:"#f2c14a",icon:"lantern",cat:"Decor",
desc:"A standing lantern that glows warmly after dark. A cosy touch."},
well:{name:"Stone Well",cost:{stone:12,wood:4},decor:true,small:true,roof:"#9aa0a6",icon:"well",cat:"Decor",
desc:"A classic stone well — a charming centrepiece for the village square."},
bench:{name:"Bench",cost:{wood:8},decor:true,small:true,roof:"#a98a5a",icon:"bench",cat:"Decor",
desc:"A wooden bench to sit and watch the day go by."},
statue:{name:"Statue",cost:{stone:16},decor:true,small:true,roof:"#cfd2d6",icon:"statue",cat:"Decor",
desc:"A little stone statue of a Mellow — the village's pride and joy."}};
const BUILD_ORDER=["house","stonehouse","storage","craft","sawmill","kitchen","herbalist","brewery",
"farm","pasture","ranch","butcher","medical","nursery","school","cemetery","wolfcentre","campfire","dock","bridge",
"flowerbed","lantern","well","bench","statue"]; // boat removed as a placeable — it's a tool/vehicle, not a building (bridges cross water)
const BUILD_CATS=["Homes","Artisan","Food","Care","Water","Decor"];
// Village level is a title/celebration only (no building unlocks) — grander titles as population grows.
const LEVEL_POP=[8,15,25,40,60,90]; // population needed for levels 2..7
const VILLAGE_TITLES=["Settlement","Hamlet","Village","Township","Town","Bustling City","Grand Metropolis"]; // level 1..7
function villageLevel(){const p=(S&&S.mellows)?S.mellows.length:0;let lv=1;for(let i=0;i<LEVEL_POP.length;i++)if(p>=LEVEL_POP[i])lv=i+2;return lv;}
function villageTitle(){return VILLAGE_TITLES[Math.min(VILLAGE_TITLES.length-1,villageLevel()-1)];}
function maxLevel(){return LEVEL_POP.length+1;}
function levelPop(lv){return LEVEL_POP[lv-2]||0;} // pop threshold that unlocks a given level
const VILLAGE_NAMES=["Meadowbrook","Willowdale","Marshmallow Hollow","Puffington","Cloudberry","Bramblewick","Honeydew","Fluffield","Dandelion Green","Sootherby","Pillowvale","Cosywick"];
function randomVillageName(){return VILLAGE_NAMES[(Math.random()*VILLAGE_NAMES.length)|0];}
const BLD_LVL={}; // no building is level-locked — every building is available from the start (gated only by resource cost)
function bldLevel(type){return BLD_LVL[type]||1;} // village level required to build this
function bldLocked(type){return bldLevel(type)>villageLevel();}
const RES_ORDER=["wood","planks","stone","iron","fruit","meat","fish","crops","corn","pumpkin","carrot","cabbage","meals","milk","herbs","potion","contra","wine","firewood","axe","pickaxe","bow","basket","hoe","wool","leather"];
const RES_NAME={contra:"Contraceptive"};
// 6 plantable crops (berries reuse the existing 'fruit' resource; the rest are their own foods).
// Each is a food you can eat, and eating it drops seeds of its own kind.
const CROPS={fruit:{name:"Berries",color:"#c1443e"},crops:{name:"Crops",color:"#e0b84a"},
corn:{name:"Corn",color:"#f2c94c"},pumpkin:{name:"Pumpkin",color:"#e08a3a"},
carrot:{name:"Carrot",color:"#e07a3a"},cabbage:{name:"Cabbage",color:"#7fae54"}};
const CROP_ORDER=["fruit","crops","corn","pumpkin","carrot","cabbage"];
const PLANT_COST=10,PLANT_YIELD=30; // 10 seeds sown → 30 harvested in fall
const DOCK_DIRS=[[1,0],[-1,0],[0,1],[0,-1]]; // land→water offset for a 2-tile dock
function startSeeds(){return {fruit:25,crops:25,corn:25,pumpkin:25,carrot:25,cabbage:25};} // 25 of each seed
// ---- Wolves ---- packs roam ~2 dens; Centres tame/train them; families adopt them
const WOLF_MAX_WILD=16, WOLF_PACK_MAX=6, WOLF_TRAIN_SEASONS=3, WOLF_TAME_MEAT=8,
FAMILY_WOLF_CAP=2, FAMILY_WOLF_TOTAL=3, FACILITY_CAP=10;
// craftable tools (village-wide boosts) — chosen & made at the Craft Shop
const TOOLS={axe:{name:"Axe",cost:{wood:5,stone:5}},pickaxe:{name:"Pickaxe",cost:{wood:5,stone:5}},
bow:{name:"Bow",cost:{wood:8,stone:2}},basket:{name:"Basket",cost:{wood:10}},hoe:{name:"Hoe",cost:{wood:5,stone:5}}};
const TOOL_ORDER=["axe","pickaxe","bow","basket","hoe"];
const JOB_TOOL={lumberjack:"axe",miner:"pickaxe",ironminer:"pickaxe",hunter:"bow",fruit:"basket"};
const FOOD_TYPES=["meals","fruit","meat","crops","corn","pumpkin","carrot","cabbage","fish","milk"];
const NODE={
tree:{res:"wood",max:40,kind:"tree"},rock:{res:"stone",max:40,kind:"rock"},ore:{res:"iron",max:30,kind:"ore"},
bush:{res:"fruit",max:20,kind:"bush",plant:true},herb:{res:"herbs",max:16,kind:"herb",plant:true},
deer:{res:"meat",max:12,kind:"deer",animal:true},sheep:{res:"wool",max:14,kind:"sheep",animal:true},
fish:{res:"fish",max:20,kind:"fish",water:true},crop:{res:"crops",max:0,kind:"crop",plant:true,farmed:true}};
const ISLANDS={
meadow:{name:"Meadow Vale",grass:"#b2c198",grass2:"#acbc8f",side:"#739051",accent:"#abc18d",
blurb:"Gentle & balanced, dotted with ponds and groves.",weights:{tree:24,rock:11,ore:6,bush:16,herb:10,deer:7,sheep:5,fish:7},water:1},
woods:{name:"Whispering Woods",grass:"#a4bc8e",grass2:"#9dba84",side:"#5e8347",accent:"#9bba83",
blurb:"Deep forest laced with streams. Wolves roam.",weights:{tree:40,rock:6,ore:3,bush:14,herb:13,deer:12,sheep:3,fish:6},water:2,wolves:2},
highlands:{name:"Rocky Highlands",grass:"#bec1a0",grass2:"#b7b899",side:"#868c5f",accent:"#bec29b",
blurb:"Stone & iron rich, with cold mountain tarns.",weights:{tree:9,rock:30,ore:18,bush:9,herb:7,deer:5,sheep:7,fish:6},water:1},
coast:{name:"Sunny Coast",grass:"#b7ca9c",grass2:"#afc296",side:"#769d56",accent:"#e0d4ac",
blurb:"Fruit, fish & water galore. Few minerals.",weights:{tree:16,rock:6,ore:3,bush:21,herb:8,deer:4,sheep:4,fish:24},water:3},
desert:{name:"Desert Oasis",grass:"#ddcdaa",grass2:"#d5c39f",side:"#af9a72",accent:"#e4d5ad",
blurb:"Life clusters around precious oasis water.",weights:{tree:8,rock:15,ore:10,bush:7,herb:5,deer:4,sheep:4,fish:8},water:2},
tundra:{name:"Frostpine Tundra",grass:"#d1dadd",grass2:"#c7d2d5",side:"#9ba9af",accent:"#dde6e9",
blurb:"Long, brutal winters and frozen lakes.",weights:{tree:20,rock:12,ore:8,bush:8,herb:7,deer:11,sheep:5,fish:6},water:1,coldland:true,wolves:2},
ashen:{name:"Ashen Isle",grass:"#b3ada8",grass2:"#aba59f",side:"#746d67",accent:"#b7b0aa",
blurb:"Mineral & crop rich, split by rivers to bridge.",weights:{tree:13,rock:24,ore:17,bush:12,herb:9,deer:4,sheep:4,fish:11},water:3}};
/* ================= STATE ================= */
let S=null, cam={x:0,y:0,z:1};
let ui={placing:null,selMel:null,selBld:null,anim:true,sound:false,speed:1,hoverX:0,hoverY:0};
let cv,ctx,DPR=1,VW=0,VH=0,lastT=0;
const NAMES=["Bumble","Pip","Nub","Momo","Tato","Wiggy","Bean","Fig","Doe","Puff","Sprout","Cricket","Ollie","Muddle","Twig","Poppy","Berry","Nib","Waddle","Clove","Pebble","Fern","Otto","Suki","Boop","Marbles","Tuck","Cinnamon","Juniper","Dizzy","Wren","Hobs","Lint","Pudding","Bramble","Moss"];
const FEM_NAMES=["Poppy","Berry","Fern","Suki","Wren","Juniper","Clove","Cinnamon","Grace","Momo","Doe","Pudding","Marbles","Fig"];
const SURNAMES=["Marshby","Puddleton","Fernwick","Thistledown","Barleymoss","Tumblewick","Honeynook","Bramblefoot","Meadowsweet","Cloverleaf","Nettlebrook","Mossbottom","Acornsby","Pinewhistle","Dewberry","Softpaw","Willowby","Sugarmell","Cobbleside","Snugglepot"];
const ELDER_AGE=55;
const TRAITS={
hardy:{name:"Hardy",desc:"Weathers cold, hunger and illness better."},
sickly:{name:"Sickly",desc:"Falls ill more easily and tires faster."},
green:{name:"Green Thumb",desc:"Faster at plants — farming, fruit and herbs."},
bookish:{name:"Bookish",desc:"Learns faster and starts a little smarter."},
cheery:{name:"Cheery",desc:"Stays happier through thick and thin."}};
const TRAIT_KEYS=Object.keys(TRAITS);
function pickTrait(){return TRAIT_KEYS[(Math.random()*TRAIT_KEYS.length)|0];}
// Hobbies: personality flavour shown on the profile. If a hobby has a favoured job, working
// that job gives a small happiness nudge (see updateMellow). The rest are pure flavour.
const HOBBIES={
angler:{label:"loves fishing",emoji:"🎣",job:"fisher"},
gardener:{label:"loves gardening",emoji:"🌱",job:"farmer"},
woodsman:{label:"loves the woods",emoji:"🪵",job:"lumberjack"},
forager:{label:"loves foraging",emoji:"🧺",job:"fruit"},
baker:{label:"loves baking",emoji:"🥧",job:"cook"},
tinkerer:{label:"loves tinkering",emoji:"🔨",job:"craftsman"},
tracker:{label:"loves tracking",emoji:"🏹",job:"hunter"},
herder:{label:"loves herding",emoji:"🐑",job:"shepherd"},
stargazer:{label:"a stargazer",emoji:"✨",job:null},
singer:{label:"loves to sing",emoji:"🎵",job:null},
dreamer:{label:"a daydreamer",emoji:"☁️",job:null},
wanderer:{label:"a wanderer",emoji:"🥾",job:null},
collector:{label:"a collector",emoji:"🐚",job:null},
napper:{label:"loves a good nap",emoji:"😴",job:null}};
const HOBBY_KEYS=Object.keys(HOBBIES);
function pickHobby(){return HOBBY_KEYS[(Math.random()*HOBBY_KEYS.length)|0];}
// Mood: derived live from a Mellow's current state — shown on the profile & as an occasional bubble.
function moodOf(m){
if(m.sick)return{emoji:"🤒",label:"poorly"};
if(m.hunger<25)return{emoji:"😖",label:"hungry"};
if(isWinter()&&m.warmth<40)return{emoji:"🥶",label:"chilly"};
const h=m.happy||0;
if(h>=82)return{emoji:"😄",label:"delighted"};
if(h>=62)return{emoji:"🙂",label:"content"};
if(h>=42)return{emoji:"😐",label:"so-so"};
return{emoji:"😔",label:"glum"};}
function isElder(m){return !isChild(m)&&m.age>=ELDER_AGE;}
const MILESTONES=[
{id:'baby',name:'First Baby',desc:'Welcome a newborn Mellow.',test:s=>s._everBaby},
{id:'winter',name:'Survived a Winter',desc:'Bring the village through its first winter.',test:s=>(s._wintersSurvived||0)>=1},
{id:'pop10',name:'Village of 10',desc:'Grow to 10 Mellows.',test:s=>s.mellows.length>=10},
{id:'pop25',name:'Bustling Town of 25',desc:'Grow to 25 Mellows.',test:s=>s.mellows.length>=25},
{id:'pop50',name:'Thriving Town of 50',desc:'Grow to 50 Mellows.',test:s=>s.mellows.length>=50},
{id:'elder',name:'First Elder',desc:'A Mellow reaches the wise age of 55.',test:s=>s.mellows.some(m=>isElder(m))},
{id:'genius',name:'A Bright Mind',desc:'Raise a Mellow to intelligence 10.',test:s=>s.mellows.some(m=>(m.intel||0)>=10)},
{id:'family',name:'Full House',desc:'Raise a home with three children.',test:s=>s.buildings.some(b=>b.built&&BUILD[b.type].house&&s.mellows.filter(m=>m.home===b.id&&isChild(m)).length>=3)}];
// Goals: a directed, ordered checklist that guides play (distinct from the passive Milestones above).
// Each completed goal gives a small happiness reward to the whole village.
const builtType=(s,t)=>s.buildings.some(b=>b.built&&b.type===t);
const QUESTS=[
{id:'q_job',name:'Put a Mellow to work',hint:'Tap a Mellow and give them a job.',test:s=>s.mellows.some(m=>!isChild(m)&&m.job!=="idle")},
{id:'q_food',name:'Secure a food supply',hint:'Assign a food gatherer (Fruit Picker, Hunter or Fisher), or build a Farm.',test:s=>s.mellows.some(m=>!isChild(m)&&["fruit","hunter","fisher","farmer"].includes(m.job))||builtType(s,'farm')},
{id:'q_store',name:'Build a Storage',hint:'Build a Storage so workers have somewhere to drop resources.',test:s=>builtType(s,'storage')},
{id:'q_home2',name:'Build a second home',hint:'Homes shelter couples — more homes mean more families.',test:s=>s.buildings.filter(b=>b.built&&BUILD[b.type].house).length>=2},
{id:'q_saw',name:'Raise a Sawmill',hint:'A Sawmill makes planks (for advanced buildings) and firewood.',test:s=>builtType(s,'sawmill')},
{id:'q_warm',name:'Prepare for winter',hint:'Build a Campfire or a Stone House to keep Mellows warm.',test:s=>builtType(s,'campfire')||builtType(s,'stonehouse')},
{id:'q_lvl2',name:'Grow to a Hamlet',hint:'Reach 8 Mellows to level the village up to a Hamlet.',test:s=>villageLevel()>=2},
{id:'q_school',name:'Open a Schoolhouse',hint:'A Teacher raises clever children who become skilled workers.',test:s=>builtType(s,'school')},
{id:'q_wolf',name:'Befriend a wolf',hint:'Build a Wolf Den and tame a wolf with meat.',test:s=>(s.wolves||[]).some(w=>w.state==="trained"||w.state==="family")},
{id:'q_pop15',name:'A village of 15',hint:'Grow your village to 15 Mellows.',test:s=>s.mellows.length>=15}];
function questTick(){S.quests=S.quests||[];
for(const q of QUESTS){if(S.quests.indexOf(q.id)>=0)continue;if(q.test(S)){S.quests.push(q.id);
for(const m of S.mellows)m.happy=Math.min(100,m.happy+8);
toast("✅ Goal complete — "+q.name+"!");logEvent("Goal complete: "+q.name+" (+8 happiness to all).");
if(ui.sound)Sound.sfx('milestone');}}}
function newMellow(gx,gy,age){const sex=Math.random()<0.5?"male":"female";
const nm=sex==="female"?FEM_NAMES[(Math.random()*FEM_NAMES.length)|0]:NAMES[(Math.random()*NAMES.length)|0];
const trait=pickTrait();
let intel=age===0?2:(3+(Math.random()*3|0));if(trait==="bookish")intel=Math.min(10,intel+1);
return {id:S.nextId++,name:nm,surname:SURNAMES[(Math.random()*SURNAMES.length)|0],sex,gx,gy,tx:gx,ty:gy,job:"idle",
hunger:80,health:100,happy:70,warmth:100,age:age==null?18+(Math.random()*12|0):age,
sick:false,pregnant:false,pregT:0,contraT:0,intel,trait,hobby:pickHobby(),parents:null,gparents:[],momId:null,dadId:null,
carry:0,carryRes:null,home:null,state:"idle",target:null,timer:0,bob:Math.random()*6,wander:0,face:1};}
function isChild(m){return m.age<12;}
function CHILD_AGE(){return 12;}
const SCHOOL_SEASONS=["Fall","Winter"];
function schoolInSession(){return SCHOOL_SEASONS.indexOf(season())>=0;}
// intelligence 1-10 -> 0..1 skill factor (drives skilled-work speed; basic gathering ignores it)
function skill(m){let v=(((m.intel||3)-1)/9);if(m.trait==="bookish")v=Math.min(1,v+0.08);return v;}
/* ================= WORLDGEN ================= */
function genWorld(islandKey,diffKey){
const isle=ISLANDS[islandKey];
S={island:islandKey,diff:diffKey,nextId:1,day:1,seasonCount:1,tsec:0,seasonIdx:0,seasonT:0,
tiles:[],nodes:[],buildings:[],mellows:[],
res:{wood:50,planks:0,stone:50,iron:0,fruit:50,meat:50,fish:0,crops:0,corn:0,pumpkin:0,carrot:0,cabbage:0,meals:0,milk:0,herbs:0,potion:0,contra:0,wine:0,firewood:0,axe:3,pickaxe:3,bow:3,basket:3,hoe:3,wool:0,leather:0},
seeds:startSeeds(),treeRegrow:0,
log:[],birthT:20,sickT:35,wolves:[],graves:[],memorial:[],quests:[],newcomers:[],_nextNewcomer:3,festYr:0,anim:true,sound:false,villageName:randomVillageName()};
for(let y=0;y<GRID;y++){S.tiles[y]=[];for(let x=0;x<GRID;x++)S.tiles[y][x]="land";}
// organic coastline — carve the outer map into ocean so each island has a natural, varied shape (bays & capes), not a square
{const cx=(GRID-1)/2,cy=(GRID-1)/2,maxR=GRID*0.5;
const a1=Math.random()*6.283,a2=Math.random()*6.283,a3=Math.random()*6.283,a4=Math.random()*6.283;
for(let y=0;y<GRID;y++)for(let x=0;x<GRID;x++){
const dx=x-cx,dy=y-cy,d=Math.hypot(dx,dy),ang=Math.atan2(dy,dx);
const wob=1+0.20*Math.sin(ang*3+a1)+0.13*Math.sin(ang*5+a2)+0.10*Math.sin(ang*2+a3)+0.06*Math.sin(ang*7+a4);
if(d>maxR*0.92*wob)S.tiles[y][x]="water";}}
if(isle.water>=1){
for(let pi=0;pi<isle.water;pi++){
if(pi===0&&isle.water>=2){ // a winding river
let ry=3+(Math.random()*(GRID-6)|0);
for(let x=0;x<GRID;x++){let yy=ry+Math.round(Math.sin(x*0.5+pi)*2.5);
for(let d=0;d<(isle.water>=3?2:1);d++){let py=yy+d;if(py>=0&&py<GRID)S.tiles[py][x]="water";}}
} else { // an organic pond
const cx=2+(Math.random()*(GRID-4)|0),cy=2+(Math.random()*(GRID-4)|0),rad=1.4+Math.random()*1.6;
for(let y=0;y<GRID;y++)for(let x=0;x<GRID;x++)
if(Math.hypot(x-cx,y-cy)<rad+Math.random()*0.7)S.tiles[y][x]="water";}}}
const DENS=Math.max(1,(GRID*GRID)/(18*18));
const w=isle.weights,entries=[];for(const k in w){const n=Math.round(w[k]*DENS);for(let i=0;i<n;i++)entries.push(k);}
const occ={},key=(x,y)=>x+","+y;let tries=entries.length*6,placed=0;
while(placed<entries.length&&tries-->0){const t=entries[placed],x=(Math.random()*GRID)|0,y=(Math.random()*GRID)|0;
const isWater=S.tiles[y][x]==="water",wantWater=NODE[t].water;
if(occ[key(x,y)]){placed++;continue;}
if(wantWater&&!isWater){continue;}if(!wantWater&&isWater){continue;}
occ[key(x,y)]=true;S.nodes.push({gx:x,gy:y,type:t,amount:NODE[t].max,dormant:false,respawn:0,seed:Math.random()});placed++;}
// found the village on the largest landmass (near its centre) so it's never stranded on a small fragment
let sx,sy;{
const seen=[];for(let y=0;y<GRID;y++)seen[y]=new Array(GRID).fill(false);
let best=null;
for(let y=0;y<GRID;y++)for(let x=0;x<GRID;x++){
if(S.tiles[y][x]!=="land"||seen[y][x])continue;
const comp=[],q=[[x,y]];seen[y][x]=true;
while(q.length){const c=q.pop();comp.push(c);
for(const d of [[1,0],[-1,0],[0,1],[0,-1]]){const nx=c[0]+d[0],ny=c[1]+d[1];
if(nx>=0&&ny>=0&&nx<GRID&&ny<GRID&&S.tiles[ny][nx]==="land"&&!seen[ny][nx]){seen[ny][nx]=true;q.push([nx,ny]);}}}
if(!best||comp.length>best.length)best=comp;}
best=best||[[(GRID/2)|0,(GRID/2)|0]];
const mx=best.reduce((a,t)=>a+t[0],0)/best.length, my=best.reduce((a,t)=>a+t[1],0)/best.length;
const fits=([x,y])=>x+1<GRID&&y+1<GRID&&S.tiles[y][x+1]==="land"&&S.tiles[y+1]&&S.tiles[y+1][x]==="land"&&S.tiles[y+1][x+1]==="land";
const cand=best.filter(fits).sort((a,b)=>Math.hypot(a[0]-mx,a[1]-my)-Math.hypot(b[0]-mx,b[1]-my));
[sx,sy]=cand[0]||best[0];}
S.nodes=S.nodes.filter(n=>Math.abs(n.gx-sx)>2||Math.abs(n.gy-sy)>2);
addBuilding("storage",sx,sy,true);addBuilding("house",sx+1,sy,true);
for(let i=0;i<4;i++){const m=newMellow(sx+(i-1.5)*0.55,sy+1);m.sex=i%2?"female":"male";S.mellows.push(m);} // start with 4 founders (2 male, 2 female)
assignHomes();weddingCheck(); // baseline the founder couple silently
const nw=isle.wolves||0;S.dens=[];
if(nw>0){for(let d=0;d<2;d++){let dx,dy,g=0;do{dx=1+(Math.random()*(GRID-2)|0);dy=1+(Math.random()*(GRID-2)|0);g++;}
while(g<80&&(S.tiles[dy][dx]==="water"||Math.abs(dx-sx)+Math.abs(dy-sy)<7));S.dens.push({gx:dx,gy:dy});}
for(let i=0;i<nw;i++){const p=i%2,den=S.dens[p];
S.wolves.push(newWolf(...wolfSpawnPos(den),{state:"wild",packId:p,adult:true}));}}
computeReach();overShown=false;cam.z=1;centerCamera(sx,sy);
}
function addBuilding(type,gx,gy,built){const b={id:S.nextId++,type,gx,gy,built:!!built,prog:built?1:0,phase:0};S.buildings.push(b);return b;}
/* ================= ISO ================= */
function isoX(gx,gy){return (gx-gy)*(TILE_W/2);}
function isoY(gx,gy){return (gx+gy)*(TILE_H/2);}
function worldToScreen(gx,gy){return {x:isoX(gx,gy),y:isoY(gx,gy)};} // world/iso space; render applies pan+zoom via ctx transform
function screenToGrid(sx,sy){const z=cam.z||1,x=(sx-cam.x)/z,y=(sy-cam.y)/z;
return {gx:(x/(TILE_W/2)+y/(TILE_H/2))/2,gy:(y/(TILE_H/2)-x/(TILE_W/2))/2};}
function centerCamera(gx,gy){const z=cam.z||1;cam.x=VW/2-isoX(gx,gy)*z;cam.y=VH/2-isoY(gx,gy)*z-30;}
function zoomAt(sx,sy,nz){nz=Math.max(0.6,Math.min(2.4,nz));const wx=(sx-cam.x)/cam.z,wy=(sy-cam.y)/cam.z;
cam.z=nz;cam.x=sx-wx*nz;cam.y=sy-wy*nz;}
/* ================= HELPERS ================= */
function difficulty(){return DIFFS[S.diff];}
function season(){return SEASONS[S.seasonIdx];}
function isWinter(){return season()==="Winter";}
function dist(ax,ay,bx,by){const dx=ax-bx,dy=ay-by;return Math.sqrt(dx*dx+dy*dy);}
function totalFood(){return FOOD_TYPES.reduce((s,f)=>s+(S.res[f]||0),0);}
function houseCapacity(){return S.buildings.filter(b=>b.built&&BUILD[b.type].house).reduce((s,b)=>s+BUILD[b.type].house,0);}
function countJob(j){return S.mellows.filter(m=>m.job===j&&!isChild(m)).length;}
function adultCount(){return S.mellows.filter(m=>!isChild(m)).length;}
function tamedWolves(){return S.wolves.filter(w=>w.state==="family").length;}
// wolf helpers ---------------------------------------------------------------
function newWolf(gx,gy,opt){opt=opt||{};const sl=difficulty().season;
return {id:S.nextId++,gx,gy,tx:gx,ty:gy,timer:0,atkCd:0,face:1,
sex:Math.random()<0.5?"male":"female",
bornAt:(S.tsec||0)-(opt.adult?sl*2:0),
state:opt.state||"wild", // wild | training | trained | family
bornTame:!!opt.bornTame,
trainT:opt.trainT||0, facId:opt.facId||null, homeId:opt.homeId||null,
packId:opt.packId==null?null:opt.packId};}
function wolfAge(w){return ((S.tsec||0)-(w.bornAt||0))/difficulty().season;} // in seasons
function wolfBaby(w){return w.bornTame&&wolfAge(w)<1;} // "baby" = facility/home-born, under 1 season
function wolfAdoptable(w){return w.state==="trained"||(w.state==="training"&&w.bornTame&&wolfAge(w)>=1);}
function facilityWolves(id){return S.wolves.filter(w=>w.facId===id&&(w.state==="training"||w.state==="trained"));}
function familyWolves(homeId){return S.wolves.filter(w=>w.state==="family"&&w.homeId===homeId);}
function homeHasWolf(homeId){return homeId&&S.wolves.some(w=>w.state==="family"&&w.homeId===homeId);}
function wolfCentres(){return S.buildings.filter(b=>b.built&&BUILD[b.type].wolfcentre);}
function isOrphan(m){return isChild(m)&&!(m.momId&&S.mellows.some(x=>x.id===m.momId))&&!(m.dadId&&S.mellows.some(x=>x.id===m.dadId));}
// one-tap adoption → the family that needs a wolf most (has children, has room, fewest wolves)
function adoptFromCentre(f){const ready=facilityWolves(f.id).filter(wolfAdoptable);
if(!ready.length){toast("No wolves are ready to adopt yet");return;}
let best=null,bestScore=-1;
for(const h of S.buildings.filter(b=>b.built&&BUILD[b.type].house)){const fam=familyWolves(h.id).length;
if(fam>=FAMILY_WOLF_CAP)continue;const res=S.mellows.filter(m=>m.home===h.id);if(!res.length)continue;
const score=(res.some(isChild)?100:0)+(FAMILY_WOLF_CAP-fam)*10;if(score>bestScore){bestScore=score;best=h;}}
if(!best){toast("No family has room for a wolf right now");return;}
const w=ready[0];w.state="family";w.homeId=best.id;w.facId=null;w.trainT=0;
const head=S.mellows.find(m=>m.home===best.id&&!isChild(m));
toast("A wolf joined "+(head?head.name+"'s family":"a family")+"!");logEvent("A family adopted a trained wolf");}
// If a family's home is demolished, its wolves return to a Wolf Den (else revert to the wild).
function reclaimOrphanWolves(){for(const w of S.wolves){if(w.state!=="family")continue;
if(S.buildings.find(b=>b.id===w.homeId&&b.built&&BUILD[b.type].house))continue; // home still stands
const c=wolfCentres().find(f=>facilityWolves(f.id).length<FACILITY_CAP);
if(c){w.state="trained";w.homeId=null;w.facId=c.id;w.trainT=0;logEvent("A wolf returned to the Wolf Den after its family's home was lost.");}
else{w.state="wild";w.homeId=null;w.facId=null;w.packId=0;}}}
function homeValid(m){return m.home&&S.buildings.some(b=>b.id===m.home&&b.built);}
function partnerOf(m){if(isChild(m)||!m.home)return null;return S.mellows.find(x=>x!==m&&x.home===m.home&&!isChild(x)&&x.sex!==m.sex)||null;}
function childrenOf(m){if(!m.home)return[];return S.mellows.filter(x=>x.home===m.home&&isChild(x));}
function friendsOf(m){return(m.friends||[]).map(id=>S.mellows.find(x=>x.id===id)).filter(Boolean);}
function friendNames(m){return friendsOf(m).map(f=>f.name).join(", ");}
// Light friendships: adults befriend a nearby non-family adult over time (max 2 each). Being near a friend lifts the mood.
function friendshipTick(dt){S._friendT=(S._friendT||0)-dt;if(S._friendT>0)return;S._friendT=6;
const adults=S.mellows.filter(x=>!isChild(x));
for(const m of adults){m.friends=m.friends||[];
if(m.friends.length>=2||Math.random()>0.3)continue;
const cand=adults.filter(o=>o.id!==m.id&&!m.friends.includes(o.id)&&!(m.home&&o.home===m.home)&&(o.friends||[]).length<2&&dist(m.gx,m.gy,o.gx,o.gy)<5)
.sort((a,b)=>dist(m.gx,m.gy,a.gx,a.gy)-dist(m.gx,m.gy,b.gx,b.gy));
const f=cand[0];
if(f){f.friends=f.friends||[];m.friends.push(f.id);f.friends.push(m.id);logEvent(m.name+" and "+f.name+" became friends.");}}}
function nearest(gx,gy,arr,pred){let best=null,bd=1e9;for(const o of arr){if(pred&&!pred(o))continue;
const d=dist(gx,gy,o.gx,o.gy);if(d<bd){bd=d;best=o;}}return best;}
function storages(){return S.buildings.filter(b=>b.built&&(BUILD[b.type].store||b.type==="storage"));}
function buildingsOfJob(job){const bt=JOBS[job]&&JOBS[job].bld;return S.buildings.filter(b=>b.built&&b.type===bt);}
function toast(msg){const t=document.getElementById("toast");t.textContent=msg;t.classList.add("show");
clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove("show"),1900);}
function logEvent(msg){S.log.unshift({d:S.day,m:msg});if(S.log.length>40)S.log.pop();}
/* ================= AI ================= */
function canGather(job){const j=JOBS[job];if(!j||j.kind!=="gather")return false;if(j.seasonal&&isWinter())return false;return true;}
function nodeAvailable(n){if(n.dormant||n.amount<=0)return false;const t=NODE[n.type];if(t.plant&&isWinter()&&!t.farmed)return false;
if(t.water)return [[1,0],[-1,0],[0,1],[0,-1]].some(d=>reachable(n.gx+d[0],n.gy+d[1])); // fish only if a reachable shore is adjacent
if(!reachable(n.gx,n.gy))return false;return true;}
function updateMellow(m,dt){
if(isChild(m)){m.age+=dt*0.22;m.hunger=Math.max(0,m.hunger-dt*0.8);
if(m.hunger<25&&totalFood()>0){for(const f of FOOD_TYPES)if(S.res[f]>0){S.res[f]--;m.hunger=Math.min(100,m.hunger+foodFill(f));dropSeed(f);break;}}
const canSchool=m.age>=6&&schoolInSession()&&schooling();
if(canSchool){
const sch=nearest(m.gx,m.gy,S.buildings.filter(b=>b.built&&b.type==="school"));
if(sch){if(dist(m.gx,m.gy,sch.gx,sch.gy)>1.1){m.tx=sch.gx+(hash2(m.id,1)-0.5)*1.5;m.ty=sch.gy+(hash2(m.id,2)-0.5)*1.5;}
else {m.schooled=true;let rate=0.34;if(m.trait==="bookish")rate*=1.4;
if(S.mellows.some(t=>t.job==="teacher"&&isElder(t)))rate*=1.4; // elder mentor
m.intel=Math.min(10,(m.intel||2)+dt*rate);}}else wanderStep(m,dt);
} else { // not in school — toddle near a living parent, else near home
const par=(m.momId&&S.mellows.find(x=>x.id===m.momId))||(m.dadId&&S.mellows.find(x=>x.id===m.dadId));
if(par){m.wander-=dt;if(m.wander<=0){m.wander=1+Math.random()*2;m.tx=par.gx+(Math.random()*1.5-0.75);m.ty=par.gy+(Math.random()*1.5-0.75);}}
else wanderStep(m,dt);
}
moveToward(m,dt);
if(m.age>=12){m.job="idle";m.home=null;
logEvent(m.name+" came of age"+((m.intel||0)>=7?" — bright and well-schooled!":"!"));
toast("🎂 "+m.name+" came of age!");m.happy=Math.min(100,(m.happy||60)+15);
if(S.festT<=0){S.festT=3;S.festKind="confetti";}if(ui.sound)Sound.sfx('festival');}return;}
m.age+=dt/90;const dk=difficulty().decay;
if(m.contraT>0)m.contraT=Math.max(0,m.contraT-dt);
m.hunger=Math.max(0,m.hunger-dt*1.7*dk);
if(isWinter())m.warmth+=(warmthProvided()?8:-6)*dt*dk;else m.warmth+=10*dt;
m.warmth=Math.max(0,Math.min(100,m.warmth));
let hd=0;if(m.hunger<=0)hd-=4;if(m.warmth<25)hd-=3;if(m.sick)hd-=3.5;
if(hd<0){if(m.trait==="hardy")hd*=0.6;else if(m.trait==="sickly")hd*=1.4;}
if(hd===0&&m.health<100)hd+=1.2;
m.health=Math.max(0,Math.min(100,m.health+hd*dt));
let tg=60;if(m.job!=="idle")tg+=12;if(totalFood()>S.mellows.length*2)tg+=10;else tg-=8;
if(homeHasWolf(m.home))tg+=8; // a family wolf brings joy
if(m.friends&&m.friends.length)for(const fid of m.friends){const f=S.mellows.find(x=>x.id===fid);if(f&&dist(m.gx,m.gy,f.gx,f.gy)<4){tg+=8;break;}} // a friend nearby lifts the mood
tg+=(S._beauty||0); // a beautiful, decorated village lifts everyone's spirits
if(m.hunger<25)tg-=20;if(isWinter()&&m.warmth<40)tg-=15;if(m.sick)tg-=20;if(S.res.wine>0)tg+=8;
if(m.trait==="cheery")tg+=12;
{const hb=HOBBIES[m.hobby];if(hb&&hb.job&&m.job===hb.job)tg+=6;} // doing the work they love lifts the spirits
m.happy=Math.max(0,Math.min(100,m.happy+(tg-m.happy)*Math.min(1,dt*0.3)));
if(m.health<=0){killMellow(m,"fell ill and passed");return;}
if(m.age>=75){killMellow(m,"passed of old age");return;} // hard ceiling — nobody lives past 75
if(m.age>=60){ // old age creeps in from 60; most pass in their 60s, the healthy & lucky reach 70
let h=0.0016*(1+(m.age-60)*0.06);
if(m.trait==="hardy")h*=0.7;else if(m.trait==="sickly")h*=1.5;
if(m.health>85)h*=0.55;else if(m.health<40)h*=1.6;
if(Math.random()<h*dt){killMellow(m,"passed of old age");return;}}
const j=JOBS[m.job];
if(m.hunger<28&&totalFood()>0&&m.state!=="goeat"){
const eatSpot=nearest(m.gx,m.gy,storages())||S.buildings.find(b=>b.built&&BUILD[b.type].house);
if(eatSpot){m.state="goeat";m.tx=eatSpot.gx;m.ty=eatSpot.gy;}}
if(m.state==="goeat"){moveToward(m,dt);if(arrived(m)){eat(m);m.state="idle";}return;}
behaveByJob(m,j,dt);
}
function behaveByJob(m,j,dt){
if(!j||m.job==="idle"){wanderStep(m,dt);moveToward(m,dt);return;}
if(j.kind==="gather")return gatherLoop(m,j,dt);
if(j.kind==="builder")return builderLoop(m,dt);
if(j.kind==="produce")return produceLoop(m,j,dt);
if(j.kind==="doctor")return serviceLoop(m,"medical",dt,()=>doctorTick(m,dt));
if(j.kind==="midwife")return serviceLoop(m,"nursery",dt,()=>{});
if(j.kind==="teacher")return serviceLoop(m,"school",dt,()=>{});
if(j.kind==="trainer")return serviceLoop(m,"wolfcentre",dt,()=>{});
wanderStep(m,dt);moveToward(m,dt);
}
function schooling(){return countJob("teacher")>0&&S.buildings.some(b=>b.built&&b.type==="school");}
function avgSkill(job){const w=S.mellows.filter(m=>m.job===job&&!isChild(m));return w.length?w.reduce((s,m)=>s+skill(m),0)/w.length:0;}
// basket sets carry capacity: none=6, basket=10, basket+schooled=15
function carryCap(m){return (S.res.basket||0)>0 ? (m.schooled?15:10) : 6;}
function gatherLoop(m,j,dt){
if(!canGather(m.job)){wanderStep(m,dt);moveToward(m,dt);return;}
if(m.carry>=carryCap(m)){const st=nearest(m.gx,m.gy,storages());
if(st){m.tx=st.gx;m.ty=st.gy;moveToward(m,dt);
if(arrived(m)){S.res[m.carryRes]=(S.res[m.carryRes]||0)+m.carry;m.carry=0;m.carryRes=null;}return;}}
if(!m.target||S.nodes.indexOf(m.target)<0||!nodeAvailable(m.target))
m.target=nearest(m.gx,m.gy,S.nodes,n=>n.type===j.node&&nodeAvailable(n));
if(!m.target){wanderStep(m,dt);moveToward(m,dt);return;}
m.tx=m.target.gx;m.ty=m.target.gy;moveToward(m,dt);
const atNode=NODE[m.target.type].water?dist(m.gx,m.gy,m.target.gx,m.target.gy)<1.6:arrived(m); // fishers fish from the adjacent shore
if(atNode){m.timer+=dt;const hb=0;let thr=Math.max(0.55,1.1-hb*0.18);
if(m.trait==="green"&&NODE[m.target.type].plant)thr*=0.7;
const tk=JOB_TOOL[m.job];if(tk&&(S.res[tk]||0)>0)thr*=0.6; // right tool in stock → faster
if(m.timer>thr){m.timer=0;
let base=2;if(m.target.type==="deer")base=2+(Math.random()<0.5?1:0);else if(m.target.type==="sheep")base=1+(Math.random()<0.5?1:0); // deer drop 2-3, sheep 1-2
const yld=base+hb;m.target.amount-=2;m.carry+=yld;m.carryRes=j.res;
if(m.job==="hunter"&&Math.random()<0.5)S.res.leather++;
if(m.target.amount<=0){m.target.amount=0; // collected / hunted out → despawn here and respawn at a random new spot
m.target.dormant=true;m.target.respawn=NODE[m.target.type].animal?25:14;
relocateNode(m.target);m.target=null;}}}
}
function builderLoop(m,dt){const site=nearest(m.gx,m.gy,S.buildings,b=>!b.built);
if(!site){wanderStep(m,dt);moveToward(m,dt);return;}
m.tx=site.gx;m.ty=site.gy;moveToward(m,dt);
const atSite=BUILD[site.type].bridge?dist(m.gx,m.gy,site.gx,site.gy)<1.7:arrived(m); // bridges are built from the adjacent shore (can't stand on open water)
if(atSite){site.prog+=dt*0.18;if(site.prog>=1){site.built=true;site.prog=1;logEvent("Built a "+BUILD[site.type].name);computeReach();if(ui.sound)Sound.sfx('build');}}}
function produceLoop(m,j,dt){const b=nearest(m.gx,m.gy,buildingsOfJob(m.job));
if(!b){wanderStep(m,dt);moveToward(m,dt);return;}
m.tx=b.gx;m.ty=b.gy;moveToward(m,dt);
if(arrived(m)){const info=BUILD[b.type];
if(info.craftTools){const tk=b.makeTool||"axe",rc=TOOLS[tk];
let can=true;for(const k in rc.cost)if((S.res[k]||0)<rc.cost[k])can=false;
if(can){m.timer+=dt*(1+skill(m)*1.2)*upgMult(b);if(m.timer>=(info.recipe.t||3.2)){m.timer=0;
for(const k in rc.cost)S.res[k]-=rc.cost[k];S.res[tk]=(S.res[tk]||0)+1;b.phase=(b.phase+1)%4;}}return;}
const r=info.recipe;if(!r)return;
let can=true;for(const k in r.in)if((S.res[k]||0)<r.in[k])can=false;
if(can){m.timer+=dt*(1+skill(m)*1.2)*upgMult(b);if(m.timer>=r.t){m.timer=0;
for(const k in r.in)S.res[k]-=r.in[k];for(const k in r.out)S.res[k]=(S.res[k]||0)+r.out[k];
if(r.bonus&&Math.random()<r.bonus.chance)S.res[r.bonus.res]=(S.res[r.bonus.res]||0)+1;b.phase=(b.phase+1)%4;}}}}
function serviceLoop(m,bld,dt,onTick){const b=nearest(m.gx,m.gy,S.buildings.filter(x=>x.built&&x.type===bld));
if(!b){wanderStep(m,dt);moveToward(m,dt);return;}
m.tx=b.gx;m.ty=b.gy;moveToward(m,dt);if(arrived(m))onTick();}
function doctorTick(m,dt){const sick=S.mellows.find(x=>x.sick);
if(sick&&S.res.potion>0){m.timer+=dt;if(m.timer>2/(1+skill(m)*1.2)){m.timer=0;S.res.potion--;sick.sick=false;
sick.health=Math.min(100,sick.health+40+skill(m)*35);logEvent(sick.name+" was healed");}}}
function foodFill(f){return f==="meals"?70:f==="meat"?33:45;} // meat is lean — ~3 pieces fully refill hunger
function eat(m){for(const f of FOOD_TYPES)if(S.res[f]>0){S.res[f]--;m.hunger=Math.min(100,m.hunger+foodFill(f));dropSeed(f);return;}}
// eating a crop drops seeds of its own kind: 1/5 none, 2/5 one, 2/5 two
function dropSeed(f){if(CROP_ORDER.indexOf(f)<0)return;S.seeds=S.seeds||startSeeds();
const r=Math.random(),n=r<0.2?0:(r<0.6?1:2);if(n)S.seeds[f]=(S.seeds[f]||0)+n;}
function killMellow(m,why){S.mellows=S.mellows.filter(x=>x!==m);
S.memorial=S.memorial||[];S.memorial.unshift({name:m.name,surname:m.surname||"",age:Math.floor(m.age),why,day:S.day});
if(S.memorial.length>80)S.memorial.pop();
const cem=S.buildings.find(b=>b.built&&BUILD[b.type].cemetery);
if(cem){S.graves=S.graves||[];const i=S.graves.length;
if(i<16){const col=i%4,row=(i/4|0); // neat 4×4 plot beside the cemetery — never scattered
S.graves.push({gx:cem.gx-0.54+col*0.36,gy:cem.gy+0.66+row*0.4,name:m.name,seed:Math.random()});}}
logEvent(m.name+" "+(m.surname||"")+" "+why);toast(m.name+" "+why);if(ui.sound)Sound.sfx('death');}
// ---- tile pathfinding: Mellows walk only on land (or a bridge), never across open water ----
function bridgeAt(x,y){return S.buildings.some(b=>b.built&&BUILD[b.type].bridge&&Math.round(b.gx)===x&&Math.round(b.gy)===y);}
function walkableTile(x,y){return x>=0&&y>=0&&x<GRID&&y<GRID&&(S.tiles[y][x]==="land"||bridgeAt(x,y));}
function nearestWalkableNeighbor(gx,gy,fx,fy){let best=null,bd=1e9;gx=Math.round(gx);gy=Math.round(gy);
for(const d of [[1,0],[-1,0],[0,1],[0,-1]]){const nx=gx+d[0],ny=gy+d[1];
if(reachable(nx,ny)&&walkableTile(nx,ny)){const dd=dist(fx,fy,nx,ny);if(dd<bd){bd=dd;best=[nx,ny];}}}return best;} // reachable shore only
function findPath(sx,sy,gx,gy){sx=Math.round(sx);sy=Math.round(sy);gx=Math.round(gx);gy=Math.round(gy);
if(sx===gx&&sy===gy)return [];
if(!walkableTile(sx,sy)||!walkableTile(gx,gy))return null;
const W=GRID,seen=new Uint8Array(W*W),prev=new Int32Array(W*W),q=new Int32Array(W*W);
let qh=0,qt=0;const si=sx+sy*W;seen[si]=1;prev[si]=-1;q[qt++]=si;
while(qh<qt){const c=q[qh++],x=c%W,y=(c/W)|0;
if(x===gx&&y===gy){const path=[];let cur=c;while(cur!==si&&cur!==-1){path.push([cur%W,(cur/W)|0]);cur=prev[cur];}return path.reverse();}
for(const d of [[1,0],[-1,0],[0,1],[0,-1]]){const nx=x+d[0],ny=y+d[1];if(nx<0||ny<0||nx>=W||ny>=W)continue;
const ni=nx+ny*W;if(!seen[ni]&&walkableTile(nx,ny)){seen[ni]=1;prev[ni]=c;q[qt++]=ni;}}}
return null;}
function moveToward(m,dt){const sp=1.4;
let goalX=m.tx,goalY=m.ty,exact=true;
if(!walkableTile(Math.round(m.tx),Math.round(m.ty))){ // target is on water (e.g. a fish): stand on the nearest shore
const n=nearestWalkableNeighbor(m.tx,m.ty,m.gx,m.gy);
if(n){goalX=n[0];goalY=n[1];exact=false;}}
const gk=Math.round(goalX)+Math.round(goalY)*GRID;
if(m._pg!==gk){m._pg=gk;m._path=findPath(m.gx,m.gy,goalX,goalY);m._pi=0;} // (re)route only when the goal tile changes
let tx,ty;
if(m._path&&m._path.length){ // follow waypoints around the water
while(m._pi<m._path.length&&dist(m.gx,m.gy,m._path[m._pi][0],m._path[m._pi][1])<0.3)m._pi++;
if(m._pi<m._path.length){tx=m._path[m._pi][0];ty=m._path[m._pi][1];}
else{tx=exact?m.tx:goalX;ty=exact?m.ty:goalY;}
} else {tx=exact?m.tx:goalX;ty=exact?m.ty:goalY;} // already on the goal tile, or no path → straight line (graceful fallback)
const dx=tx-m.gx,dy=ty-m.gy,d=Math.hypot(dx,dy);
if(d<0.05){m.gx=tx;m.gy=ty;return;}if(Math.abs(dx)>0.02)m.face=dx>0?1:-1;
const step=Math.min(d,sp*dt),nx=m.gx+dx/d*step,ny=m.gy+dy/d*step;
if(walkableTile(Math.round(nx),Math.round(ny))||!walkableTile(Math.round(m.gx),Math.round(m.gy))){m.gx=nx;m.gy=ny;}} // hard rule: never step onto open water
// A depleted node despawns and respawns at a random new spot — land nodes on reachable land, fish on water by a reachable shore.
function relocateNode(n){const t=NODE[n.type];
for(let tries=0;tries<100;tries++){const x=(Math.random()*GRID)|0,y=(Math.random()*GRID)|0;
const isWater=S.tiles[y][x]==="water";
if(t.water){if(!isWater)continue;
if(![[1,0],[-1,0],[0,1],[0,-1]].some(d=>reachable(x+d[0],y+d[1])))continue;} // fish: water beside a reachable shore
else{if(isWater||!reachable(x,y))continue;} // land node: reachable land
if(tileTaken(x,y))continue;if(S.nodes.some(o=>o!==n&&Math.round(o.gx)===x&&Math.round(o.gy)===y))continue;
n.gx=x;n.gy=y;n.seed=Math.random();return true;}
return false;}
function arrived(m){return Math.hypot(m.tx-m.gx,m.ty-m.gy)<0.15;}
function wanderStep(m,dt){m.wander-=dt;if(m.wander<=0){m.wander=1+Math.random()*2;
const h=S.buildings.find(b=>b.id===m.home)||S.buildings[0];const hx=h?h.gx:GRID/2,hy=h?h.gy:GRID/2;
m.tx=hx+(Math.random()*2-1);m.ty=hy+(Math.random()*2-1);}}
/* ================= SIM ================= */
function step(dt){if(!S)return;S.tsec+=dt;
if(S.tsec-(S._lastDay||0)>12){S._lastDay=S.tsec;S.day++;}
S.seasonT+=dt;if(S.seasonT>=difficulty().season){S.seasonT=0;S.seasonIdx=(S.seasonIdx+1)%4;S.seasonCount++;onSeasonChange();}
for(const n of S.nodes)if(n.dormant){n.respawn-=dt;if(n.respawn<=0){n.dormant=false;n.amount=NODE[n.type].max*(NODE[n.type].animal?0.6:1);}}
farmTick(dt);for(const m of S.mellows.slice())updateMellow(m,dt);
birthTick(dt);diseaseTick(dt);wolfTick(dt);burnFirewood(dt);friendshipTick(dt);
alertTick(dt);immigrationTick(dt);milestoneTick();questTick();
if(S.festT>0)S.festT-=dt;
if((S._weaT=(S._weaT||18)-dt)<=0){S._weaT=28+Math.random()*40;S.weather=(!isWinter()&&Math.random()<0.35)?"rain":"clear";}
if(isWinter())S.weather="clear"; // winter shows its own snow, not rain
{const vl=villageLevel();if(vl>(S.vlevel||1)){for(let L=(S.vlevel||1)+1;L<=vl;L++)onLevelUp(L);S.vlevel=vl;}}
S._beauty=Math.min(8,S.buildings.filter(b=>b.built&&BUILD[b.type].decor).length); // decorations beautify the village
if(ui.sound&&(S._ambT=(S._ambT||3)-dt)<=0){ // time-of-day ambient: crickets at night, birdsong by day
const aph=((S.tsec||0)%TOD_LEN)/TOD_LEN, anight=aph>0.6&&aph<0.9;
if(isWinter())S._ambT=7+Math.random()*4;
else if(anight){S._ambT=2+Math.random()*2.5;Sound.sfx('cricket');}
else {S._ambT=5+Math.random()*4;if(Math.random()<0.6)Sound.sfx('birds');}}
if((S._homeT=(S._homeT||0)-dt)<=0){S._homeT=1.5;assignHomes();weddingCheck();reclaimOrphanWolves();computeReach();}}
function onSeasonChange(){const s=season();toast(s+" has arrived");logEvent(s+" begins");
regrowTrees();growHerds();wolfSeason();
if(s==="Spring"){S._wintersSurvived=(S._wintersSurvived||0)+1;
for(const n of S.nodes)if(NODE[n.type].plant&&!NODE[n.type].farmed){n.dormant=false;n.amount=NODE[n.type].max;}
plantFarms();
for(const m of S.mellows)m.happy=Math.min(100,m.happy+15);S.festT=4;S.festKind="petal";
toast("Spring Bloom — flowers everywhere!");logEvent("Spring Bloom — blossoms open across the village.");if(ui.sound)Sound.sfx('festival');}
if(s==="Fall"){harvestFarms();
for(const m of S.mellows)m.happy=Math.min(100,m.happy+18);S.festT=4;S.festKind="leaf";
toast("The Harvest Festival — a bountiful autumn!");logEvent("The village held a Harvest Festival — the larder is full.");if(ui.sound)Sound.sfx('festival');}
if(s==="Winter"){for(const m of S.mellows)if(isChild(m))m.happy=Math.min(100,m.happy+10);
toast("❄ The first snow falls — bundle up!");logEvent("The first snow of winter falls — the children are delighted.");}
if(s==="Summer"){for(const m of S.mellows)m.happy=Math.min(100,m.happy+25);S.festT=4;S.festKind="confetti";
toast("The Summer Festival — everyone is merry!");logEvent("The village held a Summer Festival — spirits soar!");if(ui.sound)Sound.sfx('festival');}
if(S.seasonCount>=(S._nextNewcomer||3)){S._nextNewcomer=S.seasonCount+2+(Math.random()*2|0);newcomerEvent();} // newcomers every 2–3 seasons
}
// The village earns a grander title as population grows (celebration only — nothing is unlocked).
function onLevelUp(lv){const unlocked=BUILD_ORDER.filter(t=>bldLevel(t)===lv).map(t=>BUILD[t].name);
for(const m of S.mellows)m.happy=Math.min(100,m.happy+12);
S.festT=4;S.festKind="confetti"; // a celebratory burst on level-up
const title=VILLAGE_TITLES[Math.min(VILLAGE_TITLES.length-1,lv-1)];
toast("🎉 Level "+lv+" — now a "+title+"!");
logEvent((S.villageName||"The village")+" grew to Level "+lv+" — now a "+title+(unlocked.length?", unlocking "+unlocked.join(", "):"")+".");
if(ui.sound)Sound.sfx('milestone');}
// Farms need a Farmer to tend them: min(farmers,farms) get sown each spring, harvested each fall.
function plantFarms(){S.seeds=S.seeds||startSeeds();const farms=S.buildings.filter(b=>b.built&&BUILD[b.type].farm);
let hands=countJob("farmer"),sown=0;
for(const b of farms){if(hands<=0)break;if((b.stage||"empty")==="growing")continue;
const c=b.crop||"crops";if((S.seeds[c]||0)>=PLANT_COST){S.seeds[c]-=PLANT_COST;b.stage="growing";b.growing=c;hands--;sown++;}}
if(sown)logEvent("The Farmers sowed "+sown+" field"+(sown>1?"s":""));}
function harvestFarms(){let got=0;for(const b of S.buildings.filter(b=>b.built&&BUILD[b.type].farm)){
if((b.stage||"empty")==="growing"){const c=b.growing||"crops";const yld=Math.round(PLANT_YIELD*upgMult(b));S.res[c]=(S.res[c]||0)+yld;b.stage="empty";b.growing=null;got++;
logEvent("Harvested "+yld+" "+CROPS[c].name);}}
if(got)toast("Harvest time — "+got+" field"+(got>1?"s":"")+" brought in");}
// A tended pasture's herd grows by one each season, up to 6.
const HERD_CAP=6;
function growHerds(){if(countJob("shepherd")<=0)return;
for(const b of S.buildings.filter(b=>b.built&&BUILD[b.type].pasture))b.herd=Math.min(HERD_CAP,(b.herd||2)+1);}
// Chopped trees vanish and regrow next season on empty land (never on water, nodes or buildings).
function regrowTrees(){let n=S.treeRegrow||0;if(n<=0)return;const key=(x,y)=>x+","+y,occ={};
for(const nd of S.nodes)occ[key(nd.gx,nd.gy)]=true;
for(const b of S.buildings)for(const t of bldTiles(b.type,b.gx,b.gy,b.dir))occ[key(t[0],t[1])]=true;
let guard=n*50;
while(n>0&&guard-->0){const x=(Math.random()*GRID)|0,y=(Math.random()*GRID)|0;
if(S.tiles[y][x]!=="land"||occ[key(x,y)])continue;
occ[key(x,y)]=true;S.nodes.push({gx:x,gy:y,type:"tree",amount:NODE.tree.max,dormant:false,respawn:0,seed:Math.random()});n--;}
S.treeRegrow=n;}
function farmTick(dt){ // crops now grow on a seasonal plant→harvest cycle (see plantFarms/harvestFarms); pasture wool is continuous
if(isWinter())return;
const past=S.buildings.filter(b=>b.built&&BUILD[b.type].pasture);
const shepList=S.mellows.filter(m=>m.job==="shepherd"&&!isChild(m));
if(past.length&&shepList.length){let si=0; // one Shepherd tends one pasture; a family wolf gives that shepherd +50%
for(const b of past){if(si>=shepList.length)break;const shep=shepList[si++];
const herd=b.herd||2,boost=homeHasWolf(shep.home)?1.5:1;b._wa=(b._wa||0)+dt*0.12*herd*boost*upgMult(b);
while(b._wa>=1){b._wa-=1;
if((b.animal||"sheep")==="sheep"){S.res.wool++;if(Math.random()<0.5)S.res.meat++;if(Math.random()<0.2)S.res.leather++;} // sheep: wool + some meat/leather
else{S.res.meat+=1+(Math.random()<0.5?1:0);if(Math.random()<0.35)S.res.leather++;}}}}} // deer: meat (1-2) + leather
function warmthProvided(){const fires=S.buildings.filter(b=>b.built&&(BUILD[b.type].warmth||BUILD[b.type].warm));
if(fires.length===0)return false;if(S.res.firewood>0)return true;return S.buildings.some(b=>b.built&&BUILD[b.type].warm);}
function burnFirewood(dt){if(!isWinter())return;const fires=S.buildings.filter(b=>b.built&&BUILD[b.type].warmth).length;
if(fires>0&&S.res.firewood>0){S._burn=(S._burn||0)+dt*fires*0.25;while(S._burn>=1&&S.res.firewood>0){S._burn-=1;S.res.firewood--;}}}
function assignHomes(){
const houses=S.buildings.filter(b=>b.built&&BUILD[b.type].house);
const occ={};houses.forEach(h=>occ[h.id]={m:0,f:0,ad:0});
for(const mm of S.mellows){if(mm.home&&!occ[mm.home])mm.home=null;
if(mm.home&&!isChild(mm)){const o=occ[mm.home];o.ad++;if(mm.sex==="male")o.m++;else o.f++;}}
for(const mm of S.mellows){if(isChild(mm)||(mm.home&&occ[mm.home]))continue;
let best=null;
for(const h of houses){const o=occ[h.id];if(o.ad>=2)continue;
const pair=(mm.sex==="male"&&o.f>0&&o.m===0)||(mm.sex==="female"&&o.m>0&&o.f===0);
if(pair){best=h;break;}if(!best)best=h;}
if(best){mm.home=best.id;const o=occ[best.id];o.ad++;if(mm.sex==="male")o.m++;else o.f++;}}}
// Life event: when a new male+female couple settles in a home, celebrate a wedding (silent baseline on first run/load).
function weddingCheck(){const homes={};
for(const m of S.mellows){if(isChild(m)||!m.home)continue;(homes[m.home]=homes[m.home]||[]).push(m);}
for(const id in homes){const ad=homes[id],male=ad.find(x=>x.sex==="male"),female=ad.find(x=>x.sex==="female");
if(!male||!female)continue;
if(male.wedTo===female.id&&female.wedTo===male.id)continue; // already a couple
male.wedTo=female.id;female.wedTo=male.id;
if(S._weddingsInit){ // celebrate (not the initial founders / a fresh load)
male.happy=Math.min(100,male.happy+20);female.happy=Math.min(100,female.happy+20);
if(S.festT<=0){S.festT=4;S.festKind="petal";}
toast("💍 "+male.name+" & "+female.name+" wed!");
logEvent(male.name+" and "+female.name+" were married in "+(S.villageName||"the village")+".");
if(ui.sound)Sound.sfx('festival');}}
S._weddingsInit=true;}
function birthTick(dt){
for(const m of S.mellows){if(m.pregnant){
const h=S.buildings.find(b=>b.id===m.home&&b.built&&BUILD[b.type].house);
if(!h){m.pregnant=false;m.pregT=0;continue;} // a Mellow cannot be pregnant without a home
m.pregT-=dt;if(m.pregT<=0){m.pregnant=false;
const baby=newMellow(h.gx,h.gy,0);baby.home=m.home;
const dad=partnerOf(m);const pa=((m.intel||3)+((dad&&dad.intel)||3))/2;
baby.intel=Math.max(1,Math.min(10,Math.round(pa+(Math.random()*2-1))));
baby.surname=(dad&&dad.surname)||m.surname;
baby.parents={mom:m.name,dad:dad?dad.name:"—"};baby.momId=m.id;baby.dadId=dad?dad.id:null;
baby.gparents=[];if(m.parents)baby.gparents.push(m.parents.mom,m.parents.dad);
if(dad&&dad.parents)baby.gparents.push(dad.parents.mom,dad.parents.dad);
if(Math.random()<0.5)baby.trait=Math.random()<0.5?m.trait:(dad?dad.trait:m.trait); // trait partly inherited
if(baby.trait==="bookish")baby.intel=Math.min(10,baby.intel+1);
S.mellows.push(baby);S._everBaby=true;logEvent("Baby "+baby.name+" "+baby.surname+" was born!");toast("A baby Mellow was born!");if(ui.sound)Sound.sfx('birth');}}}
S.birthT-=dt;if(S.birthT>0)return;S.birthT=6;
if(totalFood()<=S.mellows.length*2)return;
const midwife=countJob("midwife")>0&&S.buildings.some(b=>b.built&&b.type==="nursery");
const fert=isWinter()?1.5:(season()==="Fall"?1.2:1.0); // more fertile in winter/fall -> more summer/fall births
for(const h of S.buildings.filter(b=>b.built&&BUILD[b.type].house)){
const res=S.mellows.filter(m=>m.home===h.id);
const males=res.filter(m=>!isChild(m)&&m.sex==="male").length;
const fem=res.filter(m=>!isChild(m)&&m.sex==="female"&&!m.pregnant&&!(m.contraT>0));
const kids=res.filter(m=>isChild(m)).length;
if(males>=1&&fem.length>=1&&kids<3&&Math.random()<(midwife?0.5:0.28)*fert){
fem[0].pregnant=true;fem[0].pregT=3*difficulty().season;}}}
function diseaseTick(dt){S.sickT-=dt;if(S.sickT<=0){S.sickT=90+Math.random()*90;
if(S.seasonCount<=4)return; // grace period: no illness for the first 4 seasons (year one)
if(Math.random()<0.16+(isWinter()?0.14:0)){const healthy=S.mellows.filter(m=>!m.sick&&!isChild(m));
if(healthy.length){let tot=0;const wts=healthy.map(m=>{const w=m.trait==="sickly"?3.2:m.trait==="hardy"?0.15:0.55;tot+=w;return w;});
let r=Math.random()*tot,idx=0;for(;idx<healthy.length-1;idx++){r-=wts[idx];if(r<=0)break;}
const v=healthy[idx];v.sick=true;logEvent(v.name+" caught a cold");}}}}
// wolves, like Mellows, never step onto open water
function stepOnLand(e,nx,ny){if(walkableTile(Math.round(nx),Math.round(ny))||!walkableTile(Math.round(e.gx),Math.round(e.gy))){e.gx=nx;e.gy=ny;}}
function wolfSpawnPos(den){for(let i=0;i<14;i++){const x=den.gx+(Math.random()*2-1),y=den.gy+(Math.random()*2-1);if(walkableTile(Math.round(x),Math.round(y)))return [x,y];}return [den.gx,den.gy];}
function wolfTick(dt){
for(const w of S.wolves){w.timer-=dt;if(w.atkCd>0)w.atkCd-=dt;
if(w.state==="family"){wolfFollow(w,dt,familyAnchor(w));continue;}
if(w.state==="training"||w.state==="trained"){wolfFollow(w,dt,facilityAnchor(w),0.9);continue;}
const den=(S.dens&&S.dens[w.packId])||{gx:w.gx,gy:w.gy}; // wild: roam near its den
if(w.timer<=0){w.timer=3+Math.random()*4;w.tx=den.gx+(Math.random()*5-2.5);w.ty=den.gy+(Math.random()*5-2.5);}
const dx=w.tx-w.gx,dy=w.ty-w.gy,d=Math.hypot(dx,dy)||1;
if(d>0.3){w.face=dx>0?1:-1;const st=Math.min(d,1.1*dt);stepOnLand(w,w.gx+dx/d*st,w.gy+dy/d*st);} // stop at the target (no overshoot/jitter, no face-flip)
wildWolfAttack(w);}
wolfRecruitTick(dt);}
function wolfFollow(w,dt,anchor,spd){spd=spd||1.4;if(!anchor)return;
const dx=anchor.gx-w.gx,dy=anchor.gy-w.gy,d=Math.hypot(dx,dy)||1;if(d>1.3){w.face=dx>0?1:-1;stepOnLand(w,w.gx+dx/d*spd*dt,w.gy+dy/d*spd*dt);}}
function familyAnchor(w){const res=S.mellows.filter(m=>m.home===w.homeId);const kid=res.find(isChild)||res[0];
const h=S.buildings.find(b=>b.id===w.homeId);return kid?{gx:kid.gx,gy:kid.gy}:(h?{gx:h.gx,gy:h.gy}:null);}
function facilityAnchor(w){const f=S.buildings.find(b=>b.id===w.facId);
return f?{gx:f.gx+(hash2(w.id,1)-0.5)*1.5,gy:f.gy+(hash2(w.id,2)-0.5)*1.5}:null;}
// one encounter roll when a wild wolf reaches a Mellow, then a cooldown (never per-tick)
function wildWolfAttack(w){if(w.atkCd>0)return;
let best=null,bd=1.4;for(const m of S.mellows){const dd=dist(w.gx,w.gy,m.gx,m.gy);if(dd<bd){bd=dd;best=m;}}
if(!best)return;w.atkCd=6+Math.random()*6;
if(protectedByWolf(best)){if(Math.random()<0.12){const fw=nearestFamilyWolf(best); // a family wolf may die defending
if(fw){S.wolves=S.wolves.filter(x=>x!==fw);logEvent("A family wolf died protecting "+best.name);toast("A family wolf gave its life protecting "+best.name);}}return;}
const r=Math.random(),worker=(best.job==="hunter"||best.job==="miner"||best.job==="ironminer"||best.job==="lumberjack");
if(isChild(best)){if(isOrphan(best)&&r<0.05){best.health-=20;toast("A wolf attacked "+best.name+"!");logEvent(best.name+" (orphaned) was attacked by a wolf");}return;}
if(worker){if(r<0.02)killMellow(best,"was killed by a wolf");else if(r<0.12){best.health-=18;toast("A wolf injured "+best.name+"!");logEvent(best.name+" was hurt by a wolf");}return;}
if(r<0.02){best.health-=12;toast("A wolf attacked "+best.name+"!");logEvent(best.name+" was attacked by a wolf");}} // lone villager
function nearestFamilyWolf(m){for(const w of S.wolves)if(w.state==="family"){if(w.homeId&&w.homeId===m.home)return w;if(dist(w.gx,w.gy,m.gx,m.gy)<2.2)return w;}return null;}
function protectedByWolf(m){return !!nearestFamilyWolf(m);}
// Centres in "train" mode pull in the nearest wild wolf for 8 meat (needs a Wolf Trainer)
function wolfRecruitTick(dt){S._recT=(S._recT||0)-dt;if(S._recT>0)return;S._recT=4;
if(countJob("trainer")<=0)return;
for(const f of wolfCentres()){if(f.mode==="house")continue;
if(facilityWolves(f.id).length>=FACILITY_CAP)continue;if((S.res.meat||0)<WOLF_TAME_MEAT)break;
const wild=S.wolves.filter(w=>w.state==="wild");if(!wild.length)break;
let best=null,bd=1e9;for(const w of wild){const dd=dist(w.gx,w.gy,f.gx,f.gy);if(dd<bd){bd=dd;best=w;}}
if(!best)break;S.res.meat-=WOLF_TAME_MEAT;best.state="training";best.facId=f.id;best.trainT=WOLF_TRAIN_SEASONS;best.packId=null;
logEvent("A wild wolf was brought in to the Wolf Den for training");}}
// seasonal wolf lifecycle: training completes, breeding, family surplus, wild pack growth
function wolfSeason(){const s=season();
for(const w of S.wolves)if(w.state==="training"){w.trainT-=1;if(w.trainT<=0)w.state="trained";}
for(const f of wolfCentres()){const list=facilityWolves(f.id),adults=list.filter(w=>!wolfBaby(w));
if(adults.length>=2&&list.length<FACILITY_CAP&&Math.random()<0.6){const fb=S.buildings.find(b=>b.id===f.id);
S.wolves.push(newWolf(fb.gx,fb.gy,{state:"training",bornTame:true,trainT:WOLF_TRAIN_SEASONS,facId:f.id}));}}
for(const h of S.buildings.filter(b=>b.built&&BUILD[b.type].house)){const fam=familyWolves(h.id);
if(fam.length>=2&&Math.random()<0.5){const litter=1+(Math.random()*3|0);
for(let i=0;i<litter;i++)S.wolves.push(newWolf(h.gx,h.gy,{state:"family",bornTame:true,homeId:h.id}));
logEvent("A family's wolves had "+litter+" pup"+(litter>1?"s":""));}}
for(const h of S.buildings.filter(b=>b.built&&BUILD[b.type].house)){const fam=familyWolves(h.id);
if(fam.length>FAMILY_WOLF_TOTAL){fam.sort((a,b)=>wolfAge(b)-wolfAge(a));const centre=wolfCentres()[0];
for(let i=FAMILY_WOLF_TOTAL;i<fam.length;i++){const w=fam[i];
if(centre&&facilityWolves(centre.id).length<FACILITY_CAP){w.state="trained";w.homeId=null;w.facId=centre.id;}
else{w.state="wild";w.homeId=null;w.facId=null;w.packId=0;}}}}
if(s==="Spring"&&S.dens&&S.dens.length){let add=2+(Math.random()*3|0);
while(add-->0){const wild=S.wolves.filter(w=>w.state==="wild");if(wild.length>=WOLF_MAX_WILD)break;
const counts=[0,0];for(const w of wild)if(w.packId!=null)counts[w.packId]++;
let p=counts[0]<=counts[1]?0:1;if(counts[p]>=WOLF_PACK_MAX){p=1-p;if(counts[p]>=WOLF_PACK_MAX)break;}
const den=S.dens[p]||S.dens[0];S.wolves.push(newWolf(...wolfSpawnPos(den),{state:"wild",packId:p,adult:true}));}}}
// ---- reachability: which land tiles can workers actually walk to from town (rivers need bridges) ----
function computeReach(){const start=S.buildings.find(b=>!BUILD[b.type].dock&&!BUILD[b.type].bridge&&!BUILD[b.type].boat)||S.buildings[0];
if(!start){S._reach=null;return;}
const R=[];for(let y=0;y<GRID;y++){R[y]=new Array(GRID).fill(false);}
const crossing=(x,y)=>S.buildings.some(b=>b.built&&(BUILD[b.type].bridge||BUILD[b.type].boat)&&b.gx===x&&b.gy===y);
const pass=(x,y)=>x>=0&&y>=0&&x<GRID&&y<GRID&&(S.tiles[y][x]==="land"||crossing(x,y));
const sx=Math.round(start.gx),sy=Math.round(start.gy),q=[[sx,sy]];if(pass(sx,sy))R[sy][sx]=true;
while(q.length){const c=q.pop(),x=c[0],y=c[1];
for(const d of [[1,0],[-1,0],[0,1],[0,-1]]){const nx=x+d[0],ny=y+d[1];
if(pass(nx,ny)&&!R[ny][nx]){R[ny][nx]=true;q.push([nx,ny]);}}}
S._reach=R;}
function reachable(gx,gy){if(!S._reach)return true;const x=Math.round(gx),y=Math.round(gy);
return x>=0&&y>=0&&x<GRID&&y<GRID&&!!S._reach[y][x];}
// footprint tiles a building covers (size N -> N×N block anchored at gx,gy)
function bldTiles(type,gx,gy,dir){if(BUILD[type].dock){const d=DOCK_DIRS[dir||0];return [[gx,gy],[gx+d[0],gy+d[1]]];} // dock = land anchor + water pier
const s=BUILD[type].size||1,t=[];for(let dy=0;dy<s;dy++)for(let dx=0;dx<s;dx++)t.push([gx+dx,gy+dy]);return t;}
function tileTaken(gx,gy,ignore){return S.buildings.some(b=>b!==ignore&&bldTiles(b.type,b.gx,b.gy,b.dir).some(t=>t[0]===gx&&t[1]===gy));}
// A dock spans 2 tiles: the tapped water tile (pier) + an adjacent reachable land tile (anchor). Returns {gx,gy=land, dir, wx,wy=water} or null.
function dockPlace(wx,wy){if(wx<0||wy<0||wx>=GRID||wy>=GRID||S.tiles[wy][wx]!=="water"||tileTaken(wx,wy))return null;
for(let d=0;d<4;d++){const lx=wx-DOCK_DIRS[d][0],ly=wy-DOCK_DIRS[d][1];
if(lx<0||ly<0||lx>=GRID||ly>=GRID)continue;
if(S.tiles[ly][lx]!=="land"||!reachable(lx,ly)||tileTaken(lx,ly))continue;
if(S.nodes.some(n=>n.gx===lx&&n.gy===ly&&!n.dormant&&n.amount>0))continue;
return {gx:lx,gy:ly,dir:d,wx,wy};}
return null;}
// ---- proactive alerts so problems never build up silently ----
function alertTick(dt){S.alertT=(S.alertT==null?5:S.alertT)-dt;if(S.alertT>0)return;S.alertT=9;
S._acd=S._acd||{};const now=S.tsec,pop=S.mellows.length;if(pop===0)return;
const fire=(k,msg,cd)=>{if((S._acd[k]||-1e9)+(cd||26)<now){S._acd[k]=now;toast(msg);logEvent(msg);}};
if(S.mellows.some(m=>m.hunger<=1&&!isChild(m)))fire('starve',"A Mellow is starving — get food fast!");
else if(totalFood()<=6&&totalFood()<pop)fire('food',"Food is running low — assign gatherers or a Farm.");
const foodJobs=S.mellows.filter(m=>!isChild(m)&&["hunter","fruit","fisher","farmer"].indexOf(m.job)>=0).length;
if(foodJobs===0&&totalFood()<pop*3)fire('nofood',"No one is gathering food.");
if(season()==="Fall"&&(S.res.firewood||0)<3&&!S.buildings.some(b=>b.built&&BUILD[b.type].warm))fire('winter',"Winter is coming — stock firewood or build warmth.",70);
if(S.buildings.some(b=>!b.built)&&countJob("builder")===0)fire('builder',"A building is waiting — assign a Builder.");
if(S.mellows.some(m=>m.sick)&&countJob("doctor")===0)fire('sick',"Someone is ill and there's no Doctor.");}
// ---- wandering Mellows arrive to keep the village recoverable ----
function immigrationTick(dt){if(S.mellows.length===0){gameOver();return;}
S.immT=(S.immT==null?45:S.immT)-dt;if(S.immT>0)return;S.immT=40+Math.random()*45;
// regular arrivals come via the seasonal Newcomers event; this only rescues a near-dead village
if(adultCount()>=2)return;
if(Math.random()<0.9){const b0=S.buildings.find(b=>b.built)||S.buildings[0];if(!b0)return;
const m=newMellow(b0.gx,b0.gy,20+(Math.random()*16|0));m.home=null;S.mellows.push(m);
logEvent("A wandering Mellow, "+m.name+" "+m.surname+", joined the village!");
toast(m.name+" wandered in to join the village!");if(ui.sound)Sound.sfx('arrive');}}
// A group of Mellows "dressed in white" arrives every few seasons; the player accepts or turns them away.
function landNear(cx,cy,rad){for(let i=0;i<50;i++){const x=Math.round(cx+(Math.random()*2-1)*rad),y=Math.round(cy+(Math.random()*2-1)*rad);
if(x>=0&&y>=0&&x<GRID&&y<GRID&&S.tiles[y][x]==="land"&&!tileTaken(x,y))return {x,y};}return {x:Math.max(0,Math.min(GRID-1,Math.round(cx))),y:Math.max(0,Math.min(GRID-1,Math.round(cy)))};}
function newcomerEvent(){
if((S.newcomers||[]).length||!S.mellows.length)return; // one pending group at a time
const b0=S.buildings.find(b=>b.built)||S.buildings[0];const cx=b0?b0.gx:GRID/2,cy=b0?b0.gy:GRID/2;
const n=2+(Math.random()*3|0); // 2–4 newcomers
S.newcomers=[];
for(let i=0;i<n;i++){const sp=landNear(cx,cy+2,3.5);
const m=newMellow(sp.x,sp.y,18+(Math.random()*22|0));m.home=null;m.job="idle";m.newcomer=true;m.bob=Math.random()*6;
S.newcomers.push(m);}
showNewcomerPopup();if(ui.sound)Sound.sfx('arrive');}
function showNewcomerPopup(){const n=(S.newcomers||[]).length;if(!n)return;
document.getElementById("newcomerOverlay")&&document.getElementById("newcomerOverlay").remove();
const ov=document.createElement("div");ov.className="overlay";ov.id="newcomerOverlay";ov.style.display="flex";
ov.innerHTML=`<div class="card parch" style="max-width:340px;text-align:center">
<h2>Newcomers have arrived</h2>
<div class="hint"><p><b>${n}</b> Mellow${n>1?"s":""} dressed all in white stand at the edge of ${S.villageName||"the village"}, hoping to join the community.</p></div>
<div class="row" style="justify-content:center;gap:10px;margin-top:12px">
<button class="btn sec" id="newcNo">Turn them away</button>
<button class="btn" id="newcYes">Welcome them in</button></div></div>`;
document.body.appendChild(ov);
document.getElementById("newcYes").onclick=acceptNewcomers;
document.getElementById("newcNo").onclick=declineNewcomers;}
function acceptNewcomers(){const list=S.newcomers||[],n=list.length;
for(const m of list){m.newcomer=false;S.mellows.push(m);} // change into normal clothes & seamlessly join
S.newcomers=[];assignHomes();weddingCheck();computeReach();
toast("🤍 "+n+" newcomer"+(n>1?"s":"")+" joined "+(S.villageName||"the village")+"!");
logEvent(n+" newcomer"+(n>1?"s were":" was")+" welcomed into the village.");
if(ui.sound)Sound.sfx('arrive');
const o=document.getElementById("newcomerOverlay");if(o)o.remove();}
function declineNewcomers(){const n=(S.newcomers||[]).length;S.newcomers=[];
toast("The newcomers moved on.");logEvent("The village turned away "+n+" newcomer"+(n>1?"s":"")+".");
const o=document.getElementById("newcomerOverlay");if(o)o.remove();}
function milestoneTick(){S.achieved=S.achieved||[];
for(const ms of MILESTONES){if(S.achieved.indexOf(ms.id)>=0)continue;if(ms.test(S)){S.achieved.push(ms.id);
toast("Milestone — "+ms.name+"!");logEvent("Milestone reached: "+ms.name);if(ui.sound)Sound.sfx('milestone');}}}
let overShown=false;
function gameOver(){if(overShown)return;overShown=true;ui.speed=0;
$("overStats").textContent=(S.villageName||"Your village")+" lasted "+S.seasonCount+" season"+(S.seasonCount===1?"":"s")+".";
$("overOverlay").style.display="flex";}
/* ================= ART (hand-drawn ink outline style) ================= */
const INK="#5a4835"; // softer warm brown outline (closer to Life of Mellow's gentle look)
function hx(c){if(c[0]==="#"){const n=parseInt(c.slice(1),16);return[(n>>16)&255,(n>>8)&255,n&255];}
const m=c.match(/\d+/g);return m?[+m[0],+m[1],+m[2]]:[0,0,0];}
function blend(a,b,t){const p=hx(a),q=hx(b);
return "rgb("+Math.round(p[0]+(q[0]-p[0])*t)+","+Math.round(p[1]+(q[1]-p[1])*t)+","+Math.round(p[2]+(q[2]-p[2])*t)+")";}
const lite=(c,t)=>blend(c,"#ffffff",t), dark=(c,t)=>blend(c,"#000000",t);
function inkStroke(g,w){g.strokeStyle=INK;g.lineWidth=(w||1.6)*0.85;g.lineJoin="round";g.lineCap="round";g.stroke();}
function poly(g,pts,fill){g.beginPath();g.moveTo(pts[0][0],pts[0][1]);for(let i=1;i<pts.length;i++)g.lineTo(pts[i][0],pts[i][1]);g.closePath();if(fill){g.fillStyle=fill;g.fill();}}
function fillP(g,pts,fill,w){poly(g,pts,fill);if(w!==0)inkStroke(g,w);}
function circ(g,x,y,r){g.beginPath();g.arc(x,y,r,0,7);}
function fillC(g,x,y,r,fill,w){g.beginPath();g.arc(x,y,r,0,7);if(fill){g.fillStyle=fill;g.fill();}if(w!==0)inkStroke(g,w);}
function fillE(g,x,y,rx,ry,fill,w){g.beginPath();g.ellipse(x,y,rx,ry,0,0,7);if(fill){g.fillStyle=fill;g.fill();}if(w!==0)inkStroke(g,w);}
function blobPath(g,x,y,r,n,seed){g.beginPath();
for(let i=0;i<=n;i++){const a=i/n*Math.PI*2,wob=0.82+0.18*Math.sin(i*2.7+(seed||0)*12);
const px=x+Math.cos(a)*r*wob,py=y+Math.sin(a)*r*wob*0.92;if(i===0)g.moveTo(px,py);else g.lineTo(px,py);}g.closePath();}
function shadow(g,x,y,rx){g.fillStyle="rgba(70,55,35,.12)";g.beginPath();g.ellipse(x,y,rx,rx*0.36,0,0,7);g.fill();}
function artTree(g,x,y,s,winter,seed){s=s||1;seed=seed||0;
shadow(g,x,y,12*s);
fillP(g,[[x-2.6*s,y+1],[x-2*s,y-9*s],[x+2*s,y-9*s],[x+2.6*s,y+1]],"#7a5230",1.5*s);
const cy=y-18*s,cr=13*s;
blobPath(g,x,cy,cr,9,seed);g.fillStyle=winter?"#dbe6da":"#6aa63e";g.fill();inkStroke(g,1.9*s);
g.strokeStyle=winter?"#c4d3c4":"#4d8a2c";g.lineWidth=1.5*s;
for(let i=0;i<4;i++){const a=seed*7+i*1.7;g.beginPath();g.arc(x+Math.cos(a)*cr*0.4,cy+Math.sin(a)*cr*0.28,cr*0.34,a,a+1.3);g.stroke();}
g.fillStyle=winter?"#ffffff":"rgba(255,255,255,.20)";g.beginPath();g.ellipse(x-cr*0.35,cy-cr*0.42,cr*0.3,cr*0.18,-0.6,0,7);g.fill();
if(winter){g.fillStyle="#fff";g.beginPath();g.ellipse(x,cy-cr*0.55,cr*0.7,cr*0.28,0,0,7);g.fill();}}
function artBush(g,x,y,s,berry,winter){s=s||1;shadow(g,x,y,9*s);
blobPath(g,x,y-5*s,7*s,8,x*0.3);g.fillStyle=winter?"#cdd8cd":"#569a3f";g.fill();inkStroke(g,1.6*s);
g.fillStyle="rgba(255,255,255,.18)";g.beginPath();g.ellipse(x-2*s,y-8*s,3*s,1.8*s,-0.5,0,7);g.fill();
if(!winter&&berry){g.fillStyle=berry;for(const p of [[-3,-5],[3,-6],[5,-3],[-1,-3]]){fillC(g,x+p[0]*s,y+p[1]*s,1.6*s,berry,1*s);}}}
function artHerb(g,x,y,s,winter){s=s||1;g.strokeStyle=winter?"#c4d3c4":INK;g.lineWidth=1.6*s;g.lineCap="round";
for(const a of [-0.5,0,0.5]){g.beginPath();g.moveTo(x,y);g.quadraticCurveTo(x+a*9*s,y-6*s,x+a*13*s,y-12*s);g.stroke();}
g.strokeStyle=winter?"#cdd8cd":"#57a03e";g.lineWidth=2.4*s;
for(const a of [-0.5,0,0.5]){g.beginPath();g.moveTo(x,y);g.quadraticCurveTo(x+a*9*s,y-6*s,x+a*13*s,y-12*s);g.stroke();}
if(!winter){g.fillStyle="#e7c94a";fillC(g,x,y-13*s,2*s,"#e7c94a",1*s);fillC(g,x-6*s,y-9*s,1.6*s,"#e7c94a",1*s);fillC(g,x+6*s,y-9*s,1.6*s,"#e7c94a",1*s);}}
function artRock(g,x,y,s){s=s||1;shadow(g,x,y,12*s);
g.beginPath();g.moveTo(x-11*s,y+1*s);g.quadraticCurveTo(x-13*s,y-7*s,x-4*s,y-11*s);
g.quadraticCurveTo(x+4*s,y-14*s,x+10*s,y-7*s);g.quadraticCurveTo(x+13*s,y-1*s,x+8*s,y+2*s);
g.quadraticCurveTo(x-2*s,y+4*s,x-11*s,y+1*s);g.closePath();
g.fillStyle="#a9afb6";g.fill();inkStroke(g,1.9*s);
g.strokeStyle=INK;g.lineWidth=1.3*s;g.beginPath();g.moveTo(x-2*s,y-10*s);g.lineTo(x+1*s,y-2*s);g.lineTo(x-6*s,y+0*s);g.stroke();
g.fillStyle="#c4cad0";g.beginPath();g.ellipse(x-4*s,y-7*s,3.4*s,2*s,-0.4,0,7);g.fill();}
function artOre(g,x,y,s){s=s||1;shadow(g,x,y,12*s);
g.beginPath();g.moveTo(x-10*s,y+1*s);g.quadraticCurveTo(x-12*s,y-8*s,x-3*s,y-12*s);
g.quadraticCurveTo(x+5*s,y-14*s,x+10*s,y-6*s);g.quadraticCurveTo(x+12*s,y-1*s,x+7*s,y+2*s);
g.quadraticCurveTo(x-2*s,y+4*s,x-10*s,y+1*s);g.closePath();
g.fillStyle="#5c5a5f";g.fill();inkStroke(g,1.9*s);
g.fillStyle="#6f6d72";g.beginPath();g.ellipse(x-3*s,y-8*s,3.4*s,2.2*s,-0.4,0,7);g.fill();
g.fillStyle="#d9a24a";fillC(g,x-2*s,y-6*s,1.7*s,"#d9a24a",1*s);fillC(g,x+4*s,y-2*s,1.4*s,"#d9a24a",1*s);}
function artDeer(g,x,y,s,f){s=s||1;f=f||1;shadow(g,x,y,10*s);const c="#b07c46",cl="#caa068";
g.strokeStyle=INK;g.lineWidth=2*s;g.lineCap="round";
for(const dx of [-3.5,-1,2.5,4.5]){g.beginPath();g.moveTo(x+dx*s,y-5*s);g.lineTo(x+dx*s,y);g.stroke();}
g.fillStyle="#f2e8d2";g.beginPath();g.ellipse(x-f*6*s,y-8*s,1.7*s,2.3*s,0.3,0,7);g.fill(); // tail
fillE(g,x,y-8*s,6.8*s,4.4*s,c,1.9*s); // body
g.fillStyle=cl;g.beginPath();g.ellipse(x-f*1*s,y-9*s,4*s,2.2*s,0,0,7);g.fill(); // back highlight
fillE(g,x+f*7.6*s,y-13*s,3.5*s,4.3*s,c,1.8*s); // head/neck
g.fillStyle=cl;g.beginPath();g.ellipse(x+f*8.6*s,y-11.2*s,2.5*s,2*s,0,0,7);g.fill(); // muzzle
fillE(g,x+f*5.7*s,y-16*s,1.4*s,2.3*s,c,1.3*s);fillE(g,x+f*9.5*s,y-16*s,1.4*s,2.3*s,c,1.3*s); // ears
g.strokeStyle="#d9c6a0";g.lineWidth=1.6*s;g.lineCap="round";
g.beginPath();g.moveTo(x+f*6.6*s,y-16.5*s);g.lineTo(x+f*5.4*s,y-20.5*s);g.moveTo(x+f*6.1*s,y-18*s);g.lineTo(x+f*4.2*s,y-19.5*s);
g.moveTo(x+f*8.6*s,y-16.5*s);g.lineTo(x+f*9.8*s,y-20.5*s);g.moveTo(x+f*9.1*s,y-18*s);g.lineTo(x+f*11*s,y-19.5*s);g.stroke();
g.fillStyle=INK;g.beginPath();g.ellipse(x+f*6.5*s,y-13.5*s,0.9*s,1.15*s,0,0,7);g.fill();g.beginPath();g.ellipse(x+f*8.9*s,y-13.5*s,0.9*s,1.15*s,0,0,7);g.fill();
g.fillStyle="#fff";circ(g,x+f*6.8*s,y-13.9*s,0.35*s);g.fill();circ(g,x+f*9.2*s,y-13.9*s,0.35*s);g.fill();
g.fillStyle=INK;circ(g,x+f*10.4*s,y-11*s,0.7*s);g.fill(); // nose
g.fillStyle="#f0e2c4";circ(g,x-f*2*s,y-7*s,1.2*s);g.fill();circ(g,x+f*1.5*s,y-8.5*s,1*s);g.fill();} // spots
function artSheep(g,x,y,s,f){s=s||1;f=f||1;shadow(g,x,y,10*s);
g.strokeStyle=INK;g.lineWidth=2*s;g.lineCap="round";
for(const dx of [-3.5,-1,2.5,4.5]){g.beginPath();g.moveTo(x+dx*s,y-4*s);g.lineTo(x+dx*s,y);g.stroke();}
blobPath(g,x,y-7*s,7.3*s,13,x*0.2);g.fillStyle="#f8f3ea";g.fill();inkStroke(g,1.9*s);
fillE(g,x+f*6.6*s,y-9.6*s,2.9*s,3.4*s,"#7c6858",1.7*s); // face
fillE(g,x+f*4.6*s,y-10.8*s,1.5*s,1.1*s,"#6a5648",1.2*s);fillE(g,x+f*8.6*s,y-10.8*s,1.5*s,1.1*s,"#6a5648",1.2*s); // ears
g.fillStyle="#f8f3ea";circ(g,x+f*6.6*s,y-11.8*s,2.3*s);g.fill(); // forelock
g.fillStyle="#fff";circ(g,x+f*5.6*s,y-9.6*s,1*s);g.fill();circ(g,x+f*7.6*s,y-9.6*s,1*s);g.fill();
g.fillStyle=INK;circ(g,x+f*5.75*s,y-9.5*s,0.45*s);g.fill();circ(g,x+f*7.75*s,y-9.5*s,0.45*s);g.fill();
circ(g,x+f*6.65*s,y-8*s,0.55*s);g.fill();} // nose
function artFish(g,x,y,s){s=s||1;
g.strokeStyle="rgba(255,255,255,.55)";g.lineWidth=1.4*s;g.beginPath();g.ellipse(x,y,10*s,3*s,0,0,7);g.stroke();
fillE(g,x,y-1*s,5*s,2.6*s,"#4a86c8",1.4*s);
fillP(g,[[x+4*s,y-1*s],[x+8*s,y-3*s],[x+8*s,y+1*s]],"#4a86c8",1.2*s);
g.fillStyle="#fff";circ(g,x-3*s,y-1.5*s,0.8*s);g.fill();}
function artWolf(g,x,y,s,f,tamed){s=s||1;f=f||1;shadow(g,x,y,10*s);
const gr=tamed?"#9a8a6e":"#82828c",gl=tamed?"#b2a184":"#9a9aa4";
g.strokeStyle=INK;g.lineWidth=2.1*s;g.lineCap="round";
for(const dx of [-3.5,-1,2.5,4.5]){g.beginPath();g.moveTo(x+dx*s,y-4.5*s);g.lineTo(x+dx*s,y);g.stroke();}
g.save();g.translate(x-f*6*s,y-8*s);g.rotate(-f*0.5);fillE(g,0,0,3.3*s,2.2*s,gr,1.5*s);g.restore(); // bushy tail
fillE(g,x,y-7.5*s,6.6*s,4.3*s,gr,1.9*s); // body
g.fillStyle=gl;g.beginPath();g.ellipse(x-f*1*s,y-8.5*s,3.6*s,2*s,0,0,7);g.fill(); // back highlight
fillE(g,x+f*7*s,y-11.5*s,4.1*s,3.8*s,gr,1.8*s); // head (forward)
g.fillStyle=gl;g.beginPath();g.ellipse(x+f*8.4*s,y-9.9*s,2.4*s,1.8*s,0,0,7);g.fill(); // muzzle
fillP(g,[[x+f*4.6*s,y-13*s],[x+f*4.2*s,y-17*s],[x+f*6.8*s,y-14*s]],gr,1.3*s); // ear
fillP(g,[[x+f*7.4*s,y-14*s],[x+f*9.8*s,y-17*s],[x+f*9.6*s,y-13*s]],gr,1.3*s); // ear
g.fillStyle=INK;circ(g,x+f*5.9*s,y-12*s,1.1*s);g.fill();circ(g,x+f*8.3*s,y-12*s,1.1*s);g.fill();
g.fillStyle="#fff";circ(g,x+f*6.2*s,y-12.4*s,0.4*s);g.fill();circ(g,x+f*8.6*s,y-12.4*s,0.4*s);g.fill();
g.fillStyle=INK;g.beginPath();g.ellipse(x+f*10*s,y-9.6*s,0.95*s,0.75*s,0,0,7);g.fill(); // nose
if(tamed){g.strokeStyle="#c0492f";g.lineWidth=1.9*s;g.beginPath();g.moveTo(x+f*3.6*s,y-6.6*s);g.quadraticCurveTo(x+f*7*s,y-5*s,x+f*10*s,y-7*s);g.stroke();
g.fillStyle="#e8c84a";circ(g,x+f*6.8*s,y-5.3*s,1*s);g.fill();}} // collar + tag
function artMellow(g,px,py,scarf,tool,opt){opt=opt||{};
const s=opt.s||1,bob=opt.bob||0,face=opt.face||1;
const bw=8*s,H=22.5*s,footY=py-bob,bodyBot=footY-3*s,bodyTop=bodyBot-H;
shadow(g,px,py,bw*1.15);
// legs + feet
g.strokeStyle=INK;g.lineWidth=2.2*s;g.lineCap="round";
g.beginPath();g.moveTo(px-bw*0.4,bodyBot-1*s);g.lineTo(px-bw*0.4,footY+1*s);
g.moveTo(px+bw*0.4,bodyBot-1*s);g.lineTo(px+bw*0.4,footY+1*s);g.stroke();
g.fillStyle=INK;g.beginPath();g.ellipse(px-bw*0.5,footY+1.6*s,2.2*s,1.4*s,0,0,7);g.fill();
g.beginPath();g.ellipse(px+bw*0.5,footY+1.6*s,2.2*s,1.4*s,0,0,7);g.fill();
// leaf backpack behind (two leaves)
const packX=px-face*bw*0.5,packY=bodyTop+H*0.52;
for(const dl of [-1,1]){g.save();g.translate(packX,packY);g.rotate(face*(-0.45+dl*0.45));
fillP(g,[[0,-bw*0.95],[bw*0.5,-bw*0.1],[0,bw*0.5],[-bw*0.22,-bw*0.1]],scarf,1.4*s);
g.strokeStyle=dark(scarf,0.22);g.lineWidth=1*s;g.beginPath();g.moveTo(0,-bw*0.85);g.lineTo(0,bw*0.35);g.stroke();g.restore();}
// body (marshmallow — soft pillow cylinder: flatter top, straighter sides)
const rTop=bw*0.55,rBot=bw*0.5,L=px-bw,R=px+bw,T=bodyTop,B=bodyBot+2.6*s;
const bodyPath=()=>{g.beginPath();
g.moveTo(L,T+rTop);g.quadraticCurveTo(L,T,px-bw+rTop,T);g.lineTo(px+bw-rTop,T);
g.quadraticCurveTo(R,T,R,T+rTop);g.lineTo(R,B-rBot);
g.quadraticCurveTo(R,B,px+bw-rBot,B);g.lineTo(px-bw+rBot,B);
g.quadraticCurveTo(L,B,L,B-rBot);g.closePath();};
bodyPath();
const bg=g.createLinearGradient(px,T,px,B);
bg.addColorStop(0,opt.sick?"#eef6e2":"#fffef9");bg.addColorStop(0.55,opt.sick?"#e4efd6":"#faf4e8");bg.addColorStop(1,opt.sick?"#cfe0bf":"#eee2ca");
g.fillStyle=bg;g.fill();
// clothes: little shorts on the lower body (Life of Mellow style) — job colour lives in the scarf
const shortsY=T+H*0.72,shortsC=opt.white?"#f4f4f4":"#c2a86a"; // newcomers are dressed all in white
g.save();bodyPath();g.clip();
g.fillStyle=shortsC;g.fillRect(L-1,shortsY,(R-L)+2,B-shortsY+2);
g.fillStyle="rgba(0,0,0,.10)";g.fillRect(px+0.5*s,shortsY,R-px,B-shortsY+2); // right-side shading
g.restore();
bodyPath();inkStroke(g,2*s); // body outline over white + shorts
g.strokeStyle=dark(shortsC,0.32);g.lineWidth=1*s;g.lineCap="round";
g.beginPath();g.moveTo(L+2*s,shortsY);g.lineTo(R-2*s,shortsY); // waistband
g.moveTo(px,shortsY+1.4*s);g.lineTo(px,B-0.6*s);g.stroke(); // leg split
g.fillStyle="rgba(255,255,255,.55)";g.beginPath();g.ellipse(px-bw*0.26,T+rTop*0.7,bw*0.46,bw*0.3,-0.4,0,7);g.fill();
// eyes (soft ovals — a touch smaller & calmer, less baby-cute)
// anchored near the top as a "head zone" so a taller body doesn't create a big forehead
const ey=bodyTop+7.5*s,ex=bw*0.33,eo=bw*0.05*face;
g.fillStyle=INK;
g.beginPath();g.ellipse(px-ex+eo,ey,1.5*s,1.85*s,0,0,7);g.fill();g.beginPath();g.ellipse(px+ex+eo,ey,1.5*s,1.85*s,0,0,7);g.fill();
g.fillStyle="#fff";circ(g,px-ex+eo+0.55*s,ey-0.85*s,0.5*s);g.fill();circ(g,px+ex+eo+0.55*s,ey-0.85*s,0.5*s);g.fill();
// tiny mouth
g.strokeStyle=INK;g.lineWidth=1.0*s;g.lineCap="round";g.beginPath();
g.moveTo(px+eo-1.4*s,ey+3.1*s);g.quadraticCurveTo(px+eo,ey+4.2*s,px+eo+1.4*s,ey+3.1*s);g.stroke();
// (no rosy cheeks — the real Mellows have a minimal dots + smile face)
// neckerchief (job colour) — small & tidy, sits like a collar just under the face
const sy=bodyTop+H*0.63;
fillP(g,[[px-bw*0.92,sy-1*s],[px+bw*0.92,sy-1*s],[px+bw*0.74,sy+2.4*s],[px-bw*0.74,sy+2.4*s]],scarf,1.4*s);
fillP(g,[[px+face*bw*0.5,sy+0.2*s],[px+face*bw*0.98,sy+1.8*s],[px+face*bw*0.7,sy+5*s],[px+face*bw*0.36,sy+2.6*s]],dark(scarf,0.12),1.2*s);
// thin stick arms (Life of Mellow) — back arm rests at the side, front arm reaches out to hold the tool
const armY=bodyTop+H*0.52,hasTool=tool&&tool!=="none";
g.strokeStyle=INK;g.lineWidth=1.9*s;g.lineCap="round";
g.beginPath();g.moveTo(px-face*bw*0.82,armY);g.lineTo(px-face*(bw*0.82+2.6*s),armY+3.6*s);g.stroke();
g.beginPath();g.moveTo(px+face*bw*0.82,armY);
if(hasTool)g.lineTo(px+face*(bw+2*s),bodyTop+H*0.5); else g.lineTo(px+face*(bw*0.82+2.6*s),armY+3.6*s);
g.stroke();
if(hasTool)artTool(g,px+face*(bw+2*s),bodyTop+H*0.5,tool,s,face);
}
function artTool(g,x,y,tool,s,f){g.save();g.lineCap="round";g.lineJoin="round";
const handle=(x1,y1,x2,y2)=>{g.strokeStyle="#7a5230";g.lineWidth=2*s;g.beginPath();g.moveTo(x1,y1);g.lineTo(x2,y2);g.stroke();
g.strokeStyle=INK;g.lineWidth=2.9*s;g.globalAlpha=0.001;g.stroke();g.globalAlpha=1;};
if(tool==="axe"){handle(x-2*s,y+6*s,x+2*s,y-8*s);fillP(g,[[x+1*s,y-10*s],[x+7*f*s,y-9*s],[x+6*f*s,y-4*s],[x+1*s,y-5*s]],"#c7ccd2",1.3*s);}
else if(tool==="pick"){handle(x-2*s,y+6*s,x+2*s,y-8*s);g.strokeStyle=INK;g.lineWidth=3.4*s;g.beginPath();g.moveTo(x-4*f*s,y-6*s);g.quadraticCurveTo(x+2*s,y-11*s,x+8*f*s,y-8*s);g.stroke();
g.strokeStyle="#b0b6bd";g.lineWidth=2*s;g.stroke();}
else if(tool==="hammer"){handle(x-1*s,y+6*s,x+2*s,y-7*s);fillP(g,[[x-2*s,y-11*s],[x+5*f*s,y-11*s],[x+5*f*s,y-7*s],[x-2*s,y-7*s]],"#9098a0",1.3*s);}
else if(tool==="bow"){g.strokeStyle=INK;g.lineWidth=2.4*s;g.beginPath();g.arc(x+2*f*s,y-2*s,8*s,-1.1,1.1);g.stroke();
g.strokeStyle="#8a5a2e";g.lineWidth=1.4*s;g.stroke();g.strokeStyle="#e8e0c8";g.lineWidth=1*s;g.beginPath();g.moveTo(x+2*f*s-6*s,y-9*s);g.lineTo(x+2*f*s-6*s,y+5*s);g.stroke();}
else if(tool==="rod"){handle(x-3*s,y+7*s,x+5*f*s,y-10*s);g.strokeStyle="rgba(255,255,255,.7)";g.lineWidth=0.9*s;g.beginPath();g.moveTo(x+5*f*s,y-10*s);g.lineTo(x+7*f*s,y-2*s);g.stroke();}
else if(tool==="scythe"){handle(x-2*s,y+7*s,x+3*s,y-9*s);g.strokeStyle=INK;g.lineWidth=2.8*s;g.beginPath();g.arc(x+3*f*s,y-9*s,6*s,-0.3,1.4);g.stroke();g.strokeStyle="#c7ccd2";g.lineWidth=1.6*s;g.stroke();}
else if(tool==="staff"){handle(x,y+7*s,x,y-10*s);g.strokeStyle="#8a6b45";g.lineWidth=1.6*s;g.beginPath();g.arc(x+1*f*s,y-10*s,3*s,-1.6,1.2);g.stroke();}
else if(tool==="saw"){handle(x-3*s,y+6*s,x-3*s,y-2*s);fillP(g,[[x-4*s,y-2*s],[x+8*f*s,y-6*s],[x+8*f*s,y-3*s],[x-4*s,y+1*s]],"#c7ccd2",1.3*s);}
g.restore();}
function artBubble(g,x,y,paint){const w=11,h=9,cy=y-15;
g.beginPath();
const r=4,x0=x-w,y0=cy-h,x1=x+w,y1=cy+h;
g.moveTo(x0+r,y0);g.arcTo(x1,y0,x1,y1,r);g.arcTo(x1,y1,x0,y1,r);g.arcTo(x0,y1,x0,y0,r);g.arcTo(x0,y0,x1,y0,r);g.closePath();
g.fillStyle="#fdf6e3";g.fill();inkStroke(g,1.5);
fillP(g,[[x-3,cy+h-1],[x+3,cy+h-1],[x,cy+h+5]],"#fdf6e3",1.5);
paint(g,x,cy,6);}
/* --- icon glyphs (used in-world small + UI) --- */
function artResIcon(g,key,x,y,r){const u=r/8;
switch(key){
case "wood":fillE(g,x,y,r*0.95,r*0.5,"#b3814d",1.2);fillE(g,x+r*0.55,y-r*0.3,r*0.34,r*0.44,"#c99a5a",1.1);
g.strokeStyle="#8a5f38";g.lineWidth=u;g.beginPath();g.ellipse(x+r*0.55,y-r*0.3,r*0.16,r*0.24,0,0,7);g.stroke();break;
case "planks":{const bw2=r*1.8;for(let i=0;i<3;i++){const yy=y-r*0.6+i*r*0.55;
fillP(g,[[x-bw2/2,yy],[x+bw2/2,yy],[x+bw2/2,yy+r*0.34],[x-bw2/2,yy+r*0.34]],"#d3a96a",1.1);
g.strokeStyle="#a9784a";g.lineWidth=u*0.7;g.beginPath();g.moveTo(x-bw2/2+u,yy+r*0.17);g.lineTo(x+bw2/2-u,yy+r*0.17);g.stroke();}}break;
case "stone":fillP(g,[[x-r,y+r*0.3],[x-r*0.5,y-r*0.7],[x+r*0.5,y-r*0.8],[x+r,y+r*0.1],[x,y+r*0.7]],"#9098a0",1.2);
g.fillStyle="#b3bac1";poly(g,[[x-r*0.5,y-r*0.7],[x+r*0.5,y-r*0.8],[x,y-r*0.2]]);g.fill();break;
case "iron":fillP(g,[[x-r,y+r*0.3],[x-r*0.4,y-r*0.8],[x+r*0.6,y-r*0.7],[x+r,y+r*0.2],[x,y+r*0.7]],"#5c5a5f",1.2);
g.fillStyle="#d9a24a";circ(g,x-r*0.2,y-r*0.2,u*1.3);g.fill();circ(g,x+r*0.4,y+r*0.1,u);g.fill();break;
case "fruit":fillC(g,x-r*0.35,y+r*0.1,r*0.5,"#c1443e",1.2);fillC(g,x+r*0.35,y+r*0.1,r*0.5,"#c1443e",1.2);fillC(g,x,y-r*0.3,r*0.5,"#c1443e",1.2);
g.fillStyle="#5aa03e";poly(g,[[x-u,y-r*0.7],[x+r*0.4,y-r],[x+u,y-r*0.4]]);g.fill();break;
case "meat":fillE(g,x+r*0.1,y,r*0.7,r*0.55,"#b96a44",1.2);g.strokeStyle="#f0e4d0";g.lineWidth=u*1.6;g.lineCap="round";
g.beginPath();g.moveTo(x-r*0.4,y+r*0.3);g.lineTo(x-r,y+r*0.7);g.stroke();g.fillStyle="#f0e4d0";circ(g,x-r,y+r*0.6,u*1.2);g.fill();circ(g,x-r*0.8,y+r*0.85,u*1.2);g.fill();break;
case "fish":artFish(g,x,y,r/9);break;
case "milk":fillP(g,[[x-r*0.45,y-r*0.35],[x-r*0.22,y-r*0.72],[x+r*0.22,y-r*0.72],[x+r*0.45,y-r*0.35],[x+r*0.45,y+r*0.7],[x-r*0.45,y+r*0.7]],"#f6f2e8",1.2);
g.fillStyle="#7fb8d8";g.fillRect(x-r*0.45,y+r*0.02,r*0.9,r*0.32);g.fillStyle="#cfc3ac";g.fillRect(x-r*0.2,y-r*0.85,r*0.4,r*0.18);break;
case "crops":g.strokeStyle="#a97f2e";g.lineWidth=u*1.4;g.lineCap="round";g.beginPath();g.moveTo(x,y+r*0.8);g.lineTo(x,y-r*0.9);g.stroke();
g.fillStyle="#e0b84a";for(let i=0;i<3;i++){const yy=y-r*0.5+i*r*0.5;g.beginPath();g.ellipse(x-r*0.35,yy,r*0.18,r*0.3,0.5,0,7);g.fill();g.beginPath();g.ellipse(x+r*0.35,yy,r*0.18,r*0.3,-0.5,0,7);g.fill();}break;
case "corn":g.save();g.translate(x,y);fillE(g,0,r*0.05,r*0.42,r*0.82,"#f2c94c",1.2);
g.fillStyle="#d9a72e";for(let a=-1;a<=1;a++)for(let b2=-2;b2<=2;b2++){circ(g,a*r*0.22,b2*r*0.28,r*0.07);g.fill();}
g.fillStyle="#6aa63e";poly(g,[[-r*0.4,r*0.2],[-r*0.75,-r*0.5],[-r*0.15,r*0.6]]);g.fill();
poly(g,[[r*0.4,r*0.2],[r*0.75,-r*0.5],[r*0.15,r*0.6]]);g.fill();g.restore();break;
case "pumpkin":fillE(g,x,y+r*0.1,r*0.92,r*0.75,"#e08a3a",1.2);
g.strokeStyle="#b96a24";g.lineWidth=u*0.9;for(const dx of [-0.45,0,0.45]){g.beginPath();g.ellipse(x+dx*r*0.7,y+r*0.1,r*0.2,r*0.7,0,-1.1,1.1);g.stroke();}
g.strokeStyle="#5f8a3a";g.lineWidth=u*1.4;g.lineCap="round";g.beginPath();g.moveTo(x,y-r*0.6);g.lineTo(x+r*0.12,y-r*0.95);g.stroke();break;
case "carrot":g.save();g.translate(x,y);fillP(g,[[-r*0.4,-r*0.35],[r*0.4,-r*0.35],[0,r*0.9]],"#e07a3a",1.2);
g.strokeStyle="#eaa06a";g.lineWidth=u*0.7;for(const yy of [-0.15,0.15,0.45]){g.beginPath();g.moveTo(-r*0.32,yy*r);g.lineTo(r*0.32,yy*r);g.stroke();}
g.strokeStyle="#5f9e3a";g.lineWidth=u*1.3;g.lineCap="round";for(const a of [-0.5,0,0.5]){g.beginPath();g.moveTo(0,-r*0.32);g.lineTo(Math.sin(a)*r*0.5,-r*0.32-Math.cos(a)*r*0.55);g.stroke();}g.restore();break;
case "cabbage":fillC(g,x,y,r*0.82,"#7fae54",1.2);g.strokeStyle="#5f8a3a";g.lineWidth=u*0.9;
for(const a of [0.3,1.4,2.5,3.6,4.7,5.8]){g.beginPath();g.arc(x,y,r*0.55,a,a+0.9);g.stroke();}
g.fillStyle="#a3c777";circ(g,x-r*0.12,y-r*0.1,r*0.3);g.fill();break;
case "seed":g.fillStyle="#8a6b45";for(const p of [[-r*0.3,r*0.15],[r*0.25,-r*0.1],[0,r*0.45]]){g.save();g.translate(x+p[0],y+p[1]);g.rotate(0.6);
g.beginPath();g.ellipse(0,0,r*0.24,r*0.42,0,0,7);g.fill();inkStroke(g,0.9);g.restore();}break;
case "meals":g.fillStyle="#e0d6c0";g.beginPath();g.arc(x,y,r*0.85,0,Math.PI);g.fill();inkStroke(g,1.2);
g.fillStyle="#c0563e";g.beginPath();g.ellipse(x,y,r*0.72,r*0.24,0,0,Math.PI,true);g.fill();
g.strokeStyle="rgba(180,140,90,.9)";g.lineWidth=u;g.beginPath();g.moveTo(x-r*0.3,y-r*0.3);g.quadraticCurveTo(x-r*0.1,y-r*0.7,x-r*0.3,y-r);g.moveTo(x+r*0.3,y-r*0.3);g.quadraticCurveTo(x+r*0.5,y-r*0.7,x+r*0.3,y-r);g.stroke();break;
case "herbs":artHerb(g,x,y+r*0.6,r/11);break;
case "contra":g.strokeStyle="#6a9e52";g.lineWidth=u*1.2;g.lineCap="round";g.beginPath();g.moveTo(x,y+r*0.85);g.lineTo(x,y-r*0.35);g.stroke();
g.fillStyle="#cf7fb0";for(const sgn of [-1,1]){g.save();g.translate(x,y+r*0.1);g.rotate(sgn*0.55);g.beginPath();g.ellipse(sgn*r*0.33,0,r*0.32,r*0.16,0,0,7);g.fill();g.restore();}
g.fillStyle="#ecb3d6";circ(g,x,y-r*0.55,r*0.28);g.fill();inkStroke(g,1);g.fillStyle="#fff";circ(g,x-r*0.08,y-r*0.63,r*0.08);g.fill();break;
case "potion":g.fillStyle="#8e5cc0";g.beginPath();g.arc(x,y+r*0.15,r*0.62,Math.PI*0.85,Math.PI*0.15);g.closePath();g.fill();inkStroke(g,1.2);
g.fillStyle="#cfc6b8";g.fillRect(x-u*0.8,y-r,u*1.6,r*0.7);inkStroke(g,1);break;
case "wine":fillP(g,[[x-r*0.28,y-r],[x+r*0.28,y-r],[x+r*0.28,y+r*0.7],[x-r*0.28,y+r*0.7]],"#5a7a3a",1.2);
g.fillStyle="#8a2f3a";g.fillRect(x-r*0.24,y+r*0.05,r*0.48,r*0.55);g.fillStyle=INK;g.fillRect(x-u*0.7,y-r-u*1.2,u*1.4,u*1.4);break;
case "firewood":{const fl=g.createLinearGradient(x,y+r,x,y-r);fl.addColorStop(0,"#f2c14a");fl.addColorStop(0.6,"#e8862f");fl.addColorStop(1,"#d5432a");
poly(g,[[x-r*0.6,y+r*0.7],[x,y-r],[x+r*0.6,y+r*0.7],[x+r*0.25,y-r*0.1],[x-r*0.25,y-r*0.1]]);g.fillStyle=fl;g.fill();inkStroke(g,1.1);
g.fillStyle="#ffe08a";poly(g,[[x-u,y+r*0.4],[x+u*0.5,y-r*0.5],[x+u*1.5,y+r*0.4]]);g.fill();}break;
case "tools":g.strokeStyle="#7a5230";g.lineWidth=u*1.7;g.lineCap="round";g.beginPath();g.moveTo(x-r*0.5,y+r*0.6);g.lineTo(x+r*0.2,y-r*0.3);g.stroke();
g.save();g.translate(x+r*0.35,y-r*0.5);g.rotate(-0.7);fillP(g,[[-r*0.5,-u*1.4],[r*0.5,-u*1.4],[r*0.5,u*1.4],[-r*0.5,u*1.4]],"#9098a0",1.1);g.restore();break;
case "wool":fillC(g,x,y,r*0.82,"#efe7d6",1.2);g.strokeStyle="#cfc3ac";g.lineWidth=u;
for(const a of [0.4,1.2,2.0,2.8]){g.beginPath();g.arc(x,y,r*0.55,a,a+1.6);g.stroke();}break;
case "leather":fillP(g,[[x-r*0.8,y-r*0.5],[x+r*0.7,y-r*0.7],[x+r*0.8,y+r*0.5],[x-r*0.6,y+r*0.7]],"#a06a3e",1.2);break;
case "axe":g.strokeStyle="#7a5230";g.lineWidth=u*1.7;g.lineCap="round";g.beginPath();g.moveTo(x-r*0.45,y+r*0.75);g.lineTo(x+r*0.35,y-r*0.6);g.stroke();
g.save();g.translate(x+r*0.4,y-r*0.55);g.rotate(-0.5);fillP(g,[[-u*0.6,-r*0.5],[r*0.55,-r*0.3],[r*0.55,r*0.05],[-u*0.6,r*0.15]],"#c7ccd2",1.1);g.restore();break;
case "pickaxe":g.strokeStyle="#7a5230";g.lineWidth=u*1.7;g.lineCap="round";g.beginPath();g.moveTo(x,y+r*0.85);g.lineTo(x,y-r*0.45);g.stroke();
g.strokeStyle=INK;g.lineWidth=u*2.4;g.beginPath();g.moveTo(x-r*0.8,y-r*0.05);g.quadraticCurveTo(x,y-r*0.78,x+r*0.8,y-r*0.05);g.stroke();
g.strokeStyle="#b0b6bd";g.lineWidth=u*1.3;g.stroke();break;
case "bow":{g.strokeStyle="#8a5a2e";g.lineWidth=u*1.5;g.lineCap="round";const bx=x-r*0.2;g.beginPath();g.arc(bx,y,r*0.85,-1.0,1.0);g.stroke();
g.strokeStyle="#e8e0c8";g.lineWidth=u*0.7;g.beginPath();g.moveTo(bx+r*0.85*Math.cos(-1.0),y+r*0.85*Math.sin(-1.0));g.lineTo(bx+r*0.85*Math.cos(1.0),y+r*0.85*Math.sin(1.0));g.stroke();
g.strokeStyle="#9098a0";g.lineWidth=u*0.9;g.beginPath();g.moveTo(x-r*0.4,y);g.lineTo(x+r*0.7,y);g.stroke();
fillP(g,[[x+r*0.78,y],[x+r*0.48,y-r*0.24],[x+r*0.48,y+r*0.24]],"#c7ccd2",0);}break;
case "basket":fillP(g,[[x-r*0.72,y-r*0.15],[x+r*0.72,y-r*0.15],[x+r*0.52,y+r*0.72],[x-r*0.52,y+r*0.72]],"#c2914e",1.2);
g.strokeStyle="#8a5f38";g.lineWidth=u*0.7;for(const dx of [-0.35,0,0.35]){g.beginPath();g.moveTo(x+dx*r*0.85,y-r*0.15);g.lineTo(x+dx*r*0.6,y+r*0.72);g.stroke();}
g.beginPath();g.moveTo(x-r*0.66,y+r*0.28);g.lineTo(x+r*0.66,y+r*0.28);g.stroke();
g.strokeStyle="#a06a3e";g.lineWidth=u*1.1;g.beginPath();g.arc(x,y-r*0.15,r*0.5,Math.PI,0);g.stroke();
g.fillStyle="#c1443e";circ(g,x-r*0.18,y-r*0.02,u*1.1);g.fill();circ(g,x+r*0.16,y-r*0.02,u*1.1);g.fill();break;
case "hoe":g.strokeStyle="#7a5230";g.lineWidth=u*1.6;g.lineCap="round";g.beginPath();g.moveTo(x-r*0.5,y+r*0.7);g.lineTo(x+r*0.35,y-r*0.55);g.stroke();
g.strokeStyle=INK;g.lineWidth=u*2.6;g.beginPath();g.moveTo(x+r*0.3,y-r*0.55);g.lineTo(x+r*0.75,y-r*0.35);g.stroke();
g.strokeStyle="#9098a0";g.lineWidth=u*1.5;g.stroke();break;
default:fillC(g,x,y,r*0.6,"#b98a4a",1.2);}}
function artBldIcon(g,key,x,y,r){const u=r/8;
switch(key){
case "storage":fillP(g,[[x-r*0.7,y-r*0.5],[x+r*0.7,y-r*0.5],[x+r*0.7,y+r*0.6],[x-r*0.7,y+r*0.6]],"#c99a5a",1.3);
g.strokeStyle=INK;g.lineWidth=u;g.beginPath();g.moveTo(x-r*0.7,y);g.lineTo(x+r*0.7,y);g.moveTo(x,y-r*0.5);g.lineTo(x,y+r*0.6);g.stroke();break;
case "house":fillP(g,[[x-r*0.6,y-r*0.1],[x+r*0.6,y-r*0.1],[x+r*0.6,y+r*0.7],[x-r*0.6,y+r*0.7]],"#e8d9b0",1.3);
fillP(g,[[x-r*0.8,y-r*0.1],[x,y-r*0.9],[x+r*0.8,y-r*0.1]],"#b06a44",1.3);
g.fillStyle=INK;g.fillRect(x-r*0.15,y+r*0.2,r*0.3,r*0.5);break;
case "fire":artResIcon(g,"firewood",x,y,r);break;
case "saw":g.strokeStyle="#7a5230";g.lineWidth=u*1.5;g.beginPath();g.moveTo(x-r*0.8,y+r*0.5);g.lineTo(x-r*0.4,y+r*0.5);g.stroke();
fillP(g,[[x-r*0.5,y+r*0.5],[x+r*0.8,y-r*0.4],[x+r*0.8,y-r*0.05],[x-r*0.5,y+r*0.85]],"#c7ccd2",1.2);break;
case "hammer":artResIcon(g,"tools",x,y,r);break;
case "flask":artResIcon(g,"potion",x,y,r);break;
case "wine":artResIcon(g,"wine",x,y,r);break;
case "pot":g.fillStyle="#6b6b72";g.beginPath();g.arc(x,y+r*0.1,r*0.7,0,Math.PI);g.fill();inkStroke(g,1.2);
fillP(g,[[x-r*0.75,y-r*0.05],[x+r*0.75,y-r*0.05],[x+r*0.75,y+r*0.2],[x-r*0.75,y+r*0.2]],"#7a7a82",1.1);break;
case "crops":artResIcon(g,"crops",x,y,r);break;
case "milk":artResIcon(g,"milk",x,y,r);break;
case "cleaver":artResIcon(g,"meat",x-r*0.2,y,r*0.8);g.strokeStyle="#8a929a";g.lineWidth=r*0.18;g.lineCap="round";g.beginPath();g.moveTo(x+r*0.2,y-r*0.7);g.lineTo(x+r*0.7,y+r*0.6);g.stroke();break;
case "sheep":artSheep(g,x,y+r*0.5,r/10,1);break;
case "cross":fillC(g,x,y,r*0.82,"#f4ecd4",1.2);g.fillStyle="#cf5a5a";g.fillRect(x-r*0.15,y-r*0.5,r*0.3,r);g.fillRect(x-r*0.5,y-r*0.15,r,r*0.3);break;
case "baby":fillC(g,x,y-r*0.1,r*0.55,"#f4ecd4",1.2);g.fillStyle=INK;circ(g,x-r*0.18,y-r*0.15,u*0.9);g.fill();circ(g,x+r*0.18,y-r*0.15,u*0.9);g.fill();
fillP(g,[[x-r*0.55,y+r*0.7],[x,y+r*0.2],[x+r*0.55,y+r*0.7]],"#d98ab4",1.2);break;
case "school":fillP(g,[[x-r*0.9,y-r*0.5],[x,y-r*0.25],[x,y+r*0.7],[x-r*0.9,y+r*0.45]],"#f3ecd8",1.2);
fillP(g,[[x+r*0.9,y-r*0.5],[x,y-r*0.25],[x,y+r*0.7],[x+r*0.9,y+r*0.45]],"#e8dcc0",1.2);
g.strokeStyle="#b98a4a";g.lineWidth=u*0.7;g.beginPath();g.moveTo(x-r*0.7,y-r*0.15);g.lineTo(x-r*0.15,y+r*0.05);g.moveTo(x-r*0.7,y+r*0.15);g.lineTo(x-r*0.15,y+r*0.35);
g.moveTo(x+r*0.7,y-r*0.15);g.lineTo(x+r*0.15,y+r*0.05);g.moveTo(x+r*0.7,y+r*0.15);g.lineTo(x+r*0.15,y+r*0.35);g.stroke();break;
case "grave":g.beginPath();g.moveTo(x-r*0.5,y+r*0.7);g.lineTo(x-r*0.5,y-r*0.3);g.arc(x,y-r*0.3,r*0.5,Math.PI,0);g.lineTo(x+r*0.5,y+r*0.7);g.closePath();g.fillStyle="#cccfd3";g.fill();inkStroke(g,1.2);
g.strokeStyle="#9aa0a6";g.lineWidth=r*0.12;g.lineCap="round";g.beginPath();g.moveTo(x,y-r*0.5);g.lineTo(x,y+r*0.3);g.moveTo(x-r*0.22,y-r*0.2);g.lineTo(x+r*0.22,y-r*0.2);g.stroke();break;
case "wolf":{const gr="#8a8a94";fillP(g,[[x-r*0.5,y-r*0.75],[x-r*0.2,y-r*0.25],[x-r*0.6,y-r*0.2]],gr,1);fillP(g,[[x+r*0.5,y-r*0.75],[x+r*0.2,y-r*0.25],[x+r*0.6,y-r*0.2]],gr,1); // ears
fillP(g,[[x-r*0.6,y-r*0.35],[x+r*0.6,y-r*0.35],[x+r*0.4,y+r*0.3],[x,y+r*0.7],[x-r*0.4,y+r*0.3]],gr,1.3); // head+snout
g.fillStyle="#f0f0f4";circ(g,x,y+r*0.55,r*0.14);g.fill();g.fillStyle=INK;circ(g,x-r*0.22,y-r*0.02,u*0.9);g.fill();circ(g,x+r*0.22,y-r*0.02,u*0.9);g.fill();circ(g,x,y+r*0.32,u*1.1);g.fill();}break;
case "boat":fillP(g,[[x-r*0.8,y+r*0.1],[x+r*0.8,y+r*0.1],[x+r*0.5,y+r*0.6],[x-r*0.5,y+r*0.6]],"#8a5f38",1.2);
g.strokeStyle=INK;g.lineWidth=u;g.beginPath();g.moveTo(x,y+r*0.1);g.lineTo(x,y-r*0.7);g.stroke();fillP(g,[[x,y-r*0.7],[x+r*0.5,y-r*0.2],[x,y-r*0.15]],"#d5715a",1.1);break;
case "bridge":g.strokeStyle="#a98a5a";g.lineWidth=u*1.6;g.beginPath();g.arc(x,y+r*0.5,r*0.8,Math.PI,0);g.stroke();
g.strokeStyle=INK;g.lineWidth=u*0.9;g.beginPath();g.moveTo(x-r*0.8,y-r*0.3);g.lineTo(x+r*0.8,y-r*0.3);g.stroke();break;
case "flower":{var fc=["#e0607a","#f2b84a","#6aa9e0"];g.strokeStyle="#5f8a3a";g.lineWidth=u;g.beginPath();g.moveTo(x,y+r*0.7);g.lineTo(x,y-r*0.2);g.stroke();
for(var fi=0;fi<3;fi++){var fx=x+(fi-1)*r*0.5,fy=y-r*0.2-Math.abs(fi-1)*r*0.1;g.fillStyle=fc[fi];for(var fk=0;fk<5;fk++){var fa=fk/5*6.283;circ(g,fx+Math.cos(fa)*r*0.22,fy+Math.sin(fa)*r*0.22,r*0.16);g.fill();}g.fillStyle="#fff3c0";circ(g,fx,fy,r*0.11);g.fill();}break;}
case "lantern":g.strokeStyle="#5a4835";g.lineWidth=u*1.3;g.lineCap="round";g.beginPath();g.moveTo(x,y+r*0.8);g.lineTo(x,y-r*0.3);g.stroke();
fillP(g,[[x-r*0.4,y-r*0.3],[x+r*0.4,y-r*0.3],[x+r*0.4,y+r*0.35],[x-r*0.4,y+r*0.35]],"#ffe89a",1.1);
fillP(g,[[x-r*0.5,y-r*0.3],[x+r*0.5,y-r*0.3],[x+r*0.3,y-r*0.65],[x-r*0.3,y-r*0.65]],"#6b5640",1);break;
case "well":fillE(g,x,y+r*0.3,r*0.7,r*0.35,"#9aa0a6",1.3);g.fillStyle="#3a4550";g.beginPath();g.ellipse(x,y+r*0.25,r*0.45,r*0.2,0,0,7);g.fill();
g.strokeStyle="#6b5640";g.lineWidth=u*1.1;g.beginPath();g.moveTo(x-r*0.5,y+r*0.2);g.lineTo(x-r*0.45,y-r*0.6);g.moveTo(x+r*0.5,y+r*0.2);g.lineTo(x+r*0.45,y-r*0.6);g.stroke();
fillP(g,[[x-r*0.65,y-r*0.55],[x+r*0.65,y-r*0.55],[x+r*0.35,y-r*0.95],[x-r*0.35,y-r*0.95]],"#a35a4a",1.1);break;
case "bench":g.strokeStyle="#5a4835";g.lineWidth=u*1.1;g.lineCap="round";g.beginPath();g.moveTo(x-r*0.55,y+r*0.1);g.lineTo(x-r*0.55,y+r*0.6);g.moveTo(x+r*0.55,y+r*0.1);g.lineTo(x+r*0.55,y+r*0.6);g.stroke();
fillP(g,[[x-r*0.7,y],[x+r*0.7,y],[x+r*0.7,y+r*0.18],[x-r*0.7,y+r*0.18]],"#b0895a",1.1);
g.strokeStyle="#5a4835";g.lineWidth=u*0.9;g.beginPath();g.moveTo(x-r*0.55,y);g.lineTo(x-r*0.55,y-r*0.55);g.moveTo(x+r*0.55,y);g.lineTo(x+r*0.55,y-r*0.55);g.stroke();
fillP(g,[[x-r*0.7,y-r*0.5],[x+r*0.7,y-r*0.5],[x+r*0.7,y-r*0.32],[x-r*0.7,y-r*0.32]],"#b0895a",1);break;
case "statue":fillP(g,[[x-r*0.5,y+r*0.7],[x+r*0.5,y+r*0.7],[x+r*0.4,y+r*0.3],[x-r*0.4,y+r*0.3]],"#aab0b6",1.2);
g.fillStyle="#c4c9ce";g.beginPath();g.moveTo(x-r*0.32,y+r*0.3);g.quadraticCurveTo(x-r*0.42,y-r*0.7,x,y-r*0.7);g.quadraticCurveTo(x+r*0.42,y-r*0.7,x+r*0.32,y+r*0.3);g.closePath();g.fill();inkStroke(g,1.2);
g.fillStyle="#868c92";circ(g,x-r*0.12,y-r*0.35,u*0.7);g.fill();circ(g,x+r*0.12,y-r*0.35,u*0.7);g.fill();break;
default:fillP(g,[[x-r*0.6,y-r*0.5],[x+r*0.6,y-r*0.5],[x+r*0.6,y+r*0.5],[x-r*0.6,y+r*0.5]],"#c99a5a",1.2);}}
function artSeasonIcon(g,name,x,y,r){
if(name==="Spring"){artHerb(g,x,y+r*0.7,r/9);}
else if(name==="Summer"){fillC(g,x,y,r*0.5,"#f2b83a",1.4);g.strokeStyle="#f2b83a";g.lineWidth=r*0.16;g.lineCap="round";
for(let i=0;i<8;i++){const a=i/8*7;g.beginPath();g.moveTo(x+Math.cos(a)*r*0.72,y+Math.sin(a)*r*0.72);g.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);g.stroke();}}
else if(name==="Fall"){fillP(g,[[x,y-r],[x+r*0.7,y-r*0.2],[x+r*0.4,y+r*0.8],[x-r*0.4,y+r*0.8],[x-r*0.7,y-r*0.2]],"#d5772f",1.3);
g.strokeStyle="#8a4e1f";g.lineWidth=r*0.1;g.beginPath();g.moveTo(x,y-r*0.6);g.lineTo(x,y+r*0.8);g.stroke();}
else{g.strokeStyle="#7fb8d8";g.lineWidth=r*0.16;g.lineCap="round";for(let i=0;i<6;i++){const a=i/6*7;g.beginPath();g.moveTo(x,y);g.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);g.stroke();}}}
function artMelHead(g,x,y,r){fillE(g,x,y+r*0.15,r*0.78,r*0.9,"#f3ecd8",1.3);
g.fillStyle=INK;circ(g,x-r*0.24,y,r*0.13);g.fill();circ(g,x+r*0.24,y,r*0.13);g.fill();
fillP(g,[[x-r*0.7,y+r*0.5],[x+r*0.7,y+r*0.5],[x+r*0.55,y+r*0.72],[x-r*0.55,y+r*0.72]],"#6ab04a",1.1);}
function artNeedIcon(g,key,x,y,r){
if(key==="home")artBldIcon(g,"house",x,y,r);
else if(key==="idle"){g.fillStyle=INK;g.font="bold "+(r*1.7)+"px sans-serif";g.textAlign="center";g.textBaseline="middle";g.fillText("?",x,y);}
else if(key==="preg"){g.beginPath();g.moveTo(x,y+r*0.7);g.bezierCurveTo(x-r,y-r*0.2,x-r*0.4,y-r*0.95,x,y-r*0.3);
g.bezierCurveTo(x+r*0.4,y-r*0.95,x+r,y-r*0.2,x,y+r*0.7);g.closePath();g.fillStyle="#d98ab4";g.fill();inkStroke(g,1.2);}
else if(key==="cold")artSeasonIcon(g,"Winter",x,y,r);
else if(key==="food")artResIcon(g,"meat",x,y,r);
else if(key==="sick")artBldIcon(g,"cross",x,y,r);
else if(key==="tool")artResIcon(g,"tools",x,y,r);
else artMelHead(g,x,y,r);}
const _ic={};
function icon(kind,key,px){const k=kind+":"+key+":"+px;if(_ic[k])return _ic[k];
const c=document.createElement("canvas");c.width=px;c.height=px;const g=c.getContext("2d");
const r=px*0.36,cx=px/2,cy=px/2;
if(kind==="res")artResIcon(g,key,cx,cy,r);
else if(kind==="bld")artBldIcon(g,BUILD[key].icon,cx,cy,r);
else if(kind==="season")artSeasonIcon(g,key,cx,cy,r);
else if(kind==="need")artNeedIcon(g,key,cx,cy,r);
else if(kind==="mel")artMelHead(g,cx,cy,r);
else if(kind==="job"){const j=JOBS[key];
if(j.res)artResIcon(g,j.res,cx,cy,r);
else if(key==="builder"||key==="craftsman")artResIcon(g,"tools",cx,cy,r);
else if(key==="herbalist")artResIcon(g,"potion",cx,cy,r);
else if(key==="brewer")artResIcon(g,"wine",cx,cy,r);
else if(key==="cook")artBldIcon(g,"pot",cx,cy,r);
else if(key==="doctor")artBldIcon(g,"cross",cx,cy,r);
else if(key==="midwife")artBldIcon(g,"baby",cx,cy,r);
else artMelHead(g,cx,cy,r);}
const u=c.toDataURL();_ic[k]=u;return u;}
function img(kind,key,cls){return `<img class="ic${cls?' '+cls:''}" src="${icon(kind,key,44)}">`;}
/* ================= RENDER ================= */
function resize(){DPR=Math.min(2,window.devicePixelRatio||1);VW=window.innerWidth;VH=window.innerHeight;
cv.width=VW*DPR;cv.height=VH*DPR;cv.style.width=VW+"px";cv.style.height=VH+"px";ctx.setTransform(DPR,0,0,DPR,0,0);}
function seasonTint(){switch(season()){case "Spring":return null;case "Summer":return null;
case "Fall":return "rgba(216,150,70,0.07)";case "Winter":return "rgba(200,225,255,0.19)";}}
// gentle time-of-day wash: warm dawn → clear noon → amber dusk → soft-cool "night" (never fully dark). ~90s per cycle.
const TOD_LEN=90, TOD_KEYS=[[0.00,247,188,158,0.12],[0.27,255,255,255,0.0],[0.5,240,158,92,0.11],[0.72,120,142,198,0.14],[1.0,247,188,158,0.12]];
function timeTint(){const p=((S.tsec||0)%TOD_LEN)/TOD_LEN;let a=TOD_KEYS[0],b=TOD_KEYS[TOD_KEYS.length-1];
for(let i=0;i<TOD_KEYS.length-1;i++)if(p>=TOD_KEYS[i][0]&&p<TOD_KEYS[i+1][0]){a=TOD_KEYS[i];b=TOD_KEYS[i+1];break;}
const t=(p-a[0])/((b[0]-a[0])||1),L=(x,y)=>x+(y-x)*t;
return `rgba(${Math.round(L(a[1],b[1]))},${Math.round(L(a[2],b[2]))},${Math.round(L(a[3],b[3]))},${L(a[4],b[4]).toFixed(3)})`;}
function hash2(x,y){let n=(x*73856093)^(y*19349663);n=(n^(n>>>13))>>>0;return (n%1000)/1000;}
function drawTileBlock(gx,gy){
const p=worldToScreen(gx,gy),isle=ISLANDS[S.island],kind=S.tiles[gy][gx],winter=isWinter();
const hw=TILE_W/2,hh=TILE_H/2;
let topC,sideC;
if(kind==="water"){
const coastal=[[1,0],[-1,0],[0,1],[0,-1]].some(d=>{const nx=gx+d[0],ny=gy+d[1];return S.tiles[ny]&&S.tiles[ny][nx]==="land";});
topC=coastal?(winter?"#d8e6ee":"#accdd8"):(winter?"#c4d7e3":"#8db2cb");sideC=winter?"#a5bcca":"#7299b5";} // paler shallows at the shore
else{topC=((gx+gy)&1)?isle.grass:isle.grass2;sideC=isle.side;
if(winter){topC=blend(topC,"#eef6fc",0.55);sideC=blend(sideC,"#cdd8e0",0.4);}}
poly(ctx,[[p.x-hw,p.y],[p.x,p.y+hh],[p.x,p.y+hh+TILE_TH],[p.x-hw,p.y+TILE_TH]],dark(sideC,0.05));
poly(ctx,[[p.x+hw,p.y],[p.x,p.y+hh],[p.x,p.y+hh+TILE_TH],[p.x+hw,p.y+TILE_TH]],dark(sideC,0.18));
poly(ctx,[[p.x,p.y-hh],[p.x+hw,p.y],[p.x,p.y+hh],[p.x-hw,p.y]],topC);
ctx.strokeStyle=kind==="water"?"rgba(255,255,255,.14)":"rgba(50,70,30,.16)";ctx.lineWidth=1;ctx.stroke();
if(kind==="water"){ctx.strokeStyle="rgba(255,255,255,.3)";ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(p.x-hw*0.4,p.y);ctx.quadraticCurveTo(p.x,p.y-3,p.x+hw*0.4,p.y);ctx.stroke();return;}
// doodads (grass tufts / flowers) — deterministic
const r=hash2(gx,gy);
if(!winter){
if(r<0.55){ctx.strokeStyle=dark(topC,0.28);ctx.lineWidth=1.3;ctx.lineCap="round";
const ox=p.x+(r-0.3)*20,oy=p.y+(hash2(gy,gx)-0.5)*10;
for(const dx of [-3,0,3]){ctx.beginPath();ctx.moveTo(ox+dx,oy+2);ctx.lineTo(ox+dx*0.6,oy-3);ctx.stroke();}}
else if(r>0.9){const ox=p.x+(r-0.5)*16,oy=p.y+(hash2(gy,gx)-0.5)*8;
ctx.strokeStyle=dark(topC,0.2);ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(ox,oy+2);ctx.lineTo(ox,oy-2);ctx.stroke();
ctx.fillStyle=r>0.95?"#d98ab4":"#e6d34a";circ(ctx,ox,oy-3,1.6);ctx.fill();}
} else if(r<0.3){ctx.fillStyle="rgba(255,255,255,.6)";circ(ctx,p.x+(r-0.15)*20,p.y+2,1.4);ctx.fill();}
}
function islandCorners(pad){const hw=TILE_W/2,hh=TILE_H/2;
const A=worldToScreen(0,0),B=worldToScreen(GRID-1,0),C=worldToScreen(GRID-1,GRID-1),D=worldToScreen(0,GRID-1);
const pts=[[A.x,A.y-hh],[B.x+hw,B.y],[C.x,C.y+hh+TILE_TH],[D.x-hw,D.y]];
const cx=(pts[0][0]+pts[2][0])/2,cy=(pts[1][1]+pts[3][1])/2;
return pts.map(p=>{const dx=p[0]-cx,dy=p[1]-cy,d=Math.hypot(dx,dy)||1;return [p[0]+dx/d*pad,p[1]+dy/d*pad];});}
function render(){
const isle=ISLANDS[S.island],winter=isWinter();
ctx.clearRect(0,0,VW,VH);
// ocean (inaccessible boundary)
const oc1=winter?"#b0c2cf":"#84a3bb",oc2=winter?"#98afbe":"#6990af";
const og=ctx.createLinearGradient(0,0,0,VH);og.addColorStop(0,oc1);og.addColorStop(1,oc2);
ctx.fillStyle=og;ctx.fillRect(0,0,VW,VH);
ctx.save();ctx.translate(cam.x,cam.y);ctx.scale(cam.z,cam.z); // pan + zoom for the world
// (organic coastline now carved into the tile map — the ocean gradient + shallows show the shape; no square beach frame)
for(let gy=0;gy<GRID;gy++)for(let gx=0;gx<GRID;gx++){const p=worldToScreen(gx,gy);
const sx=p.x*cam.z+cam.x,sy=p.y*cam.z+cam.y,mg=TILE_W*cam.z;
if(sx<-mg||sx>VW+mg||sy<-mg||sy>VH+mg)continue;drawTileBlock(gx,gy);}
if(ui.placing){const g=screenToGrid(ui.hoverX||VW/2,ui.hoverY||VH/2),gx=Math.round(g.gx),gy=Math.round(g.gy),hw=TILE_W/2,hh=TILE_H/2;
let ptiles;if(BUILD[ui.placing].dock){const dp=dockPlace(gx,gy);ptiles=dp?[[dp.gx,dp.gy],[dp.wx,dp.wy]]:[[gx,gy]];}else ptiles=bldTiles(ui.placing,gx,gy);
for(const [tx,ty] of ptiles){if(tx<0||ty<0||tx>=GRID||ty>=GRID)continue;const p=worldToScreen(tx,ty);
poly(ctx,[[p.x,p.y-hh],[p.x+hw,p.y],[p.x,p.y+hh],[p.x-hw,p.y]],"rgba(255,230,150,0.55)");ctx.strokeStyle="#fff";ctx.lineWidth=2;ctx.stroke();}}
const draw=[];
for(const n of S.nodes)draw.push({d:n.gx+n.gy,t:"node",o:n});
for(const b of S.buildings)draw.push({d:b.gx+b.gy+0.15,t:"bld",o:b});
for(const gr of (S.graves||[]))draw.push({d:gr.gx+gr.gy+0.1,t:"grave",o:gr});
for(const m of S.mellows)draw.push({d:m.gx+m.gy+0.25,t:"mel",o:m});
for(const m of (S.newcomers||[]))draw.push({d:m.gx+m.gy+0.25,t:"newc",o:m});
for(const w of S.wolves)draw.push({d:w.gx+w.gy+0.25,t:"wolf",o:w});
draw.sort((a,b)=>a.d-b.d);
for(const it of draw){if(it.t==="node")renderNode(it.o,winter);
else if(it.t==="bld")renderBuilding(it.o);else if(it.t==="mel")renderMellow(it.o);
else if(it.t==="newc")renderNewcomer(it.o);
else if(it.t==="grave"){const p=worldToScreen(it.o.gx,it.o.gy);artGrave(ctx,p.x,p.y,it.o.seed);}
else if(it.t==="wolf"){const w=it.o,p=worldToScreen(w.gx,w.gy); // pups at the old adult size; adults 1.5× bigger
artWolf(ctx,p.x,p.y,wolfBaby(w)?0.72:1.08,w.face||(w.tx>w.gx?1:-1),w.state&&w.state!=="wild");}}
// hearts over homes that are expecting a baby
if(ui.anim)for(const b of S.buildings)if(b.built&&BUILD[b.type].house&&S.mellows.some(m=>m.home===b.id&&m.pregnant)){
const p=worldToScreen(b.gx,b.gy);artHeart(ctx,p.x+1,p.y-46+Math.sin(S.tsec*2+b.id)*2,3.4,"rgba(224,110,120,.92)");}
ctx.restore(); // end world transform
const st=seasonTint();if(st){ctx.fillStyle=st;ctx.fillRect(0,0,VW,VH);}
const tt=timeTint();if(tt){ctx.fillStyle=tt;ctx.fillRect(0,0,VW,VH);}
if(winter&&ui.anim)drawSnow();
else if(S.weather==="rain"&&ui.anim)drawRain();
drawAmbient();
if(S.festT>0&&ui.anim)drawFestival(S.festKind||"confetti");
}
function renderNode(n,winter){if(n.dormant||n.amount<=0)return; // a hunted-out / collected node just disappears (respawns fresh elsewhere)
ctx.globalAlpha=1; // resources & animals are ALWAYS full opacity — never faded, no matter how many times they're gathered/hunted
const p=worldToScreen(n.gx,n.gy);const s=0.85+(n.seed||0)*0.35;
switch(NODE[n.type].kind){
case "tree":artTree(ctx,p.x,p.y,s,winter,n.seed);break;case "rock":artRock(ctx,p.x,p.y,s);break;
case "ore":artOre(ctx,p.x,p.y,s);break;case "bush":artBush(ctx,p.x,p.y,s,"#c1443e",winter);break;
case "herb":artHerb(ctx,p.x,p.y,s,winter);break;case "deer":artDeer(ctx,p.x,p.y,s*1.2,n.seed>0.5?1:-1);break;
case "sheep":artSheep(ctx,p.x,p.y,s*1.2,n.seed>0.5?1:-1);break;case "fish":artFish(ctx,p.x,p.y,1.15);break;}}
function renderBuilding(b){const info0=BUILD[b.type];
if(info0.dock){const d=DOCK_DIRS[b.dir||0],lp=worldToScreen(b.gx,b.gy),wp=worldToScreen(b.gx+d[0],b.gy+d[1]);
if(!b.built)artBuilding(ctx,(lp.x+wp.x)/2,(lp.y+wp.y)/2,b);else artDock2(ctx,lp,wp);
if(ui.selBld===b){const hw=TILE_W/2,hh=TILE_H/2;ctx.strokeStyle="#ffd24a";ctx.lineWidth=2.5;
for(const [tx,ty] of bldTiles(b.type,b.gx,b.gy,b.dir)){const q=worldToScreen(tx,ty);poly(ctx,[[q.x,q.y-hh],[q.x+hw,q.y],[q.x,q.y+hh],[q.x-hw,q.y]]);ctx.stroke();}}
return;}
const sz=BUILD[b.type].size||1;const p=worldToScreen(b.gx+(sz-1)/2,b.gy+(sz-1)/2);artBuilding(ctx,p.x,p.y,b);
if(b.built&&BUILD[b.type].job){const n=S.mellows.filter(m=>m.job===BUILD[b.type].job&&!isChild(m)).length;
if(n>0){fillC(ctx,p.x+15,p.y-34,8,"#f4ecd4",2);ctx.fillStyle="#4a3826";ctx.font="bold 10px sans-serif";
ctx.textAlign="center";ctx.textBaseline="middle";ctx.fillText(n,p.x+15,p.y-33);}}
if(ui.selBld===b){const hw=TILE_W/2,hh=TILE_H/2;ctx.strokeStyle="#ffd24a";ctx.lineWidth=2.5;
for(const [tx,ty] of bldTiles(b.type,b.gx,b.gy)){const q=worldToScreen(tx,ty);
poly(ctx,[[q.x,q.y-hh],[q.x+hw,q.y],[q.x,q.y+hh],[q.x-hw,q.y]]);ctx.stroke();}}}
function renderMellow(m){const p=worldToScreen(m.gx,m.gy),j=JOBS[m.job];
const bobv=ui.anim?Math.sin((S.tsec+m.bob)*6)*1.3:0;
const childScale=isChild(m)?(0.52+Math.min(1,m.age/12)*0.48):1;
artMellow(ctx,p.x,p.y-6,j?j.cloak:"#9fb0a0",isChild(m)?"none":(j?j.tool:"none"),
{s:childScale,bob:bobv,face:m.face,sick:m.sick,carry:m.carry>0?m.carryRes:null});
// need bubble
if(!isChild(m)){let need=null;
if(m.sick)need=g2=>artBldIcon(g2,"cross",g2._bx,g2._by,g2._br);
else if(m.hunger<32&&totalFood()>0)need=g2=>artResIcon(g2,"meat",g2._bx,g2._by,g2._br);
else if(m.hunger<32)need=g2=>artResIcon(g2,"crops",g2._bx,g2._by,g2._br);
else if(m.job==="idle")need=g2=>{g2.fillStyle=INK;g2.font="bold 11px sans-serif";g2.textAlign="center";g2.textBaseline="middle";g2.fillText("?",g2._bx,g2._by);};
else if(m.happy>=82&&(Math.floor(S.tsec/6)+(m.id||0))%7===0)need=g2=>artHeart(g2,g2._bx,g2._by,g2._br*0.72,"#e06e78"); // a content Mellow, now and then, shows the love
if(need)artBubble(ctx,p.x,p.y-33-Math.abs(bobv),(g2,bx,by,br)=>{g2._bx=bx;g2._by=by;g2._br=br;need(g2);});}
if(ui.selMel===m){ctx.strokeStyle="#ffd24a";ctx.lineWidth=2.5;circ(ctx,p.x,p.y-16,15);ctx.stroke();}}
// a pending newcomer — drawn all in white with a soft glow, awaiting the player's decision
function renderNewcomer(m){const p=worldToScreen(m.gx,m.gy),bobv=ui.anim?Math.sin((S.tsec+(m.bob||0))*4)*1.1:0;
ctx.save();ctx.globalAlpha=0.45+(ui.anim?Math.sin(S.tsec*2+(m.bob||0))*0.12:0);
const glow=ctx.createRadialGradient(p.x,p.y-14,2,p.x,p.y-14,16);glow.addColorStop(0,"rgba(255,255,255,.85)");glow.addColorStop(1,"rgba(255,255,255,0)");
ctx.fillStyle=glow;ctx.beginPath();ctx.ellipse(p.x,p.y-14,13,17,0,0,7);ctx.fill();ctx.restore();
artMellow(ctx,p.x,p.y-6,"#f4f4f4","none",{s:1,bob:bobv,face:m.face||1,white:true});}
function artBuilding(g,px,py,b){const info=BUILD[b.type];
if(!b.built){g.strokeStyle="#8a6b45";g.lineWidth=2;const w=info.small?12:18;
for(const dx of [-w,w])for(const dy of [-w/2,w/2]){g.beginPath();g.moveTo(px+dx,py-2);g.lineTo(px+dx,py-16);g.stroke();}
g.fillStyle="rgba(0,0,0,.25)";g.fillRect(px-18,py-30,36,6);g.fillStyle="#7bbf55";g.fillRect(px-18,py-30,36*b.prog,6);
g.fillStyle="#f4ecd4";g.font="bold 9px sans-serif";g.textAlign="center";g.textBaseline="middle";g.fillText(Math.round(b.prog*100)+"%",px,py-40);return;}
if(BLDART[b.type]){BLDART[b.type](g,px,py,isWinter());return;}
if(info.tent){artTent(g,px,py,info.roof);return;}
if(info.cemetery){artCemetery(g,px,py);return;}
if(info.store){artStorehouse(g,px,py,info);return;}
if(info.farm){const growing=(b.stage||"empty")==="growing";
artFarm(g,px,py,isWinter()||!growing,info.size||1,growing?(b.growing||b.crop||"crops"):null);return;}
if(info.pasture){artPasture(g,px,py,info.size||1,b.animal||"sheep",b.herd||2);return;}
if(info.dock){artDock(g,px,py);return;}
if(info.bridge){artBridge(g,px,py);return;}
if(info.boat){artBoat(g,px,py);return;}
if(info.small){shadow(g,px,py,13);g.strokeStyle="#7a5230";g.lineWidth=3.4;g.lineCap="round";
g.beginPath();g.moveTo(px-8,py-1);g.lineTo(px+8,py-3);g.moveTo(px-8,py-3);g.lineTo(px+8,py-1);g.stroke();
g.strokeStyle=INK;g.lineWidth=1.2;g.stroke();
const fl=g.createLinearGradient(px,py-4,px,py-20);fl.addColorStop(0,"#f2c14a");fl.addColorStop(1,"#e0562f");
poly(g,[[px-5,py-3],[px,py-19],[px+5,py-3],[px+2,py-7],[px-2,py-7]]);g.fillStyle=fl;g.fill();
g.fillStyle="#ffe08a";poly(g,[[px-2,py-4],[px+1,py-12],[px+3,py-4]]);g.fill();return;}
// cabin
const bw=20,bh=10,wallH=17,roofH=15,winter=isWinter();
const baseY=py;
const S_=[px,baseY+bh],E=[px+bw,baseY],W=[px-bw,baseY],N=[px,baseY-bh];
const St=[px,baseY+bh-wallH],Et=[px+bw,baseY-wallH],Wt=[px-bw,baseY-wallH],Nt=[px,baseY-bh-wallH];
shadow(g,px,baseY+bh*0.5,bw*1.05);
const wall=info.stone?"#cfd2d6":"#e6cf9c";
fillP(g,[W,S_,St,Wt],wall,1.8); // SW face
fillP(g,[S_,E,Et,St],dark(wall,0.1),1.8); // SE face
// wall texture: stone-block masonry (Stone House) vs stacked-log courses (Wood House)
g.strokeStyle=blend(wall,INK,info.stone?0.34:0.3);g.lineWidth=1.1;g.lineCap="butt";
for(const t of [0.25,0.5,0.75]){
g.beginPath();g.moveTo(W[0]+(Wt[0]-W[0])*t,W[1]+(Wt[1]-W[1])*t);g.lineTo(S_[0]+(St[0]-S_[0])*t,S_[1]+(St[1]-S_[1])*t);g.stroke();
g.beginPath();g.moveTo(S_[0]+(St[0]-S_[0])*t,S_[1]+(St[1]-S_[1])*t);g.lineTo(E[0]+(Et[0]-E[0])*t,E[1]+(Et[1]-E[1])*t);g.stroke();}
if(info.stone){ // staggered vertical joints → reads as cut-stone blocks
g.lineWidth=0.9;const joints=(A,B,C)=>{for(let r=0;r<3;r++){const v0=r/3,v1=(r+1)/3,sh=r%2?0.17:0;
for(const u of [0.28,0.6]){const uu=u+sh;
g.beginPath();g.moveTo(A[0]+(B[0]-A[0])*uu+(C[0]-A[0])*v0,A[1]+(B[1]-A[1])*uu+(C[1]-A[1])*v0);
g.lineTo(A[0]+(B[0]-A[0])*uu+(C[0]-A[0])*v1,A[1]+(B[1]-A[1])*uu+(C[1]-A[1])*v1);g.stroke();}}};
joints(W,S_,Wt);joints(S_,E,St);}
// front corner post (notched log end)
g.strokeStyle=dark(wall,0.3);g.lineWidth=2.6;g.lineCap="round";g.beginPath();g.moveTo(S_[0],S_[1]-1);g.lineTo(St[0],St[1]);g.stroke();
// warm-lit window on the SW face
{const wc=[W[0]+(S_[0]-W[0])*0.4+(Wt[0]-W[0])*0.56, W[1]+(S_[1]-W[1])*0.4+(Wt[1]-W[1])*0.56];
const wu=[(S_[0]-W[0])*0.15,(S_[1]-W[1])*0.15],wv=[(Wt[0]-W[0])*0.2,(Wt[1]-W[1])*0.2];
fillP(g,[[wc[0]-wu[0]-wv[0],wc[1]-wu[1]-wv[1]],[wc[0]+wu[0]-wv[0],wc[1]+wu[1]-wv[1]],[wc[0]+wu[0]+wv[0],wc[1]+wu[1]+wv[1]],[wc[0]-wu[0]+wv[0],wc[1]-wu[1]+wv[1]]],"#f6cb5c",1.2);
g.strokeStyle="rgba(120,80,40,.5)";g.lineWidth=0.9;g.beginPath();g.moveTo(wc[0]-wv[0],wc[1]-wv[1]);g.lineTo(wc[0]+wv[0],wc[1]+wv[1]);g.stroke();}
// door on SE face
const dcx=(S_[0]+E[0])/2, dcy=(S_[1]+E[1])/2;
fillP(g,[[dcx-3,dcy-1],[dcx+3,dcy-4],[dcx+3,dcy-15],[dcx-3,dcy-12]],dark(info.roof,0.28),1.3);
g.fillStyle="#e9c56a";circ(g,dcx+1.6,dcy-8,1.0);g.fill(); // door knob
// roof (hip)
const apex=[px,baseY-bh-wallH-roofH];
fillP(g,[Wt,St,apex],info.roof,1.8);
fillP(g,[St,Et,apex],dark(info.roof,0.12),1.8);
fillP(g,[Wt,Nt,apex],lite(info.roof,0.1),1.8);
fillP(g,[Nt,Et,apex],info.roof,1.8);
// shingle lines
g.strokeStyle=dark(info.roof,0.25);g.lineWidth=1;
for(const t of [0.4,0.7]){g.beginPath();g.moveTo(Wt[0]+(apex[0]-Wt[0])*t,Wt[1]+(apex[1]-Wt[1])*t);
g.lineTo(St[0]+(apex[0]-St[0])*t,St[1]+(apex[1]-St[1])*t);g.stroke();}
if(winter){g.fillStyle="rgba(255,255,255,.85)";poly(g,[Wt,Nt,apex]);g.fill();poly(g,[Nt,Et,apex]);g.fill();
g.fillStyle="rgba(255,255,255,.6)";poly(g,[Wt,St,[apex[0],apex[1]+4]]);g.fill();}
// stone chimney + smoke puffs (cozy)
const chx=px+8,chy=apex[1]+8,chC=info.stone?"#9aa0a6":"#8a6b52";
fillP(g,[[chx-2.6,chy],[chx+2.6,chy],[chx+2.6,chy-10],[chx-2.6,chy-10]],chC,1.4);
fillP(g,[[chx-3.3,chy-10],[chx+3.3,chy-10],[chx+3.3,chy-12.6],[chx-3.3,chy-12.6]],dark(chC,0.22),1.2);
g.fillStyle="rgba(246,246,250,.72)";circ(g,chx,chy-16,2.1);g.fill();circ(g,chx+1.7,chy-20,2.7);g.fill();circ(g,chx-0.6,chy-25,3.2);g.fill();}
function artTent(g,px,py,color){shadow(g,px,py+2,22);
fillP(g,[[px-18,py],[px,py-30],[px+18,py]],dark(color,0.1),1.8);
g.fillStyle=dark(color,0.28);poly(g,[[px-6,py],[px,py-14],[px+6,py]]);g.fill();
g.strokeStyle=INK;g.lineWidth=1.6;g.beginPath();g.moveTo(px,py-30);g.lineTo(px,py-34);g.stroke();
fillP(g,[[px,py-34],[px+7,py-32],[px,py-30]],"#d5715a",1.2);}
function isoPlot(g,px,py,hw,hh,fill){fillP(g,[[px,py-hh],[px+hw,py],[px,py+hh],[px-hw,py]],fill,1.6);}
// a distinct little sprite per crop, drawn sitting on the soil at (x,y); f scales it
function cropSprite(g,x,y,crop,f){f=f||1;g.lineCap="round";g.lineJoin="round";
if(crop==="corn"){ // tall stalks + yellow cob
g.strokeStyle="#4e8b3a";g.lineWidth=1.5*f;g.beginPath();g.moveTo(x,y);g.lineTo(x,y-9*f);g.stroke();
g.strokeStyle="#5aa03e";g.lineWidth=1.2*f;g.beginPath();g.moveTo(x,y-4.5*f);g.lineTo(x-3*f,y-8*f);g.moveTo(x,y-6*f);g.lineTo(x+3*f,y-9.5*f);g.stroke();
g.fillStyle="#f2c94c";g.beginPath();g.ellipse(x+1.1*f,y-6.3*f,1.2*f,2.5*f,-0.3,0,7);g.fill();inkStroke(g,0.7*f);
}else if(crop==="pumpkin"){ // orange gourd on the soil
g.fillStyle="#e08a3a";g.beginPath();g.ellipse(x,y-2.1*f,3.4*f,2.6*f,0,0,7);g.fill();inkStroke(g,1*f);
g.strokeStyle="#c06a26";g.lineWidth=0.7*f;g.beginPath();g.moveTo(x,y-4.5*f);g.lineTo(x,y+0.3*f);g.moveTo(x-1.9*f,y-4*f);g.lineTo(x-1.9*f,y);g.moveTo(x+1.9*f,y-4*f);g.lineTo(x+1.9*f,y);g.stroke();
g.strokeStyle="#4e8b3a";g.lineWidth=1.2*f;g.beginPath();g.moveTo(x,y-4.4*f);g.lineTo(x+1.5*f,y-6.4*f);g.stroke();
}else if(crop==="carrot"){ // feathery tops + orange root peeking
g.fillStyle="#e07a3a";fillP(g,[[x-1.6*f,y-1*f],[x+1.6*f,y-1*f],[x,y+2.4*f]],"#e07a3a",0.8*f);
g.strokeStyle="#4e8b3a";g.lineWidth=1.1*f;for(const a of [-0.7,-0.24,0.24,0.7]){g.beginPath();g.moveTo(x,y-0.8*f);g.lineTo(x+Math.sin(a)*5*f,y-0.8*f-Math.cos(a)*7*f);g.stroke();}
}else if(crop==="cabbage"){ // round blue-green leaf head
g.fillStyle="#7fae54";circ(g,x,y-2.4*f,3.2*f);g.fill();inkStroke(g,1*f);
g.fillStyle="#9ec773";circ(g,x-0.3*f,y-2.7*f,1.7*f);g.fill();
g.strokeStyle="#5f8a3a";g.lineWidth=0.6*f;g.beginPath();g.arc(x,y-2.4*f,2.6*f,-2.5,-0.6);g.stroke();
}else if(crop==="fruit"){ // berry bush with red dots
g.fillStyle="#4e8b3a";circ(g,x,y-2.6*f,3.2*f);g.fill();inkStroke(g,1*f);
g.fillStyle="#c1443e";for(const [dx,dy] of [[-1.4,-3.2],[1.3,-3.5],[0,-1.7],[-1.8,-1.3],[1.7,-1.5]]){circ(g,x+dx*f,y+dy*f,0.9*f);g.fill();}
}else{ // crops = golden wheat
for(const dx of [-1.8,0,1.8]){g.strokeStyle="#d9a63a";g.lineWidth=1.2*f;g.beginPath();g.moveTo(x+dx*f,y);g.quadraticCurveTo(x+dx*f+f,y-6*f,x+dx*f+2*f,y-9*f);g.stroke();
g.fillStyle="#e8c65a";g.beginPath();g.ellipse(x+dx*f+2*f,y-9.6*f,0.9*f,1.9*f,0.35,0,7);g.fill();}
}}
// a cute little picket fence around the plot — `front` picks the near or far pair of edges (for depth)
function farmFence(g,px,py,hw,hh,f,front){
const N=[px,py-hh],E=[px+hw,py],S=[px,py+hh],W=[px-hw,py];
const edges=front?[[W,S],[S,E]]:[[W,N],[N,E]];
const ph=7*f;
for(const [A,B] of edges){
const tops=[],bases=[];
for(let i=0;i<=3;i++){const t=i/3,bx=A[0]+(B[0]-A[0])*t,by=A[1]+(B[1]-A[1])*t;bases.push([bx,by]);tops.push([bx,by-ph]);}
const rail=()=>{g.beginPath();g.moveTo(tops[0][0],tops[0][1]);for(let i=1;i<tops.length;i++)g.lineTo(tops[i][0],tops[i][1]);};
g.lineCap="round";
rail();g.strokeStyle=INK;g.lineWidth=2.3*f;g.stroke();
rail();g.strokeStyle="#b98a5a";g.lineWidth=1.2*f;g.stroke();
for(let i=0;i<bases.length;i++){
g.beginPath();g.moveTo(bases[i][0],bases[i][1]);g.lineTo(tops[i][0],tops[i][1]);g.strokeStyle=INK;g.lineWidth=2.7*f;g.stroke();
g.beginPath();g.moveTo(bases[i][0],bases[i][1]);g.lineTo(tops[i][0],tops[i][1]);g.strokeStyle="#c49a62";g.lineWidth=1.4*f;g.stroke();
g.fillStyle="#d0a86e";circ(g,tops[i][0],tops[i][1],1.2*f);g.fill();}}}
// a tiny 5-petal flower for cute little accents
function tinyFlower(g,x,y,col,f){f=f||1;g.fillStyle=col;
for(let k=0;k<5;k++){const a=k/5*6.2832-1.2;circ(g,x+Math.cos(a)*1.5*f,y+Math.sin(a)*1.5*f,1.05*f);g.fill();}
g.fillStyle="#ffd94a";circ(g,x,y,0.95*f);g.fill();}
function artFarm(g,px,py,winter,sz,crop){sz=sz||1;const hw=sz>1?58:24,hh=sz>1?29:12,rows=sz>1?7:4,f=sz>1?1.5:1;
shadow(g,px,py+hh*0.4,hw*0.95);
isoPlot(g,px,py,hw,hh,"#b07f4c"); // tilled soil (warmer)
isoPlot(g,px,py,hw-3,hh-1.5,"#c1925a"); // lighter inner
farmFence(g,px,py,hw,hh,f,false); // far fence (behind the crops)
// furrow rows (parallel to the W→N edge); planted rows carry per-crop sprites
const spriteF=sz>1?1.05:0.8, n=sz>1?3:2;
for(let i=1;i<=rows;i++){const k=i/(rows+1);
const ax=px-hw+hw*k, ay=py+hh*k, bx=px+hw*k, by=py-hh+hh*k;
g.strokeStyle="#8a5f38";g.lineWidth=1.4;g.lineCap="round";g.beginPath();g.moveTo(ax,ay);g.lineTo(bx,by);g.stroke();
if(!winter&&crop){for(let j=1;j<=n;j++){const t=j/(n+1),sx=ax+(bx-ax)*t,sy=ay+(by-ay)*t;
cropSprite(g,sx,sy,crop,spriteF);}}}
if(winter){g.fillStyle="rgba(255,255,255,.6)";g.beginPath();g.moveTo(px,py-hh);g.lineTo(px+hw,py);g.lineTo(px,py+hh);g.lineTo(px-hw,py);g.closePath();g.fill();}
// a cheerful little scarecrow at the back corner
const sx=px-hw*0.42,sy=py-hh*0.42;g.lineCap="round";
g.strokeStyle=INK;g.lineWidth=2.7*f;g.beginPath();g.moveTo(sx,sy);g.lineTo(sx,sy-13*f);g.moveTo(sx-5.5*f,sy-9.5*f);g.lineTo(sx+5.5*f,sy-9.5*f);g.stroke();
g.strokeStyle="#a97e4c";g.lineWidth=1.4*f;g.beginPath();g.moveTo(sx,sy);g.lineTo(sx,sy-13*f);g.moveTo(sx-5.5*f,sy-9.5*f);g.lineTo(sx+5.5*f,sy-9.5*f);g.stroke();
g.fillStyle="#d8b877";g.beginPath();g.ellipse(sx,sy-8*f,3.3*f,4.1*f,0,0,7);g.fill();inkStroke(g,1.1); // plump burlap body
g.strokeStyle="#b08a4a";g.lineWidth=0.7*f;g.beginPath();g.moveTo(sx-2.3*f,sy-6*f);g.lineTo(sx+2.3*f,sy-6*f);g.stroke();
g.fillStyle="#f0d9a2";circ(g,sx,sy-15*f,3*f);g.fill();inkStroke(g,1.1); // round head
g.fillStyle=INK;circ(g,sx-1*f,sy-15.4*f,0.5*f);g.fill();circ(g,sx+1*f,sy-15.4*f,0.5*f);g.fill(); // eyes
g.strokeStyle=INK;g.lineWidth=0.7*f;g.beginPath();g.arc(sx,sy-14.7*f,1.3*f,0.2,Math.PI-0.2);g.stroke(); // smile
g.fillStyle="rgba(228,138,150,.5)";circ(g,sx-1.9*f,sy-14.2*f,0.75*f);g.fill();circ(g,sx+1.9*f,sy-14.2*f,0.75*f);g.fill(); // rosy cheeks
fillP(g,[[sx-4.2*f,sy-17*f],[sx+4.2*f,sy-17*f],[sx,sy-22*f]],"#c79a52",1.1); // straw hat cone
fillP(g,[[sx-4.7*f,sy-16.7*f],[sx+4.7*f,sy-16.7*f],[sx+3*f,sy-18*f],[sx-3*f,sy-18*f]],"#b8863f",1); // hat brim
const bxx=sx+5.5*f,byy=sy-9.5*f; // a little bird perched on the arm
g.fillStyle="#7fa8c9";g.beginPath();g.ellipse(bxx,byy-1.7*f,1.9*f,1.5*f,0,0,7);g.fill();inkStroke(g,0.8);
fillP(g,[[bxx+1.6*f,byy-2*f],[bxx+3*f,byy-1.5*f],[bxx+1.6*f,byy-1*f]],"#e8a24a",0.5); // beak
g.fillStyle=INK;circ(g,bxx+0.5*f,byy-2.1*f,0.42*f);g.fill(); // eye
const fcols=["#f2a0c0","#fdfdfd","#c9a0e0","#ffd0e0","#f6b84a"]; // cheerful little flowers dotted around the bed
[[-0.72,0.08],[0.72,0.08],[0.0,0.72],[-0.36,0.5],[0.36,0.5]].forEach((s,i)=>tinyFlower(g,px+s[0]*hw,py+s[1]*hh,fcols[i%fcols.length],f*0.9));
farmFence(g,px,py,hw,hh,f,true);} // near fence (in front)
function artPasture(g,px,py,sz,animal,herd){sz=sz||1;animal=animal||"sheep";herd=herd||2;const hw=sz>1?58:24,hh=sz>1?29:12;
shadow(g,px,py+hh*0.4,hw*0.95);
isoPlot(g,px,py,hw,hh,"#7faa54");isoPlot(g,px,py,hw-3,hh-1.5,"#8cb85f");
g.strokeStyle="#5f8a3a";g.lineWidth=1.2;g.lineCap="round";
const tufts=sz>1?[[-26,4],[16,-6],[0,10],[28,6],[-12,-8],[10,13],[-32,0],[24,-11],[-6,16]]:[[-10,2],[7,-3],[0,5],[12,3],[-4,-4],[4,6]];
for(const [dx,dy] of tufts){for(const o of [-2,0,2]){g.beginPath();g.moveTo(px+dx+o,py+dy);g.lineTo(px+dx+o*0.6,py+dy-3);g.stroke();}}
// low rail fence around the plot
const cor=[[px,py-hh],[px+hw,py],[px,py+hh],[px-hw,py]];g.strokeStyle="#a9784a";g.lineWidth=1.5;g.lineCap="round";const posts=sz>1?6:3;
for(let e=0;e<4;e++){const a=cor[e],b=cor[(e+1)%4];for(let i=0;i<=posts;i++){const t=i/posts,fx=a[0]+(b[0]-a[0])*t,fy=a[1]+(b[1]-a[1])*t;g.beginPath();g.moveTo(fx,fy);g.lineTo(fx,fy-5);g.stroke();}
g.beginPath();g.moveTo(a[0],a[1]-3.6);g.lineTo(b[0],b[1]-3.6);g.stroke();}
// scatter one animal per head of herd (capped), back-to-front so they overlap nicely
const drawA=animal==="deer"?artDeer:artSheep;
const spots=[[-0.30,-0.14],[0.28,-0.10],[0.05,-0.02],[-0.34,0.12],[0.30,0.16],[-0.06,0.28]];
const nA=Math.max(1,Math.min(sz>1?6:2,Math.round(herd))),asc=sz>1?0.72:0.6;
spots.slice(0,nA).sort((a,b)=>a[1]-b[1]).forEach(([fx,fy],i)=>drawA(g,px+fx*hw,py+fy*hh,asc,i%2?-1:1));}
// 2-tile dock: a plank walkway from the land anchor (lp) out to the pier platform on water (wp)
function artDock2(g,lp,wp){
g.strokeStyle="#8a6b45";g.lineWidth=6;g.lineCap="round";g.beginPath();g.moveTo(lp.x,lp.y);g.lineTo(wp.x,wp.y);g.stroke();
g.strokeStyle="#b08a58";g.lineWidth=3.4;g.beginPath();g.moveTo(lp.x,lp.y);g.lineTo(wp.x,wp.y);g.stroke();
// cross-planks along the walkway
g.strokeStyle="#7a5230";g.lineWidth=1;const n=4;for(let i=1;i<n;i++){const t=i/n,cx=lp.x+(wp.x-lp.x)*t,cy=lp.y+(wp.y-lp.y)*t,px=-(wp.y-lp.y),py=(wp.x-lp.x),pl=Math.hypot(px,py)||1;
g.beginPath();g.moveTo(cx-px/pl*4,cy-py/pl*4);g.lineTo(cx+px/pl*4,cy+py/pl*4);g.stroke();}
artDock(g,wp.x,wp.y);}
function artDock(g,px,py){const hw=18,hh=9;
isoPlot(g,px,py,hw,hh,"#b98a4a");g.strokeStyle="#8a5f38";g.lineWidth=1;
for(let i=1;i<4;i++){const k=i/4;g.beginPath();g.moveTo(px-hw+hw*k,py+hh*k);g.lineTo(px+hw*k,py-hh+hh*k);g.stroke();}
g.strokeStyle="#7a5230";g.lineWidth=2.4;g.lineCap="round";
for(const c of [[px-hw,py],[px+hw,py],[px,py+hh]]){g.beginPath();g.moveTo(c[0],c[1]);g.lineTo(c[0],c[1]+5);g.stroke();}
artBarrel(g,px+7,py-2,3);}
function artBridge(g,px,py){const hw=20,hh=10;
isoPlot(g,px,py,hw,hh,"#a9784a");g.strokeStyle="#7a5230";g.lineWidth=1.2;
for(let i=1;i<5;i++){const k=i/5;g.beginPath();g.moveTo(px-hw+hw*k,py+hh*k);g.lineTo(px+hw*k,py-hh+hh*k);g.stroke();}
g.strokeStyle="#8a6b45";g.lineWidth=1.5;g.lineCap="round";g.beginPath();g.moveTo(px-hw,py-4);g.lineTo(px,py-hh-4);g.lineTo(px+hw,py-4);g.stroke();}
function artBoat(g,px,py){shadow(g,px,py+4,15);
fillP(g,[[px-14,py-2],[px+14,py-2],[px+8,py+7],[px-8,py+7]],"#a06a3e",1.7); // hull
fillP(g,[[px-14,py-2],[px+14,py-2],[px+9,py+1],[px-9,py+1]],"#c08a54",1.1); // rim
g.strokeStyle="#8a5f38";g.lineWidth=1;g.beginPath();g.moveTo(px-9,py+1);g.lineTo(px-5,py+7);g.moveTo(px,py+1);g.lineTo(px,py+7);g.moveTo(px+9,py+1);g.lineTo(px+5,py+7);g.stroke();
g.strokeStyle="#7a5230";g.lineWidth=2;g.lineCap="round";g.beginPath();g.moveTo(px+1,py-2);g.lineTo(px+1,py-17);g.stroke();
fillP(g,[[px+2,py-16],[px+11,py-5],[px+2,py-5]],"#f2ead6",1.3);} // sail
function artGrave(g,x,y,seed){shadow(g,x,y+1,6);
g.fillStyle="#84a35f";g.beginPath();g.ellipse(x,y+1,6.5,2.6,0,0,7);g.fill(); // grassy mound
g.beginPath();g.moveTo(x-3.6,y);g.lineTo(x-3.6,y-6);g.arc(x,y-6,3.6,Math.PI,0);g.lineTo(x+3.6,y);g.closePath();
g.fillStyle=(seed||0)>0.5?"#cccfd3":"#c4c1b8";g.fill();inkStroke(g,1.3);
g.strokeStyle="#9aa0a6";g.lineWidth=1.1;g.lineCap="round";
g.beginPath();g.moveTo(x,y-7.4);g.lineTo(x,y-2.6);g.moveTo(x-1.8,y-5.8);g.lineTo(x+1.8,y-5.8);g.stroke();}
function artHeart(g,x,y,r,color){g.save();g.fillStyle=color||"#e06e78";g.beginPath();
g.moveTo(x,y+r*0.9);g.bezierCurveTo(x-r*1.4,y-r*0.3,x-r*0.5,y-r*1.2,x,y-r*0.35);
g.bezierCurveTo(x+r*0.5,y-r*1.2,x+r*1.4,y-r*0.3,x,y+r*0.9);g.closePath();g.fill();g.restore();}
function artCemetery(g,px,py){shadow(g,px,py+3,20);
const hw=17,hh=8.5;
fillP(g,[[px,py-hh],[px+hw,py],[px,py+hh],[px-hw,py]],"#6f7d55",1.5); // earth plot
fillP(g,[[px,py-hh+2],[px+hw-3,py],[px,py+hh-2],[px-hw+3,py]],"#78875e",1); // inner
// low picket fence along the two back edges
g.strokeStyle="#8a6b45";g.lineWidth=1.5;g.lineCap="round";
for(let i=0;i<=5;i++){const t=i/5;const ax=px-hw+hw*t,ay=py-hh*(1-t);g.beginPath();g.moveTo(ax,ay);g.lineTo(ax,ay-6);g.stroke();
const bx=px+hw*t,by=py-hh+hh*t;g.beginPath();g.moveTo(bx,by);g.lineTo(bx,by-6);g.stroke();}
g.lineWidth=1.3;g.beginPath();g.moveTo(px-hw,py-6);g.lineTo(px,py-hh-6);g.lineTo(px+hw,py-6);g.stroke();
// a few headstones on the plot
artGrave(g,px-6,py-1,0.2);artGrave(g,px+6,py+1,0.8);artGrave(g,px+1,py-4,0.6);}
function artCrate(g,x,y,s){shadow(g,x,y+1,s*1.15);
fillP(g,[[x-s,y-s*1.2],[x+s,y-s*1.2],[x+s,y+s*0.4],[x-s,y+s*0.4]],"#c69a5c",1.4); // front
fillP(g,[[x-s,y-s*1.2],[x,y-s*1.75],[x+s,y-s*1.2],[x,y-s*0.65]],"#d9b072",1.3); // top
g.strokeStyle="#8a6238";g.lineWidth=1.1;g.lineCap="round";
g.beginPath();g.moveTo(x-s,y-s*1.2);g.lineTo(x+s,y+s*0.4);g.moveTo(x+s,y-s*1.2);g.lineTo(x-s,y+s*0.4);g.stroke();}
function artBarrel(g,x,y,s){shadow(g,x,y+1,s*1.1);
fillP(g,[[x-s*0.82,y-s*1.4],[x+s*0.82,y-s*1.4],[x+s*0.68,y+s*0.3],[x-s*0.68,y+s*0.3]],"#a9784a",1.4);
g.fillStyle="#c9a066";g.beginPath();g.ellipse(x,y-s*1.4,s*0.82,s*0.34,0,0,7);g.fill();inkStroke(g,1.2);
g.strokeStyle="#6f4a2c";g.lineWidth=1.1;g.beginPath();g.moveTo(x-s*0.8,y-s*0.62);g.lineTo(x+s*0.8,y-s*0.62);g.moveTo(x-s*0.75,y-s*0.12);g.lineTo(x+s*0.75,y-s*0.12);g.stroke();}
function artStorehouse(g,px,py,info){
const bw=21,bh=10.5,wallH=15,th=6,baseY=py;
const S_=[px,baseY+bh],E=[px+bw,baseY],W=[px-bw,baseY];
const St=[px,baseY+bh-wallH],Et=[px+bw,baseY-wallH],Wt=[px-bw,baseY-wallH],Nt=[px,baseY-bh-wallH];
shadow(g,px,baseY+bh*0.5,bw*1.08);
const wall="#e0c793";
fillP(g,[W,S_,St,Wt],wall,1.8); // SW face
fillP(g,[S_,E,Et,St],dark(wall,0.12),1.8); // SE face
// horizontal plank courses
g.strokeStyle=blend(wall,INK,0.3);g.lineWidth=1;g.lineCap="butt";
for(const t of [0.33,0.66]){
g.beginPath();g.moveTo(W[0]+(Wt[0]-W[0])*t,W[1]+(Wt[1]-W[1])*t);g.lineTo(S_[0]+(St[0]-S_[0])*t,S_[1]+(St[1]-S_[1])*t);g.stroke();
g.beginPath();g.moveTo(S_[0]+(St[0]-S_[0])*t,S_[1]+(St[1]-S_[1])*t);g.lineTo(E[0]+(Et[0]-E[0])*t,E[1]+(Et[1]-E[1])*t);g.stroke();}
// wide loading bay (dark opening) on SE face
const bcx=(S_[0]+E[0])/2, bcy=(S_[1]+E[1])/2;
fillP(g,[[bcx-6,bcy-1],[bcx+6,bcy-6],[bcx+6,bcy-15],[bcx-6,bcy-10]],"#5a4632",1.4);
g.strokeStyle="rgba(230,215,180,.35)";g.lineWidth=1;g.beginPath();g.moveTo(bcx,bcy-3.5);g.lineTo(bcx,bcy-12.5);g.stroke();
// FLAT roof slab — clearly not a peaked house roof
const roof=info.roof||"#b98a4a";
const NtR=[Nt[0],Nt[1]-th],EtR=[Et[0],Et[1]-th],StR=[St[0],St[1]-th],WtR=[Wt[0],Wt[1]-th];
fillP(g,[Wt,St,StR,WtR],dark(roof,0.16),1.6); // slab SW edge
fillP(g,[St,Et,EtR,StR],dark(roof,0.3),1.6); // slab SE edge
fillP(g,[NtR,EtR,StR,WtR],roof,1.6); // slab top
// rooftop hatch
fillP(g,[[px-4,NtR[1]+9],[px+2,NtR[1]+6],[px+2,NtR[1]+12],[px-4,NtR[1]+15]],dark(roof,0.34),1.2);
if(isWinter()){g.fillStyle="rgba(255,255,255,.82)";poly(g,[NtR,EtR,StR,WtR]);g.fill();}
// crates + barrel stacked out front (drawn last → in front)
artCrate(g,px-11,baseY+bh+2,5);artCrate(g,px-3,baseY+bh+4,5);artCrate(g,px-7,baseY+bh-3.5,5);
artBarrel(g,px+10,baseY+bh+2,5);}
// ---- distinct building silhouettes (unique shape + signature prop, shared doodle style) ----
// Two visible iso wall faces (SW + SE); returns wall-top corners for adding roofs/props.
function isoWalls(g,px,py,bw,bh,wallH,wall){
const S_=[px,py+bh],E=[px+bw,py],W=[px-bw,py],N=[px,py-bh];
const St=[px,py+bh-wallH],Et=[px+bw,py-wallH],Wt=[px-bw,py-wallH],Nt=[px,py-bh-wallH];
fillP(g,[W,S_,St,Wt],wall,1.8);
fillP(g,[S_,E,Et,St],dark(wall,0.12),1.8);
return {S_,E,W,N,St,Et,Wt,Nt,bw,bh,wallH};
}
// Long ridged (barn/gable) roof over the diamond footprint; ridge runs front↔back.
function ridgeRoof(g,c,roofH,col,winter){
const Rf=[c.St[0],c.St[1]-roofH],Rb=[c.Nt[0],c.Nt[1]-roofH];
fillP(g,[c.St,c.Wt,c.Nt,Rb,Rf],lite(col,0.06),1.7); // SW slope
fillP(g,[c.St,c.Et,c.Nt,Rb,Rf],dark(col,0.14),1.7); // SE slope
g.strokeStyle=dark(col,0.32);g.lineWidth=1.1;g.beginPath();g.moveTo(Rf[0],Rf[1]);g.lineTo(Rb[0],Rb[1]);g.stroke();
if(winter){g.fillStyle="rgba(255,255,255,.72)";poly(g,[c.St,c.Wt,c.Nt,Rb,Rf]);g.fill();poly(g,[c.St,c.Et,c.Nt,Rb,Rf]);g.fill();}
return {Rf,Rb};
}
// Hip roof to a single apex (cozy cabin style); returns apex.
function hipRoof(g,c,roofH,col,winter){
const ap=[c.St[0],(c.St[1]+c.Nt[1])/2-roofH];
fillP(g,[c.Wt,c.St,ap],col,1.7);fillP(g,[c.St,c.Et,ap],dark(col,0.12),1.7);
fillP(g,[c.Wt,c.Nt,ap],lite(col,0.08),1.7);fillP(g,[c.Nt,c.Et,ap],col,1.7);
if(winter){g.fillStyle="rgba(255,255,255,.76)";poly(g,[c.Wt,c.Nt,ap]);g.fill();poly(g,[c.Nt,c.Et,ap]);g.fill();
g.fillStyle="rgba(255,255,255,.5)";poly(g,[c.Wt,c.St,ap]);g.fill();}
return ap;
}
// Small hanging shop sign on a post at (x,y=ground); iconFn(cx,cy) draws the emblem.
function shopSign(g,x,y,boardCol,iconFn){
g.strokeStyle="#7a5230";g.lineWidth=2.2;g.lineCap="round";
g.beginPath();g.moveTo(x,y);g.lineTo(x,y-19);g.lineTo(x+8,y-19);g.stroke();
g.strokeStyle="#5a3f28";g.lineWidth=1;g.beginPath();g.moveTo(x+8,y-19);g.lineTo(x+8,y-15.5);g.stroke();
fillP(g,[[x+2.5,y-15],[x+13.5,y-15],[x+13.5,y-6],[x+2.5,y-6]],boardCol,1.3);
if(iconFn)iconFn(x+8,y-10.5);
}
function artCraftShop(g,px,py,winter){shadow(g,px,py+5,22);
const wall="#d8c39a",c=isoWalls(g,px,py,17,8.5,15,wall);
timberFace(g,c.W,c.S_,c.Wt,"#6b4a30");
const Psw=faceP(c.W,c.S_,c.Wt),Pse=timberFace(g,c.S_,c.E,c.St,"#5f4128");
ridgeRoof(g,c,12,"#8a5a3c",winter);
chimney(g,px-8,c.St[1]-7,"#8f8a82"); // stone forge chimney
warmWin(g,Psw,0.52,0.32,0.8,0.62,"#f6cb5c"); // side window
// door + open forge bay on the front
doorway(g,Pse,0.1,0.28,0.5,"#5a4130");
fillP(g,[Pse(0.42,0.02),Pse(0.9,0.02),Pse(0.9,0.54),Pse(0.42,0.54)],"#3f2f22",1.3);
{const bc=Pse(0.66,0.26);const fg=g.createRadialGradient(bc[0],bc[1],0.5,bc[0],bc[1],7);fg.addColorStop(0,"#ffcf72");fg.addColorStop(1,"rgba(224,86,47,0)");g.fillStyle=fg;g.beginPath();g.ellipse(bc[0],bc[1],6.4,6,0,0,7);g.fill();}
// anvil on a stump out front (signature)
const axx=px-12,ayy=py+12;shadow(g,axx,ayy+2.5,6);
fillP(g,[[axx-3,ayy+1],[axx+3,ayy+1],[axx+3,ayy+4],[axx-3,ayy+4]],"#8a6b45",1.2);
fillP(g,[[axx-5,ayy-3.4],[axx+3.5,ayy-3.4],[axx+7,ayy-2.4],[axx+3.5,ayy-1.4],[axx+1.6,ayy-1.4],[axx+1.6,ayy+1],[axx-1.6,ayy+1],[axx-1.6,ayy-1.4],[axx-5,ayy-1.4]],"#54545c",1.4);
g.fillStyle="#767680";fillP(g,[[axx-5,ayy-3.4],[axx+3.5,ayy-3.4],[axx+3.5,ayy-2.5],[axx-5,ayy-2.5]],"#767680",0);
g.fillStyle="#ffd66b";circ(g,axx+2,ayy-5.6,0.9);g.fill();circ(g,axx+5.4,ayy-7,0.7);g.fill();
// hanging hammer sign
shopSign(g,px+15,py+6,"#e9d3a0",(cx,cy)=>{g.strokeStyle="#7a5230";g.lineWidth=2;g.lineCap="round";g.beginPath();g.moveTo(cx-2,cy+3);g.lineTo(cx+1,cy-2);g.stroke();g.fillStyle="#8a8f98";fillP(g,[[cx-1,cy-4],[cx+4,cy-4],[cx+4,cy-1.5],[cx-1,cy-1.5]],"#8a8f98",1);});}
function artSawmill(g,px,py,winter){shadow(g,px,py+5,23);
const wall="#b98a54",c=isoWalls(g,px+3,py,14,8,13,wall);
const Psw=plankFace(g,c.W,c.S_,c.Wt,dark(wall,0.2),4),Pse=plankFace(g,c.S_,c.E,c.St,dark(wall,0.28),4);
ridgeRoof(g,c,11,"#9c6b3a",winter);
warmWin(g,Pse,0.44,0.32,0.68,0.62,"#f6cb5c"); // mill window
doorway(g,Pse,0.74,0.92,0.5,"#6f4a2c");
// big water wheel on the left (signature) in its mill-race
const wx=px-15,wy=py+2,wr=10;
g.fillStyle="#9fb9c9";g.beginPath();g.ellipse(wx-2,wy+7,11,3.2,0,0,7);g.fill();
g.fillStyle="rgba(255,255,255,.5)";g.beginPath();g.ellipse(wx-4,wy+6.5,3,1,0,0,7);g.fill();
fillC(g,wx,wy,wr,"#8a6b45",1.6);fillC(g,wx,wy,wr*0.42,"#c79b62",1.3);
g.strokeStyle="#6f4a2c";g.lineWidth=1.4;
for(let i=0;i<8;i++){const a=i/8*Math.PI*2;g.beginPath();g.moveTo(wx+Math.cos(a)*wr*0.42,wy+Math.sin(a)*wr*0.42);g.lineTo(wx+Math.cos(a)*wr,wy+Math.sin(a)*wr);g.stroke();
const a2=(i+0.5)/8*Math.PI*2;g.strokeStyle="#5a3f28";g.lineWidth=2.2;g.beginPath();g.moveTo(wx+Math.cos(a2)*wr*0.86,wy+Math.sin(a2)*wr*0.86);g.lineTo(wx+Math.cos(a2)*(wr+2),wy+Math.sin(a2)*(wr+2));g.stroke();g.lineWidth=1.4;g.strokeStyle="#6f4a2c";}
// fresh-cut log pile out front
for(const [lx,ly] of [[px+7,py+13],[px+14,py+11],[px+10.5,py+8]]){fillC(g,lx,ly,3.2,"#c9a066",1.3);fillC(g,lx,ly,1.5,"#e0c793",1);}}
function artKitchen(g,px,py,winter){shadow(g,px,py+5,22);
const wall="#efd9a4",c=isoWalls(g,px-2,py,15,8,14,wall);
timberFace(g,c.W,c.S_,c.Wt,"#7a5230");
const Pse=timberFace(g,c.S_,c.E,c.St,"#6b4a2a");
ridgeRoof(g,c,12,"#c06636",winter);
// door + glowing shopfront window (with pies on the sill)
doorway(g,Pse,0.1,0.28,0.5,"#8a5f38");
warmWin(g,Pse,0.36,0.1,0.72,0.5,"#f6cb5c");
["#c76b3a","#b0894a","#c76b3a"].forEach((cc,i)=>{const b=Pse(0.44+i*0.11,0.17);g.fillStyle=cc;g.beginPath();g.arc(b[0],b[1],1.3,Math.PI,0);g.fill();});
// massive stone oven-chimney on the right (signature)
const ox=px+14,oy=py+3;
fillP(g,[[ox-5,oy-2],[ox+5,oy-2],[ox+5,oy-21],[ox-5,oy-21]],"#bdb8b0",1.5);
fillP(g,[[ox-6,oy-21],[ox+6,oy-21],[ox+6,oy-24],[ox-6,oy-24]],"#a4a29b",1.3);
g.strokeStyle="#948f86";g.lineWidth=0.8;for(const yy of [-7,-12,-17]){g.beginPath();g.moveTo(ox-5,oy+yy);g.lineTo(ox+5,oy+yy);g.stroke();}
g.beginPath();g.moveTo(ox,oy-2);g.lineTo(ox,oy-7);g.moveTo(ox-2.5,oy-7);g.lineTo(ox-2.5,oy-12);g.moveTo(ox+2.5,oy-12);g.lineTo(ox+2.5,oy-17);g.stroke();
// arched glowing oven mouth
g.beginPath();g.moveTo(ox-3.4,oy-3);g.lineTo(ox-3.4,oy-6);g.arc(ox,oy-6,3.4,Math.PI,0);g.lineTo(ox+3.4,oy-3);g.closePath();
const fg=g.createLinearGradient(ox,oy-9,ox,oy-2);fg.addColorStop(0,"#ffd66b");fg.addColorStop(1,"#e0562f");g.fillStyle=fg;g.fill();inkStroke(g,1.2);
g.strokeStyle="#7a3410";g.lineWidth=1;g.beginPath();g.moveTo(ox-2,oy-3.4);g.lineTo(ox+2,oy-3.4);g.stroke();
g.fillStyle="rgba(246,246,250,.62)";for(const [sx,sy,sr] of [[ox,oy-28,2.2],[ox+2,oy-32,2.7],[ox-1,oy-37,3]]){circ(g,sx,sy,sr);g.fill();}
// hanging pot sign
shopSign(g,px-15,py+6,"#efe0b6",(cx,cy)=>{g.fillStyle="#5a5a62";g.beginPath();g.arc(cx,cy+0.8,3,0,Math.PI);g.fill();inkStroke(g,1.1);
g.strokeStyle=INK;g.lineWidth=1.1;g.beginPath();g.moveTo(cx-3.4,cy+0.6);g.lineTo(cx+3.4,cy+0.6);g.stroke();
g.strokeStyle="#6b6b72";g.beginPath();g.arc(cx,cy-0.4,1.2,Math.PI,0);g.stroke();});}
// Tudor half-timber framing on an iso wall face (A=bottom-left, B=bottom-right, C=top-left corner).
function timberFace(g,A,B,C,beam){
const P=(u,v)=>[A[0]+(B[0]-A[0])*u+(C[0]-A[0])*v, A[1]+(B[1]-A[1])*u+(C[1]-A[1])*v];
g.strokeStyle=beam;g.lineCap="round";
g.lineWidth=1.7;for(const u of [0.03,0.5,0.97]){const a=P(u,0),b=P(u,1);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();} // studs
for(const v of [0.5,0.97]){const a=P(0,v),b=P(1,v);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();} // rails
g.lineWidth=1.3; // braces meeting at the mid-panel
for(const u of [0.03,0.97]){const a=P(u,0),b=P(0.5,0.5);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();}
return P;
}
// Barycentric point-fn for an iso wall face (A=bottom-left, B=bottom-right, C=top-left corner).
function faceP(A,B,C){return (u,v)=>[A[0]+(B[0]-A[0])*u+(C[0]-A[0])*v, A[1]+(B[1]-A[1])*u+(C[1]-A[1])*v];}
// Horizontal plank/siding courses on a face.
function plankFace(g,A,B,C,col,rows){const P=faceP(A,B,C);g.strokeStyle=col;g.lineWidth=0.8;
for(let i=1;i<(rows||3);i++){const v=i/(rows||3),a=P(0,v),b=P(1,v);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();}return P;}
// Warm-lit window (quad + mullion cross) on a face given its point-fn.
function warmWin(g,P,u0,v0,u1,v1,col){fillP(g,[P(u0,v0),P(u1,v0),P(u1,v1),P(u0,v1)],col||"#f6cb5c",1.1);
g.strokeStyle="rgba(90,60,30,.42)";g.lineWidth=0.7;
let a=P((u0+u1)/2,v0),b=P((u0+u1)/2,v1);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();
a=P(u0,(v0+v1)/2);b=P(u1,(v0+v1)/2);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();}
// Plank door on a face.
function doorway(g,P,u0,u1,vTop,col,knob){const q=[P(u0,0.02),P(u1,0.02),P(u1,vTop),P(u0,vTop)];fillP(g,q,col||"#6f4a2c",1.3);
if(knob!==false){const k=P(u1-0.04,vTop*0.5);g.fillStyle="#e9c56a";circ(g,k[0],k[1],0.9);g.fill();}return q;}
// Brick chimney with smoke; (x,baseY)=where it meets the roof, grows upward.
function chimney(g,x,baseY,col){col=col||"#9c6b5a";
fillP(g,[[x-2.2,baseY],[x+2.2,baseY],[x+2.2,baseY-10],[x-2.2,baseY-10]],col,1.3);
fillP(g,[[x-2.8,baseY-10],[x+2.8,baseY-10],[x+2.8,baseY-12.4],[x-2.8,baseY-12.4]],dark(col,0.18),1.1);
g.fillStyle="rgba(246,246,250,.58)";circ(g,x+0.3,baseY-14.5,1.6);g.fill();circ(g,x+1.6,baseY-18,2);g.fill();}
function artApothecary(g,px,py,winter){shadow(g,px,py+5,20);
// a proper half-timbered alchemist's shop (walls, framing, shopfront, roof) — themed by details, not a giant bottle
const wall="#ece0c4",c=isoWalls(g,px,py,14,7.5,21,wall);
timberFace(g,c.W,c.S_,c.Wt,"#6b4a30");
const Pse=timberFace(g,c.S_,c.E,c.St,"#5f4128");
// gabled shingle roof (muted slate-violet keeps a subtle apothecary mood)
ridgeRoof(g,c,12,"#877e97",winter);
// brick chimney + smoke on the SW slope
const chx=px-8,chy=c.St[1]-13;
fillP(g,[[chx-2,chy],[chx+2.4,chy-1.2],[chx+2.4,chy-9.5],[chx-2,chy-8.3]],"#9c6b5a",1.3);
fillP(g,[[chx-2.4,chy-8.3],[chx+2.8,chy-9.6],[chx+2.8,chy-11.4],[chx-2.4,chy-10.1]],"#7a5246",1.1);
g.fillStyle="rgba(246,246,250,.6)";circ(g,chx+0.4,chy-14,1.7);g.fill();circ(g,chx+1.8,chy-17.4,2.1);g.fill();
// door on the SE shopfront
fillP(g,[Pse(0.08,0.02),Pse(0.24,0.02),Pse(0.24,0.44),Pse(0.08,0.44)],"#5a4130",1.3);
{const k=Pse(0.215,0.22);g.fillStyle="#e9c56a";circ(g,k[0],k[1],0.9);g.fill();}
// glowing bay shop-window full of potion bottles (the real identity)
const w=[Pse(0.32,0.06),Pse(0.74,0.06),Pse(0.74,0.5),Pse(0.32,0.5)];
fillP(g,w,"#f6c23f",1.3);
g.strokeStyle="rgba(90,60,30,.45)";g.lineWidth=0.8;
let a=Pse(0.53,0.06),b=Pse(0.53,0.5);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();
a=Pse(0.32,0.28);b=Pse(0.74,0.28);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();
// little coloured bottles on the sill
["#8e5cc0","#4b86b6","#6ab04a","#c94b6f"].forEach((cc,i)=>{const bb=Pse(0.38+i*0.095,0.13);g.fillStyle=cc;g.beginPath();g.ellipse(bb[0],bb[1],1.05,1.7,0,0,7);g.fill();});
// dried herb bundle hanging by the door
const hb=Pse(0.28,0.52);g.strokeStyle="#6f8a3a";g.lineWidth=1.3;g.lineCap="round";
for(const dx of [-1.2,0.2,1.4]){g.beginPath();g.moveTo(hb[0],hb[1]);g.lineTo(hb[0]+dx,hb[1]+5.5);g.stroke();}
// hanging mortar-&-pestle sign
shopSign(g,px+15,py+5,"#e7dcf2",(cx,cy)=>{fillP(g,[[cx-3,cy-0.3],[cx+3,cy-0.3],[cx+2,cy+3],[cx-2,cy+3]],"#b3a6c7",1.1);
g.fillStyle="#dcd3e6";g.beginPath();g.ellipse(cx,cy-0.3,3,1,0,0,7);g.fill();
g.strokeStyle="#6b4a30";g.lineWidth=1.5;g.lineCap="round";g.beginPath();g.moveTo(cx+0.5,cy-0.8);g.lineTo(cx+3,cy-4);g.stroke();});}
function artBrewery(g,px,py,winter){shadow(g,px,py+5,23);
// a real brewhouse: stone base + timbered upper, gabled roof, steaming copper flue
const wall="#c9b59a",c=isoWalls(g,px-1,py,16,8.5,15,wall);
// stone base course
fillP(g,[c.W,c.S_,faceP(c.W,c.S_,c.Wt)(1,0.3),faceP(c.W,c.S_,c.Wt)(0,0.3)],"#b7b2ab",1.5);
fillP(g,[c.S_,c.E,faceP(c.S_,c.E,c.St)(1,0.3),faceP(c.S_,c.E,c.St)(0,0.3)],dark("#b7b2ab",0.12),1.5);
timberFace(g,c.W,c.S_,c.Wt,"#6b4a30");
const Pse=timberFace(g,c.S_,c.E,c.St,"#5f4128");
ridgeRoof(g,c,12,"#8a5a3c",winter);
// copper flue with rising steam (the boil)
const fx=px-8,fy=c.St[1]-7;
fillP(g,[[fx-2.4,fy],[fx+2.4,fy],[fx+2,fy-11],[fx-2,fy-11]],"#b5793f",1.3);
fillE(g,fx,fy-11,2.6,1.1,"#caa06a",1.1);
g.fillStyle="rgba(246,246,250,.6)";for(const [sx,sy,sr] of [[fx,fy-15,1.8],[fx+1.4,fy-18.5,2.2],[fx-0.4,fy-22,2.6]]){circ(g,sx,sy,sr);g.fill();}
// arched brewhouse doorway + a warm window
g.beginPath();g.moveTo(Pse(0.12,0.02)[0],Pse(0.12,0.02)[1]);g.lineTo(Pse(0.12,0.4)[0],Pse(0.12,0.4)[1]);
{const t=Pse(0.24,0.52),r=Pse(0.36,0.4);g.quadraticCurveTo(t[0],t[1],r[0],r[1]);}
g.lineTo(Pse(0.36,0.02)[0],Pse(0.36,0.02)[1]);g.closePath();g.fillStyle="#4a3826";g.fill();inkStroke(g,1.3);
warmWin(g,Pse,0.5,0.28,0.74,0.56,"#f6cb5c");
// barrels stacked outside (the signature)
artBarrel(g,px+15,py+13,4.6);artBarrel(g,px+20,py+11,4.2);artBarrel(g,px+17.5,py+7,4.4);
fillP(g,[[px-20,py+13],[px-13,py+13],[px-14,py+5.5],[px-19,py+5.5]],"#d8c79a",1.3); // grain sack
g.strokeStyle="#b6a374";g.lineWidth=0.8;g.beginPath();g.moveTo(px-18.4,py+7.2);g.lineTo(px-13.9,py+7.2);g.stroke();}
function artRanch(g,px,py,winter){shadow(g,px,py+5,24);
const wall="#b0533f",c=isoWalls(g,px,py,18,9,14,wall); // barn-red
const Psw=plankFace(g,c.W,c.S_,c.Wt,dark(wall,0.16),5),Pse=faceP(c.S_,c.E,c.St);
ridgeRoof(g,c,13,"#8a8f98",winter);
// white gable trim + hayloft door high on the SW gable
fillP(g,[Psw(0.4,0.62),Psw(0.6,0.62),Psw(0.6,0.9),Psw(0.4,0.9)],"#c9a066",1.2);
g.strokeStyle="#f2ead6";g.lineWidth=1;let a=Psw(0.5,0.62),b=Psw(0.5,0.9);g.beginPath();g.moveTo(a[0],a[1]);g.lineTo(b[0],b[1]);g.stroke();
// big barn door on SE — brown planks, clean white frame + X
const dcx=(c.S_[0]+c.E[0])/2,dcy=(c.S_[1]+c.E[1])/2;
const D=[[dcx-7,dcy-1],[dcx+7,dcy-8],[dcx+7,dcy-19],[dcx-7,dcy-12]];
fillP(g,D,"#8a5a3c",1.4);
g.strokeStyle="#f2ead6";g.lineWidth=1.6;g.lineCap="round";
g.beginPath();g.moveTo(D[0][0],D[0][1]);g.lineTo(D[2][0],D[2][1]);g.moveTo(D[1][0],D[1][1]);g.lineTo(D[3][0],D[3][1]);g.stroke();
g.lineWidth=1.1;g.beginPath();g.moveTo((D[0][0]+D[3][0])/2,(D[0][1]+D[3][1])/2);g.lineTo((D[1][0]+D[2][0])/2,(D[1][1]+D[2][1])/2);g.stroke();
// weathervane on the ridge
g.strokeStyle="#4b4b52";g.lineWidth=1.2;g.lineCap="round";g.beginPath();g.moveTo(c.St[0],c.St[1]-13);g.lineTo(c.St[0],c.St[1]-19);g.stroke();
fillP(g,[[c.St[0],c.St[1]-19],[c.St[0]+5,c.St[1]-18],[c.St[0],c.St[1]-16.5]],"#4b4b52",1);
// round hay bale out front
const hx=px+15,hy=py+12;
fillP(g,[[hx-5,hy-3],[hx+5,hy-3],[hx+5,hy+2],[hx-5,hy+2]],"#dcc373",1.3);
fillE(g,hx,hy-3,5,2.2,"#e6d38a",1.2);
g.strokeStyle="#c2ab5e";g.lineWidth=0.8;for(const o of [-2.6,0,2.6]){g.beginPath();g.moveTo(hx+o,hy-2.4);g.lineTo(hx+o,hy+1.6);g.stroke();}}
function artButcher(g,px,py,winter){shadow(g,px,py+5,22);
const wall="#ecd8b6",c=isoWalls(g,px,py,16,8,14,wall);
timberFace(g,c.W,c.S_,c.Wt,"#7a5230");
const Pse=timberFace(g,c.S_,c.E,c.St,"#6b4a2a");
ridgeRoof(g,c,11,"#9c4f42",winter);
chimney(g,px-8,c.St[1]-7,"#9c6b5a");
const S_=c.S_,E=c.E;
// dark shopfront window with hanging meat behind glass
fillP(g,[Pse(0.2,0.06),Pse(0.82,0.06),Pse(0.82,0.46),Pse(0.2,0.46)],"#5a4632",1.3);
["#c96b6f","#d98a86","#b0533f"].forEach((cc,i)=>{const b=Pse(0.32+i*0.19,0.34);g.strokeStyle="#3a2f26";g.lineWidth=0.7;g.beginPath();g.moveTo(b[0],b[1]);g.lineTo(b[0],b[1]-4);g.stroke();g.fillStyle=cc;g.beginPath();g.ellipse(b[0],b[1],1.6,1.1,0,0,7);g.fill();});
// striped awning over the shopfront — scalloped bottom edge (signature)
const a0=[S_[0]-1,S_[1]-7],a1=[E[0]+1,E[1]-7],b0=[S_[0]+1,S_[1]+4],b1=[E[0]-1,E[1]+4];
for(let i=0;i<7;i++){const t0=i/7,t1=(i+1)/7,col=i%2?"#f4ecd8":"#c94b3f";
const p0=[a0[0]+(a1[0]-a0[0])*t0,a0[1]+(a1[1]-a0[1])*t0],p1=[a0[0]+(a1[0]-a0[0])*t1,a0[1]+(a1[1]-a0[1])*t1];
const q0=[b0[0]+(b1[0]-b0[0])*t0,b0[1]+(b1[1]-b0[1])*t0],q1=[b0[0]+(b1[0]-b0[0])*t1,b0[1]+(b1[1]-b0[1])*t1];
fillP(g,[p0,p1,q1,q0],col,1.1);
g.fillStyle=col;g.beginPath();g.ellipse((q0[0]+q1[0])/2,(q0[1]+q1[1])/2+1.3,2.2,1.7,0,0,7);g.fill();inkStroke(g,1);}
// hanging ham sign
shopSign(g,px-15,py+5,"#efe0b6",(cx,cy)=>{g.fillStyle="#e39a94";g.beginPath();g.ellipse(cx+0.6,cy+0.2,3.4,2.5,0.3,0,7);g.fill();inkStroke(g,1.1);
g.fillStyle="#f4ecd8";circ(g,cx-2.6,cy-1.2,1.3);g.fill();inkStroke(g,0.9);});}
function artNursery(g,px,py,winter){shadow(g,px,py+5,20);
const wall="#f3d4de",c=isoWalls(g,px,py,15,8,13,wall); // soft pink plaster
const Psw=faceP(c.W,c.S_,c.Wt),Pse=faceP(c.S_,c.E,c.St);
const rr=ridgeRoof(g,c,11,"#cf6f9c",winter);
chimney(g,px-8,c.St[1]-7,"#c98aa8");
// arched door + warm windows
const dc=Pse(0.5,0.02);
g.beginPath();g.moveTo(Pse(0.4,0.02)[0],Pse(0.4,0.02)[1]);g.lineTo(Pse(0.4,0.34)[0],Pse(0.4,0.34)[1]);
{const t=Pse(0.5,0.46),r=Pse(0.6,0.34);g.quadraticCurveTo(t[0],t[1],r[0],r[1]);}
g.lineTo(Pse(0.6,0.02)[0],Pse(0.6,0.02)[1]);g.closePath();g.fillStyle="#c98aa8";g.fill();inkStroke(g,1.3);
warmWin(g,Pse,0.16,0.3,0.34,0.56,"#f7e6ef");
warmWin(g,Psw,0.5,0.3,0.72,0.56,"#f7e6ef");
// bunting flags across the SE eaves
const flg=["#e06e78","#f2c14a","#6ab04a","#4b86b6"];
for(let i=0;i<4;i++){const t=0.12+i*0.24,p=Pse(t,0.96);fillP(g,[[p[0],p[1]],[p[0]+3,p[1]-0.4],[p[0]+1.5,p[1]+3.2]],flg[i],1);}
// hanging heart above the ridge
g.strokeStyle="#7a5230";g.lineWidth=1.3;g.beginPath();g.moveTo(rr.Rf[0],rr.Rf[1]);g.lineTo(rr.Rf[0],rr.Rf[1]-6);g.stroke();
artHeart(g,rr.Rf[0],rr.Rf[1]-8,3,"#e06e78");}
function artSchool(g,px,py,winter){shadow(g,px,py+5,22);
const wall="#efe3c4",c=isoWalls(g,px,py,16,8.5,15,wall);
plankFace(g,c.W,c.S_,c.Wt,dark(wall,0.14),4);
const Pse=faceP(c.S_,c.E,c.St);
const rr=ridgeRoof(g,c,11,"#b7863a",winter);
// white bell tower over the FRONT gable (classic schoolhouse)
const bx=rr.Rf[0],by=rr.Rf[1];
fillP(g,[[bx-3.4,by+1],[bx+3.4,by+1],[bx+3.4,by-7],[bx-3.4,by-7]],"#f4ecd8",1.4); // tower box
fillC(g,bx,by-3,1.9,"#6b6b72",1); // bell
fillP(g,[[bx-4,by-7],[bx+4,by-7],[bx,by-15]],"#b04a3a",1.4); // spire
g.strokeStyle="#7a5230";g.lineWidth=1.2;g.beginPath();g.moveTo(bx,by-15);g.lineTo(bx,by-19);g.stroke();
fillP(g,[[bx,by-19],[bx+7,by-17.6],[bx,by-16]],"#4b86b6",1.1); // flag
// gable clock
fillC(g,c.St[0],c.St[1]-7,2.4,"#f4ecd8",1.2);g.strokeStyle=INK;g.lineWidth=0.7;g.beginPath();g.moveTo(c.St[0],c.St[1]-7);g.lineTo(c.St[0],c.St[1]-8.6);g.moveTo(c.St[0],c.St[1]-7);g.lineTo(c.St[0]+1.2,c.St[1]-6.4);g.stroke();
// arched door + tall windows on the front
g.beginPath();g.moveTo(Pse(0.42,0.02)[0],Pse(0.42,0.02)[1]);g.lineTo(Pse(0.42,0.36)[0],Pse(0.42,0.36)[1]);
{const t=Pse(0.5,0.48),r=Pse(0.58,0.36);g.quadraticCurveTo(t[0],t[1],r[0],r[1]);}
g.lineTo(Pse(0.58,0.02)[0],Pse(0.58,0.02)[1]);g.closePath();g.fillStyle="#8a6b45";g.fill();inkStroke(g,1.3);
warmWin(g,Pse,0.16,0.24,0.32,0.6,"#bcd6e0");warmWin(g,Pse,0.68,0.24,0.84,0.6,"#bcd6e0");}
function artWolfDen(g,px,py,winter){shadow(g,px,py+5,22);
const wall="#9a938a",c=isoWalls(g,px,py,15,8,13,wall); // timber lodge
plankFace(g,c.W,c.S_,c.Wt,dark(wall,0.18),4);
const Pse=faceP(c.S_,c.E,c.St);
ridgeRoof(g,c,12,"#5f5f66",winter);
// round gable window
fillC(g,c.St[0],c.St[1]-7,2.4,"#3f2f22",1.2);
// big arched kennel mouth (signature) on the front
const dcx=(c.S_[0]+c.E[0])/2,dcy=(c.S_[1]+c.E[1])/2;
g.beginPath();g.moveTo(dcx-6,dcy+2);g.lineTo(dcx-6,dcy-7);g.arc(dcx,dcy-7,6,Math.PI,0);g.lineTo(dcx+6,dcy+2);g.closePath();g.fillStyle="#2f2722";g.fill();inkStroke(g,1.5);
g.strokeStyle="#6f4a2c";g.lineWidth=1.4;g.beginPath();g.arc(dcx,dcy-7,6.6,Math.PI,0);g.stroke(); // timber arch frame
// low fenced yard out front
g.strokeStyle="#8a6b45";g.lineWidth=1.5;g.lineCap="round";
for(const [fx,fy] of [[px-14,py+9],[px-8,py+12],[px-1,py+14],[px+7,py+13]]){g.beginPath();g.moveTo(fx,fy);g.lineTo(fx,fy-6);g.stroke();}
g.beginPath();g.moveTo(px-14,py+5.5);g.lineTo(px+7,py+8);g.stroke();
// bone sign on a post
shopSign(g,px+14,py+5,"#efe6d6",(cx,cy)=>{g.fillStyle="#f2ead6";g.strokeStyle=INK;g.lineWidth=1;
g.beginPath();g.arc(cx-3,cy-1.5,1.5,0,7);g.arc(cx-3,cy+1.5,1.5,0,7);g.arc(cx+3,cy-1.5,1.5,0,7);g.arc(cx+3,cy+1.5,1.5,0,7);g.fill();
g.fillRect(cx-3,cy-1.4,6,2.8);g.beginPath();g.moveTo(cx-2.5,cy-1.6);g.lineTo(cx+2.5,cy-1.6);g.moveTo(cx-2.5,cy+1.6);g.lineTo(cx+2.5,cy+1.6);g.stroke();});}
// ---- Decorative items (cosmetic; a prettier village lifts everyone's mood a little) ----
function artFlowerbed(g,px,py,winter){shadow(g,px,py+3,12);
fillP(g,[[px,py+5],[px+11,py],[px,py-5],[px-11,py]],"#8a6b45",1.4);
fillP(g,[[px,py+3.2],[px+8.5,py],[px,py-3.2],[px-8.5,py]],"#9c7a4e",1);
if(winter){fillP(g,[[px,py+3.2],[px+8.5,py],[px,py-3.2],[px-8.5,py]],"rgba(255,255,255,.6)",0);return;}
var cols=["#e0607a","#f2b84a","#c86ec8","#6aa9e0","#e57e8e"],spots=[[-6,0.5],[-2,-1.5],[2.5,1],[6,-0.5],[0,1.8],[-3.5,1.6]];
for(var i=0;i<spots.length;i++){var fx=px+spots[i][0],fy=py+spots[i][1];
g.strokeStyle="#5f8a3a";g.lineWidth=1;g.lineCap="round";g.beginPath();g.moveTo(fx,fy);g.lineTo(fx,fy-4.5);g.stroke();
g.fillStyle=cols[i%cols.length];for(var k=0;k<5;k++){var a=k/5*6.283;circ(g,fx+Math.cos(a)*1.5,fy-4.5+Math.sin(a)*1.5,1.15);g.fill();}
g.fillStyle="#fff3c0";circ(g,fx,fy-4.5,0.85);g.fill();}}
function artLantern(g,px,py,winter){shadow(g,px,py+3,6);
var ph=((S.tsec||0)%TOD_LEN)/TOD_LEN,night=ph>0.55&&ph<0.92;
g.strokeStyle="#5a4835";g.lineWidth=2.4;g.lineCap="round";
g.beginPath();g.moveTo(px,py+2);g.lineTo(px,py-15);g.lineTo(px+4,py-15);g.stroke();
var lx=px+4,ly=py-11.5;
if(night){var gr=g.createRadialGradient(lx,ly,0,lx,ly,11);gr.addColorStop(0,"rgba(255,224,130,.85)");gr.addColorStop(1,"rgba(255,224,130,0)");g.fillStyle=gr;circ(g,lx,ly,11);g.fill();}
fillP(g,[[lx-2.6,ly-3.2],[lx+2.6,ly-3.2],[lx+2.6,ly+3.2],[lx-2.6,ly+3.2]],night?"#ffe89a":"#cdbf9a",1.1);
g.fillStyle=night?"#fff4c2":"#e8dcc0";g.fillRect(lx-1.3,ly-2,2.6,4);
fillP(g,[[lx-3.2,ly-3.2],[lx+3.2,ly-3.2],[lx+2,ly-5.6],[lx-2,ly-5.6]],"#6b5640",1);}
function artWell(g,px,py,winter){shadow(g,px,py+3,12);
fillE(g,px,py,10,5,"#b7bdc2",1.5);fillE(g,px,py-1.5,8,3.8,"#9aa0a6",1.2);
g.fillStyle="#3a4550";g.beginPath();g.ellipse(px,py-1.5,6,2.9,0,0,7);g.fill();
g.strokeStyle="#6b5640";g.lineWidth=2.2;g.lineCap="round";
g.beginPath();g.moveTo(px-7,py-1);g.lineTo(px-6,py-15);g.moveTo(px+7,py-1);g.lineTo(px+6,py-15);g.stroke();
fillP(g,[[px-9,py-14],[px+9,py-14],[px+5,py-20],[px-5,py-20]],"#a35a4a",1.2);
g.strokeStyle="#5a4835";g.lineWidth=1.2;g.beginPath();g.moveTo(px,py-13.5);g.lineTo(px,py-6);g.stroke();
fillP(g,[[px-1.6,py-6],[px+1.6,py-6],[px+1.2,py-3],[px-1.2,py-3]],"#7a5230",1);}
function artBench(g,px,py,winter){shadow(g,px,py+2,11);var seatC="#b0895a";
g.strokeStyle="#5a4835";g.lineWidth=2;g.lineCap="round";
g.beginPath();g.moveTo(px-7,py-1);g.lineTo(px-7,py+3);g.moveTo(px+7,py-2);g.lineTo(px+7,py+2);g.stroke();
fillP(g,[[px-9,py-1],[px+9,py-3],[px+9,py-1.5],[px-9,py+0.5]],seatC,1.2);
g.strokeStyle="#5a4835";g.lineWidth=1.7;g.beginPath();g.moveTo(px-7,py-1.5);g.lineTo(px-7,py-8);g.moveTo(px+7,py-2.5);g.lineTo(px+7,py-9);g.stroke();
fillP(g,[[px-8,py-7],[px+8,py-9],[px+8,py-7.6],[px-8,py-5.6]],seatC,1.1);}
function artStatue(g,px,py,winter){shadow(g,px,py+3,9);
fillP(g,[[px-6,py+3],[px+6,py+3],[px+6,py-1],[px-6,py-1]],"#aab0b6",1.3); // pedestal base
fillP(g,[[px-5,py-1],[px+5,py-1],[px+4,py-4],[px-4,py-4]],"#c6ccd1",1.1); // pedestal top
var cx=px,cy=py-4,stone="#c4c9ce";
g.strokeStyle="#aab0b6";g.lineWidth=1.9;g.lineCap="round"; // little stone arms
g.beginPath();g.moveTo(cx-3.6,cy-8);g.lineTo(cx-5.2,cy-5);g.moveTo(cx+3.6,cy-8);g.lineTo(cx+5.2,cy-5);g.stroke();
g.fillStyle=stone;g.beginPath();g.moveTo(cx-4,cy-2);g.quadraticCurveTo(cx-5,cy-16,cx,cy-16);g.quadraticCurveTo(cx+5,cy-16,cx+4,cy-2);g.closePath();g.fill();inkStroke(g,1.3); // carved body
g.fillStyle="#dadfe3";g.beginPath();g.ellipse(cx-1.4,cy-11,1.5,4.2,0.18,0,7);g.fill(); // stone highlight
g.fillStyle="#868c92";circ(g,cx-1.6,cy-11,0.7);g.fill();circ(g,cx+1.6,cy-11,0.7);g.fill(); // carved eyes
g.strokeStyle="#868c92";g.lineWidth=0.7;g.beginPath();g.arc(cx,cy-9.2,1.4,0.25,Math.PI-0.25);g.stroke();} // carved smile
const BLDART={craft:artCraftShop,sawmill:artSawmill,kitchen:artKitchen,herbalist:artApothecary,
brewery:artBrewery,ranch:artRanch,butcher:artButcher,nursery:artNursery,school:artSchool,wolfcentre:artWolfDen,
flowerbed:artFlowerbed,lantern:artLantern,well:artWell,bench:artBench,statue:artStatue};
// themed falling particles for the seasonal festivals (confetti=Summer, leaf=Fall Harvest, petal=Spring Bloom)
let festP=[];
const FEST_SET={confetti:{cols:["#e06e78","#f2c14a","#6ab04a","#4b86b6","#b1567f","#e0913f"],fall:1.3,sway:0.6,shape:"sq"},
leaf:{cols:["#d98a3a","#c85a2a","#e0b24a","#a8642e","#c89a3a"],fall:0.85,sway:1.15,shape:"leaf"},
petal:{cols:["#f4c2d0","#f8dbe6","#ffffff","#f0b8cf","#e79ec0"],fall:0.65,sway:1.2,shape:"petal"}};
function drawFestival(kind){const set=FEST_SET[kind]||FEST_SET.confetti;
if(festP.length===0)for(let i=0;i<44;i++)festP.push({x:Math.random()*VW,y:Math.random()*VH,s:0.6+Math.random(),ci:i,r:Math.random()*6});
for(const f of festP){f.y+=f.s*set.fall;f.x+=Math.sin(f.y*0.06)*set.sway;f.r+=0.18;if(f.y>VH){f.y=-8;f.x=Math.random()*VW;}
ctx.save();ctx.translate(f.x,f.y);ctx.rotate(f.r);ctx.fillStyle=set.cols[f.ci%set.cols.length];
if(set.shape==="sq")ctx.fillRect(-1.7,-1.7,3.4,3.4);
else if(set.shape==="leaf"){ctx.beginPath();ctx.ellipse(0,0,1.4,2.9,0,0,7);ctx.fill();ctx.strokeStyle="rgba(90,72,53,.45)";ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,-2.6);ctx.lineTo(0,2.6);ctx.stroke();}
else {ctx.beginPath();ctx.ellipse(0,0,2.5,1.5,0,0,7);ctx.fill();}
ctx.restore();}}
let snow=[];
function drawSnow(){if(snow.length===0)for(let i=0;i<45;i++)snow.push({x:Math.random()*VW,y:Math.random()*VH,s:0.4+Math.random()});
ctx.fillStyle="rgba(255,255,255,0.85)";
for(const f of snow){f.y+=f.s*0.7;f.x+=Math.sin(f.y*0.05)*0.3;if(f.y>VH){f.y=-5;f.x=Math.random()*VW;}circ(ctx,f.x,f.y,f.s*1.6);ctx.fill();}}
let rain=[];
function drawRain(){if(rain.length===0)for(let i=0;i<95;i++)rain.push({x:Math.random()*VW,y:Math.random()*VH,s:6+Math.random()*5});
ctx.fillStyle="rgba(120,140,170,0.08)";ctx.fillRect(0,0,VW,VH); // light overcast wash
ctx.strokeStyle="rgba(150,175,205,0.5)";ctx.lineWidth=1.1;ctx.lineCap="round";
for(const d of rain){d.y+=d.s*2.4;d.x+=d.s*0.35;if(d.y>VH){d.y=-10;d.x=Math.random()*VW-20;}ctx.beginPath();ctx.moveTo(d.x,d.y);ctx.lineTo(d.x-2.6,d.y-8.5);ctx.stroke();}}
// ambient world life: butterflies flutter by day in warm seasons, fireflies glow at night — makes the island breathe
let bflies=[], flies=[];
function drawAmbient(){if(!ui.anim||!S)return;
const t=S.tsec||0, ph=(t%TOD_LEN)/TOD_LEN, night=ph>0.6&&ph<0.9, warm=(season()==="Spring"||season()==="Summer");
if(isWinter())return;
if(night){
if(flies.length===0)for(let i=0;i<16;i++)flies.push({x:Math.random()*VW,y:VH*0.3+Math.random()*VH*0.55,ph:Math.random()*6.28});
for(const fl of flies){const glow=(Math.sin(t*2.2+fl.ph)+1)/2;
fl.x+=Math.sin(t*0.5+fl.ph)*0.35; fl.y+=Math.cos(t*0.42+fl.ph*1.3)*0.22;
if(fl.x<0)fl.x=VW; if(fl.x>VW)fl.x=0;
ctx.fillStyle="rgba(200,230,120,"+(0.22+glow*0.45).toFixed(3)+")";circ(ctx,fl.x,fl.y,2.6+glow*1.6);ctx.fill(); // soft green halo
ctx.fillStyle="rgba(255,246,170,"+(0.35+glow*0.55).toFixed(3)+")";circ(ctx,fl.x,fl.y,1.1+glow*0.9);ctx.fill(); // warm core
ctx.fillStyle="rgba(255,255,235,"+(glow*0.7).toFixed(3)+")";circ(ctx,fl.x,fl.y,0.6);ctx.fill();}
} else if(warm){
if(bflies.length===0)for(let i=0;i<5;i++)bflies.push({x:Math.random()*VW,y:VH*0.2+Math.random()*VH*0.55,ph:Math.random()*6.28,c:["#e57e8e","#e0a0d0","#efd24a","#a9c8ef","#efb0c0"][i%5],dir:Math.random()<0.5?1:-1});
for(const bf of bflies){bf.x+=bf.dir*0.5+Math.sin(t*1.3+bf.ph)*0.4; bf.y+=Math.cos(t*2.1+bf.ph)*0.55;
if(bf.x>VW+12)bf.x=-12; if(bf.x<-12)bf.x=VW+12; if(bf.y<VH*0.12)bf.y=VH*0.12; if(bf.y>VH*0.8)bf.y=VH*0.8;
const flap=Math.abs(Math.sin(t*11+bf.ph))*2.6+0.6;
ctx.save();ctx.translate(bf.x,bf.y);ctx.fillStyle=bf.c;
ctx.beginPath();ctx.ellipse(-flap,0,2.3,3.3,-0.5,0,7);ctx.fill();
ctx.beginPath();ctx.ellipse(flap,0,2.3,3.3,0.5,0,7);ctx.fill();
ctx.strokeStyle=INK;ctx.lineWidth=0.9;ctx.lineCap="round";ctx.beginPath();ctx.moveTo(0,-2.4);ctx.lineTo(0,2.4);ctx.stroke();
ctx.restore();}}}
/* ================= INPUT ================= */
let drag={on:false,moved:false,x:0,y:0,cx:0,cy:0};
function setupInput(){
const down=(x,y)=>{drag.on=true;drag.moved=false;drag.x=x;drag.y=y;drag.cx=cam.x;drag.cy=cam.y;ui.hoverX=x;ui.hoverY=y;};
const move=(x,y)=>{ui.hoverX=x;ui.hoverY=y;if(!drag.on)return;
if(Math.abs(x-drag.x)+Math.abs(y-drag.y)>6)drag.moved=true;if(!ui.placing){cam.x=drag.cx+(x-drag.x);cam.y=drag.cy+(y-drag.y);}};
const up=(x,y)=>{if(!drag.on)return;drag.on=false;if(!drag.moved)tap(x,y);};
cv.addEventListener("mousedown",e=>down(e.clientX,e.clientY));
window.addEventListener("mousemove",e=>move(e.clientX,e.clientY));
window.addEventListener("mouseup",e=>up(e.clientX,e.clientY));
// mouse wheel zoom (desktop) — zoom toward the cursor
cv.addEventListener("wheel",e=>{e.preventDefault();zoomAt(e.clientX,e.clientY,cam.z*(e.deltaY<0?1.12:0.89));},{passive:false});
// touch: 1 finger pans/taps, 2 fingers pinch-zoom
let pinch=null;const tdist=t=>Math.hypot(t[0].clientX-t[1].clientX,t[0].clientY-t[1].clientY);
const tmid=t=>[(t[0].clientX+t[1].clientX)/2,(t[0].clientY+t[1].clientY)/2];
cv.addEventListener("touchstart",e=>{if(e.touches.length>=2){pinch={d:tdist(e.touches),z:cam.z};drag.on=false;}
else{const t=e.touches[0];down(t.clientX,t.clientY);}},{passive:true});
cv.addEventListener("touchmove",e=>{if(e.touches.length>=2&&pinch){const m=tmid(e.touches);zoomAt(m[0],m[1],pinch.z*tdist(e.touches)/pinch.d);}
else{const t=e.touches[0];move(t.clientX,t.clientY);}},{passive:true});
cv.addEventListener("touchend",e=>{if(e.touches.length<2)pinch=null;
if(e.touches.length===0){const t=e.changedTouches[0];up(t.clientX,t.clientY);}},{passive:true});
}
function tap(sx,sy){if(ui.placing){tryPlace(sx,sy);return;}
// Pick by screen-space proximity to each sprite's DRAWN anchor (not ground-tile distance) so a tap
// selects whatever you actually pressed on — even when other objects are close by. Everything is scored
// in pure-iso space (worldToScreen space); radii are in iso px, so picking feels the same at any zoom.
const z=cam.z||1, px=(sx-cam.x)/z, py=(sy-cam.y)/z, cand=[];
const add=(gx,gy,ay,rad,kind,obj)=>{const a=worldToScreen(gx,gy);
const s=Math.hypot(px-a.x,py-(a.y+ay))/rad; if(s<1)cand.push({s,front:gx+gy,kind,obj});};
for(const m of S.mellows) add(m.gx,m.gy,-14,14,"mel",m); // little bodies drawn at p.y-6, ~24 tall
for(const w of S.wolves) add(w.gx,w.gy,-7,15,"wolf",w);
for(const b of S.buildings){const info=BUILD[b.type],sz=info.size||1;
if(info.dock){for(const t of bldTiles(b.type,b.gx,b.gy,b.dir))add(t[0],t[1],-10,22,"bld",b);continue;}
if(sz>1){for(const t of bldTiles(b.type,b.gx,b.gy))add(t[0],t[1],-8,26,"bld",b);continue;} // cover a 2×2 plot tile-by-tile
add(b.gx,b.gy, info.small?-8:-16, info.small?16:23, "bld",b);}
if(!cand.length)return;
cand.sort((A,B)=> Math.abs(A.s-B.s)<0.15 ? B.front-A.front : A.s-B.s); // nearest sprite; near-ties → front-most (visually on top)
const p=cand[0];
if(p.kind==="mel")openProfile(p.obj); else if(p.kind==="bld")openBuilding(p.obj); else openWolf(p.obj);}
function openWolf(w){ui.selBld=null;ui.selMel=null;const c=$("bldCard");
const sex=w.sex==="male"?"♂":"♀",age=wolfBaby(w)?"pup":"adult";
if(w.state==="family"){const head=S.mellows.find(m=>m.home===w.homeId&&!isChild(m));
c.innerHTML=`<h2>Family Wolf ${sex}</h2><div class="mini">A loyal ${age} — part of ${head?head.name+"'s family":"a family"}. It guards them (especially the little ones), lifts their spirits, and helps a Shepherd in the family work faster.</div>`;}
else if(w.state==="trained"||w.state==="training"){
c.innerHTML=`<h2>${w.state==="trained"?"Trained":"Training"} Wolf ${sex}</h2><div class="mini">A ${age} at the Wolf Den. ${w.state==="training"?"Still learning — ready to adopt "+(wolfAdoptable(w)?"now":"soon")+".":"Ready for a family to adopt."} Use the Wolf Den's <b>Adopt</b> button.</div>`;}
else{c.innerHTML=`<h2>Wild Wolf ${sex}</h2><div class="mini">A wild ${age} that roams its pack's territory and can harm Mellows — hunters, miners and lumberjacks most of all. Build a <b>Wolf Den</b> and a Wolf Trainer will tame the pack (8 meat each).</div>`;}
$("bldOverlay").style.display="flex";}
function tryPlace(sx,sy){const g=screenToGrid(sx,sy),gx=Math.round(g.gx),gy=Math.round(g.gy);
if(gx<0||gy<0||gx>=GRID||gy>=GRID)return;const type=ui.placing;
if(BUILD[type].needsCraft&&!S.buildings.some(b=>b.built&&b.type==="craft")){toast("Build a Craft Shop first — boats are made there");return;}
if(BUILD[type].dock){const dp=dockPlace(gx,gy);
if(!dp){toast("Tap a water tile next to reachable land");return;}
const dc=scaledCost(type);for(const k in dc)if((S.res[k]||0)<dc[k]){toast("Not enough "+k);return;}
for(const k in dc)S.res[k]-=dc[k];const nb=addBuilding(type,dp.gx,dp.gy,false);nb.dir=dp.dir;
toast("Placed Dock — a Builder will construct it");if(!affordable(type))endPlacing();refreshBuildBar();return;}
const needWater=BUILD[type].bridge||BUILD[type].boat;
const tiles=bldTiles(type,gx,gy);
if(tiles.some(t=>t[0]<0||t[1]<0||t[0]>=GRID||t[1]>=GRID)){toast("It doesn't fit there");return;}
if(needWater){ // single-tile water structures
if(S.tiles[gy][gx]!=="water"){toast("Place on water");return;}
if(tileTaken(gx,gy)){toast("Tile is taken");return;}
if((BUILD[type].bridge||BUILD[type].boat)&&![[1,0],[-1,0],[0,1],[0,-1]].some(d=>reachable(gx+d[0],gy+d[1]))){toast("Place it beside reachable land");return;}
} else { // land building — every tile of its footprint must be clear, land & reachable
for(const [tx,ty] of tiles){
if(S.tiles[ty][tx]==="water"){toast("Can't build on water");return;}
if(!reachable(tx,ty)){toast("Your Mellows can't reach there — bridge the water first");return;}
if(tileTaken(tx,ty)){toast("Not enough room — needs a clear "+(BUILD[type].size||1)+"×"+(BUILD[type].size||1)+" space");return;}
if(S.nodes.some(n=>n.gx===tx&&n.gy===ty&&!n.dormant&&n.amount>0)){toast("Clear that spot first");return;}
}
}
const cost=scaledCost(type);for(const k in cost)if((S.res[k]||0)<cost[k]){toast("Not enough "+k);return;}
const deco=!!BUILD[type].decor;
for(const k in cost)S.res[k]-=cost[k];addBuilding(type,gx,gy,deco); // decor is placed finished (cosmetic, no builder needed)
if(deco){computeReach();toast("Placed "+BUILD[type].name);} else toast("Placed "+BUILD[type].name+" — a Builder will construct it");
if(!affordable(type))endPlacing();refreshBuildBar();}
function scaledCost(type){const base=BUILD[type].cost,m=difficulty().cost,out={};for(const k in base)out[k]=Math.round(base[k]*m);return out;}
// Building upgrades: production/farm/pasture buildings can be upgraded once for +40% output, marked by a banner.
const UPGRADE_MULT=1.4;
function canUpgrade(b){const info=BUILD[b.type];return b.built&&!b.upg&&!!(info.recipe||info.craftTools||info.farm||info.pasture);}
function upgradeCost(type){const sc=scaledCost(type),out={};for(const k in sc)out[k]=Math.max(1,Math.ceil(sc[k]*0.6));return out;}
function upgMult(b){return b&&b.upg?UPGRADE_MULT:1;}
function tryUpgrade(b){const cost=upgradeCost(b.type);
for(const k in cost)if((S.res[k]||0)<cost[k]){toast("Not enough "+(RES_NAME[k]||k)+" to upgrade");return false;}
for(const k in cost)S.res[k]-=cost[k];b.upg=true;
toast("⭐ "+BUILD[b.type].name+" upgraded — +40% output!");logEvent("Upgraded the "+BUILD[b.type].name+" (+40% output).");
if(ui.sound)Sound.sfx('build');return true;}
function affordable(type){const c=scaledCost(type);for(const k in c)if((S.res[k]||0)<c[k])return false;return true;}
/* ================= UI ================= */
function $(id){return document.getElementById(id);}
function buildStartScreen(){
const grid=$("isleGrid");grid.innerHTML="";let selIsle="meadow",selDiff="Normal";
for(const k in ISLANDS){const is=ISLANDS[k];const d=document.createElement("div");d.className="isle"+(k===selIsle?" sel":"");
d.innerHTML=`<canvas width="180" height="116"></canvas><b>${is.name}</b><small>${is.blurb}</small>`;
d.onclick=()=>{selIsle=k;[...grid.children].forEach(c=>c.classList.remove("sel"));d.classList.add("sel");};
grid.appendChild(d);drawIslePreview(d.querySelector("canvas"),k);}
const dr=$("diffRow");dr.innerHTML="";
for(const k in DIFFS){const d=document.createElement("div");d.className="diff"+(k===selDiff?" sel":"");
d.innerHTML=`${k}<small>${DIFFS[k].label}</small>`;
d.onclick=()=>{selDiff=k;[...dr.children].forEach(c=>c.classList.remove("sel"));d.classList.add("sel");};dr.appendChild(d);}
$("btnStart").onclick=()=>{genWorld(selIsle,selDiff);ui.anim=true;S.anim=true;$("startOverlay").style.display="none";refreshBuildBar();save();
try{if(!localStorage.getItem("mellow_tut"))showTutorial();}catch(e){}};
if(localStorage.getItem("mellow_save")){$("btnContinue").style.display="";
$("btnContinue").onclick=()=>{if(load()){$("startOverlay").style.display="none";refreshBuildBar();}};}
}
function drawIslePreview(c,k){const g=c.getContext("2d"),is=ISLANDS[k];
g.fillStyle=is.accent;g.fillRect(0,0,c.width,c.height);
const ox=c.width/2,oy=20,tw=26,th=13;
for(let gy=0;gy<5;gy++)for(let gx=0;gx<5;gx++){const x=ox+(gx-gy)*tw/2,y=oy+(gx+gy)*th/2;
const top=((gx+gy)&1)?is.grass:is.grass2;
poly(g,[[x-tw/2,y],[x,y+th/2],[x,y+th/2+4],[x-tw/2,y+4]],dark(is.side,0.05));
poly(g,[[x+tw/2,y],[x,y+th/2],[x,y+th/2+4],[x+tw/2,y+4]],dark(is.side,0.18));
poly(g,[[x,y-th/2],[x+tw/2,y],[x,y+th/2],[x-tw/2,y]],top);}
artTree(g,ox-tw,oy+th*1.6,0.7,false,0.3);artTree(g,ox+tw*1.2,oy+th*2.2,0.6,false,0.7);
artMellow(g,ox,oy+th*2.4,is===ISLANDS.tundra?"#7d8a92":"#6ab04a","none",{s:0.85});}
function refreshBuildBar(){ if($("buildOverlay").style.display==="flex") openBuildPanel(); }
function openBuildPanel(){
const c=$("buildCard"),vl=villageLevel();
let html=`<div class="bldTitle">Buildings <span class="vlvl">Lv ${vl} · ${villageTitle()}</span></div>`;
for(const cat of BUILD_CATS){const list=BUILD_ORDER.filter(t=>BUILD[t].cat===cat);if(!list.length)continue;
html+=`<div class="catHead">${cat}</div>`;
for(const type of list){const info=BUILD[type];
const cost=scaledCost(type),ok=affordable(type);
const costStr=Object.entries(cost).map(([k,v])=>`<b>${img("res",k,"sm")}${v}</b>`).join("");
html+=`<div class="bcard"><div class="prev">${img("bld",type,"lg")}</div>
<div class="binfo"><b>${info.name}</b><div class="bdesc">${info.desc}</div>
<div class="bcost">Cost ${costStr}</div></div>
<button class="buildbtn ${ok?'':'no'}" data-type="${type}">Build</button></div>`;}}
c.innerHTML=html;
c.querySelectorAll(".buildbtn").forEach(b=>b.onclick=()=>{const t=b.dataset.type;
if(!affordable(t)){toast("Not enough resources");return;}
$("buildOverlay").style.display="none";startPlacing(t);});
$("buildOverlay").style.display="flex";
}
function startPlacing(type){if(!affordable(type)){toast("Not enough resources");return;}
ui.placing=type;$("placeBanner").style.display="flex";$("placeTxt").textContent="Tap a tile to place "+BUILD[type].name;}
function endPlacing(){ui.placing=null;$("placeBanner").style.display="none";}
function melPortrait(m){const c=document.createElement("canvas");c.width=74;c.height=82;const g=c.getContext("2d");
const j=JOBS[m.job];artMellow(g,37,70,j?j.cloak:"#9fb0a0",isChild(m)?"none":(j?j.tool:"none"),{s:isChild(m)?1.4:1.8,face:1});return c.toDataURL();}
function mellowNeeds(m){const out=[];
if(!homeValid(m)&&!isChild(m))out.push(["home","Needs a home"]);
if(isWinter()&&m.warmth<40)out.push(["cold","Is cold"]);
if(m.hunger<30)out.push(["food","Needs food"]);
if(m.sick)out.push(["sick","Feeling unwell"]);
if(!isChild(m)&&m.job==="idle")out.push(["idle","Looking for a job"]);
if(m.pregnant)out.push(["preg","Expecting a baby"]);
return out;}
function villageNeeds(){const ad=S.mellows.filter(m=>!isChild(m)),out=[];
const homeless=ad.filter(m=>!homeValid(m)).length;if(homeless)out.push(["home",homeless,"need a home"]);
const idle=ad.filter(m=>m.job==="idle").length;if(idle)out.push(["idle",idle,"are looking for something to do"]);
const preg=S.mellows.filter(m=>m.pregnant).length;if(preg)out.push(["preg",preg,"are expecting a baby"]);
const cold=isWinter()?S.mellows.filter(m=>m.warmth<40).length:0;if(cold)out.push(["cold",cold,"are hurt by the cold"]);
const hungry=S.mellows.filter(m=>m.hunger<30).length;if(hungry)out.push(["food",hungry,"are hungry"]);
const sick=S.mellows.filter(m=>m.sick).length;if(sick)out.push(["sick",sick,"are unwell"]);
if(S.res.tools<=0){const nt=ad.filter(m=>["axe","pick","bow","saw","scythe"].includes(JOBS[m.job].tool)).length;if(nt)out.push(["tool",nt,"need new tools"]);}
return out;}
function drawPyramid(c2){const g=c2.getContext("2d"),W=c2.width,H=c2.height;g.clearRect(0,0,W,H);
const buckets=[];for(let a=0;a<65;a+=5)buckets.push([a,a+4]);
const mC=buckets.map(()=>0),fC=buckets.map(()=>0);
for(const m of S.mellows){let bi=Math.min(buckets.length-1,Math.floor(m.age/5));if(bi<0)bi=0;if(m.sex==="male")mC[bi]++;else fC[bi]++;}
const maxC=Math.max(1,...mC,...fC),cx=W/2,labelW=46,barMax=cx-labelW/2-10,rowH=(H-24)/buckets.length;
g.font="9px sans-serif";g.textBaseline="middle";g.fillStyle="#4a3316";
g.textAlign="left";g.fillText("Male",6,10);g.textAlign="right";g.fillText("Female",W-6,10);
for(let i=0;i<buckets.length;i++){const y=H-12-(i+0.5)*rowH,bh=rowH*0.66;
const mL=mC[i]/maxC*barMax,fL=fC[i]/maxC*barMax;
g.fillStyle="#5f86bf";g.fillRect(cx-labelW/2-mL,y-bh/2,mL,bh);
g.fillStyle="#cf7d7d";g.fillRect(cx+labelW/2,y-bh/2,fL,bh);
g.strokeStyle="#c69a5a";g.lineWidth=1;g.strokeRect(cx-labelW/2,y-rowH*0.42,labelW,rowH*0.84);
g.fillStyle="#4a3316";g.textAlign="center";g.fillText(buckets[i][0]+"-"+buckets[i][1],cx,y);}}
function openProfile(m){ui.selMel=m;ui.selBld=null;const c=$("profCard"),j=JOBS[m.job];
const homeB=S.buildings.find(b=>b.id===m.home),child=isChild(m);
const bars=[["Health",m.health,"#cf5a5a"],["Happiness",m.happy,"#6ab04a"],["Hunger",m.hunger,"#e0913f"]];
if(isWinter())bars.push(["Warmth",m.warmth,"#4bb4d6"]);
bars.push(["Intelligence "+Math.round(m.intel||3)+"/10",(m.intel||3)*10,"#7a8ec9"]);
const barsHtml=bars.map(([n,v,cc])=>`<div class="mini" style="margin-bottom:1px">${n}</div><div class="statbar"><i style="width:${Math.max(2,v)}%;background:${cc}"></i></div>`).join("");
const jobs=Object.keys(JOBS).map(jk=>`<div class="jobopt ${jk===m.job?'sel':''}" data-job="${jk}">${img("job",jk)}${JOBS[jk].name}</div>`).join("");
const needs=mellowNeeds(m);
const needsHtml=needs.length?needs.map(([k,t])=>`<div class="noticeRow"><img class="ic nb" src="${icon('need',k,40)}"><span>${t}</span></div>`).join(""):`<div class="noticeRow"><span>Content and cared for.</span></div>`;
let contraHtml="";
if(!child&&m.sex==="female"){
if(m.contraT>0){const sl=Math.ceil(m.contraT/difficulty().season);
contraHtml=`<div class="noticeRow">${img("res","contra","nb")}<span>On contraceptive herb — ${sl} season${sl!==1?"s":""} left</span><button class="btn sec small" id="pContraStop" style="margin-left:auto">Stop</button></div>`;
} else { const have=(S.res.contra||0);
contraHtml=`<div class="noticeRow">${img("res","contra","nb")}<span>Contraceptive herb — ${have} in storage (from the Apothecary)</span></div>
<div class="row" style="margin-top:6px"><button class="btn sec small" id="pContra"${have<=0?" disabled":""}>Give contraceptive herb (lasts 1 yr)</button></div>`;
}
}
const eld=isElder(m),tr=TRAITS[m.trait],mood=moodOf(m),hob=HOBBIES[m.hobby];
const lineage=m.parents?`Child of ${m.parents.mom} & ${m.parents.dad}${m.gparents&&m.gparents.length?` · grandchild of ${m.gparents.filter(Boolean).join(", ")}`:""}`:"A village founder";
c.innerHTML=`<h2>${m.name} ${m.surname||""} <span style="font-size:13px;color:var(--ink2);font-weight:normal">${child?"a child":eld?"an elder":(j?j.name:"Nothing")}</span></h2>
<div class="pinfo"><img class="portrait" src="${melPortrait(m)}">
<div class="pmeta"><div><b>Age</b> &nbsp;: ${Math.floor(m.age)}${eld?" (elder)":""}</div><div><b>Sex</b> &nbsp;: ${m.sex==="male"?"Male":"Female"}</div>
<div><b>Job</b> &nbsp;: ${child?"—":(j?j.name:"Unassigned")}</div><div><b>Home</b> : ${homeB?BUILD[homeB.type].name:"none"}</div>
<div><b>Intelligence</b> : ${Math.round(m.intel||3)} / 10</div>
<div><b>Trait</b> &nbsp;: ${tr?tr.name:"—"}</div>
<div><b>Hobby</b> : ${hob?hob.emoji+" "+hob.label:"—"}</div>
${child?`<div><b>School</b> : ${m.age<6?"too young (starts at 6)":!schooling()?"no schoolhouse":schoolInSession()?"in session":"off until fall"}</div>`:""}</div></div>
<div class="mini" style="text-align:center">Mood: ${mood.emoji} ${mood.label}${hob&&hob.job&&hob.job===m.job?` · doing what they love ${hob.emoji}`:""}</div>
${tr?`<div class="mini" style="text-align:center">${tr.desc}${eld?" · An elder — assign as Teacher to mentor children faster.":""}</div>`:""}
<div class="mini" style="text-align:center">${lineage}</div>
${child?`<div class="mini" style="text-align:center">Lives with ${S.mellows.filter(x=>x.home===m.home&&!isChild(x)).map(a=>a.name).join(" & ")||"their family"}</div>`
:`<div class="mini" style="text-align:center">Partner: <b>${partnerOf(m)?partnerOf(m).name:"—"}</b>${childrenOf(m).length?" &nbsp;·&nbsp; Children: "+childrenOf(m).map(k=>k.name).join(", "):""}</div>
<div class="mini" style="text-align:center">Friends: <b>${friendNames(m)||"—"}</b></div>`}
<h3 style="text-align:center">Status</h3>${barsHtml}
${child?'<div class="mini" style="text-align:center">Still growing — will take on a job at age 12.</div>':`<h3>Assign job</h3><div class="jobgrid">${jobs}</div>`}
<h3 style="text-align:center">Notice</h3>${needsHtml}
${contraHtml}
<div class="row" style="margin-top:12px"><button class="btn sec small" id="pRename">Rename</button><button class="btn sec small" id="pReset">Send home</button>
${child||m.job==="idle"?"":'<button class="btn sec small" id="pIdle">Take off job</button>'}</div>`;
$("pRename").onclick=()=>{const nm=(prompt("Rename this Mellow:",m.name||"")||"").replace(/[<>]/g,"").trim();if(nm){m.name=nm.slice(0,18);openProfile(m);}};
c.querySelectorAll(".jobopt").forEach(el=>el.onclick=()=>{m.job=el.dataset.job;m.state="idle";m.target=null;m.timer=0;openProfile(m);refreshBuildBar();});
$("pReset").onclick=()=>{const h=S.buildings.find(b=>b.id===m.home)||S.buildings[0];m.gx=h.gx;m.gy=h.gy;m.tx=h.gx;m.ty=h.gy;m.state="idle";m.target=null;toast(m.name+" sent home");};
const idl=$("pIdle");if(idl)idl.onclick=()=>{m.job="idle";m.state="idle";m.target=null;toast(m.name+" is now free");openProfile(m);};
const pc=$("pContra");if(pc)pc.onclick=()=>{if((S.res.contra||0)<=0){toast("No contraceptive herbs — staff an Apothecary");return;}S.res.contra--;m.contraT=4*difficulty().season;m.pregnant=false;m.pregT=0;toast(m.name+" took a contraceptive herb");openProfile(m);};
const pcs=$("pContraStop");if(pcs)pcs.onclick=()=>{m.contraT=0;toast(m.name+" stopped the contraceptive");openProfile(m);};
$("profOverlay").style.display="flex";}
function closeProfile(){$("profOverlay").style.display="none";ui.selMel=null;}
function openBuilding(b){ui.selBld=b;ui.selMel=null;const info=BUILD[b.type],c=$("bldCard");
let body=`<h2>${img("bld",b.type,'lg')} ${info.name}</h2>`;
if(!b.built){body+=`<div class="mini">Under construction — ${Math.round(b.prog*100)}%</div>
<div class="statbar"><i style="width:${Math.max(2,b.prog*100)}%;background:#6ab04a"></i></div>
<div class="hint">Assign a Mellow to <b>Builder</b> to finish it.</div>`;}
else{if(info.job){const workers=S.mellows.filter(m=>m.job===info.job&&!isChild(m));
body+=`<div class="mini">Needs a <b>${JOBS[info.job].name}</b> to run.</div>
<div class="mini">Working here: ${workers.length?workers.map(w=>w.name).join(", "):"nobody"}</div>`;
if(info.craftTools){const sel=b.makeTool||"axe";
body+=`<div class="mini">Choose what to craft — make more of whatever you're low on:</div><div class="toolPick">`;
for(const tk of TOOL_ORDER){const t=TOOLS[tk];const costS=Object.entries(t.cost).map(([k,v])=>img("res",k,"sm")+v).join(" ");
body+=`<div class="tpick ${tk===sel?'sel':''}" data-tool="${tk}">${img("res",tk,"")}<b>${t.name}</b><span class="tcost">${costS}</span><span class="tcost">have ${S.res[tk]||0}</span></div>`;}
body+=`</div>`;}
else if(info.recipe){const r=info.recipe;const inS=Object.entries(r.in).map(([k,v])=>img("res",k,"sm")+v).join(" ");
const outS=Object.entries(r.out).map(([k,v])=>img("res",k,"sm")+v).join(" ");body+=`<div class="mini">Recipe: ${inS} → ${outS}</div>`;}
if(info.wolfcentre){const list=facilityWolves(b.id),mode=b.mode||"train";
const training=list.filter(w=>w.state==="training"),babies=list.filter(wolfBaby),males=list.filter(w=>w.sex==="male");
const ready=list.filter(wolfAdoptable);
body+=`<div class="mini" style="text-align:center">Housed <b>${list.length}</b>/${FACILITY_CAP} &nbsp;·&nbsp; Training <b>${training.length}</b> &nbsp;·&nbsp; Ready <b>${ready.length}</b></div>`;
body+=`<div class="mini" style="text-align:center">♂ ${males.length} &nbsp;·&nbsp; ♀ ${list.length-males.length} &nbsp;·&nbsp; ${babies.length} pup${babies.length===1?"":"s"} &nbsp;·&nbsp; ${list.length-babies.length} adult${list.length-babies.length===1?"":"s"}</div>`;
body+=`<div class="toolPick"><div class="tpick ${mode==="train"?'sel':''}" data-mode="train"><b>Train + House</b><span class="tcost">tame & raise</span></div>
<div class="tpick ${mode==="house"?'sel':''}" data-mode="house"><b>House only</b><span class="tcost">no new taming</span></div></div>`;
body+=`<button class="btn small ${ready.length?'':'no'}" id="adoptWolf" style="margin-top:6px">Adopt a wolf → a family</button>`;}
body+=`<button class="btn small" id="assignJob" style="margin-top:8px">Assign an idle Mellow here</button>`;}
else if(info.house){body+=`<div class="mini">Shelters ${info.house} Mellows${info.warm?" · extra warm in winter":""}.</div>`;
const occ=S.mellows.filter(m=>m.home===b.id),ad=occ.filter(m=>!isChild(m)),kd=occ.filter(m=>isChild(m));
if(occ.length){body+=`<h3 style="text-align:center">Who lives here</h3>`;
body+=ad.map(m=>`<div class="noticeRow resRow" data-id="${m.id}" style="cursor:pointer"><img class="ic nb" src="${melPortrait(m)}"><span><b>${m.name}</b> — ${JOBS[m.job]?JOBS[m.job].name:"Unassigned"}${m.pregnant?" · expecting":""}${m.contraT>0?" · on contraceptive":""}</span></div>`).join("");
body+=kd.map(m=>`<div class="noticeRow resRow" data-id="${m.id}" style="cursor:pointer"><img class="ic nb" src="${melPortrait(m)}"><span><b>${m.name}</b> — child, age ${Math.floor(m.age)}</span></div>`).join("");
} else body+=`<div class="mini" style="text-align:center">Empty — nobody has moved in yet.</div>`;}
else if(info.warmth){body+=`<div class="mini">Keeps Mellows warm in winter (burns firewood).</div>`;}
else if(info.store){body+=`<div class="mini" style="text-align:center">What the village has stored${totalFood()?` · food ${totalFood()}`:""}</div><div class="resgrid">`;
for(const r of RES_ORDER){const v=S.res[r]||0;const nm=RES_NAME[r]||(r[0].toUpperCase()+r.slice(1));
body+=`<div class="ritem">${img("res",r,"")}<span>${nm}</span><b>${v}</b></div>`;}
body+=seedRow()+`</div>`;}
else if(info.farm){const cur=b.crop||"crops",stage=b.stage||"empty",isWin=season()==="Winter",canChoose=isWin&&stage!=="growing";S.seeds=S.seeds||startSeeds();
const statusTxt=stage==="growing"
?`🌱 Growing <b>${CROPS[b.growing||cur].name}</b> — ripens over Summer, harvested in Fall`
:isWin?`❄ Winter — choose the crop to sow next Spring`
:`Will sow <b>${CROPS[cur].name}</b> in Spring (${PLANT_COST} seeds → ${PLANT_YIELD})`;
body+=`<div class="mini" style="text-align:center">${statusTxt}</div>`;
body+=`<div class="mini">${canChoose?`Pick what to plant — ${PLANT_COST} seeds of one kind yields ${PLANT_YIELD}:`:`Crop: <b>${CROPS[cur].name}</b> — the crop can only be changed in <b>Winter</b>, before spring planting.`}</div><div class="toolPick">`;
for(const c of CROP_ORDER){const have=S.seeds[c]||0,enough=have>=PLANT_COST;
body+=`<div class="tpick ${c===cur?'sel':''} ${canChoose&&enough?'':'no'}" ${canChoose?`data-crop="${c}"`:''}>${img("res",c,"")}<b>${CROPS[c].name}</b><span class="tcost">${have} seeds</span></div>`;}
body+=`</div><div class="mini" style="text-align:center">Sown in <b>Spring</b>, grows through <b>Summer</b>, harvested in <b>Fall</b>. Needs a <b>Farmer</b>.</div>`;}
else if(info.pasture){const animal=b.animal||"sheep",herd=b.herd||2;
body+=`<div class="mini" style="text-align:center"><b>${animal==="sheep"?"Sheep":"Deer"} Pasture</b> — herd <b>${herd}</b>/${HERD_CAP}</div>`;
body+=`<div class="mini">Choose what to raise (they don't mix):</div><div class="toolPick">`;
for(const a of ["sheep","deer"]){body+=`<div class="tpick ${a===animal?'sel':''}" data-animal="${a}">${img("res",a==="sheep"?"wool":"meat","")}<b>${a==="sheep"?"Sheep":"Deer"}</b><span class="tcost">${a==="sheep"?"wool + meat":"meat + leather"}</span></div>`;}
body+=`</div><div class="mini" style="text-align:center">Assign a <b>Shepherd</b>; the herd grows each season.</div>`;}
else if(info.cemetery){const mem=S.memorial||[];
body+=`<div class="mini">A quiet resting place. ${mem.length?mem.length+" laid to rest.":"No one has passed yet."}</div>`;
if(mem.length){body+=`<h3 style="text-align:center">In Memoriam</h3>`;
body+=mem.slice(0,40).map(e=>`<div class="noticeRow"><img class="ic nb" src="${icon('bld','cemetery',40)}"><span><b>${e.name} ${e.surname||""}</b> — ${e.why}, age ${e.age}</span></div>`).join("");}}
else if(info.decor){body+=`<div class="mini" style="text-align:center">${info.desc}</div>
<div class="mini" style="text-align:center">✨ Decorations make ${S.villageName||"the village"} lovelier — a little happiness for everyone.</div>`;}}
if(b.built&&(info.recipe||info.craftTools||info.farm||info.pasture)){
if(b.upg)body+=`<div class="mini" style="text-align:center;color:#c98a1a">⭐ Upgraded — producing +40%.</div>`;
else{const uc=upgradeCost(b.type),ucS=Object.entries(uc).map(([k,v])=>img("res",k,"sm")+v).join(" ");
body+=`<button class="btn small" id="upgradeBld" style="margin-top:8px">⭐ Upgrade &nbsp;·&nbsp; +40% output &nbsp; ${ucS}</button>`;}}
body+=`<button class="btn sec small" id="demolish" style="margin-top:10px">Demolish</button>`;c.innerHTML=body;
c.querySelectorAll(".resRow").forEach(el=>el.onclick=()=>{const mm=S.mellows.find(x=>x.id==el.dataset.id);if(mm){closeBuilding();openProfile(mm);}});
c.querySelectorAll(".tpick[data-tool]").forEach(el=>el.onclick=()=>{b.makeTool=el.dataset.tool;openBuilding(b);});
c.querySelectorAll(".tpick[data-crop]").forEach(el=>el.onclick=()=>{b.crop=el.dataset.crop;openBuilding(b);});
c.querySelectorAll(".tpick[data-animal]").forEach(el=>el.onclick=()=>{b.animal=el.dataset.animal;openBuilding(b);});
c.querySelectorAll(".tpick[data-mode]").forEach(el=>el.onclick=()=>{b.mode=el.dataset.mode;openBuilding(b);});
{const ab=$("adoptWolf");if(ab)ab.onclick=()=>{adoptFromCentre(b);openBuilding(b);};}
{const ub=$("upgradeBld");if(ub)ub.onclick=()=>{if(tryUpgrade(b)){openBuilding(b);refreshBuildBar();}};}
const aj=$("assignJob");if(aj)aj.onclick=()=>{const idle=S.mellows.find(m=>m.job==="idle"&&!isChild(m))||S.mellows.find(m=>JOBS[m.job].kind==="gather"&&!isChild(m));
if(idle){idle.job=info.job;idle.state="idle";idle.target=null;toast(idle.name+" now works the "+info.name);openBuilding(b);refreshBuildBar();}else toast("No spare Mellow — free one up first");};
let confirmDemo=false;
$("demolish").onclick=()=>{if(S.buildings.length<=1){toast("Keep at least one building");return;}
if(!confirmDemo){confirmDemo=true;$("demolish").textContent="Really demolish? (salvages ~half)";
setTimeout(()=>{confirmDemo=false;const d=$("demolish");if(d)d.textContent="Demolish";},3000);return;}
const cost=scaledCost(b.type);for(const k in cost)S.res[k]=(S.res[k]||0)+Math.floor(cost[k]*0.5);
S.buildings=S.buildings.filter(x=>x!==b);computeReach();toast("Demolished "+info.name+" — materials salvaged");closeBuilding();refreshBuildBar();};
$("bldOverlay").style.display="flex";}
function closeBuilding(){$("bldOverlay").style.display="none";ui.selBld=null;}
const TUTORIAL=[
{t:"Welcome!",b:"This is your village. Guide your Mellows to gather food, build homes and grow a thriving town — through every season."},
{t:"Give them jobs",b:"Tap a Mellow, then choose a job. Gatherers collect food &amp; materials and drop them at Storage. Start with food — a Fruit Picker or Hunter."},
{t:"Build things",b:"Tap the <b>Build</b> button, pick a building, then tap a tile. Keep at least one Mellow on <b>Builder</b> so construction finishes."},
{t:"Keep them fed",b:"Hunger falls over time — always keep food in stock. A <b>Kitchen</b> turns raw food into filling meals."},
{t:"Survive winter",b:"In winter, plants stop growing and it gets cold. Stock <b>firewood</b> (from a Sawmill) and build a <b>Campfire</b>, or Mellows lose warmth."},
{t:"Grow your village",b:"Couples in houses have babies. Every building is available from the start — but the fancier ones need better materials like <b>planks</b> (from a Sawmill) and <b>iron</b> (mined from ore). Have fun!"}];
function showTutorial(){let i=0;const ov=document.createElement("div");ov.className="overlay";ov.id="tutOverlay";ov.style.display="flex";
function close(){try{localStorage.setItem("mellow_tut","1");}catch(e){}ov.remove();}
function render(){const s=TUTORIAL[i];
ov.innerHTML=`<div class="card parch" style="max-width:340px;text-align:center">
<div style="font-size:12px;color:var(--ink2)">Step ${i+1} of ${TUTORIAL.length}</div>
<h2>${s.t}</h2><div class="hint"><p>${s.b}</p></div>
<div class="row" style="justify-content:space-between;align-items:center;margin-top:10px">
<button class="btn sec small" id="tutSkip">Skip</button>
<div class="row" style="gap:6px">${i>0?'<button class="btn sec small" id="tutBack">Back</button>':''}<button class="btn" id="tutNext">${i<TUTORIAL.length-1?"Next":"Start playing"}</button></div>
</div></div>`;
$("tutNext").onclick=()=>{if(i<TUTORIAL.length-1){i++;render();}else close();};
const bk=$("tutBack");if(bk)bk.onclick=()=>{i--;render();};
$("tutSkip").onclick=close;}
document.body.appendChild(ov);render();}
function openJobs(){const c=$("jobsCard");
c.innerHTML=`<div class="vhead"><div><b id="vnameTxt">${S.villageName||"Our Village"}</b><div class="vsub">a ${villageTitle()} · Level ${villageLevel()} · ${S.mellows.length} Mellows</div></div><button class="btn sec small" id="vnameEdit">Rename</button></div>
<div class="vtabs"><div class="vtab on" id="tabNotice">Notice</div><div class="vtab" id="tabGoals">Goals</div><div class="vtab" id="tabPop">Population</div><div class="vtab" id="tabMile">Milestones</div></div>
<div id="vNotice"></div><div id="vGoals" style="display:none"></div><div id="vPop" style="display:none"></div><div id="vMile" style="display:none"></div>`;
$("vnameEdit").onclick=()=>{const nm=(prompt("Name your village:",S.villageName||"")||"").replace(/[<>]/g,"").trim();if(nm){S.villageName=nm.slice(0,24);openJobs();}};
function renderMile(){S.achieved=S.achieved||[];
$("vMile").innerHTML=MILESTONES.map(ms=>{const got=S.achieved.includes(ms.id);
return `<div class="noticeRow" style="opacity:${got?1:0.5}"><span style="font-size:16px">${got?"🏆":"○"}</span><span><b>${ms.name}</b><br><small style="color:var(--ink2)">${ms.desc}</small></span></div>`;}).join("");}
function renderGoals(){S.quests=S.quests||[];let nextShown=false;
const done=S.quests.length,tot=QUESTS.length;
const rows=QUESTS.map(q=>{const got=S.quests.includes(q.id);const isNext=!got&&!nextShown;if(isNext)nextShown=true;
return `<div class="noticeRow" style="opacity:${got?1:isNext?1:0.55};${isNext?'background:#fff3d6;border-radius:8px':''}">
<span style="font-size:16px">${got?"✅":isNext?"➡️":"○"}</span>
<span><b>${q.name}</b>${got?"":`<br><small style="color:var(--ink2)">${q.hint}</small>`}</span></div>`;}).join("");
$("vGoals").innerHTML=`<div class="mini" style="text-align:center;margin-bottom:6px">${done} of ${tot} goals complete · each gives +8 happiness</div>${rows}`;}
function renderNotice(){const nd=villageNeeds();
$("vNotice").innerHTML=nd.length?nd.map(([k,n,t])=>`<div class="noticeRow"><img class="ic nb" src="${icon('need',k,40)}"><span><b class="num">${n}</b> ${n===1?'Mellow':'Mellows'} ${t}</span></div>`).join("")
:`<div class="noticeRow"><span>All is well in the village.</span></div>`;}
function renderPop(){const males=S.mellows.filter(m=>m.sex==="male").length,females=S.mellows.length-males,kids=S.mellows.filter(isChild).length,preg=S.mellows.filter(m=>m.pregnant).length;
$("vPop").innerHTML=`<div class="pmeta" style="text-align:center;margin-bottom:8px;font-size:13px">${S.mellows.length} Mellows &nbsp;·&nbsp; ${males} male &nbsp;·&nbsp; ${females} female<br>${kids} ${kids===1?'child':'children'} &nbsp;·&nbsp; ${preg} expecting</div>
<canvas id="pyr" width="360" height="300" style="width:100%;background:#fff8ea;border:2px solid #c69a5a;border-radius:10px"></canvas>`;
drawPyramid($("pyr"));}
const tabs={tabNotice:"vNotice",tabGoals:"vGoals",tabPop:"vPop",tabMile:"vMile"};
const show=(on,fn)=>{for(const t in tabs){$(t).classList.toggle("on",t===on);$(tabs[t]).style.display=t===on?"":"none";}fn();};
$("tabNotice").onclick=()=>show("tabNotice",renderNotice);
$("tabGoals").onclick=()=>show("tabGoals",renderGoals);
$("tabPop").onclick=()=>show("tabPop",renderPop);
$("tabMile").onclick=()=>show("tabMile",renderMile);
renderNotice();$("jobsOverlay").style.display="flex";}
function ordinal(n){const s=["th","st","nd","rd"],v=n%100;return n+(s[(v-20)%10]||s[v]||s[0]);}
function openResources(){const c=$("resCard");
const males=S.mellows.filter(m=>m.sex==="male").length,females=S.mellows.length-males;
let html=`<h2>Storage</h2><div class="mini">${S.mellows.length} Mellows · ${males} male · ${females} female · food ${totalFood()}</div><div class="resgrid">`;
for(const r of RES_ORDER){const v=S.res[r]||0;const nm=RES_NAME[r]||(r[0].toUpperCase()+r.slice(1));
html+=`<div class="ritem">${img("res",r,"")}<span>${nm}</span><b>${v}</b></div>`;}
c.innerHTML=html+seedRow()+`</div>`;$("resOverlay").style.display="flex";}
// one aggregate Seeds row for the Items/Storage views (per-type breakdown lives in the Farm panel)
function seedRow(){const t=S.seeds?Object.values(S.seeds).reduce((a,b)=>a+b,0):0;
return `<div class="ritem">${img("res","seed","")}<span>Seeds</span><b>${t}</b></div>`;}
let _hudT=0,_resIcoSet=false;
function updateHUD(dt){_hudT-=dt||0;if(_hudT>0)return;_hudT=0.25;
$("seasonIco").src=icon("season",season(),32);$("seasonName").textContent=season();
$("dayNum").textContent=ordinal(S.seasonCount)+" Season";
$("tempTxt").textContent=(SEASON_TEMP[season()]+(ISLANDS[S.island].coldland?-6:0))+"°";
if(!_resIcoSet){$("resIco").src=icon("res","wood",40);_resIcoSet=true;}
if($("resOverlay").style.display==="flex")openResources();}
/* ================= SOUND (synth, no assets) ================= */
const Sound=(function(){let ctx=null,master=null,amb=null,on=false;
function ensure(){if(ctx)return;const AC=window.AudioContext||window.webkitAudioContext;if(!AC)return;
ctx=new AC();master=ctx.createGain();master.gain.value=0.5;master.connect(ctx.destination);}
function blip(freq,dur,type,vol,slideTo){if(!ctx)return;try{const t0=ctx.currentTime,o=ctx.createOscillator(),g=ctx.createGain();
o.type=type||"sine";o.frequency.setValueAtTime(freq,t0);if(slideTo)o.frequency.exponentialRampToValueAtTime(slideTo,t0+dur);
g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(vol||0.2,t0+0.02);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);o.connect(g);g.connect(master);o.start(t0);o.stop(t0+dur+0.03);}catch(e){}}
const SFX={
build:()=>{blip(180,0.13,"triangle",0.24);setTimeout(()=>blip(135,0.15,"triangle",0.2),95);},
birth:()=>{blip(523,0.12,"sine",0.22);setTimeout(()=>blip(784,0.18,"sine",0.2),110);},
festival:()=>[523,659,784,1047].forEach((f,i)=>setTimeout(()=>blip(f,0.2,"triangle",0.18),i*95)),
milestone:()=>[659,988].forEach((f,i)=>setTimeout(()=>blip(f,0.22,"sine",0.2),i*130)),
arrive:()=>blip(420,0.12,"sine",0.18,640),
death:()=>blip(300,0.5,"sine",0.16,150),
birds:()=>[880,1175,988,1318].forEach((f,i)=>setTimeout(()=>blip(f,0.08,"sine",0.05),i*95)), // a little daytime birdsong
cricket:()=>{for(let i=0;i<3;i++)setTimeout(()=>blip(2550,0.016,"square",0.024),i*75);}}; // a nighttime cricket chirp
function startAmb(){if(!ctx||amb)return;try{const o=ctx.createOscillator(),o2=ctx.createOscillator(),g=ctx.createGain();
o.type="sine";o2.type="sine";o.frequency.value=98;o2.frequency.value=147;g.gain.value=0.0;
o.connect(g);o2.connect(g);g.connect(master);o.start();o2.start();
g.gain.linearRampToValueAtTime(0.03,ctx.currentTime+3);amb={o,o2,g};}catch(e){}}
function stopAmb(){if(!amb)return;const a=amb;amb=null;try{a.g.gain.linearRampToValueAtTime(0,ctx.currentTime+0.5);
setTimeout(()=>{try{a.o.stop();a.o2.stop();}catch(e){}},600);}catch(e){}}
return {set(v){on=v;if(on){ensure();if(ctx&&ctx.state==="suspended")ctx.resume();startAmb();}else stopAmb();},
sfx(n){if(!on)return;ensure();if(ctx&&ctx.state==="suspended")ctx.resume();if(SFX[n])SFX[n]();}};
})();
/* ================= SAVE ================= */
function save(){if(!S)return;try{S.anim=ui.anim;S.sound=ui.sound;localStorage.setItem("mellow_save",JSON.stringify(S));}catch(e){}}
function load(){try{const raw=localStorage.getItem("mellow_save");if(!raw)return false;S=JSON.parse(raw);
if(S.seasonCount==null)S.seasonCount=1;if(S.res.contra==null)S.res.contra=0;
if(S.graves==null)S.graves=[];if(S.memorial==null)S.memorial=[];if(S.festT==null)S.festT=0;if(S.festKind==null)S.festKind="confetti";if(S.weather==null)S.weather="clear";if(S.vlevel==null)S.vlevel=villageLevel();if(S.villageName==null)S.villageName=randomVillageName();S._weddingsInit=false;
if(S.achieved==null)S.achieved=[];if(S.quests==null)S.quests=[];
if(S.newcomers==null)S.newcomers=[];if(S._nextNewcomer==null)S._nextNewcomer=S.seasonCount+2;
for(const m of S.newcomers){m.newcomer=true;m._pg=undefined;m._path=null;}
overShown=false;cam.z=1;
if(S.seeds==null)S.seeds=startSeeds();for(const c of CROP_ORDER){if(S.seeds[c]==null)S.seeds[c]=0;if(S.res[c]==null)S.res[c]=0;}
if(S.treeRegrow==null)S.treeRegrow=0;
for(const b of S.buildings)if(BUILD[b.type]&&BUILD[b.type].dock&&b.dir==null)b.dir=0;
S.dens=S.dens||[];S.wolves=S.wolves||[]; // wolf model migration (old saves had {tamed})
for(const w of S.wolves){if(w.sex==null)w.sex=Math.random()<0.5?"male":"female";
if(w.state==null)w.state="wild";if(w.bornAt==null)w.bornAt=(S.tsec||0)-difficulty().season*2;
if(w.packId==null)w.packId=0;if(w.homeId===undefined)w.homeId=null;if(w.facId===undefined)w.facId=null;
if(w.trainT==null)w.trainT=0;if(w.atkCd==null)w.atkCd=0;if(w.bornTame==null)w.bornTame=false;if(w.face==null)w.face=1;delete w.tamed;}
if(S.dens.length<1&&S.wolves.length)S.dens.push({gx:S.wolves[0].gx|0,gy:S.wolves[0].gy|0});
while(S.dens.length<2)S.dens.push({gx:(GRID*0.7)|0,gy:(GRID*0.3)|0});
for(const m of S.mellows){m.target=null;
if(m.intel==null)m.intel=m.edu!=null?Math.max(1,Math.min(10,Math.round(1+m.edu*9))):(m.age<12?2:(3+(Math.random()*3|0)));
if(m.contraT==null)m.contraT=0;
if(m.friends==null)m.friends=[];
if(m.surname==null)m.surname=SURNAMES[(Math.random()*SURNAMES.length)|0];
if(m.trait==null)m.trait=pickTrait();
if(m.hobby==null)m.hobby=pickHobby();
if(m.parents===undefined)m.parents=null;if(m.gparents==null)m.gparents=[];
if(m.momId===undefined)m.momId=null;if(m.dadId===undefined)m.dadId=null;}
ui.anim=S.anim!==false;ui.sound=!!S.sound;$("swAnim").classList.toggle("on",ui.anim);$("swSound").classList.toggle("on",ui.sound);
if(ui.sound)Sound.set(true);
computeReach();centerCamera(GRID/2,GRID/2);refreshBuildBar();if((S.newcomers||[]).length)showNewcomerPopup();return true;}catch(e){return false;}}
/* ================= BOOT ================= */
function loop(t){requestAnimationFrame(loop);if(!lastT)lastT=t;let dt=(t-lastT)/1000;lastT=t;if(dt>0.1)dt=0.1;
if(S&&$("startOverlay").style.display==="none"){step(dt*(ui.speed==null?1:ui.speed));render();updateHUD(dt);
S._saveT=(S._saveT||0)+dt;if(S._saveT>5){S._saveT=0;save();}}}
function boot(){cv=$("cv");ctx=cv.getContext("2d");resize();
window.addEventListener("resize",()=>{resize();if(S)centerCamera(GRID/2,GRID/2);});
setupInput();buildStartScreen();
$("buildIco").src=icon("bld","craft",40);
$("btnBuild").onclick=()=>{if(S)openBuildPanel();};
$("buildClose").onclick=()=>$("buildOverlay").style.display="none";
$("btnRes").onclick=()=>{if(S)openResources();};
$("resClose").onclick=()=>$("resOverlay").style.display="none";
$("btnJobs").onclick=()=>{if(S)openJobs();};
$("btnHelp").onclick=()=>$("helpOverlay").style.display="flex";
$("btnSettings").onclick=()=>$("setOverlay").style.display="flex";
{const SPD=[1,2,3,0],SPDL=["1×","2×","3×","❚❚"];$("speedBtn").onclick=()=>{let i=(SPD.indexOf(ui.speed)+1)%SPD.length;ui.speed=SPD[i];$("speedBtn").textContent=SPDL[i];};}
$("placeCancel").onclick=endPlacing;$("profClose").onclick=closeProfile;$("bldClose").onclick=closeBuilding;
$("jobsClose").onclick=()=>$("jobsOverlay").style.display="none";$("setClose").onclick=()=>$("setOverlay").style.display="none";
$("helpClose").onclick=()=>$("helpOverlay").style.display="none";$("helpOk").onclick=()=>$("helpOverlay").style.display="none";
$("helpTut").onclick=()=>{$("helpOverlay").style.display="none";showTutorial();};
["buildOverlay","resOverlay","profOverlay","bldOverlay","jobsOverlay","setOverlay","helpOverlay"].forEach(id=>{
const o=$(id);o.addEventListener("click",e=>{if(e.target===o){o.style.display="none";
if(id==="profOverlay")ui.selMel=null;if(id==="bldOverlay")ui.selBld=null;}});});
$("swAnim").onclick=()=>{ui.anim=!ui.anim;$("swAnim").classList.toggle("on",ui.anim);};
$("swSound").onclick=()=>{ui.sound=!ui.sound;$("swSound").classList.toggle("on",ui.sound);Sound.set(ui.sound);};
$("btnSave").onclick=()=>{save();toast("Saved");};
$("btnQuit").onclick=()=>{save();$("setOverlay").style.display="none";$("startOverlay").style.display="flex";buildStartScreen();};
$("overRestart").onclick=()=>{$("overOverlay").style.display="none";overShown=false;ui.speed=1;$("speedBtn").textContent="1×";genWorld(S.island,S.diff);refreshBuildBar();save();};
$("overMenu").onclick=()=>{$("overOverlay").style.display="none";overShown=false;ui.speed=1;$("speedBtn").textContent="1×";localStorage.removeItem("mellow_save");$("startOverlay").style.display="flex";buildStartScreen();};
requestAnimationFrame(loop);}
boot();
</script>
<script>
/* register the service worker for offline / installable app */
if('serviceWorker' in navigator){window.addEventListener('load',function(){navigator.serviceWorker.register('sw.js').catch(function(){});});}
</script>
</body>
</html>