This commit is contained in:
33333-33333 2026-06-29 21:02:04 +09:00
commit e00bfd8879
51 changed files with 1203 additions and 833 deletions

View file

@ -1,6 +1,6 @@
{
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
"version": "39.15.47",
"version": "39.15.75",
"entrypoints": {
"app": "index.html",
"runtime": "js/main.js",
@ -9,75 +9,159 @@
"service_worker": "service-worker.js"
},
"read_strategy": {
"default": ["README.md"],
"architecture": ["README.md", "AI_MAP.md"],
"exact_inventory": ["powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All", "python scripts/ai_inventory.py --all"],
"machine_routing": ["FILES.json"]
"default": [
"README.md"
],
"architecture": [
"README.md",
"AI_MAP.md"
],
"exact_inventory": [
"powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All",
"python scripts/ai_inventory.py --all"
],
"machine_routing": [
"FILES.json"
]
},
"groups": [
{
"id": "root",
"patterns": [".gitignore", ".htaccess", "app_manifest.json", "favicon.ico", "index.html", "README.md", "AI_MAP.md", "GAME_FEATURES.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."
},
{
"id": "css",
"patterns": ["css/*.css"],
"patterns": [
"css/*.css"
],
"meaning": "Base, layout, panel, component, and mobile responsive styling."
},
{
"id": "scripts",
"patterns": ["scripts/*.py", "scripts/*.ps1"],
"patterns": [
"scripts/*.py",
"scripts/*.ps1"
],
"meaning": "Generators, regression checks, and token-aware AI inventory tooling."
},
{
"id": "core",
"patterns": ["js/version.js", "js/event_bus.js", "js/registry_base.js", "js/disease_registry.js", "js/sound_pack.js", "js/data.js", "js/ground_types.js", "js/health.js", "js/input_mode_manager.js", "js/main.js", "js/math.js", "js/perf_profiler.js", "js/debug_tools.js"],
"patterns": [
"js/version.js",
"js/event_bus.js",
"js/registry_base.js",
"js/disease_registry.js",
"js/sound_pack.js",
"js/data.js",
"js/ground_types.js",
"js/health.js",
"js/input_mode_manager.js",
"js/main.js",
"js/math.js",
"js/perf_profiler.js",
"js/debug_tools.js"
],
"meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug."
},
{
"id": "assets_runtime",
"patterns": ["js/assets.js", "js/audio.js", "js/render.js", "js/tarinai_render.js", "js/item_render_runtime.js"],
"patterns": [
"js/assets.js",
"js/audio.js",
"js/render.js",
"js/tarinai_render.js",
"js/item_render_runtime.js"
],
"meaning": "Image/audio loading and canvas rendering."
},
{
"id": "simulation",
"patterns": ["js/sim_core.js", "js/simulation*.js", "js/system_order.js"],
"patterns": [
"js/sim_core.js",
"js/simulation*.js",
"js/system_order.js"
],
"meaning": "Simulation primitives, concrete systems, phase ordering."
},
{
"id": "world",
"patterns": ["js/world*.js", "js/weather_system.js"],
"patterns": [
"js/world*.js",
"js/weather_system.js"
],
"meaning": "World state/view/update/tools/environment/combat/family/social/ants."
},
{
"id": "physics",
"patterns": ["js/physics*.js", "js/mechanical_system.js", "js/constraint_system.js", "js/collision_footprint_system.js"],
"patterns": [
"js/physics*.js",
"js/mechanical_system.js",
"js/constraint_system.js",
"js/collision_footprint_system.js"
],
"meaning": "Geometry, collisions, constraints, mechanical item behavior."
},
{
"id": "items",
"patterns": ["js/item*.js", "js/items.js", "js/structures.js", "js/structure_lifecycle.js"],
"patterns": [
"js/item*.js",
"js/items.js",
"js/structures.js",
"js/structure_lifecycle.js"
],
"meaning": "Item/structure registry, lifecycle, dynamic behavior, rendering."
},
{
"id": "tarinai",
"patterns": ["js/tarinai*.js", "js/ants.js"],
"patterns": [
"js/tarinai*.js",
"js/ants.js"
],
"meaning": "Creature state/actions/needs/social life/item effects/disease/rendering plus ant actor model."
},
{
"id": "save",
"patterns": ["js/save*.js", "js/snapshot_system.js", "js/restore_coordinator.js", "js/history_system.js"],
"patterns": [
"js/save*.js",
"js/snapshot_system.js",
"js/restore_coordinator.js",
"js/history_system.js"
],
"meaning": "Snapshot schema/codec/storage/restore/history."
},
{
"id": "ui",
"patterns": ["js/ui*.js", "js/text_catalog.js", "js/command_dispatcher.js", "js/family_graph.js"],
"patterns": [
"js/ui*.js",
"js/text_catalog.js",
"js/command_dispatcher.js",
"js/family_graph.js"
],
"meaning": "DOM UI, input, tools, selected panel, logs, charts, family tree, text labels, command routing."
},
{
"id": "assets",
"patterns": ["assets/sprites/tarinai_*.webp", "assets/ui/tool_*", "assets/ui/favicon*", "assets/ui/apple-touch-icon.png", "assets/ui/ecology_*.webp", "assets/objects/*", "assets/sounds/*"],
"patterns": [
"assets/sprites/tarinai_*.webp",
"assets/ui/tool_*",
"assets/ui/favicon*",
"assets/ui/apple-touch-icon.png",
"assets/ui/ecology_*.webp",
"assets/objects/*",
"assets/sounds/*"
],
"meaning": "Creature sprites, UI/tool icons, field object images, voice/SFX samples."
}
],

View file

@ -67,7 +67,7 @@ The core loop is:
### Habitat
- **ずんち / zunchi**: tarinai waste; affects disease and can fertilize grass.
- **スプリンクラー / sprinkler**: cleans one target at a time in its effective radius.
- **ロボ掃除機 / robot_cleaner**: moves through the field at fixed normal speed 60, or 120 in high-speed mode. It checks the nearest matching cleanup target every 2 seconds, keeps pursuing it between scans, and cleans every matching object it touches. The 死骸 target also covers blood traces; clicking the item opens its target settings.
- **干草寝床 / 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.

View file

@ -2,6 +2,11 @@
AI-first entrypoint for a static browser simulation/game. Keep this file small; read deeper docs only when needed.
## v39.15.70 robot cleaner tuning
- Tuned ロボ掃除機 targeting: normal speed is 60, high-speed mode is 120, and speed no longer changes based on the selected cleanup targets.
- The cleaner checks the nearest matching target every 2 seconds, then keeps trying to clean that target between scans. Contact cleanup still removes every matching object it touches.
- The 死骸 target also covers blood traces. When たりない is enabled, nearby Tarinai panic while chased and are cleaned on contact.
## Read Strategy
- Start here for architecture, script order constraints, and where to look next.
@ -68,12 +73,11 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
- Oshibyo lodged position is anchored to the left side of the tarinai sprite.
## v39.15.47 family / sprinkler polish
## v39.15.47 family polish
- Family tree generation row spacing is tightened.
- Family tree personality tags also use a 2x2 grid (up to 4 visible tags).
- Sprinkler effective cleaning radius is increased to 3x the previous value and still filters targets by actual distance.
- Sprinkler rendering is redesigned to a more realistic yard-sprinkler look and now emits water-like spray effects when active.
## v39.15.47 owned object panic / shoot effect

View file

@ -1,5 +1,5 @@
{
"version": "39.15.65",
"version": "39.15.75",
"css": [
"css/base.css",
"css/layout.css",

View file

@ -524,6 +524,31 @@ h1 {
margin-top: 12px;
}
.robot-cleaner-editor-panel { width: min(300px, calc(100vw - 28px)); }
.robot-cleaner-editor-help {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
margin: 0 2px 10px;
}
.robot-cleaner-targets {
display: grid;
gap: 8px;
margin: 8px 0 2px;
}
.robot-cleaner-targets label {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255,255,255,0.58);
border: 1px solid rgba(84,68,48,0.11);
cursor: pointer;
user-select: none;
}
.robot-cleaner-targets input { accent-color: #70b242; }
/* v16.07.08: right menu fixed quick tabs */
.panel-quick-tabs {
position: sticky;

View file

@ -185,274 +185,57 @@ body::before {
.family-personality-tags {
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(13px, auto);
align-items: stretch;
overflow: visible !important;
text-overflow: clip !important;
gap: 2px 4px;
gap: 2px 3px;
margin-top: 2px;
white-space: normal !important;
max-height: none;
max-width: 84px;
max-width: 100%;
width: 100%;
}
.family-personality-tags em {
display: block;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: none;
padding: 1px 4px;
max-width: 100%;
padding: 1px 3px;
border-radius: 999px;
background: rgba(103,178,44,0.11);
border: 1px solid rgba(103,178,44,0.20);
color: rgba(64,72,48,0.86);
font-size: 8.2px;
font-style: normal;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.family-copy strong { font-size: 12.5px; }
.family-copy span { color: var(--muted); font-size: 9.6px; margin-top: 1px; }
.colony-card { overflow: visible; min-height: 0; flex: 0 0 auto; }
.colony-stats {
grid-template-columns: repeat(2, 1fr);
margin-bottom: 8px;
}
.colony-chart {
display: block;
width: 100%;
height: 188px;
min-height: 188px;
border: 1px solid rgba(84, 68, 48, 0.14);
border-radius: 14px;
background: linear-gradient(180deg, rgba(255,255,255,0.70), rgba(246,240,228,0.54));
overflow: hidden;
}
.colony-card h2 { line-height: 1.45; margin-bottom: 10px; }
.colony-chart svg { display: block; width: 100%; height: 100%; }
.colony-chart-legend {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
align-items: center;
margin: 4px 0 8px;
font-size: 11px;
color: var(--muted);
line-height: 1.25;
}
.colony-chart-legend span {
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.colony-chart-legend i {
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
box-shadow: 0 0 0 1px rgba(70, 52, 36, 0.10);
}
.relation-row strong { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
.relation-face {
width: 24px;
height: 24px;
object-fit: contain;
border-radius: 999px;
background: rgba(255,255,255,0.66);
border: 1px solid rgba(84,68,48,0.12);
vertical-align: middle;
flex: 0 0 auto;
}
.lineage-family {
padding: 10px;
margin: 0 0 12px;
border-radius: 15px;
background: rgba(255,253,248,0.62);
border: 1px solid rgba(84,68,48,0.12);
}
.lineage-family h3 {
margin: 0 0 8px;
font-size: 13px;
color: rgba(64,52,38,0.86);
}
/* Family archive: actual top-down tree diagram. */
.lineage-family-tree {
overflow-x: auto;
overflow-y: visible;
padding: 10px 10px 22px;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
}
.lineage-family-tree h3 {
position: sticky;
left: 0;
z-index: 3;
width: max-content;
padding-right: 12px;
background: rgba(255,253,248,0.82);
border-radius: 8px;
}
.lineage-family-tree .lineage-tree-panel {
position: relative;
line-height: 1.18;
overflow: visible;
min-width: 360px;
margin-top: 6px;
border-radius: 14px;
background:
linear-gradient(90deg, rgba(84,68,48,0.045) 1px, transparent 1px),
linear-gradient(180deg, rgba(84,68,48,0.040) 1px, transparent 1px),
rgba(255,250,244,0.48);
background-size: 34px 34px, 34px 34px, auto;
border: 1px solid rgba(84,68,48,0.10);
}
.lineage-family-tree .lineage-links {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
z-index: 1;
isolation: isolate;
}
.lineage-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.btn.active {
background: #efffde;
border-color: rgba(103,178,44,0.46);
font-weight: 700;
}
.lineage-render-status {
position: sticky;
top: 0;
left: 0;
z-index: 6;
width: max-content;
margin: 0 0 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: var(--muted);
background: rgba(255,255,255,0.88);
border: 1px solid rgba(84,68,48,0.10);
box-shadow: 0 4px 10px rgba(64,48,32,0.06);
}
.lineage-stale-status {
position: sticky;
top: 0;
left: 0;
z-index: 6;
width: max-content;
max-width: calc(100% - 12px);
margin: 0 0 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: rgba(92, 75, 52, 0.82);
background: rgba(255, 249, 232, 0.92);
border: 1px solid rgba(150, 115, 46, 0.16);
box-shadow: 0 4px 10px rgba(64,48,32,0.05);
}
.lineage-family-tree .lineage-links path {
fill: none;
stroke: rgba(107, 88, 61, 0.58);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.lineage-family-tree .lineage-links .partner-link {
stroke-width: 2.4;
stroke: rgba(98, 80, 56, 0.68);
stroke-dasharray: 5 4;
}
.lineage-family-tree .lineage-links .child-link {
stroke-width: 2;
stroke: rgba(107, 88, 61, 0.58);
}
.lineage-family-tree .tree-node {
position: absolute;
width: 150px;
height: 104px;
min-height: 104px;
box-sizing: border-box;
align-items: flex-start;
background: linear-gradient(180deg, #fffdf8, #f8f1e5);
box-shadow: 0 2px 5px rgba(64,48,32,0.035);
contain: layout paint style;
z-index: 2;
isolation: isolate;
}
.lineage-family-tree .tree-node.child {
border-color: rgba(103,178,44,0.34);
}
.lineage-family-tree .tree-node.dead {
opacity: 1;
filter: none;
}
.lineage-family-tree .tree-node[data-family-tarinai-id]:hover {
transform: none;
}
.lineage-family-tree .tree-node .family-portrait {
right: 4px;
top: 4px;
bottom: auto;
width: 48px;
height: 48px;
}
.lineage-family-tree .tree-generation-label {
position: absolute;
left: 10px;
width: 54px;
padding: 3px 0;
border-radius: 999px;
font-size: 11px;
color: var(--muted);
text-align: center;
background: rgba(255,255,255,0.70);
border: 1px solid rgba(84,68,48,0.08);
z-index: 2;
}
.lineage-family-tree .lineage-empty-family {
position: absolute;
left: 72px;
bottom: 8px;
padding: 5px 8px;
border-radius: 9px;
background: rgba(255,255,255,0.70);
}
@media (max-width: 720px) {
.ecology-grid { grid-template-columns: 1fr; }
.ecology-card { grid-template-columns: 68px minmax(0, 1fr); }
.ecology-card img { width: 68px; height: 68px; }
}
@media (max-width: 720px) {
.lineage-main-content {
height: 70vh;
min-height: 420px;
max-height: none;
}
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
word-break: keep-all;
}
.tool[data-tip] { position: relative; }
.lineage-family-tree .tree-node .family-copy {
max-width: calc(100% - 46px);
max-width: calc(100% - 8px);
width: 100%;
}
.lineage-family-tree .tree-node .family-copy strong,
.lineage-family-tree .tree-node .family-copy > span:not(.family-genetic-stats):not(.family-fight-stat):not(.family-personality-tags) {
max-width: 100%;
}
.lineage-family-tree .tree-node .family-personality-tags {
max-width: 100%;
width: 100%;
}
.lineage-family-tree .tree-node .family-personality-tags em {
overflow: visible;
text-overflow: clip;
white-space: normal;
}

View file

@ -7,11 +7,11 @@
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
<link rel="stylesheet" href="css/base.css?v=39.15.65" />
<link rel="stylesheet" href="css/layout.css?v=39.15.65" />
<link rel="stylesheet" href="css/panel.css?v=39.15.65" />
<link rel="stylesheet" href="css/components.css?v=39.15.65" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.65" />
<link rel="stylesheet" href="css/base.css?v=39.15.76" />
<link rel="stylesheet" href="css/layout.css?v=39.15.76" />
<link rel="stylesheet" href="css/panel.css?v=39.15.76" />
<link rel="stylesheet" href="css/components.css?v=39.15.76" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.76" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -201,152 +201,152 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=39.15.65" defer></script>
<script src="js/event_bus.js?v=39.15.65" defer></script>
<script src="js/registry_base.js?v=39.15.65" defer></script>
<script src="js/disease_registry.js?v=39.15.65" defer></script>
<script src="js/sound_pack.js?v=39.15.65" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.65" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.65" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.65" defer></script>
<script src="js/item_food_definitions.js?v=39.15.65" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.65" defer></script>
<script src="js/item_registry.js?v=39.15.65" defer></script>
<script src="js/data.js?v=39.15.65" defer></script>
<script src="js/ground_types.js?v=39.15.65" defer></script>
<script src="js/input_mode_manager.js?v=39.15.65" defer></script>
<script src="js/math.js?v=39.15.65" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.65" defer></script>
<script src="js/physics_helpers.js?v=39.15.65" defer></script>
<script src="js/placement_preview_system.js?v=39.15.65" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.65" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.65" defer></script>
<script src="js/mechanical_system.js?v=39.15.65" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.65" defer></script>
<script src="js/constraint_system.js?v=39.15.65" defer></script>
<script src="js/physics_world_system.js?v=39.15.65" defer></script>
<script src="js/physics_projection_system.js?v=39.15.65" defer></script>
<script src="js/assets.js?v=39.15.65" defer></script>
<script src="js/audio.js?v=39.15.65" defer></script>
<script src="js/perf_profiler.js?v=39.15.65" defer></script>
<script src="js/render.js?v=39.15.65" defer></script>
<script src="js/sim_core.js?v=39.15.65" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.65" defer></script>
<script src="js/structures.js?v=39.15.65" defer></script>
<script src="js/items.js?v=39.15.65" defer></script>
<script src="js/item_type_initializers.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.65" defer></script>
<script src="js/item_update_policy.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.65" defer></script>
<script src="js/item_runtime.js?v=39.15.65" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.65" defer></script>
<script src="js/item_render_runtime.js?v=39.15.65" defer></script>
<script src="js/ants.js?v=39.15.65" defer></script>
<script src="js/health.js?v=39.15.65" defer></script>
<script src="js/tarinai.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.65" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.65" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.65" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.65" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.65" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.65" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.65" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.65" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.65" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.65" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.65" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.65" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.65" defer></script>
<script src="js/tarinai_render.js?v=39.15.65" defer></script>
<script src="js/world.js?v=39.15.65" defer></script>
<script src="js/world_view.js?v=39.15.65" defer></script>
<script src="js/family_graph.js?v=39.15.65" defer></script>
<script src="js/world_family_social.js?v=39.15.65" defer></script>
<script src="js/impact_core_system.js?v=39.15.65" defer></script>
<script src="js/impact_response_system.js?v=39.15.65" defer></script>
<script src="js/collision_response_system.js?v=39.15.65" defer></script>
<script src="js/world_combat_effects.js?v=39.15.65" defer></script>
<script src="js/world_environment.js?v=39.15.65" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.65" defer></script>
<script src="js/world_ants_system.js?v=39.15.65" defer></script>
<script src="js/weather_system.js?v=39.15.65" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.65" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.65" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.65" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.65" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.65" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.65" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.65" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.65" defer></script>
<script src="js/simulation_systems.js?v=39.15.65" defer></script>
<script src="js/system_order.js?v=39.15.65" defer></script>
<script src="js/simulation.js?v=39.15.65" defer></script>
<script src="js/colony_situation_system.js?v=39.15.65" defer></script>
<script src="js/world_update.js?v=39.15.65" defer></script>
<script src="js/world_tool_actions.js?v=39.15.65" defer></script>
<script src="js/world_placement_log.js?v=39.15.65" defer></script>
<script src="js/world_event_effects.js?v=39.15.65" defer></script>
<script src="js/command_dispatcher.js?v=39.15.65" defer></script>
<script src="js/text_catalog.js?v=39.15.65" defer></script>
<script src="js/ui.js?v=39.15.65" defer></script>
<script src="js/ui_log.js?v=39.15.65" defer></script>
<script src="js/ui_helpers.js?v=39.15.65" defer></script>
<script src="js/ui_selected.js?v=39.15.65" defer></script>
<script src="js/save_schema.js?v=39.15.65" defer></script>
<script src="js/snapshot_system.js?v=39.15.65" defer></script>
<script src="js/restore_coordinator.js?v=39.15.65" defer></script>
<script src="js/history_system.js?v=39.15.65" defer></script>
<script src="js/save_codec.js?v=39.15.65" defer></script>
<script src="js/save_storage.js?v=39.15.65" defer></script>
<script src="js/save_system.js?v=39.15.65" defer></script>
<script src="js/ui_tooltips.js?v=39.15.65" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.65" defer></script>
<script src="js/ui_ground.js?v=39.15.65" defer></script>
<script src="js/ui_tools.js?v=39.15.65" defer></script>
<script src="js/ui_input_shared.js?v=39.15.65" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.65" defer></script>
<script src="js/ui_input_touch.js?v=39.15.65" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.65" defer></script>
<script src="js/ui_bind.js?v=39.15.65" defer></script>
<script src="js/ui_charts.js?v=39.15.65" defer></script>
<script src="js/ui_family_data.js?v=39.15.65" defer></script>
<script src="js/ui_family_async.js?v=39.15.65" defer></script>
<script src="js/ui_family_layout.js?v=39.15.65" defer></script>
<script src="js/ui_family_paths.js?v=39.15.65" defer></script>
<script src="js/ui_family_render.js?v=39.15.65" defer></script>
<script src="js/main.js?v=39.15.65" defer></script>
<script src="js/debug_tools.js?v=39.15.65" defer></script>
<script src="js/version.js?v=39.15.76" defer></script>
<script src="js/event_bus.js?v=39.15.76" defer></script>
<script src="js/registry_base.js?v=39.15.76" defer></script>
<script src="js/disease_registry.js?v=39.15.76" defer></script>
<script src="js/sound_pack.js?v=39.15.76" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.76" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.76" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.76" defer></script>
<script src="js/item_food_definitions.js?v=39.15.76" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.76" defer></script>
<script src="js/item_registry.js?v=39.15.76" defer></script>
<script src="js/data.js?v=39.15.76" defer></script>
<script src="js/ground_types.js?v=39.15.76" defer></script>
<script src="js/input_mode_manager.js?v=39.15.76" defer></script>
<script src="js/math.js?v=39.15.76" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.76" defer></script>
<script src="js/physics_helpers.js?v=39.15.76" defer></script>
<script src="js/placement_preview_system.js?v=39.15.76" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.76" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.76" defer></script>
<script src="js/mechanical_system.js?v=39.15.76" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.76" defer></script>
<script src="js/constraint_system.js?v=39.15.76" defer></script>
<script src="js/physics_world_system.js?v=39.15.76" defer></script>
<script src="js/physics_projection_system.js?v=39.15.76" defer></script>
<script src="js/assets.js?v=39.15.76" defer></script>
<script src="js/audio.js?v=39.15.76" defer></script>
<script src="js/perf_profiler.js?v=39.15.76" defer></script>
<script src="js/render.js?v=39.15.76" defer></script>
<script src="js/sim_core.js?v=39.15.76" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.76" defer></script>
<script src="js/structures.js?v=39.15.76" defer></script>
<script src="js/items.js?v=39.15.76" defer></script>
<script src="js/item_type_initializers.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.76" defer></script>
<script src="js/item_update_policy.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.76" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.76" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.76" defer></script>
<script src="js/item_runtime.js?v=39.15.76" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.76" defer></script>
<script src="js/item_render_runtime.js?v=39.15.76" defer></script>
<script src="js/ants.js?v=39.15.76" defer></script>
<script src="js/health.js?v=39.15.76" defer></script>
<script src="js/tarinai.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.76" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.76" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.76" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.76" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.76" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.76" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.76" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.76" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.76" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.76" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.76" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.76" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.76" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.76" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.76" defer></script>
<script src="js/tarinai_render.js?v=39.15.76" defer></script>
<script src="js/world.js?v=39.15.76" defer></script>
<script src="js/world_view.js?v=39.15.76" defer></script>
<script src="js/family_graph.js?v=39.15.76" defer></script>
<script src="js/world_family_social.js?v=39.15.76" defer></script>
<script src="js/impact_core_system.js?v=39.15.76" defer></script>
<script src="js/impact_response_system.js?v=39.15.76" defer></script>
<script src="js/collision_response_system.js?v=39.15.76" defer></script>
<script src="js/world_combat_effects.js?v=39.15.76" defer></script>
<script src="js/world_environment.js?v=39.15.76" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.76" defer></script>
<script src="js/world_ants_system.js?v=39.15.76" defer></script>
<script src="js/weather_system.js?v=39.15.76" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.76" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.76" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.76" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.76" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.76" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.76" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.76" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.76" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.76" defer></script>
<script src="js/simulation_systems.js?v=39.15.76" defer></script>
<script src="js/system_order.js?v=39.15.76" defer></script>
<script src="js/simulation.js?v=39.15.76" defer></script>
<script src="js/colony_situation_system.js?v=39.15.76" defer></script>
<script src="js/world_update.js?v=39.15.76" defer></script>
<script src="js/world_tool_actions.js?v=39.15.76" defer></script>
<script src="js/world_placement_log.js?v=39.15.76" defer></script>
<script src="js/world_event_effects.js?v=39.15.76" defer></script>
<script src="js/command_dispatcher.js?v=39.15.76" defer></script>
<script src="js/text_catalog.js?v=39.15.76" defer></script>
<script src="js/ui.js?v=39.15.76" defer></script>
<script src="js/ui_log.js?v=39.15.76" defer></script>
<script src="js/ui_helpers.js?v=39.15.76" defer></script>
<script src="js/ui_selected.js?v=39.15.76" defer></script>
<script src="js/save_schema.js?v=39.15.76" defer></script>
<script src="js/snapshot_system.js?v=39.15.76" defer></script>
<script src="js/restore_coordinator.js?v=39.15.76" defer></script>
<script src="js/history_system.js?v=39.15.76" defer></script>
<script src="js/save_codec.js?v=39.15.76" defer></script>
<script src="js/save_storage.js?v=39.15.76" defer></script>
<script src="js/save_system.js?v=39.15.76" defer></script>
<script src="js/ui_tooltips.js?v=39.15.76" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.76" defer></script>
<script src="js/ui_ground.js?v=39.15.76" defer></script>
<script src="js/ui_tools.js?v=39.15.76" defer></script>
<script src="js/ui_input_shared.js?v=39.15.76" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.76" defer></script>
<script src="js/ui_input_touch.js?v=39.15.76" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.76" defer></script>
<script src="js/ui_bind.js?v=39.15.76" defer></script>
<script src="js/ui_charts.js?v=39.15.76" defer></script>
<script src="js/ui_family_data.js?v=39.15.76" defer></script>
<script src="js/ui_family_async.js?v=39.15.76" defer></script>
<script src="js/ui_family_layout.js?v=39.15.76" defer></script>
<script src="js/ui_family_paths.js?v=39.15.76" defer></script>
<script src="js/ui_family_render.js?v=39.15.76" defer></script>
<script src="js/main.js?v=39.15.76" defer></script>
<script src="js/debug_tools.js?v=39.15.76" defer></script>
</body>
</html>

View file

@ -158,6 +158,10 @@
return Math.max(14, reach);
}
const radiusFor = typeof global.itemRadiusFor === "function" ? global.itemRadiusFor : ((type, fallback = 12) => fallback);
if (item.type === "duplicator") {
// 保存済みデータに古い r=34 が残っていても、配置判定は外観に近い半径で扱う。
return Math.max(14, Math.min(24, num(item.r, radiusFor(item.type, 24)) || radiusFor(item.type, 24)));
}
return Math.max(14, num(item.r, radiusFor(item.type, 12)) || radiusFor(item.type, 12));
}
@ -191,8 +195,10 @@
const hub = Math.max(18, Math.min(40, (global.TarinaiPhysicsBodySystem.scalar(item, "thickness", 12) ?? 12) * 2.3));
if (Math.hypot(num(x) - num(item.x), num(y) - num(item.y)) <= hub + padding) hit = true;
} else if (!rects.length) {
const mul = num(opts.radiusMultiplier, item.type === "ball" ? 4.0 : 2.2);
const r = Math.max(24, num(item.r, 12) * mul);
const baseRadius = item.type === "duplicator" ? Math.min(24, num(item.r, 24) || 24) : num(item.r, 12);
const defaultMul = item.type === "ball" ? 4.0 : (item.type === "duplicator" ? 1.45 : 2.2);
const mul = num(opts.radiusMultiplier, defaultMul);
const r = Math.max(item.type === "duplicator" ? 18 : 24, baseRadius * mul);
hit = bestDistance <= r + padding;
}
return { hit, distance: bestDistance };

View file

@ -9,7 +9,6 @@
function emit(worldRef, type, payload) {
worldRef?.emit?.(type, payload);
return payload;
}
function ok(payload = {}) {
@ -108,12 +107,6 @@
return ok({ paused: Boolean(worldRef.paused) });
}
function setPaused(worldRef, paused) {
if (!worldRef) return fail("missing_world");
worldRef.paused = Boolean(paused);
emit(worldRef, "simulation:pause-changed", { paused: Boolean(worldRef.paused) });
return ok({ paused: Boolean(worldRef.paused) });
}
function cycleSpeed(worldRef, speeds = DEFAULT_SPEEDS) {
if (!worldRef) return fail("missing_world");
@ -140,20 +133,14 @@
switch (command.type) {
case "tool.select":
return selectTool(worldRef, command.toolId);
case "tool.size.set":
return setToolSize(worldRef, command.toolId, command.size);
case "tool.size.cycle":
return cycleToolSize(worldRef, command.toolId, command.order);
case "selection.set":
return setSelection(worldRef, command.target || null);
case "selection.clear":
return clearSelection(worldRef);
case "selection.focus":
return focusSelection(worldRef, command.target || null, command.options || {});
case "selection.favorite.toggle":
return toggleSelectedFavorite(worldRef, command.target || worldRef?.selected || null);
case "simulation.paused.set":
return setPaused(worldRef, command.paused);
case "simulation.paused.toggle":
return togglePaused(worldRef);
case "simulation.speed.cycle":

View file

@ -48,7 +48,6 @@ const CONFIG = {
dayLength: 120,
childGrowTime: 58,
fightCooldown: 8,
lightingQuality: "cinematic",
weatherChangeMin: 46,
weatherChangeMax: 96,
bubbleLimit: 32,

View file

@ -6,21 +6,13 @@ class TarinaiEventBus {
}
on(type, handler) {
if (!type || typeof handler !== "function") return () => {};
if (!type || typeof handler !== "function") return;
let list = this.handlers.get(type);
if (!list) {
list = new Set();
this.handlers.set(type, list);
}
list.add(handler);
return () => this.off(type, handler);
}
off(type, handler) {
const list = this.handlers.get(type);
if (!list) return;
list.delete(handler);
if (!list.size) this.handlers.delete(type);
}
emit(type, detail = {}) {

View file

@ -20,7 +20,6 @@
function snapshotKey(snapshot) {
try {
const clone = cloneSnapshot(snapshot);
if (clone) clone.c = 0;
return JSON.stringify(clone);
} catch (_) {
return "";

View file

@ -13,7 +13,7 @@
case "poison_block":
case "reciprocator":
case "gate_fence":
case "sprinkler":
case "robot_cleaner":
case "rope":
case "rod":
return global.TarinaiItemDynamicToolSystem.update(item, dt, worldRef) !== false;

View file

@ -48,67 +48,327 @@
function sprinklerRange(item) {
// v39.15.47: effective cleaning radius increased to 3x the prior radius.
// Previous formula: max(96, r * 4.6). Current: max(288, r * 13.8).
return Math.max(288, (Number(item?.r || 28) || 28) * 13.8);
const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains
const ROBOT_MOVE_SPEED = 60;
const ROBOT_HIGH_SPEED = 120;
const ROBOT_RESCAN_INTERVAL = 2.0;
const ROBOT_TARGET_BITS = Object.freeze({
zunchi: 1 << 0,
ant_corpse: 1 << 1,
tarinai: 1 << 2,
grass: 1 << 3,
grass_bed: 1 << 4,
water: 1 << 5,
});
const ROBOT_TARGET_ORDER = Object.freeze(["zunchi", "ant_corpse", "tarinai", "grass", "grass_bed", "water"]);
function robotCleanerDefaultMask() {
return ROBOT_DEFAULT_MASK;
}
function spawnSprinklerWaterEffects(item, worldRef, range, flashStrength = 1) {
if (!worldRef?.effects || !item) return;
const burst = 8;
for (let i = 0; i < burst; i++) {
const base = (Math.PI * 2 * i) / burst + (Number(item.sprinklerSpin || 0) || 0) * 0.35;
const dist = Math.min(range * 0.55, 46 + i * 6);
const px = item.x + Math.cos(base) * dist;
const py = item.y + Math.sin(base) * dist * 0.72 - 6;
worldRef.effects.push(new Effect("ring", px, py, {
size: 6 + (i % 3) * 2,
life: 0.16 + (i % 2) * 0.05,
color: `rgba(120,204,245,${0.28 + 0.12 * flashStrength})`,
vx: Math.cos(base) * 8,
vy: Math.sin(base) * 6 - 10,
function robotCleanerMask(item) {
const raw = Number(item?.robotCleanerMask);
return Number.isFinite(raw) ? (raw | 0) : ROBOT_DEFAULT_MASK;
}
function setRobotCleanerMask(item, mask = ROBOT_DEFAULT_MASK) {
if (!item || item.type !== "robot_cleaner") return false;
item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(mask) | 0));
return true;
}
function robotCleanerHighSpeed(item) {
return Boolean(item?.robotCleanerHighSpeed);
}
function setRobotCleanerHighSpeed(item, enabled = false) {
if (!item || item.type !== "robot_cleaner") return false;
item.robotCleanerHighSpeed = Boolean(enabled);
item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item);
return true;
}
function robotCleanerEffectiveSpeed(item) {
return robotCleanerHighSpeed(item) ? ROBOT_HIGH_SPEED : ROBOT_MOVE_SPEED;
}
function robotCleanerHasTarget(item, key = "") {
const bit = ROBOT_TARGET_BITS[String(key || "")];
return Boolean(bit && (robotCleanerMask(item) & bit));
}
function robotTargetLabel(key = "") {
switch (String(key || "")) {
case "zunchi": return "\u305a\u3093\u3061";
case "ant_corpse": return "\u6b7b\u9ab8";
case "trace": return "\u6b7b\u9ab8";
case "splat": return "\u8840\u75d5";
case "tarinai": return "\u305f\u308a\u306a\u3044";
case "grass": return "\u8349";
case "grass_bed": return "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9";
case "water": return "\u6c34\u6ef4";
default: return "\u5bfe\u8c61";
}
}
function normalizeRobotCleanerState(item) {
if (!item || item.type !== "robot_cleaner") return null;
if (!Number.isFinite(Number(item.robotCleanerMask))) item.robotCleanerMask = ROBOT_DEFAULT_MASK;
item.robotCleanerMask = Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask) | 0));
item.amount = Math.max(Number(item.amount || 0) || 0, 999);
if (!Number.isFinite(Number(item.robotCleanerScanTimer))) item.robotCleanerScanTimer = 0;
if (!Number.isFinite(Number(item.robotCleanerCleanFlash))) item.robotCleanerCleanFlash = 0;
if (!Number.isFinite(Number(item.robotCleanerAngle))) item.robotCleanerAngle = Number(item.angle || 0) || 0;
if (!Number.isFinite(Number(item.robotCleanerWanderAngle))) item.robotCleanerWanderAngle = Number(item.robotCleanerAngle || 0) || 0;
item.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed);
item.robotCleanerSpeed = robotCleanerEffectiveSpeed(item);
return item;
}
function robotTargetAlive(target) {
if (!target) return false;
return !target.dead && Number.isFinite(Number(target.x)) && Number.isFinite(Number(target.y));
}
function robotTargetBlocked(item, target, worldRef) {
if (!item || !target || !worldRef) return false;
return Boolean(worldRef.targetBlockedByObstacle?.(item, target, Math.max(18, (item.r || 22) * 0.65)));
}
function currentRobotTarget(item, worldRef) {
const key = String(item?.robotCleanerTargetType || "");
const id = String(item?.robotCleanerTargetId || "");
if (!key || !id || !worldRef) return null;
if (key === "tarinai") {
const t = (worldRef.tarinai || []).find(t => t && t.id === id && !t.dead);
return robotTargetAlive(t) && !robotTargetBlocked(item, t, worldRef) ? t : null;
}
const found = (worldRef.items || []).find(it => it && it.id === id && !it.dead && (
it.type === key || (key === "ant_corpse" && (it.type === "trace" || it.type === "splat"))
));
return robotTargetAlive(found) && !robotTargetBlocked(item, found, worldRef) ? found : null;
}
function robotTargetCandidates(item, worldRef) {
const out = [];
if (!worldRef || !item) return out;
const mask = robotCleanerMask(item);
worldRef.ensureItemBuckets?.("robot-cleaner-scan");
for (const key of ROBOT_TARGET_ORDER) {
const bit = ROBOT_TARGET_BITS[key];
if (!(mask & bit)) continue;
if (key === "tarinai") {
for (const t of worldRef.tarinai || []) {
if (!t || t.dead) continue;
if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
if (robotTargetBlocked(item, t, worldRef)) continue;
out.push({ target: t, type: key });
}
continue;
}
const buckets = key === "ant_corpse"
? ["ant_corpse", "trace", "splat"].flatMap(type => worldRef.itemsOfType?.(type) || (worldRef.items || []).filter(it => it && it.type === type))
: (worldRef.itemsOfType?.(key) || (worldRef.items || []).filter(it => it && it.type === key));
for (const it of buckets || []) {
if (!it || it.dead || it === item) continue;
if (robotTargetBlocked(item, it, worldRef)) continue;
out.push({ target: it, type: key });
}
}
return out;
}
function retargetRobotCleaner(item, worldRef, force = false) {
const current = currentRobotTarget(item, worldRef);
if (current && !force) return current;
item.robotCleanerTargetId = "";
item.robotCleanerTargetType = "";
let best = null;
let bestType = "";
let bestD = Infinity;
for (const rec of robotTargetCandidates(item, worldRef)) {
const target = rec.target;
if (!robotTargetAlive(target)) continue;
const d = distXY(item.x, item.y, target.x, target.y);
if (d < bestD) { best = target; bestType = rec.type; bestD = d; }
}
if (best) {
item.robotCleanerTargetId = best.id || "";
item.robotCleanerTargetType = bestType || best.type || "";
}
return best;
}
function triggerRobotPanic(item, target, worldRef, force = false) {
if (!item || !target || target.dead) return false;
const now = Number(worldRef?.time || target.world?.time || 0) || 0;
if (!force && now < Number(target.robotCleanerPanicCooldownUntil || 0)) return false;
target.robotCleanerPanicCooldownUntil = now + 0.65;
const reason = "\u30ed\u30dc\u6383\u9664\u6a5f\u304b\u3089\u9003\u3052\u3066\u3044\u308b";
if (typeof target.enterPanic === "function") {
target.enterPanic({ threat: item, reason, fear: 0.86, wake: true, cause: "robot_cleaner", surpriseTimer: 0.18, awakeLockTimer: 0.45 });
} else {
target.fearTimer = Math.max(target.fearTimer || 0, 0.86);
target.setActionState?.("panic", { target: target.panicDestination ? target.panicDestination(item, true) : null, reason, wake: true });
}
return true;
}
function isRobotTarinaiTarget(target) {
return Boolean(target && (target.constructor?.name === "Tarinai" || (target.thought !== undefined && target.name && target.die)));
}
function robotTargetTypeForContact(item, target) {
if (!item || !target || target.dead) return "";
if (isRobotTarinaiTarget(target)) return robotCleanerHasTarget(item, "tarinai") ? "tarinai" : "";
const type = String(target.type || "");
if ((type === "ant_corpse" || type === "trace" || type === "splat") && robotCleanerHasTarget(item, "ant_corpse")) return type;
return robotCleanerHasTarget(item, type) ? type : "";
}
function robotCleanerCaptureRadius(item, target) {
return Math.max(18, (item?.r || 22) * 0.72 + (target?.r || target?.radius || 10) * 0.72);
}
function clearRobotTrackedTargetIfMatched(item, target) {
if (!item || !target) return false;
const id = String(target.id || "");
if (!id || String(item.robotCleanerTargetId || "") !== id) return false;
item.robotCleanerTargetId = "";
item.robotCleanerTargetType = "";
item.robotCleanerScanTimer = 0;
return true;
}
function cleanRobotTarget(item, target, worldRef, typeOverride = "") {
if (!item || !target || !worldRef || target.dead) return false;
const type = String(typeOverride || item.robotCleanerTargetType || target.type || "");
const label = robotTargetLabel(type);
let removed = false;
const cleanReason = "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u5438\u3044\u8fbc\u307e\u308c\u305f";
if (type === "tarinai" || isRobotTarinaiTarget(target)) {
triggerRobotPanic(item, target, worldRef, true);
if (typeof target.die === "function") {
target.die(cleanReason, { lowHealth: true });
removed = true;
} else {
target.dead = true;
target.deathReason = cleanReason;
worldRef.markDead?.(target, cleanReason);
removed = true;
}
} else {
removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, target, {
reason: "robot-cleaner",
userReason: type === "grass_bed" ? "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u7247\u4ed8\u3051\u3089\u308c\u305f" : cleanReason,
wake: true,
breaker: item,
}));
if (!removed) {
target.amount = 0;
target.hp = 0;
target.dead = true;
target.removedReason = "robot-cleaner";
removed = true;
}
}
}
function updateSprinkler(item, dt, worldRef) {
if (!item || item.dead || !worldRef) return false;
item.amount = Math.max(item.amount || 0, 999);
item.sprinklerSpin = (Number(item.sprinklerSpin || 0) || 0) + dt * 4.2;
item.sprinklerFlash = Math.max(0, (Number(item.sprinklerFlash || 0) || 0) - dt * 1.8);
item.sprinklerTimer = Math.max(0, (Number(item.sprinklerTimer || 0) || 0) - dt);
if (item.sprinklerTimer > 0) return true;
item.sprinklerTimer = global.deterministicRange ? global.deterministicRange(worldRef, "sprinkler-next", 0.55, 1.25, item) : (0.55 + Math.random() * 0.70);
const range = sprinklerRange(item);
const nearby = worldRef.nearbyItems?.(item.x, item.y, range, true) || worldRef.items || [];
const targets = [];
for (const it of nearby) {
if (!it || it.dead || it === item) continue;
if (!(it.type === "zunchi" || it.type === "trace" || it.type === "ant_corpse" || it.type === "splat")) continue;
const r = Math.max(4, Number(it.r || 0) || 4);
if (distXY(item.x, item.y, it.x, it.y) <= range + r) targets.push(it);
}
const sprayAngle = global.deterministicAngle ? global.deterministicAngle(worldRef, "sprinkler-spray-angle", item) : Math.random() * Math.PI * 2;
const sprayX = item.x + Math.cos(sprayAngle) * Math.min(range * 0.42, 132);
const sprayY = item.y + Math.sin(sprayAngle) * Math.min(range * 0.42, 132);
worldRef.effects?.push(new Effect("ring", sprayX, sprayY, { size: 14, life: 0.18, color: "rgba(95,166,230,0.22)" }));
spawnSprinklerWaterEffects(item, worldRef, range, 0.8);
if (!targets.length) return true;
const pickIndex = Math.floor((global.deterministicUnit ? global.deterministicUnit(worldRef, "sprinkler-pick", item, targets.length) : Math.random()) * targets.length) % targets.length;
const target = targets[pickIndex];
if (!target || target.dead) return true;
target.amount = 0;
target.hp = 0;
target.removedReason = "sprinkler-clean";
item.sprinklerFlash = 0.55;
item.sprinklerJustActivated = 0.22;
worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(18, (target.r || 12) * 1.6), life: 0.28, color: "rgba(95,166,230,0.34)" }));
spawnSprinklerWaterEffects(item, worldRef, range, 1);
worldRef.markItemBucketsDirty?.("sprinkler-clean");
worldRef.markSpatialDirty?.("sprinkler-clean");
if (!removed) return false;
clearRobotTrackedTargetIfMatched(item, target);
item.robotCleanerScanTimer = 0;
item.robotCleanerCleanFlash = 0.42;
item.robotCleanerCleanCount = (Number(item.robotCleanerCleanCount || 0) || 0) + 1;
worldRef.effects?.push(new Effect("ring", target.x, target.y, { size: Math.max(16, (target.r || target.radius || 14) * 1.8), life: 0.24, color: "rgba(146,206,220,0.36)" }));
worldRef.markItemBucketsDirty?.("robot-cleaner");
worldRef.markSpatialDirty?.("robot-cleaner");
if (target.type === "trace" || target.type === "splat") worldRef.markTerrainDirtyAt?.(target.x, target.y, Math.max(target.r || target.radius || 24, 36), "robot-cleaner");
worldRef.updateItemCounts?.();
worldRef.drawListDirty = true;
if ((worldRef.time || 0) - Number(item.robotCleanerLastLogAt || -999) > 1.2) {
worldRef.log?.(`\u30ed\u30dc\u6383\u9664\u6a5f\u304c${label}\u3092\u7247\u4ed8\u3051\u305f\u3002`, "observe");
item.robotCleanerLastLogAt = worldRef.time || 0;
}
return true;
}
function cleanRobotContacts(item, worldRef) {
if (!item || !worldRef) return false;
const seen = new Set();
let changed = false;
for (const rec of robotTargetCandidates(item, worldRef)) {
const target = rec?.target;
if (!robotTargetAlive(target)) continue;
const key = String(target.id || `${rec.type}:${target.x}:${target.y}`);
if (seen.has(key)) continue;
seen.add(key);
const type = robotTargetTypeForContact(item, target) || rec.type || "";
if (!type) continue;
const capture = robotCleanerCaptureRadius(item, target);
if (distXY(item.x, item.y, target.x, target.y) <= capture) {
if (cleanRobotTarget(item, target, worldRef, type)) changed = true;
}
}
return changed;
}
function moveRobotCleaner(item, dt, target, worldRef) {
const r = Math.max(14, Number(item.r || 22) || 22);
const speed = robotCleanerEffectiveSpeed(item);
let desiredAngle = Number(item.robotCleanerWanderAngle || 0) || 0;
let desiredSpeed = speed;
if (target && robotTargetAlive(target)) {
desiredAngle = Math.atan2((target.y || 0) - (item.y || 0), (target.x || 0) - (item.x || 0));
desiredSpeed = speed;
} else if ((item.robotCleanerWanderTimer || 0) <= 0) {
desiredAngle = global.deterministicAngle ? global.deterministicAngle(worldRef, "robot-cleaner-wander", item, Math.floor((worldRef.time || 0) * 2)) : Math.random() * Math.PI * 2;
item.robotCleanerWanderTimer = global.deterministicRange ? global.deterministicRange(worldRef, "robot-cleaner-wander-timer", 1.2, 3.2, item) : (1.2 + Math.random() * 2.0);
}
item.robotCleanerWanderTimer = Math.max(0, Number(item.robotCleanerWanderTimer || 0) - dt);
item.robotCleanerWanderAngle = desiredAngle;
const vx = Math.cos(desiredAngle) * desiredSpeed;
const vy = Math.sin(desiredAngle) * desiredSpeed;
const blend = Math.min(1, dt * 6.2);
item.vx = (Number(item.vx || 0) || 0) + (vx - (Number(item.vx || 0) || 0)) * blend;
item.vy = (Number(item.vy || 0) || 0) + (vy - (Number(item.vy || 0) || 0)) * blend;
item.prevX = item.x;
item.prevY = item.y;
item.x += item.vx * dt;
item.y += item.vy * dt;
const minX = CONFIG.worldPadding + r * 0.5;
const minY = CONFIG.worldPadding + r * 0.5;
const maxX = Math.max(minX, (worldRef.w || 0) - CONFIG.worldPadding - r * 0.5);
const maxY = Math.max(minY, (worldRef.h || 0) - CONFIG.worldPadding - r * 0.5);
let bounced = false;
if (item.x < minX) { item.x = minX; item.vx = Math.abs(item.vx) * 0.35; bounced = true; }
if (item.x > maxX) { item.x = maxX; item.vx = -Math.abs(item.vx) * 0.35; bounced = true; }
if (item.y < minY) { item.y = minY; item.vy = Math.abs(item.vy) * 0.35; bounced = true; }
if (item.y > maxY) { item.y = maxY; item.vy = -Math.abs(item.vy) * 0.35; bounced = true; }
if (bounced) {
item.robotCleanerWanderAngle = Math.atan2(item.vy || 0, item.vx || 0) + (global.deterministicRange ? global.deterministicRange(worldRef, "robot-cleaner-bounce", -0.8, 0.8, item) : (Math.random() - 0.5) * 1.6);
item.robotCleanerWanderTimer = 0.4;
}
if (Math.hypot(item.vx || 0, item.vy || 0) > 2) item.robotCleanerAngle = Math.atan2(item.vy || 0, item.vx || 0);
worldRef.markSpatialDirty?.("robot-cleaner-move");
worldRef.drawListDirty = true;
}
function updateRobotCleaner(item, dt, worldRef) {
if (!item || item.dead || !worldRef) return false;
normalizeRobotCleanerState(item);
dt = Math.max(0, Math.min(0.05, Number(dt || 0) || 0));
item.robotCleanerCleanFlash = Math.max(0, Number(item.robotCleanerCleanFlash || 0) - dt * 2.8);
item.robotCleanerScanTimer = Math.max(0, Number(item.robotCleanerScanTimer || 0) - dt);
let target = currentRobotTarget(item, worldRef);
if (!target || item.robotCleanerScanTimer <= 0) {
target = retargetRobotCleaner(item, worldRef, true);
item.robotCleanerScanTimer = ROBOT_RESCAN_INTERVAL;
}
moveRobotCleaner(item, dt, target, worldRef);
if (target && robotTargetAlive(target)) {
const isTarinaiTarget = String(item.robotCleanerTargetType || "") === "tarinai" || isRobotTarinaiTarget(target);
const d = distXY(item.x, item.y, target.x, target.y);
if (isTarinaiTarget && d <= Math.max(96, (item.r || 22) + (target.radius || target.r || 12) + 70)) triggerRobotPanic(item, target, worldRef);
}
cleanRobotContacts(item, worldRef);
return true;
}
@ -125,7 +385,7 @@
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
if (item.type === "reciprocator") return updateReciprocator(item, dt, worldRef);
if (item.type === "gate_fence") return updateGateFence(item, dt, worldRef);
if (item.type === "sprinkler") return updateSprinkler(item, dt, worldRef);
if (item.type === "robot_cleaner") return updateRobotCleaner(item, dt, worldRef);
if (item.type === "rope") return updateFlexibleLink(item, dt, worldRef);
if (item.type === "rod") return updateRigidLink(item, dt, worldRef);
return false;
@ -133,6 +393,17 @@
global.TarinaiItemDynamicToolSystem = Object.freeze({
update,
sprinklerRange,
robotCleanerDefaultMask,
robotCleanerMask,
setRobotCleanerMask,
robotCleanerHasTarget,
robotCleanerHighSpeed,
setRobotCleanerHighSpeed,
robotCleanerEffectiveSpeed,
robotTargetLabel,
robotCleanerTargetBits: ROBOT_TARGET_BITS,
robotCleanerTargetOrder: ROBOT_TARGET_ORDER,
robotCleanerMoveSpeed: ROBOT_MOVE_SPEED,
robotCleanerHighMoveSpeed: ROBOT_HIGH_SPEED,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -110,7 +110,6 @@ draw(ctx, t, lighting = null) {
drawProjectedShadow(ctx, this.x, this.y + this.r * 0.52, this.r * 0.82, this.r * 0.26, { ...shadow, alpha: shadow.alpha * visibleAlpha * 1.25 });
}
}
drawItemGlow(ctx, this, lightState, visibleAlpha);
if (world.pointer?.inside && this.type !== "trace" && this.type !== "splat" && distXY(this.x, this.y, world.pointer.x, world.pointer.y) < this.r * 2.2) {
ctx.save();
ctx.globalAlpha = 0.32;
@ -293,120 +292,58 @@ draw(ctx, t, lighting = null) {
ctx.beginPath();
ctx.arc(0, -this.r * 0.08, this.r * 0.40, 0, Math.PI * 2);
ctx.fill();
} else if (this.type === "sprinkler") {
} else if (this.type === "robot_cleaner") {
ctx.save();
const flash = clamp(Number(this.sprinklerFlash || 0) || 0, 0, 1);
const spin = Number(this.sprinklerSpin || 0) || 0;
const range = globalThis.TarinaiItemDynamicToolSystem.sprinklerRange ? globalThis.TarinaiItemDynamicToolSystem.sprinklerRange(this) : Math.max(288, (Number(this.r || 28) || 28) * 13.8);
const metalA = styleNight ? "#6a7782" : "#d6dde3";
const metalB = styleNight ? "#39434d" : "#8a969f";
const darkMetal = styleNight ? "#1e252b" : "#53606a";
const baseGreen = styleNight ? "#4f6a3a" : "#76a54b";
const baseGreenDark = styleNight ? "#304324" : "#547635";
const hose = styleNight ? "#2f3a42" : "#4b5963";
// base shadow
ctx.fillStyle = "rgba(30,40,48,0.14)";
const flash = clamp(Number(this.robotCleanerCleanFlash || 0) || 0, 0, 1);
const angle = Number(this.robotCleanerAngle || this.angle || 0) || 0;
const bodyA = styleNight ? "#39434a" : "#d8e1e3";
const bodyB = styleNight ? "#1f272d" : "#a7b4b8";
const trim = styleNight ? "#657781" : "#ffffff";
const dark = styleNight ? "#12191e" : "#526168";
ctx.fillStyle = "rgba(20,24,28,0.16)";
ctx.beginPath();
ctx.ellipse(0, this.r * 0.70, this.r * 0.92, this.r * 0.26, 0, 0, Math.PI * 2);
ctx.ellipse(0, this.r * 0.56, this.r * 1.05, this.r * 0.30, 0, 0, Math.PI * 2);
ctx.fill();
// ground stake and foot
ctx.fillStyle = baseGreen;
ctx.strokeStyle = baseGreenDark;
ctx.lineWidth = Math.max(1.6, this.r * 0.05);
roundedRect(ctx, -this.r * 0.56, this.r * 0.12, this.r * 1.12, this.r * 0.46, this.r * 0.12);
ctx.fill();
ctx.stroke();
const bodyGrad = ctx.createRadialGradient(-this.r * 0.34, -this.r * 0.38, this.r * 0.18, 0, 0, this.r * 1.28);
bodyGrad.addColorStop(0, trim);
bodyGrad.addColorStop(0.46, bodyA);
bodyGrad.addColorStop(1, bodyB);
ctx.fillStyle = bodyGrad;
ctx.strokeStyle = dark;
ctx.lineWidth = Math.max(2, this.r * 0.08);
ctx.beginPath();
ctx.moveTo(-this.r * 0.18, this.r * 0.10);
ctx.lineTo(-this.r * 0.38, this.r * 0.88);
ctx.lineTo(-this.r * 0.18, this.r * 0.88);
ctx.lineTo(-this.r * 0.02, this.r * 0.10);
ctx.closePath();
ctx.ellipse(0, 0, this.r * 1.04, this.r * 0.88, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(this.r * 0.18, this.r * 0.10);
ctx.lineTo(this.r * 0.38, this.r * 0.88);
ctx.lineTo(this.r * 0.18, this.r * 0.88);
ctx.lineTo(this.r * 0.02, this.r * 0.10);
ctx.closePath();
ctx.fill();
ctx.stroke();
// riser pipe
const pipeGrad = ctx.createLinearGradient(-this.r * 0.12, -this.r * 0.54, this.r * 0.12, this.r * 0.36);
pipeGrad.addColorStop(0, metalA);
pipeGrad.addColorStop(0.55, metalB);
pipeGrad.addColorStop(1, metalA);
ctx.fillStyle = pipeGrad;
ctx.strokeStyle = darkMetal;
roundedRect(ctx, -this.r * 0.12, -this.r * 0.30, this.r * 0.24, this.r * 0.62, this.r * 0.08);
ctx.fill();
ctx.stroke();
// head and rotor
ctx.save();
ctx.translate(0, -this.r * 0.40);
ctx.rotate(spin);
const armGrad = ctx.createLinearGradient(-this.r * 0.84, 0, this.r * 0.84, 0);
armGrad.addColorStop(0, metalB);
armGrad.addColorStop(0.5, metalA);
armGrad.addColorStop(1, metalB);
ctx.strokeStyle = armGrad;
ctx.lineWidth = Math.max(2.4, this.r * 0.09);
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(-this.r * 0.72, 0);
ctx.lineTo(this.r * 0.72, 0);
ctx.rotate(angle);
ctx.fillStyle = flash > 0.02 ? "rgba(105,210,225,0.84)" : (styleNight ? "#50636d" : "#71868e");
ctx.strokeStyle = dark;
ctx.lineWidth = Math.max(1, this.r * 0.045);
roundedRect(ctx, this.r * 0.18, -this.r * 0.15, this.r * 0.58, this.r * 0.30, this.r * 0.09);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = darkMetal;
ctx.lineWidth = Math.max(1.2, this.r * 0.04);
ctx.beginPath();
ctx.moveTo(0, -this.r * 0.28);
ctx.lineTo(0, this.r * 0.20);
ctx.stroke();
for (const sx of [-1, 1]) {
ctx.fillStyle = hose;
ctx.beginPath();
ctx.arc(sx * this.r * 0.74, 0, this.r * 0.10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = flash > 0 ? "rgba(170,225,255,0.95)" : "rgba(118,188,232,0.80)";
ctx.beginPath();
ctx.arc(sx * this.r * 0.80, 0, this.r * 0.06, 0, Math.PI * 2);
ctx.fill();
}
if (flash > 0.02) {
ctx.strokeStyle = `rgba(142,216,255,${0.45 + flash * 0.35})`;
ctx.lineWidth = Math.max(1.0, this.r * 0.04);
for (const sx of [-1, 1]) {
for (let i = 0; i < 3; i++) {
const dir = sx < 0 ? -1 : 1;
const spread = (i - 1) * 0.18;
const len = this.r * (0.95 + i * 0.20);
ctx.beginPath();
ctx.moveTo(sx * this.r * 0.80, 0);
ctx.quadraticCurveTo(sx * this.r * (1.10 + i * 0.14), -this.r * (0.10 + Math.abs(spread) * 0.4), sx * this.r * (1.36 + i * 0.18), -this.r * (0.18 + spread));
ctx.stroke();
ctx.beginPath();
ctx.arc(sx * this.r * (1.36 + i * 0.18), -this.r * (0.18 + spread), this.r * 0.035 * (1.0 + i * 0.14), 0, Math.PI * 2);
ctx.fillStyle = `rgba(164,228,255,${0.35 + flash * 0.25})`;
ctx.fill();
}
}
}
ctx.restore();
// top cap
ctx.fillStyle = metalA;
ctx.strokeStyle = darkMetal;
ctx.fillStyle = styleNight ? "#20292f" : "#eef4f5";
ctx.strokeStyle = styleNight ? "#11181d" : "#6a7980";
ctx.lineWidth = Math.max(1.4, this.r * 0.055);
ctx.beginPath();
ctx.arc(0, -this.r * 0.40, this.r * 0.16, 0, Math.PI * 2);
ctx.arc(-this.r * 0.26, -this.r * 0.12, this.r * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
const rangePulse = 0.14 + flash * 0.30;
ctx.strokeStyle = `rgba(88,157,214,${rangePulse})`;
ctx.lineWidth = 1.2;
ctx.setLineDash([7, 6]);
ctx.beginPath();
ctx.arc(0, 0, range, 0, Math.PI * 2);
ctx.arc(this.r * 0.26, -this.r * 0.12, this.r * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = flash > 0.02 ? `rgba(110,220,235,${0.42 + flash * 0.42})` : "rgba(80,100,110,0.38)";
ctx.lineWidth = Math.max(1.1, this.r * 0.04);
ctx.beginPath();
ctx.arc(0, 0, this.r * 0.56, 0.22, Math.PI - 0.22);
ctx.stroke();
ctx.fillStyle = flash > 0.02 ? `rgba(125,230,240,${0.28 + flash * 0.36})` : "rgba(60,78,86,0.20)";
ctx.beginPath();
ctx.arc(0, this.r * 0.20, this.r * (0.18 + flash * 0.10), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
} else if (this.type === "duplicator") {
ctx.save();

View file

@ -4,7 +4,7 @@
(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: "選択中の道具ボタンを解除し、通常の観察状態に戻す。" },
clear_tool: { id: "clear_tool", label: "\u89e3\u9664", placeable: false, scalable: false, iconText: "\u2715", tooltip: "\u9078\u629e\u4e2d\u306e\u9053\u5177\u30dc\u30bf\u30f3\u3092\u89e3\u9664\u3057\u3001\u901a\u5e38\u306e\u89b3\u5bdf\u72b6\u614b\u306b\u623b\u3059\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" },
@ -17,7 +17,7 @@ const TOOL_DEFINITIONS = Object.freeze({
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" },
robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\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" },
@ -41,7 +41,7 @@ const TOOL_DEFINITIONS = Object.freeze({
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: "食べ物、薬、鋲をセットすると無限に供給できる。" },
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 24, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u3001\u85ac\u3001\u92f2\u3092\u30bb\u30c3\u30c8\u3059\u308b\u3068\u7121\u9650\u306b\u4f9b\u7d66\u3067\u304d\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" },
@ -68,7 +68,7 @@ const ITEM_TRAITS = Object.freeze({
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"]),
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "plushie", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
});
@ -130,7 +130,7 @@ 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: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "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"] },
]);

View file

@ -35,12 +35,19 @@ function initializeItemTypeState(item, type, x, y) {
item.needEffects.food = 55;
item.amount = 999;
}
if (type === "sprinkler") {
if (type === "robot_cleaner") {
item.amount = 999;
item.sprinklerTimer = rand(0.18, 0.72);
item.sprinklerSpin = rand(0, Math.PI * 2);
item.sprinklerFlash = 0;
item.sprinklerJustActivated = 0;
item.robotCleanerMask = globalThis.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3;
item.robotCleanerTargetId = "";
item.robotCleanerTargetType = "";
item.robotCleanerScanTimer = 0;
item.robotCleanerCleanFlash = 0;
item.robotCleanerCleanCount = 0;
item.robotCleanerAngle = rand(0, Math.PI * 2);
item.robotCleanerWanderAngle = item.robotCleanerAngle;
item.robotCleanerWanderTimer = rand(0.2, 1.4);
item.robotCleanerHighSpeed = false;
item.robotCleanerSpeed = globalThis.TarinaiItemDynamicToolSystem?.robotCleanerMoveSpeed ?? 60;
}
if (type === "ball") {
item.vx = rand(-5, 5);

View file

@ -5,11 +5,11 @@
// helpers and the item scheduler both read this single policy instead of
// duplicating item-type timing rules.
(function (global) {
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker"]);
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker", "robot_cleaner"]);
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
const SCHEDULED_ITEM_TYPES = Object.freeze([
"ball", "genkotsu", "firecracker", "pushpin", "oshibyo",
"plushie", "grass_bed", "zunchi", "sprinkler", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
"plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "rope", "rod",
]);
@ -31,7 +31,6 @@
if (item.isStructure && item.type === "grass_bed") return 3.0;
if ((item.dropTimer || 0) > 0 || Math.hypot(item.vx || 0, item.vy || 0) > 0.08) return 0;
if (item.type === "zunchi") return 2.4;
if (item.type === "sprinkler") return mobile ? 0.45 : 0.25;
if (item.type === "grass") return Infinity;
if (item.type === "duplicator") return mobile ? 0.8 : 0.5;
if (item.type === "water") return mobile ? 2.4 : 1.8;

View file

@ -537,7 +537,7 @@
const applyConstraintState = (item, c = item?.physicsConstraint) => { if (item && c) { item.physicsConstraint = normalizeConstraint(c, item); return true; } return false; };
const bodyApi = Object.freeze({
isPhysicsType,
isBodyType, isConstraintType, isPhysicsType,
ensureBody, ensureConstraint,
normalizeBodyState, applyBodyState, markBodyChanged, normalizeConstraintState, applyConstraintState,
compactItemExtra, applyCompactExtra, purgeLegacyPhysicsStorage,

View file

@ -152,11 +152,6 @@
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");

View file

@ -318,9 +318,8 @@ function getLightingState(targetWorld) {
const horizon = Math.cos(progress * Math.PI * 2 - Math.PI / 2);
const shadowLength = lerp(1.58, 0.52, elevation);
const warmth = clamp(0.28 + goldenStrength * 0.70 + noonStrength * 0.18 - nightStrength * 0.12, 0, 1);
const bloom = CONFIG.lightingQuality === "cinematic" ? clamp(goldenStrength * 0.42 + nightStrength * 0.18 + noonStrength * 0.14, 0, 0.58) : 0;
const bloom = clamp(goldenStrength * 0.42 + nightStrength * 0.18 + noonStrength * 0.14, 0, 0.58);
return {
quality: CONFIG.lightingQuality,
progress,
light,
dawnStrength,
@ -408,12 +407,6 @@ function drawRadialGlow(ctx, x, y, radius, color, alpha) {
ctx.restore();
}
function drawItemGlow(ctx, item, lighting, visibleAlpha = 1) {
// \u500b\u5225\u306e\u767a\u5149\u30b0\u30e9\u30c7\u30fc\u30b7\u30e7\u30f3\u306f\u500b\u4f53\u6570\u5897\u52a0\u6642\u306e\u8ca0\u8377\u304c\u9ad8\u3044\u305f\u3081\u3001\u901a\u5e38\u63cf\u753b\u306b\u7d71\u5408\u3059\u308b\u3002
return;
}
function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale = 1) {
if (!influence) return;
const x = Number(influence.x || 0) || 0;
@ -645,10 +638,6 @@ function drawCreatureGlow(ctx, tarinai, drawW, drawH, lighting) {
ctx.restore();
}
function drawDawnRimLight(ctx, drawW, drawH, lighting) {
return;
}
// Cached static garden layer: expensive soil/decor loops are rendered only when
// canvas size, coarse light phase, or weather changes.
let activeCtx = null;
@ -661,15 +650,24 @@ const backgroundCache = {
key: "",
};
function backgroundTimePhase(world) {
const progress = typeof world?.dayProgress === "function" ? world.dayProgress() : (((world?.time || 0) % CONFIG.dayLength) / CONFIG.dayLength);
const hour = ((progress % 1) + 1) % 1 * 24;
if (hour >= 5 && hour < 10) return "morning";
if (hour >= 10 && hour < 17) return "day";
if (hour >= 17 && hour < 20) return "evening";
return "night";
}
function backgroundCacheKey(world, lighting) {
const weather = world.weather || "sunny";
const field = world.fieldType || "garden";
const ground = world.groundType || "soil";
const phase = backgroundTimePhase(world);
// Keep the heavy garden/sky redraw coarse. Fine light changes are handled by overlays.
const lightBucket = Math.round((lighting.light || 0) * 8);
const warmthBucket = Math.round((lighting.goldenStrength || 0) * 5);
const weatherBucket = Math.floor((world.time || 0) / 8);
return `${field}:${ground}:${weather}:${lightBucket}:${warmthBucket}:${weatherBucket}`;
return `${field}:${ground}:${weather}:${phase}:${lightBucket}:${warmthBucket}`;
}
@ -702,7 +700,7 @@ function isCachedTerrainItem(it) {
}
function isBehindSpriteLayerItem(it) {
return Boolean(it && !it.dead && (it.type === "rope" || it.type === "rod" || it.type === "bed" || it.type === "grass_bed" || it.type === "splat"));
return Boolean(it && !it.dead && (it.type === "rope" || it.type === "rod" || it.type === "bed" || it.type === "grass_bed"));
}
function renderLayerSortY(entity) {
@ -714,7 +712,6 @@ function renderLayerSortY(entity) {
function renderLayerKindRank(entity) {
if (!entity) return 0;
if (entity.kind === "ant_worker" || entity.kind === "ant_queen") return 1;
if (typeof Tarinai !== "undefined" && entity instanceof Tarinai) return 2;
return 0;
}
@ -1261,7 +1258,7 @@ function drawGardenBed(w, h, lighting) {
}
function drawLightRays(ctx, w, h, lighting) {
if (lighting.quality !== "cinematic" || lighting.goldenStrength <= 0.08) return;
if (lighting.goldenStrength <= 0.08) return;
ctx.save();
ctx.globalCompositeOperation = "screen";
const rayAlpha = lighting.goldenStrength * LIGHTING_TUNING.rayAlpha;
@ -1303,9 +1300,7 @@ function drawAtmosphericLighting(ctx, w, h, lighting) {
}
ctx.restore();
if (lighting.quality === "cinematic") {
drawRadialGlow(ctx, w * 0.5, h * 0.46, Math.max(w, h) * 0.50, lighting.nightStrength > 0.18 ? "rgba(91, 121, 255, ALPHA)" : "rgba(255, 236, 175, ALPHA)", lighting.bloom * LIGHTING_TUNING.bloomWashAlpha);
}
drawRadialGlow(ctx, w * 0.5, h * 0.46, Math.max(w, h) * 0.50, lighting.nightStrength > 0.18 ? "rgba(91, 121, 255, ALPHA)" : "rgba(255, 236, 175, ALPHA)", lighting.bloom * LIGHTING_TUNING.bloomWashAlpha);
ctx.save();
const vignette = ctx.createRadialGradient(w * 0.5, h * 0.46, Math.min(w, h) * 0.18, w * 0.5, h * 0.48, Math.max(w, h) * 0.76);

View file

@ -13,7 +13,6 @@
FIELD_IDS,
GROUND_IDS,
MOOD_IDS,
LEGACY_MOOD_IDS,
canonicalMoodId,
enumIndex,
ITEM_TYPE_IDS,
@ -390,7 +389,7 @@
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 & 4) { writeBitFields(writer, [stateId], [1]); 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); }
}
@ -412,7 +411,7 @@
const [tailMask] = readBitFields(reader, [5]);
const pinIdx = (tailMask & 1) ? reader.u() : -1;
const nestIdx = (tailMask & 2) ? reader.u() : -1;
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -1];
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [1])[0], reader.s()] : [0, -1];
const genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s()] : [];
const fightStats = (tailMask & 16) ? [reader.u(), reader.u(), reader.s()] : [];
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
@ -429,6 +428,9 @@
writer.str(extra[0] || "");
} else if (typeId === 6) { // duplicator
writer.s(extra[0] ?? -1);
} else if (typeId === 42) { // robot_cleaner
writer.u(Math.max(0, Math.min(0x3f, Number(extra[0] ?? 3) | 0)));
writer.u(extra[1] ? 1 : 0);
} else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
writer.str(JSON.stringify(extra || []));
} else if (typeId === 21) { // ball
@ -453,6 +455,7 @@
if (typeId === 1) return [reader.u(), reader.s(), reader.s()];
if (typeId === 22) return [reader.str()];
if (typeId === 6) return [reader.s()];
if (typeId === 42) return [reader.u(), reader.u()];
if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
try {
const parsed = JSON.parse(reader.str() || "[]");
@ -538,13 +541,12 @@
function fieldValue(index = 0) { return FIELD_IDS[Number(index) | 0] || "garden"; }
function groundValue(index = 0) { return GROUND_IDS[Number(index) | 0] || "soil"; }
function summaryFromPayload(w = [], tarinaiCount = 0, itemCount = 0) {
function summaryFromPayload(w = [], tarinaiCount = 0) {
const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {});
const dayLength = Math.max(1, Number(config.dayLength || 120));
const totalTime = (Number(w[3]) || 0) / 10;
const day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
const time10 = Math.round((totalTime - (day - 1) * dayLength) * 10);
return [day, tarinaiCount, fieldValue(w[0]), time10, itemCount, groundValue(w[1])];
return [day, tarinaiCount, fieldValue(w[0])];
}
function encodeBinarySnapshot(snapshot) {
@ -565,22 +567,17 @@
function decodeBinarySnapshot(bytes) {
const reader = new BinaryReader(bytes);
const schema = reader.b();
if (schema !== BINARY_SCHEMA_VERSION && schema !== 15) throw new Error("unsupported binary save schema");
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
const snapshotVersion = reader.u();
if (snapshotVersion !== Snapshot.version && snapshotVersion !== 11) throw new Error("unsupported binary save version");
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
const w = readWorld(reader);
if (schema === 15) {
const legacyMood = LEGACY_MOOD_IDS?.[Number(w[2]) | 0] || "relaxed";
w[2] = enumIndex(MOOD_IDS, canonicalMoodId?.(legacyMood) || "relaxed");
}
const tCount = reader.u();
const t = [];
const tarCoordState = { x: 0, y: 0 };
for (let i = 0; i < tCount; i++) t.push(readTarinai(reader, tarCoordState));
const items = readItemBlocks(reader);
const itemCount = items.length;
if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save");
return { v: Snapshot.version, a: "tj1", c: Date.now(), m: summaryFromPayload(w, t.length, items.length), w, t, i: items };
return { v: Snapshot.version, a: "tj1", m: summaryFromPayload(w, t.length), w, t, i: items };
}
async function packBytesForSave(bytes) {

View file

@ -4,14 +4,14 @@
// Owns stable save identifiers shared by snapshot_system.js and save_codec.js.
// Current-format save identifiers only.
(function (global) {
const SNAPSHOT_VERSION = 12;
const BINARY_SCHEMA_VERSION = 16;
const SNAPSHOT_VERSION = 15;
const BINARY_SCHEMA_VERSION = 19;
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
const MOOD_IDS = Object.freeze(["relaxed", "devastation", "crisis", "confusion", "fearful", "disease_spread", "weakened", "breeding", "happy", "overcrowded", "isolated"]);
const STATE_IDS = Object.freeze(["idle", "sleep", "panic", "sunbath", "fight", "seek_food", "seek_bed", "wander"]);
const STATE_IDS = Object.freeze(["idle", "sleep"]);
const POWER_MODE_IDS = Object.freeze(["", "protein", "niteropu"]);
const SIZE_MODE_IDS = Object.freeze(["", "giant_drug", "dwarf_drug"]);
const LIFE_MODE_IDS = Object.freeze(["", "mercury"]);
@ -21,7 +21,7 @@
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "sprinkler"
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner"
]);
function typeIds(names) {
@ -39,20 +39,9 @@
return i >= 0 ? i : fallback;
}
const LEGACY_MOOD_IDS = Object.freeze(["relaxed", "calm", "prickly", "stinky", "weaklings", "zunda_party", "ball_party", "sun_party", "ant_overrun"]);
const LEGACY_MOOD_ALIASES = Object.freeze({
calm: "relaxed",
prickly: "confusion",
stinky: "disease_spread",
weaklings: "weakened",
zunda_party: "happy",
ball_party: "happy",
sun_party: "happy",
ant_overrun: "fearful",
});
function canonicalMoodId(value = "relaxed") {
const id = String(value || "relaxed");
return LEGACY_MOOD_ALIASES[id] || id;
return MOOD_IDS.includes(id) ? id : "relaxed";
}
function enumValue(list, index, fallback = "") {
@ -62,7 +51,7 @@
function moodValue(index, fallback = "relaxed") {
const i = Number(index) | 0;
return canonicalMoodId(MOOD_IDS[i] ?? LEGACY_MOOD_IDS[i] ?? fallback);
return canonicalMoodId(MOOD_IDS[i] ?? fallback);
}
global.TarinaiSaveSchema = Object.freeze({
@ -72,8 +61,6 @@
GROUND_IDS,
WEATHER_IDS,
MOOD_IDS,
LEGACY_MOOD_IDS,
LEGACY_MOOD_ALIASES,
canonicalMoodId,
moodValue,
STATE_IDS,

View file

@ -28,7 +28,7 @@
async function saveSlot(slot) {
const snapshot = createSnapshot(global.world);
const hash = await encodeSnapshot(snapshot);
return Storage.writeSlot(slot, hash, { c: snapshot.c, m: snapshot.m });
return Storage.writeSlot(slot, hash, { m: snapshot.m });
}
async function loadSlot(slot) {
@ -48,9 +48,8 @@
const day = m[0] || 1;
const population = m[1] || 0;
const fieldType = m[2] || "\u5ead";
const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : "";
const hashLen = entry.hash ? ` / ${entry.hash.length}\u5b57` : "";
return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339${hashLen}`;
return `${date} / ${fieldType} / ${day}\u65e5\u76ee / ${population}\u5339${hashLen}`;
}
function ensureDialog() {

View file

@ -199,7 +199,7 @@
const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")];
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1;
const state = t.state === "sleep" ? "sleep" : (STATE_IDS.includes(t.state) ? t.state : "idle");
const state = t.state === "sleep" ? "sleep" : "idle";
let targetIdx = -1;
if (state === "sleep") {
const targetId = t.sleepSession?.targetId || t.pendingGrassBedWakeId || t.target?.id || "";
@ -231,7 +231,7 @@
x: u(row[2], 1), y: u(row[3], 1), vx: 0, vy: 0,
age, birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[5], 1, 1)),
hunger: u(row[6], 1), loneliness: Number(needs.social) || 0, energy: u(row[7], 1), circadianSleepPressure: u(row[8], 1),
needs, state: enumValue(STATE_IDS, row[17]?.[0], "idle"),
needs, state: "idle",
isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)),
isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)),
totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0,
@ -274,6 +274,7 @@
if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0];
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)];
if (type === "signboard") return [item.text || ""];
if (type === "robot_cleaner") return [Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask ?? 3) | 0)), item.robotCleanerHighSpeed ? 1 : 0];
if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
const packed = global.TarinaiPhysicsBodySystem.compactItemExtra(item, tarinaiIndex, itemIndex);
if (packed) return packed;
@ -324,6 +325,13 @@
item.stage = enumValue(STAGE_IDS, extra[0], "fresh"); item.freshness = u(extra[1], 100, item.freshness || 1); item.fertility = u(extra[2], 100, item.fertility || 1);
} else if (type === "signboard") {
item.text = String(extra[0] || "");
} else if (type === "robot_cleaner") {
const mask = Number(extra[0]);
item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x3f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3);
item.robotCleanerHighSpeed = Boolean(extra[1]);
item.robotCleanerSpeed = global.TarinaiItemDynamicToolSystem?.robotCleanerEffectiveSpeed?.(item) ?? (item.robotCleanerHighSpeed ? 120 : 60);
item.robotCleanerTargetId = "";
item.robotCleanerTargetType = "";
} else if (global.TarinaiPhysicsBodySystem.applyCompactExtra(item, extra, tarinaiList, itemList)) {
return;
} else if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
@ -373,8 +381,7 @@
return {
v: SNAPSHOT_VERSION,
a: "tj1",
c: Date.now(),
m: [worldRef.day || 1, tarinaiLive.length, worldRef.fieldType || "garden", q(worldRef.time || 0, 10), itemRows.length, worldRef.groundType || "soil"],
m: [worldRef.day || 1, tarinaiLive.length, worldRef.fieldType || "garden"],
w: compactWorld(worldRef),
t: tarinaiLive.map((t, i) => compactTarinai(t, i, familyIndex, tarinaiIndex, itemIndex)),
i: itemRows,

View file

@ -31,6 +31,12 @@
this.onHead = definition.type === "plushie";
if (definition.type === "plushie") {
this.plushieSpriteId = "plushie_bear";
this.plushieFlingTimer = 0;
this.plushieTrailTimer = 0;
this.spin = 0;
this.spinVelocity = 0;
this.vx = 0;
this.vy = 0;
}
this.dead = false;
this.world = world || null;
@ -47,6 +53,30 @@
return;
}
if (this.type === "plushie") {
if ((this.plushieFlingTimer || 0) > 0) {
this.carriedById = "";
this.onHead = false;
this.x += (Number(this.vx || 0) || 0) * dt;
this.y += (Number(this.vy || 0) || 0) * dt;
this.vy = (Number(this.vy || 0) || 0) + 34 * dt;
this.spin = (Number(this.spin || 0) || 0) + (Number(this.spinVelocity || 0) || 0) * dt;
this.plushieFlingTimer = Math.max(0, (this.plushieFlingTimer || 0) - dt);
this.plushieTrailTimer = Math.max(0, (this.plushieTrailTimer || 0) - dt);
if (worldRef?.effects && typeof Effect !== "undefined" && this.plushieTrailTimer <= 0) {
this.plushieTrailTimer = 0.045;
worldRef.effects.push(new Effect("ring", this.x, this.y, { size: 10 + Math.random() * 8, life: 0.16, color: "rgba(190,142,92,0.28)" }));
}
const margin = 96;
if (this.plushieFlingTimer <= 0 || this.x < -margin || this.x > (worldRef?.w || 0) + margin || this.y < -margin || this.y > (worldRef?.h || 0) + margin) {
this.dead = true;
this.hp = 0;
this.amount = 0;
worldRef?.markItemBucketsDirty?.("plushie-fling-out");
worldRef?.markSpatialDirty?.("plushie-fling-out");
}
worldRef && (worldRef.drawListDirty = true);
return;
}
const owner = this.owner(worldRef);
if (!owner) {
this.dead = true;
@ -59,6 +89,13 @@
worldRef?.markSpatialDirty?.("plushie-owner-gone");
return;
}
if (this.playerHeld || this._heldByPlayer) {
this.carriedById = "";
this.onHead = false;
this.amount = Math.max(0.001, Number(this.hp || this.amount || 1) || 1);
worldRef && (worldRef.drawListDirty = true);
return;
}
this.ownerId = owner.id || this.ownerId;
this.carriedById = owner.id || this.ownerId || this.carriedById;
this.onHead = true;
@ -102,6 +139,68 @@
return true;
}
fling(worldRef = this.world, sourceX = this.x, sourceY = this.y, options = {}) {
if (this.dead || this.type !== "plushie") return false;
const world = worldRef || this.world;
this.world = world || this.world;
const owner = this.owner(world);
let vx = Number(options.vx);
let vy = Number(options.vy);
let speed = Math.hypot(Number.isFinite(vx) ? vx : 0, Number.isFinite(vy) ? vy : 0);
if (!Number.isFinite(vx) || !Number.isFinite(vy) || speed < 80) {
let dx = (Number(this.x || 0) || 0) - (Number(sourceX || 0) || 0);
let dy = (Number(this.y || 0) || 0) - (Number(sourceY || 0) || 0);
let len = Math.hypot(dx, dy);
if (len < 0.001 && owner) {
dx = Number(owner.facing || owner.faceDir || 1) || 1;
dy = -0.34;
len = Math.hypot(dx, dy);
}
if (len < 0.001) {
dx = Math.random() < 0.5 ? -1 : 1;
dy = -0.24;
len = Math.hypot(dx, dy);
}
const base = Math.max(520, Math.min(900, Number(options.speed || 650) || 650));
vx = dx / len * base;
vy = dy / len * base - Math.max(80, base * 0.18);
speed = Math.hypot(vx, vy);
} else if (speed < 520) {
const scale = 520 / Math.max(1, speed);
vx *= scale;
vy *= scale;
speed = Math.hypot(vx, vy);
}
this.vx = clamp(vx, -980, 980);
this.vy = clamp(vy, -980, 980);
this.spin = Number(this.spin || 0) || 0;
const spinSign = Math.sign(this.vx) || (Math.random() < 0.5 ? -1 : 1);
const autoSpin = spinSign * (11 + Math.min(14, speed / 80));
this.spinVelocity = clamp(Number(options.spinVelocity || autoSpin) || autoSpin, -28, 28);
this.plushieFlingTimer = Math.max(1.3, Math.min(3.4, Number(options.life || 2.7) || 2.7));
this.plushieTrailTimer = 0;
this.carriedById = "";
this.onHead = false;
this.playerHeld = false;
this._heldByPlayer = false;
if (owner) {
owner.target = null;
owner.thought = "ぬいぐるみが飛んでいった";
owner.pinchThoughtTimer = Math.max(owner.pinchThoughtTimer || 0, 1.1);
owner.surpriseTimer = Math.max(owner.surpriseTimer || 0, 0.45);
if (typeof applyNeedShock === "function") applyNeedShock(owner, { fulfill: 12, safety: 4 }, this);
}
if (world?.effects && typeof Effect !== "undefined") {
world.effects.push(new Effect("ring", this.x, this.y, { size: 30, life: 0.22, color: "rgba(190,142,92,0.38)" }));
world.effects.push(new Effect("fall", this.x, this.y, { size: 18, life: 0.28, color: "rgba(190,142,92,0.28)" }));
}
world?.markItemBucketsDirty?.("plushie-fling");
world?.markSpatialDirty?.("plushie-fling");
if (world) world.drawListDirty = true;
if (options.log !== false) world?.log?.("ぬいぐるみが回転しながら飛んでいった。", "observe", { eventType: "plushie_fling", participants: owner ? [owner] : [] });
return true;
}
draw(ctx, time = 0, lighting = {}) {
if (this.dead) return;
let ownerAlpha = 1;
@ -146,7 +245,7 @@
const spriteId = "plushie_bear";
this.plushieSpriteId = spriteId;
const img = getRenderableImage(spriteId, spriteId);
ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05);
ctx.rotate((Number(this.spin || 0) || 0) + Math.sin(time * 2 + this.seed) * 0.05);
if (img) {
const metrics = getImageMetrics(spriteId) || getImageMetrics("smile");
const w = this.r * 3.35;

View file

@ -277,25 +277,6 @@ function createPanicEscapeActionSpec() {
};
}
function createFleeActionSpec() {
return {
id: "flee",
need: "safety",
subNeed: "avoid",
state: "flee",
label: "\u5371\u306a\u3044\u3082\u306e\u304b\u3089\u96e2\u308c\u3066\u3044\u308b",
phrase: "\u96e2\u308c\u305f",
weight: 40,
selectTarget(t, world) { return findNearbyDanger(world, t, 220); },
canStart: basicCanStartWithTarget,
start(t, world, ctx = {}) {
const danger = ctx.target ?? this.selectTarget(t, world, ctx);
t.setActionState?.("flee", { target: t.panicDestination?.(danger), reason: this.label, wake: true, sleeping: false });
return true;
},
tick: updatePanicBehavior,
};
}
function createApproachFriendActionSpec() {
return {
@ -473,7 +454,6 @@ const TARINAI_ACTION_DEFINITIONS = [
createRestActionSpec(),
createSunbathActionSpec(),
createPanicEscapeActionSpec(),
createFleeActionSpec(),
{
id: "hide_at_owned_structure",
need: "safety",

View file

@ -12,11 +12,6 @@
return Number.isFinite(n) ? n : fallback;
}
function toList(value) {
if (Array.isArray(value)) return value.filter(Boolean).map(String);
if (value == null || value === "") return [];
return [String(value)];
}
function hasOwnFn(obj, key) {
return typeof obj?.[key] === "function";
@ -51,9 +46,7 @@
phrase: String(input.phrase || input.label || id),
weight: finiteOr(input.weight, 1),
priority: finiteOr(input.priority, 0),
tags: toList(input.tags),
timerBacked: Boolean(input.timerBacked),
interruptible: input.interruptible !== false,
};
const originalCanStart = hasOwnFn(input, "canStart") ? input.canStart : null;
@ -98,11 +91,6 @@
return spec.label;
};
Object.defineProperty(spec, "__actionSpec", {
value: true,
enumerable: false,
configurable: false,
});
return spec;
}
@ -135,14 +123,14 @@
const canCtx = isObject(ctx) ? ctx : {};
if (hasOwnFn(spec, "canStart") && !spec.canStart(tarinai, world, canCtx)) return false;
const nextCtx = buildActionContext(tarinai, world, canCtx, spec);
return hasOwnFn(spec, "start") ? spec.start(tarinai, world, nextCtx) : spec.run?.(tarinai, world, nextCtx);
return spec.start(tarinai, world, nextCtx);
}
function tickTarinaiAction(action, tarinai, world, dt = 0, needs = null, ctx = {}) {
const spec = resolveActionSpec(action);
if (!spec) return undefined;
const nextCtx = buildActionContext(tarinai, world, ctx, spec);
return hasOwnFn(spec, "tick") ? spec.tick(tarinai, world, dt, needs, nextCtx) : spec.update?.(tarinai, world, dt, needs, nextCtx);
return spec.tick(tarinai, world, dt, needs, nextCtx);
}
function finishTarinaiAction(action, tarinai, world, ctx = {}) {

View file

@ -37,7 +37,6 @@
seek_bed: "sleep_in_bed",
sleep: "sleep_anywhere",
panic: "panic_escape",
flee: "flee",
fight: "fight_rival",
intimidate: "intimidate_enemy",
birth_ritual: "birth_ritual",

View file

@ -4,8 +4,6 @@
const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_behavior_state.js");
const PHASES = new Set(["starting", "seeking", "approaching", "acting", "recovering", "finished", "failed", "idle", "start", "active", "perform"]);
function isObject(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@ -19,10 +17,6 @@
return finiteOr(tarinai?.world?.time, 0);
}
function normalizePhase(value, fallback = "acting") {
const phase = String(value || fallback || "acting");
return PHASES.has(phase) ? phase : phase;
}
function targetIdFor(target = null) {
if (!target) return null;
@ -121,7 +115,7 @@
const behavior = {
actionId,
phase: normalizePhase(input.phase || previous?.phase || (String(tarinai?.state || "") === "idle" ? "idle" : "acting")),
phase: String(input.phase || previous?.phase || (String(tarinai?.state || "") === "idle" ? "idle" : "acting")),
target,
source,
reason,

View file

@ -44,7 +44,7 @@ function actionTextForReason(action = null, tarinai = null) {
if (cause.includes("\u3051\u3093\u304b\u3092\u58f2\u3089\u308c")) return "\u53cd\u6483\u3057\u3066\u3044\u308b";
return target?.name ? `${name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b";
}
if (id === "panic_escape" || id === "flee") return "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
if (id === "panic_escape") return "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
return String(action?.label || action?.phrase || behavior?.label || "\u307c\u30fc\u3063\u3068\u3057\u3066\u3044\u308b").replace(/[\u3002\uff01\uff1f]+$/, "");
}

View file

@ -137,6 +137,7 @@ function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
for (const item of candidates) {
if (!item || item.dead || !roleMatches(item, "grassMaterial")) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue;
if (world?.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65))) continue;
const available = grassMaterialAvailableStages(item);
if (available < required) continue;
const reserved = reservedGrassStagesForBuildMaterial(world, item, type, tarinai);

View file

@ -34,10 +34,6 @@ function isLiveTarinaiEntity(value) {
return Boolean(value && !value.dead && typeof value.relationTo === "function" && Number.isFinite(value.x) && Number.isFinite(value.y));
}
function relationFearSafe(holder, other) {
if (!holder || !other || typeof holder.relationTo !== "function") return 0;
return Number(holder.relationTo(other.id)?.fear || 0) || 0;
}
function nearestForcedFightTarget(tarinai, world, maxDist = 720) {
if (!tarinai || !world) return null;

View file

@ -17,8 +17,8 @@
const canBite = !tarinai.isZunchiSlave && tarinai.eatCooldown <= 0 && mealReady && wantsFood && tarinai.hunger > 18;
const canSweetBite = !tarinai.isZunchiSlave && tarinai.eatCooldown <= 0 && mealReady && (wantsFood || wantsMedicine) && tarinai.hunger > 7;
const canZunchiBite = tarinai.eatCooldown <= 0 && (tarinai.isZunchiSlave ? tarinai.hunger > 16 : (tarinai.hunger >= 102 || foodNeed >= 92)) && (tarinai.isZunchiSlave || !hasBetterFoodThanZunchiNearby(tarinai, tarinai.world, 260));
const scanLimit = (tarinai.hunger >= 96 || foodNeed >= 84) ? 32 : 18;
const foodScanRadius = tarinai.radius + ((tarinai.hunger >= 96 || foodNeed >= 84) ? 104 : 56);
const scanLimit = 28;
const foodScanRadius = tarinai.radius + 92;
const nearbyFood = tarinai.world.nearbyFood?.(tarinai.x, tarinai.y, foodScanRadius, true) || [];
const nearbyItems = nearbyFood.length ? nearbyFood : tarinai.world.nearbyItems(tarinai.x, tarinai.y, foodScanRadius);
const foodCandidates = [];

View file

@ -101,6 +101,7 @@ function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) {
for (const it of world.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world.items || []) {
if (!it || it.dead || it.type === "zunchi") continue;
if (tarinai.shouldAvoidTarget?.(it)) continue;
if (tarinaiTargetBlockedByObstacle(world, tarinai, it, "food")) continue;
const type = effectiveFoodTypeForItem(it);
if (!type) continue;
if (["sweet", "food", "grass", "water", "zunda_juice", "ant_corpse"].includes(type) || (isServingFoodType(type))) {
@ -130,25 +131,48 @@ function zunchiEatingBlocked(tarinai, item, world) {
return false;
}
function tarinaiTargetBlockedByObstacle(world, tarinai, item, role = "") {
if (!world || !tarinai || !item) return false;
// ずんだ餅だけは障害物越しでも食べに行く。
if ((role === "food" || role === "medicine") && effectiveFoodTypeForItem(item) === "sweet") return false;
return Boolean(world.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65)));
}
function foodRoleSearchTypes(role = "") {
const set = new Set();
if (role === "food") {
for (const type of ["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"]) set.add(type);
if (typeof FOOD_SERVING_TYPES !== "undefined") for (const type of FOOD_SERVING_TYPES) set.add(type);
} else if (role === "drink") {
for (const type of ["water", "zunda_juice", "duplicator"]) set.add(type);
} else if (role === "medicine") {
for (const type of ["sweet", "water", "zunda_juice", "sleep_drug", "love_mochi", "fight_mochi", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "duplicator"]) set.add(type);
if (typeof PARAM_EFFECT_ITEM_TYPES !== "undefined") for (const type of PARAM_EFFECT_ITEM_TYPES) set.add(type);
}
return set;
}
function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
let best = null, bestScore = Infinity;
const hunger = Number(tarinai?.hunger || 0) || 0;
const foodNeed = Number(tarinai?.needRaw?.food ?? tarinai?.needs?.food ?? 0) || 0;
const foodEmergency = role === "food" && (hunger >= 96 || foodNeed >= 84);
const searchDist = foodEmergency ? Math.max(maxDist, 1180) : maxDist;
const primary = ((role === "food" || role === "drink" || role === "medicine") && world?.nearbyFood)
const searchDist = Math.max(1, Number(maxDist) || 520);
const foodLikeRole = role === "food" || role === "drink" || role === "medicine";
const primary = (foodLikeRole && world?.nearbyFood)
? world.nearbyFood(tarinai.x, tarinai.y, searchDist, true)
: (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []);
const candidates = Array.isArray(primary) ? primary.slice() : [];
if (foodEmergency && world?.itemsOfType) {
const emergencyTypes = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "grass", "zunchi", "ant_corpse", "zunda_juice", "duplicator"]);
if (typeof FOOD_SERVING_TYPES !== "undefined") for (const type of FOOD_SERVING_TYPES) emergencyTypes.add(type);
for (const type of emergencyTypes) {
for (const it of world.itemsOfType(type) || []) {
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
}
const addCandidate = (it) => {
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
};
// 食料・水・薬は、通常時から空間インデックスだけに依存しない。
// 生成直後/移動直後のアイテムが nearbyFood のキャッシュから漏れても、距離と roleMatches で最終判定する。
if (foodLikeRole) {
if (Array.isArray(world?.items)) for (const it of world.items) addCandidate(it);
if (world?.itemsOfType) {
for (const type of foodRoleSearchTypes(role)) for (const it of world.itemsOfType(type) || []) addCandidate(it);
}
}
for (const item of candidates) {
if (!roleMatches(item, role)) continue;
if (role === "food" && typeof tarinai?.foodItemHasServingLeft === "function") {
@ -160,11 +184,16 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
if (tarinai?.shouldAvoidTarget?.(item)) continue;
const d = item.type === "duplicator" ? foodInteractionDistance(tarinai, item) : dist(tarinai, item);
if (d > searchDist) continue;
const blocked = tarinaiTargetBlockedByObstacle(world, tarinai, item, role);
let score = d;
if (role === "food" || role === "medicine") {
const priority = foodPriorityScore(tarinai, item, d);
if (!Number.isFinite(priority)) continue;
score = -priority;
score = -priority + (blocked ? 260 : 0);
} else if (blocked && !foodLikeRole) {
continue;
} else if (blocked) {
score += 180;
}
if (score < bestScore) { best = item; bestScore = score; }
}
@ -295,6 +324,7 @@ function findOwnedStructure(world, tarinai, type = "", maxDist = Infinity) {
for (const item of world?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId !== tarinai.id) continue;
if (type && item.type !== type) continue;
if (tarinaiTargetBlockedByObstacle(world, tarinai, item, "structure")) continue;
const d = dist(tarinai, item);
if (d < bestD) { best = item; bestD = d; }
}
@ -307,6 +337,7 @@ function findUnownedStructure(world, tarinai, type = "", maxDist = Infinity) {
if (!item?.isStructure || item.dead || item.ownerId) continue;
if (type && item.type !== type) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue;
if (tarinaiTargetBlockedByObstacle(world, tarinai, item, "structure")) continue;
const d = dist(tarinai, item);
if (d < bestD) { best = item; bestD = d; }
}

View file

@ -47,7 +47,6 @@ const TARINAI_ACTION_LOCK_SECONDS = {
rest_to_recover: 6.0,
sunbath: 10.5,
panic_escape: 2.1,
flee: 1.7,
hide_at_owned_structure: 6.4,
approach_friend: 6.4,
approach_parent_or_child: 7.0,
@ -66,7 +65,6 @@ const TARINAI_ACTION_PRIORITY = {
fight_rival: 56,
intimidate_enemy: 42,
panic_escape: 94,
flee: 92,
hide_at_owned_structure: 90,
use_medicine: 86,
rest_to_recover: 84,
@ -94,7 +92,6 @@ function actionPriority(actionOrId) {
return Number(TARINAI_ACTION_PRIORITY[id] ?? actionOrId?.priority ?? 0) || 0;
}
const TARINAI_UNINTERRUPTIBLE_STATES = ["panic", "intimidate", "fight", "birth_ritual"];
const TARINAI_FORCED_BEHAVIOR_SOURCE_PRIORITY = {
fight_mochi: 180,
love_mochi: 150,
@ -229,7 +226,7 @@ function startNeedDrivenEmergencyReaction(tarinai, breaker = null, reason = "\u3
return true;
}
}
const panicAction = actions.find(a => a.id === "panic_escape") || actions.find(a => a.id === "flee");
const panicAction = actions.find(a => a.id === "panic_escape");
if (!panicAction) return false;
const choice = { need: "safety", tiedNeeds: ["safety"], max: Number(tarinai.needRaw?.safety || tarinai.needs?.safety || 0) || 80 };
const reasonText = reason && reason.endsWith("\u3002") ? reason : buildReasonText("safety", choice.tiedNeeds, panicAction, tarinai, world);

View file

@ -5,7 +5,7 @@
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
if (typeof window !== "undefined") {
Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, chooseActionForNeed, buildReasonText, formatCauseActionText, forcedCausePhrase, refreshBehaviorReasonText, fulfillPrimaryReason, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, 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 });
Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, 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, isLiveTarinaiEntity, actionById });
}
function decayNeedSatisfaction(tarinai, dt = 0) {
@ -242,16 +242,6 @@ function actionSelectionBonus(action, need, tarinai, options = {}) {
return bonus;
}
function chooseActionForNeed(need, tarinai, world) {
const actions = TARINAI_ACTIONS.filter(action => action.need === need && canStartTarinaiAction(action, tarinai, world));
const weighted = actions.map(action => {
const bonus = actionSelectionBonus(action, need, tarinai, { includeComfortActions: true });
return { ...action, weight: Math.max(0.01, (Number(action.weight) || 1) + bonus) };
});
const picked = weightedPickAction(weighted) || TARINAI_ACTIONS.find(action => action.id === "wander_lightly");
return picked ? TARINAI_ACTIONS.find(action => action.id === picked.id) || picked : null;
}
function socialSubNeedStartThreshold(subNeed = "") {
return ({ conflict: 34, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16;
@ -409,17 +399,11 @@ function startNeedAction(tarinai, world, choice, action, reasonText, options = {
return Boolean(ran);
}
function shouldInterruptCurrentAction(tarinai, needs) {
// \u6b32\u6c42\u5024\u306e\u81ea\u7136\u5909\u52d5\u3060\u3051\u3067\u306f\u5b9f\u884c\u4e2d\u884c\u52d5\u3092\u4e2d\u65ad\u3057\u306a\u3044\u3002
// \u85ac\u30fb\u9905\u306a\u3069\u306e\u5916\u90e8\u5f37\u5236\u884c\u52d5\u306f forcedBehaviorQueue \u5074\u306e\u512a\u5148\u9806\u4f4d\u3067\u51e6\u7406\u3059\u308b\u3002
return false;
}
function continueCurrentBehavior(tarinai, world, needs, dt) {
const behavior = currentTarinaiBehavior(tarinai);
const actionId = behavior?.actionId;
if (!actionId) return false;
if (shouldInterruptCurrentAction(tarinai, needs) && !isCurrentBehaviorForced(tarinai)) return false;
const action = actionById(actionId);
if (!action) return false;
if (tarinai.target && tarinai.target.dead) return false;
@ -456,9 +440,19 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
if (behaviorIsForced(behavior)) retryForcedBehavior(tarinai, behavior, "update_failed");
tarinai.actionCooldowns = tarinai.actionCooldowns || {};
tarinai.actionIdCooldowns = tarinai.actionIdCooldowns || {};
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 1.6);
tarinai.actionIdCooldowns[action.id] = Math.max(tarinai.actionIdCooldowns[action.id] || 0, 2.4);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.55);
const consumableAction = action.id === "eat_food" || action.id === "drink_water" || action.id === "use_medicine";
if (consumableAction) {
// 食料・水・薬は環境変化が速いので、失敗時も短い再評価だけにする。
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 0.35);
tarinai.actionIdCooldowns[action.id] = Math.max(tarinai.actionIdCooldowns[action.id] || 0, 0.45);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.18);
tarinai.target = null;
tarinai.targetKey = "";
} else {
tarinai.actionCooldowns[need] = Math.max(tarinai.actionCooldowns[need] || 0, 1.6);
tarinai.actionIdCooldowns[action.id] = Math.max(tarinai.actionIdCooldowns[action.id] || 0, 2.4);
tarinai.behaviorSwitchCooldownUntil = Math.max(tarinai.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 0.55);
}
clearBehavior(tarinai);
return false;
}
@ -510,6 +504,8 @@ function shouldWakeFromSleep(tarinai, world, needs) {
const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now));
if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true;
if (findNearbyDanger(world, tarinai, 160)) return true;
const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
if ((tarinai.hunger || 0) >= 84 || foodNeed >= needThreshold("food", "start")) return true;
// \u81ea\u7136\u306a\u7761\u7720\u306f\u77ed\u3044\u5b89\u5168\u30ed\u30c3\u30af\u5f8c\u3001\u5341\u5206\u306b\u56de\u5fa9\u3057\u305f\u3089\u8d77\u304d\u308b\u3002
if (elapsed < (Number(session.minDuration) || 8)) return false;
const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0;

View file

@ -12,9 +12,13 @@
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 sleepTargetBlocked(worldRef, tarinai, item) {
if (!worldRef || !tarinai || !item) return false;
return Boolean(worldRef.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65)));
}
function nestBoxCapacity(worldRef, box = null) {
if (worldRef?.nestBoxCapacity && worldRef.nestBoxCapacity !== nestBoxCapacityFacade) return worldRef.nestBoxCapacity(box);
return 5;
return typeof worldRef?.nestBoxCapacity === "function" ? worldRef.nestBoxCapacity(box) : 5;
}
function nestBoxOccupants(worldRef, box, limit = 5) {
@ -72,6 +76,7 @@
function sleepPlaceAvailableFor(worldRef, tarinai, item) {
if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false;
if (tarinai.shouldAvoidTarget?.(item)) return false;
if (sleepTargetBlocked(worldRef, tarinai, 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);
@ -82,6 +87,7 @@
for (const item of worldRef?.items || []) {
if (!item?.isStructure || item.dead || item.ownerId !== tarinai?.id) continue;
if (type && item.type !== type) continue;
if (sleepTargetBlocked(worldRef, tarinai, item)) continue;
const d = distFn(tarinai, item);
if (d < bestD) { best = item; bestD = d; }
}
@ -94,6 +100,7 @@
if (!item?.isStructure || item.dead || item.ownerId) continue;
if (type && item.type !== type) continue;
if (tarinai?.shouldAvoidTarget?.(item)) continue;
if (sleepTargetBlocked(worldRef, tarinai, item)) continue;
const d = distFn(tarinai, item);
if (d < bestD) { best = item; bestD = d; }
}
@ -129,9 +136,6 @@
|| 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,

View file

@ -473,7 +473,6 @@
ctx.restore();
}
}
drawDawnRimLight(ctx, dw, dh, lightState);
if (faceRight) ctx.scale(-1, 1);

View file

@ -370,7 +370,13 @@
}
if (this.target && !this.target.dead) {
const moveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "ant_attack"]);
const routeTargetItem = this.target?.hostItem || this.target;
const waypoint = routeStates.has(this.state) && this.world?.findTarinaiPathWaypoint
? this.world.findTarinaiPathWaypoint(this, baseMoveTarget, { targetItem: routeTargetItem, padding: Math.max(18, (this.radius || 20) * 0.82), state: this.state })
: null;
const moveTarget = waypoint || baseMoveTarget;
const dx = moveTarget.x - this.x;
const dy = moveTarget.y - this.y;
const d = Math.hypot(dx, dy) || 1;

View file

@ -90,10 +90,10 @@
if (hardRealtimeUrgent) return { interval: 0, lane: "urgent", smoothMotion: false, visible: isVisible, urgent: true };
// Panic movement must stay visually continuous, but the full creature
// pipeline does not need to run every rendered frame. Treat panic/flee as
// pipeline does not need to run every rendered frame. Treat panic as
// a high-priority motion lane: AI/timers are quantized, motion-only still
// runs on visible skipped frames.
const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic|flee/i.test(id);
const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic/i.test(id);
if (panicUrgent) {
const interval = isVisible ? (mobile ? 0.13 : (tier === "low" ? 0.12 : 0.08)) : (isNear ? (mobile ? 0.24 : 0.18) : (mobile ? 0.46 : 0.36));
return { interval, lane: "panic", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, urgent: true };

View file

@ -121,15 +121,6 @@ function bindUI() {
ui.panelQuickTabs?.addEventListener("pointerup", activatePanelQuickTab, { passive: false });
ui.panelQuickTabs?.addEventListener("touchend", activatePanelQuickTab, { passive: false });
/* old quick-tab binding removed */
if (false) ui.panelQuickTabs?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-panel-tab]");
if (!btn) return;
e.preventDefault();
audio.uiClick?.();
scrollPanelToTab(btn.dataset.panelTab || "");
});
ui.panel?.addEventListener("scroll", () => {
const panel = ui.panel;
if (!panel || !ui.panelQuickTabs) return;

View file

@ -148,7 +148,10 @@
function syncGrabbedPhysicsItem(target, reason = "pinch-move") {
if (!target || target.dead) return false;
const pb = global.TarinaiPhysicsBodySystem;
if (!pb.isBodyType(target.type)) return false;
const isBody = typeof pb?.isBodyType === "function"
? pb.isBodyType(target)
: Boolean(typeof pb?.isPhysicsType === "function" && pb.isPhysicsType(target) && typeof pb?.ensureBody === "function" && pb.ensureBody(target, null, { syncFromLegacy: false }));
if (!isBody) return false;
pb.syncPoseFromItem(target);
pb.setScalar(target, "awakeUntil", Math.max(Number(pb.scalar(target, "awakeUntil", 0) || 0), (world.time || 0) + 0.65), reason);
pb.markBodyChanged(target, reason);
@ -278,7 +281,13 @@
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900);
const releaseSpeed = Math.hypot(releaseVx, releaseVy);
if (releaseSpeed > 18) {
if (target.type === "plushie" && typeof target.fling === "function") {
const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x;
const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y;
const vx = releaseSpeed > 80 ? releaseVx : 0;
const vy = releaseSpeed > 80 ? releaseVy : 0;
target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 });
} else 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));

View file

@ -114,7 +114,6 @@
dispatchTapAt,
dispatchTapClient,
cancel,
clearHoseHoldTimer,
});
}

View file

@ -54,19 +54,8 @@ function drawToolPreviewCanvas(canvas, itemType, { watermark = false } = {}) {
const ctx = canvas.getContext("2d");
if (!ctx) return false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (typeof window.drawToolItemPreview === "function") {
return window.drawToolItemPreview(ctx, itemType, { width: canvas.width, height: canvas.height, compact: true, watermark }) !== false;
}
if (typeof window.isConsumableSpriteType === "function" && window.isConsumableSpriteType(itemType) && typeof window.drawConsumableFieldSprite === "function") {
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
const baseScale = itemType === "protein" || itemType === "niteropu" ? 0.9 : 1.0;
ctx.scale(baseScale, baseScale);
window.drawConsumableFieldSprite(ctx, itemType, 17.5, 112.4, { compact: true });
ctx.restore();
return true;
}
return false;
if (typeof window.drawToolItemPreview !== "function") return false;
return window.drawToolItemPreview(ctx, itemType, { width: canvas.width, height: canvas.height, compact: true, watermark }) !== false;
}
function createToolPreviewCanvas(className, size = 96) {
@ -83,7 +72,7 @@ function attachToolSpritePreview(btn, def, toolId) {
if (!btn || !itemType || !def?.placeable) return;
if (toolId === "reciprocator") return;
if (toolId === "ball" || itemType === "ball") return;
if (typeof window.drawToolItemPreview !== "function" && typeof window.drawConsumableFieldSprite !== "function") return;
if (typeof window.drawToolItemPreview !== "function") return;
const iconCanvas = createToolPreviewCanvas("tool-sprite-preview", 160);
const iconDrawn = drawToolPreviewCanvas(iconCanvas, itemType);

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "39.15.65";
const APP_BUILD = "tarinai-mood-unification-v39-15-65";
const APP_VERSION = "39.15.75";
const APP_BUILD = "tarinai-ai-needs-cleanup-v39-15-75";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -835,11 +835,13 @@
|| intersects(x1, y1, x2, y2, r.left, r.bottom, r.left, r.top);
},
pathBlockedByFence(x1, y1, x2, y2, padding = 16) {
pathBlockedByFence(x1, y1, x2, y2, padding = 16, opts = {}) {
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const maxD = Math.hypot(x2 - x1, y2 - y1) / 2 + 120;
for (const rect of this.nearbySolidObstacleRects(cx, cy, maxD + padding + 170)) {
const exclude = opts?.exclude || null;
for (const rect of this.nearbySolidObstacleRects(cx, cy, maxD + padding + 170, { exclude })) {
if (exclude && rect?.item === exclude) continue;
if (rect?.oriented) {
const r = { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding };
if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true;
@ -851,6 +853,118 @@
return false;
},
targetBlockedByObstacle(actor, target, padding = 14) {
if (!actor || !target || !Number.isFinite(Number(actor.x)) || !Number.isFinite(Number(actor.y)) || !Number.isFinite(Number(target.x)) || !Number.isFinite(Number(target.y))) return false;
return Boolean(this.pathBlockedByFence?.(actor.x, actor.y, target.x, target.y, padding, { exclude: target }));
},
pointBlockedByObstacle(x, y, padding = 14, opts = {}) {
const exclude = opts?.exclude || null;
const searchRadius = Math.max(48, Number(padding || 0) + 72);
for (const rect of this.nearbySolidObstacleRects?.(x, y, searchRadius, { exclude, maxChecks: Math.max(10, Number(opts.maxChecks || 0) || 24) }) || []) {
if (exclude && rect?.item === exclude) continue;
if (this.pointInRect?.(x, y, rect, padding)) return true;
}
return false;
},
findTarinaiPathWaypoint(actor, target, opts = {}) {
if (!actor || !target || !Number.isFinite(Number(actor.x)) || !Number.isFinite(Number(actor.y)) || !Number.isFinite(Number(target.x)) || !Number.isFinite(Number(target.y))) return null;
const tx = Number(target.x);
const ty = Number(target.y);
const ax = Number(actor.x);
const ay = Number(actor.y);
const targetItem = opts?.targetItem || target?.hostItem || (target?.isStructure || target?.type ? target : null);
const exclude = opts?.exclude || targetItem || null;
const rr = Math.max(8, Number(actor.radius || 20) || 20);
const padding = Math.max(16, Number(opts.padding || 0) || rr * 0.78);
const directClear = !this.pathBlockedByFence?.(ax, ay, tx, ty, padding, { exclude });
if (directClear) {
actor._pathWaypoint = null;
return null;
}
const targetId = targetItem?.id || target?.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
const cached = actor._pathWaypoint || null;
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
&& !this.pointBlockedByObstacle(cached.x, cached.y, Math.max(6, padding * 0.45), { exclude, maxChecks: 18 })
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId };
}
const midX = (ax + tx) * 0.5;
const midY = (ay + ty) * 0.5;
const totalD = Math.max(1, Math.hypot(tx - ax, ty - ay));
const rects = this.nearbySolidObstacleRects?.(midX, midY, totalD * 0.5 + padding + 190, { exclude, maxChecks: Math.max(32, Number(opts.maxChecks || 0) || 46), maxRects: 80 }) || [];
const worldPad = (typeof CONFIG !== "undefined" ? CONFIG.worldPadding : 28) + rr * 0.45;
const candidates = [];
const add = (x, y, sourceRect, grade = 0) => {
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
if (x < worldPad || y < worldPad || x > this.w - worldPad || y > this.h - worldPad) return;
candidates.push({ x, y, sourceRect, grade });
};
const worldPoint = (r, lx, ly) => {
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
const sn = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0);
const cx = Number(r.cx ?? ((Number(r.left || 0) + Number(r.right || 0)) * 0.5)) || 0;
const cy = Number(r.cy ?? ((Number(r.top || 0) + Number(r.bottom || 0)) * 0.5)) || 0;
return { x: cx + lx * c - ly * sn, y: cy + lx * sn + ly * c };
};
const addRectCandidates = (r, index = 0) => {
const pad = Math.max(34, padding + rr * 1.45 + 18 + Math.min(18, index * 3));
if (r.oriented) {
const hw = Math.max(1, Number(r.halfW || 0) || 1) + pad;
const hh = Math.max(1, Number(r.halfH || 0) || 1) + pad;
for (const [lx, ly, grade] of [[-hw, -hh, 0], [hw, -hh, 0], [hw, hh, 0], [-hw, hh, 0], [0, -hh, 1], [hw, 0, 1], [0, hh, 1], [-hw, 0, 1]]) {
const p = worldPoint(r, lx, ly);
add(p.x, p.y, r, grade);
}
} else {
const left = Number(r.left || 0) - pad;
const right = Number(r.right || 0) + pad;
const top = Number(r.top || 0) - pad;
const bottom = Number(r.bottom || 0) + pad;
const cx = (left + right) * 0.5;
const cy = (top + bottom) * 0.5;
for (const [x, y, grade] of [[left, top, 0], [right, top, 0], [right, bottom, 0], [left, bottom, 0], [cx, top, 1], [right, cy, 1], [cx, bottom, 1], [left, cy, 1]]) add(x, y, r, grade);
}
};
let blockers = 0;
for (const rect of rects) {
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor?.(actor, rect.item)) continue;
const inflated = rect?.oriented
? { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding }
: { left: (rect.left || 0) - padding, right: (rect.right || 0) + padding, top: (rect.top || 0) - padding, bottom: (rect.bottom || 0) + padding };
if (!this.segmentIntersectsRect?.(ax, ay, tx, ty, inflated)) continue;
addRectCandidates(rect, blockers);
blockers += 1;
if (blockers >= 4) break;
}
if (!candidates.length) return null;
let best = null;
let bestScore = Infinity;
const old = cached && Number.isFinite(cached.x) && Number.isFinite(cached.y) ? cached : null;
for (const c of candidates) {
if (this.pointBlockedByObstacle(c.x, c.y, Math.max(5, padding * 0.42), { exclude, maxChecks: 20 })) continue;
const firstBlocked = this.pathBlockedByFence?.(ax, ay, c.x, c.y, padding, { exclude });
if (firstBlocked) continue;
const secondBlocked = this.pathBlockedByFence?.(c.x, c.y, tx, ty, padding, { exclude });
const d1 = Math.hypot(c.x - ax, c.y - ay);
const d2 = Math.hypot(tx - c.x, ty - c.y);
const progress = totalD - d2;
const cacheBias = old ? Math.min(90, Math.hypot(c.x - old.x, c.y - old.y) * 0.35) : 0;
const score = d1 + d2 + (secondBlocked ? 780 - progress * 0.45 : 0) + (c.grade || 0) * 26 + cacheBias;
if (score < bestScore) { best = c; bestScore = score; }
}
if (!best) return null;
actor._pathWaypoint = { x: best.x, y: best.y, targetId, until: (this.time || 0) + 0.95 };
return { x: best.x, y: best.y, dead: false, detour: true, routeTargetId: targetId };
},
grassBlockedAt(x, y, self = null, opts = {}) {
const ignoreSoft = Boolean(opts?.ignoreSoftPlacement);
for (const it of this.nearbyItems(x, y, 96)) {

View file

@ -148,6 +148,88 @@
}
function ensureRobotCleanerEditor() {
let dialog = document.getElementById("robotCleanerEditor");
if (dialog) return dialog;
dialog = document.createElement("div");
dialog.id = "robotCleanerEditor";
dialog.className = "signboard-editor robot-cleaner-editor hidden";
dialog.innerHTML = `
<div class="signboard-editor-panel robot-cleaner-editor-panel" role="dialog" aria-modal="true" aria-labelledby="robotCleanerEditorTitle">
<div class="signboard-editor-titlebar">
<h2 id="robotCleanerEditorTitle">\u30ed\u30dc\u6383\u9664\u6a5f</h2>
<button id="robotCleanerEditorClose" class="signboard-editor-close" type="button" aria-label="\u9589\u3058\u308b">\u00d7</button>
</div>
<div class="robot-cleaner-editor-help">\u7247\u4ed8\u3051\u308b\u5bfe\u8c61\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002</div>
<div class="robot-cleaner-targets">
<label><input type="checkbox" data-robot-target="zunchi"> \u305a\u3093\u3061</label>
<label><input type="checkbox" data-robot-target="ant_corpse"> \u6b7b\u9ab8</label>
<label><input type="checkbox" data-robot-target="tarinai"> \u305f\u308a\u306a\u3044</label>
<label><input type="checkbox" data-robot-target="grass"> \u8349</label>
<label><input type="checkbox" data-robot-target="grass_bed"> \u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9</label>
<label><input type="checkbox" data-robot-target="water"> \u6c34\u6ef4</label>
</div>
<div class="robot-cleaner-targets robot-cleaner-target-scan">
<label><input id="robotCleanerHighSpeed" type="checkbox"> \u9ad8\u901f\u30e2\u30fc\u30c9</label>
</div>
<div class="signboard-editor-actions">
<button id="robotCleanerDefault" class="btn" type="button">\u65e2\u5b9a\u306b\u623b\u3059</button>
<button id="robotCleanerCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
<button id="robotCleanerApply" class="btn primary" type="button">\u4fdd\u5b58</button>
</div>
</div>`;
document.body.appendChild(dialog);
return dialog;
}
function openRobotCleanerEditor(robot, worldRef) {
if (!robot || robot.type !== "robot_cleaner") return false;
const api = global.TarinaiItemDynamicToolSystem;
const dialog = ensureRobotCleanerEditor();
const inputs = [...dialog.querySelectorAll("[data-robot-target]")];
const closeBtn = dialog.querySelector("#robotCleanerEditorClose");
const cancel = dialog.querySelector("#robotCleanerCancel");
const apply = dialog.querySelector("#robotCleanerApply");
const def = dialog.querySelector("#robotCleanerDefault");
const highSpeed = dialog.querySelector("#robotCleanerHighSpeed");
const bits = api?.robotCleanerTargetBits || {};
const defaultMask = api?.robotCleanerDefaultMask?.() ?? 3;
const readMask = () => inputs.reduce((mask, input) => input.checked ? (mask | (bits[input.dataset.robotTarget] || 0)) : mask, 0);
const writeMask = (mask) => {
for (const input of inputs) input.checked = Boolean((Number(mask) | 0) & (bits[input.dataset.robotTarget] || 0));
};
const close = () => {
dialog.classList.add("hidden");
dialog.__robot = null;
dialog.__worldRef = null;
};
const commit = (mask) => {
api?.setRobotCleanerMask?.(robot, mask);
api?.setRobotCleanerHighSpeed?.(robot, Boolean(highSpeed?.checked));
robot.robotCleanerTargetId = "";
robot.robotCleanerTargetType = "";
robot.robotCleanerScanTimer = 0;
worldRef?.markItemBucketsDirty?.("robot-cleaner-settings");
if (worldRef) worldRef.drawListDirty = true;
worldRef?.log?.("\u30ed\u30dc\u6383\u9664\u6a5f\u306e\u5bfe\u8c61\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u305f\u3002", "observe");
if (typeof showToast === "function") showToast("\u30ed\u30dc\u6383\u9664\u6a5f\u306e\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002");
global.render?.();
close();
};
dialog.__robot = robot;
dialog.__worldRef = worldRef;
writeMask(api?.robotCleanerMask?.(robot) ?? robot.robotCleanerMask ?? defaultMask);
if (highSpeed) highSpeed.checked = Boolean(api?.robotCleanerHighSpeed?.(robot) ?? robot.robotCleanerHighSpeed);
if (closeBtn) closeBtn.onclick = close;
if (cancel) cancel.onclick = close;
if (def) def.onclick = () => { writeMask(defaultMask); if (highSpeed) highSpeed.checked = false; };
if (apply) apply.onclick = () => commit(readMask());
dialog.onclick = (e) => { if (e.target === dialog) close(); };
dialog.classList.remove("hidden");
return true;
}
function ensureRotatorEditor() {
let dialog = document.getElementById("rotatorEditor");
if (dialog) return dialog;
@ -167,14 +249,14 @@
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
</div>
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
<button type="button" data-rotator-mode="line" class="active"> \u76f4\u7dda</button>
<button type="button" data-rotator-mode="free"> \u81ea\u7531\u63cf\u753b</button>
<button type="button" data-rotator-mode="erase"> \u6d88\u3057\u30b4\u30e0</button>
<button type="button" data-rotator-template="bar"> \u68d2</button>
<button type="button" data-rotator-template="cross"> \u5341\u5b57</button>
<button type="button" data-rotator-template="circle"> \u5186</button>
<button id="rotatorUndo" type="button"> \u4e00\u3064\u623b\u3059</button>
<button id="rotatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
<button type="button" data-rotator-mode="line" class="active">\u2501 \u76f4\u7dda</button>
<button type="button" data-rotator-mode="free">\u3030 \u81ea\u7531\u63cf\u753b</button>
<button type="button" data-rotator-mode="erase">\u232b \u6d88\u3057\u30b4\u30e0</button>
<button type="button" data-rotator-template="bar">\u2503 \u68d2</button>
<button type="button" data-rotator-template="cross">\uff0b \u5341\u5b57</button>
<button type="button" data-rotator-template="circle">\u25cb \u5186</button>
<button id="rotatorUndo" type="button">\u21b6 \u4e00\u3064\u623b\u3059</button>
<button id="rotatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
</div>
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
<p class="rotator-editor-hint">\u4e2d\u592e\u304c\u56de\u8ee2\u8ef8\u3002\u76f4\u7dda\u30fb\u81ea\u7531\u63cf\u753b\u30fb\u6d88\u3057\u30b4\u30e0\u3092\u9078\u3073\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u8f2a\u90ed\u3092\u7de8\u96c6\u3002</p>
@ -448,14 +530,14 @@
<label><input id="reciprocatorSolidEnabled" type="checkbox"> \u5f53\u305f\u308a\u5224\u5b9aON</label>
</div>
<div class="rotator-editor-tools" role="toolbar" aria-label="\u63cf\u753b\u65b9\u6cd5">
<button type="button" data-reciprocator-mode="line" class="active"> \u76f4\u7dda</button>
<button type="button" data-reciprocator-mode="free"> \u81ea\u7531\u63cf\u753b</button>
<button type="button" data-reciprocator-mode="erase"> \u6d88\u3057\u30b4\u30e0</button>
<button type="button" data-reciprocator-template="bar"> \u68d2</button>
<button type="button" data-reciprocator-template="cross"> \u5341\u5b57</button>
<button type="button" data-reciprocator-template="circle"> \u5186</button>
<button id="reciprocatorUndo" type="button"> \u4e00\u3064\u623b\u3059</button>
<button id="reciprocatorClear" type="button">🗑 \u5168\u6d88\u3057</button>
<button type="button" data-reciprocator-mode="line" class="active">\u2501 \u76f4\u7dda</button>
<button type="button" data-reciprocator-mode="free">\u3030 \u81ea\u7531\u63cf\u753b</button>
<button type="button" data-reciprocator-mode="erase">\u232b \u6d88\u3057\u30b4\u30e0</button>
<button type="button" data-reciprocator-template="bar">\u2503 \u68d2</button>
<button type="button" data-reciprocator-template="cross">\uff0b \u5341\u5b57</button>
<button type="button" data-reciprocator-template="circle">\u25cb \u5186</button>
<button id="reciprocatorUndo" type="button">\u21b6 \u4e00\u3064\u623b\u3059</button>
<button id="reciprocatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
</div>
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
<p class="rotator-editor-hint">\u4e2d\u592e\u306e\u8f2a\u90ed\u5168\u4f53\u304c\u3001\u8a2d\u5b9a\u3057\u305f\u65b9\u5411\u3078\u5f80\u5fa9\u79fb\u52d5\u3057\u307e\u3059\u3002\u52d5\u529bOFF\u3067\u306f\u3001\u305f\u308a\u306a\u3044\u30fb\u305a\u3093\u3061\u30fb\u4ed6\u306e\u6a5f\u69cb\u304b\u3089\u62bc\u3055\u308c\u305f\u3068\u304d\u3060\u3051\u52d5\u304d\u307e\u3059\u3002</p>
@ -769,6 +851,7 @@
if (item.type === "signboard") data.text = item.text || "";
if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen);
if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; }
if (item.type === "robot_cleaner") { data.robotCleanerMask = Number(item.robotCleanerMask ?? 3) | 0; data.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); }
if (item.type === "rotator") {
data.physics = { solid: phyScalar(item, "solid", true) !== false, motorOn: phyScalar(item, "motorOn", true) !== false, motorSpeed: Number(phyScalar(item, "motorSpeed", 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
}
@ -784,6 +867,7 @@
function applyCopyBufferToItem(item, data) {
if (!item || !data) return item;
item.r = Number(data.r || item.r || itemRadiusFor?.(item.type, 12) || 12);
if (item.type === "duplicator") item.r = Math.min(24, Math.max(14, Number(item.r || 24) || 24));
item.amount = Math.max(1, Number(data.amount || item.amount || 1));
if (Number.isFinite(Number(data.seed))) item.seed = Number(data.seed);
if (Number.isFinite(Number(data.spin))) item.spin = Number(data.spin);
@ -812,6 +896,14 @@
item.storedFoodLabel = String(data.storedFoodLabel || (item.storedFoodType ? toolLabel(item.storedFoodType) : ""));
if (item.roles) item.roles.food = Boolean(item.storedFoodType);
}
if (item.type === "robot_cleaner") {
const mask = Number(data.robotCleanerMask);
item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x3f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3);
item.robotCleanerHighSpeed = Boolean(data.robotCleanerHighSpeed);
item.robotCleanerSpeed = global.TarinaiItemDynamicToolSystem?.robotCleanerEffectiveSpeed?.(item) ?? (item.robotCleanerHighSpeed ? 120 : 60);
item.robotCleanerTargetId = "";
item.robotCleanerTargetType = "";
}
if (item.type === "rotator") {
const ph = data.physics || {};
phySet(item, "solid", ph.solid !== false, "copy");
@ -882,7 +974,7 @@
function isEditableItem(item) {
return item && !item.dead && (item.type === "signboard" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator");
return item && !item.dead && (item.type === "signboard" || item.type === "robot_cleaner" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator");
}
function findEditableItemAt(worldRef, x, y) {
@ -906,6 +998,7 @@
function openEditableItemEditor(item, worldRef) {
if (!isEditableItem(item)) return false;
if (item.type === "signboard") return openSignboardEditor(item, worldRef);
if (item.type === "robot_cleaner") return openRobotCleanerEditor(item, worldRef);
if (item.type === "rotator" || item.type === "poison_block") return openRotatorEditor(item, worldRef);
if (item.type === "reciprocator") return openReciprocatorEditor(item, worldRef);
if (item.type === "gate_fence") {
@ -1033,7 +1126,7 @@
for (const soft of items) {
const ok = global.TarinaiStructureLifecycle.deleteItem(worldRef, soft, {
reason,
userReason: "置きものが重なった",
userReason: "\u7f6e\u304d\u3082\u306e\u304c\u91cd\u306a\u3063\u305f",
wake: true,
});
if (ok) removed += 1;
@ -1259,6 +1352,7 @@
}
if (hit && d < foundItemD) { foundItem = it; foundItemD = d; }
}
if (foundItem?.type === "plushie") return foundItem;
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;
@ -1359,6 +1453,24 @@
return;
}
}
let plushieTarget = null;
let plushieTargetD = Infinity;
for (let i = this.items.length - 1; i >= 0; i--) {
const it = this.items[i];
if (!it || it.dead || it.type !== "plushie") continue;
const hit = Math.max(34, (it.r || 7) * 4.8);
const d = distXY(x, y, it.x, it.y);
if (d <= hit && d < plushieTargetD) { plushieTarget = it; plushieTargetD = d; }
}
if (plushieTarget && typeof plushieTarget.fling === "function") {
if (plushieTarget.fling(this, x, y, { source: "poke", speed: 720, life: 2.8 })) {
this.effects?.push(new Effect("ring", x, y, { size: 24, life: 0.18, color: "rgba(190,142,92,0.34)" }));
this.drawListDirty = true;
audio.poke?.();
return;
}
}
let ballTarget = null;
let ballTargetD = Infinity;
for (let i = this.items.length - 1; i >= 0; i--) {

View file

@ -35,10 +35,6 @@
return this.events?.emit?.(type, { ...detail, world: this }) || null;
},
on(type, handler) {
return this.events?.on?.(type, handler) || (() => {});
},
markSpatialDirty(reason = "manual") {
this.spatialDirty = true;
this.spatialDirtyReason = reason;

View file

@ -247,7 +247,6 @@ def check_social_action_spec_runtime() -> None:
source = "\n".join(read_text(ROOT / rel) for rel in ["js/tarinai_action_definitions.js", "js/tarinai_needs_items.js"])
required_factories = [
"createPanicEscapeActionSpec",
"createFleeActionSpec",
"createApproachFriendActionSpec",
"createApproachFamilyActionSpec",
"createApproachMateActionSpec",
@ -269,7 +268,6 @@ def check_social_action_spec_runtime() -> None:
fail("configureTarinaiNeedActions should not be needed after ActionSpec runtime consolidation")
forbidden_late_ticks = [
'set("panic_escape",',
'set("flee",',
'set("approach_friend",',
'set("approach_parent_or_child",',
'set("approach_mate",',
@ -615,7 +613,6 @@ def check_processing_split() -> None:
(social, "function updateMateBehavior"),
(social, "function updatePanicBehavior"),
(building, "function continueBuildPlan"),
(needs, "function chooseActionForNeed"),
]
for source, token in required_pairs:
if token not in source:

View file

@ -1,6 +1,6 @@
"use strict";
const APP_VERSION = "39.15.65";
const APP_VERSION = "39.15.75";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`;
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.