diff --git a/AI_MAP.md b/AI_MAP.md index 36b8cf4..6c04a96 100644 --- a/AI_MAP.md +++ b/AI_MAP.md @@ -6,7 +6,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th - Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order. - Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader. -- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.101`. +- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.103`. - Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render. - Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`. - Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents. diff --git a/BUILD_NOTE.txt b/BUILD_NOTE.txt index 72ad9b0..106d468 100644 --- a/BUILD_NOTE.txt +++ b/BUILD_NOTE.txt @@ -3,4 +3,4 @@ tarinai_king_fix_v10 - Burning tarinai and ants draw lightweight flame-shaped overlays. - Flame firecracker is redder and shows a countdown timer; normal firecracker is darker. - Standalone flame and fire particles use a simpler flame silhouette. -- Version: 39.15.101 +- Version: 39.15.102 diff --git a/FILES.json b/FILES.json index fd9ff73..376200a 100644 --- a/FILES.json +++ b/FILES.json @@ -1,6 +1,6 @@ { "purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.", - "version": "39.15.101", + "version": "39.15.103", "entrypoints": { "app": "index.html", "runtime": "js/main.js", diff --git a/GAME_FEATURES.md b/GAME_FEATURES.md index d71fcb5..8a77557 100644 --- a/GAME_FEATURES.md +++ b/GAME_FEATURES.md @@ -16,7 +16,8 @@ The core loop is: ## Main UI - **Canvas / field**: the main simulation area. Tarinai, items, effects, ants, weather, and terrain are drawn here. -- **Top buttons**: pause/resume, speed control, ecology guide, sound settings, credits, save, and reset. +- **Top buttons**: pause/resume, speed control, ecology guide, sound settings, credits, save, display settings, and reset. +- **Display settings**: manually adjust effects, shadows, and drawing detail. `High` keeps quality-preserving optimizations active, while lower tiers cut heavier visuals for crowded scenes without lowering object limits. - **Mobile mode controls**: auto, camera, and tool modes for touch screens. - **Right panel tabs**: jump between main game, selected tarinai data, tools, colony, events, and family tree. - **Tarinai data panel**: appears when a tarinai is selected. Shows health, needs, personality, active effects, relations, inventory, behavior reason, and records. diff --git a/README.md b/README.md index 5376277..b85e8c8 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small; ## Runtime Shape -- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.101`. +- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.103`. - Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots. - Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases. - UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`. diff --git a/app_manifest.json b/app_manifest.json index 120434a..c7f4c56 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.101", + "version": "39.15.107", "css": [ "css/base.css", "css/layout.css", diff --git a/css/panel.css b/css/panel.css index c9818a8..0dc1763 100644 --- a/css/panel.css +++ b/css/panel.css @@ -412,27 +412,27 @@ body::before { width: 100% !important; max-width: 100% !important; position: static !important; + display: flex !important; + flex-direction: column !important; + gap: 1px !important; } .lineage-family-tree .tree-node .family-copy strong, .lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { max-width: 100% !important; } .lineage-family-tree .tree-node .family-copy > .family-personality-tags { - position: absolute !important; - left: 7px !important; - right: 7px !important; - bottom: 5px !important; - width: auto !important; - max-width: none !important; - height: 26px !important; - max-height: 26px !important; - margin: 0 !important; - grid-auto-rows: 12px !important; + position: static !important; + width: 100% !important; + max-width: 100% !important; + height: 24px !important; + max-height: 24px !important; + margin: 2px 0 0 !important; + grid-auto-rows: 11px !important; gap: 1px 3px !important; z-index: 3 !important; } .lineage-family-tree .tree-node .family-personality-tags em { - height: 12px !important; - line-height: 10px !important; - font-size: 7px !important; + height: 11px !important; + line-height: 9px !important; + font-size: 6.8px !important; } diff --git a/index.html b/index.html index f829e79..fb2185f 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -191,20 +191,25 @@
エフェクト破片、雨、光の演出
- + + +
地面の落ち影とスプライトの影
- + + +
描画装飾と解像度の設定
+
@@ -252,164 +257,164 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/fire_runtime_system.js b/js/fire_runtime_system.js index 47bb40e..1e88fd4 100644 --- a/js/fire_runtime_system.js +++ b/js/fire_runtime_system.js @@ -428,6 +428,7 @@ function updateFirecracker(item, dt, worldRef) { if (fire) { fire.vx = 0; fire.vy = 0; + updateFireContacts(fire, FIRE_CONTACT_INTERVAL, worldRef); } pushFireEffect(worldRef, item.x + Math.cos(a) * Math.min(d, 28), item.y + Math.sin(a) * Math.min(d, 28), 14, item); } diff --git a/js/health.js b/js/health.js index d3514ba..7ed0c23 100644 --- a/js/health.js +++ b/js/health.js @@ -11,6 +11,7 @@ const HEALTH = (() => { stress: "\u30b9\u30c8\u30ec\u30b9\u904e\u591a", poke: "\u3064\u3064\u304b\u308c\u3059\u304e", firecracker: "\u7206\u7af9", + fire: "\u708e\u4e0a", genkotsu: "\u3052\u3093\u3053\u3064", shooting: "\u5c04\u6483", stoneCrush: "\u77f3\u306b\u6f70\u3055\u308c\u305f", @@ -48,6 +49,7 @@ const HEALTH = (() => { 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 (/\u708e\u4e0a|\u706b|\u708e|\u71c3|burn|fire/.test(text)) return CAUSES.fire; 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|\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316/.test(text)) return CAUSES.collision; if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke; @@ -117,6 +119,7 @@ const HEALTH = (() => { return CAUSES.accident; } + if (direct === CAUSES.fire || recent === CAUSES.fire || t.lastMajorDamageCause === CAUSES.fire) return CAUSES.fire; if (recentMajor && t.lastMajorDamageCause === CAUSES.collision && !(t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) return CAUSES.collision; if (recentMajor && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { return weakenedDeathReasonFor(t.lastMajorDamageCause); @@ -151,7 +154,7 @@ const HEALTH = (() => { const cause = dominantDeathCause(t, text, opts) || CAUSES.accident; if (cause.includes(WEAKENED_SUFFIX)) return cause; if (opts.fromDamage) return cause; - const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu && cause !== CAUSES.weakness && cause !== CAUSES.freeze && cause !== CAUSES.heatstroke; + const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu && cause !== CAUSES.fire && cause !== CAUSES.weakness && cause !== CAUSES.freeze && cause !== CAUSES.heatstroke; if (canWeaken && (opts.weakened || (hasRecentMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause); return cause; } diff --git a/js/item_lifecycle_decay_system.js b/js/item_lifecycle_decay_system.js index 8f7f4a5..6d0f6df 100644 --- a/js/item_lifecycle_decay_system.js +++ b/js/item_lifecycle_decay_system.js @@ -26,6 +26,12 @@ changed = true; item.foodServingsRemaining = Math.max(0, before - lost); item.amount = item.foodServingsRemaining; + const resized = (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); + if (resized) { + worldRef?.markItemBucketsDirty?.("food-passive-resize"); + worldRef?.markSpatialDirty?.("food-passive-resize"); + if (worldRef) worldRef.drawListDirty = true; + } worldRef?.markTerrainDirty?.("food-passive-decay"); worldRef?.emit?.("item:decayed", { item, type: item.type, amount: lost, passive: true }); if (item.foodServingsRemaining <= 0.015) item.amount = 0; diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index 6d6f9f8..6736c07 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -13,7 +13,7 @@ draw(ctx, t, lighting = null) { } const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null; const forceOpaque = this.type === "slave_chain" || this.type === "golden_crown"; - const visibleAlpha = forceOpaque ? 1 : (isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1)); + const visibleAlpha = forceOpaque ? 1 : (isServingFoodType(this.type) ? clamp(0.72 + (servingRatio ?? 1) * 0.28, 0.72, 1) : clamp(this.amount / 40, 0.24, 1)); const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0; const dropFallHeight = this.type === "genkotsu" ? Math.max(360, this.r * 5.8) : 170; const dropY = dropT > 0 ? -dropFallHeight * dropT : 0; diff --git a/js/item_tool_definitions.js b/js/item_tool_definitions.js index 5719f83..b22a327 100644 --- a/js/item_tool_definitions.js +++ b/js/item_tool_definitions.js @@ -13,7 +13,7 @@ const TOOL_DEFINITIONS = Object.freeze({ poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" }, pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" }, new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" }, - water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, + water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" }, rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" }, shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" }, diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index 6970dfc..262a467 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -205,6 +205,7 @@ function initializeItemTypeState(item, type, x, y) { item.foodServingsRemaining = item.foodServingsMax; item.passiveFoodDecayTimer = rand(0, passiveFoodDecayInterval()); item.amount = item.foodServingsRemaining; + (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } return item; } diff --git a/js/items.js b/js/items.js index 4797cd3..59d5b85 100644 --- a/js/items.js +++ b/js/items.js @@ -14,6 +14,29 @@ function foodServingsForSize(size = "medium") { function isServingFoodType(type = "") { return FOOD_SERVING_TYPES.has(type); } +function foodServingSizeForRemaining(remaining = FOOD_SERVINGS_BY_SIZE.medium) { + const n = Math.max(0, Number(remaining) || 0); + if (n <= FOOD_SERVINGS_BY_SIZE.small + 0.05) return "small"; + if (n <= FOOD_SERVINGS_BY_SIZE.medium + 0.05) return "medium"; + return "large"; +} +function foodServingVisualScaleFor(item) { + if (!item || !isServingFoodType(item.type)) return 1; + const remaining = Math.max(0, Number(item.foodServingsRemaining ?? item.amount ?? FOOD_SERVINGS_BY_SIZE.medium) || 0); + const ratio = remaining / FOOD_SERVINGS_BY_SIZE.medium; + return clamp(Math.sqrt(Math.max(0.04, ratio)), 0.72, 1.35); +} +function updateServingFoodVisualSize(item) { + if (!item || !isServingFoodType(item.type)) return false; + const baseRadius = itemRadiusFor(item.type, 12); + const scale = foodServingVisualScaleFor(item); + const nextR = Math.max(5, baseRadius * scale); + const changed = Math.abs((Number(item.r) || 0) - nextR) > 0.05; + item.r = nextR; + item.foodServingScale = scale; + item.foodServingVisualSize = foodServingSizeForRemaining(item.foodServingsRemaining ?? item.amount); + return changed; +} const PASSIVE_FOOD_DECAY_PER_SECOND = 0.004; const GRASS_STAGE_COUNT = 5; @@ -600,6 +623,9 @@ function drawToolItemPreview(ctx, type = "", opts = {}) { if (typeof window !== "undefined") { window.isConsumableSpriteType = isConsumableSpriteType; + window.foodServingSizeForRemaining = foodServingSizeForRemaining; + window.foodServingVisualScaleFor = foodServingVisualScaleFor; + window.updateServingFoodVisualSize = updateServingFoodVisualSize; window.drawConsumableFieldSprite = drawConsumableFieldSprite; window.drawStoredConsumableSprite = drawStoredConsumableSprite; window.drawToolItemPreview = drawToolItemPreview; diff --git a/js/perf_profiler.js b/js/perf_profiler.js index 34ffb26..34dfc9d 100644 --- a/js/perf_profiler.js +++ b/js/perf_profiler.js @@ -19,11 +19,11 @@ let lastQualityCheckAt = 0; const storageKey = "tarinai_visual_settings_v1"; const allowed = { - effects: new Set(["off", "on"]), - shadows: new Set(["off", "on"]), - details: new Set(["low", "high"]), + effects: new Set(["off", "low", "mid", "high"]), + shadows: new Set(["off", "low", "mid", "high"]), + details: new Set(["low", "mid", "high"]), }; - const defaults = Object.freeze({ effects: "on", shadows: "on", details: "high" }); + const defaults = Object.freeze({ effects: "high", shadows: "high", details: "high" }); let visualSettings = loadVisualSettings(); let resizeRequested = false; @@ -83,11 +83,12 @@ const raw = String(value || ""); if (key === "effects" || key === "shadows") { if (raw === "off") return "off"; - if (raw === "on" || raw === "low" || raw === "mid" || raw === "high") return "on"; + if (raw === "on") return "high"; + if (raw === "low" || raw === "mid" || raw === "high") return raw; } if (key === "details") { if (raw === "low") return "low"; - if (raw === "high" || raw === "mid") return "high"; + if (raw === "mid" || raw === "high") return raw; } return defaults[key]; } @@ -119,6 +120,7 @@ function dprScaleForDetails(details) { if (details === "low") return 0.72; + if (details === "mid") return 0.86; return 1; } @@ -182,7 +184,7 @@ function renderQualityTier() { return visualSettings.details; } function shadowQualityTier() { return visualSettings.shadows; } function effectsQualityTier() { return visualSettings.effects; } - function simulationQualityTier() { return visualSettings.details; } + function simulationQualityTier() { return "high"; } function dprScaleValue() { return dprScaleForDetails(visualSettings.details); } function consumeResizeRequest() { const v = resizeRequested; diff --git a/js/placement_preview_system.js b/js/placement_preview_system.js index 8c6338b..08050b7 100644 --- a/js/placement_preview_system.js +++ b/js/placement_preview_system.js @@ -80,6 +80,12 @@ const sizeName = opts.toolSize ?? toolSizeName(worldRef, type); const r = (opts.r ?? itemRadius(type, 12)) * scale; const item = { type, x: num(x), y: num(y), r, toolSize: sizeName, foodServingScale: scale, dead: false, isPlacementPreview: true }; + if (isServingFoodType(type)) { + item.foodServingsMax = foodServingsForSize(sizeName); + item.foodServingsRemaining = item.foodServingsMax; + item.amount = item.foodServingsRemaining; + (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); + } if (isRotatable(type)) item.angle = opts.angle ?? (worldRef?.toolAngleFor ? worldRef.toolAngleFor(type) : defaultAngle(type)); if (type === "reciprocator") { const axis = item.angle ?? defaultAngle(type); diff --git a/js/save_codec.js b/js/save_codec.js index 7382f0c..428898a 100644 --- a/js/save_codec.js +++ b/js/save_codec.js @@ -426,6 +426,16 @@ if (SERVING_FOOD_TYPE_IDS.has(typeId)) return false; return true; } + function defaultStructureHp10(typeId) { + if (ITEM_TYPE_IDS[typeId] === "grass_bed") return 1000; + if (ITEM_TYPE_IDS[typeId] === "plushie") return 10; + return 10; + } + function defaultNeedsFromVitals(hunger = 0, energy = 0, sleepPressure = 0) { + const food = Math.max(0, Math.min(100, Math.round(Number(hunger) || 0))); + const sleep = Math.max(0, Math.min(100, Math.round(Math.max(Number(sleepPressure) || 0, 100 - (Number(energy) || 0))))); + return [food, sleep, 0, 0, 0, 0]; + } function writeRelationships(writer, rows = []) { const list = Array.isArray(rows) ? rows : []; writer.u(list.length); @@ -523,19 +533,17 @@ const defaultPersonality = seedPersonalityArray(birthSeed, flags); const defaultTimers = [0, 0, 0, 0, 0, 0, 0, 0, 100]; let bodyMask = 0; - if (arrayHasValues(row[9], [0, 0, 0, 0, 0, 0])) bodyMask |= 1; - if (arrayHasValues(row[10], defaultPersonality)) bodyMask |= 2; - if ((row[11] || []).length) bodyMask |= 4; - if ((row[12] || []).length) bodyMask |= 8; - if (arrayHasValues(row[13], defaultTimers)) bodyMask |= 16; - if (arrayHasValues(row[14], [0, 0, 0])) bodyMask |= 32; - writeBitFields(writer, [bodyMask], [6]); - if (bodyMask & 1) writeFixedPacked(writer, row[9], 6, 6, 2); - if (bodyMask & 2) writeFixedDiffPacked(writer, defaultPersonality, row[10], 4, 8); - if (bodyMask & 4) writeIndexList(writer, row[11]); - if (bodyMask & 8) writeRelationships(writer, row[12]); - if (bodyMask & 16) writeTimers(writer, row[13]); - if (bodyMask & 32) writeModes(writer, row[14]); + if (arrayHasValues(row[10], defaultPersonality)) bodyMask |= 1; + if ((row[11] || []).length) bodyMask |= 2; + if ((row[12] || []).length) bodyMask |= 4; + if (arrayHasValues(row[13], defaultTimers)) bodyMask |= 8; + if (arrayHasValues(row[14], [0, 0, 0])) bodyMask |= 16; + writeBitFields(writer, [bodyMask], [5]); + if (bodyMask & 1) writeFixedDiffPacked(writer, defaultPersonality, row[10], 4, 8); + if (bodyMask & 2) writeIndexList(writer, row[11]); + if (bodyMask & 4) writeRelationships(writer, row[12]); + if (bodyMask & 8) writeTimers(writer, row[13]); + if (bodyMask & 16) writeModes(writer, row[14]); let tailMask = 0; if (pinIdx >= 0) tailMask |= 1; if (nestIdx >= 0) tailMask |= 2; @@ -558,13 +566,13 @@ const generation = (coordState?.generation || 1) + reader.s(); if (coordState) { coordState.x = x; coordState.y = y; coordState.age = age; coordState.generation = generation; } const [hunger, energy, sleepPressure] = readBitFields(reader, [8, 8, 8]); - const [bodyMask] = readBitFields(reader, [6]); - const needs = (bodyMask & 1) ? readFixedPacked(reader, 6, 6, 2) : [0, 0, 0, 0, 0, 0]; - const current = (bodyMask & 2) ? readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8) : seedPersonalityArray(birthSeed, flags); - const parents = (bodyMask & 4) ? readIndexList(reader) : []; - const relationships = (bodyMask & 8) ? readRelationships(reader) : []; - const timers = (bodyMask & 16) ? readTimers(reader) : [0, 0, 0, 0, 0, 0, 0, 0, 100]; - const modes = (bodyMask & 32) ? readModes(reader) : [0, 0, 0]; + const [bodyMask] = readBitFields(reader, [5]); + const needs = defaultNeedsFromVitals(hunger, energy, sleepPressure); + const current = (bodyMask & 1) ? readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8) : seedPersonalityArray(birthSeed, flags); + const parents = (bodyMask & 2) ? readIndexList(reader) : []; + const relationships = (bodyMask & 4) ? readRelationships(reader) : []; + const timers = (bodyMask & 8) ? readTimers(reader) : [0, 0, 0, 0, 0, 0, 0, 0, 100]; + const modes = (bodyMask & 16) ? readModes(reader) : [0, 0, 0]; const [tailMask] = readBitFields(reader, [5]); const pinIdx = (tailMask & 1) ? reader.u() : -1; const nestIdx = (tailMask & 2) ? reader.u() : -1; @@ -576,31 +584,41 @@ function writeItemExtra(writer, typeId, extra = []) { if (STRUCTURE_TYPE_IDS.has(typeId)) { - writer.s(extra[0] || 0); - writer.s(extra[1] || 0); + const defaultHp = defaultStructureHp10(typeId); + const hp = toSafeInt(extra[0] || 0); + const maxHp = toSafeInt(extra[1] || 0); let mask = 0; - if ((extra[2] ?? -1) >= 0) mask |= 1; - if ((extra[3] ?? -1) >= 0) mask |= 2; - if (extra[4]) mask |= 4; - if ((extra[5] ?? -1) >= 0) mask |= 8; - if (valuesDiffer(extra[6], 0)) mask |= 16; - writeBitFields(writer, [mask], [5]); - if (mask & 1) writer.u(extra[2]); - if (mask & 2) writer.u(extra[3]); - if (mask & 8) writer.u(extra[5]); - if (mask & 16) writer.s(extra[6] || 0); + if (valuesDiffer(hp, defaultHp)) mask |= 1; + if (valuesDiffer(maxHp, defaultHp)) mask |= 2; + if ((extra[2] ?? -1) >= 0) mask |= 4; + if ((extra[3] ?? -1) >= 0) mask |= 8; + if (extra[4]) mask |= 16; + if ((extra[5] ?? -1) >= 0) mask |= 32; + if (valuesDiffer(extra[6], 0)) mask |= 64; + writeBitFields(writer, [mask], [7]); + if (mask & 1) writer.s(hp); + if (mask & 2) writer.s(maxHp); + if (mask & 4) writer.u(extra[2]); + if (mask & 8) writer.u(extra[3]); + if (mask & 32) writer.u(extra[5]); + if (mask & 64) writer.s(extra[6] || 0); } else if (typeId === 0) { // grass - writer.s(extra[0] || 0); writer.s(extra[1] || 0); + writer.s(extra[0] || 0); let mask = 0; - if (valuesDiffer(extra[2], 0)) mask |= 1; - if (extra[3]) mask |= 2; - writeBitFields(writer, [mask], [2]); - if (mask & 1) writer.s(extra[2] || 0); + if (valuesDiffer(extra[1], 100)) mask |= 1; + if (valuesDiffer(extra[2], 0)) mask |= 2; + if (extra[3]) mask |= 4; + writeBitFields(writer, [mask], [3]); + if (mask & 1) writer.s(extra[1] || 0); + if (mask & 2) writer.s(extra[2] || 0); } else if (typeId === 1) { // zunchi writeBitFields(writer, [extra[0] || 0], [2]); - writer.s(extra[1] || 0); - if (valuesDiffer(extra[2], 0)) { writer.u(1); writer.s(extra[2] || 0); } - else writer.u(0); + let mask = 0; + if (valuesDiffer(extra[1], 100)) mask |= 1; + if (valuesDiffer(extra[2], 0)) mask |= 2; + writeBitFields(writer, [mask], [2]); + if (mask & 1) writer.s(extra[1] || 0); + if (mask & 2) writer.s(extra[2] || 0); } else if (typeId === 22) { // signboard writeSaveString(writer, extra[0] || ""); } else if (typeId === 6) { // duplicator @@ -643,29 +661,29 @@ } function readItemExtra(reader, typeId) { if (STRUCTURE_TYPE_IDS.has(typeId)) { - const hp = reader.s(); - const maxHp = reader.s(); - const [mask] = readBitFields(reader, [5]); + const defaultHp = defaultStructureHp10(typeId); + const [mask] = readBitFields(reader, [7]); + const hp = (mask & 1) ? reader.s() : defaultHp; + const maxHp = (mask & 2) ? reader.s() : defaultHp; return [ hp, maxHp, - (mask & 1) ? reader.u() : -1, - (mask & 2) ? reader.u() : -1, - (mask & 4) ? 1 : 0, + (mask & 4) ? reader.u() : -1, (mask & 8) ? reader.u() : -1, - (mask & 16) ? reader.s() : 0, + (mask & 16) ? 1 : 0, + (mask & 32) ? reader.u() : -1, + (mask & 64) ? reader.s() : 0, ]; } if (typeId === 0) { const growth = reader.s(); - const health = reader.s(); - const [mask] = readBitFields(reader, [2]); - return [growth, health, (mask & 1) ? reader.s() : 0, (mask & 2) ? 1 : 0]; + const [mask] = readBitFields(reader, [3]); + return [growth, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0, (mask & 4) ? 1 : 0]; } if (typeId === 1) { const [stage] = readBitFields(reader, [2]); - const freshness = reader.s(); - return [stage, freshness, reader.u() ? reader.s() : 0]; + const [mask] = readBitFields(reader, [2]); + return [stage, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0]; } if (typeId === 22) return [readSaveString(reader)]; if (typeId === 6) return [reader.s()]; diff --git a/js/save_schema.js b/js/save_schema.js index 43d854a..cf9d7bf 100644 --- a/js/save_schema.js +++ b/js/save_schema.js @@ -4,8 +4,8 @@ // Owns stable save identifiers shared by snapshot_system.js and save_codec.js. // Current-format save identifiers only. (function (global) { - const SNAPSHOT_VERSION = 17; - const BINARY_SCHEMA_VERSION = 22; + const SNAPSHOT_VERSION = 18; + const BINARY_SCHEMA_VERSION = 23; const FIELD_IDS = Object.freeze(["garden", "cage", "park"]); const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]); diff --git a/js/snapshot_system.js b/js/snapshot_system.js index eb60a0d..b95f93a 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -369,7 +369,7 @@ item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback) : u(extra[0], 1000, fallback); if (type === "gate_fence") item.gateOpen = !!extra[1]; } else if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) { - item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; + item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } } function createSnapshot(worldRef = global.world) { @@ -482,6 +482,9 @@ t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef); delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships; delete t.__savedStateInfo; } + if (typeof global.updateNeeds === "function") { + for (const t of worldRef.tarinai) global.updateNeeds(t, worldRef, 0); + } worldRef.liveIdNext = Math.max(1, worldRef.tarinai.length + 1); worldRef.liveIdSerial = Math.max(1, worldRef.tarinai.length + 1); worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, ...worldRef.tarinai.map(t => Number(t.birthSerial) || 0)); diff --git a/js/tarinai_action_state.js b/js/tarinai_action_state.js index eaf526c..fcb37ad 100644 --- a/js/tarinai_action_state.js +++ b/js/tarinai_action_state.js @@ -115,6 +115,14 @@ target.singleUsePending = true; target.singleUseConsumedById = this.id || target.singleUseConsumedById || ""; } + if (target && target.type !== "nest_box") { + this._sleepExternalMotionUntil = 0; + this._sleepExternalWakeSuppressUntil = Math.max(Number(this._sleepExternalWakeSuppressUntil || 0) || 0, now + 2.4); + this.vx = 0; + this.vy = 0; + this.impulseVx = 0; + this.impulseVy = 0; + } const text = reason || (target?.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b"); setBehaviorText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "\u7720\u3063\u3066\u3044\u308b", reasonText: text, target, phase: "perform", source: "behavior" }); return this.setActionState("sleep", { target, reason: text, sleeping: true }); @@ -138,9 +146,9 @@ const sleptFor = Number.isFinite(started) ? Math.max(0, now - started) : 0; const wasActuallySleeping = Boolean((this.state === "sleep" || this.sleeping || session) && session?.consumesGrassBedOnWake); // Reserve-on-use must not consume the simple bed. It is consumed only by - // a real wake after sleep has started; pre-sleep wake flags just release - // the reservation and leave the bed intact. - if (!wasActuallySleeping || sleptFor < 0.75) { + // a real wake after sleep has had time to settle; immediate collision or + // movement wakeups just release the reservation and leave the bed intact. + if (!wasActuallySleeping || sleptFor < 2) { if (bed.singleUseConsumedById === this.id) bed.singleUseConsumedById = ""; if (bed.singleUsePending && (!bed.singleUseConsumedById || bed.singleUseConsumedById === this.id)) bed.singleUsePending = false; this.pendingGrassBedWakeId = ""; diff --git a/js/tarinai_disease_nest.js b/js/tarinai_disease_nest.js index b3d3b56..2c1277a 100644 --- a/js/tarinai_disease_nest.js +++ b/js/tarinai_disease_nest.js @@ -518,8 +518,13 @@ if (typeof this.enterPanic === "function") this.enterPanic({ threat: source, target: null, reason: text, fear: 2.2, wake: true, cause: "burning", surpriseTimer: 0.65, awakeLockTimer: 0.8 }); else this.setActionState?.("panic", { target: null, reason: text, wake: true }); this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(26, this.radius * 1.35), life: 0.26, color: "rgba(255,110,36,0.58)" })); + const now = Number(this.world?.time || 0); + if (force || !already || now >= Number(this.nextBurnVoiceAt || -999)) { + this.nextBurnVoiceAt = now + 2.6; + this.bubble?.("\u3042\u3061\u3085\u3044\uff01", 1.35, "rgba(214,82,38,0.90)", { force: true }); + } this.world?.emit?.("fire:ignite", { tarinai: this, source }); - this.recordChangeCause?.("\u708e\u4e0a", "\u708e"); + this.recordChangeCause?.("\u708e\u4e0a", "\u708e\u4e0a"); return true; }, @@ -548,7 +553,7 @@ this.sleeping = false; this.eatTimer = 0; if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 6.2, safety: dt * 3.0 }); - if (typeof this.damage === "function") this.damage(dt * 3.8, "\u708e"); + if (typeof this.damage === "function") this.damage(dt * 3.8, "\u708e\u4e0a"); else this.energy = clamp((this.energy || 0) - dt * 0.12, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100)); this.hurtTimer = Math.max(this.hurtTimer || 0, 0.22); const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.radius || 18, 16)); diff --git a/js/tarinai_food_prototype_mixin.js b/js/tarinai_food_prototype_mixin.js index f48d08c..dfa95b3 100644 --- a/js/tarinai_food_prototype_mixin.js +++ b/js/tarinai_food_prototype_mixin.js @@ -30,6 +30,7 @@ } it.foodServingsRemaining = clamp(it.foodServingsRemaining, 0, it.foodServingsMax); it.amount = it.foodServingsRemaining; + (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(it) : false); }, consumeFoodServing(it, nutrition) { if (it?.type === "duplicator") { @@ -44,6 +45,12 @@ const value = Number.isFinite(nutrition) ? nutrition : (window.TarinaiItemRegistry.food?.nutrition?.(it.type, 0) ?? 0); it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1); it.amount = it.foodServingsRemaining; + const resized = (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(it) : false); + if (resized) { + this.world?.markItemBucketsDirty?.("food-serving-resize"); + this.world?.markSpatialDirty?.("food-serving-resize"); + this.world && (this.world.drawListDirty = true); + } this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition: value }); return value; }, diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index 4cd8a73..0c0f944 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -554,12 +554,17 @@ function shouldWakeFromSleep(tarinai, world, needs) { const session = tarinai.sleepSession || { startedAt: now, minDuration: 8, targetEnergy: 74, maxDuration: 28 }; const started = Number(session.startedAt); const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now)); + const minSleepDuration = Math.max(2, Number(session.minDuration) || 8); + + // 明確な妨害だけは最低睡眠時間より優先して起床させる。 if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true; if (findNearbyDanger(world, tarinai, 160)) return true; + + // 空腹・睡眠欲の回復・体力回復などの通常要因では、最低2時間は寝続ける。 + if (elapsed < minSleepDuration) return false; + const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0; if ((tarinai.hunger || 0) >= 84 || foodNeed >= needThreshold("food", "start")) return true; - // \u81ea\u7136\u306a\u7761\u7720\u306f\u77ed\u3044\u5b89\u5168\u30ed\u30c3\u30af\u5f8c\u3001\u5341\u5206\u306b\u56de\u5fa9\u3057\u305f\u3089\u8d77\u304d\u308b\u3002 - if (elapsed < (Number(session.minDuration) || 8)) return false; const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0; if (sleepNeed <= ((typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34) + 4)) return true; if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 74)) return true; @@ -567,6 +572,7 @@ function shouldWakeFromSleep(tarinai, world, needs) { return false; } + function protectSleepSession(tarinai, world, needs) { if (!(tarinai?.state === "sleep" || tarinai?.sleeping)) return false; tarinai.sleeping = true; diff --git a/js/tarinai_nest_sleep_system.js b/js/tarinai_nest_sleep_system.js index 0bf9e82..c8b5623 100644 --- a/js/tarinai_nest_sleep_system.js +++ b/js/tarinai_nest_sleep_system.js @@ -61,7 +61,7 @@ const near = typeof worldRef.nearbyTarinai === "function" ? worldRef.nearbyTarinai(bed.x, bed.y, radius) : (worldRef.tarinai || []); for (const t of near || []) { if (!t || t.dead || isTarinaiHiddenInNestBox(worldRef, t)) continue; - if (t.state === "sleep" || t.state === "seek_bed" || t.target === bed) n += 1; + if (t.target === bed && (t.state === "sleep" || t.state === "seek_bed" || t.sleeping)) n += 1; } return n; } @@ -81,7 +81,7 @@ if (sleepTargetBlocked(worldRef, tarinai, item)) return false; const capacity = item.type === "nest_box" ? nestBoxCapacity(worldRef, item) : 1; const occupancy = bedOccupancy(worldRef, item); - return occupancy < capacity || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id); + return occupancy < capacity || tarinai.target === item || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id); } function findStructure(worldRef, tarinai, opts = {}) { @@ -135,12 +135,12 @@ return findOwnedStructure(worldRef, tarinai, "grass_bed", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "nest_box", maxDist) || nearestSleepPlaceOfType(worldRef, tarinai, "bed", maxDist) + || nearestSleepPlaceOfType(worldRef, tarinai, "grass_bed", maxDist) || null; } function findNearestSleepPlace(worldRef, tarinai, maxDist = 520) { return findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist) - || findUnownedStructure(worldRef, tarinai, "grass_bed", maxDist) || null; } diff --git a/js/tarinai_render.js b/js/tarinai_render.js index 6575b5b..86eeb98 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -581,11 +581,13 @@ const hpActive = (this.hpBarTimer || 0) > 0; const stressActive = (this.stressBarTimer || 0) > 0; - if ((hpActive || stressActive) && (showDecor || this.world.selected === this || this.hurtTimer > 0.04)) { + if (hpActive || stressActive) { const bw = Math.max(28, this.radius * 2.35); const bh = 5; const x = this.x - bw / 2; const baseY = this.y - drawH * 0.55 - 16; + const stressY = hpActive ? baseY + 7 : baseY; + const hpY = stressActive ? baseY - 7 : baseY; const drawMeter = (y, pct, fill, alpha) => { ctx.save(); ctx.globalAlpha = alpha; @@ -607,11 +609,11 @@ }; if (stressActive) { const pct = clamp((this.stress || 0) / 130, 0, 1); - drawMeter(baseY, pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1)); + drawMeter(stressY, pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1)); } if (hpActive) { const pct = typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(this) : clamp(this.energy / Math.max(1, this.maxEnergy || 100), 0, 1); - drawMeter(baseY - (stressActive ? 11 : 0), pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1)); + drawMeter(hpY, pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1)); } } }, diff --git a/js/text_catalog.js b/js/text_catalog.js index c0089b0..621a2ee 100644 --- a/js/text_catalog.js +++ b/js/text_catalog.js @@ -144,7 +144,7 @@ TEXT_CATALOG.targetLabel = function targetLabelFromCatalog(target) { TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) { if (!t) return "\u306a\u3057"; - if ((t.pinchThoughtTimer || 0) > 0) return "\u3064\u307e\u307e\u308c\u3066\u3044\u308b"; + if ((t.pinchThoughtTimer || 0) > 0 && (t.playerHeld || t._heldByPlayer)) return "\u3064\u307e\u307e\u308c\u3066\u3044\u308b"; if (t.lastPanicCause === "tarinai_champion" && (t.state === "panic" || (t.fearTimer || 0) > 0.04)) return "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002"; const behaviorState = currentTarinaiBehavior(t); const behaviorLabel = behaviorText(t); diff --git a/js/ui_family_paths.js b/js/ui_family_paths.js index 5803446..83025fe 100644 --- a/js/ui_family_paths.js +++ b/js/ui_family_paths.js @@ -102,6 +102,24 @@ function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffse if (a.rowIndex !== b.rowIndex) return lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context); return lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA, attachOffsetB); } +function lineageChildAnchorOnMarriageLine(partner, fallbackX, fallbackY) { + const horizontals = (partner?.segments || []) + .filter(seg => seg.kind === "h" && Math.abs(seg.x2 - seg.x1) > 10) + .sort((a, b) => { + const ty = Number.isFinite(partner?.unionY) ? partner.unionY : fallbackY; + const dy = Math.abs(a.y1 - ty) - Math.abs(b.y1 - ty); + if (Math.abs(dy) > 0.01) return dy; + return Math.abs(b.x2 - b.x1) - Math.abs(a.x2 - a.x1); + }); + const seg = horizontals[0] || null; + if (!seg) return { x: fallbackX, y: fallbackY }; + const minX = Math.min(seg.x1, seg.x2) + 8; + const maxX = Math.max(seg.x1, seg.x2) - 8; + const midX = (seg.x1 + seg.x2) / 2; + const x = minX <= maxX ? clamp(Number.isFinite(fallbackX) ? fallbackX : midX, minX, maxX) : midX; + return { x, y: seg.y1 }; +} + function lineageVerticalBridgePath(x, y1, y2, bridges = [], radius = 8.5) { if (!Number.isFinite(x) || !Number.isFinite(y1) || !Number.isFinite(y2)) return ''; @@ -170,6 +188,44 @@ function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) { return { d }; } +function lineageChildBusSpan(group, nodeW) { + const childCenters = (group.children || []).map(c => c.x + nodeW / 2); + if (!childCenters.length) return null; + return { + left: Math.min(group.joinX, ...childCenters), + right: Math.max(group.joinX, ...childCenters), + }; +} + +function lineageSpansOverlap(a, b, pad = 12) { + if (!a || !b) return false; + return Math.max(a.left, b.left) <= Math.min(a.right, b.right) + pad; +} + +function lineageSeparateChildBusLanes(groupsInGap, nodeW) { + const lanes = []; + const ordered = (groupsInGap || []) + .filter(group => group && Number.isFinite(group.busY)) + .map(group => ({ group, span: lineageChildBusSpan(group, nodeW), baseY: group.busY })) + .filter(entry => entry.span) + .sort((a, b) => a.baseY - b.baseY || a.span.left - b.span.left || a.span.right - b.span.right); + + for (const entry of ordered) { + let laneIndex = 0; + while (lanes[laneIndex]?.some(prev => Math.abs(prev.group.busY - entry.baseY) < 12 && lineageSpansOverlap(prev.span, entry.span))) { + laneIndex += 1; + } + if (!lanes[laneIndex]) lanes[laneIndex] = []; + const fromY = Number(entry.group.fromY); + const childTop = Number(entry.group.childTop); + const minY = Number.isFinite(fromY) ? fromY + 26 : -Infinity; + const maxY = Number.isFinite(childTop) ? childTop - 18 : Infinity; + const offset = laneIndex * 9; + entry.group.busY = clamp(entry.baseY - offset, minY, maxY); + lanes[laneIndex].push(entry); + } +} + function lineageComponentSignature(component, family) { family = lineageEdgeFamily(family); const idSet = new Set(component.map(x => x?.id).filter(Boolean)); @@ -281,7 +337,7 @@ function lineageAssignPartnerAttachOffsets(linkGroups) { for (const [key, groups] of buckets.entries()) { groups.sort((a, b) => a.joinX - b.joinX || a.childTop - b.childTop); const n = groups.length; - const step = 9; + const step = 14; groups.forEach((group, i) => { const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step; const [nodeId] = key.split(':'); @@ -328,8 +384,8 @@ function lineageBuildLinks(nodes, idSet, layout) { for (const groupsInGap of byGap.values()) { groupsInGap.sort((a, b) => a.joinX - b.joinX || a.children.length - b.children.length); - const top = Math.min(...groupsInGap.map(g => g.parentBottom)) + 44; - const bottom = Math.max(...groupsInGap.map(g => g.childTop)) - 42; + const top = Math.min(...groupsInGap.map(g => g.parentBottom)) + 58; + const bottom = Math.max(...groupsInGap.map(g => g.childTop)) - 54; const span = Math.max(16, bottom - top); groupsInGap.forEach((group, i) => { const laneY = top + span * ((i + 1) / (groupsInGap.length + 1)); @@ -343,22 +399,21 @@ function lineageBuildLinks(nodes, idSet, layout) { const attachB = 0; const partnerY = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2; const partner = lineagePartnerPathFromPositions(left, right, nodeW, nodeH, partnerY, attachA, attachB, layout); - if (Number.isFinite(partner.unionX)) group.joinX = partner.unionX; const unionY = Number.isFinite(partner.unionY) ? partner.unionY : partnerY; - const clearY = clamp(group.parentBottom + 22 + i * 2, unionY + 10, group.childTop - 36); - group.fromY = clearY; - group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, clearY + 28) : Math.min(group.childTop - 24, Math.max(clearY + 24, laneY + 18 + i * 3)); + const anchor = lineageChildAnchorOnMarriageLine(partner, Number.isFinite(partner.unionX) ? partner.unionX : group.joinX, unionY); + group.joinX = anchor.x; + group.fromY = anchor.y; + group.busY = group.children.length === 1 ? Math.min(group.childTop - 22, anchor.y + 38) : Math.min(group.childTop - 28, Math.max(anchor.y + 34, laneY + 24 + i * 5)); if (partner.d) { partnerLinks.push(partner.d); horizontalSegments.push(...partner.segments.filter(seg => seg.kind === 'h')); } - const trunk = lineageVerticalBridgePath(group.joinX, unionY, group.fromY, []); - if (trunk) childLinks.push(trunk); } else { group.fromY = group.parentBottom; - group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, group.fromY + 30) : Math.min(group.childTop - 24, Math.max(group.fromY + 28, laneY + 18 + i * 3)); + group.busY = group.children.length === 1 ? Math.min(group.childTop - 22, group.fromY + 38) : Math.min(group.childTop - 28, Math.max(group.fromY + 34, laneY + 24 + i * 5)); } }); + lineageSeparateChildBusLanes(groupsInGap, nodeW); } for (const groupsInGap of byGap.values()) { diff --git a/js/ui_family_render.js b/js/ui_family_render.js index ce0f275..ee7024f 100644 --- a/js/ui_family_render.js +++ b/js/ui_family_render.js @@ -91,19 +91,25 @@ function lineageBuildLinksFast(nodes, idSet, layout) { childLinkEdges += parents.length; const childX = childPos.x + nodeW / 2; const childY = childPos.y; - const fromY = Math.max(...parents.map(p => p.y + nodeH)); - const joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length; + let fromY = Math.max(...parents.map(p => p.y + nodeH)); + let joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length; if (parents.length >= 2) { const left = parents[0]; const right = parents[parents.length - 1]; const partnerKey = parents.map(p => p.node?.id).filter(Boolean).sort().join("+"); - if (partnerKey && !seenPartners.has(partnerKey)) { - seenPartners.add(partnerKey); - const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2; - partnerLinks.push(`M ${(left.x + nodeW).toFixed(1)} ${y.toFixed(1)} L ${right.x.toFixed(1)} ${y.toFixed(1)}`); + const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2; + const x1 = left.x + nodeW; + const x2 = right.x; + if (x2 > x1) { + joinX = clamp(joinX, x1 + 8, x2 - 8); + fromY = y; + if (partnerKey && !seenPartners.has(partnerKey)) { + seenPartners.add(partnerKey); + partnerLinks.push(`M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`); + } } } - const laneY = Math.min(childY - 18, fromY + Math.max(28, (childY - fromY) * 0.48)); + const laneY = Math.min(childY - 22, fromY + Math.max(36, (childY - fromY) * 0.54)); childLinks.push(`M ${joinX.toFixed(1)} ${fromY.toFixed(1)} L ${joinX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${childY.toFixed(1)}`); } return { partnerLinks, childLinks, childLinkGroups, childLinkEdges }; diff --git a/js/ui_input_shared.js b/js/ui_input_shared.js index c3baf10..d6e458b 100644 --- a/js/ui_input_shared.js +++ b/js/ui_input_shared.js @@ -302,6 +302,13 @@ if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release"); target.playerHeld = false; target._heldByPlayer = false; + if (uiCache.grabKind === "tarinai") { + target.pinchThoughtTimer = 0; + if (target.thought === "つままれている") target.thought = ""; + const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(target) : null; + const reason = String(behavior?.reason || behavior?.text || behavior?.label || ""); + if (reason === "つままれている" && typeof clearBehavior === "function") clearBehavior(target); + } if (target.type === "duplicator") { target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.2); } diff --git a/js/version.js b/js/version.js index d3cc074..f9db371 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.15.101"; - const APP_BUILD = "tarinai-fire-effects-v39-15-101"; + const APP_VERSION = "39.15.107"; + const APP_BUILD = "tarinai-family-line-lanes-v39-15-107"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; @@ -21,4 +21,3 @@ window.TARINAI_VERSION = APP_VERSION; window.TARINAI_APP = config; })(); - diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 89155c5..387829b 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -295,6 +295,15 @@ it.grassStage = -1; } cleaned += p * 0.72; + } else if (it.type === "fire") { + it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 540 * (0.35 + p)); + it.flameLife = Math.max(0, (Number(it.flameLife) || 0) - dt * 5.2 * (0.35 + p)); + it.fireShrinkPulse = Math.max(Number(it.fireShrinkPulse || 0), 0.18); + if (it.amount <= 4 || it.flameLife <= 0.05) { + it.amount = 0; + it.flameLife = 0; + } + cleaned += p * 1.15; } else if (it.type === "grass_bed") { it.amount = 0; it.hp = 0; @@ -320,6 +329,20 @@ t.vy += ny * (95 + 165 * p) + deterministicRange(this, "water-hose-jitter-y", -10, 10, t, x, y); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24); t.applyWaterEffect?.(dt * (1.0 + p * 1.8), "hose"); + if (t.burning || (t.burnTimer || 0) > 0) { + t.extinguishFire?.("\u6d17\u6d44\u3067\u6d88\u706b\u3055\u308c\u305f"); + cleaned += p * 1.10; + } + } + for (const ant of this.nearbyAnts?.(x, y, radius + 45) || []) { + if (!ant || ant.dead) continue; + const d = distXY(ant.x, ant.y, x, y); + if (d > radius + (ant.r || 4)) continue; + const p = clamp(1 - d / (radius + (ant.r || 4)), 0, 1); + if (ant.burning || (ant.burnTimer || 0) > 0) { + ant.extinguishFire?.("water-hose"); + cleaned += p * 0.75; + } } if (deterministicChance(this, "water-hose-ring", 0.72, x, y)) { this.effects.push(new Effect("ring", x + deterministicRange(this, "water-hose-ring-x", -18, 18, x, y), y + deterministicRange(this, "water-hose-ring-y", -12, 12, x, y), { size: deterministicRange(this, "water-hose-ring-size", 8, 22, x, y), life: deterministicRange(this, "water-hose-ring-life", 0.18, 0.32, x, y), color: "rgba(128,204,238,0.60)" })); diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 800e8e1..4042b06 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -660,10 +660,10 @@ } if (isServingFoodType(item.type)) { item.toolSize = data.toolSize || item.toolSize || "medium"; - item.foodServingScale = Number(data.foodServingScale || item.foodServingScale || 1) || 1; item.foodServingsMax = Math.max(1, Number(data.foodServingsMax || item.foodServingsMax || item.amount || 1)); - item.foodServingsRemaining = item.foodServingsMax; + item.foodServingsRemaining = clamp(Number(data.foodServingsRemaining ?? item.foodServingsMax) || item.foodServingsMax, 0, item.foodServingsMax); item.amount = item.foodServingsRemaining; + (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } if (Number.isFinite(Number(item.x))) item.prevX = item.x; if (Number.isFinite(Number(item.y))) item.prevY = item.y; diff --git a/js/world_view.js b/js/world_view.js index c9678db..d97407a 100644 --- a/js/world_view.js +++ b/js/world_view.js @@ -406,10 +406,10 @@ item.r *= scale; if (isServingFoodType(item.type)) { const servings = foodServingsForSize(size); - item.foodServingScale = scale; item.foodServingsMax = servings; item.foodServingsRemaining = servings; item.amount = servings; + (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } else if (["grass", "zunchi"].includes(item.type)) { item.amount *= scale * scale; item.foodServingScale = scale; diff --git a/service-worker.js b/service-worker.js index 63b3a53..7c27b35 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "39.15.101"; +const APP_VERSION = "39.15.107"; 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.