From 0a6148c73f3c6e64cfd49b3bca480d06aba8c8c0 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Sun, 21 Jun 2026 22:29:00 +0900 Subject: [PATCH] many --- PATCH_NOTES.md | 19 +- README.md | 26 +- app_manifest.json | 9 +- assets/objects/pushpin.webp | Bin 0 -> 9566 bytes assets/objects/pushpin_stuck.webp | Bin 0 -> 13966 bytes assets/sprites/tarinai_28_zunchi_slave.webp | Bin 21728 -> 67040 bytes assets/sprites/tarinai_29_hungry_70.webp | Bin 0 -> 55892 bytes .../sprites/tarinai_30_zunchi_slave_alt.webp | Bin 0 -> 82612 bytes assets/ui/tool_pushpin.webp | Bin 0 -> 1524 bytes css/base.css | 23 +- css/components.css | 143 +- css/layout.css | 5 +- css/mobile.css | 27 +- css/panel.css | 36 +- css/styles.css | 1 - docs/ARCHITECTURE.md | 38 +- docs/CONTENT_GUIDE.md | 30 +- docs/MAINTENANCE.md | 24 +- docs/prompt.txt | 1374 ++++++----------- index.html | 170 +- js/ants.js | 30 +- js/assets.js | 45 +- js/audio.js | 76 +- js/data.js | 45 +- js/debug_tools.js | 2 + js/disease_registry.js | 61 + js/effect_registry.js | 230 ++- js/food_registry.js | 52 + js/health.js | 26 +- js/input_mode_manager.js | 119 ++ js/item_visual_registry.js | 129 ++ js/items.js | 248 ++- js/main.js | 4 +- js/performance_manager.js | 5 +- js/render.js | 25 +- js/sim_core.js | 124 +- js/sound_pack.js | 18 +- js/tarinai.js | 30 +- js/tarinai_disease_nest.js | 20 +- js/tarinai_identity_social.js | 52 +- js/tarinai_item_effects.js | 143 +- js/tarinai_needs_items.js | 74 +- js/tarinai_render.js | 17 +- js/tarinai_social_move_life.js | 24 +- js/ui.js | 2 +- js/ui_bind.js | 29 +- js/ui_charts.js | 14 +- js/ui_family.js | 3 - js/ui_family_async.js | 2 +- js/ui_family_layout.js | 15 +- js/ui_family_paths.js | 3 + js/ui_family_render.js | 2 +- js/ui_layout_dialogs.js | 7 +- js/ui_log.js | 2 +- js/ui_selected.js | 143 +- js/ui_tools.js | 31 + js/version.js | 5 +- js/weather_system.js | 49 + js/world.js | 5 - js/world_ants_system.js | 4 +- js/world_combat_effects.js | 103 +- js/world_family_social.js | 63 +- js/world_placement_log.js | 30 +- js/world_spatial_budget.js | 11 +- js/world_update.js | 39 +- scripts/generate_item_icons.py | 82 +- service-worker.js | 4 +- 67 files changed, 2653 insertions(+), 1519 deletions(-) create mode 100644 assets/objects/pushpin.webp create mode 100644 assets/objects/pushpin_stuck.webp create mode 100644 assets/sprites/tarinai_29_hungry_70.webp create mode 100644 assets/sprites/tarinai_30_zunchi_slave_alt.webp create mode 100644 assets/ui/tool_pushpin.webp delete mode 100644 css/styles.css create mode 100644 js/disease_registry.js create mode 100644 js/food_registry.js create mode 100644 js/input_mode_manager.js create mode 100644 js/item_visual_registry.js delete mode 100644 js/ui_family.js create mode 100644 js/weather_system.js diff --git a/PATCH_NOTES.md b/PATCH_NOTES.md index 7b345cd..305a63f 100644 --- a/PATCH_NOTES.md +++ b/PATCH_NOTES.md @@ -1,6 +1,21 @@ # Patch notes -## 15.20.3 +## 15.20.12 + +- Added canonical item visual, food, and disease registries; wired food decay/visual helpers and mystery-drug disease sampling to registry data. +- Generated the tool palette from `TOOL_CATEGORIES` at runtime and removed duplicated category markup from `index.html`. +- Added compact family-tree personality tags and a selected-Tarinai recent-change section for personality, damage, item-effect, disease, and ant-carry causes. +- Added `InputModeManager` for touch-first mobile classification and `WeatherSystem` for weather transitions/rain-water spawning. +- Added central domain events for placement, eating, decay, effects, diseases, damage, death, birth, fights, ants, weather, and rain item drops. +- Moved item-effect modifiers, active summaries, apply/tick behavior callbacks, and food interest class queries into registries. +- Moved canonical sound gains, sample paths, categories, cooldowns, and ID category mapping out of `audio.js` into `sound_pack.js`. +- Added named ant ecology extension hooks/comments without changing ant balance. +- Bumped cache/version references to `15.20.12`; documented the new source-of-truth rules. + +## 15.20.12 + +- Updated documentation after inspecting the current game surface: field choices, startup flow, tool groups, chart/family/debug surfaces, localStorage settings, and validation fallback when Node is unavailable. +- Removed unused `css/styles.css` shim and no-op `js/ui_family.js` loader entry; refactored duplicate world initialization and allocation-heavy live-count paths. - Removed item-effect speech bubbles: `ドン`, `ぐるる`, `すっきり`, `Hg xN`, `ちまっ`, `でかっ`, `へにゃ`, `ムキッ`, and `mm...`. - Reorganized the tool palette into color-coded collapsible groups: operation, food/water/grass, medicine/effects, habitat/living things, hazards/obstacles. @@ -12,7 +27,7 @@ - Tightened mobile operation UI visibility to smartphone-class compact touch devices. - Sleep-disease Tarinai can be moved by ant hauling without snapping back to their sleep anchor. -## 15.20.2 and earlier summary +## 15.20.12 and earlier summary - Consolidated documentation into dense English files; left `docs/prompt.txt` untouched. - Restored rain, revised water/zunda/mercury item sprites, added zunchi-slave head zunchi, fight win rate, fight defeat chance, and post-wake lock. diff --git a/README.md b/README.md index 5e9aa0a..9466597 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Tarinai Colony Observation Game +# Tarinai Colony Observation Game -Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/ants.js b/js/ants.js index a82d66d..81909f0 100644 --- a/js/ants.js +++ b/js/ants.js @@ -13,6 +13,15 @@ const ANT_WORKER_RETURN_WEAK_SPEED = 9.0; const ANT_WORKER_DRAG_SPEED = 6.1875; const ANT_QUEEN_SPEED = 13.5; +const ANT_EXTENSION_HOOKS = Object.freeze({ + nestHunger: "reserved", + nestGrowth: "reserved", + queenReproduction: "existing-queen-founding", + workerLifecycle: "worker-pool-hp", + carryingSuccessRate: "current-deterministic-haul", + territorialBehavior: "reserved", +}); + function clampAntHp(v) { return clamp(Number(v) || 0, 0, ANT_WORKER_HP); } @@ -30,6 +39,8 @@ function sanitizeAntWorkerPool(list, fallbackCount = 0) { function ensureAntNestWorkerPool(nest) { if (!nest || nest.type !== "ant_nest") return []; + // Extension hook: future nest hunger/growth state should attach to the nest + // item here without changing current worker count balance. if (!Array.isArray(nest.antWorkers)) { nest.antWorkers = buildAntWorkerPool(nest.antCount ?? ANT_NEST_START_COUNT); } else { @@ -150,6 +161,8 @@ class AntActor { } chooseTarget(maxDist = 56) { + // Extension hook: future carrying success and territorial behavior should + // filter or rank candidates here while preserving sleep-disease hauling. let best = null; let bestScore = Infinity; for (const t of this.world?.tarinai || []) { @@ -207,6 +220,8 @@ class AntActor { target.antDraggedBy = this.id; target.fearTimer = Math.max(target.fearTimer || 0, 0.55); target.thought = "\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b"; + target.recordChangeCause?.("\u30a2\u30ea\u306b\u904b\u3070\u308c\u305f", "\u72b6\u614b"); + this.world?.emit?.("ant:grabbed", { ant: this, target }); audio.antDrag?.(); this.world.drawListDirty = true; return; @@ -268,17 +283,21 @@ class AntActor { const targetWasSleeping = Boolean(target.sleeping || target.state === "sleep"); target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.35); target.antDraggedBy = this.id; + if (leadHauler && (this.world?.time || 0) - (this.lastCarryEventAt || -999) > 0.75) { + this.lastCarryEventAt = this.world?.time || 0; + this.world?.emit?.("ant:carried", { ant: this, target, lead: true, haulers }); + } target.target = null; if (targetWasSleeping) { target.sleeping = true; target.state = "sleep"; target.sleepTimer = Math.max(target.sleepTimer || 0, 0.8); - target.thought = "眠ったままアリに運ばれている"; + target.thought = "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b"; } else { target.state = "panic"; target.sleeping = false; target.fearTimer = Math.max(target.fearTimer || 0, 0.36); - target.thought = "アリに巣へ運ばれている"; + target.thought = "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b"; } const dHome = distXY(target.x, target.y, home.x, home.y) || 1; if (dHome < Math.max(18, home.r * 0.66)) { @@ -290,7 +309,8 @@ class AntActor { return; } const hpFactor = clamp(this.hp / ANT_WORKER_HP, 0.45, 1.0); - const speed = ANT_WORKER_DRAG_SPEED * hpFactor; + const haulerBoost = 1 + Math.min(0.18, Math.max(0, haulers - 1) * 0.045); + const speed = ANT_WORKER_DRAG_SPEED * hpFactor * haulerBoost; const ux = (home.x - target.x) / dHome; const uy = (home.y - target.y) / dHome; if (leadHauler) { @@ -324,6 +344,7 @@ class AntActor { if (this.dead) return; this.dead = true; audio.antDie?.(); + this.world?.emit?.("ant:died", { ant: this, home, reason: "hp" }); const corpse = new Item("ant_corpse", this.x, this.y); corpse.amount = 24; corpse.workerSprite = this.workerSpriteId(); @@ -336,6 +357,8 @@ class AntActor { buildNestAtFoundingSpot(force = false) { if (!this.world) return null; + // Extension hook: queen reproduction currently means founding a new nest. + // Future growth rules should branch here without changing current behavior. const base = new Item("ant_nest", this.foundingX, this.foundingY); let spot = null; if (this.world.findPlacementSpot) { @@ -466,6 +489,7 @@ window.ANT_NEST_MAX_COUNT = ANT_NEST_MAX_COUNT; window.ANT_NEST_MAX_OUTSIDE = ANT_NEST_MAX_OUTSIDE; window.ANT_WORKER_HP = ANT_WORKER_HP; window.ANT_QUEEN_HP = ANT_QUEEN_HP; +window.ANT_EXTENSION_HOOKS = ANT_EXTENSION_HOOKS; window.buildAntWorkerPool = buildAntWorkerPool; window.ensureAntNestWorkerPool = ensureAntNestWorkerPool; window.syncAntNestCount = syncAntNestCount; diff --git a/js/assets.js b/js/assets.js index 9929c39..f8153aa 100644 --- a/js/assets.js +++ b/js/assets.js @@ -7,11 +7,13 @@ const imageAssetIndex = new Map(); const scaledSpriteCache = new Map(); const stainOverlayCache = new Map(); +const SPRITE_CACHE_LIMIT = 128; +const STAIN_CACHE_LIMIT = 64; -const INITIAL_IMAGE_IDS = new Set(["smile", "zunchi", "zunchi_02", "genkotsu"]); -const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "zunchi", "zunchi_02", "genkotsu"]); +const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck"]); +const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck"]); -function trimCanvasCache(cache, limit = 420) { +function trimCanvasCache(cache, limit = 128) { while (cache.size > limit) { const first = cache.keys().next().value; if (first === undefined) break; @@ -19,16 +21,16 @@ function trimCanvasCache(cache, limit = 420) { } } -function quantizedCanvasSize(v, step = 16) { +function quantizedCanvasSize(v, step = 24) { return Math.max(1, Math.round(v / step) * step); } -const SPRITE_CACHE_SCALE = 2.0; +const SPRITE_CACHE_SCALE = 1.55; function getCachedSpriteCanvas(id, img, w, h) { if (!img || !w || !h) return img; - const qw = quantizedCanvasSize(w, 16); - const qh = quantizedCanvasSize(h, 16); + const qw = quantizedCanvasSize(w); + const qh = quantizedCanvasSize(h); const key = `${id}:${qw}x${qh}`; let canvas = scaledSpriteCache.get(key); if (canvas) return canvas; @@ -41,14 +43,14 @@ function getCachedSpriteCanvas(id, img, w, h) { c.clearRect(0, 0, qw, qh); c.drawImage(img, 0, 0, qw, qh); scaledSpriteCache.set(key, canvas); - trimCanvasCache(scaledSpriteCache, 520); + trimCanvasCache(scaledSpriteCache, SPRITE_CACHE_LIMIT); return canvas; } function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) { if (!img || stain <= 0.01 || !w || !h) return null; - const qw = quantizedCanvasSize(w, 16); - const qh = quantizedCanvasSize(h, 16); + const qw = quantizedCanvasSize(w); + const qh = quantizedCanvasSize(h); const bucket = Math.max(1, Math.min(8, Math.ceil(stain * 8))); const seedBucket = Math.round((seed || 0) * 16); const key = `${id}:${qw}x${qh}:s${bucket}:r${seedBucket}`; @@ -80,7 +82,7 @@ function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) { c.globalAlpha = 1; c.globalCompositeOperation = "source-over"; stainOverlayCache.set(key, canvas); - trimCanvasCache(stainOverlayCache, 360); + trimCanvasCache(stainOverlayCache, STAIN_CACHE_LIMIT); return canvas; } @@ -175,9 +177,23 @@ function loadImages(ids = null, onProgress = null) { return Promise.all(tasks).then(() => undefined); } +function trimImageMemory(aggressive = false) { + trimCanvasCache(scaledSpriteCache, aggressive ? 48 : SPRITE_CACHE_LIMIT); + trimCanvasCache(stainOverlayCache, aggressive ? 16 : STAIN_CACHE_LIMIT); +} + +if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", () => { + if (document.hidden) trimImageMemory(true); + }); +} + +if (typeof window !== "undefined") { + window.setInterval(() => trimImageMemory(false), 45000); +} + function startBackgroundImageLoading() { setupAssetIndex(); - const allAssets = [...SPRITES, ...DECOR_ASSETS]; const loadGroup = (ids, priority = "low") => { for (const id of ids) { const asset = imageAssetIndex.get(id); @@ -189,7 +205,8 @@ function startBackgroundImageLoading() { : (fn) => setTimeout(fn, 60); schedule(() => loadGroup(EARLY_IMAGE_IDS, "auto"), 450); schedule(() => { - for (const asset of allAssets) loadImageAsset(asset, "low"); + for (const asset of [...SPRITES, ...DECOR_ASSETS]) loadImageAsset(asset, "low"); + trimImageMemory(false); }, 2200); } @@ -224,4 +241,4 @@ function getImageMetrics(idOrImg) { setupAssetIndex(); -window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas }; +window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, trimImageMemory, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas }; diff --git a/js/audio.js b/js/audio.js index d0fb872..6abcfbc 100644 --- a/js/audio.js +++ b/js/audio.js @@ -17,66 +17,13 @@ const audio = { masterGainNode: null, compressorNode: null, _synthIdGain: 1, - idGain: { - sfx_notify: 2.60, - sfx_log_push: 2.60, - sfx_ui_click: 2.35, - sfx_ui_fold: 2.20, - sfx_place: 2.10, - sfx_delete: 2.25, - sfx_poke: 1.85, - sfx_grab: 2.15, - sfx_drop: 2.10, - sfx_water_hose: 1.85, - sfx_genkotsu_fall: 1.70, - sfx_genkotsu_impact: 1.90, - sfx_ground_rumble: 1.75, - sfx_firecracker_fuse: 1.55, - sfx_firecracker_explode: 1.70, - }, - idMinGap: { - sfx_ui_click: 0.030, - sfx_place: 0.055, - sfx_grass: 0.12, - sfx_zunchi: 0.14, - sfx_ant_die: 0.14, - sfx_notify: 0.10, - sfx_log_push: 0.10, - }, + idGain: {}, + idMinGap: {}, categoryEnabled: { voice: false, notify: true, ops: true }, - categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 }, - samplePaths: { - voice_hau: "assets/sounds/voice_001_hau.wav", - voice_flee: "assets/sounds/voice_002_flee.wav", - voice_poop: "assets/sounds/voice_003_po.wav", - sfx_eat: "assets/sounds/voice_004_eat.wav", - voice_stress: "assets/sounds/voice_005_stress.wav", - sfx_sleep: "assets/sounds/voice_006_sleep.wav", - sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3", - - // Optional replacement slots. Missing files are never requested unless you add a path. - // sfx_notify: "assets/sounds/sfx_notify.mp3", - // sfx_ui_click: "assets/sounds/sfx_ui_click.mp3", - // sfx_genkotsu_fall: "assets/sounds/sfx_genkotsu_fall.mp3", - // sfx_genkotsu_impact: "assets/sounds/sfx_genkotsu_impact.mp3", - }, - soundCategories: { - voice: "たりないの声", - notify: "通知", - ops: "操作", - }, - soundMap: { - voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", - sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice", - sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice", - sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice", - sfx_notify: "notify", sfx_phase: "notify", sfx_log_push: "notify", - sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops", - sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops", - sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops", - sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops", - sfx_zunchi: "ops", sfx_ant_nest: "ops", - }, + categoryGain: {}, + samplePaths: {}, + soundCategories: {}, + soundMap: {}, applySoundPack(pack = window.TARINAI_SOUND_PACK) { if (!pack || typeof pack !== "object") return false; this.soundPackVersion = String(pack.version || "custom"); @@ -85,6 +32,7 @@ const audio = { if (pack.idMinGap && typeof pack.idMinGap === "object") this.idMinGap = { ...this.idMinGap, ...pack.idMinGap }; if (pack.samplePaths && typeof pack.samplePaths === "object") this.samplePaths = { ...this.samplePaths, ...pack.samplePaths }; if (pack.categories && typeof pack.categories === "object") this.soundCategories = { ...this.soundCategories, ...pack.categories }; + if (pack.soundMap && typeof pack.soundMap === "object") this.soundMap = { ...this.soundMap, ...pack.soundMap }; this._samplesReady = false; this.samples = {}; return true; @@ -344,12 +292,12 @@ const audio = { }, bubble(text) { const s = String(text || ""); - if (s.includes("もぐ")) return this.playSample("sfx_eat", 0.20, "voice") || this.play("sfx_eat", { category: "voice", minGap: 0.20 }); - if (s.includes("ぶり")) return this.playSample("voice_poop", 0.28, "voice") || this.play("sfx_zunchi", { category: "ops", minGap: 0.28 }); - if (s.includes("はっ")) return this.playSample("voice_stress", 0.80, "voice") || this.play("sfx_damage_soft", { category: "voice", minGap: 0.80 }); + if (s.includes("\u3082\u3050")) return this.playSample("sfx_eat", 0.20, "voice") || this.play("sfx_eat", { category: "voice", minGap: 0.20 }); + if (s.includes("\u3076\u308a")) return this.playSample("voice_poop", 0.28, "voice") || this.play("sfx_zunchi", { category: "ops", minGap: 0.28 }); + if (s.includes("\u306f\u3063")) return this.playSample("voice_stress", 0.80, "voice") || this.play("sfx_damage_soft", { category: "voice", minGap: 0.80 }); if (s.includes("Zzz")) return this.playSample("sfx_sleep", 1.25, "voice") || this.play("sfx_sleep", { category: "voice", minGap: 1.25 }); - if (s.includes("はぁ") || s.includes("はあ")) return this.playSample("voice_flee", 0.70, "voice") || this.play("sfx_damage_heavy", { category: "voice", minGap: 0.70 }); - if (s.includes("はう") || s.includes("はぅ")) return this.playSample("voice_hau", 0.45, "voice") || this.play("sfx_wake", { category: "voice", minGap: 0.45 }); + if (s.includes("\u306f\u3041") || s.includes("\u306f\u3042")) return this.playSample("voice_flee", 0.70, "voice") || this.play("sfx_damage_heavy", { category: "voice", minGap: 0.70 }); + if (s.includes("\u306f\u3046") || s.includes("\u306f\u3045")) return this.playSample("voice_hau", 0.45, "voice") || this.play("sfx_wake", { category: "voice", minGap: 0.45 }); return false; }, eat() { if (!this.playSample("sfx_eat", 0.20, "voice")) this.play("sfx_eat", { category: "voice", minGap: 0.20 }); }, diff --git a/js/data.js b/js/data.js index dba56dc..504424b 100644 --- a/js/data.js +++ b/js/data.js @@ -28,6 +28,8 @@ const SPRITES = [ { id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true }, { id: "birth_ritual", label: "\u7e41\u6b96\u524d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true }, { id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true }, + { id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true }, + { id: "zunchi_slave_alt", label: "\u305a\u3093\u3061\u3069\u308c\u3044 2", path: "assets/sprites/tarinai_30_zunchi_slave_alt.webp", displayOnly: true }, ]; const DECOR_ASSETS = [ { id: "zunchi", path: "assets/objects/zunchi.webp" }, @@ -35,6 +37,8 @@ const DECOR_ASSETS = [ { id: "ant_queen", path: "assets/objects/ant_queen.webp" }, { id: "ant_worker", path: "assets/objects/ant_worker.webp" }, { id: "genkotsu", path: "assets/objects/genkotsu.webp" }, + { id: "pushpin", path: "assets/objects/pushpin.webp" }, + { id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" }, ]; const TOOL_DEFINITIONS = Object.freeze({ @@ -51,23 +55,24 @@ const TOOL_DEFINITIONS = Object.freeze({ water_bowl: { id: "water_bowl", itemType: "water_bowl", label: "\u6c34\u306e\u76bf", placeable: true, scalable: false, radius: 20, amount: 999, icon: "assets/ui/tool_water_bowl.webp", tooltip: "\u885b\u751f\u7684\u306a\u6c34\u304c\u5165\u3063\u305f\u76bf\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" }, sleep_drug: { id: "sleep_drug", itemType: "sleep_drug", label: "\u306d\u3080\u308a\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_sleep_drug.webp", tooltip: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002" }, - laxative: { id: "laxative", itemType: "laxative", label: "下剤", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "ずんちを大量に排泄させる。何かを食べてもすぐに出て行ってしまう。" }, - protein: { id: "protein", itemType: "protein", label: "プロテイン", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "体をたくましくし、病気に強くなる。" }, - niteropu: { id: "niteropu", itemType: "niteropu", label: "ンイテロプ", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "体を頼りなくし、病気に弱くなる。" }, - ammo: { id: "ammo", itemType: "ammo", label: "弾薬", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "全てを加速させる。" }, - mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "怪しい薬", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "何かが起こる。" }, - mercury: { id: "mercury", itemType: "mercury", label: "水銀", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "長生きの秘訣。" }, - giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "巨大薬", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "体を大きく強くするが、負担が大きい。" }, - dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "矮小薬", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "体を小さく弱くするが、負担も大きい。" }, - zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "ずんだ汁", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "食べると全てのアイテム効果を除去する。" }, + laxative: { id: "laxative", itemType: "laxative", label: "\u4e0b\u5264", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "\u305a\u3093\u3061\u3092\u5927\u91cf\u306b\u6392\u6cc4\u3055\u305b\u308b\u3002\u4f55\u304b\u3092\u98df\u3079\u3066\u3082\u3059\u3050\u306b\u51fa\u3066\u884c\u3063\u3066\u3057\u307e\u3046\u3002" }, + protein: { id: "protein", itemType: "protein", label: "\u30d7\u30ed\u30c6\u30a4\u30f3", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "\u75c5\u6c17\u306b\u5f37\u304f\u3001\u305f\u304f\u307e\u3057\u304f\u3002" }, + niteropu: { id: "niteropu", itemType: "niteropu", label: "\u30f3\u30a4\u30c6\u30ed\u30d7", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "\u75c5\u6c17\u306b\u5f31\u304f\u3001\u305f\u3088\u308a\u306a\u304f\u3002" }, + ammo: { id: "ammo", itemType: "ammo", label: "\u5f3e\u85ac", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "\u5168\u3066\u3092\u52a0\u901f\u3055\u305b\u308b\u3002" }, + mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "\u602a\u3057\u3044\u85ac", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "\u4f55\u304b\u304c\u8d77\u3053\u308b\u3002" }, + mercury: { id: "mercury", itemType: "mercury", label: "\u6c34\u9280", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "\u9577\u751f\u304d\u306e\u79d8\u8a23\u3002" }, + giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "\u5de8\u5927\u85ac", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "\u4f53\u3092\u5927\u304d\u304f\u5f37\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u304c\u5927\u304d\u3044\u3002" }, + dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "\u77ee\u5c0f\u85ac", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "\u4f53\u3092\u5c0f\u3055\u304f\u5f31\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u3082\u5927\u304d\u3044\u3002" }, + zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "\u305a\u3093\u3060\u6c41", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "\u98df\u3079\u308b\u3068\u5168\u3066\u306e\u30a2\u30a4\u30c6\u30e0\u52b9\u679c\u3092\u9664\u53bb\u3059\u308b\u3002" }, grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.webp", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" }, stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.webp", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" }, - genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "上から正義げんこつを高速で落とし、アリとたりないに大ダメージと吹っ飛びを与える。" }, + genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "\u62f3\u3092\u5730\u9762\u306b\u305f\u305f\u304d\u3064\u3051\u308b\u3002" }, bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" }, nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" }, ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" }, ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" }, firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" }, + pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" }, fence_v: { id: "fence_v", itemType: "fence_v", label: "\u7e26\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" }, fence_h: { id: "fence_h", itemType: "fence_h", label: "\u6a2a\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" }, water: { id: "water", itemType: "water", label: "\u6c34", placeable: false, scalable: false, radius: 12, amount: 64, simulationOnly: true }, @@ -77,6 +82,18 @@ const TOOL_DEFINITIONS = Object.freeze({ ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true }, }); +const TOOL_CATEGORIES = Object.freeze([ + { id: "operate", label: "\u64cd\u4f5c\u30fb\u89b3\u5bdf", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] }, + { id: "food", label: "\u98df\u3079\u7269\u30fb\u6c34\u30fb\u8349", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice"] }, + { id: "medicine", label: "\u85ac\u30fb\u500b\u4f53\u52b9\u679c", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] }, + { id: "habitat", label: "\u751f\u6d3b\u30fb\u751f\u304d\u7269", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ant_nest", "ball"] }, + { id: "hazard", label: "\u5371\u967a\u30fb\u969c\u5bb3\u7269", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "fence_v", "fence_h"] }, +]); + +function toolCategories() { + return [...TOOL_CATEGORIES].sort((a, b) => (a.order || 0) - (b.order || 0)); +} + function toolDefinition(id = "") { return TOOL_DEFINITIONS[id] || null; } @@ -139,6 +156,8 @@ const ALPHA_BOUNDS = { stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 }, intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 }, zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 }, + hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 }, + zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 }, zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 }, zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 }, ant_queen: { x: 0, y: 0, w: 300, h: 155, imageW: 300, imageH: 155 }, @@ -157,12 +176,12 @@ const CONFIG = { zunchiLimit: 56, traceLimit: 64, splatLimit: 48, - effectLimit: 96, + effectLimit: 78, worldPadding: 30, hungerPerMinute: 11.2, lonelinessPerMinute: 7.6, energyPerMinute: 8.2, - stressDecayPerMinute: 9, + stressDecayPerMinute: 10.2, reproductionCooldown: 18, dayLength: 120, childGrowTime: 58, @@ -170,7 +189,7 @@ const CONFIG = { lightingQuality: "cinematic", weatherChangeMin: 46, weatherChangeMax: 96, - bubbleLimit: 42, + bubbleLimit: 32, grassLimit: 96, highPopulationMode: 70, ultraPopulationMode: 120, diff --git a/js/debug_tools.js b/js/debug_tools.js index df85fc4..30a7b7c 100644 --- a/js/debug_tools.js +++ b/js/debug_tools.js @@ -20,6 +20,7 @@ window.setInterval(() => { const w = window.world; const perf = window.TarinaiPerformance?.debugSnapshot(w) || {}; + const input = window.TarinaiInputMode?.snapshot?.() || {}; panel.textContent = [ `tarinai v${window.TARINAI_VERSION || "?"}`, `fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`, @@ -29,6 +30,7 @@ `drawList ${(w?.drawList || []).length} logs ${(w?.logs || []).length}`, `dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`, `sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`, + `input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)} uaMobile:${Boolean(input.userAgentMobile)}`, `audio ${lastAudio.join(", ") || window.TarinaiAudio?.lastPlayedId || "-"}`, `effects ${window.TarinaiEffectRegistry?.debugSnapshot?.().length || 0} registered`, `events ${lastEvents.join(", ")}`, diff --git a/js/disease_registry.js b/js/disease_registry.js new file mode 100644 index 0000000..425694c --- /dev/null +++ b/js/disease_registry.js @@ -0,0 +1,61 @@ +"use strict"; + +(function (global) { + const rawDefinitions = { + disease_zunchi: { + id: "disease_zunchi", + flag: "zunchiDisease", + label: "\u305a\u3093\u3061\u75c5", + symptoms: ["stress", "energy_loss", "infection"], + displayText: "\u305a\u3093\u3061\u75c5", + color: "rgba(113,93,59,0.74)", + mysteryEligible: true, + cureRules: ["water", "natural_recovery"] + }, + disease_sleep: { + id: "disease_sleep", + flag: "sleepDisease", + label: "\u306d\u3080\u308a\u75c5", + symptoms: ["sleep_lock", "no_counterattack"], + displayText: "\u306d\u3080\u308a\u75c5", + color: "rgba(104,84,168,0.78)", + mysteryEligible: true, + cureRules: ["damage"] + }, + disease_explosion: { + id: "disease_explosion", + flag: "explosionDisease", + label: "\u7206\u767a\u75c5", + symptoms: ["explodes_after_timer", "water_seeking"], + displayText: "\u7206\u767a\u75c5", + color: "rgba(210,90,44,0.78)", + baseInfectionChance: 0.15, + mysteryEligible: true, + cureRules: ["water", "zunda_mochi"] + }, + disease_fight: { + id: "disease_fight", + flag: "fightDisease", + label: "\u304d\u305a\u3064\u304d\u75c5", + symptoms: ["fight_lash_out", "zunda_seeking"], + displayText: "\u304d\u305a\u3064\u304d\u75c5", + color: "rgba(180,86,52,0.80)", + mysteryEligible: true, + cureRules: ["zunda_mochi", "natural_recovery"] + } + }; + + class DiseaseRegistry { + constructor(defs) { this.defs = new Map(Object.entries(defs || {})); } + get(id = "") { return this.defs.get(String(id || "")) || null; } + byFlag(flag = "") { return [...this.defs.values()].find(def => def.flag === flag) || null; } + label(id = "") { return this.get(id)?.label || id || ""; } + color(id = "") { return this.get(id)?.color || "rgba(255,242,160,0.85)"; } + mysteryPool() { return [...this.defs.values()].filter(def => def.mysteryEligible).map(def => def.id); } + debugSnapshot() { return [...this.defs.values()].map(def => ({ id: def.id, label: def.label, flag: def.flag, cures: def.cureRules || [] })); } + } + + const registry = new DiseaseRegistry(rawDefinitions); + global.TarinaiDiseaseRegistry = registry; + global.TARINAI_DISEASE_DEFINITIONS = Object.freeze(rawDefinitions); +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/effect_registry.js b/js/effect_registry.js index 6fc4bf7..20c3f66 100644 --- a/js/effect_registry.js +++ b/js/effect_registry.js @@ -3,74 +3,248 @@ (function (global) { const rawDefinitions = { laxative: { - label: "下剤", color: "rgba(119, 136, 58, 0.78)", kind: "timed", defaultDuration: 60, - tags: ["item", "behavior", "food"], display: timer => `${Math.ceil(timer || 0)}秒 / 3秒ごとにずんち / 食事の空腹回復なし`, + label: "\u4e0b\u5264", color: "rgba(119, 136, 58, 0.78)", kind: "timed", defaultDuration: 60, + tags: ["item", "behavior", "food"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / 3\u79d2\u3054\u3068\u306b\u305a\u3093\u3061 / \u98df\u4e8b\u306e\u7a7a\u8179\u56de\u5fa9\u306a\u3057`, }, ammo: { - label: "弾薬", color: "rgba(216, 142, 38, 0.82)", kind: "timed", defaultDuration: 18, - tags: ["item", "behavior", "movement"], display: timer => `${Math.ceil(timer || 0)}秒 / 歩行速度10倍 / 衝突で弾き飛ばす`, + label: "\u5f3e\u85ac", color: "rgba(216, 142, 38, 0.82)", kind: "timed", defaultDuration: 18, + tags: ["item", "behavior", "movement"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u6b69\u884c\u901f\u5ea610\u500d / \u885d\u7a81\u3067\u5f3e\u304d\u98db\u3070\u3059`, }, protein: { - label: "プロテイン", color: "rgba(239, 122, 51, 0.86)", kind: "permanent", exclusiveGroup: "power", - tags: ["item", "behavior", "power"], display: () => "永続 / 与ダメージ1.75倍 / 病気確率0.5倍", + label: "\u30d7\u30ed\u30c6\u30a4\u30f3", color: "rgba(239, 122, 51, 0.86)", kind: "permanent", exclusiveGroup: "power", + tags: ["item", "behavior", "power"], display: () => "\u6c38\u7d9a / \u4e0e\u30c0\u30e1\u30fc\u30b81.75\u500d / \u75c5\u6c17\u78ba\u73870.5\u500d", }, niteropu: { - label: "ンイテロプ", color: "rgba(88, 76, 166, 0.78)", kind: "permanent", exclusiveGroup: "power", - tags: ["item", "behavior", "power"], display: () => "永続 / 与ダメージ0.5倍 / 病気確率2倍", + label: "\u30f3\u30a4\u30c6\u30ed\u30d7", color: "rgba(88, 76, 166, 0.78)", kind: "permanent", exclusiveGroup: "power", + tags: ["item", "behavior", "power"], display: () => "\u6c38\u7d9a / \u4e0e\u30c0\u30e1\u30fc\u30b80.5\u500d / \u75c5\u6c17\u78ba\u73872\u500d", }, mercury: { - label: "水銀", color: "rgba(96, 146, 166, 0.82)", kind: "permanent", stack: "multiply", - tags: ["item", "life"], display: (_timer, t) => `永続 / 寿命x${((t && t.mercuryLifeMultiplier) || 1.3).toFixed(2)}(食べるたび乗算)`, + label: "\u6c34\u9280", color: "rgba(96, 146, 166, 0.82)", kind: "permanent", stack: "multiply", + tags: ["item", "life"], display: (_timer, t) => `\u6c38\u7d9a / \u5bff\u547dx${((t && t.mercuryLifeMultiplier) || 1.3).toFixed(2)}\uff08\u98df\u3079\u308b\u305f\u3073\u4e57\u7b97\uff09`, }, giant_drug: { - label: "巨大薬", color: "rgba(228, 123, 58, 0.86)", kind: "permanent", exclusiveGroup: "body_size", - tags: ["item", "body", "behavior"], display: () => "永続 / サイズ3倍 / 攻撃2倍 / 速度1.5倍 / 毎秒1ダメージ", + label: "\u5de8\u5927\u85ac", color: "rgba(228, 123, 58, 0.86)", kind: "permanent", exclusiveGroup: "body_size", + tags: ["item", "body", "behavior"], display: () => "\u6c38\u7d9a / \u30b5\u30a4\u30ba3\u500d / \u653b\u64832\u500d / \u901f\u5ea61.5\u500d / \u6bce\u79d21\u30c0\u30e1\u30fc\u30b8", }, dwarf_drug: { - label: "矮小薬", color: "rgba(109, 107, 190, 0.82)", kind: "permanent", exclusiveGroup: "body_size", - tags: ["item", "body", "behavior"], display: () => "永続 / サイズ0.25倍 / 攻撃0.25倍 / 速度0.5倍 / 毎秒1ダメージ", + label: "\u77ee\u5c0f\u85ac", color: "rgba(109, 107, 190, 0.82)", kind: "permanent", exclusiveGroup: "body_size", + tags: ["item", "body", "behavior"], display: () => "\u6c38\u7d9a / \u30b5\u30a4\u30ba0.25\u500d / \u653b\u64830.25\u500d / \u901f\u5ea60.5\u500d / \u6bce\u79d21\u30c0\u30e1\u30fc\u30b8", }, love_mochi: { - label: "へこ餅", color: "rgba(190,82,132,0.78)", kind: "timer", timerProp: "loveMochiTimer", - tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}秒 / 繁殖行動が起きやすい`, + label: "\u3078\u3053\u9905", color: "rgba(190,82,132,0.78)", kind: "timer", timerProp: "loveMochiTimer", + tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u3044`, }, fight_mochi: { - label: "けんか餅", color: "rgba(180,86,52,0.80)", kind: "timer", timerProp: "fightMochiTimer", - tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}秒 / 喧嘩が起きやすい`, + label: "\u3051\u3093\u304b\u9905", color: "rgba(180,86,52,0.80)", kind: "timer", timerProp: "fightMochiTimer", + tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044`, }, sleep_drug: { - label: "ねむり薬", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"], + label: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"], }, mystery_drug: { - label: "怪しい薬", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"], + label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"], }, zunda_juice: { - label: "ずんだ汁", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"], + label: "\u305a\u3093\u3060\u6c41", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"], }, - disease_zunchi: { label: "ずんち病", color: "rgba(113, 93, 59, 0.74)", kind: "disease", tags: ["disease", "behavior"] }, - disease_sleep: { label: "ねむり病", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["disease", "behavior"] }, - disease_explosion: { label: "爆発病", color: "rgba(210, 90, 44, 0.78)", kind: "disease", tags: ["disease", "behavior"] }, - disease_fight: { label: "きずつき病", color: "rgba(180,86,52,0.80)", kind: "disease", tags: ["disease", "behavior"] }, + disease_zunchi: { label: "\u305a\u3093\u3061\u75c5", color: "rgba(113, 93, 59, 0.74)", kind: "disease", tags: ["disease", "behavior"] }, + disease_sleep: { label: "\u306d\u3080\u308a\u75c5", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["disease", "behavior"] }, + disease_explosion: { label: "\u7206\u767a\u75c5", color: "rgba(210, 90, 44, 0.78)", kind: "disease", tags: ["disease", "behavior"] }, + disease_fight: { label: "\u304d\u305a\u3064\u304d\u75c5", color: "rgba(180,86,52,0.80)", kind: "disease", tags: ["disease", "behavior"] }, }; + Object.assign(rawDefinitions.laxative, { + modifiers: { hungerReliefMultiplier: 0 }, + tickInterval: 3, + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.ammo, { + modifiers: { speedMultiplier: 10 }, + visualEffectId: "fall", + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.protein, { + modifiers: { damageMultiplier: 1.75, diseaseChanceMultiplier: 0.5 }, + visualEffectId: "fall_up_red", + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.niteropu, { + modifiers: { damageMultiplier: 0.5, diseaseChanceMultiplier: 2 }, + visualEffectId: "fall_down_purple", + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.mercury, { + modifiers: { lifeMultiplierPerUse: 1.3 }, + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.giant_drug, { + modifiers: { sizeMultiplier: 3, damageMultiplier: 2, speedMultiplier: 1.5, damagePerSecond: 1 }, + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + Object.assign(rawDefinitions.dwarf_drug, { + modifiers: { sizeMultiplier: 0.25, damageMultiplier: 0.25, speedMultiplier: 0.5, damagePerSecond: 1 }, + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); + for (const id of ["love_mochi", "fight_mochi", "sleep_drug"]) { + Object.assign(rawDefinitions[id], { mysteryEligible: true, zundaJuiceRemovable: id !== "sleep_drug" }); + } + Object.assign(rawDefinitions.zunda_juice, { removesItemEffects: true, mysteryEligible: false }); + + Object.assign(rawDefinitions.love_mochi, { + onApply(tarinai, context = {}) { + const scale = context.scale ?? 1; + tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + rand(4, 12)); + tarinai.stress = clamp(tarinai.stress - 8, 0, 130); + return true; + }, + }); + Object.assign(rawDefinitions.fight_mochi, { + onApply(tarinai, context = {}) { + const scale = context.scale ?? 1; + tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + rand(10, 18)); + tarinai.stress = clamp(tarinai.stress + 10, 0, 130); + return true; + }, + }); + Object.assign(rawDefinitions.sleep_drug, { + onApply(tarinai, context = {}) { + return tarinai.infectSleepDisease?.(context.item || null) || true; + }, + }); + Object.assign(rawDefinitions.laxative, { + onApply(tarinai, context = {}) { + return tarinai.setTimedItemEffect?.("laxative", Math.round((this.defaultDuration || 60) * (context.scale ?? 1))); + }, + onTick(tarinai) { + tarinai.forceLaxativePoop?.(); + return true; + }, + }); + Object.assign(rawDefinitions.ammo, { + onApply(tarinai, context = {}) { + return tarinai.setTimedItemEffect?.("ammo", Math.round((this.defaultDuration || 18) * (context.scale ?? 1))); + }, + }); + Object.assign(rawDefinitions.protein, { + onApply(tarinai) { return tarinai.setPowerItemMode?.("protein"); }, + }); + Object.assign(rawDefinitions.niteropu, { + onApply(tarinai) { return tarinai.setPowerItemMode?.("niteropu"); }, + }); + Object.assign(rawDefinitions.mercury, { + onApply(tarinai) { return tarinai.setMercuryEffect?.(); }, + }); + Object.assign(rawDefinitions.giant_drug, { + onApply(tarinai) { return tarinai.setSizeItemMode?.("giant_drug"); }, + onTick(tarinai) { + tarinai.damage?.(this.modifiers?.damagePerSecond || 1, this.label); + if (tarinai.world?.effects) tarinai.spawnPowerItemEffect?.("giant_drug", 0.25); + return true; + }, + }); + Object.assign(rawDefinitions.dwarf_drug, { + onApply(tarinai) { return tarinai.setSizeItemMode?.("dwarf_drug"); }, + onTick(tarinai) { + tarinai.damage?.(this.modifiers?.damagePerSecond || 1, this.label); + if (tarinai.world?.effects) tarinai.spawnPowerItemEffect?.("dwarf_drug", 0.25); + return true; + }, + }); + Object.assign(rawDefinitions.zunda_juice, { + onApply(tarinai) { return tarinai.clearAllItemEffects?.(); }, + }); + Object.assign(rawDefinitions.disease_zunchi, { + onApply(tarinai, context = {}) { return tarinai.infectZunchiDisease?.(context.item || null, true) || true; }, + }); + Object.assign(rawDefinitions.disease_sleep, { + onApply(tarinai, context = {}) { return tarinai.infectSleepDisease?.(context.item || null) || true; }, + }); + Object.assign(rawDefinitions.disease_explosion, { + onApply(tarinai, context = {}) { return tarinai.infectExplosionDisease?.(context.item || null) || true; }, + }); + Object.assign(rawDefinitions.disease_fight, { + onApply(tarinai, context = {}) { return tarinai.infectFightDisease?.(context.item || null) || true; }, + }); + class EffectRegistry { constructor(defs) { this.defs = new Map(Object.entries(defs || {})); } get(id = "") { return this.defs.get(String(id || "")) || null; } has(id = "") { return this.defs.has(String(id || "")); } - label(id = "") { return this.get(id)?.label || id || "不明"; } + label(id = "") { return this.get(id)?.label || id || "\u4e0d\u660e"; } color(id = "") { return this.get(id)?.color || "rgba(255,242,160,0.85)"; } display(id = "", timer = 0, tarinai = null) { const def = this.get(id); if (!def) return ""; return typeof def.display === "function" ? def.display(timer, tarinai, def) : (def.display || ""); } + apply(id = "", tarinai = null, context = {}) { + const def = this.get(id); + if (!def || !tarinai || typeof def.onApply !== "function") return false; + return Boolean(def.onApply.call(def, tarinai, { ...context, effectId: id, registry: this })); + } + tick(id = "", tarinai = null, dt = 0, context = {}) { + const def = this.get(id); + if (!def || !tarinai || typeof def.onTick !== "function") return false; + return Boolean(def.onTick.call(def, tarinai, Math.max(0, Number(dt) || 0), { ...context, effectId: id, registry: this })); + } + remove(id = "", tarinai = null, context = {}) { + const def = this.get(id); + if (!def || !tarinai || typeof def.onRemove !== "function") return false; + return Boolean(def.onRemove.call(def, tarinai, { ...context, effectId: id, registry: this })); + } + modifier(id = "", key = "", fallback = 1) { + const value = this.get(id)?.modifiers?.[key]; + return Number.isFinite(value) ? value : fallback; + } + timerValue(tarinai = null, id = "") { + const def = this.get(id); + if (!def || !tarinai) return 0; + if (def.timerProp) return Number(tarinai[def.timerProp] || 0); + if (def.kind === "timed") return Number(tarinai.itemEffectTimers?.[id] || 0); + return 0; + } + isActive(tarinai = null, id = "") { + if (!tarinai) return false; + const def = this.get(id); + if (!def) return false; + if (id === "protein" || id === "niteropu") return tarinai.powerItemMode === id; + if (id === "giant_drug" || id === "dwarf_drug") return tarinai.sizeItemMode === id; + if (id === "mercury") return tarinai.lifeItemMode === "mercury"; + return this.timerValue(tarinai, id) > 0.04; + } + activeSummary(tarinai = null) { + const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "love_mochi", "fight_mochi"]; + return order + .filter(id => this.isActive(tarinai, id)) + .map(id => ({ + id, + label: this.label(id), + detail: this.display(id, this.timerValue(tarinai, id), tarinai), + })); + } labels() { return Object.fromEntries([...this.defs.entries()].map(([id, def]) => [id, def.label])); } colors() { return Object.fromEntries([...this.defs.entries()].map(([id, def]) => [id, def.color]).filter(([, color]) => color)); } randomPool() { - return [...this.defs.entries()] - .filter(([, def]) => (def.tags || []).includes("item") || (def.tags || []).includes("behavior") || (def.tags || []).includes("disease")) + const itemEffects = [...this.defs.entries()] + .filter(([id, def]) => Boolean(def.mysteryEligible && id !== "mystery_drug" && id !== "zunda_juice" && !(def.tags || []).includes("disease"))) .map(([id]) => id) .filter(id => id !== "mystery_drug" && id !== "zunda_juice"); + const diseases = global.TarinaiDiseaseRegistry?.mysteryPool?.() || []; + return [...new Set([...itemEffects, ...diseases])]; } permanentIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "permanent").map(([id]) => id); } timedIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "timed" || def.kind === "timer").map(([id]) => id); } diff --git a/js/food_registry.js b/js/food_registry.js new file mode 100644 index 0000000..b946aff --- /dev/null +++ b/js/food_registry.js @@ -0,0 +1,52 @@ +"use strict"; + +(function (global) { + const rawDefinitions = { + food: { label: "\u98df\u3079\u7269", nutrition: 8.5, hungerRelief: 0.95, interest: "normal", decayRateMultiplier: 1.0, hygienePenalty: 0.055 }, + sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 7.2, hungerRelief: 0.75, interest: "zunda_high", decayRateMultiplier: 0.82, hygienePenalty: 0.055, cures: ["disease_explosion", "disease_fight"] }, + love_mochi: { label: "\u3078\u3053\u9905", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "love_mochi" }, + fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "fight_mochi" }, + sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "sleep_drug" }, + laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "laxative", passThrough: true }, + protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "protein" }, + niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "niteropu" }, + ammo: { label: "\u5f3e\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "ammo" }, + mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mystery_drug" }, + mercury: { label: "\u6c34\u9280", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mercury" }, + giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "giant_drug" }, + dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" }, + zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true }, + grass: { label: "\u8349", nutrition: 0.46, hungerRelief: 0.46, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 }, + water: { label: "\u6c34", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 }, + water_bowl: { label: "\u6c34\u306e\u76bf", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 }, + ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 0.62, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 } + }; + + class FoodRegistry { + constructor(defs) { this.defs = new Map(Object.entries(defs || {})); } + get(type = "") { return this.defs.get(String(type || "")) || null; } + has(type = "") { return this.defs.has(String(type || "")); } + label(type = "") { return this.get(type)?.label || type || ""; } + servingTypes() { + return [...this.defs.entries()] + .filter(([, def]) => def.interest && !["grass_like", "water", "corpse"].includes(def.interest)) + .map(([id]) => id); + } + typesByInterest(...interests) { + const wanted = new Set(interests.flat().filter(Boolean)); + return [...this.defs.entries()] + .filter(([, def]) => wanted.has(def.interest)) + .map(([id]) => id); + } + paramEffectTypes() { + return [...this.defs.entries()].filter(([, def]) => def.effectId && !def.effectId.endsWith("_mochi") && def.effectId !== "sleep_drug").map(([id]) => id); + } + passiveDecayMultiplier(type = "") { return this.get(type)?.decayRateMultiplier ?? 1; } + hygienePenalty(type = "") { return this.get(type)?.hygienePenalty ?? 0.055; } + debugSnapshot() { return [...this.defs.entries()].map(([id, def]) => ({ id, label: def.label, interest: def.interest, effectId: def.effectId || "" })); } + } + + const registry = new FoodRegistry(rawDefinitions); + global.TarinaiFoodRegistry = registry; + global.TARINAI_FOOD_DEFINITIONS = Object.freeze(rawDefinitions); +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/health.js b/js/health.js index 7a8eca0..d56d484 100644 --- a/js/health.js +++ b/js/health.js @@ -1,13 +1,14 @@ "use strict"; const HEALTH = (() => { - const MAJOR_DAMAGE_WINDOW = 10; + const MAJOR_DAMAGE_WINDOW = 24; + const WEAKENED_TRACE_WINDOW = 45; const WEAKENED_SUFFIX = "\u3067\u8870\u5f31"; const CAUSES = Object.freeze({ fight: "\u55a7\u5629", hunger: "\u98e2\u9913", stress: "\u30b9\u30c8\u30ec\u30b9\u904e\u591a", - poke: "\u3064\u3064\u304b\u308c\u3059\u304e\u305f", + poke: "\u3064\u3064\u304b\u308c\u3059\u304e", firecracker: "\u7206\u7af9", genkotsu: "\u3052\u3093\u3053\u3064", accident: "\u4e8b\u6545", @@ -19,8 +20,8 @@ const HEALTH = (() => { fightDisease: "\u304d\u305a\u3064\u304d\u75c5", zunchiDisease: "\u305a\u3093\u3061\u75c5", antEaten: "\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f", - giantDrug: "巨大薬", - dwarfDrug: "矮小薬", + giantDrug: "\u5de8\u5927\u85ac", + dwarfDrug: "\u77ee\u5c0f\u85ac", }); const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/; @@ -30,8 +31,8 @@ const HEALTH = (() => { if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, ""); if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight; if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu; - if (/巨大薬|giant/.test(text)) return CAUSES.giantDrug; - if (/矮小薬|dwarf/.test(text)) return CAUSES.dwarfDrug; + if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug; + if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug; if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker; if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds; if (/\u885d\u7a81|collision/.test(text)) return CAUSES.collision; @@ -64,7 +65,7 @@ const HEALTH = (() => { t.lastDamageCause = cause; t.lastDamageAmount = Math.max(0, amount || 0); t.lastDamageAt = now; - if (t.lastDamageAmount >= 10 || t.lastDamageAmount >= Math.max(6, (t.energy || 0) * 0.36)) { + if (t.lastDamageAmount >= 6 || t.lastDamageAmount >= Math.max(4, (t.energy || 0) * 0.22)) { t.lastMajorDamageCause = cause; t.lastMajorDamageAmount = t.lastDamageAmount; t.lastMajorDamageAt = now; @@ -94,14 +95,19 @@ const HEALTH = (() => { const recent = recentDamageCause(t, 20); const fighting = isFightingContext(t, recent); if (recentMajor && t.lastMajorDamageCause === CAUSES.collision) return CAUSES.collision; - if (recentMajor && (!direct || direct === t.lastMajorDamageCause || opts.fromDamage || t.energy <= 22 || t.mood <= 20 || t.stress >= 118)) { + if (recentMajor && (!direct || direct === t.lastMajorDamageCause || opts.fromDamage || t.energy <= 28 || t.mood <= 24 || t.stress >= 112 || t.hunger >= 100)) { return weakenedDeathReasonFor(t.lastMajorDamageCause); } + const weakenedTrace = recentDamageCause(t, WEAKENED_TRACE_WINDOW); + const traceCanWeaken = weakenedTrace && weakenedTrace !== CAUSES.stress && weakenedTrace !== CAUSES.hunger && weakenedTrace !== CAUSES.lifespan && weakenedTrace !== CAUSES.outOfBounds; + if (traceCanWeaken && (!direct || direct === CAUSES.stress || direct === CAUSES.hunger || direct === weakenedTrace || opts.fromDamage) && (t.energy <= 30 || t.mood <= 24 || t.stress >= 112 || t.hunger >= 104)) { + return weakenedDeathReasonFor(weakenedTrace); + } if (direct && direct !== CAUSES.stress) return direct; if (t.zunchiDisease && ((t.zunchiDiseaseSeverity || 0) >= 0.30 || /\u75c5|zunchi/.test(text))) return CAUSES.zunchiDisease; if (t.hunger >= 108 || (t.hunger >= 94 && t.energy <= 26) || /\u98e2|\u7a7a\u8179|hunger/.test(text)) return CAUSES.hunger; if (fighting && (t.energy <= 44 || t.stress >= 112 || t.mood <= 22 || opts.fromDamage)) return CAUSES.fight; - if (recent && recent !== CAUSES.stress && (t.energy <= 34 || t.mood <= 20 || t.stress >= 118 || opts.fromDamage)) return recent; + if (recent && recent !== CAUSES.stress && (t.energy <= 34 || t.mood <= 22 || t.stress >= 116 || opts.fromDamage)) return weakenedDeathReasonFor(recent); if (text.includes(CAUSES.lifespan) || text.includes("\u5bff\u547d")) return CAUSES.lifespan; if (direct) return direct; if (t.stress >= 118 || /\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return CAUSES.stress; @@ -127,6 +133,7 @@ const HEALTH = (() => { let cause = ""; if (actualLoss > 0.01) { cause = rememberDamage(t, actualLoss, reason); + t.world?.emit?.("tarinai:damaged", { tarinai: t, amount: actualLoss, cause, reason }); if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure(); audio.damage?.(actualLoss); if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f"); @@ -149,6 +156,7 @@ const HEALTH = (() => { return Object.freeze({ MAJOR_DAMAGE_WINDOW, + WEAKENED_TRACE_WINDOW, CAUSES, causeLabel, recentDamageCause, diff --git a/js/input_mode_manager.js b/js/input_mode_manager.js new file mode 100644 index 0000000..9d198b4 --- /dev/null +++ b/js/input_mode_manager.js @@ -0,0 +1,119 @@ +"use strict"; + +(function (global) { + const MODES = Object.freeze(["auto", "camera", "tool", "family"]); + const STORAGE_KEY = "tarinai_mobile_input_mode_v1"; + + class InputModeManager { + constructor() { + this.storageKey = STORAGE_KEY; + this.currentMode = this.loadMode(); + this.gestureState = { + active: false, + kind: "", + startedAt: 0, + pointerCount: 0, + }; + this.classification = this.classify(); + } + + loadMode() { + try { + const mode = global.localStorage?.getItem(STORAGE_KEY) || "auto"; + return MODES.includes(mode) ? mode : "auto"; + } catch (_) { + return "auto"; + } + } + + setMode(mode = "auto") { + this.currentMode = MODES.includes(mode) ? mode : "auto"; + try { global.localStorage?.setItem(STORAGE_KEY, this.currentMode); } catch (_) {} + this.emit("input:mode", this.snapshot()); + return this.currentMode; + } + + classify() { + const nav = global.navigator || {}; + const ua = String(nav.userAgent || ""); + const mm = typeof global.matchMedia === "function" ? global.matchMedia.bind(global) : null; + const width = Number(global.innerWidth || 9999); + const height = Number(global.innerHeight || 9999); + const shortSide = Math.min(width, height); + const longSide = Math.max(width, height); + const pointerCoarse = Boolean(mm?.("(pointer: coarse)")?.matches); + const hasHover = Boolean(mm?.("(hover: hover)")?.matches); + const noHover = Boolean(mm?.("(hover: none)")?.matches); + const isTouchDevice = Number(nav.maxTouchPoints || 0) > 0 || "ontouchstart" in global; + const isCompactViewport = shortSide <= 720 && longSide <= 1100; + const userAgentMobile = /Android|iPhone|iPod|iPad|Mobile|Silk|Tablet/i.test(ua); + const touchFirst = Boolean(pointerCoarse && noHover && !hasHover && isTouchDevice && (isCompactViewport || userAgentMobile)); + this.classification = Object.freeze({ + isTouchDevice, + isCompactViewport, + hasHover, + pointerCoarse, + userAgentMobile, + touchFirst, + width, + height, + }); + return this.classification; + } + + refresh() { + const previous = this.classification; + const next = this.classify(); + if (JSON.stringify(previous) !== JSON.stringify(next)) this.emit("input:classification", this.snapshot()); + return next; + } + + beginGesture(kind = "", pointerCount = 1) { + this.gestureState = { + active: true, + kind: String(kind || ""), + startedAt: performance?.now?.() ?? Date.now(), + pointerCount: Math.max(0, Number(pointerCount) || 0), + }; + return this.gestureState; + } + + endGesture() { + this.gestureState = { ...this.gestureState, active: false, pointerCount: 0 }; + return this.gestureState; + } + + canPanCanvas() { + if (!this.classification?.touchFirst) return true; + return this.currentMode === "auto" || this.currentMode === "camera"; + } + + canPlaceTools() { + if (!this.classification?.touchFirst) return true; + return this.currentMode === "auto" || this.currentMode === "tool"; + } + + canFamilyTreeCaptureTouch() { + if (!this.classification?.touchFirst) return true; + return this.currentMode === "auto" || this.currentMode === "family"; + } + + snapshot() { + return Object.freeze({ + ...this.classification, + currentMode: this.currentMode, + gestureState: { ...this.gestureState }, + canPanCanvas: this.canPanCanvas(), + canPlaceTools: this.canPlaceTools(), + canFamilyTreeCaptureTouch: this.canFamilyTreeCaptureTouch(), + }); + } + + emit(type, detail) { + global.TarinaiEvents?.emit?.(type, detail); + } + } + + global.InputModeManager = InputModeManager; + global.TarinaiInputMode = global.TarinaiInputMode || new InputModeManager(); +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/item_visual_registry.js b/js/item_visual_registry.js new file mode 100644 index 0000000..23f750c --- /dev/null +++ b/js/item_visual_registry.js @@ -0,0 +1,129 @@ +"use strict"; + +(function (global) { + const raw = String.raw`{ + "water": { + "renderer": "oval_droplet", + "shape": "horizontal_oval_liquid", + "palette": { "fill": "rgba(86,157,224,0.82)", "stroke": "rgba(45,101,168,0.52)", "highlight": "rgba(255,255,255,0.64)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["horizontal", "oval", "liquid", "blue"] + }, + "zunda_juice": { + "renderer": "oval_droplet", + "shape": "horizontal_oval_liquid", + "palette": { "fill": "rgba(125,218,82,0.94)", "stroke": "rgba(63,150,50,0.62)", "highlight": "rgba(238,255,226,0.72)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["horizontal", "oval", "liquid", "green"] + }, + "mercury": { + "renderer": "oval_droplet", + "shape": "horizontal_oval_liquid", + "palette": { "fill": "rgba(210,216,222,0.94)", "stroke": "rgba(112,122,132,0.62)", "highlight": "rgba(255,255,255,0.72)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["horizontal", "oval", "liquid", "silver"] + }, + "sleep_drug": { + "renderer": "sleep_tablet", + "shape": "moon_tablet", + "palette": { "fill": "#ebe4ff", "stroke": "rgba(74,60,144,0.72)", "accent": "rgba(112,91,188,0.82)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["tablet", "moon", "purple"] + }, + "laxative": { + "renderer": "laxative_tablet", + "shape": "rounded_tablet_zunchi_motif", + "palette": { "fill": "#f4d49a", "stroke": "rgba(98,61,29,0.82)", "accent": "rgba(96,70,38,0.88)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["tablet", "zunchi motif", "tan"] + }, + "mystery_drug": { + "renderer": "mystery_tablet", + "shape": "question_mark_tablet", + "palette": { "fill": "#f3ecff", "stroke": "rgba(91,55,150,0.84)", "accent": "rgba(91,55,150,0.92)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["tablet", "question mark", "purple"] + }, + "ammo": { + "renderer": "bullet", + "shape": "bullet", + "palette": { "fill": "#f4c46a", "stroke": "rgba(87,51,22,0.78)", "highlight": "rgba(255,237,176,0.62)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["bullet", "gold"] + }, + "protein": { + "renderer": "powder_pile", + "shape": "powder_pile", + "palette": { "fill": "#f4a24f", "stroke": "#b45b27", "highlight": "#ffe2b9" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["powder", "orange"] + }, + "niteropu": { + "renderer": "powder_pile", + "shape": "powder_pile", + "palette": { "fill": "#8a79d0", "stroke": "#4c3f91", "highlight": "#d8d1ff" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["powder", "purple"] + }, + "giant_drug": { + "renderer": "split_pill", + "shape": "split_pill", + "palette": { "fill": "#ffe3cf", "stroke": "#c9632f", "highlight": "#ffb47b" }, + "scale": 1.08, + "uiPreviewScale": 1.08, + "fieldScale": 1.08, + "accessibleShapeTraits": ["pill", "large", "orange"] + }, + "dwarf_drug": { + "renderer": "split_pill", + "shape": "split_pill", + "palette": { "fill": "#e9e9ff", "stroke": "#6967ac", "highlight": "#b8b5f4" }, + "scale": 0.92, + "uiPreviewScale": 0.92, + "fieldScale": 0.92, + "accessibleShapeTraits": ["pill", "small", "purple"] + }, + "ball": { + "renderer": "emoji", + "shape": "soccer_ball", + "palette": {}, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["round", "soccer ball"], + "emojiFallback": "\u26bd" + } + }`; + + const defs = Object.freeze(JSON.parse(raw)); + + function itemVisualDefinition(type = "") { + return defs[String(type || "")] || null; + } + + function itemVisualPalette(type = "") { + return itemVisualDefinition(type)?.palette || null; + } + + global.TARINAI_ITEM_VISUALS = defs; + global.itemVisualDefinition = itemVisualDefinition; + global.itemVisualPalette = itemVisualPalette; +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/items.js b/js/items.js index 15230b3..0784fda 100644 --- a/js/items.js +++ b/js/items.js @@ -1,8 +1,8 @@ // tarinai_colony_game build 15.6.2: food servings, nest box access, and editable tarinai data "use strict"; -const FOOD_SERVING_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); -const PARAM_EFFECT_ITEM_TYPES = new Set(["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); +const FOOD_SERVING_TYPES = new Set(window.TarinaiFoodRegistry?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); +const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiFoodRegistry?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]); function isParamEffectItemType(type = "") { return PARAM_EFFECT_ITEM_TYPES.has(type); } @@ -23,7 +23,7 @@ function passiveFoodDecayInterval(worldRef = null) { function passiveFoodDecayRate(item) { const size = item?.toolSize || "medium"; const sizeMul = size === "small" ? 0.74 : (size === "large" ? 1.32 : 1); - const typeMul = item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1); + const typeMul = window.TarinaiFoodRegistry?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1)); return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul; } @@ -232,6 +232,24 @@ class Item { this.lingerTimer = 0; this.lingerDuration = 2.0; } + if (type === "pushpin") { + this.amount = 100; + this.vx = rand(-120, 120); + this.vy = rand(-90, 90); + this.prevX = x; + this.prevY = y; + this.spin = rand(-0.35, 0.35); + this.spinVelocity = rand(-5.4, 5.4); + this.pinState = "loose"; + this.pinTargetId = ""; + this.pinAttachAngle = 0; + this.pinAttachDistance = 0; + this.pinOffsetY = 0; + this.pinDamageTick = 0; + this.pinFallCheckTimer = 0; + this.pinLogAt = -999; + this.deletable = true; + } if (isServingFoodType(type)) { this.toolSize = "medium"; this.foodServingScale = 1; @@ -265,8 +283,10 @@ class Item { if (lost > 0) { this.foodServingsRemaining = Math.max(0, before - lost); this.amount = this.foodServingsRemaining; - worldRef?.registerFoodSpoilage?.(lost * PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING, this); + const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING; + worldRef?.registerFoodSpoilage?.(lost * penalty, this); worldRef?.markTerrainDirty?.("food-passive-decay"); + worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true }); if (this.foodServingsRemaining <= 0.015) this.amount = 0; } } else if (["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(this.type)) { @@ -274,8 +294,10 @@ class Item { const lost = Math.min(before, interval * 0.12); this.amount -= lost; if (lost > 0) { - worldRef?.registerFoodSpoilage?.(lost * 0.002, this); + const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? 0.002; + worldRef?.registerFoodSpoilage?.(lost * penalty, this); worldRef?.markTerrainDirty?.("food-passive-decay"); + worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true }); } } } @@ -300,6 +322,7 @@ class Item { } } if (this.type === "ball") this.updateBall(dt, worldRef); + if (this.type === "pushpin") this.updatePushpin(dt, worldRef); if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef); if (this.type === "grass") { this.lifecycleTimer += dt; @@ -542,6 +565,157 @@ class Item { worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" })); } + updatePushpin(dt, worldRef) { + if (!worldRef) return; + if (this.pinState === "lodged") { + this.updateLodgedPushpin(dt, worldRef); + return; + } + this.prevX = this.x; + this.prevY = this.y; + const moving = Math.hypot(this.vx || 0, this.vy || 0); + if (moving > 0.05) { + this.x += (this.vx || 0) * dt; + this.y += (this.vy || 0) * dt; + const p = Math.max(26, CONFIG.worldPadding || 30); + if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; } + if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; } + if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; } + if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; } + const friction = Math.pow(0.18, dt * 0.85); + this.vx *= friction; + this.vy *= friction; + this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(7, this.r || 16); + this.spinVelocity *= Math.pow(0.38, dt); + if (Math.hypot(this.vx || 0, this.vy || 0) < 7.5) { this.vx = 0; this.vy = 0; this.spinVelocity *= 0.3; } + worldRef.drawListDirty = true; + } else { + this.vx = 0; + this.vy = 0; + this.spinVelocity *= Math.pow(0.08, dt); + this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt; + } + this.tryStickPushpin(worldRef); + } + + tryStickPushpin(worldRef) { + if (!worldRef?.tarinai?.length) return false; + if (this.pinState === "lodged") return false; + let best = null; + let bestD = Infinity; + const hitRange = Math.max(10, (this.r || 8) * 1.25); + for (const t of worldRef.tarinai) { + if (!t || t.dead) continue; + const d = distXY(this.x, this.y, t.x, t.y); + const hit = (t.radius || 16) * 0.86 + hitRange; + if (d <= hit && d < bestD) { best = t; bestD = d; } + } + if (!best) return false; + return this.attachPushpin(best, worldRef, bestD); + } + + attachPushpin(t, worldRef, d = null) { + if (!t || t.dead) return false; + if (t.stuckPushpinId && t.stuckPushpinId !== this.id) return false; + const dx = this.x - t.x; + const dy = this.y - t.y; + const distToTarget = Number.isFinite(d) ? d : Math.hypot(dx, dy); + this.pinState = "lodged"; + this.deletable = false; + this.pinTargetId = t.id; + this.pinAttachAngle = Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1)); + this.pinAttachDistance = clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74); + this.pinOffsetY = clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38); + this.pinDamageTick = 0; + this.pinFallCheckTimer = 0; + this.vx = 0; + this.vy = 0; + this.spinVelocity = 0; + this.spin = this.pinAttachAngle; + t.stuckPushpinId = this.id; + t.sleeping = false; + t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7); + t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2); + t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8); + t.fearTimer = Math.max(t.fearTimer || 0, 1.3); + t.state = "panic"; + t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }; + t.addStress ? t.addStress(18, { threshold: 8 }) : (t.stress = clamp((t.stress || 0) + 18, 0, 130)); + if (t.damage) t.damage(6, "\u753b\u92f2"); + if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) { + this.pinLogAt = worldRef.time || 0; + worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] }); + } + worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" })); + return true; + } + + updateLodgedPushpin(dt, worldRef) { + const t = worldRef.tarinai?.find(o => o && !o.dead && o.id === this.pinTargetId) || null; + if (!t) { + this.detachPushpin(worldRef, "owner-lost"); + return; + } + t.stuckPushpinId = this.id; + this.deletable = false; + this.x = t.x + (t.radius || 16) * 0.36; + this.y = t.y - (t.radius || 16) * 0.04; + this.prevX = this.x; + this.prevY = this.y; + this.spin = 0.28; + this.vx = t.vx || 0; + this.vy = t.vy || 0; + this.pinDamageTick = (this.pinDamageTick || 0) + dt; + while (this.pinDamageTick >= 0.55) { + this.pinDamageTick -= 0.55; + if (t.damage) t.damage(1.4, "\u753b\u92f2"); + t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50); + t.addStress ? t.addStress(2.6, { threshold: 8, duration: 3.4 }) : (t.stress = clamp((t.stress || 0) + 2.6, 0, 130)); + if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)"); + } + t.sleeping = false; + t.state = "panic"; + t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }; + t.fearTimer = Math.max(t.fearTimer || 0, 0.65); + t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22); + t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4); + t.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b"; + this.pinFallCheckTimer = (this.pinFallCheckTimer || 0) + dt; + while (this.pinFallCheckTimer >= 1.0) { + this.pinFallCheckTimer -= 1.0; + if (Math.random() < 0.10) { + this.detachPushpin(worldRef, "fall"); + return; + } + } + } + + detachPushpin(worldRef, reason = "released") { + const t = worldRef?.tarinai?.find(o => o && o.id === this.pinTargetId) || null; + if (t && t.stuckPushpinId === this.id) t.stuckPushpinId = null; + this.pinState = "loose"; + this.pinTargetId = ""; + this.pinDamageTick = 0; + this.pinFallCheckTimer = 0; + this.deletable = true; + if (reason === "pinch") { + this.vx = 0; + this.vy = 0; + this.spinVelocity = 0; + } else { + this.vx = rand(-24, 24); + this.vy = rand(-10, 18); + this.spinVelocity = rand(-1.6, 1.6); + this.spin += rand(-0.25, 0.25); + if (t) { + this.x = clamp(t.x + rand(-(t.radius || 16) * 0.75, (t.radius || 16) * 0.75), CONFIG.worldPadding || 30, (worldRef?.w || this.x) - (CONFIG.worldPadding || 30)); + this.y = clamp(t.y + rand((t.radius || 16) * 0.12, (t.radius || 16) * 0.72), CONFIG.worldPadding || 30, (worldRef?.h || this.y) - (CONFIG.worldPadding || 30)); + } + worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, (this.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" })); + if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306e\u753b\u92f2\u304c\u629c\u3051\u843d\u3061\u305f\u3002`, "observe", { participants: [t] }); + } + } + updateGrass(dt, worldRef) { const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7; const weather = worldRef.weather || "sunny"; @@ -754,18 +928,16 @@ class Item { } } } else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(this.type)) { + const visual = typeof itemVisualDefinition === "function" ? itemVisualDefinition(this.type) : null; + const palette = visual?.palette || {}; + const renderer = visual?.renderer || "split_pill"; const meta = { - laxative: { fill: "#f1d18a", stroke: "#774c1f", accent: "#5f3a1b", kind: "laxative" }, - protein: { fill: "#f4a24f", stroke: "#b45b27", accent: "#ffe2b9", kind: "powder" }, - niteropu: { fill: "#8a79d0", stroke: "#4c3f91", accent: "#d8d1ff", kind: "powder" }, - ammo: { fill: "#f4c46a", stroke: "#6f431f", accent: "#ffe8a8", kind: "bullet" }, - mystery_drug: { fill: "#dff4ff", stroke: "#27587b", accent: "#437c97", kind: "mystery" }, - mercury: { fill: "rgba(210,216,222,0.94)", stroke: "rgba(112,122,132,0.62)", accent: "rgba(255,255,255,0.72)", kind: "droplet" }, - giant_drug: { fill: "#ffe3cf", stroke: "#c9632f", accent: "#ffb47b", kind: "pill" }, - dwarf_drug: { fill: "#e9e9ff", stroke: "#6967ac", accent: "#b8b5f4", kind: "pill" }, - zunda_juice: { fill: "rgba(125,218,82,0.94)", stroke: "rgba(63,150,50,0.62)", accent: "rgba(238,255,226,0.72)", kind: "droplet" }, - }[this.type] || { fill: "#fffaf1", stroke: "#8d6f48", accent: "#ddd", kind: "pill" }; - const localR = this.r * (this.type === "giant_drug" ? 1.08 : (this.type === "dwarf_drug" ? 0.92 : 1)); + fill: palette.fill || "#fffaf1", + stroke: palette.stroke || "#8d6f48", + accent: palette.highlight || palette.accent || "#ddd", + kind: renderer === "powder_pile" ? "powder" : renderer === "oval_droplet" ? "droplet" : renderer === "laxative_tablet" ? "laxative" : renderer === "mystery_tablet" ? "mystery" : renderer === "bullet" ? "bullet" : "pill", + }; + const localR = this.r * (visual?.fieldScale || visual?.scale || 1); ctx.rotate(Math.sin(this.seed) * 0.10); if (meta.kind === "powder") { ctx.save(); @@ -873,6 +1045,50 @@ class Item { ctx.fill(); } ctx.restore(); + } else if (this.type === "pushpin") { + const stuck = this.pinState === "lodged"; + const assetId = stuck ? "pushpin_stuck" : "pushpin"; + const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId); + ctx.save(); + if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5); + ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)"; + ctx.shadowBlur = 3; + ctx.shadowOffsetY = 2; + if (img) { + const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null; + const ratio = metrics?.ratio || (stuck ? 1 : 2.0); + const w = stuck ? this.r * 2.15 : this.r * 1.78; + const h = w * ratio; + ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h); + } else if (stuck) { + ctx.fillStyle = "#d92736"; + ctx.strokeStyle = "rgba(125,18,30,0.78)"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(0, 0, this.r * 0.68, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, this.r * 0.26, 0, Math.PI * 2); + ctx.fillStyle = "rgba(176,20,30,0.82)"; + ctx.fill(); + } else { + ctx.fillStyle = "#d92736"; + ctx.strokeStyle = "rgba(125,18,30,0.78)"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.ellipse(0, -this.r * 0.26, this.r * 0.70, this.r * 0.48, 0, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + ctx.beginPath(); + ctx.ellipse(0, this.r * 0.18, this.r * 0.82, this.r * 0.30, 0, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + ctx.strokeStyle = "rgba(210,210,215,0.92)"; + ctx.lineWidth = Math.max(1.5, this.r * 0.12); + ctx.beginPath(); + ctx.moveTo(0, this.r * 0.42); + ctx.lineTo(0, this.r * 1.65); + ctx.stroke(); + } + ctx.restore(); } else if (this.type === "water_bowl") { ctx.fillStyle = "rgba(171, 120, 76, 0.72)"; ctx.strokeStyle = "rgba(94, 69, 48, 0.55)"; diff --git a/js/main.js b/js/main.js index 5ad3ed3..85ae9cd 100644 --- a/js/main.js +++ b/js/main.js @@ -18,6 +18,7 @@ const LOADING_SPRITES = [ "assets/sprites/tarinai_08_pokan.webp", "assets/sprites/tarinai_11_weak.webp", "assets/sprites/tarinai_21_normal_happy.webp", + "assets/sprites/tarinai_29_hungry_70.webp", ]; let loadingSpriteTimer = 0; @@ -133,8 +134,9 @@ loadImages(null, (done, total) => { if ("serviceWorker" in navigator) { window.addEventListener("load", () => { - const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.3"; + const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.11"; navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined); }); } + diff --git a/js/performance_manager.js b/js/performance_manager.js index d7c9b3f..8c6e27b 100644 --- a/js/performance_manager.js +++ b/js/performance_manager.js @@ -35,7 +35,8 @@ class TarinaiPerformanceManager { quality(worldRef = null) { const level = this.level; - const antCount = (worldRef?.ants || []).reduce((n, a) => n + (a && !a.dead ? 1 : 0), 0); + let antCount = 0; + for (const ant of worldRef?.ants || []) if (ant && !ant.dead) antCount += 1; const itemCount = (worldRef?.items || []).length || 0; const effectCount = (worldRef?.effects || []).length || 0; const antStride = antCount >= 95 || level >= 3 ? 3 : (antCount >= 35 || level >= 2 ? 2 : 1); @@ -46,7 +47,7 @@ class TarinaiPerformanceManager { itemCount, antCount, effectCount, - effectLimit: level >= 3 ? 42 : level >= 2 ? 64 : level >= 1 ? 84 : 112, + effectLimit: level >= 3 ? 34 : level >= 2 ? 52 : level >= 1 ? 68 : 88, statsInterval: level >= 3 ? 1.20 : level === 2 ? 0.92 : level === 1 ? 0.68 : 0.48, terrainRedrawInterval: level >= 3 ? 1.30 : level === 2 ? 1.05 : level === 1 ? 0.82 : 0.62, backgroundLightStep: level >= 3 ? 64 : level === 2 ? 96 : 180, diff --git a/js/render.js b/js/render.js index 167b9df..5328e3b 100644 --- a/js/render.js +++ b/js/render.js @@ -131,11 +131,13 @@ function placementPreviewFor(world) { const pad = CONFIG.worldPadding || 30; let x = p.x; let y = p.y; - const r = itemRadiusFor(type, 12); + const sizeScale = world?.toolSizeScale ? world.toolSizeScale(type) : 1; + const sizeName = world?.toolSizeFor ? world.toolSizeFor(type) : "medium"; + const r = itemRadiusFor(type, 12) * sizeScale; const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad)); const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad; let blocked = false; - let tmp = { type, x, y, r, dead: false }; + let tmp = { type, x, y, r, toolSize: sizeName, foodServingScale: sizeScale, dead: false }; if ((type === "fence_v" || type === "fence_h") && world.fenceRect) { const rawRect = world.fenceRect(tmp); return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) || world.fencePlacementBlocked?.(tmp) }; @@ -173,14 +175,14 @@ function drawPlacementPreview(ctx, world) { ctx.strokeRect(part.left, part.top, part.right - part.left, part.bottom - part.top); } } - } else if (type === "water_bowl" || type === "stone" || type === "ball" || type === "firecracker" || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") { + } else if (type === "water_bowl" || type === "stone" || type === "ball" || type === "firecracker" || type === "pushpin" || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") { ctx.beginPath(); - ctx.arc(x, y, Math.max(16, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2); + ctx.arc(x, y, Math.max(8, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else { ctx.beginPath(); - ctx.ellipse(x, y, Math.max(14, r * 1.55), Math.max(9, r * 0.86), 0, 0, Math.PI * 2); + ctx.ellipse(x, y, Math.max(9, r * 1.55), Math.max(6, r * 0.86), 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } @@ -740,6 +742,7 @@ function render() { } for (const it of world.items) { if (isCachedTerrainItem(it)) continue; + if (it.type === "pushpin" && it.pinState === "lodged") continue; if (it.type !== "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); } for (const it of world.items) { @@ -766,6 +769,9 @@ function render() { for (const t of sorted) { if (isEntityVisibleInRect(t, visibleRect)) t.draw(ctx, world.time, lighting); } + for (const it of world.items) { + if (it.type === "pushpin" && it.pinState === "lodged" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + } for (const ef of world.effects) { if (ef.type !== "bleed" && isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx); } @@ -837,11 +843,8 @@ function drawNestBoxTooltip(ctx, world) { const info = world.pointerNestBoxInfo?.(); if (!info) return; const box = info.box; - const capacity = info.capacity || world.nestBoxCapacity?.(box) || 5; - const title = `\u5de3\u7bb1 (${info.occupants.length}/${capacity})\u5339`; - const lines = info.occupants.length - ? [title, ...info.occupants.map(t => t.name)] - : [title, "\u4e2d\u306b\u306f\u8ab0\u3082\u3044\u306a\u3044"]; + const title = "\u5de3\u7bb1"; + const lines = [title]; ctx.save(); ctx.font = "12px Yomogi, sans-serif"; const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0); @@ -889,7 +892,7 @@ function drawSelectedCard(ctx, world, lighting) { const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : ""; ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35); const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : ""; - ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 52); + ctx.fillText(`\u7dcf\u5408\u7684\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 52); ctx.fillText(`\u30b9\u30c8\u30ec\u30b9 ${fmt(t.stress)} \u7a7a\u8179 ${fmt(t.hunger)}`, x + 10, y + 68); ctx.restore(); } diff --git a/js/sim_core.js b/js/sim_core.js index c9ff611..fcaf5b9 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -64,6 +64,62 @@ function stableUnit(seed, salt = "") { return ((h >>> 0) % 100000) / 100000; } +const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]; +const GENETIC_LIMITS = Object.freeze({ + lifeSpanMul: [0.82, 1.24], + attackMul: [0.80, 1.24], + speedMul: [0.86, 1.18], + sizeMul: [0.84, 1.18], +}); + +function clampGeneticValue(key, value) { + const range = GENETIC_LIMITS[key] || [0.75, 1.25]; + return clamp(Number(value) || 1, range[0], range[1]); +} + +function randomGenetics(seed = "") { + return { + lifeSpanMul: clampGeneticValue("lifeSpanMul", 0.82 + stableUnit(seed, "gene-life") * 0.42), + attackMul: clampGeneticValue("attackMul", 0.80 + stableUnit(seed, "gene-attack") * 0.44), + speedMul: clampGeneticValue("speedMul", 0.86 + stableUnit(seed, "gene-speed") * 0.32), + sizeMul: clampGeneticValue("sizeMul", 0.84 + stableUnit(seed, "gene-size") * 0.34), + }; +} + +function normalizeGenetics(source = null, seed = "") { + const fallback = randomGenetics(seed); + const out = {}; + for (const key of GENETIC_KEYS) out[key] = clampGeneticValue(key, source?.[key] ?? fallback[key]); + return out; +} + +function inheritedGenetics(a, b, seed = "") { + const ga = normalizeGenetics(a?.genetics, a?.familyKey || a?.id || "a"); + const gb = normalizeGenetics(b?.genetics, b?.familyKey || b?.id || "b"); + const out = {}; + for (const key of GENETIC_KEYS) { + const base = ((ga[key] || 1) + (gb[key] || 1)) / 2; + const mutation = (stableUnit(seed, `gene-mut-${key}`) - 0.5) * 0.10; + out[key] = clampGeneticValue(key, base + mutation); + } + return out; +} + +function adultScaleFromGenetics(seed = "", genetics = null) { + const g = normalizeGenetics(genetics, seed); + return clamp((0.265 + stableUnit(seed, "adult-scale") * 0.060) * g.sizeMul, 0.205, 0.390); +} + +function childGrowthScaleFactor(growth = 1) { + const x = clamp(Number(growth) || 0, 0, 1); + const smooth = x * x * (3 - 2 * x); + return 0.46 + smooth * 0.54; +} + +function scaleForGrowth(adultScale = 0.28, growth = 1) { + return clamp((Number(adultScale) || 0.28) * childGrowthScaleFactor(growth), 0.10, 0.42); +} + const PERSONALITIES = Object.freeze({}); const PERSONALITY_IDS = []; @@ -73,7 +129,7 @@ function personalityForSeed(seed) { const PERSONALITY_KEYS = ["aggression", "openness", "sociability", "neuroticism"]; const PERSONALITY_DAILY_LIMIT = { perAxis: 0.08, total: 0.20 }; -const FRIEND_AFFINITY_THRESHOLD = 16; +const FRIEND_AFFINITY_THRESHOLD = 14; const PERSONALITY_LABELS = { aggression: { label: "\u653b\u6483\u6027", low: "\u304a\u3060\u3084\u304b", high: "\u6012\u308a\u3063\u307d\u3044" }, openness: { label: "\u958b\u653e\u6027", low: "\u4fdd\u5b88\u7684", high: "\u904a\u3073\u597d\u304d" }, @@ -91,6 +147,28 @@ function normalizePersonality(source = null) { return out; } +function zunchiSlavePersonalityValue() { + const out = {}; + for (const key of PERSONALITY_KEYS) out[key] = -1; + return out; +} + +function applyZunchiSlavePersonality(tarinai, opts = {}) { + if (!tarinai) return false; + const next = zunchiSlavePersonalityValue(); + let changed = false; + tarinai.birthPersonality = normalizePersonality(tarinai.birthPersonality || next); + tarinai.currentPersonality = normalizePersonality(tarinai.currentPersonality || next); + for (const key of PERSONALITY_KEYS) { + if ((tarinai.currentPersonality[key] || 0) !== -1) changed = true; + tarinai.currentPersonality[key] = -1; + if (opts.birth) tarinai.birthPersonality[key] = -1; + } + tarinai.personalityDaily = { day: tarinai.world?.day || 1, total: 0, byKey: {}, byCause: {} }; + if (changed && opts.record !== false) tarinai.recordChangeCause?.("\u305a\u3093\u3061\u3069\u308c\u3044\u5316", "\u6027\u683c", { value: -1 }); + return changed; +} + function randomBirthPersonality(seed = "") { const out = {}; for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(stableUnit(seed, `personality-${key}`) * 1.2 - 0.6); @@ -137,6 +215,10 @@ function personalityReasonText(reason = "") { "after repeated contact with poop": "\u305a\u3093\u3061\u306b\u6163\u308c\u305f\u5f71\u97ff\u3067", "after repeated contact with corpses": "\u6b7b\u9ab8\u306b\u6163\u308c\u305f\u5f71\u97ff\u3067", "after being hurt": "\u75db\u307f\u3092\u899a\u3048\u305f\u5f71\u97ff\u3067", + "after ammo knockback": "\u5f3e\u85ac\u3067\u5f3e\u304d\u98db\u3070\u3057\u305f\u5f71\u97ff\u3067", + "after successful intimidation": "\u5a01\u5687\u306b\u6210\u529f\u3057\u305f\u5f71\u97ff\u3067", + "after failed intimidation": "\u5a01\u5687\u306b\u5931\u6557\u3057\u305f\u5f71\u97ff\u3067", + "after being struck by genkotsu": "\u3052\u3093\u3053\u3064\u3092\u53d7\u3051\u305f\u5f71\u97ff\u3067", }; return map[key] || key; } @@ -171,14 +253,31 @@ function personalityDailyState(tarinai) { const day = tarinai?.world?.day || 1; const state = tarinai.personalityDaily || {}; if (state.day !== day) { - tarinai.personalityDaily = { day, total: 0, byKey: {} }; + tarinai.personalityDaily = { day, total: 0, byKey: {}, byCause: {} }; return tarinai.personalityDaily; } state.byKey = state.byKey || {}; for (const key of Object.keys(state.byKey)) { const v = state.byKey[key]; - if (typeof v === "number") state.byKey[key] = { up: Math.max(0, v), down: Math.max(0, -v) }; - else state.byKey[key] = { up: Math.max(0, Number(v?.up) || 0), down: Math.max(0, Number(v?.down) || 0) }; + if (typeof v === "number") state.byKey[key] = { up: Math.max(0, v), down: Math.max(0, -v), causes: [] }; + else { + const causes = Array.isArray(v?.causes) ? [...new Set(v.causes.map(c => String(c || "").trim()).filter(Boolean))].slice(0, 4) : []; + state.byKey[key] = { up: Math.max(0, Number(v?.up) || 0), down: Math.max(0, Number(v?.down) || 0), causes }; + } + } + state.byCause = state.byCause && typeof state.byCause === "object" ? state.byCause : {}; + for (const cause of Object.keys(state.byCause)) { + const entry = state.byCause[cause] || {}; + const byKey = entry.byKey && typeof entry.byKey === "object" ? entry.byKey : {}; + const nextByKey = {}; + for (const key of PERSONALITY_KEYS) { + const v = byKey[key] || {}; + const up = Math.max(0, Number(v.up) || 0); + const down = Math.max(0, Number(v.down) || 0); + if (up > 0.0001 || down > 0.0001) nextByKey[key] = { up, down }; + } + if (Object.keys(nextByKey).length) state.byCause[cause] = { byKey: nextByKey }; + else delete state.byCause[cause]; } state.total = Math.max(0, Number(state.total) || 0); tarinai.personalityDaily = state; @@ -229,8 +328,23 @@ function adjustPersonality(tarinai, key, delta, reason = "") { if (!actual) return 0; tarinai.currentPersonality[key] = after; state.byKey[key] = used; + if (!Array.isArray(used.causes)) used.causes = []; used[actual > 0 ? "up" : "down"] = (used[actual > 0 ? "up" : "down"] || 0) + Math.abs(actual); state.total = (state.total || 0) + Math.abs(actual); + const label = PERSONALITY_LABELS[key]?.label || key; + const reasonText = personalityReasonText(reason) || "\u6027\u683c\u5909\u5316"; + if (reasonText && !used.causes.includes(reasonText)) used.causes.push(reasonText); + if (used.causes.length > 4) used.causes.length = 4; + if (reasonText) { + state.byCause = state.byCause && typeof state.byCause === "object" ? state.byCause : {}; + const causeEntry = state.byCause[reasonText] || { byKey: {} }; + causeEntry.byKey = causeEntry.byKey && typeof causeEntry.byKey === "object" ? causeEntry.byKey : {}; + const causeKeyEntry = causeEntry.byKey[key] || { up: 0, down: 0 }; + causeKeyEntry[actual > 0 ? "up" : "down"] = (causeKeyEntry[actual > 0 ? "up" : "down"] || 0) + Math.abs(actual); + causeEntry.byKey[key] = causeKeyEntry; + state.byCause[reasonText] = causeEntry; + } + tarinai.recordChangeCause?.(reasonText, label, { value: actual }); const afterBand = personalityBand(after); if (beforeBand !== afterBand) { const text = personalityThresholdLogText(tarinai, key, after, reason, beforeBand); @@ -458,7 +572,7 @@ class SpatialGrid { const type = it.type || ""; if (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h") this.add(this.obstacleCells, it); if (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "water_bowl" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))) this.add(this.foodCells, it); - if (type === "firecracker" || type === "genkotsu" || type === "sleep_drug" || type === "zunchi" || type === "splat" || type === "mystery_drug" || type === "laxative" || type === "niteropu" || type === "mercury" || type === "giant_drug" || type === "dwarf_drug") this.add(this.hazardCells, it); + if (type === "firecracker" || type === "genkotsu" || type === "pushpin" || type === "sleep_drug" || type === "zunchi" || type === "splat" || type === "mystery_drug" || type === "laxative" || type === "niteropu" || type === "mercury" || type === "giant_drug" || type === "dwarf_drug") this.add(this.hazardCells, it); } rebuild(items, tarinai, ants = []) { diff --git a/js/sound_pack.js b/js/sound_pack.js index b187823..40682ea 100644 --- a/js/sound_pack.js +++ b/js/sound_pack.js @@ -41,9 +41,21 @@ sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3" }, categories: { - voice: "たりないの声", - notify: "通知", - ops: "操作" + voice: "\u305f\u308a\u306a\u3044\u306e\u58f0", + notify: "\u901a\u77e5", + ops: "\u64cd\u4f5c" + }, + soundMap: { + voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", + sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice", + sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice", + sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice", + sfx_notify: "notify", sfx_phase: "notify", sfx_log_push: "notify", + sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops", + sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops", + sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops", + sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops", + sfx_zunchi: "ops", sfx_ant_nest: "ops" } }); })(typeof window !== "undefined" ? window : globalThis); diff --git a/js/tarinai.js b/js/tarinai.js index 6542aa1..75acc60 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -30,19 +30,25 @@ class Tarinai { constructor(world, opts = {}) { this.currentPersonality = normalizePersonality(opts.currentPersonality || opts.personalityCurrent || this.birthPersonality); this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null; ensurePersonality(this); + this.genetics = normalizeGenetics(opts.genetics, this.familyKey); this.trait = makeTrait(this.type); + this.trait.speed *= this.genetics.speedMul || 1; + this.birthTime = opts.birthTime ?? this.world.time; + const generationOpt = opts.generation ?? 1; + const growthAtConstruct = generationOpt > 1 ? clamp(((this.world?.time || 0) - this.birthTime) / Math.max(1, CONFIG.childGrowTime || 120), 0, 1) : 1; this.x = opts.x ?? rand(100, world.w - 100); this.y = opts.y ?? rand(100, world.h - 100); this.vx = opts.vx ?? rand(-12, 12); this.vy = opts.vy ?? rand(-12, 12); this.impulseVx = opts.impulseVx ?? 0; this.impulseVy = opts.impulseVy ?? 0; - this.scale = opts.scale ?? rand(0.22, 0.33); + this.adultScale = Number.isFinite(opts.adultScale) ? opts.adultScale : (Number.isFinite(opts.baseAdultScale) ? opts.baseAdultScale : (Number.isFinite(opts.scale) && growthAtConstruct >= 1 ? opts.scale : adultScaleFromGenetics(this.familyKey, this.genetics))); + this.scale = Number.isFinite(opts.scale) ? opts.scale : (generationOpt > 1 ? scaleForGrowth(this.adultScale, growthAtConstruct) : this.adultScale); this.radius = 56 * this.scale; this.age = opts.age ?? rand(0, 20); - this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200); - if (this.lifeSpan < 1200) this.lifeSpan = rand(1400, 2200); - this.generation = opts.generation ?? 1; + this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200) * (this.genetics.lifeSpanMul || 1); + if (this.lifeSpan < 900) this.lifeSpan = rand(1200, 1800) * (this.genetics.lifeSpanMul || 1); + this.generation = generationOpt; this.hasPaired = opts.hasPaired ?? false; this.hunger = opts.hunger ?? rand(16, 45); this.loneliness = opts.loneliness ?? rand(18, 62); @@ -104,14 +110,15 @@ class Tarinai { constructor(world, opts = {}) { this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0; this.totalFightLosses = opts.totalFightLosses || 0; this.totalFightWins = opts.totalFightWins || 0; - this.isZunchiSlave = Boolean(opts.isZunchiSlave); + this.isZunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked); + this.zunchiSlaveLocked = Boolean(opts.zunchiSlaveLocked || this.isZunchiSlave); this.formerName = opts.formerName || null; this.loveMochiTimer = opts.loveMochiTimer ?? 0; this.fightMochiTimer = opts.fightMochiTimer ?? 0; this.sleepDisease = Boolean(opts.sleepDisease); this.sleepDiseaseCooldown = opts.sleepDiseaseCooldown ?? 0; this.explosionDisease = Boolean(opts.explosionDisease); - this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? (CONFIG.dayLength || 120) * 0.5 : 0); + this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? 60 : 0); this.fightDisease = Boolean(opts.fightDisease); this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0; this.lastBleedAt = opts.lastBleedAt ?? -999; @@ -121,13 +128,17 @@ class Tarinai { constructor(world, opts = {}) { this.nestBoxEnteredAt = opts.nestBoxEnteredAt ?? (this.insideNestBoxId ? (world?.time || 0) : -Infinity); this.nestBoxStayUntil = opts.nestBoxStayUntil ?? (this.insideNestBoxId ? ((world?.time || 0) + 10) : -Infinity); this.focusPulseTimer = opts.focusPulseTimer ?? 0; - if (this.isZunchiSlave) this.name = "\u305a\u3093\u3061\u3069\u308c\u3044"; + if (this.isZunchiSlave) { + this.zunchiSlaveLocked = true; + this.name = "\u305a\u3093\u3061\u3069\u308c\u3044"; + applyZunchiSlavePersonality(this, { record: false, birth: false }); + this.visibleSpriteId = "zunchi_slave"; + } this.tempComfort = opts.tempComfort ?? 0.62; this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55; this.parents = opts.parents || []; this.parentNames = opts.parentNames || []; this.children = opts.children || []; - this.birthTime = opts.birthTime ?? this.world.time; this.nextEatSound = opts.nextEatSound ?? 0; this.fightTimer = opts.fightTimer ?? 0; this.fightCooldown = opts.fightCooldown ?? rand(0, CONFIG.fightCooldown); @@ -140,6 +151,7 @@ class Tarinai { constructor(world, opts = {}) { this.lastMealColor = opts.lastMealColor || "#f7cf67"; this.grassEatTimer = opts.grassEatTimer ?? 0; this.hurtTimer = opts.hurtTimer ?? 0; + this.stuckPushpinId = opts.stuckPushpinId || null; this.defeatedTimer = opts.defeatedTimer ?? 0; this.defeatedById = opts.defeatedById || null; this.fightWinnerId = opts.fightWinnerId || null; @@ -161,6 +173,7 @@ class Tarinai { constructor(world, opts = {}) { this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22; this.envTimer = opts.envTimer ?? stableUnit(this.familyKey, "env-start") * 0.55; this.visibleSpriteId = opts.visibleSpriteId || null; + this.zunchiSlaveSpriteVariant = opts.zunchiSlaveSpriteVariant || (opts.isZunchiSlave ? (Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt") : ""); this.spriteLockUntil = opts.spriteLockUntil ?? 0; this.entryTimer = opts.entryTimer ?? 0; const effectTimers = opts.itemEffectTimers && typeof opts.itemEffectTimers === "object" ? opts.itemEffectTimers : {}; @@ -179,6 +192,7 @@ class Tarinai { constructor(world, opts = {}) { this.powerEffectPulse = opts.powerEffectPulse || 0; this.powerEffectKind = opts.powerEffectKind || this.powerItemMode || this.sizeItemMode || this.lifeItemMode || ""; this.mysteryDrugHistory = Array.isArray(opts.mysteryDrugHistory) ? opts.mysteryDrugHistory.slice(-8) : []; + this.recentChangeCauses = Array.isArray(opts.recentChangeCauses) ? opts.recentChangeCauses.slice(-8) : []; if (this.refreshEffectiveSize) this.refreshEffectiveSize(); } } diff --git a/js/tarinai_disease_nest.js b/js/tarinai_disease_nest.js index 29f6c13..14b4d7d 100644 --- a/js/tarinai_disease_nest.js +++ b/js/tarinai_disease_nest.js @@ -15,6 +15,8 @@ this.thought = "\u305a\u3093\u3061\u75c5\u306b\u304b\u304b\u3063\u305f"; audio.disease?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "\u3046\u3045\u2026", "rgba(38,82,38,0.82)"); + this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_zunchi", source }); + this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u305a\u3093\u3061\u75c5"); return true; }, @@ -28,6 +30,8 @@ this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f"; audio.heal?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)"); + this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_zunchi", reason }); + this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u305a\u3093\u3061\u75c5"); return true; }, @@ -91,6 +95,8 @@ audio.disease?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(72,76,120,0.76)"); this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident", { participants: [this] }); + this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_sleep", source }); + this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u306d\u3080\u308a\u75c5"); return true; }, @@ -108,18 +114,22 @@ this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "!", "rgba(72,76,120,0.76)"); this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); + this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_sleep", reason }); + this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u306d\u3080\u308a\u75c5"); return true; }, infectExplosionDisease(source = null) { if (this.dead || this.explosionDisease) return false; this.explosionDisease = true; - this.explosionDiseaseTimer = Math.max(this.explosionDiseaseTimer || 0, (CONFIG.dayLength || 120) * 0.5); + this.explosionDiseaseTimer = Math.max(this.explosionDiseaseTimer || 0, 60); this.fearTimer = Math.max(this.fearTimer || 0, 0.55); this.thought = "\u7206\u767a\u75c5\u3067\u843d\u3061\u7740\u304b\u306a\u3044"; audio.disease?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.2, "!", "rgba(178,78,42,0.82)"); this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] }); + this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_explosion", source }); + this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u7206\u767a\u75c5"); return true; }, @@ -132,6 +142,8 @@ this.stress = clamp(this.stress - 10, 0, 130); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); + this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_explosion", reason }); + this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u7206\u767a\u75c5"); return true; }, @@ -148,6 +160,8 @@ audio.disease?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)"); this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight", { participants: [this] }); + this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_fight", source }); + this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u304d\u305a\u3064\u304d\u75c5"); return true; }, @@ -160,6 +174,8 @@ this.stress = clamp(this.stress - 12, 0, 130); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] }); + this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_fight", reason }); + this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u304d\u305a\u3064\u304d\u75c5"); return true; }, @@ -181,7 +197,7 @@ this.y = this.sleepDiseaseAnchorY; } this.wanderAngle = this.wanderAngle || 0; - this.thought = beingDraggedByAnt ? "眠ったままアリに運ばれている" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b"; + this.thought = beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b"; if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) { this.nextSleepBubbleAt = this.world.time + 5.0; this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); diff --git a/js/tarinai_identity_social.js b/js/tarinai_identity_social.js index 3cf436c..c54c756 100644 --- a/js/tarinai_identity_social.js +++ b/js/tarinai_identity_social.js @@ -24,6 +24,29 @@ return this.generation > 1 && this.growth < 1; }, + syncGrowthScale() { + if (!Number.isFinite(this.adultScale)) this.adultScale = adultScaleFromGenetics(this.familyKey || this.id, this.genetics); + const nextScale = this.generation > 1 ? scaleForGrowth(this.adultScale, this.growth) : this.adultScale; + this.scale = clamp(nextScale, 0.10, 0.42); + if (this.refreshEffectiveSize) this.refreshEffectiveSize(); + else this.radius = 56 * this.scale; + const growthBucket = Math.floor(clamp(this.growth || 0, 0, 1) * 20); + if (this.generation > 1 && this.world?.recordFamily && this.familyJoined?.() && this.familyGrowthBucket !== growthBucket) { + this.familyGrowthBucket = growthBucket; + this.world.recordFamily(this); + } + return this.scale; + }, + + geneticStatRatio(key = "") { + this.genetics = normalizeGenetics(this.genetics, this.familyKey || this.id); + if (key === "life") return this.genetics.lifeSpanMul || 1; + if (key === "attack") return this.genetics.attackMul || 1; + if (key === "speed") return this.genetics.speedMul || 1; + if (key === "size") return this.genetics.sizeMul || 1; + return 1; + }, + parentToFollow() { if (!this.juvenile || !this.parents?.length) return null; let best = null, bestD = Infinity; @@ -52,6 +75,32 @@ if (this.records.length > 42) this.records.length = 42; }, + recordChangeCause(reason = "", target = "", options = {}) { + const text = String(reason || "").trim(); + const affected = String(target || "").trim(); + if (!text && !affected) return false; + if (!Array.isArray(this.recentChangeCauses)) this.recentChangeCauses = []; + const now = this.world?.time || 0; + const entry = { + time: now, + reason: text || "\u5909\u5316", + target: affected || "\u72b6\u614b", + value: options.value ?? "", + }; + const head = this.recentChangeCauses[0]; + if (head && head.reason === entry.reason && head.target === entry.target) { + const oldValue = Number(head.value); + const addValue = Number(entry.value); + if (Number.isFinite(oldValue) || Number.isFinite(addValue)) head.value = (Number.isFinite(oldValue) ? oldValue : 0) + (Number.isFinite(addValue) ? addValue : 0); + else head.value = entry.value || head.value || ""; + head.time = now; + return true; + } + this.recentChangeCauses.unshift(entry); + if (this.recentChangeCauses.length > 8) this.recentChangeCauses.length = 8; + return true; + }, + personalityProfile() { const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 }; const p = ensurePersonality(this).currentPersonality; @@ -205,7 +254,8 @@ }, addStress(amount, opts = {}) { - const delta = Math.max(0, amount || 0); + const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88; + const delta = Math.max(0, (amount || 0) * tolerance); if (delta <= 0) return 0; const before = this.stress || 0; this.stress = clamp(before + delta, 0, 130); diff --git a/js/tarinai_item_effects.js b/js/tarinai_item_effects.js index 13aec6d..386da11 100644 --- a/js/tarinai_item_effects.js +++ b/js/tarinai_item_effects.js @@ -13,7 +13,7 @@ ]); function effectLabel(id = "") { - return EFFECT_REGISTRY?.label?.(id) || ITEM_EFFECT_LABELS[id] || id || "不明"; + return EFFECT_REGISTRY?.label?.(id) || ITEM_EFFECT_LABELS[id] || id || "\u4e0d\u660e"; } function sizeDurationScale(item = null) { @@ -23,6 +23,10 @@ return 1; } + function recordItemChange(tarinai, id = "", reason = "\u30a2\u30a4\u30c6\u30e0\u52b9\u679c") { + tarinai?.recordChangeCause?.(reason, effectLabel(id)); + } + function pickRandomEffects(count = 2) { const pool = [...RANDOM_EFFECT_POOL]; const out = []; @@ -64,12 +68,15 @@ if (!(id in this.itemEffectTimers)) this.itemEffectTimers[id] = 0; this.itemEffectTimers[id] = Math.max(this.itemEffectTimers[id] || 0, Number(seconds) || 0); if (id === "laxative") this.laxativePoopTimer = Math.min(Math.max(this.laxativePoopTimer || 3, 0.2), 3); + this.world?.emit?.("effect:applied", { tarinai: this, effectId: id, duration: this.itemEffectTimers[id] || 0 }); + recordItemChange(this, id, "\u6642\u9593\u52b9\u679c"); + return true; }, itemSizeMultiplier() { this.ensureItemEffectState(); - if (this.sizeItemMode === "giant_drug") return 3; - if (this.sizeItemMode === "dwarf_drug") return 0.25; + if (this.sizeItemMode === "giant_drug") return EFFECT_REGISTRY?.modifier?.("giant_drug", "sizeMultiplier", 3) ?? 3; + if (this.sizeItemMode === "dwarf_drug") return EFFECT_REGISTRY?.modifier?.("dwarf_drug", "sizeMultiplier", 0.25) ?? 0.25; return 1; }, @@ -90,6 +97,8 @@ this.powerEffectKind = mode; this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 3.2); this.spawnPowerItemEffect(mode, 1.5); + this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true }); + recordItemChange(this, mode, "\u80fd\u529b\u5909\u5316"); return true; }, @@ -102,6 +111,8 @@ this.powerEffectKind = mode; this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 4.0); this.spawnPowerItemEffect(mode, mode === "giant_drug" ? 1.9 : 1.2); + this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true }); + recordItemChange(this, mode, "\u4f53\u683c\u5909\u5316"); return true; }, @@ -117,6 +128,8 @@ this.powerEffectKind = "mercury"; this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.7); this.spawnPowerItemEffect("mercury", 0.95); + this.world?.emit?.("effect:applied", { tarinai: this, effectId: "mercury", permanent: true, multiplier: this.mercuryLifeMultiplier }); + recordItemChange(this, "mercury", "\u5bff\u547d\u5909\u5316"); return true; }, @@ -140,6 +153,8 @@ this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.2); this.refreshEffectiveSize(); this.spawnPowerItemEffect("zunda_juice", 1.4); + this.world?.emit?.("effect:removed", { tarinai: this, effectId: "all_removable", source: "zunda_juice" }); + recordItemChange(this, "zunda_juice", "\u52b9\u679c\u89e3\u9664"); return true; }, @@ -147,101 +162,61 @@ this.ensureItemEffectState(); const scale = sizeDurationScale(item); let applied = []; - if (type === "laxative") { - this.setTimedItemEffect("laxative", Math.round(60 * scale)); - this.spawnPowerItemEffect("laxative", 0.75); - applied.push("laxative"); - } else if (type === "protein") { - this.setPowerItemMode("protein"); - applied.push("protein"); - } else if (type === "niteropu") { - this.setPowerItemMode("niteropu"); - applied.push("niteropu"); - } else if (type === "ammo") { - this.setTimedItemEffect("ammo", Math.round(18 * scale)); - this.spawnPowerItemEffect("ammo", 1.1); - applied.push("ammo"); - } else if (type === "mercury") { - this.setMercuryEffect(); - applied.push("mercury"); - } else if (type === "giant_drug") { - this.setSizeItemMode("giant_drug"); - applied.push("giant_drug"); - } else if (type === "dwarf_drug") { - this.setSizeItemMode("dwarf_drug"); - applied.push("dwarf_drug"); - } else if (type === "zunda_juice") { - this.clearAllItemEffects(); - applied.push("zunda_juice"); - } else if (type === "mystery_drug") { + if (type === "mystery_drug") { const picks = pickRandomEffects(2); for (const effect of picks) this.applyRandomizedBehaviorEffect(effect, item); this.mysteryDrugHistory = [...(this.mysteryDrugHistory || []), `${picks.map(effectLabel).join(" + ")}`].slice(-8); this.bubble("???", 1.2, ITEM_EFFECT_COLORS.mystery_drug); this.spawnPowerItemEffect("mystery_drug", 1.0); applied = picks; + } else if (EFFECT_REGISTRY?.apply?.(type, this, { item, scale, source: "item" })) { + applied.push(type); + if (type === "laxative") { + this.spawnPowerItemEffect("laxative", 0.75); + } else if (type === "ammo") { + this.spawnPowerItemEffect("ammo", 1.1); + } } else { return false; } if (!options.silentLog && this.world?.log) { - const label = type === "mystery_drug" ? `怪しい薬(${applied.map(effectLabel).join(" + ")})` : effectLabel(type); - this.world.log(`${this.name}に${label}の効果が出た。`, "food", { participants: [this] }); + const label = type === "mystery_drug" ? `\u602a\u3057\u3044\u85ac(${applied.map(effectLabel).join(" + ")})` : effectLabel(type); + this.world.log(`${this.name}\u306b${label}\u306e\u52b9\u679c\u304c\u51fa\u305f\u3002`, "food", { participants: [this] }); } + this.recordChangeCause?.("\u30a2\u30a4\u30c6\u30e0\u6442\u53d6", applied.map(effectLabel).join(" + ") || effectLabel(type)); return true; }, applyRandomizedBehaviorEffect(effect = "", item = null) { const scale = sizeDurationScale(item); - if (effect === "love_mochi") { - this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 * scale + rand(4, 12)); - this.stress = clamp(this.stress - 8, 0, 130); - return true; - } - if (effect === "fight_mochi") { - this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 52 * scale + rand(10, 18)); - this.stress = clamp(this.stress + 10, 0, 130); - return true; - } - if (effect === "sleep_drug") return this.infectSleepDisease?.(item) || true; - if (effect === "laxative") { this.setTimedItemEffect("laxative", Math.round(60 * scale)); return true; } - if (effect === "ammo") { this.setTimedItemEffect("ammo", Math.round(18 * scale)); return true; } - if (effect === "protein") return this.setPowerItemMode("protein"); - if (effect === "niteropu") return this.setPowerItemMode("niteropu"); - if (effect === "mercury") return this.setMercuryEffect(); - if (effect === "giant_drug") return this.setSizeItemMode("giant_drug"); - if (effect === "dwarf_drug") return this.setSizeItemMode("dwarf_drug"); - if (effect === "disease_zunchi") return this.infectZunchiDisease?.(item, true) || true; - if (effect === "disease_sleep") return this.infectSleepDisease?.(item) || true; - if (effect === "disease_explosion") return this.infectExplosionDisease?.(item) || true; - if (effect === "disease_fight") return this.infectFightDisease?.(item) || true; - return false; + return Boolean(EFFECT_REGISTRY?.apply?.(effect, this, { item, scale, source: "mystery_drug" })); }, diseaseChance(base = 0) { const b = Math.max(0, Number(base) || 0); - if (this.powerItemMode === "protein") return b * 0.5; - if (this.powerItemMode === "niteropu") return b * 2.0; + if (this.powerItemMode === "protein") return b * (EFFECT_REGISTRY?.modifier?.("protein", "diseaseChanceMultiplier", 0.5) ?? 0.5); + if (this.powerItemMode === "niteropu") return b * (EFFECT_REGISTRY?.modifier?.("niteropu", "diseaseChanceMultiplier", 2.0) ?? 2.0); return b; }, outgoingDamage(amount = 0) { - let value = Number(amount) || 0; - if (this.powerItemMode === "protein") value *= 1.75; - if (this.powerItemMode === "niteropu") value *= 0.5; - if (this.sizeItemMode === "giant_drug") value *= 2.0; - if (this.sizeItemMode === "dwarf_drug") value *= 0.25; + let value = (Number(amount) || 0) * (this.geneticStatRatio ? this.geneticStatRatio("attack") : 1); + if (this.powerItemMode === "protein") value *= EFFECT_REGISTRY?.modifier?.("protein", "damageMultiplier", 1.75) ?? 1.75; + if (this.powerItemMode === "niteropu") value *= EFFECT_REGISTRY?.modifier?.("niteropu", "damageMultiplier", 0.5) ?? 0.5; + if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "damageMultiplier", 2.0) ?? 2.0; + if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "damageMultiplier", 0.25) ?? 0.25; return value; }, itemSpeedMultiplier() { - let value = this.hasItemEffect("ammo") ? 10 : 1; - if (this.sizeItemMode === "giant_drug") value *= 1.5; - if (this.sizeItemMode === "dwarf_drug") value *= 0.5; + let value = this.hasItemEffect("ammo") ? (EFFECT_REGISTRY?.modifier?.("ammo", "speedMultiplier", 10) ?? 10) : 1; + if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "speedMultiplier", 1.5) ?? 1.5; + if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "speedMultiplier", 0.5) ?? 0.5; return value; }, applyFoodHungerRelief(amount = 0) { - if (this.hasItemEffect("laxative")) return 0; + if (this.hasItemEffect("laxative")) return (Number(amount) || 0) * (EFFECT_REGISTRY?.modifier?.("laxative", "hungerReliefMultiplier", 0) ?? 0); this.hunger -= Number(amount) || 0; return amount; }, @@ -256,8 +231,8 @@ if (this.hasItemEffect("laxative")) { this.laxativePoopTimer = Math.max(0, (this.laxativePoopTimer || 3) - step); while (this.laxativePoopTimer <= 0 && this.hasItemEffect("laxative")) { - this.laxativePoopTimer += 3; - this.forceLaxativePoop(); + this.laxativePoopTimer += EFFECT_REGISTRY?.get?.("laxative")?.tickInterval || 3; + EFFECT_REGISTRY?.tick?.("laxative", this, step); } } else { this.laxativePoopTimer = Math.min(Math.max(this.laxativePoopTimer || 3, 0.2), 3); @@ -266,8 +241,7 @@ this.itemDamageTimer = Math.max(0, (this.itemDamageTimer || 1) - step); while (this.itemDamageTimer <= 0 && !this.dead && (this.sizeItemMode === "giant_drug" || this.sizeItemMode === "dwarf_drug")) { this.itemDamageTimer += 1; - this.damage?.(1, this.sizeItemMode === "giant_drug" ? "巨大薬" : "矮小薬"); - if (this.world?.effects) this.spawnPowerItemEffect(this.sizeItemMode, 0.25); + EFFECT_REGISTRY?.tick?.(this.sizeItemMode, this, step); } } else { this.itemDamageTimer = Math.min(Math.max(this.itemDamageTimer || 1, 0.08), 1); @@ -289,10 +263,14 @@ this.world.spawnZunchi?.(x, y); this.poopCount = (this.poopCount || 0) + 1; this.digest = Math.max(0, (this.digest || 0) - 3.0); - this.bubble("ぶりゅっ", 1.2, ITEM_EFFECT_COLORS.laxative); + const now = this.world?.time || 0; + if (now >= (this.nextPoopBubbleAt || -999)) { + this.nextPoopBubbleAt = now + 2.2; + this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative); + } if ((this.world.time || 0) > (this.nextLaxativeLogAt || -999)) { this.nextLaxativeLogAt = (this.world.time || 0) + 9; - this.world.log?.(`${this.name}は下剤の効果でずんちを放出した。`, "accident", { participants: [this] }); + this.world.log?.(`${this.name}\u306f\u4e0b\u5264\u306e\u52b9\u679c\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [this] }); } }, @@ -338,6 +316,9 @@ o.vy += py * 160; o.hurtTimer = Math.max(o.hurtTimer || 0, 1.1); o.fallTimer = Math.max(o.fallTimer || 0, 0.72); + const bumpDamage = Math.max(0.6, Math.min(3.0, speed / 260)); + o.damage?.(bumpDamage, "\u5f3e\u85ac\u885d\u7a81"); + this.adjustPersonality?.("aggression", 0.012, "after ammo knockback"); this.world.spawnFallEffect?.(o.x, o.y + o.radius * 0.45, 1.05); audio.damage?.(12); this.ammoBumpCooldown = 0.12; @@ -363,22 +344,8 @@ activeItemEffectSummary() { this.ensureItemEffectState(); - const rows = []; - const add = (id, timer = 0) => { - const label = effectLabel(id); - const detail = EFFECT_REGISTRY?.display?.(id, timer, this) || ""; - rows.push({ id, label, detail }); - }; - if (this.powerItemMode === "protein") add("protein"); - if (this.powerItemMode === "niteropu") add("niteropu"); - if (this.lifeItemMode === "mercury") add("mercury"); - if (this.sizeItemMode === "giant_drug") add("giant_drug"); - if (this.sizeItemMode === "dwarf_drug") add("dwarf_drug"); - if ((this.itemEffectTimers.laxative || 0) > 0.04) add("laxative", this.itemEffectTimers.laxative); - if ((this.itemEffectTimers.ammo || 0) > 0.04) add("ammo", this.itemEffectTimers.ammo); - if ((this.loveMochiTimer || 0) > 0.1) add("love_mochi", this.loveMochiTimer); - if ((this.fightMochiTimer || 0) > 0.1) add("fight_mochi", this.fightMochiTimer); - return rows; + if (EFFECT_REGISTRY?.activeSummary) return EFFECT_REGISTRY.activeSummary(this); + return []; }, })); diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index df9a0d9..f8ed366 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -16,22 +16,35 @@ return (this.fightMochiTimer || 0) > 0.04; }, - becomeZunchiSlave() { - if (this.isZunchiSlave) return false; + zunchiSlaveSpriteId() { + if (!this.zunchiSlaveSpriteVariant) this.zunchiSlaveSpriteVariant = Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt"; + return this.zunchiSlaveSpriteVariant; + }, + + becomeZunchiSlave(options = {}) { + if (this.isZunchiSlave) { + this.zunchiSlaveLocked = true; + applyZunchiSlavePersonality(this, { record: false, birth: !!options.birth }); + this.visibleSpriteId = this.zunchiSlaveSpriteId(); + return false; + } this.formerName = this.formerName || this.name; this.isZunchiSlave = true; + this.zunchiSlaveLocked = true; this.name = "\u305a\u3093\u3061\u3069\u308c\u3044"; this.birthRitualTimer = 0; this.birthPartnerId = null; this.birthRitualLeader = false; this.birthRitualRole = 0; this.target = null; - this.reproductionTimer = Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18)); - this.loveMochiTimer = 0; - this.addStress(14, { threshold: 8 }); + this.reproductionTimer = options.birth ? Math.max(0, this.reproductionTimer || 0) : Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18)); + if (!options.birth) this.loveMochiTimer = 0; + this.addStress(options.birth ? 0 : 14, { threshold: 8 }); this.hunger = Math.max(this.hunger, 40); - this.visibleSpriteId = "zunchi_slave"; + applyZunchiSlavePersonality(this, { birth: !!options.birth }); + this.visibleSpriteId = this.zunchiSlaveSpriteId(); this.spriteLockUntil = 0; + this.recordChangeCause?.("\u305a\u3093\u3061\u3069\u308c\u3044\u5316", "\u72b6\u614b"); return true; }, @@ -40,7 +53,7 @@ if (this.sleepDisease) return this.variantSprite("sleep"); if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan"; if (this.fightDisease) return "stress_dizzy"; - if (this.isZunchiSlave) return "zunchi_slave"; + if (this.isZunchiSlave) return this.zunchiSlaveSpriteId(); if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry"; if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat"; if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat"; @@ -48,6 +61,7 @@ if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual"; const low = this.lowHealthSprite(); if (low === "stretch") return low; + if (this.hunger >= 70 && !low) return "hungry_70"; if (this.defeatedTimer > 0.04) return this.variantSprite("flee"); if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate"); if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear"); @@ -69,9 +83,12 @@ spriteId() { const next = this.rawSpriteId(); const now = this.world?.time || 0; - if (!this.visibleSpriteId) { + const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04; + const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04; + const forceImmediate = String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate"); + if (!this.visibleSpriteId || forceImmediate) { this.visibleSpriteId = next; - this.spriteLockUntil = now + 1.0; + this.spriteLockUntil = forceImmediate ? now : now + 1.0; return next; } if (next !== this.visibleSpriteId && now >= this.spriteLockUntil) { @@ -220,12 +237,7 @@ this.vy *= Math.pow(1.001, dt * 60); } - if (this.juvenile) { - const adultScale = 0.25 + (makeTrait(this.type).speed % 0.08); - this.scale = lerp(this.scale, adultScale, dt * 0.018 + 0.002); - if (this.refreshEffectiveSize) this.refreshEffectiveSize(); - else this.radius = 56 * this.scale; - } + if (this.generation > 1 && this.syncGrowthScale) this.syncGrowthScale(); this.hunger = clamp(this.hunger, 0, 115); this.loneliness = clamp(this.loneliness, 0, 110); @@ -363,6 +375,7 @@ if (!this.isServingFoodItem(it) || (it.foodServingsRemaining || 0) <= 0) return 0; it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1); it.amount = it.foodServingsRemaining; + this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition }); return nutrition; }, @@ -470,6 +483,22 @@ this.defeatedById = null; this.fightWinnerId = null; } + + if (this.stuckPushpinId) { + const pin = this.world.items?.find(it => it && !it.dead && it.id === this.stuckPushpinId && it.type === "pushpin" && it.pinState === "lodged") || null; + if (pin) { + this.state = "panic"; + this.sleeping = false; + this.target = this.panicDestination(pin); + this.fearTimer = Math.max(this.fearTimer || 0, 0.60); + this.hurtTimer = Math.max(this.hurtTimer || 0, 0.24); + this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 1.8); + this.bubble("!!", 0.9, "rgba(168,72,72,0.82)"); + this.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b"; + return; + } + this.stuckPushpinId = null; + } const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null; if (friendNearby && friendNearby.state === "panic" && friendNearby.target) { @@ -586,14 +615,15 @@ } if (!this.isZunchiSlave) { - const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"], wasSleeping ? 190 : 155); + const medicineTypes = window.TarinaiFoodRegistry?.typesByInterest?.("medicine_low") || ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]; + const specialMochi = this.world.nearest(this, medicineTypes, wasSleeping ? 190 : 155); const specialDist = specialMochi ? distXY(this.x, this.y, specialMochi.x, specialMochi.y) : Infinity; const weakInterest = this.hunger > (wasSleeping ? 68 : 76) && (specialDist < 72 || Math.random() < dt * 0.028); if (specialMochi && this.foodItemHasServingLeft(specialMochi) && weakInterest && !this.shouldAvoidTarget(specialMochi)) { this.state = "seek_food"; this.target = specialMochi; this.foodReactTimer = Math.max(this.foodReactTimer, 0.18); - this.thought = `${toolLabel(specialMochi.type)}が少し気になっている`; + this.thought = `${toolLabel(specialMochi.type)}\u304c\u5c11\u3057\u6c17\u306b\u306a\u3063\u3066\u3044\u308b`; return; } } else if (this.hunger > 20) { @@ -721,7 +751,7 @@ } if (this.hunger > 62) { - const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", "grass", "ant_corpse"]; + const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])]; const item = this.world.nearest(this, itemTypes, 420); if (item) { this.state = "seek_food"; @@ -764,7 +794,11 @@ const backX = this.x - side * this.radius * rand(0.72, 0.96); const backY = this.y + this.radius * rand(0.25, 0.48); this.world.spawnZunchi(backX, backY); - this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)"); + const now = this.world?.time || 0; + if (now >= (this.nextPoopBubbleAt || -999)) { + this.nextPoopBubbleAt = now + 2.8; + this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)"); + } if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); }, @@ -856,7 +890,7 @@ this.makePoop(eating * 0.20); if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; } if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) { - this.world.log(`${this.name}は${toolLabel(it.type)}をしばらく味わった。`, "food", { participants: [this] }); + this.world.log(`${this.name}\u306f${toolLabel(it.type)}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] }); this.lastLog = this.world.time; } break; diff --git a/js/tarinai_render.js b/js/tarinai_render.js index 662ad00..810cf62 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -284,16 +284,16 @@ ctx.restore(); } } - if (sprite === "zunchi_slave") { + if (String(sprite || "").startsWith("zunchi_slave")) { const zImg = typeof getRenderableImage === "function" ? getRenderableImage("zunchi", "zunchi") : (images && images.get ? images.get("zunchi") : null); if (zImg) { ctx.save(); ctx.shadowColor = "rgba(46, 36, 24, 0.28)"; ctx.shadowBlur = 5; ctx.shadowOffsetY = Math.max(1, drawH * 0.012); - const zw = Math.max(12, drawW * 0.30); + const zw = Math.max(16, drawW * 0.42); const zh = zw * 0.74; - ctx.drawImage(zImg, -zw * 0.50, -dh * 0.48 - zh * 0.25, zw, zh); + ctx.drawImage(zImg, -zw * 0.78, -dh * 0.50 - zh * 0.28, zw, zh); ctx.restore(); } } @@ -342,15 +342,20 @@ const drawMeter = (y, pct, fill, alpha) => { ctx.save(); ctx.globalAlpha = alpha; + const safePct = clamp(Number(pct) || 0, 0, 1); + const fillW = safePct <= 0 ? 0 : Math.min(bw, Math.max(2, bw * safePct)); ctx.fillStyle = "rgba(255,252,242,0.86)"; roundedRect(ctx, x - 2, y - 2, bw + 4, bh + 4, 5); ctx.fill(); ctx.fillStyle = "rgba(72,58,45,0.22)"; roundedRect(ctx, x, y, bw, bh, 4); ctx.fill(); - ctx.fillStyle = fill; - roundedRect(ctx, x, y, bw * pct, bh, 4); - ctx.fill(); + if (fillW > 0) { + const fillR = Math.min(4, bh / 2, fillW / 2); + ctx.fillStyle = fill; + roundedRect(ctx, x, y, fillW, bh, fillR); + ctx.fill(); + } ctx.restore(); }; if (stressActive) { diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index fc8349e..e6b1b0d 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -26,6 +26,22 @@ } const pairBond = this.world.areCoParents?.(this, o); + const mixedZunchiSlave = !!this.isZunchiSlave !== !!o.isZunchiSlave; + if (mixedZunchiSlave && d < 86) { + const nonSlave = this.isZunchiSlave ? o : this; + const slave = this.isZunchiSlave ? this : o; + const nx = (nonSlave.x - slave.x) / d; + const ny = (nonSlave.y - slave.y) / d; + nonSlave.vx += nx * dt * 34; + nonSlave.vy += ny * dt * 34; + nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); + nonSlave.target = slave; + if (nonSlave.state === "idle" || nonSlave.state === "seek_friend") nonSlave.state = "panic"; + nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid"); + slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid"); + if (Math.random() < dt * 0.35) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18); + continue; + } const overlap = this.radius + o.radius - d; if (overlap > -2) { const push = (overlap + 6) * (pairBond ? 0.08 : 0.17); @@ -69,6 +85,8 @@ o.stress = clamp(o.stress + 2.8, 0, 130); const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2; const damageToOther = this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6; + this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" }); + this.world.emit?.("fight:hit", { attacker: this, target: o, amount: damageToOther, cause: "\u55a7\u5629" }); this.damage(damageToThis, "\u55a7\u5629"); o.damage(damageToOther, "\u55a7\u5629"); this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis); @@ -111,7 +129,8 @@ this.world.startConflict(this, o); } - if (!this.isZunchiSlave && !o.isZunchiSlave && !this.sleepDisease && !o.sleepDisease && !this.fightDisease && !o.fightDisease && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) { + const compatibleBirthStatus = this.isZunchiSlave === o.isZunchiSlave; + if (compatibleBirthStatus && !this.sleepDisease && !o.sleepDisease && !this.fightDisease && !o.fightDisease && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) { const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44); const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24); if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) { @@ -295,7 +314,7 @@ this.vx *= Math.pow(0.84, dt * 8); this.vy *= Math.pow(0.84, dt * 8); - const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1); + const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1); const v = Math.hypot(this.vx, this.vy); if (v > maxV) { this.vx = this.vx / v * maxV; @@ -362,6 +381,7 @@ this.releasedLiveToken = this.liveToken || 0; this.world.releaseTarinaiLiveId?.(this); audio.death(); + this.world.emit?.("tarinai:died", { tarinai: this, reason: finalReason }); this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] }); if (this.world.selected === this) this.world.selected = null; } diff --git a/js/ui.js b/js/ui.js index 89d26bc..026fbf5 100644 --- a/js/ui.js +++ b/js/ui.js @@ -95,7 +95,7 @@ const uiCache = { grabLastWorldX: 0, grabLastWorldY: 0, grabLastSpatialAt: 0, - mobileInputMode: localStorage.getItem("tarinai_mobile_input_mode_v1") || "auto", + mobileInputMode: window.TarinaiInputMode?.currentMode || "auto", touchMode: "", touchMoved: false, touchStartX: 0, diff --git a/js/ui_bind.js b/js/ui_bind.js index c7b271e..509f955 100644 --- a/js/ui_bind.js +++ b/js/ui_bind.js @@ -1,6 +1,7 @@ "use strict"; function bindUI() { + renderToolPalette(); loadLogPushSettings(); syncLogPushControls(); syncAudioControls(); @@ -16,7 +17,7 @@ function bindUI() { const collapsed = card.classList.toggle("collapsed"); toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); const state = toggle.querySelector(".panel-card-state"); - if (state) state.textContent = collapsed ? "展開" : "折り畳み"; + if (state) state.textContent = collapsed ? "\u5c55\u958b" : "\u6298\u308a\u7573\u307f"; }); } ui.colonyChartTabs?.addEventListener("click", (e) => { @@ -33,7 +34,7 @@ function bindUI() { uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked); audio.notify?.(); saveLogPushSettings(); - showToast(uiCache.logPushEnabled ? "ログのプッシュ通知をONにしました。" : "ログのプッシュ通知をOFFにしました。"); + showToast(uiCache.logPushEnabled ? "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); }); ui.logPushControls?.addEventListener("change", (e) => { const input = e.target.closest("[data-push-kind]"); @@ -170,7 +171,7 @@ function bindUI() { const on = audio.setEnabled ? audio.setEnabled(ui.soundMasterToggle.checked) : audio.toggle(); if (on) audio.uiClick?.(); syncAudioControls(); - showToast(on ? "効果音をONにしました。" : "効果音をOFFにしました。"); + showToast(on ? "\u52b9\u679c\u97f3\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u52b9\u679c\u97f3\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); }); document.addEventListener("click", (e) => { if (!ui.soundMenu || ui.soundMenu.contains(e.target)) return; @@ -264,30 +265,22 @@ function bindUI() { chainScroll(ui.archiveContent); transferEdgeScroll(ui.log, rightPanel); - const isTouchDevice = () => matchMedia?.("(pointer: coarse)")?.matches || navigator.maxTouchPoints > 0; + const inputModeManager = window.TarinaiInputMode; + const isTouchDevice = () => Boolean(inputModeManager?.snapshot?.().isTouchDevice || matchMedia?.("(pointer: coarse)")?.matches || navigator.maxTouchPoints > 0); const setMobileInputMode = (mode = "auto") => { - if (!["auto", "camera", "tool", "family"].includes(mode)) mode = "auto"; + mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto"); uiCache.mobileInputMode = mode; - try { localStorage.setItem("tarinai_mobile_input_mode_v1", mode); } catch (_) {} ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode)); document.body.classList.toggle("mobile-mode-camera", mode === "camera"); document.body.classList.toggle("mobile-mode-tool", mode === "tool"); document.body.classList.toggle("mobile-mode-family", mode === "family"); }; const updateTouchMobileUiClass = () => { - const ua = String(navigator.userAgent || ""); - const coarse = Boolean(window.matchMedia && window.matchMedia("(pointer: coarse)").matches); - const noHover = Boolean(window.matchMedia && window.matchMedia("(hover: none)").matches); - const touchPoints = Number(navigator.maxTouchPoints || 0); - const w = window.innerWidth || 9999; - const h = window.innerHeight || 9999; - const mobileUa = /Android|iPhone|iPod|Mobile|Silk/i.test(ua); - const compactPhoneViewport = Math.min(w, h) <= 640 && Math.max(w, h) <= 980; - const enabled = Boolean(coarse && noHover && touchPoints > 0 && compactPhoneViewport && mobileUa); + const enabled = Boolean(inputModeManager?.refresh?.().touchFirst); document.body.classList.toggle("touch-mobile-ui", enabled); if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled; }; - const mobileInputMode = () => uiCache.mobileInputMode || "auto"; + const mobileInputMode = () => inputModeManager?.currentMode || uiCache.mobileInputMode || "auto"; updateTouchMobileUiClass(); window.addEventListener("resize", updateTouchMobileUiClass, { passive: true }); window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass); @@ -297,7 +290,7 @@ function bindUI() { if (!btn) return; audio.uiClick?.(); setMobileInputMode(btn.dataset.mobileMode || "auto"); - showToast(`スマホ操作: ${btn.textContent || btn.dataset.mobileMode}`); + showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`); }); const touchPoint = (touches, index = 0) => { @@ -355,6 +348,7 @@ function bindUI() { if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle"; target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18); } else { + if (target.type === "pushpin" && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch"); target.dropTimer = 0; target.dropMax = 0; target.dropImpactDone = true; @@ -600,6 +594,7 @@ function bindUI() { if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle"; target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18); } else { + if (target.type === "pushpin" && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch"); target.dropTimer = 0; target.dropMax = 0; target.dropImpactDone = true; diff --git a/js/ui_charts.js b/js/ui_charts.js index caf9f7b..c5297c8 100644 --- a/js/ui_charts.js +++ b/js/ui_charts.js @@ -10,7 +10,7 @@ function renderStats() { hunger: avg(t => t.hunger), lonely: avg(t => t.loneliness), stress: avg(t => t.stress), - lack: avg(t => t.lack), + mood: avg(t => t.mood), security: environment.security, hygiene: environment.hygiene, happiness: environment.happiness, @@ -28,7 +28,7 @@ function updateColonyHistory(values) { const t = Math.max(0, world.time || 0); const history = uiCache.chartHistory; const interval = Math.max(1, (CONFIG.dayLength || 120) / 12); - const maxPoints = 7 * 12; + const maxPoints = 5 * 12; if ((history.length && t < history[history.length - 1].t) || (Number.isFinite(uiCache.chartBucketStart) && t < uiCache.chartBucketStart)) { history.length = 0; uiCache.chartBucketStart = -Infinity; @@ -42,7 +42,7 @@ function updateColonyHistory(values) { start, end: start + interval, count: 0, - sums: { pop: 0, hunger: 0, lonely: 0, stress: 0, lack: 0, security: 0, hygiene: 0, happiness: 0 }, + sums: { pop: 0, hunger: 0, lonely: 0, stress: 0, mood: 0, security: 0, hygiene: 0, happiness: 0 }, objects: Object.fromEntries(objectKeys.map(k => [k, 0])), }); if (!uiCache.chartBucketAccum) { @@ -59,7 +59,7 @@ function updateColonyHistory(values) { hunger: bucket.sums.hunger / bucket.count, lonely: bucket.sums.lonely / bucket.count, stress: bucket.sums.stress / bucket.count, - lack: bucket.sums.lack / bucket.count, + mood: bucket.sums.mood / bucket.count, security: bucket.sums.security / bucket.count, hygiene: bucket.sums.hygiene / bucket.count, happiness: bucket.sums.happiness / bucket.count, @@ -81,7 +81,7 @@ function updateColonyHistory(values) { bucket.sums.hunger += values.hunger || 0; bucket.sums.lonely += values.lonely || 0; bucket.sums.stress += values.stress || 0; - bucket.sums.lack += values.lack || 0; + bucket.sums.mood += values.mood || 0; bucket.sums.security += values.security || 0; bucket.sums.hygiene += values.hygiene || 0; bucket.sums.happiness += values.happiness || 0; @@ -97,7 +97,7 @@ function chartRows(values) { hunger: bucket.sums.hunger / bucket.count, lonely: bucket.sums.lonely / bucket.count, stress: bucket.sums.stress / bucket.count, - lack: bucket.sums.lack / bucket.count, + mood: bucket.sums.mood / bucket.count, security: bucket.sums.security / bucket.count, hygiene: bucket.sums.hygiene / bucket.count, happiness: bucket.sums.happiness / bucket.count, @@ -177,7 +177,7 @@ function chartSeries(mode = "population") { { key: "hunger", label: "\u7a7a\u8179", color: "#e4a33f", percent: true }, { key: "lonely", label: "\u5bc2\u3057\u3055", color: "#9b78d4", percent: true }, { key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true }, - { key: "lack", label: "\u305f\u308a\u306a\u3044", color: "#5b9d61", percent: true }, + { key: "mood", label: "\u7dcf\u5408\u7684\u6c17\u5206", color: "#0072b2", percent: true }, ]; if (mode === "objects") return [ { key: "obj_grass", label: "\u8349", color: "#5b9d61" }, diff --git a/js/ui_family.js b/js/ui_family.js deleted file mode 100644 index 8df68fb..0000000 --- a/js/ui_family.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -// Split into ui_family_data.js, ui_family_async.js, ui_family_layout.js, ui_family_paths.js, ui_family_render.js. diff --git a/js/ui_family_async.js b/js/ui_family_async.js index 7506f08..60458e2 100644 --- a/js/ui_family_async.js +++ b/js/ui_family_async.js @@ -175,7 +175,7 @@ function familyDeathReasonLabel(rawReason, entity = null) { if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9"; if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664"; if (/\u885d\u7a81|collision/.test(text)) return "\u885d\u7a81"; - if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return "\u3064\u3064\u304b\u308c\u3059\u304e\u305f"; + if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return "\u3064\u3064\u304b\u308c\u3059\u304e"; if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return "\u305a\u3093\u3061\u75c5"; if (/\u75c5/.test(text)) { const disease = text.match(/([^\u3001\u3002\s]+\u75c5)/); diff --git a/js/ui_family_layout.js b/js/ui_family_layout.js index 784559c..db238fd 100644 --- a/js/ui_family_layout.js +++ b/js/ui_family_layout.js @@ -17,17 +17,26 @@ function lineageTreeNodeHtml(n, x, y) { const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : ""; const briefReason = familyDeathReasonLabel(rawReason, entity); const reason = briefReason ? ` / ${briefReason}` : ""; - const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}`); + const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []); + const tags = rawTags.slice(); + const tagText = tags.length ? ` / ${tags.join(" ")}` : ""; + const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}${tagText}`); const deathLine = briefReason ? `${escapeHtml(briefReason)}` : ""; + const tagLine = tags.length ? `${tags.map(tag => `${escapeHtml(tag)}`).join("")}` : ""; const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing"; const childCls = (n?.parents || []).length ? " child" : ""; + const iconBaseScale = live + ? (live.effectiveScale ? live.effectiveScale() : (Number.isFinite(live.scale) ? live.scale : (Number.isFinite(live.adultScale) ? live.adultScale : 0.28))) + : (Number.isFinite(n?.scale) ? n.scale : (Number.isFinite(n?.adultScale) ? n.adultScale : 0.28)); + const iconScale = clamp((Number(iconBaseScale) || 0.28) / 0.28, 0.64, 1.42).toFixed(2); const data = n?.id ? ` data-family-tarinai-id="${escapeHtml(n.id)}"` : ""; - return `
+ return `
${name} ${escapeHtml(gen)} / ${escapeHtml(status)} ${deathLine} + ${tagLine}
`; } @@ -343,7 +352,7 @@ function lineageBuildRowGroups(row, idSet) { function lineageLayoutRows(rows, idSet) { const NODE_W = 132; - const NODE_H = 68; + const NODE_H = 84; const SIBLING_GAP = 32; const GROUP_GAP = 52; const PARTNER_GROUP_GAP = 24; diff --git a/js/ui_family_paths.js b/js/ui_family_paths.js index e09bae7..6cc8158 100644 --- a/js/ui_family_paths.js +++ b/js/ui_family_paths.js @@ -140,6 +140,9 @@ function lineageComponentSignature(component, family) { n.type || "", n.generation || 1, n.birthTime || 0, + Number(n.scale || 0).toFixed(3), + Number(n.adultScale || 0).toFixed(3), + Number(n.growth || 0).toFixed(2), n.hasPaired ? 1 : 0, n.alive === false ? 0 : 1, n.deathReason || "", diff --git a/js/ui_family_render.js b/js/ui_family_render.js index 395a005..6cc275c 100644 --- a/js/ui_family_render.js +++ b/js/ui_family_render.js @@ -75,7 +75,7 @@ function lineageFastLayoutNeeded(nodes, idSet, family) { function lineageLayoutRowsFast(rows, idSet) { const NODE_W = 132; - const NODE_H = 68; + const NODE_H = 84; const SIBLING_GAP = 22; const ROW_GAP = 116; const LEFT_PAD = 82; diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index d881b63..1f150f2 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -60,12 +60,7 @@ function applyToolTips() { const showTip = (btn) => { const baseTip = btn?.dataset?.tip || ""; if (!baseTip) return; - let tip = baseTip; - if (btn?.dataset?.tool === "nest_box") { - const count = world?.nestBoxToolTipCount?.(); - if (count) tip = `${baseTip} -(${count.current}/${count.max})\u5339`; - } + const tip = baseTip; pop.textContent = tip; pop.classList.remove("hidden"); const rect = btn.getBoundingClientRect(); diff --git a/js/ui_log.js b/js/ui_log.js index 0d1e5c0..8a3211e 100644 --- a/js/ui_log.js +++ b/js/ui_log.js @@ -35,7 +35,7 @@ function syncAudioControls() { } if (ui.soundMasterToggle) ui.soundMasterToggle.checked = Boolean(audio.enabled); if (ui.soundBtn) { - ui.soundBtn.textContent = audio.enabled ? "音設定 ON" : "音設定 OFF"; + ui.soundBtn.textContent = audio.enabled ? "\u97f3\u8a2d\u5b9a ON" : "\u97f3\u8a2d\u5b9a OFF"; ui.soundBtn.classList.toggle("active", Boolean(audio.enabled)); ui.soundBtn.setAttribute("aria-expanded", ui.soundPanel && !ui.soundPanel.classList.contains("hidden") ? "true" : "false"); } diff --git a/js/ui_selected.js b/js/ui_selected.js index de32c82..b53a748 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -49,6 +49,97 @@ function personalityRowsHtml(personality = {}) { return PERSONALITY_KEYS.map(key => `${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}`).join(""); } +function personalityShortLabel(key) { + return ({ + aggression: "\u653b\u6483", + openness: "\u958b\u653e", + sociability: "\u793e\u4ea4", + neuroticism: "\u795e\u7d4c", + })[key] || (PERSONALITY_LABELS[key]?.label || key); +} + + +function personalityDisplayCauseText(cause = "") { + return String(cause || "").replace(/(?:\u306e)?\u5f71\u97ff\u3067$/g, "").trim(); +} + +function geneticPercentText(t, key = "") { + const ratio = t?.geneticStatRatio ? t.geneticStatRatio(key) : 1; + const pct = Math.round((ratio - 1) * 100); + return `${pct >= 0 ? "+" : ""}${pct}%`; +} + +function geneticSizeText(t) { + const adult = Number.isFinite(t?.adultScale) ? t.adultScale : (t?.scale || 0.28); + const now = Number.isFinite(t?.scale) ? t.scale : adult; + const growth = Math.round((t?.growth ?? 1) * 100); + return `${geneticPercentText(t, "size")} / ${now.toFixed(2)}\u2192${adult.toFixed(2)} (${growth}%)`; +} + +function personalityDailyChangeHtml(t) { + const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || { day: world?.day || 1, byKey: {}, byCause: {} }); + const dayLabel = `D${state.day || world?.day || 1}`; + const causeMap = new Map(); + const addCauseChange = (cause, key, up, down) => { + const cleanCause = personalityDisplayCauseText(cause || "") || "\u8981\u56e0\u4e0d\u660e"; + if (!causeMap.has(cleanCause)) causeMap.set(cleanCause, new Map()); + const byKey = causeMap.get(cleanCause); + const current = byKey.get(key) || { up: 0, down: 0 }; + current.up += Math.max(0, Number(up) || 0); + current.down += Math.max(0, Number(down) || 0); + byKey.set(key, current); + }; + for (const [cause, entry] of Object.entries(state.byCause || {})) { + for (const key of PERSONALITY_KEYS) { + const v = entry?.byKey?.[key] || {}; + const up = Math.max(0, Number(v.up) || 0); + const down = Math.max(0, Number(v.down) || 0); + if (up > 0.0001 || down > 0.0001) addCauseChange(cause, key, up, down); + } + } + if (!causeMap.size) { + for (const key of PERSONALITY_KEYS) { + const entry = state.byKey?.[key] || {}; + const up = Math.max(0, Number(entry.up) || 0); + const down = Math.max(0, Number(entry.down) || 0); + if (up <= 0.0001 && down <= 0.0001) continue; + const causes = Array.isArray(entry.causes) ? [...new Set(entry.causes.map(c => personalityDisplayCauseText(c)).filter(Boolean))].slice(0, 4) : []; + for (const cause of causes.length ? causes : ["\u8981\u56e0\u4e0d\u660e"]) addCauseChange(cause, key, up, down); + } + } + const rows = []; + for (const [cause, byKey] of causeMap.entries()) { + const parts = []; + for (const key of PERSONALITY_KEYS) { + const entry = byKey.get(key); + if (!entry) continue; + const up = Math.max(0, Number(entry.up) || 0); + const down = Math.max(0, Number(entry.down) || 0); + if (up > 0.0001) parts.push(`${personalityShortLabel(key)} +${up.toFixed(3)}`); + if (down > 0.0001) parts.push(`${personalityShortLabel(key)} -${down.toFixed(3)}`); + } + if (parts.length) rows.push(`
  • ${escapeHtml(cause)}${escapeHtml(parts.join(" / "))}${escapeHtml(dayLabel)}
  • `); + } + return rows.join("") || `
  • \u8981\u56e0\u5909\u5316\u306a\u3057${escapeHtml(dayLabel)}
  • `; +} + +function personalityDailySnapshot(t) { + const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || {}); + const keyPart = PERSONALITY_KEYS.map(key => { + const entry = state.byKey?.[key] || {}; + const causes = Array.isArray(entry.causes) ? entry.causes.join("/") : ""; + return `${key}:${Number(entry.up || 0).toFixed(4)}:${Number(entry.down || 0).toFixed(4)}:${causes}`; + }).join(","); + const causePart = Object.entries(state.byCause || {}).map(([cause, entry]) => { + const values = PERSONALITY_KEYS.map(key => { + const v = entry?.byKey?.[key] || {}; + return `${key}:${Number(v.up || 0).toFixed(4)}:${Number(v.down || 0).toFixed(4)}`; + }).join(";"); + return `${cause}=${values}`; + }).join("|"); + return `${keyPart}#${causePart}`; +} + function selectedDataCategoryHtml(id, label, bodyHtml) { if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set(); const collapsed = uiCache.selectedCollapsedCategories.has(id); @@ -58,6 +149,11 @@ function selectedDataCategoryHtml(id, label, bodyHtml) { `; } +function selectedInfoRowHtml(label, valueHtml, metric = "") { + const metricAttr = metric ? ` data-metric="${escapeHtml(metric)}"` : ""; + return `
    ${escapeHtml(label)}${valueHtml}
    `; +} + function renderSelected() { const t = world.selected; if (!t || t.dead || !world.tarinai.includes(t)) { @@ -75,7 +171,6 @@ function renderSelected() { const activeNameInput = document.activeElement?.matches?.("[data-selected-name]") && document.activeElement?.dataset?.selectedId === t.id; if (activeNameInput) return; const rel = t.relationSummary ? t.relationSummary() : {}; - const fightRecord = relationFightRecord(t); const generationDisplay = t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : ""); ensurePersonality(t); const personalityTags = t.personalityTags ? t.personalityTags() : getPersonalityTraitTags(t); @@ -90,11 +185,12 @@ function renderSelected() { fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness), fmt(t.energy), fmt(t.stress), fmt(t.mood), t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack), - relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightRecord, + relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightWinRateLabel(t), t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0), t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0), t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0, - personalitySnapshot, + personalitySnapshot, personalityDailySnapshot(t), + ["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100), itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","), (t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~") ].join("|"); @@ -114,10 +210,12 @@ function renderSelected() { const personalityTagHtml = personalityTags.length ? personalityTags.map(tag => `${escapeHtml(tag)}`).join("") : `\u4e2d\u7acb`; + const recentChangeHtml = personalityDailyChangeHtml(t); const personalityHtml = `
    \u6027\u683c\u30bf\u30b0${personalityTagHtml}
    \u8a95\u751f\u6642\u6027\u683c${personalityRowsHtml(t.birthPersonality)}
    \u73fe\u5728\u6027\u683c${personalityRowsHtml(t.currentPersonality)}
    +
    \u4eca\u65e5\u306e\u5909\u5316
      ${recentChangeHtml}
    `; const recordHtml = (t.records || []).slice(0, 8).map(r => { const timeText = world.clockString ? (world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0)) : formatRecordTime(r.time || 0); @@ -130,24 +228,26 @@ function renderSelected() {
    \u7406\u7531${escapeHtml(reasonLabel(t))}
    \u5bfe\u8c61${escapeHtml(targetLabel(t.target))}
    \u5e74\u9f62${fmt(t.age)} / ${fmt(t.lifeSpan)}
    +
    \u6700\u5927\u5bff\u547d${geneticPercentText(t, "life")}
    +
    \u653b\u6483\u529b${geneticPercentText(t, "attack")}
    +
    \u79fb\u52d5\u901f\u5ea6${geneticPercentText(t, "speed")}
    +
    \u30b5\u30a4\u30ba${geneticSizeText(t)}
    `; - const healthHtml = ` -
    \u7a7a\u8179${fmt(t.hunger)}
    -
    \u5bc2\u3057\u3055${fmt(t.loneliness)}
    -
    \u4f53\u529b${fmt(t.energy)}
    -
    \u30b9\u30c8\u30ec\u30b9${fmt(t.stress)}
    -
    \u305a\u3093\u3061\u6c5a\u308c${fmt(t.zunchiStain || 0)}
    -
    \u6c17\u5206${fmt(t.mood)}
    -
    \u305f\u308a\u306a\u3044${fmt(t.lack)}
    - `; + const healthHtml = [ + selectedInfoRowHtml("\u7dcf\u5408\u7684\u6c17\u5206", fmt(t.mood), "mood"), + selectedInfoRowHtml("\u7a7a\u8179", fmt(t.hunger), "hunger"), + selectedInfoRowHtml("\u5bc2\u3057\u3055", fmt(t.loneliness), "loneliness"), + selectedInfoRowHtml("\u4f53\u529b", fmt(t.energy), "energy"), + selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"), + selectedInfoRowHtml("\u305a\u3093\u3061\u6c5a\u308c", fmt(t.zunchiStain || 0), "zunchi"), + ].join(""); const itemEffectHtml = itemEffectSummary.length ? itemEffectSummary.map(e => `
    ${escapeHtml(e.label)}${escapeHtml(e.detail)}
    `).join("") - : `
    アイテム効果なし
    `; + : `
    \u30a2\u30a4\u30c6\u30e0\u52b9\u679c\u306a\u3057
    `; const relationHtml = `
    \u53cb\u9054${relationListHtml(t, "friend")}
    \u6575\u5bfe${relationListHtml(t, "enemy")}
    -
    \u55a7\u5629\u8a18\u61b6${escapeHtml(fightRecord)}
    -
    けんか勝率${escapeHtml(fightWinRateLabel(t))}
    +
    \u3051\u3093\u304b\u52dd\u7387${escapeHtml(fightWinRateLabel(t))}
    `; const familyHtml = `
    \u89aa${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}
    @@ -224,21 +324,12 @@ function targetLabel(target) { return "\u4e0d\u660e"; } -function relationFightRecord(t) { - let won = 0, lost = 0; - for (const rel of Object.values(t.relationships || {})) { - won += rel.fightsWon || 0; - lost += rel.fightsLost || 0; - } - return won || lost ? `\u52dd${won} / \u8ca0${lost}` : "\u306a\u3057"; -} - function fightWinRateLabel(t) { const won = Number(t.totalFightWins || 0); const lost = Number(t.totalFightLosses || 0); const total = won + lost; - if (!total) return "なし"; - return `${Math.round((won / total) * 100)}%(${won}勝${lost}敗)`; + if (!total) return "\u306a\u3057"; + return `${Math.round((won / total) * 100)}% (${won}\u52dd${lost}\u6557)`; } function setTextIfChanged(el, key, value) { diff --git a/js/ui_tools.js b/js/ui_tools.js index e200c47..f15e2a3 100644 --- a/js/ui_tools.js +++ b/js/ui_tools.js @@ -6,6 +6,37 @@ const SIZE_ORDER = ["small", "medium", "large"]; const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" }; +function renderToolPalette() { + if (!ui?.toolPalette || typeof toolCategories !== "function") return; + ui.toolPalette.innerHTML = ""; + const frag = document.createDocumentFragment(); + for (const category of toolCategories()) { + const section = document.createElement("section"); + section.className = `tool-category ${category.themeClass || ""}`.trim(); + section.dataset.toolCategory = category.id; + const toggle = document.createElement("button"); + toggle.className = "tool-category-toggle"; + toggle.type = "button"; + toggle.setAttribute("aria-expanded", "true"); + toggle.textContent = category.label || category.id; + const body = document.createElement("div"); + body.className = "tool-grid tool-category-body"; + for (const toolId of category.toolIds || []) { + const def = toolDefinition(toolId); + if (!def || def.simulationOnly) continue; + const btn = document.createElement("button"); + btn.className = "tool"; + btn.dataset.tool = toolId; + btn.textContent = def.label || toolId; + if (toolId === world?.tool) btn.classList.add("selected"); + body.appendChild(btn); + } + section.append(toggle, body); + frag.appendChild(section); + } + ui.toolPalette.appendChild(frag); +} + function toolSizeFor(tool) { if (!SCALABLE_TOOLS.has(tool)) return ""; return (world.toolSizes && world.toolSizes[tool]) || world.toolSize || "medium"; diff --git a/js/version.js b/js/version.js index e1b6768..56122cf 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "15.20.3"; - const APP_BUILD = "systems-split"; + const APP_VERSION = "15.20.16"; + const APP_BUILD = "registry-ui"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; @@ -21,3 +21,4 @@ window.TARINAI_VERSION = APP_VERSION; window.TARINAI_APP = config; })(); + diff --git a/js/weather_system.js b/js/weather_system.js new file mode 100644 index 0000000..ea67d8c --- /dev/null +++ b/js/weather_system.js @@ -0,0 +1,49 @@ +"use strict"; + +(function (global) { + const WEATHER_STATES = Object.freeze(["sunny", "cloudy", "light_rain"]); + + const WeatherSystem = Object.freeze({ + states: WEATHER_STATES, + + chooseNext(current = "sunny") { + const choices = current === "light_rain" + ? ["sunny", "cloudy"] + : ["sunny", "cloudy", "light_rain"]; + return pick(choices); + }, + + resetTimer() { + return rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); + }, + + shouldDropRainWater(worldRef, dt = 0) { + return Boolean( + worldRef?.weather === "light_rain" && + (worldRef.itemCounts?.water || 0) < 24 && + Math.random() < Math.max(0, Number(dt) || 0) * 0.12 + ); + }, + + createRainWaterItem(worldRef) { + const drop = new Item("water", rand(58, worldRef.w - 58), rand(58, worldRef.h - 58)); + drop.amount = rand(24, 58); + return drop; + }, + + applyNextWeather(worldRef) { + if (!worldRef) return null; + const previous = worldRef.weather || "sunny"; + const next = this.chooseNext(previous); + worldRef.weather = next; + worldRef.nextWeatherChange = this.resetTimer(); + if (next !== previous) { + worldRef.emit?.("weather:changed", { previous, next }); + worldRef.log?.(`\u5929\u6c17: ${weatherLabel(next)}`); + } + return next; + }, + }); + + global.TarinaiWeatherSystem = WeatherSystem; +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/world.js b/js/world.js index c641d85..79b03f5 100644 --- a/js/world.js +++ b/js/world.js @@ -68,11 +68,6 @@ class World { constructor() { this.effectCounts = {}; this.foodSpoilagePenalty = 0; this.foodSpoilageEvents = 0; - this.countsTimer = 0; - this.compactTimer = 0; - this.drawSortTimer = 0; - this.drawListDirty = true; - this.antUpdatePhase = 0; this.updateItemCounts(); this.updateEffectCounts(); this.markTerrainDirty?.("reset"); diff --git a/js/world_ants_system.js b/js/world_ants_system.js index 1024ab5..2b48aee 100644 --- a/js/world_ants_system.js +++ b/js/world_ants_system.js @@ -26,6 +26,7 @@ maxHp: ANT_WORKER_HP || 32, }); this.ants.push(a); + this.emit?.("ant:spawned", { ant: a, nest }); if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest); nest.antSpawnTimer = rand(1.6, 3.8) + outside * 0.45; this.drawListDirty = true; @@ -66,6 +67,7 @@ target.y = home.y; audio.antNest?.(); target.die("\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f"); + this.emit?.("ant:carried", { nest: home, target, complete: true }); this.drawListDirty = true; }, @@ -88,8 +90,8 @@ foundingDelayUntil: (this.time || 0) + 0.35, }); this.ants.push(queen); + this.emit?.("ant:spawned", { ant: queen, nest: home }); audio.queenAnt?.(); - this.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u5de3\u5834\u6240\u3092\u63a2\u3057\u59cb\u3081\u305f\u3002", "event"); this.drawListDirty = true; return queen; }, diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 150ea3e..60c9f49 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -133,18 +133,33 @@ blastedBalls += 1; } audio.explode(); - this.log(blastedBalls ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u30dc\u30fc\u30eb\u304c\u6025\u52a0\u901f\u3057\u305f\u3002" : "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u5ead\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident"); + this.log(blastedBalls ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u30dc\u30fc\u30eb\u304c\u6025\u52a0\u901f\u3057\u305f\u3002" : "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u8fba\u308a\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident"); }, explodeDiseaseTarinai(t) { if (!t || t.dead) return; const x = t.x, y = t.y; - const blastRadius = 190; + const blastScale = 0.68; + const blastRadius = 240 * blastScale; t.explosionDisease = false; t.explosionDiseaseTimer = 0; - this.effects.push(new Effect("explosion", x, y, { size: 72, life: 0.82, color: "rgba(255,176,68,0.88)" })); - this.effects.push(new Effect("ring", x, y, { size: 58, life: 0.52, color: "rgba(255,238,132,0.82)" })); - this.blastZunchiFrom(x, y, blastRadius * 0.92, 0.82); + this.effects.push(new Effect("explosion", x, y, { size: 86 * blastScale, life: 1.05, color: "rgba(255,182,66,0.92)" })); + this.effects.push(new Effect("explosion", x, y, { size: 128 * blastScale, life: 0.82, color: "rgba(255,92,42,0.70)" })); + for (let r = 0; r < 4; r++) { + this.effects.push(new Effect("ring", x, y, { size: (30 + r * 24) * blastScale, life: 0.48 + r * 0.11, color: r % 2 ? "rgba(255,116,62,0.78)" : "rgba(255,240,124,0.88)" })); + } + this.blastZunchiFrom(x, y, blastRadius * 0.92, blastScale); + for (let i = 0; i < 34; i++) { + const a = Math.PI * 2 * i / 34 + rand(-0.10, 0.10); + const speed = rand(90, 260) * blastScale; + this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * rand(2, 22) * blastScale, y + Math.sin(a) * rand(2, 22) * blastScale, { + vx: Math.cos(a) * speed, + vy: Math.sin(a) * speed, + size: rand(8, i % 3 === 0 ? 24 : 20) * blastScale, + life: rand(0.26, 0.72), + color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)", + })); + } for (const o of this.tarinai) { if (!o || o.dead || o === t) continue; const dx = o.x - x; @@ -152,8 +167,8 @@ const d = Math.hypot(dx, dy) || 1; if (d > blastRadius) continue; const q = clamp(1 - d / blastRadius, 0, 1); - o.damage(4 + q * 15, "\u7206\u767a\u75c5"); - o.addStress ? o.addStress(16 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 16 * q, 0, 130)); + o.damage(5 + q * 18, "\u7206\u767a\u75c5"); + o.addStress ? o.addStress(22 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 22 * q, 0, 130)); let nx = dx / d; let ny = dy / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { @@ -161,15 +176,47 @@ nx = Math.cos(a); ny = Math.sin(a); } - const diseaseBlast = (190 + q * 620) * rand(0.80, 1.22); - this.applyImpulse(o, nx * diseaseBlast + rand(-64, 64) * (0.5 + q), ny * diseaseBlast + rand(-64, 64) * (0.5 + q), { + const distanceBoost = q * q * 0.55 + q * 0.45; + const power = (260 + distanceBoost * 920) * rand(0.78, 1.24) * blastScale; + this.applyImpulse(o, nx * power + rand(-95, 95) * (0.6 + q), ny * power + rand(-95, 95) * (0.6 + q), { panic: true, target: { x, y }, - fearTimer: 1.0 + q, + fearTimer: 1.7 + q, }); - o.hurtTimer = Math.max(o.hurtTimer || 0, 1.2 + q); - o.fearTimer = Math.max(o.fearTimer || 0, 1.0 + q); - if (!o.dead && q > 0.18 && Math.random() < (o.diseaseChance ? o.diseaseChance(0.15) : 0.15)) o.infectExplosionDisease?.(t); + o.hurtTimer = Math.max(o.hurtTimer || 0, 1.5 + q * 1.8); + o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.85); + o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q); + o.state = "panic"; + o.target = { x, y }; + o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85); + o.fallMax = Math.max(o.fallMax || 0, o.fallTimer); + o.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1); + o.blastSpinTimer = Math.max(o.blastSpinTimer || 0, 1.35 + q * 0.70); + o.blastSpinMax = Math.max(o.blastSpinMax || 0, o.blastSpinTimer); + this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q); + if (!o.dead && q > 0.22 && Math.random() < (o.diseaseChance ? o.diseaseChance(0.15) : 0.15)) o.infectExplosionDisease?.(t); + if (!o.dead && Math.random() < 0.45) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); + } + this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", { + push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale, + }); + for (const ball of this.items) { + if (!ball || ball.dead || ball.type !== "ball") continue; + let dx = ball.x - x; + let dy = ball.y - y; + let d = Math.hypot(dx, dy) || 1; + if (d > blastRadius * 1.18) continue; + const q = clamp(1 - d / (blastRadius * 1.18), 0, 1); + if (d < 0.001) { + const a = rand(0, Math.PI * 2); + dx = Math.cos(a); dy = Math.sin(a); d = 1; + } + const blast = 430 + q * 960; + this.applyImpulse(ball, dx / d * blast * blastScale + rand(-80, 80), dy / d * blast * blastScale + rand(-80, 80)); + ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38); + ball.lastPokedAt = this.time || 0; + ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5); + this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" })); } t.die("\u7206\u767a\u75c5"); audio.explode(); @@ -235,7 +282,6 @@ this.items.push(it); this.itemCounts.zunchi = (this.itemCounts.zunchi || 0) + 1; audio.place?.("zunchi"); - this.spawnBubble(it.x, it.y - 16, "\u3076\u308a\u3085\u3063", "rgba(62,84,45,0.78)"); }, spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") { @@ -400,6 +446,7 @@ applyImpactDamage(target, amount, cause = "\u885d\u7a81", options = {}) { if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false; const normalized = options.cause || cause || "\u885d\u7a81"; + this.emit?.(String(normalized).includes("\u55a7\u5629") ? "fight:hit" : "impact:hit", { target, amount, cause: normalized, source: options.source || null }); if (target.damage) target.damage(amount, normalized); else if (Number.isFinite(target.hp)) target.hp -= amount; return true; @@ -422,6 +469,7 @@ attacker.defeatedById = null; victim.fightTimer = Math.min(victim.fightTimer || 0, rand(0.20, 0.42)); attacker.fightTimer = Math.min(attacker.fightTimer || 0, rand(0.20, 0.42)); + this.emit?.("fight:lost", { winner: attacker, loser: victim, damage }); victim.state = "panic"; victim.target = attacker; victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); @@ -430,7 +478,7 @@ const d = Math.hypot(dx, dy) || 1; victim.vx += dx / d * rand(38, 76); victim.vy += dy / d * rand(22, 56); - this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "はぁぁぁ!", "rgba(84,66,86,0.80)"); + this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); return true; }, @@ -451,13 +499,11 @@ loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1; winner.totalFightWins = (winner.totalFightWins || 0) + 1; loser.totalFightLosses = (loser.totalFightLosses || 0) + 1; + this.emit?.("fight:ended", { winner, loser }); winner.adjustPersonality?.("aggression", 0.018, "after winning fights."); loser.adjustPersonality?.("aggression", -0.018, "after losing fights."); - if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) { - const oldName = loser.formerName || loser.name; - this.spawnBubble(loser.x, loser.y - loser.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)"); - this.log(`${oldName}\u306f5\u56de\u55a7\u5629\u306b\u8ca0\u3051\u3001\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: [loser] }); - } + this.maybeApplyZunchiSlaveByFightRecord?.(winner, loser); + this.maybeApplyZunchiSlaveByFightRecord?.(loser, winner); loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear); this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); winner.affection = clamp(winner.affection + 0.8, 0, 80); @@ -467,6 +513,21 @@ } }, + maybeApplyZunchiSlaveByFightRecord(tarinai, other = null) { + if (!tarinai || tarinai.dead || tarinai.isZunchiSlave) return false; + const wins = Number(tarinai.totalFightWins || 0); + const losses = Number(tarinai.totalFightLosses || 0); + const total = wins + losses; + if (total < 5) return false; + const winRate = total > 0 ? wins / total : 0; + if (winRate > 0.5) return false; + const oldName = tarinai.formerName || tarinai.name; + if (!tarinai.becomeZunchiSlave?.({ fightRecord: true, locked: true })) return false; + this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)"); + this.log?.(`${oldName}\u306f\u55a7\u5629\u30925\u56de\u7d4c\u9a13\u3057\u3001\u52dd\u738750%\u4ee5\u4e0b\u3067\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] }); + return true; + }, + itemDropImpact(it) { if (!it || it.dead || it.type === "water") return; const isStone = it.type === "stone"; @@ -497,7 +558,7 @@ t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2); t.stress = clamp((t.stress || 0) + 10 + p * 18, 0, 130); t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu."); - t.thought = "げんこつがこわい"; + t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044"; } t.hurtTimer = Math.max(t.hurtTimer, isGenkotsu ? 3.4 : (isStone ? 2.7 : 1.25)); t.fallTimer = Math.max(t.fallTimer, isGenkotsu ? 0.86 : (isStone ? 0.62 : 0.26)); diff --git a/js/world_family_social.js b/js/world_family_social.js index 2cebe07..87b6c04 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -70,7 +70,6 @@ this.updateEffectCounts(); this.markTerrainDirty?.("reset"); this.rebuildSpatial(true); - this.log("\u305f\u308a\u306a\u3044\u3092\u89b3\u5bdf\u7bb1\u306b\u914d\u7f6e\u3057\u307e\u3057\u305f\u3002"); showToast("\u65b0\u3057\u3044\u89b3\u5bdf\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002"); syncTopButtons(); }, @@ -130,6 +129,9 @@ const nextParents = uniqueIds(t.parents || []); const nextChildren = uniqueIds(t.children || []); const nextPersonalityTags = getPersonalityTraitTags(t); + const nextScale = Number.isFinite(t.scale) ? t.scale : (Number.isFinite(t.adultScale) ? t.adultScale : 0.28); + const nextAdultScale = Number.isFinite(t.adultScale) ? t.adultScale : nextScale; + const nextGrowth = Number.isFinite(t.growth) ? t.growth : 1; if (!this.family[familyKey]) { this.family[familyKey] = { id: familyKey, @@ -142,6 +144,9 @@ parentNames: [...(t.parentNames || [])], children: nextChildren, birthTime: t.birthTime, + scale: nextScale, + adultScale: nextAdultScale, + growth: nextGrowth, alive: !t.dead, deathReason: t.deathReason || "", }; @@ -154,6 +159,9 @@ if (!sameList(entry.personalityTags, nextPersonalityTags)) changed = true; if (entry.hasPaired !== !!t.hasPaired) changed = true; if (entry.generation !== t.generation) changed = true; + if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true; + if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true; + if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true; if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true; if (entry.alive !== nextAlive) changed = true; if (!sameList(entry.parents, nextParents)) changed = true; @@ -164,6 +172,9 @@ entry.personalityTags = nextPersonalityTags; entry.alive = nextAlive; entry.generation = t.generation; + entry.scale = nextScale; + entry.adultScale = nextAdultScale; + entry.growth = nextGrowth; entry.hasPaired = !!t.hasPaired; entry.deathReason = t.deathReason || entry.deathReason || ""; entry.parents = nextParents; @@ -220,6 +231,9 @@ parentNames: live?.parentNames ? [...live.parentNames] : [...(n.parentNames || [])], children, birthTime: live?.birthTime ?? n.birthTime ?? 0, + scale: Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : (Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : 0.28))), + adultScale: Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : (Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : 0.28))), + growth: Number.isFinite(live?.growth) ? live.growth : (Number.isFinite(n.growth) ? n.growth : 1), alive: live ? !live.dead : n.alive !== false, deathReason: live?.deathReason || n.deathReason || "", }; @@ -414,12 +428,13 @@ this.drawListDirty = true; if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t); this.maxGeneration = Math.max(this.maxGeneration, t.generation); + this.emit?.("tarinai:born", { tarinai: t, source: opts.parents?.length ? "family" : "spawn" }); return t; }, startBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) return false; - if (a.isZunchiSlave || b.isZunchiSlave) return false; + if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false; if (this.areParentChild(a, b)) return false; if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false; if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false; @@ -457,7 +472,7 @@ a.birthPartnerId = b.birthPartnerId = null; a.birthRitualLeader = b.birthRitualLeader = false; a.state = b.state = "idle"; - if (reproductionBlockedByDisease) { + if (reproductionBlockedByDisease || (!!a.isZunchiSlave !== !!b.isZunchiSlave)) { a.birthRitualRole = b.birthRitualRole = 0; return null; } @@ -526,29 +541,41 @@ }, spawnChild(a, b, index = 0, total = 1) { + const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave); const inherited = Math.random() < 0.56 ? a.type : b.type; - const mutation = Math.random() < 0.18 ? baseSpriteId() : inherited; - const childBirthPersonality = personalityFromParents(a, b); + const mutation = zunchiSlaveChild ? "zunchi_slave" : (Math.random() < 0.18 ? baseSpriteId() : inherited); + const childSeed = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}-${index}`; + const childGenetics = inheritedGenetics(a, b, childSeed); + const childAdultScale = adultScaleFromGenetics(childSeed, childGenetics); + const childBirthPersonality = zunchiSlaveChild ? zunchiSlavePersonalityValue() : personalityFromParents(a, b); const child = this.addTarinai({ + familyKey: childSeed, type: mutation, birthPersonality: childBirthPersonality, currentPersonality: childBirthPersonality, - name: makeName(), + genetics: childGenetics, + adultScale: childAdultScale, + name: zunchiSlaveChild ? "\u305a\u3093\u3061\u3069\u308c\u3044" : makeName(), + isZunchiSlave: zunchiSlaveChild, age: 0, x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18, y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0), - scale: rand(0.17, 0.23), + scale: scaleForGrowth(childAdultScale, 0), hunger: rand(20, 40), loneliness: rand(30, 56), energy: rand(55, 95), stress: rand(0, 14), need: Math.random() < 0.6 ? pick([a.need, b.need]) : pick(NEEDS), generation: Math.max(a.generation, b.generation) + 1, - lifeSpan: rand(1500, 2400), + lifeSpan: rand(1500, 2200) * (childGenetics.lifeSpanMul || 1), parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)], parentNames: [a.name, b.name], birthTime: this.time, + powerItemMode: "", + sizeItemMode: "", + lifeItemMode: "", }); + if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true }); this.maxGeneration = Math.max(this.maxGeneration, child.generation); a.hasPaired = true; b.hasPaired = true; @@ -654,8 +681,14 @@ if (!actor || !target || actor.dead || target.dead) return false; if (actor.lowHealthSprite && actor.lowHealthSprite()) return false; const fearLoad = (target.relationTo(actor.id).fear || 0) * 0.72 + target.personalityProfile().fear * 8; - const resistance = target.energy * 0.30 + target.mood * 0.16 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5); - const successChance = clamp(0.62 + (actorScore - targetScore + fearLoad - resistance) / 76, 0.50, 0.96); + const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0); + const targetAggression = Math.max(0, target.currentPersonality?.aggression || 0); + const actorSize = actor.effectiveScale ? actor.effectiveScale() : (actor.scale || 0.28); + const targetSize = target.effectiveScale ? target.effectiveScale() : (target.scale || 0.28); + const sizeEdge = clamp((actorSize - targetSize) / 0.16, -1, 1); + const aggressionEdge = clamp(actorAggression - targetAggression * 0.55, -1, 1); + const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5); + const successChance = clamp(0.56 + (actorScore - targetScore + fearLoad - resistance) / 82 + aggressionEdge * 0.13 + sizeEdge * 0.16, 0.30, 0.96); actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20); target.surpriseTimer = Math.max(target.surpriseTimer, 0.32); actor.fightCooldown = CONFIG.fightCooldown + rand(1, 4); @@ -672,9 +705,19 @@ target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.65, 2.45)); target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear); if (Math.random() >= successChance) { + actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation"); target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate"); + actor.intimidateTimer = 0; + actor.intimidateTargetId = null; + target.intimidatedTimer = 0; + if (actor.state === "intimidate") actor.state = "idle"; + if (target.state === "panic") target.state = "idle"; + actor.spriteLockUntil = 0; + target.spriteLockUntil = 0; + this.startFight(actor, target); return true; } + actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation"); target.defeatedById = actor.id; target.fightWinnerId = actor.id; target.defeatedTimer = Math.max(target.defeatedTimer, rand(2.8, 4.2)); diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 9ffae69..1a781b8 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -197,7 +197,7 @@ return null; } if (dropped) { - item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : 0.58); + item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : (item.type === "pushpin" ? 0.48 : 0.58)); item.dropTimer = item.dropMax; item.dropImpactDone = false; } @@ -205,6 +205,7 @@ this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1; this.markTerrainDirty?.(`place:${item.type}`); this.emit("tool:place", { item, type: item.type, dropped }); + this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type }); if (dropped) audio.place?.(item.type); this.rebuildSpatial(true); return item; @@ -237,8 +238,6 @@ if (target) { target.poke(); if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "observe", { participants: [target] }); - } else { - this.log("\u7a7a\u6c17\u3092\u3064\u3064\u3044\u305f\u3002\u305d\u3053\u306b\u306f\u4f55\u3082\u305f\u308a\u306a\u304b\u3063\u305f\u3002", "observe", { hiddenFromObservation: true, countInStats: false }); } return; } @@ -263,6 +262,7 @@ if (d < hit && d < bestD) { best = it; bestD = d; } } if (!best) { showToast("\u524a\u9664\u3067\u304d\u308b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; } + if (best.type === "pushpin" && best.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u753b\u92f2\u306f\u3064\u307e\u3093\u3067\u53d6\u308a\u9664\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return; } best.amount = 0; audio.delete?.(); this.compactItems(); @@ -281,13 +281,13 @@ const placed = this.placeItem(rawItem, true); if (!placed) return; const label = toolLabel(itemType); - this.log(itemType === "genkotsu" ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); + this.log(itemType === "genkotsu" || itemType === "pushpin" ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`); } }, inferLogKind(text) { const s = String(text || "").toLowerCase(); - if (s.includes("配置") || s.includes("設置") || s.includes("placed") || s.includes("selected")) return "observe"; + if (s.includes("\u914d\u7f6e") || s.includes("\u8a2d\u7f6e") || s.includes("placed") || s.includes("selected")) return "observe"; if (s.includes("firecracker") || s.includes("accident") || s.includes("drop") || s.includes("\u7206\u7af9") || s.includes("\u843d\u3061") || s.includes("\u4e8b\u6545") || s.includes("\u5439\u304d\u98db")) return "accident"; if (s.includes("fight") || s.includes("lost to") || s.includes("\u55a7\u5629") || s.includes("\u8ca0\u3051")) return "fight"; if (s.includes("appeared") || s.includes("wandered in") || s.includes("birth") || s.includes("\u73fe\u308c") || s.includes("\u8ff7\u3044\u8fbc")) return "birth"; @@ -311,24 +311,24 @@ const text = String(entry.text || ""); if (kind === "death") return audio.death?.(); if (kind === "birth") return audio.birth?.(); - if (kind === "fight") return /決着|勝|負|覚え/.test(text) ? audio.fightFinish?.() : audio.fight?.(); + if (kind === "fight") return /\u6c7a\u7740|\u52dd|\u8ca0|\u899a\u3048/.test(text) ? audio.fightFinish?.() : audio.fight?.(); if (kind === "food") return audio.eat?.(); if (kind === "grass") return audio.play?.("sfx_grass", { category: "ops", minGap: 0.25 }); if (kind === "weather") return audio.phase?.(); if (kind === "relation") return audio.notify?.(); if (kind === "accident") { - if (/配置|置いた|設置/.test(text)) return audio.place?.(/爆竹/.test(text) ? "firecracker" : ""); - if (/げんこつ/.test(text)) return audio.genkotsuImpact?.(); - if (/爆竹|爆発/.test(text)) return audio.explode?.(); - if (/石/.test(text)) return audio.stoneImpact?.(); - if (/ボール/.test(text)) return audio.ballHit?.(); + if (/\u914d\u7f6e|\u7f6e\u3044\u305f|\u8a2d\u7f6e/.test(text)) return audio.place?.(/\u7206\u7af9/.test(text) ? "firecracker" : ""); + if (/\u3052\u3093\u3053\u3064/.test(text)) return audio.genkotsuImpact?.(); + if (/\u7206\u7af9|\u7206\u767a/.test(text)) return audio.explode?.(); + if (/\u77f3/.test(text)) return audio.stoneImpact?.(); + if (/\u30dc\u30fc\u30eb/.test(text)) return audio.ballHit?.(); return audio.damage?.(18); } if (kind === "event") { - if (/女王アリ/.test(text)) return audio.queenAnt?.(); - if (/アリ|巣/.test(text)) return audio.antNest?.(); - if (/病|発病/.test(text)) return audio.disease?.(); - if (/回復|治/.test(text)) return audio.heal?.(); + if (/\u5973\u738b\u30a2\u30ea/.test(text)) return audio.queenAnt?.(); + if (/\u30a2\u30ea|\u5de3/.test(text)) return audio.antNest?.(); + if (/\u75c5|\u767a\u75c5/.test(text)) return audio.disease?.(); + if (/\u56de\u5fa9|\u6cbb/.test(text)) return audio.heal?.(); } }, diff --git a/js/world_spatial_budget.js b/js/world_spatial_budget.js index d812ad8..92145b9 100644 --- a/js/world_spatial_budget.js +++ b/js/world_spatial_budget.js @@ -3,6 +3,13 @@ (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_spatial_budget.js"); + + function liveCount(list) { + let count = 0; + for (const item of list || []) if (item && !item.dead) count += 1; + return count; + } + Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ performanceLevel() { if (window.TarinaiPerformance) return window.TarinaiPerformance.level || 0; @@ -33,8 +40,8 @@ sortedDrawList() { if (!this.drawList) this.drawList = []; - const antCount = (this.ants || []).filter(a => a && !a.dead).length; - const tarinaiCount = (this.tarinai || []).filter(t => t && !t.dead).length; + const antCount = liveCount(this.ants); + const tarinaiCount = liveCount(this.tarinai); if (this.drawListDirty || this.drawList.length !== tarinaiCount + antCount) { this.drawList.length = 0; for (const t of this.tarinai || []) if (t && !t.dead) this.drawList.push(t); diff --git a/js/world_update.js b/js/world_update.js index a237dbc..8ab961d 100644 --- a/js/world_update.js +++ b/js/world_update.js @@ -116,7 +116,7 @@ const previousDay = this.day || 1; this.day = Math.floor(this.time / CONFIG.dayLength) + 1; if (this.day !== previousDay) { - for (const t of this.tarinai || []) if (t) t.personalityDaily = { day: this.day, total: 0, byKey: {} }; + for (const t of this.tarinai || []) if (t) t.personalityDaily = { day: this.day, total: 0, byKey: {}, byCause: {} }; } if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4); this.shakeTimer = Math.max(0, (this.shakeTimer || 0) - dt); @@ -212,24 +212,17 @@ } } - if (this.weather === "light_rain" && (this.itemCounts.water || 0) < 24 && Math.random() < dt * 0.12) { - const drop = new Item("water", rand(58, this.w - 58), rand(58, this.h - 58)); - drop.amount = rand(24, 58); + // Rain visuals are diagonal line streaks in render.js. This branch creates + // horizontal oval water items spawned by rain; it intentionally does not + // share the rain overlay renderer. + if (window.TarinaiWeatherSystem?.shouldDropRainWater?.(this, dt)) { + const drop = window.TarinaiWeatherSystem.createRainWaterItem(this); this.items.push(drop); this.itemCounts.water = (this.itemCounts.water || 0) + 1; this.markTerrainDirty?.("rain-water"); + this.emit?.("rain:itemDropped", { item: drop, type: "water" }); } - - if (Math.random() < dt * 0.004) { - const entries = [ - "\u4f55\u5339\u304b\u304c\u3001\u5168\u54e1\u3067\u306f\u306a\u3044\u304c\u3001\u540c\u3058\u65b9\u5411\u3092\u898b\u305f\u3002", - "\u98df\u3079\u7269\u306f\u3042\u308b\u3002\u8db3\u308a\u3066\u3044\u308b\u304b\u306f\u5225\u554f\u984c\u3002", - "\u4e00\u5339\u304c\u7720\u308a\u3001\u8fd1\u304f\u306e\u5225\u306e\u4e00\u5339\u3082\u7720\u308b\u3053\u3068\u3092\u8003\u3048\u305f\u3002", - "\u8349\u306e\u8fd1\u304f\u306b\u3001\u77ed\u304f\u3066\u610f\u5473\u306e\u306a\u3044\u5217\u304c\u3067\u304d\u305f\u3002", - `${this.phaseName()}\u3002\u6c17\u5206\u306f\u3086\u3063\u304f\u308a\u5909\u308f\u3063\u3066\u3044\u308b\u3002` - ]; - this.log(pick(entries)); - } + }, sanitizeOutOfBounds() { @@ -310,16 +303,18 @@ updateWeather(dt) { this.nextWeatherChange -= dt; if (this.nextWeatherChange > 0) return; + if (window.TarinaiWeatherSystem?.applyNextWeather) { + window.TarinaiWeatherSystem.applyNextWeather(this); + return; + } const current = this.weather || "sunny"; - const choices = current === "sunny" - ? ["sunny", "cloudy", "light_rain"] - : current === "cloudy" - ? ["sunny", "cloudy", "light_rain"] - : ["sunny", "cloudy"]; - const next = pick(choices); + const next = pick(current === "light_rain" ? ["sunny", "cloudy"] : ["sunny", "cloudy", "light_rain"]); this.weather = next; this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); - if (next !== current) this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); + if (next !== current) { + this.emit?.("weather:changed", { previous: current, next }); + this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`); + } } })); })(typeof window !== "undefined" ? window : globalThis); diff --git a/scripts/generate_item_icons.py b/scripts/generate_item_icons.py index 872b64f..50577df 100644 --- a/scripts/generate_item_icons.py +++ b/scripts/generate_item_icons.py @@ -6,11 +6,20 @@ appearance. Do not invent separate tool-UI-only designs. """ from __future__ import annotations from pathlib import Path -import math +import json, math, re from PIL import Image, ImageDraw ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "assets" / "ui" +REGISTRY = ROOT / "js" / "item_visual_registry.js" + + +def visual_registry() -> dict: + text = REGISTRY.read_text(encoding="utf-8") + match = re.search(r"String\.raw`([\s\S]*?)`", text) + if not match: + raise RuntimeError("Could not find JSON visual registry in js/item_visual_registry.js") + return json.loads(match.group(1)) def save_icon(name: str, draw_fn) -> None: @@ -29,6 +38,45 @@ def draw_drop(fill, stroke, highlight): return fn +def rgba(value, fallback): + if not value: + return fallback + if value.startswith("#"): + value = value.lstrip("#") + if len(value) == 6: + return tuple(int(value[i:i+2], 16) for i in (0, 2, 4)) + (255,) + match = re.match(r"rgba?\(([^)]+)\)", value.replace(" ", "")) + if match: + parts = match.group(1).split(",") + r, g, b = (int(float(parts[i])) for i in range(3)) + a = int(float(parts[3]) * 255) if len(parts) > 3 else 255 + return (r, g, b, a) + return fallback + + +def draw_powder(fill, stroke, highlight): + def fn(d: ImageDraw.ImageDraw) -> None: + d.ellipse([18, 62, 78, 76], fill=(60, 45, 35, 36)) + for i in range(20): + x = 48 + math.cos(i * 1.9) * (8 + (i % 5) * 4) + y = 56 - abs(math.sin(i * 1.3)) * 24 + (i % 3) * 3 + r = 5 + (i % 4) + d.ellipse([x - r, y - r * 0.65, x + r, y + r * 0.65], fill=highlight if i % 4 == 0 else fill, outline=stroke, width=1) + d.ellipse([31, 31, 52, 39], fill=rgba("rgba(255,255,255,0.62)", (255, 255, 255, 158))) + return fn + + +def draw_pill(fill, stroke, highlight, scale=1.0): + def fn(d: ImageDraw.ImageDraw) -> None: + cx, cy = 48, 48 + w, h = 66 * scale, 28 * scale + box = [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2] + d.rounded_rectangle(box, radius=int(h / 2), fill=fill, outline=stroke, width=4) + d.line([(cx, cy - h / 2 + 2), (cx, cy + h / 2 - 2)], fill=highlight, width=4) + d.ellipse([cx - w * 0.32, cy - h * 0.24, cx - w * 0.08, cy - h * 0.04], fill=(255, 255, 255, 110)) + return fn + + def draw_laxative(d: ImageDraw.ImageDraw) -> None: d.rounded_rectangle([14, 28, 82, 68], radius=15, fill=(244, 212, 154, 255), outline=(98, 61, 29, 235), width=4) # Zunchi mound motif inside the tablet. @@ -68,13 +116,31 @@ def draw_sleep(d: ImageDraw.ImageDraw) -> None: def main() -> None: - save_icon("tool_laxative.webp", draw_laxative) - save_icon("tool_mystery_drug.webp", draw_mystery) - save_icon("tool_ammo.webp", draw_bullet) - save_icon("tool_sleep_drug.webp", draw_sleep) - save_icon("tool_water.webp", draw_drop((86, 157, 224, 220), (45, 101, 168, 150), (255, 255, 255, 150))) - save_icon("tool_zunda_juice.webp", draw_drop((125, 218, 82, 235), (63, 150, 50, 160), (238, 255, 226, 160))) - save_icon("tool_mercury.webp", draw_drop((210, 216, 222, 240), (112, 122, 132, 165), (255, 255, 255, 170))) + registry = visual_registry() + renderer_map = { + "laxative_tablet": draw_laxative, + "mystery_tablet": draw_mystery, + "bullet": draw_bullet, + "sleep_tablet": draw_sleep, + } + for item_id, visual in registry.items(): + if item_id == "ball": + continue + palette = visual.get("palette") or {} + renderer = visual.get("renderer") + fill = rgba(palette.get("fill"), (255, 250, 241, 255)) + stroke = rgba(palette.get("stroke"), (141, 111, 72, 220)) + highlight = rgba(palette.get("highlight") or palette.get("accent"), (255, 255, 255, 150)) + if renderer == "oval_droplet": + draw_fn = draw_drop(fill, stroke, highlight) + elif renderer == "powder_pile": + draw_fn = draw_powder(fill, stroke, highlight) + elif renderer == "split_pill": + draw_fn = draw_pill(fill, stroke, highlight, visual.get("uiPreviewScale") or visual.get("scale") or 1.0) + else: + draw_fn = renderer_map.get(renderer) + if draw_fn: + save_icon(f"tool_{item_id}.webp", draw_fn) if __name__ == "__main__": diff --git a/service-worker.js b/service-worker.js index d92003b..a994443 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,11 +1,11 @@ "use strict"; -const APP_VERSION = "15.20.3"; +const APP_VERSION = "15.20.16"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files. const coreScriptNames = [ - "version", "event_bus", "performance_manager", "effect_registry", "sound_pack", "data", "math", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_selected", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools" + "version", "event_bus", "performance_manager", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_selected", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools" ]; const CORE_ASSETS = [ "./",