h
This commit is contained in:
parent
e00bfd8879
commit
f941200504
70 changed files with 1856 additions and 429 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.15.75",
|
||||
"version": "39.15.84",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
|
|||
BIN
assets/sounds/voice_008_temperature_buruburu.wav
Normal file
BIN
assets/sounds/voice_008_temperature_buruburu.wav
Normal file
Binary file not shown.
BIN
assets/sounds/voice_009_temperature_achui.wav
Normal file
BIN
assets/sounds/voice_009_temperature_achui.wav
Normal file
Binary file not shown.
BIN
assets/sprites/tarinai_34_hot_01.webp
Normal file
BIN
assets/sprites/tarinai_34_hot_01.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/sprites/tarinai_35_hot_02.webp
Normal file
BIN
assets/sprites/tarinai_35_hot_02.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/sprites/tarinai_36_hot_03.webp
Normal file
BIN
assets/sprites/tarinai_36_hot_03.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/sprites/tarinai_37_cold_01.webp
Normal file
BIN
assets/sprites/tarinai_37_cold_01.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/sprites/tarinai_38_cold_02.webp
Normal file
BIN
assets/sprites/tarinai_38_cold_02.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/sprites/tarinai_39_cold_03.webp
Normal file
BIN
assets/sprites/tarinai_39_cold_03.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
21
css/base.css
21
css/base.css
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)); }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
253
css/panel.css
253
css/panel.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
333
index.html
333
index.html
|
|
@ -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>状態</span><strong id="statColonyMood">安定</strong></div>
|
||||
<div class="stat ground-stat" data-ground-toggle role="button" tabindex="0"><span>地面</span><button id="groundTypeBtn" class="ground-type-btn" type="button" data-ground-toggle aria-label="地面をきりかえる"><strong id="statGroundType">砂</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">個体数</button><button data-chart="life">欲求</button><button data-chart="objects">物</button></div>
|
||||
<div id="colonyChartLegend" class="colony-chart-legend"></div>
|
||||
<div id="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" 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">フィールドを選択</h2>
|
||||
<div class="field-choice-grid">
|
||||
<button class="field-choice" data-field-type="park" type="button"><strong>公園</strong><span>大</span></button>
|
||||
<button class="field-choice" data-field-type="garden" type="button"><strong>庭</strong><span>中</span></button>
|
||||
<button class="field-choice" data-field-type="cage" type="button"><strong>ケージ</strong><span>小</span></button>
|
||||
<div class="field-dialog-panel reset-dialog-panel">
|
||||
<h2 id="fieldDialogTitle">リセット設定</h2>
|
||||
<p class="field-dialog-lead">サイズとプリセットを組み合わせて観察環境を生成します。</p>
|
||||
<div class="field-dialog-section-title">サイズ</div>
|
||||
<div class="field-choice-grid reset-size-choice-grid">
|
||||
<button class="field-choice" data-field-type="park" type="button" aria-pressed="false"><strong>公園</strong><span>大</span></button>
|
||||
<button class="field-choice" data-field-type="garden" type="button" aria-pressed="true"><strong>庭</strong><span>中</span></button>
|
||||
<button class="field-choice" data-field-type="cage" type="button" aria-pressed="false"><strong>ケージ</strong><span>小</span></button>
|
||||
</div>
|
||||
<div class="field-dialog-section-title">プリセット</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>空白</strong><span>何も無い</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="default" type="button" aria-pressed="true"><strong>デフォルト</strong><span>初期生成</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="athletic" type="button" aria-pressed="false"><strong>アスレチック</strong><span>柵とボール</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="crowded" type="button" aria-pressed="false"><strong>過密</strong><span>大量とずんだ</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="family" type="button" aria-pressed="false"><strong>家族</strong><span>2匹とへこ餅</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="war" type="button" aria-pressed="false"><strong>戦争</strong><span>けんかと柵</span></button>
|
||||
<button class="field-choice preset-choice" data-reset-preset="happy" type="button" aria-pressed="false"><strong>幸福</strong><span>掃除機と巣箱</span></button>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="fieldCancelBtn" class="btn" type="button">キャンセル</button>
|
||||
<button id="fieldConfirmBtn" class="btn danger" type="button">再生成</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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
11
js/assets.js
11
js/assets.js
|
|
@ -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,
|
||||
|
|
|
|||
32
js/audio.js
32
js/audio.js
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
17
js/data.js
17
js/data.js
|
|
@ -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,
|
||||
|
|
|
|||
13
js/health.js
13
js/health.js
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
}
|
||||
|
||||
refresh() {
|
||||
const previous = this.classification;
|
||||
const next = this.classify();
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"] },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
86
js/render.js
86
js/render.js
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 ?? ""));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
|
||||
function updateEnvironment(worldRef, dt) {
|
||||
worldRef.updateWeather(dt);
|
||||
worldRef.updateTemperature?.(dt);
|
||||
worldRef.updateTerrainDirtyState?.(dt);
|
||||
worldRef.evaluateColonyMood?.();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 : "";
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) : [];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
|
||||
|
|
|
|||
6
js/ui.js
6
js/ui.js
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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?.();
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue