many
|
|
@ -1,6 +1,21 @@
|
|||
# Patch notes
|
||||
|
||||
## 15.20.3
|
||||
## 15.20.12
|
||||
|
||||
- Added canonical item visual, food, and disease registries; wired food decay/visual helpers and mystery-drug disease sampling to registry data.
|
||||
- Generated the tool palette from `TOOL_CATEGORIES` at runtime and removed duplicated category markup from `index.html`.
|
||||
- Added compact family-tree personality tags and a selected-Tarinai recent-change section for personality, damage, item-effect, disease, and ant-carry causes.
|
||||
- Added `InputModeManager` for touch-first mobile classification and `WeatherSystem` for weather transitions/rain-water spawning.
|
||||
- Added central domain events for placement, eating, decay, effects, diseases, damage, death, birth, fights, ants, weather, and rain item drops.
|
||||
- Moved item-effect modifiers, active summaries, apply/tick behavior callbacks, and food interest class queries into registries.
|
||||
- Moved canonical sound gains, sample paths, categories, cooldowns, and ID category mapping out of `audio.js` into `sound_pack.js`.
|
||||
- Added named ant ecology extension hooks/comments without changing ant balance.
|
||||
- Bumped cache/version references to `15.20.12`; documented the new source-of-truth rules.
|
||||
|
||||
## 15.20.12
|
||||
|
||||
- Updated documentation after inspecting the current game surface: field choices, startup flow, tool groups, chart/family/debug surfaces, localStorage settings, and validation fallback when Node is unavailable.
|
||||
- Removed unused `css/styles.css` shim and no-op `js/ui_family.js` loader entry; refactored duplicate world initialization and allocation-heavy live-count paths.
|
||||
|
||||
- Removed item-effect speech bubbles: `ドン`, `ぐるる`, `すっきり`, `Hg xN`, `ちまっ`, `でかっ`, `へにゃ`, `ムキッ`, and `mm...`.
|
||||
- Reorganized the tool palette into color-coded collapsible groups: operation, food/water/grass, medicine/effects, habitat/living things, hazards/obstacles.
|
||||
|
|
@ -12,7 +27,7 @@
|
|||
- Tightened mobile operation UI visibility to smartphone-class compact touch devices.
|
||||
- Sleep-disease Tarinai can be moved by ant hauling without snapping back to their sleep anchor.
|
||||
|
||||
## 15.20.2 and earlier summary
|
||||
## 15.20.12 and earlier summary
|
||||
|
||||
- Consolidated documentation into dense English files; left `docs/prompt.txt` untouched.
|
||||
- Restored rain, revised water/zunda/mercury item sprites, added zunchi-slave head zunchi, fight win rate, fight defeat chance, and post-wake lock.
|
||||
|
|
|
|||
26
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# Tarinai Colony Observation Game
|
||||
# Tarinai Colony Observation Game
|
||||
|
||||
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`.
|
||||
Buildless single-page browser simulation. Open `index.html`; no package manager, transpiler, server API, or module loader is required. Runtime order is explicit `<script defer>` order generated from `app_manifest.json`. Current inspected version: `15.20.9`.
|
||||
|
||||
## Edit contract
|
||||
|
||||
|
|
@ -18,18 +18,34 @@ Buildless single-page browser simulation. Open `index.html`; no package manager,
|
|||
- `docs/MAINTENANCE.md`: release process, validation, debugging, cache/version rules.
|
||||
- `docs/prompt.txt`: untouched user prompt/context archive.
|
||||
|
||||
## Player-facing surface
|
||||
|
||||
- Startup uses a loading overlay that eagerly preloads a small sprite set, initializes UI/world state, then defers remaining image work.
|
||||
- Field choices are cage, garden, and park. `FIELD_TYPES` controls world scale, starting population, initial grass count, and responsive field height.
|
||||
- The default field is garden. Reset opens the field dialog; cancel keeps the current field.
|
||||
- Tools are grouped in collapsible categories: operation/observation, food/water/grass, medicine/effects, habitat/living things, hazards/obstacles.
|
||||
- The top bar owns pause, speed, ecology, sound, credits, and reset controls. Event push toasts live inside the Event card, while sound category toggles live only in the sound menu.
|
||||
- The colony panel charts population, mental state, environment, and object counts. The family tree is a separate main section with manual/auto refresh and reset controls.
|
||||
- Mobile operation modes are available only on compact touch-first phone viewports: auto, camera, tool, family.
|
||||
|
||||
## Core systems
|
||||
|
||||
- `World`: `js/world.js` plus `js/world_*.js`; time, weather, placement, effects, family records, spatial queries, ants, combat, logging.
|
||||
- `Tarinai`: `js/tarinai.js` plus `js/tarinai_*.js`; identity, personality, needs, items, disease, movement, social behavior, rendering.
|
||||
- `Item`: `js/items.js`; lifecycle, scaling, field rendering. Field rendering is the visual source of truth for tool icons.
|
||||
- `EffectRegistry`: `js/effect_registry.js`; item-effect names, exclusivity, display metadata, random-effect candidates.
|
||||
- `Audio`: `js/audio.js` + `js/sound_pack.js`; category toggles, synthetic fallbacks, sample paths, per-ID volume/cooldown.
|
||||
- `ItemVisualRegistry`: `js/item_visual_registry.js`; canonical item visuals used by field sprites and generated icon previews.
|
||||
- `EffectRegistry`: `js/effect_registry.js`; item-effect names, modifiers, exclusivity, display metadata, random-effect candidates.
|
||||
- `DiseaseRegistry`: `js/disease_registry.js`; disease flags, labels, display metadata, cure-rule notes, mystery-drug eligibility.
|
||||
- `FoodRegistry`: `js/food_registry.js`; serving classes, labels, decay, hygiene penalty, special effect IDs.
|
||||
- `InputModeManager`: `js/input_mode_manager.js`; mobile/touch classification, gesture capability policy, mode persistence.
|
||||
- `WeatherSystem`: `js/weather_system.js`; weather state transitions and rain-spawned water items.
|
||||
- `Audio`: `js/sound_pack.js` owns sound metadata; `js/audio.js` loads samples, applies settings, enforces cooldowns, and runs synthetic fallbacks.
|
||||
- `Performance`: `js/performance_manager.js`; FPS quality level, ant stride, effect budget, terrain redraw cadence.
|
||||
|
||||
## Current high-risk areas
|
||||
|
||||
- Item effects touch `effect_registry`, `tarinai_item_effects`, `tarinai_needs_items`, `items`, and selected-info UI.
|
||||
- Mobile input must remain hidden on desktop-class devices; use body class `touch-mobile-ui` only for compact no-hover coarse-pointer devices.
|
||||
- Mobile input must remain hidden on desktop-class devices; `TarinaiInputMode.snapshot().touchFirst` controls body class `touch-mobile-ui`.
|
||||
- Weather rain overlay is diagonal line streaks. Fallen water items, zunda juice, and mercury share the same horizontal oval item-droplet silhouette; recolor only.
|
||||
- Tool UI icons must be generated previews of `js/items.js` drawing helpers, never separate designs.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "15.20.3",
|
||||
"version": "15.20.16",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -11,9 +11,13 @@
|
|||
"js/version.js",
|
||||
"js/event_bus.js",
|
||||
"js/performance_manager.js",
|
||||
"js/disease_registry.js",
|
||||
"js/effect_registry.js",
|
||||
"js/sound_pack.js",
|
||||
"js/data.js",
|
||||
"js/item_visual_registry.js",
|
||||
"js/food_registry.js",
|
||||
"js/input_mode_manager.js",
|
||||
"js/math.js",
|
||||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
|
|
@ -36,6 +40,7 @@
|
|||
"js/world_environment.js",
|
||||
"js/world_spatial_budget.js",
|
||||
"js/world_ants_system.js",
|
||||
"js/weather_system.js",
|
||||
"js/world_update.js",
|
||||
"js/world_placement_log.js",
|
||||
"js/world_bootstrap.js",
|
||||
|
|
@ -47,7 +52,6 @@
|
|||
"js/ui_tools.js",
|
||||
"js/ui_bind.js",
|
||||
"js/ui_charts.js",
|
||||
"js/ui_family.js",
|
||||
"js/ui_family_data.js",
|
||||
"js/ui_family_async.js",
|
||||
"js/ui_family_layout.js",
|
||||
|
|
@ -65,3 +69,4 @@
|
|||
],
|
||||
"notes": "Source of truth for static loading order; generated files are index.html script/link tags and service-worker.js cache list."
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
assets/objects/pushpin.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
assets/objects/pushpin_stuck.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 66 KiB |
BIN
assets/sprites/tarinai_29_hungry_70.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/sprites/tarinai_30_zunchi_slave_alt.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
assets/ui/tool_pushpin.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
23
css/base.css
|
|
@ -1,4 +1,4 @@
|
|||
/* Generated CSS split from styles.css; preserve load order. */
|
||||
/* CSS split file; preserve manifest load order. */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;500;700&family=M+PLUS+Rounded+1c:wght@400;500;700&family=Yomogi&display=swap");
|
||||
|
||||
:root {
|
||||
|
|
@ -399,13 +399,14 @@ h1 {
|
|||
.tool[data-tool="fight_mochi"] { --tool-icon: url("../assets/ui/tool_fight_mochi.webp"); }
|
||||
.tool[data-tool="water_bowl"] { --tool-icon: url("../assets/ui/tool_water_bowl.webp"); }
|
||||
|
||||
/* v15.20.3: updated parameter item tool icons */
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.3"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.3"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.3"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.3"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.3"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.3"); }
|
||||
/* v15.20.11: updated parameter item tool icons */
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.11"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.11"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.11"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.11"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.11"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.11"); }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Generated CSS split from styles.css; preserve load order. */
|
||||
/* CSS split file; preserve manifest load order. */
|
||||
.tool-tip-popover {
|
||||
position: fixed;
|
||||
z-index: 2147483000;
|
||||
|
|
@ -70,6 +70,39 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.selected-change-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.selected-change-list li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 0.85fr) minmax(120px, 1.3fr) 34px;
|
||||
gap: 6px;
|
||||
align-items: start;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.selected-change-list span {
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.selected-change-list strong,
|
||||
.selected-change-list em {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.selected-change-list strong {
|
||||
font-weight: 650;
|
||||
}
|
||||
.selected-change-list em {
|
||||
color: var(--muted);
|
||||
font-style: normal;
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
.relation-list-row {
|
||||
align-items: start;
|
||||
}
|
||||
|
|
@ -409,3 +442,111 @@
|
|||
background: radial-gradient(circle at top, rgba(255,251,243,0.98), rgba(245,238,225,0.96) 56%, rgba(234,225,208,0.98));
|
||||
transition: opacity .22s ease, visibility .22s ease;
|
||||
}
|
||||
|
||||
.selected-info .info-row[data-metric] {
|
||||
border-radius: 8px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.selected-info .info-row[data-metric="hunger"] { background: rgba(244, 174, 88, 0.10); }
|
||||
.selected-info .info-row[data-metric="loneliness"] { background: rgba(116, 150, 214, 0.10); }
|
||||
.selected-info .info-row[data-metric="energy"] { background: rgba(104, 184, 96, 0.10); }
|
||||
.selected-info .info-row[data-metric="stress"] { background: rgba(214, 104, 88, 0.10); }
|
||||
.selected-info .info-row[data-metric="zunchi"] { background: rgba(112, 142, 70, 0.10); }
|
||||
.selected-info .info-row[data-metric="mood"] { background: rgba(222, 196, 86, 0.10); }
|
||||
.selected-info .info-row[data-metric="lack"] { background: rgba(142, 112, 184, 0.10); }
|
||||
.selected-personality-changes {
|
||||
align-items: start;
|
||||
}
|
||||
.selected-personality-changes > strong {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.selected-category[data-selected-category="basic"] { background: rgba(246, 236, 214, 0.32); }
|
||||
.selected-category[data-selected-category="health"] { background: rgba(232, 246, 222, 0.32); }
|
||||
.selected-category[data-selected-category="effects"] { background: rgba(238, 229, 252, 0.28); }
|
||||
.selected-category[data-selected-category="personality"] { background: rgba(255, 239, 214, 0.34); }
|
||||
.selected-category[data-selected-category="relation"] { background: rgba(224, 240, 252, 0.30); }
|
||||
.selected-category[data-selected-category="family"] { background: rgba(246, 226, 236, 0.28); }
|
||||
.selected-category[data-selected-category="records"] { background: rgba(242, 238, 226, 0.30); }
|
||||
|
||||
/* v15.20.6: color-blind safe panel themes and alternating inner tones. */
|
||||
.card.tool-card,
|
||||
.card.selected-card,
|
||||
.card.colony-card,
|
||||
.card.log-card {
|
||||
--ui-theme-rgb: 102, 102, 102;
|
||||
--ui-theme-strong: rgba(var(--ui-theme-rgb), 0.64);
|
||||
--ui-theme-mid: rgba(var(--ui-theme-rgb), 0.24);
|
||||
--ui-theme-soft: rgba(var(--ui-theme-rgb), 0.105);
|
||||
--ui-theme-soft-2: rgba(var(--ui-theme-rgb), 0.165);
|
||||
border-color: var(--ui-theme-mid);
|
||||
box-shadow: inset 4px 0 0 var(--ui-theme-strong), 0 10px 30px rgba(64, 48, 32, 0.08);
|
||||
}
|
||||
.card.tool-card { --ui-theme-rgb: 0, 114, 178; background: linear-gradient(180deg, rgba(242, 248, 252, 0.94), rgba(232, 243, 250, 0.91)); }
|
||||
.card.selected-card { --ui-theme-rgb: 204, 121, 167; background: linear-gradient(180deg, rgba(253, 245, 250, 0.94), rgba(249, 235, 244, 0.91)); }
|
||||
.card.colony-card { --ui-theme-rgb: 0, 158, 115; background: linear-gradient(180deg, rgba(240, 250, 247, 0.94), rgba(229, 245, 239, 0.91)); }
|
||||
.card.log-card { --ui-theme-rgb: 230, 159, 0; background: linear-gradient(180deg, rgba(255, 249, 236, 0.94), rgba(253, 242, 215, 0.91)); }
|
||||
.card.tool-card .panel-card-toggle,
|
||||
.card.colony-card .panel-card-toggle,
|
||||
.card.log-card .panel-card-toggle {
|
||||
border: 1px solid var(--ui-theme-mid);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), var(--ui-theme-soft-2));
|
||||
}
|
||||
.selected-card .selected-head {
|
||||
border: 1px solid var(--ui-theme-mid);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), var(--ui-theme-soft-2));
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.tool-card .tool-category:nth-child(odd),
|
||||
.selected-card .selected-category:nth-child(odd) {
|
||||
background: var(--ui-theme-soft);
|
||||
border-color: var(--ui-theme-mid);
|
||||
}
|
||||
.tool-card .tool-category:nth-child(even),
|
||||
.selected-card .selected-category:nth-child(even) {
|
||||
background: var(--ui-theme-soft-2);
|
||||
border-color: rgba(var(--ui-theme-rgb), 0.30);
|
||||
}
|
||||
.tool-card .tool-category-toggle,
|
||||
.selected-card .selected-category-toggle {
|
||||
background: rgba(255, 255, 255, 0.54);
|
||||
border-bottom-color: rgba(var(--ui-theme-rgb), 0.20);
|
||||
}
|
||||
.tool-card .tool-category:nth-child(even) .tool-category-toggle,
|
||||
.selected-card .selected-category:nth-child(even) .selected-category-toggle {
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
}
|
||||
.tool-card .tool-category-toggle::before,
|
||||
.selected-card .selected-category-toggle::before,
|
||||
.card .panel-card-state::before {
|
||||
color: var(--ui-theme-strong);
|
||||
}
|
||||
.colony-card .stat:nth-child(odd),
|
||||
.colony-card .chart-tabs button:nth-child(odd),
|
||||
.log-card .log-entry:nth-child(odd),
|
||||
.log-card .push-kind-grid label:nth-child(odd) {
|
||||
background: var(--ui-theme-soft);
|
||||
border-color: var(--ui-theme-mid);
|
||||
}
|
||||
.colony-card .stat:nth-child(even),
|
||||
.colony-card .chart-tabs button:nth-child(even),
|
||||
.log-card .log-entry:nth-child(even),
|
||||
.log-card .push-kind-grid label:nth-child(even) {
|
||||
background: var(--ui-theme-soft-2);
|
||||
border-color: rgba(var(--ui-theme-rgb), 0.30);
|
||||
}
|
||||
.colony-card .chart-tabs button.active,
|
||||
.log-card .push-master,
|
||||
.selected-card .selected-name-input:focus {
|
||||
border-color: var(--ui-theme-strong);
|
||||
outline-color: rgba(var(--ui-theme-rgb), 0.24);
|
||||
}
|
||||
.colony-card .chart-tabs button.active {
|
||||
background: rgba(var(--ui-theme-rgb), 0.22);
|
||||
}
|
||||
.selected-card .badge,
|
||||
.selected-card .personality-tag {
|
||||
background: rgba(var(--ui-theme-rgb), 0.11);
|
||||
border-color: rgba(var(--ui-theme-rgb), 0.28);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Generated CSS split from styles.css; preserve load order. */
|
||||
/* CSS split file; preserve manifest load order. */
|
||||
.tool[data-tool="sleep_drug"] { --tool-icon: url("../assets/ui/tool_sleep_drug.webp"); }
|
||||
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.webp"); }
|
||||
.tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.webp"); }
|
||||
|
|
@ -365,7 +365,7 @@ meter {
|
|||
}
|
||||
|
||||
|
||||
/* v15.20.3: clearer tool category grouping. */
|
||||
/* v15.20.4: clearer tool category grouping. */
|
||||
.tool-category[data-tool-category="operate"] { --tool-cat-a: rgba(224,239,255,0.82); --tool-cat-b: rgba(203,226,255,0.74); --tool-cat-line: rgba(82,128,190,0.26); }
|
||||
.tool-category[data-tool-category="food"] { --tool-cat-a: rgba(236,252,219,0.82); --tool-cat-b: rgba(214,244,194,0.74); --tool-cat-line: rgba(96,156,72,0.26); }
|
||||
.tool-category[data-tool-category="medicine"] { --tool-cat-a: rgba(243,231,255,0.84); --tool-cat-b: rgba(230,214,252,0.78); --tool-cat-line: rgba(136,94,184,0.28); }
|
||||
|
|
@ -388,3 +388,4 @@ meter {
|
|||
@media (min-width: 921px) {
|
||||
.mobile-mode-controls { display: none !important; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Generated CSS split from styles.css; preserve load order. */
|
||||
/* CSS split file; preserve manifest load order. */
|
||||
.loading-screen.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
|
||||
.loading-kicker { font-size: 12px; letter-spacing: .14em; color: rgba(116,96,76,.82); margin-bottom: 6px; }
|
||||
.loading-title { margin: 0 0 8px; font-size: 28px; color: #3f3429; }
|
||||
|
|
@ -180,15 +180,15 @@
|
|||
}
|
||||
|
||||
/* v15.18.0: item icons and mobile input mode */
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.3"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.3"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.3"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.3"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.3"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.3"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.3"); }
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.11"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.11"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.11"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.11"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.11"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.11"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.11"); }
|
||||
.mobile-mode-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
|
@ -223,3 +223,10 @@ body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; }
|
|||
body.touch-mobile-ui.mobile-mode-camera #gameCanvas { cursor: grab; }
|
||||
body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
||||
body.touch-mobile-ui.mobile-mode-family .lineage-main-card { z-index: 9; }
|
||||
|
||||
|
||||
/* v15.20.11: keep panel themes on narrow layouts. */
|
||||
.card.tool-card { --ui-theme-rgb: 0, 114, 178; background: linear-gradient(180deg, rgba(242, 248, 252, 0.94), rgba(232, 243, 250, 0.91)); border-color: rgba(0, 114, 178, 0.24); }
|
||||
.card.selected-card { --ui-theme-rgb: 204, 121, 167; background: linear-gradient(180deg, rgba(253, 245, 250, 0.94), rgba(249, 235, 244, 0.91)); border-color: rgba(204, 121, 167, 0.24); }
|
||||
.card.colony-card { --ui-theme-rgb: 0, 158, 115; background: linear-gradient(180deg, rgba(240, 250, 247, 0.94), rgba(229, 245, 239, 0.91)); border-color: rgba(0, 158, 115, 0.24); }
|
||||
.card.log-card { --ui-theme-rgb: 230, 159, 0; background: linear-gradient(180deg, rgba(255, 249, 236, 0.94), rgba(253, 242, 215, 0.91)); border-color: rgba(230, 159, 0, 0.24); }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Generated CSS split from styles.css; preserve load order. */
|
||||
/* CSS split file; preserve manifest load order. */
|
||||
.lineage-tree {
|
||||
overflow: auto;
|
||||
padding: 8px 10px 18px;
|
||||
|
|
@ -114,8 +114,8 @@ body::before {
|
|||
position: absolute;
|
||||
right: -6px;
|
||||
bottom: -8px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
width: calc(64px * var(--family-icon-scale, 1));
|
||||
height: calc(64px * var(--family-icon-scale, 1));
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -142,6 +142,32 @@ body::before {
|
|||
margin-top: 1px;
|
||||
color: rgba(126, 80, 64, 0.92);
|
||||
}
|
||||
.family-personality-tags {
|
||||
display: flex !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
white-space: normal !important;
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
}
|
||||
.family-personality-tags em {
|
||||
display: inline-block;
|
||||
max-width: none;
|
||||
padding: 1px 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(103,178,44,0.11);
|
||||
border: 1px solid rgba(103,178,44,0.20);
|
||||
color: rgba(64,72,48,0.86);
|
||||
font-size: 7.5px;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.family-copy strong { font-size: 11px; }
|
||||
.family-copy span { color: var(--muted); font-size: 8.5px; margin-top: 1px; }
|
||||
|
||||
|
|
@ -330,8 +356,8 @@ body::before {
|
|||
.lineage-family-tree .tree-node {
|
||||
position: absolute;
|
||||
width: 132px;
|
||||
height: 68px;
|
||||
min-height: 68px;
|
||||
height: 84px;
|
||||
min-height: 84px;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
/* v15.18.0: split into base.css, layout.css, panel.css, components.css, mobile.css. Keep this file as a compatibility shim only. */
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
# Architecture
|
||||
# Architecture
|
||||
|
||||
Build target: static browser page. No bundler. Globals and ordered deferred scripts are intentional.
|
||||
Build target: static browser page. No bundler. Globals and ordered deferred scripts are intentional. Current inspected version: `15.20.11`.
|
||||
|
||||
## Load order
|
||||
|
||||
`app_manifest.json` owns CSS/JS order. Regenerate `index.html` and `service-worker.js` with `python3 scripts/generate_app_files.py` after version/order changes. `js/version.js` exposes `TARINAI_APP.version`, cache name, and query helper.
|
||||
|
||||
The old `css/styles.css` compatibility shim and `js/ui_family.js` split marker have been removed. The family tree entry point now starts at `ui_family_data.js` and continues through async/layout/path/render modules.
|
||||
|
||||
## Runtime objects
|
||||
|
||||
- `World`: constructor in `js/world.js`; methods split into view, family/social, combat/effects, environment, spatial/budget, ants, update, placement/log, bootstrap.
|
||||
|
|
@ -14,18 +16,44 @@ Build target: static browser page. No bundler. Globals and ordered deferred scri
|
|||
- `Effect`: transient visuals in `js/sim_core.js`; combat/weather/food effects are spawned through `World` helpers.
|
||||
- `TarinaiEvents`: global event bus for logs, terrain dirtiness, audio failures, phase/performance changes.
|
||||
- `TarinaiPerformance`: FPS quality manager; provides effect caps, ant stride, terrain redraw cadence, UI intervals.
|
||||
- `TarinaiInputMode`: input classification and mobile-mode state. UI code reads this manager instead of duplicating touch heuristics.
|
||||
- `TarinaiWeatherSystem`: weather transitions and rain-spawned water item creation. Rain overlay drawing remains in `render.js`.
|
||||
|
||||
## Startup path
|
||||
|
||||
`main.js` owns the user-visible boot sequence:
|
||||
|
||||
1. Start the loading showcase and preload the initial sprite/image set through `loadImages`.
|
||||
2. Bind UI, apply the default `garden` field layout, reset the world, and select `observe`.
|
||||
3. Render logs, family archive, first frame, and stats.
|
||||
4. Start deferred background image loading, enter `requestAnimationFrame(loop)`, and hide the loading screen.
|
||||
5. Register `service-worker.js` with `updateViaCache: "none"` after window load.
|
||||
|
||||
## Data ownership
|
||||
|
||||
- `js/data.js`: item/tool definitions, config, image metrics, field presets.
|
||||
- `js/data.js`: base item/tool definitions, `TOOL_CATEGORIES`, config, image metrics, field presets.
|
||||
- `js/item_visual_registry.js`: canonical item visual descriptors used by field drawing and generated tool-icon previews.
|
||||
- `js/food_registry.js`: food/medicine semantics such as labels, serving classes, decay multipliers, and hygiene penalties.
|
||||
- `js/disease_registry.js`: disease labels, flags, display metadata, cure-rule notes, and mystery-drug eligibility.
|
||||
- `js/input_mode_manager.js`: touch/mobile classification, mobile mode persistence, and gesture capability policy.
|
||||
- `js/weather_system.js`: weather state transitions and rain-water item spawning.
|
||||
- `js/text_catalog.js`: Japanese UI copy/tooltips. Keep JS strings escaped where existing style uses escapes.
|
||||
- `js/effect_registry.js`: item-effect display names, exclusivity, random-effect candidates, UI colors.
|
||||
- `js/sound_pack.js`: sound IDs, category, volume multiplier, cooldown, optional sample path.
|
||||
- `js/effect_registry.js`: item-effect labels, colors, modifiers, exclusivity/removal metadata, behavior callbacks (`onApply`, `onTick`, `onRemove`), and random-effect candidates.
|
||||
- `js/sound_pack.js`: canonical sound metadata: IDs, categories, gain multipliers, cooldowns, optional sample paths, and ID-to-category mapping.
|
||||
- `FIELD_TYPES` in `js/data.js`: cage/garden/park field labels, responsive heights, world scale, starting population, and initial grass.
|
||||
|
||||
## State loops
|
||||
|
||||
`main.js` starts asset preload, world bootstrap, animation loop, deferred image loading, service-worker registration. `World.update(dt)` advances time, weather, items, ants, Tarinai, effects, budgets, terrain dirtiness. `render()` draws cached background/terrain, visible entities, weather overlay, cards, HUD, and debug overlays.
|
||||
|
||||
World time is `CONFIG.dayLength` seconds per day. Version `15.20.11` uses 120 seconds/day and weather changes every 46-96 seconds. The phase boundary rules live in `World.phaseName()`.
|
||||
|
||||
Domain events are emitted at central mutation points: placement, eating, decay, effect apply/remove, disease infect/cure, damage/death/birth, fight hit/loss/end, ant spawn/grab/carry/death, weather changes, and rain item drops. Logs/audio still use direct calls where they already existed; do not add duplicate log or sound side effects when subscribing to these events.
|
||||
|
||||
Ant behavior remains intentionally conservative. `ANT_EXTENSION_HOOKS` names future ecology slots for nest hunger, nest growth, queen reproduction, worker lifecycle, carrying success, and territorial behavior, but this pass does not rebalance those mechanics.
|
||||
|
||||
## Spatial/performance rules
|
||||
|
||||
Use `World.nearby*` helpers rather than full-array scans in hot paths. Terrain cache uses dirty/version flags; do not reintroduce every-frame full terrain hashing. Performance degradation should be centralized in `performance_manager.js`.
|
||||
|
||||
Family tree rendering is intentionally asynchronous and cached. `ui_family_async.js` chunks component discovery with `requestIdleCallback` when available; `ui_family_render.js` hydrates cached HTML, keeps archive dirty/version state, and exposes `validateFamilyTree()` plus `familyTreeDiagnostics()` for browser-console checks.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Visual source of truth
|
||||
|
||||
Do not invent tool-UI-only item designs. Field visuals in `js/items.js` are primary. Tool icons in `assets/ui/tool_*.webp` are generated previews and must match field rendering. Update `scripts/generate_item_icons.py` when changing item helper silhouettes; regenerate icons before shipping.
|
||||
Do not invent tool-UI-only item designs. `js/item_visual_registry.js` is the canonical descriptor table for drift-prone item visuals; field drawing in `js/items.js` and generated previews must consume the same concepts. Tool icons in `assets/ui/tool_*.webp` are generated previews and must match field rendering. Update `scripts/generate_item_icons.py` when changing item helper silhouettes; regenerate icons before shipping.
|
||||
|
||||
Canonical helper mapping:
|
||||
|
||||
|
|
@ -14,6 +14,26 @@ Canonical helper mapping:
|
|||
- Ammo: `drawBulletSprite`; bullet silhouette.
|
||||
- Protein/niteropu: powder-pile rendering in the parameter-item branch.
|
||||
|
||||
## Tool groups
|
||||
|
||||
`TOOL_CATEGORIES` in `js/data.js` owns the runtime-generated tool palette. CSS colors/icons are tied to `data-tool-category` and `data-tool` values:
|
||||
|
||||
- Operation/observation: observe, delete, poke, pinch, new, water hose.
|
||||
- Food/water/grass: zunda mochi, love mochi, fight mochi, grass, water bowl, zunda juice.
|
||||
- Medicine/effects: sleep drug, laxative, protein, niteropu, ammo, mystery drug, mercury, giant drug, dwarf drug.
|
||||
- Habitat/living things: zunchi, bed, nest box, ant nest, ball.
|
||||
- Hazards/obstacles: stone, genkotsu, firecracker, vertical fence, horizontal fence.
|
||||
|
||||
When adding or renaming a tool, update `TOOL_DEFINITIONS`, `TOOL_CATEGORIES`, `TEXT_CATALOG.toolTips`, CSS icon mapping, generated `assets/ui/tool_*.webp`, and docs. Do not duplicate category membership in `index.html`.
|
||||
|
||||
## Gameplay data registries
|
||||
|
||||
- Item effects: `TarinaiEffectRegistry` owns labels, display colors, modifiers, exclusivity/removal metadata, behavior callbacks, and mystery-drug candidates. Tarinai item-effect methods are the integration layer and should route apply/tick/remove behavior through the registry.
|
||||
- Diseases: `TarinaiDiseaseRegistry` owns disease flags, labels, symptoms/display text, cure-rule notes, and mystery-drug eligibility.
|
||||
- Food: `TarinaiFoodRegistry` owns labels, serving classes, decay multipliers, hygiene penalties, and special effect IDs.
|
||||
- Weather: `TarinaiWeatherSystem` owns weather transitions and rain-spawned water items. Rain visuals are diagonal line streaks only. Rain-spawned water items, water bowls, zunda juice, and mercury are separate item/object concepts.
|
||||
- Ants: `ANT_EXTENSION_HOOKS` documents future ecology slots only. Do not change worker count, queen founding, carrying, or nest removal balance as part of registry/event cleanup.
|
||||
|
||||
## UI copy and labels
|
||||
|
||||
Most visible UI remains Japanese. Keep user-facing naming consistent: `生活` has been renamed to `精神`; selected Tarinai combat summary shows fight win rate, not cumulative defeats.
|
||||
|
|
@ -34,4 +54,10 @@ When adding bubbles, keep them short; every bubble may also trigger `audio.bubbl
|
|||
|
||||
## Audio
|
||||
|
||||
Sound settings are in the top-right sound menu, not inside Event UI. Categories: Tarinai voice, notification, operation. File-backed sound packs belong in `js/sound_pack.js`; missing samples fall back to synthetic audio. Notification and operation sounds are intentionally louder than voice sounds.
|
||||
Sound settings are in the top-right sound menu, not inside Event UI. Categories: Tarinai voice, notification, operation. `js/sound_pack.js` is the canonical metadata source for sound IDs, categories, gains, cooldowns, sample paths, and ID mapping; `js/audio.js` is the playback engine. Missing samples fall back to synthetic audio. Notification and operation sounds are intentionally louder than voice sounds.
|
||||
|
||||
The Event card owns in-page push toast settings for log kinds: death, accident, birth, fight, food, weather, grass, relation. Those settings are separate from audio categories and persist through localStorage.
|
||||
|
||||
## Ecology and credits
|
||||
|
||||
Ecology dialog card copy and images come from `TEXT_CATALOG.ecologyCards`; image paths should use real assets or generated WebP assets. Credits are hardcoded in `index.html`; update them when adding new external media sources.
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
3. Regenerate static references: `python3 scripts/generate_app_files.py`.
|
||||
4. Regenerate item icons if item visuals changed: `python3 scripts/generate_item_icons.py`.
|
||||
5. Check versions: no stale `v=<old>` in `index.html`, CSS icon URLs, `service-worker.js`.
|
||||
6. Validate syntax: `node --check js/*.js service-worker.js`.
|
||||
7. Zip from project root and run `zip -T`.
|
||||
6. Validate syntax: `node --check js/*.js service-worker.js`. If Node is not installed on the local machine, document that explicitly in the handoff and use browser-console smoke checks instead.
|
||||
7. Open `index.html` in a browser and verify startup, reset field selection, one tool placement, observe selection, family tree render, sound menu toggles, and `?debug=1`.
|
||||
8. Zip from project root and run `zip -T`.
|
||||
|
||||
If `scripts/generate_app_files.py` cannot run locally, manually keep these in sync: `app_manifest.json`, `index.html` script/link tags, and `service-worker.js` `coreScriptNames`/CSS cache entries.
|
||||
|
||||
## Cache rules
|
||||
|
||||
|
|
@ -18,9 +21,24 @@
|
|||
|
||||
Use `?debug=1` for FPS, performance level, counts, cache/SW state, last audio ID, effect registry count, terrain/spatial version, and recent event-bus history.
|
||||
|
||||
Useful browser-console checks:
|
||||
|
||||
- `TARINAI_VERSION` and `TARINAI_APP.cacheName` confirm version/cache naming.
|
||||
- `world.fieldType`, `world.tarinai.length`, `world.items.length`, and `world.ants.length` confirm the active simulation shape.
|
||||
- `TarinaiEffectRegistry.debugSnapshot()` confirms registered item effects.
|
||||
- `TarinaiDiseaseRegistry.debugSnapshot()` confirms disease metadata.
|
||||
- `TarinaiFoodRegistry.debugSnapshot()` confirms food semantics.
|
||||
- `toolCategories().map(c => c.id)` confirms the runtime tool palette registry.
|
||||
- `TarinaiInputMode.snapshot()` confirms mobile classification and mode policy.
|
||||
- `TarinaiEvents.recent(null, 20).map(e => e.type)` confirms domain-event emission.
|
||||
- `validateFamilyTree()` checks reciprocal family links and rendered family tree node/link consistency.
|
||||
- `familyTreeDiagnostics()` reports the latest async archive render state.
|
||||
|
||||
## Mobile input
|
||||
|
||||
The mobile mode controls are only for compact no-hover coarse-pointer devices. Desktop browsers, including narrow windows, should not show the mobile controls unless the device is genuinely compact touch-first.
|
||||
The mobile mode controls are owned by `TarinaiInputMode` and are only for compact/no-hover/coarse-pointer touch-first devices. Desktop browsers, including touch-screen PCs, should not show the mobile controls unless the viewport or UA also indicates mobile/tablet.
|
||||
|
||||
The mode state is stored as `tarinai_mobile_input_mode_v1`; sound settings use `tarinai_audio_settings_v2`; log push settings use `tarinai_log_push_settings_v2`.
|
||||
|
||||
## Prompt archive
|
||||
|
||||
|
|
|
|||
1374
docs/prompt.txt
170
index.html
|
|
@ -1,14 +1,14 @@
|
|||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=15.20.3" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.20.3" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.20.3" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.20.3" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.20.3" />
|
||||
<link rel="stylesheet" href="css/base.css?v=15.20.11" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=15.20.11" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=15.20.11" />
|
||||
<link rel="stylesheet" href="css/components.css?v=15.20.11" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=15.20.11" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -74,64 +74,7 @@
|
|||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">道具</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div class="tool-body">
|
||||
<div id="toolPalette" class="tool-palette">
|
||||
<section class="tool-category" data-tool-category="operate">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">操作・観察</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool selected" data-tool="observe">観察</button>
|
||||
<button class="tool" data-tool="delete">削除</button>
|
||||
<button class="tool" data-tool="poke">つつく</button>
|
||||
<button class="tool" data-tool="pinch">つまむ</button>
|
||||
<button class="tool" data-tool="new">追加</button>
|
||||
<button class="tool" data-tool="water_hose">洗浄</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tool-category" data-tool-category="food">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">食べ物・水・草</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="sweet">ずんだ餅</button>
|
||||
<button class="tool" data-tool="love_mochi">へこ餅</button>
|
||||
<button class="tool" data-tool="fight_mochi">けんか餅</button>
|
||||
<button class="tool" data-tool="grass">草</button>
|
||||
<button class="tool" data-tool="water_bowl">水の皿</button>
|
||||
<button class="tool" data-tool="zunda_juice">ずんだ汁</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tool-category" data-tool-category="medicine">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">薬・個体効果</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="sleep_drug">ねむり薬</button>
|
||||
<button class="tool" data-tool="laxative">下剤</button>
|
||||
<button class="tool" data-tool="protein">プロテイン</button>
|
||||
<button class="tool" data-tool="niteropu">ンイテロプ</button>
|
||||
<button class="tool" data-tool="ammo">弾薬</button>
|
||||
<button class="tool" data-tool="mystery_drug">怪しい薬</button>
|
||||
<button class="tool" data-tool="mercury">水銀</button>
|
||||
<button class="tool" data-tool="giant_drug">巨大薬</button>
|
||||
<button class="tool" data-tool="dwarf_drug">矮小薬</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tool-category" data-tool-category="habitat">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">生活・生き物</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="zunchi">ずんち</button>
|
||||
<button class="tool" data-tool="bed">干草寝床</button>
|
||||
<button class="tool" data-tool="nest_box">巣箱</button>
|
||||
<button class="tool" data-tool="ant_nest">アリの巣</button>
|
||||
<button class="tool" data-tool="ball">ボール</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tool-category" data-tool-category="hazard">
|
||||
<button class="tool-category-toggle" type="button" aria-expanded="true">危険・障害物</button>
|
||||
<div class="tool-grid tool-category-body">
|
||||
<button class="tool" data-tool="stone">石</button>
|
||||
<button class="tool" data-tool="genkotsu">げんこつ</button>
|
||||
<button class="tool" data-tool="firecracker">爆竹</button>
|
||||
<button class="tool" data-tool="fence_v">縦の柵</button>
|
||||
<button class="tool" data-tool="fence_h">横の柵</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="toolPalette" class="tool-palette" aria-live="polite"></div>
|
||||
<p class="hint">同じ道具ボタンをもう一度押すと、対応道具の大きさが小・中・大で切り替わります。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -237,52 +180,57 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=15.20.3" defer></script>
|
||||
<script src="js/event_bus.js?v=15.20.3" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.20.3" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.20.3" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.20.3" defer></script>
|
||||
<script src="js/data.js?v=15.20.3" defer></script>
|
||||
<script src="js/math.js?v=15.20.3" defer></script>
|
||||
<script src="js/assets.js?v=15.20.3" defer></script>
|
||||
<script src="js/audio.js?v=15.20.3" defer></script>
|
||||
<script src="js/render.js?v=15.20.3" defer></script>
|
||||
<script src="js/sim_core.js?v=15.20.3" defer></script>
|
||||
<script src="js/items.js?v=15.20.3" defer></script>
|
||||
<script src="js/ants.js?v=15.20.3" defer></script>
|
||||
<script src="js/health.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.20.3" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.20.3" defer></script>
|
||||
<script src="js/world.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_view.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_environment.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_update.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.20.3" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.20.3" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_log.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.20.3" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.20.3" defer></script>
|
||||
<script src="js/main.js?v=15.20.3" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.20.3" defer></script>
|
||||
<script src="js/version.js?v=15.20.11" defer></script>
|
||||
<script src="js/event_bus.js?v=15.20.11" defer></script>
|
||||
<script src="js/performance_manager.js?v=15.20.11" defer></script>
|
||||
<script src="js/disease_registry.js?v=15.20.11" defer></script>
|
||||
<script src="js/effect_registry.js?v=15.20.11" defer></script>
|
||||
<script src="js/sound_pack.js?v=15.20.11" defer></script>
|
||||
<script src="js/data.js?v=15.20.11" defer></script>
|
||||
<script src="js/item_visual_registry.js?v=15.20.11" defer></script>
|
||||
<script src="js/food_registry.js?v=15.20.11" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=15.20.11" defer></script>
|
||||
<script src="js/math.js?v=15.20.11" defer></script>
|
||||
<script src="js/assets.js?v=15.20.11" defer></script>
|
||||
<script src="js/audio.js?v=15.20.11" defer></script>
|
||||
<script src="js/render.js?v=15.20.11" defer></script>
|
||||
<script src="js/sim_core.js?v=15.20.11" defer></script>
|
||||
<script src="js/items.js?v=15.20.11" defer></script>
|
||||
<script src="js/ants.js?v=15.20.11" defer></script>
|
||||
<script src="js/health.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=15.20.11" defer></script>
|
||||
<script src="js/tarinai_render.js?v=15.20.11" defer></script>
|
||||
<script src="js/world.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_view.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_family_social.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_environment.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_ants_system.js?v=15.20.11" defer></script>
|
||||
<script src="js/weather_system.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_update.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_placement_log.js?v=15.20.11" defer></script>
|
||||
<script src="js/world_bootstrap.js?v=15.20.11" defer></script>
|
||||
<script src="js/text_catalog.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_log.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_selected.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_tools.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_bind.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_charts.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_family_data.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_family_async.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=15.20.11" defer></script>
|
||||
<script src="js/ui_family_render.js?v=15.20.11" defer></script>
|
||||
<script src="js/main.js?v=15.20.11" defer></script>
|
||||
<script src="js/debug_tools.js?v=15.20.11" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
30
js/ants.js
|
|
@ -13,6 +13,15 @@ const ANT_WORKER_RETURN_WEAK_SPEED = 9.0;
|
|||
const ANT_WORKER_DRAG_SPEED = 6.1875;
|
||||
const ANT_QUEEN_SPEED = 13.5;
|
||||
|
||||
const ANT_EXTENSION_HOOKS = Object.freeze({
|
||||
nestHunger: "reserved",
|
||||
nestGrowth: "reserved",
|
||||
queenReproduction: "existing-queen-founding",
|
||||
workerLifecycle: "worker-pool-hp",
|
||||
carryingSuccessRate: "current-deterministic-haul",
|
||||
territorialBehavior: "reserved",
|
||||
});
|
||||
|
||||
function clampAntHp(v) {
|
||||
return clamp(Number(v) || 0, 0, ANT_WORKER_HP);
|
||||
}
|
||||
|
|
@ -30,6 +39,8 @@ function sanitizeAntWorkerPool(list, fallbackCount = 0) {
|
|||
|
||||
function ensureAntNestWorkerPool(nest) {
|
||||
if (!nest || nest.type !== "ant_nest") return [];
|
||||
// Extension hook: future nest hunger/growth state should attach to the nest
|
||||
// item here without changing current worker count balance.
|
||||
if (!Array.isArray(nest.antWorkers)) {
|
||||
nest.antWorkers = buildAntWorkerPool(nest.antCount ?? ANT_NEST_START_COUNT);
|
||||
} else {
|
||||
|
|
@ -150,6 +161,8 @@ class AntActor {
|
|||
}
|
||||
|
||||
chooseTarget(maxDist = 56) {
|
||||
// Extension hook: future carrying success and territorial behavior should
|
||||
// filter or rank candidates here while preserving sleep-disease hauling.
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
for (const t of this.world?.tarinai || []) {
|
||||
|
|
@ -207,6 +220,8 @@ class AntActor {
|
|||
target.antDraggedBy = this.id;
|
||||
target.fearTimer = Math.max(target.fearTimer || 0, 0.55);
|
||||
target.thought = "\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b";
|
||||
target.recordChangeCause?.("\u30a2\u30ea\u306b\u904b\u3070\u308c\u305f", "\u72b6\u614b");
|
||||
this.world?.emit?.("ant:grabbed", { ant: this, target });
|
||||
audio.antDrag?.();
|
||||
this.world.drawListDirty = true;
|
||||
return;
|
||||
|
|
@ -268,17 +283,21 @@ class AntActor {
|
|||
const targetWasSleeping = Boolean(target.sleeping || target.state === "sleep");
|
||||
target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.35);
|
||||
target.antDraggedBy = this.id;
|
||||
if (leadHauler && (this.world?.time || 0) - (this.lastCarryEventAt || -999) > 0.75) {
|
||||
this.lastCarryEventAt = this.world?.time || 0;
|
||||
this.world?.emit?.("ant:carried", { ant: this, target, lead: true, haulers });
|
||||
}
|
||||
target.target = null;
|
||||
if (targetWasSleeping) {
|
||||
target.sleeping = true;
|
||||
target.state = "sleep";
|
||||
target.sleepTimer = Math.max(target.sleepTimer || 0, 0.8);
|
||||
target.thought = "眠ったままアリに運ばれている";
|
||||
target.thought = "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b";
|
||||
} else {
|
||||
target.state = "panic";
|
||||
target.sleeping = false;
|
||||
target.fearTimer = Math.max(target.fearTimer || 0, 0.36);
|
||||
target.thought = "アリに巣へ運ばれている";
|
||||
target.thought = "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b";
|
||||
}
|
||||
const dHome = distXY(target.x, target.y, home.x, home.y) || 1;
|
||||
if (dHome < Math.max(18, home.r * 0.66)) {
|
||||
|
|
@ -290,7 +309,8 @@ class AntActor {
|
|||
return;
|
||||
}
|
||||
const hpFactor = clamp(this.hp / ANT_WORKER_HP, 0.45, 1.0);
|
||||
const speed = ANT_WORKER_DRAG_SPEED * hpFactor;
|
||||
const haulerBoost = 1 + Math.min(0.18, Math.max(0, haulers - 1) * 0.045);
|
||||
const speed = ANT_WORKER_DRAG_SPEED * hpFactor * haulerBoost;
|
||||
const ux = (home.x - target.x) / dHome;
|
||||
const uy = (home.y - target.y) / dHome;
|
||||
if (leadHauler) {
|
||||
|
|
@ -324,6 +344,7 @@ class AntActor {
|
|||
if (this.dead) return;
|
||||
this.dead = true;
|
||||
audio.antDie?.();
|
||||
this.world?.emit?.("ant:died", { ant: this, home, reason: "hp" });
|
||||
const corpse = new Item("ant_corpse", this.x, this.y);
|
||||
corpse.amount = 24;
|
||||
corpse.workerSprite = this.workerSpriteId();
|
||||
|
|
@ -336,6 +357,8 @@ class AntActor {
|
|||
|
||||
buildNestAtFoundingSpot(force = false) {
|
||||
if (!this.world) return null;
|
||||
// Extension hook: queen reproduction currently means founding a new nest.
|
||||
// Future growth rules should branch here without changing current behavior.
|
||||
const base = new Item("ant_nest", this.foundingX, this.foundingY);
|
||||
let spot = null;
|
||||
if (this.world.findPlacementSpot) {
|
||||
|
|
@ -466,6 +489,7 @@ window.ANT_NEST_MAX_COUNT = ANT_NEST_MAX_COUNT;
|
|||
window.ANT_NEST_MAX_OUTSIDE = ANT_NEST_MAX_OUTSIDE;
|
||||
window.ANT_WORKER_HP = ANT_WORKER_HP;
|
||||
window.ANT_QUEEN_HP = ANT_QUEEN_HP;
|
||||
window.ANT_EXTENSION_HOOKS = ANT_EXTENSION_HOOKS;
|
||||
window.buildAntWorkerPool = buildAntWorkerPool;
|
||||
window.ensureAntNestWorkerPool = ensureAntNestWorkerPool;
|
||||
window.syncAntNestCount = syncAntNestCount;
|
||||
|
|
|
|||
45
js/assets.js
|
|
@ -7,11 +7,13 @@ const imageAssetIndex = new Map();
|
|||
|
||||
const scaledSpriteCache = new Map();
|
||||
const stainOverlayCache = new Map();
|
||||
const SPRITE_CACHE_LIMIT = 128;
|
||||
const STAIN_CACHE_LIMIT = 64;
|
||||
|
||||
const INITIAL_IMAGE_IDS = new Set(["smile", "zunchi", "zunchi_02", "genkotsu"]);
|
||||
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "zunchi", "zunchi_02", "genkotsu"]);
|
||||
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck"]);
|
||||
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck"]);
|
||||
|
||||
function trimCanvasCache(cache, limit = 420) {
|
||||
function trimCanvasCache(cache, limit = 128) {
|
||||
while (cache.size > limit) {
|
||||
const first = cache.keys().next().value;
|
||||
if (first === undefined) break;
|
||||
|
|
@ -19,16 +21,16 @@ function trimCanvasCache(cache, limit = 420) {
|
|||
}
|
||||
}
|
||||
|
||||
function quantizedCanvasSize(v, step = 16) {
|
||||
function quantizedCanvasSize(v, step = 24) {
|
||||
return Math.max(1, Math.round(v / step) * step);
|
||||
}
|
||||
|
||||
const SPRITE_CACHE_SCALE = 2.0;
|
||||
const SPRITE_CACHE_SCALE = 1.55;
|
||||
|
||||
function getCachedSpriteCanvas(id, img, w, h) {
|
||||
if (!img || !w || !h) return img;
|
||||
const qw = quantizedCanvasSize(w, 16);
|
||||
const qh = quantizedCanvasSize(h, 16);
|
||||
const qw = quantizedCanvasSize(w);
|
||||
const qh = quantizedCanvasSize(h);
|
||||
const key = `${id}:${qw}x${qh}`;
|
||||
let canvas = scaledSpriteCache.get(key);
|
||||
if (canvas) return canvas;
|
||||
|
|
@ -41,14 +43,14 @@ function getCachedSpriteCanvas(id, img, w, h) {
|
|||
c.clearRect(0, 0, qw, qh);
|
||||
c.drawImage(img, 0, 0, qw, qh);
|
||||
scaledSpriteCache.set(key, canvas);
|
||||
trimCanvasCache(scaledSpriteCache, 520);
|
||||
trimCanvasCache(scaledSpriteCache, SPRITE_CACHE_LIMIT);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) {
|
||||
if (!img || stain <= 0.01 || !w || !h) return null;
|
||||
const qw = quantizedCanvasSize(w, 16);
|
||||
const qh = quantizedCanvasSize(h, 16);
|
||||
const qw = quantizedCanvasSize(w);
|
||||
const qh = quantizedCanvasSize(h);
|
||||
const bucket = Math.max(1, Math.min(8, Math.ceil(stain * 8)));
|
||||
const seedBucket = Math.round((seed || 0) * 16);
|
||||
const key = `${id}:${qw}x${qh}:s${bucket}:r${seedBucket}`;
|
||||
|
|
@ -80,7 +82,7 @@ function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) {
|
|||
c.globalAlpha = 1;
|
||||
c.globalCompositeOperation = "source-over";
|
||||
stainOverlayCache.set(key, canvas);
|
||||
trimCanvasCache(stainOverlayCache, 360);
|
||||
trimCanvasCache(stainOverlayCache, STAIN_CACHE_LIMIT);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
|
|
@ -175,9 +177,23 @@ function loadImages(ids = null, onProgress = null) {
|
|||
return Promise.all(tasks).then(() => undefined);
|
||||
}
|
||||
|
||||
function trimImageMemory(aggressive = false) {
|
||||
trimCanvasCache(scaledSpriteCache, aggressive ? 48 : SPRITE_CACHE_LIMIT);
|
||||
trimCanvasCache(stainOverlayCache, aggressive ? 16 : STAIN_CACHE_LIMIT);
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) trimImageMemory(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.setInterval(() => trimImageMemory(false), 45000);
|
||||
}
|
||||
|
||||
function startBackgroundImageLoading() {
|
||||
setupAssetIndex();
|
||||
const allAssets = [...SPRITES, ...DECOR_ASSETS];
|
||||
const loadGroup = (ids, priority = "low") => {
|
||||
for (const id of ids) {
|
||||
const asset = imageAssetIndex.get(id);
|
||||
|
|
@ -189,7 +205,8 @@ function startBackgroundImageLoading() {
|
|||
: (fn) => setTimeout(fn, 60);
|
||||
schedule(() => loadGroup(EARLY_IMAGE_IDS, "auto"), 450);
|
||||
schedule(() => {
|
||||
for (const asset of allAssets) loadImageAsset(asset, "low");
|
||||
for (const asset of [...SPRITES, ...DECOR_ASSETS]) loadImageAsset(asset, "low");
|
||||
trimImageMemory(false);
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
|
|
@ -224,4 +241,4 @@ function getImageMetrics(idOrImg) {
|
|||
|
||||
setupAssetIndex();
|
||||
|
||||
window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };
|
||||
window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, trimImageMemory, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };
|
||||
|
|
|
|||
76
js/audio.js
|
|
@ -17,66 +17,13 @@ const audio = {
|
|||
masterGainNode: null,
|
||||
compressorNode: null,
|
||||
_synthIdGain: 1,
|
||||
idGain: {
|
||||
sfx_notify: 2.60,
|
||||
sfx_log_push: 2.60,
|
||||
sfx_ui_click: 2.35,
|
||||
sfx_ui_fold: 2.20,
|
||||
sfx_place: 2.10,
|
||||
sfx_delete: 2.25,
|
||||
sfx_poke: 1.85,
|
||||
sfx_grab: 2.15,
|
||||
sfx_drop: 2.10,
|
||||
sfx_water_hose: 1.85,
|
||||
sfx_genkotsu_fall: 1.70,
|
||||
sfx_genkotsu_impact: 1.90,
|
||||
sfx_ground_rumble: 1.75,
|
||||
sfx_firecracker_fuse: 1.55,
|
||||
sfx_firecracker_explode: 1.70,
|
||||
},
|
||||
idMinGap: {
|
||||
sfx_ui_click: 0.030,
|
||||
sfx_place: 0.055,
|
||||
sfx_grass: 0.12,
|
||||
sfx_zunchi: 0.14,
|
||||
sfx_ant_die: 0.14,
|
||||
sfx_notify: 0.10,
|
||||
sfx_log_push: 0.10,
|
||||
},
|
||||
idGain: {},
|
||||
idMinGap: {},
|
||||
categoryEnabled: { voice: false, notify: true, ops: true },
|
||||
categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 },
|
||||
samplePaths: {
|
||||
voice_hau: "assets/sounds/voice_001_hau.wav",
|
||||
voice_flee: "assets/sounds/voice_002_flee.wav",
|
||||
voice_poop: "assets/sounds/voice_003_po.wav",
|
||||
sfx_eat: "assets/sounds/voice_004_eat.wav",
|
||||
voice_stress: "assets/sounds/voice_005_stress.wav",
|
||||
sfx_sleep: "assets/sounds/voice_006_sleep.wav",
|
||||
sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3",
|
||||
|
||||
// Optional replacement slots. Missing files are never requested unless you add a path.
|
||||
// sfx_notify: "assets/sounds/sfx_notify.mp3",
|
||||
// sfx_ui_click: "assets/sounds/sfx_ui_click.mp3",
|
||||
// sfx_genkotsu_fall: "assets/sounds/sfx_genkotsu_fall.mp3",
|
||||
// sfx_genkotsu_impact: "assets/sounds/sfx_genkotsu_impact.mp3",
|
||||
},
|
||||
soundCategories: {
|
||||
voice: "たりないの声",
|
||||
notify: "通知",
|
||||
ops: "操作",
|
||||
},
|
||||
soundMap: {
|
||||
voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice",
|
||||
sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice",
|
||||
sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice",
|
||||
sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice",
|
||||
sfx_notify: "notify", sfx_phase: "notify", sfx_log_push: "notify",
|
||||
sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops",
|
||||
sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops",
|
||||
sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops",
|
||||
sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops",
|
||||
sfx_zunchi: "ops", sfx_ant_nest: "ops",
|
||||
},
|
||||
categoryGain: {},
|
||||
samplePaths: {},
|
||||
soundCategories: {},
|
||||
soundMap: {},
|
||||
applySoundPack(pack = window.TARINAI_SOUND_PACK) {
|
||||
if (!pack || typeof pack !== "object") return false;
|
||||
this.soundPackVersion = String(pack.version || "custom");
|
||||
|
|
@ -85,6 +32,7 @@ const audio = {
|
|||
if (pack.idMinGap && typeof pack.idMinGap === "object") this.idMinGap = { ...this.idMinGap, ...pack.idMinGap };
|
||||
if (pack.samplePaths && typeof pack.samplePaths === "object") this.samplePaths = { ...this.samplePaths, ...pack.samplePaths };
|
||||
if (pack.categories && typeof pack.categories === "object") this.soundCategories = { ...this.soundCategories, ...pack.categories };
|
||||
if (pack.soundMap && typeof pack.soundMap === "object") this.soundMap = { ...this.soundMap, ...pack.soundMap };
|
||||
this._samplesReady = false;
|
||||
this.samples = {};
|
||||
return true;
|
||||
|
|
@ -344,12 +292,12 @@ const audio = {
|
|||
},
|
||||
bubble(text) {
|
||||
const s = String(text || "");
|
||||
if (s.includes("もぐ")) return this.playSample("sfx_eat", 0.20, "voice") || this.play("sfx_eat", { category: "voice", minGap: 0.20 });
|
||||
if (s.includes("ぶり")) return this.playSample("voice_poop", 0.28, "voice") || this.play("sfx_zunchi", { category: "ops", minGap: 0.28 });
|
||||
if (s.includes("はっ")) return this.playSample("voice_stress", 0.80, "voice") || this.play("sfx_damage_soft", { category: "voice", minGap: 0.80 });
|
||||
if (s.includes("\u3082\u3050")) return this.playSample("sfx_eat", 0.20, "voice") || this.play("sfx_eat", { category: "voice", minGap: 0.20 });
|
||||
if (s.includes("\u3076\u308a")) return this.playSample("voice_poop", 0.28, "voice") || this.play("sfx_zunchi", { category: "ops", minGap: 0.28 });
|
||||
if (s.includes("\u306f\u3063")) return this.playSample("voice_stress", 0.80, "voice") || this.play("sfx_damage_soft", { category: "voice", minGap: 0.80 });
|
||||
if (s.includes("Zzz")) return this.playSample("sfx_sleep", 1.25, "voice") || this.play("sfx_sleep", { category: "voice", minGap: 1.25 });
|
||||
if (s.includes("はぁ") || s.includes("はあ")) return this.playSample("voice_flee", 0.70, "voice") || this.play("sfx_damage_heavy", { category: "voice", minGap: 0.70 });
|
||||
if (s.includes("はう") || s.includes("はぅ")) return this.playSample("voice_hau", 0.45, "voice") || this.play("sfx_wake", { category: "voice", minGap: 0.45 });
|
||||
if (s.includes("\u306f\u3041") || s.includes("\u306f\u3042")) return this.playSample("voice_flee", 0.70, "voice") || this.play("sfx_damage_heavy", { category: "voice", minGap: 0.70 });
|
||||
if (s.includes("\u306f\u3046") || s.includes("\u306f\u3045")) return this.playSample("voice_hau", 0.45, "voice") || this.play("sfx_wake", { category: "voice", minGap: 0.45 });
|
||||
return false;
|
||||
},
|
||||
eat() { if (!this.playSample("sfx_eat", 0.20, "voice")) this.play("sfx_eat", { category: "voice", minGap: 0.20 }); },
|
||||
|
|
|
|||
45
js/data.js
|
|
@ -28,6 +28,8 @@ const SPRITES = [
|
|||
{ id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true },
|
||||
{ id: "birth_ritual", label: "\u7e41\u6b96\u524d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
|
||||
{ id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true },
|
||||
{ id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true },
|
||||
{ id: "zunchi_slave_alt", label: "\u305a\u3093\u3061\u3069\u308c\u3044 2", path: "assets/sprites/tarinai_30_zunchi_slave_alt.webp", displayOnly: true },
|
||||
];
|
||||
const DECOR_ASSETS = [
|
||||
{ id: "zunchi", path: "assets/objects/zunchi.webp" },
|
||||
|
|
@ -35,6 +37,8 @@ const DECOR_ASSETS = [
|
|||
{ id: "ant_queen", path: "assets/objects/ant_queen.webp" },
|
||||
{ id: "ant_worker", path: "assets/objects/ant_worker.webp" },
|
||||
{ id: "genkotsu", path: "assets/objects/genkotsu.webp" },
|
||||
{ id: "pushpin", path: "assets/objects/pushpin.webp" },
|
||||
{ id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" },
|
||||
];
|
||||
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
|
|
@ -51,23 +55,24 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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.webp", 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.webp", tooltip: "\u98df\u3079\u308b\u3068\u306d\u3080\u308a\u75c5\u306b\u306a\u308b\u3002\u30c0\u30e1\u30fc\u30b8\u3067\u5b8c\u6cbb\u3059\u308b\u3002" },
|
||||
|
||||
laxative: { id: "laxative", itemType: "laxative", label: "下剤", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "ずんちを大量に排泄させる。何かを食べてもすぐに出て行ってしまう。" },
|
||||
protein: { id: "protein", itemType: "protein", label: "プロテイン", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "体をたくましくし、病気に強くなる。" },
|
||||
niteropu: { id: "niteropu", itemType: "niteropu", label: "ンイテロプ", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "体を頼りなくし、病気に弱くなる。" },
|
||||
ammo: { id: "ammo", itemType: "ammo", label: "弾薬", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "全てを加速させる。" },
|
||||
mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "怪しい薬", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "何かが起こる。" },
|
||||
mercury: { id: "mercury", itemType: "mercury", label: "水銀", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "長生きの秘訣。" },
|
||||
giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "巨大薬", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "体を大きく強くするが、負担が大きい。" },
|
||||
dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "矮小薬", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "体を小さく弱くするが、負担も大きい。" },
|
||||
zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "ずんだ汁", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "食べると全てのアイテム効果を除去する。" },
|
||||
laxative: { id: "laxative", itemType: "laxative", label: "\u4e0b\u5264", placeable: true, scalable: true, radius: 13, amount: 58, icon: "assets/ui/tool_laxative.webp", tooltip: "\u305a\u3093\u3061\u3092\u5927\u91cf\u306b\u6392\u6cc4\u3055\u305b\u308b\u3002\u4f55\u304b\u3092\u98df\u3079\u3066\u3082\u3059\u3050\u306b\u51fa\u3066\u884c\u3063\u3066\u3057\u307e\u3046\u3002" },
|
||||
protein: { id: "protein", itemType: "protein", label: "\u30d7\u30ed\u30c6\u30a4\u30f3", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_protein.webp", tooltip: "\u75c5\u6c17\u306b\u5f37\u304f\u3001\u305f\u304f\u307e\u3057\u304f\u3002" },
|
||||
niteropu: { id: "niteropu", itemType: "niteropu", label: "\u30f3\u30a4\u30c6\u30ed\u30d7", placeable: true, scalable: true, radius: 14, amount: 58, icon: "assets/ui/tool_niteropu.webp", tooltip: "\u75c5\u6c17\u306b\u5f31\u304f\u3001\u305f\u3088\u308a\u306a\u304f\u3002" },
|
||||
ammo: { id: "ammo", itemType: "ammo", label: "\u5f3e\u85ac", placeable: true, scalable: true, radius: 12, amount: 48, icon: "assets/ui/tool_ammo.webp", tooltip: "\u5168\u3066\u3092\u52a0\u901f\u3055\u305b\u308b\u3002" },
|
||||
mystery_drug: { id: "mystery_drug", itemType: "mystery_drug", label: "\u602a\u3057\u3044\u85ac", placeable: true, scalable: true, radius: 13, amount: 52, icon: "assets/ui/tool_mystery_drug.webp", tooltip: "\u4f55\u304b\u304c\u8d77\u3053\u308b\u3002" },
|
||||
mercury: { id: "mercury", itemType: "mercury", label: "\u6c34\u9280", placeable: true, scalable: true, radius: 13, amount: 44, icon: "assets/ui/tool_mercury.webp", tooltip: "\u9577\u751f\u304d\u306e\u79d8\u8a23\u3002" },
|
||||
giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "\u5de8\u5927\u85ac", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "\u4f53\u3092\u5927\u304d\u304f\u5f37\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u304c\u5927\u304d\u3044\u3002" },
|
||||
dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "\u77ee\u5c0f\u85ac", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "\u4f53\u3092\u5c0f\u3055\u304f\u5f31\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u3082\u5927\u304d\u3044\u3002" },
|
||||
zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "\u305a\u3093\u3060\u6c41", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "\u98df\u3079\u308b\u3068\u5168\u3066\u306e\u30a2\u30a4\u30c6\u30e0\u52b9\u679c\u3092\u9664\u53bb\u3059\u308b\u3002" },
|
||||
grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.webp", 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.webp", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" },
|
||||
genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "上から正義げんこつを高速で落とし、アリとたりないに大ダメージと吹っ飛びを与える。" },
|
||||
genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "\u62f3\u3092\u5730\u9762\u306b\u305f\u305f\u304d\u3064\u3051\u308b\u3002" },
|
||||
bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", 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.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\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.webp", 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.webp", 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 },
|
||||
|
|
@ -77,6 +82,18 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
});
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c\u30fb\u89b3\u5bdf", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269\u30fb\u6c34\u30fb\u8349", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water_bowl", "zunda_juice"] },
|
||||
{ id: "medicine", label: "\u85ac\u30fb\u500b\u4f53\u52b9\u679c", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b\u30fb\u751f\u304d\u7269", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ant_nest", "ball"] },
|
||||
{ id: "hazard", label: "\u5371\u967a\u30fb\u969c\u5bb3\u7269", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "fence_v", "fence_h"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
return [...TOOL_CATEGORIES].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
|
||||
function toolDefinition(id = "") {
|
||||
return TOOL_DEFINITIONS[id] || null;
|
||||
}
|
||||
|
|
@ -139,6 +156,8 @@ const ALPHA_BOUNDS = {
|
|||
stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
|
||||
intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 },
|
||||
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
|
||||
hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 },
|
||||
zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 },
|
||||
zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 },
|
||||
zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 },
|
||||
ant_queen: { x: 0, y: 0, w: 300, h: 155, imageW: 300, imageH: 155 },
|
||||
|
|
@ -157,12 +176,12 @@ const CONFIG = {
|
|||
zunchiLimit: 56,
|
||||
traceLimit: 64,
|
||||
splatLimit: 48,
|
||||
effectLimit: 96,
|
||||
effectLimit: 78,
|
||||
worldPadding: 30,
|
||||
hungerPerMinute: 11.2,
|
||||
lonelinessPerMinute: 7.6,
|
||||
energyPerMinute: 8.2,
|
||||
stressDecayPerMinute: 9,
|
||||
stressDecayPerMinute: 10.2,
|
||||
reproductionCooldown: 18,
|
||||
dayLength: 120,
|
||||
childGrowTime: 58,
|
||||
|
|
@ -170,7 +189,7 @@ const CONFIG = {
|
|||
lightingQuality: "cinematic",
|
||||
weatherChangeMin: 46,
|
||||
weatherChangeMax: 96,
|
||||
bubbleLimit: 42,
|
||||
bubbleLimit: 32,
|
||||
grassLimit: 96,
|
||||
highPopulationMode: 70,
|
||||
ultraPopulationMode: 120,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
window.setInterval(() => {
|
||||
const w = window.world;
|
||||
const perf = window.TarinaiPerformance?.debugSnapshot(w) || {};
|
||||
const input = window.TarinaiInputMode?.snapshot?.() || {};
|
||||
panel.textContent = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"}`,
|
||||
`fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`,
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
`drawList ${(w?.drawList || []).length} logs ${(w?.logs || []).length}`,
|
||||
`dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`,
|
||||
`sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`,
|
||||
`input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)} uaMobile:${Boolean(input.userAgentMobile)}`,
|
||||
`audio ${lastAudio.join(", ") || window.TarinaiAudio?.lastPlayedId || "-"}`,
|
||||
`effects ${window.TarinaiEffectRegistry?.debugSnapshot?.().length || 0} registered`,
|
||||
`events ${lastEvents.join(", ")}`,
|
||||
|
|
|
|||
61
js/disease_registry.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
disease_zunchi: {
|
||||
id: "disease_zunchi",
|
||||
flag: "zunchiDisease",
|
||||
label: "\u305a\u3093\u3061\u75c5",
|
||||
symptoms: ["stress", "energy_loss", "infection"],
|
||||
displayText: "\u305a\u3093\u3061\u75c5",
|
||||
color: "rgba(113,93,59,0.74)",
|
||||
mysteryEligible: true,
|
||||
cureRules: ["water", "natural_recovery"]
|
||||
},
|
||||
disease_sleep: {
|
||||
id: "disease_sleep",
|
||||
flag: "sleepDisease",
|
||||
label: "\u306d\u3080\u308a\u75c5",
|
||||
symptoms: ["sleep_lock", "no_counterattack"],
|
||||
displayText: "\u306d\u3080\u308a\u75c5",
|
||||
color: "rgba(104,84,168,0.78)",
|
||||
mysteryEligible: true,
|
||||
cureRules: ["damage"]
|
||||
},
|
||||
disease_explosion: {
|
||||
id: "disease_explosion",
|
||||
flag: "explosionDisease",
|
||||
label: "\u7206\u767a\u75c5",
|
||||
symptoms: ["explodes_after_timer", "water_seeking"],
|
||||
displayText: "\u7206\u767a\u75c5",
|
||||
color: "rgba(210,90,44,0.78)",
|
||||
baseInfectionChance: 0.15,
|
||||
mysteryEligible: true,
|
||||
cureRules: ["water", "zunda_mochi"]
|
||||
},
|
||||
disease_fight: {
|
||||
id: "disease_fight",
|
||||
flag: "fightDisease",
|
||||
label: "\u304d\u305a\u3064\u304d\u75c5",
|
||||
symptoms: ["fight_lash_out", "zunda_seeking"],
|
||||
displayText: "\u304d\u305a\u3064\u304d\u75c5",
|
||||
color: "rgba(180,86,52,0.80)",
|
||||
mysteryEligible: true,
|
||||
cureRules: ["zunda_mochi", "natural_recovery"]
|
||||
}
|
||||
};
|
||||
|
||||
class DiseaseRegistry {
|
||||
constructor(defs) { this.defs = new Map(Object.entries(defs || {})); }
|
||||
get(id = "") { return this.defs.get(String(id || "")) || null; }
|
||||
byFlag(flag = "") { return [...this.defs.values()].find(def => def.flag === flag) || null; }
|
||||
label(id = "") { return this.get(id)?.label || id || ""; }
|
||||
color(id = "") { return this.get(id)?.color || "rgba(255,242,160,0.85)"; }
|
||||
mysteryPool() { return [...this.defs.values()].filter(def => def.mysteryEligible).map(def => def.id); }
|
||||
debugSnapshot() { return [...this.defs.values()].map(def => ({ id: def.id, label: def.label, flag: def.flag, cures: def.cureRules || [] })); }
|
||||
}
|
||||
|
||||
const registry = new DiseaseRegistry(rawDefinitions);
|
||||
global.TarinaiDiseaseRegistry = registry;
|
||||
global.TARINAI_DISEASE_DEFINITIONS = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -3,74 +3,248 @@
|
|||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
laxative: {
|
||||
label: "下剤", color: "rgba(119, 136, 58, 0.78)", kind: "timed", defaultDuration: 60,
|
||||
tags: ["item", "behavior", "food"], display: timer => `${Math.ceil(timer || 0)}秒 / 3秒ごとにずんち / 食事の空腹回復なし`,
|
||||
label: "\u4e0b\u5264", color: "rgba(119, 136, 58, 0.78)", kind: "timed", defaultDuration: 60,
|
||||
tags: ["item", "behavior", "food"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / 3\u79d2\u3054\u3068\u306b\u305a\u3093\u3061 / \u98df\u4e8b\u306e\u7a7a\u8179\u56de\u5fa9\u306a\u3057`,
|
||||
},
|
||||
ammo: {
|
||||
label: "弾薬", color: "rgba(216, 142, 38, 0.82)", kind: "timed", defaultDuration: 18,
|
||||
tags: ["item", "behavior", "movement"], display: timer => `${Math.ceil(timer || 0)}秒 / 歩行速度10倍 / 衝突で弾き飛ばす`,
|
||||
label: "\u5f3e\u85ac", color: "rgba(216, 142, 38, 0.82)", kind: "timed", defaultDuration: 18,
|
||||
tags: ["item", "behavior", "movement"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u6b69\u884c\u901f\u5ea610\u500d / \u885d\u7a81\u3067\u5f3e\u304d\u98db\u3070\u3059`,
|
||||
},
|
||||
protein: {
|
||||
label: "プロテイン", color: "rgba(239, 122, 51, 0.86)", kind: "permanent", exclusiveGroup: "power",
|
||||
tags: ["item", "behavior", "power"], display: () => "永続 / 与ダメージ1.75倍 / 病気確率0.5倍",
|
||||
label: "\u30d7\u30ed\u30c6\u30a4\u30f3", color: "rgba(239, 122, 51, 0.86)", kind: "permanent", exclusiveGroup: "power",
|
||||
tags: ["item", "behavior", "power"], display: () => "\u6c38\u7d9a / \u4e0e\u30c0\u30e1\u30fc\u30b81.75\u500d / \u75c5\u6c17\u78ba\u73870.5\u500d",
|
||||
},
|
||||
niteropu: {
|
||||
label: "ンイテロプ", color: "rgba(88, 76, 166, 0.78)", kind: "permanent", exclusiveGroup: "power",
|
||||
tags: ["item", "behavior", "power"], display: () => "永続 / 与ダメージ0.5倍 / 病気確率2倍",
|
||||
label: "\u30f3\u30a4\u30c6\u30ed\u30d7", color: "rgba(88, 76, 166, 0.78)", kind: "permanent", exclusiveGroup: "power",
|
||||
tags: ["item", "behavior", "power"], display: () => "\u6c38\u7d9a / \u4e0e\u30c0\u30e1\u30fc\u30b80.5\u500d / \u75c5\u6c17\u78ba\u73872\u500d",
|
||||
},
|
||||
mercury: {
|
||||
label: "水銀", color: "rgba(96, 146, 166, 0.82)", kind: "permanent", stack: "multiply",
|
||||
tags: ["item", "life"], display: (_timer, t) => `永続 / 寿命x${((t && t.mercuryLifeMultiplier) || 1.3).toFixed(2)}(食べるたび乗算)`,
|
||||
label: "\u6c34\u9280", color: "rgba(96, 146, 166, 0.82)", kind: "permanent", stack: "multiply",
|
||||
tags: ["item", "life"], display: (_timer, t) => `\u6c38\u7d9a / \u5bff\u547dx${((t && t.mercuryLifeMultiplier) || 1.3).toFixed(2)}\uff08\u98df\u3079\u308b\u305f\u3073\u4e57\u7b97\uff09`,
|
||||
},
|
||||
giant_drug: {
|
||||
label: "巨大薬", color: "rgba(228, 123, 58, 0.86)", kind: "permanent", exclusiveGroup: "body_size",
|
||||
tags: ["item", "body", "behavior"], display: () => "永続 / サイズ3倍 / 攻撃2倍 / 速度1.5倍 / 毎秒1ダメージ",
|
||||
label: "\u5de8\u5927\u85ac", color: "rgba(228, 123, 58, 0.86)", kind: "permanent", exclusiveGroup: "body_size",
|
||||
tags: ["item", "body", "behavior"], display: () => "\u6c38\u7d9a / \u30b5\u30a4\u30ba3\u500d / \u653b\u64832\u500d / \u901f\u5ea61.5\u500d / \u6bce\u79d21\u30c0\u30e1\u30fc\u30b8",
|
||||
},
|
||||
dwarf_drug: {
|
||||
label: "矮小薬", color: "rgba(109, 107, 190, 0.82)", kind: "permanent", exclusiveGroup: "body_size",
|
||||
tags: ["item", "body", "behavior"], display: () => "永続 / サイズ0.25倍 / 攻撃0.25倍 / 速度0.5倍 / 毎秒1ダメージ",
|
||||
label: "\u77ee\u5c0f\u85ac", color: "rgba(109, 107, 190, 0.82)", kind: "permanent", exclusiveGroup: "body_size",
|
||||
tags: ["item", "body", "behavior"], display: () => "\u6c38\u7d9a / \u30b5\u30a4\u30ba0.25\u500d / \u653b\u64830.25\u500d / \u901f\u5ea60.5\u500d / \u6bce\u79d21\u30c0\u30e1\u30fc\u30b8",
|
||||
},
|
||||
love_mochi: {
|
||||
label: "へこ餅", color: "rgba(190,82,132,0.78)", kind: "timer", timerProp: "loveMochiTimer",
|
||||
tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}秒 / 繁殖行動が起きやすい`,
|
||||
label: "\u3078\u3053\u9905", color: "rgba(190,82,132,0.78)", kind: "timer", timerProp: "loveMochiTimer",
|
||||
tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u3044`,
|
||||
},
|
||||
fight_mochi: {
|
||||
label: "けんか餅", color: "rgba(180,86,52,0.80)", kind: "timer", timerProp: "fightMochiTimer",
|
||||
tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}秒 / 喧嘩が起きやすい`,
|
||||
label: "\u3051\u3093\u304b\u9905", color: "rgba(180,86,52,0.80)", kind: "timer", timerProp: "fightMochiTimer",
|
||||
tags: ["item", "mochi", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u55a7\u5629\u304c\u8d77\u304d\u3084\u3059\u3044`,
|
||||
},
|
||||
sleep_drug: {
|
||||
label: "ねむり薬", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"],
|
||||
label: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"],
|
||||
},
|
||||
mystery_drug: {
|
||||
label: "怪しい薬", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"],
|
||||
label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"],
|
||||
},
|
||||
zunda_juice: {
|
||||
label: "ずんだ汁", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"],
|
||||
label: "\u305a\u3093\u3060\u6c41", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"],
|
||||
},
|
||||
disease_zunchi: { label: "ずんち病", color: "rgba(113, 93, 59, 0.74)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_sleep: { label: "ねむり病", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_explosion: { label: "爆発病", color: "rgba(210, 90, 44, 0.78)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_fight: { label: "きずつき病", color: "rgba(180,86,52,0.80)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_zunchi: { label: "\u305a\u3093\u3061\u75c5", color: "rgba(113, 93, 59, 0.74)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_sleep: { label: "\u306d\u3080\u308a\u75c5", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_explosion: { label: "\u7206\u767a\u75c5", color: "rgba(210, 90, 44, 0.78)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
disease_fight: { label: "\u304d\u305a\u3064\u304d\u75c5", color: "rgba(180,86,52,0.80)", kind: "disease", tags: ["disease", "behavior"] },
|
||||
};
|
||||
|
||||
Object.assign(rawDefinitions.laxative, {
|
||||
modifiers: { hungerReliefMultiplier: 0 },
|
||||
tickInterval: 3,
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.ammo, {
|
||||
modifiers: { speedMultiplier: 10 },
|
||||
visualEffectId: "fall",
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.protein, {
|
||||
modifiers: { damageMultiplier: 1.75, diseaseChanceMultiplier: 0.5 },
|
||||
visualEffectId: "fall_up_red",
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.niteropu, {
|
||||
modifiers: { damageMultiplier: 0.5, diseaseChanceMultiplier: 2 },
|
||||
visualEffectId: "fall_down_purple",
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.mercury, {
|
||||
modifiers: { lifeMultiplierPerUse: 1.3 },
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.giant_drug, {
|
||||
modifiers: { sizeMultiplier: 3, damageMultiplier: 2, speedMultiplier: 1.5, damagePerSecond: 1 },
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.dwarf_drug, {
|
||||
modifiers: { sizeMultiplier: 0.25, damageMultiplier: 0.25, speedMultiplier: 0.5, damagePerSecond: 1 },
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
for (const id of ["love_mochi", "fight_mochi", "sleep_drug"]) {
|
||||
Object.assign(rawDefinitions[id], { mysteryEligible: true, zundaJuiceRemovable: id !== "sleep_drug" });
|
||||
}
|
||||
Object.assign(rawDefinitions.zunda_juice, { removesItemEffects: true, mysteryEligible: false });
|
||||
|
||||
Object.assign(rawDefinitions.love_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + rand(4, 12));
|
||||
tarinai.stress = clamp(tarinai.stress - 8, 0, 130);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.fight_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + rand(10, 18));
|
||||
tarinai.stress = clamp(tarinai.stress + 10, 0, 130);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.sleep_drug, {
|
||||
onApply(tarinai, context = {}) {
|
||||
return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.laxative, {
|
||||
onApply(tarinai, context = {}) {
|
||||
return tarinai.setTimedItemEffect?.("laxative", Math.round((this.defaultDuration || 60) * (context.scale ?? 1)));
|
||||
},
|
||||
onTick(tarinai) {
|
||||
tarinai.forceLaxativePoop?.();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.ammo, {
|
||||
onApply(tarinai, context = {}) {
|
||||
return tarinai.setTimedItemEffect?.("ammo", Math.round((this.defaultDuration || 18) * (context.scale ?? 1)));
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.protein, {
|
||||
onApply(tarinai) { return tarinai.setPowerItemMode?.("protein"); },
|
||||
});
|
||||
Object.assign(rawDefinitions.niteropu, {
|
||||
onApply(tarinai) { return tarinai.setPowerItemMode?.("niteropu"); },
|
||||
});
|
||||
Object.assign(rawDefinitions.mercury, {
|
||||
onApply(tarinai) { return tarinai.setMercuryEffect?.(); },
|
||||
});
|
||||
Object.assign(rawDefinitions.giant_drug, {
|
||||
onApply(tarinai) { return tarinai.setSizeItemMode?.("giant_drug"); },
|
||||
onTick(tarinai) {
|
||||
tarinai.damage?.(this.modifiers?.damagePerSecond || 1, this.label);
|
||||
if (tarinai.world?.effects) tarinai.spawnPowerItemEffect?.("giant_drug", 0.25);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.dwarf_drug, {
|
||||
onApply(tarinai) { return tarinai.setSizeItemMode?.("dwarf_drug"); },
|
||||
onTick(tarinai) {
|
||||
tarinai.damage?.(this.modifiers?.damagePerSecond || 1, this.label);
|
||||
if (tarinai.world?.effects) tarinai.spawnPowerItemEffect?.("dwarf_drug", 0.25);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.zunda_juice, {
|
||||
onApply(tarinai) { return tarinai.clearAllItemEffects?.(); },
|
||||
});
|
||||
Object.assign(rawDefinitions.disease_zunchi, {
|
||||
onApply(tarinai, context = {}) { return tarinai.infectZunchiDisease?.(context.item || null, true) || true; },
|
||||
});
|
||||
Object.assign(rawDefinitions.disease_sleep, {
|
||||
onApply(tarinai, context = {}) { return tarinai.infectSleepDisease?.(context.item || null) || true; },
|
||||
});
|
||||
Object.assign(rawDefinitions.disease_explosion, {
|
||||
onApply(tarinai, context = {}) { return tarinai.infectExplosionDisease?.(context.item || null) || true; },
|
||||
});
|
||||
Object.assign(rawDefinitions.disease_fight, {
|
||||
onApply(tarinai, context = {}) { return tarinai.infectFightDisease?.(context.item || null) || true; },
|
||||
});
|
||||
|
||||
class EffectRegistry {
|
||||
constructor(defs) { this.defs = new Map(Object.entries(defs || {})); }
|
||||
get(id = "") { return this.defs.get(String(id || "")) || null; }
|
||||
has(id = "") { return this.defs.has(String(id || "")); }
|
||||
label(id = "") { return this.get(id)?.label || id || "不明"; }
|
||||
label(id = "") { return this.get(id)?.label || id || "\u4e0d\u660e"; }
|
||||
color(id = "") { return this.get(id)?.color || "rgba(255,242,160,0.85)"; }
|
||||
display(id = "", timer = 0, tarinai = null) {
|
||||
const def = this.get(id);
|
||||
if (!def) return "";
|
||||
return typeof def.display === "function" ? def.display(timer, tarinai, def) : (def.display || "");
|
||||
}
|
||||
apply(id = "", tarinai = null, context = {}) {
|
||||
const def = this.get(id);
|
||||
if (!def || !tarinai || typeof def.onApply !== "function") return false;
|
||||
return Boolean(def.onApply.call(def, tarinai, { ...context, effectId: id, registry: this }));
|
||||
}
|
||||
tick(id = "", tarinai = null, dt = 0, context = {}) {
|
||||
const def = this.get(id);
|
||||
if (!def || !tarinai || typeof def.onTick !== "function") return false;
|
||||
return Boolean(def.onTick.call(def, tarinai, Math.max(0, Number(dt) || 0), { ...context, effectId: id, registry: this }));
|
||||
}
|
||||
remove(id = "", tarinai = null, context = {}) {
|
||||
const def = this.get(id);
|
||||
if (!def || !tarinai || typeof def.onRemove !== "function") return false;
|
||||
return Boolean(def.onRemove.call(def, tarinai, { ...context, effectId: id, registry: this }));
|
||||
}
|
||||
modifier(id = "", key = "", fallback = 1) {
|
||||
const value = this.get(id)?.modifiers?.[key];
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
timerValue(tarinai = null, id = "") {
|
||||
const def = this.get(id);
|
||||
if (!def || !tarinai) return 0;
|
||||
if (def.timerProp) return Number(tarinai[def.timerProp] || 0);
|
||||
if (def.kind === "timed") return Number(tarinai.itemEffectTimers?.[id] || 0);
|
||||
return 0;
|
||||
}
|
||||
isActive(tarinai = null, id = "") {
|
||||
if (!tarinai) return false;
|
||||
const def = this.get(id);
|
||||
if (!def) return false;
|
||||
if (id === "protein" || id === "niteropu") return tarinai.powerItemMode === id;
|
||||
if (id === "giant_drug" || id === "dwarf_drug") return tarinai.sizeItemMode === id;
|
||||
if (id === "mercury") return tarinai.lifeItemMode === "mercury";
|
||||
return this.timerValue(tarinai, id) > 0.04;
|
||||
}
|
||||
activeSummary(tarinai = null) {
|
||||
const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "love_mochi", "fight_mochi"];
|
||||
return order
|
||||
.filter(id => this.isActive(tarinai, id))
|
||||
.map(id => ({
|
||||
id,
|
||||
label: this.label(id),
|
||||
detail: this.display(id, this.timerValue(tarinai, id), tarinai),
|
||||
}));
|
||||
}
|
||||
labels() { return Object.fromEntries([...this.defs.entries()].map(([id, def]) => [id, def.label])); }
|
||||
colors() { return Object.fromEntries([...this.defs.entries()].map(([id, def]) => [id, def.color]).filter(([, color]) => color)); }
|
||||
randomPool() {
|
||||
return [...this.defs.entries()]
|
||||
.filter(([, def]) => (def.tags || []).includes("item") || (def.tags || []).includes("behavior") || (def.tags || []).includes("disease"))
|
||||
const itemEffects = [...this.defs.entries()]
|
||||
.filter(([id, def]) => Boolean(def.mysteryEligible && id !== "mystery_drug" && id !== "zunda_juice" && !(def.tags || []).includes("disease")))
|
||||
.map(([id]) => id)
|
||||
.filter(id => id !== "mystery_drug" && id !== "zunda_juice");
|
||||
const diseases = global.TarinaiDiseaseRegistry?.mysteryPool?.() || [];
|
||||
return [...new Set([...itemEffects, ...diseases])];
|
||||
}
|
||||
permanentIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "permanent").map(([id]) => id); }
|
||||
timedIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "timed" || def.kind === "timer").map(([id]) => id); }
|
||||
|
|
|
|||
52
js/food_registry.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
food: { label: "\u98df\u3079\u7269", nutrition: 8.5, hungerRelief: 0.95, interest: "normal", decayRateMultiplier: 1.0, hygienePenalty: 0.055 },
|
||||
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 7.2, hungerRelief: 0.75, interest: "zunda_high", decayRateMultiplier: 0.82, hygienePenalty: 0.055, cures: ["disease_explosion", "disease_fight"] },
|
||||
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "love_mochi" },
|
||||
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "fight_mochi" },
|
||||
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "sleep_drug" },
|
||||
laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "laxative", passThrough: true },
|
||||
protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "protein" },
|
||||
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "niteropu" },
|
||||
ammo: { label: "\u5f3e\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "ammo" },
|
||||
mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mystery_drug" },
|
||||
mercury: { label: "\u6c34\u9280", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mercury" },
|
||||
giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "giant_drug" },
|
||||
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" },
|
||||
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 6.8, hungerRelief: 0.82, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true },
|
||||
grass: { label: "\u8349", nutrition: 0.46, hungerRelief: 0.46, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water: { label: "\u6c34", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water_bowl: { label: "\u6c34\u306e\u76bf", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 0.62, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 }
|
||||
};
|
||||
|
||||
class FoodRegistry {
|
||||
constructor(defs) { this.defs = new Map(Object.entries(defs || {})); }
|
||||
get(type = "") { return this.defs.get(String(type || "")) || null; }
|
||||
has(type = "") { return this.defs.has(String(type || "")); }
|
||||
label(type = "") { return this.get(type)?.label || type || ""; }
|
||||
servingTypes() {
|
||||
return [...this.defs.entries()]
|
||||
.filter(([, def]) => def.interest && !["grass_like", "water", "corpse"].includes(def.interest))
|
||||
.map(([id]) => id);
|
||||
}
|
||||
typesByInterest(...interests) {
|
||||
const wanted = new Set(interests.flat().filter(Boolean));
|
||||
return [...this.defs.entries()]
|
||||
.filter(([, def]) => wanted.has(def.interest))
|
||||
.map(([id]) => id);
|
||||
}
|
||||
paramEffectTypes() {
|
||||
return [...this.defs.entries()].filter(([, def]) => def.effectId && !def.effectId.endsWith("_mochi") && def.effectId !== "sleep_drug").map(([id]) => id);
|
||||
}
|
||||
passiveDecayMultiplier(type = "") { return this.get(type)?.decayRateMultiplier ?? 1; }
|
||||
hygienePenalty(type = "") { return this.get(type)?.hygienePenalty ?? 0.055; }
|
||||
debugSnapshot() { return [...this.defs.entries()].map(([id, def]) => ({ id, label: def.label, interest: def.interest, effectId: def.effectId || "" })); }
|
||||
}
|
||||
|
||||
const registry = new FoodRegistry(rawDefinitions);
|
||||
global.TarinaiFoodRegistry = registry;
|
||||
global.TARINAI_FOOD_DEFINITIONS = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
26
js/health.js
|
|
@ -1,13 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
const HEALTH = (() => {
|
||||
const MAJOR_DAMAGE_WINDOW = 10;
|
||||
const MAJOR_DAMAGE_WINDOW = 24;
|
||||
const WEAKENED_TRACE_WINDOW = 45;
|
||||
const WEAKENED_SUFFIX = "\u3067\u8870\u5f31";
|
||||
const CAUSES = Object.freeze({
|
||||
fight: "\u55a7\u5629",
|
||||
hunger: "\u98e2\u9913",
|
||||
stress: "\u30b9\u30c8\u30ec\u30b9\u904e\u591a",
|
||||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e\u305f",
|
||||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e",
|
||||
firecracker: "\u7206\u7af9",
|
||||
genkotsu: "\u3052\u3093\u3053\u3064",
|
||||
accident: "\u4e8b\u6545",
|
||||
|
|
@ -19,8 +20,8 @@ const HEALTH = (() => {
|
|||
fightDisease: "\u304d\u305a\u3064\u304d\u75c5",
|
||||
zunchiDisease: "\u305a\u3093\u3061\u75c5",
|
||||
antEaten: "\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f",
|
||||
giantDrug: "巨大薬",
|
||||
dwarfDrug: "矮小薬",
|
||||
giantDrug: "\u5de8\u5927\u85ac",
|
||||
dwarfDrug: "\u77ee\u5c0f\u85ac",
|
||||
});
|
||||
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
||||
|
||||
|
|
@ -30,8 +31,8 @@ const HEALTH = (() => {
|
|||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight;
|
||||
if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu;
|
||||
if (/巨大薬|giant/.test(text)) return CAUSES.giantDrug;
|
||||
if (/矮小薬|dwarf/.test(text)) return CAUSES.dwarfDrug;
|
||||
if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug;
|
||||
if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug;
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
||||
if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds;
|
||||
if (/\u885d\u7a81|collision/.test(text)) return CAUSES.collision;
|
||||
|
|
@ -64,7 +65,7 @@ const HEALTH = (() => {
|
|||
t.lastDamageCause = cause;
|
||||
t.lastDamageAmount = Math.max(0, amount || 0);
|
||||
t.lastDamageAt = now;
|
||||
if (t.lastDamageAmount >= 10 || t.lastDamageAmount >= Math.max(6, (t.energy || 0) * 0.36)) {
|
||||
if (t.lastDamageAmount >= 6 || t.lastDamageAmount >= Math.max(4, (t.energy || 0) * 0.22)) {
|
||||
t.lastMajorDamageCause = cause;
|
||||
t.lastMajorDamageAmount = t.lastDamageAmount;
|
||||
t.lastMajorDamageAt = now;
|
||||
|
|
@ -94,14 +95,19 @@ const HEALTH = (() => {
|
|||
const recent = recentDamageCause(t, 20);
|
||||
const fighting = isFightingContext(t, recent);
|
||||
if (recentMajor && t.lastMajorDamageCause === CAUSES.collision) return CAUSES.collision;
|
||||
if (recentMajor && (!direct || direct === t.lastMajorDamageCause || opts.fromDamage || t.energy <= 22 || t.mood <= 20 || t.stress >= 118)) {
|
||||
if (recentMajor && (!direct || direct === t.lastMajorDamageCause || opts.fromDamage || t.energy <= 28 || t.mood <= 24 || t.stress >= 112 || t.hunger >= 100)) {
|
||||
return weakenedDeathReasonFor(t.lastMajorDamageCause);
|
||||
}
|
||||
const weakenedTrace = recentDamageCause(t, WEAKENED_TRACE_WINDOW);
|
||||
const traceCanWeaken = weakenedTrace && weakenedTrace !== CAUSES.stress && weakenedTrace !== CAUSES.hunger && weakenedTrace !== CAUSES.lifespan && weakenedTrace !== CAUSES.outOfBounds;
|
||||
if (traceCanWeaken && (!direct || direct === CAUSES.stress || direct === CAUSES.hunger || direct === weakenedTrace || opts.fromDamage) && (t.energy <= 30 || t.mood <= 24 || t.stress >= 112 || t.hunger >= 104)) {
|
||||
return weakenedDeathReasonFor(weakenedTrace);
|
||||
}
|
||||
if (direct && direct !== CAUSES.stress) return direct;
|
||||
if (t.zunchiDisease && ((t.zunchiDiseaseSeverity || 0) >= 0.30 || /\u75c5|zunchi/.test(text))) return CAUSES.zunchiDisease;
|
||||
if (t.hunger >= 108 || (t.hunger >= 94 && t.energy <= 26) || /\u98e2|\u7a7a\u8179|hunger/.test(text)) return CAUSES.hunger;
|
||||
if (fighting && (t.energy <= 44 || t.stress >= 112 || t.mood <= 22 || opts.fromDamage)) return CAUSES.fight;
|
||||
if (recent && recent !== CAUSES.stress && (t.energy <= 34 || t.mood <= 20 || t.stress >= 118 || opts.fromDamage)) return recent;
|
||||
if (recent && recent !== CAUSES.stress && (t.energy <= 34 || t.mood <= 22 || t.stress >= 116 || opts.fromDamage)) return weakenedDeathReasonFor(recent);
|
||||
if (text.includes(CAUSES.lifespan) || text.includes("\u5bff\u547d")) return CAUSES.lifespan;
|
||||
if (direct) return direct;
|
||||
if (t.stress >= 118 || /\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return CAUSES.stress;
|
||||
|
|
@ -127,6 +133,7 @@ const HEALTH = (() => {
|
|||
let cause = "";
|
||||
if (actualLoss > 0.01) {
|
||||
cause = rememberDamage(t, actualLoss, reason);
|
||||
t.world?.emit?.("tarinai:damaged", { tarinai: t, amount: actualLoss, cause, reason });
|
||||
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
||||
audio.damage?.(actualLoss);
|
||||
if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
|
|
@ -149,6 +156,7 @@ const HEALTH = (() => {
|
|||
|
||||
return Object.freeze({
|
||||
MAJOR_DAMAGE_WINDOW,
|
||||
WEAKENED_TRACE_WINDOW,
|
||||
CAUSES,
|
||||
causeLabel,
|
||||
recentDamageCause,
|
||||
|
|
|
|||
119
js/input_mode_manager.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const MODES = Object.freeze(["auto", "camera", "tool", "family"]);
|
||||
const STORAGE_KEY = "tarinai_mobile_input_mode_v1";
|
||||
|
||||
class InputModeManager {
|
||||
constructor() {
|
||||
this.storageKey = STORAGE_KEY;
|
||||
this.currentMode = this.loadMode();
|
||||
this.gestureState = {
|
||||
active: false,
|
||||
kind: "",
|
||||
startedAt: 0,
|
||||
pointerCount: 0,
|
||||
};
|
||||
this.classification = this.classify();
|
||||
}
|
||||
|
||||
loadMode() {
|
||||
try {
|
||||
const mode = global.localStorage?.getItem(STORAGE_KEY) || "auto";
|
||||
return MODES.includes(mode) ? mode : "auto";
|
||||
} catch (_) {
|
||||
return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode = "auto") {
|
||||
this.currentMode = MODES.includes(mode) ? mode : "auto";
|
||||
try { global.localStorage?.setItem(STORAGE_KEY, this.currentMode); } catch (_) {}
|
||||
this.emit("input:mode", this.snapshot());
|
||||
return this.currentMode;
|
||||
}
|
||||
|
||||
classify() {
|
||||
const nav = global.navigator || {};
|
||||
const ua = String(nav.userAgent || "");
|
||||
const mm = typeof global.matchMedia === "function" ? global.matchMedia.bind(global) : null;
|
||||
const width = Number(global.innerWidth || 9999);
|
||||
const height = Number(global.innerHeight || 9999);
|
||||
const shortSide = Math.min(width, height);
|
||||
const longSide = Math.max(width, height);
|
||||
const pointerCoarse = Boolean(mm?.("(pointer: coarse)")?.matches);
|
||||
const hasHover = Boolean(mm?.("(hover: hover)")?.matches);
|
||||
const noHover = Boolean(mm?.("(hover: none)")?.matches);
|
||||
const isTouchDevice = Number(nav.maxTouchPoints || 0) > 0 || "ontouchstart" in global;
|
||||
const isCompactViewport = shortSide <= 720 && longSide <= 1100;
|
||||
const userAgentMobile = /Android|iPhone|iPod|iPad|Mobile|Silk|Tablet/i.test(ua);
|
||||
const touchFirst = Boolean(pointerCoarse && noHover && !hasHover && isTouchDevice && (isCompactViewport || userAgentMobile));
|
||||
this.classification = Object.freeze({
|
||||
isTouchDevice,
|
||||
isCompactViewport,
|
||||
hasHover,
|
||||
pointerCoarse,
|
||||
userAgentMobile,
|
||||
touchFirst,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
return this.classification;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const previous = this.classification;
|
||||
const next = this.classify();
|
||||
if (JSON.stringify(previous) !== JSON.stringify(next)) this.emit("input:classification", this.snapshot());
|
||||
return next;
|
||||
}
|
||||
|
||||
beginGesture(kind = "", pointerCount = 1) {
|
||||
this.gestureState = {
|
||||
active: true,
|
||||
kind: String(kind || ""),
|
||||
startedAt: performance?.now?.() ?? Date.now(),
|
||||
pointerCount: Math.max(0, Number(pointerCount) || 0),
|
||||
};
|
||||
return this.gestureState;
|
||||
}
|
||||
|
||||
endGesture() {
|
||||
this.gestureState = { ...this.gestureState, active: false, pointerCount: 0 };
|
||||
return this.gestureState;
|
||||
}
|
||||
|
||||
canPanCanvas() {
|
||||
if (!this.classification?.touchFirst) return true;
|
||||
return this.currentMode === "auto" || this.currentMode === "camera";
|
||||
}
|
||||
|
||||
canPlaceTools() {
|
||||
if (!this.classification?.touchFirst) return true;
|
||||
return this.currentMode === "auto" || this.currentMode === "tool";
|
||||
}
|
||||
|
||||
canFamilyTreeCaptureTouch() {
|
||||
if (!this.classification?.touchFirst) return true;
|
||||
return this.currentMode === "auto" || this.currentMode === "family";
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
return Object.freeze({
|
||||
...this.classification,
|
||||
currentMode: this.currentMode,
|
||||
gestureState: { ...this.gestureState },
|
||||
canPanCanvas: this.canPanCanvas(),
|
||||
canPlaceTools: this.canPlaceTools(),
|
||||
canFamilyTreeCaptureTouch: this.canFamilyTreeCaptureTouch(),
|
||||
});
|
||||
}
|
||||
|
||||
emit(type, detail) {
|
||||
global.TarinaiEvents?.emit?.(type, detail);
|
||||
}
|
||||
}
|
||||
|
||||
global.InputModeManager = InputModeManager;
|
||||
global.TarinaiInputMode = global.TarinaiInputMode || new InputModeManager();
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
129
js/item_visual_registry.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const raw = String.raw`{
|
||||
"water": {
|
||||
"renderer": "oval_droplet",
|
||||
"shape": "horizontal_oval_liquid",
|
||||
"palette": { "fill": "rgba(86,157,224,0.82)", "stroke": "rgba(45,101,168,0.52)", "highlight": "rgba(255,255,255,0.64)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["horizontal", "oval", "liquid", "blue"]
|
||||
},
|
||||
"zunda_juice": {
|
||||
"renderer": "oval_droplet",
|
||||
"shape": "horizontal_oval_liquid",
|
||||
"palette": { "fill": "rgba(125,218,82,0.94)", "stroke": "rgba(63,150,50,0.62)", "highlight": "rgba(238,255,226,0.72)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["horizontal", "oval", "liquid", "green"]
|
||||
},
|
||||
"mercury": {
|
||||
"renderer": "oval_droplet",
|
||||
"shape": "horizontal_oval_liquid",
|
||||
"palette": { "fill": "rgba(210,216,222,0.94)", "stroke": "rgba(112,122,132,0.62)", "highlight": "rgba(255,255,255,0.72)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["horizontal", "oval", "liquid", "silver"]
|
||||
},
|
||||
"sleep_drug": {
|
||||
"renderer": "sleep_tablet",
|
||||
"shape": "moon_tablet",
|
||||
"palette": { "fill": "#ebe4ff", "stroke": "rgba(74,60,144,0.72)", "accent": "rgba(112,91,188,0.82)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "moon", "purple"]
|
||||
},
|
||||
"laxative": {
|
||||
"renderer": "laxative_tablet",
|
||||
"shape": "rounded_tablet_zunchi_motif",
|
||||
"palette": { "fill": "#f4d49a", "stroke": "rgba(98,61,29,0.82)", "accent": "rgba(96,70,38,0.88)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "zunchi motif", "tan"]
|
||||
},
|
||||
"mystery_drug": {
|
||||
"renderer": "mystery_tablet",
|
||||
"shape": "question_mark_tablet",
|
||||
"palette": { "fill": "#f3ecff", "stroke": "rgba(91,55,150,0.84)", "accent": "rgba(91,55,150,0.92)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "question mark", "purple"]
|
||||
},
|
||||
"ammo": {
|
||||
"renderer": "bullet",
|
||||
"shape": "bullet",
|
||||
"palette": { "fill": "#f4c46a", "stroke": "rgba(87,51,22,0.78)", "highlight": "rgba(255,237,176,0.62)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["bullet", "gold"]
|
||||
},
|
||||
"protein": {
|
||||
"renderer": "powder_pile",
|
||||
"shape": "powder_pile",
|
||||
"palette": { "fill": "#f4a24f", "stroke": "#b45b27", "highlight": "#ffe2b9" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["powder", "orange"]
|
||||
},
|
||||
"niteropu": {
|
||||
"renderer": "powder_pile",
|
||||
"shape": "powder_pile",
|
||||
"palette": { "fill": "#8a79d0", "stroke": "#4c3f91", "highlight": "#d8d1ff" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["powder", "purple"]
|
||||
},
|
||||
"giant_drug": {
|
||||
"renderer": "split_pill",
|
||||
"shape": "split_pill",
|
||||
"palette": { "fill": "#ffe3cf", "stroke": "#c9632f", "highlight": "#ffb47b" },
|
||||
"scale": 1.08,
|
||||
"uiPreviewScale": 1.08,
|
||||
"fieldScale": 1.08,
|
||||
"accessibleShapeTraits": ["pill", "large", "orange"]
|
||||
},
|
||||
"dwarf_drug": {
|
||||
"renderer": "split_pill",
|
||||
"shape": "split_pill",
|
||||
"palette": { "fill": "#e9e9ff", "stroke": "#6967ac", "highlight": "#b8b5f4" },
|
||||
"scale": 0.92,
|
||||
"uiPreviewScale": 0.92,
|
||||
"fieldScale": 0.92,
|
||||
"accessibleShapeTraits": ["pill", "small", "purple"]
|
||||
},
|
||||
"ball": {
|
||||
"renderer": "emoji",
|
||||
"shape": "soccer_ball",
|
||||
"palette": {},
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["round", "soccer ball"],
|
||||
"emojiFallback": "\u26bd"
|
||||
}
|
||||
}`;
|
||||
|
||||
const defs = Object.freeze(JSON.parse(raw));
|
||||
|
||||
function itemVisualDefinition(type = "") {
|
||||
return defs[String(type || "")] || null;
|
||||
}
|
||||
|
||||
function itemVisualPalette(type = "") {
|
||||
return itemVisualDefinition(type)?.palette || null;
|
||||
}
|
||||
|
||||
global.TARINAI_ITEM_VISUALS = defs;
|
||||
global.itemVisualDefinition = itemVisualDefinition;
|
||||
global.itemVisualPalette = itemVisualPalette;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
248
js/items.js
|
|
@ -1,8 +1,8 @@
|
|||
// tarinai_colony_game build 15.6.2: food servings, nest box access, and editable tarinai data
|
||||
"use strict";
|
||||
|
||||
const FOOD_SERVING_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
|
||||
const PARAM_EFFECT_ITEM_TYPES = new Set(["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
|
||||
const FOOD_SERVING_TYPES = new Set(window.TarinaiFoodRegistry?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
|
||||
const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiFoodRegistry?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
|
||||
function isParamEffectItemType(type = "") {
|
||||
return PARAM_EFFECT_ITEM_TYPES.has(type);
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ function passiveFoodDecayInterval(worldRef = null) {
|
|||
function passiveFoodDecayRate(item) {
|
||||
const size = item?.toolSize || "medium";
|
||||
const sizeMul = size === "small" ? 0.74 : (size === "large" ? 1.32 : 1);
|
||||
const typeMul = item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1);
|
||||
const typeMul = window.TarinaiFoodRegistry?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1));
|
||||
return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul;
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +232,24 @@ class Item {
|
|||
this.lingerTimer = 0;
|
||||
this.lingerDuration = 2.0;
|
||||
}
|
||||
if (type === "pushpin") {
|
||||
this.amount = 100;
|
||||
this.vx = rand(-120, 120);
|
||||
this.vy = rand(-90, 90);
|
||||
this.prevX = x;
|
||||
this.prevY = y;
|
||||
this.spin = rand(-0.35, 0.35);
|
||||
this.spinVelocity = rand(-5.4, 5.4);
|
||||
this.pinState = "loose";
|
||||
this.pinTargetId = "";
|
||||
this.pinAttachAngle = 0;
|
||||
this.pinAttachDistance = 0;
|
||||
this.pinOffsetY = 0;
|
||||
this.pinDamageTick = 0;
|
||||
this.pinFallCheckTimer = 0;
|
||||
this.pinLogAt = -999;
|
||||
this.deletable = true;
|
||||
}
|
||||
if (isServingFoodType(type)) {
|
||||
this.toolSize = "medium";
|
||||
this.foodServingScale = 1;
|
||||
|
|
@ -265,8 +283,10 @@ class Item {
|
|||
if (lost > 0) {
|
||||
this.foodServingsRemaining = Math.max(0, before - lost);
|
||||
this.amount = this.foodServingsRemaining;
|
||||
worldRef?.registerFoodSpoilage?.(lost * PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING, this);
|
||||
const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING;
|
||||
worldRef?.registerFoodSpoilage?.(lost * penalty, this);
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true });
|
||||
if (this.foodServingsRemaining <= 0.015) this.amount = 0;
|
||||
}
|
||||
} else if (["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(this.type)) {
|
||||
|
|
@ -274,8 +294,10 @@ class Item {
|
|||
const lost = Math.min(before, interval * 0.12);
|
||||
this.amount -= lost;
|
||||
if (lost > 0) {
|
||||
worldRef?.registerFoodSpoilage?.(lost * 0.002, this);
|
||||
const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? 0.002;
|
||||
worldRef?.registerFoodSpoilage?.(lost * penalty, this);
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -300,6 +322,7 @@ class Item {
|
|||
}
|
||||
}
|
||||
if (this.type === "ball") this.updateBall(dt, worldRef);
|
||||
if (this.type === "pushpin") this.updatePushpin(dt, worldRef);
|
||||
if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef);
|
||||
if (this.type === "grass") {
|
||||
this.lifecycleTimer += dt;
|
||||
|
|
@ -542,6 +565,157 @@ class Item {
|
|||
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" }));
|
||||
}
|
||||
|
||||
updatePushpin(dt, worldRef) {
|
||||
if (!worldRef) return;
|
||||
if (this.pinState === "lodged") {
|
||||
this.updateLodgedPushpin(dt, worldRef);
|
||||
return;
|
||||
}
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
const moving = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
if (moving > 0.05) {
|
||||
this.x += (this.vx || 0) * dt;
|
||||
this.y += (this.vy || 0) * dt;
|
||||
const p = Math.max(26, CONFIG.worldPadding || 30);
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; }
|
||||
if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; }
|
||||
if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; }
|
||||
const friction = Math.pow(0.18, dt * 0.85);
|
||||
this.vx *= friction;
|
||||
this.vy *= friction;
|
||||
this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(7, this.r || 16);
|
||||
this.spinVelocity *= Math.pow(0.38, dt);
|
||||
if (Math.hypot(this.vx || 0, this.vy || 0) < 7.5) { this.vx = 0; this.vy = 0; this.spinVelocity *= 0.3; }
|
||||
worldRef.drawListDirty = true;
|
||||
} else {
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.spinVelocity *= Math.pow(0.08, dt);
|
||||
this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt;
|
||||
}
|
||||
this.tryStickPushpin(worldRef);
|
||||
}
|
||||
|
||||
tryStickPushpin(worldRef) {
|
||||
if (!worldRef?.tarinai?.length) return false;
|
||||
if (this.pinState === "lodged") return false;
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
const hitRange = Math.max(10, (this.r || 8) * 1.25);
|
||||
for (const t of worldRef.tarinai) {
|
||||
if (!t || t.dead) continue;
|
||||
const d = distXY(this.x, this.y, t.x, t.y);
|
||||
const hit = (t.radius || 16) * 0.86 + hitRange;
|
||||
if (d <= hit && d < bestD) { best = t; bestD = d; }
|
||||
}
|
||||
if (!best) return false;
|
||||
return this.attachPushpin(best, worldRef, bestD);
|
||||
}
|
||||
|
||||
attachPushpin(t, worldRef, d = null) {
|
||||
if (!t || t.dead) return false;
|
||||
if (t.stuckPushpinId && t.stuckPushpinId !== this.id) return false;
|
||||
const dx = this.x - t.x;
|
||||
const dy = this.y - t.y;
|
||||
const distToTarget = Number.isFinite(d) ? d : Math.hypot(dx, dy);
|
||||
this.pinState = "lodged";
|
||||
this.deletable = false;
|
||||
this.pinTargetId = t.id;
|
||||
this.pinAttachAngle = Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1));
|
||||
this.pinAttachDistance = clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
|
||||
this.pinOffsetY = clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
||||
this.pinDamageTick = 0;
|
||||
this.pinFallCheckTimer = 0;
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.spinVelocity = 0;
|
||||
this.spin = this.pinAttachAngle;
|
||||
t.stuckPushpinId = this.id;
|
||||
t.sleeping = false;
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 1.3);
|
||||
t.state = "panic";
|
||||
t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) };
|
||||
t.addStress ? t.addStress(18, { threshold: 8 }) : (t.stress = clamp((t.stress || 0) + 18, 0, 130));
|
||||
if (t.damage) t.damage(6, "\u753b\u92f2");
|
||||
if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) {
|
||||
this.pinLogAt = worldRef.time || 0;
|
||||
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
}
|
||||
worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" }));
|
||||
return true;
|
||||
}
|
||||
|
||||
updateLodgedPushpin(dt, worldRef) {
|
||||
const t = worldRef.tarinai?.find(o => o && !o.dead && o.id === this.pinTargetId) || null;
|
||||
if (!t) {
|
||||
this.detachPushpin(worldRef, "owner-lost");
|
||||
return;
|
||||
}
|
||||
t.stuckPushpinId = this.id;
|
||||
this.deletable = false;
|
||||
this.x = t.x + (t.radius || 16) * 0.36;
|
||||
this.y = t.y - (t.radius || 16) * 0.04;
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
this.spin = 0.28;
|
||||
this.vx = t.vx || 0;
|
||||
this.vy = t.vy || 0;
|
||||
this.pinDamageTick = (this.pinDamageTick || 0) + dt;
|
||||
while (this.pinDamageTick >= 0.55) {
|
||||
this.pinDamageTick -= 0.55;
|
||||
if (t.damage) t.damage(1.4, "\u753b\u92f2");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
|
||||
t.addStress ? t.addStress(2.6, { threshold: 8, duration: 3.4 }) : (t.stress = clamp((t.stress || 0) + 2.6, 0, 130));
|
||||
if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
|
||||
}
|
||||
t.sleeping = false;
|
||||
t.state = "panic";
|
||||
t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) };
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.65);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22);
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4);
|
||||
t.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||
this.pinFallCheckTimer = (this.pinFallCheckTimer || 0) + dt;
|
||||
while (this.pinFallCheckTimer >= 1.0) {
|
||||
this.pinFallCheckTimer -= 1.0;
|
||||
if (Math.random() < 0.10) {
|
||||
this.detachPushpin(worldRef, "fall");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detachPushpin(worldRef, reason = "released") {
|
||||
const t = worldRef?.tarinai?.find(o => o && o.id === this.pinTargetId) || null;
|
||||
if (t && t.stuckPushpinId === this.id) t.stuckPushpinId = null;
|
||||
this.pinState = "loose";
|
||||
this.pinTargetId = "";
|
||||
this.pinDamageTick = 0;
|
||||
this.pinFallCheckTimer = 0;
|
||||
this.deletable = true;
|
||||
if (reason === "pinch") {
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.spinVelocity = 0;
|
||||
} else {
|
||||
this.vx = rand(-24, 24);
|
||||
this.vy = rand(-10, 18);
|
||||
this.spinVelocity = rand(-1.6, 1.6);
|
||||
this.spin += rand(-0.25, 0.25);
|
||||
if (t) {
|
||||
this.x = clamp(t.x + rand(-(t.radius || 16) * 0.75, (t.radius || 16) * 0.75), CONFIG.worldPadding || 30, (worldRef?.w || this.x) - (CONFIG.worldPadding || 30));
|
||||
this.y = clamp(t.y + rand((t.radius || 16) * 0.12, (t.radius || 16) * 0.72), CONFIG.worldPadding || 30, (worldRef?.h || this.y) - (CONFIG.worldPadding || 30));
|
||||
}
|
||||
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, (this.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" }));
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306e\u753b\u92f2\u304c\u629c\u3051\u843d\u3061\u305f\u3002`, "observe", { participants: [t] });
|
||||
}
|
||||
}
|
||||
|
||||
updateGrass(dt, worldRef) {
|
||||
const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7;
|
||||
const weather = worldRef.weather || "sunny";
|
||||
|
|
@ -754,18 +928,16 @@ class Item {
|
|||
}
|
||||
}
|
||||
} else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(this.type)) {
|
||||
const visual = typeof itemVisualDefinition === "function" ? itemVisualDefinition(this.type) : null;
|
||||
const palette = visual?.palette || {};
|
||||
const renderer = visual?.renderer || "split_pill";
|
||||
const meta = {
|
||||
laxative: { fill: "#f1d18a", stroke: "#774c1f", accent: "#5f3a1b", kind: "laxative" },
|
||||
protein: { fill: "#f4a24f", stroke: "#b45b27", accent: "#ffe2b9", kind: "powder" },
|
||||
niteropu: { fill: "#8a79d0", stroke: "#4c3f91", accent: "#d8d1ff", kind: "powder" },
|
||||
ammo: { fill: "#f4c46a", stroke: "#6f431f", accent: "#ffe8a8", kind: "bullet" },
|
||||
mystery_drug: { fill: "#dff4ff", stroke: "#27587b", accent: "#437c97", kind: "mystery" },
|
||||
mercury: { fill: "rgba(210,216,222,0.94)", stroke: "rgba(112,122,132,0.62)", accent: "rgba(255,255,255,0.72)", kind: "droplet" },
|
||||
giant_drug: { fill: "#ffe3cf", stroke: "#c9632f", accent: "#ffb47b", kind: "pill" },
|
||||
dwarf_drug: { fill: "#e9e9ff", stroke: "#6967ac", accent: "#b8b5f4", kind: "pill" },
|
||||
zunda_juice: { fill: "rgba(125,218,82,0.94)", stroke: "rgba(63,150,50,0.62)", accent: "rgba(238,255,226,0.72)", kind: "droplet" },
|
||||
}[this.type] || { fill: "#fffaf1", stroke: "#8d6f48", accent: "#ddd", kind: "pill" };
|
||||
const localR = this.r * (this.type === "giant_drug" ? 1.08 : (this.type === "dwarf_drug" ? 0.92 : 1));
|
||||
fill: palette.fill || "#fffaf1",
|
||||
stroke: palette.stroke || "#8d6f48",
|
||||
accent: palette.highlight || palette.accent || "#ddd",
|
||||
kind: renderer === "powder_pile" ? "powder" : renderer === "oval_droplet" ? "droplet" : renderer === "laxative_tablet" ? "laxative" : renderer === "mystery_tablet" ? "mystery" : renderer === "bullet" ? "bullet" : "pill",
|
||||
};
|
||||
const localR = this.r * (visual?.fieldScale || visual?.scale || 1);
|
||||
ctx.rotate(Math.sin(this.seed) * 0.10);
|
||||
if (meta.kind === "powder") {
|
||||
ctx.save();
|
||||
|
|
@ -873,6 +1045,50 @@ class Item {
|
|||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "pushpin") {
|
||||
const stuck = this.pinState === "lodged";
|
||||
const assetId = stuck ? "pushpin_stuck" : "pushpin";
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
|
||||
ctx.save();
|
||||
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
|
||||
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
if (img) {
|
||||
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
|
||||
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
|
||||
const w = stuck ? this.r * 2.15 : this.r * 1.78;
|
||||
const h = w * ratio;
|
||||
ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h);
|
||||
} else if (stuck) {
|
||||
ctx.fillStyle = "#d92736";
|
||||
ctx.strokeStyle = "rgba(125,18,30,0.78)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.r * 0.68, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.r * 0.26, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(176,20,30,0.82)";
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.fillStyle = "#d92736";
|
||||
ctx.strokeStyle = "rgba(125,18,30,0.78)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -this.r * 0.26, this.r * 0.70, this.r * 0.48, 0, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, this.r * 0.18, this.r * 0.82, this.r * 0.30, 0, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(210,210,215,0.92)";
|
||||
ctx.lineWidth = Math.max(1.5, this.r * 0.12);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, this.r * 0.42);
|
||||
ctx.lineTo(0, this.r * 1.65);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "water_bowl") {
|
||||
ctx.fillStyle = "rgba(171, 120, 76, 0.72)";
|
||||
ctx.strokeStyle = "rgba(94, 69, 48, 0.55)";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const LOADING_SPRITES = [
|
|||
"assets/sprites/tarinai_08_pokan.webp",
|
||||
"assets/sprites/tarinai_11_weak.webp",
|
||||
"assets/sprites/tarinai_21_normal_happy.webp",
|
||||
"assets/sprites/tarinai_29_hungry_70.webp",
|
||||
];
|
||||
let loadingSpriteTimer = 0;
|
||||
|
||||
|
|
@ -133,8 +134,9 @@ loadImages(null, (done, total) => {
|
|||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.3";
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.11";
|
||||
navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ class TarinaiPerformanceManager {
|
|||
|
||||
quality(worldRef = null) {
|
||||
const level = this.level;
|
||||
const antCount = (worldRef?.ants || []).reduce((n, a) => n + (a && !a.dead ? 1 : 0), 0);
|
||||
let antCount = 0;
|
||||
for (const ant of worldRef?.ants || []) if (ant && !ant.dead) antCount += 1;
|
||||
const itemCount = (worldRef?.items || []).length || 0;
|
||||
const effectCount = (worldRef?.effects || []).length || 0;
|
||||
const antStride = antCount >= 95 || level >= 3 ? 3 : (antCount >= 35 || level >= 2 ? 2 : 1);
|
||||
|
|
@ -46,7 +47,7 @@ class TarinaiPerformanceManager {
|
|||
itemCount,
|
||||
antCount,
|
||||
effectCount,
|
||||
effectLimit: level >= 3 ? 42 : level >= 2 ? 64 : level >= 1 ? 84 : 112,
|
||||
effectLimit: level >= 3 ? 34 : level >= 2 ? 52 : level >= 1 ? 68 : 88,
|
||||
statsInterval: level >= 3 ? 1.20 : level === 2 ? 0.92 : level === 1 ? 0.68 : 0.48,
|
||||
terrainRedrawInterval: level >= 3 ? 1.30 : level === 2 ? 1.05 : level === 1 ? 0.82 : 0.62,
|
||||
backgroundLightStep: level >= 3 ? 64 : level === 2 ? 96 : 180,
|
||||
|
|
|
|||
25
js/render.js
|
|
@ -131,11 +131,13 @@ function placementPreviewFor(world) {
|
|||
const pad = CONFIG.worldPadding || 30;
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
const r = itemRadiusFor(type, 12);
|
||||
const sizeScale = world?.toolSizeScale ? world.toolSizeScale(type) : 1;
|
||||
const sizeName = world?.toolSizeFor ? world.toolSizeFor(type) : "medium";
|
||||
const r = itemRadiusFor(type, 12) * sizeScale;
|
||||
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;
|
||||
let blocked = false;
|
||||
let tmp = { type, x, y, r, dead: false };
|
||||
let tmp = { type, x, y, r, toolSize: sizeName, foodServingScale: sizeScale, dead: false };
|
||||
if ((type === "fence_v" || type === "fence_h") && world.fenceRect) {
|
||||
const rawRect = world.fenceRect(tmp);
|
||||
return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) || world.fencePlacementBlocked?.(tmp) };
|
||||
|
|
@ -173,14 +175,14 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.strokeRect(part.left, part.top, part.right - part.left, part.bottom - part.top);
|
||||
}
|
||||
}
|
||||
} else if (type === "water_bowl" || type === "stone" || type === "ball" || type === "firecracker" || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") {
|
||||
} else if (type === "water_bowl" || type === "stone" || type === "ball" || type === "firecracker" || type === "pushpin" || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, Math.max(16, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2);
|
||||
ctx.arc(x, y, Math.max(8, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, Math.max(14, r * 1.55), Math.max(9, r * 0.86), 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(x, y, Math.max(9, r * 1.55), Math.max(6, r * 0.86), 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
|
@ -740,6 +742,7 @@ function render() {
|
|||
}
|
||||
for (const it of world.items) {
|
||||
if (isCachedTerrainItem(it)) continue;
|
||||
if (it.type === "pushpin" && it.pinState === "lodged") continue;
|
||||
if (it.type !== "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
|
|
@ -766,6 +769,9 @@ function render() {
|
|||
for (const t of sorted) {
|
||||
if (isEntityVisibleInRect(t, visibleRect)) t.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (it.type === "pushpin" && it.pinState === "lodged" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const ef of world.effects) {
|
||||
if (ef.type !== "bleed" && isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
}
|
||||
|
|
@ -837,11 +843,8 @@ function drawNestBoxTooltip(ctx, world) {
|
|||
const info = world.pointerNestBoxInfo?.();
|
||||
if (!info) return;
|
||||
const box = info.box;
|
||||
const capacity = info.capacity || world.nestBoxCapacity?.(box) || 5;
|
||||
const title = `\u5de3\u7bb1 (${info.occupants.length}/${capacity})\u5339`;
|
||||
const lines = info.occupants.length
|
||||
? [title, ...info.occupants.map(t => t.name)]
|
||||
: [title, "\u4e2d\u306b\u306f\u8ab0\u3082\u3044\u306a\u3044"];
|
||||
const title = "\u5de3\u7bb1";
|
||||
const lines = [title];
|
||||
ctx.save();
|
||||
ctx.font = "12px Yomogi, sans-serif";
|
||||
const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0);
|
||||
|
|
@ -889,7 +892,7 @@ function drawSelectedCard(ctx, world, lighting) {
|
|||
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
|
||||
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
|
||||
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 + 52);
|
||||
ctx.fillText(`\u7dcf\u5408\u7684\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();
|
||||
}
|
||||
|
|
|
|||
124
js/sim_core.js
|
|
@ -64,6 +64,62 @@ function stableUnit(seed, salt = "") {
|
|||
return ((h >>> 0) % 100000) / 100000;
|
||||
}
|
||||
|
||||
const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
|
||||
const GENETIC_LIMITS = Object.freeze({
|
||||
lifeSpanMul: [0.82, 1.24],
|
||||
attackMul: [0.80, 1.24],
|
||||
speedMul: [0.86, 1.18],
|
||||
sizeMul: [0.84, 1.18],
|
||||
});
|
||||
|
||||
function clampGeneticValue(key, value) {
|
||||
const range = GENETIC_LIMITS[key] || [0.75, 1.25];
|
||||
return clamp(Number(value) || 1, range[0], range[1]);
|
||||
}
|
||||
|
||||
function randomGenetics(seed = "") {
|
||||
return {
|
||||
lifeSpanMul: clampGeneticValue("lifeSpanMul", 0.82 + stableUnit(seed, "gene-life") * 0.42),
|
||||
attackMul: clampGeneticValue("attackMul", 0.80 + stableUnit(seed, "gene-attack") * 0.44),
|
||||
speedMul: clampGeneticValue("speedMul", 0.86 + stableUnit(seed, "gene-speed") * 0.32),
|
||||
sizeMul: clampGeneticValue("sizeMul", 0.84 + stableUnit(seed, "gene-size") * 0.34),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGenetics(source = null, seed = "") {
|
||||
const fallback = randomGenetics(seed);
|
||||
const out = {};
|
||||
for (const key of GENETIC_KEYS) out[key] = clampGeneticValue(key, source?.[key] ?? fallback[key]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function inheritedGenetics(a, b, seed = "") {
|
||||
const ga = normalizeGenetics(a?.genetics, a?.familyKey || a?.id || "a");
|
||||
const gb = normalizeGenetics(b?.genetics, b?.familyKey || b?.id || "b");
|
||||
const out = {};
|
||||
for (const key of GENETIC_KEYS) {
|
||||
const base = ((ga[key] || 1) + (gb[key] || 1)) / 2;
|
||||
const mutation = (stableUnit(seed, `gene-mut-${key}`) - 0.5) * 0.10;
|
||||
out[key] = clampGeneticValue(key, base + mutation);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function adultScaleFromGenetics(seed = "", genetics = null) {
|
||||
const g = normalizeGenetics(genetics, seed);
|
||||
return clamp((0.265 + stableUnit(seed, "adult-scale") * 0.060) * g.sizeMul, 0.205, 0.390);
|
||||
}
|
||||
|
||||
function childGrowthScaleFactor(growth = 1) {
|
||||
const x = clamp(Number(growth) || 0, 0, 1);
|
||||
const smooth = x * x * (3 - 2 * x);
|
||||
return 0.46 + smooth * 0.54;
|
||||
}
|
||||
|
||||
function scaleForGrowth(adultScale = 0.28, growth = 1) {
|
||||
return clamp((Number(adultScale) || 0.28) * childGrowthScaleFactor(growth), 0.10, 0.42);
|
||||
}
|
||||
|
||||
const PERSONALITIES = Object.freeze({});
|
||||
const PERSONALITY_IDS = [];
|
||||
|
||||
|
|
@ -73,7 +129,7 @@ function personalityForSeed(seed) {
|
|||
|
||||
const PERSONALITY_KEYS = ["aggression", "openness", "sociability", "neuroticism"];
|
||||
const PERSONALITY_DAILY_LIMIT = { perAxis: 0.08, total: 0.20 };
|
||||
const FRIEND_AFFINITY_THRESHOLD = 16;
|
||||
const FRIEND_AFFINITY_THRESHOLD = 14;
|
||||
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" },
|
||||
|
|
@ -91,6 +147,28 @@ function normalizePersonality(source = null) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function zunchiSlavePersonalityValue() {
|
||||
const out = {};
|
||||
for (const key of PERSONALITY_KEYS) out[key] = -1;
|
||||
return out;
|
||||
}
|
||||
|
||||
function applyZunchiSlavePersonality(tarinai, opts = {}) {
|
||||
if (!tarinai) return false;
|
||||
const next = zunchiSlavePersonalityValue();
|
||||
let changed = false;
|
||||
tarinai.birthPersonality = normalizePersonality(tarinai.birthPersonality || next);
|
||||
tarinai.currentPersonality = normalizePersonality(tarinai.currentPersonality || next);
|
||||
for (const key of PERSONALITY_KEYS) {
|
||||
if ((tarinai.currentPersonality[key] || 0) !== -1) changed = true;
|
||||
tarinai.currentPersonality[key] = -1;
|
||||
if (opts.birth) tarinai.birthPersonality[key] = -1;
|
||||
}
|
||||
tarinai.personalityDaily = { day: tarinai.world?.day || 1, total: 0, byKey: {}, byCause: {} };
|
||||
if (changed && opts.record !== false) tarinai.recordChangeCause?.("\u305a\u3093\u3061\u3069\u308c\u3044\u5316", "\u6027\u683c", { value: -1 });
|
||||
return changed;
|
||||
}
|
||||
|
||||
function randomBirthPersonality(seed = "") {
|
||||
const out = {};
|
||||
for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(stableUnit(seed, `personality-${key}`) * 1.2 - 0.6);
|
||||
|
|
@ -137,6 +215,10 @@ function personalityReasonText(reason = "") {
|
|||
"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",
|
||||
"after ammo knockback": "\u5f3e\u85ac\u3067\u5f3e\u304d\u98db\u3070\u3057\u305f\u5f71\u97ff\u3067",
|
||||
"after successful intimidation": "\u5a01\u5687\u306b\u6210\u529f\u3057\u305f\u5f71\u97ff\u3067",
|
||||
"after failed intimidation": "\u5a01\u5687\u306b\u5931\u6557\u3057\u305f\u5f71\u97ff\u3067",
|
||||
"after being struck by genkotsu": "\u3052\u3093\u3053\u3064\u3092\u53d7\u3051\u305f\u5f71\u97ff\u3067",
|
||||
};
|
||||
return map[key] || key;
|
||||
}
|
||||
|
|
@ -171,14 +253,31 @@ function personalityDailyState(tarinai) {
|
|||
const day = tarinai?.world?.day || 1;
|
||||
const state = tarinai.personalityDaily || {};
|
||||
if (state.day !== day) {
|
||||
tarinai.personalityDaily = { day, total: 0, byKey: {} };
|
||||
tarinai.personalityDaily = { day, total: 0, byKey: {}, byCause: {} };
|
||||
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) };
|
||||
if (typeof v === "number") state.byKey[key] = { up: Math.max(0, v), down: Math.max(0, -v), causes: [] };
|
||||
else {
|
||||
const causes = Array.isArray(v?.causes) ? [...new Set(v.causes.map(c => String(c || "").trim()).filter(Boolean))].slice(0, 4) : [];
|
||||
state.byKey[key] = { up: Math.max(0, Number(v?.up) || 0), down: Math.max(0, Number(v?.down) || 0), causes };
|
||||
}
|
||||
}
|
||||
state.byCause = state.byCause && typeof state.byCause === "object" ? state.byCause : {};
|
||||
for (const cause of Object.keys(state.byCause)) {
|
||||
const entry = state.byCause[cause] || {};
|
||||
const byKey = entry.byKey && typeof entry.byKey === "object" ? entry.byKey : {};
|
||||
const nextByKey = {};
|
||||
for (const key of PERSONALITY_KEYS) {
|
||||
const v = byKey[key] || {};
|
||||
const up = Math.max(0, Number(v.up) || 0);
|
||||
const down = Math.max(0, Number(v.down) || 0);
|
||||
if (up > 0.0001 || down > 0.0001) nextByKey[key] = { up, down };
|
||||
}
|
||||
if (Object.keys(nextByKey).length) state.byCause[cause] = { byKey: nextByKey };
|
||||
else delete state.byCause[cause];
|
||||
}
|
||||
state.total = Math.max(0, Number(state.total) || 0);
|
||||
tarinai.personalityDaily = state;
|
||||
|
|
@ -229,8 +328,23 @@ function adjustPersonality(tarinai, key, delta, reason = "") {
|
|||
if (!actual) return 0;
|
||||
tarinai.currentPersonality[key] = after;
|
||||
state.byKey[key] = used;
|
||||
if (!Array.isArray(used.causes)) used.causes = [];
|
||||
used[actual > 0 ? "up" : "down"] = (used[actual > 0 ? "up" : "down"] || 0) + Math.abs(actual);
|
||||
state.total = (state.total || 0) + Math.abs(actual);
|
||||
const label = PERSONALITY_LABELS[key]?.label || key;
|
||||
const reasonText = personalityReasonText(reason) || "\u6027\u683c\u5909\u5316";
|
||||
if (reasonText && !used.causes.includes(reasonText)) used.causes.push(reasonText);
|
||||
if (used.causes.length > 4) used.causes.length = 4;
|
||||
if (reasonText) {
|
||||
state.byCause = state.byCause && typeof state.byCause === "object" ? state.byCause : {};
|
||||
const causeEntry = state.byCause[reasonText] || { byKey: {} };
|
||||
causeEntry.byKey = causeEntry.byKey && typeof causeEntry.byKey === "object" ? causeEntry.byKey : {};
|
||||
const causeKeyEntry = causeEntry.byKey[key] || { up: 0, down: 0 };
|
||||
causeKeyEntry[actual > 0 ? "up" : "down"] = (causeKeyEntry[actual > 0 ? "up" : "down"] || 0) + Math.abs(actual);
|
||||
causeEntry.byKey[key] = causeKeyEntry;
|
||||
state.byCause[reasonText] = causeEntry;
|
||||
}
|
||||
tarinai.recordChangeCause?.(reasonText, label, { value: actual });
|
||||
const afterBand = personalityBand(after);
|
||||
if (beforeBand !== afterBand) {
|
||||
const text = personalityThresholdLogText(tarinai, key, after, reason, beforeBand);
|
||||
|
|
@ -458,7 +572,7 @@ class SpatialGrid {
|
|||
const type = it.type || "";
|
||||
if (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h") this.add(this.obstacleCells, it);
|
||||
if (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "water_bowl" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))) this.add(this.foodCells, it);
|
||||
if (type === "firecracker" || type === "genkotsu" || type === "sleep_drug" || type === "zunchi" || type === "splat" || type === "mystery_drug" || type === "laxative" || type === "niteropu" || type === "mercury" || type === "giant_drug" || type === "dwarf_drug") this.add(this.hazardCells, it);
|
||||
if (type === "firecracker" || type === "genkotsu" || type === "pushpin" || type === "sleep_drug" || type === "zunchi" || type === "splat" || type === "mystery_drug" || type === "laxative" || type === "niteropu" || type === "mercury" || type === "giant_drug" || type === "dwarf_drug") this.add(this.hazardCells, it);
|
||||
}
|
||||
|
||||
rebuild(items, tarinai, ants = []) {
|
||||
|
|
|
|||
|
|
@ -41,9 +41,21 @@
|
|||
sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3"
|
||||
},
|
||||
categories: {
|
||||
voice: "たりないの声",
|
||||
notify: "通知",
|
||||
ops: "操作"
|
||||
voice: "\u305f\u308a\u306a\u3044\u306e\u58f0",
|
||||
notify: "\u901a\u77e5",
|
||||
ops: "\u64cd\u4f5c"
|
||||
},
|
||||
soundMap: {
|
||||
voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice",
|
||||
sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice",
|
||||
sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice",
|
||||
sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice",
|
||||
sfx_notify: "notify", sfx_phase: "notify", sfx_log_push: "notify",
|
||||
sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops",
|
||||
sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops",
|
||||
sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops",
|
||||
sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops",
|
||||
sfx_zunchi: "ops", sfx_ant_nest: "ops"
|
||||
}
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -30,19 +30,25 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
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.genetics = normalizeGenetics(opts.genetics, this.familyKey);
|
||||
this.trait = makeTrait(this.type);
|
||||
this.trait.speed *= this.genetics.speedMul || 1;
|
||||
this.birthTime = opts.birthTime ?? this.world.time;
|
||||
const generationOpt = opts.generation ?? 1;
|
||||
const growthAtConstruct = generationOpt > 1 ? clamp(((this.world?.time || 0) - this.birthTime) / Math.max(1, CONFIG.childGrowTime || 120), 0, 1) : 1;
|
||||
this.x = opts.x ?? rand(100, world.w - 100);
|
||||
this.y = opts.y ?? rand(100, world.h - 100);
|
||||
this.vx = opts.vx ?? 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.adultScale = Number.isFinite(opts.adultScale) ? opts.adultScale : (Number.isFinite(opts.baseAdultScale) ? opts.baseAdultScale : (Number.isFinite(opts.scale) && growthAtConstruct >= 1 ? opts.scale : adultScaleFromGenetics(this.familyKey, this.genetics)));
|
||||
this.scale = Number.isFinite(opts.scale) ? opts.scale : (generationOpt > 1 ? scaleForGrowth(this.adultScale, growthAtConstruct) : this.adultScale);
|
||||
this.radius = 56 * this.scale;
|
||||
this.age = opts.age ?? rand(0, 20);
|
||||
this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200);
|
||||
if (this.lifeSpan < 1200) this.lifeSpan = rand(1400, 2200);
|
||||
this.generation = opts.generation ?? 1;
|
||||
this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200) * (this.genetics.lifeSpanMul || 1);
|
||||
if (this.lifeSpan < 900) this.lifeSpan = rand(1200, 1800) * (this.genetics.lifeSpanMul || 1);
|
||||
this.generation = generationOpt;
|
||||
this.hasPaired = opts.hasPaired ?? false;
|
||||
this.hunger = opts.hunger ?? rand(16, 45);
|
||||
this.loneliness = opts.loneliness ?? rand(18, 62);
|
||||
|
|
@ -104,14 +110,15 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0;
|
||||
this.totalFightLosses = opts.totalFightLosses || 0;
|
||||
this.totalFightWins = opts.totalFightWins || 0;
|
||||
this.isZunchiSlave = Boolean(opts.isZunchiSlave);
|
||||
this.isZunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
|
||||
this.zunchiSlaveLocked = Boolean(opts.zunchiSlaveLocked || this.isZunchiSlave);
|
||||
this.formerName = opts.formerName || null;
|
||||
this.loveMochiTimer = opts.loveMochiTimer ?? 0;
|
||||
this.fightMochiTimer = opts.fightMochiTimer ?? 0;
|
||||
this.sleepDisease = Boolean(opts.sleepDisease);
|
||||
this.sleepDiseaseCooldown = opts.sleepDiseaseCooldown ?? 0;
|
||||
this.explosionDisease = Boolean(opts.explosionDisease);
|
||||
this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? (CONFIG.dayLength || 120) * 0.5 : 0);
|
||||
this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? 60 : 0);
|
||||
this.fightDisease = Boolean(opts.fightDisease);
|
||||
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
|
||||
this.lastBleedAt = opts.lastBleedAt ?? -999;
|
||||
|
|
@ -121,13 +128,17 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
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.zunchiSlaveLocked = true;
|
||||
this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
|
||||
applyZunchiSlavePersonality(this, { record: false, birth: false });
|
||||
this.visibleSpriteId = "zunchi_slave";
|
||||
}
|
||||
this.tempComfort = opts.tempComfort ?? 0.62;
|
||||
this.tempTimer = opts.tempTimer ?? stableUnit(this.familyKey, "temp-start") * 0.55;
|
||||
this.parents = opts.parents || [];
|
||||
this.parentNames = opts.parentNames || [];
|
||||
this.children = opts.children || [];
|
||||
this.birthTime = opts.birthTime ?? this.world.time;
|
||||
this.nextEatSound = opts.nextEatSound ?? 0;
|
||||
this.fightTimer = opts.fightTimer ?? 0;
|
||||
this.fightCooldown = opts.fightCooldown ?? rand(0, CONFIG.fightCooldown);
|
||||
|
|
@ -140,6 +151,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.lastMealColor = opts.lastMealColor || "#f7cf67";
|
||||
this.grassEatTimer = opts.grassEatTimer ?? 0;
|
||||
this.hurtTimer = opts.hurtTimer ?? 0;
|
||||
this.stuckPushpinId = opts.stuckPushpinId || null;
|
||||
this.defeatedTimer = opts.defeatedTimer ?? 0;
|
||||
this.defeatedById = opts.defeatedById || null;
|
||||
this.fightWinnerId = opts.fightWinnerId || null;
|
||||
|
|
@ -161,6 +173,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22;
|
||||
this.envTimer = opts.envTimer ?? stableUnit(this.familyKey, "env-start") * 0.55;
|
||||
this.visibleSpriteId = opts.visibleSpriteId || null;
|
||||
this.zunchiSlaveSpriteVariant = opts.zunchiSlaveSpriteVariant || (opts.isZunchiSlave ? (Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt") : "");
|
||||
this.spriteLockUntil = opts.spriteLockUntil ?? 0;
|
||||
this.entryTimer = opts.entryTimer ?? 0;
|
||||
const effectTimers = opts.itemEffectTimers && typeof opts.itemEffectTimers === "object" ? opts.itemEffectTimers : {};
|
||||
|
|
@ -179,6 +192,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.powerEffectPulse = opts.powerEffectPulse || 0;
|
||||
this.powerEffectKind = opts.powerEffectKind || this.powerItemMode || this.sizeItemMode || this.lifeItemMode || "";
|
||||
this.mysteryDrugHistory = Array.isArray(opts.mysteryDrugHistory) ? opts.mysteryDrugHistory.slice(-8) : [];
|
||||
this.recentChangeCauses = Array.isArray(opts.recentChangeCauses) ? opts.recentChangeCauses.slice(-8) : [];
|
||||
if (this.refreshEffectiveSize) this.refreshEffectiveSize();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
this.thought = "\u305a\u3093\u3061\u75c5\u306b\u304b\u304b\u3063\u305f";
|
||||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "\u3046\u3045\u2026", "rgba(38,82,38,0.82)");
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_zunchi", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u305a\u3093\u3061\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -28,6 +30,8 @@
|
|||
this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f";
|
||||
audio.heal?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)");
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_zunchi", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u305a\u3093\u3061\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -91,6 +95,8 @@
|
|||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(72,76,120,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_sleep", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u306d\u3080\u308a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -108,18 +114,22 @@
|
|||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "!", "rgba(72,76,120,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_sleep", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u306d\u3080\u308a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
infectExplosionDisease(source = null) {
|
||||
if (this.dead || this.explosionDisease) return false;
|
||||
this.explosionDisease = true;
|
||||
this.explosionDiseaseTimer = Math.max(this.explosionDiseaseTimer || 0, (CONFIG.dayLength || 120) * 0.5);
|
||||
this.explosionDiseaseTimer = Math.max(this.explosionDiseaseTimer || 0, 60);
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.55);
|
||||
this.thought = "\u7206\u767a\u75c5\u3067\u843d\u3061\u7740\u304b\u306a\u3044";
|
||||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.2, "!", "rgba(178,78,42,0.82)");
|
||||
this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_explosion", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u7206\u767a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -132,6 +142,8 @@
|
|||
this.stress = clamp(this.stress - 10, 0, 130);
|
||||
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\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_explosion", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u7206\u767a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -148,6 +160,8 @@
|
|||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)");
|
||||
this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_fight", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u304d\u305a\u3064\u304d\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -160,6 +174,8 @@
|
|||
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?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_fight", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u304d\u305a\u3064\u304d\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -181,7 +197,7 @@
|
|||
this.y = this.sleepDiseaseAnchorY;
|
||||
}
|
||||
this.wanderAngle = this.wanderAngle || 0;
|
||||
this.thought = beingDraggedByAnt ? "眠ったままアリに運ばれている" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
|
||||
this.thought = beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b";
|
||||
if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) {
|
||||
this.nextSleepBubbleAt = this.world.time + 5.0;
|
||||
this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
||||
|
|
|
|||
|
|
@ -24,6 +24,29 @@
|
|||
return this.generation > 1 && this.growth < 1;
|
||||
},
|
||||
|
||||
syncGrowthScale() {
|
||||
if (!Number.isFinite(this.adultScale)) this.adultScale = adultScaleFromGenetics(this.familyKey || this.id, this.genetics);
|
||||
const nextScale = this.generation > 1 ? scaleForGrowth(this.adultScale, this.growth) : this.adultScale;
|
||||
this.scale = clamp(nextScale, 0.10, 0.42);
|
||||
if (this.refreshEffectiveSize) this.refreshEffectiveSize();
|
||||
else this.radius = 56 * this.scale;
|
||||
const growthBucket = Math.floor(clamp(this.growth || 0, 0, 1) * 20);
|
||||
if (this.generation > 1 && this.world?.recordFamily && this.familyJoined?.() && this.familyGrowthBucket !== growthBucket) {
|
||||
this.familyGrowthBucket = growthBucket;
|
||||
this.world.recordFamily(this);
|
||||
}
|
||||
return this.scale;
|
||||
},
|
||||
|
||||
geneticStatRatio(key = "") {
|
||||
this.genetics = normalizeGenetics(this.genetics, this.familyKey || this.id);
|
||||
if (key === "life") return this.genetics.lifeSpanMul || 1;
|
||||
if (key === "attack") return this.genetics.attackMul || 1;
|
||||
if (key === "speed") return this.genetics.speedMul || 1;
|
||||
if (key === "size") return this.genetics.sizeMul || 1;
|
||||
return 1;
|
||||
},
|
||||
|
||||
parentToFollow() {
|
||||
if (!this.juvenile || !this.parents?.length) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
|
|
@ -52,6 +75,32 @@
|
|||
if (this.records.length > 42) this.records.length = 42;
|
||||
},
|
||||
|
||||
recordChangeCause(reason = "", target = "", options = {}) {
|
||||
const text = String(reason || "").trim();
|
||||
const affected = String(target || "").trim();
|
||||
if (!text && !affected) return false;
|
||||
if (!Array.isArray(this.recentChangeCauses)) this.recentChangeCauses = [];
|
||||
const now = this.world?.time || 0;
|
||||
const entry = {
|
||||
time: now,
|
||||
reason: text || "\u5909\u5316",
|
||||
target: affected || "\u72b6\u614b",
|
||||
value: options.value ?? "",
|
||||
};
|
||||
const head = this.recentChangeCauses[0];
|
||||
if (head && head.reason === entry.reason && head.target === entry.target) {
|
||||
const oldValue = Number(head.value);
|
||||
const addValue = Number(entry.value);
|
||||
if (Number.isFinite(oldValue) || Number.isFinite(addValue)) head.value = (Number.isFinite(oldValue) ? oldValue : 0) + (Number.isFinite(addValue) ? addValue : 0);
|
||||
else head.value = entry.value || head.value || "";
|
||||
head.time = now;
|
||||
return true;
|
||||
}
|
||||
this.recentChangeCauses.unshift(entry);
|
||||
if (this.recentChangeCauses.length > 8) this.recentChangeCauses.length = 8;
|
||||
return true;
|
||||
},
|
||||
|
||||
personalityProfile() {
|
||||
const base = { label: "\u4e2d\u7acb", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
|
||||
const p = ensurePersonality(this).currentPersonality;
|
||||
|
|
@ -205,7 +254,8 @@
|
|||
},
|
||||
|
||||
addStress(amount, opts = {}) {
|
||||
const delta = Math.max(0, amount || 0);
|
||||
const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88;
|
||||
const delta = Math.max(0, (amount || 0) * tolerance);
|
||||
if (delta <= 0) return 0;
|
||||
const before = this.stress || 0;
|
||||
this.stress = clamp(before + delta, 0, 130);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
]);
|
||||
|
||||
function effectLabel(id = "") {
|
||||
return EFFECT_REGISTRY?.label?.(id) || ITEM_EFFECT_LABELS[id] || id || "不明";
|
||||
return EFFECT_REGISTRY?.label?.(id) || ITEM_EFFECT_LABELS[id] || id || "\u4e0d\u660e";
|
||||
}
|
||||
|
||||
function sizeDurationScale(item = null) {
|
||||
|
|
@ -23,6 +23,10 @@
|
|||
return 1;
|
||||
}
|
||||
|
||||
function recordItemChange(tarinai, id = "", reason = "\u30a2\u30a4\u30c6\u30e0\u52b9\u679c") {
|
||||
tarinai?.recordChangeCause?.(reason, effectLabel(id));
|
||||
}
|
||||
|
||||
function pickRandomEffects(count = 2) {
|
||||
const pool = [...RANDOM_EFFECT_POOL];
|
||||
const out = [];
|
||||
|
|
@ -64,12 +68,15 @@
|
|||
if (!(id in this.itemEffectTimers)) this.itemEffectTimers[id] = 0;
|
||||
this.itemEffectTimers[id] = Math.max(this.itemEffectTimers[id] || 0, Number(seconds) || 0);
|
||||
if (id === "laxative") this.laxativePoopTimer = Math.min(Math.max(this.laxativePoopTimer || 3, 0.2), 3);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: id, duration: this.itemEffectTimers[id] || 0 });
|
||||
recordItemChange(this, id, "\u6642\u9593\u52b9\u679c");
|
||||
return true;
|
||||
},
|
||||
|
||||
itemSizeMultiplier() {
|
||||
this.ensureItemEffectState();
|
||||
if (this.sizeItemMode === "giant_drug") return 3;
|
||||
if (this.sizeItemMode === "dwarf_drug") return 0.25;
|
||||
if (this.sizeItemMode === "giant_drug") return EFFECT_REGISTRY?.modifier?.("giant_drug", "sizeMultiplier", 3) ?? 3;
|
||||
if (this.sizeItemMode === "dwarf_drug") return EFFECT_REGISTRY?.modifier?.("dwarf_drug", "sizeMultiplier", 0.25) ?? 0.25;
|
||||
return 1;
|
||||
},
|
||||
|
||||
|
|
@ -90,6 +97,8 @@
|
|||
this.powerEffectKind = mode;
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 3.2);
|
||||
this.spawnPowerItemEffect(mode, 1.5);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true });
|
||||
recordItemChange(this, mode, "\u80fd\u529b\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -102,6 +111,8 @@
|
|||
this.powerEffectKind = mode;
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 4.0);
|
||||
this.spawnPowerItemEffect(mode, mode === "giant_drug" ? 1.9 : 1.2);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true });
|
||||
recordItemChange(this, mode, "\u4f53\u683c\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -117,6 +128,8 @@
|
|||
this.powerEffectKind = "mercury";
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.7);
|
||||
this.spawnPowerItemEffect("mercury", 0.95);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: "mercury", permanent: true, multiplier: this.mercuryLifeMultiplier });
|
||||
recordItemChange(this, "mercury", "\u5bff\u547d\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -140,6 +153,8 @@
|
|||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.2);
|
||||
this.refreshEffectiveSize();
|
||||
this.spawnPowerItemEffect("zunda_juice", 1.4);
|
||||
this.world?.emit?.("effect:removed", { tarinai: this, effectId: "all_removable", source: "zunda_juice" });
|
||||
recordItemChange(this, "zunda_juice", "\u52b9\u679c\u89e3\u9664");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -147,101 +162,61 @@
|
|||
this.ensureItemEffectState();
|
||||
const scale = sizeDurationScale(item);
|
||||
let applied = [];
|
||||
if (type === "laxative") {
|
||||
this.setTimedItemEffect("laxative", Math.round(60 * scale));
|
||||
this.spawnPowerItemEffect("laxative", 0.75);
|
||||
applied.push("laxative");
|
||||
} else if (type === "protein") {
|
||||
this.setPowerItemMode("protein");
|
||||
applied.push("protein");
|
||||
} else if (type === "niteropu") {
|
||||
this.setPowerItemMode("niteropu");
|
||||
applied.push("niteropu");
|
||||
} else if (type === "ammo") {
|
||||
this.setTimedItemEffect("ammo", Math.round(18 * scale));
|
||||
this.spawnPowerItemEffect("ammo", 1.1);
|
||||
applied.push("ammo");
|
||||
} else if (type === "mercury") {
|
||||
this.setMercuryEffect();
|
||||
applied.push("mercury");
|
||||
} else if (type === "giant_drug") {
|
||||
this.setSizeItemMode("giant_drug");
|
||||
applied.push("giant_drug");
|
||||
} else if (type === "dwarf_drug") {
|
||||
this.setSizeItemMode("dwarf_drug");
|
||||
applied.push("dwarf_drug");
|
||||
} else if (type === "zunda_juice") {
|
||||
this.clearAllItemEffects();
|
||||
applied.push("zunda_juice");
|
||||
} else if (type === "mystery_drug") {
|
||||
if (type === "mystery_drug") {
|
||||
const picks = pickRandomEffects(2);
|
||||
for (const effect of picks) this.applyRandomizedBehaviorEffect(effect, item);
|
||||
this.mysteryDrugHistory = [...(this.mysteryDrugHistory || []), `${picks.map(effectLabel).join(" + ")}`].slice(-8);
|
||||
this.bubble("???", 1.2, ITEM_EFFECT_COLORS.mystery_drug);
|
||||
this.spawnPowerItemEffect("mystery_drug", 1.0);
|
||||
applied = picks;
|
||||
} else if (EFFECT_REGISTRY?.apply?.(type, this, { item, scale, source: "item" })) {
|
||||
applied.push(type);
|
||||
if (type === "laxative") {
|
||||
this.spawnPowerItemEffect("laxative", 0.75);
|
||||
} else if (type === "ammo") {
|
||||
this.spawnPowerItemEffect("ammo", 1.1);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (!options.silentLog && this.world?.log) {
|
||||
const label = type === "mystery_drug" ? `怪しい薬(${applied.map(effectLabel).join(" + ")})` : effectLabel(type);
|
||||
this.world.log(`${this.name}に${label}の効果が出た。`, "food", { participants: [this] });
|
||||
const label = type === "mystery_drug" ? `\u602a\u3057\u3044\u85ac(${applied.map(effectLabel).join(" + ")})` : effectLabel(type);
|
||||
this.world.log(`${this.name}\u306b${label}\u306e\u52b9\u679c\u304c\u51fa\u305f\u3002`, "food", { participants: [this] });
|
||||
}
|
||||
this.recordChangeCause?.("\u30a2\u30a4\u30c6\u30e0\u6442\u53d6", applied.map(effectLabel).join(" + ") || effectLabel(type));
|
||||
return true;
|
||||
},
|
||||
|
||||
applyRandomizedBehaviorEffect(effect = "", item = null) {
|
||||
const scale = sizeDurationScale(item);
|
||||
if (effect === "love_mochi") {
|
||||
this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 * scale + rand(4, 12));
|
||||
this.stress = clamp(this.stress - 8, 0, 130);
|
||||
return true;
|
||||
}
|
||||
if (effect === "fight_mochi") {
|
||||
this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 52 * scale + rand(10, 18));
|
||||
this.stress = clamp(this.stress + 10, 0, 130);
|
||||
return true;
|
||||
}
|
||||
if (effect === "sleep_drug") return this.infectSleepDisease?.(item) || true;
|
||||
if (effect === "laxative") { this.setTimedItemEffect("laxative", Math.round(60 * scale)); return true; }
|
||||
if (effect === "ammo") { this.setTimedItemEffect("ammo", Math.round(18 * scale)); return true; }
|
||||
if (effect === "protein") return this.setPowerItemMode("protein");
|
||||
if (effect === "niteropu") return this.setPowerItemMode("niteropu");
|
||||
if (effect === "mercury") return this.setMercuryEffect();
|
||||
if (effect === "giant_drug") return this.setSizeItemMode("giant_drug");
|
||||
if (effect === "dwarf_drug") return this.setSizeItemMode("dwarf_drug");
|
||||
if (effect === "disease_zunchi") return this.infectZunchiDisease?.(item, true) || true;
|
||||
if (effect === "disease_sleep") return this.infectSleepDisease?.(item) || true;
|
||||
if (effect === "disease_explosion") return this.infectExplosionDisease?.(item) || true;
|
||||
if (effect === "disease_fight") return this.infectFightDisease?.(item) || true;
|
||||
return false;
|
||||
return Boolean(EFFECT_REGISTRY?.apply?.(effect, this, { item, scale, source: "mystery_drug" }));
|
||||
},
|
||||
|
||||
diseaseChance(base = 0) {
|
||||
const b = Math.max(0, Number(base) || 0);
|
||||
if (this.powerItemMode === "protein") return b * 0.5;
|
||||
if (this.powerItemMode === "niteropu") return b * 2.0;
|
||||
if (this.powerItemMode === "protein") return b * (EFFECT_REGISTRY?.modifier?.("protein", "diseaseChanceMultiplier", 0.5) ?? 0.5);
|
||||
if (this.powerItemMode === "niteropu") return b * (EFFECT_REGISTRY?.modifier?.("niteropu", "diseaseChanceMultiplier", 2.0) ?? 2.0);
|
||||
return b;
|
||||
},
|
||||
|
||||
outgoingDamage(amount = 0) {
|
||||
let value = Number(amount) || 0;
|
||||
if (this.powerItemMode === "protein") value *= 1.75;
|
||||
if (this.powerItemMode === "niteropu") value *= 0.5;
|
||||
if (this.sizeItemMode === "giant_drug") value *= 2.0;
|
||||
if (this.sizeItemMode === "dwarf_drug") value *= 0.25;
|
||||
let value = (Number(amount) || 0) * (this.geneticStatRatio ? this.geneticStatRatio("attack") : 1);
|
||||
if (this.powerItemMode === "protein") value *= EFFECT_REGISTRY?.modifier?.("protein", "damageMultiplier", 1.75) ?? 1.75;
|
||||
if (this.powerItemMode === "niteropu") value *= EFFECT_REGISTRY?.modifier?.("niteropu", "damageMultiplier", 0.5) ?? 0.5;
|
||||
if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "damageMultiplier", 2.0) ?? 2.0;
|
||||
if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "damageMultiplier", 0.25) ?? 0.25;
|
||||
return value;
|
||||
},
|
||||
|
||||
itemSpeedMultiplier() {
|
||||
let value = this.hasItemEffect("ammo") ? 10 : 1;
|
||||
if (this.sizeItemMode === "giant_drug") value *= 1.5;
|
||||
if (this.sizeItemMode === "dwarf_drug") value *= 0.5;
|
||||
let value = this.hasItemEffect("ammo") ? (EFFECT_REGISTRY?.modifier?.("ammo", "speedMultiplier", 10) ?? 10) : 1;
|
||||
if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "speedMultiplier", 1.5) ?? 1.5;
|
||||
if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "speedMultiplier", 0.5) ?? 0.5;
|
||||
return value;
|
||||
},
|
||||
|
||||
applyFoodHungerRelief(amount = 0) {
|
||||
if (this.hasItemEffect("laxative")) return 0;
|
||||
if (this.hasItemEffect("laxative")) return (Number(amount) || 0) * (EFFECT_REGISTRY?.modifier?.("laxative", "hungerReliefMultiplier", 0) ?? 0);
|
||||
this.hunger -= Number(amount) || 0;
|
||||
return amount;
|
||||
},
|
||||
|
|
@ -256,8 +231,8 @@
|
|||
if (this.hasItemEffect("laxative")) {
|
||||
this.laxativePoopTimer = Math.max(0, (this.laxativePoopTimer || 3) - step);
|
||||
while (this.laxativePoopTimer <= 0 && this.hasItemEffect("laxative")) {
|
||||
this.laxativePoopTimer += 3;
|
||||
this.forceLaxativePoop();
|
||||
this.laxativePoopTimer += EFFECT_REGISTRY?.get?.("laxative")?.tickInterval || 3;
|
||||
EFFECT_REGISTRY?.tick?.("laxative", this, step);
|
||||
}
|
||||
} else {
|
||||
this.laxativePoopTimer = Math.min(Math.max(this.laxativePoopTimer || 3, 0.2), 3);
|
||||
|
|
@ -266,8 +241,7 @@
|
|||
this.itemDamageTimer = Math.max(0, (this.itemDamageTimer || 1) - step);
|
||||
while (this.itemDamageTimer <= 0 && !this.dead && (this.sizeItemMode === "giant_drug" || this.sizeItemMode === "dwarf_drug")) {
|
||||
this.itemDamageTimer += 1;
|
||||
this.damage?.(1, this.sizeItemMode === "giant_drug" ? "巨大薬" : "矮小薬");
|
||||
if (this.world?.effects) this.spawnPowerItemEffect(this.sizeItemMode, 0.25);
|
||||
EFFECT_REGISTRY?.tick?.(this.sizeItemMode, this, step);
|
||||
}
|
||||
} else {
|
||||
this.itemDamageTimer = Math.min(Math.max(this.itemDamageTimer || 1, 0.08), 1);
|
||||
|
|
@ -289,10 +263,14 @@
|
|||
this.world.spawnZunchi?.(x, y);
|
||||
this.poopCount = (this.poopCount || 0) + 1;
|
||||
this.digest = Math.max(0, (this.digest || 0) - 3.0);
|
||||
this.bubble("ぶりゅっ", 1.2, ITEM_EFFECT_COLORS.laxative);
|
||||
const now = this.world?.time || 0;
|
||||
if (now >= (this.nextPoopBubbleAt || -999)) {
|
||||
this.nextPoopBubbleAt = now + 2.2;
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 1.2, ITEM_EFFECT_COLORS.laxative);
|
||||
}
|
||||
if ((this.world.time || 0) > (this.nextLaxativeLogAt || -999)) {
|
||||
this.nextLaxativeLogAt = (this.world.time || 0) + 9;
|
||||
this.world.log?.(`${this.name}は下剤の効果でずんちを放出した。`, "accident", { participants: [this] });
|
||||
this.world.log?.(`${this.name}\u306f\u4e0b\u5264\u306e\u52b9\u679c\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [this] });
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -338,6 +316,9 @@
|
|||
o.vy += py * 160;
|
||||
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.1);
|
||||
o.fallTimer = Math.max(o.fallTimer || 0, 0.72);
|
||||
const bumpDamage = Math.max(0.6, Math.min(3.0, speed / 260));
|
||||
o.damage?.(bumpDamage, "\u5f3e\u85ac\u885d\u7a81");
|
||||
this.adjustPersonality?.("aggression", 0.012, "after ammo knockback");
|
||||
this.world.spawnFallEffect?.(o.x, o.y + o.radius * 0.45, 1.05);
|
||||
audio.damage?.(12);
|
||||
this.ammoBumpCooldown = 0.12;
|
||||
|
|
@ -363,22 +344,8 @@
|
|||
|
||||
activeItemEffectSummary() {
|
||||
this.ensureItemEffectState();
|
||||
const rows = [];
|
||||
const add = (id, timer = 0) => {
|
||||
const label = effectLabel(id);
|
||||
const detail = EFFECT_REGISTRY?.display?.(id, timer, this) || "";
|
||||
rows.push({ id, label, detail });
|
||||
};
|
||||
if (this.powerItemMode === "protein") add("protein");
|
||||
if (this.powerItemMode === "niteropu") add("niteropu");
|
||||
if (this.lifeItemMode === "mercury") add("mercury");
|
||||
if (this.sizeItemMode === "giant_drug") add("giant_drug");
|
||||
if (this.sizeItemMode === "dwarf_drug") add("dwarf_drug");
|
||||
if ((this.itemEffectTimers.laxative || 0) > 0.04) add("laxative", this.itemEffectTimers.laxative);
|
||||
if ((this.itemEffectTimers.ammo || 0) > 0.04) add("ammo", this.itemEffectTimers.ammo);
|
||||
if ((this.loveMochiTimer || 0) > 0.1) add("love_mochi", this.loveMochiTimer);
|
||||
if ((this.fightMochiTimer || 0) > 0.1) add("fight_mochi", this.fightMochiTimer);
|
||||
return rows;
|
||||
if (EFFECT_REGISTRY?.activeSummary) return EFFECT_REGISTRY.activeSummary(this);
|
||||
return [];
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -16,22 +16,35 @@
|
|||
return (this.fightMochiTimer || 0) > 0.04;
|
||||
},
|
||||
|
||||
becomeZunchiSlave() {
|
||||
if (this.isZunchiSlave) return false;
|
||||
zunchiSlaveSpriteId() {
|
||||
if (!this.zunchiSlaveSpriteVariant) this.zunchiSlaveSpriteVariant = Math.random() < 0.5 ? "zunchi_slave" : "zunchi_slave_alt";
|
||||
return this.zunchiSlaveSpriteVariant;
|
||||
},
|
||||
|
||||
becomeZunchiSlave(options = {}) {
|
||||
if (this.isZunchiSlave) {
|
||||
this.zunchiSlaveLocked = true;
|
||||
applyZunchiSlavePersonality(this, { record: false, birth: !!options.birth });
|
||||
this.visibleSpriteId = this.zunchiSlaveSpriteId();
|
||||
return false;
|
||||
}
|
||||
this.formerName = this.formerName || this.name;
|
||||
this.isZunchiSlave = true;
|
||||
this.zunchiSlaveLocked = true;
|
||||
this.name = "\u305a\u3093\u3061\u3069\u308c\u3044";
|
||||
this.birthRitualTimer = 0;
|
||||
this.birthPartnerId = null;
|
||||
this.birthRitualLeader = false;
|
||||
this.birthRitualRole = 0;
|
||||
this.target = null;
|
||||
this.reproductionTimer = Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18));
|
||||
this.loveMochiTimer = 0;
|
||||
this.addStress(14, { threshold: 8 });
|
||||
this.reproductionTimer = options.birth ? Math.max(0, this.reproductionTimer || 0) : Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18));
|
||||
if (!options.birth) this.loveMochiTimer = 0;
|
||||
this.addStress(options.birth ? 0 : 14, { threshold: 8 });
|
||||
this.hunger = Math.max(this.hunger, 40);
|
||||
this.visibleSpriteId = "zunchi_slave";
|
||||
applyZunchiSlavePersonality(this, { birth: !!options.birth });
|
||||
this.visibleSpriteId = this.zunchiSlaveSpriteId();
|
||||
this.spriteLockUntil = 0;
|
||||
this.recordChangeCause?.("\u305a\u3093\u3061\u3069\u308c\u3044\u5316", "\u72b6\u614b");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -40,7 +53,7 @@
|
|||
if (this.sleepDisease) return this.variantSprite("sleep");
|
||||
if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan";
|
||||
if (this.fightDisease) return "stress_dizzy";
|
||||
if (this.isZunchiSlave) return "zunchi_slave";
|
||||
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
|
||||
if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry";
|
||||
if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat";
|
||||
if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat";
|
||||
|
|
@ -48,6 +61,7 @@
|
|||
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual";
|
||||
const low = this.lowHealthSprite();
|
||||
if (low === "stretch") return low;
|
||||
if (this.hunger >= 70 && !low) return "hungry_70";
|
||||
if (this.defeatedTimer > 0.04) return this.variantSprite("flee");
|
||||
if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate");
|
||||
if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear");
|
||||
|
|
@ -69,9 +83,12 @@
|
|||
spriteId() {
|
||||
const next = this.rawSpriteId();
|
||||
const now = this.world?.time || 0;
|
||||
if (!this.visibleSpriteId) {
|
||||
const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04;
|
||||
const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04;
|
||||
const forceImmediate = String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate");
|
||||
if (!this.visibleSpriteId || forceImmediate) {
|
||||
this.visibleSpriteId = next;
|
||||
this.spriteLockUntil = now + 1.0;
|
||||
this.spriteLockUntil = forceImmediate ? now : now + 1.0;
|
||||
return next;
|
||||
}
|
||||
if (next !== this.visibleSpriteId && now >= this.spriteLockUntil) {
|
||||
|
|
@ -220,12 +237,7 @@
|
|||
this.vy *= Math.pow(1.001, dt * 60);
|
||||
}
|
||||
|
||||
if (this.juvenile) {
|
||||
const adultScale = 0.25 + (makeTrait(this.type).speed % 0.08);
|
||||
this.scale = lerp(this.scale, adultScale, dt * 0.018 + 0.002);
|
||||
if (this.refreshEffectiveSize) this.refreshEffectiveSize();
|
||||
else this.radius = 56 * this.scale;
|
||||
}
|
||||
if (this.generation > 1 && this.syncGrowthScale) this.syncGrowthScale();
|
||||
|
||||
this.hunger = clamp(this.hunger, 0, 115);
|
||||
this.loneliness = clamp(this.loneliness, 0, 110);
|
||||
|
|
@ -363,6 +375,7 @@
|
|||
if (!this.isServingFoodItem(it) || (it.foodServingsRemaining || 0) <= 0) return 0;
|
||||
it.foodServingsRemaining = Math.max(0, (it.foodServingsRemaining || 0) - 1);
|
||||
it.amount = it.foodServingsRemaining;
|
||||
this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition });
|
||||
return nutrition;
|
||||
},
|
||||
|
||||
|
|
@ -470,6 +483,22 @@
|
|||
this.defeatedById = null;
|
||||
this.fightWinnerId = null;
|
||||
}
|
||||
|
||||
if (this.stuckPushpinId) {
|
||||
const pin = this.world.items?.find(it => it && !it.dead && it.id === this.stuckPushpinId && it.type === "pushpin" && it.pinState === "lodged") || null;
|
||||
if (pin) {
|
||||
this.state = "panic";
|
||||
this.sleeping = false;
|
||||
this.target = this.panicDestination(pin);
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.60);
|
||||
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.24);
|
||||
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 1.8);
|
||||
this.bubble("!!", 0.9, "rgba(168,72,72,0.82)");
|
||||
this.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
|
||||
return;
|
||||
}
|
||||
this.stuckPushpinId = null;
|
||||
}
|
||||
|
||||
const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null;
|
||||
if (friendNearby && friendNearby.state === "panic" && friendNearby.target) {
|
||||
|
|
@ -586,14 +615,15 @@
|
|||
}
|
||||
|
||||
if (!this.isZunchiSlave) {
|
||||
const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"], wasSleeping ? 190 : 155);
|
||||
const medicineTypes = window.TarinaiFoodRegistry?.typesByInterest?.("medicine_low") || ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"];
|
||||
const specialMochi = this.world.nearest(this, medicineTypes, wasSleeping ? 190 : 155);
|
||||
const specialDist = specialMochi ? distXY(this.x, this.y, specialMochi.x, specialMochi.y) : Infinity;
|
||||
const weakInterest = this.hunger > (wasSleeping ? 68 : 76) && (specialDist < 72 || Math.random() < dt * 0.028);
|
||||
if (specialMochi && this.foodItemHasServingLeft(specialMochi) && weakInterest && !this.shouldAvoidTarget(specialMochi)) {
|
||||
this.state = "seek_food";
|
||||
this.target = specialMochi;
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.18);
|
||||
this.thought = `${toolLabel(specialMochi.type)}が少し気になっている`;
|
||||
this.thought = `${toolLabel(specialMochi.type)}\u304c\u5c11\u3057\u6c17\u306b\u306a\u3063\u3066\u3044\u308b`;
|
||||
return;
|
||||
}
|
||||
} else if (this.hunger > 20) {
|
||||
|
|
@ -721,7 +751,7 @@
|
|||
}
|
||||
|
||||
if (this.hunger > 62) {
|
||||
const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", "grass", "ant_corpse"];
|
||||
const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])];
|
||||
const item = this.world.nearest(this, itemTypes, 420);
|
||||
if (item) {
|
||||
this.state = "seek_food";
|
||||
|
|
@ -764,7 +794,11 @@
|
|||
const backX = this.x - side * this.radius * rand(0.72, 0.96);
|
||||
const backY = this.y + this.radius * rand(0.25, 0.48);
|
||||
this.world.spawnZunchi(backX, backY);
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)");
|
||||
const now = this.world?.time || 0;
|
||||
if (now >= (this.nextPoopBubbleAt || -999)) {
|
||||
this.nextPoopBubbleAt = now + 2.8;
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)");
|
||||
}
|
||||
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
|
||||
},
|
||||
|
||||
|
|
@ -856,7 +890,7 @@
|
|||
this.makePoop(eating * 0.20);
|
||||
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
|
||||
if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) {
|
||||
this.world.log(`${this.name}は${toolLabel(it.type)}をしばらく味わった。`, "food", { participants: [this] });
|
||||
this.world.log(`${this.name}\u306f${toolLabel(it.type)}\u3092\u3057\u3070\u3089\u304f\u5473\u308f\u3063\u305f\u3002`, "food", { participants: [this] });
|
||||
this.lastLog = this.world.time;
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -284,16 +284,16 @@
|
|||
ctx.restore();
|
||||
}
|
||||
}
|
||||
if (sprite === "zunchi_slave") {
|
||||
if (String(sprite || "").startsWith("zunchi_slave")) {
|
||||
const zImg = typeof getRenderableImage === "function" ? getRenderableImage("zunchi", "zunchi") : (images && images.get ? images.get("zunchi") : null);
|
||||
if (zImg) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(46, 36, 24, 0.28)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = Math.max(1, drawH * 0.012);
|
||||
const zw = Math.max(12, drawW * 0.30);
|
||||
const zw = Math.max(16, drawW * 0.42);
|
||||
const zh = zw * 0.74;
|
||||
ctx.drawImage(zImg, -zw * 0.50, -dh * 0.48 - zh * 0.25, zw, zh);
|
||||
ctx.drawImage(zImg, -zw * 0.78, -dh * 0.50 - zh * 0.28, zw, zh);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
|
@ -342,15 +342,20 @@
|
|||
const drawMeter = (y, pct, fill, alpha) => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
const safePct = clamp(Number(pct) || 0, 0, 1);
|
||||
const fillW = safePct <= 0 ? 0 : Math.min(bw, Math.max(2, bw * safePct));
|
||||
ctx.fillStyle = "rgba(255,252,242,0.86)";
|
||||
roundedRect(ctx, x - 2, y - 2, bw + 4, bh + 4, 5);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(72,58,45,0.22)";
|
||||
roundedRect(ctx, x, y, bw, bh, 4);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = fill;
|
||||
roundedRect(ctx, x, y, bw * pct, bh, 4);
|
||||
ctx.fill();
|
||||
if (fillW > 0) {
|
||||
const fillR = Math.min(4, bh / 2, fillW / 2);
|
||||
ctx.fillStyle = fill;
|
||||
roundedRect(ctx, x, y, fillW, bh, fillR);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
if (stressActive) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,22 @@
|
|||
}
|
||||
|
||||
const pairBond = this.world.areCoParents?.(this, o);
|
||||
const mixedZunchiSlave = !!this.isZunchiSlave !== !!o.isZunchiSlave;
|
||||
if (mixedZunchiSlave && d < 86) {
|
||||
const nonSlave = this.isZunchiSlave ? o : this;
|
||||
const slave = this.isZunchiSlave ? this : o;
|
||||
const nx = (nonSlave.x - slave.x) / d;
|
||||
const ny = (nonSlave.y - slave.y) / d;
|
||||
nonSlave.vx += nx * dt * 34;
|
||||
nonSlave.vy += ny * dt * 34;
|
||||
nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear);
|
||||
nonSlave.target = slave;
|
||||
if (nonSlave.state === "idle" || nonSlave.state === "seek_friend") nonSlave.state = "panic";
|
||||
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
|
||||
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
|
||||
if (Math.random() < dt * 0.35) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
|
||||
continue;
|
||||
}
|
||||
const overlap = this.radius + o.radius - d;
|
||||
if (overlap > -2) {
|
||||
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
|
||||
|
|
@ -69,6 +85,8 @@
|
|||
o.stress = clamp(o.stress + 2.8, 0, 130);
|
||||
const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2;
|
||||
const damageToOther = this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6;
|
||||
this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" });
|
||||
this.world.emit?.("fight:hit", { attacker: this, target: o, amount: damageToOther, cause: "\u55a7\u5629" });
|
||||
this.damage(damageToThis, "\u55a7\u5629");
|
||||
o.damage(damageToOther, "\u55a7\u5629");
|
||||
this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis);
|
||||
|
|
@ -111,7 +129,8 @@
|
|||
this.world.startConflict(this, o);
|
||||
}
|
||||
|
||||
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 compatibleBirthStatus = this.isZunchiSlave === o.isZunchiSlave;
|
||||
if (compatibleBirthStatus && !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 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)) {
|
||||
|
|
@ -295,7 +314,7 @@
|
|||
this.vx *= Math.pow(0.84, dt * 8);
|
||||
this.vy *= Math.pow(0.84, dt * 8);
|
||||
|
||||
const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1);
|
||||
const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1);
|
||||
const v = Math.hypot(this.vx, this.vy);
|
||||
if (v > maxV) {
|
||||
this.vx = this.vx / v * maxV;
|
||||
|
|
@ -362,6 +381,7 @@
|
|||
this.releasedLiveToken = this.liveToken || 0;
|
||||
this.world.releaseTarinaiLiveId?.(this);
|
||||
audio.death();
|
||||
this.world.emit?.("tarinai:died", { tarinai: this, reason: finalReason });
|
||||
this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] });
|
||||
if (this.world.selected === this) this.world.selected = null;
|
||||
}
|
||||
|
|
|
|||
2
js/ui.js
|
|
@ -95,7 +95,7 @@ const uiCache = {
|
|||
grabLastWorldX: 0,
|
||||
grabLastWorldY: 0,
|
||||
grabLastSpatialAt: 0,
|
||||
mobileInputMode: localStorage.getItem("tarinai_mobile_input_mode_v1") || "auto",
|
||||
mobileInputMode: window.TarinaiInputMode?.currentMode || "auto",
|
||||
touchMode: "",
|
||||
touchMoved: false,
|
||||
touchStartX: 0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function bindUI() {
|
||||
renderToolPalette();
|
||||
loadLogPushSettings();
|
||||
syncLogPushControls();
|
||||
syncAudioControls();
|
||||
|
|
@ -16,7 +17,7 @@ function bindUI() {
|
|||
const collapsed = card.classList.toggle("collapsed");
|
||||
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
const state = toggle.querySelector(".panel-card-state");
|
||||
if (state) state.textContent = collapsed ? "展開" : "折り畳み";
|
||||
if (state) state.textContent = collapsed ? "\u5c55\u958b" : "\u6298\u308a\u7573\u307f";
|
||||
});
|
||||
}
|
||||
ui.colonyChartTabs?.addEventListener("click", (e) => {
|
||||
|
|
@ -33,7 +34,7 @@ function bindUI() {
|
|||
uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked);
|
||||
audio.notify?.();
|
||||
saveLogPushSettings();
|
||||
showToast(uiCache.logPushEnabled ? "ログのプッシュ通知をONにしました。" : "ログのプッシュ通知をOFFにしました。");
|
||||
showToast(uiCache.logPushEnabled ? "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002");
|
||||
});
|
||||
ui.logPushControls?.addEventListener("change", (e) => {
|
||||
const input = e.target.closest("[data-push-kind]");
|
||||
|
|
@ -170,7 +171,7 @@ function bindUI() {
|
|||
const on = audio.setEnabled ? audio.setEnabled(ui.soundMasterToggle.checked) : audio.toggle();
|
||||
if (on) audio.uiClick?.();
|
||||
syncAudioControls();
|
||||
showToast(on ? "効果音をONにしました。" : "効果音をOFFにしました。");
|
||||
showToast(on ? "\u52b9\u679c\u97f3\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u52b9\u679c\u97f3\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002");
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!ui.soundMenu || ui.soundMenu.contains(e.target)) return;
|
||||
|
|
@ -264,30 +265,22 @@ function bindUI() {
|
|||
chainScroll(ui.archiveContent);
|
||||
transferEdgeScroll(ui.log, rightPanel);
|
||||
|
||||
const isTouchDevice = () => matchMedia?.("(pointer: coarse)")?.matches || navigator.maxTouchPoints > 0;
|
||||
const inputModeManager = window.TarinaiInputMode;
|
||||
const isTouchDevice = () => Boolean(inputModeManager?.snapshot?.().isTouchDevice || matchMedia?.("(pointer: coarse)")?.matches || navigator.maxTouchPoints > 0);
|
||||
const setMobileInputMode = (mode = "auto") => {
|
||||
if (!["auto", "camera", "tool", "family"].includes(mode)) mode = "auto";
|
||||
mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto");
|
||||
uiCache.mobileInputMode = mode;
|
||||
try { localStorage.setItem("tarinai_mobile_input_mode_v1", mode); } catch (_) {}
|
||||
ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode));
|
||||
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
||||
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
||||
document.body.classList.toggle("mobile-mode-family", mode === "family");
|
||||
};
|
||||
const updateTouchMobileUiClass = () => {
|
||||
const ua = String(navigator.userAgent || "");
|
||||
const coarse = Boolean(window.matchMedia && window.matchMedia("(pointer: coarse)").matches);
|
||||
const noHover = Boolean(window.matchMedia && window.matchMedia("(hover: none)").matches);
|
||||
const touchPoints = Number(navigator.maxTouchPoints || 0);
|
||||
const w = window.innerWidth || 9999;
|
||||
const h = window.innerHeight || 9999;
|
||||
const mobileUa = /Android|iPhone|iPod|Mobile|Silk/i.test(ua);
|
||||
const compactPhoneViewport = Math.min(w, h) <= 640 && Math.max(w, h) <= 980;
|
||||
const enabled = Boolean(coarse && noHover && touchPoints > 0 && compactPhoneViewport && mobileUa);
|
||||
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
||||
document.body.classList.toggle("touch-mobile-ui", enabled);
|
||||
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
||||
};
|
||||
const mobileInputMode = () => uiCache.mobileInputMode || "auto";
|
||||
const mobileInputMode = () => inputModeManager?.currentMode || uiCache.mobileInputMode || "auto";
|
||||
updateTouchMobileUiClass();
|
||||
window.addEventListener("resize", updateTouchMobileUiClass, { passive: true });
|
||||
window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass);
|
||||
|
|
@ -297,7 +290,7 @@ function bindUI() {
|
|||
if (!btn) return;
|
||||
audio.uiClick?.();
|
||||
setMobileInputMode(btn.dataset.mobileMode || "auto");
|
||||
showToast(`スマホ操作: ${btn.textContent || btn.dataset.mobileMode}`);
|
||||
showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`);
|
||||
});
|
||||
|
||||
const touchPoint = (touches, index = 0) => {
|
||||
|
|
@ -355,6 +348,7 @@ function bindUI() {
|
|||
if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
} else {
|
||||
if (target.type === "pushpin" && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
||||
target.dropTimer = 0;
|
||||
target.dropMax = 0;
|
||||
target.dropImpactDone = true;
|
||||
|
|
@ -600,6 +594,7 @@ function bindUI() {
|
|||
if (target.state === "sleep" || target.state === "seek_bed") target.state = "idle";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18);
|
||||
} else {
|
||||
if (target.type === "pushpin" && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch");
|
||||
target.dropTimer = 0;
|
||||
target.dropMax = 0;
|
||||
target.dropImpactDone = true;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function renderStats() {
|
|||
hunger: avg(t => t.hunger),
|
||||
lonely: avg(t => t.loneliness),
|
||||
stress: avg(t => t.stress),
|
||||
lack: avg(t => t.lack),
|
||||
mood: avg(t => t.mood),
|
||||
security: environment.security,
|
||||
hygiene: environment.hygiene,
|
||||
happiness: environment.happiness,
|
||||
|
|
@ -28,7 +28,7 @@ function updateColonyHistory(values) {
|
|||
const t = Math.max(0, world.time || 0);
|
||||
const history = uiCache.chartHistory;
|
||||
const interval = Math.max(1, (CONFIG.dayLength || 120) / 12);
|
||||
const maxPoints = 7 * 12;
|
||||
const maxPoints = 5 * 12;
|
||||
if ((history.length && t < history[history.length - 1].t) || (Number.isFinite(uiCache.chartBucketStart) && t < uiCache.chartBucketStart)) {
|
||||
history.length = 0;
|
||||
uiCache.chartBucketStart = -Infinity;
|
||||
|
|
@ -42,7 +42,7 @@ function updateColonyHistory(values) {
|
|||
start,
|
||||
end: start + interval,
|
||||
count: 0,
|
||||
sums: { pop: 0, hunger: 0, lonely: 0, stress: 0, lack: 0, security: 0, hygiene: 0, happiness: 0 },
|
||||
sums: { pop: 0, hunger: 0, lonely: 0, stress: 0, mood: 0, security: 0, hygiene: 0, happiness: 0 },
|
||||
objects: Object.fromEntries(objectKeys.map(k => [k, 0])),
|
||||
});
|
||||
if (!uiCache.chartBucketAccum) {
|
||||
|
|
@ -59,7 +59,7 @@ function updateColonyHistory(values) {
|
|||
hunger: bucket.sums.hunger / bucket.count,
|
||||
lonely: bucket.sums.lonely / bucket.count,
|
||||
stress: bucket.sums.stress / bucket.count,
|
||||
lack: bucket.sums.lack / bucket.count,
|
||||
mood: bucket.sums.mood / bucket.count,
|
||||
security: bucket.sums.security / bucket.count,
|
||||
hygiene: bucket.sums.hygiene / bucket.count,
|
||||
happiness: bucket.sums.happiness / bucket.count,
|
||||
|
|
@ -81,7 +81,7 @@ function updateColonyHistory(values) {
|
|||
bucket.sums.hunger += values.hunger || 0;
|
||||
bucket.sums.lonely += values.lonely || 0;
|
||||
bucket.sums.stress += values.stress || 0;
|
||||
bucket.sums.lack += values.lack || 0;
|
||||
bucket.sums.mood += values.mood || 0;
|
||||
bucket.sums.security += values.security || 0;
|
||||
bucket.sums.hygiene += values.hygiene || 0;
|
||||
bucket.sums.happiness += values.happiness || 0;
|
||||
|
|
@ -97,7 +97,7 @@ function chartRows(values) {
|
|||
hunger: bucket.sums.hunger / bucket.count,
|
||||
lonely: bucket.sums.lonely / bucket.count,
|
||||
stress: bucket.sums.stress / bucket.count,
|
||||
lack: bucket.sums.lack / bucket.count,
|
||||
mood: bucket.sums.mood / bucket.count,
|
||||
security: bucket.sums.security / bucket.count,
|
||||
hygiene: bucket.sums.hygiene / bucket.count,
|
||||
happiness: bucket.sums.happiness / bucket.count,
|
||||
|
|
@ -177,7 +177,7 @@ function chartSeries(mode = "population") {
|
|||
{ key: "hunger", label: "\u7a7a\u8179", color: "#e4a33f", percent: true },
|
||||
{ key: "lonely", label: "\u5bc2\u3057\u3055", color: "#9b78d4", percent: true },
|
||||
{ key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true },
|
||||
{ key: "lack", label: "\u305f\u308a\u306a\u3044", color: "#5b9d61", percent: true },
|
||||
{ key: "mood", label: "\u7dcf\u5408\u7684\u6c17\u5206", color: "#0072b2", percent: true },
|
||||
];
|
||||
if (mode === "objects") return [
|
||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Split into ui_family_data.js, ui_family_async.js, ui_family_layout.js, ui_family_paths.js, ui_family_render.js.
|
||||
|
|
@ -175,7 +175,7 @@ function familyDeathReasonLabel(rawReason, entity = null) {
|
|||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9";
|
||||
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 (/\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";
|
||||
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return "\u305a\u3093\u3061\u75c5";
|
||||
if (/\u75c5/.test(text)) {
|
||||
const disease = text.match(/([^\u3001\u3002\s]+\u75c5)/);
|
||||
|
|
|
|||
|
|
@ -17,17 +17,26 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
||||
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
||||
const reason = briefReason ? ` / ${briefReason}` : "";
|
||||
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}`);
|
||||
const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []);
|
||||
const tags = rawTags.slice();
|
||||
const tagText = tags.length ? ` / ${tags.join(" ")}` : "";
|
||||
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}${tagText}`);
|
||||
const deathLine = briefReason ? `<span class="death-reason">${escapeHtml(briefReason)}</span>` : "";
|
||||
const tagLine = tags.length ? `<span class="family-personality-tags">${tags.map(tag => `<em>${escapeHtml(tag)}</em>`).join("")}</span>` : "";
|
||||
const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing";
|
||||
const childCls = (n?.parents || []).length ? " child" : "";
|
||||
const iconBaseScale = live
|
||||
? (live.effectiveScale ? live.effectiveScale() : (Number.isFinite(live.scale) ? live.scale : (Number.isFinite(live.adultScale) ? live.adultScale : 0.28)))
|
||||
: (Number.isFinite(n?.scale) ? n.scale : (Number.isFinite(n?.adultScale) ? n.adultScale : 0.28));
|
||||
const iconScale = clamp((Number(iconBaseScale) || 0.28) / 0.28, 0.64, 1.42).toFixed(2);
|
||||
const data = n?.id ? ` data-family-tarinai-id="${escapeHtml(n.id)}"` : "";
|
||||
return `<div class="family-node tree-node ${cls}${childCls}"${data} title="${title}" style="left:${Math.round(x)}px;top:${Math.round(y)}px">
|
||||
return `<div class="family-node tree-node ${cls}${childCls}"${data} title="${title}" style="left:${Math.round(x)}px;top:${Math.round(y)}px;--family-icon-scale:${iconScale}">
|
||||
<div class="family-portrait" aria-hidden="true"><img src="${img}" loading="lazy" decoding="async" alt=""></div>
|
||||
<div class="family-copy">
|
||||
<strong>${name}</strong>
|
||||
<span>${escapeHtml(gen)} / ${escapeHtml(status)}</span>
|
||||
${deathLine}
|
||||
${tagLine}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -343,7 +352,7 @@ function lineageBuildRowGroups(row, idSet) {
|
|||
|
||||
function lineageLayoutRows(rows, idSet) {
|
||||
const NODE_W = 132;
|
||||
const NODE_H = 68;
|
||||
const NODE_H = 84;
|
||||
const SIBLING_GAP = 32;
|
||||
const GROUP_GAP = 52;
|
||||
const PARTNER_GROUP_GAP = 24;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ function lineageComponentSignature(component, family) {
|
|||
n.type || "",
|
||||
n.generation || 1,
|
||||
n.birthTime || 0,
|
||||
Number(n.scale || 0).toFixed(3),
|
||||
Number(n.adultScale || 0).toFixed(3),
|
||||
Number(n.growth || 0).toFixed(2),
|
||||
n.hasPaired ? 1 : 0,
|
||||
n.alive === false ? 0 : 1,
|
||||
n.deathReason || "",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ function lineageFastLayoutNeeded(nodes, idSet, family) {
|
|||
|
||||
function lineageLayoutRowsFast(rows, idSet) {
|
||||
const NODE_W = 132;
|
||||
const NODE_H = 68;
|
||||
const NODE_H = 84;
|
||||
const SIBLING_GAP = 22;
|
||||
const ROW_GAP = 116;
|
||||
const LEFT_PAD = 82;
|
||||
|
|
|
|||
|
|
@ -60,12 +60,7 @@ function applyToolTips() {
|
|||
const showTip = (btn) => {
|
||||
const baseTip = btn?.dataset?.tip || "";
|
||||
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`;
|
||||
}
|
||||
const tip = baseTip;
|
||||
pop.textContent = tip;
|
||||
pop.classList.remove("hidden");
|
||||
const rect = btn.getBoundingClientRect();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function syncAudioControls() {
|
|||
}
|
||||
if (ui.soundMasterToggle) ui.soundMasterToggle.checked = Boolean(audio.enabled);
|
||||
if (ui.soundBtn) {
|
||||
ui.soundBtn.textContent = audio.enabled ? "音設定 ON" : "音設定 OFF";
|
||||
ui.soundBtn.textContent = audio.enabled ? "\u97f3\u8a2d\u5b9a ON" : "\u97f3\u8a2d\u5b9a OFF";
|
||||
ui.soundBtn.classList.toggle("active", Boolean(audio.enabled));
|
||||
ui.soundBtn.setAttribute("aria-expanded", ui.soundPanel && !ui.soundPanel.classList.contains("hidden") ? "true" : "false");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,97 @@ function personalityRowsHtml(personality = {}) {
|
|||
return PERSONALITY_KEYS.map(key => `<span>${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}</span>`).join("");
|
||||
}
|
||||
|
||||
function personalityShortLabel(key) {
|
||||
return ({
|
||||
aggression: "\u653b\u6483",
|
||||
openness: "\u958b\u653e",
|
||||
sociability: "\u793e\u4ea4",
|
||||
neuroticism: "\u795e\u7d4c",
|
||||
})[key] || (PERSONALITY_LABELS[key]?.label || key);
|
||||
}
|
||||
|
||||
|
||||
function personalityDisplayCauseText(cause = "") {
|
||||
return String(cause || "").replace(/(?:\u306e)?\u5f71\u97ff\u3067$/g, "").trim();
|
||||
}
|
||||
|
||||
function geneticPercentText(t, key = "") {
|
||||
const ratio = t?.geneticStatRatio ? t.geneticStatRatio(key) : 1;
|
||||
const pct = Math.round((ratio - 1) * 100);
|
||||
return `${pct >= 0 ? "+" : ""}${pct}%`;
|
||||
}
|
||||
|
||||
function geneticSizeText(t) {
|
||||
const adult = Number.isFinite(t?.adultScale) ? t.adultScale : (t?.scale || 0.28);
|
||||
const now = Number.isFinite(t?.scale) ? t.scale : adult;
|
||||
const growth = Math.round((t?.growth ?? 1) * 100);
|
||||
return `${geneticPercentText(t, "size")} / ${now.toFixed(2)}\u2192${adult.toFixed(2)} (${growth}%)`;
|
||||
}
|
||||
|
||||
function personalityDailyChangeHtml(t) {
|
||||
const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || { day: world?.day || 1, byKey: {}, byCause: {} });
|
||||
const dayLabel = `D${state.day || world?.day || 1}`;
|
||||
const causeMap = new Map();
|
||||
const addCauseChange = (cause, key, up, down) => {
|
||||
const cleanCause = personalityDisplayCauseText(cause || "") || "\u8981\u56e0\u4e0d\u660e";
|
||||
if (!causeMap.has(cleanCause)) causeMap.set(cleanCause, new Map());
|
||||
const byKey = causeMap.get(cleanCause);
|
||||
const current = byKey.get(key) || { up: 0, down: 0 };
|
||||
current.up += Math.max(0, Number(up) || 0);
|
||||
current.down += Math.max(0, Number(down) || 0);
|
||||
byKey.set(key, current);
|
||||
};
|
||||
for (const [cause, entry] of Object.entries(state.byCause || {})) {
|
||||
for (const key of PERSONALITY_KEYS) {
|
||||
const v = entry?.byKey?.[key] || {};
|
||||
const up = Math.max(0, Number(v.up) || 0);
|
||||
const down = Math.max(0, Number(v.down) || 0);
|
||||
if (up > 0.0001 || down > 0.0001) addCauseChange(cause, key, up, down);
|
||||
}
|
||||
}
|
||||
if (!causeMap.size) {
|
||||
for (const key of PERSONALITY_KEYS) {
|
||||
const entry = state.byKey?.[key] || {};
|
||||
const up = Math.max(0, Number(entry.up) || 0);
|
||||
const down = Math.max(0, Number(entry.down) || 0);
|
||||
if (up <= 0.0001 && down <= 0.0001) continue;
|
||||
const causes = Array.isArray(entry.causes) ? [...new Set(entry.causes.map(c => personalityDisplayCauseText(c)).filter(Boolean))].slice(0, 4) : [];
|
||||
for (const cause of causes.length ? causes : ["\u8981\u56e0\u4e0d\u660e"]) addCauseChange(cause, key, up, down);
|
||||
}
|
||||
}
|
||||
const rows = [];
|
||||
for (const [cause, byKey] of causeMap.entries()) {
|
||||
const parts = [];
|
||||
for (const key of PERSONALITY_KEYS) {
|
||||
const entry = byKey.get(key);
|
||||
if (!entry) continue;
|
||||
const up = Math.max(0, Number(entry.up) || 0);
|
||||
const down = Math.max(0, Number(entry.down) || 0);
|
||||
if (up > 0.0001) parts.push(`${personalityShortLabel(key)} +${up.toFixed(3)}`);
|
||||
if (down > 0.0001) parts.push(`${personalityShortLabel(key)} -${down.toFixed(3)}`);
|
||||
}
|
||||
if (parts.length) rows.push(`<li><span>${escapeHtml(cause)}</span><strong>${escapeHtml(parts.join(" / "))}</strong><em>${escapeHtml(dayLabel)}</em></li>`);
|
||||
}
|
||||
return rows.join("") || `<li><span>\u8981\u56e0</span><strong>\u5909\u5316\u306a\u3057</strong><em>${escapeHtml(dayLabel)}</em></li>`;
|
||||
}
|
||||
|
||||
function personalityDailySnapshot(t) {
|
||||
const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || {});
|
||||
const keyPart = PERSONALITY_KEYS.map(key => {
|
||||
const entry = state.byKey?.[key] || {};
|
||||
const causes = Array.isArray(entry.causes) ? entry.causes.join("/") : "";
|
||||
return `${key}:${Number(entry.up || 0).toFixed(4)}:${Number(entry.down || 0).toFixed(4)}:${causes}`;
|
||||
}).join(",");
|
||||
const causePart = Object.entries(state.byCause || {}).map(([cause, entry]) => {
|
||||
const values = PERSONALITY_KEYS.map(key => {
|
||||
const v = entry?.byKey?.[key] || {};
|
||||
return `${key}:${Number(v.up || 0).toFixed(4)}:${Number(v.down || 0).toFixed(4)}`;
|
||||
}).join(";");
|
||||
return `${cause}=${values}`;
|
||||
}).join("|");
|
||||
return `${keyPart}#${causePart}`;
|
||||
}
|
||||
|
||||
function selectedDataCategoryHtml(id, label, bodyHtml) {
|
||||
if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set();
|
||||
const collapsed = uiCache.selectedCollapsedCategories.has(id);
|
||||
|
|
@ -58,6 +149,11 @@ function selectedDataCategoryHtml(id, label, bodyHtml) {
|
|||
</section>`;
|
||||
}
|
||||
|
||||
function selectedInfoRowHtml(label, valueHtml, metric = "") {
|
||||
const metricAttr = metric ? ` data-metric="${escapeHtml(metric)}"` : "";
|
||||
return `<div class="info-row"${metricAttr}><span>${escapeHtml(label)}</span><strong>${valueHtml}</strong></div>`;
|
||||
}
|
||||
|
||||
function renderSelected() {
|
||||
const t = world.selected;
|
||||
if (!t || t.dead || !world.tarinai.includes(t)) {
|
||||
|
|
@ -75,7 +171,6 @@ function renderSelected() {
|
|||
const activeNameInput = document.activeElement?.matches?.("[data-selected-name]") && document.activeElement?.dataset?.selectedId === t.id;
|
||||
if (activeNameInput) return;
|
||||
const rel = t.relationSummary ? t.relationSummary() : {};
|
||||
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);
|
||||
|
|
@ -90,11 +185,12 @@ function renderSelected() {
|
|||
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
|
||||
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
||||
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
||||
relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), 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(","), fightWinRateLabel(t),
|
||||
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.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
|
||||
personalitySnapshot,
|
||||
personalitySnapshot, personalityDailySnapshot(t),
|
||||
["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100),
|
||||
itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","),
|
||||
(t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~")
|
||||
].join("|");
|
||||
|
|
@ -114,10 +210,12 @@ function renderSelected() {
|
|||
const personalityTagHtml = personalityTags.length
|
||||
? personalityTags.map(tag => `<span class="personality-tag">${escapeHtml(tag)}</span>`).join("")
|
||||
: `<span class="personality-tag neutral">\u4e2d\u7acb</span>`;
|
||||
const recentChangeHtml = personalityDailyChangeHtml(t);
|
||||
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>
|
||||
<div class="info-row selected-personality-changes"><span>\u4eca\u65e5\u306e\u5909\u5316</span><strong><ol class="selected-change-list">${recentChangeHtml}</ol></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);
|
||||
|
|
@ -130,24 +228,26 @@ function renderSelected() {
|
|||
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(reasonLabel(t))}</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>\u6700\u5927\u5bff\u547d</span><strong>${geneticPercentText(t, "life")}</strong></div>
|
||||
<div class="info-row"><span>\u653b\u6483\u529b</span><strong>${geneticPercentText(t, "attack")}</strong></div>
|
||||
<div class="info-row"><span>\u79fb\u52d5\u901f\u5ea6</span><strong>${geneticPercentText(t, "speed")}</strong></div>
|
||||
<div class="info-row"><span>\u30b5\u30a4\u30ba</span><strong>${geneticSizeText(t)}</strong></div>
|
||||
`;
|
||||
const healthHtml = `
|
||||
<div class="info-row"><span>\u7a7a\u8179</span><strong>${fmt(t.hunger)}</strong></div>
|
||||
<div class="info-row"><span>\u5bc2\u3057\u3055</span><strong>${fmt(t.loneliness)}</strong></div>
|
||||
<div class="info-row"><span>\u4f53\u529b</span><strong>${fmt(t.energy)}</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>\u6c17\u5206</span><strong>${fmt(t.mood)}</strong></div>
|
||||
<div class="info-row"><span>\u305f\u308a\u306a\u3044</span><strong>${fmt(t.lack)}</strong></div>
|
||||
`;
|
||||
const healthHtml = [
|
||||
selectedInfoRowHtml("\u7dcf\u5408\u7684\u6c17\u5206", fmt(t.mood), "mood"),
|
||||
selectedInfoRowHtml("\u7a7a\u8179", fmt(t.hunger), "hunger"),
|
||||
selectedInfoRowHtml("\u5bc2\u3057\u3055", fmt(t.loneliness), "loneliness"),
|
||||
selectedInfoRowHtml("\u4f53\u529b", fmt(t.energy), "energy"),
|
||||
selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"),
|
||||
selectedInfoRowHtml("\u305a\u3093\u3061\u6c5a\u308c", fmt(t.zunchiStain || 0), "zunchi"),
|
||||
].join("");
|
||||
const itemEffectHtml = itemEffectSummary.length
|
||||
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
|
||||
: `<div class="info-row"><span>アイテム効果</span><strong>なし</strong></div>`;
|
||||
: `<div class="info-row"><span>\u30a2\u30a4\u30c6\u30e0\u52b9\u679c</span><strong>\u306a\u3057</strong></div>`;
|
||||
const relationHtml = `
|
||||
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
||||
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
||||
<div class="info-row"><span>\u55a7\u5629\u8a18\u61b6</span><strong>${escapeHtml(fightRecord)}</strong></div>
|
||||
<div class="info-row"><span>けんか勝率</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
<div class="info-row"><span>\u3051\u3093\u304b\u52dd\u7387</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
||||
`;
|
||||
const familyHtml = `
|
||||
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
||||
|
|
@ -224,21 +324,12 @@ function targetLabel(target) {
|
|||
return "\u4e0d\u660e";
|
||||
}
|
||||
|
||||
function relationFightRecord(t) {
|
||||
let won = 0, lost = 0;
|
||||
for (const rel of Object.values(t.relationships || {})) {
|
||||
won += rel.fightsWon || 0;
|
||||
lost += rel.fightsLost || 0;
|
||||
}
|
||||
return won || lost ? `\u52dd${won} / \u8ca0${lost}` : "\u306a\u3057";
|
||||
}
|
||||
|
||||
function fightWinRateLabel(t) {
|
||||
const won = Number(t.totalFightWins || 0);
|
||||
const lost = Number(t.totalFightLosses || 0);
|
||||
const total = won + lost;
|
||||
if (!total) return "なし";
|
||||
return `${Math.round((won / total) * 100)}%(${won}勝${lost}敗)`;
|
||||
if (!total) return "\u306a\u3057";
|
||||
return `${Math.round((won / total) * 100)}% (${won}\u52dd${lost}\u6557)`;
|
||||
}
|
||||
|
||||
function setTextIfChanged(el, key, value) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,37 @@ const SIZE_ORDER = ["small", "medium", "large"];
|
|||
|
||||
const SIZE_LABELS = { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||
|
||||
function renderToolPalette() {
|
||||
if (!ui?.toolPalette || typeof toolCategories !== "function") return;
|
||||
ui.toolPalette.innerHTML = "";
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const category of toolCategories()) {
|
||||
const section = document.createElement("section");
|
||||
section.className = `tool-category ${category.themeClass || ""}`.trim();
|
||||
section.dataset.toolCategory = category.id;
|
||||
const toggle = document.createElement("button");
|
||||
toggle.className = "tool-category-toggle";
|
||||
toggle.type = "button";
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
toggle.textContent = category.label || category.id;
|
||||
const body = document.createElement("div");
|
||||
body.className = "tool-grid tool-category-body";
|
||||
for (const toolId of category.toolIds || []) {
|
||||
const def = toolDefinition(toolId);
|
||||
if (!def || def.simulationOnly) continue;
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "tool";
|
||||
btn.dataset.tool = toolId;
|
||||
btn.textContent = def.label || toolId;
|
||||
if (toolId === world?.tool) btn.classList.add("selected");
|
||||
body.appendChild(btn);
|
||||
}
|
||||
section.append(toggle, body);
|
||||
frag.appendChild(section);
|
||||
}
|
||||
ui.toolPalette.appendChild(frag);
|
||||
}
|
||||
|
||||
function toolSizeFor(tool) {
|
||||
if (!SCALABLE_TOOLS.has(tool)) return "";
|
||||
return (world.toolSizes && world.toolSizes[tool]) || world.toolSize || "medium";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "15.20.3";
|
||||
const APP_BUILD = "systems-split";
|
||||
const APP_VERSION = "15.20.16";
|
||||
const APP_BUILD = "registry-ui";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
@ -21,3 +21,4 @@
|
|||
window.TARINAI_VERSION = APP_VERSION;
|
||||
window.TARINAI_APP = config;
|
||||
})();
|
||||
|
||||
|
|
|
|||
49
js/weather_system.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const WEATHER_STATES = Object.freeze(["sunny", "cloudy", "light_rain"]);
|
||||
|
||||
const WeatherSystem = Object.freeze({
|
||||
states: WEATHER_STATES,
|
||||
|
||||
chooseNext(current = "sunny") {
|
||||
const choices = current === "light_rain"
|
||||
? ["sunny", "cloudy"]
|
||||
: ["sunny", "cloudy", "light_rain"];
|
||||
return pick(choices);
|
||||
},
|
||||
|
||||
resetTimer() {
|
||||
return rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
||||
},
|
||||
|
||||
shouldDropRainWater(worldRef, dt = 0) {
|
||||
return Boolean(
|
||||
worldRef?.weather === "light_rain" &&
|
||||
(worldRef.itemCounts?.water || 0) < 24 &&
|
||||
Math.random() < Math.max(0, Number(dt) || 0) * 0.12
|
||||
);
|
||||
},
|
||||
|
||||
createRainWaterItem(worldRef) {
|
||||
const drop = new Item("water", rand(58, worldRef.w - 58), rand(58, worldRef.h - 58));
|
||||
drop.amount = rand(24, 58);
|
||||
return drop;
|
||||
},
|
||||
|
||||
applyNextWeather(worldRef) {
|
||||
if (!worldRef) return null;
|
||||
const previous = worldRef.weather || "sunny";
|
||||
const next = this.chooseNext(previous);
|
||||
worldRef.weather = next;
|
||||
worldRef.nextWeatherChange = this.resetTimer();
|
||||
if (next !== previous) {
|
||||
worldRef.emit?.("weather:changed", { previous, next });
|
||||
worldRef.log?.(`\u5929\u6c17: ${weatherLabel(next)}`);
|
||||
}
|
||||
return next;
|
||||
},
|
||||
});
|
||||
|
||||
global.TarinaiWeatherSystem = WeatherSystem;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -68,11 +68,6 @@ class World { constructor() {
|
|||
this.effectCounts = {};
|
||||
this.foodSpoilagePenalty = 0;
|
||||
this.foodSpoilageEvents = 0;
|
||||
this.countsTimer = 0;
|
||||
this.compactTimer = 0;
|
||||
this.drawSortTimer = 0;
|
||||
this.drawListDirty = true;
|
||||
this.antUpdatePhase = 0;
|
||||
this.updateItemCounts();
|
||||
this.updateEffectCounts();
|
||||
this.markTerrainDirty?.("reset");
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
maxHp: ANT_WORKER_HP || 32,
|
||||
});
|
||||
this.ants.push(a);
|
||||
this.emit?.("ant:spawned", { ant: a, nest });
|
||||
if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest);
|
||||
nest.antSpawnTimer = rand(1.6, 3.8) + outside * 0.45;
|
||||
this.drawListDirty = true;
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
target.y = home.y;
|
||||
audio.antNest?.();
|
||||
target.die("\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f");
|
||||
this.emit?.("ant:carried", { nest: home, target, complete: true });
|
||||
this.drawListDirty = true;
|
||||
},
|
||||
|
||||
|
|
@ -88,8 +90,8 @@
|
|||
foundingDelayUntil: (this.time || 0) + 0.35,
|
||||
});
|
||||
this.ants.push(queen);
|
||||
this.emit?.("ant:spawned", { ant: queen, nest: home });
|
||||
audio.queenAnt?.();
|
||||
this.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u5de3\u5834\u6240\u3092\u63a2\u3057\u59cb\u3081\u305f\u3002", "event");
|
||||
this.drawListDirty = true;
|
||||
return queen;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -133,18 +133,33 @@
|
|||
blastedBalls += 1;
|
||||
}
|
||||
audio.explode();
|
||||
this.log(blastedBalls ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u30dc\u30fc\u30eb\u304c\u6025\u52a0\u901f\u3057\u305f\u3002" : "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u5ead\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident");
|
||||
this.log(blastedBalls ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u30dc\u30fc\u30eb\u304c\u6025\u52a0\u901f\u3057\u305f\u3002" : "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u8fba\u308a\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident");
|
||||
},
|
||||
|
||||
explodeDiseaseTarinai(t) {
|
||||
if (!t || t.dead) return;
|
||||
const x = t.x, y = t.y;
|
||||
const blastRadius = 190;
|
||||
const blastScale = 0.68;
|
||||
const blastRadius = 240 * blastScale;
|
||||
t.explosionDisease = false;
|
||||
t.explosionDiseaseTimer = 0;
|
||||
this.effects.push(new Effect("explosion", x, y, { size: 72, life: 0.82, color: "rgba(255,176,68,0.88)" }));
|
||||
this.effects.push(new Effect("ring", x, y, { size: 58, life: 0.52, color: "rgba(255,238,132,0.82)" }));
|
||||
this.blastZunchiFrom(x, y, blastRadius * 0.92, 0.82);
|
||||
this.effects.push(new Effect("explosion", x, y, { size: 86 * blastScale, life: 1.05, color: "rgba(255,182,66,0.92)" }));
|
||||
this.effects.push(new Effect("explosion", x, y, { size: 128 * blastScale, life: 0.82, color: "rgba(255,92,42,0.70)" }));
|
||||
for (let r = 0; r < 4; r++) {
|
||||
this.effects.push(new Effect("ring", x, y, { size: (30 + r * 24) * blastScale, life: 0.48 + r * 0.11, color: r % 2 ? "rgba(255,116,62,0.78)" : "rgba(255,240,124,0.88)" }));
|
||||
}
|
||||
this.blastZunchiFrom(x, y, blastRadius * 0.92, blastScale);
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const a = Math.PI * 2 * i / 34 + rand(-0.10, 0.10);
|
||||
const speed = rand(90, 260) * blastScale;
|
||||
this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * rand(2, 22) * blastScale, y + Math.sin(a) * rand(2, 22) * blastScale, {
|
||||
vx: Math.cos(a) * speed,
|
||||
vy: Math.sin(a) * speed,
|
||||
size: rand(8, i % 3 === 0 ? 24 : 20) * blastScale,
|
||||
life: rand(0.26, 0.72),
|
||||
color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)",
|
||||
}));
|
||||
}
|
||||
for (const o of this.tarinai) {
|
||||
if (!o || o.dead || o === t) continue;
|
||||
const dx = o.x - x;
|
||||
|
|
@ -152,8 +167,8 @@
|
|||
const d = Math.hypot(dx, dy) || 1;
|
||||
if (d > blastRadius) continue;
|
||||
const q = clamp(1 - d / blastRadius, 0, 1);
|
||||
o.damage(4 + q * 15, "\u7206\u767a\u75c5");
|
||||
o.addStress ? o.addStress(16 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 16 * q, 0, 130));
|
||||
o.damage(5 + q * 18, "\u7206\u767a\u75c5");
|
||||
o.addStress ? o.addStress(22 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 22 * q, 0, 130));
|
||||
let nx = dx / d;
|
||||
let ny = dy / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
|
|
@ -161,15 +176,47 @@
|
|||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
const diseaseBlast = (190 + q * 620) * rand(0.80, 1.22);
|
||||
this.applyImpulse(o, nx * diseaseBlast + rand(-64, 64) * (0.5 + q), ny * diseaseBlast + rand(-64, 64) * (0.5 + q), {
|
||||
const distanceBoost = q * q * 0.55 + q * 0.45;
|
||||
const power = (260 + distanceBoost * 920) * rand(0.78, 1.24) * blastScale;
|
||||
this.applyImpulse(o, nx * power + rand(-95, 95) * (0.6 + q), ny * power + rand(-95, 95) * (0.6 + q), {
|
||||
panic: true,
|
||||
target: { x, y },
|
||||
fearTimer: 1.0 + q,
|
||||
fearTimer: 1.7 + q,
|
||||
});
|
||||
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.2 + q);
|
||||
o.fearTimer = Math.max(o.fearTimer || 0, 1.0 + q);
|
||||
if (!o.dead && q > 0.18 && Math.random() < (o.diseaseChance ? o.diseaseChance(0.15) : 0.15)) o.infectExplosionDisease?.(t);
|
||||
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.5 + q * 1.8);
|
||||
o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.85);
|
||||
o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q);
|
||||
o.state = "panic";
|
||||
o.target = { x, y };
|
||||
o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85);
|
||||
o.fallMax = Math.max(o.fallMax || 0, o.fallTimer);
|
||||
o.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
||||
o.blastSpinTimer = Math.max(o.blastSpinTimer || 0, 1.35 + q * 0.70);
|
||||
o.blastSpinMax = Math.max(o.blastSpinMax || 0, o.blastSpinTimer);
|
||||
this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q);
|
||||
if (!o.dead && q > 0.22 && Math.random() < (o.diseaseChance ? o.diseaseChance(0.15) : 0.15)) o.infectExplosionDisease?.(t);
|
||||
if (!o.dead && Math.random() < 0.45) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
}
|
||||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", {
|
||||
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale,
|
||||
});
|
||||
for (const ball of this.items) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
let dx = ball.x - x;
|
||||
let dy = ball.y - y;
|
||||
let d = Math.hypot(dx, dy) || 1;
|
||||
if (d > blastRadius * 1.18) continue;
|
||||
const q = clamp(1 - d / (blastRadius * 1.18), 0, 1);
|
||||
if (d < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||
}
|
||||
const blast = 430 + q * 960;
|
||||
this.applyImpulse(ball, dx / d * blast * blastScale + rand(-80, 80), dy / d * blast * blastScale + rand(-80, 80));
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38);
|
||||
ball.lastPokedAt = this.time || 0;
|
||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" }));
|
||||
}
|
||||
t.die("\u7206\u767a\u75c5");
|
||||
audio.explode();
|
||||
|
|
@ -235,7 +282,6 @@
|
|||
this.items.push(it);
|
||||
this.itemCounts.zunchi = (this.itemCounts.zunchi || 0) + 1;
|
||||
audio.place?.("zunchi");
|
||||
this.spawnBubble(it.x, it.y - 16, "\u3076\u308a\u3085\u3063", "rgba(62,84,45,0.78)");
|
||||
},
|
||||
|
||||
spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") {
|
||||
|
|
@ -400,6 +446,7 @@
|
|||
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";
|
||||
this.emit?.(String(normalized).includes("\u55a7\u5629") ? "fight:hit" : "impact:hit", { target, amount, cause: normalized, source: options.source || null });
|
||||
if (target.damage) target.damage(amount, normalized);
|
||||
else if (Number.isFinite(target.hp)) target.hp -= amount;
|
||||
return true;
|
||||
|
|
@ -422,6 +469,7 @@
|
|||
attacker.defeatedById = null;
|
||||
victim.fightTimer = Math.min(victim.fightTimer || 0, rand(0.20, 0.42));
|
||||
attacker.fightTimer = Math.min(attacker.fightTimer || 0, rand(0.20, 0.42));
|
||||
this.emit?.("fight:lost", { winner: attacker, loser: victim, damage });
|
||||
victim.state = "panic";
|
||||
victim.target = attacker;
|
||||
victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1));
|
||||
|
|
@ -430,7 +478,7 @@
|
|||
const d = Math.hypot(dx, dy) || 1;
|
||||
victim.vx += dx / d * rand(38, 76);
|
||||
victim.vy += dy / d * rand(22, 56);
|
||||
this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "はぁぁぁ!", "rgba(84,66,86,0.80)");
|
||||
this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -451,13 +499,11 @@
|
|||
loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1;
|
||||
winner.totalFightWins = (winner.totalFightWins || 0) + 1;
|
||||
loser.totalFightLosses = (loser.totalFightLosses || 0) + 1;
|
||||
this.emit?.("fight:ended", { winner, loser });
|
||||
winner.adjustPersonality?.("aggression", 0.018, "after winning fights.");
|
||||
loser.adjustPersonality?.("aggression", -0.018, "after losing fights.");
|
||||
if ((loser.totalFightLosses || 0) >= 5 && loser.becomeZunchiSlave?.(winner)) {
|
||||
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.log(`${oldName}\u306f5\u56de\u55a7\u5629\u306b\u8ca0\u3051\u3001\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: [loser] });
|
||||
}
|
||||
this.maybeApplyZunchiSlaveByFightRecord?.(winner, loser);
|
||||
this.maybeApplyZunchiSlaveByFightRecord?.(loser, winner);
|
||||
loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear);
|
||||
this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
winner.affection = clamp(winner.affection + 0.8, 0, 80);
|
||||
|
|
@ -467,6 +513,21 @@
|
|||
}
|
||||
},
|
||||
|
||||
maybeApplyZunchiSlaveByFightRecord(tarinai, other = null) {
|
||||
if (!tarinai || tarinai.dead || tarinai.isZunchiSlave) return false;
|
||||
const wins = Number(tarinai.totalFightWins || 0);
|
||||
const losses = Number(tarinai.totalFightLosses || 0);
|
||||
const total = wins + losses;
|
||||
if (total < 5) return false;
|
||||
const winRate = total > 0 ? wins / total : 0;
|
||||
if (winRate > 0.5) return false;
|
||||
const oldName = tarinai.formerName || tarinai.name;
|
||||
if (!tarinai.becomeZunchiSlave?.({ fightRecord: true, locked: true })) return false;
|
||||
this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)");
|
||||
this.log?.(`${oldName}\u306f\u55a7\u5629\u30925\u56de\u7d4c\u9a13\u3057\u3001\u52dd\u738750%\u4ee5\u4e0b\u3067\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] });
|
||||
return true;
|
||||
},
|
||||
|
||||
itemDropImpact(it) {
|
||||
if (!it || it.dead || it.type === "water") return;
|
||||
const isStone = it.type === "stone";
|
||||
|
|
@ -497,7 +558,7 @@
|
|||
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
|
||||
t.stress = clamp((t.stress || 0) + 10 + p * 18, 0, 130);
|
||||
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
|
||||
t.thought = "げんこつがこわい";
|
||||
t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044";
|
||||
}
|
||||
t.hurtTimer = Math.max(t.hurtTimer, isGenkotsu ? 3.4 : (isStone ? 2.7 : 1.25));
|
||||
t.fallTimer = Math.max(t.fallTimer, isGenkotsu ? 0.86 : (isStone ? 0.62 : 0.26));
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@
|
|||
this.updateEffectCounts();
|
||||
this.markTerrainDirty?.("reset");
|
||||
this.rebuildSpatial(true);
|
||||
this.log("\u305f\u308a\u306a\u3044\u3092\u89b3\u5bdf\u7bb1\u306b\u914d\u7f6e\u3057\u307e\u3057\u305f\u3002");
|
||||
showToast("\u65b0\u3057\u3044\u89b3\u5bdf\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002");
|
||||
syncTopButtons();
|
||||
},
|
||||
|
|
@ -130,6 +129,9 @@
|
|||
const nextParents = uniqueIds(t.parents || []);
|
||||
const nextChildren = uniqueIds(t.children || []);
|
||||
const nextPersonalityTags = getPersonalityTraitTags(t);
|
||||
const nextScale = Number.isFinite(t.scale) ? t.scale : (Number.isFinite(t.adultScale) ? t.adultScale : 0.28);
|
||||
const nextAdultScale = Number.isFinite(t.adultScale) ? t.adultScale : nextScale;
|
||||
const nextGrowth = Number.isFinite(t.growth) ? t.growth : 1;
|
||||
if (!this.family[familyKey]) {
|
||||
this.family[familyKey] = {
|
||||
id: familyKey,
|
||||
|
|
@ -142,6 +144,9 @@
|
|||
parentNames: [...(t.parentNames || [])],
|
||||
children: nextChildren,
|
||||
birthTime: t.birthTime,
|
||||
scale: nextScale,
|
||||
adultScale: nextAdultScale,
|
||||
growth: nextGrowth,
|
||||
alive: !t.dead,
|
||||
deathReason: t.deathReason || "",
|
||||
};
|
||||
|
|
@ -154,6 +159,9 @@
|
|||
if (!sameList(entry.personalityTags, nextPersonalityTags)) changed = true;
|
||||
if (entry.hasPaired !== !!t.hasPaired) changed = true;
|
||||
if (entry.generation !== t.generation) changed = true;
|
||||
if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true;
|
||||
if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true;
|
||||
if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true;
|
||||
if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true;
|
||||
if (entry.alive !== nextAlive) changed = true;
|
||||
if (!sameList(entry.parents, nextParents)) changed = true;
|
||||
|
|
@ -164,6 +172,9 @@
|
|||
entry.personalityTags = nextPersonalityTags;
|
||||
entry.alive = nextAlive;
|
||||
entry.generation = t.generation;
|
||||
entry.scale = nextScale;
|
||||
entry.adultScale = nextAdultScale;
|
||||
entry.growth = nextGrowth;
|
||||
entry.hasPaired = !!t.hasPaired;
|
||||
entry.deathReason = t.deathReason || entry.deathReason || "";
|
||||
entry.parents = nextParents;
|
||||
|
|
@ -220,6 +231,9 @@
|
|||
parentNames: live?.parentNames ? [...live.parentNames] : [...(n.parentNames || [])],
|
||||
children,
|
||||
birthTime: live?.birthTime ?? n.birthTime ?? 0,
|
||||
scale: Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : (Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : 0.28))),
|
||||
adultScale: Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : (Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : 0.28))),
|
||||
growth: Number.isFinite(live?.growth) ? live.growth : (Number.isFinite(n.growth) ? n.growth : 1),
|
||||
alive: live ? !live.dead : n.alive !== false,
|
||||
deathReason: live?.deathReason || n.deathReason || "",
|
||||
};
|
||||
|
|
@ -414,12 +428,13 @@
|
|||
this.drawListDirty = true;
|
||||
if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t);
|
||||
this.maxGeneration = Math.max(this.maxGeneration, t.generation);
|
||||
this.emit?.("tarinai:born", { tarinai: t, source: opts.parents?.length ? "family" : "spawn" });
|
||||
return t;
|
||||
},
|
||||
|
||||
startBirthRitual(a, b) {
|
||||
if (!a || !b || a.dead || b.dead) return false;
|
||||
if (a.isZunchiSlave || b.isZunchiSlave) return false;
|
||||
if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false;
|
||||
if (this.areParentChild(a, b)) return false;
|
||||
if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false;
|
||||
if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false;
|
||||
|
|
@ -457,7 +472,7 @@
|
|||
a.birthPartnerId = b.birthPartnerId = null;
|
||||
a.birthRitualLeader = b.birthRitualLeader = false;
|
||||
a.state = b.state = "idle";
|
||||
if (reproductionBlockedByDisease) {
|
||||
if (reproductionBlockedByDisease || (!!a.isZunchiSlave !== !!b.isZunchiSlave)) {
|
||||
a.birthRitualRole = b.birthRitualRole = 0;
|
||||
return null;
|
||||
}
|
||||
|
|
@ -526,29 +541,41 @@
|
|||
},
|
||||
|
||||
spawnChild(a, b, index = 0, total = 1) {
|
||||
const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave);
|
||||
const inherited = Math.random() < 0.56 ? a.type : b.type;
|
||||
const mutation = Math.random() < 0.18 ? baseSpriteId() : inherited;
|
||||
const childBirthPersonality = personalityFromParents(a, b);
|
||||
const mutation = zunchiSlaveChild ? "zunchi_slave" : (Math.random() < 0.18 ? baseSpriteId() : inherited);
|
||||
const childSeed = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}-${index}`;
|
||||
const childGenetics = inheritedGenetics(a, b, childSeed);
|
||||
const childAdultScale = adultScaleFromGenetics(childSeed, childGenetics);
|
||||
const childBirthPersonality = zunchiSlaveChild ? zunchiSlavePersonalityValue() : personalityFromParents(a, b);
|
||||
const child = this.addTarinai({
|
||||
familyKey: childSeed,
|
||||
type: mutation,
|
||||
birthPersonality: childBirthPersonality,
|
||||
currentPersonality: childBirthPersonality,
|
||||
name: makeName(),
|
||||
genetics: childGenetics,
|
||||
adultScale: childAdultScale,
|
||||
name: zunchiSlaveChild ? "\u305a\u3093\u3061\u3069\u308c\u3044" : makeName(),
|
||||
isZunchiSlave: zunchiSlaveChild,
|
||||
age: 0,
|
||||
x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18,
|
||||
y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0),
|
||||
scale: rand(0.17, 0.23),
|
||||
scale: scaleForGrowth(childAdultScale, 0),
|
||||
hunger: rand(20, 40),
|
||||
loneliness: rand(30, 56),
|
||||
energy: rand(55, 95),
|
||||
stress: rand(0, 14),
|
||||
need: Math.random() < 0.6 ? pick([a.need, b.need]) : pick(NEEDS),
|
||||
generation: Math.max(a.generation, b.generation) + 1,
|
||||
lifeSpan: rand(1500, 2400),
|
||||
lifeSpan: rand(1500, 2200) * (childGenetics.lifeSpanMul || 1),
|
||||
parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)],
|
||||
parentNames: [a.name, b.name],
|
||||
birthTime: this.time,
|
||||
powerItemMode: "",
|
||||
sizeItemMode: "",
|
||||
lifeItemMode: "",
|
||||
});
|
||||
if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true });
|
||||
this.maxGeneration = Math.max(this.maxGeneration, child.generation);
|
||||
a.hasPaired = true;
|
||||
b.hasPaired = true;
|
||||
|
|
@ -654,8 +681,14 @@
|
|||
if (!actor || !target || actor.dead || target.dead) return false;
|
||||
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
||||
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.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5);
|
||||
const successChance = clamp(0.62 + (actorScore - targetScore + fearLoad - resistance) / 76, 0.50, 0.96);
|
||||
const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0);
|
||||
const targetAggression = Math.max(0, target.currentPersonality?.aggression || 0);
|
||||
const actorSize = actor.effectiveScale ? actor.effectiveScale() : (actor.scale || 0.28);
|
||||
const targetSize = target.effectiveScale ? target.effectiveScale() : (target.scale || 0.28);
|
||||
const sizeEdge = clamp((actorSize - targetSize) / 0.16, -1, 1);
|
||||
const aggressionEdge = clamp(actorAggression - targetAggression * 0.55, -1, 1);
|
||||
const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5);
|
||||
const successChance = clamp(0.56 + (actorScore - targetScore + fearLoad - resistance) / 82 + aggressionEdge * 0.13 + sizeEdge * 0.16, 0.30, 0.96);
|
||||
actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20);
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
||||
actor.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
||||
|
|
@ -672,9 +705,19 @@
|
|||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.65, 2.45));
|
||||
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
|
||||
if (Math.random() >= successChance) {
|
||||
actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation");
|
||||
target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate");
|
||||
actor.intimidateTimer = 0;
|
||||
actor.intimidateTargetId = null;
|
||||
target.intimidatedTimer = 0;
|
||||
if (actor.state === "intimidate") actor.state = "idle";
|
||||
if (target.state === "panic") target.state = "idle";
|
||||
actor.spriteLockUntil = 0;
|
||||
target.spriteLockUntil = 0;
|
||||
this.startFight(actor, target);
|
||||
return true;
|
||||
}
|
||||
actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation");
|
||||
target.defeatedById = actor.id;
|
||||
target.fightWinnerId = actor.id;
|
||||
target.defeatedTimer = Math.max(target.defeatedTimer, rand(2.8, 4.2));
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@
|
|||
return null;
|
||||
}
|
||||
if (dropped) {
|
||||
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : 0.58);
|
||||
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : (item.type === "pushpin" ? 0.48 : 0.58));
|
||||
item.dropTimer = item.dropMax;
|
||||
item.dropImpactDone = false;
|
||||
}
|
||||
|
|
@ -205,6 +205,7 @@
|
|||
this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1;
|
||||
this.markTerrainDirty?.(`place:${item.type}`);
|
||||
this.emit("tool:place", { item, type: item.type, dropped });
|
||||
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
||||
if (dropped) audio.place?.(item.type);
|
||||
this.rebuildSpatial(true);
|
||||
return item;
|
||||
|
|
@ -237,8 +238,6 @@
|
|||
if (target) {
|
||||
target.poke();
|
||||
if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "observe", { participants: [target] });
|
||||
} else {
|
||||
this.log("\u7a7a\u6c17\u3092\u3064\u3064\u3044\u305f\u3002\u305d\u3053\u306b\u306f\u4f55\u3082\u305f\u308a\u306a\u304b\u3063\u305f\u3002", "observe", { hiddenFromObservation: true, countInStats: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -263,6 +262,7 @@
|
|||
if (d < hit && d < bestD) { best = it; bestD = d; }
|
||||
}
|
||||
if (!best) { showToast("\u524a\u9664\u3067\u304d\u308b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; }
|
||||
if (best.type === "pushpin" && best.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u753b\u92f2\u306f\u3064\u307e\u3093\u3067\u53d6\u308a\u9664\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
best.amount = 0;
|
||||
audio.delete?.();
|
||||
this.compactItems();
|
||||
|
|
@ -281,13 +281,13 @@
|
|||
const placed = this.placeItem(rawItem, true);
|
||||
if (!placed) return;
|
||||
const label = toolLabel(itemType);
|
||||
this.log(itemType === "genkotsu" ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
|
||||
this.log(itemType === "genkotsu" || itemType === "pushpin" ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
|
||||
}
|
||||
},
|
||||
|
||||
inferLogKind(text) {
|
||||
const s = String(text || "").toLowerCase();
|
||||
if (s.includes("配置") || s.includes("設置") || s.includes("placed") || s.includes("selected")) return "observe";
|
||||
if (s.includes("\u914d\u7f6e") || s.includes("\u8a2d\u7f6e") || s.includes("placed") || s.includes("selected")) return "observe";
|
||||
if (s.includes("firecracker") || s.includes("accident") || s.includes("drop") || s.includes("\u7206\u7af9") || s.includes("\u843d\u3061") || s.includes("\u4e8b\u6545") || s.includes("\u5439\u304d\u98db")) return "accident";
|
||||
if (s.includes("fight") || s.includes("lost to") || s.includes("\u55a7\u5629") || s.includes("\u8ca0\u3051")) return "fight";
|
||||
if (s.includes("appeared") || s.includes("wandered in") || s.includes("birth") || s.includes("\u73fe\u308c") || s.includes("\u8ff7\u3044\u8fbc")) return "birth";
|
||||
|
|
@ -311,24 +311,24 @@
|
|||
const text = String(entry.text || "");
|
||||
if (kind === "death") return audio.death?.();
|
||||
if (kind === "birth") return audio.birth?.();
|
||||
if (kind === "fight") return /決着|勝|負|覚え/.test(text) ? audio.fightFinish?.() : audio.fight?.();
|
||||
if (kind === "fight") return /\u6c7a\u7740|\u52dd|\u8ca0|\u899a\u3048/.test(text) ? audio.fightFinish?.() : audio.fight?.();
|
||||
if (kind === "food") return audio.eat?.();
|
||||
if (kind === "grass") return audio.play?.("sfx_grass", { category: "ops", minGap: 0.25 });
|
||||
if (kind === "weather") return audio.phase?.();
|
||||
if (kind === "relation") return audio.notify?.();
|
||||
if (kind === "accident") {
|
||||
if (/配置|置いた|設置/.test(text)) return audio.place?.(/爆竹/.test(text) ? "firecracker" : "");
|
||||
if (/げんこつ/.test(text)) return audio.genkotsuImpact?.();
|
||||
if (/爆竹|爆発/.test(text)) return audio.explode?.();
|
||||
if (/石/.test(text)) return audio.stoneImpact?.();
|
||||
if (/ボール/.test(text)) return audio.ballHit?.();
|
||||
if (/\u914d\u7f6e|\u7f6e\u3044\u305f|\u8a2d\u7f6e/.test(text)) return audio.place?.(/\u7206\u7af9/.test(text) ? "firecracker" : "");
|
||||
if (/\u3052\u3093\u3053\u3064/.test(text)) return audio.genkotsuImpact?.();
|
||||
if (/\u7206\u7af9|\u7206\u767a/.test(text)) return audio.explode?.();
|
||||
if (/\u77f3/.test(text)) return audio.stoneImpact?.();
|
||||
if (/\u30dc\u30fc\u30eb/.test(text)) return audio.ballHit?.();
|
||||
return audio.damage?.(18);
|
||||
}
|
||||
if (kind === "event") {
|
||||
if (/女王アリ/.test(text)) return audio.queenAnt?.();
|
||||
if (/アリ|巣/.test(text)) return audio.antNest?.();
|
||||
if (/病|発病/.test(text)) return audio.disease?.();
|
||||
if (/回復|治/.test(text)) return audio.heal?.();
|
||||
if (/\u5973\u738b\u30a2\u30ea/.test(text)) return audio.queenAnt?.();
|
||||
if (/\u30a2\u30ea|\u5de3/.test(text)) return audio.antNest?.();
|
||||
if (/\u75c5|\u767a\u75c5/.test(text)) return audio.disease?.();
|
||||
if (/\u56de\u5fa9|\u6cbb/.test(text)) return audio.heal?.();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
(function (global) {
|
||||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_spatial_budget.js");
|
||||
|
||||
function liveCount(list) {
|
||||
let count = 0;
|
||||
for (const item of list || []) if (item && !item.dead) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
performanceLevel() {
|
||||
if (window.TarinaiPerformance) return window.TarinaiPerformance.level || 0;
|
||||
|
|
@ -33,8 +40,8 @@
|
|||
|
||||
sortedDrawList() {
|
||||
if (!this.drawList) this.drawList = [];
|
||||
const antCount = (this.ants || []).filter(a => a && !a.dead).length;
|
||||
const tarinaiCount = (this.tarinai || []).filter(t => t && !t.dead).length;
|
||||
const antCount = liveCount(this.ants);
|
||||
const tarinaiCount = liveCount(this.tarinai);
|
||||
if (this.drawListDirty || this.drawList.length !== tarinaiCount + antCount) {
|
||||
this.drawList.length = 0;
|
||||
for (const t of this.tarinai || []) if (t && !t.dead) this.drawList.push(t);
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
const previousDay = this.day || 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: {} };
|
||||
for (const t of this.tarinai || []) if (t) t.personalityDaily = { day: this.day, total: 0, byKey: {}, byCause: {} };
|
||||
}
|
||||
if (this.pointer) this.pointer.motion = Math.max(0, (this.pointer.motion || 0) - dt * 2.4);
|
||||
this.shakeTimer = Math.max(0, (this.shakeTimer || 0) - dt);
|
||||
|
|
@ -212,24 +212,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (this.weather === "light_rain" && (this.itemCounts.water || 0) < 24 && Math.random() < dt * 0.12) {
|
||||
const drop = new Item("water", rand(58, this.w - 58), rand(58, this.h - 58));
|
||||
drop.amount = rand(24, 58);
|
||||
// Rain visuals are diagonal line streaks in render.js. This branch creates
|
||||
// horizontal oval water items spawned by rain; it intentionally does not
|
||||
// share the rain overlay renderer.
|
||||
if (window.TarinaiWeatherSystem?.shouldDropRainWater?.(this, dt)) {
|
||||
const drop = window.TarinaiWeatherSystem.createRainWaterItem(this);
|
||||
this.items.push(drop);
|
||||
this.itemCounts.water = (this.itemCounts.water || 0) + 1;
|
||||
this.markTerrainDirty?.("rain-water");
|
||||
this.emit?.("rain:itemDropped", { item: drop, type: "water" });
|
||||
}
|
||||
|
||||
if (Math.random() < dt * 0.004) {
|
||||
const entries = [
|
||||
"\u4f55\u5339\u304b\u304c\u3001\u5168\u54e1\u3067\u306f\u306a\u3044\u304c\u3001\u540c\u3058\u65b9\u5411\u3092\u898b\u305f\u3002",
|
||||
"\u98df\u3079\u7269\u306f\u3042\u308b\u3002\u8db3\u308a\u3066\u3044\u308b\u304b\u306f\u5225\u554f\u984c\u3002",
|
||||
"\u4e00\u5339\u304c\u7720\u308a\u3001\u8fd1\u304f\u306e\u5225\u306e\u4e00\u5339\u3082\u7720\u308b\u3053\u3068\u3092\u8003\u3048\u305f\u3002",
|
||||
"\u8349\u306e\u8fd1\u304f\u306b\u3001\u77ed\u304f\u3066\u610f\u5473\u306e\u306a\u3044\u5217\u304c\u3067\u304d\u305f\u3002",
|
||||
`${this.phaseName()}\u3002\u6c17\u5206\u306f\u3086\u3063\u304f\u308a\u5909\u308f\u3063\u3066\u3044\u308b\u3002`
|
||||
];
|
||||
this.log(pick(entries));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
sanitizeOutOfBounds() {
|
||||
|
|
@ -310,16 +303,18 @@
|
|||
updateWeather(dt) {
|
||||
this.nextWeatherChange -= dt;
|
||||
if (this.nextWeatherChange > 0) return;
|
||||
if (window.TarinaiWeatherSystem?.applyNextWeather) {
|
||||
window.TarinaiWeatherSystem.applyNextWeather(this);
|
||||
return;
|
||||
}
|
||||
const current = this.weather || "sunny";
|
||||
const choices = current === "sunny"
|
||||
? ["sunny", "cloudy", "light_rain"]
|
||||
: current === "cloudy"
|
||||
? ["sunny", "cloudy", "light_rain"]
|
||||
: ["sunny", "cloudy"];
|
||||
const next = pick(choices);
|
||||
const next = pick(current === "light_rain" ? ["sunny", "cloudy"] : ["sunny", "cloudy", "light_rain"]);
|
||||
this.weather = next;
|
||||
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
||||
if (next !== current) this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`);
|
||||
if (next !== current) {
|
||||
this.emit?.("weather:changed", { previous: current, next });
|
||||
this.log(`\u5929\u6c17: ${weatherLabel(this.weather)}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,20 @@ appearance. Do not invent separate tool-UI-only designs.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import math
|
||||
import json, math, re
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "assets" / "ui"
|
||||
REGISTRY = ROOT / "js" / "item_visual_registry.js"
|
||||
|
||||
|
||||
def visual_registry() -> dict:
|
||||
text = REGISTRY.read_text(encoding="utf-8")
|
||||
match = re.search(r"String\.raw`([\s\S]*?)`", text)
|
||||
if not match:
|
||||
raise RuntimeError("Could not find JSON visual registry in js/item_visual_registry.js")
|
||||
return json.loads(match.group(1))
|
||||
|
||||
|
||||
def save_icon(name: str, draw_fn) -> None:
|
||||
|
|
@ -29,6 +38,45 @@ def draw_drop(fill, stroke, highlight):
|
|||
return fn
|
||||
|
||||
|
||||
def rgba(value, fallback):
|
||||
if not value:
|
||||
return fallback
|
||||
if value.startswith("#"):
|
||||
value = value.lstrip("#")
|
||||
if len(value) == 6:
|
||||
return tuple(int(value[i:i+2], 16) for i in (0, 2, 4)) + (255,)
|
||||
match = re.match(r"rgba?\(([^)]+)\)", value.replace(" ", ""))
|
||||
if match:
|
||||
parts = match.group(1).split(",")
|
||||
r, g, b = (int(float(parts[i])) for i in range(3))
|
||||
a = int(float(parts[3]) * 255) if len(parts) > 3 else 255
|
||||
return (r, g, b, a)
|
||||
return fallback
|
||||
|
||||
|
||||
def draw_powder(fill, stroke, highlight):
|
||||
def fn(d: ImageDraw.ImageDraw) -> None:
|
||||
d.ellipse([18, 62, 78, 76], fill=(60, 45, 35, 36))
|
||||
for i in range(20):
|
||||
x = 48 + math.cos(i * 1.9) * (8 + (i % 5) * 4)
|
||||
y = 56 - abs(math.sin(i * 1.3)) * 24 + (i % 3) * 3
|
||||
r = 5 + (i % 4)
|
||||
d.ellipse([x - r, y - r * 0.65, x + r, y + r * 0.65], fill=highlight if i % 4 == 0 else fill, outline=stroke, width=1)
|
||||
d.ellipse([31, 31, 52, 39], fill=rgba("rgba(255,255,255,0.62)", (255, 255, 255, 158)))
|
||||
return fn
|
||||
|
||||
|
||||
def draw_pill(fill, stroke, highlight, scale=1.0):
|
||||
def fn(d: ImageDraw.ImageDraw) -> None:
|
||||
cx, cy = 48, 48
|
||||
w, h = 66 * scale, 28 * scale
|
||||
box = [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2]
|
||||
d.rounded_rectangle(box, radius=int(h / 2), fill=fill, outline=stroke, width=4)
|
||||
d.line([(cx, cy - h / 2 + 2), (cx, cy + h / 2 - 2)], fill=highlight, width=4)
|
||||
d.ellipse([cx - w * 0.32, cy - h * 0.24, cx - w * 0.08, cy - h * 0.04], fill=(255, 255, 255, 110))
|
||||
return fn
|
||||
|
||||
|
||||
def draw_laxative(d: ImageDraw.ImageDraw) -> None:
|
||||
d.rounded_rectangle([14, 28, 82, 68], radius=15, fill=(244, 212, 154, 255), outline=(98, 61, 29, 235), width=4)
|
||||
# Zunchi mound motif inside the tablet.
|
||||
|
|
@ -68,13 +116,31 @@ def draw_sleep(d: ImageDraw.ImageDraw) -> None:
|
|||
|
||||
|
||||
def main() -> None:
|
||||
save_icon("tool_laxative.webp", draw_laxative)
|
||||
save_icon("tool_mystery_drug.webp", draw_mystery)
|
||||
save_icon("tool_ammo.webp", draw_bullet)
|
||||
save_icon("tool_sleep_drug.webp", draw_sleep)
|
||||
save_icon("tool_water.webp", draw_drop((86, 157, 224, 220), (45, 101, 168, 150), (255, 255, 255, 150)))
|
||||
save_icon("tool_zunda_juice.webp", draw_drop((125, 218, 82, 235), (63, 150, 50, 160), (238, 255, 226, 160)))
|
||||
save_icon("tool_mercury.webp", draw_drop((210, 216, 222, 240), (112, 122, 132, 165), (255, 255, 255, 170)))
|
||||
registry = visual_registry()
|
||||
renderer_map = {
|
||||
"laxative_tablet": draw_laxative,
|
||||
"mystery_tablet": draw_mystery,
|
||||
"bullet": draw_bullet,
|
||||
"sleep_tablet": draw_sleep,
|
||||
}
|
||||
for item_id, visual in registry.items():
|
||||
if item_id == "ball":
|
||||
continue
|
||||
palette = visual.get("palette") or {}
|
||||
renderer = visual.get("renderer")
|
||||
fill = rgba(palette.get("fill"), (255, 250, 241, 255))
|
||||
stroke = rgba(palette.get("stroke"), (141, 111, 72, 220))
|
||||
highlight = rgba(palette.get("highlight") or palette.get("accent"), (255, 255, 255, 150))
|
||||
if renderer == "oval_droplet":
|
||||
draw_fn = draw_drop(fill, stroke, highlight)
|
||||
elif renderer == "powder_pile":
|
||||
draw_fn = draw_powder(fill, stroke, highlight)
|
||||
elif renderer == "split_pill":
|
||||
draw_fn = draw_pill(fill, stroke, highlight, visual.get("uiPreviewScale") or visual.get("scale") or 1.0)
|
||||
else:
|
||||
draw_fn = renderer_map.get(renderer)
|
||||
if draw_fn:
|
||||
save_icon(f"tool_{item_id}.webp", draw_fn)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "15.20.3";
|
||||
const APP_VERSION = "15.20.16";
|
||||
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const v = `v=${APP_VERSION}`;
|
||||
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
||||
const coreScriptNames = [
|
||||
"version", "event_bus", "performance_manager", "effect_registry", "sound_pack", "data", "math", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_selected", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "performance_manager", "disease_registry", "effect_registry", "sound_pack", "data", "item_visual_registry", "food_registry", "input_mode_manager", "math", "assets", "audio", "render", "sim_core", "items", "ants", "health", "tarinai", "tarinai_identity_social", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "world_update", "world_placement_log", "world_bootstrap", "text_catalog", "ui", "ui_log", "ui_selected", "ui_layout_dialogs", "ui_tools", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
];
|
||||
const CORE_ASSETS = [
|
||||
"./",
|
||||
|
|
|
|||