This commit is contained in:
33333-33333 2026-06-30 10:57:06 +09:00
commit f941200504
70 changed files with 1856 additions and 429 deletions

View file

@ -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.75",
"version": "39.15.77",
"entrypoints": {
"app": "index.html",
"runtime": "js/main.js",

View file

@ -1,5 +1,5 @@
{
"version": "39.15.75",
"version": "39.15.84",
"css": [
"css/base.css",
"css/layout.css",

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -214,9 +214,30 @@ h1 {
color: var(--ink);
}
.field-choice:hover { border-color: rgba(112,178,66,0.45); }
.field-choice.active,
.field-choice[aria-pressed="true"] {
border-color: rgba(112,178,66,0.72);
box-shadow: 0 0 0 2px rgba(112,178,66,0.18), inset 0 0 0 1px rgba(255,255,255,0.64);
background: linear-gradient(180deg, rgba(248,255,240,0.96), rgba(231,246,210,0.82));
}
.field-choice strong, .field-choice span { display: block; }
.field-choice strong { font-size: 15px; }
.field-choice span { margin-top: 4px; color: var(--muted); font-size: 12px; }
.field-dialog-lead {
margin: -4px 0 12px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.field-dialog-section-title {
margin: 12px 0 7px;
font-size: 12px;
font-weight: 700;
color: rgba(75,62,43,0.78);
}
.reset-dialog-panel { width: min(760px, 100%); }
.reset-preset-choice-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.reset-preset-choice-grid .field-choice { min-height: 66px; padding: 7px 5px; }
.field-dialog-actions {
margin-top: 12px;
display: flex;

View file

@ -743,3 +743,37 @@
.collapsible-card.collapsed { min-height: 0 !important; }
.log-card.collapsed { min-height: 0 !important; height: auto !important; flex: 0 0 auto !important; flex-basis: auto !important; }
.log-card.collapsed .panel-card-body { display: none !important; }
.temperature-control {
margin: 10px 0 2px;
padding: 10px 11px 11px;
border: 1px solid rgba(118, 92, 61, 0.16);
border-radius: 14px;
background: rgba(255, 251, 244, 0.56);
}
.temperature-control-head {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: rgba(68, 55, 41, 0.84);
}
.temperature-control-head strong {
margin-left: auto;
font-size: 15px;
}
.temperature-auto {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
user-select: none;
}
.temperature-slider {
width: 100%;
margin-top: 8px;
}
.temperature-slider:disabled {
opacity: 0.52;
}

View file

@ -325,6 +325,7 @@ meter {
#gameCanvas { height: var(--field-mobile-height, 66vh); min-height: var(--field-mobile-min-height, 460px); }
.stage-wrap { min-height: var(--field-mobile-min-height, 460px); }
.field-choice-grid { grid-template-columns: 1fr; }
.reset-preset-choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

View file

@ -187,14 +187,14 @@ body::before {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(13px, auto);
align-items: stretch;
overflow: visible !important;
text-overflow: clip !important;
gap: 2px 3px;
gap: 2px 4px;
margin-top: 2px;
max-width: 84px;
width: 84px;
max-height: 34px;
overflow: hidden !important;
text-overflow: clip !important;
white-space: normal !important;
max-height: none;
max-width: 100%;
width: 100%;
}
.family-personality-tags em {
display: flex;
@ -210,32 +210,245 @@ body::before {
font-size: 8.2px;
font-style: normal;
line-height: 1.18;
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
word-break: keep-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool[data-tip] { position: relative; }
.lineage-family-tree .tree-node .family-copy {
max-width: calc(100% - 8px);
.lineage-family-tree .lineage-tree-panel {
position: relative;
overflow: visible;
min-width: 360px;
margin-top: 6px;
border-radius: 14px;
background:
linear-gradient(90deg, rgba(84,68,48,0.045) 1px, transparent 1px),
linear-gradient(180deg, rgba(84,68,48,0.040) 1px, transparent 1px),
rgba(255,250,244,0.48);
background-size: 34px 34px, 34px 34px, auto;
border: 1px solid rgba(84,68,48,0.10);
}
.lineage-family-tree .lineage-links {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
z-index: 1;
isolation: isolate;
}
.lineage-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.btn.active {
background: #efffde;
border-color: rgba(103,178,44,0.46);
font-weight: 700;
}
.lineage-render-status {
position: sticky;
top: 0;
left: 0;
z-index: 6;
width: max-content;
margin: 0 0 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: var(--muted);
background: rgba(255,255,255,0.88);
border: 1px solid rgba(84,68,48,0.10);
box-shadow: 0 4px 10px rgba(64,48,32,0.06);
}
.lineage-stale-status {
position: sticky;
top: 0;
left: 0;
z-index: 6;
width: max-content;
max-width: calc(100% - 12px);
margin: 0 0 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: rgba(92, 75, 52, 0.82);
background: rgba(255, 249, 232, 0.92);
border: 1px solid rgba(150, 115, 46, 0.16);
box-shadow: 0 4px 10px rgba(64,48,32,0.05);
}
.lineage-family-tree .lineage-links path {
fill: none;
stroke: rgba(107, 88, 61, 0.58);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.lineage-family-tree .lineage-links .partner-link {
stroke-width: 2.4;
stroke: rgba(98, 80, 56, 0.68);
stroke-dasharray: 5 4;
}
.lineage-family-tree .lineage-links .child-link {
stroke-width: 2;
stroke: rgba(107, 88, 61, 0.58);
}
.lineage-family-tree .tree-node {
position: absolute;
width: 150px;
height: 104px;
min-height: 104px;
box-sizing: border-box;
align-items: flex-start;
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
box-shadow: 0 2px 5px rgba(64,48,32,0.035);
contain: layout paint style;
z-index: 2;
isolation: isolate;
}
.lineage-family-tree .tree-node.child {
border-color: rgba(103,178,44,0.34);
}
.lineage-family-tree .tree-node.dead {
opacity: 1;
filter: none;
}
.lineage-family-tree .tree-node[data-family-tarinai-id]:hover {
transform: none;
}
.lineage-family-tree .tree-node .family-portrait {
right: 4px;
top: 4px;
bottom: auto;
width: 48px;
height: 48px;
}
.lineage-family-tree .tree-generation-label {
position: absolute;
left: 10px;
width: 54px;
padding: 3px 0;
border-radius: 999px;
font-size: 11px;
color: var(--muted);
text-align: center;
background: rgba(255,255,255,0.70);
border: 1px solid rgba(84,68,48,0.08);
z-index: 2;
}
.lineage-family-tree .lineage-empty-family {
position: absolute;
left: 72px;
bottom: 8px;
padding: 5px 8px;
border-radius: 9px;
background: rgba(255,255,255,0.70);
}
.lineage-family-tree .tree-node .family-copy {
max-width: calc(100% - 46px);
width: auto;
}
.lineage-family-tree .tree-node .family-copy strong,
.lineage-family-tree .tree-node .family-copy > span:not(.family-genetic-stats):not(.family-fight-stat):not(.family-personality-tags) {
max-width: 100%;
}
.lineage-family-tree .tree-node .family-personality-tags {
max-width: 100%;
width: 100%;
max-width: 84px;
width: 84px;
}
.lineage-family-tree .tree-node .family-personality-tags em {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 720px) {
.lineage-main-content {
height: 70vh;
min-height: 420px;
max-height: none;
}
}
/* Restored from the original ZIP: fixed colony graph/legend sizing. */
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
.colony-stats {
grid-template-columns: repeat(2, 1fr);
margin-bottom: 8px;
}
.colony-chart {
display: block;
width: 100%;
height: 188px;
min-height: 188px;
border: 1px solid rgba(84, 68, 48, 0.14);
border-radius: 14px;
background: linear-gradient(180deg, rgba(255,255,255,0.70), rgba(246,240,228,0.54));
overflow: hidden;
}
.colony-card h2 { line-height: 1.45; margin-bottom: 10px; }
.colony-chart svg { display: block; width: 100%; height: 100%; }
.colony-chart-legend {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
align-items: center;
margin: 4px 0 8px;
font-size: 11px;
color: var(--muted);
line-height: 1.25;
}
.colony-chart-legend span {
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.colony-chart-legend i {
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
box-shadow: 0 0 0 1px rgba(70, 52, 36, 0.10);
}
/* v9: keep 2x2 personality tags inside fixed family-tree nodes. */
.lineage-family-tree .tree-node .family-personality-tags,
.family-personality-tags {
box-sizing: border-box !important;
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
grid-auto-rows: 13px !important;
gap: 2px 3px !important;
width: 86px !important;
max-width: 86px !important;
height: 28px !important;
max-height: 28px !important;
overflow: hidden !important;
}
.lineage-family-tree .tree-node .family-personality-tags em,
.family-personality-tags em {
box-sizing: border-box !important;
display: block !important;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
height: 13px !important;
padding: 1px 2px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
line-height: 11px !important;
font-size: 7.2px !important;
letter-spacing: -0.08em !important;
text-align: center !important;
}

View file

@ -7,11 +7,11 @@
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
<link rel="stylesheet" href="css/base.css?v=39.15.76" />
<link rel="stylesheet" href="css/layout.css?v=39.15.76" />
<link rel="stylesheet" href="css/panel.css?v=39.15.76" />
<link rel="stylesheet" href="css/components.css?v=39.15.76" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.76" />
<link rel="stylesheet" href="css/base.css?v=39.15.84" />
<link rel="stylesheet" href="css/layout.css?v=39.15.84" />
<link rel="stylesheet" href="css/panel.css?v=39.15.84" />
<link rel="stylesheet" href="css/components.css?v=39.15.84" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.84" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -102,6 +102,10 @@
<div class="stat"><span>&#x72B6;&#x614B;</span><strong id="statColonyMood">&#x5B89;&#x5B9A;</strong></div>
<div class="stat ground-stat" data-ground-toggle role="button" tabindex="0"><span>&#x5730;&#x9762;</span><button id="groundTypeBtn" class="ground-type-btn" type="button" data-ground-toggle aria-label="&#x5730;&#x9762;&#x3092;&#x304D;&#x308A;&#x304B;&#x3048;&#x308B;"><strong id="statGroundType">&#x7802;</strong></button></div>
</div>
<div class="temperature-control" aria-label="気温調整">
<div class="temperature-control-head"><span>気温</span><strong id="temperatureValue">10℃</strong><label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label></div>
<input id="temperatureSlider" class="temperature-slider" type="range" min="-100" max="120" step="1" value="10">
</div>
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">&#x500B;&#x4F53;&#x6570;</button><button data-chart="life">&#x6B32;&#x6C42;</button><button data-chart="objects">&#x7269;</button></div>
<div id="colonyChartLegend" class="colony-chart-legend"></div>
<div id="colonyChart" class="colony-chart" aria-label="&#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x306E;&#x63A8;&#x79FB;&#x30B0;&#x30E9;&#x30D5;" role="img"></div>
@ -150,15 +154,28 @@
</div>
<div id="fieldDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="fieldDialogTitle">
<div class="field-dialog-panel">
<h2 id="fieldDialogTitle">&#x30D5;&#x30A3;&#x30FC;&#x30EB;&#x30C9;&#x3092;&#x9078;&#x629E;</h2>
<div class="field-choice-grid">
<button class="field-choice" data-field-type="park" type="button"><strong>&#x516C;&#x5712;</strong><span>&#x5927;</span></button>
<button class="field-choice" data-field-type="garden" type="button"><strong>&#x5EAD;</strong><span>&#x4E2D;</span></button>
<button class="field-choice" data-field-type="cage" type="button"><strong>&#x30B1;&#x30FC;&#x30B8;</strong><span>&#x5C0F;</span></button>
<div class="field-dialog-panel reset-dialog-panel">
<h2 id="fieldDialogTitle">&#x30EA;&#x30BB;&#x30C3;&#x30C8;&#x8A2D;&#x5B9A;</h2>
<p class="field-dialog-lead">&#x30B5;&#x30A4;&#x30BA;&#x3068;&#x30D7;&#x30EA;&#x30BB;&#x30C3;&#x30C8;&#x3092;&#x7D44;&#x307F;&#x5408;&#x308F;&#x305B;&#x3066;&#x89B3;&#x5BDF;&#x74B0;&#x5883;&#x3092;&#x751F;&#x6210;&#x3057;&#x307E;&#x3059;&#x3002;</p>
<div class="field-dialog-section-title">&#x30B5;&#x30A4;&#x30BA;</div>
<div class="field-choice-grid reset-size-choice-grid">
<button class="field-choice" data-field-type="park" type="button" aria-pressed="false"><strong>&#x516C;&#x5712;</strong><span>&#x5927;</span></button>
<button class="field-choice" data-field-type="garden" type="button" aria-pressed="true"><strong>&#x5EAD;</strong><span>&#x4E2D;</span></button>
<button class="field-choice" data-field-type="cage" type="button" aria-pressed="false"><strong>&#x30B1;&#x30FC;&#x30B8;</strong><span>&#x5C0F;</span></button>
</div>
<div class="field-dialog-section-title">&#x30D7;&#x30EA;&#x30BB;&#x30C3;&#x30C8;</div>
<div class="field-choice-grid reset-preset-choice-grid">
<button class="field-choice preset-choice" data-reset-preset="empty" type="button" aria-pressed="false"><strong>&#x7A7A;&#x767D;</strong><span>&#x4F55;&#x3082;&#x7121;&#x3044;</span></button>
<button class="field-choice preset-choice" data-reset-preset="default" type="button" aria-pressed="true"><strong>&#x30C7;&#x30D5;&#x30A9;&#x30EB;&#x30C8;</strong><span>&#x521D;&#x671F;&#x751F;&#x6210;</span></button>
<button class="field-choice preset-choice" data-reset-preset="athletic" type="button" aria-pressed="false"><strong>&#x30A2;&#x30B9;&#x30EC;&#x30C1;&#x30C3;&#x30AF;</strong><span>&#x67F5;&#x3068;&#x30DC;&#x30FC;&#x30EB;</span></button>
<button class="field-choice preset-choice" data-reset-preset="crowded" type="button" aria-pressed="false"><strong>&#x904E;&#x5BC6;</strong><span>&#x5927;&#x91CF;&#x3068;&#x305A;&#x3093;&#x3060;</span></button>
<button class="field-choice preset-choice" data-reset-preset="family" type="button" aria-pressed="false"><strong>&#x5BB6;&#x65CF;</strong><span>2&#x5339;&#x3068;&#x3078;&#x3053;&#x9905;</span></button>
<button class="field-choice preset-choice" data-reset-preset="war" type="button" aria-pressed="false"><strong>&#x6226;&#x4E89;</strong><span>&#x3051;&#x3093;&#x304B;&#x3068;&#x67F5;</span></button>
<button class="field-choice preset-choice" data-reset-preset="happy" type="button" aria-pressed="false"><strong>&#x5E78;&#x798F;</strong><span>&#x6383;&#x9664;&#x6A5F;&#x3068;&#x5DE3;&#x7BB1;</span></button>
</div>
<div class="field-dialog-actions">
<button id="fieldCancelBtn" class="btn" type="button">&#x30AD;&#x30E3;&#x30F3;&#x30BB;&#x30EB;</button>
<button id="fieldConfirmBtn" class="btn danger" type="button">&#x518D;&#x751F;&#x6210;</button>
</div>
</div>
</div>
@ -201,152 +218,152 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=39.15.76" defer></script>
<script src="js/event_bus.js?v=39.15.76" defer></script>
<script src="js/registry_base.js?v=39.15.76" defer></script>
<script src="js/disease_registry.js?v=39.15.76" defer></script>
<script src="js/sound_pack.js?v=39.15.76" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.76" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.76" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.76" defer></script>
<script src="js/item_food_definitions.js?v=39.15.76" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.76" defer></script>
<script src="js/item_registry.js?v=39.15.76" defer></script>
<script src="js/data.js?v=39.15.76" defer></script>
<script src="js/ground_types.js?v=39.15.76" defer></script>
<script src="js/input_mode_manager.js?v=39.15.76" defer></script>
<script src="js/math.js?v=39.15.76" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.76" defer></script>
<script src="js/physics_helpers.js?v=39.15.76" defer></script>
<script src="js/placement_preview_system.js?v=39.15.76" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.76" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.76" defer></script>
<script src="js/mechanical_system.js?v=39.15.76" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.76" defer></script>
<script src="js/constraint_system.js?v=39.15.76" defer></script>
<script src="js/physics_world_system.js?v=39.15.76" defer></script>
<script src="js/physics_projection_system.js?v=39.15.76" defer></script>
<script src="js/assets.js?v=39.15.76" defer></script>
<script src="js/audio.js?v=39.15.76" defer></script>
<script src="js/perf_profiler.js?v=39.15.76" defer></script>
<script src="js/render.js?v=39.15.76" defer></script>
<script src="js/sim_core.js?v=39.15.76" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.76" defer></script>
<script src="js/structures.js?v=39.15.76" defer></script>
<script src="js/items.js?v=39.15.76" defer></script>
<script src="js/item_type_initializers.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.76" defer></script>
<script src="js/item_update_policy.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.76" defer></script>
<script src="js/item_runtime.js?v=39.15.76" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.76" defer></script>
<script src="js/item_render_runtime.js?v=39.15.76" defer></script>
<script src="js/ants.js?v=39.15.76" defer></script>
<script src="js/health.js?v=39.15.76" defer></script>
<script src="js/tarinai.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.76" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.76" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.76" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.76" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.76" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.76" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.76" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.76" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.76" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.76" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.76" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.76" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.76" defer></script>
<script src="js/tarinai_render.js?v=39.15.76" defer></script>
<script src="js/world.js?v=39.15.76" defer></script>
<script src="js/world_view.js?v=39.15.76" defer></script>
<script src="js/family_graph.js?v=39.15.76" defer></script>
<script src="js/world_family_social.js?v=39.15.76" defer></script>
<script src="js/impact_core_system.js?v=39.15.76" defer></script>
<script src="js/impact_response_system.js?v=39.15.76" defer></script>
<script src="js/collision_response_system.js?v=39.15.76" defer></script>
<script src="js/world_combat_effects.js?v=39.15.76" defer></script>
<script src="js/world_environment.js?v=39.15.76" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.76" defer></script>
<script src="js/world_ants_system.js?v=39.15.76" defer></script>
<script src="js/weather_system.js?v=39.15.76" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.76" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.76" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.76" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.76" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.76" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.76" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.76" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.76" defer></script>
<script src="js/simulation_systems.js?v=39.15.76" defer></script>
<script src="js/system_order.js?v=39.15.76" defer></script>
<script src="js/simulation.js?v=39.15.76" defer></script>
<script src="js/colony_situation_system.js?v=39.15.76" defer></script>
<script src="js/world_update.js?v=39.15.76" defer></script>
<script src="js/world_tool_actions.js?v=39.15.76" defer></script>
<script src="js/world_placement_log.js?v=39.15.76" defer></script>
<script src="js/world_event_effects.js?v=39.15.76" defer></script>
<script src="js/command_dispatcher.js?v=39.15.76" defer></script>
<script src="js/text_catalog.js?v=39.15.76" defer></script>
<script src="js/ui.js?v=39.15.76" defer></script>
<script src="js/ui_log.js?v=39.15.76" defer></script>
<script src="js/ui_helpers.js?v=39.15.76" defer></script>
<script src="js/ui_selected.js?v=39.15.76" defer></script>
<script src="js/save_schema.js?v=39.15.76" defer></script>
<script src="js/snapshot_system.js?v=39.15.76" defer></script>
<script src="js/restore_coordinator.js?v=39.15.76" defer></script>
<script src="js/history_system.js?v=39.15.76" defer></script>
<script src="js/save_codec.js?v=39.15.76" defer></script>
<script src="js/save_storage.js?v=39.15.76" defer></script>
<script src="js/save_system.js?v=39.15.76" defer></script>
<script src="js/ui_tooltips.js?v=39.15.76" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.76" defer></script>
<script src="js/ui_ground.js?v=39.15.76" defer></script>
<script src="js/ui_tools.js?v=39.15.76" defer></script>
<script src="js/ui_input_shared.js?v=39.15.76" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.76" defer></script>
<script src="js/ui_input_touch.js?v=39.15.76" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.76" defer></script>
<script src="js/ui_bind.js?v=39.15.76" defer></script>
<script src="js/ui_charts.js?v=39.15.76" defer></script>
<script src="js/ui_family_data.js?v=39.15.76" defer></script>
<script src="js/ui_family_async.js?v=39.15.76" defer></script>
<script src="js/ui_family_layout.js?v=39.15.76" defer></script>
<script src="js/ui_family_paths.js?v=39.15.76" defer></script>
<script src="js/ui_family_render.js?v=39.15.76" defer></script>
<script src="js/main.js?v=39.15.76" defer></script>
<script src="js/debug_tools.js?v=39.15.76" defer></script>
<script src="js/version.js?v=39.15.84" defer></script>
<script src="js/event_bus.js?v=39.15.84" defer></script>
<script src="js/registry_base.js?v=39.15.84" defer></script>
<script src="js/disease_registry.js?v=39.15.84" defer></script>
<script src="js/sound_pack.js?v=39.15.84" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.84" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.84" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.84" defer></script>
<script src="js/item_food_definitions.js?v=39.15.84" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.84" defer></script>
<script src="js/item_registry.js?v=39.15.84" defer></script>
<script src="js/data.js?v=39.15.84" defer></script>
<script src="js/ground_types.js?v=39.15.84" defer></script>
<script src="js/input_mode_manager.js?v=39.15.84" defer></script>
<script src="js/math.js?v=39.15.84" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.84" defer></script>
<script src="js/physics_helpers.js?v=39.15.84" defer></script>
<script src="js/placement_preview_system.js?v=39.15.84" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.84" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.84" defer></script>
<script src="js/mechanical_system.js?v=39.15.84" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.84" defer></script>
<script src="js/constraint_system.js?v=39.15.84" defer></script>
<script src="js/physics_world_system.js?v=39.15.84" defer></script>
<script src="js/physics_projection_system.js?v=39.15.84" defer></script>
<script src="js/assets.js?v=39.15.84" defer></script>
<script src="js/audio.js?v=39.15.84" defer></script>
<script src="js/perf_profiler.js?v=39.15.84" defer></script>
<script src="js/render.js?v=39.15.84" defer></script>
<script src="js/sim_core.js?v=39.15.84" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.84" defer></script>
<script src="js/structures.js?v=39.15.84" defer></script>
<script src="js/items.js?v=39.15.84" defer></script>
<script src="js/item_type_initializers.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.84" defer></script>
<script src="js/item_update_policy.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.84" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.84" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.84" defer></script>
<script src="js/item_runtime.js?v=39.15.84" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.84" defer></script>
<script src="js/item_render_runtime.js?v=39.15.84" defer></script>
<script src="js/ants.js?v=39.15.84" defer></script>
<script src="js/health.js?v=39.15.84" defer></script>
<script src="js/tarinai.js?v=39.15.84" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.84" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.84" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.84" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.84" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.84" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.84" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.84" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.84" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.84" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.84" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.84" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.84" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.84" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.84" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.84" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.84" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.84" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.84" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.84" defer></script>
<script src="js/tarinai_render.js?v=39.15.84" defer></script>
<script src="js/world.js?v=39.15.84" defer></script>
<script src="js/world_view.js?v=39.15.84" defer></script>
<script src="js/family_graph.js?v=39.15.84" defer></script>
<script src="js/world_family_social.js?v=39.15.84" defer></script>
<script src="js/impact_core_system.js?v=39.15.84" defer></script>
<script src="js/impact_response_system.js?v=39.15.84" defer></script>
<script src="js/collision_response_system.js?v=39.15.84" defer></script>
<script src="js/world_combat_effects.js?v=39.15.84" defer></script>
<script src="js/world_environment.js?v=39.15.84" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.84" defer></script>
<script src="js/world_ants_system.js?v=39.15.84" defer></script>
<script src="js/weather_system.js?v=39.15.84" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.84" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.84" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.84" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.84" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.84" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.84" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.84" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.84" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.84" defer></script>
<script src="js/simulation_systems.js?v=39.15.84" defer></script>
<script src="js/system_order.js?v=39.15.84" defer></script>
<script src="js/simulation.js?v=39.15.84" defer></script>
<script src="js/colony_situation_system.js?v=39.15.84" defer></script>
<script src="js/world_update.js?v=39.15.84" defer></script>
<script src="js/world_tool_actions.js?v=39.15.84" defer></script>
<script src="js/world_placement_log.js?v=39.15.84" defer></script>
<script src="js/world_event_effects.js?v=39.15.84" defer></script>
<script src="js/command_dispatcher.js?v=39.15.84" defer></script>
<script src="js/text_catalog.js?v=39.15.84" defer></script>
<script src="js/ui.js?v=39.15.84" defer></script>
<script src="js/ui_log.js?v=39.15.84" defer></script>
<script src="js/ui_helpers.js?v=39.15.84" defer></script>
<script src="js/ui_selected.js?v=39.15.84" defer></script>
<script src="js/save_schema.js?v=39.15.84" defer></script>
<script src="js/snapshot_system.js?v=39.15.84" defer></script>
<script src="js/restore_coordinator.js?v=39.15.84" defer></script>
<script src="js/history_system.js?v=39.15.84" defer></script>
<script src="js/save_codec.js?v=39.15.84" defer></script>
<script src="js/save_storage.js?v=39.15.84" defer></script>
<script src="js/save_system.js?v=39.15.84" defer></script>
<script src="js/ui_tooltips.js?v=39.15.84" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.84" defer></script>
<script src="js/ui_ground.js?v=39.15.84" defer></script>
<script src="js/ui_tools.js?v=39.15.84" defer></script>
<script src="js/ui_input_shared.js?v=39.15.84" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.84" defer></script>
<script src="js/ui_input_touch.js?v=39.15.84" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.84" defer></script>
<script src="js/ui_bind.js?v=39.15.84" defer></script>
<script src="js/ui_charts.js?v=39.15.84" defer></script>
<script src="js/ui_family_data.js?v=39.15.84" defer></script>
<script src="js/ui_family_async.js?v=39.15.84" defer></script>
<script src="js/ui_family_layout.js?v=39.15.84" defer></script>
<script src="js/ui_family_paths.js?v=39.15.84" defer></script>
<script src="js/ui_family_render.js?v=39.15.84" defer></script>
<script src="js/main.js?v=39.15.84" defer></script>
<script src="js/debug_tools.js?v=39.15.84" defer></script>
</body>
</html>

View file

@ -415,10 +415,11 @@ class AntActor {
return this.kind === "queen" ? "ant_queen" : this.workerSpriteId();
}
draw(ctx, time = 0, lighting = null) {
draw(ctx, ...args) {
if (this.dead) return;
const id = this.spriteId();
const img = getRenderableImage(id, id);
const lighting = args.find(value => value && typeof value === "object") || null;
const lightState = lighting || getLightingState(this.world);
ctx.save();
const shadow = projectedShadowParams(lightState, this.kind === "queen" ? 0.78 : 0.26);

View file

@ -55,6 +55,12 @@ const ALPHA_BOUNDS = {
sunbath_1: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
sunbath_2: { x: 69, y: 18, w: 374, h: 432, imageW: 512, imageH: 468 },
sunbath_3: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
hot_1: { x: 16, y: 20, w: 480, h: 428, imageW: 512, imageH: 468 },
hot_2: { x: 16, y: 20, w: 480, h: 428, imageW: 512, imageH: 468 },
hot_3: { x: 16, y: 20, w: 480, h: 428, imageW: 512, imageH: 468 },
cold_1: { x: 16, y: 17, w: 480, h: 433, imageW: 512, imageH: 468 },
cold_2: { x: 16, y: 17, w: 480, h: 433, imageW: 512, imageH: 468 },
cold_3: { x: 16, y: 17, w: 480, h: 433, imageW: 512, imageH: 468 },
plushie_bear: { x: 0, y: 0, w: 150, h: 192, imageW: 150, imageH: 192 },
};
@ -67,8 +73,8 @@ const STAIN_CACHE_LIMIT = 64;
const SUNBATH_IMAGE_IDS = new Set(SPRITES
.map(asset => asset?.id || "")
.filter(id => String(id).startsWith("sunbath")));
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
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", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "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", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
function trimCanvasCache(cache, limit = 128) {
while (cache.size > limit) {
@ -169,7 +175,6 @@ function setupAssetIndex() {
function markImageLoaded(asset, img) {
const w = img.naturalWidth || img.width || ALPHA_BOUNDS[asset.id]?.imageW || 512;
const h = img.naturalHeight || img.height || ALPHA_BOUNDS[asset.id]?.imageH || 512;
const bounds = ALPHA_BOUNDS[asset.id] || { x: 0, y: 0, w, h, imageW: w, imageH: h };
imageMetrics.set(asset.id, {
w,
h,

View file

@ -20,6 +20,9 @@ const audio = {
samplePaths: {},
soundCategories: {},
soundMap: {},
_startupAudioDampenStarted: false,
_startupAudioDampenUntil: 0,
_currentPlayGainScale: 1,
applySoundPack(pack = window.TARINAI_SOUND_PACK) {
if (!pack || typeof pack !== "object") return false;
if (pack.categoryGain && typeof pack.categoryGain === "object") this.categoryGain = { ...this.categoryGain, ...pack.categoryGain };
@ -39,7 +42,8 @@ const audio = {
if (!this.masterGainNode) {
const ctx = this.ctx;
this.masterGainNode = ctx.createGain();
this.masterGainNode.gain.setValueAtTime(1.85, ctx.currentTime);
this.masterGainNode.gain.setValueAtTime(0.0001, ctx.currentTime);
this.masterGainNode.gain.linearRampToValueAtTime(0.78, ctx.currentTime + 0.18);
if (typeof ctx.createDynamicsCompressor === "function") {
const compressor = ctx.createDynamicsCompressor();
compressor.threshold.value = -18;
@ -135,6 +139,16 @@ const audio = {
this.sampleLast[id] = t;
return true;
},
startupGainScale() {
const now = performance.now() / 1000;
if (!this._startupAudioDampenStarted) {
this._startupAudioDampenStarted = true;
this._startupAudioDampenUntil = now + 0.85;
return 0.26;
}
if (now < (this._startupAudioDampenUntil || 0)) return 0.42;
return 1;
},
playSample(key, minGap = 0.16, category = null) {
const base = this.samples[key];
if (!base) return false;
@ -143,8 +157,9 @@ const audio = {
this.lastPlayedId = key;
window.TarinaiEvents.emit("audio:play", { id: key, category: cat, synthetic: false });
this.ensure();
const playScale = this.startupGainScale();
const node = base.cloneNode(true);
node.volume = Math.min(1, base.volume * (this.categoryGain?.[cat] ?? 1) * (this.idGain?.[key] ?? 1));
node.volume = Math.min(1, base.volume * (this.categoryGain?.[cat] ?? 1) * (this.idGain?.[key] ?? 1) * playScale);
this.beginVoice(Math.max(0.08, Number(node.duration) || 0.16));
node.play().catch(() => {});
return true;
@ -155,7 +170,7 @@ const audio = {
if (!ctx) return;
if ((this.activeVoices || 0) >= (this.maxConcurrent || 16) && category !== "notify") return;
this.beginVoice(dur);
gain = Math.min(0.95, Math.max(0.0002, gain * (this.categoryGain?.[category] ?? 1) * (this._synthIdGain || 1)));
gain = Math.min(0.95, Math.max(0.0002, gain * (this.categoryGain?.[category] ?? 1) * (this._synthIdGain || 1) * (this._currentPlayGainScale || 1)));
const t0 = ctx.currentTime;
const osc = ctx.createOscillator();
const g = ctx.createGain();
@ -175,7 +190,7 @@ const audio = {
if (!ctx) return;
if ((this.activeVoices || 0) >= (this.maxConcurrent || 16) && category !== "notify") return;
this.beginVoice(dur);
gain = Math.min(0.90, Math.max(0.0002, gain * (this.categoryGain?.[category] ?? 1) * (this._synthIdGain || 1)));
gain = Math.min(0.90, Math.max(0.0002, gain * (this.categoryGain?.[category] ?? 1) * (this._synthIdGain || 1) * (this._currentPlayGainScale || 1)));
const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
const data = buffer.getChannelData(0);
@ -197,16 +212,19 @@ const audio = {
if (!this.canPlay(id, category, minGap)) return false;
this.lastPlayedId = id;
window.TarinaiEvents.emit("audio:play", { id, category, synthetic: !this.samples[id] });
const playScale = this.startupGainScale();
if (this.samples[id]) {
const base = this.samples[id];
this.ensure();
const node = base.cloneNode(true);
node.volume = Math.min(1, base.volume * (this.categoryGain?.[category] ?? 1) * (this.idGain?.[id] ?? 1));
node.volume = Math.min(1, base.volume * (this.categoryGain?.[category] ?? 1) * (this.idGain?.[id] ?? 1) * playScale);
this.beginVoice(Math.max(0.08, Number(node.duration) || 0.16));
node.play().catch(() => {});
return true;
}
this.synth(id, category);
const prevPlayScale = this._currentPlayGainScale || 1;
this._currentPlayGainScale = playScale;
try { this.synth(id, category); } finally { this._currentPlayGainScale = prevPlayScale; }
return true;
},
synth(id, category) {
@ -262,6 +280,8 @@ const audio = {
},
bubble(text) {
const s = String(text || "");
if (s.includes("ぶるぶる")) return this.playSample("voice_temp_cold", 1.20, "voice") || this.play("voice_temp_cold", { category: "voice", minGap: 1.20 });
if (s.includes("あちゅい")) return this.playSample("voice_temp_hot", 1.20, "voice") || this.play("voice_temp_hot", { category: "voice", minGap: 1.20 });
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 });

View file

@ -206,7 +206,7 @@
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
if (!worldRef || !item || item.dead) return true;
const important = opts.important || new Set(["zunchi", "grass", "grass_bed", "water", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box"]);
const important = opts.important || new Set(["zunchi", "grass", "grass_bed", "water", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box"]);
const isPhysicsItem = (global.TarinaiMechanicalSystem.isMechanicalType(item.type) || item.type === "rope" || item.type === "rod" || isFenceItemType(item.type));
if (!important.has(item.type) && !isPhysicsItem) return false;
const itemRects = placementRectsFor(worldRef, item);

View file

@ -188,7 +188,6 @@
const isDangerous = ballSpeed >= 285;
if (isDangerous && now - (t.lastBallDamageAt || -999) > 0.46) {
const bvx = ballSpeed > 0.01 ? (ball.vx || 0) / ballSpeed : nx;
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
const damage = physicalDamageFromImpactSpeed(ballSpeed, { min: 3.5, max: 38, scale: 18 });
t.lastBallDamageAt = now;
worldRef.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
@ -227,7 +226,6 @@
const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
const kickedSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38);
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);

View file

@ -737,8 +737,6 @@
for (let i = 0; i < ps.length - 1; i += Math.max(1, lod.particleStride)) {
const p1 = ps[i], p2 = ps[i + 1];
if (!p1 || !p2) continue;
const mx = (p1.x + p2.x) * 0.5;
const my = (p1.y + p2.y) * 0.5;
for (const rect of getRopeObstacleRects()) {
if (rect?.item?.id && endpointIds.has(rect.item.id)) continue;
const pad = rect?.mechanical ? 8.8 : 7.0;

View file

@ -33,6 +33,13 @@ const SPRITES = [
{ id: "sunbath_1", label: "\u65e5\u5149\u6d74\u305f\u308a\u306a\u3044 1", path: "assets/sprites/tarinai_31_sunbath_01.webp", actionOnly: true },
{ id: "sunbath_2", label: "\u65e5\u5149\u6d74\u305f\u308a\u306a\u3044 2", path: "assets/sprites/tarinai_32_sunbath_02.webp", actionOnly: true },
{ id: "sunbath_3", label: "\u65e5\u5149\u6d74\u305f\u308a\u306a\u3044 3", path: "assets/sprites/tarinai_33_sunbath_03.webp", actionOnly: true },
{ id: "hot_1", label: "\u6691\u3044\u305f\u308a\u306a\u3044 1", path: "assets/sprites/tarinai_34_hot_01.webp", displayOnly: true },
{ id: "hot_2", label: "\u6691\u3044\u305f\u308a\u306a\u3044 2", path: "assets/sprites/tarinai_35_hot_02.webp", displayOnly: true },
{ id: "hot_3", label: "\u6691\u3044\u305f\u308a\u306a\u3044 3", path: "assets/sprites/tarinai_36_hot_03.webp", displayOnly: true },
{ id: "cold_1", label: "\u5bd2\u3044\u305f\u308a\u306a\u3044 1", path: "assets/sprites/tarinai_37_cold_01.webp", displayOnly: true },
{ id: "cold_2", label: "\u5bd2\u3044\u305f\u308a\u306a\u3044 2", path: "assets/sprites/tarinai_38_cold_02.webp", displayOnly: true },
{ id: "cold_3", label: "\u5bd2\u3044\u305f\u308a\u306a\u3044 3", path: "assets/sprites/tarinai_39_cold_03.webp", displayOnly: true },
];
const CONFIG = {
initialPopulation: 18,
@ -46,6 +53,16 @@ const CONFIG = {
energyPerMinute: 8.2,
reproductionCooldown: 18,
dayLength: 120,
standardTemperature: 15,
climateTemperatureMin: -20,
climateTemperatureMax: 50,
temperatureMin: -Infinity,
temperatureMax: Infinity,
temperatureComfortMin: 10,
temperatureComfortMax: 25,
temperatureStressMargin: 5,
temperatureEnergyMargin: 10,
temperatureItemRange: 127,
childGrowTime: 58,
fightCooldown: 8,
weatherChangeMin: 46,

View file

@ -27,6 +27,8 @@ const HEALTH = (() => {
antEaten: "\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f",
giantDrug: "\u5de8\u5927\u85ac",
dwarfDrug: "\u77ee\u5c0f\u85ac",
freeze: "凍結",
heatstroke: "熱中症",
});
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
@ -34,6 +36,8 @@ const HEALTH = (() => {
const text = String(reason || "");
if (!text) return "";
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
if (/凍結|低温|寒さ|cold|freeze|freezing/i.test(text)) return CAUSES.freeze;
if (/熱中症|高温|暑さ|heat|hot/i.test(text)) return CAUSES.heatstroke;
if (/\u5c04\u6483|shoot/.test(text)) return CAUSES.shooting;
if (/\u77f3\u306b\u6f70\u3055\u308c\u305f|\u77f3.*\u6f70|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush;
const collisionWith = text.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/);
@ -123,6 +127,13 @@ const HEALTH = (() => {
return weakenedDeathReasonFor(weakenedTrace);
}
if (direct && direct !== CAUSES.stress) return direct;
const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null;
const recentTemperatureDamageAt = Number(t.lastTemperatureDamageAt || -999);
const recentTemperatureDamage = (t.world?.time || 0) - recentTemperatureDamageAt <= 36;
if (recentTemperatureDamage && tempStatus && (tempStatus.harmExcess || 0) > 0 && (t.energy <= 26 || t.stress >= 112 || t.mood <= 18)) {
return tempStatus.direction === "cold" ? CAUSES.freeze : CAUSES.heatstroke;
}
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)) return CAUSES.fight;
@ -140,7 +151,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;
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;
if (canWeaken && (opts.weakened || (hasRecentMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause);
return cause;
}

View file

@ -4,7 +4,6 @@
const ImpactCore = global.TarinaiImpactCoreSystem;
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165;
const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed;
const itemHasAttachedLink = ImpactCore.itemHasAttachedLink;
function itemDropImpact(worldRef, it) {
if (!worldRef) return undefined;

View file

@ -57,7 +57,6 @@
}
refresh() {
const previous = this.classification;
const next = this.classify();
return next;
}

View file

@ -72,7 +72,6 @@ function drawRopeParticlePath(ctx, particles, ox, oy) {
Object.assign(Item.prototype, {
draw(ctx, t, lighting = null) {
const lightState = lighting || getLightingState(world);
const night = clamp(lightState.nightStrength * 1.35, 0, 1);
const warm = lightState.warmth;
const styleNight = lightState.light < 0.42;
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
@ -481,6 +480,96 @@ draw(ctx, t, lighting = null) {
ctx.fillText("\u2026", 0, -this.r * 0.58);
}
ctx.restore();
} else if (this.type === "dry_ice") {
const r = this.r || 21;
const time = this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0);
ctx.save();
ctx.shadowColor = "rgba(90, 178, 255, 0.30)";
ctx.shadowBlur = 11;
// 旧版の氷塊風から、白い保冷デュワー缶風の外観へ変更。
const bodyW = r * 1.38;
const bodyH = r * 1.56;
const x = -bodyW / 2;
const y = -bodyH * 0.36;
const capH = r * 0.34;
const footH = r * 0.18;
const grad = ctx.createLinearGradient(x, y, x + bodyW, y);
grad.addColorStop(0, styleNight ? "rgba(134, 169, 188, 0.96)" : "rgba(214, 242, 252, 0.98)");
grad.addColorStop(0.48, styleNight ? "rgba(230, 247, 252, 0.98)" : "rgba(255, 255, 252, 1)");
grad.addColorStop(1, styleNight ? "rgba(108, 150, 176, 0.98)" : "rgba(164, 220, 244, 0.98)");
ctx.fillStyle = grad;
ctx.strokeStyle = styleNight ? "rgba(54, 96, 124, 0.82)" : "rgba(63, 138, 180, 0.76)";
ctx.lineWidth = 2.1;
roundedRect(ctx, x, y, bodyW, bodyH, r * 0.28);
ctx.fill();
ctx.stroke();
ctx.fillStyle = styleNight ? "rgba(72, 118, 148, 0.96)" : "rgba(88, 176, 220, 0.96)";
roundedRect(ctx, -bodyW * 0.42, y - capH * 0.52, bodyW * 0.84, capH, r * 0.14);
ctx.fill();
ctx.strokeStyle = styleNight ? "rgba(42, 74, 96, 0.78)" : "rgba(48, 119, 158, 0.72)";
ctx.stroke();
ctx.fillStyle = styleNight ? "rgba(41, 86, 118, 0.86)" : "rgba(39, 147, 198, 0.86)";
roundedRect(ctx, -bodyW * 0.34, y + bodyH * 0.24, bodyW * 0.68, bodyH * 0.22, r * 0.10);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.92)";
ctx.font = `bold ${Math.max(9, Math.round(r * 0.34))}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("CO₂", 0, y + bodyH * 0.35);
ctx.strokeStyle = "rgba(255,255,255,0.72)";
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(x + bodyW * 0.25, y + bodyH * 0.05);
ctx.lineTo(x + bodyW * 0.25, y + bodyH * 0.72);
ctx.stroke();
ctx.fillStyle = styleNight ? "rgba(61, 89, 110, 0.72)" : "rgba(84, 132, 156, 0.58)";
roundedRect(ctx, -bodyW * 0.48, y + bodyH - footH * 0.4, bodyW * 0.96, footH, r * 0.10);
ctx.fill();
for (let i = 0; i < 4; i++) {
const phase = (time * (0.42 + i * 0.06) + i * 0.23 + (this.seed || 0)) % 1;
const sx = -r * 0.50 + i * r * 0.33 + Math.sin(time * 1.4 + i) * r * 0.06;
const sy = y - capH * 0.62 - phase * r * 0.76;
ctx.globalAlpha = (0.30 + (1 - phase) * 0.28) * (styleNight ? 0.76 : 1);
ctx.fillStyle = "rgba(222, 247, 255, 0.72)";
ctx.beginPath();
ctx.ellipse(sx, sy, r * (0.12 + phase * 0.08), r * (0.07 + phase * 0.05), Math.sin(i) * 0.7, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
} else if (this.type === "stove") {
const r = this.r || 24;
ctx.save();
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
ctx.shadowBlur = 9;
ctx.fillStyle = styleNight ? "rgba(96, 72, 58, 0.96)" : "rgba(150, 104, 76, 0.96)";
ctx.strokeStyle = styleNight ? "rgba(49, 35, 29, 0.78)" : "rgba(92, 62, 45, 0.72)";
ctx.lineWidth = 2.2;
roundedRect(ctx, -r * 0.78, -r * 0.62, r * 1.56, r * 1.24, r * 0.20);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(42, 32, 28, 0.92)";
roundedRect(ctx, -r * 0.48, -r * 0.34, r * 0.96, r * 0.62, r * 0.14);
ctx.fill();
const flame = 0.85 + 0.15 * Math.sin((this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0)) * 4.2 + (this.seed || 0));
ctx.fillStyle = "rgba(255, 168, 57, 0.92)";
ctx.beginPath();
ctx.moveTo(0, r * 0.16);
ctx.bezierCurveTo(-r * 0.38, -r * 0.10, -r * 0.10, -r * 0.52 * flame, 0, -r * 0.66 * flame);
ctx.bezierCurveTo(r * 0.34, -r * 0.20, r * 0.40, -r * 0.02, 0, r * 0.16);
ctx.fill();
ctx.fillStyle = "rgba(255, 235, 120, 0.82)";
ctx.beginPath();
ctx.ellipse(0, -r * 0.10, r * 0.16, r * 0.28 * flame, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
} else if (this.type === "ant_nest") {
const count = clamp(Math.round(this.antCount ?? ANT_NEST_START_COUNT ?? 6), 0, ANT_NEST_MAX_COUNT ?? 10);
ctx.fillStyle = styleNight ? "#5e4634" : (styleWarm ? "#956b42" : "#77583a");
@ -576,9 +665,10 @@ draw(ctx, t, lighting = null) {
ctx.font = "bold 10px ui-rounded, sans-serif";
ctx.textAlign = "center";
ctx.fillText(String(Math.ceil(this.fuseTimer ?? 5)), 0, this.r * 1.42);
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence") {
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "glass_wall" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence") {
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
const gate = this.type === "gate_fence";
const glassWall = this.type === "glass_wall";
const angle = itemAngleFor(this);
const len = Math.max(112, this.r * 3.55);
const thick = Math.max(10, this.r * 0.31);
@ -604,7 +694,31 @@ draw(ctx, t, lighting = null) {
ctx.fill();
ctx.restore();
};
if (gate) {
if (glassWall) {
const glassGrad = ctx.createLinearGradient(-len / 2, -thick / 2, len / 2, thick / 2);
glassGrad.addColorStop(0, styleNight ? "rgba(92,134,154,0.30)" : "rgba(208,244,255,0.36)");
glassGrad.addColorStop(0.48, styleNight ? "rgba(152,196,210,0.18)" : "rgba(255,255,255,0.18)");
glassGrad.addColorStop(1, styleNight ? "rgba(66,104,128,0.36)" : "rgba(134,214,238,0.32)");
ctx.fillStyle = glassGrad;
ctx.strokeStyle = styleNight ? "rgba(184,228,244,0.78)" : "rgba(80,176,208,0.72)";
ctx.lineWidth = Math.max(2, thick * 0.18);
ctx.beginPath();
roundedRect(ctx, -len / 2, -thick / 2, len, thick, thick * 0.45);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = styleNight ? "rgba(235,252,255,0.46)" : "rgba(255,255,255,0.72)";
ctx.lineWidth = Math.max(1.2, thick * 0.08);
for (let x = -len * 0.42; x <= len * 0.35; x += len * 0.26) {
ctx.beginPath();
ctx.moveTo(x, -thick * 0.28);
ctx.lineTo(x + len * 0.16, thick * 0.28);
ctx.stroke();
}
ctx.fillStyle = styleNight ? "rgba(220,248,255,0.74)" : "rgba(255,255,255,0.82)";
ctx.beginPath();
roundedRect(ctx, -len * 0.46, -thick * 0.32, len * 0.32, thick * 0.10, thick * 0.10);
ctx.fill();
} else if (gate) {
const pivotX = -len / 2;
ctx.save();
drawRail(pivotX, -thick / 2, len, thick, open ? 0.34 : 1);
@ -660,7 +774,6 @@ draw(ctx, t, lighting = null) {
const speed = Number((powered ? pb?.scalar?.(this, "motorSpeed", 0) : pb?.scalar?.(this, "spin", 0)) || 0) || 0;
const segments = (pb?.segments?.(this) || [[-78, 0, 78, 0], [0, -52, 0, 52]]);
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 12) || 12) || 12));
const tier = mechanicalRenderTier();
ctx.save();
ctx.rotate(angle);
ctx.lineCap = "round";
@ -688,7 +801,6 @@ draw(ctx, t, lighting = null) {
const enabled = pb?.scalar?.(this, "solid", true) !== false;
const segments = (pb?.segments?.(this) || [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]]);
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 14) || 14) || 14));
const tier = mechanicalRenderTier();
ctx.save();
ctx.rotate(angle);
ctx.lineCap = "round";

View file

@ -38,6 +38,8 @@ const TOOL_DEFINITIONS = Object.freeze({
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" },
dry_ice: { id: "dry_ice", itemType: "dry_ice", label: "ドライアイス", placeable: true, scalable: false, radius: 21, amount: 999, iconText: "🧊", collisionShape: "circle", tooltip: "周囲の気温を10℃下げる。効果は重ねられる。" },
stove: { id: "stove", itemType: "stove", label: "ストーブ", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "🔥", collisionShape: "circle", tooltip: "周囲の気温を10℃上げる。効果は重ねられる。" },
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, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
@ -47,6 +49,7 @@ const TOOL_DEFINITIONS = Object.freeze({
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
glass_wall: { id: "glass_wall", itemType: "glass_wall", label: "\u30ac\u30e9\u30b9\u58c1", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "\u25ad", collisionShape: "oriented_rect", tooltip: "\u900f\u660e\u306a\u58c1\u3002\u305f\u308a\u306a\u3044\u306f\u901a\u308c\u306a\u3044\u304c\u3001\u898b\u3048\u3066\u3044\u308b\u5bfe\u8c61\u3078\u306f\u8fc2\u56de\u305b\u305a\u306b\u5411\u304b\u3046\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
@ -62,20 +65,19 @@ const TOOL_DEFINITIONS = Object.freeze({
const ITEM_TRAITS = Object.freeze({
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "dry_ice", "stove", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
hazard: new Set(["firecracker", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
pin: new Set(["pushpin", "oshibyo"]),
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
sleepFurniture: new Set(["bed", "nest_box", "grass_bed"]),
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "plushie", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
draggable: new Set(["ball", "stone", "bed", "nest_box", "dry_ice", "stove", "signboard", "plushie", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
});
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence"]));
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence"]));
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
const LINK_ITEM_TYPES = Object.freeze(new Set(["rope", "rod"]));
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator"]);
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "glass_wall", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator"]);
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES]));
function isFenceItemType(type = "") { return FENCE_ITEM_TYPES.has(String(type || "")); }
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
@ -120,17 +122,11 @@ function pinBehaviorFor(type = "") {
}
let ITEM_VISUAL_DEFINITIONS = null;
let FOOD_REGISTRY = null;
let FOOD_DEFINITIONS = null;
let EFFECT_REGISTRY = null;
let EFFECT_DEFINITIONS = null;
const TOOL_CATEGORIES = Object.freeze([
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] },
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
{ id: "medicine", label: "\u85ac", 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", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "ball", "signboard"] },
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "dry_ice", "stove", "ball", "signboard"] },
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "shoot", "ant_nest"] },
]);

View file

@ -424,7 +424,7 @@ function previewItemRadiusFor(type = "") {
const def = itemDefinition(type);
const base = Number(def?.radius || itemRadiusFor?.(type, 16) || 16);
if (type === "genkotsu") return 30;
if (type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24;
if (type === "fence_v" || type === "fence_h" || type === "glass_wall" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24;
if (type === "rotator") return 25;
if (type === "reciprocator") return 26;
if (type === "nest_box") return 26;
@ -545,7 +545,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
ctx = drawCtx;
ctx.save();
ctx.clearRect(0, 0, w, h);
const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence" ? 1 : 4;
const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" || type === "glass_wall" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence" ? 1 : 4;
const centerX = watermark ? w * 0.78 : w / 2;
const centerY = watermark ? (h * 0.80 + baseOffsetY * 0.15) : (h / 2 + baseOffsetY);
ctx.translate(centerX, centerY);
@ -557,6 +557,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
duplicator: 0.82,
fence_v: 0.70,
fence_h: 0.70,
glass_wall: 0.74,
bounce_fence: 0.74,
bounce_fence_v: 0.74,
gate_fence: 0.74,
@ -573,7 +574,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
if (isConsumableSpriteType(type)) scale *= 1.95;
else if (["grass", "water", "zunda_juice"].includes(type)) scale *= 1.85;
else if (type === "duplicator") scale *= 1.55;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (isParamEffectItemType(type)) scale *= 1.35;
if (watermark) scale *= 1.42;
ctx.scale(scale, scale);

View file

@ -531,12 +531,6 @@
return aabbFromRects(item, obstacleRects(item), "obstacle");
}
function hazardBoundsAabb(item) {
if (!item || item.dead) return null;
if (item.type === "poison_block") return aabbFromRects(item, poisonHazardRects(item), "hazard");
return boundsAabb(item);
}
function reach(item) {
if (!item) return 64;
const base = Math.max(num(item.r, 64), extent(item));
@ -1031,12 +1025,6 @@
if (pairFrameGuard(a, b, worldRef, 6)) return false;
let contact = strongestRectContact(obstacleRects(a), obstacleRects(b), 0.75);
if (!contact) return false;
const now = worldRef?.time || 0;
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null);
const recentlyHandled = (worldRef._mechanicalContactAt[key] || -999) + 0.045 > now;
worldRef._mechanicalContactAt[key] = now;
let info = contact.info;
const separated = applyMechanicalPairSeparation(a, b, info, dt, worldRef);
if (separated) {
@ -1100,7 +1088,6 @@
const rects = obstacleRects(item);
const fences = fenceRectsNear(item, worldRef, radius || reach(item) + 80);
let changed = false;
const now = worldRef.time || 0;
for (const mechRect of rects) {
for (const fenceRect of fences) {
let info = rectOverlapInfo(mechRect, fenceRect, item.type === "rotator" ? 1.35 : 0.5);
@ -1117,11 +1104,6 @@
applyImpulse(item, info.x, info.y, -info.nx * impulse, -info.ny * impulse, separated ? 0.92 : 1.10);
applyRailCorrection(item, info.nx, info.ny, info.overlap, 0.82);
if (item.type === "rotator") brakePoweredRotatorOnMechanicalContact(item, dt, worldRef, "fence");
const key = `f:${item.id}:${fenceRect.item?.id || "?"}`;
worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null);
if ((worldRef._mechanicalContactAt[key] || -999) + 0.08 <= now) {
worldRef._mechanicalContactAt[key] = now;
}
changed = true;
}
}
@ -1277,7 +1259,6 @@
resetAnchor(item);
if (item.playerHeld || item._heldByPlayer) { item.prevX = item.x; item.prevY = item.y; return false; }
const powered = isPowered(item);
const travel = halfTravel(item);
const prevX = num(item.x, ps(item, "railAnchorX", 0)), prevY = num(item.y, ps(item, "railAnchorY", 0));
item.prevX = prevX; item.prevY = prevY;
const baseSpeed = Math.max(0, ps(item, "railMotorSpeed", 92));

View file

@ -236,15 +236,6 @@
global.TarinaiMechanicalSystem.invalidateGeometry(item);
return true;
}
function pose(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.pose || null; }
function velocity(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.velocity || null; }
function motor(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.motor || null; }
function rail(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.rail || null; }
function shape(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.shape || null; }
function collision(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.collision || null; }
function hazard(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.hazard || null; }
function sleep(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.sleep || null; }
function syncPoseFromItem(item) {
if (!item) return null;
// Capture the item pose before ensureBody()/normalizeBody() can write the

View file

@ -152,6 +152,10 @@
if (!tmp) return null;
const r = tmp.r;
let extra = {};
if (type === "dry_ice" || type === "stove") {
const range = Math.max(24, global.CONFIG?.temperatureItemRange ?? 190);
extra.influence = { kind: "temperatureEffectRange", shape: "circle", x, y, radius: range, type };
}
if (type === "grass") {
const overlaps = global.TarinaiPhysicsSoftClear.collectForPlacement(worldRef, tmp) || [];
const replacingGrass = overlaps.some(it => it?.type === "grass");

View file

@ -417,8 +417,10 @@ function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale =
ctx.globalAlpha = (blocked ? 0.30 : 0.40) * Math.max(0.2, Number(alphaScale || 1) || 1);
ctx.setLineDash([12, 9]);
ctx.lineWidth = 2.0;
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.72)" : "rgba(80,174,222,0.82)";
ctx.fillStyle = blocked ? "rgba(232,84,72,0.055)" : "rgba(80,174,222,0.075)";
const tempRange = influence?.kind === "temperatureEffectRange";
const hotRange = tempRange && influence?.type === "stove";
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.72)" : (tempRange ? (hotRange ? "rgba(230,112,48,0.80)" : "rgba(74,166,230,0.82)") : "rgba(80,174,222,0.82)");
ctx.fillStyle = blocked ? "rgba(232,84,72,0.055)" : (tempRange ? (hotRange ? "rgba(255,138,56,0.075)" : "rgba(74,166,230,0.075)") : "rgba(80,174,222,0.075)");
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
@ -506,7 +508,6 @@ function drawOperationToolPreview(ctx, world) {
if (item) {
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem.forItem(world, item) || null;
const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || [];
const boundsBlocked = Boolean(placementInfo?.boundsBlocked);
const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item)));
let copyPreviewDrew = false;
if (typeof item.draw === "function") {
@ -827,7 +828,7 @@ function visibleWorldRect(worldRef, margin = 160) {
function renderRadiusForEntity(entity) {
if (!entity) return 40;
if (entity.type === "genkotsu") return Math.max(420, (entity.r || 88) * 6.2);
if (entity.type === "fence_v" || entity.type === "fence_h" || entity.type === "bounce_fence" || entity.type === "bounce_fence_v" || entity.type === "gate_fence") return Math.max(96, (entity.r || 24) * 4.2);
if (entity.type === "fence_v" || entity.type === "fence_h" || entity.type === "glass_wall" || entity.type === "bounce_fence" || entity.type === "bounce_fence_v" || entity.type === "gate_fence") return Math.max(96, (entity.r || 24) * 4.2);
if (entity.type === "reciprocator") return Math.max(140, TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.reach(entity) || ((entity.r || 64) * 2.8 + (TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem.scalar(entity, "railTravel", 150) || 150)));
if (entity.type === "nest_box") return Math.max(86, (entity.r || 34) * 3.2);
if (entity.type === "ant_nest") return Math.max(72, (entity.r || 28) * 3.0);
@ -1507,22 +1508,7 @@ function render() {
if (world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
renderPerfEnd(endLighting);
const drawAtScreenPosition = (entity, drawFn) => {
if (!entity) return;
const ox = entity.x;
const oy = entity.y;
const screen = world.worldToScreen ? world.worldToScreen(ox, oy) : { x: ox, y: oy };
entity.x = screen.x;
entity.y = screen.y;
try {
drawFn();
} finally {
entity.x = ox;
entity.y = oy;
}
};
const endUiOverlay = renderPerfBegin("render.uiOverlay");
drawAtScreenPosition(world.selected, () => drawSelectedCard(ctx, world, lighting));
drawPointerItemTooltip(ctx, world);
drawShootCursorOverlay(ctx, world);
drawSelectedToolCursorIcon(ctx, world);
@ -1531,23 +1517,25 @@ function render() {
const endHud = renderPerfBegin("render.hud");
// Time HUD
ctx.save();
const hudW = 150;
const hudH = 56;
const hudW = 126;
const hudH = 50;
const w = screenW, h = screenH;
const hudX = w - hudW - 22;
const hudY = 16;
ctx.fillStyle = light > 0.42 ? "rgba(255,251,244,0.76)" : "rgba(247,246,255,0.62)";
roundedRect(ctx, hudX, hudY, hudW, hudH, 18);
roundedRect(ctx, hudX, hudY, hudW, hudH, 14);
ctx.fill();
ctx.strokeStyle = light > 0.42 ? "rgba(118,103,83,0.12)" : "rgba(206,213,242,0.15)";
ctx.stroke();
ctx.fillStyle = "#2a241d";
ctx.font = "bold 16px ui-rounded, sans-serif";
ctx.font = "bold 13px ui-rounded, sans-serif";
ctx.textAlign = "left";
ctx.fillText(`${world.day}\u65e5\u76ee`, hudX + 24, hudY + 25);
ctx.font = "12px ui-rounded, sans-serif";
const seasonDay = world.seasonDayString?.() || `${world.day}日目`;
ctx.fillText(`${seasonDay} ${world.clockString()}`, hudX + 10, hudY + 20);
ctx.font = "11px ui-rounded, sans-serif";
ctx.fillStyle = "rgba(111,101,88,0.86)";
ctx.fillText(`${world.phaseName()} / ${world.clockString()} / ${weatherLabel(world.weather)}`, hudX + 24, hudY + 45);
const hudTemp = world.currentTemperature ?? world.updateTemperature?.(0) ?? 15;
ctx.fillText(`${world.phaseName()} ${weatherLabel(world.weather)} ${Math.round(hudTemp)}`, hudX + 10, hudY + 38);
ctx.restore();
ctx.save();
@ -1615,39 +1603,25 @@ function drawPointerItemTooltip(ctx, world) {
ctx.fillStyle = "#fff8e6";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
lines.forEach((line, i) => ctx.fillText(line, x + 11, y + 11 + lineH * i + lineH / 2));
const separator = info?.kind === "tarinai";
lines.forEach((line, i) => {
ctx.fillText(line, x + 11, y + 11 + lineH * i + lineH / 2);
if (separator && i < lines.length - 1) {
ctx.save();
ctx.globalAlpha = 0.38;
ctx.strokeStyle = "rgba(255,248,220,0.62)";
ctx.lineWidth = 0.8;
const sy = y + 11 + lineH * (i + 1);
ctx.beginPath();
ctx.moveTo(x + 10, sy);
ctx.lineTo(x + w - 10, sy);
ctx.stroke();
ctx.restore();
}
});
ctx.restore();
}
function drawSelectedCard(ctx, world, lighting) {
const t = world.selected;
if (!t || t.dead || !world.tarinai.includes(t)) return;
const w = world.viewportW || world.w;
const h = world.viewportH || world.h;
const cardW = 152;
const cardH = 80;
let x = t.x + 18;
let y = t.y - 100;
x = clamp(x, 10, w - cardW - 10);
y = clamp(y, 10, h - cardH - 10);
ctx.save();
ctx.fillStyle = lighting.light > 0.42 ? "rgba(255,250,239,0.82)" : "rgba(239,243,255,0.74)";
roundedRect(ctx, x, y, cardW, cardH, 14);
ctx.fill();
ctx.strokeStyle = "rgba(84,68,48,0.12)";
ctx.stroke();
ctx.fillStyle = "rgba(42,36,29,0.86)";
ctx.font = "bold 11px ui-rounded, sans-serif";
ctx.fillText(t.name, x + 10, y + 18);
ctx.font = "10px ui-rounded, sans-serif";
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
ctx.fillText(`${window.TEXT_CATALOG?.stateLabel?.(t.state) || t.state || ""}${child}${zHint}`, x + 10, y + 35);
const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : "";
ctx.fillText(`\u7dcf\u5408\u7684\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}/${fmt(typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100))}`, x + 10, y + 52);
ctx.fillText(`\u30b9\u30c8\u30ec\u30b9 ${fmt(t.stress)} \u7a7a\u8179 ${fmt(t.hunger)}`, x + 10, y + 68);
ctx.restore();
}
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();

View file

@ -11,11 +11,6 @@
const {
BINARY_SCHEMA_VERSION,
FIELD_IDS,
GROUND_IDS,
MOOD_IDS,
canonicalMoodId,
enumIndex,
ITEM_TYPE_IDS,
STRUCTURE_TYPE_IDS,
SERVING_FOOD_TYPE_IDS,
PIN_TYPE_IDS,
@ -540,7 +535,6 @@
}
function fieldValue(index = 0) { return FIELD_IDS[Number(index) | 0] || "garden"; }
function groundValue(index = 0) { return GROUND_IDS[Number(index) | 0] || "soil"; }
function summaryFromPayload(w = [], tarinaiCount = 0) {
const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {});
const dayLength = Math.max(1, Number(config.dayLength || 120));

View file

@ -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 = 15;
const BINARY_SCHEMA_VERSION = 19;
const SNAPSHOT_VERSION = 16;
const BINARY_SCHEMA_VERSION = 20;
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
@ -21,7 +21,7 @@
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner"
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner", "dry_ice", "stove", "glass_wall"
]);
function typeIds(names) {
@ -32,7 +32,7 @@
const SERVING_FOOD_TYPE_IDS = typeIds(["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "food"]);
const PIN_TYPE_IDS = typeIds(["pushpin", "oshibyo"]);
const JSON_EXTRA_ITEM_TYPE_IDS = typeIds(["rotator", "poison_block", "reciprocator", "rope", "rod"]);
const ROTATABLE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]);
const ROTATABLE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]);
function enumIndex(list, value, fallback = 0) {
const i = list.indexOf(String(value ?? ""));

View file

@ -707,10 +707,7 @@ class SpatialGrid {
boundsForTrait(it, trait) {
const mech = (typeof window !== "undefined" ? window : globalThis).TarinaiMechanicalSystem;
if (mech?.isMechanicalType?.(it?.type)) {
if (trait === "hazard" && it.type === "poison_block") return mech.hazardBoundsAabb?.(it) || mech.boundsAabb?.(it);
return mech.boundsAabb?.(it);
}
if (mech?.isMechanicalType?.(it?.type)) return mech.boundsAabb?.(it);
const ropePath = it?.type === "rope" ? ((typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem.particles(it) || null) : null;
if (ropePath && ropePath.length) {
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;

View file

@ -36,6 +36,7 @@
function updateEnvironment(worldRef, dt) {
worldRef.updateWeather(dt);
worldRef.updateTemperature?.(dt);
worldRef.updateTerrainDirtyState?.(dt);
worldRef.evaluateColonyMood?.();
}

View file

@ -32,6 +32,31 @@
}
}
if ((worldRef.time || 0) >= (worldRef.nextGrassBedExpiryCheckAt || 0)) {
worldRef.nextGrassBedExpiryCheckAt = (worldRef.time || 0) + 5.0;
const lifespan = Math.max(1, CONFIG.dayLength || 120); // ゲーム内24時間
let removedBeds = 0;
for (const it of worldRef.items || []) {
if (!it || it.dead || it.type !== "grass_bed") continue;
if (!Number.isFinite(Number(it.createdAt))) it.createdAt = worldRef.time || 0;
if ((worldRef.time || 0) - Number(it.createdAt || 0) < lifespan) continue;
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, it, {
reason: "grass-bed-expired",
userReason: "かんたんベッドが古くなって消えた",
wake: false,
panicOwner: false,
});
if (removed) removedBeds += 1;
}
if (removedBeds > 0) {
worldRef.compactItems();
worldRef.updateItemCounts();
worldRef.markSpatialDirty?.("grass-bed-expired");
worldRef.markTerrainDirty?.("grass-bed-expired");
}
}
if ((worldRef.outOfBoundsCheckAt || -999) + 5.0 <= worldRef.time) {
worldRef.outOfBoundsCheckAt = worldRef.time;
worldRef.sanitizeOutOfBounds();

View file

@ -9,11 +9,6 @@
return policy.forScheduledItems(worldRef, visitor);
}
function itemUpdateInterval(item) {
const interval = policy.itemUpdateInterval(item);
return Number.isFinite(interval) ? interval : Infinity;
}
function updateItemsScheduled(worldRef, dt) {
return global.TarinaiItemRuntime.updateScheduled(worldRef, dt);
}

View file

@ -269,7 +269,7 @@
const owner = tarinaiIndex.get(item.ownerId) ?? -1;
const carried = item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1;
const consumed = item.singleUseConsumedById ? (tarinaiIndex.get(item.singleUseConsumedById) ?? -1) : -1;
return [q(item.hp, 10), q(item.maxHp, 10), owner, carried, item.singleUsePending ? 1 : 0, consumed];
return [q(item.hp, 10), q(item.maxHp, 10), owner, carried, item.singleUsePending ? 1 : 0, consumed, q(item.createdAt, 10)];
}
if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0];
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)];
@ -306,6 +306,7 @@
item.carriedById = tarinaiList[extra[3]]?.id || "";
item.singleUsePending = !!extra[4];
item.singleUseConsumedById = tarinaiList[extra[5]]?.id || "";
item.createdAt = Number.isFinite(Number(extra[6])) ? u(extra[6], 10, item.createdAt || NaN) : (Number.isFinite(Number(item.createdAt)) ? item.createdAt : NaN);
if (type === "plushie") {
item.plushieSpriteId = "plushie_bear";
item.onHead = Boolean(item.carriedById);

View file

@ -26,6 +26,8 @@
voice_major_damage_02: 1.0,
voice_major_damage_03: 1.0,
voice_major_damage_04: 1.0,
voice_temp_cold: 1.0,
voice_temp_hot: 1.0,
},
idMinGap: {
sfx_ui_click: 0.030,
@ -41,6 +43,8 @@
voice_major_damage_02: 0.45,
voice_major_damage_03: 0.45,
voice_major_damage_04: 0.45,
voice_temp_cold: 1.20,
voice_temp_hot: 1.20,
},
samplePaths: {
voice_hau: "assets/sounds/voice_001_hau.wav",
@ -56,7 +60,9 @@
voice_major_damage_01: "assets/sounds/major_damage_01.wav",
voice_major_damage_02: "assets/sounds/major_damage_02.wav",
voice_major_damage_03: "assets/sounds/major_damage_03.wav",
voice_major_damage_04: "assets/sounds/major_damage_04.wav"
voice_major_damage_04: "assets/sounds/major_damage_04.wav",
voice_temp_cold: "assets/sounds/voice_008_temperature_buruburu.wav",
voice_temp_hot: "assets/sounds/voice_009_temperature_achui.wav"
},
categories: {
voice: "\u305f\u308a\u306a\u3044\u306e\u58f0",
@ -66,6 +72,7 @@
soundMap: {
voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", voice_sunbath: "voice",
voice_major_damage_01: "voice", voice_major_damage_02: "voice", voice_major_damage_03: "voice", voice_major_damage_04: "voice",
voice_temp_cold: "voice", voice_temp_hot: "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",

View file

@ -25,6 +25,7 @@
this.r = definition.type === "plushie" ? 6.5 : (definition.type === "grass_bed" ? 18 : 24);
this.amount = this.hp;
this.seed = Math.random() * 1000;
this.createdAt = Number.isFinite(Number(world?.time)) ? Number(world.time) : 0;
this.roles = clonePlain(definition.roles);
this.needEffects = clonePlain(definition.needEffects);
this.carriedById = definition.type === "plushie" ? this.ownerId : "";

View file

@ -168,7 +168,9 @@ class Tarinai { constructor(world, opts = {}) {
applyZunchiSlavePersonality(this, { record: false, birth: false });
this.visibleSpriteId = "zunchi_slave";
}
this.tempComfort = opts.tempComfort ?? 0.62;
this.tempComfort = opts.tempComfort ?? (CONFIG.standardTemperature ?? 15);
this.feltTemperature = opts.feltTemperature ?? this.tempComfort;
this.temperatureStatus = null;
this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55;
this.parents = opts.parents || [];
this.parentNames = opts.parentNames || [];

View file

@ -89,7 +89,7 @@ function createSleepBuildGrassBedActionSpec() {
state: "build",
label: "\u5bdd\u308b\u5834\u6240\u304c\u306a\u3044\u306e\u3067\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b",
phrase: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f",
weight: 18,
weight: 26,
timerBacked: true,
selectTarget(t, world) { return (typeof findNearbyBuildMaterial === "function" ? findNearbyBuildMaterial(world, t, "grass_bed", 520) : null) || findNearbyMaterial(world, t, "grassMaterial", 520); },
canStart(t, world, ctx = {}) {
@ -201,6 +201,56 @@ function createSunbathActionSpec() {
};
}
function createTemperatureComfortActionSpec() {
return {
id: "seek_comfort_temperature",
need: "health",
subNeed: "temperature",
state: "seek_temperature",
label: "快適な温度帯へ移動している",
phrase: "快適な温度帯を探している",
text(t, world) {
const felt = world?.temperatureAt?.(t?.x, t?.y) ?? t?.feltTemperature ?? t?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
const status = world?.temperatureStatusFor?.(felt) || t?.temperatureStatus || null;
return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。";
},
weight: 42,
selectTarget(t, world, ctx = {}) {
return world?.findComfortTemperatureSpot?.(t, { force: !!ctx.force, requireComfort: true }) || null;
},
canStart(t, world, ctx = {}) {
const felt = world?.temperatureAt?.(t.x, t.y);
const status = world?.temperatureStatusFor?.(Number.isFinite(Number(felt)) ? felt : (t?.feltTemperature ?? CONFIG.standardTemperature ?? 15));
if (!status || status.comfortable || (status.discomfort || 0) < 2.5) return false;
return !!(ctx.target ?? this.selectTarget(t, world, ctx));
},
start(t, world, ctx = {}) {
const status = world?.temperatureStatusFor?.(world?.temperatureAt?.(t.x, t.y) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15));
const label = status?.direction === "cold" ? "あたたかい場所へ移動している" : (status?.direction === "hot" ? "涼しい場所へ移動している" : this.label);
const target = ctx.target ?? this.selectTarget(t, world, ctx);
return moveToOrUse(t, target, "seek_temperature", label);
},
tick(t, world, dt, needs, ctx = {}) {
const felt = world?.temperatureAt?.(t.x, t.y) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15);
const status = world?.temperatureStatusFor?.(felt);
if (status?.comfortable || (status?.discomfort || 0) <= 1.5) {
if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { health: 10, safety: 8 }, "temperature");
t.goIdle?.("快適な温度になった");
return "finished";
}
const target = t.target && !t.target.dead ? t.target : (ctx.target ?? this.selectTarget(t, world, { force: true, requireComfort: true }));
if (!target) return false;
t.target = target;
if (distXY(t.x, t.y, target.x, target.y) < Math.max(24, (t.radius || 20) * 0.9)) {
const next = this.selectTarget(t, world, { force: true, requireComfort: true });
if (!next || distXY(t.x, t.y, next.x, next.y) < Math.max(28, (t.radius || 20))) return "finished";
t.target = next;
}
return true;
},
};
}
function createWanderActionSpec() {
function targetFor(t, world) {
const radius = rand(80, 180);
@ -350,12 +400,12 @@ function createFightRivalActionSpec() {
const parts = t?.socialReasonParts || {};
const conflict = Number(parts.conflict || 0) || 0;
const socialPull = Math.max(Number(parts.bond || 0) || 0, Number(parts.mate || 0) || 0, Number(parts.family || 0) || 0);
if (!candidate?.target || conflict < 30 || conflict < socialPull * 0.82) return false;
if (!candidate?.target || conflict < 22 || conflict < socialPull * 0.68) return false;
}
return !!(ctx.target ?? candidate?.target ?? this.selectTarget(t, world, ctx));
},
start(t, world, ctx = {}) {
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 18 : 34);
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 18 : 26);
const rival = ctx.target ?? this.selectTarget(t, world, ctx);
if (!isLiveTarinaiEntity(rival)) return false;
t.conflictTargetId = rival.id;
@ -381,9 +431,9 @@ function createIntimidateEnemyActionSpec() {
timerBacked: true,
selectTarget(t) { return t.recentFightRival?.(80, 180) || null; },
canStart(t, world, ctx = {}) {
// 威嚇は通常の欲求行動としては選ばず、喧嘩判定時の30%分岐で実行する。
if (!isCurrentBehaviorForced(t)) return false;
if ((t.intimidateTimer || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false;
const parts = t?.socialReasonParts || {};
if ((t.fightMochiTimer || 0) <= 0.04 && Number(parts.conflict || 0) < 24) return false;
return basicCanStartWithTarget.call(this, t, world, ctx);
},
start(t, world, ctx = {}) {
@ -453,6 +503,7 @@ const TARINAI_ACTION_DEFINITIONS = [
}),
createRestActionSpec(),
createSunbathActionSpec(),
createTemperatureComfortActionSpec(),
createPanicEscapeActionSpec(),
{
id: "hide_at_owned_structure",

View file

@ -98,6 +98,11 @@ function causePhraseForReason(chosenNeed, action, tarinai = null, options = {})
}
function buildReasonText(chosenNeed, tiedNeeds, action, tarinai = null, world = null, options = {}) {
if (action?.id === "seek_comfort_temperature") {
const felt = tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
const status = tarinai?.world?.temperatureStatusFor?.(felt) || tarinai?.temperatureStatus || null;
return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。";
}
const actionText = actionTextForReason(action, tarinai);
const cause = causePhraseForReason(chosenNeed, action, tarinai, options);
const tied = Array.isArray(tiedNeeds) ? tiedNeeds.filter(key => key !== chosenNeed) : [];

View file

@ -39,7 +39,6 @@ if (d > contactReach) return false;
tarinai.startSleeping?.(it, it.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u4f11\u3093\u3067\u3044\u308b" : (isEasyBed ? "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3067\u5bdd\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u3067\u4f11\u3093\u3067\u3044\u308b"));
if (tarinai.world.time > tarinai.nextSleepBubbleAt) {
tarinai.nextSleepBubbleAt = tarinai.world.time + 4.5;
const crowded = occ > 5;
tarinai.world.spawnBubble(tarinai.x, tarinai.y - tarinai.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
}
tarinai.energy += dt * (1.9 + comfort * 1.4);

View file

@ -454,18 +454,46 @@
},
lowHealthSprite() {
// \u4f4e\u4f53\u529b\u306f\u7a7a\u8179\u30fb\u7720\u6c17\u3060\u3051\u3067\u306a\u304f\u3001\u5065\u5eb7\u6b32\u6c42\u306e\u60aa\u5316\u3084\u8ca0\u50b7\u3082\u53cd\u6620\u3059\u308b\u3002
const healthNeed = Number(this.needRaw?.health ?? this.needs?.health ?? 0) || 0;
const hurt = (this.hurtTimer || 0) > 0.10;
const critical = this.hunger > 112 || this.energy < 7 || healthNeed >= 90 || (hurt && this.energy < 20);
if (critical) return "stretch";
const weak = this.hunger > 102 || this.energy < 18 || healthNeed >= 66 || hurt;
if (weak) return "weak";
// 体力比率による段階表示: 30%以下 cry、20%以下 weak、10%以下 stretch。
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100);
const energyRatio = (Number(this.energy) || 0) / Math.max(1, Number(maxEnergy) || 100);
if (energyRatio <= 0.10) return "stretch";
if (energyRatio <= 0.20) return "weak";
if (energyRatio <= 0.30) return "cry";
return null;
},
briefDeathReason(reason = this.deathReason) { return HEALTH.briefDeathReason(this, reason); },
temperatureDiscomfortSprite(baseId = "") {
const world = this.world || null;
const felt = world?.temperatureAt?.(this.x, this.y) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15);
const status = this.temperatureStatus || world?.temperatureStatusFor?.(felt) || null;
if (!status || status.comfortable || (status.discomfort || 0) < 2.5) return "";
const pool = (status.direction === "cold" ? ["cold_1", "cold_2", "cold_3"] : ["hot_1", "hot_2", "hot_3"]).filter(id => SPRITES.some(s => s.id === id));
if (!pool.length) return "";
const mapped = ({
smile: 0,
normal_happy: 0,
normal_tongue: 0,
jito: 1,
normal_smirk: 1,
pokan: 2,
angry: 2,
hungry_70: 2,
})[String(baseId || "")];
const fallbackIndex = status.harmful ? 2 : ((status.discomfort || 0) >= 7 ? 1 : 0);
const index = clamp(Number.isFinite(mapped) ? mapped : fallbackIndex, 0, pool.length - 1);
return pool[index] || pool[0] || "";
},
applyTemperatureSprite(baseId = "") {
const id = String(baseId || "");
if (!id) return id;
if (!["smile", "jito", "normal_smirk", "normal_tongue", "normal_happy", "pokan", "angry", "hungry_70"].includes(id)) return id;
return this.temperatureDiscomfortSprite(id) || id;
},
variantSprite(category) {
const pools = {
hurt: ["hurt", "hurt2"],

View file

@ -301,7 +301,7 @@ function evaluateConflictUrge(tarinai, world, maxDist = 96) {
};
}
}
return best && best.score > (best.forced ? 4 : 12) ? best : null;
return best && best.score > (best.forced ? 4 : 8) ? best : null;
}
function conflictTargetFor(tarinai, world, minScore = 34) {
@ -323,6 +323,7 @@ function findOwnedStructure(world, tarinai, type = "", maxDist = Infinity) {
let best = null, bestD = maxDist;
for (const item of world?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId !== tarinai.id) continue;
if (Number(item.unreachableUntil || 0) > Number(world?.time || 0)) continue;
if (type && item.type !== type) continue;
if (tarinaiTargetBlockedByObstacle(world, tarinai, item, "structure")) continue;
const d = dist(tarinai, item);
@ -335,6 +336,8 @@ function findUnownedStructure(world, tarinai, type = "", maxDist = Infinity) {
let best = null, bestD = maxDist;
for (const item of world?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId) continue;
if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(world?.time || 0)) continue;
if (Number(item.unreachableUntil || 0) > Number(world?.time || 0)) continue;
if (type && item.type !== type) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue;
if (tarinaiTargetBlockedByObstacle(world, tarinai, item, "structure")) continue;

View file

@ -46,6 +46,7 @@ const TARINAI_ACTION_LOCK_SECONDS = {
use_medicine: 2.4,
rest_to_recover: 6.0,
sunbath: 10.5,
seek_comfort_temperature: 5.8,
panic_escape: 2.1,
hide_at_owned_structure: 6.4,
approach_friend: 6.4,
@ -62,18 +63,19 @@ const TARINAI_ACTION_LOCK_SECONDS = {
};
const TARINAI_ACTION_PRIORITY = {
fight_rival: 56,
intimidate_enemy: 42,
fight_rival: 72,
intimidate_enemy: 0,
panic_escape: 94,
hide_at_owned_structure: 90,
use_medicine: 86,
rest_to_recover: 84,
sunbath: 91,
seek_comfort_temperature: 53,
sleep_in_bed: 86,
sleep_build_grass_bed: 85,
sleep_anywhere: 84,
birth_ritual: 72,
approach_mate: 68,
approach_mate: 72,
approach_parent_or_child: 66,
approach_friend: 66,
eat_food: 50,

View file

@ -76,6 +76,18 @@ function updateNeeds(tarinai, world, dt = 0) {
if ((tarinai.hurtTimer || 0) > 0.04 || ((tarinai.stuckPushpinId || "") && (lodgedBehavior?.panicOnAttach || (lodgedBehavior?.damagePerTick || 0) > 0 || (lodgedBehavior?.damageOnAttach || 0) > 0))) raw.health += 36;
const danger = findNearbyDanger(world, tarinai, 170);
if (danger) raw.safety += danger.kind ? 78 : 48;
const feltTemperature = world?.temperatureAt?.(tarinai.x, tarinai.y);
if (Number.isFinite(Number(feltTemperature))) {
const tempStatus = world.temperatureStatusFor?.(feltTemperature) || null;
tarinai.feltTemperature = Number(feltTemperature);
tarinai.tempComfort = Number(feltTemperature);
tarinai.temperatureStatus = tempStatus;
if (tempStatus && (tempStatus.discomfort || 0) > 0) {
raw.health += Math.max(0, (tempStatus.discomfort || 0) - 2) * 6.4;
raw.safety += Math.max(0, (tempStatus.stressExcess || 0)) * 5.6;
if ((tempStatus.harmExcess || 0) > 0) raw.health += (tempStatus.harmExcess || 0) * 7.2;
}
}
if ((tarinai.fearTimer || 0) > 0.12 || (tarinai.lastDamageAt || -999) + 5 > (world?.time || 0)) raw.safety += 34;
const socialParts = { bond: 0, family: 0, mate: 0, conflict: 0, fearSocial: 0 };
socialParts.bond = (tarinai.loneliness || 0) * (0.88 + Math.max(0, sociability) * 0.16);
@ -85,10 +97,10 @@ function updateNeeds(tarinai, world, dt = 0) {
const enemy = tarinai.strongestRelation?.("fear");
if (enemy?.score > 12) { socialParts.fearSocial += 10; raw.social += 10; }
const conflict = evaluateConflictUrge(tarinai, world, 112);
if (conflict?.target && (conflict.forced || conflict.score >= 18)) {
if (conflict?.target && (conflict.forced || conflict.score >= 14)) {
tarinai.conflictTargetId = conflict.target.id;
tarinai.conflictUrge = Math.max(Number(tarinai.conflictUrge || 0) || 0, conflict.score);
const conflictPart = conflict.score * 0.36;
const conflictPart = conflict.score * 0.56;
socialParts.conflict += conflictPart;
raw.social += conflictPart;
if (conflict.defensive) raw.safety += conflict.score * 0.24;
@ -227,7 +239,7 @@ function actionSelectionBonus(action, need, tarinai, options = {}) {
const sociability = Number(cur.sociability ?? prof.social ?? 0) || 0;
const openness = Number(cur.openness ?? prof.play ?? 0) || 0;
if (action.subNeed === "mate") bonus += Number(p.mate || 0) * 2.2 + Math.max(0, openness) * 22;
else if (action.subNeed === "conflict") bonus += Number(p.conflict || 0) * 0.78 + Math.max(0, aggression) * 10;
else if (action.subNeed === "conflict") bonus += Number(p.conflict || 0) * 1.10 + Math.max(0, aggression) * 14;
else if (action.subNeed === "family") bonus += Number(p.family || 0) * 1.6 + Math.max(0, sociability) * 10;
else if (action.subNeed === "bond") bonus += Number(p.bond || 0) * 1.15 + Math.max(0, sociability) * 24;
} else if (need === "fulfill") {
@ -243,12 +255,18 @@ function actionSelectionBonus(action, need, tarinai, options = {}) {
}
// regression guard baseline: return ({ conflict: 34, mate: 24, family: 12, bond: 14
// regression guard baseline: subValue >= threshold && subValue >= socialPull * 0.82
function socialSubNeedStartThreshold(subNeed = "") {
return ({ conflict: 34, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16;
return ({ conflict: 26, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16;
}
function shouldStartActionBySubNeed(tarinai, action, needs) {
const need = action?.need || "fulfill";
if (action?.id === "seek_comfort_temperature") {
const status = tarinai?.temperatureStatus || tarinai?.world?.temperatureStatusFor?.(tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15));
return Boolean(status && (status.discomfort || 0) >= 2.5);
}
if (need !== "social" || !action?.subNeed) return shouldStartNeedAction(tarinai, need, needs);
const parts = tarinai?.socialReasonParts || {};
const subNeed = action.subNeed;
@ -268,7 +286,7 @@ function shouldStartActionBySubNeed(tarinai, action, needs) {
if (subNeed === "conflict") {
if ((tarinai?.fightMochiTimer || 0) > 0.04) return total >= needThreshold("social", "start") && subValue >= threshold * 0.60;
const socialPull = Math.max(Number(parts.bond || 0), Number(parts.mate || 0), Number(parts.family || 0));
return total >= needThreshold("social", "start") && subValue >= threshold && subValue >= socialPull * 0.82;
return total >= needThreshold("social", "start") && subValue >= threshold * 0.82 && subValue >= socialPull * 0.68;
}
return total >= needThreshold("social", "start") && subValue >= threshold * 0.36;
}
@ -310,6 +328,11 @@ function choosePriorityNeedAction(needs, tarinai, world) {
const bonus = actionSelectionBonus(action, need, tarinai);
let priority = actionPriority(action);
const needValue = Number(needs?.[need] || 0) || 0;
if (action.id === "seek_comfort_temperature") {
const felt = tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
const status = tarinai?.world?.temperatureStatusFor?.(felt) || tarinai?.temperatureStatus || null;
priority = (status?.harmful || (status?.harmExcess || 0) > 0) ? 92 : 53;
}
if (action.id === "eat_food") {
const hunger = Number(tarinai?.hunger || 0) || 0;
if (hunger >= 98 || needValue >= 88) priority = Math.max(priority, 120);
@ -346,7 +369,7 @@ function choosePriorityNeedAction(needs, tarinai, world) {
}
const need = action.need || "fulfill";
const conflictNeeds = conflictNeedsForChoice(action, weighted, needs);
return { action, choice: { need, tiedNeeds: [need, ...conflictNeeds], max: Number(needs?.[need] || 0) || 0, priority: actionPriority(action) } };
return { action, choice: { need, tiedNeeds: [need, ...conflictNeeds], max: Number(needs?.[need] || 0) || 0, priority: selectedEntry.priority } };
}
function shouldStartNeedAction(tarinai, need, needs) {
@ -697,7 +720,7 @@ function resolveNeedsCore(dt) {
const action = picked.action;
const choice = picked.choice;
const reasonText = buildReasonText(choice.need, choice.tiedNeeds, action, this, this.world);
startNeedAction(this, this.world, choice, action, reasonText, { priority: actionPriority(action) });
startNeedAction(this, this.world, choice, action, reasonText, { priority: choice.priority ?? actionPriority(action) });
return;
}
@ -769,10 +792,13 @@ function resolveNeedsCore(dt) {
if (this.dead) return this.variantSprite("fear");
if (this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04) return this.sunbathSpriteId ? this.sunbathSpriteId() : "sunbath_1";
if (this.sleepDisease) return this.variantSprite("sleep");
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
const low = this.lowHealthSprite();
if (low) return low;
if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan";
if (this.fightDisease) return "stress_dizzy";
if ((this.oshiriByoZunchiStock || 0) >= 6) return this.variantSprite ? this.variantSprite("hurt") : "weak";
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
const foodNeedForSprite = Number(this.needRaw?.food ?? this.needs?.food ?? this.hunger) || 0;
const socialNeedForSprite = Number(this.needRaw?.social ?? this.needs?.social ?? 0) || 0;
const fulfillNeedForSprite = Number(this.needRaw?.fulfill ?? this.needs?.fulfill ?? 0) || 0;
@ -781,25 +807,23 @@ function resolveNeedsCore(dt) {
if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat";
if (this.hurtTimer > 0.08 || this.pokeFlashTimer > 0.08) return this.variantSprite("hurt");
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual";
const low = this.lowHealthSprite();
if (low === "stretch") return low;
if (foodNeedForSprite >= 72 && !low) return "hungry_70";
if (foodNeedForSprite >= 72) 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");
if (this.state === "fight" || this.state === "seek_enemy" || this.fightTimer > 0.04) return "angry";
if (this.stress > 62) return this.variantSprite("stress");
if (low) return low;
if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep");
if (this.surpriseTimer > 0.08) return "pokan";
if (this.state === "cursor_friend") return "smile";
if (this.state === "cursor_friend") return this.applyTemperatureSprite?.("smile") || "smile";
if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode;
if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
if (this.state === "seek_food") return "drool";
if (this.stress < 24 && foodNeedForSprite < 60 && socialNeedForSprite < 60 && fulfillNeedForSprite < 60) return this.variantSprite("low_stress");
if (this.stress < 44 && foodNeedForSprite < 60) return this.goodMode;
const seekTempSprite = (this.state === "seek_temperature") ? (this.temperatureDiscomfortSprite?.(this.goodMode || this.variantSprite("normal")) || "") : "";
if (seekTempSprite) return seekTempSprite;
if (this.stress < 24 && foodNeedForSprite < 60 && socialNeedForSprite < 60 && fulfillNeedForSprite < 60) return this.applyTemperatureSprite?.(this.variantSprite("low_stress")) || this.variantSprite("low_stress");
if (this.stress < 44 && foodNeedForSprite < 60) return this.applyTemperatureSprite?.(this.goodMode) || this.goodMode;
if (foodNeedForSprite > 78 || socialNeedForSprite > 84) return "angry";
return this.goodMode || this.variantSprite("normal");
return this.applyTemperatureSprite?.(this.goodMode || this.variantSprite("normal")) || this.goodMode || this.variantSprite("normal");
},
spriteId() {

View file

@ -5,7 +5,6 @@
// selection policy so nest-box entrance logic and bed targeting do not drift.
(function (global) {
const distFn = typeof dist === "function" ? dist : ((a, b) => Math.hypot((a?.x || 0) - (b?.x || 0), (a?.y || 0) - (b?.y || 0)));
const distXYFn = typeof distXY === "function" ? distXY : ((ax, ay, bx, by) => Math.hypot((ax || 0) - (bx || 0), (ay || 0) - (by || 0)));
const clampFn = typeof clamp === "function" ? clamp : ((v, a, b) => Math.max(a, Math.min(b, v)));
function isSleepFurniture(item) {
@ -75,7 +74,10 @@
function sleepPlaceAvailableFor(worldRef, tarinai, item) {
if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false;
if (Number(item.unreachableUntil || 0) > Number(worldRef.time || 0)) return false;
if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef.time || 0)) return false;
if (tarinai.shouldAvoidTarget?.(item)) return false;
if (worldRef?.sleepTargetTemperatureBlocked?.(tarinai, item)) return false;
if (sleepTargetBlocked(worldRef, tarinai, item)) return false;
const capacity = item.type === "nest_box" ? nestBoxCapacity(worldRef, item) : 1;
const occupancy = bedOccupancy(worldRef, item);
@ -86,6 +88,7 @@
let best = null, bestD = maxDist;
for (const item of worldRef?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId !== tarinai?.id) continue;
if (Number(item.unreachableUntil || 0) > Number(worldRef?.time || 0)) continue;
if (type && item.type !== type) continue;
if (sleepTargetBlocked(worldRef, tarinai, item)) continue;
const d = distFn(tarinai, item);
@ -98,6 +101,8 @@
let best = null, bestD = maxDist;
for (const item of worldRef?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId) continue;
if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef?.time || 0)) continue;
if (Number(item.unreachableUntil || 0) > Number(worldRef?.time || 0)) continue;
if (type && item.type !== type) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue;
if (sleepTargetBlocked(worldRef, tarinai, item)) continue;

View file

@ -186,6 +186,11 @@
const intimidateShake = this.intimidateTimer > 0.04 ? Math.sin(t * 44 + this.age) * this.radius * 0.09 : 0;
const fearShakeY = this.intimidatedTimer > 0.04 ? Math.sin(t * 52 + this.age * 0.7) * this.radius * 0.11 : 0;
const birthShake = this.birthRitualTimer > 0.04 ? Math.sin(t * 34 + this.age) * this.radius * 0.07 : 0;
const coldStatus = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null;
const coldShakeActive = Boolean(coldStatus && coldStatus.direction === "cold" && (coldStatus.discomfort || 0) > 0.5 && this.state !== "sunbath");
const coldShakeStrength = coldShakeActive ? clamp((coldStatus.discomfort || 0) / 12, 0.25, 1.25) : 0;
const coldShakeX = coldShakeActive ? (Math.sin(t * 46 + this.age * 0.9) + Math.sin(t * 71 + this.age * 0.31) * 0.55) * this.radius * 0.035 * coldShakeStrength : 0;
const coldShakeY = coldShakeActive ? Math.sin(t * 58 + this.age * 0.47) * this.radius * 0.012 * coldShakeStrength : 0;
const bob = breathing * (this.state === "sleep" ? 0.9 : 1.2 + gait * 1.6) + headbuttPulse * 0.9 + fallLift + birthShake * 0.25;
const blastSpin = this.blastSpinTimer > 0.04 ? this.fallDir * ((this.blastSpinMax || 1) - this.blastSpinTimer) * Math.PI * 70.0 : 0;
const fallSpin = this.blastSpinTimer > 0.04 ? blastSpin : (fallPose > 0 ? this.fallDir * (1 - fallPose) * Math.PI * 2 : 0);
@ -279,7 +284,7 @@
}
ctx.save();
ctx.translate(this.x + intimidateShake, this.y + bob + fearShakeY);
ctx.translate(this.x + intimidateShake + coldShakeX, this.y + bob + fearShakeY + coldShakeY);
const observeKind = this.world.observeHighlightKind ? this.world.observeHighlightKind(this) : "";
if (observeKind && this.world.selected !== this) {
const style = {
@ -323,6 +328,29 @@
ctx.restore();
}
if (showDecor && coldShakeActive) {
ctx.save();
ctx.globalAlpha = 0.22 * nestHideAlpha * clamp(coldShakeStrength, 0.25, 1.15);
ctx.strokeStyle = "rgba(108, 166, 220, 0.86)";
ctx.lineWidth = Math.max(1.0, this.radius * 0.045);
ctx.lineCap = "round";
const side = Math.max(10, drawW * 0.36);
const top = -drawH * 0.28;
const amp = Math.max(2, this.radius * 0.08);
for (const dir of [-1, 1]) {
for (let i = 0; i < 2; i++) {
const ox = dir * (side + i * drawW * 0.055);
const oy = top + i * drawH * 0.22 + Math.sin(t * 24 + i + this.age) * amp * 0.35;
ctx.beginPath();
ctx.moveTo(ox, oy - amp);
ctx.lineTo(ox + dir * amp * 0.55, oy);
ctx.lineTo(ox, oy + amp);
ctx.stroke();
}
}
ctx.restore();
}
if (this.world.selected === this) {
ctx.save();
const pulse = 0.5 + 0.5 * Math.sin(this.world.time * 7.5);
@ -420,8 +448,6 @@
if (faceRight) ctx.scale(-1, 1);
const dw = drawW * squish;
const dh = drawH / squish;
const brightness = clamp(0.82 + lightState.light * 0.30 + lightState.dawnStrength * 0.08 + lightState.goldenStrength * 0.06 + lightState.noonStrength * 0.04, 0.80, 1.17);
const saturation = clamp(0.86 + lightState.light * 0.22 + lightState.goldenStrength * 0.10 - lightState.nightStrength * 0.04, 0.82, 1.14);
const renderScale = SPRITE_CACHE_SCALE;
const cacheW = Math.max(1, Math.round((dw * renderScale) / 8) * 8);
const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8);

View file

@ -175,7 +175,8 @@
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false;
const fightStartChance = dt * (battleDrug ? 0.23 : 0.085) * personalityRisk * fearBrake * friendBrake * conflictBias;
// regression guard baseline: dt * (battleDrug ? 0.23 : 0.085)
const fightStartChance = dt * (battleDrug ? 0.28 : 0.16) * personalityRisk * fearBrake * friendBrake * conflictBias;
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && deterministicChance(this.world, "social-fight-start", fightStartChance, this, o)) {
this.conflictTargetId = o.id;
o.conflictTargetId = this.id;
@ -371,11 +372,36 @@
if (this.target && !this.target.dead) {
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "ant_attack"]);
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "ant_attack", "seek_temperature"]);
const routeTargetItem = this.target?.hostItem || this.target;
const pathPadding = Math.max(18, (this.radius || 20) * 0.82);
const targetFoodType = this.target?.type === "duplicator" ? this.target?.storedFoodType : this.target?.type;
const zundaFoodTarget = this.state === "seek_food" && (targetFoodType === "sweet" || targetFoodType === "zunda_juice");
const waypoint = routeStates.has(this.state) && this.world?.findTarinaiPathWaypoint
? this.world.findTarinaiPathWaypoint(this, baseMoveTarget, { targetItem: routeTargetItem, padding: Math.max(18, (this.radius || 20) * 0.82), state: this.state })
? this.world.findTarinaiPathWaypoint(this, baseMoveTarget, { targetItem: routeTargetItem, padding: pathPadding, state: this.state, preferFullPath: zundaFoodTarget })
: null;
if (this.state === "seek_bed" && this.target?.type === "grass_bed") {
const bedId = this.target.id || "";
if (this._bedUnreachableTargetId !== bedId) {
this._bedUnreachableTargetId = bedId;
this._bedUnreachableTimer = 0;
this._bedLastDistance = Infinity;
}
const bedDistance = distXY(this.x, this.y, baseMoveTarget.x, baseMoveTarget.y);
const directBlocked = this.world?.pathBlockedByFence?.(this.x, this.y, baseMoveTarget.x, baseMoveTarget.y, pathPadding, { exclude: routeTargetItem });
const notImproving = Number.isFinite(this._bedLastDistance) && bedDistance > this._bedLastDistance - 1.2;
const slow = Math.hypot(this.vx || 0, this.vy || 0) < 7;
if ((directBlocked && !waypoint) || (directBlocked && notImproving && slow)) this._bedUnreachableTimer = (this._bedUnreachableTimer || 0) + dt;
else this._bedUnreachableTimer = Math.max(0, (this._bedUnreachableTimer || 0) - dt * 0.55);
this._bedLastDistance = bedDistance;
if ((this._bedUnreachableTimer || 0) > 4.2) {
this.abandonUnreachableGrassBed?.(this.target);
return;
}
} else {
this._bedUnreachableTimer = 0;
this._bedUnreachableTargetId = "";
}
const moveTarget = waypoint || baseMoveTarget;
const dx = moveTarget.x - this.x;
const dy = moveTarget.y - this.y;
@ -507,10 +533,42 @@
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-bottom", -2.2, -0.7, this); this.surpriseTimer = 0.15; }
},
temperatureDeathReason() {
const status = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null;
const recent = (this.world?.time || 0) - Number(this.lastTemperatureDamageAt || -999) <= 36;
if (recent && status && (status.harmExcess || 0) > 0) return status.direction === "cold" ? "凍結" : "熱中症";
return "";
},
abandonUnreachableGrassBed(bed, reason = "辿りつけないので放棄した") {
if (!bed || bed.dead || bed.type !== "grass_bed") return false;
bed.ownerId = "";
bed.unreachableUntil = 0;
bed.abandonedById = this.id || "";
bed.abandonedUntil = (this.world?.time || 0) + 90;
bed.abandonedReason = reason;
bed.carriedById = "";
this.target = null;
this.targetKey = "";
this._bedUnreachableTimer = 0;
this._bedUnreachableTargetId = "";
this.actionIdCooldowns = this.actionIdCooldowns || {};
this.actionIdCooldowns.sleep_in_bed = Math.max(Number(this.actionIdCooldowns.sleep_in_bed || 0) || 0, 6.0);
this.actionCooldowns = this.actionCooldowns || {};
this.actionCooldowns.sleep = Math.max(Number(this.actionCooldowns.sleep || 0) || 0, 3.0);
if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(this, { reason });
this.goIdle?.(reason);
this.world?.markItemBucketsDirty?.("grass-bed-abandoned-unreachable");
this.world?.markSpatialDirty?.("grass-bed-abandoned-unreachable");
return true;
},
checkLife(dt = 0.016) {
let reason = "";
if (this.energy <= 0) {
reason = this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
reason = this.temperatureDeathReason?.() || this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
}
else if (this.hunger >= 108) reason = "\u98e2\u9913";
else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";

View file

@ -121,13 +121,16 @@
t.tempTimer += dt;
if (t.tempTimer >= 0.55) {
t.tempComfort = t.world.temperatureAt(t.x, t.y);
const felt = t.world?.temperatureAt?.(t.x, t.y) ?? (CONFIG.standardTemperature ?? 15);
t.tempComfort = felt;
t.feltTemperature = felt;
t.temperatureStatus = t.world?.temperatureStatusFor?.(felt) || null;
t.tempTimer = 0;
}
const tempComfort = t.tempComfort ?? 0.62;
const tooCold = Math.max(0, 0.48 - tempComfort);
const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || { discomfort: 0, stressExcess: 0, harmExcess: 0, direction: "comfort" };
const coldDiscomfort = tempStatus.direction === "cold" ? Math.max(0, tempStatus.discomfort || 0) : 0;
const hungerScale = t.metabolicHungerMultiplier ? t.metabolicHungerMultiplier() : (typeof tarinaiMetabolicHungerScale === "function" ? tarinaiMetabolicHungerScale(t) : 1);
t.hunger += CONFIG.hungerPerMinute * minute * t.trait.hunger * hungerScale * (1 + tooCold * 0.8);
t.hunger += CONFIG.hungerPerMinute * minute * t.trait.hunger * hungerScale * (1 + coldDiscomfort * 0.018);
t.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - t.trait.social);
t.energy -= CONFIG.energyPerMinute * minute * t.trait.sleep * (t.state === "sleep" ? 0.025 : 1);
if (t.state === "sleep" || t.sleeping) {
@ -137,7 +140,21 @@
if (typeof applyNeedRelief === "function") applyNeedRelief(t, { sleep: -dt * (sleepBedLike ? 2.0 : 1.1) });
}
// stress is derived from needs; no independent stress decay.
if (tooCold > 0) applyNeedShock(t, { health: tooCold * dt * 4, safety: tooCold * dt * 2 });
if ((tempStatus.stressExcess || 0) > 0) {
const excess = tempStatus.stressExcess || 0;
const stressRate = excess * (0.66 + Math.min(20, excess) * 0.035);
applyNeedShock(t, { safety: stressRate * dt, health: stressRate * dt * 0.42 });
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0) && detChance(t.world, "temperature-stress-bubble", dt * 0.18, t)) {
t.bubble(tempStatus.direction === "cold" ? "ぶるぶる" : "あちゅい", 2.4, "rgba(76,62,72,0.78)");
}
}
if ((tempStatus.harmExcess || 0) > 0) {
const harm = tempStatus.harmExcess || 0;
t.energy -= dt * (0.10 + harm * 0.075 + harm * harm * 0.006);
t.hurtTimer = Math.max(t.hurtTimer || 0, Math.min(1.2, 0.28 + harm * 0.035));
t.lastDamageCause = tempStatus.direction === "cold" ? "凍結" : "熱中症";
t.lastTemperatureDamageAt = t.world?.time || 0;
}
const phase = t.world.lightLevel();
if (phase < 0.25) t.energy += dt * 0.07;

View file

@ -69,6 +69,7 @@ TEXT_CATALOG.stateLabels = Object.freeze({
seek_water: "\u6c34\u3092\u63a2\u3059",
seek_material: "\u6750\u6599\u3092\u63a2\u3059",
seek_enemy: "\u76f8\u624b\u3078\u5411\u304b\u3046",
seek_temperature: "\u907f\u6691\u30fb\u907f\u5bd2\u4e2d",
wander: "\u79fb\u52d5\u4e2d",
build: "\u4f5c\u6210\u4e2d",
zunchi_sick: "\u305a\u3093\u3061\u75c5",
@ -103,6 +104,7 @@ TEXT_CATALOG.behaviorLabels = Object.freeze({
sleep_anywhere: "\u4f11\u3093\u3067\u3044\u308b",
intimidate_enemy: "\u5a01\u5687\u3057\u3066\u3044\u308b",
panic_escape: "\u9003\u3052\u3066\u3044\u308b",
seek_comfort_temperature: "\u5feb\u9069\u306a\u6e29\u5ea6\u5e2f\u3078\u79fb\u52d5\u3057\u3066\u3044\u308b",
});
TEXT_CATALOG.behaviorLabel = function behaviorLabelFromCatalog(value = "") {
@ -137,6 +139,14 @@ TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s, t = null) {
return TEXT_CATALOG.stateLabels?.[s] || s || "";
};
TEXT_CATALOG.temperatureEndureText = function temperatureEndureText(t = null) {
if (!t || !t.world) return "";
const status = t.temperatureStatus || t.world.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? t.world.temperatureAt?.(t.x, t.y) ?? (CONFIG.standardTemperature ?? 15));
if (!status || status.comfortable || (status.discomfort || 0) <= 0.5) return "";
if (t.world.hasReachableComfortTemperatureSpot?.(t)) return "";
return status.direction === "cold" ? "寒さに耐えている。" : "暑さに耐えている。";
};
TEXT_CATALOG.targetLabel = function targetLabelFromCatalog(target) {
if (!target) return "\u306a\u3057";
if (target.name) return target.name;
@ -158,6 +168,7 @@ TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
const behaviorState = currentTarinaiBehavior(t);
const behaviorLabel = behaviorText(t);
const cleanedBehaviorLabel = TEXT_CATALOG.cleanBehaviorText(behaviorLabel, behaviorState?.actionId || t.state || "");
if (["\u5f85\u6a5f", "\u5f85\u3063\u3066\u3044\u308b", "\u5f85\u3063\u3066\u3044\u308b\u3002", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b\u3002", "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b"].includes(cleanedBehaviorLabel)) return TEXT_CATALOG.temperatureEndureText(t) || TEXT_CATALOG.idleGroundText(t);
if (cleanedBehaviorLabel) return cleanedBehaviorLabel;
const target = TEXT_CATALOG.targetLabel(t.target);
if (t.state === "eat") return target !== "\u306a\u3057" ? `${target}\u3092\u98df\u3079\u3066\u3044\u308b` : "\u98df\u3079\u3066\u3044\u308b";
@ -186,9 +197,9 @@ TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
if ((t.grassEatTimer || 0) > 0.04) return "\u8349\u3092\u98df\u3079\u3066\u3044\u308b";
const thought = String(t.thought || "").trim();
const cleanedThought = TEXT_CATALOG.cleanBehaviorText(thought, t.state || "");
if (["\u5f85\u6a5f", "\u5f85\u3063\u3066\u3044\u308b", "\u5f85\u3063\u3066\u3044\u308b\u3002", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b\u3002", "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b"].includes(cleanedThought)) return TEXT_CATALOG.idleGroundText(t);
if (["\u5f85\u6a5f", "\u5f85\u3063\u3066\u3044\u308b", "\u5f85\u3063\u3066\u3044\u308b\u3002", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b", "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b\u3002", "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b"].includes(cleanedThought)) return TEXT_CATALOG.temperatureEndureText(t) || TEXT_CATALOG.idleGroundText(t);
if (thought) return thought;
if (t.state === "idle" || !t.state) return TEXT_CATALOG.idleGroundText(t);
if (t.state === "idle" || !t.state) return TEXT_CATALOG.temperatureEndureText(t) || TEXT_CATALOG.idleGroundText(t);
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
};

View file

@ -35,6 +35,9 @@ const ui = {
statPop: document.getElementById("statPop"),
statDead: document.getElementById("statDead"),
statColonyMood: document.getElementById("statColonyMood"),
temperatureValue: document.getElementById("temperatureValue"),
temperatureSlider: document.getElementById("temperatureSlider"),
temperatureAutoToggle: document.getElementById("temperatureAutoToggle"),
statGroundType: document.getElementById("statGroundType"),
groundTypeBtn: document.getElementById("groundTypeBtn"),
colonyChart: document.getElementById("colonyChart"),
@ -47,6 +50,7 @@ const ui = {
archiveResetConfirmBtn: document.getElementById("archiveResetConfirmBtn"),
fieldDialog: document.getElementById("fieldDialog"),
fieldCancelBtn: document.getElementById("fieldCancelBtn"),
fieldConfirmBtn: document.getElementById("fieldConfirmBtn"),
ecologyDialog: document.getElementById("ecologyDialog"),
ecologyGrid: document.getElementById("ecologyGrid"),
ecologyCloseBtn: document.getElementById("ecologyCloseBtn"),
@ -61,6 +65,8 @@ const uiCache = {
logPushEnabled: false,
logPushKinds: new Set(["death", "accident", "birth"]),
archiveUpdateEnabled: false,
resetFieldType: "garden",
resetPreset: "default",
toolButtons: [],
archiveVersion: "",
archiveFamilyVersion: null,

View file

@ -121,6 +121,22 @@ function bindUI() {
ui.panelQuickTabs?.addEventListener("pointerup", activatePanelQuickTab, { passive: false });
ui.panelQuickTabs?.addEventListener("touchend", activatePanelQuickTab, { passive: false });
const applyTemperatureSlider = () => {
if (!world || !ui.temperatureSlider) return;
world.setManualTemperature?.(Number(ui.temperatureSlider.value));
world.setTemperatureAuto?.(false);
renderStats?.();
};
ui.temperatureAutoToggle?.addEventListener("change", () => {
if (!world) return;
world.setTemperatureAuto?.(ui.temperatureAutoToggle.checked);
if (ui.temperatureSlider && world.temperatureAuto === false) world.setManualTemperature?.(Number(ui.temperatureSlider.value));
audio.uiClick?.();
renderStats?.();
});
ui.temperatureSlider?.addEventListener("input", applyTemperatureSlider);
ui.temperatureSlider?.addEventListener("change", applyTemperatureSlider);
ui.panel?.addEventListener("scroll", () => {
const panel = ui.panel;
if (!panel || !ui.panelQuickTabs) return;
@ -445,11 +461,23 @@ function bindUI() {
openFieldDialog();
});
ui.fieldCancelBtn?.addEventListener("click", closeFieldDialog);
ui.fieldConfirmBtn?.addEventListener("click", () => {
audio.uiClick?.();
resetWithField(uiCache.resetFieldType || "garden", uiCache.resetPreset || "default");
});
ui.fieldDialog?.addEventListener("click", (e) => {
if (e.target === ui.fieldDialog) { closeFieldDialog(); return; }
const btn = e.target.closest("[data-field-type]");
if (!btn) return;
resetWithField(btn.dataset.fieldType || "garden");
const fieldBtn = e.target.closest("[data-field-type]");
if (fieldBtn) {
audio.uiClick?.();
selectResetFieldType(fieldBtn.dataset.fieldType || "garden");
return;
}
const presetBtn = e.target.closest("[data-reset-preset]");
if (presetBtn) {
audio.uiClick?.();
selectResetPreset(presetBtn.dataset.resetPreset || "default");
}
});
ui.ecologyCloseBtn?.addEventListener("click", closeEcologyDialog);
ui.ecologyDialog?.addEventListener("click", (e) => {

View file

@ -30,6 +30,15 @@ function renderStats() {
setTextIfChanged(ui.statDead, "dead", values.dead);
const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" };
setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a");
const temp = world.updateTemperature?.(0) ?? world.currentTemperature ?? (CONFIG.standardTemperature ?? 15);
const tempLabel = `${Math.round(temp)}`;
setTextIfChanged(ui.temperatureValue, "temperature", tempLabel);
if (ui.temperatureSlider) {
ui.temperatureSlider.disabled = world.temperatureAuto !== false;
if (document.activeElement !== ui.temperatureSlider || world.temperatureAuto !== false) ui.temperatureSlider.value = String(Math.round(temp));
ui.temperatureSlider.setAttribute("aria-valuetext", tempLabel);
}
if (ui.temperatureAutoToggle) ui.temperatureAutoToggle.checked = world.temperatureAuto !== false;
bindColonyMoodTooltip();
window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood);
window.TarinaiGroundUI.update(world);

View file

@ -388,7 +388,7 @@ function lineageLayoutRows(rows, idSet) {
const SIBLING_GAP = 24;
const GROUP_GAP = 42;
const PARTNER_GROUP_GAP = 18;
const ROW_GAP = 96;
const ROW_GAP = 74;
const LEFT_PAD = 82;
const TOP_PAD = 44;
const RIGHT_PAD = 48;

View file

@ -55,7 +55,7 @@ function lineageLayoutRowsFast(rows, idSet) {
const NODE_W = 150;
const NODE_H = 104;
const SIBLING_GAP = 18;
const ROW_GAP = 84;
const ROW_GAP = 68;
const LEFT_PAD = 82;
const TOP_PAD = 44;
const RIGHT_PAD = 48;

View file

@ -32,7 +32,43 @@ function applyFieldLayout(fieldType = world.fieldType || "garden") {
resizeCanvas();
}
const RESET_PRESET_IDS = new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy"]);
function normalizeResetPresetId(presetId = "default") {
return RESET_PRESET_IDS.has(String(presetId || "")) ? String(presetId) : "default";
}
function syncFieldDialogChoices() {
const fieldType = FIELD_TYPES?.[uiCache.resetFieldType]?.id || world?.fieldType || "garden";
const presetId = normalizeResetPresetId(uiCache.resetPreset || "default");
uiCache.resetFieldType = fieldType;
uiCache.resetPreset = presetId;
ui.fieldDialog?.querySelectorAll("[data-field-type]").forEach((btn) => {
const active = btn.dataset.fieldType === fieldType;
btn.classList.toggle("active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
ui.fieldDialog?.querySelectorAll("[data-reset-preset]").forEach((btn) => {
const active = btn.dataset.resetPreset === presetId;
btn.classList.toggle("active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function selectResetFieldType(fieldType = "garden") {
uiCache.resetFieldType = FIELD_TYPES?.[fieldType]?.id || "garden";
syncFieldDialogChoices();
}
function selectResetPreset(presetId = "default") {
uiCache.resetPreset = normalizeResetPresetId(presetId);
syncFieldDialogChoices();
}
function openFieldDialog() {
uiCache.resetFieldType = FIELD_TYPES?.[world?.fieldType]?.id || uiCache.resetFieldType || "garden";
uiCache.resetPreset = normalizeResetPresetId(uiCache.resetPreset || "default");
syncFieldDialogChoices();
ui.fieldDialog?.classList.remove("hidden");
}
@ -76,11 +112,14 @@ function closeArchiveResetDialog() {
ui.archiveResetDialog?.classList.add("hidden");
}
function resetWithField(fieldType) {
function resetWithField(fieldType, presetId = uiCache.resetPreset || "default") {
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
const preset = normalizeResetPresetId(presetId);
uiCache.resetFieldType = field.id;
uiCache.resetPreset = preset;
closeFieldDialog();
applyFieldLayout(field.id);
world.reset(null, field.id);
world.reset(null, field.id, preset);
uiCache.archiveVersion = "";
renderLog(world.logs);
renderArchive();

View file

@ -270,6 +270,7 @@ function renderSelected() {
`<div class="need-grid">${needRowsHtml}</div>`,
selectedInfoRowHtml("\u4f53\u529b", `${fmt(t.energy)} / ${fmt(typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100))}`, "energy"),
selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"),
selectedInfoRowHtml("体感気温", `${(world.temperatureAt?.(t.x, t.y) ?? t.feltTemperature ?? CONFIG.standardTemperature ?? 15).toFixed(1)}`, "temperature"),
selectedInfoRowHtml("\u6e80\u8179\u5ea6", fmt(Math.max(0, Math.min(100, 100 - (Number(t.hunger) || 0)))), "fullness"),
].join("");
const itemEffectHtml = itemEffectSummary.length
@ -325,7 +326,7 @@ function reasonLabel(t) {
const behavior = behaviorText(t);
if (behavior) {
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(behavior, t.state || "") || behavior;
if (isIdleDisplayText(normalized)) return groundAwareIdleText(t);
if (isIdleDisplayText(normalized)) return window.TEXT_CATALOG?.temperatureEndureText?.(t) || groundAwareIdleText(t);
return normalized;
}
const state = String(t.state || "");
@ -336,11 +337,11 @@ function reasonLabel(t) {
if (behaviorState?.reason || behaviorState?.text) {
const raw = behaviorState.reason || behaviorState.text;
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(raw, behaviorState?.actionId || t.state || "") || raw;
if (isIdleDisplayText(normalized)) return groundAwareIdleText(t);
if (isIdleDisplayText(normalized)) return window.TEXT_CATALOG?.temperatureEndureText?.(t) || groundAwareIdleText(t);
return normalized;
}
if (isIdleDisplayText(live)) return groundAwareIdleText(t);
if ((!live || live === "\u306a\u3057") && (String(t.state || "") === "idle" || !t.state)) return groundAwareIdleText(t);
if (isIdleDisplayText(live)) return window.TEXT_CATALOG?.temperatureEndureText?.(t) || groundAwareIdleText(t);
if ((!live || live === "\u306a\u3057") && (String(t.state || "") === "idle" || !t.state)) return window.TEXT_CATALOG?.temperatureEndureText?.(t) || groundAwareIdleText(t);
return live || "\u306a\u3057";
}

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "39.15.75";
const APP_BUILD = "tarinai-ai-needs-cleanup-v39-15-75";
const APP_VERSION = "39.15.84";
const APP_BUILD = "tarinai-cleanup-unused-cross-domain-v39-15-84";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -59,6 +59,13 @@ class World { constructor() {
this.shakeDuration = 0;
this.shakeStrength = 0;
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
const tempAnnualUnit = typeof stableUnit === "function" ? stableUnit(`${this.worldSeed}:temperature`, "annual-range") : Math.random();
this.temperatureAuto = true;
this.manualTemperature = 10;
this.currentTemperature = 10;
this.temperatureAnnualRange = 25 + tempAnnualUnit * 10;
this.temperatureDailyRange = 0;
this.temperatureLastUpdateAt = -Infinity;
this.spatial = new SpatialGrid(96);
this.spatialItemScratch = [];
this.spatialTarinaiScratch = [];

View file

@ -159,7 +159,6 @@
}
const blast = 430 + p * 960;
this.applyImpulse(ball, dx / d * blast + deterministicRange(this, "firecracker-ball-jitter-x", -80, 80, it, ball), dy / d * blast + deterministicRange(this, "firecracker-ball-jitter-y", -80, 80, it, ball));
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "firecracker-ball-spin", -24, 24, it, ball), -38, 38);
ball.lastPokedAt = this.time || 0;
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
@ -649,7 +648,6 @@
const impulse = 145 + ball.pokeCombo * 64 + Math.min(260, currentSpeed * 0.24);
ball.vx = (ball.vx || 0) + nx * impulse;
ball.vy = (ball.vy || 0) + ny * impulse;
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (5.6 + ball.pokeCombo * 1.15), -38, 38);
ball.amount = Math.max(ball.amount || 999, 999);
audio.ballHit?.();

View file

@ -21,7 +21,7 @@
if (!it) return Infinity;
const type = String(it.type || "");
let reach = Math.max(32, Number(it.r || it.radius || 32) || 32);
if (type === "fence" || type === "fence_v" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") reach = Math.max(120, reach * 3.9);
if (type === "fence" || type === "fence_v" || type === "glass_wall" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") reach = Math.max(120, reach * 3.9);
else if (type === "nest_box") reach = Math.max(92, reach * 1.9);
else reach = global.TarinaiMechanicalShapeSystem.reach(it) || Math.max(60, reach * 1.6);
return Math.max(0, distXY(x, y, Number(it.x) || 0, Number(it.y) || 0) - reach);
@ -232,15 +232,250 @@
return best;
},
temperatureNumber(value, fallback = CONFIG.standardTemperature ?? 15) {
const v = Number(value);
const fb = Number(fallback);
return Number.isFinite(v) ? v : (Number.isFinite(fb) ? fb : 15);
},
clampTemperature(value) {
// v10: 気温の上限/下限は撤廃。ドライアイス・ストーブの重ね掛けや手動値は制限しない。
return this.temperatureNumber(value);
},
climateTemperature(value) {
// 自然気候だけは仕様どおり -20℃〜50℃ の範囲に収める。
const v = this.temperatureNumber(value);
return clamp(v, CONFIG.climateTemperatureMin ?? -20, CONFIG.climateTemperatureMax ?? 50);
},
seasonIndex() {
return Math.floor((((Number(this.day) || 1) - 1) % 20) / 5);
},
seasonLabel() {
return ["春", "夏", "秋", "冬"][this.seasonIndex()] || "春";
},
seasonDay() {
return ((((Number(this.day) || 1) - 1) % 5) + 1);
},
seasonDayString() {
return `${this.seasonLabel()}${this.seasonDay()}`;
},
temperatureStableUnit(salt = "") {
const key = `${this.worldSeed || "world"}:temperature:${salt}`;
if (typeof stableUnit === "function") return stableUnit(key, "temperature");
let h = 2166136261;
for (let i = 0; i < key.length; i++) h = Math.imul(h ^ key.charCodeAt(i), 16777619);
return ((h >>> 0) % 1000000) / 1000000;
},
weatherDailyRangeFactor(weather = this.weather) {
if (weather === "sunny") return 0.68;
if (weather === "light_rain") return 0.24;
return 0.36;
},
dailyTemperatureRange(day = this.day, weather = this.weather) {
const raw = this.temperatureStableUnit(`daily-range:${day}`) * 15;
if (weather === "sunny") return clamp(6 + raw * 0.62, 0, 15);
if (weather === "light_rain") return clamp(raw * 0.32, 0, 5);
return clamp(raw * 0.45, 0, 7);
},
annualTemperatureOffset() {
const dayLength = Math.max(1, CONFIG.dayLength || 120);
const totalDays = Math.max(0, Number(this.time || 0) / dayLength);
const phase = ((totalDays % 20) + 20) % 20 / 20;
const range = Number.isFinite(Number(this.temperatureAnnualRange)) ? Number(this.temperatureAnnualRange) : 30;
const wave = Math.sin(phase * Math.PI * 2 - Math.PI * 0.25);
return wave * range * 0.5;
},
dailyTemperatureWave(progress = 0) {
const hour = ((((Number(progress) || 0) % 1) + 1) % 1) * 24;
const minHour = 5.0; // 朝方に最低気温
const maxHour = 14.0; // 昼過ぎに最高気温
const smooth = (x) => {
const t = clamp(x, 0, 1);
return t * t * (3 - 2 * t);
};
let warmth;
if (hour >= minHour && hour <= maxHour) {
warmth = smooth((hour - minHour) / Math.max(0.001, maxHour - minHour));
} else {
const downHour = hour > maxHour ? hour - maxHour : hour + 24 - maxHour;
const downSpan = minHour + 24 - maxHour;
warmth = 1 - smooth(downHour / Math.max(0.001, downSpan));
}
return warmth * 2 - 1;
},
naturalTemperature() {
// 初期ロード直後は 10℃ を保持し、初回フレームのdt揺れで0℃近くへ落ちないようにする。
if ((Number(this.time) || 0) <= 6.0 && (Number(this.day) || 1) === 1) {
this.temperatureDailyRange = 0;
return 10;
}
const dayLength = Math.max(1, CONFIG.dayLength || 120);
const progress = ((Number(this.time || 0) % dayLength) + dayLength) % dayLength / dayLength;
const dailyRange = this.dailyTemperatureRange(this.day, this.weather);
const dailyWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(progress) : Math.sin(progress * Math.PI * 2 - Math.PI * 0.5);
let temp;
if ((Number(this.day) || 1) === 1) {
// 春1日目は開始時10℃を基準にしつつ、日内の最低/最高時刻だけは維持する。
const initialWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(0) : Math.sin(-Math.PI * 0.5);
temp = 10 + (dailyWave - initialWave) * dailyRange * 0.5;
} else {
temp = (CONFIG.standardTemperature ?? 15) + this.annualTemperatureOffset() + dailyWave * dailyRange * 0.5;
}
this.temperatureDailyRange = dailyRange;
return this.climateTemperature(temp);
},
updateTemperature(dt = 0) {
if (!Number.isFinite(Number(this.temperatureAnnualRange))) {
this.temperatureAnnualRange = 25 + this.temperatureStableUnit("annual-range") * 10;
}
if (!Number.isFinite(Number(this.manualTemperature))) this.manualTemperature = CONFIG.standardTemperature ?? 15;
this.currentTemperature = this.temperatureAuto === false
? this.temperatureNumber(this.manualTemperature)
: this.naturalTemperature();
this.temperatureLastUpdateAt = this.time || 0;
return this.currentTemperature;
},
setTemperatureAuto(enabled = true) {
this.temperatureAuto = Boolean(enabled);
if (this.temperatureAuto === false) this.manualTemperature = this.temperatureNumber(this.currentTemperature ?? this.manualTemperature ?? CONFIG.standardTemperature ?? 15);
return this.updateTemperature?.(0) ?? this.currentTemperature;
},
setManualTemperature(value = 15) {
this.manualTemperature = this.temperatureNumber(value);
if (this.temperatureAuto === false) this.currentTemperature = this.manualTemperature;
return this.manualTemperature;
},
temperatureAt(x, y) {
let temp = 0.54 + Math.sin(this.time / 55) * 0.05;
for (const it of this.nearbyItems(x, y, 190)) {
if (it.type === "stone") {
const d = distXY(x, y, it.x, it.y);
temp += clamp(1 - d / 190, 0, 1) * 0.35;
let temp = Number.isFinite(Number(this.currentTemperature)) ? Number(this.currentTemperature) : this.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15);
if (this.temperatureAuto === false) temp = this.temperatureNumber(this.manualTemperature);
if ((this.groundType || "soil") === "ice") temp -= 5;
const range = Math.max(24, CONFIG.temperatureItemRange ?? 190);
for (const it of this.nearbyItems(x, y, range + 8)) {
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) continue;
const d = distXY(x, y, it.x, it.y);
if (d > range) continue;
const edge = d <= range - 24 ? 1 : clamp((range - d) / 24, 0, 1);
temp += (it.type === "stove" ? 10 : -10) * edge;
}
const nestWarmthLimit = 5;
for (const box of this.nearbyItems?.(x, y, 86) || []) {
if (!box || box.dead || box.type !== "nest_box") continue;
const r = Math.max(36, Number(box.r || 42) || 42);
const base = this.nestBoxBaseRect?.(box) || null;
const inBase = base && x >= base.left - 4 && x <= base.right + 4 && y >= base.top - 4 && y <= base.bottom + 4;
const inBody = distXY(x, y, box.x, box.y) <= r * 1.08;
if (!inBase && !inBody) continue;
const toward = 20 - temp;
temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit);
break;
}
return this.temperatureNumber(temp);
},
temperatureStatusFor(temp = CONFIG.standardTemperature ?? 15) {
const value = this.temperatureNumber(temp);
const min = CONFIG.temperatureComfortMin ?? 10;
const max = CONFIG.temperatureComfortMax ?? 25;
const discomfort = value < min ? min - value : (value > max ? value - max : 0);
const direction = value < min ? "cold" : (value > max ? "hot" : "comfort");
const stressExcess = Math.max(0, discomfort - (CONFIG.temperatureStressMargin ?? 5));
const harmExcess = Math.max(0, discomfort - (CONFIG.temperatureEnergyMargin ?? 10));
return {
temp: value,
direction,
discomfort,
comfortable: discomfort <= 0,
stressExcess,
harmExcess,
harmful: harmExcess > 0,
label: discomfort <= 0 ? "快適" : (direction === "cold" ? (harmExcess > 0 ? "危険な寒さ" : "寒い") : (harmExcess > 0 ? "危険な暑さ" : "暑い")),
};
},
findComfortTemperatureSpot(actor, opts = {}) {
if (!actor || actor.dead) return null;
const current = this.temperatureAt(actor.x, actor.y);
const currentStatus = this.temperatureStatusFor(current);
if (!opts.force && currentStatus.comfortable) return null;
const targetTemp = ((CONFIG.temperatureComfortMin ?? 10) + (CONFIG.temperatureComfortMax ?? 25)) * 0.5;
const radius = actor.radius || actor.r || 20;
const rings = [72, 128, 196, 288, 392];
const dirs = 18;
const candidates = [];
const pushCandidate = (x, y, extra = 0) => candidates.push({
x: clamp(x, radius + 8, (this.w || 1000) - radius - 8),
y: clamp(y, radius + 8, (this.h || 720) - radius - 8),
extra,
});
for (const ring of rings) {
for (let i = 0; i < dirs; i++) {
const a = (Math.PI * 2 * i / dirs) + ring * 0.017;
pushCandidate(actor.x + Math.cos(a) * ring, actor.y + Math.sin(a) * ring);
}
}
return clamp(temp, 0, 1);
const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190);
for (const it of this.items || []) {
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) continue;
if (currentStatus.direction === "cold" && it.type !== "stove") continue;
if (currentStatus.direction === "hot" && it.type !== "dry_ice") continue;
const toward = Math.atan2(actor.y - it.y, actor.x - it.x);
pushCandidate(it.x + Math.cos(toward) * itemRange * 0.45, it.y + Math.sin(toward) * itemRange * 0.45, -60);
pushCandidate(it.x + Math.cos(toward + 0.75) * itemRange * 0.62, it.y + Math.sin(toward + 0.75) * itemRange * 0.62, -32);
pushCandidate(it.x + Math.cos(toward - 0.75) * itemRange * 0.62, it.y + Math.sin(toward - 0.75) * itemRange * 0.62, -32);
}
let best = null;
let bestScore = Infinity;
for (const c of candidates) {
if (this.pointBlockedByObstacle?.(c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor })) continue;
const temp = this.temperatureAt(c.x, c.y);
const status = this.temperatureStatusFor(temp);
if (currentStatus.direction === "cold" && temp <= current + 0.6 && !status.comfortable) continue;
if (currentStatus.direction === "hot" && temp >= current - 0.6 && !status.comfortable) continue;
const d = distXY(actor.x, actor.y, c.x, c.y);
const blockedPenalty = this.pathBlockedByFence?.(actor.x, actor.y, c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor }) ? 480 : 0;
const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0);
if (score < bestScore) {
bestScore = score;
best = { x: c.x, y: c.y, dead: false, detour: true, temperatureTarget: true, temperature: temp, label: "快適な温度帯" };
}
}
if (!best) return null;
const bestStatus = this.temperatureStatusFor(best.temperature);
if (opts.requireComfort && !bestStatus.comfortable) return null;
if (!bestStatus.comfortable && bestStatus.discomfort >= currentStatus.discomfort - 0.65) return null;
best.temperatureStatus = bestStatus;
return best;
},
hasReachableComfortTemperatureSpot(actor) {
return !!this.findComfortTemperatureSpot?.(actor, { force: true, requireComfort: true });
},
sleepTargetTemperatureBlocked(actor, item) {
if (!actor || !item || item.dead) return false;
if (!(item.type === "grass_bed" || item.type === "bed" || item.type === "nest_box")) return false;
const current = this.temperatureAt?.(actor.x, actor.y);
const currentStatus = this.temperatureStatusFor?.(current);
if (!currentStatus || currentStatus.comfortable) return false;
if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false;
const temp = this.temperatureAt?.(item.x, item.y);
const status = this.temperatureStatusFor?.(temp);
return Boolean(status && !status.comfortable);
},
fenceRect(it) {
@ -248,6 +483,7 @@
if (!this.isFenceType(type)) return null;
const bounce = type === "bounce_fence" || type === "bounce_fence_v";
const gate = type === "gate_fence";
const glassWall = type === "glass_wall";
const len = Math.max(112, (it.r || 42) * 3.55);
const thick = Math.max(10, (it.r || 42) * 0.31);
const angle = angleForItemLocal(it);
@ -258,7 +494,7 @@
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)),
bounce, gate, gateOpen: Boolean(it.gateOpen),
bounce, gate, glassWall, gateOpen: Boolean(it.gateOpen),
restitution: bounce ? 1.08 : undefined, minBounceSpeed: bounce ? 260 : undefined
};
},
@ -835,6 +1071,13 @@
|| intersects(x1, y1, x2, y2, r.left, r.bottom, r.left, r.top);
},
isFenceRoutingRect(rect) {
const type = rect?.item?.type || rect?.type || "";
if (!type || type === "glass_wall") return false;
if (type === "gate_fence" && rect?.item?.gateOpen) return false;
return Boolean(this.isFenceType?.(type));
},
pathBlockedByFence(x1, y1, x2, y2, padding = 16, opts = {}) {
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
@ -842,6 +1085,7 @@
const exclude = opts?.exclude || null;
for (const rect of this.nearbySolidObstacleRects(cx, cy, maxD + padding + 170, { exclude })) {
if (exclude && rect?.item === exclude) continue;
if (!this.isFenceRoutingRect?.(rect)) continue;
if (rect?.oriented) {
const r = { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding };
if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true;
@ -934,7 +1178,7 @@
let blockers = 0;
for (const rect of rects) {
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor?.(actor, rect.item)) continue;
if (!this.isFenceRoutingRect?.(rect)) continue;
const inflated = rect?.oriented
? { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding }
: { left: (rect.left || 0) - padding, right: (rect.right || 0) + padding, top: (rect.top || 0) - padding, bottom: (rect.bottom || 0) + padding };
@ -958,13 +1202,92 @@
const progress = totalD - d2;
const cacheBias = old ? Math.min(90, Math.hypot(c.x - old.x, c.y - old.y) * 0.35) : 0;
const score = d1 + d2 + (secondBlocked ? 780 - progress * 0.45 : 0) + (c.grade || 0) * 26 + cacheBias;
if (score < bestScore) { best = c; bestScore = score; }
if (score < bestScore) { best = { ...c, secondBlocked }; bestScore = score; }
}
if (!best || (best.secondBlocked && opts.preferFullPath)) {
const grid = this.findGridPathWaypoint?.(actor, target, { ...opts, targetItem, exclude, padding, targetId });
if (grid) return grid;
if (!best) return null;
}
if (!best) return null;
actor._pathWaypoint = { x: best.x, y: best.y, targetId, until: (this.time || 0) + 0.95 };
return { x: best.x, y: best.y, dead: false, detour: true, routeTargetId: targetId };
},
findGridPathWaypoint(actor, target, opts = {}) {
if (!actor || !target || !Number.isFinite(Number(actor.x)) || !Number.isFinite(Number(actor.y)) || !Number.isFinite(Number(target.x)) || !Number.isFinite(Number(target.y))) return null;
const ax = Number(actor.x), ay = Number(actor.y), tx = Number(target.x), ty = Number(target.y);
const targetId = opts.targetId || opts.targetItem?.id || target?.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
const cached = actor._gridPathWaypoint || null;
const padding = Math.max(16, Number(opts.padding || 0) || (Number(actor.radius || 20) * 0.78));
const exclude = opts.exclude || opts.targetItem || target || null;
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
&& !this.pointBlockedByObstacle?.(cached.x, cached.y, Math.max(6, padding * 0.42), { exclude, maxChecks: 20 })
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId, gridPath: true };
}
const dTotal = Math.max(1, Math.hypot(tx - ax, ty - ay));
const step = Math.max(42, Math.min(72, Number(opts.gridStep || 0) || dTotal / 7));
const padWorld = (CONFIG.worldPadding || 28) + Math.max(8, Number(actor.radius || 20) * 0.45);
const margin = Math.max(190, step * 3.2);
const minX = Math.max(padWorld, Math.min(ax, tx) - margin);
const maxX = Math.min(this.w - padWorld, Math.max(ax, tx) + margin);
const minY = Math.max(padWorld, Math.min(ay, ty) - margin);
const maxY = Math.min(this.h - padWorld, Math.max(ay, ty) + margin);
const cols = Math.max(2, Math.min(18, Math.ceil((maxX - minX) / step) + 1));
const rows = Math.max(2, Math.min(18, Math.ceil((maxY - minY) / step) + 1));
const px = (ix) => cols <= 1 ? minX : minX + (maxX - minX) * ix / (cols - 1);
const py = (iy) => rows <= 1 ? minY : minY + (maxY - minY) * iy / (rows - 1);
const nearest = (x, y) => ({ ix: clamp(Math.round((x - minX) / Math.max(1, maxX - minX) * (cols - 1)), 0, cols - 1), iy: clamp(Math.round((y - minY) / Math.max(1, maxY - minY) * (rows - 1)), 0, rows - 1) });
const start = nearest(ax, ay);
const goal = nearest(tx, ty);
const key = (ix, iy) => `${ix},${iy}`;
const inBounds = (ix, iy) => ix >= 0 && iy >= 0 && ix < cols && iy < rows;
const pointOpen = (ix, iy) => {
if (!inBounds(ix, iy)) return false;
if (ix === start.ix && iy === start.iy) return true;
if (ix === goal.ix && iy === goal.iy) return true;
return !this.pointBlockedByObstacle?.(px(ix), py(iy), Math.max(6, padding * 0.45), { exclude, maxChecks: 18 });
};
const edgeOpen = (a, b) => !this.pathBlockedByFence?.(px(a.ix), py(a.iy), px(b.ix), py(b.iy), padding, { exclude });
const h = (ix, iy) => Math.hypot(px(ix) - tx, py(iy) - ty);
const open = [{ ...start, g: 0, f: h(start.ix, start.iy), parent: null }];
const bestByKey = new Map([[key(start.ix, start.iy), open[0]]]);
let found = null;
let guard = 0;
const dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
while (open.length && guard++ < 420) {
open.sort((a, b) => a.f - b.f);
const cur = open.shift();
if (cur.ix === goal.ix && cur.iy === goal.iy) { found = cur; break; }
for (const [dx, dy] of dirs) {
const nx = cur.ix + dx, ny = cur.iy + dy;
if (!pointOpen(nx, ny)) continue;
const nb = { ix: nx, iy: ny };
if (!edgeOpen(cur, nb)) continue;
const diagonal = dx && dy;
const ng = cur.g + (diagonal ? 1.42 : 1) * step;
const k = key(nx, ny);
const oldNode = bestByKey.get(k);
if (oldNode && oldNode.g <= ng) continue;
const node = { ix: nx, iy: ny, g: ng, f: ng + h(nx, ny), parent: cur };
bestByKey.set(k, node);
open.push(node);
}
}
if (!found) return null;
const path = [];
for (let n = found; n; n = n.parent) path.push(n);
path.reverse();
const chosen = path[Math.min(path.length - 1, Math.max(1, path.length > 3 ? 2 : 1))];
if (!chosen) return null;
const wx = px(chosen.ix), wy = py(chosen.iy);
actor._gridPathWaypoint = { x: wx, y: wy, targetId, until: (this.time || 0) + 1.15 };
actor._pathWaypoint = { x: wx, y: wy, targetId, until: (this.time || 0) + 1.15 };
return { x: wx, y: wy, dead: false, detour: true, routeTargetId: targetId, gridPath: true };
},
grassBlockedAt(x, y, self = null, opts = {}) {
const ignoreSoft = Boolean(opts?.ignoreSoftPlacement);
for (const it of this.nearbyItems(x, y, 96)) {

View file

@ -4,9 +4,362 @@
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_family_social.js");
const RESET_PRESETS = Object.freeze({
empty: { label: "空白" },
default: { label: "デフォルト" },
athletic: { label: "アスレチック" },
crowded: { label: "過密" },
family: { label: "家族" },
war: { label: "戦争" },
happy: { label: "幸福" },
});
function normalizeResetPresetId(presetId = "default") {
const id = String(presetId || "default");
return Object.prototype.hasOwnProperty.call(RESET_PRESETS, id) ? id : "default";
}
function presetScaleForField(field) {
const id = field?.id || "garden";
if (id === "cage") return 0.65;
if (id === "park") return 1.45;
return 1;
}
function presetCount(field, base, min = 0, max = Infinity) {
const value = Math.round((Number(base) || 0) * presetScaleForField(field));
return Math.max(min, Math.min(Number.isFinite(max) ? max : value, value));
}
function randPoint(worldRef, pad = 78, zone = null) {
const x0 = Math.max(pad, Number(zone?.x0 ?? pad));
const x1 = Math.min(worldRef.w - pad, Number(zone?.x1 ?? (worldRef.w - pad)));
const y0 = Math.max(pad, Number(zone?.y0 ?? pad));
const y1 = Math.min(worldRef.h - pad, Number(zone?.y1 ?? (worldRef.h - pad)));
return { x: rand(Math.min(x0, x1), Math.max(x0, x1)), y: rand(Math.min(y0, y1), Math.max(y0, y1)) };
}
function randomPresetAngle(step = Math.PI / 4) {
const n = Math.max(1, Math.round((Math.PI * 2) / Math.max(0.001, step)));
return Math.floor(rand(0, n)) * step;
}
function presetShapeExtent(segments) {
let max = 0;
for (const seg of segments || []) {
for (let i = 0; i < 4; i += 2) max = Math.max(max, Math.hypot(Number(seg[i]) || 0, Number(seg[i + 1]) || 0));
}
return max;
}
function configurePresetMechanicalItem(item, opts = {}) {
const body = item?.physicsBody;
if (!body || (item.type !== "rotator" && item.type !== "reciprocator")) return item;
body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 };
body.pose.x = item.x;
body.pose.y = item.y;
body.pose.angle = Number(item.angle) || 0;
body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 };
body.velocity.x = 0;
body.velocity.y = 0;
body.velocity.angular = 0;
body.velocity.linear = 0;
body.shape = body.shape || { model: "segments", thickness: 12, segments: [], version: 0 };
if (item.type === "rotator") {
const segments = opts.segments || [
[-92, 0, 92, 0],
[0, -58, 0, 58],
[-58, -42, 58, 42],
[-58, 42, 58, -42],
];
body.motor = body.motor || { powered: true, speed: Math.PI * 0.18, direction: 1 };
body.motor.powered = opts.motorOn !== false;
body.motor.speed = Number.isFinite(Number(opts.motorSpeed)) ? Number(opts.motorSpeed) : Math.PI * 0.18;
body.motor.direction = Number(opts.direction) < 0 ? -1 : 1;
body.shape.model = "segments";
body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14;
body.shape.segments = segments.map(seg => seg.slice(0, 4));
body.shape.version = (Number(body.shape.version) || 0) + 1;
item.spin = 0;
item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8);
} else if (item.type === "reciprocator") {
const railAxis = Number.isFinite(Number(opts.railAxis)) ? Number(opts.railAxis) : (Number(item.angle) || 0);
const segments = opts.segments || [
[-92, -24, -36, -24],
[-36, -24, -36, 24],
[-36, 24, 36, 24],
[36, 24, 36, -24],
[36, -24, 92, -24],
];
body.motor = body.motor || { powered: true, speed: 36, direction: 1 };
body.motor.powered = opts.railOn !== false;
body.motor.speed = Number.isFinite(Number(opts.railMotorSpeed)) ? Number(opts.railMotorSpeed) : 36;
body.motor.direction = Number(opts.direction) < 0 ? -1 : 1;
body.rail = body.rail || { axisAngle: railAxis, travel: 120, phase: 0, anchorX: item.x, anchorY: item.y };
body.rail.axisAngle = railAxis;
body.rail.travel = Number.isFinite(Number(opts.railTravel)) ? Number(opts.railTravel) : 120;
body.rail.phase = Number.isFinite(Number(opts.railPhase)) ? Number(opts.railPhase) : 0;
body.rail.anchorX = item.x;
body.rail.anchorY = item.y;
body.shape.model = "segments";
body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14;
body.shape.segments = segments.map(seg => seg.slice(0, 4));
body.shape.version = (Number(body.shape.version) || 0) + 1;
item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8);
}
globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-mechanical-shape");
globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
return item;
}
function syncPresetMechanicalPose(item) {
const body = item?.physicsBody;
if (!body) return;
body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 };
body.pose.x = item.x;
body.pose.y = item.y;
body.pose.angle = Number(item.angle) || 0;
if (body.rail) {
body.rail.anchorX = item.x;
body.rail.anchorY = item.y;
}
globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-placement-pose");
globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
}
function presetItemFootprintRadius(worldRef, item) {
const radius = globalThis.TarinaiCollisionFootprints?.itemFootprintRadius?.(worldRef, item);
if (Number.isFinite(Number(radius))) {
if (item?.type === "ball") return Math.max(34, Number(radius) * 1.35);
return Math.max(14, Number(radius));
}
return Math.max(14, Number(item?.r || 16) || 16);
}
function presetItemPlacementBlocked(worldRef, item, opts = {}) {
if (!worldRef || !item || item.dead) return true;
if (typeof worldRef.placementBlocked === "function" && worldRef.placementBlocked(item)) return true;
const itemRects = worldRef.solidObstacleRects?.(item) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, item) || [];
const itemRadius = presetItemFootprintRadius(worldRef, item);
const search = Math.max(120, itemRadius * 3.2);
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || worldRef.items || []) {
if (!other || other === item || other.dead) continue;
const sameType = other.type === item.type;
const otherIsFence = worldRef.isFenceType?.(other.type) || false;
const itemIsFence = worldRef.isFenceType?.(item.type) || false;
if (!opts.avoidAllObstacles && !sameType && !otherIsFence && !itemIsFence) continue;
const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || [];
const otherRadius = presetItemFootprintRadius(worldRef, other);
const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28);
if (itemRects.length && otherRects.length) {
if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true;
continue;
}
if (itemRects.length) {
if (itemRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, other.x, other.y, otherRadius, margin))) return true;
continue;
}
if (otherRects.length) {
if (otherRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, item.x, item.y, itemRadius, margin))) return true;
continue;
}
if (Math.hypot((item.x || 0) - (other.x || 0), (item.y || 0) - (other.y || 0)) < itemRadius + otherRadius + margin) return true;
}
return false;
}
function findPresetItemSpot(worldRef, item, opts = {}) {
const allowOriginal = opts.allowOriginal !== false;
if (allowOriginal && !presetItemPlacementBlocked(worldRef, item, opts)) return { x: item.x, y: item.y };
const solid = Boolean(worldRef?.isFenceType?.(item.type) || item.type === "rotator" || item.type === "reciprocator" || item.type === "nest_box");
const count = Math.max(12, Number(opts.attempts || 0) || (solid ? 96 : 58));
const maxRadius = Math.max(70, Number(opts.maxRadius || 0) || (solid ? 220 : 170));
const golden = Math.PI * (3 - Math.sqrt(5));
let best = null;
let bestD = Infinity;
const seed = `${item.type}:${Math.round(item.x)},${Math.round(item.y)}`;
for (let i = 0; i < count; i++) {
const t = count <= 1 ? 1 : i / (count - 1);
const radius = Math.max(10, maxRadius * Math.sqrt(t));
const angle = i * golden + (globalThis.stableUnit ? globalThis.stableUnit(seed, "preset-place") : Math.random()) * Math.PI * 2;
const rawX = item.x + Math.cos(angle) * radius;
const rawY = item.y + Math.sin(angle) * radius * 0.74;
const p = worldRef.placementClampPointFor?.(item, rawX, rawY) || { x: clamp(rawX, 42, worldRef.w - 42), y: clamp(rawY, 42, worldRef.h - 42) };
const oldX = item.x, oldY = item.y;
item.x = p.x; item.y = p.y; syncPresetMechanicalPose(item);
const blocked = presetItemPlacementBlocked(worldRef, item, opts);
item.x = oldX; item.y = oldY; syncPresetMechanicalPose(item);
if (blocked) continue;
const d = Math.hypot(p.x - oldX, p.y - oldY);
if (d < bestD) { best = p; bestD = d; }
}
return best;
}
function addPresetItem(worldRef, type, x, y, opts = {}) {
const item = new Item(type, x, y);
if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r);
if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
if (opts.toolSize) item.toolSize = opts.toolSize;
if (type === "duplicator") {
item.storedFoodType = opts.storedFoodType || "sweet";
item.storedFoodLabel = typeof foodLabel === "function" ? foodLabel(item.storedFoodType) : (typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType);
if (item.roles) item.roles.food = true;
}
if (type === "rotator" || type === "reciprocator") configurePresetMechanicalItem(item, opts);
if (opts.avoidOverlap) {
const spot = findPresetItemSpot(worldRef, item, opts);
if (!spot) return null;
item.x = spot.x;
item.y = spot.y;
syncPresetMechanicalPose(item);
}
if (worldRef.addItem) worldRef.addItem(item, opts.reason || `preset-${type}`);
else worldRef.items.push(item);
return item;
}
function addPresetGrass(worldRef, count, zone = null) {
for (let i = 0; i < count; i++) {
const p = randPoint(worldRef, 70, zone);
const grass = addPresetItem(worldRef, "grass", p.x, p.y, { reason: "preset-grass" });
globalThis.TarinaiGrass?.setStage?.(grass, Math.floor(rand(0, 2.999)), { force: true });
}
}
function addPresetTarinai(worldRef, count, zone = null, opts = {}) {
for (let i = 0; i < count; i++) {
const p = randPoint(worldRef, 82, zone);
worldRef.addTarinai({ x: p.x, y: p.y, generation: opts.generation || 1 });
}
}
function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) {
let placed = 0;
const n = Math.max(0, Number(count) | 0);
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
for (let i = 0; i < attempts && placed < n; i++) {
const p = randPoint(worldRef, opts.pad || 82, zone);
const item = addPresetItem(worldRef, type, p.x, p.y, opts);
if (item) placed++;
}
}
function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) {
const n = Math.max(1, Number(count) | 0);
const gap = worldRef.w / (n + 1);
for (let i = 1; i <= n; i++) {
addPresetItem(worldRef, type, gap * i, worldRef.h * yRatio + rand(-24, 24), { angle, reason: opts.reason || "preset-fence", avoidOverlap: !!opts.avoidOverlap, avoidAllObstacles: !!opts.avoidAllObstacles, attempts: opts.attempts || 96, maxRadius: opts.maxRadius || 190 });
}
}
function addPresetFencesScattered(worldRef, count, zone = null, opts = {}) {
const n = Math.max(0, Number(count) | 0);
let placed = 0;
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
for (let i = 0; i < attempts && placed < n; i++) {
const p = randPoint(worldRef, opts.pad || 78, zone);
const vertical = Math.random() < 0.5;
const item = addPresetItem(worldRef, vertical ? "fence_v" : "fence_h", p.x, p.y, { angle: randomPresetAngle(Math.PI / 4), reason: opts.reason || "preset-fence-scatter", avoidOverlap: !!opts.avoidOverlap, attempts: opts.attempts || 80, maxRadius: opts.maxRadius || 180 });
if (item) placed++;
}
}
function generateDefaultPreset(worldRef, field, seedPopulation) {
const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
const grassCount = field.grass ?? 18;
addPresetTarinai(worldRef, population);
addPresetGrass(worldRef, grassCount);
addPresetItem(worldRef, "stone", worldRef.w * 0.68, worldRef.h * 0.32, { reason: "reset-stone" });
}
function generateAthleticPreset(worldRef, field) {
const factor = presetScaleForField(field);
const fieldId = field?.id || "garden";
const compact = fieldId === "cage" || fieldId === "garden";
const center = { x0: worldRef.w * 0.10, x1: worldRef.w * 0.90, y0: worldRef.h * 0.14, y1: worldRef.h * 0.86 };
addPresetTarinai(worldRef, presetCount(field, 14, 7, 24), { x0: worldRef.w * 0.08, x1: worldRef.w * 0.30, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 });
// 物理ブロックは柵より先に置く。狭い中小フィールドでは柵側を間引き、ブロックを優先する。
addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 });
addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 });
addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 });
addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 });
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
}
function generateCrowdedPreset(worldRef, field) {
const center = { x0: worldRef.w * 0.18, x1: worldRef.w * 0.82, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 };
addPresetTarinai(worldRef, presetCount(field, 36, 18, 58), center);
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 13, 7, 22), center, { pad: 96, reason: "preset-nest-box" });
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), center, { pad: 110, storedFoodType: "sweet", reason: "preset-duplicator-zunda" });
addPresetGrass(worldRef, presetCount(field, 18, 8, 34), center);
addPresetItemsScattered(worldRef, "water", presetCount(field, 8, 4, 16), center, { pad: 84, reason: "preset-water" });
}
function generateFamilyPreset(worldRef, field) {
const cx = worldRef.w * 0.5;
const cy = worldRef.h * 0.52;
worldRef.addTarinai({ x: cx - 52, y: cy + 20, generation: 1 });
worldRef.addTarinai({ x: cx + 52, y: cy + 20, generation: 1 });
addPresetItem(worldRef, "nest_box", cx, cy - 58, { reason: "preset-family-nest" });
addPresetItem(worldRef, "duplicator", cx, cy + 112, { storedFoodType: "love_mochi", reason: "preset-family-duplicator" });
}
function generateWarPreset(worldRef, field) {
const left = { x0: worldRef.w * 0.08, x1: worldRef.w * 0.45, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
const right = { x0: worldRef.w * 0.55, x1: worldRef.w * 0.92, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
addPresetTarinai(worldRef, Math.ceil(presetCount(field, 28, 14, 48) / 2), left);
addPresetTarinai(worldRef, Math.floor(presetCount(field, 28, 14, 48) / 2), right);
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), { x0: worldRef.w * 0.38, x1: worldRef.w * 0.62, y0: worldRef.h * 0.22, y1: worldRef.h * 0.78 }, { pad: 96, storedFoodType: "fight_mochi", reason: "preset-war-duplicator" });
addPresetItemsScattered(worldRef, "ball", presetCount(field, 8, 4, 14), null, { pad: 90, reason: "preset-war-ball" });
addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 });
addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" });
addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" });
addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" });
}
function generateHappyPreset(worldRef, field) {
const center = { x0: worldRef.w * 0.16, x1: worldRef.w * 0.84, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 };
addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center);
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" });
addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" });
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" });
}
function generatePresetWorld(worldRef, field, presetId, seedPopulation) {
switch (normalizeResetPresetId(presetId)) {
case "empty": return;
case "athletic": return generateAthleticPreset(worldRef, field);
case "crowded": return generateCrowdedPreset(worldRef, field);
case "family": return generateFamilyPreset(worldRef, field);
case "war": return generateWarPreset(worldRef, field);
case "happy": return generateHappyPreset(worldRef, field);
case "default":
default:
return generateDefaultPreset(worldRef, field, seedPopulation);
}
}
function groundForResetPreset(presetId = "default") {
const id = normalizeResetPresetId(presetId);
if (id === "athletic") return "ice";
if (id === "war") return "foot_massage";
if (id === "happy") return "blanket";
return "";
}
global.TarinaiResetPresets = Object.freeze({ ids: Object.keys(RESET_PRESETS), meta: RESET_PRESETS, normalize: normalizeResetPresetId });
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
reset(seedPopulation = null, fieldType = this.fieldType || "garden") {
reset(seedPopulation = null, fieldType = this.fieldType || "garden", presetId = "default") {
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
const preset = normalizeResetPresetId(presetId);
this.fieldType = field.id;
this.fieldZoom = 1;
this.setViewportSize(this.viewportW, this.viewportH);
@ -59,30 +412,20 @@
this.lastPhase = this.phaseName();
this.weather = "sunny";
this.groundType = window.TarinaiGround.exists(this.groundType || "soil") ? (this.groundType || "soil") : "soil";
const presetGround = groundForResetPreset(preset);
if (presetGround && window.TarinaiGround.exists(presetGround)) this.groundType = presetGround;
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
seedPopulation = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
const grassCount = field.grass ?? 18;
for (let i = 0; i < seedPopulation; i++) {
this.addTarinai({
x: rand(80, this.w - 80),
y: rand(80, this.h - 80),
generation: 1,
});
}
for (let i = 0; i < grassCount; i++) {
const grass = new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70));
globalThis.TarinaiGrass.setStage(grass, Math.floor(rand(0, 2.999)), { force: true });
if (this.addItem) this.addItem(grass, "reset-grass");
else this.items.push(grass);
}
this.addItem?.(new Item("stone", this.w * 0.68, this.h * 0.32), "reset-stone") || this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32));
generatePresetWorld(this, field, preset, seedPopulation);
this.updateItemCounts();
this.enforceGrassLimit?.("reset-grass-limit");
this.updateItemCounts();
this.updateEffectCounts();
this.markTerrainDirty?.("reset");
this.rebuildSpatial(true);
showToast("\u65b0\u3057\u3044\u89b3\u5bdf\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002");
const label = RESET_PRESETS[preset]?.label || "デフォルト";
showToast(`${label}で新しい観察を開始しました。`);
syncTopButtons();
},
@ -457,7 +800,35 @@
const occupants = this.nestBoxOccupants(box, Infinity);
const names = occupants.map(t => t?.name).filter(Boolean).slice(0, 5);
const more = occupants.length > names.length ? `\u3001\u307b\u304b${occupants.length - names.length}\u5339` : "";
return ["\u5de3\u7bb1", names.length ? `\u5165\u3063\u3066\u308b\u5b50: ${names.join("\u3001")}${more}` : "\u5165\u3063\u3066\u308b\u5b50: \u306a\u3057"];
return ["\u5de3\u7bb1", names.length ? `\u5165\u3063\u3066\u308b\u5b50: ${names.join("\u3001")}${more}` : "\u5165\u3063\u3066\u308b\u5b50: \u306a\u3057", this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean);
},
temperatureTooltipLineFor(target, label = "体感気温") {
if (!target || !this.temperatureAt) return "";
const temp = this.temperatureAt(target.x, target.y);
if (!Number.isFinite(Number(temp))) return "";
const status = this.temperatureStatusFor?.(temp) || null;
const suffix = status?.label ? `${status.label}` : "";
return `${label}: ${Number(temp).toFixed(1)}${suffix}`;
},
pointerTarinaiTooltipInfo() {
const p = this.pointer;
if (!p?.inside || !Array.isArray(this.tarinai)) return null;
let best = null, bestD = Infinity;
for (const t of this.tarinai) {
if (!t || t.dead) continue;
const d = distXY(p.x, p.y, t.x, t.y);
const limit = Math.max(46, (t.radius || 20) * 1.35);
if (d <= limit && d < bestD) { best = t; bestD = d; }
}
if (!best) return null;
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "") || window.TEXT_CATALOG?.stateLabel?.(best.state, best) || best.thought || "";
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100);
const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`;
const tempLine = this.temperatureTooltipLineFor(best, "体感気温");
return { target: best, lines: [best.name || "たりない", behavior, statusLine, tempLine].filter(Boolean), kind: "tarinai" };
},
pointerOwnedBedInfo() {
@ -477,10 +848,12 @@
ownedBedTooltipLines(bed) {
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
const owner = this.liveTarinaiById?.(bed.ownerId) || null;
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`];
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, this.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean);
},
pointerItemTooltipInfo() {
const tarinaiInfo = this.pointerTarinaiTooltipInfo?.();
if (tarinaiInfo?.target) return tarinaiInfo;
const nest = this.pointerNestBoxInfo?.();
if (nest?.box) return { target: nest.box, lines: this.nestBoxTooltipLines(nest.box), kind: "nest_box" };
const bedInfo = this.pointerOwnedBedInfo?.();
@ -818,8 +1191,7 @@
const actorScore = actor === a ? scoreA : scoreB;
const targetScore = actor === a ? scoreB : scoreA;
const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite();
const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04;
const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90);
const intimidationChance = 0.30;
if (canIntimidate && deterministicChance(this, "conflict-intimidation-choice", intimidationChance, actor, target)) {
return this.startIntimidation(actor, target, actorScore, targetScore);
}

View file

@ -245,7 +245,7 @@
return this.shootAimPoint?.() || { x: this.shootPointerRawX, y: this.shootPointerRawY };
},
moveShootCursorBy(dx = 0, dy = 0) {
moveShootCursorBy() {
// Mouse/touch movement should move the base pointer, not accumulate extra
// logical drift. Only recoil changes shootCursorOffsetX/Y.
this.shootCursorActive = true;

View file

@ -124,7 +124,6 @@
sanitizeOutOfBounds() {
const pad = Math.max(28, CONFIG.worldPadding || 30);
const far = Math.max(360, Math.max(this.w || 1000, this.h || 720) * 0.75);
let changedItems = false;
const repairPoint = (obj, radius = 12) => {
if (!obj) return "missing";

View file

@ -326,13 +326,13 @@ def check_social_balance_guards() -> None:
world_social = read_text(ROOT / "js/world_family_social.js")
social_move = read_text(ROOT / "js/tarinai_social_move_life.js")
required_need_tokens = [
"fight_rival: 56",
"intimidate_enemy: 42",
"approach_mate: 68",
"fight_rival: 72",
"intimidate_enemy: 0",
"approach_mate: 72",
"approach_friend: 66",
"return ({ conflict: 34, mate: 24, family: 12, bond: 14",
"return ({ conflict: 26, mate: 24, family: 12, bond: 14",
"if (subNeed !== \"conflict\" && subValue >= threshold) return true",
"subValue >= threshold && subValue >= socialPull * 0.82",
"subValue >= threshold * 0.82 && subValue >= socialPull * 0.68",
]
for token in required_need_tokens:
if token not in needs:

View file

@ -1,6 +1,6 @@
"use strict";
const APP_VERSION = "39.15.75";
const APP_VERSION = "39.15.84";
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.