1.4
This commit is contained in:
parent
ad2cc76429
commit
7455d630fc
82 changed files with 2179 additions and 900 deletions
|
|
@ -6,9 +6,9 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th
|
|||
|
||||
- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order.
|
||||
- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.47`.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.92`.
|
||||
- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render.
|
||||
- Update order: `js/system_order.js` calls phase facades from `js/world_update_phases.js` / `js/simulation_systems.js`.
|
||||
- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`.
|
||||
- Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents.
|
||||
- Persistence: save files are schema/codec/storage/coordinator modules; snapshot/history modules keep world restoration and stats consistent.
|
||||
|
||||
|
|
|
|||
7
BUILD_NOTE.txt
Normal file
7
BUILD_NOTE.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
tarinai_king_fix_v9
|
||||
- Added fire item: spreads to tarinai, ants, and grass; burning causes continuous damage and panic movement.
|
||||
- Added flame_firecracker: scatters fire when the fuse ends without blast damage/explosion.
|
||||
- Burning tarinai behavior text and fire-sighting panic text are forced through observation data.
|
||||
- Fire can be extinguished by water droplets or random extinguish checks; rain and <=10C increase extinguish chance.
|
||||
- Fixed duplicator stored-water preview color by rendering droplet sprites instead of fallback orange food shape.
|
||||
- Version bumped to 39.15.98.
|
||||
|
|
@ -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.77",
|
||||
"version": "39.15.92",
|
||||
"entrypoints": {
|
||||
"app": "index.html",
|
||||
"runtime": "js/main.js",
|
||||
|
|
|
|||
|
|
@ -31,68 +31,68 @@ The core loop is:
|
|||
|
||||
### Operation
|
||||
|
||||
- **解除 / clear**: cancels the selected tool and returns to normal observe mode.
|
||||
- **たりないデータ / observe**: select a tarinai or inspect compatible objects such as signboards.
|
||||
- **追加 / new**: calls a new tarinai into the field.
|
||||
- **つつく / poke**: pokes tarinai and can push balls.
|
||||
- **つまむ / pinch**: drags tarinai and some placed objects.
|
||||
- **コピー / copy**: copies a placed item and pastes a new instance with its settings.
|
||||
- **戻す / undo** and **進む / redo**: reverse or reapply placement/edit actions.
|
||||
- **削除 / delete**: removes placed tools, objects, and stains.
|
||||
- **範囲削除 / area delete**: removes many nearby objects or stains around the cursor.
|
||||
- **洗浄 / water hose**: drag to clean dirt, zunchi, corpses, and stains.
|
||||
- **解除 / clear**: cancels the selected tool and returns to normal observe mode.
|
||||
- **たりないデータ / observe**: select a tarinai or inspect compatible objects such as signboards.
|
||||
- **追加 / new**: calls a new tarinai into the field.
|
||||
- **つつく / poke**: pokes tarinai and can push balls.
|
||||
- **つまむ / pinch**: drags tarinai and some placed objects.
|
||||
- **コピー / copy**: copies a placed item and pastes a new instance with its settings.
|
||||
- **戻す / undo** and **進む / redo**: reverse or reapply placement/edit actions.
|
||||
- **削除 / delete**: removes placed tools, objects, and stains.
|
||||
- **範囲削除 / area delete**: removes many nearby objects or stains around the cursor.
|
||||
- **洗浄 / water hose**: drag to clean dirt, zunchi, corpses, and stains.
|
||||
|
||||
### Food
|
||||
|
||||
- **ずんだ餅 / sweet**: favored food; can cure some illness.
|
||||
- **へこ餅 / love mochi**: makes reproduction behavior more likely for a while.
|
||||
- **けんか餅 / fight mochi**: makes fighting behavior more likely for a while.
|
||||
- **草 / grass**: food and comfort source; can grow and be used for simple beds.
|
||||
- **水滴 / water**: drinkable water, also appears during rain and can be set in duplicators.
|
||||
- **ずんだ汁 / zunda juice**: clears item effects from a tarinai.
|
||||
- **複製機 / duplicator**: can be loaded with compatible food, medicine, grass, water, pushpins, or oshibyo and then produces or applies that content.
|
||||
- **ずんだ餅 / sweet**: favored food; can cure some illness.
|
||||
- **へこ餅 / love mochi**: makes reproduction behavior more likely for a while.
|
||||
- **けんか餅 / fight mochi**: makes fighting behavior more likely for a while.
|
||||
- **草 / grass**: food and comfort source; can grow and be used for simple beds.
|
||||
- **水滴 / water**: drinkable water, also appears during rain and can be set in duplicators.
|
||||
- **ずんだ汁 / zunda juice**: clears item effects from a tarinai.
|
||||
- **複製機 / duplicator**: can be loaded with compatible food, medicine, grass, water, pushpins, or oshibyo and then produces or applies that content.
|
||||
|
||||
### Medicine
|
||||
|
||||
- **ねむり薬 / sleep drug**: causes sleepiness or sleep disease; damage can cure the disease.
|
||||
- **下剤 / laxative**: causes repeated zunchi and reduces food benefit.
|
||||
- **プロテイン / protein**: permanent power mode; more damage dealt, lower disease chance.
|
||||
- **ンイテロプ / niteropu**: permanent weak mode; less damage dealt, higher disease chance.
|
||||
- **弾薬 / ammo**: temporary high-speed movement and strong collision impact.
|
||||
- **怪しい薬 / mystery drug**: randomly applies an eligible item effect or disease.
|
||||
- **水銀 / mercury**: increases lifespan multiplier on each use.
|
||||
- **巨大薬 / giant drug**: greatly increases size, attack, and speed, but deals ongoing damage.
|
||||
- **矮小薬 / dwarf drug**: greatly decreases size, attack, and speed, and also deals ongoing damage.
|
||||
- **ねむり薬 / sleep drug**: causes sleepiness or sleep disease; damage can cure the disease.
|
||||
- **下剤 / laxative**: causes repeated zunchi and reduces food benefit.
|
||||
- **プロテイン / protein**: permanent power mode; more damage dealt, lower disease chance.
|
||||
- **ンイテロプ / niteropu**: permanent weak mode; less damage dealt, higher disease chance.
|
||||
- **弾薬 / ammo**: temporary high-speed movement and strong collision impact.
|
||||
- **怪しい薬 / mystery drug**: randomly applies an eligible item effect or disease.
|
||||
- **水銀 / mercury**: increases lifespan multiplier on each use.
|
||||
- **巨大薬 / giant drug**: greatly increases size, attack, and speed, but deals ongoing damage.
|
||||
- **矮小薬 / dwarf drug**: greatly decreases size, attack, and speed, and also deals ongoing damage.
|
||||
|
||||
### Habitat
|
||||
|
||||
- **ずんち / zunchi**: tarinai waste; affects disease and can fertilize grass.
|
||||
- **ロボ掃除機 / 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.
|
||||
- **看板 / signboard**: editable text object; inspect with observe after placing.
|
||||
- **ずんち / zunchi**: tarinai waste; affects disease and can fertilize grass.
|
||||
- **ロボ掃除機 / 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.
|
||||
- **看板 / signboard**: editable text object; inspect with observe after placing.
|
||||
|
||||
### Physics
|
||||
|
||||
- **ヒモ / rope**: connect two targets with a flexible link.
|
||||
- **棒 / rod**: connect two targets with a rigid link.
|
||||
- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
|
||||
- **バウンス柵 / bounce fence**: fence that rebounds things that hit it.
|
||||
- **ゲート柵 / gate fence**: click to open or close.
|
||||
- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
|
||||
- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
|
||||
- **往復体 / reciprocator**: custom mechanical shape that moves back and forth; click to edit shape and speed.
|
||||
- **ヒモ / rope**: connect two targets with a flexible link.
|
||||
- **棒 / rod**: connect two targets with a rigid link.
|
||||
- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
|
||||
- **バウンス柵 / bounce fence**: fence that rebounds things that hit it.
|
||||
- **ゲート柵 / gate fence**: click to open or close.
|
||||
- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
|
||||
- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
|
||||
- **往復体 / reciprocator**: custom mechanical shape that moves back and forth; click to edit shape and speed.
|
||||
|
||||
### Danger
|
||||
|
||||
- **石 / stone**: heavy object; dangerous when dropped or moved into others.
|
||||
- **げんこつ / genkotsu**: large fist impact on the ground.
|
||||
- **爆竹 / firecracker**: creates an explosion.
|
||||
- **画鋲 / pushpin**: rolls when dropped; can lodge in tarinai and cause panic plus continuous damage.
|
||||
- **おしり鋲 / oshibyo**: blocks zunchi when lodged; pulling it out releases stored zunchi.
|
||||
- **射撃 / shoot**: attacks with a gun effect.
|
||||
- **アリの巣 / ant nest**: spawns ants that search for tarinai and carry them to the nest.
|
||||
- **石 / stone**: heavy object; dangerous when dropped or moved into others.
|
||||
- **げんこつ / genkotsu**: large fist impact on the ground.
|
||||
- **爆竹 / firecracker**: creates an explosion.
|
||||
- **画鋲 / pushpin**: rolls when dropped; can lodge in tarinai and cause panic plus continuous damage.
|
||||
- **おしり鋲 / oshibyo**: blocks zunchi when lodged; pulling it out releases stored zunchi.
|
||||
- **射撃 / shoot**: attacks with a gun effect.
|
||||
- **アリの巣 / ant nest**: spawns ants that search for tarinai and carry them to the nest.
|
||||
|
||||
## Tarinai Needs And Stats
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -3,9 +3,9 @@
|
|||
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.
|
||||
- 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.
|
||||
- The 死骸 target also covers blood traces. When たりない is enabled, nearby Tarinai panic while chased and are cleaned on contact.
|
||||
|
||||
## Read Strategy
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
|
||||
## Runtime Shape
|
||||
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.47`.
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.92`.
|
||||
- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots.
|
||||
- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases.
|
||||
- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`.
|
||||
|
|
@ -57,7 +57,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
- Inventory sanity: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All`
|
||||
|
||||
|
||||
## v39.15.47 UI media isolation / scroll repair
|
||||
## UI media isolation / scroll repair
|
||||
|
||||
- PC / smartphone UI override rules are separated into `@media (min-width: 981px)` and `@media (max-width: 980px)`.
|
||||
- PC family tree card is restored below the main game layout, not inside the right menu.
|
||||
|
|
@ -65,7 +65,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
- Auto-scroll does not open collapsed cards.
|
||||
|
||||
|
||||
## v39.15.47 event/family/oshibyo UI fixes
|
||||
## Event/family/oshibyo UI fixes
|
||||
|
||||
- Event UI hides direct player item placement records while keeping those records in the internal log.
|
||||
- Collapsed Event UI no longer leaves a large blank area from the log-card min-height.
|
||||
|
|
@ -73,14 +73,14 @@ 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 polish
|
||||
## 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.
|
||||
|
||||
|
||||
## v39.15.47 owned object panic / shoot effect
|
||||
## Owned object panic / shoot effect
|
||||
|
||||
- Tarinai panic when their own plushie or simple grass bed is destroyed.
|
||||
- The selected Tarinai data behavior text explicitly shows the owned-object panic reason.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.15.90",
|
||||
"version": "39.15.98",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -155,14 +155,5 @@
|
|||
"js/ui_family_render.js",
|
||||
"js/main.js",
|
||||
"js/debug_tools.js"
|
||||
],
|
||||
"assetGlobs": [
|
||||
"assets/ui/*.webp",
|
||||
"assets/objects/*.webp",
|
||||
"assets/sprites/*.webp",
|
||||
"assets/sounds/*.wav",
|
||||
"assets/sounds/*.mp3",
|
||||
"assets/objects/*.png"
|
||||
],
|
||||
"notes": "Boundary cleanup v38: freezer removed, washing expanded and continuous, laboratory ground added, and save schema synchronized."
|
||||
]
|
||||
}
|
||||
|
|
|
|||
33
css/base.css
33
css/base.css
|
|
@ -85,15 +85,17 @@ button {
|
|||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -121,16 +123,19 @@ h1 {
|
|||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn, .tool {
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,0.76);
|
||||
border-radius: 999px;
|
||||
padding: 9px 13px;
|
||||
padding: 8px 11px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(80,60,40,0.06);
|
||||
transition: transform 130ms ease, background 130ms ease, border-color 130ms ease;
|
||||
|
|
@ -448,15 +453,15 @@ h1 {
|
|||
.tool[data-tool="oshibyo"] { --tool-icon: url("../assets/ui/tool_oshibyo.webp"); }
|
||||
|
||||
/* Generated parameter item tool icons. */
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=15.20.16"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=15.20.16"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=15.20.16"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=15.20.16"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=15.20.16"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=15.20.16"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=15.20.16"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=15.20.16"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=15.20.16"); }
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=39.15.92"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=39.15.92"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=39.15.92"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=39.15.92"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=39.15.92"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=39.15.92"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=39.15.92"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=39.15.92"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=39.15.92"); }
|
||||
|
||||
|
||||
.signboard-editor {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
backdrop-filter: blur(1px);
|
||||
text-shadow: 0 1px 3px rgba(25, 18, 12, 0.55);
|
||||
pointer-events: none;
|
||||
white-space: normal;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
@ -643,6 +643,60 @@
|
|||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.visual-settings-dialog-panel {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
}
|
||||
.visual-setting-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid rgba(115, 91, 62, 0.16);
|
||||
}
|
||||
.visual-setting-row strong,
|
||||
.visual-setting-row span {
|
||||
display: block;
|
||||
}
|
||||
.visual-setting-row strong {
|
||||
color: #4f3928;
|
||||
font-size: 15px;
|
||||
}
|
||||
.visual-setting-row span {
|
||||
margin-top: 3px;
|
||||
color: #6d5a46;
|
||||
font-size: 12px;
|
||||
}
|
||||
.visual-setting-options {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(115, 91, 62, 0.18);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.50);
|
||||
}
|
||||
.visual-setting-options button {
|
||||
min-width: 46px;
|
||||
min-height: 32px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #5d4938;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
}
|
||||
.visual-setting-options button.active,
|
||||
.visual-setting-options button[aria-pressed="true"] {
|
||||
border-color: rgba(127, 94, 54, 0.28);
|
||||
background: #fff4d8;
|
||||
color: #3f3023;
|
||||
box-shadow: 0 2px 8px rgba(96, 70, 40, 0.12);
|
||||
}
|
||||
.visual-setting-options button:focus-visible {
|
||||
outline: 2px solid rgba(184, 128, 54, 0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.save-slot-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -650,6 +704,16 @@
|
|||
.save-slot-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.visual-setting-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.visual-setting-options {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.visual-setting-options button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ground-stat {
|
||||
|
|
@ -738,7 +802,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* v39.15.47: collapsed event card must shrink to its header.
|
||||
/* Collapsed event card must shrink to its header.
|
||||
layout.css gives .log-card a min-height, which left a large blank panel when folded. */
|
||||
.collapsible-card.collapsed { min-height: 0 !important; }
|
||||
.log-card.collapsed { min-height: 0 !important; height: auto !important; flex: 0 0 auto !important; flex-basis: auto !important; }
|
||||
|
|
|
|||
|
|
@ -362,18 +362,18 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
}
|
||||
|
||||
|
||||
/* v39.15.47: desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */
|
||||
/* Desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */
|
||||
@media (min-width: 981px) {
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 18px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.top-left {
|
||||
flex: 1 0 auto;
|
||||
min-width: max-content;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.brand,
|
||||
|
|
@ -386,19 +386,26 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
text-overflow: clip !important;
|
||||
}
|
||||
.top-actions {
|
||||
flex: 1 1 560px;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-actions .btn,
|
||||
.top-actions .sound-menu-btn,
|
||||
.room-link {
|
||||
min-height: 42px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
.room-link {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.panel-quick-tabs {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
|
|
@ -408,7 +415,7 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
}
|
||||
}
|
||||
|
||||
/* v39.15.47: PC / smartphone UI isolation. All override UI placement is scoped by width. */
|
||||
/* PC / smartphone UI isolation. All override UI placement is scoped by width. */
|
||||
@media (min-width: 981px) {
|
||||
html,
|
||||
body {
|
||||
|
|
@ -424,12 +431,12 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 18px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.top-left {
|
||||
flex: 1 0 auto;
|
||||
min-width: max-content;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.brand,
|
||||
|
|
@ -442,19 +449,26 @@ body.touch-mobile-ui .tool-tip-popover {
|
|||
text-overflow: clip !important;
|
||||
}
|
||||
.top-actions {
|
||||
flex: 1 1 560px;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-actions .btn,
|
||||
.top-actions .sound-menu-btn,
|
||||
.room-link {
|
||||
min-height: 42px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
.room-link {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.layout {
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ body::before {
|
|||
box-shadow: inset 0 0 0 1px rgba(247, 200, 64, 0.30), 0 3px 8px rgba(120, 78, 12, 0.09);
|
||||
}
|
||||
.lineage-family-tree .tree-node.champion::before {
|
||||
content: "♛";
|
||||
content: "\265B ";
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 4px;
|
||||
|
|
@ -488,3 +488,31 @@ body::before {
|
|||
.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
/* v39.15.92: keep personality tags inside the family-tree node bottom edge. */
|
||||
.lineage-family-tree .tree-node {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.lineage-family-tree .tree-node .family-copy {
|
||||
position: static !important;
|
||||
}
|
||||
.lineage-family-tree .tree-node .family-copy > .family-personality-tags {
|
||||
position: absolute !important;
|
||||
left: 7px !important;
|
||||
right: 7px !important;
|
||||
bottom: 5px !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
height: 26px !important;
|
||||
max-height: 26px !important;
|
||||
margin: 0 !important;
|
||||
grid-auto-rows: 12px !important;
|
||||
gap: 1px 3px !important;
|
||||
z-index: 3 !important;
|
||||
}
|
||||
.lineage-family-tree .tree-node .family-personality-tags em {
|
||||
height: 12px !important;
|
||||
line-height: 10px !important;
|
||||
font-size: 7px !important;
|
||||
}
|
||||
|
|
|
|||
342
index.html
342
index.html
|
|
@ -7,11 +7,11 @@
|
|||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.90" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.90" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.90" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.90" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.90" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.98" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.98" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.98" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.98" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.98" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
</div>
|
||||
<button id="creditsBtn" class="btn">クレジット</button>
|
||||
<button id="saveBtn" class="btn">セーブ</button>
|
||||
<button id="visualSettingsBtn" class="btn" type="button">表示設定</button>
|
||||
<button id="resetBtn" class="btn danger">リセット</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -102,8 +103,8 @@
|
|||
<div class="stat"><span>状態</span><strong id="statColonyMood">安定</strong></div>
|
||||
<div class="stat ground-stat" data-ground-toggle role="button" tabindex="0"><span>地面</span><button id="groundTypeBtn" class="ground-type-btn" type="button" data-ground-toggle aria-label="地面をきりかえる"><strong id="statGroundType">砂</strong></button></div>
|
||||
</div>
|
||||
<div class="temperature-control" aria-label="気温調整">
|
||||
<div class="temperature-control-head"><span>気温</span><strong id="temperatureValue">10℃</strong><label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label></div>
|
||||
<div class="temperature-control" aria-label="気温調整">
|
||||
<div class="temperature-control-head"><span>気温</span><strong id="temperatureValue">10℃</strong><label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label></div>
|
||||
<input id="temperatureSlider" class="temperature-slider" type="range" min="-100" max="120" step="1" value="10">
|
||||
</div>
|
||||
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects">物</button></div>
|
||||
|
|
@ -182,6 +183,39 @@
|
|||
|
||||
|
||||
|
||||
<div id="visualSettingsDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="visualSettingsDialogTitle">
|
||||
<div class="field-dialog-panel visual-settings-dialog-panel">
|
||||
<h2 id="visualSettingsDialogTitle">表示設定</h2>
|
||||
<p class="hint">重い表現を手動で調整します。個体数や物の上限は変えません。</p>
|
||||
<div class="visual-setting-row">
|
||||
<div><strong>エフェクト</strong><span>破片、雨、光の演出</span></div>
|
||||
<div class="visual-setting-options" role="group" aria-label="エフェクト">
|
||||
<button type="button" data-visual-setting="effects" data-visual-value="off">OFF</button>
|
||||
<button type="button" data-visual-setting="effects" data-visual-value="on">ON</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="visual-setting-row">
|
||||
<div><strong>影</strong><span>地面の落ち影とスプライトの影</span></div>
|
||||
<div class="visual-setting-options" role="group" aria-label="影">
|
||||
<button type="button" data-visual-setting="shadows" data-visual-value="off">OFF</button>
|
||||
<button type="button" data-visual-setting="shadows" data-visual-value="on">ON</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="visual-setting-row">
|
||||
<div><strong>描画</strong><span>装飾と解像度の設定</span></div>
|
||||
<div class="visual-setting-options" role="group" aria-label="描画">
|
||||
<button type="button" data-visual-setting="details" data-visual-value="low">低</button>
|
||||
<button type="button" data-visual-setting="details" data-visual-value="high">高</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="visualSettingsCloseBtn" class="btn" type="button">閉じる</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="creditsDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="creditsDialogTitle">
|
||||
<div class="field-dialog-panel credits-dialog-panel">
|
||||
<h2 id="creditsDialogTitle">クレジット</h2>
|
||||
|
|
@ -218,152 +252,152 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.15.90" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.90" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.90" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.90" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.90" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.90" defer></script>
|
||||
<script src="js/data.js?v=39.15.90" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.90" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.90" defer></script>
|
||||
<script src="js/math.js?v=39.15.90" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.90" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.15.90" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/assets.js?v=39.15.90" defer></script>
|
||||
<script src="js/audio.js?v=39.15.90" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.90" defer></script>
|
||||
<script src="js/render.js?v=39.15.90" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.90" defer></script>
|
||||
<script src="js/structures.js?v=39.15.90" defer></script>
|
||||
<script src="js/items.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.90" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.90" defer></script>
|
||||
<script src="js/ants.js?v=39.15.90" defer></script>
|
||||
<script src="js/health.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.90" defer></script>
|
||||
<script src="js/world.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.90" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.90" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.90" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.90" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.90" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.90" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.90" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.90" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.90" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.90" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.90" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.90" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.90" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.90" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.90" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.90" defer></script>
|
||||
<script src="js/main.js?v=39.15.90" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.90" defer></script>
|
||||
<script src="js/version.js?v=39.15.98" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.98" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.98" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.98" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.98" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.98" defer></script>
|
||||
<script src="js/data.js?v=39.15.98" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.98" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.98" defer></script>
|
||||
<script src="js/math.js?v=39.15.98" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.98" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.15.98" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/assets.js?v=39.15.98" defer></script>
|
||||
<script src="js/audio.js?v=39.15.98" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.98" defer></script>
|
||||
<script src="js/render.js?v=39.15.98" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.98" defer></script>
|
||||
<script src="js/structures.js?v=39.15.98" defer></script>
|
||||
<script src="js/items.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.98" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.98" defer></script>
|
||||
<script src="js/ants.js?v=39.15.98" defer></script>
|
||||
<script src="js/health.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.98" defer></script>
|
||||
<script src="js/world.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.98" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.98" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.98" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.98" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.98" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.98" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.98" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.98" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.98" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.98" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.98" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.98" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.98" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.98" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.98" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.98" defer></script>
|
||||
<script src="js/main.js?v=39.15.98" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.98" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
92
js/ants.js
92
js/ants.js
|
|
@ -82,6 +82,81 @@ class AntActor {
|
|||
this.carryLogAt = opts.carryLogAt || -999;
|
||||
this.foundingAttempts = opts.foundingAttempts || 0;
|
||||
this.dead = Boolean(opts.dead);
|
||||
this.burning = Boolean(opts.burning);
|
||||
this.burnTimer = Number.isFinite(opts.burnTimer) ? opts.burnTimer : 0;
|
||||
this.burnTurnTimer = 0;
|
||||
}
|
||||
|
||||
igniteFire(source = null) {
|
||||
if (this.dead) return false;
|
||||
this.burning = true;
|
||||
this.burnTimer = Math.max(this.burnTimer || 0, 4.2 + rand(0, 2.4));
|
||||
this.state = "panic";
|
||||
this.targetId = "";
|
||||
this.targetToken = 0;
|
||||
this.targetRef = null;
|
||||
this.burnWanderAngle = rand(0, Math.PI * 2);
|
||||
this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.0);
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(10, this.r * 2.8), life: 0.18, color: "rgba(255,110,36,0.56)" }));
|
||||
this.world?.emit?.("fire:ignite-ant", { ant: this, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
extinguishFire(reason = "") {
|
||||
if (!this.burning && !(this.burnTimer > 0)) return false;
|
||||
this.burning = false;
|
||||
this.burnTimer = 0;
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(9, this.r * 2.2), life: 0.16, color: "rgba(108,192,236,0.58)" }));
|
||||
return true;
|
||||
}
|
||||
|
||||
updateBurning(dt) {
|
||||
if (!this.burning && !(this.burnTimer > 0)) return false;
|
||||
this.burning = true;
|
||||
this.burnTimer = Math.max(0, Number(this.burnTimer || 0) - dt);
|
||||
this.hp = Math.max(0, Number(this.hp || 0) - dt * (this.kind === "queen" ? 11 : 7.5));
|
||||
this.hpBarTimer = Math.max(this.hpBarTimer || 0, 0.55);
|
||||
const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.r || 5, 7));
|
||||
if (water) {
|
||||
globalThis.TarinaiItemDynamicToolSystem?.consumeWaterForFire?.(this.world, water, 5);
|
||||
this.extinguishFire("water");
|
||||
return true;
|
||||
}
|
||||
const mul = globalThis.TarinaiItemDynamicToolSystem?.fireTempMultiplier?.(this.world, this.x, this.y, this) || 1;
|
||||
if (deterministicChance(this.world, "ant-fire-extinguish", dt * 0.020 * mul, this)) {
|
||||
this.extinguishFire("natural");
|
||||
return true;
|
||||
}
|
||||
this.burnTurnTimer = Math.max(0, Number(this.burnTurnTimer || 0) - dt);
|
||||
if (this.burnTurnTimer <= 0) {
|
||||
this.burnWanderAngle = deterministicAngle(this.world, "ant-burn-angle", this, Math.floor((this.world?.time || 0) * 6));
|
||||
this.burnTurnTimer = deterministicRange(this.world, "ant-burn-turn", 0.08, 0.28, this);
|
||||
} else {
|
||||
this.burnWanderAngle += deterministicRange(this.world, "ant-burn-jitter", -9.0, 9.0, this) * dt;
|
||||
}
|
||||
const speed = this.kind === "queen" ? ANT_QUEEN_SPEED * 3.0 : ANT_WORKER_RETURN_SPEED * 3.3;
|
||||
this.vx = Math.cos(this.burnWanderAngle || 0) * speed;
|
||||
this.vy = Math.sin(this.burnWanderAngle || 0) * speed;
|
||||
this.x = clamp(this.x + this.vx * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
|
||||
this.y = clamp(this.y + this.vy * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
|
||||
for (const viewer of [...(this.world?.nearbyTarinai?.(this.x, this.y, 170, true) || [])]) {
|
||||
if (!viewer || viewer.dead) continue;
|
||||
globalThis.TarinaiItemDynamicToolSystem?.panicTarinaiFromFire?.(viewer, this, this.world);
|
||||
}
|
||||
const contactRadius = Math.max(10, (this.r || 5) * 2.1);
|
||||
for (const t of [...(this.world?.nearbyTarinai?.(this.x, this.y, contactRadius + 26, true) || [])]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (distXY(this.x, this.y, t.x, t.y) <= contactRadius + Math.max(t.radius || 18, 12) * 0.7) t.igniteFire?.(this, false);
|
||||
}
|
||||
for (const it of [...(this.world?.nearbyItems?.(this.x, this.y, contactRadius + 28, true) || [])]) {
|
||||
if (!it || it.dead || it.type !== "grass") continue;
|
||||
if (distXY(this.x, this.y, it.x, it.y) <= contactRadius + Math.max(it.r || 16, 12)) globalThis.TarinaiItemDynamicToolSystem?.igniteGrassByFire?.(it, this, this.world, dt);
|
||||
}
|
||||
if (deterministicChance(this.world, "ant-fire-effect", dt * 2.0, this)) {
|
||||
this.world?.effects?.push(new Effect("fight", this.x, this.y - this.r, { vx: rand(-6, 6), vy: rand(-22, -8), size: Math.max(5, this.r * 1.8), life: 0.28, color: "rgba(255,118,35,0.72)" }));
|
||||
}
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("timeout");
|
||||
return true;
|
||||
}
|
||||
|
||||
homeNest() {
|
||||
|
|
@ -180,7 +255,10 @@ class AntActor {
|
|||
if (this.dead) return;
|
||||
const hpBefore = this.hp;
|
||||
this.age += dt;
|
||||
if (this.kind === "queen") this.updateQueen(dt);
|
||||
if (this.burning || (this.burnTimer || 0) > 0.02) {
|
||||
this.updateBurning(dt);
|
||||
if (this.hp <= 0) this.dieAsCorpse(this.homeNest?.() || null);
|
||||
} else if (this.kind === "queen") this.updateQueen(dt);
|
||||
else this.updateWorker(dt);
|
||||
if (Number.isFinite(hpBefore) && this.hp < hpBefore - 0.01) this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.25);
|
||||
this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt);
|
||||
|
|
@ -444,6 +522,18 @@ class AntActor {
|
|||
ctx.arc(this.r * 1.15, -this.r * 0.05, this.r * 0.46, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
if (this.burning || (this.burnTimer || 0) > 0.02) {
|
||||
ctx.fillStyle = "rgba(255,112,34,0.82)";
|
||||
ctx.strokeStyle = "rgba(255,226,92,0.70)";
|
||||
ctx.lineWidth = Math.max(0.8, this.r * 0.16);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a = this.seed + this.age * 8 + i * 2.1;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(Math.cos(a) * this.r * 0.34, -this.r * (0.8 + i * 0.32), this.r * (0.45 - i * 0.05), this.r * (0.75 - i * 0.08), a * 0.25, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
this.drawHealthBar(ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,8 +280,8 @@ const audio = {
|
|||
},
|
||||
bubble(text) {
|
||||
const s = String(text || "");
|
||||
if (s.includes("ぶるぶる")) return this.playSample("voice_temp_cold", 1.20, "voice") || this.play("voice_temp_cold", { category: "voice", minGap: 1.20 });
|
||||
if (s.includes("あちゅい")) return this.playSample("voice_temp_hot", 1.20, "voice") || this.play("voice_temp_hot", { category: "voice", minGap: 1.20 });
|
||||
if (s.includes("\u3076\u308B\u3076\u308B")) return this.playSample("voice_temp_cold", 1.20, "voice") || this.play("voice_temp_cold", { category: "voice", minGap: 1.20 });
|
||||
if (s.includes("\u3042\u3061\u3085\u3044")) return this.playSample("voice_temp_hot", 1.20, "voice") || this.play("voice_temp_hot", { category: "voice", minGap: 1.20 });
|
||||
if (s.includes("\u3082\u3050")) return this.playSample("sfx_eat", 0.20, "voice") || this.play("sfx_eat", { category: "voice", minGap: 0.20 });
|
||||
if (s.includes("\u3076\u308a")) return this.playSample("voice_poop", 0.28, "voice") || this.play("sfx_zunchi", { category: "ops", minGap: 0.28 });
|
||||
if (s.includes("\u306f\u3063")) return this.playSample("voice_stress", 0.80, "voice") || this.play("sfx_damage_soft", { category: "voice", minGap: 0.80 });
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@
|
|||
}
|
||||
const radiusFor = typeof global.itemRadiusFor === "function" ? global.itemRadiusFor : ((type, fallback = 12) => fallback);
|
||||
if (item.type === "duplicator") {
|
||||
// 保存済みデータに古い r=34 が残っていても、配置判定は外観に近い半径で扱う。
|
||||
// \u4FDD\u5B58\u6E08\u307F\u30C7\u30FC\u30BF\u306B\u53E4\u3044 r=34 \u304C\u6B8B\u3063\u3066\u3044\u3066\u3082\u3001\u914D\u7F6E\u5224\u5B9A\u306F\u5916\u89B3\u306B\u8FD1\u3044\u534A\u5F84\u3067\u6271\u3046\u3002
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@
|
|||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(18, ball.r * 1.05), life: 0.20, color: "rgba(196,65,72,0.46)" }));
|
||||
if (worldRef.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) worldRef.log(`${t.name}にボールから鋲が移った。`, "accident", { participants: [t] });
|
||||
if (worldRef.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) worldRef.log(`${t.name}\u306B\u30DC\u30FC\u30EB\u304B\u3089\u92F2\u304C\u79FB\u3063\u305F\u3002`, "accident", { participants: [t] });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
}
|
||||
function evaluate(worldRef, force = false) {
|
||||
const now = worldRef?.time || 0;
|
||||
if (!worldRef) return { id: "relaxed", label: "安定", effects: { personality: {} } };
|
||||
if (!worldRef) return { id: "relaxed", label: "\u5B89\u5B9A", effects: { personality: {} } };
|
||||
if (!force && worldRef.colonyMood && now < (worldRef.nextColonyMoodEvalAt || 0)) return worldRef.colonyMood;
|
||||
const metrics = collect(worldRef);
|
||||
const id = choose(worldRef, metrics);
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@
|
|||
return found || null;
|
||||
}
|
||||
let found = worldRef.itemById?.(endpoint.id) || null;
|
||||
if (!found && endpoint.id != null) found = (worldRef.items || []).find(it => it && !it.dead && it.id === endpoint.id);
|
||||
if (found && (found.dead || (Number.isFinite(Number(found.amount)) && Number(found.amount) <= 0))) found = null;
|
||||
if (!found && endpoint.id != null) found = (worldRef.items || []).find(it => it && !it.dead && it.id === endpoint.id && !(Number.isFinite(Number(it.amount)) && Number(it.amount) <= 0));
|
||||
if (!found && allowFuzzy && Number.isFinite(endpoint.x) && Number.isFinite(endpoint.y)) {
|
||||
const source = worldRef.nearbyItems?.(endpoint.x, endpoint.y, 120, true) || worldRef.items || [];
|
||||
found = source.find(it => it && !it.dead && it.type === endpoint.type && distXY(it.x, it.y, endpoint.x, endpoint.y) <= Math.max(44, (it.r || 20) * 1.8));
|
||||
|
|
@ -313,13 +314,9 @@
|
|||
|
||||
|
||||
function endpointCanRemainAnchored(endpoint, point) {
|
||||
if (!endpoint || !point) return false;
|
||||
if (point.obj && !point.obj.dead) return true;
|
||||
// When a linked Tarinai dies, its live id is released before link cleanup.
|
||||
// Keep the affected endpoint as a frozen world-space anchor instead of
|
||||
// deleting the rope/rod item; otherwise unrelated links can be swept up by
|
||||
// the lifecycle pass that removes zero-amount link items.
|
||||
return endpoint.kind === "tarinai" && Number.isFinite(Number(point.x)) && Number.isFinite(Number(point.y));
|
||||
if (!endpoint || !point || !point.obj || point.obj.dead) return false;
|
||||
if (Number.isFinite(Number(point.obj.amount)) && Number(point.obj.amount) <= 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function endpointMass(obj) {
|
||||
|
|
@ -486,7 +483,7 @@
|
|||
}
|
||||
|
||||
function ropeLodSettings(worldRef, length) {
|
||||
const tier = global.TarinaiPerf.renderQualityTier() || "high";
|
||||
const tier = global.TarinaiPerf.simulationQualityTier?.() || "high";
|
||||
const ropeCount = Number(worldRef?.itemCounts?.rope || 0) || 0;
|
||||
let spacing = 34;
|
||||
let maxParticles = 24;
|
||||
|
|
@ -861,10 +858,7 @@
|
|||
function endpointRuntimeStamp(endpoint, worldRef) {
|
||||
const obj = resolveEndpointObject(endpoint, worldRef);
|
||||
const q = (v, scale = 10) => Math.round((Number(v) || 0) * scale);
|
||||
if (!obj || obj.dead) {
|
||||
if (endpoint?.kind === "tarinai" && Number.isFinite(Number(endpoint.x)) && Number.isFinite(Number(endpoint.y))) {
|
||||
return { stamp: `tarinai-anchor:${endpoint.id || "?"}:${q(endpoint.x)}:${q(endpoint.y)}`, missing: false, obj: null };
|
||||
}
|
||||
if (!obj || obj.dead || (Number.isFinite(Number(obj.amount)) && Number(obj.amount) <= 0)) {
|
||||
return { stamp: "missing", missing: true, obj: null };
|
||||
}
|
||||
let stamp = `${endpoint?.kind || "item"}:${obj.id || "?"}:${obj.type || (obj.name ? "tarinai" : "entity")}:${q(obj.x)}:${q(obj.y)}`;
|
||||
|
|
|
|||
|
|
@ -140,13 +140,14 @@
|
|||
const topBehaviors = topMap(behaviorCounts, 5);
|
||||
const perf = diag.perf || window.TarinaiPerf.snapshot() || {};
|
||||
const metrics = perf.metrics || {};
|
||||
const visual = perf.visual || {};
|
||||
const currentDpr = (typeof uiCache !== "undefined" && uiCache) ? uiCache.canvasDpr : "?";
|
||||
const creature = diag.creatures || {};
|
||||
const scheduler = diag.scheduler || {};
|
||||
const terrain = diag.terrain || {};
|
||||
const lines = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"} debug profiler:${perf.enabled ? "on" : "off"}${Date.now() < copiedMessageUntil ? " copied" : " copy:button/select"}`,
|
||||
`fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms quality ${perf.tier || "?"} dprScale:${perf.dprScale || 1}`,
|
||||
`fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms visual effects:${visual.effects || "?"} shadows:${visual.shadows || "?"} details:${visual.details || perf.tier || "?"} dprScale:${perf.dprScale || 1}`,
|
||||
`live ${diag.runtime?.liveTarinai ?? (w?.tarinai || []).length} items ${(w?.items || []).length} ants ${(w?.ants || []).length} effects ${(w?.effects || []).length}`,
|
||||
`visible layered:${metrics["render.visibleLayered"] ?? "?"} back:${metrics["render.visibleBackItems"] ?? "?"} effects:${metrics["render.visibleEffects"] ?? "?"}`,
|
||||
`creatures full:${creature.full ?? 0} realtime:${creature.realtime ?? 0} smooth:${creature.smooth ?? 0} skipped:${creature.skipped ?? 0} selected:${creature.selected ?? 0} urgent:${creature.urgent ?? 0} visible:${creature.visible ?? 0} sleepPhys:${creature.sleepPhysical ?? 0} passive:${creature.passivePhysical ?? 0}`,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ const HEALTH = (() => {
|
|||
antEaten: "\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f",
|
||||
giantDrug: "\u5de8\u5927\u85ac",
|
||||
dwarfDrug: "\u77ee\u5c0f\u85ac",
|
||||
freeze: "凍結",
|
||||
heatstroke: "熱中症",
|
||||
freeze: "\u51CD\u7D50",
|
||||
heatstroke: "\u71B1\u4E2D\u75C7",
|
||||
});
|
||||
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
||||
|
||||
|
|
@ -36,8 +36,8 @@ const HEALTH = (() => {
|
|||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/凍結|低温|寒さ|cold|freeze|freezing/i.test(text)) return CAUSES.freeze;
|
||||
if (/熱中症|高温|暑さ|heat|hot/i.test(text)) return CAUSES.heatstroke;
|
||||
if (/\u51CD\u7D50|\u4F4E\u6E29|\u5BD2\u3055|cold|freeze|freezing/i.test(text)) return CAUSES.freeze;
|
||||
if (/\u71B1\u4E2D\u75C7|\u9AD8\u6E29|\u6691\u3055|heat|hot/i.test(text)) return CAUSES.heatstroke;
|
||||
if (/\u5c04\u6483|shoot/.test(text)) return CAUSES.shooting;
|
||||
if (/\u77f3\u306b\u6f70\u3055\u308c\u305f|\u77f3.*\u6f70|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush;
|
||||
const collisionWith = text.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/);
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@
|
|||
item.vy = clamp(baseVy * 0.45 + Math.max(12, ny * 18), -60, 110);
|
||||
item.spinVelocity = clamp((item.spinVelocity || 0) * 0.35 + (Math.sign(nx) || 1) * 0.9, -2.2, 2.2);
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, (item.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" }));
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}の画鋲が抜け落ちた。`, "observe", { participants: [t] });
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306E\u753B\u92F2\u304C\u629C\u3051\u843D\u3061\u305F\u3002`, "observe", { participants: [t] });
|
||||
}
|
||||
if (wasOshibyo && storedZunchi > 0 && worldRef?.spawnBurstZunchi) {
|
||||
worldRef.spawnBurstZunchi(t || item, storedZunchi, item);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
if (!item || item.dead) return false;
|
||||
switch (item.type) {
|
||||
case "firecracker":
|
||||
case "flame_firecracker":
|
||||
case "fire":
|
||||
case "genkotsu":
|
||||
case "rotator":
|
||||
case "poison_block":
|
||||
|
|
|
|||
|
|
@ -14,6 +14,228 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const FIRE_BURNING_TEXT = "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
const FIRE_SEEN_TEXT = "\u708e\u3092\u898b\u3066\u9a5a\u3044\u3066\u3044\u308b\u3002";
|
||||
|
||||
function fireTempMultiplier(worldRef, x, y, entity = null) {
|
||||
const weather = String(worldRef?.weather || "");
|
||||
const temp = Number(worldRef?.temperatureAt?.(x, y, entity) ?? worldRef?.feltTemperatureFor?.(entity) ?? worldRef?.temperature ?? global.CONFIG?.standardTemperature ?? 15);
|
||||
let mul = 1;
|
||||
if (weather === "light_rain") mul *= 2;
|
||||
if (Number.isFinite(temp) && temp <= 10) mul *= 2;
|
||||
return mul;
|
||||
}
|
||||
|
||||
function pushFireEffect(worldRef, x, y, size = 12, seed = null) {
|
||||
if (!worldRef?.effects || typeof Effect !== "function") return;
|
||||
const a = global.deterministicAngle ? global.deterministicAngle(worldRef, "fire-effect-angle", seed || x, y, worldRef.time || 0) : Math.random() * Math.PI * 2;
|
||||
const speed = global.deterministicRange ? global.deterministicRange(worldRef, "fire-effect-speed", 4, 28, seed || x, y) : (4 + Math.random() * 24);
|
||||
worldRef.effects.push(new Effect("fight", x, y, {
|
||||
vx: Math.cos(a) * speed,
|
||||
vy: Math.sin(a) * speed - Math.max(5, speed * 0.45),
|
||||
size,
|
||||
life: global.deterministicRange ? global.deterministicRange(worldRef, "fire-effect-life", 0.22, 0.62, seed || x, y) : 0.38,
|
||||
color: "rgba(255,118,35,0.76)",
|
||||
}));
|
||||
}
|
||||
|
||||
function contactWaterDrop(worldRef, x, y, radius = 16) {
|
||||
if (!worldRef) return null;
|
||||
const list = typeof worldRef.nearbyItems === "function" ? [...(worldRef.nearbyItems(x, y, radius + 28, true) || [])] : (worldRef.items || []);
|
||||
for (const it of list) {
|
||||
if (!it || it.dead || it.type !== "water") continue;
|
||||
const rr = Math.max(radius, 10) + Math.max(it.r || 10, 10);
|
||||
if (distXY(x, y, it.x, it.y) <= rr) return it;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function consumeWaterForFire(worldRef, water, amount = 8) {
|
||||
if (!water) return false;
|
||||
water.amount = Math.max(0, Number(water.amount || 0) - amount);
|
||||
worldRef?.effects?.push(new Effect("ring", water.x, water.y, { size: Math.max(10, (water.r || 10) * 1.4), life: 0.18, color: "rgba(108,192,236,0.58)" }));
|
||||
worldRef?.markItemBucketsDirty?.("fire-water");
|
||||
worldRef?.markSpatialDirty?.("fire-water");
|
||||
return true;
|
||||
}
|
||||
|
||||
function igniteTarinaiByFire(t, source = null, worldRef = null, force = false) {
|
||||
if (!t || t.dead) return false;
|
||||
if (typeof t.igniteFire === "function") return t.igniteFire(source, force);
|
||||
t.burning = true;
|
||||
t.burnTimer = Math.max(t.burnTimer || 0, 5.0);
|
||||
t.thought = FIRE_BURNING_TEXT;
|
||||
t.lastPanicCause = "burning";
|
||||
t.lastPanicDetail = FIRE_BURNING_TEXT;
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 1.5);
|
||||
t.sleeping = false;
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(source || null, true) : null, reason: FIRE_BURNING_TEXT, wake: true });
|
||||
worldRef?.emit?.("fire:ignite", { tarinai: t, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
function panicTarinaiFromFire(t, source = null, worldRef = null) {
|
||||
if (!t || t.dead || t.burning || (t.burnTimer || 0) > 0.02) return false;
|
||||
const now = Number(worldRef?.time || t.world?.time || 0) || 0;
|
||||
if (now < Number(t.firePanicCooldownUntil || 0)) return false;
|
||||
t.firePanicCooldownUntil = now + 0.85;
|
||||
t.firePanicTimer = Math.max(t.firePanicTimer || 0, 1.4);
|
||||
t.lastPanicCause = "fire_seen";
|
||||
t.lastPanicDetail = FIRE_SEEN_TEXT;
|
||||
t.thought = FIRE_SEEN_TEXT;
|
||||
if (typeof t.enterPanic === "function") t.enterPanic({ threat: source, target: source, reason: FIRE_SEEN_TEXT, fear: 1.25, wake: true, cause: "fire_seen", surpriseTimer: 0.55, awakeLockTimer: 0.35 });
|
||||
else { t.fearTimer = Math.max(t.fearTimer || 0, 1.25); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(source || null, true) : null, reason: FIRE_SEEN_TEXT, wake: true }); }
|
||||
return true;
|
||||
}
|
||||
|
||||
function igniteAntByFire(ant, source = null, worldRef = null) {
|
||||
if (!ant || ant.dead) return false;
|
||||
if (typeof ant.igniteFire === "function") return ant.igniteFire(source);
|
||||
ant.burning = true;
|
||||
ant.burnTimer = Math.max(ant.burnTimer || 0, 5.0);
|
||||
ant.state = "panic";
|
||||
ant.targetId = "";
|
||||
ant.targetRef = null;
|
||||
worldRef?.emit?.("fire:ignite-ant", { ant, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
function igniteGrassByFire(grass, source = null, worldRef = null, dt = 0.016) {
|
||||
if (!grass || grass.dead || grass.type !== "grass") return false;
|
||||
grass.burning = true;
|
||||
grass.burnTimer = Math.max(grass.burnTimer || 0, 2.8);
|
||||
grass.health = clamp((grass.health ?? 1) - dt * 0.30, 0, 1);
|
||||
grass.amount = Math.max(0, Number(grass.amount || 0) - dt * 18);
|
||||
if (grass.health <= 0.02 || grass.amount <= 0.5) grass.amount = 0;
|
||||
if (worldRef && (worldRef.time || 0) > Number(grass.nextFireSpreadAt || -Infinity)) {
|
||||
grass.nextFireSpreadAt = (worldRef.time || 0) + 1.6;
|
||||
if (typeof worldRef.spawnFireAt === "function") {
|
||||
const a = global.deterministicAngle ? global.deterministicAngle(worldRef, "grass-fire-spread", grass, source) : Math.random() * Math.PI * 2;
|
||||
worldRef.spawnFireAt(grass.x + Math.cos(a) * (grass.r || 14) * 0.55, grass.y + Math.sin(a) * (grass.r || 14) * 0.35, { amount: 60, radius: 12, life: 4.2, source: grass });
|
||||
}
|
||||
}
|
||||
worldRef?.markTerrainDirtyAt?.(grass.x, grass.y, Math.max(grass.r || 17, 26), "grass-fire");
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateFireContacts(item, dt, worldRef) {
|
||||
if (!item || !worldRef) return false;
|
||||
const r = Math.max(14, Number(item.r || 0) || 14);
|
||||
const spreadRadius = r + 16;
|
||||
let changed = false;
|
||||
for (const t of [...(worldRef.nearbyTarinai?.(item.x, item.y, spreadRadius + 32, true) || [])]) {
|
||||
if (!t || t.dead) continue;
|
||||
const d = distXY(item.x, item.y, t.x, t.y);
|
||||
if (d <= spreadRadius + Math.max(t.radius || 18, 12) * 0.72) changed = igniteTarinaiByFire(t, item, worldRef, false) || changed;
|
||||
}
|
||||
for (const ant of [...(worldRef.nearbyAnts?.(item.x, item.y, spreadRadius + 20, true) || [])]) {
|
||||
if (!ant || ant.dead) continue;
|
||||
const d = distXY(item.x, item.y, ant.x, ant.y);
|
||||
if (d <= spreadRadius + Math.max(ant.r || 5, 4)) changed = igniteAntByFire(ant, item, worldRef) || changed;
|
||||
}
|
||||
const nearby = typeof worldRef.nearbyItems === "function" ? [...(worldRef.nearbyItems(item.x, item.y, spreadRadius + 38, true) || [])] : (worldRef.items || []);
|
||||
for (const it of nearby) {
|
||||
if (!it || it.dead || it === item) continue;
|
||||
if (it.type === "grass" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 16, 12)) changed = igniteGrassByFire(it, item, worldRef, dt) || changed;
|
||||
if (it.type === "water" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 10, 8)) {
|
||||
consumeWaterForFire(worldRef, it, Math.max(6, r * 0.45));
|
||||
item.amount = 0;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function panicFromNearbyFire(item, worldRef) {
|
||||
if (!item || !worldRef) return;
|
||||
const r = Math.max(170, (item.r || 14) + 170);
|
||||
for (const t of [...(worldRef.nearbyTarinai?.(item.x, item.y, r, true) || [])]) {
|
||||
if (!t || t.dead) continue;
|
||||
panicTarinaiFromFire(t, item, worldRef);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFire(item, dt, worldRef) {
|
||||
if (!item || item.dead || !worldRef) return false;
|
||||
dt = Math.max(0, Math.min(0.08, Number(dt || 0) || 0));
|
||||
item.amount = Math.max(0, Number(item.amount || 0) - dt * 4.2);
|
||||
item.flameLife = Math.max(0, Number(item.flameLife ?? 8.0) - dt);
|
||||
item.flameSpreadCooldown = Math.max(0, Number(item.flameSpreadCooldown || 0) - dt);
|
||||
item.flamePanicCooldown = Math.max(0, Number(item.flamePanicCooldown || 0) - dt);
|
||||
const water = contactWaterDrop(worldRef, item.x, item.y, Math.max(item.r || 14, 12));
|
||||
if (water) {
|
||||
consumeWaterForFire(worldRef, water, Math.max(8, (item.r || 14) * 0.6));
|
||||
item.amount = 0;
|
||||
return true;
|
||||
}
|
||||
const chance = dt * 0.012 * fireTempMultiplier(worldRef, item.x, item.y, item);
|
||||
if (global.deterministicChance ? global.deterministicChance(worldRef, "fire-random-extinguish", chance, item) : Math.random() < chance) {
|
||||
item.amount = 0;
|
||||
return true;
|
||||
}
|
||||
if (item.flameLife <= 0) item.amount = Math.min(item.amount, 1);
|
||||
if (item.flameSpreadCooldown <= 0) {
|
||||
updateFireContacts(item, dt, worldRef);
|
||||
item.flameSpreadCooldown = 0.12;
|
||||
}
|
||||
if (item.flamePanicCooldown <= 0) {
|
||||
panicFromNearbyFire(item, worldRef);
|
||||
item.flamePanicCooldown = 0.32;
|
||||
}
|
||||
if (global.deterministicChance ? global.deterministicChance(worldRef, "fire-effect", dt * 2.4, item) : Math.random() < dt * 2.4) pushFireEffect(worldRef, item.x, item.y - (item.r || 14) * 0.2, Math.max(8, (item.r || 14) * 0.9), item);
|
||||
return true;
|
||||
}
|
||||
|
||||
function spawnFireAt(worldRef, x, y, opts = {}) {
|
||||
if (!worldRef || typeof Item !== "function") return null;
|
||||
const fire = new Item("fire", x, y);
|
||||
const scale = Math.max(0.45, Math.min(2.0, Number(opts.scale || 1) || 1));
|
||||
fire.r = Math.max(9, Number(opts.radius || fire.r || 14) * scale);
|
||||
fire.amount = Math.max(12, Number(opts.amount || fire.amount || 100) * scale);
|
||||
fire.flameLife = Math.max(1.2, Number(opts.life || fire.flameLife || 7.5));
|
||||
fire.sourceFireId = opts.source?.id || "";
|
||||
fire.vx = Number(opts.vx || 0) || 0;
|
||||
fire.vy = Number(opts.vy || 0) || 0;
|
||||
worldRef.addItem?.(fire, "spawn-fire", { terrain: false });
|
||||
worldRef.drawListDirty = true;
|
||||
return fire;
|
||||
}
|
||||
|
||||
function scatterFireFrom(item, worldRef) {
|
||||
if (!item || !worldRef) return false;
|
||||
const count = Math.max(7, Math.round(10 * Math.max(0.75, Math.min(1.7, (item.r || 15) / 15))));
|
||||
for (let i = 0; i < count; i++) {
|
||||
const a = (Math.PI * 2 * i / count) + (global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-angle", -0.20, 0.20, item, i) : (Math.random() - 0.5) * 0.4);
|
||||
const d = global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-radius", 28, 118, item, i) : (28 + Math.random() * 90);
|
||||
const fire = spawnFireAt(worldRef, item.x + Math.cos(a) * d, item.y + Math.sin(a) * d, {
|
||||
radius: global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-fire-radius", 10, 18, item, i) : (10 + Math.random() * 8),
|
||||
amount: global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-fire-amount", 55, 120, item, i) : (55 + Math.random() * 65),
|
||||
life: global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-fire-life", 4.0, 8.5, item, i) : (4 + Math.random() * 4.5),
|
||||
source: item,
|
||||
});
|
||||
if (fire) {
|
||||
fire.vx = Math.cos(a) * (global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-vx", 10, 42, item, i) : 24);
|
||||
fire.vy = Math.sin(a) * (global.deterministicRange ? global.deterministicRange(worldRef, "flame-cracker-vy", 10, 42, item, i) : 24);
|
||||
}
|
||||
pushFireEffect(worldRef, item.x + Math.cos(a) * Math.min(d, 28), item.y + Math.sin(a) * Math.min(d, 28), 14, item);
|
||||
}
|
||||
worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(42, (item.r || 15) * 3.0), life: 0.36, color: "rgba(255,126,54,0.58)" }));
|
||||
item.amount = 0;
|
||||
worldRef.markItemBucketsDirty?.("flame-firecracker");
|
||||
worldRef.markSpatialDirty?.("flame-firecracker");
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateFlameFirecracker(item, dt, worldRef) {
|
||||
item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt);
|
||||
if (item.fuseTimer <= 0) {
|
||||
if (worldRef) scatterFireFrom(item, worldRef);
|
||||
else item.amount = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateGenkotsu(item, dt) {
|
||||
item.impactFlash = Math.max(0, (item.impactFlash || 0) - dt);
|
||||
if (item.dropImpactDone) {
|
||||
|
|
@ -380,6 +602,8 @@
|
|||
function update(item, dt, worldRef) {
|
||||
if (!item || item.dead) return false;
|
||||
if (item.type === "firecracker") return updateFirecracker(item, dt, worldRef);
|
||||
if (item.type === "flame_firecracker") return updateFlameFirecracker(item, dt, worldRef);
|
||||
if (item.type === "fire") return updateFire(item, dt, worldRef);
|
||||
if (item.type === "genkotsu") return updateGenkotsu(item, dt, worldRef);
|
||||
if (item.type === "rotator") return updateRotator(item, dt, worldRef);
|
||||
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
|
||||
|
|
@ -405,5 +629,17 @@
|
|||
robotCleanerTargetOrder: ROBOT_TARGET_ORDER,
|
||||
robotCleanerMoveSpeed: ROBOT_MOVE_SPEED,
|
||||
robotCleanerHighMoveSpeed: ROBOT_HIGH_SPEED,
|
||||
fireBurningText: FIRE_BURNING_TEXT,
|
||||
fireSeenText: FIRE_SEEN_TEXT,
|
||||
fireTempMultiplier,
|
||||
contactWaterDrop,
|
||||
consumeWaterForFire,
|
||||
igniteTarinaiByFire,
|
||||
panicTarinaiFromFire,
|
||||
igniteAntByFire,
|
||||
igniteGrassByFire,
|
||||
spawnFireAt: (worldRef, x, y, opts = {}) => spawnFireAt(worldRef, x, y, opts),
|
||||
scatterFireFrom,
|
||||
updateFire,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,14 @@ function mechanicalRenderTier() {
|
|||
return window.TarinaiPerf.renderQualityTier();
|
||||
}
|
||||
|
||||
function itemShadowQualityTier() {
|
||||
return window.TarinaiPerf?.shadowQualityTier?.() || "high";
|
||||
}
|
||||
|
||||
function itemShadowsEnabled() {
|
||||
return itemShadowQualityTier() !== "off";
|
||||
}
|
||||
|
||||
function drawRopeParticlePath(ctx, particles, ox, oy) {
|
||||
if (!Array.isArray(particles) || particles.length < 2) return false;
|
||||
const tier = mechanicalRenderTier();
|
||||
|
|
@ -86,7 +94,7 @@ draw(ctx, t, lighting = null) {
|
|||
const dropFallHeight = this.type === "genkotsu" ? Math.max(360, this.r * 5.8) : 170;
|
||||
const dropY = dropT > 0 ? -dropFallHeight * dropT : 0;
|
||||
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
|
||||
if (this.type !== "trace" && this.type !== "splat") {
|
||||
if (this.type !== "trace" && this.type !== "splat" && shadow.alpha > 0) {
|
||||
const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null;
|
||||
const zunchiImg = zunchiId ? getRenderableImage(zunchiId, "zunchi") : null;
|
||||
if (zunchiImg) {
|
||||
|
|
@ -129,9 +137,11 @@ draw(ctx, t, lighting = null) {
|
|||
const img = getRenderableImage("genkotsu", "genkotsu");
|
||||
const impact = clamp(this.impactFlash || 0, 0, 1);
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(54,42,31,0.26)";
|
||||
ctx.shadowBlur = 5 + impact * 8;
|
||||
ctx.shadowOffsetY = 4;
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(54,42,31,0.26)";
|
||||
ctx.shadowBlur = 5 + impact * 8;
|
||||
ctx.shadowOffsetY = 4;
|
||||
}
|
||||
const squashY = 1 - impact * 0.08;
|
||||
const stretchX = 1 + impact * 0.05;
|
||||
ctx.scale(stretchX, squashY);
|
||||
|
|
@ -170,9 +180,11 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.save();
|
||||
if (stuck) ctx.scale((this.pinVisualSide || 1) < 0 ? -1 : 1, 1);
|
||||
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
|
||||
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
}
|
||||
if (img) {
|
||||
const metrics = getImageMetrics(assetId);
|
||||
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
|
||||
|
|
@ -477,14 +489,14 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "signboard") {
|
||||
ctx.save();
|
||||
const boardGrad = ctx.createLinearGradient(0, -this.r * 1.28, 0, this.r * 0.12);
|
||||
const boardHalfW = this.r * 1.20;
|
||||
const boardHalfW = this.r * 1.34;
|
||||
boardGrad.addColorStop(0, styleNight ? "#d5cab8" : "#fff8e8");
|
||||
boardGrad.addColorStop(0.55, styleNight ? "#c0b29a" : "#f3e3c6");
|
||||
boardGrad.addColorStop(1, styleNight ? "#aa9a82" : "#dfc397");
|
||||
ctx.fillStyle = styleNight ? "#8a7557" : "#d6ba87";
|
||||
ctx.strokeStyle = styleNight ? "#5d4d3b" : "#9b7444";
|
||||
ctx.lineWidth = 2.2;
|
||||
roundedRect(ctx, -this.r * 0.13, -this.r * 0.02, this.r * 0.26, this.r * 1.78, this.r * 0.04);
|
||||
roundedRect(ctx, -this.r * 0.13, this.r * 0.04, this.r * 0.26, this.r * 1.48, this.r * 0.04);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = boardGrad;
|
||||
|
|
@ -536,10 +548,12 @@ draw(ctx, t, lighting = null) {
|
|||
const r = this.r || 21;
|
||||
const time = this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0);
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(90, 178, 255, 0.30)";
|
||||
ctx.shadowBlur = 11;
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(90, 178, 255, 0.30)";
|
||||
ctx.shadowBlur = 11;
|
||||
}
|
||||
|
||||
// 旧版の氷塊風から、白い保冷デュワー缶風の外観へ変更。
|
||||
// \u65E7\u7248\u306E\u6C37\u584A\u98A8\u304B\u3089\u3001\u767D\u3044\u4FDD\u51B7\u30C7\u30E5\u30EF\u30FC\u7F36\u98A8\u306E\u5916\u89B3\u3078\u5909\u66F4\u3002
|
||||
const bodyW = r * 1.38;
|
||||
const bodyH = r * 1.56;
|
||||
const x = -bodyW / 2;
|
||||
|
|
@ -572,7 +586,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.font = `bold ${Math.max(9, Math.round(r * 0.34))}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("CO₂", 0, y + bodyH * 0.35);
|
||||
ctx.fillText("CO\u2082", 0, y + bodyH * 0.35);
|
||||
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.72)";
|
||||
ctx.lineWidth = 1.2;
|
||||
|
|
@ -599,8 +613,10 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "stove") {
|
||||
const r = this.r || 24;
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
|
||||
ctx.shadowBlur = 9;
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
|
||||
ctx.shadowBlur = 9;
|
||||
}
|
||||
ctx.fillStyle = styleNight ? "rgba(96, 72, 58, 0.96)" : "rgba(150, 104, 76, 0.96)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(49, 35, 29, 0.78)" : "rgba(92, 62, 45, 0.72)";
|
||||
ctx.lineWidth = 2.2;
|
||||
|
|
@ -671,16 +687,18 @@ draw(ctx, t, lighting = null) {
|
|||
const r = this.r || 18;
|
||||
ctx.save();
|
||||
ctx.rotate(this.spin || 0);
|
||||
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
||||
ctx.shadowBlur = 2 + moving * 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = styleNight ? "rgba(0,0,0,0.34)" : "rgba(52,40,26,0.18)";
|
||||
ctx.shadowBlur = 2 + moving * 3;
|
||||
ctx.shadowOffsetY = 2;
|
||||
}
|
||||
// Use the original soccer-ball emoji as the visible sprite. The
|
||||
// collision center remains item.x/item.y; rendering is centered via
|
||||
// textAlign/textBaseline rather than replacing the emoji with vector art.
|
||||
ctx.font = `${Math.max(18, r * 2.05)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("⚽", 0, 0);
|
||||
ctx.fillText("\u26BD", 0, 0);
|
||||
if (moving > 0.55) {
|
||||
ctx.globalAlpha *= 0.10 + moving * 0.10;
|
||||
ctx.shadowColor = "transparent";
|
||||
|
|
@ -690,17 +708,53 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "firecracker") {
|
||||
} else if (this.type === "fire") {
|
||||
const r = this.r || 14;
|
||||
const pulse = 0.82 + Math.sin(t * 10 + this.seed) * 0.12;
|
||||
ctx.save();
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(255,104,28,0.42)";
|
||||
ctx.shadowBlur = 12;
|
||||
}
|
||||
ctx.globalAlpha *= 0.88;
|
||||
const grad = ctx.createRadialGradient(0, r * 0.22, r * 0.18, 0, 0, r * 1.55);
|
||||
grad.addColorStop(0, "rgba(255,238,112,0.94)");
|
||||
grad.addColorStop(0.36, "rgba(255,116,36,0.86)");
|
||||
grad.addColorStop(1, "rgba(198,32,16,0.08)");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, r * 0.18, r * 1.25 * pulse, r * 0.72, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,96,24,0.90)";
|
||||
ctx.strokeStyle = "rgba(255,224,86,0.76)";
|
||||
ctx.lineWidth = Math.max(1.2, r * 0.10);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const a = -Math.PI / 2 + (i - 1.5) * 0.36 + Math.sin(t * 6 + this.seed + i) * 0.10;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.cos(a + 1.7) * r * 0.38, r * 0.48);
|
||||
ctx.quadraticCurveTo(Math.cos(a) * r * (0.45 + i * 0.08), -r * 0.10, Math.cos(a) * r * 0.38, -r * (1.18 + i * 0.13) * pulse);
|
||||
ctx.quadraticCurveTo(Math.cos(a - 0.7) * r * 0.20, -r * 0.20, Math.cos(a - 1.4) * r * 0.36, r * 0.48);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "rgba(255,236,106,0.92)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -r * 0.30, r * 0.42, r * 0.82 * pulse, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.type === "firecracker" || this.type === "flame_firecracker") {
|
||||
const flameMode = this.type === "flame_firecracker";
|
||||
const fuse = clamp((this.fuseTimer ?? 5) / Math.max(this.fuseMax || 5, 0.1), 0, 1);
|
||||
const flash = fuse < 0.35 ? (0.5 + Math.sin(t * 20 + this.seed) * 0.5) : 0;
|
||||
ctx.rotate(-0.22 + Math.sin(this.seed) * 0.12);
|
||||
ctx.fillStyle = flash > 0.65 ? "#ffed77" : "#d94b43";
|
||||
ctx.fillStyle = flash > 0.65 ? "#ffed77" : (flameMode ? "#e76d29" : "#d94b43");
|
||||
ctx.strokeStyle = "rgba(96,48,34,0.78)";
|
||||
ctx.lineWidth = 2;
|
||||
roundedRect(ctx, -this.r * 0.72, -this.r * 0.86, this.r * 1.44, this.r * 1.72, 5);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#f4d15b";
|
||||
ctx.fillStyle = flameMode ? "#ffbb43" : "#f4d15b";
|
||||
ctx.fillRect(-this.r * 0.55, -this.r * 0.54, this.r * 1.1, this.r * 0.20);
|
||||
ctx.fillRect(-this.r * 0.55, this.r * 0.34, this.r * 1.1, this.r * 0.20);
|
||||
ctx.strokeStyle = "rgba(74,55,39,0.82)";
|
||||
|
|
@ -716,7 +770,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.fillStyle = "rgba(42,36,29,0.66)";
|
||||
ctx.font = "bold 10px ui-rounded, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(String(Math.ceil(this.fuseTimer ?? 5)), 0, this.r * 1.42);
|
||||
ctx.fillText(flameMode ? "F" : String(Math.ceil(this.fuseTimer ?? 5)), 0, this.r * 1.42);
|
||||
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "glass_wall" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence") {
|
||||
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
|
||||
const gate = this.type === "gate_fence";
|
||||
|
|
@ -864,8 +918,8 @@ draw(ctx, t, lighting = null) {
|
|||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
// v39.15.53: 毒ブロックは物理輪郭だけを安定描画する。
|
||||
// 中央の塗りつぶし/文字はキャッシュ更新時に明滅して見えるため描画しない。
|
||||
// v39.15.53: \u6BD2\u30D6\u30ED\u30C3\u30AF\u306F\u7269\u7406\u8F2A\u90ED\u3060\u3051\u3092\u5B89\u5B9A\u63CF\u753B\u3059\u308B\u3002
|
||||
// \u4E2D\u592E\u306E\u5857\u308A\u3064\u3076\u3057/\u6587\u5B57\u306F\u30AD\u30E3\u30C3\u30B7\u30E5\u66F4\u65B0\u6642\u306B\u660E\u6EC5\u3057\u3066\u898B\u3048\u308B\u305F\u3081\u63CF\u753B\u3057\u306A\u3044\u3002
|
||||
} else if (this.type === "reciprocator") {
|
||||
const bodyAngle = itemAngleFor(this);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
|
|
@ -907,8 +961,10 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.globalAlpha *= powered ? 1 : 0.72;
|
||||
ctx.strokeStyle = styleNight ? "#4f7192" : "#6da6cf";
|
||||
ctx.lineWidth = thick;
|
||||
ctx.shadowColor = powered ? (styleNight ? "rgba(86,130,180,0.34)" : "rgba(80,150,205,0.28)") : "rgba(74,82,90,0.20)";
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = powered ? (styleNight ? "rgba(86,130,180,0.34)" : "rgba(80,150,205,0.28)") : "rgba(74,82,90,0.20)";
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
||||
}
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.shadowBlur = 0;
|
||||
if (tier !== "low") {
|
||||
|
|
|
|||
|
|
@ -33,20 +33,22 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "\u5de8\u5927\u85ac", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "\u4f53\u3092\u5927\u304d\u304f\u5f37\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u304c\u5927\u304d\u3044\u3002" },
|
||||
dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "\u77ee\u5c0f\u85ac", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "\u4f53\u3092\u5c0f\u3055\u304f\u5f31\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u3082\u5927\u304d\u3044\u3002" },
|
||||
zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "\u305a\u3093\u3060\u6c41", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "\u98df\u3079\u308b\u3068\u5168\u3066\u306e\u30a2\u30a4\u30c6\u30e0\u52b9\u679c\u3092\u9664\u53bb\u3059\u308b\u3002" },
|
||||
slave_chain: { id: "slave_chain", itemType: "slave_chain", label: "奴隷の鎖", placeable: true, scalable: true, radius: 15, amount: 1, iconText: "⛓", tooltip: "触れたたりないを強制的にずんちどれいにする。" },
|
||||
golden_crown: { id: "golden_crown", itemType: "golden_crown", label: "黄金の冠", placeable: true, scalable: true, radius: 16, amount: 1, iconText: "♛", tooltip: "触れたたりないを強制的にたりない王にする。" },
|
||||
slave_chain: { id: "slave_chain", itemType: "slave_chain", label: "\u5974\u96B7\u306E\u9396", placeable: true, scalable: true, radius: 15, amount: 1, iconText: "\u26D3", tooltip: "\u89E6\u308C\u305F\u305F\u308A\u306A\u3044\u3092\u5F37\u5236\u7684\u306B\u305A\u3093\u3061\u3069\u308C\u3044\u306B\u3059\u308B\u3002" },
|
||||
golden_crown: { id: "golden_crown", itemType: "golden_crown", label: "\u9EC4\u91D1\u306E\u51A0", placeable: true, scalable: true, radius: 16, amount: 1, iconText: "\u265B", tooltip: "\u89E6\u308C\u305F\u305F\u308A\u306A\u3044\u3092\u5F37\u5236\u7684\u306B\u305F\u308A\u306A\u3044\u738B\u306B\u3059\u308B\u3002" },
|
||||
grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.webp", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" },
|
||||
stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.webp", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" },
|
||||
genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "\u62f3\u3092\u5730\u9762\u306b\u305f\u305f\u304d\u3064\u3051\u308b\u3002" },
|
||||
bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||
dry_ice: { id: "dry_ice", itemType: "dry_ice", label: "ドライアイス", placeable: true, scalable: false, radius: 21, amount: 999, iconText: "🧊", collisionShape: "circle", tooltip: "周囲の気温を10℃下げる。効果は重ねられる。" },
|
||||
stove: { id: "stove", itemType: "stove", label: "ストーブ", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "🔥", collisionShape: "circle", tooltip: "周囲の気温を10℃上げる。効果は重ねられる。" },
|
||||
dry_ice: { id: "dry_ice", itemType: "dry_ice", label: "\u30C9\u30E9\u30A4\u30A2\u30A4\u30B9", placeable: true, scalable: false, radius: 21, amount: 999, iconText: "\uD83E\uDDCA", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0B\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" },
|
||||
stove: { id: "stove", itemType: "stove", label: "\u30B9\u30C8\u30FC\u30D6", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "\uD83D\uDD25", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0A\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\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: 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" },
|
||||
fire: { id: "fire", itemType: "fire", label: "\u708e", placeable: true, scalable: true, radius: 18, amount: 140, iconText: "\uD83D\uDD25", collisionShape: "circle", tooltip: "\u89e6\u308c\u308b\u3068\u71c3\u3048\u79fb\u308a\u3001\u8349\u3084\u500b\u4f53\u306b\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002\u6c34\u6ef4\u3084\u96e8\u3001\u4f4e\u6e29\u3067\u6d88\u3048\u3084\u3059\u3044\u3002" },
|
||||
flame_firecracker: { id: "flame_firecracker", itemType: "flame_firecracker", label: "\u706b\u708e\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, iconText: "\uD83E\uDDE8", collisionShape: "circle", tooltip: "\u30bf\u30a4\u30de\u30fc\u5f8c\u306b\u7206\u767a\u305b\u305a\u3001\u5468\u56f2\u306b\u708e\u3092\u6492\u304d\u6563\u3089\u3059\u3002" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
|
||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||
|
|
@ -69,11 +71,11 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "dry_ice", "stove", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
hazard: new Set(["firecracker", "flame_firecracker", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box", "grass_bed"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "dry_ice", "stove", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
draggable: new Set(["ball", "stone", "firecracker", "flame_firecracker", "bed", "nest_box", "dry_ice", "stove", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -130,7 +132,7 @@ const TOOL_CATEGORIES = Object.freeze([
|
|||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "dry_ice", "stove", "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"] },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "flame_firecracker", "fire", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
||||
function toolCategories() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
if (type === "water" || type === "zunda_juice") item.roles.drink = true;
|
||||
if (["sweet", "water", "zunda_juice", "slave_chain", "golden_crown"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
|
||||
if (type === "bed" || type === "nest_box") item.roles.sleepPlace = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (["firecracker", "flame_firecracker", "fire", "genkotsu", "pushpin", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (type === "grass") item.roles.grassMaterial = true;
|
||||
if (type === "grass") {
|
||||
item.grassStage = Math.floor(rand(0, 2.999));
|
||||
|
|
@ -87,10 +87,18 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.workerSprite = "ant_worker";
|
||||
item.decayTimer = 0;
|
||||
}
|
||||
if (type === "firecracker") {
|
||||
if (type === "firecracker" || type === "flame_firecracker") {
|
||||
item.fuseTimer = 5;
|
||||
item.fuseMax = 5;
|
||||
}
|
||||
if (type === "fire") {
|
||||
item.amount = Math.max(Number(item.amount || 0) || 0, 120);
|
||||
item.r = Math.max(Number(item.r || 0) || 0, 14);
|
||||
item.flameLife = Math.max(Number(item.flameLife || 0) || 0, 10.0);
|
||||
item.flameSpreadCooldown = 0;
|
||||
item.flamePanicCooldown = 0;
|
||||
item.roles.danger = true;
|
||||
}
|
||||
if (type === "genkotsu") {
|
||||
item.amount = 100;
|
||||
item.dropMax = 0;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
// 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", "robot_cleaner"]);
|
||||
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker", "flame_firecracker", "fire", "robot_cleaner"]);
|
||||
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
|
||||
const SCHEDULED_ITEM_TYPES = Object.freeze([
|
||||
"ball", "genkotsu", "firecracker", "pushpin", "oshibyo",
|
||||
"ball", "genkotsu", "firecracker", "flame_firecracker", "fire", "pushpin", "oshibyo",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
}
|
||||
|
||||
function physicsBudgetFor(worldRef) {
|
||||
const tier = global.TarinaiPerf.renderQualityTier();
|
||||
const tier = global.TarinaiPerf.simulationQualityTier?.() || "high";
|
||||
const itemCount = (worldRef?.items || []).length || 0;
|
||||
const activeHint = worldRef?._itemUpdateRealtime?.length || 0;
|
||||
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
|
|
|
|||
12
js/items.js
12
js/items.js
|
|
@ -248,7 +248,7 @@ function drawBulletSprite(ctx, r) {
|
|||
|
||||
function isConsumableSpriteType(type = "") {
|
||||
const key = String(type || "");
|
||||
return ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug"].includes(key)
|
||||
return ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "water", "zunda_juice", "mercury"].includes(key)
|
||||
|| isParamEffectItemType(key);
|
||||
}
|
||||
|
||||
|
|
@ -318,6 +318,10 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
|
|||
ctx.quadraticCurveTo(rootX + Math.cos(a) * len * 0.46, -localR * 0.05, rootX + Math.cos(a) * len, Math.sin(a) * len * 0.72);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (["water", "zunda_juice", "mercury"].includes(key)) {
|
||||
const visual = itemVisualFor(key);
|
||||
const palette = visual?.palette || {};
|
||||
drawOvalWaterDropSprite(ctx, localR * (visual?.fieldScale || visual?.scale || 1), { fill: palette.fill, stroke: palette.stroke, highlight: palette.highlight || palette.accent });
|
||||
} else if (key === "sleep_drug") {
|
||||
drawSleepTabletSprite(ctx, localR * visualFieldScaleFor("sleep_drug"), visualPaletteFor("sleep_drug"));
|
||||
} else if (isParamEffectItemType(key)) {
|
||||
|
|
@ -567,14 +571,16 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
|
|||
pushpin: 0.92,
|
||||
oshibyo: 0.62,
|
||||
firecracker: 0.90,
|
||||
flame_firecracker: 0.90,
|
||||
fire: 1.05,
|
||||
giant_drug: 1.22,
|
||||
dwarf_drug: 0.56,
|
||||
};
|
||||
let scale = scaleMap[type] || 0.92;
|
||||
if (isConsumableSpriteType(type)) scale *= 1.95;
|
||||
else if (["grass", "water", "zunda_juice"].includes(type)) scale *= 1.85;
|
||||
else if (["grass", "water", "zunda_juice", "mercury", "fire"].includes(type)) scale *= 1.85;
|
||||
else if (type === "duplicator") scale *= 1.55;
|
||||
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
|
||||
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "fire", "genkotsu"].includes(type)) scale *= 1.08;
|
||||
else if (isParamEffectItemType(type)) scale *= 1.35;
|
||||
if (watermark) scale *= 1.42;
|
||||
ctx.scale(scale, scale);
|
||||
|
|
|
|||
|
|
@ -136,8 +136,7 @@ loadImages((done, total) => {
|
|||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=15.20.16";
|
||||
const swUrl = window.TARINAI_APP?.withVersion?.("./service-worker.js") || "./service-worker.js?v=39.15.92";
|
||||
navigator.serviceWorker.register(swUrl, { updateViaCache: "none" }).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -668,7 +668,6 @@
|
|||
obj.impulseVy = clamp(num(obj.impulseVy) + mvy * num(opts.impulseVScale), -num(opts.impulseMax, 320), num(opts.impulseMax, 320));
|
||||
}
|
||||
applyPassiveReciprocatorImpulse(rect.item, rect.axisX || 1, rect.axisY || 0, nX, nY, vx, vy, num(opts.passiveImpulseScale, 1));
|
||||
reverseReciprocatorOnCircleContact(rect.item, obj, nX, nY, 0.016, obj.world || rect.item?.world || null, "circle");
|
||||
applied = true;
|
||||
} else if (rect.poisonBlock) {
|
||||
const omega = clamp(num(rect.angularVelocity), -5.5, 5.5);
|
||||
|
|
@ -894,7 +893,7 @@
|
|||
const t = String(obj.type || "");
|
||||
// Tarinai also collide as circles, but a walking creature should not flip a
|
||||
// powered rail. Keep contact reversal to item-like physical circles.
|
||||
if (t === "ball" || t === "stone" || t === "genkotsu" || t === "firecracker" || t === "pushpin" || t === "oshibyo") return true;
|
||||
if (t === "ball" || t === "stone" || t === "genkotsu" || t === "firecracker" || t === "pushpin" || t === "oshibyo" || t === "zunchi") return true;
|
||||
return Boolean(obj.physicsBody && !isMechanicalType(t));
|
||||
}
|
||||
|
||||
|
|
@ -952,46 +951,71 @@
|
|||
}
|
||||
|
||||
function resolveReciprocatorPhysicalContacts(item, dt = 0.016, worldRef = null) {
|
||||
// Circle contacts are solved as push-away contacts in resolveMechanicalCircleContacts().
|
||||
// A powered reciprocator must reverse at rail limits or mechanical blockers,
|
||||
// not merely because it touched Tarinai, a ball, or a pin-like item.
|
||||
if (!item || item.dead || item.type !== "reciprocator" || !isPowered(item) || !worldRef?.items) return false;
|
||||
const rects = obstacleRects(item);
|
||||
if (!rects.length) return false;
|
||||
const radius = reach(item) + 90;
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.nearbyObstacles?.(item.x, item.y, radius, false) || worldRef.items || [];
|
||||
const a = axis(item);
|
||||
const dir = Math.sign(ps(item, "railDir", 1)) || 1;
|
||||
const driveX = a.x * dir;
|
||||
const driveY = a.y * dir;
|
||||
let best = null;
|
||||
for (const other of source) {
|
||||
if (!other || other === item || other.dead || !isPhysicalCircleContactObject(other)) continue;
|
||||
const rr = itemCircleRadius(other);
|
||||
for (const rect of rects) {
|
||||
const info = orientedRectCircleContactInfo(rect, other, rr, 1.25);
|
||||
if (!info) continue;
|
||||
const aheadNormal = driveX * info.nx + driveY * info.ny;
|
||||
const aheadCenter = (num(other.x) - num(item.x)) * driveX + (num(other.y) - num(item.y)) * driveY;
|
||||
// A reciprocator is a moving bar. When a circle is centered on the
|
||||
// bar stroke, the geometric normal often points sideways, so use the
|
||||
// rail-direction center test as the authoritative "hit the front" test.
|
||||
if (aheadNormal < 0.16 && aheadCenter < -rr * 0.35) continue;
|
||||
const contactInfo = aheadNormal >= 0.16 ? info : { ...info, nx: driveX, ny: driveY };
|
||||
const score = Math.max(aheadNormal, 0) * 1000 + Math.max(0, aheadCenter) + num(info.overlap);
|
||||
if (!best || score > best.score) best = { other, info: contactInfo, score };
|
||||
}
|
||||
}
|
||||
if (!best) return false;
|
||||
const changed = reverseReciprocatorOnMechanicalContact(item, best.info.nx, best.info.ny, dt, worldRef, "physical-item");
|
||||
if (changed) {
|
||||
item._reciprocatorBlockedTimer = 0;
|
||||
worldRef.markSpatialDirty?.("reciprocator-physical-contact");
|
||||
worldRef.drawListDirty = true;
|
||||
}
|
||||
return changed;
|
||||
return resolveMechanicalCircleContacts(item, dt, worldRef, reach(item) + 96);
|
||||
}
|
||||
|
||||
function reverseReciprocatorOnCircleContact(item, obj, nx, ny, dt = 0.016, worldRef = null, reason = "circle-contact") {
|
||||
if (!isPhysicalCircleContactObject(obj)) return false;
|
||||
return reverseReciprocatorOnMechanicalContact(item, nx, ny, dt, worldRef, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function resolveMechanicalCircleContacts(item, dt = 0.016, worldRef = null, radius = null) {
|
||||
if (!item || item.dead || !isMechanicalType(item) || !worldRef?.items) return false;
|
||||
const rects = obstacleRects(item);
|
||||
if (!rects.length) return false;
|
||||
const queryRadius = radius || reach(item) + 96;
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, queryRadius, true) || worldRef.nearbyObstacles?.(item.x, item.y, queryRadius, false) || worldRef.items || [];
|
||||
let changed = false;
|
||||
let contacts = 0;
|
||||
for (const other of source) {
|
||||
if (!other || other === item || other.dead || !isPhysicalCircleContactObject(other)) continue;
|
||||
if (typeof isPinType === "function" && isPinType(other.type) && other.pinState === "lodged") continue;
|
||||
const rr = itemCircleRadius(other);
|
||||
if (Math.hypot(num(other.x) - num(item.x), num(other.y) - num(item.y)) > queryRadius + rr + 12) continue;
|
||||
let best = null;
|
||||
for (const rect of rects) {
|
||||
const info = orientedRectCircleContactInfo(rect, other, rr, 2.75);
|
||||
if (!info) continue;
|
||||
if (!best || info.overlap > best.overlap) best = info;
|
||||
}
|
||||
if (!best) continue;
|
||||
const nx = Number.isFinite(best.nx) ? best.nx : 1;
|
||||
const ny = Number.isFinite(best.ny) ? best.ny : 0;
|
||||
const sep = Math.max(0.5, Math.min(28, best.overlap + 1.4));
|
||||
if (Number.isFinite(other.x)) other.x += nx * sep;
|
||||
if (Number.isFinite(other.y)) other.y += ny * sep;
|
||||
const v = pointVelocity(item, best.x, best.y);
|
||||
const closing = Math.max(0, v.x * nx + v.y * ny);
|
||||
const baseImpulse = clamp(best.overlap * 7.2 + closing * 0.24 + 8.0, 5.5, item.type === "reciprocator" ? 150 : 210);
|
||||
if (Number.isFinite(other.vx)) other.vx = clamp((other.vx || 0) + nx * baseImpulse, -1180, 1180);
|
||||
if (Number.isFinite(other.vy)) other.vy = clamp((other.vy || 0) + ny * baseImpulse, -1180, 1180);
|
||||
if (other.type === "ball") {
|
||||
other.prevX = Number.isFinite(other.prevX) ? other.prevX : other.x - nx * sep;
|
||||
other.prevY = Number.isFinite(other.prevY) ? other.prevY : other.y - ny * sep;
|
||||
other.spinVelocity = clamp((other.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * (5.8 + Math.min(10, closing * 0.02)), -48, 48);
|
||||
other.lastKickedAt = worldRef.time || 0;
|
||||
other.lastKickerId = item.id || "mechanical";
|
||||
}
|
||||
if (item.type === "reciprocator") {
|
||||
applyRailCorrection(item, -nx, -ny, best.overlap, 0.18);
|
||||
} else {
|
||||
applyImpulse(item, best.x, best.y, -nx * baseImpulse * (item.type === "poison_block" ? 0.38 : 0.16), -ny * baseImpulse * (item.type === "poison_block" ? 0.38 : 0.16), item.type === "poison_block" ? 0.58 : 0.34);
|
||||
if (item.type === "rotator") brakePoweredRotatorOnMechanicalContact(item, dt, worldRef, "physical-circle");
|
||||
}
|
||||
contacts += 1;
|
||||
changed = true;
|
||||
if (contacts >= 18) break;
|
||||
}
|
||||
if (changed) {
|
||||
commitBody(item, "mechanical-circle-contact");
|
||||
worldRef.markSpatialDirty?.("mechanical-circle-contact");
|
||||
worldRef.drawListDirty = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function strongestRectContact(rectsA, rectsB, padding = 0) {
|
||||
|
|
@ -1060,9 +1084,9 @@
|
|||
function fenceRectsNear(item, worldRef, radius) {
|
||||
const out = [];
|
||||
if (!item || !worldRef?.items) return out;
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.items;
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.nearbyObstacles?.(item.x, item.y, radius, false) || worldRef.items;
|
||||
for (const other of source) {
|
||||
if (!other || other === item || other.dead || !worldRef.isFenceType?.(other.type)) continue;
|
||||
if (!other || other === item || other.dead || isMechanicalType(other)) continue;
|
||||
if (other.type === "gate_fence" && other.gateOpen) continue;
|
||||
for (const rect of worldRef.solidObstacleRects?.(other) || []) out.push(rect);
|
||||
}
|
||||
|
|
@ -1082,9 +1106,6 @@
|
|||
|
||||
function resolveFenceContacts(item, dt, worldRef, radius = null) {
|
||||
if (!item || !worldRef || !isMechanicalType(item)) return false;
|
||||
// \u5f80\u5fa9\u4f53\u306e\u505c\u6b62\u30fb\u53cd\u8ee2\u5bfe\u8c61\u306f\u7269\u7406\u30a2\u30a4\u30c6\u30e0\u306b\u9650\u5b9a\u3059\u308b\u3002
|
||||
// \u9759\u7684\u306a\u67f5/\u969c\u5bb3\u7269\u306f\u30ec\u30fc\u30eb\u99c6\u52d5\u3092\u6bba\u3057\u3084\u3059\u3044\u305f\u3081\u3001\u5f80\u5fa9\u4f53\u5074\u3067\u306f\u3053\u3053\u3067\u6271\u308f\u306a\u3044\u3002
|
||||
if (item.type === "reciprocator") return false;
|
||||
const rects = obstacleRects(item);
|
||||
const fences = fenceRectsNear(item, worldRef, radius || reach(item) + 80);
|
||||
let changed = false;
|
||||
|
|
@ -1104,6 +1125,7 @@
|
|||
applyImpulse(item, info.x, info.y, -info.nx * impulse, -info.ny * impulse, separated ? 0.92 : 1.10);
|
||||
applyRailCorrection(item, info.nx, info.ny, info.overlap, 0.82);
|
||||
if (item.type === "rotator") brakePoweredRotatorOnMechanicalContact(item, dt, worldRef, "fence");
|
||||
if (item.type === "reciprocator") reverseReciprocatorOnMechanicalContact(item, info.nx, info.ny, dt, worldRef, "solid-obstacle");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1114,14 +1136,11 @@
|
|||
function hasInteractionCandidates(item, worldRef, radius = null) {
|
||||
if (!item || !worldRef) return false;
|
||||
const r = radius || reach(item) + 80;
|
||||
const source = worldRef.nearbyObstacles?.(item.x, item.y, r, false) || worldRef.nearbyItems?.(item.x, item.y, r, true) || worldRef.items || [];
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, r, true) || worldRef.nearbyObstacles?.(item.x, item.y, r, false) || worldRef.items || [];
|
||||
for (const other of source) {
|
||||
if (!other || other === item || other.dead) continue;
|
||||
if (item.type === "reciprocator") {
|
||||
if (isMechanicalType(other)) return true;
|
||||
continue;
|
||||
}
|
||||
if (isMechanicalType(other) || worldRef.isFenceType?.(other.type)) return true;
|
||||
if (isMechanicalType(other) || worldRef.isFenceType?.(other.type) || isPhysicalCircleContactObject(other)) return true;
|
||||
if (worldRef.solidObstacleRects?.(other)?.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1130,11 +1149,12 @@
|
|||
if (!item || !worldRef?.items || !isMechanicalType(item)) return false;
|
||||
let changed = false;
|
||||
const radius = reach(item) + 80;
|
||||
const source = worldRef.nearbyObstacles?.(item.x, item.y, radius, false) || worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.items;
|
||||
const source = worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.nearbyObstacles?.(item.x, item.y, radius, false) || worldRef.items;
|
||||
for (const other of source) {
|
||||
if (!other || other === item || other.dead || !isMechanicalType(other)) continue;
|
||||
changed = resolvePair(item, other, dt, worldRef) || changed;
|
||||
}
|
||||
changed = resolveMechanicalCircleContacts(item, dt, worldRef, radius) || changed;
|
||||
changed = resolveFenceContacts(item, dt, worldRef, radius) || changed;
|
||||
return changed;
|
||||
}
|
||||
|
|
@ -1865,8 +1885,14 @@
|
|||
isMechanicalType,
|
||||
isPowered,
|
||||
sanitizeSegments,
|
||||
worldSegments,
|
||||
obstacleRects,
|
||||
placementRects,
|
||||
poisonHazardRects,
|
||||
boundsAabb,
|
||||
extent,
|
||||
reach,
|
||||
resetAnchor,
|
||||
axis,
|
||||
railAxisAngle,
|
||||
applySurfaceVelocityToCircle,
|
||||
|
|
@ -1878,7 +1904,6 @@
|
|||
updatePoisonBlock,
|
||||
applyPokeImpulse,
|
||||
hitTest,
|
||||
reach,
|
||||
invalidateGeometry,
|
||||
wakeItem,
|
||||
passiveItemAwake,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,14 @@
|
|||
let frameOpen = null;
|
||||
let frameCount = 0;
|
||||
let lastQualityCheckAt = 0;
|
||||
const mobilePerfHint = () => Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
let tier = mobilePerfHint() ? "mid" : "high";
|
||||
let dprScale = mobilePerfHint() ? 0.82 : 1;
|
||||
const storageKey = "tarinai_visual_settings_v1";
|
||||
const allowed = {
|
||||
effects: new Set(["off", "on"]),
|
||||
shadows: new Set(["off", "on"]),
|
||||
details: new Set(["low", "high"]),
|
||||
};
|
||||
const defaults = Object.freeze({ effects: "on", shadows: "on", details: "high" });
|
||||
let visualSettings = loadVisualSettings();
|
||||
let resizeRequested = false;
|
||||
|
||||
function bucketFor(label) {
|
||||
|
|
@ -74,14 +79,47 @@
|
|||
}
|
||||
}
|
||||
|
||||
function qualityFromFps(fps) {
|
||||
const f = Number(fps) || 0;
|
||||
const mobile = mobilePerfHint();
|
||||
const maxDpr = mobile ? 0.86 : 1;
|
||||
if (f > 55) return { tier: mobile ? "mid" : "high", dpr: maxDpr };
|
||||
if (f < 32) return { tier: "low", dpr: mobile ? 0.64 : 0.72 };
|
||||
if (f < 45) return { tier: "mid", dpr: mobile ? 0.74 : 0.86 };
|
||||
return { tier: tier === "low" ? "mid" : tier, dpr: Math.min(maxDpr, tier === "low" ? 0.82 : dprScale) };
|
||||
function normalizeVisualSetting(key, value) {
|
||||
const raw = String(value || "");
|
||||
if (key === "effects" || key === "shadows") {
|
||||
if (raw === "off") return "off";
|
||||
if (raw === "on" || raw === "low" || raw === "mid" || raw === "high") return "on";
|
||||
}
|
||||
if (key === "details") {
|
||||
if (raw === "low") return "low";
|
||||
if (raw === "high" || raw === "mid") return "high";
|
||||
}
|
||||
return defaults[key];
|
||||
}
|
||||
|
||||
function sanitizeVisualSettings(value) {
|
||||
const src = value && typeof value === "object" ? value : {};
|
||||
const next = { ...defaults };
|
||||
for (const key of Object.keys(defaults)) {
|
||||
const normalized = normalizeVisualSetting(key, src[key]);
|
||||
if (allowed[key]?.has(normalized)) next[key] = normalized;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function loadVisualSettings() {
|
||||
try {
|
||||
const raw = global.localStorage?.getItem(storageKey);
|
||||
return sanitizeVisualSettings(raw ? JSON.parse(raw) : null);
|
||||
} catch (_) {
|
||||
return { ...defaults };
|
||||
}
|
||||
}
|
||||
|
||||
function saveVisualSettings() {
|
||||
try {
|
||||
global.localStorage?.setItem(storageKey, JSON.stringify(visualSettings));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function dprScaleForDetails(details) {
|
||||
if (details === "low") return 0.72;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function endFrame(rawDt = 0, fps = global.__tarinaiFps || 0) {
|
||||
|
|
@ -102,18 +140,50 @@
|
|||
if (t - lastQualityCheckAt >= 750) {
|
||||
lastQualityCheckAt = t;
|
||||
smoothBuckets();
|
||||
const next = qualityFromFps(fps);
|
||||
const nextDpr = next.dpr;
|
||||
if (next.tier !== tier || Math.abs(nextDpr - dprScale) > 0.04) {
|
||||
tier = next.tier;
|
||||
dprScale = nextDpr;
|
||||
resizeRequested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderQualityTier() { return tier; }
|
||||
function dprScaleValue() { return dprScale; }
|
||||
function visualLevel(key, fallback = "high") {
|
||||
return visualSettings[key] || fallback;
|
||||
}
|
||||
|
||||
function visualSettingsValue() {
|
||||
return { ...visualSettings };
|
||||
}
|
||||
|
||||
function setVisualSetting(key, value) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaults, key)) return false;
|
||||
const nextValue = normalizeVisualSetting(key, value);
|
||||
if (!allowed[key]?.has(nextValue)) return false;
|
||||
if (visualSettings[key] === nextValue) return true;
|
||||
visualSettings = { ...visualSettings, [key]: nextValue };
|
||||
saveVisualSettings();
|
||||
if (key === "details") resizeRequested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setVisualSettings(patch) {
|
||||
let changed = false;
|
||||
const next = { ...visualSettings };
|
||||
for (const [key, value] of Object.entries(patch || {})) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaults, key)) continue;
|
||||
const nextValue = normalizeVisualSetting(key, value);
|
||||
if (!allowed[key]?.has(nextValue) || next[key] === nextValue) continue;
|
||||
next[key] = nextValue;
|
||||
changed = true;
|
||||
if (key === "details") resizeRequested = true;
|
||||
}
|
||||
if (!changed) return false;
|
||||
visualSettings = next;
|
||||
saveVisualSettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderQualityTier() { return visualSettings.details; }
|
||||
function shadowQualityTier() { return visualSettings.shadows; }
|
||||
function effectsQualityTier() { return visualSettings.effects; }
|
||||
function simulationQualityTier() { return "high"; }
|
||||
function dprScaleValue() { return dprScaleForDetails(visualSettings.details); }
|
||||
function consumeResizeRequest() {
|
||||
const v = resizeRequested;
|
||||
resizeRequested = false;
|
||||
|
|
@ -142,8 +212,9 @@
|
|||
const topSelf = entries.slice().sort((a, b) => b.selfAvg - a.selfAvg).slice(0, 12);
|
||||
return {
|
||||
enabled: debugEnabled,
|
||||
tier,
|
||||
dprScale,
|
||||
tier: visualSettings.details,
|
||||
visual: visualSettingsValue(),
|
||||
dprScale: dprScaleValue(),
|
||||
frameCount,
|
||||
frameAvg: Number(frameAvg.toFixed(2)),
|
||||
entries: topInclusive,
|
||||
|
|
@ -158,6 +229,13 @@
|
|||
beginFrame,
|
||||
endFrame,
|
||||
renderQualityTier,
|
||||
shadowQualityTier,
|
||||
effectsQualityTier,
|
||||
simulationQualityTier,
|
||||
visualLevel,
|
||||
visualSettings: visualSettingsValue,
|
||||
setVisualSetting,
|
||||
setVisualSettings,
|
||||
dprScale: dprScaleValue,
|
||||
consumeResizeRequest,
|
||||
setMetric,
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@
|
|||
// segment editors. Low-level editor pointer behavior remains in placement log.
|
||||
(function (global) {
|
||||
const MODE_LABELS = Object.freeze({
|
||||
line: "📏 直線",
|
||||
free: "✎ 自由描画",
|
||||
erase: "⌫ 消しゴム",
|
||||
line: "\uD83D\uDCCF \u76F4\u7DDA",
|
||||
free: "\u270E \u81EA\u7531\u63CF\u753B",
|
||||
erase: "\u232B \u6D88\u3057\u30B4\u30E0",
|
||||
});
|
||||
const TEMPLATE_LABELS = Object.freeze({
|
||||
bar: "┃ 棒",
|
||||
cross: "+ 十字",
|
||||
circle: "○ 円",
|
||||
bar: "\u2503 \u68D2",
|
||||
cross: "\uFF0B \u5341\u5B57",
|
||||
circle: "\u25CB \u5186",
|
||||
});
|
||||
const ACTION_LABELS = Object.freeze({
|
||||
undo: "↶ 一つ戻す",
|
||||
clear: "🗑 全消し",
|
||||
undo: "\u21B6 \u4E00\u3064\u623B\u3059",
|
||||
clear: "\uD83D\uDDD1 \u5168\u6D88\u3057",
|
||||
});
|
||||
function templateSegments(name) {
|
||||
if (name === "bar") return [[-130, 0, 130, 0]];
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/placement-collision
|
||||
// Single facade for placement, copy, and pointer hit testing. Existing callers
|
||||
// should use this instead of reaching into raw footprint helpers directly.
|
||||
(function (global) {
|
||||
function footprints() { return global.TarinaiCollisionFootprints || null; }
|
||||
|
||||
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
||||
return footprints()?.hitTestItem?.(worldRef, item, x, y, opts) || { hit: false, distance: Infinity };
|
||||
}
|
||||
|
||||
function placementRectsFor(worldRef, item) {
|
||||
return footprints()?.placementRectsFor?.(worldRef, item) || [];
|
||||
}
|
||||
|
||||
function isSoftPlacementIgnoredType(type) {
|
||||
return Boolean(footprints()?.isSoftPlacementIgnoredType?.(type));
|
||||
}
|
||||
|
||||
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
||||
return Boolean(footprints()?.itemPlacementOverlapBlocked?.(worldRef, item, opts));
|
||||
}
|
||||
|
||||
function rectsOverlap(a, b, margin = 0) {
|
||||
return Boolean(footprints()?.rectsOverlap?.(a, b, margin));
|
||||
}
|
||||
|
||||
function rectCircleOverlap(rect, cx, cy, radius, margin = 0) {
|
||||
return Boolean(footprints()?.rectCircleOverlap?.(rect, cx, cy, radius, margin));
|
||||
}
|
||||
|
||||
global.TarinaiPlacementCollisionSystem = Object.freeze({
|
||||
hitTestItem,
|
||||
placementRectsFor,
|
||||
isSoftPlacementIgnoredType,
|
||||
itemPlacementOverlapBlocked,
|
||||
rectsOverlap,
|
||||
rectCircleOverlap,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
49
js/render.js
49
js/render.js
|
|
@ -341,6 +341,10 @@ function getLightingState(targetWorld) {
|
|||
}
|
||||
|
||||
function projectedShadowParams(lighting, scale = 1) {
|
||||
const shadowTier = TARINAI_RENDER_GLOBAL.TarinaiPerf?.shadowQualityTier?.() || "high";
|
||||
if (shadowTier === "off") {
|
||||
return { dx: 0, dy: 0, skew: 0, flatness: 0.42, spread: 1, alpha: 0, color: "rgba(78, 65, 43, 0)" };
|
||||
}
|
||||
const edgeLight = clamp((1 - lighting.elevation) * 0.60 + lighting.goldenStrength * 0.40 + lighting.nightStrength * 0.55, 0, 1);
|
||||
const moonLit = lighting.nightStrength > 0.18;
|
||||
const lightEdge = moonLit
|
||||
|
|
@ -349,14 +353,15 @@ function projectedShadowParams(lighting, scale = 1) {
|
|||
const castDir = -lightEdge;
|
||||
const weather = world.weather || "sunny";
|
||||
const weatherAlpha = weather === "sunny" ? 1.12 : weather === "cloudy" ? 0.72 : 0.38;
|
||||
const length = lerp(0.62, 1.82, edgeLight) * scale;
|
||||
const qualityScale = shadowTier === "low" ? 0.42 : shadowTier === "mid" ? 0.72 : 1;
|
||||
const length = lerp(0.62, 1.82, edgeLight) * scale * qualityScale;
|
||||
return {
|
||||
dx: castDir * 12 * length,
|
||||
dy: 5.2 * length,
|
||||
skew: castDir * lerp(0.08, 0.34, edgeLight),
|
||||
flatness: lerp(0.42, 0.24, edgeLight),
|
||||
spread: lerp(0.98, 1.52, edgeLight),
|
||||
alpha: clamp((0.16 + edgeLight * 0.09 - lighting.nightStrength * 0.04) * (0.92 + scale * 0.12) * weatherAlpha, 0.05, 0.32),
|
||||
alpha: clamp((0.16 + edgeLight * 0.09 - lighting.nightStrength * 0.04) * (0.92 + scale * 0.12) * weatherAlpha * qualityScale, 0.02, 0.32),
|
||||
color: lighting.nightStrength > 0.22 ? "rgba(42, 48, 72, 0.34)" : "rgba(78, 65, 43, 0.31)",
|
||||
};
|
||||
}
|
||||
|
|
@ -370,6 +375,7 @@ function imageVisibleBottomFromTop(img, top, drawH) {
|
|||
function drawImageProjectedShadow(ctx, img, x, groundY, drawW, drawH, params, options = {}) {
|
||||
if (!img) return false;
|
||||
const alpha = options.alpha ?? params.alpha;
|
||||
if (alpha <= 0) return false;
|
||||
const rx = Math.max(5, drawW * (options.widthScale ?? 1) * 0.30);
|
||||
const ry = Math.max(2, drawH * (options.heightScale ?? 1) * 0.055);
|
||||
ctx.save();
|
||||
|
|
@ -383,6 +389,7 @@ function drawImageProjectedShadow(ctx, img, x, groundY, drawW, drawH, params, op
|
|||
}
|
||||
|
||||
function drawProjectedShadow(ctx, x, y, rx, ry, params) {
|
||||
if (!params || params.alpha <= 0) return false;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = clamp(params.alpha || 0.10, 0.035, 0.16);
|
||||
ctx.fillStyle = params.color || "rgba(78, 65, 43, 0.24)";
|
||||
|
|
@ -390,6 +397,7 @@ function drawProjectedShadow(ctx, x, y, rx, ry, params) {
|
|||
ctx.ellipse(Math.round(x), Math.round(y), Math.max(3, rx * 0.82), Math.max(1.5, ry * 0.46), 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawRadialGlow(ctx, x, y, radius, color, alpha) {
|
||||
|
|
@ -1365,6 +1373,17 @@ function renderPerfEnd(end) {
|
|||
if (end) end();
|
||||
}
|
||||
|
||||
function renderVisualLevel(kind, fallback = "high") {
|
||||
return TARINAI_RENDER_GLOBAL.TarinaiPerf?.visualLevel?.(kind, fallback) || fallback;
|
||||
}
|
||||
|
||||
function visualEffectLimit(level) {
|
||||
if (level === "off") return 0;
|
||||
if (level === "low") return 36;
|
||||
if (level === "mid") return 96;
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
function fillScreenFallback(ctx, w, h, lighting) {
|
||||
const light = lighting?.light ?? 0.7;
|
||||
ctx.save();
|
||||
|
|
@ -1416,7 +1435,9 @@ function render() {
|
|||
drawGardenBed(sceneW, sceneH, lighting);
|
||||
}
|
||||
ctx.restore();
|
||||
drawLightRays(ctx, screenW, screenH, lighting);
|
||||
if (renderVisualLevel("effects") !== "off" && renderVisualLevel("details") !== "low") {
|
||||
drawLightRays(ctx, screenW, screenH, lighting);
|
||||
}
|
||||
renderPerfEnd(endBackground);
|
||||
|
||||
const visibleRect = visibleWorldRect(world, 180);
|
||||
|
|
@ -1481,6 +1502,11 @@ function render() {
|
|||
syncCarriedPlushieToOwner(it, world);
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const t of world.tarinai || []) {
|
||||
if (t && !t.dead && t.isTarinaiChampion && typeof t.drawChampionCrown === "function" && isEntityVisibleInRect(t, visibleRect)) {
|
||||
t.drawChampionCrown(ctx, world.time, lighting);
|
||||
}
|
||||
}
|
||||
for (const it of renderStack.ballLodgedPins || []) {
|
||||
if (!it || drawnAttachedPinIds.has(it.id)) continue;
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
|
|
@ -1496,16 +1522,23 @@ function render() {
|
|||
drawToolTargetHoverHighlight(ctx, world);
|
||||
|
||||
const endEffects = renderPerfBegin("render.draw.effects");
|
||||
const effectLevel = renderVisualLevel("effects");
|
||||
let drawnEffects = 0;
|
||||
const maxEffects = visualEffectLimit(effectLevel);
|
||||
for (const ef of world.effects) {
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
if (drawnEffects >= maxEffects) break;
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) {
|
||||
ef.draw(ctx);
|
||||
drawnEffects += 1;
|
||||
}
|
||||
}
|
||||
renderPerfEnd(endEffects);
|
||||
ctx.restore();
|
||||
|
||||
// Light affecting the whole visible scene, including characters and ground.
|
||||
const endLighting = renderPerfBegin("render.lightingWeather");
|
||||
drawAtmosphericLighting(ctx, screenW, screenH, lighting);
|
||||
if (world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
|
||||
if (renderVisualLevel("details") !== "low") drawAtmosphericLighting(ctx, screenW, screenH, lighting);
|
||||
if (effectLevel !== "off" && world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
|
||||
renderPerfEnd(endLighting);
|
||||
|
||||
const endUiOverlay = renderPerfBegin("render.uiOverlay");
|
||||
|
|
@ -1530,12 +1563,12 @@ function render() {
|
|||
ctx.fillStyle = "#2a241d";
|
||||
ctx.font = "bold 13px ui-rounded, sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
const seasonDay = world.seasonDayString?.() || `${world.day}日目`;
|
||||
const seasonDay = world.seasonDayString?.() || `${world.day}\u65E5\u76EE`;
|
||||
ctx.fillText(`${seasonDay} ${world.clockString()}`, hudX + 10, hudY + 20);
|
||||
ctx.font = "11px ui-rounded, sans-serif";
|
||||
ctx.fillStyle = "rgba(111,101,88,0.86)";
|
||||
const hudTemp = world.currentTemperature ?? world.updateTemperature?.(0) ?? 15;
|
||||
ctx.fillText(`${world.phaseName()} ${weatherLabel(world.weather)} ${Math.round(hudTemp)}℃`, hudX + 10, hudY + 38);
|
||||
ctx.fillText(`${world.phaseName()} ${weatherLabel(world.weather)} ${Math.round(hudTemp)}\u2103`, hudX + 10, hudY + 38);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
|
|
|
|||
496
js/save_codec.js
496
js/save_codec.js
|
|
@ -5,9 +5,13 @@
|
|||
const SaveSchema = global.TarinaiSaveSchema;
|
||||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_codec.js");
|
||||
if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for save_codec.js");
|
||||
const EXPORT_PREFIX = "\u305f\u308a";
|
||||
const RAW_CODEC = "\u751f";
|
||||
const DEFLATE_CODEC = "\u7e2e";
|
||||
const EXPORT_PREFIX = "\u305f";
|
||||
const RAW_CODEC = "raw";
|
||||
const DEFLATE_CODEC = "deflate";
|
||||
const DICT_CODEC = "dict";
|
||||
const DICT_DEFLATE_CODEC = "dict-deflate";
|
||||
const SAVE_HEADER_CHARS = "\u3089\u308A\u308B\u308C\u308D\u308F\u3092\u3093\u3083\u3085\u3087\u3063";
|
||||
const SAVE_HEADER_CODECS = Object.freeze([RAW_CODEC, DEFLATE_CODEC, DICT_CODEC, DICT_DEFLATE_CODEC]);
|
||||
const {
|
||||
BINARY_SCHEMA_VERSION,
|
||||
FIELD_IDS,
|
||||
|
|
@ -16,12 +20,15 @@
|
|||
PIN_TYPE_IDS,
|
||||
ROTATABLE_ITEM_TYPE_IDS,
|
||||
JSON_EXTRA_ITEM_TYPE_IDS,
|
||||
ITEM_TYPE_IDS,
|
||||
} = SaveSchema;
|
||||
const FIRE_TYPE_ID = ITEM_TYPE_IDS.indexOf("fire");
|
||||
const FLAME_FIRECRACKER_TYPE_ID = ITEM_TYPE_IDS.indexOf("flame_firecracker");
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
const SAVE_TEXT_BITS = 11;
|
||||
const SAVE_TEXT_BITS = 12;
|
||||
const SAVE_TEXT_BASE = 1 << SAVE_TEXT_BITS;
|
||||
const SAVE_TEXT_ALPHABET = (() => {
|
||||
const chars = [];
|
||||
|
|
@ -40,38 +47,14 @@
|
|||
for (const ch of SAVE_TEXT_ALPHABET) map.set(ch, index++);
|
||||
return map;
|
||||
})();
|
||||
if ([...SAVE_TEXT_ALPHABET].length !== SAVE_TEXT_BASE) throw new Error("save alphabet must contain 2048 Japanese chars");
|
||||
if ([...SAVE_TEXT_ALPHABET].length !== SAVE_TEXT_BASE) throw new Error("save alphabet must contain 4096 Japanese chars");
|
||||
|
||||
function writeVaruintToArray(out, value = 0) {
|
||||
let n = toSafeUInt(value);
|
||||
while (n >= 128) {
|
||||
out.push((n % 128) | 128);
|
||||
n = Math.floor(n / 128);
|
||||
}
|
||||
out.push(n & 127);
|
||||
}
|
||||
function readVaruintFromArray(bytes, start = 0) {
|
||||
let n = 0;
|
||||
let mul = 1;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const byte = bytes[start + i];
|
||||
if (byte == null) throw new Error("truncated Japanese save length");
|
||||
n += (byte & 127) * mul;
|
||||
if (!(byte & 128)) return { value: n, next: start + i + 1 };
|
||||
mul *= 128;
|
||||
}
|
||||
throw new Error("invalid Japanese save length");
|
||||
}
|
||||
|
||||
function jp2048Encode(bytes) {
|
||||
function jp4096Encode(bytes) {
|
||||
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||
const packet = [];
|
||||
writeVaruintToArray(packet, source.length);
|
||||
for (const byte of source) packet.push(byte & 255);
|
||||
let b = 0;
|
||||
let n = 0;
|
||||
let out = "";
|
||||
for (const byte of packet) {
|
||||
for (const byte of source) {
|
||||
b |= (byte & 255) << n;
|
||||
n += 8;
|
||||
while (n >= SAVE_TEXT_BITS) {
|
||||
|
|
@ -84,24 +67,90 @@
|
|||
return out;
|
||||
}
|
||||
|
||||
function jp2048Decode(text) {
|
||||
function jp4096Decode(text, remainder = 0) {
|
||||
let b = 0;
|
||||
let n = 0;
|
||||
const packet = [];
|
||||
const out = [];
|
||||
for (const ch of String(text || "").trim()) {
|
||||
const value = SAVE_TEXT_REVERSE.get(ch);
|
||||
if (value == null) continue;
|
||||
b |= value << n;
|
||||
n += SAVE_TEXT_BITS;
|
||||
while (n >= 8) {
|
||||
packet.push(b & 255);
|
||||
out.push(b & 255);
|
||||
b >>= 8;
|
||||
n -= 8;
|
||||
}
|
||||
}
|
||||
const { value: length, next } = readVaruintFromArray(packet, 0);
|
||||
if (packet.length < next + length) throw new Error("truncated Japanese save payload");
|
||||
return new Uint8Array(packet.slice(next, next + length));
|
||||
if ((Number(remainder) || 0) === 2 && out.length) out.pop();
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
function saveHeader(codec = RAW_CODEC, byteLength = 0) {
|
||||
const codecIndex = Math.max(0, SAVE_HEADER_CODECS.indexOf(codec));
|
||||
const remainder = ((Number(byteLength) || 0) % 3 + 3) % 3;
|
||||
return SAVE_HEADER_CHARS.charAt(codecIndex * 3 + remainder);
|
||||
}
|
||||
function parseSaveHeader(ch = "") {
|
||||
const index = SAVE_HEADER_CHARS.indexOf(String(ch || "").charAt(0));
|
||||
if (index < 0) throw new Error("unsupported Japanese save header");
|
||||
return {
|
||||
codec: SAVE_HEADER_CODECS[Math.floor(index / 3)] || RAW_CODEC,
|
||||
remainder: index % 3,
|
||||
};
|
||||
}
|
||||
|
||||
const BYTE_DICTIONARY = Object.freeze([
|
||||
[0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[100, 100, 100, 100],
|
||||
[0, 0, 100],
|
||||
[0, 0, 0, 100],
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
].map(row => Uint8Array.from(row)));
|
||||
const DICT_ESCAPE = 255;
|
||||
function dictionaryTransform(bytes) {
|
||||
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||
const out = [];
|
||||
for (let i = 0; i < source.length;) {
|
||||
let match = -1;
|
||||
for (let d = BYTE_DICTIONARY.length - 1; d >= 0; d--) {
|
||||
const entry = BYTE_DICTIONARY[d];
|
||||
if (i + entry.length > source.length) continue;
|
||||
let ok = true;
|
||||
for (let j = 0; j < entry.length; j++) {
|
||||
if (source[i + j] !== entry[j]) { ok = false; break; }
|
||||
}
|
||||
if (ok) { match = d; break; }
|
||||
}
|
||||
if (match >= 0) {
|
||||
out.push(DICT_ESCAPE, match + 1);
|
||||
i += BYTE_DICTIONARY[match].length;
|
||||
} else {
|
||||
const byte = source[i++];
|
||||
if (byte === DICT_ESCAPE) out.push(DICT_ESCAPE, 0);
|
||||
else out.push(byte);
|
||||
}
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
function dictionaryRestore(bytes) {
|
||||
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||
const out = [];
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const byte = source[i];
|
||||
if (byte !== DICT_ESCAPE) { out.push(byte); continue; }
|
||||
const code = source[++i];
|
||||
if (code == null) throw new Error("truncated Japanese save dictionary payload");
|
||||
if (code === 0) out.push(DICT_ESCAPE);
|
||||
else {
|
||||
const entry = BYTE_DICTIONARY[code - 1];
|
||||
if (!entry) throw new Error("invalid Japanese save dictionary token");
|
||||
for (const v of entry) out.push(v);
|
||||
}
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
|
||||
function toSafeUInt(value = 0) {
|
||||
|
|
@ -169,6 +218,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
let activeStringWriteMap = null;
|
||||
let activeStringReadList = null;
|
||||
function utf8Length(value = "") {
|
||||
return textEncoder.encode(String(value || "")).length;
|
||||
}
|
||||
function collectStringCounts(snapshot) {
|
||||
const counts = new Map();
|
||||
const add = value => {
|
||||
const s = String(value || "");
|
||||
if (s.length < 2 || utf8Length(s) < 4) return;
|
||||
counts.set(s, (counts.get(s) || 0) + 1);
|
||||
};
|
||||
add(snapshot?.w?.[8]);
|
||||
for (const row of snapshot?.t || []) add(row?.[1]);
|
||||
for (const row of snapshot?.i || []) {
|
||||
const typeId = row?.[0];
|
||||
const extra = row?.[4];
|
||||
if (typeId === 22) add(extra?.[0]);
|
||||
else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) add(JSON.stringify(extra || []));
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.filter(([, count]) => count > 1)
|
||||
.sort((a, b) => (b[0].length * b[1]) - (a[0].length * a[1]))
|
||||
.map(([value]) => value)
|
||||
.slice(0, 63);
|
||||
}
|
||||
function writeStringTable(writer, snapshot) {
|
||||
const list = collectStringCounts(snapshot);
|
||||
writer.u(list.length);
|
||||
activeStringWriteMap = list.length ? new Map(list.map((value, i) => [value, i])) : null;
|
||||
for (const value of list) writer.str(value);
|
||||
}
|
||||
function readStringTable(reader) {
|
||||
const count = reader.u();
|
||||
const list = [];
|
||||
for (let i = 0; i < count; i++) list.push(reader.str());
|
||||
activeStringReadList = list.length ? list : null;
|
||||
}
|
||||
function writeSaveString(writer, value = "") {
|
||||
const s = String(value || "");
|
||||
if (!activeStringWriteMap) return writer.str(s);
|
||||
const index = activeStringWriteMap.get(s);
|
||||
if (index == null) {
|
||||
writer.u(0);
|
||||
writer.str(s);
|
||||
} else {
|
||||
writer.u(index + 1);
|
||||
}
|
||||
}
|
||||
function readSaveString(reader) {
|
||||
if (!activeStringReadList) return reader.str();
|
||||
const index = reader.u();
|
||||
if (!index) return reader.str();
|
||||
return activeStringReadList[index - 1] || "";
|
||||
}
|
||||
|
||||
function clampToBits(value = 0, bits = 8) {
|
||||
const max = (2 ** Math.max(0, bits)) - 1;
|
||||
return Math.max(0, Math.min(max, toSafeUInt(value)));
|
||||
|
|
@ -288,8 +393,14 @@
|
|||
return out;
|
||||
}
|
||||
|
||||
function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); }
|
||||
function writeTimers(writer, timers = []) {
|
||||
function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); }
|
||||
function arrayHasValues(arr = [], defaults = []) {
|
||||
for (let i = 0; i < Math.max(arr?.length || 0, defaults.length); i++) {
|
||||
if (valuesDiffer(arr?.[i], defaults?.[i] || 0)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function writeTimers(writer, timers = []) {
|
||||
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100];
|
||||
let mask = 0;
|
||||
for (let i = 0; i < defaults.length; i++) if (valuesDiffer(timers?.[i], defaults[i])) mask |= (1 << i);
|
||||
|
|
@ -334,15 +445,28 @@
|
|||
function writeWorld(writer, w = []) {
|
||||
writeBitFields(writer, [w[0] || 0, w[1] || 0, w[2] || 0, w[4] || 0], [2, 3, 4, 3]);
|
||||
writer.u(w[3] || 0); // worldTick10
|
||||
writer.s(w[5] || 0); // lastBirthAt10 can be negative
|
||||
writer.u(w[6] || 1); // generation
|
||||
writer.u(w[7] || 0); // dead count
|
||||
writer.str(w[8] || ""); // worldSeed
|
||||
writer.u(w[9] || 0); // birthSerial
|
||||
let mask = 0;
|
||||
if (valuesDiffer(w[5], -9990)) mask |= 1;
|
||||
if (valuesDiffer(w[6], 1)) mask |= 2;
|
||||
if (valuesDiffer(w[7], 0)) mask |= 4;
|
||||
if (valuesDiffer(w[9], 0)) mask |= 8;
|
||||
writeBitFields(writer, [mask], [4]);
|
||||
if (mask & 1) writer.s(w[5] || 0); // lastBirthAt10 can be negative
|
||||
if (mask & 2) writer.u(w[6] || 1); // generation
|
||||
if (mask & 4) writer.u(w[7] || 0); // dead count
|
||||
writeSaveString(writer, w[8] || ""); // worldSeed
|
||||
if (mask & 8) writer.u(w[9] || 0); // birthSerial
|
||||
}
|
||||
function readWorld(reader) {
|
||||
const [field, ground, mood, weather] = readBitFields(reader, [2, 3, 4, 3]);
|
||||
return [field, ground, mood, reader.u(), weather, reader.s(), reader.u(), reader.u(), reader.str(), reader.u()];
|
||||
const tick = reader.u();
|
||||
const [mask] = readBitFields(reader, [4]);
|
||||
const lastBirthAt = (mask & 1) ? reader.s() : -9990;
|
||||
const generation = (mask & 2) ? reader.u() : 1;
|
||||
const deadCount = (mask & 4) ? reader.u() : 0;
|
||||
const seed = readSaveString(reader);
|
||||
const birthSerial = (mask & 8) ? reader.u() : 0;
|
||||
return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial];
|
||||
}
|
||||
|
||||
function seedPersonalityArray(seed = "", flags = 0) {
|
||||
|
|
@ -350,6 +474,29 @@
|
|||
if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0];
|
||||
return global.TarinaiSeedFactory.personalityArray(seed) || [100, 100, 100, 100];
|
||||
}
|
||||
function seedGeneticsArray(seed = "", flags = 0) {
|
||||
const zunchiSlave = Boolean((Number(flags) || 0) & ((1 << 5) | (1 << 6)));
|
||||
const genetics = global.TarinaiSeedFactory?.birthProfile?.(seed, { isZunchiSlave: zunchiSlave, zunchiSlaveLocked: zunchiSlave })?.genetics || {};
|
||||
return [
|
||||
Math.round((Number(genetics.lifeSpanMul) || 1) * 100),
|
||||
Math.round((Number(genetics.attackMul) || 1) * 100),
|
||||
Math.round((Number(genetics.speedMul) || 1) * 100),
|
||||
Math.round((Number(genetics.sizeMul) || 1) * 100),
|
||||
Math.round((Number(genetics.temperatureOffset) || 0) * 10),
|
||||
];
|
||||
}
|
||||
function writeSignedDiffList(writer, base = [], current = [], count = 0) {
|
||||
let mask = 0;
|
||||
for (let i = 0; i < count; i++) if (valuesDiffer(current?.[i], base?.[i])) mask |= (1 << i);
|
||||
writeBitFields(writer, [mask], [count]);
|
||||
for (let i = 0; i < count; i++) if (mask & (1 << i)) writer.s((current?.[i] || 0) - (base?.[i] || 0));
|
||||
}
|
||||
function readSignedDiffList(reader, base = [], count = 0) {
|
||||
const [mask] = readBitFields(reader, [count]);
|
||||
const out = Array.from({ length: count }, (_, i) => base?.[i] || 0);
|
||||
for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = (base?.[i] || 0) + reader.s();
|
||||
return out;
|
||||
}
|
||||
|
||||
function writeTarinai(writer, row = [], coordState = null) {
|
||||
const pinIdx = row[15] ?? -1;
|
||||
|
|
@ -359,101 +506,172 @@
|
|||
const genetics = Array.isArray(row[18]) ? row[18] : [];
|
||||
const fightStats = Array.isArray(row[19]) ? row[19] : [];
|
||||
const birthSeed = row[1] || "";
|
||||
const flags = row[0] || 0;
|
||||
writeBitFields(writer, [row[0] || 0], [10]);
|
||||
writer.str(birthSeed);
|
||||
writeSaveString(writer, birthSeed);
|
||||
const x = toSafeInt(row[2] || 0);
|
||||
const y = toSafeInt(row[3] || 0);
|
||||
const age = toSafeInt(row[4] || 0);
|
||||
const generation = toSafeInt(row[5] || 1);
|
||||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
writer.u(row[4] || 0);
|
||||
writer.u(row[5] || 1);
|
||||
writer.s(age - (coordState?.age || 0));
|
||||
writer.s(generation - (coordState?.generation || 1));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; coordState.age = age; coordState.generation = generation; }
|
||||
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
|
||||
writeFixedPacked(writer, row[9], 6, 6, 2);
|
||||
writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8);
|
||||
writeIndexList(writer, row[11]);
|
||||
writeRelationships(writer, row[12]);
|
||||
writeTimers(writer, row[13]);
|
||||
writeModes(writer, row[14]);
|
||||
const defaultPersonality = seedPersonalityArray(birthSeed, flags);
|
||||
const defaultTimers = [0, 0, 0, 0, 0, 0, 0, 0, 100];
|
||||
let bodyMask = 0;
|
||||
if (arrayHasValues(row[9], [0, 0, 0, 0, 0, 0])) bodyMask |= 1;
|
||||
if (arrayHasValues(row[10], defaultPersonality)) bodyMask |= 2;
|
||||
if ((row[11] || []).length) bodyMask |= 4;
|
||||
if ((row[12] || []).length) bodyMask |= 8;
|
||||
if (arrayHasValues(row[13], defaultTimers)) bodyMask |= 16;
|
||||
if (arrayHasValues(row[14], [0, 0, 0])) bodyMask |= 32;
|
||||
writeBitFields(writer, [bodyMask], [6]);
|
||||
if (bodyMask & 1) writeFixedPacked(writer, row[9], 6, 6, 2);
|
||||
if (bodyMask & 2) writeFixedDiffPacked(writer, defaultPersonality, row[10], 4, 8);
|
||||
if (bodyMask & 4) writeIndexList(writer, row[11]);
|
||||
if (bodyMask & 8) writeRelationships(writer, row[12]);
|
||||
if (bodyMask & 16) writeTimers(writer, row[13]);
|
||||
if (bodyMask & 32) writeModes(writer, row[14]);
|
||||
let tailMask = 0;
|
||||
if (pinIdx >= 0) tailMask |= 1;
|
||||
if (nestIdx >= 0) tailMask |= 2;
|
||||
if (stateId || stateTarget >= 0) tailMask |= 4;
|
||||
if (genetics.length) tailMask |= 8;
|
||||
if (fightStats.length) tailMask |= 16;
|
||||
if (arrayHasValues(genetics, seedGeneticsArray(birthSeed, flags))) tailMask |= 8;
|
||||
if (arrayHasValues(fightStats, [0, 0, 0])) tailMask |= 16;
|
||||
writeBitFields(writer, [tailMask], [5]);
|
||||
if (tailMask & 1) writer.u(pinIdx);
|
||||
if (tailMask & 2) writer.u(nestIdx);
|
||||
if (tailMask & 4) { writeBitFields(writer, [stateId], [1]); writer.s(stateTarget); }
|
||||
if (tailMask & 8) for (let i = 0; i < 5; i++) writer.s(genetics[i] ?? (i === 4 ? 0 : 100));
|
||||
if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); }
|
||||
if (tailMask & 8) writeSignedDiffList(writer, seedGeneticsArray(birthSeed, flags), genetics, 5);
|
||||
if (tailMask & 16) writeSignedDiffList(writer, [0, 0, 0], fightStats, 3);
|
||||
}
|
||||
function readTarinai(reader, coordState = null) {
|
||||
const flags = readBitFields(reader, [10])[0];
|
||||
const birthSeed = reader.str();
|
||||
const birthSeed = readSaveString(reader);
|
||||
const x = (coordState?.x || 0) + reader.s();
|
||||
const y = (coordState?.y || 0) + reader.s();
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
const age = reader.u();
|
||||
const generation = reader.u();
|
||||
const age = (coordState?.age || 0) + reader.s();
|
||||
const generation = (coordState?.generation || 1) + reader.s();
|
||||
if (coordState) { coordState.x = x; coordState.y = y; coordState.age = age; coordState.generation = generation; }
|
||||
const [hunger, energy, sleepPressure] = readBitFields(reader, [8, 8, 8]);
|
||||
const needs = readFixedPacked(reader, 6, 6, 2);
|
||||
const current = readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8);
|
||||
const parents = readIndexList(reader);
|
||||
const relationships = readRelationships(reader);
|
||||
const timers = readTimers(reader);
|
||||
const modes = readModes(reader);
|
||||
const [bodyMask] = readBitFields(reader, [6]);
|
||||
const needs = (bodyMask & 1) ? readFixedPacked(reader, 6, 6, 2) : [0, 0, 0, 0, 0, 0];
|
||||
const current = (bodyMask & 2) ? readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8) : seedPersonalityArray(birthSeed, flags);
|
||||
const parents = (bodyMask & 4) ? readIndexList(reader) : [];
|
||||
const relationships = (bodyMask & 8) ? readRelationships(reader) : [];
|
||||
const timers = (bodyMask & 16) ? readTimers(reader) : [0, 0, 0, 0, 0, 0, 0, 0, 100];
|
||||
const modes = (bodyMask & 32) ? readModes(reader) : [0, 0, 0];
|
||||
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, [1])[0], reader.s()] : [0, -1];
|
||||
const genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s(), reader.s()] : [];
|
||||
const fightStats = (tailMask & 16) ? [reader.u(), reader.u(), reader.s()] : [];
|
||||
const genetics = (tailMask & 8) ? readSignedDiffList(reader, seedGeneticsArray(birthSeed, flags), 5) : [];
|
||||
const fightStats = (tailMask & 16) ? readSignedDiffList(reader, [0, 0, 0], 3) : [];
|
||||
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
|
||||
}
|
||||
|
||||
function writeItemExtra(writer, typeId, extra = []) {
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) {
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] ?? -1); writer.s(extra[3] ?? -1); writer.u(extra[4] || 0); writer.s(extra[5] ?? -1);
|
||||
writer.s(extra[0] || 0);
|
||||
writer.s(extra[1] || 0);
|
||||
let mask = 0;
|
||||
if ((extra[2] ?? -1) >= 0) mask |= 1;
|
||||
if ((extra[3] ?? -1) >= 0) mask |= 2;
|
||||
if (extra[4]) mask |= 4;
|
||||
if ((extra[5] ?? -1) >= 0) mask |= 8;
|
||||
if (valuesDiffer(extra[6], 0)) mask |= 16;
|
||||
writeBitFields(writer, [mask], [5]);
|
||||
if (mask & 1) writer.u(extra[2]);
|
||||
if (mask & 2) writer.u(extra[3]);
|
||||
if (mask & 8) writer.u(extra[5]);
|
||||
if (mask & 16) writer.s(extra[6] || 0);
|
||||
} else if (typeId === 0) { // grass
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] || 0); writer.u(extra[3] || 0);
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
let mask = 0;
|
||||
if (valuesDiffer(extra[2], 0)) mask |= 1;
|
||||
if (extra[3]) mask |= 2;
|
||||
writeBitFields(writer, [mask], [2]);
|
||||
if (mask & 1) writer.s(extra[2] || 0);
|
||||
} else if (typeId === 1) { // zunchi
|
||||
writer.u(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] || 0);
|
||||
writeBitFields(writer, [extra[0] || 0], [2]);
|
||||
writer.s(extra[1] || 0);
|
||||
if (valuesDiffer(extra[2], 0)) { writer.u(1); writer.s(extra[2] || 0); }
|
||||
else writer.u(0);
|
||||
} else if (typeId === 22) { // signboard
|
||||
writer.str(extra[0] || "");
|
||||
writeSaveString(writer, 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);
|
||||
writeBitFields(writer, [Math.max(0, Math.min(0x3f, Number(extra[0] ?? 3) | 0)), extra[1] ? 1 : 0], [6, 1]);
|
||||
} else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
|
||||
writer.str(JSON.stringify(extra || []));
|
||||
writeSaveString(writer, JSON.stringify(extra || []));
|
||||
} else if (typeId === 21) { // ball
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
} else if (typeId === 23) { // ant_nest
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
} else if (typeId === 24) { // ant_corpse
|
||||
writer.s(extra[0] || 0);
|
||||
} else if (typeId === 25) { // firecracker
|
||||
} else if (typeId === 25 || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) { // firecracker/flame/fire
|
||||
writer.s(extra[0] || 0);
|
||||
} else if (PIN_TYPE_IDS.has(typeId)) {
|
||||
writer.u(extra[0] || 0); writer.s(extra[1] ?? -1); writer.s(extra[2] || 0); writer.s(extra[3] || 0); writer.s(extra[4] || 0);
|
||||
const target = extra[1] ?? -1;
|
||||
let mask = 0;
|
||||
if (target >= 0) mask |= 1;
|
||||
if (valuesDiffer(extra[2], 0)) mask |= 2;
|
||||
if (valuesDiffer(extra[3], 0)) mask |= 4;
|
||||
if (valuesDiffer(extra[4], 0)) mask |= 8;
|
||||
writeBitFields(writer, [extra[0] || 0, mask], [2, 4]);
|
||||
if (mask & 1) writer.u(target);
|
||||
if (mask & 2) writer.s(extra[2] || 0);
|
||||
if (mask & 4) writer.s(extra[3] || 0);
|
||||
if (mask & 8) writer.s(extra[4] || 0);
|
||||
} else if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
|
||||
writer.s(extra[0] || 0); writer.u(extra[1] || 0);
|
||||
let mask = 0;
|
||||
if (valuesDiffer(extra[0], 0)) mask |= 1;
|
||||
if (extra[1]) mask |= 2;
|
||||
writeBitFields(writer, [mask], [2]);
|
||||
if (mask & 1) writer.s(extra[0] || 0);
|
||||
} else if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
writer.s(extra[0] || 0);
|
||||
if (valuesDiffer(extra[1], extra[0])) { writer.u(1); writer.s(extra[1] || 0); }
|
||||
else writer.u(0);
|
||||
}
|
||||
}
|
||||
function readItemExtra(reader, typeId) {
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) return [reader.s(), reader.s(), reader.s(), reader.s(), reader.u(), reader.s()];
|
||||
if (typeId === 0) return [reader.s(), reader.s(), reader.s(), reader.u()];
|
||||
if (typeId === 1) return [reader.u(), reader.s(), reader.s()];
|
||||
if (typeId === 22) return [reader.str()];
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) {
|
||||
const hp = reader.s();
|
||||
const maxHp = reader.s();
|
||||
const [mask] = readBitFields(reader, [5]);
|
||||
return [
|
||||
hp,
|
||||
maxHp,
|
||||
(mask & 1) ? reader.u() : -1,
|
||||
(mask & 2) ? reader.u() : -1,
|
||||
(mask & 4) ? 1 : 0,
|
||||
(mask & 8) ? reader.u() : -1,
|
||||
(mask & 16) ? reader.s() : 0,
|
||||
];
|
||||
}
|
||||
if (typeId === 0) {
|
||||
const growth = reader.s();
|
||||
const health = reader.s();
|
||||
const [mask] = readBitFields(reader, [2]);
|
||||
return [growth, health, (mask & 1) ? reader.s() : 0, (mask & 2) ? 1 : 0];
|
||||
}
|
||||
if (typeId === 1) {
|
||||
const [stage] = readBitFields(reader, [2]);
|
||||
const freshness = reader.s();
|
||||
return [stage, freshness, reader.u() ? reader.s() : 0];
|
||||
}
|
||||
if (typeId === 22) return [readSaveString(reader)];
|
||||
if (typeId === 6) return [reader.s()];
|
||||
if (typeId === 42) return [reader.u(), reader.u()];
|
||||
if (typeId === 42) return readBitFields(reader, [6, 1]);
|
||||
if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
|
||||
try {
|
||||
const parsed = JSON.parse(reader.str() || "[]");
|
||||
const parsed = JSON.parse(readSaveString(reader) || "[]");
|
||||
return (Array.isArray(parsed) || (parsed && typeof parsed === "object")) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
|
|
@ -462,10 +680,25 @@
|
|||
if (typeId === 21) return [reader.s(), reader.s()];
|
||||
if (typeId === 23) return [reader.s(), reader.s()];
|
||||
if (typeId === 24) return [reader.s()];
|
||||
if (typeId === 25) return [reader.s()];
|
||||
if (PIN_TYPE_IDS.has(typeId)) return [reader.u(), reader.s(), reader.s(), reader.s(), reader.s()];
|
||||
if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) return [reader.s(), reader.u()];
|
||||
if (SERVING_FOOD_TYPE_IDS.has(typeId)) return [reader.s(), reader.s()];
|
||||
if (typeId === 25 || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) return [reader.s()];
|
||||
if (PIN_TYPE_IDS.has(typeId)) {
|
||||
const [state, mask] = readBitFields(reader, [2, 4]);
|
||||
return [
|
||||
state,
|
||||
(mask & 1) ? reader.u() : -1,
|
||||
(mask & 2) ? reader.s() : 0,
|
||||
(mask & 4) ? reader.s() : 0,
|
||||
(mask & 8) ? reader.s() : 0,
|
||||
];
|
||||
}
|
||||
if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
|
||||
const [mask] = readBitFields(reader, [2]);
|
||||
return [(mask & 1) ? reader.s() : 0, (mask & 2) ? 1 : 0];
|
||||
}
|
||||
if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
|
||||
const remaining = reader.s();
|
||||
return [remaining, reader.u() ? reader.s() : remaining];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function itemCoordKey(row = []) {
|
||||
|
|
@ -546,57 +779,76 @@
|
|||
function encodeBinarySnapshot(snapshot) {
|
||||
if (!snapshot || snapshot.v !== Snapshot.version || snapshot.a !== "tj1") throw new Error("snapshot version mismatch");
|
||||
const writer = new BinaryWriter();
|
||||
writer.b(BINARY_SCHEMA_VERSION);
|
||||
writer.u(Snapshot.version);
|
||||
writeWorld(writer, snapshot.w || []);
|
||||
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||
writer.u(tarinaiRows.length);
|
||||
const tarCoordState = { x: 0, y: 0 };
|
||||
for (const row of tarinaiRows) writeTarinai(writer, row || [], tarCoordState);
|
||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||
writeItemBlocks(writer, itemRows);
|
||||
return writer.out();
|
||||
activeStringWriteMap = null;
|
||||
try {
|
||||
writer.b(BINARY_SCHEMA_VERSION);
|
||||
writer.u(Snapshot.version);
|
||||
writeStringTable(writer, snapshot);
|
||||
writeWorld(writer, snapshot.w || []);
|
||||
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
|
||||
writer.u(tarinaiRows.length);
|
||||
const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 };
|
||||
for (const row of tarinaiRows) writeTarinai(writer, row || [], tarCoordState);
|
||||
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
|
||||
writeItemBlocks(writer, itemRows);
|
||||
return writer.out();
|
||||
} finally {
|
||||
activeStringWriteMap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeBinarySnapshot(bytes) {
|
||||
const reader = new BinaryReader(bytes);
|
||||
const schema = reader.b();
|
||||
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||
const snapshotVersion = reader.u();
|
||||
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
||||
const w = readWorld(reader);
|
||||
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);
|
||||
if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save");
|
||||
return { v: Snapshot.version, a: "tj1", m: summaryFromPayload(w, t.length), w, t, i: items };
|
||||
activeStringReadList = null;
|
||||
try {
|
||||
const schema = reader.b();
|
||||
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||
const snapshotVersion = reader.u();
|
||||
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
|
||||
readStringTable(reader);
|
||||
const w = readWorld(reader);
|
||||
const tCount = reader.u();
|
||||
const t = [];
|
||||
const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 };
|
||||
for (let i = 0; i < tCount; i++) t.push(readTarinai(reader, tarCoordState));
|
||||
const items = readItemBlocks(reader);
|
||||
if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save");
|
||||
return { v: Snapshot.version, a: "tj1", m: summaryFromPayload(w, t.length), w, t, i: items };
|
||||
} finally {
|
||||
activeStringReadList = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function packBytesForSave(bytes) {
|
||||
let codec = RAW_CODEC;
|
||||
let packed = bytes;
|
||||
const candidates = [{ codec: RAW_CODEC, packed: bytes }];
|
||||
const dict = dictionaryTransform(bytes);
|
||||
if (dict.length < bytes.length) candidates.push({ codec: DICT_CODEC, packed: dict });
|
||||
try {
|
||||
const compressed = await compressBytes(bytes);
|
||||
if (compressed.length < bytes.length) { codec = DEFLATE_CODEC; packed = compressed; }
|
||||
if (compressed.length < bytes.length) candidates.push({ codec: DEFLATE_CODEC, packed: compressed });
|
||||
if (dict.length < bytes.length) {
|
||||
const dictCompressed = await compressBytes(dict);
|
||||
if (dictCompressed.length < bytes.length) candidates.push({ codec: DICT_DEFLATE_CODEC, packed: dictCompressed });
|
||||
}
|
||||
} catch (error) { console.warn("save compression fallback", error); }
|
||||
return { codec, packed };
|
||||
return candidates.sort((a, b) => a.packed.length - b.packed.length)[0];
|
||||
}
|
||||
|
||||
async function encodeSnapshot(snapshot) {
|
||||
const bytes = encodeBinarySnapshot(snapshot);
|
||||
const { codec, packed } = await packBytesForSave(bytes);
|
||||
return `${EXPORT_PREFIX}${codec}${jp2048Encode(packed)}`;
|
||||
return `${EXPORT_PREFIX}${saveHeader(codec, packed.length)}${jp4096Encode(packed)}`;
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
const raw = String(text || "").trim();
|
||||
if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported Japanese save text");
|
||||
const codec = raw.charAt(EXPORT_PREFIX.length) || RAW_CODEC;
|
||||
const header = parseSaveHeader(raw.charAt(EXPORT_PREFIX.length));
|
||||
const payload = raw.slice(EXPORT_PREFIX.length + 1);
|
||||
const packed = jp2048Decode(payload);
|
||||
const bytes = await decompressBytes(packed, codec);
|
||||
const packed = jp4096Decode(payload, header.remainder);
|
||||
const packedCodec = header.codec === DICT_DEFLATE_CODEC ? DEFLATE_CODEC : (header.codec === DICT_CODEC ? RAW_CODEC : header.codec);
|
||||
let bytes = await decompressBytes(packed, packedCodec);
|
||||
if (header.codec === DICT_CODEC || header.codec === DICT_DEFLATE_CODEC) bytes = dictionaryRestore(bytes);
|
||||
return decodeBinarySnapshot(bytes);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
// Owns stable save identifiers shared by snapshot_system.js and save_codec.js.
|
||||
// Current-format save identifiers only.
|
||||
(function (global) {
|
||||
const SNAPSHOT_VERSION = 16;
|
||||
const BINARY_SCHEMA_VERSION = 20;
|
||||
const SNAPSHOT_VERSION = 17;
|
||||
const BINARY_SCHEMA_VERSION = 22;
|
||||
|
||||
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
|
||||
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
|
|
@ -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", "robot_cleaner", "dry_ice", "stove", "glass_wall", "slave_chain", "golden_crown"
|
||||
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner", "dry_ice", "stove", "glass_wall", "slave_chain", "golden_crown", "fire", "flame_firecracker"
|
||||
]);
|
||||
|
||||
function typeIds(names) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
function metaKey(slot) { return `${META_PREFIX}${slot}`; }
|
||||
function writeSlot(slot, hashText, meta = null) {
|
||||
const hash = String(hashText || "").trim();
|
||||
if (!hash.startsWith("\u305f\u308a")) throw new Error("unsupported Japanese hash slot value");
|
||||
if (!hash.startsWith("\u305f")) throw new Error("unsupported Japanese hash slot value");
|
||||
global.localStorage?.setItem(slotKey(slot), hash);
|
||||
if (meta) global.localStorage?.setItem(metaKey(slot), JSON.stringify({ ...meta, savedAt: Date.now() }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,8 +568,8 @@ class Effect {
|
|||
const ellipsize = (text, maxW) => {
|
||||
let s = String(text || "");
|
||||
if (ctx.measureText(s).width <= maxW) return s;
|
||||
while (s.length > 1 && ctx.measureText(`${s}…`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}…`;
|
||||
while (s.length > 1 && ctx.measureText(`${s}\u2026`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}\u2026`;
|
||||
};
|
||||
const wrap = (text, maxW) => {
|
||||
const src = String(text || "").replace(/\s+/g, " ").trim() || " ";
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
if ((worldRef.time || 0) >= (worldRef.nextGrassBedExpiryCheckAt || 0)) {
|
||||
worldRef.nextGrassBedExpiryCheckAt = (worldRef.time || 0) + 5.0;
|
||||
const lifespan = Math.max(1, CONFIG.dayLength || 120); // ゲーム内24時間
|
||||
const lifespan = Math.max(1, CONFIG.dayLength || 120); // \u30B2\u30FC\u30E0\u518524\u6642\u9593
|
||||
let removedBeds = 0;
|
||||
for (const it of worldRef.items || []) {
|
||||
if (!it || it.dead || it.type !== "grass_bed") continue;
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
if ((worldRef.time || 0) - Number(it.createdAt || 0) < lifespan) continue;
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, it, {
|
||||
reason: "grass-bed-expired",
|
||||
userReason: "かんたんベッドが古くなって消えた",
|
||||
userReason: "\u304B\u3093\u305F\u3093\u30D9\u30C3\u30C9\u304C\u53E4\u304F\u306A\u3063\u3066\u6D88\u3048\u305F",
|
||||
wake: false,
|
||||
panicOwner: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
id: `st${index}`,
|
||||
liveToken: 1,
|
||||
currentPersonality: arrayToPersonality(row[10]),
|
||||
genetics: Array.isArray(row[18]) ? arrayToGenetics(row[18]) : profile.genetics,
|
||||
genetics: Array.isArray(row[18]) && row[18].length ? arrayToGenetics(row[18]) : profile.genetics,
|
||||
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),
|
||||
|
|
@ -294,7 +294,8 @@
|
|||
if (type === "ball") return [q(item.vx, 10), q(item.vy, 10)];
|
||||
if (type === "ant_nest") return [q(item.antCount, 1), q(item.queenSpawnAt, 10)];
|
||||
if (type === "ant_corpse") return [q(item.decayTimer, 10)];
|
||||
if (type === "firecracker") return [q(item.fuseTimer, 10)];
|
||||
if (type === "firecracker" || type === "flame_firecracker") return [q(item.fuseTimer, 10)];
|
||||
if (type === "fire") return [q(item.flameLife, 10)];
|
||||
if (typeof global.isPinType === "function" && global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
|
||||
if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) return [q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0];
|
||||
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
|
||||
|
|
@ -351,15 +352,17 @@
|
|||
global.TarinaiPhysicsBodySystem.invalidateItem(item, "physics-extra-missing");
|
||||
return;
|
||||
} else if (type === "duplicator") {
|
||||
item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.foodLabel ? global.foodLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
||||
item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.toolLabel ? global.toolLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
||||
} else if (type === "ball") {
|
||||
item.vx = u(extra[0], 10); item.vy = u(extra[1], 10);
|
||||
} else if (type === "ant_nest") {
|
||||
item.antCount = u(extra[0], 1); item.antWorkers = []; item.queenSpawnAt = u(extra[1], 10);
|
||||
} else if (type === "ant_corpse") {
|
||||
item.workerSprite = "ant_worker"; item.decayTimer = u(extra[0], 10);
|
||||
} else if (type === "firecracker") {
|
||||
} else if (type === "firecracker" || type === "flame_firecracker") {
|
||||
item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0);
|
||||
} else if (type === "fire") {
|
||||
item.flameLife = u(extra[0], 10, item.flameLife || 6);
|
||||
} else if (typeof global.isPinType === "function" && global.isPinType(type)) {
|
||||
item.pinState = enumValue(PIN_STATE_IDS, extra[0], "loose"); item.pinTargetId = tarinaiList[extra[1]]?.id || ""; item.pinAttachAngle = u(extra[2], 1000); item.pinAttachDistance = u(extra[3], 10); item.pinOffsetY = u(extra[4], 10);
|
||||
} else if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) {
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@
|
|||
}
|
||||
|
||||
function ownedStructureDestroyedLabel(structure) {
|
||||
if (structure?.type === "plushie") return "ぬいぐるみ";
|
||||
if (structure?.type === "grass_bed") return "かんたんベッド";
|
||||
return structure?.label || "自分のもの";
|
||||
if (structure?.type === "plushie") return "\u306C\u3044\u3050\u308B\u307F";
|
||||
if (structure?.type === "grass_bed") return "\u304B\u3093\u305F\u3093\u30D9\u30C3\u30C9";
|
||||
return structure?.label || "\u81EA\u5206\u306E\u3082\u306E";
|
||||
}
|
||||
|
||||
function shouldPanicOwnerOnStructureGone(structure, reason = "") {
|
||||
|
|
@ -56,8 +56,8 @@
|
|||
const owner = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
||||
if (!owner) return false;
|
||||
const label = ownedStructureDestroyedLabel(structure);
|
||||
const detail = `自分の${label}が壊された`;
|
||||
const panicReason = `自分の${label}を壊されてパニックになっている`;
|
||||
const detail = `\u81EA\u5206\u306E${label}\u304C\u58CA\u3055\u308C\u305F`;
|
||||
const panicReason = `\u81EA\u5206\u306E${label}\u3092\u58CA\u3055\u308C\u3066\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u3066\u3044\u308B`;
|
||||
const threat = breaker || {
|
||||
id: `${structure.id || "structure"}:destroyed`,
|
||||
x: Number(structure.x) || owner.x,
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
global.setBehaviorText(owner, {
|
||||
need: "safety",
|
||||
actionId: "panic_escape",
|
||||
actionLabel: "パニックになっている",
|
||||
actionLabel: "\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u3066\u3044\u308B",
|
||||
reasonText: panicReason,
|
||||
causeText: detail,
|
||||
target: threat,
|
||||
|
|
@ -99,8 +99,8 @@
|
|||
}
|
||||
owner.thought = panicReason;
|
||||
owner.behaviorLockTimer = Math.max(owner.behaviorLockTimer || 0, 1.2);
|
||||
worldRef.spawnBubble?.(owner.x, owner.y - (owner.radius || 20) * 1.20, "!?", "rgba(188,66,76,0.88)");
|
||||
worldRef.log?.(`${owner.name || "たりない"}は自分の${label}を壊されてパニックになった。`, "danger");
|
||||
worldRef.spawnBubble?.(owner.x, owner.y - (owner.radius || 20) * 1.20, "\uFF01\uFF1F", "rgba(188,66,76,0.88)");
|
||||
worldRef.log?.(`${owner.name || "\u305F\u308A\u306A\u3044"}\u306F\u81EA\u5206\u306E${label}\u3092\u58CA\u3055\u308C\u3066\u30D1\u30CB\u30C3\u30AF\u306B\u306A\u3063\u305F\u3002`, "danger");
|
||||
worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -133,6 +133,38 @@
|
|||
return released;
|
||||
}
|
||||
|
||||
|
||||
function removeDependentLinks(worldRef, target, reason = "support-removed") {
|
||||
if (!worldRef || !target || !target.id) return 0;
|
||||
const pb = global.TarinaiPhysicsBodySystem || null;
|
||||
if (!pb?.endpoint) return 0;
|
||||
let removed = 0;
|
||||
const removedIds = new Set([target.id]);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const link of worldRef.items || []) {
|
||||
if (!link || link.dead || (link.type !== "rope" && link.type !== "rod")) continue;
|
||||
const a = pb.endpoint(link, 0);
|
||||
const b = pb.endpoint(link, 1);
|
||||
const hit = (a && a.kind === "item" && removedIds.has(a.id)) || (b && b.kind === "item" && removedIds.has(b.id));
|
||||
if (!hit) continue;
|
||||
link.amount = 0;
|
||||
link.hp = 0;
|
||||
link.removedReason = String(reason || "support-removed");
|
||||
removedIds.add(link.id);
|
||||
removed += 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (removed) {
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markItemBucketsDirty?.("dependent-links-removed");
|
||||
worldRef.markSpatialDirty?.("dependent-links-removed");
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
function deleteStructure(worldRef, structure, opts = {}) {
|
||||
if (!structure || structure.dead) return false;
|
||||
const reason = opts.reason || "structure-deleted";
|
||||
|
|
@ -140,6 +172,7 @@
|
|||
const ownerPanic = shouldPanicOwnerOnStructureGone(structure, reason);
|
||||
const changed = markStructureGone(structure, reason);
|
||||
if (!changed) return false;
|
||||
removeDependentLinks(worldRef, structure, reason);
|
||||
if (ownerPanic && opts.panicOwner !== false) triggerOwnedStructureDestroyedPanic(worldRef, structure, opts.breaker || null, reason);
|
||||
markWorldAfterStructureChange(worldRef, reason);
|
||||
worldRef?.emit?.("structure:deleted", { structure, type: structure.type, reason });
|
||||
|
|
@ -165,6 +198,7 @@
|
|||
if (item.isStructure) return deleteStructure(worldRef, item, { reason, userReason: opts.userReason, wake: opts.wake, breaker: opts.breaker || null, panicOwner: opts.panicOwner });
|
||||
const changed = markLooseItemGone(item, reason);
|
||||
if (!changed) return false;
|
||||
removeDependentLinks(worldRef, item, reason);
|
||||
markWorldAfterStructureChange(worldRef, reason, item, Math.max(36, Number(item.r || item.radius || 24) + 18));
|
||||
worldRef?.emit?.("item:deleted", { item, type: item.type, reason });
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@
|
|||
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 (options.source === "poke" && owner) {
|
||||
const fromOwnerX = (Number(this.x || owner.x || 0) || 0) - (Number(owner.x || 0) || 0);
|
||||
const pointerSide = Math.sign((Number(this.x || 0) || 0) - (Number(sourceX || 0) || 0)) || Math.sign(fromOwnerX) || Number(owner.facing || owner.faceDir || 1) || 1;
|
||||
if (len < 18 || Math.abs(dx) < Math.abs(dy) * 0.42) {
|
||||
dx = pointerSide;
|
||||
dy = -0.42;
|
||||
len = Math.hypot(dx, dy);
|
||||
}
|
||||
}
|
||||
if (len < 0.001 && owner) {
|
||||
dx = Number(owner.facing || owner.faceDir || 1) || 1;
|
||||
dy = -0.34;
|
||||
|
|
@ -162,9 +171,14 @@
|
|||
dy = -0.24;
|
||||
len = Math.hypot(dx, dy);
|
||||
}
|
||||
const base = Math.max(520, Math.min(900, Number(options.speed || 650) || 650));
|
||||
const base = Math.max(520, Math.min(980, Number(options.speed || 650) || 650));
|
||||
vx = dx / len * base;
|
||||
vy = dy / len * base - Math.max(80, base * 0.18);
|
||||
if (options.source === "poke") {
|
||||
const side = Math.sign(vx) || Math.sign(dx) || Number(owner?.facing || owner?.faceDir || 1) || 1;
|
||||
vx = side * Math.max(Math.abs(vx), base * 0.58);
|
||||
vy = Math.min(vy, -base * 0.26);
|
||||
}
|
||||
speed = Math.hypot(vx, vy);
|
||||
} else if (speed < 520) {
|
||||
const scale = 520 / Math.max(1, speed);
|
||||
|
|
@ -172,8 +186,11 @@
|
|||
vy *= scale;
|
||||
speed = Math.hypot(vx, vy);
|
||||
}
|
||||
this.vx = clamp(vx, -980, 980);
|
||||
this.vy = clamp(vy, -980, 980);
|
||||
this.vx = clamp(vx, -1100, 1100);
|
||||
this.vy = clamp(vy, -1100, 980);
|
||||
this.prevX = this.x;
|
||||
this.prevY = this.y;
|
||||
this.detachedUntil = world ? Math.max(this.detachedUntil || 0, (Number(world.time || 0) || 0) + 0.35) : (this.detachedUntil || 0);
|
||||
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));
|
||||
|
|
@ -186,7 +203,7 @@
|
|||
this._heldByPlayer = false;
|
||||
if (owner) {
|
||||
owner.target = null;
|
||||
owner.thought = "ぬいぐるみが飛んでいった";
|
||||
owner.thought = "\u306C\u3044\u3050\u308B\u307F\u304C\u98DB\u3093\u3067\u3044\u3063\u305F";
|
||||
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);
|
||||
|
|
@ -198,7 +215,7 @@
|
|||
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] : [] });
|
||||
if (options.log !== false) world?.log?.("\u306C\u3044\u3050\u308B\u307F\u304C\u56DE\u8EE2\u3057\u306A\u304C\u3089\u98DB\u3093\u3067\u3044\u3063\u305F\u3002", "observe", { eventType: "plushie_fling", participants: owner ? [owner] : [] });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
|
|||
ctx.quadraticCurveTo(x, y, x + rr, y);
|
||||
}
|
||||
|
||||
function tarinaiChampionName(name = "") {
|
||||
const map = { "\u306F": "\u8987", "\u3046": "\u5B87", "\u3045": "\u30A5", "\u3063": "\uFF01" };
|
||||
return String(name || "").replace(/[\u306F\u3046\u3045\u3063]/g, ch => map[ch] || ch);
|
||||
}
|
||||
if (typeof window !== "undefined") window.tarinaiChampionName = tarinaiChampionName;
|
||||
if (typeof globalThis !== "undefined") globalThis.tarinaiChampionName = tarinaiChampionName;
|
||||
|
||||
class Tarinai { constructor(world, opts = {}) {
|
||||
if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory.prepareAddOptions) {
|
||||
opts = globalThis.TarinaiSeedFactory.prepareAddOptions(world, opts) || opts;
|
||||
|
|
@ -130,6 +137,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.intimidateTimer = opts.intimidateTimer ?? 0;
|
||||
this.intimidateTargetId = opts.intimidateTargetId || null;
|
||||
this.intimidatedTimer = opts.intimidatedTimer ?? 0;
|
||||
this.intimidationCooldown = opts.intimidationCooldown ?? 0;
|
||||
this.birthRitualTimer = opts.birthRitualTimer ?? 0;
|
||||
this.birthRitualMax = opts.birthRitualMax ?? 0;
|
||||
this.birthPartnerId = opts.birthPartnerId || null;
|
||||
|
|
@ -168,6 +176,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
applyZunchiSlavePersonality(this, { record: false, birth: false });
|
||||
this.visibleSpriteId = "zunchi_slave";
|
||||
}
|
||||
if (this.isTarinaiChampion) this.name = tarinaiChampionName(this.name);
|
||||
this.tempComfort = opts.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
this.feltTemperature = opts.feltTemperature ?? this.tempComfort;
|
||||
this.temperatureStatus = null;
|
||||
|
|
|
|||
|
|
@ -207,12 +207,12 @@ function createTemperatureComfortActionSpec() {
|
|||
need: "health",
|
||||
subNeed: "temperature",
|
||||
state: "seek_temperature",
|
||||
label: "快適な温度帯へ移動している",
|
||||
phrase: "快適な温度帯を探している",
|
||||
label: "\u5FEB\u9069\u306A\u6E29\u5EA6\u5E2F\u3078\u79FB\u52D5\u3057\u3066\u3044\u308B",
|
||||
phrase: "\u5FEB\u9069\u306A\u6E29\u5EA6\u5E2F\u3092\u63A2\u3057\u3066\u3044\u308B",
|
||||
text(t, world) {
|
||||
const felt = world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t?.x, t?.y, t) ?? t?.feltTemperature ?? t?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
const status = world?.temperatureStatusFor?.(felt, t) || t?.temperatureStatus || null;
|
||||
return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。";
|
||||
return status?.direction === "cold" ? "\u5BD2\u3044\u306E\u3067\u6E29\u307E\u308D\u3046\u3068\u3057\u3066\u3044\u308B\u3002" : "\u6691\u3044\u306E\u3067\u6DBC\u3082\u3046\u3068\u3057\u3066\u3044\u308B\u3002";
|
||||
},
|
||||
weight: 42,
|
||||
selectTarget(t, world, ctx = {}) {
|
||||
|
|
@ -226,7 +226,7 @@ function createTemperatureComfortActionSpec() {
|
|||
},
|
||||
start(t, world, ctx = {}) {
|
||||
const status = world?.temperatureStatusFor?.(world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t.x, t.y, t) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15), t);
|
||||
const label = status?.direction === "cold" ? "あたたかい場所へ移動している" : (status?.direction === "hot" ? "涼しい場所へ移動している" : this.label);
|
||||
const label = status?.direction === "cold" ? "\u3042\u305F\u305F\u304B\u3044\u5834\u6240\u3078\u79FB\u52D5\u3057\u3066\u3044\u308B" : (status?.direction === "hot" ? "\u6DBC\u3057\u3044\u5834\u6240\u3078\u79FB\u52D5\u3057\u3066\u3044\u308B" : this.label);
|
||||
const target = ctx.target ?? this.selectTarget(t, world, ctx);
|
||||
return moveToOrUse(t, target, "seek_temperature", label);
|
||||
},
|
||||
|
|
@ -235,7 +235,7 @@ function createTemperatureComfortActionSpec() {
|
|||
const status = world?.temperatureStatusFor?.(felt, t);
|
||||
if (status?.comfortable || (status?.discomfort || 0) <= 1.5) {
|
||||
if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { health: 10, safety: 8 }, "temperature");
|
||||
t.goIdle?.("快適な温度になった");
|
||||
t.goIdle?.("\u5FEB\u9069\u306A\u6E29\u5EA6\u306B\u306A\u3063\u305F");
|
||||
return "finished";
|
||||
}
|
||||
const target = t.target && !t.target.dead ? t.target : (ctx.target ?? this.selectTarget(t, world, { force: true, requireComfort: true }));
|
||||
|
|
@ -426,13 +426,17 @@ function createIntimidateEnemyActionSpec() {
|
|||
timerBacked: true,
|
||||
selectTarget(t) { return t.recentFightRival?.(80, 180) || null; },
|
||||
canStart(t, world, ctx = {}) {
|
||||
// 威嚇は通常の欲求行動としては選ばず、喧嘩判定時の30%分岐で実行する。
|
||||
// \u5A01\u5687\u306F\u901A\u5E38\u306E\u6B32\u6C42\u884C\u52D5\u3068\u3057\u3066\u306F\u9078\u3070\u305A\u3001\u55A7\u5629\u5224\u5B9A\u6642\u306E30%\u5206\u5C90\u3067\u5B9F\u884C\u3059\u308B\u3002
|
||||
if (!isCurrentBehaviorForced(t)) return false;
|
||||
if ((t.intimidateTimer || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false;
|
||||
return basicCanStartWithTarget.call(this, t, world, ctx);
|
||||
if ((t.intimidateTimer || 0) > 0.04 || (t.intimidationCooldown || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false;
|
||||
const target = ctx.target ?? this.selectTarget(t, world, ctx);
|
||||
if (!isLiveTarinaiEntity(target) || target === t) return false;
|
||||
if (target?.isTarinaiChampion || (target?.intimidationCooldown || 0) > 0.04) return false;
|
||||
return basicCanStartWithTarget.call(this, t, world, { ...ctx, target });
|
||||
},
|
||||
start(t, world, ctx = {}) {
|
||||
const rival = ctx.target ?? this.selectTarget(t, world, ctx);
|
||||
if (!isLiveTarinaiEntity(rival) || rival === t || rival.isTarinaiChampion) return false;
|
||||
t.setActionState?.("intimidate", { target: rival, reason: this.label, sleeping: false });
|
||||
t.intimidateTimer = Math.max(t.intimidateTimer || 0, 0.85);
|
||||
return true;
|
||||
|
|
@ -440,8 +444,8 @@ function createIntimidateEnemyActionSpec() {
|
|||
tick(t, world) {
|
||||
if ((t.intimidateTimer || 0) > 0.04) return true;
|
||||
const rival = currentBehaviorTarget(t) || t.recentFightRival?.(80, 180);
|
||||
if (!rival) return false;
|
||||
return world?.startIntimidation?.(t, rival) || true;
|
||||
if (!isLiveTarinaiEntity(rival) || rival === t || rival.isTarinaiChampion) return "finished";
|
||||
return world?.startIntimidation?.(t, rival) ? "finished" : false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,18 +152,10 @@
|
|||
return window.TarinaiStructureLifecycle.consumeGrassBedOnWake(worldRef, bed, this, reason) || false;
|
||||
},
|
||||
|
||||
seekTarget(state, target = null, reason = "") {
|
||||
return this.setActionState(state, { target, reason, sleeping: false });
|
||||
},
|
||||
|
||||
forceBehavior(actionId = "", opts = {}) {
|
||||
return queueForcedTarinaiBehavior(this, actionId, opts);
|
||||
},
|
||||
|
||||
currentBehaviorText() {
|
||||
return behaviorText(this);
|
||||
},
|
||||
|
||||
enterPanic(opts = {}) {
|
||||
if (this.dead) return false;
|
||||
const profile = this.personalityProfile ? this.personalityProfile() : { fear: 1 };
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
return /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
}
|
||||
|
||||
function normalizeBehaviorValue(input, tarinai = null, previous = null) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function actionTextForReason(action = null, tarinai = null) {
|
|||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
return /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
}
|
||||
|
||||
function forcedCausePhrase(source = "", fallback = "") {
|
||||
|
|
@ -101,7 +101,7 @@ function buildReasonText(chosenNeed, tiedNeeds, action, tarinai = null, world =
|
|||
if (action?.id === "seek_comfort_temperature") {
|
||||
const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null;
|
||||
return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。";
|
||||
return status?.direction === "cold" ? "\u5BD2\u3044\u306E\u3067\u6E29\u307E\u308D\u3046\u3068\u3057\u3066\u3044\u308B\u3002" : "\u6691\u3044\u306E\u3067\u6DBC\u3082\u3046\u3068\u3057\u3066\u3044\u308B\u3002";
|
||||
}
|
||||
const actionText = actionTextForReason(action, tarinai);
|
||||
const cause = causePhraseForReason(chosenNeed, action, tarinai, options);
|
||||
|
|
@ -144,6 +144,8 @@ function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {
|
|||
}
|
||||
|
||||
function composeTarinaiBehaviorText(tarinai, behavior = null, options = {}) {
|
||||
if (tarinai && (tarinai.burning || (tarinai.burnTimer || 0) > 0.02)) return globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
if (tarinai && ((tarinai.firePanicTimer || 0) > 0.02 || (tarinai.state === "panic" && tarinai.lastPanicCause === "fire_seen"))) return globalThis.TarinaiItemDynamicToolSystem?.fireSeenText || "\u708e\u3092\u898b\u3066\u9a5a\u3044\u3066\u3044\u308b\u3002";
|
||||
const b = behavior || (currentTarinaiBehavior(tarinai)) || null;
|
||||
if (!b) return "";
|
||||
const stored = String(b.text || b.reason || "").trim();
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ function continueBuildPlan(t, world, dt) {
|
|||
? { x: t.x, y: t.y - Math.max(28, (t.radius || 24) * 1.28) }
|
||||
: findStructureBuildSpot(t, world, plan.type);
|
||||
if (!spot) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "作る場所が見つからない");
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u4F5C\u308B\u5834\u6240\u304C\u898B\u3064\u304B\u3089\u306A\u3044");
|
||||
}
|
||||
consumeGrassMaterial(material, structureGrassStageCost(plan.type));
|
||||
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
||||
|
|
|
|||
|
|
@ -116,10 +116,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
|
|||
if (target.type === "zunchi" && (target.amount || 0) > 0) {
|
||||
const foodNeed = Number(t.needRaw?.food ?? t.needs?.food ?? 0) || 0;
|
||||
if (zunchiEatingBlocked(t, target, world)) return false;
|
||||
if (!t.isZunchiSlave && hasBetterFoodThanZunchiNearby(t, world, 240)) return false;
|
||||
const emergencyHunger = (t.hunger || 0) >= 104 || foodNeed >= 94;
|
||||
if (!emergencyHunger && !t.isZunchiSlave && target.stage === "fresh" && (target.age || 0) < 150 && foodNeed < 94 && (t.hunger || 0) < 104) return false;
|
||||
if (!t.isZunchiSlave && foodNeed < 90 && (t.hunger || 0) < 100) return false;
|
||||
if (!t.isZunchiSlave && hasBetterFoodThanZunchiNearby(t, world, 900)) return false;
|
||||
const servingScale = Number.isFinite(target.foodServingScale) ? target.foodServingScale : (target.toolSize === "large" ? 1.18 : (target.toolSize === "small" ? 0.82 : 1));
|
||||
const bite = Math.min(target.amount, (t.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale);
|
||||
if (!(bite > 0)) return false;
|
||||
|
|
|
|||
|
|
@ -8,24 +8,26 @@
|
|||
const { foodActionActive = false } = ctx;
|
||||
|
||||
if (!it || it.dead) return false;
|
||||
const ceremonialContact = it.type === "slave_chain" || it.type === "golden_crown";
|
||||
const d = it?.type === "duplicator" ? foodInteractionDistance(tarinai, it) : dist(tarinai, it);
|
||||
const contactReach = foodInteractionReach(tarinai, it, "food") + (it === tarinai.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0);
|
||||
const baseReach = foodInteractionReach(tarinai, it, ceremonialContact ? "medicine" : "food");
|
||||
const contactReach = baseReach + (it === tarinai.target && foodActionActive && it?.type !== "duplicator" ? (ceremonialContact ? 10 : 4) : 0);
|
||||
if (d > contactReach) return false;
|
||||
|
||||
if (it.type === "slave_chain" || it.type === "golden_crown") {
|
||||
const itemLabel = typeof toolLabel === "function" ? toolLabel(it.type) : (it.type === "slave_chain" ? "奴隷の鎖" : "黄金の冠");
|
||||
if (ceremonialContact) {
|
||||
const itemLabel = typeof toolLabel === "function" ? toolLabel(it.type) : (it.type === "slave_chain" ? "\u5974\u96B7\u306E\u9396" : "\u9EC4\u91D1\u306E\u51A0");
|
||||
if (it.type === "slave_chain") {
|
||||
tarinai.isTarinaiChampion = false;
|
||||
tarinai.tarinaiChampionSince = 0;
|
||||
tarinai.becomeZunchiSlave?.({ locked: true, forcedItem: true });
|
||||
tarinai.bubble("鎖", 2.4, "rgba(74,64,56,0.82)");
|
||||
tarinai.world?.log?.(`${tarinai.name}は${itemLabel}に触れてずんちどれいになった。`, "event", { participants: [tarinai], sound: false });
|
||||
tarinai.bubble("\u9396", 2.4, "rgba(74,64,56,0.82)");
|
||||
tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305A\u3093\u3061\u3069\u308C\u3044\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false });
|
||||
} else {
|
||||
tarinai.becomeTarinaiChampion?.({ force: true, forcePersonality: true, forcedItem: true });
|
||||
tarinai.bubble("王", 2.4, "rgba(166,112,22,0.84)");
|
||||
tarinai.world?.log?.(`${tarinai.name}は${itemLabel}に触れてたりない王になった。`, "event", { participants: [tarinai], sound: false });
|
||||
tarinai.bubble("\u738B", 2.4, "rgba(166,112,22,0.84)");
|
||||
tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305F\u308A\u306A\u3044\u738B\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false });
|
||||
}
|
||||
global.TarinaiStructureLifecycle?.deleteItem?.(tarinai.world, it, { reason: `consumed-${it.type}`, userReason: `${itemLabel}が使われた`, wake: false, panicOwner: false });
|
||||
global.TarinaiStructureLifecycle?.deleteItem?.(tarinai.world, it, { reason: `consumed-${it.type}`, userReason: `${itemLabel}\u304C\u4F7F\u308F\u308C\u305F`, wake: false, panicOwner: false });
|
||||
tarinai.world?.recordFamily?.(tarinai);
|
||||
tarinai.world?.markFamilyTreeDirty?.(`consume-${it.type}`);
|
||||
if (global.uiCache) global.uiCache.selectedSnapshot = "";
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@
|
|||
return now < (this._nestBoxEntrySmoothUntil || -Infinity) || ((this.nestFade || 0) > 0.01 && (this.nestFade || 0) < 0.985);
|
||||
},
|
||||
|
||||
abortNestBoxEntry(box = null, reason = "巣箱がいっぱいで入れない") {
|
||||
abortNestBoxEntry(box = null, reason = "\u5DE3\u7BB1\u304C\u3044\u3063\u3071\u3044\u3067\u5165\u308C\u306A\u3044") {
|
||||
const now = this.world?.time || 0;
|
||||
if (this.insideNestBoxId && (!box || this.insideNestBoxId === box.id)) this.leaveNestBox?.(box);
|
||||
if (!this.insideNestBoxId) this.nestFade = 0;
|
||||
|
|
@ -248,13 +248,28 @@
|
|||
return this.target === box || this.insideNestBoxId === box.id;
|
||||
},
|
||||
|
||||
enterNestBox(box, dt = 0.016) {
|
||||
enterNestBox(box, dt = 0.016, options = {}) {
|
||||
if (!box || box.dead || box.type !== "nest_box" || this.dead) return false;
|
||||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||
const occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||
let occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||
const alreadyInside = this.insideNestBoxId === box.id;
|
||||
const now = this.world?.time || 0;
|
||||
if (!alreadyInside && occupants.length >= capacity) return this.abortNestBoxEntry?.(box, "巣箱がいっぱいで入れない") || false;
|
||||
if (!alreadyInside && occupants.length >= capacity) {
|
||||
if (options?.forceDisplace) {
|
||||
const victim = occupants.find(t => t && t !== this && !t.dead && !t.isTarinaiChampion)
|
||||
|| occupants.find(t => t && t !== this && !t.dead);
|
||||
if (victim) {
|
||||
victim.leaveNestBox?.(box);
|
||||
victim.awakeLockTimer = Math.max(victim.awakeLockTimer || 0, 0.8);
|
||||
victim.fearTimer = Math.max(victim.fearTimer || 0, 0.22);
|
||||
victim.thought = "\u5DE3\u7BB1\u304B\u3089\u62BC\u3057\u51FA\u3055\u308C\u305F";
|
||||
victim.pinchThoughtTimer = Math.max(victim.pinchThoughtTimer || 0, 0.65);
|
||||
victim.goIdle?.("\u5DE3\u7BB1\u304B\u3089\u62BC\u3057\u51FA\u3055\u308C\u305F");
|
||||
occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : occupants.filter(t => t !== victim);
|
||||
}
|
||||
}
|
||||
if (occupants.length >= capacity) return this.abortNestBoxEntry?.(box, "\u5DE3\u7BB1\u304C\u3044\u3063\u3071\u3044\u3067\u5165\u308C\u306A\u3044") || false;
|
||||
}
|
||||
if (!alreadyInside) {
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
|
|
@ -392,7 +407,7 @@
|
|||
}
|
||||
this.sleeping = false;
|
||||
this.sleepSession = null;
|
||||
if (this.state === "sleep" || this.state === "seek_bed") this.goIdle?.("巣箱から出た");
|
||||
if (this.state === "sleep" || this.state === "seek_bed") this.goIdle?.("\u5DE3\u7BB1\u304B\u3089\u51FA\u305F");
|
||||
this.nestBoxStayUntil = -Infinity;
|
||||
this.nestBoxSlotIndex = -1;
|
||||
this._nestBoxEntrySmoothUntil = -Infinity;
|
||||
|
|
@ -450,7 +465,7 @@
|
|||
this._nestBoxSeekTargetId = targetBox.id;
|
||||
}
|
||||
if (!this.insideNestBoxId && global.TarinaiNestSleepSystem.sleepPlaceAvailableFor && !global.TarinaiNestSleepSystem.sleepPlaceAvailableFor(this.world, this, targetBox)) {
|
||||
return this.abortNestBoxEntry?.(targetBox, "巣箱がいっぱいで入れない") || null;
|
||||
return this.abortNestBoxEntry?.(targetBox, "\u5DE3\u7BB1\u304C\u3044\u3063\u3071\u3044\u3067\u5165\u308C\u306A\u3044") || null;
|
||||
}
|
||||
|
||||
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(targetBox) : { x: targetBox.x, y: targetBox.y };
|
||||
|
|
@ -484,6 +499,92 @@
|
|||
if (this.explosionDisease) this.recoverExplosionDisease("\u6c34\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
},
|
||||
|
||||
|
||||
igniteFire(source = null, force = false) {
|
||||
if (this.dead) return false;
|
||||
const already = Boolean(this.burning || (this.burnTimer || 0) > 0.02);
|
||||
if (already && !force) return false;
|
||||
const text = globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
this.burning = true;
|
||||
this.burnTimer = Math.max(Number(this.burnTimer || 0) || 0, 5.2 + deterministicRange(this.world, "tarinai-burn-duration", 0, 2.8, this, source || null));
|
||||
this.firePanicTimer = 0;
|
||||
this.lastPanicCause = "burning";
|
||||
this.lastPanicDetail = text;
|
||||
this.thought = text;
|
||||
this.sleeping = false;
|
||||
this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 0.8);
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 2.2);
|
||||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
||||
this.eatTimer = 0;
|
||||
this.target = null;
|
||||
this.burnWanderAngle = Number.isFinite(this.burnWanderAngle) ? this.burnWanderAngle : deterministicAngle(this.world, "tarinai-burn-angle", this, source || null);
|
||||
if (typeof this.enterPanic === "function") this.enterPanic({ threat: source, target: null, reason: text, fear: 2.2, wake: true, cause: "burning", surpriseTimer: 0.65, awakeLockTimer: 0.8 });
|
||||
else this.setActionState?.("panic", { target: null, reason: text, wake: true });
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(26, this.radius * 1.35), life: 0.26, color: "rgba(255,110,36,0.58)" }));
|
||||
this.world?.emit?.("fire:ignite", { tarinai: this, source });
|
||||
this.recordChangeCause?.("\u708e\u4e0a", "\u708e");
|
||||
return true;
|
||||
},
|
||||
|
||||
extinguishFire(reason = "") {
|
||||
if (!this.burning && !(this.burnTimer > 0)) return false;
|
||||
this.burning = false;
|
||||
this.burnTimer = 0;
|
||||
this.firePanicTimer = Math.max(this.firePanicTimer || 0, 0.25);
|
||||
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.35);
|
||||
if (reason) this.thought = reason;
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(22, this.radius * 1.2), life: 0.22, color: "rgba(108,192,236,0.60)" }));
|
||||
this.world?.emit?.("fire:extinguish", { tarinai: this, reason });
|
||||
return true;
|
||||
},
|
||||
|
||||
updateBurning(dt) {
|
||||
this.firePanicTimer = Math.max(0, Number(this.firePanicTimer || 0) - dt);
|
||||
if (!this.burning && !(this.burnTimer > 0)) return;
|
||||
const text = globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
this.burning = true;
|
||||
this.burnTimer = Math.max(0, Number(this.burnTimer || 0) - dt);
|
||||
this.lastPanicCause = "burning";
|
||||
this.lastPanicDetail = text;
|
||||
this.thought = text;
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.8);
|
||||
this.sleeping = false;
|
||||
this.eatTimer = 0;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 6.2, safety: dt * 3.0 });
|
||||
if (typeof this.damage === "function") this.damage(dt * 3.8, "\u708e");
|
||||
else this.energy = clamp((this.energy || 0) - dt * 0.12, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100));
|
||||
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.22);
|
||||
const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.radius || 18, 16));
|
||||
if (water) {
|
||||
globalThis.TarinaiItemDynamicToolSystem?.consumeWaterForFire?.(this.world, water, Math.max(10, (this.radius || 18) * 0.55));
|
||||
this.extinguishFire("\u6c34\u3067\u6d88\u706b\u3055\u308c\u305f");
|
||||
return;
|
||||
}
|
||||
const mul = globalThis.TarinaiItemDynamicToolSystem?.fireTempMultiplier?.(this.world, this.x, this.y, this) || 1;
|
||||
if (deterministicChance(this.world, "tarinai-fire-extinguish", dt * 0.016 * mul, this)) {
|
||||
this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f");
|
||||
return;
|
||||
}
|
||||
for (const viewer of [...(this.world?.nearbyTarinai?.(this.x, this.y, 190, true) || [])]) {
|
||||
if (!viewer || viewer === this || viewer.dead) continue;
|
||||
globalThis.TarinaiItemDynamicToolSystem?.panicTarinaiFromFire?.(viewer, this, this.world);
|
||||
}
|
||||
const contactRadius = Math.max(20, (this.radius || 18) * 1.1);
|
||||
for (const other of [...(this.world?.nearbyTarinai?.(this.x, this.y, contactRadius + 28, true) || [])]) {
|
||||
if (!other || other === this || other.dead) continue;
|
||||
if (distXY(this.x, this.y, other.x, other.y) <= contactRadius + Math.max(other.radius || 18, 12) * 0.72) other.igniteFire?.(this, false);
|
||||
}
|
||||
for (const ant of [...(this.world?.nearbyAnts?.(this.x, this.y, contactRadius + 18, true) || [])]) {
|
||||
if (!ant || ant.dead) continue;
|
||||
if (distXY(this.x, this.y, ant.x, ant.y) <= contactRadius + Math.max(ant.r || 4, 4)) globalThis.TarinaiItemDynamicToolSystem?.igniteAntByFire?.(ant, this, this.world);
|
||||
}
|
||||
for (const it of [...(this.world?.nearbyItems?.(this.x, this.y, contactRadius + 32, true) || [])]) {
|
||||
if (!it || it.dead || it.type !== "grass") continue;
|
||||
if (distXY(this.x, this.y, it.x, it.y) <= contactRadius + Math.max(it.r || 16, 12)) globalThis.TarinaiItemDynamicToolSystem?.igniteGrassByFire?.(it, this, this.world, dt);
|
||||
}
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f");
|
||||
},
|
||||
|
||||
updateSpecialDiseases(dt) {
|
||||
this.sleepDiseaseCooldown = Math.max(0, (this.sleepDiseaseCooldown || 0) - dt);
|
||||
this.fightDiseaseCooldown = Math.max(0, (this.fightDiseaseCooldown || 0) - dt);
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ function processForcedBehaviorQueue(tarinai, world, needs) {
|
|||
}
|
||||
|
||||
function effectCauseTextLooksStale(text = "") {
|
||||
return /の効果$/.test(String(text || "").trim().replace(/[。!?]+$/, ""));
|
||||
return /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
}
|
||||
|
||||
function setBehaviorText(tarinai, { need = null, actionId = null, actionLabel = null, reasonText = null, reason = null, target = undefined, subNeed = "", phase = "active", source = "state", forced = null, forcedId = "", sourceReasonText = "", forcedReasonText = "", causeText = "" } = {}) {
|
||||
|
|
|
|||
|
|
@ -145,9 +145,18 @@
|
|||
|
||||
becomeTarinaiChampion(options = {}) {
|
||||
if (!this.tarinaiChampionEligible?.() && !options.force) return false;
|
||||
if (this.isTarinaiChampion && !options.forcePersonality) return false;
|
||||
if (this.isTarinaiChampion && !options.forcePersonality) {
|
||||
if (typeof tarinaiChampionName === "function") this.name = tarinaiChampionName(this.name);
|
||||
return false;
|
||||
}
|
||||
this.isTarinaiChampion = true;
|
||||
this.tarinaiChampionSince = Number.isFinite(this.tarinaiChampionSince) && this.tarinaiChampionSince > 0 ? this.tarinaiChampionSince : (this.world?.time || 0);
|
||||
if (typeof tarinaiChampionName === "function") {
|
||||
this.name = tarinaiChampionName(this.name);
|
||||
} else {
|
||||
const map = { "\u306F": "\u8987", "\u3046": "\u5B87", "\u3045": "\u30A5", "\u3063": "\uFF01" };
|
||||
this.name = String(this.name || "").replace(/[\u306F\u3046\u3045\u3063]/g, ch => map[ch] || ch);
|
||||
}
|
||||
ensurePersonality(this);
|
||||
const boosted = [];
|
||||
for (const key of ["aggression", "openness", "sociability"]) {
|
||||
|
|
@ -159,7 +168,7 @@
|
|||
}
|
||||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
||||
this.focusPulseTimer = Math.max(this.focusPulseTimer || 0, 1.0);
|
||||
if (boosted.length) this.recordChangeCause?.("たりない王者", boosted.join("・"), { value: +1 });
|
||||
if (boosted.length) this.recordChangeCause?.("\u305F\u308A\u306A\u3044\u738B\u8005", boosted.join("\u30FB"), { value: +1 });
|
||||
this.world?.recordFamily?.(this);
|
||||
return true;
|
||||
},
|
||||
|
|
@ -454,7 +463,7 @@
|
|||
},
|
||||
|
||||
lowHealthSprite() {
|
||||
// 体力比率による段階表示: 30%以下 cry、20%以下 weak、10%以下 stretch。
|
||||
// \u4F53\u529B\u6BD4\u7387\u306B\u3088\u308B\u6BB5\u968E\u8868\u793A: 30%\u4EE5\u4E0B cry\u300120%\u4EE5\u4E0B weak\u300110%\u4EE5\u4E0B stretch\u3002
|
||||
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100);
|
||||
const energyRatio = (Number(this.energy) || 0) / Math.max(1, Number(maxEnergy) || 100);
|
||||
if (energyRatio <= 0.10) return "stretch";
|
||||
|
|
|
|||
|
|
@ -98,10 +98,6 @@
|
|||
return this.radius;
|
||||
},
|
||||
|
||||
effectiveMaxEnergy() {
|
||||
return typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100);
|
||||
},
|
||||
|
||||
energyRatio() {
|
||||
return typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(this) : clamp((this.energy || 0) / Math.max(1, this.maxEnergy || 100), 0, 1);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ function foodInteractionDistance(tarinai, item) {
|
|||
}
|
||||
|
||||
function foodInteractionReach(tarinai, item, role = "food") {
|
||||
if (item?.type === "slave_chain" || item?.type === "golden_crown") {
|
||||
// \u738B\u51A0\u30FB\u9396\u306F\u7DDA\u753B/\u6A2A\u9577\u306E\u5916\u89B3\u306A\u306E\u3067\u3001\u901A\u5E38\u306E\u98DF\u7269\u5224\u5B9A\u3088\u308A\u898B\u305F\u76EE\u57FA\u6E96\u3067\u5E83\u304F\u53D6\u308B\u3002
|
||||
// \u4E2D\u5FC3\u70B9\u304C\u5C11\u3057\u96E2\u308C\u3066\u3044\u3066\u3082\u3001\u305F\u308A\u306A\u3044\u306E\u4F53\u8868\u3068\u30A2\u30A4\u30C6\u30E0\u5916\u89B3\u304C\u89E6\u308C\u305F\u6642\u70B9\u3067\u53D6\u5F97\u3067\u304D\u308B\u3002
|
||||
const bodyR = Math.max(16, Number(tarinai?.radius || 20) || 20);
|
||||
const itemR = Math.max(item.type === "slave_chain" ? 15 : 16, Number(item?.r || 0) || 0);
|
||||
const visualPad = item.type === "slave_chain" ? 14 : 12;
|
||||
return Math.max(46, bodyR * 0.94 + itemR * 1.28 + visualPad);
|
||||
}
|
||||
if (item?.type === "duplicator") return Math.max(30, (tarinai?.radius || 20) * 0.72 + (item?.r || 34) * 0.36);
|
||||
if (role === "drink") return Math.max(16, (tarinai?.radius || 20) * 0.58 + (item?.r || 12) * 0.40);
|
||||
return Math.max(13, (tarinai?.radius || 20) * 0.50 + (item?.r || 12) * 0.28);
|
||||
|
|
@ -98,42 +106,36 @@ function foodPriorityScore(tarinai, item, distance = 0) {
|
|||
|
||||
function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) {
|
||||
if (!tarinai || !world) return false;
|
||||
for (const it of world.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world.items || []) {
|
||||
if (!it || it.dead || it.type === "zunchi") continue;
|
||||
const source = world.nearbyItems?.(tarinai.x, tarinai.y, maxDist, true) || world.items || [];
|
||||
for (const it of source) {
|
||||
if (!it || it.dead) continue;
|
||||
const type = effectiveFoodTypeForItem(it);
|
||||
if (!type || type === "zunchi") continue;
|
||||
if (!roleMatches(it, "food")) 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))) {
|
||||
if (it.type === "grass" && (it.amount || 0) <= 0) continue;
|
||||
if (typeof tarinai.foodItemHasServingLeft === "function" && ["sweet", "food", "zunda_juice"].includes(type) && !tarinai.foodItemHasServingLeft(it)) continue;
|
||||
return true;
|
||||
if (it.type === "grass" && (it.amount || 0) <= 0) continue;
|
||||
if (it.type === "duplicator" || isServingFoodType(type) || ["sweet", "food", "zunda_juice"].includes(type)) {
|
||||
if (typeof tarinai.foodItemHasServingLeft === "function" && !tarinai.foodItemHasServingLeft(it)) continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function zunchiEatingBlocked(tarinai, item, world) {
|
||||
if (!tarinai || !item || item.type !== "zunchi") return false;
|
||||
const itemAge = Number(item.age || 0) || 0;
|
||||
const worldAge = Number.isFinite(item.producedAt) ? Math.max(0, (world?.time || 0) - item.producedAt) : itemAge;
|
||||
const protectedAge = Math.max(itemAge, worldAge);
|
||||
if ((Number(item.amount || 0) || 0) <= 0) return true;
|
||||
const protectedUntil = Number(item.eatProtectedUntil || 0) || 0;
|
||||
if (protectedUntil > 0 && (world?.time || 0) < protectedUntil) return true;
|
||||
const foodNeed = Number(tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
|
||||
const hunger = Number(tarinai.hunger || 0) || 0;
|
||||
// \u6392\u6cc4\u76f4\u5f8c\u306e\u305a\u3093\u3061\u306f\u89b3\u5bdf\u30fb\u63a5\u89e6\u5bfe\u8c61\u3067\u3042\u308a\u3001\u901a\u5e38\u98df\u3068\u3057\u3066\u5373\u6d88\u8cbb\u3055\u305b\u306a\u3044\u3002
|
||||
// \u305a\u3093\u3061\u3069\u308c\u3044\u3082\u6570\u79d2\u306f\u5f85\u3064\u3002\u901a\u5e38\u500b\u4f53\u306f\u6975\u9650\u98e2\u9913\u3067\u3082\u6700\u4f4e90\u79d2\u306f\u98df\u3079\u306a\u3044\u3002
|
||||
const emergency = hunger >= 104 || foodNeed >= 94;
|
||||
const grace = tarinai.isZunchiSlave ? (emergency ? 4 : 18) : (emergency ? 20 : 90);
|
||||
if (protectedAge < grace) return true;
|
||||
if (!emergency && !tarinai.isZunchiSlave && item.stage === "fresh" && protectedAge < 150 && foodNeed < 94 && hunger < 104) return true;
|
||||
// \u305A\u3093\u3061\u306F\u98DF\u7269\u306E\u6700\u4E0B\u4F4D\u5019\u88DC\u3002\u307B\u304B\u306B\u98DF\u3079\u7269\u304C\u3042\u308B\u9593\u3060\u3051\u4FDD\u8B77\u3057\u3001
|
||||
// \u307B\u304B\u306B\u98DF\u3079\u7269\u304C\u7121\u3044\u5834\u5408\u306F\u512A\u5148\u9806\u3069\u304A\u308A\u6700\u5F8C\u306E\u98DF\u6599\u3068\u3057\u3066\u98DF\u3079\u3089\u308C\u308B\u3002
|
||||
if (protectedUntil > 0 && (world?.time || 0) < protectedUntil && hasBetterFoodThanZunchiNearby(tarinai, world, 900)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function tarinaiTargetBlockedByObstacle(world, tarinai, item, role = "") {
|
||||
if (!world || !tarinai || !item) return false;
|
||||
// ずんだ餅だけは障害物越しでも食べに行く。
|
||||
// \u305A\u3093\u3060\u9905\u3060\u3051\u306F\u969C\u5BB3\u7269\u8D8A\u3057\u3067\u3082\u98DF\u3079\u306B\u884C\u304F\u3002
|
||||
if ((role === "food" || role === "medicine") && effectiveFoodTypeForItem(item) === "sweet") return false;
|
||||
return Boolean(world.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65)));
|
||||
}
|
||||
|
|
@ -164,8 +166,8 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
|
||||
};
|
||||
|
||||
// 食料・水・薬は、通常時から空間インデックスだけに依存しない。
|
||||
// 生成直後/移動直後のアイテムが nearbyFood のキャッシュから漏れても、距離と roleMatches で最終判定する。
|
||||
// \u98DF\u6599\u30FB\u6C34\u30FB\u85AC\u306F\u3001\u901A\u5E38\u6642\u304B\u3089\u7A7A\u9593\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3060\u3051\u306B\u4F9D\u5B58\u3057\u306A\u3044\u3002
|
||||
// \u751F\u6210\u76F4\u5F8C/\u79FB\u52D5\u76F4\u5F8C\u306E\u30A2\u30A4\u30C6\u30E0\u304C nearbyFood \u306E\u30AD\u30E3\u30C3\u30B7\u30E5\u304B\u3089\u6F0F\u308C\u3066\u3082\u3001\u8DDD\u96E2\u3068 roleMatches \u3067\u6700\u7D42\u5224\u5B9A\u3059\u308B\u3002
|
||||
if (foodLikeRole) {
|
||||
if (Array.isArray(world?.items)) for (const it of world.items) addCandidate(it);
|
||||
if (world?.itemsOfType) {
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
|
|||
tarinai.actionIdCooldowns = tarinai.actionIdCooldowns || {};
|
||||
const consumableAction = action.id === "eat_food" || action.id === "drink_water" || action.id === "use_medicine";
|
||||
if (consumableAction) {
|
||||
// 食料・水・薬は環境変化が速いので、失敗時も短い再評価だけにする。
|
||||
// \u98DF\u6599\u30FB\u6C34\u30FB\u85AC\u306F\u74B0\u5883\u5909\u5316\u304C\u901F\u3044\u306E\u3067\u3001\u5931\u6557\u6642\u3082\u77ED\u3044\u518D\u8A55\u4FA1\u3060\u3051\u306B\u3059\u308B\u3002
|
||||
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);
|
||||
|
|
@ -514,7 +514,7 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
|
|||
source: behavior?.source || "need",
|
||||
forced: behaviorIsForced(behavior),
|
||||
priority: behavior?.priority || 0,
|
||||
causeText: (behaviorIsForced(behavior) || !/の効果$/.test(String(behavior?.causeText || "").trim().replace(/[。!?]+$/, ""))) ? (behavior?.causeText || "") : "",
|
||||
causeText: (behaviorIsForced(behavior) || !/\u306E\u52B9\u679C$/.test(String(behavior?.causeText || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""))) ? (behavior?.causeText || "") : "",
|
||||
sourceReasonText: behavior?.sourceReasonText || "",
|
||||
forcedReasonText: behavior?.sourceReasonText || "",
|
||||
forcedRequest: behavior?.forcedRequest || null,
|
||||
|
|
@ -1045,19 +1045,6 @@ function resolveNeedsCore(dt) {
|
|||
return true;
|
||||
},
|
||||
|
||||
updateSunbath(dt) {
|
||||
return globalThis.TarinaiSunbathSystem.updateOne(this, dt) || false;
|
||||
},
|
||||
|
||||
applyCursorContactCare(dt) {
|
||||
return globalThis.TarinaiCursorCareSystem.updateOne(this, dt) || false;
|
||||
},
|
||||
|
||||
|
||||
applyEnvironment(dt) {
|
||||
return globalThis.TarinaiLocalEnvironmentSystem.updateOne(this, dt) || false;
|
||||
},
|
||||
|
||||
bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) {
|
||||
if (!options.force && this.world.time < this.nextBubbleAt) return false;
|
||||
this.nextBubbleAt = this.world.time + cooldown;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,63 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function tarinaiShadowsEnabled() {
|
||||
return (global.TarinaiPerf?.shadowQualityTier?.() || "high") !== "off";
|
||||
}
|
||||
|
||||
function drawTarinaiChampionCrown(ctx, tarinai, drawH, nestHideAlpha = 1, showDecor = true) {
|
||||
if (!ctx || !tarinai?.isTarinaiChampion || tarinai.dead) return false;
|
||||
const alpha = clamp(Number(nestHideAlpha ?? 1) || 0, 0, 1);
|
||||
if (alpha <= 0.01) return false;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
const crownW = Math.max(24, tarinai.radius * 1.34);
|
||||
const crownH = Math.max(14, tarinai.radius * 0.58);
|
||||
const cx = tarinai.x;
|
||||
const cy = tarinai.y - drawH * 0.64 - crownH * 0.55;
|
||||
if (showDecor) {
|
||||
ctx.shadowColor = "rgba(244, 184, 42, 0.78)";
|
||||
ctx.shadowBlur = 12;
|
||||
}
|
||||
ctx.fillStyle = "rgba(248, 201, 55, 0.96)";
|
||||
ctx.strokeStyle = "rgba(116, 78, 18, 0.92)";
|
||||
ctx.lineWidth = Math.max(1.4, crownH * 0.10);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.lineTo(cx - crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx - crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx, cy - crownH * 0.42);
|
||||
ctx.lineTo(cx + crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx + crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx + crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = "rgba(255, 243, 154, 0.92)";
|
||||
ctx.fillRect(cx - crownW * 0.38, cy + crownH * 0.16, crownW * 0.76, crownH * 0.16);
|
||||
ctx.fillStyle = "rgba(151, 88, 38, 0.82)";
|
||||
for (const px of [-0.28, 0, 0.28]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx + crownW * px, cy + crownH * 0.02, Math.max(1.4, crownH * 0.075), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
drawChampionCrown(ctx, t = 0, lighting = null) {
|
||||
if (!this.isTarinaiChampion || this.dead) return false;
|
||||
const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1;
|
||||
if (nestHideAlpha <= 0.01) return false;
|
||||
const sprite = this.visibleSpriteId || this.goodMode || this.type || "smile";
|
||||
const metrics = getImageMetrics(sprite) || getImageMetrics("smile") || {};
|
||||
const visualScale = this.effectiveScale ? this.effectiveScale() : this.scale;
|
||||
const drawH = (metrics?.h || 512) * visualScale * 0.30;
|
||||
return drawTarinaiChampionCrown(ctx, this, drawH, nestHideAlpha, shouldShowTarinaiDecor(this));
|
||||
},
|
||||
|
||||
drawSunbathPose(ctx, t, lighting = null) {
|
||||
if (this.dead) return true;
|
||||
const active = this.state === "sunbath" || (this.sunbathTimer || 0) > 0.04;
|
||||
|
|
@ -467,9 +523,11 @@
|
|||
const zImg = getRenderableImage("zunchi", "zunchi");
|
||||
if (zImg) {
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(46, 36, 24, 0.28)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = Math.max(1, drawH * 0.012);
|
||||
if (tarinaiShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(46, 36, 24, 0.28)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = Math.max(1, drawH * 0.012);
|
||||
}
|
||||
const zw = Math.max(16, drawW * 0.42);
|
||||
const zh = zw * 0.74;
|
||||
ctx.drawImage(zImg, -zw * 0.78, -dh * 0.50 - zh * 0.28, zw, zh);
|
||||
|
|
@ -481,42 +539,7 @@
|
|||
|
||||
ctx.restore();
|
||||
|
||||
if (this.isTarinaiChampion) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = nestHideAlpha;
|
||||
const crownW = Math.max(24, this.radius * 1.34);
|
||||
const crownH = Math.max(14, this.radius * 0.58);
|
||||
const cx = this.x;
|
||||
const cy = this.y - drawH * 0.64 - crownH * 0.55;
|
||||
if (showDecor) {
|
||||
ctx.shadowColor = "rgba(244, 184, 42, 0.78)";
|
||||
ctx.shadowBlur = 12;
|
||||
}
|
||||
ctx.fillStyle = "rgba(248, 201, 55, 0.96)";
|
||||
ctx.strokeStyle = "rgba(116, 78, 18, 0.92)";
|
||||
ctx.lineWidth = Math.max(1.4, crownH * 0.10);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.lineTo(cx - crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx - crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx, cy - crownH * 0.42);
|
||||
ctx.lineTo(cx + crownW * 0.17, cy + crownH * 0.05);
|
||||
ctx.lineTo(cx + crownW * 0.38, cy - crownH * 0.22);
|
||||
ctx.lineTo(cx + crownW * 0.48, cy + crownH * 0.26);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = "rgba(255, 243, 154, 0.92)";
|
||||
ctx.fillRect(cx - crownW * 0.38, cy + crownH * 0.16, crownW * 0.76, crownH * 0.16);
|
||||
ctx.fillStyle = "rgba(151, 88, 38, 0.82)";
|
||||
for (const px of [-0.28, 0, 0.28]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx + crownW * px, cy + crownH * 0.02, Math.max(1.4, crownH * 0.075), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// \u305F\u308A\u306A\u3044\u738B\u306E\u738B\u51A0\u306F\u3001\u643A\u884C\u306C\u3044\u3050\u308B\u307F\u3088\u308A\u524D\u9762\u306B\u51FA\u3059\u305F\u3081 render.js \u306E\u5F8C\u6BB5\u3067\u63CF\u753B\u3059\u308B\u3002
|
||||
|
||||
if (this.favorite) {
|
||||
ctx.save();
|
||||
|
|
|
|||
|
|
@ -198,6 +198,37 @@ function updatePanicBehavior(t, world, dt, needs) {
|
|||
const now = Number(world?.time || 0) || 0;
|
||||
if (!Number.isFinite(t.panicStartedAt) || t.state !== "panic") t.panicStartedAt = now;
|
||||
|
||||
if (t.lastPanicCause === "tarinai_champion") {
|
||||
const kingFleeReason = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
||||
const kingHardStop = Number(t.panicHardStopAt || 0) || 0;
|
||||
if (kingHardStop > 0 && now >= kingHardStop) {
|
||||
t.lastPanicCause = "";
|
||||
t.fearTimer = 0;
|
||||
t.lastNeedShockBreaker = null;
|
||||
return "finished";
|
||||
}
|
||||
const nest = t.target?.type === "nest_box" ? t.target : null;
|
||||
if (nest && !nest.dead) {
|
||||
if (typeof setBehaviorText === "function") {
|
||||
setBehaviorText(t, {
|
||||
need: "safety",
|
||||
actionId: "panic_escape",
|
||||
actionLabel: "\u9003\u3052\u3066\u3044\u308B",
|
||||
reasonText: kingFleeReason,
|
||||
target: nest,
|
||||
phase: "perform",
|
||||
source: "behavior",
|
||||
});
|
||||
}
|
||||
if (t.state !== "panic") t.setActionState?.("panic", { target: nest, reason: kingFleeReason, wake: true, sleeping: false });
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.34);
|
||||
if (dist(t, nest) <= Math.max(50, (t.radius || 20) + (nest.r || 24) + 18)) {
|
||||
t.enterNestBox?.(nest, dt, { forceDisplace: true, reason: kingFleeReason });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Survival needs must be able to break panic. Otherwise a stale fear state
|
||||
// can suppress eating/sleeping long enough to kill the colony.
|
||||
if (!realDanger && survivalNeedShouldCancelPanic(t, needs)) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,80 @@
|
|||
return attacker?.outgoingDamage ? attacker.outgoingDamage(raw) : raw;
|
||||
}
|
||||
|
||||
const TARINAI_KING_FLEE_REASON = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
||||
|
||||
function tarinaiIsChampionBreedingPartner(champion, candidate) {
|
||||
if (!champion || !candidate || champion === candidate) return false;
|
||||
if (champion.birthPartnerId && champion.birthPartnerId === candidate.id) return true;
|
||||
if (candidate.birthPartnerId && candidate.birthPartnerId === champion.id) return true;
|
||||
const championBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(champion) : null;
|
||||
const candidateBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(candidate) : null;
|
||||
const breedingIds = new Set(["approach_mate", "birth_ritual"]);
|
||||
if (breedingIds.has(championBehavior?.actionId) && champion.target === candidate) return true;
|
||||
if (breedingIds.has(candidateBehavior?.actionId) && candidate.target === champion) return true;
|
||||
if ((champion.birthRitualTimer || 0) > 0.04 && champion.target === candidate) return true;
|
||||
if ((candidate.birthRitualTimer || 0) > 0.04 && candidate.target === champion) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function nearestKingFleeNestBox(tarinai, maxDist = 460) {
|
||||
const worldRef = tarinai?.world;
|
||||
if (!tarinai || !worldRef) return null;
|
||||
const source = typeof worldRef.nearbyItems === "function" ? worldRef.nearbyItems(tarinai.x, tarinai.y, maxDist) : (worldRef.items || []);
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
for (const item of source || []) {
|
||||
if (!item || item.dead || item.type !== "nest_box") continue;
|
||||
const d = distXY(tarinai.x, tarinai.y, item.x, item.y);
|
||||
if (d > maxDist) continue;
|
||||
const padding = Math.max(16, (tarinai.radius || 20) * 0.72);
|
||||
if (typeof worldRef.targetBlockedByObstacle === "function" && worldRef.targetBlockedByObstacle(tarinai, item, padding)) continue;
|
||||
const occupants = typeof worldRef.nestBoxOccupants === "function" ? worldRef.nestBoxOccupants(item, Infinity) : [];
|
||||
const capacity = typeof worldRef.nestBoxCapacity === "function" ? worldRef.nestBoxCapacity(item) : 5;
|
||||
const fullnessPenalty = Math.max(0, occupants.length - capacity + 1) * 18;
|
||||
const score = d + fullnessPenalty;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
best = item;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function forceKingFlee(avoider, champion, dt = 0.016) {
|
||||
if (!avoider || !champion || avoider.dead) return false;
|
||||
const nest = nearestKingFleeNestBox(avoider);
|
||||
const target = nest || avoider.panicDestination?.(champion, true) || {
|
||||
x: avoider.x + (avoider.x - champion.x || 1) * 1.8,
|
||||
y: avoider.y + (avoider.y - champion.y || 0) * 1.8,
|
||||
};
|
||||
const now = avoider.world?.time || 0;
|
||||
avoider.lastNeedShockBreaker = champion;
|
||||
avoider.lastPanicCause = "tarinai_champion";
|
||||
avoider.lastPanicDetail = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u3044";
|
||||
avoider.panicStartedAt = now;
|
||||
avoider.panicHardStopAt = Math.max(avoider.panicHardStopAt || 0, now + 6.5);
|
||||
avoider.target = target;
|
||||
avoider.setActionState?.("panic", { target, reason: TARINAI_KING_FLEE_REASON, wake: true, sleeping: false });
|
||||
if (typeof setBehaviorText === "function") {
|
||||
setBehaviorText(avoider, {
|
||||
need: "safety",
|
||||
actionId: "panic_escape",
|
||||
actionLabel: "\u9003\u3052\u3066\u3044\u308B",
|
||||
reasonText: TARINAI_KING_FLEE_REASON,
|
||||
target,
|
||||
phase: "perform",
|
||||
source: "behavior",
|
||||
});
|
||||
}
|
||||
avoider.thought = TARINAI_KING_FLEE_REASON;
|
||||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 1.05 * avoider.personalityProfile().fear);
|
||||
if (nest && distXY(avoider.x, avoider.y, nest.x, nest.y) <= Math.max(50, (avoider.radius || 20) + (nest.r || 24) + 18)) {
|
||||
avoider.enterNestBox?.(nest, dt, { forceDisplace: true, reason: TARINAI_KING_FLEE_REASON });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
interactWithOthers(dt) {
|
||||
let closeCount = 0;
|
||||
|
|
@ -41,29 +115,33 @@
|
|||
const champion = this.isTarinaiChampion ? this : (o.isTarinaiChampion ? o : null);
|
||||
if (champion && this.isTarinaiChampion !== o.isTarinaiChampion) {
|
||||
const avoider = champion === this ? o : this;
|
||||
const nx = (avoider.x - champion.x) / d;
|
||||
const ny = (avoider.y - champion.y) / d;
|
||||
const avoidRange = Math.max(92, selfRadius + otherRadius + 34);
|
||||
const urgency = clamp(1 - d / avoidRange, 0, 1);
|
||||
const push = dt * (64 + urgency * 260);
|
||||
avoider.vx += nx * push;
|
||||
avoider.vy += ny * push;
|
||||
if (d <= selfRadius + otherRadius + 14) {
|
||||
avoider.vx += nx * dt * 190;
|
||||
avoider.vy += ny * dt * 190;
|
||||
champion.vx -= nx * dt * 8;
|
||||
champion.vy -= ny * dt * 8;
|
||||
avoider.surpriseTimer = Math.max(avoider.surpriseTimer || 0, 0.20);
|
||||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
|
||||
if (this.world.relationNotice?.(avoider.id, champion.id, "tarinai-champion-avoid", 18)) {
|
||||
this.world.log(`${avoider.name}はたりない王者の${champion.name}にぶつかりそうになって避けた。`, "relation", { participants: [avoider, champion], eventType: "avoid_relationship", sound: false });
|
||||
if (tarinaiIsChampionBreedingPartner(champion, avoider)) {
|
||||
avoider.adjustRelation?.(champion, dt * 0.035, -dt * 0.06, "tarinai_champion_partner");
|
||||
if (this !== champion) continue;
|
||||
} else {
|
||||
const nx = (avoider.x - champion.x) / d;
|
||||
const ny = (avoider.y - champion.y) / d;
|
||||
const avoidRange = Math.max(92, selfRadius + otherRadius + 34);
|
||||
const urgency = clamp(1 - d / avoidRange, 0, 1);
|
||||
const push = dt * (64 + urgency * 260);
|
||||
avoider.vx += nx * push;
|
||||
avoider.vy += ny * push;
|
||||
if (d <= selfRadius + otherRadius + 14) {
|
||||
avoider.vx += nx * dt * 190;
|
||||
avoider.vy += ny * dt * 190;
|
||||
champion.vx -= nx * dt * 8;
|
||||
champion.vy -= ny * dt * 8;
|
||||
avoider.surpriseTimer = Math.max(avoider.surpriseTimer || 0, 0.20);
|
||||
avoider.fearTimer = Math.max(avoider.fearTimer || 0, 0.32 * avoider.personalityProfile().fear);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(avoider, { safety: dt * 26, social: dt * 5 }, champion);
|
||||
if (this.world.relationNotice?.(avoider.id, champion.id, "tarinai-champion-avoid", 18)) {
|
||||
this.world.log(`${avoider.name}\u306F\u305F\u308A\u306A\u3044\u738B\u8005\u306E${champion.name}\u306B\u3076\u3064\u304B\u308A\u305D\u3046\u306B\u306A\u3063\u3066\u907F\u3051\u305F\u3002`, "relation", { participants: [avoider, champion], eventType: "avoid_relationship", sound: false });
|
||||
}
|
||||
}
|
||||
forceKingFlee(avoider, champion, dt);
|
||||
avoider.adjustRelation?.(champion, -dt * 0.025, dt * 0.18, "tarinai_champion_avoid");
|
||||
if (this !== champion) continue;
|
||||
}
|
||||
avoider.lastNeedShockBreaker = champion;
|
||||
avoider.thought = `${champion.name || "相手"}はたりない王者だ`;
|
||||
avoider.adjustRelation?.(champion, -dt * 0.025, dt * 0.18, "tarinai_champion_avoid");
|
||||
if (this !== champion) continue;
|
||||
}
|
||||
|
||||
const fightMochiContact = this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
|
||||
|
|
@ -210,7 +288,7 @@
|
|||
},
|
||||
|
||||
move(dt) {
|
||||
if (this.freezeSleepDiseaseMotion(dt)) return;
|
||||
if (!(this.burning || (this.burnTimer || 0) > 0.02) && this.freezeSleepDiseaseMotion(dt)) return;
|
||||
let ax = 0, ay = 0;
|
||||
const itemSpeedMul = this.itemSpeedMultiplier ? this.itemSpeedMultiplier() : 1;
|
||||
const baseSpeed = 46.5 * 1.5 * this.trait.speed * itemSpeedMul * (this.zunchiDisease ? 0.5 : 1) * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1);
|
||||
|
|
@ -349,6 +427,24 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const burningNow = Boolean(this.burning || (this.burnTimer || 0) > 0.02);
|
||||
if (burningNow) {
|
||||
const burnText = globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
this.state = "panic";
|
||||
this.target = null;
|
||||
this.lastPanicCause = "burning";
|
||||
this.lastPanicDetail = burnText;
|
||||
this.thought = burnText;
|
||||
this.burnTurnTimer = Math.max(0, Number(this.burnTurnTimer || 0) - dt);
|
||||
if (this.burnTurnTimer <= 0) {
|
||||
this.burnWanderAngle = deterministicAngle(this.world, "tarinai-burn-run-angle", this, Math.floor((this.world?.time || 0) * 5));
|
||||
this.burnTurnTimer = deterministicRange(this.world, "tarinai-burn-run-turn", 0.10, 0.34, this);
|
||||
} else {
|
||||
this.burnWanderAngle += deterministicRange(this.world, "tarinai-burn-run-jitter", -8.0, 8.0, this) * dt;
|
||||
}
|
||||
ax += Math.cos(this.burnWanderAngle || this.wanderAngle || 0) * baseSpeed * 3.15;
|
||||
ay += Math.sin(this.burnWanderAngle || this.wanderAngle || 0) * baseSpeed * 3.15;
|
||||
} else {
|
||||
if (this.state === "panic") {
|
||||
const targetDist = this.target && !this.target.dead ? distXY(this.x, this.y, this.target.x, this.target.y) : Infinity;
|
||||
const speed = Math.hypot(this.vx, this.vy);
|
||||
|
|
@ -417,7 +513,15 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (this.state === "panic" && this.lastPanicCause === "tarinai_champion" && this.target?.type === "nest_box") {
|
||||
const nestDist = distXY(this.x, this.y, this.target.x, this.target.y);
|
||||
if (nestDist <= Math.max(50, (this.radius || 20) + (this.target.r || 24) + 18)) {
|
||||
this.enterNestBox?.(this.target, dt, { forceDisplace: true, reason: TARINAI_KING_FLEE_REASON });
|
||||
return;
|
||||
}
|
||||
}
|
||||
let sign = (this.state === "panic" || this.state === "cursor_enemy") ? -1 : 1;
|
||||
if (this.state === "panic" && this.lastPanicCause === "tarinai_champion" && this.target?.type === "nest_box") sign = 1;
|
||||
if (this.target?.detour) sign = 1;
|
||||
let strength = 1;
|
||||
if (this.state === "follow_parent") {
|
||||
|
|
@ -473,6 +577,7 @@
|
|||
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.34;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.34;
|
||||
}
|
||||
}
|
||||
|
||||
this.vx += ax * dt;
|
||||
this.vy += ay * dt;
|
||||
|
|
@ -481,7 +586,7 @@
|
|||
this.vx *= Math.pow(moveFriction, dt * 8);
|
||||
this.vy *= Math.pow(moveFriction, dt * 8);
|
||||
|
||||
const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1) * (iceGround ? 1.35 : 1);
|
||||
const maxV = (burningNow ? 168 : (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76)))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1) * (iceGround ? 1.35 : 1);
|
||||
const v = Math.hypot(this.vx, this.vy);
|
||||
if (v > maxV) {
|
||||
this.vx = this.vx / v * maxV;
|
||||
|
|
@ -525,12 +630,12 @@
|
|||
temperatureDeathReason() {
|
||||
const status = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15), this) || null;
|
||||
const recent = (this.world?.time || 0) - Number(this.lastTemperatureDamageAt || -999) <= 36;
|
||||
if (recent && status && (status.harmExcess || 0) > 0) return status.direction === "cold" ? "凍結" : "熱中症";
|
||||
if (recent && status && (status.harmExcess || 0) > 0) return status.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
|
||||
return "";
|
||||
},
|
||||
|
||||
|
||||
abandonUnreachableGrassBed(bed, reason = "辿りつけないので放棄した") {
|
||||
abandonUnreachableGrassBed(bed, reason = "\u8FBF\u308A\u3064\u3051\u306A\u3044\u306E\u3067\u653E\u68C4\u3057\u305F") {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") return false;
|
||||
bed.ownerId = "";
|
||||
bed.unreachableUntil = 0;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
const id = behaviorId(tarinai);
|
||||
const state = String(tarinai.state || id || "");
|
||||
const tier = global.TarinaiPerf.renderQualityTier() || "high";
|
||||
const tier = global.TarinaiPerf.simulationQualityTier?.() || "high";
|
||||
const count = (worldRef.tarinai || []).length;
|
||||
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
const isVisible = visible(worldRef, tarinai, visibleRect);
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@
|
|||
t.fearTimer = Math.max(0, t.fearTimer - dt);
|
||||
t.intimidateTimer = Math.max(0, t.intimidateTimer - dt);
|
||||
t.intimidatedTimer = Math.max(0, t.intimidatedTimer - dt);
|
||||
t.intimidationCooldown = Math.max(0, (t.intimidationCooldown || 0) - dt);
|
||||
t.blastSpinTimer = Math.max(0, (t.blastSpinTimer || 0) - dt);
|
||||
t.postBirthPeaceTimer = Math.max(0, (t.postBirthPeaceTimer || 0) - dt);
|
||||
t.hpBarTimer = Math.max(0, (t.hpBarTimer || 0) - dt);
|
||||
t.stressBarTimer = Math.max(0, (t.stressBarTimer || 0) - dt);
|
||||
t.updateZunchiDisease(dt);
|
||||
t.updateSpecialDiseases(dt);
|
||||
if (t.updateBurning) t.updateBurning(dt);
|
||||
if (t.updateItemEffects) t.updateItemEffects(dt);
|
||||
if (t.dead) return { done: true };
|
||||
t.loveMochiTimer = Math.max(0, (t.loveMochiTimer || 0) - dt);
|
||||
|
|
@ -100,7 +102,7 @@
|
|||
t.fightTargetIds = [];
|
||||
t.fightTargetId = null;
|
||||
t.counterAttackFromId = null;
|
||||
if (t.state === "fight" || t.state === "seek_enemy") t.setActionState?.("idle", { target: null, reason: "喧嘩が終わって少し距離を置いている。", sleeping: false, clearTarget: true });
|
||||
if (t.state === "fight" || t.state === "seek_enemy") t.setActionState?.("idle", { target: null, reason: "\u55A7\u5629\u304C\u7D42\u308F\u3063\u3066\u5C11\u3057\u8DDD\u96E2\u3092\u7F6E\u3044\u3066\u3044\u308B\u3002", sleeping: false, clearTarget: true });
|
||||
}
|
||||
}
|
||||
t.grassEatTimer = Math.max(0, t.grassEatTimer - dt);
|
||||
|
|
@ -160,13 +162,13 @@
|
|||
const stressRate = excess * (0.66 + Math.min(20, excess) * 0.035);
|
||||
applyNeedShock(t, { safety: stressRate * dt, health: stressRate * dt * 0.42 });
|
||||
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0) && detChance(t.world, "temperature-stress-bubble", dt * 0.18, t)) {
|
||||
t.bubble(tempStatus.direction === "cold" ? "ぶるぶる" : "あちゅい", 2.4, "rgba(76,62,72,0.78)");
|
||||
t.bubble(tempStatus.direction === "cold" ? "\u3076\u308B\u3076\u308B" : "\u3042\u3061\u3085\u3044", 2.4, "rgba(76,62,72,0.78)");
|
||||
}
|
||||
}
|
||||
if ((tempStatus.harmExcess || 0) > 0) {
|
||||
const harm = tempStatus.harmExcess || 0;
|
||||
t.energy -= dt * (0.10 + harm * 0.075 + harm * harm * 0.006);
|
||||
t.lastDamageCause = tempStatus.direction === "cold" ? "凍結" : "熱中症";
|
||||
t.lastDamageCause = tempStatus.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
|
||||
t.lastTemperatureDamageAt = t.world?.time || 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,59 +4,39 @@ const TEXT_CATALOG = {
|
|||
toolTips: toolTipsFromDefinitions(),
|
||||
ecologyCards: [
|
||||
{
|
||||
title: "\u305f\u308a\u306a\u3044\u3068\u306f",
|
||||
title: "\u305F\u308A\u306A\u3044\u3068\u306F",
|
||||
image: "assets/sprites/tarinai_01_smile.webp",
|
||||
text: "\u5ead\u3084\u30b1\u30fc\u30b8\u3067\u751f\u6d3b\u3059\u308b\u89b3\u5bdf\u5bfe\u8c61\u3002\u7a7a\u8179\u3001\u7761\u7720\u3001\u5065\u5eb7\u3001\u5b89\u5168\u3001\u95a2\u4fc2\u3001\u5145\u8db3\u306e6\u6b32\u6c42\u3068\u30b9\u30c8\u30ec\u30b9\u3067\u884c\u52d5\u304c\u5909\u308f\u308a\u307e\u3059\u3002\u500b\u4f53\u3054\u3068\u306b\u6027\u683c\u3001\u4f53\u683c\u3001\u5bb6\u65cf\u3001\u7d4c\u9a13\u304c\u6b8b\u308a\u307e\u3059\u3002",
|
||||
text: "\u5EAD\u3084\u30B1\u30FC\u30B8\u3067\u751F\u6D3B\u3059\u308B\u89B3\u5BDF\u5BFE\u8C61\u3002\u7A7A\u8179\u3001\u7761\u7720\u3001\u5065\u5EB7\u3001\u5B89\u5168\u3001\u95A2\u4FC2\u3001\u5145\u8DB3\u306E\u6B32\u6C42\u3068\u30B9\u30C8\u30EC\u30B9\u3067\u884C\u52D5\u304C\u5909\u308F\u308A\u307E\u3059\u3002\u6027\u683C\u3001\u4F53\u683C\u3001\u5BB6\u65CF\u3001\u7D4C\u9A13\u3001\u75C5\u6C17\u3001\u6226\u7E3E\u306F\u500B\u4F53\u3054\u3068\u306B\u6B8B\u308A\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u6b32\u6c42\u3068\u30b9\u30c8\u30ec\u30b9",
|
||||
title: "\u6B32\u6C42\u3068\u884C\u52D5",
|
||||
image: "assets/ui/ecology_hp_stress.webp",
|
||||
text: "\u7a7a\u8179\u3084\u7720\u6c17\u304c\u5f37\u3044\u3068\u98df\u3079\u7269\u3084\u5bdd\u5e8a\u3092\u63a2\u3057\u307e\u3059\u3002\u30b9\u30c8\u30ec\u30b9\u306f\u6050\u6016\u3001\u6df7\u96d1\u3001\u6c5a\u308c\u3001\u3051\u3093\u304b\u3001\u5730\u9762\u306e\u7a2e\u985e\u306a\u3069\u3067\u5897\u6e1b\u3057\u3001\u9ad8\u3044\u307b\u3069\u9003\u8d70\u3001\u5a01\u5687\u3001\u4f53\u8abf\u4e0d\u826f\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308a\u307e\u3059\u3002",
|
||||
text: "\u8DB3\u308A\u306A\u3044\u6B32\u6C42\u304C\u5F37\u3044\u307B\u3069\u3001\u98DF\u4E8B\u3001\u7761\u7720\u3001\u4EF2\u9593\u63A2\u3057\u3001\u904A\u3073\u3001\u907F\u6691\u30FB\u907F\u5BD2\u3001\u9003\u8D70\u306A\u3069\u3092\u512A\u5148\u3057\u307E\u3059\u3002\u6050\u6016\u3001\u6DF7\u96D1\u3001\u6C5A\u308C\u3001\u3051\u3093\u304B\u3001\u5730\u9762\u3084\u6C17\u6E29\u306F\u30B9\u30C8\u30EC\u30B9\u3068\u884C\u52D5\u9078\u629E\u306B\u5F71\u97FF\u3057\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u98df\u4e8b\u3068\u6c34\u5206",
|
||||
title: "\u98DF\u4E8B\u3001\u75C5\u6C17\u3001\u56DE\u5FA9",
|
||||
image: "assets/sprites/tarinai_05_drool.webp",
|
||||
text: "\u8349\u3001\u305a\u3093\u3061\u3001\u9905\u3001\u30b8\u30e5\u30fc\u30b9\u306a\u3069\u3092\u72b6\u6cc1\u306b\u5fdc\u3058\u3066\u98df\u3079\u307e\u3059\u3002\u75c5\u6c17\u3084\u6975\u5ea6\u306e\u7a7a\u8179\u3067\u306f\u512a\u5148\u9806\u4f4d\u304c\u5909\u308f\u308a\u3001\u8fd1\u304f\u306e\u98df\u3079\u7269\u3092\u63a2\u3057\u307e\u3059\u3002\u6c34\u3084\u96e8\u6ef4\u306f\u4e00\u90e8\u306e\u75c5\u6c17\u3084\u74b0\u5883\u306e\u56de\u5fa9\u306b\u3082\u95a2\u308f\u308a\u307e\u3059\u3002",
|
||||
text: "\u8349\u3001\u305A\u3093\u3061\u3001\u9905\u3001\u30B8\u30E5\u30FC\u30B9\u3001\u6C34\u3001\u85AC\u306A\u3069\u3092\u72B6\u6CC1\u306B\u5FDC\u3058\u3066\u4F7F\u3044\u307E\u3059\u3002\u305A\u3093\u3061\u75C5\u3001\u306D\u3080\u308A\u75C5\u3001\u7206\u767A\u75C5\u3001\u304D\u305A\u3064\u304D\u75C5\u3067\u306F\u6C42\u3081\u308B\u3082\u306E\u3084\u884C\u52D5\u304C\u5909\u308F\u308A\u3001\u6C34\u30FB\u85AC\u30FB\u4F11\u606F\u30FB\u65E5\u5149\u6D74\u30FB\u74B0\u5883\u6539\u5584\u304C\u56DE\u5FA9\u306B\u95A2\u308F\u308A\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u7761\u7720\u3068\u5bdd\u5834\u6240",
|
||||
image: "assets/sprites/tarinai_20_sleep2.webp",
|
||||
text: "\u7720\u304f\u306a\u308b\u3068\u5de3\u7bb1\u3084\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u3092\u63a2\u3057\u307e\u3059\u3002\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306f\u8349\u3092\u4f7f\u3063\u3066\u4f5c\u3089\u308c\u3001\u8d77\u5e8a\u6642\u306b\u6d88\u3048\u307e\u3059\u3002\u9577\u304f\u5bdd\u5e8a\u3092\u7528\u610f\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u3001\u4f55\u3082\u306a\u3044\u5834\u6240\u3067\u7720\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059\u3002",
|
||||
title: "\u7761\u7720\u3068\u5DE3\u7BB1",
|
||||
image: "assets/ui/tool_nest_box.webp",
|
||||
text: "\u7720\u3044\u500B\u4F53\u306F\u5DE3\u7BB1\u3001\u30D9\u30C3\u30C9\u3001\u304B\u3093\u305F\u3093\u30D9\u30C3\u30C9\u3092\u63A2\u3057\u3001\u898B\u3064\u304B\u3089\u306A\u3044\u6642\u306F\u305D\u306E\u5834\u3067\u7720\u308A\u307E\u3059\u3002\u5DE3\u7BB1\u306F\u4E2D\u3067\u4F11\u3081\u308B\u5BDD\u5834\u6240\u3067\u3059\u3002\u901A\u5E38\u306F\u5B9A\u54E1\u3092\u8D85\u3048\u3066\u5165\u308C\u307E\u305B\u3093\u304C\u3001\u305F\u308A\u306A\u3044\u738B\u304B\u3089\u9003\u3052\u308B\u6642\u306F\u4E00\u5339\u3092\u62BC\u3057\u306E\u3051\u3066\u5165\u308C\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u8349\u3001\u5730\u9762\u3001\u5929\u6c17",
|
||||
image: "assets/ui/tool_grass.webp",
|
||||
text: "\u8349\u306f\u6210\u9577\u3057\u3001\u98df\u6599\u3084\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306e\u6750\u6599\u306b\u306a\u308a\u307e\u3059\u3002\u5730\u9762\u306f\u7802\u3001\u571f\u3001\u30b3\u30f3\u30af\u30ea\u30fc\u30c8\u3001\u3075\u308f\u3075\u308f\u6bdb\u5e03\u3001\u8db3\u3064\u307c\u3001\u6c37\u3092\u5207\u308a\u66ff\u3048\u3089\u308c\u3001\u8349\u306e\u767a\u751f\u3084\u30b9\u30c8\u30ec\u30b9\u5897\u52a0\u306b\u5f71\u97ff\u3057\u307e\u3059\u3002\u96e8\u3084\u6642\u9593\u5e2f\u3082\u72b6\u614b\u306b\u95a2\u308f\u308a\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u6027\u683c\u3068\u884c\u52d5\u50be\u5411",
|
||||
image: "assets/sprites/tarinai_21_normal_happy.webp",
|
||||
text: "\u653b\u6483\u6027\u3001\u958b\u653e\u6027\u3001\u793e\u4ea4\u6027\u3001\u795e\u7d4c\u8cea\u6027\u306a\u3069\u304c\u3042\u308a\u3001\u7d4c\u9a13\u3067\u5c11\u3057\u305a\u3064\u5909\u5316\u3057\u307e\u3059\u3002\u6027\u683c\u306f\u98df\u4e8b\u3001\u63a5\u8fd1\u3001\u9003\u8d70\u3001\u3051\u3093\u304b\u3001\u7e41\u6b96\u76f8\u624b\u306e\u63a2\u3057\u65b9\u306a\u3069\u306e\u5dee\u3068\u3057\u3066\u8868\u308c\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u95a2\u4fc2\u3001\u5bb6\u65cf\u3001\u7e41\u6b96",
|
||||
title: "\u95A2\u4FC2\u3001\u5BB6\u65CF\u3001\u7E41\u6B96",
|
||||
image: "assets/sprites/tarinai_27_birth_ritual.webp",
|
||||
text: "\u89aa\u5b50\u3001\u5171\u540c\u89aa\u3001\u53cb\u597d\u3001\u6575\u5bfe\u3001\u6050\u6016\u304c\u8a18\u9332\u3055\u308c\u307e\u3059\u3002\u6761\u4ef6\u304c\u305d\u308d\u3046\u3068\u3078\u3053\u3078\u3053\u3092\u884c\u3044\u3001\u5b50\u304c\u751f\u307e\u308c\u307e\u3059\u3002\u5b50\u306f\u89aa\u306e\u6027\u683c\u50be\u5411\u3092\u53d7\u3051\u7d99\u304e\u3001\u4e16\u4ee3\u60c5\u5831\u306f\u5bb6\u7cfb\u56f3\u306b\u6b8b\u308a\u307e\u3059\u3002",
|
||||
text: "\u89AA\u5B50\u3001\u5171\u540C\u89AA\u3001\u53CB\u597D\u3001\u6575\u5BFE\u3001\u6050\u6016\u3092\u8A18\u9332\u3057\u307E\u3059\u3002\u6761\u4EF6\u304C\u305D\u308D\u3046\u3068\u7E41\u6B96\u76F8\u624B\u3078\u8FD1\u3065\u304D\u3001\u3078\u3053\u3078\u3053\u5F8C\u306B\u5B50\u304C\u751F\u307E\u308C\u307E\u3059\u3002\u5BB6\u7CFB\u56F3\u3067\u306F\u4E16\u4EE3\u3001\u6027\u683C\u30BF\u30B0\u3001\u6226\u7E3E\u3001\u907A\u4F1D\u50BE\u5411\u3092\u78BA\u8A8D\u3067\u304D\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u75c5\u6c17\u3068\u56de\u5fa9",
|
||||
image: "assets/sprites/tarinai_24_stress_dizzy.webp",
|
||||
text: "\u305a\u3093\u3061\u75c5\u3001\u306d\u3080\u308a\u75c5\u3001\u7206\u767a\u75c5\u3001\u304d\u305a\u3064\u304d\u75c5\u306a\u3069\u304c\u3042\u308a\u307e\u3059\u3002\u75c5\u6c17\u3054\u3068\u306b\u6c42\u3081\u308b\u3082\u306e\u3084\u884c\u52d5\u304c\u5909\u308f\u308a\u3001\u6c34\u3001\u85ac\u3001\u98df\u3079\u7269\u3001\u4f11\u606f\u3001\u74b0\u5883\u306e\u6539\u5584\u304c\u56de\u5fa9\u306b\u95a2\u4fc2\u3057\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u5371\u967a\u3068\u4e8b\u6545",
|
||||
title: "\u305F\u308A\u306A\u3044\u738B\u3068\u305A\u3093\u3061\u3069\u308C\u3044",
|
||||
image: "assets/sprites/tarinai_26_intimidate.webp",
|
||||
text: "\u3051\u3093\u304b\u3001\u7206\u7af9\u3001\u3052\u3093\u3053\u3064\u3001\u30d4\u30f3\u3001\u30dc\u30fc\u30eb\u3001\u30a2\u30ea\u3001\u843d\u4e0b\u7269\u306a\u3069\u3067\u4f53\u529b\u304c\u6e1b\u308a\u307e\u3059\u3002\u4f53\u529b\u304c0\u306b\u306a\u308b\u3068\u6b7b\u4ea1\u3057\u3001\u6b7b\u56e0\u306f\u30ed\u30b0\u3068\u7d71\u8a08\u306b\u6b8b\u308a\u307e\u3059\u3002\u5371\u967a\u306a\u9053\u5177\u306f\u89b3\u5bdf\u306e\u6d41\u308c\u3092\u5927\u304d\u304f\u5909\u3048\u307e\u3059\u3002",
|
||||
text: "\u9EC4\u91D1\u306E\u51A0\u306B\u89E6\u308C\u308B\u3001\u307E\u305F\u306F\u3051\u3093\u304B\u3067\u5F37\u3044\u5B9F\u7E3E\u3092\u51FA\u3059\u3068\u305F\u308A\u306A\u3044\u738B\u306B\u306A\u308A\u307E\u3059\u3002\u5974\u96B7\u306E\u9396\u306B\u89E6\u308C\u305F\u500B\u4F53\u306F\u305A\u3093\u3061\u3069\u308C\u3044\u306B\u306A\u308A\u3001\u666E\u901A\u306E\u305F\u308A\u306A\u3044\u3068\u306F\u8DDD\u96E2\u3092\u53D6\u308A\u307E\u3059\u3002\u738B\u3068\u3069\u308C\u3044\u306E\u72B6\u614B\u306F\u500B\u4F53\u30C7\u30FC\u30BF\u3068\u5BB6\u7CFB\u56F3\u306B\u6B8B\u308A\u3001\u95A2\u4FC2\u3084\u884C\u52D5\u306E\u89B3\u5BDF\u5BFE\u8C61\u306B\u306A\u308A\u307E\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u9053\u5177\u3068\u69cb\u9020\u7269",
|
||||
title: "\u9053\u5177\u3068\u4E8B\u6545",
|
||||
image: "assets/ui/tool_duplicator.webp",
|
||||
text: "\u98df\u3079\u7269\u3001\u85ac\u3001\u5de3\u7bb1\u3001\u67f5\u3001\u770b\u677f\u3001\u8907\u88fd\u6a5f\u306a\u3069\u3092\u914d\u7f6e\u3067\u304d\u307e\u3059\u3002\u8907\u88fd\u6a5f\u306b\u306f\u5bfe\u5fdc\u3059\u308b\u98df\u3079\u7269\u3001\u85ac\u3001\u8349\u306a\u3069\u3092\u30bb\u30c3\u30c8\u3067\u304d\u307e\u3059\u3002\u524a\u9664\u30c4\u30fc\u30eb\u3067\u306f\u4e0d\u8981\u306a\u69cb\u9020\u7269\u3084\u4e00\u90e8\u30a2\u30a4\u30c6\u30e0\u3092\u53d6\u308a\u9664\u3051\u307e\u3059\u3002",
|
||||
},
|
||||
{
|
||||
title: "\u30b3\u30ed\u30cb\u30fc\u306e\u898b\u65b9",
|
||||
image: "assets/sprites/tarinai_11_weak.webp",
|
||||
text: "\u30b3\u30ed\u30cb\u30fc\u6b04\u3067\u306f\u500b\u4f53\u6570\u3001\u6b7b\u4ea1\u6570\u3001\u5168\u4f53\u72b6\u614b\u3001\u5730\u9762\u3001\u6b32\u6c42\u5e73\u5747\u3001\u7269\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002\u89b3\u5bdf\u3067\u306f\u9078\u629e\u4e2d\u306e\u500b\u4f53\u306e\u5065\u5eb7\u3001\u6027\u683c\u3001\u95a2\u4fc2\u3001\u884c\u52d5\u7406\u7531\u3001\u8a18\u9332\u3092\u8ffd\u3048\u307e\u3059\u3002",
|
||||
text: "\u98DF\u3079\u7269\u3001\u85AC\u3001\u5DE3\u7BB1\u3001\u67F5\u3001\u770B\u677F\u3001\u8907\u88FD\u6A5F\u306A\u3069\u3092\u914D\u7F6E\u3067\u304D\u307E\u3059\u3002\u7206\u7AF9\u3001\u3052\u3093\u3053\u3064\u3001\u30D4\u30F3\u3001\u30DC\u30FC\u30EB\u3001\u30A2\u30EA\u3001\u843D\u4E0B\u7269\u306A\u3069\u306F\u4E8B\u6545\u3084\u6B7B\u4EA1\u306B\u3064\u306A\u304C\u308A\u3001\u51FA\u6765\u4E8B\u306F\u30ED\u30B0\u3068\u7D71\u8A08\u306B\u6B8B\u308A\u307E\u3059\u3002",
|
||||
},
|
||||
],};
|
||||
|
||||
|
|
@ -144,7 +124,7 @@ TEXT_CATALOG.temperatureEndureText = function temperatureEndureText(t = null) {
|
|||
const status = t.temperatureStatus || t.world.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? t.world.temperatureAt?.(t.x, t.y, t) ?? (CONFIG.standardTemperature ?? 15), t);
|
||||
if (!status || status.comfortable || (status.discomfort || 0) <= 0.5) return "";
|
||||
if (t.world.hasReachableComfortTemperatureSpot?.(t)) return "";
|
||||
return status.direction === "cold" ? "寒さに耐えている。" : "暑さに耐えている。";
|
||||
return status.direction === "cold" ? "\u5BD2\u3055\u306B\u8010\u3048\u3066\u3044\u308B\u3002" : "\u6691\u3055\u306B\u8010\u3048\u3066\u3044\u308B\u3002";
|
||||
};
|
||||
|
||||
TEXT_CATALOG.targetLabel = function targetLabelFromCatalog(target) {
|
||||
|
|
@ -165,6 +145,7 @@ TEXT_CATALOG.targetLabel = function targetLabelFromCatalog(target) {
|
|||
TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
|
||||
if (!t) return "\u306a\u3057";
|
||||
if ((t.pinchThoughtTimer || 0) > 0) return "\u3064\u307e\u307e\u308c\u3066\u3044\u308b";
|
||||
if (t.lastPanicCause === "tarinai_champion" && (t.state === "panic" || (t.fearTimer || 0) > 0.04)) return "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
||||
const behaviorState = currentTarinaiBehavior(t);
|
||||
const behaviorLabel = behaviorText(t);
|
||||
const cleanedBehaviorLabel = TEXT_CATALOG.cleanBehaviorText(behaviorLabel, behaviorState?.actionId || t.state || "");
|
||||
|
|
|
|||
3
js/ui.js
3
js/ui.js
|
|
@ -12,7 +12,10 @@ const ui = {
|
|||
soundMasterToggle: document.getElementById("soundMasterToggle"),
|
||||
creditsBtn: document.getElementById("creditsBtn"),
|
||||
saveBtn: document.getElementById("saveBtn"),
|
||||
visualSettingsBtn: document.getElementById("visualSettingsBtn"),
|
||||
resetBtn: document.getElementById("resetBtn"),
|
||||
visualSettingsDialog: document.getElementById("visualSettingsDialog"),
|
||||
visualSettingsCloseBtn: document.getElementById("visualSettingsCloseBtn"),
|
||||
creditsDialog: document.getElementById("creditsDialog"),
|
||||
creditsCloseBtn: document.getElementById("creditsCloseBtn"),
|
||||
panel: document.querySelector(".panel"),
|
||||
|
|
|
|||
|
|
@ -40,6 +40,24 @@ function bindUI() {
|
|||
return window.matchMedia?.("(min-width: 981px)")?.matches ?? window.innerWidth >= 981;
|
||||
}
|
||||
|
||||
function syncVisualSettingsDialog() {
|
||||
const settings = window.TarinaiPerf?.visualSettings?.() || {};
|
||||
ui.visualSettingsDialog?.querySelectorAll("[data-visual-setting]").forEach((btn) => {
|
||||
const active = settings[btn.dataset.visualSetting] === btn.dataset.visualValue;
|
||||
btn.classList.toggle("active", active);
|
||||
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function openVisualSettingsDialog() {
|
||||
syncVisualSettingsDialog();
|
||||
ui.visualSettingsDialog?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function closeVisualSettingsDialog() {
|
||||
ui.visualSettingsDialog?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function markCollapsibleCardState(card, collapsed, { user = false } = {}) {
|
||||
if (!card) return;
|
||||
card.classList.toggle("collapsed", Boolean(collapsed));
|
||||
|
|
@ -50,7 +68,7 @@ function bindUI() {
|
|||
const toggle = card.querySelector(".panel-card-toggle");
|
||||
toggle?.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
const state = toggle?.querySelector(".panel-card-state");
|
||||
if (state) state.textContent = collapsed ? "展開" : "折り畳み";
|
||||
if (state) state.textContent = collapsed ? "\u5C55\u958B" : "\u6298\u308A\u7573\u307F";
|
||||
}
|
||||
|
||||
function markToolCategoryState(category, collapsed, { user = false } = {}) {
|
||||
|
|
@ -83,13 +101,13 @@ function bindUI() {
|
|||
uiCache.showSelectedEmpty = true;
|
||||
uiCache.selectedEmpty = false;
|
||||
renderSelected();
|
||||
showToast("たりないが未選択です。たりないデータを開ける状態にしました。");
|
||||
showToast("\u305F\u308A\u306A\u3044\u304C\u672A\u9078\u629E\u3067\u3059\u3002\u305F\u308A\u306A\u3044\u30C7\u30FC\u30BF\u3092\u958B\u3051\u308B\u72B6\u614B\u306B\u3057\u307E\u3057\u305F\u3002");
|
||||
}
|
||||
const target = panelSectionForTab(tab);
|
||||
if (!target) return false;
|
||||
target.classList?.remove?.("hidden");
|
||||
if (tab === "family") {
|
||||
// 更新OFFでも、ユーザーが家系図へ移動した時だけ現在スナップショットを描画する。
|
||||
// \u66F4\u65B0OFF\u3067\u3082\u3001\u30E6\u30FC\u30B6\u30FC\u304C\u5BB6\u7CFB\u56F3\u3078\u79FB\u52D5\u3057\u305F\u6642\u3060\u3051\u73FE\u5728\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u63CF\u753B\u3059\u308B\u3002
|
||||
window.resetArchiveRenderState?.();
|
||||
renderArchive?.();
|
||||
}
|
||||
|
|
@ -99,7 +117,7 @@ function bindUI() {
|
|||
const top = Math.max(0, target.offsetTop - tabsH - 8);
|
||||
panel.scrollTo({ top, behavior: "auto" });
|
||||
} else {
|
||||
// スマホはページ全体スクロール。目的地UIの上端を画面上端へ合わせる。
|
||||
// \u30B9\u30DE\u30DB\u306F\u30DA\u30FC\u30B8\u5168\u4F53\u30B9\u30AF\u30ED\u30FC\u30EB\u3002\u76EE\u7684\u5730UI\u306E\u4E0A\u7AEF\u3092\u753B\u9762\u4E0A\u7AEF\u3078\u5408\u308F\u305B\u308B\u3002
|
||||
scrollWindowToElement(target, 0, "auto");
|
||||
}
|
||||
setActivePanelTab(tab);
|
||||
|
|
@ -456,6 +474,22 @@ function bindUI() {
|
|||
ui.creditsDialog?.addEventListener("click", (e) => {
|
||||
if (e.target === ui.creditsDialog) ui.creditsDialog.classList.add("hidden");
|
||||
});
|
||||
ui.visualSettingsBtn?.addEventListener("click", () => {
|
||||
audio.uiClick?.();
|
||||
openVisualSettingsDialog();
|
||||
});
|
||||
ui.visualSettingsCloseBtn?.addEventListener("click", closeVisualSettingsDialog);
|
||||
ui.visualSettingsDialog?.addEventListener("click", (e) => {
|
||||
if (e.target === ui.visualSettingsDialog) { closeVisualSettingsDialog(); return; }
|
||||
const btn = e.target.closest("[data-visual-setting]");
|
||||
if (!btn) return;
|
||||
const ok = window.TarinaiPerf?.setVisualSetting?.(btn.dataset.visualSetting || "", btn.dataset.visualValue || "");
|
||||
if (!ok) return;
|
||||
audio.uiClick?.();
|
||||
syncVisualSettingsDialog();
|
||||
resizeCanvas?.();
|
||||
render?.();
|
||||
});
|
||||
ui.resetBtn?.addEventListener("click", () => {
|
||||
audio.uiClick?.();
|
||||
openFieldDialog();
|
||||
|
|
@ -530,7 +564,7 @@ function bindUI() {
|
|||
world.copyBuffer = null;
|
||||
selectTool("observe");
|
||||
for (const b of uiCache.toolButtons || []) b.classList.remove("selected");
|
||||
showToast("道具選択を解除しました。");
|
||||
showToast("\u9053\u5177\u9078\u629E\u3092\u89E3\u9664\u3057\u307E\u3057\u305F\u3002");
|
||||
return;
|
||||
}
|
||||
if (world.tool === tool && cycleToolSize(tool)) return;
|
||||
|
|
@ -596,6 +630,7 @@ function bindUI() {
|
|||
if (e.key !== "Escape") return;
|
||||
closeFieldDialog();
|
||||
closeEcologyDialog();
|
||||
closeVisualSettingsDialog();
|
||||
closeArchiveResetDialog();
|
||||
ui.creditsDialog?.classList.add("hidden");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function renderStats() {
|
|||
const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" };
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a");
|
||||
const temp = world.updateTemperature?.(0) ?? world.currentTemperature ?? (CONFIG.standardTemperature ?? 15);
|
||||
const tempLabel = `${Math.round(temp)}℃`;
|
||||
const tempLabel = `${Math.round(temp)}\u2103`;
|
||||
setTextIfChanged(ui.temperatureValue, "temperature", tempLabel);
|
||||
if (ui.temperatureSlider) {
|
||||
ui.temperatureSlider.disabled = world.temperatureAuto !== false;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
const gen = n?.generation ? `G${n.generation}` : "G?";
|
||||
const rawReason = !n?.alive && n?.deathReason ? String(n.deathReason) : "";
|
||||
const briefReason = familyDeathReasonLabel(rawReason, entity);
|
||||
const status = n?.alive ? familyStatusText(n) : (briefReason || "死亡");
|
||||
const status = n?.alive ? familyStatusText(n) : (briefReason || "\u6B7B\u4EA1");
|
||||
const reason = briefReason ? ` / ${briefReason}` : "";
|
||||
const rawTags = live?.personalityTags ? live.personalityTags() : (Array.isArray(n?.personalityTags) ? n.personalityTags : []);
|
||||
const tags = rawTags.slice();
|
||||
|
|
@ -48,12 +48,12 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
const deathLine = "";
|
||||
const tagLine = displayTags.length ? `<span class="family-personality-tags">${displayTags.map(tag => `<em>${escapeHtml(tag)}</em>`).join("")}</span>` : "";
|
||||
const champion = !!(live?.isTarinaiChampion || n?.isTarinaiChampion);
|
||||
const statLine = n ? `<span class="family-genetic-stats" title="寿命 / 体格 / 攻撃 / 速度">
|
||||
<span><b>寿</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "life")))}</span>
|
||||
<span><b>体</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "size")))}</span>
|
||||
<span><b>攻</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "attack")))}</span>
|
||||
<span><b>速</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "speed")))}</span>
|
||||
</span><span class="family-fight-stat" title="けんか勝率 / 回数"><b>勝</b>${escapeHtml(lineageFightRateText(n))}</span>` : "";
|
||||
const statLine = n ? `<span class="family-genetic-stats" title="\u5BFF\u547D / \u4F53\u683C / \u653B\u6483 / \u901F\u5EA6">
|
||||
<span><b>\u5BFF</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "life")))}</span>
|
||||
<span><b>\u4F53</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "size")))}</span>
|
||||
<span><b>\u653B</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "attack")))}</span>
|
||||
<span><b>\u901F</b>${escapeHtml(lineageSignedPercentText(lineageGeneticPercentFromNode(n, "speed")))}</span>
|
||||
</span><span class="family-fight-stat" title="\u3051\u3093\u304B\u52DD\u7387 / \u56DE\u6570"><b>\u52DD</b>${escapeHtml(lineageFightRateText(n))}</span>` : "";
|
||||
const cls = n ? ((live || n.alive) ? "alive" : "dead") : "missing";
|
||||
const childCls = (n?.parents || []).length ? " child" : "";
|
||||
const iconBaseScale = live
|
||||
|
|
@ -64,8 +64,8 @@ function lineageTreeNodeHtml(n, x, y) {
|
|||
return `<div class="family-node tree-node ${cls}${childCls}${champion ? " champion" : ""}"${data} title="${title}" style="left:${Math.round(x)}px;top:${Math.round(y)}px;--family-icon-scale:${iconScale}">
|
||||
<div class="family-portrait" aria-hidden="true"><img src="${img}" loading="lazy" decoding="async" alt=""></div>
|
||||
<div class="family-copy">
|
||||
<strong>${champion ? "♛ " : ""}${name}</strong>
|
||||
<span>${escapeHtml(gen)} / ${escapeHtml(status)}${champion ? " / 王者" : ""}</span>
|
||||
<strong>${champion ? "\u265B " : ""}${name}</strong>
|
||||
<span>${escapeHtml(gen)} / ${escapeHtml(status)}${champion ? " / \u738B\u8005" : ""}</span>
|
||||
${statLine}
|
||||
${deathLine}
|
||||
${tagLine}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function geneticComfortTemperatureText(t) {
|
|||
const min = (CONFIG.temperatureComfortMin ?? 10) + offset;
|
||||
const max = (CONFIG.temperatureComfortMax ?? 25) + offset;
|
||||
const sign = offset >= 0 ? "+" : "";
|
||||
return `${min.toFixed(1)}〜${max.toFixed(1)}℃ <small>(${sign}${offset.toFixed(1)}℃)</small>`;
|
||||
return `${min.toFixed(1)}\u301C${max.toFixed(1)}\u2103 <small>(${sign}${offset.toFixed(1)}\u2103)</small>`;
|
||||
}
|
||||
|
||||
function personalityDailyChangeHtml(t) {
|
||||
|
|
@ -267,7 +267,7 @@ function renderSelected() {
|
|||
<div class="info-row"><span>\u653b\u6483\u529b</span><strong>${geneticPercentText(t, "attack")}</strong></div>
|
||||
<div class="info-row"><span>\u79fb\u52d5\u901f\u5ea6</span><strong>${geneticPercentText(t, "speed")}</strong></div>
|
||||
<div class="info-row"><span>\u30b5\u30a4\u30ba</span><strong>${geneticSizeText(t)}</strong></div>
|
||||
<div class="info-row"><span>快適温度</span><strong>${geneticComfortTemperatureText(t)}</strong></div>
|
||||
<div class="info-row"><span>\u5FEB\u9069\u6E29\u5EA6</span><strong>${geneticComfortTemperatureText(t)}</strong></div>
|
||||
`;
|
||||
const needRowsHtml = needKeys.map(key => {
|
||||
const label = (typeof TARINAI_NEED_LABELS !== "undefined" && TARINAI_NEED_LABELS[key]) || key;
|
||||
|
|
@ -280,7 +280,7 @@ function renderSelected() {
|
|||
`<div class="need-grid">${needRowsHtml}</div>`,
|
||||
selectedInfoRowHtml("\u4f53\u529b", `${fmt(t.energy)} / ${fmt(typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100))}`, "energy"),
|
||||
selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"),
|
||||
selectedInfoRowHtml("体感気温", `${(world.feltTemperatureFor?.(t) ?? world.temperatureAt?.(t.x, t.y, t) ?? t.feltTemperature ?? CONFIG.standardTemperature ?? 15).toFixed(1)}℃`, "temperature"),
|
||||
selectedInfoRowHtml("\u4F53\u611F\u6C17\u6E29", `${(world.feltTemperatureFor?.(t) ?? world.temperatureAt?.(t.x, t.y, t) ?? t.feltTemperature ?? CONFIG.standardTemperature ?? 15).toFixed(1)}\u2103`, "temperature"),
|
||||
selectedInfoRowHtml("\u6e80\u8179\u5ea6", fmt(Math.max(0, Math.min(100, 100 - (Number(t.hunger) || 0)))), "fullness"),
|
||||
].join("");
|
||||
const itemEffectHtml = itemEffectSummary.length
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const SCALABLE_TOOLS = new Set(scalableToolIds());
|
||||
const EMOJI_ICON_TOOLS = new Set(["poke", "pinch", "water_hose", "rope", "rod", "reciprocator", "copy", "undo", "redo", "area_delete", "clear_tool"]);
|
||||
const EMOJI_ICON_TOOLS = new Set(["poke", "pinch", "water_hose", "fire", "flame_firecracker", "rope", "rod", "reciprocator", "copy", "undo", "redo", "area_delete", "clear_tool"]);
|
||||
|
||||
const SIZE_ORDER = typeof TOOL_SIZE_ORDER !== "undefined" ? TOOL_SIZE_ORDER : ["small", "medium", "large"];
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,15 @@
|
|||
return String(el.dataset?.tip || "");
|
||||
}
|
||||
|
||||
function formatTooltipText(text) {
|
||||
return String(text || "")
|
||||
.replace(/\u3002(?!\n)/g, "\u3002\n")
|
||||
.replace(/\n+$/g, "");
|
||||
}
|
||||
|
||||
function showTooltip(el, options = {}) {
|
||||
if (!el || !document.documentElement.contains(el)) return;
|
||||
const tip = resolveTip(el);
|
||||
const tip = formatTooltipText(resolveTip(el));
|
||||
if (!tip) return;
|
||||
if (hideTimer) { clearTimeout(hideTimer); hideTimer = 0; }
|
||||
const pop = ensureTooltipPopover();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.15.90";
|
||||
const APP_BUILD = "tarinai-fight-cooldown-damage-distributed-v39-15-90";
|
||||
const APP_VERSION = "39.15.98";
|
||||
const APP_BUILD = "tarinai-fire-system-v39-15-98";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
worldRef.nextWeatherChange = resetTimer();
|
||||
if (previous !== fixed) {
|
||||
worldRef.emit?.("weather:changed", { previous, next: fixed });
|
||||
worldRef.log?.(`天気: ${weatherLabel(fixed)}`);
|
||||
worldRef.log?.(`\u5929\u6C17: ${weatherLabel(fixed)}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
worldRef.nextWeatherChange = resetTimer();
|
||||
if (next !== previous) {
|
||||
worldRef.emit?.("weather:changed", { previous, next });
|
||||
worldRef.log?.(`天気: ${weatherLabel(next)}`);
|
||||
worldRef.log?.(`\u5929\u6C17: ${weatherLabel(next)}`);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@
|
|||
return blastedPins;
|
||||
},
|
||||
|
||||
spawnFireAt(x, y, opts = {}) {
|
||||
return globalThis.TarinaiItemDynamicToolSystem?.spawnFireAt?.(this, x, y, opts) || null;
|
||||
},
|
||||
|
||||
scatterFireFrom(it) {
|
||||
return globalThis.TarinaiItemDynamicToolSystem?.scatterFireFrom?.(it, this) || false;
|
||||
},
|
||||
|
||||
explodeFirecracker(it) {
|
||||
if (!it || it.dead || it.type !== "firecracker") return;
|
||||
it.amount = 0;
|
||||
|
|
@ -472,7 +480,7 @@
|
|||
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
||||
const sourceLabel = (src) => {
|
||||
if (!src) return "";
|
||||
if (src instanceof Tarinai || (src.name && src.radius && !src.type)) return `「${String(src.name || "たりない")}」`;
|
||||
if (src instanceof Tarinai || (src.name && src.radius && !src.type)) return `\u300C${String(src.name || "\u305F\u308A\u306A\u3044")}\u300D`;
|
||||
if (src.type && typeof toolLabel === "function") return toolLabel(src.type);
|
||||
if (src.type) return String(src.type);
|
||||
if (src.name) return String(src.name);
|
||||
|
|
@ -580,8 +588,8 @@
|
|||
const winRate = total > 0 ? wins / total : 0;
|
||||
if (winRate < 0.75) return false;
|
||||
if (!tarinai.becomeTarinaiChampion?.({ fightRecord: true })) return false;
|
||||
this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.35, "王者", "rgba(144,96,24,0.84)");
|
||||
this.log?.(`${tarinai.name}は喧嘩${total}回、勝率${Math.round(winRate * 100)}%でたりない王者になった。`, "fight", { participants: other ? [tarinai, other] : [tarinai] });
|
||||
this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.35, "\u738B\u8005", "rgba(144,96,24,0.84)");
|
||||
this.log?.(`${tarinai.name}\u306F\u55A7\u5629${total}\u56DE\u3001\u52DD\u7387${Math.round(winRate * 100)}%\u3067\u305F\u308A\u306A\u3044\u738B\u8005\u306B\u306A\u3063\u305F\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -238,13 +238,8 @@
|
|||
return Number.isFinite(v) ? v : (Number.isFinite(fb) ? fb : 15);
|
||||
},
|
||||
|
||||
clampTemperature(value) {
|
||||
// v10: 気温の上限/下限は撤廃。ドライアイス・ストーブの重ね掛けや手動値は制限しない。
|
||||
return this.temperatureNumber(value);
|
||||
},
|
||||
|
||||
climateTemperature(value) {
|
||||
// 自然気候だけは仕様どおり -20℃〜50℃ の範囲に収める。
|
||||
// \u81EA\u7136\u6C17\u5019\u3060\u3051\u306F\u4ED5\u69D8\u3069\u304A\u308A -20\u2103\u301C50\u2103 \u306E\u7BC4\u56F2\u306B\u53CE\u3081\u308B\u3002
|
||||
const v = this.temperatureNumber(value);
|
||||
return clamp(v, CONFIG.climateTemperatureMin ?? -20, CONFIG.climateTemperatureMax ?? 50);
|
||||
},
|
||||
|
|
@ -254,7 +249,7 @@
|
|||
},
|
||||
|
||||
seasonLabel() {
|
||||
return ["春", "夏", "秋", "冬"][this.seasonIndex()] || "春";
|
||||
return ["\u6625", "\u590F", "\u79CB", "\u51AC"][this.seasonIndex()] || "\u6625";
|
||||
},
|
||||
|
||||
seasonDay() {
|
||||
|
|
@ -262,7 +257,7 @@
|
|||
},
|
||||
|
||||
seasonDayString() {
|
||||
return `${this.seasonLabel()}${this.seasonDay()}日`;
|
||||
return `${this.seasonLabel()}${this.seasonDay()}\u65E5`;
|
||||
},
|
||||
|
||||
temperatureStableUnit(salt = "") {
|
||||
|
|
@ -273,12 +268,6 @@
|
|||
return ((h >>> 0) % 1000000) / 1000000;
|
||||
},
|
||||
|
||||
weatherDailyRangeFactor(weather = this.weather) {
|
||||
if (weather === "sunny") return 0.68;
|
||||
if (weather === "light_rain") return 0.24;
|
||||
return 0.36;
|
||||
},
|
||||
|
||||
dailyTemperatureRange(day = this.day, weather = this.weather) {
|
||||
const raw = this.temperatureStableUnit(`daily-range:${day}`) * 15;
|
||||
if (weather === "sunny") return clamp(6 + raw * 0.62, 0, 15);
|
||||
|
|
@ -297,8 +286,8 @@
|
|||
|
||||
dailyTemperatureWave(progress = 0) {
|
||||
const hour = ((((Number(progress) || 0) % 1) + 1) % 1) * 24;
|
||||
const minHour = 5.0; // 朝方に最低気温
|
||||
const maxHour = 14.0; // 昼過ぎに最高気温
|
||||
const minHour = 5.0; // \u671D\u65B9\u306B\u6700\u4F4E\u6C17\u6E29
|
||||
const maxHour = 14.0; // \u663C\u904E\u304E\u306B\u6700\u9AD8\u6C17\u6E29
|
||||
const smooth = (x) => {
|
||||
const t = clamp(x, 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
|
|
@ -315,7 +304,7 @@
|
|||
},
|
||||
|
||||
naturalTemperature() {
|
||||
// 初期ロード直後は 10℃ を保持し、初回フレームのdt揺れで0℃近くへ落ちないようにする。
|
||||
// \u521D\u671F\u30ED\u30FC\u30C9\u76F4\u5F8C\u306F 10\u2103 \u3092\u4FDD\u6301\u3057\u3001\u521D\u56DE\u30D5\u30EC\u30FC\u30E0\u306Edt\u63FA\u308C\u30670\u2103\u8FD1\u304F\u3078\u843D\u3061\u306A\u3044\u3088\u3046\u306B\u3059\u308B\u3002
|
||||
if ((Number(this.time) || 0) <= 6.0 && (Number(this.day) || 1) === 1) {
|
||||
this.temperatureDailyRange = 0;
|
||||
return 10;
|
||||
|
|
@ -326,7 +315,7 @@
|
|||
const dailyWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(progress) : Math.sin(progress * Math.PI * 2 - Math.PI * 0.5);
|
||||
let temp;
|
||||
if ((Number(this.day) || 1) === 1) {
|
||||
// 春1日目は開始時10℃を基準にしつつ、日内の最低/最高時刻だけは維持する。
|
||||
// \u66251\u65E5\u76EE\u306F\u958B\u59CB\u664210\u2103\u3092\u57FA\u6E96\u306B\u3057\u3064\u3064\u3001\u65E5\u5185\u306E\u6700\u4F4E/\u6700\u9AD8\u6642\u523B\u3060\u3051\u306F\u7DAD\u6301\u3059\u308B\u3002
|
||||
const initialWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(0) : Math.sin(-Math.PI * 0.5);
|
||||
temp = 10 + (dailyWave - initialWave) * dailyRange * 0.5;
|
||||
} else {
|
||||
|
|
@ -416,7 +405,7 @@
|
|||
stressExcess,
|
||||
harmExcess,
|
||||
harmful: harmExcess > 0,
|
||||
label: discomfort <= 0 ? "快適" : (direction === "cold" ? (harmExcess > 0 ? "危険な寒さ" : "寒い") : (harmExcess > 0 ? "危険な暑さ" : "暑い")),
|
||||
label: discomfort <= 0 ? "\u5FEB\u9069" : (direction === "cold" ? (harmExcess > 0 ? "\u5371\u967A\u306A\u5BD2\u3055" : "\u5BD2\u3044") : (harmExcess > 0 ? "\u5371\u967A\u306A\u6691\u3055" : "\u6691\u3044")),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -465,7 +454,7 @@
|
|||
const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0);
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
best = { x: c.x, y: c.y, dead: false, detour: true, temperatureTarget: true, temperature: temp, label: "快適な温度帯" };
|
||||
best = { x: c.x, y: c.y, dead: false, detour: true, temperatureTarget: true, temperature: temp, label: "\u5FEB\u9069\u306A\u6E29\u5EA6\u5E2F" };
|
||||
}
|
||||
}
|
||||
if (!best) return null;
|
||||
|
|
@ -788,7 +777,7 @@
|
|||
t.lastPoisonBlockBubbleAt = now;
|
||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||
}
|
||||
// 物理ブロック衝突の視覚エフェクトは出さない。接触時の状態変化はHPバーと吹き出しだけに限定する。
|
||||
// \u7269\u7406\u30D6\u30ED\u30C3\u30AF\u885D\u7A81\u306E\u8996\u899A\u30A8\u30D5\u30A7\u30AF\u30C8\u306F\u51FA\u3055\u306A\u3044\u3002\u63A5\u89E6\u6642\u306E\u72B6\u614B\u5909\u5316\u306FHP\u30D0\u30FC\u3068\u5439\u304D\u51FA\u3057\u3060\u3051\u306B\u9650\u5B9A\u3059\u308B\u3002
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -841,7 +830,7 @@
|
|||
const wasLive = !target.dead;
|
||||
const destroyed = (lifecycle)?.deleteItem?.(this, target, {
|
||||
reason: "poison-block-contact",
|
||||
userReason: "毒ブロックに触れて消えた",
|
||||
userReason: "\u6BD2\u30D6\u30ED\u30C3\u30AF\u306B\u89E6\u308C\u3066\u6D88\u3048\u305F",
|
||||
wake: true,
|
||||
breaker: poison,
|
||||
panicOwner: false,
|
||||
|
|
|
|||
|
|
@ -2,17 +2,41 @@
|
|||
|
||||
|
||||
(function (global) {
|
||||
|
||||
function socialPersonalityProfile(tarinai) {
|
||||
if (tarinai && typeof tarinai.personalityProfile === "function") return tarinai.personalityProfile();
|
||||
const base = { label: "\u4E2D\u7ACB", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 };
|
||||
const cur = tarinai?.currentPersonality || {};
|
||||
const neu = Number(cur.neuroticism || 0) || 0;
|
||||
const aggr = Number(cur.aggression || 0) || 0;
|
||||
const soc = Number(cur.sociability || 0) || 0;
|
||||
const open = Number(cur.openness || 0) || 0;
|
||||
return {
|
||||
...base,
|
||||
fear: clamp(1 + neu * 0.38, 0.55, 1.85),
|
||||
fight: clamp(1 + aggr * 0.55, 0.35, 2.35),
|
||||
social: clamp(1 + soc * 0.24, 0.62, 1.52),
|
||||
relation: clamp(1 + soc * 0.24, 0.62, 1.60),
|
||||
play: clamp(1 + open * 0.45, 0.45, 2.10),
|
||||
zunda: 1,
|
||||
sleep: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function isSocialTarinaiEntity(value) {
|
||||
return Boolean(value && !value.dead && typeof value.relationTo === "function" && Number.isFinite(value.x) && Number.isFinite(value.y));
|
||||
}
|
||||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_family_social.js");
|
||||
|
||||
const RESET_PRESETS = Object.freeze({
|
||||
empty: { label: "空白" },
|
||||
default: { label: "デフォルト" },
|
||||
athletic: { label: "アスレチック" },
|
||||
crowded: { label: "過密" },
|
||||
family: { label: "家族" },
|
||||
war: { label: "戦争" },
|
||||
happy: { label: "幸福" },
|
||||
empty: { label: "\u7A7A\u767D" },
|
||||
default: { label: "\u30C7\u30D5\u30A9\u30EB\u30C8" },
|
||||
athletic: { label: "\u30A2\u30B9\u30EC\u30C1\u30C3\u30AF" },
|
||||
crowded: { label: "\u904E\u5BC6" },
|
||||
family: { label: "\u5BB6\u65CF" },
|
||||
war: { label: "\u6226\u4E89" },
|
||||
happy: { label: "\u5E78\u798F" },
|
||||
});
|
||||
|
||||
function normalizeResetPresetId(presetId = "default") {
|
||||
|
|
@ -205,7 +229,7 @@
|
|||
if (opts.toolSize) item.toolSize = opts.toolSize;
|
||||
if (type === "duplicator") {
|
||||
item.storedFoodType = opts.storedFoodType || "sweet";
|
||||
item.storedFoodLabel = typeof foodLabel === "function" ? foodLabel(item.storedFoodType) : (typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType);
|
||||
item.storedFoodLabel = typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType;
|
||||
if (item.roles) item.roles.food = true;
|
||||
}
|
||||
if (type === "rotator" || type === "reciprocator") configurePresetMechanicalItem(item, opts);
|
||||
|
|
@ -281,7 +305,7 @@
|
|||
const compact = fieldId === "cage" || fieldId === "garden";
|
||||
const center = { x0: worldRef.w * 0.10, x1: worldRef.w * 0.90, y0: worldRef.h * 0.14, y1: worldRef.h * 0.86 };
|
||||
addPresetTarinai(worldRef, presetCount(field, 14, 7, 24), { x0: worldRef.w * 0.08, x1: worldRef.w * 0.30, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 });
|
||||
// 物理ブロックは柵より先に置く。狭い中小フィールドでは柵側を間引き、ブロックを優先する。
|
||||
// \u7269\u7406\u30D6\u30ED\u30C3\u30AF\u306F\u67F5\u3088\u308A\u5148\u306B\u7F6E\u304F\u3002\u72ED\u3044\u4E2D\u5C0F\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u306F\u67F5\u5074\u3092\u9593\u5F15\u304D\u3001\u30D6\u30ED\u30C3\u30AF\u3092\u512A\u5148\u3059\u308B\u3002
|
||||
addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 });
|
||||
addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 });
|
||||
addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 });
|
||||
|
|
@ -354,8 +378,6 @@
|
|||
return "soil";
|
||||
}
|
||||
|
||||
global.TarinaiResetPresets = Object.freeze({ ids: Object.keys(RESET_PRESETS), meta: RESET_PRESETS, normalize: normalizeResetPresetId });
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
reset(seedPopulation = null, fieldType = this.fieldType || "garden", presetId = "default") {
|
||||
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
|
||||
|
|
@ -424,8 +446,8 @@
|
|||
this.updateEffectCounts();
|
||||
this.markTerrainDirty?.("reset");
|
||||
this.rebuildSpatial(true);
|
||||
const label = RESET_PRESETS[preset]?.label || "デフォルト";
|
||||
showToast(`${label}で新しい観察を開始しました。`);
|
||||
const label = RESET_PRESETS[preset]?.label || "\u30C7\u30D5\u30A9\u30EB\u30C8";
|
||||
showToast(`${label}\u3067\u65B0\u3057\u3044\u89B3\u5BDF\u3092\u958B\u59CB\u3057\u307E\u3057\u305F\u3002`);
|
||||
syncTopButtons();
|
||||
},
|
||||
|
||||
|
|
@ -810,17 +832,17 @@
|
|||
const occupants = this.nestBoxOccupants(box, Infinity);
|
||||
const names = occupants.map(t => t?.name).filter(Boolean).slice(0, 5);
|
||||
const more = occupants.length > names.length ? `\u3001\u307b\u304b${occupants.length - names.length}\u5339` : "";
|
||||
return ["\u5de3\u7bb1", names.length ? `\u5165\u3063\u3066\u308b\u5b50: ${names.join("\u3001")}${more}` : "\u5165\u3063\u3066\u308b\u5b50: \u306a\u3057", this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean);
|
||||
return ["\u5de3\u7bb1", names.length ? `\u5165\u3063\u3066\u308b\u5b50: ${names.join("\u3001")}${more}` : "\u5165\u3063\u3066\u308b\u5b50: \u306a\u3057", this.temperatureTooltipLineFor?.(box, "\u5185\u90E8\u6C17\u6E29")].filter(Boolean);
|
||||
},
|
||||
|
||||
|
||||
temperatureTooltipLineFor(target, label = "体感気温") {
|
||||
temperatureTooltipLineFor(target, label = "\u4F53\u611F\u6C17\u6E29") {
|
||||
if (!target || !this.temperatureAt) return "";
|
||||
const temp = this.temperatureAt(target.x, target.y);
|
||||
if (!Number.isFinite(Number(temp))) return "";
|
||||
const status = this.temperatureStatusFor?.(temp) || null;
|
||||
const suffix = status?.label ? `(${status.label})` : "";
|
||||
return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`;
|
||||
const suffix = status?.label ? `\uFF08${status.label}\uFF09` : "";
|
||||
return `${label}: ${Number(temp).toFixed(1)}\u2103${suffix}`;
|
||||
},
|
||||
|
||||
pointerTarinaiTooltipInfo() {
|
||||
|
|
@ -836,9 +858,9 @@
|
|||
if (!best) return null;
|
||||
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "") || window.TEXT_CATALOG?.stateLabel?.(best.state, best) || best.thought || "";
|
||||
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100);
|
||||
const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`;
|
||||
const tempLine = this.temperatureTooltipLineFor(best, "体感気温");
|
||||
return { target: best, lines: [best.name || "たりない", behavior, statusLine, tempLine].filter(Boolean), kind: "tarinai" };
|
||||
const statusLine = `\u4F53\u529B: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)}\u3000\u7A7A\u8179: ${Math.round(best.hunger || 0)}\u3000\u30B9\u30C8\u30EC\u30B9: ${Math.round(best.stress || 0)}`;
|
||||
const tempLine = this.temperatureTooltipLineFor(best, "\u4F53\u611F\u6C17\u6E29");
|
||||
return { target: best, lines: [best.name || "\u305F\u308A\u306A\u3044", behavior, statusLine, tempLine].filter(Boolean), kind: "tarinai" };
|
||||
},
|
||||
|
||||
pointerOwnedBedInfo() {
|
||||
|
|
@ -858,7 +880,7 @@
|
|||
ownedBedTooltipLines(bed) {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
|
||||
const owner = this.liveTarinaiById?.(bed.ownerId) || null;
|
||||
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, this.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean);
|
||||
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, this.temperatureTooltipLineFor?.(bed, "\u5468\u8FBA\u6C17\u6E29")].filter(Boolean);
|
||||
},
|
||||
|
||||
pointerItemTooltipInfo() {
|
||||
|
|
@ -1161,19 +1183,24 @@
|
|||
},
|
||||
|
||||
startFightMochiIntimidation(a, b) {
|
||||
if (!a || !b || a.dead || b.dead) return false;
|
||||
if (!isSocialTarinaiEntity(a) || !isSocialTarinaiEntity(b)) return false;
|
||||
const aDrug = (a.fightMochiTimer || 0) > 0.04;
|
||||
const bDrug = (b.fightMochiTimer || 0) > 0.04;
|
||||
const actor = aDrug && !bDrug ? a : (!aDrug && bDrug ? b : (a.energy + a.stress * 0.36 >= b.energy + b.stress * 0.36 ? a : b));
|
||||
const target = actor === a ? b : a;
|
||||
if (target.isTarinaiChampion) return false;
|
||||
if ((actor.intimidationCooldown || 0) > 0.04 || (target.intimidationCooldown || 0) > 0.04) return false;
|
||||
if (!this.canFightPair(actor, target)) return false;
|
||||
actor.intimidationCooldown = Math.max(actor.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-actor", 5.5, 8.5, actor, target));
|
||||
target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-target", 4.5, 7.5, actor, target));
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.85, 1.35, actor, target));
|
||||
actor.intimidateTargetId = target.id;
|
||||
actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28);
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.75, 1.25, actor, target));
|
||||
target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * target.personalityProfile().fear);
|
||||
const targetProfile = socialPersonalityProfile(target);
|
||||
target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * targetProfile.fear);
|
||||
actor.adjustRelation(target, -0.05, 0.06, "intimidate");
|
||||
target.adjustRelation(actor, -0.12, 0.30 * target.personalityProfile().fear, "intimidate");
|
||||
target.adjustRelation(actor, -0.12, 0.30 * targetProfile.fear, "intimidate");
|
||||
this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, {
|
||||
life: 0.34,
|
||||
size: actor.radius * 0.62,
|
||||
|
|
@ -1201,7 +1228,7 @@
|
|||
if (!this.canFightPair(actor, target)) return false;
|
||||
const actorScore = actor === a ? scoreA : scoreB;
|
||||
const targetScore = actor === a ? scoreB : scoreA;
|
||||
const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite();
|
||||
const canIntimidate = !target.isTarinaiChampion && (actor.intimidationCooldown || 0) <= 0.04 && (target.intimidationCooldown || 0) <= 0.04 && (!actor.lowHealthSprite || !actor.lowHealthSprite());
|
||||
const intimidationChance = 0.30;
|
||||
if (canIntimidate && deterministicChance(this, "conflict-intimidation-choice", intimidationChance, actor, target)) {
|
||||
return this.startIntimidation(actor, target, actorScore, targetScore);
|
||||
|
|
@ -1210,12 +1237,15 @@
|
|||
},
|
||||
|
||||
startIntimidation(actor, target, actorScore = 0, targetScore = 0) {
|
||||
if (!actor || !target || actor.dead || target.dead) return false;
|
||||
if (!isSocialTarinaiEntity(actor) || !isSocialTarinaiEntity(target) || actor === target) return false;
|
||||
if (target.isTarinaiChampion) return false;
|
||||
if ((actor.fightTimer || 0) > 0.04 || (target.fightTimer || 0) > 0.04) return false;
|
||||
if ((actor.intimidateTimer || 0) > 0.04 || (target.intimidateTimer || 0) > 0.04) return false;
|
||||
if ((actor.intimidationCooldown || 0) > 0.04 || (target.intimidationCooldown || 0) > 0.04) return false;
|
||||
if (this.fightPairCooldownRemaining?.(actor, target) > 0.04) return false;
|
||||
if (actor.lowHealthSprite && actor.lowHealthSprite()) return false;
|
||||
const fearLoad = ((typeof target.relationTo === "function" ? (target.relationTo(actor.id).fear || 0) : 0) * 0.72) + target.personalityProfile().fear * 8;
|
||||
const targetProfile = socialPersonalityProfile(target);
|
||||
const fearLoad = ((typeof target.relationTo === "function" ? (target.relationTo(actor.id).fear || 0) : 0) * 0.72) + targetProfile.fear * 8;
|
||||
const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0);
|
||||
const targetAggression = Math.max(0, target.currentPersonality?.aggression || 0);
|
||||
const actorSize = actor.effectiveScale ? actor.effectiveScale() : (actor.scale || 0.28);
|
||||
|
|
@ -1228,6 +1258,8 @@
|
|||
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
||||
actor.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-actor-cooldown", 1, 4, actor, target);
|
||||
target.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-target-cooldown", 1, 4, actor, target);
|
||||
actor.intimidationCooldown = Math.max(actor.intimidationCooldown || 0, deterministicRange(this, "intimidation-repeat-cooldown-actor", 7.5, 11.5, actor, target));
|
||||
target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "intimidation-repeat-cooldown-target", 5.5, 9.0, actor, target));
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer, deterministicRange(this, "intimidation-actor-timer", 1.05, 1.65, actor, target));
|
||||
actor.intimidateTargetId = target.id;
|
||||
actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
|
||||
|
|
@ -1239,10 +1271,10 @@
|
|||
}));
|
||||
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)");
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-target-prep-timer", 0.95, 1.55, actor, target));
|
||||
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
|
||||
target.fearTimer = Math.max(target.fearTimer, 0.65 * targetProfile.fear);
|
||||
if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) {
|
||||
actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation");
|
||||
target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate");
|
||||
target.adjustRelation(actor, -0.08, 0.22 * targetProfile.fear, "intimidate");
|
||||
actor.intimidateTimer = 0;
|
||||
actor.intimidateTargetId = null;
|
||||
target.intimidatedTimer = 0;
|
||||
|
|
@ -1259,17 +1291,19 @@
|
|||
target.defeatedTimer = Math.max(target.defeatedTimer, deterministicRange(this, "intimidation-defeated-timer", 1.8, 2.8, actor, target));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 1.2, 2.0, actor, target));
|
||||
if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" });
|
||||
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * target.personalityProfile().fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); }
|
||||
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * targetProfile.fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); }
|
||||
this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
target.vx += (target.x < actor.x ? -1 : 1) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target);
|
||||
target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target);
|
||||
actor.adjustRelation(target, -0.08, 0.06, "intimidate");
|
||||
target.adjustRelation(actor, -0.22, 0.85 * target.personalityProfile().fear, "intimidate");
|
||||
target.adjustRelation(actor, -0.22, 0.85 * targetProfile.fear, "intimidate");
|
||||
if (this.time - Math.max(actor.lastLog, target.lastLog) > 5.5) {
|
||||
this.log(`${actor.name}\u306f${target.name}\u3092\u5a01\u5687\u3057\u3066\u8ffd\u3044\u6255\u3063\u305f\u3002`, "fight", { participants: [actor, target] });
|
||||
actor.lastLog = target.lastLog = this.time;
|
||||
}
|
||||
this.markFightPairCooldown?.(actor, target, 2.8);
|
||||
this.markFightPairCooldown?.(actor, target, Math.max(6.5, CONFIG.fightCooldown + 3));
|
||||
actor.postConflictPeaceTimer = Math.max(actor.postConflictPeaceTimer || 0, deterministicRange(this, "intimidation-success-peace-actor", 3.8, 6.2, actor, target));
|
||||
target.postConflictPeaceTimer = Math.max(target.postConflictPeaceTimer || 0, deterministicRange(this, "intimidation-success-peace-target", 3.8, 6.2, actor, target));
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1378,7 +1378,7 @@
|
|||
showToast(this.isFenceType(item.type) ? "\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002" : "\u305d\u3053\u306b\u306f\u914d\u7f6e\u3067\u304d\u307e\u305b\u3093\u3002");
|
||||
return null;
|
||||
}
|
||||
if (dropped && (item.type === "genkotsu" || item.type === "stone" || isPinType(item.type))) {
|
||||
if (dropped && (item.type === "genkotsu" || item.type === "stone" || item.type === "ball" || isPinType(item.type))) {
|
||||
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : 0.48);
|
||||
item.dropTimer = item.dropMax;
|
||||
item.dropImpactDone = false;
|
||||
|
|
@ -1457,12 +1457,35 @@
|
|||
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(58, (it.r || 7) * 7.8);
|
||||
const d = distXY(x, y, it.x, it.y);
|
||||
if (d <= hit && d < plushieTargetD) { plushieTarget = it; plushieTargetD = d; }
|
||||
const owner = it.carriedById ? this.liveTarinaiById?.(it.carriedById) || (this.tarinai || []).find(t => t && !t.dead && t.id === it.carriedById) : null;
|
||||
const cx = owner ? owner.x : it.x;
|
||||
const cy = owner ? owner.y - Math.max(22, (owner.radius || 24) * 1.02) : it.y;
|
||||
const hitX = Math.max(72, (owner?.radius || 24) * 1.45, (it.r || 7) * 9.0);
|
||||
const hitY = Math.max(86, (owner?.radius || 24) * 1.75, (it.r || 7) * 10.5);
|
||||
const ndx = (x - cx) / hitX;
|
||||
const ndy = (y - cy) / hitY;
|
||||
const elliptical = ndx * ndx + ndy * ndy;
|
||||
const d = Math.sqrt(Math.max(0, elliptical));
|
||||
if (elliptical <= 1.0 && d < plushieTargetD) {
|
||||
plushieTarget = it;
|
||||
plushieTargetD = d;
|
||||
}
|
||||
}
|
||||
if (plushieTarget && typeof plushieTarget.fling === "function") {
|
||||
if (plushieTarget.fling(this, x, y, { source: "poke", speed: 860, life: 3.0 })) {
|
||||
const owner = plushieTarget.carriedById ? this.liveTarinaiById?.(plushieTarget.carriedById) || (this.tarinai || []).find(t => t && !t.dead && t.id === plushieTarget.carriedById) : null;
|
||||
const cx = owner ? owner.x : plushieTarget.x;
|
||||
const cy = owner ? owner.y - Math.max(22, (owner.radius || 24) * 1.02) : plushieTarget.y;
|
||||
let dx = (Number(cx || 0) || 0) - (Number(x || 0) || 0);
|
||||
let dy = (Number(cy || 0) || 0) - (Number(y || 0) || 0);
|
||||
if (Math.hypot(dx, dy) < 12 || Math.abs(dx) < Math.abs(dy) * 0.36) {
|
||||
dx = Math.sign(dx) || Number(owner?.facing || owner?.faceDir || 1) || 1;
|
||||
dy = -0.42;
|
||||
}
|
||||
const len = Math.max(1, Math.hypot(dx, dy));
|
||||
const speed = 920;
|
||||
const vx = dx / len * speed;
|
||||
const vy = Math.min(dy / len * speed - 190, -260);
|
||||
if (plushieTarget.fling(this, x, y, { source: "poke", vx, vy, speed, life: 3.1 })) {
|
||||
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?.();
|
||||
|
|
|
|||
|
|
@ -333,11 +333,6 @@
|
|||
this.itemBucketRebuildsTotal = (this.itemBucketRebuildsTotal || 0) + 1;
|
||||
},
|
||||
|
||||
liveItemsOfType(type, predicate = null) {
|
||||
const list = this.itemsOfType ? this.itemsOfType(type) : (this.items || []).filter(it => it && !it.dead && it.type === type);
|
||||
return predicate ? list.filter(predicate) : list;
|
||||
},
|
||||
|
||||
updateEffectCounts() {
|
||||
const counts = this.effectCounts;
|
||||
for (const key of Object.keys(counts)) counts[key] = 0;
|
||||
|
|
|
|||
|
|
@ -181,6 +181,20 @@
|
|||
if (isMechanism) global.TarinaiLinkRuntime.snapEndpointToTarget(best, this);
|
||||
bestD = d;
|
||||
}
|
||||
if ((this.tool === "rope" || this.tool === "rod") && bestD > 14) {
|
||||
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
|
||||
const it = this.items[i];
|
||||
if (!it || it.dead || !global.TarinaiMechanicalSystem.isMechanicalType(it.type)) continue;
|
||||
const reach = global.TarinaiMechanicalSystem.reach?.(it) || this.rotatorExtent?.(it) || Math.max(64, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > reach + 32) continue;
|
||||
const result = global.TarinaiCollisionFootprints.hitTestItem(this, it, x, y, { padding: 26, radiusMultiplier: 1.45 }) || { hit: false, distance: Infinity };
|
||||
if (!result.hit || result.distance >= bestD) continue;
|
||||
const local = this.linkPointLocalForItem?.(it, x, y, true) || { localX: 0, localY: 0 };
|
||||
best = { kind: "item", id: it.id, type: it.type, label: toolLabel(it.type), x, y, localX: local.localX, localY: local.localY, center: false };
|
||||
global.TarinaiLinkRuntime.snapEndpointToTarget(best, this);
|
||||
bestD = result.distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -10,17 +10,7 @@
|
|||
},
|
||||
|
||||
colonyMoodDefinition(id = "relaxed") {
|
||||
const aliases = {
|
||||
calm: "relaxed",
|
||||
prickly: "confusion",
|
||||
stinky: "disease_spread",
|
||||
weaklings: "weakened",
|
||||
zunda_party: "happy",
|
||||
ball_party: "happy",
|
||||
sun_party: "happy",
|
||||
ant_overrun: "fearful",
|
||||
};
|
||||
const canonicalId = aliases[String(id || "")] || String(id || "relaxed");
|
||||
const canonicalId = String(id || "relaxed");
|
||||
const defs = {
|
||||
devastation: { id: "devastation", label: "\u58ca\u6ec5\u5bf8\u524d", description: "\u30b3\u30ed\u30cb\u30fc\u306e\u5b58\u7d9a\u304c\u304b\u306a\u308a\u5371\u3046\u3044\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
crisis: { id: "crisis", label: "\u5371\u6a5f", description: "\u3051\u304c\u3084\u6d88\u8017\u304c\u76ee\u7acb\u3061\u3001\u7acb\u3066\u76f4\u3057\u304c\u5fc5\u8981\u306a\u72b6\u614b\u3067\u3059\u3002", effects: { personality: {} } },
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ $Routing = Get-Content -Raw -LiteralPath $RoutingPath | ConvertFrom-Json
|
|||
$Exact = @{
|
||||
".gitignore" = "ignore rules for local/generated artifacts"
|
||||
".htaccess" = "Apache/static-host cache, MIME, and service-worker behavior"
|
||||
"app_manifest.json" = "canonical version plus CSS/JS/static asset manifest"
|
||||
"app_manifest.json" = "canonical version plus CSS/JS app shell manifest"
|
||||
"favicon.ico" = "legacy favicon"
|
||||
"index.html" = "app DOM shell, canvas, panels, dialogs, and ordered script tags"
|
||||
"README.md" = "small AI entrypoint and read strategy"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ EXCLUDE_DIRS = {".git", "__pycache__"}
|
|||
EXACT_DESCRIPTIONS = {
|
||||
".gitignore": "ignore rules for local/generated artifacts",
|
||||
".htaccess": "Apache/static-host cache, MIME, and service-worker behavior",
|
||||
"app_manifest.json": "canonical version plus CSS/JS/static asset manifest",
|
||||
"app_manifest.json": "canonical version plus CSS/JS app shell manifest",
|
||||
"favicon.ico": "legacy favicon",
|
||||
"index.html": "app DOM shell, canvas, panels, dialogs, and ordered script tags",
|
||||
"README.md": "small AI entrypoint and read strategy",
|
||||
|
|
|
|||
|
|
@ -364,7 +364,6 @@ def check_item_bucket_polish() -> None:
|
|||
"addItem(item",
|
||||
"itemById(id)",
|
||||
"itemsOfType(type)",
|
||||
"liveItemsOfType(type",
|
||||
"this.itemIdMap = new Map()",
|
||||
"bucketRebuildsTotal",
|
||||
]
|
||||
|
|
@ -423,12 +422,15 @@ def check_current_save_format() -> None:
|
|||
if offenders:
|
||||
fail("broad snapshot fields still present after light-save conversion: " + ", ".join(offenders))
|
||||
required_codec = [
|
||||
'EXPORT_PREFIX = "\\u305f\\u308a"',
|
||||
'RAW_CODEC = "\\u751f"',
|
||||
'DEFLATE_CODEC = "\\u7e2e"',
|
||||
'EXPORT_PREFIX = "\\u305f"',
|
||||
'SAVE_TEXT_BITS = 12',
|
||||
"SAVE_HEADER_CHARS",
|
||||
"DICT_DEFLATE_CODEC",
|
||||
"dictionaryTransform",
|
||||
"writeStringTable",
|
||||
"SAVE_TEXT_ALPHABET",
|
||||
"jp2048Encode",
|
||||
"jp2048Decode",
|
||||
"jp4096Encode",
|
||||
"jp4096Decode",
|
||||
'CompressionStream("deflate-raw")',
|
||||
"DecompressionStream",
|
||||
"async function encodeSnapshot",
|
||||
|
|
@ -439,6 +441,7 @@ def check_current_save_format() -> None:
|
|||
fail(f"binary save codec token missing: {token}")
|
||||
required_storage = [
|
||||
'STORAGE_PREFIX = "tarinai_japanese_hash_slot_v1_"',
|
||||
'hash.startsWith("\\u305f")',
|
||||
"function writeSlot",
|
||||
"function readSlot",
|
||||
"function requireSlot",
|
||||
|
|
@ -1041,10 +1044,8 @@ const t = {{
|
|||
updateZunchiDisease() {{ calls.push('zunchiDisease'); }},
|
||||
updateSpecialDiseases() {{ calls.push('specialDisease'); }},
|
||||
freezeSleepDiseaseMotion() {{ return false; }},
|
||||
applyCursorContactCare() {{ calls.push('cursorCare'); }},
|
||||
personalityProfile() {{ return {{ fear: 1 }}; }},
|
||||
updateFacing() {{ calls.push('facing'); }},
|
||||
updateSunbath() {{ calls.push('sunbath'); }},
|
||||
maintainTargetProgress() {{ calls.push('targetProgress'); }},
|
||||
move() {{ calls.push('move'); }},
|
||||
updateNestBoxPresence() {{ calls.push('nestBox'); }},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "39.15.90";
|
||||
const APP_VERSION = "39.15.98";
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue