diff --git a/README.md b/README.md index 209f736..f9f7224 100644 --- a/README.md +++ b/README.md @@ -281,3 +281,20 @@ Use this to compare minified JSON, gzip, zlib, and Brotli when available. Produc ## v16b hotfix - Fixed the dark/blank world regression caused by using `source-in` compositing directly on the main world canvas for ship water reflection. - Ship reflections are now precomposited on an offscreen canvas before drawing, so terrain and sprites are not erased or darkened. + + +## Phase 6 create-layout / authority prep + +- The Create tab editor now follows the tighter canvas-first layout: top metadata row, canvas with a vertical palette, large tool buttons, and a full-width Advanced settings panel. +- The palette ramps were rebuilt into cleaner 4-color rows so hue/lightness progression is easier to read. +- Local browser data uses a new storage key so older palette-corrupted art is discarded automatically. +- Shared-world prep now treats publish, object move, day/night time, and spontaneous dynamic motion as server-authoritative concerns. +- The client can keep pending shared publish/move visuals locally while waiting for server validation, and dynamic runtime motion can interpolate toward server-supplied target positions. + +## Phase 6b server-authority / Create tab correction + +- CREATE layout now groups the canvas and primary tools in the left column with the palette as the right column, matching the attached design more closely. +- Save schema was bumped to 15. Existing local art data from older palette mappings is intentionally reset and replaced with new seed artwork. +- Added server-side `world.phase` and `dynamic.move` event helpers/tests. Shared clients should treat these events as the source of truth for day/night and autonomous dynamic positions. +- Pending shared object moves now render as temporary local interpolation only; the persistent world state is updated only by server-issued events. +- Depth shading can now use a cursor-local light source on rendered sprites at night. diff --git a/app.js b/app.js index 0d5dedb..9859b1d 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,9 @@ console.info('Pixel Island Summoner loaded'); - const STORAGE_KEY = 'pixel-island-summoner:phase5e'; - const SAVE_SCHEMA = 13; + const STORAGE_KEY = 'pixel-island-summoner:phase6b'; + const LEGACY_STORAGE_KEYS = ['pixel-island-summoner:phase6a']; + const SAVE_SCHEMA = 23; const WORLD_W = 144; const WORLD_H = 112; const TILE_W = 32; @@ -21,6 +22,7 @@ const DYNAMIC_LOGIC_STEP_MS = 1000 / 15; const MAX_DYNAMIC_STEPS_PER_FRAME = 3; const MAX_SPRITE_CACHE_ENTRIES = 260; + const DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER = 5; const COLOR_CODES = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const PALETTE = buildPalette(); @@ -90,6 +92,55 @@ return state.settings; } + const DEFAULT_SERVER_AUTHORITY = Object.freeze({ + publish: 'server', + objectMove: 'server', + dayNight: 'server', + dynamicMotion: 'server' + }); + + function serverAuthority() { + ensureWorldProtectionState(); + state.serverSync.authority = { ...DEFAULT_SERVER_AUTHORITY, ...(state.serverSync.authority || {}) }; + return state.serverSync.authority; + } + + function isServerAuthoritative(feature) { + return serverAuthority()[feature] === 'server'; + } + + function getAuthoritativeNow() { + const clock = state.serverSync?.clock || null; + if (isServerAuthoritative('dayNight') && clock && Number.isFinite(clock.worldTimeMs)) { + const syncedAt = Number(clock.syncedAt || Date.now()); + return Number(clock.worldTimeMs) + Math.max(0, Date.now() - syncedAt); + } + return Date.now(); + } + + function getPendingSharedVisuals() { + ensureWorldProtectionState(); + const visuals = state.serverSync.pendingObjectVisuals || {}; + const now = Date.now(); + const out = []; + let dirty = false; + for (const [id, entry] of Object.entries(visuals)) { + if (!entry || !entry.object || !entry.assetId) { + delete visuals[id]; + dirty = true; + continue; + } + if (Number(entry.expiresAt || 0) > 0 && now > Number(entry.expiresAt)) { + delete visuals[id]; + dirty = true; + continue; + } + out.push(entry); + } + if (dirty) state.serverSync.pendingObjectVisuals = visuals; + return out; + } + function hydrateVisualSettingsUI() { const settings = visualSettings(); if (els.settingLights) els.settingLights.checked = settings.enableLights !== false; @@ -1044,7 +1095,11 @@ target.worldMode = target.worldMode === 'shared' ? 'shared' : 'local'; target.serverSync = { lastServerEventId: target.serverSync?.lastServerEventId || null, - pendingCommands: Array.isArray(target.serverSync?.pendingCommands) ? target.serverSync.pendingCommands.slice(-300) : [] + pendingCommands: Array.isArray(target.serverSync?.pendingCommands) ? target.serverSync.pendingCommands.slice(-300) : [], + authority: { ...DEFAULT_SERVER_AUTHORITY, ...(target.serverSync?.authority || {}) }, + clock: target.serverSync?.clock && typeof target.serverSync.clock === 'object' ? { ...target.serverSync.clock } : { worldTimeMs: Date.now(), syncedAt: Date.now() }, + dynamicTargets: target.serverSync?.dynamicTargets && typeof target.serverSync.dynamicTargets === 'object' ? { ...target.serverSync.dynamicTargets } : {}, + pendingObjectVisuals: target.serverSync?.pendingObjectVisuals && typeof target.serverSync.pendingObjectVisuals === 'object' ? { ...target.serverSync.pendingObjectVisuals } : {} }; target.tombstones = { assets: target.tombstones?.assets && typeof target.tombstones.assets === 'object' ? target.tombstones.assets : {}, @@ -1109,6 +1164,26 @@ ensureWorldProtectionState(); state.serverSync.pendingCommands.push(command); state.serverSync.pendingCommands = state.serverSync.pendingCommands.slice(-300); + if (command.object?.id && command.object?.assetId) { + const visualKind = command.kind === 'dynamic' ? 'dynamic' : 'static'; + const existing = visualKind === 'dynamic' + ? state.dynamicSummons.find((item) => item.id === command.object.id) + : state.placed.find((item) => item.id === command.object.id); + const toX = visualKind === 'dynamic' ? Number(command.object.homeX) : Number(command.object.x); + const toY = visualKind === 'dynamic' ? Number(command.object.homeY) : Number(command.object.y); + state.serverSync.pendingObjectVisuals[command.object.id] = { + kind: visualKind, + assetId: command.object.assetId, + object: { ...command.object }, + fromX: Number(existing ? (visualKind === 'dynamic' ? existing.homeX : existing.x) : toX), + fromY: Number(existing ? (visualKind === 'dynamic' ? existing.homeY : existing.y) : toY), + toX, + toY, + createdAt: command.createdAt || Date.now(), + expiresAt: Date.now() + 15000, + commandId: command.id + }; + } saveState(); toast('Change queued for server validation.'); } @@ -1122,7 +1197,10 @@ function getDrawableItems(time = performance.now(), viewport = null) { const rect = viewport || getViewportWorldRect(VIEW_CULL_MARGIN); const items = []; + const pendingVisuals = getPendingSharedVisuals(); + const pendingVisualIds = new Set(pendingVisuals.map((entry) => String(entry.object?.id || ''))); for (const placed of state.placed) { + if (pendingVisualIds.has(String(placed.id || ''))) continue; const asset = findAsset(placed.assetId); if (!asset || isModeratedAssetHidden(asset) || state.hiddenAssets?.[asset.id] || state.hiddenObjects?.[placed.id] || isPermanentlyHiddenObject(placed) || !isWorldObjectVisibleByRotation('static', placed.id)) continue; if (shouldHideUnderPlacementPreview(asset.id, placed.x + .5, placed.y + .5)) continue; @@ -1132,6 +1210,7 @@ items.push({ kind: 'static', asset, x: itemX, y: itemY, source: placed }); } for (const runtime of dynamicRuntime) { + if (pendingVisualIds.has(String(runtime.id || ''))) continue; const asset = findAsset(runtime.assetId); if (!asset || isModeratedAssetHidden(asset) || state.hiddenAssets?.[asset.id] || state.hiddenObjects?.[runtime.id] || isPermanentlyHiddenObject(runtime) || !isWorldObjectVisibleByRotation('dynamic', runtime.id)) continue; if (shouldHideUnderPlacementPreview(asset.id, runtime.x, runtime.y)) continue; @@ -1139,6 +1218,22 @@ if (!isApproxVisible(asset, runtime.x, runtime.y, rect)) continue; items.push({ kind: 'dynamic', asset, x: runtime.x, y: runtime.y, source: runtime }); } + for (const pending of pendingVisuals) { + const asset = findAsset(pending.assetId); + if (!asset || isModeratedAssetHidden(asset) || state.hiddenAssets?.[asset.id]) continue; + const source = pending.object || {}; + const targetX = Number.isFinite(Number(pending.toX)) ? Number(pending.toX) : (pending.kind === 'dynamic' ? Number(source.homeX) : Number(source.x)); + const targetY = Number.isFinite(Number(pending.toY)) ? Number(pending.toY) : (pending.kind === 'dynamic' ? Number(source.homeY) : Number(source.y)); + const startX = Number.isFinite(Number(pending.fromX)) ? Number(pending.fromX) : targetX; + const startY = Number.isFinite(Number(pending.fromY)) ? Number(pending.fromY) : targetY; + const t = clamp((Date.now() - Number(pending.createdAt || Date.now())) / 520, 0, 1); + const eased = 1 - Math.pow(1 - t, 3); + const itemX = lerp(startX, targetX, eased) + .5; + const itemY = lerp(startY, targetY, eased) + .5; + if (!Number.isFinite(itemX) || !Number.isFinite(itemY)) continue; + if (!isApproxVisible(asset, itemX, itemY, rect)) continue; + items.push({ kind: pending.kind, asset, x: itemX, y: itemY, source: { ...source, pending: true }, pending: true }); + } if (placementPreview?.asset && placementPreview.x != null && placementPreview.y != null) { const asset = placementPreview.asset; items.push({ kind: asset.category === 'dynamic' ? 'dynamic' : 'static', asset, x: placementPreview.x + .5, y: placementPreview.y + .5, source: { id: 'placement-preview', preview: true }, preview: true }); @@ -1214,11 +1309,11 @@ localY = worldY - info.drawY; } if (localX < 0 || localY < 0 || localX >= info.sprite.width || localY >= info.sprite.height) return false; - // Tiny 8×8 works are hard to click. Use the whole sprite box for them, - // including transparent cells, while larger works still use opaque pixels. + // Small works at 16×16 or below are hard to click. Use the whole sprite box + // for them, including transparent cells, while larger works still use opaque pixels. const aw = assetWidth(asset); const ah = assetHeight(asset); - if (Math.max(aw, ah) <= 8) return true; + if (Math.max(aw, ah) <= 16) return true; const px = Math.floor(localX / scale); const py = Math.floor(localY / scale); if (px < 0 || py < 0 || px >= aw || py >= ah) return false; @@ -3668,30 +3763,38 @@ } function hydrateRuntime() { + const authoritativeMotion = isSharedWorld() && isServerAuthoritative('dynamicMotion'); dynamicRuntime = state.dynamicSummons.map((summon) => { const asset = findAsset(summon.assetId); if (!asset) return null; const homeTile = world.get(Math.round(summon.homeX), Math.round(summon.homeY)); const homeValid = homeTile && ((asset.subtype === 'fish') ? homeTile.type === 'water' : homeTile.type !== 'water'); const spawn = homeValid ? { x: Math.round(summon.homeX), y: Math.round(summon.homeY) } : findValidSpawn(asset, summon.homeX, summon.homeY, 6); + const synced = authoritativeMotion ? (state.serverSync?.dynamicTargets?.[summon.id] || summon.serverState || {}) : {}; + const startX = Number.isFinite(Number(synced.x)) ? Number(synced.x) : (spawn.x + .5); + const startY = Number.isFinite(Number(synced.y)) ? Number(synced.y) : (spawn.y + .5); + const targetX = Number.isFinite(Number(synced.targetX)) ? Number(synced.targetX) : startX; + const targetY = Number.isFinite(Number(synced.targetY)) ? Number(synced.targetY) : startY; + const facing = Number.isFinite(Number(synced.facing)) && Number(synced.facing) !== 0 ? Math.sign(Number(synced.facing)) : 1; return { id: summon.id, assetId: summon.assetId, homeX: summon.homeX, homeY: summon.homeY, - x: spawn.x + .5, - y: spawn.y + .5, - targetX: spawn.x + .5, - targetY: spawn.y + .5, + x: startX, + y: startY, + targetX, + targetY, vx: 0, lastMoveX: 0, - facing: 1, - idleUntil: performance.now() + 700 + Math.random() * 1600, + facing, + idleUntil: authoritativeMotion ? 0 : performance.now() + 700 + Math.random() * 1600, hiddenUntil: 0, seed: Math.random() * 9999, nextDecisionAt: 0, nextBubbleAt: 800 + Math.random() * 1500, - nextStepParticleAt: performance.now() + 300 + Math.random() * 280 + nextStepParticleAt: performance.now() + 300 + Math.random() * 280, + serverMotion: authoritativeMotion }; }).filter(Boolean); } @@ -3718,6 +3821,46 @@ return candidates[Math.floor(Math.random() * candidates.length)]; } + function applyServerDrivenMotion(item, asset, dt, time) { + const synced = state.serverSync?.dynamicTargets?.[item.id] || null; + if (synced) { + if (Number.isFinite(Number(synced.homeX))) item.homeX = Number(synced.homeX); + if (Number.isFinite(Number(synced.homeY))) item.homeY = Number(synced.homeY); + if (Number.isFinite(Number(synced.targetX))) item.targetX = Number(synced.targetX); + if (Number.isFinite(Number(synced.targetY))) item.targetY = Number(synced.targetY); + else { + item.targetX = Number.isFinite(Number(synced.x)) ? Number(synced.x) : item.targetX; + item.targetY = Number.isFinite(Number(synced.y)) ? Number(synced.y) : item.targetY; + } + if (Number.isFinite(Number(synced.facing)) && Number(synced.facing) !== 0) item.facing = Math.sign(Number(synced.facing)); + } else { + item.targetX = item.homeX + .5; + item.targetY = item.homeY + .5; + } + const dx = item.targetX - item.x; + const dy = item.targetY - item.y; + const len = Math.hypot(dx, dy); + if (len < 0.001) { + item.vx = 0; + item.lastMoveX = 0; + return; + } + const speed = ({ human: .85, animal: .74, fish: .55, bird: .8 }[asset.subtype] || .6) * dt; + const step = Math.min(speed, len); + const moveX = (dx / len) * step; + const moveY = (dy / len) * step; + if (Math.abs(moveX) > 0.002) item.facing = Math.sign(moveX); + item.vx = Math.abs(moveX) > 0.002 ? Math.sign(moveX) : 0; + item.lastMoveX = moveX; + item.x += moveX; + item.y += moveY; + if (visualSettings().enableParticles && asset.subtype !== 'fish' && asset.subtype !== 'bird' && time >= (item.nextStepParticleAt || 0)) { + const groundTile = world.get(clamp(Math.floor(item.x), 0, WORLD_W - 1), clamp(Math.floor(item.y), 0, WORLD_H - 1)); + if (groundTile && groundTile.type !== 'water') spawnGroundStepParticles(item, time, groundTile); + item.nextStepParticleAt = time + 300 + Math.random() * 320; + } + } + function updateDynamicRuntime(dt, time) { bubbleParticles = bubbleParticles.filter((p) => time - p.started < p.life); confettiParticles = confettiParticles.filter((p) => time - p.started < p.life); @@ -3731,6 +3874,10 @@ spawnFishBubbleCluster(item.x, item.y, time, item.seed); item.nextBubbleAt = time + 1100 + Math.random() * 1800; } + if (item.serverMotion) { + applyServerDrivenMotion(item, asset, dt, time); + continue; + } if (time < (item.idleUntil || 0)) { item.vx = 0; item.lastMoveX = 0; @@ -3875,7 +4022,7 @@ els.analogClock.setAttribute('aria-label', `${phase.label} island clock`); els.analogClock.title = `${phase.label} · 10 min = 1 island day`; } - const second = Math.floor(Date.now() / 1000); + const second = Math.floor(getAuthoritativeNow() / 1000); if (second !== lastClockSecond) { lastClockSecond = second; if (els.phaseLabel) els.phaseLabel.textContent = phase.label; @@ -3953,11 +4100,13 @@ } function getPhase() { - if (Lighting?.getPhase) return Lighting.getPhase({ dayMs: DAY_MS, now: Date.now(), dayNightEnabled: visualSettings().enableDayNight !== false, mixHex }); + const authorityNow = getAuthoritativeNow(); + const dayMs = Number(state.serverSync?.clock?.dayMs) || DAY_MS; + if (Lighting?.getPhase) return Lighting.getPhase({ dayMs, now: authorityNow, dayNightEnabled: visualSettings().enableDayNight !== false, mixHex }); if (visualSettings().enableDayNight === false) { return { key: 'day', label: 'Day', progress: 0.25, sky: '#86d5ff', darkness: 0, darkOverlay: 'rgba(12, 19, 45, 0)', tint: 'rgba(255,255,255,0)', tintAlpha: 0, shadow: getShadowForMinute(3) }; } - const t = mod(Date.now(), DAY_MS); + const t = mod(authorityNow, dayMs); const minute = t / 60000; const stops = [ { at: 0.00, key: 'preDawn', label: 'Night', sky: '#17254e', darkness: 0.52, tint: [22, 28, 66, 0.10], overlay: [12, 19, 45] }, @@ -3986,7 +4135,7 @@ return { key: dominantKey, label, - progress: t / DAY_MS, + progress: t / dayMs, sky: mixSky, darkness, darkOverlay: `rgba(${Math.round(overlay[0])}, ${Math.round(overlay[1])}, ${Math.round(overlay[2])}, ${darkness.toFixed(3)})`, @@ -4201,8 +4350,10 @@ function drawNightCursorGlow(time, phase) { if (!areNightLightsActive(phase)) return; - const sx = cursorScreen.active ? cursorScreen.x : cw / 2; - const sy = cursorScreen.active ? cursorScreen.y : ch / 2; + const cursorPoint = getCursorLightScreenPoint(phase); + if (!cursorPoint) return; + const sx = cursorPoint.x; + const sy = cursorPoint.y; const pulse = 0.86 + Math.sin(time / 420) * 0.14; const radius = Math.max(52, 86 * view.zoom) * pulse; ctx.save(); @@ -4338,6 +4489,9 @@ function drawSpriteItem(item, time, lightSources, underwater, phase, drawShadow = true) { const info = getSpriteDrawInfo(item, time, true); + const localLights = getLocalSpriteLights(info, phase); + const litSprite = localLights.length ? getSpriteCanvas(info.asset, info.side, localLights) : info.sprite; + info.sprite = litSprite; const { asset, pos, sprite, drawX, drawY, alpha, angle, stretchX = 1, stretchY = 1 } = info; lastRenderedSpriteInfo.set(item.source?.id || asset.id, { ...info, time }); @@ -4382,6 +4536,38 @@ }; } + function getCursorLightScreenPoint(phase) { + if (cursorScreen.active) return { x: cursorScreen.x, y: cursorScreen.y }; + if (areNightLightsActive(phase)) return { x: cw / 2, y: ch / 2 }; + return null; + } + + function getLocalSpriteLights(info, phase) { + if (visualSettings().enableLights === false || !info?.asset) return []; + const lights = areNightLightsActive(phase) ? getAssetLightPointsForSide(info.asset, info.side) : []; + // The cursor is a local inspection light. It must affect depth shading when active, + // and at night it should also follow the blue-white center glow even before the mouse moves. + const cursorPoint = getCursorLightScreenPoint(phase); + if (cursorPoint) { + const worldX = (cursorPoint.x - view.x) / view.zoom; + const worldY = (cursorPoint.y - view.y) / view.zoom; + const local = spriteLocalPoint(info, worldX, worldY); + const scale = info.asset.category === 'static' ? STATIC_SCALE : DYNAMIC_SCALE; + const lx = local.x / scale - .5; + const ly = local.y / scale - .5; + const reach = Math.max(3.5, Math.max(assetWidth(info.asset), assetHeight(info.asset)) * .72); + if (lx >= -reach && ly >= -reach && lx <= assetWidth(info.asset) + reach && ly <= assetHeight(info.asset) + reach) { + lights.push({ x: lx, y: ly, c: '#8edaff', cursor: true, intensity: 1.35 }); + } + } + return lights; + } + + + function hasCursorInspectionLight(lights) { + return Array.isArray(lights) && lights.some((light) => light?.cursor); + } + function drawWaterLightReflections(lightSources, time, phase) { if (!lightSources?.length || !areNightLightsActive(phase)) return; @@ -4478,7 +4664,7 @@ const sourceIntensity = clamp(Number(source.intensity ?? 1) || 1, 0.25, 1.75); // Make weak light subtle and strong light visibly snap on, instead of a flat linear ramp. const exponential = (Math.exp(3.15 * t * sourceIntensity) - 1) / (Math.exp(3.15 * sourceIntensity) - 1); - const amount = exponential * depthFactor; + const amount = exponential * depthFactor * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER; rr += lc.r * amount; gg += lc.g * amount; bb += lc.b * amount; @@ -4486,7 +4672,7 @@ } if (aa <= 0.002) continue; const inv = 1 / aa; - const alpha = Math.min(0.56, 0.08 + Math.pow(aa, 1.22) * 0.34); + const alpha = Math.min(0.86, 0.10 + Math.pow(aa, 1.05) * 0.42); octx.fillStyle = `rgba(${Math.round(rr * inv)}, ${Math.round(gg * inv)}, ${Math.round(bb * inv)}, ${alpha.toFixed(3)})`; octx.fillRect(x * scale, y * scale, scale, scale); if (aa > 0.42 && scale >= 4) { @@ -5170,14 +5356,17 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { ctx.restore(); } - function getSpriteCanvas(asset, side) { + function getSpriteCanvas(asset, side, localLights = null) { const lightsOn = areNightLightsActive(renderPhase); - const phaseKey = renderPhase ? `${renderPhase.key}:${Math.round((renderPhase.darkness || 0) * 10)}:${lightsOn ? 'lit' : 'unlit'}:${visualSettings().enableLights === false ? 'depthoff' : 'depthon'}` : 'day:unlit'; + const dynamicLights = Array.isArray(localLights) && localLights.length > 0; + const lightSignature = dynamicLights ? localLights.map((l) => `${Math.round(l.x * 2) / 2},${Math.round(l.y * 2) / 2},${l.c || ''},${l.cursor ? 'cursor' : 'self'}`).join('|') : ''; + const phaseKey = renderPhase ? `${renderPhase.key}:${Math.round((renderPhase.darkness || 0) * 10)}:${lightsOn ? 'lit' : 'unlit'}:${visualSettings().enableLights === false ? 'depthoff' : 'depthon'}:${lightSignature}` : `day:unlit:${lightSignature}`; const w = assetWidth(asset); const h = assetHeight(asset); const revision = asset.contentHash || asset.updatedAt || asset.createdAt || asset.pixels || asset.faces?.right || ''; const key = `${asset.id}:${revision}:${side}:${w}x${h}:${asset.category === 'static' ? STATIC_SCALE : DYNAMIC_SCALE}:${phaseKey}`; - if (spriteCache.has(key)) { + const skipCache = dynamicLights && localLights.some((l) => l.cursor); + if (!skipCache && spriteCache.has(key)) { const cached = spriteCache.get(key); spriteCache.delete(key); spriteCache.set(key, cached); @@ -5186,7 +5375,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const pixels = getAssetPixels(asset, side); const rawDepths = normalizeDepthPixels(asset.meta?.depthPixels || [], w, h); const depths = asset.category === 'dynamic' && side === 'left' ? mirrorDepthPixels(rawDepths, w, h) : rawDepths; - const lights = []; + const lights = dynamicLights ? localLights : (lightsOn ? getAssetLightPointsForSide(asset, side) : []); const depthVisualsOn = visualSettings().enableLights !== false; const scale = asset.category === 'static' ? STATIC_SCALE : DYNAMIC_SCALE; const canvas = document.createElement('canvas'); @@ -5203,7 +5392,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { c.fillRect(x * scale, y * scale, scale, scale); } } - rememberSpriteCanvas(key, canvas); + if (!skipCache) rememberSpriteCanvas(key, canvas); return canvas; } @@ -5524,11 +5713,18 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { function loadState() { try { + for (const key of LEGACY_STORAGE_KEYS) localStorage.removeItem(key); const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); const expanded = Phase2Sync?.isCompactState?.(parsed) ? Phase2Sync.expandState(parsed) : parsed; - if (expanded && Array.isArray(expanded.assets)) return normalizeState(expanded); + if (expanded && Array.isArray(expanded.assets)) { + const loadedSchema = Number(expanded.schema || 0); + // v16 reset local art because the palette code mapping and seed artwork were intentionally rebuilt. + // v17 expanded the default gallery. v18 added more nature-themed defaults. v19 added giant showcase objects. v20 added more animals, fish, and birds. v21 added human and artificial-object defaults. v22 widened small-sprite hitboxes and fixed night cursor depth light. v23 strengthens multi-light depth response and upgrades lower-quality seed art while preserving v16+ user state. + if (loadedSchema < 16) return seedState(); + return mergeDefaultGallery(normalizeState(expanded)); + } } } catch (error) { console.warn('Could not load local state.', error); @@ -5713,7 +5909,8 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { publishedAt: item.publishedAt || item.createdAt || item.placedAt || Date.now(), status: item.status || 'active', ownerAccountId: normalizeOwnerAccountId(item.ownerAccountId), - version: Number(item.version) || 1 + version: Number(item.version) || 1, + serverState: item.serverState && typeof item.serverState === 'object' ? { ...item.serverState } : null })).filter((item) => item.assetId); } @@ -5747,31 +5944,383 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return { homeX: pos.x, homeY: pos.y }; } - function seedState() { + function buildDefaultGalleryPack(now = Date.now()) { const assets = [ - makeAsset('Hill Cottage', 'static', 'building', drawCottage(), { hasLight: true, lightPixels: [{ x: 8, y: 8 }, { x: 9, y: 8 }], lightColor: '#ffd86a', door: { x: 7, y: 14 } }), - makeAsset('Pine Cluster', 'static', 'nature', drawPineCluster(), { particlePixels: [{ x: 7, y: 4, c: nearestPaletteCode('#9bdc6d') }, { x: 10, y: 6, c: nearestPaletteCode('#f4a1bb') }] }), - makeAsset('Shell Rock', 'static', 'nature', drawShellRock(), { particlePixels: [{ x: 8, y: 9, c: nearestPaletteCode('#ffffff') }] }), - makeAsset('Wave Skiff', 'static', 'ship', drawShip(), { depthPixels: drawShipDepth(), hasLight: false }), - makeAsset('Fisher Kid', 'dynamic', 'human', drawFisherKidRight()), - makeAsset('Moss Cat', 'dynamic', 'animal', drawCatRight()), - makeAsset('Koi Fish', 'dynamic', 'fish', drawKoiRight()), - makeAsset('Cloud Gull', 'dynamic', 'bird', drawGullRight()) + makeAsset('Crescent Tea House', 'static', 'building', drawCrescentTeaHouse(), { + depthPixels: drawCrescentTeaHouseDepth(), + lightPixels: [{ x: 6, y: 5 }, { x: 9, y: 5 }, { x: 7, y: 10 }, { x: 8, y: 10 }], + lightColor: '#ffe55d', + door: { x: 7, y: 14 } + }), + makeAsset('Prism Sakura', 'static', 'nature', drawPrismSakura(), { + depthPixels: drawPrismSakuraDepth(), + particlePixels: [{ x: 5, y: 4, c: nearestPaletteCode('#ffb0c7'), dir: 'up' }, { x: 10, y: 5, c: nearestPaletteCode('#ead0ff'), dir: 'up' }] + }), + makeAsset('Clockwork Whale', 'static', 'ship', drawClockworkWhale(), { depthPixels: drawClockworkWhaleDepth() }), + makeAsset('Lantern Cat', 'dynamic', 'animal', drawLanternCatRight(), { + lightPixels: [{ x: 11, y: 8 }], + lightColor: '#ffe55d', + depthPixels: drawSmallRaisedDepth() + }), + makeAsset('Cloud Koi', 'dynamic', 'fish', drawCloudKoiRight(), { + particlePixels: [{ x: 5, y: 8, c: nearestPaletteCode('#aef8ff'), dir: 'up' }] + }), + makeAsset('Paper Crane', 'dynamic', 'bird', drawPaperCraneRight(), { + particlePixels: [{ x: 8, y: 6, c: nearestPaletteCode('#eef4ff'), dir: 'up' }] + }), + makeAsset('Moon Lantern', 'static', 'building', drawMoonLantern(), { + depthPixels: drawMoonLanternDepth(), + lightPixels: [{ x: 6, y: 7 }, { x: 7, y: 7 }, { x: 8, y: 7 }, { x: 7, y: 8 }, { x: 8, y: 8 }], + lightColor: '#ffe99a', + door: { x: 7, y: 14 } + }), + makeAsset('Glass Fern', 'static', 'nature', drawGlassFern(), { + depthPixels: filledDepthFromPixels(drawGlassFern()) + }), + makeAsset('Linen Cottage', 'static', 'building', drawCottage(), { + depthPixels: filledDepthFromPixels(drawCottage()), + lightPixels: [{ x: 10, y: 9 }], + lightColor: '#ffe38a', + door: { x: 7, y: 13 } + }), + makeAsset('Pine Cluster', 'static', 'nature', drawPineCluster(), { + depthPixels: filledDepthFromPixels(drawPineCluster()) + }), + makeAsset('Shell Rock', 'static', 'nature', drawShellRock(), { + depthPixels: filledDepthFromPixels(drawShellRock()) + }), + makeAsset('Coral Skiff', 'static', 'ship', drawCoralSkiff(), { depthPixels: drawCoralSkiffDepth() }), + makeAsset('Sprout Fox', 'dynamic', 'animal', drawSproutFoxRight(), { + depthPixels: drawSmallRaisedDepth() + }), + makeAsset('Azure Minnow', 'dynamic', 'fish', drawAzureMinnowRight(), { + particlePixels: [{ x: 5, y: 7, c: nearestPaletteCode('#bff7ff'), dir: 'up' }] + }), + makeAsset('Violet Moth', 'dynamic', 'animal', drawVioletMothRight(), { + particlePixels: [{ x: 7, y: 3, c: nearestPaletteCode('#ead1ff'), dir: 'up' }] + }), + makeAsset('Lantern Walker', 'dynamic', 'human', drawLanternWalkerRight(), { + lightPixels: [{ x: 9, y: 7 }, { x: 11, y: 8 }], + lightColor: '#ffe99a', + depthPixels: drawSmallRaisedDepth() + }), + makeAssetSized('Wildflower Patch', 'static', 'nature', drawWildflowerPatch(), 8, 8, { + depthPixels: filledDepthFromPixelsSized(drawWildflowerPatch(), 8, 8) + }), + makeAssetSized('River Stones', 'static', 'nature', drawRiverStones(), 8, 8, { + depthPixels: filledDepthFromPixelsSized(drawRiverStones(), 8, 8) + }), + makeAsset('Maple Canopy', 'static', 'nature', drawMapleCanopy(), { + depthPixels: drawMapleCanopyDepth() + }), + makeAsset('Misty Falls', 'static', 'nature', drawMistyFalls(), { + depthPixels: drawMistyFallsDepth(), + particlePixels: [{ x: 6, y: 8, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 10, y: 8, c: nearestPaletteCode('#dffbff'), dir: 'up' }] + }), + makeAssetSized('Lotus Pond', 'static', 'nature', drawLotusPond(), 16, 12, { + depthPixels: drawLotusPondDepth(), + particlePixels: [{ x: 6, y: 6, c: nearestPaletteCode('#ffd9e8'), dir: 'up' }, { x: 11, y: 5, c: nearestPaletteCode('#ffffff'), dir: 'up' }] + }), + makeAsset('Mossy Arch', 'static', 'nature', drawMossyArch(), { + depthPixels: drawMossyArchDepth() + }), + makeAssetSized('Sunflower Grove', 'static', 'nature', drawSunflowerGrove(), 12, 16, { + depthPixels: filledDepthFromPixelsSized(drawSunflowerGrove(), 12, 16) + }), + makeAssetSized('Reed Bed', 'static', 'nature', drawReedBed(), 12, 12, { + depthPixels: filledDepthFromPixelsSized(drawReedBed(), 12, 12) + }), + makeAssetSized('Firefly Swirl', 'dynamic', 'animal', drawFireflySwirlRight(), 8, 8, { + lightPixels: [{ x: 3, y: 4 }, { x: 5, y: 3 }], + lightColor: '#fff08a', + depthPixels: filledDepthFromPixelsSized(drawFireflySwirlRight(), 8, 8) + }), + makeAssetSized('Meadow Hare', 'dynamic', 'animal', drawMeadowHareRight(), 12, 12, { + depthPixels: filledDepthFromPixelsSized(drawMeadowHareRight(), 12, 12) + }), + makeAssetSized('Brook Turtle', 'dynamic', 'animal', drawBrookTurtleRight(), 12, 10, { + depthPixels: filledDepthFromPixelsSized(drawBrookTurtleRight(), 12, 10), + particlePixels: [{ x: 1, y: 7, c: nearestPaletteCode('#a8e5ff'), dir: 'up' }] + }), + makeAssetSized('Leaf Sparrow', 'dynamic', 'bird', drawLeafSparrowRight(), 8, 8, { + depthPixels: filledDepthFromPixelsSized(drawLeafSparrowRight(), 8, 8) + }), + makeAssetSized('Red Panda', 'dynamic', 'animal', drawRedPandaRight(), 12, 12, { + depthPixels: filledDepthFromPixelsSized(drawRedPandaRight(), 12, 12) + }), + makeAssetSized('River Otter', 'dynamic', 'animal', drawRiverOtterRight(), 14, 8, { + depthPixels: filledDepthFromPixelsSized(drawRiverOtterRight(), 14, 8) + }), + makeAssetSized('Amber Deer', 'dynamic', 'animal', drawAmberDeerRight(), 14, 14, { + depthPixels: filledDepthFromPixelsSized(drawAmberDeerRight(), 14, 14) + }), + makeAssetSized('Forest Owl', 'dynamic', 'bird', drawForestOwlRight(), 10, 12, { + depthPixels: filledDepthFromPixelsSized(drawForestOwlRight(), 10, 12) + }), + makeAssetSized('Kingfisher', 'dynamic', 'bird', drawKingfisherRight(), 10, 8, { + depthPixels: filledDepthFromPixelsSized(drawKingfisherRight(), 10, 8) + }), + makeAssetSized('Pond Duck', 'dynamic', 'bird', drawPondDuckRight(), 12, 10, { + depthPixels: filledDepthFromPixelsSized(drawPondDuckRight(), 12, 10) + }), + makeAssetSized('Heron', 'dynamic', 'bird', drawHeronRight(), 10, 16, { + depthPixels: filledDepthFromPixelsSized(drawHeronRight(), 10, 16) + }), + makeAssetSized('Sunset Koi', 'dynamic', 'fish', drawSunsetKoiRight(), 12, 8, { + depthPixels: filledDepthFromPixelsSized(drawSunsetKoiRight(), 12, 8), + particlePixels: [{ x: 2, y: 4, c: nearestPaletteCode('#c8f4ff'), dir: 'up' }] + }), + makeAssetSized('Silver Trout', 'dynamic', 'fish', drawSilverTroutRight(), 12, 6, { + depthPixels: filledDepthFromPixelsSized(drawSilverTroutRight(), 12, 6), + particlePixels: [{ x: 1, y: 3, c: nearestPaletteCode('#dffbff'), dir: 'up' }] + }), + makeAssetSized('Butterfly Fish', 'dynamic', 'fish', drawButterflyFishRight(), 10, 8, { + depthPixels: filledDepthFromPixelsSized(drawButterflyFishRight(), 10, 8), + particlePixels: [{ x: 1, y: 4, c: nearestPaletteCode('#bff7ff'), dir: 'up' }] + }), + makeAssetSized('Town Gardener', 'dynamic', 'human', drawTownGardenerRight(), 12, 16, { + depthPixels: filledDepthFromPixelsSized(drawTownGardenerRight(), 12, 16) + }), + makeAssetSized('Lantern Courier', 'dynamic', 'human', drawLanternCourierRight(), 12, 16, { + depthPixels: filledDepthFromPixelsSized(drawLanternCourierRight(), 12, 16), + lightPixels: [{ x: 8, y: 9 }], + lightColor: '#ffe99a' + }), + makeAssetSized('Plaza Musician', 'dynamic', 'human', drawPlazaMusicianRight(), 14, 16, { + depthPixels: filledDepthFromPixelsSized(drawPlazaMusicianRight(), 14, 16) + }), + makeAssetSized('Bridge Mechanic', 'dynamic', 'human', drawBridgeMechanicRight(), 14, 16, { + depthPixels: filledDepthFromPixelsSized(drawBridgeMechanicRight(), 14, 16) + }), + makeAssetSized('Harbor Clocktower', 'static', 'building', drawHarborClocktower(), 32, 40, { + depthPixels: filledDepthFromPixelsSized(drawHarborClocktower(), 32, 40), + lightPixels: [{ x: 15, y: 12 }, { x: 16, y: 12 }, { x: 13, y: 22 }, { x: 18, y: 22 }, { x: 14, y: 28 }, { x: 17, y: 28 }], + lightColor: '#ffe99a', + door: { x: 15, y: 38 } + }), + makeAssetSized('Glass Greenhouse', 'static', 'building', drawGlassGreenhouse(), 28, 20, { + depthPixels: filledDepthFromPixelsSized(drawGlassGreenhouse(), 28, 20), + lightPixels: [{ x: 10, y: 10 }, { x: 13, y: 10 }, { x: 16, y: 10 }], + lightColor: '#fff4c2', + door: { x: 13, y: 18 } + }), + makeAssetSized('Steam Workshop', 'static', 'building', drawSteamWorkshop(), 40, 24, { + depthPixels: filledDepthFromPixelsSized(drawSteamWorkshop(), 40, 24), + lightPixels: [{ x: 13, y: 14 }, { x: 18, y: 14 }, { x: 27, y: 14 }, { x: 32, y: 14 }], + lightColor: '#ffd979', + door: { x: 20, y: 22 } + }), + makeAssetSized('Canal Bridge', 'static', 'building', drawCanalBridge(), 48, 16, { + depthPixels: filledDepthFromPixelsSized(drawCanalBridge(), 48, 16) + }), + makeAssetSized('Grand Fountain', 'static', 'building', drawGrandFountain(), 24, 24, { + depthPixels: filledDepthFromPixelsSized(drawGrandFountain(), 24, 24), + lightPixels: [{ x: 11, y: 7 }, { x: 12, y: 7 }, { x: 9, y: 12 }, { x: 14, y: 12 }], + lightColor: '#dffbff', + particlePixels: [{ x: 11, y: 5, c: nearestPaletteCode('#eafcff'), dir: 'up' }, { x: 12, y: 5, c: nearestPaletteCode('#eafcff'), dir: 'up' }] + }), + makeAssetSized('Rocket Monument', 'static', 'building', drawRocketMonument(), 20, 32, { + depthPixels: filledDepthFromPixelsSized(drawRocketMonument(), 20, 32), + lightPixels: [{ x: 9, y: 8 }, { x: 10, y: 8 }, { x: 8, y: 24 }, { x: 11, y: 24 }, { x: 9, y: 27 }, { x: 10, y: 27 }], + lightColor: '#ffdca6', + particlePixels: [{ x: 8, y: 27, c: nearestPaletteCode('#ffd979'), dir: 'up' }, { x: 11, y: 27, c: nearestPaletteCode('#ff8a5c'), dir: 'up' }], + door: { x: 9, y: 30 } + }), + makeAssetSized('Arcade Booth', 'static', 'building', drawArcadeBooth(), 24, 20, { + depthPixels: filledDepthFromPixelsSized(drawArcadeBooth(), 24, 20), + lightPixels: [{ x: 7, y: 10 }, { x: 16, y: 10 }, { x: 11, y: 6 }], + lightColor: '#98d8ff', + door: { x: 11, y: 18 } + }), + makeAssetSized('Chess Knight Statue', 'static', 'building', drawChessKnightStatue(), 16, 24, { + depthPixels: filledDepthFromPixelsSized(drawChessKnightStatue(), 16, 24), + lightPixels: [{ x: 7, y: 8 }, { x: 8, y: 8 }], + lightColor: '#dffbff' + }), + makeAssetSized('Desert Train', 'static', 'building', drawDesertTrain(), 44, 16, { + depthPixels: filledDepthFromPixelsSized(drawDesertTrain(), 44, 16), + lightPixels: [{ x: 7, y: 7 }, { x: 13, y: 8 }, { x: 19, y: 8 }, { x: 28, y: 8 }, { x: 35, y: 8 }], + lightColor: '#ffd979', + particlePixels: [{ x: 5, y: 3, c: nearestPaletteCode('#d9e4e8'), dir: 'up' }, { x: 7, y: 2, c: nearestPaletteCode('#d9e4e8'), dir: 'up' }] + }), + makeAssetSized('Tea Robot', 'dynamic', 'human', drawTeaRobotRight(), 12, 16, { + depthPixels: filledDepthFromPixelsSized(drawTeaRobotRight(), 12, 16), + lightPixels: [{ x: 8, y: 8 }], + lightColor: '#9be6ff' + }), + makeAssetSized('Balloon Vendor', 'dynamic', 'human', drawBalloonVendorRight(), 14, 18, { + depthPixels: filledDepthFromPixelsSized(drawBalloonVendorRight(), 14, 18) + }), + makeAssetSized('Jelly Comet', 'dynamic', 'animal', drawJellyCometRight(), 12, 12, { + depthPixels: filledDepthFromPixelsSized(drawJellyCometRight(), 12, 12), + lightPixels: [{ x: 7, y: 4 }], + lightColor: '#b8d6ff', + particlePixels: [{ x: 2, y: 6, c: nearestPaletteCode('#d8f0ff'), dir: 'up' }] + }), + makeAssetSized('Courier Bike', 'dynamic', 'human', drawCourierBikeRight(), 16, 12, { + depthPixels: filledDepthFromPixelsSized(drawCourierBikeRight(), 16, 12) + }), + makeAssetSized('Great Cedar', 'static', 'nature', drawGreatCedar(), 24, 32, { + depthPixels: drawGreatCedarDepth() + }), + makeAssetSized('Moonfall Cascade', 'static', 'nature', drawMoonfallCascade(), 24, 32, { + depthPixels: drawMoonfallCascadeDepth(), + particlePixels: [{ x: 9, y: 20, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 14, y: 20, c: nearestPaletteCode('#dffbff'), dir: 'up' }] + }), + makeAssetSized('Sunblossom Gate', 'static', 'nature', drawSunblossomGate(), 24, 24, { + depthPixels: drawSunblossomGateDepth() + }), + makeAssetSized('Echo Cavern', 'static', 'nature', drawEchoCavern(), 32, 20, { + depthPixels: drawEchoCavernDepth() + }), + makeAssetSized('Worldroot Shrine', 'static', 'nature', drawWorldrootShrine(), 28, 28, { + depthPixels: drawWorldrootShrineDepth(), + lightPixels: [{ x: 13, y: 12 }, { x: 14, y: 12 }, { x: 12, y: 13 }, { x: 15, y: 13 }], + lightColor: '#ffe99a', + door: { x: 14, y: 24 } + }) ]; const idByName = Object.fromEntries(assets.map((a) => [a.name, a.id])); - const shipPos = findNearestTerrain('water', 62, 58); - const fishPos = findNearestTerrain('water', 66, 60); + const whalePos = findNearestTerrain('water', 62, 58); + const koiPos = findNearestTerrain('water', 66, 60); + const skiffPos = findNearestTerrain('water', 56, 58); + const minnowPos = findNearestTerrain('water', 71, 57); + const turtlePos = findNearestTerrain('water', 53, 60); + const sunsetKoiPos = findNearestTerrain('water', 60, 61); + const troutPos = findNearestTerrain('water', 75, 56); + const butterflyFishPos = findNearestTerrain('water', 68, 63); + return { + assets, + placed: [ + { id: uid(), assetId: idByName['Crescent Tea House'], ownerAccountId: 'island-team', x: 36, y: 36, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Prism Sakura'], ownerAccountId: 'island-team', x: 32, y: 38, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Clockwork Whale'], ownerAccountId: 'island-team', ...whalePos, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Moon Lantern'], ownerAccountId: 'island-team', x: 41, y: 37, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Glass Fern'], ownerAccountId: 'island-team', x: 29, y: 40, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Linen Cottage'], ownerAccountId: 'island-team', x: 45, y: 40, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Pine Cluster'], ownerAccountId: 'island-team', x: 47, y: 34, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Shell Rock'], ownerAccountId: 'island-team', x: 40, y: 43, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Coral Skiff'], ownerAccountId: 'island-team', ...skiffPos, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Wildflower Patch'], ownerAccountId: 'island-team', x: 50, y: 39, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['River Stones'], ownerAccountId: 'island-team', x: 52, y: 41, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Maple Canopy'], ownerAccountId: 'island-team', x: 25, y: 34, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Misty Falls'], ownerAccountId: 'island-team', x: 59, y: 33, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Lotus Pond'], ownerAccountId: 'island-team', x: 53, y: 36, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Mossy Arch'], ownerAccountId: 'island-team', x: 20, y: 38, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Sunflower Grove'], ownerAccountId: 'island-team', x: 24, y: 43, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Reed Bed'], ownerAccountId: 'island-team', x: 58, y: 40, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Harbor Clocktower'], ownerAccountId: 'island-team', x: 108, y: 34, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Glass Greenhouse'], ownerAccountId: 'island-team', x: 87, y: 38, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Steam Workshop'], ownerAccountId: 'island-team', x: 78, y: 47, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Canal Bridge'], ownerAccountId: 'island-team', x: 84, y: 60, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Grand Fountain'], ownerAccountId: 'island-team', x: 96, y: 52, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Rocket Monument'], ownerAccountId: 'island-team', x: 117, y: 46, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Arcade Booth'], ownerAccountId: 'island-team', x: 101, y: 58, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Chess Knight Statue'], ownerAccountId: 'island-team', x: 92, y: 53, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Desert Train'], ownerAccountId: 'island-team', x: 23, y: 68, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Great Cedar'], ownerAccountId: 'island-team', x: 16, y: 39, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Moonfall Cascade'], ownerAccountId: 'island-team', x: 72, y: 35, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Sunblossom Gate'], ownerAccountId: 'island-team', x: 34, y: 47, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Echo Cavern'], ownerAccountId: 'island-team', x: 98, y: 42, placedAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Worldroot Shrine'], ownerAccountId: 'island-team', x: 58, y: 31, placedAt: now, publishedAt: now, version: 1 } + ], + dynamicSummons: [ + { id: uid(), assetId: idByName['Lantern Cat'], ownerAccountId: 'island-team', homeX: 38, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Cloud Koi'], ownerAccountId: 'island-team', ...homeFromPos(koiPos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Paper Crane'], ownerAccountId: 'island-team', homeX: 91, homeY: 31, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Sprout Fox'], ownerAccountId: 'island-team', homeX: 46, homeY: 40, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Violet Moth'], ownerAccountId: 'island-team', homeX: 31, homeY: 35, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Lantern Walker'], ownerAccountId: 'island-team', homeX: 34, homeY: 34, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Azure Minnow'], ownerAccountId: 'island-team', ...homeFromPos(minnowPos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Firefly Swirl'], ownerAccountId: 'island-team', homeX: 26, homeY: 37, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Meadow Hare'], ownerAccountId: 'island-team', homeX: 43, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Brook Turtle'], ownerAccountId: 'island-team', ...homeFromPos(turtlePos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Leaf Sparrow'], ownerAccountId: 'island-team', homeX: 23, homeY: 34, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Red Panda'], ownerAccountId: 'island-team', homeX: 27, homeY: 41, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['River Otter'], ownerAccountId: 'island-team', homeX: 62, homeY: 42, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Amber Deer'], ownerAccountId: 'island-team', homeX: 18, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Forest Owl'], ownerAccountId: 'island-team', homeX: 21, homeY: 33, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Kingfisher'], ownerAccountId: 'island-team', homeX: 57, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Pond Duck'], ownerAccountId: 'island-team', homeX: 54, homeY: 38, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Heron'], ownerAccountId: 'island-team', homeX: 64, homeY: 36, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Sunset Koi'], ownerAccountId: 'island-team', ...homeFromPos(sunsetKoiPos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Silver Trout'], ownerAccountId: 'island-team', ...homeFromPos(troutPos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Butterfly Fish'], ownerAccountId: 'island-team', ...homeFromPos(butterflyFishPos), createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Town Gardener'], ownerAccountId: 'island-team', homeX: 89, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Lantern Courier'], ownerAccountId: 'island-team', homeX: 110, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Plaza Musician'], ownerAccountId: 'island-team', homeX: 97, homeY: 53, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Bridge Mechanic'], ownerAccountId: 'island-team', homeX: 84, homeY: 58, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Tea Robot'], ownerAccountId: 'island-team', homeX: 104, homeY: 59, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Balloon Vendor'], ownerAccountId: 'island-team', homeX: 98, homeY: 58, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Jelly Comet'], ownerAccountId: 'island-team', homeX: 112, homeY: 53, createdAt: now, publishedAt: now, version: 1 }, + { id: uid(), assetId: idByName['Courier Bike'], ownerAccountId: 'island-team', homeX: 88, homeY: 60, createdAt: now, publishedAt: now, version: 1 } + ] + }; + } + + function mergeDefaultGallery(baseState) { + if (!baseState || typeof baseState !== 'object') return seedState(); + const seeded = buildDefaultGalleryPack(Date.now()); + baseState.assets = Array.isArray(baseState.assets) ? baseState.assets : []; + baseState.placed = Array.isArray(baseState.placed) ? baseState.placed : []; + baseState.dynamicSummons = Array.isArray(baseState.dynamicSummons) ? baseState.dynamicSummons : []; + const assetByName = new Map(baseState.assets.map((asset) => [asset.name, asset])); + const assetIdByName = new Map(baseState.assets.map((asset) => [asset.name, asset.id])); + for (const seededAsset of seeded.assets) { + const existing = assetByName.get(seededAsset.name); + if (!existing) { + baseState.assets.push(seededAsset); + assetByName.set(seededAsset.name, seededAsset); + assetIdByName.set(seededAsset.name, seededAsset.id); + continue; + } + const isDefaultAsset = existing.ownerAccountId === 'island-team' || existing.author === 'Island Team'; + if (isDefaultAsset) { + const existingId = existing.id; + const createdAt = existing.createdAt || seededAsset.createdAt; + Object.assign(existing, { + ...seededAsset, + id: existingId, + createdAt, + updatedAt: Date.now(), + ownerAccountId: 'island-team', + author: 'Island Team' + }); + existing.contentHash = computeAssetContentHash(existing); + assetIdByName.set(seededAsset.name, existingId); + } + } + const seededNameById = new Map(seeded.assets.map((asset) => [asset.id, asset.name])); + const hasPlacedAsset = new Set(baseState.placed.filter((item) => item && item.ownerAccountId === 'island-team').map((item) => item.assetId)); + for (const item of seeded.placed) { + const name = seededNameById.get(item.assetId); + const resolvedAssetId = assetIdByName.get(name); + if (resolvedAssetId && !hasPlacedAsset.has(resolvedAssetId)) { + baseState.placed.push({ ...item, id: uid(), assetId: resolvedAssetId }); + hasPlacedAsset.add(resolvedAssetId); + } + } + const hasDynamicAsset = new Set(baseState.dynamicSummons.filter((item) => item && item.ownerAccountId === 'island-team').map((item) => item.assetId)); + for (const item of seeded.dynamicSummons) { + const name = seededNameById.get(item.assetId); + const resolvedAssetId = assetIdByName.get(name); + if (resolvedAssetId && !hasDynamicAsset.has(resolvedAssetId)) { + baseState.dynamicSummons.push({ ...item, id: uid(), assetId: resolvedAssetId }); + hasDynamicAsset.add(resolvedAssetId); + } + } + return baseState; + } + + function seedState() { + const now = Date.now(); + const gallery = buildDefaultGalleryPack(now); return { schema: SAVE_SCHEMA, authorName: 'Local Artist', - assets, - placed: [ - { id: uid(), assetId: idByName['Hill Cottage'], ownerAccountId: 'island-team', x: 36, y: 36, placedAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Pine Cluster'], ownerAccountId: 'island-team', x: 32, y: 36, placedAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Shell Rock'], ownerAccountId: 'island-team', x: 39, y: 40, placedAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Wave Skiff'], ownerAccountId: 'island-team', ...shipPos, placedAt: Date.now(), version: 1 } - ], + assets: gallery.assets, + placed: gallery.placed, objectVotes: {}, assetVotes: {}, hiddenAssets: {}, @@ -5782,29 +6331,40 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { account: null, publishLog: [], worldMode: 'local', - serverSync: { lastServerEventId: null, pendingCommands: [] }, + serverSync: { + lastServerEventId: null, + pendingCommands: [], + authority: { ...DEFAULT_SERVER_AUTHORITY }, + clock: { worldTimeMs: now, syncedAt: now, dayMs: DAY_MS }, + dynamicTargets: {}, + pendingObjectVisuals: {} + }, tombstones: { assets: {}, objects: {} }, - dynamicSummons: [ - { id: uid(), assetId: idByName['Fisher Kid'], ownerAccountId: 'island-team', homeX: 36, homeY: 38, createdAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Moss Cat'], ownerAccountId: 'island-team', homeX: 33, homeY: 40, createdAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Koi Fish'], ownerAccountId: 'island-team', ...homeFromPos(fishPos), createdAt: Date.now(), version: 1 }, - { id: uid(), assetId: idByName['Cloud Gull'], ownerAccountId: 'island-team', homeX: 90, homeY: 30, createdAt: Date.now(), version: 1 } - ] + dynamicSummons: gallery.dynamicSummons }; } function makeAsset(name, category, subtype, pixels, meta = {}, leftPixels = null) { - const size = 16; + return makeAssetSized(name, category, subtype, pixels, 16, 16, meta, leftPixels); + } + + function makeAssetSized(name, category, subtype, pixels, width = 16, height = width, meta = {}, leftPixels = null) { + const w = clampDimension(width, 16); + const h = clampDimension(height, w); + const size = Math.max(w, h); const id = uid(); - const right = alignPixelsToBottom(normalizePixels(pixels, size), size); + const right = alignPixelsToBottomRect(normalizePixels(pixels, w, h), w, h); + const left = leftPixels ? alignPixelsToBottomRect(normalizePixels(leftPixels, w, h), w, h) : null; const asset = { id, name, category, subtype, size, - pixels: encodePixels(right), - faces: category === 'dynamic' ? { right: encodePixels(right), left: 'mirror' } : null, + width: w, + height: h, + pixels: encodePixels(right, w, h), + faces: category === 'dynamic' ? { right: encodePixels(right, w, h), left: left ? encodePixels(left, w, h) : 'mirror' } : null, parentAssetId: null, originalAssetId: null, createdAt: Date.now(), @@ -5812,7 +6372,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { author: 'Island Team', ownerAccountId: 'island-team', version: 1, - meta: buildAssetMeta(category, subtype, meta.depthPixels || [], meta.lightPixels || [], meta.lightColor || '#ffd86a', meta.door || null, size, meta.particleConfig || meta.particlePixels || [], right, size) + meta: buildAssetMeta(category, subtype, meta.depthPixels || [], meta.lightPixels || [], meta.lightColor || '#ffd86a', meta.door || null, w, meta.particleConfig || meta.particlePixels || [], right, h) }; asset.contentHash = computeAssetContentHash(asset); return asset; @@ -5820,20 +6380,24 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { function alignPixelsToBottom(pixels, size) { - const source = normalizePixels(pixels, size); + return alignPixelsToBottomRect(pixels, size, size); + } + + function alignPixelsToBottomRect(pixels, width, height = width) { + const source = normalizePixels(pixels, width, height); let maxY = -1; - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - if (source[y * size + x]) maxY = Math.max(maxY, y); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (source[y * width + x]) maxY = Math.max(maxY, y); } } - if (maxY < 0 || maxY === size - 1) return source; - const dy = size - 1 - maxY; - const out = blankPixels(size); - for (let y = 0; y < size; y++) { - for (let x = 0; x < size; x++) { - const value = source[y * size + x]; - if (value && y + dy < size) out[(y + dy) * size + x] = value; + if (maxY < 0 || maxY === height - 1) return source; + const dy = height - 1 - maxY; + const out = blankPixels(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const value = source[y * width + x]; + if (value && y + dy < height) out[(y + dy) * width + x] = value; } } return out; @@ -5994,6 +6558,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { else if (StateIndex?.reduce) StateIndex.reduce(state, event, { unpackAsset: Phase2Sync?.unpackAsset, unpackObject }); else Phase2Sync?.applyEvent?.(state, event); rebuildWorldIndex(); + if (isSharedWorld() && (event.type === 'dynamic.move' || (event.type === 'object.upsert' && event.kind === 'dynamic'))) hydrateRuntime(); spriteCache.clear(); state.sync ||= { lastEventId: null }; state.sync.lastEventId = event.id || state.sync.lastEventId; @@ -6002,7 +6567,38 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { function applyServerIssuedEvent(event, unpackObject) { ensureWorldProtectionState(); - if (event.type === 'asset.upsert' && event.asset) { + if (event.type === 'world.phase') { + state.serverSync.clock = { + worldTimeMs: Number(event.worldTimeMs ?? event.serverNow ?? event.serverAt) || Date.now(), + syncedAt: Date.now(), + dayMs: Number(event.dayMs) || DAY_MS, + phase: event.phase || null, + serverEventId: event.serverEventId || null + }; + } else if (event.type === 'dynamic.move') { + const object = event.object ? unpackObject('dynamic', event.object) : null; + const objectId = String(event.objectId || event.dynamicId || object?.id || ''); + if (!objectId || isObjectTombstoned(objectId, Number(object?.version || event.objectVersion || 1))) return; + const target = { + objectId, + x: Number(event.x ?? object?.serverState?.x ?? object?.x ?? object?.homeX ?? 0), + y: Number(event.y ?? object?.serverState?.y ?? object?.y ?? object?.homeY ?? 0), + targetX: Number(event.targetX ?? object?.serverState?.targetX ?? event.x ?? object?.x ?? object?.homeX ?? 0), + targetY: Number(event.targetY ?? object?.serverState?.targetY ?? event.y ?? object?.y ?? object?.homeY ?? 0), + homeX: Number(event.homeX ?? object?.homeX ?? 0), + homeY: Number(event.homeY ?? object?.homeY ?? 0), + facing: Number(event.facing ?? object?.serverState?.facing ?? 1) || 1, + serverAt: Number(event.serverAt) || Date.now() + }; + state.serverSync.dynamicTargets[objectId] = target; + delete state.serverSync.pendingObjectVisuals?.[objectId]; + if (object?.id) { + const normalized = normalizeDynamicSummons([{ ...object, serverState: target }])[0]; + const index = state.dynamicSummons.findIndex((item) => item.id === normalized.id); + if (index >= 0) state.dynamicSummons[index] = normalized; + else state.dynamicSummons.push(normalized); + } + } else if (event.type === 'asset.upsert' && event.asset) { const asset = event.asset.p && Phase2Sync?.unpackAsset ? Phase2Sync.unpackAsset(event.asset) : event.asset; if (!asset?.id || isAssetTombstoned(asset.id, asset.version || event.assetVersion || 1)) return; const index = state.assets.findIndex((item) => item.id === asset.id); @@ -6017,12 +6613,14 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const index = list.findIndex((item) => item.id === normalized.id); if (index >= 0) list[index] = normalized; else list.push(normalized); + delete state.serverSync.pendingObjectVisuals?.[normalized.id]; } else if (event.type === 'object.delete' && event.objectId) { const kind = event.kind === 'dynamic' ? 'dynamic' : 'static'; const tombstone = event.tombstone || { id: event.objectId, deletedAt: event.serverAt, deletedBy: event.actorAccountId, version: event.objectVersion || 1 }; state.tombstones.objects[event.objectId] = tombstone; if (kind === 'dynamic') state.dynamicSummons = state.dynamicSummons.filter((item) => item.id !== event.objectId); else state.placed = state.placed.filter((item) => item.id !== event.objectId); + delete state.serverSync.pendingObjectVisuals?.[event.objectId]; delete state.objectVotes?.[event.objectId]; delete state.hiddenObjects?.[event.objectId]; state.moderationReports = (state.moderationReports || []).filter((report) => report.objectId !== event.objectId); @@ -6176,7 +6774,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const saved = report.savedPercent > 0 ? `${report.savedPercent}% smaller` : 'no saving yet'; const statsRect = getViewportWorldRect(64); const visibleChunkCount = terrainCache?.chunks?.filter((chunk) => !(chunk.x + chunk.w < statsRect.left || chunk.x > statsRect.right || chunk.y + chunk.h < statsRect.top || chunk.y > statsRect.bottom)).length || 0; - els.syncStats.textContent = `Compact local save: ${report.compactBytes.toLocaleString()} bytes / full ${report.fullBytes.toLocaleString()} bytes (${saved}). Assets ${report.assets}, objects ${report.objects}, queued events ${state.eventLog?.length || 0}. Terrain chunks ${visibleChunkCount}/${terrainCache?.chunks?.length || 0}.`; + els.syncStats.textContent = `Compact local save: ${report.compactBytes.toLocaleString()} bytes / full ${report.fullBytes.toLocaleString()} bytes (${saved}). Assets ${report.assets}, objects ${report.objects}, queued events ${state.eventLog?.length || 0}, pending commands ${state.serverSync?.pendingCommands?.length || 0}. Authority: publish ${serverAuthority().publish}, move ${serverAuthority().objectMove}, day/night ${serverAuthority().dayNight}, dynamic ${serverAuthority().dynamicMotion}. Terrain chunks ${visibleChunkCount}/${terrainCache?.chunks?.length || 0}.`; } function computeAssetContentHash(asset) { @@ -6360,22 +6958,36 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { let r = rgb.r + depthBoost + exposure + edgeBoost + selfShadow; let g = rgb.g + depthBoost + exposure + edgeBoost + selfShadow; let b = rgb.b + depthBoost + exposure + edgeBoost + selfShadow; + let localTintR = 0; + let localTintG = 0; + let localTintB = 0; + let localTintWeight = 0; + let localLift = 0; - if (areNightLightsActive(activePhase) && lights?.length) { + if ((areNightLightsActive(activePhase) || hasCursorInspectionLight(lights)) && lights?.length) { for (const light of lights) { const lc = parseHex(light.c || '#ffd86a'); if (!lc) continue; const dist = Math.hypot((light.x + 0.5) - (x + 0.5), (light.y + 0.5) - (y + 0.5)); - const reach = Math.max(2.25, size * 0.42); + const reach = Math.max(2.25, size * 0.46); const t = 1 - clamp(dist / reach, 0, 1); if (t <= 0) continue; + const sourceIntensity = clamp(Number(light.intensity ?? 1) || 1, 0.25, 1.85); const edge = getDepthLightFacing(depth, x, y, width, pixels, depths, light.x, light.y, height); - const depthFactor = depth > 0 ? 0.72 + edge * 0.24 : depth < 0 ? 0.55 + edge * 0.12 : 0.42 + edge * 0.06; - const strength = t * t * (6 + Math.abs(depth) * 4) * depthFactor; - const mix = Math.min(0.34, 0.08 + t * (depth ? 0.16 : 0.12)); + const localDepthMultiplier = depth ? DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 1.15; + const depthFactor = depth > 0 ? 0.82 + edge * 0.46 : depth < 0 ? 0.56 + edge * 0.24 : 0.34 + edge * 0.08; + const strength = t * t * (6 + Math.abs(depth) * 4) * depthFactor * sourceIntensity * localDepthMultiplier; + const mix = Math.min(depth ? 0.78 : 0.32, (0.08 + t * (depth ? 0.18 : 0.12)) * sourceIntensity * (depth ? 1.85 : 1)); r = lerp(r, lc.r + strength, mix); g = lerp(g, lc.g + strength * 0.82, mix * 0.94); b = lerp(b, lc.b + strength * 0.74, mix * 0.88); + + const tintPower = Math.pow(t, 1.35) * sourceIntensity * (depth ? 0.17 * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 0.04) * (0.72 + edge * 0.72); + localTintR += lc.r * tintPower; + localTintG += lc.g * tintPower; + localTintB += lc.b * tintPower; + localTintWeight += tintPower; + localLift += Math.pow(t, 2.0) * sourceIntensity * (depth ? 7.5 * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 4.5) * (0.65 + edge * 0.75); } } @@ -6383,12 +6995,19 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { r = clamp(quantized.r, 0, 255); g = clamp(quantized.g, 0, 255); b = clamp(quantized.b, 0, 255); - return `#${Math.round(r).toString(16).padStart(2,'0')}${Math.round(g).toString(16).padStart(2,'0')}${Math.round(b).toString(16).padStart(2,'0')}`; + if (localTintWeight > 0) { + const inv = 1 / localTintWeight; + const tintMix = clamp(localTintWeight / (depth ? 3.2 : 8.0), 0, depth ? 0.64 : 0.18); + const lift = clamp(localLift, 0, depth ? 72 : 14); + r = lerp(clamp(r + lift, 0, 255), localTintR * inv, tintMix); + g = lerp(clamp(g + lift * 0.9, 0, 255), localTintG * inv, tintMix * 0.94); + b = lerp(clamp(b + lift * 0.82, 0, 255), localTintB * inv, tintMix * 0.9); + } + return `#${Math.round(clamp(r, 0, 255)).toString(16).padStart(2,'0')}${Math.round(clamp(g, 0, 255)).toString(16).padStart(2,'0')}${Math.round(clamp(b, 0, 255)).toString(16).padStart(2,'0')}`; } - function quantizePixelShade(base, shaded, phase, depth = 0) { // Pixel-art lighting: use 11 discrete bands (-5..+5). The old -2/+2 endpoints // map to the new -5/+5 endpoints, so maximum contrast is preserved. @@ -6487,23 +7106,27 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } function getDepthEdgeLightBoost(depth, x, y, width, pixels, depths, lights, phase, height = width) { - if (!depth || !pixels || !depths || !areNightLightsActive(phase) || !lights?.length) return 0; - let best = null, bestD = Infinity; - for (const light of lights) { - const d = Math.hypot(light.x - x, light.y - y); - if (d < bestD) { bestD = d; best = light; } - } - if (!best) return 0; - const facing = getDepthLightFacing(depth, x, y, width, pixels, depths, best.x, best.y, height); - if (!facing) return 0; - const outerEdge = collectDepthEdgeDirs(x, y, width, pixels, depths, depth, height).some(([dx, dy]) => { + if (!depth || !pixels || !depths || !(areNightLightsActive(phase) || hasCursorInspectionLight(lights)) || !lights?.length) return 0; + const edgeDirs = collectDepthEdgeDirs(x, y, width, pixels, depths, depth, height); + if (!edgeDirs.length) return 0; + const outerEdge = edgeDirs.some(([dx, dy]) => { const nx = x + dx, ny = y + dy; return nx < 0 || ny < 0 || nx >= width || ny >= height || !pixels[ny * width + nx]; }); - const distanceFalloff = 1 - clamp(bestD / Math.max(2.25, Math.max(width, height) * 0.44), 0, 1); - if (distanceFalloff <= 0) return 0; - const strength = outerEdge ? 0.64 : 1; - return facing * strength * distanceFalloff * distanceFalloff * (depth > 0 ? 20 : 14); + const reach = Math.max(2.25, Math.max(width, height) * 0.46); + let total = 0; + for (const light of lights) { + if (!light) continue; + const d = Math.hypot(light.x - x, light.y - y); + const distanceFalloff = 1 - clamp(d / reach, 0, 1); + if (distanceFalloff <= 0) continue; + const facing = getDepthLightFacing(depth, x, y, width, pixels, depths, light.x, light.y, height); + if (!facing) continue; + const sourceIntensity = clamp(Number(light.intensity ?? 1) || 1, 0.25, 1.85); + const edgeStrength = outerEdge ? 0.72 : 1; + total += facing * edgeStrength * distanceFalloff * distanceFalloff * sourceIntensity * (depth > 0 ? 20 : 14) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER; + } + return clamp(total, 0, depth > 0 ? 150 : 105); } function resizePixels(source, oldSize, newSize) { @@ -6590,19 +7213,29 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { function buildPalette() { - // Ten-column visual gradient: neutrals across the top, hue columns beneath. - // Keep all 62 legacy codes valid; the final two accents stay inside the grid. - const neutrals = ['#fffdf7','#f4ecd9','#e4d8c4','#c9baa2','#a69884','#817568','#625a54','#464248','#30303a','#151720']; - const hues = [356, 24, 50, 82, 122, 154, 188, 222, 266, 318]; - const lights = [82, 70, 58, 46, 34]; - const colors = [...neutrals]; - for (const light of lights) { - for (const hue of hues) { - const saturation = light > 76 ? 78 : light > 62 ? 76 : light > 48 ? 74 : 70; - colors.push(hslToHex(hue, saturation, light)); - } - } - colors.push('#ff7ab3', '#79f0ff'); + // 62 stable palette slots. The first 48 entries are arranged as four + // top-to-bottom ramps in the 4-column picker: neutral, warm, green/cyan, blue/purple. + // The remaining slots are accents; this avoids the old zig-zag gradient. + const colors = [ + '#fffdf7', '#fff0c7', '#e9fff2', '#eef4ff', + '#eee5d4', '#ffdca0', '#bff6d2', '#cbdcff', + '#d4c8b8', '#ffc071', '#87e79f', '#99b5f3', + '#b8a58f', '#ee9a55', '#57cf78', '#6f8fe2', + '#95806b', '#d6723d', '#35ad5a', '#4f68c5', + '#75614f', '#ad4f2b', '#248443', '#36469a', + '#5a4d43', '#84351f', '#1a6435', '#27306e', + '#454048', '#5e2418', '#124827', '#1b214d', + '#31333d', '#3f1712', '#0b311c', '#111733', + '#222632', '#2a0e0b', '#071f13', '#0a0d20', + '#151923', '#160806', '#04140c', '#050812', + '#090b10', '#090403', '#010804', '#01030a', + '#fff2f5', '#ffe55d', '#aef8ff', '#ead0ff', + '#ffb0c7', '#f2c438', '#60d6e8', '#c58df0', + '#ef6b88', '#e88c3e', '#37b9c9', '#9a56d9', + '#cf263f', '#c55a1c', '#1f7fa2', '#7132ad', + '#18c33a', '#76d322', '#22c78e', '#e247ae', + '#1e79e8', '#8148db' + ]; return COLOR_CODES.split('').map((code, index) => ({ code, color: colors[index] || '#14151c' })); } @@ -6693,6 +7326,1694 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } } + function artRows(rows, legend) { + return artRowsSized(rows, legend, 16, 16); + } + + function artRowsSized(rows, legend, width, height = width) { + const w = clampDimension(width, 16); + const h = clampDimension(height, w); + const out = blankPixels(w, h); + for (let y = 0; y < h; y++) { + const row = String(rows[y] || '').padEnd(w, '.').slice(0, w); + for (let x = 0; x < w; x++) { + const value = legend[row[x]]; + if (value) out[y * w + x] = value; + } + } + return out; + } + + function depthRows(rows) { + return depthRowsSized(rows, 16, 16); + } + + function depthRowsSized(rows, width, height = width) { + const w = clampDimension(width, 16); + const h = clampDimension(height, w); + const out = Array(w * h).fill(0); + for (let y = 0; y < h; y++) { + const row = String(rows[y] || '').padEnd(w, '.').slice(0, w); + for (let x = 0; x < w; x++) { + const ch = row[x]; + if (ch === '^' || ch === '+') out[y * w + x] = 1; + if (ch === 'v' || ch === '-') out[y * w + x] = -1; + } + } + return out; + } + + function drawCrescentTeaHouse() { + return artRows([ + '.......YY.......', + '......YWWY......', + '.....YWWY.......', + '......YY........', + '....NNNNNNN.....', + '...NMMMMMMMN....', + '..NMMYMMYMMN....', + '..MMMWMMWMMM....', + '..MMMMMMMMMM....', + '..MMMYDDYMMM....', + '..MMMDYYDMMM....', + '..MMMDYYDMMM....', + '...MMDDDDMM.....', + '....NNNNNN......', + '....MMDDMM......', + '...NNNNNNNN.....' + ], { + Y: '#ffe55d', W: '#fffdf7', N: '#31333d', M: '#95806b', D: '#222632' + }); + } + + function drawCrescentTeaHouseDepth() { + return depthRows([ + '.......^^.......', + '......^^^^......', + '.....^^^^.......', + '......^^........', + '....-------.....', + '...-^^^^^^^-....', + '..-^^^^^^^^-....', + '..-^^^^^^^^-....', + '..-^^^^^^^^-....', + '..-^^^--^^^-....', + '..-^^^--^^^-....', + '..-^^^--^^^-....', + '...--------.....', + '....------......', + '....--^^--......', + '...--------.....' + ]); + } + + function drawPrismSakura() { + return artRows([ + '................', + '......PP.P......', + '....PPRRPPP.....', + '...PRRPPPRP.....', + '..PPPRPPRPPP....', + '...PPRRRPP......', + '.....PRP........', + '......T.........', + '.....GTG........', + '....GTTTG.......', + '...GGTTTGG......', + '.....TTT........', + '.....TNT........', + '....NNNNN.......', + '...NNNNNNN......', + '................' + ], { + P: '#ffb0c7', R: '#ef6b88', T: '#75614f', G: '#57cf78', N: '#454048' + }); + } + + function drawPrismSakuraDepth() { + return depthRows([ + '................', + '......^^.^......', + '....^^^^^^^.....', + '...^^^^^^^^.....', + '..^^^^^^^^^^....', + '...^^^^^^^......', + '.....^^^........', + '......-.........', + '.....---........', + '....-----.......', + '...-------......', + '.....---........', + '.....---........', + '....-----.......', + '...-------......', + '................' + ]); + } + + function drawClockworkWhale() { + return artRows([ + '................', + '................', + '.......Y........', + '......YY........', + '...BBQQQQQB.....', + '..BQQQQQQQQB....', + '.BQQQWQQQWQB....', + '.BQQQQQQQQQB.Y..', + '..BQQQQQQQB.YY..', + '...BBBBBBB.YY...', + '....NNNNNN......', + '.....NNNN.......', + '................', + '................', + '................', + '................' + ], { + B: '#1f7fa2', Q: '#60d6e8', W: '#fffdf7', Y: '#ffe55d', N: '#5a4d43' + }); + } + + function drawClockworkWhaleDepth() { + return depthRows([ + '................', + '................', + '.......^........', + '......^^........', + '...^^^^^^^^.....', + '..^^^^^^^^^^....', + '.^^^^^^^^^^^....', + '.^^^^^^^^^^^.^..', + '..^^^^^^^^^.^^..', + '...-------.^^...', + '....------......', + '.....----.......', + '................', + '................', + '................', + '................' + ]); + } + + function drawLanternCatRight() { + return artRows([ + '................', + '................', + '.....N..N.......', + '....NSSSSN......', + '....SWSWS.......', + '....SSKSS.......', + '...OOOOOOO......', + '..OOOYYOOOY.....', + '..OOOOOOOYY.....', + '...O..OOO.......', + '..NN..N.N.......', + '................', + '................', + '................', + '................', + '................' + ], { + N: '#151923', S: '#ffc071', W: '#fffdf7', K: '#31333d', O: '#ee9a55', Y: '#ffe55d' + }); + } + + function drawCloudKoiRight() { + return artRows([ + '................', + '................', + '................', + '.....C..........', + '....CCC.........', + '...CQQQCCC......', + '..CQQQQQQQC.....', + '.CQQWQQQWQQC....', + '..CQQQQQQQC..P..', + '...CQQQCCC..PP..', + '.....C......P...', + '................', + '................', + '................', + '................', + '................' + ], { + C: '#aef8ff', Q: '#60d6e8', W: '#fffdf7', P: '#ffb0c7' + }); + } + + function drawPaperCraneRight() { + return artRows([ + '................', + '................', + '.......W........', + '......WWW.......', + '.....WWCWW......', + '....WWCCWWW.....', + '...WWCCNCCWW....', + '.....WCNW.......', + '......NN........', + '.....N..N.......', + '....N....N......', + '................', + '................', + '................', + '................', + '................' + ], { + W: '#eef4ff', C: '#cbdcff', N: '#151923' + }); + } + + function drawMoonLantern() { + return artRows([ + '.......Y........', + '......YYY.......', + '......YWY.......', + '.....YYYYY......', + '....NNNNNNN.....', + '...NMMMMMMMN....', + '...MBBMMBBM.....', + '..NMMYYYYMMN....', + '..MMMMYYMMMM....', + '..MMMDYYDMMM....', + '..MMMDDDDMMM....', + '..MMMRDDRRMM....', + '...MMRDDRM......', + '...MMMMMMMM.....', + '....MMMMMM......', + '....NNNNNN......' + ], { + Y: '#ffe99a', W: '#fffdf7', N: '#3f3e42', M: '#806f5f', B: '#8aa7ef', D: '#191a22', R: '#de4d6d' + }); + } + + function drawMoonLanternDepth() { + return depthRows([ + '.......^........', + '......^^^.......', + '......^^^.......', + '.....^^^^^......', + '....-------.....', + '...-^^^^^^^-....', + '...-^^^^^^-.....', + '..-^^^^^^^^-....', + '..-^^^^^^^^-....', + '..-^^^--^^^-....', + '..-^^^--^^^-....', + '..-^^^--^^^-....', + '...-^^^^^^-.....', + '...--------.....', + '....------......', + '....------......' + ]); + } + + function drawGlassFern() { + return artRows([ + '................', + '.......G........', + '......GEG.......', + '.....GEEEG......', + '..C..GEGEG..C...', + '...C.GEGEG.C....', + '....CGEEEGC.....', + '.....GEGEG......', + '....GEEEEE......', + '...GEEEGEG......', + '..GEGEGEGEG.....', + '....TTTTT.......', + '....TNNNT.......', + '...TTNNNTT......', + '...NNNNNNN......', + '................' + ], { + G: '#45b96b', E: '#aaf0ce', C: '#bff7ff', T: '#674d0b', N: '#806f5f' + }); + } + + function drawStarTotem() { + return artRows([ + '.......Y........', + '......YYY.......', + '.......Y........', + '.....PYPYP......', + '....PPPPPPP.....', + '.....PBPBP......', + '......BBB.......', + '.....BNNNB......', + '.....NNYNN......', + '.....NNYNN......', + '....NNNYNNN.....', + '....NNNNNNN.....', + '.....NNNNN......', + '....MMMMMMM.....', + '...MMMMMMMMM....', + '................' + ], { + Y: '#ffe99a', P: '#c79bed', B: '#8aa7ef', N: '#5e554e', M: '#3f3e42' + }); + } + + function drawStarTotemDepth() { + return depthRows([ + '.......^........', + '......^^^.......', + '.......^........', + '.....^^^^^......', + '....^^^^^^^.....', + '.....^^^^^......', + '......---.......', + '.....-^^^-......', + '.....-^^^-......', + '.....-^^^-......', + '....-^^^^^-.....', + '....-------.....', + '.....-----......', + '....-------.....', + '...---------....', + '................' + ]); + } + + function drawCoralSkiff() { + return artRows([ + '................', + '........N.......', + '........N.......', + '.......WYN......', + '......WWYN......', + '.....WWYYN......', + '....WYYYYN......', + '.......NNN......', + '...R...NN...R...', + '..RRRMMMMMMRR...', + '..RMMMMMMMMMR...', + '...MMMMMMMM.....', + '....DDDDDD......', + '.....DDDD.......', + '................', + '................' + ], { + N: '#5e554e', W: '#fffdf7', Y: '#ffe99a', R: '#fa6e5a', M: '#a95a32', D: '#3d2218' + }); + } + + function drawCoralSkiffDepth() { + return depthRows([ + '................', + '........^.......', + '........^.......', + '.......^^.......', + '......^^^.......', + '.....^^^^.......', + '....^^^^^.......', + '.......---......', + '...^...--...^...', + '..^^^-------^...', + '..-----------...', + '...--------.....', + '....------......', + '.....----.......', + '................', + '................' + ]); + } + + function drawLanternWalkerRight() { + return artRows([ + '................', + '................', + '......NNN.......', + '.....NSSSN......', + '.....NSKSN......', + '......SSS.......', + '.....RRRRR......', + '....RRRRYRY.....', + '....RRRR.YY.....', + '.....BBBR.......', + '.....B.BB.......', + '.....B..B.......', + '....NN..NN......', + '................', + '................', + '................' + ], { + N: '#191a22', S: '#ffd59c', K: '#3f3e42', R: '#de4d6d', Y: '#ffe99a', B: '#354ca3' + }); + } + + function drawSproutFoxRight() { + return artRows([ + '................', + '................', + '................', + '.........G......', + '........GEG.....', + '....OOO..G......', + '...OWWOO........', + '..OOOWWOOO......', + '..ODODDOWOO.....', + '..OOOOOOO.......', + '...O..O.O.......', + '..NN..NNN.......', + '................', + '................', + '................', + '................' + ], { + O: '#ffad63', W: '#fffdf7', D: '#a95a32', N: '#3d2218', G: '#45b96b', E: '#aaf0ce' + }); + } + + function drawAzureMinnowRight() { + return artRows([ + '................', + '................', + '................', + '................', + '................', + '.....CQQQC......', + '...CCQQQQQC.....', + '..CQQQWBQQQCC...', + '...CCQQQQQC.....', + '.....CQQQC......', + '.......C........', + '................', + '................', + '................', + '................', + '................' + ], { + C: '#bff7ff', Q: '#35b9d0', W: '#fffdf7', B: '#141942' + }); + } + + function drawVioletMothRight() { + return artRows([ + '................', + '................', + '.......P........', + '.....PPBPP......', + '....PPBKBPP.....', + '...PPBBKBBPP....', + '.....PBKBP......', + '......NKN.......', + '.....I.N.I......', + '....I.....I.....', + '................', + '................', + '................', + '................', + '................', + '................' + ], { + P: '#c79bed', B: '#9e65d8', K: '#4b206f', N: '#191a22', I: '#ead1ff' + }); + } + + + function drawWildflowerPatch() { + return artRowsSized([ + '........', + '.gPgYg..', + 'gWgBgPg.', + 'ggggggg.', + 'gYgPgWg.', + 'ggggggg.', + '.GgGgG..', + '..NN....' + ], { + g: '#69c96f', G: '#3f9a46', P: '#f38db6', Y: '#f2dd59', W: '#fffdf7', B: '#77d7ff', N: '#7e5a3e' + }, 8, 8); + } + + function drawRiverStones() { + return artRowsSized([ + '........', + '..bb....', + '.bssb...', + 'bsmmsb..', + '.bssssb.', + '..bgGb..', + '...bb...', + '........' + ], { + b: '#7ba9cf', s: '#c7cdd1', m: '#8d959c', g: '#86d06c', G: '#4d9d45' + }, 8, 8); + } + + function drawMapleCanopy() { + return artRows([ + '......OO........', + '....OORROO......', + '...OORRYYOO.....', + '..OORRYYYYOO....', + '..ORRYYGGYYO....', + '.ORRYYGGGGYYO...', + '.ORYYGGGGGGYO...', + '.OYYGGWWGGGYO...', + '..OYYGGGGGGYO...', + '..OOYGGGGYYO....', + '...OOYYYYYO.....', + '.....TTTT.......', + '....TTTTTT......', + '....TTTTTT......', + '...NNNNNNNN.....', + '................' + ], { + O: '#f08c4a', R: '#de4d5d', Y: '#f2d255', G: '#6fbf5e', W: '#fff1d6', T: '#7a4f2c', N: '#4f3828' + }); + } + + function drawMapleCanopyDepth() { + return depthRows([ + '......^^........', + '....^^^^^^......', + '...^^^^^^^^.....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '.^^^^^^^^^^^^...', + '.^^^^^^^^^^^^...', + '.^^^^^^^^^^^^...', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '...^^^^^^^^.....', + '.....----.......', + '....------......', + '....------......', + '...--------.....', + '................' + ]); + } + + function drawMistyFalls() { + return artRows([ + '....GGGGGG......', + '...GSSSSSSG.....', + '..GSSCCCCSSG....', + '..GSCWWWWCSG....', + '..GSCWBBWCSG....', + '..GSCWBBWCSG....', + '..GSCWBBWCSG....', + '..GSCWBBWCSG....', + '..GSCWWWWCSG....', + '...GCWWWWCG.....', + '....CCWWCC......', + '....WWWWWW......', + '...WTTTTTTW.....', + '..WTTTTTTTTW....', + '..WWWWWWWWWW....', + '................' + ], { + G: '#63b867', S: '#747f88', C: '#b7f2ff', W: '#e8fbff', B: '#63c8ef', T: '#7bb7d6' + }); + } + + function drawMistyFallsDepth() { + return depthRows([ + '....^^^^^^......', + '...^^^^^^^^.....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '..^^^^^^^^^^....', + '...^^^^^^^^.....', + '....^^^^^^......', + '....------......', + '...--------.....', + '..----------....', + '..----------....', + '................' + ]); + } + + function drawLotusPond() { + return artRowsSized([ + '................', + '....gG....gG....', + '..gGGGg..gGGg...', + '..GGPPGGGGPWG...', + '.GGPPWPPGGPPGG..', + '.GGBBBBBBBBBBG..', + '.GBBBBBBBBBBBG..', + '.GGBBBBBBBBBGG..', + '..GGGBBBBGGG....', + '...GGGGGGGG.....', + '....n....n......', + '................' + ], { + g: '#7dd27c', G: '#4ea759', P: '#f59bc4', W: '#fff8f3', B: '#5bc3e7', n: '#56713d' + }, 16, 12); + } + + function drawLotusPondDepth() { + return depthRowsSized([ + '................', + '....^^....^^....', + '..^^^^^..^^^^...', + '..^^^^^^^^^^^...', + '.^^^^^^^^^^^^^..', + '.^^^^vvvv^^^^^..', + '.^^^vvvvvvv^^^..', + '.^^^^vvvv^^^^^..', + '..^^^^^^^^^^....', + '...^^^^^^^^.....', + '....-....-......', + '................' + ], 16, 12); + } + + function drawMossyArch() { + return artRows([ + '................', + '.....GGGG.......', + '....GSSSSG......', + '...GSSSSSSG.....', + '..GSSGGGGSSG....', + '..GSG....GSG....', + '..GSG....GSG....', + '..GSG....GSG....', + '..GSG....GSG....', + '..GSG....GSG....', + '..GSSGGGGSSG....', + '..GGGLLLLGGG....', + '....LLLLLL......', + '...LLL..LLL.....', + '..NNNN..NNNN....', + '................' + ], { + G: '#58b96c', S: '#8e8f92', L: '#c1d57f', N: '#556046' + }); + } + + function drawMossyArchDepth() { + return depthRows([ + '................', + '.....^^^^.......', + '....^^^^^^......', + '...^^^^^^^^.....', + '..^^^^..^^^^....', + '..^^^....^^^....', + '..^^^....^^^....', + '..^^^....^^^....', + '..^^^....^^^....', + '..^^^....^^^....', + '..^^^^..^^^^....', + '..^^^^^^^^^^....', + '....------......', + '...---..---.....', + '..----..----....', + '................' + ]); + } + + function drawSunflowerGrove() { + return artRowsSized([ + '............', + '.yy..yy..yy.', + 'yOOy.OOy.yOO', + 'yOWOyOWOyOWO', + '.yOO..OO..OO', + '..GG..GG..GG', + '..GgGGGgGGgG', + '.gGGGGGGGGGg', + '.gGgGgGgGgGg', + '..G..G..G..G', + '..T..T..T..T', + '..T..T..T..T', + '.TTTTTTTTTT.', + '.GgGgGgGgGg.', + '..NNNNNNNN..', + '............' + ], { + y: '#f6df56', O: '#f09a3c', W: '#6f4b2a', G: '#64bd62', g: '#8fe17c', T: '#6b8f3f', N: '#7b5a41' + }, 12, 16); + } + + function drawReedBed() { + return artRowsSized([ + '............', + '....rrr.....', + '..r.rrR.r...', + '..RrrrRrR...', + '..RrrrRrr...', + '..RrrrRrr...', + '.wwwwwccww..', + 'wcccccbbccw.', + 'wcbbbbbbbcw.', + '.wwccccccw..', + '..GGGGGG....', + '...NNNN.....' + ], { + r: '#b8d96f', R: '#7fb04a', w: '#d8f5ff', c: '#7bd1e6', b: '#4ca7d3', G: '#67bf68', N: '#7b6347' + }, 12, 12); + } + + function drawFireflySwirlRight() { + return artRowsSized([ + '........', + '...Y....', + '..YGY...', + '.YGWGY..', + '..YGY...', + '...Y....', + '..S.S...', + '........' + ], { + Y: '#fff08a', G: '#7dcf56', W: '#fffdf7', S: '#6bb2ff' + }, 8, 8); + } + + function drawMeadowHareRight() { + return artRowsSized([ + '............', + '....EE......', + '...EFFE.....', + '...EFFFE....', + '..EFFWWFE...', + '..EFFGGFE...', + '..EFFFFFE...', + '..EFFFFF....', + '...F.FF.....', + '..NN.N.N....', + '............', + '............' + ], { + E: '#d8b08a', F: '#c79267', W: '#fff4ec', G: '#5a4333', N: '#6a574d' + }, 12, 12); + } + + function drawBrookTurtleRight() { + return artRowsSized([ + '............', + '............', + '....GGG.....', + '..GYYYYG....', + '.GYYWWYYG...', + '.GYYGGYYGG..', + '..GYYYYYG...', + '...GGGG..B..', + '....N..NBB..', + '............' + ], { + G: '#5ebc68', Y: '#a3d56c', W: '#fff5d8', B: '#87dfff', N: '#516046' + }, 12, 10); + } + + function drawLeafSparrowRight() { + return artRowsSized([ + '........', + '...G....', + '..GGW...', + '.GWWWW..', + '.WWCCW..', + '..WCCG..', + '...N.N..', + '........' + ], { + G: '#70bf5d', W: '#eef9ec', C: '#8edc9e', N: '#5b4e43' + }, 8, 8); + } + + + function drawRedPandaRight() { + return artRowsSized([ + '............', + '...EE..EE...', + '..EOEOEOOE..', + '..EOOWWOOE..', + '..EOOGGOOE..', + '...ORRRRO...', + '..ORRRRRRO..', + '..ORRWWRRO..', + '..ORRRRRRO..', + '...R..R.R...', + '..N...N.N...', + '............' + ], { + E: '#f4efe9', O: '#d87436', W: '#fff7ef', G: '#4b3428', R: '#c95f2c', N: '#7b624e' + }, 12, 12); + } + + function drawRiverOtterRight() { + return artRowsSized([ + '..............', + '...BBB........', + '..BCCCCBB.....', + '.BCCWWCCCBB...', + '.BCCGGCCCCCB..', + '.BCCCCCCCCCB..', + '..BCCCC..CC...', + '...NNN...N....' + ], { + B: '#7a5b44', C: '#9b7658', W: '#efe1d2', G: '#3a2b24', N: '#5c534f' + }, 14, 8); + } + + function drawAmberDeerRight() { + return artRowsSized([ + '..............', + '.....A........', + '....A.A.......', + '...AAWAA......', + '...AWWWAA.....', + '..AAWGGWAA....', + '..AWWWWWWA....', + '..AWWWWWWAA...', + '..AWWWWWWWA...', + '...AWWWWWA....', + '...A.AA.A.....', + '..N..AA..N.....', + '..N..AA..N.....', + '..............' + ], { + A: '#d5954c', W: '#efc27d', G: '#3f2c21', N: '#71584a' + }, 14, 14); + } + + function drawForestOwlRight() { + return artRowsSized([ + '..........', + '...EE.E...', + '..EBBBBE..', + '..BWWWWB..', + '.BWWGGWWB.', + '.BWWWWWWB.', + '.BBWWWWBB.', + '..BYYYYB..', + '..BYYYYB..', + '..N.BB.N..', + '...N..N...', + '..........' + ], { + E: '#c9a05b', B: '#7b5d40', W: '#f3e9d7', G: '#46352a', Y: '#b99658', N: '#6c5b4b' + }, 10, 12); + } + + function drawKingfisherRight() { + return artRowsSized([ + '..........', + '....C.....', + '...CCCW...', + '..CBWWWWY.', + '.CCBWWGGY.', + '..CBBBBY..', + '....N.N...', + '..........' + ], { + C: '#2b91c9', B: '#1d5d8e', W: '#f8f5ed', G: '#44403b', Y: '#e6a74c', N: '#5a514a' + }, 10, 8); + } + + function drawPondDuckRight() { + return artRowsSized([ + '............', + '............', + '....GG......', + '...GWWWW....', + '..GWWWGGGY...', + '..GWWWWWWWY..', + '..GGGWWWWY...', + '....W..W.....', + '...N....N....', + '............' + ], { + G: '#4f8f46', W: '#efe6d8', Y: '#e7aa55', N: '#6a5e4f' + }, 12, 10); + } + + function drawHeronRight() { + return artRowsSized([ + '..........', + '.....W....', + '....WWW...', + '.....WWY..', + '....WWWW..', + '...WWWGG..', + '...WWWWW..', + '....WWWW..', + '.....WW...', + '.....WW...', + '.....WW...', + '.....WW...', + '....N..N..', + '....N..N..', + '..........', + '..........' + ], { + W: '#f4f6f7', Y: '#d5b073', G: '#49525e', N: '#756b5f' + }, 10, 16); + } + + function drawSunsetKoiRight() { + return artRowsSized([ + '............', + '............', + '....O.......', + '..OORRRROO..', + '.ORRWWWRRRY.', + '..ORRRRRROO.', + '....O.O.....', + '............' + ], { + O: '#f39a44', R: '#ef635b', W: '#fff6ea', Y: '#f4c14a' + }, 12, 8); + } + + function drawSilverTroutRight() { + return artRowsSized([ + '............', + '....S.......', + '..SSCCCSS...', + '.SCCWWCCCB..', + '..SSCCCSS...', + '....S.......' + ], { + S: '#8aa6bf', C: '#c6d6e3', W: '#f5fbff', B: '#3f596b' + }, 12, 6); + } + + function drawButterflyFishRight() { + return artRowsSized([ + '..........', + '....Y.....', + '..YYWWYY..', + '.YWWBBWYY.', + '..YYWWYYK.', + '....Y.....', + '..........', + '..........' + ], { + Y: '#f3d454', W: '#fff7e6', B: '#263347', K: '#f09a42' + }, 10, 8); + } + + function drawTownGardenerRight() { + return artRowsSized([ + '............', + '....NN......', + '...NSSN.....', + '...SWWS.....', + '...SSGSS....', + '....GRR.....', + '...GRRRR....', + '...GRRRR....', + '....BBB.....', + '...BB.BB....', + '...B...B....', + '..NN...NN...', + '............', + '............', + '............', + '............' + ], { + N: '#27252b', S: '#f0c099', W: '#fff6ef', G: '#4b6f3d', R: '#89c96a', B: '#6e4f3a' + }, 12, 16); + } + + function drawLanternCourierRight() { + return artRowsSized([ + '............', + '....NN......', + '...NSSN.....', + '...SWWS.....', + '...SSSS.....', + '....RRRR....', + '...RRRRRY...', + '...RRRR.Y...', + '....BBBB....', + '...BB.BB....', + '...B...B....', + '..NN...NN...', + '............', + '............', + '............', + '............' + ], { + N: '#232531', S: '#f2c7a3', W: '#fff7ef', R: '#c85252', Y: '#ffe99a', B: '#3f5078' + }, 12, 16); + } + + function drawPlazaMusicianRight() { + return artRowsSized([ + '..............', + '.....NN.......', + '....NSSN......', + '....SWWS......', + '....SSSS......', + '...PPPPPP.....', + '...PBBQQQ.....', + '...PBBQQQ.....', + '....QTTQ......', + '...QT..TQ.....', + '...N....N.....', + '..NN....NN....', + '..............', + '..............', + '..............', + '..............' + ], { + N: '#2b2530', S: '#f0c39b', W: '#fff7ee', P: '#8a5ab1', B: '#5a3f26', Q: '#d8a45a', T: '#6e5749' + }, 14, 16); + } + + function drawBridgeMechanicRight() { + return artRowsSized([ + '..............', + '.....NN.......', + '....NSSN......', + '....SWWS......', + '....SSSS......', + '...CCCCCC.....', + '...CCKCCC.....', + '...CCKCCY.....', + '....BBBB......', + '...BB.BB......', + '...B...B......', + '..NN...NN.....', + '..............', + '..............', + '..............', + '..............' + ], { + N: '#272934', S: '#efc39e', W: '#fff8f1', C: '#4f8ca8', K: '#293645', Y: '#e8b65d', B: '#6b5241' + }, 14, 16); + } + + function drawHarborClocktower() { + const w = 32, h = 40, p = blankPixels(w, h); + rect(p, w, 12, 6, 8, 26, '#b08867'); + rect(p, w, 11, 30, 10, 7, '#9a7658'); + rect(p, w, 10, 35, 12, 3, '#6f5746'); + rect(p, w, 13, 1, 6, 5, '#8f5c4d'); + for (let y = 0; y < 3; y++) for (let x = 13 - y; x <= 18 + y; x++) px(p, w, x, y, y === 0 ? '#6d4650' : '#835764'); + rect(p, w, 13, 10, 6, 6, '#f0eadb'); + rect(p, w, 14, 11, 4, 4, '#fffdf7'); + px(p, w, 15, 13, '#4f535f'); px(p, w, 16, 13, '#4f535f'); px(p, w, 16, 12, '#4f535f'); + rect(p, w, 13, 19, 2, 3, '#ffe99a'); rect(p, w, 17, 19, 2, 3, '#ffe99a'); + rect(p, w, 14, 25, 2, 3, '#ffe99a'); rect(p, w, 17, 25, 2, 3, '#ffe99a'); + rect(p, w, 14, 32, 4, 5, '#3b3348'); + rect(p, w, 8, 37, 16, 2, '#544539'); + return p; + } + + function drawGlassGreenhouse() { + const w = 28, h = 20, p = blankPixels(w, h); + for (let y = 2; y <= 6; y++) { + const inset = 10 - y; + for (let x = 6 + inset; x < w - 6 - inset; x++) px(p, w, x, y, y % 2 ? '#dff7ff' : '#b7ecff'); + } + rect(p, w, 4, 7, 20, 10, '#dff7ff'); + for (let x = 4; x <= 23; x += 4) rect(p, w, x, 7, 1, 10, '#7aa6a0'); + for (let y = 7; y <= 16; y += 3) rect(p, w, 4, y, 20, 1, '#7aa6a0'); + rect(p, w, 12, 12, 4, 5, '#fff4c2'); + rect(p, w, 11, 17, 6, 2, '#6d5442'); + rect(p, w, 7, 14, 3, 2, '#6fc26b'); rect(p, w, 18, 14, 3, 2, '#6fc26b'); + rect(p, w, 8, 12, 2, 2, '#8adf73'); rect(p, w, 18, 11, 2, 2, '#8adf73'); + return p; + } + + function drawSteamWorkshop() { + const w = 40, h = 24, p = blankPixels(w, h); + rect(p, w, 4, 8, 32, 12, '#8b6a58'); + rect(p, w, 6, 10, 10, 8, '#a27d66'); + rect(p, w, 18, 10, 16, 8, '#9a735d'); + rect(p, w, 6, 6, 13, 2, '#5f4d43'); + rect(p, w, 18, 5, 17, 3, '#4d4e55'); + rect(p, w, 9, 11, 3, 3, '#ffd979'); rect(p, w, 14, 11, 3, 3, '#ffd979'); + rect(p, w, 24, 11, 3, 3, '#ffd979'); rect(p, w, 29, 11, 3, 3, '#ffd979'); + rect(p, w, 18, 15, 6, 5, '#3d3340'); + rect(p, w, 10, 0, 4, 8, '#6d5548'); + rect(p, w, 27, 0, 5, 8, '#5b5b62'); + rect(p, w, 11, 0, 2, 3, '#9fc8d4'); rect(p, w, 28, 0, 3, 3, '#9fc8d4'); + rect(p, w, 2, 20, 36, 3, '#5b4940'); + return p; + } + + function drawCanalBridge() { + const w = 48, h = 16, p = blankPixels(w, h); + rect(p, w, 3, 12, 42, 2, '#6a5446'); + rect(p, w, 6, 10, 36, 2, '#8e715d'); + rect(p, w, 8, 8, 32, 2, '#b08c72'); + for (let i = 0; i < 8; i++) { + rect(p, w, 10 + i * 4, 5, 1, 5, '#d9c4a8'); + rect(p, w, 11 + i * 4, 4, 2, 1, '#d9c4a8'); + } + for (let y = 10; y <= 13; y++) { + const inset = Math.abs(11 - y) * 2; + rect(p, w, 16 + inset, y, 16 - inset * 2, 1, '#3a78a6'); + } + return p; + } + + function drawGrandFountain() { + return artRowsSized([ + '........................', + '...........WW...........', + '..........WWWW..........', + '.........WBBBBW.........', + '..........WBBW..........', + '.........WWBBWW.........', + '........WBBBBBBW........', + '.........WBBBBW.........', + '.......SSSWWWWSSS.......', + '......SCCCSSSSCCCS......', + '.....SCCBBBBBBCCCS......', + '....SCCBBBBBBBBCCCS.....', + '....SCBBBBWWBBBBCCS.....', + '....SCCBBBBBBBBCCCS.....', + '.....SCCBBBBBBCCCS......', + '......SCCCSSSSCCS.......', + '.......SSSSSSSSS........', + '.......NNNNNNNNN........', + '......NNNSSSSNNNN.......', + '.....NNSSSSSSSSNN.......', + '....NNNNNNNNNNNNNN......', + '....NNBBBBBBBBBBNN......', + '.....NNNNNNNNNNNN.......', + '........................' + ], { W: '#eafcff', B: '#7ed3ef', C: '#bff2ff', S: '#9b8b7d', N: '#6f6258' }, 24, 24); + } + + function drawRocketMonument() { + return artRowsSized([ + '.........RR.........', + '........RWWR........', + '.......RWWWWR.......', + '.......RWWWWR.......', + '......RRWGGWRR......', + '......RWWGGWWR......', + '......RWWWWWWR......', + '......RWWBBWWR......', + '......RWWBBWWR......', + '......RWWWWWWR......', + '.....RRWWWWWWRR.....', + '.....RWWWWWWWWR.....', + '.....RWWWWWWWWR.....', + '.....RWWRRWWWWR.....', + '.....RWWRRWWWWR.....', + '.....RWWWWWWWWR.....', + '.....RRWWWWWWRR.....', + '......RWWWWWWR......', + '.....RRWWWWWWRR.....', + '....RRRWWWWWWRRR....', + '....RWWWWWWWWWWR....', + '....RWWWWWWWWWWR....', + '....RRRRWWWWRRRR....', + '.....BBBWWWWBBB.....', + '.....BBYYYYYYBB.....', + '.....BBOOYYOOBB.....', + '.....BBYYYYYYBB.....', + '.....BBBBBBBBBB.....', + '......NNN..NNN......', + '.....NNN....NNN.....', + '.....NN......NN.....', + '....................' + ], { R: '#d95f5f', W: '#f3f4f8', G: '#86d9ff', B: '#776154', Y: '#ffd979', O: '#ff8a5c', N: '#514740' }, 20, 32); + } + + function drawArcadeBooth() { + const w = 24, h = 20, p = blankPixels(w,h); + rect(p,w,3,5,18,11,'#5a3d70'); + rect(p,w,4,3,16,3,'#8d5cc2'); + rect(p,w,6,7,12,6,'#161c2f'); + rect(p,w,7,8,10,4,'#2bb4ff'); + rect(p,w,9,14,6,2,'#3b3348'); + rect(p,w,8,16,8,2,'#6f5142'); + px(p,w,10,4,'#ffe66a'); px(p,w,13,4,'#ff8cc8'); px(p,w,16,4,'#8ef2ff'); + rect(p,w,2,18,20,1,'#47352b'); + return p; + } + + function drawChessKnightStatue() { + return artRowsSized([ + '................', + '......GG........', + '.....GWWG.......', + '....GWWWWG......', + '....GWWBWWG.....', + '....GWWWWGG.....', + '...GWWWWWWG.....', + '...GWWGWWWG.....', + '...GWWGGWWG.....', + '....GWWWWG......', + '....GWWWWG......', + '...GGWWWWGG.....', + '..GGGWWWWGGG....', + '..GWWWWWWWWG....', + '..GGGGWWGGGG....', + '....NNWWNN......', + '...NNNWWNNN.....', + '..NNNNNNNNNN....', + '..NNNSSSSNNN....', + '..NNSSSSSSNN....', + '.NNNNNNNNNNNN...', + '.NBBBBBBBBBBN...', + '..NNNNNNNNNN....', + '................' + ], { G: '#c8cfda', W: '#f8fbff', B: '#8fa1bd', N: '#706257', S: '#9e9184' }, 16, 24); + } + + function drawDesertTrain() { + const w = 44, h = 16, p = blankPixels(w, h); + rect(p, w, 4, 8, 7, 4, '#8b6b58'); + rect(p, w, 11, 7, 13, 5, '#b27d4c'); + rect(p, w, 24, 7, 13, 5, '#c58a54'); + rect(p, w, 37, 8, 5, 4, '#915f40'); + rect(p, w, 5, 5, 4, 3, '#645a5f'); + rect(p, w, 12, 5, 10, 2, '#744f45'); + rect(p, w, 25, 5, 10, 2, '#744f45'); + rect(p, w, 13, 8, 3, 2, '#ffd979'); + rect(p, w, 18, 8, 3, 2, '#ffd979'); + rect(p, w, 27, 8, 3, 2, '#ffd979'); + rect(p, w, 32, 8, 3, 2, '#ffd979'); + rect(p, w, 7, 7, 2, 2, '#ffe7a4'); + rect(p, w, 6, 3, 2, 2, '#d9e4e8'); + rect(p, w, 8, 2, 2, 1, '#eef4f7'); + rect(p, w, 11, 12, 24, 1, '#684f41'); + for (const x of [6, 15, 21, 29, 35, 40]) { + circle(p, w, x, 13, 2, '#302a2a'); + px(p, w, x, 13, '#8f857b'); + } + rect(p, w, 0, 15, 44, 1, '#5f4a3f'); + rect(p, w, 2, 14, 40, 1, '#b98d5d'); + return p; + } + + function drawTeaRobotRight() { + return artRowsSized([ + '............', + '.....CC.....', + '....CWWC....', + '....CWGWC...', + '....CWWWC...', + '.....MMM....', + '....MKKKM...', + '....MKYKMY..', + '.....MKKM...', + '....B.BBB...', + '...BB..BB...', + '...N....N...', + '............', + '............', + '............', + '............' + ], {C:'#8fd3e8',W:'#f5fbff',G:'#4b525d',M:'#c38f61',K:'#6d584b',Y:'#9be6ff',B:'#86736b',N:'#4d4c55'}, 12, 16); + } + + function drawBalloonVendorRight() { + return artRowsSized([ + '..P..Y..B.....', + '..P..Y..B.....', + '..PP.YY.BB....', + '.....S........', + '.....N........', + '....NSSN......', + '....SWWS......', + '....SSSS......', + '...RRRRRR.....', + '...RRRRRR.....', + '....BB.BB.....', + '...BB...B.....', + '...N....N.....', + '..NN....NN....', + '..............', + '..............', + '..............', + '..............' + ], {P:'#f38db6',Y:'#f3d454',B:'#8ecfff',S:'#7c5b48',N:'#27252b',W:'#fff6ef',R:'#d76b52'}, 14, 18); + } + + function drawJellyCometRight() { + return artRowsSized([ + '............', + '......C.....', + '....CCCCC...', + '..CCWWWWWCC.', + '..CWWGGGWWC.', + '..CWWWWWWWC.', + '...CWWWWWC..', + '....CCCCC...', + '.....T.T....', + '....T...T...', + '............', + '............' + ], {C:'#98c3ff',W:'#dff0ff',G:'#5b5d8c',T:'#b7d8ff'}, 12, 12); + } + + function drawCourierBikeRight() { + return artRowsSized([ + '................', + '......NN........', + '.....NSSN.......', + '.....SWWS.......', + '.....SSSS.......', + '....RRRRRR......', + '....RBBB.R......', + '....R.BBBR......', + '.....K.K........', + '...OO...OO......', + '..OOWOOOOWO.....', + '...OO...OO......' + ], {N:'#252732',S:'#f0c39b',W:'#fff7ef',R:'#4da0d8',B:'#6b4d39',K:'#4a4f58',O:'#d9dce5'}, 16, 12); + } + + function drawGreatCedar() { + return artRowsSized([ + '..........GGGG..........', + '........GGLLLLGG........', + '......GGLLLWWLLGG.......', + '.....GLLLWWWWLLLGG......', + '....GLLLWWGGWWLLLG......', + '...GLLLWWGGGGWWLLLG.....', + '...GLLWWGGGGGGWWLLG.....', + '..GLLWWGGDDDDGGWWLLG....', + '..GLLWGGDDDDDDGGWLLG....', + '.GLLWWGDDGGGGDDGWWLLG...', + '.GLLWGGDGGGGGGDGGWLLG...', + '.GLWWGGGGGGGGGGGGWWLG...', + '.GLWGGGGGGGGGGGGGGWLG...', + 'GLWWGGGGGGGGGGGGGGWWLG..', + 'GLWGGGGGGGGGGGGGGGGWLG..', + 'GLWGGGGGGGGGGGGGGGGWLG..', + '.GLWGGGGGGGGGGGGGGWLG...', + '.GLWWGGGGGGGGGGGGWWLG...', + '.GLLWGGGGGGGGGGGGWLLG...', + '.GLLWWGGGGGGGGGGWWLLG...', + '..GLLWGGGGGGGGGGWLLG....', + '..GLLWWGGGGGGGGWWLLG....', + '...GLLWWGGGGGGWWLLG.....', + '....GLLLWWGGWWLLLG......', + '.....GLLLWWWWLLLG.......', + '......GLLLTTLLLG........', + '.......GLLTTLLG.........', + '.......TTTTTTTT.........', + '......TTTTTTTTTT........', + '......TTTTRRTTTT........', + '.....NNNNNNNNNNNN.......', + '........................' + ], { + G: '#4ca35b', L: '#71c872', W: '#b7f2b6', D: '#3a7b44', T: '#7a4f30', R: '#c79664', N: '#4f3828' + }, 24, 32); + } + + function drawGreatCedarDepth() { + return depthRowsSized([ + '..........^^^^..........', + '........^^^^^^^^........', + '......^^^^^^^^^^^.......', + '.....^^^^^^^^^^^^^^......', + '....^^^^^^^^^^^^^^^......', + '...^^^^^^^^^^^^^^^^^.....', + '...^^^^^^^^^^^^^^^^^.....', + '..^^^^^^^^^^^^^^^^^^^....', + '..^^^^^^^^^^^^^^^^^^^....', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '^^^^^^^^^^^^^^^^^^^^^^^..', + '^^^^^^^^^^^^^^^^^^^^^^^..', + '^^^^^^^^^^^^^^^^^^^^^^^..', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^^^^....', + '..^^^^^^^^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^^^^.....', + '....^^^^^^^^^^^^^^^......', + '.....^^^^^^^^^^^^^.......', + '......^^^^----^^^........', + '.......^^^----^^.........', + '.......--------.........', + '......----------........', + '......----------........', + '.....------------.......', + '........................' + ], 24, 32); + } + + function drawMoonfallCascade() { + return artRowsSized([ + '......GGGGGGGGGGGG......', + '....GGSSSSSSSSSSSSGG....', + '...GSSSCCWWWWCCSSSSG....', + '..GSSCCWWWWWWWWCCSSSG...', + '..GSCWWWWBBBBWWWWCSG...', + '.GSCWWWBBBBBBBBWWCSG...', + '.GSWWWBBBTTTTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBTWWTBBBWWSG...', + '.GSWWWBBBWWWWBBBWWSG...', + '.GSWWWWWWWWWWWWWWWSG...', + '..GSWWWWCCWWCCWWWSG....', + '..GSWWWCCWWWWCCWWSG....', + '...GSWWWWWWWWWWWSG.....', + '....GSWWWWWWWWWWSG.....', + '....GGWWWWWWWWWWGG.....', + '.....WWTTTTTTTTWW......', + '....WWTTTTTTTTTTWW.....', + '....WTTTTTTTTTTTTW.....', + '....WWWWWWWWWWWWWW.....', + '....BBBBBBBBBBBBBB.....', + '........................' + ], { + G: '#58a55d', S: '#6f7e87', C: '#c7f3ff', W: '#eafcff', B: '#58c0ea', T: '#7aaec8' + }, 24, 32); + } + + function drawMoonfallCascadeDepth() { + return depthRowsSized([ + '......^^^^^^^^^^^^......', + '....^^^^^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^^^^....', + '..^^^^^^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^^^^^^^^^^^^^^...', + '.^^^^^^^vvvv^^^^^^^...', + '..^^^^^^vvvv^^^^^^....', + '..^^^^^^vvvv^^^^^^....', + '...^^^^^vvvv^^^^^.....', + '....^^^^vvvv^^^^.....', + '....^^^^vvvv^^^^.....', + '.....------------......', + '....--------------.....', + '....--------------.....', + '....--------------.....', + '....--------------.....', + '........................' + ], 24, 32); + } + + function drawSunblossomGate() { + return artRowsSized([ + '........YYYY........', + '......YYOOOOYY......', + '.....YOOOGGOOOY.....', + '....YOOOGGGGOOOY....', + '...YYOOGGWWGGOOYY...', + '..YYOOGGWWWWGGOOYY..', + '..YOOOGWWWWWWGOOOY..', + '.YOOOGWW....WWGOOOY.', + '.YOOOGW......WGOOOY.', + '.YOOOGW......WGOOOY.', + '.YOOOGW......WGOOOY.', + '.YOOOGW......WGOOOY.', + '.YOOOGW......WGOOOY.', + '.YOOOGW......WGOOOY.', + '..YOOOGG....GGOOOY..', + '..YYOOOGGGGGGOOYY...', + '...YYOONNNNNNOYY....', + '....NNNNTTTTNNN.....', + '....NNNNTTTTNNN.....', + '....NNNNTTTTNNN.....', + '....NNNNNNNNNNN.....', + '.....GGGGGGGG.......', + '.....GGGGGGGG.......', + '........................' + ], { + Y: '#f3dd5c', O: '#ef9b43', G: '#69bf67', W: '#fff3d9', N: '#866b52', T: '#6a4a33' + }, 24, 24); + } + + function drawSunblossomGateDepth() { + return depthRowsSized([ + '........^^^^........', + '......^^^^^^^^......', + '.....^^^^^^^^^^.....', + '....^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^..', + '..^^^^^^^^^^^^^^^^..', + '.^^^^^^^....^^^^^^^.', + '.^^^^^^......^^^^^^.', + '.^^^^^^......^^^^^^.', + '.^^^^^^......^^^^^^.', + '.^^^^^^......^^^^^^.', + '.^^^^^^......^^^^^^.', + '.^^^^^^......^^^^^^.', + '..^^^^^^....^^^^^^..', + '..^^^^^^^^^^^^^^^^..', + '...^^^^^------^^^...', + '....----^^^^----....', + '....----^^^^----....', + '....----^^^^----....', + '....-------------....', + '.....--------.......', + '.....--------.......', + '........................' + ], 24, 24); + } + + function drawEchoCavern() { + return artRowsSized([ + '................................', + '..........SSSSSSSSSS............', + '.......SSSSGGGGGGGGSSSS.........', + '.....SSGGGGGNNNNGGGGGGSS........', + '....SGGGGNNNNNNNNNNGGGGS........', + '...SGGGNNNNNNNNNNNNNNGGGS.......', + '..SGGNNNNNNNN..NNNNNNNNGGS......', + '.SGGNNNNNN......NNNNNNNGGS......', + '.SGNNNNN..........NNNNNNGS......', + '.SGNNNN............NNNNNGS......', + '.SGNNN..............NNNNGS......', + '.SGNNN..............NNNNGS......', + '.SGNNNN............NNNNNGS......', + '.SGNNNNN..........NNNNNNGS......', + '.SGGNNNNNN......NNNNNNNGGS......', + '..SGGNNNNNNNN..NNNNNNNNGGS......', + '...SGGGNNNNNNNNNNNNNNGGGS.......', + '....SGGGGNNNNNNNNNNGGGGS........', + '.....SSGGGGGNNNNGGGGGSS.........', + '......BBBBBBBBBBBBBBBB..........' + ], { + S: '#8a8c90', G: '#5ea362', N: '#21252d', B: '#556048' + }, 32, 20); + } + + function drawEchoCavernDepth() { + return depthRowsSized([ + '................................', + '..........^^^^^^^^^^............', + '.......^^^^^^^^^^^^^^^^.........', + '.....^^^^^^^^^^^^^^^^^^^^........', + '....^^^^^^^^^^^^^^^^^^^^^........', + '...^^^^^^^^^^^^^^^^^^^^^^^.......', + '..^^^^^^^^^^^^..^^^^^^^^^^......', + '.^^^^^^^^^^......^^^^^^^^^......', + '.^^^^^^^^..........^^^^^^^^......', + '.^^^^^^^............^^^^^^^......', + '.^^^^^^..............^^^^^^......', + '.^^^^^^..............^^^^^^......', + '.^^^^^^^............^^^^^^^......', + '.^^^^^^^^..........^^^^^^^^......', + '.^^^^^^^^^^......^^^^^^^^^......', + '..^^^^^^^^^^^^..^^^^^^^^^^......', + '...^^^^^^^^^^^^^^^^^^^^^^^.......', + '....^^^^^^^^^^^^^^^^^^^^^........', + '.....^^^^^^^^^^^^^^^^^^^.........', + '......----------------..........' + ], 32, 20); + } + + function drawWorldrootShrine() { + return artRowsSized([ + '............GGGG............', + '..........GGLLLLGG..........', + '........GGLLLWWLLLGG........', + '.......GLLLWWWWWWLLLG.......', + '......GLLWWGGGGGGWWLLG......', + '.....GLLWGGGGGGGGGGWLLG.....', + '....GLLWGGGGTTGGGGGGWLLG....', + '....GLLWGGGTTTTGGGGGWLLG....', + '...GLLWGGTTTTTTTTGGGWLLG....', + '...GLLWGGTTTNNNTTTGGWLLG....', + '..GLLWGGTTTNNWWNNTTGGWLLG...', + '..GLLWGGTTNNWWWWNNTGGWLLG...', + '..GLLWGGTTNNWYYWNNTGGWLLG...', + '..GLLWGGTTNNYYYYNNTGGWLLG...', + '..GLLWGGTTNNWYYWNNTGGWLLG...', + '..GLLWGGTTNNWWWWNNTGGWLLG...', + '..GLLWGGTTTNNWWNNTTGGWLLG...', + '...GLLWGGTTTNNNTTTGGWLLG....', + '...GLLWGGTTTTTTTTGGGWLLG....', + '....GLLWGGGTTTTGGGGGWLLG....', + '....GLLWGGGGTTGGGGGGWLLG....', + '.....GLLWGGGGGGGGGGWLLG.....', + '......GLLWWGGGGGGWWLLG......', + '.......GLLLWWTTWWLLLG.......', + '........GLLLTTTTLLGG........', + '.........TTTTTTTTTT.........', + '........TTTTRRTTTTTT........', + '.......NNNNNNNNNNNNNN.......' + ], { + G: '#4fa85c', L: '#7ad07b', W: '#c6f5be', T: '#7c5232', N: '#69523b', Y: '#ffe99a', R: '#c49668' + }, 28, 28); + } + + function drawWorldrootShrineDepth() { + return depthRowsSized([ + '............^^^^............', + '..........^^^^^^^^..........', + '........^^^^^^^^^^^^........', + '.......^^^^^^^^^^^^^^.......', + '......^^^^^^^^^^^^^^^^......', + '.....^^^^^^^^^^^^^^^^^^.....', + '....^^^^^^^^^^^^^^^^^^^^....', + '....^^^^^^^^^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^^^^^^^^^....', + '..^^^^^^^^^^^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^++^^^^^^^^^^...', + '..^^^^^^^^^^^++++^^^^^^^^^...', + '..^^^^^^^^^^^^++^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^^^^^^^^^...', + '..^^^^^^^^^^^^^^^^^^^^^^^^...', + '...^^^^^^^^^^^^^^^^^^^^^^....', + '...^^^^^^^^^^^^^^^^^^^^^^....', + '....^^^^^^^^^^^^^^^^^^^^....', + '....^^^^^^^^^^^^^^^^^^^^....', + '.....^^^^^^^^^^^^^^^^^^.....', + '......^^^^^^^^^^^^^^^^......', + '.......^^^^^^^^^^^^^^.......', + '........^^^^----^^^^........', + '.........----------.........', + '........------------........', + '.......----------------.......' + ], 28, 28); + } + + function drawSmallRaisedDepth() { + const d = Array(16 * 16).fill(0); + for (let y = 2; y <= 12; y++) for (let x = 3; x <= 12; x++) d[y * 16 + x] = 1; + return d; + } + + function filledDepthFromPixels(pixels) { + return filledDepthFromPixelsSized(pixels, 16, 16); + } + + function filledDepthFromPixelsSized(pixels, width, height = width) { + const w = clampDimension(width, 16); + const h = clampDimension(height, w); + const src = normalizePixels(pixels, w, h); + const out = Array(w * h).fill(0); + for (let i = 0; i < src.length; i++) { + if (src[i]) out[i] = 1; + } + return out; + } + function drawCottage() { const s = 16, p = blankPixels(s); rect(p, s, 4, 8, 8, 6, '#b86b4f'); diff --git a/index.html b/index.html index 28de878..8f47eeb 100644 --- a/index.html +++ b/index.html @@ -89,59 +89,63 @@
- +
+ +
+ + + + +
+
+ + + +
+
- -
- - - - -
-
- - -
-