fixed issueeessss
This commit is contained in:
parent
aea08dc3fb
commit
874911821f
20 changed files with 1947 additions and 897 deletions
35
PATCH_NOTES.md
Normal file
35
PATCH_NOTES.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Patch Notes
|
||||||
|
|
||||||
|
## Encoding cleanup
|
||||||
|
- Normalized `index.html` back to UTF-8.
|
||||||
|
- Replaced mojibake strings with the intended labels.
|
||||||
|
- Escaped Japanese text in source files to avoid encoding-dependent corruption.
|
||||||
|
|
||||||
|
## Personality specification fixes
|
||||||
|
- Personality tags and data UI now use Japanese labels from the four-axis personality model.
|
||||||
|
- Threshold-crossing personality logs are now generated in Japanese and only when visible trait bands change.
|
||||||
|
- Daily personality caps now track increase and decrease separately per parameter.
|
||||||
|
- Current personality effects are neutral inside the -0.5 to +0.5 band.
|
||||||
|
- Direct conversion to the legacy timid personality after damage was removed; cowardly behavior is derived from high neuroticism plus low aggression.
|
||||||
|
- Corpse/splat contact now increases stress and slowly reduces neuroticism through habituation.
|
||||||
|
|
||||||
|
## Follow-up fixes
|
||||||
|
- Fixed CSS emoji escapes for tool UI icons. CSS now uses code-point escapes such as `\\1F448` instead of invalid surrogate escapes.
|
||||||
|
- Hid the detailed four-axis personality rows from the compact tarinai data UI.
|
||||||
|
- Restored `bestBedFor()` fallback so tarinai can choose hay beds when no usable nest box is available.
|
||||||
|
- Damage now wakes sleeping tarinai and forces tarinai hidden inside a nest box to leave the box.
|
||||||
|
- Added a short minimum stay window after entering a nest box to avoid immediate exit loops.
|
||||||
|
- Renamed the friend relation label from `\\u4ef2\\u826f\\u3057` to `\\u53cb\\u9054` in UI text and related logs.
|
||||||
|
|
||||||
|
|
||||||
|
## Personality cleanup v3
|
||||||
|
- Current live friend count now changes sociability drift. Family/friend proximity raises sociability more when the tarinai has more current friends, while a lonely tarinai with no current friends can slowly lose sociability.
|
||||||
|
- Removed the legacy named personality route from constructor, serialization, child inheritance, selected data UI, selected-card overlay, and family-tree titles. The game now uses only birthPersonality/currentPersonality four-axis data.
|
||||||
|
- Kept empty legacy stubs only for old-save compatibility so older saves cannot revive the pre-four-axis personality model.
|
||||||
|
|
||||||
|
## Follow-up fixes v4
|
||||||
|
- Restored compact personality information in the tarinai data panel: visible tags, birth personality, and current personality.
|
||||||
|
- Moved the focused-tarinai highlight outside the sprite rotation transform so the ring and label stay upright.
|
||||||
|
- Shortened ecology cards and added the HP/stress meter placement note: upper meter is HP, lower meter is stress.
|
||||||
|
- Raised the friendship threshold to 16 affinity and reused the same threshold for friend count, friend behavior, friend UI, and observe highlights.
|
||||||
|
- Observe mode now highlights friends of the selected tarinai as well as parents, children, and enemies.
|
||||||
|
|
@ -15,7 +15,7 @@ Read only these first: `README.md`, `docs/AI_CONTEXT.md`, `docs/code_map.md`. Th
|
||||||
|
|
||||||
## Important mechanics
|
## Important mechanics
|
||||||
|
|
||||||
- Personality affects fear, fighting, social pull, sleep priority, zunda preference, playfulness; children inherit one parent's personality.
|
- Personality uses four axes: aggression, openness, sociability, and neuroticism. Children inherit from the parents' current four-axis personality average with variation.
|
||||||
- Relationships track affinity/fear/fight outcomes and influence approach, avoidance, intimidation, conflict, co-parent behavior, and UI summaries.
|
- Relationships track affinity/fear/fight outcomes and influence approach, avoidance, intimidation, conflict, co-parent behavior, and UI summaries.
|
||||||
- Death causes are normalized by `js/health.js`: fight, hunger, disease, stress overload, over-poking, firecracker, accident, lifespan. Major damage within 10 seconds can become `<cause> de suyaku` in UI text (`...\u3067\u8870\u5f31`).
|
- Death causes are normalized by `js/health.js`: fight, hunger, disease, stress overload, over-poking, firecracker, accident, lifespan. Major damage within 10 seconds can become `<cause> de suyaku` in UI text (`...\u3067\u8870\u5f31`).
|
||||||
- Water-curable diseases force high-priority water seeking. Zunda-curable diseases force high-priority zunda seeking.
|
- Water-curable diseases force high-priority water seeking. Zunda-curable diseases force high-priority zunda seeking.
|
||||||
|
|
@ -72,3 +72,8 @@ docs/code_map.md Symbol-to-file lookup table.
|
||||||
- Selected Tarinai UI includes per-individual records derived from matching observation log entries.
|
- Selected Tarinai UI includes per-individual records derived from matching observation log entries.
|
||||||
- Observation highlights selected relatives and major social relations: parent, child, friend, enemy.
|
- Observation highlights selected relatives and major social relations: parent, child, friend, enemy.
|
||||||
- Explosions impart velocity to nearby zunchi items; cursor contact reduces stress and emits dense heart effects.
|
- Explosions impart velocity to nearby zunchi items; cursor contact reduces stress and emits dense heart effects.
|
||||||
|
|
||||||
|
## v4 \u8ffd\u52a0\u4fee\u6b63
|
||||||
|
- \u5de3\u7bb1\u5165\u9000\u5ba4\u3092\u5c02\u7528\u72b6\u614b\u5316\u3057\u3001\u900f\u660e\u5316\u30fb\u5f53\u305f\u308a\u5224\u5b9a\u6d88\u6ec5\u30fb\u500b\u4f53\u6570\u53cd\u6620\u30fb\u9000\u51fa\u5fa9\u5e30\u3092\u5b89\u5b9a\u5316\u3002
|
||||||
|
- \u7761\u7720\u6642\u306e\u512a\u5148\u9806\u4f4d\u3092\u300c\u5de3\u7bb1 \u2192 \u5e72\u3057\u8349\u30d9\u30c3\u30c9 \u2192 \u305d\u306e\u5834\u300d\u306b\u5909\u66f4\u3002
|
||||||
|
- \u305f\u308a\u306a\u3044\u30c7\u30fc\u30bfUI\u306e\u53cb\u9054/\u6575\u5bfe\u3092\u5168\u4ef6\u30c1\u30c3\u30d7\u8868\u793a\u306b\u3057\u3001\u30af\u30ea\u30c3\u30af\u3067\u500b\u4f53\u3078\u79fb\u52d5\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3002
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
162
css/styles.css
162
css/styles.css
|
|
@ -156,6 +156,8 @@ h1 {
|
||||||
.field-dialog.hidden { display: none; }
|
.field-dialog.hidden { display: none; }
|
||||||
.field-dialog-panel {
|
.field-dialog-panel {
|
||||||
width: min(440px, 100%);
|
width: min(440px, 100%);
|
||||||
|
max-height: calc(100vh - 36px);
|
||||||
|
overflow: auto;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(255, 252, 245, 0.96);
|
background: rgba(255, 252, 245, 0.96);
|
||||||
|
|
@ -220,6 +222,9 @@ h1 {
|
||||||
|
|
||||||
.ecology-dialog-panel {
|
.ecology-dialog-panel {
|
||||||
width: min(760px, 100%);
|
width: min(760px, 100%);
|
||||||
|
max-height: calc(100vh - 36px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
.ecology-grid {
|
.ecology-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -228,17 +233,17 @@ h1 {
|
||||||
}
|
}
|
||||||
.ecology-card {
|
.ecology-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 84px minmax(0, 1fr);
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(243,237,225,0.76));
|
background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(243,237,225,0.76));
|
||||||
border: 1px solid rgba(84,68,48,0.12);
|
border: 1px solid rgba(84,68,48,0.12);
|
||||||
}
|
}
|
||||||
.ecology-card img {
|
.ecology-card img {
|
||||||
width: 84px;
|
width: 72px;
|
||||||
height: 84px;
|
height: 72px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(0 8px 18px rgba(56, 44, 32, 0.10));
|
filter: drop-shadow(0 8px 18px rgba(56, 44, 32, 0.10));
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +256,7 @@ h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.55;
|
line-height: 1.40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
|
@ -338,11 +343,10 @@ h1 {
|
||||||
opacity: 0.10;
|
opacity: 0.10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.tool span { min-width: 16px; text-align: center; color: rgba(72,58,43,0.64); font-size: 10px; }
|
.tool > span:not(.tool-size-badge) { display: none; }
|
||||||
.tool[data-tool="observe"] { --tool-icon: url("../assets/ui/tool_observe.png"); }
|
.tool[data-tool="observe"] { --tool-icon: url("../assets/ui/tool_observe.png"); }
|
||||||
.tool[data-tool="zunchi"] { --tool-icon: url("../assets/ui/tool_zunchi.png"); }
|
.tool[data-tool="zunchi"] { --tool-icon: url("../assets/ui/tool_zunchi.png"); }
|
||||||
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.png"); }
|
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.png"); }
|
||||||
.tool[data-tool="water"] { --tool-icon: url("../assets/ui/tool_water.png"); }
|
|
||||||
.tool[data-tool="grass"] { --tool-icon: url("../assets/ui/tool_grass.png"); }
|
.tool[data-tool="grass"] { --tool-icon: url("../assets/ui/tool_grass.png"); }
|
||||||
.tool[data-tool="stone"] { --tool-icon: url("../assets/ui/tool_stone.png"); }
|
.tool[data-tool="stone"] { --tool-icon: url("../assets/ui/tool_stone.png"); }
|
||||||
.tool[data-tool="firecracker"] { --tool-icon: url("../assets/ui/tool_firecracker.png"); }
|
.tool[data-tool="firecracker"] { --tool-icon: url("../assets/ui/tool_firecracker.png"); }
|
||||||
|
|
@ -373,14 +377,14 @@ h1 {
|
||||||
}
|
}
|
||||||
.tool[data-tool="poke"] { --tool-icon: none; }
|
.tool[data-tool="poke"] { --tool-icon: none; }
|
||||||
.tool[data-tool="poke"]::before {
|
.tool[data-tool="poke"]::before {
|
||||||
content: "\1f448 ";
|
content: "\1F448";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||||
}
|
}
|
||||||
.tool[data-tool="poke"]::after {
|
.tool[data-tool="poke"]::after {
|
||||||
content: "\1f448 ";
|
content: "\1F448";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -396,14 +400,14 @@ h1 {
|
||||||
|
|
||||||
.tool[data-tool="pinch"] { --tool-icon: none; }
|
.tool[data-tool="pinch"] { --tool-icon: none; }
|
||||||
.tool[data-tool="pinch"]::before {
|
.tool[data-tool="pinch"]::before {
|
||||||
content: "\1f90f ";
|
content: "\1F90F";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||||
}
|
}
|
||||||
.tool[data-tool="pinch"]::after {
|
.tool[data-tool="pinch"]::after {
|
||||||
content: "\1f90f ";
|
content: "\1F90F";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -418,14 +422,14 @@ h1 {
|
||||||
}
|
}
|
||||||
.tool[data-tool="love_mochi"] { --tool-icon: none; }
|
.tool[data-tool="love_mochi"] { --tool-icon: none; }
|
||||||
.tool[data-tool="love_mochi"]::before {
|
.tool[data-tool="love_mochi"]::before {
|
||||||
content: "\1f495 ";
|
content: "\1F495";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||||
}
|
}
|
||||||
.tool[data-tool="love_mochi"]::after {
|
.tool[data-tool="love_mochi"]::after {
|
||||||
content: "\1f495 ";
|
content: "\1F495";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 46px;
|
font-size: 46px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -440,14 +444,14 @@ h1 {
|
||||||
}
|
}
|
||||||
.tool[data-tool="fight_mochi"] { --tool-icon: none; }
|
.tool[data-tool="fight_mochi"] { --tool-icon: none; }
|
||||||
.tool[data-tool="fight_mochi"]::before {
|
.tool[data-tool="fight_mochi"]::before {
|
||||||
content: "\1f4a2 ";
|
content: "\1F4A2";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
|
||||||
}
|
}
|
||||||
.tool[data-tool="fight_mochi"]::after {
|
.tool[data-tool="fight_mochi"]::after {
|
||||||
content: "\1f4a2 ";
|
content: "\1F4A2";
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 46px;
|
font-size: 46px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -472,7 +476,7 @@ h1 {
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.55;
|
line-height: 1.40;
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,11 +586,36 @@ meter {
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.55;
|
line-height: 1.40;
|
||||||
}
|
}
|
||||||
.selected-info.empty {
|
.selected-info.empty {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-name-editor {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.selected-name-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(103,178,44,0.34);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: rgba(246,252,238,0.96);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.82);
|
||||||
|
}
|
||||||
|
.selected-name-input:focus {
|
||||||
|
outline: 2px solid rgba(103,178,44,0.24);
|
||||||
|
border-color: rgba(103,178,44,0.62);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.selected-effects {
|
||||||
|
margin: 2px 0 6px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -605,6 +634,29 @@ meter {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
.info-row span:first-child { color: var(--muted); }
|
.info-row span:first-child { color: var(--muted); }
|
||||||
|
.personality-values strong {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 2px 8px;
|
||||||
|
max-width: 68%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.personality-values strong span {
|
||||||
|
color: var(--ink);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.personality-tag {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
background: rgba(103,178,44,0.10);
|
||||||
|
border: 1px solid rgba(103,178,44,0.22);
|
||||||
|
}
|
||||||
|
.personality-tag.neutral {
|
||||||
|
background: rgba(120,110,90,0.08);
|
||||||
|
border-color: rgba(120,110,90,0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.log-card { min-height: 260px; flex: 0 0 auto; display: flex; flex-direction: column; }
|
.log-card { min-height: 260px; flex: 0 0 auto; display: flex; flex-direction: column; }
|
||||||
.log {
|
.log {
|
||||||
|
|
@ -1250,8 +1302,8 @@ body::before {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 0.96;
|
opacity: 0.96;
|
||||||
}
|
}
|
||||||
.tool[data-tool="poke"]::before { content: "👈"; }
|
.tool[data-tool="poke"]::before { content: "\1F448"; }
|
||||||
.tool[data-tool="pinch"]::before { content: "🤏"; }
|
.tool[data-tool="pinch"]::before { content: "\1F90F"; }
|
||||||
.tool[data-tool="poke"]::after,
|
.tool[data-tool="poke"]::after,
|
||||||
.tool[data-tool="pinch"]::after {
|
.tool[data-tool="pinch"]::after {
|
||||||
background: none;
|
background: none;
|
||||||
|
|
@ -1268,8 +1320,8 @@ body::before {
|
||||||
width: 54px;
|
width: 54px;
|
||||||
height: 54px;
|
height: 54px;
|
||||||
}
|
}
|
||||||
.tool[data-tool="poke"]::after { content: "👈"; }
|
.tool[data-tool="poke"]::after { content: "\1F448"; }
|
||||||
.tool[data-tool="pinch"]::after { content: "🤏"; }
|
.tool[data-tool="pinch"]::after { content: "\1F90F"; }
|
||||||
.tool[data-tool="poke"] span,
|
.tool[data-tool="poke"] span,
|
||||||
.tool[data-tool="pinch"] span { display: none; }
|
.tool[data-tool="pinch"] span { display: none; }
|
||||||
|
|
||||||
|
|
@ -1277,7 +1329,7 @@ body::before {
|
||||||
/* 15.3.0: shower uses emoji; nest box and fences use appearance-matched assets/hitboxes. */
|
/* 15.3.0: shower uses emoji; nest box and fences use appearance-matched assets/hitboxes. */
|
||||||
.tool[data-tool="water_hose"] { --tool-icon: none; }
|
.tool[data-tool="water_hose"] { --tool-icon: none; }
|
||||||
.tool[data-tool="water_hose"]::before {
|
.tool[data-tool="water_hose"]::before {
|
||||||
content: "🚿";
|
content: "\1F6BF";
|
||||||
background: none;
|
background: none;
|
||||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
@ -1287,7 +1339,7 @@ body::before {
|
||||||
opacity: 0.96;
|
opacity: 0.96;
|
||||||
}
|
}
|
||||||
.tool[data-tool="water_hose"]::after {
|
.tool[data-tool="water_hose"]::after {
|
||||||
content: "🚿";
|
content: "\1F6BF";
|
||||||
background: none;
|
background: none;
|
||||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||||
font-size: 46px;
|
font-size: 46px;
|
||||||
|
|
@ -1300,3 +1352,67 @@ body::before {
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relation-list-row {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.relation-list-row > strong {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.relation-chip-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.relation-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 138px;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgba(104, 150, 84, 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(250, 255, 242, 0.88);
|
||||||
|
color: #4d6140;
|
||||||
|
padding: 2px 6px 2px 2px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 0 rgba(255,255,255,0.74);
|
||||||
|
}
|
||||||
|
.relation-chip.enemy {
|
||||||
|
border-color: rgba(178, 92, 74, 0.28);
|
||||||
|
background: rgba(255, 246, 240, 0.92);
|
||||||
|
color: #754739;
|
||||||
|
}
|
||||||
|
.relation-chip:hover,
|
||||||
|
.relation-chip:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: rgba(255,255,255,0.98);
|
||||||
|
outline: 2px solid rgba(118, 186, 72, 0.18);
|
||||||
|
}
|
||||||
|
.relation-chip.enemy:hover,
|
||||||
|
.relation-chip.enemy:focus-visible {
|
||||||
|
outline-color: rgba(190, 92, 72, 0.18);
|
||||||
|
}
|
||||||
|
.relation-chip .relation-face {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
.relation-chip-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.relation-chip em {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.72;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
574
docs/prompt.txt
574
docs/prompt.txt
|
|
@ -1,485 +1,265 @@
|
||||||
You are working on the Tarinai observation game.
|
You are modifying an existing JavaScript browser game project.
|
||||||
|
|
||||||
Base input:
|
Implement a personality system for the \u201ctarinai\u201d creatures. Keep the implementation conservative and debuggable. Do not add extra systems beyond the specification below.
|
||||||
|
|
||||||
* Start from `tarinai_colony_game_fixed_v6.zip`.
|
## Goal
|
||||||
* Modify the project files directly.
|
|
||||||
* Return a new zip.
|
|
||||||
* Do not remove existing gameplay features unless explicitly instructed.
|
|
||||||
* Preserve the existing Japanese UI text style.
|
|
||||||
* After changes, run syntax checks on all JavaScript files and verify zip integrity.
|
|
||||||
* Bump the cache-busting version in `index.html` so browsers do not keep loading old JS/CSS.
|
|
||||||
|
|
||||||
Primary goals:
|
Add four continuous personality parameters to each tarinai:
|
||||||
Fix the remaining complexity, behavior mismatch, redundant logic, and fragmented-spec issues identified in the v6 audit. Implement all items below. Do not skip any item.
|
|
||||||
|
|
||||||
---
|
* aggression
|
||||||
|
* openness
|
||||||
|
* sociability
|
||||||
|
* neuroticism
|
||||||
|
|
||||||
## 1. Family tree update performance
|
Each parameter ranges from `-1.0` to `+1.0`.
|
||||||
|
|
||||||
Current issue:
|
Personality should affect behavior only when the value crosses clear thresholds. Small changes should be recorded internally, but observation logs should only be written when a threshold is crossed.
|
||||||
The family tree update still performs heavy synchronous preprocessing, causing the main game to freeze.
|
|
||||||
|
|
||||||
Implement:
|
## Personality axes
|
||||||
|
|
||||||
* Split heavy preprocessing into small chunks.
|
### aggression
|
||||||
* Use `requestIdleCallback` when available, with a safe `setTimeout` fallback.
|
|
||||||
* Avoid doing large graph construction, signature generation, relationship counting, or DOM creation in one blocking pass.
|
|
||||||
* The game loop must remain responsive while family tree data is prepared.
|
|
||||||
|
|
||||||
Update timing rules:
|
Range:
|
||||||
|
|
||||||
* When the family tree panel is hidden:
|
* low side: calm
|
||||||
|
* high side: irritable
|
||||||
|
|
||||||
* Do not update every 5 seconds.
|
Behavior:
|
||||||
* Check/update at most once every 30 seconds.
|
|
||||||
* When the family tree panel is visible:
|
|
||||||
|
|
||||||
* Do not update on a fixed 5-second timer.
|
* High aggression makes the tarinai more likely to threaten others, start fights, or retaliate.
|
||||||
* Update only when the family tree is dirty, meaning a relevant change occurred.
|
* Low aggression makes the tarinai less likely to start fights.
|
||||||
* Relevant dirty events include at least:
|
|
||||||
|
|
||||||
* birth
|
Changes:
|
||||||
* death
|
|
||||||
* parent/child relationship creation
|
|
||||||
* mate/partner relationship creation or change
|
|
||||||
* name/identity data change that affects the family tree display
|
|
||||||
* Add or use a clear dirty flag such as `familyTreeDirty`.
|
|
||||||
* Mark dirty from the source of the change, not by repeatedly recomputing expensive signatures.
|
|
||||||
* If a background update is already running, do not start another one.
|
|
||||||
* If a newer dirty event occurs while an update is running, schedule one additional update after the current update completes.
|
|
||||||
|
|
||||||
Expected result:
|
* Winning fights should slightly increase aggression.
|
||||||
|
* Losing fights should slightly decrease aggression.
|
||||||
|
|
||||||
* Opening or updating the family tree should not freeze the main simulation.
|
### openness
|
||||||
* Hidden family tree work should be very rare.
|
|
||||||
* Visible family tree should refresh promptly after actual changes.
|
|
||||||
|
|
||||||
---
|
Range:
|
||||||
|
|
||||||
## 2. Coward personality condition
|
* low side: conservative
|
||||||
|
* high side: playful
|
||||||
|
|
||||||
Current issue:
|
Behavior:
|
||||||
The “coward” condition may count total fight results and per-relationship fight results together, causing double counting.
|
|
||||||
|
|
||||||
Implement:
|
* High openness makes the tarinai more likely to wander farther, investigate new objects, and play with toys such as balls.
|
||||||
|
* Low openness makes the tarinai prefer familiar or nearby areas.
|
||||||
|
|
||||||
* The 5% chance to become `臆病` must be based only on total fight wins and total fight losses.
|
Changes:
|
||||||
* Do not include per-relationship win/loss counts in this condition.
|
|
||||||
* Trigger condition:
|
|
||||||
|
|
||||||
* The Tarinai has just taken damage.
|
* Being petted by the cursor should slightly increase openness.
|
||||||
* Its total fight losses are greater than its total fight wins.
|
|
||||||
* Then it has a 5% chance to change personality to `臆病`.
|
|
||||||
* Avoid changing the personality repeatedly if it is already `臆病`.
|
|
||||||
|
|
||||||
Expected result:
|
### sociability
|
||||||
|
|
||||||
* The calculation uses only aggregate lifetime fight result counters.
|
Range:
|
||||||
* No double counting.
|
|
||||||
|
|
||||||
---
|
* low side: loner
|
||||||
|
* high side: social
|
||||||
|
|
||||||
## 3. Disease fight initiation and targeting rules
|
Behavior:
|
||||||
|
|
||||||
Implement the following disease fight rules exactly:
|
* High sociability makes the tarinai more likely to approach other tarinai, especially parents, children, friends, or the cursor.
|
||||||
|
* High sociability also makes the tarinai more likely to react when a parent, child, or friend is fighting nearby.
|
||||||
|
* Low sociability makes the tarinai prefer being alone.
|
||||||
|
|
||||||
Fight initiation:
|
Changes:
|
||||||
|
|
||||||
* Tarinai with `爆発病` can initiate fights.
|
* Spending time near parents, children, or friends should slightly increase sociability.
|
||||||
* Tarinai with `ずんち病` can initiate fights.
|
|
||||||
* Tarinai with any other disease must not initiate fights.
|
|
||||||
* Healthy Tarinai may initiate fights according to the existing rules.
|
|
||||||
|
|
||||||
Being targeted / challenged:
|
### neuroticism
|
||||||
|
|
||||||
* Tarinai with `ねむり病` must not be targeted for fights.
|
Range:
|
||||||
* Tarinai with any other disease may still be targeted/challenged by another Tarinai.
|
|
||||||
* This means:
|
|
||||||
|
|
||||||
* `爆発病` and `ずんち病`: can initiate and can be targeted.
|
* low side: insensitive / unfazed
|
||||||
* Other non-sleep diseases: cannot initiate, but can be targeted.
|
* high side: delicate / sensitive
|
||||||
* `ねむり病`: cannot initiate and cannot be targeted.
|
|
||||||
|
|
||||||
Apply consistently to:
|
Behavior:
|
||||||
|
|
||||||
* Normal fight selection.
|
* High neuroticism makes the tarinai gain more stress from unpleasant stimuli such as corpses, poop, nearby fights, or overcrowding.
|
||||||
* Forced fight logic.
|
* Low neuroticism makes the tarinai less affected by such stimuli.
|
||||||
* Fight-food effects such as `けんか餅`.
|
|
||||||
* Any helper such as `canStartFight`, `canBeFightTarget`, or equivalent.
|
|
||||||
|
|
||||||
Expected result:
|
Changes:
|
||||||
|
|
||||||
* Disease rules are not handled by scattered ad-hoc checks.
|
* Touching poop or corpses should still increase stress immediately.
|
||||||
* Use clear helper functions if they do not already exist.
|
* However, repeated exposure should slightly reduce neuroticism over time, representing habituation.
|
||||||
|
* This means tarinai can become less sensitive to unpleasant stimuli after repeated exposure.
|
||||||
|
|
||||||
---
|
Important:
|
||||||
|
|
||||||
## 4. Collision and external-force unification
|
* Do not treat \u201ccowardice\u201d as a direct low-aggression state.
|
||||||
|
* If a cowardly tag is needed, derive it as a compound state from high neuroticism and low aggression.
|
||||||
|
* Do not implement a separate cowardice parameter.
|
||||||
|
|
||||||
Current issues:
|
## Birth personality and current personality
|
||||||
|
|
||||||
* Ball speed cap was removed, but some collision detection still uses capped sweep/search distances.
|
Each tarinai should have two personality objects:
|
||||||
* Very fast balls can still tunnel through objects.
|
|
||||||
* Explosion knockback and ball knockback can be reduced by normal movement speed caps.
|
|
||||||
* Knockback, collision damage, explosion impulse, and normal movement are handled inconsistently.
|
|
||||||
|
|
||||||
Implement:
|
```js
|
||||||
|
birthPersonality
|
||||||
|
currentPersonality
|
||||||
|
```
|
||||||
|
|
||||||
* Separate normal self-propelled movement from external impulse/knockback.
|
Rules:
|
||||||
* External impulses must not be clamped by the normal Tarinai walking speed cap.
|
|
||||||
* Add or refactor toward helper methods such as:
|
|
||||||
|
|
||||||
* `applyImpulse(entity, vx, vy, options)`
|
* `birthPersonality` is created at birth and normally does not change.
|
||||||
* `applyKnockback(entity, sourceX, sourceY, strength, options)`
|
* `currentPersonality` starts equal to `birthPersonality`.
|
||||||
* `applyImpactDamage(target, amount, cause)`
|
* Experience modifies only `currentPersonality`.
|
||||||
* `resolveCircleCircleCollision(...)`
|
* The tarinai data UI should show both birth personality and current personality.
|
||||||
* `resolveCircleRectCollision(...)`
|
|
||||||
* Exact helper names are flexible, but the behavior must be centralized and not duplicated.
|
|
||||||
|
|
||||||
Unlimited ball speed:
|
## Inheritance
|
||||||
|
|
||||||
* Do not reintroduce a ball speed cap.
|
When a child is born:
|
||||||
* Collision detection must handle high-speed balls using swept movement based on actual frame movement distance.
|
|
||||||
* Remove fixed low `Math.min(...)` collision-search caps that contradict unlimited ball speed.
|
|
||||||
* Ball vs wall, ball vs fence, ball vs nest box, ball vs ball, and ball vs Tarinai should remain reliable at high speed.
|
|
||||||
|
|
||||||
High-speed ball vs Tarinai:
|
```text
|
||||||
|
child birth personality =
|
||||||
|
average of the parents' current personality
|
||||||
|
+ random variation between -0.2 and +0.2 for each parameter
|
||||||
|
```
|
||||||
|
|
||||||
* When a sufficiently fast ball hits a Tarinai:
|
Clamp each resulting value to `-1.0` through `+1.0`.
|
||||||
|
|
||||||
* Knock the Tarinai away using velocity equal to 1/5 of the ball velocity.
|
Then:
|
||||||
* This knockback must be an external impulse, not normal walking movement.
|
|
||||||
* Do not let the normal “play with ball” behavior also run for that same impact if it would conflict.
|
|
||||||
* If the damage from this impact kills the Tarinai, death cause must be `衝突`.
|
|
||||||
|
|
||||||
High-speed Tarinai vs Tarinai:
|
```js
|
||||||
|
child.currentPersonality = child.birthPersonality
|
||||||
|
```
|
||||||
|
|
||||||
* When a sufficiently fast Tarinai hits another Tarinai:
|
The child should pass its changed current personality to future descendants, not only its original birth personality.
|
||||||
|
|
||||||
* The hit Tarinai should also be knocked away.
|
## Thresholds and personality tags
|
||||||
* Use the attacker’s actual movement/impulse direction and speed.
|
|
||||||
* If collision damage kills a Tarinai, death cause must be `衝突`.
|
|
||||||
|
|
||||||
Explosion knockback:
|
Use two threshold levels:
|
||||||
|
|
||||||
* Make explosion knockback stronger than before.
|
```text
|
||||||
* Use external impulse, not normal movement velocity.
|
-1.0 to -0.75: strong low-side trait
|
||||||
* Knockback should be partly random.
|
-0.75 to -0.5: slight low-side trait
|
||||||
* Tarinai closer to the blast center should be thrown faster and farther.
|
-0.5 to +0.5: neutral, no visible personality tag
|
||||||
* Tarinai farther from the blast center should be thrown less strongly.
|
+0.5 to +0.75: slight high-side trait
|
||||||
* Preserve or improve existing explosion damage behavior.
|
+0.75 to +1.0: strong high-side trait
|
||||||
* If an explosion kills a Tarinai, keep the appropriate explosion-related death cause, not `衝突`.
|
```
|
||||||
|
|
||||||
Expected result:
|
Display tags:
|
||||||
|
|
||||||
* Normal walking speed remains bounded.
|
* Slight traits should be displayed as \u201cslightly X\u201d.
|
||||||
* Knockback from explosions and collisions can exceed walking speed.
|
* Strong traits should be displayed as \u201cX\u201d.
|
||||||
* High-speed objects do not frequently tunnel through collisions.
|
* Neutral values should not create a visible tag.
|
||||||
* Collision behavior is handled by shared primitives rather than several unrelated implementations.
|
|
||||||
|
|
||||||
---
|
Example:
|
||||||
|
|
||||||
## 5. Ball-to-ball collision
|
* aggression `+0.62` -> \u201cslightly irritable\u201d
|
||||||
|
* aggression `+0.83` -> \u201cirritable\u201d
|
||||||
|
* sociability `-0.58` -> \u201cslightly loner\u201d
|
||||||
|
* sociability `-0.82` -> \u201cloner\u201d
|
||||||
|
* openness `+0.20` -> no tag
|
||||||
|
|
||||||
Ensure balls collide with each other robustly.
|
Behavior activation:
|
||||||
|
|
||||||
Requirements:
|
* Slight traits affect relevant behavior with a 50% chance when choosing a behavior.
|
||||||
|
* Strong traits affect relevant behavior with a 100% chance when choosing a behavior.
|
||||||
|
* Do not check this every frame. Apply it only during behavior selection or when evaluating a relevant event.
|
||||||
|
|
||||||
* Ball vs ball collision must use circle-circle collision.
|
## Daily personality change limit
|
||||||
* Resolve overlap.
|
|
||||||
* Exchange or reflect velocity in a physically plausible way.
|
|
||||||
* Work with high-speed balls, not only overlapping balls.
|
|
||||||
* Avoid creating infinite acceleration or NaN velocity.
|
|
||||||
* Do not apply arbitrary maximum speed caps.
|
|
||||||
|
|
||||||
---
|
Add a daily change cap so personality values cannot change too quickly.
|
||||||
|
|
||||||
## 6. Solid obstacle and placement logic unification
|
Recommended limits:
|
||||||
|
|
||||||
Current issues:
|
```text
|
||||||
Fence, nest box, and placement preview logic are still partly fragmented.
|
Per parameter:
|
||||||
|
maximum increase per day: +0.08
|
||||||
|
maximum decrease per day: -0.08
|
||||||
|
|
||||||
Implement:
|
Across all parameters:
|
||||||
|
maximum total absolute personality change per day: 0.20
|
||||||
|
```
|
||||||
|
|
||||||
* Unify all solid rectangular obstacle collision logic.
|
If a change would exceed the daily limit, reduce or ignore the excess change.
|
||||||
* Fence and nest box should use the same obstacle system where possible.
|
|
||||||
* Rename misleading helpers if needed:
|
|
||||||
|
|
||||||
* For example, functions named only for “fence” but now handling nest boxes too should be renamed or wrapped with neutral names such as `solidObstacle`.
|
Reset these daily counters at the start of each new in-game day.
|
||||||
* Ball, Tarinai movement, path blocking, and placement checks should reference the same obstacle data.
|
|
||||||
|
|
||||||
Fence collision:
|
## Observation log rules
|
||||||
|
|
||||||
* Fence collision bounds must match the visible fence appearance.
|
Write personality change entries to the observation log only when a threshold is crossed.
|
||||||
* If the visual fence is not the same size as the previous hitbox, update the hitbox to match the visual footprint.
|
|
||||||
* Avoid separate visual size and collision size unless explicitly documented in one definition table.
|
|
||||||
|
|
||||||
Nest box collision:
|
Log when:
|
||||||
|
|
||||||
* Preserve the existing 3×3 nest box hitbox rule:
|
* a value crosses `+0.5`
|
||||||
|
* a value crosses `+0.75`
|
||||||
|
* a value crosses `-0.5`
|
||||||
|
* a value crosses `-0.75`
|
||||||
|
* a value returns from a visible trait range back into neutral
|
||||||
|
|
||||||
* If the nest box is divided into a 3×3 grid, the collidable cells are:
|
Do not log every small personality change.
|
||||||
|
|
||||||
* top-left
|
Example log messages:
|
||||||
* top-center
|
|
||||||
* top-right
|
|
||||||
* middle-left
|
|
||||||
* middle-right
|
|
||||||
* The middle-center, bottom-left, bottom-center, and bottom-right cells are passable/enterable according to the current nest behavior.
|
|
||||||
* Ball must bounce off these nest box solid cells.
|
|
||||||
* Tarinai must not path through these solid cells.
|
|
||||||
|
|
||||||
Placement preview:
|
```text
|
||||||
|
\u201cMochi\u201d has become slightly irritable after winning fights.
|
||||||
|
\u201cMimi\u201d has become playful after spending time with toys.
|
||||||
|
\u201cNoko\u201d has become less sensitive after repeated contact with corpses.
|
||||||
|
\u201cTari\u201d no longer seems especially social.
|
||||||
|
```
|
||||||
|
|
||||||
* The preview highlight must use the exact same placement validation as actual placement.
|
Use the existing observation log system if one exists.
|
||||||
* If the preview is red, clicking must not place the object.
|
|
||||||
* If the preview is green, clicking should place the object at the previewed position.
|
|
||||||
* Do not silently clamp or auto-correct an out-of-bounds placement to an in-bounds location.
|
|
||||||
* If the planned placement would extend outside the field, the highlight must turn red.
|
|
||||||
* Fix the issue where a fence preview extending outside the field does not turn red.
|
|
||||||
* The highlighted preview position should show the true placement footprint, not a different corrected footprint.
|
|
||||||
|
|
||||||
Expected result:
|
## Tarinai data UI
|
||||||
|
|
||||||
* Placement UI, actual placement, and collision behavior agree.
|
Update the tarinai data UI to show personality information clearly.
|
||||||
* No “red but placeable” or “green but placed elsewhere” behavior.
|
|
||||||
|
|
||||||
---
|
It should include:
|
||||||
|
|
||||||
## 7. Item/tool definition unification
|
1. Birth personality
|
||||||
|
2. Current personality
|
||||||
|
3. Visible personality tags derived from current personality
|
||||||
|
|
||||||
Current issues:
|
Example structure:
|
||||||
Tool names, labels, icons, sizes, placement rules, and descriptions are scattered across multiple files.
|
|
||||||
|
|
||||||
Implement:
|
```text
|
||||||
|
Personality Tags:
|
||||||
|
slightly irritable / social
|
||||||
|
|
||||||
* Create or consolidate a single source of truth for tool/item definitions.
|
Birth Personality:
|
||||||
* It should include at least:
|
Aggression: -0.12
|
||||||
|
Openness: +0.31
|
||||||
|
Sociability: +0.48
|
||||||
|
Neuroticism: +0.66
|
||||||
|
|
||||||
* internal id
|
Current Personality:
|
||||||
* Japanese label
|
Aggression: +0.57
|
||||||
* tooltip text
|
Openness: +0.34
|
||||||
* UI icon or icon renderer
|
Sociability: +0.76
|
||||||
* game appearance reference where applicable
|
Neuroticism: +0.22
|
||||||
* placement size / footprint
|
```
|
||||||
* whether it is placeable
|
|
||||||
* whether it is scaleable, if applicable
|
|
||||||
* collision shape if applicable
|
|
||||||
* Use this definition in the tool UI, placement preview, placement logic, and logs where appropriate.
|
|
||||||
* Do not leave conflicting duplicate label/icon/size definitions in separate files unless unavoidable.
|
|
||||||
|
|
||||||
Specific icon requirements:
|
Keep the UI compact. Do not create a large new panel unless necessary.
|
||||||
|
|
||||||
* `つつく` must use `👈`.
|
## Implementation constraints
|
||||||
* `つまむ` must use `🤏`.
|
|
||||||
* `洗浄` must use the shower emoji `🚿`.
|
|
||||||
* Remove or stop referencing old PNG icons for `つつく` and `つまむ`.
|
|
||||||
* Fix the nest box tool UI icon so it matches the actual in-game nest box appearance.
|
|
||||||
* Ensure every tool that appears in the tool UI has an icon.
|
|
||||||
* Avoid mojibake/garbled icon rendering.
|
|
||||||
|
|
||||||
Water tool:
|
* Keep the code simple and localized.
|
||||||
|
* Reuse existing behavior, logging, and UI systems where possible.
|
||||||
|
* Avoid adding unrelated features.
|
||||||
|
* Avoid adding complex new simulation systems.
|
||||||
|
* Preserve existing save data as much as possible.
|
||||||
|
* If old save data lacks personality fields, initialize them safely.
|
||||||
|
* Clamp all personality values to `-1.0` through `+1.0`.
|
||||||
|
* Add helper functions where useful, such as:
|
||||||
|
|
||||||
* `水` must remain removed from the tool UI.
|
* `ensurePersonality(tarinai)`
|
||||||
* If water still exists as a game object produced by rain or other simulation systems, keep that object behavior.
|
* `adjustPersonality(tarinai, key, delta, reason)`
|
||||||
* Remove leftover UI-only water tool entries, labels, tooltips, and placement handlers unless they are still required by non-UI simulation.
|
* `getPersonalityTraitTags(tarinai)`
|
||||||
|
* `getTraitStrength(value)`
|
||||||
|
* `shouldApplyPersonalityBehavior(tarinai, key, direction)`
|
||||||
|
|
||||||
Expected result:
|
## Validation
|
||||||
|
|
||||||
* Tool UI no longer has missing icons, old icons, or mismatched nest box visuals.
|
After implementation:
|
||||||
* Tool data is not scattered unnecessarily.
|
|
||||||
|
|
||||||
---
|
* Run syntax checks for all JavaScript files.
|
||||||
|
* Verify that old tarinai without personality data do not crash the game.
|
||||||
## 8. Observation UI event filtering
|
* Verify that personality values are clamped.
|
||||||
|
* Verify that daily limits work.
|
||||||
Current issue:
|
* Verify that threshold crossing creates observation logs.
|
||||||
Observation UI hides some events by matching Japanese text strings, which is fragile.
|
* Verify that minor personality changes do not spam logs.
|
||||||
|
* Verify that tarinai data UI shows birth personality, current personality, and visible tags.
|
||||||
Implement:
|
* Verify that children inherit from parents' current personality with \u00b10.2 random variation.
|
||||||
|
|
||||||
* Remove from the observation UI:
|
|
||||||
|
|
||||||
* events about poking a ball
|
|
||||||
* events of the form “X avoided Y” / `○○が××を避けた`
|
|
||||||
* Prefer event type/category filtering over string matching.
|
|
||||||
* If event objects do not have a type field, add one at event creation.
|
|
||||||
* Keep the events available internally if needed, but do not show them in the observation UI.
|
|
||||||
|
|
||||||
Expected result:
|
|
||||||
|
|
||||||
* Future wording changes do not break event filtering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Death cause normalization
|
|
||||||
|
|
||||||
Ensure these death causes are applied consistently:
|
|
||||||
|
|
||||||
* Death by old age / lifespan:
|
|
||||||
|
|
||||||
* `天寿を全うした`
|
|
||||||
* Tarinai deleted because it went too far outside the field:
|
|
||||||
|
|
||||||
* `仕様により画面外で削除`
|
|
||||||
* Death caused by ball collision or Tarinai collision damage:
|
|
||||||
|
|
||||||
* `衝突`
|
|
||||||
* Explosion death:
|
|
||||||
|
|
||||||
* Keep explosion-related cause.
|
|
||||||
* Existing disease-related deaths:
|
|
||||||
|
|
||||||
* Preserve current intended disease death causes unless they conflict with the above.
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
|
|
||||||
* Centralize death cause normalization if possible.
|
|
||||||
* Avoid scattered caller-side strings that can drift.
|
|
||||||
* Ensure the family tree, observation UI, and any memorial/death display show the normalized cause.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Out-of-bounds object policy
|
|
||||||
|
|
||||||
Current issue:
|
|
||||||
Different object types handle out-of-bounds behavior differently.
|
|
||||||
|
|
||||||
Implement a unified out-of-bounds policy:
|
|
||||||
|
|
||||||
* For objects or entities slightly outside the field:
|
|
||||||
|
|
||||||
* Return them to the nearest valid in-field position.
|
|
||||||
* For objects or entities far outside the field, or with invalid coordinates such as NaN/Infinity:
|
|
||||||
|
|
||||||
* Delete them.
|
|
||||||
* For Tarinai deleted by this system:
|
|
||||||
|
|
||||||
* Death cause must be `仕様により画面外で削除`.
|
|
||||||
* For non-living objects:
|
|
||||||
|
|
||||||
* Delete without a death cause.
|
|
||||||
* Keep wall bounce behavior for balls at normal field boundaries, but still sanitize balls that somehow get far outside or invalid coordinates.
|
|
||||||
|
|
||||||
Expected result:
|
|
||||||
|
|
||||||
* No permanent invisible offscreen objects.
|
|
||||||
* No NaN entities poisoning the simulation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Observation relationship highlight
|
|
||||||
|
|
||||||
Current issue:
|
|
||||||
Relationship highlight during observation can be affected by sprite rotation.
|
|
||||||
|
|
||||||
Implement:
|
|
||||||
|
|
||||||
* Relationship highlights must be drawn in world/screen space independent of the Tarinai sprite rotation.
|
|
||||||
* Do not draw the highlight inside a rotated sprite transform.
|
|
||||||
* Ensure highlight position follows the Tarinai but does not rotate with the Tarinai body.
|
|
||||||
|
|
||||||
Expected result:
|
|
||||||
|
|
||||||
* Highlight remains visually stable regardless of sprite rotation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Nest box behavior preservation
|
|
||||||
|
|
||||||
Preserve and verify previous nest box fixes:
|
|
||||||
|
|
||||||
* Tarinai inside the nest box should be fully transparent in the main field.
|
|
||||||
* Nest box can contain up to 5 Tarinai.
|
|
||||||
* Nest box occupant popup should show each individual on a separate line.
|
|
||||||
* The nest box solid hitbox must follow the 3×3 rule described above.
|
|
||||||
* Ball should bounce off nest box solid hitbox cells.
|
|
||||||
|
|
||||||
Do not regress these behaviors while refactoring obstacles.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Encyclopedia / ecology page
|
|
||||||
|
|
||||||
Preserve and verify the `たりないの生態` addition:
|
|
||||||
|
|
||||||
* It must include an explanation of health.
|
|
||||||
* It must include an explanation of stress.
|
|
||||||
* It must use a Tarinai sprite/image with both the health bar and stress bar visible.
|
|
||||||
* Do not break existing encyclopedia entries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Naming and Japanese text preservation
|
|
||||||
|
|
||||||
Preserve previous Japanese terminology changes:
|
|
||||||
|
|
||||||
* `喧嘩傷病` must remain renamed to `きずつき病`.
|
|
||||||
* `寿命` death text must display as `天寿を全うした`.
|
|
||||||
* Use the existing tone/style for Japanese labels and descriptions.
|
|
||||||
|
|
||||||
Do not reintroduce removed or old names.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Code quality expectations
|
|
||||||
|
|
||||||
Refactor carefully:
|
|
||||||
|
|
||||||
* Prefer small, well-named helper functions.
|
|
||||||
* Avoid adding more scattered special cases.
|
|
||||||
* Avoid duplicating collision math.
|
|
||||||
* Avoid duplicating item/tool metadata.
|
|
||||||
* Avoid string-matching for gameplay logic where structured data is possible.
|
|
||||||
* Preserve save compatibility where possible.
|
|
||||||
* If save migration is needed, add a safe fallback.
|
|
||||||
|
|
||||||
Add comments only where they clarify non-obvious game rules, especially:
|
|
||||||
|
|
||||||
* disease fight rules
|
|
||||||
* nest box 3×3 collision cells
|
|
||||||
* external impulse vs normal movement
|
|
||||||
* family tree dirty/update scheduling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 16. Validation checklist
|
|
||||||
|
|
||||||
Before returning the zip, verify at minimum:
|
|
||||||
|
|
||||||
Static checks:
|
|
||||||
|
|
||||||
* Run `node --check` on every JavaScript file.
|
|
||||||
* Ensure there are no `ReferenceError` risks from renamed helpers.
|
|
||||||
* Ensure old cache version references are updated.
|
|
||||||
|
|
||||||
Manual or scripted behavior checks where practical:
|
|
||||||
|
|
||||||
* Ball vs ball collision works.
|
|
||||||
* Fast ball hitting Tarinai knocks it by ball velocity / 5.
|
|
||||||
* Fast collision death cause is `衝突`.
|
|
||||||
* Explosion throws nearby Tarinai farther than distant Tarinai and is not clamped by walking speed.
|
|
||||||
* Fence preview turns red when the footprint leaves the field.
|
|
||||||
* Red placement preview cannot place an object.
|
|
||||||
* Green placement preview places at the same footprint shown.
|
|
||||||
* Nest box collision uses the required 3×3 cells.
|
|
||||||
* Ball bounces off nest box solid cells.
|
|
||||||
* Family tree hidden update is no more frequent than every 30 seconds.
|
|
||||||
* Family tree visible update happens when dirty, not every 5 seconds.
|
|
||||||
* Family tree processing does not freeze the game.
|
|
||||||
* `臆病` condition uses only total wins/losses.
|
|
||||||
* `爆発病` and `ずんち病` can initiate fights.
|
|
||||||
* Other diseases cannot initiate fights.
|
|
||||||
* `ねむり病` cannot initiate and cannot be targeted.
|
|
||||||
* Other non-sleep diseases can be targeted.
|
|
||||||
* Observation UI does not show ball-poke events or avoid events.
|
|
||||||
* Tool icons are correct: `👈`, `🤏`, `🚿`, and nest box matching in-game appearance.
|
|
||||||
* `水` is absent from the tool UI.
|
|
||||||
* Lifespan death cause is `天寿を全うした`.
|
|
||||||
* Far-outside Tarinai deletion cause is `仕様により画面外で削除`.
|
|
||||||
|
|
||||||
Deliverable:
|
|
||||||
|
|
||||||
* Return a zip named something like `tarinai_colony_game_fixed_v7.zip`.
|
|
||||||
* Include a concise summary of changed areas and validation results.
|
|
||||||
|
|
|
||||||
74
index.html
74
index.html
|
|
@ -7,7 +7,7 @@
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
<meta http-equiv="Expires" content="0" />
|
<meta http-equiv="Expires" content="0" />
|
||||||
<title>たりない観察</title>
|
<title>たりない観察</title>
|
||||||
<link rel="stylesheet" href="css/styles.css?v=15.5.0" />
|
<link rel="stylesheet" href="css/styles.css?v=15.6.2" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
<section id="selectedCard" class="card selected-card hidden">
|
<section id="selectedCard" class="card selected-card hidden">
|
||||||
<div class="selected-head">
|
<div class="selected-head">
|
||||||
<h2>選択中</h2>
|
<h2>たりないデータ</h2>
|
||||||
<button id="selectedCloseBtn" class="selected-close" type="button" aria-label="観察をやめる">×</button>
|
<button id="selectedCloseBtn" class="selected-close" type="button" aria-label="観察をやめる">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="selectedInfo" class="selected-info empty">未選択</div>
|
<div id="selectedInfo" class="selected-info empty">未選択</div>
|
||||||
|
|
@ -50,36 +50,36 @@
|
||||||
<section class="tool-category" data-tool-category="observe">
|
<section class="tool-category" data-tool-category="observe">
|
||||||
<button class="tool-category-toggle" type="button" aria-expanded="true">観察</button>
|
<button class="tool-category-toggle" type="button" aria-expanded="true">観察</button>
|
||||||
<div class="tool-grid tool-category-body">
|
<div class="tool-grid tool-category-body">
|
||||||
<button class="tool selected" data-tool="observe"><span>O</span>観察</button>
|
<button class="tool selected" data-tool="observe">観察</button>
|
||||||
<button class="tool" data-tool="delete"><span>D</span>削除</button>
|
<button class="tool" data-tool="delete">削除</button>
|
||||||
<button class="tool" data-tool="poke"><span>👈</span>つつく</button>
|
<button class="tool" data-tool="poke">つつく</button>
|
||||||
<button class="tool" data-tool="pinch"><span>🤏</span>つまむ</button>
|
<button class="tool" data-tool="pinch">つまむ</button>
|
||||||
<button class="tool" data-tool="new"><span>N</span>追加</button>
|
<button class="tool" data-tool="new">追加</button>
|
||||||
<button class="tool" data-tool="water_hose"><span>🚿</span>洗浄</button>
|
<button class="tool" data-tool="water_hose">洗浄</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="tool-category" data-tool-category="food">
|
<section class="tool-category" data-tool-category="food">
|
||||||
<button class="tool-category-toggle" type="button" aria-expanded="true">食べ物・自然</button>
|
<button class="tool-category-toggle" type="button" aria-expanded="true">食べ物・自然</button>
|
||||||
<div class="tool-grid tool-category-body">
|
<div class="tool-grid tool-category-body">
|
||||||
<button class="tool" data-tool="zunchi"><span>Z</span>ずんち</button>
|
<button class="tool" data-tool="zunchi">ずんち</button>
|
||||||
<button class="tool" data-tool="sweet"><span>S</span>ずんだ餅</button>
|
<button class="tool" data-tool="sweet">ずんだ餅</button>
|
||||||
<button class="tool" data-tool="love_mochi"><span>H</span>へこ餅</button>
|
<button class="tool" data-tool="love_mochi">へこ餅</button>
|
||||||
<button class="tool" data-tool="fight_mochi"><span>K</span>けんか餅</button>
|
<button class="tool" data-tool="fight_mochi">けんか餅</button>
|
||||||
<button class="tool" data-tool="water_bowl"><span>U</span>水の皿</button>
|
<button class="tool" data-tool="water_bowl">水の皿</button>
|
||||||
<button class="tool" data-tool="sleep_drug"><span>M</span>ねむり薬</button>
|
<button class="tool" data-tool="sleep_drug">ねむり薬</button>
|
||||||
<button class="tool" data-tool="grass"><span>G</span>草</button>
|
<button class="tool" data-tool="grass">草</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="tool-category" data-tool-category="object">
|
<section class="tool-category" data-tool-category="object">
|
||||||
<button class="tool-category-toggle" type="button" aria-expanded="true">オブジェクト</button>
|
<button class="tool-category-toggle" type="button" aria-expanded="true">オブジェクト</button>
|
||||||
<div class="tool-grid tool-category-body">
|
<div class="tool-grid tool-category-body">
|
||||||
<button class="tool" data-tool="stone"><span>R</span>石</button>
|
<button class="tool" data-tool="stone">石</button>
|
||||||
<button class="tool" data-tool="bed"><span>B</span>干草寝床</button>
|
<button class="tool" data-tool="bed">干草寝床</button>
|
||||||
<button class="tool" data-tool="nest_box"><span>X</span>巣箱</button>
|
<button class="tool" data-tool="nest_box">巣箱</button>
|
||||||
<button class="tool" data-tool="ball"><span>L</span>ボール</button>
|
<button class="tool" data-tool="ball">ボール</button>
|
||||||
<button class="tool" data-tool="firecracker"><span>F</span>爆竹</button>
|
<button class="tool" data-tool="firecracker">爆竹</button>
|
||||||
<button class="tool" data-tool="fence_v"><span>V</span>縦の柵</button>
|
<button class="tool" data-tool="fence_v">縦の柵</button>
|
||||||
<button class="tool" data-tool="fence_h"><span>H</span>横の柵</button>
|
<button class="tool" data-tool="fence_h">横の柵</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -165,20 +165,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/data.js?v=15.5.0"></script>
|
<script src="js/data.js?v=15.6.2"></script>
|
||||||
<script src="js/math.js?v=15.5.0"></script>
|
<script src="js/math.js?v=15.6.2"></script>
|
||||||
<script src="js/assets.js?v=15.5.0"></script>
|
<script src="js/assets.js?v=15.6.2"></script>
|
||||||
<script src="js/audio.js?v=15.5.0"></script>
|
<script src="js/audio.js?v=15.6.2"></script>
|
||||||
<script src="js/render.js?v=15.5.0"></script>
|
<script src="js/render.js?v=15.6.2"></script>
|
||||||
<script src="js/sim_core.js?v=15.5.0"></script>
|
<script src="js/sim_core.js?v=15.6.2"></script>
|
||||||
<script src="js/items.js?v=15.5.0"></script>
|
<script src="js/items.js?v=15.6.2"></script>
|
||||||
<script src="js/health.js?v=15.5.0"></script>
|
<script src="js/health.js?v=15.6.2"></script>
|
||||||
<script src="js/tarinai.js?v=15.5.0"></script>
|
<script src="js/tarinai.js?v=15.6.2"></script>
|
||||||
<script src="js/world.js?v=15.5.0"></script>
|
<script src="js/world.js?v=15.6.2"></script>
|
||||||
<script src="js/text_catalog.js?v=15.5.0"></script>
|
<script src="js/text_catalog.js?v=15.6.2"></script>
|
||||||
<script src="js/ui.js?v=15.5.0"></script>
|
<script src="js/ui.js?v=15.6.2"></script>
|
||||||
<script src="js/ui_charts.js?v=15.5.0"></script>
|
<script src="js/ui_charts.js?v=15.6.2"></script>
|
||||||
<script src="js/ui_family.js?v=15.5.0"></script>
|
<script src="js/ui_family.js?v=15.6.2"></script>
|
||||||
<script src="js/main.js?v=15.5.0"></script>
|
<script src="js/main.js?v=15.6.2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
63
js/data.js
63
js/data.js
|
|
@ -33,6 +33,68 @@ const DECOR_ASSETS = [
|
||||||
{ id: "zunchi", path: "assets/objects/zunchi.png" },
|
{ id: "zunchi", path: "assets/objects/zunchi.png" },
|
||||||
{ id: "zunchi_02", path: "assets/objects/zunchi_02.png" },
|
{ id: "zunchi_02", path: "assets/objects/zunchi_02.png" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TOOL_DEFINITIONS = Object.freeze({
|
||||||
|
observe: { id: "observe", label: "\u89b3\u5bdf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.png", 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.png", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\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: "\u305f\u308a\u306a\u3044\u3084\u7269\u3092\u3064\u307e\u3093\u3067\u79fb\u52d5\u3055\u305b\u308b\u3002" },
|
||||||
|
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.png", 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}", icon: "assets/ui/tool_water_hose.png", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\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.png", 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.png", 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.png", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" },
|
||||||
|
fight_mochi: { id: "fight_mochi", itemType: "fight_mochi", label: "\u3051\u3093\u304b\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_fight_mochi.png", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u3051\u3093\u304b\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" },
|
||||||
|
water_bowl: { id: "water_bowl", itemType: "water_bowl", label: "\u6c34\u306e\u76bf", placeable: true, scalable: false, radius: 20, amount: 999, icon: "assets/ui/tool_water_bowl.png", tooltip: "\u885b\u751f\u7684\u306a\u6c34\u304c\u5165\u3063\u305f\u76bf\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
|
||||||
|
sleep_drug: { id: "sleep_drug", itemType: "sleep_drug", label: "\u306d\u3080\u308a\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_sleep_drug.png", tooltip: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002" },
|
||||||
|
grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.png", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" },
|
||||||
|
stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.png", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" },
|
||||||
|
bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.png", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" },
|
||||||
|
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.png", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||||
|
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.png", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||||
|
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.png", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
|
||||||
|
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u7e26\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.png", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
|
||||||
|
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u6a2a\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.png", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
|
||||||
|
water: { id: "water", itemType: "water", label: "\u6c34", placeable: false, scalable: false, radius: 12, amount: 64, simulationOnly: true },
|
||||||
|
trace: { id: "trace", itemType: "trace", label: "\u8db3\u8de1", 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 },
|
||||||
|
});
|
||||||
|
|
||||||
|
function toolDefinition(id = "") {
|
||||||
|
return TOOL_DEFINITIONS[id] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolLabel(id = "") {
|
||||||
|
const def = toolDefinition(id);
|
||||||
|
return def?.label || id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolItemType(id = "") {
|
||||||
|
const def = toolDefinition(id);
|
||||||
|
return def?.placeable ? (def.itemType || def.id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemDefinition(type = "") {
|
||||||
|
return Object.values(TOOL_DEFINITIONS).find(def => (def.itemType || def.id) === type) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemRadiusFor(type = "", fallback = 12) {
|
||||||
|
return itemDefinition(type)?.radius ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemAmountFor(type = "", fallback = 80) {
|
||||||
|
return itemDefinition(type)?.amount ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scalableToolIds() {
|
||||||
|
return Object.values(TOOL_DEFINITIONS).filter(def => def.scalable).map(def => def.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolTipsFromDefinitions() {
|
||||||
|
return Object.fromEntries(Object.entries(TOOL_DEFINITIONS).filter(([, def]) => def.tooltip).map(([id, def]) => [id, def.tooltip]));
|
||||||
|
}
|
||||||
|
|
||||||
const ALPHA_BOUNDS = {
|
const ALPHA_BOUNDS = {
|
||||||
smile: { x: 18, y: 18, w: 476, h: 432, imageW: 512, imageH: 468 },
|
smile: { x: 18, y: 18, w: 476, h: 432, imageW: 512, imageH: 468 },
|
||||||
angry: { x: 16, y: 16, w: 480, h: 432, imageW: 512, imageH: 464 },
|
angry: { x: 16, y: 16, w: 480, h: 432, imageW: 512, imageH: 464 },
|
||||||
|
|
@ -106,4 +168,3 @@ const LIGHTING_TUNING = {
|
||||||
goldenOverlayAlpha: 0.045,
|
goldenOverlayAlpha: 0.045,
|
||||||
bloomWashAlpha: 0.040,
|
bloomWashAlpha: 0.040,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
17
js/health.js
17
js/health.js
|
|
@ -10,12 +10,12 @@ const HEALTH = (() => {
|
||||||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e\u305f",
|
poke: "\u3064\u3064\u304b\u308c\u3059\u304e\u305f",
|
||||||
firecracker: "\u7206\u7af9",
|
firecracker: "\u7206\u7af9",
|
||||||
accident: "\u4e8b\u6545",
|
accident: "\u4e8b\u6545",
|
||||||
collision: "衝突",
|
collision: "\u885d\u7a81",
|
||||||
outOfBounds: "仕様により画面外で削除",
|
outOfBounds: "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664",
|
||||||
lifespan: "天寿を全うした",
|
lifespan: "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f",
|
||||||
sleepDisease: "\u306d\u3080\u308a\u75c5",
|
sleepDisease: "\u306d\u3080\u308a\u75c5",
|
||||||
explosionDisease: "\u7206\u767a\u75c5",
|
explosionDisease: "\u7206\u767a\u75c5",
|
||||||
fightDisease: "きずつき病",
|
fightDisease: "\u304d\u305a\u3064\u304d\u75c5",
|
||||||
zunchiDisease: "\u305a\u3093\u3061\u75c5",
|
zunchiDisease: "\u305a\u3093\u3061\u75c5",
|
||||||
});
|
});
|
||||||
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
||||||
|
|
@ -26,12 +26,12 @@ const HEALTH = (() => {
|
||||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||||
if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight;
|
if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight;
|
||||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
||||||
if (/仕様により画面外で削除|画面外で削除|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds;
|
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 (/衝突|collision/.test(text)) return CAUSES.collision;
|
if (/\u885d\u7a81|collision/.test(text)) return CAUSES.collision;
|
||||||
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke;
|
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 (/\u306d\u3080\u308a\u75c5|sleep.*disease/.test(text)) return CAUSES.sleepDisease;
|
||||||
if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease;
|
if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease;
|
||||||
if (/\u3051\u3093\u304b\u75c5|喧嘩傷病|きずつき病|fight.*disease/.test(text)) return CAUSES.fightDisease;
|
if (/\u3051\u3093\u304b\u75c5|\u304d\u305a\u3064\u304d\u75c5|fight.*disease/.test(text)) return CAUSES.fightDisease;
|
||||||
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return CAUSES.zunchiDisease;
|
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return CAUSES.zunchiDisease;
|
||||||
if (/\u75c5/.test(text)) {
|
if (/\u75c5/.test(text)) {
|
||||||
const disease = text.match(DISEASE_RE);
|
const disease = text.match(DISEASE_RE);
|
||||||
|
|
@ -40,7 +40,7 @@ const HEALTH = (() => {
|
||||||
if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return CAUSES.hunger;
|
if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return CAUSES.hunger;
|
||||||
if (/\u30b9\u30c8\u30ec\u30b9|stress/.test(text)) return CAUSES.stress;
|
if (/\u30b9\u30c8\u30ec\u30b9|stress/.test(text)) return CAUSES.stress;
|
||||||
if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return CAUSES.accident;
|
if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return CAUSES.accident;
|
||||||
if (/\u5bff\u547d|天寿を全うした/.test(text)) return CAUSES.lifespan;
|
if (/\u5bff\u547d|\u5929\u5bff/.test(text)) return CAUSES.lifespan;
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +121,7 @@ const HEALTH = (() => {
|
||||||
cause = rememberDamage(t, actualLoss, reason);
|
cause = rememberDamage(t, actualLoss, reason);
|
||||||
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
||||||
if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");
|
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);
|
||||||
if (t.maybeBecomeTimidAfterDamage) t.maybeBecomeTimidAfterDamage(cause || reason);
|
if (t.maybeBecomeTimidAfterDamage) t.maybeBecomeTimidAfterDamage(cause || reason);
|
||||||
t.showHpBar();
|
t.showHpBar();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
js/items.js
53
js/items.js
|
|
@ -1,18 +1,23 @@
|
||||||
// tarinai_colony_game build 15.4.0: ball collisions, fence hitboxes, and archive throttling
|
// tarinai_colony_game build 15.6.2: food servings, nest box access, and editable tarinai data
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const FOOD_SERVING_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug"]);
|
||||||
|
const FOOD_SERVINGS_BY_SIZE = { small: 1, medium: 5, large: 15 };
|
||||||
|
function foodServingsForSize(size = "medium") {
|
||||||
|
return FOOD_SERVINGS_BY_SIZE[size] || FOOD_SERVINGS_BY_SIZE.medium;
|
||||||
|
}
|
||||||
|
function isServingFoodType(type = "") {
|
||||||
|
return FOOD_SERVING_TYPES.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
class Item {
|
class Item {
|
||||||
constructor(type, x, y) {
|
constructor(type, x, y) {
|
||||||
this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
|
this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.r = {
|
this.r = itemRadiusFor(type, 12);
|
||||||
food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, sleep_drug: 13, water: 12, water_bowl: 20, grass: 17, stone: 20, bed: 37, nest_box: 42, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
this.amount = itemAmountFor(type, 80);
|
||||||
}[type] || 12;
|
|
||||||
this.amount = {
|
|
||||||
food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, water: 64, water_bowl: 999, grass: 120, stone: 999, bed: 999, nest_box: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
|
||||||
}[type] || 80;
|
|
||||||
this.age = 0;
|
this.age = 0;
|
||||||
this.seed = Math.random() * 1000;
|
this.seed = Math.random() * 1000;
|
||||||
this.dropTimer = 0;
|
this.dropTimer = 0;
|
||||||
|
|
@ -57,6 +62,13 @@ class Item {
|
||||||
this.fuseTimer = 5;
|
this.fuseTimer = 5;
|
||||||
this.fuseMax = 5;
|
this.fuseMax = 5;
|
||||||
}
|
}
|
||||||
|
if (isServingFoodType(type)) {
|
||||||
|
this.toolSize = "medium";
|
||||||
|
this.foodServingScale = 1;
|
||||||
|
this.foodServingsMax = foodServingsForSize(this.toolSize);
|
||||||
|
this.foodServingsRemaining = this.foodServingsMax;
|
||||||
|
this.amount = this.foodServingsRemaining;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
update(dt, worldRef = world) {
|
update(dt, worldRef = world) {
|
||||||
this.age += dt;
|
this.age += dt;
|
||||||
|
|
@ -69,7 +81,7 @@ class Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.type === "water") this.amount -= dt * 0.9;
|
if (this.type === "water") this.amount -= dt * 0.9;
|
||||||
if (["sweet", "love_mochi", "fight_mochi", "sleep_drug"].includes(this.type)) this.amount -= dt * 0.12;
|
if (["sweet", "love_mochi", "fight_mochi", "sleep_drug"].includes(this.type) && !Number.isFinite(this.foodServingsRemaining)) this.amount -= dt * 0.12;
|
||||||
if (this.type === "trace") this.amount -= dt * 1.35;
|
if (this.type === "trace") this.amount -= dt * 1.35;
|
||||||
if (this.type === "splat") this.amount -= dt * 1.05;
|
if (this.type === "splat") this.amount -= dt * 1.05;
|
||||||
if (this.type === "firecracker") {
|
if (this.type === "firecracker") {
|
||||||
|
|
@ -445,7 +457,7 @@ class Item {
|
||||||
type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed,
|
type: this.type, x: this.x, y: this.y, amount: this.amount, age: this.age, seed: this.seed,
|
||||||
stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness,
|
stage: this.stage, stageTimer: this.stageTimer, fertility: this.fertility, freshness: this.freshness,
|
||||||
growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither,
|
growth: this.growth, health: this.health, seedTimer: this.seedTimer, eatenAmount: this.eatenAmount, fertilityBoost: this.fertilityBoost, lifeSpan: this.lifeSpan, wither: this.wither,
|
||||||
comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax,
|
comfort: this.comfort, wear: this.wear, crawlOpen: this.crawlOpen, zunchiVariant: this.zunchiVariant, toolSize: this.toolSize, foodServingScale: this.foodServingScale, foodServingsMax: this.foodServingsMax, foodServingsRemaining: this.foodServingsRemaining, blastScale: this.blastScale, fuseTimer: this.fuseTimer, fuseMax: this.fuseMax, dropTimer: this.dropTimer, dropMax: this.dropMax,
|
||||||
vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY,
|
vx: this.vx, vy: this.vy, prevX: this.prevX, prevY: this.prevY,
|
||||||
spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId,
|
spin: this.spin, spinVelocity: this.spinVelocity, lastKickedAt: this.lastKickedAt, lastKickerId: this.lastKickerId,
|
||||||
lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle
|
lastPokedAt: this.lastPokedAt, pokeCombo: this.pokeCombo, lastPokeAngle: this.lastPokeAngle
|
||||||
|
|
@ -459,12 +471,8 @@ class Item {
|
||||||
it.type = type;
|
it.type = type;
|
||||||
it.x = o.x;
|
it.x = o.x;
|
||||||
it.y = o.y;
|
it.y = o.y;
|
||||||
it.r = {
|
it.r = itemRadiusFor(type, 12);
|
||||||
food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, sleep_drug: 13, water: 12, water_bowl: 20, grass: 17, stone: 20, bed: 37, nest_box: 42, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, trace: 16, splat: 26, zunchi: 14
|
it.amount = o.amount ?? itemAmountFor(type, 80);
|
||||||
}[type] || 12;
|
|
||||||
it.amount = o.amount ?? ({
|
|
||||||
food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, water: 64, water_bowl: 999, grass: 120, stone: 999, bed: 999, nest_box: 999, ball: 999, firecracker: 999, fence_v: 999, fence_h: 999, trace: 220, splat: 260, zunchi: 240
|
|
||||||
}[type] || 80);
|
|
||||||
it.age = o.age || 0;
|
it.age = o.age || 0;
|
||||||
it.seed = o.seed || Math.random() * 1000;
|
it.seed = o.seed || Math.random() * 1000;
|
||||||
it.dropTimer = 0;
|
it.dropTimer = 0;
|
||||||
|
|
@ -534,6 +542,17 @@ class Item {
|
||||||
it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2);
|
it.lastPokeAngle = o.lastPokeAngle ?? it.lastPokeAngle ?? rand(0, Math.PI * 2);
|
||||||
it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi";
|
it.zunchiVariant = o.zunchiVariant || it.zunchiVariant || "zunchi";
|
||||||
it.toolSize = o.toolSize || it.toolSize || "medium";
|
it.toolSize = o.toolSize || it.toolSize || "medium";
|
||||||
|
it.foodServingScale = o.foodServingScale || it.foodServingScale || ({ small: 0.68, medium: 1.0, large: 1.48 }[it.toolSize] || 1);
|
||||||
|
if (isServingFoodType(it.type)) {
|
||||||
|
const servingMax = foodServingsForSize(it.toolSize);
|
||||||
|
it.foodServingsMax = Number.isFinite(o.foodServingsMax) ? o.foodServingsMax : servingMax;
|
||||||
|
if (Number.isFinite(o.foodServingsRemaining)) {
|
||||||
|
it.foodServingsRemaining = clamp(o.foodServingsRemaining, 0, it.foodServingsMax);
|
||||||
|
} else {
|
||||||
|
it.foodServingsRemaining = Math.max(1, Math.min(it.foodServingsMax, Math.ceil(it.foodServingsMax * clamp((o.amount ?? it.amount) / 70, 0.05, 1))));
|
||||||
|
}
|
||||||
|
it.amount = it.foodServingsRemaining;
|
||||||
|
}
|
||||||
it.blastScale = o.blastScale || it.blastScale || 1;
|
it.blastScale = o.blastScale || it.blastScale || 1;
|
||||||
it.fuseTimer = o.fuseTimer ?? it.fuseTimer;
|
it.fuseTimer = o.fuseTimer ?? it.fuseTimer;
|
||||||
it.fuseMax = o.fuseMax ?? it.fuseMax;
|
it.fuseMax = o.fuseMax ?? it.fuseMax;
|
||||||
|
|
@ -548,7 +567,8 @@ class Item {
|
||||||
const warm = lightState.warmth;
|
const warm = lightState.warmth;
|
||||||
const styleNight = lightState.light < 0.42;
|
const styleNight = lightState.light < 0.42;
|
||||||
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
||||||
const visibleAlpha = clamp(this.amount / 40, 0.24, 1);
|
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
||||||
|
const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1);
|
||||||
const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0;
|
const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0;
|
||||||
const dropY = dropT > 0 ? -170 * Math.pow(dropT, 0.42) : 0;
|
const dropY = dropT > 0 ? -170 * Math.pow(dropT, 0.42) : 0;
|
||||||
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
|
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
|
||||||
|
|
@ -869,4 +889,3 @@ class Item {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
53
js/render.js
53
js/render.js
|
|
@ -124,51 +124,30 @@ function drawItemGlow(ctx, item, lighting, visibleAlpha = 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLACEABLE_TOOL_TYPES = Object.freeze({
|
|
||||||
zunchi: "zunchi", sweet: "sweet", love_mochi: "love_mochi", fight_mochi: "fight_mochi", sleep_drug: "sleep_drug", water: "water", water_bowl: "water_bowl", grass: "grass",
|
|
||||||
stone: "stone", bed: "bed", nest_box: "nest_box", ball: "ball", firecracker: "firecracker", fence_v: "fence_v", fence_h: "fence_h",
|
|
||||||
});
|
|
||||||
|
|
||||||
function placementPreviewFor(world) {
|
function placementPreviewFor(world) {
|
||||||
const p = world?.pointer;
|
const p = world?.pointer;
|
||||||
const type = PLACEABLE_TOOL_TYPES[world?.tool || ""];
|
const type = toolItemType(world?.tool || "");
|
||||||
if (!p?.inside || !type) return null;
|
if (!p?.inside || !type) return null;
|
||||||
const pad = CONFIG.worldPadding || 30;
|
const pad = CONFIG.worldPadding || 30;
|
||||||
let x = clamp(p.x, pad, world.w - pad);
|
let x = p.x;
|
||||||
let y = clamp(p.y, pad, world.h - pad);
|
let y = p.y;
|
||||||
const r = {
|
const r = itemRadiusFor(type, 12);
|
||||||
food: 14, sweet: 13, love_mochi: 13, fight_mochi: 13, sleep_drug: 13, water: 12, water_bowl: 20, grass: 17, stone: 20, bed: 37, nest_box: 42, ball: 18, firecracker: 15, fence_v: 42, fence_h: 42, zunchi: 14
|
|
||||||
}[type] || 12;
|
|
||||||
const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad));
|
const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad));
|
||||||
const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad;
|
const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad;
|
||||||
let blocked = false;
|
let blocked = false;
|
||||||
if (type === "grass" && world.findGrassPlantingSpot) {
|
let tmp = { type, x, y, r, dead: false };
|
||||||
const spot = world.findGrassPlantingSpot(x, y, { allowOriginal: true, minRadius: 22, maxRadius: 128, attempts: 16 });
|
|
||||||
if (!spot) return { type, x, y, r, blocked: true };
|
|
||||||
x = spot.x;
|
|
||||||
y = spot.y;
|
|
||||||
blocked = circleOutside(x, y, Math.max(14, r * 1.55));
|
|
||||||
}
|
|
||||||
if ((type === "fence_v" || type === "fence_h") && world.fenceRect) {
|
if ((type === "fence_v" || type === "fence_h") && world.fenceRect) {
|
||||||
const raw = { type, x, y, r };
|
const rawRect = world.fenceRect(tmp);
|
||||||
const rawRect = world.fenceRect(raw);
|
return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) || world.fencePlacementBlocked?.(tmp) };
|
||||||
blocked = rectOutside(rawRect);
|
|
||||||
const tmp = { type, x, y, r };
|
|
||||||
const rect = world.fenceRect(tmp);
|
|
||||||
if (rect) {
|
|
||||||
const dx = Math.max(pad - rect.left, 0) - Math.max(rect.right - (world.w - pad), 0);
|
|
||||||
const dy = Math.max(pad - rect.top, 0) - Math.max(rect.bottom - (world.h - pad), 0);
|
|
||||||
tmp.x += dx;
|
|
||||||
tmp.y += dy;
|
|
||||||
return { type, x: tmp.x, y: tmp.y, r, rect: world.fenceRect(tmp), blocked: blocked || world.fencePlacementBlocked?.(raw) || world.fencePlacementBlocked?.(tmp) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (type === "nest_box" && world.nestBoxBaseRect) {
|
if (type === "nest_box" && world.nestBoxBaseRect) {
|
||||||
const tmp = { type, x, y, r, dead: false };
|
|
||||||
const rect = world.nestBoxBaseRect(tmp);
|
const rect = world.nestBoxBaseRect(tmp);
|
||||||
return { type, x, y, r, rect, blocked: rectOutside(rect) };
|
return { type, x, y, r, rect, blocked: rectOutside(rect) || world.placementBlocked?.(tmp) };
|
||||||
}
|
}
|
||||||
if (!blocked) blocked = circleOutside(x, y, Math.max(14, r * (type === "bed" ? 1.55 : 1.25)));
|
if (type === "grass") {
|
||||||
|
blocked = !world.grassSpotOpen?.(x, y, { avoidTarinai: true }) || circleOutside(x, y, Math.max(14, r * 1.55));
|
||||||
|
}
|
||||||
|
if (!blocked) blocked = circleOutside(x, y, Math.max(14, r * (type === "bed" ? 1.55 : 1.25))) || world.placementBlocked?.(tmp);
|
||||||
return { type, x, y, r, blocked };
|
return { type, x, y, r, blocked };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -773,7 +752,8 @@ function drawNestBoxTooltip(ctx, world) {
|
||||||
const info = world.pointerNestBoxInfo?.();
|
const info = world.pointerNestBoxInfo?.();
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
const box = info.box;
|
const box = info.box;
|
||||||
const title = "\u5de3\u7bb1";
|
const capacity = info.capacity || world.nestBoxCapacity?.(box) || 5;
|
||||||
|
const title = `\u5de3\u7bb1 (${info.occupants.length}/${capacity})\u5339`;
|
||||||
const lines = info.occupants.length
|
const lines = info.occupants.length
|
||||||
? [title, ...info.occupants.map(t => t.name)]
|
? [title, ...info.occupants.map(t => t.name)]
|
||||||
: [title, "\u4e2d\u306b\u306f\u8ab0\u3082\u3044\u306a\u3044"];
|
: [title, "\u4e2d\u306b\u306f\u8ab0\u3082\u3044\u306a\u3044"];
|
||||||
|
|
@ -823,10 +803,9 @@ function drawSelectedCard(ctx, world, lighting) {
|
||||||
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
|
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
|
||||||
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
|
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
|
||||||
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
|
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
|
||||||
const personality = t.personalityLabel ? t.personalityLabel() : (t.personality || "\u4e0d\u660e");
|
|
||||||
ctx.fillText(`\u6027\u683c ${personality}`, x + 10, y + 52);
|
|
||||||
const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : "";
|
const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : "";
|
||||||
ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 68);
|
ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}`, x + 10, y + 52);
|
||||||
|
ctx.fillText(`\u30b9\u30c8\u30ec\u30b9 ${fmt(t.stress)} \u7a7a\u8179 ${fmt(t.hunger)}`, x + 10, y + 68);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
193
js/sim_core.js
193
js/sim_core.js
|
|
@ -64,19 +64,189 @@ function stableUnit(seed, salt = "") {
|
||||||
return ((h >>> 0) % 100000) / 100000;
|
return ((h >>> 0) % 100000) / 100000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PERSONALITIES = {
|
// Legacy named personalities were removed. Keep these stubs only so old save data
|
||||||
timid: { label: "\u81c6\u75c5", desc: "\u9003\u3052\u3084\u3059\u304f\u3001\u55a7\u5629\u3092\u907f\u3051\u308b", fear: 1.42, fight: 0.48, social: 0.96, relation: 1.04, sleep: 1.00, zunda: 1.00, play: 0.78 },
|
// and old UI fallback references cannot revive the pre-four-axis personality model.
|
||||||
glutton: { label: "\u98df\u3044\u3057\u3093\u574a", desc: "\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3057\u3084\u3059\u3044", fear: 0.92, fight: 0.82, social: 1.00, relation: 1.00, sleep: 0.95, zunda: 1.42, play: 0.84 },
|
const PERSONALITIES = Object.freeze({});
|
||||||
sleepy: { label: "\u7720\u305f\u304c\u308a", desc: "\u65e9\u3081\u306b\u5bdd\u5e8a\u3078\u5411\u304b\u3046", fear: 0.96, fight: 0.72, social: 0.92, relation: 0.96, sleep: 1.34, zunda: 0.92, play: 0.68 },
|
const PERSONALITY_IDS = [];
|
||||||
lonely: { label: "\u5bc2\u3057\u304c\u308a", desc: "\u4ed6\u306e\u305f\u308a\u306a\u3044\u3078\u5bc4\u308a\u3084\u3059\u3044", fear: 1.02, fight: 0.70, social: 1.34, relation: 1.38, sleep: 0.96, zunda: 1.00, play: 1.08 },
|
|
||||||
irritable: { label: "\u6012\u308a\u3063\u307d\u3044", desc: "\u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044", fear: 0.82, fight: 1.82, social: 0.78, relation: 0.78, sleep: 0.90, zunda: 0.94, play: 0.82 },
|
|
||||||
playful: { label: "\u904a\u3073\u597d\u304d", desc: "\u30dc\u30fc\u30eb\u3084\u52d5\u304f\u3082\u306e\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3084\u3059\u3044", fear: 0.92, fight: 0.74, social: 1.10, relation: 1.04, sleep: 0.94, zunda: 0.96, play: 1.72 },
|
|
||||||
calm: { label: "\u30de\u30a4\u30da\u30fc\u30b9", desc: "\u53cd\u5fdc\u304c\u7a4f\u3084\u304b\u3067\u5b89\u5b9a\u3057\u3084\u3059\u3044", fear: 0.82, fight: 0.58, social: 1.02, relation: 1.08, sleep: 1.02, zunda: 0.94, play: 0.86 },
|
|
||||||
};
|
|
||||||
const PERSONALITY_IDS = Object.keys(PERSONALITIES);
|
|
||||||
|
|
||||||
function personalityForSeed(seed) {
|
function personalityForSeed(seed) {
|
||||||
return stableChoice(seed, "personality", PERSONALITY_IDS) || "calm";
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERSONALITY_KEYS = ["aggression", "openness", "sociability", "neuroticism"];
|
||||||
|
const PERSONALITY_DAILY_LIMIT = { perAxis: 0.08, total: 0.20 };
|
||||||
|
const FRIEND_AFFINITY_THRESHOLD = 16;
|
||||||
|
const PERSONALITY_LABELS = {
|
||||||
|
aggression: { label: "\u653b\u6483\u6027", low: "\u304a\u3060\u3084\u304b", high: "\u6012\u308a\u3063\u307d\u3044" },
|
||||||
|
openness: { label: "\u958b\u653e\u6027", low: "\u4fdd\u5b88\u7684", high: "\u904a\u3073\u597d\u304d" },
|
||||||
|
sociability: { label: "\u793e\u4ea4\u6027", low: "\u4e00\u4eba\u597d\u304d", high: "\u4ef2\u9593\u597d\u304d" },
|
||||||
|
neuroticism: { label: "\u795e\u7d4c\u8cea\u6027", low: "\u7121\u9813\u7740", high: "\u7e4a\u7d30" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function clampPersonalityValue(value) {
|
||||||
|
return clamp(Number(value) || 0, -1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePersonality(source = null) {
|
||||||
|
const out = {};
|
||||||
|
for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(source?.[key]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomBirthPersonality(seed = "") {
|
||||||
|
const out = {};
|
||||||
|
for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(stableUnit(seed, `personality-${key}`) * 1.2 - 0.6);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityFromParents(a, b) {
|
||||||
|
const pa = ensurePersonality(a).currentPersonality;
|
||||||
|
const pb = ensurePersonality(b).currentPersonality;
|
||||||
|
const out = {};
|
||||||
|
for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(((pa[key] || 0) + (pb[key] || 0)) / 2 + rand(-0.2, 0.2));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTraitStrength(value) {
|
||||||
|
const v = clampPersonalityValue(value);
|
||||||
|
if (v >= 0.75) return { direction: 1, strength: "strong" };
|
||||||
|
if (v >= 0.5) return { direction: 1, strength: "slight" };
|
||||||
|
if (v <= -0.75) return { direction: -1, strength: "strong" };
|
||||||
|
if (v <= -0.5) return { direction: -1, strength: "slight" };
|
||||||
|
return { direction: 0, strength: "neutral" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityBand(value) {
|
||||||
|
const t = getTraitStrength(value);
|
||||||
|
return t.direction === 0 ? "neutral" : `${t.direction > 0 ? "high" : "low"}-${t.strength}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityEffectValue(value) {
|
||||||
|
const trait = getTraitStrength(value);
|
||||||
|
if (!trait.direction) return 0;
|
||||||
|
return trait.direction * (trait.strength === "strong" ? 1 : 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityReasonText(reason = "") {
|
||||||
|
const key = String(reason || "").replace(/[.\u3002]+$/g, "").trim();
|
||||||
|
const map = {
|
||||||
|
"after winning fights": "\u55a7\u5629\u306b\u52dd\u3063\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after losing fights": "\u55a7\u5629\u306b\u8ca0\u3051\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after being petted": "\u64ab\u3067\u3089\u308c\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after staying near family": "\u89aa\u5b50\u306e\u8fd1\u304f\u3067\u904e\u3054\u3057\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after staying near friends": "\u53cb\u9054\u306e\u8fd1\u304f\u3067\u904e\u3054\u3057\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after having no friends": "\u53cb\u9054\u304c\u3044\u306a\u3044\u6642\u9593\u304c\u7d9a\u3044\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after repeated contact with poop": "\u305a\u3093\u3061\u306b\u6163\u308c\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after repeated contact with corpses": "\u6b7b\u9ab8\u306b\u6163\u308c\u305f\u5f71\u97ff\u3067",
|
||||||
|
"after being hurt": "\u75db\u307f\u3092\u899a\u3048\u305f\u5f71\u97ff\u3067",
|
||||||
|
};
|
||||||
|
return map[key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityBecomeText(label = "") {
|
||||||
|
const text = String(label || "");
|
||||||
|
if (text === "\u6012\u308a\u3063\u307d\u3044") return "\u6012\u308a\u3063\u307d\u304f";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityAppearanceText(label = "") {
|
||||||
|
const text = String(label || "");
|
||||||
|
if (text === "\u6012\u308a\u3063\u307d\u3044") return "\u6012\u308a\u3063\u307d\u3044\u69d8\u5b50";
|
||||||
|
return `${text}\u306a\u69d8\u5b50`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalityTraitTags(tarinai) {
|
||||||
|
const p = ensurePersonality(tarinai).currentPersonality;
|
||||||
|
const tags = [];
|
||||||
|
for (const key of PERSONALITY_KEYS) {
|
||||||
|
const trait = getTraitStrength(p[key]);
|
||||||
|
if (!trait.direction) continue;
|
||||||
|
const label = PERSONALITY_LABELS[key]?.[trait.direction > 0 ? "high" : "low"] || key;
|
||||||
|
tags.push(trait.strength === "slight" ? `\u5c11\u3057${label}` : label);
|
||||||
|
}
|
||||||
|
const cowardly = (p.neuroticism || 0) >= 0.75 && (p.aggression || 0) <= -0.5;
|
||||||
|
if (cowardly && !tags.includes("\u81c6\u75c5")) tags.push("\u81c6\u75c5");
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityDailyState(tarinai) {
|
||||||
|
const day = tarinai?.world?.day || 1;
|
||||||
|
const state = tarinai.personalityDaily || {};
|
||||||
|
if (state.day !== day) {
|
||||||
|
tarinai.personalityDaily = { day, total: 0, byKey: {} };
|
||||||
|
return tarinai.personalityDaily;
|
||||||
|
}
|
||||||
|
state.byKey = state.byKey || {};
|
||||||
|
for (const key of Object.keys(state.byKey)) {
|
||||||
|
const v = state.byKey[key];
|
||||||
|
if (typeof v === "number") state.byKey[key] = { up: Math.max(0, v), down: Math.max(0, -v) };
|
||||||
|
else state.byKey[key] = { up: Math.max(0, Number(v?.up) || 0), down: Math.max(0, Number(v?.down) || 0) };
|
||||||
|
}
|
||||||
|
state.total = Math.max(0, Number(state.total) || 0);
|
||||||
|
tarinai.personalityDaily = state;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePersonality(tarinai) {
|
||||||
|
if (!tarinai) return { birthPersonality: normalizePersonality(), currentPersonality: normalizePersonality() };
|
||||||
|
if (!tarinai.birthPersonality) tarinai.birthPersonality = randomBirthPersonality(tarinai.id || Math.random());
|
||||||
|
tarinai.birthPersonality = normalizePersonality(tarinai.birthPersonality);
|
||||||
|
tarinai.currentPersonality = normalizePersonality(tarinai.currentPersonality || tarinai.birthPersonality);
|
||||||
|
personalityDailyState(tarinai);
|
||||||
|
return { birthPersonality: tarinai.birthPersonality, currentPersonality: tarinai.currentPersonality };
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityThresholdLogText(tarinai, key, value, reason = "", previousBand = "") {
|
||||||
|
const trait = getTraitStrength(value);
|
||||||
|
const name = tarinai?.name || "\u305f\u308a\u306a\u3044";
|
||||||
|
if (!trait.direction) {
|
||||||
|
const oldSide = String(previousBand).startsWith("low") ? "low" : "high";
|
||||||
|
const oldLabel = PERSONALITY_LABELS[key]?.[oldSide] || key;
|
||||||
|
return `\u300c${name}\u300d\u306f${personalityAppearanceText(oldLabel)}\u304c\u76ee\u7acb\u305f\u306a\u304f\u306a\u3063\u305f\u3002`;
|
||||||
|
}
|
||||||
|
const label = PERSONALITY_LABELS[key]?.[trait.direction > 0 ? "high" : "low"] || key;
|
||||||
|
const visible = trait.strength === "slight" ? `\u5c11\u3057${personalityBecomeText(label)}` : personalityBecomeText(label);
|
||||||
|
const prefix = personalityReasonText(reason);
|
||||||
|
return `\u300c${name}\u300d\u306f${prefix ? `${prefix}\u3001` : ""}${visible}\u306a\u3063\u305f\u3002`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustPersonality(tarinai, key, delta, reason = "") {
|
||||||
|
if (!tarinai || !PERSONALITY_KEYS.includes(key)) return 0;
|
||||||
|
ensurePersonality(tarinai);
|
||||||
|
const requested = Number(delta) || 0;
|
||||||
|
if (!requested) return 0;
|
||||||
|
const state = personalityDailyState(tarinai);
|
||||||
|
const dir = Math.sign(requested);
|
||||||
|
const used = state.byKey[key] || { up: 0, down: 0 };
|
||||||
|
const dirKey = dir > 0 ? "up" : "down";
|
||||||
|
const keyRoom = Math.max(0, PERSONALITY_DAILY_LIMIT.perAxis - (used[dirKey] || 0));
|
||||||
|
const totalRoom = Math.max(0, PERSONALITY_DAILY_LIMIT.total - (state.total || 0));
|
||||||
|
const allowed = Math.min(Math.abs(requested), keyRoom, totalRoom);
|
||||||
|
if (allowed <= 0) return 0;
|
||||||
|
const before = tarinai.currentPersonality[key] || 0;
|
||||||
|
const beforeBand = personalityBand(before);
|
||||||
|
const applied = dir * allowed;
|
||||||
|
const after = clampPersonalityValue(before + applied);
|
||||||
|
const actual = after - before;
|
||||||
|
if (!actual) return 0;
|
||||||
|
tarinai.currentPersonality[key] = after;
|
||||||
|
state.byKey[key] = used;
|
||||||
|
used[actual > 0 ? "up" : "down"] = (used[actual > 0 ? "up" : "down"] || 0) + Math.abs(actual);
|
||||||
|
state.total = (state.total || 0) + Math.abs(actual);
|
||||||
|
const afterBand = personalityBand(after);
|
||||||
|
if (beforeBand !== afterBand) {
|
||||||
|
const text = personalityThresholdLogText(tarinai, key, after, reason, beforeBand);
|
||||||
|
tarinai.world?.log?.(text, "observe");
|
||||||
|
tarinai.addRecord?.(text, "observe");
|
||||||
|
}
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldApplyPersonalityBehavior(tarinai, key, direction = 1) {
|
||||||
|
const p = ensurePersonality(tarinai).currentPersonality;
|
||||||
|
const trait = getTraitStrength(p[key]);
|
||||||
|
if (trait.direction !== Math.sign(direction || 1)) return false;
|
||||||
|
return trait.strength === "strong" || Math.random() < 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
function relationDefaults() {
|
function relationDefaults() {
|
||||||
|
|
@ -321,4 +491,3 @@ function drawHeartShape(ctx, x, y, size, opts = {}) {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
471
js/tarinai.js
471
js/tarinai.js
|
|
@ -19,19 +19,23 @@ class Tarinai {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.id = opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
|
this.id = opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
|
||||||
this.name = opts.name || makeName(world);
|
this.name = opts.name || makeName(world);
|
||||||
this.personality = opts.personality || personalityForSeed(this.id);
|
// Legacy string personality is intentionally ignored. The game now uses only the four-axis personality model.
|
||||||
|
this.personality = "";
|
||||||
this.type = opts.type || baseSpriteId();
|
this.type = opts.type || baseSpriteId();
|
||||||
const typeMeta = SPRITES.find(s => s.id === this.type);
|
const typeMeta = SPRITES.find(s => s.id === this.type);
|
||||||
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId();
|
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId();
|
||||||
|
this.birthPersonality = normalizePersonality(opts.birthPersonality || opts.personalityBirth);
|
||||||
|
if (!opts.birthPersonality && !opts.personalityBirth) this.birthPersonality = randomBirthPersonality(this.id);
|
||||||
|
this.currentPersonality = normalizePersonality(opts.currentPersonality || opts.personalityCurrent || this.birthPersonality);
|
||||||
|
this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null;
|
||||||
|
ensurePersonality(this);
|
||||||
this.trait = makeTrait(this.type);
|
this.trait = makeTrait(this.type);
|
||||||
const pdef = PERSONALITIES[this.personality] || PERSONALITIES.calm;
|
|
||||||
this.trait.social *= pdef.social ?? 1;
|
|
||||||
this.trait.stress /= pdef.fear ?? 1;
|
|
||||||
this.trait.sleep *= pdef.sleep ?? 1;
|
|
||||||
this.x = opts.x ?? rand(100, world.w - 100);
|
this.x = opts.x ?? rand(100, world.w - 100);
|
||||||
this.y = opts.y ?? rand(100, world.h - 100);
|
this.y = opts.y ?? rand(100, world.h - 100);
|
||||||
this.vx = opts.vx ?? rand(-12, 12);
|
this.vx = opts.vx ?? rand(-12, 12);
|
||||||
this.vy = opts.vy ?? rand(-12, 12);
|
this.vy = opts.vy ?? rand(-12, 12);
|
||||||
|
this.impulseVx = opts.impulseVx ?? 0;
|
||||||
|
this.impulseVy = opts.impulseVy ?? 0;
|
||||||
this.scale = opts.scale ?? rand(0.22, 0.33);
|
this.scale = opts.scale ?? rand(0.22, 0.33);
|
||||||
this.radius = 56 * this.scale;
|
this.radius = 56 * this.scale;
|
||||||
this.age = opts.age ?? rand(0, 20);
|
this.age = opts.age ?? rand(0, 20);
|
||||||
|
|
@ -109,6 +113,11 @@ class Tarinai {
|
||||||
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
|
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
|
||||||
this.lastBleedAt = opts.lastBleedAt ?? -999;
|
this.lastBleedAt = opts.lastBleedAt ?? -999;
|
||||||
this.favorite = Boolean(opts.favorite);
|
this.favorite = Boolean(opts.favorite);
|
||||||
|
this.insideNestBoxId = opts.insideNestBoxId || null;
|
||||||
|
this.nestFade = opts.nestFade ?? (this.insideNestBoxId ? 1 : 0);
|
||||||
|
this.nestBoxEnteredAt = opts.nestBoxEnteredAt ?? (this.insideNestBoxId ? (world?.time || 0) : -Infinity);
|
||||||
|
this.nestBoxStayUntil = opts.nestBoxStayUntil ?? (this.insideNestBoxId ? ((world?.time || 0) + 10) : -Infinity);
|
||||||
|
this.focusPulseTimer = opts.focusPulseTimer ?? 0;
|
||||||
if (this.isZunchiSlave) this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
|
if (this.isZunchiSlave) this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
|
||||||
this.tempComfort = opts.tempComfort ?? 0.62;
|
this.tempComfort = opts.tempComfort ?? 0.62;
|
||||||
this.tempTimer = opts.tempTimer ?? stableUnit(this.id, "temp-start") * 0.55;
|
this.tempTimer = opts.tempTimer ?? stableUnit(this.id, "temp-start") * 0.55;
|
||||||
|
|
@ -167,7 +176,7 @@ class Tarinai {
|
||||||
birthRitualRole: this.birthRitualRole, birthRitualLeader: this.birthRitualLeader,
|
birthRitualRole: this.birthRitualRole, birthRitualLeader: this.birthRitualLeader,
|
||||||
pokeFlashTimer: this.pokeFlashTimer, zunchiStain: this.zunchiStain, zunchiStainSeed: this.zunchiStainSeed,
|
pokeFlashTimer: this.pokeFlashTimer, zunchiStain: this.zunchiStain, zunchiStainSeed: this.zunchiStainSeed,
|
||||||
totalFightLosses: this.totalFightLosses, totalFightWins: this.totalFightWins, isZunchiSlave: this.isZunchiSlave, formerName: this.formerName, loveMochiTimer: this.loveMochiTimer, fightMochiTimer: this.fightMochiTimer,
|
totalFightLosses: this.totalFightLosses, totalFightWins: this.totalFightWins, isZunchiSlave: this.isZunchiSlave, formerName: this.formerName, loveMochiTimer: this.loveMochiTimer, fightMochiTimer: this.fightMochiTimer,
|
||||||
sleepDisease: this.sleepDisease, sleepDiseaseCooldown: this.sleepDiseaseCooldown, explosionDisease: this.explosionDisease, explosionDiseaseTimer: this.explosionDiseaseTimer, fightDisease: this.fightDisease, fightDiseaseCooldown: this.fightDiseaseCooldown, lastBleedAt: this.lastBleedAt, favorite: this.favorite, insideNestBoxId: this.insideNestBoxId || null, nestFade: this.nestFade || 0, sleepDiseaseAnchorX: this.sleepDiseaseAnchorX, sleepDiseaseAnchorY: this.sleepDiseaseAnchorY, records: this.records ? this.records.slice(0, 42) : [],
|
sleepDisease: this.sleepDisease, sleepDiseaseCooldown: this.sleepDiseaseCooldown, explosionDisease: this.explosionDisease, explosionDiseaseTimer: this.explosionDiseaseTimer, fightDisease: this.fightDisease, fightDiseaseCooldown: this.fightDiseaseCooldown, lastBleedAt: this.lastBleedAt, favorite: this.favorite, insideNestBoxId: this.insideNestBoxId || null, nestFade: this.nestFade || 0, nestBoxEnteredAt: this.nestBoxEnteredAt, nestBoxStayUntil: this.nestBoxStayUntil, focusPulseTimer: this.focusPulseTimer || 0, sleepDiseaseAnchorX: this.sleepDiseaseAnchorX, sleepDiseaseAnchorY: this.sleepDiseaseAnchorY, records: this.records ? this.records.slice(0, 42) : [],
|
||||||
zunchiDisease: this.zunchiDisease, zunchiDiseaseSeverity: this.zunchiDiseaseSeverity, zunchiDiseaseCooldown: this.zunchiDiseaseCooldown,
|
zunchiDisease: this.zunchiDisease, zunchiDiseaseSeverity: this.zunchiDiseaseSeverity, zunchiDiseaseCooldown: this.zunchiDiseaseCooldown,
|
||||||
tempComfort: this.tempComfort, tempTimer: this.tempTimer,
|
tempComfort: this.tempComfort, tempTimer: this.tempTimer,
|
||||||
parents: this.parents, parentNames: this.parentNames, children: this.children, birthTime: this.birthTime,
|
parents: this.parents, parentNames: this.parentNames, children: this.children, birthTime: this.birthTime,
|
||||||
|
|
@ -176,7 +185,8 @@ class Tarinai {
|
||||||
nextHeadbutt: this.nextHeadbutt, digest: this.digest, poopCount: this.poopCount, stretchTimer: this.stretchTimer,
|
nextHeadbutt: this.nextHeadbutt, digest: this.digest, poopCount: this.poopCount, stretchTimer: this.stretchTimer,
|
||||||
grassEatTimer: this.grassEatTimer, hurtTimer: this.hurtTimer,
|
grassEatTimer: this.grassEatTimer, hurtTimer: this.hurtTimer,
|
||||||
defeatedTimer: this.defeatedTimer, defeatedById: this.defeatedById, fightWinnerId: this.fightWinnerId,
|
defeatedTimer: this.defeatedTimer, defeatedById: this.defeatedById, fightWinnerId: this.fightWinnerId,
|
||||||
relationships: this.relationships, personality: this.personality, fallTimer: this.fallTimer, fallMax: this.fallMax, fallDir: this.fallDir,
|
relationships: this.relationships, fallTimer: this.fallTimer, fallMax: this.fallMax, fallDir: this.fallDir,
|
||||||
|
birthPersonality: normalizePersonality(this.birthPersonality), currentPersonality: normalizePersonality(this.currentPersonality), personalityDaily: this.personalityDaily || null,
|
||||||
blastSpinTimer: this.blastSpinTimer, blastSpinMax: this.blastSpinMax, postBirthPeaceTimer: this.postBirthPeaceTimer,
|
blastSpinTimer: this.blastSpinTimer, blastSpinMax: this.blastSpinMax, postBirthPeaceTimer: this.postBirthPeaceTimer,
|
||||||
lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, nextCursorHeartAt: this.nextCursorHeartAt, cursorPetting: this.cursorPetting,
|
lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, nextCursorHeartAt: this.nextCursorHeartAt, cursorPetting: this.cursorPetting,
|
||||||
entryTimer: this.entryTimer || 0,
|
entryTimer: this.entryTimer || 0,
|
||||||
|
|
@ -225,9 +235,9 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
addRecord(text, kind = "note") {
|
addRecord(text, kind = "note", options = {}) {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
if (this.world?.shouldHideFromObservationRecords?.(text)) return;
|
if (options.hiddenFromObservation) return;
|
||||||
if (!Array.isArray(this.records)) this.records = [];
|
if (!Array.isArray(this.records)) this.records = [];
|
||||||
const entry = { time: this.world?.time || 0, text: String(text), kind: kind || "note" };
|
const entry = { time: this.world?.time || 0, text: String(text), kind: kind || "note" };
|
||||||
const head = this.records[0];
|
const head = this.records[0];
|
||||||
|
|
@ -237,13 +247,33 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
personalityProfile() {
|
personalityProfile() {
|
||||||
return PERSONALITIES[this.personality] || PERSONALITIES.calm;
|
const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
|
||||||
|
const p = ensurePersonality(this).currentPersonality;
|
||||||
|
const e = {
|
||||||
|
neuroticism: personalityEffectValue(p.neuroticism),
|
||||||
|
aggression: personalityEffectValue(p.aggression),
|
||||||
|
sociability: personalityEffectValue(p.sociability),
|
||||||
|
openness: personalityEffectValue(p.openness),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
fear: clamp((base.fear || 1) * (1 + e.neuroticism * 0.38), 0.55, 1.85),
|
||||||
|
fight: clamp((base.fight || 1) * (1 + e.aggression * 0.55), 0.35, 2.35),
|
||||||
|
social: clamp((base.social || 1) * (1 + e.sociability * 0.24), 0.62, 1.52),
|
||||||
|
relation: clamp((base.relation || 1) * (1 + e.sociability * 0.24), 0.62, 1.60),
|
||||||
|
play: clamp((base.play || 1) * (1 + e.openness * 0.45), 0.45, 2.10),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
personalityLabel() {
|
personalityLabel() {
|
||||||
return this.personalityProfile().label || this.personality || "\u4e0d\u660e";
|
const tags = getPersonalityTraitTags(this);
|
||||||
|
return tags.length ? tags.join(" / ") : "\u4e2d\u7acb";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
personalityTags() { return getPersonalityTraitTags(this); }
|
||||||
|
adjustPersonality(key, delta, reason = "") { return adjustPersonality(this, key, delta, reason); }
|
||||||
|
shouldApplyPersonalityBehavior(key, direction = 1) { return shouldApplyPersonalityBehavior(this, key, direction); }
|
||||||
|
|
||||||
fightWinCount() {
|
fightWinCount() {
|
||||||
return this.totalFightWins || 0;
|
return this.totalFightWins || 0;
|
||||||
}
|
}
|
||||||
|
|
@ -253,17 +283,17 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeBecomeTimidAfterDamage(cause = "") {
|
maybeBecomeTimidAfterDamage(cause = "") {
|
||||||
if (this.dead || this.personality === "timid") return false;
|
if (this.dead) return false;
|
||||||
const losses = this.totalFightLosses || 0;
|
const losses = this.totalFightLosses || 0;
|
||||||
const wins = this.totalFightWins || 0;
|
const wins = this.totalFightWins || 0;
|
||||||
if (losses <= wins) return false;
|
if (losses <= wins) return false;
|
||||||
if (Math.random() >= 0.05) return false;
|
if (Math.random() >= 0.05) return false;
|
||||||
this.personality = "timid";
|
const changedA = this.adjustPersonality?.("neuroticism", 0.025, "after being hurt") || 0;
|
||||||
|
const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0;
|
||||||
this.fearTimer = Math.max(this.fearTimer || 0, 1.2);
|
this.fearTimer = Math.max(this.fearTimer || 0, 1.2);
|
||||||
this.stress = clamp((this.stress || 0) + 5, 0, 130);
|
this.stress = clamp((this.stress || 0) + 5, 0, 130);
|
||||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.35, "こわい…", "rgba(90,80,120,0.78)");
|
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.35, "\u3053\u308f\u3044\u2026", "rgba(90,80,120,0.78)");
|
||||||
this.addRecord?.(`${this.name}は痛みで臆病になった。`, "health");
|
return Boolean(changedA || changedB);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
relationTo(id) {
|
relationTo(id) {
|
||||||
|
|
@ -285,7 +315,7 @@ class Tarinai {
|
||||||
|
|
||||||
strongestRelation(kind = "friend") {
|
strongestRelation(kind = "friend") {
|
||||||
let bestId = null;
|
let bestId = null;
|
||||||
let bestScore = kind === "fear" ? 5 : 8;
|
let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD;
|
||||||
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||||
const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0);
|
const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0);
|
||||||
if (score > bestScore) { bestId = id; bestScore = score; }
|
if (score > bestScore) { bestId = id; bestScore = score; }
|
||||||
|
|
@ -299,7 +329,23 @@ class Tarinai {
|
||||||
return { friend, fear };
|
return { friend, fear };
|
||||||
}
|
}
|
||||||
|
|
||||||
bestFriendLive(minScore = 10, maxDist = Infinity) {
|
currentFriendCount(minScore = FRIEND_AFFINITY_THRESHOLD, liveOnly = true) {
|
||||||
|
let count = 0;
|
||||||
|
const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id)) : null;
|
||||||
|
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||||
|
if ((rel.affinity || 0) <= minScore) continue;
|
||||||
|
if (liveIds && !liveIds.has(id)) continue;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
sociabilityFriendScale() {
|
||||||
|
const friends = this.currentFriendCount ? this.currentFriendCount(FRIEND_AFFINITY_THRESHOLD, true) : 0;
|
||||||
|
return clamp(0.82 + Math.min(6, friends) * 0.16, 0.82, 1.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
bestFriendLive(minScore = FRIEND_AFFINITY_THRESHOLD, maxDist = Infinity) {
|
||||||
let best = null, bestScore = minScore;
|
let best = null, bestScore = minScore;
|
||||||
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||||
const score = rel.affinity || 0;
|
const score = rel.affinity || 0;
|
||||||
|
|
@ -369,6 +415,24 @@ class Tarinai {
|
||||||
normalizeDeathReason(reason = "", opts = {}) { return HEALTH.normalizeDeathReason(this, reason, opts); }
|
normalizeDeathReason(reason = "", opts = {}) { return HEALTH.normalizeDeathReason(this, reason, opts); }
|
||||||
damage(amount, reason = "") { return HEALTH.applyDamage(this, amount, reason); }
|
damage(amount, reason = "") { return HEALTH.applyDamage(this, amount, reason); }
|
||||||
|
|
||||||
|
wakeFromDamage(reason = "") {
|
||||||
|
if (this.dead) return false;
|
||||||
|
const wasSleeping = this.state === "sleep" || this.sleeping || this.insideNestBoxId;
|
||||||
|
if (!wasSleeping) return false;
|
||||||
|
const box = this.nestBoxById ? this.nestBoxById(this.insideNestBoxId) : null;
|
||||||
|
this.sleeping = false;
|
||||||
|
this.state = "panic";
|
||||||
|
this.target = null;
|
||||||
|
this.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u76ee\u304c\u899a\u3081\u305f";
|
||||||
|
this.fearTimer = Math.max(this.fearTimer || 0, 0.85);
|
||||||
|
this.hurtTimer = Math.max(this.hurtTimer || 0, 1.2);
|
||||||
|
this.nestBoxStayUntil = -Infinity;
|
||||||
|
if (this.insideNestBoxId) this.leaveNestBox(box);
|
||||||
|
this.vx += rand(-34, 34);
|
||||||
|
this.vy += rand(-26, 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
recoverHealth(amount) {
|
recoverHealth(amount) {
|
||||||
if (this.dead) return;
|
if (this.dead) return;
|
||||||
this.energy = clamp(this.energy + Math.max(0, amount || 0), 0, 100);
|
this.energy = clamp(this.energy + Math.max(0, amount || 0), 0, 100);
|
||||||
|
|
@ -406,7 +470,7 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
targetIgnoresFence(target = this.target) {
|
targetIgnoresFence(target = this.target) {
|
||||||
return Boolean(target?.type === "sweet" || this.state === "panic" || this.state === "fight" || this.state === "intimidate" || this.state === "birth_ritual" || this.state === "cursor_enemy");
|
return Boolean(target?.type === "sweet" || target?.type === "nest_box" || (target?.type === "bed" && (this.state === "seek_bed" || this.state === "sleep")) || this.state === "panic" || this.state === "fight" || this.state === "intimidate" || this.state === "birth_ritual" || this.state === "cursor_enemy");
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldAvoidTarget(target) {
|
shouldAvoidTarget(target) {
|
||||||
|
|
@ -647,9 +711,9 @@ class Tarinai {
|
||||||
this.fightDisease = true;
|
this.fightDisease = true;
|
||||||
this.fightDiseaseCooldown = 18;
|
this.fightDiseaseCooldown = 18;
|
||||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.35);
|
this.fearTimer = Math.max(this.fearTimer || 0, 0.35);
|
||||||
this.thought = "きずつき病\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
this.thought = "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
||||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)");
|
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)");
|
||||||
this.world?.log?.(`${this.name}\u306fきずつき病\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight");
|
this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -657,10 +721,10 @@ class Tarinai {
|
||||||
if (!this.fightDisease) return false;
|
if (!this.fightDisease) return false;
|
||||||
this.fightDisease = false;
|
this.fightDisease = false;
|
||||||
this.fightDiseaseCooldown = 20;
|
this.fightDiseaseCooldown = 20;
|
||||||
this.thought = reason || "きずつき病\u304c\u6cbb\u3063\u305f";
|
this.thought = reason || "\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f";
|
||||||
this.stress = clamp(this.stress - 12, 0, 130);
|
this.stress = clamp(this.stress - 12, 0, 130);
|
||||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
|
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
|
||||||
this.world?.log?.(`${this.name}\u306eきずつき病\u304c\u6cbb\u3063\u305f\u3002`, "fight");
|
this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,34 +748,112 @@ class Tarinai {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNestBoxPresence(dt = 0) {
|
nestBoxById(id) {
|
||||||
const previous = this.insideNestBoxId || null;
|
if (!id) return null;
|
||||||
let box = null;
|
return this.world?.items?.find?.(it => it && !it.dead && it.id === id && it.type === "nest_box") || null;
|
||||||
if (previous) box = this.world?.items?.find?.(it => it && !it.dead && it.id === previous && it.type === "nest_box") || null;
|
}
|
||||||
if (!box && this.target && !this.target.dead && this.target.type === "nest_box") box = this.target;
|
|
||||||
if (!box) {
|
isInsideNestBox() {
|
||||||
this.insideNestBoxId = null;
|
return Boolean(this.insideNestBoxId);
|
||||||
this.nestFade = clamp((this.nestFade || 0) - dt * 1.8, 0, 1);
|
}
|
||||||
return null;
|
|
||||||
}
|
wantsNestBox(box = null) {
|
||||||
const d = distXY(this.x, this.y, box.x, box.y);
|
if (!box || box.dead || box.type !== "nest_box") return false;
|
||||||
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) return true;
|
||||||
|
if (!(this.state === "sleep" || this.state === "seek_bed")) return false;
|
||||||
|
return this.target === box || this.insideNestBoxId === box.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterNestBox(box, dt = 0.016) {
|
||||||
|
if (!box || box.dead || box.type !== "nest_box" || this.dead) return false;
|
||||||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||||
const occupantIndex = occupants.indexOf(this);
|
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||||
const hasRoom = occupantIndex >= 0 ? occupantIndex < capacity : occupants.length < capacity;
|
const alreadyInside = this.insideNestBoxId === box.id;
|
||||||
const canEnter = hasRoom && (this.state === "sleep" || this.state === "seek_bed") && d <= Math.max(18, box.r * 0.58);
|
if (!alreadyInside && occupants.length >= capacity) return false;
|
||||||
if (canEnter) {
|
if (!alreadyInside) {
|
||||||
this.insideNestBoxId = box.id;
|
this.nestBoxEnteredAt = this.world?.time || 0;
|
||||||
this.nestFade = clamp((this.nestFade || 0) + dt * 2.4, 0, 1);
|
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, (this.world?.time || 0) + 14);
|
||||||
const pull = clamp(dt * 4.2, 0, 1);
|
}
|
||||||
this.x = lerp(this.x, box.x, pull);
|
this.insideNestBoxId = box.id;
|
||||||
this.y = lerp(this.y, box.y, pull);
|
this.target = box;
|
||||||
this.vx *= Math.pow(0.45, dt * 60);
|
if (this.state === "seek_bed") this.state = "sleep";
|
||||||
this.vy *= Math.pow(0.45, dt * 60);
|
if (this.state === "sleep") this.sleeping = true;
|
||||||
return box;
|
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);
|
||||||
|
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) {
|
||||||
|
this.x = inner.x;
|
||||||
|
this.y = inner.y;
|
||||||
|
}
|
||||||
|
this.nestFade = clamp((this.nestFade || 0) + dt * 5.8, 0, 1);
|
||||||
|
this.vx = 0;
|
||||||
|
this.vy = 0;
|
||||||
|
this.impulseVx = 0;
|
||||||
|
this.impulseVy = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveNestBox(box = null) {
|
||||||
|
const b = box || this.nestBoxById(this.insideNestBoxId);
|
||||||
|
if (b && this.world?.nestBoxEntryPoint) {
|
||||||
|
const entry = this.world.nestBoxExitPoint ? this.world.nestBoxExitPoint(b, this) : this.world.nestBoxEntryPoint(b);
|
||||||
|
this.x = clamp(entry.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
|
||||||
|
this.y = clamp(entry.y, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
|
||||||
|
this.vx = rand(-8, 8);
|
||||||
|
this.vy = rand(6, 18);
|
||||||
}
|
}
|
||||||
this.insideNestBoxId = null;
|
this.insideNestBoxId = null;
|
||||||
this.nestFade = clamp((this.nestFade || 0) - dt * 1.8, 0, 1);
|
this.nestFade = 0;
|
||||||
|
this.nestBoxEnteredAt = -Infinity;
|
||||||
|
this.nestBoxStayUntil = -Infinity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNestBoxPresence(dt = 0) {
|
||||||
|
const currentBox = this.nestBoxById(this.insideNestBoxId);
|
||||||
|
const targetBox = this.target && !this.target.dead && this.target.type === "nest_box" ? this.target : null;
|
||||||
|
const box = currentBox || targetBox;
|
||||||
|
if (!box) {
|
||||||
|
this.insideNestBoxId = null;
|
||||||
|
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBox && !this.wantsNestBox(currentBox)) {
|
||||||
|
this.leaveNestBox(currentBox);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBox) {
|
||||||
|
if ((this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity)) {
|
||||||
|
this.state = "sleep";
|
||||||
|
this.sleeping = true;
|
||||||
|
this.target = currentBox;
|
||||||
|
}
|
||||||
|
this.enterNestBox(currentBox, dt);
|
||||||
|
return currentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.wantsNestBox(targetBox)) {
|
||||||
|
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(targetBox) : { x: targetBox.x, y: targetBox.y };
|
||||||
|
const base = this.world?.nestBoxBaseRect ? this.world.nestBoxBaseRect(targetBox) : null;
|
||||||
|
const r = targetBox.r || 42;
|
||||||
|
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;
|
||||||
|
if (nearEnough) {
|
||||||
|
this.enterNestBox(targetBox, dt || 0.016);
|
||||||
|
return targetBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,7 +898,7 @@ class Tarinai {
|
||||||
if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident");
|
if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident");
|
||||||
}
|
}
|
||||||
if (this.fightDisease && Math.random() < dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0)) {
|
if (this.fightDisease && Math.random() < dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0)) {
|
||||||
this.recoverFightDisease("\u81ea\u7136\u306bきずつき病\u304c\u6cbb\u3063\u305f");
|
this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
|
||||||
}
|
}
|
||||||
if (this.fightDisease) {
|
if (this.fightDisease) {
|
||||||
this.stress = clamp(this.stress + dt * 0.16, 0, 130);
|
this.stress = clamp(this.stress + dt * 0.16, 0, 130);
|
||||||
|
|
@ -849,7 +991,7 @@ class Tarinai {
|
||||||
if (["fight", "panic", "cursor_enemy", "hurt", "defeated", "fight_sick"].includes(this.state)) return false;
|
if (["fight", "panic", "cursor_enemy", "hurt", "defeated", "fight_sick"].includes(this.state)) return false;
|
||||||
if ((this.fearTimer || 0) > 0.18 || (this.intimidatedTimer || 0) > 0.04 || (this.intimidateTimer || 0) > 0.04) return false;
|
if ((this.fearTimer || 0) > 0.18 || (this.intimidatedTimer || 0) > 0.04 || (this.intimidateTimer || 0) > 0.04) return false;
|
||||||
if (this.mood < 46 || this.stress > 64) return false;
|
if (this.mood < 46 || this.stress > 64) return false;
|
||||||
return this.affection > 12 || this.mood > 58 || this.goodMode === "smile" || ["lonely", "calm", "playful", "teary", "cry"].includes(this.personality);
|
return this.affection > 12 || this.mood > 58 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCursorContactCare(dt) {
|
applyCursorContactCare(dt) {
|
||||||
|
|
@ -861,6 +1003,7 @@ class Tarinai {
|
||||||
const p = clamp(1 - d / touch, 0, 1);
|
const p = clamp(1 - d / touch, 0, 1);
|
||||||
const relief = dt * (1.35 + 2.65 * p);
|
const relief = dt * (1.35 + 2.65 * p);
|
||||||
this.stress = clamp((this.stress || 0) - relief, 0, 130);
|
this.stress = clamp((this.stress || 0) - relief, 0, 130);
|
||||||
|
this.adjustPersonality("openness", dt * 0.006 * (0.4 + p), "after being petted.");
|
||||||
this.moodLiftTimer = Math.max(this.moodLiftTimer || 0, 0.5);
|
this.moodLiftTimer = Math.max(this.moodLiftTimer || 0, 0.5);
|
||||||
this.cursorPetting = clamp((this.cursorPetting || 0) + dt * (1.0 + p * 2.4), 0, 1.75);
|
this.cursorPetting = clamp((this.cursorPetting || 0) + dt * (1.0 + p * 2.4), 0, 1.75);
|
||||||
this.goodMode = "smile";
|
this.goodMode = "smile";
|
||||||
|
|
@ -914,6 +1057,7 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt);
|
this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt);
|
||||||
|
this.focusPulseTimer = Math.max(0, (this.focusPulseTimer || 0) - dt);
|
||||||
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
|
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
|
||||||
if (this.freezeSleepDiseaseMotion(dt)) {
|
if (this.freezeSleepDiseaseMotion(dt)) {
|
||||||
this.checkLife(dt);
|
this.checkLife(dt);
|
||||||
|
|
@ -1035,9 +1179,16 @@ class Tarinai {
|
||||||
if (parent && dist(this, parent) < 96) {
|
if (parent && dist(this, parent) < 96) {
|
||||||
this.stress = clamp(this.stress - dt * 0.42, 0, 130);
|
this.stress = clamp(this.stress - dt * 0.42, 0, 130);
|
||||||
this.loneliness = clamp(this.loneliness - dt * 0.32, 0, 110);
|
this.loneliness = clamp(this.loneliness - dt * 0.32, 0, 110);
|
||||||
|
this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near family.");
|
||||||
|
}
|
||||||
|
const friend = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 92) : null;
|
||||||
|
const friendCount = this.currentFriendCount ? this.currentFriendCount(FRIEND_AFFINITY_THRESHOLD, true) : 0;
|
||||||
|
if (friend) {
|
||||||
|
this.stress = clamp(this.stress - dt * 0.36, 0, 130);
|
||||||
|
this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near friends.");
|
||||||
|
} else if (friendCount <= 0 && this.loneliness > 72 && this.age > 8) {
|
||||||
|
this.adjustPersonality("sociability", -dt * 0.0018, "after having no friends.");
|
||||||
}
|
}
|
||||||
const friend = this.bestFriendLive ? this.bestFriendLive(12, 92) : null;
|
|
||||||
if (friend) this.stress = clamp(this.stress - dt * 0.36, 0, 130);
|
|
||||||
let grassComfort = 0;
|
let grassComfort = 0;
|
||||||
for (const it of this.world.nearbyItems(this.x, this.y, 105)) {
|
for (const it of this.world.nearbyItems(this.x, this.y, 105)) {
|
||||||
if (it.type !== "grass" || it.dead) continue;
|
if (it.type !== "grass" || it.dead) continue;
|
||||||
|
|
@ -1054,7 +1205,7 @@ class Tarinai {
|
||||||
if (grassComfort > 0) {
|
if (grassComfort > 0) {
|
||||||
this.stress = clamp(this.stress - dt * Math.min(0.32, grassComfort * 0.08), 0, 130);
|
this.stress = clamp(this.stress - dt * Math.min(0.32, grassComfort * 0.08), 0, 130);
|
||||||
if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.22, grassComfort * 0.06));
|
if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.22, grassComfort * 0.06));
|
||||||
if (this.need === "\u9759\u3051\u3055" || this.personality === "timid") this.affection += dt * Math.min(0.06, grassComfort * 0.012);
|
if (this.need === "\u9759\u3051\u3055" || this.shouldApplyPersonalityBehavior("neuroticism", 1)) this.affection += dt * Math.min(0.06, grassComfort * 0.012);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1080,10 +1231,44 @@ class Tarinai {
|
||||||
return target?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5e72\u8349\u5bdd\u5e8a";
|
return target?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5e72\u8349\u5bdd\u5e8a";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isServingFoodItem(it) {
|
||||||
|
return Boolean(it && typeof isServingFoodType === "function" && isServingFoodType(it.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
foodItemHasServingLeft(it) {
|
||||||
|
if (!it || it.dead) return false;
|
||||||
|
if (!this.isServingFoodItem(it)) return (it.amount || 0) > 1.2;
|
||||||
|
this.ensureFoodServings(it);
|
||||||
|
return (it.foodServingsRemaining || 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureFoodServings(it) {
|
||||||
|
if (!this.isServingFoodItem(it)) return;
|
||||||
|
const max = typeof foodServingsForSize === "function" ? foodServingsForSize(it.toolSize || "medium") : 5;
|
||||||
|
if (!Number.isFinite(it.foodServingsMax) || it.foodServingsMax <= 0) it.foodServingsMax = max;
|
||||||
|
if (!Number.isFinite(it.foodServingsRemaining)) {
|
||||||
|
const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58 }[it.type] || 62;
|
||||||
|
const ratio = clamp((it.amount ?? original) / original, 0.05, 1);
|
||||||
|
it.foodServingsRemaining = Math.max(1, Math.ceil(it.foodServingsMax * ratio));
|
||||||
|
}
|
||||||
|
it.foodServingsRemaining = clamp(it.foodServingsRemaining, 0, it.foodServingsMax);
|
||||||
|
it.amount = it.foodServingsRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeFoodServing(it, nutrition) {
|
||||||
|
this.ensureFoodServings(it);
|
||||||
|
if (!this.isServingFoodItem(it) || (it.foodServingsRemaining || 0) <= 0) return 0;
|
||||||
|
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
|
||||||
|
it.amount = it.foodServingsRemaining;
|
||||||
|
return nutrition;
|
||||||
|
}
|
||||||
|
|
||||||
sleepSpotFor(bed) {
|
sleepSpotFor(bed) {
|
||||||
if (!bed) return { x: this.x, y: this.y };
|
if (!bed) return { x: this.x, y: this.y };
|
||||||
if (bed.type === "nest_box") {
|
if (bed.type === "nest_box") {
|
||||||
return { x: clamp(bed.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding), y: clamp(bed.y, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding) };
|
if (this.insideNestBoxId === bed.id && this.world?.nestBoxInnerPoint) return this.world.nestBoxInnerPoint(bed, this);
|
||||||
|
if (this.world?.nestBoxEntryPoint) return this.world.nestBoxEntryPoint(bed);
|
||||||
|
return { x: clamp(bed.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding), y: clamp(bed.y + (bed.r || 42) * 0.24, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding) };
|
||||||
}
|
}
|
||||||
const angle = stableUnit(this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2;
|
const angle = stableUnit(this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2;
|
||||||
const baseRing = bed.r * 0.52;
|
const baseRing = bed.r * 0.52;
|
||||||
|
|
@ -1128,6 +1313,7 @@ class Tarinai {
|
||||||
const d = dist(this, z);
|
const d = dist(this, z);
|
||||||
const push = clamp(1 - d / 90, 0, 1);
|
const push = clamp(1 - d / 90, 0, 1);
|
||||||
this.stress = clamp(this.stress + push * dt * 1.2 * this.personalityProfile().fear, 0, 130);
|
this.stress = clamp(this.stress + push * dt * 1.2 * this.personalityProfile().fear, 0, 130);
|
||||||
|
if (push > 0.2) this.adjustPersonality("neuroticism", -dt * push * 0.0035, "after repeated contact with poop.");
|
||||||
this.vx += (this.x - z.x) / Math.max(d, 1) * push * dt * 40;
|
this.vx += (this.x - z.x) / Math.max(d, 1) * push * dt * 40;
|
||||||
this.vy += (this.y - z.y) / Math.max(d, 1) * push * dt * 40;
|
this.vy += (this.y - z.y) / Math.max(d, 1) * push * dt * 40;
|
||||||
}
|
}
|
||||||
|
|
@ -1182,12 +1368,12 @@ class Tarinai {
|
||||||
this.fightWinnerId = null;
|
this.fightWinnerId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendNearby = this.bestFriendLive ? this.bestFriendLive(12, 170) : null;
|
const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null;
|
||||||
if (friendNearby && friendNearby.state === "panic" && friendNearby.target) {
|
if (friendNearby && friendNearby.state === "panic" && friendNearby.target) {
|
||||||
this.state = "panic";
|
this.state = "panic";
|
||||||
this.target = this.panicDestination(friendNearby.target);
|
this.target = this.panicDestination(friendNearby.target);
|
||||||
this.fearTimer = Math.max(this.fearTimer, 0.32);
|
this.fearTimer = Math.max(this.fearTimer, 0.32);
|
||||||
this.thought = "\u4ef2\u826f\u3057\u3068\u4e00\u7dd2\u306b\u9003\u3052\u3066\u3044\u308b";
|
this.thought = "\u53cb\u9054\u3068\u4e00\u7dd2\u306b\u9003\u3052\u3066\u3044\u308b";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1245,11 +1431,11 @@ class Tarinai {
|
||||||
|
|
||||||
if (this.hasZundaCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
if (this.hasZundaCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
||||||
const cureFood = this.world.nearest(this, ["sweet"], 700);
|
const cureFood = this.world.nearest(this, ["sweet"], 700);
|
||||||
if (cureFood && cureFood.amount > 1.2 && !this.shouldAvoidTarget(cureFood)) {
|
if (cureFood && this.foodItemHasServingLeft(cureFood) && !this.shouldAvoidTarget(cureFood)) {
|
||||||
this.state = "seek_food";
|
this.state = "seek_food";
|
||||||
this.target = cureFood;
|
this.target = cureFood;
|
||||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.42);
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.42);
|
||||||
this.thought = this.fightDisease ? "きずつき病\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u7206\u767a\u75c5\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b";
|
this.thought = this.fightDisease ? "\u304d\u305a\u3064\u304d\u75c5\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u7206\u767a\u75c5\u3067\u305a\u3093\u3060\u9905\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1257,14 +1443,14 @@ class Tarinai {
|
||||||
if (this.fightDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
if (this.fightDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
||||||
this.state = "fight_sick";
|
this.state = "fight_sick";
|
||||||
this.target = null;
|
this.target = null;
|
||||||
this.thought = "きずつき病\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
this.thought = "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
||||||
if (Math.random() < dt * 0.45) this.wanderAngle += rand(-3.4, 3.4);
|
if (Math.random() < dt * 0.45) this.wanderAngle += rand(-3.4, 3.4);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isZunchiSlave) {
|
if (!this.isZunchiSlave) {
|
||||||
const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi", "sleep_drug"], wasSleeping ? 520 : 390);
|
const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi", "sleep_drug"], wasSleeping ? 520 : 390);
|
||||||
if (specialMochi && specialMochi.amount > 1.2 && this.hunger > 10 && !this.shouldAvoidTarget(specialMochi)) {
|
if (specialMochi && this.foodItemHasServingLeft(specialMochi) && this.hunger > 10 && !this.shouldAvoidTarget(specialMochi)) {
|
||||||
this.state = "seek_food";
|
this.state = "seek_food";
|
||||||
this.target = specialMochi;
|
this.target = specialMochi;
|
||||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.34);
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.34);
|
||||||
|
|
@ -1286,7 +1472,7 @@ class Tarinai {
|
||||||
const zunda = this.world.nearest(this, ["sweet"], zundaRange);
|
const zunda = this.world.nearest(this, ["sweet"], zundaRange);
|
||||||
const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1));
|
const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1));
|
||||||
const zundaReward = this.affection > 8 && this.hunger > 7;
|
const zundaReward = this.affection > 8 && this.hunger > 7;
|
||||||
if (zunda && zunda.amount > 1.2 && ((wasSleeping && this.hunger > 7) || zundaHungry || zundaReward)) {
|
if (zunda && this.foodItemHasServingLeft(zunda) && ((wasSleeping && this.hunger > 7) || zundaHungry || zundaReward)) {
|
||||||
this.state = "seek_food";
|
this.state = "seek_food";
|
||||||
this.target = zunda;
|
this.target = zunda;
|
||||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.36);
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.36);
|
||||||
|
|
@ -1306,7 +1492,7 @@ class Tarinai {
|
||||||
if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) {
|
if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) {
|
||||||
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
|
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
|
||||||
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
|
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
|
||||||
if (!bed && !wasSleeping) bed = this.world.bestBedFor(this, 360);
|
if (!bed) bed = this.world.bestBedFor(this, wasSleeping ? 480 : 420);
|
||||||
if (bed && !wasSleeping && this.energy > 18) {
|
if (bed && !wasSleeping && this.energy > 18) {
|
||||||
const spot = this.sleepSpotFor(bed);
|
const spot = this.sleepSpotFor(bed);
|
||||||
if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) {
|
if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) {
|
||||||
|
|
@ -1323,7 +1509,7 @@ class Tarinai {
|
||||||
this.nextSleepBubbleAt = this.world.time + 4.5;
|
this.nextSleepBubbleAt = this.world.time + 4.5;
|
||||||
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
||||||
}
|
}
|
||||||
this.thought = bed ? "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b";
|
this.thought = bed ? (bed.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b") : "\u7720\u3063\u3066\u3044\u308b";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1355,19 +1541,21 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
const playTrait = this.personalityProfile().play ?? 1;
|
const playTrait = this.personalityProfile().play ?? 1;
|
||||||
const ballRange = this.personality === "playful" ? 540 : 240 + 60 * playTrait;
|
const openPlay = this.shouldApplyPersonalityBehavior("openness", 1);
|
||||||
|
const closedPlay = this.shouldApplyPersonalityBehavior("openness", -1);
|
||||||
|
const ballRange = openPlay ? 540 : (closedPlay ? 170 : 240 + 60 * playTrait);
|
||||||
const ball = this.world.nearest(this, ["ball"], ballRange);
|
const ball = this.world.nearest(this, ["ball"], ballRange);
|
||||||
if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) {
|
if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) {
|
||||||
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||||
const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0;
|
const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0;
|
||||||
const baseChance = this.personality === "playful" ? 0.74 : 0.08;
|
const baseChance = openPlay ? 0.74 : (closedPlay ? 0.025 : 0.08);
|
||||||
const movingBonus = clamp(ballSpeed / 120, 0, 0.42);
|
const movingBonus = clamp(ballSpeed / 120, 0, 0.42);
|
||||||
const socialBonus = this.loneliness > 48 ? 0.10 : 0;
|
const socialBonus = this.loneliness > 48 ? 0.10 : 0;
|
||||||
if (chasers < 5 && (this.personality === "playful" || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) {
|
if (chasers < 5 && (openPlay || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) {
|
||||||
this.state = "play_ball";
|
this.state = "play_ball";
|
||||||
this.target = ball;
|
this.target = ball;
|
||||||
this.goodMode = "smile";
|
this.goodMode = "smile";
|
||||||
this.thought = this.personality === "playful" ? "\u30dc\u30fc\u30eb\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3066\u3044\u308b" : "\u8ee2\u304c\u308b\u3082\u306e\u304c\u6c17\u306b\u306a\u3063\u3066\u3044\u308b";
|
this.thought = openPlay ? "\u30dc\u30fc\u30eb\u306b\u5f15\u304d\u5bc4\u305b\u3089\u308c\u3066\u3044\u308b" : "\u8ee2\u304c\u308b\u3082\u306e\u304c\u6c17\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1388,7 +1576,7 @@ class Tarinai {
|
||||||
this.state = "seek_food";
|
this.state = "seek_food";
|
||||||
this.target = friendNearby.target;
|
this.target = friendNearby.target;
|
||||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
|
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
|
||||||
this.thought = "\u4ef2\u826f\u3057\u3068\u4e00\u7dd2\u306b\u98df\u3079\u306b\u884c\u304f";
|
this.thought = "\u53cb\u9054\u3068\u4e00\u7dd2\u306b\u98df\u3079\u306b\u884c\u304f";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1404,8 +1592,10 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.loneliness > 63 - (this.personality === "lonely" ? 16 : 0)) {
|
const socialSeek = this.shouldApplyPersonalityBehavior("sociability", 1);
|
||||||
const other = this.world.nearestOther(this, 300);
|
const lonerSeek = this.shouldApplyPersonalityBehavior("sociability", -1);
|
||||||
|
if (this.loneliness > 63 - (socialSeek ? 16 : 0) + (lonerSeek ? 10 : 0)) {
|
||||||
|
const other = this.world.nearestOther(this, socialSeek ? 390 : (lonerSeek ? 190 : 300));
|
||||||
if (other) {
|
if (other) {
|
||||||
this.state = "seek_friend";
|
this.state = "seek_friend";
|
||||||
this.target = other;
|
this.target = other;
|
||||||
|
|
@ -1446,14 +1636,13 @@ class Tarinai {
|
||||||
const d = dist(this, it);
|
const d = dist(this, it);
|
||||||
if (d > this.radius + it.r + 12) continue;
|
if (d > this.radius + it.r + 12) continue;
|
||||||
|
|
||||||
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && canBite)) && it.amount > 1.2) {
|
if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && canBite)) && this.foodItemHasServingLeft(it)) {
|
||||||
const eating = Math.min(it.amount, it.type === "sweet" ? 7.2 : 8.5);
|
const eating = this.consumeFoodServing(it, it.type === "sweet" ? 7.2 : 8.5);
|
||||||
if (eating > 0) {
|
if (eating > 0) {
|
||||||
this.state = "eat";
|
this.state = "eat";
|
||||||
this.target = it;
|
this.target = it;
|
||||||
this.vx *= Math.pow(0.25, dt * 60);
|
this.vx *= Math.pow(0.25, dt * 60);
|
||||||
this.vy *= Math.pow(0.25, dt * 60);
|
this.vy *= Math.pow(0.25, dt * 60);
|
||||||
it.amount -= eating;
|
|
||||||
this.hunger -= eating * (it.type === "sweet" ? 0.75 : 0.95);
|
this.hunger -= eating * (it.type === "sweet" ? 0.75 : 0.95);
|
||||||
this.recoverHealth(eating * (it.type === "sweet" ? 0.28 : 0.46));
|
this.recoverHealth(eating * (it.type === "sweet" ? 0.28 : 0.46));
|
||||||
this.affection += it.type === "sweet" ? eating * 0.03 : eating * 0.012;
|
this.affection += it.type === "sweet" ? eating * 0.03 : eating * 0.012;
|
||||||
|
|
@ -1461,7 +1650,7 @@ class Tarinai {
|
||||||
this.stress = clamp(this.stress - eating * 0.42, 0, 130);
|
this.stress = clamp(this.stress - eating * 0.42, 0, 130);
|
||||||
this.fearTimer = Math.max(0, this.fearTimer - eating * 0.035);
|
this.fearTimer = Math.max(0, this.fearTimer - eating * 0.035);
|
||||||
if (this.explosionDisease) this.recoverExplosionDisease("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
|
if (this.explosionDisease) this.recoverExplosionDisease("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||||
if (this.fightDisease) this.recoverFightDisease("\u305a\u3093\u3060\u9905\u3067きずつき病\u304c\u6cbb\u3063\u305f");
|
if (this.fightDisease) this.recoverFightDisease("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
|
||||||
}
|
}
|
||||||
this.eatTimer = Math.max(this.eatTimer, 0.32);
|
this.eatTimer = Math.max(this.eatTimer, 0.32);
|
||||||
this.eatCooldown = it.type === "sweet" ? rand(0.34, 0.62) : rand(0.44, 0.78);
|
this.eatCooldown = it.type === "sweet" ? rand(0.34, 0.62) : rand(0.44, 0.78);
|
||||||
|
|
@ -1482,13 +1671,13 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug") && it.amount > 1.2) {
|
if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug") && this.foodItemHasServingLeft(it)) {
|
||||||
const eating = Math.min(it.amount, 6.8);
|
const eating = this.consumeFoodServing(it, 6.8);
|
||||||
|
if (eating <= 0) continue;
|
||||||
this.state = "eat";
|
this.state = "eat";
|
||||||
this.target = it;
|
this.target = it;
|
||||||
this.vx *= Math.pow(0.25, dt * 60);
|
this.vx *= Math.pow(0.25, dt * 60);
|
||||||
this.vy *= Math.pow(0.25, dt * 60);
|
this.vy *= Math.pow(0.25, dt * 60);
|
||||||
it.amount -= eating;
|
|
||||||
this.hunger -= eating * 0.82;
|
this.hunger -= eating * 0.82;
|
||||||
this.recoverHealth(eating * 0.24);
|
this.recoverHealth(eating * 0.24);
|
||||||
this.affection += eating * 0.04;
|
this.affection += eating * 0.04;
|
||||||
|
|
@ -1497,12 +1686,20 @@ class Tarinai {
|
||||||
this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 + rand(4, 12));
|
this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 + rand(4, 12));
|
||||||
this.bubble("\u2661", 2.4, "rgba(190,82,132,0.78)");
|
this.bubble("\u2661", 2.4, "rgba(190,82,132,0.78)");
|
||||||
this.lastMealColor = "#ff7bab";
|
this.lastMealColor = "#ff7bab";
|
||||||
} else {
|
} else if (it.type === "fight_mochi") {
|
||||||
this.stress = clamp(this.stress + eating * 0.22, 0, 130);
|
this.stress = clamp(this.stress + eating * 0.22, 0, 130);
|
||||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.32);
|
this.fearTimer = Math.max(this.fearTimer || 0, 0.32);
|
||||||
this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 52 + rand(10, 18));
|
this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 52 + rand(10, 18));
|
||||||
this.bubble("!", 2.0, "rgba(180,86,52,0.80)");
|
this.bubble("!", 2.0, "rgba(180,86,52,0.80)");
|
||||||
this.lastMealColor = "#e07c43";
|
this.lastMealColor = "#e07c43";
|
||||||
|
} else {
|
||||||
|
this.stress = clamp(this.stress - eating * 0.10, 0, 130);
|
||||||
|
this.energy = clamp(this.energy - eating * 0.20, 0, 100);
|
||||||
|
this.fearTimer = Math.max(0, (this.fearTimer || 0) - 0.18);
|
||||||
|
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - 2.5);
|
||||||
|
this.bubble("Zzz...", 2.3, "rgba(104,84,168,0.78)");
|
||||||
|
this.lastMealColor = "#b89cff";
|
||||||
|
if (!this.sleepDisease && Math.random() < Math.min(0.98, 0.35 + eating * 0.075)) this.infectSleepDisease(it);
|
||||||
}
|
}
|
||||||
this.eatTimer = Math.max(this.eatTimer, 0.30);
|
this.eatTimer = Math.max(this.eatTimer, 0.30);
|
||||||
this.eatCooldown = rand(0.38, 0.68);
|
this.eatCooldown = rand(0.38, 0.68);
|
||||||
|
|
@ -1521,7 +1718,8 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canBite && it.type === "grass") {
|
if (canBite && it.type === "grass") {
|
||||||
const bite = Math.min(it.amount, rand(4.2, 7.2));
|
const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1));
|
||||||
|
const bite = Math.min(it.amount, rand(4.2, 7.2) * servingScale);
|
||||||
this.state = "eat";
|
this.state = "eat";
|
||||||
this.target = it;
|
this.target = it;
|
||||||
this.grassEatTimer = Math.max(this.grassEatTimer, 0.22);
|
this.grassEatTimer = Math.max(this.grassEatTimer, 0.22);
|
||||||
|
|
@ -1548,10 +1746,14 @@ class Tarinai {
|
||||||
|
|
||||||
if (it.type === "zunchi") {
|
if (it.type === "zunchi") {
|
||||||
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
||||||
if (!this.isZunchiSlave) this.stress = clamp(this.stress + touch * dt * 3.4, 0, 130);
|
if (!this.isZunchiSlave) {
|
||||||
|
this.stress = clamp(this.stress + touch * dt * 3.4 * this.personalityProfile().fear, 0, 130);
|
||||||
|
if (touch > 0.08) this.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop.");
|
||||||
|
}
|
||||||
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
|
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
|
||||||
if (canZunchiBite) {
|
if (canZunchiBite) {
|
||||||
const bite = Math.min(it.amount, this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8));
|
const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1));
|
||||||
|
const bite = Math.min(it.amount, (this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale);
|
||||||
this.state = "eat";
|
this.state = "eat";
|
||||||
this.target = it;
|
this.target = it;
|
||||||
this.vx *= Math.pow(0.18, dt * 60);
|
this.vx *= Math.pow(0.18, dt * 60);
|
||||||
|
|
@ -1559,7 +1761,8 @@ class Tarinai {
|
||||||
it.amount -= bite;
|
it.amount -= bite;
|
||||||
this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18);
|
this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18);
|
||||||
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
|
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
|
||||||
this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52), 0, 130);
|
this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52) * this.personalityProfile().fear, 0, 130);
|
||||||
|
if (!this.isZunchiSlave) this.adjustPersonality("neuroticism", -bite * 0.0015, "after repeated contact with poop.");
|
||||||
this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100);
|
this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100);
|
||||||
this.lastMealColor = "#6b8d3f";
|
this.lastMealColor = "#6b8d3f";
|
||||||
this.eatTimer = Math.max(this.eatTimer, 0.24);
|
this.eatTimer = Math.max(this.eatTimer, 0.24);
|
||||||
|
|
@ -1604,7 +1807,7 @@ class Tarinai {
|
||||||
|
|
||||||
if (it.type === "ball") {
|
if (it.type === "ball") {
|
||||||
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
||||||
if (this.state === "play_ball" || this.target === it || this.personality === "playful") {
|
if (this.state === "play_ball" || this.target === it || this.shouldApplyPersonalityBehavior("openness", 1)) {
|
||||||
this.goodMode = "smile";
|
this.goodMode = "smile";
|
||||||
this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130);
|
this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130);
|
||||||
this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110);
|
this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110);
|
||||||
|
|
@ -1612,9 +1815,17 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (it.type === "splat") {
|
||||||
|
const touch = clamp(1 - d / (this.radius + it.r + 16), 0, 1);
|
||||||
|
if (touch > 0) {
|
||||||
|
this.stress = clamp(this.stress + touch * dt * 2.8 * this.personalityProfile().fear, 0, 130);
|
||||||
|
if (touch > 0.06) this.adjustPersonality("neuroticism", -dt * touch * 0.0042, "after repeated contact with corpses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (it.type === "trace") {
|
if (it.type === "trace") {
|
||||||
this.loneliness -= dt * 0.25;
|
this.loneliness -= dt * 0.25;
|
||||||
this.stress += dt * 0.06;
|
this.stress += dt * 0.06 * this.personalityProfile().fear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.hunger = clamp(this.hunger, 0, 115);
|
this.hunger = clamp(this.hunger, 0, 115);
|
||||||
|
|
@ -1663,12 +1874,12 @@ class Tarinai {
|
||||||
this.loneliness -= dt * 1.65 * this.trait.social;
|
this.loneliness -= dt * 1.65 * this.trait.social;
|
||||||
this.affection += dt * 0.28 * this.trait.social;
|
this.affection += dt * 0.28 * this.trait.social;
|
||||||
this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby");
|
this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby");
|
||||||
if ((rel.affinity || 0) > 10) {
|
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) {
|
||||||
this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130);
|
this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130);
|
||||||
this.affection = clamp(this.affection + dt * 0.18, 0, 80);
|
this.affection = clamp(this.affection + dt * 0.18, 0, 80);
|
||||||
if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation");
|
if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation");
|
||||||
}
|
}
|
||||||
if ((rel.affinity || 0) > 8 && this.world.relationNotice(this.id, o.id, "friend", 32)) {
|
if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) {
|
||||||
this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation");
|
this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation");
|
||||||
}
|
}
|
||||||
if (this.type === "angry") this.stress += dt * 0.08;
|
if (this.type === "angry") this.stress += dt * 0.08;
|
||||||
|
|
@ -1711,10 +1922,14 @@ class Tarinai {
|
||||||
const relB = o.relationTo(this.id);
|
const relB = o.relationTo(this.id);
|
||||||
const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect();
|
const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect();
|
||||||
const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect();
|
const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect();
|
||||||
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || this.personality === "irritable" || o.personality === "irritable" || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
|
const aggressiveThis = this.shouldApplyPersonalityBehavior("aggression", 1);
|
||||||
|
const aggressiveOther = o.shouldApplyPersonalityBehavior?.("aggression", 1);
|
||||||
|
const calmThis = this.shouldApplyPersonalityBehavior("aggression", -1);
|
||||||
|
const calmOther = o.shouldApplyPersonalityBehavior?.("aggression", -1);
|
||||||
|
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
|
||||||
const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
|
const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
|
||||||
const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
|
const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
|
||||||
const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight;
|
const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight * (calmThis ? 0.62 : 1) * (calmOther ? 0.62 : 1);
|
||||||
const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1);
|
const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1);
|
||||||
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
|
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
|
||||||
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
|
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
|
||||||
|
|
@ -1723,7 +1938,7 @@ class Tarinai {
|
||||||
this.world.startConflict(this, o);
|
this.world.startConflict(this, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isZunchiSlave && !o.isZunchiSlave && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) {
|
if (!this.isZunchiSlave && !o.isZunchiSlave && !this.sleepDisease && !o.sleepDisease && !this.fightDisease && !o.fightDisease && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) {
|
||||||
const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44);
|
const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44);
|
||||||
const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24);
|
const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24);
|
||||||
if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) {
|
if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) {
|
||||||
|
|
@ -1733,7 +1948,7 @@ class Tarinai {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeCount > 8) {
|
if (closeCount > 8) {
|
||||||
this.stress += dt * 0.9;
|
this.stress += dt * 0.9 * this.personalityProfile().fear;
|
||||||
this.energy -= dt * 0.25;
|
this.energy -= dt * 0.25;
|
||||||
this.fearTimer = Math.max(this.fearTimer, 0.18);
|
this.fearTimer = Math.max(this.fearTimer, 0.18);
|
||||||
}
|
}
|
||||||
|
|
@ -1755,6 +1970,12 @@ class Tarinai {
|
||||||
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100);
|
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100);
|
||||||
this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
|
this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
|
||||||
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
|
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
|
||||||
|
if (this.target && !this.target.dead && this.target.type === "nest_box") {
|
||||||
|
this.updateNestBoxPresence(dt);
|
||||||
|
this.vx = 0;
|
||||||
|
this.vy = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
|
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
|
||||||
const spot = this.sleepSpotFor(this.target);
|
const spot = this.sleepSpotFor(this.target);
|
||||||
const dx = spot.x - this.x;
|
const dx = spot.x - this.x;
|
||||||
|
|
@ -1828,9 +2049,13 @@ class Tarinai {
|
||||||
const bedDx = this.target.x - this.x;
|
const bedDx = this.target.x - this.x;
|
||||||
const bedDy = this.target.y - this.y;
|
const bedDy = this.target.y - this.y;
|
||||||
const bedDist = Math.hypot(bedDx, bedDy);
|
const bedDist = Math.hypot(bedDx, bedDy);
|
||||||
if (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20)) {
|
const reachedSleepFurniture = this.target.type === "nest_box"
|
||||||
|
? (this.insideNestBoxId === this.target.id || d <= Math.max(this.radius + 34, this.target.r * 1.08) || bedDist <= Math.max(this.target.r * 1.05, 62))
|
||||||
|
: (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20));
|
||||||
|
if (reachedSleepFurniture) {
|
||||||
this.state = "sleep";
|
this.state = "sleep";
|
||||||
this.sleeping = true;
|
this.sleeping = true;
|
||||||
|
if (this.target.type === "nest_box") this.enterNestBox(this.target, dt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1890,8 +2115,13 @@ class Tarinai {
|
||||||
this.vy = this.vy / v * maxV;
|
this.vy = this.vy / v * maxV;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.x += this.vx * dt;
|
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
|
||||||
this.y += this.vy * dt;
|
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
|
||||||
|
this.x += (this.vx + impulseVx) * dt;
|
||||||
|
this.y += (this.vy + impulseVy) * dt;
|
||||||
|
const impulseDrag = Math.pow(0.58, dt * 3.2);
|
||||||
|
this.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag;
|
||||||
|
this.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag;
|
||||||
|
|
||||||
if (this.entryTimer > 0) {
|
if (this.entryTimer > 0) {
|
||||||
this.entryTimer = Math.max(0, this.entryTimer - dt);
|
this.entryTimer = Math.max(0, this.entryTimer - dt);
|
||||||
|
|
@ -1914,7 +2144,7 @@ class Tarinai {
|
||||||
reason = this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
|
reason = this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
|
||||||
}
|
}
|
||||||
else if (this.hunger >= 108) reason = "\u98e2\u9913";
|
else if (this.hunger >= 108) reason = "\u98e2\u9913";
|
||||||
else if (this.age >= this.lifeSpan) reason = "天寿を全うした";
|
else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
|
||||||
else if (this.stress >= 124) {
|
else if (this.stress >= 124) {
|
||||||
reason = this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
reason = this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
||||||
}
|
}
|
||||||
|
|
@ -2032,7 +2262,7 @@ class Tarinai {
|
||||||
const style = {
|
const style = {
|
||||||
parent: { color: "rgba(86,142,232,0.86)", label: "\u89aa" },
|
parent: { color: "rgba(86,142,232,0.86)", label: "\u89aa" },
|
||||||
child: { color: "rgba(93,170,92,0.88)", label: "\u5b50" },
|
child: { color: "rgba(93,170,92,0.88)", label: "\u5b50" },
|
||||||
friend: { color: "rgba(240,91,135,0.90)", label: "\u4ef2\u826f\u3057" },
|
friend: { color: "rgba(240,91,135,0.90)", label: "\u53cb\u9054" },
|
||||||
enemy: { color: "rgba(214,82,66,0.90)", label: "\u6575\u5bfe" },
|
enemy: { color: "rgba(214,82,66,0.90)", label: "\u6575\u5bfe" },
|
||||||
}[observeKind] || { color: "rgba(255,250,220,0.78)", label: "" };
|
}[observeKind] || { color: "rgba(255,250,220,0.78)", label: "" };
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -2070,27 +2300,40 @@ class Tarinai {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ctx.rotate(angle);
|
|
||||||
ctx.globalAlpha *= nestHideAlpha;
|
|
||||||
if (insideCardboard) ctx.globalAlpha *= 0.58;
|
|
||||||
|
|
||||||
|
|
||||||
if (this.world.selected === this) {
|
if (this.world.selected === this) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = 0.85;
|
const pulse = 0.5 + 0.5 * Math.sin(this.world.time * 7.5);
|
||||||
ctx.strokeStyle = "rgba(165, 220, 98, 0.92)";
|
const jumpPulse = clamp(this.focusPulseTimer || 0, 0, 1);
|
||||||
ctx.lineWidth = 2;
|
ctx.globalAlpha = (0.82 + pulse * 0.12) * nestHideAlpha;
|
||||||
|
ctx.shadowColor = "rgba(124, 210, 76, 0.92)";
|
||||||
|
ctx.shadowBlur = 14 + 18 * Math.max(pulse, jumpPulse);
|
||||||
|
ctx.strokeStyle = "rgba(112, 198, 70, 0.98)";
|
||||||
|
ctx.lineWidth = 4.2 + jumpPulse * 2.0;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.ellipse(0, drawH * 0.14, drawW * 0.62, drawH * 0.48, 0, 0, Math.PI * 2);
|
ctx.ellipse(0, drawH * 0.14, drawW * (0.74 + jumpPulse * 0.10), drawH * (0.56 + jumpPulse * 0.08), 0, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([5, 4]);
|
ctx.shadowBlur = 0;
|
||||||
ctx.lineWidth = 1.2;
|
ctx.setLineDash([9, 5]);
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.86)";
|
ctx.lineWidth = 2.0;
|
||||||
|
ctx.strokeStyle = "rgba(255,255,255,0.96)";
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.ellipse(0, drawH * 0.14, drawW * 0.72, drawH * 0.56, 0, 0, Math.PI * 2);
|
ctx.ellipse(0, drawH * 0.14, drawW * 0.88, drawH * 0.68, 0, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle = "rgba(255,252,232,0.94)";
|
||||||
|
ctx.strokeStyle = "rgba(112,198,70,0.88)";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
const focusLabel = "\u6ce8\u76ee";
|
||||||
|
ctx.font = `${Math.max(11, this.radius * 0.54)}px Yomogi, ui-rounded, sans-serif`;
|
||||||
|
const lw = ctx.measureText(focusLabel).width + 18;
|
||||||
|
const ly = -drawH * 0.64;
|
||||||
|
tarinaiRoundRectPath(ctx, -lw / 2, ly - 11, lw, 22, 11);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = "rgba(72,126,42,0.96)";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(focusLabel, 0, ly);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
} else if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) {
|
} else if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -2103,6 +2346,11 @@ class Tarinai {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.rotate(angle);
|
||||||
|
ctx.globalAlpha *= nestHideAlpha;
|
||||||
|
if (insideCardboard) ctx.globalAlpha *= 0.58;
|
||||||
|
|
||||||
|
|
||||||
if (this.eatTimer > 0.02) {
|
if (this.eatTimer > 0.02) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = 0.46 * (this.eatTimer / 0.32);
|
ctx.globalAlpha = 0.46 * (this.eatTimer / 0.32);
|
||||||
|
|
@ -2235,16 +2483,17 @@ class Tarinai {
|
||||||
};
|
};
|
||||||
if (stressActive) {
|
if (stressActive) {
|
||||||
const pct = clamp((this.stress || 0) / 130, 0, 1);
|
const pct = clamp((this.stress || 0) / 130, 0, 1);
|
||||||
drawMeter(baseY - (hpActive ? 11 : 0), pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1));
|
drawMeter(baseY, pct, pct > 0.72 ? "rgba(178,74,110,0.92)" : "rgba(212,126,84,0.90)", clamp((this.stressBarTimer || 0) / 0.7, 0.35, 1));
|
||||||
}
|
}
|
||||||
if (hpActive) {
|
if (hpActive) {
|
||||||
const pct = clamp(this.energy / 100, 0, 1);
|
const pct = clamp(this.energy / 100, 0, 1);
|
||||||
drawMeter(baseY, pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1));
|
drawMeter(baseY - (stressActive ? 11 : 0), pct, pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"), clamp((this.hpBarTimer || 0) / 0.7, 0.35, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contains(x, y) {
|
contains(x, y) {
|
||||||
|
if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false;
|
||||||
const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9;
|
const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9;
|
||||||
return distXY(this.x, this.y, x, y) < hit;
|
return distXY(this.x, this.y, x, y) < hit;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,54 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const TEXT_CATALOG = {
|
const TEXT_CATALOG = {
|
||||||
toolTips: {
|
toolTips: toolTipsFromDefinitions(),
|
||||||
observe: "\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: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002",
|
|
||||||
poke: "たりないをつつく。",
|
|
||||||
pinch: "たりないや物を移動させる。",
|
|
||||||
new: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002",
|
|
||||||
zunchi: "たりないのうんち。病気を生み出すかも。",
|
|
||||||
sweet: "たりないの大好物。一部の病気を治せる。",
|
|
||||||
love_mochi: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002",
|
|
||||||
fight_mochi: "食後しばらくの間けんかが起きやすくなる。",
|
|
||||||
water: "\u98f2\u3080\u3068\u6c17\u5206\u56de\u5fa9\u3002\u305a\u3093\u3061\u75c5\u3068\u7206\u767a\u75c5\u3092\u6cbb\u3059\u3002",
|
|
||||||
water_bowl: "衛生的な水が入った皿。一部の病気を治せる。",
|
|
||||||
sleep_drug: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002",
|
|
||||||
grass: "食べ物。たりないが安心する。",
|
|
||||||
stone: "あぶない。",
|
|
||||||
bed: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002",
|
|
||||||
nest_box: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002",
|
|
||||||
water_hose: "ドラッグで汚れをきれいにする。",
|
|
||||||
ball: "たりないが遊ぶたのしいおもちゃ。",
|
|
||||||
firecracker: "爆発を起こす。",
|
|
||||||
fence_v: "たりないを通せんぼする。",
|
|
||||||
fence_h: "たりないを通せんぼする。"
|
|
||||||
},
|
|
||||||
ecologyCards: [
|
ecologyCards: [
|
||||||
{
|
{
|
||||||
"title": "\u305f\u308a\u306a\u3044\u3068\u306f",
|
title: "\u305f\u308a\u306a\u3044\u3068\u306f",
|
||||||
"image": "assets/sprites/tarinai_01_smile.png",
|
image: "assets/sprites/tarinai_01_smile.png",
|
||||||
"text": "\u5ead\u3067\u52dd\u624b\u306b\u66ae\u3089\u3059\u5c0f\u3055\u306a\u751f\u304d\u7269\u3002\u304a\u306a\u304b\u3001\u7720\u6c17\u3001\u6c17\u5206\u3001\u30b9\u30c8\u30ec\u30b9\u3001\u4f53\u529b\u3067\u884c\u52d5\u304c\u5909\u308f\u308b\u3002"
|
text: "\u5ead\u3067\u66ae\u3089\u3059\u5c0f\u3055\u306a\u751f\u304d\u7269\u3002\u7a7a\u8179\u3001\u7720\u6c17\u3001\u6c17\u5206\u3001\u30b9\u30c8\u30ec\u30b9\u3001\u4f53\u529b\u3067\u884c\u52d5\u304c\u5909\u308f\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "体力とストレス",
|
title: "\u4f53\u529b\u3068\u30b9\u30c8\u30ec\u30b9",
|
||||||
"image": "assets/ui/ecology_hp_stress.png",
|
image: "assets/ui/ecology_hp_stress.png",
|
||||||
"text": "体力はけがや衝突、爆発、空腹などで減り、0になると死亡する。ストレスは恐怖、混雑、汚れ、けんかなどで増え、高すぎると行動が乱れたり命に関わる。体力バーとストレスバーは、状態が変化した直後や観察時に表示される。"
|
text: "\u30d0\u30fc\u306f\u4e0a\u304c\u4f53\u529b\u3001\u4e0b\u304c\u30b9\u30c8\u30ec\u30b9\u3002\u4f53\u529b\u306f\u3051\u304c\u3084\u4e8b\u6545\u3067\u6e1b\u308a\u30010\u3067\u6b7b\u4ea1\u3002\u30b9\u30c8\u30ec\u30b9\u306f\u6050\u6016\u3001\u6df7\u96d1\u3001\u6c5a\u308c\u3001\u3051\u3093\u304b\u3067\u5897\u3048\u3001\u9ad8\u3044\u3068\u884c\u52d5\u304c\u4e71\u308c\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u66ae\u3089\u3057\u3068\u6b32\u6c42",
|
title: "\u66ae\u3089\u3057\u3068\u6b32\u6c42",
|
||||||
"image": "assets/sprites/tarinai_05_drool.png",
|
image: "assets/sprites/tarinai_05_drool.png",
|
||||||
"text": "\u98df\u3079\u7269\u3001\u6c34\u3001\u5bdd\u5e8a\u3001\u5de3\u7bb1\u3001\u4ef2\u9593\u3092\u63a2\u3057\u3066\u52d5\u304f\u3002\u96e8\u3084\u6c17\u6e29\u3001\u8349\u3001\u6c5a\u308c\u3001\u9053\u5177\u3082\u65e5\u3005\u306e\u72b6\u614b\u306b\u5f71\u97ff\u3059\u308b\u3002"
|
text: "\u98df\u3079\u7269\u3001\u6c34\u3001\u5bdd\u5e8a\u3001\u5de3\u7bb1\u3001\u4ef2\u9593\u3092\u63a2\u3057\u3066\u52d5\u304f\u3002\u96e8\u3001\u6c17\u6e29\u3001\u8349\u3001\u6c5a\u308c\u3001\u9053\u5177\u3082\u72b6\u614b\u306b\u5f71\u97ff\u3059\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u6027\u683c\u3068\u500b\u6027",
|
title: "\u6027\u683c\u3068\u500b\u6027",
|
||||||
"image": "assets/sprites/tarinai_21_normal_happy.png",
|
image: "assets/sprites/tarinai_21_normal_happy.png",
|
||||||
"text": "\u81c6\u75c5\u3001\u98df\u3044\u3057\u3093\u574a\u3001\u7720\u305f\u304c\u308a\u3001\u5bc2\u3057\u304c\u308a\u306a\u3069\u306e\u6027\u683c\u304c\u3042\u308b\u3002\u6027\u683c\u306f\u6050\u6016\u3001\u55a7\u5629\u3001\u7761\u7720\u3001\u4ef2\u9593\u3078\u306e\u5bc4\u308a\u65b9\u306b\u73fe\u308c\u308b\u3002"
|
text: "\u653b\u6483\u6027\u3001\u958b\u653e\u6027\u3001\u793e\u4ea4\u6027\u3001\u795e\u7d4c\u8cea\u6027\u304c\u7d4c\u9a13\u3067\u5c11\u3057\u305a\u3064\u5909\u308f\u308b\u3002\u95be\u5024\u3092\u8d85\u3048\u308b\u3068\u6027\u683c\u30bf\u30b0\u3068\u884c\u52d5\u5dee\u306b\u51fa\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u5bb6\u65cf\u3068\u95a2\u4fc2",
|
title: "\u5bb6\u65cf\u3068\u95a2\u4fc2",
|
||||||
"image": "assets/sprites/tarinai_27_birth_ritual.png",
|
image: "assets/sprites/tarinai_27_birth_ritual.png",
|
||||||
"text": "\u89aa\u5b50\u3001\u5171\u540c\u89aa\u3001\u4ef2\u826f\u3057\u3001\u82e6\u624b\u3001\u6050\u6016\u3092\u8a18\u9332\u3059\u308b\u3002\u5b50\u306f\u89aa\u306e\u7247\u65b9\u306e\u6027\u683c\u3092\u7d99\u304e\u3001\u5bb6\u7cfb\u56f3\u306b\u4e16\u4ee3\u304c\u6b8b\u308b\u3002"
|
text: "\u89aa\u5b50\u3001\u5171\u540c\u89aa\u3001\u53cb\u9054\u3001\u6575\u5bfe\u3001\u6050\u6016\u3092\u8a18\u9332\u3059\u308b\u3002\u5b50\u306f\u89aa\u306e\u73fe\u5728\u6027\u683c\u3092\u53d7\u3051\u7d99\u304e\u3001\u5bb6\u7cfb\u56f3\u306b\u4e16\u4ee3\u304c\u6b8b\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u75c5\u6c17\u3068\u6cbb\u7642",
|
title: "\u75c5\u6c17\u3068\u6cbb\u7642",
|
||||||
"image": "assets/sprites/tarinai_24_stress_dizzy.png",
|
image: "assets/sprites/tarinai_24_stress_dizzy.png",
|
||||||
"text": "\u305a\u3093\u3061\u75c5\u3001\u306d\u3080\u308a\u75c5\u3001\u7206\u767a\u75c5\u3001きずつき病\u304c\u3042\u308b\u3002\u6c34\u3067\u6cbb\u308b\u75c5\u6c17\u306e\u500b\u4f53\u306f\u6c34\u3092\u3001\u305a\u3093\u3060\u9905\u3067\u6cbb\u308b\u75c5\u6c17\u306e\u500b\u4f53\u306f\u305a\u3093\u3060\u9905\u3092\u512a\u5148\u3059\u308b\u3002"
|
text: "\u305a\u3093\u3061\u75c5\u3001\u306d\u3080\u308a\u75c5\u3001\u7206\u767a\u75c5\u3001\u304d\u305a\u3064\u304d\u75c5\u304c\u3042\u308b\u3002\u75c5\u6c17\u3054\u3068\u306b\u6c34\u3084\u305a\u3093\u3060\u9905\u306a\u3069\u3092\u6c42\u3081\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u55a7\u5629\u3068\u4e8b\u6545",
|
title: "\u3051\u3093\u304b\u3068\u4e8b\u6545",
|
||||||
"image": "assets/sprites/tarinai_26_intimidate.png",
|
image: "assets/sprites/tarinai_26_intimidate.png",
|
||||||
"text": "\u5a01\u5687\u3001\u6d41\u8840\u3001\u7206\u7af9\u3001\u30dc\u30fc\u30eb\u3001\u843d\u4e0b\u7269\u3067\u50b7\u3064\u304f\u3002\u6b7b\u56e0\u306f\u55a7\u5629\u3001\u98e2\u9913\u3001\u75c5\u6c17\u3001\u30b9\u30c8\u30ec\u30b9\u904e\u591a\u3001\u7206\u7af9\u3001\u4e8b\u6545\u306a\u3069\u306b\u5206\u304b\u308c\u308b\u3002"
|
text: "\u5a01\u5687\u3001\u6d41\u8840\u3001\u7206\u7af9\u3001\u30dc\u30fc\u30eb\u3001\u843d\u4e0b\u7269\u3067\u50b7\u3064\u304f\u3002\u6b7b\u56e0\u306f\u3051\u3093\u304b\u3001\u98e2\u9913\u3001\u75c5\u6c17\u3001\u4e8b\u6545\u306a\u3069\u306b\u5206\u304b\u308c\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u9053\u5177\u3068\u4ecb\u5165",
|
title: "\u9053\u5177\u3068\u4ecb\u5165",
|
||||||
"image": "assets/sprites/tarinai_18_fear.png",
|
image: "assets/sprites/tarinai_18_fear.png",
|
||||||
"text": "\u3064\u3064\u304f\u3001\u3064\u307e\u3080\u3001\u9905\u3001\u6c34\u3001\u6d17\u6d44\u3001\u5de3\u7bb1\u3001\u67f5\u3001\u7206\u7af9\u306a\u3069\u3067\u74b0\u5883\u3092\u4f5c\u308b\u3002\u9053\u5177\u306f\u52a9\u3051\u306b\u3082\u5371\u967a\u306b\u3082\u306a\u308b\u3002"
|
text: "\u3064\u3064\u304f\u3001\u3064\u307e\u3080\u3001\u9905\u3001\u6c34\u3001\u6d17\u6d44\u3001\u5de3\u7bb1\u3001\u67f5\u3001\u7206\u7af9\u306a\u3069\u3067\u74b0\u5883\u3092\u4f5c\u308b\u3002\u52a9\u3051\u306b\u3082\u5371\u967a\u306b\u3082\u306a\u308b\u3002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "\u89b3\u5bdf\u306e\u65b9\u6cd5",
|
title: "\u89b3\u5bdf\u306e\u65b9\u6cd5",
|
||||||
"image": "assets/sprites/tarinai_11_weak.png",
|
image: "assets/sprites/tarinai_11_weak.png",
|
||||||
"text": "\u305f\u308a\u306a\u3044\u9054\u306e\u72b6\u6cc1\u3092\u89b3\u5bdf\u30c4\u30fc\u30eb\u3001\u661f\u30de\u30fc\u30af\u3001\u89b3\u5bdf\u30ed\u30b0\u3001\u5bb6\u7cfb\u56f3\u3001\u6b7b\u56e0\u7b49\u304b\u3089\u77e5\u308b\u3053\u3068\u304c\u51fa\u6765\u308b\u3002"
|
text: "\u89b3\u5bdf\u30c4\u30fc\u30eb\u3001\u661f\u30de\u30fc\u30af\u3001\u89b3\u5bdf\u30ed\u30b0\u3001\u5bb6\u7cfb\u56f3\u3001\u6b7b\u56e0\u7d71\u8a08\u304b\u3089\u500b\u4f53\u306e\u5909\u5316\u3092\u8ffd\u3048\u308b\u3002",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
window.TEXT_CATALOG = TEXT_CATALOG;
|
window.TEXT_CATALOG = TEXT_CATALOG;
|
||||||
|
|
|
||||||
179
js/ui.js
179
js/ui.js
|
|
@ -136,6 +136,100 @@ function relationIconHtml(id) {
|
||||||
return `<img class="relation-face" src="${tarinaiSpritePathForId(id)}" alt="">`;
|
return `<img class="relation-face" src="${tarinaiSpritePathForId(id)}" alt="">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function relationEntries(t, kind = "friend") {
|
||||||
|
const rows = [];
|
||||||
|
const liveIds = new Set((world?.tarinai || []).filter(o => o && !o.dead && o !== t).map(o => o.id));
|
||||||
|
for (const [id, rel] of Object.entries(t?.relationships || {})) {
|
||||||
|
if (!liveIds.has(id)) continue;
|
||||||
|
const score = kind === "enemy" ? (rel.fear || 0) : (rel.affinity || 0);
|
||||||
|
const threshold = kind === "enemy" ? 0.5 : FRIEND_AFFINITY_THRESHOLD;
|
||||||
|
if (score <= threshold) continue;
|
||||||
|
rows.push({ id, score, name: relationDisplayName(world, id) });
|
||||||
|
}
|
||||||
|
rows.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name, "ja"));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationListHtml(t, kind = "friend") {
|
||||||
|
const rows = relationEntries(t, kind);
|
||||||
|
if (!rows.length) return "\u306a\u3057";
|
||||||
|
const sign = kind === "friend" ? "+" : "";
|
||||||
|
return `<div class="relation-chip-list">${rows.map(r => `
|
||||||
|
<button type="button" class="relation-chip ${kind === "enemy" ? "enemy" : "friend"}" data-focus-tarinai-id="${escapeHtml(r.id)}" title="${escapeHtml(r.name)}\u3078\u79fb\u52d5">
|
||||||
|
${relationIconHtml(r.id)}
|
||||||
|
<span class="relation-chip-name">${escapeHtml(r.name)}</span>
|
||||||
|
<em>${sign}${fmt(r.score)}</em>
|
||||||
|
</button>`).join("")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusTarinaiById(id) {
|
||||||
|
const t = world?.tarinai?.find(o => o && !o.dead && o.id === id);
|
||||||
|
if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return false; }
|
||||||
|
world.selected = t;
|
||||||
|
t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.25);
|
||||||
|
selectTool("observe");
|
||||||
|
if (Number.isFinite(t.x) && Number.isFinite(t.y)) {
|
||||||
|
world.cameraX = t.x - (world.visibleWorldW ? world.visibleWorldW() : (world.viewportW || world.w)) / 2;
|
||||||
|
world.cameraY = t.y - (world.visibleWorldH ? world.visibleWorldH() : (world.viewportH || world.h)) / 2;
|
||||||
|
world.clampCamera?.();
|
||||||
|
}
|
||||||
|
uiCache.selectedSnapshot = "";
|
||||||
|
renderSelected();
|
||||||
|
renderStats();
|
||||||
|
render?.();
|
||||||
|
showToast(`\u300c${t.name}\u300d\u3078\u79fb\u52d5\u3057\u307e\u3057\u305f\u3002`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteNameInRecordText(text, name) {
|
||||||
|
const raw = String(text || "");
|
||||||
|
const n = String(name || "").trim();
|
||||||
|
if (!n) return raw;
|
||||||
|
const parts = raw.split(n);
|
||||||
|
if (parts.length <= 1) return raw;
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const before = parts[i].slice(-1);
|
||||||
|
const after = parts[i + 1].charAt(0);
|
||||||
|
out += parts[i] + ((before === "\u300c" && after === "\u300d") ? n : `\u300c${n}\u300d`);
|
||||||
|
}
|
||||||
|
return out + parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameSelectedTarinai(rawName) {
|
||||||
|
const t = world.selected;
|
||||||
|
if (!t || t.dead || !world.tarinai.includes(t)) return false;
|
||||||
|
const next = String(rawName || "").replace(/\s+/g, " ").trim().slice(0, 18);
|
||||||
|
if (!next) {
|
||||||
|
uiCache.selectedSnapshot = "";
|
||||||
|
renderSelected();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (next === t.name) return false;
|
||||||
|
const oldName = t.name;
|
||||||
|
t.name = next;
|
||||||
|
if (t.isZunchiSlave) t.formerName = next;
|
||||||
|
world.recordFamily?.(t);
|
||||||
|
world.markFamilyTreeDirty?.("rename");
|
||||||
|
uiCache.archiveFamilyVersion = null;
|
||||||
|
uiCache.selectedSnapshot = "";
|
||||||
|
renderSelected();
|
||||||
|
renderArchive?.();
|
||||||
|
render?.();
|
||||||
|
showToast(`\u300c${oldName}\u300d\u3092\u300c${next}\u300d\u306b\u6539\u540d\u3057\u307e\u3057\u305f\u3002`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityValueText(value) {
|
||||||
|
const n = clamp(Number(value) || 0, -1, 1);
|
||||||
|
return `${n >= 0 ? "+" : ""}${n.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personalityRowsHtml(personality = {}) {
|
||||||
|
const p = normalizePersonality(personality);
|
||||||
|
return PERSONALITY_KEYS.map(key => `<span>${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}</span>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function renderSelected() {
|
function renderSelected() {
|
||||||
const t = world.selected;
|
const t = world.selected;
|
||||||
if (!t || t.dead || !world.tarinai.includes(t)) {
|
if (!t || t.dead || !world.tarinai.includes(t)) {
|
||||||
|
|
@ -150,17 +244,28 @@ function renderSelected() {
|
||||||
|
|
||||||
ui.selectedCard?.classList.remove("hidden");
|
ui.selectedCard?.classList.remove("hidden");
|
||||||
uiCache.selectedEmpty = false;
|
uiCache.selectedEmpty = false;
|
||||||
|
const activeNameInput = document.activeElement?.matches?.("[data-selected-name]") && document.activeElement?.dataset?.selectedId === t.id;
|
||||||
|
if (activeNameInput) return;
|
||||||
const rel = t.relationSummary ? t.relationSummary() : {};
|
const rel = t.relationSummary ? t.relationSummary() : {};
|
||||||
const fightRecord = relationFightRecord(t);
|
const fightRecord = relationFightRecord(t);
|
||||||
|
const generationDisplay = t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : "");
|
||||||
|
ensurePersonality(t);
|
||||||
|
const personalityTags = t.personalityTags ? t.personalityTags() : getPersonalityTraitTags(t);
|
||||||
|
const personalitySnapshot = [
|
||||||
|
...PERSONALITY_KEYS.map(key => personalityValueText(t.birthPersonality?.[key])),
|
||||||
|
...PERSONALITY_KEYS.map(key => personalityValueText(t.currentPersonality?.[key])),
|
||||||
|
personalityTags.join(",")
|
||||||
|
].join(",");
|
||||||
const snapshot = [
|
const snapshot = [
|
||||||
t.id, t.type, t.name, (t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : "")), t.state, t.personality, t.thought, targetLabel(t.target),
|
t.id, t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target),
|
||||||
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
|
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
|
||||||
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
||||||
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
||||||
rel.friend?.id || "", rel.friend?.score || 0, rel.friend?.id ? tarinaiSpritePathForId(rel.friend.id) : "", rel.fear?.id || "", rel.fear?.score || 0, rel.fear?.id ? tarinaiSpritePathForId(rel.fear.id) : "", fightRecord,
|
relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightRecord,
|
||||||
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
||||||
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0),
|
t.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,
|
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
|
||||||
|
personalitySnapshot,
|
||||||
(t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~")
|
(t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~")
|
||||||
].join("|");
|
].join("|");
|
||||||
if (uiCache.selectedSnapshot === snapshot) return;
|
if (uiCache.selectedSnapshot === snapshot) return;
|
||||||
|
|
@ -168,21 +273,36 @@ function renderSelected() {
|
||||||
|
|
||||||
ui.selectedInfo.className = "selected-info";
|
ui.selectedInfo.className = "selected-info";
|
||||||
const effectBadges = [];
|
const effectBadges = [];
|
||||||
effectBadges.push(`<span class="badge">\u6027\u683c ${escapeHtml(t.personalityLabel ? t.personalityLabel() : t.personality || "\u4e0d\u660e")}</span>`);
|
|
||||||
if (t.isZunchiSlave) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u3069\u308c\u3044</span>`);
|
if (t.isZunchiSlave) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u3069\u308c\u3044</span>`);
|
||||||
if (t.favorite) effectBadges.push(`<span class="badge">\u2605</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.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>`);
|
if (t.sleepDisease) effectBadges.push(`<span class="badge">\u306d\u3080\u308a\u75c5</span>`);
|
||||||
if (t.explosionDisease) effectBadges.push(`<span class="badge">\u7206\u767a\u75c5 ${fmt(t.explosionDiseaseTimer || 0)}</span>`);
|
if (t.explosionDisease) effectBadges.push(`<span class="badge">\u7206\u767a\u75c5 ${fmt(t.explosionDiseaseTimer || 0)}</span>`);
|
||||||
if (t.fightDisease) effectBadges.push(`<span class="badge">きずつき病</span>`);
|
if (t.fightDisease) effectBadges.push(`<span class="badge">\u304d\u305a\u3064\u304d\u75c5</span>`);
|
||||||
if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}</span>`);
|
if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}</span>`);
|
||||||
if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}</span>`);
|
if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}</span>`);
|
||||||
const recordHtml = (t.records || []).slice(0, 8).map(r => `<li><span>${escapeHtml(world.clockString ? world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0) : formatRecordTime(r.time || 0))}</span><strong>${escapeHtml(r.text || "")}</strong></li>`).join("") || `<li><span>-</span><strong>\u8a18\u9332\u306a\u3057</strong></li>`;
|
const personalityTagHtml = personalityTags.length
|
||||||
|
? personalityTags.map(tag => `<span class="personality-tag">${escapeHtml(tag)}</span>`).join("")
|
||||||
|
: `<span class="personality-tag neutral">\u4e2d\u7acb</span>`;
|
||||||
|
const personalityHtml = `
|
||||||
|
<div class="info-row personality-values"><span>\u6027\u683c\u30bf\u30b0</span><strong>${personalityTagHtml}</strong></div>
|
||||||
|
<div class="info-row personality-values"><span>\u8a95\u751f\u6642\u6027\u683c</span><strong>${personalityRowsHtml(t.birthPersonality)}</strong></div>
|
||||||
|
<div class="info-row personality-values"><span>\u73fe\u5728\u6027\u683c</span><strong>${personalityRowsHtml(t.currentPersonality)}</strong></div>
|
||||||
|
`;
|
||||||
|
const recordHtml = (t.records || []).slice(0, 8).map(r => {
|
||||||
|
const timeText = world.clockString ? (world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0)) : formatRecordTime(r.time || 0);
|
||||||
|
const recordText = quoteNameInRecordText(r.text || "", t.name);
|
||||||
|
return `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(recordText)}</strong></li>`;
|
||||||
|
}).join("") || `<li><span>-</span><strong>\u8a18\u9332\u306a\u3057</strong></li>`;
|
||||||
ui.selectedInfo.innerHTML = `
|
ui.selectedInfo.innerHTML = `
|
||||||
<div><span class="badge">${escapeHtml(t.label())}</span><span class="badge">${escapeHtml(t.name)}</span>${(t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : null)) ? `<span class="badge">\u4e16\u4ee3 ${t.generation}</span>` : ""}${effectBadges.join("")}</div>
|
<div class="selected-name-editor">
|
||||||
<div class="info-row"><span>\u6027\u683c</span><strong>${escapeHtml(t.personalityLabel ? t.personalityLabel() : t.personality || "\u4e0d\u660e")}</strong></div>
|
<input class="selected-name-input" data-selected-name data-selected-id="${escapeHtml(t.id)}" value="${escapeHtml(t.name)}" maxlength="18" aria-label="\u305f\u308a\u306a\u3044\u306e\u540d\u524d" title="\u540d\u524d\u3092\u7de8\u96c6">
|
||||||
|
</div>
|
||||||
|
${effectBadges.length ? `<div class="selected-effects">${effectBadges.join("")}</div>` : ""}
|
||||||
|
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
|
||||||
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
||||||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
|
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</strong></div>
|
||||||
|
${personalityHtml}
|
||||||
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
|
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
|
||||||
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
||||||
<div class="info-row"><span>\u7a7a\u8179</span><strong>${fmt(t.hunger)}</strong></div>
|
<div class="info-row"><span>\u7a7a\u8179</span><strong>${fmt(t.hunger)}</strong></div>
|
||||||
|
|
@ -191,8 +311,8 @@ function renderSelected() {
|
||||||
<div class="info-row"><span>\u30b9\u30c8\u30ec\u30b9</span><strong>${fmt(t.stress)}</strong></div>
|
<div class="info-row"><span>\u30b9\u30c8\u30ec\u30b9</span><strong>${fmt(t.stress)}</strong></div>
|
||||||
<div class="info-row"><span>\u305a\u3093\u3061\u6c5a\u308c</span><strong>${fmt(t.zunchiStain || 0)}</strong></div>
|
<div class="info-row"><span>\u305a\u3093\u3061\u6c5a\u308c</span><strong>${fmt(t.zunchiStain || 0)}</strong></div>
|
||||||
<div class="info-row"><span>\u6c17\u5206</span><strong>${fmt(t.mood)}</strong></div>
|
<div class="info-row"><span>\u6c17\u5206</span><strong>${fmt(t.mood)}</strong></div>
|
||||||
<div class="info-row relation-row"><span>\u4ef2\u826f\u3057</span><strong>${rel.friend ? `${relationIconHtml(rel.friend.id)}${escapeHtml(`${rel.friend.name} +${fmt(rel.friend.score)}`)}` : "\u306a\u3057"}</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"><span>\u907f\u3051\u308b\u76f8\u624b</span><strong>${rel.fear ? `${relationIconHtml(rel.fear.id)}${escapeHtml(`${rel.fear.name} ${fmt(rel.fear.score)}`)}` : "\u306a\u3057"}</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>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
|
<div class="info-row"><span>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
|
||||||
<div class="info-row"><span>\u7d2f\u8a08\u6557\u5317</span><strong>${t.totalFightLosses || 0}</strong></div>
|
<div class="info-row"><span>\u7d2f\u8a08\u6557\u5317</span><strong>${t.totalFightLosses || 0}</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>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||||
|
|
@ -218,7 +338,7 @@ function stateLabel(s) {
|
||||||
play_ball: "\u30dc\u30fc\u30eb\u904a\u3073",
|
play_ball: "\u30dc\u30fc\u30eb\u904a\u3073",
|
||||||
seek_water: "\u6c34\u3092\u63a2\u3059",
|
seek_water: "\u6c34\u3092\u63a2\u3059",
|
||||||
zunchi_sick: "\u305a\u3093\u3061\u75c5",
|
zunchi_sick: "\u305a\u3093\u3061\u75c5",
|
||||||
fight_sick: "きずつき病",
|
fight_sick: "\u304d\u305a\u3064\u304d\u75c5",
|
||||||
system: "\u305a\u3093\u3061\u75c5",
|
system: "\u305a\u3093\u3061\u75c5",
|
||||||
seek_friend: "\u4ef2\u9593\u3092\u63a2\u3059",
|
seek_friend: "\u4ef2\u9593\u3092\u63a2\u3059",
|
||||||
follow_parent: "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f",
|
follow_parent: "\u89aa\u306b\u3064\u3044\u3066\u3044\u304f",
|
||||||
|
|
@ -243,7 +363,7 @@ function reasonLabel(t) {
|
||||||
if (t.state === "birth_ritual") return "\u5b50\u304c\u73fe\u308c\u308b\u524d\u306b\u4f53\u3092\u3086\u3059\u3063\u3066\u3044\u308b";
|
if (t.state === "birth_ritual") return "\u5b50\u304c\u73fe\u308c\u308b\u524d\u306b\u4f53\u3092\u3086\u3059\u3063\u3066\u3044\u308b";
|
||||||
if (t.state === "seek_food") return "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
|
if (t.state === "seek_food") return "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
|
||||||
if (t.state === "seek_water") return t.explosionDisease ? "\u7206\u767a\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u305a\u3093\u3061\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b";
|
if (t.state === "seek_water") return t.explosionDisease ? "\u7206\u767a\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b" : "\u305a\u3093\u3061\u75c5\u3067\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b";
|
||||||
if (t.state === "fight_sick") return "きずつき病\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
if (t.state === "fight_sick") return "\u304d\u305a\u3064\u304d\u75c5\u3067\u5f53\u3066\u3082\u306a\u304f\u5f77\u5fa8\u3063\u3066\u3044\u308b";
|
||||||
if (t.state === "zunchi_sick" || t.state === "system") return "\u305a\u3093\u3061\u75c5";
|
if (t.state === "zunchi_sick" || t.state === "system") return "\u305a\u3093\u3061\u75c5";
|
||||||
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
|
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +371,7 @@ function reasonLabel(t) {
|
||||||
function targetLabel(target) {
|
function targetLabel(target) {
|
||||||
if (!target) return "\u306a\u3057";
|
if (!target) return "\u306a\u3057";
|
||||||
if (target.name) return target.name;
|
if (target.name) return target.name;
|
||||||
if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", sleep_drug: "\u306d\u3080\u308a\u85ac", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", system: "\u305a\u3093\u3061\u75c5", zunchi_sick: "\u305a\u3093\u3061\u75c5", fight_sick: "きずつき病", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type;
|
if (target.type) return { sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", sleep_drug: "\u306d\u3080\u308a\u85ac", food: "\u98df\u4e8b", grass: "\u8349", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", stone: "\u77f3", zunchi: "\u305a\u3093\u3061", system: "\u305a\u3093\u3061\u75c5", zunchi_sick: "\u305a\u3093\u3061\u75c5", fight_sick: "\u304d\u305a\u3064\u304d\u75c5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[target.type] || target.type;
|
||||||
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u30ab\u30fc\u30bd\u30eb";
|
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return "\u30ab\u30fc\u30bd\u30eb";
|
||||||
return "\u4e0d\u660e";
|
return "\u4e0d\u660e";
|
||||||
}
|
}
|
||||||
|
|
@ -366,8 +486,14 @@ function applyToolTips() {
|
||||||
document.body.appendChild(pop);
|
document.body.appendChild(pop);
|
||||||
}
|
}
|
||||||
const showTip = (btn) => {
|
const showTip = (btn) => {
|
||||||
const tip = btn?.dataset?.tip || "";
|
const baseTip = btn?.dataset?.tip || "";
|
||||||
if (!tip) return;
|
if (!baseTip) return;
|
||||||
|
let tip = baseTip;
|
||||||
|
if (btn?.dataset?.tool === "nest_box") {
|
||||||
|
const count = world?.nestBoxToolTipCount?.();
|
||||||
|
if (count) tip = `${baseTip}
|
||||||
|
(${count.current}/${count.max})\u5339`;
|
||||||
|
}
|
||||||
pop.textContent = tip;
|
pop.textContent = tip;
|
||||||
pop.classList.remove("hidden");
|
pop.classList.remove("hidden");
|
||||||
const rect = btn.getBoundingClientRect();
|
const rect = btn.getBoundingClientRect();
|
||||||
|
|
@ -487,7 +613,7 @@ function syncTopButtons() {
|
||||||
if (ui.soundBtn) ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF";
|
if (ui.soundBtn) ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF";
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCALABLE_TOOLS = new Set(["zunchi", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "grass", "stone", "firecracker", "fence_v", "fence_h"]);
|
const SCALABLE_TOOLS = new Set(scalableToolIds());
|
||||||
const SIZE_ORDER = ["small", "medium", "large"];
|
const SIZE_ORDER = ["small", "medium", "large"];
|
||||||
const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||||
|
|
||||||
|
|
@ -569,7 +695,13 @@ function bindUI() {
|
||||||
const t = world.tarinai.find(x => x.id === id && !x.dead);
|
const t = world.tarinai.find(x => x.id === id && !x.dead);
|
||||||
if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return; }
|
if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return; }
|
||||||
world.selected = t;
|
world.selected = t;
|
||||||
|
t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.15);
|
||||||
selectTool("observe");
|
selectTool("observe");
|
||||||
|
if (Number.isFinite(t.x) && Number.isFinite(t.y)) {
|
||||||
|
world.cameraX = t.x - (world.visibleWorldW ? world.visibleWorldW() : (world.viewportW || world.w)) / 2;
|
||||||
|
world.cameraY = t.y - (world.visibleWorldH ? world.visibleWorldH() : (world.viewportH || world.h)) / 2;
|
||||||
|
world.clampCamera?.();
|
||||||
|
}
|
||||||
renderStats();
|
renderStats();
|
||||||
showToast(`${t.name}\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002`);
|
showToast(`${t.name}\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002`);
|
||||||
});
|
});
|
||||||
|
|
@ -582,6 +714,12 @@ function bindUI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.selectedInfo?.addEventListener("click", (e) => {
|
ui.selectedInfo?.addEventListener("click", (e) => {
|
||||||
|
const focusBtn = e.target.closest("[data-focus-tarinai-id]");
|
||||||
|
if (focusBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusTarinaiById(focusBtn.getAttribute("data-focus-tarinai-id"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const btn = e.target.closest("[data-selected-action=\"favorite\"]");
|
const btn = e.target.closest("[data-selected-action=\"favorite\"]");
|
||||||
if (!btn || !world.selected || world.selected.dead) return;
|
if (!btn || !world.selected || world.selected.dead) return;
|
||||||
world.selected.favorite = !world.selected.favorite;
|
world.selected.favorite = !world.selected.favorite;
|
||||||
|
|
@ -590,6 +728,17 @@ function bindUI() {
|
||||||
render();
|
render();
|
||||||
showToast(world.selected.favorite ? "\u304a\u6c17\u306b\u5165\u308a\u306b\u3057\u307e\u3057\u305f\u3002" : "\u304a\u6c17\u306b\u5165\u308a\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002");
|
showToast(world.selected.favorite ? "\u304a\u6c17\u306b\u5165\u308a\u306b\u3057\u307e\u3057\u305f\u3002" : "\u304a\u6c17\u306b\u5165\u308a\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002");
|
||||||
});
|
});
|
||||||
|
ui.selectedInfo?.addEventListener("change", (e) => {
|
||||||
|
const input = e.target.closest("[data-selected-name]");
|
||||||
|
if (!input) return;
|
||||||
|
renameSelectedTarinai(input.value);
|
||||||
|
});
|
||||||
|
ui.selectedInfo?.addEventListener("keydown", (e) => {
|
||||||
|
const input = e.target.closest("[data-selected-name]");
|
||||||
|
if (!input) return;
|
||||||
|
if (e.key === "Enter") { e.preventDefault(); input.blur(); }
|
||||||
|
if (e.key === "Escape") { e.preventDefault(); input.value = world.selected?.name || ""; input.blur(); }
|
||||||
|
});
|
||||||
ui.selectedCloseBtn?.addEventListener("click", () => {
|
ui.selectedCloseBtn?.addEventListener("click", () => {
|
||||||
world.selected = null;
|
world.selected = null;
|
||||||
uiCache.selectedSnapshot = "";
|
uiCache.selectedSnapshot = "";
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ function updateColonyHistory(values) {
|
||||||
uiCache.environmentInitialRow = null;
|
uiCache.environmentInitialRow = null;
|
||||||
uiCache.lastChartDraw = "";
|
uiCache.lastChartDraw = "";
|
||||||
}
|
}
|
||||||
const objectKeys = ["grass", "zunchi"];
|
const objectKeys = ["grass", "zunchi", "trace"];
|
||||||
const makeBucket = (start) => ({
|
const makeBucket = (start) => ({
|
||||||
start,
|
start,
|
||||||
end: start + interval,
|
end: start + interval,
|
||||||
|
|
@ -90,7 +90,7 @@ function updateColonyHistory(values) {
|
||||||
|
|
||||||
function chartRows(values) {
|
function chartRows(values) {
|
||||||
const bucket = uiCache.chartBucketAccum;
|
const bucket = uiCache.chartBucketAccum;
|
||||||
const objectKeys = ["grass", "zunchi"];
|
const objectKeys = ["grass", "zunchi", "trace"];
|
||||||
const preview = bucket && bucket.count > 0 ? {
|
const preview = bucket && bucket.count > 0 ? {
|
||||||
t: Math.max(world.time || 0, bucket.start || 0),
|
t: Math.max(world.time || 0, bucket.start || 0),
|
||||||
pop: bucket.sums.pop / bucket.count,
|
pop: bucket.sums.pop / bucket.count,
|
||||||
|
|
@ -182,6 +182,7 @@ function chartSeries(mode = "population") {
|
||||||
if (mode === "objects") return [
|
if (mode === "objects") return [
|
||||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
||||||
|
{ key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" },
|
||||||
];
|
];
|
||||||
if (mode === "environment") return [
|
if (mode === "environment") return [
|
||||||
{ key: "security", label: "\u6cbb\u5b89", color: "#d65e46", percent: true },
|
{ key: "security", label: "\u6cbb\u5b89", color: "#d65e46", percent: true },
|
||||||
|
|
|
||||||
383
js/ui_family.js
383
js/ui_family.js
|
|
@ -152,6 +152,167 @@ function liveFamilyComponents(family) {
|
||||||
return comps;
|
return comps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lineageIdleSchedule(fn, timeout = 180) {
|
||||||
|
const schedule = window.requestIdleCallback || ((cb) => setTimeout(cb, 0));
|
||||||
|
schedule(fn, { timeout });
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageSameFamilyToken(token) {
|
||||||
|
return uiCache.archiveRenderToken === token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageBuildComponentsIdle(family, token, onProgress, onDone) {
|
||||||
|
family = lineageEdgeFamily(family);
|
||||||
|
const entries = Object.values(family || {}).filter(n => n?.id);
|
||||||
|
const known = new Set(entries.map(n => n.id));
|
||||||
|
const parentsByChild = new Map();
|
||||||
|
const childrenByParent = new Map();
|
||||||
|
const relatedIds = new Set();
|
||||||
|
const graph = new Map();
|
||||||
|
const comps = [];
|
||||||
|
let stage = "relations";
|
||||||
|
let entryIndex = 0;
|
||||||
|
let nodeIndex = 0;
|
||||||
|
let nodes = [];
|
||||||
|
let sortedNodes = [];
|
||||||
|
let dfsIndex = 0;
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
const addParent = (childId, parentId) => {
|
||||||
|
if (!childId || !parentId || childId === parentId || !known.has(childId) || !known.has(parentId)) return;
|
||||||
|
if (!parentsByChild.has(childId)) parentsByChild.set(childId, new Set());
|
||||||
|
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, new Set());
|
||||||
|
parentsByChild.get(childId).add(parentId);
|
||||||
|
childrenByParent.get(parentId).add(childId);
|
||||||
|
};
|
||||||
|
const addGraphEdge = (a, b) => {
|
||||||
|
if (!a || !b || !graph.has(a) || !graph.has(b)) return;
|
||||||
|
graph.get(a).add(b);
|
||||||
|
graph.get(b).add(a);
|
||||||
|
};
|
||||||
|
const validParentChild = (childId, parentId) => {
|
||||||
|
const child = family[childId];
|
||||||
|
const parent = family[parentId];
|
||||||
|
if (!child || !parent) return false;
|
||||||
|
const childListsParent = Array.isArray(child.parents) && child.parents.includes(parentId);
|
||||||
|
const parentListsChild = Array.isArray(parent.children) && parent.children.includes(childId);
|
||||||
|
return childListsParent || parentListsChild;
|
||||||
|
};
|
||||||
|
const finishEmpty = () => {
|
||||||
|
lineageRelationIndexCache = { family, version: world.familyVersion || 0, index: { parentsByChild, childrenByParent } };
|
||||||
|
onDone([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pump = () => {
|
||||||
|
if (!lineageSameFamilyToken(token)) return;
|
||||||
|
const started = performance.now();
|
||||||
|
while (performance.now() - started < 6) {
|
||||||
|
if (stage === "relations") {
|
||||||
|
if (entryIndex >= entries.length) {
|
||||||
|
lineageRelationIndexCache = { family, version: world.familyVersion || 0, index: { parentsByChild, childrenByParent } };
|
||||||
|
stage = "related";
|
||||||
|
entryIndex = 0;
|
||||||
|
onProgress?.("\u95a2\u4fc2\u3092\u6574\u7406\u4e2d", 0, entries.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const n = entries[entryIndex++];
|
||||||
|
for (const parentId of n.parents || []) addParent(n.id, parentId);
|
||||||
|
for (const childId of n.children || []) addParent(childId, n.id);
|
||||||
|
} else if (stage === "related") {
|
||||||
|
if (entryIndex >= entries.length) {
|
||||||
|
nodes = Array.from(relatedIds).map(id => family[id]).filter(Boolean);
|
||||||
|
for (const n of nodes) graph.set(n.id, new Set());
|
||||||
|
if (!nodes.length) return finishEmpty();
|
||||||
|
stage = "graph";
|
||||||
|
nodeIndex = 0;
|
||||||
|
onProgress?.("\u5bb6\u7cfb\u3092\u7d50\u5408\u4e2d", 0, nodes.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const n = entries[entryIndex++];
|
||||||
|
const parents = parentsByChild.get(n.id) || new Set();
|
||||||
|
const children = childrenByParent.get(n.id) || new Set();
|
||||||
|
if (parents.size || children.size) relatedIds.add(n.id);
|
||||||
|
for (const id of parents) relatedIds.add(id);
|
||||||
|
for (const id of children) relatedIds.add(id);
|
||||||
|
} else if (stage === "graph") {
|
||||||
|
if (nodeIndex >= nodes.length) {
|
||||||
|
sortedNodes = nodes.slice().sort(lineageNodeSort);
|
||||||
|
stage = "components";
|
||||||
|
dfsIndex = 0;
|
||||||
|
onProgress?.("\u5bb6\u7cfb\u3092\u5206\u5272\u4e2d", 0, sortedNodes.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const n = nodes[nodeIndex++];
|
||||||
|
const parentCandidates = new Set((n.parents || []).filter(Boolean));
|
||||||
|
for (const parentId of parentsByChild.get(n.id) || []) parentCandidates.add(parentId);
|
||||||
|
for (const parentId of parentCandidates) {
|
||||||
|
if (parentId !== n.id && graph.has(parentId) && validParentChild(n.id, parentId)) addGraphEdge(n.id, parentId);
|
||||||
|
}
|
||||||
|
const childCandidates = new Set((n.children || []).filter(Boolean));
|
||||||
|
for (const childId of childrenByParent.get(n.id) || []) childCandidates.add(childId);
|
||||||
|
for (const childId of childCandidates) {
|
||||||
|
if (childId !== n.id && graph.has(childId) && validParentChild(childId, n.id)) addGraphEdge(n.id, childId);
|
||||||
|
}
|
||||||
|
} else if (stage === "components") {
|
||||||
|
while (dfsIndex < sortedNodes.length && seen.has(sortedNodes[dfsIndex].id)) dfsIndex += 1;
|
||||||
|
if (dfsIndex >= sortedNodes.length) {
|
||||||
|
comps.sort((a, b) => {
|
||||||
|
const ag = Math.min(...a.map(n => n.generation || 1));
|
||||||
|
const bg = Math.min(...b.map(n => n.generation || 1));
|
||||||
|
const at = Math.min(...a.map(n => n.birthTime || 0));
|
||||||
|
const bt = Math.min(...b.map(n => n.birthTime || 0));
|
||||||
|
return ag - bg || at - bt || b.length - a.length;
|
||||||
|
});
|
||||||
|
onDone(comps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const start = sortedNodes[dfsIndex++];
|
||||||
|
const stack = [start.id];
|
||||||
|
const comp = [];
|
||||||
|
seen.add(start.id);
|
||||||
|
while (stack.length && performance.now() - started < 6) {
|
||||||
|
const id = stack.pop();
|
||||||
|
const item = family[id];
|
||||||
|
if (item) comp.push(item);
|
||||||
|
for (const next of graph.get(id) || []) {
|
||||||
|
if (seen.has(next)) continue;
|
||||||
|
seen.add(next);
|
||||||
|
stack.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stack.length) {
|
||||||
|
const continueComp = () => {
|
||||||
|
if (!lineageSameFamilyToken(token)) return;
|
||||||
|
const resumedStarted = performance.now();
|
||||||
|
while (stack.length && performance.now() - resumedStarted < 6) {
|
||||||
|
const id = stack.pop();
|
||||||
|
const item = family[id];
|
||||||
|
if (item) comp.push(item);
|
||||||
|
for (const next of graph.get(id) || []) {
|
||||||
|
if (seen.has(next)) continue;
|
||||||
|
seen.add(next);
|
||||||
|
stack.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stack.length) return lineageIdleSchedule(continueComp, 180);
|
||||||
|
if (comp.some(x => x.alive)) comps.push(comp);
|
||||||
|
lineageIdleSchedule(pump, 180);
|
||||||
|
};
|
||||||
|
return lineageIdleSchedule(continueComp, 180);
|
||||||
|
}
|
||||||
|
if (comp.some(x => x.alive)) comps.push(comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onProgress?.(
|
||||||
|
stage === "relations" ? "\u95a2\u4fc2\u3092\u6574\u7406\u4e2d" : stage === "related" ? "\u8868\u793a\u5bfe\u8c61\u3092\u9078\u5225\u4e2d" : stage === "graph" ? "\u5bb6\u7cfb\u3092\u7d50\u5408\u4e2d" : "\u5bb6\u7cfb\u3092\u5206\u5272\u4e2d",
|
||||||
|
stage === "graph" ? nodeIndex : entryIndex,
|
||||||
|
stage === "graph" ? nodes.length : entries.length
|
||||||
|
);
|
||||||
|
lineageIdleSchedule(pump, 180);
|
||||||
|
};
|
||||||
|
lineageIdleSchedule(pump, 160);
|
||||||
|
}
|
||||||
|
|
||||||
function familyStatusText(n) {
|
function familyStatusText(n) {
|
||||||
if (!n) return "\u4e0d\u660e";
|
if (!n) return "\u4e0d\u660e";
|
||||||
return n.alive ? "\u751f\u5b58" : "\u6b7b\u4ea1";
|
return n.alive ? "\u751f\u5b58" : "\u6b7b\u4ea1";
|
||||||
|
|
@ -163,8 +324,8 @@ function familyDeathReasonLabel(rawReason, entity = null) {
|
||||||
if (text.includes("\u3067\u8870\u5f31")) return text.length > 12 ? `${text.slice(0, 12)}\u2026` : text;
|
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 (/\u55a7\u5629|fight/.test(text)) return "\u55a7\u5629";
|
||||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9";
|
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9";
|
||||||
if (/仕様により画面外で削除|画面外で削除|out.*bounds|offscreen/.test(text)) return "仕様により画面外で削除";
|
if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664";
|
||||||
if (/衝突|collision/.test(text)) return "衝突";
|
if (/\u885d\u7a81|collision/.test(text)) return "\u885d\u7a81";
|
||||||
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return "\u3064\u3064\u304b\u308c\u3059\u304e\u305f";
|
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return "\u3064\u3064\u304b\u308c\u3059\u304e\u305f";
|
||||||
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return "\u305a\u3093\u3061\u75c5";
|
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return "\u305a\u3093\u3061\u75c5";
|
||||||
if (/\u75c5/.test(text)) {
|
if (/\u75c5/.test(text)) {
|
||||||
|
|
@ -173,7 +334,7 @@ function familyDeathReasonLabel(rawReason, entity = null) {
|
||||||
}
|
}
|
||||||
if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return "\u98e2\u9913";
|
if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return "\u98e2\u9913";
|
||||||
if (/\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
if (/\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
||||||
if (/\u5bff\u547d|天寿を全うした/.test(text)) return "天寿を全うした";
|
if (/\u5bff\u547d|\u5929\u5bff/.test(text)) return "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
|
||||||
if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return "\u4e8b\u6545";
|
if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return "\u4e8b\u6545";
|
||||||
return "\u4e8b\u6545";
|
return "\u4e8b\u6545";
|
||||||
}
|
}
|
||||||
|
|
@ -196,9 +357,7 @@ function lineageTreeNodeHtml(n, x, y) {
|
||||||
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
||||||
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
||||||
const reason = briefReason ? ` / ${briefReason}` : "";
|
const reason = briefReason ? ` / ${briefReason}` : "";
|
||||||
const personality = live?.personalityLabel ? live.personalityLabel() : ((PERSONALITIES[n?.personality] || {}).label || n?.personality || "");
|
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}`);
|
||||||
const personalityPart = personality ? ` / \u6027\u683c ${personality}` : "";
|
|
||||||
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${personalityPart}${reason}`);
|
|
||||||
const deathLine = briefReason ? `<span class="death-reason">${escapeHtml(briefReason)}</span>` : "";
|
const deathLine = briefReason ? `<span class="death-reason">${escapeHtml(briefReason)}</span>` : "";
|
||||||
const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing";
|
const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing";
|
||||||
const childCls = (n?.parents || []).length ? " child" : "";
|
const childCls = (n?.parents || []).length ? " child" : "";
|
||||||
|
|
@ -485,7 +644,8 @@ function lineageRowsForLayout(nodes, generations, idToNode, idSet) {
|
||||||
const partnerEdges = lineagePartnerEdgesByGeneration(nodes, idToNode, idSet);
|
const partnerEdges = lineagePartnerEdgesByGeneration(nodes, idToNode, idSet);
|
||||||
const totalNodes = nodes.length;
|
const totalNodes = nodes.length;
|
||||||
const maxRow = Math.max(0, ...rows.map(row => row.length));
|
const maxRow = Math.max(0, ...rows.map(row => row.length));
|
||||||
const passes = totalNodes > 180 || maxRow > 32 ? 2 : (totalNodes > 90 || maxRow > 18 ? 3 : 6);
|
const veryLarge = totalNodes > 220 || maxRow > 44;
|
||||||
|
const passes = veryLarge ? 1 : (totalNodes > 180 || maxRow > 32 ? 2 : (totalNodes > 90 || maxRow > 18 ? 3 : 6));
|
||||||
|
|
||||||
for (let pass = 0; pass < passes; pass++) {
|
for (let pass = 0; pass < passes; pass++) {
|
||||||
let rank = lineageRankMap(rows);
|
let rank = lineageRankMap(rows);
|
||||||
|
|
@ -495,9 +655,11 @@ function lineageRowsForLayout(nodes, generations, idToNode, idSet) {
|
||||||
const childRank = new Map(rows[i + 1].map((n, index) => [n.id, index]));
|
const childRank = new Map(rows[i + 1].map((n, index) => [n.id, index]));
|
||||||
rows[i] = lineageOrderParentsByChildren(rows[i], idSet, childRank);
|
rows[i] = lineageOrderParentsByChildren(rows[i], idSet, childRank);
|
||||||
}
|
}
|
||||||
rank = lineageRankMap(rows);
|
if (!veryLarge) {
|
||||||
rows = rows.map((row, i) => lineageOrderPartnerClusters(row, partnerEdges.get(generations[i]), rank, idToNode));
|
rank = lineageRankMap(rows);
|
||||||
if (totalNodes <= 90 && maxRow <= 16) rows = lineageOptimizeRows(rows, idSet);
|
rows = rows.map((row, i) => lineageOrderPartnerClusters(row, partnerEdges.get(generations[i]), rank, idToNode));
|
||||||
|
if (totalNodes <= 90 && maxRow <= 16) rows = lineageOptimizeRows(rows, idSet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
@ -880,6 +1042,32 @@ function lineageHydrateFamilyHtml(html, index) {
|
||||||
.split("{{LINEAGE_MASK_ID}}").join(`lineage-node-mask-${index}`);
|
.split("{{LINEAGE_MASK_ID}}").join(`lineage-node-mask-${index}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lineageCaptureArchiveScroll() {
|
||||||
|
const root = ui.archiveContent;
|
||||||
|
if (!root) return null;
|
||||||
|
const familyScroll = {};
|
||||||
|
for (const el of root.querySelectorAll(".lineage-family-tree[data-lineage-key]")) {
|
||||||
|
const key = el.getAttribute("data-lineage-key");
|
||||||
|
if (key) familyScroll[key] = el.scrollLeft || 0;
|
||||||
|
}
|
||||||
|
return { top: root.scrollTop || 0, left: root.scrollLeft || 0, familyScroll };
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageRestoreArchiveScroll(state) {
|
||||||
|
const root = ui.archiveContent;
|
||||||
|
if (!root || !state) return;
|
||||||
|
const maxTop = Math.max(0, root.scrollHeight - root.clientHeight);
|
||||||
|
const maxLeft = Math.max(0, root.scrollWidth - root.clientWidth);
|
||||||
|
root.scrollTop = Math.min(state.top || 0, maxTop);
|
||||||
|
root.scrollLeft = Math.min(state.left || 0, maxLeft);
|
||||||
|
for (const el of root.querySelectorAll(".lineage-family-tree[data-lineage-key]")) {
|
||||||
|
const key = el.getAttribute("data-lineage-key");
|
||||||
|
if (!key || !Object.prototype.hasOwnProperty.call(state.familyScroll || {}, key)) continue;
|
||||||
|
el.scrollLeft = Math.min(state.familyScroll[key] || 0, Math.max(0, el.scrollWidth - el.clientWidth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function lineageExpectedChildEdgeCount(component, family) {
|
function lineageExpectedChildEdgeCount(component, family) {
|
||||||
family = lineageEdgeFamily(family);
|
family = lineageEdgeFamily(family);
|
||||||
const idSet = new Set(component.map(x => x?.id).filter(Boolean));
|
const idSet = new Set(component.map(x => x?.id).filter(Boolean));
|
||||||
|
|
@ -1113,6 +1301,73 @@ function lineageCleanComponent(component, family) {
|
||||||
return expanded.filter(n => lineageMutualParentIds(n, family, idSet).length || lineageMutualChildIds(n, family, idSet).length);
|
return expanded.filter(n => lineageMutualParentIds(n, family, idSet).length || lineageMutualChildIds(n, family, idSet).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lineageFastLayoutNeeded(nodes, idSet, family) {
|
||||||
|
const edgeCount = nodes.reduce((sum, n) => sum + lineageParentIds(n, idSet, family).length, 0);
|
||||||
|
const maxGenerationWidth = Math.max(0, ...Array.from(new Set(nodes.map(n => n.generation || 1))).map(g => nodes.filter(n => (n.generation || 1) === g).length));
|
||||||
|
return {
|
||||||
|
edgeCount,
|
||||||
|
fast: nodes.length > 140 || edgeCount > 260 || maxGenerationWidth > 34,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageLayoutRowsFast(rows, idSet) {
|
||||||
|
const NODE_W = 132;
|
||||||
|
const NODE_H = 68;
|
||||||
|
const SIBLING_GAP = 22;
|
||||||
|
const ROW_GAP = 116;
|
||||||
|
const LEFT_PAD = 82;
|
||||||
|
const TOP_PAD = 44;
|
||||||
|
const RIGHT_PAD = 48;
|
||||||
|
const BOTTOM_PAD = 30;
|
||||||
|
const positions = new Map();
|
||||||
|
let width = 420;
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
const y = TOP_PAD + rowIndex * (NODE_H + ROW_GAP);
|
||||||
|
let x = LEFT_PAD;
|
||||||
|
for (const n of row) {
|
||||||
|
positions.set(n.id, { x, y, rowIndex, node: n });
|
||||||
|
x += NODE_W + SIBLING_GAP;
|
||||||
|
}
|
||||||
|
width = Math.max(width, x - SIBLING_GAP + RIGHT_PAD);
|
||||||
|
});
|
||||||
|
const height = TOP_PAD + Math.max(0, rows.length - 1) * (NODE_H + ROW_GAP) + NODE_H + BOTTOM_PAD;
|
||||||
|
return { positions, width, height, nodeW: NODE_W, nodeH: NODE_H, topPad: TOP_PAD, rowGap: ROW_GAP, fastMode: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineageBuildLinksFast(nodes, idSet, layout) {
|
||||||
|
const { positions, nodeW, nodeH } = layout;
|
||||||
|
const partnerLinks = [];
|
||||||
|
const childLinks = [];
|
||||||
|
const seenPartners = new Set();
|
||||||
|
let childLinkGroups = 0;
|
||||||
|
let childLinkEdges = 0;
|
||||||
|
for (const child of nodes) {
|
||||||
|
const childPos = positions.get(child.id);
|
||||||
|
if (!childPos) continue;
|
||||||
|
const parents = lineageParentIds(child, idSet).map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x);
|
||||||
|
if (!parents.length) continue;
|
||||||
|
childLinkGroups += 1;
|
||||||
|
childLinkEdges += parents.length;
|
||||||
|
const childX = childPos.x + nodeW / 2;
|
||||||
|
const childY = childPos.y;
|
||||||
|
const fromY = Math.max(...parents.map(p => p.y + nodeH));
|
||||||
|
const joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length;
|
||||||
|
if (parents.length >= 2) {
|
||||||
|
const left = parents[0];
|
||||||
|
const right = parents[parents.length - 1];
|
||||||
|
const partnerKey = parents.map(p => p.node?.id).filter(Boolean).sort().join("+");
|
||||||
|
if (partnerKey && !seenPartners.has(partnerKey)) {
|
||||||
|
seenPartners.add(partnerKey);
|
||||||
|
const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
|
||||||
|
partnerLinks.push(`M ${(left.x + nodeW).toFixed(1)} ${y.toFixed(1)} L ${right.x.toFixed(1)} ${y.toFixed(1)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const laneY = Math.min(childY - 18, fromY + Math.max(28, (childY - fromY) * 0.48));
|
||||||
|
childLinks.push(`M ${joinX.toFixed(1)} ${fromY.toFixed(1)} L ${joinX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${childY.toFixed(1)}`);
|
||||||
|
}
|
||||||
|
return { partnerLinks, childLinks, childLinkGroups, childLinkEdges };
|
||||||
|
}
|
||||||
|
|
||||||
function buildLineageTreeLayout(component, family) {
|
function buildLineageTreeLayout(component, family) {
|
||||||
family = lineageEdgeFamily(family);
|
family = lineageEdgeFamily(family);
|
||||||
const cleaned = lineageCleanComponent(component, family);
|
const cleaned = lineageCleanComponent(component, family);
|
||||||
|
|
@ -1121,12 +1376,13 @@ function buildLineageTreeLayout(component, family) {
|
||||||
const idSet = new Set(idToNode.keys());
|
const idSet = new Set(idToNode.keys());
|
||||||
const generations = Array.from(new Set(nodes.map(n => n.generation || 1))).sort((a, b) => a - b);
|
const generations = Array.from(new Set(nodes.map(n => n.generation || 1))).sort((a, b) => a - b);
|
||||||
const rows = lineageRowsForLayout(nodes, generations, idToNode, idSet);
|
const rows = lineageRowsForLayout(nodes, generations, idToNode, idSet);
|
||||||
const layout = lineageLayoutRows(rows, idSet);
|
const perf = lineageFastLayoutNeeded(nodes, idSet, family);
|
||||||
const links = lineageBuildLinks(nodes, idSet, layout);
|
const layout = perf.fast ? lineageLayoutRowsFast(rows, idSet) : lineageLayoutRows(rows, idSet);
|
||||||
|
const links = perf.fast ? lineageBuildLinksFast(nodes, idSet, layout) : lineageBuildLinks(nodes, idSet, layout);
|
||||||
|
|
||||||
const labels = generations.map((g, rowIndex) => ({ generation: g, y: layout.topPad + rowIndex * (layout.nodeH + layout.rowGap) + 18 }));
|
const labels = generations.map((g, rowIndex) => ({ generation: g, y: layout.topPad + rowIndex * (layout.nodeH + layout.rowGap) + 18 }));
|
||||||
const renderedNodes = nodes.map(n => ({ node: n, ...(layout.positions.get(n.id) || {}) })).filter(n => Number.isFinite(n.x) && Number.isFinite(n.y));
|
const renderedNodes = nodes.map(n => ({ node: n, ...(layout.positions.get(n.id) || {}) })).filter(n => Number.isFinite(n.x) && Number.isFinite(n.y));
|
||||||
return { ...layout, labels, nodes: renderedNodes, childLinks: links.childLinks, partnerLinks: links.partnerLinks, childLinkGroups: links.childLinkGroups, childLinkEdges: links.childLinkEdges };
|
return { ...layout, labels, nodes: renderedNodes, childLinks: links.childLinks, partnerLinks: links.partnerLinks, childLinkGroups: links.childLinkGroups, childLinkEdges: links.childLinkEdges, fastMode: perf.fast, edgeCount: perf.edgeCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineageFamilyHtml(component, index, family, signature = "") {
|
function lineageFamilyHtml(component, index, family, signature = "") {
|
||||||
|
|
@ -1146,7 +1402,7 @@ function lineageFamilyHtml(component, index, family, signature = "") {
|
||||||
const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join("");
|
const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join("");
|
||||||
const empty = !layout.childLinks.length ? `<div class="lineage-empty-family">\u89aa\u5b50\u95a2\u4fc2\u306e\u7dda\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>` : "";
|
const empty = !layout.childLinks.length ? `<div class="lineage-empty-family">\u89aa\u5b50\u95a2\u4fc2\u306e\u7dda\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>` : "";
|
||||||
|
|
||||||
const html = `<section class="lineage-family lineage-family-tree" data-lineage-child-edges="${childEdgeCount}" data-lineage-rendered-child-edges="${layout.childLinkEdges || 0}" data-lineage-child-groups="${layout.childLinkGroups || 0}" data-lineage-child-paths="${layout.childLinks.length}">
|
const html = `<section class="lineage-family lineage-family-tree" data-lineage-index="{{LINEAGE_INDEX}}" data-lineage-key="${escapeHtml(cacheKey)}" data-lineage-child-edges="${childEdgeCount}" data-lineage-rendered-child-edges="${layout.childLinkEdges || 0}" data-lineage-child-groups="${layout.childLinkGroups || 0}" data-lineage-child-paths="${layout.childLinks.length}">
|
||||||
<h3>\u5bb6\u7cfb {{LINEAGE_INDEX}} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}</h3>
|
<h3>\u5bb6\u7cfb {{LINEAGE_INDEX}} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}</h3>
|
||||||
<div class="lineage-tree-panel" style="width:${Math.ceil(layout.width)}px;height:${Math.ceil(layout.height)}px">
|
<div class="lineage-tree-panel" style="width:${Math.ceil(layout.width)}px;height:${Math.ceil(layout.height)}px">
|
||||||
<svg class="lineage-links" viewBox="0 0 ${Math.ceil(layout.width)} ${Math.ceil(layout.height)}" aria-hidden="true">
|
<svg class="lineage-links" viewBox="0 0 ${Math.ceil(layout.width)} ${Math.ceil(layout.height)}" aria-hidden="true">
|
||||||
|
|
@ -1165,6 +1421,7 @@ function renderArchive() {
|
||||||
if (uiCache.deferArchiveUntil && performance.now() < uiCache.deferArchiveUntil) return;
|
if (uiCache.deferArchiveUntil && performance.now() < uiCache.deferArchiveUntil) return;
|
||||||
uiCache.deferArchiveUntil = 0;
|
uiCache.deferArchiveUntil = 0;
|
||||||
uiCache.archiveScheduled = false;
|
uiCache.archiveScheduled = false;
|
||||||
|
const scrollState = lineageCaptureArchiveScroll();
|
||||||
const familyVersion = world.familyVersion || 0;
|
const familyVersion = world.familyVersion || 0;
|
||||||
const familyDirty = Boolean(world.familyTreeDirty);
|
const familyDirty = Boolean(world.familyTreeDirty);
|
||||||
if (uiCache.archiveRenderRunning) {
|
if (uiCache.archiveRenderRunning) {
|
||||||
|
|
@ -1197,30 +1454,12 @@ function renderArchive() {
|
||||||
world.pruneExtinctFamilies();
|
world.pruneExtinctFamilies();
|
||||||
}
|
}
|
||||||
const family = world.family || {};
|
const family = world.family || {};
|
||||||
const relationSignature = lineageVisibleRelationSignature(family);
|
const version = String(world.familyVersion || 0);
|
||||||
if (uiCache.archiveVersion && uiCache.archiveRelationSignature === relationSignature) {
|
|
||||||
uiCache.archiveFamilyVersion = world.familyVersion || 0;
|
|
||||||
world.familyTreeDirty = false;
|
|
||||||
uiCache.archivePendingDirtyAfterRun = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ids = Object.keys(family);
|
|
||||||
const edgeCount = ids.reduce((sum, id) => {
|
|
||||||
const n = family[id];
|
|
||||||
return sum + (Array.isArray(n?.parents) ? n.parents.length : 0) + (Array.isArray(n?.children) ? n.children.length : 0);
|
|
||||||
}, 0);
|
|
||||||
const version = `${world.familyVersion || 0}:${ids.length}:${edgeCount}`;
|
|
||||||
if (uiCache.archiveVersion === version) {
|
if (uiCache.archiveVersion === version) {
|
||||||
world.familyTreeDirty = false;
|
world.familyTreeDirty = false;
|
||||||
uiCache.archivePendingDirtyAfterRun = false;
|
uiCache.archivePendingDirtyAfterRun = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const heavyArchive = ids.length > 90 || edgeCount > 160;
|
|
||||||
if (!world.paused && heavyArchive) {
|
|
||||||
uiCache.archivePendingWhileRunning = true;
|
|
||||||
lineageSetArchiveStaleStatus(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const renderStarted = performance.now();
|
const renderStarted = performance.now();
|
||||||
lineageSetArchiveStaleStatus(false);
|
lineageSetArchiveStaleStatus(false);
|
||||||
if (uiCache.archiveRenderTimer) {
|
if (uiCache.archiveRenderTimer) {
|
||||||
|
|
@ -1230,63 +1469,71 @@ function renderArchive() {
|
||||||
uiCache.archiveVersion = version;
|
uiCache.archiveVersion = version;
|
||||||
uiCache.archiveFamilyVersion = world.familyVersion || 0;
|
uiCache.archiveFamilyVersion = world.familyVersion || 0;
|
||||||
uiCache.archiveLastScheduleFamilyVersion = world.familyVersion || 0;
|
uiCache.archiveLastScheduleFamilyVersion = world.familyVersion || 0;
|
||||||
uiCache.archiveRelationSignature = relationSignature;
|
|
||||||
uiCache.archiveLastRenderedAt = performance.now();
|
uiCache.archiveLastRenderedAt = performance.now();
|
||||||
uiCache.lastArchiveWindow = "";
|
uiCache.lastArchiveWindow = "";
|
||||||
const components = liveFamilyComponents(family).flatMap(lineageSplitByActualEdges).filter(c => c.length);
|
|
||||||
uiCache.archiveRows = components;
|
|
||||||
const signatures = components.map(component => lineageComponentSignature(component, family));
|
|
||||||
if (!uiCache.archiveHtmlCache) uiCache.archiveHtmlCache = new Map();
|
if (!uiCache.archiveHtmlCache) uiCache.archiveHtmlCache = new Map();
|
||||||
const activeCacheKeys = new Set(components.map((component, index) => lineageFamilyCacheKey(component, family, signatures[index])));
|
const activeCacheKeys = new Set();
|
||||||
for (const key of Array.from(uiCache.archiveHtmlCache.keys())) {
|
|
||||||
if (!activeCacheKeys.has(key)) uiCache.archiveHtmlCache.delete(key);
|
|
||||||
}
|
|
||||||
const token = `${version}:${Math.random().toString(36).slice(2)}`;
|
const token = `${version}:${Math.random().toString(36).slice(2)}`;
|
||||||
uiCache.archiveRenderToken = token;
|
uiCache.archiveRenderToken = token;
|
||||||
uiCache.archiveRenderRunning = true;
|
uiCache.archiveRenderRunning = true;
|
||||||
if (!components.length) {
|
ui.archiveContent.innerHTML = `<div class="lineage-render-status">\u5bb6\u7cfb\u56f3\u3092\u6e96\u5099\u4e2d\u2026</div><div class="lineage-forest"></div>`;
|
||||||
ui.archiveContent.innerHTML = `<div class="selected-info empty">\u751f\u304d\u3066\u3044\u308b\u69cb\u6210\u54e1\u3092\u6301\u3064\u5bb6\u7cfb\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>`;
|
lineageRestoreArchiveScroll(scrollState);
|
||||||
uiCache.archiveRenderRunning = false;
|
|
||||||
world.familyTreeDirty = false;
|
|
||||||
uiCache.archivePendingDirtyAfterRun = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.archiveContent.innerHTML = `<div class="lineage-render-status">\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 0 / ${components.length}</div><div class="lineage-forest"></div>`;
|
|
||||||
const status = ui.archiveContent.querySelector(".lineage-render-status");
|
const status = ui.archiveContent.querySelector(".lineage-render-status");
|
||||||
const forest = ui.archiveContent.querySelector(".lineage-forest");
|
const forest = ui.archiveContent.querySelector(".lineage-forest");
|
||||||
let index = 0;
|
const renderComponents = (components) => {
|
||||||
const pump = () => {
|
|
||||||
if (uiCache.archiveRenderToken !== token || !forest) {
|
if (uiCache.archiveRenderToken !== token || !forest) {
|
||||||
uiCache.archiveRenderRunning = false;
|
uiCache.archiveRenderRunning = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const splitComponents = components.flatMap(lineageSplitByActualEdges).filter(c => c.length);
|
||||||
|
uiCache.archiveRows = splitComponents;
|
||||||
|
if (!splitComponents.length) {
|
||||||
|
ui.archiveContent.innerHTML = `<div class="selected-info empty">\u751f\u304d\u3066\u3044\u308b\u69cb\u6210\u54e1\u3092\u6301\u3064\u5bb6\u7cfb\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>`;
|
||||||
|
lineageRestoreArchiveScroll(scrollState);
|
||||||
|
uiCache.archiveRenderRunning = false;
|
||||||
|
world.familyTreeDirty = false;
|
||||||
|
uiCache.archivePendingDirtyAfterRun = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status) status.textContent = `\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 0 / ${splitComponents.length}`;
|
||||||
|
let index = 0;
|
||||||
|
const pump = () => {
|
||||||
|
if (uiCache.archiveRenderToken !== token || !forest) {
|
||||||
|
uiCache.archiveRenderRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
let nodeBudget = 28;
|
let nodeBudget = 28;
|
||||||
let renderedThisSlice = 0;
|
let renderedThisSlice = 0;
|
||||||
while (index < components.length) {
|
while (index < splitComponents.length) {
|
||||||
const component = components[index];
|
const component = splitComponents[index];
|
||||||
if (renderedThisSlice > 0 && component.length > nodeBudget) break;
|
if (renderedThisSlice > 0 && component.length > nodeBudget) break;
|
||||||
forest.insertAdjacentHTML("beforeend", lineageFamilyHtml(component, index, family, signatures[index]));
|
const signature = lineageComponentSignature(component, family);
|
||||||
|
activeCacheKeys.add(lineageFamilyCacheKey(component, family, signature));
|
||||||
|
forest.insertAdjacentHTML("beforeend", lineageFamilyHtml(component, index, family, signature));
|
||||||
nodeBudget -= Math.max(1, component.length);
|
nodeBudget -= Math.max(1, component.length);
|
||||||
index += 1;
|
index += 1;
|
||||||
renderedThisSlice += 1;
|
renderedThisSlice += 1;
|
||||||
if (index < components.length && (nodeBudget <= 0 || performance.now() - started > 6)) break;
|
if (index < splitComponents.length && (nodeBudget <= 0 || performance.now() - started > 6)) break;
|
||||||
}
|
}
|
||||||
if (status) status.textContent = `\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 ${index} / ${components.length}`;
|
lineageRestoreArchiveScroll(scrollState);
|
||||||
if (index < components.length) {
|
if (status) status.textContent = `\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 ${index} / ${splitComponents.length}`;
|
||||||
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
|
if (index < splitComponents.length) {
|
||||||
schedule(pump, { timeout: 220 });
|
lineageIdleSchedule(pump, 220);
|
||||||
} else {
|
} else {
|
||||||
if (status) status.remove();
|
if (status) status.remove();
|
||||||
|
for (const key of Array.from(uiCache.archiveHtmlCache.keys())) {
|
||||||
|
if (!activeCacheKeys.has(key)) uiCache.archiveHtmlCache.delete(key);
|
||||||
|
}
|
||||||
|
lineageRestoreArchiveScroll(scrollState);
|
||||||
const renderDirtiedAgain = uiCache.archivePendingDirtyAfterRun || (world.familyVersion || 0) !== familyVersion;
|
const renderDirtiedAgain = uiCache.archivePendingDirtyAfterRun || (world.familyVersion || 0) !== familyVersion;
|
||||||
uiCache.archiveRenderRunning = false;
|
uiCache.archiveRenderRunning = false;
|
||||||
world.familyTreeDirty = renderDirtiedAgain;
|
world.familyTreeDirty = renderDirtiedAgain;
|
||||||
if (uiCache.archiveDiagnostics) {
|
if (uiCache.archiveDiagnostics) {
|
||||||
uiCache.archiveDiagnostics.renders = (uiCache.archiveDiagnostics.renders || 0) + 1;
|
uiCache.archiveDiagnostics.renders = (uiCache.archiveDiagnostics.renders || 0) + 1;
|
||||||
uiCache.archiveDiagnostics.lastMs = Math.round((performance.now() - renderStarted) * 10) / 10;
|
uiCache.archiveDiagnostics.lastMs = Math.round((performance.now() - renderStarted) * 10) / 10;
|
||||||
uiCache.archiveDiagnostics.lastFamilies = components.length;
|
uiCache.archiveDiagnostics.lastFamilies = splitComponents.length;
|
||||||
uiCache.archiveDiagnostics.lastNodes = components.reduce((sum, component) => sum + component.length, 0);
|
uiCache.archiveDiagnostics.lastNodes = splitComponents.reduce((sum, component) => sum + component.length, 0);
|
||||||
}
|
}
|
||||||
if (renderDirtiedAgain) {
|
if (renderDirtiedAgain) {
|
||||||
uiCache.archivePendingDirtyAfterRun = false;
|
uiCache.archivePendingDirtyAfterRun = false;
|
||||||
|
|
@ -1294,8 +1541,16 @@ function renderArchive() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
|
lineageIdleSchedule(pump, 160);
|
||||||
schedule(pump, { timeout: 160 });
|
};
|
||||||
|
lineageBuildComponentsIdle(
|
||||||
|
family,
|
||||||
|
token,
|
||||||
|
(label, done, total) => {
|
||||||
|
if (status) status.textContent = `${label}\u2026 ${Math.min(done || 0, total || 0)} / ${total || 0}`;
|
||||||
|
},
|
||||||
|
renderComponents
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFamilyTree() {
|
function validateFamilyTree() {
|
||||||
|
|
|
||||||
465
js/world.js
465
js/world.js
|
|
@ -1,4 +1,4 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
class World {
|
class World {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -276,12 +276,12 @@ class World {
|
||||||
}
|
}
|
||||||
|
|
||||||
toolSizeFor(type = "") {
|
toolSizeFor(type = "") {
|
||||||
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return "medium";
|
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return "medium";
|
||||||
return (this.toolSizes && this.toolSizes[type]) || this.toolSize || "medium";
|
return (this.toolSizes && this.toolSizes[type]) || this.toolSize || "medium";
|
||||||
}
|
}
|
||||||
|
|
||||||
toolSizeScale(type = "") {
|
toolSizeScale(type = "") {
|
||||||
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return 1;
|
if (!["firecracker", "fence_v", "fence_h", "stone", "grass", "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "zunchi"].includes(type)) return 1;
|
||||||
return { small: 0.68, medium: 1.0, large: 1.48 }[this.toolSizeFor(type)] || 1;
|
return { small: 0.68, medium: 1.0, large: 1.48 }[this.toolSizeFor(type)] || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,7 +292,16 @@ class World {
|
||||||
item.toolSize = size;
|
item.toolSize = size;
|
||||||
this.toolSize = size;
|
this.toolSize = size;
|
||||||
item.r *= scale;
|
item.r *= scale;
|
||||||
if (["sweet", "love_mochi", "fight_mochi", "sleep_drug", "grass", "zunchi"].includes(item.type)) item.amount *= scale * scale;
|
if (typeof isServingFoodType === "function" && isServingFoodType(item.type)) {
|
||||||
|
const servings = typeof foodServingsForSize === "function" ? foodServingsForSize(size) : 5;
|
||||||
|
item.foodServingScale = scale;
|
||||||
|
item.foodServingsMax = servings;
|
||||||
|
item.foodServingsRemaining = servings;
|
||||||
|
item.amount = servings;
|
||||||
|
} else if (["grass", "zunchi"].includes(item.type)) {
|
||||||
|
item.amount *= scale * scale;
|
||||||
|
item.foodServingScale = scale;
|
||||||
|
}
|
||||||
if (item.type === "firecracker") item.blastScale = scale;
|
if (item.type === "firecracker") item.blastScale = scale;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
@ -368,12 +377,13 @@ class World {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const nextParents = uniqueIds(t.parents || []);
|
const nextParents = uniqueIds(t.parents || []);
|
||||||
const nextChildren = uniqueIds(t.children || []);
|
const nextChildren = uniqueIds(t.children || []);
|
||||||
|
const nextPersonalityTags = getPersonalityTraitTags(t);
|
||||||
if (!this.family[t.id]) {
|
if (!this.family[t.id]) {
|
||||||
this.family[t.id] = {
|
this.family[t.id] = {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
type: t.type,
|
type: t.type,
|
||||||
personality: t.personality || "",
|
personalityTags: nextPersonalityTags,
|
||||||
generation: t.generation,
|
generation: t.generation,
|
||||||
hasPaired: !!t.hasPaired,
|
hasPaired: !!t.hasPaired,
|
||||||
parents: nextParents,
|
parents: nextParents,
|
||||||
|
|
@ -389,7 +399,7 @@ class World {
|
||||||
const nextAlive = !t.dead;
|
const nextAlive = !t.dead;
|
||||||
if (entry.name !== t.name) changed = true;
|
if (entry.name !== t.name) changed = true;
|
||||||
if (entry.type !== t.type) changed = true;
|
if (entry.type !== t.type) changed = true;
|
||||||
if ((entry.personality || "") !== (t.personality || "")) changed = true;
|
if (!sameList(entry.personalityTags, nextPersonalityTags)) changed = true;
|
||||||
if (entry.hasPaired !== !!t.hasPaired) changed = true;
|
if (entry.hasPaired !== !!t.hasPaired) changed = true;
|
||||||
if (entry.generation !== t.generation) changed = true;
|
if (entry.generation !== t.generation) changed = true;
|
||||||
if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true;
|
if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true;
|
||||||
|
|
@ -398,7 +408,8 @@ class World {
|
||||||
if (!sameList(entry.children, nextChildren)) changed = true;
|
if (!sameList(entry.children, nextChildren)) changed = true;
|
||||||
entry.name = t.name;
|
entry.name = t.name;
|
||||||
entry.type = t.type;
|
entry.type = t.type;
|
||||||
entry.personality = t.personality || entry.personality || "";
|
delete entry.personality;
|
||||||
|
entry.personalityTags = nextPersonalityTags;
|
||||||
entry.alive = nextAlive;
|
entry.alive = nextAlive;
|
||||||
entry.generation = t.generation;
|
entry.generation = t.generation;
|
||||||
entry.hasPaired = !!t.hasPaired;
|
entry.hasPaired = !!t.hasPaired;
|
||||||
|
|
@ -449,7 +460,7 @@ class World {
|
||||||
id,
|
id,
|
||||||
name: live?.name || n.name || id,
|
name: live?.name || n.name || id,
|
||||||
type: live?.type || n.type || "smile",
|
type: live?.type || n.type || "smile",
|
||||||
personality: live?.personality || n.personality || "",
|
personalityTags: live ? getPersonalityTraitTags(live) : [...(n.personalityTags || [])],
|
||||||
generation: Math.max(1, live?.generation || n.generation || 1),
|
generation: Math.max(1, live?.generation || n.generation || 1),
|
||||||
hasPaired: !!(live?.hasPaired || n.hasPaired),
|
hasPaired: !!(live?.hasPaired || n.hasPaired),
|
||||||
parents,
|
parents,
|
||||||
|
|
@ -565,7 +576,7 @@ class World {
|
||||||
if (this.family[t.id].alive !== false || this.family[t.id].deathReason !== finalReason) {
|
if (this.family[t.id].alive !== false || this.family[t.id].deathReason !== finalReason) {
|
||||||
this.family[t.id].alive = false;
|
this.family[t.id].alive = false;
|
||||||
this.family[t.id].deathReason = finalReason;
|
this.family[t.id].deathReason = finalReason;
|
||||||
this.family[t.id].personality = t.personality || this.family[t.id].personality || "";
|
delete this.family[t.id].personality;
|
||||||
this.familyVersion = (this.familyVersion || 0) + 1;
|
this.familyVersion = (this.familyVersion || 0) + 1;
|
||||||
this.markFamilyTreeDirty("death");
|
this.markFamilyTreeDirty("death");
|
||||||
}
|
}
|
||||||
|
|
@ -629,6 +640,7 @@ class World {
|
||||||
startBirthRitual(a, b) {
|
startBirthRitual(a, b) {
|
||||||
if (!a || !b || a.dead || b.dead) return false;
|
if (!a || !b || a.dead || b.dead) return false;
|
||||||
if (a.isZunchiSlave || b.isZunchiSlave) return false;
|
if (a.isZunchiSlave || b.isZunchiSlave) return false;
|
||||||
|
if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false;
|
||||||
if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false;
|
if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false;
|
||||||
const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length);
|
const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length);
|
||||||
const bJoined = b.familyJoined ? b.familyJoined() : Boolean((b.parents || []).length || (b.children || []).length);
|
const bJoined = b.familyJoined ? b.familyJoined() : Boolean((b.parents || []).length || (b.children || []).length);
|
||||||
|
|
@ -659,10 +671,15 @@ class World {
|
||||||
|
|
||||||
finishBirthRitual(a, b) {
|
finishBirthRitual(a, b) {
|
||||||
if (!a || !b || a.dead || b.dead) return null;
|
if (!a || !b || a.dead || b.dead) return null;
|
||||||
|
const reproductionBlockedByDisease = a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease;
|
||||||
a.birthRitualTimer = b.birthRitualTimer = 0;
|
a.birthRitualTimer = b.birthRitualTimer = 0;
|
||||||
a.birthPartnerId = b.birthPartnerId = null;
|
a.birthPartnerId = b.birthPartnerId = null;
|
||||||
a.birthRitualLeader = b.birthRitualLeader = false;
|
a.birthRitualLeader = b.birthRitualLeader = false;
|
||||||
a.state = b.state = "idle";
|
a.state = b.state = "idle";
|
||||||
|
if (reproductionBlockedByDisease) {
|
||||||
|
a.birthRitualRole = b.birthRitualRole = 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
a.fightTimer = b.fightTimer = 0;
|
a.fightTimer = b.fightTimer = 0;
|
||||||
a.fightTargetIds = []; b.fightTargetIds = [];
|
a.fightTargetIds = []; b.fightTargetIds = [];
|
||||||
a.fightTargetId = b.fightTargetId = null;
|
a.fightTargetId = b.fightTargetId = null;
|
||||||
|
|
@ -707,8 +724,10 @@ class World {
|
||||||
canStartFight(t) {
|
canStartFight(t) {
|
||||||
if (!t || t.dead) return false;
|
if (!t || t.dead) return false;
|
||||||
if (t.sleepDisease) return false;
|
if (t.sleepDisease) return false;
|
||||||
if (t.explosionDisease || t.fightDisease) return true;
|
// Disease fight rule: explosion disease and zunchi disease can lash out;
|
||||||
if (t.zunchiDisease) return false;
|
// sleep disease cannot fight or be challenged, and other diseases cannot start fights.
|
||||||
|
if (t.explosionDisease || t.zunchiDisease) return true;
|
||||||
|
if (t.fightDisease) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,10 +742,11 @@ class World {
|
||||||
spawnChild(a, b, index = 0, total = 1) {
|
spawnChild(a, b, index = 0, total = 1) {
|
||||||
const inherited = Math.random() < 0.56 ? a.type : b.type;
|
const inherited = Math.random() < 0.56 ? a.type : b.type;
|
||||||
const mutation = Math.random() < 0.18 ? baseSpriteId() : inherited;
|
const mutation = Math.random() < 0.18 ? baseSpriteId() : inherited;
|
||||||
const inheritedPersonality = a.personality === b.personality ? a.personality : (Math.random() < 0.5 ? a.personality : b.personality);
|
const childBirthPersonality = personalityFromParents(a, b);
|
||||||
const child = this.addTarinai({
|
const child = this.addTarinai({
|
||||||
type: mutation,
|
type: mutation,
|
||||||
personality: inheritedPersonality || personalityForSeed(`${a.id}:${b.id}:${this.time}:${index}`),
|
birthPersonality: childBirthPersonality,
|
||||||
|
currentPersonality: childBirthPersonality,
|
||||||
name: makeName(),
|
name: makeName(),
|
||||||
age: 0,
|
age: 0,
|
||||||
x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18,
|
x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18,
|
||||||
|
|
@ -822,8 +842,10 @@ class World {
|
||||||
if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return;
|
if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return;
|
||||||
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return;
|
if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return;
|
||||||
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return;
|
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return;
|
||||||
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + (a.personality === "irritable" ? 8 : 0) + ((b.relationTo(a.id).fear || 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||||
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + (b.personality === "irritable" ? 8 : 0) + ((a.relationTo(b.id).fear || 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||||
|
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((b.relationTo(a.id).fear || 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||||
|
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((a.relationTo(b.id).fear || 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||||
const actor = scoreA >= scoreB ? a : b;
|
const actor = scoreA >= scoreB ? a : b;
|
||||||
const target = actor === a ? b : a;
|
const target = actor === a ? b : a;
|
||||||
if (!this.canFightPair(actor, target)) return;
|
if (!this.canFightPair(actor, target)) return;
|
||||||
|
|
@ -843,7 +865,7 @@ class World {
|
||||||
if (!actor || !target || actor.dead || target.dead) return false;
|
if (!actor || !target || actor.dead || target.dead) return false;
|
||||||
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
||||||
const fearLoad = (target.relationTo(actor.id).fear || 0) * 0.72 + target.personalityProfile().fear * 8;
|
const fearLoad = (target.relationTo(actor.id).fear || 0) * 0.72 + target.personalityProfile().fear * 8;
|
||||||
const resistance = target.energy * 0.30 + target.mood * 0.16 + (target.personality === "irritable" ? 5 : 0) + rand(-5, 5);
|
const resistance = target.energy * 0.30 + target.mood * 0.16 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5);
|
||||||
const successChance = clamp(0.62 + (actorScore - targetScore + fearLoad - resistance) / 76, 0.50, 0.96);
|
const successChance = clamp(0.62 + (actorScore - targetScore + fearLoad - resistance) / 76, 0.50, 0.96);
|
||||||
actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20);
|
actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20);
|
||||||
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
||||||
|
|
@ -902,8 +924,10 @@ class World {
|
||||||
b.fightTargetIds = b.fightTargetIds.slice(-4);
|
b.fightTargetIds = b.fightTargetIds.slice(-4);
|
||||||
a.fightTargetId = a.fightTargetIds[0];
|
a.fightTargetId = a.fightTargetIds[0];
|
||||||
b.fightTargetId = b.fightTargetIds[0];
|
b.fightTargetId = b.fightTargetIds[0];
|
||||||
const aScore = a.energy * 0.52 + a.mood * 0.30 + (a.type === "angry" ? 10 : 0) + (a.personality === "irritable" ? 5 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (a.isZunchiSlave ? -14 : 0) + rand(-7, 7);
|
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||||
const bScore = b.energy * 0.52 + b.mood * 0.30 + (b.type === "angry" ? 10 : 0) + (b.personality === "irritable" ? 5 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (b.isZunchiSlave ? -14 : 0) + rand(-7, 7);
|
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||||
|
const aScore = a.energy * 0.52 + a.mood * 0.30 + (a.type === "angry" ? 10 : 0) + aAggressiveIntent * 5 + ((a.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (a.isZunchiSlave ? -14 : 0) + rand(-7, 7);
|
||||||
|
const bScore = b.energy * 0.52 + b.mood * 0.30 + (b.type === "angry" ? 10 : 0) + bAggressiveIntent * 5 + ((b.fightMochiTimer || 0) > 0.04 ? 12 : 0) + (b.isZunchiSlave ? -14 : 0) + rand(-7, 7);
|
||||||
const loser = aScore <= bScore ? a : b;
|
const loser = aScore <= bScore ? a : b;
|
||||||
const winner = loser === a ? b : a;
|
const winner = loser === a ? b : a;
|
||||||
loser.defeatedById = winner.id;
|
loser.defeatedById = winner.id;
|
||||||
|
|
@ -987,9 +1011,12 @@ class World {
|
||||||
ny = Math.sin(a);
|
ny = Math.sin(a);
|
||||||
}
|
}
|
||||||
const distanceBoost = p * p * 0.55 + p * 0.45;
|
const distanceBoost = p * p * 0.55 + p * 0.45;
|
||||||
const power = (260 + distanceBoost * 820) * rand(0.78, 1.24) * (it.blastScale || 1);
|
const power = (260 + distanceBoost * 920) * rand(0.78, 1.24) * (it.blastScale || 1);
|
||||||
t.vx += nx * power + rand(-95, 95) * (0.6 + p);
|
this.applyImpulse(t, nx * power + rand(-95, 95) * (0.6 + p), ny * power + rand(-95, 95) * (0.6 + p), {
|
||||||
t.vy += ny * power + rand(-95, 95) * (0.6 + p);
|
panic: true,
|
||||||
|
target: { x, y },
|
||||||
|
fearTimer: 1.7 + p,
|
||||||
|
});
|
||||||
t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85);
|
t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85);
|
||||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||||
t.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
t.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
||||||
|
|
@ -1012,8 +1039,7 @@ class World {
|
||||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||||
}
|
}
|
||||||
const blast = 430 + p * 960;
|
const blast = 430 + p * 960;
|
||||||
ball.vx = (ball.vx || 0) + dx / d * blast + rand(-80, 80);
|
this.applyImpulse(ball, dx / d * blast + rand(-80, 80), dy / d * blast + rand(-80, 80));
|
||||||
ball.vy = (ball.vy || 0) + dy / d * blast + rand(-80, 80);
|
|
||||||
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38);
|
ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38);
|
||||||
ball.lastPokedAt = this.time || 0;
|
ball.lastPokedAt = this.time || 0;
|
||||||
|
|
@ -1050,9 +1076,12 @@ class World {
|
||||||
nx = Math.cos(a);
|
nx = Math.cos(a);
|
||||||
ny = Math.sin(a);
|
ny = Math.sin(a);
|
||||||
}
|
}
|
||||||
const diseaseBlast = (190 + q * 560) * rand(0.80, 1.22);
|
const diseaseBlast = (190 + q * 620) * rand(0.80, 1.22);
|
||||||
o.vx += nx * diseaseBlast + rand(-64, 64) * (0.5 + q);
|
this.applyImpulse(o, nx * diseaseBlast + rand(-64, 64) * (0.5 + q), ny * diseaseBlast + rand(-64, 64) * (0.5 + q), {
|
||||||
o.vy += ny * diseaseBlast + rand(-64, 64) * (0.5 + q);
|
panic: true,
|
||||||
|
target: { x, y },
|
||||||
|
fearTimer: 1.0 + q,
|
||||||
|
});
|
||||||
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.2 + q);
|
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.2 + q);
|
||||||
o.fearTimer = Math.max(o.fearTimer || 0, 1.0 + q);
|
o.fearTimer = Math.max(o.fearTimer || 0, 1.0 + q);
|
||||||
if (!o.dead && Math.random() < 0.020 * q) o.infectExplosionDisease?.(t);
|
if (!o.dead && Math.random() < 0.020 * q) o.infectExplosionDisease?.(t);
|
||||||
|
|
@ -1088,7 +1117,7 @@ class World {
|
||||||
cleaned += p * 0.55;
|
cleaned += p * 0.55;
|
||||||
}
|
}
|
||||||
for (const t of this.nearbyTarinai(x, y, radius + 70)) {
|
for (const t of this.nearbyTarinai(x, y, radius + 70)) {
|
||||||
if (!t || t.dead) continue;
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
const d = distXY(t.x, t.y, x, y);
|
const d = distXY(t.x, t.y, x, y);
|
||||||
if (d > radius + t.radius) continue;
|
if (d > radius + t.radius) continue;
|
||||||
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
||||||
|
|
@ -1187,12 +1216,19 @@ class World {
|
||||||
return Boolean(it && (it.type === "bed" || it.type === "nest_box"));
|
return Boolean(it && (it.type === "bed" || it.type === "nest_box"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTarinaiHiddenInNestBox(t) {
|
||||||
|
return Boolean(t && !t.dead && t.insideNestBoxId);
|
||||||
|
}
|
||||||
|
|
||||||
bedOccupancy(bed) {
|
bedOccupancy(bed) {
|
||||||
if (!this.isSleepFurniture(bed)) return 0;
|
if (!this.isSleepFurniture(bed)) return 0;
|
||||||
|
if (bed.type === "nest_box") {
|
||||||
|
return this.nestBoxOccupants ? this.nestBoxOccupants(bed, Infinity).length : 0;
|
||||||
|
}
|
||||||
let n = 0;
|
let n = 0;
|
||||||
const radius = Math.max(86, bed.r * 2.45);
|
const radius = Math.max(86, bed.r * 2.45);
|
||||||
for (const t of this.nearbyTarinai(bed.x, bed.y, radius)) {
|
for (const t of this.nearbyTarinai(bed.x, bed.y, radius)) {
|
||||||
if (t.dead) continue;
|
if (t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
if (t.state === "sleep" || t.state === "seek_bed" || t.target === bed) n += 1;
|
if (t.state === "sleep" || t.state === "seek_bed" || t.target === bed) n += 1;
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
|
|
@ -1205,23 +1241,27 @@ class World {
|
||||||
}
|
}
|
||||||
|
|
||||||
bestBedFor(t, maxDist = 360) {
|
bestBedFor(t, maxDist = 360) {
|
||||||
let best = null;
|
const choose = (kind) => {
|
||||||
let bestScore = Infinity;
|
let best = null;
|
||||||
for (const bed of this.nearbyItems(t.x, t.y, maxDist)) {
|
let bestScore = Infinity;
|
||||||
if (bed.dead || !this.isSleepFurniture(bed)) continue;
|
for (const bed of this.nearbyItems(t.x, t.y, maxDist)) {
|
||||||
if (bed.type === "nest_box" && t?.insideNestBoxId !== bed.id) {
|
if (bed.dead || !this.isSleepFurniture(bed) || bed.type !== kind) continue;
|
||||||
const occupants = this.nestBoxOccupants ? this.nestBoxOccupants(bed, Infinity) : [];
|
if (bed.type === "nest_box" && t?.insideNestBoxId !== bed.id) {
|
||||||
if (occupants.length >= this.nestBoxCapacity(bed)) continue;
|
const occupants = this.nestBoxOccupants ? this.nestBoxOccupants(bed, Infinity) : [];
|
||||||
|
if (occupants.length >= this.nestBoxCapacity(bed)) continue;
|
||||||
|
}
|
||||||
|
if (t?.shouldAvoidTarget && t.shouldAvoidTarget(bed)) continue;
|
||||||
|
const d = dist(t, bed);
|
||||||
|
const occ = this.bedOccupancy(bed);
|
||||||
|
const comfort = this.bedComfort(bed);
|
||||||
|
const crowdPenalty = bed.type === "nest_box" ? Math.max(0, occ - this.nestBoxCapacity(bed) + 1) * 120 : Math.max(0, occ - 3) * 34;
|
||||||
|
const score = d - comfort * (bed.type === "nest_box" ? 72 : 46) + crowdPenalty;
|
||||||
|
if (score < bestScore) { best = bed; bestScore = score; }
|
||||||
}
|
}
|
||||||
if (t?.shouldAvoidTarget && t.shouldAvoidTarget(bed)) continue;
|
return best;
|
||||||
const d = dist(t, bed);
|
};
|
||||||
const occ = this.bedOccupancy(bed);
|
// Prefer an available nest box, then fall back to a hay bed.
|
||||||
const comfort = this.bedComfort(bed);
|
return choose("nest_box") || choose("bed");
|
||||||
const crowdPenalty = Math.max(0, occ - 3) * 34;
|
|
||||||
const score = d - comfort * 46 + crowdPenalty;
|
|
||||||
if (score < bestScore) { best = bed; bestScore = score; }
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
relationNotice(aId, bId, kind, cooldown = 30) {
|
relationNotice(aId, bId, kind, cooldown = 30) {
|
||||||
|
|
@ -1233,6 +1273,52 @@ class World {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyImpulse(entity, vx = 0, vy = 0, options = {}) {
|
||||||
|
if (!entity || entity.dead) return false;
|
||||||
|
if (!Number.isFinite(vx) || !Number.isFinite(vy)) return false;
|
||||||
|
if (entity instanceof Tarinai || "impulseVx" in entity || "impulseVy" in entity) {
|
||||||
|
entity.impulseVx = (Number.isFinite(entity.impulseVx) ? entity.impulseVx : 0) + vx;
|
||||||
|
entity.impulseVy = (Number.isFinite(entity.impulseVy) ? entity.impulseVy : 0) + vy;
|
||||||
|
} else {
|
||||||
|
entity.vx = (Number.isFinite(entity.vx) ? entity.vx : 0) + vx;
|
||||||
|
entity.vy = (Number.isFinite(entity.vy) ? entity.vy : 0) + vy;
|
||||||
|
}
|
||||||
|
if (options.panic && entity instanceof Tarinai) {
|
||||||
|
entity.state = "panic";
|
||||||
|
entity.target = options.target || null;
|
||||||
|
entity.fearTimer = Math.max(entity.fearTimer || 0, options.fearTimer ?? 0.55);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyKnockback(entity, sourceX, sourceY, strength = 0, options = {}) {
|
||||||
|
if (!entity || !Number.isFinite(strength)) return false;
|
||||||
|
let dx = (entity.x || 0) - sourceX;
|
||||||
|
let dy = (entity.y || 0) - sourceY;
|
||||||
|
let d = Math.hypot(dx, dy);
|
||||||
|
if (!Number.isFinite(d) || d < 0.001) {
|
||||||
|
const a = Number.isFinite(options.angle) ? options.angle : rand(0, Math.PI * 2);
|
||||||
|
dx = Math.cos(a);
|
||||||
|
dy = Math.sin(a);
|
||||||
|
d = 1;
|
||||||
|
}
|
||||||
|
const random = options.random || 0;
|
||||||
|
return this.applyImpulse(
|
||||||
|
entity,
|
||||||
|
dx / d * strength + rand(-random, random),
|
||||||
|
dy / d * strength + rand(-random, random),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyImpactDamage(target, amount, cause = "\u885d\u7a81", options = {}) {
|
||||||
|
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
||||||
|
const normalized = options.cause || cause || "\u885d\u7a81";
|
||||||
|
if (target.damage) target.damage(amount, normalized);
|
||||||
|
else if (Number.isFinite(target.hp)) target.hp -= amount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
resolveFightOutcome(winner, loser) {
|
resolveFightOutcome(winner, loser) {
|
||||||
if (!winner || !loser || winner.dead || loser.dead) return;
|
if (!winner || !loser || winner.dead || loser.dead) return;
|
||||||
const key = `${winner.id}:${loser.id}:${Math.floor(this.time / 2)}`;
|
const key = `${winner.id}:${loser.id}:${Math.floor(this.time / 2)}`;
|
||||||
|
|
@ -1250,6 +1336,8 @@ class World {
|
||||||
loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1;
|
loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1;
|
||||||
winner.totalFightWins = (winner.totalFightWins || 0) + 1;
|
winner.totalFightWins = (winner.totalFightWins || 0) + 1;
|
||||||
loser.totalFightLosses = (loser.totalFightLosses || 0) + 1;
|
loser.totalFightLosses = (loser.totalFightLosses || 0) + 1;
|
||||||
|
winner.adjustPersonality?.("aggression", 0.018, "after winning fights.");
|
||||||
|
loser.adjustPersonality?.("aggression", -0.018, "after losing fights.");
|
||||||
if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) {
|
if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) {
|
||||||
const oldName = loser.formerName || loser.name;
|
const oldName = loser.formerName || loser.name;
|
||||||
this.spawnBubble(loser.x, loser.y - loser.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)");
|
this.spawnBubble(loser.x, loser.y - loser.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)");
|
||||||
|
|
@ -1272,7 +1360,7 @@ class World {
|
||||||
const push = isStone ? rand(145, 210) : rand(54, 92);
|
const push = isStone ? rand(145, 210) : rand(54, 92);
|
||||||
let hit = 0;
|
let hit = 0;
|
||||||
for (const t of this.nearbyTarinai(it.x, it.y, radius + 48)) {
|
for (const t of this.nearbyTarinai(it.x, it.y, radius + 48)) {
|
||||||
if (!t || t.dead) continue;
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
|
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
|
||||||
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
||||||
if (p <= 0) continue;
|
if (p <= 0) continue;
|
||||||
|
|
@ -1315,8 +1403,8 @@ class World {
|
||||||
for (const [ball, arr] of groups) {
|
for (const [ball, arr] of groups) {
|
||||||
if (arr.length <= 5) continue;
|
if (arr.length <= 5) continue;
|
||||||
arr.sort((a, b) => {
|
arr.sort((a, b) => {
|
||||||
const ap = a.personality === "playful" ? -120 : 0;
|
const ap = ((a.currentPersonality?.openness || 0) >= 0.5) ? -120 : 0;
|
||||||
const bp = b.personality === "playful" ? -120 : 0;
|
const bp = ((b.currentPersonality?.openness || 0) >= 0.5) ? -120 : 0;
|
||||||
return dist(a, ball) + ap - (dist(b, ball) + bp);
|
return dist(a, ball) + ap - (dist(b, ball) + bp);
|
||||||
});
|
});
|
||||||
for (const t of arr.slice(5)) {
|
for (const t of arr.slice(5)) {
|
||||||
|
|
@ -1355,7 +1443,8 @@ class World {
|
||||||
ball.amount = Math.max(ball.amount || 999, 999);
|
ball.amount = Math.max(ball.amount || 999, 999);
|
||||||
audio.poke();
|
audio.poke();
|
||||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: "rgba(255,255,255,0.58)" }));
|
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: "rgba(255,255,255,0.58)" }));
|
||||||
// 繝懊・繝ォ繧偵▽縺、縺・◆繧、繝吶Φ繝医・隕ウ蟇欟I縺ク谿九&縺ェ縺・・ return true;
|
this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke", hiddenFromObservation: true });
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveBallBallCollisions(dt) {
|
resolveBallBallCollisions(dt) {
|
||||||
|
|
@ -1368,7 +1457,7 @@ class World {
|
||||||
const avx = a.vx || 0;
|
const avx = a.vx || 0;
|
||||||
const avy = a.vy || 0;
|
const avy = a.vy || 0;
|
||||||
const aSpeed = Math.hypot(avx, avy);
|
const aSpeed = Math.hypot(avx, avy);
|
||||||
const range = ar * 2 + Math.min(560, aSpeed * Math.max(0.016, dt || 0.016)) + 52;
|
const range = ar * 2 + aSpeed * Math.max(0.016, dt || 0.016) + 52;
|
||||||
for (const b of this.nearbyItems(a.x, a.y, range) || []) {
|
for (const b of this.nearbyItems(a.x, a.y, range) || []) {
|
||||||
if (!b || b === a || b.dead || b.type !== "ball") continue;
|
if (!b || b === a || b.dead || b.type !== "ball") continue;
|
||||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||||
|
|
@ -1460,7 +1549,7 @@ class World {
|
||||||
const sy = ball.y - py;
|
const sy = ball.y - py;
|
||||||
const segLenSq = sx * sx + sy * sy;
|
const segLenSq = sx * sx + sy * sy;
|
||||||
for (const t of this.nearbyTarinai(ball.x, ball.y, range)) {
|
for (const t of this.nearbyTarinai(ball.x, ball.y, range)) {
|
||||||
if (!t || t.dead || t.state === "sleep") continue;
|
if (!t || t.dead || t.state === "sleep" || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
let hitX = ball.x;
|
let hitX = ball.x;
|
||||||
let hitY = ball.y;
|
let hitY = ball.y;
|
||||||
if (segLenSq > 1) {
|
if (segLenSq > 1) {
|
||||||
|
|
@ -1492,11 +1581,14 @@ class World {
|
||||||
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
|
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
|
||||||
const damage = clamp((ballSpeed - 285) / 18, 3.5, 38);
|
const damage = clamp((ballSpeed - 285) / 18, 3.5, 38);
|
||||||
t.lastBallDamageAt = now;
|
t.lastBallDamageAt = now;
|
||||||
t.vx = (t.vx || 0) * 0.35 + (ball.vx || 0) * 0.20;
|
this.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
|
||||||
t.vy = (t.vy || 0) * 0.35 + (ball.vy || 0) * 0.20;
|
panic: true,
|
||||||
t.damage(damage, "\u885d\u7a81");
|
target: { x: ball.x, y: ball.y },
|
||||||
const timidStress = t.personality === "timid" ? 1.55 : 1.0;
|
fearTimer: 0.95,
|
||||||
const stressGain = clamp(damage * 0.55 * timidStress, 3, t.personality === "timid" ? 26 : 18);
|
});
|
||||||
|
this.applyImpactDamage(t, damage, "\u885d\u7a81");
|
||||||
|
const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0);
|
||||||
|
const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18);
|
||||||
t.addStress ? t.addStress(stressGain, { threshold: 8 }) : (t.stress = clamp(t.stress + stressGain, 0, 130));
|
t.addStress ? t.addStress(stressGain, { threshold: 8 }) : (t.stress = clamp(t.stress + stressGain, 0, 130));
|
||||||
t.state = "panic";
|
t.state = "panic";
|
||||||
t.target = { x: ball.x, y: ball.y };
|
t.target = { x: ball.x, y: ball.y };
|
||||||
|
|
@ -1513,14 +1605,14 @@ class World {
|
||||||
ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10);
|
ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10);
|
||||||
ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10);
|
ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10);
|
||||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
|
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
|
||||||
if (t.dead) continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relVx = (t.vx || 0) - (ball.vx || 0);
|
const relVx = (t.vx || 0) - (ball.vx || 0);
|
||||||
const relVy = (t.vy || 0) - (ball.vy || 0);
|
const relVy = (t.vy || 0) - (ball.vy || 0);
|
||||||
const relativeSpeed = Math.hypot(relVx, relVy);
|
const relativeSpeed = Math.hypot(relVx, relVy);
|
||||||
const playIntent = t.state === "play_ball" || t.target === ball;
|
const playIntent = t.state === "play_ball" || t.target === ball;
|
||||||
const playful = t.personality === "playful";
|
const playful = t.shouldApplyPersonalityBehavior?.("openness", 1);
|
||||||
const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
|
const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
|
||||||
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
|
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
|
||||||
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
|
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
|
||||||
|
|
@ -1534,8 +1626,8 @@ class World {
|
||||||
t.vy -= ny * Math.min(22, impulse * 0.10);
|
t.vy -= ny * Math.min(22, impulse * 0.10);
|
||||||
t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, 100);
|
t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, 100);
|
||||||
const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20;
|
const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20;
|
||||||
const timidBump = t.personality === "timid" && !playIntent ? clamp(ballSpeed / 190, 0.9, 3.8) : 0;
|
const sensitiveBump = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0;
|
||||||
t.stress = clamp(t.stress - playRelief + timidBump, 0, 130);
|
t.stress = clamp(t.stress - playRelief + sensitiveBump, 0, 130);
|
||||||
if (playful && playIntent) {
|
if (playful && playIntent) {
|
||||||
t.loneliness = clamp(t.loneliness - 0.9, 0, 100);
|
t.loneliness = clamp(t.loneliness - 0.9, 0, 100);
|
||||||
t.affection = clamp(t.affection + 0.18, 0, 100);
|
t.affection = clamp(t.affection + 0.18, 0, 100);
|
||||||
|
|
@ -1556,7 +1648,7 @@ class World {
|
||||||
const now = this.time || 0;
|
const now = this.time || 0;
|
||||||
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
|
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
|
||||||
for (const a of this.tarinai || []) {
|
for (const a of this.tarinai || []) {
|
||||||
if (!a || a.dead) continue;
|
if (!a || a.dead || this.isTarinaiHiddenInNestBox(a)) continue;
|
||||||
const avx = a.vx || 0;
|
const avx = a.vx || 0;
|
||||||
const avy = a.vy || 0;
|
const avy = a.vy || 0;
|
||||||
const aSpeed = Math.hypot(avx, avy);
|
const aSpeed = Math.hypot(avx, avy);
|
||||||
|
|
@ -1568,7 +1660,7 @@ class World {
|
||||||
const segLenSq = sx * sx + sy * sy;
|
const segLenSq = sx * sx + sy * sy;
|
||||||
const range = (a.radius || 22) * 2.5 + aSpeed * Math.max(0.016, dt || 0.016) + 64;
|
const range = (a.radius || 22) * 2.5 + aSpeed * Math.max(0.016, dt || 0.016) + 64;
|
||||||
for (const b of this.nearbyTarinai(a.x, a.y, range)) {
|
for (const b of this.nearbyTarinai(a.x, a.y, range)) {
|
||||||
if (!b || b === a || b.dead) continue;
|
if (!b || b === a || b.dead || this.isTarinaiHiddenInNestBox(b)) continue;
|
||||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||||
const bvx = b.vx || 0;
|
const bvx = b.vx || 0;
|
||||||
|
|
@ -1601,12 +1693,15 @@ class World {
|
||||||
ny = Math.sin(aa);
|
ny = Math.sin(aa);
|
||||||
}
|
}
|
||||||
const damage = clamp((relSpeed - 118) / 26, 1.2, 22);
|
const damage = clamp((relSpeed - 118) / 26, 1.2, 22);
|
||||||
target.vx += ivx * 0.20 + nx * 28;
|
this.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
|
||||||
target.vy += ivy * 0.20 + ny * 28;
|
panic: true,
|
||||||
|
target: { x: impactor.x, y: impactor.y },
|
||||||
|
fearTimer: 0.55,
|
||||||
|
});
|
||||||
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
||||||
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
||||||
target.damage(damage, "\u885d\u7a81");
|
this.applyImpactDamage(target, damage, "\u885d\u7a81");
|
||||||
if (!impactor.dead) impactor.damage(damage * 0.35, "\u885d\u7a81");
|
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81");
|
||||||
for (const t of [target, impactor]) {
|
for (const t of [target, impactor]) {
|
||||||
if (!t || t.dead) continue;
|
if (!t || t.dead) continue;
|
||||||
t.state = "panic";
|
t.state = "panic";
|
||||||
|
|
@ -1639,7 +1734,7 @@ class World {
|
||||||
let best = null, bestD = maxDist;
|
let best = null, bestD = maxDist;
|
||||||
const candidates = Number.isFinite(maxDist) ? this.nearbyTarinai(entity.x, entity.y, maxDist) : this.tarinai;
|
const candidates = Number.isFinite(maxDist) ? this.nearbyTarinai(entity.x, entity.y, maxDist) : this.tarinai;
|
||||||
for (const o of candidates) {
|
for (const o of candidates) {
|
||||||
if (o === entity || o.dead) continue;
|
if (o === entity || o.dead || this.isTarinaiHiddenInNestBox(o)) continue;
|
||||||
if (predicate && !predicate(o)) continue;
|
if (predicate && !predicate(o)) continue;
|
||||||
if (entity?.shouldAvoidTarget && entity.shouldAvoidTarget(o)) continue;
|
if (entity?.shouldAvoidTarget && entity.shouldAvoidTarget(o)) continue;
|
||||||
const d = dist(entity, o);
|
const d = dist(entity, o);
|
||||||
|
|
@ -1707,6 +1802,47 @@ class World {
|
||||||
return this.nestBoxSolidRects(it)[0] || null;
|
return this.nestBoxSolidRects(it)[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nestBoxEntryPoint(box) {
|
||||||
|
const base = this.nestBoxBaseRect(box);
|
||||||
|
if (!base) return { x: box?.x || 0, y: box?.y || 0 };
|
||||||
|
const r = box.r || 42;
|
||||||
|
// Move the approach point to the center of the open middle cell. The
|
||||||
|
// collision resolver also ignores this nest box for active sleepers near
|
||||||
|
// the door, so tarinai can cross the threshold instead of sliding off posts.
|
||||||
|
return {
|
||||||
|
x: clamp(box.x, CONFIG.worldPadding, this.w - CONFIG.worldPadding),
|
||||||
|
y: clamp(box.y + r * 0.02, base.top + r * 0.46, base.bottom - r * 0.18),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
nestBoxExitPoint(box, occupant = null) {
|
||||||
|
const base = this.nestBoxBaseRect(box);
|
||||||
|
if (!base) return this.nestBoxEntryPoint(box);
|
||||||
|
const r = box.r || 42;
|
||||||
|
const side = occupant ? (stableUnit(occupant.id || "nest", `nest-exit-${box.id || "box"}`) - 0.5) * r * 0.36 : 0;
|
||||||
|
return {
|
||||||
|
x: clamp(box.x + side, CONFIG.worldPadding, this.w - CONFIG.worldPadding),
|
||||||
|
y: clamp(base.bottom + r * 0.22, CONFIG.worldPadding, this.h - CONFIG.worldPadding),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
nestBoxInnerPoint(box, occupant = null) {
|
||||||
|
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);
|
||||||
|
if (index < 0) index = Math.min(occupants.length, 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],
|
||||||
|
];
|
||||||
|
const slot = slots[index % slots.length];
|
||||||
|
return {
|
||||||
|
x: clamp(box.x + slot[0] * r, base.left + r * 0.46, base.right - r * 0.46),
|
||||||
|
y: clamp(box.y + slot[1] * r, base.top + r * 0.34, base.bottom - r * 0.18),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
solidObstacleRects(it) {
|
solidObstacleRects(it) {
|
||||||
if (!it || it.dead) return [];
|
if (!it || it.dead) return [];
|
||||||
if (this.isFenceType(it.type)) {
|
if (this.isFenceType(it.type)) {
|
||||||
|
|
@ -1717,6 +1853,20 @@ class World {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldIgnoreNestBoxCollisionFor(t, box) {
|
||||||
|
if (!t || !box || box.dead || box.type !== "nest_box") return false;
|
||||||
|
if (t.insideNestBoxId) return true;
|
||||||
|
if (t.target !== box || (t.state !== "seek_bed" && t.state !== "sleep")) return false;
|
||||||
|
const entry = this.nestBoxEntryPoint(box);
|
||||||
|
const base = this.nestBoxBaseRect(box);
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24 } = {}) {
|
nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24 } = {}) {
|
||||||
const rects = [];
|
const rects = [];
|
||||||
let checked = 0;
|
let checked = 0;
|
||||||
|
|
@ -1764,10 +1914,11 @@ class World {
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveSolidObstacleCollision(t) {
|
resolveSolidObstacleCollision(t) {
|
||||||
if (!t || t.dead) return;
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return;
|
||||||
const rr = Math.max(8, t.radius * 0.74);
|
const rr = Math.max(8, t.radius * 0.74);
|
||||||
let pushed = false;
|
let pushed = false;
|
||||||
for (const rect of this.nearbySolidObstacleRects(t.x, t.y, rr + 150)) {
|
for (const rect of this.nearbySolidObstacleRects(t.x, t.y, rr + 150)) {
|
||||||
|
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor(t, rect.item)) continue;
|
||||||
if (this.pushTarinaiOutOfRect(t, rect, rr)) pushed = true;
|
if (this.pushTarinaiOutOfRect(t, rect, rr)) pushed = true;
|
||||||
}
|
}
|
||||||
if (pushed) {
|
if (pushed) {
|
||||||
|
|
@ -1833,7 +1984,7 @@ class World {
|
||||||
|
|
||||||
grassOnTarinaiAt(x, y, minGap = 24) {
|
grassOnTarinaiAt(x, y, minGap = 24) {
|
||||||
for (const t of this.nearbyTarinai(x, y, minGap + 36)) {
|
for (const t of this.nearbyTarinai(x, y, minGap + 36)) {
|
||||||
if (t.dead) continue;
|
if (t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
if (distXY(x, y, t.x, t.y) < Math.max(minGap, t.radius * 1.15)) return true;
|
if (distXY(x, y, t.x, t.y) < Math.max(minGap, t.radius * 1.15)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2041,12 +2192,12 @@ class World {
|
||||||
|
|
||||||
observeHighlightKind(t) {
|
observeHighlightKind(t) {
|
||||||
const s = this.selected;
|
const s = this.selected;
|
||||||
if (!s || !t || t === s || t.dead) return "";
|
if (!s || !t || t === s || t.dead || this.tool !== "observe") return "";
|
||||||
if ((s.parents || []).includes(t.id)) return "parent";
|
if ((s.parents || []).includes(t.id)) return "parent";
|
||||||
if ((s.children || []).includes(t.id)) return "child";
|
if ((s.children || []).includes(t.id)) return "child";
|
||||||
const rel = s.relationTo ? s.relationTo(t.id) : null;
|
const rel = s.relationTo ? s.relationTo(t.id) : null;
|
||||||
|
if (rel && (rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) return "friend";
|
||||||
if (rel && (rel.fear || 0) >= 8) return "enemy";
|
if (rel && (rel.fear || 0) >= 8) return "enemy";
|
||||||
if (rel && (rel.affinity || 0) >= 10) return "friend";
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2066,8 +2217,22 @@ class World {
|
||||||
if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; }
|
if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; }
|
||||||
}
|
}
|
||||||
if (!best) return null;
|
if (!best) return null;
|
||||||
const occupants = this.nestBoxOccupants(best);
|
const occupants = this.nestBoxOccupants(best, Infinity);
|
||||||
return { box: best, occupants };
|
const capacity = this.nestBoxCapacity(best);
|
||||||
|
return { box: best, occupants, capacity };
|
||||||
|
}
|
||||||
|
|
||||||
|
nestBoxToolTipCount() {
|
||||||
|
const pointed = this.pointerNestBoxInfo?.();
|
||||||
|
if (pointed?.box) return { current: pointed.occupants.length, max: pointed.capacity || this.nestBoxCapacity(pointed.box) };
|
||||||
|
const boxes = (this.items || []).filter(it => it && !it.dead && it.type === "nest_box");
|
||||||
|
if (!boxes.length) return { current: 0, max: this.nestBoxCapacity() };
|
||||||
|
let current = 0, max = 0;
|
||||||
|
for (const box of boxes) {
|
||||||
|
current += this.nestBoxOccupants(box, Infinity).length;
|
||||||
|
max += this.nestBoxCapacity(box);
|
||||||
|
}
|
||||||
|
return { current, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
blastZunchiFrom(x, y, radius, strength = 1) {
|
blastZunchiFrom(x, y, radius, strength = 1) {
|
||||||
|
|
@ -2100,7 +2265,11 @@ class World {
|
||||||
dt = Math.min(dt, 0.12 * this.speed);
|
dt = Math.min(dt, 0.12 * this.speed);
|
||||||
|
|
||||||
this.time += dt;
|
this.time += dt;
|
||||||
|
const previousDay = this.day || 1;
|
||||||
this.day = Math.floor(this.time / CONFIG.dayLength) + 1;
|
this.day = Math.floor(this.time / CONFIG.dayLength) + 1;
|
||||||
|
if (this.day !== previousDay) {
|
||||||
|
for (const t of this.tarinai || []) if (t) t.personalityDaily = { day: this.day, total: 0, byKey: {} };
|
||||||
|
}
|
||||||
if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4);
|
if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4);
|
||||||
this.updateWeather(dt);
|
this.updateWeather(dt);
|
||||||
this.rebuildSpatial();
|
this.rebuildSpatial();
|
||||||
|
|
@ -2242,7 +2411,7 @@ class World {
|
||||||
t.vx = 0;
|
t.vx = 0;
|
||||||
t.vy = 0;
|
t.vy = 0;
|
||||||
t.entryTimer = 0;
|
t.entryTimer = 0;
|
||||||
t.die("莉墓ァ倥↓繧医j逕サ髱「螟悶〒蜑企勁");
|
t.die(HEALTH?.CAUSES?.outOfBounds || "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664");
|
||||||
} else if (result === "moved") {
|
} else if (result === "moved") {
|
||||||
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
|
t.x = clamp(Number.isFinite(t.x) ? t.x : this.w * 0.5, pad, this.w - pad);
|
||||||
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
|
t.y = clamp(Number.isFinite(t.y) ? t.y : this.h * 0.5, pad, this.h - pad);
|
||||||
|
|
@ -2289,11 +2458,104 @@ class World {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
placementBlocked(item) {
|
||||||
|
if (!item || item.dead) return true;
|
||||||
|
const pad = CONFIG.worldPadding || 30;
|
||||||
|
const rects = this.solidObstacleRects(item);
|
||||||
|
if (rects.length) {
|
||||||
|
for (const rect of rects) {
|
||||||
|
if (rect.left < pad || rect.top < pad || rect.right > this.w - pad || rect.bottom > this.h - pad) return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
|
||||||
|
if (item.x - radius < pad || item.y - radius < pad || item.x + radius > this.w - pad || item.y + radius > this.h - pad) return true;
|
||||||
|
}
|
||||||
|
if (item.type === "grass" && !this.grassSpotOpen(item.x, item.y, { avoidTarinai: true })) return true;
|
||||||
|
if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true;
|
||||||
|
if (item.type === "nest_box") {
|
||||||
|
const margin = Math.max(8, (item.r || 42) * 0.18);
|
||||||
|
for (const it of this.nearbyItems(item.x, item.y, Math.max(120, (item.r || 42) * 3))) {
|
||||||
|
if (!it || it === item || it.dead) continue;
|
||||||
|
for (const rect of this.solidObstacleRects(it)) {
|
||||||
|
for (const own of rects) {
|
||||||
|
const separated = own.right + margin < rect.left || own.left - margin > rect.right || own.bottom + margin < rect.top || own.top - margin > rect.bottom;
|
||||||
|
if (!separated) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
placementClampPointFor(item, x = item?.x || 0, y = item?.y || 0) {
|
||||||
|
const pad = CONFIG.worldPadding || 30;
|
||||||
|
const rects = this.solidObstacleRects(item);
|
||||||
|
if (rects.length) {
|
||||||
|
let left = 0, right = 0, top = 0, bottom = 0;
|
||||||
|
for (const rect of rects) {
|
||||||
|
left = Math.max(left, (item.x || 0) - rect.left);
|
||||||
|
right = Math.max(right, rect.right - (item.x || 0));
|
||||||
|
top = Math.max(top, (item.y || 0) - rect.top);
|
||||||
|
bottom = Math.max(bottom, rect.bottom - (item.y || 0));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: clamp(x, pad + left, this.w - pad - right),
|
||||||
|
y: clamp(y, pad + top, this.h - pad - bottom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
|
||||||
|
return {
|
||||||
|
x: clamp(x, pad + radius, this.w - pad - radius),
|
||||||
|
y: clamp(y, pad + radius, this.h - pad - radius),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
placementProbeItem(item, x, y) {
|
||||||
|
return { ...item, x, y, dead: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
findPlacementSpot(item, { allowOriginal = true, maxRadius = null, attempts = null } = {}) {
|
||||||
|
if (!item || item.dead) return null;
|
||||||
|
if (item.type === "grass") {
|
||||||
|
const spot = this.findGrassPlantingSpot
|
||||||
|
? this.findGrassPlantingSpot(item.x, item.y, { allowOriginal, attempts: attempts || 42, maxRadius: maxRadius || 170, avoidTarinai: true })
|
||||||
|
: null;
|
||||||
|
if (!spot) return null;
|
||||||
|
const probe = this.placementProbeItem(item, spot.x, spot.y);
|
||||||
|
return this.placementBlocked(probe) ? null : spot;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = this.placementClampPointFor(item, item.x, item.y);
|
||||||
|
const original = this.placementProbeItem(item, start.x, start.y);
|
||||||
|
if (allowOriginal && !this.placementBlocked(original)) return start;
|
||||||
|
|
||||||
|
const solid = this.isFenceType(item.type) || item.type === "nest_box";
|
||||||
|
const searchRadius = maxRadius || (solid ? Math.max(150, (item.r || 42) * 4.2) : Math.max(90, (item.r || 16) * 3.2));
|
||||||
|
const count = attempts || (solid ? 72 : 36);
|
||||||
|
const golden = Math.PI * (3 - Math.sqrt(5));
|
||||||
|
let best = null;
|
||||||
|
let bestD = Infinity;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const t = count <= 1 ? 1 : i / (count - 1);
|
||||||
|
const radius = Math.max(8, searchRadius * Math.sqrt(t));
|
||||||
|
const angle = i * golden + stableUnit(`${item.type}:${item.x.toFixed(1)},${item.y.toFixed(1)}`, "place") * Math.PI * 2;
|
||||||
|
const rawX = item.x + Math.cos(angle) * radius;
|
||||||
|
const rawY = item.y + Math.sin(angle) * radius * 0.74;
|
||||||
|
const p = this.placementClampPointFor(item, rawX, rawY);
|
||||||
|
const probe = this.placementProbeItem(item, p.x, p.y);
|
||||||
|
if (this.placementBlocked(probe)) continue;
|
||||||
|
const d = distXY(item.x, item.y, p.x, p.y);
|
||||||
|
if (d < bestD) { best = p; bestD = d; }
|
||||||
|
if (d < Math.max(18, (item.r || 12) * 0.75)) break;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
findGrabTargetAt(x, y) {
|
findGrabTargetAt(x, y) {
|
||||||
let foundT = null;
|
let foundT = null;
|
||||||
for (let i = this.tarinai.length - 1; i >= 0; i--) {
|
for (let i = this.tarinai.length - 1; i >= 0; i--) {
|
||||||
const t = this.tarinai[i];
|
const t = this.tarinai[i];
|
||||||
if (!t || t.dead) continue;
|
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
if (t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.4) { foundT = t; break; }
|
if (t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.4) { foundT = t; break; }
|
||||||
}
|
}
|
||||||
let foundItem = null, foundItemD = Infinity;
|
let foundItem = null, foundItemD = Infinity;
|
||||||
|
|
@ -2324,10 +2586,10 @@ class World {
|
||||||
showToast("\u67f5\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u5148\u306b\u4e00\u90e8\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
showToast("\u67f5\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u5148\u306b\u4e00\u90e8\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.fencePlacementBlocked(item)) {
|
}
|
||||||
showToast("\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002");
|
if (this.placementBlocked(item)) {
|
||||||
return null;
|
showToast(this.isFenceType(item.type) ? "\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002" : "\u305d\u3053\u306b\u306f\u914d\u7f6e\u3067\u304d\u307e\u305b\u3093\u3002");
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
if (dropped) {
|
if (dropped) {
|
||||||
item.dropMax = item.type === "stone" ? 0.72 : 0.58;
|
item.dropMax = item.type === "stone" ? 0.72 : 0.58;
|
||||||
|
|
@ -2345,7 +2607,7 @@ class World {
|
||||||
if (tool === "observe") {
|
if (tool === "observe") {
|
||||||
let found = null;
|
let found = null;
|
||||||
for (let i = this.tarinai.length - 1; i >= 0; i--) {
|
for (let i = this.tarinai.length - 1; i >= 0; i--) {
|
||||||
if (this.tarinai[i].contains(x, y)) { found = this.tarinai[i]; break; }
|
if (!this.isTarinaiHiddenInNestBox(this.tarinai[i]) && this.tarinai[i].contains(x, y)) { found = this.tarinai[i]; break; }
|
||||||
}
|
}
|
||||||
this.selected = found;
|
this.selected = found;
|
||||||
if (found) this.log(`${found.name}\u3092\u9078\u629e\u3057\u305f\u3002`);
|
if (found) this.log(`${found.name}\u3092\u9078\u629e\u3057\u305f\u3002`);
|
||||||
|
|
@ -2398,31 +2660,19 @@ class World {
|
||||||
this.compactItems();
|
this.compactItems();
|
||||||
this.updateItemCounts();
|
this.updateItemCounts();
|
||||||
this.rebuildSpatial();
|
this.rebuildSpatial();
|
||||||
const label = { zunchi: "\u305a\u3093\u3061", food: "\u98df\u3079\u7269", sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", sleep_drug: "\u306d\u3080\u308a\u85ac", grass: "\u8349", stone: "\u77f3", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", firecracker: "\u7206\u7af9", fence_v: "\u7e26\u306e\u67f5", fence_h: "\u6a2a\u306e\u67f5", trace: "\u8db3\u8de1", splat: "\u3057\u3076\u304d" }[best.type] || best.type;
|
const label = toolLabel(best.type);
|
||||||
this.log(`${label}\u3092\u524a\u9664\u3057\u305f\u3002`, "observe");
|
this.log(`${label}\u3092\u524a\u9664\u3057\u305f\u3002`, "observe");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemType = {
|
const itemType = toolItemType(tool);
|
||||||
zunchi: "zunchi", sweet: "sweet", love_mochi: "love_mochi", fight_mochi: "fight_mochi", sleep_drug: "sleep_drug", water: "water", water_bowl: "water_bowl", grass: "grass",
|
|
||||||
stone: "stone", bed: "bed", nest_box: "nest_box", ball: "ball", firecracker: "firecracker", fence_v: "fence_v", fence_h: "fence_h"
|
|
||||||
}[tool];
|
|
||||||
if (itemType) {
|
if (itemType) {
|
||||||
let px = x, py = y;
|
let px = x, py = y;
|
||||||
if (itemType === "grass") {
|
|
||||||
const spot = this.findGrassPlantingSpot(x, y, { allowOriginal: true, minRadius: 22, maxRadius: 128, attempts: 48 });
|
|
||||||
if (!spot) {
|
|
||||||
showToast("\u8349\u306b\u306f\u7a7a\u3044\u305f\u571f\u304c\u5fc5\u8981\u3067\u3059\u3002");
|
|
||||||
this.log("\u305d\u3053\u306b\u306f\u8349\u304c\u690d\u3048\u3089\u308c\u308b\u7a7a\u304d\u5730\u304c\u3042\u308a\u307e\u305b\u3093\u3002");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
px = spot.x;
|
|
||||||
py = spot.y;
|
|
||||||
}
|
|
||||||
const rawItem = this.applyToolSize(new Item(itemType, px, py));
|
const rawItem = this.applyToolSize(new Item(itemType, px, py));
|
||||||
// 譟オ縺ッ隕九◆逶ョ縺ョ遏ゥ蠖「縺後ヵ繧」繝シ繝ォ繝牙、悶↓縺ッ縺ソ蜃コ縺吩ス咲スョ縺ォ縺ッ鄂ョ縺九↑縺・・ // 驟咲スョ繝励Ξ繝薙Η繝シ縺ョ襍、陦ィ遉コ縺ィ螳滄圀縺ョ驟咲スョ蜿ッ蜷ヲ繧剃ク閾エ縺輔○繧九・ const placed = this.placeItem(rawItem, true);
|
// Placement uses the same footprint that the preview validates.
|
||||||
|
const placed = this.placeItem(rawItem, true);
|
||||||
if (!placed) return;
|
if (!placed) return;
|
||||||
const label = { zunchi: "\u305a\u3093\u3061", food: "\u98df\u3079\u7269", sweet: "\u305a\u3093\u3060\u9905", love_mochi: "\u3078\u3053\u9905", fight_mochi: "\u3051\u3093\u304b\u9905", water: "\u6c34", water_bowl: "\u6c34\u306e\u76bf", sleep_drug: "\u306d\u3080\u308a\u85ac", grass: "\u8349", stone: "\u77f3", bed: "\u5e72\u8349\u5bdd\u5e8a", nest_box: "\u5de3\u7bb1", ball: "\u30dc\u30fc\u30eb", firecracker: "\u7206\u7af9", fence_v: "\u7e26\u306e\u67f5", fence_h: "\u6a2a\u306e\u67f5" }[itemType];
|
const label = toolLabel(itemType);
|
||||||
this.log(`${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
|
this.log(`${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2441,22 +2691,25 @@ class World {
|
||||||
return "note";
|
return "note";
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldHideFromObservationRecords(text = "") {
|
shouldHideFromObservationRecords(entry = {}) {
|
||||||
const s = String(text || "");
|
const type = typeof entry === "string" ? "" : (entry.eventType || "");
|
||||||
return [
|
return type === "ball_poke" || type === "avoid_relationship";
|
||||||
"\u30dc\u30fc\u30eb\u3092\u9023\u7d9a\u3067\u3064\u3064",
|
|
||||||
"\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066",
|
|
||||||
"\u3092\u907f\u3051\u305f",
|
|
||||||
].some(part => s.includes(part));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(text, kind = null) {
|
log(text, kind = null, options = {}) {
|
||||||
const entry = { time: this.time, text, kind: kind || this.inferLogKind(text) };
|
const entry = {
|
||||||
|
time: this.time,
|
||||||
|
text,
|
||||||
|
kind: kind || this.inferLogKind(text),
|
||||||
|
eventType: options.eventType || "",
|
||||||
|
hiddenFromObservation: Boolean(options.hiddenFromObservation),
|
||||||
|
};
|
||||||
|
if (this.shouldHideFromObservationRecords(entry)) entry.hiddenFromObservation = true;
|
||||||
if (!this.eventCounters) this.eventCounters = {};
|
if (!this.eventCounters) this.eventCounters = {};
|
||||||
this.eventCounters[entry.kind] = (this.eventCounters[entry.kind] || 0) + 1;
|
this.eventCounters[entry.kind] = (this.eventCounters[entry.kind] || 0) + 1;
|
||||||
for (const t of this.tarinai || []) {
|
for (const t of this.tarinai || []) {
|
||||||
if (!t || !t.name || !text || !String(text).includes(t.name)) continue;
|
if (!t || !t.name || !text || !String(text).includes(t.name)) continue;
|
||||||
if (!this.shouldHideFromObservationRecords(text) && t.addRecord) t.addRecord(text, entry.kind);
|
if (!entry.hiddenFromObservation && t.addRecord) t.addRecord(text, entry.kind);
|
||||||
}
|
}
|
||||||
this.logs.unshift(entry);
|
this.logs.unshift(entry);
|
||||||
this.logs = this.logs.slice(0, 100);
|
this.logs = this.logs.slice(0, 100);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue