hm
This commit is contained in:
parent
cefa2ace5a
commit
0e432c62ae
87 changed files with 4282 additions and 3066 deletions
|
|
@ -17,7 +17,7 @@
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"id": "root",
|
"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."
|
"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
|
## Read Strategy
|
||||||
|
|
||||||
- Start here for architecture, script order constraints, and where to look next.
|
- 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 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 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`.
|
- For machine-readable routing, read `FILES.json`.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "39.15.47",
|
"version": "39.15.63",
|
||||||
"css": [
|
"css": [
|
||||||
"css/base.css",
|
"css/base.css",
|
||||||
"css/layout.css",
|
"css/layout.css",
|
||||||
|
|
@ -13,16 +13,27 @@
|
||||||
"js/registry_base.js",
|
"js/registry_base.js",
|
||||||
"js/disease_registry.js",
|
"js/disease_registry.js",
|
||||||
"js/sound_pack.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/item_registry.js",
|
||||||
"js/data.js",
|
"js/data.js",
|
||||||
"js/ground_types.js",
|
"js/ground_types.js",
|
||||||
"js/input_mode_manager.js",
|
"js/input_mode_manager.js",
|
||||||
"js/math.js",
|
"js/math.js",
|
||||||
"js/physics_helpers.js",
|
|
||||||
"js/collision_footprint_system.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_system.js",
|
||||||
|
"js/mechanical_shape_bridge.js",
|
||||||
"js/constraint_system.js",
|
"js/constraint_system.js",
|
||||||
"js/physics_world_system.js",
|
"js/physics_world_system.js",
|
||||||
|
"js/physics_projection_system.js",
|
||||||
"js/assets.js",
|
"js/assets.js",
|
||||||
"js/audio.js",
|
"js/audio.js",
|
||||||
"js/perf_profiler.js",
|
"js/perf_profiler.js",
|
||||||
|
|
@ -63,6 +74,7 @@
|
||||||
"js/tarinai_needs_core.js",
|
"js/tarinai_needs_core.js",
|
||||||
"js/tarinai_behavior_text.js",
|
"js/tarinai_behavior_text.js",
|
||||||
"js/tarinai_forced_behavior.js",
|
"js/tarinai_forced_behavior.js",
|
||||||
|
"js/tarinai_nest_sleep_system.js",
|
||||||
"js/tarinai_item_targeting.js",
|
"js/tarinai_item_targeting.js",
|
||||||
"js/tarinai_consumable_behavior.js",
|
"js/tarinai_consumable_behavior.js",
|
||||||
"js/tarinai_social_action_runtime.js",
|
"js/tarinai_social_action_runtime.js",
|
||||||
|
|
@ -92,6 +104,9 @@
|
||||||
"js/world_view.js",
|
"js/world_view.js",
|
||||||
"js/family_graph.js",
|
"js/family_graph.js",
|
||||||
"js/world_family_social.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_combat_effects.js",
|
||||||
"js/world_environment.js",
|
"js/world_environment.js",
|
||||||
"js/world_spatial_budget.js",
|
"js/world_spatial_budget.js",
|
||||||
|
|
@ -110,6 +125,7 @@
|
||||||
"js/world_update_phases.js",
|
"js/world_update_phases.js",
|
||||||
"js/system_order.js",
|
"js/system_order.js",
|
||||||
"js/simulation.js",
|
"js/simulation.js",
|
||||||
|
"js/colony_situation_system.js",
|
||||||
"js/world_update.js",
|
"js/world_update.js",
|
||||||
"js/world_tool_actions.js",
|
"js/world_tool_actions.js",
|
||||||
"js/world_placement_log.js",
|
"js/world_placement_log.js",
|
||||||
|
|
@ -133,6 +149,7 @@
|
||||||
"js/ui_ground.js",
|
"js/ui_ground.js",
|
||||||
"js/ui_tools.js",
|
"js/ui_tools.js",
|
||||||
"js/ui_input_shared.js",
|
"js/ui_input_shared.js",
|
||||||
|
"js/ui_pointer_action_system.js",
|
||||||
"js/ui_input_touch.js",
|
"js/ui_input_touch.js",
|
||||||
"js/ui_input_mouse.js",
|
"js/ui_input_mouse.js",
|
||||||
"js/ui_bind.js",
|
"js/ui_bind.js",
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,7 @@
|
||||||
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
||||||
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
||||||
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
||||||
|
.log-push-toast.log-push-event::before { background: #8b82d6; }
|
||||||
.log-push-toast.leaving {
|
.log-push-toast.leaving {
|
||||||
animation: logPushOut .18s ease both;
|
animation: logPushOut .18s ease both;
|
||||||
}
|
}
|
||||||
|
|
@ -684,60 +685,6 @@
|
||||||
outline: 2px solid rgba(103,178,44,0.58);
|
outline: 2px solid rgba(103,178,44,0.58);
|
||||||
outline-offset: 2px;
|
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. */
|
/* v36: pin only the operation tool category under the quick-scroll tabs. */
|
||||||
.tool-category[data-tool-category="operate"]:not(.collapsed) {
|
.tool-category[data-tool-category="operate"]:not(.collapsed) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
.tool[data-tool="poke"] { --tool-emoji: "\1F448"; }
|
.tool[data-tool="poke"] { --tool-emoji: "\1F448"; }
|
||||||
.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; }
|
.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; }
|
||||||
.tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; }
|
.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="copy"] { --tool-emoji: "\29c9 "; }
|
||||||
.tool[data-tool="area_delete"] { --tool-emoji: "\d7 "; }
|
.tool[data-tool="area_delete"] { --tool-emoji: "\d7 "; }
|
||||||
.tool[data-tool="rope"] { --tool-emoji: "\2570 "; }
|
.tool[data-tool="rope"] { --tool-emoji: "\2570 "; }
|
||||||
|
|
@ -322,6 +323,7 @@ meter {
|
||||||
.log-weather { border-left-color: rgba(92, 142, 210, 0.62); }
|
.log-weather { border-left-color: rgba(92, 142, 210, 0.62); }
|
||||||
.log-relation { border-left-color: rgba(204, 126, 176, 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-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) {
|
@media (max-width: 980px) {
|
||||||
#app { padding: 10px; }
|
#app { padding: 10px; }
|
||||||
|
|
@ -362,4 +364,3 @@ meter {
|
||||||
@media (min-width: 921px) {
|
@media (min-width: 921px) {
|
||||||
.mobile-mode-controls { display: none !important; }
|
.mobile-mode-controls { display: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
.log-push-toast.log-push-food::before { background: #e9bd61; }
|
||||||
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
|
||||||
.log-push-toast.log-push-grass::before { background: #8cc77a; }
|
.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; }
|
.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); } }
|
@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.touch-mobile-ui .mobile-mode-controls:not([hidden]) { display: flex; }
|
||||||
body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; }
|
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-camera #gameCanvas { cursor: grab; }
|
||||||
body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
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. */
|
/* 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) {
|
@media (min-width: 981px) {
|
||||||
.panel-quick-tabs {
|
.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="game"] {
|
||||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -402,10 +442,9 @@ body.touch-mobile-ui .tool-tip-popover {
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
.panel-quick-tabs {
|
.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="game"] {
|
||||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -474,10 +513,9 @@ body.touch-mobile-ui .tool-tip-popover {
|
||||||
.panel-quick-tabs {
|
.panel-quick-tabs {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
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="game"] {
|
||||||
.panel-quick-tabs [data-panel-tab="tools"] {
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.lineage-main-card {
|
.lineage-main-card {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.lineage-head h2 { margin-bottom: 0; }
|
.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 {
|
.lineage-main-content {
|
||||||
height: min(78vh, 980px);
|
height: min(78vh, 980px);
|
||||||
|
|
@ -125,8 +133,8 @@ body::before {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1px 5px;
|
gap: 1px 5px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 9px;
|
font-size: 9.7px;
|
||||||
line-height: 1.12;
|
line-height: 1.13;
|
||||||
color: rgba(88, 70, 48, 0.86);
|
color: rgba(88, 70, 48, 0.86);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
@ -147,8 +155,8 @@ body::before {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
max-width: 82px;
|
max-width: 82px;
|
||||||
font-size: 9px;
|
font-size: 9.7px;
|
||||||
line-height: 1.12;
|
line-height: 1.13;
|
||||||
color: rgba(88, 70, 48, 0.82);
|
color: rgba(88, 70, 48, 0.82);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -212,15 +220,15 @@ body::before {
|
||||||
background: rgba(103,178,44,0.11);
|
background: rgba(103,178,44,0.11);
|
||||||
border: 1px solid rgba(103,178,44,0.20);
|
border: 1px solid rgba(103,178,44,0.20);
|
||||||
color: rgba(64,72,48,0.86);
|
color: rgba(64,72,48,0.86);
|
||||||
font-size: 7.5px;
|
font-size: 8.2px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.family-copy strong { font-size: 11px; }
|
.family-copy strong { font-size: 12.5px; }
|
||||||
.family-copy span { color: var(--muted); font-size: 8.5px; margin-top: 1px; }
|
.family-copy span { color: var(--muted); font-size: 9.6px; margin-top: 1px; }
|
||||||
|
|
||||||
|
|
||||||
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
|
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
|
||||||
|
|
@ -440,7 +448,7 @@ body::before {
|
||||||
width: 54px;
|
width: 54px;
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(255,255,255,0.70);
|
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="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="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="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/base.css?v=39.15.63" />
|
||||||
<link rel="stylesheet" href="css/layout.css?v=39.15.47" />
|
<link rel="stylesheet" href="css/layout.css?v=39.15.63" />
|
||||||
<link rel="stylesheet" href="css/panel.css?v=39.15.47" />
|
<link rel="stylesheet" href="css/panel.css?v=39.15.63" />
|
||||||
<link rel="stylesheet" href="css/components.css?v=39.15.47" />
|
<link rel="stylesheet" href="css/components.css?v=39.15.63" />
|
||||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.47" />
|
<link rel="stylesheet" href="css/mobile.css?v=39.15.63" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||||
|
|
@ -37,11 +37,11 @@
|
||||||
<button id="speedBtn" class="btn">速度 x1</button>
|
<button id="speedBtn" class="btn">速度 x1</button>
|
||||||
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
<button id="ecologyBtn" class="btn">たりないの生態</button>
|
||||||
<div id="soundMenu" class="sound-menu">
|
<div id="soundMenu" class="sound-menu">
|
||||||
<button id="soundBtn" class="btn sound-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">音設定 ON</button>
|
<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="効果音設定">
|
<div id="soundPanel" class="sound-panel hidden" role="menu" aria-label="音">
|
||||||
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
|
<label class="sound-master"><input id="soundMasterToggle" type="checkbox" checked><span>効果音</span></label>
|
||||||
<div class="sound-panel-divider"></div>
|
<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="notify" checked><span>通知</span></label>
|
||||||
<label><input type="checkbox" data-sound-category="ops" checked><span>操作</span></label>
|
<label><input type="checkbox" data-sound-category="ops" checked><span>操作</span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -60,6 +60,10 @@
|
||||||
<button type="button" data-mobile-mode="camera">カメラ</button>
|
<button type="button" data-mobile-mode="camera">カメラ</button>
|
||||||
<button type="button" data-mobile-mode="tool">道具</button>
|
<button type="button" data-mobile-mode="tool">道具</button>
|
||||||
</div>
|
</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>
|
<div id="toast" class="toast hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -118,6 +122,7 @@
|
||||||
<label><input type="checkbox" data-push-kind="food"> 食事</label>
|
<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="weather"> 天気</label>
|
||||||
<label><input type="checkbox" data-push-kind="grass"> 草</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>
|
<label><input type="checkbox" data-push-kind="relation"> 関係</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -130,7 +135,7 @@
|
||||||
|
|
||||||
<section class="card lineage-main-card">
|
<section class="card lineage-main-card">
|
||||||
<div class="lineage-head">
|
<div class="lineage-head">
|
||||||
<h2>家系図</h2>
|
<h2>家系図<span class="lineage-title-note">重い場合があるので注意</span></h2>
|
||||||
<div class="lineage-actions">
|
<div class="lineage-actions">
|
||||||
<button id="archiveUpdateToggleBtn" class="btn" type="button" aria-pressed="false">更新 OFF</button>
|
<button id="archiveUpdateToggleBtn" class="btn" type="button" aria-pressed="false">更新 OFF</button>
|
||||||
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
<button id="archiveResetBtn" class="btn danger" type="button">家系図をリセット</button>
|
||||||
|
|
@ -196,141 +201,158 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></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/version.js?v=39.15.63" defer></script>
|
||||||
<script src="js/event_bus.js?v=39.15.47" defer></script>
|
<script src="js/event_bus.js?v=39.15.63" defer></script>
|
||||||
<script src="js/registry_base.js?v=39.15.47" defer></script>
|
<script src="js/registry_base.js?v=39.15.63" defer></script>
|
||||||
<script src="js/disease_registry.js?v=39.15.47" defer></script>
|
<script src="js/disease_registry.js?v=39.15.63" defer></script>
|
||||||
<script src="js/sound_pack.js?v=39.15.47" defer></script>
|
<script src="js/sound_pack.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_registry.js?v=39.15.47" defer></script>
|
<script src="js/deterministic_helpers.js?v=39.15.63" defer></script>
|
||||||
<script src="js/data.js?v=39.15.47" defer></script>
|
<script src="js/item_tool_definitions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ground_types.js?v=39.15.47" defer></script>
|
<script src="js/item_visual_definitions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/input_mode_manager.js?v=39.15.47" defer></script>
|
<script src="js/item_food_definitions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/math.js?v=39.15.47" defer></script>
|
<script src="js/item_effect_definitions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/physics_helpers.js?v=39.15.47" defer></script>
|
<script src="js/item_registry.js?v=39.15.63" defer></script>
|
||||||
<script src="js/collision_footprint_system.js?v=39.15.47" defer></script>
|
<script src="js/data.js?v=39.15.63" defer></script>
|
||||||
<script src="js/mechanical_system.js?v=39.15.47" defer></script>
|
<script src="js/ground_types.js?v=39.15.63" defer></script>
|
||||||
<script src="js/constraint_system.js?v=39.15.47" defer></script>
|
<script src="js/input_mode_manager.js?v=39.15.63" defer></script>
|
||||||
<script src="js/physics_world_system.js?v=39.15.47" defer></script>
|
<script src="js/math.js?v=39.15.63" defer></script>
|
||||||
<script src="js/assets.js?v=39.15.47" defer></script>
|
<script src="js/collision_footprint_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/audio.js?v=39.15.47" defer></script>
|
<script src="js/physics_helpers.js?v=39.15.63" defer></script>
|
||||||
<script src="js/perf_profiler.js?v=39.15.47" defer></script>
|
<script src="js/placement_collision_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/render.js?v=39.15.47" defer></script>
|
<script src="js/placement_preview_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/sim_core.js?v=39.15.47" defer></script>
|
<script src="js/pin_attachment_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_seed_factory.js?v=39.15.47" defer></script>
|
<script src="js/physics_shape_editor_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/structures.js?v=39.15.47" defer></script>
|
<script src="js/mechanical_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/items.js?v=39.15.47" defer></script>
|
<script src="js/mechanical_shape_bridge.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_type_initializers.js?v=39.15.47" defer></script>
|
<script src="js/constraint_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_support.js?v=39.15.47" defer></script>
|
<script src="js/physics_world_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_runtime.js?v=39.15.47" defer></script>
|
<script src="js/physics_projection_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_update_policy.js?v=39.15.47" defer></script>
|
<script src="js/assets.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_tool_system.js?v=39.15.47" defer></script>
|
<script src="js/audio.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_ball_system.js?v=39.15.47" defer></script>
|
<script src="js/perf_profiler.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.47" defer></script>
|
<script src="js/render.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_pin_system.js?v=39.15.47" defer></script>
|
<script src="js/sim_core.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_seed_factory.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_dynamic_system.js?v=39.15.47" defer></script>
|
<script src="js/structures.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.47" defer></script>
|
<script src="js/items.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.47" defer></script>
|
<script src="js/item_type_initializers.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_support.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_runtime.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.47" defer></script>
|
<script src="js/item_update_policy.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_tool_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_ball_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_runtime.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_duplicator_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/structure_lifecycle.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_pin_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_render_runtime.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_zunchi_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ants.js?v=39.15.47" defer></script>
|
<script src="js/item_dynamic_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/health.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_decay_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_growth_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_action_spec.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_step_frame.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_behavior_state.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_step_decay.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_identity_social.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_action_state.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_step_growth.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_disease_nest.js?v=39.15.47" defer></script>
|
<script src="js/item_lifecycle_pipeline.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_item_effects.js?v=39.15.47" defer></script>
|
<script src="js/item_runtime.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_needs_core.js?v=39.15.47" defer></script>
|
<script src="js/structure_lifecycle.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_behavior_text.js?v=39.15.47" defer></script>
|
<script src="js/item_render_runtime.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_forced_behavior.js?v=39.15.47" defer></script>
|
<script src="js/ants.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_item_targeting.js?v=39.15.47" defer></script>
|
<script src="js/health.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.47" defer></script>
|
<script src="js/tarinai.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_action_spec.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_building_behavior.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_behavior_state.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_action_definitions.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_identity_social.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_needs_items.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_action_state.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_disease_nest.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_action_planner_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_item_effects.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_need_planner_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_needs_core.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_behavior_text.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_forced_behavior.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_contact_item_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_nest_sleep_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_item_targeting.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_sunbath_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_consumable_behavior.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_social_action_runtime.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_local_environment_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_building_behavior.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_social_move_life.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_action_definitions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_step_frame.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_needs_items.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_step_ai.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_step_environment.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_action_planner_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_step_movement.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_need_planner_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_step_health.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_item_interaction_context.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_pipeline.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_food_interaction_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_runtime.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_contact_item_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_render.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_item_interaction_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_sunbath_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_view.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_cursor_care_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/family_graph.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_local_environment_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_family_social.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_social_move_life.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_combat_effects.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_step_frame.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_environment.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_step_ai.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_spatial_budget.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_step_environment.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_ants_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_step_movement.js?v=39.15.63" defer></script>
|
||||||
<script src="js/weather_system.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_step_health.js?v=39.15.63" defer></script>
|
||||||
<script src="js/item_update_scheduler.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_pipeline.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_runtime_helpers.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_runtime.js?v=39.15.63" defer></script>
|
||||||
<script src="js/tarinai_update_policy.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_render.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_environment_system.js?v=39.15.47" defer></script>
|
<script src="js/world.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_item_ant_system.js?v=39.15.47" defer></script>
|
<script src="js/world_view.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_effects_system.js?v=39.15.47" defer></script>
|
<script src="js/family_graph.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_creature_system.js?v=39.15.47" defer></script>
|
<script src="js/world_family_social.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_maintenance_system.js?v=39.15.47" defer></script>
|
<script src="js/impact_core_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_ambient_system.js?v=39.15.47" defer></script>
|
<script src="js/impact_response_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation_systems.js?v=39.15.47" defer></script>
|
<script src="js/collision_response_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_update_phases.js?v=39.15.47" defer></script>
|
<script src="js/world_combat_effects.js?v=39.15.63" defer></script>
|
||||||
<script src="js/system_order.js?v=39.15.47" defer></script>
|
<script src="js/world_environment.js?v=39.15.63" defer></script>
|
||||||
<script src="js/simulation.js?v=39.15.47" defer></script>
|
<script src="js/world_spatial_budget.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_update.js?v=39.15.47" defer></script>
|
<script src="js/world_ants_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_tool_actions.js?v=39.15.47" defer></script>
|
<script src="js/weather_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_placement_log.js?v=39.15.47" defer></script>
|
<script src="js/item_update_scheduler.js?v=39.15.63" defer></script>
|
||||||
<script src="js/world_event_effects.js?v=39.15.47" defer></script>
|
<script src="js/simulation_runtime_helpers.js?v=39.15.63" defer></script>
|
||||||
<script src="js/command_dispatcher.js?v=39.15.47" defer></script>
|
<script src="js/tarinai_update_policy.js?v=39.15.63" defer></script>
|
||||||
<script src="js/text_catalog.js?v=39.15.47" defer></script>
|
<script src="js/simulation_environment_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui.js?v=39.15.47" defer></script>
|
<script src="js/simulation_item_ant_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_log.js?v=39.15.47" defer></script>
|
<script src="js/simulation_effects_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_helpers.js?v=39.15.47" defer></script>
|
<script src="js/simulation_creature_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_selected.js?v=39.15.47" defer></script>
|
<script src="js/simulation_maintenance_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/save_schema.js?v=39.15.47" defer></script>
|
<script src="js/simulation_ambient_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/snapshot_system.js?v=39.15.47" defer></script>
|
<script src="js/simulation_systems.js?v=39.15.63" defer></script>
|
||||||
<script src="js/restore_coordinator.js?v=39.15.47" defer></script>
|
<script src="js/world_update_phases.js?v=39.15.63" defer></script>
|
||||||
<script src="js/history_system.js?v=39.15.47" defer></script>
|
<script src="js/system_order.js?v=39.15.63" defer></script>
|
||||||
<script src="js/save_codec.js?v=39.15.47" defer></script>
|
<script src="js/simulation.js?v=39.15.63" defer></script>
|
||||||
<script src="js/save_storage.js?v=39.15.47" defer></script>
|
<script src="js/colony_situation_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/save_system.js?v=39.15.47" defer></script>
|
<script src="js/world_update.js?v=39.15.63" defer></script>
|
||||||
<script src="js/patch_helpers.js?v=39.15.47" defer></script>
|
<script src="js/world_tool_actions.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_tooltips.js?v=39.15.47" defer></script>
|
<script src="js/world_placement_log.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_layout_dialogs.js?v=39.15.47" defer></script>
|
<script src="js/world_event_effects.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_ground.js?v=39.15.47" defer></script>
|
<script src="js/command_dispatcher.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_tools.js?v=39.15.47" defer></script>
|
<script src="js/text_catalog.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_input_shared.js?v=39.15.47" defer></script>
|
<script src="js/ui.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_input_touch.js?v=39.15.47" defer></script>
|
<script src="js/ui_log.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_input_mouse.js?v=39.15.47" defer></script>
|
<script src="js/ui_helpers.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_bind.js?v=39.15.47" defer></script>
|
<script src="js/ui_selected.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_charts.js?v=39.15.47" defer></script>
|
<script src="js/save_schema.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_family_data.js?v=39.15.47" defer></script>
|
<script src="js/snapshot_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_family_async.js?v=39.15.47" defer></script>
|
<script src="js/restore_coordinator.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_family_layout.js?v=39.15.47" defer></script>
|
<script src="js/history_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_family_paths.js?v=39.15.47" defer></script>
|
<script src="js/save_codec.js?v=39.15.63" defer></script>
|
||||||
<script src="js/ui_family_render.js?v=39.15.47" defer></script>
|
<script src="js/save_storage.js?v=39.15.63" defer></script>
|
||||||
<script src="js/main.js?v=39.15.47" defer></script>
|
<script src="js/save_system.js?v=39.15.63" defer></script>
|
||||||
<script src="js/debug_tools.js?v=39.15.47" 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,18 @@
|
||||||
return Math.max(14, num(item.r, radiusFor(item.type, 12)) || radiusFor(item.type, 12));
|
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 = {}) {
|
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
||||||
if (!item || item.dead) return { hit: false, distance: Infinity };
|
if (!item || item.dead) return { hit: false, distance: Infinity };
|
||||||
const padding = Math.max(0, num(opts.padding, 0));
|
const padding = Math.max(0, num(opts.padding, 0));
|
||||||
|
|
@ -188,24 +200,17 @@
|
||||||
|
|
||||||
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
||||||
if (!worldRef || !item || item.dead) return true;
|
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)));
|
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;
|
if (!important.has(item.type) && !isPhysicsItem) return false;
|
||||||
const placingGrass = item.type === "grass";
|
const itemRects = placementRectsFor(worldRef, item);
|
||||||
const itemRects = worldRef.solidObstacleRects?.(item) || [];
|
|
||||||
const itemRadius = itemFootprintRadius(worldRef, item, itemRects);
|
const itemRadius = itemFootprintRadius(worldRef, item, itemRects);
|
||||||
const search = Math.max(132, itemRadius * 3.2);
|
const search = Math.max(132, itemRadius * 3.2);
|
||||||
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
||||||
if (!other || other === item || other.dead) continue;
|
if (!other || other === item || other.dead) continue;
|
||||||
if (isPhysicsItem && (other.type === "water" || other.type === "grass_bed")) continue;
|
if (isSoftPlacementIgnoredType(other.type)) 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 (!important.has(other.type)) continue;
|
if (!important.has(other.type)) continue;
|
||||||
if (!placingGrass && other.type === "grass") continue;
|
const otherRects = placementRectsFor(worldRef, other);
|
||||||
const otherRects = worldRef.solidObstacleRects?.(other) || [];
|
|
||||||
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
||||||
const hasMechanism = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || global.TarinaiMechanicalSystem?.isMechanicalType?.(other.type);
|
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 || ""));
|
const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || ""));
|
||||||
|
|
@ -240,6 +245,8 @@
|
||||||
rectsOverlap,
|
rectsOverlap,
|
||||||
rectCircleOverlap,
|
rectCircleOverlap,
|
||||||
itemFootprintRadius,
|
itemFootprintRadius,
|
||||||
|
isSoftPlacementIgnoredType,
|
||||||
|
placementRectsFor,
|
||||||
hitTestItem,
|
hitTestItem,
|
||||||
itemPlacementOverlapBlocked,
|
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;
|
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) {
|
function rectNearPointAabb(rect, x, y, padding = 0) {
|
||||||
if (!rect) return false;
|
if (!rect) return false;
|
||||||
|
|
@ -81,7 +74,8 @@
|
||||||
item._lastConstraintSpatialDirtyY = y;
|
item._lastConstraintSpatialDirtyY = y;
|
||||||
item._lastConstraintSpatialDirtyR = r;
|
item._lastConstraintSpatialDirtyR = r;
|
||||||
item._nextConstraintSpatialDirtyAt = now + Math.max(0.016, Number(minInterval || 0.065) || 0.065);
|
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;
|
if (world.constraintDirtyStatsThisFrame) world.constraintDirtyStatsThisFrame.marked = (world.constraintDirtyStatsThisFrame.marked || 0) + 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +321,17 @@
|
||||||
return { x: (obj.x || 0) + lx * c - ly * s, y: (obj.y || 0) + lx * s + ly * c, obj };
|
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) {
|
function endpointMass(obj) {
|
||||||
if (!obj || obj.dead) return Infinity;
|
if (!obj || obj.dead) return Infinity;
|
||||||
if (obj._heldByPlayer || obj.playerHeld) return Infinity;
|
if (obj._heldByPlayer || obj.playerHeld) return Infinity;
|
||||||
|
|
@ -337,7 +342,7 @@
|
||||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
||||||
if (obj.type === "rotator") return Infinity; // anchored pivot: links may torque it, not translate it
|
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 === "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;
|
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;
|
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;
|
return changed;
|
||||||
}
|
}
|
||||||
if (obj.radius && obj.name) {
|
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.x += moveX;
|
||||||
obj.y += moveY;
|
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) {
|
if (obj.state === "sleep" || obj.sleeping) {
|
||||||
global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(obj, dt, `constraint-${mode}`);
|
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));
|
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;
|
obj.y = next.y;
|
||||||
}
|
}
|
||||||
const changed = Math.abs(ps(obj, "railPhase", 0) - beforePhase) > 0.0001;
|
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");
|
if (changed) commitConstraintMovedMechanical(obj, "constraint-reciprocator-project");
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
@ -417,25 +416,6 @@
|
||||||
obj.prevY = beforeY;
|
obj.prevY = beforeY;
|
||||||
obj.x = beforeX + px;
|
obj.x = beforeX + px;
|
||||||
obj.y = beforeY + py;
|
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");
|
commitConstraintMovedMechanical(obj, "constraint-poison-project");
|
||||||
global.TarinaiMechanicalSystem?.wakeItem?.(obj, "constraint-poison-project");
|
global.TarinaiMechanicalSystem?.wakeItem?.(obj, "constraint-poison-project");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -458,9 +438,6 @@
|
||||||
obj.prevY = obj.y;
|
obj.prevY = obj.y;
|
||||||
obj.x += moveX;
|
obj.x += moveX;
|
||||||
obj.y += moveY;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -617,6 +594,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function rectLocalPointForConstraint(r, x, y) {
|
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 dx = x - (r.cx ?? 0);
|
||||||
const dy = y - (r.cy ?? 0);
|
const dy = y - (r.cy ?? 0);
|
||||||
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 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; }
|
if (d < 0.001) { dx = 0; dy = -1; d = 1; }
|
||||||
nx = dx / d; ny = dy / d; overlap = radius - d;
|
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.x += nx * push;
|
||||||
p.y += ny * push;
|
p.y += ny * push;
|
||||||
// Do not let collision projection become a full Verlet velocity on the next frame.
|
// Do not let collision projection become a full Verlet velocity on the next frame.
|
||||||
|
|
@ -785,15 +764,10 @@
|
||||||
if (d >= minD) continue;
|
if (d >= minD) continue;
|
||||||
if (d < 0.001) { d = 1; }
|
if (d < 0.001) { d = 1; }
|
||||||
const nx = dx / d || 0, ny = dy / 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));
|
const push = Math.min(Math.max(0, minD - d), Math.max(3, minD * 0.48));
|
||||||
p.x += nx * push * 0.82;
|
p.x += nx * push;
|
||||||
p.y += ny * push * 0.82;
|
p.y += ny * push;
|
||||||
t.x -= nx * push * 0.10;
|
c = push > 0.001 || c;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
for (const rect of getRopeObstacleRects()) {
|
for (const rect of getRopeObstacleRects()) {
|
||||||
if (rect?.item?.id && endpointIds.has(rect.item.id)) continue;
|
if (rect?.item?.id && endpointIds.has(rect.item.id)) continue;
|
||||||
|
|
@ -858,7 +832,7 @@
|
||||||
item.world = worldRef || item.world || null;
|
item.world = worldRef || item.world || null;
|
||||||
const a = endpointWorld(ep(item, 0), worldRef);
|
const a = endpointWorld(ep(item, 0), worldRef);
|
||||||
const b = endpointWorld(ep(item, 1), 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;
|
item.amount = 0;
|
||||||
if (worldRef) worldRef.drawListDirty = true;
|
if (worldRef) worldRef.drawListDirty = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -870,7 +844,7 @@
|
||||||
let changed = applyDistanceConstraint(item, a, b, dt, length, "rope");
|
let changed = applyDistanceConstraint(item, a, b, dt, length, "rope");
|
||||||
const aa = changed ? endpointWorld(ep(item, 0), worldRef) : a;
|
const aa = changed ? endpointWorld(ep(item, 0), worldRef) : a;
|
||||||
const bb = changed ? endpointWorld(ep(item, 1), worldRef) : b;
|
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;
|
item.amount = 0;
|
||||||
if (worldRef) worldRef.drawListDirty = true;
|
if (worldRef) worldRef.drawListDirty = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -893,7 +867,7 @@
|
||||||
item.world = worldRef || item.world || null;
|
item.world = worldRef || item.world || null;
|
||||||
const a = endpointWorld(ep(item, 0), worldRef);
|
const a = endpointWorld(ep(item, 0), worldRef);
|
||||||
const b = endpointWorld(ep(item, 1), 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;
|
item.amount = 0;
|
||||||
if (worldRef) worldRef.drawListDirty = true;
|
if (worldRef) worldRef.drawListDirty = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -949,8 +923,13 @@
|
||||||
|
|
||||||
function endpointRuntimeStamp(endpoint, worldRef) {
|
function endpointRuntimeStamp(endpoint, worldRef) {
|
||||||
const obj = resolveEndpointObject(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);
|
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)}`;
|
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 || ""}`;
|
if (obj.radius && obj.name) stamp += `:${q(obj.vx, 4)}:${q(obj.vy, 4)}:${obj.state || ""}`;
|
||||||
else if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
else if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
|
||||||
|
|
@ -1036,6 +1015,8 @@
|
||||||
worldRef._constraintWorldStats = { visited: 0, ran: 0, skipped: 0, changed: 0, maxLinks, totalLinks: 0 };
|
worldRef._constraintWorldStats = { visited: 0, ran: 0, skipped: 0, changed: 0, maxLinks, totalLinks: 0 };
|
||||||
return 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 cursor = Math.max(0, Math.min(links.length - 1, Number(worldRef._constraintBudgetCursor || 0) || 0));
|
||||||
const ordered = links.slice(cursor).concat(links.slice(0, cursor));
|
const ordered = links.slice(cursor).concat(links.slice(0, cursor));
|
||||||
let ran = 0;
|
let ran = 0;
|
||||||
|
|
@ -1055,6 +1036,8 @@
|
||||||
}
|
}
|
||||||
pending.push({ item, score: linkScheduleScore(item, worldRef) });
|
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)));
|
pending.sort((a, b) => (b.score - a.score) || ((a.item.id || 0) - (b.item.id || 0)));
|
||||||
for (const entry of pending) {
|
for (const entry of pending) {
|
||||||
if (ran >= maxLinks) break;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore(worldRef, entry, source) {
|
function restore(worldRef, entry) {
|
||||||
if (!worldRef || !entry?.snapshot || !global.TarinaiRestoreCoordinator?.restoreSnapshot) return false;
|
if (!worldRef || !entry?.snapshot || !global.TarinaiRestoreCoordinator?.restoreSnapshot) return false;
|
||||||
worldRef._historyRestoring = true;
|
worldRef._historyRestoring = true;
|
||||||
try {
|
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));
|
worldRef._lastUndoSnapshotKey = snapshotKey(global.TarinaiSnapshot?.createSnapshot?.(worldRef));
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
const prev = world._undoStack.pop();
|
const prev = world._undoStack.pop();
|
||||||
if (current) world._redoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "redo", time: world.time || 0 });
|
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);
|
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 || "" };
|
return { ok, label: prev.label || "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
const next = world._redoStack.pop();
|
const next = world._redoStack.pop();
|
||||||
if (current) world._undoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "undo", time: world.time || 0 });
|
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);
|
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 || "" };
|
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() {
|
constructor() {
|
||||||
this.storageKey = STORAGE_KEY;
|
this.storageKey = STORAGE_KEY;
|
||||||
this.currentMode = this.loadMode();
|
this.currentMode = this.loadMode();
|
||||||
this.gestureState = {
|
|
||||||
active: false,
|
|
||||||
kind: "",
|
|
||||||
startedAt: 0,
|
|
||||||
pointerCount: 0,
|
|
||||||
};
|
|
||||||
this.classification = this.classify();
|
this.classification = this.classify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +23,6 @@
|
||||||
setMode(mode = "auto") {
|
setMode(mode = "auto") {
|
||||||
this.currentMode = MODES.includes(mode) ? mode : "auto";
|
this.currentMode = MODES.includes(mode) ? mode : "auto";
|
||||||
try { global.localStorage?.setItem(STORAGE_KEY, this.currentMode); } catch (_) {}
|
try { global.localStorage?.setItem(STORAGE_KEY, this.currentMode); } catch (_) {}
|
||||||
this.emit("input:mode", this.snapshot());
|
|
||||||
return this.currentMode;
|
return this.currentMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,54 +60,15 @@
|
||||||
refresh() {
|
refresh() {
|
||||||
const previous = this.classification;
|
const previous = this.classification;
|
||||||
const next = this.classify();
|
const next = this.classify();
|
||||||
if (JSON.stringify(previous) !== JSON.stringify(next)) this.emit("input:classification", this.snapshot());
|
|
||||||
return next;
|
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() {
|
snapshot() {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...this.classification,
|
...this.classification,
|
||||||
currentMode: this.currentMode,
|
currentMode: this.currentMode,
|
||||||
gestureState: { ...this.gestureState },
|
|
||||||
canPanCanvas: this.canPanCanvas(),
|
|
||||||
canPlaceTools: this.canPlaceTools(),
|
|
||||||
canFamilyTreeCaptureTouch: this.canFamilyTreeCaptureTouch(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(type, detail) {
|
|
||||||
global.TarinaiEvents?.emit?.(type, detail);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global.InputModeManager = InputModeManager;
|
global.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 < 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; }
|
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);
|
resolveObstacleCollisions(item, dt, worldRef);
|
||||||
|
global.TarinaiItemDynamicPinSystem?.tryBallPickup?.(item, worldRef);
|
||||||
item.x = clamp(item.x, p, worldRef.w - p);
|
item.x = clamp(item.x, p, worldRef.w - p);
|
||||||
item.y = clamp(item.y, p, worldRef.h - p);
|
item.y = clamp(item.y, p, worldRef.h - p);
|
||||||
const groundFriction = Math.pow(0.72, dt);
|
const groundFriction = Math.pow(0.72, dt);
|
||||||
item.vx *= groundFriction;
|
item.vx *= groundFriction;
|
||||||
item.vy *= 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.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);
|
item.spinVelocity *= Math.pow(0.65, dt);
|
||||||
if (Math.hypot(item.vx || 0, item.vy || 0) < 0.18) { item.vx = 0; item.vy = 0; }
|
if (Math.hypot(item.vx || 0, item.vy || 0) < 0.18) { item.vx = 0; item.vy = 0; }
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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
|
// Layer: entity-runtime/items/dynamic-system
|
||||||
|
|
@ -35,7 +6,121 @@
|
||||||
// behavior. Prototype methods delegate here.
|
// behavior. Prototype methods delegate here.
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function isPin(item) {
|
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) {
|
function update(item, dt, worldRef) {
|
||||||
|
|
@ -45,8 +130,13 @@
|
||||||
updateLodged(item, dt, worldRef);
|
updateLodged(item, dt, worldRef);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (item.pinState === "ball_lodged") {
|
||||||
|
updateBallLodged(item, dt, worldRef);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (global.TarinaiPhysics?.applyKinematicItemMotion) {
|
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.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 {
|
} else {
|
||||||
item.prevX = item.x;
|
item.prevX = item.x;
|
||||||
item.prevY = item.y;
|
item.prevY = item.y;
|
||||||
|
|
@ -77,6 +167,17 @@
|
||||||
return attach(item, best, worldRef, bestD);
|
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) {
|
function attach(item, t, worldRef, d = null) {
|
||||||
if (!t || t.dead) return false;
|
if (!t || t.dead) return false;
|
||||||
if (t.stuckPushpinId && t.stuckPushpinId !== item.id) return false;
|
if (t.stuckPushpinId && t.stuckPushpinId !== item.id) return false;
|
||||||
|
|
@ -88,12 +189,16 @@
|
||||||
item.pinState = "lodged";
|
item.pinState = "lodged";
|
||||||
item.deletable = false;
|
item.deletable = false;
|
||||||
item.pinTargetId = t.id;
|
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 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.pinVisualSide = visualSide;
|
||||||
item.pinAttachAngle = isOshibyo ? -0.46 : Math.atan2(dy || -1, dx || visualSide);
|
const oshibyoAnchor = isOshibyo ? (sharedAnchor || oshibyoAnchorFor(t, visualSide, item)) : null;
|
||||||
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.pinAttachAngle = isOshibyo ? oshibyoAnchor.angle : (sharedAnchor?.angle ?? Math.atan2(dy || -1, dx || visualSide));
|
||||||
item.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.40 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
|
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.pinDamageTick = 0;
|
||||||
item.pinFallCheckTimer = 0;
|
item.pinFallCheckTimer = 0;
|
||||||
item.vx = 0;
|
item.vx = 0;
|
||||||
|
|
@ -102,8 +207,8 @@
|
||||||
item.spin = item.pinAttachAngle;
|
item.spin = item.pinAttachAngle;
|
||||||
t.stuckPushpinId = item.id;
|
t.stuckPushpinId = item.id;
|
||||||
t.sleeping = false;
|
t.sleeping = false;
|
||||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
if (!isOshibyo) t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
|
||||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, isOshibyo ? 0.35 : 0.8);
|
if (!isOshibyo) t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8);
|
||||||
if (isOshibyo) {
|
if (isOshibyo) {
|
||||||
t.thought = "\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u3066\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u3063\u305f";
|
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] });
|
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 });
|
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.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) {
|
if ((worldRef.time || 0) >= (item.pinLogAt || -999) + 1.2) {
|
||||||
item.pinLogAt = worldRef.time || 0;
|
item.pinLogAt = worldRef.time || 0;
|
||||||
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
|
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);
|
const isOshibyo = Boolean(behavior?.blocksZunchi);
|
||||||
t.stuckPushpinId = item.id;
|
t.stuckPushpinId = item.id;
|
||||||
item.deletable = false;
|
item.deletable = false;
|
||||||
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
const sharedPose = global.TarinaiPinAttachmentSystem?.applyTarinaiPose?.(item, t, worldRef);
|
||||||
const visualSide = isOshibyo ? -1 : faceDir;
|
if (!sharedPose) {
|
||||||
item.pinVisualSide = visualSide;
|
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||||
item.x = isOshibyo ? t.x - (t.radius || 16) * 0.88 : t.x + visualSide * (t.radius || 16) * 0.36;
|
const faceSide = faceDir >= 0 ? 1 : -1;
|
||||||
item.y = isOshibyo ? t.y + (t.radius || 16) * 0.42 : t.y - (t.radius || 16) * 0.04;
|
const visualSide = isOshibyo ? -faceSide : faceSide;
|
||||||
item.prevX = item.x;
|
item.pinVisualSide = visualSide;
|
||||||
item.prevY = item.y;
|
const oshibyoAnchor = isOshibyo ? oshibyoAnchorFor(t, visualSide, item) : null;
|
||||||
item.spin = isOshibyo ? -0.52 : visualSide * 0.28;
|
item.x = isOshibyo ? oshibyoAnchor.x : t.x + visualSide * (t.radius || 16) * 0.36;
|
||||||
item.vx = t.vx || 0;
|
item.y = isOshibyo ? oshibyoAnchor.y : t.y - (t.radius || 16) * 0.04;
|
||||||
item.vy = t.vy || 0;
|
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 (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";
|
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;
|
return;
|
||||||
|
|
@ -154,7 +264,7 @@
|
||||||
item.pinDamageTick = (item.pinDamageTick || 0) + dt;
|
item.pinDamageTick = (item.pinDamageTick || 0) + dt;
|
||||||
while (item.pinDamageTick >= 0.55) {
|
while (item.pinDamageTick >= 0.55) {
|
||||||
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);
|
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
|
||||||
if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 });
|
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)");
|
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") {
|
function detach(item, worldRef, reason = "released") {
|
||||||
|
const carriedBall = removeFromBall(item, worldRef);
|
||||||
const t = worldRef?.liveTarinaiById?.(item.pinTargetId) || null;
|
const t = worldRef?.liveTarinaiById?.(item.pinTargetId) || null;
|
||||||
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
|
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
|
||||||
const wasOshibyo = Boolean(behavior?.blocksZunchi);
|
const wasOshibyo = Boolean(behavior?.blocksZunchi);
|
||||||
|
|
@ -204,22 +315,51 @@
|
||||||
item.vy = 0;
|
item.vy = 0;
|
||||||
item.spinVelocity = 0;
|
item.spinVelocity = 0;
|
||||||
} else {
|
} else {
|
||||||
item.vx = deterministicRange(worldRef, "pin-detach-vx", -24, 24, item, t, reason);
|
let nx = 0;
|
||||||
item.vy = deterministicRange(worldRef, "pin-detach-vy", -10, 18, item, t, reason);
|
let ny = 1;
|
||||||
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);
|
|
||||||
if (t) {
|
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));
|
const dx = Number(item.x || 0) - Number(t.x || 0);
|
||||||
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 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)" }));
|
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) {
|
if (wasOshibyo && storedZunchi > 0 && worldRef?.spawnBurstZunchi) {
|
||||||
worldRef.spawnBurstZunchi(t || item, storedZunchi, item);
|
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);
|
})(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";
|
"use strict";
|
||||||
|
|
||||||
// Layer: entity-runtime/items/legacy-lifecycle
|
// Layer: entity-runtime/items/legacy-lifecycle
|
||||||
// Compatibility fallback for the pre-pipeline monolithic Item update body.
|
// Compatibility shim. The former monolithic fallback was removed so item
|
||||||
// Normal runtime goes through item_lifecycle_pipeline.js; this exists only so
|
// behavior has a single source of truth: TarinaiItemLifecyclePipeline.
|
||||||
// old callers and partial-load environments have a stable fallback.
|
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function updateOne(item, dt, worldRef = global.world) {
|
function updateOne(item, dt, worldRef = global.world) {
|
||||||
item.age += dt;
|
if (!item || item.dead || !global.TarinaiItemLifecyclePipeline?.updateOne) return false;
|
||||||
if (item.type === "zunchi") item.spawnGrace = Math.max(0, (item.spawnGrace || 0) - dt);
|
return global.TarinaiItemLifecyclePipeline.updateOne(item, dt, worldRef) !== false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiItemLifecycleLegacySystem = Object.freeze({ updateOne });
|
global.TarinaiItemLifecycleLegacySystem = Object.freeze({ updateOne });
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Layer: entity-runtime/items/lifecycle-step
|
// Layer: entity-runtime/items/lifecycle-step
|
||||||
// Fallback step for environments where the split item lifecycle pipeline cannot
|
// Compatibility shim for old step lists. Do not call a captured monolithic
|
||||||
// run. The captured legacy Item.prototype.update is kept by item_runtime.js.
|
// update body; delegate to the concrete pipeline only.
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function update(item, dt, worldRef, context = {}) {
|
function update(item, dt, worldRef) {
|
||||||
const legacyUpdate = context.legacyUpdate;
|
const ok = global.TarinaiItemLifecyclePipeline?.updateOne?.(item, dt, worldRef);
|
||||||
if (!item || item.dead || typeof legacyUpdate !== "function") return { done: true, ok: false };
|
return { done: true, ok: ok !== false };
|
||||||
legacyUpdate.call(item, dt, worldRef);
|
|
||||||
return { done: true, ok: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiItemLegacyLifecycleStep = Object.freeze({ update });
|
global.TarinaiItemLegacyLifecycleStep = Object.freeze({ update });
|
||||||
|
|
|
||||||
|
|
@ -1,696 +1,18 @@
|
||||||
"use strict";
|
"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 (global) {
|
||||||
function itemTypeDefinition(type = "") {
|
function itemTypeDefinition(type = "") {
|
||||||
const id = String(type || "");
|
const id = String(type || "");
|
||||||
const tool = typeof itemDefinition === "function" ? itemDefinition(id) : null;
|
const tool = typeof global.itemDefinition === "function" ? global.itemDefinition(id) : null;
|
||||||
const food = FOOD_DEFINITIONS?.[id] || null;
|
const foodDefinitions = global.TarinaiFoodDefinitions || {};
|
||||||
const effectId = food?.effectId || (EFFECT_DEFINITIONS?.[id] ? id : "");
|
const effectDefinitions = global.TarinaiEffectDefinitions || {};
|
||||||
const effect = effectId ? (EFFECT_DEFINITIONS?.[effectId] || null) : null;
|
const visualDefinitions = global.TarinaiItemVisualDefinitions || {};
|
||||||
const visual = ITEM_VISUAL_DEFINITIONS?.[id] || null;
|
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 {
|
return {
|
||||||
id,
|
id,
|
||||||
tool,
|
tool,
|
||||||
|
|
@ -703,7 +25,7 @@ function toolTipsFromDefinitions() {
|
||||||
amount: tool?.amount,
|
amount: tool?.amount,
|
||||||
placeable: Boolean(tool?.placeable),
|
placeable: Boolean(tool?.placeable),
|
||||||
scalable: Boolean(tool?.scalable),
|
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() {
|
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([
|
const ids = new Set([
|
||||||
...Object.values(TOOL_DEFINITIONS).map(def => def.itemType || def.id),
|
...Object.values(toolDefinitions).map(def => def.itemType || def.id),
|
||||||
...Object.keys(FOOD_DEFINITIONS || {}),
|
...Object.keys(foodDefinitions),
|
||||||
...Object.keys(EFFECT_DEFINITIONS || {}),
|
...Object.keys(effectDefinitions),
|
||||||
...Object.keys(ITEM_VISUAL_DEFINITIONS || {}),
|
...Object.keys(visualDefinitions),
|
||||||
]);
|
]);
|
||||||
return [...ids].sort().map(id => itemTypeDefinition(id));
|
return [...ids].sort().map(id => itemTypeDefinition(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global.itemTypeDefinition = itemTypeDefinition;
|
||||||
|
global.itemTypeLabel = itemTypeLabel;
|
||||||
|
global.itemTypeDebugSnapshot = itemTypeDebugSnapshot;
|
||||||
|
|
||||||
global.TarinaiItemRegistry = Object.freeze({
|
global.TarinaiItemRegistry = Object.freeze({
|
||||||
definition: itemTypeDefinition,
|
definition: itemTypeDefinition,
|
||||||
label: itemTypeLabel,
|
label: itemTypeLabel,
|
||||||
debugSnapshot: itemTypeDebugSnapshot,
|
debugSnapshot: itemTypeDebugSnapshot,
|
||||||
tool: toolDefinition,
|
tool: global.toolDefinition,
|
||||||
item: itemDefinition,
|
item: global.itemDefinition,
|
||||||
food: FOOD_REGISTRY,
|
food: global.TarinaiFoodRegistry,
|
||||||
effect: EFFECT_REGISTRY,
|
effect: global.TarinaiEffectRegistry,
|
||||||
visual: itemVisualDefinition,
|
visual: global.itemVisualDefinition,
|
||||||
hasTrait: itemHasTrait,
|
hasTrait: global.itemHasTrait,
|
||||||
radius: itemRadiusFor,
|
radius: global.itemRadiusFor,
|
||||||
amount: itemAmountFor,
|
amount: global.itemAmountFor,
|
||||||
categories: toolCategories,
|
categories: global.toolCategories,
|
||||||
physicsToolIds: () => [...PHYSICS_TOOL_IDS],
|
physicsToolIds: () => [...(global.PHYSICS_TOOL_IDS || [])],
|
||||||
isFence: isFenceItemType,
|
isFence: global.isFenceItemType,
|
||||||
isMechanical: isMechanicalItemType,
|
isMechanical: global.isMechanicalItemType,
|
||||||
isLink: isLinkItemType,
|
isLink: global.isLinkItemType,
|
||||||
scalableToolIds,
|
scalableToolIds: global.scalableToolIds,
|
||||||
pinBehavior: pinBehaviorFor,
|
pinBehavior: global.pinBehaviorFor,
|
||||||
});
|
});
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(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);
|
const styleWarm = !styleNight && (lightState.goldenStrength > 0.22 || warm > 0.55);
|
||||||
if (isPinType(this.type) && this.pinState === "lodged") {
|
if (isPinType(this.type) && this.pinState === "lodged") {
|
||||||
const owner = world?.liveTarinaiById?.(this.pinTargetId) || null;
|
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 servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null;
|
||||||
const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1);
|
const 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") {
|
} else if (this.type === "ball") {
|
||||||
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
const speed = Math.hypot(this.vx || 0, this.vy || 0);
|
||||||
const moving = clamp(speed / 260, 0, 1);
|
const moving = clamp(speed / 260, 0, 1);
|
||||||
|
const r = this.r || 18;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.rotate(this.spin || 0);
|
ctx.rotate(this.spin || 0);
|
||||||
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
||||||
ctx.shadowBlur = 2 + moving * 3;
|
ctx.shadowBlur = 2 + moving * 3;
|
||||||
ctx.shadowOffsetY = 2;
|
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.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillText("\u26bd", 0, 1);
|
ctx.fillText("⚽", 0, 0);
|
||||||
if (moving > 0.55) {
|
if (moving > 0.55) {
|
||||||
ctx.globalAlpha = 0.14 + moving * 0.10;
|
ctx.globalAlpha *= 0.10 + moving * 0.10;
|
||||||
ctx.shadowColor = "transparent";
|
ctx.shadowColor = "transparent";
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.beginPath();
|
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.fill();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
@ -727,15 +731,8 @@ draw(ctx, t, lighting = null) {
|
||||||
ctx.globalAlpha *= powered ? 1 : 0.72;
|
ctx.globalAlpha *= powered ? 1 : 0.72;
|
||||||
ctx.strokeStyle = styleNight ? "#6e5f89" : "#8b6cc5";
|
ctx.strokeStyle = styleNight ? "#6e5f89" : "#8b6cc5";
|
||||||
ctx.lineWidth = thick;
|
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;
|
ctx.shadowBlur = 0;
|
||||||
if (tier !== "low") {
|
strokeSegments(ctx, this, segments, "body");
|
||||||
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");
|
|
||||||
}
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
ctx.fillStyle = powered ? (styleNight ? "#3c324e" : "#f4eaff") : (styleNight ? "#34343a" : "#ece8ef");
|
ctx.fillStyle = powered ? (styleNight ? "#3c324e" : "#f4eaff") : (styleNight ? "#34343a" : "#ece8ef");
|
||||||
ctx.strokeStyle = powered ? (styleNight ? "#a28cc8" : "#6e4fa4") : "rgba(92,88,98,0.82)";
|
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.globalAlpha *= enabled ? 1 : 0.48;
|
||||||
ctx.strokeStyle = styleNight ? "#4d8e4e" : "#5cac4d";
|
ctx.strokeStyle = styleNight ? "#4d8e4e" : "#5cac4d";
|
||||||
ctx.lineWidth = thick;
|
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;
|
ctx.shadowBlur = 0;
|
||||||
|
strokeSegments(ctx, this, segments, "body");
|
||||||
ctx.setLineDash([]);
|
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.restore();
|
||||||
ctx.fillStyle = styleNight ? "#263b27" : "#efffe9";
|
// v39.15.53: 毒ブロックは物理輪郭だけを安定描画する。
|
||||||
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);
|
|
||||||
} else if (this.type === "reciprocator") {
|
} else if (this.type === "reciprocator") {
|
||||||
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||||
const pb = window.TarinaiPhysicsBodySystem;
|
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 (type === "water" || type === "zunda_juice") item.roles.drink = true;
|
||||||
if (["sweet", "water", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = 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 (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.roles.grassMaterial = true;
|
||||||
if (type === "grass") {
|
if (type === "grass") {
|
||||||
item.grassStage = Math.floor(rand(0, 2.999));
|
item.grassStage = Math.floor(rand(0, 2.999));
|
||||||
|
|
@ -133,8 +133,8 @@ function initializeItemTypeState(item, type, x, y) {
|
||||||
collision: { solid: true, hazard: true },
|
collision: { solid: true, hazard: true },
|
||||||
hazard: { kind: "poison", damage: 7 },
|
hazard: { kind: "poison", damage: 7 },
|
||||||
sleep: { awakeUntil: 0 },
|
sleep: { awakeUntil: 0 },
|
||||||
mass: 4.8,
|
mass: 0.45,
|
||||||
inertia: 36000,
|
inertia: 3200,
|
||||||
};
|
};
|
||||||
item.prevX = x;
|
item.prevX = x;
|
||||||
item.prevY = y;
|
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 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 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 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();
|
if (physicsEnd) physicsEnd();
|
||||||
state.signature = schedulerSignature(worldRef);
|
state.signature = schedulerSignature(worldRef);
|
||||||
worldRef._itemUpdateSchedulerStats = {
|
worldRef._itemUpdateSchedulerStats = {
|
||||||
|
|
@ -211,7 +212,7 @@
|
||||||
mechanicalStats: worldRef._mechanicalWorldStats || null,
|
mechanicalStats: worldRef._mechanicalWorldStats || null,
|
||||||
constraintStats: worldRef._constraintWorldStats || null,
|
constraintStats: worldRef._constraintWorldStats || null,
|
||||||
};
|
};
|
||||||
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld;
|
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld + poisonContactRemoved;
|
||||||
} finally {
|
} finally {
|
||||||
if (end) end();
|
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");
|
return rectsFor(item, "obstacle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function placementRects(item) {
|
||||||
|
if (!item || item.dead || !isMechanicalType(item)) return [];
|
||||||
|
return rectsFor(item, "obstacle");
|
||||||
|
}
|
||||||
|
|
||||||
function poisonHazardRects(item) {
|
function poisonHazardRects(item) {
|
||||||
if (!item || item.dead || item.type !== "poison_block") return [];
|
if (!item || item.dead || item.type !== "poison_block") return [];
|
||||||
return rectsFor(item, "hazard");
|
return rectsFor(item, "hazard");
|
||||||
|
|
@ -487,7 +492,7 @@
|
||||||
body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 };
|
body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 };
|
||||||
const vel = body.velocity;
|
const vel = body.velocity;
|
||||||
const pose = body.pose || {};
|
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 prevVx = num(vel.x, item.vx);
|
||||||
const prevVy = num(vel.y, item.vy);
|
const prevVy = num(vel.y, item.vy);
|
||||||
const nextVx = clamp(prevVx + num(fx) * 0.42 * scale / mass, -260, 260);
|
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 rx = num(contactX) - num(pose.x, item.x);
|
||||||
const ry = num(contactY) - num(pose.y, item.y);
|
const ry = num(contactY) - num(pose.y, item.y);
|
||||||
const torque = rx * num(fy) - ry * num(fx);
|
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);
|
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;
|
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"); }
|
if (changed) { commitBody(item, "poison-block-impulse"); wakeItem(item, "poison-block-impulse"); }
|
||||||
|
|
@ -648,7 +653,7 @@
|
||||||
|
|
||||||
function mechanicalSeparationWeight(item, nx, ny) {
|
function mechanicalSeparationWeight(item, nx, ny) {
|
||||||
if (!item || item.dead || item.playerHeld || item._heldByPlayer) return 0;
|
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") {
|
if (item.type === "reciprocator") {
|
||||||
const a = axis(item);
|
const a = axis(item);
|
||||||
const projection = Math.abs(num(nx) * a.x + num(ny) * a.y);
|
const projection = Math.abs(num(nx) * a.x + num(ny) * a.y);
|
||||||
|
|
@ -715,9 +720,9 @@
|
||||||
const wb = mechanicalSeparationWeight(b, nx, ny);
|
const wb = mechanicalSeparationWeight(b, nx, ny);
|
||||||
const sum = wa + wb;
|
const sum = wa + wb;
|
||||||
if (sum <= 0.0001) return false;
|
if (sum <= 0.0001) return false;
|
||||||
// Solve most of the penetration as position, not velocity. Capping avoids
|
// Resolve the actual penetration only. The former extra bias made bodies
|
||||||
// large teleports when old saves spawn bodies deeply overlapped.
|
// visibly pop when many mechanical items touched at once.
|
||||||
const correction = Math.min(overlap + 0.35, Math.max(2.2, Math.min(18, overlap * 0.82)));
|
const correction = Math.min(Math.max(0, overlap - 0.05) * 0.92, 18);
|
||||||
const ax = -nx * correction * (wa / sum);
|
const ax = -nx * correction * (wa / sum);
|
||||||
const ay = -ny * correction * (wa / sum);
|
const ay = -ny * correction * (wa / sum);
|
||||||
const bx = nx * correction * (wb / sum);
|
const bx = nx * correction * (wb / sum);
|
||||||
|
|
@ -735,7 +740,7 @@
|
||||||
if (!item || !info) return false;
|
if (!item || !info) return false;
|
||||||
const overlap = Math.max(0, num(info.overlap));
|
const overlap = Math.max(0, num(info.overlap));
|
||||||
if (overlap <= 0.001) return false;
|
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");
|
const moved = moveMechanicalBodyForSeparation(item, -num(info.nx) * correction, -num(info.ny) * correction, dt, "fence-separation");
|
||||||
if (moved) {
|
if (moved) {
|
||||||
worldRef?.markSpatialDirty?.("mechanical-fence-separation");
|
worldRef?.markSpatialDirty?.("mechanical-fence-separation");
|
||||||
|
|
@ -1001,7 +1006,6 @@
|
||||||
if (a.type === "rotator") brakePoweredRotatorOnMechanicalContact(a, dt, worldRef, "mechanical");
|
if (a.type === "rotator") brakePoweredRotatorOnMechanicalContact(a, dt, worldRef, "mechanical");
|
||||||
if (b.type === "rotator") brakePoweredRotatorOnMechanicalContact(b, 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"); }
|
if (worldRef) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("mechanical-contact"); }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1058,7 +1062,6 @@
|
||||||
worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null);
|
worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null);
|
||||||
if ((worldRef._mechanicalContactAt[key] || -999) + 0.08 <= now) {
|
if ((worldRef._mechanicalContactAt[key] || -999) + 0.08 <= now) {
|
||||||
worldRef._mechanicalContactAt[key] = 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;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
@ -1185,12 +1188,11 @@
|
||||||
if (Math.abs(num(moved)) <= 0.001) {
|
if (Math.abs(num(moved)) <= 0.001) {
|
||||||
const now = Number(worldRef?.time || item.world?.time || 0) || 0;
|
const now = Number(worldRef?.time || item.world?.time || 0) || 0;
|
||||||
if ((item._reciprocatorMovedAt || -999) + 0.18 < now) {
|
if ((item._reciprocatorMovedAt || -999) + 0.18 < now) {
|
||||||
// The motor is powered but the pose did not change for several frames.
|
// Do not nudge the rail body forward. Treat a stalled powered rail as
|
||||||
// Reset only rail-local transient state; do not touch user speed/travel.
|
// blocked and let the existing blocked/reverse path decide the next move.
|
||||||
pset(item, "slideSpeed", 0, "rail-stall-slide-clear");
|
pset(item, "slideSpeed", 0, "rail-stall-slide-clear");
|
||||||
if (!changed && Math.abs(phase) < 1 - 0.000001) {
|
if (!changed && Math.abs(phase) < 1 - 0.000001) {
|
||||||
integrateReciprocatorPhase(item, dir * motorSpeed, dt, true, "rail-stall-nudge");
|
changed = noteReciprocatorBlocked(item, dt, worldRef, "rail-stall-blocked") || changed;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1271,6 +1273,59 @@
|
||||||
return changed;
|
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 = {}) {
|
function updatePoisonBlock(item, dt, worldRef, opts = {}) {
|
||||||
if (!item || item.dead || item.type !== "poison_block") return false;
|
if (!item || item.dead || item.type !== "poison_block") return false;
|
||||||
applyBodyState(item);
|
applyBodyState(item);
|
||||||
|
|
@ -1305,6 +1360,7 @@
|
||||||
item.angle = normalizeAngle(beforeA + ps(item, "spin", 0) * subDt);
|
item.angle = normalizeAngle(beforeA + ps(item, "spin", 0) * subDt);
|
||||||
changed = constrainPassiveBlockInsideWorld(item, worldRef) || changed;
|
changed = constrainPassiveBlockInsideWorld(item, worldRef) || changed;
|
||||||
if (hasContacts) changed = resolveInteractions(item, subDt, 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;
|
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);
|
const friction = Math.pow(0.42, stepTime);
|
||||||
|
|
@ -1472,6 +1528,9 @@
|
||||||
const active = all.filter(r => r.active);
|
const active = all.filter(r => r.active);
|
||||||
if (!active.length) return [];
|
if (!active.length) return [];
|
||||||
const minUseFocused = Number(opts.focusThreshold || 48) || 48;
|
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;
|
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 chosen = new Set(active.map(r => r.item));
|
||||||
const mech = global.TarinaiMechanicalSystem;
|
const mech = global.TarinaiMechanicalSystem;
|
||||||
|
|
@ -1508,6 +1567,8 @@
|
||||||
let cells = (maxX - minX + 1) * (maxY - minY + 1);
|
let cells = (maxX - minX + 1) * (maxY - minY + 1);
|
||||||
if (!Number.isFinite(cells) || cells <= 0) cells = 1;
|
if (!Number.isFinite(cells) || cells <= 0) cells = 1;
|
||||||
if (cells > maxCellsPerBody) {
|
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)}`;
|
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);
|
let bucket = grid.get(key);
|
||||||
if (!bucket) { bucket = []; grid.set(key, bucket); }
|
if (!bucket) { bucket = []; grid.set(key, bucket); }
|
||||||
|
|
@ -1574,6 +1635,8 @@
|
||||||
pairs += 1;
|
pairs += 1;
|
||||||
if (pairCandidates.length < maxCandidates) pairCandidates.push({ a, b, key, score });
|
if (pairCandidates.length < maxCandidates) pairCandidates.push({ a, b, key, score });
|
||||||
else {
|
else {
|
||||||
|
// Keep only the highest-priority candidates so pathological overlap
|
||||||
|
// scenes stay bounded before narrowphase resolution.
|
||||||
let worst = 0;
|
let worst = 0;
|
||||||
for (let k = 1; k < pairCandidates.length; k += 1) if (pairCandidates[k].score < pairCandidates[worst].score) worst = k;
|
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 };
|
if (score > pairCandidates[worst].score) pairCandidates[worst] = { a, b, key, score };
|
||||||
|
|
@ -1768,6 +1831,7 @@
|
||||||
extent,
|
extent,
|
||||||
worldSegments,
|
worldSegments,
|
||||||
obstacleRects,
|
obstacleRects,
|
||||||
|
placementRects,
|
||||||
poisonHazardRects,
|
poisonHazardRects,
|
||||||
boundsAabb,
|
boundsAabb,
|
||||||
hazardBoundsAabb,
|
hazardBoundsAabb,
|
||||||
|
|
@ -1803,6 +1867,7 @@
|
||||||
updateRotator,
|
updateRotator,
|
||||||
updateReciprocator,
|
updateReciprocator,
|
||||||
updatePoisonBlock,
|
updatePoisonBlock,
|
||||||
|
resolvePoisonBlockTarinaiContacts,
|
||||||
applyPokeImpulse,
|
applyPokeImpulse,
|
||||||
hitTest,
|
hitTest,
|
||||||
reach,
|
reach,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
(function (global) {
|
(function (global) {
|
||||||
|
|
||||||
function rectLocalPoint(rect, x, y) {
|
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 dx = x - (rect.cx ?? 0);
|
||||||
const dy = y - (rect.cy ?? 0);
|
const dy = y - (rect.cy ?? 0);
|
||||||
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
|
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
|
||||||
|
|
@ -12,6 +14,8 @@
|
||||||
|
|
||||||
|
|
||||||
function rectWorldVector(rect, x, y) {
|
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 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);
|
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
|
||||||
return { x: x * c - y * ss, y: x * ss + y * c };
|
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" },
|
collision: { solid: true, hazard: type === "poison_block" },
|
||||||
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
||||||
sleep: { awakeUntil: 0 },
|
sleep: { awakeUntil: 0 },
|
||||||
mass: type === "poison_block" ? 4.8 : 1,
|
mass: type === "poison_block" ? 0.45 : 1,
|
||||||
inertia: type === "poison_block" ? 36000 : 22000,
|
inertia: type === "poison_block" ? 3200 : 22000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,8 +232,8 @@
|
||||||
case "slideSpeed": b.velocity.linear = num(value); break;
|
case "slideSpeed": b.velocity.linear = num(value); break;
|
||||||
case "solid": b.collision.solid = !!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 "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 "mass": b.mass = Math.max(0.12, num(value, 0.45)); break;
|
||||||
case "inertia": b.inertia = Math.max(1, num(value, 36000)); 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 "railOn": b.motor.powered = !!value; break;
|
||||||
case "railMotorSpeed": b.motor.speed = Math.max(0, num(value, 92)); 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;
|
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) {
|
function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale = 1) {
|
||||||
if (!influence) return;
|
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.save();
|
||||||
ctx.setLineDash([10, 7]);
|
ctx.globalAlpha = (blocked ? 0.30 : 0.40) * Math.max(0.2, Number(alphaScale || 1) || 1);
|
||||||
ctx.lineWidth = 1.6;
|
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();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function placementPreviewFor(world) {
|
function placementPreviewFor(world) {
|
||||||
const p = world?.pointer;
|
return TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forWorld?.(world) || null;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawPlacementPreview(ctx, world) {
|
function drawPlacementPreview(ctx, world) {
|
||||||
const preview = placementPreviewFor(world);
|
const preview = placementPreviewFor(world);
|
||||||
if (!preview) return;
|
if (!preview) return;
|
||||||
|
|
@ -614,19 +561,10 @@ function drawOperationToolPreview(ctx, world) {
|
||||||
const type = world.copyBuffer.type || "";
|
const type = world.copyBuffer.type || "";
|
||||||
const item = world.copyPreviewItemAt?.(p.x, p.y);
|
const item = world.copyPreviewItemAt?.(p.x, p.y);
|
||||||
if (item) {
|
if (item) {
|
||||||
const pad = typeof CONFIG !== "undefined" ? (CONFIG.worldPadding ?? 0) : 0;
|
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forItem?.(world, item) || null;
|
||||||
const rectOutside = (rect) => Boolean(rect && (rect.left < pad || rect.top < pad || rect.right > world.w - pad || rect.bottom > world.h - pad));
|
const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || [];
|
||||||
const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad;
|
const boundsBlocked = Boolean(placementInfo?.boundsBlocked);
|
||||||
const previewRects = world.solidObstacleRects?.(item) || [];
|
const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(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));
|
|
||||||
let copyPreviewDrew = false;
|
let copyPreviewDrew = false;
|
||||||
if (typeof item.draw === "function") {
|
if (typeof item.draw === "function") {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -694,6 +632,30 @@ function drawOperationToolPreview(ctx, world) {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
return;
|
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;
|
if (world?.tool !== "area_delete") return;
|
||||||
const radius = world.areaDeleteRadius?.() || 138;
|
const radius = world.areaDeleteRadius?.() || 138;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -1000,7 +962,7 @@ function syncCarriedPlushieToOwner(it, worldRef) {
|
||||||
|
|
||||||
function ensureWorldRenderStack(worldRef) {
|
function ensureWorldRenderStack(worldRef) {
|
||||||
if (!worldRef._renderStack) {
|
if (!worldRef._renderStack) {
|
||||||
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [] };
|
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
|
||||||
}
|
}
|
||||||
const stack = worldRef._renderStack;
|
const stack = worldRef._renderStack;
|
||||||
const signature = renderStackSignature(worldRef);
|
const signature = renderStackSignature(worldRef);
|
||||||
|
|
@ -1010,6 +972,7 @@ function ensureWorldRenderStack(worldRef) {
|
||||||
stack.layered.length = 0;
|
stack.layered.length = 0;
|
||||||
stack.carriedPlushies.length = 0;
|
stack.carriedPlushies.length = 0;
|
||||||
stack.lodgedPins.length = 0;
|
stack.lodgedPins.length = 0;
|
||||||
|
if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0;
|
||||||
|
|
||||||
for (const it of worldRef.items || []) {
|
for (const it of worldRef.items || []) {
|
||||||
if (!it || it.dead || isCachedTerrainItem(it)) continue;
|
if (!it || it.dead || isCachedTerrainItem(it)) continue;
|
||||||
|
|
@ -1017,6 +980,10 @@ function ensureWorldRenderStack(worldRef) {
|
||||||
stack.lodgedPins.push(it);
|
stack.lodgedPins.push(it);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isPinType(it.type) && it.pinState === "ball_lodged") {
|
||||||
|
(stack.ballLodgedPins || (stack.ballLodgedPins = [])).push(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
||||||
stack.carriedPlushies.push(it);
|
stack.carriedPlushies.push(it);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1042,12 +1009,13 @@ function ensureWorldRenderStack(worldRef) {
|
||||||
function collectVisibleRenderStack(worldRef, visibleRect) {
|
function collectVisibleRenderStack(worldRef, visibleRect) {
|
||||||
const end = window.TarinaiPerf?.begin?.("render.stackBuild") || null;
|
const end = window.TarinaiPerf?.begin?.("render.stackBuild") || null;
|
||||||
try {
|
try {
|
||||||
if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [] };
|
if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
|
||||||
const stack = worldRef._visibleRenderStack;
|
const stack = worldRef._visibleRenderStack;
|
||||||
stack.backItems.length = 0;
|
stack.backItems.length = 0;
|
||||||
stack.layered.length = 0;
|
stack.layered.length = 0;
|
||||||
stack.carriedPlushies.length = 0;
|
stack.carriedPlushies.length = 0;
|
||||||
stack.lodgedPins.length = 0;
|
stack.lodgedPins.length = 0;
|
||||||
|
if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0;
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const addItem = (it) => {
|
const addItem = (it) => {
|
||||||
if (it && it.isStructure && it.type === "plushie" && it.carriedById) syncCarriedPlushieToOwner(it, worldRef);
|
if (it && it.isStructure && it.type === "plushie" && it.carriedById) syncCarriedPlushieToOwner(it, worldRef);
|
||||||
|
|
@ -1057,6 +1025,10 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
|
||||||
stack.lodgedPins.push(it);
|
stack.lodgedPins.push(it);
|
||||||
return;
|
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) {
|
if (it.isStructure && it.type === "plushie" && it.carriedById) {
|
||||||
stack.carriedPlushies.push(it);
|
stack.carriedPlushies.push(it);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1071,6 +1043,8 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
|
||||||
};
|
};
|
||||||
worldRef.ensureSpatial?.("render-visible");
|
worldRef.ensureSpatial?.("render-visible");
|
||||||
if (worldRef.spatial?.nearbyRectInto) {
|
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 = []);
|
const itemScratch = worldRef._renderVisibleItemsScratch || (worldRef._renderVisibleItemsScratch = []);
|
||||||
itemScratch.length = 0;
|
itemScratch.length = 0;
|
||||||
worldRef.spatial.nearbyRectInto(worldRef.spatial.staticItemCells || worldRef.spatial.itemCells, visibleRect, itemScratch);
|
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.
|
// 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");
|
const endLayered = renderPerfBegin("render.draw.layered");
|
||||||
for (const entry of renderStack.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);
|
renderPerfEnd(endLayered);
|
||||||
|
|
||||||
|
|
@ -1627,8 +1629,15 @@ function render() {
|
||||||
syncCarriedPlushieToOwner(it, world);
|
syncCarriedPlushieToOwner(it, world);
|
||||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
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);
|
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);
|
renderPerfEnd(endAttachments);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,13 @@
|
||||||
if (typeof syncTopButtons === "function") syncTopButtons();
|
if (typeof syncTopButtons === "function") syncTopButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncRestoreDependents(worldRef = global.world, context = {}) {
|
function syncRestoreDependents(worldRef = global.world) {
|
||||||
worldRef?.emit?.("ui:restore-sync", { source: context.source || "restore" });
|
|
||||||
renderWorldAfterRestore(worldRef);
|
renderWorldAfterRestore(worldRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreSnapshot(snapshot, worldRef = global.world, options = {}) {
|
function restoreSnapshot(snapshot, worldRef = global.world, options = {}) {
|
||||||
const result = Snapshot.restoreSnapshot(snapshot, worldRef);
|
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;
|
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));
|
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) {
|
async function compressBytes(bytes) {
|
||||||
if (typeof CompressionStream === "function") {
|
if (typeof CompressionStream === "function") {
|
||||||
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
||||||
|
|
@ -301,7 +239,7 @@
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
async function decompressBytes(bytes, codec = "R") {
|
async function decompressBytes(bytes, codec) {
|
||||||
if (codec === DEFLATE_CODEC) {
|
if (codec === DEFLATE_CODEC) {
|
||||||
if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser");
|
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"));
|
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
|
||||||
|
|
@ -472,7 +410,7 @@
|
||||||
return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100];
|
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 pinIdx = row[15] ?? -1;
|
||||||
const nestIdx = row[16] ?? -1;
|
const nestIdx = row[16] ?? -1;
|
||||||
const stateId = row[17]?.[0] || 0;
|
const stateId = row[17]?.[0] || 0;
|
||||||
|
|
@ -480,42 +418,34 @@
|
||||||
const genetics = Array.isArray(row[18]) ? row[18] : [];
|
const genetics = Array.isArray(row[18]) ? row[18] : [];
|
||||||
const fightStats = Array.isArray(row[19]) ? row[19] : [];
|
const fightStats = Array.isArray(row[19]) ? row[19] : [];
|
||||||
const birthSeed = row[1] || "";
|
const birthSeed = row[1] || "";
|
||||||
trackBytes(writer, profile, "tarinai.flags", () => writeBitFields(writer, [row[0] || 0], [10]));
|
writeBitFields(writer, [row[0] || 0], [10]);
|
||||||
trackBytes(writer, profile, "tarinai.seed", () => writer.str(birthSeed));
|
writer.str(birthSeed);
|
||||||
trackBytes(writer, profile, "tarinai.core", () => {
|
const x = toSafeInt(row[2] || 0);
|
||||||
const x = toSafeInt(row[2] || 0);
|
const y = toSafeInt(row[3] || 0);
|
||||||
const y = toSafeInt(row[3] || 0);
|
writer.s(x - (coordState?.x || 0));
|
||||||
writer.s(x - (coordState?.x || 0));
|
writer.s(y - (coordState?.y || 0));
|
||||||
writer.s(y - (coordState?.y || 0));
|
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
writer.u(row[4] || 0);
|
||||||
writer.u(row[4] || 0);
|
writer.u(row[5] || 1);
|
||||||
writer.u(row[5] || 1);
|
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
|
||||||
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);
|
||||||
trackBytes(writer, profile, "tarinai.needs", () => writeFixedPacked(writer, row[9], 6, 6, 2));
|
writeIndexList(writer, row[11]);
|
||||||
trackBytes(writer, profile, "tarinai.traits", () => {
|
writeRelationships(writer, row[12]);
|
||||||
writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8);
|
writeTimers(writer, row[13]);
|
||||||
});
|
writeModes(writer, row[14]);
|
||||||
trackBytes(writer, profile, "tarinai.family", () => {
|
let tailMask = 0;
|
||||||
writeIndexList(writer, row[11]);
|
if (pinIdx >= 0) tailMask |= 1;
|
||||||
});
|
if (nestIdx >= 0) tailMask |= 2;
|
||||||
trackBytes(writer, profile, "tarinai.relationships", () => writeRelationships(writer, row[12]));
|
if (stateId || stateTarget >= 0) tailMask |= 4;
|
||||||
trackBytes(writer, profile, "tarinai.timers", () => writeTimers(writer, row[13]));
|
if (genetics.length) tailMask |= 8;
|
||||||
trackBytes(writer, profile, "tarinai.modes", () => writeModes(writer, row[14]));
|
if (fightStats.length) tailMask |= 16;
|
||||||
trackBytes(writer, profile, "tarinai.tail", () => {
|
writeBitFields(writer, [tailMask], [5]);
|
||||||
let tailMask = 0;
|
if (tailMask & 1) writer.u(pinIdx);
|
||||||
if (pinIdx >= 0) tailMask |= 1;
|
if (tailMask & 2) writer.u(nestIdx);
|
||||||
if (nestIdx >= 0) tailMask |= 2;
|
if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); }
|
||||||
if (stateId || stateTarget >= 0) tailMask |= 4;
|
if (tailMask & 8) for (let i = 0; i < 4; i++) writer.s(genetics[i] || 100);
|
||||||
if (genetics.length) tailMask |= 8;
|
if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); }
|
||||||
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) {
|
function readTarinai(reader, coordState = null) {
|
||||||
const flags = readBitFields(reader, [10])[0];
|
const flags = readBitFields(reader, [10])[0];
|
||||||
|
|
@ -532,13 +462,12 @@
|
||||||
const relationships = readRelationships(reader);
|
const relationships = readRelationships(reader);
|
||||||
const timers = readTimers(reader);
|
const timers = readTimers(reader);
|
||||||
const modes = readModes(reader);
|
const modes = readModes(reader);
|
||||||
const modernTail = Number(reader.schemaVersion || BINARY_SCHEMA_VERSION) >= 15;
|
const [tailMask] = readBitFields(reader, [5]);
|
||||||
const [tailMask] = readBitFields(reader, [modernTail ? 5 : 3]);
|
|
||||||
const pinIdx = (tailMask & 1) ? reader.u() : -1;
|
const pinIdx = (tailMask & 1) ? reader.u() : -1;
|
||||||
const nestIdx = (tailMask & 2) ? reader.u() : -1;
|
const nestIdx = (tailMask & 2) ? reader.u() : -1;
|
||||||
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -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 genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s()] : [];
|
||||||
const fightStats = (modernTail && (tailMask & 16)) ? [reader.u(), reader.u(), 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];
|
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)));
|
for (let i = 0; i < 16; i++) out += (((x >> i) & 1) * (2 ** (2 * i))) + (((y >> i) & 1) * (2 ** (2 * i + 1)));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
function writeItemPayload(writer, row = [], typeId = 0, coordState = null, profile = null) {
|
function writeItemPayload(writer, row = [], typeId = 0, coordState = null) {
|
||||||
const start = writer.bytes.length;
|
const x = toSafeInt(row[1] || 0);
|
||||||
trackBytes(writer, profile, "items.core", () => {
|
const y = toSafeInt(row[2] || 0);
|
||||||
const x = toSafeInt(row[1] || 0);
|
writer.s(x - (coordState?.x || 0));
|
||||||
const y = toSafeInt(row[2] || 0);
|
writer.s(y - (coordState?.y || 0));
|
||||||
writer.s(x - (coordState?.x || 0));
|
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||||
writer.s(y - (coordState?.y || 0));
|
if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0);
|
||||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
writeItemExtra(writer, typeId, row[4] || []);
|
||||||
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 readItemPayload(reader, typeId = 0, coordState = null) {
|
function readItemPayload(reader, typeId = 0, coordState = null) {
|
||||||
const x = (coordState?.x || 0) + reader.s();
|
const x = (coordState?.x || 0) + reader.s();
|
||||||
|
|
@ -642,14 +567,14 @@
|
||||||
}
|
}
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
function writeItemBlocks(writer, rows = [], profile = null) {
|
function writeItemBlocks(writer, rows = []) {
|
||||||
const blocks = itemBlocksFromRows(rows);
|
const blocks = itemBlocksFromRows(rows);
|
||||||
writer.u(blocks.length);
|
writer.u(blocks.length);
|
||||||
for (const [typeId, list] of blocks) {
|
for (const [typeId, list] of blocks) {
|
||||||
writer.u(typeId);
|
writer.u(typeId);
|
||||||
writer.u(list.length);
|
writer.u(list.length);
|
||||||
const coordState = { x: 0, y: 0 };
|
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) {
|
function readItemBlocks(reader) {
|
||||||
|
|
@ -675,35 +600,25 @@
|
||||||
return [day, tarinaiCount, fieldValue(w[0]), time10, itemCount, groundValue(w[1])];
|
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");
|
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();
|
const writer = new BinaryWriter();
|
||||||
trackBytes(writer, profile, "header", () => {
|
writer.b(BINARY_SCHEMA_VERSION);
|
||||||
writer.b(BINARY_SCHEMA_VERSION);
|
writer.u(Snapshot.version);
|
||||||
writer.u(Snapshot.version);
|
writeWorld(writer, snapshot.w || []);
|
||||||
});
|
|
||||||
trackBytes(writer, profile, "world", () => writeWorld(writer, snapshot.w || []));
|
|
||||||
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||||
trackBytes(writer, profile, "tarinai.count", () => writer.u(tarinaiRows.length));
|
writer.u(tarinaiRows.length);
|
||||||
const tarinaiStart = writer.bytes.length;
|
|
||||||
const tarCoordState = { x: 0, y: 0 };
|
const tarCoordState = { x: 0, y: 0 };
|
||||||
for (const row of tarinaiRows) writeTarinai(writer, row || [], profile, tarCoordState);
|
for (const row of tarinaiRows) writeTarinai(writer, row || [], tarCoordState);
|
||||||
addProfileBytes(profile, "tarinai.total", writer.bytes.length - tarinaiStart);
|
|
||||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||||
const itemStart = writer.bytes.length;
|
writeItemBlocks(writer, itemRows);
|
||||||
trackBytes(writer, profile, "items.blocks", () => writeItemBlocks(writer, itemRows, profile));
|
return writer.out();
|
||||||
addProfileBytes(profile, "items.total", writer.bytes.length - itemStart);
|
|
||||||
const bytes = writer.out();
|
|
||||||
if (profile) return { bytes, profile };
|
|
||||||
return bytes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBinarySnapshot(bytes) {
|
function decodeBinarySnapshot(bytes) {
|
||||||
const reader = new BinaryReader(bytes);
|
const reader = new BinaryReader(bytes);
|
||||||
const schema = reader.b();
|
const schema = reader.b();
|
||||||
if (schema !== BINARY_SCHEMA_VERSION && schema !== 14) throw new Error("unsupported binary save schema");
|
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||||
reader.schemaVersion = schema;
|
|
||||||
const snapshotVersion = reader.u();
|
const snapshotVersion = reader.u();
|
||||||
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
||||||
const w = readWorld(reader);
|
const w = readWorld(reader);
|
||||||
|
|
@ -727,18 +642,10 @@
|
||||||
return { codec, packed };
|
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) {
|
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) {
|
async function decodeSnapshot(text) {
|
||||||
|
|
@ -752,20 +659,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiSaveCodec = {
|
global.TarinaiSaveCodec = {
|
||||||
EXPORT_PREFIX,
|
|
||||||
RAW_CODEC,
|
|
||||||
DEFLATE_CODEC,
|
|
||||||
SAVE_TEXT_ALPHABET,
|
|
||||||
SAVE_TEXT_BASE,
|
|
||||||
SAVE_TEXT_BITS,
|
|
||||||
jp2048Encode,
|
|
||||||
jp2048Decode,
|
|
||||||
encodeSnapshot,
|
encodeSnapshot,
|
||||||
encodeSnapshotWithProfile,
|
|
||||||
decodeSnapshot,
|
decodeSnapshot,
|
||||||
encodeBinarySnapshot,
|
|
||||||
decodeBinarySnapshot,
|
|
||||||
BinaryWriter,
|
|
||||||
BinaryReader,
|
|
||||||
};
|
};
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
if (!hash.startsWith("\u305f\u308a")) throw new Error("unsupported Japanese hash slot value");
|
if (!hash.startsWith("\u305f\u308a")) throw new Error("unsupported Japanese hash slot value");
|
||||||
global.localStorage?.setItem(slotKey(slot), hash);
|
global.localStorage?.setItem(slotKey(slot), hash);
|
||||||
if (meta) global.localStorage?.setItem(metaKey(slot), JSON.stringify({ ...meta, savedAt: Date.now() }));
|
if (meta) global.localStorage?.setItem(metaKey(slot), JSON.stringify({ ...meta, savedAt: Date.now() }));
|
||||||
return { hash, meta: readMeta(slot) };
|
|
||||||
}
|
}
|
||||||
function readMeta(slot) {
|
function readMeta(slot) {
|
||||||
const raw = global.localStorage?.getItem(metaKey(slot));
|
const raw = global.localStorage?.getItem(metaKey(slot));
|
||||||
|
|
@ -32,5 +31,5 @@
|
||||||
global.localStorage?.removeItem(slotKey(slot));
|
global.localStorage?.removeItem(slotKey(slot));
|
||||||
global.localStorage?.removeItem(metaKey(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);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreSnapshot(snapshot, worldRef = global.world, options = {}) {
|
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)) {
|
async function encodeSnapshot(snapshot = createSnapshot(global.world)) {
|
||||||
return Codec.encodeSnapshot(snapshot);
|
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) {
|
async function decodeSnapshot(text) {
|
||||||
return Codec.decodeSnapshot(text);
|
return Codec.decodeSnapshot(text);
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +34,7 @@
|
||||||
async function loadSlot(slot) {
|
async function loadSlot(slot) {
|
||||||
const hash = Storage.requireSlot(slot);
|
const hash = Storage.requireSlot(slot);
|
||||||
const snapshot = await decodeSnapshot(hash);
|
const snapshot = await decodeSnapshot(hash);
|
||||||
return restoreSnapshot(snapshot, global.world, { source: "slot" });
|
return restoreSnapshot(snapshot, global.world);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSlot(slot) { Storage.deleteSlot(slot); }
|
function deleteSlot(slot) { Storage.deleteSlot(slot); }
|
||||||
|
|
@ -49,7 +44,7 @@
|
||||||
if (!entry) return "\u7a7a\u304d";
|
if (!entry) return "\u7a7a\u304d";
|
||||||
const meta = entry.m || entry;
|
const meta = entry.m || entry;
|
||||||
const m = meta.m || [];
|
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 day = m[0] || 1;
|
||||||
const population = m[1] || 0;
|
const population = m[1] || 0;
|
||||||
const fieldType = m[2] || "\u5ead";
|
const fieldType = m[2] || "\u5ead";
|
||||||
|
|
@ -58,60 +53,6 @@
|
||||||
return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339${hashLen}`;
|
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() {
|
function ensureDialog() {
|
||||||
let dialog = document.getElementById("saveDialog");
|
let dialog = document.getElementById("saveDialog");
|
||||||
if (dialog) return dialog;
|
if (dialog) return dialog;
|
||||||
|
|
@ -131,7 +72,6 @@
|
||||||
<button id="saveImportBtn" class="btn primary" type="button">\u546a\u6587\u3092\u30ed\u30fc\u30c9</button>
|
<button id="saveImportBtn" class="btn primary" type="button">\u546a\u6587\u3092\u30ed\u30fc\u30c9</button>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
<div class="field-dialog-actions">
|
<div class="field-dialog-actions">
|
||||||
<button id="saveCloseBtn" class="btn" type="button">\u9589\u3058\u308b</button>
|
<button id="saveCloseBtn" class="btn" type="button">\u9589\u3058\u308b</button>
|
||||||
|
|
@ -173,7 +113,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindSaveSystem() {
|
function bindSaveSystem() {
|
||||||
const btn = document.getElementById("saveBtn") || global.ui?.saveBtn;
|
const btn = document.getElementById("saveBtn");
|
||||||
if (!btn || btn.dataset.saveBound === "1") return;
|
if (!btn || btn.dataset.saveBound === "1") return;
|
||||||
btn.dataset.saveBound = "1";
|
btn.dataset.saveBound = "1";
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
|
|
@ -228,9 +168,7 @@
|
||||||
if (e.target.closest("#saveExportBtn")) {
|
if (e.target.closest("#saveExportBtn")) {
|
||||||
const area = dialog.querySelector("#saveHashText");
|
const area = dialog.querySelector("#saveHashText");
|
||||||
if (area) {
|
if (area) {
|
||||||
const result = await encodeSnapshotWithProfile(createSnapshot(global.world));
|
area.value = await encodeSnapshot(createSnapshot(global.world));
|
||||||
area.value = result.text || "";
|
|
||||||
renderHashProfile(result.profile, dialog);
|
|
||||||
area.focus();
|
area.focus();
|
||||||
area.select();
|
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 (!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;
|
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 {
|
try {
|
||||||
restoreSnapshot(await decodeSnapshot(text), global.world, { source: "import" });
|
restoreSnapshot(await decodeSnapshot(text), global.world);
|
||||||
closeDialog();
|
closeDialog();
|
||||||
global.showToast?.("\u546a\u6587\u304b\u3089\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002");
|
global.showToast?.("\u546a\u6587\u304b\u3089\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -257,7 +195,6 @@
|
||||||
createSnapshot,
|
createSnapshot,
|
||||||
restoreSnapshot,
|
restoreSnapshot,
|
||||||
encodeSnapshot,
|
encodeSnapshot,
|
||||||
encodeSnapshotWithProfile,
|
|
||||||
decodeSnapshot,
|
decodeSnapshot,
|
||||||
saveSlot,
|
saveSlot,
|
||||||
loadSlot,
|
loadSlot,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ function stableUnit(seed, salt = "") {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stableKeyPart(value) {
|
function stableKeyPart(value) {
|
||||||
|
const shared = globalThis.TarinaiDeterministicHelpers?.keyPart;
|
||||||
|
if (typeof shared === "function") return shared(value);
|
||||||
if (value == null) return "null";
|
if (value == null) return "null";
|
||||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
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 === "string" || typeof value === "boolean") return String(value);
|
||||||
|
|
@ -78,42 +80,16 @@ function stableKeyPart(value) {
|
||||||
return String(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") {
|
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"];
|
const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
|
||||||
|
|
@ -622,8 +598,38 @@ class Effect {
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
} else if (this.type === "bubble") {
|
} else if (this.type === "bubble") {
|
||||||
const w = Math.max(30, 14 + this.text.length * 7);
|
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
|
||||||
const h = 20;
|
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.globalAlpha = Math.min(1, alpha * 1.25);
|
||||||
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
|
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
|
||||||
ctx.strokeStyle = "rgba(78, 64, 48, 0.22)";
|
ctx.strokeStyle = "rgba(78, 64, 48, 0.22)";
|
||||||
|
|
@ -640,10 +646,11 @@ class Effect {
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.fillStyle = this.color || "rgba(42,36,29,0.78)";
|
ctx.fillStyle = this.color || "rgba(42,36,29,0.78)";
|
||||||
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
|
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
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();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -742,6 +749,8 @@ class SpatialGrid {
|
||||||
const minY = Math.floor(Math.min(top, bottom) / this.cellSize);
|
const minY = Math.floor(Math.min(top, bottom) / this.cellSize);
|
||||||
const maxY = Math.floor(Math.max(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));
|
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);
|
if (cellCount > 144) return this.add(map, entity);
|
||||||
for (let cy = minY; cy <= maxY; cy += 1) {
|
for (let cy = minY; cy <= maxY; cy += 1) {
|
||||||
for (let cx = minX; cx <= maxX; cx += 1) {
|
for (let cx = minX; cx <= maxX; cx += 1) {
|
||||||
|
|
@ -919,6 +928,8 @@ class SpatialGrid {
|
||||||
|
|
||||||
nearbyInto(map, x, y, radius, out = [], filterDistance = false) {
|
nearbyInto(map, x, y, radius, out = [], filterDistance = false) {
|
||||||
const r = Number.isFinite(radius) ? radius : Math.max(1200, this.cellSize * 12);
|
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 stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||||
const minX = Math.floor((x - r) / this.cellSize);
|
const minX = Math.floor((x - r) / this.cellSize);
|
||||||
const maxX = Math.floor((x + r) / this.cellSize);
|
const maxX = Math.floor((x + r) / this.cellSize);
|
||||||
|
|
@ -953,6 +964,7 @@ class SpatialGrid {
|
||||||
|
|
||||||
nearbyRectInto(map, rect, out = []) {
|
nearbyRectInto(map, rect, out = []) {
|
||||||
if (!rect) return out;
|
if (!rect) return out;
|
||||||
|
// See nearbyInto(): multi-cell insertions require per-query dedupe.
|
||||||
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||||
const minX = Math.floor((rect.left || 0) / this.cellSize);
|
const minX = Math.floor((rect.left || 0) / this.cellSize);
|
||||||
const maxX = Math.floor((rect.right || 0) / this.cellSize);
|
const maxX = Math.floor((rect.right || 0) / this.cellSize);
|
||||||
|
|
|
||||||
|
|
@ -541,7 +541,6 @@
|
||||||
worldRef.rebuildSpatial?.(true);
|
worldRef.rebuildSpatial?.(true);
|
||||||
worldRef.markTerrainDirty?.("load");
|
worldRef.markTerrainDirty?.("load");
|
||||||
worldRef.clampCamera?.();
|
worldRef.clampCamera?.();
|
||||||
worldRef.emit?.("snapshot:restored", { snapshotVersion: SNAPSHOT_VERSION });
|
|
||||||
return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION };
|
return { world: worldRef, snapshotVersion: SNAPSHOT_VERSION };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,32 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function markWorldAfterStructureChange(worldRef, reason = "structure-change") {
|
function markWorldAfterStructureChange(worldRef, reason = "structure-change", item = null, radius = 40) {
|
||||||
if (!worldRef) return;
|
if (!worldRef) return;
|
||||||
worldRef.drawListDirty = true;
|
worldRef.drawListDirty = true;
|
||||||
worldRef.markItemBucketsDirty?.(reason);
|
worldRef.markItemBucketsDirty?.(reason);
|
||||||
worldRef.markSpatialDirty?.(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") {
|
function markStructureGone(structure, reason = "structure-removed") {
|
||||||
|
|
@ -142,20 +162,25 @@
|
||||||
|
|
||||||
function deleteItem(worldRef, item, opts = {}) {
|
function deleteItem(worldRef, item, opts = {}) {
|
||||||
if (!item || item.dead) return false;
|
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 });
|
const reason = opts.reason || "delete-tool";
|
||||||
item.amount = 0;
|
if (item.isStructure) return deleteStructure(worldRef, item, { reason, userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner });
|
||||||
item.hp = 0;
|
const changed = markLooseItemGone(item, reason);
|
||||||
item.removedReason = String(opts.reason || "delete-tool");
|
if (!changed) return false;
|
||||||
markWorldAfterStructureChange(worldRef, opts.reason || "delete-tool");
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiStructureLifecycle = Object.freeze({
|
const api = Object.freeze({
|
||||||
markWorldAfterStructureChange,
|
markWorldAfterStructureChange,
|
||||||
|
markLooseItemGone,
|
||||||
releaseStructureUsers,
|
releaseStructureUsers,
|
||||||
deleteStructure,
|
deleteStructure,
|
||||||
consumeGrassBedOnWake,
|
consumeGrassBedOnWake,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
|
destroyItem: deleteItem,
|
||||||
triggerOwnedStructureDestroyedPanic,
|
triggerOwnedStructureDestroyedPanic,
|
||||||
});
|
});
|
||||||
})(window);
|
global.TarinaiStructureLifecycle = api;
|
||||||
|
global.TarinaiItemDestruction = api;
|
||||||
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,14 @@
|
||||||
|
|
||||||
draw(ctx, time = 0, lighting = {}) {
|
draw(ctx, time = 0, lighting = {}) {
|
||||||
if (this.dead) return;
|
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();
|
ctx.save();
|
||||||
|
if (this.type === "plushie") ctx.globalAlpha *= ownerAlpha;
|
||||||
ctx.translate(this.x, this.y);
|
ctx.translate(this.x, this.y);
|
||||||
const night = (lighting?.light || 1) < 0.42;
|
const night = (lighting?.light || 1) < 0.42;
|
||||||
if (this.type === "grass_bed") {
|
if (this.type === "grass_bed") {
|
||||||
|
|
@ -257,7 +264,7 @@
|
||||||
buildNeed: "fulfill",
|
buildNeed: "fulfill",
|
||||||
buildTime: 4,
|
buildTime: 4,
|
||||||
maxHp: 1,
|
maxHp: 1,
|
||||||
materials: { grassMaterial: 1 },
|
materials: { grassMaterial: 4 },
|
||||||
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
|
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
|
||||||
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
|
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); },
|
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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) {
|
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",
|
phrase: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f",
|
||||||
weight: 18,
|
weight: 18,
|
||||||
timerBacked: true,
|
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 = {}) {
|
canStart(t, world, ctx = {}) {
|
||||||
|
const higherPriorityPlace = typeof findPrioritySleepPlaceBeforeGrassBedBuild === "function"
|
||||||
|
? findPrioritySleepPlaceBeforeGrassBedBuild(world, t, 760)
|
||||||
|
: findNearestSleepPlace(world, t, 760);
|
||||||
return canBuildOwnedStructureByAge(t)
|
return canBuildOwnedStructureByAge(t)
|
||||||
&& !findNearestSleepPlace(world, t, 760)
|
&& !higherPriorityPlace
|
||||||
&& !findOwnedStructure(world, t, "grass_bed")
|
&& !findOwnedStructure(world, t, "grass_bed")
|
||||||
&& !!(ctx.target ?? this.selectTarget(t, world, ctx))
|
&& !!(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);
|
&& !["fight", "panic", "intimidate", "birth_ritual"].includes(t.state);
|
||||||
},
|
},
|
||||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); },
|
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",
|
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",
|
phrase: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f",
|
||||||
weight: 34,
|
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); },
|
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); },
|
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",
|
label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b",
|
||||||
phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f",
|
phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f",
|
||||||
weight: 28,
|
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); },
|
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); },
|
start(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@
|
||||||
return String(behavior.forcedRequest?.source || behavior.data?.forcedSource || behavior.sourceDetail || "");
|
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) {
|
function normalizeBehaviorValue(input, tarinai = null, previous = null) {
|
||||||
if (!isObject(input)) return null;
|
if (!isObject(input)) return null;
|
||||||
const now = nowFor(tarinai);
|
const now = nowFor(tarinai);
|
||||||
|
|
@ -138,7 +142,7 @@
|
||||||
forcedId: forcedRequest?.uid || String(input.forcedId || previous?.forcedId || ""),
|
forcedId: forcedRequest?.uid || String(input.forcedId || previous?.forcedId || ""),
|
||||||
forcedRequest,
|
forcedRequest,
|
||||||
sourceReasonText: String(input.sourceReasonText || input.forcedReasonText || forcedRequest?.reasonText || previous?.sourceReasonText || ""),
|
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),
|
specId: String(input.specId || input.actionSpecId || previous?.specId || actionId),
|
||||||
specKind: String(input.specKind || previous?.specKind || "need_action"),
|
specKind: String(input.specKind || previous?.specKind || "need_action"),
|
||||||
};
|
};
|
||||||
|
|
@ -199,7 +203,7 @@
|
||||||
if (!value) {
|
if (!value) {
|
||||||
const data = isObject(b.data) ? { ...b.data } : {};
|
const data = isObject(b.data) ? { ...b.data } : {};
|
||||||
delete data.forcedSource;
|
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;
|
tarinai.behavior = next;
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +216,7 @@
|
||||||
forcedId: String(request?.uid || value.uid || value.forcedId || b.forcedId || ""),
|
forcedId: String(request?.uid || value.uid || value.forcedId || b.forcedId || ""),
|
||||||
forcedRequest: request,
|
forcedRequest: request,
|
||||||
sourceReasonText: request?.reasonText || b.sourceReasonText || "",
|
sourceReasonText: request?.reasonText || b.sourceReasonText || "",
|
||||||
causeText: b.causeText || request?.causeText || "",
|
causeText: request?.causeText || b.causeText || "",
|
||||||
data: { ...(b.data || {}), forcedSource: request?.source || value.source || "external" },
|
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]+$/, "");
|
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 = "") {
|
function forcedCausePhrase(source = "", fallback = "") {
|
||||||
const key = String(source || "");
|
const key = String(source || "");
|
||||||
if (key === "love_mochi") return "\u3078\u3053\u9905\u306e\u52b9\u679c";
|
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 = {}) {
|
function causePhraseForReason(chosenNeed, action, tarinai = null, options = {}) {
|
||||||
if (options?.causeText) return String(options.causeText || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
if (options?.causeText) return String(options.causeText || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
||||||
const b = (typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior) || {};
|
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) {
|
if ((typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(b) : Boolean(b.forcedId || b.forcedRequest)) || b.source) {
|
||||||
const forced = forcedCausePhrase(b.source || "", b.sourceReasonText || b.sourceReasonText || "");
|
const forced = forcedCausePhrase(b.source || "", b.sourceReasonText || b.sourceReasonText || "");
|
||||||
if (forced) return forced;
|
if (forced) return forced;
|
||||||
|
|
@ -112,8 +120,15 @@ function needsForBehaviorText(tarinai, behavior = {}, fallbackNeed = "fulfill")
|
||||||
|
|
||||||
function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {}) {
|
function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {}) {
|
||||||
const request = behavior?.forcedRequest || (typeof currentForcedBehaviorRequest === "function" ? currentForcedBehaviorRequest(tarinai) : null) || null;
|
const request = behavior?.forcedRequest || (typeof currentForcedBehaviorRequest === "function" ? currentForcedBehaviorRequest(tarinai) : null) || null;
|
||||||
const directCause = options?.causeText || behavior?.causeText || request?.causeText || "";
|
const optionCause = options?.causeText || "";
|
||||||
if (directCause) return String(directCause || "").replace(/[\u3002\uff01\uff1f]+$/, "");
|
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) {
|
if ((typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(behavior) : Boolean(behavior?.forcedId || behavior?.forcedRequest)) || request) {
|
||||||
const source = request?.source || behavior?.source || "external";
|
const source = request?.source || behavior?.source || "external";
|
||||||
const sourceReason = request?.reasonText || behavior?.sourceReasonText || behavior?.sourceReasonText || "";
|
const sourceReason = request?.reasonText || behavior?.sourceReasonText || behavior?.sourceReasonText || "";
|
||||||
|
|
|
||||||
|
|
@ -79,19 +79,82 @@ function findStructureBuildSpot(t, world, type) {
|
||||||
if (world?.placementBlocked?.(probe)) continue;
|
if (world?.placementBlocked?.(probe)) continue;
|
||||||
let nearestSame = Infinity;
|
let nearestSame = Infinity;
|
||||||
if (type === "grass_bed") {
|
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;
|
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;
|
const score = (Number.isFinite(nearestSame) ? nearestSame : 160) - distXY(c.x, c.y, baseX, baseY) * 0.12;
|
||||||
if (score > bestScore) { best = c; bestScore = score; }
|
if (score > bestScore) { best = c; bestScore = score; }
|
||||||
}
|
}
|
||||||
|
if (type === "grass_bed") return best;
|
||||||
return best || candidates[0] || { x: baseX, y: baseY };
|
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) {
|
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;
|
if (!material || !globalThis.StructureRegistry) return false;
|
||||||
t.buildPlan = {
|
t.buildPlan = {
|
||||||
type,
|
type,
|
||||||
|
|
@ -111,8 +174,8 @@ function continueBuildPlan(t, world, dt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let material = (world?.itemById?.(plan.materialId) || null);
|
let material = (world?.itemById?.(plan.materialId) || null);
|
||||||
if (material && (material.dead || !roleMatches(material, "grassMaterial"))) material = null;
|
if (material && (material.dead || !roleMatches(material, "grassMaterial") || grassMaterialAvailableStages(material) < structureGrassStageCost(plan.type))) material = null;
|
||||||
if (!material) material = findNearbyMaterial(world, t, "grassMaterial", 520);
|
if (!material) material = findNearbyBuildMaterial(world, t, plan.type, 520);
|
||||||
if (!material) {
|
if (!material) {
|
||||||
return stopFailedBuildPlan(t, world, plan.type, "\u6750\u6599\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
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));
|
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 });
|
t.setActionState?.("build", { target: material, reason: `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`, sleeping: false });
|
||||||
if (plan.timer > 0) return true;
|
if (plan.timer > 0) return true;
|
||||||
consumeGrassMaterial(material, plan.type === "grass_bed" ? 4 : 1);
|
|
||||||
const spot = plan.type === "plushie"
|
const spot = plan.type === "plushie"
|
||||||
? { x: t.x, y: t.y - Math.max(28, (t.radius || 24) * 1.28) }
|
? { x: t.x, y: t.y - Math.max(28, (t.radius || 24) * 1.28) }
|
||||||
: findStructureBuildSpot(t, world, plan.type);
|
: 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);
|
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
||||||
if (!structure) {
|
if (!structure) {
|
||||||
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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) {
|
(function (global) {
|
||||||
|
|
@ -224,8 +195,54 @@
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
return this.world?.items?.find?.(it => it && !it.dead && it.id === id && it.type === "nest_box") || 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) {
|
wantsNestBox(box = null) {
|
||||||
if (!box || box.dead || box.type !== "nest_box") return false;
|
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.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;
|
if (!(this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return false;
|
||||||
return this.target === box || this.insideNestBoxId === box.id;
|
return this.target === box || this.insideNestBoxId === box.id;
|
||||||
|
|
@ -236,12 +253,22 @@
|
||||||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||||
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||||
const alreadyInside = this.insideNestBoxId === box.id;
|
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) {
|
if (!alreadyInside) {
|
||||||
this.nestBoxEnteredAt = this.world?.time || 0;
|
this._nestBoxSeekStartedAt = NaN;
|
||||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, (this.world?.time || 0) + 14);
|
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.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.insideNestBoxId = box.id;
|
||||||
this.target = box;
|
this.target = box;
|
||||||
|
|
@ -249,15 +276,31 @@
|
||||||
if (this.state === "sleep") this.sleeping = true;
|
if (this.state === "sleep") this.sleeping = true;
|
||||||
const inner = this.world?.nestBoxInnerPoint ? this.world.nestBoxInnerPoint(box, this) : { x: box.x, y: box.y };
|
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 smoothDt = clamp(Number(dt || 0.016) || 0.016, 0.008, 0.045);
|
||||||
const firstPhase = (this.world?.time || 0) < (this._nestBoxEntrySmoothUntil || -Infinity);
|
const duration = Math.max(0.35, Number(this._nestBoxEntryDuration || 0.95) || 0.95);
|
||||||
const pull = clamp(smoothDt * (alreadyInside ? (firstPhase ? 5.2 : 8.5) : 7.2), 0, 0.55);
|
const progress = clamp(1 - Math.max(0, (this._nestBoxEntrySmoothUntil || now) - now) / duration, 0, 1);
|
||||||
this.x = lerp(this.x, inner.x, pull);
|
const eased = progress * progress * (3 - 2 * progress);
|
||||||
this.y = lerp(this.y, inner.y, pull);
|
if (progress < 1) {
|
||||||
if (!firstPhase && (this.nestFade >= 0.97 || distXY(this.x, this.y, inner.x, inner.y) < 2.0)) {
|
const sx = Number.isFinite(this._nestBoxEntryStartX) ? this._nestBoxEntryStartX : this.x;
|
||||||
this.x = inner.x;
|
const sy = Number.isFinite(this._nestBoxEntryStartY) ? this._nestBoxEntryStartY : this.y;
|
||||||
this.y = inner.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.vx = 0;
|
||||||
this.vy = 0;
|
this.vy = 0;
|
||||||
this.impulseVx = 0;
|
this.impulseVx = 0;
|
||||||
|
|
@ -265,25 +308,108 @@
|
||||||
return true;
|
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) {
|
leaveNestBox(box = null) {
|
||||||
const b = box || this.nestBoxById(this.insideNestBoxId);
|
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);
|
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.x = clamp(entry.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
|
||||||
this.y = clamp(entry.y, CONFIG.worldPadding, this.world.h - 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.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.nestBoxEnteredAt = -Infinity;
|
||||||
this.nestBoxStayUntil = -Infinity;
|
this.nestBoxStayUntil = -Infinity;
|
||||||
this.nestBoxSlotIndex = -1;
|
this.nestBoxSlotIndex = -1;
|
||||||
this._nestBoxEntrySmoothUntil = -Infinity;
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNestBoxPresence(dt = 0) {
|
updateNestBoxPresence(dt = 0) {
|
||||||
|
if (this.updateNestBoxExitAnimation?.(dt || 0.016)) return null;
|
||||||
const currentBox = this.nestBoxById(this.insideNestBoxId);
|
const currentBox = this.nestBoxById(this.insideNestBoxId);
|
||||||
const targetBox = this.target && !this.target.dead && this.target.type === "nest_box" ? this.target : null;
|
const targetBox = this.target && !this.target.dead && this.target.type === "nest_box" ? this.target : null;
|
||||||
const box = currentBox || targetBox;
|
const box = currentBox || targetBox;
|
||||||
|
|
@ -310,22 +436,37 @@
|
||||||
|
|
||||||
if (!this.wantsNestBox(targetBox)) {
|
if (!this.wantsNestBox(targetBox)) {
|
||||||
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
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;
|
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 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 base = this.world?.nestBoxBaseRect ? this.world.nestBoxBaseRect(targetBox) : null;
|
||||||
const r = targetBox.r || 42;
|
const r = targetBox.r || 42;
|
||||||
const entryD = distXY(this.x, this.y, entry.x, entry.y);
|
const entryD = distXY(this.x, this.y, entry.x, entry.y);
|
||||||
const centerD = distXY(this.x, this.y, targetBox.x, targetBox.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 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(76, r * 1.55, this.radius + 48);
|
const captureRange = Math.max(58, r * 1.18, (this.radius || 20) + 30);
|
||||||
const nearEnough = entryD <= captureRange || centerD <= Math.max(78, r * 1.38) || inDoorColumn;
|
const nearEnough = entryD <= captureRange || centerD <= Math.max(58, r * 1.08) || inDoorColumn;
|
||||||
if (nearEnough) {
|
if (nearEnough) {
|
||||||
this.enterNestBox(targetBox, dt || 0.016);
|
if (this.enterNestBox(targetBox, dt || 0.016)) return targetBox;
|
||||||
return targetBox;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
this.nestFade = clamp((this.nestFade || 0) - dt * 3.0, 0, 1);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,10 @@ function processForcedBehaviorQueue(tarinai, world, needs) {
|
||||||
return false;
|
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 = "" } = {}) {
|
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;
|
if (!tarinai) return;
|
||||||
const now = tarinai.world?.time || 0;
|
const now = tarinai.world?.time || 0;
|
||||||
|
|
@ -400,9 +404,11 @@ function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel =
|
||||||
}
|
}
|
||||||
const nextForced = forced ?? preserveForced;
|
const nextForced = forced ?? preserveForced;
|
||||||
const nextSource = nextForced ? "forced" : (source || previousBehavior.source || "state");
|
const nextSource = nextForced ? "forced" : (source || previousBehavior.source || "state");
|
||||||
const nextForcedId = forcedId || previousBehavior.forcedId || "";
|
const nextForcedId = nextForced ? (forcedId || previousBehavior.forcedId || "") : "";
|
||||||
const nextSourceReason = sourceReasonText || forcedReasonText || previousBehavior.sourceReasonText || "";
|
const nextSourceReason = nextForced ? (sourceReasonText || forcedReasonText || previousBehavior.sourceReasonText || "") : (sourceReasonText || forcedReasonText || "");
|
||||||
const nextCauseText = causeText || previousBehavior.causeText || "";
|
const explicitCauseText = String(causeText || "");
|
||||||
|
const previousCauseText = String(previousBehavior.causeText || "");
|
||||||
|
const nextCauseText = explicitCauseText || ((nextForced || !effectCauseTextLooksStale(previousCauseText)) ? previousCauseText : "");
|
||||||
if (nextCauseText) {
|
if (nextCauseText) {
|
||||||
nextReasonText = buildReasonText(nextNeed, existingTiedNeeds.length ? existingTiedNeeds : [nextNeed], { ...(action || {}), id: nextActionId, label: nextActionLabel, subNeed: subNeed || action?.subNeed || "" }, tarinai, tarinai.world || null, { causeText: 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,
|
causeText: nextCauseText,
|
||||||
forced: nextForced,
|
forced: nextForced,
|
||||||
forcedId: nextForcedId,
|
forcedId: nextForcedId,
|
||||||
forcedRequest: previousBehavior.forcedRequest || null,
|
forcedRequest: nextForced ? (previousBehavior.forcedRequest || null) : null,
|
||||||
priority: previousBehavior.priority ?? actionPriority(action),
|
priority: previousBehavior.priority ?? actionPriority(action),
|
||||||
phase,
|
phase,
|
||||||
startedAt: previousBehavior.startedAt || now,
|
startedAt: previousBehavior.startedAt || now,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ function roleMatches(item, role) {
|
||||||
if (item.type === "duplicator") return effectiveFoodRoleMatches(item, role);
|
if (item.type === "duplicator") return effectiveFoodRoleMatches(item, role);
|
||||||
if (item.roles?.[role]) return true;
|
if (item.roles?.[role]) return true;
|
||||||
if (role === "food" || role === "drink" || role === "medicine") return effectiveFoodRoleMatches(item, role);
|
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 === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(item.type);
|
||||||
if (role === "grassMaterial") return item.type === "grass";
|
if (role === "grassMaterial") return item.type === "grass";
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -65,21 +65,21 @@ function foodPriorityRank(tarinai, item) {
|
||||||
const need = typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need;
|
const need = typeof getTarinaiBehaviorNeed === "function" ? getTarinaiBehaviorNeed(tarinai) : tarinai?.behavior?.need;
|
||||||
const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0;
|
const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0;
|
||||||
const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 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 === "water") return 3650 + Math.max(0, healthNeed - 42) * 7;
|
||||||
if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6;
|
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 === "love_mochi") return 3340 + Math.min(120, Math.max(0, socialNeed - 45) * 3);
|
||||||
if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4;
|
if (type === "fight_mochi") return 3280 + Math.min(90, Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 3);
|
||||||
if (type === "sleep_drug") return need === "sleep" ? 3600 : 2450;
|
if (type === "sleep_drug") return need === "sleep" ? 3380 : 3220;
|
||||||
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return 3000 + Math.max(0, healthNeed - 45) * 5;
|
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") {
|
if (type === "grass") {
|
||||||
const hunger = Number(tarinai?.hunger || 0) || 0;
|
const hunger = Number(tarinai?.hunger || 0) || 0;
|
||||||
const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 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 === "zunchi") return 900 + (tarinai?.isZunchiSlave ? 180 : 0);
|
||||||
if (type === "food" || type === "ant_corpse") return 3000;
|
|
||||||
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return 3000;
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +141,9 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
||||||
: (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []);
|
: (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []);
|
||||||
const candidates = Array.isArray(primary) ? primary.slice() : [];
|
const candidates = Array.isArray(primary) ? primary.slice() : [];
|
||||||
if (foodEmergency && world?.itemsOfType) {
|
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) || []) {
|
for (const it of world.itemsOfType(type) || []) {
|
||||||
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
|
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) {
|
for (const item of candidates) {
|
||||||
if (!roleMatches(item, role)) continue;
|
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;
|
if (tarinai?.shouldAvoidTarget?.(item)) continue;
|
||||||
const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item);
|
const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item);
|
||||||
if (d > searchDist) continue;
|
if (d > searchDist) continue;
|
||||||
|
|
@ -164,40 +172,19 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleepPlaceAvailableFor(world, tarinai, item) {
|
function sleepPlaceAvailableFor(world, tarinai, item) {
|
||||||
if (!world || !tarinai || !item || item.dead) return false;
|
return globalThis.TarinaiNestSleepSystem?.sleepPlaceAvailableFor?.(world, tarinai, item) || 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) {
|
function nearestSleepPlaceOfType(world, tarinai, type, maxDist = 520) {
|
||||||
let best = null;
|
return globalThis.TarinaiNestSleepSystem?.nearestSleepPlaceOfType?.(world, tarinai, type, maxDist) || null;
|
||||||
let bestScore = Infinity;
|
}
|
||||||
const list = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world?.items || [];
|
|
||||||
for (const item of list) {
|
function findPrioritySleepPlaceBeforeGrassBedBuild(world, tarinai, maxDist = 520) {
|
||||||
if (!item || item.dead || item.type !== type) continue;
|
return globalThis.TarinaiNestSleepSystem?.findPrioritySleepPlaceBeforeGrassBedBuild?.(world, tarinai, maxDist) || null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNearestSleepPlace(world, tarinai, maxDist = 520) {
|
function findNearestSleepPlace(world, tarinai, maxDist = 520) {
|
||||||
return nearestSleepPlaceOfType(world, tarinai, "nest_box", maxDist)
|
return globalThis.TarinaiNestSleepSystem?.findNearestSleepPlace?.(world, tarinai, maxDist) || null;
|
||||||
|| nearestSleepPlaceOfType(world, tarinai, "bed", maxDist)
|
|
||||||
|| findOwnedStructure(world, tarinai, "grass_bed", maxDist)
|
|
||||||
|| findUnownedStructure(world, tarinai, "grass_bed", maxDist)
|
|
||||||
|| null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNearbyDanger(world, tarinai, maxDist = 150) {
|
function findNearbyDanger(world, tarinai, maxDist = 150) {
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,11 @@
|
||||||
"use strict";
|
"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.
|
// Need planning runtime and Tarinai prototype extensions.
|
||||||
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
|
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
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) {
|
function decayNeedSatisfaction(tarinai, dt = 0) {
|
||||||
|
|
@ -503,7 +474,7 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
|
||||||
source: behavior?.source || "need",
|
source: behavior?.source || "need",
|
||||||
forced: behaviorIsForced(behavior),
|
forced: behaviorIsForced(behavior),
|
||||||
priority: behavior?.priority || 0,
|
priority: behavior?.priority || 0,
|
||||||
causeText: behavior?.causeText || "",
|
causeText: (behaviorIsForced(behavior) || !/の効果$/.test(String(behavior?.causeText || "").trim().replace(/[。!?]+$/, ""))) ? (behavior?.causeText || "") : "",
|
||||||
sourceReasonText: behavior?.sourceReasonText || "",
|
sourceReasonText: behavior?.sourceReasonText || "",
|
||||||
forcedReasonText: behavior?.sourceReasonText || "",
|
forcedReasonText: behavior?.sourceReasonText || "",
|
||||||
forcedRequest: behavior?.forcedRequest || null,
|
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";
|
"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.
|
// Social, panic, fight, mate behavior runtime ticks.
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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) {
|
(function (global) {
|
||||||
|
|
@ -70,7 +41,7 @@
|
||||||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
|
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 (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
|
||||||
if (this.world.relationNotice?.(avoider.id, champion.id, "tarinai-champion-avoid", 18)) {
|
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;
|
avoider.lastNeedShockBreaker = champion;
|
||||||
|
|
|
||||||
|
|
@ -1,247 +1,16 @@
|
||||||
"use strict";
|
"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
|
// Layer: entity-runtime/tarinai/legacy
|
||||||
// Compatibility owner for the former monolithic Tarinai.prototype.update body.
|
// Compatibility shim. The former monolithic update body was removed so the
|
||||||
// The normal runtime path uses TarinaiUpdatePipeline concrete steps; this module
|
// concrete TarinaiUpdatePipeline remains the single runtime path.
|
||||||
// exists only as a stable fallback while update steps continue to be refined.
|
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function legacyUpdate(dt) {
|
function legacyUpdate(dt) {
|
||||||
if (this.dead) return;
|
return global.TarinaiUpdatePipeline?.updateOne?.(this, dt) || false;
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOne(tarinai, dt) {
|
function updateOne(tarinai, dt) {
|
||||||
if (!tarinai || tarinai.dead) return false;
|
if (!tarinai || tarinai.dead) return false;
|
||||||
legacyUpdate.call(tarinai, dt);
|
return global.TarinaiUpdatePipeline?.updateOne?.(tarinai, dt) !== false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWithThis(dt) {
|
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);
|
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) {
|
function hasPassivePhysicsNeed(tarinai, worldRef = null, isVisible = false) {
|
||||||
if (!tarinai || tarinai.dead) return false;
|
if (!tarinai || tarinai.dead) return false;
|
||||||
const vx = Number.isFinite(tarinai.vx) ? tarinai.vx : 0;
|
const vx = Number.isFinite(tarinai.vx) ? tarinai.vx : 0;
|
||||||
|
|
@ -36,8 +53,12 @@
|
||||||
const ivx = Number.isFinite(tarinai.impulseVx) ? tarinai.impulseVx : 0;
|
const ivx = Number.isFinite(tarinai.impulseVx) ? tarinai.impulseVx : 0;
|
||||||
const ivy = Number.isFinite(tarinai.impulseVy) ? tarinai.impulseVy : 0;
|
const ivy = Number.isFinite(tarinai.impulseVy) ? tarinai.impulseVy : 0;
|
||||||
if (Math.hypot(vx + ivx, vy + ivy) > 0.35) return true;
|
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 (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;
|
if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && tarinai.target.type !== "nest_box") return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +78,9 @@
|
||||||
const isNear = isVisible || Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect));
|
const isNear = isVisible || Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||||
const motionNeed = hasMotionNeed(tarinai);
|
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 =
|
const hardRealtimeUrgent =
|
||||||
(tarinai.hurtTimer || 0) > 0.04 ||
|
(tarinai.hurtTimer || 0) > 0.04 ||
|
||||||
(tarinai.pokeFlashTimer || 0) > 0.03 ||
|
(tarinai.pokeFlashTimer || 0) > 0.03 ||
|
||||||
|
|
@ -113,7 +137,7 @@
|
||||||
&& interval <= 0.75
|
&& interval <= 0.75
|
||||||
&& !worldRef.isTarinaiHiddenInNestBox?.(tarinai)
|
&& !worldRef.isTarinaiHiddenInNestBox?.(tarinai)
|
||||||
&& ((smoothAllowed && motionNeed) || sleepPhysical || eatPhysical));
|
&& ((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) {
|
function updateInterval(worldRef, tarinai, visibleRect) {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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
|
// Layer: entity-runtime/tarinai/update-step
|
||||||
|
|
@ -35,6 +6,24 @@
|
||||||
// pressure, and early freeze exits. This is extracted from the former
|
// pressure, and early freeze exits. This is extracted from the former
|
||||||
// Tarinai.prototype.update body without changing behavior.
|
// Tarinai.prototype.update body without changing behavior.
|
||||||
(function (global) {
|
(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 = {}) {
|
function update(t, dt, context = {}) {
|
||||||
if (!t || t.dead) return { done: true };
|
if (!t || t.dead) return { done: true };
|
||||||
t.prevX = t.x;
|
t.prevX = t.x;
|
||||||
|
|
@ -106,7 +95,7 @@
|
||||||
t.defeatedTimer = Math.max(0, t.defeatedTimer - dt);
|
t.defeatedTimer = Math.max(0, t.defeatedTimer - dt);
|
||||||
if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) {
|
if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) {
|
||||||
const winner = t.world.liveTarinaiById?.(t.defeatedById) || null;
|
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.fearTimer = Math.max(t.fearTimer, 1.0 * t.personalityProfile().fear);
|
||||||
t.hurtTimer = Math.max(t.hurtTimer, 1.8);
|
t.hurtTimer = Math.max(t.hurtTimer, 1.8);
|
||||||
t.fallTimer = Math.max(t.fallTimer, 1.15);
|
t.fallTimer = Math.max(t.fallTimer, 1.15);
|
||||||
|
|
@ -126,27 +115,27 @@
|
||||||
&& (t.fallTimer || 0) <= 0.04
|
&& (t.fallTimer || 0) <= 0.04
|
||||||
&& !t.dead
|
&& !t.dead
|
||||||
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(t.state || ""))
|
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(t.state || ""))
|
||||||
&& deterministicChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
&& detChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
||||||
const dir = deterministicSigned(t.world, "foot-massage-stumble-dir", t);
|
const dir = detSigned(t.world, "foot-massage-stumble-dir", t);
|
||||||
t.groundStumbleCooldown = deterministicRange(t.world, "foot-massage-stumble-cooldown", 8.5, 16.0, t);
|
t.groundStumbleCooldown = detRange(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));
|
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.fallMax = Math.max(t.fallMax || 1.05, t.fallTimer);
|
||||||
t.fallDir = dir;
|
t.fallDir = dir;
|
||||||
t.vx += dir * deterministicRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
t.vx += dir * detRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
||||||
t.vy += deterministicRange(t.world, "foot-massage-stumble-vy", -18, 18, 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.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 });
|
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.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)");
|
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)");
|
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();
|
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;
|
t.tempTimer += dt;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Layer: entity-runtime/tarinai/update-step
|
// 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 (global) {
|
||||||
function update(t, dt, context = {}) {
|
function update(t, dt) {
|
||||||
const legacyUpdate = context.legacyUpdate;
|
const ok = global.TarinaiUpdatePipeline?.updateOne?.(t, dt);
|
||||||
if (typeof legacyUpdate !== "function") return { done: true, ok: false };
|
return { done: true, ok: ok !== false };
|
||||||
legacyUpdate.call(t, dt);
|
|
||||||
return { done: true, ok: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiLegacyUpdateStep = Object.freeze({ update });
|
global.TarinaiLegacyUpdateStep = Object.freeze({ update });
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@
|
||||||
if (now - (t.lastVelocityShockDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false;
|
if (now - (t.lastVelocityShockDamageAt || -999) < SHOCK_DAMAGE_COOLDOWN) return false;
|
||||||
if (now - (t.lastPhysicalCollisionDamageAt || -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 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.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.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.fallTimer = Math.max(t.fallTimer || 0, amount > 12 ? 0.72 : 0.28);
|
||||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer || 0);
|
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer || 0);
|
||||||
|
|
@ -178,7 +179,8 @@
|
||||||
|
|
||||||
const dx = numeric(t.x) - beforeX;
|
const dx = numeric(t.x) - beforeX;
|
||||||
const dy = numeric(t.y) - beforeY;
|
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
|
// 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
|
// own velocity is zero. In that case swept movement will never run, so
|
||||||
// do a small contact pass every passive-physics tick.
|
// do a small contact pass every passive-physics tick.
|
||||||
|
|
@ -189,7 +191,7 @@
|
||||||
searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120),
|
searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120),
|
||||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||||
slop: 0.28,
|
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) {
|
if (pushed) {
|
||||||
t.lastSolidObstacleCollisionAt = world.time || 0;
|
t.lastSolidObstacleCollisionAt = world.time || 0;
|
||||||
|
|
@ -197,7 +199,7 @@
|
||||||
const cdx = numeric(t.x) - beforeContactX;
|
const cdx = numeric(t.x) - beforeContactX;
|
||||||
const cdy = numeric(t.y) - beforeContactY;
|
const cdy = numeric(t.y) - beforeContactY;
|
||||||
if (Math.hypot(cdx, cdy) > 0.05) {
|
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.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);
|
t.vy = clamp(numeric(t.vy) + cdy / Math.max(0.016, dt) * pushCarry, -170, 170);
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +226,7 @@
|
||||||
function update(t, dt, context = {}) {
|
function update(t, dt, context = {}) {
|
||||||
if (!t || t.dead) return { done: true };
|
if (!t || t.dead) return { done: true };
|
||||||
const motionDt = Math.max(0.001, Math.min(0.05, Number(context.motionDt ?? dt) || 0.016));
|
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);
|
return passivePhysicalMotion(t, motionDt, context);
|
||||||
}
|
}
|
||||||
const beforeX = Number.isFinite(t.x) ? t.x : 0;
|
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"),
|
logPushEnabled: document.getElementById("logPushEnabled"),
|
||||||
logPushOverlay: document.getElementById("logPushOverlay"),
|
logPushOverlay: document.getElementById("logPushOverlay"),
|
||||||
mobileModeControls: document.getElementById("mobileModeControls"),
|
mobileModeControls: document.getElementById("mobileModeControls"),
|
||||||
|
mobileRotateControls: document.getElementById("mobileRotateControls"),
|
||||||
backToGameBtn: document.getElementById("backToGameBtn"),
|
backToGameBtn: document.getElementById("backToGameBtn"),
|
||||||
soundCategoryControls: document.getElementById("soundPanel"),
|
soundCategoryControls: document.getElementById("soundPanel"),
|
||||||
toast: document.getElementById("toast"),
|
toast: document.getElementById("toast"),
|
||||||
|
|
@ -109,8 +110,6 @@ const uiCache = {
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchLastX: 0,
|
touchLastX: 0,
|
||||||
touchLastY: 0,
|
touchLastY: 0,
|
||||||
touchLastWorldX: 0,
|
|
||||||
touchLastWorldY: 0,
|
|
||||||
pinchStartDistance: 0,
|
pinchStartDistance: 0,
|
||||||
pinchStartZoom: 1,
|
pinchStartZoom: 1,
|
||||||
pinchFocusWorld: null,
|
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");
|
return Boolean(world?.tool && world.tool !== "observe");
|
||||||
}
|
}
|
||||||
function clickedInsideGameOrToolUi(target) {
|
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 = "") {
|
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);
|
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 } = {}) {
|
function markCollapsibleCardState(card, collapsed, { user = false } = {}) {
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
card.classList.toggle("collapsed", Boolean(collapsed));
|
card.classList.toggle("collapsed", Boolean(collapsed));
|
||||||
|
|
@ -160,6 +164,15 @@ function bindUI() {
|
||||||
markCollapsibleCardState(card, collapsed);
|
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() {
|
function applyMobileInitialPanelState() {
|
||||||
const touchFirst = Boolean(window.TarinaiInputMode?.classify?.().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
const touchFirst = Boolean(window.TarinaiInputMode?.classify?.().touchFirst || document.body.classList.contains("touch-mobile-ui"));
|
||||||
if (!touchFirst || uiCache.mobileInitialPanelStateApplied) return;
|
if (!touchFirst || uiCache.mobileInitialPanelStateApplied) return;
|
||||||
|
|
@ -171,8 +184,9 @@ function bindUI() {
|
||||||
markToolCategoryState(category, !keepOpen);
|
markToolCategoryState(category, !keepOpen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyDesktopInitialPanelState();
|
||||||
applyMobileInitialPanelState();
|
applyMobileInitialPanelState();
|
||||||
window.addEventListener("resize", applyMobileInitialPanelState, { passive: true });
|
window.addEventListener("resize", () => { applyDesktopInitialPanelState(); applyMobileInitialPanelState(); }, { passive: true });
|
||||||
|
|
||||||
function updateBackToGameButton() {
|
function updateBackToGameButton() {
|
||||||
const btn = ui.backToGameBtn;
|
const btn = ui.backToGameBtn;
|
||||||
|
|
@ -194,6 +208,15 @@ function bindUI() {
|
||||||
updateBackToGameButton();
|
updateBackToGameButton();
|
||||||
|
|
||||||
window.TarinaiEvents?.on?.("selection:changed", () => {
|
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 touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||||
const mode = window.TarinaiInputMode?.currentMode || uiCache.mobileInputMode || "auto";
|
const mode = window.TarinaiInputMode?.currentMode || uiCache.mobileInputMode || "auto";
|
||||||
if (!touchFirst || mode !== "auto" || world.tool !== "observe" || !world.selected || world.selected.dead) return;
|
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 || "") : "";
|
const type = typeof toolItemType === "function" ? toolItemType(world?.tool || "") : "";
|
||||||
return type && typeof isRotatableItemType === "function" && isRotatableItemType(type) ? type : "";
|
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) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
const tag = String(e.target?.tagName || "").toLowerCase();
|
const tag = String(e.target?.tagName || "").toLowerCase();
|
||||||
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
|
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
|
||||||
|
|
@ -218,14 +274,17 @@ function bindUI() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
world.rotateToolAngle(type, -step);
|
world.rotateToolAngle(type, -step);
|
||||||
render();
|
render();
|
||||||
|
syncMobileRotateControls();
|
||||||
} else if (key === "e" || key === "]") {
|
} else if (key === "e" || key === "]") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
world.rotateToolAngle(type, step);
|
world.rotateToolAngle(type, step);
|
||||||
render();
|
render();
|
||||||
|
syncMobileRotateControls();
|
||||||
} else if (key === "r") {
|
} else if (key === "r") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
world.setToolAngle?.(type, typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
|
world.setToolAngle?.(type, typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
|
||||||
render();
|
render();
|
||||||
|
syncMobileRotateControls();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -456,37 +515,61 @@ function bindUI() {
|
||||||
renderStats();
|
renderStats();
|
||||||
return;
|
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;
|
if (world.tool === tool && cycleToolSize(tool)) return;
|
||||||
selectTool(tool);
|
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) => {
|
const chainScroll = (el) => {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.addEventListener("wheel", (e) => {
|
el.addEventListener("wheel", (e) => {
|
||||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||||
const atTop = el.scrollTop <= 0;
|
const dy = normalizedWheelDeltaY(e);
|
||||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
const max = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||||
if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
const atTop = el.scrollTop <= 0.5;
|
||||||
const page = document.scrollingElement || document.documentElement;
|
const atBottom = el.scrollTop >= max - 0.5;
|
||||||
page.scrollTop += e.deltaY;
|
if (!((dy < 0 && atTop) || (dy > 0 && atBottom))) return;
|
||||||
}
|
e.preventDefault();
|
||||||
}, { passive: true });
|
e.stopPropagation();
|
||||||
|
applyScrollDelta(document.scrollingElement || document.documentElement, dy);
|
||||||
|
}, { passive: false });
|
||||||
};
|
};
|
||||||
const transferEdgeScroll = (child, parent) => {
|
const transferEdgeScroll = (child, parent) => {
|
||||||
if (!child || !parent) return;
|
if (!child || !parent) return;
|
||||||
child.addEventListener("wheel", (e) => {
|
child.addEventListener("wheel", (e) => {
|
||||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||||
|
const dy = normalizedWheelDeltaY(e);
|
||||||
const childCanScroll = child.scrollHeight > child.clientHeight + 1;
|
const childCanScroll = child.scrollHeight > child.clientHeight + 1;
|
||||||
const atTop = child.scrollTop <= 0;
|
const childMax = Math.max(0, child.scrollHeight - child.clientHeight);
|
||||||
const atBottom = child.scrollTop + child.clientHeight >= child.scrollHeight - 1;
|
const movingInside = childCanScroll && ((dy < 0 && child.scrollTop > 0.5) || (dy > 0 && child.scrollTop < childMax - 0.5));
|
||||||
const movingInside = childCanScroll && ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom));
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (movingInside) {
|
let rest = dy;
|
||||||
child.scrollTop += e.deltaY;
|
if (movingInside) rest = applyScrollDelta(child, dy);
|
||||||
return;
|
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 });
|
}, { passive: false });
|
||||||
};
|
};
|
||||||
const rightPanel = document.querySelector(".panel");
|
const rightPanel = document.querySelector(".panel");
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,9 @@ function chartSeries(mode = "population") {
|
||||||
];
|
];
|
||||||
if (mode === "objects") return [
|
if (mode === "objects") return [
|
||||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f8a3f" },
|
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f5a2f" },
|
||||||
{ key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" },
|
{ 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" },
|
{ key: "obj_water", label: "\u96e8\u6ef4", color: "#5aa8d6" },
|
||||||
];
|
];
|
||||||
return [{ key: "pop", label: "\u500b\u4f53", color: "#5d8ee6" }];
|
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 live = entity && !entity.dead ? entity : null;
|
||||||
const img = n ? tarinaiSpritePathForId(n.id) : SPRITES[0].path;
|
const img = n ? tarinaiSpritePathForId(n.id) : SPRITES[0].path;
|
||||||
const name = escapeHtml(n?.name || "\u4e0d\u660e");
|
const name = escapeHtml(n?.name || "\u4e0d\u660e");
|
||||||
const status = familyStatusText(n);
|
|
||||||
const gen = n?.generation ? `G${n.generation}` : "G?";
|
const gen = n?.generation ? `G${n.generation}` : "G?";
|
||||||
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
||||||
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
||||||
|
const status = n?.alive ? familyStatusText(n) : (briefReason || "死亡");
|
||||||
const reason = briefReason ? ` / ${briefReason}` : "";
|
const reason = briefReason ? ` / ${briefReason}` : "";
|
||||||
const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []);
|
const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []);
|
||||||
const tags = rawTags.slice();
|
const tags = rawTags.slice();
|
||||||
const displayTags = tags.slice(0, 4);
|
const displayTags = tags.slice(0, 4);
|
||||||
const tagText = tags.length ? ` / ${tags.join(" ")}` : "";
|
const tagText = tags.length ? ` / ${tags.join(" ")}` : "";
|
||||||
const title = escapeHtml(`${n?.name || "\u4e0d\u660e"} / \u4e16\u4ee3 ${n?.generation || "?"} / ${status}${reason}${tagText}`);
|
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 tagLine = displayTags.length ? `<span class="family-personality-tags">${displayTags.map(tag => `<em>${escapeHtml(tag)}</em>`).join("")}</span>` : "";
|
||||||
const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion);
|
const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion);
|
||||||
const statLine = n ? `<span class="family-genetic-stats" title="寿命 / 体格 / 攻撃 / 速度">
|
const statLine = n ? `<span class="family-genetic-stats" title="寿命 / 体格 / 攻撃 / 速度">
|
||||||
|
|
@ -385,10 +385,10 @@ function lineageBuildRowGroups(row, idSet) {
|
||||||
function lineageLayoutRows(rows, idSet) {
|
function lineageLayoutRows(rows, idSet) {
|
||||||
const NODE_W = 150;
|
const NODE_W = 150;
|
||||||
const NODE_H = 104;
|
const NODE_H = 104;
|
||||||
const SIBLING_GAP = 32;
|
const SIBLING_GAP = 24;
|
||||||
const GROUP_GAP = 52;
|
const GROUP_GAP = 42;
|
||||||
const PARTNER_GROUP_GAP = 24;
|
const PARTNER_GROUP_GAP = 18;
|
||||||
const ROW_GAP = 108;
|
const ROW_GAP = 96;
|
||||||
const LEFT_PAD = 82;
|
const LEFT_PAD = 82;
|
||||||
const TOP_PAD = 44;
|
const TOP_PAD = 44;
|
||||||
const RIGHT_PAD = 48;
|
const RIGHT_PAD = 48;
|
||||||
|
|
@ -447,7 +447,7 @@ function lineageLayoutRows(rows, idSet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const lefts = [];
|
const lefts = [];
|
||||||
const SOFT_EXTRA_GAP = 68;
|
const SOFT_EXTRA_GAP = 54;
|
||||||
for (let gIndex = 0; gIndex < groups.length; gIndex++) {
|
for (let gIndex = 0; gIndex < groups.length; gIndex++) {
|
||||||
const prevGroup = groups[gIndex - 1];
|
const prevGroup = groups[gIndex - 1];
|
||||||
const pairGap = prevGroup && prevGroup.items.length === 1 && groups[gIndex].items.length === 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) {
|
function lineageLayoutRowsFast(rows, idSet) {
|
||||||
const NODE_W = 150;
|
const NODE_W = 150;
|
||||||
const NODE_H = 104;
|
const NODE_H = 104;
|
||||||
const SIBLING_GAP = 22;
|
const SIBLING_GAP = 18;
|
||||||
const ROW_GAP = 92;
|
const ROW_GAP = 84;
|
||||||
const LEFT_PAD = 82;
|
const LEFT_PAD = 82;
|
||||||
const TOP_PAD = 44;
|
const TOP_PAD = 44;
|
||||||
const RIGHT_PAD = 48;
|
const RIGHT_PAD = 48;
|
||||||
|
|
|
||||||
|
|
@ -4,45 +4,17 @@
|
||||||
function bindMouseInput(ctx) {
|
function bindMouseInput(ctx) {
|
||||||
if (!ctx?.canvas) return false;
|
if (!ctx?.canvas) return false;
|
||||||
const { canvas, world, uiCache } = ctx;
|
const { canvas, world, uiCache } = ctx;
|
||||||
|
const actions = global.TarinaiPointerActionSystem?.create?.(ctx);
|
||||||
|
|
||||||
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||||
canvas.addEventListener("mousedown", (e) => {
|
canvas.addEventListener("mousedown", (e) => {
|
||||||
if (e.button === 0 && world.tool === "area_delete") {
|
if (e.button === 0) {
|
||||||
const p = ctx.screenToWorldEvent(e);
|
const p = ctx.screenToWorldEvent(e);
|
||||||
if (!p.inside) return;
|
const started = actions?.beginToolAt?.(p, e.clientX, e.clientY, { notifyNoTarget: world.tool === "pinch" });
|
||||||
e.preventDefault();
|
if (started) {
|
||||||
ctx.beginAreaDelete(p, e.clientX, e.clientY);
|
e.preventDefault();
|
||||||
return;
|
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;
|
|
||||||
}
|
}
|
||||||
if (e.button !== 2) return;
|
if (e.button !== 2) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -50,64 +22,20 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("mouseup", (e) => {
|
window.addEventListener("mouseup", (e) => {
|
||||||
if (ctx.endAreaDelete()) return;
|
if (actions?.endActive?.(e.clientX, e.clientY)) 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;
|
|
||||||
ctx.endPan(e.button);
|
ctx.endPan(e.button);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("mousemove", (e) => {
|
window.addEventListener("mousemove", (e) => {
|
||||||
if (uiCache.areaDeleting) {
|
const p = ctx.screenToWorldEvent(e);
|
||||||
const p = ctx.screenToWorldEvent(e);
|
if (actions?.moveActive?.(p, e.clientX, e.clientY)) return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
ctx.movePan(e.clientX, e.clientY);
|
ctx.movePan(e.clientX, e.clientY);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("click", (e) => {
|
canvas.addEventListener("click", (e) => {
|
||||||
if (uiCache.areaDeleteMoved) {
|
if (actions?.shouldSuppressClick?.()) return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
const p = ctx.screenToWorldEvent(e);
|
const p = ctx.screenToWorldEvent(e);
|
||||||
if (!p.inside) return;
|
actions?.dispatchTapAt?.(p);
|
||||||
window.TarinaiCommands?.dispatch?.(world, { type: "pointer.primary", x: p.x, y: p.y });
|
|
||||||
renderStats();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", (e) => ctx.updatePointerFromEvent(e));
|
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-auto", mode === "auto");
|
||||||
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
document.body.classList.toggle("mobile-mode-camera", mode === "camera");
|
||||||
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
document.body.classList.toggle("mobile-mode-tool", mode === "tool");
|
||||||
|
global.syncMobileRotateControls?.();
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
const enabled = Boolean(inputModeManager?.refresh?.().touchFirst);
|
||||||
document.body.classList.toggle("touch-mobile-ui", enabled);
|
document.body.classList.toggle("touch-mobile-ui", enabled);
|
||||||
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled;
|
||||||
|
global.syncMobileRotateControls?.();
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,6 +189,9 @@
|
||||||
rememberGrabOrigin(target);
|
rememberGrabOrigin(target);
|
||||||
uiCache.grabLastWorldX = p.x;
|
uiCache.grabLastWorldX = p.x;
|
||||||
uiCache.grabLastWorldY = p.y;
|
uiCache.grabLastWorldY = p.y;
|
||||||
|
uiCache.grabVelocityX = 0;
|
||||||
|
uiCache.grabVelocityY = 0;
|
||||||
|
uiCache.grabLastMoveAt = performance?.now?.() ?? Date.now();
|
||||||
uiCache.grabStartX = clientX || 0;
|
uiCache.grabStartX = clientX || 0;
|
||||||
uiCache.grabStartY = clientY || 0;
|
uiCache.grabStartY = clientY || 0;
|
||||||
target.playerHeld = true;
|
target.playerHeld = true;
|
||||||
|
|
@ -215,6 +220,11 @@
|
||||||
}
|
}
|
||||||
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
const dx = p.x - (uiCache.grabLastWorldX ?? p.x);
|
||||||
const dy = p.y - (uiCache.grabLastWorldY ?? p.y);
|
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.grabLastWorldX = p.x;
|
||||||
uiCache.grabLastWorldY = p.y;
|
uiCache.grabLastWorldY = p.y;
|
||||||
const beforeX = Number(target.x || 0) || 0;
|
const beforeX = Number(target.x || 0) || 0;
|
||||||
|
|
@ -238,8 +248,8 @@
|
||||||
target.prevY = target.y;
|
target.prevY = target.y;
|
||||||
}
|
}
|
||||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-move");
|
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-move");
|
||||||
target.vx = clamp(dx * 16, -160, 160);
|
target.vx = clamp(uiCache.grabVelocityX || dx * 16, -820, 820);
|
||||||
target.vy = clamp(dy * 16, -160, 160);
|
target.vy = clamp(uiCache.grabVelocityY || dy * 16, -820, 820);
|
||||||
if (uiCache.grabKind === "tarinai") {
|
if (uiCache.grabKind === "tarinai") {
|
||||||
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
target.goIdle?.("\u3064\u307e\u307e\u308c\u3066\u3044\u308b");
|
||||||
if (options.touch) target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
if (options.touch) target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8);
|
||||||
|
|
@ -268,9 +278,21 @@
|
||||||
target.targetKey = "";
|
target.targetKey = "";
|
||||||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
||||||
finishTarinaiGrab(target, clientX, clientY);
|
finishTarinaiGrab(target, clientX, clientY);
|
||||||
} else if (target.type === "ball") {
|
} else if (uiCache.grabKind === "item") {
|
||||||
target.prevX = target.x;
|
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
|
||||||
target.prevY = target.y;
|
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");
|
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release");
|
||||||
target.playerHeld = false;
|
target.playerHeld = false;
|
||||||
|
|
@ -286,6 +308,9 @@
|
||||||
uiCache.grabbing = false;
|
uiCache.grabbing = false;
|
||||||
uiCache.grabTarget = null;
|
uiCache.grabTarget = null;
|
||||||
uiCache.grabKind = "";
|
uiCache.grabKind = "";
|
||||||
|
uiCache.grabVelocityX = 0;
|
||||||
|
uiCache.grabVelocityY = 0;
|
||||||
|
uiCache.grabLastMoveAt = 0;
|
||||||
world.rebuildSpatial?.();
|
world.rebuildSpatial?.();
|
||||||
uiCache.grabLastSpatialAt = 0;
|
uiCache.grabLastSpatialAt = 0;
|
||||||
canvas.style.cursor = cursorForTool(world.tool);
|
canvas.style.cursor = cursorForTool(world.tool);
|
||||||
|
|
@ -571,6 +596,9 @@
|
||||||
uiCache.grabbing = false;
|
uiCache.grabbing = false;
|
||||||
uiCache.grabTarget = null;
|
uiCache.grabTarget = null;
|
||||||
uiCache.grabKind = "";
|
uiCache.grabKind = "";
|
||||||
|
uiCache.grabVelocityX = 0;
|
||||||
|
uiCache.grabVelocityY = 0;
|
||||||
|
uiCache.grabLastMoveAt = 0;
|
||||||
if (world) {
|
if (world) {
|
||||||
world.hoverGrabTarget = null;
|
world.hoverGrabTarget = null;
|
||||||
world.hoverDeleteTarget = null;
|
world.hoverDeleteTarget = null;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
function bindTouchInput(ctx) {
|
function bindTouchInput(ctx) {
|
||||||
if (!ctx?.canvas) return false;
|
if (!ctx?.canvas) return false;
|
||||||
const { canvas, world, uiCache } = ctx;
|
const { canvas, world, uiCache } = ctx;
|
||||||
|
const actions = global.TarinaiPointerActionSystem?.create?.(ctx);
|
||||||
ctx.bindMobileModeControls();
|
ctx.bindMobileModeControls();
|
||||||
const isMobileAutoScrollMode = () => {
|
const isMobileAutoScrollMode = () => {
|
||||||
const mode = ctx.mobileInputMode?.() || "auto";
|
const mode = ctx.mobileInputMode?.() || "auto";
|
||||||
|
|
@ -41,33 +42,13 @@
|
||||||
uiCache.touchStartY = t.clientY;
|
uiCache.touchStartY = t.clientY;
|
||||||
uiCache.touchLastX = t.clientX;
|
uiCache.touchLastX = t.clientX;
|
||||||
uiCache.touchLastY = t.clientY;
|
uiCache.touchLastY = t.clientY;
|
||||||
uiCache.touchLastWorldX = p.x;
|
|
||||||
uiCache.touchLastWorldY = p.y;
|
|
||||||
if (mode === "tool" && world.tool === "observe") {
|
if (mode === "tool" && world.tool === "observe") {
|
||||||
uiCache.touchMode = "toolTapOnly";
|
uiCache.touchMode = "toolTapOnly";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (world.tool === "shoot" && p.inside) {
|
const touchModeByTool = { shoot: "shoot", area_delete: "areaDelete", water_hose: "hose", pinch: "grab" };
|
||||||
ctx.beginShoot(p, t.clientX, t.clientY, { touchMode: "shoot" });
|
const touchMode = touchModeByTool[world.tool];
|
||||||
return;
|
if (touchMode && actions?.beginToolAt?.(p, t.clientX, t.clientY, { touch: true, touchMode })) 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;
|
|
||||||
uiCache.touchMode = autoPageScroll ? "autoTapOrScroll" : (mode === "tool" ? "toolTapOnly" : "panOrTap");
|
uiCache.touchMode = autoPageScroll ? "autoTapOrScroll" : (mode === "tool" ? "toolTapOnly" : "panOrTap");
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
|
@ -105,23 +86,7 @@
|
||||||
if (totalMove > 8) uiCache.touchMoved = true;
|
if (totalMove > 8) uiCache.touchMoved = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (uiCache.touchMode === "shoot" && uiCache.shooting) {
|
if (actions?.moveActive?.(p, t.clientX, t.clientY, { touch: true, renderFrame: uiCache.touchMode === "grab" })) return;
|
||||||
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 (uiCache.touchMode === "autoTapOrScroll") {
|
if (uiCache.touchMode === "autoTapOrScroll") {
|
||||||
if (totalMove > 7) uiCache.touchMoved = true;
|
if (totalMove > 7) uiCache.touchMoved = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -137,30 +102,15 @@
|
||||||
|
|
||||||
canvas.addEventListener("touchend", (e) => {
|
canvas.addEventListener("touchend", (e) => {
|
||||||
if (uiCache.touchMode !== "autoTapOrScroll") e.preventDefault();
|
if (uiCache.touchMode !== "autoTapOrScroll") e.preventDefault();
|
||||||
if (uiCache.areaDeleting) ctx.endAreaDelete();
|
if (actions?.endActive?.(e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0)) {
|
||||||
if (uiCache.hosing) { ctx.endHose(); if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } }
|
// Active tool ended; click suppression is handled through movement flags.
|
||||||
if (uiCache.shooting) ctx.endShoot();
|
|
||||||
if (uiCache.grabbing) {
|
|
||||||
ctx.endGrab(e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0);
|
|
||||||
} else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) {
|
} else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) {
|
||||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
actions?.dispatchTapClient?.(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();
|
|
||||||
}
|
|
||||||
} else if (uiCache.touchMode === "autoTapOrScroll" && !uiCache.touchMoved) {
|
} else if (uiCache.touchMode === "autoTapOrScroll" && !uiCache.touchMoved) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
actions?.dispatchTapClient?.(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();
|
|
||||||
}
|
|
||||||
} else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) {
|
} else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) {
|
||||||
const p = ctx.screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY);
|
actions?.dispatchTapClient?.(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!e.touches?.length) {
|
if (!e.touches?.length) {
|
||||||
uiCache.touchMode = "";
|
uiCache.touchMode = "";
|
||||||
|
|
@ -168,7 +118,7 @@
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
canvas.addEventListener("touchcancel", () => ctx.cancelPointerActions(), { passive: true });
|
canvas.addEventListener("touchcancel", () => actions?.cancel?.(), { passive: true });
|
||||||
return 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.soundMasterToggle) ui.soundMasterToggle.checked = Boolean(audio.enabled);
|
||||||
if (ui.soundBtn) {
|
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.classList.toggle("active", Boolean(audio.enabled));
|
||||||
ui.soundBtn.setAttribute("aria-expanded", ui.soundPanel && !ui.soundPanel.classList.contains("hidden") ? "true" : "false");
|
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";
|
const kind = entry.kind || "note";
|
||||||
if (!uiCache.logPushKinds?.has(kind)) return;
|
if (!uiCache.logPushKinds?.has(kind)) return;
|
||||||
const overlay = ui.logPushOverlay;
|
const overlay = ui.logPushOverlay;
|
||||||
audio.notify?.();
|
|
||||||
if (!overlay) return;
|
if (!overlay) return;
|
||||||
|
audio.notify?.();
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = `log-push-toast log-push-${kind}`;
|
card.className = `log-push-toast log-push-${kind}`;
|
||||||
const tag = document.createElement("span");
|
const tag = document.createElement("span");
|
||||||
|
|
@ -86,11 +86,6 @@ window.pushLogNotification = pushLogNotification;
|
||||||
function shouldShowInEventLog(entry = {}) {
|
function shouldShowInEventLog(entry = {}) {
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
if (entry.eventType === "player_item_placement") 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,13 +94,11 @@ function renderLog(logs) {
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
const touchFirst = Boolean(window.TarinaiInputMode?.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
|
||||||
const limit = touchFirst ? 34 : 80;
|
const limit = touchFirst ? 34 : 80;
|
||||||
const visibleLogs = Array.isArray(logs) ? logs.filter(raw => {
|
const visibleLogs = Array.isArray(logs)
|
||||||
const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw;
|
? logs.map(raw => typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw).filter(shouldShowInEventLog)
|
||||||
return shouldShowInEventLog(entry);
|
: [];
|
||||||
}) : [];
|
|
||||||
const rows = visibleLogs.slice(0, limit);
|
const rows = visibleLogs.slice(0, limit);
|
||||||
for (const raw of rows) {
|
for (const entry of rows) {
|
||||||
const entry = typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw;
|
|
||||||
const kind = entry.kind || "note";
|
const kind = entry.kind || "note";
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = `log-entry log-${kind}`;
|
div.className = `log-entry log-${kind}`;
|
||||||
|
|
@ -136,7 +129,7 @@ function formatLogTime(timeValue) {
|
||||||
function logKindLabel(kind) {
|
function logKindLabel(kind) {
|
||||||
return {
|
return {
|
||||||
birth: "\u8a95\u751f", death: "\u6b7b\u4ea1", fight: "\u55a7\u5629", food: "\u98df\u4e8b", grass: "\u8349",
|
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";
|
}[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";
|
"use strict";
|
||||||
|
|
||||||
const SCALABLE_TOOLS = new Set(scalableToolIds());
|
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"];
|
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");
|
const section = document.createElement("section");
|
||||||
section.className = `tool-category ${category.themeClass || ""}`.trim();
|
section.className = `tool-category ${category.themeClass || ""}`.trim();
|
||||||
section.dataset.toolCategory = category.id;
|
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";
|
const initiallyCollapsed = mobileCompact && category.id !== "operate";
|
||||||
if (initiallyCollapsed) section.classList.add("collapsed");
|
if (initiallyCollapsed) section.classList.add("collapsed");
|
||||||
const toggle = document.createElement("button");
|
const toggle = document.createElement("button");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const APP_VERSION = "39.15.47";
|
const APP_VERSION = "39.15.63";
|
||||||
const APP_BUILD = "tarinai-event-family-oshibyo-fix-v39-15-45";
|
const APP_BUILD = "tarinai-medium-unification-v39-15-60";
|
||||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,9 @@
|
||||||
"use strict";
|
"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) {
|
(function (global) {
|
||||||
const World = global.World;
|
const World = global.World;
|
||||||
if (!World) throw new Error("World is not available for mixin: world_combat_effects.js");
|
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({
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||||
damageAntsInRadius(x, y, radius, damageFn, reason = "", opts = {}) {
|
damageAntsInRadius(x, y, radius, damageFn, reason = "", opts = {}) {
|
||||||
let hit = 0;
|
let hit = 0;
|
||||||
|
|
@ -77,6 +40,39 @@
|
||||||
return hit;
|
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) {
|
explodeFirecracker(it) {
|
||||||
if (!it || it.dead || it.type !== "firecracker") return;
|
if (!it || it.dead || it.type !== "firecracker") return;
|
||||||
it.amount = 0;
|
it.amount = 0;
|
||||||
|
|
@ -164,14 +160,15 @@
|
||||||
const blast = 430 + p * 960;
|
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));
|
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);
|
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.lastPokedAt = this.time || 0;
|
||||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
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.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;
|
blastedBalls += 1;
|
||||||
}
|
}
|
||||||
|
const blastedPins = this.blastLoosePinsFrom(x, y, blastRadius, it, it.blastScale || 1, "firecracker");
|
||||||
audio.explode();
|
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) {
|
explodeDiseaseTarinai(t) {
|
||||||
|
|
@ -257,13 +254,18 @@
|
||||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
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.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");
|
t.die("\u7206\u767a\u75c5");
|
||||||
audio.explode();
|
audio.explode();
|
||||||
this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident", { participants: [t] });
|
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) {
|
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 speed = Math.hypot(dx, dy) || 1;
|
||||||
const nx = speed > 1 ? dx / speed : Math.cos(this.time * 9.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);
|
const ny = speed > 1 ? dy / speed : Math.sin(this.time * 7.7);
|
||||||
|
|
@ -279,7 +281,13 @@
|
||||||
} else if (it.type === "grass") {
|
} else if (it.type === "grass") {
|
||||||
it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 135 * (0.30 + p));
|
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));
|
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;
|
cleaned += p * 0.72;
|
||||||
} else if (it.type === "grass_bed") {
|
} else if (it.type === "grass_bed") {
|
||||||
it.amount = 0;
|
it.amount = 0;
|
||||||
|
|
@ -315,6 +323,7 @@
|
||||||
this.markItemBucketsDirty?.("water-hose-clean");
|
this.markItemBucketsDirty?.("water-hose-clean");
|
||||||
this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean");
|
this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean");
|
||||||
this.markSpatialDirty?.("water-hose-clean");
|
this.markSpatialDirty?.("water-hose-clean");
|
||||||
|
this.compactItems?.();
|
||||||
this.updateItemCounts();
|
this.updateItemCounts();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -412,31 +421,19 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
isSleepFurniture(it) {
|
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) {
|
isTarinaiHiddenInNestBox(t) {
|
||||||
return Boolean(t && !t.dead && t.insideNestBoxId);
|
return Boolean(globalThis.TarinaiNestSleepSystem?.isTarinaiHiddenInNestBox?.(this, t));
|
||||||
},
|
},
|
||||||
|
|
||||||
bedOccupancy(bed) {
|
bedOccupancy(bed) {
|
||||||
if (!this.isSleepFurniture(bed)) return 0;
|
return globalThis.TarinaiNestSleepSystem?.bedOccupancy?.(this, bed) || 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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
bedComfort(bed) {
|
bedComfort(bed) {
|
||||||
if (!this.isSleepFurniture(bed)) return 0.7;
|
return globalThis.TarinaiNestSleepSystem?.bedComfort?.(this, bed) ?? 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);
|
|
||||||
},
|
},
|
||||||
relationNotice(aId, bId, kind, cooldown = 30) {
|
relationNotice(aId, bId, kind, cooldown = 30) {
|
||||||
const key = [aId, bId].sort().join(":") + `:${kind}`;
|
const key = [aId, bId].sort().join(":") + `:${kind}`;
|
||||||
|
|
@ -478,9 +475,10 @@
|
||||||
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
||||||
const sourceLabel = (src) => {
|
const sourceLabel = (src) => {
|
||||||
if (!src) return "";
|
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 && typeof toolLabel === "function") return toolLabel(src.type);
|
||||||
if (src.type) return String(src.type);
|
if (src.type) return String(src.type);
|
||||||
|
if (src.name) return String(src.name);
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
let normalized = options.cause || cause || "\u885d\u7a81";
|
let normalized = options.cause || cause || "\u885d\u7a81";
|
||||||
|
|
@ -606,74 +604,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
itemDropImpact(it) {
|
itemDropImpact(it) {
|
||||||
if (!it || it.dead || it.type === "water") return;
|
return global.TarinaiImpactResponseSystem?.itemDropImpact?.(this, it);
|
||||||
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");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
limitBallChasers() {
|
limitBallChasers() {
|
||||||
if (!this.itemCounts?.ball) return;
|
if (!this.itemCounts?.ball) return;
|
||||||
|
|
@ -725,327 +656,20 @@
|
||||||
ball.amount = Math.max(ball.amount || 999, 999);
|
ball.amount = Math.max(ball.amount || 999, 999);
|
||||||
audio.ballHit?.();
|
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.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;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveBallBallCollisions(dt) {
|
resolveBallBallCollisions(dt) {
|
||||||
if ((this.itemCounts?.ball || 0) < 2) return;
|
return global.TarinaiCollisionResponseSystem?.resolveBallBallCollisions?.(this, dt);
|
||||||
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)" }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveBallInteractions(dt) {
|
resolveBallInteractions(dt) {
|
||||||
if (!this.itemCounts?.ball) return;
|
return global.TarinaiCollisionResponseSystem?.resolveBallInteractions?.(this, dt);
|
||||||
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)" }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveTarinaiHighSpeedCollisions(dt) {
|
resolveTarinaiHighSpeedCollisions(dt) {
|
||||||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
return global.TarinaiCollisionResponseSystem?.resolveTarinaiHighSpeedCollisions?.(this, dt);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
let reach = Math.max(32, Number(it.r || it.radius || 32) || 32);
|
let reach = Math.max(32, Number(it.r || it.radius || 32) || 32);
|
||||||
if (type === "fence" || type === "fence_v" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") reach = Math.max(120, reach * 3.9);
|
if (type === "fence" || type === "fence_v" || type === "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 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);
|
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) {
|
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 dx = x - (r.cx ?? 0);
|
||||||
const dy = y - (r.cy ?? 0);
|
const dy = y - (r.cy ?? 0);
|
||||||
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
|
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
|
||||||
|
|
@ -427,25 +429,25 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
rotatorWorldSegments(it) {
|
rotatorWorldSegments(it) {
|
||||||
return global.TarinaiMechanicalSystem?.worldSegments?.(it) || rotatorWorldSegmentsLocal(it);
|
return global.TarinaiMechanicalShapeSystem?.worldSegments?.(it) || rotatorWorldSegmentsLocal(it);
|
||||||
},
|
},
|
||||||
|
|
||||||
rotatorExtent(it) {
|
rotatorExtent(it) {
|
||||||
return global.TarinaiMechanicalSystem?.extent?.(it) || rotatorExtentLocal(it);
|
return global.TarinaiMechanicalShapeSystem?.extent?.(it) || rotatorExtentLocal(it);
|
||||||
},
|
},
|
||||||
|
|
||||||
rotatorObstacleRects(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 [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
reciprocatorObstacleRects(it) {
|
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 [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
rotatorBoundsAabb(it) {
|
rotatorBoundsAabb(it) {
|
||||||
const bounds = global.TarinaiMechanicalSystem?.boundsAabb?.(it);
|
const bounds = global.TarinaiMechanicalShapeSystem?.boundsAabb?.(it);
|
||||||
if (bounds) return bounds;
|
if (bounds) return bounds;
|
||||||
const rects = rotatorObstacleRectsLocal(it);
|
const rects = rotatorObstacleRectsLocal(it);
|
||||||
if (!rects.length) return null;
|
if (!rects.length) return null;
|
||||||
|
|
@ -465,7 +467,7 @@
|
||||||
const rect = this.fenceRect(it);
|
const rect = this.fenceRect(it);
|
||||||
return rect ? [{ ...rect, type: it.type, item: 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);
|
if (it.type === "nest_box") return this.nestBoxSolidRects(it);
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
@ -479,8 +481,8 @@
|
||||||
const r = box.r || 42;
|
const r = box.r || 42;
|
||||||
const entryD = distXY(t.x, t.y, entry.x, entry.y);
|
const entryD = distXY(t.x, t.y, entry.x, entry.y);
|
||||||
const centerD = distXY(t.x, t.y, box.x, box.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 aroundDoor = entryD <= Math.max(68, r * 1.32) || centerD <= Math.max(66, r * 1.18);
|
||||||
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 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);
|
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 maxRects = Math.max(12, Math.min(72, Number(requestedMaxRects || 0) || Math.ceil(limit * 2.25)));
|
||||||
const cacheable = !include && !exclude;
|
const cacheable = !include && !exclude;
|
||||||
const stats = this.nearbyQueryStatsThisFrame || null;
|
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 q = 48;
|
||||||
const qx = Math.floor((Number(x) || 0) / q);
|
const qx = Math.floor((Number(x) || 0) / q);
|
||||||
const qy = Math.floor((Number(y) || 0) / q);
|
const qy = Math.floor((Number(y) || 0) / q);
|
||||||
|
|
@ -582,7 +586,7 @@
|
||||||
let checked = 0;
|
let checked = 0;
|
||||||
const addRectsFor = (it) => {
|
const addRectsFor = (it) => {
|
||||||
if (!it || it.dead || it.type !== "poison_block" || it._poisonHazardQueryStamp === stamp) return false;
|
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;
|
if (distXY(x, y, it.x, it.y) > radius + reach + 36) return false;
|
||||||
const partRects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(it) || [];
|
const partRects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(it) || [];
|
||||||
if (!partRects.length) return false;
|
if (!partRects.length) return false;
|
||||||
|
|
@ -647,10 +651,7 @@
|
||||||
t.lastPoisonBlockBubbleAt = now;
|
t.lastPoisonBlockBubbleAt = now;
|
||||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||||
}
|
}
|
||||||
if (now >= (t.lastPoisonBlockEffectAt || -999) + 0.10) {
|
// 物理ブロック衝突の視覚エフェクトは出さない。接触時の状態変化はHPバーと吹き出しだけに限定する。
|
||||||
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)" }));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -668,6 +669,61 @@
|
||||||
return hit;
|
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 = {}) {
|
pushTarinaiOutOfRect(t, r, rr, opts = {}) {
|
||||||
if (!t || !r) return false;
|
if (!t || !r) return false;
|
||||||
const preVx = Number(t.vx || 0) || 0;
|
const preVx = Number(t.vx || 0) || 0;
|
||||||
|
|
@ -726,6 +782,8 @@
|
||||||
: Math.min(rr - d + slop, maxPush);
|
: Math.min(rr - d + slop, maxPush);
|
||||||
t.x += nx * push;
|
t.x += nx * push;
|
||||||
t.y += ny * push;
|
t.y += ny * push;
|
||||||
|
t.lastSolidObstacleCollisionSource = r.item || null;
|
||||||
|
t.lastSolidObstacleCollisionAt = this.time || 0;
|
||||||
if (r.bounce) {
|
if (r.bounce) {
|
||||||
const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08;
|
const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08;
|
||||||
const vx = t.vx || 0;
|
const vx = t.vx || 0;
|
||||||
|
|
@ -773,11 +831,6 @@
|
||||||
spinKick: 0,
|
spinKick: 0,
|
||||||
});
|
});
|
||||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, r.rotator ? 0.12 : 0.10);
|
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 if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18;
|
||||||
else t.vy *= -0.18;
|
else t.vy *= -0.18;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -911,9 +964,11 @@
|
||||||
return false;
|
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)) {
|
for (const it of this.nearbyItems(x, y, 96)) {
|
||||||
if (it === self || it.dead) continue;
|
if (it === self || it.dead) continue;
|
||||||
|
if (ignoreSoft && global.TarinaiPhysicsSoftClear?.isSoftPlacementType?.(it.type)) continue;
|
||||||
for (const r of this.solidObstacleRects(it)) {
|
for (const r of this.solidObstacleRects(it)) {
|
||||||
if (this.pointInRect(x, y, r, 10)) return true;
|
if (this.pointInRect(x, y, r, 10)) return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
"use strict";
|
"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) {
|
(function (global) {
|
||||||
|
|
@ -492,9 +463,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
nestBoxOccupants(box, limit = 5) {
|
nestBoxOccupants(box, limit = 5) {
|
||||||
if (!box || box.dead || box.type !== "nest_box") return [];
|
return globalThis.TarinaiNestSleepSystem?.nestBoxOccupants?.(this, box, limit) || [];
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
pointerNestBoxInfo() {
|
pointerNestBoxInfo() {
|
||||||
|
|
|
||||||
|
|
@ -167,14 +167,14 @@
|
||||||
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
<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="line" class="active">━ \u76f4\u7dda</button>
|
||||||
<button type="button" data-rotator-mode="free">\u81ea\u7531\u63cf\u753b</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-mode="erase">⌫ \u6d88\u3057\u30b4\u30e0</button>
|
||||||
<button type="button" data-rotator-template="bar">\u68d2</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="cross">+ \u5341\u5b57</button>
|
||||||
<button type="button" data-rotator-template="circle">\u5186\u3063\u307d\u3044</button>
|
<button type="button" data-rotator-template="circle">○ \u5186</button>
|
||||||
<button id="rotatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
<button id="rotatorUndo" type="button">↶ \u4e00\u3064\u623b\u3059</button>
|
||||||
<button id="rotatorClear" type="button">\u5168\u6d88\u3057</button>
|
<button id="rotatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
<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>
|
<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>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(dialog);
|
document.body.appendChild(dialog);
|
||||||
|
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "rotator");
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +204,7 @@
|
||||||
if (!item || (item.type !== "rotator" && item.type !== "poison_block")) return false;
|
if (!item || (item.type !== "rotator" && item.type !== "poison_block")) return false;
|
||||||
const isPoisonBlock = item.type === "poison_block";
|
const isPoisonBlock = item.type === "poison_block";
|
||||||
const dialog = ensureRotatorEditor();
|
const dialog = ensureRotatorEditor();
|
||||||
|
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "rotator");
|
||||||
const canvas = dialog.querySelector("#rotatorCanvas");
|
const canvas = dialog.querySelector("#rotatorCanvas");
|
||||||
const ctx = canvas?.getContext?.("2d");
|
const ctx = canvas?.getContext?.("2d");
|
||||||
const speed = dialog.querySelector("#mechMotorSpeed");
|
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);
|
for (const btn of dialog.querySelectorAll("[data-rotator-mode]")) btn.classList.toggle("active", btn.dataset.rotatorMode === mode);
|
||||||
canvas.style.cursor = mode === "erase" ? "cell" : "crosshair";
|
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;
|
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 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);
|
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));
|
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));
|
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||||
};
|
})();
|
||||||
const eraseAt = (p) => {
|
const eraseAt = (p) => {
|
||||||
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||||
const radius = Math.max(24, lw * 1.35);
|
const radius = Math.max(24, lw * 1.35);
|
||||||
|
|
@ -330,17 +332,8 @@
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const setTemplate = (name) => {
|
const setTemplate = (name) => {
|
||||||
if (name === "bar") segments = [[-130, 0, 130, 0]];
|
const shared = global.TarinaiPhysicsShapeEditorSystem?.templateSegments?.(name);
|
||||||
else if (name === "cross") segments = [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
if (shared) segments = shared.map(seg => seg.slice(0, 4));
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanSegments(); draw();
|
cleanSegments(); draw();
|
||||||
};
|
};
|
||||||
const syncPair = (a, b) => { if (!a || !b) return; a.oninput = () => { b.value = a.value; draw(); }; b.oninput = () => { a.value = b.value; 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>
|
<label><input id="reciprocatorSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
|
<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="line" class="active">━ \u76f4\u7dda</button>
|
||||||
<button type="button" data-reciprocator-mode="free">\u81ea\u7531\u63cf\u753b</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-mode="erase">⌫ \u6d88\u3057\u30b4\u30e0</button>
|
||||||
<button type="button" data-reciprocator-template="bar">\u68d2</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="cross">+ \u5341\u5b57</button>
|
||||||
<button type="button" data-reciprocator-template="circle">\u5186\u3063\u307d\u3044</button>
|
<button type="button" data-reciprocator-template="circle">○ \u5186</button>
|
||||||
<button id="reciprocatorUndo" type="button">\u4e00\u3064\u623b\u3059</button>
|
<button id="reciprocatorUndo" type="button">↶ \u4e00\u3064\u623b\u3059</button>
|
||||||
<button id="reciprocatorClear" type="button">\u5168\u6d88\u3057</button>
|
<button id="reciprocatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
<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>
|
<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>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(dialog);
|
document.body.appendChild(dialog);
|
||||||
|
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "reciprocator");
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReciprocatorEditor(item, worldRef) {
|
function openReciprocatorEditor(item, worldRef) {
|
||||||
if (!item || item.type !== "reciprocator") return false;
|
if (!item || item.type !== "reciprocator") return false;
|
||||||
const dialog = ensureReciprocatorEditor();
|
const dialog = ensureReciprocatorEditor();
|
||||||
|
global.TarinaiPhysicsShapeEditorSystem?.decorateToolbar?.(dialog, "reciprocator");
|
||||||
const canvas = dialog.querySelector("#reciprocatorCanvas");
|
const canvas = dialog.querySelector("#reciprocatorCanvas");
|
||||||
const ctx = canvas?.getContext?.("2d");
|
const ctx = canvas?.getContext?.("2d");
|
||||||
const angle = dialog.querySelector("#reciprocatorAngle");
|
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);
|
for (const btn of dialog.querySelectorAll("[data-reciprocator-mode]")) btn.classList.toggle("active", btn.dataset.reciprocatorMode === mode);
|
||||||
canvas.style.cursor = mode === "erase" ? "cell" : "crosshair";
|
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;
|
if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity;
|
||||||
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0;
|
||||||
const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 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);
|
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));
|
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));
|
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
|
||||||
};
|
})();
|
||||||
const eraseAt = (p) => {
|
const eraseAt = (p) => {
|
||||||
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||||
const radius = Math.max(28, lw * 1.55);
|
const radius = Math.max(28, lw * 1.55);
|
||||||
|
|
@ -616,17 +611,8 @@
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const setTemplate = (name) => {
|
const setTemplate = (name) => {
|
||||||
if (name === "bar") segments = [[-130, 0, 130, 0]];
|
const shared = global.TarinaiPhysicsShapeEditorSystem?.templateSegments?.(name);
|
||||||
else if (name === "cross") segments = [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
if (shared) segments = shared.map(seg => seg.slice(0, 4));
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanSegments(); draw();
|
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(); }; };
|
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) {
|
function copyTargetAt(worldRef, x, y) {
|
||||||
let blockedReason = "";
|
let blockedReason = "";
|
||||||
const collision = global.TarinaiCollisionFootprints;
|
const collision = global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints;
|
||||||
const items = worldRef?.items || [];
|
const items = worldRef?.items || [];
|
||||||
for (let i = items.length - 1; i >= 0; i--) {
|
for (let i = items.length - 1; i >= 0; i--) {
|
||||||
const it = items[i];
|
const it = items[i];
|
||||||
|
|
@ -911,7 +897,7 @@
|
||||||
let d = distXY(x, y, it.x, it.y);
|
let d = distXY(x, y, it.x, it.y);
|
||||||
let hit = d <= Math.max(30, (it.r || 24) * 1.35);
|
let hit = d <= Math.max(30, (it.r || 24) * 1.35);
|
||||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || worldRef?.isFenceType?.(it.type)) {
|
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);
|
hit = Boolean(result.hit);
|
||||||
d = Math.min(d, result.distance ?? d);
|
d = Math.min(d, result.distance ?? d);
|
||||||
}
|
}
|
||||||
|
|
@ -989,9 +975,13 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSoftPhysicsIgnoredItemType(type = "") {
|
function isSoftPlacementIgnoredItemType(type = "") {
|
||||||
const key = String(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) {
|
function softItemRadius(item) {
|
||||||
|
|
@ -999,42 +989,57 @@
|
||||||
return Math.max(8, Number(item.r || item.radius || itemRadiusFor?.(item.type, 12) || 12) || 12);
|
return Math.max(8, Number(item.r || item.radius || itemRadiusFor?.(item.type, 12) || 12) || 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
function physicsItemOverlapsSoftItem(worldRef, physicsItem, softItem) {
|
function placementRectsForItem(worldRef, item) {
|
||||||
if (!worldRef || !physicsItem || !softItem || softItem.dead) return false;
|
const solid = worldRef?.solidObstacleRects?.(item) || [];
|
||||||
const r = softItemRadius(softItem);
|
if (solid.length) return solid;
|
||||||
if (physicsItem.type === "rope" || physicsItem.type === "rod") {
|
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item?.type)) return global.TarinaiMechanicalSystem?.placementRects?.(item) || [];
|
||||||
const runtime = global.TarinaiLinkRuntime;
|
return solid;
|
||||||
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 clearSoftItemsUnderPhysicsItem(worldRef, item, reason = "physics-overlap-clear") {
|
function placementItemOverlapsSoftItem(worldRef, placedItem, softItem) {
|
||||||
if (!worldRef || !item || !isPhysicsObstacleItemType(item.type)) return 0;
|
if (!worldRef || !placedItem || !softItem || softItem.dead) return false;
|
||||||
let search = Math.max(80, Number(item.r || item.radius || 36) + 90);
|
const r = softItemRadius(softItem);
|
||||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) {
|
if (placedItem.type === "rope" || placedItem.type === "rod") {
|
||||||
search = Math.max(search, (global.TarinaiMechanicalSystem?.reach?.(item) || item.r || 80) + 96);
|
const runtime = global.TarinaiLinkRuntime;
|
||||||
} else if (item.type === "rope" || item.type === "rod") {
|
const pb = physicsApi();
|
||||||
search = Math.max(search, Number(physicsApi()?.linkScalar?.(item, "len", (item.r || 40) * 2 || 80)) * 0.55 + 96);
|
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 || []) {
|
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 (!soft || soft === item || soft.dead || !isSoftPlacementIgnoredItemType(soft.type)) continue;
|
||||||
if (!physicsItemOverlapsSoftItem(worldRef, item, soft)) continue;
|
if (placementItemOverlapsSoftItem(worldRef, item, soft)) out.push(soft);
|
||||||
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, 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,
|
reason,
|
||||||
userReason: "\u7269\u7406\u30a2\u30a4\u30c6\u30e0\u306b\u91cd\u306a\u3063\u305f",
|
userReason: "置きものが重なった",
|
||||||
wake: true,
|
wake: true,
|
||||||
});
|
});
|
||||||
if (ok) removed += 1;
|
if (ok) removed += 1;
|
||||||
|
|
@ -1047,34 +1052,35 @@
|
||||||
return removed;
|
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({
|
global.TarinaiPhysicsSoftClear = Object.freeze({
|
||||||
isPhysicsType: isPhysicsObstacleItemType,
|
isPhysicsType: isPhysicsObstacleItemType,
|
||||||
isSoftType: isSoftPhysicsIgnoredItemType,
|
isSoftType: isSoftPhysicsIgnoredItemType,
|
||||||
|
isSoftPlacementType: isSoftPlacementIgnoredItemType,
|
||||||
overlaps: physicsItemOverlapsSoftItem,
|
overlaps: physicsItemOverlapsSoftItem,
|
||||||
|
overlapsPlacement: placementItemOverlapsSoftItem,
|
||||||
|
collectForPlacement: collectSoftItemsUnderPlacementItem,
|
||||||
|
clearForPlacement: clearSoftItemsUnderPlacementItem,
|
||||||
clearForItem: clearSoftItemsUnderPhysicsItem,
|
clearForItem: clearSoftItemsUnderPhysicsItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
function clearOverlappingGrassBedsForPlacement(worldRef, item) {
|
function clearOverlappingGrassBedsForPlacement(worldRef, item) {
|
||||||
if (!worldRef || !item || item.type === "grass_bed" || item.type === "trace" || item.type === "splat") return 0;
|
return clearSoftItemsUnderPlacementItem(worldRef, item, "placement-soft-overlap-clear");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-change") {
|
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-change") {
|
||||||
if (!worldRef) return;
|
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;
|
worldRef.drawListDirty = true;
|
||||||
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
||||||
if (worldRef._visibleRenderStack) {
|
if (worldRef._visibleRenderStack) {
|
||||||
|
|
@ -1085,11 +1091,6 @@
|
||||||
}
|
}
|
||||||
worldRef.markItemBucketsDirty?.(reason);
|
worldRef.markItemBucketsDirty?.(reason);
|
||||||
worldRef.markSpatialDirty?.(reason);
|
worldRef.markSpatialDirty?.(reason);
|
||||||
worldRef.ensureSpatial?.(reason);
|
|
||||||
if (typeof global.render === "function") {
|
|
||||||
global.render();
|
|
||||||
global.requestAnimationFrame?.(() => global.render?.());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||||
|
|
@ -1156,30 +1157,34 @@
|
||||||
const otherIsGate = it.type === "gate_fence";
|
const otherIsGate = it.type === "gate_fence";
|
||||||
const margin = (item.type === "gate_fence" || otherIsGate) ? 1.5 : baseMargin;
|
const margin = (item.type === "gate_fence" || otherIsGate) ? 1.5 : baseMargin;
|
||||||
for (const r of this.solidObstacleRects(it)) {
|
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;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
importantPlacementOverlapBlocked(item) {
|
importantPlacementOverlapBlocked(item) {
|
||||||
return global.TarinaiCollisionFootprints?.itemPlacementOverlapBlocked?.(this, item) ?? false;
|
return (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.itemPlacementOverlapBlocked?.(this, item) ?? false;
|
||||||
},
|
},
|
||||||
|
|
||||||
placementBlocked(item) {
|
placementBlocked(item) {
|
||||||
if (!item || item.dead) return true;
|
if (!item || item.dead) return true;
|
||||||
if (item.type === "genkotsu") return false;
|
if (item.type === "genkotsu") return false;
|
||||||
const pad = CONFIG.worldPadding || 30;
|
|
||||||
const rects = this.solidObstacleRects(item);
|
const rects = this.solidObstacleRects(item);
|
||||||
if (rects.length) {
|
const sharedBoundsBlocked = global.TarinaiPlacementPreviewSystem?.boundsBlocked?.(this, item, { rects });
|
||||||
for (const rect of rects) {
|
if (sharedBoundsBlocked === true) return true;
|
||||||
if (rect.left < pad || rect.top < pad || rect.right > this.w - pad || rect.bottom > this.h - pad) 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.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true;
|
||||||
if (this.importantPlacementOverlapBlocked(item)) return true;
|
if (this.importantPlacementOverlapBlocked(item)) return true;
|
||||||
if (item.type === "nest_box") {
|
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;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
placementClampPointFor(item, x = item?.x || 0, y = item?.y || 0) {
|
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 pad = CONFIG.worldPadding || 30;
|
||||||
const rects = this.solidObstacleRects(item);
|
const rects = this.solidObstacleRects(item);
|
||||||
if (rects.length) {
|
if (rects.length) {
|
||||||
|
|
@ -1287,12 +1286,16 @@
|
||||||
hit = Boolean(result.hit);
|
hit = Boolean(result.hit);
|
||||||
d = Math.min(d, result.distance ?? d);
|
d = Math.min(d, result.distance ?? d);
|
||||||
} else {
|
} 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);
|
hit = Boolean(result.hit);
|
||||||
d = Math.min(d, result.distance ?? d);
|
d = Math.min(d, result.distance ?? d);
|
||||||
}
|
}
|
||||||
if (hit && d < foundItemD) { foundItem = it; foundItemD = 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 (foundItem && isPinType(foundItem.type) && foundItem.pinState === "lodged") return foundItem;
|
||||||
if (foundT) {
|
if (foundT) {
|
||||||
const lodged = foundT.currentLodgedPin?.() || (this.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === foundT.id);
|
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.dropTimer = 0;
|
||||||
item.dropImpactDone = true;
|
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}`);
|
const placed = this.addItem?.(item, `place:${item.type}`);
|
||||||
if (!placed) {
|
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`);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
clearSoftItemsUnderPhysicsItem(this, item, `place:${item.type}:soft-overlap-clear`);
|
|
||||||
this.emit("tool:place", { item, type: item.type, dropped });
|
this.emit("tool:place", { item, type: item.type, dropped });
|
||||||
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
||||||
this.drawListDirty = true;
|
this.drawListDirty = true;
|
||||||
|
|
@ -1360,6 +1363,9 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const wasSelectionEmpty = !this.selected;
|
||||||
|
if (found && wasSelectionEmpty) this._scrollSelectedDataTopOnNextSelection = true;
|
||||||
|
else this._scrollSelectedDataTopOnNextSelection = false;
|
||||||
global.TarinaiCommands?.setSelection?.(this, found) || (this.selected = found);
|
global.TarinaiCommands?.setSelection?.(this, found) || (this.selected = found);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1382,7 +1388,7 @@
|
||||||
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color }));
|
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color }));
|
||||||
this.drawListDirty = true;
|
this.drawListDirty = true;
|
||||||
audio.poke?.();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1432,8 +1438,8 @@
|
||||||
const placed = this.placeItem(rawItem, true);
|
const placed = this.placeItem(rawItem, true);
|
||||||
if (!placed) return;
|
if (!placed) return;
|
||||||
forceImmediateToolVisualRefresh(this, "tool-place");
|
forceImmediateToolVisualRefresh(this, "tool-place");
|
||||||
const label = toolLabel(itemType);
|
// Item placement intentionally no longer writes observation-log entries.
|
||||||
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" });
|
// Placement audio is still emitted through tool:placed.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1454,7 +1460,7 @@
|
||||||
|
|
||||||
shouldHideFromObservationRecords(entry = {}) {
|
shouldHideFromObservationRecords(entry = {}) {
|
||||||
const type = typeof entry === "string" ? "" : (entry.eventType || "");
|
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 = {}) {
|
log(text, kind = null, options = {}) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-action") {
|
function forceImmediateToolVisualRefresh(worldRef, reason = "tool-action") {
|
||||||
if (!worldRef) return;
|
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;
|
worldRef.drawListDirty = true;
|
||||||
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
if (worldRef._renderStack) worldRef._renderStack.signature = "";
|
||||||
if (worldRef._visibleRenderStack) {
|
if (worldRef._visibleRenderStack) {
|
||||||
|
|
@ -16,11 +19,6 @@
|
||||||
}
|
}
|
||||||
worldRef.markItemBucketsDirty?.(reason);
|
worldRef.markItemBucketsDirty?.(reason);
|
||||||
worldRef.markSpatialDirty?.(reason);
|
worldRef.markSpatialDirty?.(reason);
|
||||||
worldRef.ensureSpatial?.(reason);
|
|
||||||
if (typeof global.render === "function") {
|
|
||||||
global.render();
|
|
||||||
global.requestAnimationFrame?.(() => global.render?.());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||||
|
|
@ -149,7 +147,7 @@
|
||||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||||
const it = itemCandidates[i];
|
const it = itemCandidates[i];
|
||||||
if (!it || it.dead) continue;
|
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 isMechanism = Boolean(global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type));
|
||||||
const isFence = this.isFenceType?.(it.type) || false;
|
const isFence = this.isFenceType?.(it.type) || false;
|
||||||
const isConstraint = it.type === "rope" || it.type === "rod";
|
const isConstraint = it.type === "rope" || it.type === "rod";
|
||||||
|
|
@ -400,20 +398,45 @@
|
||||||
} else if (hit?.kind === "item") {
|
} else if (hit?.kind === "item") {
|
||||||
handled = true;
|
handled = true;
|
||||||
const target = hit.target;
|
const target = hit.target;
|
||||||
const shouldRemove = Math.random() < 0.30;
|
if (target && typeof isPinType === "function" && isPinType(target.type)) {
|
||||||
if (shouldRemove) {
|
const blasted = this.blastLoosePinsFrom?.(aim.x, aim.y, 92, target, 1.25, "shoot-tool") || 0;
|
||||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
|
if (!blasted && target.pinState !== "lodged") {
|
||||||
reason: "shoot-tool",
|
let dx = (target.x || 0) - aim.x;
|
||||||
userReason: "\u5c04\u6483\u3067\u6d88\u3048\u305f",
|
let dy = (target.y || 0) - aim.y;
|
||||||
wake: true,
|
let d = Math.hypot(dx, dy) || 1;
|
||||||
});
|
if (d < 0.001) {
|
||||||
if (removed) {
|
const a = deterministicAngle(this, "shoot-pin-overlap", target, aim.x, aim.y);
|
||||||
this.markItemBucketsDirty?.("shoot-delete");
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||||
this.markSpatialDirty?.("shoot-delete");
|
}
|
||||||
this.drawListDirty = true;
|
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 {
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,76 +72,10 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
evaluateColonyMood(force = false) {
|
evaluateColonyMood(force = false) {
|
||||||
const now = this.time || 0;
|
return global.TarinaiColonySituationSystem?.evaluate?.(this, force) || this.colonyMood || this.colonyMoodDefinition?.("relaxed");
|
||||||
if (!force && this.colonyMood && now < (this.nextColonyMoodEvalAt || 0)) return this.colonyMood;
|
|
||||||
const alive = (this.tarinai || []).filter(t => t && !t.dead);
|
|
||||||
const n = alive.length;
|
|
||||||
const avg = (fn) => n ? alive.reduce((sum, t) => sum + (Number(fn(t)) || 0), 0) / n : 0;
|
|
||||||
const count = (fn) => alive.reduce((sum, t) => sum + (fn(t) ? 1 : 0), 0);
|
|
||||||
const ratio = (value) => n ? value / n : 0;
|
|
||||||
const avgEnergy = avg(t => t.energy || 0);
|
|
||||||
const avgStress = avg(t => t.stress || 0);
|
|
||||||
const avgHunger = avg(t => t.hunger || 0);
|
|
||||||
const avgSafety = avg(t => t.needs?.safety || 0);
|
|
||||||
const panicCount = count(t => t.state === "panic" || (t.fearTimer || 0) > 0.2 || (t.panicTargetUntil || 0) > now);
|
|
||||||
const safetyHighCount = count(t => (t.needs?.safety || 0) >= 52 || (t.fearTimer || 0) > 0.35);
|
|
||||||
const weakCount = count(t => (t.energy || 0) < 45 || (t.hunger || 0) > 88 || (t.needs?.health || 0) > 58);
|
|
||||||
const diseasedCount = count(t => t.zunchiDisease || t.sleepDisease || t.explosionDisease || t.fightDisease);
|
|
||||||
const breedingCount = count(t => (t.loveMochiTimer || 0) > 0.1 || t.state === "birth_ritual" || (t.birthRitualTimer || 0) > 0.1);
|
|
||||||
const recentDeaths = (Array.isArray(this.recentDeathTimes) ? this.recentDeathTimes : []).filter(t => now - (Number(t) || -999) <= 30).length;
|
|
||||||
let nearestAvg = Infinity;
|
|
||||||
if (n >= 2) {
|
|
||||||
let totalNearest = 0;
|
|
||||||
for (const a of alive) {
|
|
||||||
let best = Infinity;
|
|
||||||
for (const b of alive) {
|
|
||||||
if (a === b) continue;
|
|
||||||
const dx = (a.x || 0) - (b.x || 0);
|
|
||||||
const dy = (a.y || 0) - (b.y || 0);
|
|
||||||
const d2 = dx * dx + dy * dy;
|
|
||||||
if (d2 < best) best = d2;
|
|
||||||
}
|
|
||||||
if (Number.isFinite(best)) totalNearest += Math.sqrt(best);
|
|
||||||
}
|
|
||||||
nearestAvg = totalNearest / n;
|
|
||||||
}
|
|
||||||
const recentShock = (now - (this.lastShootAt || -999) <= 8) || (now - (this.lastGenkotsuAt || -999) <= 8);
|
|
||||||
const panicRatio = ratio(panicCount);
|
|
||||||
const safetyRatio = ratio(safetyHighCount);
|
|
||||||
const weakRatio = ratio(weakCount);
|
|
||||||
const diseaseRatio = ratio(diseasedCount);
|
|
||||||
const breedingRatio = ratio(breedingCount);
|
|
||||||
const fieldId = String(this.fieldType || "garden");
|
|
||||||
const overcrowdLimit = fieldId === "cage" ? 20 : (fieldId === "park" ? 100 : 50);
|
|
||||||
const denseCrowd = Number.isFinite(nearestAvg) && nearestAvg < 84 && n >= Math.max(8, Math.floor(overcrowdLimit * 0.55));
|
|
||||||
const candidates = [];
|
|
||||||
const add = (id, priority, ok) => { if (ok) candidates.push({ id, priority }); };
|
|
||||||
|
|
||||||
// Severe or multi-signal situations are evaluated first. Simple population
|
|
||||||
// and comfort labels are deliberately fallback states so they do not mask
|
|
||||||
// richer current events.
|
|
||||||
add("devastation", 120, n <= 0 || recentDeaths >= 3 || (n <= 2 && this.deadCount > 0 && (recentDeaths > 0 || weakRatio >= 0.35 || panicRatio >= 0.20)));
|
|
||||||
add("crisis", 110, recentDeaths >= 2 || (avgEnergy < 38 && weakRatio >= 0.34) || (avgHunger > 88 && weakRatio >= 0.40));
|
|
||||||
add("confusion", 100, (recentShock && (panicRatio >= 0.10 || safetyRatio >= 0.18)) || panicRatio >= 0.25);
|
|
||||||
add("fearful", 90, (avgSafety >= 50 && safetyRatio >= 0.28) || (recentShock && safetyRatio >= 0.18));
|
|
||||||
add("disease_spread", 82, diseaseRatio >= 0.25 || (diseasedCount >= 3 && diseaseRatio >= 0.16));
|
|
||||||
add("weakened", 74, (avgEnergy < 54 && weakRatio >= 0.18) || avgHunger > 82 || weakRatio >= 0.34);
|
|
||||||
add("breeding", 66, breedingRatio >= 0.10 || breedingCount >= 2 || now - (this.lastBirthAt || -999) <= 45);
|
|
||||||
add("happy", 46, avgStress < 28 && panicRatio < 0.08 && diseaseRatio < 0.12 && weakRatio < 0.18);
|
|
||||||
add("overcrowded", 34, n >= overcrowdLimit || denseCrowd);
|
|
||||||
add("isolated", 28, n <= 1 || (n <= 3 && nearestAvg > Math.max(180, Math.min(this.w || 900, this.h || 600) * 0.32)));
|
|
||||||
|
|
||||||
let id = "relaxed";
|
|
||||||
if (candidates.length) {
|
|
||||||
candidates.sort((a, b) => b.priority - a.priority);
|
|
||||||
id = candidates[0].id;
|
|
||||||
}
|
|
||||||
const next = this.colonyMoodDefinition(id);
|
|
||||||
this.colonyMood = next;
|
|
||||||
this.nextColonyMoodEvalAt = now + 1.25;
|
|
||||||
return next;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
observeHighlightKind(t) {
|
observeHighlightKind(t) {
|
||||||
const s = this.selected;
|
const s = this.selected;
|
||||||
if (!s || !t || t === s || t.dead || this.tool !== "observe") return "";
|
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"
|
"index.html" = "app DOM shell, canvas, panels, dialogs, and ordered script tags"
|
||||||
"README.md" = "small AI entrypoint and read strategy"
|
"README.md" = "small AI entrypoint and read strategy"
|
||||||
"AI_MAP.md" = "subsystem map and task-oriented read sets"
|
"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"
|
"FILES.json" = "machine-readable file routing and glob semantics"
|
||||||
"service-worker.js" = "generated offline/cache service worker"
|
"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",
|
"index.html": "app DOM shell, canvas, panels, dialogs, and ordered script tags",
|
||||||
"README.md": "small AI entrypoint and read strategy",
|
"README.md": "small AI entrypoint and read strategy",
|
||||||
"AI_MAP.md": "subsystem map and task-oriented read sets",
|
"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",
|
"FILES.json": "machine-readable file routing and glob semantics",
|
||||||
"service-worker.js": "generated offline/cache service worker",
|
"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")
|
placement = read_text(ROOT / "js/world_placement_log.js")
|
||||||
if "playLogSoundForEntry" in placement:
|
if "playLogSoundForEntry" in placement:
|
||||||
fail("log sound mapping still lives in world_placement_log.js")
|
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")
|
ok("binary hash save snapshot, codec, storage, and restore coordinator present")
|
||||||
|
|
||||||
def check_unified_item_registry() -> None:
|
def check_unified_item_registry() -> None:
|
||||||
data = manifest()
|
data = manifest()
|
||||||
js_files = data["js"]
|
js_files = data["js"]
|
||||||
if "js/item_registry.js" not in js_files:
|
required_files = [
|
||||||
fail("unified item registry missing from manifest")
|
"js/item_tool_definitions.js",
|
||||||
removed = ["js/food_registry.js", "js/effect_registry.js", "js/item_visual_registry.js"]
|
"js/item_visual_definitions.js",
|
||||||
stale_manifest = [rel for rel in removed if rel in js_files]
|
"js/item_food_definitions.js",
|
||||||
if stale_manifest:
|
"js/item_effect_definitions.js",
|
||||||
fail("old split item registry files still listed in manifest: " + ", ".join(stale_manifest))
|
"js/item_registry.js",
|
||||||
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",
|
|
||||||
]
|
]
|
||||||
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:
|
if token not in source:
|
||||||
fail(f"unified item registry token missing: {token}")
|
fail(f"split 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))
|
|
||||||
data_source = read_text(ROOT / "js/data.js")
|
data_source = read_text(ROOT / "js/data.js")
|
||||||
forbidden_data = ["const TOOL_DEFINITIONS", "const ITEM_TRAITS", "class FoodRegistry", "class EffectRegistry", "TARINAI_ITEM_VISUALS"]
|
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]
|
offenders = [token for token in forbidden_data if token in data_source]
|
||||||
if offenders:
|
if offenders:
|
||||||
fail("item definitions still mixed into data.js: " + ", ".join(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:
|
def check_docs_removed() -> None:
|
||||||
docs = ROOT / "docs"
|
docs = ROOT / "docs"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "39.15.47";
|
const APP_VERSION = "39.15.63";
|
||||||
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||||
const v = `v=${APP_VERSION}`;
|
const v = `v=${APP_VERSION}`;
|
||||||
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
||||||
const coreScriptNames = [
|
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 = [
|
const CORE_ASSETS = [
|
||||||
"./",
|
"./",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue