hm
This commit is contained in:
parent
cefa2ace5a
commit
0e432c62ae
87 changed files with 4282 additions and 3066 deletions
|
|
@ -17,7 +17,7 @@
|
|||
"groups": [
|
||||
{
|
||||
"id": "root",
|
||||
"patterns": [".gitignore", ".htaccess", "app_manifest.json", "favicon.ico", "index.html", "README.md", "AI_MAP.md", "FILES.json", "service-worker.js"],
|
||||
"patterns": [".gitignore", ".htaccess", "app_manifest.json", "favicon.ico", "index.html", "README.md", "AI_MAP.md", "GAME_FEATURES.md", "FILES.json", "service-worker.js"],
|
||||
"meaning": "Repo/app shell metadata, server/static cache config, docs, manifest, entrypoint."
|
||||
},
|
||||
{
|
||||
|
|
|
|||
171
GAME_FEATURES.md
Normal file
171
GAME_FEATURES.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# tarinai_ Game Features
|
||||
|
||||
This document describes the playable features of `tarinai_`: what the player sees, what tools do, and how tarinai behave. It is intentionally player/design-facing, not an implementation map.
|
||||
|
||||
## Game Overview
|
||||
|
||||
`tarinai_` is an observation sandbox about small creatures called tarinai. The player places food, furniture, hazards, machines, and environment tools, then watches how individuals live, move, eat, sleep, panic, fight, reproduce, get sick, recover, and leave family records.
|
||||
|
||||
The core loop is:
|
||||
|
||||
1. Choose a field and observe the colony.
|
||||
2. Place tools or objects that change the environment.
|
||||
3. Select individual tarinai to inspect health, needs, personality, relations, diseases, records, and current behavior.
|
||||
4. Watch the event log, colony graphs, and family tree to understand long-term outcomes.
|
||||
|
||||
## Main UI
|
||||
|
||||
- **Canvas / field**: the main simulation area. Tarinai, items, effects, ants, weather, and terrain are drawn here.
|
||||
- **Top buttons**: pause/resume, speed control, ecology guide, sound settings, credits, save, and reset.
|
||||
- **Mobile mode controls**: auto, camera, and tool modes for touch screens.
|
||||
- **Right panel tabs**: jump between main game, selected tarinai data, tools, colony, events, and family tree.
|
||||
- **Tarinai data panel**: appears when a tarinai is selected. Shows health, needs, personality, active effects, relations, inventory, behavior reason, and records.
|
||||
- **Tool palette**: grouped buttons for operation, food, medicine, habitat, physics, and danger tools.
|
||||
- **Colony panel**: shows population, deaths, colony mood, ground type, and charts for population, needs, and objects.
|
||||
- **Event panel**: logs births, deaths, accidents, fights, food events, weather, grass, and relation changes. Optional push-style notifications can be enabled by event type.
|
||||
- **Family tree**: shows parent-child generations, inherited stats, personality tags, family links, and archived lineage records.
|
||||
- **Ecology dialog**: in-game guide explaining tarinai needs, stress, food, sleep, personality, family, disease, accidents, tools, and colony reading.
|
||||
- **Sound menu**: master sound plus voice, notification, and operation categories.
|
||||
|
||||
## Tool Categories
|
||||
|
||||
### Operation
|
||||
|
||||
- **解除 / clear**: cancels the selected tool and returns to normal observe mode.
|
||||
- **たりないデータ / observe**: select a tarinai or inspect compatible objects such as signboards.
|
||||
- **追加 / new**: calls a new tarinai into the field.
|
||||
- **つつく / poke**: pokes tarinai and can push balls.
|
||||
- **つまむ / pinch**: drags tarinai and some placed objects.
|
||||
- **コピー / copy**: copies a placed item and pastes a new instance with its settings.
|
||||
- **戻す / undo** and **進む / redo**: reverse or reapply placement/edit actions.
|
||||
- **削除 / delete**: removes placed tools, objects, and stains.
|
||||
- **範囲削除 / area delete**: removes many nearby objects or stains around the cursor.
|
||||
- **洗浄 / water hose**: drag to clean dirt, zunchi, corpses, and stains.
|
||||
|
||||
### Food
|
||||
|
||||
- **ずんだ餅 / sweet**: favored food; can cure some illness.
|
||||
- **へこ餅 / love mochi**: makes reproduction behavior more likely for a while.
|
||||
- **けんか餅 / fight mochi**: makes fighting behavior more likely for a while.
|
||||
- **草 / grass**: food and comfort source; can grow and be used for simple beds.
|
||||
- **水滴 / water**: drinkable water, also appears during rain and can be set in duplicators.
|
||||
- **ずんだ汁 / zunda juice**: clears item effects from a tarinai.
|
||||
- **複製機 / duplicator**: can be loaded with compatible food, medicine, grass, water, pushpins, or oshibyo and then produces or applies that content.
|
||||
|
||||
### Medicine
|
||||
|
||||
- **ねむり薬 / sleep drug**: causes sleepiness or sleep disease; damage can cure the disease.
|
||||
- **下剤 / laxative**: causes repeated zunchi and reduces food benefit.
|
||||
- **プロテイン / protein**: permanent power mode; more damage dealt, lower disease chance.
|
||||
- **ンイテロプ / niteropu**: permanent weak mode; less damage dealt, higher disease chance.
|
||||
- **弾薬 / ammo**: temporary high-speed movement and strong collision impact.
|
||||
- **怪しい薬 / mystery drug**: randomly applies an eligible item effect or disease.
|
||||
- **水銀 / mercury**: increases lifespan multiplier on each use.
|
||||
- **巨大薬 / giant drug**: greatly increases size, attack, and speed, but deals ongoing damage.
|
||||
- **矮小薬 / dwarf drug**: greatly decreases size, attack, and speed, and also deals ongoing damage.
|
||||
|
||||
### Habitat
|
||||
|
||||
- **ずんち / zunchi**: tarinai waste; affects disease and can fertilize grass.
|
||||
- **スプリンクラー / sprinkler**: cleans one target at a time in its effective radius.
|
||||
- **干草寝床 / bed**: placeable sleeping spot; helps HP recovery.
|
||||
- **巣箱 / nest box**: reduces stress nearby; improves sleep quality when used.
|
||||
- **ボール / ball**: toy that tarinai can play with and the player can poke.
|
||||
- **看板 / signboard**: editable text object; inspect with observe after placing.
|
||||
|
||||
### Physics
|
||||
|
||||
- **ヒモ / rope**: connect two targets with a flexible link.
|
||||
- **棒 / rod**: connect two targets with a rigid link.
|
||||
- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
|
||||
- **バウンス柵 / bounce fence**: fence that rebounds things that hit it.
|
||||
- **ゲート柵 / gate fence**: click to open or close.
|
||||
- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
|
||||
- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
|
||||
- **往復体 / reciprocator**: custom mechanical shape that moves back and forth; click to edit shape and speed.
|
||||
|
||||
### Danger
|
||||
|
||||
- **石 / stone**: heavy object; dangerous when dropped or moved into others.
|
||||
- **げんこつ / genkotsu**: large fist impact on the ground.
|
||||
- **爆竹 / firecracker**: creates an explosion.
|
||||
- **画鋲 / pushpin**: rolls when dropped; can lodge in tarinai and cause panic plus continuous damage.
|
||||
- **おしり鋲 / oshibyo**: blocks zunchi when lodged; pulling it out releases stored zunchi.
|
||||
- **射撃 / shoot**: attacks with a gun effect.
|
||||
- **アリの巣 / ant nest**: spawns ants that search for tarinai and carry them to the nest.
|
||||
|
||||
## Tarinai Needs And Stats
|
||||
|
||||
Tarinai behavior is shaped by health, energy, needs, stress, personality, family, disease, and nearby objects.
|
||||
|
||||
Key needs include:
|
||||
|
||||
- **Food / hunger**: hungry tarinai seek grass, sweets, food, water-related options, or other edible targets.
|
||||
- **Sleep**: sleepy tarinai seek beds, nest boxes, grass beds, or eventually sleep anywhere.
|
||||
- **Safety**: danger, pain, hazards, ants, fights, and panic increase defensive behavior.
|
||||
- **Social / relations**: tarinai can seek friends, follow parents, avoid enemies, fight rivals, or approach mates.
|
||||
- **Fulfillment / comfort**: toys, ground, sun, shelter, and environment affect mood and stress.
|
||||
- **Health**: damage, disease, stress, and recovery decide whether tarinai live, weaken, or die.
|
||||
|
||||
Stress changes with fear, crowding, dirt, disease, fights, ground type, and environment. High stress makes fleeing, intimidation, sickness, and poor behavior more likely.
|
||||
|
||||
Personality and genetics affect tendencies such as aggression, openness, sociability, nervousness, body size, lifespan, damage, and speed. Some traits are inherited by children and shown in the family tree.
|
||||
|
||||
## Movement And Behavior
|
||||
|
||||
Tarinai are autonomous. Their movement is usually a result of current state, target choice, and local environment.
|
||||
|
||||
Common behavior states:
|
||||
|
||||
- **Idle / wander**: waits, watches, or moves lightly around the field.
|
||||
- **Seek food**: moves toward edible items when hungry or affected by disease.
|
||||
- **Eat / drink**: consumes food, grass, water, medicine, or special items.
|
||||
- **Seek bed / sleep**: finds a bed, nest box, grass bed, or sleeps in place.
|
||||
- **Play ball**: approaches and interacts with balls.
|
||||
- **Seek material / build**: gathers material and creates simple structures such as grass beds.
|
||||
- **Seek friend / follow parent**: moves toward social targets or parents.
|
||||
- **Fight / seek enemy**: approaches and attacks rivals, especially under fight effects or aggression.
|
||||
- **Panic / flee**: runs from danger, pain, destroyed owned objects, hazards, or fear triggers.
|
||||
- **Intimidate**: threatens another tarinai or ants instead of directly fleeing or attacking.
|
||||
- **Birth ritual**: pre-birth/reproduction interaction with a partner.
|
||||
- **Sunbath**: rests in good sun to reduce stress.
|
||||
- **Disease states**: zunchi sickness, sleep disease, explosion disease, and injury sickness can override normal priorities.
|
||||
- **Frozen**: outside-field preservation state.
|
||||
|
||||
Tarinai can be pushed by physics objects, carried or affected by ants, dragged by the player, attached with ropes/rods, blocked by fences, and damaged by hazards. Some objects become owned or emotionally important, so losing them can trigger panic.
|
||||
|
||||
## Environment
|
||||
|
||||
- **Field types**: park, garden, and cage provide different field sizes.
|
||||
- **Ground types**: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style ground affect stress, grass, and movement.
|
||||
- **Weather**: rain and other weather influence water, mood, and environment effects.
|
||||
- **Grass ecology**: grass grows, feeds tarinai, comforts them, and can become bed material.
|
||||
- **Dirt and corpses**: zunchi, splats, corpses, and ant corpses affect hygiene, disease, and observation results.
|
||||
- **Day/light conditions**: lighting and sun influence rendering and sunbath behavior.
|
||||
|
||||
## Disease And Recovery
|
||||
|
||||
Diseases and item effects can strongly redirect behavior.
|
||||
|
||||
- **Zunchi disease**: changes food/water priorities and makes the tarinai feel unwell.
|
||||
- **Sleep disease**: causes sleep-focused behavior.
|
||||
- **Explosion disease**: creates dangerous explosive outcomes and water-seeking pressure.
|
||||
- **Fight disease / injury sickness**: causes unstable movement or fighting-related behavior.
|
||||
- **Item effects**: mochi, medicine, size drugs, mercury, ammo, and power modes create temporary or permanent modifiers.
|
||||
|
||||
Recovery depends on the disease or effect. Water, rest, food, medicine, zunda juice, damage, environment cleanup, or time can all matter depending on the case.
|
||||
|
||||
## Family, Records, And Colony Observation
|
||||
|
||||
- Tarinai can form parent-child records and partner links.
|
||||
- Children inherit parts of their parents' tendencies and are grouped by generation.
|
||||
- Family tree nodes show portraits, inherited stats, personality tags, and relationship structure.
|
||||
- Event logs record important simulation events such as birth, death, accidents, fights, weather, grass, and relation changes.
|
||||
- Colony charts summarize population, needs, and object counts over time.
|
||||
- Selected tarinai records help explain why an individual is acting a certain way.
|
||||
|
||||
## Save, Reset, And Persistence
|
||||
|
||||
- The app supports manual save and restore through browser storage.
|
||||
- Reset opens field selection and starts a new field state.
|
||||
- Service worker caching allows the static app shell to remain available after load, depending on browser support.
|
||||
|
|
@ -5,6 +5,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
## Read Strategy
|
||||
|
||||
- Start here for architecture, script order constraints, and where to look next.
|
||||
- For game mechanics, UI, items, and tarinai behavior, read `GAME_FEATURES.md`.
|
||||
- For subsystem work, read `AI_MAP.md` plus only the matching files.
|
||||
- For exact file inventory, run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` or narrow it: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 ui`. Python equivalent: `python scripts/ai_inventory.py --all`.
|
||||
- For machine-readable routing, read `FILES.json`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.15.47",
|
||||
"version": "39.15.63",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -13,16 +13,27 @@
|
|||
"js/registry_base.js",
|
||||
"js/disease_registry.js",
|
||||
"js/sound_pack.js",
|
||||
"js/deterministic_helpers.js",
|
||||
"js/item_tool_definitions.js",
|
||||
"js/item_visual_definitions.js",
|
||||
"js/item_food_definitions.js",
|
||||
"js/item_effect_definitions.js",
|
||||
"js/item_registry.js",
|
||||
"js/data.js",
|
||||
"js/ground_types.js",
|
||||
"js/input_mode_manager.js",
|
||||
"js/math.js",
|
||||
"js/physics_helpers.js",
|
||||
"js/collision_footprint_system.js",
|
||||
"js/physics_helpers.js",
|
||||
"js/placement_collision_system.js",
|
||||
"js/placement_preview_system.js",
|
||||
"js/pin_attachment_system.js",
|
||||
"js/physics_shape_editor_system.js",
|
||||
"js/mechanical_system.js",
|
||||
"js/mechanical_shape_bridge.js",
|
||||
"js/constraint_system.js",
|
||||
"js/physics_world_system.js",
|
||||
"js/physics_projection_system.js",
|
||||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
"js/perf_profiler.js",
|
||||
|
|
@ -63,6 +74,7 @@
|
|||
"js/tarinai_needs_core.js",
|
||||
"js/tarinai_behavior_text.js",
|
||||
"js/tarinai_forced_behavior.js",
|
||||
"js/tarinai_nest_sleep_system.js",
|
||||
"js/tarinai_item_targeting.js",
|
||||
"js/tarinai_consumable_behavior.js",
|
||||
"js/tarinai_social_action_runtime.js",
|
||||
|
|
@ -92,6 +104,9 @@
|
|||
"js/world_view.js",
|
||||
"js/family_graph.js",
|
||||
"js/world_family_social.js",
|
||||
"js/impact_core_system.js",
|
||||
"js/impact_response_system.js",
|
||||
"js/collision_response_system.js",
|
||||
"js/world_combat_effects.js",
|
||||
"js/world_environment.js",
|
||||
"js/world_spatial_budget.js",
|
||||
|
|
@ -110,6 +125,7 @@
|
|||
"js/world_update_phases.js",
|
||||
"js/system_order.js",
|
||||
"js/simulation.js",
|
||||
"js/colony_situation_system.js",
|
||||
"js/world_update.js",
|
||||
"js/world_tool_actions.js",
|
||||
"js/world_placement_log.js",
|
||||
|
|
@ -133,6 +149,7 @@
|
|||
"js/ui_ground.js",
|
||||
"js/ui_tools.js",
|
||||
"js/ui_input_shared.js",
|
||||
"js/ui_pointer_action_system.js",
|
||||
"js/ui_input_touch.js",
|
||||
"js/ui_input_mouse.js",
|
||||
"js/ui_bind.js",
|
||||
|
|
|
|||
|
|
@ -419,6 +419,7 @@
|
|||
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
||||
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
||||
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
||||
.log-push-toast.log-push-event::before { background: #8b82d6; }
|
||||
.log-push-toast.leaving {
|
||||
animation: logPushOut .18s ease both;
|
||||
}
|
||||
|
|
@ -684,60 +685,6 @@
|
|||
outline: 2px solid rgba(103,178,44,0.58);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.save-hash-profile {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(115, 91, 62, 0.18);
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.56);
|
||||
color: #4a3929;
|
||||
font-size: 12px;
|
||||
}
|
||||
.save-hash-profile:empty {
|
||||
display: none;
|
||||
}
|
||||
.save-profile-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
}
|
||||
.save-profile-summary span {
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(115, 91, 62, 0.08);
|
||||
}
|
||||
.save-profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
.save-profile-grid > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.54);
|
||||
}
|
||||
.save-profile-grid em {
|
||||
opacity: 0.62;
|
||||
font-style: normal;
|
||||
}
|
||||
.save-profile-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px 7px;
|
||||
align-items: center;
|
||||
}
|
||||
.save-profile-items span {
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(111, 153, 102, 0.13);
|
||||
}
|
||||
|
||||
|
||||
/* v36: pin only the operation tool category under the quick-scroll tabs. */
|
||||
.tool-category[data-tool-category="operate"]:not(.collapsed) {
|
||||
position: sticky;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
.tool[data-tool="poke"] { --tool-emoji: "\1F448"; }
|
||||
.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; }
|
||||
.tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; }
|
||||
.tool[data-tool="clear_tool"] { --tool-emoji: "\2715"; }
|
||||
.tool[data-tool="copy"] { --tool-emoji: "\29c9 "; }
|
||||
.tool[data-tool="area_delete"] { --tool-emoji: "\d7 "; }
|
||||
.tool[data-tool="rope"] { --tool-emoji: "\2570 "; }
|
||||
|
|
@ -322,6 +323,7 @@ meter {
|
|||
.log-weather { border-left-color: rgba(92, 142, 210, 0.62); }
|
||||
.log-relation { border-left-color: rgba(204, 126, 176, 0.62); }
|
||||
.log-observe { border-left-color: rgba(175, 146, 86, 0.62); }
|
||||
.log-event { border-left-color: rgba(132, 120, 210, 0.62); }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
#app { padding: 10px; }
|
||||
|
|
@ -362,4 +364,3 @@ meter {
|
|||
@media (min-width: 921px) {
|
||||
.mobile-mode-controls { display: none !important; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
||||
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
||||
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
||||
.log-push-toast.log-push-event::before { background: #8b82d6; }
|
||||
.log-push-toast.leaving { opacity: 0; transform: translateY(8px); transition: opacity .22s ease, transform .22s ease; }
|
||||
@keyframes logPushIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
|
|
@ -164,6 +165,46 @@
|
|||
}
|
||||
body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { display: flex; }
|
||||
body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; }
|
||||
|
||||
.mobile-rotate-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 58px;
|
||||
z-index: 24;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 251, 245, 0.92);
|
||||
box-shadow: 0 8px 22px rgba(80,60,35,.18);
|
||||
border: 1px solid rgba(120, 95, 60, .16);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.mobile-rotate-controls button {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #65533f;
|
||||
background: rgba(255,255,255,.82);
|
||||
box-shadow: inset 0 -1px 0 rgba(0,0,0,.05);
|
||||
}
|
||||
.mobile-rotate-controls button:active { transform: translateY(1px); }
|
||||
body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) { display: flex; }
|
||||
body:not(.touch-mobile-ui) .mobile-rotate-controls { display: none !important; }
|
||||
@media (max-width: 980px) {
|
||||
body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: calc(env(safe-area-inset-bottom, 0px) + 56px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 12002;
|
||||
max-width: calc(100vw - 22px);
|
||||
}
|
||||
}
|
||||
|
||||
body.touch-mobile-ui.mobile-mode-camera #gameCanvas { cursor: grab; }
|
||||
body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
||||
|
||||
|
|
@ -249,10 +290,9 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
/* v39.15.38: keep desktop layout close to v39.15.36; only hide the PC quick-scroll buttons that duplicate the canvas/tool card. */
|
||||
@media (min-width: 981px) {
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.panel-quick-tabs [data-panel-tab="game"],
|
||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
||||
.panel-quick-tabs [data-panel-tab="game"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -402,10 +442,9 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
line-height: 1.15;
|
||||
}
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.panel-quick-tabs [data-panel-tab="game"],
|
||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
||||
.panel-quick-tabs [data-panel-tab="game"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -474,10 +513,9 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
.panel-quick-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.panel-quick-tabs [data-panel-tab="game"],
|
||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
||||
.panel-quick-tabs [data-panel-tab="game"] {
|
||||
display: none !important;
|
||||
}
|
||||
.lineage-main-card {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@
|
|||
}
|
||||
|
||||
.lineage-head h2 { margin-bottom: 0; }
|
||||
.lineage-title-note {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(126, 86, 52, 0.78);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.lineage-main-content {
|
||||
height: min(78vh, 980px);
|
||||
|
|
@ -125,8 +133,8 @@ body::before {
|
|||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px 5px;
|
||||
margin-top: 2px;
|
||||
font-size: 9px;
|
||||
line-height: 1.12;
|
||||
font-size: 9.7px;
|
||||
line-height: 1.13;
|
||||
color: rgba(88, 70, 48, 0.86);
|
||||
letter-spacing: -0.02em;
|
||||
white-space: normal;
|
||||
|
|
@ -147,8 +155,8 @@ body::before {
|
|||
display: block;
|
||||
margin-top: 1px;
|
||||
max-width: 82px;
|
||||
font-size: 9px;
|
||||
line-height: 1.12;
|
||||
font-size: 9.7px;
|
||||
line-height: 1.13;
|
||||
color: rgba(88, 70, 48, 0.82);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -212,15 +220,15 @@ body::before {
|
|||
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-size: 8.2px;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.family-copy strong { font-size: 11px; }
|
||||
.family-copy span { color: var(--muted); font-size: 8.5px; margin-top: 1px; }
|
||||
.family-copy strong { font-size: 12.5px; }
|
||||
.family-copy span { color: var(--muted); font-size: 9.6px; margin-top: 1px; }
|
||||
|
||||
|
||||
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
|
||||
|
|
@ -440,7 +448,7 @@ body::before {
|
|||
width: 54px;
|
||||
padding: 3px 0;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.70);
|
||||
|
|
|
|||
312
index.html
312
index.html
|
|
@ -7,11 +7,11 @@
|
|||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.47" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.47" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.47" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.47" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.47" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.63" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.63" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.63" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.63" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.63" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -37,11 +37,11 @@
|
|||
<button id="speedBtn" class="btn">速度 x1</button>
|
||||
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
||||
<div id="soundMenu" class="sound-menu">
|
||||
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音設定 ON</button>
|
||||
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="効果音設定">
|
||||
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音 ON</button>
|
||||
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="音">
|
||||
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
|
||||
<div class="sound-panel-divider"></div>
|
||||
<label><input type="checkbox" data-sound-category="voice"><span>個体の声</span></label>
|
||||
<label><input type="checkbox" data-sound-category="voice"><span>たりないの声</span></label>
|
||||
<label><input type="checkbox" data-sound-category="notify" checked><span>通知</span></label>
|
||||
<label><input type="checkbox" data-sound-category="ops" checked><span>操作</span></label>
|
||||
</div>
|
||||
|
|
@ -60,6 +60,10 @@
|
|||
<button type="button" data-mobile-mode="camera">カメラ</button>
|
||||
<button type="button" data-mobile-mode="tool">道具</button>
|
||||
</div>
|
||||
<div id="mobileRotateControls" class="mobile-rotate-controls" hidden aria-label="物理アイテムの回転">
|
||||
<button type="button" data-mobile-rotate="left">左回転</button>
|
||||
<button type="button" data-mobile-rotate="right">右回転</button>
|
||||
</div>
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
</section>
|
||||
|
||||
|
|
@ -118,6 +122,7 @@
|
|||
<label><input type="checkbox" data-push-kind="food"> 食事</label>
|
||||
<label><input type="checkbox" data-push-kind="weather"> 天気</label>
|
||||
<label><input type="checkbox" data-push-kind="grass"> 草</label>
|
||||
<label><input type="checkbox" data-push-kind="event"> 出来事</label>
|
||||
<label><input type="checkbox" data-push-kind="relation"> 関係</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -130,7 +135,7 @@
|
|||
|
||||
<section class="card lineage-main-card">
|
||||
<div class="lineage-head">
|
||||
<h2>家系図</h2>
|
||||
<h2>家系図<span class="lineage-title-note">重い場合があるので注意</span></h2>
|
||||
<div class="lineage-actions">
|
||||
<button id="archiveUpdateToggleBtn" class="btn" type="button" aria-pressed="false">更新 OFF</button>
|
||||
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
||||
|
|
@ -196,141 +201,158 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.15.47" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.47" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.47" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.47" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.47" defer></script>
|
||||
<script src="js/data.js?v=39.15.47" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.47" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.47" defer></script>
|
||||
<script src="js/math.js?v=39.15.47" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.47" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/assets.js?v=39.15.47" defer></script>
|
||||
<script src="js/audio.js?v=39.15.47" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.47" defer></script>
|
||||
<script src="js/render.js?v=39.15.47" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.47" defer></script>
|
||||
<script src="js/structures.js?v=39.15.47" defer></script>
|
||||
<script src="js/items.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_runtime.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.47" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.47" defer></script>
|
||||
<script src="js/ants.js?v=39.15.47" defer></script>
|
||||
<script src="js/health.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_action_planner_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.47" defer></script>
|
||||
<script src="js/world.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.47" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.47" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_update_phases.js?v=39.15.47" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.47" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.47" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.47" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.47" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.47" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.47" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.47" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.47" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.47" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.47" defer></script>
|
||||
<script src="js/patch_helpers.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.47" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.47" defer></script>
|
||||
<script src="js/main.js?v=39.15.47" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.47" defer></script>
|
||||
<script src="js/version.js?v=39.15.63" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.63" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.63" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.63" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.63" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.63" defer></script>
|
||||
<script src="js/data.js?v=39.15.63" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.63" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.63" defer></script>
|
||||
<script src="js/math.js?v=39.15.63" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.63" defer></script>
|
||||
<script src="js/placement_collision_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.15.63" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/assets.js?v=39.15.63" defer></script>
|
||||
<script src="js/audio.js?v=39.15.63" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.63" defer></script>
|
||||
<script src="js/render.js?v=39.15.63" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.63" defer></script>
|
||||
<script src="js/structures.js?v=39.15.63" defer></script>
|
||||
<script src="js/items.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_runtime.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.63" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.63" defer></script>
|
||||
<script src="js/ants.js?v=39.15.63" defer></script>
|
||||
<script src="js/health.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_action_planner_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.63" defer></script>
|
||||
<script src="js/world.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.63" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.63" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.63" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_update_phases.js?v=39.15.63" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.63" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.63" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.63" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.63" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.63" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.63" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.63" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.63" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.63" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.63" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/patch_helpers.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.63" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.63" defer></script>
|
||||
<script src="js/main.js?v=39.15.63" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.63" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -161,6 +161,18 @@
|
|||
return Math.max(14, num(item.r, radiusFor(item.type, 12)) || radiusFor(item.type, 12));
|
||||
}
|
||||
|
||||
function isSoftPlacementIgnoredType(type = "") {
|
||||
const key = String(type || "");
|
||||
return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water";
|
||||
}
|
||||
|
||||
function placementRectsFor(worldRef, item) {
|
||||
const solid = worldRef?.solidObstacleRects?.(item) || [];
|
||||
if (solid.length) return solid;
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item?.type)) return global.TarinaiMechanicalSystem?.placementRects?.(item) || [];
|
||||
return solid;
|
||||
}
|
||||
|
||||
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
||||
if (!item || item.dead) return { hit: false, distance: Infinity };
|
||||
const padding = Math.max(0, num(opts.padding, 0));
|
||||
|
|
@ -188,24 +200,17 @@
|
|||
|
||||
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
||||
if (!worldRef || !item || item.dead) return true;
|
||||
const important = opts.important || new Set(["grass", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box", "water"]);
|
||||
const important = opts.important || new Set(["zunchi", "grass", "grass_bed", "water", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box"]);
|
||||
const isPhysicsItem = (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || item.type === "rope" || item.type === "rod" || (typeof isFenceItemType === "function" && isFenceItemType(item.type)));
|
||||
if (!important.has(item.type) && !isPhysicsItem) return false;
|
||||
const placingGrass = item.type === "grass";
|
||||
const itemRects = worldRef.solidObstacleRects?.(item) || [];
|
||||
const itemRects = placementRectsFor(worldRef, item);
|
||||
const itemRadius = itemFootprintRadius(worldRef, item, itemRects);
|
||||
const search = Math.max(132, itemRadius * 3.2);
|
||||
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
||||
if (!other || other === item || other.dead) continue;
|
||||
if (isPhysicsItem && (other.type === "water" || other.type === "grass_bed")) continue;
|
||||
if (other.type === "poison_block") {
|
||||
const body = global.TarinaiPhysicsBodySystem?.ensureBody?.(other, worldRef, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
if (!solid) continue;
|
||||
}
|
||||
if (isSoftPlacementIgnoredType(other.type)) continue;
|
||||
if (!important.has(other.type)) continue;
|
||||
if (!placingGrass && other.type === "grass") continue;
|
||||
const otherRects = worldRef.solidObstacleRects?.(other) || [];
|
||||
const otherRects = placementRectsFor(worldRef, other);
|
||||
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
||||
const hasMechanism = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || global.TarinaiMechanicalSystem?.isMechanicalType?.(other.type);
|
||||
const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || ""));
|
||||
|
|
@ -240,6 +245,8 @@
|
|||
rectsOverlap,
|
||||
rectCircleOverlap,
|
||||
itemFootprintRadius,
|
||||
isSoftPlacementIgnoredType,
|
||||
placementRectsFor,
|
||||
hitTestItem,
|
||||
itemPlacementOverlapBlocked,
|
||||
});
|
||||
|
|
|
|||
423
js/collision_response_system.js
Normal file
423
js/collision_response_system.js
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const ImpactCore = global.TarinaiImpactCoreSystem || {};
|
||||
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165;
|
||||
const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed;
|
||||
const itemHasAttachedLink = ImpactCore.itemHasAttachedLink;
|
||||
|
||||
function pairKey(a, b) {
|
||||
const aid = String(a?.id ?? "a");
|
||||
const bid = String(b?.id ?? "b");
|
||||
return aid < bid ? `${aid}:${bid}` : `${bid}:${aid}`;
|
||||
}
|
||||
|
||||
function normalizeVector(x, y, fallback = null) {
|
||||
const d = Math.hypot(Number(x) || 0, Number(y) || 0);
|
||||
if (d > 0.0001) return { x: x / d, y: y / d, d };
|
||||
if (fallback) return { x: fallback.x || 1, y: fallback.y || 0, d: 1 };
|
||||
return { x: 1, y: 0, d: 1 };
|
||||
}
|
||||
|
||||
function separatePair(worldRef, a, b, nx, ny, overlap, aWeight = 0.5, bWeight = 0.5) {
|
||||
const amount = Math.max(0, Number(overlap || 0) || 0);
|
||||
if (!worldRef || amount <= 0 || !a || !b) return false;
|
||||
a.x = clamp((a.x || 0) - nx * amount * aWeight, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
|
||||
a.y = clamp((a.y || 0) - ny * amount * aWeight, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
|
||||
b.x = clamp((b.x || 0) + nx * amount * bWeight, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
|
||||
b.y = clamp((b.y || 0) + ny * amount * bWeight, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function resolveBallBallCollisions(worldRef, dt) {
|
||||
if (!worldRef) return undefined;
|
||||
return (function resolveBallBallCollisionsUnified(dt) {
|
||||
if ((this.itemCounts?.ball || 0) < 2) return;
|
||||
const now = this.time || 0;
|
||||
if (!this.ballCollisionMemo) this.ballCollisionMemo = new Map();
|
||||
const balls = this.itemsOfType?.("ball") || (this.items || []).filter(it => it && !it.dead && it.type === "ball");
|
||||
for (const a of balls) {
|
||||
if (!a || a.dead || a.type !== "ball") continue;
|
||||
const ar = a.r || 18;
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
const aSpeed = Math.hypot(avx, avy);
|
||||
const range = ar * 2 + aSpeed * Math.max(0.016, dt || 0.016) + 52;
|
||||
for (const b of this.nearbyItems(a.x, a.y, range) || []) {
|
||||
if (!b || b === a || b.dead || b.type !== "ball") continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (now - (this.ballCollisionMemo.get(key) || -999) < 0.055) continue;
|
||||
const br = b.r || 18;
|
||||
const hitRadius = ar + br;
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
let hitX = a.x;
|
||||
let hitY = a.y;
|
||||
if (d > hitRadius) {
|
||||
const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x;
|
||||
const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y;
|
||||
const rvx = (a.x - ax0) - (b.x - bx0);
|
||||
const rvy = (a.y - ay0) - (b.y - by0);
|
||||
const rx = ax0 - bx0;
|
||||
const ry = ay0 - by0;
|
||||
const aa = rvx * rvx + rvy * rvy;
|
||||
if (aa <= 0.0001) continue;
|
||||
const bb = 2 * (rx * rvx + ry * rvy);
|
||||
const cc = rx * rx + ry * ry - hitRadius * hitRadius;
|
||||
const disc = bb * bb - 4 * aa * cc;
|
||||
if (disc < 0) continue;
|
||||
const u = clamp((-bb - Math.sqrt(disc)) / (2 * aa), 0, 1);
|
||||
const axHit = ax0 + (a.x - ax0) * u;
|
||||
const ayHit = ay0 + (a.y - ay0) * u;
|
||||
const bxHit = bx0 + (b.x - bx0) * u;
|
||||
const byHit = by0 + (b.y - by0) * u;
|
||||
dx = bxHit - axHit;
|
||||
dy = byHit - ayHit;
|
||||
d = Math.hypot(dx, dy);
|
||||
hitX = (axHit + bxHit) * 0.5;
|
||||
hitY = (ayHit + byHit) * 0.5;
|
||||
if (d > hitRadius + 0.5) continue;
|
||||
} else {
|
||||
hitX = (a.x + b.x) * 0.5;
|
||||
hitY = (a.y + b.y) * 0.5;
|
||||
}
|
||||
if (d < 0.001) {
|
||||
const rvx = (b.vx || 0) - (a.vx || 0);
|
||||
const rvy = (b.vy || 0) - (a.vy || 0);
|
||||
const rs = Math.hypot(rvx, rvy);
|
||||
if (rs > 0.001) { dx = rvx / rs; dy = rvy / rs; d = 1; }
|
||||
else { const ang = deterministicAngle(this, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
|
||||
}
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
const rvx = (b.vx || 0) - (a.vx || 0);
|
||||
const rvy = (b.vy || 0) - (a.vy || 0);
|
||||
const relNormal = rvx * nx + rvy * ny;
|
||||
if (relNormal > 0 && d >= hitRadius - 0.5) continue;
|
||||
this.ballCollisionMemo.set(key, now);
|
||||
const restitution = 0.86;
|
||||
const impulse = -(1 + restitution) * relNormal / 2;
|
||||
if (Number.isFinite(impulse)) {
|
||||
a.vx = (a.vx || 0) - impulse * nx;
|
||||
a.vy = (a.vy || 0) - impulse * ny;
|
||||
b.vx = (b.vx || 0) + impulse * nx;
|
||||
b.vy = (b.vy || 0) + impulse * ny;
|
||||
}
|
||||
const overlap = Math.max(0, hitRadius - d + 0.6);
|
||||
if (overlap > 0) {
|
||||
a.x -= nx * overlap * 0.5;
|
||||
a.y -= ny * overlap * 0.5;
|
||||
b.x += nx * overlap * 0.5;
|
||||
b.y += ny * overlap * 0.5;
|
||||
}
|
||||
const tangent = nx * ((a.vy || 0) - (b.vy || 0)) - ny * ((a.vx || 0) - (b.vx || 0));
|
||||
a.spinVelocity = clamp((a.spinVelocity || 0) - tangent / Math.max(14, ar) * 1.8, -38, 38);
|
||||
b.spinVelocity = clamp((b.spinVelocity || 0) + tangent / Math.max(14, br) * 1.8, -38, 38);
|
||||
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(13, hitRadius * 0.38), life: 0.16, color: "rgba(255,255,255,0.44)" }));
|
||||
}
|
||||
}
|
||||
|
||||
}).call(worldRef, dt);
|
||||
}
|
||||
|
||||
|
||||
function resolveBallInteractions(worldRef, dt) {
|
||||
if (!worldRef) return undefined;
|
||||
return (function resolveBallInteractionsUnified(dt) {
|
||||
if (!this.itemCounts?.ball) return;
|
||||
for (const ball of (this.itemsOfType ? this.itemsOfType("ball") : this.items)) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
const sweep = ballSpeed * Math.max(0.016, dt || 0.016) + 84;
|
||||
const range = (ball.r || 18) + 72 + sweep;
|
||||
const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x;
|
||||
const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y;
|
||||
const sx = ball.x - px;
|
||||
const sy = ball.y - py;
|
||||
const segLenSq = sx * sx + sy * sy;
|
||||
for (const t of this.nearbyTarinai(ball.x, ball.y, range)) {
|
||||
if (!t || t.dead || t.state === "sleep" || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
let hitX = ball.x;
|
||||
let hitY = ball.y;
|
||||
if (segLenSq > 1) {
|
||||
const u = clamp(((t.x - px) * sx + (t.y - py) * sy) / segLenSq, 0, 1);
|
||||
hitX = px + sx * u;
|
||||
hitY = py + sy * u;
|
||||
}
|
||||
let d = Math.max(0.001, distXY(hitX, hitY, t.x, t.y));
|
||||
const hitDistance = (ball.r || 18) + t.radius * 0.74;
|
||||
if (d > hitDistance) continue;
|
||||
const now = this.time || 0;
|
||||
const repeat = ball.lastKickerId === t.id && now - (ball.lastKickedAt || -999) < 0.18;
|
||||
if (repeat && ballSpeed < 300) continue;
|
||||
let nx = (hitX - t.x) / d;
|
||||
let ny = (hitY - t.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
if (ballSpeed > 0.01) {
|
||||
nx = (ball.vx || 0) / ballSpeed;
|
||||
ny = (ball.vy || 0) / ballSpeed;
|
||||
} else {
|
||||
nx = t.facingDir ? t.facingDir() : 1;
|
||||
ny = deterministicRange(this, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t);
|
||||
}
|
||||
}
|
||||
|
||||
const transferredPins = globalThis.TarinaiItemDynamicPinSystem?.transferBallPinsToTarinai?.(ball, t, this) || 0;
|
||||
if (transferredPins > 0) {
|
||||
ball.vx = (ball.vx || 0) * 0.72 - nx * Math.min(78, Math.max(18, ballSpeed * 0.06));
|
||||
ball.vy = (ball.vy || 0) * 0.72 - ny * Math.min(78, Math.max(18, ballSpeed * 0.06));
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) * 0.72 + (nx >= 0 ? -1 : 1) * 5.2, -38, 38);
|
||||
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(18, ball.r * 1.05), life: 0.20, color: "rgba(196,65,72,0.46)" }));
|
||||
if (this.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) this.log(`${t.name}にボールから鋲が移った。`, "accident", { participants: [t] });
|
||||
continue;
|
||||
}
|
||||
|
||||
const constrainedBall = itemHasAttachedLink(this, ball);
|
||||
const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
const moveVx = sx / frameDt;
|
||||
const moveVy = sy / frameDt;
|
||||
const effectiveVx = Math.abs(ball.vx || 0) + Math.abs(ball.vy || 0) > 0.1 ? (ball.vx || 0) : moveVx;
|
||||
const effectiveVy = Math.abs(ball.vx || 0) + Math.abs(ball.vy || 0) > 0.1 ? (ball.vy || 0) : moveVy;
|
||||
const incomingSpeed = -(effectiveVx * nx + effectiveVy * ny);
|
||||
const overlapPush = Math.max(0, hitDistance - d);
|
||||
const playIntentForPush = t.state === "play_ball" || t.target === ball;
|
||||
if (constrainedBall && !playIntentForPush && (incomingSpeed > 8 || overlapPush > 1.5)) {
|
||||
const pushImpulse = clamp(20 + incomingSpeed * 0.48 + overlapPush * 2.6, 20, 150);
|
||||
t.vx = clamp((t.vx || 0) - nx * pushImpulse + effectiveVx * 0.08, -360, 360);
|
||||
t.vy = clamp((t.vy || 0) - ny * pushImpulse + effectiveVy * 0.08, -360, 360);
|
||||
if (overlapPush > 0.4) {
|
||||
const sep = Math.min(14, overlapPush * 0.58 + 0.8);
|
||||
t.x = clamp((t.x || 0) - nx * sep, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
t.y = clamp((t.y || 0) - ny * sep, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
ball.x = clamp((ball.x || 0) + nx * sep * 0.28, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
ball.y = clamp((ball.y || 0) + ny * sep * 0.28, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
}
|
||||
ball.vx = clamp((ball.vx || effectiveVx) * 0.62 + nx * Math.min(42, pushImpulse * 0.22), -720, 720);
|
||||
ball.vy = clamp((ball.vy || effectiveVy) * 0.62 + ny * Math.min(42, pushImpulse * 0.22), -720, 720);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 4.6, -38, 38);
|
||||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
t.target = null;
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.16);
|
||||
this.markSpatialDirty?.("constrained-ball-push");
|
||||
continue;
|
||||
}
|
||||
|
||||
const isDangerous = ballSpeed >= 285;
|
||||
if (isDangerous && now - (t.lastBallDamageAt || -999) > 0.46) {
|
||||
const bvx = ballSpeed > 0.01 ? (ball.vx || 0) / ballSpeed : nx;
|
||||
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
|
||||
const damage = physicalDamageFromImpactSpeed(ballSpeed, { min: 3.5, max: 38, scale: 18 });
|
||||
t.lastBallDamageAt = now;
|
||||
this.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
|
||||
panic: true,
|
||||
target: { x: ball.x, y: ball.y },
|
||||
fearTimer: 0.95,
|
||||
thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
});
|
||||
this.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball });
|
||||
const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0);
|
||||
const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18);
|
||||
if (t.addStress) t.addStress(stressGain, { threshold: 8 });
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" });
|
||||
else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35);
|
||||
t.fallTimer = Math.max(t.fallTimer, damage > 14 ? 0.98 : 0.46);
|
||||
t.fallMax = Math.max(t.fallMax || 0.98, t.fallTimer);
|
||||
t.fallDir = bvx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer, 0.95);
|
||||
t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, damage > 14 ? 1.0 : 0.44);
|
||||
t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer);
|
||||
this.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8));
|
||||
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" }));
|
||||
if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] });
|
||||
ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10);
|
||||
ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relVx = (t.vx || 0) - (ball.vx || 0);
|
||||
const relVy = (t.vy || 0) - (ball.vy || 0);
|
||||
const relativeSpeed = Math.hypot(relVx, relVy);
|
||||
const playIntent = t.state === "play_ball" || t.target === ball;
|
||||
const playful = t.shouldApplyPersonalityBehavior?.("openness", 1);
|
||||
const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
|
||||
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
|
||||
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
|
||||
const kickedSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38);
|
||||
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
t.vx -= nx * Math.min(22, impulse * 0.10);
|
||||
t.vy -= ny * Math.min(22, impulse * 0.10);
|
||||
t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
||||
const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20;
|
||||
const sensitiveBump = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0;
|
||||
if (typeof applyNeedShock === "function" && sensitiveBump > playRelief) applyNeedShock(t, { safety: (sensitiveBump - playRelief) * 4 }); else if (typeof applyNeedRelief === "function") applyNeedRelief(t, { fulfill: -playRelief * 6, social: -playRelief * 2 });
|
||||
if (playful && playIntent) {
|
||||
t.loneliness = clamp(t.loneliness - 0.9, 0, 100);
|
||||
t.affection = clamp(t.affection + 0.18, 0, 100);
|
||||
}
|
||||
t.goodMode = playIntent || playful ? "smile" : t.goodMode;
|
||||
if (playIntent && now > (t.nextPlayBubbleAt || 0)) {
|
||||
t.nextPlayBubbleAt = now + deterministicRange(this, "ball-play-bubble-delay", 2.0, 3.8, ball, t);
|
||||
this.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)");
|
||||
}
|
||||
if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] });
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" }));
|
||||
}
|
||||
}
|
||||
|
||||
}).call(worldRef, dt);
|
||||
}
|
||||
|
||||
|
||||
function resolveTarinaiHighSpeedCollisions(worldRef, dt) {
|
||||
if (!worldRef) return undefined;
|
||||
return (function resolveTarinaiHighSpeedCollisionsUnified(dt) {
|
||||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
||||
const now = this.time || 0;
|
||||
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
|
||||
const live = Array.from(this.tarinai || [])
|
||||
.filter(t => t && !t.dead && !this.isTarinaiHiddenInNestBox(t))
|
||||
.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||
const candidates = [];
|
||||
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
|
||||
for (let i = 0; i < live.length; i += 1) {
|
||||
const a = live[i];
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
const aSpeed = Math.hypot(avx, avy);
|
||||
if (aSpeed < threshold) continue;
|
||||
const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const ax1 = Number.isFinite(a.x) ? a.x : ax0;
|
||||
const ay1 = Number.isFinite(a.y) ? a.y : ay0;
|
||||
const asx = ax1 - ax0;
|
||||
const asy = ay1 - ay0;
|
||||
for (let j = 0; j < live.length; j += 1) {
|
||||
const b = live[j];
|
||||
if (!b || b === a) continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
const bvx = b.vx || 0;
|
||||
const bvy = b.vy || 0;
|
||||
const relVx = avx - bvx;
|
||||
const relVy = avy - bvy;
|
||||
const relSpeed = Math.hypot(relVx, relVy);
|
||||
if (relSpeed < threshold) continue;
|
||||
const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x;
|
||||
const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y;
|
||||
const bx1 = Number.isFinite(b.x) ? b.x : bx0;
|
||||
const by1 = Number.isFinite(b.y) ? b.y : by0;
|
||||
const bsx = bx1 - bx0;
|
||||
const bsy = by1 - by0;
|
||||
const rx0 = ax0 - bx0;
|
||||
const ry0 = ay0 - by0;
|
||||
const rvx = asx - bsx;
|
||||
const rvy = asy - bsy;
|
||||
const relMoveSq = rvx * rvx + rvy * rvy;
|
||||
const u = relMoveSq > 0.001 ? clamp(-(rx0 * rvx + ry0 * rvy) / relMoveSq, 0, 1) : 1;
|
||||
const cx = rx0 + rvx * u;
|
||||
const cy = ry0 + rvy * u;
|
||||
const d = Math.max(0.001, Math.hypot(cx, cy));
|
||||
const hitDistance = (a.radius || 22) * 0.74 + (b.radius || 22) * 0.74;
|
||||
if (d > hitDistance) continue;
|
||||
const score = (1 - u) * 4 + Math.max(0, hitDistance - d) * 0.12 + Math.min(5, relSpeed / 120);
|
||||
candidates.push({ a, b, key, relSpeed, u, cx, cy, d, hitDistance, score });
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
candidates.sort((p, q) => (p.u - q.u) || (q.score - p.score) || String(p.key).localeCompare(String(q.key)));
|
||||
const maxImpacts = Math.max(8, Math.min(64, Number(this.maxTarinaiCollisionImpacts || 32) || 32));
|
||||
let solved = 0;
|
||||
for (const c of candidates) {
|
||||
if (solved >= maxImpacts) break;
|
||||
const { a, b, key, relSpeed, hitDistance } = c;
|
||||
if (!a || !b || a.dead || b.dead) continue;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
this.tarinaiCollisionMemo.set(key, now);
|
||||
solved += 1;
|
||||
const as = Math.hypot(a.vx || 0, a.vy || 0);
|
||||
const bs = Math.hypot(b.vx || 0, b.vy || 0);
|
||||
const impactor = as >= bs ? a : b;
|
||||
const target = impactor === a ? b : a;
|
||||
const ivx = impactor.vx || 0;
|
||||
const ivy = impactor.vy || 0;
|
||||
const impactSpeed = Math.hypot(ivx, ivy) || relSpeed || 1;
|
||||
let nx = impactSpeed > 0.001 ? ivx / impactSpeed : 0;
|
||||
let ny = impactSpeed > 0.001 ? ivy / impactSpeed : 0;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const dx = target.x - impactor.x;
|
||||
const dy = target.y - impactor.y;
|
||||
const dd = Math.hypot(dx, dy);
|
||||
if (dd > 0.001) { nx = dx / dd; ny = dy / dd; }
|
||||
else {
|
||||
const angle = (typeof stableUnit === "function" ? stableUnit(key, "tarinai-collision-normal") : 0.5) * Math.PI * 2;
|
||||
nx = Math.cos(angle);
|
||||
ny = Math.sin(angle);
|
||||
}
|
||||
}
|
||||
const overlap = Math.max(0, hitDistance - Math.hypot((a.x || 0) - (b.x || 0), (a.y || 0) - (b.y || 0)));
|
||||
if (overlap > 0.5) {
|
||||
const sep = Math.min(18, overlap * 0.45 + 0.8);
|
||||
impactor.x = clamp((impactor.x || 0) - nx * sep * 0.45, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
impactor.y = clamp((impactor.y || 0) - ny * sep * 0.45, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
target.x = clamp((target.x || 0) + nx * sep * 0.55, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
target.y = clamp((target.y || 0) + ny * sep * 0.55, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
this.markSpatialDirty?.("tarinai-highspeed-separation");
|
||||
}
|
||||
const damage = physicalDamageFromImpactSpeed(relSpeed, { min: 1.2, max: 22, scale: 26 });
|
||||
this.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
|
||||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b",
|
||||
});
|
||||
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
||||
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
||||
this.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor });
|
||||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target });
|
||||
for (const t of [target, impactor]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
|
||||
}
|
||||
this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" }));
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] });
|
||||
}
|
||||
if (solved && this.spatialDirty) this.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
|
||||
|
||||
}).call(worldRef, dt);
|
||||
}
|
||||
|
||||
|
||||
global.TarinaiCollisionResponseSystem = Object.freeze({
|
||||
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
|
||||
physicalDamageFromImpactSpeed,
|
||||
itemHasAttachedLink,
|
||||
pairKey,
|
||||
normalizeVector,
|
||||
separatePair,
|
||||
resolveBallBallCollisions,
|
||||
resolveBallInteractions,
|
||||
resolveTarinaiHighSpeedCollisions,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
95
js/colony_situation_system.js
Normal file
95
js/colony_situation_system.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: simulation/colony-situation
|
||||
// Owns colony mood/situation signal collection and priority arbitration.
|
||||
(function (global) {
|
||||
function avgOf(list, fn) { return list.length ? list.reduce((sum, t) => sum + (Number(fn(t)) || 0), 0) / list.length : 0; }
|
||||
function countOf(list, fn) { return list.reduce((sum, t) => sum + (fn(t) ? 1 : 0), 0); }
|
||||
function nearestAverage(alive) {
|
||||
if (alive.length < 2) return Infinity;
|
||||
let total = 0;
|
||||
for (const a of alive) {
|
||||
let best = Infinity;
|
||||
for (const b of alive) {
|
||||
if (a === b) continue;
|
||||
const dx = (a.x || 0) - (b.x || 0);
|
||||
const dy = (a.y || 0) - (b.y || 0);
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < best) best = d2;
|
||||
}
|
||||
if (Number.isFinite(best)) total += Math.sqrt(best);
|
||||
}
|
||||
return total / alive.length;
|
||||
}
|
||||
function collect(worldRef) {
|
||||
const now = worldRef?.time || 0;
|
||||
const alive = (worldRef?.tarinai || []).filter(t => t && !t.dead);
|
||||
const n = alive.length;
|
||||
const ratio = value => n ? value / n : 0;
|
||||
const panicCount = countOf(alive, t => t.state === "panic" || (t.fearTimer || 0) > 0.2 || (t.panicTargetUntil || 0) > now);
|
||||
const safetyHighCount = countOf(alive, t => (t.needs?.safety || 0) >= 52 || (t.fearTimer || 0) > 0.35);
|
||||
const weakCount = countOf(alive, t => (t.energy || 0) < 45 || (t.hunger || 0) > 88 || (t.needs?.health || 0) > 58);
|
||||
const diseasedCount = countOf(alive, t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease);
|
||||
const breedingCount = countOf(alive, t => (t.loveMochiTimer || 0) > 0.1 || t.state === "birth_ritual" || (t.birthRitualTimer || 0) > 0.1);
|
||||
const recentDeaths = (Array.isArray(worldRef?.recentDeathTimes) ? worldRef.recentDeathTimes : []).filter(t => now - (Number(t) || -999) <= 30).length;
|
||||
const nearestAvg = nearestAverage(alive);
|
||||
const fieldId = String(worldRef?.fieldType || "garden");
|
||||
const overcrowdLimit = fieldId === "cage" ? 20 : (fieldId === "park" ? 100 : 50);
|
||||
return {
|
||||
now,
|
||||
alive,
|
||||
n,
|
||||
avgEnergy: avgOf(alive, t => t.energy || 0),
|
||||
avgStress: avgOf(alive, t => t.stress || 0),
|
||||
avgHunger: avgOf(alive, t => t.hunger || 0),
|
||||
avgSafety: avgOf(alive, t => t.needs?.safety || 0),
|
||||
panicCount,
|
||||
safetyHighCount,
|
||||
weakCount,
|
||||
diseasedCount,
|
||||
breedingCount,
|
||||
recentDeaths,
|
||||
nearestAvg,
|
||||
recentShock: (now - (worldRef?.lastShootAt || -999) <= 8) || (now - (worldRef?.lastGenkotsuAt || -999) <= 8),
|
||||
panicRatio: ratio(panicCount),
|
||||
safetyRatio: ratio(safetyHighCount),
|
||||
weakRatio: ratio(weakCount),
|
||||
diseaseRatio: ratio(diseasedCount),
|
||||
breedingRatio: ratio(breedingCount),
|
||||
overcrowdLimit,
|
||||
denseCrowd: Number.isFinite(nearestAvg) && nearestAvg < 84 && n >= Math.max(8, Math.floor(overcrowdLimit * 0.55)),
|
||||
};
|
||||
}
|
||||
function choose(worldRef, m) {
|
||||
const candidates = [];
|
||||
const add = (id, priority, ok) => { if (ok) candidates.push({ id, priority }); };
|
||||
const devastationSignal = m.recentDeaths >= 3 || (m.n <= 2 && (worldRef?.deadCount || 0) > 0 && (m.recentDeaths > 0 || m.weakRatio >= 0.35 || m.panicRatio >= 0.20));
|
||||
const crisisExhaustionSignal = m.avgEnergy < 30 && m.weakRatio >= 0.48;
|
||||
add("devastation", 120, m.n <= 10 && devastationSignal);
|
||||
add("crisis", 110, m.recentDeaths >= 2 || crisisExhaustionSignal || (m.avgHunger > 92 && m.weakRatio >= 0.48));
|
||||
add("confusion", 100, (m.recentShock && (m.panicRatio >= 0.10 || m.safetyRatio >= 0.18)) || m.panicRatio >= 0.25);
|
||||
add("fearful", 90, (m.avgSafety >= 50 && m.safetyRatio >= 0.28) || (m.recentShock && m.safetyRatio >= 0.18));
|
||||
add("disease_spread", 82, m.diseaseRatio >= 0.25 || (m.diseasedCount >= 3 && m.diseaseRatio >= 0.16));
|
||||
add("weakened", 74, (m.avgEnergy < 54 && m.weakRatio >= 0.18) || m.avgHunger > 82 || m.weakRatio >= 0.34);
|
||||
add("breeding", 66, m.breedingRatio >= 0.10 || m.breedingCount >= 2 || m.now - (worldRef?.lastBirthAt || -999) <= 45);
|
||||
add("happy", 46, m.avgStress < 28 && m.panicRatio < 0.08 && m.diseaseRatio < 0.12 && m.weakRatio < 0.18);
|
||||
add("overcrowded", 34, m.n >= m.overcrowdLimit || m.denseCrowd);
|
||||
add("isolated", 28, m.n <= 1 || (m.n <= 3 && m.nearestAvg > Math.max(180, Math.min(worldRef?.w || 900, worldRef?.h || 600) * 0.32)));
|
||||
if (!candidates.length) return "relaxed";
|
||||
candidates.sort((a, b) => b.priority - a.priority);
|
||||
return candidates[0].id;
|
||||
}
|
||||
function evaluate(worldRef, force = false) {
|
||||
const now = worldRef?.time || 0;
|
||||
if (!worldRef) return { id: "relaxed", label: "安定", effects: { personality: {} } };
|
||||
if (!force && worldRef.colonyMood && now < (worldRef.nextColonyMoodEvalAt || 0)) return worldRef.colonyMood;
|
||||
const metrics = collect(worldRef);
|
||||
const id = choose(worldRef, metrics);
|
||||
const next = worldRef.colonyMoodDefinition?.(id) || { id, label: id, effects: { personality: {} } };
|
||||
worldRef.colonyMood = next;
|
||||
worldRef.colonySituationMetrics = metrics;
|
||||
worldRef.nextColonyMoodEvalAt = now + 1.25;
|
||||
return next;
|
||||
}
|
||||
global.TarinaiColonySituationSystem = Object.freeze({ collect, choose, evaluate });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -36,13 +36,6 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function constraintKick(move, dt, scale = 0.055, limit = 90) {
|
||||
// Constraint projection is primarily positional. Convert only a small,
|
||||
// capped part of the correction back to velocity so links do not inject
|
||||
// energy when the frame time spikes or endpoints start deeply overlapped.
|
||||
const step = Math.max(0.028, Math.min(0.055, Number(dt || 0.016) || 0.016));
|
||||
return clamp(move / step * scale, -limit, limit);
|
||||
}
|
||||
|
||||
function rectNearPointAabb(rect, x, y, padding = 0) {
|
||||
if (!rect) return false;
|
||||
|
|
@ -81,7 +74,8 @@
|
|||
item._lastConstraintSpatialDirtyY = y;
|
||||
item._lastConstraintSpatialDirtyR = r;
|
||||
item._nextConstraintSpatialDirtyAt = now + Math.max(0.016, Number(minInterval || 0.065) || 0.065);
|
||||
world.markSpatialDirty?.(reason);
|
||||
const marked = global.TarinaiPhysicsProjectionSystem?.markSpatialDirty?.(world, item, reason, threshold, minInterval) ?? false;
|
||||
if (!marked) world.markSpatialDirty?.(reason);
|
||||
if (world.constraintDirtyStatsThisFrame) world.constraintDirtyStatsThisFrame.marked = (world.constraintDirtyStatsThisFrame.marked || 0) + 1;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -327,6 +321,17 @@
|
|||
return { x: (obj.x || 0) + lx * c - ly * s, y: (obj.y || 0) + lx * s + ly * c, obj };
|
||||
}
|
||||
|
||||
|
||||
function endpointCanRemainAnchored(endpoint, point) {
|
||||
if (!endpoint || !point) return false;
|
||||
if (point.obj && !point.obj.dead) return true;
|
||||
// When a linked Tarinai dies, its live id is released before link cleanup.
|
||||
// Keep the affected endpoint as a frozen world-space anchor instead of
|
||||
// deleting the rope/rod item; otherwise unrelated links can be swept up by
|
||||
// the lifecycle pass that removes zero-amount link items.
|
||||
return endpoint.kind === "tarinai" && Number.isFinite(Number(point.x)) && Number.isFinite(Number(point.y));
|
||||
}
|
||||
|
||||
function endpointMass(obj) {
|
||||
if (!obj || obj.dead) return Infinity;
|
||||
if (obj._heldByPlayer || obj.playerHeld) return Infinity;
|
||||
|
|
@ -337,7 +342,7 @@
|
|||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
||||
if (obj.type === "rotator") return Infinity; // anchored pivot: links may torque it, not translate it
|
||||
if (obj.type === "reciprocator") return mechanicalPowered(obj) ? 18.0 : 5.8;
|
||||
if (obj.type === "poison_block") return Math.max(2.4, ps(obj, "mass", 4.8));
|
||||
if (obj.type === "poison_block") return Math.max(0.35, ps(obj, "mass", 0.45));
|
||||
return 8.0;
|
||||
}
|
||||
if (obj.type && (obj.type.includes("fence") || obj.type === "bed" || obj.type === "nest_box")) return obj.type.includes("fence") ? Infinity : 5.5;
|
||||
|
|
@ -361,13 +366,10 @@
|
|||
return changed;
|
||||
}
|
||||
if (obj.radius && obj.name) {
|
||||
obj.prevX = Number.isFinite(Number(obj.x)) ? Number(obj.x) : obj.prevX;
|
||||
obj.prevY = Number.isFinite(Number(obj.y)) ? Number(obj.y) : obj.prevY;
|
||||
obj.x += moveX;
|
||||
obj.y += moveY;
|
||||
const kickScale = mode === "rod" ? 0.020 : 0.052;
|
||||
const kickLimit = mode === "rod" ? 54 : 105;
|
||||
if (mode === "rod") { obj.vx = (obj.vx || 0) * 0.90; obj.vy = (obj.vy || 0) * 0.90; }
|
||||
obj.vx = clamp((obj.vx || 0) + constraintKick(moveX, dt, kickScale, kickLimit), -150, 150);
|
||||
obj.vy = clamp((obj.vy || 0) + constraintKick(moveY, dt, kickScale, kickLimit), -150, 150);
|
||||
if (obj.state === "sleep" || obj.sleeping) {
|
||||
global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(obj, dt, `constraint-${mode}`);
|
||||
obj._sleepExternalMotionUntil = Math.max(Number(obj._sleepExternalMotionUntil || 0) || 0, (Number(obj.world?.time || 0) || 0) + (mode === "rod" ? 1.1 : 0.6));
|
||||
|
|
@ -399,9 +401,6 @@
|
|||
obj.y = next.y;
|
||||
}
|
||||
const changed = Math.abs(ps(obj, "railPhase", 0) - beforePhase) > 0.0001;
|
||||
const current = ps(obj, "slideSpeed", 0);
|
||||
const kick = constraintKick(along, dt, mode === "rod" ? (powered ? 0.0025 : 0.0055) : (powered ? 0.0045 : 0.012), mode === "rod" ? (powered ? 12 : 26) : (powered ? 22 : 58));
|
||||
pset(obj, "slideSpeed", clamp(current * (powered ? 0.70 : (mode === "rod" ? 0.72 : 0.88)) + kick, -120, 120), "constraint-slide-project");
|
||||
if (changed) commitConstraintMovedMechanical(obj, "constraint-reciprocator-project");
|
||||
return changed;
|
||||
}
|
||||
|
|
@ -417,25 +416,6 @@
|
|||
obj.prevY = beforeY;
|
||||
obj.x = beforeX + px;
|
||||
obj.y = beforeY + py;
|
||||
const len = Math.max(0.001, Math.hypot(px, py));
|
||||
const nx = px / len;
|
||||
const ny = py / len;
|
||||
const vx = ps(obj, "xv", obj.vx || 0);
|
||||
const vy = ps(obj, "yv", obj.vy || 0);
|
||||
const intoCorrection = vx * nx + vy * ny;
|
||||
let nextVx = vx;
|
||||
let nextVy = vy;
|
||||
if (intoCorrection < 0) {
|
||||
nextVx -= nx * intoCorrection * 0.50;
|
||||
nextVy -= ny * intoCorrection * 0.50;
|
||||
}
|
||||
if (mode === "rod") { nextVx *= 0.86; nextVy *= 0.86; }
|
||||
nextVx += constraintKick(px, dt, mode === "rod" ? 0.006 : 0.012, mode === "rod" ? 20 : 36);
|
||||
nextVy += constraintKick(py, dt, mode === "rod" ? 0.006 : 0.012, mode === "rod" ? 20 : 36);
|
||||
pset(obj, "xv", clamp(nextVx, -165, 165), "constraint-poison-project");
|
||||
pset(obj, "yv", clamp(nextVy, -165, 165), "constraint-poison-project");
|
||||
obj.vx = ps(obj, "xv", obj.vx || 0);
|
||||
obj.vy = ps(obj, "yv", obj.vy || 0);
|
||||
commitConstraintMovedMechanical(obj, "constraint-poison-project");
|
||||
global.TarinaiMechanicalSystem?.wakeItem?.(obj, "constraint-poison-project");
|
||||
return true;
|
||||
|
|
@ -458,9 +438,6 @@
|
|||
obj.prevY = obj.y;
|
||||
obj.x += moveX;
|
||||
obj.y += moveY;
|
||||
if (mode === "rod") { obj.vx = (obj.vx || 0) * 0.90; obj.vy = (obj.vy || 0) * 0.90; }
|
||||
obj.vx = clamp((obj.vx || 0) + constraintKick(moveX, dt, mode === "rod" ? 0.016 : 0.042, mode === "rod" ? 42 : 78), -125, 125);
|
||||
obj.vy = clamp((obj.vy || 0) + constraintKick(moveY, dt, mode === "rod" ? 0.016 : 0.042, mode === "rod" ? 42 : 78), -125, 125);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -617,6 +594,8 @@
|
|||
}
|
||||
|
||||
function rectLocalPointForConstraint(r, x, y) {
|
||||
const shared = global.TarinaiCollisionFootprints?.rectLocalPoint;
|
||||
if (typeof shared === "function") return shared(r, x, y);
|
||||
const dx = x - (r.cx ?? 0);
|
||||
const dy = y - (r.cy ?? 0);
|
||||
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
|
||||
|
|
@ -664,7 +643,7 @@
|
|||
if (d < 0.001) { dx = 0; dy = -1; d = 1; }
|
||||
nx = dx / d; ny = dy / d; overlap = radius - d;
|
||||
}
|
||||
const push = Math.min(Math.max(0, overlap) + 0.35, Math.max(4, radius * 0.9));
|
||||
const push = Math.min(Math.max(0, overlap), Math.max(2, radius * 0.9));
|
||||
p.x += nx * push;
|
||||
p.y += ny * push;
|
||||
// Do not let collision projection become a full Verlet velocity on the next frame.
|
||||
|
|
@ -785,15 +764,10 @@
|
|||
if (d >= minD) continue;
|
||||
if (d < 0.001) { d = 1; }
|
||||
const nx = dx / d || 0, ny = dy / d || -1;
|
||||
const push = Math.min(minD - d + 0.4, Math.max(6, minD * 0.56));
|
||||
p.x += nx * push * 0.82;
|
||||
p.y += ny * push * 0.82;
|
||||
t.x -= nx * push * 0.10;
|
||||
t.y -= ny * push * 0.10;
|
||||
t.vx = clamp((t.vx || 0) - nx * 16, -160, 160);
|
||||
t.vy = clamp((t.vy || 0) - ny * 16, -160, 160);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.08);
|
||||
c = true;
|
||||
const push = Math.min(Math.max(0, minD - d), Math.max(3, minD * 0.48));
|
||||
p.x += nx * push;
|
||||
p.y += ny * push;
|
||||
c = push > 0.001 || c;
|
||||
}
|
||||
for (const rect of getRopeObstacleRects()) {
|
||||
if (rect?.item?.id && endpointIds.has(rect.item.id)) continue;
|
||||
|
|
@ -858,7 +832,7 @@
|
|||
item.world = worldRef || item.world || null;
|
||||
const a = endpointWorld(ep(item, 0), worldRef);
|
||||
const b = endpointWorld(ep(item, 1), worldRef);
|
||||
if (!a || !b || !a.obj || !b.obj) {
|
||||
if (!endpointCanRemainAnchored(ep(item, 0), a) || !endpointCanRemainAnchored(ep(item, 1), b)) {
|
||||
item.amount = 0;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
|
|
@ -870,7 +844,7 @@
|
|||
let changed = applyDistanceConstraint(item, a, b, dt, length, "rope");
|
||||
const aa = changed ? endpointWorld(ep(item, 0), worldRef) : a;
|
||||
const bb = changed ? endpointWorld(ep(item, 1), worldRef) : b;
|
||||
if (!aa || !bb || !aa.obj || !bb.obj) {
|
||||
if (!endpointCanRemainAnchored(ep(item, 0), aa) || !endpointCanRemainAnchored(ep(item, 1), bb)) {
|
||||
item.amount = 0;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
|
|
@ -893,7 +867,7 @@
|
|||
item.world = worldRef || item.world || null;
|
||||
const a = endpointWorld(ep(item, 0), worldRef);
|
||||
const b = endpointWorld(ep(item, 1), worldRef);
|
||||
if (!a || !b || !a.obj || !b.obj) {
|
||||
if (!endpointCanRemainAnchored(ep(item, 0), a) || !endpointCanRemainAnchored(ep(item, 1), b)) {
|
||||
item.amount = 0;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
|
|
@ -949,8 +923,13 @@
|
|||
|
||||
function endpointRuntimeStamp(endpoint, worldRef) {
|
||||
const obj = resolveEndpointObject(endpoint, worldRef);
|
||||
if (!obj || obj.dead) return { stamp: "missing", missing: true, obj: null };
|
||||
const q = (v, scale = 10) => Math.round((Number(v) || 0) * scale);
|
||||
if (!obj || obj.dead) {
|
||||
if (endpoint?.kind === "tarinai" && Number.isFinite(Number(endpoint.x)) && Number.isFinite(Number(endpoint.y))) {
|
||||
return { stamp: `tarinai-anchor:${endpoint.id || "?"}:${q(endpoint.x)}:${q(endpoint.y)}`, missing: false, obj: null };
|
||||
}
|
||||
return { stamp: "missing", missing: true, obj: null };
|
||||
}
|
||||
let stamp = `${endpoint?.kind || "item"}:${obj.id || "?"}:${obj.type || (obj.name ? "tarinai" : "entity")}:${q(obj.x)}:${q(obj.y)}`;
|
||||
if (obj.radius && obj.name) stamp += `:${q(obj.vx, 4)}:${q(obj.vy, 4)}:${obj.state || ""}`;
|
||||
else if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
||||
|
|
@ -1036,6 +1015,8 @@
|
|||
worldRef._constraintWorldStats = { visited: 0, ran: 0, skipped: 0, changed: 0, maxLinks, totalLinks: 0 };
|
||||
return 0;
|
||||
}
|
||||
// Rotate the starting point each frame so a large rope/rod set does not
|
||||
// starve low-score links that are always beyond the per-frame budget.
|
||||
const cursor = Math.max(0, Math.min(links.length - 1, Number(worldRef._constraintBudgetCursor || 0) || 0));
|
||||
const ordered = links.slice(cursor).concat(links.slice(0, cursor));
|
||||
let ran = 0;
|
||||
|
|
@ -1055,6 +1036,8 @@
|
|||
}
|
||||
pending.push({ item, score: linkScheduleScore(item, worldRef) });
|
||||
}
|
||||
// Active/strained links run first, but cursor rotation above preserves
|
||||
// fairness when maxLinks prevents solving every pending link.
|
||||
pending.sort((a, b) => (b.score - a.score) || ((a.item.id || 0) - (b.item.id || 0)));
|
||||
for (const entry of pending) {
|
||||
if (ran >= maxLinks) break;
|
||||
|
|
|
|||
45
js/deterministic_helpers.js
Normal file
45
js/deterministic_helpers.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: core/deterministic
|
||||
// Shared deterministic pseudo-random helpers used by behavior, item runtime,
|
||||
// and visual effects. Previously this bootstrap lived inline in several
|
||||
// modules; keeping it here prevents helper drift and load-order surprises.
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
global.TarinaiDeterministicHelpers = Object.freeze({ keyPart, hashUnit });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
// Keep the historical bare helper names available to legacy scripts while the
|
||||
// implementation lives on globalThis.
|
||||
var deterministicFrame = (typeof window !== "undefined" ? window : globalThis).deterministicFrame;
|
||||
var deterministicUnit = (typeof window !== "undefined" ? window : globalThis).deterministicUnit;
|
||||
var deterministicRange = (typeof window !== "undefined" ? window : globalThis).deterministicRange;
|
||||
var deterministicSigned = (typeof window !== "undefined" ? window : globalThis).deterministicSigned;
|
||||
var deterministicChance = (typeof window !== "undefined" ? window : globalThis).deterministicChance;
|
||||
var deterministicAngle = (typeof window !== "undefined" ? window : globalThis).deterministicAngle;
|
||||
|
|
@ -41,11 +41,11 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function restore(worldRef, entry, source) {
|
||||
function restore(worldRef, entry) {
|
||||
if (!worldRef || !entry?.snapshot || !global.TarinaiRestoreCoordinator?.restoreSnapshot) return false;
|
||||
worldRef._historyRestoring = true;
|
||||
try {
|
||||
global.TarinaiRestoreCoordinator.restoreSnapshot(cloneSnapshot(entry.snapshot), worldRef, { source, syncUi: true });
|
||||
global.TarinaiRestoreCoordinator.restoreSnapshot(cloneSnapshot(entry.snapshot), worldRef, { syncUi: true });
|
||||
worldRef._lastUndoSnapshotKey = snapshotKey(global.TarinaiSnapshot?.createSnapshot?.(worldRef));
|
||||
return true;
|
||||
} finally {
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
const prev = world._undoStack.pop();
|
||||
if (current) world._redoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "redo", time: world.time || 0 });
|
||||
if (world._redoStack.length > MAX_HISTORY) world._redoStack.splice(0, world._redoStack.length - MAX_HISTORY);
|
||||
const ok = restore(world, prev, "undo");
|
||||
const ok = restore(world, prev);
|
||||
return { ok, label: prev.label || "" };
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
const next = world._redoStack.pop();
|
||||
if (current) world._undoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "undo", time: world.time || 0 });
|
||||
if (world._undoStack.length > MAX_HISTORY) world._undoStack.splice(0, world._undoStack.length - MAX_HISTORY);
|
||||
const ok = restore(world, next, "redo");
|
||||
const ok = restore(world, next);
|
||||
return { ok, label: next.label || "" };
|
||||
}
|
||||
|
||||
|
|
|
|||
52
js/impact_core_system.js
Normal file
52
js/impact_core_system.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/impact-core
|
||||
// Shared impact constants and link-attachment helpers used by drop impacts,
|
||||
// ball collisions, and high-speed tarinai collisions.
|
||||
(function (global) {
|
||||
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = 165;
|
||||
|
||||
function physicalDamageFromImpactSpeed(speed = 0, { min = 0, max = 58, scale = 8.6 } = {}) {
|
||||
const v = Math.max(0, Number(speed) || 0);
|
||||
if (v < PHYSICAL_DAMAGE_SPEED_THRESHOLD) return 0;
|
||||
const raw = (v - PHYSICAL_DAMAGE_SPEED_THRESHOLD) / Math.max(1, scale);
|
||||
return clamp(raw, min, max);
|
||||
}
|
||||
|
||||
function linkEndpointMatchesItem(endpoint, item) {
|
||||
if (!endpoint || !item) return false;
|
||||
if (endpoint.kind && endpoint.kind !== "item") return false;
|
||||
if (endpoint.id && item.id && endpoint.id === item.id) return true;
|
||||
const dx = Number(endpoint.x) - Number(item.x || 0);
|
||||
const dy = Number(endpoint.y) - Number(item.y || 0);
|
||||
return Number.isFinite(dx) && Number.isFinite(dy) && Math.hypot(dx, dy) <= Math.max(14, (item.r || 18) * 0.75);
|
||||
}
|
||||
|
||||
function itemHasAttachedLink(worldRef, item) {
|
||||
if (!worldRef || !item || !item.id) return false;
|
||||
const pb = global.TarinaiPhysicsBodySystem || null;
|
||||
const links = [];
|
||||
if (worldRef.itemsOfType) {
|
||||
for (const linkType of ["rope", "rod"]) {
|
||||
for (const link of worldRef.itemsOfType(linkType) || []) if (link && !link.dead) links.push(link);
|
||||
}
|
||||
} else {
|
||||
for (const link of worldRef.items || []) if (link && !link.dead && (link.type === "rope" || link.type === "rod")) links.push(link);
|
||||
}
|
||||
for (const link of links) {
|
||||
const endpoints = [pb?.endpoint?.(link, 0), pb?.endpoint?.(link, 1)];
|
||||
const fallback = link.physicsConstraint?.endpoints || link.constraint?.endpoints || [];
|
||||
if (!endpoints[0] && fallback[0]) endpoints[0] = fallback[0];
|
||||
if (!endpoints[1] && fallback[1]) endpoints[1] = fallback[1];
|
||||
if (linkEndpointMatchesItem(endpoints[0], item) || linkEndpointMatchesItem(endpoints[1], item)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
global.TarinaiImpactCoreSystem = Object.freeze({
|
||||
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
|
||||
physicalDamageFromImpactSpeed,
|
||||
linkEndpointMatchesItem,
|
||||
itemHasAttachedLink,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
118
js/impact_response_system.js
Normal file
118
js/impact_response_system.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const ImpactCore = global.TarinaiImpactCoreSystem || {};
|
||||
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165;
|
||||
const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed;
|
||||
const itemHasAttachedLink = ImpactCore.itemHasAttachedLink;
|
||||
|
||||
function itemDropImpact(worldRef, it) {
|
||||
if (!worldRef) return undefined;
|
||||
return (function itemDropImpactUnified(it) {
|
||||
if (!it || it.dead || it.type === "water") return;
|
||||
const isStone = it.type === "stone";
|
||||
const isGenkotsu = it.type === "genkotsu";
|
||||
const dropFallHeight = isGenkotsu ? Math.max(360, (it.r || 88) * 5.8) : 170;
|
||||
const dropDuration = Math.max(0.08, Number(it.dropMax || 0.42) || 0.42);
|
||||
const impactSpeed = dropFallHeight / dropDuration;
|
||||
const radius = Math.max(32, Math.min(220, (it.r || 14) * 1.35 + impactSpeed * 0.055));
|
||||
const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 });
|
||||
const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260);
|
||||
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u306b\u6f70\u3055\u308c\u305f" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"));
|
||||
if (isGenkotsu) { this.lastGenkotsuAt = this.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
|
||||
let hit = 0;
|
||||
for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
|
||||
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
||||
if (p <= 0) continue;
|
||||
let nx = (t.x - it.x) / d;
|
||||
let ny = (t.y - it.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
t.vx += nx * push * (0.35 + p);
|
||||
t.vy += ny * push * (0.35 + p) - Math.min(210, push * 0.22) * p;
|
||||
const dealtDamage = damage * (0.35 + p * 0.65);
|
||||
if (dealtDamage > 0) t.damage(dealtDamage, reason);
|
||||
if (isGenkotsu) {
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: 18 + p * 30, health: 8 + p * 15 });
|
||||
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
|
||||
t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044";
|
||||
} else {
|
||||
t.thought = isStone ? "\u77f3\u304c\u843d\u3061\u3066\u304d\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b";
|
||||
}
|
||||
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));
|
||||
t.fallMax = Math.max(t.fallMax || t.fallTimer, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
hit += 1;
|
||||
}
|
||||
let ballHit = 0;
|
||||
if (isGenkotsu && this.itemCounts?.ball) {
|
||||
const ballRange = radius + Math.max(72, (it.r || 88) * 0.78);
|
||||
const balls = this.nearbyItems?.(it.x, it.y, ballRange + 42, true) || this.items || [];
|
||||
for (const ball of balls) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
const d = Math.max(1, distXY(ball.x, ball.y, it.x, it.y));
|
||||
const p = clamp(1 - d / (ballRange + (ball.r || 18)), 0, 1);
|
||||
if (p <= 0) continue;
|
||||
let nx = (ball.x - it.x) / d;
|
||||
let ny = (ball.y - it.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = deterministicAngle(this, "genkotsu-ball-impact-normal", it, ball);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
const ballPush = clamp(push * (0.44 + p * 0.86), 160, 1080);
|
||||
ball.prevX = Number.isFinite(ball.x) ? ball.x : (ball.prevX || 0);
|
||||
ball.prevY = Number.isFinite(ball.y) ? ball.y : (ball.prevY || 0);
|
||||
ball.vx = clamp((ball.vx || 0) + nx * ballPush, -1180, 1180);
|
||||
ball.vy = clamp((ball.vy || 0) + ny * ballPush - Math.min(180, ballPush * 0.12) * p, -1180, 1180);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (10 + p * 12), -48, 48);
|
||||
ball.lastPokedAt = this.time || 0;
|
||||
ballHit += 1;
|
||||
this.effects?.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" }));
|
||||
}
|
||||
if (ballHit > 0) this.markSpatialDirty?.("genkotsu-ball-impact");
|
||||
}
|
||||
if (damage > 0 && typeof damageNearbyStructures === "function") damageNearbyStructures(this, it.x, it.y, radius, damage, it);
|
||||
const antHit = damage > 0 ? this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
|
||||
push: p => push * (0.62 + p * 1.25),
|
||||
}) : 0;
|
||||
if (isGenkotsu) {
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
|
||||
this.blastLoosePinsFrom?.(it.x, it.y, radius + 92, it, 2.75, "genkotsu-impact");
|
||||
const existingEffects = (this.effects || []).length;
|
||||
const ringCount = existingEffects > 180 ? 2 : 3;
|
||||
const burstCount = existingEffects > 180 ? 4 : 7;
|
||||
for (let r = 0; r < ringCount; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
|
||||
for (let i = 0; i < burstCount; i++) this.effects.push(new Effect("fight", it.x + deterministicRange(this, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(this, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(this, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(this, "genkotsu-impact-effect-life", 0.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
|
||||
} else {
|
||||
this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" }));
|
||||
}
|
||||
if (hit > 0 || antHit > 0 || isGenkotsu) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
|
||||
if (isGenkotsu) audio.genkotsuImpact?.();
|
||||
else if (isStone) audio.stoneImpact?.();
|
||||
else if (hit > 0 || antHit > 0) audio.poke?.();
|
||||
else audio.drop?.();
|
||||
if (hit > 0 || antHit > 0) {
|
||||
const label = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3" : "\u9053\u5177");
|
||||
const victims = [];
|
||||
if (hit > 0) victims.push(`${hit}\u5339\u306e\u305f\u308a\u306a\u3044`);
|
||||
if (antHit > 0) victims.push(`${antHit}\u5339\u306e\u30a2\u30ea`);
|
||||
this.log(`${label}\u304c\u843d\u3061\u3001${victims.join("\u3068")}\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident");
|
||||
}
|
||||
|
||||
}).call(worldRef, it);
|
||||
}
|
||||
|
||||
global.TarinaiImpactResponseSystem = Object.freeze({
|
||||
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
|
||||
physicalDamageFromImpactSpeed,
|
||||
itemDropImpact,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -8,12 +8,6 @@
|
|||
constructor() {
|
||||
this.storageKey = STORAGE_KEY;
|
||||
this.currentMode = this.loadMode();
|
||||
this.gestureState = {
|
||||
active: false,
|
||||
kind: "",
|
||||
startedAt: 0,
|
||||
pointerCount: 0,
|
||||
};
|
||||
this.classification = this.classify();
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +23,6 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -67,54 +60,15 @@
|
|||
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 true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@
|
|||
if (item.y < p) { item.y = p; item.vy = Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; }
|
||||
if (item.y > worldRef.h - p) { item.y = worldRef.h - p; item.vy = -Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; }
|
||||
resolveObstacleCollisions(item, dt, worldRef);
|
||||
global.TarinaiItemDynamicPinSystem?.tryBallPickup?.(item, worldRef);
|
||||
item.x = clamp(item.x, p, worldRef.w - p);
|
||||
item.y = clamp(item.y, p, worldRef.h - p);
|
||||
const groundFriction = Math.pow(0.72, dt);
|
||||
item.vx *= groundFriction;
|
||||
item.vy *= groundFriction;
|
||||
}
|
||||
global.TarinaiItemDynamicPinSystem?.tryBallPickup?.(item, worldRef);
|
||||
item.spin = (item.spin || 0) + (item.spinVelocity || 0) * dt + (item.vx || 0) * dt / Math.max(8, item.r || 18);
|
||||
item.spinVelocity *= Math.pow(0.65, dt);
|
||||
if (Math.hypot(item.vx || 0, item.vy || 0) < 0.18) { item.vx = 0; item.vy = 0; }
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/items/dynamic-system
|
||||
|
|
@ -35,7 +6,121 @@
|
|||
// behavior. Prototype methods delegate here.
|
||||
(function (global) {
|
||||
function isPin(item) {
|
||||
return Boolean(item && typeof global.isPinType === "function" && global.isPinType(item.type));
|
||||
return Boolean(global.TarinaiPinAttachmentSystem?.isPin?.(item) || (item && typeof global.isPinType === "function" && global.isPinType(item.type)));
|
||||
}
|
||||
|
||||
function ballById(worldRef, id) {
|
||||
if (!worldRef || !id) return null;
|
||||
return (worldRef.items || []).find(it => it && !it.dead && it.type === "ball" && it.id === id) || null;
|
||||
}
|
||||
|
||||
function removeFromBall(item, worldRef = null) {
|
||||
const ball = ballById(worldRef, item?.pinBallId) || null;
|
||||
if (ball?.stuckPinIds) ball.stuckPinIds = ball.stuckPinIds.filter(id => id !== item.id);
|
||||
if (ball && ball.stuckPinId === item?.id) ball.stuckPinId = "";
|
||||
if (item) item.pinBallId = "";
|
||||
return ball;
|
||||
}
|
||||
|
||||
function updateBallLodged(item, dt, worldRef) {
|
||||
const ball = ballById(worldRef, item.pinBallId);
|
||||
if (!ball) {
|
||||
detach(item, worldRef, "ball-lost");
|
||||
return;
|
||||
}
|
||||
if (global.TarinaiPinAttachmentSystem?.applyBallPose?.(item, ball, worldRef)) return;
|
||||
const angle = (ball.spin || 0) + (item.pinBallOffsetAngle || 0);
|
||||
const distance = Math.max(4, Number(item.pinBallOffsetDistance || (ball.r || 18) * 0.55) || (ball.r || 18) * 0.55);
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.x = ball.x + Math.cos(angle) * distance;
|
||||
item.y = ball.y + Math.sin(angle) * distance;
|
||||
item.vx = ball.vx || 0;
|
||||
item.vy = ball.vy || 0;
|
||||
item.pinVisualSide = Math.cos(angle) >= 0 ? 1 : -1;
|
||||
item.spin = angle + (item.type === "oshibyo" ? 0.18 : Math.PI * 0.5);
|
||||
item.deletable = false;
|
||||
if (Number.isFinite(ball.spinVelocity)) item.spinVelocity = ball.spinVelocity;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
}
|
||||
|
||||
function attachToBall(item, ball, worldRef) {
|
||||
if (!isPin(item) || !ball || ball.dead || ball.type !== "ball" || item.dead) return false;
|
||||
if (item.pinState === "lodged" || item.pinState === "ball_lodged") return false;
|
||||
const now = worldRef?.time || 0;
|
||||
if ((item.noStickUntil || 0) > now) return false;
|
||||
const angle = deterministicAngle(worldRef, "pin-ball-stick-angle", item, ball, now);
|
||||
const dist = Math.max(4, (ball.r || 18) * deterministicRange(worldRef, "pin-ball-stick-distance", 0.24, 0.78, item, ball, now));
|
||||
item.pinState = "ball_lodged";
|
||||
item.pinBallId = ball.id;
|
||||
item.pinTargetId = "";
|
||||
item.pinBallOffsetAngle = angle - (ball.spin || 0);
|
||||
item.pinBallOffsetDistance = dist;
|
||||
item.pinVisualSide = Math.cos(angle) >= 0 ? 1 : -1;
|
||||
item.deletable = false;
|
||||
item.noStickUntil = now + 0.16;
|
||||
ball.stuckPinIds = Array.isArray(ball.stuckPinIds) ? ball.stuckPinIds : [];
|
||||
if (!ball.stuckPinIds.includes(item.id)) ball.stuckPinIds.push(item.id);
|
||||
ball.stuckPinId = ball.stuckPinIds[0] || item.id;
|
||||
updateBallLodged(item, 0, worldRef);
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(12, (item.r || 8) * 1.4), life: 0.16, color: item.type === "oshibyo" ? "rgba(73,119,205,0.42)" : "rgba(214,72,72,0.42)" }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFromBallToTarinai(item, ball, t, worldRef) {
|
||||
if (!item || item.dead || item.pinState !== "ball_lodged" || !ball || !t || t.dead) return false;
|
||||
const now = worldRef?.time || 0;
|
||||
if ((item._ballPinTransferredAt || -999) + 0.18 > now) return false;
|
||||
removeFromBall(item, worldRef);
|
||||
item.pinState = "loose";
|
||||
item.pinTargetId = "";
|
||||
item.noStickUntil = 0;
|
||||
item.noStickTargetId = "";
|
||||
item.noStickTargetUntil = 0;
|
||||
item.x = ball.x + deterministicRange(worldRef, "pin-transfer-x", -(ball.r || 18) * 0.25, (ball.r || 18) * 0.25, item, ball, t);
|
||||
item.y = ball.y + deterministicRange(worldRef, "pin-transfer-y", -(ball.r || 18) * 0.25, (ball.r || 18) * 0.25, item, ball, t);
|
||||
item._ballPinTransferredAt = now;
|
||||
const ok = attach(item, t, worldRef, distXY(item.x, item.y, t.x, t.y));
|
||||
if (!ok) attachToBall(item, ball, worldRef);
|
||||
return ok;
|
||||
}
|
||||
|
||||
function transferBallPinsToTarinai(ball, t, worldRef) {
|
||||
if (!ball || !t || t.dead) return 0;
|
||||
const ids = Array.isArray(ball.stuckPinIds) ? [...ball.stuckPinIds] : (ball.stuckPinId ? [ball.stuckPinId] : []);
|
||||
let moved = 0;
|
||||
for (const id of ids) {
|
||||
const pin = (worldRef?.items || []).find(it => it && !it.dead && it.id === id && isPin(it));
|
||||
if (pin && transferFromBallToTarinai(pin, ball, t, worldRef)) moved += 1;
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
function tryBallPickup(ball, worldRef) {
|
||||
if (!ball || ball.dead || ball.type !== "ball" || !worldRef?.nearbyItems) return 0;
|
||||
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
const range = (ball.r || 18) + speed * Math.max(0.016, Number(worldRef.dt || 0.016) || 0.016) + 28;
|
||||
const candidates = worldRef.nearbyItems(ball.x, ball.y, range) || [];
|
||||
let picked = 0;
|
||||
const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x;
|
||||
const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y;
|
||||
const sx = ball.x - px;
|
||||
const sy = ball.y - py;
|
||||
const len2 = sx * sx + sy * sy;
|
||||
for (const pin of candidates) {
|
||||
if (!isPin(pin) || pin.dead || pin.pinState === "lodged" || pin.pinState === "ball_lodged") continue;
|
||||
let hitX = ball.x;
|
||||
let hitY = ball.y;
|
||||
if (len2 > 1) {
|
||||
const u = clamp(((pin.x - px) * sx + (pin.y - py) * sy) / len2, 0, 1);
|
||||
hitX = px + sx * u;
|
||||
hitY = py + sy * u;
|
||||
}
|
||||
const hit = (ball.r || 18) + Math.max(5, pin.r || 8) + 2;
|
||||
if (distXY(hitX, hitY, pin.x, pin.y) > hit) continue;
|
||||
if (attachToBall(pin, ball, worldRef)) picked += 1;
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
function update(item, dt, worldRef) {
|
||||
|
|
@ -45,8 +130,13 @@
|
|||
updateLodged(item, dt, worldRef);
|
||||
return true;
|
||||
}
|
||||
if (item.pinState === "ball_lodged") {
|
||||
updateBallLodged(item, dt, worldRef);
|
||||
return true;
|
||||
}
|
||||
if (global.TarinaiPhysics?.applyKinematicItemMotion) {
|
||||
global.TarinaiPhysics.applyKinematicItemMotion(item, worldRef, dt, { padding: 26, bounce: 0.44, spinBounce: -0.68, frictionBase: 0.18, frictionRate: 0.85, spinFrictionBase: 0.38, spinRestDamp: 0.08, stopSpeed: 0.05, zeroBelow: 7.5 });
|
||||
global.TarinaiItemDynamicBallSystem?.resolveObstacleCollisions?.(item, dt, worldRef);
|
||||
} else {
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
|
|
@ -77,6 +167,17 @@
|
|||
return attach(item, best, worldRef, bestD);
|
||||
}
|
||||
|
||||
function oshibyoAnchorFor(t, visualSide, item = { type: "oshibyo" }) {
|
||||
return global.TarinaiPinAttachmentSystem?.tarinaiAnchor?.(item, t, { visualSide }) || {
|
||||
x: t.x + visualSide * (t?.radius || 16) * 0.96,
|
||||
y: t.y + (t?.radius || 16) * 0.50,
|
||||
angle: visualSide * 0.48,
|
||||
distance: (t?.radius || 16) * 0.96,
|
||||
offsetY: (t?.radius || 16) * 0.48,
|
||||
visualSide,
|
||||
};
|
||||
}
|
||||
|
||||
function attach(item, t, worldRef, d = null) {
|
||||
if (!t || t.dead) return false;
|
||||
if (t.stuckPushpinId && t.stuckPushpinId !== item.id) return false;
|
||||
|
|
@ -88,12 +189,16 @@
|
|||
item.pinState = "lodged";
|
||||
item.deletable = false;
|
||||
item.pinTargetId = t.id;
|
||||
removeFromBall(item, worldRef);
|
||||
const sharedAnchor = global.TarinaiPinAttachmentSystem?.tarinaiAnchor?.(item, t, { dx, dy, distance: distToTarget });
|
||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
const visualSide = isOshibyo ? -1 : faceDir;
|
||||
const faceSide = faceDir >= 0 ? 1 : -1;
|
||||
const visualSide = sharedAnchor?.visualSide ?? (isOshibyo ? -faceSide : faceSide);
|
||||
item.pinVisualSide = visualSide;
|
||||
item.pinAttachAngle = isOshibyo ? -0.46 : Math.atan2(dy || -1, dx || visualSide);
|
||||
item.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.88 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
|
||||
item.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.40 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
||||
const oshibyoAnchor = isOshibyo ? (sharedAnchor || oshibyoAnchorFor(t, visualSide, item)) : null;
|
||||
item.pinAttachAngle = isOshibyo ? oshibyoAnchor.angle : (sharedAnchor?.angle ?? Math.atan2(dy || -1, dx || visualSide));
|
||||
item.pinAttachDistance = isOshibyo ? oshibyoAnchor.distance : (sharedAnchor?.distance ?? clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74));
|
||||
item.pinOffsetY = isOshibyo ? oshibyoAnchor.offsetY : (sharedAnchor?.offsetY ?? clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38));
|
||||
item.pinDamageTick = 0;
|
||||
item.pinFallCheckTimer = 0;
|
||||
item.vx = 0;
|
||||
|
|
@ -102,8 +207,8 @@
|
|||
item.spin = item.pinAttachAngle;
|
||||
t.stuckPushpinId = item.id;
|
||||
t.sleeping = false;
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, isOshibyo ? 0.35 : 0.8);
|
||||
if (!isOshibyo) t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
||||
if (!isOshibyo) t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8);
|
||||
if (isOshibyo) {
|
||||
t.thought = "\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u3066\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u3063\u305f";
|
||||
worldRef.log?.(`${t.name}\u306b\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
|
|
@ -118,7 +223,7 @@
|
|||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + deterministicRange(worldRef, "pin-panic-target-x", -80, 80, item, t), y: t.y + deterministicRange(worldRef, "pin-panic-target-y", -80, 80, item, t) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 });
|
||||
}
|
||||
if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "\u753b\u92f2");
|
||||
if (!isOshibyo && t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "\u753b\u92f2");
|
||||
if ((worldRef.time || 0) >= (item.pinLogAt || -999) + 1.2) {
|
||||
item.pinLogAt = worldRef.time || 0;
|
||||
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
||||
|
|
@ -137,16 +242,21 @@
|
|||
const isOshibyo = Boolean(behavior?.blocksZunchi);
|
||||
t.stuckPushpinId = item.id;
|
||||
item.deletable = false;
|
||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
const visualSide = isOshibyo ? -1 : faceDir;
|
||||
item.pinVisualSide = visualSide;
|
||||
item.x = isOshibyo ? t.x - (t.radius || 16) * 0.88 : t.x + visualSide * (t.radius || 16) * 0.36;
|
||||
item.y = isOshibyo ? t.y + (t.radius || 16) * 0.42 : t.y - (t.radius || 16) * 0.04;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.spin = isOshibyo ? -0.52 : visualSide * 0.28;
|
||||
item.vx = t.vx || 0;
|
||||
item.vy = t.vy || 0;
|
||||
const sharedPose = global.TarinaiPinAttachmentSystem?.applyTarinaiPose?.(item, t, worldRef);
|
||||
if (!sharedPose) {
|
||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
const faceSide = faceDir >= 0 ? 1 : -1;
|
||||
const visualSide = isOshibyo ? -faceSide : faceSide;
|
||||
item.pinVisualSide = visualSide;
|
||||
const oshibyoAnchor = isOshibyo ? oshibyoAnchorFor(t, visualSide, item) : null;
|
||||
item.x = isOshibyo ? oshibyoAnchor.x : t.x + visualSide * (t.radius || 16) * 0.36;
|
||||
item.y = isOshibyo ? oshibyoAnchor.y : t.y - (t.radius || 16) * 0.04;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.spin = isOshibyo ? oshibyoAnchor.angle : visualSide * 0.28;
|
||||
item.vx = t.vx || 0;
|
||||
item.vy = t.vy || 0;
|
||||
}
|
||||
if (isOshibyo) {
|
||||
if ((t.oshiriByoZunchiStock || 0) >= 6 && t.state !== "eat" && t.state !== "sleep") t.thought = "\u304a\u3057\u308a\u92f2\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044";
|
||||
return;
|
||||
|
|
@ -154,7 +264,7 @@
|
|||
item.pinDamageTick = (item.pinDamageTick || 0) + dt;
|
||||
while (item.pinDamageTick >= 0.55) {
|
||||
item.pinDamageTick -= 0.55;
|
||||
if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "\u753b\u92f2");
|
||||
if (!isOshibyo && t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "\u753b\u92f2");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
|
||||
if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 });
|
||||
if (deterministicChance(worldRef, "pin-pain-bubble", 0.22, item, t)) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
|
||||
|
|
@ -178,6 +288,7 @@
|
|||
}
|
||||
|
||||
function detach(item, worldRef, reason = "released") {
|
||||
const carriedBall = removeFromBall(item, worldRef);
|
||||
const t = worldRef?.liveTarinaiById?.(item.pinTargetId) || null;
|
||||
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
|
||||
const wasOshibyo = Boolean(behavior?.blocksZunchi);
|
||||
|
|
@ -204,22 +315,51 @@
|
|||
item.vy = 0;
|
||||
item.spinVelocity = 0;
|
||||
} else {
|
||||
item.vx = deterministicRange(worldRef, "pin-detach-vx", -24, 24, item, t, reason);
|
||||
item.vy = deterministicRange(worldRef, "pin-detach-vy", -10, 18, item, t, reason);
|
||||
item.spinVelocity = deterministicRange(worldRef, "pin-detach-spin-velocity", -1.6, 1.6, item, t, reason);
|
||||
item.spin += deterministicRange(worldRef, "pin-detach-spin", -0.25, 0.25, item, t, reason);
|
||||
let nx = 0;
|
||||
let ny = 1;
|
||||
if (t) {
|
||||
item.x = clamp(t.x + deterministicRange(worldRef, "pin-detach-x", -(t.radius || 16) * 0.75, (t.radius || 16) * 0.75, item, t, reason), CONFIG.worldPadding || 30, (worldRef?.w || item.x) - (CONFIG.worldPadding || 30));
|
||||
item.y = clamp(t.y + deterministicRange(worldRef, "pin-detach-y", (t.radius || 16) * 0.12, (t.radius || 16) * 0.72, item, t, reason), CONFIG.worldPadding || 30, (worldRef?.h || item.y) - (CONFIG.worldPadding || 30));
|
||||
const dx = Number(item.x || 0) - Number(t.x || 0);
|
||||
const dy = Number(item.y || 0) - Number(t.y || 0);
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 0.001) { nx = dx / d; ny = dy / d; }
|
||||
else { nx = Math.sign(item.pinVisualSide || 0) || (t.facingDir?.() || 1); ny = 0.28; }
|
||||
const len = Math.hypot(nx, ny) || 1;
|
||||
nx /= len; ny /= len;
|
||||
const dropR = Math.max(8, (item.r || 8) + (t.radius || 16) * 0.58);
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
item.x = clamp((t.x || 0) + nx * dropR, pad, Math.max(pad, (worldRef?.w || item.x) - pad));
|
||||
item.y = clamp((t.y || 0) + ny * dropR + (wasOshibyo ? 4 : 0), pad, Math.max(pad, (worldRef?.h || item.y) - pad));
|
||||
} else if (carriedBall) {
|
||||
const dx = Number(item.x || 0) - Number(carriedBall.x || 0);
|
||||
const dy = Number(item.y || 0) - Number(carriedBall.y || 0);
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
nx = dx / d;
|
||||
ny = dy / d;
|
||||
}
|
||||
const baseVx = Number(carriedBall?.vx || t?.vx || 0) || 0;
|
||||
const baseVy = Number(carriedBall?.vy || t?.vy || 0) || 0;
|
||||
item.vx = clamp(baseVx * 0.45 + nx * 18, -90, 90);
|
||||
item.vy = clamp(baseVy * 0.45 + Math.max(12, ny * 18), -60, 110);
|
||||
item.spinVelocity = clamp((item.spinVelocity || 0) * 0.35 + (Math.sign(nx) || 1) * 0.9, -2.2, 2.2);
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, (item.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] });
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}の画鋲が抜け落ちた。`, "observe", { participants: [t] });
|
||||
}
|
||||
|
||||
if (wasOshibyo && storedZunchi > 0 && worldRef?.spawnBurstZunchi) {
|
||||
worldRef.spawnBurstZunchi(t || item, storedZunchi, item);
|
||||
}
|
||||
}
|
||||
|
||||
global.TarinaiItemDynamicPinSystem = Object.freeze({ update, isPin, tryStick, attach, updateLodged, detach });
|
||||
global.TarinaiItemDynamicPinSystem = Object.freeze({
|
||||
update,
|
||||
isPin,
|
||||
tryStick,
|
||||
attach,
|
||||
updateLodged,
|
||||
detach,
|
||||
attachToBall,
|
||||
transferFromBallToTarinai,
|
||||
transferBallPinsToTarinai,
|
||||
tryBallPickup,
|
||||
updateBallLodged,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
282
js/item_effect_definitions.js
Normal file
282
js/item_effect_definitions.js
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
"use strict";
|
||||
|
||||
// Item effect behavior, timers, and display metadata.
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
laxative: {
|
||||
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: "\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: "\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: "\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: "\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: "\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: "\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: "\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: "\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: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"],
|
||||
},
|
||||
mystery_drug: {
|
||||
label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"],
|
||||
},
|
||||
zunda_juice: {
|
||||
label: "\u305a\u3093\u3060\u6c41", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"],
|
||||
},
|
||||
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 + deterministicRange(tarinai.world, "love-mochi-duration", 4, 12, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 });
|
||||
if (context.source === "eating") {
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.fight_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + deterministicRange(tarinai.world, "fight-mochi-duration", 10, 18, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(tarinai, { safety: context.source === "eating" ? nutrition * 0.5 : 16 });
|
||||
if (context.source === "eating") {
|
||||
tarinai.fearTimer = Math.max(tarinai.fearTimer || 0, 0.32);
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.sleep_drug, {
|
||||
onApply(tarinai, context = {}) {
|
||||
if (context.source === "eating") {
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -nutrition * 0.25, sleep: -nutrition * 0.15 });
|
||||
tarinai.energy = clamp(tarinai.energy - nutrition * 0.20, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100));
|
||||
tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 0.18);
|
||||
tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 2.5);
|
||||
const baseChance = Math.min(0.98, 0.35 + nutrition * 0.075);
|
||||
const sleepChance = tarinai.diseaseChance ? tarinai.diseaseChance(baseChance) : baseChance;
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
if (!tarinai.sleepDisease && deterministicChance(tarinai.world, "sleep-drug-disease", Math.min(0.98, sleepChance), tarinai, context.item)) return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
return true;
|
||||
}
|
||||
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 extends global.TarinaiRegistryBase {
|
||||
label(id = "") { return super.label(id, id || "\u4e0d\u660e"); }
|
||||
color(id = "") { return super.color(id, "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 super.labels(); }
|
||||
colors() { return super.colors(); }
|
||||
randomPool() {
|
||||
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); }
|
||||
debugSnapshot() {
|
||||
return [...this.defs.entries()].map(([id, def]) => ({ id, label: def.label, kind: def.kind, group: def.exclusiveGroup || "" }));
|
||||
}
|
||||
}
|
||||
|
||||
const registry = new EffectRegistry(rawDefinitions);
|
||||
global.TarinaiEffectRegistry = registry;
|
||||
global.TarinaiEffectDefinitions = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
59
js/item_food_definitions.js
Normal file
59
js/item_food_definitions.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"use strict";
|
||||
|
||||
// Food and medicine consumption metadata.
|
||||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0, hygienePenalty: 0.055 },
|
||||
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82, hygienePenalty: 0.055, cures: ["disease_explosion", "disease_fight"] },
|
||||
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "love_mochi" },
|
||||
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "fight_mochi" },
|
||||
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, 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: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "protein" },
|
||||
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "niteropu" },
|
||||
ammo: { label: "\u5f3e\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "ammo" },
|
||||
mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mystery_drug" },
|
||||
mercury: { label: "\u6c34\u9280", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mercury" },
|
||||
giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "giant_drug" },
|
||||
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" },
|
||||
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true },
|
||||
grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water: { label: "\u6c34\u6ef4", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 }
|
||||
};
|
||||
|
||||
class FoodRegistry extends global.TarinaiRegistryBase {
|
||||
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);
|
||||
}
|
||||
nutrition(type = "", fallback = 0) {
|
||||
const value = this.get(type)?.nutrition;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
hungerRelief(type = "", fallback = 0) {
|
||||
const value = this.get(type)?.hungerRelief;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
effectId(type = "") { return this.get(type)?.effectId || ""; }
|
||||
passiveDecayMultiplier(type = "") { return this.get(type)?.decayRateMultiplier ?? 1; }
|
||||
hygienePenalty(type = "") { return this.get(type)?.hygienePenalty ?? 0.055; }
|
||||
debugSnapshot() { return super.debugSnapshot((id, def) => ({ id, label: def.label, interest: def.interest, effectId: def.effectId || "" })); }
|
||||
}
|
||||
|
||||
const registry = new FoodRegistry(rawDefinitions);
|
||||
global.TarinaiFoodRegistry = registry;
|
||||
global.TarinaiFoodDefinitions = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
|
@ -1,95 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/items/legacy-lifecycle
|
||||
// Compatibility fallback for the pre-pipeline monolithic Item update body.
|
||||
// Normal runtime goes through item_lifecycle_pipeline.js; this exists only so
|
||||
// old callers and partial-load environments have a stable fallback.
|
||||
// Compatibility shim. The former monolithic fallback was removed so item
|
||||
// behavior has a single source of truth: TarinaiItemLifecyclePipeline.
|
||||
(function (global) {
|
||||
function updateOne(item, dt, worldRef = global.world) {
|
||||
item.age += dt;
|
||||
if (item.type === "zunchi") item.spawnGrace = Math.max(0, (item.spawnGrace || 0) - dt);
|
||||
if (item.dropTimer > 0) {
|
||||
const beforeDrop = item.dropTimer;
|
||||
item.dropTimer = Math.max(0, item.dropTimer - dt);
|
||||
if (beforeDrop > 0 && item.dropTimer <= 0 && !item.dropImpactDone) {
|
||||
item.dropImpactDone = true;
|
||||
if (worldRef.itemDropImpact) worldRef.itemDropImpact(item);
|
||||
}
|
||||
}
|
||||
if (item.type === "water") item.amount -= dt * 0.9;
|
||||
if (isServingFoodType(item.type)) {
|
||||
const interval = passiveFoodDecayInterval(worldRef);
|
||||
item.passiveFoodDecayTimer = (item.passiveFoodDecayTimer || 0) + dt;
|
||||
let ticks = 0;
|
||||
while (item.passiveFoodDecayTimer >= interval && ticks < 3) {
|
||||
item.passiveFoodDecayTimer -= interval;
|
||||
ticks++;
|
||||
if (Number.isFinite(item.foodServingsRemaining)) {
|
||||
const before = Math.max(0, item.foodServingsRemaining || 0);
|
||||
const lost = Math.min(before, interval * passiveFoodDecayRate(item));
|
||||
if (lost > 0) {
|
||||
item.foodServingsRemaining = Math.max(0, before - lost);
|
||||
item.amount = item.foodServingsRemaining;
|
||||
const penalty = global.TarinaiItemRegistry?.food?.hygienePenalty?.(item.type) ?? PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING;
|
||||
worldRef?.registerFoodSpoilage?.(lost * penalty, item);
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item: item, type: item.type, amount: lost, passive: true });
|
||||
if (item.foodServingsRemaining <= 0.015) item.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(item.type)) {
|
||||
const before = item.amount || 0;
|
||||
const lost = Math.min(before, interval * 0.12);
|
||||
item.amount -= lost;
|
||||
if (lost > 0) {
|
||||
const penalty = global.TarinaiItemRegistry?.food?.hygienePenalty?.(item.type) ?? 0.002;
|
||||
worldRef?.registerFoodSpoilage?.(lost * penalty, item);
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item: item, type: item.type, amount: lost, passive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.type === "trace") item.amount -= dt * 1.35;
|
||||
if (item.type === "splat") item.amount -= dt * 1.05;
|
||||
if (item.type === "ant_corpse") item.amount -= dt * 0.018;
|
||||
if (item.type === "firecracker") {
|
||||
item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt);
|
||||
if (item.fuseTimer <= 0) {
|
||||
if (worldRef.explodeFirecracker) worldRef.explodeFirecracker(item);
|
||||
else item.amount = 0;
|
||||
}
|
||||
}
|
||||
if (item.type === "genkotsu") {
|
||||
item.impactFlash = Math.max(0, (item.impactFlash || 0) - dt);
|
||||
if (item.dropImpactDone) {
|
||||
item.lingerTimer = Math.max(0, (item.lingerTimer || 0) - dt);
|
||||
if ((item.lingerTimer || 0) <= 0) item.amount = 0;
|
||||
} else {
|
||||
item.amount = Math.max(item.amount || 0, 100);
|
||||
}
|
||||
}
|
||||
if (item.type === "ball") item.updateBall(dt, worldRef);
|
||||
if (item.type === "fan") item.updateFan(dt, worldRef);
|
||||
if (item.type === "rotator") item.updateRotator(dt, worldRef);
|
||||
if (item.type === "gate_fence") item.amount = 999;
|
||||
if (item.type === "duplicator") item.updateDuplicator(dt, worldRef);
|
||||
if (isPinType(item.type)) item.updatePushpin(dt, worldRef);
|
||||
if (item.type === "zunchi") item.updateZunchiMotion(dt, worldRef);
|
||||
if (item.type === "grass") {
|
||||
// Grass is intentionally not simulated per item. World.update randomly
|
||||
// advances one grass by one discrete stage every few seconds.
|
||||
normalizeGrassStage(item);
|
||||
} else if (item.type === "zunchi") {
|
||||
item.lifecycleTimer += dt;
|
||||
const interval = item.lifecycleInterval || (0.95 + stableUnit(item.id, "zunchi-life") * 0.45);
|
||||
if (item.lifecycleTimer >= interval) {
|
||||
const lifecycleDt = Math.min(item.lifecycleTimer, 2.4);
|
||||
item.lifecycleTimer = 0;
|
||||
item.updateZunchi(lifecycleDt, worldRef);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
if (!item || item.dead || !global.TarinaiItemLifecyclePipeline?.updateOne) return false;
|
||||
return global.TarinaiItemLifecyclePipeline.updateOne(item, dt, worldRef) !== false;
|
||||
}
|
||||
|
||||
global.TarinaiItemLifecycleLegacySystem = Object.freeze({ updateOne });
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/items/lifecycle-step
|
||||
// Fallback step for environments where the split item lifecycle pipeline cannot
|
||||
// run. The captured legacy Item.prototype.update is kept by item_runtime.js.
|
||||
// Compatibility shim for old step lists. Do not call a captured monolithic
|
||||
// update body; delegate to the concrete pipeline only.
|
||||
(function (global) {
|
||||
function update(item, dt, worldRef, context = {}) {
|
||||
const legacyUpdate = context.legacyUpdate;
|
||||
if (!item || item.dead || typeof legacyUpdate !== "function") return { done: true, ok: false };
|
||||
legacyUpdate.call(item, dt, worldRef);
|
||||
return { done: true, ok: true };
|
||||
function update(item, dt, worldRef) {
|
||||
const ok = global.TarinaiItemLifecyclePipeline?.updateOne?.(item, dt, worldRef);
|
||||
return { done: true, ok: ok !== false };
|
||||
}
|
||||
|
||||
global.TarinaiItemLegacyLifecycleStep = Object.freeze({ update });
|
||||
|
|
|
|||
|
|
@ -1,696 +1,18 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Unified item definition source: tools, item traits, visuals, food metadata, and item effects.
|
||||
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
observe: { id: "observe", label: "\u305f\u308a\u306a\u3044\u30c7\u30fc\u30bf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" },
|
||||
area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" },
|
||||
copy: { id: "copy", label: "\u30b3\u30d4\u30fc", placeable: false, scalable: false, iconText: "\u29c9", tooltip: "\u7f6e\u3044\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30b3\u30d4\u30fc\u3057\u3001\u6b21\u306e\u30af\u30ea\u30c3\u30af\u3067\u8a2d\u5b9a\u3054\u3068\u8cbc\u308a\u4ed8\u3051\u308b\u3002" },
|
||||
undo: { id: "undo", label: "\u623b\u3059", placeable: false, scalable: false, iconText: "\u21b6", tooltip: "\u76f4\u524d\u306e\u64cd\u4f5c\u3092\u53d6\u308a\u6d88\u3059\u3002" },
|
||||
redo: { id: "redo", label: "\u9032\u3080", placeable: false, scalable: false, iconText: "\u21b7", tooltip: "\u53d6\u308a\u6d88\u3057\u305f\u64cd\u4f5c\u3092\u3084\u308a\u76f4\u3059\u3002" },
|
||||
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" },
|
||||
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" },
|
||||
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
|
||||
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
|
||||
shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" },
|
||||
sprinkler: { id: "sprinkler", itemType: "sprinkler", label: "\u30b9\u30d7\u30ea\u30f3\u30af\u30e9\u30fc", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u2739", tooltip: "\u52b9\u679c\u7bc4\u56f2\u5185\u306e\u305a\u3093\u3061\u3068\u6b7b\u9ab8\u3092\u30e9\u30f3\u30c0\u30e0\u306b1\u3064\u6d17\u6d44\u3059\u308b\u3002" },
|
||||
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
|
||||
sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
|
||||
love_mochi: { id: "love_mochi", itemType: "love_mochi", label: "\u3078\u3053\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_love_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" },
|
||||
fight_mochi: { id: "fight_mochi", itemType: "fight_mochi", label: "\u3051\u3093\u304b\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_fight_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u3051\u3093\u304b\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\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: "\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: "\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, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u30fb\u85ac\u30fb\u753b\u92f2\u30fb\u304a\u3057\u308a\u92f2\u3092\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002\u92f2\u7cfb\u306f\u89e6\u308c\u305f\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\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" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
|
||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
||||
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
|
||||
food: { id: "food", itemType: "food", label: "\u98df\u3079\u7269", placeable: false, scalable: false, radius: 14, amount: 85, simulationOnly: true },
|
||||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
});
|
||||
|
||||
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "oshibyo", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "sprinkler", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
});
|
||||
|
||||
|
||||
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
|
||||
const LINK_ITEM_TYPES = Object.freeze(new Set(["rope", "rod"]));
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator"]);
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES]));
|
||||
function isFenceItemType(type = "") { return FENCE_ITEM_TYPES.has(String(type || "")); }
|
||||
function isMechanicalItemType(type = "") { return MECHANICAL_ITEM_TYPES.has(String(type || "")); }
|
||||
function isLinkItemType(type = "") { return LINK_ITEM_TYPES.has(String(type || "")); }
|
||||
function isPhysicsToolId(id = "") { return PHYSICS_TOOL_IDS.includes(String(id || "")); }
|
||||
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
|
||||
function defaultItemAngle(type = "") {
|
||||
const t = String(type || "");
|
||||
if (t === "fence_v" || t === "bounce_fence_v") return Math.PI / 2;
|
||||
return 0;
|
||||
}
|
||||
function normalizedItemAngle(angle = 0, fallback = 0) {
|
||||
const twoPi = Math.PI * 2;
|
||||
const n = Number(angle);
|
||||
const base = Number.isFinite(n) ? n : (Number.isFinite(Number(fallback)) ? Number(fallback) : 0);
|
||||
return ((base % twoPi) + twoPi) % twoPi;
|
||||
}
|
||||
function itemAngleFor(itemOrType = "") {
|
||||
if (itemOrType && typeof itemOrType === "object") return normalizedItemAngle(itemOrType.angle, defaultItemAngle(itemOrType.type));
|
||||
return defaultItemAngle(String(itemOrType || ""));
|
||||
}
|
||||
function rotatableToolIds() {
|
||||
return Object.values(TOOL_DEFINITIONS).filter(def => isRotatableItemType(def.itemType || def.id)).map(def => def.id);
|
||||
}
|
||||
|
||||
function itemHasTrait(type = "", trait = "") {
|
||||
return Boolean(ITEM_TRAITS[trait]?.has?.(String(type || "")));
|
||||
}
|
||||
|
||||
function isPinType(type = "") { return itemHasTrait(type, "pin"); }
|
||||
function isSleepFurnitureType(type = "") { return itemHasTrait(type, "sleepFurniture"); }
|
||||
function isLodgedPin(item = null) { return Boolean(item && !item.dead && isPinType(item.type) && item.pinState === "lodged"); }
|
||||
function lodgedPinFor(tarinai = null, worldRef = null) {
|
||||
if (!tarinai?.stuckPushpinId) return null;
|
||||
const items = worldRef?.items || tarinai.world?.items || [];
|
||||
return items.find(it => isLodgedPin(it) && it.id === tarinai.stuckPushpinId) || null;
|
||||
}
|
||||
function lodgedPinBehaviorFor(tarinai = null, worldRef = null) { return pinBehaviorFor(lodgedPinFor(tarinai, worldRef)?.type); }
|
||||
function hasLodgedPinEffect(tarinai = null, effectKey = "") { return Boolean(lodgedPinBehaviorFor(tarinai)?.[effectKey]); }
|
||||
|
||||
const PIN_BEHAVIORS = Object.freeze({
|
||||
pushpin: { type: "pushpin", looseAsset: "pushpin", lodgedAsset: "pushpin_stuck", attachMode: "impact", panicOnAttach: true, damageOnAttach: 6, damagePerTick: 1.4, stressOnAttach: 18, stressPerTick: 2.6, blocksZunchi: false, burstZunchiOnDetach: false },
|
||||
oshibyo: { type: "oshibyo", looseAsset: "oshibyo", lodgedAsset: "oshibyo_stuck", attachMode: "butt", panicOnAttach: false, damageOnAttach: 0, damagePerTick: 0, stressOnAttach: 0, stressPerTick: 0, blocksZunchi: true, burstZunchiOnDetach: true },
|
||||
});
|
||||
|
||||
function pinBehaviorFor(type = "") {
|
||||
return PIN_BEHAVIORS[String(type || "")] || null;
|
||||
}
|
||||
|
||||
|
||||
let ITEM_VISUAL_DEFINITIONS = null;
|
||||
let FOOD_REGISTRY = null;
|
||||
let FOOD_DEFINITIONS = null;
|
||||
let EFFECT_REGISTRY = null;
|
||||
let EFFECT_DEFINITIONS = null;
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "sprinkler", "bed", "nest_box", "ball", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
return [...TOOL_CATEGORIES].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
|
||||
function toolDefinition(id = "") {
|
||||
return TOOL_DEFINITIONS[id] || null;
|
||||
}
|
||||
|
||||
function toolLabel(id = "") {
|
||||
const def = toolDefinition(id);
|
||||
return def?.label || id || "";
|
||||
}
|
||||
|
||||
function toolItemType(id = "") {
|
||||
const def = toolDefinition(id);
|
||||
return def?.placeable ? (def.itemType || def.id) : null;
|
||||
}
|
||||
|
||||
function itemDefinition(type = "") {
|
||||
return Object.values(TOOL_DEFINITIONS).find(def => (def.itemType || def.id) === type) || null;
|
||||
}
|
||||
|
||||
function itemRadiusFor(type = "", fallback = 12) {
|
||||
return itemDefinition(type)?.radius ?? fallback;
|
||||
}
|
||||
|
||||
function itemAmountFor(type = "", fallback = 80) {
|
||||
return itemDefinition(type)?.amount ?? fallback;
|
||||
}
|
||||
|
||||
const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);
|
||||
const TOOL_SIZE_LABELS = Object.freeze({ small: "\u5c0f", medium: "\u4e2d", large: "\u5927" });
|
||||
const TOOL_SIZE_SCALES = Object.freeze({ small: 0.68, medium: 1.0, large: 1.48 });
|
||||
function normalizedToolSizeFor(worldRef = null, type = "") {
|
||||
const def = itemDefinition(type);
|
||||
if (!def?.scalable) return "medium";
|
||||
const value = worldRef?.toolSizes?.[type] || worldRef?.toolSize || "medium";
|
||||
return TOOL_SIZE_ORDER.includes(value) ? value : "medium";
|
||||
}
|
||||
function toolSizeScaleForValue(size = "medium") { return TOOL_SIZE_SCALES[size] || TOOL_SIZE_SCALES.medium; }
|
||||
function toolSizeScaleFor(worldRef = null, type = "") { return itemDefinition(type)?.scalable ? toolSizeScaleForValue(normalizedToolSizeFor(worldRef, type)) : 1; }
|
||||
|
||||
function scalableToolIds() {
|
||||
return Object.values(TOOL_DEFINITIONS).filter(def => def.scalable).map(def => def.id);
|
||||
}
|
||||
|
||||
function toolTipsFromDefinitions() {
|
||||
return Object.fromEntries(Object.entries(TOOL_DEFINITIONS).filter(([, def]) => def.tooltip).map(([id, def]) => [id, def.tooltip]));
|
||||
}
|
||||
|
||||
(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;
|
||||
}
|
||||
|
||||
ITEM_VISUAL_DEFINITIONS = defs;
|
||||
global.itemVisualDefinition = itemVisualDefinition;
|
||||
global.itemVisualPalette = itemVisualPalette;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0, hygienePenalty: 0.055 },
|
||||
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82, hygienePenalty: 0.055, cures: ["disease_explosion", "disease_fight"] },
|
||||
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "love_mochi" },
|
||||
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "fight_mochi" },
|
||||
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, 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: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "protein" },
|
||||
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "niteropu" },
|
||||
ammo: { label: "\u5f3e\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "ammo" },
|
||||
mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mystery_drug" },
|
||||
mercury: { label: "\u6c34\u9280", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mercury" },
|
||||
giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "giant_drug" },
|
||||
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" },
|
||||
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true },
|
||||
grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
water: { label: "\u6c34\u6ef4", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
|
||||
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 }
|
||||
};
|
||||
|
||||
class FoodRegistry extends global.TarinaiRegistryBase {
|
||||
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);
|
||||
}
|
||||
nutrition(type = "", fallback = 0) {
|
||||
const value = this.get(type)?.nutrition;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
hungerRelief(type = "", fallback = 0) {
|
||||
const value = this.get(type)?.hungerRelief;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
effectId(type = "") { return this.get(type)?.effectId || ""; }
|
||||
passiveDecayMultiplier(type = "") { return this.get(type)?.decayRateMultiplier ?? 1; }
|
||||
hygienePenalty(type = "") { return this.get(type)?.hygienePenalty ?? 0.055; }
|
||||
debugSnapshot() { return super.debugSnapshot((id, def) => ({ id, label: def.label, interest: def.interest, effectId: def.effectId || "" })); }
|
||||
}
|
||||
|
||||
const registry = new FoodRegistry(rawDefinitions);
|
||||
FOOD_REGISTRY = registry;
|
||||
FOOD_DEFINITIONS = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
laxative: {
|
||||
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: "\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: "\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: "\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: "\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: "\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: "\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: "\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: "\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: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"],
|
||||
},
|
||||
mystery_drug: {
|
||||
label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"],
|
||||
},
|
||||
zunda_juice: {
|
||||
label: "\u305a\u3093\u3060\u6c41", color: "rgba(87, 180, 79, 0.86)", kind: "cleanse", tags: ["item", "cleanse"],
|
||||
},
|
||||
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 + deterministicRange(tarinai.world, "love-mochi-duration", 4, 12, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 });
|
||||
if (context.source === "eating") {
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "approach_mate", { source: "love_mochi", priority: 150, ttl: 24, searchRange: 820, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.fight_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + deterministicRange(tarinai.world, "fight-mochi-duration", 10, 18, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(tarinai, { safety: context.source === "eating" ? nutrition * 0.5 : 16 });
|
||||
if (context.source === "eating") {
|
||||
tarinai.fearTimer = Math.max(tarinai.fearTimer || 0, 0.32);
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "fight_rival", { source: "fight_mochi", priority: 180, ttl: 24, searchRange: 820, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.sleep_drug, {
|
||||
onApply(tarinai, context = {}) {
|
||||
if (context.source === "eating") {
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -nutrition * 0.25, sleep: -nutrition * 0.15 });
|
||||
tarinai.energy = clamp(tarinai.energy - nutrition * 0.20, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100));
|
||||
tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 0.18);
|
||||
tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 2.5);
|
||||
const baseChance = Math.min(0.98, 0.35 + nutrition * 0.075);
|
||||
const sleepChance = tarinai.diseaseChance ? tarinai.diseaseChance(baseChance) : baseChance;
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
if (!tarinai.sleepDisease && deterministicChance(tarinai.world, "sleep-drug-disease", Math.min(0.98, sleepChance), tarinai, context.item)) return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
return true;
|
||||
}
|
||||
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 extends global.TarinaiRegistryBase {
|
||||
label(id = "") { return super.label(id, id || "\u4e0d\u660e"); }
|
||||
color(id = "") { return super.color(id, "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 super.labels(); }
|
||||
colors() { return super.colors(); }
|
||||
randomPool() {
|
||||
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); }
|
||||
debugSnapshot() {
|
||||
return [...this.defs.entries()].map(([id, def]) => ({ id, label: def.label, kind: def.kind, group: def.exclusiveGroup || "" }));
|
||||
}
|
||||
}
|
||||
|
||||
const registry = new EffectRegistry(rawDefinitions);
|
||||
EFFECT_REGISTRY = registry;
|
||||
EFFECT_DEFINITIONS = Object.freeze(rawDefinitions);
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Public facade joining the split item registry sources.
|
||||
(function (global) {
|
||||
function itemTypeDefinition(type = "") {
|
||||
const id = String(type || "");
|
||||
const tool = typeof itemDefinition === "function" ? itemDefinition(id) : null;
|
||||
const food = FOOD_DEFINITIONS?.[id] || null;
|
||||
const effectId = food?.effectId || (EFFECT_DEFINITIONS?.[id] ? id : "");
|
||||
const effect = effectId ? (EFFECT_DEFINITIONS?.[effectId] || null) : null;
|
||||
const visual = ITEM_VISUAL_DEFINITIONS?.[id] || null;
|
||||
const tool = typeof global.itemDefinition === "function" ? global.itemDefinition(id) : null;
|
||||
const foodDefinitions = global.TarinaiFoodDefinitions || {};
|
||||
const effectDefinitions = global.TarinaiEffectDefinitions || {};
|
||||
const visualDefinitions = global.TarinaiItemVisualDefinitions || {};
|
||||
const traits = global.ITEM_TRAITS || global.TarinaiToolRegistry?.traits || {};
|
||||
const food = foodDefinitions[id] || null;
|
||||
const effectId = food?.effectId || (effectDefinitions[id] ? id : "");
|
||||
const effect = effectId ? (effectDefinitions[effectId] || null) : null;
|
||||
const visual = visualDefinitions[id] || null;
|
||||
return {
|
||||
id,
|
||||
tool,
|
||||
|
|
@ -703,7 +25,7 @@ function toolTipsFromDefinitions() {
|
|||
amount: tool?.amount,
|
||||
placeable: Boolean(tool?.placeable),
|
||||
scalable: Boolean(tool?.scalable),
|
||||
traits: Object.fromEntries(Object.keys(ITEM_TRAITS).map(trait => [trait, itemHasTrait(id, trait)])),
|
||||
traits: Object.fromEntries(Object.keys(traits).map(trait => [trait, typeof global.itemHasTrait === "function" ? global.itemHasTrait(id, trait) : false])),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -712,45 +34,41 @@ function toolTipsFromDefinitions() {
|
|||
}
|
||||
|
||||
function itemTypeDebugSnapshot() {
|
||||
const toolDefinitions = global.TOOL_DEFINITIONS || global.TarinaiToolRegistry?.definitions || {};
|
||||
const foodDefinitions = global.TarinaiFoodDefinitions || {};
|
||||
const effectDefinitions = global.TarinaiEffectDefinitions || {};
|
||||
const visualDefinitions = global.TarinaiItemVisualDefinitions || {};
|
||||
const ids = new Set([
|
||||
...Object.values(TOOL_DEFINITIONS).map(def => def.itemType || def.id),
|
||||
...Object.keys(FOOD_DEFINITIONS || {}),
|
||||
...Object.keys(EFFECT_DEFINITIONS || {}),
|
||||
...Object.keys(ITEM_VISUAL_DEFINITIONS || {}),
|
||||
...Object.values(toolDefinitions).map(def => def.itemType || def.id),
|
||||
...Object.keys(foodDefinitions),
|
||||
...Object.keys(effectDefinitions),
|
||||
...Object.keys(visualDefinitions),
|
||||
]);
|
||||
return [...ids].sort().map(id => itemTypeDefinition(id));
|
||||
}
|
||||
|
||||
global.itemTypeDefinition = itemTypeDefinition;
|
||||
global.itemTypeLabel = itemTypeLabel;
|
||||
global.itemTypeDebugSnapshot = itemTypeDebugSnapshot;
|
||||
|
||||
global.TarinaiItemRegistry = Object.freeze({
|
||||
definition: itemTypeDefinition,
|
||||
label: itemTypeLabel,
|
||||
debugSnapshot: itemTypeDebugSnapshot,
|
||||
tool: toolDefinition,
|
||||
item: itemDefinition,
|
||||
food: FOOD_REGISTRY,
|
||||
effect: EFFECT_REGISTRY,
|
||||
visual: itemVisualDefinition,
|
||||
hasTrait: itemHasTrait,
|
||||
radius: itemRadiusFor,
|
||||
amount: itemAmountFor,
|
||||
categories: toolCategories,
|
||||
physicsToolIds: () => [...PHYSICS_TOOL_IDS],
|
||||
isFence: isFenceItemType,
|
||||
isMechanical: isMechanicalItemType,
|
||||
isLink: isLinkItemType,
|
||||
scalableToolIds,
|
||||
pinBehavior: pinBehaviorFor,
|
||||
tool: global.toolDefinition,
|
||||
item: global.itemDefinition,
|
||||
food: global.TarinaiFoodRegistry,
|
||||
effect: global.TarinaiEffectRegistry,
|
||||
visual: global.itemVisualDefinition,
|
||||
hasTrait: global.itemHasTrait,
|
||||
radius: global.itemRadiusFor,
|
||||
amount: global.itemAmountFor,
|
||||
categories: global.toolCategories,
|
||||
physicsToolIds: () => [...(global.PHYSICS_TOOL_IDS || [])],
|
||||
isFence: global.isFenceItemType,
|
||||
isMechanical: global.isMechanicalItemType,
|
||||
isLink: global.isLinkItemType,
|
||||
scalableToolIds: global.scalableToolIds,
|
||||
pinBehavior: global.pinBehaviorFor,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.isFenceItemType = isFenceItemType;
|
||||
window.isMechanicalItemType = isMechanicalItemType;
|
||||
window.isLinkItemType = isLinkItemType;
|
||||
window.isPhysicsToolId = isPhysicsToolId;
|
||||
window.isRotatableItemType = isRotatableItemType;
|
||||
window.defaultItemAngle = defaultItemAngle;
|
||||
window.normalizedItemAngle = normalizedItemAngle;
|
||||
window.itemAngleFor = itemAngleFor;
|
||||
window.rotatableToolIds = rotatableToolIds;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ draw(ctx, t, lighting = null) {
|
|||
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
||||
if (isPinType(this.type) && this.pinState === "lodged") {
|
||||
const owner = world?.liveTarinaiById?.(this.pinTargetId) || null;
|
||||
if (owner?.insideNestBoxId) return;
|
||||
if (owner && world?.isTarinaiHiddenInNestBox?.(owner)) return;
|
||||
}
|
||||
const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
||||
const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1);
|
||||
|
|
@ -590,21 +590,25 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "ball") {
|
||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||
const moving = clamp(speed / 260, 0, 1);
|
||||
const r = this.r || 18;
|
||||
ctx.save();
|
||||
ctx.rotate(this.spin || 0);
|
||||
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
||||
ctx.shadowBlur = 2 + moving * 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
ctx.font = `${Math.round((this.r || 18) * 2.0)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
||||
// Use the original soccer-ball emoji as the visible sprite. The
|
||||
// collision center remains item.x/item.y; rendering is centered via
|
||||
// textAlign/textBaseline rather than replacing the emoji with vector art.
|
||||
ctx.font = `${Math.max(18, r * 2.05)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("\u26bd", 0, 1);
|
||||
ctx.fillText("⚽", 0, 0);
|
||||
if (moving > 0.55) {
|
||||
ctx.globalAlpha = 0.14 + moving * 0.10;
|
||||
ctx.globalAlpha *= 0.10 + moving * 0.10;
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-this.r * 0.35, -this.r * 0.42, this.r * 0.25, this.r * 0.12, -0.45, 0, Math.PI * 2);
|
||||
ctx.ellipse(-r * 0.42, -r * 0.45, r * 0.25, r * 0.12, -0.45, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
|
|
@ -727,15 +731,8 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.globalAlpha *= powered ? 1 : 0.72;
|
||||
ctx.strokeStyle = styleNight ? "#6e5f89" : "#8b6cc5";
|
||||
ctx.lineWidth = thick;
|
||||
ctx.shadowColor = powered ? (styleNight ? "rgba(116,92,170,0.38)" : "rgba(126,82,190,0.30)") : "rgba(88,80,96,0.20)";
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.shadowBlur = 0;
|
||||
if (tier !== "low") {
|
||||
ctx.strokeStyle = powered ? (styleNight ? "rgba(236,220,255,0.44)" : "rgba(255,255,255,0.46)") : "rgba(255,255,255,0.28)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
}
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.restore();
|
||||
ctx.fillStyle = powered ? (styleNight ? "#3c324e" : "#f4eaff") : (styleNight ? "#34343a" : "#ece8ef");
|
||||
ctx.strokeStyle = powered ? (styleNight ? "#a28cc8" : "#6e4fa4") : "rgba(92,88,98,0.82)";
|
||||
|
|
@ -762,28 +759,12 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.globalAlpha *= enabled ? 1 : 0.48;
|
||||
ctx.strokeStyle = styleNight ? "#4d8e4e" : "#5cac4d";
|
||||
ctx.lineWidth = thick;
|
||||
ctx.shadowColor = styleNight ? "rgba(92,190,86,0.34)" : "rgba(62,170,54,0.28)";
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.18) : Math.max(4, thick * 0.32));
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.shadowBlur = 0;
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.setLineDash([]);
|
||||
if (tier !== "low") {
|
||||
ctx.strokeStyle = styleNight ? "rgba(226,255,218,0.46)" : "rgba(255,255,255,0.50)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.fillStyle = styleNight ? "#263b27" : "#efffe9";
|
||||
ctx.strokeStyle = styleNight ? "#80c56e" : "#3e8f35";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, Math.max(8, thick * 0.74), 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "#d8ffd0" : "#26711f";
|
||||
ctx.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("\u6bd2", 0, 0);
|
||||
// v39.15.53: 毒ブロックは物理輪郭だけを安定描画する。
|
||||
// 中央の塗りつぶし/文字はキャッシュ更新時に明滅して見えるため描画しない。
|
||||
} else if (this.type === "reciprocator") {
|
||||
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
|
|
|
|||
255
js/item_tool_definitions.js
Normal file
255
js/item_tool_definitions.js
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"use strict";
|
||||
|
||||
// Tool definitions, item traits, categories, and basic type helpers.
|
||||
(function (global) {
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
observe: { id: "observe", label: "\u305f\u308a\u306a\u3044\u30c7\u30fc\u30bf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
clear_tool: { id: "clear_tool", label: "\u89e3\u9664", placeable: false, scalable: false, iconText: "\u2715", tooltip: "選択中の道具ボタンを解除し、通常の観察状態に戻す。" },
|
||||
delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" },
|
||||
area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" },
|
||||
copy: { id: "copy", label: "\u30b3\u30d4\u30fc", placeable: false, scalable: false, iconText: "\u29c9", tooltip: "\u7f6e\u3044\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30b3\u30d4\u30fc\u3057\u3001\u6b21\u306e\u30af\u30ea\u30c3\u30af\u3067\u8a2d\u5b9a\u3054\u3068\u8cbc\u308a\u4ed8\u3051\u308b\u3002" },
|
||||
undo: { id: "undo", label: "\u623b\u3059", placeable: false, scalable: false, iconText: "\u21b6", tooltip: "\u76f4\u524d\u306e\u64cd\u4f5c\u3092\u53d6\u308a\u6d88\u3059\u3002" },
|
||||
redo: { id: "redo", label: "\u9032\u3080", placeable: false, scalable: false, iconText: "\u21b7", tooltip: "\u53d6\u308a\u6d88\u3057\u305f\u64cd\u4f5c\u3092\u3084\u308a\u76f4\u3059\u3002" },
|
||||
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" },
|
||||
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" },
|
||||
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
|
||||
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
|
||||
shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" },
|
||||
sprinkler: { id: "sprinkler", itemType: "sprinkler", label: "\u30b9\u30d7\u30ea\u30f3\u30af\u30e9\u30fc", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u2739", tooltip: "\u52b9\u679c\u7bc4\u56f2\u5185\u306e\u305a\u3093\u3061\u3068\u6b7b\u9ab8\u3092\u30e9\u30f3\u30c0\u30e0\u306b1\u3064\u6d17\u6d44\u3059\u308b\u3002" },
|
||||
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
|
||||
sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },
|
||||
love_mochi: { id: "love_mochi", itemType: "love_mochi", label: "\u3078\u3053\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_love_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" },
|
||||
fight_mochi: { id: "fight_mochi", itemType: "fight_mochi", label: "\u3051\u3093\u304b\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_fight_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u3051\u3093\u304b\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\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: "\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: "\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, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u30fb\u85ac\u30fb\u753b\u92f2\u30fb\u304a\u3057\u308a\u92f2\u3092\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002\u92f2\u7cfb\u306f\u89e6\u308c\u305f\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\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" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
|
||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
||||
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
|
||||
food: { id: "food", itemType: "food", label: "\u98df\u3079\u7269", placeable: false, scalable: false, radius: 14, amount: 85, simulationOnly: true },
|
||||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
});
|
||||
|
||||
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box", "grass_bed"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "sprinkler", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
});
|
||||
|
||||
|
||||
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
|
||||
const LINK_ITEM_TYPES = Object.freeze(new Set(["rope", "rod"]));
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator"]);
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES]));
|
||||
function isFenceItemType(type = "") { return FENCE_ITEM_TYPES.has(String(type || "")); }
|
||||
function isMechanicalItemType(type = "") { return MECHANICAL_ITEM_TYPES.has(String(type || "")); }
|
||||
function isLinkItemType(type = "") { return LINK_ITEM_TYPES.has(String(type || "")); }
|
||||
function isPhysicsToolId(id = "") { return PHYSICS_TOOL_IDS.includes(String(id || "")); }
|
||||
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
|
||||
function defaultItemAngle(type = "") {
|
||||
const t = String(type || "");
|
||||
if (t === "fence_v" || t === "bounce_fence_v") return Math.PI / 2;
|
||||
return 0;
|
||||
}
|
||||
function normalizedItemAngle(angle = 0, fallback = 0) {
|
||||
const twoPi = Math.PI * 2;
|
||||
const n = Number(angle);
|
||||
const base = Number.isFinite(n) ? n : (Number.isFinite(Number(fallback)) ? Number(fallback) : 0);
|
||||
return ((base % twoPi) + twoPi) % twoPi;
|
||||
}
|
||||
function itemAngleFor(itemOrType = "") {
|
||||
if (itemOrType && typeof itemOrType === "object") return normalizedItemAngle(itemOrType.angle, defaultItemAngle(itemOrType.type));
|
||||
return defaultItemAngle(String(itemOrType || ""));
|
||||
}
|
||||
function rotatableToolIds() {
|
||||
return Object.values(TOOL_DEFINITIONS).filter(def => isRotatableItemType(def.itemType || def.id)).map(def => def.id);
|
||||
}
|
||||
|
||||
function itemHasTrait(type = "", trait = "") {
|
||||
return Boolean(ITEM_TRAITS[trait]?.has?.(String(type || "")));
|
||||
}
|
||||
|
||||
function isPinType(type = "") { return itemHasTrait(type, "pin"); }
|
||||
function isSleepFurnitureType(type = "") { return itemHasTrait(type, "sleepFurniture"); }
|
||||
function isLodgedPin(item = null) { return Boolean(item && !item.dead && isPinType(item.type) && item.pinState === "lodged"); }
|
||||
function lodgedPinFor(tarinai = null, worldRef = null) {
|
||||
if (!tarinai?.stuckPushpinId) return null;
|
||||
const items = worldRef?.items || tarinai.world?.items || [];
|
||||
return items.find(it => isLodgedPin(it) && it.id === tarinai.stuckPushpinId) || null;
|
||||
}
|
||||
function lodgedPinBehaviorFor(tarinai = null, worldRef = null) { return pinBehaviorFor(lodgedPinFor(tarinai, worldRef)?.type); }
|
||||
function hasLodgedPinEffect(tarinai = null, effectKey = "") { return Boolean(lodgedPinBehaviorFor(tarinai)?.[effectKey]); }
|
||||
|
||||
const PIN_BEHAVIORS = Object.freeze({
|
||||
pushpin: { type: "pushpin", looseAsset: "pushpin", lodgedAsset: "pushpin_stuck", attachMode: "impact", panicOnAttach: true, damageOnAttach: 6, damagePerTick: 1.4, stressOnAttach: 18, stressPerTick: 2.6, blocksZunchi: false, burstZunchiOnDetach: false },
|
||||
oshibyo: { type: "oshibyo", looseAsset: "oshibyo", lodgedAsset: "oshibyo_stuck", attachMode: "butt", panicOnAttach: false, damageOnAttach: 0, damagePerTick: 0, stressOnAttach: 0, stressPerTick: 0, blocksZunchi: true, burstZunchiOnDetach: true },
|
||||
});
|
||||
|
||||
function pinBehaviorFor(type = "") {
|
||||
return PIN_BEHAVIORS[String(type || "")] || null;
|
||||
}
|
||||
|
||||
|
||||
let ITEM_VISUAL_DEFINITIONS = null;
|
||||
let FOOD_REGISTRY = null;
|
||||
let FOOD_DEFINITIONS = null;
|
||||
let EFFECT_REGISTRY = null;
|
||||
let EFFECT_DEFINITIONS = null;
|
||||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "sprinkler", "bed", "nest_box", "ball", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
return [...TOOL_CATEGORIES].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
|
||||
function toolDefinition(id = "") {
|
||||
return TOOL_DEFINITIONS[id] || null;
|
||||
}
|
||||
|
||||
function toolLabel(id = "") {
|
||||
const def = toolDefinition(id);
|
||||
return def?.label || id || "";
|
||||
}
|
||||
|
||||
function toolItemType(id = "") {
|
||||
const def = toolDefinition(id);
|
||||
return def?.placeable ? (def.itemType || def.id) : null;
|
||||
}
|
||||
|
||||
function itemDefinition(type = "") {
|
||||
return Object.values(TOOL_DEFINITIONS).find(def => (def.itemType || def.id) === type) || null;
|
||||
}
|
||||
|
||||
function itemRadiusFor(type = "", fallback = 12) {
|
||||
return itemDefinition(type)?.radius ?? fallback;
|
||||
}
|
||||
|
||||
function itemAmountFor(type = "", fallback = 80) {
|
||||
return itemDefinition(type)?.amount ?? fallback;
|
||||
}
|
||||
|
||||
const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);
|
||||
const TOOL_SIZE_LABELS = Object.freeze({ small: "\u5c0f", medium: "\u4e2d", large: "\u5927" });
|
||||
const TOOL_SIZE_SCALES = Object.freeze({ small: 0.68, medium: 1.0, large: 1.48 });
|
||||
function normalizedToolSizeFor(worldRef = null, type = "") {
|
||||
const def = itemDefinition(type);
|
||||
if (!def?.scalable) return "medium";
|
||||
const value = worldRef?.toolSizes?.[type] || worldRef?.toolSize || "medium";
|
||||
return TOOL_SIZE_ORDER.includes(value) ? value : "medium";
|
||||
}
|
||||
function toolSizeScaleForValue(size = "medium") { return TOOL_SIZE_SCALES[size] || TOOL_SIZE_SCALES.medium; }
|
||||
function toolSizeScaleFor(worldRef = null, type = "") { return itemDefinition(type)?.scalable ? toolSizeScaleForValue(normalizedToolSizeFor(worldRef, type)) : 1; }
|
||||
|
||||
function scalableToolIds() {
|
||||
return Object.values(TOOL_DEFINITIONS).filter(def => def.scalable).map(def => def.id);
|
||||
}
|
||||
|
||||
function toolTipsFromDefinitions() {
|
||||
return Object.fromEntries(Object.entries(TOOL_DEFINITIONS).filter(([, def]) => def.tooltip).map(([id, def]) => [id, def.tooltip]));
|
||||
}
|
||||
|
||||
|
||||
Object.assign(global, {
|
||||
TOOL_DEFINITIONS,
|
||||
ITEM_TRAITS,
|
||||
FENCE_ITEM_TYPES,
|
||||
MECHANICAL_ITEM_TYPES,
|
||||
LINK_ITEM_TYPES,
|
||||
PHYSICS_TOOL_IDS,
|
||||
ROTATABLE_ITEM_TYPES,
|
||||
isFenceItemType,
|
||||
isMechanicalItemType,
|
||||
isLinkItemType,
|
||||
isPhysicsToolId,
|
||||
isRotatableItemType,
|
||||
defaultItemAngle,
|
||||
normalizedItemAngle,
|
||||
itemAngleFor,
|
||||
rotatableToolIds,
|
||||
itemHasTrait,
|
||||
isPinType,
|
||||
isSleepFurnitureType,
|
||||
isLodgedPin,
|
||||
lodgedPinFor,
|
||||
lodgedPinBehaviorFor,
|
||||
hasLodgedPinEffect,
|
||||
PIN_BEHAVIORS,
|
||||
pinBehaviorFor,
|
||||
TOOL_CATEGORIES,
|
||||
toolCategories,
|
||||
toolDefinition,
|
||||
toolLabel,
|
||||
toolItemType,
|
||||
itemDefinition,
|
||||
itemRadiusFor,
|
||||
itemAmountFor,
|
||||
TOOL_SIZE_ORDER,
|
||||
TOOL_SIZE_LABELS,
|
||||
TOOL_SIZE_SCALES,
|
||||
normalizedToolSizeFor,
|
||||
toolSizeScaleForValue,
|
||||
toolSizeScaleFor,
|
||||
scalableToolIds,
|
||||
toolTipsFromDefinitions,
|
||||
});
|
||||
|
||||
global.TarinaiToolRegistry = Object.freeze({
|
||||
definitions: TOOL_DEFINITIONS,
|
||||
traits: ITEM_TRAITS,
|
||||
categories: toolCategories,
|
||||
definition: toolDefinition,
|
||||
label: toolLabel,
|
||||
itemType: toolItemType,
|
||||
item: itemDefinition,
|
||||
radius: itemRadiusFor,
|
||||
amount: itemAmountFor,
|
||||
hasTrait: itemHasTrait,
|
||||
physicsToolIds: () => [...PHYSICS_TOOL_IDS],
|
||||
rotatableToolIds,
|
||||
scalableToolIds,
|
||||
pinBehavior: pinBehaviorFor,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -10,7 +10,7 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
if (type === "water" || type === "zunda_juice") item.roles.drink = true;
|
||||
if (["sweet", "water", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
|
||||
if (type === "bed" || type === "nest_box") item.roles.sleepPlace = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (type === "grass") item.roles.grassMaterial = true;
|
||||
if (type === "grass") {
|
||||
item.grassStage = Math.floor(rand(0, 2.999));
|
||||
|
|
@ -133,8 +133,8 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
collision: { solid: true, hazard: true },
|
||||
hazard: { kind: "poison", damage: 7 },
|
||||
sleep: { awakeUntil: 0 },
|
||||
mass: 4.8,
|
||||
inertia: 36000,
|
||||
mass: 0.45,
|
||||
inertia: 3200,
|
||||
};
|
||||
item.prevX = x;
|
||||
item.prevY = y;
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@
|
|||
const mechanicalWorld = physicsWorld?.mechanical || (global.TarinaiMechanicalSystem?.updateWorld?.(worldRef, dt, { maxPairs: physicsBudget.maxPairs, maxLinks: 0, skipLinks: true, maxSubsteps: physicsBudget.maxSubsteps }) || null);
|
||||
const constraintWorld = physicsWorld ? physicsWorld.constraintRan : (global.TarinaiConstraintSystem?.updateWorld?.(worldRef, dt, { maxLinks: Math.min(180, physicsBudget.maxLinks) }) || 0);
|
||||
const postConstraintPairs = physicsWorld ? physicsWorld.postConstraintPairs : (constraintWorld ? (global.TarinaiMechanicalSystem?.resolveMechanicalPairs?.(worldRef, dt, { maxPairs: Math.min(80, physicsBudget.postMaxPairs) }) || 0) : 0);
|
||||
const poisonContactRemoved = worldRef.resolvePoisonBlockItemContacts?.({ maxPoison: 24, maxTargets: 80 }) || 0;
|
||||
if (physicsEnd) physicsEnd();
|
||||
state.signature = schedulerSignature(worldRef);
|
||||
worldRef._itemUpdateSchedulerStats = {
|
||||
|
|
@ -211,7 +212,7 @@
|
|||
mechanicalStats: worldRef._mechanicalWorldStats || null,
|
||||
constraintStats: worldRef._constraintWorldStats || null,
|
||||
};
|
||||
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld;
|
||||
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld + poisonContactRemoved;
|
||||
} finally {
|
||||
if (end) end();
|
||||
}
|
||||
|
|
|
|||
131
js/item_visual_definitions.js
Normal file
131
js/item_visual_definitions.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"use strict";
|
||||
|
||||
// Item visual metadata and palettes.
|
||||
(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.TarinaiItemVisualDefinitions = defs;
|
||||
global.itemVisualDefinition = itemVisualDefinition;
|
||||
global.itemVisualPalette = itemVisualPalette;
|
||||
global.TarinaiItemVisualRegistry = Object.freeze({ definitions: defs, definition: itemVisualDefinition, palette: itemVisualPalette });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
32
js/mechanical_shape_bridge.js
Normal file
32
js/mechanical_shape_bridge.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/mechanical-shape-bridge
|
||||
// Single read-only facade for mechanical shape/footprint queries. It keeps
|
||||
// world, placement, and renderer code from binding directly to the body solver.
|
||||
(function (global) {
|
||||
function mech() { return global.TarinaiMechanicalSystem || null; }
|
||||
|
||||
function isMechanicalType(itemOrType) { return Boolean(mech()?.isMechanicalType?.(itemOrType)); }
|
||||
function worldSegments(item) { return mech()?.worldSegments?.(item) || []; }
|
||||
function obstacleRects(item) { return mech()?.obstacleRects?.(item) || []; }
|
||||
function placementRects(item) { return mech()?.placementRects?.(item) || obstacleRects(item); }
|
||||
function poisonHazardRects(item) { return mech()?.poisonHazardRects?.(item) || []; }
|
||||
function boundsAabb(item) { return mech()?.boundsAabb?.(item) || null; }
|
||||
function hazardAabb(item) { return mech()?.hazardAabb?.(item) || null; }
|
||||
function extent(item) { return mech()?.extent?.(item) || 0; }
|
||||
function reach(item) { return mech()?.reach?.(item) || extent(item); }
|
||||
function hitTest(item, x, y, worldRef, opts = {}) { return mech()?.hitTest?.(item, x, y, worldRef, opts) || { hit: false, distance: Infinity }; }
|
||||
|
||||
global.TarinaiMechanicalShapeSystem = Object.freeze({
|
||||
isMechanicalType,
|
||||
worldSegments,
|
||||
obstacleRects,
|
||||
placementRects,
|
||||
poisonHazardRects,
|
||||
boundsAabb,
|
||||
hazardAabb,
|
||||
extent,
|
||||
reach,
|
||||
hitTest,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -434,6 +434,11 @@
|
|||
return rectsFor(item, "obstacle");
|
||||
}
|
||||
|
||||
function placementRects(item) {
|
||||
if (!item || item.dead || !isMechanicalType(item)) return [];
|
||||
return rectsFor(item, "obstacle");
|
||||
}
|
||||
|
||||
function poisonHazardRects(item) {
|
||||
if (!item || item.dead || item.type !== "poison_block") return [];
|
||||
return rectsFor(item, "hazard");
|
||||
|
|
@ -487,7 +492,7 @@
|
|||
body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 };
|
||||
const vel = body.velocity;
|
||||
const pose = body.pose || {};
|
||||
const mass = Math.max(1.2, ps(item, "mass", 4.8));
|
||||
const mass = Math.max(0.20, ps(item, "mass", 0.45));
|
||||
const prevVx = num(vel.x, item.vx);
|
||||
const prevVy = num(vel.y, item.vy);
|
||||
const nextVx = clamp(prevVx + num(fx) * 0.42 * scale / mass, -260, 260);
|
||||
|
|
@ -501,7 +506,7 @@
|
|||
const rx = num(contactX) - num(pose.x, item.x);
|
||||
const ry = num(contactY) - num(pose.y, item.y);
|
||||
const torque = rx * num(fy) - ry * num(fx);
|
||||
const inertia = Math.max(12000, ps(item, "inertia", 36000));
|
||||
const inertia = Math.max(1200, ps(item, "inertia", 3200));
|
||||
vel.angular = clamp(num(vel.angular, ps(item, "spin", 0)) + torque / inertia * scale, -2.2, 2.2);
|
||||
const changed = Math.hypot(nextVx - prevVx, nextVy - prevVy) > 0.002 || Math.abs(torque) > 0.001;
|
||||
if (changed) { commitBody(item, "poison-block-impulse"); wakeItem(item, "poison-block-impulse"); }
|
||||
|
|
@ -648,7 +653,7 @@
|
|||
|
||||
function mechanicalSeparationWeight(item, nx, ny) {
|
||||
if (!item || item.dead || item.playerHeld || item._heldByPlayer) return 0;
|
||||
if (item.type === "poison_block") return 1 / Math.max(1.2, ps(item, "mass", 4.8));
|
||||
if (item.type === "poison_block") return 1 / Math.max(0.20, ps(item, "mass", 0.45));
|
||||
if (item.type === "reciprocator") {
|
||||
const a = axis(item);
|
||||
const projection = Math.abs(num(nx) * a.x + num(ny) * a.y);
|
||||
|
|
@ -715,9 +720,9 @@
|
|||
const wb = mechanicalSeparationWeight(b, nx, ny);
|
||||
const sum = wa + wb;
|
||||
if (sum <= 0.0001) return false;
|
||||
// Solve most of the penetration as position, not velocity. Capping avoids
|
||||
// large teleports when old saves spawn bodies deeply overlapped.
|
||||
const correction = Math.min(overlap + 0.35, Math.max(2.2, Math.min(18, overlap * 0.82)));
|
||||
// Resolve the actual penetration only. The former extra bias made bodies
|
||||
// visibly pop when many mechanical items touched at once.
|
||||
const correction = Math.min(Math.max(0, overlap - 0.05) * 0.92, 18);
|
||||
const ax = -nx * correction * (wa / sum);
|
||||
const ay = -ny * correction * (wa / sum);
|
||||
const bx = nx * correction * (wb / sum);
|
||||
|
|
@ -735,7 +740,7 @@
|
|||
if (!item || !info) return false;
|
||||
const overlap = Math.max(0, num(info.overlap));
|
||||
if (overlap <= 0.001) return false;
|
||||
const correction = Math.min(overlap + 0.4, Math.max(2.5, Math.min(20, overlap * 0.88)));
|
||||
const correction = Math.min(Math.max(0, overlap - 0.05) * 0.94, 20);
|
||||
const moved = moveMechanicalBodyForSeparation(item, -num(info.nx) * correction, -num(info.ny) * correction, dt, "fence-separation");
|
||||
if (moved) {
|
||||
worldRef?.markSpatialDirty?.("mechanical-fence-separation");
|
||||
|
|
@ -1001,7 +1006,6 @@
|
|||
if (a.type === "rotator") brakePoweredRotatorOnMechanicalContact(a, dt, worldRef, "mechanical");
|
||||
if (b.type === "rotator") brakePoweredRotatorOnMechanicalContact(b, dt, worldRef, "mechanical");
|
||||
|
||||
if (!recentlyHandled) worldRef?.effects?.push(new Effect("ring", info.x, info.y, { size: Math.max(14, Math.min(28, 12 + info.overlap)), life: 0.14, color: "rgba(184,132,230,0.34)" }));
|
||||
if (worldRef) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("mechanical-contact"); }
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1058,7 +1062,6 @@
|
|||
worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null);
|
||||
if ((worldRef._mechanicalContactAt[key] || -999) + 0.08 <= now) {
|
||||
worldRef._mechanicalContactAt[key] = now;
|
||||
worldRef.effects?.push(new Effect("ring", info.x, info.y, { size: Math.max(14, Math.min(30, 13 + info.overlap)), life: 0.13, color: "rgba(145,120,94,0.34)" }));
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
|
@ -1185,12 +1188,11 @@
|
|||
if (Math.abs(num(moved)) <= 0.001) {
|
||||
const now = Number(worldRef?.time || item.world?.time || 0) || 0;
|
||||
if ((item._reciprocatorMovedAt || -999) + 0.18 < now) {
|
||||
// The motor is powered but the pose did not change for several frames.
|
||||
// Reset only rail-local transient state; do not touch user speed/travel.
|
||||
// Do not nudge the rail body forward. Treat a stalled powered rail as
|
||||
// blocked and let the existing blocked/reverse path decide the next move.
|
||||
pset(item, "slideSpeed", 0, "rail-stall-slide-clear");
|
||||
if (!changed && Math.abs(phase) < 1 - 0.000001) {
|
||||
integrateReciprocatorPhase(item, dir * motorSpeed, dt, true, "rail-stall-nudge");
|
||||
changed = true;
|
||||
changed = noteReciprocatorBlocked(item, dt, worldRef, "rail-stall-blocked") || changed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1271,6 +1273,59 @@
|
|||
return changed;
|
||||
}
|
||||
|
||||
function resolvePoisonBlockTarinaiContacts(item, worldRef, dt = 0.016) {
|
||||
if (!item || item.dead || item.type !== "poison_block" || !worldRef?.tarinai) return false;
|
||||
if (ps(item, "solid", true) === false) return false;
|
||||
const rects = obstacleRects(item);
|
||||
if (!rects.length) return false;
|
||||
const radius = reach(item) + 96;
|
||||
const source = worldRef.nearbyTarinai?.(item.x, item.y, radius + 48) || worldRef.tarinai || [];
|
||||
let changed = false;
|
||||
let contacts = 0;
|
||||
for (const t of source) {
|
||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const rr = Math.max(8, (Number(t.radius) || 22) * 0.74);
|
||||
if (Math.hypot(num(t.x) - num(item.x), num(t.y) - num(item.y)) > radius + rr) continue;
|
||||
for (const rect of rects) {
|
||||
if (!worldRef.circleOverlapsObstacleRect?.(t.x, t.y, rr, rect, 1.0)) continue;
|
||||
const beforeX = num(t.x);
|
||||
const beforeY = num(t.y);
|
||||
const pushed = worldRef.pushTarinaiOutOfRect?.(t, rect, rr, {
|
||||
maxPasses: 1,
|
||||
maxPush: Math.max(14, rr * 1.65),
|
||||
slop: 0.45,
|
||||
reason: "poison-block-direct-tarinai-contact",
|
||||
});
|
||||
if (!pushed) continue;
|
||||
contacts += 1;
|
||||
changed = true;
|
||||
const dx = num(t.x) - beforeX;
|
||||
const dy = num(t.y) - beforeY;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 0.001) {
|
||||
// Eating/sleeping bodies can be nearly stationary. Feed a small,
|
||||
// opposite impulse back into the passive block so the contact is not
|
||||
// visually perceived as the block tunneling through the Tarinai.
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
const speed = Math.hypot(ps(item, "xv", 0), ps(item, "yv", 0));
|
||||
const impulse = Math.min(34, 5.5 + d * 0.85 + speed * 0.035);
|
||||
applyImpulse(item, num(t.x) - nx * rr, num(t.y) - ny * rr, -nx * impulse, -ny * impulse, 0.42);
|
||||
t.lastSolidObstacleCollisionSource = item;
|
||||
t.lastSolidObstacleCollisionAt = worldRef.time || 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (contacts >= 10) break;
|
||||
}
|
||||
if (changed) {
|
||||
commitBody(item, "poison-block-tarinai-contact");
|
||||
worldRef.markSpatialDirty?.("poison-block-tarinai-contact");
|
||||
worldRef.drawListDirty = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function updatePoisonBlock(item, dt, worldRef, opts = {}) {
|
||||
if (!item || item.dead || item.type !== "poison_block") return false;
|
||||
applyBodyState(item);
|
||||
|
|
@ -1305,6 +1360,7 @@
|
|||
item.angle = normalizeAngle(beforeA + ps(item, "spin", 0) * subDt);
|
||||
changed = constrainPassiveBlockInsideWorld(item, worldRef) || changed;
|
||||
if (hasContacts) changed = resolveInteractions(item, subDt, worldRef) || changed;
|
||||
changed = resolvePoisonBlockTarinaiContacts(item, worldRef, subDt) || changed;
|
||||
changed = changed || Math.hypot(num(item.x) - beforeX, num(item.y) - beforeY) > 0.001 || Math.abs(num(item.angle) - beforeA) > 0.0001;
|
||||
}
|
||||
const friction = Math.pow(0.42, stepTime);
|
||||
|
|
@ -1472,6 +1528,9 @@
|
|||
const active = all.filter(r => r.active);
|
||||
if (!active.length) return [];
|
||||
const minUseFocused = Number(opts.focusThreshold || 48) || 48;
|
||||
// When only a few bodies are awake, build the broadphase around those
|
||||
// bodies plus nearby mechanical neighbors instead of scanning every static
|
||||
// collider in the field.
|
||||
if (!worldRef?.nearbyObstacles || all.length <= minUseFocused || active.length > Math.max(10, all.length * 0.45)) return all;
|
||||
const chosen = new Set(active.map(r => r.item));
|
||||
const mech = global.TarinaiMechanicalSystem;
|
||||
|
|
@ -1508,6 +1567,8 @@
|
|||
let cells = (maxX - minX + 1) * (maxY - minY + 1);
|
||||
if (!Number.isFinite(cells) || cells <= 0) cells = 1;
|
||||
if (cells > maxCellsPerBody) {
|
||||
// Oversized AABBs can explode grid insertion cost. Center-bucketing
|
||||
// keeps the frame bounded; the later narrowphase still checks geometry.
|
||||
const key = `${Math.floor(((a.left + a.right) * 0.5) / cellSize)}:${Math.floor(((a.top + a.bottom) * 0.5) / cellSize)}`;
|
||||
let bucket = grid.get(key);
|
||||
if (!bucket) { bucket = []; grid.set(key, bucket); }
|
||||
|
|
@ -1574,6 +1635,8 @@
|
|||
pairs += 1;
|
||||
if (pairCandidates.length < maxCandidates) pairCandidates.push({ a, b, key, score });
|
||||
else {
|
||||
// Keep only the highest-priority candidates so pathological overlap
|
||||
// scenes stay bounded before narrowphase resolution.
|
||||
let worst = 0;
|
||||
for (let k = 1; k < pairCandidates.length; k += 1) if (pairCandidates[k].score < pairCandidates[worst].score) worst = k;
|
||||
if (score > pairCandidates[worst].score) pairCandidates[worst] = { a, b, key, score };
|
||||
|
|
@ -1768,6 +1831,7 @@
|
|||
extent,
|
||||
worldSegments,
|
||||
obstacleRects,
|
||||
placementRects,
|
||||
poisonHazardRects,
|
||||
boundsAabb,
|
||||
hazardBoundsAabb,
|
||||
|
|
@ -1803,6 +1867,7 @@
|
|||
updateRotator,
|
||||
updateReciprocator,
|
||||
updatePoisonBlock,
|
||||
resolvePoisonBlockTarinaiContacts,
|
||||
applyPokeImpulse,
|
||||
hitTest,
|
||||
reach,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
(function (global) {
|
||||
|
||||
function rectLocalPoint(rect, x, y) {
|
||||
const shared = global.TarinaiCollisionFootprints?.rectLocalPoint;
|
||||
if (typeof shared === "function") return shared(rect, x, y);
|
||||
const dx = x - (rect.cx ?? 0);
|
||||
const dy = y - (rect.cy ?? 0);
|
||||
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
|
||||
|
|
@ -12,6 +14,8 @@
|
|||
|
||||
|
||||
function rectWorldVector(rect, x, y) {
|
||||
const shared = global.TarinaiCollisionFootprints?.rectWorldVector;
|
||||
if (typeof shared === "function") return shared(rect, x, y);
|
||||
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
|
||||
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
|
||||
return { x: x * c - y * ss, y: x * ss + y * c };
|
||||
|
|
|
|||
57
js/physics_projection_system.js
Normal file
57
js/physics_projection_system.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/projection-bridge
|
||||
// Shared tiny helpers for positional projection used by links, mechanical
|
||||
// bodies, and circle/circle collision code.
|
||||
(function (global) {
|
||||
function num(v, fallback = 0) { const n = Number(v); return Number.isFinite(n) ? n : fallback; }
|
||||
|
||||
function clampInsideWorld(worldRef, obj, radius = 0) {
|
||||
if (!worldRef || !obj) return false;
|
||||
const pad = Math.max(CONFIG.worldPadding || 30, num(radius, 0));
|
||||
const ox = num(obj.x), oy = num(obj.y);
|
||||
obj.x = clamp(ox, pad, worldRef.w - pad);
|
||||
obj.y = clamp(oy, pad, worldRef.h - pad);
|
||||
return obj.x !== ox || obj.y !== oy;
|
||||
}
|
||||
|
||||
function projectPair(worldRef, a, b, nx, ny, overlap, weights = {}) {
|
||||
const amount = Math.max(0, num(overlap));
|
||||
if (!worldRef || !a || !b || amount <= 0) return false;
|
||||
const aw = Number.isFinite(Number(weights.a)) ? Number(weights.a) : 0.5;
|
||||
const bw = Number.isFinite(Number(weights.b)) ? Number(weights.b) : 0.5;
|
||||
a.x = num(a.x) - num(nx) * amount * aw;
|
||||
a.y = num(a.y) - num(ny) * amount * aw;
|
||||
b.x = num(b.x) + num(nx) * amount * bw;
|
||||
b.y = num(b.y) + num(ny) * amount * bw;
|
||||
clampInsideWorld(worldRef, a, a.r || a.radius || 0);
|
||||
clampInsideWorld(worldRef, b, b.r || b.radius || 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
function markSpatialDirty(worldRef, item, reason = "projection", threshold = 0.35, minInterval = 0.065) {
|
||||
const world = worldRef || item?.world || null;
|
||||
if (!world || !item) return false;
|
||||
const now = Number(world.time || 0) || 0;
|
||||
const x = num(item.x), y = num(item.y), r = num(item.r || item.radius || 0);
|
||||
const lastX = Number(item._lastProjectionSpatialDirtyX);
|
||||
const lastY = Number(item._lastProjectionSpatialDirtyY);
|
||||
const lastR = Number(item._lastProjectionSpatialDirtyR);
|
||||
const missing = !Number.isFinite(lastX) || !Number.isFinite(lastY) || !Number.isFinite(lastR);
|
||||
const moved = missing || Math.hypot(x - lastX, y - lastY) > threshold || Math.abs(r - lastR) > threshold;
|
||||
const due = now >= Number(item._nextProjectionSpatialDirtyAt || 0);
|
||||
if (!moved && !due) return false;
|
||||
item._lastProjectionSpatialDirtyX = x;
|
||||
item._lastProjectionSpatialDirtyY = y;
|
||||
item._lastProjectionSpatialDirtyR = r;
|
||||
item._nextProjectionSpatialDirtyAt = now + Math.max(0.016, num(minInterval, 0.065));
|
||||
world.markSpatialDirty?.(reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
global.TarinaiPhysicsProjectionSystem = Object.freeze({
|
||||
clampInsideWorld,
|
||||
projectPair,
|
||||
markSpatialDirty,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
70
js/physics_shape_editor_system.js
Normal file
70
js/physics_shape_editor_system.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: ui/physics-shape-editor
|
||||
// Shared button labels/templates for rotator, reciprocator, and poison-block
|
||||
// segment editors. Low-level editor pointer behavior remains in placement log.
|
||||
(function (global) {
|
||||
const MODE_LABELS = Object.freeze({
|
||||
line: "📏 直線",
|
||||
free: "✎ 自由描画",
|
||||
erase: "⌫ 消しゴム",
|
||||
});
|
||||
const TEMPLATE_LABELS = Object.freeze({
|
||||
bar: "┃ 棒",
|
||||
cross: "+ 十字",
|
||||
circle: "○ 円",
|
||||
});
|
||||
const ACTION_LABELS = Object.freeze({
|
||||
undo: "↶ 一つ戻す",
|
||||
clear: "🗑 全消し",
|
||||
});
|
||||
function templateSegments(name) {
|
||||
if (name === "bar") return [[-130, 0, 130, 0]];
|
||||
if (name === "cross") return [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
||||
if (name === "circle") {
|
||||
const segments = [];
|
||||
const n = 18, r = 108;
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
const a = i / n * Math.PI * 2;
|
||||
const b = (i + 1) / n * Math.PI * 2;
|
||||
segments.push([Math.cos(a) * r, Math.sin(a) * r, Math.cos(b) * r, Math.sin(b) * r]);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function decorateToolbar(dialog, prefix = "rotator") {
|
||||
if (!dialog) return false;
|
||||
for (const btn of dialog.querySelectorAll(`[data-${prefix}-mode]`)) {
|
||||
const key = btn.dataset[`${prefix}Mode`];
|
||||
if (MODE_LABELS[key]) { btn.textContent = MODE_LABELS[key]; btn.title = MODE_LABELS[key].replace(/^[^\s]+\s*/, ""); }
|
||||
}
|
||||
for (const btn of dialog.querySelectorAll(`[data-${prefix}-template]`)) {
|
||||
const key = btn.dataset[`${prefix}Template`];
|
||||
if (TEMPLATE_LABELS[key]) { btn.textContent = TEMPLATE_LABELS[key]; btn.title = TEMPLATE_LABELS[key].replace(/^[^\s]+\s*/, ""); }
|
||||
}
|
||||
const undo = dialog.querySelector(`#${prefix}Undo`);
|
||||
const clear = dialog.querySelector(`#${prefix}Clear`);
|
||||
if (undo) undo.textContent = ACTION_LABELS.undo;
|
||||
if (clear) clear.textContent = ACTION_LABELS.clear;
|
||||
return true;
|
||||
}
|
||||
function pointSegmentDistance(p, seg) {
|
||||
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0;
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const lenSq = dx * dx + dy * dy;
|
||||
if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1);
|
||||
const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq));
|
||||
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||
}
|
||||
global.TarinaiPhysicsShapeEditorSystem = Object.freeze({
|
||||
MODE_LABELS,
|
||||
TEMPLATE_LABELS,
|
||||
ACTION_LABELS,
|
||||
decorateToolbar,
|
||||
templateSegments,
|
||||
pointSegmentDistance,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -71,8 +71,8 @@
|
|||
collision: { solid: true, hazard: type === "poison_block" },
|
||||
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
||||
sleep: { awakeUntil: 0 },
|
||||
mass: type === "poison_block" ? 4.8 : 1,
|
||||
inertia: type === "poison_block" ? 36000 : 22000,
|
||||
mass: type === "poison_block" ? 0.45 : 1,
|
||||
inertia: type === "poison_block" ? 3200 : 22000,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -232,8 +232,8 @@
|
|||
case "slideSpeed": b.velocity.linear = num(value); break;
|
||||
case "solid": b.collision.solid = !!value; break;
|
||||
case "damage": b.hazard = b.hazard || { kind: "poison", damage: 7 }; b.hazard.damage = Math.max(1, num(value, 7)); break;
|
||||
case "mass": b.mass = Math.max(0.2, num(value, 4.8)); break;
|
||||
case "inertia": b.inertia = Math.max(1, num(value, 36000)); break;
|
||||
case "mass": b.mass = Math.max(0.12, num(value, 0.45)); break;
|
||||
case "inertia": b.inertia = Math.max(1, num(value, item.type === "poison_block" ? 3200 : 36000)); break;
|
||||
case "railOn": b.motor.powered = !!value; break;
|
||||
case "railMotorSpeed": b.motor.speed = Math.max(0, num(value, 92)); break;
|
||||
case "railTravel": if (b.rail) b.rail.travel = Math.max(24, num(value, 150)); break;
|
||||
|
|
|
|||
130
js/pin_attachment_system.js
Normal file
130
js/pin_attachment_system.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/items/pin-attachment
|
||||
// Centralizes pushpin/oshibyo attachment geometry, target classification, and
|
||||
// render attachment placement. Motion/effects still live in dynamic systems.
|
||||
(function (global) {
|
||||
function isPin(itemOrType) {
|
||||
const type = typeof itemOrType === "string" ? itemOrType : itemOrType?.type;
|
||||
return Boolean(type && typeof global.isPinType === "function" && global.isPinType(type));
|
||||
}
|
||||
function behaviorFor(itemOrType) {
|
||||
const type = typeof itemOrType === "string" ? itemOrType : itemOrType?.type;
|
||||
return typeof global.pinBehaviorFor === "function" ? global.pinBehaviorFor(type) : null;
|
||||
}
|
||||
function isOshibyo(itemOrType) {
|
||||
return Boolean(behaviorFor(itemOrType)?.blocksZunchi);
|
||||
}
|
||||
function tarinaiVisualSide(item, tarinai) {
|
||||
const faceDir = tarinai?.facingDir ? tarinai.facingDir() : (tarinai?.facing || 1);
|
||||
const faceSide = faceDir >= 0 ? 1 : -1;
|
||||
return isOshibyo(item) ? -faceSide : faceSide;
|
||||
}
|
||||
function tarinaiAnchor(item, tarinai, opts = {}) {
|
||||
const r = tarinai?.radius || 16;
|
||||
const visualSide = Number.isFinite(opts.visualSide) ? opts.visualSide : tarinaiVisualSide(item, tarinai);
|
||||
if (isOshibyo(item)) {
|
||||
const sprite = String(tarinai?.visibleSpriteId || (tarinai?.rawSpriteId ? tarinai.rawSpriteId() : "") || "");
|
||||
const backSprite = sprite === "back" || sprite === "tarinai12_back" || /(^|_)back$/i.test(sprite);
|
||||
const xFactor = backSprite ? 0.62 : 0.96;
|
||||
const yFactor = backSprite ? 0.28 : 0.50;
|
||||
const angleFactor = backSprite ? 0.32 : 0.48;
|
||||
const distFactor = backSprite ? 0.70 : 0.96;
|
||||
return {
|
||||
visualSide,
|
||||
x: (tarinai?.x || 0) + visualSide * r * xFactor,
|
||||
y: (tarinai?.y || 0) + r * yFactor,
|
||||
angle: visualSide * angleFactor,
|
||||
distance: r * distFactor,
|
||||
offsetY: r * yFactor,
|
||||
};
|
||||
}
|
||||
const dx = Number.isFinite(opts.dx) ? opts.dx : 0;
|
||||
const dy = Number.isFinite(opts.dy) ? opts.dy : -1;
|
||||
const dist = Number.isFinite(opts.distance) ? opts.distance : Math.hypot(dx, dy);
|
||||
const clampFn = global.clamp || ((v, lo, hi) => Math.max(lo, Math.min(hi, v)));
|
||||
return {
|
||||
visualSide,
|
||||
x: (tarinai?.x || 0) + visualSide * r * 0.36,
|
||||
y: (tarinai?.y || 0) - r * 0.04,
|
||||
angle: Math.atan2(dy || -1, dx || visualSide),
|
||||
poseAngle: visualSide * 0.28,
|
||||
distance: clampFn(dist || r * 0.58, r * 0.20, r * 0.74),
|
||||
offsetY: clampFn(dy, -r * 0.74, r * 0.38),
|
||||
};
|
||||
}
|
||||
function applyTarinaiPose(item, tarinai, worldRef = null) {
|
||||
if (!item || !tarinai) return null;
|
||||
const anchor = tarinaiAnchor(item, tarinai);
|
||||
item.pinVisualSide = anchor.visualSide;
|
||||
item.x = anchor.x;
|
||||
item.y = anchor.y;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.spin = isOshibyo(item) ? anchor.angle : (anchor.poseAngle ?? anchor.angle);
|
||||
item.vx = tarinai.vx || 0;
|
||||
item.vy = tarinai.vy || 0;
|
||||
item.deletable = false;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return anchor;
|
||||
}
|
||||
function ballAnchor(item, ball) {
|
||||
const angle = (ball?.spin || 0) + (item?.pinBallOffsetAngle || 0);
|
||||
const distance = Math.max(4, Number(item?.pinBallOffsetDistance || (ball?.r || 18) * 0.55) || (ball?.r || 18) * 0.55);
|
||||
return {
|
||||
angle,
|
||||
distance,
|
||||
visualSide: Math.cos(angle) >= 0 ? 1 : -1,
|
||||
x: (ball?.x || 0) + Math.cos(angle) * distance,
|
||||
y: (ball?.y || 0) + Math.sin(angle) * distance,
|
||||
spin: angle + (isOshibyo(item) ? 0.18 : Math.PI * 0.5),
|
||||
};
|
||||
}
|
||||
function applyBallPose(item, ball, worldRef = null) {
|
||||
if (!item || !ball) return null;
|
||||
const anchor = ballAnchor(item, ball);
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.x = anchor.x;
|
||||
item.y = anchor.y;
|
||||
item.vx = ball.vx || 0;
|
||||
item.vy = ball.vy || 0;
|
||||
item.pinVisualSide = anchor.visualSide;
|
||||
item.spin = anchor.spin;
|
||||
item.deletable = false;
|
||||
if (Number.isFinite(ball.spinVelocity)) item.spinVelocity = ball.spinVelocity;
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return anchor;
|
||||
}
|
||||
function isAttachedToEntity(pin, entity) {
|
||||
if (!pin || !entity || !isPin(pin)) return false;
|
||||
if (entity.type === "ball") return pin.pinState === "ball_lodged" && pin.pinBallId === entity.id;
|
||||
const isTarinaiEntity = typeof global.Tarinai !== "undefined" && entity instanceof global.Tarinai;
|
||||
return isTarinaiEntity && pin.pinState === "lodged" && pin.pinTargetId === entity.id;
|
||||
}
|
||||
function drawAttachedForEntity(args = {}) {
|
||||
const { renderStack, entity, visibleRect, worldRef, time, drawnIds, isVisible } = args;
|
||||
if (!renderStack || !entity || !drawnIds || typeof isVisible !== "function") return 0;
|
||||
const source = entity.type === "ball" ? (renderStack.ballLodgedPins || []) : (renderStack.lodgedPins || []);
|
||||
let drawn = 0;
|
||||
for (const it of source) {
|
||||
if (!it || drawnIds.has(it.id) || !isAttachedToEntity(it, entity)) continue;
|
||||
if (isVisible(it, visibleRect)) it.draw?.(global.ctx || args.ctx, time ?? worldRef?.time ?? 0, args.lighting || null);
|
||||
drawnIds.add(it.id);
|
||||
drawn += 1;
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
global.TarinaiPinAttachmentSystem = Object.freeze({
|
||||
isPin,
|
||||
behaviorFor,
|
||||
isOshibyo,
|
||||
tarinaiVisualSide,
|
||||
tarinaiAnchor,
|
||||
applyTarinaiPose,
|
||||
ballAnchor,
|
||||
applyBallPose,
|
||||
isAttachedToEntity,
|
||||
drawAttachedForEntity,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
41
js/placement_collision_system.js
Normal file
41
js/placement_collision_system.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/placement-collision
|
||||
// Single facade for placement, copy, and pointer hit testing. Existing callers
|
||||
// should use this instead of reaching into raw footprint helpers directly.
|
||||
(function (global) {
|
||||
function footprints() { return global.TarinaiCollisionFootprints || null; }
|
||||
|
||||
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
||||
return footprints()?.hitTestItem?.(worldRef, item, x, y, opts) || { hit: false, distance: Infinity };
|
||||
}
|
||||
|
||||
function placementRectsFor(worldRef, item) {
|
||||
return footprints()?.placementRectsFor?.(worldRef, item) || [];
|
||||
}
|
||||
|
||||
function isSoftPlacementIgnoredType(type) {
|
||||
return Boolean(footprints()?.isSoftPlacementIgnoredType?.(type));
|
||||
}
|
||||
|
||||
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
||||
return Boolean(footprints()?.itemPlacementOverlapBlocked?.(worldRef, item, opts));
|
||||
}
|
||||
|
||||
function rectsOverlap(a, b, margin = 0) {
|
||||
return Boolean(footprints()?.rectsOverlap?.(a, b, margin));
|
||||
}
|
||||
|
||||
function rectCircleOverlap(rect, cx, cy, radius, margin = 0) {
|
||||
return Boolean(footprints()?.rectCircleOverlap?.(rect, cx, cy, radius, margin));
|
||||
}
|
||||
|
||||
global.TarinaiPlacementCollisionSystem = Object.freeze({
|
||||
hitTestItem,
|
||||
placementRectsFor,
|
||||
isSoftPlacementIgnoredType,
|
||||
itemPlacementOverlapBlocked,
|
||||
rectsOverlap,
|
||||
rectCircleOverlap,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
204
js/placement_preview_system.js
Normal file
204
js/placement_preview_system.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/placement-preview
|
||||
// Single source of truth for placement preview geometry and the boundary part
|
||||
// of placement blocking. Render and actual placement both delegate here so a
|
||||
// green preview means the same footprint the click will try to place.
|
||||
(function (global) {
|
||||
function num(v, fallback = 0) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
function itemRadius(type, fallback = 12) {
|
||||
return typeof global.itemRadiusFor === "function" ? global.itemRadiusFor(type, fallback) : fallback;
|
||||
}
|
||||
function toolTypeFor(worldRef) {
|
||||
return typeof global.toolItemType === "function" ? global.toolItemType(worldRef?.tool || "") : "";
|
||||
}
|
||||
function toolSizeScale(worldRef, type) {
|
||||
return worldRef?.toolSizeScale ? worldRef.toolSizeScale(type) : 1;
|
||||
}
|
||||
function toolSizeName(worldRef, type) {
|
||||
return worldRef?.toolSizeFor ? worldRef.toolSizeFor(type) : (worldRef?.toolSize || "medium");
|
||||
}
|
||||
function isRotatable(type) {
|
||||
return typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type);
|
||||
}
|
||||
function defaultAngle(type) {
|
||||
return typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
}
|
||||
function rectOutside(worldRef, rect) {
|
||||
if (!worldRef || !rect) return true;
|
||||
const pad = global.CONFIG?.worldPadding || 30;
|
||||
return num(rect.left) < pad || num(rect.top) < pad || num(rect.right) > (worldRef.w || 0) - pad || num(rect.bottom) > (worldRef.h || 0) - pad;
|
||||
}
|
||||
function circleOutside(worldRef, x, y, radius) {
|
||||
if (!worldRef) return true;
|
||||
const pad = global.CONFIG?.worldPadding || 30;
|
||||
const r = Math.max(0, num(radius));
|
||||
return num(x) - r < pad || num(y) - r < pad || num(x) + r > (worldRef.w || 0) - pad || num(y) + r > (worldRef.h || 0) - pad;
|
||||
}
|
||||
function orientedAabb(rect) {
|
||||
if (!rect) return null;
|
||||
if (!rect.oriented) return rect;
|
||||
const fp = global.TarinaiCollisionFootprints;
|
||||
const pts = fp?.rectCorners?.(rect, 0) || [];
|
||||
if (!pts.length) {
|
||||
const reach = Math.max(num(rect.halfW), num(rect.halfH));
|
||||
return { left: num(rect.cx) - reach, right: num(rect.cx) + reach, top: num(rect.cy) - reach, bottom: num(rect.cy) + reach };
|
||||
}
|
||||
return {
|
||||
left: Math.min(...pts.map(p => num(p.x))),
|
||||
right: Math.max(...pts.map(p => num(p.x))),
|
||||
top: Math.min(...pts.map(p => num(p.y))),
|
||||
bottom: Math.max(...pts.map(p => num(p.y))),
|
||||
};
|
||||
}
|
||||
function rectsFor(worldRef, item) {
|
||||
return (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.placementRectsFor?.(worldRef, item) || worldRef?.solidObstacleRects?.(item) || [];
|
||||
}
|
||||
function footprintRadius(worldRef, item, rects = null) {
|
||||
return (global.TarinaiCollisionFootprints || global.TarinaiPlacementCollisionSystem)?.itemFootprintRadius?.(worldRef, item, rects)
|
||||
|| Math.max(14, num(item?.r, itemRadius(item?.type, 12)) || itemRadius(item?.type, 12));
|
||||
}
|
||||
function ensureMechanicalPreview(item, worldRef) {
|
||||
if (!item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) return item;
|
||||
const pb = global.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(item, worldRef || null, { syncFromLegacy: false });
|
||||
if (item.type === "rotator") {
|
||||
pb?.setScalar?.(item, "motorSpeed", Math.PI * 0.65, "preview");
|
||||
pb?.setScalar?.(item, "motorOn", true, "preview");
|
||||
pb?.setScalar?.(item, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(item, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview");
|
||||
} else if (item.type === "poison_block") {
|
||||
pb?.setScalar?.(item, "thickness", 14, "preview");
|
||||
item.world = worldRef || null;
|
||||
} else if (item.type === "reciprocator") {
|
||||
pb?.setScalar?.(item, "railOn", true, "preview");
|
||||
pb?.setScalar?.(item, "railMotorSpeed", 92, "preview");
|
||||
pb?.setScalar?.(item, "railTravel", 150, "preview");
|
||||
pb?.setScalar?.(item, "railPhase", 0, "preview");
|
||||
pb?.setScalar?.(item, "railAnchorX", item.x, "preview");
|
||||
pb?.setScalar?.(item, "railAnchorY", item.y, "preview");
|
||||
pb?.setScalar?.(item, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(item, [[-78, 0, 78, 0]], "preview");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
function makePreviewItem(worldRef, type, x, y, opts = {}) {
|
||||
if (!worldRef || !type) return null;
|
||||
const scale = opts.sizeScale ?? toolSizeScale(worldRef, type);
|
||||
const sizeName = opts.toolSize ?? toolSizeName(worldRef, type);
|
||||
const r = (opts.r ?? itemRadius(type, 12)) * scale;
|
||||
const item = { type, x: num(x), y: num(y), r, toolSize: sizeName, foodServingScale: scale, dead: false, isPlacementPreview: true };
|
||||
if (isRotatable(type)) item.angle = opts.angle ?? (worldRef?.toolAngleFor ? worldRef.toolAngleFor(type) : defaultAngle(type));
|
||||
if (type === "reciprocator") {
|
||||
const axis = item.angle ?? defaultAngle(type);
|
||||
global.TarinaiPhysicsBodySystem?.setScalar?.(item, "railAxis", axis, "preview-axis");
|
||||
item.angle = defaultAngle(type);
|
||||
}
|
||||
ensureMechanicalPreview(item, worldRef);
|
||||
return item;
|
||||
}
|
||||
function boundsBlocked(worldRef, item, opts = {}) {
|
||||
if (!worldRef || !item || item.dead) return true;
|
||||
const rects = opts.rects || rectsFor(worldRef, item);
|
||||
if (rects.length) return rects.some(rect => rectOutside(worldRef, orientedAabb(rect)));
|
||||
const type = item.type || "";
|
||||
const radius = opts.radius ?? Math.max(14, (num(item.r, itemRadius(type, 12)) || itemRadius(type, 12)) * (type === "bed" ? 1.55 : 1.25));
|
||||
return circleOutside(worldRef, item.x, item.y, radius);
|
||||
}
|
||||
function clampPointFor(worldRef, item, x = item?.x || 0, y = item?.y || 0) {
|
||||
if (!worldRef || !item) return { x, y };
|
||||
const pad = global.CONFIG?.worldPadding || 30;
|
||||
const rects = rectsFor(worldRef, item);
|
||||
if (rects.length) {
|
||||
let left = 0, right = 0, top = 0, bottom = 0;
|
||||
for (const rect of rects) {
|
||||
const aabb = orientedAabb(rect);
|
||||
if (!aabb) continue;
|
||||
left = Math.max(left, num(item.x) - num(aabb.left));
|
||||
right = Math.max(right, num(aabb.right) - num(item.x));
|
||||
top = Math.max(top, num(item.y) - num(aabb.top));
|
||||
bottom = Math.max(bottom, num(aabb.bottom) - num(item.y));
|
||||
}
|
||||
return { x: global.clamp ? global.clamp(x, pad + left, (worldRef.w || 0) - pad - right) : Math.max(pad + left, Math.min((worldRef.w || 0) - pad - right, x)), y: global.clamp ? global.clamp(y, pad + top, (worldRef.h || 0) - pad - bottom) : Math.max(pad + top, Math.min((worldRef.h || 0) - pad - bottom, y)) };
|
||||
}
|
||||
const radius = Math.max(14, (num(item.r, itemRadius(item.type, 12)) || itemRadius(item.type, 12)) * (item.type === "bed" ? 1.55 : 1.25));
|
||||
return { x: global.clamp ? global.clamp(x, pad + radius, (worldRef.w || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.w || 0) - pad - radius, x)), y: global.clamp ? global.clamp(y, pad + radius, (worldRef.h || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.h || 0) - pad - radius, y)) };
|
||||
}
|
||||
function geometryFor(worldRef, item) {
|
||||
if (!worldRef || !item) return null;
|
||||
ensureMechanicalPreview(item, worldRef);
|
||||
if ((item.type === "fence_v" || item.type === "fence_h" || item.type === "bounce_fence" || item.type === "bounce_fence_v" || item.type === "gate_fence") && worldRef.fenceRect) {
|
||||
return { rect: worldRef.fenceRect(item), rects: [] };
|
||||
}
|
||||
if (item.type === "nest_box" && worldRef.nestBoxBaseRect) return { rect: worldRef.nestBoxBaseRect(item), rects: [] };
|
||||
const rects = rectsFor(worldRef, item);
|
||||
return { rect: null, rects };
|
||||
}
|
||||
function blockedFor(worldRef, item, opts = {}) {
|
||||
const geometry = opts.geometry || geometryFor(worldRef, item) || {};
|
||||
const rects = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null);
|
||||
const b = boundsBlocked(worldRef, item, { rects });
|
||||
if (b) return true;
|
||||
if (opts.skipWorldBlocked) return false;
|
||||
return Boolean(worldRef?.placementBlocked?.(item) || (worldRef?.fencePlacementBlocked?.(item) && worldRef?.isFenceType?.(item.type)));
|
||||
}
|
||||
function previewForItem(worldRef, item, opts = {}) {
|
||||
if (!worldRef || !item) return null;
|
||||
const geometry = geometryFor(worldRef, item) || {};
|
||||
const rects = geometry.rects || [];
|
||||
const radius = footprintRadius(worldRef, item, rects);
|
||||
const bounds = boundsBlocked(worldRef, item, { rects: rects.length ? rects : (geometry.rect ? [geometry.rect] : null) });
|
||||
return {
|
||||
type: item.type,
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
r: item.r || radius,
|
||||
rect: geometry.rect || null,
|
||||
rects,
|
||||
boundsBlocked: bounds,
|
||||
blocked: opts.skipBlocked ? bounds : (bounds || Boolean(worldRef.placementBlocked?.(item) || worldRef.fencePlacementBlocked?.(item))),
|
||||
};
|
||||
}
|
||||
function previewForWorld(worldRef) {
|
||||
const p = worldRef?.pointer;
|
||||
const type = toolTypeFor(worldRef);
|
||||
if (!p?.inside || !type) return null;
|
||||
const x = num(p.x), y = num(p.y);
|
||||
const tmp = makePreviewItem(worldRef, type, x, y);
|
||||
if (!tmp) return null;
|
||||
const r = tmp.r;
|
||||
let extra = {};
|
||||
if (type === "sprinkler") {
|
||||
const range = global.TarinaiItemDynamicToolSystem?.sprinklerRange?.(tmp) || Math.max(288, r * 13.8);
|
||||
extra.influence = { kind: "sprinkler", x, y, radius: range };
|
||||
extra.boundsRadius = Math.max(14, r * 1.25);
|
||||
}
|
||||
if (type === "grass") {
|
||||
const overlaps = global.TarinaiPhysicsSoftClear?.collectForPlacement?.(worldRef, tmp) || [];
|
||||
const replacingGrass = overlaps.some(it => it?.type === "grass");
|
||||
const limitBlocked = !(worldRef.canAddGrass?.(1) ?? true) && !replacingGrass;
|
||||
const softBlocked = Boolean(worldRef.grassBlockedAt?.(x, y, tmp, { ignoreSoftPlacement: true }) || worldRef.grassOnTarinaiAt?.(x, y));
|
||||
const b = limitBlocked || softBlocked || circleOutside(worldRef, x, y, Math.max(14, r * 1.55));
|
||||
return { type, x, y, r, blocked: b, item: tmp };
|
||||
}
|
||||
const geometry = geometryFor(worldRef, tmp) || {};
|
||||
const rectsForBounds = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null);
|
||||
const ownBounds = extra.boundsRadius ? circleOutside(worldRef, x, y, extra.boundsRadius) : boundsBlocked(worldRef, tmp, { rects: rectsForBounds });
|
||||
const blocked = ownBounds || Boolean(worldRef.placementBlocked?.(tmp));
|
||||
return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], influence: extra.influence || null, blocked, item: tmp };
|
||||
}
|
||||
global.TarinaiPlacementPreviewSystem = Object.freeze({
|
||||
makePreviewItem,
|
||||
rectOutside,
|
||||
circleOutside,
|
||||
boundsBlocked,
|
||||
clampPointFor,
|
||||
geometryFor,
|
||||
blockedFor,
|
||||
forItem: previewForItem,
|
||||
forWorld: previewForWorld,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
189
js/render.js
189
js/render.js
|
|
@ -466,86 +466,33 @@ function drawItemGlow(ctx, item, lighting, visibleAlpha = 1) {
|
|||
|
||||
function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale = 1) {
|
||||
if (!influence) return;
|
||||
const x = Number(influence.x || 0) || 0;
|
||||
const y = Number(influence.y || 0) || 0;
|
||||
const radius = Math.max(1, Number(influence.radius || 0) || 0);
|
||||
if (!(radius > 0)) return;
|
||||
ctx.save();
|
||||
ctx.setLineDash([10, 7]);
|
||||
ctx.lineWidth = 1.6;
|
||||
ctx.globalAlpha = (blocked ? 0.30 : 0.40) * Math.max(0.2, Number(alphaScale || 1) || 1);
|
||||
ctx.setLineDash([12, 9]);
|
||||
ctx.lineWidth = 2.0;
|
||||
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.72)" : "rgba(80,174,222,0.82)";
|
||||
ctx.fillStyle = blocked ? "rgba(232,84,72,0.055)" : "rgba(80,174,222,0.075)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([3, 9]);
|
||||
ctx.globalAlpha *= 0.72;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius * 0.55, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function placementPreviewFor(world) {
|
||||
const p = world?.pointer;
|
||||
const type = toolItemType(world?.tool || "");
|
||||
if (!p?.inside || !type) return null;
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
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, toolSize: sizeName, foodServingScale: sizeScale, dead: false };
|
||||
if (typeof isRotatableItemType === "function" && isRotatableItemType(type)) tmp.angle = world?.toolAngleFor ? world.toolAngleFor(type) : (typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
|
||||
if (type === "reciprocator") {
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "railAxis", tmp.angle, "preview-axis");
|
||||
tmp.angle = typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0;
|
||||
pb?.syncPoseFromItem?.(tmp);
|
||||
}
|
||||
if ((type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") && world.fenceRect) {
|
||||
const rawRect = world.fenceRect(tmp);
|
||||
return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "rotator" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "motorSpeed", Math.PI * 0.65, "preview");
|
||||
pb?.setScalar?.(tmp, "motorOn", true, "preview");
|
||||
pb?.setScalar?.(tmp, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(tmp, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview");
|
||||
const rects = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.obstacleRects(tmp) || [];
|
||||
const bounds = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.boundsAabb(tmp) || null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "poison_block" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
// Use the actual default poison-block physics footprint for placement
|
||||
// feedback. Previously it fell through to the generic oval preview, which
|
||||
// did not match the placed shape.
|
||||
pb?.ensureBody?.(tmp, null, { syncFromLegacy: false });
|
||||
tmp.world = null;
|
||||
const rects = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.obstacleRects(tmp) || [];
|
||||
const bounds = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.boundsAabb(tmp) || null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "reciprocator" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "railOn", true, "preview");
|
||||
pb?.setScalar?.(tmp, "railMotorSpeed", 92, "preview");
|
||||
pb?.setScalar?.(tmp, "railTravel", 150, "preview");
|
||||
pb?.setScalar?.(tmp, "railPhase", 0, "preview");
|
||||
pb?.setScalar?.(tmp, "railAnchorX", x, "preview");
|
||||
pb?.setScalar?.(tmp, "railAnchorY", y, "preview");
|
||||
pb?.setScalar?.(tmp, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(tmp, [[-78, 0, 78, 0]], "preview");
|
||||
const rects = world.reciprocatorObstacleRects(tmp) || [];
|
||||
const bounds = rects.length ? { left: Math.min(...rects.map(rr => rr.left)), right: Math.max(...rects.map(rr => rr.right)), top: Math.min(...rects.map(rr => rr.top)), bottom: Math.max(...rects.map(rr => rr.bottom)) } : null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "nest_box" && world.nestBoxBaseRect) {
|
||||
const rect = world.nestBoxBaseRect(tmp);
|
||||
return { type, x, y, r, rect, blocked: rectOutside(rect) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "grass") {
|
||||
blocked = !(world.canAddGrass?.(1) ?? true) || !world.grassSpotOpen?.(x, y, { avoidTarinai: true }) || circleOutside(x, y, Math.max(14, r * 1.55));
|
||||
}
|
||||
if (!blocked) blocked = circleOutside(x, y, Math.max(14, r * (type === "bed" ? 1.55 : 1.25))) || world.placementBlocked?.(tmp);
|
||||
return { type, x, y, r, blocked };
|
||||
return TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forWorld?.(world) || null;
|
||||
}
|
||||
|
||||
|
||||
function drawPlacementPreview(ctx, world) {
|
||||
const preview = placementPreviewFor(world);
|
||||
if (!preview) return;
|
||||
|
|
@ -614,19 +561,10 @@ function drawOperationToolPreview(ctx, world) {
|
|||
const type = world.copyBuffer.type || "";
|
||||
const item = world.copyPreviewItemAt?.(p.x, p.y);
|
||||
if (item) {
|
||||
const pad = typeof CONFIG !== "undefined" ? (CONFIG.worldPadding ?? 0) : 0;
|
||||
const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad));
|
||||
const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad;
|
||||
const previewRects = world.solidObstacleRects?.(item) || [];
|
||||
const boundsBlocked = previewRects.length
|
||||
? previewRects.some(rr => rectOutside(rr.oriented ? {
|
||||
left: (rr.cx || p.x) - (rr.halfW || 10) - (rr.halfH || 5),
|
||||
right: (rr.cx || p.x) + (rr.halfW || 10) + (rr.halfH || 5),
|
||||
top: (rr.cy || p.y) - (rr.halfW || 10) - (rr.halfH || 5),
|
||||
bottom: (rr.cy || p.y) + (rr.halfW || 10) + (rr.halfH || 5),
|
||||
} : rr))
|
||||
: circleOutside(p.x, p.y, Math.max(14, (item.r || itemRadiusFor?.(type, 16) || 16) * 1.25));
|
||||
const blocked = boundsBlocked || Boolean(world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item));
|
||||
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forItem?.(world, item) || null;
|
||||
const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || [];
|
||||
const boundsBlocked = Boolean(placementInfo?.boundsBlocked);
|
||||
const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item)));
|
||||
let copyPreviewDrew = false;
|
||||
if (typeof item.draw === "function") {
|
||||
ctx.save();
|
||||
|
|
@ -694,6 +632,30 @@ function drawOperationToolPreview(ctx, world) {
|
|||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (world?.tool === "water_hose") {
|
||||
const radius = world.waterHoseRadius?.() || 138;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.48;
|
||||
ctx.lineWidth = 2.1;
|
||||
ctx.setLineDash([8, 7]);
|
||||
ctx.strokeStyle = "rgba(72,166,216,0.84)";
|
||||
ctx.fillStyle = "rgba(72,166,216,0.10)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.globalAlpha = 0.72;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x - 10, p.y);
|
||||
ctx.lineTo(p.x + 10, p.y);
|
||||
ctx.moveTo(p.x, p.y - 10);
|
||||
ctx.lineTo(p.x, p.y + 10);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
if (world?.tool !== "area_delete") return;
|
||||
const radius = world.areaDeleteRadius?.() || 138;
|
||||
ctx.save();
|
||||
|
|
@ -1000,7 +962,7 @@ function syncCarriedPlushieToOwner(it, worldRef) {
|
|||
|
||||
function ensureWorldRenderStack(worldRef) {
|
||||
if (!worldRef._renderStack) {
|
||||
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [] };
|
||||
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
|
||||
}
|
||||
const stack = worldRef._renderStack;
|
||||
const signature = renderStackSignature(worldRef);
|
||||
|
|
@ -1010,6 +972,7 @@ function ensureWorldRenderStack(worldRef) {
|
|||
stack.layered.length = 0;
|
||||
stack.carriedPlushies.length = 0;
|
||||
stack.lodgedPins.length = 0;
|
||||
if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0;
|
||||
|
||||
for (const it of worldRef.items || []) {
|
||||
if (!it || it.dead || isCachedTerrainItem(it)) continue;
|
||||
|
|
@ -1017,6 +980,10 @@ function ensureWorldRenderStack(worldRef) {
|
|||
stack.lodgedPins.push(it);
|
||||
continue;
|
||||
}
|
||||
if (isPinType(it.type) && it.pinState === "ball_lodged") {
|
||||
(stack.ballLodgedPins || (stack.ballLodgedPins = [])).push(it);
|
||||
continue;
|
||||
}
|
||||
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
||||
stack.carriedPlushies.push(it);
|
||||
continue;
|
||||
|
|
@ -1042,12 +1009,13 @@ function ensureWorldRenderStack(worldRef) {
|
|||
function collectVisibleRenderStack(worldRef, visibleRect) {
|
||||
const end = window.TarinaiPerf?.begin?.("render.stackBuild") || null;
|
||||
try {
|
||||
if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [] };
|
||||
if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
|
||||
const stack = worldRef._visibleRenderStack;
|
||||
stack.backItems.length = 0;
|
||||
stack.layered.length = 0;
|
||||
stack.carriedPlushies.length = 0;
|
||||
stack.lodgedPins.length = 0;
|
||||
if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0;
|
||||
const seen = new Set();
|
||||
const addItem = (it) => {
|
||||
if (it && it.isStructure && it.type === "plushie" && it.carriedById) syncCarriedPlushieToOwner(it, worldRef);
|
||||
|
|
@ -1057,6 +1025,10 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
|
|||
stack.lodgedPins.push(it);
|
||||
return;
|
||||
}
|
||||
if (typeof isPinType === "function" && isPinType(it.type) && it.pinState === "ball_lodged") {
|
||||
(stack.ballLodgedPins || (stack.ballLodgedPins = [])).push(it);
|
||||
return;
|
||||
}
|
||||
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
||||
stack.carriedPlushies.push(it);
|
||||
return;
|
||||
|
|
@ -1071,6 +1043,8 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
|
|||
};
|
||||
worldRef.ensureSpatial?.("render-visible");
|
||||
if (worldRef.spatial?.nearbyRectInto) {
|
||||
// Pull only visible spatial buckets, then do a render-radius check for
|
||||
// oversized sprites/effects that extend outside their origin cell.
|
||||
const itemScratch = worldRef._renderVisibleItemsScratch || (worldRef._renderVisibleItemsScratch = []);
|
||||
itemScratch.length = 0;
|
||||
worldRef.spatial.nearbyRectInto(worldRef.spatial.staticItemCells || worldRef.spatial.itemCells, visibleRect, itemScratch);
|
||||
|
|
@ -1616,9 +1590,37 @@ function render() {
|
|||
|
||||
// Parent-follow guide lines intentionally disabled; they looked like stray lines between Tarinai.
|
||||
|
||||
const drawnAttachedPinIds = new Set();
|
||||
const drawAttachedPinsForEntity = (entity) => {
|
||||
if (!entity) return;
|
||||
if (TARINAI_RENDER_GLOBAL.TarinaiPinAttachmentSystem?.drawAttachedForEntity) {
|
||||
TARINAI_RENDER_GLOBAL.TarinaiPinAttachmentSystem.drawAttachedForEntity({ renderStack, entity, visibleRect, worldRef: world, time: world.time, drawnIds: drawnAttachedPinIds, isVisible: isEntityVisibleInRect, ctx, lighting });
|
||||
return;
|
||||
}
|
||||
const isBall = entity.type === "ball";
|
||||
const isTarinaiEntity = typeof Tarinai !== "undefined" && entity instanceof Tarinai;
|
||||
if (isBall) {
|
||||
for (const it of renderStack.ballLodgedPins || []) {
|
||||
if (!it || drawnAttachedPinIds.has(it.id) || it.pinBallId !== entity.id) continue;
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
drawnAttachedPinIds.add(it.id);
|
||||
}
|
||||
}
|
||||
if (isTarinaiEntity) {
|
||||
for (const it of renderStack.lodgedPins || []) {
|
||||
if (!it || drawnAttachedPinIds.has(it.id) || it.pinTargetId !== entity.id) continue;
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
drawnAttachedPinIds.add(it.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const endLayered = renderPerfBegin("render.draw.layered");
|
||||
for (const entry of renderStack.layered) {
|
||||
if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting);
|
||||
if (isEntityVisibleInRect(entry.entity, visibleRect)) {
|
||||
entry.entity.draw(ctx, world.time, lighting);
|
||||
drawAttachedPinsForEntity(entry.entity);
|
||||
}
|
||||
}
|
||||
renderPerfEnd(endLayered);
|
||||
|
||||
|
|
@ -1627,8 +1629,15 @@ function render() {
|
|||
syncCarriedPlushieToOwner(it, world);
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of renderStack.lodgedPins) {
|
||||
for (const it of renderStack.ballLodgedPins || []) {
|
||||
if (!it || drawnAttachedPinIds.has(it.id)) continue;
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
drawnAttachedPinIds.add(it.id);
|
||||
}
|
||||
for (const it of renderStack.lodgedPins) {
|
||||
if (!it || drawnAttachedPinIds.has(it.id)) continue;
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
drawnAttachedPinIds.add(it.id);
|
||||
}
|
||||
renderPerfEnd(endAttachments);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@
|
|||
if (typeof syncTopButtons === "function") syncTopButtons();
|
||||
}
|
||||
|
||||
function syncRestoreDependents(worldRef = global.world, context = {}) {
|
||||
worldRef?.emit?.("ui:restore-sync", { source: context.source || "restore" });
|
||||
function syncRestoreDependents(worldRef = global.world) {
|
||||
renderWorldAfterRestore(worldRef);
|
||||
}
|
||||
|
||||
function restoreSnapshot(snapshot, worldRef = global.world, options = {}) {
|
||||
const result = Snapshot.restoreSnapshot(snapshot, worldRef);
|
||||
if (options.syncUi !== false) syncRestoreDependents(worldRef, { source: options.source || "snapshot", snapshotVersion: result?.snapshotVersion || Snapshot.version });
|
||||
if (options.syncUi !== false) syncRestoreDependents(worldRef);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
216
js/save_codec.js
216
js/save_codec.js
|
|
@ -232,68 +232,6 @@
|
|||
return readBitFields(reader, Array.from({ length: count }, () => bits)).map(v => v * Math.max(1, step));
|
||||
}
|
||||
|
||||
function makeProfile(snapshot = {}) {
|
||||
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||
return {
|
||||
format: "TJ1",
|
||||
schema: BINARY_SCHEMA_VERSION,
|
||||
alphabetSize: SAVE_TEXT_BASE,
|
||||
counts: { tarinai: tarinaiRows.length, items: itemRows.length },
|
||||
sections: {},
|
||||
itemTypes: {},
|
||||
compression: {},
|
||||
};
|
||||
}
|
||||
|
||||
function addProfileBytes(profile, key, bytes = 0) {
|
||||
if (!profile || !key) return;
|
||||
const n = Math.max(0, Number(bytes) || 0);
|
||||
profile.sections[key] = (profile.sections[key] || 0) + n;
|
||||
}
|
||||
|
||||
function trackBytes(writer, profile, key, fn) {
|
||||
if (!profile || !writer) return fn();
|
||||
const start = writer.bytes.length;
|
||||
const result = fn();
|
||||
addProfileBytes(profile, key, writer.bytes.length - start);
|
||||
return result;
|
||||
}
|
||||
|
||||
function addItemTypeProfile(profile, typeId, bytes = 0) {
|
||||
if (!profile) return;
|
||||
const type = ITEM_TYPE_IDS[typeId] || `type_${typeId}`;
|
||||
const entry = profile.itemTypes[type] || { count: 0, bytes: 0 };
|
||||
entry.count += 1;
|
||||
entry.bytes += Math.max(0, Number(bytes) || 0);
|
||||
profile.itemTypes[type] = entry;
|
||||
}
|
||||
|
||||
function finalizeProfile(profile, rawBytes, packedBytes, text, codec = "R") {
|
||||
if (!profile) return null;
|
||||
const raw = rawBytes?.length || 0;
|
||||
const packed = packedBytes?.length || 0;
|
||||
const payloadChars = Math.max(0, String(text || "").length - EXPORT_PREFIX.length - 1);
|
||||
profile.sections.totalRaw = raw;
|
||||
profile.compression = {
|
||||
codec,
|
||||
rawBytes: raw,
|
||||
packedBytes: packed,
|
||||
packedDelta: raw - packed,
|
||||
ratio: raw > 0 ? packed / raw : 0,
|
||||
prefixChars: EXPORT_PREFIX.length + 1,
|
||||
payloadChars,
|
||||
totalChars: String(text || "").length,
|
||||
alphabetSize: SAVE_TEXT_BASE,
|
||||
containsBacktick: false,
|
||||
japaneseText: true,
|
||||
bitsPerChar: SAVE_TEXT_BITS,
|
||||
asciiOnly: false,
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
async function compressBytes(bytes) {
|
||||
if (typeof CompressionStream === "function") {
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
||||
|
|
@ -301,7 +239,7 @@
|
|||
}
|
||||
return bytes;
|
||||
}
|
||||
async function decompressBytes(bytes, codec = "R") {
|
||||
async function decompressBytes(bytes, codec) {
|
||||
if (codec === DEFLATE_CODEC) {
|
||||
if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser");
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
|
||||
|
|
@ -472,7 +410,7 @@
|
|||
return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100];
|
||||
}
|
||||
|
||||
function writeTarinai(writer, row = [], profile = null, coordState = null) {
|
||||
function writeTarinai(writer, row = [], coordState = null) {
|
||||
const pinIdx = row[15] ?? -1;
|
||||
const nestIdx = row[16] ?? -1;
|
||||
const stateId = row[17]?.[0] || 0;
|
||||
|
|
@ -480,42 +418,34 @@
|
|||
const genetics = Array.isArray(row[18]) ? row[18] : [];
|
||||
const fightStats = Array.isArray(row[19]) ? row[19] : [];
|
||||
const birthSeed = row[1] || "";
|
||||
trackBytes(writer, profile, "tarinai.flags", () => writeBitFields(writer, [row[0] || 0], [10]));
|
||||
trackBytes(writer, profile, "tarinai.seed", () => writer.str(birthSeed));
|
||||
trackBytes(writer, profile, "tarinai.core", () => {
|
||||
const x = toSafeInt(row[2] || 0);
|
||||
const y = toSafeInt(row[3] || 0);
|
||||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
writer.u(row[4] || 0);
|
||||
writer.u(row[5] || 1);
|
||||
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
|
||||
});
|
||||
trackBytes(writer, profile, "tarinai.needs", () => writeFixedPacked(writer, row[9], 6, 6, 2));
|
||||
trackBytes(writer, profile, "tarinai.traits", () => {
|
||||
writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8);
|
||||
});
|
||||
trackBytes(writer, profile, "tarinai.family", () => {
|
||||
writeIndexList(writer, row[11]);
|
||||
});
|
||||
trackBytes(writer, profile, "tarinai.relationships", () => writeRelationships(writer, row[12]));
|
||||
trackBytes(writer, profile, "tarinai.timers", () => writeTimers(writer, row[13]));
|
||||
trackBytes(writer, profile, "tarinai.modes", () => writeModes(writer, row[14]));
|
||||
trackBytes(writer, profile, "tarinai.tail", () => {
|
||||
let tailMask = 0;
|
||||
if (pinIdx >= 0) tailMask |= 1;
|
||||
if (nestIdx >= 0) tailMask |= 2;
|
||||
if (stateId || stateTarget >= 0) tailMask |= 4;
|
||||
if (genetics.length) tailMask |= 8;
|
||||
if (fightStats.length) tailMask |= 16;
|
||||
writeBitFields(writer, [tailMask], [5]);
|
||||
if (tailMask & 1) writer.u(pinIdx);
|
||||
if (tailMask & 2) writer.u(nestIdx);
|
||||
if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); }
|
||||
if (tailMask & 8) for (let i = 0; i < 4; i++) writer.s(genetics[i] || 100);
|
||||
if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); }
|
||||
});
|
||||
writeBitFields(writer, [row[0] || 0], [10]);
|
||||
writer.str(birthSeed);
|
||||
const x = toSafeInt(row[2] || 0);
|
||||
const y = toSafeInt(row[3] || 0);
|
||||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
writer.u(row[4] || 0);
|
||||
writer.u(row[5] || 1);
|
||||
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
|
||||
writeFixedPacked(writer, row[9], 6, 6, 2);
|
||||
writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8);
|
||||
writeIndexList(writer, row[11]);
|
||||
writeRelationships(writer, row[12]);
|
||||
writeTimers(writer, row[13]);
|
||||
writeModes(writer, row[14]);
|
||||
let tailMask = 0;
|
||||
if (pinIdx >= 0) tailMask |= 1;
|
||||
if (nestIdx >= 0) tailMask |= 2;
|
||||
if (stateId || stateTarget >= 0) tailMask |= 4;
|
||||
if (genetics.length) tailMask |= 8;
|
||||
if (fightStats.length) tailMask |= 16;
|
||||
writeBitFields(writer, [tailMask], [5]);
|
||||
if (tailMask & 1) writer.u(pinIdx);
|
||||
if (tailMask & 2) writer.u(nestIdx);
|
||||
if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); }
|
||||
if (tailMask & 8) for (let i = 0; i < 4; i++) writer.s(genetics[i] || 100);
|
||||
if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); }
|
||||
}
|
||||
function readTarinai(reader, coordState = null) {
|
||||
const flags = readBitFields(reader, [10])[0];
|
||||
|
|
@ -532,13 +462,12 @@
|
|||
const relationships = readRelationships(reader);
|
||||
const timers = readTimers(reader);
|
||||
const modes = readModes(reader);
|
||||
const modernTail = Number(reader.schemaVersion || BINARY_SCHEMA_VERSION) >= 15;
|
||||
const [tailMask] = readBitFields(reader, [modernTail ? 5 : 3]);
|
||||
const [tailMask] = readBitFields(reader, [5]);
|
||||
const pinIdx = (tailMask & 1) ? reader.u() : -1;
|
||||
const nestIdx = (tailMask & 2) ? reader.u() : -1;
|
||||
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -1];
|
||||
const genetics = (modernTail && (tailMask & 8)) ? [reader.s(), reader.s(), reader.s(), reader.s()] : [];
|
||||
const fightStats = (modernTail && (tailMask & 16)) ? [reader.u(), reader.u(), reader.s()] : [];
|
||||
const genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s()] : [];
|
||||
const fightStats = (tailMask & 16) ? [reader.u(), reader.u(), reader.s()] : [];
|
||||
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
|
||||
}
|
||||
|
||||
|
|
@ -604,18 +533,14 @@
|
|||
for (let i = 0; i < 16; i++) out += (((x >> i) & 1) * (2 ** (2 * i))) + (((y >> i) & 1) * (2 ** (2 * i + 1)));
|
||||
return out;
|
||||
}
|
||||
function writeItemPayload(writer, row = [], typeId = 0, coordState = null, profile = null) {
|
||||
const start = writer.bytes.length;
|
||||
trackBytes(writer, profile, "items.core", () => {
|
||||
const x = toSafeInt(row[1] || 0);
|
||||
const y = toSafeInt(row[2] || 0);
|
||||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0);
|
||||
});
|
||||
trackBytes(writer, profile, "items.extra", () => writeItemExtra(writer, typeId, row[4] || []));
|
||||
addItemTypeProfile(profile, typeId, writer.bytes.length - start);
|
||||
function writeItemPayload(writer, row = [], typeId = 0, coordState = null) {
|
||||
const x = toSafeInt(row[1] || 0);
|
||||
const y = toSafeInt(row[2] || 0);
|
||||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0);
|
||||
writeItemExtra(writer, typeId, row[4] || []);
|
||||
}
|
||||
function readItemPayload(reader, typeId = 0, coordState = null) {
|
||||
const x = (coordState?.x || 0) + reader.s();
|
||||
|
|
@ -642,14 +567,14 @@
|
|||
}
|
||||
return blocks;
|
||||
}
|
||||
function writeItemBlocks(writer, rows = [], profile = null) {
|
||||
function writeItemBlocks(writer, rows = []) {
|
||||
const blocks = itemBlocksFromRows(rows);
|
||||
writer.u(blocks.length);
|
||||
for (const [typeId, list] of blocks) {
|
||||
writer.u(typeId);
|
||||
writer.u(list.length);
|
||||
const coordState = { x: 0, y: 0 };
|
||||
for (const row of list) writeItemPayload(writer, row, typeId, coordState, profile);
|
||||
for (const row of list) writeItemPayload(writer, row, typeId, coordState);
|
||||
}
|
||||
}
|
||||
function readItemBlocks(reader) {
|
||||
|
|
@ -675,35 +600,25 @@
|
|||
return [day, tarinaiCount, fieldValue(w[0]), time10, itemCount, groundValue(w[1])];
|
||||
}
|
||||
|
||||
function encodeBinarySnapshot(snapshot, options = {}) {
|
||||
function encodeBinarySnapshot(snapshot) {
|
||||
if (!snapshot || snapshot.v !== Snapshot.version || snapshot.a !== "tj1") throw new Error("snapshot version mismatch");
|
||||
const profile = options?.profile ? makeProfile(snapshot) : null;
|
||||
const writer = new BinaryWriter();
|
||||
trackBytes(writer, profile, "header", () => {
|
||||
writer.b(BINARY_SCHEMA_VERSION);
|
||||
writer.u(Snapshot.version);
|
||||
});
|
||||
trackBytes(writer, profile, "world", () => writeWorld(writer, snapshot.w || []));
|
||||
writer.b(BINARY_SCHEMA_VERSION);
|
||||
writer.u(Snapshot.version);
|
||||
writeWorld(writer, snapshot.w || []);
|
||||
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||
trackBytes(writer, profile, "tarinai.count", () => writer.u(tarinaiRows.length));
|
||||
const tarinaiStart = writer.bytes.length;
|
||||
writer.u(tarinaiRows.length);
|
||||
const tarCoordState = { x: 0, y: 0 };
|
||||
for (const row of tarinaiRows) writeTarinai(writer, row || [], profile, tarCoordState);
|
||||
addProfileBytes(profile, "tarinai.total", writer.bytes.length - tarinaiStart);
|
||||
for (const row of tarinaiRows) writeTarinai(writer, row || [], tarCoordState);
|
||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||
const itemStart = writer.bytes.length;
|
||||
trackBytes(writer, profile, "items.blocks", () => writeItemBlocks(writer, itemRows, profile));
|
||||
addProfileBytes(profile, "items.total", writer.bytes.length - itemStart);
|
||||
const bytes = writer.out();
|
||||
if (profile) return { bytes, profile };
|
||||
return bytes;
|
||||
writeItemBlocks(writer, itemRows);
|
||||
return writer.out();
|
||||
}
|
||||
|
||||
function decodeBinarySnapshot(bytes) {
|
||||
const reader = new BinaryReader(bytes);
|
||||
const schema = reader.b();
|
||||
if (schema !== BINARY_SCHEMA_VERSION && schema !== 14) throw new Error("unsupported binary save schema");
|
||||
reader.schemaVersion = schema;
|
||||
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||
const snapshotVersion = reader.u();
|
||||
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
||||
const w = readWorld(reader);
|
||||
|
|
@ -727,18 +642,10 @@
|
|||
return { codec, packed };
|
||||
}
|
||||
|
||||
async function encodeSnapshotWithProfile(snapshot) {
|
||||
const encoded = encodeBinarySnapshot(snapshot, { profile: true });
|
||||
const bytes = encoded.bytes || encoded;
|
||||
const profile = encoded.profile || null;
|
||||
const { codec, packed } = await packBytesForSave(bytes);
|
||||
const text = `${EXPORT_PREFIX}${codec}${jp2048Encode(packed)}`;
|
||||
finalizeProfile(profile, bytes, packed, text, codec);
|
||||
return { text, profile };
|
||||
}
|
||||
|
||||
async function encodeSnapshot(snapshot) {
|
||||
return (await encodeSnapshotWithProfile(snapshot)).text;
|
||||
const bytes = encodeBinarySnapshot(snapshot);
|
||||
const { codec, packed } = await packBytesForSave(bytes);
|
||||
return `${EXPORT_PREFIX}${codec}${jp2048Encode(packed)}`;
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
|
|
@ -752,20 +659,7 @@
|
|||
}
|
||||
|
||||
global.TarinaiSaveCodec = {
|
||||
EXPORT_PREFIX,
|
||||
RAW_CODEC,
|
||||
DEFLATE_CODEC,
|
||||
SAVE_TEXT_ALPHABET,
|
||||
SAVE_TEXT_BASE,
|
||||
SAVE_TEXT_BITS,
|
||||
jp2048Encode,
|
||||
jp2048Decode,
|
||||
encodeSnapshot,
|
||||
encodeSnapshotWithProfile,
|
||||
decodeSnapshot,
|
||||
encodeBinarySnapshot,
|
||||
decodeBinarySnapshot,
|
||||
BinaryWriter,
|
||||
BinaryReader,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
if (!hash.startsWith("\u305f\u308a")) throw new Error("unsupported Japanese hash slot value");
|
||||
global.localStorage?.setItem(slotKey(slot), hash);
|
||||
if (meta) global.localStorage?.setItem(metaKey(slot), JSON.stringify({ ...meta, savedAt: Date.now() }));
|
||||
return { hash, meta: readMeta(slot) };
|
||||
}
|
||||
function readMeta(slot) {
|
||||
const raw = global.localStorage?.getItem(metaKey(slot));
|
||||
|
|
@ -32,5 +31,5 @@
|
|||
global.localStorage?.removeItem(slotKey(slot));
|
||||
global.localStorage?.removeItem(metaKey(slot));
|
||||
}
|
||||
global.TarinaiSaveStorage = { STORAGE_PREFIX, SLOT_COUNT, slotKey, writeSlot, readSlot, requireSlot, deleteSlot };
|
||||
global.TarinaiSaveStorage = { STORAGE_PREFIX, SLOT_COUNT, writeSlot, readSlot, requireSlot, deleteSlot };
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -14,18 +14,13 @@
|
|||
}
|
||||
|
||||
function restoreSnapshot(snapshot, worldRef = global.world, options = {}) {
|
||||
return Restore.restoreSnapshot(snapshot, worldRef, { source: options.source || "save", syncUi: options.syncUi });
|
||||
return Restore.restoreSnapshot(snapshot, worldRef, { syncUi: options.syncUi });
|
||||
}
|
||||
|
||||
async function encodeSnapshot(snapshot = createSnapshot(global.world)) {
|
||||
return Codec.encodeSnapshot(snapshot);
|
||||
}
|
||||
|
||||
async function encodeSnapshotWithProfile(snapshot = createSnapshot(global.world)) {
|
||||
if (typeof Codec.encodeSnapshotWithProfile === "function") return Codec.encodeSnapshotWithProfile(snapshot);
|
||||
return { text: await Codec.encodeSnapshot(snapshot), profile: null };
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
return Codec.decodeSnapshot(text);
|
||||
}
|
||||
|
|
@ -39,7 +34,7 @@
|
|||
async function loadSlot(slot) {
|
||||
const hash = Storage.requireSlot(slot);
|
||||
const snapshot = await decodeSnapshot(hash);
|
||||
return restoreSnapshot(snapshot, global.world, { source: "slot" });
|
||||
return restoreSnapshot(snapshot, global.world);
|
||||
}
|
||||
|
||||
function deleteSlot(slot) { Storage.deleteSlot(slot); }
|
||||
|
|
@ -49,7 +44,7 @@
|
|||
if (!entry) return "\u7a7a\u304d";
|
||||
const meta = entry.m || entry;
|
||||
const m = meta.m || [];
|
||||
const date = meta.savedAt ? new Date(meta.savedAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : (meta.c ? new Date(meta.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e");
|
||||
const date = meta.savedAt ? new Date(meta.savedAt).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e";
|
||||
const day = m[0] || 1;
|
||||
const population = m[1] || 0;
|
||||
const fieldType = m[2] || "\u5ead";
|
||||
|
|
@ -58,60 +53,6 @@
|
|||
return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339${hashLen}`;
|
||||
}
|
||||
|
||||
function bytesText(value = 0) {
|
||||
const n = Math.max(0, Math.round(Number(value) || 0));
|
||||
return `${n.toLocaleString("ja-JP")}B`;
|
||||
}
|
||||
|
||||
function percentText(value = 0, total = 0) {
|
||||
const t = Number(total) || 0;
|
||||
if (t <= 0) return "-";
|
||||
return `${((Number(value) || 0) / t * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function profileRow(label, bytes, total) {
|
||||
return `<div><span>${htmlEscape(label)}</span><strong>${htmlEscape(bytesText(bytes))}</strong><em>${htmlEscape(percentText(bytes, total))}</em></div>`;
|
||||
}
|
||||
|
||||
function renderHashProfile(profile, dialog = ensureDialog()) {
|
||||
const box = dialog?.querySelector?.("#saveHashProfile");
|
||||
if (!box) return;
|
||||
if (!profile) { box.innerHTML = ""; return; }
|
||||
const sections = profile.sections || {};
|
||||
const comp = profile.compression || {};
|
||||
const raw = comp.rawBytes || sections.totalRaw || 0;
|
||||
const itemEntries = Object.entries(profile.itemTypes || {})
|
||||
.sort((a, b) => (b[1]?.bytes || 0) - (a[1]?.bytes || 0))
|
||||
.slice(0, 10);
|
||||
const topItems = itemEntries.length
|
||||
? itemEntries.map(([type, entry]) => `<span>${htmlEscape(type)} ${Number(entry.count || 0)}\u500b / ${htmlEscape(bytesText(entry.bytes || 0))}</span>`).join("")
|
||||
: `<span>\u306a\u3057</span>`;
|
||||
box.innerHTML = `
|
||||
<div class="save-profile-summary">
|
||||
<span>\u30cf\u30c3\u30b7\u30e5 <strong>${Number(comp.totalChars || 0).toLocaleString("ja-JP")}\u5b57</strong></span>
|
||||
<span>\u5727\u7e2e\u524d <strong>${htmlEscape(bytesText(raw))}</strong></span>
|
||||
<span>\u5727\u7e2e\u5f8c <strong>${htmlEscape(bytesText(comp.packedBytes || 0))}</strong></span>
|
||||
<span>\u5727\u7e2e\u7387 <strong>${comp.ratio ? `${(comp.ratio * 100).toFixed(1)}%` : "-"}</strong></span>
|
||||
<span>\u6587\u5b57\u8868 <strong>${comp.japaneseText ? "\u65e5\u672c\u8a9e2048" : "\u4e0d\u660e"}</strong></span>
|
||||
</div>
|
||||
<div class="save-profile-grid">
|
||||
${profileRow("\u30d8\u30c3\u30c0", sections.header, raw)}
|
||||
${profileRow("\u30ef\u30fc\u30eb\u30c9", sections.world, raw)}
|
||||
${profileRow("\u305f\u308a\u306a\u3044\u5408\u8a08", sections["tarinai.total"], raw)}
|
||||
${profileRow("\u751f\u6210seed", sections["tarinai.seed"], raw)}
|
||||
${profileRow("\u57fa\u672c\u5024", sections["tarinai.core"], raw)}
|
||||
${profileRow("\u6b32\u6c42", sections["tarinai.needs"], raw)}
|
||||
${profileRow("\u6027\u683c\u5dee\u5206", sections["tarinai.traits"], raw)}
|
||||
${profileRow("\u5bb6\u65cf", sections["tarinai.family"], raw)}
|
||||
${profileRow("\u95a2\u4fc2", sections["tarinai.relationships"], raw)}
|
||||
${profileRow("\u30bf\u30a4\u30de\u30fc/\u52b9\u679c", (sections["tarinai.timers"] || 0) + (sections["tarinai.modes"] || 0) + (sections["tarinai.tail"] || 0), raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u5408\u8a08", sections["items.total"], raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u57fa\u672c", sections["items.core"], raw)}
|
||||
${profileRow("\u30a2\u30a4\u30c6\u30e0\u8ffd\u52a0", sections["items.extra"], raw)}
|
||||
</div>
|
||||
<div class="save-profile-items"><b>\u30a2\u30a4\u30c6\u30e0\u4e0a\u4f4d:</b>${topItems}</div>`;
|
||||
}
|
||||
|
||||
function ensureDialog() {
|
||||
let dialog = document.getElementById("saveDialog");
|
||||
if (dialog) return dialog;
|
||||
|
|
@ -131,7 +72,6 @@
|
|||
<button id="saveImportBtn" class="btn primary" type="button">\u546a\u6587\u3092\u30ed\u30fc\u30c9</button>
|
||||
</div>
|
||||
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="\u30bb\u30fc\u30d6\u3057\u305f\u546a\u6587\u306f\u30b3\u30d4\u30da\u3057\u3066\u4fdd\u5b58\u3057\u3066\u304f\u3060\u3055\u3044\u3002"></textarea>
|
||||
<div id="saveHashProfile" class="save-hash-profile" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="saveCloseBtn" class="btn" type="button">\u9589\u3058\u308b</button>
|
||||
|
|
@ -173,7 +113,7 @@
|
|||
}
|
||||
|
||||
function bindSaveSystem() {
|
||||
const btn = document.getElementById("saveBtn") || global.ui?.saveBtn;
|
||||
const btn = document.getElementById("saveBtn");
|
||||
if (!btn || btn.dataset.saveBound === "1") return;
|
||||
btn.dataset.saveBound = "1";
|
||||
btn.addEventListener("click", () => {
|
||||
|
|
@ -228,9 +168,7 @@
|
|||
if (e.target.closest("#saveExportBtn")) {
|
||||
const area = dialog.querySelector("#saveHashText");
|
||||
if (area) {
|
||||
const result = await encodeSnapshotWithProfile(createSnapshot(global.world));
|
||||
area.value = result.text || "";
|
||||
renderHashProfile(result.profile, dialog);
|
||||
area.value = await encodeSnapshot(createSnapshot(global.world));
|
||||
area.focus();
|
||||
area.select();
|
||||
}
|
||||
|
|
@ -242,7 +180,7 @@
|
|||
if (!text.trim()) { global.showToast?.("\u30ed\u30fc\u30c9\u3059\u308b\u546a\u6587\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
if (!global.confirm?.("\u546a\u6587\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002")) return;
|
||||
try {
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world, { source: "import" });
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world);
|
||||
closeDialog();
|
||||
global.showToast?.("\u546a\u6587\u304b\u3089\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002");
|
||||
} catch (error) {
|
||||
|
|
@ -257,7 +195,6 @@
|
|||
createSnapshot,
|
||||
restoreSnapshot,
|
||||
encodeSnapshot,
|
||||
encodeSnapshotWithProfile,
|
||||
decodeSnapshot,
|
||||
saveSlot,
|
||||
loadSlot,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ function stableUnit(seed, salt = "") {
|
|||
}
|
||||
|
||||
function stableKeyPart(value) {
|
||||
const shared = globalThis.TarinaiDeterministicHelpers?.keyPart;
|
||||
if (typeof shared === "function") return shared(value);
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
|
|
@ -78,42 +80,16 @@ function stableKeyPart(value) {
|
|||
return String(value);
|
||||
}
|
||||
|
||||
function deterministicFrame(worldRef = null, hz = 60) {
|
||||
const t = Number(worldRef?.time || 0) || 0;
|
||||
return Math.floor(t * Math.max(1, Number(hz) || 60) + 1e-6);
|
||||
}
|
||||
|
||||
function deterministicUnit(worldRef = null, salt = "", ...parts) {
|
||||
const seed = String(worldRef?.worldSeed || "tarinai-world");
|
||||
const frame = deterministicFrame(worldRef, 60);
|
||||
const serial = Number(worldRef?._deterministicEpoch || 0) || 0;
|
||||
const key = [salt, frame, serial, ...parts.map(stableKeyPart)].join("|");
|
||||
return stableUnit(seed, key);
|
||||
}
|
||||
|
||||
function deterministicRange(worldRef = null, salt = "", min = 0, max = 1, ...parts) {
|
||||
const a = Number(min) || 0;
|
||||
const b = Number(max) || 0;
|
||||
return a + deterministicUnit(worldRef, salt, ...parts) * (b - a);
|
||||
}
|
||||
|
||||
function deterministicSigned(worldRef = null, salt = "", ...parts) {
|
||||
return deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1;
|
||||
}
|
||||
|
||||
function deterministicChance(worldRef = null, salt = "", chance = 0, ...parts) {
|
||||
const p = clamp(Number(chance) || 0, 0, 1);
|
||||
if (p <= 0) return false;
|
||||
if (p >= 1) return true;
|
||||
return deterministicUnit(worldRef, salt, ...parts) < p;
|
||||
}
|
||||
|
||||
function deterministicAngle(worldRef = null, salt = "", ...parts) {
|
||||
return deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.TarinaiDeterminism = { stableKeyPart, deterministicFrame, deterministicUnit, deterministicRange, deterministicSigned, deterministicChance, deterministicAngle };
|
||||
window.TarinaiDeterminism = {
|
||||
stableKeyPart,
|
||||
deterministicFrame: window.deterministicFrame,
|
||||
deterministicUnit: window.deterministicUnit,
|
||||
deterministicRange: window.deterministicRange,
|
||||
deterministicSigned: window.deterministicSigned,
|
||||
deterministicChance: window.deterministicChance,
|
||||
deterministicAngle: window.deterministicAngle,
|
||||
};
|
||||
}
|
||||
|
||||
const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
|
||||
|
|
@ -622,8 +598,38 @@ class Effect {
|
|||
ctx.fill();
|
||||
}
|
||||
} else if (this.type === "bubble") {
|
||||
const w = Math.max(30, 14 + this.text.length * 7);
|
||||
const h = 20;
|
||||
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
|
||||
const maxTextW = 118;
|
||||
const lineH = 14;
|
||||
const ellipsize = (text, maxW) => {
|
||||
let s = String(text || "");
|
||||
if (ctx.measureText(s).width <= maxW) return s;
|
||||
while (s.length > 1 && ctx.measureText(`${s}…`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}…`;
|
||||
};
|
||||
const wrap = (text, maxW) => {
|
||||
const src = String(text || "").replace(/\s+/g, " ").trim() || " ";
|
||||
const units = src.includes(" ") ? src.split(/(\s+)/).filter(Boolean) : Array.from(src);
|
||||
const out = [];
|
||||
let line = "";
|
||||
for (const unit of units) {
|
||||
const candidate = line ? `${line}${unit}` : unit;
|
||||
if (ctx.measureText(candidate).width <= maxW) line = candidate;
|
||||
else {
|
||||
if (line) out.push(line.trim());
|
||||
line = unit.trim();
|
||||
if (ctx.measureText(line).width > maxW) { out.push(ellipsize(line, maxW)); line = ""; }
|
||||
}
|
||||
if (out.length >= 3) break;
|
||||
}
|
||||
if (line && out.length < 3) out.push(line.trim());
|
||||
if (out.length > 2) out[1] = ellipsize([out[1], ...out.slice(2)].join(""), maxW);
|
||||
return out.slice(0, 2).map(line => ellipsize(line, maxW));
|
||||
};
|
||||
const lines = wrap(this.text, maxTextW);
|
||||
const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0);
|
||||
const w = Math.max(30, Math.min(142, textW + 18));
|
||||
const h = Math.max(20, 10 + lines.length * lineH);
|
||||
ctx.globalAlpha = Math.min(1, alpha * 1.25);
|
||||
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
|
||||
ctx.strokeStyle = "rgba(78, 64, 48, 0.22)";
|
||||
|
|
@ -640,10 +646,11 @@ class Effect {
|
|||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = this.color || "rgba(42,36,29,0.78)";
|
||||
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.text, 0, -10);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
ctx.fillText(lines[i], 0, -h + 8 + lineH * i + lineH / 2);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
|
@ -742,6 +749,8 @@ class SpatialGrid {
|
|||
const minY = Math.floor(Math.min(top, bottom) / this.cellSize);
|
||||
const maxY = Math.floor(Math.max(top, bottom) / this.cellSize);
|
||||
const cellCount = Math.max(1, (maxX - minX + 1) * (maxY - minY + 1));
|
||||
// Very large bodies would pollute most buckets; keep them in their origin
|
||||
// cell and let callers use their own radius/AABB checks after retrieval.
|
||||
if (cellCount > 144) return this.add(map, entity);
|
||||
for (let cy = minY; cy <= maxY; cy += 1) {
|
||||
for (let cx = minX; cx <= maxX; cx += 1) {
|
||||
|
|
@ -919,6 +928,8 @@ class SpatialGrid {
|
|||
|
||||
nearbyInto(map, x, y, radius, out = [], filterDistance = false) {
|
||||
const r = Number.isFinite(radius) ? radius : Math.max(1200, this.cellSize * 12);
|
||||
// AABB-indexed entities can appear in several buckets. The per-query stamp
|
||||
// dedupes without allocating a Set on every hot spatial lookup.
|
||||
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||
const minX = Math.floor((x - r) / this.cellSize);
|
||||
const maxX = Math.floor((x + r) / this.cellSize);
|
||||
|
|
@ -953,6 +964,7 @@ class SpatialGrid {
|
|||
|
||||
nearbyRectInto(map, rect, out = []) {
|
||||
if (!rect) return out;
|
||||
// See nearbyInto(): multi-cell insertions require per-query dedupe.
|
||||
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||
const minX = Math.floor((rect.left || 0) / this.cellSize);
|
||||
const maxX = Math.floor((rect.right || 0) / this.cellSize);
|
||||
|
|
|
|||
|
|
@ -541,7 +541,6 @@
|
|||
worldRef.rebuildSpatial?.(true);
|
||||
worldRef.markTerrainDirty?.("load");
|
||||
worldRef.clampCamera?.();
|
||||
worldRef.emit?.("snapshot:restored", { snapshotVersion: SNAPSHOT_VERSION });
|
||||
return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
function markWorldAfterStructureChange(worldRef, reason = "structure-change") {
|
||||
function markWorldAfterStructureChange(worldRef, reason = "structure-change", item = null, radius = 40) {
|
||||
if (!worldRef) return;
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markItemBucketsDirty?.(reason);
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
worldRef.markTerrainDirty?.(reason);
|
||||
if (item && Number.isFinite(item.x) && Number.isFinite(item.y)) {
|
||||
worldRef.markTerrainDirtyAt?.(item.x, item.y, Math.max(24, Number(radius || item.r || 40) || 40), reason);
|
||||
} else {
|
||||
worldRef.markTerrainDirty?.(reason);
|
||||
}
|
||||
}
|
||||
|
||||
function markLooseItemGone(item, reason = "item-removed") {
|
||||
if (!item || item.dead) return false;
|
||||
// Normal Item.dead is a getter, so all non-structure removals go through
|
||||
// amount/hp zeroing instead of assigning to .dead.
|
||||
item.amount = 0;
|
||||
item.hp = 0;
|
||||
item.foodServingsRemaining = 0;
|
||||
if (item.type === "grass") {
|
||||
item.growth = 0;
|
||||
item.grassStage = -1;
|
||||
item.seedTimer = Math.max(Number(item.seedTimer || 0), 2.5);
|
||||
}
|
||||
item.removedReason = String(reason || "item-removed");
|
||||
return true;
|
||||
}
|
||||
|
||||
function markStructureGone(structure, reason = "structure-removed") {
|
||||
|
|
@ -142,20 +162,25 @@
|
|||
|
||||
function deleteItem(worldRef, item, opts = {}) {
|
||||
if (!item || item.dead) return false;
|
||||
if (item.isStructure) return deleteStructure(worldRef, item, { reason: opts.reason || "delete-tool", userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner });
|
||||
item.amount = 0;
|
||||
item.hp = 0;
|
||||
item.removedReason = String(opts.reason || "delete-tool");
|
||||
markWorldAfterStructureChange(worldRef, opts.reason || "delete-tool");
|
||||
const reason = opts.reason || "delete-tool";
|
||||
if (item.isStructure) return deleteStructure(worldRef, item, { reason, userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner });
|
||||
const changed = markLooseItemGone(item, reason);
|
||||
if (!changed) return false;
|
||||
markWorldAfterStructureChange(worldRef, reason, item, Math.max(36, Number(item.r || item.radius || 24) + 18));
|
||||
worldRef?.emit?.("item:deleted", { item, type: item.type, reason });
|
||||
return true;
|
||||
}
|
||||
|
||||
global.TarinaiStructureLifecycle = Object.freeze({
|
||||
const api = Object.freeze({
|
||||
markWorldAfterStructureChange,
|
||||
markLooseItemGone,
|
||||
releaseStructureUsers,
|
||||
deleteStructure,
|
||||
consumeGrassBedOnWake,
|
||||
deleteItem,
|
||||
destroyItem: deleteItem,
|
||||
triggerOwnedStructureDestroyedPanic,
|
||||
});
|
||||
})(window);
|
||||
global.TarinaiStructureLifecycle = api;
|
||||
global.TarinaiItemDestruction = api;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -104,7 +104,14 @@
|
|||
|
||||
draw(ctx, time = 0, lighting = {}) {
|
||||
if (this.dead) return;
|
||||
let ownerAlpha = 1;
|
||||
if (this.type === "plushie") {
|
||||
const owner = this.owner(this.world);
|
||||
ownerAlpha = owner ? clamp(1 - (owner.nestFade || (owner.insideNestBoxId ? 1 : 0)), 0, 1) : 1;
|
||||
if (ownerAlpha <= 0.02) return;
|
||||
}
|
||||
ctx.save();
|
||||
if (this.type === "plushie") ctx.globalAlpha *= ownerAlpha;
|
||||
ctx.translate(this.x, this.y);
|
||||
const night = (lighting?.light || 1) < 0.42;
|
||||
if (this.type === "grass_bed") {
|
||||
|
|
@ -257,7 +264,7 @@
|
|||
buildNeed: "fulfill",
|
||||
buildTime: 4,
|
||||
maxHp: 1,
|
||||
materials: { grassMaterial: 1 },
|
||||
materials: { grassMaterial: 4 },
|
||||
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
|
||||
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
|
||||
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
|
||||
|
|
|
|||
|
|
@ -91,12 +91,16 @@ function createSleepBuildGrassBedActionSpec() {
|
|||
phrase: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f",
|
||||
weight: 18,
|
||||
timerBacked: true,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
selectTarget(t, world) { return (typeof findNearbyBuildMaterial === "function" ? findNearbyBuildMaterial(world, t, "grass_bed", 520) : null) || findNearbyMaterial(world, t, "grassMaterial", 520); },
|
||||
canStart(t, world, ctx = {}) {
|
||||
const higherPriorityPlace = typeof findPrioritySleepPlaceBeforeGrassBedBuild === "function"
|
||||
? findPrioritySleepPlaceBeforeGrassBedBuild(world, t, 760)
|
||||
: findNearestSleepPlace(world, t, 760);
|
||||
return canBuildOwnedStructureByAge(t)
|
||||
&& !findNearestSleepPlace(world, t, 760)
|
||||
&& !higherPriorityPlace
|
||||
&& !findOwnedStructure(world, t, "grass_bed")
|
||||
&& !!(ctx.target ?? this.selectTarget(t, world, ctx))
|
||||
&& !(Number(t?.actionIdCooldowns?.sleep_build_grass_bed || 0) > 0.01)
|
||||
&& !["fight", "panic", "intimidate", "birth_ritual"].includes(t.state);
|
||||
},
|
||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); },
|
||||
|
|
@ -504,7 +508,7 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b",
|
||||
phrase: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f",
|
||||
weight: 34,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
selectTarget(t, world) { return (typeof findNearbyBuildMaterial === "function" ? findNearbyBuildMaterial(world, t, "grass_bed", 520) : null) || findNearbyMaterial(world, t, "grassMaterial", 520); },
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); },
|
||||
},
|
||||
|
|
@ -515,7 +519,7 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b",
|
||||
phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f",
|
||||
weight: 28,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
selectTarget(t, world) { return (typeof findNearbyBuildMaterial === "function" ? findNearbyBuildMaterial(world, t, "plushie", 520) : null) || findNearbyMaterial(world, t, "grassMaterial", 520); },
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@
|
|||
return String(behavior.forcedRequest?.source || behavior.data?.forcedSource || behavior.sourceDetail || "");
|
||||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
}
|
||||
|
||||
function normalizeBehaviorValue(input, tarinai = null, previous = null) {
|
||||
if (!isObject(input)) return null;
|
||||
const now = nowFor(tarinai);
|
||||
|
|
@ -138,7 +142,7 @@
|
|||
forcedId: forcedRequest?.uid || String(input.forcedId || previous?.forcedId || ""),
|
||||
forcedRequest,
|
||||
sourceReasonText: String(input.sourceReasonText || input.forcedReasonText || forcedRequest?.reasonText || previous?.sourceReasonText || ""),
|
||||
causeText: String(input.causeText || forcedRequest?.causeText || previous?.causeText || ""),
|
||||
causeText: String(input.causeText || forcedRequest?.causeText || ((forced || !isEffectCauseText(previous?.causeText)) ? previous?.causeText : "") || ""),
|
||||
specId: String(input.specId || input.actionSpecId || previous?.specId || actionId),
|
||||
specKind: String(input.specKind || previous?.specKind || "need_action"),
|
||||
};
|
||||
|
|
@ -199,7 +203,7 @@
|
|||
if (!value) {
|
||||
const data = isObject(b.data) ? { ...b.data } : {};
|
||||
delete data.forcedSource;
|
||||
const next = normalizeBehaviorValue({ ...b, source: b.source === "forced" ? "need" : (b.source || "need"), forcedId: "", forcedRequest: null, sourceReasonText: "", data }, tarinai, null);
|
||||
const next = normalizeBehaviorValue({ ...b, source: b.source === "forced" ? "need" : (b.source || "need"), forcedId: "", forcedRequest: null, sourceReasonText: "", causeText: "", data }, tarinai, null);
|
||||
tarinai.behavior = next;
|
||||
return next;
|
||||
}
|
||||
|
|
@ -212,7 +216,7 @@
|
|||
forcedId: String(request?.uid || value.uid || value.forcedId || b.forcedId || ""),
|
||||
forcedRequest: request,
|
||||
sourceReasonText: request?.reasonText || b.sourceReasonText || "",
|
||||
causeText: b.causeText || request?.causeText || "",
|
||||
causeText: request?.causeText || b.causeText || "",
|
||||
data: { ...(b.data || {}), forcedSource: request?.source || value.source || "external" },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ function actionTextForReason(action = null, tarinai = null) {
|
|||
return String(action?.label || action?.phrase || behavior?.label || "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
}
|
||||
|
||||
function forcedCausePhrase(source = "", fallback = "") {
|
||||
const key = String(source || "");
|
||||
if (key === "love_mochi") return "\u3078\u3053\u9905\u306e\u52b9\u679c";
|
||||
|
|
@ -72,7 +76,11 @@ function formatCauseActionText(cause = "", actionText = "\u884c\u52d5\u3057\u306
|
|||
function causePhraseForReason(chosenNeed, action, tarinai = null, options = {}) {
|
||||
if (options?.causeText) return String(options.causeText || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
const b = (typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior) || {};
|
||||
if (b.causeText) return String(b.causeText).replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
if (b.causeText) {
|
||||
const cause = String(b.causeText).replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
const forced = (typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(b) : Boolean(b.forcedId || b.forcedRequest)) || (typeof isTarinaiBehaviorForced === "function" ? isTarinaiBehaviorForced(tarinai) : false);
|
||||
if (forced || !isEffectCauseText(cause)) return cause;
|
||||
}
|
||||
if ((typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(b) : Boolean(b.forcedId || b.forcedRequest)) || b.source) {
|
||||
const forced = forcedCausePhrase(b.source || "", b.sourceReasonText || b.sourceReasonText || "");
|
||||
if (forced) return forced;
|
||||
|
|
@ -112,8 +120,15 @@ function needsForBehaviorText(tarinai, behavior = {}, fallbackNeed = "fulfill")
|
|||
|
||||
function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {}) {
|
||||
const request = behavior?.forcedRequest || (typeof currentForcedBehaviorRequest === "function" ? currentForcedBehaviorRequest(tarinai) : null) || null;
|
||||
const directCause = options?.causeText || behavior?.causeText || request?.causeText || "";
|
||||
if (directCause) return String(directCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
const optionCause = options?.causeText || "";
|
||||
if (optionCause) return String(optionCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
const requestCause = request?.causeText || "";
|
||||
if (requestCause) return String(requestCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
const behaviorCause = behavior?.causeText || "";
|
||||
if (behaviorCause) {
|
||||
const forced = (typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(behavior) : Boolean(behavior?.forcedId || behavior?.forcedRequest)) || (typeof isTarinaiBehaviorForced === "function" ? isTarinaiBehaviorForced(tarinai) : false);
|
||||
if (forced || !isEffectCauseText(behaviorCause)) return String(behaviorCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||
}
|
||||
if ((typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(behavior) : Boolean(behavior?.forcedId || behavior?.forcedRequest)) || request) {
|
||||
const source = request?.source || behavior?.source || "external";
|
||||
const sourceReason = request?.reasonText || behavior?.sourceReasonText || behavior?.sourceReasonText || "";
|
||||
|
|
|
|||
|
|
@ -79,19 +79,82 @@ function findStructureBuildSpot(t, world, type) {
|
|||
if (world?.placementBlocked?.(probe)) continue;
|
||||
let nearestSame = Infinity;
|
||||
if (type === "grass_bed") {
|
||||
for (const it of world?.nearbyItems?.(c.x, c.y, 160) || world?.items || []) {
|
||||
const spacing = Math.max(46, (probe.r || 18) * 3.0);
|
||||
for (const it of world?.nearbyItems?.(c.x, c.y, spacing + 90) || world?.items || []) {
|
||||
if (!it || it.dead || it.type !== "grass_bed") continue;
|
||||
nearestSame = Math.min(nearestSame, distXY(c.x, c.y, it.x, it.y));
|
||||
const d = distXY(c.x, c.y, it.x, it.y);
|
||||
const required = spacing + Math.max(0, ((it.r || 18) - (probe.r || 18)) * 0.5);
|
||||
nearestSame = Math.min(nearestSame, d);
|
||||
if (d < required) { nearestSame = -Infinity; break; }
|
||||
}
|
||||
if (nearestSame === -Infinity) continue;
|
||||
}
|
||||
const score = (Number.isFinite(nearestSame) ? nearestSame : 160) - distXY(c.x, c.y, baseX, baseY) * 0.12;
|
||||
if (score > bestScore) { best = c; bestScore = score; }
|
||||
}
|
||||
if (type === "grass_bed") return best;
|
||||
return best || candidates[0] || { x: baseX, y: baseY };
|
||||
}
|
||||
|
||||
|
||||
function structureGrassStageCost(type = "") {
|
||||
return (type === "grass_bed" || type === "plushie") ? 4 : 1;
|
||||
}
|
||||
|
||||
function grassMaterialAvailableStages(material) {
|
||||
if (!material || material.dead || material.type !== "grass" || (material.amount || 0) <= 0) return 0;
|
||||
const stage = typeof normalizeGrassStage === "function"
|
||||
? normalizeGrassStage(material)
|
||||
: (typeof grassStageFromGrowth === "function" ? grassStageFromGrowth(material.growth, material.amount) : Math.floor((Number(material.amount || 0) || 0) / 24) - 1);
|
||||
return Math.max(0, Math.floor(Number(stage) || 0) + 1);
|
||||
}
|
||||
|
||||
function reservedGrassStagesForBuildMaterial(world, material, type, self = null) {
|
||||
if (!world || !material) return 0;
|
||||
const id = material.id || "";
|
||||
if (!id) return 0;
|
||||
let reserved = 0;
|
||||
for (const other of world.tarinai || []) {
|
||||
if (!other || other.dead || other === self) continue;
|
||||
const plan = other.buildPlan || null;
|
||||
const planType = plan?.type || (/build_plushie/.test(other.behavior?.actionId || "") ? "plushie" : (/build_grass_bed/.test(other.behavior?.actionId || "") ? "grass_bed" : ""));
|
||||
if (type && planType && planType !== type) continue;
|
||||
const targetId = plan?.materialId || other.target?.id || "";
|
||||
if (targetId !== id) continue;
|
||||
const state = String(other.state || "");
|
||||
const actionId = String(other.behavior?.actionId || other.currentActionId || "");
|
||||
if (state === "seek_material" || state === "build" || /build_grass_bed|build_plushie|seek_material/.test(actionId)) {
|
||||
reserved += structureGrassStageCost(planType || type);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
}
|
||||
|
||||
function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
|
||||
const required = structureGrassStageCost(type);
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
const candidates = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist + 40, true) || world?.items || [];
|
||||
for (const item of candidates) {
|
||||
if (!item || item.dead || !roleMatches(item, "grassMaterial")) continue;
|
||||
if (tarinai?.shouldAvoidTarget?.(item)) continue;
|
||||
const available = grassMaterialAvailableStages(item);
|
||||
if (available < required) continue;
|
||||
const reserved = reservedGrassStagesForBuildMaterial(world, item, type, tarinai);
|
||||
if (available - reserved < required) continue;
|
||||
const d = dist(tarinai, item);
|
||||
if (d > maxDist) continue;
|
||||
// Prefer nearby grass, but lightly spread simultaneous builders across
|
||||
// separate tufts so a crowd does not all reserve one valid material.
|
||||
const jitter = typeof stableUnit === "function" ? stableUnit(tarinai.id || tarinai.familyKey || "", `build-material:${item.id || ""}`) : Math.random();
|
||||
const score = d + Math.max(0, reserved) * 42 + jitter * 12;
|
||||
if (score < bestScore) { best = item; bestScore = score; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function buildOwnedStructureFromGrass(t, world, type, label) {
|
||||
const material = findNearbyMaterial(world, t, "grassMaterial", 520);
|
||||
const material = findNearbyBuildMaterial(world, t, type, 520);
|
||||
if (!material || !globalThis.StructureRegistry) return false;
|
||||
t.buildPlan = {
|
||||
type,
|
||||
|
|
@ -111,8 +174,8 @@ function continueBuildPlan(t, world, dt) {
|
|||
return false;
|
||||
}
|
||||
let material = (world?.itemById?.(plan.materialId) || null);
|
||||
if (material && (material.dead || !roleMatches(material, "grassMaterial"))) material = null;
|
||||
if (!material) material = findNearbyMaterial(world, t, "grassMaterial", 520);
|
||||
if (material && (material.dead || !roleMatches(material, "grassMaterial") || grassMaterialAvailableStages(material) < structureGrassStageCost(plan.type))) material = null;
|
||||
if (!material) material = findNearbyBuildMaterial(world, t, plan.type, 520);
|
||||
if (!material) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u6750\u6599\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||
}
|
||||
|
|
@ -125,10 +188,13 @@ function continueBuildPlan(t, world, dt) {
|
|||
plan.timer = Math.max(0, Number(plan.timer || 0) - Math.max(0.05, dt || 0.1));
|
||||
t.setActionState?.("build", { target: material, reason: `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`, sleeping: false });
|
||||
if (plan.timer > 0) return true;
|
||||
consumeGrassMaterial(material, plan.type === "grass_bed" ? 4 : 1);
|
||||
const spot = plan.type === "plushie"
|
||||
? { x: t.x, y: t.y - Math.max(28, (t.radius || 24) * 1.28) }
|
||||
: findStructureBuildSpot(t, world, plan.type);
|
||||
if (!spot) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "作る場所が見つからない");
|
||||
}
|
||||
consumeGrassMaterial(material, structureGrassStageCost(plan.type));
|
||||
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
||||
if (!structure) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
|
|
@ -224,8 +195,54 @@
|
|||
if (!id) return null;
|
||||
return this.world?.items?.find?.(it => it && !it.dead && it.id === id && it.type === "nest_box") || null;
|
||||
},
|
||||
|
||||
nestBoxEntryActive() {
|
||||
const now = this.world?.time || 0;
|
||||
if (now < (this._nestBoxExitSmoothUntil || -Infinity)) return true;
|
||||
if (!this.insideNestBoxId) return false;
|
||||
return now < (this._nestBoxEntrySmoothUntil || -Infinity) || ((this.nestFade || 0) > 0.01 && (this.nestFade || 0) < 0.985);
|
||||
},
|
||||
|
||||
abortNestBoxEntry(box = null, reason = "巣箱がいっぱいで入れない") {
|
||||
const now = this.world?.time || 0;
|
||||
if (this.insideNestBoxId && (!box || this.insideNestBoxId === box.id)) this.leaveNestBox?.(box);
|
||||
if (!this.insideNestBoxId) this.nestFade = 0;
|
||||
if (!box || this.target === box) {
|
||||
this.target = null;
|
||||
this.targetKey = "";
|
||||
}
|
||||
this.sleeping = false;
|
||||
this.sleepSession = null;
|
||||
this._nestBoxAvoidId = box?.id || this._nestBoxAvoidId || "";
|
||||
this._nestBoxAvoidUntil = now + 10.0;
|
||||
this.actionIdCooldowns = this.actionIdCooldowns || {};
|
||||
this.actionCooldowns = this.actionCooldowns || {};
|
||||
this.actionIdCooldowns.sleep_in_bed = Math.max(this.actionIdCooldowns.sleep_in_bed || 0, 8.0);
|
||||
this.actionIdCooldowns.return_owned_structure = Math.max(this.actionIdCooldowns.return_owned_structure || 0, 5.0);
|
||||
this.actionCooldowns.sleep = Math.max(this.actionCooldowns.sleep || 0, 4.5);
|
||||
if (box) {
|
||||
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(box) : { x: box.x, y: box.y };
|
||||
const dx = this.x - (entry.x || box.x || this.x);
|
||||
const dy = this.y - (entry.y || box.y || this.y);
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
// Do not teleport/push away from the nest. A failed entry becomes a normal
|
||||
// short avoidance target so ordinary movement handles the exit path.
|
||||
this.vx = clamp((this.vx || 0) + dx / d * 10, -80, 80);
|
||||
this.vy = clamp((this.vy || 0) + dy / d * 8, -80, 80);
|
||||
}
|
||||
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 0.7);
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
this.goIdle?.(reason);
|
||||
this.thought = reason;
|
||||
this.pinchThoughtTimer = Math.max(this.pinchThoughtTimer || 0, 1.0);
|
||||
return false;
|
||||
},
|
||||
|
||||
wantsNestBox(box = null) {
|
||||
if (!box || box.dead || box.type !== "nest_box") return false;
|
||||
const now = this.world?.time || 0;
|
||||
if (this._nestBoxAvoidId === box.id && now < (this._nestBoxAvoidUntil || -Infinity)) return false;
|
||||
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity) && (this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return true;
|
||||
if (!(this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return false;
|
||||
return this.target === box || this.insideNestBoxId === box.id;
|
||||
|
|
@ -236,12 +253,22 @@
|
|||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||
const alreadyInside = this.insideNestBoxId === box.id;
|
||||
if (!alreadyInside && occupants.length >= capacity) return false;
|
||||
const now = this.world?.time || 0;
|
||||
if (!alreadyInside && occupants.length >= capacity) return this.abortNestBoxEntry?.(box, "巣箱がいっぱいで入れない") || false;
|
||||
if (!alreadyInside) {
|
||||
this.nestBoxEnteredAt = this.world?.time || 0;
|
||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, (this.world?.time || 0) + 14);
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(box) : { x: box.x, y: box.y };
|
||||
this.nestBoxEnteredAt = now;
|
||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, now + 14);
|
||||
this.nestBoxSlotIndex = Math.min(occupants.length, Math.max(0, capacity - 1));
|
||||
this._nestBoxEntrySmoothUntil = (this.world?.time || 0) + 0.85;
|
||||
this._nestBoxEntryStartX = Number.isFinite(this.x) ? this.x : entry.x;
|
||||
this._nestBoxEntryStartY = Number.isFinite(this.y) ? this.y : entry.y;
|
||||
this._nestBoxEntryDoorX = Number.isFinite(entry.x) ? entry.x : box.x;
|
||||
this._nestBoxEntryDoorY = Number.isFinite(entry.y) ? entry.y : box.y;
|
||||
this._nestBoxEntryDuration = 0.72;
|
||||
this._nestBoxEntrySmoothUntil = now + this._nestBoxEntryDuration;
|
||||
this.nestFade = Math.min(this.nestFade || 0, 0.14);
|
||||
}
|
||||
this.insideNestBoxId = box.id;
|
||||
this.target = box;
|
||||
|
|
@ -249,15 +276,31 @@
|
|||
if (this.state === "sleep") this.sleeping = true;
|
||||
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(box, this) : { x: box.x, y: box.y };
|
||||
const smoothDt = clamp(Number(dt || 0.016) || 0.016, 0.008, 0.045);
|
||||
const firstPhase = (this.world?.time || 0) < (this._nestBoxEntrySmoothUntil || -Infinity);
|
||||
const pull = clamp(smoothDt * (alreadyInside ? (firstPhase ? 5.2 : 8.5) : 7.2), 0, 0.55);
|
||||
this.x = lerp(this.x, inner.x, pull);
|
||||
this.y = lerp(this.y, inner.y, pull);
|
||||
if (!firstPhase && (this.nestFade >= 0.97 || distXY(this.x, this.y, inner.x, inner.y) < 2.0)) {
|
||||
this.x = inner.x;
|
||||
this.y = inner.y;
|
||||
const duration = Math.max(0.35, Number(this._nestBoxEntryDuration || 0.95) || 0.95);
|
||||
const progress = clamp(1 - Math.max(0, (this._nestBoxEntrySmoothUntil || now) - now) / duration, 0, 1);
|
||||
const eased = progress * progress * (3 - 2 * progress);
|
||||
if (progress < 1) {
|
||||
const sx = Number.isFinite(this._nestBoxEntryStartX) ? this._nestBoxEntryStartX : this.x;
|
||||
const sy = Number.isFinite(this._nestBoxEntryStartY) ? this._nestBoxEntryStartY : this.y;
|
||||
const doorX = Number.isFinite(this._nestBoxEntryDoorX) ? this._nestBoxEntryDoorX : box.x;
|
||||
const doorY = Number.isFinite(this._nestBoxEntryDoorY) ? this._nestBoxEntryDoorY : box.y;
|
||||
const ax = lerp(sx, doorX, eased);
|
||||
const ay = lerp(sy, doorY, eased);
|
||||
const bx = lerp(doorX, inner.x, eased);
|
||||
const by = lerp(doorY, inner.y, eased);
|
||||
this.x = lerp(ax, bx, eased);
|
||||
this.y = lerp(ay, by, eased);
|
||||
this.nestFade = clamp(Math.max(this.nestFade || 0, eased * 0.92), 0, 1);
|
||||
} else {
|
||||
const pull = clamp(1 - Math.exp(-smoothDt * 5.2), 0, 0.22);
|
||||
this.x = lerp(this.x, inner.x, pull);
|
||||
this.y = lerp(this.y, inner.y, pull);
|
||||
if (this.nestFade >= 0.97 || distXY(this.x, this.y, inner.x, inner.y) < 1.6) {
|
||||
this.x = inner.x;
|
||||
this.y = inner.y;
|
||||
}
|
||||
this.nestFade = clamp((this.nestFade || 0) + smoothDt * 1.8, 0, 1);
|
||||
}
|
||||
this.nestFade = clamp((this.nestFade || 0) + smoothDt * 4.2, 0, 1);
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.impulseVx = 0;
|
||||
|
|
@ -265,25 +308,108 @@
|
|||
return true;
|
||||
},
|
||||
|
||||
beginNestBoxExitAnimation(box = null) {
|
||||
const b = box || this.nestBoxById(this.insideNestBoxId);
|
||||
const now = this.world?.time || 0;
|
||||
if (!b || !this.world?.nestBoxEntryPoint) {
|
||||
this._nestBoxExitSmoothUntil = -Infinity;
|
||||
this._nestBoxExitDuration = 0;
|
||||
this._nestBoxExitBoxId = "";
|
||||
return false;
|
||||
}
|
||||
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(b, this) : { x: b.x, y: b.y };
|
||||
const door = this.world.nestBoxEntryPoint(b);
|
||||
const exit = this.world.nestBoxExitPoint ? this.world.nestBoxExitPoint(b, this) : door;
|
||||
this._nestBoxExitStartX = Number.isFinite(this.x) ? this.x : inner.x;
|
||||
this._nestBoxExitStartY = Number.isFinite(this.y) ? this.y : inner.y;
|
||||
if ((this.nestFade || 0) > 0.82 || distXY(this._nestBoxExitStartX, this._nestBoxExitStartY, inner.x, inner.y) > (b.r || 42) * 0.7) {
|
||||
this._nestBoxExitStartX = inner.x;
|
||||
this._nestBoxExitStartY = inner.y;
|
||||
}
|
||||
this._nestBoxExitDoorX = Number.isFinite(door.x) ? door.x : b.x;
|
||||
this._nestBoxExitDoorY = Number.isFinite(door.y) ? door.y : b.y;
|
||||
this._nestBoxExitEndX = Number.isFinite(exit.x) ? exit.x : this._nestBoxExitDoorX;
|
||||
this._nestBoxExitEndY = Number.isFinite(exit.y) ? exit.y : this._nestBoxExitDoorY;
|
||||
this._nestBoxExitDuration = 0.58;
|
||||
this._nestBoxExitSmoothUntil = now + this._nestBoxExitDuration;
|
||||
this._nestBoxExitBoxId = b.id || "";
|
||||
this.nestFade = Math.max(this.nestFade || 0, 0.96);
|
||||
return true;
|
||||
},
|
||||
|
||||
updateNestBoxExitAnimation(dt = 0.016) {
|
||||
const now = this.world?.time || 0;
|
||||
const until = this._nestBoxExitSmoothUntil || -Infinity;
|
||||
if (!(now < until)) return false;
|
||||
const duration = Math.max(0.20, Number(this._nestBoxExitDuration || 0.58) || 0.58);
|
||||
const progress = clamp(1 - Math.max(0, until - now) / duration, 0, 1);
|
||||
const eased = progress * progress * (3 - 2 * progress);
|
||||
const sx = Number.isFinite(this._nestBoxExitStartX) ? this._nestBoxExitStartX : this.x;
|
||||
const sy = Number.isFinite(this._nestBoxExitStartY) ? this._nestBoxExitStartY : this.y;
|
||||
const dx = Number.isFinite(this._nestBoxExitDoorX) ? this._nestBoxExitDoorX : sx;
|
||||
const dy = Number.isFinite(this._nestBoxExitDoorY) ? this._nestBoxExitDoorY : sy;
|
||||
const ex = Number.isFinite(this._nestBoxExitEndX) ? this._nestBoxExitEndX : dx;
|
||||
const ey = Number.isFinite(this._nestBoxExitEndY) ? this._nestBoxExitEndY : dy;
|
||||
const ax = lerp(sx, dx, eased);
|
||||
const ay = lerp(sy, dy, eased);
|
||||
const bx = lerp(dx, ex, eased);
|
||||
const by = lerp(dy, ey, eased);
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
this.x = clamp(lerp(ax, bx, eased), pad, Math.max(pad, (this.world?.w || this.x) - pad));
|
||||
this.y = clamp(lerp(ay, by, eased), pad, Math.max(pad, (this.world?.h || this.y) - pad));
|
||||
this.nestFade = clamp(1 - eased, 0, 1);
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
if (progress >= 0.995 || until - now <= Math.max(0.018, dt || 0.016)) {
|
||||
this.x = clamp(ex, pad, Math.max(pad, (this.world?.w || ex) - pad));
|
||||
this.y = clamp(ey, pad, Math.max(pad, (this.world?.h || ey) - pad));
|
||||
const outDx = ex - dx;
|
||||
const outDy = ey - dy;
|
||||
const outD = Math.hypot(outDx, outDy) || 1;
|
||||
this.vx = outDx / outD * deterministicRange(this.world, "nest-exit-vx", 6, 14, this);
|
||||
this.vy = outDy / outD * deterministicRange(this.world, "nest-exit-vy", 8, 18, this);
|
||||
this.nestFade = 0;
|
||||
this._nestBoxExitSmoothUntil = -Infinity;
|
||||
this._nestBoxExitDuration = 0;
|
||||
this._nestBoxExitBoxId = "";
|
||||
this._nestBoxExitStartX = this._nestBoxExitStartY = this._nestBoxExitDoorX = this._nestBoxExitDoorY = this._nestBoxExitEndX = this._nestBoxExitEndY = NaN;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
leaveNestBox(box = null) {
|
||||
const b = box || this.nestBoxById(this.insideNestBoxId);
|
||||
if (b && this.world?.nestBoxEntryPoint) {
|
||||
const hadInside = Boolean(this.insideNestBoxId);
|
||||
if (b && this.world?.nestBoxEntryPoint && hadInside) this.beginNestBoxExitAnimation?.(b);
|
||||
else if (b && this.world?.nestBoxEntryPoint) {
|
||||
const entry = this.world.nestBoxExitPoint ? this.world.nestBoxExitPoint(b, this) : this.world.nestBoxEntryPoint(b);
|
||||
this.x = clamp(entry.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
|
||||
this.y = clamp(entry.y, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
|
||||
this.vx = deterministicRange(this.world, "sleep-disease-shiver-vx", -8, 8, this);
|
||||
this.vy = deterministicRange(this.world, "sleep-disease-shiver-vy", 6, 18, this);
|
||||
}
|
||||
this.insideNestBoxId = null;
|
||||
this.nestFade = 0;
|
||||
if (this.target === b) {
|
||||
this.target = null;
|
||||
this.targetKey = "";
|
||||
}
|
||||
this.sleeping = false;
|
||||
this.sleepSession = null;
|
||||
if (this.state === "sleep" || this.state === "seek_bed") this.goIdle?.("巣箱から出た");
|
||||
this.nestBoxEnteredAt = -Infinity;
|
||||
this.nestBoxStayUntil = -Infinity;
|
||||
this.nestBoxSlotIndex = -1;
|
||||
this._nestBoxEntrySmoothUntil = -Infinity;
|
||||
this._nestBoxEntryStartX = NaN;
|
||||
this._nestBoxEntryStartY = NaN;
|
||||
this._nestBoxEntryDoorX = NaN;
|
||||
this._nestBoxEntryDoorY = NaN;
|
||||
this._nestBoxEntryDuration = 0;
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
return true;
|
||||
},
|
||||
|
||||
updateNestBoxPresence(dt = 0) {
|
||||
if (this.updateNestBoxExitAnimation?.(dt || 0.016)) return null;
|
||||
const currentBox = this.nestBoxById(this.insideNestBoxId);
|
||||
const targetBox = this.target && !this.target.dead && this.target.type === "nest_box" ? this.target : null;
|
||||
const box = currentBox || targetBox;
|
||||
|
|
@ -310,22 +436,37 @@
|
|||
|
||||
if (!this.wantsNestBox(targetBox)) {
|
||||
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||
if (targetBox && this.target === targetBox && !this.insideNestBoxId) {
|
||||
const now = this.world?.time || 0;
|
||||
this.target = null;
|
||||
this.targetKey = "";
|
||||
this._nestBoxAvoidId = targetBox.id || this._nestBoxAvoidId || "";
|
||||
this._nestBoxAvoidUntil = Math.max(this._nestBoxAvoidUntil || -Infinity, now + 3.2);
|
||||
if (this.state === "idle" || this.state === "wander") this.wanderAngle = (this.wanderAngle || 0) + deterministicRange(this.world, "nest-idle-dispersal-angle", -1.1, 1.1, this, targetBox);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(this._nestBoxSeekStartedAt) || this._nestBoxSeekTargetId !== targetBox.id) {
|
||||
this._nestBoxSeekStartedAt = this.world?.time || 0;
|
||||
this._nestBoxSeekTargetId = targetBox.id;
|
||||
}
|
||||
if (!this.insideNestBoxId && global.TarinaiNestSleepSystem?.sleepPlaceAvailableFor && !global.TarinaiNestSleepSystem.sleepPlaceAvailableFor(this.world, this, targetBox)) {
|
||||
return this.abortNestBoxEntry?.(targetBox, "巣箱がいっぱいで入れない") || null;
|
||||
}
|
||||
|
||||
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(targetBox) : { x: targetBox.x, y: targetBox.y };
|
||||
const base = this.world?.nestBoxBaseRect ? this.world.nestBoxBaseRect(targetBox) : null;
|
||||
const r = targetBox.r || 42;
|
||||
const entryD = distXY(this.x, this.y, entry.x, entry.y);
|
||||
const centerD = distXY(this.x, this.y, targetBox.x, targetBox.y);
|
||||
const inDoorColumn = base && this.x >= base.left + r * 0.18 && this.x <= base.right - r * 0.18 && this.y >= base.top + r * 0.06 && this.y <= base.bottom + r * 0.48;
|
||||
const captureRange = Math.max(76, r * 1.55, this.radius + 48);
|
||||
const nearEnough = entryD <= captureRange || centerD <= Math.max(78, r * 1.38) || inDoorColumn;
|
||||
const inDoorColumn = base && this.x >= base.left + r * 0.30 && this.x <= base.right - r * 0.30 && this.y >= base.top + r * 0.16 && this.y <= base.bottom + r * 0.34;
|
||||
const captureRange = Math.max(58, r * 1.18, (this.radius || 20) + 30);
|
||||
const nearEnough = entryD <= captureRange || centerD <= Math.max(58, r * 1.08) || inDoorColumn;
|
||||
if (nearEnough) {
|
||||
this.enterNestBox(targetBox, dt || 0.016);
|
||||
return targetBox;
|
||||
if (this.enterNestBox(targetBox, dt || 0.016)) return targetBox;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||
return null;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -379,6 +379,10 @@ function processForcedBehaviorQueue(tarinai, world, needs) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function effectCauseTextLooksStale(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
}
|
||||
|
||||
function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel = null, reasonText = null, reason = null, target = undefined, subNeed = "", phase = "active", source = "state", forced = null, forcedId = "", sourceReasonText = "", forcedReasonText = "", causeText = "" } = {}) {
|
||||
if (!tarinai) return;
|
||||
const now = tarinai.world?.time || 0;
|
||||
|
|
@ -400,9 +404,11 @@ function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel =
|
|||
}
|
||||
const nextForced = forced ?? preserveForced;
|
||||
const nextSource = nextForced ? "forced" : (source || previousBehavior.source || "state");
|
||||
const nextForcedId = forcedId || previousBehavior.forcedId || "";
|
||||
const nextSourceReason = sourceReasonText || forcedReasonText || previousBehavior.sourceReasonText || "";
|
||||
const nextCauseText = causeText || previousBehavior.causeText || "";
|
||||
const nextForcedId = nextForced ? (forcedId || previousBehavior.forcedId || "") : "";
|
||||
const nextSourceReason = nextForced ? (sourceReasonText || forcedReasonText || previousBehavior.sourceReasonText || "") : (sourceReasonText || forcedReasonText || "");
|
||||
const explicitCauseText = String(causeText || "");
|
||||
const previousCauseText = String(previousBehavior.causeText || "");
|
||||
const nextCauseText = explicitCauseText || ((nextForced || !effectCauseTextLooksStale(previousCauseText)) ? previousCauseText : "");
|
||||
if (nextCauseText) {
|
||||
nextReasonText = buildReasonText(nextNeed, existingTiedNeeds.length ? existingTiedNeeds : [nextNeed], { ...(action || {}), id: nextActionId, label: nextActionLabel, subNeed: subNeed || action?.subNeed || "" }, tarinai, tarinai.world || null, { causeText: nextCauseText });
|
||||
}
|
||||
|
|
@ -422,7 +428,7 @@ function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel =
|
|||
causeText: nextCauseText,
|
||||
forced: nextForced,
|
||||
forcedId: nextForcedId,
|
||||
forcedRequest: previousBehavior.forcedRequest || null,
|
||||
forcedRequest: nextForced ? (previousBehavior.forcedRequest || null) : null,
|
||||
priority: previousBehavior.priority ?? actionPriority(action),
|
||||
phase,
|
||||
startedAt: previousBehavior.startedAt || now,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function roleMatches(item, role) {
|
|||
if (item.type === "duplicator") return effectiveFoodRoleMatches(item, role);
|
||||
if (item.roles?.[role]) return true;
|
||||
if (role === "food" || role === "drink" || role === "medicine") return effectiveFoodRoleMatches(item, role);
|
||||
if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box";
|
||||
if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box" || item.type === "grass_bed";
|
||||
if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(item.type);
|
||||
if (role === "grassMaterial") return item.type === "grass";
|
||||
return false;
|
||||
|
|
@ -65,21 +65,21 @@ function foodPriorityRank(tarinai, item) {
|
|||
const need = typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need;
|
||||
const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0;
|
||||
const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 0) || 0;
|
||||
if (type === "sweet") return 4100 + Math.max(0, healthNeed - 40) * 6;
|
||||
if (type === "sweet") return 4300 + Math.min(120, Math.max(0, healthNeed - 40) * 3);
|
||||
if (type === "water") return 3650 + Math.max(0, healthNeed - 42) * 7;
|
||||
if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6;
|
||||
if (type === "love_mochi") return 3300 + Math.max(0, socialNeed - 45) * 7;
|
||||
if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4;
|
||||
if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450;
|
||||
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3000 + Math.max(0, healthNeed - 45) * 5;
|
||||
if (type === "love_mochi") return 3340 + Math.min(120, Math.max(0, socialNeed - 45) * 3);
|
||||
if (type === "fight_mochi") return 3280 + Math.min(90, Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 3);
|
||||
if (type === "sleep_drug") return need === "sleep" ? 3380 : 3220;
|
||||
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3260 + Math.min(120, Math.max(0, healthNeed - 45) * 3);
|
||||
if (type === "food" || type === "ant_corpse") return 3300;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 3300;
|
||||
if (type === "grass") {
|
||||
const hunger = Number(tarinai?.hunger || 0) || 0;
|
||||
const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0;
|
||||
return 2000 + Math.max(0, hunger - 64) * 18 + Math.max(0, foodNeed - 58) * 12;
|
||||
return 2200 + Math.min(260, Math.max(0, hunger - 64) * 3 + Math.max(0, foodNeed - 58) * 2);
|
||||
}
|
||||
if (type === "zunchi") return 1000 + (tarinai?.isZunchiSlave ? 250 : 0);
|
||||
if (type === "food" || type === "ant_corpse") return 3000;
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 3000;
|
||||
if (type === "zunchi") return 900 + (tarinai?.isZunchiSlave ? 180 : 0);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,9 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
: (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []);
|
||||
const candidates = Array.isArray(primary) ? primary.slice() : [];
|
||||
if (foodEmergency && world?.itemsOfType) {
|
||||
for (const type of ["food", "sweet", "grass", "zunchi", "ant_corpse", "zunda_juice", "duplicator"]) {
|
||||
const emergencyTypes = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "grass", "zunchi", "ant_corpse", "zunda_juice", "duplicator"]);
|
||||
if (typeof FOOD_SERVING_TYPES !== "undefined") for (const type of FOOD_SERVING_TYPES) emergencyTypes.add(type);
|
||||
for (const type of emergencyTypes) {
|
||||
for (const it of world.itemsOfType(type) || []) {
|
||||
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
|
||||
}
|
||||
|
|
@ -149,6 +151,12 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
}
|
||||
for (const item of candidates) {
|
||||
if (!roleMatches(item, role)) continue;
|
||||
if (role === "food" && typeof tarinai?.foodItemHasServingLeft === "function") {
|
||||
const type = effectiveFoodTypeForItem(item);
|
||||
if ((item.type === "duplicator" || (typeof isServingFoodType === "function" && isServingFoodType(type))) && !tarinai.foodItemHasServingLeft(item)) continue;
|
||||
if (item.type === "grass" && (Number(item.amount || 0) || 0) <= 0) continue;
|
||||
if (item.type === "zunchi" && (Number(item.amount || 0) || 0) <= 0) continue;
|
||||
}
|
||||
if (tarinai?.shouldAvoidTarget?.(item)) continue;
|
||||
const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item);
|
||||
if (d > searchDist) continue;
|
||||
|
|
@ -164,40 +172,19 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
}
|
||||
|
||||
function sleepPlaceAvailableFor(world, tarinai, item) {
|
||||
if (!world || !tarinai || !item || item.dead) return false;
|
||||
if (!(item.type === "nest_box" || item.type === "bed")) return false;
|
||||
if (tarinai.shouldAvoidTarget?.(item)) return false;
|
||||
const capacity = item.type === "nest_box"
|
||||
? (world.nestBoxCapacity ? world.nestBoxCapacity(item) : 5)
|
||||
: 1;
|
||||
const occupancy = item.type === "nest_box"
|
||||
? (world.nestBoxOccupants ? world.nestBoxOccupants(item, Infinity).length : world.bedOccupancy?.(item) || 0)
|
||||
: (world.bedOccupancy?.(item) || 0);
|
||||
return occupancy < capacity;
|
||||
return globalThis.TarinaiNestSleepSystem?.sleepPlaceAvailableFor?.(world, tarinai, item) || false;
|
||||
}
|
||||
|
||||
function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) {
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
const list = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world?.items || [];
|
||||
for (const item of list) {
|
||||
if (!item || item.dead || item.type !== type) continue;
|
||||
if (!sleepPlaceAvailableFor(world, tarinai, item)) continue;
|
||||
const d = dist(tarinai, item);
|
||||
if (d > maxDist) continue;
|
||||
const comfort = world?.bedComfort ? world.bedComfort(item) : 1;
|
||||
const score = d - comfort * 18;
|
||||
if (score < bestScore) { best = item; bestScore = score; }
|
||||
}
|
||||
return best;
|
||||
return globalThis.TarinaiNestSleepSystem?.nearestSleepPlaceOfType?.(world, tarinai, type, maxDist) || null;
|
||||
}
|
||||
|
||||
function findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist = 520) {
|
||||
return globalThis.TarinaiNestSleepSystem?.findPrioritySleepPlaceBeforeGrassBedBuild?.(world, tarinai, maxDist) || null;
|
||||
}
|
||||
|
||||
function findNearestSleepPlace(world, tarinai, maxDist = 520) {
|
||||
return nearestSleepPlaceOfType(world, tarinai, "nest_box", maxDist)
|
||||
|| nearestSleepPlaceOfType(world, tarinai, "bed", maxDist)
|
||||
|| findOwnedStructure(world, tarinai, "grass_bed", maxDist)
|
||||
|| findUnownedStructure(world, tarinai, "grass_bed", maxDist)
|
||||
|| null;
|
||||
return globalThis.TarinaiNestSleepSystem?.findNearestSleepPlace?.(world, tarinai, maxDist) || null;
|
||||
}
|
||||
|
||||
function findNearbyDanger(world, tarinai, maxDist = 150) {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,11 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Need planning runtime and Tarinai prototype extensions.
|
||||
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, chooseActionForNeed, buildReasonText, formatCauseActionText, forcedCausePhrase, refreshBehaviorReasonText, fulfillPrimaryReason, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, findNearbyDanger, findNearbyMaterial, evaluateConflictUrge, conflictTargetFor, calculateStressFromNeeds, applyGroundStressModifier, applyNeedShock, applyNeedRelief, triggerNeedShockReaction, applyNeedSatisfaction, synchronizeActionTextFromState, setBehaviorText, setBehavior, clearBehavior, behaviorText, queueForcedTarinaiBehavior, clearForcedBehaviorQueue, processForcedBehaviorQueue, forcedBehaviorQueueOf, currentForcedBehaviorRequest, actionPriority, forcedBehaviorSourcePriority, completeForcedBehavior, retryForcedBehavior, relationFearSafe, isLiveTarinaiEntity, actionById });
|
||||
Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, chooseActionForNeed, buildReasonText, formatCauseActionText, forcedCausePhrase, refreshBehaviorReasonText, fulfillPrimaryReason, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, findPrioritySleepPlaceBeforeGrassBedBuild, findNearbyDanger, findNearbyMaterial, evaluateConflictUrge, conflictTargetFor, calculateStressFromNeeds, applyGroundStressModifier, applyNeedShock, applyNeedRelief, triggerNeedShockReaction, applyNeedSatisfaction, synchronizeActionTextFromState, setBehaviorText, setBehavior, clearBehavior, behaviorText, queueForcedTarinaiBehavior, clearForcedBehaviorQueue, processForcedBehaviorQueue, forcedBehaviorQueueOf, currentForcedBehaviorRequest, actionPriority, forcedBehaviorSourcePriority, completeForcedBehavior, retryForcedBehavior, relationFearSafe, isLiveTarinaiEntity, actionById });
|
||||
}
|
||||
|
||||
function decayNeedSatisfaction(tarinai, dt = 0) {
|
||||
|
|
@ -503,7 +474,7 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
|
|||
source: behavior?.source || "need",
|
||||
forced: behaviorIsForced(behavior),
|
||||
priority: behavior?.priority || 0,
|
||||
causeText: behavior?.causeText || "",
|
||||
causeText: (behaviorIsForced(behavior) || !/の効果$/.test(String(behavior?.causeText || "").trim().replace(/[。!?]+$/, ""))) ? (behavior?.causeText || "") : "",
|
||||
sourceReasonText: behavior?.sourceReasonText || "",
|
||||
forcedReasonText: behavior?.sourceReasonText || "",
|
||||
forcedRequest: behavior?.forcedRequest || null,
|
||||
|
|
|
|||
151
js/tarinai_nest_sleep_system.js
Normal file
151
js/tarinai_nest_sleep_system.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: tarinai/nest-sleep
|
||||
// Centralizes sleep furniture, nest occupancy/reservation, and sleep-target
|
||||
// selection policy so nest-box entrance logic and bed targeting do not drift.
|
||||
(function (global) {
|
||||
const math = global.TarinaiMath || {};
|
||||
const distFn = math.dist || ((a, b) => Math.hypot((a?.x || 0) - (b?.x || 0), (a?.y || 0) - (b?.y || 0)));
|
||||
const distXYFn = math.distXY || ((ax, ay, bx, by) => Math.hypot((ax || 0) - (bx || 0), (ay || 0) - (by || 0)));
|
||||
const clampFn = math.clamp || ((v, a, b) => Math.max(a, Math.min(b, v)));
|
||||
|
||||
function isSleepFurniture(item) {
|
||||
return Boolean(item && (item.roles?.sleepPlace || (typeof global.isSleepFurnitureType === "function" ? global.isSleepFurnitureType(item.type) : (item.type === "bed" || item.type === "nest_box" || item.type === "grass_bed"))));
|
||||
}
|
||||
|
||||
function nestBoxCapacity(worldRef, box = null) {
|
||||
if (worldRef?.nestBoxCapacity && worldRef.nestBoxCapacity !== nestBoxCapacityFacade) return worldRef.nestBoxCapacity(box);
|
||||
return 5;
|
||||
}
|
||||
|
||||
function nestBoxOccupants(worldRef, box, limit = 5) {
|
||||
if (!worldRef || !box || box.dead || box.type !== "nest_box") return [];
|
||||
const occupants = (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
return Number.isFinite(limit) ? occupants.slice(0, Math.max(0, limit)) : occupants;
|
||||
}
|
||||
|
||||
function isTarinaiHiddenInNestBox(worldRef, t) {
|
||||
if (!t || t.dead || !t.insideNestBoxId) return false;
|
||||
if (typeof t.nestBoxEntryActive === "function" && t.nestBoxEntryActive()) return false;
|
||||
const now = Number(worldRef?.time ?? t.world?.time ?? 0) || 0;
|
||||
if (now < (t._nestBoxEntrySmoothUntil || -Infinity) || ((t.nestFade || 0) > 0.01 && (t.nestFade || 0) < 0.985)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function tarinaiIsReservingNest(t, box, now = 0) {
|
||||
if (!t || t.dead || !box || t.insideNestBoxId === box.id) return false;
|
||||
if (t.target !== box) return false;
|
||||
if (t._nestBoxAvoidId === box.id && now < (t._nestBoxAvoidUntil || -Infinity)) return false;
|
||||
const seekingNest = t.state === "seek_bed" || t.state === "sleep" || t.sleeping || t.behavior?.actionId === "sleep_in_bed";
|
||||
if (!seekingNest) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function nestBoxReservationCount(worldRef, box) {
|
||||
if (!worldRef || !box || box.dead || box.type !== "nest_box") return 0;
|
||||
const now = Number(worldRef.time || 0) || 0;
|
||||
let n = 0;
|
||||
for (const t of worldRef.tarinai || []) if (tarinaiIsReservingNest(t, box, now)) n += 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
function bedOccupancy(worldRef, bed) {
|
||||
if (!worldRef || !isSleepFurniture(bed)) return 0;
|
||||
if (bed.type === "nest_box") {
|
||||
return nestBoxOccupants(worldRef, bed, Infinity).length + nestBoxReservationCount(worldRef, bed);
|
||||
}
|
||||
let n = 0;
|
||||
const radius = Math.max(86, (bed.r || 24) * 2.45);
|
||||
const near = typeof worldRef.nearbyTarinai === "function" ? worldRef.nearbyTarinai(bed.x, bed.y, radius) : (worldRef.tarinai || []);
|
||||
for (const t of near || []) {
|
||||
if (!t || t.dead || isTarinaiHiddenInNestBox(worldRef, t)) continue;
|
||||
if (t.state === "sleep" || t.state === "seek_bed" || t.target === bed) n += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function bedComfort(worldRef, bed) {
|
||||
if (!isSleepFurniture(bed)) return 0.7;
|
||||
const occ = bedOccupancy(worldRef, bed);
|
||||
return clampFn((bed.comfort ?? 1) - Math.max(0, occ - 3) * 0.075 - (bed.wear || 0) * 0.18, 0.42, 1.18);
|
||||
}
|
||||
|
||||
function sleepPlaceAvailableFor(worldRef, tarinai, item) {
|
||||
if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false;
|
||||
if (tarinai.shouldAvoidTarget?.(item)) return false;
|
||||
const capacity = item.type === "nest_box" ? nestBoxCapacity(worldRef, item) : 1;
|
||||
const occupancy = bedOccupancy(worldRef, item);
|
||||
return occupancy < capacity || (tarinai.insideNestBoxId && tarinai.insideNestBoxId === item.id);
|
||||
}
|
||||
|
||||
function findOwnedStructure(worldRef, tarinai, type = "", maxDist = Infinity) {
|
||||
let best = null, bestD = maxDist;
|
||||
for (const item of worldRef?.items || []) {
|
||||
if (!item?.isStructure || item.dead || item.ownerId !== tarinai?.id) continue;
|
||||
if (type && item.type !== type) continue;
|
||||
const d = distFn(tarinai, item);
|
||||
if (d < bestD) { best = item; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function findUnownedStructure(worldRef, tarinai, type = "", maxDist = Infinity) {
|
||||
let best = null, bestD = maxDist;
|
||||
for (const item of worldRef?.items || []) {
|
||||
if (!item?.isStructure || item.dead || item.ownerId) continue;
|
||||
if (type && item.type !== type) continue;
|
||||
if (tarinai?.shouldAvoidTarget?.(item)) continue;
|
||||
const d = distFn(tarinai, item);
|
||||
if (d < bestD) { best = item; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function nearestSleepPlaceOfType(worldRef, tarinai, type, maxDist = 520) {
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
const list = worldRef?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || worldRef?.items || [];
|
||||
for (const item of list) {
|
||||
if (!item || item.dead || item.type !== type) continue;
|
||||
if (!sleepPlaceAvailableFor(worldRef, tarinai, item)) continue;
|
||||
const d = distFn(tarinai, item);
|
||||
if (d > maxDist) continue;
|
||||
const comfort = bedComfort(worldRef, item);
|
||||
const score = d - comfort * 18;
|
||||
if (score < bestScore) { best = item; bestScore = score; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist = 520) {
|
||||
return findOwnedStructure(worldRef, tarinai, "grass_bed", maxDist)
|
||||
|| nearestSleepPlaceOfType(worldRef, tarinai, "nest_box", maxDist)
|
||||
|| nearestSleepPlaceOfType(worldRef, tarinai, "bed", maxDist)
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findNearestSleepPlace(worldRef, tarinai, maxDist = 520) {
|
||||
return findPrioritySleepPlaceBeforeGrassBedBuild(worldRef, tarinai, maxDist)
|
||||
|| findUnownedStructure(worldRef, tarinai, "grass_bed", maxDist)
|
||||
|| null;
|
||||
}
|
||||
|
||||
// Named separately so nestBoxCapacity() can detect and avoid recursive facade calls.
|
||||
function nestBoxCapacityFacade(box) { return nestBoxCapacity(this, box); }
|
||||
|
||||
global.TarinaiNestSleepSystem = Object.freeze({
|
||||
isSleepFurniture,
|
||||
isTarinaiHiddenInNestBox,
|
||||
nestBoxOccupants,
|
||||
nestBoxReservationCount,
|
||||
bedOccupancy,
|
||||
bedComfort,
|
||||
sleepPlaceAvailableFor,
|
||||
nearestSleepPlaceOfType,
|
||||
findOwnedStructure,
|
||||
findUnownedStructure,
|
||||
findPrioritySleepPlaceBeforeGrassBedBuild,
|
||||
findNearestSleepPlace,
|
||||
distXY: distXYFn,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Social, panic, fight, mate behavior runtime ticks.
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
|
|
@ -70,7 +41,7 @@
|
|||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
|
||||
if (this.world.relationNotice?.(avoider.id, champion.id, "tarinai-champion-avoid", 18)) {
|
||||
this.world.log(`${avoider.name}はたりない王者の${champion.name}にぶつかりそうになって避けた。`, "relation", { participants: [avoider, champion], eventType: "avoid_relationship", hiddenFromObservation: true, sound: false });
|
||||
this.world.log(`${avoider.name}はたりない王者の${champion.name}にぶつかりそうになって避けた。`, "relation", { participants: [avoider, champion], eventType: "avoid_relationship", sound: false });
|
||||
}
|
||||
}
|
||||
avoider.lastNeedShockBreaker = champion;
|
||||
|
|
|
|||
|
|
@ -1,247 +1,16 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/tarinai/legacy
|
||||
// Compatibility owner for the former monolithic Tarinai.prototype.update body.
|
||||
// The normal runtime path uses TarinaiUpdatePipeline concrete steps; this module
|
||||
// exists only as a stable fallback while update steps continue to be refined.
|
||||
// Compatibility shim. The former monolithic update body was removed so the
|
||||
// concrete TarinaiUpdatePipeline remains the single runtime path.
|
||||
(function (global) {
|
||||
function legacyUpdate(dt) {
|
||||
if (this.dead) return;
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
|
||||
const minute = dt / 60;
|
||||
this.age += dt;
|
||||
this.reproductionTimer -= dt;
|
||||
this.blink += dt;
|
||||
this.eatTimer = Math.max(0, this.eatTimer - dt);
|
||||
if (this.state === "eat" && this.eatTimer <= 0.02) {
|
||||
this.setActionState?.("idle", { target: null, reason: "\u98df\u3079\u7d42\u3048\u305f", sleeping: false, clearTarget: true });
|
||||
}
|
||||
this.eatCooldown = Math.max(0, this.eatCooldown - dt);
|
||||
this.foodReactTimer = Math.max(0, this.foodReactTimer - dt);
|
||||
this.surpriseTimer = Math.max(0, this.surpriseTimer - dt);
|
||||
this.fearTimer = Math.max(0, this.fearTimer - dt);
|
||||
this.intimidateTimer = Math.max(0, this.intimidateTimer - dt);
|
||||
this.intimidatedTimer = Math.max(0, this.intimidatedTimer - dt);
|
||||
this.blastSpinTimer = Math.max(0, (this.blastSpinTimer || 0) - dt);
|
||||
this.postBirthPeaceTimer = Math.max(0, (this.postBirthPeaceTimer || 0) - dt);
|
||||
this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt);
|
||||
this.stressBarTimer = Math.max(0, (this.stressBarTimer || 0) - dt);
|
||||
this.updateZunchiDisease(dt);
|
||||
this.updateSpecialDiseases(dt);
|
||||
if (this.updateItemEffects) this.updateItemEffects(dt);
|
||||
if (this.dead) return;
|
||||
this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt);
|
||||
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt);
|
||||
if (this.fightMochiTimer > 0.04) {
|
||||
applyNeedShock(this, { safety: dt * 4 });
|
||||
if (this.state !== "fight") this.fearTimer = Math.max(this.fearTimer || 0, 0.10);
|
||||
}
|
||||
if (this.intimidateTimer <= 0.04) this.intimidateTargetId = null;
|
||||
if (this.birthRitualTimer > 0) {
|
||||
const beforeBirthTimer = this.birthRitualTimer;
|
||||
this.birthRitualTimer = Math.max(0, this.birthRitualTimer - dt);
|
||||
if (beforeBirthTimer > 0 && this.birthRitualTimer <= 0.04 && this.birthRitualLeader) {
|
||||
const partner = this.world.liveTarinaiById?.(this.birthPartnerId);
|
||||
if (partner) this.world.finishBirthRitual(this, partner);
|
||||
this.birthRitualLeader = false;
|
||||
}
|
||||
}
|
||||
this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt);
|
||||
this.focusPulseTimer = Math.max(0, (this.focusPulseTimer || 0) - dt);
|
||||
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
|
||||
this.pinchThoughtTimer = Math.max(0, (this.pinchThoughtTimer || 0) - dt);
|
||||
this.sunbathCooldown = Math.max(0, (this.sunbathCooldown || 0) - dt);
|
||||
if (this.freezeSleepDiseaseMotion(dt)) {
|
||||
this.checkLife(dt);
|
||||
return;
|
||||
}
|
||||
this.applyCursorContactCare(dt);
|
||||
const colonyMoodId = this.world?.colonyMood?.id || "relaxed";
|
||||
if (colonyMoodId === "weaklings" && !this.isZunchiSlave) applyNeedRelief(this, { safety: -dt * 2, fulfill: -dt * 1 });
|
||||
if (colonyMoodId === "zunda_party" || colonyMoodId === "ball_party") applyNeedRelief(this, { fulfill: -dt * 3 });
|
||||
if (colonyMoodId === "ant_overrun" && this.state !== "panic") {
|
||||
const antSeen = (this.world?.nearbyAnts?.(this.x, this.y, 110, true) || []).some(a => a && !a.dead);
|
||||
if (antSeen) {
|
||||
if (this.enterPanic) this.enterPanic({ target: null, reason: "\u30a2\u30ea\u3060", fear: 1.1, wake: true, cause: "colony_ant_overrun" });
|
||||
else { this.setActionState?.("panic", { target: null, reason: "\u30a2\u30ea\u3060", wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 1.1 * this.personalityProfile().fear); }
|
||||
}
|
||||
}
|
||||
const wasFighting = this.fightTimer > 0.04;
|
||||
this.fightTimer = Math.max(0, this.fightTimer - dt);
|
||||
this.fightCooldown = Math.max(0, this.fightCooldown - dt);
|
||||
this.groundStumbleCooldown = Math.max(0, (this.groundStumbleCooldown || 0) - dt);
|
||||
this.defeatedTimer = Math.max(0, this.defeatedTimer - dt);
|
||||
if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) {
|
||||
const winner = this.world.liveTarinaiById?.(this.defeatedById) || null;
|
||||
this.defeatedTimer = Math.max(this.defeatedTimer, deterministicRange(this.world, "defeated-recover-timer", 3.4, 5.2, this));
|
||||
this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear);
|
||||
this.hurtTimer = Math.max(this.hurtTimer, 1.8);
|
||||
this.fallTimer = Math.max(this.fallTimer, 1.15);
|
||||
this.fallMax = Math.max(this.fallMax || 1.15, this.fallTimer);
|
||||
this.fallDir = winner ? (this.x < winner.x ? -1 : 1) : this.fallDir;
|
||||
this.world.spawnFallEffect(this.x, this.y + this.radius * 0.45);
|
||||
if (winner) this.world.resolveFightOutcome(winner, this);
|
||||
this.fightTargetIds = [];
|
||||
this.fightTargetId = null;
|
||||
}
|
||||
this.stretchTimer = Math.max(0, this.stretchTimer - dt);
|
||||
this.grassEatTimer = Math.max(0, this.grassEatTimer - dt);
|
||||
this.hurtTimer = Math.max(0, this.hurtTimer - dt);
|
||||
this.fallTimer = Math.max(0, this.fallTimer - dt);
|
||||
if ((this.world?.groundType || "soil") === "foot_massage"
|
||||
&& (this.groundStumbleCooldown || 0) <= 0.04
|
||||
&& (this.fallTimer || 0) <= 0.04
|
||||
&& !this.dead
|
||||
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(this.state || ""))
|
||||
&& deterministicChance(this.world, "foot-massage-stumble", dt * 0.055, this)) {
|
||||
const dir = deterministicSigned(this.world, "foot-massage-stumble-dir", this);
|
||||
this.groundStumbleCooldown = deterministicRange(this.world, "foot-massage-stumble-cooldown", 8.5, 16.0, this);
|
||||
this.fallTimer = Math.max(this.fallTimer || 0, deterministicRange(this.world, "foot-massage-stumble-fall", 0.86, 1.18, this));
|
||||
this.fallMax = Math.max(this.fallMax || 1.05, this.fallTimer);
|
||||
this.fallDir = dir;
|
||||
this.vx += dir * deterministicRange(this.world, "foot-massage-stumble-vx", 26, 52, this);
|
||||
this.vy += deterministicRange(this.world, "foot-massage-stumble-vy", -18, 18, this);
|
||||
this.world?.spawnFallEffect?.(this.x, this.y + this.radius * 0.45);
|
||||
this.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && deterministicChance(this.world, "stress-breath-bubble", dt * 0.050, this)) {
|
||||
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
if (this.state === "idle" && this.stress < 78 && deterministicChance(this.world, "idle-bubble", dt * 0.018, this)) {
|
||||
this.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
}
|
||||
if (this.hurtTimer > 0 && deterministicChance(this.world, "hurt-bleed-effect", dt * 5.2, this)) {
|
||||
const side = this.facingDir();
|
||||
this.world.spawnBleedEffect(this.x - side * this.radius * deterministicRange(this.world, "hurt-bleed-x", 0.15, 0.42, this), this.y - this.radius * deterministicRange(this.world, "hurt-bleed-y", 0.08, 0.34, this));
|
||||
}
|
||||
|
||||
this.tempTimer += dt;
|
||||
if (this.tempTimer >= 0.55) {
|
||||
this.tempComfort = this.world.temperatureAt(this.x, this.y);
|
||||
this.tempTimer = 0;
|
||||
}
|
||||
const tempComfort = this.tempComfort ?? 0.62;
|
||||
const tooCold = Math.max(0, 0.48 - tempComfort);
|
||||
const hungerScale = this.metabolicHungerMultiplier ? this.metabolicHungerMultiplier() : (typeof tarinaiMetabolicHungerScale === "function" ? tarinaiMetabolicHungerScale(this) : 1);
|
||||
this.hunger += CONFIG.hungerPerMinute * minute * this.trait.hunger * hungerScale * (1 + tooCold * 0.8);
|
||||
this.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - this.trait.social);
|
||||
this.energy -= CONFIG.energyPerMinute * minute * this.trait.sleep * (this.state === "sleep" ? 0.025 : 1);
|
||||
// stress is derived from needs; no independent stress decay.
|
||||
if (tooCold > 0) applyNeedShock(this, { health: tooCold * dt * 4, safety: tooCold * dt * 2 });
|
||||
|
||||
const phase = this.world.lightLevel();
|
||||
if (phase < 0.25) this.energy += dt * 0.07;
|
||||
if (phase > 0.75) applyNeedShock(this, { health: dt * 1.2 });
|
||||
if (this.world.weather === "cloudy") {
|
||||
applyNeedRelief(this, { safety: -dt * 1 });
|
||||
this.vx *= Math.pow(0.996, dt * 60);
|
||||
this.vy *= Math.pow(0.996, dt * 60);
|
||||
} else if (this.world.weather === "light_rain") {
|
||||
this.energy += dt * 0.018;
|
||||
applyNeedRelief(this, { health: -dt * 1, safety: -dt * 1 });
|
||||
} else if (this.world.weather === "sunny") {
|
||||
this.vx *= Math.pow(1.001, dt * 60);
|
||||
this.vy *= Math.pow(1.001, dt * 60);
|
||||
}
|
||||
|
||||
if (this.generation > 1 && this.syncGrowthScale) this.syncGrowthScale();
|
||||
|
||||
this.hunger = clamp(this.hunger, 0, 115);
|
||||
this.loneliness = clamp(this.loneliness, 0, 110);
|
||||
this.energy = clamp(this.energy, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100));
|
||||
this.stress = applyGroundStressModifier(this, calculateStressFromNeeds(this.needRaw || this.needs || createDefaultNeeds()));
|
||||
|
||||
const wasSleepingAtFrameStart = this.state === "sleep" || this.sleeping;
|
||||
this.awakeLockTimer = Math.max(0, (this.awakeLockTimer || 0) - dt);
|
||||
|
||||
if (this.freezeSleepDiseaseMotion(dt)) {
|
||||
this.checkLife(dt);
|
||||
return;
|
||||
}
|
||||
|
||||
this.behaviorLockTimer = Math.max(0, (this.behaviorLockTimer || 0) - dt);
|
||||
this.aiTimer += dt;
|
||||
const urgentFoodNeed = Number(this.needRaw?.food ?? this.needs?.food ?? 0) || 0;
|
||||
const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02 || this.hunger >= 96 || urgentFoodNeed >= 84;
|
||||
const aiJitter = stableUnit(this.familyKey || this.id, "ai-interval") * 0.35;
|
||||
const aiInterval = urgentAi ? 0.35 : (1.0 + aiJitter);
|
||||
if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) {
|
||||
const elapsedAiDt = Math.max(0.016, Math.min(this.aiTimer, urgentAi ? 0.75 : 1.50));
|
||||
const scanDt = Math.min(elapsedAiDt, 0.75);
|
||||
this.aiTimer = 0;
|
||||
// Needs intentionally advance by a fixed coarse step per AI update.
|
||||
// Exact elapsed-time integration is unnecessary for this observation game
|
||||
// and can over-amplify safety/fear transitions when updates are sparse.
|
||||
const needStepDt = urgentAi ? 0.50 : 1.00;
|
||||
this.resolveNeeds(needStepDt);
|
||||
const sleepingNow = this.state === "sleep";
|
||||
if (!sleepingNow) {
|
||||
this.reactToFreshZunchi(scanDt);
|
||||
this.updateFacing();
|
||||
this.interactWithItems(scanDt);
|
||||
this.interactWithOthers(scanDt);
|
||||
} else {
|
||||
this.updateFacing();
|
||||
}
|
||||
} else {
|
||||
this.updateFacing();
|
||||
}
|
||||
this.envTimer += dt;
|
||||
const envInterval = 3.0 + stableUnit(this.familyKey || this.id, "env-phase") * 1.4;
|
||||
if (this.envTimer >= envInterval && this.world.spendScheduledWork?.("env", urgentAi || this.world.selected === this) !== false) {
|
||||
const envDt = Math.min(this.envTimer, 1.2);
|
||||
this.envTimer = 0;
|
||||
this.applyEnvironment(envDt);
|
||||
}
|
||||
this.updateSunbath(dt);
|
||||
this.maintainTargetProgress(dt);
|
||||
this.move(dt);
|
||||
if (this.ammoCollisionKnockback) this.ammoCollisionKnockback(dt);
|
||||
this.updateNestBoxPresence(dt);
|
||||
if (this.world.resolveFenceCollision) this.world.resolveFenceCollision(this);
|
||||
const sleepingAtFrameEnd = this.state === "sleep" || this.sleeping;
|
||||
if (wasSleepingAtFrameStart && !sleepingAtFrameEnd && !this.dead) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 10);
|
||||
this.checkLife(dt);
|
||||
|
||||
return global.TarinaiUpdatePipeline?.updateOne?.(this, dt) || false;
|
||||
}
|
||||
|
||||
function updateOne(tarinai, dt) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
legacyUpdate.call(tarinai, dt);
|
||||
return true;
|
||||
return global.TarinaiUpdatePipeline?.updateOne?.(tarinai, dt) !== false;
|
||||
}
|
||||
|
||||
function updateWithThis(dt) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,23 @@
|
|||
return /idle|wander|approach|follow|seek|panic|fight|birth_ritual|sunbath|zunchi_sick|fight_sick|cursor_/i.test(id);
|
||||
}
|
||||
|
||||
function activeMechanicalObstacleNear(tarinai, worldRef = null) {
|
||||
if (!tarinai || !worldRef || worldRef.isTarinaiHiddenInNestBox?.(tarinai)) return false;
|
||||
const radius = Math.max(150, (Number(tarinai.radius) || 22) + 190);
|
||||
const source = worldRef.nearbyItems?.(tarinai.x || 0, tarinai.y || 0, radius, true) || worldRef.items || [];
|
||||
for (const it of source) {
|
||||
if (!it || it.dead || !global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type)) continue;
|
||||
const reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(64, it.r || 64);
|
||||
if (Math.hypot((tarinai.x || 0) - (it.x || 0), (tarinai.y || 0) - (it.y || 0)) > reach + radius) continue;
|
||||
if (it.type === "rotator" || it.type === "reciprocator") return true;
|
||||
if (global.TarinaiMechanicalSystem?.passiveItemAwake?.(it)) return true;
|
||||
const body = global.TarinaiPhysicsBodySystem?.ensureBody?.(it, worldRef, { syncFromLegacy: false });
|
||||
const vel = body?.velocity || {};
|
||||
if (Math.hypot(Number(vel.x || it.vx || 0) || 0, Number(vel.y || it.vy || 0) || 0) + Math.abs(Number(vel.angular || it.spin || 0) || 0) * 64 > 0.8) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasPassivePhysicsNeed(tarinai, worldRef = null, isVisible = false) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
const vx = Number.isFinite(tarinai.vx) ? tarinai.vx : 0;
|
||||
|
|
@ -36,8 +53,12 @@
|
|||
const ivx = Number.isFinite(tarinai.impulseVx) ? tarinai.impulseVx : 0;
|
||||
const ivy = Number.isFinite(tarinai.impulseVy) ? tarinai.impulseVy : 0;
|
||||
if (Math.hypot(vx + ivx, vy + ivy) > 0.35) return true;
|
||||
const sleeping = tarinai.state === "sleep" || tarinai.sleeping;
|
||||
const id = behaviorId(tarinai);
|
||||
const state = String(tarinai.state || id || "");
|
||||
const sleeping = state === "sleep" || tarinai.sleeping;
|
||||
const lockedPassive = sleeping || state === "eat" || /eat|sunbath|birth_ritual|breed|repro/i.test(id) || /sunbath|birth_ritual|breed|repro/.test(state) || (tarinai.birthRitualTimer || 0) > 0.04 || (tarinai.eatTimer || 0) > 0.04 || (tarinai.sunbathTimer || 0) > 0.04;
|
||||
if (sleeping && isVisible && !worldRef?.isTarinaiHiddenInNestBox?.(tarinai)) return true;
|
||||
if (lockedPassive && isVisible && activeMechanicalObstacleNear(tarinai, worldRef)) return true;
|
||||
if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && tarinai.target.type !== "nest_box") return true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -57,6 +78,9 @@
|
|||
const isNear = isVisible || Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||
const motionNeed = hasMotionNeed(tarinai);
|
||||
|
||||
const nestEntryActive = Boolean(tarinai.insideNestBoxId && (typeof tarinai.nestBoxEntryActive === "function" ? tarinai.nestBoxEntryActive() : ((worldRef.time || 0) < (tarinai._nestBoxEntrySmoothUntil || -Infinity) || ((tarinai.nestFade || 0) > 0.01 && (tarinai.nestFade || 0) < 0.985))));
|
||||
if (nestEntryActive) return { interval: 0, lane: "nest-entry", smoothMotion: false, visible: true, urgent: true };
|
||||
|
||||
const hardRealtimeUrgent =
|
||||
(tarinai.hurtTimer || 0) > 0.04 ||
|
||||
(tarinai.pokeFlashTimer || 0) > 0.03 ||
|
||||
|
|
@ -113,7 +137,7 @@
|
|||
&& interval <= 0.75
|
||||
&& !worldRef.isTarinaiHiddenInNestBox?.(tarinai)
|
||||
&& ((smoothAllowed && motionNeed) || sleepPhysical || eatPhysical));
|
||||
return { interval, lane, smoothMotion, visible: isVisible, urgent: false, sleepPhysical, passivePhysicsNeed };
|
||||
return { interval, lane, smoothMotion, visible: isVisible, urgent: false, sleepPhysical, eatPhysical, passivePhysicsNeed };
|
||||
}
|
||||
|
||||
function updateInterval(worldRef, tarinai, visibleRect) {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/tarinai/update-step
|
||||
|
|
@ -35,6 +6,24 @@
|
|||
// pressure, and early freeze exits. This is extracted from the former
|
||||
// Tarinai.prototype.update body without changing behavior.
|
||||
(function (global) {
|
||||
function detRange(worldRef, salt, min, max, ...parts) {
|
||||
const fn = global.deterministicRange;
|
||||
if (typeof fn === "function") return fn(worldRef, salt, min, max, ...parts);
|
||||
const a = Number(min) || 0;
|
||||
const b = Number(max) || 0;
|
||||
return (a + b) * 0.5;
|
||||
}
|
||||
|
||||
function detSigned(worldRef, salt, ...parts) {
|
||||
const fn = global.deterministicSigned;
|
||||
return typeof fn === "function" ? fn(worldRef, salt, ...parts) : 1;
|
||||
}
|
||||
|
||||
function detChance(worldRef, salt, chance, ...parts) {
|
||||
const fn = global.deterministicChance;
|
||||
return typeof fn === "function" ? fn(worldRef, salt, chance, ...parts) : (Number(chance) || 0) >= 1;
|
||||
}
|
||||
|
||||
function update(t, dt, context = {}) {
|
||||
if (!t || t.dead) return { done: true };
|
||||
t.prevX = t.x;
|
||||
|
|
@ -106,7 +95,7 @@
|
|||
t.defeatedTimer = Math.max(0, t.defeatedTimer - dt);
|
||||
if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) {
|
||||
const winner = t.world.liveTarinaiById?.(t.defeatedById) || null;
|
||||
t.defeatedTimer = Math.max(t.defeatedTimer, deterministicRange(t.world, "defeated-recover-timer", 3.4, 5.2, t));
|
||||
t.defeatedTimer = Math.max(t.defeatedTimer, detRange(t.world, "defeated-recover-timer", 3.4, 5.2, t));
|
||||
t.fearTimer = Math.max(t.fearTimer, 1.0 * t.personalityProfile().fear);
|
||||
t.hurtTimer = Math.max(t.hurtTimer, 1.8);
|
||||
t.fallTimer = Math.max(t.fallTimer, 1.15);
|
||||
|
|
@ -126,27 +115,27 @@
|
|||
&& (t.fallTimer || 0) <= 0.04
|
||||
&& !t.dead
|
||||
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(t.state || ""))
|
||||
&& deterministicChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
||||
const dir = deterministicSigned(t.world, "foot-massage-stumble-dir", t);
|
||||
t.groundStumbleCooldown = deterministicRange(t.world, "foot-massage-stumble-cooldown", 8.5, 16.0, t);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, deterministicRange(t.world, "foot-massage-stumble-fall", 0.86, 1.18, t));
|
||||
&& detChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
||||
const dir = detSigned(t.world, "foot-massage-stumble-dir", t);
|
||||
t.groundStumbleCooldown = detRange(t.world, "foot-massage-stumble-cooldown", 8.5, 16.0, t);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, detRange(t.world, "foot-massage-stumble-fall", 0.86, 1.18, t));
|
||||
t.fallMax = Math.max(t.fallMax || 1.05, t.fallTimer);
|
||||
t.fallDir = dir;
|
||||
t.vx += dir * deterministicRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
||||
t.vy += deterministicRange(t.world, "foot-massage-stumble-vy", -18, 18, t);
|
||||
t.vx += dir * detRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
||||
t.vy += detRange(t.world, "foot-massage-stumble-vy", -18, 18, t);
|
||||
t.world?.spawnFallEffect?.(t.x, t.y + t.radius * 0.45);
|
||||
t.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
|
||||
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && deterministicChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
|
||||
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
|
||||
t.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
if (t.state === "idle" && t.stress < 78 && deterministicChance(t.world, "idle-bubble", dt * 0.018, t)) {
|
||||
if (t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) {
|
||||
t.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
}
|
||||
if (t.hurtTimer > 0 && deterministicChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
|
||||
if (t.hurtTimer > 0 && detChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
|
||||
const side = t.facingDir();
|
||||
t.world.spawnBleedEffect(t.x - side * t.radius * deterministicRange(t.world, "hurt-bleed-x", 0.15, 0.42, t), t.y - t.radius * deterministicRange(t.world, "hurt-bleed-y", 0.08, 0.34, t));
|
||||
t.world.spawnBleedEffect(t.x - side * t.radius * detRange(t.world, "hurt-bleed-x", 0.15, 0.42, t), t.y - t.radius * detRange(t.world, "hurt-bleed-y", 0.08, 0.34, t));
|
||||
}
|
||||
|
||||
t.tempTimer += dt;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/tarinai/update-step
|
||||
// Fallback step for environments where the split update pipeline cannot run.
|
||||
// Compatibility shim for old step lists. Delegates to the concrete pipeline.
|
||||
(function (global) {
|
||||
function update(t, dt, context = {}) {
|
||||
const legacyUpdate = context.legacyUpdate;
|
||||
if (typeof legacyUpdate !== "function") return { done: true, ok: false };
|
||||
legacyUpdate.call(t, dt);
|
||||
return { done: true, ok: true };
|
||||
function update(t, dt) {
|
||||
const ok = global.TarinaiUpdatePipeline?.updateOne?.(t, dt);
|
||||
return { done: true, ok: ok !== false };
|
||||
}
|
||||
|
||||
global.TarinaiLegacyUpdateStep = Object.freeze({ update });
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@
|
|||
if (now - (t.lastVelocityShockDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false;
|
||||
if (now - (t.lastPhysicalCollisionDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false;
|
||||
const amount = Math.min(42, Math.max(1.0, (delta - SHOCK_DAMAGE_DELTA_THRESHOLD) / 14));
|
||||
const source = (now - Number(t.lastSolidObstacleCollisionAt || -999) < 0.75) ? (t.lastSolidObstacleCollisionSource || null) : null;
|
||||
t.lastVelocityShockDamageAt = now;
|
||||
t.world?.applyImpactDamage?.(t, amount, "\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316", { cause: "\u885d\u7a81" }) || t.damage?.(amount, "\u885d\u7a81");
|
||||
t.world?.applyImpactDamage?.(t, amount, "\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316", { cause: "\u885d\u7a81", source }) || t.damage?.(amount, "\u885d\u7a81");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, amount > 15 ? 1.9 : 0.9);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, amount > 12 ? 0.72 : 0.28);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer || 0);
|
||||
|
|
@ -178,7 +179,8 @@
|
|||
|
||||
const dx = numeric(t.x) - beforeX;
|
||||
const dy = numeric(t.y) - beforeY;
|
||||
if (world?.resolveSolidObstacleCollision && !world.isTarinaiHiddenInNestBox?.(t) && (state === "sleep" || sleepExternalMotionActive(t))) {
|
||||
const lockedPassiveContact = /^(sleep|eat|sunbath|birth_ritual|breed|reproduction)$/.test(state);
|
||||
if (world?.resolveSolidObstacleCollision && !world.isTarinaiHiddenInNestBox?.(t) && (lockedPassiveContact || sleepExternalMotionActive(t))) {
|
||||
// A moving mechanical item can hit a sleeping Tarinai while the Tarinai's
|
||||
// own velocity is zero. In that case swept movement will never run, so
|
||||
// do a small contact pass every passive-physics tick.
|
||||
|
|
@ -189,7 +191,7 @@
|
|||
searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120),
|
||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||
slop: 0.28,
|
||||
reason: "tarinai-sleep-passive-contact",
|
||||
reason: state === "eat" ? "tarinai-eat-passive-contact" : (state === "sleep" ? "tarinai-sleep-passive-contact" : "tarinai-locked-passive-contact"),
|
||||
});
|
||||
if (pushed) {
|
||||
t.lastSolidObstacleCollisionAt = world.time || 0;
|
||||
|
|
@ -197,7 +199,7 @@
|
|||
const cdx = numeric(t.x) - beforeContactX;
|
||||
const cdy = numeric(t.y) - beforeContactY;
|
||||
if (Math.hypot(cdx, cdy) > 0.05) {
|
||||
const pushCarry = state === "sleep" ? 0.085 : 0.040;
|
||||
const pushCarry = state === "sleep" ? 0.085 : (state === "eat" ? 0.070 : 0.040);
|
||||
t.vx = clamp(numeric(t.vx) + cdx / Math.max(0.016, dt) * pushCarry, -170, 170);
|
||||
t.vy = clamp(numeric(t.vy) + cdy / Math.max(0.016, dt) * pushCarry, -170, 170);
|
||||
}
|
||||
|
|
@ -224,7 +226,7 @@
|
|||
function update(t, dt, context = {}) {
|
||||
if (!t || t.dead) return { done: true };
|
||||
const motionDt = Math.max(0.001, Math.min(0.05, Number(context.motionDt ?? dt) || 0.016));
|
||||
if (context.motionOnly && /^(sleep|eat|intimidate|ant_intimidate)$/.test(String(t.state || ""))) {
|
||||
if (context.motionOnly && /^(sleep|eat|sunbath|birth_ritual|breed|reproduction|intimidate|ant_intimidate)$/.test(String(t.state || ""))) {
|
||||
return passivePhysicalMotion(t, motionDt, context);
|
||||
}
|
||||
const beforeX = Number.isFinite(t.x) ? t.x : 0;
|
||||
|
|
|
|||
3
js/ui.js
3
js/ui.js
|
|
@ -28,6 +28,7 @@ const ui = {
|
|||
logPushEnabled: document.getElementById("logPushEnabled"),
|
||||
logPushOverlay: document.getElementById("logPushOverlay"),
|
||||
mobileModeControls: document.getElementById("mobileModeControls"),
|
||||
mobileRotateControls: document.getElementById("mobileRotateControls"),
|
||||
backToGameBtn: document.getElementById("backToGameBtn"),
|
||||
soundCategoryControls: document.getElementById("soundPanel"),
|
||||
toast: document.getElementById("toast"),
|
||||
|
|
@ -109,8 +110,6 @@ const uiCache = {
|
|||
touchStartY: 0,
|
||||
touchLastX: 0,
|
||||
touchLastY: 0,
|
||||
touchLastWorldX: 0,
|
||||
touchLastWorldY: 0,
|
||||
pinchStartDistance: 0,
|
||||
pinchStartZoom: 1,
|
||||
pinchFocusWorld: null,
|
||||
|
|
|
|||
115
js/ui_bind.js
115
js/ui_bind.js
|
|
@ -14,7 +14,7 @@ function bindUI() {
|
|||
return Boolean(world?.tool && world.tool !== "observe");
|
||||
}
|
||||
function clickedInsideGameOrToolUi(target) {
|
||||
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target) || ui.panelQuickTabs?.contains?.(target) || ui.mobileModeControls?.contains?.(target) || ui.backToGameBtn?.contains?.(target));
|
||||
return Boolean((canvas && canvas.contains?.(target)) || ui.toolPalette?.contains?.(target) || ui.panelQuickTabs?.contains?.(target) || ui.mobileModeControls?.contains?.(target) || ui.mobileRotateControls?.contains?.(target) || ui.backToGameBtn?.contains?.(target));
|
||||
}
|
||||
|
||||
function panelSectionForTab(tab = "") {
|
||||
|
|
@ -37,6 +37,10 @@ function bindUI() {
|
|||
return Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
}
|
||||
|
||||
function isDesktopLayout() {
|
||||
return window.matchMedia?.("(min-width: 981px)")?.matches ?? window.innerWidth >= 981;
|
||||
}
|
||||
|
||||
function markCollapsibleCardState(card, collapsed, { user = false } = {}) {
|
||||
if (!card) return;
|
||||
card.classList.toggle("collapsed", Boolean(collapsed));
|
||||
|
|
@ -160,6 +164,15 @@ function bindUI() {
|
|||
markCollapsibleCardState(card, collapsed);
|
||||
}
|
||||
|
||||
function applyDesktopInitialPanelState() {
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classify?.().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
||||
const desktop = !touchFirst && isDesktopLayout();
|
||||
if (!desktop || uiCache.desktopInitialPanelStateApplied) return;
|
||||
uiCache.desktopInitialPanelStateApplied = true;
|
||||
for (const card of document.querySelectorAll(".collapsible-card")) markCollapsibleCardState(card, false);
|
||||
for (const category of ui.toolPalette?.querySelectorAll(".tool-category") || []) markToolCategoryState(category, false);
|
||||
}
|
||||
|
||||
function applyMobileInitialPanelState() {
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classify?.().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
||||
if (!touchFirst || uiCache.mobileInitialPanelStateApplied) return;
|
||||
|
|
@ -171,8 +184,9 @@ function bindUI() {
|
|||
markToolCategoryState(category, !keepOpen);
|
||||
}
|
||||
}
|
||||
applyDesktopInitialPanelState();
|
||||
applyMobileInitialPanelState();
|
||||
window.addEventListener("resize", applyMobileInitialPanelState, { passive: true });
|
||||
window.addEventListener("resize", () => { applyDesktopInitialPanelState(); applyMobileInitialPanelState(); }, { passive: true });
|
||||
|
||||
function updateBackToGameButton() {
|
||||
const btn = ui.backToGameBtn;
|
||||
|
|
@ -194,6 +208,15 @@ function bindUI() {
|
|||
updateBackToGameButton();
|
||||
|
||||
window.TarinaiEvents?.on?.("selection:changed", () => {
|
||||
if (world?._scrollSelectedDataTopOnNextSelection && isDesktopLayout() && world.selected && !world.selected.dead) {
|
||||
world._scrollSelectedDataTopOnNextSelection = false;
|
||||
window.setTimeout(() => {
|
||||
renderSelected?.();
|
||||
scrollPanelToTab("selected");
|
||||
}, 40);
|
||||
return;
|
||||
}
|
||||
world && (world._scrollSelectedDataTopOnNextSelection = false);
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const mode = window.TarinaiInputMode?.currentMode || uiCache.mobileInputMode || "auto";
|
||||
if (!touchFirst || mode !== "auto" || world.tool !== "observe" || !world.selected || world.selected.dead) return;
|
||||
|
|
@ -207,6 +230,39 @@ function bindUI() {
|
|||
const type = typeof toolItemType === "function" ? toolItemType(world?.tool || "") : "";
|
||||
return type && typeof isRotatableItemType === "function" && isRotatableItemType(type) ? type : "";
|
||||
}
|
||||
|
||||
function syncMobileRotateControls() {
|
||||
const controls = ui.mobileRotateControls;
|
||||
if (!controls) return;
|
||||
const show = isTouchMobileLayout()
|
||||
&& (window.TarinaiInputMode?.currentMode || uiCache.mobileInputMode || "auto") !== "camera"
|
||||
&& Boolean(activeRotatableToolType());
|
||||
controls.hidden = !show;
|
||||
}
|
||||
|
||||
window.syncMobileRotateControls = syncMobileRotateControls;
|
||||
|
||||
function rotateMobileTool(direction = "right") {
|
||||
const type = activeRotatableToolType();
|
||||
if (!type || !world?.rotateToolAngle) return false;
|
||||
const step = 45 * Math.PI / 180;
|
||||
world.rotateToolAngle(type, direction === "left" ? -step : step);
|
||||
audio.uiClick?.();
|
||||
render();
|
||||
syncMobileRotateControls();
|
||||
return true;
|
||||
}
|
||||
|
||||
ui.mobileRotateControls?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest?.("[data-mobile-rotate]");
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
rotateMobileTool(btn.dataset.mobileRotate || "right");
|
||||
});
|
||||
window.TarinaiEvents?.on?.("tool:selected", syncMobileRotateControls);
|
||||
window.addEventListener("resize", syncMobileRotateControls, { passive: true });
|
||||
syncMobileRotateControls();
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
const tag = String(e.target?.tagName || "").toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
|
||||
|
|
@ -218,14 +274,17 @@ function bindUI() {
|
|||
e.preventDefault();
|
||||
world.rotateToolAngle(type, -step);
|
||||
render();
|
||||
syncMobileRotateControls();
|
||||
} else if (key === "e" || key === "]") {
|
||||
e.preventDefault();
|
||||
world.rotateToolAngle(type, step);
|
||||
render();
|
||||
syncMobileRotateControls();
|
||||
} else if (key === "r") {
|
||||
e.preventDefault();
|
||||
world.setToolAngle?.(type, typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
|
||||
render();
|
||||
syncMobileRotateControls();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -456,37 +515,61 @@ function bindUI() {
|
|||
renderStats();
|
||||
return;
|
||||
}
|
||||
if (tool === "clear_tool") {
|
||||
world.pendingLinkEndpoint = null;
|
||||
world.copyBuffer = null;
|
||||
selectTool("observe");
|
||||
for (const b of uiCache.toolButtons || []) b.classList.remove("selected");
|
||||
showToast("道具選択を解除しました。");
|
||||
return;
|
||||
}
|
||||
if (world.tool === tool && cycleToolSize(tool)) return;
|
||||
selectTool(tool);
|
||||
});
|
||||
|
||||
const normalizedWheelDeltaY = (e) => {
|
||||
const line = 18;
|
||||
const page = Math.max(160, window.innerHeight * 0.82);
|
||||
return e.deltaY * (e.deltaMode === 1 ? line : (e.deltaMode === 2 ? page : 1));
|
||||
};
|
||||
const applyScrollDelta = (el, dy) => {
|
||||
if (!el || !Number.isFinite(dy) || Math.abs(dy) < 0.001) return dy;
|
||||
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||
const before = clamp(el.scrollTop || 0, 0, max);
|
||||
const next = clamp(before + dy, 0, max);
|
||||
el.scrollTop = next;
|
||||
return dy - (next - before);
|
||||
};
|
||||
const chainScroll = (el) => {
|
||||
if (!el) return;
|
||||
el.addEventListener("wheel", (e) => {
|
||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||
const atTop = el.scrollTop <= 0;
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
||||
if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
||||
const page = document.scrollingElement || document.documentElement;
|
||||
page.scrollTop += e.deltaY;
|
||||
}
|
||||
}, { passive: true });
|
||||
const dy = normalizedWheelDeltaY(e);
|
||||
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||
const atTop = el.scrollTop <= 0.5;
|
||||
const atBottom = el.scrollTop >= max - 0.5;
|
||||
if (!((dy < 0 && atTop) || (dy > 0 && atBottom))) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyScrollDelta(document.scrollingElement || document.documentElement, dy);
|
||||
}, { passive: false });
|
||||
};
|
||||
const transferEdgeScroll = (child, parent) => {
|
||||
if (!child || !parent) return;
|
||||
child.addEventListener("wheel", (e) => {
|
||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||
const dy = normalizedWheelDeltaY(e);
|
||||
const childCanScroll = child.scrollHeight > child.clientHeight + 1;
|
||||
const atTop = child.scrollTop <= 0;
|
||||
const atBottom = child.scrollTop + child.clientHeight >= child.scrollHeight - 1;
|
||||
const movingInside = childCanScroll && ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom));
|
||||
const childMax = Math.max(0, child.scrollHeight - child.clientHeight);
|
||||
const movingInside = childCanScroll && ((dy < 0 && child.scrollTop > 0.5) || (dy > 0 && child.scrollTop < childMax - 0.5));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (movingInside) {
|
||||
child.scrollTop += e.deltaY;
|
||||
return;
|
||||
let rest = dy;
|
||||
if (movingInside) rest = applyScrollDelta(child, dy);
|
||||
if (Math.abs(rest) > 0.001) {
|
||||
const parentRest = applyScrollDelta(parent, rest);
|
||||
if (Math.abs(parentRest) > 0.001) applyScrollDelta(document.scrollingElement || document.documentElement, parentRest);
|
||||
}
|
||||
parent.scrollTop += e.deltaY;
|
||||
}, { passive: false });
|
||||
};
|
||||
const rightPanel = document.querySelector(".panel");
|
||||
|
|
|
|||
|
|
@ -170,9 +170,9 @@ function chartSeries(mode = "population") {
|
|||
];
|
||||
if (mode === "objects") return [
|
||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f5a2f" },
|
||||
{ key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" },
|
||||
{ key: "obj_grass_bed", label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", color: "#4f9bc4" },
|
||||
{ key: "obj_grass_bed", label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", color: "#78ff45" },
|
||||
{ key: "obj_water", label: "\u96e8\u6ef4", color: "#5aa8d6" },
|
||||
];
|
||||
return [{ key: "pop", label: "\u500b\u4f53", color: "#5d8ee6" }];
|
||||
|
|
|
|||
|
|
@ -35,17 +35,17 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
const live = entity && !entity.dead ? entity : null;
|
||||
const img = n ? tarinaiSpritePathForId(n.id) : SPRITES[0].path;
|
||||
const name = escapeHtml(n?.name || "\u4e0d\u660e");
|
||||
const status = familyStatusText(n);
|
||||
const gen = n?.generation ? `G${n.generation}` : "G?";
|
||||
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
||||
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
||||
const status = n?.alive ? familyStatusText(n) : (briefReason || "死亡");
|
||||
const reason = briefReason ? ` / ${briefReason}` : "";
|
||||
const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []);
|
||||
const tags = rawTags.slice();
|
||||
const displayTags = tags.slice(0, 4);
|
||||
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 deathLine = "";
|
||||
const tagLine = displayTags.length ? `<span class="family-personality-tags">${displayTags.map(tag => `<em>${escapeHtml(tag)}</em>`).join("")}</span>` : "";
|
||||
const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion);
|
||||
const statLine = n ? `<span class="family-genetic-stats" title="寿命 / 体格 / 攻撃 / 速度">
|
||||
|
|
@ -385,10 +385,10 @@ function lineageBuildRowGroups(row, idSet) {
|
|||
function lineageLayoutRows(rows, idSet) {
|
||||
const NODE_W = 150;
|
||||
const NODE_H = 104;
|
||||
const SIBLING_GAP = 32;
|
||||
const GROUP_GAP = 52;
|
||||
const PARTNER_GROUP_GAP = 24;
|
||||
const ROW_GAP = 108;
|
||||
const SIBLING_GAP = 24;
|
||||
const GROUP_GAP = 42;
|
||||
const PARTNER_GROUP_GAP = 18;
|
||||
const ROW_GAP = 96;
|
||||
const LEFT_PAD = 82;
|
||||
const TOP_PAD = 44;
|
||||
const RIGHT_PAD = 48;
|
||||
|
|
@ -447,7 +447,7 @@ function lineageLayoutRows(rows, idSet) {
|
|||
}
|
||||
|
||||
const lefts = [];
|
||||
const SOFT_EXTRA_GAP = 68;
|
||||
const SOFT_EXTRA_GAP = 54;
|
||||
for (let gIndex = 0; gIndex < groups.length; gIndex++) {
|
||||
const prevGroup = groups[gIndex - 1];
|
||||
const pairGap = prevGroup && prevGroup.items.length === 1 && groups[gIndex].items.length === 1
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ function lineageFastLayoutNeeded(nodes, idSet, family) {
|
|||
function lineageLayoutRowsFast(rows, idSet) {
|
||||
const NODE_W = 150;
|
||||
const NODE_H = 104;
|
||||
const SIBLING_GAP = 22;
|
||||
const ROW_GAP = 92;
|
||||
const SIBLING_GAP = 18;
|
||||
const ROW_GAP = 84;
|
||||
const LEFT_PAD = 82;
|
||||
const TOP_PAD = 44;
|
||||
const RIGHT_PAD = 48;
|
||||
|
|
|
|||
|
|
@ -4,45 +4,17 @@
|
|||
function bindMouseInput(ctx) {
|
||||
if (!ctx?.canvas) return false;
|
||||
const { canvas, world, uiCache } = ctx;
|
||||
const actions = global.TarinaiPointerActionSystem?.create?.(ctx);
|
||||
|
||||
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
canvas.addEventListener("mousedown", (e) => {
|
||||
if (e.button === 0 && world.tool === "area_delete") {
|
||||
if (e.button === 0) {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (!p.inside) return;
|
||||
e.preventDefault();
|
||||
ctx.beginAreaDelete(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
if (e.button === 0 && world.tool === "water_hose") {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (!p.inside) return;
|
||||
e.preventDefault();
|
||||
ctx.beginHose(p, e.clientX, e.clientY);
|
||||
if (uiCache.hoseHoldTimer) clearInterval(uiCache.hoseHoldTimer);
|
||||
uiCache.hoseHoldTimer = setInterval(() => {
|
||||
if (!uiCache.hosing) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; return; }
|
||||
const cx = uiCache.hoseLastClientX || e.clientX;
|
||||
const cy = uiCache.hoseLastClientY || e.clientY;
|
||||
const hp = ctx.screenToWorldClient(cx, cy);
|
||||
ctx.moveHose(hp, cx, cy);
|
||||
}, 90);
|
||||
return;
|
||||
}
|
||||
if (e.button === 0 && world.tool === "shoot") {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (!p.inside) return;
|
||||
e.preventDefault();
|
||||
ctx.beginShoot(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
if (e.button === 0 && world.tool === "pinch") {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (!p.inside) return;
|
||||
if (!ctx.beginGrab(p, e.clientX, e.clientY, { notifyNoTarget: true })) return;
|
||||
e.preventDefault();
|
||||
canvas.style.cursor = "grabbing";
|
||||
return;
|
||||
const started = actions?.beginToolAt?.(p, e.clientX, e.clientY, { notifyNoTarget: world.tool === "pinch" });
|
||||
if (started) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.button !== 2) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -50,64 +22,20 @@
|
|||
});
|
||||
|
||||
window.addEventListener("mouseup", (e) => {
|
||||
if (ctx.endAreaDelete()) return;
|
||||
if (ctx.endHose()) { if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } return; }
|
||||
if (ctx.endShoot()) return;
|
||||
if (ctx.endGrab(e.clientX, e.clientY)) return;
|
||||
if (actions?.endActive?.(e.clientX, e.clientY)) return;
|
||||
ctx.endPan(e.button);
|
||||
});
|
||||
|
||||
window.addEventListener("mousemove", (e) => {
|
||||
if (uiCache.areaDeleting) {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
ctx.moveAreaDelete(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
if (uiCache.hosing) {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
ctx.moveHose(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
if (uiCache.shooting) {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
ctx.moveShoot(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
if (uiCache.grabbing) {
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY));
|
||||
if (totalMove > 3) uiCache.grabMoved = true;
|
||||
ctx.moveGrab(p, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (actions?.moveActive?.(p, e.clientX, e.clientY)) return;
|
||||
ctx.movePan(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener("click", (e) => {
|
||||
if (uiCache.areaDeleteMoved) {
|
||||
uiCache.areaDeleteMoved = false;
|
||||
return;
|
||||
}
|
||||
if (uiCache.hoseMoved) {
|
||||
uiCache.hoseMoved = false;
|
||||
return;
|
||||
}
|
||||
if (uiCache.grabMoved) {
|
||||
uiCache.grabMoved = false;
|
||||
return;
|
||||
}
|
||||
if (uiCache.shootSuppressClick) {
|
||||
uiCache.shootSuppressClick = false;
|
||||
return;
|
||||
}
|
||||
if (uiCache.panMoved) {
|
||||
uiCache.panMoved = false;
|
||||
return;
|
||||
}
|
||||
if (actions?.shouldSuppressClick?.()) return;
|
||||
const p = ctx.screenToWorldEvent(e);
|
||||
if (!p.inside) return;
|
||||
window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
||||
renderStats();
|
||||
actions?.dispatchTapAt?.(p);
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", (e) => ctx.updatePointerFromEvent(e));
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
document.body.classList.toggle("mobile-mode-auto", mode === "auto");
|
||||
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
||||
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
||||
global.syncMobileRotateControls?.();
|
||||
return mode;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
||||
document.body.classList.toggle("touch-mobile-ui", enabled);
|
||||
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
||||
global.syncMobileRotateControls?.();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +189,9 @@
|
|||
rememberGrabOrigin(target);
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
uiCache.grabVelocityX = 0;
|
||||
uiCache.grabVelocityY = 0;
|
||||
uiCache.grabLastMoveAt = performance?.now?.() ?? Date.now();
|
||||
uiCache.grabStartX = clientX || 0;
|
||||
uiCache.grabStartY = clientY || 0;
|
||||
target.playerHeld = true;
|
||||
|
|
@ -215,6 +220,11 @@
|
|||
}
|
||||
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
||||
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
||||
const moveNow = performance?.now?.() ?? Date.now();
|
||||
const moveDt = Math.max(0.012, Math.min(0.18, (moveNow - (uiCache.grabLastMoveAt || moveNow)) / 1000));
|
||||
uiCache.grabLastMoveAt = moveNow;
|
||||
uiCache.grabVelocityX = clamp(dx / moveDt, -820, 820);
|
||||
uiCache.grabVelocityY = clamp(dy / moveDt, -820, 820);
|
||||
uiCache.grabLastWorldX = p.x;
|
||||
uiCache.grabLastWorldY = p.y;
|
||||
const beforeX = Number(target.x || 0) || 0;
|
||||
|
|
@ -238,8 +248,8 @@
|
|||
target.prevY = target.y;
|
||||
}
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-move");
|
||||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
target.vx = clamp(uiCache.grabVelocityX || dx * 16, -820, 820);
|
||||
target.vy = clamp(uiCache.grabVelocityY || dy * 16, -820, 820);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||
if (options.touch) target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||
|
|
@ -268,9 +278,21 @@
|
|||
target.targetKey = "";
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
||||
finishTarinaiGrab(target, clientX, clientY);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
} else if (uiCache.grabKind === "item") {
|
||||
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
|
||||
const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900);
|
||||
const releaseSpeed = Math.hypot(releaseVx, releaseVy);
|
||||
if (releaseSpeed > 18) {
|
||||
target.vx = releaseVx;
|
||||
target.vy = releaseVy;
|
||||
const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60));
|
||||
target.prevX = target.x - releaseVx * frameDt;
|
||||
target.prevY = target.y - releaseVy * frameDt;
|
||||
if (target.type === "ball") target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42);
|
||||
} else if (target.type === "ball") {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
}
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release");
|
||||
target.playerHeld = false;
|
||||
|
|
@ -286,6 +308,9 @@
|
|||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
uiCache.grabVelocityX = 0;
|
||||
uiCache.grabVelocityY = 0;
|
||||
uiCache.grabLastMoveAt = 0;
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = 0;
|
||||
canvas.style.cursor = cursorForTool(world.tool);
|
||||
|
|
@ -571,6 +596,9 @@
|
|||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
uiCache.grabVelocityX = 0;
|
||||
uiCache.grabVelocityY = 0;
|
||||
uiCache.grabLastMoveAt = 0;
|
||||
if (world) {
|
||||
world.hoverGrabTarget = null;
|
||||
world.hoverDeleteTarget = null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
function bindTouchInput(ctx) {
|
||||
if (!ctx?.canvas) return false;
|
||||
const { canvas, world, uiCache } = ctx;
|
||||
const actions = global.TarinaiPointerActionSystem?.create?.(ctx);
|
||||
ctx.bindMobileModeControls();
|
||||
const isMobileAutoScrollMode = () => {
|
||||
const mode = ctx.mobileInputMode?.() || "auto";
|
||||
|
|
@ -41,33 +42,13 @@
|
|||
uiCache.touchStartY = t.clientY;
|
||||
uiCache.touchLastX = t.clientX;
|
||||
uiCache.touchLastY = t.clientY;
|
||||
uiCache.touchLastWorldX = p.x;
|
||||
uiCache.touchLastWorldY = p.y;
|
||||
if (mode === "tool" && world.tool === "observe") {
|
||||
uiCache.touchMode = "toolTapOnly";
|
||||
return;
|
||||
}
|
||||
if (world.tool === "shoot" && p.inside) {
|
||||
ctx.beginShoot(p, t.clientX, t.clientY, { touchMode: "shoot" });
|
||||
return;
|
||||
}
|
||||
if (world.tool === "area_delete" && p.inside) {
|
||||
ctx.beginAreaDelete(p, t.clientX, t.clientY, { touchMode: "areaDelete" });
|
||||
return;
|
||||
}
|
||||
if (world.tool === "water_hose" && p.inside) {
|
||||
ctx.beginHose(p, t.clientX, t.clientY, { touchMode: "hose" });
|
||||
if (uiCache.hoseHoldTimer) clearInterval(uiCache.hoseHoldTimer);
|
||||
uiCache.hoseHoldTimer = setInterval(() => {
|
||||
if (!uiCache.hosing) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; return; }
|
||||
const cx = uiCache.hoseLastClientX || uiCache.touchLastX || t.clientX;
|
||||
const cy = uiCache.hoseLastClientY || uiCache.touchLastY || t.clientY;
|
||||
const hp = ctx.screenToWorldClient(cx, cy);
|
||||
ctx.moveHose(hp, cx, cy, { touch: true });
|
||||
}, 90);
|
||||
return;
|
||||
}
|
||||
if (world.tool === "pinch" && p.inside && ctx.beginGrab(p, t.clientX, t.clientY, { touchMode: "grab" })) return;
|
||||
const touchModeByTool = { shoot: "shoot", area_delete: "areaDelete", water_hose: "hose", pinch: "grab" };
|
||||
const touchMode = touchModeByTool[world.tool];
|
||||
if (touchMode && actions?.beginToolAt?.(p, t.clientX, t.clientY, { touch: true, touchMode })) return;
|
||||
uiCache.touchMode = autoPageScroll ? "autoTapOrScroll" : (mode === "tool" ? "toolTapOnly" : "panOrTap");
|
||||
}, { passive: false });
|
||||
|
||||
|
|
@ -105,23 +86,7 @@
|
|||
if (totalMove > 8) uiCache.touchMoved = true;
|
||||
return;
|
||||
}
|
||||
if (uiCache.touchMode === "shoot" && uiCache.shooting) {
|
||||
ctx.moveShoot(p, t.clientX, t.clientY, { touch: true });
|
||||
return;
|
||||
}
|
||||
if (uiCache.touchMode === "areaDelete" && uiCache.areaDeleting) {
|
||||
ctx.moveAreaDelete(p, t.clientX, t.clientY, { touch: true });
|
||||
return;
|
||||
}
|
||||
if (uiCache.touchMode === "hose" && uiCache.hosing) {
|
||||
ctx.moveHose(p, t.clientX, t.clientY, { touch: true });
|
||||
return;
|
||||
}
|
||||
if (uiCache.touchMode === "grab" && uiCache.grabbing) {
|
||||
if (totalMove > 3) { uiCache.grabMoved = true; uiCache.touchMoved = true; }
|
||||
ctx.moveGrab(p, t.clientX, t.clientY, { touch: true, renderFrame: true });
|
||||
return;
|
||||
}
|
||||
if (actions?.moveActive?.(p, t.clientX, t.clientY, { touch: true, renderFrame: uiCache.touchMode === "grab" })) return;
|
||||
if (uiCache.touchMode === "autoTapOrScroll") {
|
||||
if (totalMove > 7) uiCache.touchMoved = true;
|
||||
return;
|
||||
|
|
@ -137,30 +102,15 @@
|
|||
|
||||
canvas.addEventListener("touchend", (e) => {
|
||||
if (uiCache.touchMode !== "autoTapOrScroll") e.preventDefault();
|
||||
if (uiCache.areaDeleting) ctx.endAreaDelete();
|
||||
if (uiCache.hosing) { ctx.endHose(); if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } }
|
||||
if (uiCache.shooting) ctx.endShoot();
|
||||
if (uiCache.grabbing) {
|
||||
ctx.endGrab(e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0);
|
||||
if (actions?.endActive?.(e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0)) {
|
||||
// Active tool ended; click suppression is handled through movement flags.
|
||||
} else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) {
|
||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
if (p.inside) {
|
||||
window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
||||
renderStats();
|
||||
}
|
||||
actions?.dispatchTapClient?.(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
} else if (uiCache.touchMode === "autoTapOrScroll" && !uiCache.touchMoved) {
|
||||
e.preventDefault();
|
||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
if (p.inside) {
|
||||
window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
||||
renderStats();
|
||||
}
|
||||
actions?.dispatchTapClient?.(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
} else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) {
|
||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
if (p.inside) {
|
||||
window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
||||
renderStats();
|
||||
}
|
||||
actions?.dispatchTapClient?.(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
||||
}
|
||||
if (!e.touches?.length) {
|
||||
uiCache.touchMode = "";
|
||||
|
|
@ -168,7 +118,7 @@
|
|||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener("touchcancel", () => ctx.cancelPointerActions(), { passive: true });
|
||||
canvas.addEventListener("touchcancel", () => actions?.cancel?.(), { passive: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
21
js/ui_log.js
21
js/ui_log.js
|
|
@ -35,7 +35,7 @@ function syncAudioControls() {
|
|||
}
|
||||
if (ui.soundMasterToggle) ui.soundMasterToggle.checked = Boolean(audio.enabled);
|
||||
if (ui.soundBtn) {
|
||||
ui.soundBtn.textContent = audio.enabled ? "\u97f3\u8a2d\u5b9a ON" : "\u97f3\u8a2d\u5b9a OFF";
|
||||
ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF";
|
||||
ui.soundBtn.classList.toggle("active", Boolean(audio.enabled));
|
||||
ui.soundBtn.setAttribute("aria-expanded", ui.soundPanel && !ui.soundPanel.classList.contains("hidden") ? "true" : "false");
|
||||
}
|
||||
|
|
@ -57,8 +57,8 @@ function pushLogNotification(entry = {}) {
|
|||
const kind = entry.kind || "note";
|
||||
if (!uiCache.logPushKinds?.has(kind)) return;
|
||||
const overlay = ui.logPushOverlay;
|
||||
audio.notify?.();
|
||||
if (!overlay) return;
|
||||
audio.notify?.();
|
||||
const card = document.createElement("div");
|
||||
card.className = `log-push-toast log-push-${kind}`;
|
||||
const tag = document.createElement("span");
|
||||
|
|
@ -86,11 +86,6 @@ window.pushLogNotification = pushLogNotification;
|
|||
function shouldShowInEventLog(entry = {}) {
|
||||
if (!entry) return false;
|
||||
if (entry.eventType === "player_item_placement") return false;
|
||||
const text = String(entry.text || "");
|
||||
const kind = entry.kind || "";
|
||||
// Older saves may contain direct placement records without eventType.
|
||||
// Hide only participant-less player-like placement lines, preserving births, accidents, fights, and natural events.
|
||||
if ((kind === "observe" || kind === "note") && !(entry.participants || []).length && /(?:を配置した。|を落とした。)$/.test(text)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -99,13 +94,11 @@ function renderLog(logs) {
|
|||
const frag = document.createDocumentFragment();
|
||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const limit = touchFirst ? 34 : 80;
|
||||
const visibleLogs = Array.isArray(logs) ? logs.filter(raw => {
|
||||
const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw;
|
||||
return shouldShowInEventLog(entry);
|
||||
}) : [];
|
||||
const visibleLogs = Array.isArray(logs)
|
||||
? logs.map(raw => typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw).filter(shouldShowInEventLog)
|
||||
: [];
|
||||
const rows = visibleLogs.slice(0, limit);
|
||||
for (const raw of rows) {
|
||||
const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw;
|
||||
for (const entry of rows) {
|
||||
const kind = entry.kind || "note";
|
||||
const div = document.createElement("div");
|
||||
div.className = `log-entry log-${kind}`;
|
||||
|
|
@ -136,7 +129,7 @@ function formatLogTime(timeValue) {
|
|||
function logKindLabel(kind) {
|
||||
return {
|
||||
birth: "\u8a95\u751f", death: "\u6b7b\u4ea1", fight: "\u55a7\u5629", food: "\u98df\u4e8b", grass: "\u8349",
|
||||
weather: "\u74b0\u5883", relation: "\u95a2\u4fc2", accident: "\u4e8b\u6545", observe: "\u89b3\u5bdf", note: "\u8a18\u9332"
|
||||
weather: "\u74b0\u5883", relation: "\u95a2\u4fc2", accident: "\u4e8b\u6545", observe: "\u89b3\u5bdf", event: "\u51fa\u6765\u4e8b", note: "\u8a18\u9332"
|
||||
}[kind] || "\u8a18\u9332";
|
||||
}
|
||||
|
||||
|
|
|
|||
122
js/ui_pointer_action_system.js
Normal file
122
js/ui_pointer_action_system.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"use strict";
|
||||
|
||||
// Shared pointer action router for mouse and touch. Keeps long-hold timers,
|
||||
// drag movement thresholds, and tap suppression consistent across input types.
|
||||
(function (global) {
|
||||
const DRAG_THRESHOLD = 3;
|
||||
|
||||
function createPointerActionManager(ctx) {
|
||||
if (!ctx?.canvas || !ctx?.world || !ctx?.uiCache) return null;
|
||||
const { canvas, world, uiCache } = ctx;
|
||||
|
||||
function clearHoseHoldTimer() {
|
||||
if (uiCache.hoseHoldTimer) {
|
||||
clearInterval(uiCache.hoseHoldTimer);
|
||||
uiCache.hoseHoldTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureHoseHoldTimer(clientX, clientY, options = {}) {
|
||||
clearHoseHoldTimer();
|
||||
uiCache.hoseHoldTimer = setInterval(() => {
|
||||
if (!uiCache.hosing) {
|
||||
clearHoseHoldTimer();
|
||||
return;
|
||||
}
|
||||
const cx = uiCache.hoseLastClientX || uiCache.touchLastX || clientX;
|
||||
const cy = uiCache.hoseLastClientY || uiCache.touchLastY || clientY;
|
||||
const hp = ctx.screenToWorldClient(cx, cy);
|
||||
ctx.moveHose(hp, cx, cy, { touch: Boolean(options.touch) });
|
||||
}, 90);
|
||||
}
|
||||
|
||||
function beginToolAt(p, clientX, clientY, options = {}) {
|
||||
if (!p?.inside) return false;
|
||||
const tool = String(world.tool || "");
|
||||
if (tool === "area_delete") {
|
||||
return Boolean(ctx.beginAreaDelete(p, clientX, clientY, options));
|
||||
}
|
||||
if (tool === "water_hose") {
|
||||
const started = Boolean(ctx.beginHose(p, clientX, clientY, options));
|
||||
if (started) ensureHoseHoldTimer(clientX, clientY, options);
|
||||
return started;
|
||||
}
|
||||
if (tool === "shoot") {
|
||||
return Boolean(ctx.beginShoot(p, clientX, clientY, options));
|
||||
}
|
||||
if (tool === "pinch") {
|
||||
const started = Boolean(ctx.beginGrab(p, clientX, clientY, options));
|
||||
if (started) canvas.style.cursor = "grabbing";
|
||||
return started;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function moveActive(p, clientX, clientY, options = {}) {
|
||||
if (uiCache.areaDeleting) return Boolean(ctx.moveAreaDelete(p, clientX, clientY, options) || true);
|
||||
if (uiCache.hosing) return Boolean(ctx.moveHose(p, clientX, clientY, options) || true);
|
||||
if (uiCache.shooting) return Boolean(ctx.moveShoot(p, clientX, clientY, options));
|
||||
if (uiCache.grabbing) {
|
||||
const sx = uiCache.grabStartX || clientX;
|
||||
const sy = uiCache.grabStartY || clientY;
|
||||
if (Math.hypot(clientX - sx, clientY - sy) > DRAG_THRESHOLD) {
|
||||
uiCache.grabMoved = true;
|
||||
if (options.touch) uiCache.touchMoved = true;
|
||||
}
|
||||
return Boolean(ctx.moveGrab(p, clientX, clientY, options));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function endActive(clientX = 0, clientY = 0) {
|
||||
if (ctx.endAreaDelete()) return true;
|
||||
if (ctx.endHose()) {
|
||||
clearHoseHoldTimer();
|
||||
return true;
|
||||
}
|
||||
if (ctx.endShoot()) return true;
|
||||
if (ctx.endGrab(clientX, clientY)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldSuppressClick() {
|
||||
const flags = ["areaDeleteMoved", "hoseMoved", "grabMoved", "shootSuppressClick", "panMoved"];
|
||||
for (const key of flags) {
|
||||
if (uiCache[key]) {
|
||||
uiCache[key] = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function dispatchTapAt(p) {
|
||||
if (!p?.inside) return false;
|
||||
global.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
||||
global.renderStats?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
function dispatchTapClient(clientX, clientY) {
|
||||
return dispatchTapAt(ctx.screenToWorldClient(clientX, clientY));
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
clearHoseHoldTimer();
|
||||
ctx.cancelPointerActions?.();
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
beginToolAt,
|
||||
moveActive,
|
||||
endActive,
|
||||
shouldSuppressClick,
|
||||
dispatchTapAt,
|
||||
dispatchTapClient,
|
||||
cancel,
|
||||
clearHoseHoldTimer,
|
||||
});
|
||||
}
|
||||
|
||||
global.TarinaiPointerActionSystem = Object.freeze({ create: createPointerActionManager });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const SCALABLE_TOOLS = new Set(scalableToolIds());
|
||||
const EMOJI_ICON_TOOLS = new Set(["poke", "pinch", "water_hose", "rope", "rod", "reciprocator", "copy", "undo", "redo", "area_delete"]);
|
||||
const EMOJI_ICON_TOOLS = new Set(["poke", "pinch", "water_hose", "rope", "rod", "reciprocator", "copy", "undo", "redo", "area_delete", "clear_tool"]);
|
||||
|
||||
const SIZE_ORDER = typeof TOOL_SIZE_ORDER !== "undefined" ? TOOL_SIZE_ORDER : ["small", "medium", "large"];
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ function renderToolPalette() {
|
|||
const section = document.createElement("section");
|
||||
section.className = `tool-category ${category.themeClass || ""}`.trim();
|
||||
section.dataset.toolCategory = category.id;
|
||||
const mobileCompact = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||
const mobileCompact = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
||||
const initiallyCollapsed = mobileCompact && category.id !== "operate";
|
||||
if (initiallyCollapsed) section.classList.add("collapsed");
|
||||
const toggle = document.createElement("button");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.15.47";
|
||||
const APP_BUILD = "tarinai-event-family-oshibyo-fix-v39-15-45";
|
||||
const APP_VERSION = "39.15.63";
|
||||
const APP_BUILD = "tarinai-medium-unification-v39-15-60";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +1,9 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_combat_effects.js");
|
||||
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = 165;
|
||||
function physicalDamageFromImpactSpeed(speed = 0, { min = 0, max = 58, scale = 8.6 } = {}) {
|
||||
const v = Math.max(0, Number(speed) || 0);
|
||||
if (v < PHYSICAL_DAMAGE_SPEED_THRESHOLD) return 0;
|
||||
const raw = (v - PHYSICAL_DAMAGE_SPEED_THRESHOLD) / Math.max(1, scale);
|
||||
return clamp(raw, min, max);
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
damageAntsInRadius(x, y, radius, damageFn, reason = "", opts = {}) {
|
||||
let hit = 0;
|
||||
|
|
@ -77,6 +40,39 @@
|
|||
return hit;
|
||||
},
|
||||
|
||||
|
||||
blastLoosePinsFrom(x, y, blastRadius, source = null, scale = 1, salt = "blast") {
|
||||
let blastedPins = 0;
|
||||
const pinSystem = globalThis.TarinaiItemDynamicPinSystem;
|
||||
const pins = (this.items || []).filter(it => it && !it.dead && typeof isPinType === "function" && isPinType(it.type));
|
||||
for (const pin of pins) {
|
||||
if (pin.pinState === "lodged") continue;
|
||||
let dx = pin.x - x;
|
||||
let dy = pin.y - y;
|
||||
let d = Math.hypot(dx, dy) || 1;
|
||||
if (d > blastRadius * 1.18) continue;
|
||||
const p = clamp(1 - d / (blastRadius * 1.18), 0, 1);
|
||||
if (d < 0.001) {
|
||||
const a = deterministicAngle(this, `${salt}-pin-overlap`, source || pin, pin);
|
||||
dx = Math.cos(a);
|
||||
dy = Math.sin(a);
|
||||
d = 1;
|
||||
}
|
||||
if (pin.pinState === "ball_lodged") pinSystem?.detach?.(pin, this, "blast");
|
||||
const blast = (360 + p * 820) * (Number(scale) || 1);
|
||||
this.applyImpulse(pin,
|
||||
dx / d * blast + deterministicRange(this, `${salt}-pin-jitter-x`, -70, 70, source || pin, pin),
|
||||
dy / d * blast + deterministicRange(this, `${salt}-pin-jitter-y`, -70, 70, source || pin, pin)
|
||||
);
|
||||
pin.spinVelocity = clamp((pin.spinVelocity || 0) + deterministicRange(this, `${salt}-pin-spin`, -18, 18, source || pin, pin), -38, 38);
|
||||
pin.noStickUntil = (this.time || 0) + 0.22;
|
||||
pin.lastPokedAt = this.time || 0;
|
||||
this.effects.push(new Effect("ring", pin.x, pin.y, { size: Math.max(12, (pin.r || 8) * 1.8), life: 0.18, color: "rgba(255,230,116,0.46)" }));
|
||||
blastedPins += 1;
|
||||
}
|
||||
return blastedPins;
|
||||
},
|
||||
|
||||
explodeFirecracker(it) {
|
||||
if (!it || it.dead || it.type !== "firecracker") return;
|
||||
it.amount = 0;
|
||||
|
|
@ -164,14 +160,15 @@
|
|||
const blast = 430 + p * 960;
|
||||
this.applyImpulse(ball, dx / d * blast + deterministicRange(this, "firecracker-ball-jitter-x", -80, 80, it, ball), dy / d * blast + deterministicRange(this, "firecracker-ball-jitter-y", -80, 80, it, ball));
|
||||
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "disease-explosion-ball-spin", -24, 24, t, ball), -38, 38);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "firecracker-ball-spin", -24, 24, it, ball), -38, 38);
|
||||
ball.lastPokedAt = this.time || 0;
|
||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
||||
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)" }));
|
||||
blastedBalls += 1;
|
||||
}
|
||||
const blastedPins = this.blastLoosePinsFrom(x, y, blastRadius, it, it.blastScale || 1, "firecracker");
|
||||
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\u8fba\u308a\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident");
|
||||
this.log((blastedBalls || blastedPins) ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u8efd\u3044\u7269\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) {
|
||||
|
|
@ -257,13 +254,18 @@
|
|||
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)" }));
|
||||
}
|
||||
this.blastLoosePinsFrom(x, y, blastRadius, t, blastScale, "disease-explosion");
|
||||
t.die("\u7206\u767a\u75c5");
|
||||
audio.explode();
|
||||
this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident", { participants: [t] });
|
||||
},
|
||||
|
||||
waterHoseRadius() {
|
||||
return 138;
|
||||
},
|
||||
|
||||
applyWaterHose(x, y, dx = 0, dy = 0, dt = 0.08) {
|
||||
const radius = 276;
|
||||
const radius = this.waterHoseRadius?.() || 138;
|
||||
const speed = Math.hypot(dx, dy) || 1;
|
||||
const nx = speed > 1 ? dx / speed : Math.cos(this.time * 9.1);
|
||||
const ny = speed > 1 ? dy / speed : Math.sin(this.time * 7.7);
|
||||
|
|
@ -279,7 +281,13 @@
|
|||
} else if (it.type === "grass") {
|
||||
it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 135 * (0.30 + p));
|
||||
it.growth = Math.max(0, (Number(it.growth) || 0) - dt * 0.50 * (0.20 + p));
|
||||
if (it.amount <= 0.5) { it.amount = 0; }
|
||||
if (it.amount <= 0.5) {
|
||||
it.amount = 0;
|
||||
it.hp = 0;
|
||||
it.growth = 0;
|
||||
it.grassStage = -1;
|
||||
it.removedReason = "water-hose-grass";
|
||||
}
|
||||
cleaned += p * 0.72;
|
||||
} else if (it.type === "grass_bed") {
|
||||
it.amount = 0;
|
||||
|
|
@ -315,6 +323,7 @@
|
|||
this.markItemBucketsDirty?.("water-hose-clean");
|
||||
this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean");
|
||||
this.markSpatialDirty?.("water-hose-clean");
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts();
|
||||
}
|
||||
},
|
||||
|
|
@ -412,31 +421,19 @@
|
|||
},
|
||||
|
||||
isSleepFurniture(it) {
|
||||
return Boolean(it && (it.roles?.sleepPlace || (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(it.type) : (it.type === "bed" || it.type === "nest_box"))));
|
||||
return Boolean(globalThis.TarinaiNestSleepSystem?.isSleepFurniture?.(it));
|
||||
},
|
||||
|
||||
isTarinaiHiddenInNestBox(t) {
|
||||
return Boolean(t && !t.dead && t.insideNestBoxId);
|
||||
return Boolean(globalThis.TarinaiNestSleepSystem?.isTarinaiHiddenInNestBox?.(this, t));
|
||||
},
|
||||
|
||||
bedOccupancy(bed) {
|
||||
if (!this.isSleepFurniture(bed)) return 0;
|
||||
if (bed.type === "nest_box") {
|
||||
return this.nestBoxOccupants ? this.nestBoxOccupants(bed, Infinity).length : 0;
|
||||
}
|
||||
let n = 0;
|
||||
const radius = Math.max(86, bed.r * 2.45);
|
||||
for (const t of this.nearbyTarinai(bed.x, bed.y, radius)) {
|
||||
if (t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
if (t.state === "sleep" || t.state === "seek_bed" || t.target === bed) n += 1;
|
||||
}
|
||||
return n;
|
||||
return globalThis.TarinaiNestSleepSystem?.bedOccupancy?.(this, bed) || 0;
|
||||
},
|
||||
|
||||
bedComfort(bed) {
|
||||
if (!this.isSleepFurniture(bed)) return 0.7;
|
||||
const occ = this.bedOccupancy(bed);
|
||||
return clamp((bed.comfort ?? 1) - Math.max(0, occ - 3) * 0.075 - (bed.wear || 0) * 0.18, 0.42, 1.18);
|
||||
return globalThis.TarinaiNestSleepSystem?.bedComfort?.(this, bed) ?? 0.7;
|
||||
},
|
||||
relationNotice(aId, bId, kind, cooldown = 30) {
|
||||
const key = [aId, bId].sort().join(":") + `:${kind}`;
|
||||
|
|
@ -478,9 +475,10 @@
|
|||
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
||||
const sourceLabel = (src) => {
|
||||
if (!src) return "";
|
||||
if (src.name) return String(src.name);
|
||||
if (src instanceof Tarinai || (src.name && src.radius && !src.type)) return `「${String(src.name || "たりない")}」`;
|
||||
if (src.type && typeof toolLabel === "function") return toolLabel(src.type);
|
||||
if (src.type) return String(src.type);
|
||||
if (src.name) return String(src.name);
|
||||
return "";
|
||||
};
|
||||
let normalized = options.cause || cause || "\u885d\u7a81";
|
||||
|
|
@ -606,74 +604,7 @@
|
|||
},
|
||||
|
||||
itemDropImpact(it) {
|
||||
if (!it || it.dead || it.type === "water") return;
|
||||
const isStone = it.type === "stone";
|
||||
const isGenkotsu = it.type === "genkotsu";
|
||||
const dropFallHeight = isGenkotsu ? Math.max(360, (it.r || 88) * 5.8) : 170;
|
||||
const dropDuration = Math.max(0.08, Number(it.dropMax || 0.42) || 0.42);
|
||||
const impactSpeed = dropFallHeight / dropDuration;
|
||||
const radius = Math.max(32, Math.min(220, (it.r || 14) * 1.35 + impactSpeed * 0.055));
|
||||
const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 });
|
||||
const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260);
|
||||
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u306b\u6f70\u3055\u308c\u305f" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"));
|
||||
if (isGenkotsu) { this.lastGenkotsuAt = this.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
|
||||
let hit = 0;
|
||||
for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
|
||||
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
||||
if (p <= 0) continue;
|
||||
let nx = (t.x - it.x) / d;
|
||||
let ny = (t.y - it.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
t.vx += nx * push * (0.35 + p);
|
||||
t.vy += ny * push * (0.35 + p) - Math.min(210, push * 0.22) * p;
|
||||
const dealtDamage = damage * (0.35 + p * 0.65);
|
||||
if (dealtDamage > 0) t.damage(dealtDamage, reason);
|
||||
if (isGenkotsu) {
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: 18 + p * 30, health: 8 + p * 15 });
|
||||
t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu.");
|
||||
t.thought = "\u3052\u3093\u3053\u3064\u304c\u3053\u308f\u3044";
|
||||
} else {
|
||||
t.thought = isStone ? "\u77f3\u304c\u843d\u3061\u3066\u304d\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b";
|
||||
}
|
||||
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));
|
||||
t.fallMax = Math.max(t.fallMax || t.fallTimer, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
hit += 1;
|
||||
}
|
||||
if (damage > 0 && typeof damageNearbyStructures === "function") damageNearbyStructures(this, it.x, it.y, radius, damage, it);
|
||||
const antHit = damage > 0 ? this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
|
||||
push: p => push * (0.62 + p * 1.25),
|
||||
}) : 0;
|
||||
if (isGenkotsu) {
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
|
||||
const existingEffects = (this.effects || []).length;
|
||||
const ringCount = existingEffects > 180 ? 2 : 3;
|
||||
const burstCount = existingEffects > 180 ? 4 : 7;
|
||||
for (let r = 0; r < ringCount; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
|
||||
for (let i = 0; i < burstCount; i++) this.effects.push(new Effect("fight", it.x + deterministicRange(this, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(this, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(this, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(this, "genkotsu-impact-effect-life", 0.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
|
||||
} else {
|
||||
this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" }));
|
||||
}
|
||||
if (hit > 0 || antHit > 0 || isGenkotsu) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
|
||||
if (isGenkotsu) audio.genkotsuImpact?.();
|
||||
else if (isStone) audio.stoneImpact?.();
|
||||
else if (hit > 0 || antHit > 0) audio.poke?.();
|
||||
else audio.drop?.();
|
||||
if (hit > 0 || antHit > 0) {
|
||||
const label = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3" : "\u9053\u5177");
|
||||
const victims = [];
|
||||
if (hit > 0) victims.push(`${hit}\u5339\u306e\u305f\u308a\u306a\u3044`);
|
||||
if (antHit > 0) victims.push(`${antHit}\u5339\u306e\u30a2\u30ea`);
|
||||
this.log(`${label}\u304c\u843d\u3061\u3001${victims.join("\u3068")}\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident");
|
||||
}
|
||||
return global.TarinaiImpactResponseSystem?.itemDropImpact?.(this, it);
|
||||
},
|
||||
limitBallChasers() {
|
||||
if (!this.itemCounts?.ball) return;
|
||||
|
|
@ -725,327 +656,20 @@
|
|||
ball.amount = Math.max(ball.amount || 999, 999);
|
||||
audio.ballHit?.();
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: "rgba(255,255,255,0.58)" }));
|
||||
this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke", hiddenFromObservation: true });
|
||||
this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke" });
|
||||
return true;
|
||||
},
|
||||
|
||||
resolveBallBallCollisions(dt) {
|
||||
if ((this.itemCounts?.ball || 0) < 2) return;
|
||||
const now = this.time || 0;
|
||||
if (!this.ballCollisionMemo) this.ballCollisionMemo = new Map();
|
||||
const balls = this.itemsOfType?.("ball") || (this.items || []).filter(it => it && !it.dead && it.type === "ball");
|
||||
for (const a of balls) {
|
||||
if (!a || a.dead || a.type !== "ball") continue;
|
||||
const ar = a.r || 18;
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
const aSpeed = Math.hypot(avx, avy);
|
||||
const range = ar * 2 + aSpeed * Math.max(0.016, dt || 0.016) + 52;
|
||||
for (const b of this.nearbyItems(a.x, a.y, range) || []) {
|
||||
if (!b || b === a || b.dead || b.type !== "ball") continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (now - (this.ballCollisionMemo.get(key) || -999) < 0.055) continue;
|
||||
const br = b.r || 18;
|
||||
const hitRadius = ar + br;
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
let hitX = a.x;
|
||||
let hitY = a.y;
|
||||
if (d > hitRadius) {
|
||||
const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x;
|
||||
const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y;
|
||||
const rvx = (a.x - ax0) - (b.x - bx0);
|
||||
const rvy = (a.y - ay0) - (b.y - by0);
|
||||
const rx = ax0 - bx0;
|
||||
const ry = ay0 - by0;
|
||||
const aa = rvx * rvx + rvy * rvy;
|
||||
if (aa <= 0.0001) continue;
|
||||
const bb = 2 * (rx * rvx + ry * rvy);
|
||||
const cc = rx * rx + ry * ry - hitRadius * hitRadius;
|
||||
const disc = bb * bb - 4 * aa * cc;
|
||||
if (disc < 0) continue;
|
||||
const u = clamp((-bb - Math.sqrt(disc)) / (2 * aa), 0, 1);
|
||||
const axHit = ax0 + (a.x - ax0) * u;
|
||||
const ayHit = ay0 + (a.y - ay0) * u;
|
||||
const bxHit = bx0 + (b.x - bx0) * u;
|
||||
const byHit = by0 + (b.y - by0) * u;
|
||||
dx = bxHit - axHit;
|
||||
dy = byHit - ayHit;
|
||||
d = Math.hypot(dx, dy);
|
||||
hitX = (axHit + bxHit) * 0.5;
|
||||
hitY = (ayHit + byHit) * 0.5;
|
||||
if (d > hitRadius + 0.5) continue;
|
||||
} else {
|
||||
hitX = (a.x + b.x) * 0.5;
|
||||
hitY = (a.y + b.y) * 0.5;
|
||||
}
|
||||
if (d < 0.001) {
|
||||
const rvx = (b.vx || 0) - (a.vx || 0);
|
||||
const rvy = (b.vy || 0) - (a.vy || 0);
|
||||
const rs = Math.hypot(rvx, rvy);
|
||||
if (rs > 0.001) { dx = rvx / rs; dy = rvy / rs; d = 1; }
|
||||
else { const ang = deterministicAngle(this, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
|
||||
}
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
const rvx = (b.vx || 0) - (a.vx || 0);
|
||||
const rvy = (b.vy || 0) - (a.vy || 0);
|
||||
const relNormal = rvx * nx + rvy * ny;
|
||||
if (relNormal > 0 && d >= hitRadius - 0.5) continue;
|
||||
this.ballCollisionMemo.set(key, now);
|
||||
const restitution = 0.86;
|
||||
const impulse = -(1 + restitution) * relNormal / 2;
|
||||
if (Number.isFinite(impulse)) {
|
||||
a.vx = (a.vx || 0) - impulse * nx;
|
||||
a.vy = (a.vy || 0) - impulse * ny;
|
||||
b.vx = (b.vx || 0) + impulse * nx;
|
||||
b.vy = (b.vy || 0) + impulse * ny;
|
||||
}
|
||||
const overlap = Math.max(0, hitRadius - d + 0.6);
|
||||
if (overlap > 0) {
|
||||
a.x -= nx * overlap * 0.5;
|
||||
a.y -= ny * overlap * 0.5;
|
||||
b.x += nx * overlap * 0.5;
|
||||
b.y += ny * overlap * 0.5;
|
||||
}
|
||||
const tangent = nx * ((a.vy || 0) - (b.vy || 0)) - ny * ((a.vx || 0) - (b.vx || 0));
|
||||
a.spinVelocity = clamp((a.spinVelocity || 0) - tangent / Math.max(14, ar) * 1.8, -38, 38);
|
||||
b.spinVelocity = clamp((b.spinVelocity || 0) + tangent / Math.max(14, br) * 1.8, -38, 38);
|
||||
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(13, hitRadius * 0.38), life: 0.16, color: "rgba(255,255,255,0.44)" }));
|
||||
}
|
||||
}
|
||||
return global.TarinaiCollisionResponseSystem?.resolveBallBallCollisions?.(this, dt);
|
||||
},
|
||||
|
||||
resolveBallInteractions(dt) {
|
||||
if (!this.itemCounts?.ball) return;
|
||||
for (const ball of (this.itemsOfType ? this.itemsOfType("ball") : this.items)) {
|
||||
if (!ball || ball.dead || ball.type !== "ball") continue;
|
||||
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
const sweep = ballSpeed * Math.max(0.016, dt || 0.016) + 84;
|
||||
const range = (ball.r || 18) + 72 + sweep;
|
||||
const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x;
|
||||
const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y;
|
||||
const sx = ball.x - px;
|
||||
const sy = ball.y - py;
|
||||
const segLenSq = sx * sx + sy * sy;
|
||||
for (const t of this.nearbyTarinai(ball.x, ball.y, range)) {
|
||||
if (!t || t.dead || t.state === "sleep" || this.isTarinaiHiddenInNestBox(t)) continue;
|
||||
let hitX = ball.x;
|
||||
let hitY = ball.y;
|
||||
if (segLenSq > 1) {
|
||||
const u = clamp(((t.x - px) * sx + (t.y - py) * sy) / segLenSq, 0, 1);
|
||||
hitX = px + sx * u;
|
||||
hitY = py + sy * u;
|
||||
}
|
||||
let d = Math.max(0.001, distXY(hitX, hitY, t.x, t.y));
|
||||
const hitDistance = (ball.r || 18) + t.radius * 0.74;
|
||||
if (d > hitDistance) continue;
|
||||
const now = this.time || 0;
|
||||
const repeat = ball.lastKickerId === t.id && now - (ball.lastKickedAt || -999) < 0.18;
|
||||
if (repeat && ballSpeed < 300) continue;
|
||||
let nx = (hitX - t.x) / d;
|
||||
let ny = (hitY - t.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
if (ballSpeed > 0.01) {
|
||||
nx = (ball.vx || 0) / ballSpeed;
|
||||
ny = (ball.vy || 0) / ballSpeed;
|
||||
} else {
|
||||
nx = t.facingDir ? t.facingDir() : 1;
|
||||
ny = deterministicRange(this, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t);
|
||||
}
|
||||
}
|
||||
|
||||
const isDangerous = ballSpeed >= 285;
|
||||
if (isDangerous && now - (t.lastBallDamageAt || -999) > 0.46) {
|
||||
const bvx = ballSpeed > 0.01 ? (ball.vx || 0) / ballSpeed : nx;
|
||||
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
|
||||
const damage = physicalDamageFromImpactSpeed(ballSpeed, { min: 3.5, max: 38, scale: 18 });
|
||||
t.lastBallDamageAt = now;
|
||||
this.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
|
||||
panic: true,
|
||||
target: { x: ball.x, y: ball.y },
|
||||
fearTimer: 0.95,
|
||||
thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
});
|
||||
this.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball });
|
||||
const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0);
|
||||
const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18);
|
||||
if (t.addStress) t.addStress(stressGain, { threshold: 8 });
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" });
|
||||
else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35);
|
||||
t.fallTimer = Math.max(t.fallTimer, damage > 14 ? 0.98 : 0.46);
|
||||
t.fallMax = Math.max(t.fallMax || 0.98, t.fallTimer);
|
||||
t.fallDir = bvx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer, 0.95);
|
||||
t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, damage > 14 ? 1.0 : 0.44);
|
||||
t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer);
|
||||
this.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8));
|
||||
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" }));
|
||||
if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] });
|
||||
ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10);
|
||||
ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
|
||||
continue;
|
||||
}
|
||||
|
||||
const relVx = (t.vx || 0) - (ball.vx || 0);
|
||||
const relVy = (t.vy || 0) - (ball.vy || 0);
|
||||
const relativeSpeed = Math.hypot(relVx, relVy);
|
||||
const playIntent = t.state === "play_ball" || t.target === ball;
|
||||
const playful = t.shouldApplyPersonalityBehavior?.("openness", 1);
|
||||
const impulse = clamp(48 + relativeSpeed * 0.72 + (playIntent ? 52 : 0) + (playful ? 24 : 0), 48, isDangerous ? 360 : 260);
|
||||
ball.vx = (ball.vx || 0) * (isDangerous ? 0.78 : 0.92) + nx * impulse + (t.vx || 0) * 0.52;
|
||||
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
|
||||
const kickedSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38);
|
||||
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
t.vx -= nx * Math.min(22, impulse * 0.10);
|
||||
t.vy -= ny * Math.min(22, impulse * 0.10);
|
||||
t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
||||
const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20;
|
||||
const sensitiveBump = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0;
|
||||
if (typeof applyNeedShock === "function" && sensitiveBump > playRelief) applyNeedShock(t, { safety: (sensitiveBump - playRelief) * 4 }); else if (typeof applyNeedRelief === "function") applyNeedRelief(t, { fulfill: -playRelief * 6, social: -playRelief * 2 });
|
||||
if (playful && playIntent) {
|
||||
t.loneliness = clamp(t.loneliness - 0.9, 0, 100);
|
||||
t.affection = clamp(t.affection + 0.18, 0, 100);
|
||||
}
|
||||
t.goodMode = playIntent || playful ? "smile" : t.goodMode;
|
||||
if (playIntent && now > (t.nextPlayBubbleAt || 0)) {
|
||||
t.nextPlayBubbleAt = now + deterministicRange(this, "ball-play-bubble-delay", 2.0, 3.8, ball, t);
|
||||
this.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)");
|
||||
}
|
||||
if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] });
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" }));
|
||||
}
|
||||
}
|
||||
return global.TarinaiCollisionResponseSystem?.resolveBallInteractions?.(this, dt);
|
||||
},
|
||||
|
||||
resolveTarinaiHighSpeedCollisions(dt) {
|
||||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
||||
const now = this.time || 0;
|
||||
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
|
||||
const live = Array.from(this.tarinai || [])
|
||||
.filter(t => t && !t.dead && !this.isTarinaiHiddenInNestBox(t))
|
||||
.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||
const candidates = [];
|
||||
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
|
||||
for (let i = 0; i < live.length; i += 1) {
|
||||
const a = live[i];
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
const aSpeed = Math.hypot(avx, avy);
|
||||
if (aSpeed < threshold) continue;
|
||||
const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const ax1 = Number.isFinite(a.x) ? a.x : ax0;
|
||||
const ay1 = Number.isFinite(a.y) ? a.y : ay0;
|
||||
const asx = ax1 - ax0;
|
||||
const asy = ay1 - ay0;
|
||||
for (let j = 0; j < live.length; j += 1) {
|
||||
const b = live[j];
|
||||
if (!b || b === a) continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
const bvx = b.vx || 0;
|
||||
const bvy = b.vy || 0;
|
||||
const relVx = avx - bvx;
|
||||
const relVy = avy - bvy;
|
||||
const relSpeed = Math.hypot(relVx, relVy);
|
||||
if (relSpeed < threshold) continue;
|
||||
const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x;
|
||||
const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y;
|
||||
const bx1 = Number.isFinite(b.x) ? b.x : bx0;
|
||||
const by1 = Number.isFinite(b.y) ? b.y : by0;
|
||||
const bsx = bx1 - bx0;
|
||||
const bsy = by1 - by0;
|
||||
const rx0 = ax0 - bx0;
|
||||
const ry0 = ay0 - by0;
|
||||
const rvx = asx - bsx;
|
||||
const rvy = asy - bsy;
|
||||
const relMoveSq = rvx * rvx + rvy * rvy;
|
||||
const u = relMoveSq > 0.001 ? clamp(-(rx0 * rvx + ry0 * rvy) / relMoveSq, 0, 1) : 1;
|
||||
const cx = rx0 + rvx * u;
|
||||
const cy = ry0 + rvy * u;
|
||||
const d = Math.max(0.001, Math.hypot(cx, cy));
|
||||
const hitDistance = (a.radius || 22) * 0.74 + (b.radius || 22) * 0.74;
|
||||
if (d > hitDistance) continue;
|
||||
const score = (1 - u) * 4 + Math.max(0, hitDistance - d) * 0.12 + Math.min(5, relSpeed / 120);
|
||||
candidates.push({ a, b, key, relSpeed, u, cx, cy, d, hitDistance, score });
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
candidates.sort((p, q) => (p.u - q.u) || (q.score - p.score) || String(p.key).localeCompare(String(q.key)));
|
||||
const maxImpacts = Math.max(8, Math.min(64, Number(this.maxTarinaiCollisionImpacts || 32) || 32));
|
||||
let solved = 0;
|
||||
for (const c of candidates) {
|
||||
if (solved >= maxImpacts) break;
|
||||
const { a, b, key, relSpeed, hitDistance } = c;
|
||||
if (!a || !b || a.dead || b.dead) continue;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
this.tarinaiCollisionMemo.set(key, now);
|
||||
solved += 1;
|
||||
const as = Math.hypot(a.vx || 0, a.vy || 0);
|
||||
const bs = Math.hypot(b.vx || 0, b.vy || 0);
|
||||
const impactor = as >= bs ? a : b;
|
||||
const target = impactor === a ? b : a;
|
||||
const ivx = impactor.vx || 0;
|
||||
const ivy = impactor.vy || 0;
|
||||
const impactSpeed = Math.hypot(ivx, ivy) || relSpeed || 1;
|
||||
let nx = impactSpeed > 0.001 ? ivx / impactSpeed : 0;
|
||||
let ny = impactSpeed > 0.001 ? ivy / impactSpeed : 0;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const dx = target.x - impactor.x;
|
||||
const dy = target.y - impactor.y;
|
||||
const dd = Math.hypot(dx, dy);
|
||||
if (dd > 0.001) { nx = dx / dd; ny = dy / dd; }
|
||||
else {
|
||||
const angle = (typeof stableUnit === "function" ? stableUnit(key, "tarinai-collision-normal") : 0.5) * Math.PI * 2;
|
||||
nx = Math.cos(angle);
|
||||
ny = Math.sin(angle);
|
||||
}
|
||||
}
|
||||
const overlap = Math.max(0, hitDistance - Math.hypot((a.x || 0) - (b.x || 0), (a.y || 0) - (b.y || 0)));
|
||||
if (overlap > 0.5) {
|
||||
const sep = Math.min(18, overlap * 0.45 + 0.8);
|
||||
impactor.x = clamp((impactor.x || 0) - nx * sep * 0.45, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
impactor.y = clamp((impactor.y || 0) - ny * sep * 0.45, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
target.x = clamp((target.x || 0) + nx * sep * 0.55, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
target.y = clamp((target.y || 0) + ny * sep * 0.55, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
this.markSpatialDirty?.("tarinai-highspeed-separation");
|
||||
}
|
||||
const damage = physicalDamageFromImpactSpeed(relSpeed, { min: 1.2, max: 22, scale: 26 });
|
||||
this.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
|
||||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b",
|
||||
});
|
||||
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
|
||||
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
|
||||
this.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor });
|
||||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target });
|
||||
for (const t of [target, impactor]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
|
||||
}
|
||||
this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" }));
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] });
|
||||
}
|
||||
if (solved && this.spatialDirty) this.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
|
||||
return global.TarinaiCollisionResponseSystem?.resolveTarinaiHighSpeedCollisions?.(this, dt);
|
||||
}
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
let reach = Math.max(32, Number(it.r || it.radius || 32) || 32);
|
||||
if (type === "fence" || type === "fence_v" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") reach = Math.max(120, reach * 3.9);
|
||||
else if (type === "nest_box") reach = Math.max(92, reach * 1.9);
|
||||
else reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(60, reach * 1.6);
|
||||
else reach = global.TarinaiMechanicalShapeSystem?.reach?.(it) || Math.max(60, reach * 1.6);
|
||||
return Math.max(0, distXY(x, y, Number(it.x) || 0, Number(it.y) || 0) - reach);
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +189,8 @@
|
|||
}
|
||||
|
||||
function rectLocalPoint(r, x, y) {
|
||||
const shared = global.TarinaiCollisionFootprints?.rectLocalPoint;
|
||||
if (typeof shared === "function") return shared(r, x, y);
|
||||
const dx = x - (r.cx ?? 0);
|
||||
const dy = y - (r.cy ?? 0);
|
||||
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
|
||||
|
|
@ -427,25 +429,25 @@
|
|||
},
|
||||
|
||||
rotatorWorldSegments(it) {
|
||||
return global.TarinaiMechanicalSystem?.worldSegments?.(it) || rotatorWorldSegmentsLocal(it);
|
||||
return global.TarinaiMechanicalShapeSystem?.worldSegments?.(it) || rotatorWorldSegmentsLocal(it);
|
||||
},
|
||||
|
||||
rotatorExtent(it) {
|
||||
return global.TarinaiMechanicalSystem?.extent?.(it) || rotatorExtentLocal(it);
|
||||
return global.TarinaiMechanicalShapeSystem?.extent?.(it) || rotatorExtentLocal(it);
|
||||
},
|
||||
|
||||
rotatorObstacleRects(it) {
|
||||
if (it?.type === "rotator") return global.TarinaiMechanicalSystem?.obstacleRects?.(it) || rotatorObstacleRectsLocal(it);
|
||||
if (it?.type === "rotator") return global.TarinaiMechanicalShapeSystem?.obstacleRects?.(it) || rotatorObstacleRectsLocal(it);
|
||||
return [];
|
||||
},
|
||||
|
||||
reciprocatorObstacleRects(it) {
|
||||
if (it?.type === "reciprocator") return global.TarinaiMechanicalSystem?.obstacleRects?.(it) || reciprocatorObstacleRectsLocal(it);
|
||||
if (it?.type === "reciprocator") return global.TarinaiMechanicalShapeSystem?.obstacleRects?.(it) || reciprocatorObstacleRectsLocal(it);
|
||||
return [];
|
||||
},
|
||||
|
||||
rotatorBoundsAabb(it) {
|
||||
const bounds = global.TarinaiMechanicalSystem?.boundsAabb?.(it);
|
||||
const bounds = global.TarinaiMechanicalShapeSystem?.boundsAabb?.(it);
|
||||
if (bounds) return bounds;
|
||||
const rects = rotatorObstacleRectsLocal(it);
|
||||
if (!rects.length) return null;
|
||||
|
|
@ -465,7 +467,7 @@
|
|||
const rect = this.fenceRect(it);
|
||||
return rect ? [{ ...rect, type: it.type, item: it }] : [];
|
||||
}
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type)) return global.TarinaiMechanicalSystem?.obstacleRects?.(it) || [];
|
||||
if (global.TarinaiMechanicalShapeSystem?.isMechanicalType?.(it.type)) return global.TarinaiMechanicalShapeSystem?.obstacleRects?.(it) || [];
|
||||
if (it.type === "nest_box") return this.nestBoxSolidRects(it);
|
||||
return [];
|
||||
},
|
||||
|
|
@ -479,8 +481,8 @@
|
|||
const r = box.r || 42;
|
||||
const entryD = distXY(t.x, t.y, entry.x, entry.y);
|
||||
const centerD = distXY(t.x, t.y, box.x, box.y);
|
||||
const aroundDoor = entryD <= Math.max(96, r * 1.75) || centerD <= Math.max(86, r * 1.48);
|
||||
const inDoorColumn = base && t.x >= base.left + r * 0.18 && t.x <= base.right - r * 0.18 && t.y >= base.top + r * 0.02 && t.y <= base.bottom + r * 0.58;
|
||||
const aroundDoor = entryD <= Math.max(68, r * 1.32) || centerD <= Math.max(66, r * 1.18);
|
||||
const inDoorColumn = base && t.x >= base.left + r * 0.30 && t.x <= base.right - r * 0.30 && t.y >= base.top + r * 0.12 && t.y <= base.bottom + r * 0.42;
|
||||
return Boolean(aroundDoor || inDoorColumn);
|
||||
},
|
||||
|
||||
|
|
@ -489,6 +491,8 @@
|
|||
const maxRects = Math.max(12, Math.min(72, Number(requestedMaxRects || 0) || Math.ceil(limit * 2.25)));
|
||||
const cacheable = !include && !exclude;
|
||||
const stats = this.nearbyQueryStatsThisFrame || null;
|
||||
// Quantize cacheable probes so smooth movement reuses obstacle rect lists
|
||||
// across nearby frames while the inflated radius keeps collisions conservative.
|
||||
const q = 48;
|
||||
const qx = Math.floor((Number(x) || 0) / q);
|
||||
const qy = Math.floor((Number(y) || 0) / q);
|
||||
|
|
@ -582,7 +586,7 @@
|
|||
let checked = 0;
|
||||
const addRectsFor = (it) => {
|
||||
if (!it || it.dead || it.type !== "poison_block" || it._poisonHazardQueryStamp === stamp) return false;
|
||||
const reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(60, it.r || 64);
|
||||
const reach = global.TarinaiMechanicalShapeSystem?.reach?.(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > radius + reach + 36) return false;
|
||||
const partRects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(it) || [];
|
||||
if (!partRects.length) return false;
|
||||
|
|
@ -647,10 +651,7 @@
|
|||
t.lastPoisonBlockBubbleAt = now;
|
||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||
}
|
||||
if (now >= (t.lastPoisonBlockEffectAt || -999) + 0.10) {
|
||||
t.lastPoisonBlockEffectAt = now;
|
||||
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(16, rr * 1.05), life: 0.18, color: "rgba(92,188,68,0.45)" }));
|
||||
}
|
||||
// 物理ブロック衝突の視覚エフェクトは出さない。接触時の状態変化はHPバーと吹き出しだけに限定する。
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -668,6 +669,61 @@
|
|||
return hit;
|
||||
},
|
||||
|
||||
|
||||
resolvePoisonBlockItemContacts(opts = {}) {
|
||||
if ((this.itemCounts?.poison_block || 0) <= 0) return 0;
|
||||
const meltTypes = new Set(["grass_bed", "zunchi", "water", "grass"]);
|
||||
let possible = 0;
|
||||
for (const type of meltTypes) possible += this.itemCounts?.[type] || 0;
|
||||
if (possible <= 0) return 0;
|
||||
const maxPoison = Math.max(1, Math.min(48, Number(opts.maxPoison || 24) || 24));
|
||||
const maxTargets = Math.max(1, Math.min(160, Number(opts.maxTargets || 80) || 80));
|
||||
const poisonBlocks = this.itemsOfType?.("poison_block") || [];
|
||||
let removed = 0;
|
||||
let checkedPoison = 0;
|
||||
const lifecycle = global.TarinaiStructureLifecycle;
|
||||
for (const poison of poisonBlocks) {
|
||||
if (!poison || poison.dead || poison.type !== "poison_block") continue;
|
||||
checkedPoison += 1;
|
||||
if (checkedPoison > maxPoison) break;
|
||||
const rects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(poison) || [];
|
||||
if (!rects.length) continue;
|
||||
const reach = global.TarinaiMechanicalSystem?.reach?.(poison) || Math.max(72, poison.r || 64);
|
||||
const source = this.nearbyItems?.(poison.x, poison.y, reach + 96, true) || this.items || [];
|
||||
let checkedTargets = 0;
|
||||
for (const target of source) {
|
||||
if (!target || target.dead || target === poison || !meltTypes.has(target.type)) continue;
|
||||
checkedTargets += 1;
|
||||
if (checkedTargets > maxTargets) break;
|
||||
const rr = Math.max(5, Number(target.r || target.radius || itemRadiusFor?.(target.type, 10) || 10) || 10) * (target.type === "grass_bed" ? 1.15 : 0.92);
|
||||
let touching = false;
|
||||
for (const rect of rects) {
|
||||
if (this.circleOverlapsObstacleRect(target.x, target.y, rr, rect, 1.5)) { touching = true; break; }
|
||||
}
|
||||
if (!touching) continue;
|
||||
const wasLive = !target.dead;
|
||||
const destroyed = (globalThis.TarinaiItemDestruction || lifecycle)?.deleteItem?.(this, target, {
|
||||
reason: "poison-block-contact",
|
||||
userReason: "毒ブロックに触れて消えた",
|
||||
wake: true,
|
||||
breaker: poison,
|
||||
panicOwner: false,
|
||||
});
|
||||
if (wasLive && destroyed) {
|
||||
removed += 1;
|
||||
this.markTerrainDirtyAt?.(target.x, target.y, Math.max(36, rr + 16), "poison-block-contact");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts?.("poison-block-contact");
|
||||
this.markSpatialDirty?.("poison-block-contact");
|
||||
this.drawListDirty = true;
|
||||
}
|
||||
return removed;
|
||||
},
|
||||
|
||||
pushTarinaiOutOfRect(t, r, rr, opts = {}) {
|
||||
if (!t || !r) return false;
|
||||
const preVx = Number(t.vx || 0) || 0;
|
||||
|
|
@ -726,6 +782,8 @@
|
|||
: Math.min(rr - d + slop, maxPush);
|
||||
t.x += nx * push;
|
||||
t.y += ny * push;
|
||||
t.lastSolidObstacleCollisionSource = r.item || null;
|
||||
t.lastSolidObstacleCollisionAt = this.time || 0;
|
||||
if (r.bounce) {
|
||||
const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08;
|
||||
const vx = t.vx || 0;
|
||||
|
|
@ -773,11 +831,6 @@
|
|||
spinKick: 0,
|
||||
});
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, r.rotator ? 0.12 : 0.10);
|
||||
const field = r.rotator ? "lastRotatorHitEffectAt" : (r.poisonBlock ? "lastPoisonBlockPushEffectAt" : "lastReciprocatorHitEffectAt");
|
||||
if ((this.time || 0) >= (t[field] || -999) + 0.10) {
|
||||
t[field] = this.time || 0;
|
||||
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(14, rr * 0.95), life: 0.16, color: r.rotator ? "rgba(175,116,230,0.42)" : (r.poisonBlock ? "rgba(92,188,68,0.42)" : "rgba(90,150,214,0.40)") }));
|
||||
}
|
||||
} else if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18;
|
||||
else t.vy *= -0.18;
|
||||
return true;
|
||||
|
|
@ -911,9 +964,11 @@
|
|||
return false;
|
||||
},
|
||||
|
||||
grassBlockedAt(x, y, self = null) {
|
||||
grassBlockedAt(x, y, self = null, opts = {}) {
|
||||
const ignoreSoft = Boolean(opts?.ignoreSoftPlacement);
|
||||
for (const it of this.nearbyItems(x, y, 96)) {
|
||||
if (it === self || it.dead) continue;
|
||||
if (ignoreSoft && global.TarinaiPhysicsSoftClear?.isSoftPlacementType?.(it.type)) continue;
|
||||
for (const r of this.solidObstacleRects(it)) {
|
||||
if (this.pointInRect(x, y, r, 10)) return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,4 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
|
|
@ -492,9 +463,7 @@
|
|||
},
|
||||
|
||||
nestBoxOccupants(box, limit = 5) {
|
||||
if (!box || box.dead || box.type !== "nest_box") return [];
|
||||
const occupants = (this.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
return Number.isFinite(limit) ? occupants.slice(0, Math.max(0, limit)) : occupants;
|
||||
return globalThis.TarinaiNestSleepSystem?.nestBoxOccupants?.(this, box, limit) || [];
|
||||
},
|
||||
|
||||
pointerNestBoxInfo() {
|
||||
|
|
|
|||
|
|
@ -167,14 +167,14 @@
|
|||
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
||||
<button type="button" data-rotator-mode="line" class="active">\u76f4\u7dda</button>
|
||||
<button type="button" data-rotator-mode="free">\u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-rotator-mode="erase">\u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-rotator-template="bar">\u68d2</button>
|
||||
<button type="button" data-rotator-template="cross">\u5341\u5b57</button>
|
||||
<button type="button" data-rotator-template="circle">\u5186\u3063\u307d\u3044</button>
|
||||
<button id="rotatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
||||
<button id="rotatorClear" type="button">\u5168\u6d88\u3057</button>
|
||||
<button type="button" data-rotator-mode="line" class="active">━ \u76f4\u7dda</button>
|
||||
<button type="button" data-rotator-mode="free">〰 \u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-rotator-mode="erase">⌫ \u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-rotator-template="bar">┃ \u68d2</button>
|
||||
<button type="button" data-rotator-template="cross">+ \u5341\u5b57</button>
|
||||
<button type="button" data-rotator-template="circle">○ \u5186</button>
|
||||
<button id="rotatorUndo" type="button">↶ \u4e00\u3064\u623b\u3059</button>
|
||||
<button id="rotatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
|
||||
</div>
|
||||
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||
<p class="rotator-editor-hint">\u4e2d\u592e\u304c\u56de\u8ee2\u8ef8\u3002\u76f4\u7dda\u30fb\u81ea\u7531\u63cf\u753b\u30fb\u6d88\u3057\u30b4\u30e0\u3092\u9078\u3073\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u8f2a\u90ed\u3092\u7de8\u96c6\u3002</p>
|
||||
|
|
@ -184,6 +184,7 @@
|
|||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(dialog);
|
||||
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "rotator");
|
||||
return dialog;
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +204,7 @@
|
|||
if (!item || (item.type !== "rotator" && item.type !== "poison_block")) return false;
|
||||
const isPoisonBlock = item.type === "poison_block";
|
||||
const dialog = ensureRotatorEditor();
|
||||
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "rotator");
|
||||
const canvas = dialog.querySelector("#rotatorCanvas");
|
||||
const ctx = canvas?.getContext?.("2d");
|
||||
const speed = dialog.querySelector("#mechMotorSpeed");
|
||||
|
|
@ -302,7 +304,7 @@
|
|||
for (const btn of dialog.querySelectorAll("[data-rotator-mode]")) btn.classList.toggle("active", btn.dataset.rotatorMode === mode);
|
||||
canvas.style.cursor = mode === "erase" ? "cell" : "crosshair";
|
||||
};
|
||||
const pointSegmentDistance = (p, seg) => {
|
||||
const pointSegmentDistance = (p, seg) => global.TarinaiPhysicsShapeEditorSystem?.pointSegmentDistance?.(p, seg) ?? (() => {
|
||||
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0;
|
||||
|
|
@ -311,7 +313,7 @@
|
|||
if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1);
|
||||
const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq));
|
||||
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||
};
|
||||
})();
|
||||
const eraseAt = (p) => {
|
||||
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||
const radius = Math.max(24, lw * 1.35);
|
||||
|
|
@ -330,17 +332,8 @@
|
|||
return false;
|
||||
};
|
||||
const setTemplate = (name) => {
|
||||
if (name === "bar") segments = [[-130, 0, 130, 0]];
|
||||
else if (name === "cross") segments = [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
||||
else if (name === "circle") {
|
||||
segments = [];
|
||||
const n = 18, r = 108;
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
const a = i / n * Math.PI * 2;
|
||||
const b = (i + 1) / n * Math.PI * 2;
|
||||
segments.push([Math.cos(a) * r, Math.sin(a) * r, Math.cos(b) * r, Math.sin(b) * r]);
|
||||
}
|
||||
}
|
||||
const shared = global.TarinaiPhysicsShapeEditorSystem?.templateSegments?.(name);
|
||||
if (shared) segments = shared.map(seg => seg.slice(0, 4));
|
||||
cleanSegments(); draw();
|
||||
};
|
||||
const syncPair = (a, b) => { if (!a || !b) return; a.oninput = () => { b.value = a.value; draw(); }; b.oninput = () => { a.value = b.value; draw(); }; };
|
||||
|
|
@ -455,14 +448,14 @@
|
|||
<label><input id="reciprocatorSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
||||
<button type="button" data-reciprocator-mode="line" class="active">\u76f4\u7dda</button>
|
||||
<button type="button" data-reciprocator-mode="free">\u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-reciprocator-mode="erase">\u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-reciprocator-template="bar">\u68d2</button>
|
||||
<button type="button" data-reciprocator-template="cross">\u5341\u5b57</button>
|
||||
<button type="button" data-reciprocator-template="circle">\u5186\u3063\u307d\u3044</button>
|
||||
<button id="reciprocatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
||||
<button id="reciprocatorClear" type="button">\u5168\u6d88\u3057</button>
|
||||
<button type="button" data-reciprocator-mode="line" class="active">━ \u76f4\u7dda</button>
|
||||
<button type="button" data-reciprocator-mode="free">〰 \u81ea\u7531\u63cf\u753b</button>
|
||||
<button type="button" data-reciprocator-mode="erase">⌫ \u6d88\u3057\u30b4\u30e0</button>
|
||||
<button type="button" data-reciprocator-template="bar">┃ \u68d2</button>
|
||||
<button type="button" data-reciprocator-template="cross">+ \u5341\u5b57</button>
|
||||
<button type="button" data-reciprocator-template="circle">○ \u5186</button>
|
||||
<button id="reciprocatorUndo" type="button">↶ \u4e00\u3064\u623b\u3059</button>
|
||||
<button id="reciprocatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
|
||||
</div>
|
||||
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||
<p class="rotator-editor-hint">\u4e2d\u592e\u306e\u8f2a\u90ed\u5168\u4f53\u304c\u3001\u8a2d\u5b9a\u3057\u305f\u65b9\u5411\u3078\u5f80\u5fa9\u79fb\u52d5\u3057\u307e\u3059\u3002\u52d5\u529bOFF\u3067\u306f\u3001\u305f\u308a\u306a\u3044\u30fb\u305a\u3093\u3061\u30fb\u4ed6\u306e\u6a5f\u69cb\u304b\u3089\u62bc\u3055\u308c\u305f\u3068\u304d\u3060\u3051\u52d5\u304d\u307e\u3059\u3002</p>
|
||||
|
|
@ -472,12 +465,14 @@
|
|||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(dialog);
|
||||
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "reciprocator");
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function openReciprocatorEditor(item, worldRef) {
|
||||
if (!item || item.type !== "reciprocator") return false;
|
||||
const dialog = ensureReciprocatorEditor();
|
||||
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "reciprocator");
|
||||
const canvas = dialog.querySelector("#reciprocatorCanvas");
|
||||
const ctx = canvas?.getContext?.("2d");
|
||||
const angle = dialog.querySelector("#reciprocatorAngle");
|
||||
|
|
@ -588,7 +583,7 @@
|
|||
for (const btn of dialog.querySelectorAll("[data-reciprocator-mode]")) btn.classList.toggle("active", btn.dataset.reciprocatorMode === mode);
|
||||
canvas.style.cursor = mode === "erase" ? "cell" : "crosshair";
|
||||
};
|
||||
const pointSegmentDistance = (p, seg) => {
|
||||
const pointSegmentDistance = (p, seg) => global.TarinaiPhysicsShapeEditorSystem?.pointSegmentDistance?.(p, seg) ?? (() => {
|
||||
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0;
|
||||
|
|
@ -597,7 +592,7 @@
|
|||
if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1);
|
||||
const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq));
|
||||
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||
};
|
||||
})();
|
||||
const eraseAt = (p) => {
|
||||
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||
const radius = Math.max(28, lw * 1.55);
|
||||
|
|
@ -616,17 +611,8 @@
|
|||
return false;
|
||||
};
|
||||
const setTemplate = (name) => {
|
||||
if (name === "bar") segments = [[-130, 0, 130, 0]];
|
||||
else if (name === "cross") segments = [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
||||
else if (name === "circle") {
|
||||
segments = [];
|
||||
const n = 18, r = 108;
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
const a = i / n * Math.PI * 2;
|
||||
const b = (i + 1) / n * Math.PI * 2;
|
||||
segments.push([Math.cos(a) * r, Math.sin(a) * r, Math.cos(b) * r, Math.sin(b) * r]);
|
||||
}
|
||||
}
|
||||
const shared = global.TarinaiPhysicsShapeEditorSystem?.templateSegments?.(name);
|
||||
if (shared) segments = shared.map(seg => seg.slice(0, 4));
|
||||
cleanSegments(); draw();
|
||||
};
|
||||
const syncPair = (a, b, redraw = false) => { if (!a || !b) return; a.oninput = () => { b.value = a.value; if (redraw) draw(); }; b.oninput = () => { a.value = b.value; if (redraw) draw(); }; };
|
||||
|
|
@ -723,7 +709,7 @@
|
|||
|
||||
function copyTargetAt(worldRef, x, y) {
|
||||
let blockedReason = "";
|
||||
const collision = global.TarinaiCollisionFootprints;
|
||||
const collision = global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints;
|
||||
const items = worldRef?.items || [];
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const it = items[i];
|
||||
|
|
@ -911,7 +897,7 @@
|
|||
let d = distXY(x, y, it.x, it.y);
|
||||
let hit = d <= Math.max(30, (it.r || 24) * 1.35);
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || worldRef?.isFenceType?.(it.type)) {
|
||||
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(worldRef, it, x, y, { padding: 14, radiusMultiplier: 1.25 }) || { hit: false, distance: d };
|
||||
const result = (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.hitTestItem?.(worldRef, it, x, y, { padding: 14, radiusMultiplier: 1.25 }) || { hit: false, distance: d };
|
||||
hit = Boolean(result.hit);
|
||||
d = Math.min(d, result.distance ?? d);
|
||||
}
|
||||
|
|
@ -989,9 +975,13 @@
|
|||
);
|
||||
}
|
||||
|
||||
function isSoftPhysicsIgnoredItemType(type = "") {
|
||||
function isSoftPlacementIgnoredItemType(type = "") {
|
||||
const key = String(type || "");
|
||||
return key === "water" || key === "grass_bed";
|
||||
return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water";
|
||||
}
|
||||
|
||||
function isSoftPhysicsIgnoredItemType(type = "") {
|
||||
return isSoftPlacementIgnoredItemType(type);
|
||||
}
|
||||
|
||||
function softItemRadius(item) {
|
||||
|
|
@ -999,42 +989,57 @@
|
|||
return Math.max(8, Number(item.r || item.radius || itemRadiusFor?.(item.type, 12) || 12) || 12);
|
||||
}
|
||||
|
||||
function physicsItemOverlapsSoftItem(worldRef, physicsItem, softItem) {
|
||||
if (!worldRef || !physicsItem || !softItem || softItem.dead) return false;
|
||||
const r = softItemRadius(softItem);
|
||||
if (physicsItem.type === "rope" || physicsItem.type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const pb = physicsApi();
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(physicsItem, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(physicsItem, 1), worldRef);
|
||||
if (a && b) {
|
||||
const d = runtime?.pointSegmentDistance?.(softItem.x, softItem.y, a.x, a.y, b.x, b.y);
|
||||
return Number.isFinite(d) && d <= r + (physicsItem.type === "rod" ? 9 : 7);
|
||||
}
|
||||
}
|
||||
const rects = worldRef.solidObstacleRects?.(physicsItem) || [];
|
||||
if (rects.length) {
|
||||
return rects.some(rect => global.TarinaiCollisionFootprints?.rectCircleOverlap?.(rect, softItem.x, softItem.y, r, 0.5));
|
||||
}
|
||||
const pr = Math.max(10, Number(physicsItem.r || physicsItem.radius || itemRadiusFor?.(physicsItem.type, 12) || 12) || 12);
|
||||
return distXY(physicsItem.x, physicsItem.y, softItem.x, softItem.y) <= pr + r;
|
||||
function placementRectsForItem(worldRef, item) {
|
||||
const solid = worldRef?.solidObstacleRects?.(item) || [];
|
||||
if (solid.length) return solid;
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item?.type)) return global.TarinaiMechanicalSystem?.placementRects?.(item) || [];
|
||||
return solid;
|
||||
}
|
||||
|
||||
function clearSoftItemsUnderPhysicsItem(worldRef, item, reason = "physics-overlap-clear") {
|
||||
if (!worldRef || !item || !isPhysicsObstacleItemType(item.type)) return 0;
|
||||
let search = Math.max(80, Number(item.r || item.radius || 36) + 90);
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) {
|
||||
search = Math.max(search, (global.TarinaiMechanicalSystem?.reach?.(item) || item.r || 80) + 96);
|
||||
} else if (item.type === "rope" || item.type === "rod") {
|
||||
search = Math.max(search, Number(physicsApi()?.linkScalar?.(item, "len", (item.r || 40) * 2 || 80)) * 0.55 + 96);
|
||||
function placementItemOverlapsSoftItem(worldRef, placedItem, softItem) {
|
||||
if (!worldRef || !placedItem || !softItem || softItem.dead) return false;
|
||||
const r = softItemRadius(softItem);
|
||||
if (placedItem.type === "rope" || placedItem.type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const pb = physicsApi();
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(placedItem, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(placedItem, 1), worldRef);
|
||||
if (a && b) {
|
||||
const d = runtime?.pointSegmentDistance?.(softItem.x, softItem.y, a.x, a.y, b.x, b.y);
|
||||
return Number.isFinite(d) && d <= r + (placedItem.type === "rod" ? 9 : 7);
|
||||
}
|
||||
}
|
||||
let removed = 0;
|
||||
const rects = placementRectsForItem(worldRef, placedItem);
|
||||
if (rects.length) return rects.some(rect => (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.rectCircleOverlap?.(rect, softItem.x, softItem.y, r, 0.5));
|
||||
const pr = Math.max(10, Number(placedItem.r || placedItem.radius || itemRadiusFor?.(placedItem.type, 12) || 12) || 12);
|
||||
return distXY(placedItem.x, placedItem.y, softItem.x, softItem.y) <= pr + r;
|
||||
}
|
||||
|
||||
function placementSoftSearchRadius(item) {
|
||||
let search = Math.max(80, Number(item?.r || item?.radius || 36) + 90);
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item?.type)) search = Math.max(search, (global.TarinaiMechanicalSystem?.reach?.(item) || item.r || 80) + 96);
|
||||
else if (item?.type === "rope" || item?.type === "rod") search = Math.max(search, Number(physicsApi()?.linkScalar?.(item, "len", (item.r || 40) * 2 || 80)) * 0.55 + 96);
|
||||
return search;
|
||||
}
|
||||
|
||||
function collectSoftItemsUnderPlacementItem(worldRef, item) {
|
||||
if (!worldRef || !item || item.type === "trace" || item.type === "splat") return [];
|
||||
const out = [];
|
||||
const search = placementSoftSearchRadius(item);
|
||||
for (const soft of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) {
|
||||
if (!soft || soft === item || soft.dead || !isSoftPhysicsIgnoredItemType(soft.type)) continue;
|
||||
if (!physicsItemOverlapsSoftItem(worldRef, item, soft)) continue;
|
||||
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, soft, {
|
||||
if (!soft || soft === item || soft.dead || !isSoftPlacementIgnoredItemType(soft.type)) continue;
|
||||
if (placementItemOverlapsSoftItem(worldRef, item, soft)) out.push(soft);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function clearSoftItemsUnderPlacementItem(worldRef, item, reason = "placement-soft-overlap-clear") {
|
||||
const items = collectSoftItemsUnderPlacementItem(worldRef, item);
|
||||
let removed = 0;
|
||||
for (const soft of items) {
|
||||
const ok = (global.TarinaiItemDestruction || global.TarinaiStructureLifecycle)?.deleteItem?.(worldRef, soft, {
|
||||
reason,
|
||||
userReason: "\u7269\u7406\u30a2\u30a4\u30c6\u30e0\u306b\u91cd\u306a\u3063\u305f",
|
||||
userReason: "置きものが重なった",
|
||||
wake: true,
|
||||
});
|
||||
if (ok) removed += 1;
|
||||
|
|
@ -1047,34 +1052,35 @@
|
|||
return removed;
|
||||
}
|
||||
|
||||
function physicsItemOverlapsSoftItem(worldRef, physicsItem, softItem) {
|
||||
return placementItemOverlapsSoftItem(worldRef, physicsItem, softItem);
|
||||
}
|
||||
|
||||
function clearSoftItemsUnderPhysicsItem(worldRef, item, reason = "physics-overlap-clear") {
|
||||
if (!worldRef || !item || !isPhysicsObstacleItemType(item.type)) return 0;
|
||||
return clearSoftItemsUnderPlacementItem(worldRef, item, reason);
|
||||
}
|
||||
|
||||
global.TarinaiPhysicsSoftClear = Object.freeze({
|
||||
isPhysicsType: isPhysicsObstacleItemType,
|
||||
isSoftType: isSoftPhysicsIgnoredItemType,
|
||||
isSoftPlacementType: isSoftPlacementIgnoredItemType,
|
||||
overlaps: physicsItemOverlapsSoftItem,
|
||||
overlapsPlacement: placementItemOverlapsSoftItem,
|
||||
collectForPlacement: collectSoftItemsUnderPlacementItem,
|
||||
clearForPlacement: clearSoftItemsUnderPlacementItem,
|
||||
clearForItem: clearSoftItemsUnderPhysicsItem,
|
||||
});
|
||||
|
||||
function clearOverlappingGrassBedsForPlacement(worldRef, item) {
|
||||
if (!worldRef || !item || item.type === "grass_bed" || item.type === "trace" || item.type === "splat") return 0;
|
||||
if (isPhysicsObstacleItemType(item.type)) return clearSoftItemsUnderPhysicsItem(worldRef, item, "physics-placement-soft-clear");
|
||||
const radius = Math.max(16, item.r || item.radius || 12);
|
||||
let removed = 0;
|
||||
for (const bed of worldRef.nearbyItems?.(item.x, item.y, radius + 72) || worldRef.items || []) {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
|
||||
const hit = radius * 0.95 + Math.max(14, (bed.r || 18) * 0.95);
|
||||
if (distXY(item.x, item.y, bed.x, bed.y) > hit) continue;
|
||||
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, bed, {
|
||||
reason: "grass-bed-ignored-by-placement",
|
||||
userReason: "\u7f6e\u304d\u3082\u306e\u304c\u7f6e\u304b\u308c\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (ok) removed += 1;
|
||||
}
|
||||
return removed;
|
||||
return clearSoftItemsUnderPlacementItem(worldRef, item, "placement-soft-overlap-clear");
|
||||
}
|
||||
|
||||
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-change") {
|
||||
if (!worldRef) return;
|
||||
// Render and spatial rebuild are already performed by the main frame loop.
|
||||
// Tool operations only invalidate cached views; batch placement must not
|
||||
// force synchronous render/spatial work per item.
|
||||
worldRef.drawListDirty = true;
|
||||
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
||||
if (worldRef._visibleRenderStack) {
|
||||
|
|
@ -1085,11 +1091,6 @@
|
|||
}
|
||||
worldRef.markItemBucketsDirty?.(reason);
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
worldRef.ensureSpatial?.(reason);
|
||||
if (typeof global.render === "function") {
|
||||
global.render();
|
||||
global.requestAnimationFrame?.(() => global.render?.());
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
|
|
@ -1156,30 +1157,34 @@
|
|||
const otherIsGate = it.type === "gate_fence";
|
||||
const margin = (item.type === "gate_fence" || otherIsGate) ? 1.5 : baseMargin;
|
||||
for (const r of this.solidObstacleRects(it)) {
|
||||
if (global.TarinaiCollisionFootprints?.rectsOverlap?.(rect, r, margin)) return true;
|
||||
if ((global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.rectsOverlap?.(rect, r, margin)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
importantPlacementOverlapBlocked(item) {
|
||||
return global.TarinaiCollisionFootprints?.itemPlacementOverlapBlocked?.(this, item) ?? false;
|
||||
return (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.itemPlacementOverlapBlocked?.(this, item) ?? false;
|
||||
},
|
||||
|
||||
placementBlocked(item) {
|
||||
if (!item || item.dead) return true;
|
||||
if (item.type === "genkotsu") return false;
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
const rects = this.solidObstacleRects(item);
|
||||
if (rects.length) {
|
||||
for (const rect of rects) {
|
||||
if (rect.left < pad || rect.top < pad || rect.right > this.w - pad || rect.bottom > this.h - pad) return true;
|
||||
const sharedBoundsBlocked = global.TarinaiPlacementPreviewSystem?.boundsBlocked?.(this, item, { rects });
|
||||
if (sharedBoundsBlocked === true) return true;
|
||||
if (sharedBoundsBlocked !== false) {
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
if (rects.length) {
|
||||
for (const rect of rects) {
|
||||
if (rect.left < pad || rect.top < pad || rect.right > this.w - pad || rect.bottom > this.h - pad) return true;
|
||||
}
|
||||
} else {
|
||||
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
|
||||
if (item.x - radius < pad || item.y - radius < pad || item.x + radius > this.w - pad || item.y + radius > this.h - pad) return true;
|
||||
}
|
||||
} else {
|
||||
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
|
||||
if (item.x - radius < pad || item.y - radius < pad || item.x + radius > this.w - pad || item.y + radius > this.h - pad) return true;
|
||||
}
|
||||
if (item.type === "grass" && !this.grassSpotOpen(item.x, item.y, { avoidTarinai: true })) return true;
|
||||
if (item.type === "grass" && (this.grassBlockedAt?.(item.x, item.y, item, { ignoreSoftPlacement: true }) || this.grassOnTarinaiAt?.(item.x, item.y))) return true;
|
||||
if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true;
|
||||
if (this.importantPlacementOverlapBlocked(item)) return true;
|
||||
if (item.type === "nest_box") {
|
||||
|
|
@ -1194,18 +1199,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
if (item.type === "grass_bed") {
|
||||
const spacing = Math.max(46, (item.r || 18) * 3.0);
|
||||
for (const it of this.nearbyItems(item.x, item.y, spacing + 70) || []) {
|
||||
if (!it || it === item || it.dead || it.type !== "grass_bed") continue;
|
||||
const required = spacing + Math.max(0, ((it.r || 18) - (item.r || 18)) * 0.5);
|
||||
if (distXY(item.x, item.y, it.x, it.y) < required) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
placementClampPointFor(item, x = item?.x || 0, y = item?.y || 0) {
|
||||
const shared = global.TarinaiPlacementPreviewSystem?.clampPointFor?.(this, item, x, y);
|
||||
if (shared) return shared;
|
||||
const pad = CONFIG.worldPadding || 30;
|
||||
const rects = this.solidObstacleRects(item);
|
||||
if (rects.length) {
|
||||
|
|
@ -1287,12 +1286,16 @@
|
|||
hit = Boolean(result.hit);
|
||||
d = Math.min(d, result.distance ?? d);
|
||||
} else {
|
||||
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { radiusMultiplier: it.type === "ball" ? 4.0 : 2.2 }) || { hit: false, distance: d };
|
||||
const result = (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.hitTestItem?.(this, it, x, y, { radiusMultiplier: it.type === "ball" ? 4.0 : 2.2 }) || { hit: false, distance: d };
|
||||
hit = Boolean(result.hit);
|
||||
d = Math.min(d, result.distance ?? d);
|
||||
}
|
||||
if (hit && d < foundItemD) { foundItem = it; foundItemD = d; }
|
||||
}
|
||||
if (foundItem && isPinType(foundItem.type) && foundItem.pinState === "ball_lodged") {
|
||||
const carrier = (this.items || []).find(it => it && !it.dead && it.type === "ball" && it.id === foundItem.pinBallId);
|
||||
if (carrier) return carrier;
|
||||
}
|
||||
if (foundItem && isPinType(foundItem.type) && foundItem.pinState === "lodged") return foundItem;
|
||||
if (foundT) {
|
||||
const lodged = foundT.currentLodgedPin?.() || (this.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === foundT.id);
|
||||
|
|
@ -1324,13 +1327,13 @@
|
|||
item.dropTimer = 0;
|
||||
item.dropImpactDone = true;
|
||||
}
|
||||
clearOverlappingGrassBedsForPlacement(this, item);
|
||||
const removedSoft = clearOverlappingGrassBedsForPlacement(this, item);
|
||||
if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear");
|
||||
const placed = this.addItem?.(item, `place:${item.type}`);
|
||||
if (!placed) {
|
||||
if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
||||
return null;
|
||||
}
|
||||
clearSoftItemsUnderPhysicsItem(this, item, `place:${item.type}:soft-overlap-clear`);
|
||||
this.emit("tool:place", { item, type: item.type, dropped });
|
||||
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
||||
this.drawListDirty = true;
|
||||
|
|
@ -1360,6 +1363,9 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
const wasSelectionEmpty = !this.selected;
|
||||
if (found && wasSelectionEmpty) this._scrollSelectedDataTopOnNextSelection = true;
|
||||
else this._scrollSelectedDataTopOnNextSelection = false;
|
||||
global.TarinaiCommands?.setSelection?.(this, found) || (this.selected = found);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1382,7 +1388,7 @@
|
|||
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color }));
|
||||
this.drawListDirty = true;
|
||||
audio.poke?.();
|
||||
this.log(`${label}\u3092\u3064\u3064\u3044\u3066\u52d5\u304b\u3057\u305f\u3002`, "observe", { eventType: "mechanism_poke", hiddenFromObservation: true });
|
||||
this.log(`${label}\u3092\u3064\u3064\u3044\u3066\u52d5\u304b\u3057\u305f\u3002`, "observe", { eventType: "mechanism_poke" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1432,8 +1438,8 @@
|
|||
const placed = this.placeItem(rawItem, true);
|
||||
if (!placed) return;
|
||||
forceImmediateToolVisualRefresh(this, "tool-place");
|
||||
const label = toolLabel(itemType);
|
||||
this.log(itemType === "genkotsu" || isPinType(itemType) ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`, "observe", { eventType: "player_item_placement" });
|
||||
// Item placement intentionally no longer writes observation-log entries.
|
||||
// Placement audio is still emitted through tool:placed.
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1454,7 +1460,7 @@
|
|||
|
||||
shouldHideFromObservationRecords(entry = {}) {
|
||||
const type = typeof entry === "string" ? "" : (entry.eventType || "");
|
||||
return type === "ball_poke" || type === "avoid_relationship";
|
||||
return type === "ball_poke" || type === "avoid_relationship" || type === "mechanism_poke";
|
||||
},
|
||||
|
||||
log(text, kind = null, options = {}) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-action") {
|
||||
if (!worldRef) return;
|
||||
// Render and spatial rebuild are already performed by the main frame loop.
|
||||
// Tool operations only invalidate cached views; batch placement must not
|
||||
// force synchronous render/spatial work per item.
|
||||
worldRef.drawListDirty = true;
|
||||
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
||||
if (worldRef._visibleRenderStack) {
|
||||
|
|
@ -16,11 +19,6 @@
|
|||
}
|
||||
worldRef.markItemBucketsDirty?.(reason);
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
worldRef.ensureSpatial?.(reason);
|
||||
if (typeof global.render === "function") {
|
||||
global.render();
|
||||
global.requestAnimationFrame?.(() => global.render?.());
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
|
|
@ -149,7 +147,7 @@
|
|||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead) continue;
|
||||
if ((this.tool === "rope" || this.tool === "rod") && (it.type === "water" || it.type === "grass_bed")) continue;
|
||||
if ((this.tool === "rope" || this.tool === "rod") && global.TarinaiPhysicsSoftClear?.isSoftPlacementType?.(it.type)) continue;
|
||||
const isMechanism = Boolean(global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type));
|
||||
const isFence = this.isFenceType?.(it.type) || false;
|
||||
const isConstraint = it.type === "rope" || it.type === "rod";
|
||||
|
|
@ -400,20 +398,45 @@
|
|||
} else if (hit?.kind === "item") {
|
||||
handled = true;
|
||||
const target = hit.target;
|
||||
const shouldRemove = Math.random() < 0.30;
|
||||
if (shouldRemove) {
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
||||
reason: "shoot-tool",
|
||||
userReason: "\u5c04\u6483\u3067\u6d88\u3048\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (removed) {
|
||||
this.markItemBucketsDirty?.("shoot-delete");
|
||||
this.markSpatialDirty?.("shoot-delete");
|
||||
this.drawListDirty = true;
|
||||
if (target && typeof isPinType === "function" && isPinType(target.type)) {
|
||||
const blasted = this.blastLoosePinsFrom?.(aim.x, aim.y, 92, target, 1.25, "shoot-tool") || 0;
|
||||
if (!blasted && target.pinState !== "lodged") {
|
||||
let dx = (target.x || 0) - aim.x;
|
||||
let dy = (target.y || 0) - aim.y;
|
||||
let d = Math.hypot(dx, dy) || 1;
|
||||
if (d < 0.001) {
|
||||
const a = deterministicAngle(this, "shoot-pin-overlap", target, aim.x, aim.y);
|
||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||
}
|
||||
const pinSystem = globalThis.TarinaiItemDynamicPinSystem;
|
||||
if (target.pinState === "ball_lodged") pinSystem?.detach?.(target, this, "shoot");
|
||||
this.applyImpulse?.(target,
|
||||
dx / d * 610 + deterministicRange(this, "shoot-pin-jitter-x", -70, 70, target, aim.x, aim.y),
|
||||
dy / d * 610 + deterministicRange(this, "shoot-pin-jitter-y", -70, 70, target, aim.x, aim.y)
|
||||
);
|
||||
target.spinVelocity = clamp((target.spinVelocity || 0) + deterministicRange(this, "shoot-pin-spin", -20, 20, target, aim.x, aim.y), -40, 40);
|
||||
target.noStickUntil = (this.time || 0) + 0.22;
|
||||
target.lastPokedAt = this.time || 0;
|
||||
}
|
||||
this.effects?.push(new Effect("ring", target.x || aim.x, target.y || aim.y, { size: Math.max(12, (target.r || 8) * 1.8), life: 0.18, color: "rgba(255,230,116,0.48)" }));
|
||||
this.markSpatialDirty?.("shoot-pin-blast");
|
||||
this.drawListDirty = true;
|
||||
} else {
|
||||
const shouldRemove = Math.random() < 0.30;
|
||||
if (shouldRemove) {
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
||||
reason: "shoot-tool",
|
||||
userReason: "\u5c04\u6483\u3067\u6d88\u3048\u305f",
|
||||
wake: true,
|
||||
});
|
||||
if (removed) {
|
||||
this.markItemBucketsDirty?.("shoot-delete");
|
||||
this.markSpatialDirty?.("shoot-delete");
|
||||
this.drawListDirty = true;
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,76 +72,10 @@
|
|||
},
|
||||
|
||||
evaluateColonyMood(force = false) {
|
||||
const now = this.time || 0;
|
||||
if (!force && this.colonyMood && now < (this.nextColonyMoodEvalAt || 0)) return this.colonyMood;
|
||||
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
||||
const n = alive.length;
|
||||
const avg = (fn) => n ? alive.reduce((sum, t) => sum + (Number(fn(t)) || 0), 0) / n : 0;
|
||||
const count = (fn) => alive.reduce((sum, t) => sum + (fn(t) ? 1 : 0), 0);
|
||||
const ratio = (value) => n ? value / n : 0;
|
||||
const avgEnergy = avg(t => t.energy || 0);
|
||||
const avgStress = avg(t => t.stress || 0);
|
||||
const avgHunger = avg(t => t.hunger || 0);
|
||||
const avgSafety = avg(t => t.needs?.safety || 0);
|
||||
const panicCount = count(t => t.state === "panic" || (t.fearTimer || 0) > 0.2 || (t.panicTargetUntil || 0) > now);
|
||||
const safetyHighCount = count(t => (t.needs?.safety || 0) >= 52 || (t.fearTimer || 0) > 0.35);
|
||||
const weakCount = count(t => (t.energy || 0) < 45 || (t.hunger || 0) > 88 || (t.needs?.health || 0) > 58);
|
||||
const diseasedCount = count(t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease);
|
||||
const breedingCount = count(t => (t.loveMochiTimer || 0) > 0.1 || t.state === "birth_ritual" || (t.birthRitualTimer || 0) > 0.1);
|
||||
const recentDeaths = (Array.isArray(this.recentDeathTimes) ? this.recentDeathTimes : []).filter(t => now - (Number(t) || -999) <= 30).length;
|
||||
let nearestAvg = Infinity;
|
||||
if (n >= 2) {
|
||||
let totalNearest = 0;
|
||||
for (const a of alive) {
|
||||
let best = Infinity;
|
||||
for (const b of alive) {
|
||||
if (a === b) continue;
|
||||
const dx = (a.x || 0) - (b.x || 0);
|
||||
const dy = (a.y || 0) - (b.y || 0);
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < best) best = d2;
|
||||
}
|
||||
if (Number.isFinite(best)) totalNearest += Math.sqrt(best);
|
||||
}
|
||||
nearestAvg = totalNearest / n;
|
||||
}
|
||||
const recentShock = (now - (this.lastShootAt || -999) <= 8) || (now - (this.lastGenkotsuAt || -999) <= 8);
|
||||
const panicRatio = ratio(panicCount);
|
||||
const safetyRatio = ratio(safetyHighCount);
|
||||
const weakRatio = ratio(weakCount);
|
||||
const diseaseRatio = ratio(diseasedCount);
|
||||
const breedingRatio = ratio(breedingCount);
|
||||
const fieldId = String(this.fieldType || "garden");
|
||||
const overcrowdLimit = fieldId === "cage" ? 20 : (fieldId === "park" ? 100 : 50);
|
||||
const denseCrowd = Number.isFinite(nearestAvg) && nearestAvg < 84 && n >= Math.max(8, Math.floor(overcrowdLimit * 0.55));
|
||||
const candidates = [];
|
||||
const add = (id, priority, ok) => { if (ok) candidates.push({ id, priority }); };
|
||||
|
||||
// Severe or multi-signal situations are evaluated first. Simple population
|
||||
// and comfort labels are deliberately fallback states so they do not mask
|
||||
// richer current events.
|
||||
add("devastation", 120, n <= 0 || recentDeaths >= 3 || (n <= 2 && this.deadCount > 0 && (recentDeaths > 0 || weakRatio >= 0.35 || panicRatio >= 0.20)));
|
||||
add("crisis", 110, recentDeaths >= 2 || (avgEnergy < 38 && weakRatio >= 0.34) || (avgHunger > 88 && weakRatio >= 0.40));
|
||||
add("confusion", 100, (recentShock && (panicRatio >= 0.10 || safetyRatio >= 0.18)) || panicRatio >= 0.25);
|
||||
add("fearful", 90, (avgSafety >= 50 && safetyRatio >= 0.28) || (recentShock && safetyRatio >= 0.18));
|
||||
add("disease_spread", 82, diseaseRatio >= 0.25 || (diseasedCount >= 3 && diseaseRatio >= 0.16));
|
||||
add("weakened", 74, (avgEnergy < 54 && weakRatio >= 0.18) || avgHunger > 82 || weakRatio >= 0.34);
|
||||
add("breeding", 66, breedingRatio >= 0.10 || breedingCount >= 2 || now - (this.lastBirthAt || -999) <= 45);
|
||||
add("happy", 46, avgStress < 28 && panicRatio < 0.08 && diseaseRatio < 0.12 && weakRatio < 0.18);
|
||||
add("overcrowded", 34, n >= overcrowdLimit || denseCrowd);
|
||||
add("isolated", 28, n <= 1 || (n <= 3 && nearestAvg > Math.max(180, Math.min(this.w || 900, this.h || 600) * 0.32)));
|
||||
|
||||
let id = "relaxed";
|
||||
if (candidates.length) {
|
||||
candidates.sort((a, b) => b.priority - a.priority);
|
||||
id = candidates[0].id;
|
||||
}
|
||||
const next = this.colonyMoodDefinition(id);
|
||||
this.colonyMood = next;
|
||||
this.nextColonyMoodEvalAt = now + 1.25;
|
||||
return next;
|
||||
return global.TarinaiColonySituationSystem?.evaluate?.(this, force) || this.colonyMood || this.colonyMoodDefinition?.("relaxed");
|
||||
},
|
||||
|
||||
|
||||
observeHighlightKind(t) {
|
||||
const s = this.selected;
|
||||
if (!s || !t || t === s || t.dead || this.tool !== "observe") return "";
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ $Exact = @{
|
|||
"index.html" = "app DOM shell, canvas, panels, dialogs, and ordered script tags"
|
||||
"README.md" = "small AI entrypoint and read strategy"
|
||||
"AI_MAP.md" = "subsystem map and task-oriented read sets"
|
||||
"GAME_FEATURES.md" = "player-facing game features, UI, items, and behavior guide"
|
||||
"FILES.json" = "machine-readable file routing and glob semantics"
|
||||
"service-worker.js" = "generated offline/cache service worker"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ EXACT_DESCRIPTIONS = {
|
|||
"index.html": "app DOM shell, canvas, panels, dialogs, and ordered script tags",
|
||||
"README.md": "small AI entrypoint and read strategy",
|
||||
"AI_MAP.md": "subsystem map and task-oriented read sets",
|
||||
"GAME_FEATURES.md": "player-facing game features, UI, items, and behavior guide",
|
||||
"FILES.json": "machine-readable file routing and glob semantics",
|
||||
"service-worker.js": "generated offline/cache service worker",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -473,46 +473,56 @@ def check_current_save_format() -> None:
|
|||
placement = read_text(ROOT / "js/world_placement_log.js")
|
||||
if "playLogSoundForEntry" in placement:
|
||||
fail("log sound mapping still lives in world_placement_log.js")
|
||||
if "player_item_placement" in placement:
|
||||
fail("player item placement should not create placement log entries")
|
||||
ok("binary hash save snapshot, codec, storage, and restore coordinator present")
|
||||
|
||||
def check_unified_item_registry() -> None:
|
||||
data = manifest()
|
||||
js_files = data["js"]
|
||||
if "js/item_registry.js" not in js_files:
|
||||
fail("unified item registry missing from manifest")
|
||||
removed = ["js/food_registry.js", "js/effect_registry.js", "js/item_visual_registry.js"]
|
||||
stale_manifest = [rel for rel in removed if rel in js_files]
|
||||
if stale_manifest:
|
||||
fail("old split item registry files still listed in manifest: " + ", ".join(stale_manifest))
|
||||
stale_files = [rel for rel in removed if (ROOT / rel).exists()]
|
||||
if stale_files:
|
||||
fail("old split item registry files still exist: " + ", ".join(stale_files))
|
||||
source = read_text(ROOT / "js/item_registry.js")
|
||||
required = [
|
||||
"global.TarinaiItemRegistry",
|
||||
"const TOOL_DEFINITIONS",
|
||||
"function itemDefinition",
|
||||
"function itemVisualDefinition",
|
||||
"class FoodRegistry",
|
||||
"class EffectRegistry",
|
||||
"food: FOOD_REGISTRY",
|
||||
"effect: EFFECT_REGISTRY",
|
||||
"visual: itemVisualDefinition",
|
||||
required_files = [
|
||||
"js/item_tool_definitions.js",
|
||||
"js/item_visual_definitions.js",
|
||||
"js/item_food_definitions.js",
|
||||
"js/item_effect_definitions.js",
|
||||
"js/item_registry.js",
|
||||
]
|
||||
for token in required:
|
||||
missing = [rel for rel in required_files if rel not in js_files or not (ROOT / rel).exists()]
|
||||
if missing:
|
||||
fail("split item registry files missing: " + ", ".join(missing))
|
||||
order = [js_files.index(rel) for rel in required_files]
|
||||
if order != sorted(order):
|
||||
fail("split item registry files are not loaded before the facade in manifest order")
|
||||
stale_files = [rel for rel in ["js/food_registry.js", "js/effect_registry.js", "js/item_visual_registry.js"] if (ROOT / rel).exists() or rel in js_files]
|
||||
if stale_files:
|
||||
fail("obsolete item registry split files remain: " + ", ".join(stale_files))
|
||||
|
||||
tool_source = read_text(ROOT / "js/item_tool_definitions.js")
|
||||
visual_source = read_text(ROOT / "js/item_visual_definitions.js")
|
||||
food_source = read_text(ROOT / "js/item_food_definitions.js")
|
||||
effect_source = read_text(ROOT / "js/item_effect_definitions.js")
|
||||
facade_source = read_text(ROOT / "js/item_registry.js")
|
||||
required_pairs = [
|
||||
(tool_source, "const TOOL_DEFINITIONS"),
|
||||
(tool_source, "global.TarinaiToolRegistry"),
|
||||
(visual_source, "function itemVisualDefinition"),
|
||||
(visual_source, "global.TarinaiItemVisualDefinitions"),
|
||||
(food_source, "class FoodRegistry"),
|
||||
(food_source, "global.TarinaiFoodRegistry"),
|
||||
(effect_source, "class EffectRegistry"),
|
||||
(effect_source, "global.TarinaiEffectRegistry"),
|
||||
(facade_source, "global.TarinaiItemRegistry"),
|
||||
(facade_source, "definition: itemTypeDefinition"),
|
||||
]
|
||||
for source, token in required_pairs:
|
||||
if token not in source:
|
||||
fail(f"unified item registry token missing: {token}")
|
||||
forbidden_globals = ["global.TarinaiFoodRegistry", "global.TarinaiEffectRegistry", "global.TARINAI_ITEM_VISUALS", "global.TARINAI_FOOD_DEFINITIONS", "global.TARINAI_EFFECT_DEFINITIONS"]
|
||||
offenders = [token for token in forbidden_globals if token in source]
|
||||
if offenders:
|
||||
fail("old split item registry globals still exported: " + ", ".join(offenders))
|
||||
fail(f"split item registry token missing: {token}")
|
||||
data_source = read_text(ROOT / "js/data.js")
|
||||
forbidden_data = ["const TOOL_DEFINITIONS", "const ITEM_TRAITS", "class FoodRegistry", "class EffectRegistry", "TARINAI_ITEM_VISUALS"]
|
||||
offenders = [token for token in forbidden_data if token in data_source]
|
||||
if offenders:
|
||||
fail("item definitions still mixed into data.js: " + ", ".join(offenders))
|
||||
ok("item definitions centralized in item_registry.js")
|
||||
|
||||
ok("item registry responsibilities split behind facade")
|
||||
|
||||
def check_docs_removed() -> None:
|
||||
docs = ROOT / "docs"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "39.15.47";
|
||||
const APP_VERSION = "39.15.63";
|
||||
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", "registry_base", "disease_registry", "sound_pack", "item_registry", "data", "ground_types", "input_mode_manager", "math", "physics_helpers", "collision_footprint_system", "mechanical_system", "constraint_system", "physics_world_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_lifecycle_runtime", "item_update_policy", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_forced_action_planner_system", "tarinai_action_planner_system", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "world_update_phases", "system_order", "simulation", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "patch_helpers", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "registry_base", "disease_registry", "sound_pack", "deterministic_helpers", "item_tool_definitions", "item_visual_definitions", "item_food_definitions", "item_effect_definitions", "item_registry", "data", "ground_types", "input_mode_manager", "math", "collision_footprint_system", "physics_helpers", "placement_collision_system", "placement_preview_system", "pin_attachment_system", "physics_shape_editor_system", "mechanical_system", "mechanical_shape_bridge", "constraint_system", "physics_world_system", "physics_projection_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_lifecycle_runtime", "item_update_policy", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_nest_sleep_system", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_forced_action_planner_system", "tarinai_action_planner_system", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "impact_core_system", "impact_response_system", "collision_response_system", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "world_update_phases", "system_order", "simulation", "colony_situation_system", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "patch_helpers", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_pointer_action_system", "ui_input_touch", "ui_input_mouse", "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 = [
|
||||
"./",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue