--
This commit is contained in:
parent
5ed977788c
commit
f500279abd
93 changed files with 1893 additions and 1198 deletions
18
README.md
Normal file
18
README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# tarinai v39.15.39
|
||||
|
||||
## 変更点
|
||||
|
||||
- PC表示を再度ロールバックし、PC側の変更はクイックスクロールの「本体」「道具」ボタン非表示に限定。道具UI本体は維持。
|
||||
- PC上部ボタンを右詰め・やや大きめに調整し、ゲームタイトルがボタンに押されて省略されないように修正。
|
||||
- スマホ用のスクロールスナップ系指定を無効化し、通常スクロールを妨げないように修正。
|
||||
- スマホで「家系図」へ移動したとき、更新OFFでも手動ジャンプ扱いで現在の家系図を描画してからスクロールするように修正。
|
||||
- スマホの自動スクロールボタンは、初期折り畳み部分を開いてから目的地へ移動。ユーザーが手動で閉じた部分は開かない挙動を維持。
|
||||
- たりない/道具系ツールチップの最大幅・折り返し・画面端補正を維持。
|
||||
- 「333の部屋に移動」のリンク先は `https://2012r2.nishi.boats/~333/`。
|
||||
- 「観察」は「たりないデータ」の表記を維持。
|
||||
|
||||
## 確認
|
||||
|
||||
- `node --check js/*.js` passed
|
||||
- `python3 scripts/regression_check.py` passed
|
||||
- version synchronized: `39.15.39`
|
||||
43
SENSOR_IDEAS.md
Normal file
43
SENSOR_IDEAS.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# センサー系アイデア
|
||||
|
||||
実験観察ゲームとして、直接介入ではなく「測る」ための道具案。
|
||||
|
||||
## 低コストで入れやすいもの
|
||||
|
||||
| センサー | 役割 | 記録する値 | 観察上の使い道 |
|
||||
|---|---|---|---|
|
||||
| ストレスセンサー | 範囲内の緊張度を測る | 平均ストレス / 最大ストレス / 急上昇回数 | 地形・混雑・病気・王者個体の影響を見る |
|
||||
| 通過カウンター | 置いた地点を通った回数を数える | 通過数 / 個体別通過数 | 道・縄張り・回避行動の発見 |
|
||||
| 滞在センサー | 範囲内にいた時間を測る | 滞在秒数 / 個体ID / 世代 | 好まれる場所・避けられる場所の比較 |
|
||||
| けんかセンサー | 範囲内の争いを記録 | 勝者 / 敗者 / 勝率変化 | 王者候補・危険地帯の検出 |
|
||||
| 汚染センサー | ずんち・病気・汚れを見る | 汚染量 / 増加量 / 感染者数 | スプリンクラーや隔離の効果検証 |
|
||||
| 出生センサー | 近くで生まれた個体を記録 | 親 / 子 / 遺伝値 | 繁殖条件・血統観察 |
|
||||
|
||||
## 中コストのセンサー
|
||||
|
||||
| センサー | 仕様 |
|
||||
|---|---|
|
||||
| 個体追跡ビーコン | 特定個体に付ける。移動経路、接触相手、食事、喧嘩、睡眠を時系列で記録する。 |
|
||||
| 関係センサー | 範囲内で友好・敵対が増減した時だけログを残す。群れの分裂や王者の圧力を見やすくする。 |
|
||||
| 遺伝スキャナー | 選択個体の寿命・サイズ・攻撃・速度の遺伝倍率を読み取る。家系図の補助UIにも使える。 |
|
||||
| 行動理由センサー | 近くの個体が「なぜその行動を選んだか」を短く記録する。空腹、恐怖、王者回避、繁殖欲求など。 |
|
||||
| 死因センサー | 範囲内の死亡時に、直前の状態・接触相手・地面・病気・道具をまとめる。事故検証用。 |
|
||||
|
||||
## 高コストだが強いもの
|
||||
|
||||
| センサー | 仕様 |
|
||||
|---|---|
|
||||
| ヒートマップ装置 | ストレス、滞在、喧嘩、死亡、汚染、出生を色で重ねて表示する。 |
|
||||
| A/B実験レコーダー | 2つの区画に別条件を設定し、生存数・出生数・平均寿命・王者発生率を比較する。 |
|
||||
| 血統観察端末 | 家系ごとの平均寿命、平均サイズ、勝率、移動速度、病気耐性をランキング表示する。 |
|
||||
| 社会構造センサー | 群れの中心個体、孤立個体、王者、敵対ネットワークをグラフ化する。 |
|
||||
|
||||
## 特に相性が良い実装順
|
||||
|
||||
1. 通過カウンター
|
||||
2. ストレスセンサー
|
||||
3. けんかセンサー
|
||||
4. 遺伝スキャナー
|
||||
5. ヒートマップ装置
|
||||
|
||||
最初は「画面内に結果を出す道具」ではなく、ログとグラフに測定値を流すだけでも十分に実験感が出る。
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.15.27",
|
||||
"version": "39.15.39",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
|
|||
BIN
assets/sounds/major_damage_01.wav
Normal file
BIN
assets/sounds/major_damage_01.wav
Normal file
Binary file not shown.
BIN
assets/sounds/major_damage_02.wav
Normal file
BIN
assets/sounds/major_damage_02.wav
Normal file
Binary file not shown.
BIN
assets/sounds/major_damage_03.wav
Normal file
BIN
assets/sounds/major_damage_03.wav
Normal file
Binary file not shown.
BIN
assets/sounds/major_damage_04.wav
Normal file
BIN
assets/sounds/major_damage_04.wav
Normal file
Binary file not shown.
|
|
@ -557,7 +557,7 @@ h1 {
|
|||
top: 0;
|
||||
z-index: 12;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 8px 4px 9px;
|
||||
margin: -2px 0 2px;
|
||||
|
|
|
|||
|
|
@ -278,14 +278,14 @@
|
|||
position: relative;
|
||||
}
|
||||
.panel-card-state::before {
|
||||
content: "▾";
|
||||
content: "\25be ";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-52%);
|
||||
color: rgba(98, 82, 64, 0.72);
|
||||
}
|
||||
.collapsible-card.collapsed .panel-card-state::before { content: "▸"; }
|
||||
.collapsible-card.collapsed .panel-card-state::before { content: "\25b8 "; }
|
||||
.collapsible-card.collapsed .panel-card-body { display: none; }
|
||||
.panel-card-body { padding-top: 12px; }
|
||||
|
||||
|
|
@ -756,7 +756,7 @@
|
|||
background: rgba(238, 247, 255, 0.98);
|
||||
}
|
||||
.tool-category[data-tool-category="operate"] .tool-category-body {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
.tool-category[data-tool-category="operate"] .tool {
|
||||
|
|
@ -785,3 +785,19 @@
|
|||
@media (max-width: 980px) {
|
||||
.tool-category[data-tool-category="operate"]:not(.collapsed) { top: 58px; }
|
||||
}
|
||||
|
||||
/* v39.15.38: prevent long tooltips from spilling outside the viewport. */
|
||||
.tool-tip-popover {
|
||||
width: max-content;
|
||||
min-width: 160px;
|
||||
max-width: min(320px, calc(100vw - 18px));
|
||||
box-sizing: border-box;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
body.touch-mobile-ui .tool-tip-popover {
|
||||
max-width: min(280px, calc(100vw - 24px));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
.tool[data-tool="poke"] { --tool-emoji: "\1F448"; }
|
||||
.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; }
|
||||
.tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; }
|
||||
.tool[data-tool="copy"] { --tool-emoji: "⧉"; }
|
||||
.tool[data-tool="area_delete"] { --tool-emoji: "×"; }
|
||||
.tool[data-tool="rope"] { --tool-emoji: "╰"; }
|
||||
.tool[data-tool="rod"] { --tool-emoji: "━"; }
|
||||
.tool[data-tool="reciprocator"] { --tool-emoji: "⇄"; }
|
||||
.tool[data-tool="undo"] { --tool-emoji: "↶"; }
|
||||
.tool[data-tool="redo"] { --tool-emoji: "↷"; }
|
||||
.tool[data-tool="copy"] { --tool-emoji: "\29c9 "; }
|
||||
.tool[data-tool="area_delete"] { --tool-emoji: "\d7 "; }
|
||||
.tool[data-tool="rope"] { --tool-emoji: "\2570 "; }
|
||||
.tool[data-tool="rod"] { --tool-emoji: "\2501 "; }
|
||||
.tool[data-tool="reciprocator"] { --tool-emoji: "\21c4 "; }
|
||||
.tool[data-tool="undo"] { --tool-emoji: "\21b6 "; }
|
||||
.tool[data-tool="redo"] { --tool-emoji: "\21b7 "; }
|
||||
.tool.tool-emoji-icon { --tool-icon: none; }
|
||||
.tool.tool-emoji-icon::before {
|
||||
content: var(--tool-emoji);
|
||||
|
|
@ -50,7 +50,8 @@
|
|||
transform: translateY(-54%) scale(var(--tool-icon-zoom, 1));
|
||||
}
|
||||
.tool-category[data-tool-category="operate"] .tool[data-tool="area_delete"].tool-emoji-icon::before {
|
||||
transform: translate(-50%, -54%) scale(var(--tool-icon-zoom, 1));
|
||||
top: 42% !important;
|
||||
transform: translate(-50%, -50%) scale(var(--tool-icon-zoom, 1)) !important;
|
||||
}
|
||||
|
||||
.tool[data-tool="ball"] { --tool-icon-zoom: 1.36; }
|
||||
|
|
|
|||
254
css/mobile.css
254
css/mobile.css
|
|
@ -166,7 +166,6 @@ body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { display: flex; }
|
|||
body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; }
|
||||
body.touch-mobile-ui.mobile-mode-camera #gameCanvas { cursor: grab; }
|
||||
body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
||||
body.touch-mobile-ui.mobile-mode-family .lineage-main-card { z-index: 9; }
|
||||
|
||||
|
||||
/* Keep panel themes on narrow layouts. */
|
||||
|
|
@ -174,3 +173,256 @@ body.touch-mobile-ui.mobile-mode-family .lineage-main-card { z-index: 9; }
|
|||
.card.selected-card { --ui-theme-rgb: 204, 121, 167; background: linear-gradient(180deg, rgba(253, 245, 250, 0.94), rgba(249, 235, 244, 0.91)); border-color: rgba(204, 121, 167, 0.24); }
|
||||
.card.colony-card { --ui-theme-rgb: 0, 158, 115; background: linear-gradient(180deg, rgba(240, 250, 247, 0.94), rgba(229, 245, 239, 0.91)); border-color: rgba(0, 158, 115, 0.24); }
|
||||
.card.log-card { --ui-theme-rgb: 230, 159, 0; background: linear-gradient(180deg, rgba(255, 249, 236, 0.94), rgba(253, 242, 215, 0.91)); border-color: rgba(230, 159, 0, 0.24); }
|
||||
|
||||
|
||||
/* v39.15.35: mobile controls and scroll affordances */
|
||||
@media (max-width: 980px) {
|
||||
body.touch-mobile-ui .mobile-mode-controls:not([hidden]) {
|
||||
display: flex !important;
|
||||
}
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.panel-quick-tabs button {
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
}
|
||||
body.touch-mobile-ui .tool-tip-popover {
|
||||
max-width: min(240px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
/* v39.15.36: mobile navigation redesign without family input mode */
|
||||
@media (max-width: 980px) {
|
||||
body.touch-mobile-ui .mobile-mode-controls:not([hidden]) {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 12001;
|
||||
max-width: calc(100vw - 22px);
|
||||
}
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
.lineage-main-card {
|
||||
margin: 10px 0 0;
|
||||
contain: content;
|
||||
}
|
||||
.lineage-main-card .lineage-head {
|
||||
gap: 8px;
|
||||
}
|
||||
.lineage-main-card .lineage-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
body.touch-mobile-ui .tool-category:not([data-tool-category="operate"]).collapsed .tool-category-body {
|
||||
display: none;
|
||||
}
|
||||
body.touch-mobile-ui .log {
|
||||
max-height: 240px;
|
||||
}
|
||||
}
|
||||
.back-to-game-btn {
|
||||
position: fixed;
|
||||
right: calc(env(safe-area-inset-right, 0px) + 12px);
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 68px);
|
||||
z-index: 12000;
|
||||
border: 1px solid rgba(103,178,44,.38);
|
||||
border-radius: 999px;
|
||||
padding: 9px 12px;
|
||||
background: rgba(246, 255, 236, 0.94);
|
||||
color: #3f5f2b;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 10px 25px rgba(54, 76, 40, .20);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.back-to-game-btn[hidden] { display: none !important; }
|
||||
@media (min-width: 981px) {
|
||||
.back-to-game-btn { display: none !important; }
|
||||
}
|
||||
|
||||
/* v39.15.38: keep desktop layout close to v39.15.36; only hide the PC quick-scroll buttons that duplicate the canvas/tool card. */
|
||||
@media (min-width: 981px) {
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.panel-quick-tabs [data-panel-tab="game"],
|
||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v39.15.38: mobile navigation/footer room and family-graph reachability. */
|
||||
@media (max-width: 980px) {
|
||||
html,
|
||||
body {
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-left: max(10px, env(safe-area-inset-left, 0px));
|
||||
padding-right: max(10px, env(safe-area-inset-right, 0px));
|
||||
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 92px);
|
||||
overflow-x: clip;
|
||||
}
|
||||
.topbar,
|
||||
.top-left,
|
||||
.brand,
|
||||
.layout,
|
||||
.stage-wrap,
|
||||
.panel,
|
||||
.card,
|
||||
.lineage-main-card {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.top-actions {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 1px 0 5px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.top-actions > .btn,
|
||||
.top-actions > .sound-menu,
|
||||
.top-actions .sound-menu-btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-actions .btn {
|
||||
padding: 7px 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.room-link {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
body.touch-mobile-ui .mobile-mode-controls:not([hidden]) {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: 10px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 24;
|
||||
max-width: calc(100% - 18px);
|
||||
}
|
||||
body.touch-mobile-ui .panel-quick-tabs {
|
||||
position: fixed;
|
||||
left: max(8px, env(safe-area-inset-left, 0px));
|
||||
right: max(8px, env(safe-area-inset-right, 0px));
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
|
||||
top: auto;
|
||||
z-index: 12002;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
border: 1px solid rgba(102, 169, 215, 0.34);
|
||||
border-radius: 16px;
|
||||
background: rgba(244, 250, 247, 0.95);
|
||||
box-shadow: 0 12px 28px rgba(42, 54, 64, 0.18);
|
||||
}
|
||||
body.touch-mobile-ui .panel-quick-tabs button {
|
||||
min-height: 34px;
|
||||
padding: 5px 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
body.touch-mobile-ui .selected-card,
|
||||
body.touch-mobile-ui .tool-card,
|
||||
body.touch-mobile-ui .colony-card,
|
||||
body.touch-mobile-ui .log-card,
|
||||
body.touch-mobile-ui .lineage-main-card {
|
||||
scroll-margin-top: 10px;
|
||||
scroll-margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 88px);
|
||||
}
|
||||
body.touch-mobile-ui .lineage-main-card {
|
||||
margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 78px);
|
||||
}
|
||||
.back-to-game-btn {
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 62px);
|
||||
right: max(10px, env(safe-area-inset-right, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* v39.15.39: desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */
|
||||
@media (min-width: 981px) {
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 18px;
|
||||
}
|
||||
.top-left {
|
||||
flex: 1 0 auto;
|
||||
min-width: max-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
.brand,
|
||||
.brand h1,
|
||||
h1 {
|
||||
flex: 0 0 auto;
|
||||
min-width: max-content;
|
||||
max-width: none !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
}
|
||||
.top-actions {
|
||||
flex: 1 1 560px;
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.top-actions .btn,
|
||||
.top-actions .sound-menu-btn,
|
||||
.room-link {
|
||||
min-height: 42px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.panel-quick-tabs [data-panel-tab="game"],
|
||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* v39.15.39: mobile scroll must remain manual; disable any CSS scroll snapping. */
|
||||
@media (max-width: 980px) {
|
||||
html, body, #app, .layout, .stage-wrap, .panel, .card, .lineage-main-card {
|
||||
scroll-snap-type: none !important;
|
||||
scroll-snap-align: none !important;
|
||||
scroll-snap-stop: normal !important;
|
||||
}
|
||||
.lineage-main-card {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 132px);
|
||||
}
|
||||
.lineage-main-content {
|
||||
min-height: 360px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,36 @@ body::before {
|
|||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.family-genetic-stats {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 9px;
|
||||
line-height: 1.16;
|
||||
color: rgba(88, 70, 48, 0.86);
|
||||
letter-spacing: -0.02em;
|
||||
white-space: normal;
|
||||
max-width: 104px;
|
||||
}
|
||||
.family-genetic-stats b {
|
||||
font-weight: 800;
|
||||
color: rgba(120, 86, 30, 0.92);
|
||||
}
|
||||
.lineage-family-tree .tree-node.champion {
|
||||
border-color: rgba(202, 146, 33, 0.58);
|
||||
box-shadow: inset 0 0 0 1px rgba(247, 200, 64, 0.30), 0 3px 8px rgba(120, 78, 12, 0.09);
|
||||
}
|
||||
.lineage-family-tree .tree-node.champion::before {
|
||||
content: "♛";
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 4px;
|
||||
z-index: 2;
|
||||
font-size: 14px;
|
||||
color: rgba(158, 102, 20, 0.95);
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,0.70);
|
||||
}
|
||||
|
||||
.family-node.alive .family-portrait { opacity: 0.80; }
|
||||
.family-node.dead .family-portrait { opacity: 0.20; }
|
||||
.family-portrait img {
|
||||
|
|
@ -355,9 +385,9 @@ body::before {
|
|||
}
|
||||
.lineage-family-tree .tree-node {
|
||||
position: absolute;
|
||||
width: 132px;
|
||||
height: 84px;
|
||||
min-height: 84px;
|
||||
width: 150px;
|
||||
height: 104px;
|
||||
min-height: 104px;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
|
||||
|
|
|
|||
336
index.html
336
index.html
|
|
@ -3,20 +3,20 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</title>
|
||||
<title>たりない観察</title>
|
||||
<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.27" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.27" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.27" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.27" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.27" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.39" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.39" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.39" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.39" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.39" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
<div class="loading-panel">
|
||||
<div class="loading-kicker">たりない観察</div>
|
||||
<div class="loading-kicker">たりない観察</div>
|
||||
<h1 class="loading-title">読み込み中…</h1>
|
||||
<p id="loadingText" class="loading-text">準備しています。</p>
|
||||
<div id="loadingSpriteStrip" class="loading-sprite-strip" aria-hidden="true"></div>
|
||||
|
|
@ -27,21 +27,21 @@
|
|||
<div id="app">
|
||||
<header class="topbar">
|
||||
<div class="top-left">
|
||||
<a class="btn room-link" href="https://host.nishi.boats/~333/">333の部屋に移動</a>
|
||||
<a class="btn room-link" href="https://2012r2.nishi.boats/~333/">333の部屋に移動</a>
|
||||
<div class="brand">
|
||||
<h1>たりない観察</h1>
|
||||
<h1>たりない観察</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button id="pauseBtn" class="btn primary">一時停止</button>
|
||||
<button id="speedBtn" class="btn">速度 x1</button>
|
||||
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
||||
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
||||
<div id="soundMenu" class="sound-menu">
|
||||
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音設定 ON</button>
|
||||
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="効果音設定">
|
||||
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
|
||||
<div class="sound-panel-divider"></div>
|
||||
<label><input type="checkbox" data-sound-category="voice"><span>個体の声</span></label>
|
||||
<label><input type="checkbox" data-sound-category="voice"><span>個体の声</span></label>
|
||||
<label><input type="checkbox" data-sound-category="notify" checked><span>通知</span></label>
|
||||
<label><input type="checkbox" data-sound-category="ops" checked><span>操作</span></label>
|
||||
</div>
|
||||
|
|
@ -54,29 +54,30 @@
|
|||
|
||||
<main class="layout">
|
||||
<section class="stage-wrap">
|
||||
<canvas id="gameCanvas" aria-label="たりない観察キャンバス"></canvas>
|
||||
<canvas id="gameCanvas" aria-label="たりない観察キャンバス"></canvas>
|
||||
<div id="mobileModeControls" class="mobile-mode-controls" aria-label="スマホ操作モード">
|
||||
<button type="button" data-mobile-mode="auto" class="active">自動</button>
|
||||
<button type="button" data-mobile-mode="camera">カメラ</button>
|
||||
<button type="button" data-mobile-mode="tool">道具</button>
|
||||
<button type="button" data-mobile-mode="family">家系図</button>
|
||||
</div>
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
</section>
|
||||
|
||||
<aside class="panel">
|
||||
<nav id="panelQuickTabs" class="panel-quick-tabs" aria-label="右メニューのタブ">
|
||||
<button type="button" data-panel-tab="selected">観察</button>
|
||||
<button type="button" data-panel-tab="tools">道具</button>
|
||||
<button type="button" data-panel-tab="colony">コロニー</button>
|
||||
<button type="button" data-panel-tab="events">イベント</button>
|
||||
<nav id="panelQuickTabs" class="panel-quick-tabs" aria-label="右メニューのタブ">
|
||||
<button type="button" data-panel-tab="game">本体</button>
|
||||
<button type="button" data-panel-tab="selected">たりないデータ</button>
|
||||
<button type="button" data-panel-tab="tools">道具</button>
|
||||
<button type="button" data-panel-tab="colony">コロニー</button>
|
||||
<button type="button" data-panel-tab="events">イベント</button>
|
||||
<button type="button" data-panel-tab="family">家系図</button>
|
||||
</nav>
|
||||
<section id="selectedCard" class="card selected-card hidden">
|
||||
<div class="selected-head">
|
||||
<h2>観察</h2>
|
||||
<button id="selectedCloseBtn" class="selected-close" type="button" aria-label="観察をやめる">×</button>
|
||||
<h2>たりないデータ</h2>
|
||||
<button id="selectedCloseBtn" class="selected-close" type="button" aria-label="たりないデータを閉じる">×</button>
|
||||
</div>
|
||||
<div id="selectedInfo" class="selected-info empty">たりないを選択してください</div>
|
||||
<div id="selectedInfo" class="selected-info empty">たりないを選択してください</div>
|
||||
</section>
|
||||
|
||||
<section id="toolCard" class="card tool-card collapsible-card" data-card-key="tools">
|
||||
|
|
@ -92,12 +93,12 @@
|
|||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">コロニー</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div class="stats colony-stats">
|
||||
<div class="stat"><span>個体</span><strong id="statPop">0</strong></div>
|
||||
<div class="stat"><span>死亡</span><strong id="statDead">0</strong></div>
|
||||
<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 class="stat"><span>個体</span><strong id="statPop">0</strong></div>
|
||||
<div class="stat"><span>死亡</span><strong id="statDead">0</strong></div>
|
||||
<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 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="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>
|
||||
</div>
|
||||
|
|
@ -122,8 +123,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<section class="card lineage-main-card">
|
||||
<div class="lineage-head">
|
||||
|
|
@ -135,6 +134,11 @@
|
|||
</div>
|
||||
<div id="archiveContent" class="archive-content lineage-main-content"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<button id="backToGameBtn" class="back-to-game-btn" type="button" hidden aria-label="ゲーム本体へ戻る">▲ 本体</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="fieldDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="fieldDialogTitle">
|
||||
|
|
@ -158,7 +162,7 @@
|
|||
<h2 id="creditsDialogTitle">クレジット</h2>
|
||||
<div class="credits-content">
|
||||
<p><strong>製作:</strong> 333</p>
|
||||
<p><strong>イラスト:</strong> むらさき氏 / いらすとや: <a href="https://www.irasutoya.com/" target="_blank" rel="noopener">https://www.irasutoya.com/</a></p>
|
||||
<p><strong>イラスト:</strong> むらさき氏 / いらすとや: <a href="https://www.irasutoya.com/" target="_blank" rel="noopener">https://www.irasutoya.com/</a></p>
|
||||
<p><strong>効果音:</strong> VOICEVOX: ずんだもん 効果音ラボ: <a href="https://soundeffect-lab.info/" target="_blank" rel="noopener">https://soundeffect-lab.info/</a></p>
|
||||
</div>
|
||||
<div class="field-dialog-actions credits-actions">
|
||||
|
|
@ -180,7 +184,7 @@
|
|||
|
||||
<div id="ecologyDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="ecologyDialogTitle">
|
||||
<div class="field-dialog-panel ecology-dialog-panel">
|
||||
<h2 id="ecologyDialogTitle">たりないの生態</h2>
|
||||
<h2 id="ecologyDialogTitle">たりないの生態</h2>
|
||||
<div id="ecologyGrid" class="ecology-grid"></div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="ecologyCloseBtn" class="btn" type="button">閉じる</button>
|
||||
|
|
@ -189,141 +193,141 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.15.27" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.27" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.27" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.27" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.27" defer></script>
|
||||
<script src="js/data.js?v=39.15.27" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.27" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.27" defer></script>
|
||||
<script src="js/math.js?v=39.15.27" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.27" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/assets.js?v=39.15.27" defer></script>
|
||||
<script src="js/audio.js?v=39.15.27" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.27" defer></script>
|
||||
<script src="js/render.js?v=39.15.27" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.27" defer></script>
|
||||
<script src="js/structures.js?v=39.15.27" defer></script>
|
||||
<script src="js/items.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_runtime.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.27" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.27" defer></script>
|
||||
<script src="js/ants.js?v=39.15.27" defer></script>
|
||||
<script src="js/health.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_action_planner_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.27" defer></script>
|
||||
<script src="js/world.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.27" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.27" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_update_phases.js?v=39.15.27" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.27" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.27" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.27" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.27" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.27" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.27" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.27" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.27" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.27" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.27" defer></script>
|
||||
<script src="js/patch_helpers.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.27" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.27" defer></script>
|
||||
<script src="js/main.js?v=39.15.27" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.27" defer></script>
|
||||
<script src="js/version.js?v=39.15.39" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.39" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.39" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.39" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.39" defer></script>
|
||||
<script src="js/data.js?v=39.15.39" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.39" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.39" defer></script>
|
||||
<script src="js/math.js?v=39.15.39" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.39" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/assets.js?v=39.15.39" defer></script>
|
||||
<script src="js/audio.js?v=39.15.39" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.39" defer></script>
|
||||
<script src="js/render.js?v=39.15.39" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.39" defer></script>
|
||||
<script src="js/structures.js?v=39.15.39" defer></script>
|
||||
<script src="js/items.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_runtime.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.39" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.39" defer></script>
|
||||
<script src="js/ants.js?v=39.15.39" defer></script>
|
||||
<script src="js/health.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_action_planner_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.39" defer></script>
|
||||
<script src="js/world.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.39" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.39" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_update_phases.js?v=39.15.39" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.39" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.39" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.39" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.39" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.39" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.39" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.39" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.39" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.39" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.39" defer></script>
|
||||
<script src="js/patch_helpers.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.39" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.39" defer></script>
|
||||
<script src="js/main.js?v=39.15.39" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.39" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const SPRITES = [
|
|||
{ id: "stress_dizzy", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u3050\u308b\u3050\u308b", path: "assets/sprites/tarinai_24_stress_dizzy.webp", actionOnly: true },
|
||||
{ id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.webp", actionOnly: true },
|
||||
{ id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true },
|
||||
{ id: "birth_ritual", label: "へこへこたりない", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
|
||||
{ id: "birth_ritual", label: "\u3078\u3053\u3078\u3053\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
|
||||
{ id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true },
|
||||
{ id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true },
|
||||
{ id: "zunchi_slave_alt", label: "\u305a\u3093\u3061\u3069\u308c\u3044 2", path: "assets/sprites/tarinai_30_zunchi_slave_alt.webp", displayOnly: true },
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const htmlEscape = global.TarinaiUIHelpers.htmlEscape;
|
||||
|
||||
function spritePathFor(type = "smile") {
|
||||
const sprites = (typeof SPRITES !== "undefined" ? SPRITES : (global.SPRITES || []));
|
||||
const meta = sprites.find(s => s.id === type) || sprites[0];
|
||||
return meta?.path || "assets/sprites/tarinai_01_smile.webp";
|
||||
}
|
||||
|
||||
function ensureStyles() {
|
||||
if (document.getElementById("freezeSystemStyles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "freezeSystemStyles";
|
||||
style.textContent = `
|
||||
.frozen-panel { grid-column: 1 / -1; margin-top: 8px; padding: 7px 8px; border: 1px solid rgba(82, 63, 43, 0.18); border-radius: 14px; background: rgba(244, 249, 255, 0.72); min-width: 0; }
|
||||
.frozen-panel-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-weight:800; font-size:12px; color:#4f5f70; margin-bottom:5px; }
|
||||
.frozen-panel-toggle { width:100%; border:0; background:transparent; color:inherit; font:inherit; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:0; cursor:pointer; text-align:left; }
|
||||
.frozen-panel-toggle::after { content:"\u6298\u308a\u7573\u307f"; font-size:11px; opacity:.62; font-weight:700; }
|
||||
.frozen-panel.collapsed .frozen-panel-toggle::after { content:"\u5c55\u958b"; }
|
||||
.frozen-panel-count { font-size:12px; opacity:.72; }
|
||||
.frozen-list { display:flex; gap:6px; overflow-x:auto; padding-bottom:2px; }
|
||||
.frozen-panel.collapsed .frozen-list { display:none; }
|
||||
.frozen-panel.collapsed { padding-bottom:7px; }
|
||||
.frozen-card { display:flex; align-items:center; gap:6px; flex:0 0 138px; min-width:138px; max-width:168px; border:1px solid rgba(72, 98, 124, .20); border-radius:11px; padding:5px 6px; background:rgba(255,255,255,.78); cursor:grab; color:#354657; text-align:left; box-shadow: 0 2px 7px rgba(41,53,65,.06); }
|
||||
.frozen-card:active { cursor:grabbing; }
|
||||
.frozen-card.pending-thaw { outline:2px solid rgba(70, 142, 210, .46); background:rgba(227, 242, 255, .86); }
|
||||
.frozen-card img { width:26px; height:26px; object-fit:contain; flex:none; filter: drop-shadow(0 2px 3px rgba(34,44,55,.14)); }
|
||||
.frozen-card-main { min-width:0; flex:1; }
|
||||
.frozen-card-name { display:block; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.frozen-card-meta { display:block; font-size:11px; opacity:.70; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.frozen-empty { font-size:12px; color:rgba(58,70,82,.68); padding:4px 2px; }
|
||||
canvas.freeze-drop-ready { outline: 3px solid rgba(70, 142, 210, .45); outline-offset: -3px; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
ensureStyles();
|
||||
let panel = document.getElementById("frozenPanel");
|
||||
if (panel) return panel;
|
||||
panel = document.createElement("section");
|
||||
panel.id = "frozenPanel";
|
||||
panel.className = "frozen-panel";
|
||||
panel.innerHTML = `
|
||||
<div class="frozen-panel-title"><button id="frozenPanelToggle" class="frozen-panel-toggle" type="button" aria-expanded="false"><span>\u51b7\u51cd\u5eab</span><span id="frozenPanelCount" class="frozen-panel-count">0</span></button></div>
|
||||
<div id="frozenList" class="frozen-list"></div>
|
||||
`;
|
||||
const storedCollapse = global.localStorage?.getItem("tarinaiFrozenPanelCollapsed");
|
||||
const collapsedByDefault = storedCollapse !== "0";
|
||||
if (collapsedByDefault) {
|
||||
panel.classList.add("collapsed");
|
||||
panel.querySelector("#frozenPanelToggle")?.setAttribute("aria-expanded", "false");
|
||||
} else {
|
||||
panel.querySelector("#frozenPanelToggle")?.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
const operateBody = document.querySelector('[data-tool-category="operate"] .tool-category-body');
|
||||
const anchor = operateBody || global.ui?.toolPalette || document.getElementById("toolPalette");
|
||||
if (operateBody) operateBody.appendChild(panel);
|
||||
else if (anchor?.parentNode) anchor.parentNode.insertBefore(panel, anchor.nextSibling);
|
||||
else document.body.appendChild(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
function render(entries = []) {
|
||||
const panel = ensurePanel();
|
||||
const listEl = panel.querySelector("#frozenList");
|
||||
const countEl = panel.querySelector("#frozenPanelCount");
|
||||
const list = Array.isArray(entries) ? entries : [];
|
||||
if (countEl) countEl.textContent = `${list.length}`;
|
||||
if (!listEl) return;
|
||||
if (!list.length) {
|
||||
listEl.innerHTML = `<div class="frozen-empty">\u51b7\u51cd\u4e2d\u306e\u305f\u308a\u306a\u3044\u306f\u3044\u307e\u305b\u3093\u3002\u51b7\u51cd\u3057\u305f\u305f\u308a\u306a\u3044\u306f\u30ea\u30bb\u30c3\u30c8\u3092\u7121\u8996\u3057\u3066\u4fdd\u5b58\u3067\u304d\u307e\u3059\u3002</div>`;
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = list.map(entry => {
|
||||
const data = entry.data || entry;
|
||||
const gen = data.generation ? `\u7b2c${data.generation}\u4e16\u4ee3` : "";
|
||||
const id = htmlEscape(entry.freezeId || entry.id || data.familyKey || "");
|
||||
return `<button class="frozen-card" type="button" draggable="true" data-frozen-id="${id}" title="\u30c9\u30e9\u30c3\u30b0\u3057\u3066\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3059">
|
||||
<img src="${htmlEscape(spritePathFor(data.type || "smile"))}" alt="">
|
||||
<span class="frozen-card-main"><span class="frozen-card-name">${htmlEscape(data.name || "\u305f\u308a\u306a\u3044")}</span><span class="frozen-card-meta">${htmlEscape(gen)}</span></span>
|
||||
</button>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function markPending(id = "") {
|
||||
const panel = ensurePanel();
|
||||
for (const c of panel.querySelectorAll(".frozen-card")) c.classList.toggle("pending-thaw", c.dataset.frozenId === id);
|
||||
}
|
||||
|
||||
function clearPending() {
|
||||
document.querySelectorAll(".frozen-card.pending-thaw").forEach(el => el.classList.remove("pending-thaw"));
|
||||
}
|
||||
|
||||
function bind({ onSelect, onDrop } = {}) {
|
||||
const panel = ensurePanel();
|
||||
if (panel.dataset.freezeBound !== "1") {
|
||||
panel.dataset.freezeBound = "1";
|
||||
panel.addEventListener("dragstart", (e) => {
|
||||
const card = e.target.closest(".frozen-card[data-frozen-id]");
|
||||
if (!card) return;
|
||||
const id = card.dataset.frozenId || "";
|
||||
e.dataTransfer?.setData("text/plain", `tarinai-frozen:${id}`);
|
||||
e.dataTransfer?.setData("application/x-tarinai-frozen", id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
global.document?.querySelector("canvas")?.classList.add("freeze-drop-ready");
|
||||
});
|
||||
panel.addEventListener("dragend", () => global.document?.querySelector("canvas")?.classList.remove("freeze-drop-ready"));
|
||||
panel.addEventListener("click", (e) => {
|
||||
const toggle = e.target.closest("#frozenPanelToggle");
|
||||
if (toggle) {
|
||||
const collapsed = panel.classList.toggle("collapsed");
|
||||
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
try { global.localStorage?.setItem("tarinaiFrozenPanelCollapsed", collapsed ? "1" : "0"); } catch (_) {}
|
||||
global.audio?.uiFold?.();
|
||||
return;
|
||||
}
|
||||
const card = e.target.closest(".frozen-card[data-frozen-id]");
|
||||
if (!card) return;
|
||||
const id = card.dataset.frozenId || "";
|
||||
markPending(id);
|
||||
onSelect?.(id);
|
||||
});
|
||||
}
|
||||
const canvas = global.canvas || document.getElementById("gameCanvas");
|
||||
if (canvas && canvas.dataset.freezeDropBound !== "1") {
|
||||
canvas.dataset.freezeDropBound = "1";
|
||||
canvas.addEventListener("dragover", (e) => {
|
||||
const text = e.dataTransfer?.types?.includes?.("application/x-tarinai-frozen") || [...(e.dataTransfer?.types || [])].includes("text/plain");
|
||||
if (!text) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
canvas.classList.add("freeze-drop-ready");
|
||||
});
|
||||
canvas.addEventListener("dragleave", () => canvas.classList.remove("freeze-drop-ready"));
|
||||
canvas.addEventListener("drop", (e) => {
|
||||
const id = e.dataTransfer?.getData("application/x-tarinai-frozen") || String(e.dataTransfer?.getData("text/plain") || "").replace(/^tarinai-frozen:/, "");
|
||||
if (!id) return;
|
||||
e.preventDefault();
|
||||
canvas.classList.remove("freeze-drop-ready");
|
||||
const p = typeof global.screenToWorld === "function" ? global.screenToWorld(e) : (() => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = e.clientX - rect.left;
|
||||
const sy = e.clientY - rect.top;
|
||||
const worldRef = global.world;
|
||||
return worldRef?.screenToWorld ? worldRef.screenToWorld(sx, sy) : { x: sx, y: sy, inside: true };
|
||||
})();
|
||||
onDrop?.(id, p.x, p.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
global.TarinaiFreezePanel = {
|
||||
ensurePanel,
|
||||
render,
|
||||
bind,
|
||||
markPending,
|
||||
clearPending,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const Snapshot = global.TarinaiSnapshot;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for freeze_snapshot_adapter.js");
|
||||
const clonePlain = Snapshot.clonePlain;
|
||||
|
||||
function snapshotEntity(worldRef, entity, skip = new Set(["world", "target", "panicTarget", "targetRef"])) {
|
||||
const data = Snapshot.copyOwnData(entity, skip);
|
||||
const targetRef = Snapshot.refFor(worldRef, entity?.target);
|
||||
const panicTargetRef = Snapshot.refFor(worldRef, entity?.panicTarget);
|
||||
const targetRefObject = Snapshot.refFor(worldRef, entity?.targetRef);
|
||||
if (targetRef) data.__targetRef = targetRef;
|
||||
if (panicTargetRef) data.__panicTargetRef = panicTargetRef;
|
||||
if (targetRefObject) data.__targetRefObject = targetRefObject;
|
||||
return data;
|
||||
}
|
||||
|
||||
function restoreTarinaiData(worldRef, data, opts = {}) {
|
||||
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
|
||||
if (!TarinaiClass) throw new Error("Tarinai is not available");
|
||||
const t = new TarinaiClass(worldRef, data || {});
|
||||
Snapshot.applyOwnData(t, data || {}, new Set(["world", "target", "panicTarget", "targetRef"]));
|
||||
t.world = worldRef;
|
||||
if (opts.clearTransient !== false) {
|
||||
t.target = null;
|
||||
t.panicTarget = null;
|
||||
t.sleeping = false;
|
||||
t.insideNestBoxId = "";
|
||||
}
|
||||
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
|
||||
return t;
|
||||
}
|
||||
|
||||
function makeEntry(t, worldRef = global.world) {
|
||||
const data = snapshotEntity(worldRef, t, new Set(["world", "target", "panicTarget", "targetRef"]));
|
||||
data.sleeping = false;
|
||||
data.insideNestBoxId = "";
|
||||
data.state = data.state === "sleep" || data.state === "seek_bed" ? "idle" : (data.state || "idle");
|
||||
data.targetKey = "";
|
||||
const id = (global.crypto?.randomUUID ? global.crypto.randomUUID() : `frozen-${Date.now()}-${Math.random()}`);
|
||||
return {
|
||||
freezeId: id,
|
||||
frozenAt: Date.now(),
|
||||
worldTime: worldRef?.time || 0,
|
||||
name: t.name || "\u305f\u308a\u306a\u3044",
|
||||
familyKey: t.familyKey || "",
|
||||
liveId: t.id || "",
|
||||
liveToken: t.liveToken || 0,
|
||||
type: t.type || "smile",
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function updateLiveIdAfterThaw(worldRef, t) {
|
||||
if (!worldRef.liveTarinai) worldRef.liveTarinai = new Map();
|
||||
const id = t.id || "";
|
||||
const conflict = id && worldRef.liveTarinai.has(id);
|
||||
if (!id || conflict) {
|
||||
worldRef.assignTarinaiLiveId?.(t);
|
||||
return;
|
||||
}
|
||||
worldRef.liveTarinai.set(id, { token: t.liveToken || 0, target: t });
|
||||
const m = /^t(\d+)$/.exec(id);
|
||||
if (m) worldRef.liveIdNext = Math.max(worldRef.liveIdNext || 1, Number(m[1]) + 1);
|
||||
worldRef.liveIdSerial = Math.max(worldRef.liveIdSerial || 0, t.liveToken || 0);
|
||||
}
|
||||
|
||||
function thawEntry(entry, x, y, worldRef = global.world) {
|
||||
const data = clonePlain(entry?.data || entry) || {};
|
||||
const t = restoreTarinaiData(worldRef, data, { clearTransient: true });
|
||||
t.dead = false;
|
||||
t.deathReason = "";
|
||||
t.x = clamp(Number(x) || worldRef.w / 2, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
|
||||
t.y = clamp(Number(y) || worldRef.h / 2, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
|
||||
t.vx = 0;
|
||||
t.vy = 0;
|
||||
t.target = null;
|
||||
t.panicTarget = null;
|
||||
t.insideNestBoxId = "";
|
||||
t.sleeping = false;
|
||||
if (t.state === "sleep" || t.state === "seek_bed") t.goIdle?.("\u89e3\u51cd\u3055\u308c\u3066\u5c11\u3057\u9a5a\u3044\u3066\u3044\u308b");
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
|
||||
t.thought = "\u89e3\u51cd\u3055\u308c\u3066\u5c11\u3057\u9a5a\u3044\u3066\u3044\u308b";
|
||||
t.frozenAt = 0;
|
||||
updateLiveIdAfterThaw(worldRef, t);
|
||||
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
|
||||
return t;
|
||||
}
|
||||
|
||||
global.TarinaiFreezeSnapshotAdapter = {
|
||||
snapshotEntity,
|
||||
restoreTarinaiData,
|
||||
makeEntry,
|
||||
thawEntry,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const Snapshot = global.TarinaiSnapshot;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for freeze_store.js");
|
||||
const STORAGE_KEY = "tarinai_frozen_storage_v1";
|
||||
const clonePlain = Snapshot.clonePlain;
|
||||
|
||||
function frozenList(worldRef = global.world) {
|
||||
if (!worldRef) return [];
|
||||
if (!Array.isArray(worldRef.frozenTarinai)) worldRef.frozenTarinai = [];
|
||||
return worldRef.frozenTarinai;
|
||||
}
|
||||
|
||||
function save(worldRef = global.world) {
|
||||
try {
|
||||
global.localStorage?.setItem(STORAGE_KEY, JSON.stringify(frozenList(worldRef)));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function load(worldRef = global.world) {
|
||||
if (!worldRef) return [];
|
||||
try {
|
||||
const raw = global.localStorage?.getItem(STORAGE_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
if (Array.isArray(parsed) && !worldRef.frozenTarinai?.length) worldRef.frozenTarinai = clonePlain(parsed) || [];
|
||||
} catch (_) {
|
||||
if (!Array.isArray(worldRef.frozenTarinai)) worldRef.frozenTarinai = [];
|
||||
}
|
||||
return frozenList(worldRef);
|
||||
}
|
||||
|
||||
function ensure(worldRef = global.world) {
|
||||
if (!Array.isArray(worldRef?.frozenTarinai)) worldRef.frozenTarinai = [];
|
||||
return frozenList(worldRef);
|
||||
}
|
||||
|
||||
global.TarinaiFreezeStore = {
|
||||
STORAGE_KEY,
|
||||
frozenList,
|
||||
load,
|
||||
save,
|
||||
ensure,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
function deps() {
|
||||
return {
|
||||
Store: global.TarinaiFreezeStore,
|
||||
Adapter: global.TarinaiFreezeSnapshotAdapter,
|
||||
Panel: global.TarinaiFreezePanel,
|
||||
};
|
||||
}
|
||||
function depsReady() {
|
||||
const d = deps();
|
||||
return Boolean(d.Store && d.Adapter && d.Panel);
|
||||
}
|
||||
function warnMissing(context) {
|
||||
if (global.console?.warn) console.warn(`freeze_system.js dependencies are not available yet (${context})`);
|
||||
}
|
||||
|
||||
let pendingThawId = "";
|
||||
|
||||
function frozenList(worldRef = global.world) {
|
||||
const { Store } = deps();
|
||||
if (!Store?.frozenList) return [];
|
||||
return Store.frozenList(worldRef);
|
||||
}
|
||||
|
||||
function renderFrozenPanel(worldRef = global.world) {
|
||||
const { Panel } = deps();
|
||||
if (!Panel?.render) return;
|
||||
Panel.render(frozenList(worldRef));
|
||||
}
|
||||
|
||||
function saveFrozenStore(worldRef = global.world) {
|
||||
const { Store } = deps();
|
||||
if (!Store?.save) return;
|
||||
Store.save(worldRef);
|
||||
}
|
||||
|
||||
function loadFrozenStore(worldRef = global.world) {
|
||||
const { Store } = deps();
|
||||
return Store?.load ? Store.load(worldRef) : [];
|
||||
}
|
||||
|
||||
function refreshWorldAfterFreezeChange(worldRef = global.world, selected = null) {
|
||||
if (!worldRef) return;
|
||||
if (selected !== undefined) worldRef.selected = selected;
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.familyTreeDirty = true;
|
||||
worldRef.updateItemCounts?.();
|
||||
worldRef.rebuildSpatial?.(true);
|
||||
saveFrozenStore(worldRef);
|
||||
worldRef.emit?.("freeze:changed", { world: worldRef, selected, renderWorld: true });
|
||||
}
|
||||
|
||||
function freezeTarinai(t, worldRef = global.world) {
|
||||
const { Adapter } = deps();
|
||||
if (!Adapter?.makeEntry) { warnMissing("freezeTarinai"); return false; }
|
||||
if (!t || t.dead || !worldRef) return false;
|
||||
const entry = Adapter.makeEntry(t, worldRef);
|
||||
worldRef.clearLiveReferencesTo?.(t);
|
||||
if (t.id && worldRef.liveTarinai?.has?.(t.id)) worldRef.liveTarinai.delete(t.id);
|
||||
worldRef.tarinai = (worldRef.tarinai || []).filter(o => o !== t);
|
||||
if (worldRef.selected === t) worldRef.selected = null;
|
||||
frozenList(worldRef).push(entry);
|
||||
refreshWorldAfterFreezeChange(worldRef, worldRef.selected);
|
||||
global.showToast?.(`${t.name}を冷凍保存しました。`);
|
||||
worldRef.log?.(`${t.name}をフィールド外で冷凍保存した。`, "observe", { participants: [t] });
|
||||
worldRef.emit?.("tarinai:frozen", { entry, tarinai: t });
|
||||
return true;
|
||||
}
|
||||
|
||||
function thawFrozen(freezeId, x, y, worldRef = global.world) {
|
||||
const { Adapter } = deps();
|
||||
if (!Adapter?.thawEntry) { warnMissing("thawFrozen"); return false; }
|
||||
const list = frozenList(worldRef);
|
||||
const index = list.findIndex(e => (e.freezeId || e.id) === freezeId);
|
||||
if (index < 0 || !worldRef) return false;
|
||||
const entry = list[index];
|
||||
let t = null;
|
||||
try {
|
||||
t = Adapter.thawEntry(entry, x, y, worldRef);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
worldRef.tarinai.push(t);
|
||||
worldRef.recordFamily?.(t);
|
||||
list.splice(index, 1);
|
||||
refreshWorldAfterFreezeChange(worldRef, t);
|
||||
global.showToast?.(`${t.name}を解凍しました。`);
|
||||
worldRef.log?.(`${t.name}を解凍してフィールドに戻した。`, "birth", { participants: [t] });
|
||||
worldRef.emit?.("tarinai:thawed", { entry, tarinai: t });
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindFreezeSystem() {
|
||||
if (!depsReady()) {
|
||||
warnMissing("bindFreezeSystem");
|
||||
global.setTimeout?.(() => { if (depsReady()) bindFreezeSystem(); }, 0);
|
||||
return false;
|
||||
}
|
||||
const { Panel } = deps();
|
||||
const worldRef = global.world;
|
||||
loadFrozenStore(worldRef);
|
||||
worldRef?.emit?.("freeze:changed", { world: worldRef, renderWorld: false });
|
||||
Panel.bind({
|
||||
onSelect(id) {
|
||||
pendingThawId = id || "";
|
||||
global.showToast?.("フィールドをクリックすると解凍します。ドラッグ&ドロップでも配置できます。");
|
||||
},
|
||||
onDrop(id, x, y) {
|
||||
thawFrozen(id, x, y, global.world);
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function tryPendingThawAt(x, y, worldRef = global.world) {
|
||||
if (!pendingThawId) return false;
|
||||
const id = pendingThawId;
|
||||
pendingThawId = "";
|
||||
deps().Panel?.clearPending?.();
|
||||
return thawFrozen(id, x, y, worldRef);
|
||||
}
|
||||
|
||||
function afterWorldRestored(worldRef = global.world) {
|
||||
deps().Store?.ensure?.(worldRef);
|
||||
saveFrozenStore(worldRef);
|
||||
worldRef?.emit?.("freeze:changed", { world: worldRef, renderWorld: false });
|
||||
}
|
||||
|
||||
function patchWorldPrototype() {
|
||||
const apply = (proto) => {
|
||||
const originalHandleClick = proto.handleClick;
|
||||
proto.handleClick = function (x, y) {
|
||||
if (tryPendingThawAt(x, y, this)) return;
|
||||
return originalHandleClick.call(this, x, y);
|
||||
};
|
||||
};
|
||||
if (global.TarinaiPatcher?.patchWorld?.("freezeSystem", apply)) return;
|
||||
const World = global.World;
|
||||
if (!World || World.prototype.__freezeSystemPatched) return;
|
||||
World.prototype.__freezeSystemPatched = true;
|
||||
apply(World.prototype);
|
||||
}
|
||||
|
||||
patchWorldPrototype();
|
||||
|
||||
global.TarinaiFreezeSystem = {
|
||||
bindFreezeSystem,
|
||||
renderFrozenPanel,
|
||||
freezeTarinai,
|
||||
thawFrozen,
|
||||
frozenList,
|
||||
loadFrozenStore,
|
||||
saveFrozenStore,
|
||||
afterWorldRestored,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
const GROUND_TYPE_ORDER = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
const GROUND_TYPES = Object.freeze({
|
||||
// デフォルト地面ID。表示名は「砂」。
|
||||
soil: Object.freeze({ id: "soil", label: "砂", description: "普通の地面。", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
dirt: Object.freeze({ id: "dirt", label: "土", description: "草がよく育つ地面。", stressMultiplier: 1.0, grassMultiplier: 1.75, grassGrowthMultiplier: 2.45 }),
|
||||
concrete: Object.freeze({ id: "concrete", label: "コンクリート", description: "草が生えない", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }),
|
||||
blanket: Object.freeze({ id: "blanket", label: "ふわふわ毛布", description: "ストレスが増えにくい", stressMultiplier: 0.5, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
foot_massage: Object.freeze({ id: "foot_massage", label: "足つぼ", description: "青いまだら地面。ストレスが増えやすい", stressMultiplier: 1.5, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
ice: Object.freeze({ id: "ice", label: "氷", description: "つるつる滑る。草は生えにくい", stressMultiplier: 1.1, grassMultiplier: 0.15, grassGrowthMultiplier: 0.35 }),
|
||||
laboratory: Object.freeze({ id: "laboratory", label: "実験室", description: "常に晴れ。草は生えず、ずんちはすぐ消える。", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0, fixedWeather: "sunny", zunchiStageMultiplier: 8.0, zunchiDecayMultiplier: 520.0 }),
|
||||
// \u30c7\u30d5\u30a9\u30eb\u30c8\u5730\u9762ID\u3002\u8868\u793a\u540d\u306f\u300c\u7802\u300d\u3002
|
||||
soil: Object.freeze({ id: "soil", label: "\u7802", description: "\u666e\u901a\u306e\u5730\u9762\u3002", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
dirt: Object.freeze({ id: "dirt", label: "\u571f", description: "\u8349\u304c\u3088\u304f\u80b2\u3064\u5730\u9762\u3002", stressMultiplier: 1.0, grassMultiplier: 1.75, grassGrowthMultiplier: 2.45 }),
|
||||
concrete: Object.freeze({ id: "concrete", label: "\u30b3\u30f3\u30af\u30ea\u30fc\u30c8", description: "\u8349\u304c\u751f\u3048\u306a\u3044", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }),
|
||||
blanket: Object.freeze({ id: "blanket", label: "\u3075\u308f\u3075\u308f\u6bdb\u5e03", description: "\u30b9\u30c8\u30ec\u30b9\u304c\u5897\u3048\u306b\u304f\u3044", stressMultiplier: 0.5, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
foot_massage: Object.freeze({ id: "foot_massage", label: "\u8db3\u3064\u307c", description: "\u9752\u3044\u307e\u3060\u3089\u5730\u9762\u3002\u30b9\u30c8\u30ec\u30b9\u304c\u5897\u3048\u3084\u3059\u3044", stressMultiplier: 1.5, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
ice: Object.freeze({ id: "ice", label: "\u6c37", description: "\u3064\u308b\u3064\u308b\u6ed1\u308b\u3002\u8349\u306f\u751f\u3048\u306b\u304f\u3044", stressMultiplier: 1.1, grassMultiplier: 0.15, grassGrowthMultiplier: 0.35 }),
|
||||
laboratory: Object.freeze({ id: "laboratory", label: "\u5b9f\u9a13\u5ba4", description: "\u5e38\u306b\u6674\u308c\u3002\u8349\u306f\u751f\u3048\u305a\u3001\u305a\u3093\u3061\u306f\u3059\u3050\u6d88\u3048\u308b\u3002", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0, fixedWeather: "sunny", zunchiStageMultiplier: 8.0, zunchiDecayMultiplier: 520.0 }),
|
||||
});
|
||||
|
||||
function groundDefinition(id = "soil") {
|
||||
|
|
@ -21,7 +21,7 @@ function groundExists(id = "soil") {
|
|||
}
|
||||
|
||||
function groundLabel(id = "soil") {
|
||||
return groundDefinition(id).label || "砂";
|
||||
return groundDefinition(id).label || "\u7802";
|
||||
}
|
||||
|
||||
function groundDescription(id = "soil") {
|
||||
|
|
@ -67,8 +67,8 @@ function nextGroundTypeId(current = "soil") {
|
|||
|
||||
function groundTooltipText(id = "soil") {
|
||||
const def = groundDefinition(id);
|
||||
const desc = def.description || "クリックで切り替え";
|
||||
return `${def.label || "じめん"}\n${desc}`;
|
||||
const desc = def.description || "\u30af\u30ea\u30c3\u30af\u3067\u5207\u308a\u66ff\u3048";
|
||||
return `${def.label || "\u3058\u3081\u3093"}\n${desc}`;
|
||||
}
|
||||
|
||||
window.TarinaiGround = Object.freeze({
|
||||
|
|
|
|||
120
js/health.js
120
js/health.js
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const HEALTH = (() => {
|
||||
const MAJOR_DAMAGE_WINDOW = 24;
|
||||
const MAJOR_DAMAGE_REACTION_WINDOW = 1.0;
|
||||
const WEAKENED_TRACE_WINDOW = 45;
|
||||
const WEAKENED_SUFFIX = "\u3067\u8870\u5f31";
|
||||
const CAUSES = Object.freeze({
|
||||
|
|
@ -11,10 +12,10 @@ const HEALTH = (() => {
|
|||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e",
|
||||
firecracker: "\u7206\u7af9",
|
||||
genkotsu: "\u3052\u3093\u3053\u3064",
|
||||
shooting: "射撃",
|
||||
stoneCrush: "石に潰された",
|
||||
weakness: "衰弱",
|
||||
poisonBlock: "毒ブロック",
|
||||
shooting: "\u5c04\u6483",
|
||||
stoneCrush: "\u77f3\u306b\u6f70\u3055\u308c\u305f",
|
||||
weakness: "\u8870\u5f31",
|
||||
poisonBlock: "\u6bd2\u30d6\u30ed\u30c3\u30af",
|
||||
accident: "\u4e8b\u6545",
|
||||
collision: "\u885d\u7a81",
|
||||
outOfBounds: "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664",
|
||||
|
|
@ -33,18 +34,18 @@ const HEALTH = (() => {
|
|||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/射撃|shoot/.test(text)) return CAUSES.shooting;
|
||||
if (/石に潰された|石.*潰|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush;
|
||||
const collisionWith = text.match(/([^、。\s]+)に衝突/);
|
||||
if (collisionWith) return `${collisionWith[1]}に衝突`;
|
||||
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/);
|
||||
if (collisionWith) return `${collisionWith[1]}\u3068\u306e\u885d\u7a81`;
|
||||
if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight;
|
||||
if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu;
|
||||
if (/毒ブロック|poison.?block/.test(text)) return CAUSES.poisonBlock;
|
||||
if (/\u6bd2\u30d6\u30ed\u30c3\u30af|poison.?block/.test(text)) return CAUSES.poisonBlock;
|
||||
if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug;
|
||||
if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug;
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
||||
if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds;
|
||||
if (/\u885d\u7a81|collision|急激な速度変化/.test(text)) return CAUSES.collision;
|
||||
if (/\u885d\u7a81|collision|\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316/.test(text)) return CAUSES.collision;
|
||||
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke;
|
||||
if (/\u306d\u3080\u308a\u75c5|sleep.*disease/.test(text)) return CAUSES.sleepDisease;
|
||||
if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease;
|
||||
|
|
@ -146,6 +147,89 @@ const HEALTH = (() => {
|
|||
}
|
||||
|
||||
|
||||
const MAJOR_DAMAGE_REACTIONS = Object.freeze([
|
||||
{ id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" },
|
||||
{ id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" },
|
||||
{ id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" },
|
||||
{ id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" },
|
||||
]);
|
||||
|
||||
function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") {
|
||||
const world = t?.world;
|
||||
if (!world) return false;
|
||||
// Use the normal Tarinai bubble path first so positioning, draw order, and
|
||||
// throttling match other player-visible speech bubbles. Major-damage
|
||||
// bubbles are forced because they must win over incidental bubbles such as
|
||||
// \u300c\u3076\u308a\u3085\u3063\u300d fired by the same damage event.
|
||||
if (typeof t.bubble === "function" && t.bubble(text, 1.65, color, { force: true })) {
|
||||
t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55);
|
||||
return true;
|
||||
}
|
||||
if (typeof Effect === "undefined") return false;
|
||||
if ((world.effectCounts?.bubble || 0) >= (CONFIG?.bubbleLimit || 80)) return false;
|
||||
const r = Math.max(16, t.radius || 22);
|
||||
world.effects?.push(new Effect("bubble", t.x, t.y - r * 1.38, {
|
||||
vx: (Math.random() - 0.5) * 2.4,
|
||||
vy: -4.8 - Math.random() * 2.8,
|
||||
life: 2.4 + Math.random() * 0.6,
|
||||
size: 9,
|
||||
text,
|
||||
color,
|
||||
}));
|
||||
world.effectCounts.bubble = (world.effectCounts.bubble || 0) + 1;
|
||||
t.nextBubbleAt = Math.max(t.nextBubbleAt || 0, (world.time || 0) + 1.55);
|
||||
t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55);
|
||||
world.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeTriggerMajorDamageReaction(t, actualLoss = 0, cause = "") {
|
||||
if (!t || !t.world || t.dead) return false;
|
||||
const maxEnergy = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
||||
const loss = Math.max(0, Number(actualLoss) || 0);
|
||||
if (loss <= 0.01) return false;
|
||||
const now = t.world.time || 0;
|
||||
const cutoff = now - MAJOR_DAMAGE_REACTION_WINDOW;
|
||||
const entries = Array.isArray(t.recentDamageWindow) ? t.recentDamageWindow : [];
|
||||
entries.push({ at: now, amount: loss, cause: cause || "" });
|
||||
const kept = [];
|
||||
let total = 0;
|
||||
let dominant = { amount: 0, cause: cause || "" };
|
||||
for (const entry of entries) {
|
||||
const at = Number(entry?.at || 0);
|
||||
const amount = Math.max(0, Number(entry?.amount) || 0);
|
||||
if (at < cutoff || amount <= 0) continue;
|
||||
kept.push({ at, amount, cause: entry?.cause || "" });
|
||||
total += amount;
|
||||
if (amount >= dominant.amount) dominant = { amount, cause: entry?.cause || "" };
|
||||
}
|
||||
t.recentDamageWindow = kept.slice(-20);
|
||||
if (total < maxEnergy * 0.30) return false;
|
||||
const majorCause = causeLabel(dominant.cause || cause) || causeLabel(cause) || CAUSES.accident;
|
||||
t.lastMajorDamageCause = majorCause;
|
||||
t.lastMajorDamageAmount = total;
|
||||
t.lastMajorDamageAt = now;
|
||||
if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false;
|
||||
t.nextMajorDamageVoiceAt = now + 1.0;
|
||||
const list = MAJOR_DAMAGE_REACTIONS;
|
||||
const reaction = list[Math.floor(Math.random() * list.length)] || list[0];
|
||||
audio.play?.(reaction.id, { category: "voice", minGap: 0.85 });
|
||||
spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.55);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 2.6);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.55);
|
||||
if (t.enterPanic) t.enterPanic({
|
||||
threat: null,
|
||||
reason: "\u5927\u304d\u306a\u30c0\u30e1\u30fc\u30b8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
fear: 1.12,
|
||||
wake: true,
|
||||
hurtTimer: 1.45,
|
||||
surpriseTimer: 0.55,
|
||||
cause: "major_damage",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeReleaseZunchiFromDamage(t, actualLoss = 0, cause = "") {
|
||||
if (!t || !t.world || t.dead) return false;
|
||||
const loss = Math.max(0, Number(actualLoss) || 0);
|
||||
|
|
@ -157,7 +241,7 @@ const HEALTH = (() => {
|
|||
t.nextDamageZunchiAt = now + Math.max(0.55, 1.55 - Math.min(0.9, loss / 38));
|
||||
if (t.hasLodgedPinEffect?.("blocksZunchi")) {
|
||||
t.oshiriByoZunchiStock = Math.min(24, Math.max(0, t.oshiriByoZunchiStock || 0) + 1);
|
||||
t.thought = "ダメージでずんちを我慢している";
|
||||
t.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b";
|
||||
return false;
|
||||
}
|
||||
const side = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
|
|
@ -168,10 +252,14 @@ const HEALTH = (() => {
|
|||
t.poopCount = (t.poopCount || 0) + 1;
|
||||
t.digest = Math.max(0, Number(t.digest || 0) - 1.0);
|
||||
t.nextPoopBubbleAt = now + 2.0;
|
||||
t.bubble?.("ぶりゅっ", 1.1, "rgba(74,91,50,0.78)", { force: true });
|
||||
// If this zunchi release was caused by a major-damage event, keep the pain
|
||||
// voice bubble visible and suppress the incidental \u300c\u3076\u308a\u3085\u3063\u300d bubble.
|
||||
if (now >= Number(t._damageBubblePriorityUntil || -999)) {
|
||||
t.bubble?.("\u3076\u308a\u3085\u3063", 1.1, "rgba(74,91,50,0.78)", { force: true });
|
||||
}
|
||||
if (now > Number(t.nextDamageZunchiLogAt || -999)) {
|
||||
t.nextDamageZunchiLogAt = now + 10;
|
||||
t.world.log?.(`${t.name}はダメージでずんちを放出した。`, "accident", { participants: [t] });
|
||||
t.world.log?.(`${t.name}\u306f\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [t] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -189,7 +277,8 @@ const HEALTH = (() => {
|
|||
const plushie = (t.world?.itemsOfType?.("plushie") || t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id);
|
||||
if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null);
|
||||
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
||||
audio.damage?.(actualLoss);
|
||||
const majorDamageReacted = maybeTriggerMajorDamageReaction(t, actualLoss, cause || reason);
|
||||
if (!majorDamageReacted) audio.damage?.(actualLoss);
|
||||
maybeReleaseZunchiFromDamage(t, actualLoss, cause || reason);
|
||||
if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
if (t.wakeFromDamage) t.wakeFromDamage(cause || reason);
|
||||
|
|
@ -197,7 +286,7 @@ const HEALTH = (() => {
|
|||
t.showHpBar();
|
||||
}
|
||||
if (t.energy <= 0.5) {
|
||||
t.die(normalizeDeathReason(t, reason || cause, { fromDamage: true }));
|
||||
t.die(reason || cause, { fromDamage: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,6 +310,7 @@ const HEALTH = (() => {
|
|||
applyDamage,
|
||||
briefDeathReason,
|
||||
maybeReleaseZunchiFromDamage,
|
||||
maybeTriggerMajorDamageReaction,
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const MODES = Object.freeze(["auto", "camera", "tool", "family"]);
|
||||
const MODES = Object.freeze(["auto", "camera", "tool"]);
|
||||
const STORAGE_KEY = "tarinai_mobile_input_mode_v1";
|
||||
|
||||
class InputModeManager {
|
||||
|
|
@ -45,9 +45,12 @@
|
|||
const hasHover = Boolean(mm?.("(hover: hover)")?.matches);
|
||||
const noHover = Boolean(mm?.("(hover: none)")?.matches);
|
||||
const isTouchDevice = Number(nav.maxTouchPoints || 0) > 0 || "ontouchstart" in global;
|
||||
const isCompactViewport = shortSide <= 720 && longSide <= 1100;
|
||||
const isCompactViewport = shortSide <= 760 && longSide <= 1200;
|
||||
const userAgentMobile = /Android|iPhone|iPod|iPad|Mobile|Silk|Tablet/i.test(ua);
|
||||
const touchFirst = Boolean(pointerCoarse && noHover && !hasHover && isTouchDevice && (isCompactViewport || userAgentMobile));
|
||||
// v39.15.35: Some mobile browsers report hover support or a desktop-like
|
||||
// viewport. Treat compact touch devices as touch-first so the on-canvas
|
||||
// mobile mode UI is reliably available.
|
||||
const touchFirst = Boolean(isTouchDevice && (isCompactViewport || userAgentMobile) && (pointerCoarse || noHover || userAgentMobile || shortSide <= 820));
|
||||
this.classification = Object.freeze({
|
||||
isTouchDevice,
|
||||
isCompactViewport,
|
||||
|
|
@ -95,7 +98,7 @@
|
|||
|
||||
canFamilyTreeCaptureTouch() {
|
||||
if (!this.classification?.touchFirst) return true;
|
||||
return this.currentMode === "auto" || this.currentMode === "family";
|
||||
return true;
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@
|
|||
const searchRadius = Math.max(150, (item.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 120);
|
||||
const items = worldRef.nearbyItems(item.x, item.y, searchRadius) || [];
|
||||
const seen = new Set(items);
|
||||
// 大きな機構は中心が近傍セル外でも当たる可能性がある。
|
||||
// 中心ではなく外接半径で追加走査して、長い棒・往復幅にも衝突させる。
|
||||
// \u5927\u304d\u306a\u6a5f\u69cb\u306f\u4e2d\u5fc3\u304c\u8fd1\u508d\u30bb\u30eb\u5916\u3067\u3082\u5f53\u305f\u308b\u53ef\u80fd\u6027\u304c\u3042\u308b\u3002
|
||||
// \u4e2d\u5fc3\u3067\u306f\u306a\u304f\u5916\u63a5\u534a\u5f84\u3067\u8ffd\u52a0\u8d70\u67fb\u3057\u3066\u3001\u9577\u3044\u68d2\u30fb\u5f80\u5fa9\u5e45\u306b\u3082\u885d\u7a81\u3055\u305b\u308b\u3002
|
||||
if (Array.isArray(worldRef.items)) {
|
||||
for (const it of worldRef.items) {
|
||||
if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || seen.has(it)) continue;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
syncRoles(item);
|
||||
if (typeof applyStoredPin === "function") applyStoredPin(item, worldRef);
|
||||
item.duplicatorPinFlash = Math.max(0, (item.duplicatorPinFlash || 0) - dt);
|
||||
// 39.15.27: auto-loading by contact is intentionally disabled.
|
||||
// 39.15.34: auto-loading by contact is intentionally disabled.
|
||||
// A duplicator accepts a stored item only through the explicit player
|
||||
// click path (World.directSetDuplicatorAt / direct:true). This prevents
|
||||
// dropped or pushed items from accidentally changing its setup.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
case "poison_block":
|
||||
case "reciprocator":
|
||||
case "gate_fence":
|
||||
case "sprinkler":
|
||||
case "rope":
|
||||
case "rod":
|
||||
return global.TarinaiItemDynamicToolSystem?.update(item, dt, worldRef) !== false;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,48 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
function sprinklerRange(item) {
|
||||
return Math.max(96, (Number(item?.r || 28) || 28) * 4.6);
|
||||
}
|
||||
|
||||
function updateSprinkler(item, dt, worldRef) {
|
||||
if (!item || item.dead || !worldRef) return false;
|
||||
item.amount = Math.max(item.amount || 0, 999);
|
||||
item.sprinklerSpin = (Number(item.sprinklerSpin || 0) || 0) + dt * 4.2;
|
||||
item.sprinklerFlash = Math.max(0, (Number(item.sprinklerFlash || 0) || 0) - dt * 1.8);
|
||||
item.sprinklerTimer = Math.max(0, (Number(item.sprinklerTimer || 0) || 0) - dt);
|
||||
if (item.sprinklerTimer > 0) return true;
|
||||
item.sprinklerTimer = global.deterministicRange ? global.deterministicRange(worldRef, "sprinkler-next", 0.55, 1.25, item) : (0.55 + Math.random() * 0.70);
|
||||
const range = sprinklerRange(item);
|
||||
const nearby = worldRef.nearbyItems?.(item.x, item.y, range, true) || worldRef.items || [];
|
||||
const targets = [];
|
||||
for (const it of nearby) {
|
||||
if (!it || it.dead || it === item) continue;
|
||||
if (!(it.type === "zunchi" || it.type === "trace" || it.type === "ant_corpse" || it.type === "splat")) continue;
|
||||
const r = Math.max(4, Number(it.r || 0) || 4);
|
||||
if (distXY(item.x, item.y, it.x, it.y) <= range + r) targets.push(it);
|
||||
}
|
||||
const sprayAngle = global.deterministicAngle ? global.deterministicAngle(worldRef, "sprinkler-spray-angle", item) : Math.random() * Math.PI * 2;
|
||||
const sprayX = item.x + Math.cos(sprayAngle) * Math.min(range * 0.42, 86);
|
||||
const sprayY = item.y + Math.sin(sprayAngle) * Math.min(range * 0.42, 86);
|
||||
worldRef.effects?.push(new Effect("ring", sprayX, sprayY, { size: 14, life: 0.18, color: "rgba(95,166,230,0.22)" }));
|
||||
if (!targets.length) return true;
|
||||
const pickIndex = Math.floor((global.deterministicUnit ? global.deterministicUnit(worldRef, "sprinkler-pick", item, targets.length) : Math.random()) * targets.length) % targets.length;
|
||||
const target = targets[pickIndex];
|
||||
if (!target || target.dead) return true;
|
||||
target.amount = 0;
|
||||
target.hp = 0;
|
||||
target.removedReason = "sprinkler-clean";
|
||||
item.sprinklerFlash = 0.45;
|
||||
worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(18, (target.r || 12) * 1.6), life: 0.28, color: "rgba(95,166,230,0.34)" }));
|
||||
worldRef.markItemBucketsDirty?.("sprinkler-clean");
|
||||
worldRef.markSpatialDirty?.("sprinkler-clean");
|
||||
worldRef.updateItemCounts?.();
|
||||
worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateGateFence(item) {
|
||||
if (item) item.amount = 999;
|
||||
return Boolean(item);
|
||||
|
|
@ -67,6 +109,7 @@
|
|||
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
|
||||
if (item.type === "reciprocator") return updateReciprocator(item, dt, worldRef);
|
||||
if (item.type === "gate_fence") return updateGateFence(item, dt, worldRef);
|
||||
if (item.type === "sprinkler") return updateSprinkler(item, dt, worldRef);
|
||||
if (item.type === "rope") return updateFlexibleLink(item, dt, worldRef);
|
||||
if (item.type === "rod") return updateRigidLink(item, dt, worldRef);
|
||||
return false;
|
||||
|
|
@ -79,6 +122,7 @@
|
|||
updateRotator,
|
||||
updateReciprocator,
|
||||
updatePoisonBlock,
|
||||
updateSprinkler,
|
||||
updateFlexibleLink,
|
||||
updateRigidLink,
|
||||
resolveMechanicalInteractions,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
t.vy = (t.vy || 0) + ny * impulse * 0.72 + rand(-18, 10);
|
||||
if (speed >= 185) {
|
||||
const damage = Math.min(18, Math.max(1, (speed - 185) / 22));
|
||||
worldRef.applyImpactDamage?.(t, damage, "衝突", { source: item }) || t.damage?.(damage, "衝突");
|
||||
worldRef.applyImpactDamage?.(t, damage, "\u885d\u7a81", { source: item }) || t.damage?.(damage, "\u885d\u7a81");
|
||||
}
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.85);
|
||||
t.fallMax = Math.max(t.fallMax || 0.85, t.fallTimer);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ function duplicatorLoadTypeForItem(item) {
|
|||
const type = String(item.type || "");
|
||||
if (!type || type === "grass") return "";
|
||||
if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return "";
|
||||
// Explicit list first. これで「ずんだ餅」「へこ餅」「けんか餅」が
|
||||
// serving-food 判定や medicine role の揺れに左右されず複製機へ入る。
|
||||
// Explicit list first. \u3053\u308c\u3067\u300c\u305a\u3093\u3060\u9905\u300d\u300c\u3078\u3053\u9905\u300d\u300c\u3051\u3093\u304b\u9905\u300d\u304c
|
||||
// serving-food \u5224\u5b9a\u3084 medicine role \u306e\u63fa\u308c\u306b\u5de6\u53f3\u3055\u308c\u305a\u8907\u88fd\u6a5f\u3078\u5165\u308b\u3002
|
||||
if (DUPLICATOR_STORABLE_TYPES.has(type)) return type;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return type;
|
||||
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return type;
|
||||
|
|
@ -92,7 +92,7 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
|
|||
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
if (options.emit !== false) worldRef?.emit?.("duplicator:loaded", { duplicator, foodType: loadType, replaced: changed, direct: Boolean(options.direct) });
|
||||
if (options.log !== false) worldRef?.log?.(`複製機に${duplicator.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(loadType) ? "observe" : "food");
|
||||
if (options.log !== false) worldRef?.log?.(`\u8907\u88fd\u6a5f\u306b${duplicator.storedFoodLabel}\u3092\u30bb\u30c3\u30c8\u3057\u305f\u3002`, typeof isPinType === "function" && isPinType(loadType) ? "observe" : "food");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,19 +33,20 @@
|
|||
// Unified item definition source: tools, item traits, visuals, food metadata, and item effects.
|
||||
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
observe: { id: "observe", label: "\u89b3\u5bdf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
observe: { id: "observe", label: "\u305f\u308a\u306a\u3044\u30c7\u30fc\u30bf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" },
|
||||
area_delete: { id: "area_delete", label: "範囲削除", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "カーソル周囲の広い範囲にある置きものや汚れをまとめて消す。" },
|
||||
copy: { id: "copy", label: "コピー", placeable: false, scalable: false, iconText: "⧉", tooltip: "置いたアイテムをクリックしてコピーし、次のクリックで設定ごと貼り付ける。" },
|
||||
undo: { id: "undo", label: "戻す", placeable: false, scalable: false, iconText: "↶", tooltip: "直前の操作を取り消す。" },
|
||||
redo: { id: "redo", label: "進む", placeable: false, scalable: false, iconText: "↷", tooltip: "取り消した操作をやり直す。" },
|
||||
area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" },
|
||||
copy: { id: "copy", label: "\u30b3\u30d4\u30fc", placeable: false, scalable: false, iconText: "\u29c9", tooltip: "\u7f6e\u3044\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30b3\u30d4\u30fc\u3057\u3001\u6b21\u306e\u30af\u30ea\u30c3\u30af\u3067\u8a2d\u5b9a\u3054\u3068\u8cbc\u308a\u4ed8\u3051\u308b\u3002" },
|
||||
undo: { id: "undo", label: "\u623b\u3059", placeable: false, scalable: false, iconText: "\u21b6", tooltip: "\u76f4\u524d\u306e\u64cd\u4f5c\u3092\u53d6\u308a\u6d88\u3059\u3002" },
|
||||
redo: { id: "redo", label: "\u9032\u3080", placeable: false, scalable: false, iconText: "\u21b7", tooltip: "\u53d6\u308a\u6d88\u3057\u305f\u64cd\u4f5c\u3092\u3084\u308a\u76f4\u3059\u3002" },
|
||||
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "たりないや一部の置きものを移動させる。" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" },
|
||||
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" },
|
||||
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
rope: { id: "rope", label: "ヒモ", placeable: false, scalable: false, iconText: "╰", tooltip: "2つの対象を順にクリックしてふにゃふにゃしたヒモで繋ぐ。" },
|
||||
rod: { id: "rod", label: "棒", placeable: false, scalable: false, iconText: "━", tooltip: "2つの対象を順にクリックして、曲がらない棒で接続する。" },
|
||||
shoot: { id: "shoot", label: "射撃", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "銃で攻撃する。" },
|
||||
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
|
||||
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
|
||||
shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" },
|
||||
sprinkler: { id: "sprinkler", itemType: "sprinkler", label: "\u30b9\u30d7\u30ea\u30f3\u30af\u30e9\u30fc", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u2739", tooltip: "\u52b9\u679c\u7bc4\u56f2\u5185\u306e\u305a\u3093\u3061\u3068\u6b7b\u9ab8\u3092\u30e9\u30f3\u30c0\u30e0\u306b1\u3064\u6d17\u6d44\u3059\u308b\u3002" },
|
||||
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
|
||||
sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
|
||||
love_mochi: { id: "love_mochi", itemType: "love_mochi", label: "\u3078\u3053\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_love_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" },
|
||||
|
|
@ -78,11 +79,11 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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" },
|
||||
rotator: { id: "rotator", itemType: "rotator", label: "回転体", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "↻", collisionShape: "custom_rotator", tooltip: "回転する機械。クリックすると形状や速度を設定できる。" },
|
||||
poison_block: { id: "poison_block", itemType: "poison_block", label: "毒ブロック", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "☠", collisionShape: "custom_poison_block", tooltip: "当たると皮膚が痺れる。クリックで形状が変更できる。" },
|
||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "往復体", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "⇄", collisionShape: "custom_reciprocator", tooltip: "往復する機械。クリックすると形状や速度を設定できる。" },
|
||||
water: { id: "water", itemType: "water", label: "水滴", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "💧", tooltip: "雨の日にも現れる水滴。たりないが飲める。複製機にもセットできる。" },
|
||||
trace: { id: "trace", itemType: "trace", label: "死骸", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||
rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
||||
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
|
||||
food: { id: "food", itemType: "food", label: "\u98df\u3079\u7269", placeable: false, scalable: false, radius: 14, amount: 85, simulationOnly: true },
|
||||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
|
|
@ -96,7 +97,7 @@ const ITEM_TRAITS = Object.freeze({
|
|||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "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", "signboard", "duplicator", "sprinkler", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -164,8 +165,8 @@ 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"] },
|
||||
{ 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", "bed", "nest_box", "ball", "signboard"] },
|
||||
{ id: "physics", label: "物理", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "sprinkler", "bed", "nest_box", "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"] },
|
||||
]);
|
||||
|
||||
|
|
@ -364,7 +365,7 @@ function toolTipsFromDefinitions() {
|
|||
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" },
|
||||
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true },
|
||||
grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water: { label: "水滴", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water: { label: "\u6c34\u6ef4", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -293,6 +293,47 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.beginPath();
|
||||
ctx.arc(0, -this.r * 0.08, this.r * 0.40, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === "sprinkler") {
|
||||
ctx.save();
|
||||
const flash = clamp(Number(this.sprinklerFlash || 0) || 0, 0, 1);
|
||||
const spin = Number(this.sprinklerSpin || 0) || 0;
|
||||
ctx.fillStyle = styleNight ? "#405b6f" : "#8fc7df";
|
||||
ctx.strokeStyle = styleNight ? "#20313f" : "#4e7f96";
|
||||
ctx.lineWidth = Math.max(1.6, this.r * 0.06);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, this.r * 0.22, this.r * 0.74, this.r * 0.34, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "#587789" : "#b8e5f2";
|
||||
roundedRect(ctx, -this.r * 0.28, -this.r * 0.46, this.r * 0.56, this.r * 0.78, this.r * 0.12);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.save();
|
||||
ctx.rotate(spin);
|
||||
ctx.strokeStyle = flash > 0 ? "rgba(174,226,255,0.95)" : (styleNight ? "rgba(142,190,214,0.72)" : "rgba(67,129,160,0.78)");
|
||||
ctx.lineWidth = Math.max(2.0, this.r * 0.075);
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.r * 0.58, -this.r * 0.46);
|
||||
ctx.lineTo(this.r * 0.58, -this.r * 0.46);
|
||||
ctx.moveTo(0, -this.r * 0.72);
|
||||
ctx.lineTo(0, -this.r * 0.20);
|
||||
ctx.stroke();
|
||||
for (const sx of [-1, 1]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx * this.r * 0.68, -this.r * 0.46, this.r * 0.10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(112,184,224,0.72)";
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
const rangePulse = 0.30 + flash * 0.45;
|
||||
ctx.strokeStyle = `rgba(88,157,214,${rangePulse})`;
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.setLineDash([4, 5]);
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, Math.max(this.r * 2.15, 44), 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
} else if (this.type === "duplicator") {
|
||||
ctx.save();
|
||||
const loaded = !!this.storedFoodType;
|
||||
|
|
@ -632,7 +673,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(powered ? (speed >= 0 ? "↻" : "↺") : "○", 0, 0);
|
||||
ctx.fillText(powered ? (speed >= 0 ? "\u21bb" : "\u21ba") : "\u25cb", 0, 0);
|
||||
} else if (this.type === "poison_block") {
|
||||
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
|
|
@ -668,7 +709,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("毒", 0, 0);
|
||||
ctx.fillText("\u6bd2", 0, 0);
|
||||
} else if (this.type === "reciprocator") {
|
||||
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
|
|
@ -733,10 +774,10 @@ draw(ctx, t, lighting = null) {
|
|||
if (powered || Math.abs(visualVelocity) > 0.8) {
|
||||
ctx.save();
|
||||
ctx.rotate(axisAngle);
|
||||
ctx.fillText(visualDirection >= 0 ? "→" : "←", 0, 0);
|
||||
ctx.fillText(visualDirection >= 0 ? "\u2192" : "\u2190", 0, 0);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.fillText("○", 0, 0);
|
||||
ctx.fillText("\u25cb", 0, 0);
|
||||
}
|
||||
} else if (this.type === "rope" || this.type === "rod") {
|
||||
const runtime = window.TarinaiLinkRuntime;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.needEffects.food = 55;
|
||||
item.amount = 999;
|
||||
}
|
||||
if (type === "sprinkler") {
|
||||
item.amount = 999;
|
||||
item.sprinklerTimer = rand(0.18, 0.72);
|
||||
item.sprinklerSpin = rand(0, Math.PI * 2);
|
||||
item.sprinklerFlash = 0;
|
||||
}
|
||||
if (type === "ball") {
|
||||
item.vx = rand(-5, 5);
|
||||
item.vy = rand(-5, 5);
|
||||
|
|
|
|||
|
|
@ -9,13 +9,18 @@
|
|||
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
|
||||
const SCHEDULED_ITEM_TYPES = Object.freeze([
|
||||
"ball", "genkotsu", "firecracker", "pushpin", "oshibyo",
|
||||
"plushie", "grass_bed", "zunchi", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
|
||||
"plushie", "grass_bed", "zunchi", "sprinkler", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
|
||||
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "rope", "rod",
|
||||
]);
|
||||
|
||||
function mobilePerfHint() {
|
||||
return Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
}
|
||||
|
||||
function itemUpdateInterval(item) {
|
||||
if (!item || item.dead) return Infinity;
|
||||
const mobile = mobilePerfHint();
|
||||
// Mechanical bodies and links are owned by the central physics step.
|
||||
// Keeping them out of the per-item scheduler prevents N independent
|
||||
// collision passes and makes future physics-format changes cheaper.
|
||||
|
|
@ -26,10 +31,11 @@
|
|||
if (item.isStructure && item.type === "grass_bed") return 3.0;
|
||||
if ((item.dropTimer || 0) > 0 || Math.hypot(item.vx || 0, item.vy || 0) > 0.08) return 0;
|
||||
if (item.type === "zunchi") return 2.4;
|
||||
if (item.type === "sprinkler") return mobile ? 0.45 : 0.25;
|
||||
if (item.type === "grass") return Infinity;
|
||||
if (item.type === "duplicator") return 0.5;
|
||||
if (item.type === "water") return 1.8;
|
||||
if (item.type === "trace" || item.type === "splat") return 1.2;
|
||||
if (item.type === "duplicator") return mobile ? 0.8 : 0.5;
|
||||
if (item.type === "water") return mobile ? 2.4 : 1.8;
|
||||
if (item.type === "trace" || item.type === "splat") return mobile ? 1.8 : 1.2;
|
||||
if (item.type === "ant_corpse") return 3.5;
|
||||
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(item.type)) return 5.0;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(item.type)) return 5.0;
|
||||
|
|
|
|||
|
|
@ -157,6 +157,9 @@
|
|||
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
|
||||
const itemCount = (worldRef?.items || []).length || 0;
|
||||
const activeHint = worldRef?._itemUpdateRealtime?.length || 0;
|
||||
const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
if (mobile && tier !== "high") return { maxPairs: 112, maxLinks: 72, postMaxPairs: 28, maxSubsteps: 2 };
|
||||
if (mobile) return { maxPairs: 150, maxLinks: 96, postMaxPairs: 40, maxSubsteps: 2 };
|
||||
if (tier === "low") return { maxPairs: 140, maxLinks: 90, postMaxPairs: 36, maxSubsteps: 2 };
|
||||
if (tier === "medium" || tier === "mid") {
|
||||
const crowded = itemCount > 64 && activeHint <= 2;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/mechanical-body
|
||||
// Common body, footprint, motion, and contact runtime for 回転体, 毒ブロック, and 往復体.
|
||||
// Common body, footprint, motion, and contact runtime for \u56de\u8ee2\u4f53, \u6bd2\u30d6\u30ed\u30c3\u30af, and \u5f80\u5fa9\u4f53.
|
||||
(function (global) {
|
||||
const FP = () => global.TarinaiCollisionFootprints || {};
|
||||
|
||||
|
|
@ -1031,8 +1031,8 @@
|
|||
|
||||
function resolveFenceContacts(item, dt, worldRef, radius = null) {
|
||||
if (!item || !worldRef || !isMechanicalType(item)) return false;
|
||||
// 往復体の停止・反転対象は物理アイテムに限定する。
|
||||
// 静的な柵/障害物はレール駆動を殺しやすいため、往復体側ではここで扱わない。
|
||||
// \u5f80\u5fa9\u4f53\u306e\u505c\u6b62\u30fb\u53cd\u8ee2\u5bfe\u8c61\u306f\u7269\u7406\u30a2\u30a4\u30c6\u30e0\u306b\u9650\u5b9a\u3059\u308b\u3002
|
||||
// \u9759\u7684\u306a\u67f5/\u969c\u5bb3\u7269\u306f\u30ec\u30fc\u30eb\u99c6\u52d5\u3092\u6bba\u3057\u3084\u3059\u3044\u305f\u3081\u3001\u5f80\u5fa9\u4f53\u5074\u3067\u306f\u3053\u3053\u3067\u6271\u308f\u306a\u3044\u3002
|
||||
if (item.type === "reciprocator") return false;
|
||||
const rects = obstacleRects(item);
|
||||
const fences = fenceRectsNear(item, worldRef, radius || reach(item) + 80);
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@
|
|||
let frameOpen = null;
|
||||
let frameCount = 0;
|
||||
let lastQualityCheckAt = 0;
|
||||
let tier = "high";
|
||||
let dprScale = 1;
|
||||
const mobilePerfHint = () => Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
let tier = mobilePerfHint() ? "mid" : "high";
|
||||
let dprScale = mobilePerfHint() ? 0.82 : 1;
|
||||
let resizeRequested = false;
|
||||
|
||||
function bucketFor(label) {
|
||||
|
|
@ -75,10 +76,12 @@
|
|||
|
||||
function qualityFromFps(fps) {
|
||||
const f = Number(fps) || 0;
|
||||
if (f > 55) return { tier: "high", dpr: 1 };
|
||||
if (f < 32) return { tier: "low", dpr: 0.72 };
|
||||
if (f < 45) return { tier: "mid", dpr: 0.86 };
|
||||
return { tier: tier === "low" ? "mid" : tier, dpr: tier === "low" ? 0.86 : dprScale };
|
||||
const mobile = mobilePerfHint();
|
||||
const maxDpr = mobile ? 0.86 : 1;
|
||||
if (f > 55) return { tier: mobile ? "mid" : "high", dpr: maxDpr };
|
||||
if (f < 32) return { tier: "low", dpr: mobile ? 0.64 : 0.72 };
|
||||
if (f < 45) return { tier: "mid", dpr: mobile ? 0.74 : 0.86 };
|
||||
return { tier: tier === "low" ? "mid" : tier, dpr: Math.min(maxDpr, tier === "low" ? 0.82 : dprScale) };
|
||||
}
|
||||
|
||||
function endFrame(rawDt = 0, fps = global.__tarinaiFps || 0) {
|
||||
|
|
|
|||
18
js/render.js
18
js/render.js
|
|
@ -143,8 +143,8 @@ function drawToolTargetHoverHighlight(ctx, world) {
|
|||
const p = world.pointer || {};
|
||||
if (!p.inside) return;
|
||||
const style = tool === "delete"
|
||||
? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "削除" }
|
||||
: { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "つまむ" };
|
||||
? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "\u524a\u9664" }
|
||||
: { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "\u3064\u307e\u3080" };
|
||||
const pulse = 0.62 + Math.sin((world.time || 0) * 9.5) * 0.12;
|
||||
const worldLine = world.screenSizeToWorld ? world.screenSizeToWorld(4.6) : 4.6;
|
||||
const outerLine = world.screenSizeToWorld ? world.screenSizeToWorld(2.1) : 2.1;
|
||||
|
|
@ -798,7 +798,7 @@ function isCachedTerrainItem(it) {
|
|||
}
|
||||
|
||||
function isBehindSpriteLayerItem(it) {
|
||||
return Boolean(it && !it.dead && (it.type === "bed" || it.type === "grass_bed" || it.type === "splat"));
|
||||
return Boolean(it && !it.dead && (it.type === "rope" || it.type === "rod" || it.type === "bed" || it.type === "grass_bed" || it.type === "splat"));
|
||||
}
|
||||
|
||||
function renderLayerSortY(entity) {
|
||||
|
|
@ -1711,18 +1711,18 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
const ellipsize = (text, maxW) => {
|
||||
let s = String(text || "");
|
||||
if (ctx.measureText(s).width <= maxW) return s;
|
||||
while (s.length > 1 && ctx.measureText(`${s}…`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}…`;
|
||||
while (s.length > 1 && ctx.measureText(`${s}\u2026`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}\u2026`;
|
||||
};
|
||||
const wrapLine = (text, maxW) => {
|
||||
const s = String(text || "");
|
||||
if (ctx.measureText(s).width <= maxW) return [s];
|
||||
if (s.includes("、")) {
|
||||
const parts = s.split("、");
|
||||
if (s.includes("\u3001")) {
|
||||
const parts = s.split("\u3001");
|
||||
const out = [];
|
||||
let line = "";
|
||||
for (const part of parts) {
|
||||
const candidate = line ? `${line}、${part}` : part;
|
||||
const candidate = line ? `${line}\u3001${part}` : part;
|
||||
if (ctx.measureText(candidate).width <= maxW) line = candidate;
|
||||
else {
|
||||
if (line) out.push(line);
|
||||
|
|
@ -1736,7 +1736,7 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
};
|
||||
let lines = [];
|
||||
for (const line of rawLines) lines.push(...wrapLine(line, maxTextW));
|
||||
if (lines.length > 4) lines = [...lines.slice(0, 3), ellipsize(lines.slice(3).join("、"), maxTextW)];
|
||||
if (lines.length > 4) lines = [...lines.slice(0, 3), ellipsize(lines.slice(3).join("\u3001"), maxTextW)];
|
||||
const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0);
|
||||
const lineH = 17;
|
||||
const w = Math.min(280, Math.max(92, textW + 22));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
const SaveSchema = global.TarinaiSaveSchema;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_codec.js");
|
||||
if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for save_codec.js");
|
||||
const EXPORT_PREFIX = "たり";
|
||||
const RAW_CODEC = "生";
|
||||
const DEFLATE_CODEC = "縮";
|
||||
const EXPORT_PREFIX = "\u305f\u308a";
|
||||
const RAW_CODEC = "\u751f";
|
||||
const DEFLATE_CODEC = "\u7e2e";
|
||||
const {
|
||||
BINARY_SCHEMA_VERSION,
|
||||
FIELD_IDS,
|
||||
|
|
@ -30,10 +30,10 @@
|
|||
const addRange = (from, to) => {
|
||||
for (let code = from; code <= to && chars.length < SAVE_TEXT_BASE; code++) chars.push(String.fromCharCode(code));
|
||||
};
|
||||
// 正規化で分解されにくい仮名とCJK統合漢字だけを使う。
|
||||
addRange(0x3041, 0x3096); // ひらがな
|
||||
addRange(0x30A1, 0x30FA); // カタカナ
|
||||
addRange(0x4E00, 0x9FFF); // 常用漢字を含むCJK統合漢字。必要数に達したら停止。
|
||||
// \u6b63\u898f\u5316\u3067\u5206\u89e3\u3055\u308c\u306b\u304f\u3044\u4eee\u540d\u3068CJK\u7d71\u5408\u6f22\u5b57\u3060\u3051\u3092\u4f7f\u3046\u3002
|
||||
addRange(0x3041, 0x3096); // \u3072\u3089\u304c\u306a
|
||||
addRange(0x30A1, 0x30FA); // \u30ab\u30bf\u30ab\u30ca
|
||||
addRange(0x4E00, 0x9FFF); // \u5e38\u7528\u6f22\u5b57\u3092\u542b\u3080CJK\u7d71\u5408\u6f22\u5b57\u3002\u5fc5\u8981\u6570\u306b\u9054\u3057\u305f\u3089\u505c\u6b62\u3002
|
||||
return chars.join("");
|
||||
})();
|
||||
const SAVE_TEXT_REVERSE = (() => {
|
||||
|
|
@ -362,15 +362,15 @@
|
|||
|
||||
function generatedNameCode(value = "") {
|
||||
const name = String(value || "");
|
||||
if (!name.startsWith("は")) return -1;
|
||||
const stop = name.endsWith("っ");
|
||||
if (!name.startsWith("\u306f")) return -1;
|
||||
const stop = name.endsWith("\u3063");
|
||||
const core = name.slice(1, stop ? -1 : undefined);
|
||||
if (core.length > 6) return -1;
|
||||
let bits = 0;
|
||||
for (let i = 0; i < core.length; i++) {
|
||||
const ch = core.charAt(i);
|
||||
if (ch !== "う" && ch !== "ぅ") return -1;
|
||||
if (ch === "ぅ") bits |= (1 << i);
|
||||
if (ch !== "\u3046" && ch !== "\u3045") return -1;
|
||||
if (ch === "\u3045") bits |= (1 << i);
|
||||
}
|
||||
return core.length | (stop ? 8 : 0) | (bits << 4);
|
||||
}
|
||||
|
|
@ -378,9 +378,9 @@
|
|||
const middleLen = code & 7;
|
||||
const stop = !!(code & 8);
|
||||
const bits = code >> 4;
|
||||
let name = "は";
|
||||
for (let i = 0; i < middleLen; i++) name += (bits & (1 << i)) ? "ぅ" : "う";
|
||||
if (stop) name += "っ";
|
||||
let name = "\u306f";
|
||||
for (let i = 0; i < middleLen; i++) name += (bits & (1 << i)) ? "\u3045" : "\u3046";
|
||||
if (stop) name += "\u3063";
|
||||
return name;
|
||||
}
|
||||
function writeName(writer, value = "") {
|
||||
|
|
@ -467,7 +467,7 @@
|
|||
}
|
||||
|
||||
function seedPersonalityArray(seed = "", flags = 0) {
|
||||
// ずんちどれいは誕生時性格も全軸 -1 なので、差分基準も0固定にする。
|
||||
// \u305a\u3093\u3061\u3069\u308c\u3044\u306f\u8a95\u751f\u6642\u6027\u683c\u3082\u5168\u8ef8 -1 \u306a\u306e\u3067\u3001\u5dee\u5206\u57fa\u6e96\u30820\u56fa\u5b9a\u306b\u3059\u308b\u3002
|
||||
if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0];
|
||||
return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100];
|
||||
}
|
||||
|
|
@ -477,6 +477,8 @@
|
|||
const nestIdx = row[16] ?? -1;
|
||||
const stateId = row[17]?.[0] || 0;
|
||||
const stateTarget = row[17]?.[1] ?? -1;
|
||||
const genetics = Array.isArray(row[18]) ? row[18] : [];
|
||||
const fightStats = Array.isArray(row[19]) ? row[19] : [];
|
||||
const birthSeed = row[1] || "";
|
||||
trackBytes(writer, profile, "tarinai.flags", () => writeBitFields(writer, [row[0] || 0], [10]));
|
||||
trackBytes(writer, profile, "tarinai.seed", () => writer.str(birthSeed));
|
||||
|
|
@ -505,10 +507,14 @@
|
|||
if (pinIdx >= 0) tailMask |= 1;
|
||||
if (nestIdx >= 0) tailMask |= 2;
|
||||
if (stateId || stateTarget >= 0) tailMask |= 4;
|
||||
writeBitFields(writer, [tailMask], [3]);
|
||||
if (genetics.length) tailMask |= 8;
|
||||
if (fightStats.length) tailMask |= 16;
|
||||
writeBitFields(writer, [tailMask], [5]);
|
||||
if (tailMask & 1) writer.u(pinIdx);
|
||||
if (tailMask & 2) writer.u(nestIdx);
|
||||
if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); }
|
||||
if (tailMask & 8) for (let i = 0; i < 4; i++) writer.s(genetics[i] || 100);
|
||||
if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); }
|
||||
});
|
||||
}
|
||||
function readTarinai(reader, coordState = null) {
|
||||
|
|
@ -526,11 +532,14 @@
|
|||
const relationships = readRelationships(reader);
|
||||
const timers = readTimers(reader);
|
||||
const modes = readModes(reader);
|
||||
const [tailMask] = readBitFields(reader, [3]);
|
||||
const modernTail = Number(reader.schemaVersion || BINARY_SCHEMA_VERSION) >= 15;
|
||||
const [tailMask] = readBitFields(reader, [modernTail ? 5 : 3]);
|
||||
const pinIdx = (tailMask & 1) ? reader.u() : -1;
|
||||
const nestIdx = (tailMask & 2) ? reader.u() : -1;
|
||||
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -1];
|
||||
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo];
|
||||
const genetics = (modernTail && (tailMask & 8)) ? [reader.s(), reader.s(), reader.s(), reader.s()] : [];
|
||||
const fightStats = (modernTail && (tailMask & 16)) ? [reader.u(), reader.u(), reader.s()] : [];
|
||||
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
|
||||
}
|
||||
|
||||
function writeItemExtra(writer, typeId, extra = []) {
|
||||
|
|
@ -693,7 +702,7 @@
|
|||
function decodeBinarySnapshot(bytes) {
|
||||
const reader = new BinaryReader(bytes);
|
||||
const schema = reader.b();
|
||||
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||
if (schema !== BINARY_SCHEMA_VERSION && schema !== 14) throw new Error("unsupported binary save schema");
|
||||
reader.schemaVersion = schema;
|
||||
const snapshotVersion = reader.u();
|
||||
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Current-format save identifiers only.
|
||||
(function (global) {
|
||||
const SNAPSHOT_VERSION = 11;
|
||||
const BINARY_SCHEMA_VERSION = 14;
|
||||
const BINARY_SCHEMA_VERSION = 15;
|
||||
|
||||
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"
|
||||
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "sprinkler"
|
||||
]);
|
||||
|
||||
function typeIds(names) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
function metaKey(slot) { return `${META_PREFIX}${slot}`; }
|
||||
function writeSlot(slot, hashText, meta = null) {
|
||||
const hash = String(hashText || "").trim();
|
||||
if (!hash.startsWith("たり")) throw new Error("unsupported Japanese hash slot value");
|
||||
if (!hash.startsWith("\u305f\u308a")) throw new Error("unsupported Japanese hash slot value");
|
||||
global.localStorage?.setItem(slotKey(slot), hash);
|
||||
if (meta) global.localStorage?.setItem(metaKey(slot), JSON.stringify({ ...meta, savedAt: Date.now() }));
|
||||
return { hash, meta: readMeta(slot) };
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@
|
|||
if (!entry) return "\u7a7a\u304d";
|
||||
const meta = entry.m || entry;
|
||||
const m = meta.m || [];
|
||||
const date = meta.savedAt ? new Date(meta.savedAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : (meta.c ? new Date(meta.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "日時不明");
|
||||
const date = meta.savedAt ? new Date(meta.savedAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : (meta.c ? new Date(meta.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e");
|
||||
const day = m[0] || 1;
|
||||
const population = m[1] || 0;
|
||||
const fieldType = m[2] || "庭";
|
||||
const fieldType = m[2] || "\u5ead";
|
||||
const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : "";
|
||||
const hashLen = entry.hash ? ` / ${entry.hash.length}字` : "";
|
||||
return `${date} / ${fieldType} / ${day}日目 ${timeText} / ${population}匹${hashLen}`;
|
||||
const hashLen = entry.hash ? ` / ${entry.hash.length}\u5b57` : "";
|
||||
return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339${hashLen}`;
|
||||
}
|
||||
|
||||
function bytesText(value = 0) {
|
||||
|
|
@ -84,32 +84,32 @@
|
|||
.sort((a, b) => (b[1]?.bytes || 0) - (a[1]?.bytes || 0))
|
||||
.slice(0, 10);
|
||||
const topItems = itemEntries.length
|
||||
? itemEntries.map(([type, entry]) => `<span>${htmlEscape(type)} ${Number(entry.count || 0)}個 / ${htmlEscape(bytesText(entry.bytes || 0))}</span>`).join("")
|
||||
: `<span>なし</span>`;
|
||||
? itemEntries.map(([type, entry]) => `<span>${htmlEscape(type)} ${Number(entry.count || 0)}\u500b / ${htmlEscape(bytesText(entry.bytes || 0))}</span>`).join("")
|
||||
: `<span>\u306a\u3057</span>`;
|
||||
box.innerHTML = `
|
||||
<div class="save-profile-summary">
|
||||
<span>ハッシュ <strong>${Number(comp.totalChars || 0).toLocaleString("ja-JP")}字</strong></span>
|
||||
<span>圧縮前 <strong>${htmlEscape(bytesText(raw))}</strong></span>
|
||||
<span>圧縮後 <strong>${htmlEscape(bytesText(comp.packedBytes || 0))}</strong></span>
|
||||
<span>圧縮率 <strong>${comp.ratio ? `${(comp.ratio * 100).toFixed(1)}%` : "-"}</strong></span>
|
||||
<span>文字表 <strong>${comp.japaneseText ? "日本語2048" : "不明"}</strong></span>
|
||||
<span>\u30cf\u30c3\u30b7\u30e5 <strong>${Number(comp.totalChars || 0).toLocaleString("ja-JP")}\u5b57</strong></span>
|
||||
<span>\u5727\u7e2e\u524d <strong>${htmlEscape(bytesText(raw))}</strong></span>
|
||||
<span>\u5727\u7e2e\u5f8c <strong>${htmlEscape(bytesText(comp.packedBytes || 0))}</strong></span>
|
||||
<span>\u5727\u7e2e\u7387 <strong>${comp.ratio ? `${(comp.ratio * 100).toFixed(1)}%` : "-"}</strong></span>
|
||||
<span>\u6587\u5b57\u8868 <strong>${comp.japaneseText ? "\u65e5\u672c\u8a9e2048" : "\u4e0d\u660e"}</strong></span>
|
||||
</div>
|
||||
<div class="save-profile-grid">
|
||||
${profileRow("ヘッダ", sections.header, raw)}
|
||||
${profileRow("ワールド", sections.world, raw)}
|
||||
${profileRow("たりない合計", sections["tarinai.total"], raw)}
|
||||
${profileRow("生成seed", sections["tarinai.seed"], raw)}
|
||||
${profileRow("基本値", sections["tarinai.core"], raw)}
|
||||
${profileRow("欲求", sections["tarinai.needs"], raw)}
|
||||
${profileRow("性格差分", sections["tarinai.traits"], raw)}
|
||||
${profileRow("家族", sections["tarinai.family"], raw)}
|
||||
${profileRow("関係", sections["tarinai.relationships"], raw)}
|
||||
${profileRow("タイマー/効果", (sections["tarinai.timers"] || 0) + (sections["tarinai.modes"] || 0) + (sections["tarinai.tail"] || 0), raw)}
|
||||
${profileRow("アイテム合計", sections["items.total"], raw)}
|
||||
${profileRow("アイテム基本", sections["items.core"], raw)}
|
||||
${profileRow("アイテム追加", sections["items.extra"], raw)}
|
||||
${profileRow("\u30d8\u30c3\u30c0", sections.header, raw)}
|
||||
${profileRow("\u30ef\u30fc\u30eb\u30c9", sections.world, raw)}
|
||||
${profileRow("\u305f\u308a\u306a\u3044\u5408\u8a08", sections["tarinai.total"], raw)}
|
||||
${profileRow("\u751f\u6210seed", sections["tarinai.seed"], raw)}
|
||||
${profileRow("\u57fa\u672c\u5024", sections["tarinai.core"], raw)}
|
||||
${profileRow("\u6b32\u6c42", sections["tarinai.needs"], raw)}
|
||||
${profileRow("\u6027\u683c\u5dee\u5206", sections["tarinai.traits"], raw)}
|
||||
${profileRow("\u5bb6\u65cf", sections["tarinai.family"], raw)}
|
||||
${profileRow("\u95a2\u4fc2", sections["tarinai.relationships"], raw)}
|
||||
${profileRow("\u30bf\u30a4\u30de\u30fc/\u52b9\u679c", (sections["tarinai.timers"] || 0) + (sections["tarinai.modes"] || 0) + (sections["tarinai.tail"] || 0), raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u5408\u8a08", sections["items.total"], raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u57fa\u672c", sections["items.core"], raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u8ffd\u52a0", sections["items.extra"], raw)}
|
||||
</div>
|
||||
<div class="save-profile-items"><b>アイテム上位:</b>${topItems}</div>`;
|
||||
<div class="save-profile-items"><b>\u30a2\u30a4\u30c6\u30e0\u4e0a\u4f4d:</b>${topItems}</div>`;
|
||||
}
|
||||
|
||||
function ensureDialog() {
|
||||
|
|
@ -123,14 +123,14 @@
|
|||
dialog.innerHTML = `
|
||||
<div class="field-dialog-panel save-dialog-panel">
|
||||
<h2 id="saveDialogTitle">\u30bb\u30fc\u30d6 / \u30ed\u30fc\u30c9</h2>
|
||||
<p class="hint save-warning">セーブは実験的機能であり保証対象外です。</p>
|
||||
<p class="hint save-warning">\u30bb\u30fc\u30d6\u306f\u5b9f\u9a13\u7684\u6a5f\u80fd\u3067\u3042\u308a\u4fdd\u8a3c\u5bfe\u8c61\u5916\u3067\u3059\u3002</p>
|
||||
<div id="saveSlotList" class="save-slot-list"></div>
|
||||
<div class="save-export-panel">
|
||||
<div class="save-export-actions">
|
||||
<button id="saveExportBtn" class="btn" type="button">呪文をセーブ</button>
|
||||
<button id="saveImportBtn" class="btn primary" type="button">呪文をロード</button>
|
||||
<button id="saveExportBtn" class="btn" type="button">\u546a\u6587\u3092\u30bb\u30fc\u30d6</button>
|
||||
<button id="saveImportBtn" class="btn primary" type="button">\u546a\u6587\u3092\u30ed\u30fc\u30c9</button>
|
||||
</div>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="セーブした呪文はコピペして保存してください。"></textarea>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="\u30bb\u30fc\u30d6\u3057\u305f\u546a\u6587\u306f\u30b3\u30d4\u30da\u3057\u3066\u4fdd\u5b58\u3057\u3066\u304f\u3060\u3055\u3044\u3002"></textarea>
|
||||
<div id="saveHashProfile" class="save-hash-profile" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
|
|
@ -154,9 +154,9 @@
|
|||
<div class="save-slot-title">\u30b9\u30ed\u30c3\u30c8 ${i}</div>
|
||||
<div class="save-slot-meta">${htmlEscape(formatSlotSummary(snapshot))}</div>
|
||||
<div class="save-slot-actions">
|
||||
<button class="btn mini" data-save-slot="${i}" type="button">セーブ</button>
|
||||
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>ロード</button>
|
||||
<button class="btn mini danger" data-delete-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>消す</button>
|
||||
<button class="btn mini" data-save-slot="${i}" type="button">\u30bb\u30fc\u30d6</button>
|
||||
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>\u30ed\u30fc\u30c9</button>
|
||||
<button class="btn mini danger" data-delete-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>\u6d88\u3059</button>
|
||||
</div>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
|
|
@ -194,34 +194,34 @@
|
|||
await saveSlot(slot);
|
||||
renderSlotList();
|
||||
global.audio?.notify?.();
|
||||
global.showToast?.(`スロット${slot}にセーブしました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u306b\u30bb\u30fc\u30d6\u3057\u307e\u3057\u305f\u3002`);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("セーブに失敗しました。空き容量を確認してください。");
|
||||
global.showToast?.("\u30bb\u30fc\u30d6\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u7a7a\u304d\u5bb9\u91cf\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (loadBtn) {
|
||||
const slot = Number(loadBtn.dataset.loadSlot);
|
||||
if (!global.confirm?.(`スロット${slot}をロードします。現在の状態は上書きされます。`)) return;
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002`)) return;
|
||||
try {
|
||||
await loadSlot(slot);
|
||||
closeDialog();
|
||||
global.audio?.notify?.();
|
||||
global.showToast?.(`スロット${slot}をロードしました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002`);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("ロードに失敗しました。");
|
||||
global.showToast?.("\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (deleteBtn) {
|
||||
const slot = Number(deleteBtn.dataset.deleteSlot);
|
||||
if (!global.confirm?.(`スロット${slot}を消しますか?`)) return;
|
||||
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u6d88\u3057\u307e\u3059\u304b\uff1f`)) return;
|
||||
deleteSlot(slot);
|
||||
renderSlotList();
|
||||
global.audio?.delete?.();
|
||||
global.showToast?.(`スロット${slot}を消しました。`);
|
||||
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u6d88\u3057\u307e\u3057\u305f\u3002`);
|
||||
return;
|
||||
}
|
||||
if (e.target.closest("#saveCloseBtn")) closeDialog();
|
||||
|
|
@ -234,20 +234,20 @@
|
|||
area.focus();
|
||||
area.select();
|
||||
}
|
||||
global.showToast?.("呪文をセーブしました。");
|
||||
global.showToast?.("\u546a\u6587\u3092\u30bb\u30fc\u30d6\u3057\u307e\u3057\u305f\u3002");
|
||||
}
|
||||
if (e.target.closest("#saveImportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
const text = area?.value || "";
|
||||
if (!text.trim()) { global.showToast?.("ロードする呪文を貼り付けてください。"); return; }
|
||||
if (!global.confirm?.("呪文をロードします。現在の状態は上書きされます。")) return;
|
||||
if (!text.trim()) { global.showToast?.("\u30ed\u30fc\u30c9\u3059\u308b\u546a\u6587\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
if (!global.confirm?.("\u546a\u6587\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002")) return;
|
||||
try {
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world, { source: "import" });
|
||||
closeDialog();
|
||||
global.showToast?.("呪文からロードしました。");
|
||||
global.showToast?.("\u546a\u6587\u304b\u3089\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002");
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
global.showToast?.("ロードに失敗しました。呪文を確認してください。");
|
||||
global.showToast?.("\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u546a\u6587\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -141,7 +141,10 @@ function randomGenetics(seed = "") {
|
|||
function normalizeGenetics(source = null, seed = "") {
|
||||
const fallback = randomGenetics(seed);
|
||||
const out = {};
|
||||
for (const key of GENETIC_KEYS) out[key] = clampGeneticValue(key, source?.[key] ?? fallback[key]);
|
||||
for (const key of GENETIC_KEYS) {
|
||||
const legacyKey = key === "attackMul" ? "fightMul" : key;
|
||||
out[key] = clampGeneticValue(key, source?.[key] ?? source?.[legacyKey] ?? fallback[key]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
worldRef.updateWeather(dt);
|
||||
worldRef.foodSpoilagePenalty = Math.max(0, (worldRef.foodSpoilagePenalty || 0) - dt * 0.010);
|
||||
worldRef.updateTerrainDirtyState?.(dt);
|
||||
worldRef.evaluateColonyMood?.();
|
||||
}
|
||||
|
||||
function prepareSpatialFrame(worldRef) {
|
||||
|
|
|
|||
|
|
@ -142,13 +142,14 @@
|
|||
return out;
|
||||
}
|
||||
function geneticsToArray(g = {}) {
|
||||
return ["lifeSpanMul", "speedMul", "sizeMul", "fightMul"].map(k => q(Number(g?.[k]) || 1, 100, 100));
|
||||
const normalized = typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(g, "") : g;
|
||||
return ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"].map(k => q(Number(normalized?.[k]) || 1, 100, 100));
|
||||
}
|
||||
function arrayToGenetics(arr = []) {
|
||||
const keys = ["lifeSpanMul", "speedMul", "sizeMul", "fightMul"];
|
||||
const keys = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
|
||||
const out = {};
|
||||
keys.forEach((k, i) => { out[k] = u(arr[i], 100, 1); });
|
||||
return out;
|
||||
return typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(out, "") : out;
|
||||
}
|
||||
function flagPack(t = {}) {
|
||||
let flags = 0;
|
||||
|
|
@ -161,6 +162,7 @@
|
|||
if (t.zunchiSlaveLocked) flags |= 1 << 6;
|
||||
if (t.birthRitualLeader) flags |= 1 << 7;
|
||||
if (t.hasPaired) flags |= 1 << 8;
|
||||
if (t.isTarinaiChampion) flags |= 1 << 9;
|
||||
return flags;
|
||||
}
|
||||
function flagApply(t, flags = 0) {
|
||||
|
|
@ -173,6 +175,7 @@
|
|||
t.zunchiSlaveLocked = !!(flags & (1 << 6));
|
||||
t.birthRitualLeader = !!(flags & (1 << 7));
|
||||
t.hasPaired = !!(flags & (1 << 8));
|
||||
t.isTarinaiChampion = !!(flags & (1 << 9));
|
||||
if (t.isZunchiSlave) t.zunchiSlaveLocked = true;
|
||||
}
|
||||
function compactWorld(worldRef) {
|
||||
|
|
@ -263,7 +266,8 @@
|
|||
flagPack(t), t.birthSeed || t.familyKey || t.id || `s${idx}`, q(t.x, 1), q(t.y, 1),
|
||||
q(t.age, 10), q(t.generation, 1, 1), q(t.hunger, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1),
|
||||
needsToArray(t.needs || t.needRaw || {}), personalityToArray(t.currentPersonality),
|
||||
parentIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx]
|
||||
parentIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx],
|
||||
geneticsToArray(t.genetics), [q(t.totalFightWins, 1), q(t.totalFightLosses, 1), q(t.tarinaiChampionSince, 10)]
|
||||
];
|
||||
}
|
||||
function expandTarinai(row, index, worldRef) {
|
||||
|
|
@ -280,11 +284,16 @@
|
|||
id: `st${index}`,
|
||||
liveToken: 1,
|
||||
currentPersonality: arrayToPersonality(row[10]),
|
||||
genetics: Array.isArray(row[18]) ? arrayToGenetics(row[18]) : profile.genetics,
|
||||
x: u(row[2], 1), y: u(row[3], 1), vx: 0, vy: 0,
|
||||
age, birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[5], 1, 1)),
|
||||
hunger: u(row[6], 1), loneliness: Number(needs.social) || 0, energy: u(row[7], 1), circadianSleepPressure: u(row[8], 1),
|
||||
needs, state: enumValue(STATE_IDS, row[17]?.[0], "idle"),
|
||||
isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)),
|
||||
isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)),
|
||||
totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0,
|
||||
totalFightLosses: Array.isArray(row[19]) ? u(row[19][1], 1) : 0,
|
||||
tarinaiChampionSince: Array.isArray(row[19]) ? u(row[19][2], 10) : 0,
|
||||
};
|
||||
const t = new TarinaiClass(worldRef, opts);
|
||||
t.familyKey = birthSeed;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
(function (global) {
|
||||
// External audio replacement point. Add files and edit paths here; audio.js consumes this pack at startup.
|
||||
global.TARINAI_SOUND_PACK = Object.freeze({
|
||||
version: "1.3",
|
||||
version: "1.4",
|
||||
categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 },
|
||||
idGain: {
|
||||
sfx_notify: 2.60,
|
||||
|
|
@ -23,6 +23,10 @@
|
|||
sfx_firecracker_explode: 1.70,
|
||||
sfx_shoot: 0.725,
|
||||
sfx_bolt_action: 1.35,
|
||||
voice_major_damage_01: 1.0,
|
||||
voice_major_damage_02: 1.0,
|
||||
voice_major_damage_03: 1.0,
|
||||
voice_major_damage_04: 1.0,
|
||||
},
|
||||
idMinGap: {
|
||||
sfx_ui_click: 0.030,
|
||||
|
|
@ -35,6 +39,10 @@
|
|||
voice_sunbath: 0.95,
|
||||
sfx_shoot: 0.045,
|
||||
sfx_bolt_action: 0.10,
|
||||
voice_major_damage_01: 0.45,
|
||||
voice_major_damage_02: 0.45,
|
||||
voice_major_damage_03: 0.45,
|
||||
voice_major_damage_04: 0.45,
|
||||
},
|
||||
samplePaths: {
|
||||
voice_hau: "assets/sounds/voice_001_hau.wav",
|
||||
|
|
@ -46,7 +54,11 @@
|
|||
voice_sunbath: "assets/sounds/voice_007_sunbath_pokapoka.wav",
|
||||
sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3",
|
||||
sfx_shoot: "assets/sounds/shoot_pistol.mp3",
|
||||
sfx_bolt_action: "assets/sounds/shoot_bolt_action.mp3"
|
||||
sfx_bolt_action: "assets/sounds/shoot_bolt_action.mp3",
|
||||
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"
|
||||
},
|
||||
categories: {
|
||||
voice: "\u305f\u308a\u306a\u3044\u306e\u58f0",
|
||||
|
|
@ -55,6 +67,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",
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
function releaseStructureUsers(worldRef, structure, opts = {}) {
|
||||
if (!worldRef || !structure) return 0;
|
||||
const reason = opts.reason || "なくなった";
|
||||
const reason = opts.reason || "\u306a\u304f\u306a\u3063\u305f";
|
||||
const wake = opts.wake !== false;
|
||||
let released = 0;
|
||||
for (const t of worldRef.tarinai || []) {
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
function deleteStructure(worldRef, structure, opts = {}) {
|
||||
if (!structure || structure.dead) return false;
|
||||
const reason = opts.reason || "structure-deleted";
|
||||
releaseStructureUsers(worldRef, structure, { reason: opts.userReason || "置きものがなくなった", wake: opts.wake !== false });
|
||||
releaseStructureUsers(worldRef, structure, { reason: opts.userReason || "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f", wake: opts.wake !== false });
|
||||
const changed = markStructureGone(structure, reason);
|
||||
if (!changed) return false;
|
||||
markWorldAfterStructureChange(worldRef, reason);
|
||||
|
|
|
|||
|
|
@ -227,8 +227,8 @@
|
|||
if (ownerUse) structure.attachment = Math.min(100, (structure.attachment || 0) + 2);
|
||||
structure.usedCount = (structure.usedCount || 0) + 1;
|
||||
if (options?.purpose === "sleep" || options?.sleep === true) {
|
||||
// かんたんベッドは「寝た瞬間」ではなく「起きた時」に消える。
|
||||
// 睡眠中は寝場所として残すことで、見た目とホバー情報が自然になる。
|
||||
// \u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306f\u300c\u5bdd\u305f\u77ac\u9593\u300d\u3067\u306f\u306a\u304f\u300c\u8d77\u304d\u305f\u6642\u300d\u306b\u6d88\u3048\u308b\u3002
|
||||
// \u7761\u7720\u4e2d\u306f\u5bdd\u5834\u6240\u3068\u3057\u3066\u6b8b\u3059\u3053\u3068\u3067\u3001\u898b\u305f\u76ee\u3068\u30db\u30d0\u30fc\u60c5\u5831\u304c\u81ea\u7136\u306b\u306a\u308b\u3002
|
||||
structure.dead = false;
|
||||
structure.maxHp = Math.max(1, Number(structure.maxHp || 100) || 100);
|
||||
structure.hp = Math.max(1, Number(structure.hp || 0) || structure.maxHp);
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0;
|
||||
this.totalFightLosses = opts.totalFightLosses || 0;
|
||||
this.totalFightWins = opts.totalFightWins || 0;
|
||||
this.isTarinaiChampion = !!opts.isTarinaiChampion;
|
||||
this.tarinaiChampionSince = Number.isFinite(opts.tarinaiChampionSince) ? opts.tarinaiChampionSince : 0;
|
||||
this.isZunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
|
||||
this.zunchiSlaveLocked = Boolean(opts.zunchiSlaveLocked || this.isZunchiSlave);
|
||||
this.formerName = opts.formerName || null;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function selectRoleTarget(role, range) {
|
|||
};
|
||||
}
|
||||
|
||||
function startMoveToTarget(state, fallbackLabel = "行動している") {
|
||||
function startMoveToTarget(state, fallbackLabel = "\u884c\u52d5\u3057\u3066\u3044\u308b") {
|
||||
return function start(tarinai, world, ctx = {}) {
|
||||
const target = ctx.target ?? this.selectTarget?.(tarinai, world, ctx) ?? null;
|
||||
return moveToOrUse(tarinai, target, state, this.label || fallbackLabel);
|
||||
|
|
@ -59,8 +59,8 @@ function createSleepInBedActionSpec() {
|
|||
id: "sleep_in_bed",
|
||||
need: "sleep",
|
||||
state: "seek_bed",
|
||||
label: "寝る場所を探している",
|
||||
phrase: "寝る場所へ戻った",
|
||||
label: "\u5bdd\u308b\u5834\u6240\u3092\u63a2\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u5bdd\u308b\u5834\u6240\u3078\u623b\u3063\u305f",
|
||||
weight: 42,
|
||||
timerBacked: true,
|
||||
selectTarget(t, world) { return findNearestSleepPlace(world, t, 760); },
|
||||
|
|
@ -71,7 +71,7 @@ function createSleepInBedActionSpec() {
|
|||
if (dist(t, bed) > (t.radius || 20) + 18) return moveToOrUse(t, bed, "seek_bed", this.label);
|
||||
const isEasyBed = bed.type === "grass_bed";
|
||||
bed.use?.(t, world, isEasyBed ? { purpose: "sleep" } : undefined);
|
||||
t.startSleeping?.(bed, bed.type === "nest_box" ? "巣箱の中で寝ている" : (isEasyBed ? "かんたんベッドで寝ている" : "ベッドで寝ている"));
|
||||
t.startSleeping?.(bed, bed.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u5bdd\u3066\u3044\u308b" : (isEasyBed ? "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3067\u5bdd\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u3067\u5bdd\u3066\u3044\u308b"));
|
||||
return true;
|
||||
},
|
||||
tick(t) {
|
||||
|
|
@ -87,8 +87,8 @@ function createSleepBuildGrassBedActionSpec() {
|
|||
need: "sleep",
|
||||
subNeed: "bed",
|
||||
state: "build",
|
||||
label: "寝る場所がないのでかんたんベッドを作っている",
|
||||
phrase: "かんたんベッドを作った",
|
||||
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,
|
||||
timerBacked: true,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
|
|
@ -112,8 +112,8 @@ function createSleepAnywhereActionSpec() {
|
|||
id: "sleep_anywhere",
|
||||
need: "sleep",
|
||||
state: "sleep",
|
||||
label: "なにもない所で寝ている",
|
||||
phrase: "そのまま寝た",
|
||||
label: "\u306a\u306b\u3082\u306a\u3044\u6240\u3067\u5bdd\u3066\u3044\u308b",
|
||||
phrase: "\u305d\u306e\u307e\u307e\u5bdd\u305f",
|
||||
weight: 8,
|
||||
timerBacked: true,
|
||||
canStart(t, world) {
|
||||
|
|
@ -146,8 +146,8 @@ function createRestActionSpec() {
|
|||
id: "rest_to_recover",
|
||||
need: "health",
|
||||
state: "idle",
|
||||
label: "元気になるまで休んでいる",
|
||||
phrase: "休んだ",
|
||||
label: "\u5143\u6c17\u306b\u306a\u308b\u307e\u3067\u4f11\u3093\u3067\u3044\u308b",
|
||||
phrase: "\u4f11\u3093\u3060",
|
||||
weight: 24,
|
||||
canStart() { return true; },
|
||||
start(t) {
|
||||
|
|
@ -168,8 +168,8 @@ function createSunbathActionSpec() {
|
|||
need: "health",
|
||||
subNeed: "sunbath",
|
||||
state: "sunbath",
|
||||
label: "日光浴している",
|
||||
phrase: "日光浴している",
|
||||
label: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b",
|
||||
weight: 32,
|
||||
timerBacked: true,
|
||||
canStart(t, world, ctx = {}) {
|
||||
|
|
@ -190,7 +190,7 @@ function createSunbathActionSpec() {
|
|||
fail(t) {
|
||||
if (t && t.state === "sunbath") {
|
||||
t.sunbathTimer = 0;
|
||||
t.goIdle?.("日光浴をやめた");
|
||||
t.goIdle?.("\u65e5\u5149\u6d74\u3092\u3084\u3081\u305f");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
@ -211,8 +211,8 @@ function createWanderActionSpec() {
|
|||
id: "wander_lightly",
|
||||
need: "fulfill",
|
||||
state: "wander",
|
||||
label: "少し歩いている",
|
||||
phrase: "少し歩いた",
|
||||
label: "\u5c11\u3057\u6b69\u3044\u3066\u3044\u308b",
|
||||
phrase: "\u5c11\u3057\u6b69\u3044\u305f",
|
||||
weight: 14,
|
||||
canStart() { return true; },
|
||||
selectTarget: targetFor,
|
||||
|
|
@ -247,8 +247,8 @@ function createPanicEscapeActionSpec() {
|
|||
need: "safety",
|
||||
subNeed: "panic",
|
||||
state: "panic",
|
||||
label: "怖くて逃げている",
|
||||
phrase: "逃げた",
|
||||
label: "\u6016\u304f\u3066\u9003\u3052\u3066\u3044\u308b",
|
||||
phrase: "\u9003\u3052\u305f",
|
||||
weight: 62,
|
||||
timerBacked: true,
|
||||
selectTarget(t, world) {
|
||||
|
|
@ -279,8 +279,8 @@ function createFleeActionSpec() {
|
|||
need: "safety",
|
||||
subNeed: "avoid",
|
||||
state: "flee",
|
||||
label: "危ないものから離れている",
|
||||
phrase: "離れた",
|
||||
label: "\u5371\u306a\u3044\u3082\u306e\u304b\u3089\u96e2\u308c\u3066\u3044\u308b",
|
||||
phrase: "\u96e2\u308c\u305f",
|
||||
weight: 40,
|
||||
selectTarget(t, world) { return findNearbyDanger(world, t, 220); },
|
||||
canStart: basicCanStartWithTarget,
|
||||
|
|
@ -299,12 +299,12 @@ function createApproachFriendActionSpec() {
|
|||
need: "social",
|
||||
subNeed: "bond",
|
||||
state: "seek_friend",
|
||||
label: "仲間に近づいている",
|
||||
phrase: "仲間に近づいた",
|
||||
label: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
||||
phrase: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u305f",
|
||||
weight: 46,
|
||||
selectTarget(t, world) { return t.bestFriendLive?.(FRIEND_AFFINITY_THRESHOLD, 520) || world.nearestOther?.(t, 420) || null; },
|
||||
canStart: basicCanStartWithTarget,
|
||||
start: startMoveToTarget("seek_friend", "仲間に近づいている"),
|
||||
start: startMoveToTarget("seek_friend", "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b"),
|
||||
tick(t, world, dt) { return updateSocialContactBehavior(t, world, dt, "bond"); },
|
||||
};
|
||||
}
|
||||
|
|
@ -315,12 +315,12 @@ function createApproachFamilyActionSpec() {
|
|||
need: "social",
|
||||
subNeed: "family",
|
||||
state: "follow_parent",
|
||||
label: "家族に近づいている",
|
||||
phrase: "家族に近づいた",
|
||||
label: "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
||||
phrase: "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u305f",
|
||||
weight: 28,
|
||||
selectTarget(t) { return t.parentToFollow?.() || null; },
|
||||
canStart: basicCanStartWithTarget,
|
||||
start: startMoveToTarget("follow_parent", "家族に近づいている"),
|
||||
start: startMoveToTarget("follow_parent", "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b"),
|
||||
tick(t, world, dt) { return updateSocialContactBehavior(t, world, dt, "family"); },
|
||||
};
|
||||
}
|
||||
|
|
@ -331,8 +331,8 @@ function createApproachMateActionSpec() {
|
|||
need: "social",
|
||||
subNeed: "mate",
|
||||
state: "seek_friend",
|
||||
label: "繁殖できる相手に近づいている",
|
||||
phrase: "繁殖できる相手に近づいている",
|
||||
label: "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
||||
phrase: "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
||||
weight: 48,
|
||||
selectTarget(t, world) { return selectMateTarget(t, world, 520); },
|
||||
canStart(t, world, ctx = {}) { return (t.reproductionTimer || 0) <= 12 && !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
|
|
@ -352,8 +352,8 @@ function createFightRivalActionSpec() {
|
|||
need: "social",
|
||||
subNeed: "conflict",
|
||||
state: "seek_enemy",
|
||||
label: "気に入らない相手に向かっている",
|
||||
phrase: "喧嘩を始めた",
|
||||
label: "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u306b\u5411\u304b\u3063\u3066\u3044\u308b",
|
||||
phrase: "\u55a7\u5629\u3092\u59cb\u3081\u305f",
|
||||
weight: 18,
|
||||
timerBacked: true,
|
||||
selectTarget(t, world) { return selectFightRivalTarget(t, world); },
|
||||
|
|
@ -390,8 +390,8 @@ function createIntimidateEnemyActionSpec() {
|
|||
need: "social",
|
||||
subNeed: "conflict",
|
||||
state: "intimidate",
|
||||
label: "気になる相手を威嚇している",
|
||||
phrase: "威嚇した",
|
||||
label: "\u6c17\u306b\u306a\u308b\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u5a01\u5687\u3057\u305f",
|
||||
weight: 8,
|
||||
timerBacked: true,
|
||||
selectTarget(t) { return t.recentFightRival?.(80, 180) || null; },
|
||||
|
|
@ -421,8 +421,8 @@ function createBirthRitualActionSpec() {
|
|||
id: "birth_ritual",
|
||||
need: "social",
|
||||
subNeed: "mate",
|
||||
label: "へこへこしている",
|
||||
phrase: "へこへこしている",
|
||||
label: "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b",
|
||||
weight: 0,
|
||||
state: "birth_ritual",
|
||||
timerBacked: true,
|
||||
|
|
@ -437,8 +437,8 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "eat_food",
|
||||
subNeed: "meal",
|
||||
state: "seek_food",
|
||||
label: "食べ物を探している",
|
||||
phrase: "食べ物を食べている",
|
||||
label: "\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u98df\u3079\u7269\u3092\u98df\u3079\u3066\u3044\u308b",
|
||||
weight: 62,
|
||||
role: "food",
|
||||
range: 760,
|
||||
|
|
@ -447,8 +447,8 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "drink_water",
|
||||
subNeed: "water",
|
||||
state: "seek_water",
|
||||
label: "水を探している",
|
||||
phrase: "水を飲んだ",
|
||||
label: "\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u6c34\u3092\u98f2\u3093\u3060",
|
||||
weight: 24,
|
||||
role: "drink",
|
||||
range: 560,
|
||||
|
|
@ -460,8 +460,8 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "use_medicine",
|
||||
subNeed: "medicine",
|
||||
state: "seek_food",
|
||||
label: "元気になるものを探している",
|
||||
phrase: "元気になった",
|
||||
label: "\u5143\u6c17\u306b\u306a\u308b\u3082\u306e\u3092\u63a2\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u5143\u6c17\u306b\u306a\u3063\u305f",
|
||||
weight: 34,
|
||||
role: "medicine",
|
||||
range: 620,
|
||||
|
|
@ -474,12 +474,12 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "hide_at_owned_structure",
|
||||
need: "safety",
|
||||
state: "seek_bed",
|
||||
label: "自分の場所に隠れている",
|
||||
phrase: "自分の場所へ戻った",
|
||||
label: "\u81ea\u5206\u306e\u5834\u6240\u306b\u96a0\u308c\u3066\u3044\u308b",
|
||||
phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f",
|
||||
weight: 30,
|
||||
selectTarget: selectOwnedStructure("grass_bed", 620),
|
||||
canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
start: startMoveToTarget("seek_bed", "自分の場所に隠れている"),
|
||||
start: startMoveToTarget("seek_bed", "\u81ea\u5206\u306e\u5834\u6240\u306b\u96a0\u308c\u3066\u3044\u308b"),
|
||||
},
|
||||
createApproachFriendActionSpec(),
|
||||
createApproachFamilyActionSpec(),
|
||||
|
|
@ -490,19 +490,19 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "play",
|
||||
need: "fulfill",
|
||||
state: "play_ball",
|
||||
label: "遊んでいる",
|
||||
phrase: "遊んだ",
|
||||
label: "\u904a\u3093\u3067\u3044\u308b",
|
||||
phrase: "\u904a\u3093\u3060",
|
||||
weight: 24,
|
||||
selectTarget(t, world) { return world.nearest?.(t, ["ball"], 520) || null; },
|
||||
canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
start: startMoveToTarget("play_ball", "遊んでいる"),
|
||||
start: startMoveToTarget("play_ball", "\u904a\u3093\u3067\u3044\u308b"),
|
||||
},
|
||||
{
|
||||
id: "build_grass_bed",
|
||||
need: "fulfill",
|
||||
state: "build",
|
||||
label: "かんたんベッドを作っている",
|
||||
phrase: "かんたんベッドを作った",
|
||||
label: "\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: 34,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
|
|
@ -512,8 +512,8 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "build_plushie",
|
||||
need: "fulfill",
|
||||
state: "build",
|
||||
label: "ぬいぐるみを作っている",
|
||||
phrase: "ぬいぐるみを作った",
|
||||
label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b",
|
||||
phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f",
|
||||
weight: 28,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
|
|
@ -523,19 +523,19 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
id: "return_owned_structure",
|
||||
need: "fulfill",
|
||||
state: "seek_bed",
|
||||
label: "自分の場所へ戻っている",
|
||||
phrase: "自分の場所へ戻った",
|
||||
label: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u3066\u3044\u308b",
|
||||
phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f",
|
||||
weight: 22,
|
||||
selectTarget: selectOwnedStructure("grass_bed", 620),
|
||||
canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
start: startMoveToTarget("seek_bed", "自分の場所へ戻っている"),
|
||||
start: startMoveToTarget("seek_bed", "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u3066\u3044\u308b"),
|
||||
},
|
||||
{
|
||||
id: "use_plushie",
|
||||
need: "fulfill",
|
||||
state: "idle",
|
||||
label: "ぬいぐるみを抱えている",
|
||||
phrase: "ぬいぐるみを抱えた",
|
||||
label: "\u306c\u3044\u3050\u308b\u307f\u3092\u62b1\u3048\u3066\u3044\u308b",
|
||||
phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u62b1\u3048\u305f",
|
||||
weight: 18,
|
||||
selectTarget: selectOwnedStructure("plushie", Infinity),
|
||||
canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
if ("target" in opts) this.target = opts.target;
|
||||
if (opts.reason != null) this.thought = String(opts.reason || "");
|
||||
if (opts.clearTarget) this.target = null;
|
||||
const wakingNow = Boolean(opts.wake) || (wasSleeping && nextState !== "sleep" && opts.sleeping === false);
|
||||
const wakingNow = wasSleeping && (Boolean(opts.wake) || (nextState !== "sleep" && opts.sleeping === false));
|
||||
if (wakingNow) this.consumePendingGrassBedOnWake?.(opts.reason || "wake");
|
||||
if (opts.sleeping != null) this.sleeping = Boolean(opts.sleeping);
|
||||
if (opts.wake) {
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
const reason = String(opts.reason ?? this.thought ?? "");
|
||||
if (nextState === "idle") {
|
||||
if (typeof clearBehavior === "function") clearBehavior(this, reason || "待っている");
|
||||
if (typeof clearBehavior === "function") clearBehavior(this, reason || "\u5f85\u3063\u3066\u3044\u308b");
|
||||
else if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(this, { clearTarget: !!opts.clearTarget });
|
||||
return true;
|
||||
}
|
||||
|
|
@ -139,16 +139,31 @@
|
|||
|
||||
consumePendingGrassBedOnWake(reason = "wake") {
|
||||
const worldRef = this.world || null;
|
||||
const sessionTargetId = this.sleepSession?.targetId || this.pendingGrassBedWakeId || null;
|
||||
const session = this.sleepSession || null;
|
||||
const sessionTargetId = session?.targetId || this.pendingGrassBedWakeId || null;
|
||||
const bySession = sessionTargetId && worldRef?.itemById ? worldRef.itemById(sessionTargetId) : null;
|
||||
const byItems = sessionTargetId ? (worldRef?.items || []).find(it => it && it.id === sessionTargetId) : null;
|
||||
const candidates = [this.target, bySession, byItems];
|
||||
let bed = candidates.find(it => it && !it.dead && it.type === "grass_bed" && (it.singleUsePending || it.singleUseConsumedById === this.id || (this.sleepSession?.consumesGrassBedOnWake && it.id === sessionTargetId)));
|
||||
if (!bed && (this.sleepSession?.consumesGrassBedOnWake || sessionTargetId)) {
|
||||
let bed = candidates.find(it => it && !it.dead && it.type === "grass_bed" && (it.singleUsePending || it.singleUseConsumedById === this.id || (session?.consumesGrassBedOnWake && it.id === sessionTargetId)));
|
||||
if (!bed && (session?.consumesGrassBedOnWake || sessionTargetId)) {
|
||||
bed = (worldRef?.items || []).find(it => it && !it.dead && it.type === "grass_bed"
|
||||
&& (it.id === sessionTargetId || (it.singleUsePending && (it.singleUseConsumedById === this.id || it.ownerId === this.id))));
|
||||
}
|
||||
if (!bed) return false;
|
||||
const now = Number(worldRef?.time || 0) || 0;
|
||||
const started = Number(session?.startedAt);
|
||||
const sleptFor = Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
||||
const wasActuallySleeping = Boolean((this.state === "sleep" || this.sleeping || session) && session?.consumesGrassBedOnWake);
|
||||
// Reserve-on-use must not consume the simple bed. It is consumed only by
|
||||
// a real wake after sleep has started; pre-sleep wake flags just release
|
||||
// the reservation and leave the bed intact.
|
||||
if (!wasActuallySleeping || sleptFor < 0.75) {
|
||||
if (bed.singleUseConsumedById === this.id) bed.singleUseConsumedById = "";
|
||||
if (bed.singleUsePending && (!bed.singleUseConsumedById || bed.singleUseConsumedById === this.id)) bed.singleUsePending = false;
|
||||
this.pendingGrassBedWakeId = "";
|
||||
if (session) session.consumesGrassBedOnWake = false;
|
||||
return false;
|
||||
}
|
||||
this.pendingGrassBedWakeId = "";
|
||||
return window.TarinaiStructureLifecycle?.consumeGrassBedOnWake?.(worldRef, bed, this, reason) || false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
else if (forced && input.source && input.source !== "forced") data.forcedSource = String(input.source);
|
||||
|
||||
const reason = String(input.reason ?? input.reasonText ?? input.presentText ?? previous?.reason ?? previous?.reasonText ?? previous?.presentText ?? input.label ?? previous?.label ?? "");
|
||||
const label = String(input.label || input.actionLabel || previous?.label || actionId || "行動している");
|
||||
const label = String(input.label || input.actionLabel || previous?.label || actionId || "\u884c\u52d5\u3057\u3066\u3044\u308b");
|
||||
const need = String(input.need || previous?.need || "fulfill");
|
||||
const tiedNeeds = Array.isArray(input.tiedNeeds) && input.tiedNeeds.length
|
||||
? [...input.tiedNeeds].filter(Boolean).map(String)
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ function actionTextForReason(action = null, tarinai = null) {
|
|||
const id = action?.id || behavior?.actionId || "";
|
||||
const target = tarinai?.target || currentBehaviorTarget?.(tarinai) || null;
|
||||
const name = target?.name || "\u76f8\u624b";
|
||||
if (id === "birth_ritual") return target?.name ? `${name}とへこへこしている` : "へこへこしている";
|
||||
if (id === "birth_ritual") return target?.name ? `${name}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` : "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b";
|
||||
if (id === "fight_rival") {
|
||||
const cause = String(behavior?.causeText || "");
|
||||
if (cause.includes("\u3051\u3093\u304b\u3092\u58f2\u3089\u308c")) return "\u53cd\u6483\u3057\u3066\u3044\u308b";
|
||||
return target?.name ? `${name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b";
|
||||
}
|
||||
if (id === "panic_escape" || id === "flee") return "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||
return String(action?.label || action?.phrase || behavior?.label || "ぼーっとしている").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
return String(action?.label || action?.phrase || behavior?.label || "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
}
|
||||
|
||||
function forcedCausePhrase(source = "", fallback = "") {
|
||||
|
|
@ -113,7 +113,7 @@ function needsForBehaviorText(tarinai, behavior = {}, fallbackNeed = "fulfill")
|
|||
function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {}) {
|
||||
const request = behavior?.forcedRequest || (typeof currentForcedBehaviorRequest === "function" ? currentForcedBehaviorRequest(tarinai) : null) || null;
|
||||
const directCause = options?.causeText || behavior?.causeText || request?.causeText || "";
|
||||
if (directCause) return String(directCause || "").replace(/[。!?]+$/, "");
|
||||
if (directCause) return String(directCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
if ((typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(behavior) : Boolean(behavior?.forcedId || behavior?.forcedRequest)) || request) {
|
||||
const source = request?.source || behavior?.source || "external";
|
||||
const sourceReason = request?.reasonText || behavior?.sourceReasonText || behavior?.sourceReasonText || "";
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ function continueBuildPlan(t, world, dt) {
|
|||
}
|
||||
const addedStructure = world.addItem?.(structure, `build:${plan.type}`) || (world.items.push(structure), structure);
|
||||
if (!addedStructure) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "作る場所が見つからない");
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||
}
|
||||
structure.use?.(t, world);
|
||||
forceBuiltStructureVisualRefresh(world, structure, `build:${plan.type}:visual-refresh`);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
|
|||
}
|
||||
|
||||
if ((role === "drink" || role === "medicine") && isDuplicatorFoodSource(target) && foodType === "water") {
|
||||
const text = "複製機の水滴を飲んでいる";
|
||||
const text = "\u8907\u88fd\u6a5f\u306e\u6c34\u6ef4\u3092\u98f2\u3093\u3067\u3044\u308b";
|
||||
t.setActionState?.("seek_water", { target, reason: text, sleeping: false });
|
||||
t.thought = text;
|
||||
t.applyWaterEffect?.(0.55, "water");
|
||||
|
|
|
|||
|
|
@ -204,15 +204,15 @@
|
|||
const heldByPlayer = Boolean(this.playerHeld || this._heldByPlayer);
|
||||
if (!Number.isFinite(this.sleepDiseaseAnchorX)) this.sleepDiseaseAnchorX = this.x;
|
||||
if (!Number.isFinite(this.sleepDiseaseAnchorY)) this.sleepDiseaseAnchorY = this.y;
|
||||
this.startSleeping(null, beingDraggedByAnt ? "眠ったままアリに運ばれている" : "ねむり病で眠り続けている");
|
||||
// ねむり病は意識だけを固定し、物理位置は固定しない。
|
||||
// 以前は anchor に毎回スナップしていたため、外力・つまみ・機械接触を無視していた。
|
||||
this.startSleeping(null, beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b");
|
||||
// \u306d\u3080\u308a\u75c5\u306f\u610f\u8b58\u3060\u3051\u3092\u56fa\u5b9a\u3057\u3001\u7269\u7406\u4f4d\u7f6e\u306f\u56fa\u5b9a\u3057\u306a\u3044\u3002
|
||||
// \u4ee5\u524d\u306f anchor \u306b\u6bce\u56de\u30b9\u30ca\u30c3\u30d7\u3057\u3066\u3044\u305f\u305f\u3081\u3001\u5916\u529b\u30fb\u3064\u307e\u307f\u30fb\u6a5f\u68b0\u63a5\u89e6\u3092\u7121\u8996\u3057\u3066\u3044\u305f\u3002
|
||||
if (heldByPlayer || beingDraggedByAnt || Math.hypot(Number(this.vx || 0) + Number(this.impulseVx || 0), Number(this.vy || 0) + Number(this.impulseVy || 0)) > 0.1) {
|
||||
this.sleepDiseaseAnchorX = this.x;
|
||||
this.sleepDiseaseAnchorY = this.y;
|
||||
}
|
||||
this.wanderAngle = this.wanderAngle || 0;
|
||||
this.thought = beingDraggedByAnt ? "眠ったままアリに運ばれている" : "ねむり病で眠り続けている";
|
||||
this.thought = beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
|
||||
if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) {
|
||||
this.nextSleepBubbleAt = this.world.time + 5.0;
|
||||
this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
||||
|
|
@ -240,20 +240,24 @@
|
|||
if (!alreadyInside) {
|
||||
this.nestBoxEnteredAt = this.world?.time || 0;
|
||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, (this.world?.time || 0) + 14);
|
||||
this.nestBoxSlotIndex = Math.min(occupants.length, Math.max(0, capacity - 1));
|
||||
this._nestBoxEntrySmoothUntil = (this.world?.time || 0) + 0.85;
|
||||
}
|
||||
this.insideNestBoxId = box.id;
|
||||
this.target = box;
|
||||
if (this.state === "seek_bed") this.startSleeping(box, "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b");
|
||||
if (this.state === "sleep") this.sleeping = true;
|
||||
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(box, this) : { x: box.x, y: box.y };
|
||||
const pull = clamp(dt * (alreadyInside ? 10.0 : 13.5), 0, 1);
|
||||
const smoothDt = clamp(Number(dt || 0.016) || 0.016, 0.008, 0.045);
|
||||
const firstPhase = (this.world?.time || 0) < (this._nestBoxEntrySmoothUntil || -Infinity);
|
||||
const pull = clamp(smoothDt * (alreadyInside ? (firstPhase ? 5.2 : 8.5) : 7.2), 0, 0.55);
|
||||
this.x = lerp(this.x, inner.x, pull);
|
||||
this.y = lerp(this.y, inner.y, pull);
|
||||
if (this.nestFade >= 0.88 || distXY(this.x, this.y, inner.x, inner.y) < 3.5) {
|
||||
if (!firstPhase && (this.nestFade >= 0.97 || distXY(this.x, this.y, inner.x, inner.y) < 2.0)) {
|
||||
this.x = inner.x;
|
||||
this.y = inner.y;
|
||||
}
|
||||
this.nestFade = clamp((this.nestFade || 0) + dt * 5.8, 0, 1);
|
||||
this.nestFade = clamp((this.nestFade || 0) + smoothDt * 4.2, 0, 1);
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.impulseVx = 0;
|
||||
|
|
@ -274,6 +278,8 @@
|
|||
this.nestFade = 0;
|
||||
this.nestBoxEnteredAt = -Infinity;
|
||||
this.nestBoxStayUntil = -Infinity;
|
||||
this.nestBoxSlotIndex = -1;
|
||||
this._nestBoxEntrySmoothUntil = -Infinity;
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -313,8 +319,8 @@
|
|||
const entryD = distXY(this.x, this.y, entry.x, entry.y);
|
||||
const centerD = distXY(this.x, this.y, targetBox.x, targetBox.y);
|
||||
const inDoorColumn = base && this.x >= base.left + r * 0.18 && this.x <= base.right - r * 0.18 && this.y >= base.top + r * 0.06 && this.y <= base.bottom + r * 0.48;
|
||||
const captureRange = Math.max(118, r * 2.05, this.radius + 82);
|
||||
const nearEnough = entryD <= captureRange || centerD <= Math.max(104, r * 1.90) || inDoorColumn;
|
||||
const captureRange = Math.max(76, r * 1.55, this.radius + 48);
|
||||
const nearEnough = entryD <= captureRange || centerD <= Math.max(78, r * 1.38) || inDoorColumn;
|
||||
if (nearEnough) {
|
||||
this.enterNestBox(targetBox, dt || 0.016);
|
||||
return targetBox;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
const { foodNeed = 0, foodActionActive = false, canBite = false, canSweetBite = false, canZunchiBite = false } = ctx;
|
||||
|
||||
const d = it?.type === "duplicator" ? foodInteractionDistance(tarinai, it) : dist(tarinai, it);
|
||||
const contactReach = foodInteractionReach(tarinai, it, "food") + (it === tarinai.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0);
|
||||
const contactReach = foodInteractionReach(tarinai, it, "food") + (it === tarinai.target && foodActionActive ? (it?.type === "duplicator" ? 18 : 4) : 0);
|
||||
if (d > contactReach) return false;
|
||||
const foodType = it.type === "duplicator" ? (it.storedFoodType || "") : it.type;
|
||||
const duplicatorConsumable = it.type === "duplicator" && foodType && (foodType === "sweet" ? canSweetBite : (canBite || foodActionActive)) && !(typeof isParamEffectItemType === "function" && isParamEffectItemType(foodType)) && foodType !== "sleep_drug" && foodType !== "love_mochi" && foodType !== "fight_mochi";
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ function setBehavior(tarinai, fields = {}) {
|
|||
const actionId = fields.actionId || fields.id || previousActionId || tarinai.state || "idle";
|
||||
const action = fields.action || actionById(actionId) || null;
|
||||
const need = fields.need || action?.need || previous.need || "fulfill";
|
||||
const label = fields.label || fields.actionLabel || action?.label || previous.label || "行動している";
|
||||
const label = fields.label || fields.actionLabel || action?.label || previous.label || "\u884c\u52d5\u3057\u3066\u3044\u308b";
|
||||
const reason = fields.reason || fields.text || label;
|
||||
const startedAt = Number.isFinite(fields.startedAt) ? fields.startedAt : (Number.isFinite(previous.startedAt) && previousActionId === actionId ? previous.startedAt : now);
|
||||
const lockSeconds = Number(fields.lockSeconds ?? previous.lockSeconds ?? (action ? actionLockSeconds(action, tarinai) : 1.0)) || 1.0;
|
||||
|
|
@ -219,7 +219,7 @@ function setBehaviorFromAction(tarinai, action, choice = {}, reasonText = "", op
|
|||
action: action,
|
||||
need: choice.need || action.need,
|
||||
subNeed: options.subNeed || action.subNeed || "",
|
||||
label: action.label || options.actionLabel || "行動している",
|
||||
label: action.label || options.actionLabel || "\u884c\u52d5\u3057\u3066\u3044\u308b",
|
||||
text: reasonText || action.label,
|
||||
reason: reasonText || action.label,
|
||||
target: options.target !== undefined ? options.target : tarinai?.target,
|
||||
|
|
@ -387,7 +387,7 @@ function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel =
|
|||
const nextNeed = need || previousBehavior.need || "social";
|
||||
const nextActionId = actionId || previousActionId || tarinai.state || "idle";
|
||||
const displayReason = reasonText ?? reason;
|
||||
const nextActionLabel = actionLabel || displayReason || previousBehavior.label || tarinai.state || "行動している";
|
||||
const nextActionLabel = actionLabel || displayReason || previousBehavior.label || tarinai.state || "\u884c\u52d5\u3057\u3066\u3044\u308b";
|
||||
const action = actionById(nextActionId);
|
||||
const previousForced = behaviorIsForced(previousBehavior);
|
||||
const preserveForced = Boolean(previousForced && source === "state" && (!actionId || previousActionId === nextActionId));
|
||||
|
|
@ -395,7 +395,7 @@ function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel =
|
|||
const rawNeedsForText = tarinai.needRaw || tarinai.needs || {};
|
||||
const existingTiedNeeds = (Array.isArray(previousBehavior.tiedNeeds) && previousBehavior.tiedNeeds.length ? [...previousBehavior.tiedNeeds] : [nextNeed])
|
||||
.filter((key, idx) => idx === 0 || Number(rawNeedsForText?.[key] || 0) > 0.5);
|
||||
if (existingTiedNeeds.some(key => key && key !== nextNeed) && !String(nextReasonText).includes("けど、")) {
|
||||
if (existingTiedNeeds.some(key => key && key !== nextNeed) && !String(nextReasonText).includes("\u3051\u3069\u3001")) {
|
||||
nextReasonText = buildReasonText(nextNeed, existingTiedNeeds, { ...(action || {}), id: nextActionId, label: nextActionLabel, subNeed: subNeed || action?.subNeed || "" }, tarinai, tarinai.world || null);
|
||||
}
|
||||
const nextForced = forced ?? preserveForced;
|
||||
|
|
@ -440,8 +440,8 @@ function synchronizeActionTextFromState(tarinai) {
|
|||
const catalogText = typeof window !== "undefined" && window.TEXT_CATALOG?.reasonLabel ? window.TEXT_CATALOG.reasonLabel(tarinai) : "";
|
||||
if (state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) {
|
||||
const partner = tarinai.world?.tarinai?.find?.(o => o && !o.dead && o.id === tarinai.birthPartnerId) || tarinai.target || null;
|
||||
const label = partner?.name ? `${partner.name}\u3068へこへこしている` : "へこへこしている";
|
||||
setBehaviorText(tarinai, { need: "social", actionId: "birth_ritual", actionLabel: "へこへこしている", reason: label, target: partner });
|
||||
const label = partner?.name ? `${partner.name}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` : "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b";
|
||||
setBehaviorText(tarinai, { need: "social", actionId: "birth_ritual", actionLabel: "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b", reason: label, target: partner });
|
||||
return;
|
||||
}
|
||||
if (state === "eat" || (tarinai.eatTimer || 0) > 0.04 || (tarinai.grassEatTimer || 0) > 0.04) {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,42 @@
|
|||
adjustPersonality(key, delta, reason = "") { return adjustPersonality(this, key, delta, reason); },
|
||||
|
||||
shouldApplyPersonalityBehavior(key, direction = 1) { return shouldApplyPersonalityBehavior(this, key, direction); },
|
||||
|
||||
fightWinRate() {
|
||||
const wins = Number(this.totalFightWins || 0) || 0;
|
||||
const losses = Number(this.totalFightLosses || 0) || 0;
|
||||
const total = wins + losses;
|
||||
return total > 0 ? wins / total : 0;
|
||||
},
|
||||
|
||||
tarinaiChampionEligible() {
|
||||
const wins = Number(this.totalFightWins || 0) || 0;
|
||||
const losses = Number(this.totalFightLosses || 0) || 0;
|
||||
const total = wins + losses;
|
||||
return !this.dead && !this.isZunchiSlave && total >= 6 && total > 0 && wins / total >= 0.75;
|
||||
},
|
||||
|
||||
becomeTarinaiChampion(options = {}) {
|
||||
if (!this.tarinaiChampionEligible?.() && !options.force) return false;
|
||||
if (this.isTarinaiChampion && !options.forcePersonality) return false;
|
||||
this.isTarinaiChampion = true;
|
||||
this.tarinaiChampionSince = Number.isFinite(this.tarinaiChampionSince) && this.tarinaiChampionSince > 0 ? this.tarinaiChampionSince : (this.world?.time || 0);
|
||||
ensurePersonality(this);
|
||||
const boosted = [];
|
||||
for (const key of ["aggression", "openness", "sociability"]) {
|
||||
const before = Number(this.currentPersonality?.[key]) || 0;
|
||||
const next = clampPersonalityValue(Math.max(before + 0.48, 0.86));
|
||||
this.currentPersonality[key] = next;
|
||||
if (this.birthPersonality && Number(this.birthPersonality[key]) > next) this.currentPersonality[key] = clampPersonalityValue(this.birthPersonality[key]);
|
||||
if (Math.abs((Number(this.currentPersonality[key]) || 0) - before) > 0.001) boosted.push(PERSONALITY_LABELS[key]?.label || key);
|
||||
}
|
||||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
||||
this.focusPulseTimer = Math.max(this.focusPulseTimer || 0, 1.0);
|
||||
if (boosted.length) this.recordChangeCause?.("たりない王者", boosted.join("・"), { value: +1 });
|
||||
this.world?.recordFamily?.(this);
|
||||
return true;
|
||||
},
|
||||
|
||||
maybeBecomeTimidAfterDamage(cause = "") {
|
||||
if (this.dead) return false;
|
||||
const losses = this.totalFightLosses || 0;
|
||||
|
|
@ -424,7 +460,7 @@
|
|||
},
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function roleMatches(item, role) {
|
|||
|
||||
function foodInteractionPoint(item) {
|
||||
if (!item) return { x: 0, y: 0 };
|
||||
if (item.type === "duplicator") return { x: item.x + (item.r || 34) * 0.62, y: item.y + (item.r || 34) * 0.24 };
|
||||
if (item.type === "duplicator") return { x: item.x + (item.r || 34) * 0.44, y: item.y + (item.r || 34) * 0.18 };
|
||||
return { x: item.x, y: item.y };
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ function foodInteractionDistance(tarinai, item) {
|
|||
}
|
||||
|
||||
function foodInteractionReach(tarinai, item, role = "food") {
|
||||
if (item?.type === "duplicator") return Math.max(14, (tarinai?.radius || 20) * 0.44 + 5);
|
||||
if (item?.type === "duplicator") return Math.max(30, (tarinai?.radius || 20) * 0.72 + (item?.r || 34) * 0.36);
|
||||
if (role === "drink") return Math.max(16, (tarinai?.radius || 20) * 0.58 + (item?.r || 12) * 0.40);
|
||||
return Math.max(13, (tarinai?.radius || 20) * 0.50 + (item?.r || 12) * 0.28);
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ function foodPriorityScore(tarinai, item, distance = 0) {
|
|||
if (!tarinai?.isZunchiSlave) rank -= 1400;
|
||||
if (effectiveFoodTypeForItem(item) === "zunchi" && !tarinai?.isZunchiSlave && hunger < 104) rank -= 1200;
|
||||
}
|
||||
if (item?.type === "duplicator") rank += 85;
|
||||
if (item?.type === "duplicator") rank += 420;
|
||||
return rank - Math.max(0, Number(distance) || 0) * 0.65;
|
||||
}
|
||||
|
||||
|
|
@ -121,8 +121,8 @@ function zunchiEatingBlocked(tarinai, item, world) {
|
|||
if (protectedUntil > 0 && (world?.time || 0) < protectedUntil) return true;
|
||||
const foodNeed = Number(tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
|
||||
const hunger = Number(tarinai.hunger || 0) || 0;
|
||||
// 排泄直後のずんちは観察・接触対象であり、通常食として即消費させない。
|
||||
// ずんちどれいも数秒は待つ。通常個体は極限飢餓でも最低90秒は食べない。
|
||||
// \u6392\u6cc4\u76f4\u5f8c\u306e\u305a\u3093\u3061\u306f\u89b3\u5bdf\u30fb\u63a5\u89e6\u5bfe\u8c61\u3067\u3042\u308a\u3001\u901a\u5e38\u98df\u3068\u3057\u3066\u5373\u6d88\u8cbb\u3055\u305b\u306a\u3044\u3002
|
||||
// \u305a\u3093\u3061\u3069\u308c\u3044\u3082\u6570\u79d2\u306f\u5f85\u3064\u3002\u901a\u5e38\u500b\u4f53\u306f\u6975\u9650\u98e2\u9913\u3067\u3082\u6700\u4f4e90\u79d2\u306f\u98df\u3079\u306a\u3044\u3002
|
||||
const emergency = hunger >= 104 || foodNeed >= 94;
|
||||
const grace = tarinai.isZunchiSlave ? (emergency ? 4 : 18) : (emergency ? 20 : 90);
|
||||
if (protectedAge < grace) return true;
|
||||
|
|
|
|||
|
|
@ -546,7 +546,7 @@ function shouldWakeFromSleep(tarinai, world, needs) {
|
|||
const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now));
|
||||
if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true;
|
||||
if (findNearbyDanger(world, tarinai, 160)) return true;
|
||||
// 自然な睡眠は短い安全ロック後、十分に回復したら起きる。
|
||||
// \u81ea\u7136\u306a\u7761\u7720\u306f\u77ed\u3044\u5b89\u5168\u30ed\u30c3\u30af\u5f8c\u3001\u5341\u5206\u306b\u56de\u5fa9\u3057\u305f\u3089\u8d77\u304d\u308b\u3002
|
||||
if (elapsed < (Number(session.minDuration) || 8)) return false;
|
||||
const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0;
|
||||
if (sleepNeed <= ((typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34) + 4)) return true;
|
||||
|
|
@ -582,7 +582,7 @@ function protectSleepSession(tarinai, world, needs) {
|
|||
return true;
|
||||
}
|
||||
const sleptLongEnough = (world?.time || 0) - (Number(tarinai.sleepSession?.startedAt) || 0) >= 2;
|
||||
tarinai.consumePendingGrassBedOnWake?.("\u76ee\u304c\u899a\u3081\u305f");
|
||||
if (sleptLongEnough) tarinai.consumePendingGrassBedOnWake?.("\u76ee\u304c\u899a\u3081\u305f");
|
||||
tarinai.sleeping = false;
|
||||
tarinai.sleepSession = null;
|
||||
clearBehavior(tarinai);
|
||||
|
|
@ -610,7 +610,7 @@ function resolveNeedsCore(dt) {
|
|||
|
||||
if (this.birthRitualTimer > 0.04) {
|
||||
const partner = this.world.liveTarinaiById?.(this.birthPartnerId) || null;
|
||||
this.setActionState?.("birth_ritual", { target: partner, reason: "へこへこしている" });
|
||||
this.setActionState?.("birth_ritual", { target: partner, reason: "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b" });
|
||||
synchronizeActionTextFromState(this);
|
||||
return;
|
||||
}
|
||||
|
|
@ -643,7 +643,7 @@ function resolveNeedsCore(dt) {
|
|||
this.fightCooldown = Math.max(this.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this.world, "fight-lost-target-cooldown", 4, 8, this, rival));
|
||||
if (rival) this.world?.markFightPairCooldown?.(this, rival, CONFIG.fightCooldown + 6);
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(this, e => e && e.id === "fight_rival");
|
||||
this.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
|
||||
this.setActionState?.("idle", { target: null, reason: "\u76f8\u624b\u3092\u898b\u5931\u3063\u305f\u3002", sleeping: false, clearTarget: true });
|
||||
return;
|
||||
}
|
||||
const reasonText = best ? `${best.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002` : "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u304c\u3044\u308b\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002";
|
||||
|
|
@ -715,7 +715,7 @@ function resolveNeedsCore(dt) {
|
|||
&& sleepNeedForLeisure < 68
|
||||
&& Math.random() < Math.max(0.010, dt * 0.055)) {
|
||||
const choice = { need: "fulfill", tiedNeeds: ["fulfill"], max: Number(needs?.fulfill || 0) || 0, priority: actionPriority(sunbathActionForLeisure) };
|
||||
startNeedAction(this, this.world, choice, sunbathActionForLeisure, "余裕があるので、日光浴している。", { leisure: true, priority: actionPriority(sunbathActionForLeisure), lockSeconds: 6.0 });
|
||||
startNeedAction(this, this.world, choice, sunbathActionForLeisure, "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", { leisure: true, priority: actionPriority(sunbathActionForLeisure), lockSeconds: 6.0 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -491,6 +491,43 @@
|
|||
|
||||
|
||||
ctx.restore();
|
||||
|
||||
if (this.isTarinaiChampion) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = nestHideAlpha;
|
||||
const crownW = Math.max(24, this.radius * 1.34);
|
||||
const crownH = Math.max(14, this.radius * 0.58);
|
||||
const cx = this.x;
|
||||
const cy = this.y - drawH * 0.64 - crownH * 0.55;
|
||||
if (showDecor) {
|
||||
ctx.shadowColor = "rgba(244, 184, 42, 0.78)";
|
||||
ctx.shadowBlur = 12;
|
||||
}
|
||||
ctx.fillStyle = "rgba(248, 201, 55, 0.96)";
|
||||
ctx.strokeStyle = "rgba(116, 78, 18, 0.92)";
|
||||
ctx.lineWidth = Math.max(1.4, crownH * 0.10);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.lineTo(cx - crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx - crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx, cy - crownH * 0.42);
|
||||
ctx.lineTo(cx + crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx + crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx + crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = "rgba(255, 243, 154, 0.92)";
|
||||
ctx.fillRect(cx - crownW * 0.38, cy + crownH * 0.16, crownW * 0.76, crownH * 0.16);
|
||||
ctx.fillStyle = "rgba(151, 88, 38, 0.82)";
|
||||
for (const px of [-0.28, 0, 0.28]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx + crownW * px, cy + crownH * 0.02, Math.max(1.4, crownH * 0.075), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (this.favorite) {
|
||||
ctx.save();
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@
|
|||
function childSeed(worldSeed = "", serial = 0, parentSeeds = []) {
|
||||
const safeSerial = Math.max(0, Math.floor(Number(serial) || 0));
|
||||
const parents = (parentSeeds || []).filter(Boolean).join("+");
|
||||
// 区切りを入れておくと、将来 birthSeed 自体を保存しない形式へ移る時に
|
||||
// birthSerial を安全に取り出せる。
|
||||
// \u533a\u5207\u308a\u3092\u5165\u308c\u3066\u304a\u304f\u3068\u3001\u5c06\u6765 birthSeed \u81ea\u4f53\u3092\u4fdd\u5b58\u3057\u306a\u3044\u5f62\u5f0f\u3078\u79fb\u308b\u6642\u306b
|
||||
// birthSerial \u3092\u5b89\u5168\u306b\u53d6\u308a\u51fa\u305b\u308b\u3002
|
||||
return `b${safeSerial.toString(36)}_${hashString(`${worldSeed}|${safeSerial}|${parents}`)}`;
|
||||
}
|
||||
|
||||
|
|
@ -81,9 +81,9 @@
|
|||
const len = 2 + Math.floor(seedUnit(seed, "name.len") * 6); // 2..7
|
||||
const hasStop = len >= 2 && seedUnit(seed, "name.stop") < 0.30;
|
||||
const middleLen = Math.max(0, len - 1 - (hasStop ? 1 : 0));
|
||||
let name = "は";
|
||||
for (let i = 0; i < middleLen; i++) name += seedUnit(seed, `name.u.${i}`) < 0.64 ? "う" : "ぅ";
|
||||
if (hasStop) name += "っ";
|
||||
let name = "\u306f";
|
||||
for (let i = 0; i < middleLen; i++) name += seedUnit(seed, `name.u.${i}`) < 0.64 ? "\u3046" : "\u3045";
|
||||
if (hasStop) name += "\u3063";
|
||||
return name;
|
||||
}
|
||||
|
||||
|
|
@ -122,11 +122,11 @@
|
|||
|
||||
function birthProfile(seed = "", opts = {}) {
|
||||
const zunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
|
||||
const genetics = seededGenetics(seed);
|
||||
const genetics = opts.genetics && typeof normalizeGenetics === "function" ? normalizeGenetics(opts.genetics, seed) : seededGenetics(seed);
|
||||
const personality = zunchiSlave && typeof zunchiSlavePersonalityValue === "function" ? zunchiSlavePersonalityValue() : seededPersonality(seed);
|
||||
return {
|
||||
birthSeed: seed,
|
||||
name: zunchiSlave ? "ずんちどれい" : seededName(seed),
|
||||
name: zunchiSlave ? "\u305a\u3093\u3061\u3069\u308c\u3044" : seededName(seed),
|
||||
type: zunchiSlave ? "zunchi_slave" : seededType(seed),
|
||||
goodMode: zunchiSlave ? "zunchi_slave" : seededGoodMode(seed),
|
||||
birthPersonality: personality,
|
||||
|
|
|
|||
|
|
@ -110,10 +110,10 @@ function updateFightBehavior(t, world, dt) {
|
|||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(world, "fight-target-lost-cooldown", 4, 8, t, rival));
|
||||
if (isLiveTarinaiEntity(rival)) world.markFightPairCooldown?.(t, rival, CONFIG.fightCooldown + 6);
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
||||
t.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
|
||||
t.setActionState?.("idle", { target: null, reason: "\u76f8\u624b\u3092\u898b\u5931\u3063\u305f\u3002", sleeping: false, clearTarget: true });
|
||||
return true;
|
||||
}
|
||||
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}と喧嘩している` : "喧嘩している", sleeping: false });
|
||||
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b", sleeping: false });
|
||||
return true;
|
||||
}
|
||||
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 42);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,34 @@
|
|||
const bodyContactRange = Math.max(74, selfRadius + otherRadius + 18);
|
||||
if (d < 0.01 || d > bodyContactRange) continue;
|
||||
closeCount += 1;
|
||||
|
||||
const champion = this.isTarinaiChampion ? this : (o.isTarinaiChampion ? o : null);
|
||||
if (champion && this.isTarinaiChampion !== o.isTarinaiChampion) {
|
||||
const avoider = champion === this ? o : this;
|
||||
const nx = (avoider.x - champion.x) / d;
|
||||
const ny = (avoider.y - champion.y) / d;
|
||||
const avoidRange = Math.max(92, selfRadius + otherRadius + 34);
|
||||
const urgency = clamp(1 - d / avoidRange, 0, 1);
|
||||
const push = dt * (64 + urgency * 260);
|
||||
avoider.vx += nx * push;
|
||||
avoider.vy += ny * push;
|
||||
if (d <= selfRadius + otherRadius + 14) {
|
||||
avoider.vx += nx * dt * 190;
|
||||
avoider.vy += ny * dt * 190;
|
||||
champion.vx -= nx * dt * 8;
|
||||
champion.vy -= ny * dt * 8;
|
||||
avoider.surpriseTimer = Math.max(avoider.surpriseTimer || 0, 0.20);
|
||||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
|
||||
if (this.world.relationNotice?.(avoider.id, champion.id, "tarinai-champion-avoid", 18)) {
|
||||
this.world.log(`${avoider.name}はたりない王者の${champion.name}にぶつかりそうになって避けた。`, "relation", { participants: [avoider, champion], eventType: "avoid_relationship", hiddenFromObservation: true, sound: false });
|
||||
}
|
||||
}
|
||||
avoider.lastNeedShockBreaker = champion;
|
||||
avoider.thought = `${champion.name || "相手"}はたりない王者だ`;
|
||||
avoider.adjustRelation?.(champion, -dt * 0.025, dt * 0.18, "tarinai_champion_avoid");
|
||||
if (this !== champion) continue;
|
||||
}
|
||||
|
||||
const fightMochiContact = this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
|
||||
if (fightMochiContact && (this.world.fightPairCooldownRemaining?.(this, o) || 0) <= 0.04 && this.fightCooldown <= 0.04 && o.fightCooldown <= 0.04 && d <= Math.max(42, this.radius + o.radius + 16)) {
|
||||
|
|
@ -266,7 +294,7 @@
|
|||
const dx = spot.x - this.x;
|
||||
const dy = spot.y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (externalActive && d > 8.5 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "動かされて目が覚めた")) return;
|
||||
if (externalActive && d > 8.5 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "\u52d5\u304b\u3055\u308c\u3066\u76ee\u304c\u899a\u3081\u305f")) return;
|
||||
if (d > 2.2) {
|
||||
const restore = externalActive ? (2.0 + bedComfort * 2.0) : (12 + bedComfort * 14);
|
||||
const maxRestore = externalActive ? Math.min(d, 3.2) : d;
|
||||
|
|
@ -298,7 +326,7 @@
|
|||
marker?.markSleepExternalMotion?.(this, dt, "sleep-full-contact");
|
||||
const cdx = (Number(this.x || 0) || 0) - beforeContactX;
|
||||
const cdy = (Number(this.y || 0) || 0) - beforeContactY;
|
||||
if (Math.hypot(cdx, cdy) > 1.8 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "ぶつかって目が覚めた")) return;
|
||||
if (Math.hypot(cdx, cdy) > 1.8 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "\u3076\u3064\u304b\u3063\u3066\u76ee\u304c\u899a\u3081\u305f")) return;
|
||||
if (Math.hypot(cdx, cdy) > 0.05) {
|
||||
this.vx = clamp((Number(this.vx) || 0) + cdx / Math.max(0.016, dt) * 0.085, -170, 170);
|
||||
this.vy = clamp((Number(this.vy) || 0) + cdy / Math.max(0.016, dt) * 0.085, -170, 170);
|
||||
|
|
@ -318,7 +346,7 @@
|
|||
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") {
|
||||
const pendingImpulse = Math.hypot(Number.isFinite(this.impulseVx) ? this.impulseVx : 0, Number.isFinite(this.impulseVy) ? this.impulseVy : 0);
|
||||
const canceledByForce = this.world?.cancelBirthRitualOnForce?.(this, pendingImpulse, {
|
||||
reason: "強い衝撃で繁殖が中断された",
|
||||
reason: "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
||||
target: this.target || null,
|
||||
fear: 1.0,
|
||||
cause: "stored_impulse",
|
||||
|
|
@ -515,12 +543,16 @@
|
|||
if (reason) this.die(reason);
|
||||
},
|
||||
|
||||
die(reason) {
|
||||
die(reason, opts = {}) {
|
||||
if (this.dead) return;
|
||||
const finalReason = this.normalizeDeathReason ? this.normalizeDeathReason(reason) : (reason || "\u4e8b\u6545");
|
||||
const finalReason = this.normalizeDeathReason ? this.normalizeDeathReason(reason, opts) : (reason || "\u4e8b\u6545");
|
||||
this.dead = true;
|
||||
this.deathReason = finalReason;
|
||||
this.world.deadCount += 1;
|
||||
this.world.lastDeathAt = this.world.time || 0;
|
||||
if (!Array.isArray(this.world.recentDeathTimes)) this.world.recentDeathTimes = [];
|
||||
this.world.recentDeathTimes.push(this.world.lastDeathAt);
|
||||
if (this.world.recentDeathTimes.length > 24) this.world.recentDeathTimes = this.world.recentDeathTimes.slice(-24);
|
||||
for (const item of this.world.items || []) {
|
||||
if (!item || item.dead || !item.isStructure || item.type !== "plushie" || item.ownerId !== this.id) continue;
|
||||
item.dead = true;
|
||||
|
|
|
|||
|
|
@ -134,8 +134,8 @@
|
|||
this.vx += dir * deterministicRange(this.world, "foot-massage-stumble-vx", 26, 52, this);
|
||||
this.vy += deterministicRange(this.world, "foot-massage-stumble-vy", -18, 18, this);
|
||||
this.world?.spawnFallEffect?.(this.x, this.y + this.radius * 0.45);
|
||||
this.setActionState?.("idle", { target: null, reason: "足つぼの突起につまづいた。", sleeping: false, clearTarget: false });
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble?.("うっ", 1.6, "rgba(86,70,96,0.78)");
|
||||
this.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && deterministicChance(this.world, "stress-breath-bubble", dt * 0.050, this)) {
|
||||
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
const state = String(tarinai.state || id || "");
|
||||
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
|
||||
const count = (worldRef.tarinai || []).length;
|
||||
const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
const isVisible = visible(worldRef, tarinai, visibleRect);
|
||||
const farRect = isVisible ? visibleRect : (helpers().updateVisibleWorldRect?.(worldRef, 520) || visibleRect);
|
||||
const isNear = isVisible || Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||
|
|
@ -70,7 +71,7 @@
|
|||
// runs on visible skipped frames.
|
||||
const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic|flee/i.test(id);
|
||||
if (panicUrgent) {
|
||||
const interval = isVisible ? (tier === "low" ? 0.12 : 0.08) : (isNear ? 0.18 : 0.36);
|
||||
const interval = isVisible ? (mobile ? 0.13 : (tier === "low" ? 0.12 : 0.08)) : (isNear ? (mobile ? 0.24 : 0.18) : (mobile ? 0.46 : 0.36));
|
||||
return { interval, lane: "panic", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, urgent: true };
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +102,7 @@
|
|||
interval = isNear ? (tier === "low" ? 0.40 : 0.30) : (tier === "low" ? 1.20 : 0.90);
|
||||
}
|
||||
|
||||
if (mobile && Number.isFinite(interval)) interval *= count > 24 ? 1.35 : 1.18;
|
||||
const passivePhysicsNeed = hasPassivePhysicsNeed(tarinai, worldRef, isVisible);
|
||||
const sleepPhysical = lane === "sleep" && passivePhysicsNeed;
|
||||
const eatPhysical = lane === "eat" && passivePhysicsNeed;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
t.blink += dt;
|
||||
t.eatTimer = Math.max(0, t.eatTimer - dt);
|
||||
if (t.state === "eat" && t.eatTimer <= 0.02) {
|
||||
t.setActionState?.("idle", { target: null, reason: "食べ終えた", sleeping: false, clearTarget: true });
|
||||
t.setActionState?.("idle", { target: null, reason: "\u98df\u3079\u7d42\u3048\u305f", sleeping: false, clearTarget: true });
|
||||
}
|
||||
t.eatCooldown = Math.max(0, t.eatCooldown - dt);
|
||||
t.foodReactTimer = Math.max(0, t.foodReactTimer - dt);
|
||||
|
|
@ -95,8 +95,8 @@
|
|||
if (colonyMoodId === "ant_overrun" && t.state !== "panic") {
|
||||
const antSeen = (t.world?.nearbyAnts?.(t.x, t.y, 110, true) || []).some(a => a && !a.dead);
|
||||
if (antSeen) {
|
||||
if (t.enterPanic) t.enterPanic({ target: null, reason: "アリだ", fear: 1.1, wake: true, cause: "colony_ant_overrun" });
|
||||
else { t.setActionState?.("panic", { target: null, reason: "アリだ", wake: true }); t.fearTimer = Math.max(t.fearTimer || 0, 1.1 * t.personalityProfile().fear); }
|
||||
if (t.enterPanic) t.enterPanic({ target: null, reason: "\u30a2\u30ea\u3060", fear: 1.1, wake: true, cause: "colony_ant_overrun" });
|
||||
else { t.setActionState?.("panic", { target: null, reason: "\u30a2\u30ea\u3060", wake: true }); t.fearTimer = Math.max(t.fearTimer || 0, 1.1 * t.personalityProfile().fear); }
|
||||
}
|
||||
}
|
||||
const wasFighting = t.fightTimer > 0.04;
|
||||
|
|
@ -135,14 +135,14 @@
|
|||
t.vx += dir * deterministicRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
||||
t.vy += deterministicRange(t.world, "foot-massage-stumble-vy", -18, 18, t);
|
||||
t.world?.spawnFallEffect?.(t.x, t.y + t.radius * 0.45);
|
||||
t.setActionState?.("idle", { target: null, reason: "足つぼの突起につまづいた。", sleeping: false, clearTarget: false });
|
||||
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("うっ", 1.6, "rgba(86,70,96,0.78)");
|
||||
t.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
|
||||
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && deterministicChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
|
||||
t.bubble("はっ...はっ...", 3.4, "rgba(58,48,63,0.80)");
|
||||
t.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
if (t.state === "idle" && t.stress < 78 && deterministicChance(t.world, "idle-bubble", dt * 0.018, t)) {
|
||||
t.bubble(pick(["はう", "はうぅ", "はうっ", "はぅ"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
t.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
}
|
||||
if (t.hurtTimer > 0 && deterministicChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
|
||||
const side = t.facingDir();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
if (now - (t.lastPhysicalCollisionDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false;
|
||||
const amount = Math.min(42, Math.max(1.0, (delta - SHOCK_DAMAGE_DELTA_THRESHOLD) / 14));
|
||||
t.lastVelocityShockDamageAt = now;
|
||||
t.world?.applyImpactDamage?.(t, amount, "急激な速度変化", { cause: "衝突" }) || t.damage?.(amount, "衝突");
|
||||
t.world?.applyImpactDamage?.(t, amount, "\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316", { cause: "\u885d\u7a81" }) || t.damage?.(amount, "\u885d\u7a81");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, amount > 15 ? 1.9 : 0.9);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, amount > 12 ? 0.72 : 0.28);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer || 0);
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
return Boolean(t && (t.state === "sleep" || t.sleeping) && now < Number(t._sleepExternalMotionUntil || 0));
|
||||
}
|
||||
|
||||
function wakeSleepingFromExternalMotion(t, reason = "外力で目が覚めた") {
|
||||
function wakeSleepingFromExternalMotion(t, reason = "\u5916\u529b\u3067\u76ee\u304c\u899a\u3081\u305f") {
|
||||
if (!t || t.dead || t.sleepDisease || !(t.state === "sleep" || t.sleeping)) return false;
|
||||
const world = t.world || null;
|
||||
if (world?.isTarinaiHiddenInNestBox?.(t)) return false;
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
t._sleepExternalWakeSuppressUntil = now + 0.45;
|
||||
t._sleepExternalMotionUntil = 0;
|
||||
t._sleepExternalMotionReason = "";
|
||||
t.consumePendingGrassBedOnWake?.("外力で起きた");
|
||||
t.consumePendingGrassBedOnWake?.("\u5916\u529b\u3067\u8d77\u304d\u305f");
|
||||
t.sleeping = false;
|
||||
t.sleepSession = null;
|
||||
t.target = null;
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
const finalDy = numeric(t.y) - beforeY;
|
||||
const rawSpeed = Math.hypot(numeric(t.vx) + numeric(t.impulseVx), numeric(t.vy) + numeric(t.impulseVy));
|
||||
const movedByExternal = Math.hypot(finalDx, finalDy) > 4.2 || rawSpeed > 26 || (sleepExternalMotionActive(t) && Math.hypot(finalDx, finalDy) > 2.2);
|
||||
if (movedByExternal) wakeSleepingFromExternalMotion(t, "動かされて目が覚めた");
|
||||
if (movedByExternal) wakeSleepingFromExternalMotion(t, "\u52d5\u304b\u3055\u308c\u3066\u76ee\u304c\u899a\u3081\u305f");
|
||||
}
|
||||
return { done: false, passive: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,65 +4,65 @@ const TEXT_CATALOG = {
|
|||
toolTips: toolTipsFromDefinitions(),
|
||||
ecologyCards: [
|
||||
{
|
||||
title: "たりないとは",
|
||||
title: "\u305f\u308a\u306a\u3044\u3068\u306f",
|
||||
image: "assets/sprites/tarinai_01_smile.webp",
|
||||
text: "庭やケージで生活する観察対象。空腹、睡眠、健康、安全、関係、充足の6欲求とストレスで行動が変わります。個体ごとに性格、体格、家族、経験が残ります。",
|
||||
text: "\u5ead\u3084\u30b1\u30fc\u30b8\u3067\u751f\u6d3b\u3059\u308b\u89b3\u5bdf\u5bfe\u8c61\u3002\u7a7a\u8179\u3001\u7761\u7720\u3001\u5065\u5eb7\u3001\u5b89\u5168\u3001\u95a2\u4fc2\u3001\u5145\u8db3\u306e6\u6b32\u6c42\u3068\u30b9\u30c8\u30ec\u30b9\u3067\u884c\u52d5\u304c\u5909\u308f\u308a\u307e\u3059\u3002\u500b\u4f53\u3054\u3068\u306b\u6027\u683c\u3001\u4f53\u683c\u3001\u5bb6\u65cf\u3001\u7d4c\u9a13\u304c\u6b8b\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "欲求とストレス",
|
||||
title: "\u6b32\u6c42\u3068\u30b9\u30c8\u30ec\u30b9",
|
||||
image: "assets/ui/ecology_hp_stress.webp",
|
||||
text: "空腹や眠気が強いと食べ物や寝床を探します。ストレスは恐怖、混雑、汚れ、けんか、地面の種類などで増減し、高いほど逃走、威嚇、体調不良が起きやすくなります。",
|
||||
text: "\u7a7a\u8179\u3084\u7720\u6c17\u304c\u5f37\u3044\u3068\u98df\u3079\u7269\u3084\u5bdd\u5e8a\u3092\u63a2\u3057\u307e\u3059\u3002\u30b9\u30c8\u30ec\u30b9\u306f\u6050\u6016\u3001\u6df7\u96d1\u3001\u6c5a\u308c\u3001\u3051\u3093\u304b\u3001\u5730\u9762\u306e\u7a2e\u985e\u306a\u3069\u3067\u5897\u6e1b\u3057\u3001\u9ad8\u3044\u307b\u3069\u9003\u8d70\u3001\u5a01\u5687\u3001\u4f53\u8abf\u4e0d\u826f\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "食事と水分",
|
||||
title: "\u98df\u4e8b\u3068\u6c34\u5206",
|
||||
image: "assets/sprites/tarinai_05_drool.webp",
|
||||
text: "草、ずんち、餅、ジュースなどを状況に応じて食べます。病気や極度の空腹では優先順位が変わり、近くの食べ物を探します。水や雨滴は一部の病気や環境の回復にも関わります。",
|
||||
text: "\u8349\u3001\u305a\u3093\u3061\u3001\u9905\u3001\u30b8\u30e5\u30fc\u30b9\u306a\u3069\u3092\u72b6\u6cc1\u306b\u5fdc\u3058\u3066\u98df\u3079\u307e\u3059\u3002\u75c5\u6c17\u3084\u6975\u5ea6\u306e\u7a7a\u8179\u3067\u306f\u512a\u5148\u9806\u4f4d\u304c\u5909\u308f\u308a\u3001\u8fd1\u304f\u306e\u98df\u3079\u7269\u3092\u63a2\u3057\u307e\u3059\u3002\u6c34\u3084\u96e8\u6ef4\u306f\u4e00\u90e8\u306e\u75c5\u6c17\u3084\u74b0\u5883\u306e\u56de\u5fa9\u306b\u3082\u95a2\u308f\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "睡眠と寝場所",
|
||||
title: "\u7761\u7720\u3068\u5bdd\u5834\u6240",
|
||||
image: "assets/sprites/tarinai_20_sleep2.webp",
|
||||
text: "眠くなると巣箱やかんたんベッドを探します。かんたんベッドは草を使って作られ、起床時に消えます。長く寝床を用意できない場合は、何もない場所で眠ることもあります。",
|
||||
text: "\u7720\u304f\u306a\u308b\u3068\u5de3\u7bb1\u3084\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u63a2\u3057\u307e\u3059\u3002\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306f\u8349\u3092\u4f7f\u3063\u3066\u4f5c\u3089\u308c\u3001\u8d77\u5e8a\u6642\u306b\u6d88\u3048\u307e\u3059\u3002\u9577\u304f\u5bdd\u5e8a\u3092\u7528\u610f\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u3001\u4f55\u3082\u306a\u3044\u5834\u6240\u3067\u7720\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "草、地面、天気",
|
||||
title: "\u8349\u3001\u5730\u9762\u3001\u5929\u6c17",
|
||||
image: "assets/ui/tool_grass.webp",
|
||||
text: "草は成長し、食料やかんたんベッドの材料になります。地面は砂、土、コンクリート、ふわふわ毛布、足つぼ、氷を切り替えられ、草の発生やストレス増加に影響します。雨や時間帯も状態に関わります。",
|
||||
text: "\u8349\u306f\u6210\u9577\u3057\u3001\u98df\u6599\u3084\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306e\u6750\u6599\u306b\u306a\u308a\u307e\u3059\u3002\u5730\u9762\u306f\u7802\u3001\u571f\u3001\u30b3\u30f3\u30af\u30ea\u30fc\u30c8\u3001\u3075\u308f\u3075\u308f\u6bdb\u5e03\u3001\u8db3\u3064\u307c\u3001\u6c37\u3092\u5207\u308a\u66ff\u3048\u3089\u308c\u3001\u8349\u306e\u767a\u751f\u3084\u30b9\u30c8\u30ec\u30b9\u5897\u52a0\u306b\u5f71\u97ff\u3057\u307e\u3059\u3002\u96e8\u3084\u6642\u9593\u5e2f\u3082\u72b6\u614b\u306b\u95a2\u308f\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "性格と行動傾向",
|
||||
title: "\u6027\u683c\u3068\u884c\u52d5\u50be\u5411",
|
||||
image: "assets/sprites/tarinai_21_normal_happy.webp",
|
||||
text: "攻撃性、開放性、社交性、神経質性などがあり、経験で少しずつ変化します。性格は食事、接近、逃走、けんか、繁殖相手の探し方などの差として表れます。",
|
||||
text: "\u653b\u6483\u6027\u3001\u958b\u653e\u6027\u3001\u793e\u4ea4\u6027\u3001\u795e\u7d4c\u8cea\u6027\u306a\u3069\u304c\u3042\u308a\u3001\u7d4c\u9a13\u3067\u5c11\u3057\u305a\u3064\u5909\u5316\u3057\u307e\u3059\u3002\u6027\u683c\u306f\u98df\u4e8b\u3001\u63a5\u8fd1\u3001\u9003\u8d70\u3001\u3051\u3093\u304b\u3001\u7e41\u6b96\u76f8\u624b\u306e\u63a2\u3057\u65b9\u306a\u3069\u306e\u5dee\u3068\u3057\u3066\u8868\u308c\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "関係、家族、繁殖",
|
||||
title: "\u95a2\u4fc2\u3001\u5bb6\u65cf\u3001\u7e41\u6b96",
|
||||
image: "assets/sprites/tarinai_27_birth_ritual.webp",
|
||||
text: "親子、共同親、友好、敵対、恐怖が記録されます。条件がそろうとへこへこを行い、子が生まれます。子は親の性格傾向を受け継ぎ、世代情報は家系図に残ります。",
|
||||
text: "\u89aa\u5b50\u3001\u5171\u540c\u89aa\u3001\u53cb\u597d\u3001\u6575\u5bfe\u3001\u6050\u6016\u304c\u8a18\u9332\u3055\u308c\u307e\u3059\u3002\u6761\u4ef6\u304c\u305d\u308d\u3046\u3068\u3078\u3053\u3078\u3053\u3092\u884c\u3044\u3001\u5b50\u304c\u751f\u307e\u308c\u307e\u3059\u3002\u5b50\u306f\u89aa\u306e\u6027\u683c\u50be\u5411\u3092\u53d7\u3051\u7d99\u304e\u3001\u4e16\u4ee3\u60c5\u5831\u306f\u5bb6\u7cfb\u56f3\u306b\u6b8b\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "病気と回復",
|
||||
title: "\u75c5\u6c17\u3068\u56de\u5fa9",
|
||||
image: "assets/sprites/tarinai_24_stress_dizzy.webp",
|
||||
text: "ずんち病、ねむり病、爆発病、きずつき病などがあります。病気ごとに求めるものや行動が変わり、水、薬、食べ物、休息、環境の改善が回復に関係します。",
|
||||
text: "\u305a\u3093\u3061\u75c5\u3001\u306d\u3080\u308a\u75c5\u3001\u7206\u767a\u75c5\u3001\u304d\u305a\u3064\u304d\u75c5\u306a\u3069\u304c\u3042\u308a\u307e\u3059\u3002\u75c5\u6c17\u3054\u3068\u306b\u6c42\u3081\u308b\u3082\u306e\u3084\u884c\u52d5\u304c\u5909\u308f\u308a\u3001\u6c34\u3001\u85ac\u3001\u98df\u3079\u7269\u3001\u4f11\u606f\u3001\u74b0\u5883\u306e\u6539\u5584\u304c\u56de\u5fa9\u306b\u95a2\u4fc2\u3057\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "危険と事故",
|
||||
title: "\u5371\u967a\u3068\u4e8b\u6545",
|
||||
image: "assets/sprites/tarinai_26_intimidate.webp",
|
||||
text: "けんか、爆竹、げんこつ、ピン、ボール、アリ、落下物などで体力が減ります。体力が0になると死亡し、死因はログと統計に残ります。危険な道具は観察の流れを大きく変えます。",
|
||||
text: "\u3051\u3093\u304b\u3001\u7206\u7af9\u3001\u3052\u3093\u3053\u3064\u3001\u30d4\u30f3\u3001\u30dc\u30fc\u30eb\u3001\u30a2\u30ea\u3001\u843d\u4e0b\u7269\u306a\u3069\u3067\u4f53\u529b\u304c\u6e1b\u308a\u307e\u3059\u3002\u4f53\u529b\u304c0\u306b\u306a\u308b\u3068\u6b7b\u4ea1\u3057\u3001\u6b7b\u56e0\u306f\u30ed\u30b0\u3068\u7d71\u8a08\u306b\u6b8b\u308a\u307e\u3059\u3002\u5371\u967a\u306a\u9053\u5177\u306f\u89b3\u5bdf\u306e\u6d41\u308c\u3092\u5927\u304d\u304f\u5909\u3048\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "道具と構造物",
|
||||
title: "\u9053\u5177\u3068\u69cb\u9020\u7269",
|
||||
image: "assets/ui/tool_duplicator.webp",
|
||||
text: "食べ物、薬、巣箱、柵、看板、複製機などを配置できます。複製機には対応する食べ物、薬、草などをセットできます。削除ツールでは不要な構造物や一部アイテムを取り除けます。",
|
||||
text: "\u98df\u3079\u7269\u3001\u85ac\u3001\u5de3\u7bb1\u3001\u67f5\u3001\u770b\u677f\u3001\u8907\u88fd\u6a5f\u306a\u3069\u3092\u914d\u7f6e\u3067\u304d\u307e\u3059\u3002\u8907\u88fd\u6a5f\u306b\u306f\u5bfe\u5fdc\u3059\u308b\u98df\u3079\u7269\u3001\u85ac\u3001\u8349\u306a\u3069\u3092\u30bb\u30c3\u30c8\u3067\u304d\u307e\u3059\u3002\u524a\u9664\u30c4\u30fc\u30eb\u3067\u306f\u4e0d\u8981\u306a\u69cb\u9020\u7269\u3084\u4e00\u90e8\u30a2\u30a4\u30c6\u30e0\u3092\u53d6\u308a\u9664\u3051\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "コロニーの見方",
|
||||
title: "\u30b3\u30ed\u30cb\u30fc\u306e\u898b\u65b9",
|
||||
image: "assets/sprites/tarinai_11_weak.webp",
|
||||
text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。観察では選択中の個体の健康、性格、関係、行動理由、記録を追えます。",
|
||||
text: "\u30b3\u30ed\u30cb\u30fc\u6b04\u3067\u306f\u500b\u4f53\u6570\u3001\u6b7b\u4ea1\u6570\u3001\u5168\u4f53\u72b6\u614b\u3001\u5730\u9762\u3001\u6b32\u6c42\u5e73\u5747\u3001\u7269\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002\u89b3\u5bdf\u3067\u306f\u9078\u629e\u4e2d\u306e\u500b\u4f53\u306e\u5065\u5eb7\u3001\u6027\u683c\u3001\u95a2\u4fc2\u3001\u884c\u52d5\u7406\u7531\u3001\u8a18\u9332\u3092\u8ffd\u3048\u307e\u3059\u3002",
|
||||
},
|
||||
],};
|
||||
|
||||
|
||||
TEXT_CATALOG.stateLabels = Object.freeze({
|
||||
idle: "ぼーっとしている",
|
||||
idle: "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b",
|
||||
seek_food: "\u98df\u3079\u7269\u3078\u5411\u304b\u3046",
|
||||
seek_bed: "\u5bdd\u5e8a\u3078\u5411\u304b\u3046",
|
||||
play_ball: "\u30dc\u30fc\u30eb\u904a\u3073",
|
||||
|
|
@ -92,17 +92,17 @@ TEXT_CATALOG.stateLabels = Object.freeze({
|
|||
|
||||
|
||||
TEXT_CATALOG.behaviorLabels = Object.freeze({
|
||||
seek_material: "材料を探している",
|
||||
build_structure: "作成している",
|
||||
build: "作成している",
|
||||
seek_enemy: "気になる相手へ向かっている",
|
||||
wander_lightly: "少し移動している",
|
||||
eat_food: "食事をしている",
|
||||
drink_water: "水を飲んでいる",
|
||||
sleep_in_bed: "寝床で休んでいる",
|
||||
sleep_anywhere: "休んでいる",
|
||||
intimidate_enemy: "威嚇している",
|
||||
panic_escape: "逃げている",
|
||||
seek_material: "\u6750\u6599\u3092\u63a2\u3057\u3066\u3044\u308b",
|
||||
build_structure: "\u4f5c\u6210\u3057\u3066\u3044\u308b",
|
||||
build: "\u4f5c\u6210\u3057\u3066\u3044\u308b",
|
||||
seek_enemy: "\u6c17\u306b\u306a\u308b\u76f8\u624b\u3078\u5411\u304b\u3063\u3066\u3044\u308b",
|
||||
wander_lightly: "\u5c11\u3057\u79fb\u52d5\u3057\u3066\u3044\u308b",
|
||||
eat_food: "\u98df\u4e8b\u3092\u3057\u3066\u3044\u308b",
|
||||
drink_water: "\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b",
|
||||
sleep_in_bed: "\u5bdd\u5e8a\u3067\u4f11\u3093\u3067\u3044\u308b",
|
||||
sleep_anywhere: "\u4f11\u3093\u3067\u3044\u308b",
|
||||
intimidate_enemy: "\u5a01\u5687\u3057\u3066\u3044\u308b",
|
||||
panic_escape: "\u9003\u3052\u3066\u3044\u308b",
|
||||
});
|
||||
|
||||
TEXT_CATALOG.behaviorLabel = function behaviorLabelFromCatalog(value = "") {
|
||||
|
|
@ -128,10 +128,10 @@ TEXT_CATALOG.targetLabels = TEXT_CATALOG.targetLabelOverrides;
|
|||
|
||||
TEXT_CATALOG.idleGroundText = function idleGroundText(t = null) {
|
||||
const ground = String(t?.world?.groundType || (typeof world !== "undefined" ? world?.groundType : "") || "soil");
|
||||
if (ground === "foot_massage") return "足つぼに耐えている。";
|
||||
if (ground === "blanket") return "毛布にうっとりしている。";
|
||||
if (ground === "ice") return "氷の上でじっとしている。";
|
||||
return "ぼーっとしている。";
|
||||
if (ground === "foot_massage") return "\u8db3\u3064\u307c\u306b\u8010\u3048\u3066\u3044\u308b\u3002";
|
||||
if (ground === "blanket") return "\u6bdb\u5e03\u306b\u3046\u3063\u3068\u308a\u3057\u3066\u3044\u308b\u3002";
|
||||
if (ground === "ice") return "\u6c37\u306e\u4e0a\u3067\u3058\u3063\u3068\u3057\u3066\u3044\u308b\u3002";
|
||||
return "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b\u3002";
|
||||
};
|
||||
|
||||
TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s, t = null) {
|
||||
|
|
@ -170,7 +170,7 @@ TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
|
|||
if (t.state === "intimidate") return target !== "\u306a\u3057" ? `${target}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b` : "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "ant_intimidate") return "\u30a2\u30ea\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "ant_attack") return "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}とへこへこしている` : "へこへこしている";
|
||||
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` : "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b";
|
||||
if (t.state === "sunbath") return "\u6674\u308c\u305f\u65e5\u306a\u305f\u3067\u30b9\u30c8\u30ec\u30b9\u3092\u4e0b\u3052\u3066\u3044\u308b";
|
||||
if (t.state === "frozen") return "\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u3067\u51b7\u51cd\u4fdd\u5b58\u3055\u308c\u3066\u3044\u308b";
|
||||
if (t.state === "seek_food") return target !== "\u306a\u3057" ? `${target}\u3092\u63a2\u3057\u3066\u3044\u308b` : "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
|
||||
|
|
@ -190,10 +190,10 @@ 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 (["待機", "待っている", "待っている。", "ぼーっとしている", "ぼーっとしている。", "周囲を見ている"].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.idleGroundText(t);
|
||||
if (thought) return thought;
|
||||
if (t.state === "idle" || !t.state) return TEXT_CATALOG.idleGroundText(t);
|
||||
return "周囲を見ている";
|
||||
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
|
||||
};
|
||||
|
||||
function stateLabel(s) { return TEXT_CATALOG.stateLabel(s); }
|
||||
|
|
|
|||
1
js/ui.js
1
js/ui.js
|
|
@ -28,6 +28,7 @@ const ui = {
|
|||
logPushEnabled: document.getElementById("logPushEnabled"),
|
||||
logPushOverlay: document.getElementById("logPushOverlay"),
|
||||
mobileModeControls: document.getElementById("mobileModeControls"),
|
||||
backToGameBtn: document.getElementById("backToGameBtn"),
|
||||
soundCategoryControls: document.getElementById("soundPanel"),
|
||||
toast: document.getElementById("toast"),
|
||||
statPop: document.getElementById("statPop"),
|
||||
|
|
|
|||
182
js/ui_bind.js
182
js/ui_bind.js
|
|
@ -14,14 +14,16 @@ function bindUI() {
|
|||
return Boolean(world?.tool && world.tool !== "observe");
|
||||
}
|
||||
function clickedInsideGameOrToolUi(target) {
|
||||
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target) || ui.panelQuickTabs?.contains?.(target));
|
||||
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target) || ui.panelQuickTabs?.contains?.(target) || ui.mobileModeControls?.contains?.(target) || ui.backToGameBtn?.contains?.(target));
|
||||
}
|
||||
|
||||
function panelSectionForTab(tab = "") {
|
||||
if (tab === "game") return document.querySelector(".stage-wrap") || canvas;
|
||||
if (tab === "selected") return ui.selectedCard;
|
||||
if (tab === "tools") return ui.toolCard;
|
||||
if (tab === "colony") return document.querySelector(".colony-card");
|
||||
if (tab === "events") return document.querySelector(".log-card");
|
||||
if (tab === "family") return document.querySelector(".lineage-main-card");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -31,27 +33,129 @@ function bindUI() {
|
|||
}
|
||||
}
|
||||
|
||||
function isTouchMobileLayout() {
|
||||
return Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
}
|
||||
|
||||
function markCollapsibleCardState(card, collapsed, { user = false } = {}) {
|
||||
if (!card) return;
|
||||
card.classList.toggle("collapsed", Boolean(collapsed));
|
||||
if (user) {
|
||||
if (collapsed) card.dataset.userCollapsed = "1";
|
||||
else delete card.dataset.userCollapsed;
|
||||
}
|
||||
const toggle = card.querySelector(".panel-card-toggle");
|
||||
toggle?.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
const state = toggle?.querySelector(".panel-card-state");
|
||||
if (state) state.textContent = collapsed ? "展開" : "折り畳み";
|
||||
}
|
||||
|
||||
function markToolCategoryState(category, collapsed, { user = false } = {}) {
|
||||
if (!category) return;
|
||||
category.classList.toggle("collapsed", Boolean(collapsed));
|
||||
if (user) {
|
||||
if (collapsed) category.dataset.userCollapsed = "1";
|
||||
else delete category.dataset.userCollapsed;
|
||||
}
|
||||
category.querySelector(".tool-category-toggle")?.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
}
|
||||
|
||||
function openAutoCollapsedSectionsForMobileScroll() {
|
||||
if (!isTouchMobileLayout()) return;
|
||||
for (const card of document.querySelectorAll(".collapsible-card.collapsed")) {
|
||||
if (card.dataset.userCollapsed === "1") continue;
|
||||
markCollapsibleCardState(card, false);
|
||||
}
|
||||
for (const category of ui.toolPalette?.querySelectorAll(".tool-category.collapsed") || []) {
|
||||
if (category.dataset.userCollapsed === "1") continue;
|
||||
markToolCategoryState(category, false);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollWindowToElement(target, offset = 0, behavior = "smooth") {
|
||||
if (!target) return;
|
||||
const scroller = document.scrollingElement || document.documentElement;
|
||||
const clampTop = (value) => Math.max(0, Math.min(value, Math.max(0, scroller.scrollHeight - window.innerHeight)));
|
||||
const desiredTop = () => clampTop(window.scrollY + target.getBoundingClientRect().top - offset);
|
||||
const go = (b = behavior) => window.scrollTo({ top: desiredTop(), behavior: b });
|
||||
go(behavior);
|
||||
// Explicit button scroll only. No scroll-snap listener is used; these
|
||||
// delayed corrections are only for DOM height changes after opening cards
|
||||
// or rendering the family graph.
|
||||
let tries = 0;
|
||||
const correct = () => {
|
||||
tries += 1;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const delta = rect.top - offset;
|
||||
if (Math.abs(delta) > 10) go("auto");
|
||||
if (tries < 5 && Math.abs(delta) > 10) window.setTimeout(correct, tries < 2 ? 180 : 320);
|
||||
};
|
||||
window.setTimeout(correct, 180);
|
||||
}
|
||||
|
||||
function scrollPanelToTab(tab = "") {
|
||||
const panel = ui.panel;
|
||||
if (!panel) return false;
|
||||
window.TarinaiTooltips?.hide?.();
|
||||
if (tab === "selected" && (!world.selected || world.selected.dead || !world.tarinai.includes(world.selected))) {
|
||||
selectTool("observe");
|
||||
uiCache.showSelectedEmpty = true;
|
||||
uiCache.selectedEmpty = false;
|
||||
renderSelected();
|
||||
showToast("たりないが未選択です。観察モードにしました。");
|
||||
showToast("たりないが未選択です。たりないデータを開ける状態にしました。");
|
||||
}
|
||||
const target = panelSectionForTab(tab);
|
||||
if (!target) return false;
|
||||
target.classList?.remove?.("hidden");
|
||||
if (tab === "family") {
|
||||
// Auto update OFF should not make the family graph disappear.
|
||||
// A direct jump to the family section is treated as a manual refresh,
|
||||
// while continuous auto-refresh remains disabled.
|
||||
window.resetArchiveRenderState?.();
|
||||
renderArchive?.();
|
||||
}
|
||||
if (isTouchMobileLayout()) openAutoCollapsedSectionsForMobileScroll();
|
||||
const collapsedCard = target.closest?.(".collapsible-card") || (target.classList?.contains?.("collapsible-card") ? target : null);
|
||||
if (collapsedCard?.classList?.contains?.("collapsed") && collapsedCard.dataset.userCollapsed !== "1") {
|
||||
markCollapsibleCardState(collapsedCard, false);
|
||||
}
|
||||
const tabsH = ui.panelQuickTabs?.offsetHeight || 0;
|
||||
const top = Math.max(0, target.offsetTop - tabsH - 8);
|
||||
panel.scrollTo({ top, behavior: "smooth" });
|
||||
const style = window.getComputedStyle ? window.getComputedStyle(panel) : null;
|
||||
const panelScrollable = panel.scrollHeight > panel.clientHeight + 4 && style?.overflowY !== "visible";
|
||||
const mobileOffset = isTouchMobileLayout()
|
||||
? ((ui.panelQuickTabs?.getBoundingClientRect?.().height || tabsH || 0) + 10)
|
||||
: 0;
|
||||
if (tab === "game") {
|
||||
scrollWindowToElement(target, 8);
|
||||
} else if (panelScrollable && !isTouchMobileLayout()) {
|
||||
const top = Math.max(0, target.offsetTop - tabsH - 8);
|
||||
panel.scrollTo({ top, behavior: "smooth" });
|
||||
} else {
|
||||
scrollWindowToElement(target, mobileOffset);
|
||||
}
|
||||
setActivePanelTab(tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
ui.panelQuickTabs?.addEventListener("click", (e) => {
|
||||
window.scrollPanelToTab = scrollPanelToTab;
|
||||
|
||||
const activatePanelQuickTab = (e) => {
|
||||
const btn = e.target.closest("[data-panel-tab]");
|
||||
if (!btn) return;
|
||||
const now = performance?.now?.() ?? Date.now();
|
||||
if (now - Number(ui.panelQuickTabs?.dataset?.lastActivateAt || 0) < 160) return;
|
||||
if (ui.panelQuickTabs) ui.panelQuickTabs.dataset.lastActivateAt = String(now);
|
||||
e.preventDefault();
|
||||
audio.uiClick?.();
|
||||
scrollPanelToTab(btn.dataset.panelTab || "");
|
||||
};
|
||||
|
||||
ui.panelQuickTabs?.addEventListener("click", activatePanelQuickTab);
|
||||
ui.panelQuickTabs?.addEventListener("pointerup", activatePanelQuickTab, { passive: false });
|
||||
ui.panelQuickTabs?.addEventListener("touchend", activatePanelQuickTab, { passive: false });
|
||||
|
||||
/* old quick-tab binding removed */
|
||||
if (false) ui.panelQuickTabs?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-panel-tab]");
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -64,10 +168,12 @@ function bindUI() {
|
|||
if (!panel || !ui.panelQuickTabs) return;
|
||||
const tabsH = ui.panelQuickTabs.offsetHeight || 0;
|
||||
const entries = [
|
||||
["game", document.querySelector(".stage-wrap") || canvas],
|
||||
["selected", ui.selectedCard],
|
||||
["tools", ui.toolCard],
|
||||
["colony", document.querySelector(".colony-card")],
|
||||
["events", document.querySelector(".log-card")],
|
||||
["family", document.querySelector(".lineage-main-card")],
|
||||
].filter(([, el]) => el && !el.classList.contains("hidden"));
|
||||
let active = entries[0]?.[0] || "";
|
||||
const y = panel.scrollTop + tabsH + 18;
|
||||
|
|
@ -79,6 +185,54 @@ function bindUI() {
|
|||
|
||||
syncToolSizeBadges();
|
||||
setActivePanelTab(ui.selectedCard?.classList.contains("hidden") ? "tools" : "selected");
|
||||
|
||||
function setCollapsibleCardState(card, collapsed) {
|
||||
markCollapsibleCardState(card, collapsed);
|
||||
}
|
||||
|
||||
function applyMobileInitialPanelState() {
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classify?.().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
||||
if (!touchFirst || uiCache.mobileInitialPanelStateApplied) return;
|
||||
uiCache.mobileInitialPanelStateApplied = true;
|
||||
setCollapsibleCardState(document.querySelector(".colony-card"), true);
|
||||
setCollapsibleCardState(document.querySelector(".log-card"), true);
|
||||
for (const category of ui.toolPalette?.querySelectorAll(".tool-category") || []) {
|
||||
const keepOpen = category.dataset.toolCategory === "operate";
|
||||
markToolCategoryState(category, !keepOpen);
|
||||
}
|
||||
}
|
||||
applyMobileInitialPanelState();
|
||||
window.addEventListener("resize", applyMobileInitialPanelState, { passive: true });
|
||||
|
||||
function updateBackToGameButton() {
|
||||
const btn = ui.backToGameBtn;
|
||||
if (!btn) return;
|
||||
const stage = document.querySelector(".stage-wrap");
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
if (!touchFirst || !stage) { btn.hidden = true; return; }
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const visible = rect.bottom > 42 && rect.top < Math.min(window.innerHeight * 0.72, window.innerHeight - 90);
|
||||
btn.hidden = visible;
|
||||
}
|
||||
ui.backToGameBtn?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
audio.uiClick?.();
|
||||
scrollPanelToTab("game");
|
||||
});
|
||||
window.addEventListener("scroll", updateBackToGameButton, { passive: true });
|
||||
window.addEventListener("resize", updateBackToGameButton, { passive: true });
|
||||
updateBackToGameButton();
|
||||
|
||||
window.TarinaiEvents?.on?.("selection:changed", () => {
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const mode = window.TarinaiInputMode?.currentMode || uiCache.mobileInputMode || "auto";
|
||||
if (!touchFirst || mode !== "auto" || world.tool !== "observe" || !world.selected || world.selected.dead) return;
|
||||
window.setTimeout(() => {
|
||||
renderSelected?.();
|
||||
scrollPanelToTab("selected");
|
||||
}, 40);
|
||||
});
|
||||
|
||||
function activeRotatableToolType() {
|
||||
const type = typeof toolItemType === "function" ? toolItemType(world?.tool || "") : "";
|
||||
return type && typeof isRotatableItemType === "function" && isRotatableItemType(type) ? type : "";
|
||||
|
|
@ -110,10 +264,8 @@ function bindUI() {
|
|||
const card = toggle.closest(".collapsible-card");
|
||||
if (!card) return;
|
||||
audio.uiFold?.();
|
||||
const collapsed = card.classList.toggle("collapsed");
|
||||
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
const state = toggle.querySelector(".panel-card-state");
|
||||
if (state) state.textContent = collapsed ? "\u5c55\u958b" : "\u6298\u308a\u7573\u307f";
|
||||
const collapsed = !card.classList.contains("collapsed");
|
||||
markCollapsibleCardState(card, collapsed, { user: true });
|
||||
});
|
||||
}
|
||||
ui.colonyChartTabs?.addEventListener("click", (e) => {
|
||||
|
|
@ -314,17 +466,23 @@ function bindUI() {
|
|||
const category = toggle.closest(".tool-category");
|
||||
if (!category) return;
|
||||
audio.uiFold?.();
|
||||
category.classList.toggle("collapsed");
|
||||
toggle.setAttribute("aria-expanded", category.classList.contains("collapsed") ? "false" : "true");
|
||||
const collapsed = !category.classList.contains("collapsed");
|
||||
markToolCategoryState(category, collapsed, { user: true });
|
||||
return;
|
||||
}
|
||||
const btn = e.target.closest(".tool");
|
||||
if (!btn) return;
|
||||
if (btn.dataset.suppressNextClick === "1") {
|
||||
btn.dataset.suppressNextClick = "";
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const tool = btn.dataset.tool;
|
||||
audio.uiClick?.();
|
||||
if (tool === "undo" || tool === "redo") {
|
||||
const result = window.TarinaiCommands?.dispatch?.(world, { type: tool === "undo" ? "history.undo" : "history.redo" });
|
||||
showToast(result?.ok ? (tool === "undo" ? "戻しました。" : "進めました。") : (tool === "undo" ? "戻せる操作がありません。" : "進める操作がありません。"));
|
||||
showToast(result?.ok ? (tool === "undo" ? "\u623b\u3057\u307e\u3057\u305f\u3002" : "\u9032\u3081\u307e\u3057\u305f\u3002") : (tool === "undo" ? "\u623b\u305b\u308b\u64cd\u4f5c\u304c\u3042\u308a\u307e\u305b\u3093\u3002" : "\u9032\u3081\u308b\u64cd\u4f5c\u304c\u3042\u308a\u307e\u305b\u3093\u3002"));
|
||||
renderStats();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
"use strict";
|
||||
|
||||
function colonyMoodTip() {
|
||||
const mood = world?.evaluateColonyMood?.(true) || world?.colonyMood || null;
|
||||
return String(mood?.description || "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002");
|
||||
}
|
||||
|
||||
function bindColonyMoodTooltip() {
|
||||
const el = ui?.statColonyMood;
|
||||
const target = el?.closest?.(".stat") || el;
|
||||
if (!target || target.dataset.colonyMoodTipBound === "1") return;
|
||||
target.dataset.colonyMoodTipBound = "1";
|
||||
target.tabIndex = target.tabIndex >= 0 ? target.tabIndex : 0;
|
||||
window.TarinaiTooltips?.bind?.(target, colonyMoodTip);
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const alive = world.tarinai;
|
||||
const avg = (fn) => alive.length ? alive.reduce((a, t) => a + fn(t), 0) / alive.length : 0;
|
||||
|
|
@ -14,7 +28,10 @@ function renderStats() {
|
|||
};
|
||||
setTextIfChanged(ui.statPop, "pop", values.pop);
|
||||
setTextIfChanged(ui.statDead, "dead", values.dead);
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", world.colonyMood?.label || "安定");
|
||||
const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" };
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a");
|
||||
bindColonyMoodTooltip();
|
||||
window.TarinaiTooltips?.refresh?.(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood);
|
||||
window.TarinaiGroundUI?.update?.(world);
|
||||
updateColonyHistory(values);
|
||||
drawColonyChart(values);
|
||||
|
|
@ -143,22 +160,22 @@ function drawColonyChart(values) {
|
|||
|
||||
function chartSeries(mode = "population") {
|
||||
if (mode === "life") return [
|
||||
{ key: "stress", label: "ストレス", color: "#d96262", percent: true },
|
||||
{ key: "need_food", label: "摂餌", color: "#e4a33f", percent: true },
|
||||
{ key: "need_sleep", label: "睡眠", color: "#6f8edc", percent: true },
|
||||
{ key: "need_health", label: "健康", color: "#2e9d68", percent: true },
|
||||
{ key: "need_safety", label: "安全", color: "#d65e46", percent: true },
|
||||
{ key: "need_social", label: "関係", color: "#9b78d4", percent: true },
|
||||
{ key: "need_fulfill", label: "充足", color: "#0072b2", percent: true },
|
||||
{ key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true },
|
||||
{ key: "need_food", label: "\u6442\u990c", color: "#e4a33f", percent: true },
|
||||
{ key: "need_sleep", label: "\u7761\u7720", color: "#6f8edc", percent: true },
|
||||
{ key: "need_health", label: "\u5065\u5eb7", color: "#2e9d68", percent: true },
|
||||
{ key: "need_safety", label: "\u5b89\u5168", color: "#d65e46", percent: true },
|
||||
{ key: "need_social", label: "\u95a2\u4fc2", color: "#9b78d4", percent: true },
|
||||
{ key: "need_fulfill", label: "\u5145\u8db3", color: "#0072b2", percent: true },
|
||||
];
|
||||
if (mode === "objects") return [
|
||||
{ key: "obj_grass", label: "草", color: "#5b9d61" },
|
||||
{ key: "obj_zunchi", label: "ずんち", color: "#6f8a3f" },
|
||||
{ key: "obj_trace", label: "死骸", color: "#8a7054" },
|
||||
{ key: "obj_grass_bed", label: "かんたんベッド", color: "#4f9bc4" },
|
||||
{ key: "obj_water", label: "雨滴", color: "#5aa8d6" },
|
||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
||||
{ key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" },
|
||||
{ key: "obj_grass_bed", label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", color: "#4f9bc4" },
|
||||
{ key: "obj_water", label: "\u96e8\u6ef4", color: "#5aa8d6" },
|
||||
];
|
||||
return [{ key: "pop", label: "個体", color: "#5d8ee6" }];
|
||||
return [{ key: "pop", label: "\u500b\u4f53", color: "#5d8ee6" }];
|
||||
}
|
||||
|
||||
function formatChartValue(key, values) {
|
||||
|
|
|
|||
|
|
@ -170,6 +170,12 @@ function familyDeathReasonLabel(rawReason, entity = null) {
|
|||
const text = String(rawReason || "");
|
||||
if (!text) return "";
|
||||
if (entity?.briefDeathReason) return entity.briefDeathReason(text);
|
||||
const collisionText = text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
const collisionWith = collisionText.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/);
|
||||
if (collisionWith) {
|
||||
const label = `${collisionWith[1]}\u3068\u306e\u885d\u7a81${text.includes("\u3067\u8870\u5f31") ? "\u3067\u8870\u5f31" : ""}`;
|
||||
return label.length > 14 ? `${label.slice(0, 14)}\u2026` : label;
|
||||
}
|
||||
if (text.includes("\u3067\u8870\u5f31")) return text.length > 12 ? `${text.slice(0, 12)}\u2026` : text;
|
||||
if (/\u55a7\u5629|fight/.test(text)) return "\u55a7\u5629";
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9";
|
||||
|
|
|
|||
|
|
@ -7,6 +7,29 @@ function lineageNodeSort(a, b) {
|
|||
String(a.id || "").localeCompare(String(b.id || ""));
|
||||
}
|
||||
|
||||
function lineageGeneticPercentFromNode(n, key = "") {
|
||||
const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null;
|
||||
if (entity?.geneticStatRatio) return Math.round((entity.geneticStatRatio(key) - 1) * 100);
|
||||
const g = typeof normalizeGenetics === "function" ? normalizeGenetics(n?.genetics, n?.id || "") : (n?.genetics || {});
|
||||
const map = { life: "lifeSpanMul", attack: "attackMul", speed: "speedMul", size: "sizeMul" };
|
||||
const ratio = Number(g?.[map[key] || key]) || 1;
|
||||
return Math.round((ratio - 1) * 100);
|
||||
}
|
||||
|
||||
function lineageSignedPercentText(value = 0) {
|
||||
const n = Math.round(Number(value) || 0);
|
||||
return `${n >= 0 ? "+" : ""}${n}%`;
|
||||
}
|
||||
|
||||
function lineageFightRateText(n) {
|
||||
const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null;
|
||||
const wins = Number(entity?.totalFightWins ?? n?.totalFightWins ?? 0) || 0;
|
||||
const losses = Number(entity?.totalFightLosses ?? n?.totalFightLosses ?? 0) || 0;
|
||||
const total = wins + losses;
|
||||
if (!total) return "--";
|
||||
return `${Math.round((wins / total) * 100)}%/${total}`;
|
||||
}
|
||||
|
||||
function lineageTreeNodeHtml(n, x, y) {
|
||||
const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null;
|
||||
const live = entity && !entity.dead ? entity : null;
|
||||
|
|
@ -23,6 +46,8 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}${tagText}`);
|
||||
const deathLine = briefReason ? `<span class="death-reason">${escapeHtml(briefReason)}</span>` : "";
|
||||
const tagLine = tags.length ? `<span class="family-personality-tags">${tags.map(tag => `<em>${escapeHtml(tag)}</em>`).join("")}</span>` : "";
|
||||
const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion);
|
||||
const statLine = n ? `<span class="family-genetic-stats" title="寿命 / 体格 / 攻撃 / 速度 / 勝率"><b>寿</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "life")))} <b>体</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "size")))} <b>攻</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "attack")))} <b>速</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "speed")))} <b>勝</b>${escapeHtml(lineageFightRateText(n))}</span>` : "";
|
||||
const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing";
|
||||
const childCls = (n?.parents || []).length ? " child" : "";
|
||||
const iconBaseScale = live
|
||||
|
|
@ -30,11 +55,12 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
: (Number.isFinite(n?.scale) ? n.scale : (Number.isFinite(n?.adultScale) ? n.adultScale : 0.28));
|
||||
const iconScale = clamp((Number(iconBaseScale) || 0.28) / 0.28, 0.64, 1.42).toFixed(2);
|
||||
const data = n?.id ? ` data-family-tarinai-id="${escapeHtml(n.id)}"` : "";
|
||||
return `<div class="family-node tree-node ${cls}${childCls}"${data} title="${title}" style="left:${Math.round(x)}px;top:${Math.round(y)}px;--family-icon-scale:${iconScale}">
|
||||
return `<div class="family-node tree-node ${cls}${childCls}${champion ? " champion" : ""}"${data} title="${title}" style="left:${Math.round(x)}px;top:${Math.round(y)}px;--family-icon-scale:${iconScale}">
|
||||
<div class="family-portrait" aria-hidden="true"><img src="${img}" loading="lazy" decoding="async" alt=""></div>
|
||||
<div class="family-copy">
|
||||
<strong>${name}</strong>
|
||||
<span>${escapeHtml(gen)} / ${escapeHtml(status)}</span>
|
||||
<strong>${champion ? "♛ " : ""}${name}</strong>
|
||||
<span>${escapeHtml(gen)} / ${escapeHtml(status)}${champion ? " / 王者" : ""}</span>
|
||||
${statLine}
|
||||
${deathLine}
|
||||
${tagLine}
|
||||
</div>
|
||||
|
|
@ -351,8 +377,8 @@ function lineageBuildRowGroups(row, idSet) {
|
|||
}
|
||||
|
||||
function lineageLayoutRows(rows, idSet) {
|
||||
const NODE_W = 132;
|
||||
const NODE_H = 84;
|
||||
const NODE_W = 150;
|
||||
const NODE_H = 104;
|
||||
const SIBLING_GAP = 32;
|
||||
const GROUP_GAP = 52;
|
||||
const PARTNER_GROUP_GAP = 24;
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ function lineageFastLayoutNeeded(nodes, idSet, family) {
|
|||
}
|
||||
|
||||
function lineageLayoutRowsFast(rows, idSet) {
|
||||
const NODE_W = 132;
|
||||
const NODE_H = 84;
|
||||
const NODE_W = 150;
|
||||
const NODE_H = 104;
|
||||
const SIBLING_GAP = 22;
|
||||
const ROW_GAP = 116;
|
||||
const LEFT_PAD = 82;
|
||||
|
|
@ -159,14 +159,23 @@ function lineageFamilyHtml(component, index, family, signature = "") {
|
|||
|
||||
function renderArchive() {
|
||||
if (!ui.archiveContent) return;
|
||||
// Auto update OFF now means "do not keep refreshing", not "hide the tree".
|
||||
// Draw the current snapshot once so the family tree is always available under
|
||||
// the event UI, then mark it stale when the family data changes.
|
||||
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) {
|
||||
uiCache.archiveScheduled = false;
|
||||
if (!uiCache.archiveVersion) {
|
||||
ui.archiveContent.innerHTML = `<div class="selected-info empty">\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306fOFF\u3067\u3059\u3002\u300c\u66f4\u65b0\u300d\u3092ON\u306b\u3059\u308b\u3068\u63cf\u753b\u3057\u307e\u3059\u3002</div>`;
|
||||
} else if (world.familyTreeDirty) {
|
||||
lineageSetArchiveStaleStatus(true);
|
||||
if (uiCache.archiveVersion && world.familyTreeDirty) {
|
||||
const hasRenderedGraph = Boolean(ui.archiveContent.querySelector(".lineage-family-tree, [data-family-tarinai-id]"));
|
||||
const familyHasEdges = Object.values(world.family || {}).some(n => (n?.parents || []).length || (n?.children || []).length);
|
||||
// If the first snapshot was empty, keep OFF but allow the first real
|
||||
// family graph to appear. Subsequent changes are marked stale.
|
||||
if (!hasRenderedGraph && familyHasEdges) {
|
||||
resetArchiveRenderState();
|
||||
} else {
|
||||
lineageSetArchiveStaleStatus(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (uiCache.deferArchiveUntil && performance.now() < uiCache.deferArchiveUntil) return;
|
||||
uiCache.deferArchiveUntil = 0;
|
||||
|
|
|
|||
|
|
@ -16,27 +16,27 @@
|
|||
function groundDef(worldRef = null) {
|
||||
const worldObj = activeWorld(worldRef);
|
||||
const id = worldObj?.groundType || "soil";
|
||||
return global.TarinaiGround?.definition?.(id) || (typeof GROUND_TYPES !== "undefined" ? GROUND_TYPES[id] || GROUND_TYPES.soil : { id: "soil", label: "砂", description: "普通の地面。" });
|
||||
return global.TarinaiGround?.definition?.(id) || (typeof GROUND_TYPES !== "undefined" ? GROUND_TYPES[id] || GROUND_TYPES.soil : { id: "soil", label: "\u7802", description: "\u666e\u901a\u306e\u5730\u9762\u3002" });
|
||||
}
|
||||
|
||||
function groundTip(worldRef = null) {
|
||||
const def = groundDef(worldRef);
|
||||
if (global.TarinaiGround?.tooltipText) return global.TarinaiGround.tooltipText(def.id);
|
||||
return `${def.label || "地面"}\n${def.description || "クリックで切り替え"}`;
|
||||
return `${def.label || "\u5730\u9762"}\n${def.description || "\u30af\u30ea\u30c3\u30af\u3067\u5207\u308a\u66ff\u3048"}`;
|
||||
}
|
||||
|
||||
function updateGroundUI(worldRef = null) {
|
||||
const uiRef = global.ui || {};
|
||||
const def = groundDef(worldRef);
|
||||
const label = def.label || "砂";
|
||||
const label = def.label || "\u7802";
|
||||
const statEl = uiRef.statGroundType || document.getElementById("statGroundType");
|
||||
const btnEl = uiRef.groundTypeBtn || document.getElementById("groundTypeBtn");
|
||||
if (statEl) setTextIfChanged(statEl, "groundType", label);
|
||||
if (btnEl) {
|
||||
const desc = def.description || "クリックで切り替え";
|
||||
const desc = def.description || "\u30af\u30ea\u30c3\u30af\u3067\u5207\u308a\u66ff\u3048";
|
||||
btnEl.dataset.tip = desc;
|
||||
btnEl.removeAttribute("title");
|
||||
btnEl.setAttribute("aria-label", desc ? `地面を切り替える。現在は${label}。${desc}` : `地面を切り替える。現在は${label}`);
|
||||
btnEl.setAttribute("aria-label", desc ? `\u5730\u9762\u3092\u5207\u308a\u66ff\u3048\u308b\u3002\u73fe\u5728\u306f${label}\u3002${desc}` : `\u5730\u9762\u3092\u5207\u308a\u66ff\u3048\u308b\u3002\u73fe\u5728\u306f${label}`);
|
||||
global.TarinaiTooltips?.refresh?.(btnEl);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
cycleGroundFromEvent.lastAt = now;
|
||||
const currentWorld = activeWorld(fallbackWorld);
|
||||
if (!currentWorld?.cycleGroundType) {
|
||||
if (typeof showToast === "function") showToast("地面をまだ切り替えられません。");
|
||||
if (typeof showToast === "function") showToast("\u5730\u9762\u3092\u307e\u3060\u5207\u308a\u66ff\u3048\u3089\u308c\u307e\u305b\u3093\u3002");
|
||||
return;
|
||||
}
|
||||
global.audio?.uiClick?.();
|
||||
|
|
|
|||
|
|
@ -15,12 +15,11 @@
|
|||
const inputModeManager = global.TarinaiInputMode;
|
||||
|
||||
function setMobileInputMode(mode = "auto") {
|
||||
mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto");
|
||||
mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool"].includes(mode) ? mode : "auto");
|
||||
uiCache.mobileInputMode = mode;
|
||||
ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode));
|
||||
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
||||
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
||||
document.body.classList.toggle("mobile-mode-family", mode === "family");
|
||||
return mode;
|
||||
}
|
||||
|
||||
|
|
@ -40,13 +39,25 @@
|
|||
window.addEventListener("resize", updateTouchMobileUiClass, { passive: true });
|
||||
window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass);
|
||||
setMobileInputMode(uiCache.mobileInputMode || "auto");
|
||||
ui.mobileModeControls?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-mobile-mode]");
|
||||
if (!btn) return;
|
||||
const root = ui.mobileModeControls;
|
||||
if (!root || root.dataset.bound === "1") return;
|
||||
root.dataset.bound = "1";
|
||||
const activate = (e) => {
|
||||
const btn = e.target?.closest?.("[data-mobile-mode]");
|
||||
if (!btn || !root.contains(btn)) return;
|
||||
const now = performance?.now?.() ?? Date.now();
|
||||
if (now - Number(root.dataset.lastActivateAt || 0) < 180) return;
|
||||
root.dataset.lastActivateAt = String(now);
|
||||
e.preventDefault?.();
|
||||
e.stopPropagation?.();
|
||||
window.TarinaiTooltips?.hide?.();
|
||||
audio.uiClick?.();
|
||||
setMobileInputMode(btn.dataset.mobileMode || "auto");
|
||||
showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`);
|
||||
});
|
||||
};
|
||||
root.addEventListener("pointerup", activate, { passive: false });
|
||||
root.addEventListener("touchend", activate, { passive: false });
|
||||
root.addEventListener("click", activate);
|
||||
}
|
||||
|
||||
function touchPoint(touches, index = 0) {
|
||||
|
|
@ -334,7 +345,7 @@
|
|||
}
|
||||
|
||||
function updateShootHoverTarget(point = null) {
|
||||
// 射撃ツールは照準だけを表示し、対象ハイライトは出さない。
|
||||
// \u5c04\u6483\u30c4\u30fc\u30eb\u306f\u7167\u6e96\u3060\u3051\u3092\u8868\u793a\u3057\u3001\u5bfe\u8c61\u30cf\u30a4\u30e9\u30a4\u30c8\u306f\u51fa\u3055\u306a\u3044\u3002
|
||||
if (world.hoverShootTarget) {
|
||||
world.hoverShootTarget = null;
|
||||
world.drawListDirty = true;
|
||||
|
|
@ -450,8 +461,8 @@
|
|||
world.rebuildSpatial?.();
|
||||
renderStats();
|
||||
if (removed > 0) {
|
||||
world.log?.(`範囲削除で${removed}個消した。`, "observe");
|
||||
showToast(`${removed}個消しました。`);
|
||||
world.log?.(`\u7bc4\u56f2\u524a\u9664\u3067${removed}\u500b\u6d88\u3057\u305f\u3002`, "observe");
|
||||
showToast(`${removed}\u500b\u6d88\u3057\u307e\u3057\u305f\u3002`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
e.preventDefault();
|
||||
uiCache.touchMoved = false;
|
||||
const mode = ctx.mobileInputMode();
|
||||
if ((mode === "camera" || mode === "family") && e.touches.length === 1) {
|
||||
if (mode === "camera" && e.touches.length === 1) {
|
||||
const t = e.touches[0];
|
||||
uiCache.touchStartX = t.clientX;
|
||||
uiCache.touchStartY = t.clientY;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,10 @@ window.pushLogNotification = pushLogNotification;
|
|||
function renderLog(logs) {
|
||||
ui.log.innerHTML = "";
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const raw of logs.slice(0, 32)) {
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const limit = touchFirst ? 34 : 80;
|
||||
const rows = Array.isArray(logs) ? logs.slice(0, limit) : [];
|
||||
for (const raw of rows) {
|
||||
const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw;
|
||||
const kind = entry.kind || "note";
|
||||
const div = document.createElement("div");
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ function renderSelected() {
|
|||
uiCache.selectedEmpty = true;
|
||||
uiCache.selectedEmptyVisible = showEmpty;
|
||||
ui.selectedInfo.className = "selected-info empty";
|
||||
ui.selectedInfo.textContent = "たりないを選択してください";
|
||||
ui.selectedInfo.textContent = "\u305f\u308a\u306a\u3044\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ function renderSelected() {
|
|||
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
||||
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
||||
relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightWinRateLabel(t),
|
||||
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
||||
t.isZunchiSlave ? 1 : 0, t.isTarinaiChampion ? 1 : 0, fmt(t.totalFightWins || 0), fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
||||
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0),
|
||||
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
|
||||
personalitySnapshot, personalityDailySnapshot(t),
|
||||
|
|
@ -232,6 +232,7 @@ function renderSelected() {
|
|||
ui.selectedInfo.className = "selected-info";
|
||||
const effectBadges = [];
|
||||
if (t.isZunchiSlave) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u3069\u308c\u3044</span>`);
|
||||
if (t.isTarinaiChampion) effectBadges.push(`<span class="badge">\u265b \u305f\u308a\u306a\u3044\u738b\u8005</span>`);
|
||||
if (t.favorite) effectBadges.push(`<span class="badge">\u2605</span>`);
|
||||
if (t.zunchiDisease) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u75c5 ${fmt((t.zunchiDiseaseSeverity || 0) * 100)}</span>`);
|
||||
if (t.sleepDisease) effectBadges.push(`<span class="badge">\u306d\u3080\u308a\u75c5</span>`);
|
||||
|
|
@ -253,7 +254,7 @@ function renderSelected() {
|
|||
const timeText = world.clockString ? (world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0)) : formatRecordTime(r.time || 0);
|
||||
const recordText = quoteNameInRecordText(r.text || "", t.name);
|
||||
return `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(recordText)}</strong></li>`;
|
||||
}).join("") || `<li><span>-</span><strong>まだ記憶がありません</strong></li>`;
|
||||
}).join("") || `<li><span>-</span><strong>\u307e\u3060\u8a18\u61b6\u304c\u3042\u308a\u307e\u305b\u3093</strong></li>`;
|
||||
const basicHtml = `
|
||||
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
||||
|
|
@ -273,16 +274,18 @@ 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("\u6e80\u8179\u5ea6", fmt(Math.max(0, Math.min(100, 100 - (Number(t.hunger) || 0)))), "fullness"),
|
||||
].join("");
|
||||
const itemEffectHtml = itemEffectSummary.length
|
||||
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
|
||||
: `<div class="info-row"><span>\u30a2\u30a4\u30c6\u30e0\u52b9\u679c</span><strong>\u306a\u3057</strong></div>`;
|
||||
const relationHtml = `
|
||||
<div class="info-row relation-row relation-list-row"><span>友達</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>敵対</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>親</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "不明")}</strong></div>
|
||||
<div class="info-row"><span>子</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row"><span>けんか勝率</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||
<div class="info-row"><span>\u5b50</span><strong>${t.children?.length || 0}</strong></div>
|
||||
<div class="info-row"><span>\u3051\u3093\u304b\u52dd\u7387</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
<div class="info-row"><span>\u79f0\u53f7</span><strong>${t.isTarinaiChampion ? "\u305f\u308a\u306a\u3044\u738b\u8005" : "\u306a\u3057"}</strong></div>
|
||||
`;
|
||||
ui.selectedInfo.innerHTML = `
|
||||
<div class="selected-name-editor">
|
||||
|
|
@ -296,7 +299,7 @@ function renderSelected() {
|
|||
${selectedDataCategoryHtml("personality", "\u6027\u683c", personalityHtml)}
|
||||
${selectedDataCategoryHtml("relation", "\u95a2\u4fc2\u3068\u5bb6\u65cf", relationHtml)}
|
||||
${selectedDataCategoryHtml("inventory", "\u6240\u6301\u54c1", selectedInventoryHtml(t))}
|
||||
${selectedDataCategoryHtml("memories", "最近の記憶", `<div class="selected-records selected-memories"><ol>${recentMemoryHtml}</ol></div>`)}
|
||||
${selectedDataCategoryHtml("memories", "\u6700\u8fd1\u306e\u8a18\u61b6", `<div class="selected-records selected-memories"><ol>${recentMemoryHtml}</ol></div>`)}
|
||||
</div>
|
||||
<div class="selected-actions"><button type="button" class="favorite-toggle ${t.favorite ? "active" : ""}" data-selected-action="favorite">${t.favorite ? "\u2605 \u304a\u6c17\u306b\u5165\u308a\u89e3\u9664" : "\u2606 \u304a\u6c17\u306b\u5165\u308a"}</button></div>
|
||||
`;
|
||||
|
|
@ -314,11 +317,11 @@ function stateLabel(s) {
|
|||
}
|
||||
|
||||
function groundAwareIdleText(t) {
|
||||
return window.TEXT_CATALOG?.idleGroundText?.(t) || "ぼーっとしている。";
|
||||
return window.TEXT_CATALOG?.idleGroundText?.(t) || "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b\u3002";
|
||||
}
|
||||
|
||||
function isIdleDisplayText(value = "") {
|
||||
return ["待機", "待っている", "待っている。", "ぼーっとしている", "ぼーっとしている。", "周囲を見ている"].includes(String(value || "").trim());
|
||||
return ["\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(String(value || "").trim());
|
||||
}
|
||||
|
||||
function reasonLabel(t) {
|
||||
|
|
@ -343,7 +346,7 @@ function reasonLabel(t) {
|
|||
return normalized;
|
||||
}
|
||||
if (isIdleDisplayText(live)) return groundAwareIdleText(t);
|
||||
if ((!live || live === "なし") && (String(t.state || "") === "idle" || !t.state)) return groundAwareIdleText(t);
|
||||
if ((!live || live === "\u306a\u3057") && (String(t.state || "") === "idle" || !t.state)) return groundAwareIdleText(t);
|
||||
return live || "\u306a\u3057";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ function renderToolPalette() {
|
|||
const section = document.createElement("section");
|
||||
section.className = `tool-category ${category.themeClass || ""}`.trim();
|
||||
section.dataset.toolCategory = category.id;
|
||||
const mobileCompact = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const initiallyCollapsed = mobileCompact && category.id !== "operate";
|
||||
if (initiallyCollapsed) section.classList.add("collapsed");
|
||||
const toggle = document.createElement("button");
|
||||
toggle.className = "tool-category-toggle";
|
||||
toggle.type = "button";
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
toggle.setAttribute("aria-expanded", initiallyCollapsed ? "false" : "true");
|
||||
toggle.textContent = category.label || category.id;
|
||||
const body = document.createElement("div");
|
||||
body.className = "tool-grid tool-category-body";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
(function (global) {
|
||||
let activeTooltipElement = null;
|
||||
let hideTimer = 0;
|
||||
let longPressTimer = 0;
|
||||
|
||||
function ensureTooltipPopover() {
|
||||
let pop = document.getElementById("toolTipPopover");
|
||||
|
|
@ -20,10 +22,11 @@
|
|||
return String(el.dataset?.tip || "");
|
||||
}
|
||||
|
||||
function showTooltip(el) {
|
||||
function showTooltip(el, options = {}) {
|
||||
if (!el || !document.documentElement.contains(el)) return;
|
||||
const tip = resolveTip(el);
|
||||
if (!tip) return;
|
||||
if (hideTimer) { clearTimeout(hideTimer); hideTimer = 0; }
|
||||
const pop = ensureTooltipPopover();
|
||||
activeTooltipElement = el;
|
||||
pop.textContent = tip;
|
||||
|
|
@ -36,10 +39,15 @@
|
|||
const left = preferLeft >= 8 ? preferLeft : rightSide;
|
||||
pop.style.left = `${clamp(left, 8, Math.max(8, window.innerWidth - popW - 8))}px`;
|
||||
pop.style.top = `${clamp(rect.top + rect.height / 2 - popH / 2, 8, Math.max(8, window.innerHeight - popH - 8))}px`;
|
||||
if (options.autoHide || document.body.classList.contains("touch-mobile-ui")) {
|
||||
hideTimer = setTimeout(hideTooltip, Number(options.delay || 1800));
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
activeTooltipElement = null;
|
||||
if (hideTimer) { clearTimeout(hideTimer); hideTimer = 0; }
|
||||
if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = 0; }
|
||||
ensureTooltipPopover().classList.add("hidden");
|
||||
}
|
||||
|
||||
|
|
@ -64,13 +72,27 @@
|
|||
el.removeAttribute?.("title");
|
||||
if (el.dataset.tipBound === "1") return;
|
||||
el.dataset.tipBound = "1";
|
||||
el.addEventListener("mouseenter", () => showTooltip(el));
|
||||
el.addEventListener("focus", () => showTooltip(el));
|
||||
el.addEventListener("mouseenter", () => { if (!document.body.classList.contains("touch-mobile-ui")) showTooltip(el); });
|
||||
el.addEventListener("focus", () => { if (!document.body.classList.contains("touch-mobile-ui")) showTooltip(el); });
|
||||
el.addEventListener("mouseleave", hideTooltip);
|
||||
el.addEventListener("blur", hideTooltip);
|
||||
el.addEventListener("pointerdown", () => {
|
||||
if (activeTooltipElement === el) hideTooltip();
|
||||
if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = 0; }
|
||||
if (activeTooltipElement === el || document.body.classList.contains("touch-mobile-ui")) hideTooltip();
|
||||
});
|
||||
el.addEventListener("touchstart", () => {
|
||||
if (!document.body.classList.contains("touch-mobile-ui")) return;
|
||||
if (longPressTimer) clearTimeout(longPressTimer);
|
||||
longPressTimer = setTimeout(() => {
|
||||
longPressTimer = 0;
|
||||
el.dataset.suppressNextClick = "1";
|
||||
setTimeout(() => { if (el.dataset.suppressNextClick === "1") el.dataset.suppressNextClick = ""; }, 900);
|
||||
showTooltip(el, { autoHide: true, delay: 2600 });
|
||||
}, 520);
|
||||
}, { passive: true });
|
||||
el.addEventListener("touchmove", () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = 0; } }, { passive: true });
|
||||
el.addEventListener("touchend", () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = 0; } }, { passive: true });
|
||||
el.addEventListener("touchcancel", () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = 0; } }, { passive: true });
|
||||
}
|
||||
|
||||
function applyToolPaletteTooltips() {
|
||||
|
|
@ -82,6 +104,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (!global.__tarinaiTooltipGlobalBound) {
|
||||
global.__tarinaiTooltipGlobalBound = true;
|
||||
document.addEventListener("pointerdown", (e) => {
|
||||
if (!activeTooltipElement) return;
|
||||
if (activeTooltipElement.contains?.(e.target)) return;
|
||||
hideTooltip();
|
||||
}, { capture: true, passive: true });
|
||||
document.addEventListener("touchstart", (e) => {
|
||||
if (!activeTooltipElement) return;
|
||||
if (activeTooltipElement.contains?.(e.target)) return;
|
||||
hideTooltip();
|
||||
}, { capture: true, passive: true });
|
||||
window.addEventListener("scroll", hideTooltip, { passive: true });
|
||||
window.addEventListener("resize", hideTooltip, { passive: true });
|
||||
document.addEventListener("visibilitychange", () => { if (document.hidden) hideTooltip(); });
|
||||
}
|
||||
|
||||
global.TarinaiTooltips = Object.freeze({
|
||||
ensure: ensureTooltipPopover,
|
||||
bind: bindTooltip,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.15.27";
|
||||
const APP_BUILD = "tarinai-grab-sleep-fence-perf-v39-15-19";
|
||||
const APP_VERSION = "39.15.39";
|
||||
const APP_BUILD = "tarinai-pc-rollback-mobile-family-fix-v39-15-39";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
worldRef.nextWeatherChange = this.resetTimer();
|
||||
if (previous !== fixed) {
|
||||
worldRef.emit?.("weather:changed", { previous, next: fixed });
|
||||
worldRef.log?.(`天気: ${weatherLabel(fixed)}`);
|
||||
worldRef.log?.(`\u5929\u6c17: ${weatherLabel(fixed)}`);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class World { constructor() {
|
|||
this.drawList = [];
|
||||
this.itemCounts = {};
|
||||
this.effectCounts = {};
|
||||
this.colonyMood = { id: "relaxed", label: "安定", description: "標準状態", effects: { personality: {} } };
|
||||
this.colonyMood = { id: "relaxed", label: "\u5b89\u5b9a", description: "\u6a19\u6e96\u72b6\u614b", effects: { personality: {} } };
|
||||
this.lastColonyMoodDay = 0;
|
||||
this.foodSpoilagePenalty = 0;
|
||||
this.foodSpoilageEvents = 0;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@
|
|||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
damageAntsInRadius(x, y, radius, damageFn, reason = "", opts = {}) {
|
||||
let hit = 0;
|
||||
for (const ant of this.ants || []) {
|
||||
const candidates = this.nearbyAnts?.(x, y, radius + 24, true) || this.ants || [];
|
||||
for (const ant of candidates) {
|
||||
if (!ant || ant.dead) continue;
|
||||
const d = Math.max(1, distXY(ant.x, ant.y, x, y));
|
||||
if (d > radius + (ant.r || 4)) continue;
|
||||
|
|
@ -262,7 +263,7 @@
|
|||
},
|
||||
|
||||
applyWaterHose(x, y, dx = 0, dy = 0, dt = 0.08) {
|
||||
const radius = 138;
|
||||
const radius = 276;
|
||||
const speed = Math.hypot(dx, dy) || 1;
|
||||
const nx = speed > 1 ? dx / speed : Math.cos(this.time * 9.1);
|
||||
const ny = speed > 1 ? dy / speed : Math.sin(this.time * 7.7);
|
||||
|
|
@ -283,7 +284,7 @@
|
|||
} else if (it.type === "grass_bed") {
|
||||
it.amount = 0;
|
||||
it.hp = 0;
|
||||
global.TarinaiStructureLifecycle?.deleteItem?.(this, it, { reason: "water-hose-grass-bed", userReason: "洗浄で流された", wake: true });
|
||||
global.TarinaiStructureLifecycle?.deleteItem?.(this, it, { reason: "water-hose-grass-bed", userReason: "\u6d17\u6d44\u3067\u6d41\u3055\u308c\u305f", wake: true });
|
||||
cleaned += p * 0.95;
|
||||
}
|
||||
}
|
||||
|
|
@ -452,7 +453,7 @@
|
|||
const impulseMag = Math.hypot(vx, vy);
|
||||
if (entity instanceof Tarinai && (entity.birthRitualTimer || 0) > 0.04) {
|
||||
this.cancelBirthRitualOnForce?.(entity, impulseMag, {
|
||||
reason: options.birthCancelReason || options.thought || "強い衝撃で繁殖が中断された",
|
||||
reason: options.birthCancelReason || options.thought || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
||||
target: options.target || null,
|
||||
fear: options.fearTimer ?? 1.0,
|
||||
cause: options.cause || "impulse",
|
||||
|
|
@ -484,13 +485,13 @@
|
|||
};
|
||||
let normalized = options.cause || cause || "\u885d\u7a81";
|
||||
const rawCause = String(cause || "");
|
||||
const isPlainCollision = String(normalized).includes("衝突") || String(normalized).includes("collision") || rawCause.includes("急激な速度変化");
|
||||
const isPlainCollision = String(normalized).includes("\u885d\u7a81") || String(normalized).includes("collision") || rawCause.includes("\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316");
|
||||
const label = sourceLabel(options.source);
|
||||
if (isPlainCollision && label) normalized = `${label}に衝突`;
|
||||
const physicalHit = String(normalized).includes("衝突") || String(normalized).includes("collision") || rawCause.includes("急激な速度変化") || String(normalized).includes("爆竹") || String(normalized).includes("落ちてきた");
|
||||
if (isPlainCollision && label) normalized = `${label}\u3068\u306e\u885d\u7a81`;
|
||||
const physicalHit = String(normalized).includes("\u885d\u7a81") || String(normalized).includes("collision") || rawCause.includes("\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316") || String(normalized).includes("\u7206\u7af9") || String(normalized).includes("\u843d\u3061\u3066\u304d\u305f");
|
||||
if (target instanceof Tarinai && (target.birthRitualTimer || 0) > 0.04 && physicalHit && amount >= (options.birthCancelDamageThreshold ?? 8)) {
|
||||
this.cancelBirthRitualOnForce?.(target, Math.max(180, amount * 18), {
|
||||
reason: "強い衝撃で繁殖が中断された",
|
||||
reason: "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
||||
target: options.source || null,
|
||||
fear: 1.0,
|
||||
cause: "impact_damage",
|
||||
|
|
@ -553,6 +554,9 @@
|
|||
loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1;
|
||||
winner.totalFightWins = (winner.totalFightWins || 0) + 1;
|
||||
loser.totalFightLosses = (loser.totalFightLosses || 0) + 1;
|
||||
this.maybeApplyTarinaiChampionByFightRecord?.(winner, loser);
|
||||
this.recordFamily?.(winner);
|
||||
this.recordFamily?.(loser);
|
||||
this.emit?.("fight:ended", { winner, loser });
|
||||
this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8);
|
||||
for (const t of [winner, loser]) {
|
||||
|
|
@ -572,8 +576,22 @@
|
|||
}
|
||||
},
|
||||
|
||||
maybeApplyTarinaiChampionByFightRecord(tarinai, other = null) {
|
||||
if (!tarinai || tarinai.dead || tarinai.isTarinaiChampion || tarinai.isZunchiSlave) return false;
|
||||
const wins = Number(tarinai.totalFightWins || 0);
|
||||
const losses = Number(tarinai.totalFightLosses || 0);
|
||||
const total = wins + losses;
|
||||
if (total < 6) return false;
|
||||
const winRate = total > 0 ? wins / total : 0;
|
||||
if (winRate < 0.75) return false;
|
||||
if (!tarinai.becomeTarinaiChampion?.({ fightRecord: true })) return false;
|
||||
this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.35, "王者", "rgba(144,96,24,0.84)");
|
||||
this.log?.(`${tarinai.name}は喧嘩${total}回、勝率${Math.round(winRate * 100)}%でたりない王者になった。`, "fight", { participants: other ? [tarinai, other] : [tarinai] });
|
||||
return true;
|
||||
},
|
||||
|
||||
maybeApplyZunchiSlaveByFightRecord(tarinai, other = null) {
|
||||
if (!tarinai || tarinai.dead || tarinai.isZunchiSlave) return false;
|
||||
if (!tarinai || tarinai.dead || tarinai.isTarinaiChampion || tarinai.isZunchiSlave) return false;
|
||||
const wins = Number(tarinai.totalFightWins || 0);
|
||||
const losses = Number(tarinai.totalFightLosses || 0);
|
||||
const total = wins + losses;
|
||||
|
|
@ -597,8 +615,8 @@
|
|||
const radius = Math.max(32, Math.min(220, (it.r || 14) * 1.35 + impactSpeed * 0.055));
|
||||
const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 });
|
||||
const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260);
|
||||
const reason = isStone ? "石に潰された" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f");
|
||||
if (isGenkotsu) { it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
|
||||
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u306b\u6f70\u3055\u308c\u305f" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"));
|
||||
if (isGenkotsu) { this.lastGenkotsuAt = this.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
|
||||
let hit = 0;
|
||||
for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
|
|
@ -635,9 +653,12 @@
|
|||
push: p => push * (0.62 + p * 1.25),
|
||||
}) : 0;
|
||||
if (isGenkotsu) {
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75);
|
||||
for (let r = 0; r < 4; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 34, life: 0.34 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.72)" : "rgba(74,57,42,0.42)" }));
|
||||
for (let i = 0; i < 12; i++) this.effects.push(new Effect("fight", it.x + deterministicRange(this, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(this, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(this, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(this, "genkotsu-impact-effect-life", 0.25, 0.48, it, i), color: "rgba(255,205,46,0.82)" }));
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
|
||||
const existingEffects = (this.effects || []).length;
|
||||
const ringCount = existingEffects > 180 ? 2 : 3;
|
||||
const burstCount = existingEffects > 180 ? 4 : 7;
|
||||
for (let r = 0; r < ringCount; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
|
||||
for (let i = 0; i < burstCount; i++) this.effects.push(new Effect("fight", it.x + deterministicRange(this, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(this, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(this, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(this, "genkotsu-impact-effect-life", 0.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
|
||||
} else {
|
||||
this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" }));
|
||||
}
|
||||
|
|
@ -1005,16 +1026,16 @@
|
|||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "高速でぶつかって混乱している",
|
||||
thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b",
|
||||
});
|
||||
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
||||
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
||||
this.applyImpactDamage(target, damage, "衝突", { source: impactor });
|
||||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "衝突", { source: target });
|
||||
this.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor });
|
||||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target });
|
||||
for (const t of [target, impactor]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", wake: true });
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
|
|
@ -1022,7 +1043,7 @@
|
|||
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
|
||||
}
|
||||
this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" }));
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}は高速の${impactor.name}に衝突して弾き飛ばされた。`, "accident", { participants: [target, impactor] });
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] });
|
||||
}
|
||||
if (solved && this.spatialDirty) this.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,8 +407,9 @@
|
|||
const base = this.nestBoxBaseRect(box);
|
||||
if (!base) return { x: box?.x || 0, y: box?.y || 0 };
|
||||
const occupants = this.nestBoxOccupants ? this.nestBoxOccupants(box, Infinity) : [];
|
||||
let index = occupants.indexOf(occupant);
|
||||
let index = Number.isInteger(occupant?.nestBoxSlotIndex) && occupant.nestBoxSlotIndex >= 0 ? occupant.nestBoxSlotIndex : occupants.indexOf(occupant);
|
||||
if (index < 0) index = Math.min(occupants.length, this.nestBoxCapacity(box) - 1);
|
||||
index = clamp(index, 0, Math.max(0, this.nestBoxCapacity(box) - 1));
|
||||
const r = box.r || 42;
|
||||
const slots = [
|
||||
[0.00, -0.02], [-0.22, 0.04], [0.22, 0.04], [-0.11, 0.16], [0.11, 0.16],
|
||||
|
|
@ -478,7 +479,7 @@
|
|||
const r = box.r || 42;
|
||||
const entryD = distXY(t.x, t.y, entry.x, entry.y);
|
||||
const centerD = distXY(t.x, t.y, box.x, box.y);
|
||||
const aroundDoor = entryD <= Math.max(128, r * 2.25) || centerD <= Math.max(116, r * 2.0);
|
||||
const aroundDoor = entryD <= Math.max(96, r * 1.75) || centerD <= Math.max(86, r * 1.48);
|
||||
const inDoorColumn = base && t.x >= base.left + r * 0.18 && t.x <= base.right - r * 0.18 && t.y >= base.top + r * 0.02 && t.y <= base.bottom + r * 0.58;
|
||||
return Boolean(aroundDoor || inDoorColumn);
|
||||
},
|
||||
|
|
@ -635,9 +636,9 @@
|
|||
const speed = Math.hypot(vx, vy) + Math.abs(omega) * Math.max(28, rect.item.r || 64) * 0.35;
|
||||
const base = Math.max(1, Number((body?.hazard?.damage ?? global.TarinaiPhysicsBodySystem?.scalar?.(rect.item, "damage", 7)) || 7) || 7);
|
||||
const damage = clamp(base * (solid ? 1.0 : 0.72) + speed * 0.018, 2.0, 18.0);
|
||||
if (typeof this.applyImpactDamage === "function") this.applyImpactDamage(t, damage, "毒ブロック", { source: rect.item, x: t.x, y: t.y });
|
||||
else if (typeof t.damage === "function") t.damage(damage, "毒ブロック");
|
||||
// HPバーは描画側で「ダメージ中」の表示条件を見るため、毒ブロックでも明示的に傷み状態を立てる。
|
||||
if (typeof this.applyImpactDamage === "function") this.applyImpactDamage(t, damage, "\u6bd2\u30d6\u30ed\u30c3\u30af", { source: rect.item, x: t.x, y: t.y });
|
||||
else if (typeof t.damage === "function") t.damage(damage, "\u6bd2\u30d6\u30ed\u30c3\u30af");
|
||||
// HP\u30d0\u30fc\u306f\u63cf\u753b\u5074\u3067\u300c\u30c0\u30e1\u30fc\u30b8\u4e2d\u300d\u306e\u8868\u793a\u6761\u4ef6\u3092\u898b\u308b\u305f\u3081\u3001\u6bd2\u30d6\u30ed\u30c3\u30af\u3067\u3082\u660e\u793a\u7684\u306b\u50b7\u307f\u72b6\u614b\u3092\u7acb\u3066\u308b\u3002
|
||||
t.showHpBar?.(4.8);
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.82);
|
||||
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.28);
|
||||
|
|
|
|||
|
|
@ -17,24 +17,24 @@
|
|||
const text = String(entry.text || "");
|
||||
if (kind === "death") return audio.death?.();
|
||||
if (kind === "birth") return audio.birth?.();
|
||||
if (kind === "fight") return /決着|勝|負|覚え/.test(text) ? audio.fightFinish?.() : audio.fight?.();
|
||||
if (kind === "fight") return /\u6c7a\u7740|\u52dd|\u8ca0|\u899a\u3048/.test(text) ? audio.fightFinish?.() : audio.fight?.();
|
||||
if (kind === "food") return audio.eat?.();
|
||||
if (kind === "grass") return audio.play?.("sfx_grass", { category: "ops", minGap: 0.25 });
|
||||
if (kind === "weather") return audio.phase?.();
|
||||
if (kind === "relation") return audio.notify?.();
|
||||
if (kind === "accident") {
|
||||
if (/配置|置いた|設置/.test(text)) return audio.place?.(/爆竹/.test(text) ? "firecracker" : "");
|
||||
if (/げんこつ/.test(text)) return audio.genkotsuImpact?.();
|
||||
if (/爆竹|爆発/.test(text)) return audio.explode?.();
|
||||
if (/石/.test(text)) return audio.stoneImpact?.();
|
||||
if (/ボール/.test(text)) return audio.ballHit?.();
|
||||
if (/\u914d\u7f6e|\u7f6e\u3044\u305f|\u8a2d\u7f6e/.test(text)) return audio.place?.(/\u7206\u7af9/.test(text) ? "firecracker" : "");
|
||||
if (/\u3052\u3093\u3053\u3064/.test(text)) return audio.genkotsuImpact?.();
|
||||
if (/\u7206\u7af9|\u7206\u767a/.test(text)) return audio.explode?.();
|
||||
if (/\u77f3/.test(text)) return audio.stoneImpact?.();
|
||||
if (/\u30dc\u30fc\u30eb/.test(text)) return audio.ballHit?.();
|
||||
return audio.damage?.(18);
|
||||
}
|
||||
if (kind === "event") {
|
||||
if (/女王アリ/.test(text)) return audio.queenAnt?.();
|
||||
if (/アリ|巣/.test(text)) return audio.antNest?.();
|
||||
if (/病|発病/.test(text)) return audio.disease?.();
|
||||
if (/回復|治/.test(text)) return audio.heal?.();
|
||||
if (/\u5973\u738b\u30a2\u30ea/.test(text)) return audio.queenAnt?.();
|
||||
if (/\u30a2\u30ea|\u5de3/.test(text)) return audio.antNest?.();
|
||||
if (/\u75c5|\u767a\u75c5/.test(text)) return audio.disease?.();
|
||||
if (/\u56de\u5fa9|\u6cbb/.test(text)) return audio.heal?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,10 @@
|
|||
const nextScale = Number.isFinite(t.scale) ? t.scale : (Number.isFinite(t.adultScale) ? t.adultScale : 0.28);
|
||||
const nextAdultScale = Number.isFinite(t.adultScale) ? t.adultScale : nextScale;
|
||||
const nextGrowth = Number.isFinite(t.growth) ? t.growth : 1;
|
||||
const nextGenetics = typeof normalizeGenetics === "function" ? normalizeGenetics(t.genetics, t.familyKey || t.id || "") : { ...(t.genetics || {}) };
|
||||
const nextFightWins = Number(t.totalFightWins || 0) || 0;
|
||||
const nextFightLosses = Number(t.totalFightLosses || 0) || 0;
|
||||
const nextChampion = !!t.isTarinaiChampion;
|
||||
if (!this.family[familyKey]) {
|
||||
this.family[familyKey] = {
|
||||
id: familyKey,
|
||||
|
|
@ -191,6 +195,11 @@
|
|||
scale: nextScale,
|
||||
adultScale: nextAdultScale,
|
||||
growth: nextGrowth,
|
||||
genetics: nextGenetics,
|
||||
lifeSpan: Number(t.lifeSpan) || 0,
|
||||
totalFightWins: nextFightWins,
|
||||
totalFightLosses: nextFightLosses,
|
||||
isTarinaiChampion: nextChampion,
|
||||
alive: !t.dead,
|
||||
deathReason: t.deathReason || "",
|
||||
};
|
||||
|
|
@ -206,6 +215,11 @@
|
|||
if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true;
|
||||
if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true;
|
||||
if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true;
|
||||
for (const key of ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]) if (Math.abs((Number(entry.genetics?.[key]) || 1) - (Number(nextGenetics?.[key]) || 1)) > 0.002) changed = true;
|
||||
if (Math.abs((Number(entry.lifeSpan) || 0) - (Number(t.lifeSpan) || 0)) > 1) changed = true;
|
||||
if ((Number(entry.totalFightWins) || 0) !== nextFightWins) changed = true;
|
||||
if ((Number(entry.totalFightLosses) || 0) !== nextFightLosses) changed = true;
|
||||
if (!!entry.isTarinaiChampion !== nextChampion) changed = true;
|
||||
if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true;
|
||||
if (entry.alive !== nextAlive) changed = true;
|
||||
if (!sameList(entry.parents, nextParents)) changed = true;
|
||||
|
|
@ -219,6 +233,11 @@
|
|||
entry.scale = nextScale;
|
||||
entry.adultScale = nextAdultScale;
|
||||
entry.growth = nextGrowth;
|
||||
entry.genetics = nextGenetics;
|
||||
entry.lifeSpan = Number(t.lifeSpan) || entry.lifeSpan || 0;
|
||||
entry.totalFightWins = nextFightWins;
|
||||
entry.totalFightLosses = nextFightLosses;
|
||||
entry.isTarinaiChampion = nextChampion;
|
||||
entry.hasPaired = !!t.hasPaired;
|
||||
entry.deathReason = t.deathReason || entry.deathReason || "";
|
||||
entry.parents = nextParents;
|
||||
|
|
@ -278,13 +297,23 @@
|
|||
scale: Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : (Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : 0.28))),
|
||||
adultScale: Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : (Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : 0.28))),
|
||||
growth: Number.isFinite(live?.growth) ? live.growth : (Number.isFinite(n.growth) ? n.growth : 1),
|
||||
genetics: live && typeof normalizeGenetics === "function" ? normalizeGenetics(live.genetics, live.familyKey || live.id || "") : (typeof normalizeGenetics === "function" ? normalizeGenetics(n.genetics, id) : { ...(n.genetics || {}) }),
|
||||
lifeSpan: Number(live?.lifeSpan || n.lifeSpan || 0) || 0,
|
||||
totalFightWins: Number(live?.totalFightWins ?? n.totalFightWins ?? 0) || 0,
|
||||
totalFightLosses: Number(live?.totalFightLosses ?? n.totalFightLosses ?? 0) || 0,
|
||||
isTarinaiChampion: !!(live?.isTarinaiChampion || n.isTarinaiChampion),
|
||||
alive: live ? !live.dead : n.alive !== false,
|
||||
deathReason: live?.deathReason || n.deathReason || "",
|
||||
};
|
||||
for (const key of Object.keys(next)) {
|
||||
const a = family[id]?.[key];
|
||||
const b = next[key];
|
||||
const equal = Array.isArray(a) && Array.isArray(b) ? sameList(a, b) : a === b;
|
||||
let equal = false;
|
||||
if (key === "genetics") {
|
||||
equal = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"].every(gk => Math.abs((Number(a?.[gk]) || 1) - (Number(b?.[gk]) || 1)) <= 0.002);
|
||||
} else {
|
||||
equal = Array.isArray(a) && Array.isArray(b) ? sameList(a, b) : a === b;
|
||||
}
|
||||
if (!equal) changed = true;
|
||||
}
|
||||
family[id] = next;
|
||||
|
|
@ -441,6 +470,17 @@
|
|||
|
||||
addTarinai(opts = {}) {
|
||||
opts = global.TarinaiSeedFactory?.prepareAddOptions?.(this, opts) || opts;
|
||||
const seedParents = Array.isArray(opts.seedParents) ? opts.seedParents.filter(p => p && typeof p === "object") : [];
|
||||
if (!opts.genetics && seedParents.length >= 2 && typeof inheritedGenetics === "function") {
|
||||
opts.genetics = inheritedGenetics(seedParents[0], seedParents[1], opts.birthSeed || opts.familyKey || "");
|
||||
}
|
||||
if (opts.genetics) {
|
||||
if (!Number.isFinite(opts.adultScale) && typeof adultScaleFromGenetics === "function") opts.adultScale = adultScaleFromGenetics(opts.birthSeed || opts.familyKey || "", opts.genetics);
|
||||
if (!Number.isFinite(opts.lifeSpan)) {
|
||||
const profile = global.TarinaiSeedFactory?.birthProfile?.(opts.birthSeed || opts.familyKey || "", opts) || {};
|
||||
if (Number.isFinite(profile.lifeSpan)) opts.lifeSpan = profile.lifeSpan;
|
||||
}
|
||||
}
|
||||
const t = new Tarinai(this, opts);
|
||||
this.assignTarinaiLiveId(t);
|
||||
this.tarinai.push(t);
|
||||
|
|
@ -476,8 +516,8 @@
|
|||
if (!box || box.dead || box.type !== "nest_box") return [];
|
||||
const occupants = this.nestBoxOccupants(box, Infinity);
|
||||
const names = occupants.map(t => t?.name).filter(Boolean).slice(0, 5);
|
||||
const more = occupants.length > names.length ? `、ほか${occupants.length - names.length}匹` : "";
|
||||
return ["巣箱", names.length ? `入ってる子: ${names.join("、")}${more}` : "入ってる子: なし"];
|
||||
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"];
|
||||
},
|
||||
|
||||
pointerOwnedBedInfo() {
|
||||
|
|
@ -497,7 +537,7 @@
|
|||
ownedBedTooltipLines(bed) {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
|
||||
const owner = this.liveTarinaiById?.(bed.ownerId) || null;
|
||||
return ["かんたんベッド", `持ち主: ${owner?.name || "不明"}`];
|
||||
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`];
|
||||
},
|
||||
|
||||
pointerItemTooltipInfo() {
|
||||
|
|
@ -532,7 +572,7 @@
|
|||
b.birthRitualRole = 1;
|
||||
a.birthRitualLeader = true;
|
||||
b.birthRitualLeader = false;
|
||||
const ritualReason = "へこへこしている";
|
||||
const ritualReason = "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b";
|
||||
const applyRitualAction = (actor, partner) => {
|
||||
if (!actor) return false;
|
||||
if (typeof actor.setActionState === "function") {
|
||||
|
|
@ -557,10 +597,10 @@
|
|||
const bBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(b) : b.behavior;
|
||||
const aLoveCause = (aBehavior?.source === "love_mochi" || (a.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${b.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
||||
const bLoveCause = (bBehavior?.source === "love_mochi" || (b.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${a.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
|
||||
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068へこへこしている` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068へこへこしている`;
|
||||
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068へこへこしている` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068へこへこしている`;
|
||||
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068へこへこしている`, reasonText: aText, causeText: aLoveCause, target: b });
|
||||
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068へこへこしている`, reasonText: bText, causeText: bLoveCause, target: a });
|
||||
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`;
|
||||
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`;
|
||||
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
|
||||
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
|
||||
}
|
||||
a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
||||
b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
||||
|
|
@ -577,7 +617,7 @@
|
|||
if (source) participants.push(source);
|
||||
if (partner && partner !== source) participants.push(partner);
|
||||
if (!participants.length) return false;
|
||||
const reason = options.reason || "繁殖が中断された";
|
||||
const reason = options.reason || "\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f";
|
||||
let canceled = false;
|
||||
for (const t of participants) {
|
||||
if (!t || t.dead) continue;
|
||||
|
|
@ -615,7 +655,7 @@
|
|||
const threshold = Number(options.threshold || 180) || 180;
|
||||
if (amount < threshold) return false;
|
||||
return this.cancelBirthRitual(tarinai, {
|
||||
reason: options.reason || "強い衝撃で繁殖が中断された",
|
||||
reason: options.reason || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
||||
target: options.target || null,
|
||||
fear: options.fear ?? 1.0,
|
||||
cause: options.cause || "external_force",
|
||||
|
|
|
|||
|
|
@ -157,30 +157,30 @@
|
|||
dialog.innerHTML = `
|
||||
<div class="rotator-editor-panel" role="dialog" aria-modal="true" aria-labelledby="rotatorEditorTitle">
|
||||
<div class="rotator-editor-titlebar">
|
||||
<h2 id="rotatorEditorTitle">回転体</h2>
|
||||
<button id="rotatorEditorClose" class="rotator-editor-close" type="button" aria-label="閉じる">×</button>
|
||||
<h2 id="rotatorEditorTitle">\u56de\u8ee2\u4f53</h2>
|
||||
<button id="rotatorEditorClose" class="rotator-editor-close" type="button" aria-label="\u9589\u3058\u308b">\u00d7</button>
|
||||
</div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>速度 <input id="mechMotorSpeed" type="range" min="-360" max="360" step="5"><input id="mechMotorSpeedNum" type="number" min="-720" max="720" step="5"> 度/秒</label>
|
||||
<label>太さ <input id="mechShapeThickness" type="range" min="4" max="30" step="1"><input id="mechShapeThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="mechMotorPowered" type="checkbox"> 動力ON</label>
|
||||
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> 当たり判定ON</label>
|
||||
<label>\u901f\u5ea6 <input id="mechMotorSpeed" type="range" min="-360" max="360" step="5"><input id="mechMotorSpeedNum" type="number" min="-720" max="720" step="5"> \u5ea6/\u79d2</label>
|
||||
<label>\u592a\u3055 <input id="mechShapeThickness" type="range" min="4" max="30" step="1"><input id="mechShapeThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="mechMotorPowered" type="checkbox"> \u52d5\u529bON</label>
|
||||
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="描画方法">
|
||||
<button type="button" data-rotator-mode="line" class="active">直線</button>
|
||||
<button type="button" data-rotator-mode="free">自由描画</button>
|
||||
<button type="button" data-rotator-mode="erase">消しゴム</button>
|
||||
<button type="button" data-rotator-template="bar">棒</button>
|
||||
<button type="button" data-rotator-template="cross">十字</button>
|
||||
<button type="button" data-rotator-template="circle">円っぽい</button>
|
||||
<button id="rotatorUndo" type="button">一つ戻す</button>
|
||||
<button id="rotatorClear" type="button">全消し</button>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
||||
<button type="button" data-rotator-mode="line" class="active">\u76f4\u7dda</button>
|
||||
<button type="button" data-rotator-mode="free">\u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-rotator-mode="erase">\u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-rotator-template="bar">\u68d2</button>
|
||||
<button type="button" data-rotator-template="cross">\u5341\u5b57</button>
|
||||
<button type="button" data-rotator-template="circle">\u5186\u3063\u307d\u3044</button>
|
||||
<button id="rotatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
||||
<button id="rotatorClear" type="button">\u5168\u6d88\u3057</button>
|
||||
</div>
|
||||
<canvas id="rotatorCanvas" width="520" height="360" aria-label="回転体の輪郭編集"></canvas>
|
||||
<p class="rotator-editor-hint">中央が回転軸。直線・自由描画・消しゴムを選び、ドラッグで輪郭を編集。</p>
|
||||
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||
<p class="rotator-editor-hint">\u4e2d\u592e\u304c\u56de\u8ee2\u8ef8\u3002\u76f4\u7dda\u30fb\u81ea\u7531\u63cf\u753b\u30fb\u6d88\u3057\u30b4\u30e0\u3092\u9078\u3073\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u8f2a\u90ed\u3092\u7de8\u96c6\u3002</p>
|
||||
<div class="rotator-editor-actions">
|
||||
<button id="rotatorCancel" class="btn" type="button">キャンセル</button>
|
||||
<button id="rotatorApply" class="btn primary" type="button">反映</button>
|
||||
<button id="rotatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||
<button id="rotatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(dialog);
|
||||
|
|
@ -227,7 +227,7 @@
|
|||
const cy = canvas.height / 2;
|
||||
const deg = Math.round((Number(phyScalar(item, "motorSpeed", 0) || 0) || 0) * 180 / Math.PI);
|
||||
const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12)));
|
||||
if (titleEl) titleEl.textContent = isPoisonBlock ? "毒ブロック" : "回転体";
|
||||
if (titleEl) titleEl.textContent = isPoisonBlock ? "\u6bd2\u30d6\u30ed\u30c3\u30af" : "\u56de\u8ee2\u4f53";
|
||||
const speedLabel = speed?.closest?.("label");
|
||||
const poweredLabel = poweredInput?.closest?.("label");
|
||||
if (speedLabel) speedLabel.style.display = isPoisonBlock ? "none" : "";
|
||||
|
|
@ -420,7 +420,7 @@
|
|||
global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, isPoisonBlock ? "poison-block-link-remap" : "rotator-link-remap");
|
||||
worldRef?.markSpatialDirty?.(isPoisonBlock ? "poison-block-edited" : "rotator-edited");
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef?.log?.(isPoisonBlock ? "毒ブロックの設計を変更した。" : "回転体の設計を変更した。", "observe");
|
||||
worldRef?.log?.(isPoisonBlock ? "\u6bd2\u30d6\u30ed\u30c3\u30af\u306e\u8a2d\u8a08\u3092\u5909\u66f4\u3057\u305f\u3002" : "\u56de\u8ee2\u4f53\u306e\u8a2d\u8a08\u3092\u5909\u66f4\u3057\u305f\u3002", "observe");
|
||||
global.render?.();
|
||||
close();
|
||||
};
|
||||
|
|
@ -443,32 +443,32 @@
|
|||
dialog.innerHTML = `
|
||||
<div class="rotator-editor-panel" role="dialog" aria-modal="true" aria-labelledby="reciprocatorEditorTitle">
|
||||
<div class="rotator-editor-titlebar">
|
||||
<h2 id="reciprocatorEditorTitle">往復体</h2>
|
||||
<button id="reciprocatorEditorClose" class="rotator-editor-close" type="button" aria-label="閉じる">×</button>
|
||||
<h2 id="reciprocatorEditorTitle">\u5f80\u5fa9\u4f53</h2>
|
||||
<button id="reciprocatorEditorClose" class="rotator-editor-close" type="button" aria-label="\u9589\u3058\u308b">\u00d7</button>
|
||||
</div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>方向 <input id="reciprocatorAngle" type="range" min="0" max="355" step="5"><input id="reciprocatorAngleNum" type="number" min="0" max="355" step="5"> 度</label>
|
||||
<label>速度 <input id="railMotorSpeedControl" type="range" min="0" max="240" step="5"><input id="railMotorSpeedControlNum" type="number" min="0" max="360" step="5"> px/秒</label>
|
||||
<label>往復幅 <input id="railTravelControl" type="range" min="48" max="360" step="4"><input id="railTravelControlNum" type="number" min="24" max="520" step="4"> px</label>
|
||||
<label>太さ <input id="reciprocatorThickness" type="range" min="4" max="30" step="1"><input id="reciprocatorThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="railMotorPowered" type="checkbox"> 動力ON</label>
|
||||
<label><input id="reciprocatorSolidEnabled" type="checkbox"> 当たり判定ON</label>
|
||||
<label>\u65b9\u5411 <input id="reciprocatorAngle" type="range" min="0" max="355" step="5"><input id="reciprocatorAngleNum" type="number" min="0" max="355" step="5"> \u5ea6</label>
|
||||
<label>\u901f\u5ea6 <input id="railMotorSpeedControl" type="range" min="0" max="240" step="5"><input id="railMotorSpeedControlNum" type="number" min="0" max="360" step="5"> px/\u79d2</label>
|
||||
<label>\u5f80\u5fa9\u5e45 <input id="railTravelControl" type="range" min="48" max="360" step="4"><input id="railTravelControlNum" type="number" min="24" max="520" step="4"> px</label>
|
||||
<label>\u592a\u3055 <input id="reciprocatorThickness" type="range" min="4" max="30" step="1"><input id="reciprocatorThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="railMotorPowered" type="checkbox"> \u52d5\u529bON</label>
|
||||
<label><input id="reciprocatorSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="描画方法">
|
||||
<button type="button" data-reciprocator-mode="line" class="active">直線</button>
|
||||
<button type="button" data-reciprocator-mode="free">自由描画</button>
|
||||
<button type="button" data-reciprocator-mode="erase">消しゴム</button>
|
||||
<button type="button" data-reciprocator-template="bar">棒</button>
|
||||
<button type="button" data-reciprocator-template="cross">十字</button>
|
||||
<button type="button" data-reciprocator-template="circle">円っぽい</button>
|
||||
<button id="reciprocatorUndo" type="button">一つ戻す</button>
|
||||
<button id="reciprocatorClear" type="button">全消し</button>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
||||
<button type="button" data-reciprocator-mode="line" class="active">\u76f4\u7dda</button>
|
||||
<button type="button" data-reciprocator-mode="free">\u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-reciprocator-mode="erase">\u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-reciprocator-template="bar">\u68d2</button>
|
||||
<button type="button" data-reciprocator-template="cross">\u5341\u5b57</button>
|
||||
<button type="button" data-reciprocator-template="circle">\u5186\u3063\u307d\u3044</button>
|
||||
<button id="reciprocatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
||||
<button id="reciprocatorClear" type="button">\u5168\u6d88\u3057</button>
|
||||
</div>
|
||||
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="往復体の輪郭編集"></canvas>
|
||||
<p class="rotator-editor-hint">中央の輪郭全体が、設定した方向へ往復移動します。動力OFFでは、たりない・ずんち・他の機構から押されたときだけ動きます。</p>
|
||||
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||
<p class="rotator-editor-hint">\u4e2d\u592e\u306e\u8f2a\u90ed\u5168\u4f53\u304c\u3001\u8a2d\u5b9a\u3057\u305f\u65b9\u5411\u3078\u5f80\u5fa9\u79fb\u52d5\u3057\u307e\u3059\u3002\u52d5\u529bOFF\u3067\u306f\u3001\u305f\u308a\u306a\u3044\u30fb\u305a\u3093\u3061\u30fb\u4ed6\u306e\u6a5f\u69cb\u304b\u3089\u62bc\u3055\u308c\u305f\u3068\u304d\u3060\u3051\u52d5\u304d\u307e\u3059\u3002</p>
|
||||
<div class="rotator-editor-actions">
|
||||
<button id="reciprocatorCancel" class="btn" type="button">キャンセル</button>
|
||||
<button id="reciprocatorApply" class="btn primary" type="button">反映</button>
|
||||
<button id="reciprocatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||
<button id="reciprocatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(dialog);
|
||||
|
|
@ -708,7 +708,7 @@
|
|||
global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, "reciprocator-link-remap");
|
||||
worldRef?.markSpatialDirty?.("reciprocator-edited");
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef?.log?.("往復体の設計を変更した。", "observe");
|
||||
worldRef?.log?.("\u5f80\u5fa9\u4f53\u306e\u8a2d\u8a08\u3092\u5909\u66f4\u3057\u305f\u3002", "observe");
|
||||
global.render?.();
|
||||
close();
|
||||
};
|
||||
|
|
@ -930,8 +930,8 @@
|
|||
item.gateLastToggleAt = worldRef?.time || 0;
|
||||
worldRef?.markSpatialDirty?.("gate-toggle");
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef?.log?.(item.gateOpen ? "ゲート柵を開けた。" : "ゲート柵を閉めた。", "observe");
|
||||
if (typeof showToast === "function") showToast(item.gateOpen ? "ゲート柵: 開" : "ゲート柵: 閉");
|
||||
worldRef?.log?.(item.gateOpen ? "\u30b2\u30fc\u30c8\u67f5\u3092\u958b\u3051\u305f\u3002" : "\u30b2\u30fc\u30c8\u67f5\u3092\u9589\u3081\u305f\u3002", "observe");
|
||||
if (typeof showToast === "function") showToast(item.gateOpen ? "\u30b2\u30fc\u30c8\u67f5: \u958b" : "\u30b2\u30fc\u30c8\u67f5: \u9589");
|
||||
global.render?.();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -974,7 +974,7 @@
|
|||
const ok = runtime?.setStoredType?.(duplicator, loadType, worldRef, { direct: true, force: false });
|
||||
if (!ok) return false;
|
||||
duplicator.duplicatorLoadSuppressedUntil = Math.max(duplicator.duplicatorLoadSuppressedUntil || 0, (worldRef?.time || 0) + 0.45);
|
||||
if (typeof showToast === "function") showToast(`複製機: ${duplicator.storedFoodLabel || toolLabel(loadType)}をセット`);
|
||||
if (typeof showToast === "function") showToast(`\u8907\u88fd\u6a5f: ${duplicator.storedFoodLabel || toolLabel(loadType)}\u3092\u30bb\u30c3\u30c8`);
|
||||
worldRef.drawListDirty = true;
|
||||
global.render?.();
|
||||
return true;
|
||||
|
|
@ -1034,7 +1034,7 @@
|
|||
if (!physicsItemOverlapsSoftItem(worldRef, item, soft)) continue;
|
||||
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, soft, {
|
||||
reason,
|
||||
userReason: "物理アイテムに重なった",
|
||||
userReason: "\u7269\u7406\u30a2\u30a4\u30c6\u30e0\u306b\u91cd\u306a\u3063\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (ok) removed += 1;
|
||||
|
|
@ -1065,7 +1065,7 @@
|
|||
if (distXY(item.x, item.y, bed.x, bed.y) > hit) continue;
|
||||
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, bed, {
|
||||
reason: "grass-bed-ignored-by-placement",
|
||||
userReason: "置きものが置かれた",
|
||||
userReason: "\u7f6e\u304d\u3082\u306e\u304c\u7f6e\u304b\u308c\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (ok) removed += 1;
|
||||
|
|
@ -1308,7 +1308,7 @@
|
|||
placeItem(item, dropped = true) {
|
||||
if (!item) return null;
|
||||
if (item.type === "grass" && !this.canAddGrass?.(1)) {
|
||||
showToast(`草はこのフィールドでは${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}本までです。`);
|
||||
showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
||||
return null;
|
||||
}
|
||||
if (this.placementBlocked(item)) {
|
||||
|
|
@ -1327,7 +1327,7 @@
|
|||
clearOverlappingGrassBedsForPlacement(this, item);
|
||||
const placed = this.addItem?.(item, `place:${item.type}`);
|
||||
if (!placed) {
|
||||
if (item.type === "grass") showToast(`草はこのフィールドでは${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}本までです。`);
|
||||
if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
||||
return null;
|
||||
}
|
||||
clearSoftItemsUnderPhysicsItem(this, item, `place:${item.type}:soft-overlap-clear`);
|
||||
|
|
@ -1377,12 +1377,12 @@
|
|||
}
|
||||
if (mechanismTarget) {
|
||||
if (global.TarinaiMechanicalSystem?.applyPokeImpulse?.(mechanismTarget, x, y, this)) {
|
||||
const label = mechanismTarget.type === "rotator" ? "回転体" : (mechanismTarget.type === "poison_block" ? "毒ブロック" : "往復体");
|
||||
const label = mechanismTarget.type === "rotator" ? "\u56de\u8ee2\u4f53" : (mechanismTarget.type === "poison_block" ? "\u6bd2\u30d6\u30ed\u30c3\u30af" : "\u5f80\u5fa9\u4f53");
|
||||
const color = mechanismTarget.type === "rotator" ? "rgba(184,132,230,0.40)" : (mechanismTarget.type === "poison_block" ? "rgba(92,188,68,0.40)" : "rgba(90,150,214,0.40)");
|
||||
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color }));
|
||||
this.drawListDirty = true;
|
||||
audio.poke?.();
|
||||
this.log(`${label}をつついて動かした。`, "observe", { eventType: "mechanism_poke", hiddenFromObservation: true });
|
||||
this.log(`${label}\u3092\u3064\u3064\u3044\u3066\u52d5\u304b\u3057\u305f\u3002`, "observe", { eventType: "mechanism_poke", hiddenFromObservation: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@
|
|||
findDeleteToolTargetAt(x, y) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
||||
const it = this.items[i];
|
||||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead) continue;
|
||||
let d = distXY(x, y, it.x, it.y);
|
||||
if (it.type === "rope" || it.type === "rod") {
|
||||
|
|
@ -48,12 +49,12 @@
|
|||
|
||||
deleteItemAt(x, y) {
|
||||
const target = this.findDeleteToolTargetAt?.(x, y);
|
||||
if (!target) { showToast("消せるものがありません。"); return true; }
|
||||
if (isPinType(target.type) && target.pinState === "lodged") { showToast("刺さった鋲は、つまんで抜いてください。"); return true; }
|
||||
if (!target) { showToast("\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return true; }
|
||||
if (isPinType(target.type) && target.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u92f2\u306f\u3001\u3064\u307e\u3093\u3067\u629c\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return true; }
|
||||
const label = toolLabel(target.type);
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
||||
reason: "delete-tool",
|
||||
userReason: target.type === "grass_bed" ? "ベッドがなくなった" : "置きものがなくなった",
|
||||
userReason: target.type === "grass_bed" ? "\u30d9\u30c3\u30c9\u304c\u306a\u304f\u306a\u3063\u305f" : "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (!removed) return true;
|
||||
|
|
@ -61,7 +62,7 @@
|
|||
this.compactItems?.();
|
||||
this.updateItemCounts?.();
|
||||
this.rebuildSpatial?.();
|
||||
this.log?.(`${label}を消した。`, "observe");
|
||||
this.log?.(`${label}\u3092\u6d88\u3057\u305f\u3002`, "observe");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -72,8 +73,9 @@
|
|||
deleteItemsInArea(x, y, options = {}) {
|
||||
const radius = this.areaDeleteRadius?.() || 138;
|
||||
const candidates = [];
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
||||
const it = this.items[i];
|
||||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead) continue;
|
||||
if (isPinType(it.type) && it.pinState === "lodged") continue;
|
||||
const hitR = Math.max(8, it.r || itemRadiusFor?.(it.type, 12) || 12);
|
||||
|
|
@ -90,14 +92,14 @@
|
|||
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { padding: radius, radiusMultiplier: 0.85 }) || { hit: d <= radius + hitR * 0.85 };
|
||||
if (result.hit || d <= radius + hitR * 0.85) candidates.push(it);
|
||||
}
|
||||
if (!candidates.length) { if (!options.silentEmpty) showToast("範囲内に消せるものがありません。"); return true; }
|
||||
if (!candidates.length) { if (!options.silentEmpty) showToast("\u7bc4\u56f2\u5185\u306b\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return true; }
|
||||
let removedCount = 0;
|
||||
const kindCounts = new Map();
|
||||
for (const target of candidates) {
|
||||
if (!target || target.dead) continue;
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
||||
reason: "area-delete-tool",
|
||||
userReason: target.type === "grass_bed" ? "ベッドがなくなった" : "置きものがなくなった",
|
||||
userReason: target.type === "grass_bed" ? "\u30d9\u30c3\u30c9\u304c\u306a\u304f\u306a\u3063\u305f" : "\u7f6e\u304d\u3082\u306e\u304c\u306a\u304f\u306a\u3063\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (!removed) continue;
|
||||
|
|
@ -105,7 +107,7 @@
|
|||
const label = toolLabel(target.type);
|
||||
kindCounts.set(label, (kindCounts.get(label) || 0) + 1);
|
||||
}
|
||||
if (!removedCount) { if (!options.silentEmpty) showToast("範囲内に消せるものがありません。"); return true; }
|
||||
if (!removedCount) { if (!options.silentEmpty) showToast("\u7bc4\u56f2\u5185\u306b\u6d88\u305b\u308b\u3082\u306e\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return true; }
|
||||
audio.delete?.();
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts?.();
|
||||
|
|
@ -114,9 +116,9 @@
|
|||
if (typeof global.render === "function") global.render();
|
||||
if (options.drag) this._areaDeleteDragRemoved = (this._areaDeleteDragRemoved || 0) + removedCount;
|
||||
if (!options.silentLog) {
|
||||
const detail = [...kindCounts.entries()].slice(0, 4).map(([label, count]) => `${label}${count > 1 ? `×${count}` : ""}`).join("、");
|
||||
this.log?.(`範囲削除で${removedCount}個消した${detail ? `(${detail})` : ""}。`, "observe");
|
||||
showToast(`${removedCount}個消しました。`);
|
||||
const detail = [...kindCounts.entries()].slice(0, 4).map(([label, count]) => `${label}${count > 1 ? `\u00d7${count}` : ""}`).join("\u3001");
|
||||
this.log?.(`\u7bc4\u56f2\u524a\u9664\u3067${removedCount}\u500b\u6d88\u3057\u305f${detail ? `\uff08${detail}\uff09` : ""}\u3002`, "observe");
|
||||
showToast(`${removedCount}\u500b\u6d88\u3057\u307e\u3057\u305f\u3002`);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -139,12 +141,13 @@
|
|||
const hit = Math.max(18, (t.radius || 18) * 1.12);
|
||||
const d = distXY(x, y, t.x, t.y);
|
||||
if (d <= hit && d < bestD) {
|
||||
best = { kind: "tarinai", id: t.id || "", liveToken: t.liveToken ?? null, _ref: t, type: t.type || "tarinai", label: t.name || "たりない", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
|
||||
best = { kind: "tarinai", id: t.id || "", liveToken: t.liveToken ?? null, _ref: t, type: t.type || "tarinai", label: t.name || "\u305f\u308a\u306a\u3044", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
|
||||
bestD = d;
|
||||
}
|
||||
}
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
||||
const it = this.items[i];
|
||||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead) continue;
|
||||
if ((this.tool === "rope" || this.tool === "rod") && (it.type === "water" || it.type === "grass_bed")) continue;
|
||||
const isMechanism = Boolean(global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type));
|
||||
|
|
@ -189,11 +192,11 @@
|
|||
|
||||
useLinkToolAt(x, y) {
|
||||
const endpoint = this.findLinkEndpointAt?.(x, y);
|
||||
const toolName = this.tool === "rod" ? "棒" : "ヒモ";
|
||||
if (!endpoint) { showToast(`${toolName}をつなぐ対象がありません。`); return true; }
|
||||
const toolName = this.tool === "rod" ? "\u68d2" : "\u30d2\u30e2";
|
||||
if (!endpoint) { showToast(`${toolName}\u3092\u3064\u306a\u3050\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002`); return true; }
|
||||
if (!this.pendingLinkEndpoint) {
|
||||
this.pendingLinkEndpoint = endpoint;
|
||||
showToast(`${endpoint.label || "対象"}に${toolName}の片端をつなぎました。もう片端を選んでください。`);
|
||||
showToast(`${endpoint.label || "\u5bfe\u8c61"}\u306b${toolName}\u306e\u7247\u7aef\u3092\u3064\u306a\u304e\u307e\u3057\u305f\u3002\u3082\u3046\u7247\u7aef\u3092\u9078\u3093\u3067\u304f\u3060\u3055\u3044\u3002`);
|
||||
this.drawListDirty = true;
|
||||
if (typeof global.render === "function") global.render();
|
||||
return true;
|
||||
|
|
@ -202,7 +205,7 @@
|
|||
global.TarinaiLinkRuntime?.snapEndpointToTarget?.(first, this);
|
||||
global.TarinaiLinkRuntime?.snapEndpointToTarget?.(endpoint, this);
|
||||
if (first.kind === endpoint.kind && first.id === endpoint.id && distXY(first.x, first.y, endpoint.x, endpoint.y) < 8) {
|
||||
showToast("別の位置または対象を選んでください。");
|
||||
showToast("\u5225\u306e\u4f4d\u7f6e\u307e\u305f\u306f\u5bfe\u8c61\u3092\u9078\u3093\u3067\u304f\u3060\u3055\u3044\u3002");
|
||||
return true;
|
||||
}
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
|
|
@ -225,11 +228,11 @@
|
|||
item.amount = 999;
|
||||
const placed = this.addItem?.(item, constraintType);
|
||||
this.pendingLinkEndpoint = null;
|
||||
if (!placed) { showToast(`${toolName}を追加できませんでした。`); return true; }
|
||||
if (!placed) { showToast(`${toolName}\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002`); return true; }
|
||||
global.TarinaiPhysicsSoftClear?.clearForItem?.(this, item, `${constraintType}-soft-overlap-clear`);
|
||||
forceImmediateToolVisualRefresh(this, `${constraintType}-create`);
|
||||
this.log?.(`${toolName}をつないだ。`, "observe");
|
||||
showToast(`${toolName}をつなぎました。`);
|
||||
this.log?.(`${toolName}\u3092\u3064\u306a\u3044\u3060\u3002`, "observe");
|
||||
showToast(`${toolName}\u3092\u3064\u306a\u304e\u307e\u3057\u305f\u3002`);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -272,22 +275,25 @@
|
|||
findShootToolTargetAt(x, y) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
for (let i = this.tarinai.length - 1; i >= 0; i -= 1) {
|
||||
const t = this.tarinai[i];
|
||||
const tarinaiCandidates = this.nearbyTarinai?.(x, y, 82, true) || this.tarinai || [];
|
||||
for (let i = tarinaiCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const t = tarinaiCandidates[i];
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const hit = t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.28;
|
||||
const d = distXY(x, y, t.x, t.y);
|
||||
if (hit && d < bestD) { best = { kind: "tarinai", target: t, distance: d }; bestD = d; }
|
||||
}
|
||||
for (let i = (this.ants || []).length - 1; i >= 0; i -= 1) {
|
||||
const ant = this.ants[i];
|
||||
const antCandidates = this.nearbyAnts?.(x, y, 44, true) || this.ants || [];
|
||||
for (let i = antCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const ant = antCandidates[i];
|
||||
if (!ant || ant.dead) continue;
|
||||
const hitR = Math.max(10, (ant.r || 6) * 1.85);
|
||||
const d = distXY(x, y, ant.x, ant.y);
|
||||
if (d <= hitR && d < bestD) { best = { kind: "ant", target: ant, distance: d }; bestD = d; }
|
||||
}
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
||||
const it = this.items[i];
|
||||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead || it.type === "trace" || it.type === "splat") continue;
|
||||
let d = distXY(x, y, it.x, it.y);
|
||||
let hit = false;
|
||||
|
|
@ -334,7 +340,7 @@
|
|||
const direct = Boolean(options.direct);
|
||||
const strength = Math.max(0.25, Number(options.strength) || 1);
|
||||
const target = this.shootFleeTargetFor?.(t, x, y, direct ? 170 : 115, direct ? 260 : 205) || null;
|
||||
const reason = direct ? "撃たれてパニックで逃げている" : "近くに弾が当たってパニックで逃げている";
|
||||
const reason = direct ? "\u6483\u305f\u308c\u3066\u30d1\u30cb\u30c3\u30af\u3067\u9003\u3052\u3066\u3044\u308b" : "\u8fd1\u304f\u306b\u5f3e\u304c\u5f53\u305f\u3063\u3066\u30d1\u30cb\u30c3\u30af\u3067\u9003\u3052\u3066\u3044\u308b";
|
||||
const threat = { x: Number(x) || t.x, y: Number(y) || t.y, type: "shoot", dead: false };
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, (direct ? 2.8 : 1.25) * strength);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, (direct ? 0.34 : 0.28) * strength);
|
||||
|
|
@ -356,7 +362,8 @@
|
|||
panicTarinaiAroundShootImpact(x, y, directTarget = null) {
|
||||
const radius = 138;
|
||||
let count = 0;
|
||||
for (const t of this.tarinai || []) {
|
||||
const candidates = this.nearbyTarinai?.(x, y, radius + 32, true) || this.tarinai || [];
|
||||
for (const t of candidates) {
|
||||
if (!t || t.dead || t === directTarget || this.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = distXY(x, y, t.x, t.y);
|
||||
if (d > radius) continue;
|
||||
|
|
@ -377,7 +384,7 @@
|
|||
if (hit?.kind === "tarinai") {
|
||||
const t = hit.target;
|
||||
const damage = 16;
|
||||
t.damage?.(damage, "射撃");
|
||||
t.damage?.(damage, "\u5c04\u6483");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.35);
|
||||
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.22);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 2.8);
|
||||
|
|
@ -397,13 +404,13 @@
|
|||
if (shouldRemove) {
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
||||
reason: "shoot-tool",
|
||||
userReason: "射撃で消えた",
|
||||
userReason: "\u5c04\u6483\u3067\u6d88\u3048\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (removed) {
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts?.();
|
||||
this.rebuildSpatial?.();
|
||||
this.markItemBucketsDirty?.("shoot-delete");
|
||||
this.markSpatialDirty?.("shoot-delete");
|
||||
this.drawListDirty = true;
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -48,18 +48,100 @@
|
|||
|
||||
colonyMoodDefinition(id = "relaxed") {
|
||||
const defs = {
|
||||
calm: { id: "calm", label: "穏やか", description: "社交性が上がる", effects: { personality: { sociability: 0.42 } } },
|
||||
prickly: { id: "prickly", label: "緊張", description: "攻撃性が上がる", effects: { personality: { aggression: 0.46 } } },
|
||||
stinky: { id: "stinky", label: "不衛生", description: "開放性が下がる", effects: { personality: { openness: -0.44 } } },
|
||||
weaklings: { id: "weaklings", label: "弱体化", description: "ずんちどれい以外のストレスが下がる", effects: { personality: {} } },
|
||||
zunda_party: { id: "zunda_party", label: "ずんだ集中", description: "社交性と開放性が上がり、ストレスが下がる", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
ball_party: { id: "ball_party", label: "ボール集中", description: "社交性と開放性が上がり、ストレスが下がる", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
sun_party: { id: "sun_party", label: "日光浴傾向", description: "日光浴の頻度が上がる", effects: { personality: {} } },
|
||||
ant_overrun: { id: "ant_overrun", label: "アリ増加", description: "アリを見るとパニックになりやすい", effects: { personality: {} } },
|
||||
relaxed: { id: "relaxed", label: "安定", description: "標準状態", effects: { personality: {} } },
|
||||
calm: { id: "calm", label: "\u7a4f\u3084\u304b", description: "\u793e\u4ea4\u6027\u304c\u4e0a\u304c\u308b", effects: { personality: { sociability: 0.42 } } },
|
||||
prickly: { id: "prickly", label: "\u7dca\u5f35", description: "\u653b\u6483\u6027\u304c\u4e0a\u304c\u308b", effects: { personality: { aggression: 0.46 } } },
|
||||
stinky: { id: "stinky", label: "\u4e0d\u885b\u751f", description: "\u958b\u653e\u6027\u304c\u4e0b\u304c\u308b", effects: { personality: { openness: -0.44 } } },
|
||||
weaklings: { id: "weaklings", label: "\u5f31\u4f53\u5316", description: "\u305a\u3093\u3061\u3069\u308c\u3044\u4ee5\u5916\u306e\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: {} } },
|
||||
zunda_party: { id: "zunda_party", label: "\u305a\u3093\u3060\u96c6\u4e2d", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u304c\u4e0a\u304c\u308a\u3001\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
ball_party: { id: "ball_party", label: "\u30dc\u30fc\u30eb\u96c6\u4e2d", description: "\u793e\u4ea4\u6027\u3068\u958b\u653e\u6027\u304c\u4e0a\u304c\u308a\u3001\u30b9\u30c8\u30ec\u30b9\u304c\u4e0b\u304c\u308b", effects: { personality: { sociability: 0.38, openness: 0.38 } } },
|
||||
sun_party: { id: "sun_party", label: "\u65e5\u5149\u6d74\u50be\u5411", description: "\u65e5\u5149\u6d74\u306e\u983b\u5ea6\u304c\u4e0a\u304c\u308b", effects: { personality: {} } },
|
||||
ant_overrun: { id: "ant_overrun", label: "\u30a2\u30ea\u5897\u52a0", description: "\u30a2\u30ea\u3092\u898b\u308b\u3068\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308a\u3084\u3059\u3044", effects: { personality: {} } },
|
||||
devastation: { id: "devastation", label: "\u58ca\u6ec5\u5bf8\u524d", description: "\u30b3\u30ed\u30cb\u30fc\u306e\u5b58\u7d9a\u304c\u304b\u306a\u308a\u5371\u3046\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
crisis: { id: "crisis", label: "\u5371\u6a5f", description: "\u3051\u304c\u3084\u6d88\u8017\u304c\u76ee\u7acb\u3061\u3001\u7acb\u3066\u76f4\u3057\u304c\u5fc5\u8981\u306a\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
confusion: { id: "confusion", label: "\u6df7\u4e71", description: "\u9a5a\u304d\u3084\u30d1\u30cb\u30c3\u30af\u3067\u843d\u3061\u7740\u304d\u304c\u5931\u308f\u308c\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
||||
fearful: { id: "fearful", label: "\u6050\u6016", description: "\u5b89\u5168\u3092\u6c42\u3081\u308b\u500b\u4f53\u304c\u591a\u304f\u3001\u5371\u967a\u3092\u907f\u3051\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
||||
weakened: { id: "weakened", label: "\u8870\u5f31", description: "\u4f53\u529b\u3084\u5143\u6c17\u304c\u843d\u3061\u3001\u52d5\u304d\u304c\u5f31\u304f\u306a\u3063\u3066\u3044\u307e\u3059\u3002", effects: { personality: {} } },
|
||||
disease_spread: { id: "disease_spread", label: "\u75c5\u6c17\u8513\u5ef6", description: "\u75c5\u6c17\u306e\u500b\u4f53\u304c\u76ee\u7acb\u3061\u3001\u8abf\u5b50\u3092\u5d29\u3057\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
overcrowded: { id: "overcrowded", label: "\u904e\u5bc6", description: "\u500b\u4f53\u540c\u58eb\u306e\u8ddd\u96e2\u304c\u8fd1\u304f\u3001\u3076\u3064\u304b\u308a\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
isolated: { id: "isolated", label: "\u5b64\u7acb", description: "\u4ef2\u9593\u304c\u5c11\u306a\u3044\u3001\u307e\u305f\u306f\u8ddd\u96e2\u304c\u96e2\u308c\u3066\u5bc2\u3057\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
breeding: { id: "breeding", label: "\u7e41\u6b96", description: "\u7e41\u6b96\u3084\u89aa\u5bc6\u306a\u884c\u52d5\u304c\u8d77\u3053\u308a\u3084\u3059\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
happy: { id: "happy", label: "\u5e78\u798f", description: "\u30b9\u30c8\u30ec\u30b9\u304c\u5c11\u306a\u304f\u3001\u4f59\u88d5\u306e\u3042\u308b\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
relaxed: { id: "relaxed", label: "\u5b89\u5b9a", description: "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
};
|
||||
return defs[id] || defs.relaxed;
|
||||
},
|
||||
|
||||
evaluateColonyMood(force = false) {
|
||||
const now = this.time || 0;
|
||||
if (!force && this.colonyMood && now < (this.nextColonyMoodEvalAt || 0)) return this.colonyMood;
|
||||
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
||||
const n = alive.length;
|
||||
const avg = (fn) => n ? alive.reduce((sum, t) => sum + (Number(fn(t)) || 0), 0) / n : 0;
|
||||
const count = (fn) => alive.reduce((sum, t) => sum + (fn(t) ? 1 : 0), 0);
|
||||
const ratio = (value) => n ? value / n : 0;
|
||||
const avgEnergy = avg(t => t.energy || 0);
|
||||
const avgStress = avg(t => t.stress || 0);
|
||||
const avgHunger = avg(t => t.hunger || 0);
|
||||
const avgSafety = avg(t => t.needs?.safety || 0);
|
||||
const panicCount = count(t => t.state === "panic" || (t.fearTimer || 0) > 0.2 || (t.panicTargetUntil || 0) > now);
|
||||
const safetyHighCount = count(t => (t.needs?.safety || 0) >= 52 || (t.fearTimer || 0) > 0.35);
|
||||
const weakCount = count(t => (t.energy || 0) < 45 || (t.hunger || 0) > 88 || (t.needs?.health || 0) > 58);
|
||||
const diseasedCount = count(t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease);
|
||||
const breedingCount = count(t => (t.loveMochiTimer || 0) > 0.1 || t.state === "birth_ritual" || (t.birthRitualTimer || 0) > 0.1);
|
||||
const recentDeaths = (Array.isArray(this.recentDeathTimes) ? this.recentDeathTimes : []).filter(t => now - (Number(t) || -999) <= 30).length;
|
||||
let nearestAvg = Infinity;
|
||||
if (n >= 2) {
|
||||
let totalNearest = 0;
|
||||
for (const a of alive) {
|
||||
let best = Infinity;
|
||||
for (const b of alive) {
|
||||
if (a === b) continue;
|
||||
const dx = (a.x || 0) - (b.x || 0);
|
||||
const dy = (a.y || 0) - (b.y || 0);
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < best) best = d2;
|
||||
}
|
||||
if (Number.isFinite(best)) totalNearest += Math.sqrt(best);
|
||||
}
|
||||
nearestAvg = totalNearest / n;
|
||||
}
|
||||
const recentShock = (now - (this.lastShootAt || -999) <= 8) || (now - (this.lastGenkotsuAt || -999) <= 8);
|
||||
const panicRatio = ratio(panicCount);
|
||||
const safetyRatio = ratio(safetyHighCount);
|
||||
const weakRatio = ratio(weakCount);
|
||||
const diseaseRatio = ratio(diseasedCount);
|
||||
const breedingRatio = ratio(breedingCount);
|
||||
const fieldId = String(this.fieldType || "garden");
|
||||
const overcrowdLimit = fieldId === "cage" ? 20 : (fieldId === "park" ? 100 : 50);
|
||||
const denseCrowd = Number.isFinite(nearestAvg) && nearestAvg < 84 && n >= Math.max(8, Math.floor(overcrowdLimit * 0.55));
|
||||
const candidates = [];
|
||||
const add = (id, priority, ok) => { if (ok) candidates.push({ id, priority }); };
|
||||
|
||||
// Severe or multi-signal situations are evaluated first. Simple population
|
||||
// and comfort labels are deliberately fallback states so they do not mask
|
||||
// richer current events.
|
||||
add("devastation", 120, n <= 0 || recentDeaths >= 3 || (n <= 2 && this.deadCount > 0 && (recentDeaths > 0 || weakRatio >= 0.35 || panicRatio >= 0.20)));
|
||||
add("crisis", 110, recentDeaths >= 2 || (avgEnergy < 38 && weakRatio >= 0.34) || (avgHunger > 88 && weakRatio >= 0.40));
|
||||
add("confusion", 100, (recentShock && (panicRatio >= 0.10 || safetyRatio >= 0.18)) || panicRatio >= 0.25);
|
||||
add("fearful", 90, (avgSafety >= 50 && safetyRatio >= 0.28) || (recentShock && safetyRatio >= 0.18));
|
||||
add("disease_spread", 82, diseaseRatio >= 0.25 || (diseasedCount >= 3 && diseaseRatio >= 0.16));
|
||||
add("weakened", 74, (avgEnergy < 54 && weakRatio >= 0.18) || avgHunger > 82 || weakRatio >= 0.34);
|
||||
add("breeding", 66, breedingRatio >= 0.10 || breedingCount >= 2 || now - (this.lastBirthAt || -999) <= 45);
|
||||
add("happy", 46, avgStress < 28 && panicRatio < 0.08 && diseaseRatio < 0.12 && weakRatio < 0.18);
|
||||
add("overcrowded", 34, n >= overcrowdLimit || denseCrowd);
|
||||
add("isolated", 28, n <= 1 || (n <= 3 && nearestAvg > Math.max(180, Math.min(this.w || 900, this.h || 600) * 0.32)));
|
||||
|
||||
let id = "relaxed";
|
||||
if (candidates.length) {
|
||||
candidates.sort((a, b) => b.priority - a.priority);
|
||||
id = candidates[0].id;
|
||||
}
|
||||
const next = this.colonyMoodDefinition(id);
|
||||
this.colonyMood = next;
|
||||
this.nextColonyMoodEvalAt = now + 1.25;
|
||||
return next;
|
||||
},
|
||||
|
||||
observeHighlightKind(t) {
|
||||
const s = this.selected;
|
||||
if (!s || !t || t === s || t.dead || this.tool !== "observe") return "";
|
||||
|
|
@ -107,9 +189,12 @@
|
|||
return conflicts;
|
||||
},
|
||||
|
||||
blastZunchiFrom(x, y, radius, strength = 1) {
|
||||
blastZunchiFrom(x, y, radius, strength = 1, options = {}) {
|
||||
let moved = 0;
|
||||
const limit = Math.max(1, Number(options.limit || 180) || 180);
|
||||
const effectLimit = Math.max(0, Number(options.effectLimit || 12) || 12);
|
||||
for (const it of this.nearbyItems(x, y, radius + 60)) {
|
||||
if (moved >= limit) break;
|
||||
if (!it || it.dead || it.type !== "zunchi") continue;
|
||||
let dx = it.x - x;
|
||||
let dy = it.y - y;
|
||||
|
|
@ -125,7 +210,7 @@
|
|||
it.amount = Math.max(10, (it.amount || 80) - p * 8);
|
||||
it.stage = "fresh";
|
||||
moved += 1;
|
||||
if (moved < 12) this.effects.push(new Effect("zunchi_miasma", it.x, it.y - 4, { vx: (it.vx || 0) * 0.15, vy: (it.vy || 0) * 0.15 - 18, size: rand(9, 18), life: rand(0.36, 0.62), color: "rgba(58,102,38,0.48)" }));
|
||||
if (moved < effectLimit) this.effects.push(new Effect("zunchi_miasma", it.x, it.y - 4, { vx: (it.vx || 0) * 0.15, vy: (it.vy || 0) * 0.15 - 18, size: rand(9, 18), life: rand(0.36, 0.62), color: "rgba(58,102,38,0.48)" }));
|
||||
}
|
||||
if (moved) this.drawListDirty = true;
|
||||
return moved;
|
||||
|
|
|
|||
|
|
@ -148,11 +148,11 @@
|
|||
},
|
||||
|
||||
groundDefinition() {
|
||||
return window.TarinaiGround?.definition?.(this.groundType || "soil") || GROUND_TYPES?.[this.groundType || "soil"] || GROUND_TYPES?.soil || { id: "soil", label: "砂", stressMultiplier: 1, grassMultiplier: 1 };
|
||||
return window.TarinaiGround?.definition?.(this.groundType || "soil") || GROUND_TYPES?.[this.groundType || "soil"] || GROUND_TYPES?.soil || { id: "soil", label: "\u7802", stressMultiplier: 1, grassMultiplier: 1 };
|
||||
},
|
||||
|
||||
groundLabel() {
|
||||
return window.TarinaiGround?.label?.(this.groundType || "soil") || this.groundDefinition().label || "砂";
|
||||
return window.TarinaiGround?.label?.(this.groundType || "soil") || this.groundDefinition().label || "\u7802";
|
||||
},
|
||||
|
||||
groundStressMultiplier() {
|
||||
|
|
@ -184,12 +184,12 @@
|
|||
if (typeof renderSelected === "function") renderSelected();
|
||||
if (typeof document !== "undefined") {
|
||||
const groundTextEl = document.getElementById("statGroundType");
|
||||
if (groundTextEl) groundTextEl.textContent = this.groundLabel?.() || "砂";
|
||||
if (groundTextEl) groundTextEl.textContent = this.groundLabel?.() || "\u7802";
|
||||
}
|
||||
if (!opts.silent) {
|
||||
const label = this.groundLabel?.() || "砂";
|
||||
this.log?.(`じめん: ${label}`);
|
||||
if (typeof showToast === "function") showToast(`じめんを ${label} にしました。`);
|
||||
const label = this.groundLabel?.() || "\u7802";
|
||||
this.log?.(`\u3058\u3081\u3093: ${label}`);
|
||||
if (typeof showToast === "function") showToast(`\u3058\u3081\u3093\u3092 ${label} \u306b\u3057\u307e\u3057\u305f\u3002`);
|
||||
}
|
||||
this.emit?.("ground:changed", { previous, next, definition: this.groundDefinition?.() });
|
||||
}
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
const next = typeof normalizedItemAngle === "function" ? normalizedItemAngle(angle, fallback) : angle;
|
||||
this.toolAngles[key] = next;
|
||||
this.drawListDirty = true;
|
||||
if (!silent && typeof showToast === "function") showToast(`向き: ${Math.round(next * 180 / Math.PI)}°`);
|
||||
if (!silent && typeof showToast === "function") showToast(`\u5411\u304d: ${Math.round(next * 180 / Math.PI)}\u00b0`);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -425,9 +425,9 @@ def check_current_save_format() -> None:
|
|||
if offenders:
|
||||
fail("broad snapshot fields still present after light-save conversion: " + ", ".join(offenders))
|
||||
required_codec = [
|
||||
'EXPORT_PREFIX = "たり"',
|
||||
'RAW_CODEC = "生"',
|
||||
'DEFLATE_CODEC = "縮"',
|
||||
'EXPORT_PREFIX = "\\u305f\\u308a"',
|
||||
'RAW_CODEC = "\\u751f"',
|
||||
'DEFLATE_CODEC = "\\u7e2e"',
|
||||
"SAVE_TEXT_ALPHABET",
|
||||
"jp2048Encode",
|
||||
"jp2048Decode",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "39.15.27";
|
||||
const APP_VERSION = "39.15.39";
|
||||
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