1.9
This commit is contained in:
parent
fc7c94bd69
commit
2cc2f003df
71 changed files with 3359 additions and 1027 deletions
|
|
@ -4,24 +4,26 @@
|
|||
const STORAGE_KEY = "tarinai_achievements_v2";
|
||||
const PLAYER_KEY = "tarinai_achievement_player_v1";
|
||||
const API_URL = String(global.TARINAI_ACHIEVEMENT_API || "achievement_api.php");
|
||||
const GAME_VERSION = String(global.TARINAI_VERSION || "39.16.60");
|
||||
const GAME_VERSION = String(global.TARINAI_VERSION || "39.16.74");
|
||||
const DEBUG_MODE = ["1", "true"].includes(new URLSearchParams(global.location?.search || "").get("debug"));
|
||||
const PLAYER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const ABSOLUTE_ZERO_C = -273.15;
|
||||
const CONTINUOUS_PLAY_TARGET_MS = 60 * 60 * 1000;
|
||||
const CONTINUOUS_PLAY_24H_TARGET_MS = 24 * 60 * 60 * 1000;
|
||||
const HELD_TARGET_MS = 30 * 1000;
|
||||
const COMPLETIONIST_ID = "true_tarinai_observer";
|
||||
const GENERATION_TARGET = 10;
|
||||
const CHAOS_FIGHT_TARGET = 666;
|
||||
const SNIPER_SHOT_TARGET = 333;
|
||||
const FERTILITY_BIRTH_TARGET = 721;
|
||||
const DAILY_PLAY_TARGET = 7;
|
||||
const WIRE_SHOCK_TARGET = 7;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const ROBOT_CLEAN_TARGET = 200;
|
||||
const ROBOT_ALL_TARGET_MASK = 0x3f;
|
||||
const SYNC_DEBOUNCE_MS = 450;
|
||||
const FULL_SYNC_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const SPELL_STATE_VERSION = 3;
|
||||
const PREVIOUS_SPELL_STATE_VERSION = 2;
|
||||
const LEGACY_SPELL_STATE_VERSION = 1;
|
||||
const SPELL_STATE_VERSION = 5;
|
||||
const MEDICINE_LEDGER_TYPES = Object.freeze(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]);
|
||||
const DIRECT_TREATMENT_TYPES = new Set(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]);
|
||||
const PRE_UNLOCK_DESCRIPTION_IDS = new Set([
|
||||
|
|
@ -41,6 +43,8 @@
|
|||
"ants_alive_25",
|
||||
"great_mother_1000_births",
|
||||
"continuous_play_1_hour",
|
||||
"low_fps_single_digit",
|
||||
"continuous_play_24_hours",
|
||||
"robot_cleaner_100",
|
||||
"mercury_lifespan",
|
||||
"enemy_enemy_friend",
|
||||
|
|
@ -48,6 +52,7 @@
|
|||
"town_doctor_50",
|
||||
"chaos_seeker_666_fights",
|
||||
"fertility_seeker_721_love_births",
|
||||
"daily_play_7_days",
|
||||
]);
|
||||
const ACHIEVEMENT_CATEGORIES = Object.freeze([
|
||||
Object.freeze({
|
||||
|
|
@ -56,6 +61,7 @@
|
|||
ids: Object.freeze([
|
||||
"first_birth", "natural_zunchi_slave", "natural_tarinai_king",
|
||||
"natural_zunchi_slave_5_generations", "natural_tarinai_king_3_generations",
|
||||
"ant_nest_without_tarinai",
|
||||
"birth_50_in_60_seconds", "great_mother_1000_births", "lifespan_completed",
|
||||
"all_non_sleep_diseased_25", "colony_happy", "direct_feed_33",
|
||||
"safe_colony_25_5_minutes", "ants_alive_25", "overprotective",
|
||||
|
|
@ -72,15 +78,16 @@
|
|||
"ants_killed_100", "below_absolute_zero_item", "sauna_cold_plunge",
|
||||
"medicine_ledger_all", "mercury_lifespan", "enemy_enemy_friend",
|
||||
"revolution", "fuel_to_fire", "king_full_satisfaction", "slave_zero_satisfaction",
|
||||
"chaos_seeker_666_fights",
|
||||
"chaos_seeker_666_fights", "sticky_bomb_15_passes", "wire_shock_7_tarinai",
|
||||
]),
|
||||
}),
|
||||
Object.freeze({
|
||||
id: "construction",
|
||||
title: "\u5efa\u7bc9",
|
||||
ids: Object.freeze([
|
||||
"placed_objects_100", "mechanized_industry", "robot_cleaner_100",
|
||||
"placed_objects_100", "mechanized_industry", "information_industry", "robot_cleaner_100",
|
||||
"minimalist_happy", "unplanned_city_30", "clean_freak_robot_only", "megalopolis",
|
||||
"sandbox_five_toilets", "park_ground_changed", "ground_change_4_in_1_second",
|
||||
]),
|
||||
}),
|
||||
Object.freeze({
|
||||
|
|
@ -88,8 +95,9 @@
|
|||
title: "\u305d\u306e\u4ed6",
|
||||
ids: Object.freeze([
|
||||
"idle_observer_5_minutes", "secret_collection_9_slots", "pause_spam_4_in_1_second",
|
||||
"continuous_play_1_hour", "undo_mass_revival", "held_30_seconds",
|
||||
"undo_20", "redo_20", "poke_plushie_fling", "true_tarinai_observer", "sniper_333_shots",
|
||||
"undo_mass_revival", "held_30_seconds", "undo_20", "redo_20",
|
||||
"poke_plushie_fling", "sniper_333_shots", "low_fps_single_digit",
|
||||
"daily_play_7_days", "continuous_play_24_hours", "continuous_play_1_hour", "true_tarinai_observer",
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -111,14 +119,14 @@
|
|||
Object.freeze({ id: "direct_feed_33", title: "\u990c\u3084\u308a\u3058\u3044\u3055\u3093", description: "\u624b\u304b\u3089\u76f4\u63a533\u56de\u990c\u3092\u4e0e\u3048\u308b\u3002" }),
|
||||
Object.freeze({ id: "ignite_during_birth_ritual", title: "\u71c3\u3048\u4e0a\u304c\u308b\u604b", description: "\u305f\u308a\u306a\u3044\u304c\u30d8\u30b3\u30d8\u30b3\u4e2d\u306b\u7740\u706b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "laxative_starvation", title: "\u5185\u81d3\u5168\u90e8\u51fa\u305f", description: "\u4e0b\u5264\u306e\u52b9\u679c\u4e2d\u306b\u9913\u6b7b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "idle_observer_5_minutes", title: "\u76e3\u8996\u30ab\u30e1\u30e9", description: "\u30b2\u30fc\u30e0\u51855\u5206\u9593\u3001\u914d\u7f6e\u30fb\u524a\u9664\u30fb\u3064\u3064\u304f\u30fb\u3064\u307e\u3080\u30fb\u5c04\u6483\u3092\u884c\u308f\u306a\u3044\u3002" }),
|
||||
Object.freeze({ id: "idle_observer_5_minutes", title: "\u76e3\u8996\u30ab\u30e1\u30e9", description: "5\u5206\u9593\u753b\u9762\u3092\u898b\u308b\u3060\u3051" }),
|
||||
Object.freeze({ id: "placed_objects_100", title: "\u7269\u3067\u3044\u3063\u3071\u3044", description: "\u914d\u7f6e\u7269\u3092\u7d2f\u8a08100\u500b\u7f6e\u304f\u3002" }),
|
||||
Object.freeze({ id: "mechanized_industry", title: "\u6a5f\u68b0\u5316\u7523\u696d", description: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3067\u63a5\u7d9a\u5148\u3092\u4f5c\u52d5\u3055\u305b\u3001\u30ed\u30fc\u30d7\u30fb\u68d2\u30fb\u3070\u306d\u3092\u305d\u308c\u305e\u308c1\u672c\u4ee5\u4e0a\u8a2d\u7f6e\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "safe_colony_25_5_minutes", title: "\u5b89\u5168\u7b2c\u4e00", description: "25\u4f53\u4ee5\u4e0a\u3044\u308b\u72b6\u614b\u3092\u30b2\u30fc\u30e0\u51855\u5206\u9593\u7dad\u6301\u3057\u3001\u305d\u306e\u9593\u6b7b\u4ea1\u8005\u3092\u51fa\u3055\u306a\u3044\u3002" }),
|
||||
Object.freeze({ id: "safe_colony_25_5_minutes", title: "\u5b89\u5168\u7b2c\u4e00", description: "25\u4f53\u4ee5\u4e0a\u3044\u308b\u72b6\u614b\u3092\u30b2\u30fc\u30e0\u51855\u5206\u9593\u7dad\u6301\u3057\u3001\u305d\u306e\u9593\u5bff\u547d\u4ee5\u5916\u3067\u6b7b\u4ea1\u8005\u3092\u51fa\u3055\u306a\u3044\u3002" }),
|
||||
Object.freeze({ id: "ants_alive_25", title: "\u30a2\u30ea\u306e\u60d1\u661f", description: "\u751f\u5b58\u3057\u3066\u3044\u308b\u30a2\u30ea\u304c25\u4f53\u306b\u5230\u9054\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "ants_killed_100", title: "\u5bb3\u866b\u99c6\u9664", description: "\u30a2\u30ea\u3092\u7d2f\u8a08100\u4f53\u5012\u3059\u3002" }),
|
||||
Object.freeze({ id: "secret_collection_9_slots", title: "\u79d8\u5bc6\u306e\u30b3\u30ec\u30af\u30b7\u30e7\u30f3", description: "\u30bb\u30fc\u30d6\u30b9\u30ed\u30c3\u30c89\u3064\u3092\u3059\u3079\u3066\u57cb\u3081\u308b\u3002" }),
|
||||
Object.freeze({ id: "pause_spam_4_in_1_second", title: "\u4f55\u3084\u3063\u3066\u308b\u306e\uff1f", description: "\u4e00\u6642\u505c\u6b62\u30dc\u30bf\u30f3\u30921\u79d2\u9593\u306b4\u56de\u9023\u6253\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "pause_spam_4_in_1_second", title: "\u4f55\u3084\u3063\u3066\u308b\u306e\uff1f", description: "\u4f55\u3082\u306a\u3044\u90e8\u5206\u30921\u79d2\u9593\u306b4\u56de\u9023\u6253\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "continuous_play_1_hour", title: "\u3081\u3063\u3061\u3083\u305f\u308a\u306a\u3044\u89b3\u5bdf", description: "1\u6642\u9593\u9023\u7d9a\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "below_absolute_zero_item", title: "\u7269\u7406\u6cd5\u5247\u3092\u4e0b\u5074\u306b\u8d85\u8d8a", description: "\u6e29\u5ea6\u8a08\u7b97\u3055\u308c\u308b\u30a2\u30a4\u30c6\u30e0\u30921\u3064\u7d76\u5bfe\u96f6\u5ea6\u3092\u4e0b\u56de\u308b\u6e29\u5ea6\u306b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "undo_mass_revival", title: "\u898b\u306a\u304b\u3063\u305f\u3053\u3068\u306b\u3057\u3088\u3046", description: "\u4e00\u5ea6\u306eUndo\u306710\u4f53\u4ee5\u4e0a\u306e\u6b7b\u4ea1\u3092\u306a\u304b\u3063\u305f\u3053\u3068\u306b\u3059\u308b\u3002" }),
|
||||
|
|
@ -128,7 +136,7 @@
|
|||
Object.freeze({ id: "overprotective", title: "\u904e\u4fdd\u8b77", description: "\u540c\u3058\u305f\u308a\u306a\u3044\u306b\u624b\u304b\u308910\u56de\u990c\u3092\u4e0e\u3048\u30013\u56de\u4ee5\u4e0a\u85ac\u3067\u6cbb\u7642\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "self_sufficient", title: "\u81ea\u7d66\u81ea\u8db3", description: "\u8907\u88fd\u6a5f\u304c\u306a\u3044\u72b6\u614b\u3067\u300120\u4f53\u4ee5\u4e0a\u3092\u76f4\u63a5\u7d66\u990c\u305b\u305a10\u5206\u9593\u7dad\u6301\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "unplanned_city_30", title: "\u7121\u8a08\u753b\u90fd\u5e02", description: "\u914d\u7f6e\u5f8c30\u79d2\u4ee5\u5185\u306b\u524a\u9664\u3059\u308b\u64cd\u4f5c\u3092\u7d2f\u8a0830\u56de\u884c\u3046\u3002" }),
|
||||
Object.freeze({ id: "sauna_cold_plunge", title: "\u30b5\u30a6\u30ca\u6c34\u98a8\u5442", description: "\u7d76\u5bfe\u96f6\u5ea6\u3092\u4e0b\u56de\u308b\u6e29\u5ea6\u3092\u4f5c\u308b\u3002" }),
|
||||
Object.freeze({ id: "sauna_cold_plunge", title: "\u30b5\u30a6\u30ca\u6c34\u98a8\u5442", description: "\u6691\u3059\u304e\u308b\u72b6\u614b\u306b\u306a\u3063\u305f\u305f\u308a\u306a\u3044\u3092\u300115\u79d2\u4ee5\u5185\u306b\u5bd2\u3059\u304e\u308b\u72b6\u614b\u306b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "rain_shelter_all", title: "\u96e8\u5bbf\u308a\u5b8c\u4e86", description: "\u96e8\u5929\u6642\u300120\u4f53\u4ee5\u4e0a\u306e\u5168\u54e1\u3092\u30d1\u30e9\u30bd\u30eb\u30fb\u5de3\u7bb1\u30fb\u571f\u7ba1\u3067\u96e8\u5bbf\u308a\u3055\u305b\u308b\u3002" }),
|
||||
Object.freeze({ id: "medicine_ledger_all", title: "\u304a\u304f\u3059\u308a\u624b\u5e33\u5168\u57cb\u3081", description: "\u5168\u7a2e\u985e\u306e\u85ac\u3092\u5c11\u306a\u304f\u3068\u30821\u56de\u4f7f\u7528\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "mercury_lifespan", title: "\u4e0d\u8001\u4e0d\u6b7b\u307e\u3067\u3042\u3068\u4e00\u6b69", description: "\u6c34\u9280\u3092\u4f7f\u7528\u3057\u305f\u305f\u308a\u306a\u3044\u304c\u5929\u5bff\u3092\u5168\u3046\u3059\u308b\u3002" }),
|
||||
|
|
@ -148,6 +156,16 @@
|
|||
Object.freeze({ id: "sniper_333_shots", title: "\u72d9\u6483\u624b", description: "\u7d2f\u8a08333\u767a\u6483\u3064\u3002" }),
|
||||
Object.freeze({ id: "megalopolis", title: "\u30e1\u30ac\u30ed\u30dd\u30ea\u30b9", description: "\u8907\u88fd\u6a5f\u309210\u500b\u4ee5\u4e0a\u3001\u5e72\u8349\u5bdd\u5e8a\u30fb\u5de3\u7bb1\u30fb\u571f\u7ba1\u3092\u5408\u8a0815\u500b\u4ee5\u4e0a\u8a2d\u7f6e\u3057\u3001\u305f\u308a\u306a\u3044\u3092100\u4f53\u4ee5\u4e0a\u751f\u5b58\u3055\u305b\u308b\u3002" }),
|
||||
Object.freeze({ id: "fertility_seeker_721_love_births", title: "\u8c4a\u7a63\u30f2\u5e0c\u6c42\u30b9\u30eb\u8005", description: "\u3078\u3053\u9905\u306e\u5f71\u97ff\u4e0b\u3067\u7e41\u6b96\u3092721\u56de\u6210\u529f\u3055\u305b\u308b\u3002" }),
|
||||
Object.freeze({ id: "low_fps_single_digit", title: "\u30b9\u30da\u30c3\u30af\u304c\u305f\u308a\u306a\u3044", description: "fps\u30921\u6841\u306b\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "continuous_play_24_hours", title: "\u5bdd\u308d", description: "24\u6642\u9593\u9023\u7d9a\u3067\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "sandbox_five_toilets", title: "\u7802\u5834", description: "\u30c8\u30a4\u30ec\u306e\u7802\u3092\u30d5\u30a3\u30fc\u30eb\u30c9\u306b5\u500b\u8a2d\u7f6e\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "park_ground_changed", title: "\u305d\u3053\u516c\u5171\u7a7a\u9593\u3060\u3088\uff1f", description: "\u516c\u5712\u306e\u5730\u9762\u3092\u52dd\u624b\u306b\u5909\u66f4\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "ground_change_4_in_1_second", title: "\u7834\u683c\u306e\u5de5\u4e8b\u8cbb\u7528", description: "\u5730\u9762\u3092\u5909\u3048\u3059\u304e\u3066\u7834\u7523\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "ant_nest_without_tarinai", title: "\u30a2\u30ea\u89b3\u5bdf", description: "\u30a2\u30ea\u306e\u5de3\u304c\u3042\u308b\u304c\u3001\u305f\u308a\u306a\u3044\u304c\u3044\u306a\u3044\u3002" }),
|
||||
Object.freeze({ id: "sticky_bomb_15_passes", title: "\u547d\u306e\u30d0\u30c8\u30f3", description: "\u7c98\u7740\u30dc\u30e0\u3092\u7206\u767a\u307e\u3067\u306b15\u56de\u4ee5\u4e0a\u30d1\u30b9\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "daily_play_7_days", title: "\u6bce\u65e5\u305f\u308a\u306a\u3044\u89b3\u5bdf", description: "7\u65e5\u9023\u7d9a\u3067\u30d7\u30ec\u30a4\u3059\u308b\u3002" }),
|
||||
Object.freeze({ id: "wire_shock_7_tarinai", title: "\u62b5\u6297\u5668\u3058\u3083\u306a\u3044", description: "\u96fb\u6d41\u304c\u6d41\u308c\u3066\u3044\u308b1\u672c\u306e\u96fb\u7dda\u30677\u4f53\u306e\u305f\u308a\u306a\u3044\u3092\u611f\u96fb\u3055\u305b\u308b\u3002" }),
|
||||
Object.freeze({ id: "information_industry", title: "\u60c5\u5831\u5316\u7523\u696d", description: "\u57fa\u76e4\u540c\u58eb\u3092\u96fb\u7dda\u985e\u3067\u3064\u306a\u304e\u3001\u4fe1\u53f7\u3092\u6d41\u3059\u3002" }),
|
||||
]);
|
||||
const definitionById = new Map(DEFINITIONS.map(def => [def.id, def]));
|
||||
const CATEGORY_DEFINITIONS = new Map(ACHIEVEMENT_CATEGORIES.map(category => [
|
||||
|
|
@ -177,17 +195,19 @@
|
|||
let requestSerial = 0;
|
||||
let runtimeByWorld = new WeakMap();
|
||||
let dangerFightDeathsByWorld = new WeakMap();
|
||||
let wireShockTrackers = new WeakMap();
|
||||
let syncQueue = Promise.resolve();
|
||||
let toastQueue = [];
|
||||
let toastActive = false;
|
||||
let pauseClickTimes = [];
|
||||
let continuousPlayStartedAt = document.hidden ? 0 : Date.now();
|
||||
let emptyClickTimes = [];
|
||||
let groundChangeTimes = [];
|
||||
let continuousPlayStartedAt = Date.now();
|
||||
let continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
|
||||
let lastListSignature = "";
|
||||
let scheduledSyncTimer = 0;
|
||||
|
||||
function blankState() {
|
||||
return { unlocked: {}, pending: [], resetPending: false, debugUnlocked: [], progress: { placementCount: 0, linkTypes: [], signalActivated: false, antKills: 0, birthCount: 0, quickDeleteCount: 0, undoCount: 0, redoCount: 0, medicineTypes: [], firstAidHeals: 0, fightMochiFightCount: 0, shotCount: 0, loveMochiBirthCount: 0 } };
|
||||
return { unlocked: {}, pending: [], resetPending: false, debugUnlocked: [], progress: { placementCount: 0, linkTypes: [], signalActivated: false, antKills: 0, birthCount: 0, quickDeleteCount: 0, undoCount: 0, redoCount: 0, medicineTypes: [], firstAidHeals: 0, fightMochiFightCount: 0, shotCount: 0, loveMochiBirthCount: 0, dailyPlayStreak: 0, dailyPlayLastDay: 0 } };
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
|
|
@ -235,6 +255,8 @@
|
|||
fightMochiFightCount: Math.max(0, Math.floor(Number(progressSource.fightMochiFightCount || 0) || 0)),
|
||||
shotCount: Math.max(0, Math.floor(Number(progressSource.shotCount || 0) || 0)),
|
||||
loveMochiBirthCount: Math.max(0, Math.floor(Number(progressSource.loveMochiBirthCount || 0) || 0)),
|
||||
dailyPlayStreak: Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(progressSource.dailyPlayStreak || 0) || 0))),
|
||||
dailyPlayLastDay: Math.max(0, Math.floor(Number(progressSource.dailyPlayLastDay || 0) || 0)),
|
||||
};
|
||||
const missingCompletionRequirement = DEFINITIONS.some(definition => definition.id !== COMPLETIONIST_ID && !unlocked[definition.id]);
|
||||
if (unlocked[COMPLETIONIST_ID] && missingCompletionRequirement) {
|
||||
|
|
@ -316,10 +338,9 @@
|
|||
}
|
||||
|
||||
function continuousPlayElapsedMs(now = Date.now()) {
|
||||
if (state.unlocked.continuous_play_1_hour) return CONTINUOUS_PLAY_TARGET_MS;
|
||||
if (state.unlocked.continuous_play_24_hours) return CONTINUOUS_PLAY_24H_TARGET_MS;
|
||||
const timestamp = Number(now);
|
||||
if (document.hidden || !continuousPlayStartedAt || !Number.isFinite(timestamp)) return 0;
|
||||
if (continuousPlayLastHeartbeatAt && timestamp - continuousPlayLastHeartbeatAt > 15000) return 0;
|
||||
if (!continuousPlayStartedAt || !Number.isFinite(timestamp)) return 0;
|
||||
return Math.max(0, timestamp - continuousPlayStartedAt);
|
||||
}
|
||||
|
||||
|
|
@ -351,10 +372,18 @@
|
|||
const count = state.unlocked[id] ? target : Math.min(target, Math.max(0, Math.floor(Number(state.progress?.loveMochiBirthCount || 0) || 0)));
|
||||
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / target * 100).toFixed(1)}%\uff08${count} / ${target}\u56de\uff09`;
|
||||
}
|
||||
if (id === "daily_play_7_days") {
|
||||
const count = state.unlocked[id] ? DAILY_PLAY_TARGET : Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(state.progress?.dailyPlayStreak || 0) || 0)));
|
||||
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(count / DAILY_PLAY_TARGET * 100).toFixed(1)}%\uff08${count} / ${DAILY_PLAY_TARGET}\u65e5\uff09`;
|
||||
}
|
||||
if (id === "continuous_play_1_hour") {
|
||||
const elapsed = Math.min(CONTINUOUS_PLAY_TARGET_MS, continuousPlayElapsedMs());
|
||||
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(elapsed / CONTINUOUS_PLAY_TARGET_MS * 100).toFixed(1)}%\uff08${formatElapsed(elapsed)} / 60:00\uff09`;
|
||||
}
|
||||
if (id === "continuous_play_24_hours") {
|
||||
const elapsed = Math.min(CONTINUOUS_PLAY_24H_TARGET_MS, continuousPlayElapsedMs());
|
||||
return `\u73fe\u5728\u306e\u9054\u6210\u7387 ${(elapsed / CONTINUOUS_PLAY_24H_TARGET_MS * 100).toFixed(1)}%\uff08${formatElapsed(elapsed)} / 1440:00\uff09`;
|
||||
}
|
||||
if (id === "enemy_enemy_friend") {
|
||||
const world = global.world || null;
|
||||
const count = state.unlocked[id] ? 10 : Math.min(10, Math.max(0, Math.floor(Number(world?.achievementEnemyAntKills || 0) || 0)));
|
||||
|
|
@ -375,10 +404,13 @@
|
|||
const local = [
|
||||
Math.min(3333, Math.max(0, Number(state.progress?.birthCount || 0) || 0)),
|
||||
Math.floor(continuousPlayElapsedMs() / 1000),
|
||||
Math.max(0, Math.floor(Number(global.__tarinaiFps || 0) || 0)),
|
||||
Math.min(10, Math.max(0, Number(global.world?.achievementEnemyAntKills || 0) || 0)),
|
||||
Math.min(50, Math.max(0, Number(state.progress?.firstAidHeals || 0) || 0)),
|
||||
Math.min(CHAOS_FIGHT_TARGET, Math.max(0, Number(state.progress?.fightMochiFightCount || 0) || 0)),
|
||||
Math.min(FERTILITY_BIRTH_TARGET, Math.max(0, Number(state.progress?.loveMochiBirthCount || 0) || 0)),
|
||||
Math.min(DAILY_PLAY_TARGET, Math.max(0, Number(state.progress?.dailyPlayStreak || 0) || 0)),
|
||||
Math.max(0, Number(state.progress?.dailyPlayLastDay || 0) || 0),
|
||||
].join(":");
|
||||
return `${unlocks}|${rates}|${local}`;
|
||||
}
|
||||
|
|
@ -504,6 +536,10 @@
|
|||
dom.list.replaceChildren(...groups);
|
||||
}
|
||||
|
||||
function renderIfDialogOpen() {
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
}
|
||||
|
||||
function resetUnlockToastVisual() {
|
||||
if (!dom.toast) return;
|
||||
dom.toast.classList.remove("absorbing");
|
||||
|
|
@ -793,6 +829,8 @@
|
|||
state.progress.fightMochiFightCount = Math.max(0, Math.floor(Number(state.progress.fightMochiFightCount || 0) || 0));
|
||||
state.progress.shotCount = Math.max(0, Math.floor(Number(state.progress.shotCount || 0) || 0));
|
||||
state.progress.loveMochiBirthCount = Math.max(0, Math.floor(Number(state.progress.loveMochiBirthCount || 0) || 0));
|
||||
state.progress.dailyPlayStreak = Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(state.progress.dailyPlayStreak || 0) || 0)));
|
||||
state.progress.dailyPlayLastDay = Math.max(0, Math.floor(Number(state.progress.dailyPlayLastDay || 0) || 0));
|
||||
return state.progress;
|
||||
}
|
||||
|
||||
|
|
@ -855,35 +893,14 @@
|
|||
state.unlocked.chaos_seeker_666_fights ? 0 : Math.min(CHAOS_FIGHT_TARGET, progress.fightMochiFightCount),
|
||||
state.unlocked.sniper_333_shots ? 0 : Math.min(SNIPER_SHOT_TARGET, progress.shotCount),
|
||||
state.unlocked.fertility_seeker_721_love_births ? 0 : Math.min(FERTILITY_BIRTH_TARGET, progress.loveMochiBirthCount),
|
||||
state.unlocked.daily_play_7_days ? 0 : Math.min(DAILY_PLAY_TARGET, progress.dailyPlayStreak),
|
||||
state.unlocked.daily_play_7_days ? 0 : Math.min(131071, progress.dailyPlayLastDay),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function parseSpellState(payload = null) {
|
||||
if (!Array.isArray(payload)) return null;
|
||||
const version = Number(payload[0]);
|
||||
if (version === LEGACY_SPELL_STATE_VERSION) {
|
||||
const maskHex = String(payload[1] || "").toLowerCase();
|
||||
const expectedBytes = Math.ceil(DEFINITIONS.length / 8);
|
||||
if (!/^[0-9a-f]*$/.test(maskHex) || maskHex.length !== expectedBytes * 2) {
|
||||
throw new Error("invalid achievement spell mask");
|
||||
}
|
||||
const maskBytes = [];
|
||||
for (let offset = 0; offset < maskHex.length; offset += 2) maskBytes.push(parseInt(maskHex.slice(offset, offset + 2), 16));
|
||||
const unlockedIndexes = [];
|
||||
for (let index = 0; index < DEFINITIONS.length; index += 1) {
|
||||
if (maskBytes[Math.floor(index / 8)] & (1 << (index % 8))) unlockedIndexes.push(index);
|
||||
}
|
||||
return {
|
||||
unlockedIndexes,
|
||||
baseTimestamp: Math.max(0, Math.floor(Number(payload[2] || 0) || 0)),
|
||||
deltas: Array.isArray(payload[3]) ? payload[3] : [],
|
||||
packed: Array.isArray(payload[4]) ? payload[4] : [],
|
||||
timestampUnitMs: 1000,
|
||||
progressVersion: LEGACY_SPELL_STATE_VERSION,
|
||||
};
|
||||
}
|
||||
if (version !== PREVIOUS_SPELL_STATE_VERSION && version !== SPELL_STATE_VERSION) return null;
|
||||
if (!Array.isArray(payload) || Number(payload[0]) !== SPELL_STATE_VERSION) return null;
|
||||
const lowWord = Number(payload[1]);
|
||||
const highWord = Number(payload[2]);
|
||||
const highMax = (2 ** Math.max(0, DEFINITIONS.length - 32)) - 1;
|
||||
|
|
@ -897,7 +914,7 @@
|
|||
deltas: Array.isArray(payload[4]) ? payload[4] : [],
|
||||
packed: Array.isArray(payload[5]) ? payload[5] : [],
|
||||
timestampUnitMs: 60000,
|
||||
progressVersion: version,
|
||||
progressVersion: SPELL_STATE_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -958,6 +975,14 @@
|
|||
progress.fightMochiFightCount = Math.max(progress.fightMochiFightCount, Math.min(CHAOS_FIGHT_TARGET, Math.max(0, Math.floor(Number(packed[9] || 0) || 0))));
|
||||
progress.shotCount = Math.max(progress.shotCount, Math.min(SNIPER_SHOT_TARGET, Math.max(0, Math.floor(Number(packed[10] || 0) || 0))));
|
||||
progress.loveMochiBirthCount = Math.max(progress.loveMochiBirthCount, Math.min(FERTILITY_BIRTH_TARGET, Math.max(0, Math.floor(Number(packed[11] || 0) || 0))));
|
||||
const importedDailyStreak = Math.min(DAILY_PLAY_TARGET, Math.max(0, Math.floor(Number(packed[12] || 0) || 0)));
|
||||
const importedDailyLastDay = Math.min(131071, Math.max(0, Math.floor(Number(packed[13] || 0) || 0)));
|
||||
if (importedDailyLastDay > progress.dailyPlayLastDay) {
|
||||
progress.dailyPlayLastDay = importedDailyLastDay;
|
||||
progress.dailyPlayStreak = importedDailyStreak;
|
||||
} else if (importedDailyLastDay === progress.dailyPlayLastDay) {
|
||||
progress.dailyPlayStreak = Math.max(progress.dailyPlayStreak, importedDailyStreak);
|
||||
}
|
||||
}
|
||||
const progressChanged = previousProgress !== JSON.stringify(progress);
|
||||
changed = changed || progressChanged;
|
||||
|
|
@ -990,11 +1015,12 @@
|
|||
}
|
||||
let changed = evaluateCleanFreak(world, detail);
|
||||
changed = evaluateMegalopolis(world, detail) || changed;
|
||||
changed = evaluateSandbox(world, detail) || changed;
|
||||
if (!state.unlocked.placed_objects_100) {
|
||||
const progress = ensureProgress();
|
||||
progress.placementCount = Math.min(100, progress.placementCount + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (progress.placementCount >= 100) changed = unlock("placed_objects_100", { ...detail, world, count: progress.placementCount }) || changed;
|
||||
}
|
||||
return changed;
|
||||
|
|
@ -1015,7 +1041,7 @@
|
|||
if (!state.unlocked.unplanned_city_30) {
|
||||
progress.quickDeleteCount = Math.min(30, progress.quickDeleteCount + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (progress.quickDeleteCount >= 30) changed = unlock("unplanned_city_30", { ...detail, world, count: progress.quickDeleteCount }) || changed;
|
||||
}
|
||||
return changed;
|
||||
|
|
@ -1029,7 +1055,7 @@
|
|||
const achievementId = action === "undo" ? "undo_20" : "redo_20";
|
||||
progress[key] = Math.min(20, progress[key] + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
let changed = false;
|
||||
if (action === "undo" && Math.max(0, Number(detail.deadRestored || 0) || 0) >= 10) {
|
||||
changed = unlock("undo_mass_revival", { ...detail, count: Math.max(0, Number(detail.deadRestored || 0) || 0) }) || changed;
|
||||
|
|
@ -1054,7 +1080,7 @@
|
|||
const progress = ensureProgress();
|
||||
progress.linkTypes = [...new Set([...(progress.linkTypes || []), linkType])];
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
return evaluateMechanizedIndustry({ ...detail, type: linkType });
|
||||
}
|
||||
|
||||
|
|
@ -1064,7 +1090,7 @@
|
|||
if (!progress.signalActivated) {
|
||||
progress.signalActivated = true;
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
}
|
||||
return evaluateMechanizedIndustry(detail);
|
||||
}
|
||||
|
|
@ -1076,24 +1102,67 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function evaluateAntNestWithoutTarinai(worldRef = global.world, detail = {}) {
|
||||
if (!worldRef || state.unlocked.ant_nest_without_tarinai) return false;
|
||||
const hasNest = (worldRef.items || []).some(item => item && !item.dead && item.type === "ant_nest");
|
||||
if (!hasNest) return false;
|
||||
const aliveTarinai = (worldRef.tarinai || []).some(tarinai => tarinai && !tarinai.dead);
|
||||
if (aliveTarinai) return false;
|
||||
return unlock("ant_nest_without_tarinai", { ...detail, world: worldRef });
|
||||
}
|
||||
|
||||
function recordStickyBombPass(item = null, detail = {}) {
|
||||
if (!item || item.dead || item.type !== "sticky_bomb" || state.unlocked.sticky_bomb_15_passes) return false;
|
||||
item.stickyBombPassCount = Math.max(0, Math.floor(Number(item.stickyBombPassCount || 0) || 0)) + 1;
|
||||
if (item.stickyBombPassCount < 15) return false;
|
||||
return unlock("sticky_bomb_15_passes", { ...detail, item, count: item.stickyBombPassCount });
|
||||
}
|
||||
|
||||
function recordCircuitBoardSignalTransfer(sourceBoard = null, targetBoard = null, wire = null, detail = {}) {
|
||||
if (state.unlocked.information_industry) return false;
|
||||
if (!sourceBoard || !targetBoard || sourceBoard === targetBoard) return false;
|
||||
if (sourceBoard.type !== "circuit_board" || targetBoard.type !== "circuit_board") return false;
|
||||
if (!wire || !["wire", "insulated_wire"].includes(wire.type) || !wire.signalActive) return false;
|
||||
return unlock("information_industry", { ...detail, sourceBoard, targetBoard, wire });
|
||||
}
|
||||
|
||||
function recordWirePowerState(wire = null, active = false) {
|
||||
if (!wire || (typeof wire !== "object" && typeof wire !== "function")) return false;
|
||||
if (!active) return false;
|
||||
if (!wireShockTrackers.has(wire)) wireShockTrackers.set(wire, { targets: new WeakSet(), count: 0 });
|
||||
return true;
|
||||
}
|
||||
|
||||
function recordWireShock(wire = null, target = null, detail = {}) {
|
||||
if (state.unlocked.wire_shock_7_tarinai || !wire || !target || wire.type !== "wire" || !wire.signalActive) return false;
|
||||
const world = detail.world || wire.world || target.world || global.world || null;
|
||||
if (!world || !(world.tarinai || []).includes(target) || target.dead) return false;
|
||||
recordWirePowerState(wire, true);
|
||||
const tracker = wireShockTrackers.get(wire);
|
||||
if (!tracker || tracker.targets.has(target)) return false;
|
||||
tracker.targets.add(target);
|
||||
tracker.count += 1;
|
||||
if (tracker.count < WIRE_SHOCK_TARGET) return false;
|
||||
return unlock("wire_shock_7_tarinai", { ...detail, world, wire, target, count: tracker.count });
|
||||
}
|
||||
|
||||
function recordAntKilled(detail = {}) {
|
||||
let changed = false;
|
||||
const progress = ensureProgress();
|
||||
if (!state.unlocked.ants_killed_100) {
|
||||
progress.antKills = Math.min(100, progress.antKills + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (progress.antKills >= 100) changed = unlock("ants_killed_100", detail) || changed;
|
||||
}
|
||||
const world = detailWorld(detail);
|
||||
const ant = detail.ant || null;
|
||||
const now = Math.max(0, Number(world?.time || 0) || 0);
|
||||
const aliveCount = (world?.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
|
||||
const lastDamageAt = Number(world?.achievementLastTarinaiDamageAt);
|
||||
const undamagedForTenSeconds = !Number.isFinite(lastDamageAt) || now - lastDamageAt >= 10;
|
||||
if (!state.unlocked.enemy_enemy_friend && world && ant?._achievementLastDamageSource?.danger && aliveCount >= 30 && undamagedForTenSeconds) {
|
||||
if (world && aliveCount < 30) world.achievementEnemyAntKills = 0;
|
||||
if (!state.unlocked.enemy_enemy_friend && world && ant?._achievementLastDamageSource?.danger && aliveCount >= 30) {
|
||||
world.achievementEnemyAntKills = Math.min(10, Math.max(0, Math.floor(Number(world.achievementEnemyAntKills || 0) || 0)) + 1);
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (world.achievementEnemyAntKills >= 10) changed = unlock("enemy_enemy_friend", { ...detail, world, count: world.achievementEnemyAntKills }) || changed;
|
||||
}
|
||||
return changed;
|
||||
|
|
@ -1118,7 +1187,10 @@
|
|||
const dangerSource = dangerItemSource(source);
|
||||
// This achievement tracks collateral damage caused by danger-category tools,
|
||||
// not ordinary combat, hunger, disease, or ant attacks.
|
||||
if (world && dangerSource) world.achievementLastTarinaiDamageAt = now;
|
||||
if (world && dangerSource) {
|
||||
world.achievementLastTarinaiDamageAt = now;
|
||||
world.achievementEnemyAntKills = 0;
|
||||
}
|
||||
tarinai._achievementLastDamageSource = dangerSource ? {
|
||||
danger: true,
|
||||
at: now,
|
||||
|
|
@ -1204,36 +1276,100 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function recordPauseClick(detail = {}) {
|
||||
function recordEmptyClick(detail = {}) {
|
||||
if (state.unlocked.pause_spam_4_in_1_second) return false;
|
||||
const now = Number(detail.now);
|
||||
const timestamp = Number.isFinite(now) ? now : Date.now();
|
||||
pauseClickTimes = pauseClickTimes.filter(value => timestamp - value <= 1000 && timestamp >= value);
|
||||
pauseClickTimes.push(timestamp);
|
||||
if (pauseClickTimes.length >= 4) {
|
||||
pauseClickTimes = [];
|
||||
emptyClickTimes = emptyClickTimes.filter(value => timestamp - value <= 1000 && timestamp >= value);
|
||||
emptyClickTimes.push(timestamp);
|
||||
if (emptyClickTimes.length >= 4) {
|
||||
emptyClickTimes = [];
|
||||
return unlock("pause_spam_4_in_1_second", { ...detail, count: 4, windowMs: 1000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function recordGroundChange(detail = {}) {
|
||||
const world = detailWorld(detail);
|
||||
const previous = String(detail.previous || "");
|
||||
const next = String(detail.next || world?.groundType || "");
|
||||
if (!world || detail.silent || !previous || !next || previous === next) return false;
|
||||
let changed = false;
|
||||
if (!state.unlocked.park_ground_changed && String(world.fieldType || "") === "park") {
|
||||
changed = unlock("park_ground_changed", { ...detail, world, previous, next }) || changed;
|
||||
}
|
||||
if (!state.unlocked.ground_change_4_in_1_second) {
|
||||
const now = Number(detail.now);
|
||||
const timestamp = Number.isFinite(now) ? now : Date.now();
|
||||
groundChangeTimes = groundChangeTimes.filter(value => timestamp - value <= 1000 && timestamp >= value);
|
||||
groundChangeTimes.push(timestamp);
|
||||
if (groundChangeTimes.length >= 4) {
|
||||
groundChangeTimes = [];
|
||||
changed = unlock("ground_change_4_in_1_second", { ...detail, world, count: 4, windowMs: 1000 }) || changed;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function evaluateSandbox(world = global.world, detail = {}) {
|
||||
if (!world || state.unlocked.sandbox_five_toilets) return false;
|
||||
const count = (world.items || []).reduce((sum, item) => sum + (item && !item.dead && item.type === "toilet" ? 1 : 0), 0);
|
||||
if (count < 5) return false;
|
||||
return unlock("sandbox_five_toilets", { ...detail, world, count });
|
||||
}
|
||||
|
||||
function recordLowFps(detail = {}) {
|
||||
if (state.unlocked.low_fps_single_digit) return false;
|
||||
const fps = Number.isFinite(Number(detail.fps)) ? Number(detail.fps) : Number(global.__tarinaiFps);
|
||||
if (!Number.isFinite(fps) || fps <= 0 || fps >= 10) return false;
|
||||
return unlock("low_fps_single_digit", { ...detail, fps });
|
||||
}
|
||||
|
||||
function localDayNumber(value = Date.now()) {
|
||||
const date = new Date(Number(value));
|
||||
if (!Number.isFinite(date.getTime())) return 0;
|
||||
return Math.floor(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY_MS);
|
||||
}
|
||||
|
||||
function recordDailyPlay(detail = {}) {
|
||||
if (state.unlocked.daily_play_7_days) return false;
|
||||
const today = localDayNumber(Number.isFinite(Number(detail.now)) ? Number(detail.now) : Date.now());
|
||||
if (!today) return false;
|
||||
const progress = ensureProgress();
|
||||
const previousDay = progress.dailyPlayLastDay;
|
||||
if (today < previousDay || today === previousDay) return false;
|
||||
progress.dailyPlayStreak = today === previousDay + 1
|
||||
? Math.min(DAILY_PLAY_TARGET, progress.dailyPlayStreak + 1)
|
||||
: 1;
|
||||
progress.dailyPlayLastDay = today;
|
||||
saveState();
|
||||
renderIfDialogOpen();
|
||||
if (progress.dailyPlayStreak >= DAILY_PLAY_TARGET) {
|
||||
return unlock("daily_play_7_days", { ...detail, day: today, streak: progress.dailyPlayStreak });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function recordContinuousPlay(detail = {}) {
|
||||
if (state.unlocked.continuous_play_1_hour) return false;
|
||||
if (state.unlocked.continuous_play_1_hour && state.unlocked.continuous_play_24_hours) return false;
|
||||
const explicitElapsed = Number(detail.elapsedMs);
|
||||
let elapsedMs;
|
||||
if (Number.isFinite(explicitElapsed)) {
|
||||
elapsedMs = Math.max(0, explicitElapsed);
|
||||
} else {
|
||||
const timestamp = Number.isFinite(Number(detail.now)) ? Number(detail.now) : Date.now();
|
||||
if (document.hidden) return false;
|
||||
if (!continuousPlayStartedAt || !continuousPlayLastHeartbeatAt || timestamp - continuousPlayLastHeartbeatAt > 15000 || timestamp < continuousPlayLastHeartbeatAt) {
|
||||
continuousPlayStartedAt = timestamp;
|
||||
}
|
||||
if (!continuousPlayStartedAt || timestamp < continuousPlayStartedAt) continuousPlayStartedAt = timestamp;
|
||||
continuousPlayLastHeartbeatAt = timestamp;
|
||||
elapsedMs = Math.max(0, timestamp - continuousPlayStartedAt);
|
||||
}
|
||||
if (elapsedMs >= CONTINUOUS_PLAY_TARGET_MS) return unlock("continuous_play_1_hour", { ...detail, elapsedMs });
|
||||
return false;
|
||||
let changed = false;
|
||||
if (!state.unlocked.continuous_play_1_hour && elapsedMs >= CONTINUOUS_PLAY_TARGET_MS) {
|
||||
changed = unlock("continuous_play_1_hour", { ...detail, elapsedMs }) || changed;
|
||||
}
|
||||
if (!state.unlocked.continuous_play_24_hours && elapsedMs >= CONTINUOUS_PLAY_24H_TARGET_MS) {
|
||||
changed = unlock("continuous_play_24_hours", { ...detail, elapsedMs }) || changed;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function recordBelowAbsoluteZeroItem(worldRef = global.world, detail = {}) {
|
||||
|
|
@ -1253,7 +1389,7 @@
|
|||
const type = String(detail.type || detail.target?.type || "");
|
||||
if (!world || !["zunchi", "ant_corpse", "trace", "splat"].includes(type)) return false;
|
||||
world.achievementRobotCleanCount = Math.min(ROBOT_CLEAN_TARGET, Math.max(0, Math.floor(Number(world.achievementRobotCleanCount || 0) || 0)) + 1);
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (world.achievementRobotCleanCount >= ROBOT_CLEAN_TARGET) return unlock("robot_cleaner_100", { ...detail, world, count: world.achievementRobotCleanCount });
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1304,7 +1440,7 @@
|
|||
const progress = ensureProgress();
|
||||
progress.firstAidHeals = Math.min(50, progress.firstAidHeals + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (progress.firstAidHeals >= 50) return unlock("town_doctor_50", { ...detail, count: progress.firstAidHeals, recovered });
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1330,7 +1466,7 @@
|
|||
if (!progress.medicineTypes.includes(type)) {
|
||||
progress.medicineTypes = [...progress.medicineTypes, type];
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
}
|
||||
if (MEDICINE_LEDGER_TYPES.every(required => progress.medicineTypes.includes(required))) {
|
||||
changed = unlock("medicine_ledger_all", { ...detail, count: progress.medicineTypes.length }) || changed;
|
||||
|
|
@ -1374,7 +1510,7 @@
|
|||
changed = unlock("chaos_seeker_666_fights", { ...detail, world, count: progress.fightMochiFightCount, fightMochiInfluenced: true }) || changed;
|
||||
} else {
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
}
|
||||
}
|
||||
const now = Math.max(0, Number(world.time || 0) || 0);
|
||||
|
|
@ -1398,7 +1534,7 @@
|
|||
progress.shotCount = Math.min(SNIPER_SHOT_TARGET, progress.shotCount + 1);
|
||||
if (progress.shotCount >= SNIPER_SHOT_TARGET) return unlock("sniper_333_shots", { ...detail, count: progress.shotCount });
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1410,7 +1546,7 @@
|
|||
return unlock("fertility_seeker_721_love_births", { ...detail, count: progress.loveMochiBirthCount });
|
||||
}
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1486,7 +1622,7 @@
|
|||
if (!state.unlocked.great_mother_1000_births) {
|
||||
progress.birthCount = Math.min(3333, progress.birthCount + 1);
|
||||
saveState();
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (progress.birthCount >= 3333) unlock("great_mother_1000_births", { ...detail, count: progress.birthCount });
|
||||
}
|
||||
if (state.unlocked.birth_50_in_60_seconds) return;
|
||||
|
|
@ -1500,11 +1636,15 @@
|
|||
|
||||
function recordDeath(detail = {}) {
|
||||
recordFightPairDangerDeath(detail);
|
||||
const reason = String(detail.reason || detail.tarinai?.deathReason || "");
|
||||
const lifespanDeath = /\u5bff\u547d|\u5929\u5bff/.test(reason);
|
||||
const world = detail.world || detail.tarinai?.world || null;
|
||||
if (world) {
|
||||
evaluateMegalopolis(world, detail);
|
||||
const aliveAfterDeath = (world.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
|
||||
world.achievementNoDeathStartAt = aliveAfterDeath >= 25 ? Math.max(0, Number(world.time || 0) || 0) : -1;
|
||||
if (!lifespanDeath) {
|
||||
const aliveAfterDeath = (world.tarinai || []).reduce((count, candidate) => count + (candidate && !candidate.dead ? 1 : 0), 0);
|
||||
world.achievementNoDeathStartAt = aliveAfterDeath >= 25 ? Math.max(0, Number(world.time || 0) || 0) : -1;
|
||||
}
|
||||
}
|
||||
const now = Number(world?.time || 0) || 0;
|
||||
const tracker = runtimeTracker(world, now);
|
||||
|
|
@ -1513,7 +1653,6 @@
|
|||
tracker.deathTimes = tracker.deathTimes.filter(time => time >= now - 10.0001);
|
||||
if (tracker.deathTimes.length >= 50) unlock("death_50_in_10_seconds", detail);
|
||||
}
|
||||
const reason = String(detail.reason || detail.tarinai?.deathReason || "");
|
||||
const tarinai = detail.tarinai || null;
|
||||
if (reason.includes("\u5929\u5bff\u3092\u5168\u3046\u3057\u305f")) {
|
||||
unlock("lifespan_completed", detail);
|
||||
|
|
@ -1555,7 +1694,7 @@
|
|||
recordDirectCare({ ...detail, world, foodGift: true });
|
||||
if (state.unlocked.direct_feed_33) return false;
|
||||
world.achievementDirectFeedCount = Math.min(33, Math.max(0, Math.floor(Number(world.achievementDirectFeedCount || 0) || 0)) + 1);
|
||||
if (!dom.dialog?.classList.contains("hidden")) render();
|
||||
renderIfDialogOpen();
|
||||
if (world.achievementDirectFeedCount >= 33) return unlock("direct_feed_33", { ...detail, world, count: world.achievementDirectFeedCount });
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1577,7 +1716,8 @@
|
|||
|| !state.unlocked.rain_shelter_all
|
||||
|| !state.unlocked.sauna_cold_plunge
|
||||
|| !state.unlocked.idle_observer_5_minutes
|
||||
|| !state.unlocked.safe_colony_25_5_minutes;
|
||||
|| !state.unlocked.safe_colony_25_5_minutes
|
||||
|| !state.unlocked.ant_nest_without_tarinai;
|
||||
const needsAntScan = !state.unlocked.ants_alive_25;
|
||||
const needsTemperatureItemScan = !state.unlocked.below_absolute_zero_item;
|
||||
const needsCleanFreakScan = !state.unlocked.clean_freak_robot_only;
|
||||
|
|
@ -1666,7 +1806,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (!state.unlocked.sandbox_five_toilets) evaluateSandbox(worldRef, { source: "world-evaluation" });
|
||||
if (!state.unlocked.ants_alive_25) recordAntPopulation(worldRef);
|
||||
if (!state.unlocked.ant_nest_without_tarinai) evaluateAntNestWithoutTarinai(worldRef, { source: "world-evaluation" });
|
||||
if (!state.unlocked.below_absolute_zero_item) recordBelowAbsoluteZeroItem(worldRef);
|
||||
if (!state.unlocked.clean_freak_robot_only) evaluateCleanFreak(worldRef, { source: "world-evaluation" });
|
||||
if (!state.unlocked.megalopolis && alive.length >= 100) evaluateMegalopolis(worldRef, { source: "world-evaluation" });
|
||||
|
|
@ -1686,16 +1828,19 @@
|
|||
state.resetPending = true;
|
||||
runtimeByWorld = new WeakMap();
|
||||
dangerFightDeathsByWorld = new WeakMap();
|
||||
wireShockTrackers = new WeakMap();
|
||||
toastQueue = [];
|
||||
toastActive = false;
|
||||
pauseClickTimes = [];
|
||||
continuousPlayStartedAt = document.hidden ? 0 : Date.now();
|
||||
emptyClickTimes = [];
|
||||
groundChangeTimes = [];
|
||||
continuousPlayStartedAt = Date.now();
|
||||
continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
|
||||
if (global.world) {
|
||||
resetWorldProgress(global.world);
|
||||
global.world.achievementLastInterventionAt = Math.max(0, Number(global.world.time || 0) || 0);
|
||||
}
|
||||
saveState();
|
||||
recordDailyPlay({ source: "reset" });
|
||||
render();
|
||||
clearTimeout(showUnlockToast.absorbTimer);
|
||||
clearTimeout(showUnlockToast.hideTimer);
|
||||
|
|
@ -1736,14 +1881,14 @@
|
|||
dom.button?.focus?.();
|
||||
}
|
||||
|
||||
dom.pauseButton?.addEventListener("click", () => recordPauseClick());
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
continuousPlayStartedAt = document.hidden ? 0 : Date.now();
|
||||
continuousPlayLastHeartbeatAt = continuousPlayStartedAt;
|
||||
if (!document.hidden) continuousPlayLastHeartbeatAt = Date.now();
|
||||
});
|
||||
if (typeof global.setInterval === "function") {
|
||||
global.setInterval(() => {
|
||||
if (!document.hidden) recordContinuousPlay();
|
||||
recordDailyPlay();
|
||||
recordLowFps();
|
||||
recordHeldTarinai(global.world, Date.now());
|
||||
}, 1000);
|
||||
if (!DEBUG_MODE) global.setInterval(() => void syncPending({ full: true }), FULL_SYNC_INTERVAL_MS);
|
||||
|
|
@ -1801,10 +1946,19 @@
|
|||
recordLinkPlaced,
|
||||
recordSignalActivation,
|
||||
recordAntPopulation,
|
||||
evaluateAntNestWithoutTarinai,
|
||||
recordAntKilled,
|
||||
recordStickyBombPass,
|
||||
recordCircuitBoardSignalTransfer,
|
||||
recordWirePowerState,
|
||||
recordWireShock,
|
||||
recordSaveSlotsFilled,
|
||||
recordPauseClick,
|
||||
recordEmptyClick,
|
||||
recordGroundChange,
|
||||
evaluateSandbox,
|
||||
recordLowFps,
|
||||
recordContinuousPlay,
|
||||
recordDailyPlay,
|
||||
recordBelowAbsoluteZeroItem,
|
||||
recordDamageSource,
|
||||
recordAntDamageSource,
|
||||
|
|
@ -1816,6 +1970,7 @@
|
|||
recordSoccerBallDeath(detail = {}) { return unlock("soccer_ball_death", detail); },
|
||||
});
|
||||
global.TarinaiAchievements = api;
|
||||
recordDailyPlay({ source: "load" });
|
||||
if (!state.unlocked[COMPLETIONIST_ID] && allOtherAchievementsUnlocked()) unlock(COMPLETIONIST_ID, { source: "load" });
|
||||
render();
|
||||
global.setTimeout(() => recordSaveSlotsFilled(), 0);
|
||||
|
|
|
|||
1117
js/circuit_board_system.js
Normal file
1117
js/circuit_board_system.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -59,6 +59,12 @@
|
|||
const solid = worldRef?.solidObstacleRects?.(item) || [];
|
||||
if (solid.length) return solid;
|
||||
if (global.TarinaiMechanicalSystem.isMechanicalType(item?.type)) return global.TarinaiMechanicalSystem.placementRects(item) || [];
|
||||
if (item?.type === "circuit_board") {
|
||||
const circuit = global.TarinaiCircuitBoardSystem;
|
||||
const halfW = Number(circuit?.boardHalfWidth?.(item) || 32) || 32;
|
||||
const halfH = Number(circuit?.boardHalfHeight?.(item) || 32) || 32;
|
||||
return [{ oriented: true, cx: num(item.x), cy: num(item.y), halfW, halfH, angle: num(item.angle), type: item.type, item }];
|
||||
}
|
||||
return solid;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,10 +35,14 @@
|
|||
const nearestAvg = nearestAverage(alive);
|
||||
const fieldId = String(worldRef?.fieldType || "garden");
|
||||
const overcrowdLimit = fieldId === "cage" ? 28 : (fieldId === "park" ? 140 : 75);
|
||||
const previousPopulation = Number(worldRef?.lastColonyMoodPopulation);
|
||||
const populationIncreasing = Number.isFinite(previousPopulation) && n > previousPopulation;
|
||||
return {
|
||||
now,
|
||||
alive,
|
||||
n,
|
||||
previousPopulation,
|
||||
populationIncreasing,
|
||||
avgEnergy: avgOf(alive, t => t.energy || 0),
|
||||
avgStress: avgOf(alive, t => t.stress || 0),
|
||||
avgHunger: avgOf(alive, t => t.hunger || 0),
|
||||
|
|
@ -70,7 +74,7 @@
|
|||
add("confusion", 100, (m.recentShock && (m.panicRatio >= 0.10 || m.safetyRatio >= 0.18)) || m.panicRatio >= 0.25);
|
||||
add("fearful", 90, (m.avgSafety >= 50 && m.safetyRatio >= 0.28) || (m.recentShock && m.safetyRatio >= 0.18));
|
||||
add("disease_spread", 82, m.diseaseRatio >= 0.25 || (m.diseasedCount >= 3 && m.diseaseRatio >= 0.16));
|
||||
add("weakened", 74, (m.avgEnergy < 54 && m.weakRatio >= 0.18) || m.avgHunger > 82 || m.weakRatio >= 0.34);
|
||||
add("weakened", 74, !m.populationIncreasing && ((m.avgEnergy < 54 && m.weakRatio >= 0.18) || m.avgHunger > 82 || m.weakRatio >= 0.34));
|
||||
add("breeding", 66, m.breedingRatio >= 0.10 || m.breedingCount >= 2 || m.now - (worldRef?.lastBirthAt || -999) <= 45);
|
||||
add("happy", 46, m.avgStress < 28 && m.panicRatio < 0.08 && m.diseaseRatio < 0.12 && m.weakRatio < 0.18);
|
||||
add("overcrowded", 34, m.n >= m.overcrowdLimit || m.denseCrowd);
|
||||
|
|
@ -87,7 +91,9 @@
|
|||
const id = choose(worldRef, metrics);
|
||||
const next = worldRef.colonyMoodDefinition?.(id) || { id, label: id, effects: { personality: {} } };
|
||||
worldRef.colonyMood = next;
|
||||
worldRef.nextColonyMoodEvalAt = now + 1.25;
|
||||
worldRef.lastColonyMoodPopulation = metrics.n;
|
||||
const dayLength = Math.max(1, Number(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength ?? 120) || 120);
|
||||
worldRef.nextColonyMoodEvalAt = now + dayLength / 12;
|
||||
global.TarinaiAchievements?.evaluateWorld?.(worldRef, next);
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
// and visual effects. Previously this bootstrap lived inline in several
|
||||
// modules; keeping it here prevents helper drift and load-order surprises.
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
|
|
@ -28,10 +27,48 @@
|
|||
};
|
||||
const deterministicFrame = (worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6);
|
||||
const deterministicUnit = (worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|"));
|
||||
const finiteOr = (value, fallback = 0) => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
};
|
||||
const clampNumber = (value, min, max) => Math.max(min, Math.min(max, finiteOr(value, min)));
|
||||
const isPlainObject = value => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
const clonePlain = (value, fallback = {}) => {
|
||||
if (value == null) return fallback;
|
||||
try {
|
||||
if (typeof structuredClone === "function") return structuredClone(value);
|
||||
} catch (_) {}
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
};
|
||||
const birthHash32 = (seed = "") => {
|
||||
const match = /^b[0-9a-z]+_([0-9a-z]+)$/i.exec(String(seed || ""));
|
||||
if (match) {
|
||||
const parsed = parseInt(match[1], 36);
|
||||
if (Number.isFinite(parsed)) return parsed >>> 0;
|
||||
}
|
||||
let h = 2166136261;
|
||||
const value = String(seed || "");
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
h ^= value.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return h >>> 0;
|
||||
};
|
||||
const isEffectCauseText = (text = "") => /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
global.TarinaiCoreHelpers = Object.freeze({
|
||||
...(global.TarinaiCoreHelpers || {}),
|
||||
finiteOr,
|
||||
clampNumber,
|
||||
isPlainObject,
|
||||
clonePlain,
|
||||
birthHash32,
|
||||
isEffectCauseText,
|
||||
});
|
||||
global.isTarinaiEffectCauseText = global.isTarinaiEffectCauseText || isEffectCauseText;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
// Keep the historical bare high-level helper names available to legacy scripts while the
|
||||
|
|
|
|||
|
|
@ -4,14 +4,11 @@
|
|||
// Shared primitive geometry helpers used by placement, collision, mechanical bodies,
|
||||
// editor hit-testing, and world obstacle queries.
|
||||
(function (global) {
|
||||
function num(v, fallback = 0) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function clampValue(v, min, max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
});
|
||||
const clampValue = global.TarinaiCoreHelpers?.clampNumber || ((v, min, max) => Math.max(min, Math.min(max, v)));
|
||||
|
||||
function orientedRectAabb(cx, cy, halfW, halfH, angle) {
|
||||
const c = Math.cos(num(angle));
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
if (old && old !== carrier) {
|
||||
old.surpriseTimer = Math.max(old.surpriseTimer || 0, 0.18);
|
||||
old.thought = "\u7c98\u7740\u30dc\u30e0\u304c\u79fb\u3063\u305f";
|
||||
if (reason === "transfer") global.TarinaiAchievements?.recordStickyBombPass?.(item, { world: worldRef, from: old, to: carrier });
|
||||
}
|
||||
if (worldRef?.effects && typeof Effect !== "undefined") {
|
||||
worldRef.effects.push(new Effect("ring", item.x, item.y, { size: Math.max(16, (item.r || 16) * 1.45), life: 0.18, color: reason === "transfer" ? "rgba(255,210,72,0.52)" : "rgba(255,128,78,0.48)" }));
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
|
|||
function duplicatorApplyStoredPin(duplicator, worldRef) {
|
||||
const storedType = String(duplicator?.storedFoodType || "");
|
||||
if (!duplicator || duplicator.type !== "duplicator" || !isPinType(storedType)) return false;
|
||||
if (globalThis.TarinaiSignalSystem?.powerOverride?.(duplicator) === false) return false;
|
||||
const now = Number(worldRef?.time || 0);
|
||||
if (now < Number(duplicator.nextDuplicatedPinAt || 0)) return false;
|
||||
const dish = duplicatorLoadPoint(duplicator);
|
||||
|
|
|
|||
|
|
@ -549,6 +549,7 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "duplicator") {
|
||||
ctx.save();
|
||||
const loaded = !!this.storedFoodType;
|
||||
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
||||
const bodyGrad = ctx.createLinearGradient(-this.r * 1.1, -this.r * 1.0, this.r * 1.1, this.r * 0.9);
|
||||
bodyGrad.addColorStop(0, styleNight ? "#3d4852" : "#d8e2e6");
|
||||
bodyGrad.addColorStop(0.45, styleNight ? "#65717b" : "#f2f6f7");
|
||||
|
|
@ -566,7 +567,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.strokeStyle = styleNight ? "rgba(160,190,210,0.25)" : "rgba(82,98,108,0.32)";
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = loaded ? "rgba(83,221,116,0.88)" : "rgba(139,160,170,0.62)";
|
||||
ctx.fillStyle = !powered ? "rgba(180,76,70,0.72)" : (loaded ? "rgba(83,221,116,0.88)" : "rgba(139,160,170,0.62)");
|
||||
ctx.beginPath();
|
||||
ctx.arc(-this.r * 0.70, -this.r * 0.43, this.r * 0.13, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
|
@ -624,6 +625,76 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.ellipse(-this.r * 0.48, -this.r * 0.72, this.r * 0.34, this.r * 0.08, -0.14, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.type === "seesaw") {
|
||||
const r = this.r || 60;
|
||||
const tilt = Number(this.seesawTilt || 0) || 0;
|
||||
ctx.save();
|
||||
ctx.fillStyle = styleNight ? "rgba(81,68,57,0.96)" : "rgba(130,99,69,0.98)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(41,34,29,0.86)" : "rgba(78,55,37,0.82)";
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 0.22, r * 0.42);
|
||||
ctx.lineTo(0, -r * 0.05);
|
||||
ctx.lineTo(r * 0.22, r * 0.42);
|
||||
ctx.closePath(); ctx.fill(); ctx.stroke();
|
||||
ctx.save();
|
||||
ctx.rotate(tilt);
|
||||
const grad = ctx.createLinearGradient(0, -r * 0.14, 0, r * 0.14);
|
||||
grad.addColorStop(0, styleNight ? "#c5895c" : "#f0ad6f");
|
||||
grad.addColorStop(1, styleNight ? "#8d5d3f" : "#ba7447");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.strokeStyle = styleNight ? "rgba(57,37,29,0.9)" : "rgba(104,59,35,0.85)";
|
||||
roundedRect(ctx, -r * 1.08, -r * 0.13, r * 2.16, r * 0.26, r * 0.08);
|
||||
ctx.fill(); ctx.stroke();
|
||||
for (const side of [-1, 1]) {
|
||||
ctx.fillStyle = side < 0 ? "rgba(88,177,231,0.98)" : "rgba(239,111,116,0.98)";
|
||||
roundedRect(ctx, side * r * 0.79 - r * 0.18, -r * 0.26, r * 0.36, r * 0.22, r * 0.07);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(70,55,45,0.75)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(side * r * 0.90, -r * 0.22);
|
||||
ctx.lineTo(side * r * 0.90, -r * 0.52);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.fillStyle = "rgba(70,58,48,0.9)";
|
||||
ctx.beginPath(); ctx.arc(0, 0, r * 0.13, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.type === "circuit_board") {
|
||||
const circuit = globalThis.TarinaiCircuitBoardSystem;
|
||||
const angle = Number(this.angle || 0) || 0;
|
||||
const active = Object.values(this._circuitOutputState || {}).some(Boolean);
|
||||
ctx.save();
|
||||
ctx.rotate(angle);
|
||||
ctx.fillStyle = active ? "rgba(44,139,82,0.98)" : (styleNight ? "rgba(41,91,62,0.98)" : "rgba(47,126,76,0.98)");
|
||||
ctx.strokeStyle = styleNight ? "rgba(19,48,31,0.92)" : "rgba(26,74,42,0.90)";
|
||||
ctx.lineWidth = 2.4;
|
||||
const boardHalfW = Number(circuit?.boardHalfWidth?.(this) ?? circuit?.HALF_W ?? 32) || 32;
|
||||
const boardHalfH = Number(circuit?.boardHalfHeight?.(this) ?? circuit?.HALF_H ?? 32) || 32;
|
||||
roundedRect(ctx, -boardHalfW, -boardHalfH, boardHalfW * 2, boardHalfH * 2, 10); ctx.fill(); ctx.stroke();
|
||||
ctx.strokeStyle = "rgba(216,190,81,0.64)";
|
||||
ctx.lineWidth = 1.4;
|
||||
const traceRows = Math.max(1, Math.min(7, Math.floor(boardHalfH / 9)));
|
||||
for (let i = -traceRows; i <= traceRows; i++) {
|
||||
const y = i * Math.max(5, boardHalfH / Math.max(2, traceRows * 1.7));
|
||||
ctx.beginPath(); ctx.moveTo(-boardHalfW * 0.72, y); ctx.lineTo(-boardHalfW * 0.28, y); ctx.lineTo(0, y * 0.45); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(boardHalfW * 0.72, y); ctx.lineTo(boardHalfW * 0.28, y); ctx.lineTo(0, y * 0.45); ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "rgba(30,39,34,0.94)";
|
||||
roundedRect(ctx, -15, -16, 30, 32, 5); ctx.fill();
|
||||
ctx.fillStyle = "rgba(236,245,235,0.92)";
|
||||
ctx.font = "bold 11px ui-rounded, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
||||
ctx.fillText("LOGIC", 0, 0);
|
||||
for (const port of circuit?.ports?.(this) || []) {
|
||||
const local = circuit.localPortPosition(port, this);
|
||||
const on = port.direction === "input" ? Boolean(this._circuitInputState?.[port.id]) : Boolean(this._circuitOutputState?.[port.id]);
|
||||
ctx.fillStyle = on ? "rgba(255,228,91,1)" : (port.direction === "input" ? "rgba(92,189,255,0.96)" : "rgba(255,143,77,0.96)");
|
||||
ctx.strokeStyle = "rgba(25,35,31,0.88)";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.beginPath(); ctx.arc(local.x, local.y, 5.4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "signboard") {
|
||||
ctx.save();
|
||||
const boardGrad = ctx.createLinearGradient(0, -this.r * 1.28, 0, this.r * 0.12);
|
||||
|
|
@ -685,8 +756,9 @@ draw(ctx, t, lighting = null) {
|
|||
} else if (this.type === "dry_ice") {
|
||||
const r = this.r || 21;
|
||||
const time = this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0);
|
||||
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
||||
ctx.save();
|
||||
if (itemShadowsEnabled()) {
|
||||
if (powered && itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(90, 178, 255, 0.30)";
|
||||
ctx.shadowBlur = 11;
|
||||
}
|
||||
|
|
@ -737,7 +809,7 @@ draw(ctx, t, lighting = null) {
|
|||
roundedRect(ctx, -bodyW * 0.48, y + bodyH - footH * 0.4, bodyW * 0.96, footH, r * 0.10);
|
||||
ctx.fill();
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (powered) for (let i = 0; i < 4; i++) {
|
||||
const phase = (time * (0.42 + i * 0.06) + i * 0.23 + (this.seed || 0)) % 1;
|
||||
const sx = -r * 0.50 + i * r * 0.33 + Math.sin(time * 1.4 + i) * r * 0.06;
|
||||
const sy = y - capH * 0.62 - phase * r * 0.76;
|
||||
|
|
@ -750,8 +822,9 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.restore();
|
||||
} else if (this.type === "stove") {
|
||||
const r = this.r || 24;
|
||||
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
||||
ctx.save();
|
||||
if (itemShadowsEnabled()) {
|
||||
if (powered && itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(255, 120, 43, 0.34)";
|
||||
ctx.shadowBlur = 9;
|
||||
}
|
||||
|
|
@ -764,17 +837,19 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.fillStyle = "rgba(42, 32, 28, 0.92)";
|
||||
roundedRect(ctx, -r * 0.48, -r * 0.34, r * 0.96, r * 0.62, r * 0.14);
|
||||
ctx.fill();
|
||||
const flame = 0.85 + 0.15 * Math.sin((this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0)) * 4.2 + (this.seed || 0));
|
||||
ctx.fillStyle = "rgba(255, 168, 57, 0.92)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * 0.16);
|
||||
ctx.bezierCurveTo(-r * 0.38, -r * 0.10, -r * 0.10, -r * 0.52 * flame, 0, -r * 0.66 * flame);
|
||||
ctx.bezierCurveTo(r * 0.34, -r * 0.20, r * 0.40, -r * 0.02, 0, r * 0.16);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(255, 235, 120, 0.82)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -r * 0.10, r * 0.16, r * 0.28 * flame, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (powered) {
|
||||
const flame = 0.85 + 0.15 * Math.sin((this.world?.time || (typeof performance !== "undefined" ? performance.now() * 0.001 : 0)) * 4.2 + (this.seed || 0));
|
||||
ctx.fillStyle = "rgba(255, 168, 57, 0.92)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * 0.16);
|
||||
ctx.bezierCurveTo(-r * 0.38, -r * 0.10, -r * 0.10, -r * 0.52 * flame, 0, -r * 0.66 * flame);
|
||||
ctx.bezierCurveTo(r * 0.34, -r * 0.20, r * 0.40, -r * 0.02, 0, r * 0.16);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(255, 235, 120, 0.82)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -r * 0.10, r * 0.16, r * 0.28 * flame, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "ant_nest") {
|
||||
const count = clamp(Math.round(this.antCount ?? ANT_NEST_START_COUNT ?? 6), 0, ANT_NEST_MAX_COUNT ?? 10);
|
||||
|
|
@ -873,9 +948,11 @@ draw(ctx, t, lighting = null) {
|
|||
const r = this.r || 26;
|
||||
const bodyAngle = Number.isFinite(Number(this.fanBodyAngle)) ? normalizedItemAngle(Number(this.fanBodyAngle), itemAngleFor(this)) : itemAngleFor(this);
|
||||
const windAngle = itemAngleFor(this);
|
||||
const spin = (this.fanSpin || 0) + (window.TarinaiSignalSystem?.powerOverride?.(this) === false ? 0 : t * 10.5);
|
||||
const powered = globalThis.TarinaiSignalSystem?.powerOverride?.(this) !== false;
|
||||
const spin = (this.fanSpin || 0) + (powered ? t * 10.5 : 0);
|
||||
ctx.save();
|
||||
ctx.rotate(windAngle);
|
||||
if (powered) {
|
||||
const windRange = 320;
|
||||
const windStart = r * 1.02;
|
||||
const windSpread = 0.62;
|
||||
|
|
@ -934,6 +1011,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = fanAlpha;
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.save();
|
||||
ctx.rotate(bodyAngle);
|
||||
|
|
@ -1235,7 +1313,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.beginPath(); ctx.roundRect?.(-27,-18,54,36,8); if (!ctx.roundRect) { ctx.rect(-27,-18,54,36); } ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = active ? "#fff6c9" : "#7b838d"; ctx.beginPath(); ctx.arc(0,-3,9,0,Math.PI*2); ctx.fill();
|
||||
const detectorMark = { tarinai:"\u305f", item:"\u7269", time:"\u6642", hunger_avg:"\u8179", stress_avg:"\u5727", sleep_avg:"\u7720", low_hp_count:"HP", sick_count:"\u75c5", temperature:"\u2103", zunchi_count:"\u7cde", ant_count:"\u87fb", water_count:"\u6c34" }[this.pressureTarget] || "\u305f";
|
||||
const detectorLabel = this.pressureTarget === "time" ? detectorMark : `${detectorMark}${Number(this.pressureThreshold ?? 1)}`;
|
||||
const detectorLabel = this.pressureTarget === "time" ? detectorMark : `${detectorMark}${Number(this.pressureMin ?? 1)}-${Number(this.pressureMax ?? 300)}`;
|
||||
ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(detectorLabel,0,14);
|
||||
ctx.restore();
|
||||
} else if (this.type === "rotator") {
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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" },
|
||||
pipe: { id: "pipe", itemType: "pipe", label: "\u571f\u7ba1", placeable: true, scalable: false, radius: 36, amount: 999, iconText: "\u25ce", collisionShape: "circle", tooltip: "\u79fb\u52d5\u3067\u304d\u308b\u5730\u4e0b\u7a7a\u9593\u3002\u305f\u308a\u306a\u3044\u304c\u7720\u308b\u3053\u3068\u3082\u3042\u308b\u3002" },
|
||||
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" },
|
||||
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\u96fb\u7dda\u63a5\u7d9a\u6642\u306f\u4fe1\u53f7ON\u306e\u9593\u3060\u3051\u4f5c\u52d5\u3059\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\u96fb\u7dda\u63a5\u7d9a\u6642\u306f\u4fe1\u53f7ON\u306e\u9593\u3060\u3051\u4f5c\u52d5\u3059\u308b\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
balloon: { id: "balloon", itemType: "balloon", label: "\u6c34\u98a8\u8239", placeable: true, scalable: false, radius: 20, amount: 999, iconText: "\ud83d\udca7", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u306e\u304a\u3082\u3061\u3083\u3002\u5272\u308c\u308b\u3068\u6c34\u6ef4\u304c\u98db\u3073\u6563\u308b\u3002" },
|
||||
fan: { id: "fan", itemType: "fan", label: "\u6247\u98a8\u6a5f", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "\ud83c\udf00", collisionShape: "circle", tooltip: "\u524d\u65b9\u306b\u98a8\u3092\u51fa\u3059\u3002\u8efd\u3044\u3082\u306e\u3092\u62bc\u3057\u3001\u5c11\u3057\u6dbc\u3057\u304f\u3059\u308b\u3002Q/E\u3067\u7f6e\u304f\u5411\u304d\u3092\u5909\u3048\u308b\u3002\u914d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u958b\u304f\u3002" },
|
||||
fan: { id: "fan", itemType: "fan", label: "\u6247\u98a8\u6a5f", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "\ud83c\udf00", collisionShape: "circle", tooltip: "\u524d\u65b9\u306b\u98a8\u3092\u51fa\u3059\u3002\u8efd\u3044\u3082\u306e\u3092\u62bc\u3057\u3001\u5c11\u3057\u6dbc\u3057\u304f\u3059\u308b\u3002Q/E\u3067\u7f6e\u304f\u5411\u304d\u3092\u5909\u3048\u308b\u3002\u914d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u958b\u304f\u3002\u96fb\u7dda\u63a5\u7d9a\u6642\u306f\u4fe1\u53f7ON\u306e\u9593\u3060\u3051\u4f5c\u52d5\u3059\u308b\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" },
|
||||
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\u96fb\u7dda\u63a5\u7d9a\u6642\u306f\u4fe1\u53f7ON\u306e\u9593\u3060\u3051\u4f9b\u7d66\u3059\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: false, scalable: false, radius: 18, amount: 140, iconText: "\uD83D\uDD25", collisionShape: "circle", simulationOnly: true },
|
||||
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: "\u5468\u56f2\u306b\u708e\u3092\u6492\u304d\u6563\u3089\u3059\u3002" },
|
||||
|
|
@ -69,6 +69,8 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 44, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||
pressure_switch: { id: "pressure_switch", itemType: "pressure_switch", label: "\u691c\u77e5\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u25a3", collisionShape: "circle", tooltip: "\u69d8\u3005\u306a\u72b6\u6cc1\u3092\u691c\u77e5\u3057\u3066\u96fb\u7dda\u306b\u4fe1\u53f7\u3092\u9001\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" },
|
||||
seesaw: { id: "seesaw", itemType: "seesaw", label: "\u30b7\u30fc\u30bd\u30fc", placeable: true, scalable: false, radius: 60, amount: 999, iconText: "\u2195", collisionShape: "circle", tooltip: "\u4e21\u7aef\u306b\u305f\u308a\u306a\u3044\u304c\u4e57\u308b\u3068\u4e8c\u5339\u3067\u904a\u3073\u3001\u4e92\u3044\u306b\u53cb\u597d\u3092\u7bc9\u304f\u3002" },
|
||||
circuit_board: { id: "circuit_board", itemType: "circuit_board", label: "\u57fa\u76e4", placeable: true, scalable: false, radius: 46, amount: 999, iconText: "\u25a6", collisionShape: "oriented_rect", tooltip: "\u96fb\u7dda\u306e\u4fe1\u53f7\u3067\u8ad6\u7406\u6f14\u7b97\u304c\u3067\u304d\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" },
|
||||
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
||||
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
|
||||
|
|
|
|||
|
|
@ -3,21 +3,23 @@
|
|||
// Static item/tool metadata shared by tool definitions and UI helpers.
|
||||
(function (global) {
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "circuit_board"]),
|
||||
food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "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", "balloon", "pushpin", "oshibyo", "zunchi"]),
|
||||
freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]),
|
||||
});
|
||||
|
||||
|
||||
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", "glass_wall", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator", "one_way_fence", "fence_h"]);
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES, "fan"]));
|
||||
const ELECTRIC_TOOL_IDS = Object.freeze(["pressure_switch", "wire", "insulated_wire", "circuit_board"]);
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", "glass_wall", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator", "one_way_fence", "fence_h", "circuit_board"]);
|
||||
const PHYSICS_CATEGORY_TOOL_IDS = Object.freeze(PHYSICS_TOOL_IDS.filter(id => !ELECTRIC_TOOL_IDS.includes(id)));
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES, "fan", "circuit_board"]));
|
||||
|
||||
const PIN_BEHAVIORS = Object.freeze({
|
||||
pushpin: { type: "pushpin", looseAsset: "pushpin", lodgedAsset: "pushpin_stuck", attachMode: "impact", panicOnAttach: true, damageOnAttach: 6, damagePerTick: 1.4, stressOnAttach: 18, stressPerTick: 2.6, blocksZunchi: false, burstZunchiOnDetach: false },
|
||||
|
|
@ -28,8 +30,9 @@
|
|||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "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", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_CATEGORY_TOOL_IDS },
|
||||
{ id: "electric", label: "\u96fb\u6c17", themeClass: "tool-category-electric", order: 46, toolIds: ELECTRIC_TOOL_IDS },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
||||
|
|
@ -54,6 +57,8 @@
|
|||
FENCE_ITEM_TYPES,
|
||||
MECHANICAL_ITEM_TYPES,
|
||||
PHYSICS_TOOL_IDS,
|
||||
PHYSICS_CATEGORY_TOOL_IDS,
|
||||
ELECTRIC_TOOL_IDS,
|
||||
ROTATABLE_ITEM_TYPES,
|
||||
PIN_BEHAVIORS,
|
||||
TOOL_CATEGORIES,
|
||||
|
|
|
|||
|
|
@ -256,10 +256,29 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
};
|
||||
}
|
||||
|
||||
if (type === "seesaw") {
|
||||
item.amount = 999;
|
||||
item.angle = 0;
|
||||
item.seesawSeatIds = ["", ""];
|
||||
item.seesawPhase = 0;
|
||||
item.seesawTilt = 0;
|
||||
item.seesawPlayTime = 0;
|
||||
item.deletable = true;
|
||||
}
|
||||
if (type === "circuit_board") {
|
||||
item.amount = 999;
|
||||
item.angle = defaultItemAngle(type);
|
||||
item.circuitConfig = globalThis.TarinaiCircuitBoardSystem?.defaultConfig?.() || { v: 4, cols: 10, rows: 10, inputs: [], outputs: [], nodes: [], wires: [] };
|
||||
globalThis.TarinaiCircuitBoardSystem?.ensureConfig?.(item);
|
||||
item._circuitInputState = Object.create(null);
|
||||
item._circuitOutputState = Object.create(null);
|
||||
item.deletable = true;
|
||||
}
|
||||
|
||||
if (type === "pressure_switch") {
|
||||
item.pressureTarget = "tarinai";
|
||||
item.pressureComparator = "gte";
|
||||
item.pressureThreshold = 1;
|
||||
item.pressureMin = 1;
|
||||
item.pressureMax = 300;
|
||||
item.pressureWidth = 220;
|
||||
item.pressureHeight = 160;
|
||||
item.pressureTimeStart = 360;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
// Common body, footprint, motion, and contact runtime for \u56de\u8ee2\u4f53, \u6bd2\u30d6\u30ed\u30c3\u30af, and \u5f80\u5fa9\u4f53.
|
||||
(function (global) {
|
||||
const footprints = global.TarinaiCollisionFootprints;
|
||||
function num(v, fallback = 0) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
});
|
||||
|
||||
function rectCorners(rect, padding = 0) {
|
||||
return footprints?.rectCorners?.(rect, padding) || [];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// Shared tiny helpers for positional projection used by links, mechanical
|
||||
// bodies, and circle/circle collision code.
|
||||
(function (global) {
|
||||
function num(v, fallback = 0) { const n = Number(v); return Number.isFinite(n) ? n : fallback; }
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => { const n = Number(v); return Number.isFinite(n) ? n : fallback; });
|
||||
|
||||
function markSpatialDirty(worldRef, item, reason = "projection", threshold = 0.35, minInterval = 0.065) {
|
||||
const world = worldRef || item?.world || null;
|
||||
|
|
|
|||
|
|
@ -7,17 +7,15 @@
|
|||
const BODY_TYPES = new Set(["rotator", "poison_block", "reciprocator"]);
|
||||
const LINK_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
|
||||
function num(value, fallback = 0) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback = 0) => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
});
|
||||
function bool(value, fallback = true) {
|
||||
if (value == null) return !!fallback;
|
||||
return !!value;
|
||||
}
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, num(value, min)));
|
||||
}
|
||||
const clamp = global.TarinaiCoreHelpers?.clampNumber || ((value, min, max) => Math.max(min, Math.min(max, num(value, min))));
|
||||
function isBodyType(typeOrItem) {
|
||||
const type = typeof typeOrItem === "string" ? typeOrItem : String(typeOrItem?.type || "");
|
||||
return BODY_TYPES.has(type);
|
||||
|
|
@ -271,6 +269,7 @@
|
|||
x: num(endpoint.x),
|
||||
y: num(endpoint.y),
|
||||
attachT: Number.isFinite(Number(endpoint.attachT)) ? num(endpoint.attachT) : null,
|
||||
portId: String(endpoint.portId || ""),
|
||||
fuzzyResolve: endpoint.fuzzyResolve === true,
|
||||
supportLost: endpoint.supportLost === true,
|
||||
_ref: endpoint._ref || null,
|
||||
|
|
@ -476,7 +475,7 @@
|
|||
if (!ep) return null;
|
||||
const kind = ep.kind || "item";
|
||||
const refIdx = kind === "tarinai" ? (tarinaiIndex.get(ep.id) ?? -1) : (itemIndex.get(ep.id) ?? -1);
|
||||
return { kind, refIdx, id: ep.id || "", type: ep.type || "", label: ep.label || "", token: ep.liveToken ?? null, lx: Math.round(num(ep.localX)), ly: Math.round(num(ep.localY)), center: ep.center ? 1 : 0, x: Math.round(num(ep.x)), y: Math.round(num(ep.y)), t: Number.isFinite(Number(ep.attachT)) ? Math.round(num(ep.attachT) * 1000) : null };
|
||||
return { kind, refIdx, id: ep.id || "", type: ep.type || "", label: ep.label || "", token: ep.liveToken ?? null, lx: Math.round(num(ep.localX)), ly: Math.round(num(ep.localY)), center: ep.center ? 1 : 0, x: Math.round(num(ep.x)), y: Math.round(num(ep.y)), t: Number.isFinite(Number(ep.attachT)) ? Math.round(num(ep.attachT) * 1000) : null, port: String(ep.portId || "") };
|
||||
};
|
||||
return { pf: 2, constraint: { type: c.type, kind: c.kind, endpoints: [epToSave(c.endpoints?.[0]), epToSave(c.endpoints?.[1])], length: Math.round(num(c.length, 80)), mid: [Math.round(num(c.mid?.x)), Math.round(num(c.mid?.y)), Math.round(num(c.mid?.vx) * 10), Math.round(num(c.mid?.vy) * 10)] } };
|
||||
}
|
||||
|
|
@ -513,7 +512,7 @@
|
|||
const kind = ep.kind || "item";
|
||||
const refIdx = Number(ep.refIdx);
|
||||
const indexedTarget = Number.isInteger(refIdx) && refIdx >= 0 ? (kind === "tarinai" ? tarinaiList[refIdx] : itemList[refIdx]) : null;
|
||||
return endpointToPlain({ kind, id: indexedTarget?.id || ep.id || "", type: ep.type || indexedTarget?.type || "", label: ep.label || indexedTarget?.name || "", liveToken: ep.token ?? indexedTarget?.liveToken ?? null, _ref: indexedTarget || null, localX: num(ep.lx), localY: num(ep.ly), center: !!ep.center, x: num(ep.x, item.x || 0), y: num(ep.y, item.y || 0), attachT: ep.t == null ? null : clamp(num(ep.t) / 1000, 0, 1), fuzzyResolve: !indexedTarget && !(Number.isInteger(refIdx) && refIdx >= 0) });
|
||||
return endpointToPlain({ kind, id: indexedTarget?.id || ep.id || "", type: ep.type || indexedTarget?.type || "", label: ep.label || indexedTarget?.name || "", liveToken: ep.token ?? indexedTarget?.liveToken ?? null, _ref: indexedTarget || null, localX: num(ep.lx), localY: num(ep.ly), center: !!ep.center, x: num(ep.x, item.x || 0), y: num(ep.y, item.y || 0), attachT: ep.t == null ? null : clamp(num(ep.t) / 1000, 0, 1), portId: String(ep.port || ""), fuzzyResolve: !indexedTarget && !(Number.isInteger(refIdx) && refIdx >= 0) });
|
||||
};
|
||||
const midArr = Array.isArray(p.mid) ? p.mid : [];
|
||||
item.physicsConstraint = normalizeConstraint({ type: item.type, kind: p.kind || (item.type === "rope" ? "flexible-distance" : (item.type === "rod" ? "rigid-distance" : (item.type === "spring" ? "spring-distance" : "signal-wire"))), endpoints: [epFromSave(p.endpoints?.[0]), epFromSave(p.endpoints?.[1])], length: Math.max(24, num(p.length, 80)), mid: { x: num(midArr[0], item.x || 0), y: num(midArr[1], item.y || 0), vx: num(midArr[2]) / 10, vy: num(midArr[3]) / 10 }, sleep: { awakeUntil: 0 }, particles: null }, item);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
// of placement blocking. Render and actual placement both delegate here so a
|
||||
// green preview means the same footprint the click will try to place.
|
||||
(function (global) {
|
||||
function num(v, fallback = 0) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
});
|
||||
function itemRadius(type, fallback = 12) {
|
||||
return global.itemRadiusFor(type, fallback);
|
||||
}
|
||||
|
|
@ -181,8 +181,10 @@
|
|||
}
|
||||
if (item.type === "rain_shelter") {
|
||||
const footprint = global.TarinaiParasolSystem?.parasolFootprint?.(item) || null;
|
||||
if (footprint) return { shape: "ellipse", centerX: footprint.centerX, centerY: footprint.centerY, radiusX: footprint.radiusX, radiusY: footprint.radiusY };
|
||||
if (footprint) return { shape: "roundedRect", centerX: footprint.centerX, centerY: footprint.centerY, halfWidth: footprint.radiusX, halfHeight: footprint.radiusY, cornerRadius: Math.max(8, Math.min(18, footprint.radiusY * 0.32)) };
|
||||
}
|
||||
const def = global.toolDefinition?.(item.type) || null;
|
||||
if (def?.collisionShape === "circle") return { shape: "circle", centerX: num(item.x), centerY: num(item.y), radius: Math.max(8, num(item.r, itemRadius(item.type, 12))) };
|
||||
return null;
|
||||
}
|
||||
function geometryFor(worldRef, item) {
|
||||
|
|
|
|||
59
js/render.js
59
js/render.js
|
|
@ -175,6 +175,44 @@ function strokeWorldObstacleRect(ctx, rect) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function drawWireConnectableHighlights(ctx, world) {
|
||||
if (!world || !["wire", "insulated_wire"].includes(String(world.tool || ""))) return;
|
||||
const signal = globalThis.TarinaiSignalSystem;
|
||||
const circuit = globalThis.TarinaiCircuitBoardSystem;
|
||||
if (!signal?.isSignalConnectable) return;
|
||||
const pulse = 0.72 + Math.sin((world.time || 0) * 7.5) * 0.13;
|
||||
const pointer = world.pointer || {};
|
||||
ctx.save();
|
||||
ctx.setLineDash([7, 5]);
|
||||
const blockedTypes = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
for (const item of world.items || []) {
|
||||
if (!item || item.dead || item.type === "splat" || blockedTypes.has(item.type)) continue;
|
||||
const signalCapable = signal.isSignalConnectable(item);
|
||||
if (item.type === "circuit_board") {
|
||||
for (const port of circuit?.ports?.(item) || []) {
|
||||
const point = circuit.portWorld(item, port);
|
||||
if (!point) continue;
|
||||
const hovered = pointer.inside && Math.hypot(pointer.x - point.x, pointer.y - point.y) <= 17;
|
||||
const selected = world.pendingLinkEndpoint?.id === item.id && world.pendingLinkEndpoint?.portId === port.portId;
|
||||
ctx.globalAlpha = selected ? 1 : (hovered ? 0.98 : 0.72);
|
||||
ctx.fillStyle = port.direction === "input" ? "rgba(74,177,255,0.22)" : "rgba(255,132,65,0.22)";
|
||||
ctx.strokeStyle = selected ? "rgba(255,235,92,1)" : (port.direction === "input" ? "rgba(74,177,255,0.98)" : "rgba(255,132,65,0.98)");
|
||||
ctx.lineWidth = selected || hovered ? 3.8 : 2.5;
|
||||
ctx.beginPath(); ctx.arc(point.x, point.y, (selected || hovered ? 12 : 9) * pulse, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const selected = world.pendingLinkEndpoint?.id === item.id;
|
||||
const radius = Math.max(18, Number(item.r || 22) * 1.22 + 7);
|
||||
ctx.globalAlpha = selected ? 1 : (signalCapable ? 0.66 : 0.30);
|
||||
ctx.strokeStyle = selected ? "rgba(255,235,92,1)" : (signalCapable ? "rgba(90,207,255,0.94)" : "rgba(224,232,238,0.72)");
|
||||
ctx.lineWidth = selected ? 4 : (signalCapable ? 2.4 : 1.6);
|
||||
ctx.beginPath(); ctx.ellipse(item.x, item.y, radius, radius * 0.78, 0, 0, Math.PI * 2); ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawToolTargetHoverHighlight(ctx, world) {
|
||||
if (!world) return;
|
||||
const tool = String(world.tool || "");
|
||||
|
|
@ -495,7 +533,6 @@ function drawPlacementPreview(ctx, world) {
|
|||
const preview = placementPreviewFor(world);
|
||||
if (!preview) return;
|
||||
const { type, x, y, r, rect, rects, overlay, influence, blocked } = preview;
|
||||
const pulse = 0.55 + Math.sin((world.time || 0) * 7.0) * 0.10;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = blocked ? 0.44 : 0.72;
|
||||
ctx.lineWidth = blocked ? 2.2 : 2.0;
|
||||
|
|
@ -519,9 +556,9 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
if (overlay?.shape === "ellipse") {
|
||||
if (overlay?.shape === "circle") {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(overlay.centerX, overlay.centerY, Math.max(1, overlay.radiusX), Math.max(1, overlay.radiusY), 0, 0, Math.PI * 2);
|
||||
ctx.arc(overlay.centerX, overlay.centerY, Math.max(1, overlay.radius), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else if (overlay?.shape === "roundedRect") {
|
||||
|
|
@ -542,14 +579,9 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.strokeRect(part.left, part.top, part.right - part.left, part.bottom - part.top);
|
||||
}
|
||||
}
|
||||
} else if (type === "stone" || type === "ball" || type === "balloon" || type === "fan" || type === "pipe" || type === "firecracker" || isPinType(type) || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, Math.max(8, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, Math.max(9, r * 1.55), Math.max(6, r * 0.86), 0, 0, Math.PI * 2);
|
||||
ctx.arc(x, y, Math.max(8, r), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
|
@ -585,8 +617,8 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
const markerX = (overlay?.shape === "ellipse" || overlay?.shape === "roundedRect") ? overlay.centerX : x;
|
||||
const markerY = (overlay?.shape === "ellipse" || overlay?.shape === "roundedRect") ? overlay.centerY : y;
|
||||
const markerX = (overlay?.shape === "circle" || overlay?.shape === "roundedRect") ? overlay.centerX : x;
|
||||
const markerY = (overlay?.shape === "circle" || overlay?.shape === "roundedRect") ? overlay.centerY : y;
|
||||
ctx.globalAlpha = blocked ? 0.52 : 0.70;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(markerX - 8, markerY);
|
||||
|
|
@ -711,7 +743,7 @@ function drawOperationToolPreview(ctx, world) {
|
|||
}
|
||||
} else {
|
||||
const r = Math.max(10, item.r || itemRadiusFor?.(type, 16) || 16);
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, r * 1.45, r * 0.95, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(item.x, item.y, r, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
const markerX = (previewOverlay?.shape === "ellipse" || previewOverlay?.shape === "roundedRect") ? previewOverlay.centerX : item.x;
|
||||
|
|
@ -901,7 +933,7 @@ function linkIntersectsVisibleRect(it, worldRef, rect, margin = 18) {
|
|||
}
|
||||
|
||||
function isBehindSpriteLayerItem(it) {
|
||||
return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed" || it.type === "toilet"));
|
||||
return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed" || it.type === "toilet" || it.type === "seesaw"));
|
||||
}
|
||||
|
||||
function renderLayerSortY(entity) {
|
||||
|
|
@ -1764,6 +1796,7 @@ function render() {
|
|||
}
|
||||
renderPerfEnd(endAttachments);
|
||||
|
||||
drawWireConnectableHighlights(ctx, world);
|
||||
drawToolTargetHoverHighlight(ctx, world);
|
||||
|
||||
const endEffects = renderPerfBegin("render.draw.effects");
|
||||
|
|
|
|||
|
|
@ -635,7 +635,7 @@
|
|||
return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial, tarinaiPopulationLimit, objectLimit];
|
||||
}
|
||||
|
||||
function birthHash32(seed = "") {
|
||||
const birthHash32 = global.TarinaiCoreHelpers?.birthHash32 || (seed => {
|
||||
const match = /^b[0-9a-z]+_([0-9a-z]+)$/i.exec(String(seed || ""));
|
||||
if (match) {
|
||||
const parsed = parseInt(match[1], 36);
|
||||
|
|
@ -648,7 +648,7 @@
|
|||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
});
|
||||
function birthSeedFromDescriptor(descriptor = [], worldSeed = "", fallbackSerial = 1) {
|
||||
const serial = Math.max(1, Math.floor(Number(descriptor?.[0]) || fallbackSerial || 1));
|
||||
const expected = global.TarinaiSeedFactory?.childSeed?.(worldSeed, serial, []) || `b${serial.toString(36)}_0`;
|
||||
|
|
@ -1095,6 +1095,7 @@
|
|||
if (!indexed && (Number(endpoint.x) || 0) !== (Number(row?.[1]) || 0)) mask |= 128;
|
||||
if (!indexed && (Number(endpoint.y) || 0) !== (Number(row?.[2]) || 0)) mask |= 256;
|
||||
if (endpoint.t != null && Number.isFinite(Number(endpoint.t))) mask |= 512;
|
||||
if (endpoint.port) mask |= 1024;
|
||||
writer.u(mask);
|
||||
if (mask & 1) writer.str(endpoint.id);
|
||||
if (mask & 2) writer.str(endpoint.type);
|
||||
|
|
@ -1105,6 +1106,7 @@
|
|||
if (mask & 128) writer.s((Number(endpoint.x) || 0) - (Number(row?.[1]) || 0));
|
||||
if (mask & 256) writer.s((Number(endpoint.y) || 0) - (Number(row?.[2]) || 0));
|
||||
if (mask & 512) writer.u(endpoint.t);
|
||||
if (mask & 1024) writer.str(endpoint.port);
|
||||
}
|
||||
function readPhysicsEndpoint(reader, row = []) {
|
||||
const [present] = readBitFields(reader, [1]);
|
||||
|
|
@ -1124,7 +1126,8 @@
|
|||
const x = (Number(row?.[1]) || 0) + ((mask & 128) ? reader.s() : 0);
|
||||
const y = (Number(row?.[2]) || 0) + ((mask & 256) ? reader.s() : 0);
|
||||
const t = (mask & 512) ? reader.u() : null;
|
||||
return { kind, refIdx, id, type, label, token, lx, ly, center, x, y, t };
|
||||
const port = (mask & 1024) ? reader.str() : "";
|
||||
return { kind, refIdx, id, type, label, token, lx, ly, center, x, y, t, port };
|
||||
}
|
||||
function writePhysicsConstraintExtra(writer, extra, type, row = []) {
|
||||
const constraint = extra?.constraint && typeof extra.constraint === "object" ? extra.constraint : {};
|
||||
|
|
@ -1215,11 +1218,12 @@
|
|||
else if (type === "zunchi") writeFixedSignedDefaults(writer, list, [0, 100, 100], 3);
|
||||
else if (type === "signboard") writeSaveString(writer, list[0] || "");
|
||||
else if (type === "robot_cleaner") writeFixedSignedDefaults(writer, list, [3, 0], 2);
|
||||
else if (type === "circuit_board") writeSaveString(writer, list[0] || "{}");
|
||||
else if (type === "duplicator") writeFixedSignedDefaults(writer, list, [-1], 1);
|
||||
else if (type === "ball" || type === "balloon") writeFixedSignedDefaults(writer, list, [0, 0], 2);
|
||||
else if (type === "ant_nest") writeFixedSignedDefaults(writer, list, [0, 0], 2);
|
||||
else if (type === "ant_corpse" || type === "firecracker" || type === "flame_firecracker" || type === "fire") writeFixedSignedDefaults(writer, list, [0], 1);
|
||||
else if (type === "sticky_bomb") writeFixedSignedDefaults(writer, list, [0, -1], 2);
|
||||
else if (type === "sticky_bomb") writeFixedSignedDefaults(writer, list, [0, -1, 0], 3);
|
||||
else if (type === "nest_box") writeFixedSignedDefaults(writer, list, [0, 0, 0, 0], 4);
|
||||
else if (isPinSaveType(type)) writeFixedSignedDefaults(writer, list, [0, -1, 0, 0, 0], 5);
|
||||
else if (type === "fan") {
|
||||
|
|
@ -1242,11 +1246,12 @@
|
|||
if (type === "zunchi") return readFixedSignedDefaults(reader, [0, 100, 100], 3);
|
||||
if (type === "signboard") return [readSaveString(reader)];
|
||||
if (type === "robot_cleaner") return readFixedSignedDefaults(reader, [3, 0], 2);
|
||||
if (type === "circuit_board") return [readSaveString(reader)];
|
||||
if (type === "duplicator") return readFixedSignedDefaults(reader, [-1], 1);
|
||||
if (type === "ball" || type === "balloon") return readFixedSignedDefaults(reader, [0, 0], 2);
|
||||
if (type === "ant_nest") return readFixedSignedDefaults(reader, [0, 0], 2);
|
||||
if (type === "ant_corpse" || type === "firecracker" || type === "flame_firecracker" || type === "fire") return readFixedSignedDefaults(reader, [0], 1);
|
||||
if (type === "sticky_bomb") return readFixedSignedDefaults(reader, [0, -1], 2);
|
||||
if (type === "sticky_bomb") return readFixedSignedDefaults(reader, [0, -1, 0], 3);
|
||||
if (type === "nest_box") return readFixedSignedDefaults(reader, [0, 0, 0, 0], 4);
|
||||
if (isPinSaveType(type)) return readFixedSignedDefaults(reader, [0, -1, 0, 0, 0], 5);
|
||||
if (type === "fan") {
|
||||
|
|
@ -1459,16 +1464,11 @@
|
|||
return items;
|
||||
}
|
||||
|
||||
const LEGACY_JSON_BINARY_SCHEMA_VERSION = 41;
|
||||
const LEGACY_COMPACT_BINARY_SCHEMA_VERSION = 42;
|
||||
const ACHIEVEMENT_METADATA_BINARY_VERSION = 1;
|
||||
const ACHIEVEMENT_SPELL_BINARY_VERSION = 3;
|
||||
const PREVIOUS_ACHIEVEMENT_SPELL_BINARY_VERSION = 2;
|
||||
const ACHIEVEMENT_MASK_BYTES = 7;
|
||||
const LEGACY_ACHIEVEMENT_PROGRESS_BITS = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10]);
|
||||
const LEGACY_ACHIEVEMENT_PROGRESS_MAX = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666]);
|
||||
const ACHIEVEMENT_PROGRESS_BITS = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10, 9, 10]);
|
||||
const ACHIEVEMENT_PROGRESS_MAX = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666, 333, 721]);
|
||||
const ACHIEVEMENT_SPELL_BINARY_VERSION = 5;
|
||||
const ACHIEVEMENT_MASK_BYTES = 8;
|
||||
const ACHIEVEMENT_PROGRESS_BITS = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10, 9, 10, 3, 17]);
|
||||
const ACHIEVEMENT_PROGRESS_MAX = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666, 333, 721, 7, 131071]);
|
||||
|
||||
function finiteNumberOrNull(value) {
|
||||
const n = Number(value);
|
||||
|
|
@ -1493,34 +1493,14 @@
|
|||
function achievementMaskBytesFromSpell(spell = null) {
|
||||
const bytes = new Uint8Array(ACHIEVEMENT_MASK_BYTES);
|
||||
if (!Array.isArray(spell)) return { bytes, baseMinute: 0, deltas: [], progress: [] };
|
||||
const version = Number(spell[0]);
|
||||
let baseMinute = 0;
|
||||
let deltas = [];
|
||||
let progress = [];
|
||||
if (version === 1) {
|
||||
const maskHex = String(spell[1] || "").toLowerCase();
|
||||
if (!/^[0-9a-f]{14}$/.test(maskHex)) throw new Error("invalid achievement spell mask");
|
||||
for (let i = 0; i < ACHIEVEMENT_MASK_BYTES; i++) bytes[i] = parseInt(maskHex.slice(i * 2, i * 2 + 2), 16);
|
||||
const baseSecond = toSafeUInt(spell[2]);
|
||||
const secondDeltas = Array.isArray(spell[3]) ? spell[3] : [];
|
||||
const absoluteMinutes = secondDeltas.map(delta => Math.floor((baseSecond + toSafeUInt(delta)) / 60));
|
||||
baseMinute = absoluteMinutes.length ? Math.min(...absoluteMinutes) : 0;
|
||||
deltas = absoluteMinutes.map(value => value - baseMinute);
|
||||
progress = Array.isArray(spell[4]) ? spell[4] : [];
|
||||
} else if (version === PREVIOUS_ACHIEVEMENT_SPELL_BINARY_VERSION || version === ACHIEVEMENT_SPELL_BINARY_VERSION) {
|
||||
const low = Math.max(0, Math.min(0xffffffff, Math.floor(Number(spell[1]) || 0)));
|
||||
const high = Math.max(0, Math.min(0xffffff, Math.floor(Number(spell[2]) || 0)));
|
||||
for (let i = 0; i < 4; i++) bytes[i] = Math.floor(low / (2 ** (i * 8))) & 255;
|
||||
for (let i = 0; i < 3; i++) bytes[i + 4] = Math.floor(high / (2 ** (i * 8))) & 255;
|
||||
baseMinute = toSafeUInt(spell[3]);
|
||||
deltas = Array.isArray(spell[4]) ? spell[4].map(toSafeUInt) : [];
|
||||
const rawProgress = Array.isArray(spell[5]) ? spell[5] : [];
|
||||
progress = version === ACHIEVEMENT_SPELL_BINARY_VERSION
|
||||
? rawProgress
|
||||
: [...rawProgress.slice(0, 9), 0, 0, 0];
|
||||
} else {
|
||||
throw new Error("unsupported achievement spell version");
|
||||
}
|
||||
if (Number(spell[0]) !== ACHIEVEMENT_SPELL_BINARY_VERSION) throw new Error("unsupported achievement spell version");
|
||||
const low = Math.max(0, Math.min(0xffffffff, Math.floor(Number(spell[1]) || 0)));
|
||||
const high = Math.max(0, Math.min(0xffffffff, Math.floor(Number(spell[2]) || 0)));
|
||||
for (let i = 0; i < 4; i++) bytes[i] = Math.floor(low / (2 ** (i * 8))) & 255;
|
||||
for (let i = 0; i < 4; i++) bytes[i + 4] = Math.floor(high / (2 ** (i * 8))) & 255;
|
||||
const baseMinute = toSafeUInt(spell[3]);
|
||||
const deltas = Array.isArray(spell[4]) ? spell[4].map(toSafeUInt) : [];
|
||||
const progress = Array.isArray(spell[5]) ? spell[5] : [];
|
||||
let unlockedCount = 0;
|
||||
for (const byte of bytes) {
|
||||
let value = byte;
|
||||
|
|
@ -1529,11 +1509,12 @@
|
|||
if (deltas.length !== unlockedCount) throw new Error("invalid achievement spell timestamps");
|
||||
return { bytes, baseMinute, deltas, progress };
|
||||
}
|
||||
|
||||
function spellFromAchievementMaskBytes(bytes = [], baseMinute = 0, deltas = [], progress = []) {
|
||||
let low = 0;
|
||||
let high = 0;
|
||||
for (let i = 0; i < 4; i++) low += (Number(bytes[i]) || 0) * (2 ** (i * 8));
|
||||
for (let i = 0; i < 3; i++) high += (Number(bytes[i + 4]) || 0) * (2 ** (i * 8));
|
||||
for (let i = 0; i < 4; i++) high += (Number(bytes[i + 4]) || 0) * (2 ** (i * 8));
|
||||
return [ACHIEVEMENT_SPELL_BINARY_VERSION, low >>> 0, high >>> 0, toSafeUInt(baseMinute), deltas.map(toSafeUInt), progress];
|
||||
}
|
||||
function writeAchievementProgress(writer, values = [], bits = ACHIEVEMENT_PROGRESS_BITS, maxima = ACHIEVEMENT_PROGRESS_MAX) {
|
||||
|
|
@ -1568,7 +1549,7 @@
|
|||
for (const delta of normalized.deltas) writer.u(delta);
|
||||
writeAchievementProgress(writer, normalized.progress);
|
||||
}
|
||||
function readAchievementSpell(reader, schema = BINARY_SCHEMA_VERSION) {
|
||||
function readAchievementSpell(reader) {
|
||||
const bytes = reader.raw(ACHIEVEMENT_MASK_BYTES);
|
||||
let unlockedCount = 0;
|
||||
for (const byte of bytes) {
|
||||
|
|
@ -1578,11 +1559,8 @@
|
|||
const baseMinute = reader.u();
|
||||
const deltas = [];
|
||||
for (let i = 0; i < unlockedCount; i++) deltas.push(reader.u());
|
||||
const legacy = schema === LEGACY_COMPACT_BINARY_SCHEMA_VERSION;
|
||||
const progress = legacy
|
||||
? readAchievementProgress(reader, LEGACY_ACHIEVEMENT_PROGRESS_BITS, LEGACY_ACHIEVEMENT_PROGRESS_MAX)
|
||||
: readAchievementProgress(reader);
|
||||
return spellFromAchievementMaskBytes(bytes, baseMinute, deltas, legacy ? [...progress.slice(0, 9), 0, 0, 0] : progress);
|
||||
const progress = readAchievementProgress(reader);
|
||||
return spellFromAchievementMaskBytes(bytes, baseMinute, deltas, progress);
|
||||
}
|
||||
function writeAchievementMetadata(writer, metadata = null, tarinaiCount = 0, itemCount = 0) {
|
||||
const value = metadata && typeof metadata === "object" ? metadata : {};
|
||||
|
|
@ -1749,7 +1727,7 @@
|
|||
const formatHeader = reader.b();
|
||||
const schema = formatHeader & 0x7f;
|
||||
const hasStringTable = Boolean(formatHeader & 0x80);
|
||||
if (![BINARY_SCHEMA_VERSION, LEGACY_COMPACT_BINARY_SCHEMA_VERSION, LEGACY_JSON_BINARY_SCHEMA_VERSION].includes(schema)) throw new Error("unsupported binary save schema");
|
||||
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
|
||||
readStringTable(reader, hasStringTable);
|
||||
const w = readWorld(reader);
|
||||
const tCount = reader.u();
|
||||
|
|
@ -1760,12 +1738,7 @@
|
|||
const items = readItemBlocks(reader);
|
||||
let g = null;
|
||||
try {
|
||||
if (schema === LEGACY_JSON_BINARY_SCHEMA_VERSION) {
|
||||
const encodedMetadata = reader.str();
|
||||
g = encodedMetadata ? JSON.parse(encodedMetadata) : null;
|
||||
} else {
|
||||
g = readAchievementMetadata(reader, tCount, items.length, schema);
|
||||
}
|
||||
g = readAchievementMetadata(reader, tCount, items.length, schema);
|
||||
} catch (_) {
|
||||
throw new Error("invalid achievement metadata in binary save");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
if (!ids) throw new Error("TarinaiDomainIds must be loaded before save_schema.js");
|
||||
if (!itemCatalog) throw new Error("TarinaiItemTypeCatalog must be loaded before save_schema.js");
|
||||
|
||||
const SNAPSHOT_VERSION = 35;
|
||||
const BINARY_SCHEMA_VERSION = 43;
|
||||
const SNAPSHOT_VERSION = 40;
|
||||
const BINARY_SCHEMA_VERSION = 49;
|
||||
|
||||
function moodValue(index, fallback = "relaxed") {
|
||||
return ids.canonicalMoodId(ids.enumValue(ids.MOOD_IDS, index, fallback));
|
||||
|
|
|
|||
217
js/seesaw_system.js
Normal file
217
js/seesaw_system.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
"use strict";
|
||||
|
||||
// Two-creature leisure equipment. Seats are reserved independently and only
|
||||
// produce play/friendship while both ends are occupied.
|
||||
(function (global) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => Number.isFinite(Number(v)) ? Number(v) : fallback);
|
||||
const clamp = global.TarinaiCoreHelpers?.clampNumber || ((v, a, b) => Math.max(a, Math.min(b, Number(v) || 0)));
|
||||
|
||||
function ensure(item) {
|
||||
if (!item) return null;
|
||||
if (!Array.isArray(item.seesawSeatIds)) item.seesawSeatIds = ["", ""];
|
||||
if (item.seesawSeatIds.length < 2) item.seesawSeatIds = [item.seesawSeatIds[0] || "", item.seesawSeatIds[1] || ""];
|
||||
item.angle = 0;
|
||||
item.seesawPhase = num(item.seesawPhase);
|
||||
item.seesawTilt = num(item.seesawTilt);
|
||||
return item;
|
||||
}
|
||||
|
||||
function seatWorld(item, index = 0) {
|
||||
ensure(item);
|
||||
const side = index === 1 ? 1 : -1;
|
||||
const reach = Math.max(48, num(item?.r, 60) * 0.94);
|
||||
return { x: num(item?.x) + reach * side, y: num(item?.y) - 2, index: index === 1 ? 1 : 0 };
|
||||
}
|
||||
|
||||
function actorById(worldRef, id) {
|
||||
if (!id) return null;
|
||||
return worldRef?.liveTarinaiById?.(id) || (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
||||
}
|
||||
|
||||
function release(tarinai, item = null) {
|
||||
const board = item || tarinai?.target;
|
||||
if (board?.type === "seesaw") {
|
||||
ensure(board);
|
||||
for (let i = 0; i < 2; i += 1) if (board.seesawSeatIds[i] === tarinai?.id) board.seesawSeatIds[i] = "";
|
||||
}
|
||||
if (tarinai) {
|
||||
tarinai.seesawItemId = "";
|
||||
tarinai.seesawSeatIndex = -1;
|
||||
tarinai.seesawWaitStartedAt = -Infinity;
|
||||
tarinai.seesawPlayStartedAt = -Infinity;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanReservations(item, worldRef) {
|
||||
ensure(item);
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
const actor = actorById(worldRef, item.seesawSeatIds[i]);
|
||||
if (!actor || actor.dead || actor.seesawItemId !== item.id || actor.seesawSeatIndex !== i || actor.state !== "play_seesaw") item.seesawSeatIds[i] = "";
|
||||
}
|
||||
}
|
||||
|
||||
function findAvailable(worldRef, tarinai, range = 560) {
|
||||
let best = null;
|
||||
let bestScore = Number(range) || 560;
|
||||
const list = worldRef?.itemsOfType?.("seesaw") || (worldRef?.items || []).filter(item => item?.type === "seesaw");
|
||||
for (const item of list) {
|
||||
if (!item || item.dead) continue;
|
||||
cleanReservations(item, worldRef);
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
const reserved = item.seesawSeatIds[index];
|
||||
if (reserved && reserved !== tarinai?.id) continue;
|
||||
const point = seatWorld(item, index);
|
||||
const d = Math.hypot(num(tarinai?.x) - point.x, num(tarinai?.y) - point.y);
|
||||
const hasPartner = Boolean(item.seesawSeatIds[1 - index]);
|
||||
const score = d - (hasPartner ? 90 : 0);
|
||||
if (score < bestScore) { bestScore = score; best = { item, seatIndex: index, point, hasPartner }; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function shouldSeek(worldRef, tarinai, needs = null) {
|
||||
const found = findAvailable(worldRef, tarinai, 620);
|
||||
if (!found) return false;
|
||||
const fulfill = num(needs?.fulfill ?? tarinai?.needs?.fulfill);
|
||||
const social = num(needs?.social ?? tarinai?.needs?.social);
|
||||
if (found.hasPartner) return Math.max(fulfill, social) >= 8;
|
||||
return fulfill >= 26 || social >= 40;
|
||||
}
|
||||
|
||||
function actionPriority(worldRef, tarinai) {
|
||||
const found = findAvailable(worldRef, tarinai, 620);
|
||||
if (!found) return 0;
|
||||
return found.hasPartner ? 74 : 34;
|
||||
}
|
||||
|
||||
function isPlaying(tarinai, worldRef = tarinai?.world) {
|
||||
const reservation = validReservation(tarinai, worldRef);
|
||||
if (!reservation) return false;
|
||||
const partnerId = reservation.item.seesawSeatIds[1 - reservation.index];
|
||||
const partner = actorById(worldRef, partnerId);
|
||||
if (!partner || partner.dead || partner.state !== "play_seesaw") return false;
|
||||
const ownPoint = reservation.point;
|
||||
const partnerPoint = seatWorld(reservation.item, 1 - reservation.index);
|
||||
const ownArrived = Math.hypot(num(tarinai.x) - ownPoint.x, num(tarinai.y) - ownPoint.y) <= Math.max(28, num(tarinai.radius, 20) * 1.25);
|
||||
const partnerArrived = Math.hypot(num(partner.x) - partnerPoint.x, num(partner.y) - partnerPoint.y) <= Math.max(28, num(partner.radius, 20) * 1.25);
|
||||
return ownArrived && partnerArrived;
|
||||
}
|
||||
|
||||
function reserve(item, tarinai, preferredIndex = null) {
|
||||
if (!item || item.dead || item.type !== "seesaw" || !tarinai || tarinai.dead) return -1;
|
||||
ensure(item);
|
||||
const candidates = preferredIndex === 0 || preferredIndex === 1 ? [preferredIndex, 1 - preferredIndex] : [0, 1];
|
||||
for (const index of candidates) {
|
||||
if (!item.seesawSeatIds[index] || item.seesawSeatIds[index] === tarinai.id) {
|
||||
item.seesawSeatIds[index] = tarinai.id;
|
||||
tarinai.seesawItemId = item.id || "";
|
||||
tarinai.seesawSeatIndex = index;
|
||||
tarinai.seesawWaitStartedAt = num(tarinai.world?.time, 0);
|
||||
tarinai.seesawPlayStartedAt = -Infinity;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function start(tarinai, worldRef, item = null) {
|
||||
const found = item ? { item, seatIndex: null } : findAvailable(worldRef, tarinai, 620);
|
||||
const target = found?.item;
|
||||
if (!target) return false;
|
||||
cleanReservations(target, worldRef);
|
||||
const index = reserve(target, tarinai, found.seatIndex);
|
||||
if (index < 0) return false;
|
||||
tarinai.setActionState?.("play_seesaw", { target, reason: "\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u307c\u3046\u3068\u3057\u3066\u3044\u308b", sleeping: false, wake: true });
|
||||
tarinai.target = target;
|
||||
tarinai.thought = "\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u307c\u3046\u3068\u3057\u3066\u3044\u308b";
|
||||
return true;
|
||||
}
|
||||
|
||||
function validReservation(tarinai, worldRef) {
|
||||
if (!tarinai || tarinai.dead || tarinai.state !== "play_seesaw") return null;
|
||||
const item = worldRef?.itemById?.(tarinai.seesawItemId) || (worldRef?.items || []).find(value => value && !value.dead && value.id === tarinai.seesawItemId);
|
||||
if (!item || item.type !== "seesaw") return null;
|
||||
cleanReservations(item, worldRef);
|
||||
const index = tarinai.seesawSeatIndex === 1 ? 1 : 0;
|
||||
if (item.seesawSeatIds[index] !== tarinai.id) return null;
|
||||
return { item, index, point: seatWorld(item, index) };
|
||||
}
|
||||
|
||||
function tickAction(tarinai, worldRef) {
|
||||
const reservation = validReservation(tarinai, worldRef);
|
||||
if (!reservation) return false;
|
||||
const d = Math.hypot(num(tarinai.x) - reservation.point.x, num(tarinai.y) - reservation.point.y);
|
||||
tarinai.thought = d > Math.max(22, num(tarinai.radius, 20)) ? "\u30b7\u30fc\u30bd\u30fc\u3078\u5411\u304b\u3063\u3066\u3044\u308b" : (reservation.item.seesawSeatIds[1 - reservation.index] ? "\u4e00\u7dd2\u306b\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u3093\u3067\u3044\u308b" : "\u76f8\u624b\u3092\u5f85\u3063\u3066\u3044\u308b");
|
||||
const now = num(worldRef?.time);
|
||||
if (Number.isFinite(tarinai.seesawPlayStartedAt) && tarinai.seesawPlayStartedAt > -Infinity) {
|
||||
if (now - tarinai.seesawPlayStartedAt >= 18) return "finished";
|
||||
} else {
|
||||
const dayLength = Math.max(1, num(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength, 120));
|
||||
if (now - num(tarinai.seesawWaitStartedAt, now) >= Math.max(30, dayLength * 0.5)) return "finished";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateWorld(worldRef, dt = 0.016) {
|
||||
if (!worldRef?.items) return false;
|
||||
const step = Math.max(0, Math.min(0.1, num(dt, 0.016)));
|
||||
const seesaws = worldRef.itemsOfType?.("seesaw") || worldRef.items.filter(item => item?.type === "seesaw");
|
||||
for (const item of seesaws) {
|
||||
if (!item || item.dead) continue;
|
||||
ensure(item); cleanReservations(item, worldRef);
|
||||
const actors = [actorById(worldRef, item.seesawSeatIds[0]), actorById(worldRef, item.seesawSeatIds[1])];
|
||||
const arrived = actors.map((actor, index) => {
|
||||
if (!actor) return false;
|
||||
const point = seatWorld(item, index);
|
||||
return Math.hypot(num(actor.x) - point.x, num(actor.y) - point.y) <= Math.max(28, num(actor.radius, 20) * 1.25);
|
||||
});
|
||||
const playing = Boolean(actors[0] && actors[1] && arrived[0] && arrived[1]);
|
||||
const pairKey = playing ? `${actors[0].id}|${actors[1].id}` : "";
|
||||
if (pairKey && item.seesawPairKey !== pairKey) {
|
||||
item.seesawPairKey = pairKey;
|
||||
const now = num(worldRef.time);
|
||||
actors[0].seesawPlayStartedAt = now;
|
||||
actors[1].seesawPlayStartedAt = now;
|
||||
} else if (!pairKey) item.seesawPairKey = "";
|
||||
if (playing) item.seesawPhase += step * 4.6;
|
||||
const targetTilt = playing ? Math.sin(item.seesawPhase) * 0.16 : 0;
|
||||
item.seesawTilt += (targetTilt - item.seesawTilt) * Math.min(1, step * 8);
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
const actor = actors[index];
|
||||
if (!actor || !arrived[index]) continue;
|
||||
const base = seatWorld(item, index);
|
||||
const side = index === 1 ? 1 : -1;
|
||||
const lift = Math.sin(item.seesawPhase) * 9 * side;
|
||||
actor.x = base.x;
|
||||
actor.y = base.y - 12 + (playing ? lift : 0);
|
||||
actor.vx = 0; actor.vy = 0;
|
||||
const actionText = playing ? "\u30b7\u30fc\u30bd\u30fc\u3092\u3057\u3066\u3044\u308b\u3002" : "\u76f8\u624b\u3092\u5f85\u3063\u3066\u3044\u308b";
|
||||
actor.thought = actionText;
|
||||
if (playing && typeof global.setBehaviorText === "function") {
|
||||
const behavior = global.currentTarinaiBehavior?.(actor);
|
||||
if (behavior?.actionId === "play_seesaw" && behavior?.reason !== actionText) {
|
||||
global.setBehaviorText(actor, { need: "fulfill", subNeed: "social_play", actionId: "play_seesaw", actionLabel: actionText, reasonText: actionText, target: item, phase: "perform", source: behavior?.source || "need" });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (playing) {
|
||||
const [a, b] = actors;
|
||||
a.adjustRelation?.(b, step * 0.70, -step * 0.025, "seesaw_play");
|
||||
b.adjustRelation?.(a, step * 0.70, -step * 0.025, "seesaw_play");
|
||||
if (typeof global.applyNeedSatisfaction === "function") {
|
||||
global.applyNeedSatisfaction(a, { fulfill: step * 1.4, social: step * 0.55 }, "seesaw");
|
||||
global.applyNeedSatisfaction(b, { fulfill: step * 1.4, social: step * 0.55 }, "seesaw");
|
||||
} else {
|
||||
a.stress = clamp(num(a.stress) - step * 0.35, 0, 100);
|
||||
b.stress = clamp(num(b.stress) - step * 0.35, 0, 100);
|
||||
}
|
||||
item.seesawPlayTime = num(item.seesawPlayTime) + step;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
global.TarinaiSeesawSystem = Object.freeze({ ensure, seatWorld, findAvailable, shouldSeek, actionPriority, isPlaying, reserve, release, start, tickAction, updateWorld, validReservation });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
const WIRE_TYPES = new Set(["wire", "insulated_wire"]);
|
||||
const LINK_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
const SIGNAL_SOURCE_TYPES = new Set(["pressure_switch"]);
|
||||
const DIRECT_SIGNAL_SINK_TYPES = new Set(["rotator", "reciprocator", "robot_cleaner", "gate_fence"]);
|
||||
const DIRECT_SIGNAL_SINK_TYPES = new Set(["rotator", "reciprocator", "robot_cleaner", "gate_fence", "fan", "stove", "dry_ice", "duplicator"]);
|
||||
const DETECTOR_METRICS = Object.freeze({
|
||||
tarinai_count: { label: "\u305f\u308a\u306a\u3044\u6570", defaultThreshold: 1 },
|
||||
hunger_avg: { label: "\u5e73\u5747\u7a7a\u8179", defaultThreshold: 70 },
|
||||
|
|
@ -34,11 +34,11 @@
|
|||
});
|
||||
|
||||
|
||||
function num(value, fallback = 0) {
|
||||
const num = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback = 0) => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
function clamp(value, min, max) { return Math.max(min, Math.min(max, num(value, min))); }
|
||||
});
|
||||
const clamp = global.TarinaiCoreHelpers?.clampNumber || ((value, min, max) => Math.max(min, Math.min(max, num(value, min))));
|
||||
function normalizeMinute(value, fallback = 0) {
|
||||
return Math.max(0, Math.min(1435, Math.round(num(value, fallback) / 5) * 5));
|
||||
}
|
||||
|
|
@ -91,6 +91,18 @@
|
|||
return found;
|
||||
}
|
||||
|
||||
function endpointSignalNode(endpoint, object, worldRef) {
|
||||
if (!endpoint || !object) return object || null;
|
||||
if (object.type !== "circuit_board") return object;
|
||||
const circuit = global.TarinaiCircuitBoardSystem;
|
||||
const portId = endpoint.portId || circuit?.nearestPort?.(object, endpoint.x, endpoint.y, 80)?.port?.portId || "";
|
||||
return circuit?.terminal?.(object, portId) || null;
|
||||
}
|
||||
|
||||
function isSignalConnectable(item) {
|
||||
return Boolean(item && !item.dead && (SIGNAL_SOURCE_TYPES.has(item.type) || DIRECT_SIGNAL_SINK_TYPES.has(item.type) || item.type === "circuit_board"));
|
||||
}
|
||||
|
||||
function endpointPoint(endpoint, worldRef) {
|
||||
return global.TarinaiLinkRuntime?.endpointWorld?.(endpoint, worldRef)
|
||||
|| (endpoint ? { x: num(endpoint.x), y: num(endpoint.y) } : null);
|
||||
|
|
@ -145,9 +157,10 @@
|
|||
for (const item of worldRef.itemsOfType?.("pressure_switch") || []) {
|
||||
if (!item || item.dead) continue;
|
||||
item.pressureTarget = normalizePressureTarget(item.pressureTarget);
|
||||
item.pressureComparator = normalizeComparator(item.pressureComparator);
|
||||
const defaultThreshold = PRESSURE_TARGETS[item.pressureTarget]?.defaultThreshold ?? 1;
|
||||
item.pressureThreshold = clamp(num(item.pressureThreshold, defaultThreshold), -999, 999);
|
||||
item.pressureMin = clamp(num(item.pressureMin, defaultThreshold), -999, 999);
|
||||
item.pressureMax = clamp(num(item.pressureMax, 999), -999, 999);
|
||||
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
|
||||
item.pressureWidth = clamp(item.pressureWidth || 220, 60, 840);
|
||||
item.pressureHeight = clamp(item.pressureHeight || 160, 60, 840);
|
||||
item.pressureTimeStart = normalizeMinute(item.pressureTimeStart, 360);
|
||||
|
|
@ -159,7 +172,7 @@
|
|||
value = active ? 1 : 0;
|
||||
} else {
|
||||
value = detectorMetricValue(item, worldRef);
|
||||
active = item.pressureComparator === "lte" ? value <= item.pressureThreshold : value >= item.pressureThreshold;
|
||||
active = value >= item.pressureMin && value <= item.pressureMax;
|
||||
}
|
||||
if (active !== Boolean(item.signalActive)) worldRef.drawListDirty = true;
|
||||
item.pressureCount = value;
|
||||
|
|
@ -192,30 +205,40 @@
|
|||
|
||||
function buildSignalNetwork(worldRef) {
|
||||
const adjacency = new Map();
|
||||
const connected = new Set();
|
||||
const connectedNodes = new Set();
|
||||
const wires = [];
|
||||
const circuit = global.TarinaiCircuitBoardSystem;
|
||||
let wireGeometryMoved = false;
|
||||
const addEdge = (a, b) => {
|
||||
if (!a || !b || a === b) return;
|
||||
if (!adjacency.has(a)) adjacency.set(a, new Set());
|
||||
if (!adjacency.has(b)) adjacency.set(b, new Set());
|
||||
adjacency.get(a).add(b); adjacency.get(b).add(a);
|
||||
connected.add(a); connected.add(b);
|
||||
connectedNodes.add(a); connectedNodes.add(b);
|
||||
};
|
||||
const boards = [];
|
||||
for (const item of worldRef.items || []) {
|
||||
if (!item || item.dead) continue;
|
||||
if (DIRECT_SIGNAL_SINK_TYPES.has(item.type)) {
|
||||
item._wireSignalDriven = false; item._wireSignalOn = false;
|
||||
item._directSignalDriven = false; item._directSignalOn = false;
|
||||
}
|
||||
if (item.type === "circuit_board") {
|
||||
circuit?.ensureConfig?.(item);
|
||||
item._circuitInputState = Object.create(null);
|
||||
if (!item._circuitOutputState || typeof item._circuitOutputState !== "object") circuit?.evaluate?.(item, item._circuitInputState);
|
||||
boards.push(item);
|
||||
}
|
||||
}
|
||||
for (const wire of worldRef.items || []) {
|
||||
if (!isWire(wire)) continue;
|
||||
const pb = global.TarinaiPhysicsBodySystem;
|
||||
const endpointA = pb?.endpoint?.(wire, 0);
|
||||
const endpointB = pb?.endpoint?.(wire, 1);
|
||||
const a = endpointObject(endpointA, worldRef);
|
||||
const b = endpointObject(endpointB, worldRef);
|
||||
const objectA = endpointObject(endpointA, worldRef);
|
||||
const objectB = endpointObject(endpointB, worldRef);
|
||||
const nodeA = endpointSignalNode(endpointA, objectA, worldRef);
|
||||
const nodeB = endpointSignalNode(endpointB, objectB, worldRef);
|
||||
const pointA = endpointPoint(endpointA, worldRef);
|
||||
const pointB = endpointPoint(endpointB, worldRef);
|
||||
if (pointA && pointB) {
|
||||
|
|
@ -227,39 +250,81 @@
|
|||
if (endpointA) { endpointA.x = pointA.x; endpointA.y = pointA.y; }
|
||||
if (endpointB) { endpointB.x = pointB.x; endpointB.y = pointB.y; }
|
||||
}
|
||||
wires.push({ wire, a, b });
|
||||
if (a && b) addEdge(a, b);
|
||||
wires.push({ wire, a: objectA, b: objectB, nodeA, nodeB });
|
||||
if (nodeA && nodeB) addEdge(nodeA, nodeB);
|
||||
}
|
||||
|
||||
const components = [];
|
||||
const componentOf = new Map();
|
||||
const visited = new Set();
|
||||
for (const start of connected) {
|
||||
if (visited.has(start)) continue;
|
||||
const stack = [start];
|
||||
const component = [];
|
||||
let active = false;
|
||||
for (const startNode of connectedNodes) {
|
||||
if (visited.has(startNode)) continue;
|
||||
const stack = [startNode], nodes = [];
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (!node || visited.has(node)) continue;
|
||||
visited.add(node); component.push(node);
|
||||
if (SIGNAL_SOURCE_TYPES.has(node.type) && node.signalActive) active = true;
|
||||
visited.add(node); nodes.push(node);
|
||||
for (const next of adjacency.get(node) || []) if (!visited.has(next)) stack.push(next);
|
||||
}
|
||||
const hasSource = component.some(node => SIGNAL_SOURCE_TYPES.has(node.type));
|
||||
const componentSet = new Set(component);
|
||||
for (const node of component) {
|
||||
const component = { nodes, active: false, hasSource: nodes.some(node => SIGNAL_SOURCE_TYPES.has(node.type) || (node.type === "circuit_terminal" && node.direction === "output")) };
|
||||
components.push(component);
|
||||
nodes.forEach(node => componentOf.set(node, component));
|
||||
}
|
||||
|
||||
// Board outputs can depend on other boards, so resolve the graph to a
|
||||
// fixed point. Unstable internal combinational cycles are forced OFF by the
|
||||
// board evaluator; external feedback is still bounded to avoid hanging the simulation.
|
||||
const maxCircuitPasses = Math.min(256, Math.max(16, boards.length * 2 + 4));
|
||||
for (let pass = 0; pass < maxCircuitPasses; pass += 1) {
|
||||
let changed = false;
|
||||
for (const component of components) {
|
||||
const active = component.hasSource && component.nodes.some(node => (SIGNAL_SOURCE_TYPES.has(node.type) && node.signalActive) || (node.type === "circuit_terminal" && node.direction === "output" && node.signalActive));
|
||||
if (active !== component.active) { component.active = active; changed = true; }
|
||||
}
|
||||
for (const board of boards) {
|
||||
const inputs = Object.create(null);
|
||||
for (const port of circuit?.ports?.(board) || []) {
|
||||
if (port.direction !== "input") continue;
|
||||
const terminal = circuit.terminal(board, port.portId);
|
||||
inputs[port.id] = Boolean(componentOf.get(terminal)?.active);
|
||||
}
|
||||
const before = JSON.stringify(board._circuitOutputState || {});
|
||||
circuit?.evaluate?.(board, inputs);
|
||||
if (before !== JSON.stringify(board._circuitOutputState || {})) changed = true;
|
||||
}
|
||||
if (!changed) break;
|
||||
}
|
||||
for (const component of components) {
|
||||
component.active = component.hasSource && component.nodes.some(node => (SIGNAL_SOURCE_TYPES.has(node.type) && node.signalActive) || (node.type === "circuit_terminal" && node.direction === "output" && node.signalActive));
|
||||
for (const node of component.nodes) {
|
||||
if (!DIRECT_SIGNAL_SINK_TYPES.has(node.type)) continue;
|
||||
node._wireSignalDriven = hasSource;
|
||||
node._wireSignalOn = hasSource && active;
|
||||
setEffectivePower(node, hasSource, active, worldRef);
|
||||
node._wireSignalDriven = component.hasSource;
|
||||
node._wireSignalOn = component.hasSource && component.active;
|
||||
setEffectivePower(node, component.hasSource, component.active, worldRef);
|
||||
}
|
||||
for (const entry of wires) {
|
||||
if ((entry.a && componentSet.has(entry.a)) || (entry.b && componentSet.has(entry.b))) entry.wire.signalActive = hasSource && active;
|
||||
}
|
||||
for (const entry of wires) {
|
||||
const component = componentOf.get(entry.nodeA) || componentOf.get(entry.nodeB);
|
||||
entry.wire.signalActive = Boolean(component?.hasSource && component?.active);
|
||||
if (!entry.wire.signalActive || entry.a === entry.b) continue;
|
||||
let sourceNode = null, targetNode = null, sourceBoard = null, targetBoard = null;
|
||||
if (entry.nodeA?.type === "circuit_terminal" && entry.nodeA.direction === "output"
|
||||
&& entry.nodeB?.type === "circuit_terminal" && entry.nodeB.direction === "input") {
|
||||
sourceNode = entry.nodeA; targetNode = entry.nodeB; sourceBoard = entry.a; targetBoard = entry.b;
|
||||
} else if (entry.nodeB?.type === "circuit_terminal" && entry.nodeB.direction === "output"
|
||||
&& entry.nodeA?.type === "circuit_terminal" && entry.nodeA.direction === "input") {
|
||||
sourceNode = entry.nodeB; targetNode = entry.nodeA; sourceBoard = entry.b; targetBoard = entry.a;
|
||||
}
|
||||
if (!sourceNode || !targetNode || sourceBoard === targetBoard) continue;
|
||||
const sourcePortId = String(sourceNode.portId || "").replace(/^o:/, "");
|
||||
const targetPortId = String(targetNode.portId || "").replace(/^i:/, "");
|
||||
if (!sourceBoard?._circuitOutputState?.[sourcePortId] || !targetBoard?._circuitInputState?.[targetPortId]) continue;
|
||||
global.TarinaiAchievements?.recordCircuitBoardSignalTransfer?.(sourceBoard, targetBoard, entry.wire, { world: worldRef });
|
||||
}
|
||||
|
||||
for (const item of worldRef.items || []) {
|
||||
if (!item || item.dead) continue;
|
||||
if (DIRECT_SIGNAL_SINK_TYPES.has(item.type) && !connected.has(item)) {
|
||||
if (DIRECT_SIGNAL_SINK_TYPES.has(item.type) && !componentOf.has(item)) {
|
||||
item._wireSignalDriven = false; item._wireSignalOn = false;
|
||||
setEffectivePower(item, false, false, worldRef);
|
||||
}
|
||||
|
|
@ -272,12 +337,7 @@
|
|||
return wires;
|
||||
}
|
||||
|
||||
function distanceToSegment(px, py, ax, ay, bx, by) {
|
||||
const vx = bx - ax, vy = by - ay;
|
||||
const len2 = vx * vx + vy * vy || 1;
|
||||
const t = clamp(((px - ax) * vx + (py - ay) * vy) / len2, 0, 1);
|
||||
return Math.hypot(px - (ax + vx * t), py - (ay + vy * t));
|
||||
}
|
||||
const distanceToSegment = (px, py, ax, ay, bx, by) => global.TarinaiGeometry.pointSegmentDistance(px, py, ax, ay, bx, by);
|
||||
|
||||
function shockTarget(worldRef, wire, target, now, { connected = false } = {}) {
|
||||
if (!target || target.dead) return false;
|
||||
|
|
@ -306,12 +366,14 @@
|
|||
life: 0.28,
|
||||
color: "rgba(118,224,255,0.96)",
|
||||
});
|
||||
global.TarinaiAchievements?.recordWireShock?.(wire, target, { world: worldRef, connected });
|
||||
return true;
|
||||
}
|
||||
|
||||
function damageFromWires(worldRef, wires) {
|
||||
const now = num(worldRef.time);
|
||||
for (const { wire, a, b } of wires) {
|
||||
global.TarinaiAchievements?.recordWirePowerState?.(wire, Boolean(wire.signalActive));
|
||||
if (!wire.signalActive) continue;
|
||||
// Living endpoints are directly energized even through insulated wire.
|
||||
if (a && (a.radius || a.kind === "worker" || a.kind === "queen")) shockTarget(worldRef, wire, a, now, { connected: true });
|
||||
|
|
@ -358,6 +420,9 @@
|
|||
isWire,
|
||||
isMinuteInRange,
|
||||
currentMinuteOfDay,
|
||||
isSignalConnectable,
|
||||
DIRECT_SIGNAL_SINK_TYPES,
|
||||
SIGNAL_SOURCE_TYPES,
|
||||
SENSOR_METRICS: DETECTOR_METRICS,
|
||||
DETECTOR_METRICS,
|
||||
PRESSURE_TARGETS,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
return Number.isFinite(n) ? n / scale : fallback;
|
||||
}
|
||||
|
||||
function birthHash32(seed = "") {
|
||||
const birthHash32 = global.TarinaiCoreHelpers?.birthHash32 || (seed => {
|
||||
const match = /^b[0-9a-z]+_([0-9a-z]+)$/i.exec(String(seed || ""));
|
||||
if (match) {
|
||||
const parsed = parseInt(match[1], 36);
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
});
|
||||
function compactBirthDescriptor(t = null, fallbackSerial = 1) {
|
||||
const seed = String(t?.birthSeed || t?.familyKey || t?.id || "");
|
||||
const parsed = global.TarinaiSeedFactory?.serialFromBirthSeed?.(seed) || 0;
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
const proto = Object.getPrototypeOf(value);
|
||||
return proto === Object.prototype || proto === null;
|
||||
}
|
||||
function clonePlain(value, depth = 0) {
|
||||
function cloneSnapshotPlain(value, depth = 0) {
|
||||
if (value == null) return value;
|
||||
const type = typeof value;
|
||||
if (type === "number") return Number.isFinite(value) ? value : 0;
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
if (Array.isArray(value)) {
|
||||
const out = [];
|
||||
for (const v of value) {
|
||||
const c = clonePlain(v, depth + 1);
|
||||
const c = cloneSnapshotPlain(v, depth + 1);
|
||||
if (c !== undefined) out.push(c);
|
||||
}
|
||||
return out;
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
const out = {};
|
||||
for (const [key, v] of Object.entries(value)) {
|
||||
if (DEFAULT_SKIP.has(key)) continue;
|
||||
const c = clonePlain(v, depth + 1);
|
||||
const c = cloneSnapshotPlain(v, depth + 1);
|
||||
if (c !== undefined) out[key] = c;
|
||||
}
|
||||
return out;
|
||||
|
|
@ -417,6 +417,7 @@
|
|||
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)];
|
||||
if (type === "signboard") return [item.text || ""];
|
||||
if (type === "robot_cleaner") return [Math.max(0, Math.min(0x3f, Number(item.robotCleanerMask ?? 3) | 0)), item.robotCleanerHighSpeed ? 1 : 0];
|
||||
if (type === "circuit_board") return [JSON.stringify(global.TarinaiCircuitBoardSystem?.serializeConfig?.(item) || item.circuitConfig || {})];
|
||||
if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
|
||||
const packed = global.TarinaiPhysicsBodySystem.compactItemExtra(item, tarinaiIndex, itemIndex);
|
||||
if (packed) return packed;
|
||||
|
|
@ -427,7 +428,7 @@
|
|||
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" || type === "flame_firecracker") return [q(item.fuseTimer, 10)];
|
||||
if (type === "sticky_bomb") return [q(item.fuseTimer, 10), tarinaiIndex.get(item.stickyBombCarrierId) ?? -1];
|
||||
if (type === "sticky_bomb") return [q(item.fuseTimer, 10), tarinaiIndex.get(item.stickyBombCarrierId) ?? -1, q(item.stickyBombPassCount, 1)];
|
||||
if (type === "fire") return [q(item.flameLife, 10)];
|
||||
if (type === "nest_box") return [q(item.burnTimer, 10), q(item.nestFireEvacuationTimer, 10), item.burning ? 1 : 0, item.nestFireEvacuated ? 1 : 0];
|
||||
if (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)];
|
||||
|
|
@ -444,8 +445,8 @@
|
|||
const targets = ["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"];
|
||||
return [
|
||||
Math.max(0, targets.indexOf(item.pressureTarget || "tarinai")),
|
||||
item.pressureComparator === "lte" ? 1 : 0,
|
||||
q(item.pressureThreshold ?? 1, 10),
|
||||
q(item.pressureMin ?? 1, 10),
|
||||
q(item.pressureMax ?? 300, 10),
|
||||
Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)),
|
||||
Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0)),
|
||||
Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0)),
|
||||
|
|
@ -483,8 +484,8 @@
|
|||
const def = global.StructureRegistry?.get?.(type);
|
||||
if (def) {
|
||||
item.label = def.label;
|
||||
item.roles = clonePlain(def.roles) || item.roles || {};
|
||||
item.needEffects = clonePlain(def.needEffects) || item.needEffects || {};
|
||||
item.roles = cloneSnapshotPlain(def.roles) || item.roles || {};
|
||||
item.needEffects = cloneSnapshotPlain(def.needEffects) || item.needEffects || {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -516,7 +517,7 @@
|
|||
} 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 === "sticky_bomb") {
|
||||
item.fuseTimer = u(extra[0], 10, item.fuseTimer || 12); item.fuseMax = Math.max(item.fuseMax || 12, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || "";
|
||||
item.fuseTimer = u(extra[0], 10, item.fuseTimer || 12); item.fuseMax = Math.max(item.fuseMax || 12, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || ""; item.stickyBombPassCount = Math.max(0, u(extra[2], 1, 0));
|
||||
} else if (type === "fire") {
|
||||
item.flameLife = u(extra[0], 10, item.flameLife || 6);
|
||||
} else if (type === "nest_box") {
|
||||
|
|
@ -530,6 +531,11 @@
|
|||
}
|
||||
} else if (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 (type === "circuit_board") {
|
||||
let config = {};
|
||||
try { config = JSON.parse(String(extra[0] || "{}")); } catch (_) { config = {}; }
|
||||
global.TarinaiCircuitBoardSystem?.applySerializedConfig?.(item, config);
|
||||
global.TarinaiCircuitBoardSystem?.evaluate?.(item);
|
||||
} else if (type === "fan") {
|
||||
const fallback = global.defaultItemAngle(type);
|
||||
item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback);
|
||||
|
|
@ -542,8 +548,9 @@
|
|||
} else if (type === "pressure_switch") {
|
||||
const targets = ["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"];
|
||||
item.pressureTarget = targets[Number(extra[0]) | 0] || "tarinai";
|
||||
item.pressureComparator = Number(extra[1]) === 1 ? "lte" : "gte";
|
||||
item.pressureThreshold = u(extra[2], 10, 1);
|
||||
item.pressureMin = u(extra[1], 10, 1);
|
||||
item.pressureMax = u(extra[2], 10, 300);
|
||||
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(extra[3] || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(extra[4] || 160) | 0));
|
||||
item.pressureTimeStart = Math.max(0, Math.min(1435, Number(extra[5] ?? 360) | 0));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
function clonePlain(value) {
|
||||
return JSON.parse(JSON.stringify(value || {}));
|
||||
}
|
||||
const clonePlain = global.TarinaiCoreHelpers?.clonePlain || (value => JSON.parse(JSON.stringify(value || {})));
|
||||
|
||||
class TarinaiStructure {
|
||||
constructor(definition, owner, x, y, world) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
{ id: "items_and_ants", run(worldRef, ctx) { ctx.mobile = phase("updateItemsAndAnts")?.(worldRef, ctx.dt) || null; } },
|
||||
{ id: "effects", run(worldRef, ctx) { phase("updateEffects")?.(worldRef, ctx.dt); } },
|
||||
{ id: "creatures", run(worldRef, ctx) { phase("updateCreatures")?.(worldRef, ctx.dt); } },
|
||||
{ id: "seesaws", run(worldRef, ctx) { global.TarinaiSeesawSystem?.updateWorld?.(worldRef, ctx.dt); } },
|
||||
{ id: "maintenance", run(worldRef, ctx) { phase("runMaintenance")?.(worldRef, ctx.dt, ctx.mobile?.itemCountBefore ?? (worldRef.items || []).length); } },
|
||||
{ id: "phase_events", run(worldRef) { phase("emitPhaseChange")?.(worldRef); } },
|
||||
{ id: "ambient_tarinai", run(worldRef, ctx) { phase("spawnAmbientTarinai")?.(worldRef, ctx.dt); } },
|
||||
|
|
|
|||
|
|
@ -535,6 +535,22 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
createApproachMateActionSpec(),
|
||||
createFightRivalActionSpec(),
|
||||
createIntimidateEnemyActionSpec(),
|
||||
{
|
||||
id: "play_seesaw",
|
||||
need: "fulfill",
|
||||
subNeed: "social_play",
|
||||
state: "play_seesaw",
|
||||
label: "\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u307c\u3046\u3068\u3057\u3066\u3044\u308b",
|
||||
phrase: "\u4e00\u7dd2\u306b\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u3093\u3060",
|
||||
weight: 31,
|
||||
timerBacked: true,
|
||||
selectTarget(t, world) { return globalThis.TarinaiSeesawSystem?.findAvailable?.(world, t, 620)?.item || null; },
|
||||
canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||
start(t, world, ctx = {}) { return Boolean(globalThis.TarinaiSeesawSystem?.start?.(t, world, ctx.target ?? this.selectTarget(t, world, ctx))); },
|
||||
tick(t, world) { return globalThis.TarinaiSeesawSystem?.tickAction?.(t, world) ?? false; },
|
||||
finish(t) { globalThis.TarinaiSeesawSystem?.release?.(t); return true; },
|
||||
fail(t) { globalThis.TarinaiSeesawSystem?.release?.(t); return false; },
|
||||
},
|
||||
{
|
||||
id: "play",
|
||||
need: "fulfill",
|
||||
|
|
|
|||
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
(function (global) {
|
||||
const registry = new Map();
|
||||
|
||||
function isObject(value) {
|
||||
return Boolean(value && typeof value === "object");
|
||||
}
|
||||
|
||||
function finiteOr(value, fallback) {
|
||||
const isObject = global.TarinaiCoreHelpers?.isPlainObject || (value => Boolean(value && typeof value === "object"));
|
||||
const finiteOr = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback) => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function hasOwnFn(obj, key) {
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
(function (global) {
|
||||
const Tarinai = global.Tarinai;
|
||||
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_behavior_state.js");
|
||||
|
||||
function isObject(value) {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function finiteOr(value, fallback) {
|
||||
const isObject = global.TarinaiCoreHelpers?.isPlainObject || (value => Boolean(value && typeof value === "object" && !Array.isArray(value)));
|
||||
const finiteOr = global.TarinaiCoreHelpers?.finiteOr || ((value, fallback) => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
});
|
||||
|
||||
function nowFor(tarinai = null) {
|
||||
return finiteOr(tarinai?.world?.time, 0);
|
||||
|
|
@ -82,9 +78,7 @@
|
|||
return String(behavior.forcedRequest?.source || behavior.data?.forcedSource || behavior.sourceDetail || "");
|
||||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
}
|
||||
const isEffectCauseText = global.TarinaiCoreHelpers?.isEffectCauseText || (text => /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, "")));
|
||||
|
||||
function normalizeBehaviorValue(input, tarinai = null, previous = null) {
|
||||
if (!isObject(input)) return null;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ function fulfillPrimaryReason(tarinai, action = null) {
|
|||
if (actionId === "build_plushie") return "\u306c\u3044\u3050\u308b\u307f\u304c\u307b\u3057\u3044";
|
||||
if (actionId === "return_owned_structure" || actionId === "hide_at_owned_structure") return "\u81ea\u5206\u306e\u5834\u6240\u304c\u6c17\u306b\u306a\u308b";
|
||||
if (actionId === "use_plushie") return "\u306c\u3044\u3050\u308b\u307f\u3092\u62b1\u3048\u305f\u3044";
|
||||
if (actionId === "play_seesaw") return "\u904a\u3073\u305f\u3044";
|
||||
if (actionId === "play") return "\u9000\u5c48\u3057\u3066\u3044\u308b";
|
||||
if (actionId === "wander_lightly") return "\u5c11\u3057\u9000\u5c48\u3057\u3066\u3044\u308b";
|
||||
|
||||
|
|
@ -50,9 +51,8 @@ function actionTextForReason(action = null, tarinai = null) {
|
|||
}
|
||||
|
||||
function isEffectCauseText(text = "") {
|
||||
return typeof globalThis.isTarinaiEffectCauseText === "function"
|
||||
? globalThis.isTarinaiEffectCauseText(text)
|
||||
: /\u306E\u52B9\u679C$/.test(String(text || "").trim().replace(/[\u3002\uFF01\uFF1F]+$/, ""));
|
||||
return Boolean(globalThis.TarinaiCoreHelpers?.isEffectCauseText?.(text)
|
||||
?? globalThis.isTarinaiEffectCauseText?.(text));
|
||||
}
|
||||
|
||||
function forcedCausePhrase(source = "", fallback = "") {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
(function (global) {
|
||||
function updateCandidate(tarinai, ctx, it, dt = 0) {
|
||||
if (!tarinai || tarinai.dead || !ctx || !it || it.dead) return false;
|
||||
if (it.type === "duplicator" && global.TarinaiSignalSystem?.powerOverride?.(it) === false) return false;
|
||||
const { foodNeed = 0, foodActionActive = false, canBite = false, canSweetBite = false, canZunchiBite = false } = ctx;
|
||||
|
||||
const contact = global.TarinaiItemInteractionContext.contactForCandidate(tarinai, ctx, it, { role: "food" });
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
isServingFoodItem(it) {
|
||||
if (it?.type === "duplicator" && it.storedFoodType) return true;
|
||||
if (it?.type === "duplicator" && it.storedFoodType) return global.TarinaiSignalSystem?.powerOverride?.(it) !== false;
|
||||
return Boolean(it && isServingFoodType(it.type));
|
||||
},
|
||||
foodItemHasServingLeft(it) {
|
||||
if (!it || it.dead) return false;
|
||||
if (it.type === "duplicator") return Boolean(it.storedFoodType);
|
||||
if (it.type === "duplicator") return global.TarinaiSignalSystem?.powerOverride?.(it) !== false && Boolean(it.storedFoodType);
|
||||
if (!this.isServingFoodItem(it)) return (it.amount || 0) > 1.2;
|
||||
this.ensureFoodServings(it);
|
||||
return (it.foodServingsRemaining || 0) > 0;
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
consumeFoodServing(it, nutrition) {
|
||||
if (it?.type === "duplicator") {
|
||||
if (!it.storedFoodType) return 0;
|
||||
if (!it.storedFoodType || global.TarinaiSignalSystem?.powerOverride?.(it) === false) return 0;
|
||||
const value = Number.isFinite(nutrition) ? nutrition : (window.TarinaiItemRegistry.food?.nutrition?.(it.storedFoodType, 0) ?? 42);
|
||||
it.usedCount = (it.usedCount || 0) + 1;
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function effectiveFoodRoleMatches(item, role) {
|
|||
function roleMatches(item, role) {
|
||||
if (!item || item.dead) return false;
|
||||
if (item.amount != null && item.amount <= 0 && item.type !== "duplicator") return false;
|
||||
if (item.type === "duplicator") return effectiveFoodRoleMatches(item, role);
|
||||
if (item.type === "duplicator") return globalThis.TarinaiSignalSystem?.powerOverride?.(item) !== false && effectiveFoodRoleMatches(item, role);
|
||||
if (item.roles?.[role]) return true;
|
||||
if (role === "food" || role === "drink" || role === "medicine") return effectiveFoodRoleMatches(item, role);
|
||||
if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box" || item.type === "pipe" || item.type === "grass_bed";
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ const TARINAI_ACTION_PRIORITY = {
|
|||
build_plushie: 28,
|
||||
use_plushie: 24,
|
||||
return_owned_structure: 22,
|
||||
play_seesaw: 27,
|
||||
play: 20,
|
||||
wander_lightly: 10,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ function actionSelectionBonus(action, need, tarinai, options = {}) {
|
|||
const p = tarinai?.fulfillReasonParts || {};
|
||||
if (action.id === "build_grass_bed") bonus += Number(p.bed || 0) * 1.6 + Number(p.material || 0);
|
||||
else if (action.id === "build_plushie") bonus += Number(p.plushie || 0) * 1.8 + Number(p.material || 0);
|
||||
else if (action.id === "play" || action.id === "wander_lightly") bonus += Number(p.boredom || 0) * 1.2;
|
||||
else if (action.id === "play" || action.id === "play_seesaw" || action.id === "wander_lightly") bonus += Number(p.boredom || 0) * 1.2;
|
||||
else if (options.includeComfortActions && (action.id === "return_owned_structure" || action.id === "use_plushie")) {
|
||||
bonus += Math.max(0, -Number(p.bedComfort || 0) - Number(p.plushieComfort || 0)) * 0.5;
|
||||
}
|
||||
|
|
@ -311,6 +311,9 @@ function shouldStartActionBySubNeed(tarinai, action, needs) {
|
|||
const status = tarinai?.temperatureStatus || tarinai?.world?.temperatureStatusFor?.(tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15), tarinai);
|
||||
return Boolean(status && (status.discomfort || 0) >= 2.5);
|
||||
}
|
||||
if (action?.id === "play_seesaw") {
|
||||
return Boolean(globalThis.TarinaiSeesawSystem?.shouldSeek?.(tarinai?.world, tarinai, needs));
|
||||
}
|
||||
if (need !== "social" || !action?.subNeed) return shouldStartNeedAction(tarinai, need, needs);
|
||||
const parts = tarinai?.socialReasonParts || {};
|
||||
const subNeed = action.subNeed;
|
||||
|
|
@ -376,6 +379,9 @@ function choosePriorityNeedAction(needs, tarinai, world) {
|
|||
const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null;
|
||||
priority = (status?.harmful || (status?.harmExcess || 0) > 0) ? 92 : 53;
|
||||
}
|
||||
if (action.id === "play_seesaw") {
|
||||
priority = Math.max(priority, Number(globalThis.TarinaiSeesawSystem?.actionPriority?.(world, tarinai)) || 0);
|
||||
}
|
||||
if (action.id === "eat_food") {
|
||||
const hunger = Number(tarinai?.hunger || 0) || 0;
|
||||
if (hunger >= 98 || needValue >= 88) priority = Math.max(priority, 120);
|
||||
|
|
@ -495,6 +501,7 @@ function continueCurrentBehavior(tarinai, world, needs, dt) {
|
|||
|| (action.id === "panic_escape" && (tarinai.fearTimer || 0) > 0.04 && tarinai.state === "panic")
|
||||
|| (action.id === "intimidate_enemy" && (tarinai.intimidateTimer || 0) > 0.04)
|
||||
|| (action.id === "sunbath" && (tarinai.sunbathTimer || 0) > 0.04)
|
||||
|| (action.id === "play_seesaw" && Boolean(globalThis.TarinaiSeesawSystem?.validReservation?.(tarinai, world)))
|
||||
|| ((action.id === "eat_food" || action.id === "drink_water" || action.id === "use_medicine") && (tarinai.eatTimer || 0) > 0.04 && elapsed < 1.15);
|
||||
const foodSuppressed = need === "food" && !isCurrentBehaviorForced(tarinai) && (tarinai.mealCooldownUntil || 0) > now && (tarinai.hunger || 0) < 82 && value < 78 && elapsed >= Math.min(minDuration, 1.15);
|
||||
if (!foodSuppressed && (elapsed < minDuration || value >= needThreshold(need, "continue") || mustContinueByTimer || isCurrentBehaviorForced(tarinai))) {
|
||||
|
|
|
|||
|
|
@ -473,8 +473,8 @@
|
|||
}
|
||||
|
||||
if (this.target && !this.target.dead) {
|
||||
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
|
||||
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "ant_attack", "seek_temperature"]);
|
||||
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : (this.state === "play_seesaw" ? (globalThis.TarinaiSeesawSystem?.seatWorld?.(this.target, this.seesawSeatIndex) || this.target) : this.target);
|
||||
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "play_seesaw", "ant_attack", "seek_temperature"]);
|
||||
const routeTargetItem = this.target?.shelterItem || this.target?.hostItem || this.target;
|
||||
const pathPadding = Math.max(18, (this.radius || 20) * 0.82);
|
||||
const targetFoodType = this.target?.type === "duplicator" ? this.target?.storedFoodType : this.target?.type;
|
||||
|
|
@ -555,6 +555,9 @@
|
|||
} else if (this.state === "play_ball") {
|
||||
strength = d < this.radius + 18 ? 1.75 : 1.28;
|
||||
sign = 1;
|
||||
} else if (this.state === "play_seesaw") {
|
||||
strength = d < this.radius + 12 ? 0.42 : 1.18;
|
||||
sign = 1;
|
||||
} else if (this.state === "fight") {
|
||||
const headbutt = Math.sin(this.world.time * 18 + this.age * 0.37);
|
||||
if (d < 18) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
if (/birth_ritual/i.test(id) || state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) {
|
||||
lane = "birth";
|
||||
interval = isVisible ? 0.12 : 0.24;
|
||||
} else if (/approach_parent_or_child|follow_parent|approach_mate|seek_bed|play_ball|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball/.test(state)) {
|
||||
} else if (/approach_parent_or_child|follow_parent|approach_mate|seek_bed|play_ball|play_seesaw|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball|play_seesaw/.test(state)) {
|
||||
lane = "moving";
|
||||
interval = isVisible ? 0.10 : (isNear ? 0.22 : 0.55);
|
||||
} else if (/eat_food|eat/i.test(id) || state === "eat" || (tarinai.eatTimer || 0) > 0.04) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function colonyMoodTip() {
|
||||
const mood = world?.evaluateColonyMood?.(true) || world?.colonyMood || null;
|
||||
const mood = world?.evaluateColonyMood?.() || world?.colonyMood || null;
|
||||
return String(mood?.description || "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
|
||||
|
||||
function lineageNodeSort(a, b) {
|
||||
return (a.generation || 1) - (b.generation || 1) ||
|
||||
(a.birthTime || 0) - (b.birthTime || 0) ||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
|
||||
|
||||
function lineageSplitByActualEdges(component) {
|
||||
const family = lineageEdgeFamily(world.family || {});
|
||||
const nodes = Array.from(new Map(lineageCleanComponent(component, family).filter(Boolean).map(n => [n.id, n])).values());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
|
||||
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const perfScale = window.TarinaiPerf.dprScale();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
|
||||
|
||||
function loadLogPushSettings() {
|
||||
try {
|
||||
const raw = localStorage.getItem("tarinai_log_push_settings_v2");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const escapeHtml = window.TarinaiUIHelpers.htmlEscape;
|
||||
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
|
||||
|
||||
function quoteNameInRecordText(text, name) {
|
||||
const raw = String(text || "");
|
||||
|
|
@ -334,6 +334,7 @@ function isIdleDisplayText(value = "") {
|
|||
function reasonLabel(t) {
|
||||
if (!t) return "\u306a\u3057";
|
||||
if (Number(t.electricShockUntil || 0) > Number(t.world?.time || world?.time || 0)) return "\u611f\u96fb\u3057\u3066\u3044\u308b\u3002";
|
||||
if (t.state === "play_seesaw" && globalThis.TarinaiSeesawSystem?.isPlaying?.(t, t.world || world)) return "\u30b7\u30fc\u30bd\u30fc\u3092\u3057\u3066\u3044\u308b\u3002";
|
||||
const behavior = behaviorText(t);
|
||||
if (behavior) {
|
||||
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(behavior, t.state || "") || behavior;
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ function renderToolPalette() {
|
|||
if (!def || def.simulationOnly || toolId === "observe") continue;
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "tool";
|
||||
const hasPhysicsIcon = category.id === "physics" && attachPhysicsToolIcon(btn, toolId);
|
||||
const hasPhysicsIcon = attachPhysicsToolIcon(btn, toolId);
|
||||
const hasTextIcon = !hasPhysicsIcon && applyToolTextIcon(btn, def, toolId);
|
||||
if (!hasPhysicsIcon && !hasTextIcon) applyToolImageIcon(btn, def);
|
||||
btn.dataset.tool = toolId;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.16.60";
|
||||
const APP_BUILD = "tarinai-ui-v39-16-60";
|
||||
const APP_VERSION = "39.16.74";
|
||||
const APP_BUILD = "tarinai-ui-v39-16-74";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ class World { constructor() {
|
|||
this.liveIdSerial = 0;
|
||||
this.liveTarinai = new Map();
|
||||
this.lastBirthAt = -999;
|
||||
this.lastColonyMoodPopulation = NaN;
|
||||
this.nextColonyMoodEvalAt = 0;
|
||||
this.maxGeneration = 1;
|
||||
this.family = {};
|
||||
this.familyVersion = 0;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@
|
|||
return dialog;
|
||||
}
|
||||
|
||||
function bindEditorClose(dialog, close, selectors = []) {
|
||||
if (!dialog || typeof close !== "function") return;
|
||||
for (const selector of selectors) {
|
||||
const button = dialog.querySelector(selector);
|
||||
if (button) button.onclick = close;
|
||||
}
|
||||
dialog.onclick = (event) => { if (event.target === dialog) close(); };
|
||||
}
|
||||
|
||||
function ensureSignboardEditor() {
|
||||
return ensureEditorDialog("signboardEditor", "signboard-editor hidden", `
|
||||
<div class="signboard-editor-panel" role="dialog" aria-modal="true" aria-labelledby="signboardEditorTitle">
|
||||
|
|
@ -139,9 +148,7 @@
|
|||
}
|
||||
if (apply) apply.onclick = () => commit(area?.value || "");
|
||||
if (clear) clear.onclick = () => commit("");
|
||||
if (cancel) cancel.onclick = close;
|
||||
if (closeBtn) closeBtn.onclick = close;
|
||||
dialog.onclick = (e) => { if (e.target === dialog) close(); };
|
||||
bindEditorClose(dialog, close, ["#signboardEditorCancel", "#signboardEditorClose"]);
|
||||
dialog.classList.remove("hidden");
|
||||
syncCount();
|
||||
return true;
|
||||
|
|
@ -211,11 +218,9 @@
|
|||
};
|
||||
writeMask(api?.robotCleanerMask?.(robot) ?? robot.robotCleanerMask ?? defaultMask);
|
||||
if (highSpeed) highSpeed.checked = Boolean(api?.robotCleanerHighSpeed?.(robot) ?? robot.robotCleanerHighSpeed);
|
||||
if (closeBtn) closeBtn.onclick = close;
|
||||
if (cancel) cancel.onclick = close;
|
||||
bindEditorClose(dialog, close, ["#robotCleanerEditorClose", "#robotCleanerCancel"]);
|
||||
if (def) def.onclick = () => { writeMask(defaultMask); if (highSpeed) highSpeed.checked = false; };
|
||||
if (apply) apply.onclick = () => commit(readMask());
|
||||
dialog.onclick = (e) => { if (e.target === dialog) close(); };
|
||||
dialog.classList.remove("hidden");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -310,9 +315,7 @@
|
|||
};
|
||||
if (stop) stop.onclick = () => commit(true);
|
||||
if (apply) apply.onclick = () => commit(false);
|
||||
if (cancel) cancel.onclick = close;
|
||||
if (closeBtn) closeBtn.onclick = close;
|
||||
dialog.onclick = (e) => { if (e.target === dialog) close(); };
|
||||
bindEditorClose(dialog, close, ["#fanEditorCancel", "#fanEditorClose"]);
|
||||
dialog.classList.remove("hidden");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -445,9 +448,7 @@
|
|||
global.render?.();
|
||||
close();
|
||||
};
|
||||
if (cancel) cancel.onclick = close;
|
||||
if (closeBtn) closeBtn.onclick = close;
|
||||
dialog.onclick = (e) => { if (e.target === dialog) close(); };
|
||||
bindEditorClose(dialog, close, ["#rotatorCancel", "#rotatorEditorClose"]);
|
||||
controller.draw();
|
||||
dialog.classList.remove("hidden");
|
||||
return true;
|
||||
|
|
@ -591,9 +592,7 @@
|
|||
global.render?.();
|
||||
close();
|
||||
};
|
||||
if (cancel) cancel.onclick = close;
|
||||
if (closeBtn) closeBtn.onclick = close;
|
||||
dialog.onclick = (e) => { if (e.target === dialog) close(); };
|
||||
bindEditorClose(dialog, close, ["#reciprocatorCancel", "#reciprocatorEditorClose"]);
|
||||
controller.draw();
|
||||
dialog.classList.remove("hidden");
|
||||
return true;
|
||||
|
|
@ -659,13 +658,14 @@
|
|||
fuseMax: Number.isFinite(Number(item.fuseMax)) ? Number(item.fuseMax) : null,
|
||||
};
|
||||
if (item.type === "signboard") data.text = item.text || "";
|
||||
if (item.type === "circuit_board") data.circuitConfig = global.TarinaiCircuitBoardSystem?.serializeConfig?.(item) || null;
|
||||
if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen);
|
||||
if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; }
|
||||
if (item.type === "robot_cleaner") { data.robotCleanerMask = Number(item.robotCleanerMask ?? 3) | 0; data.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); }
|
||||
if (item.type === "pressure_switch") {
|
||||
data.pressureTarget = item.pressureTarget || "tarinai";
|
||||
data.pressureComparator = item.pressureComparator === "lte" ? "lte" : "gte";
|
||||
data.pressureThreshold = Math.max(-999, Math.min(999, Number(item.pressureThreshold ?? 1) || 0));
|
||||
data.pressureMin = Math.max(-999, Math.min(999, Number(item.pressureMin ?? 1) || 0));
|
||||
data.pressureMax = Math.max(-999, Math.min(999, Number(item.pressureMax ?? 300) || 0));
|
||||
data.pressureWidth = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0));
|
||||
data.pressureHeight = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0));
|
||||
data.pressureTimeStart = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0));
|
||||
|
|
@ -725,6 +725,7 @@
|
|||
item.fanBodyAngle = Number(data.fanBodyAngle ?? item.fanBodyAngle ?? item.angle ?? 0) || 0;
|
||||
}
|
||||
if (item.type === "signboard") item.text = String(data.text || "");
|
||||
if (item.type === "circuit_board") global.TarinaiCircuitBoardSystem?.applySerializedConfig?.(item, data.circuitConfig || {});
|
||||
if (item.type === "gate_fence") item.gateOpen = Boolean(data.gateOpen);
|
||||
if (item.type === "duplicator") {
|
||||
item.storedFoodType = String(data.storedFoodType || "");
|
||||
|
|
@ -734,8 +735,9 @@
|
|||
if (item.type === "pressure_switch") {
|
||||
const targets = new Set(["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"]);
|
||||
item.pressureTarget = targets.has(data.pressureTarget) ? data.pressureTarget : "tarinai";
|
||||
item.pressureComparator = data.pressureComparator === "lte" ? "lte" : "gte";
|
||||
item.pressureThreshold = Math.max(-999, Math.min(999, Number(data.pressureThreshold ?? 1) || 0));
|
||||
item.pressureMin = Math.max(-999, Math.min(999, Number(data.pressureMin ?? 1) || 0));
|
||||
item.pressureMax = Math.max(-999, Math.min(999, Number(data.pressureMax ?? 300) || 0));
|
||||
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(data.pressureWidth || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(data.pressureHeight || 160) | 0));
|
||||
item.pressureTimeStart = Math.max(0, Math.min(1435, Number(data.pressureTimeStart ?? 360) | 0));
|
||||
|
|
@ -835,10 +837,17 @@
|
|||
<div class="rotator-editor-titlebar"><h2 id="signalItemEditorTitle">\u691c\u77e5\u5668\u8a2d\u5b9a</h2><button id="signalItemEditorClose" class="rotator-editor-close" type="button">\u00d7</button></div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>\u691c\u77e5\u5bfe\u8c61 <select id="pressureSwitchTarget">${options}</select></label>
|
||||
<label id="pressureSwitchComparatorRow">\u6bd4\u8f03 <select id="pressureSwitchComparator"><option value="gte">\u4ee5\u4e0a</option><option value="lte">\u4ee5\u4e0b</option></select></label>
|
||||
<div id="pressureSwitchCountRow" class="pressure-numeric-control">
|
||||
<div class="pressure-time-summary"><span>\u3057\u304d\u3044\u5024</span><output id="pressureSwitchCountLabel">1</output></div>
|
||||
<input id="pressureSwitchCount" class="pressure-value-slider" type="range" min="0" max="300" step="1">
|
||||
<div id="pressureSwitchCountRow" class="pressure-time-control">
|
||||
<div class="pressure-time-summary"><span>\u4f5c\u52d5\u7bc4\u56f2</span><span><output id="pressureSwitchValueMinLabel">1</output><span aria-hidden="true"> \u301c </span><output id="pressureSwitchValueMaxLabel">300</output></span></div>
|
||||
<div id="pressureSwitchValueRange" class="pressure-time-range">
|
||||
<div id="pressureSwitchValueTrack" class="pressure-time-track" aria-hidden="true"></div>
|
||||
<input id="pressureSwitchValueMin" class="pressure-time-thumb pressure-time-thumb-start" type="range" min="0" max="300" step="1" aria-label="\u4f5c\u52d5\u7bc4\u56f2\u306e\u4e0b\u9650">
|
||||
<input id="pressureSwitchValueMax" class="pressure-time-thumb pressure-time-thumb-end" type="range" min="0" max="300" step="1" aria-label="\u4f5c\u52d5\u7bc4\u56f2\u306e\u4e0a\u9650">
|
||||
</div>
|
||||
<div class="pressure-number-inputs">
|
||||
<label>\u4e0b\u9650 <input id="pressureSwitchValueMinNumber" type="number" inputmode="decimal"></label>
|
||||
<label>\u4e0a\u9650 <input id="pressureSwitchValueMaxNumber" type="number" inputmode="decimal"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pressureSwitchWidthRow" class="pressure-numeric-control pressure-spatial-control">
|
||||
<div class="pressure-time-summary"><span>\u6a2a\u5e45</span><output id="pressureSwitchWidthLabel">220px</output></div>
|
||||
|
|
@ -865,15 +874,19 @@
|
|||
if (!item || item.type !== "pressure_switch") return false;
|
||||
const dialog = ensureSignalItemEditor();
|
||||
const target = dialog.querySelector("#pressureSwitchTarget");
|
||||
const comparator = dialog.querySelector("#pressureSwitchComparator");
|
||||
const count = dialog.querySelector("#pressureSwitchCount");
|
||||
const countLabel = dialog.querySelector("#pressureSwitchCountLabel");
|
||||
const valueRow = dialog.querySelector("#pressureSwitchCountRow");
|
||||
const valueRange = dialog.querySelector("#pressureSwitchValueRange");
|
||||
const valueTrack = dialog.querySelector("#pressureSwitchValueTrack");
|
||||
const valueMin = dialog.querySelector("#pressureSwitchValueMin");
|
||||
const valueMax = dialog.querySelector("#pressureSwitchValueMax");
|
||||
const valueMinNumber = dialog.querySelector("#pressureSwitchValueMinNumber");
|
||||
const valueMaxNumber = dialog.querySelector("#pressureSwitchValueMaxNumber");
|
||||
const valueMinLabel = dialog.querySelector("#pressureSwitchValueMinLabel");
|
||||
const valueMaxLabel = dialog.querySelector("#pressureSwitchValueMaxLabel");
|
||||
const width = dialog.querySelector("#pressureSwitchWidth");
|
||||
const widthLabel = dialog.querySelector("#pressureSwitchWidthLabel");
|
||||
const height = dialog.querySelector("#pressureSwitchHeight");
|
||||
const heightLabel = dialog.querySelector("#pressureSwitchHeightLabel");
|
||||
const comparatorRow = dialog.querySelector("#pressureSwitchComparatorRow");
|
||||
const countRow = dialog.querySelector("#pressureSwitchCountRow");
|
||||
const widthRow = dialog.querySelector("#pressureSwitchWidthRow");
|
||||
const heightRow = dialog.querySelector("#pressureSwitchHeightRow");
|
||||
const timeRow = dialog.querySelector("#pressureSwitchTimeRow");
|
||||
|
|
@ -885,13 +898,24 @@
|
|||
const timeEndLabel = dialog.querySelector("#pressureSwitchTimeEndLabel");
|
||||
const normalizeMinute = (value, fallback) => Math.max(0, Math.min(1435, Math.round((Number.isFinite(Number(value)) ? Number(value) : fallback) / 5) * 5));
|
||||
const formatMinute = value => { const minute = normalizeMinute(value, 0); return `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`; };
|
||||
const targetDef = () => DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
|
||||
const normalizeValue = (value, fallback, def = targetDef()) => {
|
||||
const min = Number(def.min ?? 0), max = Number(def.max ?? 100), step = Math.max(0.0001, Number(def.step ?? 1));
|
||||
const numeric = Number.isFinite(Number(value)) ? Number(value) : fallback;
|
||||
const rounded = Math.round((numeric - min) / step) * step + min;
|
||||
return Math.max(min, Math.min(max, Number(rounded.toFixed(6))));
|
||||
};
|
||||
const formatDetectorValue = (value, def) => `${Number(value) || 0}${def?.unit || ""}`;
|
||||
|
||||
target.value = DETECTOR_TARGETS[item.pressureTarget] ? item.pressureTarget : "tarinai";
|
||||
comparator.value = item.pressureComparator === "lte" ? "lte" : "gte";
|
||||
count.value = String(Number(item.pressureThreshold ?? DETECTOR_TARGETS[target.value].threshold));
|
||||
const initialDef = targetDef();
|
||||
valueMin.value = String(normalizeValue(item.pressureMin, initialDef.threshold, initialDef));
|
||||
valueMax.value = String(normalizeValue(item.pressureMax, initialDef.max, initialDef));
|
||||
width.value = String(item.pressureWidth || 220);
|
||||
height.value = String(item.pressureHeight || 160);
|
||||
timeStart.value = String(normalizeMinute(item.pressureTimeStart, 360));
|
||||
timeEnd.value = String(normalizeMinute(item.pressureTimeEnd, 1080));
|
||||
|
||||
const updateTimeRange = () => {
|
||||
const start = normalizeMinute(timeStart.value, 360), end = normalizeMinute(timeEnd.value, 1080);
|
||||
timeStart.value = String(start); timeEnd.value = String(end);
|
||||
|
|
@ -903,8 +927,25 @@
|
|||
: `linear-gradient(to right, ${active} 0%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} 100%)`;
|
||||
if (timeRange) timeRange.dataset.wraps = start > end ? "1" : "0";
|
||||
};
|
||||
const updateValueRange = source => {
|
||||
const def = targetDef();
|
||||
let low = normalizeValue(source === "min-number" ? valueMinNumber.value : valueMin.value, def.threshold, def);
|
||||
let high = normalizeValue(source === "max-number" ? valueMaxNumber.value : valueMax.value, def.max, def);
|
||||
if (low > high) {
|
||||
if (source === "min" || source === "min-number") high = low;
|
||||
else low = high;
|
||||
}
|
||||
valueMin.value = String(low); valueMax.value = String(high);
|
||||
valueMinNumber.value = String(low); valueMaxNumber.value = String(high);
|
||||
valueMinLabel.textContent = formatDetectorValue(low, def); valueMaxLabel.textContent = formatDetectorValue(high, def);
|
||||
const span = Math.max(0.0001, Number(def.max) - Number(def.min));
|
||||
const lowPct = (low - Number(def.min)) / span * 100, highPct = (high - Number(def.min)) / span * 100;
|
||||
const idle = "rgba(94,112,128,0.22)", active = "rgba(67,158,218,0.78)";
|
||||
valueTrack.style.background = `linear-gradient(to right, ${idle} 0%, ${idle} ${lowPct}%, ${active} ${lowPct}%, ${active} ${highPct}%, ${idle} ${highPct}%, ${idle} 100%)`;
|
||||
valueRange.dataset.wraps = "0";
|
||||
};
|
||||
const updateOverlay = () => {
|
||||
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
|
||||
const def = targetDef();
|
||||
if (!def.spatial) {
|
||||
if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null;
|
||||
} else {
|
||||
|
|
@ -912,13 +953,7 @@
|
|||
}
|
||||
global.render?.();
|
||||
};
|
||||
const formatDetectorValue = (value, def) => `${Number(value) || 0}${def?.unit || ""}`;
|
||||
const updateNumericDisplays = () => {
|
||||
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
|
||||
const threshold = Math.max(Number(count.min), Math.min(Number(count.max), Number(count.value) || 0));
|
||||
count.value = String(threshold);
|
||||
countLabel.textContent = formatDetectorValue(threshold, def);
|
||||
count.setAttribute("aria-valuetext", countLabel.textContent);
|
||||
widthLabel.textContent = `${Number(width.value) || 0}px`;
|
||||
heightLabel.textContent = `${Number(height.value) || 0}px`;
|
||||
width.setAttribute("aria-valuetext", widthLabel.textContent);
|
||||
|
|
@ -926,32 +961,59 @@
|
|||
};
|
||||
let previousTarget = target.value;
|
||||
const updateMode = (targetChanged = false) => {
|
||||
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
|
||||
count.min = String(def.min ?? 0);
|
||||
count.max = String(def.max ?? 100);
|
||||
count.step = String(def.step ?? 1);
|
||||
if (targetChanged && previousTarget !== target.value) count.value = String(def.threshold);
|
||||
const def = targetDef();
|
||||
for (const control of [valueMin, valueMax, valueMinNumber, valueMaxNumber]) {
|
||||
control.min = String(def.min ?? 0); control.max = String(def.max ?? 100); control.step = String(def.step ?? 1);
|
||||
}
|
||||
if (targetChanged && previousTarget !== target.value) {
|
||||
valueMin.value = String(def.threshold);
|
||||
valueMax.value = String(def.max);
|
||||
}
|
||||
previousTarget = target.value;
|
||||
comparatorRow.hidden = Boolean(def.time);
|
||||
countRow.hidden = Boolean(def.time);
|
||||
valueRow.hidden = Boolean(def.time);
|
||||
widthRow.hidden = !def.spatial;
|
||||
heightRow.hidden = !def.spatial;
|
||||
timeRow.hidden = !def.time;
|
||||
updateNumericDisplays(); updateTimeRange(); updateOverlay();
|
||||
updateValueRange(); updateNumericDisplays(); updateTimeRange(); updateOverlay();
|
||||
};
|
||||
count.oninput = updateNumericDisplays;
|
||||
valueMin.oninput = () => updateValueRange("min");
|
||||
valueMax.oninput = () => updateValueRange("max");
|
||||
valueMinNumber.oninput = () => updateValueRange("min-number");
|
||||
valueMaxNumber.oninput = () => updateValueRange("max-number");
|
||||
width.oninput = () => { updateNumericDisplays(); updateOverlay(); };
|
||||
height.oninput = () => { updateNumericDisplays(); updateOverlay(); };
|
||||
timeStart.oninput = updateTimeRange; timeEnd.oninput = updateTimeRange;
|
||||
|
||||
const bindNearestThumbTeleport = (range, first, second, normalize, update) => {
|
||||
if (!range || !first || !second) return;
|
||||
range.onpointerdown = event => {
|
||||
if (event.button !== 0 || event.target === first || event.target === second) return;
|
||||
const rect = range.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
event.preventDefault();
|
||||
const min = Number(first.min || 0), max = Number(first.max || 100);
|
||||
const ratio = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
|
||||
const raw = min + (max - min) * ratio;
|
||||
const next = normalize(raw);
|
||||
const firstValue = Number(first.value), secondValue = Number(second.value);
|
||||
const targetThumb = Math.abs(next - firstValue) <= Math.abs(next - secondValue) ? first : second;
|
||||
targetThumb.value = String(next);
|
||||
update(targetThumb === first ? "min" : "max");
|
||||
targetThumb.focus({ preventScroll: true });
|
||||
};
|
||||
};
|
||||
bindNearestThumbTeleport(valueRange, valueMin, valueMax, value => normalizeValue(value, targetDef().threshold, targetDef()), source => updateValueRange(source));
|
||||
bindNearestThumbTeleport(timeRange, timeStart, timeEnd, value => normalizeMinute(value, 0), updateTimeRange);
|
||||
target.onchange = () => updateMode(true);
|
||||
updateMode(false);
|
||||
const close = () => { dialog.classList.add("hidden"); if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null; global.render?.(); };
|
||||
dialog.querySelector("#signalItemEditorApply").onclick = () => {
|
||||
global.TarinaiHistory.capture(worldRef, "detector-edit");
|
||||
item.pressureTarget = DETECTOR_TARGETS[target.value] ? target.value : "tarinai";
|
||||
item.pressureComparator = comparator.value === "lte" ? "lte" : "gte";
|
||||
const targetDef = DETECTOR_TARGETS[item.pressureTarget] || DETECTOR_TARGETS.tarinai;
|
||||
item.pressureThreshold = Math.max(Number(targetDef.min ?? 0), Math.min(Number(targetDef.max ?? 100), Number(count.value || 0)));
|
||||
const def = targetDef();
|
||||
item.pressureMin = normalizeValue(valueMin.value, def.threshold, def);
|
||||
item.pressureMax = normalizeValue(valueMax.value, def.max, def);
|
||||
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(width.value || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(height.value || 160) | 0));
|
||||
item.pressureTimeStart = normalizeMinute(timeStart.value, 360);
|
||||
|
|
@ -966,7 +1028,7 @@
|
|||
}
|
||||
|
||||
function isEditableItem(item) {
|
||||
return item && !item.dead && (item.type === "signboard" || item.type === "robot_cleaner" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator" || item.type === "fan" || item.type === "pressure_switch");
|
||||
return item && !item.dead && (item.type === "signboard" || item.type === "robot_cleaner" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator" || item.type === "fan" || item.type === "pressure_switch" || item.type === "circuit_board");
|
||||
}
|
||||
|
||||
function findEditableItemAt(worldRef, x, y) {
|
||||
|
|
@ -995,6 +1057,7 @@
|
|||
if (item.type === "reciprocator") return openReciprocatorEditor(item, worldRef);
|
||||
if (item.type === "fan") return openFanEditor(item, worldRef);
|
||||
if (item.type === "pressure_switch") return openSignalItemEditor(item, worldRef);
|
||||
if (item.type === "circuit_board") return global.TarinaiCircuitBoardSystem?.openEditor?.(item, worldRef) || false;
|
||||
if (item.type === "gate_fence") {
|
||||
item.gateOpen = !item.gateOpen;
|
||||
worldRef?.markSpatialDirty?.("gate-toggle");
|
||||
|
|
@ -1474,6 +1537,9 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (!found && !this.findDeleteToolTargetAt?.(x, y)) {
|
||||
global.TarinaiAchievements?.recordEmptyClick?.({ world: this, x, y });
|
||||
}
|
||||
const wasSelectionEmpty = !this.selected;
|
||||
if (found && wasSelectionEmpty) this._scrollSelectedDataTopOnNextSelection = true;
|
||||
else this._scrollSelectedDataTopOnNextSelection = false;
|
||||
|
|
|
|||
|
|
@ -150,6 +150,13 @@
|
|||
if (!it || it.dead || it.type === "splat") continue;
|
||||
if ((this.tool === "rope" || this.tool === "rod" || this.tool === "spring") && global.TarinaiPhysicsSoftClear.isSoftPlacementType(it.type)) continue;
|
||||
if (wireTool && ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) continue;
|
||||
if (wireTool && it.type === "circuit_board") {
|
||||
const portHit = global.TarinaiCircuitBoardSystem?.nearestPort?.(it, x, y, 30);
|
||||
if (!portHit || portHit.distance >= bestD) continue;
|
||||
best = { kind: "item", id: it.id, type: it.type, label: `${toolLabel(it.type)} ${portHit.port.label}`, x: portHit.x, y: portHit.y, localX: portHit.localX, localY: portHit.localY, center: false, portId: portHit.port.portId };
|
||||
bestD = portHit.distance;
|
||||
continue;
|
||||
}
|
||||
const isMechanism = Boolean(global.TarinaiMechanicalSystem.isMechanicalType(it.type));
|
||||
const isFence = this.isFenceType?.(it.type) || false;
|
||||
const isConstraint = ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type);
|
||||
|
|
|
|||
|
|
@ -183,6 +183,9 @@
|
|||
const previous = this.groundType || "soil";
|
||||
this.groundType = next;
|
||||
if (next !== previous || opts.force) {
|
||||
if (next !== previous && !opts.silent) {
|
||||
window.TarinaiAchievements?.recordGroundChange?.({ world: this, previous, next, silent: false });
|
||||
}
|
||||
window.TarinaiWeatherSystem.enforceFixedWeather(this);
|
||||
this.markTerrainDirty?.("ground-type");
|
||||
this.enforceGrassLimit?.("ground-type");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue