diff --git a/.tmp-edge-annexation/Crashpad/metadata b/.tmp-edge-annexation/Crashpad/metadata new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Crashpad/settings.dat b/.tmp-edge-annexation/Crashpad/settings.dat new file mode 100644 index 0000000..f6066a3 Binary files /dev/null and b/.tmp-edge-annexation/Crashpad/settings.dat differ diff --git a/.tmp-edge-annexation/Crashpad/throttle_store.dat b/.tmp-edge-annexation/Crashpad/throttle_store.dat new file mode 100644 index 0000000..9639e1b --- /dev/null +++ b/.tmp-edge-annexation/Crashpad/throttle_store.dat @@ -0,0 +1 @@ +level=none expiry=0 diff --git a/.tmp-edge-annexation/Default/Affiliation Database b/.tmp-edge-annexation/Default/Affiliation Database new file mode 100644 index 0000000..3ad9328 Binary files /dev/null and b/.tmp-edge-annexation/Default/Affiliation Database differ diff --git a/.tmp-edge-annexation/Default/Affiliation Database-journal b/.tmp-edge-annexation/Default/Affiliation Database-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/BookmarkMergedSurfaceOrdering b/.tmp-edge-annexation/Default/BookmarkMergedSurfaceOrdering new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/.tmp-edge-annexation/Default/BookmarkMergedSurfaceOrdering @@ -0,0 +1,2 @@ +{ +} diff --git a/.tmp-edge-annexation/Default/Cache/Cache_Data/data_0 b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_0 new file mode 100644 index 0000000..9a3ed59 Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_0 differ diff --git a/.tmp-edge-annexation/Default/Cache/Cache_Data/data_1 b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_1 new file mode 100644 index 0000000..e220afb Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_1 differ diff --git a/.tmp-edge-annexation/Default/Cache/Cache_Data/data_2 b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_2 differ diff --git a/.tmp-edge-annexation/Default/Cache/Cache_Data/data_3 b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_3 new file mode 100644 index 0000000..1629660 Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/Cache_Data/data_3 differ diff --git a/.tmp-edge-annexation/Default/Cache/Cache_Data/index b/.tmp-edge-annexation/Default/Cache/Cache_Data/index new file mode 100644 index 0000000..b8eb446 Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/Cache_Data/index differ diff --git a/.tmp-edge-annexation/Default/Cache/No_Vary_Search/journal.baj b/.tmp-edge-annexation/Default/Cache/No_Vary_Search/journal.baj new file mode 100644 index 0000000..54fe66e --- /dev/null +++ b/.tmp-edge-annexation/Default/Cache/No_Vary_Search/journal.baj @@ -0,0 +1 @@ +$F~ \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Cache/No_Vary_Search/snapshot.baf b/.tmp-edge-annexation/Default/Cache/No_Vary_Search/snapshot.baf new file mode 100644 index 0000000..8912405 Binary files /dev/null and b/.tmp-edge-annexation/Default/Cache/No_Vary_Search/snapshot.baf differ diff --git a/.tmp-edge-annexation/Default/ClientCertificates/LOCK b/.tmp-edge-annexation/Default/ClientCertificates/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/ClientCertificates/LOG b/.tmp-edge-annexation/Default/ClientCertificates/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Code Cache/js/2a8a6f695a3ff316_0 b/.tmp-edge-annexation/Default/Code Cache/js/2a8a6f695a3ff316_0 new file mode 100644 index 0000000..e289010 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/2a8a6f695a3ff316_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/2cca9bc741a6516d_0 b/.tmp-edge-annexation/Default/Code Cache/js/2cca9bc741a6516d_0 new file mode 100644 index 0000000..7921f92 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/2cca9bc741a6516d_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/5cce1706364839f1_0 b/.tmp-edge-annexation/Default/Code Cache/js/5cce1706364839f1_0 new file mode 100644 index 0000000..bf87cec Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/5cce1706364839f1_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/5fbb54f36261c413_0 b/.tmp-edge-annexation/Default/Code Cache/js/5fbb54f36261c413_0 new file mode 100644 index 0000000..36a6bc7 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/5fbb54f36261c413_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/7de0dc67a2b3a69c_0 b/.tmp-edge-annexation/Default/Code Cache/js/7de0dc67a2b3a69c_0 new file mode 100644 index 0000000..b5c197e Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/7de0dc67a2b3a69c_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/8020acacb3d13763_0 b/.tmp-edge-annexation/Default/Code Cache/js/8020acacb3d13763_0 new file mode 100644 index 0000000..cd9a06a Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/8020acacb3d13763_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/8bb1867be67136eb_0 b/.tmp-edge-annexation/Default/Code Cache/js/8bb1867be67136eb_0 new file mode 100644 index 0000000..588606b Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/8bb1867be67136eb_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/945cb8fb56d09a8e_0 b/.tmp-edge-annexation/Default/Code Cache/js/945cb8fb56d09a8e_0 new file mode 100644 index 0000000..1d6ae3b Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/945cb8fb56d09a8e_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/b21fb45197b9c728_0 b/.tmp-edge-annexation/Default/Code Cache/js/b21fb45197b9c728_0 new file mode 100644 index 0000000..88088d7 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/b21fb45197b9c728_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/ba678a2fbd8c358c_0 b/.tmp-edge-annexation/Default/Code Cache/js/ba678a2fbd8c358c_0 new file mode 100644 index 0000000..be4ed8b Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/ba678a2fbd8c358c_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/baded5730168391f_0 b/.tmp-edge-annexation/Default/Code Cache/js/baded5730168391f_0 new file mode 100644 index 0000000..2392d7a Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/baded5730168391f_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/e94999eac39dfd99_0 b/.tmp-edge-annexation/Default/Code Cache/js/e94999eac39dfd99_0 new file mode 100644 index 0000000..6b47c1b Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/e94999eac39dfd99_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/e9a142a21b9ad9ef_0 b/.tmp-edge-annexation/Default/Code Cache/js/e9a142a21b9ad9ef_0 new file mode 100644 index 0000000..01d9792 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/e9a142a21b9ad9ef_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/ebce9e52a869b921_0 b/.tmp-edge-annexation/Default/Code Cache/js/ebce9e52a869b921_0 new file mode 100644 index 0000000..f5f15fc Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/ebce9e52a869b921_0 differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/index b/.tmp-edge-annexation/Default/Code Cache/js/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/index differ diff --git a/.tmp-edge-annexation/Default/Code Cache/js/index-dir/the-real-index b/.tmp-edge-annexation/Default/Code Cache/js/index-dir/the-real-index new file mode 100644 index 0000000..48f2005 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/js/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Code Cache/wasm/index b/.tmp-edge-annexation/Default/Code Cache/wasm/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/wasm/index differ diff --git a/.tmp-edge-annexation/Default/Code Cache/wasm/index-dir/the-real-index b/.tmp-edge-annexation/Default/Code Cache/wasm/index-dir/the-real-index new file mode 100644 index 0000000..a806ca1 Binary files /dev/null and b/.tmp-edge-annexation/Default/Code Cache/wasm/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/DawnGraphiteCache/data_0 b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/DawnGraphiteCache/data_1 b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/DawnGraphiteCache/data_2 b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/DawnGraphiteCache/data_3 b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnGraphiteCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/DawnGraphiteCache/index b/.tmp-edge-annexation/Default/DawnGraphiteCache/index new file mode 100644 index 0000000..13f0319 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnGraphiteCache/index differ diff --git a/.tmp-edge-annexation/Default/DawnWebGPUCache/data_0 b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/DawnWebGPUCache/data_1 b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/DawnWebGPUCache/data_2 b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/DawnWebGPUCache/data_3 b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnWebGPUCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/DawnWebGPUCache/index b/.tmp-edge-annexation/Default/DawnWebGPUCache/index new file mode 100644 index 0000000..4f47c8b Binary files /dev/null and b/.tmp-edge-annexation/Default/DawnWebGPUCache/index differ diff --git a/.tmp-edge-annexation/Default/DualEngine/SiteList-Consumer.json b/.tmp-edge-annexation/Default/DualEngine/SiteList-Consumer.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.tmp-edge-annexation/Default/DualEngine/SiteList-Consumer.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/DualEngine/SiteList-Enterprise.json b/.tmp-edge-annexation/Default/DualEngine/SiteList-Enterprise.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.tmp-edge-annexation/Default/DualEngine/SiteList-Enterprise.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/EdgeHubAppUsage/EdgeHubAppUsageSQLite.db b/.tmp-edge-annexation/Default/EdgeHubAppUsage/EdgeHubAppUsageSQLite.db new file mode 100644 index 0000000..6bccdac Binary files /dev/null and b/.tmp-edge-annexation/Default/EdgeHubAppUsage/EdgeHubAppUsageSQLite.db differ diff --git a/.tmp-edge-annexation/Default/EdgeHubAppUsage/EdgeHubAppUsageSQLite.db-journal b/.tmp-edge-annexation/Default/EdgeHubAppUsage/EdgeHubAppUsageSQLite.db-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/EdgePushStorageWithConnectTokenAndKey/LOCK b/.tmp-edge-annexation/Default/EdgePushStorageWithConnectTokenAndKey/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/EdgePushStorageWithConnectTokenAndKey/LOG b/.tmp-edge-annexation/Default/EdgePushStorageWithConnectTokenAndKey/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Extension Rules/000003.log b/.tmp-edge-annexation/Default/Extension Rules/000003.log new file mode 100644 index 0000000..192989b Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension Rules/000003.log differ diff --git a/.tmp-edge-annexation/Default/Extension Rules/CURRENT b/.tmp-edge-annexation/Default/Extension Rules/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension Rules/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension Rules/LOCK b/.tmp-edge-annexation/Default/Extension Rules/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Extension Rules/LOG b/.tmp-edge-annexation/Default/Extension Rules/LOG new file mode 100644 index 0000000..ab20211 --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension Rules/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:00.872 38d0 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension Rules since it was missing. +2026/09/16-15:38:00.906 38d0 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension Rules/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension Rules/MANIFEST-000001 b/.tmp-edge-annexation/Default/Extension Rules/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension Rules/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Extension Scripts/000003.log b/.tmp-edge-annexation/Default/Extension Scripts/000003.log new file mode 100644 index 0000000..192989b Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension Scripts/000003.log differ diff --git a/.tmp-edge-annexation/Default/Extension Scripts/CURRENT b/.tmp-edge-annexation/Default/Extension Scripts/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension Scripts/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension Scripts/LOCK b/.tmp-edge-annexation/Default/Extension Scripts/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Extension Scripts/LOG b/.tmp-edge-annexation/Default/Extension Scripts/LOG new file mode 100644 index 0000000..4f67d17 --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension Scripts/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:00.914 38d0 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension Scripts since it was missing. +2026/09/16-15:38:00.934 38d0 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension Scripts/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension Scripts/MANIFEST-000001 b/.tmp-edge-annexation/Default/Extension Scripts/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension Scripts/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Extension State/000003.log b/.tmp-edge-annexation/Default/Extension State/000003.log new file mode 100644 index 0000000..9515a12 Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension State/000003.log differ diff --git a/.tmp-edge-annexation/Default/Extension State/CURRENT b/.tmp-edge-annexation/Default/Extension State/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension State/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension State/LOCK b/.tmp-edge-annexation/Default/Extension State/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Extension State/LOG b/.tmp-edge-annexation/Default/Extension State/LOG new file mode 100644 index 0000000..7348e8b --- /dev/null +++ b/.tmp-edge-annexation/Default/Extension State/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.289 2334 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension State since it was missing. +2026/09/16-15:38:01.378 2334 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Extension State/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Extension State/MANIFEST-000001 b/.tmp-edge-annexation/Default/Extension State/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Extension State/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Favicons b/.tmp-edge-annexation/Default/Favicons new file mode 100644 index 0000000..56e14d6 Binary files /dev/null and b/.tmp-edge-annexation/Default/Favicons differ diff --git a/.tmp-edge-annexation/Default/Favicons-journal b/.tmp-edge-annexation/Default/Favicons-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/GPUCache/data_0 b/.tmp-edge-annexation/Default/GPUCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/GPUCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/GPUCache/data_1 b/.tmp-edge-annexation/Default/GPUCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/GPUCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/GPUCache/data_2 b/.tmp-edge-annexation/Default/GPUCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/GPUCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/GPUCache/data_3 b/.tmp-edge-annexation/Default/GPUCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/GPUCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/GPUCache/index b/.tmp-edge-annexation/Default/GPUCache/index new file mode 100644 index 0000000..ea819f8 Binary files /dev/null and b/.tmp-edge-annexation/Default/GPUCache/index differ diff --git a/.tmp-edge-annexation/Default/History b/.tmp-edge-annexation/Default/History new file mode 100644 index 0000000..59f8d19 Binary files /dev/null and b/.tmp-edge-annexation/Default/History differ diff --git a/.tmp-edge-annexation/Default/History-journal b/.tmp-edge-annexation/Default/History-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/HubApps Icons b/.tmp-edge-annexation/Default/HubApps Icons new file mode 100644 index 0000000..cfa1dac Binary files /dev/null and b/.tmp-edge-annexation/Default/HubApps Icons differ diff --git a/.tmp-edge-annexation/Default/HubApps Icons-journal b/.tmp-edge-annexation/Default/HubApps Icons-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/LOCK b/.tmp-edge-annexation/Default/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/LOG b/.tmp-edge-annexation/Default/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/000003.log b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/000003.log new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/CURRENT b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/LOCK b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/LOG b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/LOG new file mode 100644 index 0000000..00cd70f --- /dev/null +++ b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.629 21ac Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Local Extension Settings\jdiccldimpdaibmpdkjnbmckianbfold since it was missing. +2026/09/16-15:38:01.682 21ac Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Local Extension Settings\jdiccldimpdaibmpdkjnbmckianbfold/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/MANIFEST-000001 b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Local Extension Settings/jdiccldimpdaibmpdkjnbmckianbfold/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Local Storage/leveldb/000003.log b/.tmp-edge-annexation/Default/Local Storage/leveldb/000003.log new file mode 100644 index 0000000..988af49 Binary files /dev/null and b/.tmp-edge-annexation/Default/Local Storage/leveldb/000003.log differ diff --git a/.tmp-edge-annexation/Default/Local Storage/leveldb/CURRENT b/.tmp-edge-annexation/Default/Local Storage/leveldb/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Local Storage/leveldb/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Local Storage/leveldb/LOCK b/.tmp-edge-annexation/Default/Local Storage/leveldb/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Local Storage/leveldb/LOG b/.tmp-edge-annexation/Default/Local Storage/leveldb/LOG new file mode 100644 index 0000000..4ebc52d --- /dev/null +++ b/.tmp-edge-annexation/Default/Local Storage/leveldb/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:00.940 3544 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Local Storage\leveldb since it was missing. +2026/09/16-15:38:00.964 3544 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Local Storage\leveldb/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Local Storage/leveldb/MANIFEST-000001 b/.tmp-edge-annexation/Default/Local Storage/leveldb/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Local Storage/leveldb/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Login Data b/.tmp-edge-annexation/Default/Login Data new file mode 100644 index 0000000..49af46e Binary files /dev/null and b/.tmp-edge-annexation/Default/Login Data differ diff --git a/.tmp-edge-annexation/Default/Login Data For Account b/.tmp-edge-annexation/Default/Login Data For Account new file mode 100644 index 0000000..49af46e Binary files /dev/null and b/.tmp-edge-annexation/Default/Login Data For Account differ diff --git a/.tmp-edge-annexation/Default/Login Data For Account-journal b/.tmp-edge-annexation/Default/Login Data For Account-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Login Data-journal b/.tmp-edge-annexation/Default/Login Data-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Network/Cookies b/.tmp-edge-annexation/Default/Network/Cookies new file mode 100644 index 0000000..ae50c91 Binary files /dev/null and b/.tmp-edge-annexation/Default/Network/Cookies differ diff --git a/.tmp-edge-annexation/Default/Network/Cookies-journal b/.tmp-edge-annexation/Default/Network/Cookies-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Network/Device Bound Sessions b/.tmp-edge-annexation/Default/Network/Device Bound Sessions new file mode 100644 index 0000000..fbc96d7 Binary files /dev/null and b/.tmp-edge-annexation/Default/Network/Device Bound Sessions differ diff --git a/.tmp-edge-annexation/Default/Network/Device Bound Sessions-journal b/.tmp-edge-annexation/Default/Network/Device Bound Sessions-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Network/Network Persistent State b/.tmp-edge-annexation/Default/Network/Network Persistent State new file mode 100644 index 0000000..ba768d8 --- /dev/null +++ b/.tmp-edge-annexation/Default/Network/Network Persistent State @@ -0,0 +1 @@ +{"net":{"http_server_properties":{"servers":[{"anonymization":["HAAAABUAAABodHRwczovL21pY3Jvc29mdC5jb20AAAA=",false,0],"server":"https://edge.microsoft.com","supports_spdy":true}],"version":5},"network_qualities":{"CAISABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Network/NetworkDataMigrated b/.tmp-edge-annexation/Default/Network/NetworkDataMigrated new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Network/Reporting and NEL b/.tmp-edge-annexation/Default/Network/Reporting and NEL new file mode 100644 index 0000000..a24d6ed Binary files /dev/null and b/.tmp-edge-annexation/Default/Network/Reporting and NEL differ diff --git a/.tmp-edge-annexation/Default/Network/Reporting and NEL-journal b/.tmp-edge-annexation/Default/Network/Reporting and NEL-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Network/SCT Auditing Pending Reports b/.tmp-edge-annexation/Default/Network/SCT Auditing Pending Reports new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.tmp-edge-annexation/Default/Network/SCT Auditing Pending Reports @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Network/Sdch Dictionaries b/.tmp-edge-annexation/Default/Network/Sdch Dictionaries new file mode 100644 index 0000000..8bca100 --- /dev/null +++ b/.tmp-edge-annexation/Default/Network/Sdch Dictionaries @@ -0,0 +1 @@ +{"SDCH":{"dictionaries":{},"version":2}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Network/Trust Tokens b/.tmp-edge-annexation/Default/Network/Trust Tokens new file mode 100644 index 0000000..8d527ef Binary files /dev/null and b/.tmp-edge-annexation/Default/Network/Trust Tokens differ diff --git a/.tmp-edge-annexation/Default/Network/Trust Tokens-journal b/.tmp-edge-annexation/Default/Network/Trust Tokens-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Nurturing/campaign_history b/.tmp-edge-annexation/Default/Nurturing/campaign_history new file mode 100644 index 0000000..9c6cef1 Binary files /dev/null and b/.tmp-edge-annexation/Default/Nurturing/campaign_history differ diff --git a/.tmp-edge-annexation/Default/Nurturing/campaign_history-journal b/.tmp-edge-annexation/Default/Nurturing/campaign_history-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/PersistentOriginTrials/LOCK b/.tmp-edge-annexation/Default/PersistentOriginTrials/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/PersistentOriginTrials/LOG b/.tmp-edge-annexation/Default/PersistentOriginTrials/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Preferences b/.tmp-edge-annexation/Default/Preferences new file mode 100644 index 0000000..99cf9b9 --- /dev/null +++ b/.tmp-edge-annexation/Default/Preferences @@ -0,0 +1 @@ +{"aadc_info":{"age_group":0},"accessibility":{"captions":{"headless_caption_enabled":false}},"account_tracker_service_last_update":"13434014281145165","apps":{"shortcuts_arch":"","shortcuts_os_version":"","shortcuts_version":1},"autocomplete":{"retention_policy_last_version":153},"autofill":{"ai_last_version_deduped":153,"autofill_ai":{"opt_in_status":{}},"edge_autofill_advanced_ml_enabled":true,"edge_autofill_purge_low_quality_profiles_by_timeline":false,"last_version_deduped":153},"bookmark":{"storage_computation_last_update":"13434014281071340"},"browser":{"available_dark_theme_options":"All","color_theme_user_on_first_check":false,"edge_sidebar_visibility":{"add_app_to_bottom":true},"editor_proofing_languages":{},"mai_ds_default_theme_type":1,"recent_theme_color_list":[4293914607.0,4293914607.0,4293914607.0,4293914607.0,4293914607.0],"show_downloads_hub_pinned":false,"time_of_last_normal_window_close":"13434014281972359","underside_chat_bing_signed_in_status":false,"window_placement":{"bottom":590,"left":10,"maximized":false,"right":790,"top":10,"work_area_bottom":600,"work_area_left":0,"work_area_right":800,"work_area_top":0}},"commerce_daily_metrics_last_update_time":"13434014281072637","continuous_migration":{"equal_opt_out_users_data":{"backfilled":true}},"countryid_at_install":19024,"custom_links":{"list":[]},"dual_engine":{"consumer_site_list_with_ie_entries":false,"consumer_sitelist_location":"","consumer_sitelist_version":"","shared_cookie_data":{},"sitelist_has_consumer_data":false,"sitelist_has_enterprise_data":false,"sitelist_location":"","sitelist_source":0,"sitelist_version":""},"edge":{"msa_sso_info":{"allow_for_non_msa_profile":true},"profile_sso_info":{"aad_sso_algo_state":1,"is_first_profile":true,"is_msa_first_profile":true,"msa_sso_algo_state":1},"services":{"signin_scoped_device_id":"6fae8e4a-8196-4a6a-9ffc-26e930faf921"},"workspaces":{"state":"{\"edgeWorkspacePrefsVersion\":2,\"enableFluid\":true,\"failedRestartSpaceId\":\"\",\"failedToConnectToFluid\":false,\"fluidMigrationStatus\":false,\"fluidStatus\":0,\"fre_shown\":false,\"fromCache\":false,\"isFluidPreferencesConnected\":false,\"isSpaceOpening\":false,\"openingSpaceId\":\"\",\"statusForScreenReaders\":\"\",\"workspacePopupMode\":0,\"workspacesForExternalLinks\":[]}"}},"edge_pinning_campaign":{"precomputed_campaign_data":{}},"edge_rewards":{"cache_data":"CAA=","hva_webui_action_status_dict":{},"refresh_status_muted_until":"13434619081460564"},"edge_vpn":{"available":true},"edge_wallet":{"trigger_funnel":{"records":[]}},"enterprise_profile_guid":"366110e9-1c4a-4731-9305-efbb69a90303","extension":{"installed_extension_count":9},"extensions":{"alerts":{"initialized":true},"chrome_url_overrides":{},"last_chrome_version":"153.0.4234.32","pdf_upsell_triggered":false,"pinned_extension_migration":true,"pinned_extensions":[]},"family_safety":{"activity_reporting_enabled":false,"web_filtering_enabled":false},"fsd":{"retention_policy_last_version":153},"gaia_cookie":{"periodic_report_time_2":"13434014280782870"},"in_product_help":{"recent_session_enabled_time":"13434014280990749","recent_session_start_times":["13434014280990749"],"session_last_active_time":"13434014280990749","session_number":2,"session_start_time":"13434014280990749"},"intl":{"selected_languages":"ja,en,en-GB,en-US"},"media":{"engagement":{"schema_version":5}},"media_router":{"receiver_id_hash_token":"UYdef6kSkXqSwQwo5o4foYMhkI9LFJAdwVg8afR6WjOuCd7qlcEBmSbqZSzA0yXWthnU82tqKROp9LPlZxR9pQ=="},"muid":{"last_sync":"13434014281070361","values_seen":[]},"ntp":{"num_personal_suggestions":1},"omnibox":{"work_organization_name":{"name":""}},"optimization_guide":{"previously_registered_optimization_types":{"ABOUT_THIS_SITE":true,"AUTOFILL_ACTOR_IFRAME_ORIGIN_ALLOWLIST":true,"DIGITAL_CREDENTIALS_LOW_FRICTION":true,"GLIC_ACTION_PAGE_BLOCK":true,"LOADING_PREDICTOR":true,"MERCHANT_TRUST_SIGNALS_V2":true,"PRICE_TRACKING":true,"SAVED_TAB_GROUP":true}},"privacy_sandbox":{"first_party_sets_data_access_allowed_initialized":true},"profile":{"avatar_index":20,"background_password_check":{"check_fri_weight":9,"check_interval":"864000000000","check_mon_weight":4,"check_sat_weight":4,"check_sun_weight":4,"check_thu_weight":9,"check_tue_weight":9,"check_wed_weight":9,"next_check_time":"13434498492910172"},"content_settings":{"exceptions":{"abusive_notification_permissions":{},"access_to_get_all_screens_media_in_session":{},"anti_abuse":{},"app_banner":{},"ar":{},"are_suspicious_notifications_allowlisted_by_user":{},"auto_picture_in_picture":{},"auto_select_certificate":{},"automatic_downloads":{},"automatic_fullscreen":{},"autoplay":{},"background_sync":{},"bluetooth_chooser_data":{},"bluetooth_guard":{},"bluetooth_scanning":{},"camera_pan_tilt_zoom":{},"captured_surface_control":{},"clear_browsing_data_cookies_exceptions":{},"client_hints":{},"clipboard":{},"controlled_frame":{},"cookie_controls_metadata":{},"cookies":{},"direct_sockets":{},"direct_sockets_private_network_access":{},"display_media_system_audio":{},"disruptive_notification_permissions":{},"durable_storage":{},"edge_ad_targeting":{},"edge_ad_targeting_data":{},"edge_all_file_read_access":{},"edge_browser_action":{},"edge_notification_referrer_chain_blocked":{},"edge_sdsm":{},"edge_tech_scam_detection":{},"edge_u2f_api_request":{},"edge_user_agent_token":{},"fedcm_idp_registration":{},"fedcm_idp_signin":{},"fedcm_share":{},"file_system_access_chooser_data":{},"file_system_access_extended_permission":{},"file_system_access_restore_permission":{},"file_system_last_picked_directory":{},"file_system_read_guard":{},"file_system_write_guard":{},"formfill_metadata":{},"geolocation":{},"geolocation_with_options":{},"hand_tracking":{},"has_migrated_local_network_access":true,"hid_chooser_data":{},"hid_guard":{},"http_allowed":{},"https_enforced":{},"idle_detection":{},"images":{},"important_site_info":{},"initialized_translations":{},"inline_cue_menu":{},"intent_picker_auto_display":{},"javascript":{},"javascript_jit":{},"javascript_optimizer":{},"keyboard_lock":{},"legacy_cookie_access":{},"legacy_cookie_scope":{},"local_fonts":{},"local_network":{},"local_network_access":{},"loopback_network":{},"media_engagement":{},"media_stream_camera":{},"media_stream_mic":{},"midi_sysex":{},"mixed_script":{},"nfc_devices":{},"notification_interactions":{},"notification_permission_review":{},"notifications":{},"ondevice_languages_downloaded":{},"password_protection":{},"payment_handler":{},"permission_actions_history":{},"permission_autoblocking_data":{},"permission_autorevocation_data":{},"pointer_lock":{},"popups":{},"protected_media_identifier":{},"protocol_handler":{},"reduced_accept_language":{},"safe_browsing_url_check_data":{},"secure_network":{},"secure_network_sites":{},"sensors":{},"serial_chooser_data":{},"serial_guard":{},"site_engagement":{},"sleeping_tabs":{},"sound":{},"speaker_selection":{},"ssl_cert_decisions":{},"storage_access":{},"storage_access_header_origin_trial":{},"sub_apps_without_prompts":{},"subresource_filter":{},"subresource_filter_data":{},"suspicious_notification_ids":{},"suspicious_notification_show_original":{},"suspicious_site_warning_data":{},"top_level_storage_access":{},"trackers":{},"trackers_data":{},"tracking_org_exceptions":{},"tracking_org_relationships":{},"typosquatting":{},"unused_site_permissions":{},"usb_chooser_data":{},"usb_guard":{},"vr":{},"web_app_installation":{},"webid_api":{},"webid_auto_reauthn":{},"window_placement":{}},"pref_version":1},"created_by_version":"153.0.4234.32","creation_time":"13434014280731356","default_content_setting_values":{"has_migrated_local_network_access":true},"edge_crash_exit_count":0,"edge_password_is_using_new_login_db_path":false,"edge_password_login_db_path_flip_flop_count":0,"edge_profile_id":"b8396b15-94a2-4c37-9571-2f79389aac57","edge_user_with_non_zero_passwords":false,"exit_type":"Normal","forced_shutdown_restart_apps":false,"has_seen_signin_fre":false,"managed_user_id":"","name":"プロファイル 1","network_pbs":{},"password_hash_data_list":[],"signin_fre_seen_time":"13434014280765552"},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"reset_prepopulated_engines":false,"safety_hub":{"unused_site_permissions_revocation":{"migration_completed":true}},"saved_tab_groups":{"did_enable_shared_tab_groups_in_last_session":false,"specifics_to_data_migration":true},"sessions":{"event_log":[{"crashed":false,"time":"13434014280799065","type":0},{"did_schedule_command":true,"first_session_service":true,"tab_count":0,"time":"13434014281972578","type":2,"window_count":1}],"session_data_status":3},"shopping":{"contextual_features_enabled":true,"dma_telemetry_expiration_time":"13434100681489678","rdx_mode_cached":true,"rdx_mode_enabled":false},"signin":{"accounts_metadata_dict":{},"allowed":true},"spellcheck":{"dictionaries":["ja","en-US"],"dictionary":""},"sync":{"apps":true,"autofill":true,"bookmarks":true,"collections_edge_re_evaluated":true,"edge_account_type":0,"edge_workspaces":true,"edge_workspaces_edge_supported":true,"encryption_bootstrap_token_per_account_migration_done":true,"extensions":true,"extensions_edge_supported":true,"history_edge_supported":true,"keep_everything_synced":false,"passwords":true,"preferences":true,"tabs":true,"tabs_edge_supported":true,"typed_urls":true},"syncing_theme_prefs_migrated_to_non_syncing":true,"tab_groups":[],"toolbar":{"pinned_cast_migration_complete":true,"pinned_chrome_labs_migration_complete":true},"typosquatting":{"allowlist_migration_done":true},"user_experience_metrics":{"personalization_data_consent_enabled_last_known_value":false},"web_apps":{"link_handling_info":{"enabled_for_installed_apps":true}}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/README b/.tmp-edge-annexation/Default/README new file mode 100644 index 0000000..4a34402 --- /dev/null +++ b/.tmp-edge-annexation/Default/README @@ -0,0 +1 @@ +Microsoft Edge settings and storage represent user-selected preferences and information and MUST not be extracted, overwritten or modified except through Microsoft Edge defined APIs. \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Secure Preferences b/.tmp-edge-annexation/Default/Secure Preferences new file mode 100644 index 0000000..2abbd3b --- /dev/null +++ b/.tmp-edge-annexation/Default/Secure Preferences @@ -0,0 +1 @@ +{"edge_fundamentals_appdefaults":{"ess_lightweight_version":101},"ess_kv_states":{"restore_on_startup":{"closed_notification":false,"decrypt_success":true,"key":"restore_on_startup","notification_popup_count":0},"startup_urls":{"closed_notification":false,"decrypt_success":true,"key":"startup_urls","notification_popup_count":0},"template_url_data":{"closed_notification":false,"decrypt_success":true,"key":"template_url_data","notification_popup_count":0}},"extensions":{"settings":{"dgiklkfkllikcanfonkcabmbdfmgleag":{"account_extension_type":0,"active_permissions":{"api":[],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":[],"first_install_time":"13434014280849760","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280849760","location":5,"manifest":{"content_capabilities":{"include_globs":["https://*excel.officeapps.live.com/*","https://*onenote.officeapps.live.com/*","https://*powerpoint.officeapps.live.com/*","https://*word-edit.officeapps.live.com/*","https://*excel.officeapps.live.com.mcas.ms/*","https://*onenote.officeapps.live.com.mcas.ms/*","https://*word-edit.officeapps.live.com.mcas.ms/*","https://*excel.partner.officewebapps.cn/*","https://*onenote.partner.officewebapps.cn/*","https://*powerpoint.partner.officewebapps.cn/*","https://*word-edit.partner.officewebapps.cn/*","https://*excel.gov.online.office365.us/*","https://*onenote.gov.online.office365.us/*","https://*powerpoint.gov.online.office365.us/*","https://*word-edit.gov.online.office365.us/*","https://*excel.dod.online.office365.us/*","https://*onenote.dod.online.office365.us/*","https://*powerpoint.dod.online.office365.us/*","https://*word-edit.dod.online.office365.us/*","https://*visio.partner.officewebapps.cn/*","https://*visio.gov.online.office365.us/*","https://*visio.dod.online.office365.us/*"],"matches":["https://*.officeapps.live.com/*","https://*.officeapps.live.com.mcas.ms/*","https://*.partner.officewebapps.cn/*","https://*.gov.online.office365.us/*","https://*.dod.online.office365.us/*","https://*.app.whiteboard.microsoft.com/*","https://*.whiteboard.office.com/*","https://*.app.int.whiteboard.microsoft.com/*","https://*.whiteboard.office365.us/*","https://*.dev.whiteboard.microsoft.com/*"],"permissions":["clipboardRead","clipboardWrite"]},"default_locale":"en","description":"This extension grants Microsoft web sites permission to read and write from the clipboard.","key":"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCz4t/X7GeuP6GBpjmxndrjtzF//4CWeHlC68rkoV7hP3h5Ka6eX7ZMNlYJkSjmB5iRmPHO5kR1y7rGY8JXnRPDQh/CQNLVA7OsKeV6w+UO+vx8KGI+TrTAhzH8YGcMIsxsUjxtC4cBmprja+xDr0zVp2EMgqHu+GBKgwSRHTkDuwIDAQAB","manifest_version":2,"minimum_chrome_version":"77","name":"Microsoft Clipboard Extension","version":"1.0"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\edge_clipboard","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"fikbjbembnmfhppjfnmfkahdhfohhjmg":{"account_extension_type":0,"active_permissions":{"api":[],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":[],"first_install_time":"13434014280855146","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280855146","location":5,"manifest":{"background":{"persistent":false,"scripts":["background.js"]},"externally_connectable":{"matches":["https://*.microsoftstream.com/*"]},"incognito":"split","key":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsAmDrYmQaYQlLxSAn/jTQTGNt1IffJGIJeKucE/B42d8QIyFD2RCarmHP1bmbY1YuTng2dL3J//qyvUNwXPt9cmxH9WKwi512tzOa5r2zYaCuOgP2vAIrah/bKnpO3XmUfFWj+LRcbZahOmMDMQxzPKxFKuIz2eOiakBXDE6Ok7azHJ13LLQTte1JgZIPmyFrAciPABLp/IKLfsfnebVW1YgaOyxBNyp/7bhSmoyZI3kBv8InKOpGE8pttrBg6l5zkvD67a7ViNAYkqZIpJJV5ZTQtVWCWSG0xU2y+3zXZtx8KbGbDiWUAcwNYDVPpsV+IQXVpgAplHvrZme+hAl6QIDAQAB","manifest_version":2,"name":"Media Internals Services Extension","permissions":["mediaInternalsPrivate"],"version":"2.0.0"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\media_internals_services","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"iglcjdemknebjbklcgkfaebgojjphkec":{"account_extension_type":0,"active_permissions":{"api":["identity","management","metricsPrivate","webstorePrivate","hubPrivate"],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"app_launcher_ordinal":"n","commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":[],"first_install_time":"13434014280848991","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280848991","location":5,"manifest":{"app":{"launch":{"web_url":"https://microsoftedge.microsoft.com"},"urls":["https://microsoftedge.microsoft.com"]},"description":"Microsoft Edge の拡張機能を検出します。","icons":{"128":"webstore_icon_128.png","16":"webstore_icon_16.png"},"key":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMvN4+y6cd3el/A/NT5eUnrz1WiD1WJRaJfMBvaMtJHIuFGEmYdYL/YuE74J19+pwhjOHeFZ3XUSMTdOa5moOaXXvdMr5wWaaN2frHewtAnNDO64NGbbZvdsfGm/kRkHKVGNV6dacZsAkylcz5CkwTmq97wOZ7ETaShHvhZEGwRQIt4K1poxurOkDYQw9ERZNf3fgYJ9ZTrLZMAFDLJY+uSF03pClWrr8VGc8LUQ4Naktb8QSgVUlrS14AdF/ESdbhnTvvdB0e7peNWRyoNtCqLJsbtTtBL6sOnqfusnwPowuueOFI+XskOT9TvLo6PcgxhLX5+d0mM+Jtn6PFTU8QIDAQAB","name":"Microsoft Store","permissions":["webstorePrivate","management","metricsPrivate","identity","hubPrivate"],"version":"0.2"},"needs_sync":true,"page_ordinal":"n","path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\microsoft_web_store","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"ihmafllikibpmigkcoadcmckbfhibefp":{"account_extension_type":0,"active_permissions":{"api":["app.window.alwaysOnTop","debugger","feedbackPrivate","fileSystem","fileSystem.write","app.window.fullscreen","metricsPrivate","storage","tabs","fileSystem.readFullPath","errorReporting","edgeInternetConnectivityPrivate","edgeFeedbackPrivate"],"explicit_host":["edge://resources/*"],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":["edgeFeedbackPrivate.onFeedbackRequested"],"first_install_time":"13434014280853699","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280853699","location":5,"manifest":{"app":{"background":{"scripts":["js/event_handler.js"]},"content_security_policy":"default-src 'none'; script-src 'self' blob: filesystem: chrome://resources; style-src 'unsafe-inline' blob: chrome: file: filesystem: data: *; img-src * blob: chrome: file: filesystem: data:; media-src 'self' blob: filesystem:; connect-src data:"},"description":"User feedback extension","display_in_launcher":false,"display_in_new_tab_page":false,"icons":{"128":"images/icon128.png","16":"images/icon16.png","192":"images/icon192.png","32":"images/icon32.png","48":"images/icon48.png"},"incognito":"split","key":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl3vxWwvLjcMIFK4OfG6C8PmJkMhFYDKRnx+SqG23YlMG1A+bOkiNmAN1TWpFPPp1f2PpbiZGNq1y29u/QfkD+PC4bnO7GbNw/2X5tGoP0n2K+KGGAxhnr0ki/oyo2eiFGSTOXlQvTRo5q1vB+Lbg+9TbFsWKlHZyAkeZ/YGz/iijHTqw8Q4RWdl5Tp8SlUhS/92EsWhveNJLW22veaT/Up2iSeSSwfyoHVYy8LUPaD4fbyLvPQacVLJq1dac2bNDqjaNvSPgPWCnkZtDmawZrgxT53otLCES/e96xfAf8I24VHIc1pVP8LqdqKr1AV1Yxn93h3VJ2QejtEhIAWHU6QIDAQAB","manifest_version":2,"name":"Edge フィードバック","permissions":["alwaysOnTopWindows","chrome://resources/","debugger","edgeInternetConnectivityPrivate","edgeFeedbackPrivate","errorReporting","feedbackPrivate",{"fileSystem":["readFullPath","write"]},"fullscreen","metricsPrivate","storage","windows"],"version":"1.0.0.1"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\edge_feedback","preferences":{},"regular_only_preferences":{},"running":true,"was_installed_by_default":false,"was_installed_by_oem":false},"jdiccldimpdaibmpdkjnbmckianbfold":{"account_extension_type":0,"active_permissions":{"api":["activeTab","metricsPrivate","storage","systemPrivate","ttsEngine","errorReporting","edgeSpeechPrivate"],"explicit_host":["https://*.bing.com/*","https://api.msedgeservices.com/*"],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":["ttsEngine.onPause","ttsEngine.onResume","ttsEngine.onSpeak","ttsEngine.onStop"],"first_install_time":"13434014280861826","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280861826","location":5,"manifest":{"background":{"persistent":false,"scripts":["lifetimeHelper.js","telemetryHelper.js","errorHelper.js","voiceList/voiceListRequester.js","voiceList/voiceListSingleton.js","voiceList/voiceModel.js","manifestHelper.js","config.js","ssml.js","uuid.js","wordBoundary.js","audioStreamer.js","wordBoundaryEventManager.js","audioViewModel.js","background.js"]},"description":"Provides access to Microsoft's online text-to-speech voices","key":"AAAAB3NzaC1yc2EAAAADAQABAAAAgQDjGOAV6/3fmEtQmFqlmqm5cZ+jlNhd6XikwMDp0I7BKh+AjG3aBIG/qqwlsF/7LAGatnSxBwUwZC0qMnGXtcOPVl26Q8OvMx0gt5Va5gxca+ae0Skluj9WN9TNxPFVhw21WbCt4D9q3kb+XXDlx/7v1ktYus4Fwr/skkjADG9cIQ==","manifest_version":2,"name":"Microsoft Voices","permissions":["activeTab","edgeSpeechPrivate","errorReporting","metricsPrivate","storage","systemPrivate","ttsEngine","https://*.bing.com/","https://api.msedgeservices.com/"],"tts_engine":{"voices":[{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"af-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (af-ZA, AdriNeural)","voice_name":"Microsoft Adri Online (Natural) - Afrikaans (South Africa)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"af-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (af-ZA, WillemNeural)","voice_name":"Microsoft Willem Online (Natural) - Afrikaans (South Africa)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"am-ET","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (am-ET, MekdesNeural)","voice_name":"Microsoft መቅደስ Online (Natural) - Amharic (Ethiopia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"am-ET","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (am-ET, AmehaNeural)","voice_name":"Microsoft አምሀ Online (Natural) - Amharic (Ethiopia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-AE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-AE, FatimaNeural)","voice_name":"Microsoft فاطمة Online (Natural) - Arabic (United Arab Emirates)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-AE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-AE, HamdanNeural)","voice_name":"Microsoft حمدان Online (Natural) - Arabic (United Arab Emirates)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-BH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-BH, LailaNeural)","voice_name":"Microsoft ليلى Online (Natural) - Arabic (Bahrain)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-BH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-BH, AliNeural)","voice_name":"Microsoft علي Online (Natural) - Arabic (Bahrain)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-DZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-DZ, AminaNeural)","voice_name":"Microsoft أمينة Online (Natural) - Arabic (Algeria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-DZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-DZ, IsmaelNeural)","voice_name":"Microsoft إسماعيل Online (Natural) - Arabic (Algeria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-EG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-EG, SalmaNeural)","voice_name":"Microsoft سلمى Online (Natural) - Arabic (Egypt)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-EG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-EG, ShakirNeural)","voice_name":"Microsoft شاكر Online (Natural) - Arabic (Egypt)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-IQ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-IQ, RanaNeural)","voice_name":"Microsoft رنا Online (Natural) - Arabic (Iraq)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-IQ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-IQ, BasselNeural)","voice_name":"Microsoft باسل Online (Natural) - Arabic (Iraq)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-JO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-JO, SanaNeural)","voice_name":"Microsoft سناء Online (Natural) - Arabic (Jordan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-JO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-JO, TaimNeural)","voice_name":"Microsoft تيم Online (Natural) - Arabic (Jordan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-KW","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-KW, NouraNeural)","voice_name":"Microsoft نورا Online (Natural) - Arabic (Kuwait)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-KW","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-KW, FahedNeural)","voice_name":"Microsoft فهد Online (Natural) - Arabic (Kuwait)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-LB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-LB, LaylaNeural)","voice_name":"Microsoft ليلى Online (Natural) - Arabic (Lebanon)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-LB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-LB, RamiNeural)","voice_name":"Microsoft رامي Online (Natural) - Arabic (Lebanon)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-LY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-LY, ImanNeural)","voice_name":"Microsoft إيمان Online (Natural) - Arabic (Libya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-LY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-LY, OmarNeural)","voice_name":"Microsoft أحمد Online (Natural) - Arabic (Libya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-MA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-MA, MounaNeural)","voice_name":"Microsoft منى Online (Natural) - Arabic (Morocco)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-MA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-MA, JamalNeural)","voice_name":"Microsoft جمال Online (Natural) - Arabic (Morocco)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-OM","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-OM, AyshaNeural)","voice_name":"Microsoft عائشة Online (Natural) - Arabic (Oman)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-OM","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-OM, AbdullahNeural)","voice_name":"Microsoft عبدالله Online (Natural) - Arabic (Oman)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-QA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-QA, AmalNeural)","voice_name":"Microsoft أمل Online (Natural) - Arabic (Qatar)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-QA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-QA, MoazNeural)","voice_name":"Microsoft معاذ Online (Natural) - Arabic (Qatar)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-SA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-SA, ZariyahNeural)","voice_name":"Microsoft زارية Online (Natural) - Arabic (Saudi Arabia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-SA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-SA, HamedNeural)","voice_name":"Microsoft حامد Online (Natural) - Arabic (Saudi Arabia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-SY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-SY, AmanyNeural)","voice_name":"Microsoft أماني Online (Natural) - Arabic (Syria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-SY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-SY, LaithNeural)","voice_name":"Microsoft ليث Online (Natural) - Arabic (Syria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-TN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-TN, ReemNeural)","voice_name":"Microsoft ريم Online (Natural) - Arabic (Tunisia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-TN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-TN, HediNeural)","voice_name":"Microsoft هادي Online (Natural) - Arabic (Tunisia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-YE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-YE, MaryamNeural)","voice_name":"Microsoft مريم Online (Natural) - Arabic (Yemen)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ar-YE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ar-YE, SalehNeural)","voice_name":"Microsoft صالح Online (Natural) - Arabic (Yemen)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"az-AZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (az-AZ, BanuNeural)","voice_name":"Microsoft Banu Online (Natural) - Azerbaijani (Latin, Azerbaijan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"az-AZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (az-AZ, BabekNeural)","voice_name":"Microsoft Babək Online (Natural) - Azerbaijani (Latin, Azerbaijan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bg-BG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bg-BG, KalinaNeural)","voice_name":"Microsoft Калина Online (Natural) - Bulgarian (Bulgaria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bg-BG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bg-BG, BorislavNeural)","voice_name":"Microsoft Борислав Online (Natural) - Bulgarian (Bulgaria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bn-BD","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bn-BD, NabanitaNeural)","voice_name":"Microsoft নবনীতা Online (Natural) - Bangla (Bangladesh)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bn-BD","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bn-BD, PradeepNeural)","voice_name":"Microsoft প্রদ্বীপ Online (Natural) - Bangla (Bangladesh)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bn-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bn-IN, TanishaaNeural)","voice_name":"Microsoft তানিশা Online (Natural) - Bengali (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bn-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bn-IN, BashkarNeural)","voice_name":"Microsoft ভাস্কর Online (Natural) - Bengali (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bs-BA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bs-BA, VesnaNeural)","voice_name":"Microsoft Vesna Online (Natural) - Bosnian (Bosnia and Herzegovina)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"bs-BA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (bs-BA, GoranNeural)","voice_name":"Microsoft Goran Online (Natural) - Bosnian (Bosnia and Herzegovina)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ca-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ca-ES, JoanaNeural)","voice_name":"Microsoft Joana Online (Natural) - Catalan"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ca-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ca-ES, EnricNeural)","voice_name":"Microsoft Enric Online (Natural) - Catalan"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"cs-CZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (cs-CZ, VlastaNeural)","voice_name":"Microsoft Vlasta Online (Natural) - Czech (Czechia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"cs-CZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (cs-CZ, AntoninNeural)","voice_name":"Microsoft Antonín Online (Natural) - Czech (Czechia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"cy-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (cy-GB, NiaNeural)","voice_name":"Microsoft Nia Online (Natural) - Welsh (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"cy-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (cy-GB, AledNeural)","voice_name":"Microsoft Aled Online (Natural) - Welsh (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"da-DK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (da-DK, ChristelNeural)","voice_name":"Microsoft Christel Online (Natural) - Danish (Denmark)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"da-DK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (da-DK, JeppeNeural)","voice_name":"Microsoft Jeppe Online (Natural) - Danish (Denmark)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-AT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-AT, IngridNeural)","voice_name":"Microsoft Ingrid Online (Natural) - German (Austria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-AT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-AT, JonasNeural)","voice_name":"Microsoft Jonas Online (Natural) - German (Austria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-CH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-CH, LeniNeural)","voice_name":"Microsoft Leni Online (Natural) - German (Switzerland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-CH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-CH, JanNeural)","voice_name":"Microsoft Jan Online (Natural) - German (Switzerland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, SeraphinaMultilingualNeural)","voice_name":"Microsoft Seraphina Mehrsprachig Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, FlorianMultilingualNeural)","voice_name":"Microsoft Florian Mehrsprachig Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, KatjaNeural)","voice_name":"Microsoft Katja Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, ConradNeural)","voice_name":"Microsoft Conrad Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, AmalaNeural)","voice_name":"Microsoft Amala Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"de-DE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (de-DE, KillianNeural)","voice_name":"Microsoft Killian Online (Natural) - German (Germany)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"el-GR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (el-GR, AthinaNeural)","voice_name":"Microsoft Αθηνά Online (Natural) - Greek (Greece)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"el-GR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (el-GR, NestorasNeural)","voice_name":"Microsoft Νέστορας Online (Natural) - Greek (Greece)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-AU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-AU, WilliamMultilingualNeural)","voice_name":"Microsoft William Multilingual Online (Natural) - English (Australia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-AU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-AU, NatashaNeural)","voice_name":"Microsoft Natasha Online (Natural) - English (Australia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-CA, ClaraNeural)","voice_name":"Microsoft Clara Online (Natural) - English (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-CA, LiamNeural)","voice_name":"Microsoft Liam Online (Natural) - English (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-GB, SoniaNeural)","voice_name":"Microsoft Sonia Online (Natural) - English (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-GB, RyanNeural)","voice_name":"Microsoft Ryan Online (Natural) - English (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-GB, LibbyNeural)","voice_name":"Microsoft Libby Online (Natural) - English (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-GB, MaisieNeural)","voice_name":"Microsoft Maisie Online (Natural) - English (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-GB","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-GB, ThomasNeural)","voice_name":"Microsoft Thomas Online (Natural) - English (United Kingdom)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-HK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-HK, YanNeural)","voice_name":"Microsoft Yan Online (Natural) - English (Hong Kong SAR)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-HK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-HK, SamNeural)","voice_name":"Microsoft Sam Online (Natural) - English (Hong Kong SAR)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-IE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-IE, EmilyNeural)","voice_name":"Microsoft Emily Online (Natural) - English (Ireland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-IE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-IE, ConnorNeural)","voice_name":"Microsoft Connor Online (Natural) - English (Ireland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-IN, NeerjaNeural)","voice_name":"Microsoft Neerja Online (Natural) - English (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-IN, PrabhatNeural)","voice_name":"Microsoft Prabhat Online (Natural) - English (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-KE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-KE, AsiliaNeural)","voice_name":"Microsoft Asilia Online (Natural) - English (Kenya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-KE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-KE, ChilembaNeural)","voice_name":"Microsoft Chilemba Online (Natural) - English (Kenya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-NG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-NG, EzinneNeural)","voice_name":"Microsoft Ezinne Online (Natural) - English (Nigeria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-NG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-NG, AbeoNeural)","voice_name":"Microsoft Abeo Online (Natural) - English (Nigeria)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-NZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-NZ, MollyNeural)","voice_name":"Microsoft Molly Online (Natural) - English (New Zealand)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-NZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-NZ, MitchellNeural)","voice_name":"Microsoft Mitchell Online (Natural) - English (New Zealand)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-PH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-PH, RosaNeural)","voice_name":"Microsoft Rosa Online (Natural) - English (Philippines)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-PH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-PH, JamesNeural)","voice_name":"Microsoft James Online (Natural) - English (Philippines)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-SG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-SG, LunaNeural)","voice_name":"Microsoft Luna Online (Natural) - English (Singapore)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-SG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-SG, WayneNeural)","voice_name":"Microsoft Wayne Online (Natural) - English (Singapore)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-TZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-TZ, ImaniNeural)","voice_name":"Microsoft Imani Online (Natural) - English (Tanzania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-TZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-TZ, ElimuNeural)","voice_name":"Microsoft Elimu Online (Natural) - English (Tanzania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AvaMultilingualNeural)","voice_name":"Microsoft Ava Multilingual Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AndrewMultilingualNeural)","voice_name":"Microsoft Andrew Multilingual Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, EmmaMultilingualNeural)","voice_name":"Microsoft Emma Multilingual Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, BrianMultilingualNeural)","voice_name":"Microsoft Brian Multilingual Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AvaNeural)","voice_name":"Microsoft Ava Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AndrewNeural)","voice_name":"Microsoft Andrew Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, EmmaNeural)","voice_name":"Microsoft Emma Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, BrianNeural)","voice_name":"Microsoft Brian Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, JennyNeural)","voice_name":"Microsoft Jenny Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, GuyNeural)","voice_name":"Microsoft Guy Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)","voice_name":"Microsoft Aria Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, AnaNeural)","voice_name":"Microsoft Ana Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, ChristopherNeural)","voice_name":"Microsoft Christopher Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, EricNeural)","voice_name":"Microsoft Eric Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, MichelleNeural)","voice_name":"Microsoft Michelle Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, RogerNeural)","voice_name":"Microsoft Roger Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-US, SteffanNeural)","voice_name":"Microsoft Steffan Online (Natural) - English (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-ZA, LeahNeural)","voice_name":"Microsoft Leah Online (Natural) - English (South Africa)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"en-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (en-ZA, LukeNeural)","voice_name":"Microsoft Luke Online (Natural) - English (South Africa)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-AR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-AR, ElenaNeural)","voice_name":"Microsoft Elena Online (Natural) - Spanish (Argentina)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-AR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-AR, TomasNeural)","voice_name":"Microsoft Tomas Online (Natural) - Spanish (Argentina)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-BO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-BO, SofiaNeural)","voice_name":"Microsoft Sofia Online (Natural) - Spanish (Bolivia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-BO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-BO, MarceloNeural)","voice_name":"Microsoft Marcelo Online (Natural) - Spanish (Bolivia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CL, CatalinaNeural)","voice_name":"Microsoft Catalina Online (Natural) - Spanish (Chile)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CL, LorenzoNeural)","voice_name":"Microsoft Lorenzo Online (Natural) - Spanish (Chile)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CO, SalomeNeural)","voice_name":"Microsoft Salome Online (Natural) - Spanish (Colombia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CO, GonzaloNeural)","voice_name":"Microsoft Gonzalo Online (Natural) - Spanish (Colombia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CR, MariaNeural)","voice_name":"Microsoft María Online (Natural) - Spanish (Costa Rica)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CR, JuanNeural)","voice_name":"Microsoft Juan Online (Natural) - Spanish (Costa Rica)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CU, BelkysNeural)","voice_name":"Microsoft Belkys Online (Natural) - Spanish (Cuba)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-CU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-CU, ManuelNeural)","voice_name":"Microsoft Manuel Online (Natural) - Spanish (Cuba)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-DO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-DO, RamonaNeural)","voice_name":"Microsoft Ramona Online (Natural) - Spanish (Dominican Republic)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-DO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-DO, EmilioNeural)","voice_name":"Microsoft Emilio Online (Natural) - Spanish (Dominican Republic)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-EC","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-EC, AndreaNeural)","voice_name":"Microsoft Andrea Online (Natural) - Spanish (Ecuador)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-EC","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-EC, LuisNeural)","voice_name":"Microsoft Luis Online (Natural) - Spanish (Ecuador)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-ES, ElviraNeural)","voice_name":"Microsoft Elvira Online (Natural) - Spanish (Spain)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-ES, AlvaroNeural)","voice_name":"Microsoft Álvaro Online (Natural) - Spanish (Spain)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-ES, XimenaNeural)","voice_name":"Microsoft Ximena Online (Natural) - Spanish (Spain)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-GQ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-GQ, TeresaNeural)","voice_name":"Microsoft Teresa Online (Natural) - Spanish (Equatorial Guinea)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-GQ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-GQ, JavierNeural)","voice_name":"Microsoft Javier Online (Natural) - Spanish (Equatorial Guinea)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-GT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-GT, MartaNeural)","voice_name":"Microsoft Marta Online (Natural) - Spanish (Guatemala)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-GT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-GT, AndresNeural)","voice_name":"Microsoft Andrés Online (Natural) - Spanish (Guatemala)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-HN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-HN, KarlaNeural)","voice_name":"Microsoft Karla Online (Natural) - Spanish (Honduras)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-HN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-HN, CarlosNeural)","voice_name":"Microsoft Carlos Online (Natural) - Spanish (Honduras)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-MX","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-MX, DaliaNeural)","voice_name":"Microsoft Dalia Online (Natural) - Spanish (Mexico)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-MX","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-MX, JorgeNeural)","voice_name":"Microsoft Jorge Online (Natural) - Spanish (Mexico)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-NI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-NI, YolandaNeural)","voice_name":"Microsoft Yolanda Online (Natural) - Spanish (Nicaragua)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-NI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-NI, FedericoNeural)","voice_name":"Microsoft Federico Online (Natural) - Spanish (Nicaragua)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PA, MargaritaNeural)","voice_name":"Microsoft Margarita Online (Natural) - Spanish (Panama)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PA, RobertoNeural)","voice_name":"Microsoft Roberto Online (Natural) - Spanish (Panama)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PE, CamilaNeural)","voice_name":"Microsoft Camila Online (Natural) - Spanish (Peru)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PE, AlexNeural)","voice_name":"Microsoft Alex Online (Natural) - Spanish (Peru)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PR, KarinaNeural)","voice_name":"Microsoft Karina Online (Natural) - Spanish (Puerto Rico)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PR, VictorNeural)","voice_name":"Microsoft Víctor Online (Natural) - Spanish (Puerto Rico)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PY, TaniaNeural)","voice_name":"Microsoft Tania Online (Natural) - Spanish (Paraguay)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-PY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-PY, MarioNeural)","voice_name":"Microsoft Mario Online (Natural) - Spanish (Paraguay)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-SV","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-SV, LorenaNeural)","voice_name":"Microsoft Lorena Online (Natural) - Spanish (El Salvador)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-SV","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-SV, RodrigoNeural)","voice_name":"Microsoft Rodrigo Online (Natural) - Spanish (El Salvador)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-US, PalomaNeural)","voice_name":"Microsoft Paloma Online (Natural) - Spanish (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-US","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-US, AlonsoNeural)","voice_name":"Microsoft Alonso Online (Natural) - Spanish (United States)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-UY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-UY, ValentinaNeural)","voice_name":"Microsoft Valentina Online (Natural) - Spanish (Uruguay)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-UY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-UY, MateoNeural)","voice_name":"Microsoft Mateo Online (Natural) - Spanish (Uruguay)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-VE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-VE, PaolaNeural)","voice_name":"Microsoft Paola Online (Natural) - Spanish (Venezuela)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"es-VE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (es-VE, SebastianNeural)","voice_name":"Microsoft Sebastián Online (Natural) - Spanish (Venezuela)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"et-EE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (et-EE, AnuNeural)","voice_name":"Microsoft Anu Online (Natural) - Estonian (Estonia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"et-EE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (et-EE, KertNeural)","voice_name":"Microsoft Kert Online (Natural) - Estonian (Estonia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fa-IR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fa-IR, DilaraNeural)","voice_name":"Microsoft دلارا Online (Natural) - Persian (Iran)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fa-IR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fa-IR, FaridNeural)","voice_name":"Microsoft فرید Online (Natural) - Persian (Iran)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fi-FI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fi-FI, HarriNeural)","voice_name":"Microsoft Harri Online (Natural) - Finnish (Finland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fi-FI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fi-FI, NooraNeural)","voice_name":"Microsoft Noora Online (Natural) - Finnish (Finland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fil-PH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fil-PH, BlessicaNeural)","voice_name":"Microsoft Blessica Online (Natural) - Filipino (Philippines)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fil-PH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fil-PH, AngeloNeural)","voice_name":"Microsoft Angelo Online (Natural) - Filipino (Philippines)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-BE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-BE, CharlineNeural)","voice_name":"Microsoft Charline Online (Natural) - French (Belgium)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-BE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-BE, GerardNeural)","voice_name":"Microsoft Gerard Online (Natural) - French (Belgium)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CA, SylvieNeural)","voice_name":"Microsoft Sylvie Online (Natural) - French (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CA, JeanNeural)","voice_name":"Microsoft Jean Online (Natural) - French (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CA, AntoineNeural)","voice_name":"Microsoft Antoine Online (Natural) - French (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CA, ThierryNeural)","voice_name":"Microsoft Thierry Online (Natural) - French (Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CH, ArianeNeural)","voice_name":"Microsoft Ariane Online (Natural) - French (Switzerland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-CH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-CH, FabriceNeural)","voice_name":"Microsoft Fabrice Online (Natural) - French (Switzerland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-FR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-FR, DeniseNeural)","voice_name":"Microsoft Denise Online (Natural) - French (France)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-FR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-FR, HenriNeural)","voice_name":"Microsoft Henri Online (Natural) - French (France)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-FR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-FR, VivienneMultilingualNeural)","voice_name":"Microsoft Vivienne Multilingue Online (Natural) - French (France)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-FR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-FR, RemyMultilingualNeural)","voice_name":"Microsoft Rémy Multilingue Online (Natural) - French (France)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"fr-FR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (fr-FR, EloiseNeural)","voice_name":"Microsoft Eloise Online (Natural) - French (France)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ga-IE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ga-IE, OrlaNeural)","voice_name":"Microsoft Orla Online (Natural) - Irish (Ireland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ga-IE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ga-IE, ColmNeural)","voice_name":"Microsoft Colm Online (Natural) - Irish (Ireland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"gl-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (gl-ES, SabelaNeural)","voice_name":"Microsoft Sabela Online (Natural) - Galician"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"gl-ES","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (gl-ES, RoiNeural)","voice_name":"Microsoft Roi Online (Natural) - Galician"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"gu-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (gu-IN, DhwaniNeural)","voice_name":"Microsoft ધ્વની Online (Natural) - Gujarati (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"gu-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (gu-IN, NiranjanNeural)","voice_name":"Microsoft નિરંજન Online (Natural) - Gujarati (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"he-IL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (he-IL, HilaNeural)","voice_name":"Microsoft הילה Online (Natural) - Hebrew (Israel)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"he-IL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (he-IL, AvriNeural)","voice_name":"Microsoft אברי Online (Natural) - Hebrew (Israel)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hi-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hi-IN, SwaraNeural)","voice_name":"Microsoft स्वरा Online (Natural) - Hindi (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hi-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hi-IN, MadhurNeural)","voice_name":"Microsoft मधुर Online (Natural) - Hindi (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hr-HR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hr-HR, GabrijelaNeural)","voice_name":"Microsoft Gabrijela Online (Natural) - Croatian (Croatia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hr-HR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hr-HR, SreckoNeural)","voice_name":"Microsoft Srećko Online (Natural) - Croatian (Croatia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hu-HU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hu-HU, NoemiNeural)","voice_name":"Microsoft Noémi Online (Natural) - Hungarian (Hungary)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"hu-HU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (hu-HU, TamasNeural)","voice_name":"Microsoft Tamás Online (Natural) - Hungarian (Hungary)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"id-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (id-ID, GadisNeural)","voice_name":"Microsoft Gadis Online (Natural) - Indonesian (Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"id-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (id-ID, ArdiNeural)","voice_name":"Microsoft Ardi Online (Natural) - Indonesian (Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"is-IS","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (is-IS, GudrunNeural)","voice_name":"Microsoft Guðrún Online (Natural) - Icelandic (Iceland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"is-IS","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (is-IS, GunnarNeural)","voice_name":"Microsoft Gunnar Online (Natural) - Icelandic (Iceland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"it-IT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (it-IT, ElsaNeural)","voice_name":"Microsoft Elsa Online (Natural) - Italian (Italy)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"it-IT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (it-IT, IsabellaNeural)","voice_name":"Microsoft Isabella Online (Natural) - Italian (Italy)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"it-IT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (it-IT, DiegoNeural)","voice_name":"Microsoft Diego Online (Natural) - Italian (Italy)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"it-IT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (it-IT, GiuseppeMultilingualNeural)","voice_name":"Microsoft Giuseppe Multilingual Online (Natural) - Italian (Italy)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"iu-Cans-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (iu-Cans-CA, SiqiniqNeural)","voice_name":"Microsoft ᓯᕿᓂᖅ Online (Natural) - Inuktitut (Syllabics, Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"iu-Cans-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (iu-Cans-CA, TaqqiqNeural)","voice_name":"Microsoft ᑕᖅᑭᖅ Online (Natural) - Inuktitut (Syllabics, Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"iu-Latn-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (iu-Latn-CA, SiqiniqNeural)","voice_name":"Microsoft ᓯᕿᓂᖅ Online (Natural) - Inuktitut (Latin, Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"iu-Latn-CA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (iu-Latn-CA, TaqqiqNeural)","voice_name":"Microsoft ᑕᖅᑭᖅ Online (Natural) - Inuktitut (Latin, Canada)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ja-JP","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)","voice_name":"Microsoft 七海 Online (Natural) - Japanese (Japan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ja-JP","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)","voice_name":"Microsoft 圭太 Online (Natural) - Japanese (Japan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"jv-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (jv-ID, SitiNeural)","voice_name":"Microsoft Siti Online (Natural) - Javanese (Latin, Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"jv-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (jv-ID, DimasNeural)","voice_name":"Microsoft Dimas Online (Natural) - Javanese (Latin, Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ka-GE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ka-GE, EkaNeural)","voice_name":"Microsoft ეკა Online (Natural) - Georgian (Georgia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ka-GE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ka-GE, GiorgiNeural)","voice_name":"Microsoft გიორგი Online (Natural) - Georgian (Georgia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"kk-KZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (kk-KZ, AigulNeural)","voice_name":"Microsoft Айгүл Online (Natural) - Kazakh (Kazakhstan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"kk-KZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (kk-KZ, DauletNeural)","voice_name":"Microsoft Дәулет Online (Natural) - Kazakh (Kazakhstan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"km-KH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (km-KH, SreymomNeural)","voice_name":"Microsoft ស្រីមុំ Online (Natural) - Khmer (Cambodia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"km-KH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (km-KH, PisethNeural)","voice_name":"Microsoft ពិសិដ្ឋ Online (Natural) - Khmer (Cambodia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"kn-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (kn-IN, SapnaNeural)","voice_name":"Microsoft ಸಪ್ನಾ Online (Natural) - Kannada (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"kn-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (kn-IN, GaganNeural)","voice_name":"Microsoft ಗಗನ್ Online (Natural) - Kannada (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ko-KR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ko-KR, SunHiNeural)","voice_name":"Microsoft 선히 Online (Natural) - Korean (Korea)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ko-KR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ko-KR, InJoonNeural)","voice_name":"Microsoft 인준 Online (Natural) - Korean (Korea)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ko-KR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ko-KR, HyunsuMultilingualNeural)","voice_name":"Microsoft Hyunsu Multilingual Online (Natural) - Korean (Korea)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lo-LA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lo-LA, KeomanyNeural)","voice_name":"Microsoft ແກ້ວມະນີ Online (Natural) - Lao (Laos)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lo-LA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lo-LA, ChanthavongNeural)","voice_name":"Microsoft ຈັນທະວົງ Online (Natural) - Lao (Laos)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lt-LT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lt-LT, OnaNeural)","voice_name":"Microsoft Ona Online (Natural) - Lithuanian (Lithuania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lt-LT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lt-LT, LeonasNeural)","voice_name":"Microsoft Leonas Online (Natural) - Lithuanian (Lithuania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lv-LV","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lv-LV, EveritaNeural)","voice_name":"Microsoft Everita Online (Natural) - Latvian (Latvia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"lv-LV","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (lv-LV, NilsNeural)","voice_name":"Microsoft Nils Online (Natural) - Latvian (Latvia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mk-MK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mk-MK, MarijaNeural)","voice_name":"Microsoft Марија Online (Natural) - Macedonian (North Macedonia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mk-MK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mk-MK, AleksandarNeural)","voice_name":"Microsoft Александар Online (Natural) - Macedonian (North Macedonia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ml-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ml-IN, SobhanaNeural)","voice_name":"Microsoft ശോഭന Online (Natural) - Malayalam (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ml-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ml-IN, MidhunNeural)","voice_name":"Microsoft മിഥുൻ Online (Natural) - Malayalam (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mn-MN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mn-MN, YesuiNeural)","voice_name":"Microsoft Есүй Online (Natural) - Mongolian (Mongolia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mn-MN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mn-MN, BataaNeural)","voice_name":"Microsoft Батаа Online (Natural) - Mongolian (Mongolia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mr-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mr-IN, AarohiNeural)","voice_name":"Microsoft आरोही Online (Natural) - Marathi (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mr-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mr-IN, ManoharNeural)","voice_name":"Microsoft मनोहर Online (Natural) - Marathi (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ms-MY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ms-MY, YasminNeural)","voice_name":"Microsoft Yasmin Online (Natural) - Malay (Malaysia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ms-MY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ms-MY, OsmanNeural)","voice_name":"Microsoft Osman Online (Natural) - Malay (Malaysia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mt-MT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mt-MT, GraceNeural)","voice_name":"Microsoft Grace Online (Natural) - Maltese (Malta)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"mt-MT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (mt-MT, JosephNeural)","voice_name":"Microsoft Joseph Online (Natural) - Maltese (Malta)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"my-MM","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (my-MM, NilarNeural)","voice_name":"Microsoft နီလာ Online (Natural) - Burmese (Myanmar)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"my-MM","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (my-MM, ThihaNeural)","voice_name":"Microsoft သီဟ Online (Natural) - Burmese (Myanmar)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nb-NO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nb-NO, PernilleNeural)","voice_name":"Microsoft Pernille Online (Natural) - Norwegian Bokmål (Norway)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nb-NO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nb-NO, FinnNeural)","voice_name":"Microsoft Finn Online (Natural) - Norwegian Bokmål (Norway)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ne-NP","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ne-NP, HemkalaNeural)","voice_name":"Microsoft हेमकला Online (Natural) - Nepali (Nepal)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ne-NP","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ne-NP, SagarNeural)","voice_name":"Microsoft सागर Online (Natural) - Nepali (Nepal)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nl-BE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nl-BE, DenaNeural)","voice_name":"Microsoft Dena Online (Natural) - Dutch (Belgium)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nl-BE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nl-BE, ArnaudNeural)","voice_name":"Microsoft Arnaud Online (Natural) - Dutch (Belgium)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nl-NL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nl-NL, FennaNeural)","voice_name":"Microsoft Fenna Online (Natural) - Dutch (Netherlands)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nl-NL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nl-NL, MaartenNeural)","voice_name":"Microsoft Maarten Online (Natural) - Dutch (Netherlands)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"nl-NL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (nl-NL, ColetteNeural)","voice_name":"Microsoft Colette Online (Natural) - Dutch (Netherlands)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pl-PL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pl-PL, MarekNeural)","voice_name":"Microsoft Marek Online (Natural) - Polish (Poland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pl-PL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pl-PL, ZofiaNeural)","voice_name":"Microsoft Zofia Online (Natural) - Polish (Poland)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ps-AF","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ps-AF, LatifaNeural)","voice_name":"Microsoft لطيفه Online (Natural) - Pashto (Afghanistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ps-AF","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ps-AF, GulNawazNeural)","voice_name":"Microsoft ګل نواز Online (Natural) - Pashto (Afghanistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pt-BR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pt-BR, FranciscaNeural)","voice_name":"Microsoft Francisca Online (Natural) - Portuguese (Brazil)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pt-BR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pt-BR, AntonioNeural)","voice_name":"Microsoft Antônio Online (Natural) - Portuguese (Brazil)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pt-BR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pt-BR, ThalitaMultilingualNeural)","voice_name":"Microsoft Thalita multilíngue Online (Natural) - Portuguese (Brazil)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pt-PT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pt-PT, RaquelNeural)","voice_name":"Microsoft Raquel Online (Natural) - Portuguese (Portugal)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"pt-PT","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (pt-PT, DuarteNeural)","voice_name":"Microsoft Duarte Online (Natural) - Portuguese (Portugal)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ro-RO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ro-RO, AlinaNeural)","voice_name":"Microsoft Alina Online (Natural) - Romanian (Romania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ro-RO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ro-RO, EmilNeural)","voice_name":"Microsoft Emil Online (Natural) - Romanian (Romania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ru-RU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ru-RU, SvetlanaNeural)","voice_name":"Microsoft Светлана Online (Natural) - Russian (Russia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ru-RU","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ru-RU, DmitryNeural)","voice_name":"Microsoft Дмитрий Online (Natural) - Russian (Russia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"si-LK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (si-LK, ThiliniNeural)","voice_name":"Microsoft තිළිණි Online (Natural) - Sinhala (Sri Lanka)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"si-LK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (si-LK, SameeraNeural)","voice_name":"Microsoft සමීර Online (Natural) - Sinhala (Sri Lanka)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sk-SK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sk-SK, ViktoriaNeural)","voice_name":"Microsoft Viktória Online (Natural) - Slovak (Slovakia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sk-SK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sk-SK, LukasNeural)","voice_name":"Microsoft Lukáš Online (Natural) - Slovak (Slovakia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sl-SI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sl-SI, PetraNeural)","voice_name":"Microsoft Petra Online (Natural) - Slovenian (Slovenia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sl-SI","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sl-SI, RokNeural)","voice_name":"Microsoft Rok Online (Natural) - Slovenian (Slovenia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"so-SO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (so-SO, UbaxNeural)","voice_name":"Microsoft Ubax Online (Natural) - Somali (Somalia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"so-SO","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (so-SO, MuuseNeural)","voice_name":"Microsoft Muuse Online (Natural) - Somali (Somalia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sq-AL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sq-AL, AnilaNeural)","voice_name":"Microsoft Anila Online (Natural) - Albanian (Albania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sq-AL","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sq-AL, IlirNeural)","voice_name":"Microsoft Ilir Online (Natural) - Albanian (Albania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sr-RS","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sr-RS, SophieNeural)","voice_name":"Microsoft Софија Online (Natural) - Serbian (Cyrillic, Serbia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sr-RS","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sr-RS, NicholasNeural)","voice_name":"Microsoft Никола Online (Natural) - Serbian (Cyrillic, Serbia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"su-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (su-ID, TutiNeural)","voice_name":"Microsoft Tuti Online (Natural) - Sundanese (Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"su-ID","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (su-ID, JajangNeural)","voice_name":"Microsoft Jajang Online (Natural) - Sundanese (Indonesia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sv-SE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sv-SE, SofieNeural)","voice_name":"Microsoft Sofie Online (Natural) - Swedish (Sweden)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sv-SE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sv-SE, MattiasNeural)","voice_name":"Microsoft Mattias Online (Natural) - Swedish (Sweden)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sw-KE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sw-KE, ZuriNeural)","voice_name":"Microsoft Zuri Online (Natural) - Swahili (Kenya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sw-KE","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sw-KE, RafikiNeural)","voice_name":"Microsoft Rafiki Online (Natural) - Swahili (Kenya)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sw-TZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sw-TZ, RehemaNeural)","voice_name":"Microsoft Rehema Online (Natural) - Swahili (Tanzania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"sw-TZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (sw-TZ, DaudiNeural)","voice_name":"Microsoft Daudi Online (Natural) - Swahili (Tanzania)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-IN, PallaviNeural)","voice_name":"Microsoft பல்லவி Online (Natural) - Tamil (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-IN, ValluvarNeural)","voice_name":"Microsoft வள்ளுவர் Online (Natural) - Tamil (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-LK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-LK, SaranyaNeural)","voice_name":"Microsoft சரண்யா Online (Natural) - Tamil (Sri Lanka)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-LK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-LK, KumarNeural)","voice_name":"Microsoft குமார் Online (Natural) - Tamil (Sri Lanka)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-MY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-MY, KaniNeural)","voice_name":"Microsoft கனி Online (Natural) - Tamil (Malaysia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-MY","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-MY, SuryaNeural)","voice_name":"Microsoft சூர்யா Online (Natural) - Tamil (Malaysia)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-SG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-SG, VenbaNeural)","voice_name":"Microsoft வெண்பா Online (Natural) - Tamil (Singapore)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ta-SG","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ta-SG, AnbuNeural)","voice_name":"Microsoft அன்பு Online (Natural) - Tamil (Singapore)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"te-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (te-IN, ShrutiNeural)","voice_name":"Microsoft శ్రుతి Online (Natural) - Telugu (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"te-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (te-IN, MohanNeural)","voice_name":"Microsoft మోహన్ Online (Natural) - Telugu (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"th-TH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (th-TH, PremwadeeNeural)","voice_name":"Microsoft เปรมวดี Online (Natural) - Thai (Thailand)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"th-TH","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (th-TH, NiwatNeural)","voice_name":"Microsoft นิวัฒน์ Online (Natural) - Thai (Thailand)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"tr-TR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (tr-TR, EmelNeural)","voice_name":"Microsoft Emel Online (Natural) - Turkish (Türkiye)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"tr-TR","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (tr-TR, AhmetNeural)","voice_name":"Microsoft Ahmet Online (Natural) - Turkish (Türkiye)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"uk-UA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (uk-UA, PolinaNeural)","voice_name":"Microsoft Поліна Online (Natural) - Ukrainian (Ukraine)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"uk-UA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (uk-UA, OstapNeural)","voice_name":"Microsoft Остап Online (Natural) - Ukrainian (Ukraine)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ur-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ur-IN, GulNeural)","voice_name":"Microsoft گل Online (Natural) - Urdu (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ur-IN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ur-IN, SalmanNeural)","voice_name":"Microsoft سلمان Online (Natural) - Urdu (India)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ur-PK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ur-PK, UzmaNeural)","voice_name":"Microsoft عظمیٰ Online (Natural) - Urdu (Pakistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"ur-PK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (ur-PK, AsadNeural)","voice_name":"Microsoft اسد Online (Natural) - Urdu (Pakistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"uz-UZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (uz-UZ, MadinaNeural)","voice_name":"Microsoft Madina Online (Natural) - Uzbek (Latin, Uzbekistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"uz-UZ","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (uz-UZ, SardorNeural)","voice_name":"Microsoft Sardor Online (Natural) - Uzbek (Latin, Uzbekistan)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"vi-VN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (vi-VN, HoaiMyNeural)","voice_name":"Microsoft Hoài My Online (Natural) - Vietnamese (Vietnam)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"vi-VN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (vi-VN, NamMinhNeural)","voice_name":"Microsoft Nam Minh Online (Natural) - Vietnamese (Vietnam)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)","voice_name":"Microsoft 晓晓 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiNeural)","voice_name":"Microsoft 云希 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, YunjianNeural)","voice_name":"Microsoft 云健 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoyiNeural)","voice_name":"Microsoft 晓伊 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, YunyangNeural)","voice_name":"Microsoft 云扬 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiaNeural)","voice_name":"Microsoft 云夏 Online (Natural) - Chinese (Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN-liaoning","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN-liaoning, XiaobeiNeural)","voice_name":"Microsoft 晓北 Online (Natural) - Chinese (Northeastern Mandarin, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-CN-shaanxi","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-CN-shaanxi, XiaoniNeural)","voice_name":"Microsoft 晓妮 Online (Natural) - Chinese (Zhongyuan Mandarin Shaanxi, Simplified)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-HK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-HK, HiuMaanNeural)","voice_name":"Microsoft 曉曼 Online (Natural) - Chinese (Cantonese, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-HK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-HK, WanLungNeural)","voice_name":"Microsoft 雲龍 Online (Natural) - Chinese (Cantonese, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-HK","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-HK, HiuGaaiNeural)","voice_name":"Microsoft 曉佳 Online (Natural) - Chinese (Cantonese, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-TW","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoChenNeural)","voice_name":"Microsoft 曉臻 Online (Natural) - Chinese (Taiwanese Mandarin, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-TW","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-TW, YunJheNeural)","voice_name":"Microsoft 雲哲 Online (Natural) - Chinese (Taiwanese Mandarin, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zh-TW","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoYuNeural)","voice_name":"Microsoft 曉雨 Online (Natural) - Chinese (Taiwanese Mandarin, Traditional)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zu-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zu-ZA, ThandoNeural)","voice_name":"Microsoft Thando Online (Natural) - Zulu (South Africa)"},{"codec":"audio-24khz-48kbitrate-mono-mp3","event_types":["end","error","start","word"],"lang":"zu-ZA","remote":true,"server_name":"Microsoft Server Speech Text to Speech Voice (zu-ZA, ThembaNeural)","voice_name":"Microsoft Themba Online (Natural) - Zulu (South Africa)"}]},"version":"1.0"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\microsoft_voices","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"mhjfbmdgcfjbbpaeojofohoefgiehjai":{"account_extension_type":0,"active_permissions":{"api":["contentSettings","fileSystem","fileSystem.write","metricsPrivate","tabs","resourcesPrivate","pdfViewerPrivate","fileSystem.readFullPath","errorReporting","edgeLearningToolsPrivate","fileSystem.getCurrentEntry","edgePdfPrivate","edgeCertVerifierPrivate","edgeFeedbackPrivate"],"explicit_host":["edge://resources/*","edge://webui-test/*"],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":[],"first_install_time":"13434014280847585","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280847585","location":5,"manifest":{"content_security_policy":"script-src 'self' 'wasm-eval' blob: filesystem: chrome://resources chrome://webui-test; object-src * blob: externalfile: file: filesystem: data:; trusted-types edge-internal fast-html pdf-url edge-pdf-static-policy edge_common;","description":"","incognito":"split","key":"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN6hM0rsDYGbzQPQfOygqlRtQgKUXMfnSjhIBL7LnReAVBEd7ZmKtyN2qmSasMl4HZpMhVe2rPWVVwBDl6iyNE/Kok6E6v6V3vCLGsOpQAuuNVye/3QxzIldzG/jQAdWZiyXReRVapOhZtLjGfywCvlWq7Sl/e3sbc0vWybSDI2QIDAQAB","manifest_version":2,"mime_types":["application/pdf"],"mime_types_handler":"edge_pdf/index.html","name":"Microsoft Edge PDF Viewer","offline_enabled":true,"permissions":["errorReporting","chrome://resources/","chrome://webui-test/","contentSettings","edgeFeedbackPrivate","metricsPrivate","edgeCertVerifierPrivate","edgeLearningToolsPrivate","edgePdfPrivate","pdfViewerPrivate","resourcesPrivate","tabs",{"fileSystem":["write","readFullPath","getCurrentEntry"]}],"version":"1","web_accessible_resources":["pdf_embedder.css"]},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\edge_pdf","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"ncbjelpjchkpbikbpkcchkhkblodoama":{"account_extension_type":0,"active_permissions":{"api":[],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":[],"first_install_time":"13434014280856150","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280856150","location":5,"manifest":{"background":{"persistent":false,"scripts":["background.js"]},"externally_connectable":{"matches":["https://*.teams.cloud.microsoft/*","https://*.teams.cloud.microsoft.com/*","https://*.teams.microsoft.com/*","https://*.skype.com/*","https://*.teams.live.com/*"]},"incognito":"split","key":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtAdFAR3ckd5c7G8VSzUj4Ltt/QRInUOD00StG95LweksGcLBlFlYL46cHFVgHHj1gmzcpBtgsURdcrAC3V8yiE7GY4wtpOP+9l+adUGR+cyOG0mw9fLjyH+2Il0QqktsNXzkNiE1ogW4l0h4+PJc262j0vtm4hBzMvR0QScFWcAIcAErlUiWTt4jefXCAYqubV99ed5MvVMWBxe97wOa9hYwAhbCminOepA4RRTg9eyi0TiuHpq/bNI8C5qZgKIQNBAjgiFBaIx9hiMBFlK4NHUbFdgY6Qp/hSCMNurctwz1jpsXEnT4eHg1YWXfquoH8s4swIjkFCMBF6Ejc3cUkQIDAQAB","manifest_version":2,"name":"WebRTC Internals Extension","permissions":["webrtcInternalsPrivate"],"version":"2.0.4"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\webrtc_internals","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false},"ndcpkimcihhghdcddljkfmmjccdmcmof":{"account_extension_type":0,"active_permissions":{"api":["metricsPrivate","tabs","webNavigation","errorReporting","edgeCopilotBridgePrivate","edgeAuthPrivate","edgeNtpSettingsPrivate","edgeCopilotToolActionsPrivate","edgeCopilotCoworkPrivate","edgeCopilotBrowseConsentPrivate"],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"first_install_time":"13434014280851804","from_webstore":false,"has_started_service_worker":true,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280851804","location":5,"manifest":{"background":{"service_worker":"background.rollup.js","type":"module"},"description":"Bridge between Copilot web experience and the Edge browser process.","edge_agent_internals_enabled":false,"edge_browse_consent_origins":[],"edge_cowork_enabled":true,"edge_cowork_origins":["https://m365.cloud.microsoft","https://scuprodprv.m365.cloud.microsoft","https://www.microsoft365.com","https://cr.m365copilotapp.svc.cloud.microsoft","https://copilot.cloud.microsoft","https://copilot.com"],"edge_tools_origins":["https://m365.cloud.microsoft","https://scuprodprv.m365.cloud.microsoft","https://www.microsoft365.com","https://cr.m365copilotapp.svc.cloud.microsoft","https://copilot.cloud.microsoft","https://copilot.com"],"externally_connectable":{"matches":["https://m365.cloud.microsoft/*","https://scuprodprv.m365.cloud.microsoft/*","https://www.microsoft365.com/*","https://cr.m365copilotapp.svc.cloud.microsoft/*","https://copilot.cloud.microsoft/*","https://copilot.com/*"]},"key":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApVTPYSA1GZWjkvR+tnEN9TCNTAsQiWGWwZTuooQhuxvocQIb7W8lxSdat3o6hz2JFuEtErX3R4mhWqIXax4lPMn0GOLEfh1MNlzT3KfcmBh2HFVtDoxHHgLHIWUrv3YrQlAA+FSS4oAXRkuESKpjJVHLzZe/QkFa5txD4xyYM3tGWgKivueqJX0Kh9q5YWHOBQrW3AHTpD/yUzyv/NszUSOCJ/8A3dEJhSqwPzOLASQfeNafdibhckB+7C0h5stR5Sm+TcJHx1/Wy5ULXW1P89H/3iSvOd8udX1zf13csFXVNxcGk74JA3RYPOOuHddMv4VVQrVp+WWT14yhhVYQPwIDAQAB","manifest_version":3,"message_serialization":"structured_clone","name":"Edge Copilot Bridge","permissions":["edgeAuthPrivate","edgeCopilotBridgePrivate","edgeCopilotBrowseConsentPrivate","edgeCopilotCoworkPrivate","edgeCopilotToolActionsPrivate","edgeNtpSettingsPrivate","errorReporting","metricsPrivate","tabs","webNavigation"],"version":"153.0.4234.21"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\edge_copilot_bridge_extension","preferences":{},"regular_only_preferences":{},"service_worker_registration_info":{"version":"153.0.4234.21"},"serviceworkerevents":["copilotBridgePrivate.chat.onAskCopilot","copilotBridgePrivate.ciq.interactions.onSendPromptConfig","copilotBridgePrivate.notify.onNotification","copilotBridgePrivate.qvMessaging.onHandoffLiveModeConversation","copilotBridgePrivate.qvMessaging.onSendQuickViewPrompt","copilotBridgePrivate.qvMessaging.onUpdateAppMode","runtime.onConnectExternal"],"was_installed_by_default":false,"was_installed_by_oem":false},"nkeimhogjdpnpccoofpliimaahmaaome":{"account_extension_type":0,"active_permissions":{"api":["processes","webrtcLoggingPrivate","system.cpu","enterprise.hardwarePlatform"],"explicit_host":[],"manifest_permissions":[],"scriptable_host":[]},"commands":{},"content_settings":[],"creation_flags":1,"disable_reasons":[],"events":["runtime.onConnectExternal"],"first_install_time":"13434014280850525","from_webstore":false,"incognito_content_settings":[],"incognito_preferences":{},"last_loaded_browser_version":"153.0.4234.32","last_update_time":"13434014280850525","location":5,"manifest":{"background":{"page":"background.html","persistent":false},"externally_connectable":{"matches":["https://*.teams.cloud.microsoft/*","https://*.teams.cloud.microsoft.com/*","https://*.teams.microsoft.com/*","https://*.teams.live.com/*","https://*.skype.com/*"]},"incognito":"split","key":"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAQt2ZDdPfoSe/JI6ID5bgLHRCnCu9T36aYczmhw/tnv6QZB2I6WnOCMZXJZlRdqWc7w9jo4BWhYS50Vb4weMfh/I0On7VcRwJUgfAxW2cHB+EkmtI1v4v/OU24OqIa1Nmv9uRVeX0GjhQukdLNhAE6ACWooaf5kqKlCeK+1GOkQIDAQAB","manifest_version":2,"name":"WebRTC Extension","permissions":["enterprise.hardwarePlatform","processes","system.cpu","webrtcLoggingPrivate"],"version":"4.3.26"},"path":"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\153.0.4234.32\\resources\\hangout_services","preferences":{},"regular_only_preferences":{},"was_installed_by_default":false,"was_installed_by_oem":false}}},"protection":{"macs":{"browser":{"show_home_button":"A807B5BE54F1F9EA20941A899F1F6A772D3ACD1E63D0E43709668C303F89093F","show_home_button_encrypted_hash":"djEwZoCS5NCwLb1Vv9zI5AXupjdDoGBaaOafiQQJ/1+21kRYM3VegorizI0pHXoczTi/DkFcWqBDO1ru9aBf"},"default_search_provider_data":{"template_url_data":"132624E6257F89C768C0BCB0F9CD57D533C9BCD15F571F52BD0BCD0C95459F91","template_url_data_encrypted_hash":"djEwH1sed388HePaLStqnjlKZS5tIf14g7I9/Hra6emDa2Qb1eGx/cfUAMrJlZfW/VXnsln6RvM+c0/hrbHa"},"edge":{"services":{"account_id":"3D237FC3F89C46258F66C1E66ED04FB13D802D9C09D623410A61C30C620FFC4F","account_id_encrypted_hash":"djEwmhEP3RYEcrAiHDyRQ7YjLy2EgOPUk+TX3N5Ldq+bjV+VeXVph90e6G23P3DxEbJlYfAFQRL8MXv4hAWt","last_username":"CB212DC516B0FC816E080ECB80052E4E3B6E1DCB6B2B8871C280FDF531FF73DD","last_username_encrypted_hash":"djEwA/8oy2dLuKBQQoVT6nCCBKtCfTNOZoJ+VjR8KPLm1TzXdzBS3XcvZ1mQm/fG0kkaorvQ4Mbwue6sRSr2"}},"enterprise_signin":{"policy_recovery_token":"F94D727938F249DAE2AF695C4325224B645DEF6DD4D102254924E2EEA85610FC","policy_recovery_token_encrypted_hash":"djEw+bI5u7eZJspqe9HtN5BBZNZ8NZphXi/PWzXHyyYAELKDzjWiDMp8UEPP++0S44ObvpGkNGl4BvYq4FsK"},"extensions":{"install":{"initiallist":"25F9449695390D1BBDFBC459C99AABB93D1073820C8C0E10E68833AACFCF038F","initiallist_encrypted_hash":"djEwDTEq4WdibVa7vunLKAhQVX7QJN96msmB9T2ntFYrNAhxRuIchW1pCmFMyYieynDjRGHsuMJM3HbvrRnM","initialprovidername":"AF8813354FD719CE9686E3828F96F5C562E4684236824295490191FF1CBCCBB4","initialprovidername_encrypted_hash":"djEweqmZD4DmVCaD2Vxpthzr0l216aZD53HYRmIC9FBsUtMVOrKY3Abyir0pg81Z1h68EKDdDLZkjFi1bNLq"},"settings":{"dgiklkfkllikcanfonkcabmbdfmgleag":"D1EFB83AEAF9A59A38FB1F7F85EA99D7C420EC1EE47AF3655E5D66E24D108D53","fikbjbembnmfhppjfnmfkahdhfohhjmg":"B13674C6D6E533333D2C43EB1B1665DB3910DE964A6679FDE1ABB60F2AD7A993","iglcjdemknebjbklcgkfaebgojjphkec":"BE0F4996716718A9EF17AFDAD0D906C73A7D09FD86A673E703544E0F3E176EF0","ihmafllikibpmigkcoadcmckbfhibefp":"3855972CC9DAD34A7D533BA158749DCAB8F16B3E1B0A6E19549833CDF0CDECDD","jdiccldimpdaibmpdkjnbmckianbfold":"95A4FD6B00DD4FE870E2D209990CCF829D50CE39C65BFDF5693858DBF01C9157","mhjfbmdgcfjbbpaeojofohoefgiehjai":"532E4D0842CDFFD6CB52B87D28BA1B2E7F45B22B080800F5569E41B721DA8C08","ncbjelpjchkpbikbpkcchkhkblodoama":"44F1D54A03EB680E1E6E0BBC10B790EF5188B4CDDBCB0A2B2402A63B2A1E83D2","ndcpkimcihhghdcddljkfmmjccdmcmof":"2B0376CB780889636C4D66FA7E7202241A60725A0A8A0E1A76FFBF26D391E777","nkeimhogjdpnpccoofpliimaahmaaome":"8F150A01EDB82630FF6C6DC1F66D127425EBBC7E18FEE732EEF0B891CD1B127D"},"settings_encrypted_hash":{"dgiklkfkllikcanfonkcabmbdfmgleag":"djEwI2wO4G6FgVWYwI0WaGfEGhE1odS1Hnv7KSDA1ww6BWkorytEkvUIaXZC8Z4uJf31ncKIhHqbt6CniAHj","fikbjbembnmfhppjfnmfkahdhfohhjmg":"djEw3cBHjwwTBlXYQCTcSfxdnTogxS73mFtbd4f8FkLVONQPV9vB22kLEbnD6cSgNDE3O2IxyquXKkZOsPQs","iglcjdemknebjbklcgkfaebgojjphkec":"djEwjcNhd6THOBByUYrraksu3PhjQdM/KfXgdAAfLJWGmd4eaLHPHiR9ooDfS7CQ2bfGrhb+r0a9shsV4f30","ihmafllikibpmigkcoadcmckbfhibefp":"djEw28lVSXB21YgjoqvZOcRvH0TV7FT+JldE/D9TXtVCfW9jKLQoaJ81lR1wL0NIWE/bOFqeu+ekyXvtgzXz","jdiccldimpdaibmpdkjnbmckianbfold":"djEwx+QJc9NCiGp4JQ7OGW34E3pSBmeyJXhIpbMpBe4miCGT8TP/oExAt8bXf+MuHRkZ4nSb8HTL1DdCc0bj","mhjfbmdgcfjbbpaeojofohoefgiehjai":"djEwN3TqubQJA4wHLIpwofKkkmqK0u4MHQ/wJxqP9crScw+TqlEVFJ8ExE/KU3jf7wu+Ca9biqc6WIZGRT3c","ncbjelpjchkpbikbpkcchkhkblodoama":"djEwVZiPLyc1FrH3TBJiLEKQOtyHt8OOXlAejLa9o396DpQT9NIW6CIPC2NdsKVdikNyWmtpmV5AlWQ1O1a/","ndcpkimcihhghdcddljkfmmjccdmcmof":"djEw/I3ibQsOypwNmBbJUsHRyiEeO4boJ3TQ8veJbqe5SZEhfWmQTRSUPBR98NlGMg99LLwYHZmE7Waanl3K","nkeimhogjdpnpccoofpliimaahmaaome":"djEwpLgF9RupcSb/1lMtQqsFefKzeYBBCFWtzpseCPq1dAtdvS7g4XQeZogQifeoNqrenZECB5yB2roSax4Q"},"ui":{"developer_mode":"A31162197DD427405BFBCD786ABF0A296647BD77E9372191E55DCC1CC82EA211","developer_mode_encrypted_hash":"djEw0e36Iagj2JcKmmpxTX4cJEPed0ps0M/l3DTk7eR+ziQ7IcAvGyj5kL3o2EPdNhdQ4PDZzD6il2zuluGp"}},"google":{"services":{"last_signed_in_username":"21949E06D19934D74E5B42635CB3F6F1AE1E9BC4510B3A5A88E2BB91411396A5","last_signed_in_username_encrypted_hash":"djEwt4YL7P9BiAp0gxGi7rVRT0oRUMAAKxIQiSc5m0yH5Mr9v+JvBa6XiWHTkXsFinyrNgdZ4tO53kdQDF7c"}},"homepage":"A40D9AF99772A1926AC174E42F7BF2E684AC252154AFCEB4EF2CB27B4FE27BD7","homepage_encrypted_hash":"djEwZ7qgKCn5b4pfiARpxvlxvvo9TzsvsD2Z11FNLggDjDf8Qep+ovxYQYhM6rOGyOm89WJrE1TivbuShWm/","homepage_is_newtabpage":"534D011A643FEC965381A7A971B5B08E3EFA36AD07E5144020D8EC35C4C08A2D","homepage_is_newtabpage_encrypted_hash":"djEw9fU65E2B6E9Ib23Yfi25xKigQ8hr+DzrTA5wllk6EwNNYSC6zfxdeqF+w7F5Zgu/eYJDjHTUnN6QFah+","media":{"cdm":{"origin_data":"0A7C725EEBBD290D598BA6430B5C4CEB913B136FC1A776A7017D483C7093CD3C","origin_data_encrypted_hash":"djEwoDD5HuIZyKaX17vajIU4dYkUueVm5SA3q2dFQOMpFmU1L9jaXULMV4/Ccjb/QT+IpJZApv0mdThzOpii"},"storage_id_salt":"85F7F74400273C5881BE2D95B52421BEA8D5B3D868B47BDC33E221452C0F0F8F","storage_id_salt_encrypted_hash":"djEwMSPW9c0m0KSNMMJcTX3ohkOElSXdCaZYy6be8DqMVc4x1cY4W025oBLQX/ff0SfrUmtlLin6nUPFT3RI"},"pinned_tabs":"5E41A9BE0806546CB640E08BD311157ED29E5CE7A77C0D7485831ADAD4B73649","pinned_tabs_encrypted_hash":"djEw62hr8bedC77BwLv/SE0AnCUme7guTbxKaEjs7VWW+VkzxKCKZ0EfChlzq7rA9RpklEdok6YKssCn97XU","prefs":{"preference_reset_time":"6A6DF759276DC694DB0E993037EB19220F449424DBF60D74AA8BD1B1B7895510","preference_reset_time_encrypted_hash":"djEwBYb1AAdRXw9l54pIYNhOAxUAmxXJmaiVLEEQ2lKM9+K388vkhRhqVm5EQHy9LwtuLcbL3qgRCgidYyYA"},"safebrowsing":{"incidents_sent":"15228CF9C81F0D13A5263FF487349741BDC064493AF9227C0BD9747F085B81D8","incidents_sent_encrypted_hash":"djEwFIxp/8hR8hDUqVDpbOios6QyyF8CrW2Y9Is3e3RERtVsLpPY6UjwXLERAfGeM0LSNR3XbWLeOBtNzC3/"},"schedule_to_flush_to_disk":"9735F45C8654B2D99C33CF3C8E95B3BE484A756924624C2A82BAAF0959954040","schedule_to_flush_to_disk_encrypted_hash":"djEwHVDMYM8Tl5vmjzyIsS68B9521pnUOCzNGgJWFJNE7v2bimi/+tyvCEWNJLTlHEQn457fjdTiAR2GiM3u","search_provider_overrides":"E1DCCF99FF19327B23158FA6BBA9580F4F5C03DBADED8679803AD521CC2FE2BD","search_provider_overrides_encrypted_hash":"djEw8yZJpzmx5LIb12pAd6jOjrUT8vco4p3VEDpxnNpaECJKlMPv+HSTQDLEfg/SiLd7xJIqE7GEaKsy7Rrk","session":{"restore_on_startup":"D4CD3D41B430973808036F00CADA865E8E1AECB1F24EB7490AECE3DFC98D4AED","restore_on_startup_encrypted_hash":"djEwCGHGNdNfMLwydRMyteXa7EVlF8/XJb6X2qesadgBiNpf8CEjaMX4Eb2r+U337WqiF3HopCZJql5tAorD","startup_urls":"9852A130A25F5D467EC89B38EC0600E22467A535352A5484E7114AB876F184B3","startup_urls_encrypted_hash":"djEw6s0FvUz1yxvXiV4dh8ItRvVO/qv8uoIXbcx2D/DN0A0NRHIssry1EHgwCBbar1WEOI2LhlpYHY0j84Qg"}},"super_encrypted_hash":"djEwcRdvMJSPOxLJ+g8Yq2RofkpVcLUsf9nvteY/xTzNIFEFToSYZ2tY925cqK+tF4aYHmcHY+e0CGzKCVT2","super_mac":"79FFD37119B61D2A3705504C57B1C42BD74EB95AB457E6541BDCE410B65D30EA"},"schedule_to_flush_to_disk":"13434014281070004"} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/ServerCertificate b/.tmp-edge-annexation/Default/ServerCertificate new file mode 100644 index 0000000..86cb163 Binary files /dev/null and b/.tmp-edge-annexation/Default/ServerCertificate differ diff --git a/.tmp-edge-annexation/Default/ServerCertificate-journal b/.tmp-edge-annexation/Default/ServerCertificate-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Service Worker/Database/000003.log b/.tmp-edge-annexation/Default/Service Worker/Database/000003.log new file mode 100644 index 0000000..0be0d7a Binary files /dev/null and b/.tmp-edge-annexation/Default/Service Worker/Database/000003.log differ diff --git a/.tmp-edge-annexation/Default/Service Worker/Database/CURRENT b/.tmp-edge-annexation/Default/Service Worker/Database/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Service Worker/Database/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Service Worker/Database/LOCK b/.tmp-edge-annexation/Default/Service Worker/Database/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Service Worker/Database/LOG b/.tmp-edge-annexation/Default/Service Worker/Database/LOG new file mode 100644 index 0000000..1e44992 --- /dev/null +++ b/.tmp-edge-annexation/Default/Service Worker/Database/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.254 3c14 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Service Worker\Database since it was missing. +2026/09/16-15:38:01.469 3c14 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Service Worker\Database/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Service Worker/Database/MANIFEST-000001 b/.tmp-edge-annexation/Default/Service Worker/Database/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Service Worker/Database/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Service Worker/ScriptCache/2cc80dabc69f58b6_0 b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/2cc80dabc69f58b6_0 new file mode 100644 index 0000000..1528431 Binary files /dev/null and b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/2cc80dabc69f58b6_0 differ diff --git a/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index differ diff --git a/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index-dir/the-real-index b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index-dir/the-real-index new file mode 100644 index 0000000..c5e637e Binary files /dev/null and b/.tmp-edge-annexation/Default/Service Worker/ScriptCache/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Session Storage/000003.log b/.tmp-edge-annexation/Default/Session Storage/000003.log new file mode 100644 index 0000000..1780fb3 Binary files /dev/null and b/.tmp-edge-annexation/Default/Session Storage/000003.log differ diff --git a/.tmp-edge-annexation/Default/Session Storage/CURRENT b/.tmp-edge-annexation/Default/Session Storage/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Session Storage/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Session Storage/LOCK b/.tmp-edge-annexation/Default/Session Storage/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Session Storage/LOG b/.tmp-edge-annexation/Default/Session Storage/LOG new file mode 100644 index 0000000..014a05e --- /dev/null +++ b/.tmp-edge-annexation/Default/Session Storage/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.064 3544 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Session Storage since it was missing. +2026/09/16-15:38:01.133 3544 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Session Storage/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Session Storage/MANIFEST-000001 b/.tmp-edge-annexation/Default/Session Storage/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Session Storage/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Sessions/Session_13434014282025592 b/.tmp-edge-annexation/Default/Sessions/Session_13434014282025592 new file mode 100644 index 0000000..d4d807d Binary files /dev/null and b/.tmp-edge-annexation/Default/Sessions/Session_13434014282025592 differ diff --git a/.tmp-edge-annexation/Default/Sessions/Tabs_13434014282107761 b/.tmp-edge-annexation/Default/Sessions/Tabs_13434014282107761 new file mode 100644 index 0000000..c10413c Binary files /dev/null and b/.tmp-edge-annexation/Default/Sessions/Tabs_13434014282107761 differ diff --git a/.tmp-edge-annexation/Default/Shared Dictionary/cache/index b/.tmp-edge-annexation/Default/Shared Dictionary/cache/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Shared Dictionary/cache/index differ diff --git a/.tmp-edge-annexation/Default/Shared Dictionary/cache/index-dir/the-real-index b/.tmp-edge-annexation/Default/Shared Dictionary/cache/index-dir/the-real-index new file mode 100644 index 0000000..c46ee3e Binary files /dev/null and b/.tmp-edge-annexation/Default/Shared Dictionary/cache/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Shared Dictionary/db b/.tmp-edge-annexation/Default/Shared Dictionary/db new file mode 100644 index 0000000..85bff9e Binary files /dev/null and b/.tmp-edge-annexation/Default/Shared Dictionary/db differ diff --git a/.tmp-edge-annexation/Default/Shared Dictionary/db-journal b/.tmp-edge-annexation/Default/Shared Dictionary/db-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Site Characteristics Database/000003.log b/.tmp-edge-annexation/Default/Site Characteristics Database/000003.log new file mode 100644 index 0000000..46580fb Binary files /dev/null and b/.tmp-edge-annexation/Default/Site Characteristics Database/000003.log differ diff --git a/.tmp-edge-annexation/Default/Site Characteristics Database/CURRENT b/.tmp-edge-annexation/Default/Site Characteristics Database/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Site Characteristics Database/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Site Characteristics Database/LOCK b/.tmp-edge-annexation/Default/Site Characteristics Database/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Site Characteristics Database/LOG b/.tmp-edge-annexation/Default/Site Characteristics Database/LOG new file mode 100644 index 0000000..318178c --- /dev/null +++ b/.tmp-edge-annexation/Default/Site Characteristics Database/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:00.814 dbc Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Site Characteristics Database since it was missing. +2026/09/16-15:38:00.872 dbc Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Site Characteristics Database/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Site Characteristics Database/MANIFEST-000001 b/.tmp-edge-annexation/Default/Site Characteristics Database/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Site Characteristics Database/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/6f3a0dc5e6916f1e_0 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/6f3a0dc5e6916f1e_0 new file mode 100644 index 0000000..b2029f5 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/6f3a0dc5e6916f1e_0 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index-dir/the-real-index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index-dir/the-real-index new file mode 100644 index 0000000..b1ba616 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/js/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index-dir/the-real-index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index-dir/the-real-index new file mode 100644 index 0000000..a9faffa Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Code Cache/wasm/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_0 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_1 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_2 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_3 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/index new file mode 100644 index 0000000..ea2b586 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnGraphiteCache/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_0 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_1 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_2 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_3 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/index new file mode 100644 index 0000000..73685f6 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/DawnWebGPUCache/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_0 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_0 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_1 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_1 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_2 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_2 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_3 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/data_3 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/index new file mode 100644 index 0000000..64a1f8e Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/GPUCache/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/000003.log b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/000003.log new file mode 100644 index 0000000..988af49 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/000003.log differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/CURRENT b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/LOCK b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/LOG b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/LOG new file mode 100644 index 0000000..882994a --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.213 3bf0 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Storage\ext\ihmafllikibpmigkcoadcmckbfhibefp\def\Local Storage\leveldb since it was missing. +2026/09/16-15:38:01.327 3bf0 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Storage\ext\ihmafllikibpmigkcoadcmckbfhibefp\def\Local Storage\leveldb/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/MANIFEST-000001 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Local Storage/leveldb/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Device Bound Sessions b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Device Bound Sessions new file mode 100644 index 0000000..fbc96d7 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Device Bound Sessions differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Device Bound Sessions-journal b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Device Bound Sessions-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Network Persistent State b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Network Persistent State new file mode 100644 index 0000000..7ca9943 --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Network Persistent State @@ -0,0 +1 @@ +{"net":{"http_server_properties":{"servers":[],"version":5},"network_qualities":{"CAISABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/NetworkDataMigrated b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/NetworkDataMigrated new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/SCT Auditing Pending Reports b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/SCT Auditing Pending Reports new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/SCT Auditing Pending Reports @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Trust Tokens b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Trust Tokens new file mode 100644 index 0000000..8d527ef Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Trust Tokens differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Trust Tokens-journal b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Network/Trust Tokens-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/000003.log b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/000003.log new file mode 100644 index 0000000..59560f5 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/000003.log differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/CURRENT b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/LOCK b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/LOG b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/LOG new file mode 100644 index 0000000..0b801e8 --- /dev/null +++ b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:02.128 3544 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Storage\ext\ihmafllikibpmigkcoadcmckbfhibefp\def\Session Storage since it was missing. +2026/09/16-15:38:02.172 3544 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Storage\ext\ihmafllikibpmigkcoadcmckbfhibefp\def\Session Storage/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/MANIFEST-000001 b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Session Storage/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index new file mode 100644 index 0000000..79bd403 Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index-dir/the-real-index b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index-dir/the-real-index new file mode 100644 index 0000000..1e6bc5d Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/cache/index-dir/the-real-index differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/db b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/db new file mode 100644 index 0000000..85bff9e Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/db differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/db-journal b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/Shared Dictionary/db-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/declarative_performance_observer.db b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/declarative_performance_observer.db new file mode 100644 index 0000000..8ab056e Binary files /dev/null and b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/declarative_performance_observer.db differ diff --git a/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/declarative_performance_observer.db-journal b/.tmp-edge-annexation/Default/Storage/ext/ihmafllikibpmigkcoadcmckbfhibefp/def/declarative_performance_observer.db-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Sync Data/LevelDB/000003.log b/.tmp-edge-annexation/Default/Sync Data/LevelDB/000003.log new file mode 100644 index 0000000..80bdfdb Binary files /dev/null and b/.tmp-edge-annexation/Default/Sync Data/LevelDB/000003.log differ diff --git a/.tmp-edge-annexation/Default/Sync Data/LevelDB/CURRENT b/.tmp-edge-annexation/Default/Sync Data/LevelDB/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/Sync Data/LevelDB/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Sync Data/LevelDB/LOCK b/.tmp-edge-annexation/Default/Sync Data/LevelDB/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Sync Data/LevelDB/LOG b/.tmp-edge-annexation/Default/Sync Data/LevelDB/LOG new file mode 100644 index 0000000..f633f7e --- /dev/null +++ b/.tmp-edge-annexation/Default/Sync Data/LevelDB/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:00.789 3db8 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Sync Data\LevelDB since it was missing. +2026/09/16-15:38:00.829 3db8 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\Sync Data\LevelDB/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/Sync Data/LevelDB/MANIFEST-000001 b/.tmp-edge-annexation/Default/Sync Data/LevelDB/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/Sync Data/LevelDB/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/Top Sites b/.tmp-edge-annexation/Default/Top Sites new file mode 100644 index 0000000..37c0abd Binary files /dev/null and b/.tmp-edge-annexation/Default/Top Sites differ diff --git a/.tmp-edge-annexation/Default/Top Sites-journal b/.tmp-edge-annexation/Default/Top Sites-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Vpn Tokens b/.tmp-edge-annexation/Default/Vpn Tokens new file mode 100644 index 0000000..cffda0c Binary files /dev/null and b/.tmp-edge-annexation/Default/Vpn Tokens differ diff --git a/.tmp-edge-annexation/Default/Vpn Tokens-journal b/.tmp-edge-annexation/Default/Vpn Tokens-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/Web Data b/.tmp-edge-annexation/Default/Web Data new file mode 100644 index 0000000..5c60636 Binary files /dev/null and b/.tmp-edge-annexation/Default/Web Data differ diff --git a/.tmp-edge-annexation/Default/Web Data-journal b/.tmp-edge-annexation/Default/Web Data-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/WebStorage/QuotaManager b/.tmp-edge-annexation/Default/WebStorage/QuotaManager new file mode 100644 index 0000000..d72a065 Binary files /dev/null and b/.tmp-edge-annexation/Default/WebStorage/QuotaManager differ diff --git a/.tmp-edge-annexation/Default/WebStorage/QuotaManager-journal b/.tmp-edge-annexation/Default/WebStorage/QuotaManager-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/arbitration_service_config.json b/.tmp-edge-annexation/Default/arbitration_service_config.json new file mode 100644 index 0000000..34c6117 --- /dev/null +++ b/.tmp-edge-annexation/Default/arbitration_service_config.json @@ -0,0 +1,195 @@ +{ + "ArbitrationSignal": "notification_nsat_upper_ci-0.7", + "CustomSuppressionPolicies": { + "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.f5b8c725-cb2e-4c12-accd-73e500d88d47.AutoOpen": { + "notification_max_quick_dismiss_count": 3 + }, + "bc25fcef-8964-4e72-8287-23e2b496c128.68b8a884-6e08-46e6-8a3b-7e06ffe48ecf.AutoOpen": { + "notification_max_quick_dismiss_count": 3 + }, + "cd4688a9-e888-48ea-ad81-76193d56b1be.e06e75c1-a59a-4ab6-8361-e8b038e00a8c.AutoOpen": { + "notification_max_quick_dismiss_count": 1 + } + }, + "DynamicSuppressionBypass": { + "ExperienceIDs": [ "Nurturing.Global.OnRamp_RetriggerFREOnEdgeVersionUpgradeViaExplicitLaunch", "Nurturing.Global.OnRamp_RetriggerFREOnExplicitProtocolPDFLaunchForEdgePostDMA", "Nurturing.Global.Bing_DefaultBrowserBannerCloseBtn", "Nurturing.Global.Bing_DefaultBrowserBannerCloseBtnUpdate", "Nurturing.Global.Bing_MSStartYJCompete", "Nurturing.Global.Bing_NtpHomeStartpageUpsellFlyout", "Nurturing.Global.Fundamentals_DefaultBrowserMonitor", "Nurturing.Global.Fundamentals_DefaultBrowserMonitorOnStart", "Nurturing.Global.Fundamentals_DefaultBrowserMonitorVariant2", "Nurturing.Global.RecommendedSettingsChina", "Nurturing.Global.ContinuousImport_PasswordZeroStateV2", "Nurturing.Global.OnRamp_CIExplicitLaunchBannerUpsell", "Nurturing.Global.OnRamp_CIUpsell_ProtocolLaunch_Banner", "Nurturing.Global.EdgeGrowth_SitePinningCITopSites", "Nurturing.Global.EdgeGrowth_SitePinningWithWindowsConsent", "Nurturing.Global.EdgeGrowth_GlobalSitePinningOnCloseModal" ], + "TeamIDs": [ "NTP" ] + }, + "ExperienceCohorts": { + "DefaultCohort": { + "21f3388b-c2a5-4791-8f6e-a4cad6d17f4f.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.BingHomePage.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Covid.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Finance.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Jobs.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.KnowledgeCard.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Local.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.NTP3PCLICK.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.NotifySearchPage.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Recipe.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.SearchPage.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Sports.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Travel.Bubble": 1, + "2354565a-f412-4654-b89c-f92eaa9dbd20.Weather.Bubble": 1, + "2cb2db96-3bd0-403e-abe2-9269b3761041.AutoOpen": 1, + "2cb2db96-3bd0-403e-abe2-9269b3761041.Bubble": 1, + "439642fc-998d-4a64-8bb6-940ecaf6b60b.AutoOpen": 1, + "439642fc-998d-4a64-8bb6-940ecaf6b60b.Bubble": 1, + "523b5ef3-0b10-4154-8b62-10b2ebd00921.Bubble": 1, + "64be4f9b-3b81-4b6e-b354-0ba00d6ba485.AutoOpen": 1, + "64be4f9b-3b81-4b6e-b354-0ba00d6ba485.Bubble": 1, + "76b926d6-3738-46bf-82d7-2ab896ddf70b.MM2UpSellPopup.Bubble": 1, + "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.Bubble": 1, + "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.f5b8c725-cb2e-4c12-accd-73e500d88d47.AutoOpen": 1, + "8ac719c5-140b-4bf2-a0b7-c71617f1f377.3662f1e2-66ae-464f-b690-e7e3dc4fc662.AutoOpen": 1, + "8ac719c5-140b-4bf2-a0b7-c71617f1f377.93e5b5cd-15fc-4253-9728-d80685da304e.AutoOpen": 1, + "8ac719c5-140b-4bf2-a0b7-c71617f1f377.AutoOpen": 1, + "8ac719c5-140b-4bf2-a0b7-c71617f1f377.gaokao.Bubble": 1, + "92f1b743-e26b-433b-a1ec-912d1f0ad1fa.Bubble": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.0bdde3c3-7be8-49f1-971b-e6e2cfd62643.AutoOpen": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.128c39c6-6f70-4ce5-9d01-78978e60d666.AutoOpen": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.5ac63306-e962-42e6-8313-7e5a5b707d65.AutoOpen": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.AutoOpen": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.Bubble": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.Is1PGameDomain.Bubble": 1, + "96defd79-4015-4a32-bd09-794ff72183ef.Is3PGameDomain.Bubble": 1, + "9ce3c9c2-462f-4cc9-bbd7-57d656445be0.Bubble": 1, + "AUTOFILL_PASSWORD_NON_FUNCTIONAL_FLYOUTS": 1, + "DefaultExperience": 1, + "EdgeDownloadChromeInterceptDialog": 2, + "RewardsPromotionNotifications": 1, + "RewardsReminderNotifications": 1, + "SAN.Personalization.ConsentPrompt": 1, + "SHOPPING_AA_CONSENT": 1, + "SHOPPING_AUTO_PRODUCT_TRACKING_EXP_PRICE_DROP": 1, + "SHOPPING_AUTO_PRODUCT_TRACKING_IMP_PRICE_DROP": 1, + "SHOPPING_AUTO_PRODUCT_TRACKING_OUT_OF_STOCK": 1, + "SHOPPING_AUTO_SHOW_ABANDONED_CART": 1, + "SHOPPING_AUTO_SHOW_ADD_SPB_TO_CART_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_AMAZON_SEARCH_PC": 1, + "SHOPPING_AUTO_SHOW_ATTAINABLE_COUPONS": 1, + "SHOPPING_AUTO_SHOW_AUTOS_MARKETPLACE": 1, + "SHOPPING_AUTO_SHOW_AWARENESSEXP": 1, + "SHOPPING_AUTO_SHOW_AWARENESS_EXPANSION": 1, + "SHOPPING_AUTO_SHOW_CAMPAIGN_AWARENESS_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_CAMPAIGN_PDP_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_CASHBACK_PDP_PLUS_COUPONS": 1, + "SHOPPING_AUTO_SHOW_CJK_COUPON_FOUND": 1, + "SHOPPING_AUTO_SHOW_CJK_PRICE_DROP": 1, + "SHOPPING_AUTO_SHOW_CODEX_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_CONFIRMATION_SCRIPT": 1, + "SHOPPING_AUTO_SHOW_CONTROLLER": 1, + "SHOPPING_AUTO_SHOW_COUPONS_BACKGROUND_AUTO_APPLY": 1, + "SHOPPING_AUTO_SHOW_COUPONS_CHECKOUT": 1, + "SHOPPING_AUTO_SHOW_COUPONS_CHECKOUT_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_COUPONS_CLIPPING": 1, + "SHOPPING_AUTO_SHOW_DISCOVER_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_DISCOVER_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_EXPRESS_CHECKOUT": 1, + "SHOPPING_AUTO_SHOW_GIFT_CARD": 1, + "SHOPPING_AUTO_SHOW_GOODRX": 1, + "SHOPPING_AUTO_SHOW_GROCERY_ITEMIZED_CASHBACK": 1, + "SHOPPING_AUTO_SHOW_GROCERY_ITEMIZED_CASHBACK_LANDING": 1, + "SHOPPING_AUTO_SHOW_GUEST_DOMAIN_COUPONS": 1, + "SHOPPING_AUTO_SHOW_IBC_UPSELL": 1, + "SHOPPING_AUTO_SHOW_LOWER_PRICE_FOUND": 1, + "SHOPPING_AUTO_SHOW_LOWER_PRICE_PLUS_CASHBACK_PLUS_COUPONS": 1, + "SHOPPING_AUTO_SHOW_MOMENT_IN_TIME": 1, + "SHOPPING_AUTO_SHOW_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_ORGANIC_CASHBACK_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_OTHER_SELLER": 1, + "SHOPPING_AUTO_SHOW_OUT_OF_STOCK": 1, + "SHOPPING_AUTO_SHOW_PACKAGE_TRACKING": 1, + "SHOPPING_AUTO_SHOW_PACKAGE_TRACKING_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK_ADDRESSBAR": 1, + "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_POST_PURCHASE_TRACKING": 1, + "SHOPPING_AUTO_SHOW_PRICE_DROP_PLUS_CASHBACK": 1, + "SHOPPING_AUTO_SHOW_PRICE_DROP_PLUS_COUPONS": 1, + "SHOPPING_AUTO_SHOW_PRICE_HISTORY": 1, + "SHOPPING_AUTO_SHOW_PRICE_HISTORY_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_PRINT_GROCERY": 1, + "SHOPPING_AUTO_SHOW_PRODUCT_BEST_COUPON_CONSENT": 1, + "SHOPPING_AUTO_SHOW_PRODUCT_TRACKING_BACK_IN_STOCK": 1, + "SHOPPING_AUTO_SHOW_PRODUCT_TRACKING_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_PROFESSIONAL_REVIEWS": 1, + "SHOPPING_AUTO_SHOW_REBATES": 1, + "SHOPPING_AUTO_SHOW_REBATES_CHECKOUT": 1, + "SHOPPING_AUTO_SHOW_REBATES_ORGANIC": 1, + "SHOPPING_AUTO_SHOW_REBATES_ORGANIC_NEW": 1, + "SHOPPING_AUTO_SHOW_REBATES_PDP": 1, + "SHOPPING_AUTO_SHOW_RECOMMENDATIONS_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_SAN_CONSENT_CONFIRMATION": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_ALL_PAGES": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_CHECKOUT": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_CONFIRMATION_PAGE": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_HOME_PAGE": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_REWARDS": 1, + "SHOPPING_AUTO_SHOW_SHOPRUNNER": 1, + "SHOPPING_AUTO_SHOW_SIDEPANE_CASHBACK_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_SPB_CASHBACK_LANDING": 1, + "SHOPPING_AUTO_SHOW_SPB_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_SPB_NUDGE_MICRO_NOTIFICATION": 1, + "SHOPPING_AUTO_SHOW_SURFACE_UPSELL": 1, + "SHOPPING_AUTO_SHOW_SUSTAINABILITY_LANDING": 1, + "SHOPPING_AUTO_SHOW_TRAVEL_NOTIFICATION_CHEAPER_FLIGHTS": 1, + "SHOPPING_AUTO_SHOW_TRAVEL_NOTIFICATION_CHEAPER_HOTEL_ROOM": 1, + "SHOPPING_AUTO_SHOW_TRENDING_COUPONS": 1, + "ShorelinePrivilegedExperienceID": 1, + "TabActionsMenuVerticalTabsPromotion": 1, + "VPNFeatureNotification": 1, + "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.75a774c2-6075-ae4f-09f9-e5f5d9bef6e1.AutoOpen": 1, + "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.AutoOpen": 1, + "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.a5e9f5e1-fcee-c07b-2c44-b707ded020a3.AutoOpen": 1, + "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.c8ebbf64-e3b8-41d9-925f-4e3754151ff7.AutoOpen": 1, + "bc25fcef-8964-4e72-8287-23e2b496c128.68b8a884-6e08-46e6-8a3b-7e06ffe48ecf.AutoOpen": 1, + "bc25fcef-8964-4e72-8287-23e2b496c128.Bubble": 1, + "c8ebd871-9f47-4a0d-abd3-c1c02b4f8f53.2cfab14c-ebd6-4548-9401-30ab5afe5061.AutoOpen": 1, + "c8ebd871-9f47-4a0d-abd3-c1c02b4f8f53.AutoOpen": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.5b853177-a0be-4237-bda0-4fce5cd165c2.AutoOpen": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.8d3a928d-e911-40f0-9852-f63f6434dc7e.AutoOpen": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.AutoOpen": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.AutoOpen.PDF": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.AutoOpen.PersistentChat": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsCompeteAISite.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsCopilotJitEduAcademicAI.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsCopilotJitEduArticleAI.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsCopilotJitEduStudyAI.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsKnowledgeCardQuery.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsMsnArticleUrlFromNtpP1P2.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsShoppingNotificationNudge.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsTextPage.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsTopNewsDomain.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsTwitchNonStreamPage.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsTwitchSubPage.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.IsVideoPage.Bubble": 1, + "cd4688a9-e888-48ea-ad81-76193d56b1be.e06e75c1-a59a-4ab6-8361-e8b038e00a8c.AutoOpen": 1, + "e6723537-66ff-4f4e-ab56-a4cbaddf4e0f.MM2UpSellPopup.Bubble": 1 + } + }, + "FunctionalCohort": [ "EdgeDownloadChromeInterceptFunctionalDialog", "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.AutoOpen.Functional", "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.f5b8c725-cb2e-4c12-accd-73e500d88d47.AutoOpen.Functional", "bc25fcef-8964-4e72-8287-23e2b496c128.AutoOpen.Functional", "bc25fcef-8964-4e72-8287-23e2b496c128.68b8a884-6e08-46e6-8a3b-7e06ffe48ecf.AutoOpen.Functional", "WPOFunctionalEvent.AutofillAccountChooser.Bubble", "WPOFunctionalEvent.AutofillAutoSignin.Bubble", "WPOFunctionalEvent.AutofillPassword.Bubble", "SHOPPING_AUTO_SHOW_BING_SEARCH", "SHOPPING_AUTO_SHOW_REBATES_CONFIRMATION", "SHOPPING_AUTO_SHOW_REBATES_DEACTIVATED", "SHOPPING_AUTO_SHOW_REBATES_BING", "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK", "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK_CONFIRMATION", "SHOPPING_AUTO_SHOW_URL_PARAM_REBATES", "SHOPPING_AUTO_SHOW_URL_PARAM_PRICE_COMPARISON", "SHOPPING_AUTO_SHOW_URL_PARAM_PRICE_HISTORY", "SHOPPING_AUTO_SHOW_REBATES_ACTIVATION_FAILED", "SHOPPING_AUTO_SHOW_REBATES_SWITCHED_TO_MSA", "SHOPPING_AUTO_SHOW_ITEM_ADDED_TO_CART_FROM_OTHER_SELLER", "SHOPPING_AUTO_SHOW_VIRTUAL_CARD", "SHOPPING_AUTO_SHOW_BING_CAMPAIGN", "SHOPPING_OPEN_FLYOUT_FROM_EDGE_DRIVER", "SHOPPING_AUTO_SHOW_SUSTAINABILITY_PURCHASE", "SHOPPING_AUTO_SHOW_REWARDS_ACTIVATION_FAILED", "SHOPPING_AUTO_SHOW_REWARDS_SWITCHED_TO_MSA", "SHOPPING_AUTO_SHOW_SHOPRUNNER_CONFIRMATION", "SHOPPING_AUTO_SHOW_ENROLL_TO_CASHBACK_CONFIRMATION_NOTIFICATION", "SHOPPING_AUTO_SHOW_POST_PURCHASE_REWARDS_NOTIFICATION", "694efa4f-a304-4214-aaf7-90444bc887f5.c1c025d9-a620-4f6b-bebb-eff0cf19a671.AutoOpen.Functional" ], + "GlobalSuppressedExperiences": [ "Teams.Bubble", "Nurturing.Global.EdgeVpn_VpnFre", "Nurturing.Global.Rewards_RedemptionCoachmark_2", "Nurturing.Global.Rewards_RedemptionCoachmark_25", "Nurturing.Global.EdgeCBUD_SanPersonalizationConsentWave1Variant0", "Nurturing.Global.Bing_NewBingChatFlyout", "Copilot.AutoOpen.PDF.v2", "Nurturing.Global.PWA_SidebarSearchPromotion", "Nurturing.Global.FC_NetworkWarningFlyout", "Nurturing.Global.EdgeMobile_MobileUpsell_ChatGpt_Flyout", "E-tree.Bubble", "Nurturing.Global.EdgeEDU_BrowserFocusedTranslateNudge", "Games.Bubble", "Nurturing.Global.OnRamp_RetriggerFRECodexCoPilotAutoAprd", "Nurturing.Global.OnRamp_NurturingViaProng_Experiment", "Nurturing.Global.OnRamp_ReTriggerOnWinUpgrade", "Nurturing.Global.EdgeGrowth_CIShoppingUpsell", "Nurturing.Global.Shopping_EnableShoppingAssistance_Optin_Generic", "Nurturing.Global.Shopping_EnableShoppingAssistance", "Nurturing.Global.BrowserExperiences_TryVerticalTabs" ], + "IgnoredFunctionalNotifications": [ "Nurturing.Functional.NTP_CourtesyEngineCampaign", "Nurturing.Functional.NTP_FunctionalCampaign", "Nurturing.Functional.NTP_SettingsCampaign", "Nurturing.Functional.NTP_UpsellCampaign", "Nurturing.Functional.FN_SmartSwitchFeatureInfoView" ], + "ModelInfo": { + "segment_id": 515, + "signals": [ "notification_triggered_count", "notification_click_rate", "notification_click_rate_lower_ci", "notification_click_rate_upper_ci", "notification_dismiss_rate", "notification_dismiss_rate_lower_ci", "notification_dismiss_rate_upper_ci", "notification_ignored_rate", "notification_ignored_rate_lower_ci", "notification_ignored_rate_upper_ci", "notification_quick_dismiss_rate", "notification_quick_dismiss_rate_lower_ci", "notification_quick_dismiss_rate_upper_ci", "notification_disable_rate", "notification_disable_rate_lower_ci", "notification_disable_rate_upper_ci", "notification_snooze_rate", "notification_snooze_rate_lower_ci", "notification_snooze_rate_upper_ci" ], + "threshold_value": 0.5 + }, + "ModelSuppressionBypass": [ "Nurturing.Local.SHOPPING_AUTO_SHOW_LOWER_PRICE_FOUND", "Nurturing.Local.SHOPPING_AUTO_SHOW_REBATES_ORGANIC", "Nurturing.Local.SHOPPING_AUTO_SHOW_REBATES_CHECKOUT", "Nurturing.Local.SHOPPING_AUTO_SHOW_SPB_CASHBACK_LANDING" ], + "NotificationsAllowLists": { + }, + "PrivilegedExperiences": [ "ShorelinePrivilegedExperienceID", "SHOPPING_AUTO_SHOW_COUPONS_CHECKOUT", "SHOPPING_AUTO_SHOW_LOWER_PRICE_FOUND", "SHOPPING_AUTO_SHOW_REBATES", "SHOPPING_AUTO_SHOW_REBATES_ORGANIC", "SHOPPING_AUTO_SHOW_PRICE_HISTORY", "SHOPPING_AUTO_SHOW_REBATES_ORGANIC_NEW", "SHOPPING_AUTO_SHOW_REBATES_CHECKOUT", "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_CHECKOUT", "SHOPPING_AUTO_SHOW_EXPRESS_CHECKOUT", "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_ALL_PAGES", "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_CONFIRMATION_PAGE", "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_HOME_PAGE", "SHOPPING_AUTO_SHOW_SERVER_DRIVEN_REWARDS", "SHOPPING_AUTO_SHOW_COUPONS_CLIPPING", "SHOPPING_AUTO_SHOW_NOTIFICATION", "SHOPPING_AUTO_SHOW_OTHER_SELLER", "SHOPPING_AUTO_SHOW_PROFESSIONAL_REVIEWS", "SHOPPING_AUTO_SHOW_PACKAGE_TRACKING", "SHOPPING_AUTO_SHOW_GUEST_DOMAIN_COUPONS", "SHOPPING_AUTO_SHOW_SUSTAINABILITY_LANDING", "SHOPPING_AUTO_SHOW_ATTAINABLE_COUPONS", "SHOPPING_AUTO_SHOW_GIFT_CARD", "SHOPPING_AUTO_SHOW_PRINT_GROCERY", "SHOPPING_AUTO_SHOW_GROCERY_ITEMIZED_CASHBACK", "SHOPPING_AUTO_SHOW_CONTROLLER", "SHOPPING_AUTO_SHOW_AUTOS_MARKETPLACE", "SHOPPING_AUTO_SHOW_SHOPRUNNER", "SHOPPING_AUTO_SHOW_ABANDONED_CART", "SHOPPING_AUTO_SHOW_OUT_OF_STOCK", "SHOPPING_AUTO_SHOW_GROCERY_ITEMIZED_CASHBACK_LANDING", "SHOPPING_AUTO_SHOW_SPB_CASHBACK_LANDING", "SHOPPING_AUTO_SHOW_AWARENESSEXP", "SHOPPING_AA_CONSENT", "SHOPPING_AUTO_SHOW_SAN_CONSENT_CONFIRMATION", "SHOPPING_AUTO_SHOW_CONFIRMATION_SCRIPT", "SHOPPING_AUTO_SHOW_MOMENT_IN_TIME", "SHOPPING_AUTO_SHOW_COUPONS_CHECKOUT_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_TRENDING_COUPONS", "SHOPPING_AUTO_SHOW_RECOMMENDATIONS_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_PRODUCT_TRACKING_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_SURFACE_UPSELL", "SHOPPING_AUTO_SHOW_PRODUCT_TRACKING_BACK_IN_STOCK", "SHOPPING_AUTO_PRODUCT_TRACKING_EXP_PRICE_DROP", "SHOPPING_AUTO_PRODUCT_TRACKING_IMP_PRICE_DROP", "SHOPPING_AUTO_PRODUCT_TRACKING_OUT_OF_STOCK", "SHOPPING_AUTO_SHOW_CODEX_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_PRICE_HISTORY_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_ORGANIC_CASHBACK_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_GOODRX", "SHOPPING_AUTO_SHOW_SIDEPANE_CASHBACK_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_DISCOVER_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_REBATES_PDP", "SHOPPING_AUTO_SHOW_PRODUCT_BEST_COUPON_CONSENT", "SHOPPING_AUTO_SHOW_CAMPAIGN_PDP_NOTIFICATION", "SHOPPING_AUTO_SHOW_CAMPAIGN_AWARENESS_NOTIFICATION", "SHOPPING_AUTO_SHOW_SPB_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_PRICE_DROP_PLUS_COUPONS", "SHOPPING_AUTO_SHOW_SPB_NUDGE_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_ADD_SPB_TO_CART_MICRO_NOTIFICATION", "SHOPPING_AUTO_SHOW_CASHBACK_PDP_PLUS_COUPONS", "SHOPPING_AUTO_SHOW_PRICE_DROP_PLUS_CASHBACK", "SHOPPING_AUTO_SHOW_DISCOVER_NOTIFICATION", "SHOPPING_AUTO_SHOW_AMAZON_SEARCH_PC", "SHOPPING_AUTO_SHOW_COUPONS_BACKGROUND_AUTO_APPLY", "SHOPPING_AUTO_SHOW_PERSONALIZED_CASHBACK_ADDRESSBAR", "SHOPPING_AUTO_SHOW_TRAVEL_NOTIFICATION_CHEAPER_FLIGHTS", "SHOPPING_AUTO_SHOW_TRAVEL_NOTIFICATION_CHEAPER_HOTEL_ROOM", "SHOPPING_AUTO_SHOW_CJK_PRICE_DROP", "SHOPPING_AUTO_SHOW_CJK_COUPON_FOUND", "AUTOFILL_PASSWORD_NON_FUNCTIONAL_FLYOUTS", "76b926d6-3738-46bf-82d7-2ab896ddf70b.MM2UpSellPopup.Bubble", "e6723537-66ff-4f4e-ab56-a4cbaddf4e0f.MM2UpSellPopup.Bubble", "8ac719c5-140b-4bf2-a0b7-c71617f1f377.3662f1e2-66ae-464f-b690-e7e3dc4fc662.AutoOpen", "8ac719c5-140b-4bf2-a0b7-c71617f1f377.gaokao.Bubble", "c8ebd871-9f47-4a0d-abd3-c1c02b4f8f53.AutoOpen", "c8ebd871-9f47-4a0d-abd3-c1c02b4f8f53.2cfab14c-ebd6-4548-9401-30ab5afe5061.AutoOpen", "cd4688a9-e888-48ea-ad81-76193d56b1be.AutoOpen.PersistentChat", "cd4688a9-e888-48ea-ad81-76193d56b1be.AutoOpen.PDF", "cd4688a9-e888-48ea-ad81-76193d56b1be.8d3a928d-e911-40f0-9852-f63f6434dc7e.AutoOpen", "cd4688a9-e888-48ea-ad81-76193d56b1be.e06e75c1-a59a-4ab6-8361-e8b038e00a8c.AutoOpen", "cd4688a9-e888-48ea-ad81-76193d56b1be.5b853177-a0be-4237-bda0-4fce5cd165c2.AutoOpen", "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.AutoOpen", "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.c8ebbf64-e3b8-41d9-925f-4e3754151ff7.AutoOpen", "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.75a774c2-6075-ae4f-09f9-e5f5d9bef6e1.AutoOpen", "b7a8e9f2-6b0d-4c5b-ae7d-8a6e1f2c7a6f.a5e9f5e1-fcee-c07b-2c44-b707ded020a3.AutoOpen", "8682d0fa-50b3-4ece-aa5b-e0b33f9919e2.f5b8c725-cb2e-4c12-accd-73e500d88d47.AutoOpen", "bc25fcef-8964-4e72-8287-23e2b496c128.68b8a884-6e08-46e6-8a3b-7e06ffe48ecf.AutoOpen", "EdgeDownloadChromeInterceptDialog", "439642fc-998d-4a64-8bb6-940ecaf6b60b.AutoOpen", "694efa4f-a304-4214-aaf7-90444bc887f5.c1c025d9-a620-4f6b-bebb-eff0cf19a671.AutoOpen" ], + "ReserveApproved": { + "ExperienceIDs": [ "Nurturing.Global.Edge_SpawnNtpOptIn", "Nurturing.Global.Edge_SpawnNtpOptOut" ], + "SourceIDs": [ 3 ], + "TeamIDs": [ "Edge Consumer Content and Verticals" ] + }, + "ScenarioSuppressLists": { + }, + "SuppressedExperiences": [ ], + "TimeDelta": 5000000, + "baseConfigVersion": "41.0.0", + "configVersion": "41.0.0" +} diff --git a/.tmp-edge-annexation/Default/commerce_subscription_db/LOCK b/.tmp-edge-annexation/Default/commerce_subscription_db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/commerce_subscription_db/LOG b/.tmp-edge-annexation/Default/commerce_subscription_db/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/declarative_performance_observer.db b/.tmp-edge-annexation/Default/declarative_performance_observer.db new file mode 100644 index 0000000..8ab056e Binary files /dev/null and b/.tmp-edge-annexation/Default/declarative_performance_observer.db differ diff --git a/.tmp-edge-annexation/Default/declarative_performance_observer.db-journal b/.tmp-edge-annexation/Default/declarative_performance_observer.db-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/discount_infos_db/LOCK b/.tmp-edge-annexation/Default/discount_infos_db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/discount_infos_db/LOG b/.tmp-edge-annexation/Default/discount_infos_db/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/discounts_db/LOCK b/.tmp-edge-annexation/Default/discounts_db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/discounts_db/LOG b/.tmp-edge-annexation/Default/discounts_db/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/load_statistics.db b/.tmp-edge-annexation/Default/load_statistics.db new file mode 100644 index 0000000..cc04c10 Binary files /dev/null and b/.tmp-edge-annexation/Default/load_statistics.db differ diff --git a/.tmp-edge-annexation/Default/load_statistics.db-shm b/.tmp-edge-annexation/Default/load_statistics.db-shm new file mode 100644 index 0000000..5e3f95f Binary files /dev/null and b/.tmp-edge-annexation/Default/load_statistics.db-shm differ diff --git a/.tmp-edge-annexation/Default/load_statistics.db-wal b/.tmp-edge-annexation/Default/load_statistics.db-wal new file mode 100644 index 0000000..026cdb0 Binary files /dev/null and b/.tmp-edge-annexation/Default/load_statistics.db-wal differ diff --git a/.tmp-edge-annexation/Default/parcel_tracking_db/LOCK b/.tmp-edge-annexation/Default/parcel_tracking_db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/parcel_tracking_db/LOG b/.tmp-edge-annexation/Default/parcel_tracking_db/LOG new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/000003.log b/.tmp-edge-annexation/Default/shared_proto_db/000003.log new file mode 100644 index 0000000..bd876be Binary files /dev/null and b/.tmp-edge-annexation/Default/shared_proto_db/000003.log differ diff --git a/.tmp-edge-annexation/Default/shared_proto_db/CURRENT b/.tmp-edge-annexation/Default/shared_proto_db/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/shared_proto_db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/LOCK b/.tmp-edge-annexation/Default/shared_proto_db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/LOG b/.tmp-edge-annexation/Default/shared_proto_db/LOG new file mode 100644 index 0000000..ded601d --- /dev/null +++ b/.tmp-edge-annexation/Default/shared_proto_db/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.088 1660 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\shared_proto_db since it was missing. +2026/09/16-15:38:01.099 1660 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\shared_proto_db/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/MANIFEST-000001 b/.tmp-edge-annexation/Default/shared_proto_db/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/shared_proto_db/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/Default/shared_proto_db/metadata/000003.log b/.tmp-edge-annexation/Default/shared_proto_db/metadata/000003.log new file mode 100644 index 0000000..ffddf8d Binary files /dev/null and b/.tmp-edge-annexation/Default/shared_proto_db/metadata/000003.log differ diff --git a/.tmp-edge-annexation/Default/shared_proto_db/metadata/CURRENT b/.tmp-edge-annexation/Default/shared_proto_db/metadata/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/.tmp-edge-annexation/Default/shared_proto_db/metadata/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/metadata/LOCK b/.tmp-edge-annexation/Default/shared_proto_db/metadata/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/metadata/LOG b/.tmp-edge-annexation/Default/shared_proto_db/metadata/LOG new file mode 100644 index 0000000..53ac895 --- /dev/null +++ b/.tmp-edge-annexation/Default/shared_proto_db/metadata/LOG @@ -0,0 +1,2 @@ +2026/09/16-15:38:01.073 1660 Creating DB C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\shared_proto_db\metadata since it was missing. +2026/09/16-15:38:01.084 1660 Reusing MANIFEST C:\Users\あ\Desktop\civil\.tmp-edge-annexation\Default\shared_proto_db\metadata/MANIFEST-000001 diff --git a/.tmp-edge-annexation/Default/shared_proto_db/metadata/MANIFEST-000001 b/.tmp-edge-annexation/Default/shared_proto_db/metadata/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/.tmp-edge-annexation/Default/shared_proto_db/metadata/MANIFEST-000001 differ diff --git a/.tmp-edge-annexation/FirstLaunchAfterInstallation b/.tmp-edge-annexation/FirstLaunchAfterInstallation new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.db b/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.db new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.db-wal b/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.journal b/.tmp-edge-annexation/GPUPersistentCache/DawnGraphiteCache/SWT6YJYIIFC43O4TK6OMZG5XCE3O73F5/cache.journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/GrShaderCache/data_0 b/.tmp-edge-annexation/GrShaderCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/GrShaderCache/data_0 differ diff --git a/.tmp-edge-annexation/GrShaderCache/data_1 b/.tmp-edge-annexation/GrShaderCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/GrShaderCache/data_1 differ diff --git a/.tmp-edge-annexation/GrShaderCache/data_2 b/.tmp-edge-annexation/GrShaderCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/GrShaderCache/data_2 differ diff --git a/.tmp-edge-annexation/GrShaderCache/data_3 b/.tmp-edge-annexation/GrShaderCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/GrShaderCache/data_3 differ diff --git a/.tmp-edge-annexation/GrShaderCache/index b/.tmp-edge-annexation/GrShaderCache/index new file mode 100644 index 0000000..423acaf Binary files /dev/null and b/.tmp-edge-annexation/GrShaderCache/index differ diff --git a/.tmp-edge-annexation/Last Version b/.tmp-edge-annexation/Last Version new file mode 100644 index 0000000..4f49660 --- /dev/null +++ b/.tmp-edge-annexation/Last Version @@ -0,0 +1 @@ +153.0.4234.32 \ No newline at end of file diff --git a/.tmp-edge-annexation/Local State b/.tmp-edge-annexation/Local State new file mode 100644 index 0000000..dc10d60 --- /dev/null +++ b/.tmp-edge-annexation/Local State @@ -0,0 +1 @@ +{"autofill":{"ablation_seed":"HKSKl2O3BtM="},"breadcrumbs":{"enabled":true,"enabled_time":"13434014280724571"},"edge":{"app_user_model_id":"MSEdge","perf_center":{"efficiency_mode_v2_is_active":false,"perf_game_mode":true,"performance_mode":3,"performance_mode_is_on":false},"retrigger_features":[],"tab_stabs":{"closed_without_unfreeze_never_unfrozen":0,"closed_without_unfreeze_previously_unfrozen":0,"discard_without_unfreeze_never_unfrozen":0,"discard_without_unfreeze_previously_unfrozen":0},"tab_stats":{"frozen_daily":0,"unfrozen_daily":0}},"edge_copilot":{"copilot_new_tab_page_enabled_commercial_state":{},"copilot_new_tab_page_enabled_consumer_state":{"Default":{"enabled":false,"last_set":"13434014280790412"}}},"edge_copilot_mode_enabled_state":{"Default":{"enabled":false,"last_set":"13434014280790397"}},"fre":{"has_first_visible_browser_session_completed":true,"oem_bookmarks_set":true},"hardware_acceleration_mode_previous":true,"host_package_checked_on_browser_version":"153.0.8010.37","legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"management":{"platform":{"azure_active_directory_device":0}},"metrics":{"client_id_hash_key_created":true},"microsoft":{"os":{"cached_windows_lockdown_mode":0}},"migration":{"Default":{"migration_attempt":0,"migration_version":6}},"network_time":{"network_time_mapping":{"local":1.789540681381816e+12,"network":1.789540681e+12,"ticks":252720733609.0,"uncertainty":1533953.0}},"optimization_guide":{"model_execution":{"last_usage_by_feature":{}},"model_store_metadata":{},"on_device":{"last_version":"153.0.4234.32","model_crash_count":0}},"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAAAmGzD2EbIzSoCX+X/eMZUsEAAAAB4AAABNAGkAYwByAG8AcwBvAGYAdAAgAEUAZABnAGUAAAAQZgAAAAEAACAAAABBlt4Mi/LVrvFFeoxJSQKMyGAmW23JBx2j6St35/o9iAAAAAAOgAAAAAIAACAAAACTkmqO+rINh5819T9k78Cv7W83vVWutGuKZgkT3+wzXDAAAADfOdn+MMqtYr/a1b29Z7lx/JTNZ+Nc0sKZjc84b5rfvt64vihf4QljMYgApJh+MedAAAAA5/lPcWRzb8Pz6EkImUpSzGWZIxitLZAt8QlwX0fyWLS4h0q1EBgmENLnND8DxAzZKVR+zAoEajKzZCyH+/yx9w=="},"password_manager":{"os_password_blank":false,"os_password_last_changed":"13395955497456508"},"performance_intervention":{"last_daily_sample":"13434014281059750"},"policy":{"last_statistics_update":"13434014280722373"},"profile":{"info_cache":{"Default":{"active_time":1789540681.044701,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_20","background_apps":false,"edge_account_cid":"","edge_account_environment":0,"edge_account_environment_string":"","edge_account_first_name":"","edge_account_last_name":"","edge_account_oid":"","edge_account_sovereignty":0,"edge_account_tenant_id":"","edge_account_type":0,"edge_create_profile_shortcut":false,"edge_profile_can_be_deleted":true,"edge_profile_can_be_edited":true,"edge_test_on_premises":false,"edge_wam_aad_for_app_account_type":0,"force_signin_profile_locked":false,"gaia_id":"","is_consented_primary_account":false,"is_ephemeral":false,"is_using_default_avatar":true,"is_using_default_name":true,"managed_user_id":"","metrics_bucket_index":1,"name":"プロファイル 1","signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":[],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13434014280699032","profiles_order":["Default"]},"profiles":{"collect_potential_implicit_signin_data_started":true,"current_profile_name":"","edge":{"guided_switch_pref":[],"multiple_profiles_with_same_account":false},"edge_sso_info":{"aad_sso_algo_state":1,"first_profile_key":"Default","msa_first_profile_key":"Default","msa_sso_algo_state":1},"signin_last_seen_version":"153.0.4234.32","signin_last_updated_time":1789540680.864978},"sentinel_creation_time":"0","session_id_generator_last_value":"1631610516","shortcut_migration_failed":{},"signin":{"active_accounts_last_emitted":"13434014280647342"},"smartscreen":{"enabled":true,"pua_protection_enabled":false},"startup_boost":{"last_browser_open_time":"13434014281976552"},"subresource_filter":{"ruleset_version":{"checksum":0,"content":"","format":0}},"tab_stats":{"discards_expired":0,"discards_external":0,"discards_proactive":0,"discards_urgent":0,"last_daily_sample":"13434014280708337","max_tabs_per_window":1,"reloads_expired":0,"reloads_external":0,"reloads_urgent":0,"total_tab_count_max":1,"window_count_max":1},"telemetry_client":{"governance":{"last_dma_change_date":"13434014281068809","last_known_cps":2048},"install_source_name":"windows","os_integration_level":5,"sample_id":73827110,"updater_version":"1.3.269.9","windows_update_applied":false},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1789540680"},"updateclientdata":{"apps":{"nmcnjaceoejcjmmnabjnmfpoanaengif":{"cohort":"","cohortname":"","installdate":-1}}},"updateclientlastupdatecheckerror":0,"updateclientlastupdatecheckerrorcategory":0,"updateclientlastupdatecheckerrorextracode1":0,"user_experience_metrics":{"client_id2":"36feeaed-2fd7-4d60-bc26-c293626a3920","diagnostics":{"last_data_collection_level_on_launch":1},"last_seen":{"CrashpadMetrics":"0"},"limited_entropy_randomization_source":"1839421E2AD7A69C9FEC519C4814644C","log_record_id":1,"low_entropy_source3":1198,"payload_counter":1,"pseudo_low_entropy_source":4846,"reporting_enabled":false,"session_id":0,"stability":{"browser_last_live_timestamp":"13434014282030825","exited_cleanly":true,"saved_system_profile":"CNz6idUGEhAxNTMuMC40MjM0LjMyLTY0GAAiAmphKhgKCldpbmRvd3MgTlQSCjEwLjAuMTkwNDUy/wEKBng4Nl82NBCsPxiAgIzUvf8fIg1TdXJmYWNlIFBybyA2KAEwoAY42ARCoQEI//8DEP//AxoPMTAuMC4xOTA0MS41Nzk0MhdHb29nbGUgSW5jLiAoTWljcm9zb2Z0KTptQU5HTEUgKE1pY3Jvc29mdCwgTWljcm9zb2Z0IEJhc2ljIFJlbmRlciBEcml2ZXIgKDB4MDAwMDAwOEMpIERpcmVjdDNEMTEgdnNfNV8wIHBzXzVfMCwgRDNEMTEtMTAuMC4xOTA0MS41Nzk0KWUAAIA/ahgKDEdlbnVpbmVJbnRlbBDqjSAYCCABKACCAQCKAQCqAQZ4ODZfNjSwAQFKCg1RzEZ7FV5v0thKCg0XamWsFV5v0thKCg0q8Z62FV5v0thKCg3+csHFFV5v0thKCg1j77SyFV5v0thKCg0mSEqMFV5v0thKCg3ZeE7NFV5v0thKCg1sO7s6FV5v0thKCg000MctFV5v0thKCg2wI5tUFV5v0thKCg0u27krFV5v0thKCg0aNlUWFV5v0thKCg0BtEuYFV5v0thKCg0di/H9FV5v0thKCg1oRsjCFV5v0thKCg2mF7kVFV5v0thKCg1/R7bhFV5v0thKCg12s3kbFV5v0thKCg1M1VTRFV5v0thKCg33qzt9FV5v0thKCg1PeurXFV5v0thKCg36dPqxFV5v0thKCg3D5tnNFV5v0thKCg3w7d/MFV5v0thKCg1hnI/EFV5v0thKCg0eAaq/FV5v0thKCg3CF1/rFV5v0thKCg3OpnZ5FV5v0thKCg2BxjX/FV5v0thKCg0xzNxQFV5v0thKCg2582szFV5v0thKCg1I2SLMFV5v0thKCg2nASvgFV5v0thKCg0m3XTxFV5v0thKCg3QYz3lFV5v0thKCg2asUr6FV5v0thKCg1PUrdjFV5v0thKCg2YtjTAFV5v0thKCg0zDyjSFV5v0thKCg1GSINcFV5v0thKCg0XP2exFV5v0thKCg1//2a5FV5v0thKCg049pOrFV5v0thKCg325yX7FV5v0thKCg0T5AAxFV5v0thKCg0tk3UGFV5v0thKCg2y5e9dFV5v0thKCg0kNU3vFV5v0thKCg2VSTIFFV5v0thKCg2TOIx9FV5v0thKCg0bg+VeFV5v0thKCg23YPQsFV5v0thKCg3FXD5eFV5v0thKCg3cB6stFV5v0thKCg11bxDyFV5v0thKCg1ZIj2uFV5v0thKCg3FI62kFV5v0thKCg29RVGmFV5v0thKCg1+1JR4FV5v0thKCg3F/GlqFV5v0thKCg1DP34AFV5v0thKCg3bdYIvFV5v0thKCg2Xf+mCFV5v0thKCg3yhRgaFV5v0thKCg1TthBPFV5v0thKCg0XBxSpFV5v0thKCg0lwP8LFV5v0thKCg0wHBJiFV5v0thKCg2VI2gIFV5v0thKCg0e4KKgFV5v0thKCg1YZnHiFV5v0thKCg2yvv3GFV5v0thKCg1PqhG7FV5v0thKCg162/tNFV5v0thKCg2KcLlyFV5v0thKCg0/VavqFV5v0thKCg10qxB8FV5v0thKCg1bcsQMFV5v0thKCg110yzsFV5v0thKCg22X0dGFV5v0thKCg3BQDMDFV5v0thKCg21ug1sFV5v0thKCg368j0fFV5v0thKCg0ww9QEFV5v0thKCg0VdhIMFV5v0thKCg1xm2qHFV5v0thKCg1CEMqpFV5v0thKCg1ty53VFV5v0thKCg0YuQVIFV5v0thKCg3prlK5FV5v0thKCg31nUNJFV5v0thKCg3acGbGFV5v0thKCg13T3WqFV5v0thKCg1zkXSHFV5v0thKCg0K+kPJFV5v0thKCg2fMQwOFV5v0thKCg26eLy1FV5v0thKCg12hbdCFV5v0thKCg0xznI4FV5v0thKCg3IafLYFV5v0thKCg0zaYLUFV5v0thKCg2guYmMFV5v0thKCg0U48GTFV5v0thKCg1ATvQDFV5v0thKCg0ioHptFV5v0thKCg0+e2DXFV5v0thKCg1dC1ESFV5v0thKCg1UswobFV5v0thKCg1gVIQqFV5v0thKCg26x7w6FV5v0thKCg2b075iFV5v0thKCg2BnYM2FV5v0thKCg30K8iZFV5v0thKCg0T2ptcFV5v0thKCg3cfZmdFV5v0thKCg0cPl9LFV5v0thKCg1cPcHJFV5v0thKCg37sqKlFV5v0thKCg10o7O1FV5v0thKCg3PN1kEFV5v0thKCg3E3bbfFV5v0thKCg3nmSEXFV5v0thKCg0zG0OnFV5v0thKCg35GDykFV5v0thKCg1wG9LNFV5v0thKCg0H21nRFV5v0thKCg2E2yLCFV5v0thKCg0LUJIjFV5v0thKCg3zPaBDFV5v0thKCg3WRWneFV5v0thKCg2+7FVmFV5v0thKCg3EVN6FFV5v0thKCg0gq0W9FV5v0thKCg3cl9xGFV5v0thKCg08LXuCFV5v0thKCg2Q6as/FV5v0thKCg2kcvi3FV5v0thKCg1DO2FuFV5v0thKCg2bQnAeFV5v0thKCg15j52rFV5v0thKCg3jOshPFV5v0thKCg1vcX2jFV5v0thQBGIESU5CWGoICAAQADgGQAaAAeDgqNUGkgERBZECvAPIA98DzwSFBaMFrwaYAQDCAR4ILxIQMS4wLjcuMTY1MjkwNjgyMx0AAAAAJQAAAADKAV4IARAFGAIgACgAMAA4AEAAUABYAGAAaAB4AIABAIgBAMg+A9I+IGRnaWtsa2ZrbGxpa2NhbmZvbmtjYWJtYmRmbWdsZWFn2j4DMS4w4D7g4KjVBuo+AQDwPuDgqNUGygFgCAEQBRgCIAAoADAAOABAAFAAWABgAGgCeACAAQCIAQDIPgPSPiBmaWtiamJlbWJubWZocHBqZm5tZmthaGRoZm9oaGptZ9o+BTIuMC4w4D7g4KjVBuo+AQDwPuDgqNUGygFeCAQQBRgBIAAoADAAOABAAFAAWABgAGgAeACAAQCIAQDIPgPSPiBpZ2xjamRlbWtuZWJqYmtsY2drZmFlYmdvampwaGtlY9o+AzAuMuA+4OCo1QbqPgEA8D7g4KjVBsoBYggGEAUYAiAAKAAwADgAQABQAFgAYABoAngAgAEAiAEAyD4D0j4gaWhtYWZsbGlraWJwbWlna2NvYWRjbWNrYmZoaWJlZnDaPgcxLjAuMC4x4D7g4KjVBuo+AQDwPuDgqNUGygFeCAEQBRgCIAAoADAAOABAAFAAWABgAGgCeACAAQCIAQDIPgPSPiBqZGljY2xkaW1wZGFpYm1wZGtqbmJtY2tpYW5iZm9sZNo+AzEuMOA+4OCo1QbqPgEA8D7g4KjVBsoBXAgBEAUYAiAAKAAwADgAQABQAFgAYABoAHgAgAEAiAEAyD4D0j4gbWhqZmJtZGdjZmpiYnBhZW9qb2ZvaG9lZmdpZWhqYWnaPgEx4D7g4KjVBuo+AQDwPuDgqNUGygFgCAEQBRgCIAAoADAAOABAAFAAWABgAGgCeACAAQCIAQDIPgPSPiBuY2JqZWxwamNoa3BiaWticGtjY2hraGtibG9kb2FtYdo+BTIuMC404D7g4KjVBuo+AQDwPuDgqNUGygFoCAEQBRgDIAAoADAAOABAAFAAWABgAGgDeACAAQCIAQDIPgPSPiBuZGNwa2ltY2loaGdoZGNkZGxqa2ZtbWpjY2RtY21vZto+DTE1My4wLjQyMzQuMjHgPuDgqNUG6j4BAPA+4OCo1QbKAWEIARAFGAIgACgAMAA4AEAAUABYAGAAaAJ4AIABAIgBAMg+A9I+IG5rZWltaG9namRwbnBjY29vZnBsaWltYWFobWFhb21l2j4GNC4zLjI24D7g4KjVBuo+AQDwPuDgqNUG+AGuCYAC////////////AYgCAagC7iWyAiQZDNsJEAA8k99sKg3Du9g0N0RE6jkL3aJfDgvATPMNXYpWgDfxAtnQx2EdOuHGigMDCgEAyj6lAwoECAAQABIYCgIIBRICCAgaAggBIgQIAhACKAEyAggBIgIIAjI0CAAiLiJxT2ZLNlllbzhDcnNwMERKRSs0SUlmR3NJR0pEN1V3ZkRPanVtcE8xS2o0PSIqADoLCPP//+/3/////wFCKBIJMS4zLjI2OS45GAUgACoOUmVnS2V5Tm90Rm91bmQyB3dpbmRvd3NSAggAWvwBCT81XrpJLDpAESlcj8L1KCxAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAPA/GQAAAAAAAPA/GQAAAAAAAAAAGQAAAAAAAPA/GQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAFlAGQAAAAAAAAAAGQAAAAAAAFlAGQAAAAAAAPA/GQAAAAAAAFlAGQAAAAAAAPA/GQAAAAAAAPA/GQAAAAAAADRAGQAAAAAAAPA/GQAAAAAAAFlAGQAAAAAAAFlAegIIAYIBAhgAqgECCgCwAQA=","saved_system_profile_hash":"F491CF17135DC6972DF1C7A03A183EBCDEAD4F1D","stats_buildtime":"1789033820","stats_version":"153.0.4234.32-64","system_crash_count":0}},"variations_country":"JP","variations_crash_streak":0,"variations_failed_to_fetch_seed_streak":0,"variations_geo_level1":"","variations_google_groups":{"Default":[]},"variations_permanent_consistency_country":["153.0.4234.32","JP"],"variations_seed_client_version_at_store":"153.0.4234.32","variations_seed_etag":"\"qOfK6Yeo8Crsp0DJE+4IIfGsIGJD7UwfDOjumpO1Kj4=\"","variations_seed_serial_number":"\"qOfK6Yeo8Crsp0DJE+4IIfGsIGJD7UwfDOjumpO1Kj4=\"","was":{"restarted":false}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Nurturing/campaign_history b/.tmp-edge-annexation/Nurturing/campaign_history new file mode 100644 index 0000000..9c6cef1 Binary files /dev/null and b/.tmp-edge-annexation/Nurturing/campaign_history differ diff --git a/.tmp-edge-annexation/Nurturing/campaign_history-journal b/.tmp-edge-annexation/Nurturing/campaign_history-journal new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-annexation/ShaderCache/data_0 b/.tmp-edge-annexation/ShaderCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/.tmp-edge-annexation/ShaderCache/data_0 differ diff --git a/.tmp-edge-annexation/ShaderCache/data_1 b/.tmp-edge-annexation/ShaderCache/data_1 new file mode 100644 index 0000000..dcaafa9 Binary files /dev/null and b/.tmp-edge-annexation/ShaderCache/data_1 differ diff --git a/.tmp-edge-annexation/ShaderCache/data_2 b/.tmp-edge-annexation/ShaderCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/.tmp-edge-annexation/ShaderCache/data_2 differ diff --git a/.tmp-edge-annexation/ShaderCache/data_3 b/.tmp-edge-annexation/ShaderCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/.tmp-edge-annexation/ShaderCache/data_3 differ diff --git a/.tmp-edge-annexation/ShaderCache/index b/.tmp-edge-annexation/ShaderCache/index new file mode 100644 index 0000000..dafa201 Binary files /dev/null and b/.tmp-edge-annexation/ShaderCache/index differ diff --git a/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings b/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings new file mode 100644 index 0000000..997f02d --- /dev/null +++ b/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings @@ -0,0 +1 @@ +customSettings_F95BA787499AB4FA9EFFF472CE383A14 \ No newline at end of file diff --git a/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings_F95BA787499AB4FA9EFFF472CE383A14 b/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings_F95BA787499AB4FA9EFFF472CE383A14 new file mode 100644 index 0000000..ae2fefa --- /dev/null +++ b/.tmp-edge-annexation/SmartScreen/RemoteData/customSettings_F95BA787499AB4FA9EFFF472CE383A14 @@ -0,0 +1 @@ +{"forceServiceDetermination":false} \ No newline at end of file diff --git a/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings b/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings new file mode 100644 index 0000000..a89ca1f --- /dev/null +++ b/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings @@ -0,0 +1 @@ +edgeSettings_2.0-0 \ No newline at end of file diff --git a/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings_2.0-0 b/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings_2.0-0 new file mode 100644 index 0000000..3db8ccb --- /dev/null +++ b/.tmp-edge-annexation/SmartScreen/RemoteData/edgeSettings_2.0-0 @@ -0,0 +1 @@ +{"models":[],"geoidMaps":{"gw_my":"https://malaysia.smartscreen.microsoft.com/","gw_tw":"https://taiwan.smartscreen.microsoft.com/","gw_at":"https://austria.smartscreen.microsoft.com/","gw_es":"https://spain.smartscreen.microsoft.com/","gw_pl":"https://poland.smartscreen.microsoft.com/","gw_se":"https://sweden.smartscreen.microsoft.com/","gw_kr":"https://southkorea.smartscreen.microsoft.com/","gw_br":"https://brazil.smartscreen.microsoft.com/","au":"https://australia.smartscreen.microsoft.com/","dk":"https://denmark.smartscreen.microsoft.com/","gw_sg":"https://singapore.smartscreen.microsoft.com/","gw_fr":"https://france.smartscreen.microsoft.com/","gw_ca":"https://canada.smartscreen.microsoft.com/","gw_il":"https://israel.smartscreen.microsoft.com/","gw_au":"https://australia.smartscreen.microsoft.com/","gw_ffl4mod":"https://unitedstates4.ss.wd.microsoft.us/","gw_ffl4":"https://unitedstates1.ss.wd.microsoft.us/","gw_eu":"https://europe.smartscreen.microsoft.com/","gw_gr":"https://greece.smartscreen.microsoft.com/","gw_de":"https://germany.smartscreen.microsoft.com/","br":"https://brazil.smartscreen.microsoft.com/","gw_uk":"https://unitedkingdom.smartscreen.microsoft.com/","gw_it":"https://italy.smartscreen.microsoft.com/","gw_us":"https://unitedstates.smartscreen.microsoft.com/","il":"https://israel.smartscreen.microsoft.com/","es":"https://spain.smartscreen.microsoft.com/","ch":"https://switzerland.smartscreen.microsoft.com/","at":"https://austria.smartscreen.microsoft.com/","jp":"https://japan.smartscreen.microsoft.com/","kr":"https://southkorea.smartscreen.microsoft.com/","nz":"https://newzealand.smartscreen.microsoft.com/","gw_cl":"https://chile.smartscreen.microsoft.com/","pl":"https://poland.smartscreen.microsoft.com/","eu":"https://europe.smartscreen.microsoft.com/","de":"https://germany.smartscreen.microsoft.com/","gw_no":"https://norway.smartscreen.microsoft.com/","ffl4mod":"https://unitedstates4.ss.wd.microsoft.us/","gw_jp":"https://japan.smartscreen.microsoft.com/","mx":"https://mexico.smartscreen.microsoft.com/","gw_ffl5":"https://unitedstates2.ss.wd.microsoft.us/","se":"https://sweden.smartscreen.microsoft.com/","gw_ch":"https://switzerland.smartscreen.microsoft.com/","gw_ae":"https://uae.smartscreen.microsoft.com/","my":"https://malaysia.smartscreen.microsoft.com/","gw_nz":"https://newzealand.smartscreen.microsoft.com/","ca":"https://canada.smartscreen.microsoft.com/","fr":"https://france.smartscreen.microsoft.com/","gw_mx":"https://mexico.smartscreen.microsoft.com/","no":"https://norway.smartscreen.microsoft.com/","gr":"https://greece.smartscreen.microsoft.com/","gw_qa":"https://qatar.smartscreen.microsoft.com/","gw_in":"https://india.smartscreen.microsoft.com/","in":"https://india.smartscreen.microsoft.com/","tw":"https://taiwan.smartscreen.microsoft.com/","sg":"https://singapore.smartscreen.microsoft.com/","ffl5":"https://unitedstates2.ss.wd.microsoft.us/","ae":"https://uae.smartscreen.microsoft.com/","gw_dk":"https://denmark.smartscreen.microsoft.com/","gw_za":"https://southafrica.smartscreen.microsoft.com/","uk":"https://unitedkingdom.smartscreen.microsoft.com/","cl":"https://chile.smartscreen.microsoft.com/","us":"https://unitedstates.smartscreen.microsoft.com/","qa":"https://qatar.smartscreen.microsoft.com/","za":"https://southafrica.smartscreen.microsoft.com/","ffl4":"https://unitedstates1.ss.wd.microsoft.us/","it":"https://italy.smartscreen.microsoft.com/"},"sampleBuckets":{"evaluateModel":1.0,"error":1.0,"uriLookup":0.01,"userAction":1.0,"topTrafficHit":0.00007}} \ No newline at end of file diff --git a/.tmp-edge-annexation/Variations b/.tmp-edge-annexation/Variations new file mode 100644 index 0000000..18056c3 --- /dev/null +++ b/.tmp-edge-annexation/Variations @@ -0,0 +1 @@ +{"user_experience_metrics.stability.exited_cleanly":true,"variations_crash_streak":0} \ No newline at end of file diff --git a/.tmp-edge-annexation/VariationsRuntimeSeedV2 b/.tmp-edge-annexation/VariationsRuntimeSeedV2 new file mode 100644 index 0000000..02d4f75 Binary files /dev/null and b/.tmp-edge-annexation/VariationsRuntimeSeedV2 differ diff --git a/.tmp-edge-annexation/VariationsSafeSeedV2 b/.tmp-edge-annexation/VariationsSafeSeedV2 new file mode 100644 index 0000000..1c46953 Binary files /dev/null and b/.tmp-edge-annexation/VariationsSafeSeedV2 differ diff --git a/.tmp-edge-annexation/VariationsSeedV2 b/.tmp-edge-annexation/VariationsSeedV2 new file mode 100644 index 0000000..9e5fb65 Binary files /dev/null and b/.tmp-edge-annexation/VariationsSeedV2 differ diff --git a/.tmp-edge-annexation/en-US-10-1.bdic b/.tmp-edge-annexation/en-US-10-1.bdic new file mode 100644 index 0000000..a453358 Binary files /dev/null and b/.tmp-edge-annexation/en-US-10-1.bdic differ diff --git a/MODIFICATION_NOTES.md b/MODIFICATION_NOTES.md new file mode 100644 index 0000000..062a9b6 --- /dev/null +++ b/MODIFICATION_NOTES.md @@ -0,0 +1,321 @@ +# 文明シミュレータ 改修記録 + +## 目的 + +ユーザー指定の操作系・反乱・交易路・戦争損害・地形計算の変更を、既存シミュレーションの構造を維持したまま追加する。 + +## 実装した変更 + +- 「1週進める」ボタンを削除。 +- 初期人口スライダーを削除。初期人口は標準値 50,000 に固定。 +- 「再生成」を押すとダイアログを開き、その時点で世界サイズ(128 / 160 / 220 / 280)を選択する方式に変更。 +- 反乱ロジックを変更。 + - 忠誠度 30% 未満の都市が1つでもあれば反乱を自動発生。 + - 反乱が発生した場合、首都を除く忠誠度 50% 未満の全都市が同時に同一反乱勢力へ参加。 + - 30〜50%の範囲では従来のランダム性を残しつつ、忠誠度・不安定度・国家年齢から年次発生確率を算出。 +- 「災害発生」「都市建設」「交通接続」の3ボタンを追加。 + - 災害発生: 地図上の指定地点を中心に即時発生。 + - 都市建設: 地図上のクリック地点へ都市を即時建設。ユーザー建設都市は都市間隔・自動都市数上限を無視するが、水域には建設できない。 + - 交通接続: 都市を2つ順にクリックして交易路を作成。 +- ユーザー作成交易路に内部保護期間を追加し、期間終了後は通常管理へ戻るよう変更。 +- 戦争による都市人口損失と戦場タイル人口損失を従来の約 65% に軽減。 + - 戦争発生頻度・勝敗・占領判定そのものは変更していない。 +- 水域の肥沃度を計算対象外にし、常に 0 とするよう変更。 +- 既存コードの堅牢性修正。 + - `getCityById` / `getPolityById` がキャッシュ未初期化のテスト・部分実行でも動作するよう修正。 + - 完全に囲まれた小さな中立穴を領土平滑化で埋められるよう修正。 + +## 追加修正(連続介入・ヘルプ) + +- 災害発生・都市建設・交通接続が、1回成功するたびに解除されていた挙動を修正。選択した介入は同じボタンを再度押すまで継続する。 +- 交通接続は1本作成後、同じモードのまま次の1つ目の都市選択へ戻る。 +- ヘルプを現行仕様に合わせて更新し、説明量を圧縮。 +- 交通接続完了メッセージから保護期間に関する説明を削除。 + +## 追加修正(砂漠交易路・手動都市民族・征服後反乱) + +- 砂漠横断交易路の成立条件を改善。 + - 交易路経路探索では砂漠の移動コストを通常移動より軽く評価し、隊商路として横断しやすくした。 + - 都市間の直線上に砂漠が多い場合、交易路の有効距離上限を段階的に拡張する。 + - 砂漠を多く通る経路では、新規交易路に必要なフェロモン閾値と建設コストを緩和する。 + - 水域は従来どおり交易路不可。山地の高コストも維持。 +- ユーザー建設都市の民族構成を修正。 + - 建設地点の民族構成を優先して継承。 + - 地点に民族が存在しない場合は最寄りの民族構成を継承。 + - 世界内に参照可能な民族がない場合も、新しい現地民族を生成して民族構成が空にならないようにした。 +- 征服された都市に20年間の反乱猶予期間を追加。 + - 戦争占領、国境戦争、軍事併合、講和による都市移譲、遊牧民征服を対象とする。 + - 猶予期間中は忠誠度が30%未満でも反乱を自動発生せず、他都市の反乱にも参加しない。 + - 猶予終了後は通常の反乱判定へ戻る。 + +## 追加修正(国家ツールチップ・制度老朽化・制度改革) + +- 国家領土または国家所属都市にカーソルを合わせた際、ツールチップに「国家データ」を表示。 + - 国力、人口、都市数、国庫、正統性 / 結束、制度老朽化、忠誠度上限を表示する。 +- 国家に `institutionalAging`(制度老朽化度、0~1)を追加。 + - 国家成立後、年次更新ごとに徐々に増加する。 + - 忠誠度上限を `1 - institutionalAging` とし、首都を含む所属都市の忠誠度はこの上限を超えない。 +- 「制度改革」イベントを追加。 + - 老朽化度が30%を超えてから発生候補となり、老朽化が高いほど年次発生確率が上昇する。 + - 発生時は現在の老朽化度に対して -50%~+30% の相対変化をランダムに適用する。改革が逆効果になる場合もある。 + - 同一国家での連続発生を抑えるため8年間のクールダウンを設ける。 + - 国家興亡グラフへ専用の「制度改革」マーカーを記録し、老朽化度の変化前後と変化率を表示する。 + - 国家履歴サンプルにも制度老朽化度と忠誠度上限を保存する。 + +## 追加整理(不要処理・ツールチップ・回帰試験) + +- 都市ツールチップを「都市データ」と「国家データ」に分離し、民族構成の割合表示を都市データ内へ移動。 +- 国家人口表示は既存の `totalPolityPopulation()` を利用し、ツールチップ独自の人口集計を廃止。 +- 未参照の設定値11個を `config.js` から削除。 + - 人口: `softCapStart`, `hardCapScale`, `pressurePenaltyBase`, `pressureReproductionPenalty` + - 都市: `agingRestoreMinWear`, `agingRestoreCostPerWear`, `agingRestoreBudgetShare` + - 技術: `cityDiffusion` + - 文化: `spreadRadius`, `cityWeight`, `routeWeight` +- 未参照の描画/共通ヘルパー6個を削除。 +- 未参照または明示的に停止済みだったSimulationメソッド12個と、それ専用だった文化スクラッチ状態を削除。 + - 特に先頭で `return` していた旧 `expandPolities()` / `absorbIndependentCities()` を完全削除。 + - 旧地域文化更新・旧収容力単発計算・旧都市老朽化補助など、現行処理と責務が重複したコードを除去。 +- `annexation_policy_probe.html` を削除。内容は `tests/simulation.test.cjs` のNode回帰試験へ統合し、ブラウザ専用の古いモックを廃止。 +- 静的棚卸しで、`SimConfig` の未参照leaf設定値と明白な未参照関数/メソッドが0件であることを確認。 +- 制度改革の発生開始を「制度老朽化30%超」に変更。30%ちょうどでは発生しない。 + + +## 追加修正(ヘルプ・国庫上限・性能) + +- ヘルプに主要パラメータの短い説明を追加。 + - 人口 / 人口圧力、資源 / 肥沃度、農業 / 冶金、交通量、忠誠度、国庫、正統性 / 結束、制度老朽化、国力を対象とする。 +- 国家国庫に人口比例の動的上限を追加。 + - 上限は `国家人口 × 0.30`。人口増減に応じて毎年再評価する。 + - 国家ツールチップは重複表示を避けるため国庫残高のみを1行で表示する。動的上限は内部会計ロジックとヘルプで説明する。 + - 反乱時は、反乱人口比に応じて旧国家の国庫の一部を反乱国へ移す。移転額は `旧国庫 × 反乱人口比 × 0.65`。 + - 反乱後は旧国家・反乱国家とも、それぞれの新しい人口に応じた国庫上限へ収める。 +- 計算負荷をプロファイルし、結果を変えない範囲を優先して軽量化。 + - 世界生成の湿度計算を、各タイル半径10の総当たり探索から2パスのマンハッタン距離変換へ変更。旧方式との比較で浮動小数点誤差範囲まで一致することを確認。 + - 沿岸肥沃度補正は49マスの矩形走査から、実際に寄与するマンハッタン距離3以内のオフセットだけを走査する方式へ変更。 + - 人口収容力のうち、地形・肥沃度・鉱物由来の静的成分を初期化時にキャッシュ。年次処理で不要な全マップ再計算も削減し、人口だけが変わる段階では収容力の再計算をせず、圧力更新に限定。 + +## 追加修正(表示整理・都市間隔・追加軽量化) + +- 国家ツールチップの国庫表示を1行へ統一し、同じ国庫情報が二重に見える表示を解消。 +- 国家興亡グラフに「出来事アイコン」切替を追加。制度改革を含むイベントマーカーと凡例をまとめて表示 / 非表示にできる。 +- 自然生成都市の既存都市からの必要距離を従来のおおむね半分へ縮小。 + - 自然生成の最小都市間隔は `6`、競合評価半径は `9` とする。 +- ユーザー建設都市は通常の都市間隔・自動都市数上限制限を無視する。 + - ただし水域への建設は禁止し、陸地の指定座標へ直接建設する。 +- 追加プロファイルに基づき、通常進行をさらに軽量化。 + - 毎週相当で全世界を走査していた世界フィールド処理を月次へ集約。 + - 交易フェロモンは全タイル走査をやめ、値が存在するタイルと隣接域だけを疎更新。 + - 資源再生は四半期ごとにまとめ、年間再生量を維持。 + - 人口収容力は必要な変化時と年次処理に限定して再計算。 + - 民族統計の毎週更新を廃止し、履歴記録・年次処理で必要な時だけ更新。 + - 旧互換用で実利用されていなかった人口 `pressure` 配列と更新処理を削除。 + - 年次更新で重複呼び出しされていた国家危機判定を1回へ整理し、負荷とイベント判定回数の重複を解消。 + +## 追加修正(都市数上限・手動都市の水域制限) + +- 自動生成都市数の上限を `300` から `1200` へ引き上げ。 + - 自然発生都市・遠征による前哨都市など、自動生成側の共通上限として適用する。 + - ユーザー建設都市はこの自動上限を引き続き無視する。 +- ユーザー建設都市の水域建設を禁止。 + - 水域をクリックした場合は都市を生成せず、陸地を選択するようUIへ案内する。 + - 都市間隔制限は引き続き無視できるため、陸地上では既存都市の隣接地点にも建設可能。 + +## v9: 自然都市数の成長停滞を修正 + +### 原因 + +自然都市の創設判定が20年に1回で、1000年までは1回につき最大1都市に固定されていた。最大280×280マップでも創設流量が面積に比例せず、都市上限を1200へ増やしても約60都市前後で成長が鈍化しやすかった。 + +### 変更 + +- 自然都市の創設判定周期を20年から5年へ短縮。 +- 1回の創設可能数を世界面積に応じて増加。160×160で1、220×220で2、280×280で4都市まで。 +- 人口・定住性・資源・地形・既存都市との距離・創設確率は従来どおり判定し、無条件生成にはしない。 +- 面積スケーリングは `naturalCityFoundingLimit()` に集約し、設定値から調整できるようにした。 + +最大280×280では、同じ期間あたりの理論上の自然都市創設流量は旧仕様の最大16倍になる。実際の都市数は候補地、人口、都市消滅、戦争などで変動する。 + + +## v10: 都市人口・都市アイコン・民族動態の調整 + +- 再生成ダイアログから不要になった初期人口固定の案内を削除。 +- 新規都市の初期人口を自然生成・ユーザー建設ともに400へ統一。 +- 都市は人口200未満で消滅する。200ちょうどは存続可能。 +- 都市アイコンを人口帯で4段階に固定。 + - 200–999: 最小 + - 1000–3999: 中 + - 4000–5999: 大 + - 6000以上: 最大 +- 民族の新規分化速度を従来の1/4へ低下。分化判定間隔を4倍にし、分化条件そのものは維持する。 +- 総人口30未満の極小民族は年次処理で周辺民族へ吸収する。 + - 同一地点・隣接地点・同一都市で接触している民族を優先する。 + - 周辺に候補がない場合は親系統、さらに最寄りの存続民族へ統合する。 + - タイル人口、都市民族構成、遊牧集団の所属を移し替えるため、吸収によって総人口は減らない。 +- 旧仕様の「都市人口を populationScale に合わせて10倍化する」ことだけを確認していた回帰試験を削除し、現行仕様を直接検査する試験へ置換。 + +## v11: 都市表示・制度改革クールダウン・国庫都市建設 + +- 都市アイコンの人口帯を変更。 + - 200–999: 最小 + - 1000–4999: 中 + - 5000–9999: 大 + - 10000以上: 最大 +- 制度改革の再発クールダウンを50年へ変更。制度老朽化30%超という発生開始条件は維持する。 +- 国庫が人口比例上限に達した国家は、年次14%の確率で都市建設を試みる。 + - 建設成功時は、その時点の国庫上限の35%を消費する。 + - 建設候補は自国領土内の陸地に限定し、自然都市と同じ最小都市間隔を維持する。 + - 候補探索は全世界走査ではなく既存都市の周辺領土に限定し、国家数増加時の追加負荷を抑える。 + - 新都市は人口400で成立し、周辺の民族構成を継承する。 +- ヘルプの国庫・制度老朽化説明を新仕様へ同期。 + +## 主な設定値 + +`config.js` で変更可能。 + +- `SimConfig.world.initialPopulation`: 50000 +- `SimConfig.world.defaultSize`: 160 +- `SimConfig.city.maxCities`: 1200 +- `SimConfig.city.initialPopulation`: 400 +- `SimConfig.city.abandonmentPopulation`: 200 +- `SimConfig.city.naturalFoundingMinDistance`: 6 +- `SimConfig.city.naturalFoundingStartYears`: 160 +- `SimConfig.city.naturalFoundingIntervalYears`: 5 +- `SimConfig.city.naturalFoundingReferenceArea`: 25600 +- `SimConfig.city.naturalFoundingMaxPerCycle`: 8 +- `SimConfig.city.foundingCompetitionRadius`: 9 +- `SimConfig.culture.divergenceRateMultiplier`: 0.25 +- `SimConfig.culture.absorptionPopulationThreshold`: 30 +- `SimConfig.rebellion.participationLoyalty`: 0.50 +- `SimConfig.rebellion.automaticLoyalty`: 0.30 +- `SimConfig.rebellion.conquestGraceYears`: 20 +- `SimConfig.rebellion.treasuryTransferRate`: 0.65 +- `SimConfig.route.manualProtectionYears`: 25 +- `SimConfig.route.manualRouteMaxLength`: 120 +- `SimConfig.route.desertTraversalCostFactor`: 0.68 +- `SimConfig.route.desertRouteLengthBonus`: 30 +- `SimConfig.route.desertAdaptiveMaxRouteLength`: 96 +- `SimConfig.war.damageMultiplier`: 0.65 +- `SimConfig.polity.institutionalAgingAnnualIncrease`: 0.0015 +- `SimConfig.polity.institutionalReformThreshold`: 0.30 +- `SimConfig.polity.institutionalReformBaseChance`: 0.008 +- `SimConfig.polity.institutionalReformChanceScale`: 0.10 +- `SimConfig.polity.institutionalReformMaxChance`: 0.12 +- `SimConfig.polity.institutionalReformCooldownYears`: 50 +- `SimConfig.polity.institutionalReformMinChange`: -0.50 +- `SimConfig.polity.institutionalReformMaxChange`: 0.30 +- `SimConfig.polity.treasuryCapPerPopulation`: 0.30 +- `SimConfig.polity.treasuryCityFoundingChance`: 0.14 +- `SimConfig.polity.treasuryCityFoundingCostRatio`: 0.35 +- `SimConfig.polity.treasuryCityFoundingSearchRadius`: 20 + +## 検査結果 + +### 構文検査 + +`config.js`, `app-state.js`, `engine.js`, `render.js`, `boot.js` を `node --check` で確認し、構文エラーなし。 + +### 自動テスト + +実行コマンド: + +```bash +node --test tests/simulation.test.cjs tests/modifications.test.cjs +``` + +結果: **48 / 48 PASS**。 + +追加テストでは以下を個別確認した。 + +- 1週進行・初期人口スライダーがUIから削除されていること。 +- 再生成ダイアログと3種の介入ボタンが存在すること。 +- 水域肥沃度が全て0であること。 +- 忠誠度30%未満による自動反乱と50%未満都市の同時参加。 +- 手動都市建設。 +- 手動交易路の保護期間と保護終了。 +- 戦争人口損害が約65%へ軽減されていること。 +- 手動災害が履歴へ記録されること。 +- 砂漠横断交易路の距離上限・通行コスト・建設コストが緩和されること。 +- ユーザー建設都市に必ず民族構成が設定されること。 +- 征服都市が20年間反乱から保護され、期間終了後に通常判定へ戻ること。 +- 他都市で反乱が発生しても、征服保護中の都市が参加しないこと。 +- 制度老朽化が年次で増加し、同じ割合だけ忠誠度上限が低下すること。 +- 制度改革が老朽化度を -50%~+30% の範囲で相対変化させ、国家履歴イベントへ記録すること。 +- 国家ツールチップに制度老朽化・忠誠度上限を含む国家データが存在すること。 +- 国家興亡グラフに制度改革イベント種別と説明が存在すること。 +- 制度改革が30%ちょうどでは発生せず、30%超でのみ候補になること。 +- 都市の民族構成割合が「都市データ」内かつ「国家データ」より前に表示されること。 +- 旧ブラウザ併合試験をNode回帰試験へ統合し、平和併合/武力併合/他国所属都市拒否を確認すること。 +- ヘルプに主要パラメータの簡単な説明が存在すること。 +- 国庫上限が国家人口に比例し、人口減少時も上限へ収まること。 +- 反乱人口比に応じて国庫の一部が反乱国へ移転すること。 +- 国家ツールチップの国庫表示が1回だけであること。 +- 国家興亡グラフの出来事アイコンを表示 / 非表示に切り替えられること。 +- 自然生成都市の最小間隔が縮小されていること。 +- ユーザー建設都市が都市間隔・自動都市数上限を無視でき、水域では拒否されること。 +- 疎フェロモン更新が減衰・伝播を維持し、不要な全世界走査を行わないこと。 +- 自然都市の創設周期が5年となり、世界面積に応じて1回あたりの創設可能数が増えること。 +- 新規都市が人口400で成立し、199では消滅、200では存続すること。 +- 都市アイコンが指定された4つの人口帯で切り替わること。 +- 民族分化の時間スケールが1/4になり、総人口30未満の民族が隣接民族へ人口を失わず統合されること。 +- 再生成ダイアログに旧「初期人口は標準値で固定」案内が残っていないこと。 +- 都市アイコンが1000/5000/10000の新しい境界で切り替わること。 +- 制度改革が前回改革から50年間再発せず、50年経過後に再び候補になること。 +- 国庫が上限未満では都市を建設せず、上限到達時は確率判定後に上限の35%を都市建設へ投資すること。 +- 国家都市建設の投資額が大きいほど、初期人口と初期備蓄が単調に増えること。 +- 国家都市建設の候補地が広い適地群から加重抽選され、特定地点へ固定されないこと。 +- 都市老朽化のパラメータ・収容力ペナルティ・経年摩耗処理が残っていないこと。 +- 人口が過去ピークから落ちた都市ほど、食料余力がある場合に回復成長率が上がること。 + +### 長期スモークテスト + +280×280 / 初期人口50,000で180年進行し、例外なし。終了時は都市16、最小都市人口442、民族9、総人口30未満の存続民族0を確認した。都市創設は160年以降に始まるため、この試験は主に新しい消滅閾値と民族吸収処理の長期整合性確認を目的とする。 + +### 簡易性能計測 + +同一実行環境・固定乱数シードでの参考値。ブラウザ・CPUにより変動する。 + +| 計測 | v6 | v7 | 変化 | +| --- | ---: | ---: | ---: | +| 160×160 / 初期人口50,000 / 240週(通常状態) | 約 1.93 秒 | 約 0.19 秒 | 約 90%短縮 | +| 160×160 / 240週(交易フェロモン120地点を事前投入) | 約 1.91 秒 | 約 0.51 秒 | 約 73%短縮 | +| 160×160 初期化 | 約 0.66 秒 | 約 0.65 秒 | ほぼ同等 | + +今回の追加軽量化では、`updateWorldFields()` を毎週の全タイル走査から月次・疎更新へ変更した。交易フェロモンは値が存在するタイルと隣接域のみ更新し、資源再生は年間総量を維持したまま四半期単位へ集約した。人口収容力は毎回の全再計算を避け、必要な月次変化時と年次処理に限定した。民族統計も履歴記録・年次処理に必要な時点だけ更新する。 + +現状の残存コストは主に `updateCities()` と `updatePopulationCapacity()`、四半期の `regenerateResources()`。これ以上の更新頻度低下は都市成立・人口圧力・資源変動の時間解像度を変えるため、今回は行っていない。 + +### v11 追加スモーク / 性能確認 + +- 160×160 / 初期人口50,000で220年進行し、例外なし。終了時の都市人口に非有限値・負値なし。 +- 160×160 / 240週の参考計測は初期化約0.68秒、進行約0.19秒で、v7以降の軽量化水準を維持。 +- 国庫都市建設の候補探索は国庫上限到達・確率判定通過時のみ行い、通常年次では追加の全マップ走査を行わない。 + + +## v12 追加修正(都市建設分散・投資規模・長期人口) + +- 国庫都市建設の投資額を初期状態へ反映。 + - 初期人口は標準400人を下限に、投資額に比例して増加する。 + - 初期備蓄も投資額と増加した初期人口に比例して増加する。 + - 現在の係数は `treasuryCityPopulationPerCost = 0.18`、`treasuryCityReservePerCost = 0.45`。 +- 都市建設候補の偏りを緩和。 + - 自然都市は最上位付近の適地をランダム化して順位付けする。 + - 国家による国庫都市建設は、最高点から6.0点以内の適地を固定件数で切らず、候補全体から適性加重抽選する。 + - 適地評価そのもの(肥沃度、資源、人口圧力、農地、統制、交易、移動コスト)は維持する。 +- **都市老朽化を削除**。 + - `ageWear`、経年摩耗更新、老朽化による収容力低下、関連設定値を削除した。 + - 国家の「制度老朽化」は別概念なので維持する。 +- 長期都市人口の一方向減少を抑制。 + - 災害・戦争などで過去ピーク人口から落ちた都市に回復成長を付与する。 + - 回復中のみ必要備蓄・供給ストレス条件を緩めるが、人口は都市収容力を超えて増えない。 + - `postShockRecoveryGrowth = 0.045`、過剰収容時の減衰率は `overCapacityAttrition = 0.020`。 + +### v12 長期確認 + +固定乱数・32×32・初期人口8,000で500年進行した参考値: + +- 200年: 7都市 / 都市人口3,306 +- 300年: 23都市 / 都市人口14,478 +- 400年: 23都市 / 都市人口13,646 +- 500年: 28都市 / 都市人口18,545 + +短期的な減少は災害・戦争により残るが、都市老朽化による恒久的な下押しはなく、被害後に再成長できることを確認した。 diff --git a/README.md b/README.md index e69de29..148f52d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,11 @@ +# 文明シミュレータ + +`index.html` をブラウザで開いて実行する文明シミュレータです。 + +今回の改修内容、設定値、検査結果は `MODIFICATION_NOTES.md` を参照してください。 + +テスト: + +```bash +node --test tests/simulation.test.cjs tests/modifications.test.cjs +``` diff --git a/annexation_policy_probe.html b/annexation_policy_probe.html new file mode 100644 index 0000000..14a7c59 --- /dev/null +++ b/annexation_policy_probe.html @@ -0,0 +1,70 @@ + + +annexation policy probe +
RUNNING
+ + + + diff --git a/app-state.js b/app-state.js index 81f9eb7..5000340 100644 --- a/app-state.js +++ b/app-state.js @@ -1,10 +1,12 @@ const elementIds = [ -"world", "toggleRun", "stepOnce", "resetWorld", -"speed", "agentCount", "worldSize", "viewMode", "year", "activeGroups", "urbanPopulation", +"world", "toggleRun", "resetWorld", +"speed", "worldSize", "viewMode", "year", "activeGroups", "urbanPopulation", "ethnicities", "cities", "polities", "wars", "routes", "farmingKnowledge", "metallurgyKnowledge", -"deaths", "frameCost", "ethnicityList", "legend", "tooltip", "stateGraph", "historyRange", -"historyFilter", "historySort", "historyMetric", "toggleActiveStates", "togglePastStates", -"stateGraphInfo", "stateGraphTooltip" +"deaths", "frameCost", "ethnicityList", "fpsCounter", "legend", "tooltip", "worldTrendGraph", "stateGraph", "historyRange", +"currentDate", "historyFilter", "historySort", "historyMetric", "toggleActiveStates", "togglePastStates", "toggleEventIcons", +"stateGraphInfo", "stateGraphTooltip", "eventLegend", +"regenerateDialog", "confirmRegenerate", "cancelRegenerate", +"triggerDisaster", "buildCity", "connectCities", "interventionStatus" ]; const els = Object.fromEntries(elementIds.map(id => [id === "world" ? "canvas" : id, document.getElementById(id)])); els.sim = document.querySelector(".sim"); @@ -18,13 +20,15 @@ let lastSimTickAt = 0; let renderDirty = true; let loopTimer = null; let loopScheduled = false; +let fpsWindowStartedAt = 0; +let fpsFrames = 0; let hoverState = null; let renderImage = null; let renderImageSize = 0; const LoopConfig = Object.freeze({ simTickMs: 33, renderMs: 66, -stepBudgetMs: 4.5, +stepBudgetMs: 8, idlePollMs: 180, hiddenPollMs: 1000 }); @@ -34,6 +38,7 @@ sort: "oldest", metric: "power", showActive: true, showPast: true, +showEventIcons: true, hoverPolityId: null, rows: [], markers: [], @@ -41,11 +46,11 @@ scale: null }; const legendByMode = { terrain: () => terrainInfo.map(t => `${t.name}`).join(""), -ethnicity: () => "Ethnicity", -pressure: () => "Pressure", -polities: () => "States", -nomads: () => "Nomads", -technology: () => "Technology", -pheromone: () => "Routes", -resources: () => "Resources" +ethnicity: () => "民族の色・濃さは人口と優勢度", +pressure: () => "赤いほど人口圧力が高い", +polities: () => "", +nomads: () => "遊牧集団", +technology: () => "緑:農業 / 黄土色:冶金", +pheromone: () => "明るい黄色ほど交通量が多い", +resources: () => "明るい緑ほど資源が豊富" }; diff --git a/boot.js b/boot.js index a4f3861..9408b8a 100644 --- a/boot.js +++ b/boot.js @@ -7,6 +7,14 @@ updateStats(forceStats); renderTooltip(); renderDirty = false; lastRenderAt = performance.now(); +if (!fpsWindowStartedAt) fpsWindowStartedAt = lastRenderAt; +fpsFrames++; +const fpsElapsed = lastRenderAt - fpsWindowStartedAt; +if (fpsElapsed >= 500) { +if (els.fpsCounter) els.fpsCounter.textContent = `FPS ${Math.round(fpsFrames * 1000 / fpsElapsed)}`; +fpsWindowStartedAt = lastRenderAt; +fpsFrames = 0; +} } function scheduleLoop() { if (loopScheduled) return; @@ -28,7 +36,7 @@ loopScheduled = false; if (running && !document.hidden && now - lastSimTickAt >= LoopConfig.simTickMs) { lastSimTickAt = now; const steps = Number(els.speed.value); -const stepBudget = LoopConfig.stepBudgetMs * Math.max(1, steps); +const stepBudget = LoopConfig.stepBudgetMs; const loopStart = performance.now(); let completedSteps = 0; for (let i = 0; i < steps; i++) { @@ -38,30 +46,126 @@ if (completedSteps > 0 && performance.now() - loopStart > stepBudget) break; } if (completedSteps > 0) requestRender(); } -const shouldRender = !document.hidden && -(renderDirty || (running && now - lastRenderAt >= LoopConfig.renderMs)); +const shouldRender = !document.hidden && renderDirty && +(!running || now - lastRenderAt >= LoopConfig.renderMs); if (shouldRender) drawFrame(); scheduleLoop(); } -function reset() { -const size = Number(els.worldSize.value); -const initialPopulation = Number(els.agentCount.value); + +let interventionMode = null; +let manualRouteStartCityId = null; + +function setInterventionStatus(text) { +if (els.interventionStatus) els.interventionStatus.textContent = text; +} +function setInterventionMode(mode) { +interventionMode = interventionMode === mode ? null : mode; +manualRouteStartCityId = null; +const buttons = [ +[els.triggerDisaster, "disaster"], +[els.buildCity, "city"], +[els.connectCities, "route"] +]; +for (const [button, buttonMode] of buttons) button?.setAttribute("aria-pressed", String(interventionMode === buttonMode)); +if (interventionMode === "disaster") setInterventionStatus("災害を発生させる地点を地図上でクリックしてください。"); +else if (interventionMode === "city") setInterventionStatus("都市を建設する地点を地図上でクリックしてください。"); +else if (interventionMode === "route") setInterventionStatus("接続する1つ目の都市をクリックしてください。"); +else setInterventionStatus("ボタンを選び、地図上をクリックして実行します。"); +} +function eventToWorldCoordinates(event) { +const rect = els.canvas.getBoundingClientRect(); +return { +x: Math.floor((event.clientX - rect.left) / rect.width * sim.world.size), +y: Math.floor((event.clientY - rect.top) / rect.height * sim.world.size) +}; +} +function handleMapIntervention(event) { +if (!interventionMode || !sim) return; +const { x, y } = eventToWorldCoordinates(event); +if (x < 0 || y < 0 || x >= sim.world.size || y >= sim.world.size) return; +if (interventionMode === "disaster") { +const result = sim.triggerManualDisasterAt(x, y); +setInterventionStatus(`災害を発生させました(中心 ${result.x}, ${result.y}/半径 ${Math.round(result.radius)})。続けて地点をクリックできます。`); +requestRender(); +drawFrame(true); +renderStateGraph(); +return; +} +if (interventionMode === "city") { +const result = sim.buildCityAt(x, y); +if (!result.ok) { +setInterventionStatus(result.reason === "water" +? "水域には都市を建設できません。陸地をクリックしてください。" +: "この地点には都市を建設できません。"); +return; +} +setInterventionStatus(`都市${result.city.id}を建設しました(${result.city.x}, ${result.city.y})。続けて建設地点をクリックできます。`); +requestRender(); +drawFrame(true); +renderStateGraph(); +return; +} +if (interventionMode === "route") { +const city = cityAtDisplayTile(x, y); +if (!city) { +setInterventionStatus("都市をクリックしてください。都市アイコンの範囲内で選択できます。"); +return; +} +if (manualRouteStartCityId == null) { +manualRouteStartCityId = city.id; +setInterventionStatus(`都市${city.id}を選択しました。接続する2つ目の都市をクリックしてください。`); +return; +} +if (manualRouteStartCityId === city.id) { +setInterventionStatus("同じ都市は接続できません。別の都市をクリックしてください。"); +return; +} +const startId = manualRouteStartCityId; +const result = sim.createManualTradeRoute(startId, city.id); +manualRouteStartCityId = null; +if (!result.ok) { +const message = result.reason === "tooFar" ? `都市間が遠すぎます(上限 ${result.maxLength} タイル)。` +: result.reason === "noPath" ? "陸路で接続できる経路が見つかりませんでした。" +: "交通接続を作成できませんでした。"; +setInterventionStatus(`${message} 1つ目の都市から選び直してください。`); +return; +} +setInterventionStatus(`都市${startId}と都市${city.id}を接続しました。続けて1つ目の都市をクリックできます。`); +requestRender(); +drawFrame(true); +} +} + +function reset(size = SimConfig.world?.defaultSize ?? 160) { +const initialPopulation = SimConfig.world?.initialPopulation ?? 50000; +fpsWindowStartedAt = 0; +fpsFrames = 0; +if (els.fpsCounter) els.fpsCounter.textContent = "FPS --"; els.canvas.width = size; els.canvas.height = size; renderImage = null; renderImageSize = 0; sim = new Simulation(size, initialPopulation); +if (els.worldSize) els.worldSize.value = String(size); +interventionMode = null; +manualRouteStartCityId = null; +for (const button of [els.triggerDisaster, els.buildCity, els.connectCities]) button?.setAttribute("aria-pressed", "false"); +setInterventionStatus("ボタンを選び、地図上をクリックして実行します。"); setLegend(); drawFrame(true); renderStateGraph(); } +function openRegenerateDialog() { +if (els.worldSize && sim) els.worldSize.value = String(sim.world.size); +if (els.regenerateDialog?.showModal) els.regenerateDialog.showModal(); +} function refreshStateGraph() { renderStateGraph(); clearStateGraphInfo(); } els.toggleRun.addEventListener("click", () => { running = !running; -els.toggleRun.textContent = running ? "Pause" : "Run"; +els.toggleRun.textContent = running ? "一時停止" : "再開"; lastSimTickAt = 0; if (running && loopTimer) { clearTimeout(loopTimer); @@ -72,14 +176,16 @@ scheduleLoop(); scheduleLoop(); } }); -els.stepOnce.addEventListener("click", () => { -sim.step(); -requestRender(); -drawFrame(true); -renderStateGraph(); +els.resetWorld.addEventListener("click", openRegenerateDialog); +els.confirmRegenerate?.addEventListener("click", () => { +const size = Number(els.worldSize?.value || SimConfig.world?.defaultSize || 160); +els.regenerateDialog?.close(); +reset(size); }); -els.resetWorld.addEventListener("click", reset); -for (const control of [els.worldSize, els.agentCount]) control.addEventListener("change", reset); +els.cancelRegenerate?.addEventListener("click", () => els.regenerateDialog?.close()); +els.triggerDisaster?.addEventListener("click", () => setInterventionMode("disaster")); +els.buildCity?.addEventListener("click", () => setInterventionMode("city")); +els.connectCities?.addEventListener("click", () => setInterventionMode("route")); els.viewMode.addEventListener("change", () => { setLegend(); requestRender(); @@ -87,6 +193,7 @@ drawFrame(); }); els.canvas.addEventListener("mousemove", showTooltip); els.canvas.addEventListener("mouseleave", hideTooltip); +els.canvas.addEventListener("click", handleMapIntervention); for (const control of [els.historyFilter, els.historySort, els.historyMetric]) { control?.addEventListener("change", refreshStateGraph); } @@ -100,6 +207,11 @@ graphState.showPast = !graphState.showPast; els.togglePastStates.setAttribute("aria-pressed", String(graphState.showPast)); refreshStateGraph(); }); +els.toggleEventIcons?.addEventListener("click", () => { +graphState.showEventIcons = !graphState.showEventIcons; +els.toggleEventIcons.setAttribute("aria-pressed", String(graphState.showEventIcons)); +refreshStateGraph(); +}); els.stateGraph?.addEventListener("mousemove", updateStateGraphInfo); els.stateGraph?.addEventListener("mouseleave", clearStateGraphInfo); document.addEventListener("visibilitychange", () => { @@ -112,5 +224,5 @@ loopScheduled = false; scheduleLoop(); } }); -reset(); +reset(SimConfig.world?.defaultSize ?? 160); loop(); diff --git a/config.js b/config.js index 7a2b9c5..a120f52 100644 --- a/config.js +++ b/config.js @@ -6,34 +6,41 @@ WATER: 3, DESERT: 4 }); const terrainInfo = [ -{ name: "Plains", color: [82, 102, 74], move: 1.0, fertility: 0.55, mineral: 0.05, regen: 0.025 }, -{ name: "Forest", color: [58, 91, 68], move: 1.3, fertility: 0.62, mineral: 0.08, regen: 0.038 }, -{ name: "Mountains", color: [104, 105, 101], move: 2.6, fertility: 0.18, mineral: 0.78, regen: 0.012 }, -{ name: "Water", color: [52, 78, 103], move: 7.0, fertility: 0.04, mineral: 0.02, regen: 0.004 }, -{ name: "Desert", color: [139, 123, 86], move: 1.55, fertility: 0.16, mineral: 0.16, regen: 0.014 } +{ name: "平原", color: [82, 102, 74], move: 1.0, fertility: 0.55, mineral: 0.05, regen: 0.025 }, +{ name: "森林", color: [58, 91, 68], move: 1.3, fertility: 0.62, mineral: 0.08, regen: 0.038 }, +{ name: "山地", color: [104, 105, 101], move: 2.6, fertility: 0.18, mineral: 0.78, regen: 0.012 }, +{ name: "水域", color: [52, 78, 103], move: 7.0, fertility: 0, mineral: 0.02, regen: 0.004 }, +{ name: "砂漠", color: [139, 123, 86], move: 1.55, fertility: 0.16, mineral: 0.16, regen: 0.014 } ]; const WEEKS_PER_MONTH = 4; const MONTHS_PER_YEAR = 12; const WEEKS_PER_YEAR = MONTHS_PER_YEAR * WEEKS_PER_MONTH; const SimConfig = Object.freeze({ +world: Object.freeze({ +initialPopulation: 50000, +defaultSize: 160 +}), population: Object.freeze({ -softCapStart: 0.85, -hardCapScale: 1.35, +populationScale: 10, carryingCapacityBase: 2.4, carryingCapacityFertility: 7.8, carryingCapacityMineral: 1.8, carryingCapacityFarmland: 5.5, -pressurePenaltyBase: 0.2, -pressureReproductionPenalty: 0.16 }), city: Object.freeze({ -maxCities: 300, -agingStartYears: 180, -agingDecayPerYear: 0.002, -agingOverCapAttrition: 0.035, -agingRestoreMinWear: 0.08, -agingRestoreCostPerWear: 70, -agingRestoreBudgetShare: 0.18 +maxCities: 1200, +initialPopulation: 400, +abandonmentPopulation: 200, +populationScale: 10, +foodProductionScale: 10, +postShockRecoveryGrowth: 0.045, +overCapacityAttrition: 0.020, +naturalFoundingMinDistance: 6, +naturalFoundingStartYears: 160, +naturalFoundingIntervalYears: 5, +naturalFoundingReferenceArea: 160 * 160, +naturalFoundingMaxPerCycle: 8, +foundingCompetitionRadius: 9, }), render: Object.freeze({ graphThrottleMs: 1400, @@ -42,7 +49,6 @@ historyWindowYears: 2000, historySamplePaddingYears: 240 }), technology: Object.freeze({ -cityDiffusion: 0.0022, tradeDiffusion: 0.0032, cityInnovation: 0.00045 }), @@ -53,7 +59,47 @@ charismaMin: 0.5, charismaMax: 1.5, charismaAverage: 1.0, leaderTenureMinYears: 22, -leaderTenureMaxYears: 72 +leaderTenureMaxYears: 72, +foundingProtectionYears: 12, +warCapitalDistanceStart: 18, +warCapitalDistanceScale: 42, +warDisconnectedTerritoryFactor: 0.42, +institutionalAgingAnnualIncrease: 0.0015, +institutionalReformThreshold: 0.30, +institutionalReformBaseChance: 0.008, +institutionalReformChanceScale: 0.10, +institutionalReformMaxChance: 0.12, +institutionalReformCooldownYears: 50, +institutionalReformMinChange: -0.50, +institutionalReformMaxChange: 0.30, +treasuryCapPerPopulation: 0.30, +treasuryCityFoundingChance: 0.14, +treasuryCityFoundingCostRatio: 0.35, +treasuryCityPopulationPerCost: 0.18, +treasuryCityReservePerCost: 0.45, +treasuryCityFoundingSearchRadius: 20, +treasuryCitySiteScoreWindow: 6.0 +}), +territory: Object.freeze({ +baseCityReach: 9, +radiusScale: 0.5, +cityPowerWeight: 1.8, +capitalReachBonus: 1.5, +ownershipSwitchMargin: 1.5, +minimumInfluence: 2.4, +fullMaintenanceCityPopulation: 2400, +minimumPopulationSupport: 0.28, +maintenancePressure: 4, +supportSmoothing: 0.18, +smallStateSwitchPenalty: 2.5 +}), +rebellion: Object.freeze({ +participationLoyalty: 0.50, +automaticLoyalty: 0.30, +chanceScale: 0.20, +maxAnnualChance: 0.16, +conquestGraceYears: 20, +treasuryTransferRate: 0.65 }), campaign: Object.freeze({ checkIntervalYears: 3, @@ -64,7 +110,8 @@ maxActiveCampaigns: 12, frontierTargetChance: 0.35, nonStateTargetChance: 0.30, independentCityTargetChance: 0.20, -polityTargetChance: 0.15, +polityTargetChance: 0, +independentCityPeacefulShare: 0.4, baseDurationYears: 8, maxDurationYears: 25, claimRadius: 8, @@ -114,10 +161,14 @@ lowLoyaltyTransitPenalty: 0.012, foreignTransitPenalty: 0.008 }), culture: Object.freeze({ -spreadRadius: 3, -cityWeight: 0.035, -routeWeight: 1.35, -minimumInfluence: 0.18 +minimumInfluence: 0.18, +divergenceRateMultiplier: 0.25, +absorptionPopulationThreshold: 30, +urbanSpreadRadius: 5, +cityBlendRate: 0.045, +routeBlendRate: 0.035, +routeNeighborFactor: 0.35, +cityTradeBlendRate: 0.015 }), route: Object.freeze({ maxRouteLength: 42, @@ -136,17 +187,34 @@ routePheromoneMaintain: 0.12, routeUpkeepPerTile: 0.018, routeWeakThreshold: 8, routeUnsupportedDecay: 3, -routeMaintainedBoost: 10 +routeMaintainedBoost: 10, +manualProtectionYears: 25, +manualProtectedStrength: 32, +manualRouteMaxLength: 120, +desertTraversalCostFactor: 0.68, +desertRouteLengthBonus: 30, +desertAdaptiveMaxRouteLength: 96, +desertPheromoneThresholdFactor: 0.45, +desertConstructionCostFactor: 0.72 }), war: Object.freeze({ +damageMultiplier: 0.65, +tickWeeks: 4, +startChanceMultiplier: 3, +maxActiveWars: 12, +maxStartsPerTick: 4, +minDurationWeeks: 12, +idleEndWeeks: 24, +cooldownWeeks: 24, +decisiveScore: 6, pressureMemoryDecay: 0.88, -cityDamageMinRate: 0.003, -cityDamageMaxRate: 0.08, -captureLossBase: 0.08, -captureLossMax: 0.32, -exhaustionPopulationLossBase: 0.0007, -exhaustionPopulationLossMax: 0.012, -warZoneTileLossRate: 0.006, +cityDamageMinRate: 0.025, +cityDamageMaxRate: 0.30, +captureLossBase: 0.22, +captureLossMax: 0.40, +exhaustionPopulationLossBase: 0.003, +exhaustionPopulationLossMax: 0.03, +warZoneTileLossRate: 0.025, majorSettlementThreshold: 1.25 }), disaster: Object.freeze({ diff --git a/engine.js b/engine.js index 9fa1d51..38170a8 100644 --- a/engine.js +++ b/engine.js @@ -31,7 +31,6 @@ this.tradeRoute = new Uint8Array(this.count); this.farmland = new Float32Array(this.count); this.cityPull = new Float32Array(this.count); this.city = new Int32Array(this.count); -this.pressure = new Float32Array(this.count); this.population = new Float32Array(this.count); this.settledPopulation = new Float32Array(this.count); this.mobilePopulation = new Float32Array(this.count); @@ -42,7 +41,7 @@ this.metallurgyKnowledge = new Float32Array(this.count); this.nomadInfluence = new Float32Array(this.count); this.nomadBand = new Int32Array(this.count); this.territoryOwner = new Int32Array(this.count); -this.polity = new Int32Array(this.count); +this.polity = this.territoryOwner; this.control = new Float32Array(this.count); this.claim = new Float32Array(this.count); this.contested = new Uint8Array(this.count); @@ -54,6 +53,31 @@ this.territoryOwner.fill(-1); this.polity.fill(-1); this.dominantEthnicity.fill(-1); this.generate(); +this.buildLandNeighbors(); +} +buildLandNeighbors() { +// Terrain is fixed after generation. Keep the original dy/dx traversal order. +const starts = new Int32Array(this.count + 1); +const tiles = []; +for (let i = 0; i < this.count; i++) { +starts[i] = tiles.length; +if (this.terrain[i] === Terrain.WATER) continue; +const x = i % this.size; +const y = Math.floor(i / this.size); +for (let dy = -1; dy <= 1; dy++) { +for (let dx = -1; dx <= 1; dx++) { +if (!dx && !dy) continue; +const tx = x + dx; +const ty = y + dy; +if (tx < 0 || ty < 0 || tx >= this.size || ty >= this.size) continue; +const tile = this.idx(tx, ty); +if (this.terrain[tile] !== Terrain.WATER) tiles.push(tile); +} +} +} +starts[this.count] = tiles.length; +this.landNeighborStarts = starts; +this.landNeighbors = Int32Array.from(tiles); } idx(x, y) { return y * this.size + x; @@ -114,7 +138,11 @@ else t = Terrain.PLAINS; } const info = terrainInfo[t]; this.terrain[i] = t; -this.fertility[i] = clamp(info.fertility + humid * 0.16 - Math.abs(temp - 0.58) * 0.08 + this.rng.range(-0.03, 0.03), 0, 1); +// Water does not participate in fertility simulation. Keep it explicitly at zero +// so later systems never spend work interpreting a meaningless fertility value. +this.fertility[i] = t === Terrain.WATER +? 0 +: clamp(info.fertility + humid * 0.16 - Math.abs(temp - 0.58) * 0.08 + this.rng.range(-0.03, 0.03), 0, 1); this.mineral[i] = clamp(info.mineral + minerals * 0.16, 0, 1); this.regen[i] = info.regen * (0.7 + this.fertility[i]); this.move[i] = info.move; @@ -127,20 +155,24 @@ this.enrichWaterMargins(); enrichWaterMargins() { const s = this.size; const nextFertility = new Float32Array(this.fertility); -for (let y = 0; y < s; y++) { -for (let x = 0; x < s; x++) { -const i = this.idx(x, y); -if (this.terrain[i] === Terrain.WATER) continue; -let waterScore = 0; +const offsets = []; for (let dy = -3; dy <= 3; dy++) { for (let dx = -3; dx <= 3; dx++) { const d = Math.abs(dx) + Math.abs(dy); if (!d || d > 3) continue; +offsets.push([dx, dy, (4 - d) / 4]); +} +} +for (let y = 0; y < s; y++) { +for (let x = 0; x < s; x++) { +const i = y * s + x; +if (this.terrain[i] === Terrain.WATER) continue; +let waterScore = 0; +for (const [dx, dy, weight] of offsets) { const tx = x + dx; const ty = y + dy; if (tx < 0 || ty < 0 || tx >= s || ty >= s) continue; -if (this.terrain[this.idx(tx, ty)] === Terrain.WATER) waterScore += (4 - d) / 4; -} +if (this.terrain[ty * s + tx] === Terrain.WATER) waterScore += weight; } if (waterScore <= 0) continue; const boost = Math.min(0.32, waterScore * 0.08); @@ -150,14 +182,16 @@ if (this.terrain[i] === Terrain.DESERT && waterScore > 1.2) this.terrain[i] = Te } for (let i = 0; i < this.count; i++) { const info = terrainInfo[this.terrain[i]]; -this.fertility[i] = nextFertility[i]; -this.regen[i] = info.regen * (0.7 + this.fertility[i]); -this.move[i] = info.move; if (this.terrain[i] === Terrain.WATER) { +this.fertility[i] = 0; +this.regen[i] = info.regen * 0.7; this.resource[i] = 0; } else { +this.fertility[i] = nextFertility[i]; +this.regen[i] = info.regen * (0.7 + this.fertility[i]); this.resource[i] = Math.max(this.resource[i], this.rng.range(5, 20) * (0.45 + this.fertility[i] + this.mineral[i] * 0.25)); } +this.move[i] = info.move; } } smoothNoise(x, y, scale) { @@ -178,26 +212,43 @@ return v - Math.floor(v); computeHumidityFromWater(moistureMap) { const s = this.size; const radius = 10; +const unreachable = radius + 1; +const distance = new Uint8Array(this.count); +distance.fill(unreachable); +for (let i = 0; i < this.count; i++) { +if (this.terrain[i] === Terrain.WATER) distance[i] = 0; +} +// Exact city-block distance transform. The former implementation scanned the +// full radius around every tile; two linear passes produce the same nearest- +// water distance with O(tile count) work. for (let y = 0; y < s; y++) { for (let x = 0; x < s; x++) { -const i = this.idx(x, y); +const i = y * s + x; +if (distance[i] === 0) continue; +let d = distance[i]; +if (x > 0) d = Math.min(d, distance[i - 1] + 1); +if (y > 0) d = Math.min(d, distance[i - s] + 1); +distance[i] = Math.min(unreachable, d); +} +} +for (let y = s - 1; y >= 0; y--) { +for (let x = s - 1; x >= 0; x--) { +const i = y * s + x; +if (distance[i] === 0) continue; +let d = distance[i]; +if (x + 1 < s) d = Math.min(d, distance[i + 1] + 1); +if (y + 1 < s) d = Math.min(d, distance[i + s] + 1); +distance[i] = Math.min(unreachable, d); +} +} +for (let i = 0; i < this.count; i++) { if (this.terrain[i] === Terrain.WATER) { this.humidity[i] = 1; continue; } -let best = 0; -for (let dy = -radius; dy <= radius; dy++) { -for (let dx = -radius; dx <= radius; dx++) { -const d = Math.abs(dx) + Math.abs(dy); -if (!d || d > radius) continue; -const tx = x + dx; -const ty = y + dy; -if (tx < 0 || ty < 0 || tx >= s || ty >= s) continue; -if (this.terrain[this.idx(tx, ty)] === Terrain.WATER) best = Math.max(best, 1 - d / (radius + 1)); -} -} -this.humidity[i] = clamp(best * 0.78 + moistureMap[i] * 0.22, 0, 1); -} +const d = distance[i]; +const waterInfluence = d <= radius ? 1 - d / (radius + 1) : 0; +this.humidity[i] = clamp(waterInfluence * 0.78 + moistureMap[i] * 0.22, 0, 1); } this.smoothField(this.humidity, 1); } @@ -302,7 +353,7 @@ return values[Math.floor(clamp(ratio, 0, 1) * (values.length - 1))]; } } class Simulation { -constructor(size, initialPopulation) { +constructor(size, initialPopulation = SimConfig.world?.initialPopulation ?? 50000) { this.rng = new Rng(Date.now()); this.world = new World(size, this.rng); this.ethnicities = new Map(); @@ -316,10 +367,16 @@ this.nomadBands = []; this.disasters = []; this.disasterHistory = []; this.graphEvents = []; +this.worldHistory = []; this.polityHistory = new Map(); this.deadPolityHistories = []; this.tradeLinks = []; this.activeTradeRouteTiles = new Set(); +this.activePheromoneTiles = new Set(); +this.nextPheromoneTiles = new Set(); +this.pheromoneTouchedScratch = []; +this.manualTradeRoutes = []; +this.manualRouteProtection = new Map(); this.dominantEthnicityCache = new Map(); this.nextEthnicity = 1; this.nextCity = 1; @@ -347,15 +404,16 @@ this.radiusOffsetCache = new Map(); this.scratchFloatA = new Float32Array(this.world.count); this.scratchFloatB = new Float32Array(this.world.count); this.scratchFloatC = new Float32Array(this.world.count); +this.populationCapacityStatic = new Float32Array(this.world.count); +this.populationCapacityDynamicScale = new Float32Array(this.world.count); +this.initializePopulationCapacityCache(); +this.territoryBestSupport = new Float32Array(this.world.count); +this.territoryIncumbentSupport = new Float32Array(this.world.count); this.scratchIntA = new Int32Array(this.world.count); this.scratchIntB = new Int32Array(this.world.count); this.scratchUintA = new Uint8Array(this.world.count); this.visitStamp = new Int32Array(this.world.count); this.nextVisitStamp = 1; -this.cultureInfluence = new Float32Array(64); -this.cultureSeen = new Int32Array(64); -this.cultureSeenIds = []; -this.cultureStamp = 1; this.cityScoreScratch = []; this.pairCandidateScratch = []; this.routeCandidateScratch = []; @@ -369,6 +427,7 @@ this.routeHeapScratch = []; this.seedInitialPopulationFieldDirectly(initialPopulation); this.recomputeTerritories({ allowOwnershipChanges: true, reason: "initial" }); this.updateEthnicStats(); +this.sampleWorldHistory(); } markPopulationTile(tile) { if (tile >= 0 && tile < this.world.count) this.activePopulationTiles.add(tile); @@ -424,23 +483,6 @@ this.nextVisitStamp = 1; } return this.nextVisitStamp++; } -ensureCultureScratch() { -const needed = Math.max(64, this.nextEthnicity + 8); -if (this.cultureInfluence.length >= needed) return; -let size = this.cultureInfluence.length; -while (size < needed) size *= 2; -this.cultureInfluence = new Float32Array(size); -this.cultureSeen = new Int32Array(size); -this.cultureStamp = 1; -} -nextCultureMarker() { -this.ensureCultureScratch(); -if (this.cultureStamp >= 2147483640) { -this.cultureSeen.fill(0); -this.cultureStamp = 1; -} -return this.cultureStamp++; -} topHeapPush(heap, item, limit, scoreKey) { if (limit <= 0) return; if (heap.length < limit) { @@ -491,7 +533,8 @@ climateTemp: climate?.temperature ?? 0.55, climateHumidity: climate?.humidity ?? 0.45, color: hslToRgb((id * 0.61803398875) % 1, 0.55, 0.56), centroidX: 0, -centroidY: 0 +centroidY: 0, +nextDivergenceCheck: this.year + years(this.rng.range(18, 72) / clamp(SimConfig.culture?.divergenceRateMultiplier ?? 0.25, 0.05, 1)) }); return id; } @@ -692,7 +735,7 @@ assimilation: this.rng.range(0.02, 0.18) const population = options.population ?? Math.round(this.rng.range(cfg.minPopulation ?? 80, cfg.maxPopulation ?? 420)); const band = { id: this.nextNomadBand++, -name: `Band ${this.nextNomadBand - 1}`, +name: `遊牧集団${this.nextNomadBand - 1}`, ethnicityId, x: originTile % w.size, y: Math.floor(originTile / w.size), @@ -1085,6 +1128,7 @@ this.nomadRaidCity(band, city, true); const oldPolityId = city.polityId; if (city.polityId !== null) this.removeCityFromPolity(city); this.releaseCityTerritory(city, oldPolityId, 5, "nomadConquest"); +this.protectCityFromRebellion(city); city.loyalty = 0.22; band.prestige = clamp(band.prestige + 0.28, 0, 5); this.addGraphEvent("nomadConquest", this.year, { bandId: band.id, cityId: city.id, outcome: "puppetIndependent" }, 3); @@ -1092,6 +1136,7 @@ this.addGraphEvent("nomadConquest", this.year, { bandId: band.id, cityId: city.i const oldPolityId = city.polityId; if (city.polityId !== null) this.removeCityFromPolity(city); this.releaseCityTerritory(city, oldPolityId, 5, "nomadConquest"); +this.protectCityFromRebellion(city); const settlers = Math.min(band.population * 0.28, Math.max(30, city.population * 0.35)); band.population -= settlers; city.population += settlers; @@ -1182,9 +1227,15 @@ step() { for (const city of this.cities) city.activeVisitors = 0; this.dominantEthnicityCache.clear(); this.rebuildIndexes(); -this.updateFieldPressureFromPopulation(); -if (this.year % 2 === 0) this.updateWorldFields(2); -this.updatePopulationCapacity(); +// World fields are visually and behaviorally slow-moving. Updating them once +// per simulated month halves full-map work while preserving the same annual +// decay/regeneration scale. Annual city processing owns its capacity refresh. +const annualTick = this.year % years(1) === 0; +const fieldTick = this.year % WEEKS_PER_MONTH === 0; +const resourceTick = this.year % 12 === 2; +const pheromoneChanged = fieldTick ? this.updateWorldFields(WEEKS_PER_MONTH, 0) : false; +if (resourceTick) this.regenerateResources(12); +if (!annualTick && pheromoneChanged) this.updatePopulationCapacity(); this.maybeSpawnDisaster(); if (this.year % years(1) === 0) this.maybeSpawnFrontierWave(); if (this.year % years(1) === 0) { @@ -1192,16 +1243,16 @@ this.updateCities(); this.updateNomadBands(); this.consumeTileResources(); this.growTilePopulation(); -this.updatePopulationCapacity(); +this.updatePopulationPressure(); this.diffusePopulation(); -this.updatePopulationCapacity(); +this.updatePopulationPressure(); if (this.year % years(2) === 0) { this.updateTileTechnology(); this.diffuseTileTechnology(); this.cityTechnologyExchange(); } if (this.year % years(3) === 0) this.updateTileAssimilation(); -this.updateRegionalCultures(); +this.spreadCulturesFromCitiesAndTrade(); this.updateNomadInfluence(); if (this.year % years(100) === 0 && typeof console !== "undefined" && console.debug) { const d = this.territoryDebug; @@ -1214,17 +1265,22 @@ if (this.year % years(5) === 0) { this.updateTradeRoutesFieldBased(); } this.updatePolities(); +if (this.year % SimConfig.war.tickWeeks === 0) { +this.maybeStartWars(); +this.updateCampaigns(); +this.updateWars(); +} +const historyTick = this.year % 12 === 0; +if (historyTick) this.updateEthnicStats(); +if (this.year > 0 && annualTick) { +this.absorbSmallEthnicities(); this.updateEthnicStats(); -if (this.year % years(45) === 0) { this.splitDivergentEthnicities(); this.updateEthnicStats(); } +if (historyTick) this.sampleWorldHistory(); this.year++; } -updateFieldPressureFromPopulation() { -const w = this.world; -for (let i = 0; i < w.count; i++) w.pressure[i] = w.population[i] || 0; -} rebuildIndexes() { this.cityById.clear(); for (const city of this.cities) this.cityById.set(city.id, city); @@ -1269,7 +1325,7 @@ w.mobilePopulation.fill(0); w.populationPressure.fill(0); this.tileEthnicMix.clear(); this.activePopulationTiles.clear(); -const founders = Math.max(6, Math.min(18, Math.round(initialPopulation / 650))); +const founders = Math.max(6, Math.min(18, Math.round(initialPopulation / (650 * SimConfig.population.populationScale)))); const desertFounders = Math.max(2, Math.floor(founders * 0.25)); for (let e = 0; e < founders; e++) { const desertFounder = e < desertFounders; @@ -1605,7 +1661,7 @@ for (const tile of tiles) { const population = w.population[tile]; if (population <= 0 || w.terrain[tile] === Terrain.WATER) continue; const pressure = w.populationPressure[tile] || 0; -const demand = population * (0.0045 + Math.max(0, pressure - 1) * 0.0035); +const demand = (population / SimConfig.population.populationScale) * (0.0045 + Math.max(0, pressure - 1) * 0.0035); w.resource[tile] = Math.max(0, w.resource[tile] - demand); } } @@ -1651,69 +1707,81 @@ this.updateCultureTile(tile); } } } -updateRegionalCultures() { +blendEthnicComposition(target, targetTotal, source, rate) { +if (!target || !source?.size || targetTotal <= 0 || rate <= 0) return false; +const sourceTotal = compositionTotal(source); +if (sourceTotal <= 0) return false; +const blendRate = clamp(rate, 0, 0.25); +const ids = new Set([...target.keys(), ...source.keys()]); +for (const id of ids) { +const current = target.get(id) || 0; +const desired = targetTotal * (source.get(id) || 0) / sourceTotal; +const next = current + (desired - current) * blendRate; +if (next > 0.01) target.set(id, next); +else target.delete(id); +} +return true; +} +blendTileEthnicComposition(tile, source, rate) { +const population = this.world.population[tile] || 0; +if (population <= 0.01 || this.world.terrain[tile] === Terrain.WATER || !source?.size) return false; +let target = this.tileEthnicMix.get(tile); +if (!target?.size) { +target = new Map(); +const sourceTotal = compositionTotal(source); +if (sourceTotal <= 0) return false; +for (const [id, count] of source) target.set(id, population * count / sourceTotal); +this.tileEthnicMix.set(tile, target); +this.updateCultureTile(tile); +return true; +} +const targetTotal = compositionTotal(target); +const changed = this.blendEthnicComposition(target, targetTotal, source, rate); +if (changed) this.updateCultureTile(tile); +return changed; +} +spreadCulturesFromCitiesAndTrade() { const w = this.world; -const nextDominant = this.scratchIntA; -const nextDiversity = this.scratchFloatA; -nextDominant.set(w.dominantEthnicity); -nextDiversity.set(w.cultureDiversity); -const radius = SimConfig.culture.spreadRadius; -const offsets = this.getRadiusOffsets(radius); -this.ensureCultureScratch(); -const influence = this.cultureInfluence; -const seen = this.cultureSeen; -for (let y = 0; y < w.size; y++) { -for (let x = 0; x < w.size; x++) { +const cfg = SimConfig.culture || {}; +const urbanRadius = cfg.urbanSpreadRadius ?? 5; +const cityRate = cfg.cityBlendRate ?? 0.045; +for (const city of this.cities) { +if (!city.ethnicityComposition?.size || city.population <= 0) continue; +const populationStrength = clamp(Math.sqrt(city.population / (SimConfig.city.populationScale || 1)) / 18, 0.35, 1.4); +for (const { dx, dy, distance } of this.getRadiusOffsets(urbanRadius)) { +const x = city.x + dx; +const y = city.y + dy; +if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; const tile = w.idx(x, y); -if (w.terrain[tile] === Terrain.WATER) continue; -const stamp = this.nextCultureMarker(); -const seenIds = this.cultureSeenIds; -seenIds.length = 0; -let total = 0; -for (const offset of offsets) { -const distance = offset.distance; -const dx = offset.dx; -const dy = offset.dy; -const tx = x + dx; -const ty = y + dy; -if (tx < 0 || ty < 0 || tx >= w.size || ty >= w.size) continue; -const source = w.idx(tx, ty); -const id = w.dominantEthnicity[source]; -if (id < 0) continue; -const pressureWeight = Math.sqrt(Math.max(0, w.population[source])); -const cityWeight = w.city[source] >= 0 -? Math.sqrt(Math.max(1, this.getCityById(w.city[source])?.population || 1)) * SimConfig.culture.cityWeight -: 0; -const routeWeight = w.tradeRoute[source] ? SimConfig.culture.routeWeight : 1; -const weight = (pressureWeight + cityWeight) * routeWeight / (1 + distance); -if (weight <= 0) continue; -if (seen[id] !== stamp) { -seen[id] = stamp; -influence[id] = weight; -seenIds.push(id); -} else { -influence[id] += weight; -} -total += weight; -} -let bestId = nextDominant[tile]; -let bestWeight = 0; -for (const id of seenIds) { -const weight = influence[id]; -if (weight > bestWeight) { -bestId = id; -bestWeight = weight; +if (w.terrain[tile] === Terrain.WATER || (w.population[tile] || 0) <= 0.01) continue; +const falloff = 1 - distance / (urbanRadius + 1); +const routeBoost = w.tradeRoute[tile] ? 1.35 : 1; +this.blendTileEthnicComposition(tile, city.ethnicityComposition, cityRate * populationStrength * falloff * routeBoost); } } -if (bestWeight >= SimConfig.culture.minimumInfluence) { -nextDominant[tile] = bestId; -nextDiversity[tile] = total > 0 ? clamp(1 - bestWeight / total, 0, 1) : 0; +const routeRate = cfg.routeBlendRate ?? 0.035; +const neighborFactor = cfg.routeNeighborFactor ?? 0.35; +const cityTradeRate = cfg.cityTradeBlendRate ?? 0.015; +for (const link of this.tradeLinks || []) { +const a = this.getCityById(link.from); +const b = this.getCityById(link.to); +if (!a?.ethnicityComposition?.size || !b?.ethnicityComposition?.size) continue; +const aSource = new Map(a.ethnicityComposition); +const bSource = new Map(b.ethnicityComposition); +this.blendEthnicComposition(a.ethnicityComposition, compositionTotal(a.ethnicityComposition), bSource, cityTradeRate * (0.5 + (link.strength || 0))); +this.blendEthnicComposition(b.ethnicityComposition, compositionTotal(b.ethnicityComposition), aSource, cityTradeRate * (0.5 + (link.strength || 0))); +const routeCulture = new Map(); +for (const [id, count] of aSource) routeCulture.set(id, (routeCulture.get(id) || 0) + count / Math.max(1, a.population)); +for (const [id, count] of bSource) routeCulture.set(id, (routeCulture.get(id) || 0) + count / Math.max(1, b.population)); +const localRate = routeRate * clamp(0.6 + (link.strength || 0), 0.6, 1.8); +for (const tile of link.path || []) { +if ((w.population[tile] || 0) > 0.01) this.blendTileEthnicComposition(tile, routeCulture, localRate); +this.forCardinalNeighbors(tile, neighbor => { +if ((w.population[neighbor] || 0) > 0.01) this.blendTileEthnicComposition(neighbor, routeCulture, localRate * neighborFactor); +}); } } } -w.dominantEthnicity.set(nextDominant); -w.cultureDiversity.set(nextDiversity); -} farmabilityAt(tile) { const t = this.world.terrain[tile]; if (t === Terrain.PLAINS) return 0.75; @@ -1721,34 +1789,14 @@ if (t === Terrain.FOREST) return 0.55; if (t === Terrain.DESERT) return 0.25; return 0.0; } -carryingCapacityAt(tile) { -return this.carryingCapacityAtTile(tile); -} -carryingCapacityAtTile(tile) { -const w = this.world; -if (w.terrain[tile] === Terrain.WATER) return 0.35; -const terrainFactor = -w.terrain[tile] === Terrain.MOUNTAIN ? 0.62 : -w.terrain[tile] === Terrain.DESERT ? 0.55 : -w.terrain[tile] === Terrain.FOREST ? 0.92 : -1; -const routeBonus = w.tradeRoute[tile] ? 1.6 : clamp(w.pheromone[tile] / Math.max(1, SimConfig.route.maxPheromone), 0, 1) * 0.8; -const cityBonus = w.city[tile] >= 0 ? 3.5 : w.cityPull[tile] * 2.2; -return Math.max(0.1, ( -SimConfig.population.carryingCapacityBase + -w.fertility[tile] * SimConfig.population.carryingCapacityFertility + -w.mineral[tile] * SimConfig.population.carryingCapacityMineral + -w.farmland[tile] * SimConfig.population.carryingCapacityFarmland + -routeBonus + -cityBonus -) * terrainFactor); -} -updatePopulationCapacity() { +initializePopulationCapacityCache() { const w = this.world; +const { carryingCapacityBase, carryingCapacityFertility, carryingCapacityMineral } = SimConfig.population; +const populationScale = SimConfig.population.populationScale; for (let i = 0; i < w.count; i++) { if (w.terrain[i] === Terrain.WATER) { -w.populationCapacity[i] = 0.35; -w.populationPressure[i] = w.population[i] / 0.35; +this.populationCapacityStatic[i] = 0.35 * populationScale; +this.populationCapacityDynamicScale[i] = 0; continue; } const terrainFactor = @@ -1756,20 +1804,44 @@ w.terrain[i] === Terrain.MOUNTAIN ? 0.62 : w.terrain[i] === Terrain.DESERT ? 0.55 : w.terrain[i] === Terrain.FOREST ? 0.92 : 1; -const routeBonus = w.tradeRoute[i] ? 1.6 : clamp(w.pheromone[i] / Math.max(1, SimConfig.route.maxPheromone), 0, 1) * 0.8; -const cityBonus = w.city[i] >= 0 ? 3.5 : w.cityPull[i] * 2.2; -const capacity = Math.max(0.1, ( -SimConfig.population.carryingCapacityBase + -w.fertility[i] * SimConfig.population.carryingCapacityFertility + -w.mineral[i] * SimConfig.population.carryingCapacityMineral + -w.farmland[i] * SimConfig.population.carryingCapacityFarmland + -routeBonus + -cityBonus -) * terrainFactor); -w.populationCapacity[i] = capacity; +const scale = terrainFactor * populationScale; +this.populationCapacityStatic[i] = ( +carryingCapacityBase + +w.fertility[i] * carryingCapacityFertility + +w.mineral[i] * carryingCapacityMineral +) * scale; +this.populationCapacityDynamicScale[i] = scale; +} +} +updatePopulationPressure() { +const w = this.world; +for (let i = 0; i < w.count; i++) { +const capacity = w.populationCapacity[i]; w.populationPressure[i] = capacity > 0 ? w.population[i] / capacity : 0; } } +updatePopulationCapacity() { +const w = this.world; +const { carryingCapacityFarmland } = SimConfig.population; +const maxPheromone = Math.max(1, SimConfig.route.maxPheromone); +const staticCapacity = this.populationCapacityStatic; +const dynamicScale = this.populationCapacityDynamicScale; +for (let i = 0; i < w.count; i++) { +const scale = dynamicScale[i]; +if (scale === 0) { +w.populationCapacity[i] = staticCapacity[i]; +w.populationPressure[i] = w.population[i] / staticCapacity[i]; +continue; +} +const routeBonus = w.tradeRoute[i] ? 1.6 : Math.min(1, Math.max(0, w.pheromone[i] / maxPheromone)) * 0.8; +const cityBonus = w.city[i] >= 0 ? 3.5 : w.cityPull[i] * 2.2; +const capacity = staticCapacity[i] + ( +w.farmland[i] * carryingCapacityFarmland + routeBonus + cityBonus +) * scale; +w.populationCapacity[i] = capacity; +w.populationPressure[i] = w.population[i] / capacity; +} +} updateTileTechnology() { const w = this.world; const tiles = this.activePopulationSnapshot(); @@ -1863,46 +1935,74 @@ const tempDiff = Math.abs(this.world.temperature[tile] - ethnicity.climateTemp); const humidDiff = Math.abs(this.world.humidity[tile] - ethnicity.climateHumidity); return tempDiff * 0.58 + humidDiff * 0.42; } -updateWorldFields(scale = 1) { +updateWorldFields(scale = 1, resourceScale = scale) { +if (resourceScale > 0) this.regenerateResources(resourceScale); +// Preserve the former biweekly pheromone diffusion semantics while keeping +// pheromone work sparse. Resource regeneration is batched separately by step(). +let remaining = Math.max(0, scale); +let pheromoneChanged = false; +while (remaining > 0) { +const chunk = Math.min(2, remaining); +if (this.updatePheromoneField(chunk)) pheromoneChanged = true; +remaining -= chunk; +} +return pheromoneChanged; +} +regenerateResources(scale = 1) { const w = this.world; -const pheromoneDecay = Math.pow(SimConfig.route.pheromoneDecay, scale); -const diffusion = SimConfig.route.pheromoneDiffusion * scale; -const nextPheromone = diffusion > 0 ? this.scratchFloatC : null; -if (nextPheromone) nextPheromone.set(w.pheromone); for (let i = 0; i < w.count; i++) { w.resource[i] = Math.min(58, w.resource[i] + w.regen[i] * scale * (1 + w.farmland[i] * 1.15)); -const decayed = w.pheromone[i] * pheromoneDecay; -if (nextPheromone) nextPheromone[i] = decayed; -else w.pheromone[i] = Math.min(SimConfig.route.maxPheromone, decayed); } -if (nextPheromone) { -for (let y = 0; y < w.size; y++) { -for (let x = 0; x < w.size; x++) { -const i = w.idx(x, y); -if (w.terrain[i] === Terrain.WATER) { -w.pheromone[i] = Math.min(SimConfig.route.maxPheromone, nextPheromone[i] * 0.85); -continue; } -let neighborSum = 0; -let neighborCount = 0; -for (let dy = -1; dy <= 1; dy++) { -for (let dx = -1; dx <= 1; dx++) { -if (!dx && !dy) continue; -const tx = x + dx; -const ty = y + dy; -if (tx < 0 || ty < 0 || tx >= w.size || ty >= w.size) continue; -const tile = w.idx(tx, ty); +updatePheromoneField(scale = 1) { +const w = this.world; +const active = this.activePheromoneTiles; +if (!active?.size) return false; +const decay = Math.pow(SimConfig.route.pheromoneDecay, scale); +const diffusion = SimConfig.route.pheromoneDiffusion * scale; +const maxPheromone = SimConfig.route.maxPheromone; +const touched = this.pheromoneTouchedScratch; +touched.length = 0; +const marker = this.nextVisitMarker(); +const starts = w.landNeighborStarts; +const neighbors = w.landNeighbors; +const markTouched = tile => { +if (this.visitStamp[tile] === marker) return; +this.visitStamp[tile] = marker; +touched.push(tile); +}; +for (const tile of active) { +if (tile < 0 || tile >= w.count) continue; +markTouched(tile); if (w.terrain[tile] === Terrain.WATER) continue; -neighborSum += nextPheromone[tile]; -neighborCount++; -} -} -const neighborAverage = neighborCount ? neighborSum / neighborCount : nextPheromone[i]; -w.pheromone[i] = Math.min(SimConfig.route.maxPheromone, lerp(nextPheromone[i], neighborAverage, diffusion)); +for (let n = starts[tile]; n < starts[tile + 1]; n++) markTouched(neighbors[n]); } +const decayed = this.scratchFloatC; +for (const tile of touched) decayed[tile] = w.pheromone[tile] * decay; +const nextActive = this.nextPheromoneTiles; +nextActive.clear(); +for (const tile of touched) { +let value; +if (w.terrain[tile] === Terrain.WATER) { +value = Math.min(maxPheromone, decayed[tile] * 0.85); +} else { +let neighborSum = 0; +const begin = starts[tile]; +const end = starts[tile + 1]; +for (let n = begin; n < end; n++) neighborSum += decayed[neighbors[n]]; +const neighborCount = end - begin; +const neighborAverage = neighborCount ? neighborSum / neighborCount : decayed[tile]; +value = Math.min(maxPheromone, lerp(decayed[tile], neighborAverage, diffusion)); } +if (value < 0.0005) value = 0; +w.pheromone[tile] = value; +if (value > 0) nextActive.add(tile); } +this.activePheromoneTiles = nextActive; +this.nextPheromoneTiles = active; +return touched.length > 0; } + updateCities() { const w = this.world; w.city.fill(-1); @@ -1918,7 +2018,8 @@ const settled = w.settledPopulation[tile] || 0; if (settled < 5 && population < 9) continue; const x = tile % w.size; const y = Math.floor(tile / w.size); -if (this.getCitiesNear(x, y, 10).length) continue; +const foundingSpacing = SimConfig.city.naturalFoundingMinDistance ?? 6; +if (this.getCitiesNear(x, y, Math.max(1, foundingSpacing - 1)).length) continue; const cx = clamp(Math.round(x / 4) * 4, 0, w.size - 1); const cy = clamp(Math.round(y / 4) * 4, 0, w.size - 1); const i = w.idx(cx, cy); @@ -1941,17 +2042,29 @@ for (const [id, count] of mix) group.ethnicities.set(id, (group.ethnicities.get( } } let foundedThisTick = 0; -const canFoundCities = this.year >= years(160) && this.year % years(20) === 0; -const foundingLimit = canFoundCities ? (this.year < years(1000) ? 1 : 1 + Math.floor(this.year / years(1200))) : 0; -const candidateEntries = [...candidates].sort((a, b) => { -const scoreA = this.cityFoundingScore(a[0], a[1]); -const scoreB = this.cityFoundingScore(b[0], b[1]); -return scoreB - scoreA; -}); +const foundingStartYears = SimConfig.city.naturalFoundingStartYears ?? 160; +const foundingIntervalYears = Math.max(1, SimConfig.city.naturalFoundingIntervalYears ?? 5); +const canFoundCities = this.year >= years(foundingStartYears) && this.year % years(foundingIntervalYears) === 0; +const foundingLimit = canFoundCities ? this.naturalCityFoundingLimit() : 0; +const scoredCandidates = [...candidates].map(([tile, group]) => ({ +tile, +group, +score: this.cityFoundingScore(tile, group) +})); +const bestFoundingScore = scoredCandidates.reduce((best, entry) => Math.max(best, entry.score), -Infinity); +// Near-equivalent sites should not always resolve to the exact same maximum. +// Keep strong suitability preference, but randomize within the broad top band. +for (const entry of scoredCandidates) { +const nearBest = entry.score >= bestFoundingScore - 4.0; +entry.priority = nearBest ? bestFoundingScore + this.rng.range(0, 4.0) : entry.score; +} +scoredCandidates.sort((a, b) => b.priority - a.priority); +const candidateEntries = scoredCandidates.map(entry => [entry.tile, entry.group]); for (const [i, group] of candidateEntries) { if (group.count < 24) continue; const avgSedentary = clamp(group.sedentary / Math.max(1, group.count), 0, 1); -let city = this.getCitiesNear(i % w.size, Math.floor(i / w.size), 12)[0] || null; +const foundingSpacing = SimConfig.city.naturalFoundingMinDistance ?? 6; +let city = this.getCitiesNear(i % w.size, Math.floor(i / w.size), foundingSpacing)[0] || null; if (!city && canFoundCities && foundedThisTick < foundingLimit && this.cities.length < SimConfig.city.maxCities) { const foundingScore = this.cityFoundingScore(i, group); const foundingChance = clamp((foundingScore - 12) * 0.030, 0.01, 0.38); @@ -1974,7 +2087,7 @@ city.knowledge.farming = Math.max(city.knowledge.farming * 0.998, group.farming city.knowledge.metallurgy = Math.max(city.knowledge.metallurgy * 0.998, group.metallurgy / Math.max(1, group.techCount)); const urbanWeight = clamp((avgSedentary - 0.18) * 1.45, 0.08, 1); for (const [id, count] of group.ethnicities) { -const urbanCount = this.weightedUrbanContribution(count * 0.2, urbanWeight); +const urbanCount = this.weightedUrbanContribution(count * 0.2 * SimConfig.city.populationScale / SimConfig.population.populationScale, urbanWeight); if (urbanCount > 0) city.ethnicityComposition.set(id, (city.ethnicityComposition.get(id) || 0) + urbanCount); } city.strength = city.strength * 0.96 + group.count * 0.05; @@ -1982,8 +2095,8 @@ city.strength = city.strength * 0.96 + group.count * 0.05; } this.absorbUrbanPopulation(); this.rebuildIndexes(); -this.updateFieldPressureFromPopulation(); this.processCityEconomies(); +const removedCities = []; this.cities = this.cities.filter(c => { c.age++; c.strength *= 0.992; @@ -1993,7 +2106,10 @@ if (c.activeVisitors < 2 && foodPerCapita < 0.04) c.strength -= 0.035; else if (c.activeVisitors < 5 && foodPerCapita < 0.02) c.strength -= 0.015; if (c.population < 25 && foodPerCapita < 0.05) c.strength -= 0.018; } -if (c.population <= 0 || c.strength <= 0.06) return false; +if (c.population < (SimConfig.city.abandonmentPopulation ?? 200) || c.strength <= 0.06) { +removedCities.push(c); +return false; +} const radius = c.agriculturalRadius; for (const offset of this.getRadiusOffsets(radius)) { const dx = offset.dx; @@ -2002,23 +2118,37 @@ const x = clamp(c.x + dx, 0, w.size - 1); const y = clamp(c.y + dy, 0, w.size - 1); const d = offset.distance; const tile = w.idx(x, y); -w.city[tile] = c.id; if (w.terrain[tile] !== Terrain.WATER) { w.farmland[tile] = Math.max(w.farmland[tile], (radius - d + 1) / (radius + 1)); w.cityPull[tile] = Math.max(w.cityPull[tile], (radius - d + 1) / (radius + 1)); } } +w.city[w.idx(c.x, c.y)] = c.id; return true; }); +for (const city of removedCities) { +const oldPolityId = city.polityId; +if (oldPolityId !== null) this.removeCityFromPolity(city); +const tile = w.idx(city.x, city.y); +if (w.city[tile] === city.id) w.city[tile] = -1; +if (oldPolityId !== null) this.releaseCityTerritory(city, oldPolityId, 5, "cityAbandoned"); +} this.rebuildIndexes(); this.updatePopulationCapacity(); +if (removedCities.length) this.pruneOrphanTradeRoutes(); +} +naturalCityFoundingLimit() { +const referenceArea = Math.max(1, SimConfig.city.naturalFoundingReferenceArea ?? (160 * 160)); +const maxPerCycle = Math.max(1, SimConfig.city.naturalFoundingMaxPerCycle ?? 8); +return clamp(Math.ceil(this.world.count / referenceArea), 1, maxPerCycle); } cityFoundingScore(tile, group) { const w = this.world; const settledShare = group.count > 0 ? group.sedentary / group.count : 0; const pressure = w.populationPressure[tile] || 0; const route = w.tradeRoute[tile] ? 1.4 : clamp(w.pheromone[tile] / 10, 0, 1.2); -const nearbyPenalty = this.getCitiesNear(tile % w.size, Math.floor(tile / w.size), 18).length * 11; +const spacing = SimConfig.city.naturalFoundingMinDistance ?? 6; +const nearbyPenalty = this.getCitiesNear(tile % w.size, Math.floor(tile / w.size), SimConfig.city.foundingCompetitionRadius ?? spacing + 3).length * 11; return group.count * (0.45 + settledShare) + pressure * 2.2 + w.fertility[tile] * 3.4 + @@ -2077,14 +2207,15 @@ this.removeEthnicPopulationProportionally(tile, take); remaining -= take; } } -createCity(x, y, seedGroup = null) { -const seedPopulation = seedGroup ? Math.max(14, Math.floor(seedGroup.count * 0.42)) : 10; +createCity(x, y, seedGroup = null, options = {}) { +const scale = SimConfig.city.populationScale; +const seedPopulation = Math.max(1, Math.round(options.initialPopulation ?? SimConfig.city.initialPopulation ?? 400)); const seedSedentary = seedGroup ? seedGroup.sedentary / Math.max(1, seedGroup.count) : 0.5; const composition = new Map(); if (seedGroup) { const urbanWeight = clamp((seedSedentary - 0.18) * 1.45, 0.08, 1); for (const [id, count] of seedGroup.ethnicities) { -const urbanCount = this.weightedUrbanContribution(count * 0.42, urbanWeight); +const urbanCount = this.weightedUrbanContribution(count * 0.42 * scale / SimConfig.population.populationScale, urbanWeight); if (urbanCount > 0) composition.set(id, urbanCount); } } @@ -2093,14 +2224,13 @@ id: this.nextCity++, x, y, population: seedPopulation, -storedResources: 34 + (seedGroup?.resources || 0) * 0.55, +storedResources: options.initialResources ?? (34 + (seedGroup?.resources || 0) * 0.55) * scale, ethnicityComposition: composition, pheromoneOutput: 0, agriculturalRadius: 2, tradeLinks: new Set(), activeVisitors: 0, age: 0, -ageWear: 0, peakPopulation: seedPopulation, strength: 3, sedentaryCulture: seedSedentary, @@ -2110,27 +2240,14 @@ polityId: null, loyalty: 0.5, receivedAid: false, tradeValue: 0, -tradeReach: 0 +tradeReach: 0, +rebellionProtectedUntil: 0 }; if (seedGroup) this.drawPopulationFromTilesForCity(city, seedGroup, seedPopulation); this.projectCityPopulationToTiles(city); this.syncTilePopulationCulture(); return city; } -updateCityAging(city) { -city.ageWear ??= 0; -city.peakPopulation = Math.max(city.peakPopulation || 0, city.population || 0); -const cfg = SimConfig.city; -const ageStart = cfg.agingStartYears ?? 180; -if ((city.age || 0) <= ageStart) return; -city.ageWear = clamp(city.ageWear + (cfg.agingDecayPerYear ?? 0.002), 0, 1); -} -cityAgeCapacityFactor(city) { -return clamp(1 - (city?.ageWear || 0), 0, 1); -} -cityAgePopulationLimit(city) { -return this.cityCarryingCapacity(city); -} cityCarryingCapacity(city) { const w = this.world; if (!city) return 1; @@ -2164,8 +2281,7 @@ farmlandSum * 18 + (city.tradeValue || 0) * 45 + (city.knowledge?.farming || 0) * 90 + Math.sqrt(Math.max(0, city.storedResources || 0)) * 3; -capacity *= clamp(1 - (city.ageWear || 0) * 0.45, 0.45, 1.25); -return Math.max(20, capacity); +return Math.max(20, capacity) * SimConfig.city.populationScale; } absorbUrbanPopulation() { // Population is already absorbed into cities when field-founded settlements draw @@ -2176,11 +2292,19 @@ const value = amount * weight; const whole = Math.floor(value); return whole + (this.rng.next() < value - whole ? 1 : 0); } +cityRecoveryGrowthBonus(city) { +if (!city || city.population <= 0) return 0; +const peak = Math.max(city.population, city.peakPopulation || city.population); +if (peak <= 0) return 0; +const recoveryGap = clamp((peak - city.population) / peak, 0, 0.75); +return recoveryGap * (SimConfig.city.postShockRecoveryGrowth ?? 0.045); +} processCityEconomies() { const w = this.world; for (const city of this.cities) { -this.updateCityAging(city); -city.agriculturalRadius = clamp(Math.floor(1 + Math.sqrt(city.population) / 4.5), 2, 14); +city.peakPopulation = Math.max(city.peakPopulation || 0, city.population || 0); +const populationScale = SimConfig.city.populationScale; +city.agriculturalRadius = clamp(Math.floor(1 + Math.sqrt(city.population / populationScale) / 4.5), 2, 14); city.pheromoneOutput = clamp(Math.log2(city.population + 1) * 0.09, 0.15, 1.8); city.knowledge ??= { farming: 0, metallurgy: 0 }; let harvested = 0; @@ -2196,7 +2320,7 @@ if (w.terrain[i] === Terrain.WATER) continue; const pull = (radius - distance + 1) / (radius + 1); const farmingYield = 1 + city.knowledge.farming * 0.85; const metallurgyYield = 1 + city.knowledge.metallurgy * w.mineral[i] * 0.35; -const extraction = Math.min(w.resource[i], (0.07 + w.fertility[i] * 0.18 * farmingYield + w.mineral[i] * 0.045 * metallurgyYield) * pull); +const extraction = Math.min(w.resource[i], (0.07 + w.fertility[i] * 0.18 * farmingYield + w.mineral[i] * 0.045 * metallurgyYield) * pull * SimConfig.city.foodProductionScale); w.resource[i] -= extraction; w.farmland[i] = Math.max(w.farmland[i], pull); this.addPheromone(i, city.pheromoneOutput * pull * 0.09); @@ -2204,9 +2328,9 @@ harvested += extraction; } city.storedResources += harvested; const trade = this.cityTradeProfile(city); -const tradeIncome = trade.value * (0.18 + Math.sqrt(Math.max(0, city.population)) * 0.018); +const tradeIncome = trade.value * (0.18 + Math.sqrt(Math.max(0, city.population / populationScale)) * 0.018) * populationScale; city.storedResources += tradeIncome; -const supportRatio = city.activeVisitors / Math.max(1, city.population); +const supportRatio = (city.activeVisitors / SimConfig.population.populationScale) / Math.max(1, city.population / populationScale); const knowledgeMaintenance = city.population * (city.knowledge.farming * 0.0008 + city.knowledge.metallurgy * 0.0012); const tradeRelief = clamp(trade.value * 0.09, 0, 0.32); const upkeep = (city.population * (0.010 + Math.max(0, 0.025 - supportRatio) * 0.09)) * (1 - tradeRelief) + knowledgeMaintenance; @@ -2225,20 +2349,29 @@ city.tradeValue = 0; city.tradeReach = 0; } const capacity = this.cityCarryingCapacity(city); -if (city.storedResources > city.population * 0.14 && city.population > 0 && city.population < capacity && city.supplyStress < 0.9) { +const recoveryBonus = this.cityRecoveryGrowthBonus(city); +const recoveryMode = recoveryBonus > 0.002; +const growthReserveRatio = recoveryMode ? 0.06 : 0.11; +const growthStressLimit = recoveryMode ? 1.20 : 0.90; +if (city.storedResources > city.population * growthReserveRatio && city.population > 0 && city.population < capacity && city.supplyStress < growthStressLimit) { const prosperity = clamp(city.storedResources / Math.max(1, city.population) - 0.12 + trade.value * 0.012, 0, 0.8); const birthRoom = Math.max(0, Math.floor(capacity - city.population)); -const births = Math.min(birthRoom, Math.max(1, Math.floor(city.population * (0.012 + prosperity * 0.012 + clamp(trade.value, 0, 1.8) * 0.0015)))); +const births = Math.min(birthRoom, Math.max(1, Math.floor(city.population * ( +0.012 + +prosperity * 0.012 + +clamp(trade.value, 0, 1.8) * 0.0015 + +recoveryBonus +)))); city.population += births; city.storedResources -= births * 0.55; addBirthsToComposition(city.ethnicityComposition, births, this.rng); } if (city.population > capacity && city.supplyStress > 0.45) { const overLimit = city.population - capacity; -const ageLoss = Math.min(city.population, Math.max(1, Math.ceil(overLimit * (SimConfig.city.agingOverCapAttrition ?? 0.035)))); -city.population -= ageLoss; -removeFromComposition(city.ethnicityComposition, ageLoss); -city.strength -= Math.min(0.08, ageLoss / Math.max(1, city.population + ageLoss) * 0.18); +const overCapacityLoss = Math.min(city.population, Math.max(1, Math.ceil(overLimit * (SimConfig.city.overCapacityAttrition ?? 0.020)))); +city.population -= overCapacityLoss; +removeFromComposition(city.ethnicityComposition, overCapacityLoss); +city.strength -= Math.min(0.08, overCapacityLoss / Math.max(1, city.population + overCapacityLoss) * 0.18); } if (city.age > 20 && city.activeVisitors < 2 && city.storedResources < city.population * 0.03 && city.population > 0) { const attrition = Math.max(1, Math.ceil(city.population * (city.activeVisitors === 0 ? 0.025 : 0.010))); @@ -2327,7 +2460,7 @@ return (Math.sqrt(city.population) * 1.4 + Math.sqrt(Math.max(0, city.storedReso } setTileOwner(tile, polityId, options = {}) { const w = this.world; -if (tile == null || tile < 0 || tile >= w.count || w.terrain[tile] === Terrain.WATER) return false; +if (tile == null || tile < 0 || tile >= w.count || (w.terrain[tile] === Terrain.WATER && (polityId ?? -1) >= 0)) return false; const owner = polityId ?? -1; if (owner >= 0 && !this.getPolityById(owner)) return false; w.territoryOwner[tile] = owner; @@ -2347,14 +2480,13 @@ return true; } syncVisibleTerritoryOwners() { const w = this.world; -w.polity.set(w.territoryOwner); +if (w.polity !== w.territoryOwner) w.polity.set(w.territoryOwner); } validPolityIds() { return new Set(this.polities.map(polity => polity.id)); } -// Territory ownership is persistent. This pass refreshes control/claim/contest -// visualization from soft influence without moving borders unless explicitly -// requested for initialization/debug repair. +// Territory follows a weighted nearest-city model. Distance is primary, while +// city/state power, loyalty, trade, terrain and culture shift the boundary. clearTerritories() { const w = this.world; this.syncVisibleTerritoryOwners(); @@ -2366,19 +2498,39 @@ recomputeTerritories(options = {}) { const w = this.world; const allowOwnershipChanges = !!options.allowOwnershipChanges; this.clearTerritories(); -if (!this.polities.length || !this.cities.length) return; +if (!this.polities.length || !this.cities.length) { +this.enforceTerritoryConnectivity(); +return; +} const best = this.scratchFloatA; const second = this.scratchFloatB; +const incumbent = this.scratchFloatC; const owner = this.scratchIntB; +const bestSupport = this.territoryBestSupport; +const incumbentSupport = this.territoryIncumbentSupport; best.fill(0); second.fill(0); +incumbent.fill(0); owner.fill(-1); +bestSupport.fill(0); +incumbentSupport.fill(0); for (const polity of this.polities) { const polityCities = this.getPolityCities(polity); if (!polityCities.length) continue; const center = this.getCityById(polity.centerCityId) || polityCities[0]; const maxRange = clamp(Math.ceil(this.polityInfluenceRange(polity, false)), 8, 36); for (const city of polityCities) { +const targetPopulationSupport = clamp( +Math.sqrt(Math.max(0, city.population || 0) / (SimConfig.territory?.fullMaintenanceCityPopulation ?? 2400)), +SimConfig.territory?.minimumPopulationSupport ?? 0.28, +1 +); +if (city.territorySupport == null) city.territorySupport = targetPopulationSupport; +else if (city.territorySupportUpdatedAt !== this.year) { +city.territorySupport = lerp(city.territorySupport, targetPopulationSupport, SimConfig.territory?.supportSmoothing ?? 0.18); +} +city.territorySupportUpdatedAt = this.year; +const populationSupport = city.territorySupport; const cityTile = w.idx(city.x, city.y); const cityEthnicity = this.dominantCityEthnicity(city); const capitalDistance = center ? this.effectiveDistance(center, city) : 0; @@ -2386,46 +2538,97 @@ const capitalFactor = city.id === polity.centerCityId ? 1.28 : clamp(1 - capital const loyaltyFactor = 0.62 + clamp(city.loyalty ?? 0.5, 0, 1) * 0.58; const tradeFactor = 1 + clamp((city.tradeReach || 0) / 60, 0, 0.42) + clamp((city.tradeValue || 0) * 0.10, 0, 0.22); const baseInfluence = Math.max(1, this.cityInfluence(city)) * capitalFactor * loyaltyFactor * tradeFactor; -const radius = clamp(Math.ceil(maxRange * (0.62 + clamp(Math.sqrt(city.population || 1) / 30, 0, 0.45))), 6, maxRange); -for (const { dx, dy, distance: manhattan } of this.getRadiusOffsets(radius)) { +const normalizedInfluence = baseInfluence / Math.sqrt(SimConfig.city.populationScale || 1); +const cityPowerReach = clamp(Math.log1p(normalizedInfluence) * (SimConfig.territory?.cityPowerWeight ?? 1.8), 2, 10); +const capitalReach = city.id === polity.centerCityId ? (SimConfig.territory?.capitalReachBonus ?? 1.5) : 0; +const radiusScale = (SimConfig.territory?.radiusScale ?? 0.5) * populationSupport; +const radius = clamp( +Math.ceil(maxRange * (0.62 + clamp(Math.sqrt(city.population || 1) / 30, 0, 0.45)) * radiusScale), +3, +Math.max(3, Math.ceil(maxRange * radiusScale)) +); +for (const { dx, dy } of this.getRadiusOffsets(Math.ceil(radius * Math.SQRT2))) { +const distance = Math.hypot(dx, dy); +if (distance > radius) continue; const x = city.x + dx; const y = city.y + dy; if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; const tile = w.idx(x, y); const isWater = w.terrain[tile] === Terrain.WATER; if (isWater && tile !== cityTile && !w.tradeRoute[tile] && !this.territoryWaterAnchor(tile)) continue; +if ((w.population[tile] || 0) <= 0 && tile !== cityTile) continue; const routeBoost = w.tradeRoute[tile] ? 1.28 : 1; const terrainDrag = isWater ? 2.35 : w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.18 : 1); -const effectiveDistance = (manhattan + terrainDrag * 1.4) / routeBoost; -const distanceFalloff = clamp(1 - effectiveDistance / (radius + 4), 0, 1); -if (distanceFalloff <= 0) continue; +const effectiveDistance = (distance + terrainDrag * 1.4) / routeBoost; const localEthnicity = w.dominantEthnicity[tile]; -const ethnicityFactor = cityEthnicity !== null && localEthnicity >= 0 && cityEthnicity !== localEthnicity ? 0.82 : 1.06; -const populationAnchor = clamp(Math.sqrt(Math.max(0, w.population[tile])) / 20, 0, 0.28); -const score = baseInfluence * distanceFalloff * distanceFalloff * routeBoost * ethnicityFactor * (1 + populationAnchor) / (1 + terrainDrag * 0.18); +const ethnicityShift = cityEthnicity !== null && localEthnicity >= 0 && cityEthnicity !== localEthnicity ? -0.65 : 0.35; +const populationAnchor = clamp(Math.sqrt(Math.max(0, w.population[tile])) / 40, 0, 0.7); +const maintainedReach = (SimConfig.territory?.baseCityReach ?? 9) * (0.45 + populationSupport * 0.55); +const score = maintainedReach - effectiveDistance + cityPowerReach * populationSupport + capitalReach + ethnicityShift + populationAnchor; if (score <= 0.8) continue; +if (w.territoryOwner[tile] === polity.id && score > incumbent[tile]) { +incumbent[tile] = score; +incumbentSupport[tile] = populationSupport; +} if (score > best[tile]) { if (owner[tile] !== polity.id) second[tile] = best[tile]; best[tile] = score; owner[tile] = polity.id; +bestSupport[tile] = populationSupport; } else if (owner[tile] !== polity.id && score > second[tile]) { second[tile] = score; } } } } -const threshold = 2.4; +const threshold = SimConfig.territory?.minimumInfluence ?? 2.4; +const rebalanceOwnership = !!options.rebalanceOwnership; +const switchMargin = SimConfig.territory?.ownershipSwitchMargin ?? 1.5; +const activeWarPairs = new Set((this.wars || []) +.filter(war => war.ended === null) +.map(war => `${Math.min(war.aPolityId, war.bPolityId)}:${Math.max(war.aPolityId, war.bPolityId)}`)); for (let i = 0; i < w.count; i++) { const currentOwner = w.territoryOwner[i]; -if (allowOwnershipChanges && currentOwner < 0 && owner[i] >= 0 && best[i] >= threshold) { -this.setTileOwner(i, owner[i], { +const proposedOwner = owner[i]; +const cityHere = w.city[i] >= 0 ? this.getCityById(w.city[i]) : null; +const protectedCityCore = cityHere?.polityId != null && cityHere.polityId !== proposedOwner; +const currentCityCore = cityHere?.polityId === currentOwner; +const atWarAcrossBorder = currentOwner >= 0 && proposedOwner >= 0 && currentOwner !== proposedOwner && +activeWarPairs.has(`${Math.min(currentOwner, proposedOwner)}:${Math.max(currentOwner, proposedOwner)}`); +const currentSupport = incumbentSupport[i] || 0; +const proposedSupport = bestSupport[i] || 0; +const maintenanceThreshold = threshold + (1 - currentSupport) * (SimConfig.territory?.maintenancePressure ?? 4); +const expansionThreshold = threshold + (1 - proposedSupport) * (SimConfig.territory?.maintenancePressure ?? 4); +if ( +allowOwnershipChanges && +currentOwner >= 0 && +currentSupport < 1 && +!currentCityCore && +incumbent[i] < maintenanceThreshold +) { +this.setTileOwner(i, -1); +this.territorialClaims.delete(i); +continue; +} +const shouldSwitchOwner = +rebalanceOwnership && +currentOwner >= 0 && +proposedOwner >= 0 && +proposedOwner !== currentOwner && +!protectedCityCore && +!atWarAcrossBorder && +best[i] >= expansionThreshold && +best[i] >= incumbent[i] + switchMargin + (1 - Math.min(currentSupport, proposedSupport)) * (SimConfig.territory?.smallStateSwitchPenalty ?? 2.5); +if (allowOwnershipChanges && (currentOwner < 0 || shouldSwitchOwner) && proposedOwner >= 0 && best[i] >= expansionThreshold) { +this.setTileOwner(i, proposedOwner, { control: clamp((best[i] - threshold) / 18, 0.12, 1), claim: clamp(best[i] / 22, 0, 1) }); +if (this.territorialClaims.get(i)?.polityId !== proposedOwner) this.territorialClaims.delete(i); continue; } if (currentOwner < 0) { -if (owner[i] >= 0 && best[i] >= threshold) { +if (owner[i] >= 0 && best[i] >= expansionThreshold) { w.claim[i] = clamp(best[i] / 28, 0, 0.45); if (second[i] > best[i] * 0.72) w.contested[i] = 1; } @@ -2443,8 +2646,103 @@ if (owner[i] >= 0 && owner[i] !== currentOwner && best[i] >= threshold * 0.8) w. } this.applyStoredTerritorialClaims(); this.enforceCityTerritoryAnchors(options); +if (allowOwnershipChanges) this.smoothTerritoryBorders(1); +this.pruneUnpopulatedTerritory(); this.enforceTerritoryConnectivity(options); } +pruneUnpopulatedTerritory() { +const w = this.world; +for (let tile = 0; tile < w.count; tile++) { +if (w.territoryOwner[tile] < 0 || (w.population[tile] || 0) > 0) continue; +const city = w.city[tile] >= 0 ? this.getCityById(w.city[tile]) : null; +if (city && city.population > 0) continue; +this.setTileOwner(tile, -1); +this.territorialClaims.delete(tile); +} +} +smoothTerritoryBorders(passes = 2) { +const w = this.world; +const source = this.scratchIntA; +const protectedTiles = this.scratchUintA; +protectedTiles.fill(0); +for (const city of this.cities) { +const tile = w.idx(city.x, city.y); +protectedTiles[tile] = 1; +for (let n = w.landNeighborStarts[tile]; n < w.landNeighborStarts[tile + 1]; n++) { +protectedTiles[w.landNeighbors[n]] = 1; +} +} +for (let pass = 0; pass < passes; pass++) { +source.set(w.territoryOwner); +let changed = 0; +for (let tile = 0; tile < w.count; tile++) { +if (w.terrain[tile] === Terrain.WATER || protectedTiles[tile] || w.tradeRoute[tile]) continue; +const start = w.landNeighborStarts[tile]; +const end = w.landNeighborStarts[tile + 1]; +if (end - start < 6) continue; // Preserve coasts and the edge of the world. +const owner = source[tile]; +let candidate = -1; +let same = 0; +let neutral = 0; +let foreign = false; +for (let n = start; n < end; n++) { +const neighborOwner = source[w.landNeighbors[n]]; +if (neighborOwner < 0) neutral++; +else if (owner >= 0) { +if (neighborOwner === owner) same++; +else foreign = true; +} else if (candidate < 0) { candidate = neighborOwner; same++; } +else if (neighborOwner === candidate) same++; +else foreign = true; +} +const x = tile % w.size; +let cardinal = 0; +const target = owner >= 0 ? owner : candidate; +if (x > 0 && source[tile - 1] === target) cardinal++; +if (x < w.size - 1 && source[tile + 1] === target) cardinal++; +if (tile >= w.size && source[tile - w.size] === target) cardinal++; +if (tile < w.count - w.size && source[tile + w.size] === target) cardinal++; +if (foreign) { +// At a shared border, move only small leaves toward a clear local majority. +// Tiles with two connections to their own country can be bridges: keep them. +if (owner < 0 || cardinal > 1) continue; +let majority = -1; +let votes = 0; +for (let n = start; n < end; n++) { +const id = source[w.landNeighbors[n]]; +if (!votes) { majority = id; votes = 1; } +else votes += id === majority ? 1 : -1; +} +if (majority < 0 || majority === owner) continue; +let count = 0; +let touching = false; +for (let n = start; n < end; n++) { +const neighbor = w.landNeighbors[n]; +if (source[neighbor] !== majority) continue; +count++; +if (neighbor === tile - w.size || neighbor === tile + w.size || +(Math.floor(neighbor / w.size) === Math.floor(tile / w.size) && Math.abs(neighbor - tile) === 1)) touching = true; +} +if (count < 5 || !touching) continue; +this.setTileOwner(tile, majority, { control: 0.22, claim: 0.35, contested: false }); +this.territorialClaims.set(tile, { polityId: majority, control: 0.22, claim: 0.35, reason: "borderSmoothing", year: this.year }); +changed++; +continue; +} +if (owner < 0 && candidate >= 0 && same >= 5 && cardinal >= 2) { +this.setTileOwner(tile, candidate, { control: 0.22, claim: 0.35 }); +changed++; +} else if (owner >= 0 && neutral >= 5 && cardinal <= 1) { +// Trim only leaves; keep narrow bridges and corridors connected. +this.setTileOwner(tile, -1); +this.territorialClaims.delete(tile); +changed++; +} +} +if (!changed) break; +} +this.syncVisibleTerritoryOwners(); +} applyStoredTerritorialClaims() { const w = this.world; const validPolities = this.validPolityIds(); @@ -2477,9 +2775,8 @@ for (const city of this.cities) { if (city.population <= 0) continue; const tile = w.idx(city.x, city.y); if (city.polityId === null) { -if (w.territoryOwner[tile] >= 0) { -w.contested[tile] = 1; -} +this.setTileOwner(tile, -1); +this.territorialClaims.delete(tile); continue; } const polity = this.getPolityById(city.polityId); @@ -2494,9 +2791,13 @@ if (w.terrain[t] === Terrain.WATER) continue; const otherCity = w.city[t] >= 0 ? this.getCityById(w.city[t]) : null; if (otherCity && otherCity.id !== city.id && otherCity.polityId !== city.polityId) continue; const core = dx === 0 && dy === 0; +if (core || (allowOwnershipChanges && w.territoryOwner[t] < 0)) this.setTileOwner(t, polity.id); +if (core && this.territorialClaims.get(t)?.polityId !== polity.id) this.territorialClaims.delete(t); const ownedByCityPolity = w.territoryOwner[t] === polity.id; -if (allowOwnershipChanges && (core || w.territoryOwner[t] < 0)) this.setTileOwner(t, polity.id); -else if (!ownedByCityPolity) w.contested[t] = 1; +if (!ownedByCityPolity) { +if (w.territoryOwner[t] >= 0) w.contested[t] = 1; +continue; +} w.control[t] = Math.max(w.control[t], core ? 0.55 : 0.35); w.claim[t] = Math.max(w.claim[t], core ? 0.62 : 0.40); if (core && (ownedByCityPolity || allowOwnershipChanges)) w.contested[t] = 0; @@ -2506,40 +2807,38 @@ if (core && (ownedByCityPolity || allowOwnershipChanges)) w.contested[t] = 0; } enforceTerritoryConnectivity(options = {}) { const w = this.world; -if (!options.allowOwnershipChanges) { -this.syncVisibleTerritoryOwners(); -return 0; -} +this.enforceCityTerritoryAnchors(); let removed = 0; -for (const polity of this.polities) { -const seeds = this.getPolityCities(polity).map(city => w.idx(city.x, city.y)); -if (!seeds.length) continue; const reachable = this.visitStamp; const marker = this.nextVisitMarker(); -const queue = []; -for (const seed of seeds) { -if (w.territoryOwner[seed] !== polity.id) continue; +const queue = this.scratchIntA; +let tail = 0; +const validPolities = this.validPolityIds(); +for (const city of this.cities) { +if (city.population <= 0 || city.polityId == null || !validPolities.has(city.polityId)) continue; +const seed = w.idx(city.x, city.y); +if (w.terrain[seed] === Terrain.WATER || w.territoryOwner[seed] !== city.polityId || reachable[seed] === marker) continue; reachable[seed] = marker; -queue.push(seed); +queue[tail++] = seed; } -for (let q = 0; q < queue.length; q++) { +// All countries share one traversal: each tile is visited at most once. +for (let q = 0; q < tail; q++) { const tile = queue[q]; +const owner = w.territoryOwner[tile]; this.forCardinalNeighbors(tile, n => { -if (reachable[n] === marker || w.territoryOwner[n] !== polity.id) return; -if (w.terrain[n] === Terrain.WATER && (!w.tradeRoute[n] || !this.territoryWaterAnchor(n))) return; +if (reachable[n] === marker || w.territoryOwner[n] !== owner || w.terrain[n] === Terrain.WATER) return; reachable[n] = marker; -queue.push(n); +queue[tail++] = n; }); } for (let i = 0; i < w.count; i++) { -if (w.territoryOwner[i] !== polity.id || reachable[i] === marker) continue; +if (w.territoryOwner[i] < 0 || reachable[i] === marker) continue; this.setTileOwner(i, -1); -const stored = this.territorialClaims.get(i); -if (stored?.polityId === polity.id) this.territorialClaims.delete(i); +this.territorialClaims.delete(i); removed++; } -} this.territoryDebug.disconnectedTerritoryRemoved += removed; +this.syncVisibleTerritoryOwners(); return removed; } reconcileCityTerritoryOwnership() { @@ -2573,46 +2872,6 @@ if (Math.abs(cx - x) + Math.abs(cy - y) <= radius || campaign.targetTile === til } return false; } -surroundingPolityForCity(city, radius) { -const w = this.world; -const counts = new Map(); -const controls = new Map(); -let total = 0; -for (const { dx, dy } of this.getRadiusOffsets(radius)) { -const x = city.x + dx; -const y = city.y + dy; -if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; -const tile = w.idx(x, y); -const owner = w.polity[tile]; -if (owner < 0) continue; -total++; -counts.set(owner, (counts.get(owner) || 0) + 1); -controls.set(owner, (controls.get(owner) || 0) + (w.control[tile] || 0)); -} -let best = null; -let count = 0; -for (const [id, value] of counts) { -if (value > count) { -best = id; -count = value; -} -} -const polity = best != null ? this.getPolityById(best) : null; -return polity ? { polity, share: count / Math.max(1, total), control: (controls.get(best) || 0) / Math.max(1, count) } : null; -} -clearIndependentCityEnclave(city, radius) { -const w = this.world; -for (const { dx, dy } of this.getRadiusOffsets(radius)) { -const x = city.x + dx; -const y = city.y + dy; -if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; -const tile = w.idx(x, y); -if (w.terrain[tile] === Terrain.WATER) continue; -w.control[tile] = 0; -w.claim[tile] = 0; -w.contested[tile] = 0; -} -} territoryWaterAnchor(tile) { const w = this.world; let anchored = false; @@ -2635,23 +2894,6 @@ return false; }); return border; } -territoryStatsForPolity(polity) { -const w = this.world; -const stats = { tiles: 0, population: 0, coreTiles: 0, contestedTiles: 0, borderTiles: 0 }; -if (!polity) return stats; -for (let i = 0; i < w.count; i++) { -if (w.polity[i] !== polity.id) continue; -stats.tiles++; -stats.population += w.population?.[i] || 0; -if (w.control[i] >= 0.55 && !w.contested[i]) stats.coreTiles++; -if (w.contested[i]) stats.contestedTiles++; -if (this.isBorderTile(i)) stats.borderTiles++; -} -if (stats.population <= 0) { -stats.population = this.getPolityCities(polity).reduce((sum, city) => sum + Math.max(0, city.population || 0), 0); -} -return stats; -} campaignPressureOnTerritories() { const w = this.world; const cfg = SimConfig.campaign || {}; @@ -2681,21 +2923,26 @@ w.claim[tile] = Math.max(w.claim[tile], local); w.contested[tile] = 1; } } -this.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); } } getCityById(id) { const cached = this.cityById?.get(id); if (cached) return cached; const city = this.cities.find(c => c.id === id) || null; -if (city) this.cityById.set(id, city); +if (city) { +this.cityById ??= new Map(); +this.cityById.set(id, city); +} return city; } getPolityById(id) { const cached = this.polityById?.get(id); if (cached) return cached; const polity = this.polities.find(p => p.id === id) || null; -if (polity) this.polityById.set(id, polity); +if (polity) { +this.polityById ??= new Map(); +this.polityById.set(id, polity); +} return polity; } getPolityCities(polity) { @@ -2802,13 +3049,16 @@ cityIds: new Set(), treasury: Math.max(0, centerCity.storedResources * 0.12), color: hslToRgb((id * 0.38196601125) % 1, 0.58, 0.62), founded: this.year, +nextWarAllowedWeek: this.year + years(SimConfig.polity.foundingProtectionYears ?? 12), legitimacy: this.rng.range(0.68, 0.94), cohesion: this.rng.range(0.58, 0.9), charisma: this.rng.range(SimConfig.polity.charismaMin ?? 0.5, SimConfig.polity.charismaMax ?? 1.5), leaderStarted: this.year, leaderTenureYears: this.rng.range(SimConfig.polity.leaderTenureMinYears ?? 24, SimConfig.polity.leaderTenureMaxYears ?? 68), crisis: 0, -lastCrisisYear: this.year +lastCrisisYear: this.year, +institutionalAging: 0, +lastInstitutionalReformYear: this.year }; this.polities.push(polity); this.polityById.set(polity.id, polity); @@ -2818,6 +3068,7 @@ control: 0.72, claim: 0.85, reason: "founding" }); +this.enforcePolityTreasuryCap(polity); this.claimPolityCoreTerritory(polity, centerCity, 4, "founding"); this.ensurePolityHistory(polity); this.samplePolityHistory(polity); @@ -2871,6 +3122,8 @@ cities: cities.length, population, treasury, avgLoyalty, +institutionalAging: clamp(polity.institutionalAging ?? 0, 0, 1), +loyaltyCap: this.polityLoyaltyCap ? this.polityLoyaltyCap(polity) : 1, power: typeof this.polityPower === "function" ? this.polityPower(polity) : fallbackPower }; history.samples.push(sample); @@ -2884,6 +3137,24 @@ while (history.samples.length > maxSamples) history.samples.shift(); samplePolityHistories() { for (const polity of this.polities) this.samplePolityHistory(polity); } +sampleWorldHistory() { +let regionalPopulation = 0; +for (const tile of this.activePopulationTiles) regionalPopulation += Math.max(0, this.world.population[tile] || 0); +const urbanPopulation = this.cities.reduce((sum, city) => sum + Math.max(0, city.population || 0), 0); +const sample = { +year: this.year, +regionalPopulation, +urbanPopulation, +cities: this.cities.length, +polities: this.polities.length, +ethnicities: [...this.ethnicities.values()].filter(ethnicity => (ethnicity.population || 0) > 0).length +}; +const last = this.worldHistory[this.worldHistory.length - 1]; +if (last?.year === this.year) this.worldHistory[this.worldHistory.length - 1] = sample; +else this.worldHistory.push(sample); +const maxSamples = (SimConfig.render.historyWindowYears + SimConfig.render.historySamplePaddingYears) * 4; +while (this.worldHistory.length > maxSamples) this.worldHistory.shift(); +} addPolityEvent(polityId, type, year = this.year, data = {}, importance = 1) { let history = this.polityHistory.get(polityId) || this.deadPolityHistories.find(h => h.id === polityId) || null; if (!history) { @@ -3079,6 +3350,110 @@ this.disasters = this.disasters .slice(-(config?.maxActiveVisuals ?? 12)); while (this.disasterHistory.length > (config?.maxHistory ?? 160)) this.disasterHistory.shift(); } +triggerManualDisasterAt(x, y) { +const w = this.world; +const tx = clamp(Math.floor(x), 0, w.size - 1); +const ty = clamp(Math.floor(y), 0, w.size - 1); +const cfg = SimConfig.disaster || {}; +const radius = this.rng.range(Math.max(5, cfg.minRadius ?? 6), Math.min(24, cfg.maxRadius ?? 34)); +const intensity = this.rng.range(Math.max(0.48, cfg.minIntensity ?? 0.25), Math.min(0.82, cfg.maxIntensity ?? 0.95)); +const beforeId = this.nextDisaster; +this.applyDisaster(tx, ty, radius, intensity); +const record = this.disasterHistory.find(item => item.id === beforeId) || this.disasterHistory[this.disasterHistory.length - 1]; +return { ok: true, record, x: tx, y: ty, radius, intensity }; +} +manualCityEthnicitySeed(tile) { +const w = this.world; +const local = this.tileEthnicMix.get(tile); +if (local?.size) return new Map(local); +const dominant = w.dominantEthnicity[tile]; +if (dominant >= 0) return new Map([[dominant, 1]]); +const tx = tile % w.size; +const ty = Math.floor(tile / w.size); +let nearest = null; +let nearestDistance = Infinity; +for (const [cultureTile, mix] of this.tileEthnicMix) { +if (!mix?.size) continue; +const distance = Math.abs(cultureTile % w.size - tx) + Math.abs(Math.floor(cultureTile / w.size) - ty); +if (distance < nearestDistance) { +nearest = mix; +nearestDistance = distance; +if (distance <= 2) break; +} +} +if (nearest?.size) return new Map(nearest); +let nearestCity = null; +let cityDistance = Infinity; +for (const city of this.cities) { +if (!city.ethnicityComposition?.size) continue; +const distance = Math.abs(city.x - tx) + Math.abs(city.y - ty); +if (distance < cityDistance) { +nearestCity = city; +cityDistance = distance; +} +} +if (nearestCity) return new Map(nearestCity.ethnicityComposition); +const ethnicityId = this.createEthnicity(0, { +temperature: w.temperature[tile], +humidity: w.humidity[tile] +}); +const ethnicity = this.ethnicities.get(ethnicityId); +if (ethnicity) ethnicity.averageTraits = this.randomTraits(); +return new Map([[ethnicityId, 1]]); +} +applyCityEthnicitySeed(city, seed, population) { +city.ethnicityComposition ??= new Map(); +city.ethnicityComposition.clear(); +let total = 0; +for (const count of seed?.values?.() || []) total += Math.max(0, count || 0); +if (total <= 0) return; +let assigned = 0; +const entries = [...seed.entries()]; +for (let index = 0; index < entries.length; index++) { +const [id, count] = entries[index]; +const amount = index === entries.length - 1 +? Math.max(0, population - assigned) +: Math.max(0, Math.round(population * Math.max(0, count || 0) / total)); +if (amount <= 0) continue; +city.ethnicityComposition.set(id, amount); +assigned += amount; +} +} +buildCityAt(x, y) { +const w = this.world; +const tx = clamp(Math.floor(x), 0, w.size - 1); +const ty = clamp(Math.floor(y), 0, w.size - 1); +const tile = w.idx(tx, ty); +// Manual intervention bypasses natural spacing and the automatic-city cap, +// but cities still require land. +if (w.terrain[tile] === Terrain.WATER) return { ok: false, reason: "water", x: tx, y: ty }; +const ethnicitySeed = this.manualCityEthnicitySeed(tile); +const city = this.createCity(tx, ty); +const basePopulation = city.population; +const targetPopulation = Math.max(1, SimConfig.city.initialPopulation ?? 400); +this.applyCityEthnicitySeed(city, ethnicitySeed, targetPopulation); +city.population = targetPopulation; +city.peakPopulation = targetPopulation; +city.storedResources = Math.max(city.storedResources, targetPopulation * 0.20); +city.strength = Math.max(city.strength, 6); +city.activeVisitors = Math.max(city.activeVisitors, 12); +if (targetPopulation > basePopulation) { +city.population = targetPopulation - basePopulation; +this.projectCityPopulationToTiles(city); +city.population = targetPopulation; +} +this.cities.push(city); +this.rebuildIndexes(); +w.city[tile] = city.id; +this.assignNewCityToTerritoryOwner(city, tile, { explicit: true }); +if (city.polityId !== null) { +const polity = this.getPolityById(city.polityId); +if (polity) this.claimPolityCoreTerritory(polity, city, 3, "manualCity"); +} +this.updatePopulationCapacity(); +this.syncTilePopulationCulture(); +return { ok: true, city }; +} markPolityEnded(polity, reason = "dissolved") { if (!polity) return; const history = this.ensurePolityHistory(polity); @@ -3090,7 +3465,7 @@ history.fate = reason; history.centerCityId = polity.centerCityId ?? history.centerCityId; this.polityHistory.delete(polity.id); this.deadPolityHistories.push(history); -while (this.deadPolityHistories.length > 80) this.deadPolityHistories.shift(); +while (this.deadPolityHistories.length > 20) this.deadPolityHistories.shift(); } getAllPolityHistories() { return [...this.deadPolityHistories, ...this.polityHistory.values()] @@ -3129,6 +3504,8 @@ war.ended === null && } startWar(a, b) { if (!a || !b || a.id === b.id) return null; +if (this.year < (a.nextWarAllowedWeek ?? 0) || this.year < (b.nextWarAllowedWeek ?? 0)) return null; +if (this.wars.filter(war => war.ended === null).length >= SimConfig.war.maxActiveWars) return null; if (this.getWarBetween(a.id, b.id)) return null; if (this.isPolityAtWar(a.id) || !this.canBeWarTarget(b.id)) return null; const id = this.nextWar++; @@ -3140,7 +3517,7 @@ bPolityId: b.id, goal, started: this.year, lastActionYear: this.year, -intensity: this.rng.range(0.45, 1.05), +intensity: this.rng.range(0.9, 1.4), exhaustionA: 0, exhaustionB: 0, scoreA: 0, @@ -3152,6 +3529,8 @@ lastFrontAdvanceYear: new Map(), ended: null }; this.wars.push(war); +this.addPolityEvent(a.id, "war", this.year, { outcome: "started", targetPolityId: b.id }, 2); +this.addPolityEvent(b.id, "war", this.year, { outcome: "started", targetPolityId: a.id }, 2); a.crisis = clamp((a.crisis || 0) + 0.04, 0, 1.5); b.crisis = clamp((b.crisis || 0) + 0.04, 0, 1.5); return war; @@ -3175,6 +3554,16 @@ endWar(war) { if (!war || war.ended !== null) return; this.applyWarSettlement(war); war.ended = this.year; +for (const [id, other] of [[war.aPolityId, war.bPolityId], [war.bPolityId, war.aPolityId]]) { +const polity = this.getPolityById(id); +if (polity) polity.nextWarAllowedWeek = Math.max(polity.nextWarAllowedWeek || 0, this.year + SimConfig.war.cooldownWeeks); +this.addPolityEvent(id, "war", this.year, { outcome: "ended", targetPolityId: other }, 2); +} +for (const campaign of this.campaigns) { +if (campaign.status === "active" && campaign.type === "border_war" && +((campaign.attackerPolityId === war.aPolityId && campaign.defenderPolityId === war.bPolityId) || +(campaign.attackerPolityId === war.bPolityId && campaign.defenderPolityId === war.aPolityId))) campaign.status = "ceased"; +} } totalPolityPopulation(polity) { return this.getPolityCities(polity) @@ -3258,6 +3647,128 @@ if (distance < best) best = distance; } return best; } +polityTreasuryCap(polity) { +if (!polity) return 0; +const perPopulation = Math.max(0, SimConfig.polity?.treasuryCapPerPopulation ?? 0.30); +return Math.max(0, this.totalPolityPopulation(polity) * perPopulation); +} +enforcePolityTreasuryCap(polity) { +if (!polity) return 0; +const cap = this.polityTreasuryCap(polity); +polity.treasury = clamp(polity.treasury || 0, 0, cap); +return cap; +} +enforcePolityTreasuryCaps() { +for (const polity of this.polities) this.enforcePolityTreasuryCap(polity); +} +treasuryCityInvestmentProfile(cost) { +const cfg = SimConfig.polity || {}; +const safeCost = Math.max(0, cost || 0); +const basePopulation = Math.max(1, SimConfig.city.initialPopulation ?? 400); +const populationPerCost = Math.max(0, cfg.treasuryCityPopulationPerCost ?? 0.18); +const reservePerCost = Math.max(0, cfg.treasuryCityReservePerCost ?? 0.45); +const population = Math.max(basePopulation, Math.round(basePopulation + safeCost * populationPerCost)); +const resources = +34 * SimConfig.city.populationScale + +safeCost * reservePerCost + +Math.max(0, population - basePopulation) * 0.22; +return { population, resources }; +} +findTreasuryCityFoundingSite(polity) { +if (!polity) return -1; +const w = this.world; +const cities = this.getPolityCities(polity); +if (!cities.length) return -1; +const spacing = Math.max(1, SimConfig.city.naturalFoundingMinDistance ?? 6); +const radius = Math.max(spacing + 1, SimConfig.polity?.treasuryCityFoundingSearchRadius ?? 20); +const candidates = []; +const visited = new Set(); +for (const source of cities) { +for (const { dx, dy, distance } of this.getRadiusOffsets(radius, false)) { +if (distance < spacing) continue; +const x = source.x + dx; +const y = source.y + dy; +if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; +const tile = w.idx(x, y); +if (visited.has(tile)) continue; +visited.add(tile); +if (w.terrain[tile] === Terrain.WATER || w.city[tile] >= 0) continue; +if (w.territoryOwner[tile] !== polity.id) continue; +if (this.getCitiesNear(x, y, spacing - 1).length) continue; +const route = w.tradeRoute[tile] ? 1.25 : clamp((w.pheromone[tile] || 0) / 12, 0, 0.8); +const score = +(w.fertility[tile] || 0) * 3.2 + +clamp((w.resource[tile] || 0) / 20, 0, 2.5) + +(w.populationPressure[tile] || 0) * 1.6 + +(w.farmland[tile] || 0) * 1.4 + +(w.control[tile] || 0) * 1.2 + +route - +(w.move[tile] || 1) * 0.75; +candidates.push({ tile, score }); +} +} +if (!candidates.length) return -1; +candidates.sort((a, b) => b.score - a.score); +const bestScore = candidates[0].score; +const scoreWindow = Math.max(0.2, SimConfig.polity?.treasuryCitySiteScoreWindow ?? 6.0); +const eligible = candidates.filter(candidate => candidate.score >= bestScore - scoreWindow); +const floor = bestScore - scoreWindow; +let totalWeight = 0; +for (const candidate of eligible) { +candidate.weight = Math.pow(Math.max(0.20, candidate.score - floor + 0.20), 1.10); +totalWeight += candidate.weight; +} +let roll = this.rng.next() * totalWeight; +for (const candidate of eligible) { +roll -= candidate.weight; +if (roll <= 0) return candidate.tile; +} +return eligible[eligible.length - 1]?.tile ?? candidates[0].tile; +} +maybeBuildTreasuryCities() { +const cfg = SimConfig.polity || {}; +const chance = clamp(cfg.treasuryCityFoundingChance ?? 0.14, 0, 1); +const costRatio = clamp(cfg.treasuryCityFoundingCostRatio ?? 0.35, 0, 1); +if (chance <= 0 || costRatio <= 0 || this.cities.length >= SimConfig.city.maxCities) return 0; +let founded = 0; +for (const polity of [...this.polities]) { +if (this.cities.length >= SimConfig.city.maxCities) break; +const cap = this.polityTreasuryCap(polity); +if (cap <= 0 || (polity.treasury || 0) + 1e-6 < cap) continue; +if (this.rng.next() >= chance) continue; +const tile = this.findTreasuryCityFoundingSite(polity); +if (tile < 0) continue; +const cost = Math.min(polity.treasury || 0, cap * costRatio); +if (cost <= 0) continue; +const x = tile % this.world.size; +const y = Math.floor(tile / this.world.size); +const ethnicitySeed = this.manualCityEthnicitySeed(tile); +const investment = this.treasuryCityInvestmentProfile(cost); +const city = this.createCity(x, y, null, { +initialPopulation: investment.population, +initialResources: investment.resources +}); +this.applyCityEthnicitySeed(city, ethnicitySeed, city.population); +city.strength = Math.max(city.strength, 4.5); +city.activeVisitors = Math.max(city.activeVisitors, 6); +this.cities.push(city); +this.cityById.set(city.id, city); +const bucketKey = this.cityBucketKey(city.x, city.y); +if (!this.cityBuckets.has(bucketKey)) this.cityBuckets.set(bucketKey, []); +this.cityBuckets.get(bucketKey).push(city); +this.world.city[tile] = city.id; +this.addCityToPolity(city, polity, 0.72); +this.claimPolityCoreTerritory(polity, city, 3, "treasuryCity"); +polity.treasury = Math.max(0, (polity.treasury || 0) - cost); +founded++; +} +if (founded) { +this.rebuildIndexes(); +this.updatePopulationCapacity(); +this.syncTilePopulationCulture(); +} +return founded; +} polityAge(polity) { return (this.year - (polity?.founded ?? this.year)) / WEEKS_PER_YEAR; } @@ -3267,6 +3778,51 @@ if (age < 80) return 0; const x = (age - 80) / 420; return clamp(1 - Math.exp(-x), 0, 1.35); } +polityInstitutionalAging(polity) { +return clamp(polity?.institutionalAging ?? 0, 0, 1); +} +polityLoyaltyCap(polity) { +return clamp(1 - this.polityInstitutionalAging(polity), 0, 1); +} +updateInstitutionalAging() { +const cfg = SimConfig.polity || {}; +const annualIncrease = Math.max(0, cfg.institutionalAgingAnnualIncrease ?? 0.0015); +for (const polity of this.polities) { +polity.institutionalAging = this.polityInstitutionalAging(polity); +if (this.year <= (polity.founded ?? this.year)) continue; +polity.institutionalAging = clamp(polity.institutionalAging + annualIncrease, 0, 1); +} +} +maybeTriggerInstitutionalReforms() { +const cfg = SimConfig.polity || {}; +const threshold = clamp(cfg.institutionalReformThreshold ?? 0.30, 0, 1); +const cooldown = years(Math.max(0, cfg.institutionalReformCooldownYears ?? 50)); +for (const polity of this.polities) { +const before = this.polityInstitutionalAging(polity); +if (before <= threshold) continue; +if (this.year - (polity.lastInstitutionalReformYear ?? polity.founded ?? 0) < cooldown) continue; +const chance = clamp( +(cfg.institutionalReformBaseChance ?? 0.008) + before * (cfg.institutionalReformChanceScale ?? 0.10), +0, +cfg.institutionalReformMaxChance ?? 0.12 +); +if (this.rng.next() >= chance) continue; +const changeRate = this.rng.range( +cfg.institutionalReformMinChange ?? -0.50, +cfg.institutionalReformMaxChange ?? 0.30 +); +const after = clamp(before * (1 + changeRate), 0, 1); +polity.institutionalAging = after; +polity.lastInstitutionalReformYear = this.year; +this.addPolityEvent(polity.id, "institutionalReform", this.year, { +before, +after, +changeRate, +loyaltyCapBefore: clamp(1 - before, 0, 1), +loyaltyCapAfter: clamp(1 - after, 0, 1) +}, 3); +} +} polityOverextension(polity) { const cities = this.getPolityCities(polity); const center = this.getCityById(polity.centerCityId); @@ -3365,60 +3921,6 @@ this.createPolity(city); formed++; } } -expandPolities() { -return; -for (const polity of this.polities) { -const center = this.getCityById(polity.centerCityId); -if (!center) continue; -const centerInfluence = this.cityInfluence(center); -let absorbed = 0; -for (const city of this.cities) { -if (absorbed >= 2) break; -if (city.polityId !== null || city.id === center.id) continue; -const connection = this.polityConnectionToCity(polity, city); -if (!connection.connected) continue; -const distance = connection.distance; -const targetInfluence = this.cityInfluence(city); -if (centerInfluence <= targetInfluence * 0.9) continue; -const dominanceScore = -(centerInfluence / (targetInfluence + 1)) * -(1 / (1 + distance * 0.08)) * -(this.hasDirectTradeConnection(connection.source, city) ? 1.5 : 1.0); -if (dominanceScore > 0.62 && this.rng.next() < 0.24) { -this.addCityToPolity(city, polity, 0.62); -absorbed++; -} -} -} -} -absorbIndependentCities() { -return; -if (this.year % years(5) !== 0) return; -for (const polity of this.polities) { -const cities = this.getPolityCities(polity); -if (!cities.length) continue; -const power = this.polityPower(polity); -const range = this.polityInfluenceRange(polity, false); -let absorbed = 0; -const candidates = this.cities -.filter(city => city.polityId === null && city.population > 0) -.map(city => ({ city, connection: this.polityConnectionToCity(polity, city) })) -.filter(x => x.connection.connected && x.connection.distance <= range) -.sort((a, b) => a.connection.distance - b.connection.distance); -for (const { city, connection } of candidates) { -if (absorbed >= 1) break; -const distance = connection.distance; -const cityInfluence = this.cityInfluence ? this.cityInfluence(city) : Math.sqrt(city.population || 1); -const proximity = 1 / (1 + distance * 0.07); -const pressure = (power / Math.max(1, cityInfluence)) * proximity; -if (pressure > 0.75 && this.rng.next() < clamp(pressure * 0.055, 0.01, 0.22)) { -this.addCityToPolity(city, polity, 0.42); -city.population = Math.max(1, Math.floor(city.population * this.rng.range(0.95, 0.99))); -absorbed++; -} -} -} -} // Phase 1B: campaigns are discrete targeted events. Influence/control can show // continuous territory, but new holdings come from these active campaign objects. maybeStartCampaigns() { @@ -3500,7 +4002,7 @@ const w = this.world; if (w.polity[tile] >= 0 || w.city[tile] >= 0 || w.terrain[tile] === Terrain.WATER) return null; const expansion = this.isValidExpansionTarget(polity, tile, { maxNeutralPath: 7, requireUnowned: true }); if (!expansion) return null; -const population = w.population?.[tile] || w.pressure[tile] || 0; +const population = w.population?.[tile] || 0; const ethnicity = w.dominantEthnicity[tile]; if (population < 2 && ethnicity < 0) return null; const diversity = w.cultureDiversity[tile] || 0; @@ -3697,19 +4199,24 @@ return best; } startCampaign(polity, target) { const cfg = SimConfig.campaign || {}; +if (target.type === "border_war") { +const defender = this.getPolityById(target.defenderPolityId); +if (!defender || (!this.getWarBetween(polity.id, defender.id) && !this.startWar(polity, defender))) return null; +} const duration = years(Math.min(cfg.maxDurationYears ?? 25, (cfg.baseDurationYears ?? 8) + this.rng.int(Math.max(1, (cfg.maxDurationYears ?? 25) - (cfg.baseDurationYears ?? 8) + 1)))); const targetTile = target.targetTile ?? (target.targetCityId ? this.cityTile(this.getCityById(target.targetCityId)) : null); if (targetTile == null) return null; const campaign = { id: this.nextCampaign++, type: target.type, +annexationMode: target.type === "independent_city_annexation" ? this.independentAnnexationMode() : null, attackerPolityId: polity.id, defenderPolityId: target.defenderPolityId ?? null, targetCityId: target.targetCityId ?? null, targetTile, targetEthnicity: target.targetEthnicity ?? null, started: this.year, -expires: this.year + duration, +expires: target.type === "border_war" ? Infinity : this.year + duration, progress: 0, strength: 0, resistance: 0, @@ -3729,13 +4236,18 @@ this.addPolityEvent(polity.id, "expansion", this.year, this.campaignEventData(ca this.addPolityEvent(polity.id, "campaign", this.year, this.campaignEventData(campaign, "started"), 1); return campaign; } +independentAnnexationMode() { +const peacefulShare = SimConfig.campaign?.independentCityPeacefulShare ?? 0.4; +return this.rng.next() < peacefulShare ? "peaceful" : "military"; +} cityTile(city) { return city ? this.world.idx(city.x, city.y) : null; } updateCampaigns() { -if (this.year % years(1) !== 0) return; for (const campaign of this.campaigns) { if (campaign.status !== "active") continue; +const borderWar = campaign.type === "border_war"; +if (this.year % (borderWar ? SimConfig.war.tickWeeks : years(1)) !== 0) continue; const attacker = this.getPolityById(campaign.attackerPolityId); if (!attacker) { campaign.status = "failed"; @@ -3746,7 +4258,7 @@ const resistance = this.campaignResistance(campaign, attacker); campaign.strength = force; campaign.resistance = resistance; const momentum = (force - resistance) / Math.max(1, force + resistance); -campaign.progress = clamp(campaign.progress + 0.09 + momentum * 0.18 + this.rng.range(-0.08, 0.08), -0.35, 1.25); +campaign.progress = clamp(campaign.progress + (borderWar ? 0.25 + momentum * 0.4 : 0.09 + momentum * 0.18) + this.rng.range(-0.08, 0.08), -0.35, 1.25); attacker.treasury = Math.max(0, (attacker.treasury || 0) - Math.max(0.05, force * 0.018)); if (campaign.progress >= 1) this.resolveCampaignSuccess(campaign); else if (campaign.progress <= -0.28) this.resolveCampaignFailure(campaign); @@ -3778,7 +4290,7 @@ const targetCity = this.getCityById(campaign.targetCityId); const defender = campaign.defenderPolityId != null ? this.getPolityById(campaign.defenderPolityId) : null; const connection = this.nearestPolityTileConnection(attacker, tile); const terrain = w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.45 : 1); -const population = targetCity?.population || w.population[tile] || w.pressure[tile] || 0; +const population = targetCity?.population || w.population[tile] || 0; const defenderPower = defender ? this.polityPower(defender) * 0.55 : 0; const attackerEthnicity = this.dominantCityEthnicity(connection.source); const localEthnicity = targetCity ? this.dominantCityEthnicity(targetCity) : w.dominantEthnicity[tile]; @@ -3801,18 +4313,25 @@ const cityTile = city ? this.world.idx(city.x, city.y) : null; const cityTileOwner = cityTile != null ? this.world.territoryOwner[cityTile] : -1; if (city && city.polityId === null && (cityTileOwner < 0 || cityTileOwner === attacker.id)) { const similarity = this.campaignEthnicSimilarity(attacker, campaign); +const peaceful = campaign.annexationMode === "peaceful"; +const resistanceRatio = campaign.resistance / Math.max(1, campaign.strength + campaign.resistance); +if (!peaceful) { +this.applyCityWarDamage(city, clamp(0.025 + resistanceRatio * 0.06, 0.025, 0.10), "independentAnnexation"); +} this.transferCityToPolity(city, attacker, { -loyalty: clamp(0.28 + similarity * 0.34 - campaign.resistance / Math.max(1, campaign.strength + campaign.resistance) * 0.14, 0.16, 0.68), -control: 0.48, -claim: 0.62, -reason: "annexation" +loyalty: peaceful +? clamp(0.58 + similarity * 0.22, 0.58, 0.82) +: clamp(0.20 + similarity * 0.22 - resistanceRatio * 0.12, 0.12, 0.48), +control: peaceful ? 0.58 : 0.46, +claim: peaceful ? 0.68 : 0.62, +reason: peaceful ? "peacefulAnnexation" : "militaryAnnexation" }); -campaign.affectedTiles = this.applyCampaignTerritory(attacker, cityTile, 5, 0.42, { +campaign.affectedTiles = this.applyCampaignTerritory(attacker, cityTile, 5, peaceful ? 0.48 : 0.42, { allowForeignTakeover: false, -reason: "annexation" +reason: peaceful ? "peacefulAnnexation" : "militaryAnnexation" }) || []; } -this.addPolityEvent(attacker.id, "annexation", this.year, this.campaignEventData(campaign, "succeeded"), 2); +this.addPolityEvent(attacker.id, "annexation", this.year, this.campaignEventData(campaign, campaign.annexationMode || "military"), 2); } else if (campaign.type === "border_war") { const defender = this.getPolityById(campaign.defenderPolityId); const city = this.getCityById(campaign.targetCityId); @@ -3864,12 +4383,8 @@ this.addPolityEvent(attacker.id, "colonization", this.year, this.campaignEventDa this.recordCampaign(attacker, campaign, campaign.status); this.syncTilePopulationCulture(); this.recomputeTerritories({ allowOwnershipChanges: false, reason: "campaign" }); -this.applyStoredTerritorialClaims(); -this.enforceCityTerritoryAnchors({ allowOwnershipChanges: false }); -this.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); this.reconcileCityTerritoryOwnership(); this.campaignPressureOnTerritories(); -this.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); } resolveCampaignFailure(campaign, reason = "failed") { campaign.status = reason; @@ -3890,8 +4405,11 @@ const defender = campaign.defenderPolityId != null ? this.getPolityById(campaign if (defender) this.addWarScore(war, defender.id, 0.45 + clamp(campaign.resistance / Math.max(1, campaign.strength), 0, 1.2)); } this.addPolityEvent(attacker.id, "failedCampaign", this.year, this.campaignEventData(campaign, reason), 1); +const peacefulAnnexation = campaign.type === "independent_city_annexation" && campaign.annexationMode === "peaceful"; +if (!peacefulAnnexation) { this.applyPolityWarExhaustionPopulationLoss(attacker, 0.45, clamp(campaign.resistance / Math.max(1, campaign.strength), 0.4, 1.4)); this.applyWarZoneTileDamage(campaign.targetTile, SimConfig.war.warZoneTileLossRate ?? 0.006); +} this.recordCampaign(attacker, campaign, campaign.status); } campaignEthnicSimilarity(polity, campaign) { @@ -3911,7 +4429,12 @@ if (!toPolity && !clearing) return changed; for (const tile of tiles) { if (tile == null || tile < 0 || tile >= w.count || w.terrain[tile] === Terrain.WATER) continue; const owner = w.territoryOwner[tile]; -if (fromPolityId != null && owner !== fromPolityId) continue; +if ( +fromPolityId != null && +owner !== fromPolityId && +owner !== toPolityId && +!(options.allowNeutralWithFrom && owner < 0) +) continue; if (!clearing && owner >= 0 && owner !== toPolityId && !options.allowForeignTakeover) continue; if (owner === toPolityId || (clearing && owner < 0)) continue; this.setTileOwner(tile, toPolityId, { @@ -3950,7 +4473,9 @@ claimPolityCoreTerritory(polity, city, radius = 4, reason = "founding") { if (!polity || !city) return []; const w = this.world; const tiles = []; -for (const { dx, dy, distance } of this.getRadiusOffsets(radius)) { +for (const { dx, dy } of this.getRadiusOffsets(Math.ceil(radius * Math.SQRT2))) { +const distance = Math.hypot(dx, dy); +if (distance > radius) continue; const x = city.x + dx; const y = city.y + dy; if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; @@ -3969,6 +4494,16 @@ claim: 0.58, reason }); } +isConquestTransferReason(reason) { +return reason === "warCapture" || reason === "borderWar" || reason === "militaryAnnexation" || reason === "settlement" || reason === "nomadConquest"; +} +protectCityFromRebellion(city, graceYears = SimConfig.rebellion?.conquestGraceYears ?? 20) { +if (!city) return; +city.rebellionProtectedUntil = Math.max(city.rebellionProtectedUntil || 0, this.year + years(Math.max(0, graceYears))); +} +isCityRebellionProtected(city) { +return !!city && (city.rebellionProtectedUntil || 0) > this.year; +} transferCityToPolity(city, toPolity, options = {}) { if (!city || !toPolity) return false; const fromPolityId = city.polityId; @@ -3981,6 +4516,7 @@ control: options.control ?? 0.58, claim: options.claim ?? 0.70, reason: options.reason ?? "cityTransfer" }); +if (this.isConquestTransferReason(options.reason)) this.protectCityFromRebellion(city, options.rebellionProtectionYears); return true; } releaseCityTerritory(city, fromPolityId, radius = 4, reason = "fragmentation") { @@ -4085,7 +4621,7 @@ return false; } const visited = this.visitStamp; const marker = this.nextVisitMarker(); -const queue = seeds.map(tile => ({ tile, distance: Math.abs((tile % w.size) - cx) + Math.abs(Math.floor(tile / w.size) - cy) })); +const queue = seeds.map(tile => ({ tile, distance: Math.hypot((tile % w.size) - cx, Math.floor(tile / w.size) - cy) })); for (const seed of seeds) visited[seed] = marker; const candidates = []; const maxTiles = options.maxTiles ?? Infinity; @@ -4099,7 +4635,7 @@ const owner = w.territoryOwner[tile]; const canTake = owner === polity.id || (allowForeignTakeover -? (fromPolityId == null ? owner < 0 || owner !== polity.id : owner === fromPolityId) +? (fromPolityId == null ? owner < 0 || owner !== polity.id : owner < 0 || owner === fromPolityId) : owner < 0); if (!canTake) { if (owner >= 0 && owner !== polity.id) w.contested[tile] = 1; @@ -4110,7 +4646,7 @@ candidates.push({ tile, distance, local }); if (candidates.length >= maxTiles) break; this.forCardinalNeighbors(tile, (n, tx, ty) => { if (visited[n] === marker) return; -const nd = Math.abs(tx - cx) + Math.abs(ty - cy); +const nd = Math.hypot(tx - cx, ty - cy); if (nd > radius || w.terrain[n] === Terrain.WATER) return; const neighborOwner = w.territoryOwner[n]; if (neighborOwner >= 0 && neighborOwner !== polity.id && !allowForeignTakeover) { @@ -4131,6 +4667,7 @@ polity.id, ordered.map(entry => entry.tile), { allowForeignTakeover, +allowNeutralWithFrom: allowForeignTakeover && fromPolityId != null, control: Math.max(0.18, strength * 0.65), claim: Math.max(0.22, strength), contested: false, @@ -4209,6 +4746,7 @@ campaignEventData(campaign, outcome) { return { campaignId: campaign.id, campaignType: campaign.type, +annexationMode: campaign.annexationMode ?? null, outcome, targetCityId: campaign.targetCityId ?? null, targetPolityId: campaign.defenderPolityId ?? null, @@ -4249,20 +4787,21 @@ const regionalEthnicity = this.world.dominantEthnicity[targetTile]; const sourceEthnicity = this.dominantCityEthnicity(source) || (regionalEthnicity >= 0 ? regionalEthnicity : 1); composition.set(sourceEthnicity, isFrontier ? 12 : 18); } -const seedPopulation = Math.max(12, Math.round(compositionTotal(composition) * (isFrontier ? 1.2 : 1.6))); +const populationScale = SimConfig.city.populationScale; +const seedPopulation = Math.max(12, Math.round(compositionTotal(composition) * (isFrontier ? 1.2 : 1.6))) * populationScale; +for (const [id, count] of composition) composition.set(id, count * populationScale); const city = { id: this.nextCity++, x: target.x ?? targetTile % w.size, y: target.y ?? Math.floor(targetTile / w.size), population: seedPopulation, -storedResources: 18 + (w.resource[targetTile] || 0) * 0.25, +storedResources: (18 + (w.resource[targetTile] || 0) * 0.25) * populationScale, ethnicityComposition: composition, pheromoneOutput: 0, agriculturalRadius: 2, tradeLinks: new Set(), activeVisitors: 0, age: 0, -ageWear: 0, peakPopulation: seedPopulation, strength: isFrontier ? 1.8 : 2.4, sedentaryCulture: isFrontier ? 0.42 : 0.55, @@ -4272,7 +4811,8 @@ polityId: null, loyalty: isFrontier ? 0.52 : 0.32, receivedAid: false, tradeValue: 0, -tradeReach: 0 +tradeReach: 0, +rebellionProtectedUntil: 0 }; this.cities.push(city); this.cityById.set(city.id, city); @@ -4293,8 +4833,13 @@ this.syncTilePopulationCulture(); return city; } maybeStartWars() { -if (this.year % years(1) !== 0) return; +if (this.year % SimConfig.war.tickWeeks !== 0) return; +let actions = 0; +let warSlots = SimConfig.war.maxActiveWars - this.wars.filter(war => war.ended === null).length; +const maxActions = SimConfig.war.maxStartsPerTick; for (const a of this.polities) { +if (actions >= maxActions) break; +if (this.year < (a.nextWarAllowedWeek ?? 0)) continue; if (this.isPolityAtWar(a.id)) continue; const aPower = this.polityPower(a); if (aPower <= 0) continue; @@ -4302,6 +4847,7 @@ let started = false; for (const b of this.polities) { if (started) break; if (a.id === b.id) continue; +if (this.year < (b.nextWarAllowedWeek ?? 0)) continue; if (!this.canBeWarTarget(b.id)) continue; if (this.getWarBetween(a.id, b.id)) continue; const distance = this.polityDistance(a, b); @@ -4352,9 +4898,12 @@ generalInstability + lowLoyaltyPressure * 0.018 + weakSideLowLoyalty * 0.028 + opportunisticPressure; -if (this.rng.next() < clamp(chance, 0, 0.12)) { -this.startWar(a, b); -started = true; +if (this.rng.next() < clamp(chance * SimConfig.war.startChanceMultiplier, 0, 0.35)) { +if (warSlots > 0) { +started = !!this.startWar(a, b); +if (started) warSlots--; +} +if (started) actions++; } } } @@ -4368,7 +4917,8 @@ const sizePressure = clamp((aCities + bCities - 3) / 8, 0, 1); return proximity * (0.35 + sizePressure * 0.65); } updateWars() { -if (this.year % years(2) !== 0) return; +if (this.year % SimConfig.war.tickWeeks !== 0) return; +this.warLogisticsCache = new Map(); for (const war of this.wars) { if (war.ended !== null) continue; const a = this.getPolityById(war.aPolityId); @@ -4395,19 +4945,20 @@ defender, selected.city, selected.connection.distance, attackerPower, -defenderPower +defenderPower, +selected.connection.logisticsFactor ); const key = `${attacker.id}:${selected.city.id}`; -const memory = (war.cityPressureById?.get(key) || 0) * (SimConfig.war.pressureMemoryDecay ?? 0.88) + pressure * (war.intensity || 0.75); +const memory = (war.cityPressureById?.get(key) || 0) * (SimConfig.war.pressureMemoryDecay ?? 0.88) + pressure * (war.intensity || 0.75) * 1.6; war.cityPressureById?.set(key, memory); const captureThreshold = war.goal === "collapse_exploitation" ? 1.55 : war.goal === "city_conquest" ? 1.75 : war.goal === "capital_pressure" ? 1.85 : 2.15; -const chance = clamp((memory - captureThreshold + pressure * 0.35) * 0.16, 0.01, 0.58); +const chance = clamp((memory - captureThreshold * 0.65 + pressure * 0.35) * 0.45, 0.05, 0.85); const damageRate = clamp( -(SimConfig.war.cityDamageMinRate ?? 0.003) + pressure * 0.010 + memory * 0.006, +(SimConfig.war.cityDamageMinRate ?? 0.003) + pressure * 0.030 + memory * 0.018, SimConfig.war.cityDamageMinRate ?? 0.003, SimConfig.war.cityDamageMaxRate ?? 0.08 ); -if (memory > captureThreshold && pressure > 0.72 && this.rng.next() < chance) { +if (memory > captureThreshold * 0.65 && pressure > 0.55 && this.rng.next() < chance) { this.captureCityInWar(selected.city, attacker, defender, war, pressure); } else { this.applyCityWarDamage(selected.city, damageRate, "warPressure"); @@ -4418,7 +4969,7 @@ const lastAdvance = war.lastFrontAdvanceYear instanceof Map ? (war.lastFrontAdva const canAdvanceFront = memory > 1.05 && pressure > 0.58 && -this.year - lastAdvance > years(4); +this.year - lastAdvance >= SimConfig.war.tickWeeks; if (canAdvanceFront && this.rng.next() < clamp((memory - 0.9) * 0.10 + (pressure - 0.55) * 0.22, 0.02, 0.34)) { const border = this.nearestBorderCampaignTile(attacker, defender); if (border?.tile != null && Number.isFinite(border.distance)) { @@ -4463,6 +5014,7 @@ const routeAccess = this.hasDirectTradeConnection(connection.source, city) || th const cityValue = Math.sqrt(city.population || 1) * 0.10 + (city.tradeValue || 0) * 0.55 + (capital ? 0.9 : 0); const powerAdvantage = clamp(attackerPower / Math.max(1, defenderPower) - 1, -0.4, 2.4); const deepPenalty = Math.max(0, connection.distance - SimConfig.polity.logisticsDistance) * (war.goal === "capital_pressure" ? 0.010 : 0.018); +const logisticsPenalty = (1 - connection.logisticsFactor) * 2.2; const weakState = defenderInstability * 0.55 + (1 - defenderCohesion) * 0.35 + (1 - defenderLegitimacy) * 0.28; const goalBonus = war.goal === "punitive_raid" ? (city.storedResources || 0) * 0.004 : @@ -4470,7 +5022,7 @@ war.goal === "border_claim" ? Math.max(0, 18 - connection.distance) * 0.035 : war.goal === "collapse_exploitation" ? weakState * 0.9 + lowLoyalty * 0.45 : war.goal === "capital_pressure" && capital ? 1.15 : 0.35; -const score = cityValue + lowLoyalty * 0.85 + routeAccess + memory * 0.45 + powerAdvantage * 0.45 + weakState + goalBonus - connection.distance * 0.018 - deepPenalty + (probe ? 0 : this.rng.range(-0.08, 0.10)); +const score = cityValue + lowLoyalty * 0.85 + routeAccess + memory * 0.45 + powerAdvantage * 0.45 + weakState + goalBonus - connection.distance * 0.018 - deepPenalty - logisticsPenalty + (probe ? 0 : this.rng.range(-0.08, 0.10)); if (score > bestScore) { bestScore = score; best = { city, connection, score }; @@ -4490,18 +5042,71 @@ return clamp(base + clamp((ratio - 1) * 12, 0, 18) + routeReach + cohesion + wea warConnectionToCity(polity, city) { let source = null; let distance = Infinity; +let logisticsFactor = 0; +let campaignCost = Infinity; for (const c of this.getPolityCities(polity)) { if (c.id === city.id) continue; let d = this.effectiveDistance(c, city); if (this.hasDirectTradeConnection(c, city)) d *= 0.72; -if (d < distance) { +const logistics = this.warLogisticsFactor(polity, c, city); +const cost = d / Math.max(0.28, logistics); +if (cost < campaignCost) { source = c; distance = d; +logisticsFactor = logistics; +campaignCost = cost; } } -return { source, distance, connected: !!source }; +return { source, distance, connected: !!source, logisticsFactor, campaignCost }; } -warAbsorptionPressure(attacker, defender, city, distance, attackerPower, defenderPower) { +warLogisticsFactor(polity, sourceCity, targetCity) { +if (!polity || !sourceCity || !targetCity) return 0.25; +const capital = this.getCityById(polity.centerCityId) || sourceCity; +const capitalDistance = this.effectiveDistance(capital, targetCity); +const distanceStart = SimConfig.polity.warCapitalDistanceStart ?? 18; +const distanceScale = SimConfig.polity.warCapitalDistanceScale ?? 42; +let factor = 1 / (1 + Math.max(0, capitalDistance - distanceStart) / Math.max(1, distanceScale)); +const connected = sourceCity.id === capital.id || this.territoryRouteConnected(polity.id, capital, sourceCity); +if (!connected) factor *= SimConfig.polity.warDisconnectedTerritoryFactor ?? 0.42; +if (sourceCity.id !== capital.id && this.hasDirectTradeConnection(capital, sourceCity)) factor *= 1.08; +return clamp(factor, 0.18, 1); +} +territoryRouteConnected(polityId, fromCity, toCity) { +if (!fromCity || !toCity) return false; +if (fromCity.id === toCity.id) return true; +const key = `${polityId}:${fromCity.id}:${toCity.id}`; +if (this.warLogisticsCache?.has(key)) return this.warLogisticsCache.get(key); +const w = this.world; +const start = w.idx(fromCity.x, fromCity.y); +const goal = w.idx(toCity.x, toCity.y); +if (w.territoryOwner[start] !== polityId || w.territoryOwner[goal] !== polityId) { +this.warLogisticsCache?.set(key, false); +return false; +} +const visited = this.visitStamp; +const marker = this.nextVisitMarker(); +const queue = this.scratchIntA; +let head = 0; +let tail = 0; +queue[tail++] = start; +visited[start] = marker; +let connected = false; +while (head < tail) { +const tile = queue[head++]; +if (tile === goal) { +connected = true; +break; +} +this.forCardinalNeighbors(tile, neighbor => { +if (visited[neighbor] === marker || w.territoryOwner[neighbor] !== polityId || w.terrain[neighbor] === Terrain.WATER) return; +visited[neighbor] = marker; +queue[tail++] = neighbor; +}); +} +this.warLogisticsCache?.set(key, connected); +return connected; +} +warAbsorptionPressure(attacker, defender, city, distance, attackerPower, defenderPower, logisticsFactor = 1) { const powerRatio = attackerPower / Math.max(1, defenderPower); const proximity = 1 / (1 + distance * 0.055); const defenderInstability = this.polityInstability ? this.polityInstability(defender) : 0; @@ -4522,6 +5127,7 @@ proximity * techAdvantage * ethnicityFactor * tradeFactor * +clamp(logisticsFactor, 0.18, 1) * (1 + defenderInstability * 0.32) * (1 + defenderAge * 0.18) * (1 + defenderOverextension * 0.14) * @@ -4576,8 +5182,8 @@ a.treasury = Math.max(0, (a.treasury || 0) - costA); b.treasury = Math.max(0, (b.treasury || 0) - costB); a.crisis = clamp((a.crisis || 0) + 0.004 * intensity, 0, 1.5); b.crisis = clamp((b.crisis || 0) + 0.004 * intensity, 0, 1.5); -war.exhaustionA = clamp((war.exhaustionA || 0) + 0.006 * intensity, 0, 1); -war.exhaustionB = clamp((war.exhaustionB || 0) + 0.006 * intensity, 0, 1); +war.exhaustionA = clamp((war.exhaustionA || 0) + 0.08 * intensity, 0, 1); +war.exhaustionB = clamp((war.exhaustionB || 0) + 0.08 * intensity, 0, 1); this.applyPolityWarExhaustionPopulationLoss(a, war.exhaustionA || 0, intensity); this.applyPolityWarExhaustionPopulationLoss(b, war.exhaustionB || 0, intensity); } @@ -4591,15 +5197,15 @@ return; const age = this.year - war.started; const noActionFor = this.year - war.lastActionYear; const exhaustion = Math.max(war.exhaustionA || 0, war.exhaustionB || 0); -if (age > years(90)) { +if (!this.getPolityCities(a).length || !this.getPolityCities(b).length) { this.endWar(war); return; } -if (age > years(10) && noActionFor > years(18)) { +if (age >= SimConfig.war.minDurationWeeks && noActionFor >= SimConfig.war.idleEndWeeks) { this.endWar(war); return; } -if (exhaustion > 0.85 && this.rng.next() < 0.35) { +if (age >= SimConfig.war.minDurationWeeks && (exhaustion >= 0.8 || Math.abs((war.scoreA || 0) - (war.scoreB || 0)) >= SimConfig.war.decisiveScore)) { this.endWar(war); } } @@ -4610,16 +5216,20 @@ else if (war.bPolityId === polityId) war.scoreB = (war.scoreB || 0) + amount; } applyCityWarDamage(city, rate, reason = "war") { if (!city || city.population <= 1 || rate <= 0) return 0; -const loss = Math.min(city.population - 1, Math.max(1, Math.floor(city.population * clamp(rate, 0, SimConfig.war.cityDamageMaxRate ?? 0.08)))); +const damageMultiplier = SimConfig.war.damageMultiplier ?? 1; +const maxRate = reason === "capture" ? SimConfig.war.captureLossMax : SimConfig.war.cityDamageMaxRate; +const appliedRate = clamp(rate, 0, maxRate) * damageMultiplier; +const loss = Math.min(city.population - 1, Math.max(1, Math.floor(city.population * appliedRate))); if (loss <= 0) return 0; city.population = Math.max(1, city.population - loss); if (city.ethnicityComposition) removeFromComposition(city.ethnicityComposition, loss); city.storedResources = Math.max(0, (city.storedResources || 0) - loss * (reason === "capture" ? 0.38 : 0.16)); -city.loyalty = clamp((city.loyalty ?? 0.5) - rate * (reason === "capture" ? 1.8 : 0.9), 0, 1); +city.loyalty = clamp((city.loyalty ?? 0.5) - appliedRate * (reason === "capture" ? 1.8 : 0.9), 0, 1); city.lastWarDamageYear = this.year; this.recordWarDeaths(loss, reason === "capture" ? 0.45 : 0.65); return loss; } + applyPolityWarExhaustionPopulationLoss(polity, exhaustion, intensity) { if (!polity || exhaustion <= 0.04) return 0; const cities = this.getPolityCities(polity); @@ -4649,6 +5259,7 @@ return total; applyWarZoneTileDamage(tile, rate) { const w = this.world; if (tile == null || tile < 0 || tile >= w.count || rate <= 0) return 0; +const damageMultiplier = SimConfig.war.damageMultiplier ?? 1; let removed = 0; for (const { dx, dy, distance } of this.getRadiusOffsets(2)) { const x = tile % w.size + dx; @@ -4656,7 +5267,7 @@ const y = Math.floor(tile / w.size) + dy; if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue; const t = w.idx(x, y); if (w.terrain[t] === Terrain.WATER || w.population[t] <= 0) continue; -const localRate = rate * (1 - distance / 4); +const localRate = rate * damageMultiplier * (1 - distance / 4); const amount = w.population[t] * localRate; if (amount <= 0.03) continue; removed += this.removeEthnicPopulationProportionally(t, amount) || 0; @@ -4665,6 +5276,7 @@ w.resource[t] = Math.max(0, w.resource[t] - amount * 0.02); this.recordWarDeaths(removed, 0.8); return removed; } + recordWarDeaths(amount, civilianShare = 0.5) { if (amount <= 0) return; this.deaths += Math.max(0, Math.floor(amount * clamp(civilianShare, 0, 1))); @@ -4762,6 +5374,7 @@ city.receivedAid = true; city.loyalty = clamp(city.loyalty + 0.05, 0, 1); } } +this.enforcePolityTreasuryCap(polity); } } erodePolityLegitimacy() { @@ -4836,7 +5449,8 @@ updateCityLoyalty() { for (const polity of this.polities) { const center = this.getCityById(polity.centerCityId); if (!center) continue; -center.loyalty = 1; +const loyaltyCap = this.polityLoyaltyCap(polity); +center.loyalty = loyaltyCap; const instability = this.polityInstability(polity); const agePressure = this.polityAgePressure(polity); const overextension = this.polityOverextension(polity); @@ -4862,46 +5476,93 @@ delta -= overextension * clamp(distance / 48, 0, 1) * 0.014; delta -= logisticsStress * clamp(distance / 42, 0.25, 1) * 0.018; if ((polity.legitimacy ?? 0.7) < 0.25) delta -= 0.020; if ((polity.crisis || 0) > 0.6) delta -= 0.014; -city.loyalty = clamp(city.loyalty + delta, 0, 1); +city.loyalty = clamp(city.loyalty + delta, 0, loyaltyCap); } } } splitUnloyalCities() { -for (const polity of this.polities) { +const cfg = SimConfig.rebellion || {}; +const participationLoyalty = cfg.participationLoyalty ?? 0.50; +const automaticLoyalty = cfg.automaticLoyalty ?? 0.30; +for (const polity of [...this.polities]) { +const originalCities = this.getPolityCities(polity); +const preRebellionPopulation = originalCities.reduce((sum, city) => sum + Math.max(0, city.population || 0), 0); +const members = originalCities +.filter(city => city.id !== polity.centerCityId && city.polityId === polity.id) +.filter(city => !this.isCityRebellionProtected(city)) +.filter(city => (city.loyalty ?? 0.5) < participationLoyalty); +if (!members.length) continue; +const lowestLoyalty = Math.min(...members.map(city => city.loyalty ?? 0.5)); +const automatic = lowestLoyalty < automaticLoyalty; +if (!automatic) { const instability = this.polityInstability(polity); const agePressure = this.polityAgePressure(polity); -const threshold = clamp(0.14 + instability * 0.05 + agePressure * 0.03, 0.14, 0.28); -const rebellions = []; -for (const city of this.getPolityCities(polity)) { -if (city.id === polity.centerCityId || city.loyalty >= threshold) continue; -const chance = -(threshold - city.loyalty) * 0.28 + -instability * 0.012 + -agePressure * 0.010; -if (this.rng.next() < chance) { -const rebellionLoyalty = city.loyalty ?? 0; -const oldPolityId = city.polityId; +const averageLoyalty = members.reduce((sum, city) => sum + (city.loyalty ?? 0.5), 0) / members.length; +const chance = clamp( +(participationLoyalty - averageLoyalty) * (cfg.chanceScale ?? 0.20) + +instability * 0.018 + +agePressure * 0.012, +0, +cfg.maxAnnualChance ?? 0.16 +); +if (this.rng.next() >= chance) continue; +} +// Once a rebellion begins, every non-capital city below 50% loyalty joins +// simultaneously, regardless of distance from the first rebel city. +const trigger = members.reduce((lowest, city) => +(city.loyalty ?? 0.5) < (lowest.loyalty ?? 0.5) ? city : lowest, members[0]); +const rebellionData = members.map(city => ({ cityId: city.id, population: city.population || 0, loyalty: city.loyalty ?? 0 })); +const oldPolityId = polity.id; +this.removeCityFromPolity(trigger); +this.releaseCityTerritory(trigger, oldPolityId, 5, "rebellion"); +const rebelPolity = this.createPolity(trigger); +trigger.loyalty = 1; +for (const city of members) { +if (city.id === trigger.id) continue; +const loyalty = city.loyalty ?? 0.3; this.removeCityFromPolity(city); this.releaseCityTerritory(city, oldPolityId, 5, "rebellion"); -rebellions.push({ -cityId: city.id, -population: city.population || 0, -loyalty: rebellionLoyalty +this.transferCityToPolity(city, rebelPolity, { +loyalty: clamp(0.52 + (participationLoyalty - loyalty) * 0.38, 0.46, 0.72), +control: 0.48, +claim: 0.62, +reason: "rebellion" }); +this.claimPolityCoreTerritory(rebelPolity, city, 3, "rebellion"); } -} -if (rebellions.length >= 3) { -const population = rebellions.reduce((sum, rebellion) => sum + (rebellion.population || 0), 0); -const avgLoyalty = rebellions.reduce((sum, rebellion) => sum + (rebellion.loyalty || 0), 0) / rebellions.length; -this.addPolityEvent(polity.id, "rebellion", this.year, { -cityIds: rebellions.map(rebellion => rebellion.cityId), -cityCount: rebellions.length, +const population = rebellionData.reduce((sum, rebellion) => sum + rebellion.population, 0); +const avgLoyalty = rebellionData.reduce((sum, rebellion) => sum + rebellion.loyalty, 0) / rebellionData.length; +const rebelPopulationShare = preRebellionPopulation > 0 ? clamp(population / preRebellionPopulation, 0, 1) : 0; +const transferRate = clamp(cfg.treasuryTransferRate ?? 0.65, 0, 1); +const treasuryTransferred = Math.min( +Math.max(0, polity.treasury || 0), +Math.max(0, polity.treasury || 0) * rebelPopulationShare * transferRate +); +polity.treasury = Math.max(0, (polity.treasury || 0) - treasuryTransferred); +rebelPolity.treasury = Math.max(0, (rebelPolity.treasury || 0) + treasuryTransferred); +this.enforcePolityTreasuryCap(polity); +this.enforcePolityTreasuryCap(rebelPolity); +const eventData = { +cityIds: rebellionData.map(rebellion => rebellion.cityId), +cityCount: rebellionData.length, population, -loyalty: avgLoyalty +loyalty: avgLoyalty, +automatic, +treasuryTransferred, +newPolityId: rebelPolity.id +}; +this.addPolityEvent(polity.id, "rebellion", this.year, eventData, 3); +this.addPolityEvent(rebelPolity.id, "rebellion", this.year, { +cityIds: eventData.cityIds, +cityCount: eventData.cityCount, +population, +automatic, +treasuryTransferred, +formerPolityId: polity.id }, 3); } } -} + enforcePolityContinuity(polity) { const center = this.getCityById(polity.centerCityId); const cities = this.getPolityCities(polity); @@ -4972,7 +5633,7 @@ if (!center || center.population <= 0) { const oldCenterCityId = polity.centerCityId; center = cities.reduce((best, city) => this.cityInfluence(city) > this.cityInfluence(best) ? city : best, cities[0]); polity.centerCityId = center.id; -center.loyalty = 1; +center.loyalty = this.polityLoyaltyCap ? this.polityLoyaltyCap(polity) : 1; if (oldCenterCityId !== center.id) { this.addPolityEvent(polity.id, "capitalShift", this.year, { oldCenterCityId, @@ -5035,23 +5696,24 @@ this.cleanupPolities(); this.foundPolities(); if (this.reinforcePolityTradeRoutes) this.reinforcePolityTradeRoutes(); this.collectAndRedistributeResources(); +if (this.maybeBuildTreasuryCities) this.maybeBuildTreasuryCities(); this.detectPolityFamines(); if (this.erodePolityLegitimacy) this.erodePolityLegitimacy(); +if (this.updateInstitutionalAging) this.updateInstitutionalAging(); +if (this.maybeTriggerInstitutionalReforms) this.maybeTriggerInstitutionalReforms(); if (this.triggerPolityCrises) this.triggerPolityCrises(); if (this.applyOldStateStress) this.applyOldStateStress(); if (this.updatePolityLeaders) this.updatePolityLeaders(); this.updateCityLoyalty(); this.maybeStartCampaigns(); -this.updateCampaigns(); -this.updateWars(); this.splitUnloyalCities(); +this.enforcePolityTreasuryCaps(); this.cleanupPolities(); if (this.samplePolityHistories) this.samplePolityHistories(); this.syncTilePopulationCulture(); -this.recomputeTerritories({ allowOwnershipChanges: false, reason: "normal" }); +this.recomputeTerritories({ allowOwnershipChanges: true, rebalanceOwnership: true, reason: "normal" }); this.reconcileCityTerritoryOwnership(); this.campaignPressureOnTerritories(); -this.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); } reinforcePolityTradeRoutes() { for (const polity of this.polities) { @@ -5087,6 +5749,28 @@ if (!city) return 1; const capitalBonus = city.polityId !== null && this.getPolityById(city.polityId)?.centerCityId === city.id ? 1 : 0; return clamp((city.population > 180 ? 4 : city.population > 90 ? 3 : 2) + capitalBonus, 2, 5); } +desertCorridorShare(cityA, cityB, samples = 12) { +if (!cityA || !cityB) return 0; +const w = this.world; +let desert = 0; +let land = 0; +for (let step = 0; step <= samples; step++) { +const t = step / samples; +const x = clamp(Math.round(lerp(cityA.x, cityB.x, t)), 0, w.size - 1); +const y = clamp(Math.round(lerp(cityA.y, cityB.y, t)), 0, w.size - 1); +const terrain = w.terrain[w.idx(x, y)]; +if (terrain === Terrain.WATER) continue; +land++; +if (terrain === Terrain.DESERT) desert++; +} +return land > 0 ? desert / land : 0; +} +routeDesertShare(path) { +if (!path?.length) return 0; +let desert = 0; +for (const tile of path) if (this.world.terrain[tile] === Terrain.DESERT) desert++; +return desert / path.length; +} effectiveRouteLengthLimit(cityA = null, cityB = null, ownerPolity = null) { const cfg = SimConfig.route; const base = cfg.maxRouteLength ?? 42; @@ -5100,6 +5784,11 @@ const samePolity = cityA.polityId !== null && cityA.polityId === cityB.polityId; const capital = ownerPolity && (ownerPolity.centerCityId === cityA.id || ownerPolity.centerCityId === cityB.id) ? 1 : 0; const org = ownerPolity ? clamp((ownerPolity.cohesion ?? 0.6) * 0.7 + (ownerPolity.legitimacy ?? 0.7) * 0.3, 0, 1.2) : 0; const treasury = ownerPolity ? clamp(Math.sqrt(Math.max(0, ownerPolity.treasury || 0)) / 18, 0, 1.1) : 0; +const desertShare = this.desertCorridorShare(cityA, cityB); +const desertBonus = desertShare * (cfg.desertRouteLengthBonus ?? 30); +const normalMax = cfg.adaptiveMaxRouteLength ?? 72; +const desertMax = cfg.desertAdaptiveMaxRouteLength ?? normalMax; +const upperLimit = lerp(normalMax, desertMax, desertShare); return Math.floor(clamp( base + clamp(Math.sqrt(avgPopulation) * 0.55, 0, 10) + @@ -5107,9 +5796,10 @@ avgKnowledge * 12 + (samePolity ? 7 : 0) + capital * 6 + org * 5 + -treasury * 4, +treasury * 4 + +desertBonus, base, -cfg.adaptiveMaxRouteLength ?? 72 +upperLimit )); } cityHasRouteTilesNear(city, radius = SimConfig.route.isolatedRouteRadius ?? 4) { @@ -5194,12 +5884,97 @@ return this.distanceBetweenCities(city, a) + polityBiasA - (this.distanceBetween for (const candidate of others.slice(0, 8)) add(candidate); return result; } +createManualTradeRoute(cityAId, cityBId) { +const cityA = this.getCityById(cityAId); +const cityB = this.getCityById(cityBId); +if (!cityA || !cityB) return { ok: false, reason: "missingCity" }; +if (cityA.id === cityB.id) return { ok: false, reason: "sameCity" }; +const cfg = SimConfig.route || {}; +const direct = this.distanceBetweenCities(cityA, cityB); +const maxLength = cfg.manualRouteMaxLength ?? 120; +if (direct > maxLength) return { ok: false, reason: "tooFar", maxLength }; +const ownerPolityId = cityA.polityId !== null && cityA.polityId === cityB.polityId ? cityA.polityId : null; +const ownerPolity = ownerPolityId !== null ? this.getPolityById(ownerPolityId) : null; +const naturalLimit = this.effectiveRouteLengthLimit(cityA, cityB, ownerPolity); +const limit = Math.min(maxLength, Math.max(naturalLimit, Math.ceil(direct + 24))); +const path = this.findTerrainRoute(cityA.x, cityA.y, cityB.x, cityB.y, limit, cityA, cityB, ownerPolity); +if (!this.isValidRoutePath(path, cityB.x, cityB.y, limit)) return { ok: false, reason: "noPath" }; +const protectionYears = cfg.manualProtectionYears ?? 25; +const protectedUntil = this.year + years(protectionYears); +const strength = Math.max(0.35, this.routeStrengthForPath(path)); +this.manualTradeRoutes ??= []; +this.manualTradeRoutes = this.manualTradeRoutes.filter(route => +!((route.from === cityA.id && route.to === cityB.id) || (route.from === cityB.id && route.to === cityA.id))); +const record = { from: cityA.id, to: cityB.id, path: [...path], strength, created: this.year, protectedUntil }; +this.manualTradeRoutes.push(record); +this.manualRouteProtection ??= new Map(); +for (const tile of path) { +if (this.world.terrain[tile] === Terrain.WATER) continue; +this.manualRouteProtection.set(tile, Math.max(this.manualRouteProtection.get(tile) || 0, protectedUntil)); +this.world.tradeRoute[tile] = Math.max(this.world.tradeRoute[tile], cfg.manualProtectedStrength ?? 32); +this.activeTradeRouteTiles.add(tile); +this.addPheromone(tile, cfg.newRoutePheromoneSeed ?? 2.5); +} +const existingLink = this.tradeLinks.some(link => { +const endpoints = this.tradeLinkEndpointIds(link); +return endpoints && endpoints.includes(cityA.id) && endpoints.includes(cityB.id); +}); +cityA.tradeLinks.add(cityB.id); +cityB.tradeLinks.add(cityA.id); +if (!existingLink) { +this.tradeLinks.push({ from: cityA.id, to: cityB.id, strength, path: [...path], ownerPolityId, manual: true, protectedUntil }); +} +return { ok: true, path, protectionYears, protectedUntil }; +} +cleanupManualTradeRouteProtection() { +this.manualTradeRoutes ??= []; +this.manualRouteProtection ??= new Map(); +const active = []; +this.manualRouteProtection.clear(); +for (const route of this.manualTradeRoutes) { +if (!route || route.protectedUntil <= this.year) continue; +const a = this.getCityById(route.from); +const b = this.getCityById(route.to); +if (!a || !b || !route.path?.length) continue; +active.push(route); +for (const tile of route.path) { +if (tile < 0 || tile >= this.world.count || this.world.terrain[tile] === Terrain.WATER) continue; +this.manualRouteProtection.set(tile, Math.max(this.manualRouteProtection.get(tile) || 0, route.protectedUntil)); +} +} +this.manualTradeRoutes = active; +} +isManualRouteTileProtected(tile) { +return (this.manualRouteProtection?.get(tile) || 0) > this.year; +} +restoreProtectedManualRoutes(supportedRoutes) { +const cfg = SimConfig.route || {}; +for (const route of this.manualTradeRoutes || []) { +if (route.protectedUntil <= this.year) continue; +const cityA = this.getCityById(route.from); +const cityB = this.getCityById(route.to); +if (!cityA || !cityB) continue; +cityA.tradeLinks.add(cityB.id); +cityB.tradeLinks.add(cityA.id); +const ownerPolityId = cityA.polityId !== null && cityA.polityId === cityB.polityId ? cityA.polityId : null; +const exists = this.tradeLinks.some(link => +(link.from === cityA.id && link.to === cityB.id) || (link.from === cityB.id && link.to === cityA.id)); +if (!exists) this.tradeLinks.push({ ...route, ownerPolityId, manual: true }); +for (const tile of route.path) { +if (tile < 0 || tile >= this.world.count || this.world.terrain[tile] === Terrain.WATER) continue; +if (supportedRoutes) supportedRoutes.add(tile); +this.world.tradeRoute[tile] = Math.max(this.world.tradeRoute[tile], cfg.manualProtectedStrength ?? 32); +this.activeTradeRouteTiles.add(tile); +} +} +} updateTradeRoutes(fieldBased = false) { const w = this.world; +this.cleanupManualTradeRouteProtection(); for (let i = 0; i < w.count; i++) { const invalidTerrain = w.terrain[i] === Terrain.WATER; if (invalidTerrain) w.tradeRoute[i] = 0; -else if (w.tradeRoute[i] > 0 && this.year % years(4) === 0) { +else if (w.tradeRoute[i] > 0 && this.year % years(4) === 0 && !this.isManualRouteTileProtected(i)) { const pheromoneSupport = clamp(w.pheromone[i] / SimConfig.route.maxPheromone, 0, 1); const decay = pheromoneSupport >= SimConfig.route.routePheromoneMaintain ? 1 : SimConfig.route.routeUnsupportedDecay; w.tradeRoute[i] = Math.max(0, w.tradeRoute[i] - decay); @@ -5208,6 +5983,9 @@ if (w.tradeRoute[i] < SimConfig.route.routeWeakThreshold) w.tradeRoute[i] = 0; } this.tradeLinks = []; for (const city of this.cities) city.tradeLinks.clear(); +const supportedRoutes = this.supportedRouteScratch; +supportedRoutes.clear(); +this.restoreProtectedManualRoutes(supportedRoutes); const maxPairsToRoute = clamp(this.cities.length * 7, 80, 1400); const pairHeap = this.pairCandidateScratch; pairHeap.length = 0; @@ -5248,14 +6026,15 @@ if (!this.isValidRoutePath(path, c2.x, c2.y, limit)) continue; const strength = this.routeStrengthForPath(path); const pheromoneSupport = this.routePheromoneSupport(path); if (strength < 0.08) continue; -if (!fieldBased && ownerPolityId === null && pheromoneSupport < SimConfig.route.routePheromoneBuild) continue; +const desertShare = this.routeDesertShare(path); +const desertThresholdFactor = SimConfig.route.desertPheromoneThresholdFactor ?? 0.45; +const buildThreshold = SimConfig.route.routePheromoneBuild * (1 - desertShare * (1 - desertThresholdFactor)); +if (!fieldBased && ownerPolityId === null && pheromoneSupport < buildThreshold) continue; if (!this.canPayRouteConstructionCost(c1, c2, path, ownerPolity)) continue; const tradeScore = strength * (1 + distanceFactor * 0.7 + pheromoneSupport * 0.75 + (ownerPolityId !== null ? 0.28 : 0)) * marketScale; this.topHeapPush(candidates, { c1, c2, strength, path, tradeScore, pheromoneSupport, ownerPolityId }, routeCandidateLimit, "tradeScore"); } this.heapToDescending(candidates, "tradeScore"); -const supportedRoutes = this.supportedRouteScratch; -supportedRoutes.clear(); for (const candidate of candidates) { if (this.tradeLinks.length >= maxLinks) break; const { c1, c2, strength, path, pheromoneSupport, ownerPolityId } = candidate; @@ -5278,9 +6057,7 @@ this.applyRouteConnectionBenefits(c1, c2, strength, pheromoneSupport, upkeepPaid this.bootstrapIsolatedCityRoutes(supportedRoutes); for (let i = 0; i < w.count; i++) { if (w.tradeRoute[i] && !supportedRoutes.has(i)) { -const pheromoneSupport = clamp(w.pheromone[i] / SimConfig.route.maxPheromone, 0, 1); -w.tradeRoute[i] = Math.max(0, w.tradeRoute[i] - (pheromoneSupport >= SimConfig.route.routePheromoneMaintain ? 2 : SimConfig.route.routeUnsupportedDecay)); -if (w.tradeRoute[i] < SimConfig.route.routeWeakThreshold) w.tradeRoute[i] = 0; +w.tradeRoute[i] = 0; } } this.activeTradeRouteTiles.clear(); @@ -5289,6 +6066,59 @@ if (w.tradeRoute[i]) this.activeTradeRouteTiles.add(i); } this.diffuseCityKnowledgeThroughTrade(); } +pruneOrphanTradeRoutes() { +const w = this.world; +this.cleanupManualTradeRouteProtection(); +const validLinks = []; +const validTiles = new Set(); +for (const link of this.tradeLinks || []) { +if (link.manual && link.protectedUntil != null && link.protectedUntil <= this.year) continue; +const a = this.getCityById(link.from); +const b = this.getCityById(link.to); +const path = link.path || []; +if (!a || !b || !path.length) continue; +const first = path[0]; +const last = path[path.length - 1]; +const endpointDistance = (tile, city) => Math.abs(tile % w.size - city.x) + Math.abs(Math.floor(tile / w.size) - city.y); +const aligned = +(endpointDistance(first, a) <= 1 && endpointDistance(last, b) <= 1) || +(endpointDistance(first, b) <= 1 && endpointDistance(last, a) <= 1); +if (!aligned) continue; +validLinks.push(link); +for (const tile of path) validTiles.add(tile); +} +// Protected user-created routes remain valid even if a normal route refresh has +// not run since they were created. Once protection expires they are omitted and +// can disappear under the ordinary route rules. +for (const route of this.manualTradeRoutes || []) { +if (route.protectedUntil <= this.year) continue; +const a = this.getCityById(route.from); +const b = this.getCityById(route.to); +if (!a || !b || !route.path?.length) continue; +const already = validLinks.some(link => { +const endpoints = this.tradeLinkEndpointIds(link); +return endpoints && endpoints.includes(a.id) && endpoints.includes(b.id); +}); +if (!already) validLinks.push({ ...route, manual: true }); +for (const tile of route.path) if (w.terrain[tile] !== Terrain.WATER) validTiles.add(tile); +} +this.tradeLinks = validLinks; +for (const city of this.cities) { +city.tradeLinks ??= new Set(); +city.tradeLinks.clear(); +} +for (const link of validLinks) { +const a = this.getCityById(link.from); +const b = this.getCityById(link.to); +if (!a || !b) continue; +a.tradeLinks.add(b.id); +b.tradeLinks.add(a.id); +} +for (let tile = 0; tile < w.count; tile++) if (w.tradeRoute[tile] && !validTiles.has(tile)) w.tradeRoute[tile] = 0; +this.activeTradeRouteTiles.clear(); +for (const tile of validTiles) if (w.tradeRoute[tile]) this.activeTradeRouteTiles.add(tile); +} + registerTradeLink(cityA, cityB, strength, path, supportedRoutes = null, routeBoost = 18, pheromoneBoost = 0, ownerPolityId = null) { cityA.tradeLinks.add(cityB.id); cityB.tradeLinks.add(cityA.id); @@ -5412,7 +6242,8 @@ const w = this.world; const routeRelief = w.tradeRoute[tile] ? 0.58 : 1; const pheromoneRelief = 1 - clamp(w.pheromone[tile] / SimConfig.route.maxPheromone, 0, 0.32); const mountain = w.terrain[tile] === Terrain.MOUNTAIN ? 1.2 : 0; -return Math.max(0.45, (w.move[tile] + mountain) * routeRelief * pheromoneRelief); +const desertFactor = w.terrain[tile] === Terrain.DESERT ? (SimConfig.route.desertTraversalCostFactor ?? 0.68) : 1; +return Math.max(0.45, (w.move[tile] * desertFactor + mountain) * routeRelief * pheromoneRelief); } routeStrengthForPath(path) { const w = this.world; @@ -5423,7 +6254,7 @@ let terrainEase = 0; for (const i of path) { route += w.tradeRoute[i] > 0 ? 1 : 0; pheromone += clamp(w.pheromone[i] / 9, 0, 1); -terrainEase += 1 / Math.max(1, w.move[i]); +terrainEase += 1 / Math.max(0.75, this.routeTileCost(i)); } return route / path.length * 0.45 + pheromone / path.length * 0.35 + terrainEase / path.length * 0.2; } @@ -5516,7 +6347,8 @@ routePathDifficulty(path) { if (!path.length) return 1; let difficulty = 0; for (const tile of path) { -const move = this.world.move[tile] || 1; +let move = this.world.move[tile] || 1; +if (this.world.terrain[tile] === Terrain.DESERT) move *= SimConfig.route.desertTraversalCostFactor ?? 0.68; const hard = move > 1.8 ? 0.35 : 0; const existing = this.world.tradeRoute[tile] ? -0.25 : 0; difficulty += Math.max(0.45, move + hard + existing); @@ -5527,7 +6359,10 @@ routeConstructionCost(path, polityBacked = false, options = {}) { let weakTiles = 0; for (const tile of path) if (this.world.tradeRoute[tile] < 24) weakTiles++; const difficulty = this.routePathDifficulty(path); -const base = weakTiles * (polityBacked ? 0.16 : 0.12) * clamp(difficulty / 1.35, 0.75, 2.1); +const desertShare = this.routeDesertShare(path); +const desertFactor = SimConfig.route.desertConstructionCostFactor ?? 0.72; +const terrainConstructionFactor = 1 - desertShare * (1 - desertFactor); +const base = weakTiles * (polityBacked ? 0.16 : 0.12) * clamp(difficulty / 1.35, 0.75, 2.1) * terrainConstructionFactor; return options.bootstrap ? base * 0.62 : base; } canPayRouteConstructionCost(cityA, cityB, path, polity = null, options = {}) { @@ -5567,6 +6402,7 @@ return paid; addPheromone(tile, amount) { if (tile < 0 || tile >= this.world.count || amount <= 0) return; this.world.pheromone[tile] = Math.min(SimConfig.route.maxPheromone, this.world.pheromone[tile] + amount); +if (this.world.pheromone[tile] > 0) this.activePheromoneTiles?.add(tile); } depositRoutePheromone(path, amount) { for (const i of path) { @@ -5620,11 +6456,94 @@ e.diversity /= e.population; e.averageTraits ??= this.randomTraits(); } } +absorptionTargetForEthnicity(ethnicityId) { +const ethnicity = this.ethnicities.get(ethnicityId); +if (!ethnicity) return null; +const scores = new Map(); +const addScore = (id, amount) => { +if (id == null || id === ethnicityId || amount <= 0 || !this.ethnicities.has(id)) return; +scores.set(id, (scores.get(id) || 0) + amount); +}; +for (const [tile, mix] of this.tileEthnicMix) { +const local = mix.get(ethnicityId) || 0; +if (local <= 0) continue; +for (const [id, count] of mix) addScore(id, count * 1.5); +const begin = this.world.landNeighborStarts?.[tile] ?? 0; +const end = this.world.landNeighborStarts?.[tile + 1] ?? begin; +for (let index = begin; index < end; index++) { +const neighborMix = this.tileEthnicMix.get(this.world.landNeighbors[index]); +if (!neighborMix) continue; +for (const [id, count] of neighborMix) addScore(id, count); +} +} +for (const city of this.cities) { +const local = city.ethnicityComposition?.get(ethnicityId) || 0; +if (local <= 0) continue; +for (const [id, count] of city.ethnicityComposition) addScore(id, count * 1.25); +} +let bestId = null; +let bestScore = -Infinity; +for (const [id, score] of scores) { +if (score > bestScore) { bestId = id; bestScore = score; } +} +if (bestId !== null) return bestId; +const parent = ethnicity.parent; +if (parent != null && parent !== ethnicityId && this.ethnicities.has(parent)) return parent; +let nearest = null; +let nearestDistance = Infinity; +for (const candidate of this.ethnicities.values()) { +if (candidate.id === ethnicityId || (candidate.population || 0) <= 0) continue; +const distance = Math.hypot((candidate.centroidX || 0) - (ethnicity.centroidX || 0), (candidate.centroidY || 0) - (ethnicity.centroidY || 0)); +if (distance < nearestDistance) { nearest = candidate.id; nearestDistance = distance; } +} +return nearest; +} +absorbEthnicityInto(ethnicityId, targetId) { +if (ethnicityId === targetId || !this.ethnicities.has(ethnicityId) || !this.ethnicities.has(targetId)) return false; +for (const [tile, mix] of this.tileEthnicMix) { +const amount = mix.get(ethnicityId) || 0; +if (amount <= 0) continue; +mix.delete(ethnicityId); +mix.set(targetId, (mix.get(targetId) || 0) + amount); +this.updateCultureTile(tile); +} +for (const city of this.cities) { +const amount = city.ethnicityComposition?.get(ethnicityId) || 0; +if (amount <= 0) continue; +city.ethnicityComposition.delete(ethnicityId); +city.ethnicityComposition.set(targetId, (city.ethnicityComposition.get(targetId) || 0) + amount); +} +for (const band of this.nomadBands || []) if (band.ethnicityId === ethnicityId) band.ethnicityId = targetId; +for (const ethnicity of this.ethnicities.values()) if (ethnicity.parent === ethnicityId) ethnicity.parent = targetId; +this.ethnicities.delete(ethnicityId); +return true; +} +absorbSmallEthnicities() { +const threshold = Math.max(0, SimConfig.culture?.absorptionPopulationThreshold ?? 30); +if (threshold <= 0 || this.ethnicities.size <= 1) return 0; +let absorbed = 0; +const candidates = [...this.ethnicities.values()] +.filter(ethnicity => (ethnicity.population || 0) > 0 && (ethnicity.population || 0) < threshold) +.sort((a, b) => (a.population || 0) - (b.population || 0)); +for (const ethnicity of candidates) { +if (!this.ethnicities.has(ethnicity.id)) continue; +const target = this.absorptionTargetForEthnicity(ethnicity.id); +if (target == null) continue; +if (this.absorbEthnicityInto(ethnicity.id, target)) absorbed++; +} +return absorbed; +} splitDivergentEthnicities() { this.splitDivergentFieldEthnicities(); } splitDivergentFieldEthnicities() { +let formed = 0; +const divergenceSpeed = clamp(SimConfig.culture?.divergenceRateMultiplier ?? 0.25, 0.05, 1); +const divergenceChance = 0.32; for (const e of [...this.ethnicities.values()]) { +if (formed >= 1) break; +if (this.year < (e.nextDivergenceCheck ?? 0)) continue; +e.nextDivergenceCheck = this.year + years(this.rng.range(24, 90) / divergenceSpeed); if (e.population < 120 || e.diversity < 0.24) continue; const candidates = []; let population = 0; @@ -5637,7 +6556,7 @@ const x = tile % this.world.size; const y = Math.floor(tile / this.world.size); const spatial = Math.hypot(x - e.centroidX, y - e.centroidY) > this.world.size * 0.11; const climate = this.climateMismatch(e.id, tile) > 0.20; -if ((spatial || climate || this.world.cultureDiversity[tile] > 0.38) && this.rng.next() < 0.32) { +if ((spatial || climate || this.world.cultureDiversity[tile] > 0.38) && this.rng.next() < divergenceChance) { candidates.push({ tile, count }); population += count; tempSum += this.world.temperature[tile] * count; @@ -5661,6 +6580,7 @@ mix.set(e.id, Math.max(0, count - split)); mix.set(newId, (mix.get(newId) || 0) + split); this.updateCultureTile(tile); } +formed++; } } } diff --git a/index.html b/index.html index 59edc15..52c9302 100644 --- a/index.html +++ b/index.html @@ -1,125 +1,160 @@ - + - Emergent Civilization Simulation + 文明シミュレータ
-
+ +
+

世界を再生成

+ +

再生成すると現在の世界は破棄されます。

+
+ + +
+
+
+ diff --git a/render.js b/render.js index 105ccbe..f2accf2 100644 --- a/render.js +++ b/render.js @@ -1,3 +1,47 @@ +const displayTerms = { + "roaming": "移動中", + "grazing": "放牧中", + "leaving": "離脱中", + "settling": "定住中", + "trading": "交易中", + "raiding": "襲撃中", + "invading": "侵攻中", + "confederating": "連合形成中", + "frontier_colonization": "辺境開拓", + "nonstate_subjugation": "非国家地域の征服", + "independent_city_annexation": "独立都市の併合", + "border_war": "国境戦争", + "frontier": "辺境", + "nonstate": "非国家地域", + "independentCity": "独立都市", + "polity": "国家", + "started": "開始", + "ended": "終結", + "ceased": "停戦", + "defending": "防衛", + "succeeded": "成功", + "failed": "失敗", + "peaceful": "平和的併合", + "military": "戦争による併合", + "expired": "期限切れ", + "active": "進行中", + "puppetIndependent": "傀儡都市化", + "nomadicDynasty": "遊牧王朝の成立", + "capturedCity": "都市占領", + "lostCity": "都市喪失", + "settlementGain": "講和による領土獲得", + "settlementLoss": "講和による領土喪失", + "settlementPressure": "講和圧力", + "dissolved": "解体", + "collapsed": "崩壊", + "conquest": "征服", + "war": "戦争", + "event": "発生", + "target": "対象", + "campaign": "遠征", + "non-state": "非国家地域" +}; +function displayTerm(value) { return displayTerms[value] || "状況変化"; } function render() { const start = performance.now(); const w = sim.world; @@ -9,7 +53,6 @@ renderImageSize = size; const image = renderImage; const data = image.data; const mode = els.viewMode.value; -const cityPolityColor = new Map(); const polityColors = new Map(); if (mode === "polities") { for (const polity of sim.polities) { @@ -18,16 +61,6 @@ color: polity.color, isGraphHover: graphState.hoverPolityId === polity.id }); } -for (const city of sim.cities) { -if (city.polityId === null) continue; -const polity = cityPolity(city); -if (!polity) continue; -const isGraphHover = graphState.hoverPolityId === polity.id; -cityPolityColor.set(city.id, { -color: polity.color, -isGraphHover -}); -} } for (let i = 0; i < w.count; i++) { let color; @@ -55,12 +88,12 @@ const city = sim.getCityById(w.city[i]); if (city) color = mix(color, cityMajorityColor(city), 0.72); } } else if (mode === "pressure") { -const pressure = w.populationPressure?.[i] ?? (w.pressure[i] / 12); +const pressure = w.populationPressure[i] || 0; const v = clamp(pressure, 0, 1.6) / 1.6; color = mix(terrainInfo[w.terrain[i]].color, [210, 73, 67], v); } else if (mode === "polities") { color = terrainInfo[w.terrain[i]].color; -const territory = w.polity?.[i] ?? -1; +const territory = w.territoryOwner[i]; if (territory >= 0) { const polityColor = polityColors.get(territory); if (polityColor) { @@ -71,13 +104,6 @@ if (sim.isBorderTile?.(i)) color = mix(color, [232, 225, 185], 0.22); if (graphState.hoverPolityId !== null && !polityColor.isGraphHover) color = mix(color, [16, 18, 19], 0.30); } } -if (w.city[i] >= 0) { -const polityColor = cityPolityColor.get(w.city[i]); -if (polityColor) { -color = mix(color, polityColor.color, polityColor.isGraphHover ? 0.82 : 0.5); -if (graphState.hoverPolityId !== null && !polityColor.isGraphHover) color = mix(color, [16, 18, 19], 0.28); -} -} } else if (mode === "technology") { const farming = w.farmingKnowledge?.[i] || 0; const metallurgy = w.metallurgyKnowledge?.[i] || 0; @@ -106,7 +132,7 @@ drawCampaigns(); drawPopulationAndCities(mode); drawGraphPolityHighlight(); maybeRenderStateGraph(); -els.frameCost.textContent = `${Math.round(performance.now() - start)}ms`; +if (els.frameCost) els.frameCost.textContent = `${Math.round(performance.now() - start)}ミリ秒`; } function renderDisasters() { if (!sim?.disasters?.length) return; @@ -184,6 +210,7 @@ const w = sim.world; ctx.save(); for (const campaign of sim.campaigns) { if (campaign.status !== "active" || campaign.targetTile == null) continue; +if (campaign.type !== "border_war") continue; const x = campaign.targetTile % w.size; const y = Math.floor(campaign.targetTile / w.size); const age = Math.max(0, sim.year - campaign.started) / Math.max(1, campaign.expires - campaign.started); @@ -246,25 +273,13 @@ ctx.restore(); } function drawPopulationAndCities(mode) { ctx.save(); -if (mode !== "ethnicity") { -for (const city of sim.cities) { -const radius = cityRenderRadius(city); -const color = cityDisplayColor(city, mode); -ctx.fillStyle = rgba(color, 0.72); -ctx.globalAlpha = 0.88; -fillSquare(ctx, city.x, city.y, radius); -} -} ctx.globalAlpha = 1; -if (mode === "technology") { -// Tile-level technology is rendered in the base pass. -} else if (mode === "ethnicity") { -// Ethnicity view is driven by tile-level population/culture fields. -} else { +if (mode !== "technology" && mode !== "ethnicity") { const w = sim.world; ctx.fillStyle = "rgba(238, 236, 207, 0.16)"; -for (let tile = 0; tile < w.count; tile++) { -const pop = w.population?.[tile] || 0; +const populatedTiles = sim.activePopulationTiles || []; +for (const tile of populatedTiles) { +const pop = (w.population?.[tile] || 0) / (SimConfig.population.populationScale || 1); if (pop < 6) continue; const alpha = clamp(Math.log1p(pop) / Math.log(160), 0.04, 0.18); ctx.fillStyle = `rgba(238, 236, 207, ${alpha})`; @@ -272,15 +287,14 @@ ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1); } } for (const city of sim.cities) { -if (mode === "ethnicity") { const radius = cityRenderRadius(city); const color = cityDisplayColor(city, mode); -ctx.globalAlpha = 0.90; -ctx.fillStyle = rgba(color, 0.78); -fillSquare(ctx, city.x, city.y, radius); ctx.globalAlpha = 1; -} -const color = cityDisplayColor(city, mode); +ctx.fillStyle = rgb(color); +fillSquare(ctx, city.x, city.y, radius); +ctx.strokeStyle = "#111315"; +ctx.lineWidth = 1; +ctx.strokeRect(city.x - radius + 0.5, city.y - radius + 0.5, radius * 2, radius * 2); ctx.fillStyle = rgb(color, 55); ctx.fillRect(city.x, city.y, 1, 1); } @@ -308,7 +322,19 @@ ctx.fillRect(city.x - radius + 1, city.y - radius + 1, Math.max(1, radius * 2 - ctx.restore(); } function cityRenderRadius(city) { -return clamp(Math.floor(Math.sqrt(city.population) / 12), 1, 7); +const population = Math.max(0, city?.population || 0); +if (population >= 10000) return 4; +if (population >= 5000) return 3; +if (population >= 1000) return 2; +return 1; +} +function cityAtDisplayTile(x, y) { +for (let index = sim.cities.length - 1; index >= 0; index--) { +const city = sim.cities[index]; +const radius = cityRenderRadius(city); +if (Math.abs(city.x - x) <= radius && Math.abs(city.y - y) <= radius) return city; +} +return null; } function cityMajorityColor(city) { const id = dominantComposition(city.ethnicityComposition); @@ -325,12 +351,6 @@ return [218, 205, 154]; } return cityMajorityColor(city); } -function techLevel(holder, key) { -return holder?.tech?.[key] || 0; -} -function knowledgeLevel(city, key) { -return city?.knowledge?.[key] || 0; -} function showTooltip(event) { const rect = els.canvas.getBoundingClientRect(); const simRect = els.sim.getBoundingClientRect(); @@ -363,80 +383,64 @@ return; } const w = sim.world; const i = w.idx(x, y); -const city = w.city[i] >= 0 ? sim.getCityById(w.city[i]) : null; +const city = cityAtDisplayTile(x, y); +if (w.terrain[i] === Terrain.WATER && w.city[i] < 0) { +hideTooltip(); +return; +} const terrain = terrainInfo[w.terrain[i]]; -const waterInfluence = waterInfluenceAt(w, x, y).toFixed(2); -const cityEthnicity = city ? dominantComposition(city.ethnicityComposition) : null; const polity = cityPolity(city); -const territoryId = w.polity?.[i] ?? -1; -const territoryPolity = territoryId >= 0 ? sim.getPolityById(territoryId) : null; -const ethnicMix = sim.tileEthnicMix?.get(i); -const tileDominantEthnicity = ethnicMix?.size ? dominantComposition(ethnicMix) : w.dominantEthnicity[i]; -const regionalEthnicity = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? `E${tileDominantEthnicity}` : "-"; -const regionalLineage = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? sim.ethnicities.get(tileDominantEthnicity) : null; -const mismatch = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? sim.climateMismatch(tileDominantEthnicity, i) : 0; -const tilePopulation = w.population?.[i] || 0; -const nearbyCampaigns = activeCampaignsNear(i); -const nearbyNomad = activeNomadNear(i); -els.tooltip.innerHTML = ` - ${city ? `City #${city.id}` : terrain.name} - ${tooltipSection("Tile", [ - tooltipRow("Position", `${x}, ${y}`), - tooltipRow("Terrain", terrain.name), - tooltipRow("Resources", w.resource[i].toFixed(1)), - tooltipRow("Fertility", w.fertility[i].toFixed(2)), - tooltipRow("Minerals", w.mineral[i].toFixed(2)), - tooltipRow("Temp / humid", `${w.temperature[i].toFixed(2)} / ${w.humidity[i].toFixed(2)}`), - tooltipRow("Water", waterInfluence), - tooltipRow("Pressure", (w.populationPressure?.[i] ?? 0).toFixed(2)), - tooltipRow("Capacity", (w.populationCapacity?.[i] ?? 0).toFixed(1)), - tooltipRow("Pheromone", w.pheromone[i].toFixed(1)), - tooltipRow("Route", w.tradeRoute[i], w.tradeRoute[i]) - ])} - ${tooltipSection("Territory", [ - tooltipRow("Territory", territoryPolity ? `#${territoryPolity.id}` : "Unclaimed"), - tooltipRow("Control", `${Math.round((w.control?.[i] || 0) * 100)}%`), - tooltipRow("Contested", w.contested?.[i] ? "yes" : "no"), - tooltipRow("Campaign", nearbyCampaigns, nearbyCampaigns), - tooltipRow("Dominant ethnicity", regionalEthnicity), - tooltipRow("Tile population", tilePopulation >= 10 ? Math.round(tilePopulation).toLocaleString() : tilePopulation.toFixed(1)) - ])} - ${tooltipSection("Nomads", [ - tooltipRow("Band", nearbyNomad ? `${nearbyNomad.name} #${nearbyNomad.id}` : "", nearbyNomad), - tooltipRow("Mode", nearbyNomad?.mode, nearbyNomad), - tooltipRow("Population", nearbyNomad ? Math.round(nearbyNomad.population).toLocaleString() : "", nearbyNomad), - tooltipRow("Ethnicity", nearbyNomad ? `E${nearbyNomad.ethnicityId}` : "", nearbyNomad), - tooltipRow("Cohesion", nearbyNomad?.cohesion.toFixed(2), nearbyNomad), - tooltipRow("Prestige", nearbyNomad?.prestige.toFixed(2), nearbyNomad), - tooltipRow("Resources", nearbyNomad?.resources.toFixed(1), nearbyNomad), - tooltipRow("Herds", nearbyNomad?.herds.toFixed(1), nearbyNomad) - ])} - ${tooltipSection("City & State", [ - tooltipRow("Population", city?.population.toLocaleString(), city), - tooltipRow("Food stock", city?.storedResources.toFixed(1), city), - tooltipRow("Supply stress", (city?.supplyStress || 0).toFixed(2), city), - tooltipRow("Trade", city ? `${city.tradeLinks.size} links, ${(city.tradeValue || 0).toFixed(2)} value` : "", city), - tooltipRow("Knowledge", city ? `${knowledgeLevel(city, "farming").toFixed(2)} farm / ${knowledgeLevel(city, "metallurgy").toFixed(2)} metal` : "", city), - tooltipRow("City majority", `E${cityEthnicity}`, cityEthnicity), - city ? cityEthnicityPie(city) : "", - tooltipRow("State", polity ? `#${polity.id}` : "Independent", city), - tooltipRow("Loyalty", city?.loyalty.toFixed(2), city), - tooltipRow("Charisma", clamp(polity?.charisma ?? 1, 0.5, 1.5).toFixed(2), polity), - tooltipRow("Leader tenure", polity ? `${Math.floor((sim.year - (polity.leaderStarted ?? polity.founded ?? sim.year)) / WEEKS_PER_YEAR)}y` : "", polity), - tooltipRow("Treasury", polity?.treasury.toFixed(1), polity), - tooltipRow("Capital", polity && city ? polity.centerCityId === city.id ? "yes" : "no" : "", polity) - ])} - ${tooltipSection("Culture", [ - tooltipRow("Local culture", `${regionalEthnicity} / ${w.cultureDiversity[i].toFixed(2)}`), - tooltipRow("Lineage pop", regionalLineage?.population?.toLocaleString(), regionalLineage), - tooltipRow("Climate pref", regionalLineage ? `${regionalLineage.climateTemp.toFixed(2)} / ${regionalLineage.climateHumidity.toFixed(2)}` : "", regionalLineage), - tooltipRow("Climate mismatch", mismatch.toFixed(2), regionalLineage), - tooltipRow("Settled pop", (w.settledPopulation?.[i] || 0).toFixed(1), tilePopulation > 0), - tooltipRow("Mobile pop", (w.mobilePopulation?.[i] || 0).toFixed(1), tilePopulation > 0), - tooltipRow("Farming", (w.farmingKnowledge?.[i] || 0).toFixed(3)), - tooltipRow("Metallurgy", (w.metallurgyKnowledge?.[i] || 0).toFixed(3)) - ])} - `; +const territoryPolity = sim.getPolityById(w.territoryOwner[i]); +const displayPolity = polity || territoryPolity; +const mode = els.viewMode.value; +const ethnicity = w.dominantEthnicity[i]; +const population = w.population[i] || 0; +const rows = [ +tooltipRow("場所", `${terrain.name}(${x}, ${y})`), +tooltipRow("所属", polity ? `国家${polity.id}${polity.centerCityId === city.id ? "・首都" : ""}` : city ? "独立都市" : territoryPolity ? `国家${territoryPolity.id}` : "国家未所属") +]; +let citySection = ""; +if (city) { +const cityRows = [ +tooltipRow("都市人口", city.population > 0 ? Math.round(city.population).toLocaleString() : "希薄"), +tooltipRow("食料備蓄", city.storedResources.toFixed(1)), +polity ? tooltipRow("忠誠度", `${Math.round((city.loyalty ?? 0.5) * 100)}%`) : "" +]; +citySection = `

都市データ

${cityRows.filter(Boolean).join("")}${cityEthnicityPie(city)}
`; +} else { +rows.push(tooltipRow("地域人口", population > 0 ? Math.round(population).toLocaleString() : "希薄")); +if (mode === "resources" || mode === "terrain") { +rows.push(tooltipRow("資源", w.resource[i].toFixed(1))); +rows.push(tooltipRow("肥沃度", `${Math.round(w.fertility[i] * 100)}%`)); +} +} +if (mode === "pressure") rows.push(tooltipRow("人口圧力", `${Math.round(w.populationPressure[i] * 100)}%(収容力比)`)); +if (mode === "ethnicity" && ethnicity >= 0) rows.push(tooltipRow("主な民族", `民族${ethnicity}`)); +if (mode === "technology") rows.push(tooltipRow("農業 / 冶金", `${Math.round(w.farmingKnowledge[i] * 100)}% / ${Math.round(w.metallurgyKnowledge[i] * 100)}%`)); +if (mode === "pheromone") rows.push(tooltipRow("交通量", w.pheromone[i].toFixed(1))); +const campaign = activeCampaignsNear(i); +if (campaign) rows.push(tooltipRow("遠征", campaign)); +else if (w.contested[i]) rows.push(tooltipRow("状態", "係争地")); +const band = activeNomadNear(i); +if (band) rows.push(tooltipRow("遊牧集団", `集団${band.id}・${displayTerm(band.mode)}`)); +let politySection = ""; +if (displayPolity) { +const polityCities = sim.getPolityCities(displayPolity); +const polityPopulation = sim.totalPolityPopulation(displayPolity); +const institutionalAging = sim.polityInstitutionalAging ? sim.polityInstitutionalAging(displayPolity) : clamp(displayPolity.institutionalAging ?? 0, 0, 1); +const loyaltyCap = sim.polityLoyaltyCap ? sim.polityLoyaltyCap(displayPolity) : clamp(1 - institutionalAging, 0, 1); +politySection = tooltipSection("国家データ", [ +tooltipRow("国家", `国家${displayPolity.id}`), +tooltipRow("国力", sim.polityPower(displayPolity).toFixed(1)), +tooltipRow("人口", Math.round(polityPopulation).toLocaleString()), +tooltipRow("都市数", polityCities.length.toLocaleString()), +tooltipRow("国庫", Math.max(0, displayPolity.treasury || 0).toFixed(1)), +tooltipRow("正統性 / 結束", `${Math.round((displayPolity.legitimacy ?? 0) * 100)}% / ${Math.round((displayPolity.cohesion ?? 0) * 100)}%`), +tooltipRow("制度老朽化", `${Math.round(institutionalAging * 100)}%`), +tooltipRow("忠誠度上限", `${Math.round(loyaltyCap * 100)}%`) +]); +} +els.tooltip.innerHTML = `${city ? `都市${city.id}` : terrain.name}${rows.join("")}${citySection}${politySection}`; els.tooltip.hidden = false; const margin = 8; const width = els.tooltip.offsetWidth; @@ -470,7 +474,7 @@ return `rgb(${color.join(",")}) ${start.toFixed(2)}% ${end.toFixed(2)}%`; const labels = slices.map(([id, count]) => { const ethnicity = id ? sim.ethnicities.get(id) : null; const color = ethnicity?.color || [122, 130, 126]; -const label = id ? `E${id}` : "Other"; +const label = id ? `民族${id}` : "その他"; return `${label} ${Math.round(count / total * 100)}%`; }).join(""); return ` @@ -497,9 +501,9 @@ const cy = Math.floor(campaign.targetTile / w.size); const nearTarget = Math.abs(cx - x) + Math.abs(cy - y) <= 3; const nearFront = campaign.affectedTiles?.includes(tile); if (!nearTarget && !nearFront) continue; -labels.push(`${campaign.type.replaceAll("_", " ")} ${Math.round(clamp(campaign.progress, 0, 1) * 100)}%`); +labels.push(`${displayTerm(campaign.type)} ${Math.round(clamp(campaign.progress, 0, 1) * 100)}%`); } -return labels.slice(0, 2).join("; "); +return labels.slice(0, 2).join("、"); } function activeNomadNear(tile) { if (!sim?.nomadBands?.length) return null; @@ -519,68 +523,24 @@ bestDistance = distance; } return best; } -function waterInfluenceAt(world, x, y) { -let score = 0; -for (let dy = -3; dy <= 3; dy++) { -for (let dx = -3; dx <= 3; dx++) { -const d = Math.abs(dx) + Math.abs(dy); -if (!d || d > 3) continue; -const tx = x + dx; -const ty = y + dy; -if (tx < 0 || ty < 0 || tx >= world.size || ty >= world.size) continue; -if (world.terrain[world.idx(tx, ty)] === Terrain.WATER) score += (4 - d) / 4; -} -} -return score; -} function updateStats(force = false) { const now = performance.now(); if (!force && now - lastStatsAt < SimConfig.render.statsThrottleMs) return; lastStatsAt = now; const livingEthnicities = [...sim.ethnicities.values()].filter(e => e.population > 0); -const urbanPopulation = sim.cities.reduce((sum, c) => sum + c.population, 0); -let farmingTotal = 0; -let metallurgyTotal = 0; -let farmingHolders = 0; -let metallurgyHolders = 0; -for (let i = 0; i < sim.world.count; i++) { -const pop = sim.world.population?.[i] || 0; -if (pop <= 0) continue; -farmingTotal += (sim.world.farmingKnowledge?.[i] || 0) * pop; -metallurgyTotal += (sim.world.metallurgyKnowledge?.[i] || 0) * pop; -if ((sim.world.farmingKnowledge?.[i] || 0) > 0.02) farmingHolders++; -if ((sim.world.metallurgyKnowledge?.[i] || 0) > 0.02) metallurgyHolders++; -} -const fieldPopulation = sim.world.population -? Array.from(sim.world.population).reduce((sum, value) => sum + value, 0) -: 0; -const knowledgeDenominator = Math.max(1, fieldPopulation); -const stats = { -year: formatSimDate(sim.year), -activeGroups: Math.round(fieldPopulation).toLocaleString(), -urbanPopulation: Math.floor(urbanPopulation).toLocaleString(), -ethnicities: livingEthnicities.length.toLocaleString(), -cities: sim.cities.length.toLocaleString(), -polities: sim.polities.length.toLocaleString(), -wars: sim.wars.length.toLocaleString(), -routes: sim.tradeLinks.length.toLocaleString(), -farmingKnowledge: `${(farmingTotal / knowledgeDenominator).toFixed(4)} (${farmingHolders})`, -metallurgyKnowledge: `${(metallurgyTotal / knowledgeDenominator).toFixed(4)} (${metallurgyHolders})`, -deaths: sim.deaths.toLocaleString() -}; -for (const [id, value] of Object.entries(stats)) { -if (els[id]) els[id].textContent = value; -} +if (els.currentDate) els.currentDate.textContent = formatSimDate(sim.year); const top = livingEthnicities.sort((a, b) => b.population - a.population).slice(0, 9); -els.ethnicityList.innerHTML = top.map(e => ( +if (els.ethnicityList) els.ethnicityList.innerHTML = top.map(e => ( `
  • ` + -`E${e.id} pop ${e.population.toLocaleString()} div ${e.diversity.toFixed(2)} parent ${e.parent || "-"}
  • ` +`民族${e.id} 人口 ${Math.round(e.population).toLocaleString()}
    多様性 ${Math.round(e.diversity * 100)}% 親系統 ${e.parent ? `民族${e.parent}` : "なし"}` )).join(""); maybeRenderStateGraph(); } function setLegend() { const mode = els.viewMode.value; -els.legend.innerHTML = (legendByMode[mode] || legendByMode.resources)(); +const content = (legendByMode[mode] || legendByMode.resources)(); +els.legend.innerHTML = content; +els.legend.hidden = !content; } function maybeRenderStateGraph() { const now = performance.now(); @@ -626,10 +586,18 @@ if (!rect.width) return; const historyWindowSpan = years(SimConfig.render.historyWindowYears); const historyWindowEnd = sim.year; const historyWindowStart = Math.max(0, historyWindowEnd - historyWindowSpan); +renderWorldTrendGraph(historyWindowStart, historyWindowEnd); graphState.filter = els.historyFilter?.value || graphState.filter; graphState.sort = els.historySort?.value || graphState.sort; graphState.metric = els.historyMetric?.value || graphState.metric; -const histories = sim.getAllPolityHistories() +const allHistories = sim.getAllPolityHistories(); +const recentEndedIds = new Set(allHistories +.filter(history => !(history.active && history.ended === null)) +.sort((a, b) => (b.ended ?? 0) - (a.ended ?? 0)) +.slice(0, 20) +.map(history => history.id)); +const histories = allHistories +.filter(history => (history.active && history.ended === null) || recentEndedIds.has(history.id)) .filter(history => (history.ended ?? sim.year) >= historyWindowStart) .map(history => ({ ...history, @@ -682,11 +650,11 @@ const activeRows = selected.filter(item => item.history.active && item.history.e const pastRows = selected.filter(item => !(item.history.active && item.history.ended === null)); const displayRows = []; if (graphState.showActive && graphState.filter !== "ended" && activeRows.length) { -displayRows.push({ type: "group", label: `Living ${activeRows.length}` }); +displayRows.push({ type: "group", label: `存続中 ${activeRows.length}` }); for (const item of activeRows) displayRows.push({ type: "state", item }); } if (graphState.showPast && graphState.filter !== "active" && pastRows.length) { -displayRows.push({ type: "group", label: `Past ${pastRows.length}` }); +displayRows.push({ type: "group", label: `滅亡済み ${pastRows.length}` }); for (const item of pastRows) displayRows.push({ type: "state", item }); } const logicalHeight = paddingTop + @@ -715,7 +683,8 @@ graphState.markers = []; graphState.scale = null; g.fillStyle = "#7f8984"; g.font = "12px ui-sans-serif, system-ui, sans-serif"; -g.fillText("No state history yet", 16, 28); +g.fillText("国家の歴史はまだありません", 16, 28); +setEventLegend(); return; } let minYear = historyWindowStart; @@ -786,17 +755,20 @@ g.textAlign = "center"; g.fillText(formatGraphYear((minYear + maxYear) / 2), paddingLeft + plotWidth / 2, h - 5); g.textAlign = "right"; g.fillText(formatGraphYear(maxYear), w - paddingRight, h - 5); -const topEvents = (sim.graphEvents || []) +const topEvents = graphState.showEventIcons ? (sim.graphEvents || []) .filter(event => event.importance >= 2) .filter(event => event.year >= minYear && event.year <= maxYear) -.filter(event => graphEventTypes[event.type]); +.filter(event => event.type !== "war" || event.data?.outcome === "started") +.filter(event => graphEventTypes[event.type]) : []; const topCollisions = new Map(); for (const event of topEvents) { -const x = yearToX(event.year); -const bucket = Math.round(x / 7); +const rawX = yearToX(event.year); +const bucket = Math.round(rawX / 18); const count = topCollisions.get(bucket) || 0; topCollisions.set(bucket, count + 1); -const y = Math.max(7, paddingTop * 0.5 + ([-3, 0, 3][count % 3])); +const point = spreadGraphMarker(rawX, paddingTop * 0.5, count, paddingLeft, w - paddingRight); +const x = point.x; +const y = point.y; const r = event.importance >= 4 ? 5 : event.importance >= 3 ? 4 : 3; const graphEvent = graphEventTypes[event.type]; g.fillStyle = graphEvent.color; @@ -821,7 +793,7 @@ const history = row.history; g.textAlign = "left"; g.fillStyle = "#a8b1aa"; g.font = "9px ui-sans-serif, system-ui, sans-serif"; -g.fillText(`S${history.id}`, 4, row.y + 3); +g.fillText(`国${history.id}`, 4, row.y + 3); const inactiveMultiplier = history.active ? 1 : 0.65; const strokeSegment = (fromYear, toYear, metricValue, cities, alphaScale = 1) => { if (toYear <= fromYear) return; @@ -869,17 +841,20 @@ strokeSegment(history.visibleFounded, history.ended ?? sim.year, graphSampleValu }); rows.forEach(row => { if (row.type !== "state") return; -const events = (row.history.events || []) +const events = graphState.showEventIcons ? (row.history.events || []) .filter(event => event.importance >= 2) .filter(event => event.year >= minYear && event.year <= maxYear) -.filter(event => graphEventTypes[event.type]); +.filter(event => event.type !== "war" || event.data?.outcome === "started") +.filter(event => graphEventTypes[event.type]) : []; const collisions = new Map(); for (const event of events) { -const x = yearToX(event.year); -const bucket = Math.round(x / 7); +const rawX = yearToX(event.year); +const bucket = Math.round(rawX / 18); const count = collisions.get(bucket) || 0; collisions.set(bucket, count + 1); -const y = row.y + ([-4, 0, 4][count % 3]); +const point = spreadGraphMarker(rawX, row.y, count, paddingLeft, w - paddingRight); +const x = point.x; +const y = point.y; const r = event.importance >= 4 ? 5 : event.importance >= 3 ? 4 : 3; const graphEvent = graphEventTypes[event.type]; g.fillStyle = graphEvent.color; @@ -887,6 +862,113 @@ graphEvent.draw(g, x, y, r); graphState.markers.push({ row, event, x, y, radius: r + 3 }); } }); +setEventLegend(); +} +function renderWorldTrendGraph(minYear, maxYear) { +const canvas = els.worldTrendGraph; +if (!canvas || !sim) return; +const rect = canvas.getBoundingClientRect(); +if (!rect.width) return; +const dpr = window.devicePixelRatio || 1; +const logicalHeight = 176; +const width = Math.max(1, Math.floor(rect.width * dpr)); +const height = Math.max(1, Math.floor(logicalHeight * dpr)); +if (canvas.width !== width || canvas.height !== height) { +canvas.width = width; +canvas.height = height; +} +const g = canvas.getContext("2d"); +g.setTransform(dpr, 0, 0, dpr, 0, 0); +const w = rect.width; +g.clearRect(0, 0, w, logicalHeight); +g.fillStyle = "#101315"; +g.fillRect(0, 0, w, logicalHeight); +const samples = (sim.worldHistory || []).filter(sample => sample.year >= minYear && sample.year <= maxYear); +if (!samples.length) { +g.fillStyle = "#7f8984"; +g.font = "11px ui-sans-serif, system-ui, sans-serif"; +g.fillText("推移データはまだありません", 8, 20); +return; +} +const left = 7; +const right = 7; +const plotWidth = Math.max(1, w - left - right); +const span = Math.max(1, maxYear - minYear); +const xAt = sample => left + ((sample.year - minYear) / span) * plotWidth; +const regional = samples.map(sample => Math.max(0, sample.regionalPopulation || 0)); +const urban = samples.map(sample => Math.max(0, sample.urbanPopulation || 0)); +const populationMaximum = Math.max(1, ...regional.map((value, index) => value + urban[index])); +const populationTop = 22; +const populationBaseline = 91; +const populationHeight = populationBaseline - populationTop; +g.strokeStyle = "rgba(168, 177, 170, 0.13)"; +g.lineWidth = 1; +strokeLine(g, left, populationBaseline, w - right, populationBaseline); +const populationY = value => populationBaseline - value / populationMaximum * populationHeight; +g.beginPath(); +g.moveTo(xAt(samples[0]), populationBaseline); +for (let index = 0; index < samples.length; index++) g.lineTo(xAt(samples[index]), populationY(regional[index])); +g.lineTo(xAt(samples[samples.length - 1]), populationBaseline); +g.closePath(); +g.fillStyle = "rgba(117, 181, 126, 0.42)"; +g.fill(); +g.beginPath(); +g.moveTo(xAt(samples[0]), populationY(regional[0])); +for (let index = 0; index < samples.length; index++) g.lineTo(xAt(samples[index]), populationY(regional[index] + urban[index])); +for (let index = samples.length - 1; index >= 0; index--) g.lineTo(xAt(samples[index]), populationY(regional[index])); +g.closePath(); +g.fillStyle = "rgba(227, 198, 116, 0.58)"; +g.fill(); +g.fillStyle = "#9bc89b"; +g.font = "10px ui-sans-serif, system-ui, sans-serif"; +g.textAlign = "left"; +g.fillText(`地域人口 ${Math.round(regional.at(-1)).toLocaleString()}`, 7, 12); +g.fillStyle = "#e3c674"; +g.fillText(`都市人口 ${Math.round(urban.at(-1)).toLocaleString()}`, Math.max(112, w * 0.5), 12); +const countMetrics = [ +{ key: "cities", label: "都市", color: "#77b9d6" }, +{ key: "polities", label: "国家", color: "#d58b84" }, +{ key: "ethnicities", label: "民族", color: "#b49bd6" } +]; +const countTop = 112; +const countBaseline = 169; +const countHeight = countBaseline - countTop; +const countMaximum = Math.max(1, ...countMetrics.flatMap(metric => samples.map(sample => Math.max(0, sample[metric.key] || 0)))); +strokeLine(g, left, countBaseline, w - right, countBaseline); +for (let metricIndex = 0; metricIndex < countMetrics.length; metricIndex++) { +const metric = countMetrics[metricIndex]; +const values = samples.map(sample => Math.max(0, sample[metric.key] || 0)); +g.fillStyle = metric.color; +g.fillText(`${metric.label} ${Math.round(values.at(-1)).toLocaleString()}`, 7 + metricIndex * Math.max(78, (w - 14) / 3), 105); +g.beginPath(); +for (let index = 0; index < samples.length; index++) { +const x = xAt(samples[index]); +const y = countBaseline - values[index] / countMaximum * countHeight; +if (index === 0) g.moveTo(x, y); +else g.lineTo(x, y); +} +g.strokeStyle = metric.color; +g.lineWidth = 1.6; +g.stroke(); +const lastX = xAt(samples.at(-1)); +const lastY = countBaseline - values.at(-1) / countMaximum * countHeight; +g.beginPath(); +g.arc(lastX, lastY, 2, 0, Math.PI * 2); +g.fillStyle = metric.color; +g.fill(); +} +g.textAlign = "left"; +} +function spreadGraphMarker(x, baseY, count, minX, maxX) { +const xOffsets = [0, -8, 8, -16, 16, -24, 24]; +const yOffsets = [0, -6, 6]; +const positionsPerBand = xOffsets.length * yOffsets.length; +const band = Math.floor(count / positionsPerBand); +const position = count % positionsPerBand; +const xOffset = xOffsets[Math.floor(position / yOffsets.length)]; +const yOffset = yOffsets[position % yOffsets.length]; +const bandOffset = band ? (band * 3) * (x < (minX + maxX) * 0.5 ? 1 : -1) : 0; +return { x: clamp(x + xOffset + bandOffset, minX, maxX), y: baseY + yOffset }; } function drawGraphCircle(g, x, y, r) { g.beginPath(); @@ -910,143 +992,82 @@ function drawGraphTriangleDown(g, x, y, r) { drawGraphPolygon(g, [[x, y + r], [x + r, y - r], [x - r, y - r]]); } const graphEventTypes = { -capitalShift: { -label: "capital shift", -color: "rgba(227, 179, 65, 0.95)", -draw: drawGraphDiamond, -summary: data => `capital #${data.oldCenterCityId ?? "-"} to #${data.newCenterCityId ?? "-"}` -}, -disaster: { -label: "major disaster", -color: "rgba(211, 95, 84, 0.95)", -draw: drawGraphCircle, -summary: data => `lost ${Math.round((data.lossRate || 0) * 100)}%, ${Math.round(data.populationLoss || 0).toLocaleString()} people, ${data.affectedCities || 0} cities` -}, -famine: { -label: "famine", -color: "rgba(227, 128, 65, 0.90)", -draw: drawGraphTriangleDown, -summary: data => `poor cities ${Math.round((data.poorRate || 0) * 100)}%, food ${Number(data.avgPerCapitaFood || 0).toFixed(3)}/cap` -}, -rebellion: { -label: "rebellion", -color: "rgba(236, 126, 111, 0.95)", -draw: drawGraphTriangleUp, -summary: data => data.cityCount > 1 -? `${data.cityCount} cities, pop ${Math.round(data.population || 0).toLocaleString()}, loyalty ${Number(data.loyalty || 0).toFixed(2)}` -: `city #${data.cityId ?? "-"}, pop ${Math.round(data.population || 0).toLocaleString()}, loyalty ${Number(data.loyalty || 0).toFixed(2)}` -}, -newEthnicity: { -label: "new ethnicity", -color: "rgba(105, 181, 120, 0.95)", -draw: drawGraphDiamond, -summary: data => `E${data.ethnicity ?? "-"}, ${Math.round(data.population || 0).toLocaleString()} frontier population` -}, -campaign: { -label: "campaign", -color: "rgba(232, 205, 116, 0.95)", -draw: drawGraphTriangleUp, -summary: data => `${data.outcome || "event"} ${data.campaignType || data.targetType || "target"}` -}, -expansion: { -label: "expansion", -color: "rgba(210, 194, 104, 0.95)", -draw: drawGraphDiamond, -summary: data => `${data.outcome || "event"} ${data.campaignType || "frontier"}, tile ${data.targetTile ?? "-"}` -}, -colonization: { -label: "colonization", -color: "rgba(232, 205, 116, 0.95)", -draw: drawGraphDiamond, -summary: data => `${data.campaignType || "frontier"}, progress ${Math.round((data.progress || 0) * 100)}%` -}, -subjugation: { -label: "subjugation", -color: "rgba(196, 122, 77, 0.95)", -draw: drawGraphTriangleUp, -summary: data => `${data.campaignType || "non-state"}, tile ${data.targetTile ?? "-"}` -}, -annexation: { -label: "annexation", -color: "rgba(227, 179, 65, 0.95)", -draw: drawGraphDiamond, -summary: data => `city #${data.targetCityId ?? "-"}` -}, -failedCampaign: { -label: "failed campaign", -color: "rgba(151, 110, 91, 0.95)", -draw: drawGraphTriangleDown, -summary: data => `${data.outcome || "failed"} ${data.campaignType || "campaign"}` -}, -borderWar: { -label: "border war", -color: "rgba(236, 126, 111, 0.95)", -draw: drawGraphTriangleUp, -summary: data => `${data.outcome || "war"} vs S${data.targetPolityId ?? "-"}` -}, -nomadBand: { -label: "nomad band", -color: "rgba(210, 184, 104, 0.95)", -draw: drawGraphDiamond, -summary: data => `band #${data.bandId ?? "-"}, E${data.ethnicity ?? "-"}, pop ${Math.round(data.population || 0).toLocaleString()}` -}, -nomadTrade: { -label: "nomad trade", -color: "rgba(216, 177, 86, 0.95)", -draw: drawGraphCircle, -summary: data => `band #${data.bandId ?? "-"} with city #${data.cityId ?? "-"}` -}, -nomadRaid: { -label: "nomad raid", -color: "rgba(232, 101, 76, 0.95)", -draw: drawGraphTriangleDown, -summary: data => `band #${data.bandId ?? "-"} raided city #${data.cityId ?? "-"}` -}, -nomadConquest: { -label: "nomad conquest", -color: "rgba(202, 87, 78, 0.95)", -draw: drawGraphTriangleUp, -summary: data => `band #${data.bandId ?? "-"}, city #${data.cityId ?? "-"}, ${data.outcome || "conquest"}` -}, -nomadConfederation: { -label: "nomad confederation", -color: "rgba(191, 162, 91, 0.95)", -draw: drawGraphDiamond, -summary: data => `band #${data.bandId ?? "-"}, pop ${Math.round(data.population || 0).toLocaleString()}` -}, -nomadSplit: { -label: "nomad split", -color: "rgba(151, 130, 91, 0.95)", -draw: drawGraphTriangleDown, -summary: data => `band #${data.bandId ?? "-"} split #${data.newBandId ?? "-"}` -}, -nomadMerge: { -label: "nomad merge", -color: "rgba(170, 145, 91, 0.95)", -draw: drawGraphDiamond, -summary: data => `band #${data.mergedBandId ?? "-"} joined #${data.bandId ?? "-"}` -}, -sedentarization: { -label: "sedentarization", -color: "rgba(202, 169, 102, 0.95)", -draw: drawGraphCircle, -summary: data => `band #${data.bandId ?? "-"} settled ${Math.round(data.population || 0).toLocaleString()} in city #${data.cityId ?? "-"}` -}, -war: { -label: "war", -color: "rgba(202, 87, 78, 0.95)", -draw: drawGraphTriangleUp, -summary: data => `${data.outcome || "war"} ${data.campaignType || ""}` -}, -collapse: { -label: "collapse", -color: "rgba(130, 136, 139, 0.95)", -draw: drawGraphTriangleDown, -summary: data => data.reason || "state ended" -} +capitalShift: { label: "遷都", color: "rgba(227, 179, 65, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("capitalShift", data) }, +disaster: { label: "大災害", color: "rgba(211, 95, 84, 0.95)", draw: drawGraphCircle, summary: data => summarizeEvent("disaster", data) }, +famine: { label: "飢饉", color: "rgba(227, 128, 65, 0.90)", draw: drawGraphTriangleDown, summary: data => summarizeEvent("famine", data) }, +rebellion: { label: "反乱", color: "rgba(236, 126, 111, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("rebellion", data) }, +institutionalReform: { label: "制度改革", color: "rgba(111, 173, 206, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("institutionalReform", data) }, +newEthnicity: { label: "新しい民族", color: "rgba(105, 181, 120, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("newEthnicity", data) }, +campaign: { label: "遠征", color: "rgba(232, 205, 116, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("campaign", data) }, +expansion: { label: "領土拡大", color: "rgba(210, 194, 104, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("expansion", data) }, +colonization: { label: "開拓", color: "rgba(232, 205, 116, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("colonization", data) }, +subjugation: { label: "征服", color: "rgba(196, 122, 77, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("subjugation", data) }, +annexation: { label: "都市の併合", color: "rgba(227, 179, 65, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("annexation", data) }, +failedCampaign: { label: "遠征失敗", color: "rgba(151, 110, 91, 0.95)", draw: drawGraphTriangleDown, summary: data => summarizeEvent("failedCampaign", data) }, +borderWar: { label: "国境戦争", color: "rgba(236, 126, 111, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("borderWar", data) }, +nomadBand: { label: "遊牧集団の誕生", color: "rgba(210, 184, 104, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("nomadBand", data) }, +nomadTrade: { label: "遊牧集団との交易", color: "rgba(216, 177, 86, 0.95)", draw: drawGraphCircle, summary: data => summarizeEvent("nomadTrade", data) }, +nomadRaid: { label: "遊牧集団の襲撃", color: "rgba(232, 101, 76, 0.95)", draw: drawGraphTriangleDown, summary: data => summarizeEvent("nomadRaid", data) }, +nomadConquest: { label: "遊牧集団の征服", color: "rgba(202, 87, 78, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("nomadConquest", data) }, +nomadConfederation: { label: "遊牧連合", color: "rgba(191, 162, 91, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("nomadConfederation", data) }, +nomadSplit: { label: "遊牧集団の分裂", color: "rgba(151, 130, 91, 0.95)", draw: drawGraphTriangleDown, summary: data => summarizeEvent("nomadSplit", data) }, +nomadMerge: { label: "遊牧集団の合流", color: "rgba(170, 145, 91, 0.95)", draw: drawGraphDiamond, summary: data => summarizeEvent("nomadMerge", data) }, +sedentarization: { label: "遊牧集団の定住", color: "rgba(202, 169, 102, 0.95)", draw: drawGraphCircle, summary: data => summarizeEvent("sedentarization", data) }, +war: { label: "戦争", color: "rgba(202, 87, 78, 0.95)", draw: drawGraphTriangleUp, summary: data => summarizeEvent("war", data) }, +collapse: { label: "国家の崩壊", color: "rgba(130, 136, 139, 0.95)", draw: drawGraphTriangleDown, summary: data => summarizeEvent("collapse", data) } }; +function summarizeEvent(type, data) { +if (type === "capitalShift") return `都市${data.oldCenterCityId ?? "-"} → 都市${data.newCenterCityId ?? "-"}`; +if (type === "disaster") return `人口損失 ${Math.round((data.lossRate || 0) * 100)}%・死者 ${Math.round(data.populationLoss || 0).toLocaleString()}人`; +if (type === "famine") return `食料不足の都市 ${Math.round((data.poorRate || 0) * 100)}%`; +if (type === "institutionalReform") { +const before = Math.round((data.before || 0) * 100); +const after = Math.round((data.after || 0) * 100); +const change = Math.round((data.changeRate || 0) * 100); +return `制度老朽化 ${before}% → ${after}%(${change >= 0 ? "+" : ""}${change}%)`; +} +const details = []; +if (data.outcome || data.reason) details.push(displayTerm(data.outcome || data.reason)); +if (data.campaignType) details.push(displayTerm(data.campaignType)); +if (data.bandId != null) details.push(`遊牧集団${data.bandId}`); +if (data.cityId != null || data.targetCityId != null) details.push(`都市${data.cityId ?? data.targetCityId}`); +if (data.targetPolityId != null) details.push(`相手国${data.targetPolityId}`); +if (data.ethnicity != null) details.push(`民族${data.ethnicity}`); +if (data.population != null) details.push(`人口 ${Math.round(data.population).toLocaleString()}`); +if (data.cityCount != null) details.push(`${data.cityCount}都市`); +if (data.newBandId != null) details.push(`新集団${data.newBandId}`); +if (data.mergedBandId != null) details.push(`合流元${data.mergedBandId}`); +if (data.progress != null) details.push(`進捗 ${Math.round(data.progress * 100)}%`); +return details.join("・"); +} +function setEventLegend() { +if (!els.eventLegend) return; +if (!graphState.showEventIcons) { +els.eventLegend.setAttribute("data-event-types", "hidden"); +els.eventLegend.innerHTML = '

    出来事アイコンは非表示です。

    '; +return; +} +const types = [...new Set(graphState.markers.map(marker => marker.event.type))] +.filter(type => graphEventTypes[type]); +const key = types.join(","); +if (els.eventLegend.getAttribute("data-event-types") === key) return; +els.eventLegend.setAttribute("data-event-types", key); +if (!types.length) { +els.eventLegend.innerHTML = '

    現在、歴史グラフに出来事の記号は表示されていません。

    '; +return; +} +els.eventLegend.innerHTML = types.map(type => +`${graphEventTypes[type].label}` +).join(""); +for (const canvas of els.eventLegend.querySelectorAll("canvas")) { +const event = graphEventTypes[canvas.getAttribute("data-event-type")]; +const g = canvas.getContext("2d"); +g.fillStyle = event.color; +event.draw(g, 10, 10, 5); +} +} function graphEventLabel(event) { -return graphEventTypes[event.type]?.label || event.type; +return graphEventTypes[event.type]?.label || "出来事"; } function graphEventSummary(event) { return graphEventTypes[event.type]?.summary(event.data || {}) || ""; @@ -1054,7 +1075,7 @@ return graphEventTypes[event.type]?.summary(event.data || {}) || ""; function showStateGraphMarkerTooltip(marker, pointerX, pointerY) { if (!els.stateGraphTooltip) return; const title = marker.row -? `S${marker.row.history.id} ${graphEventLabel(marker.event)}` +? `国家${marker.row.history.id} ${graphEventLabel(marker.event)}` : graphEventLabel(marker.event); els.stateGraphTooltip.innerHTML = ` ${title} @@ -1090,8 +1111,8 @@ if (!running) render(); } return; } -const status = nearestMarker.row.history.active && nearestMarker.row.history.ended === null ? "living" : "past"; -els.stateGraphInfo.textContent = `S${nearestMarker.row.history.id} ${status} ${formatGraphYear(nearestMarker.event.year)} ${graphEventLabel(nearestMarker.event)}`; +const status = nearestMarker.row.history.active && nearestMarker.row.history.ended === null ? "存続中" : "滅亡済み"; +els.stateGraphInfo.textContent = `国家${nearestMarker.row.history.id} ${status} ${formatGraphYear(nearestMarker.event.year)} ${graphEventLabel(nearestMarker.event)}`; showStateGraphMarkerTooltip(nearestMarker, x, y); if (graphState.hoverPolityId !== nearestMarker.row.history.id) { graphState.hoverPolityId = nearestMarker.row.history.id; @@ -1104,7 +1125,7 @@ const row = graphState.rows .filter(item => item.distance <= item.candidate.height * 0.5); const nearestRow = bestBy(row, item => -item.distance)?.candidate; if (!nearestRow) { -els.stateGraphInfo.textContent = "Hover a row for details"; +els.stateGraphInfo.textContent = ""; hideStateGraphMarkerTooltip(); if (graphState.hoverPolityId !== null) { graphState.hoverPolityId = null; @@ -1127,16 +1148,13 @@ power: nearestRow.peakPower }]; const sample = bestBy(samples, candidate => -Math.abs((candidate.year ?? year) - year)); const metricValue = graphSampleValue(sample, graphState.metric); -const status = nearestRow.history.active && nearestRow.history.ended === null ? "living" : "past"; +const status = nearestRow.history.active && nearestRow.history.ended === null ? "存続中" : "滅亡済み"; els.stateGraphInfo.textContent = -`S${nearestRow.history.id} ${status} ${formatGraphYear(sample.year ?? year)} ` + -`${graphState.metric} ${formatGraphValue(metricValue, graphState.metric)} ` + -`pop ${Math.round(sample.population || 0).toLocaleString()} ` + -`cities ${Math.round(sample.cities || 0).toLocaleString()} ` + -`loyalty ${formatGraphValue(sample.avgLoyalty ?? 0.5, "loyalty")}`; +`国家${nearestRow.history.id} ${status} ${formatGraphYear(sample.year ?? year)} ` + +`${({power: "国力", population: "人口", cities: "都市数", loyalty: "忠誠度"})[graphState.metric]} ${formatGraphValue(metricValue, graphState.metric)}`; } function clearStateGraphInfo() { -if (els.stateGraphInfo) els.stateGraphInfo.textContent = "Hover a row for details"; +if (els.stateGraphInfo) els.stateGraphInfo.textContent = ""; hideStateGraphMarkerTooltip(); if (graphState.hoverPolityId !== null) { graphState.hoverPolityId = null; @@ -1144,12 +1162,12 @@ if (!running) render(); } } function formatGraphYear(week) { -return `${Math.floor(week / WEEKS_PER_YEAR).toLocaleString()}y`; +return `${Math.floor(week / WEEKS_PER_YEAR).toLocaleString()}年`; } function formatSimDate(week) { const year = Math.floor(week / WEEKS_PER_YEAR); const weekOfYear = week % WEEKS_PER_YEAR; const monthOfYear = Math.floor(weekOfYear / WEEKS_PER_MONTH) + 1; const weekOfMonth = weekOfYear % WEEKS_PER_MONTH + 1; -return `${year.toLocaleString()}y ${monthOfYear}m ${weekOfMonth}w`; +return `${year.toLocaleString()}年 ${monthOfYear}月 第${weekOfMonth}週`; } diff --git a/styles.css b/styles.css index 4ca31d4..f159287 100644 --- a/styles.css +++ b/styles.css @@ -26,7 +26,7 @@ body { body { background: var(--bg); color: var(--text); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Yu Gothic", "Meiryo", sans-serif; } button, @@ -64,6 +64,28 @@ select { background: #16191b; } +.current-date { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + padding: 9px 11px; + border: 1px solid var(--line); + border-radius: 7px; + background: #101315; +} + +.current-date span { + color: var(--muted); + font-size: 11px; +} + +.current-date strong { + color: var(--text); + font-size: 14px; + font-variant-numeric: tabular-nums; +} + .history-panel { display: flex; flex-direction: column; @@ -113,7 +135,7 @@ select { } .history-group-controls { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); } .history-group-controls button[aria-pressed="false"] { @@ -130,6 +152,29 @@ select { image-rendering: auto; } +.world-trend-wrap { + flex: none; + margin-bottom: 8px; + border: 1px solid var(--line); + border-radius: 6px; + overflow: hidden; + background: #101315; +} + +.world-trend-wrap h3 { + margin: 0; + padding: 6px 8px 2px; + color: var(--muted); + font-size: 11px; + font-weight: 600; +} + +#worldTrendGraph { + display: block; + width: 100%; + height: 176px; +} + .state-graph-scroll { position: relative; flex: 1; @@ -192,6 +237,23 @@ select { line-height: 1.35; } +.return-link { + display: inline-block; + margin-top: 8px; + padding: 5px 9px; + border: 1px solid #3d464b; + border-radius: 6px; + background: var(--panel-2); + color: var(--text); + font-size: 11px; + text-decoration: none; +} + +.return-link:hover { + border-color: #59656b; + color: var(--accent); +} + .panel { padding: 12px; border: 1px solid var(--line); @@ -206,6 +268,49 @@ select { margin-bottom: 12px; } +.row-two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.intervention-tools { + margin-top: 12px; + padding-top: 11px; + border-top: 1px solid var(--line); +} + +.control-heading { + display: block; + margin-bottom: 7px; + color: var(--muted); + font-size: 12px; +} + +.tool-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} + +.tool-row button { + min-width: 0; + padding: 0 6px; + font-size: 11px; +} + +.tool-row button[aria-pressed="true"] { + border-color: #8b7840; + background: #4b4023; + color: #fff0b1; +} + +.intervention-status { + min-height: 2.6em; + margin: 7px 0 0; + color: var(--muted); + font-size: 10px; + line-height: 1.3; +} + button, select { min-height: 34px; @@ -306,6 +411,14 @@ select { vertical-align: -1px; } +.fps-counter { + margin-top: 8px; + color: var(--muted); + font-size: 10px; + font-variant-numeric: tabular-nums; + text-align: right; +} + .sim { position: relative; display: grid; @@ -345,6 +458,10 @@ canvas#world { font-size: 11px; } +.legend[hidden] { + display: none; +} + .legend span { display: inline-flex; align-items: center; @@ -371,10 +488,42 @@ canvas#world { color: var(--text); font-size: 11px; line-height: 1.35; - pointer-events: auto; + pointer-events: none; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28); } +.map-guide summary { + cursor: pointer; + font-size: 14px; + font-weight: 700; +} +.map-guide h3 { + margin: 14px 0 6px; + font-size: 12px; +} +.map-guide p, +.guide-list { + margin: 6px 0; + color: var(--muted); + font-size: 11px; + line-height: 1.65; +} +.guide-list div { margin-bottom: 8px; } +.guide-list dt { color: var(--text); font-weight: 700; } +.guide-list dd { margin: 2px 0 0; } +.guide-list .guide-gold { color: #ffd75a; } +.guide-list .guide-red { color: #ec7e6f; } +.event-legend { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px 8px; + font-size: 11px; + color: var(--muted); +} +.event-legend span { display: flex; align-items: baseline; gap: 5px; } +.event-legend canvas { width: 20px; height: 20px; flex: 0 0 20px; } +.event-legend .event-legend-empty { grid-column: 1 / -1; } + .tooltip strong { display: block; margin-bottom: 6px; @@ -458,6 +607,51 @@ canvas#world { border-radius: 2px; } +.regenerate-dialog { + width: min(380px, calc(100vw - 32px)); + padding: 0; + border: 1px solid #465158; + border-radius: 9px; + background: var(--panel); + color: var(--text); + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.48); +} + +.regenerate-dialog::backdrop { + background: rgba(0, 0, 0, 0.62); +} + +.regenerate-form { + display: grid; + gap: 13px; + padding: 18px; +} + +.regenerate-form h2 { + margin: 0; + font-size: 17px; +} + +.regenerate-form label { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 12px; +} + +.regenerate-form p { + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.5; +} + +.dialog-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + @media (max-width: 1100px) { body { overflow: auto; diff --git a/tests/modifications.test.cjs b/tests/modifications.test.cjs new file mode 100644 index 0000000..6367f08 --- /dev/null +++ b/tests/modifications.test.cjs @@ -0,0 +1,600 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const root = path.join(__dirname, '..'); +const read = name => fs.readFileSync(path.join(root, name), 'utf8'); +const context = vm.createContext({ console, Date, performance }); +vm.runInContext( + read('config.js') + read('utils.js') + read('engine.js') + + ';globalThis.WorldClass = World;globalThis.SimulationClass = Simulation;globalThis.Settings = SimConfig;', + context +); + +function findConnectedLandPair(sim, minDistance = 6, maxDistance = 20) { + const w = sim.world; + const start = w.terrain.findIndex(t => t !== 3); + assert.ok(start >= 0, 'land tile required'); + const queue = [start]; + const seen = new Set([start]); + while (queue.length) { + const tile = queue.shift(); + const distance = Math.abs(tile % w.size - start % w.size) + Math.abs(Math.floor(tile / w.size) - Math.floor(start / w.size)); + if (distance >= minDistance && distance <= maxDistance) return [start, tile]; + const begin = w.landNeighborStarts[tile]; + const end = w.landNeighborStarts[tile + 1]; + for (let i = begin; i < end; i++) { + const next = w.landNeighbors[i]; + if (!seen.has(next)) { seen.add(next); queue.push(next); } + } + } + throw new Error('connected land pair not found'); +} + +test('UIから1週進行と初期人口スライダーを削除し、再生成ダイアログと介入ボタンを備える', () => { + const html = read('index.html'); + assert.equal(html.includes('id="stepOnce"'), false); + assert.equal(html.includes('id="agentCount"'), false); + assert.ok(html.includes('id="regenerateDialog"')); + assert.ok(html.includes('id="worldSize"')); + assert.ok(html.includes('id="triggerDisaster"')); + assert.ok(html.includes('id="buildCity"')); + assert.ok(html.includes('id="connectCities"')); +}); + +test('水域の肥沃度は常に0として生成する', () => { + const sim = new context.SimulationClass(32, 1000); + let water = 0; + for (let i = 0; i < sim.world.count; i++) { + if (sim.world.terrain[i] !== 3) continue; + water++; + assert.equal(sim.world.fertility[i], 0); + } + assert.ok(water > 0); +}); + +test('忠誠度30%未満で反乱が自動発火し、50%未満の都市が全て同時参加する', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, centerCityId: 1 }; + const cities = [ + { id: 1, polityId: 1, loyalty: 1, population: 1000 }, + { id: 2, polityId: 1, loyalty: 0.29, population: 600 }, + { id: 3, polityId: 1, loyalty: 0.49, population: 500 }, + { id: 4, polityId: 1, loyalty: 0.50, population: 400 }, + { id: 5, polityId: 1, loyalty: 0.72, population: 300 } + ]; + const events = []; + Object.assign(sim, { + polities: [polity], cities, year: 100, rng: { next: () => 0.999 }, + getPolityCities: p => cities.filter(city => city.polityId === p.id), + polityInstability: () => 0, + polityAgePressure: () => 0, + removeCityFromPolity: city => { city.polityId = null; }, + releaseCityTerritory: () => {}, + createPolity: trigger => { const rebel = { id: 2, centerCityId: trigger.id }; trigger.polityId = 2; return rebel; }, + transferCityToPolity: (city, rebel, options) => { city.polityId = rebel.id; city.loyalty = options.loyalty; return true; }, + claimPolityCoreTerritory: () => [], + addPolityEvent: (id, type, year, data) => events.push({ id, type, year, data }) + }); + sim.splitUnloyalCities(); + assert.equal(cities[1].polityId, 2); + assert.equal(cities[2].polityId, 2); + assert.equal(cities[3].polityId, 1); + assert.equal(cities[4].polityId, 1); + assert.equal(events[0].data.automatic, true); + assert.deepEqual([...events[0].data.cityIds].sort((a, b) => a - b), [2, 3]); +}); + +test('ユーザー建設都市は即時存続でき、手動交易路は保護期間終了後に通常管理へ戻る', () => { + const sim = new context.SimulationClass(32, 1000); + const [aTile, bTile] = findConnectedLandPair(sim); + const a = sim.buildCityAt(aTile % 32, Math.floor(aTile / 32)); + const b = sim.buildCityAt(bTile % 32, Math.floor(bTile / 32)); + assert.equal(a.ok, true); + assert.equal(b.ok, true); + assert.ok(a.city.population > 100); + assert.ok(b.city.population > 100); + const route = sim.createManualTradeRoute(a.city.id, b.city.id); + assert.equal(route.ok, true); + assert.equal(sim.manualTradeRoutes.length, 1); + assert.ok(route.path.some(tile => sim.isManualRouteTileProtected(tile))); + sim.year = route.protectedUntil; + sim.cleanupManualTradeRouteProtection(); + assert.equal(sim.manualTradeRoutes.length, 0); + assert.equal(route.path.some(tile => sim.isManualRouteTileProtected(tile)), false); +}); + +test('戦争人口損害は旧値の約65%に軽減される', () => { + const sim = Object.create(context.SimulationClass.prototype); + Object.assign(sim, { year: 0, deaths: 0 }); + sim.recordWarDeaths = () => {}; + const city = { population: 1000, storedResources: 500, loyalty: 1, ethnicityComposition: new Map() }; + const loss = sim.applyCityWarDamage(city, 0.10, 'war'); + assert.equal(loss, 65); + assert.equal(city.population, 935); +}); + +test('手動災害は指定地点で即時に履歴へ記録される', () => { + const sim = new context.SimulationClass(24, 800); + const before = sim.disasterHistory.length; + const result = sim.triggerManualDisasterAt(12, 12); + assert.equal(result.ok, true); + assert.equal(sim.disasterHistory.length, before + 1); + assert.equal(result.record.x, 12); + assert.equal(result.record.y, 12); +}); + + +test('介入は成功後も選択状態を維持し、連続実行できるUI実装になっている', () => { + const boot = read('boot.js'); + assert.equal(boot.includes('interventionMode = null;\nels.triggerDisaster?.setAttribute("aria-pressed", "false")'), false); + assert.equal(boot.includes('interventionMode = null;\nels.buildCity?.setAttribute("aria-pressed", "false")'), false); + assert.equal(boot.includes('interventionMode = null;\nels.connectCities?.setAttribute("aria-pressed", "false")'), false); + assert.ok(boot.includes('続けて地点をクリックできます。')); + assert.ok(boot.includes('続けて建設地点をクリックできます。')); + assert.ok(boot.includes('続けて1つ目の都市をクリックできます。')); +}); + +test('ヘルプを整理し、交易路保護期間の案内文を表示しない', () => { + const html = read('index.html'); + const boot = read('boot.js'); + assert.ok(html.includes('ヘルプ')); + assert.ok(html.includes('連続実行できます')); + assert.equal(html.includes('ヘルプ:地図・記号の見方'), false); + assert.equal(boot.includes('交易路は${result.protectionYears}年間保護され、その後は自然消滅する可能性があります。'), false); +}); + +test('砂漠横断の交易路は距離上限・通行コスト・建設コストで過度に不利にならない', () => { + const sim = new context.SimulationClass(32, 1000); + const y = 16; + const path = []; + for (let x = 4; x <= 27; x++) { + const tile = sim.world.idx(x, y); + sim.world.terrain[tile] = 4; + sim.world.move[tile] = 1.55; + sim.world.tradeRoute[tile] = 0; + sim.world.pheromone[tile] = 0; + path.push(tile); + } + const a = { id: 9001, x: 4, y, population: 80, polityId: null, knowledge: { farming: 0, metallurgy: 0 } }; + const b = { id: 9002, x: 27, y, population: 80, polityId: null, knowledge: { farming: 0, metallurgy: 0 } }; + const desertShare = sim.desertCorridorShare(a, b); + const desertLimit = sim.effectiveRouteLengthLimit(a, b, null); + const desertCost = sim.routeConstructionCost(path, false); + const desertTileCost = sim.routeTileCost(path[Math.floor(path.length / 2)]); + assert.ok(desertShare > 0.8); + assert.ok(desertTileCost < 1.2, `desert tile route cost=${desertTileCost}`); + + for (const tile of path) { + sim.world.terrain[tile] = 0; + sim.world.move[tile] = 1.0; + } + const plainLimit = sim.effectiveRouteLengthLimit(a, b, null); + const plainCost = sim.routeConstructionCost(path, false); + assert.ok(desertLimit > plainLimit, `desert=${desertLimit}, plain=${plainLimit}`); + assert.ok(desertCost < plainCost, `desert=${desertCost}, plain=${plainCost}`); +}); + +test('ユーザー建設都市には必ず民族構成が設定される', () => { + const sim = new context.SimulationClass(32, 1000); + let tile = -1; + for (let i = 0; i < sim.world.count; i++) { + if (sim.world.terrain[i] === 3) continue; + const x = i % sim.world.size; + const y = Math.floor(i / sim.world.size); + if (!sim.getCitiesNear(x, y, 4).length) { tile = i; break; } + } + assert.ok(tile >= 0, 'buildable land tile required'); + const result = sim.buildCityAt(tile % sim.world.size, Math.floor(tile / sim.world.size)); + assert.equal(result.ok, true); + assert.equal(typeof result.city.ethnicityComposition?.size, 'number'); + assert.ok(result.city.ethnicityComposition.size > 0); + const total = [...result.city.ethnicityComposition.values()].reduce((sum, value) => sum + value, 0); + assert.equal(total, result.city.population); +}); + +test('征服された都市は20年間反乱から保護され、期間終了後は通常判定へ戻る', () => { + const sim = Object.create(context.SimulationClass.prototype); + const city = { id: 2, x: 1, y: 1, polityId: 1, loyalty: 0.22, population: 500, rebellionProtectedUntil: 0 }; + const oldPolity = { id: 1, centerCityId: 1, cityIds: new Set([1, 2]) }; + const newPolity = { id: 2, centerCityId: 3, cityIds: new Set([3]) }; + Object.assign(sim, { + year: 100, + world: { idx: () => 0 }, + polities: [oldPolity, newPolity], + cities: [city], + removeCityFromPolity: target => { oldPolity.cityIds.delete(target.id); target.polityId = null; }, + addCityToPolity: (target, polity, loyalty) => { target.polityId = polity.id; target.loyalty = loyalty; polity.cityIds.add(target.id); }, + transferTerritoryTiles: () => [] + }); + sim.transferCityToPolity(city, newPolity, { loyalty: 0.22, reason: 'warCapture', allowForeignTakeover: true }); + assert.equal(city.rebellionProtectedUntil, 100 + 20 * 48); + assert.equal(sim.isCityRebellionProtected(city), true); + sim.year = city.rebellionProtectedUntil - 1; + assert.equal(sim.isCityRebellionProtected(city), true); + sim.year = city.rebellionProtectedUntil; + assert.equal(sim.isCityRebellionProtected(city), false); +}); + +test('反乱発生時も征服保護中の低忠誠都市は反乱に参加しない', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, centerCityId: 1 }; + const cities = [ + { id: 1, polityId: 1, loyalty: 1, population: 1000, rebellionProtectedUntil: 0 }, + { id: 2, polityId: 1, loyalty: 0.20, population: 600, rebellionProtectedUntil: 0 }, + { id: 3, polityId: 1, loyalty: 0.18, population: 500, rebellionProtectedUntil: 1000 } + ]; + Object.assign(sim, { + polities: [polity], cities, year: 100, rng: { next: () => 0.999 }, + getPolityCities: p => cities.filter(city => city.polityId === p.id), + polityInstability: () => 0, + polityAgePressure: () => 0, + removeCityFromPolity: city => { city.polityId = null; }, + releaseCityTerritory: () => {}, + createPolity: trigger => { const rebel = { id: 2, centerCityId: trigger.id }; trigger.polityId = 2; return rebel; }, + transferCityToPolity: (city, rebel, options) => { city.polityId = rebel.id; city.loyalty = options.loyalty; return true; }, + claimPolityCoreTerritory: () => [], + addPolityEvent: () => {} + }); + sim.splitUnloyalCities(); + assert.equal(cities[1].polityId, 2); + assert.equal(cities[2].polityId, 1); +}); + +test('制度老朽化は毎年増加し、その分だけ忠誠度上限を低下させる', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, founded: 0, institutionalAging: 0.25 }; + Object.assign(sim, { year: 48 * 100, polities: [polity] }); + const before = polity.institutionalAging; + sim.updateInstitutionalAging(); + assert.ok(polity.institutionalAging > before); + assert.equal(Number(sim.polityLoyaltyCap(polity).toFixed(4)), Number((1 - polity.institutionalAging).toFixed(4))); + + const center = { id: 1, polityId: 1, loyalty: 1, population: 1000, storedResources: 100 }; + const city = { id: 2, polityId: 1, loyalty: 1, population: 500, storedResources: 100, supplyStress: 0 }; + polity.centerCityId = 1; + Object.assign(sim, { + cities: [center, city], + getCityById: id => id === 1 ? center : id === 2 ? city : null, + getPolityCities: () => [center, city], + polityInstability: () => 0, + polityAgePressure: () => 0, + polityOverextension: () => 0, + polityLogisticsStress: () => 0, + sameDominantEthnicity: () => true, + effectiveDistance: () => 1, + hasDirectTradeConnection: () => true, + tradeAccessLoyaltyPenalty: () => 0 + }); + sim.updateCityLoyalty(); + const cap = sim.polityLoyaltyCap(polity); + assert.equal(center.loyalty, cap); + assert.ok(city.loyalty <= cap); +}); + +test('制度改革は老朽化30%を超えてから発生し、-50%から+30%の相対変動を履歴へ記録する', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 7, founded: 0, institutionalAging: 0.30, lastInstitutionalReformYear: 0 }; + const events = []; + Object.assign(sim, { + year: 48 * 100, + polities: [polity], + rng: { next: () => 0, range: (min, max) => min }, + addPolityEvent: (id, type, year, data, importance) => events.push({ id, type, year, data, importance }) + }); + assert.equal(context.Settings.polity.institutionalReformThreshold, 0.30); + sim.maybeTriggerInstitutionalReforms(); + assert.equal(events.length, 0); + assert.equal(polity.institutionalAging, 0.30); + + polity.institutionalAging = 0.31; + sim.maybeTriggerInstitutionalReforms(); + assert.equal(Number(polity.institutionalAging.toFixed(3)), 0.155); + assert.equal(events.length, 1); + assert.equal(events[0].type, 'institutionalReform'); + assert.equal(events[0].importance, 3); + assert.equal(events[0].data.changeRate, -0.5); + assert.equal(events[0].data.before, 0.31); + assert.equal(events[0].data.after, 0.155); + assert.equal(context.Settings.polity.institutionalReformCooldownYears, 50); + + polity.institutionalAging = 0.31; + sim.year = 48 * 149; + sim.maybeTriggerInstitutionalReforms(); + assert.equal(events.length, 1, '前回改革から50年未満では再改革しない'); + sim.year = 48 * 150; + sim.maybeTriggerInstitutionalReforms(); + assert.equal(events.length, 2, '50年経過後は再改革できる'); +}); + +test('国庫が上限に達した国家は確率判定で国庫を消費して都市を建設する', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, cityIds: new Set([1]), treasury: 99 }; + const world = { size: 16, city: new Int32Array(256).fill(-1) }; + const cityById = new Map(); + const cityBuckets = new Map(); + let nextCity = 2; + Object.assign(sim, { + polities: [polity], cities: [{ id: 1, x: 2, y: 2, polityId: 1, population: 1000 }], + world, cityById, cityBuckets, year: 48 * 200, rng: { next: () => 0 }, + polityTreasuryCap: () => 100, + findTreasuryCityFoundingSite: () => 5 + 5 * 16, + manualCityEthnicitySeed: () => new Map([[1, 1]]), + createCity: (x, y, _seed, options = {}) => ({ id: nextCity++, x, y, population: options.initialPopulation ?? 400, storedResources: options.initialResources ?? 340, ethnicityComposition: new Map(), polityId: null, loyalty: 0.5 }), + applyCityEthnicitySeed: (city, seed, population) => { city.ethnicityComposition = new Map([[1, population]]); }, + cityBucketKey: () => 0, + addCityToPolity: (city, state, loyalty) => { city.polityId = state.id; city.loyalty = loyalty; state.cityIds.add(city.id); }, + claimPolityCoreTerritory: () => {}, + rebuildIndexes: () => {}, updatePopulationCapacity: () => {}, syncTilePopulationCulture: () => {} + }); + assert.equal(context.Settings.polity.treasuryCityFoundingChance, 0.14); + assert.equal(context.Settings.polity.treasuryCityFoundingCostRatio, 0.35); + assert.equal(sim.maybeBuildTreasuryCities(), 0); + assert.equal(sim.cities.length, 1); + + polity.treasury = 100; + assert.equal(sim.maybeBuildTreasuryCities(), 1); + assert.equal(sim.cities.length, 2); + assert.equal(polity.treasury, 65); + assert.ok(sim.cities[1].population > 400); + assert.ok(sim.cities[1].storedResources > 340); + assert.equal(sim.cities[1].polityId, 1); + assert.equal(sim.cities[1].ethnicityComposition.get(1), sim.cities[1].population); +}); + +test('国家興亡グラフに制度改革イベントと老朽化変動を表示する', () => { + const render = read('render.js'); + assert.ok(render.includes('institutionalReform: { label: "制度改革"')); + assert.ok(render.includes('制度老朽化 ${before}% → ${after}%')); +}); + +test('ヘルプに主要パラメータの簡単な説明を表示する', () => { + const html = read('index.html'); + assert.ok(html.includes('

    主なパラメータ

    ')); + for (const label of ['人口 / 人口圧力', '資源 / 肥沃度', '農業 / 冶金', '交通量', '忠誠度', '国庫', '正統性 / 結束', '制度老朽化', '国力']) { + assert.ok(html.includes(`
    ${label}
    `), `${label} の説明が必要`); + } +}); + +test('国庫上限は国家人口に比例し、超過分を保持しない', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, cityIds: new Set([1, 2]), treasury: 999 }; + const cities = [ + { id: 1, polityId: 1, population: 1200 }, + { id: 2, polityId: 1, population: 800 } + ]; + Object.assign(sim, { + cities, + getPolityCities: () => cities + }); + assert.equal(context.Settings.polity.treasuryCapPerPopulation, 0.30); + assert.equal(sim.polityTreasuryCap(polity), 600); + sim.enforcePolityTreasuryCap(polity); + assert.equal(polity.treasury, 600); + cities[1].population = 300; + assert.equal(sim.polityTreasuryCap(polity), 450); + sim.enforcePolityTreasuryCap(polity); + assert.equal(polity.treasury, 450); +}); + +test('反乱時は反乱人口比に応じて国庫の一部が反乱国へ移る', () => { + const sim = Object.create(context.SimulationClass.prototype); + const polity = { id: 1, centerCityId: 1, cityIds: new Set([1, 2]), treasury: 300 }; + const rebelPolity = { id: 2, centerCityId: 2, cityIds: new Set([2]), treasury: 0 }; + const cities = [ + { id: 1, polityId: 1, loyalty: 1, population: 1000, storedResources: 0, rebellionProtectedUntil: 0 }, + { id: 2, polityId: 1, loyalty: 0.20, population: 1000, storedResources: 0, rebellionProtectedUntil: 0 } + ]; + const events = []; + Object.assign(sim, { + polities: [polity], cities, year: 100, + rng: { next: () => 0.999 }, + getPolityCities: p => cities.filter(city => city.polityId === p.id), + polityInstability: () => 0, + polityAgePressure: () => 0, + removeCityFromPolity: city => { city.polityId = null; polity.cityIds.delete(city.id); }, + releaseCityTerritory: () => {}, + createPolity: trigger => { trigger.polityId = 2; return rebelPolity; }, + transferCityToPolity: (city, rebel, options) => { city.polityId = rebel.id; rebel.cityIds.add(city.id); city.loyalty = options.loyalty; return true; }, + claimPolityCoreTerritory: () => [], + addPolityEvent: (id, type, year, data) => events.push({ id, type, year, data }) + }); + assert.equal(context.Settings.rebellion.treasuryTransferRate, 0.65); + sim.splitUnloyalCities(); + const expected = 300 * 0.5 * 0.65; + assert.equal(Number(rebelPolity.treasury.toFixed(3)), Number(expected.toFixed(3))); + assert.equal(Number(polity.treasury.toFixed(3)), Number((300 - expected).toFixed(3))); + assert.equal(Number(events[0].data.treasuryTransferred.toFixed(3)), Number(expected.toFixed(3))); +}); + +test('自然都市上限を大幅増加し、手動都市は近接・上限制限を無視するが水域には建設できない', () => { + assert.equal(context.Settings.city.maxCities, 1200); + assert.equal(context.Settings.city.naturalFoundingMinDistance, 6); + assert.equal(context.Settings.city.foundingCompetitionRadius, 9); + const sim = new context.SimulationClass(32, 1000); + const water = sim.world.terrain.findIndex(tile => tile === 3); + assert.ok(water >= 0, 'water tile required'); + const wx = water % sim.world.size; + const wy = Math.floor(water / sim.world.size); + const rejected = sim.buildCityAt(wx, wy); + assert.equal(rejected.ok, false); + assert.equal(rejected.reason, 'water'); + + let land = -1; + for (let i = 0; i < sim.world.count; i++) { + if (sim.world.terrain[i] !== 3) { land = i; break; } + } + assert.ok(land >= 0, 'land tile required'); + const x = land % sim.world.size; + const y = Math.floor(land / sim.world.size); + const first = sim.buildCityAt(x, y); + assert.equal(first.ok, true); + let adjacent = -1; + for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) { + const nx = x + dx, ny = y + dy; + if (nx < 0 || ny < 0 || nx >= sim.world.size || ny >= sim.world.size) continue; + if (sim.world.terrain[sim.world.idx(nx, ny)] !== 3) { adjacent = sim.world.idx(nx, ny); break; } + } + if (adjacent >= 0) { + const second = sim.buildCityAt(adjacent % sim.world.size, Math.floor(adjacent / sim.world.size)); + assert.equal(second.ok, true); + assert.ok(Math.abs(second.city.x - first.city.x) + Math.abs(second.city.y - first.city.y) <= 1); + } +}); + +test('交通量拡散は疎更新対象を追跡し、空の世界では全域拡散を行わない', () => { + const sim = new context.SimulationClass(32, 1000); + assert.equal(sim.activePheromoneTiles.size, 0); + const tile = sim.world.terrain.findIndex(t => t !== 3); + assert.ok(tile >= 0); + sim.addPheromone(tile, 4); + assert.ok(sim.activePheromoneTiles.has(tile)); + const before = sim.world.pheromone[tile]; + sim.updatePheromoneField(2); + assert.ok(sim.world.pheromone[tile] < before); + assert.ok(sim.activePheromoneTiles.size > 0); +}); + +test('国家興亡グラフに出来事アイコン表示切替を備え、国庫は一重表示にする', () => { + const html = read('index.html'); + const boot = read('boot.js'); + const render = read('render.js'); + assert.ok(html.includes('id="toggleEventIcons"')); + assert.ok(boot.includes('graphState.showEventIcons = !graphState.showEventIcons')); + assert.ok(render.includes('graphState.showEventIcons ? (sim.graphEvents || [])')); + assert.ok(render.includes('tooltipRow("国庫", Math.max(0, displayPolity.treasury || 0).toFixed(1))')); + assert.equal(render.includes('tooltipRow("国庫", `${Math.max(0, displayPolity.treasury || 0).toFixed(1)} /'), false); +}); + +test('自然都市の創設流量は5年周期でマップ面積に応じて増える', () => { + assert.equal(context.Settings.city.naturalFoundingStartYears, 160); + assert.equal(context.Settings.city.naturalFoundingIntervalYears, 5); + const sim = Object.create(context.SimulationClass.prototype); + sim.world = { count: 160 * 160 }; + assert.equal(sim.naturalCityFoundingLimit(), 1); + sim.world = { count: 220 * 220 }; + assert.equal(sim.naturalCityFoundingLimit(), 2); + sim.world = { count: 280 * 280 }; + assert.equal(sim.naturalCityFoundingLimit(), 4); +}); + +test('都市は人口400で成立し、200未満で消滅する', () => { + assert.equal(context.Settings.city.initialPopulation, 400); + assert.equal(context.Settings.city.abandonmentPopulation, 200); + const sim = new context.SimulationClass(32, 1000); + const land = sim.world.terrain.findIndex(tile => tile !== 3); + assert.ok(land >= 0, 'land tile required'); + const result = sim.buildCityAt(land % sim.world.size, Math.floor(land / sim.world.size)); + assert.equal(result.ok, true); + assert.equal(result.city.population, 400); + + result.city.population = 199; + result.city.strength = 3; + result.city.storedResources = 100; + result.city.activeVisitors = 10; + sim.processCityEconomies = () => {}; + sim.updateCities(); + assert.equal(sim.cities.some(city => city.id === result.city.id), false); + + const simAtThreshold = new context.SimulationClass(32, 1000); + const secondLand = simAtThreshold.world.terrain.findIndex(tile => tile !== 3); + const thresholdCity = simAtThreshold.buildCityAt(secondLand % simAtThreshold.world.size, Math.floor(secondLand / simAtThreshold.world.size)); + thresholdCity.city.population = 200; + thresholdCity.city.strength = 3; + thresholdCity.city.storedResources = 100; + thresholdCity.city.activeVisitors = 10; + simAtThreshold.processCityEconomies = () => {}; + simAtThreshold.updateCities(); + assert.equal(simAtThreshold.cities.some(city => city.id === thresholdCity.city.id), true); +}); + +test('民族分化の時間スケールを1/4にし、極小民族は隣接民族へ吸収する', () => { + assert.equal(context.Settings.culture.divergenceRateMultiplier, 0.25); + assert.equal(context.Settings.culture.absorptionPopulationThreshold, 30); + const sim = new context.SimulationClass(24, 500); + sim.ethnicities.clear(); + sim.tileEthnicMix.clear(); + sim.cities = []; + sim.nomadBands = []; + sim.nextEthnicity = 1; + sim.year = 0; + sim.rng = { next: () => 0.5, range: (min, max) => (min + max) / 2 }; + const targetId = sim.createEthnicity(null, { temperature: 0.5, humidity: 0.5 }); + const lowId = sim.createEthnicity(targetId, { temperature: 0.5, humidity: 0.5 }); + const expectedMinDelay = 18 / 0.25 * 48; + assert.ok(sim.ethnicities.get(lowId).nextDivergenceCheck >= expectedMinDelay); + + const [lowTile, targetTile] = findConnectedLandPair(sim, 1, 1); + sim.tileEthnicMix.set(lowTile, new Map([[lowId, 10]])); + sim.tileEthnicMix.set(targetTile, new Map([[targetId, 100]])); + sim.world.population[lowTile] = 10; + sim.world.population[targetTile] = 100; + sim.updateCultureTile(lowTile); + sim.updateCultureTile(targetTile); + sim.updateEthnicStats(); + assert.ok(sim.ethnicities.get(lowId).population < context.Settings.culture.absorptionPopulationThreshold); + assert.equal(sim.absorbSmallEthnicities(), 1); + assert.equal(sim.ethnicities.has(lowId), false); + assert.equal(sim.tileEthnicMix.get(lowTile).get(targetId), 10); + assert.equal(sim.tileEthnicMix.get(lowTile).has(lowId), false); +}); + +test('再生成ダイアログから初期人口固定の説明を削除する', () => { + const html = read('index.html'); + assert.equal(html.includes('初期人口は標準値'), false); +}); + + +test('国家の都市建設投資は消費額が大きいほど初期人口と初期備蓄を増やす', () => { + const sim = Object.create(context.SimulationClass.prototype); + const low = sim.treasuryCityInvestmentProfile(100); + const mid = sim.treasuryCityInvestmentProfile(500); + const high = sim.treasuryCityInvestmentProfile(2000); + assert.ok(low.population >= context.Settings.city.initialPopulation); + assert.ok(low.population < mid.population && mid.population < high.population); + assert.ok(low.resources < mid.resources && mid.resources < high.resources); +}); + +test('国家の都市建設候補は広い適地全体から加重抽選され、特定地点へ固定されない', () => { + const sim = Object.create(context.SimulationClass.prototype); + const size = 40; + const count = size * size; + const fillFloat = value => { const array = new Float32Array(count); array.fill(value); return array; }; + const world = { + size, count, + terrain: new Uint8Array(count), + city: new Int32Array(count).fill(-1), + territoryOwner: new Int32Array(count).fill(1), + fertility: fillFloat(0.6), resource: fillFloat(20), populationPressure: fillFloat(0.5), + farmland: fillFloat(0.2), control: fillFloat(0.7), pheromone: fillFloat(0), move: fillFloat(1), + tradeRoute: new Uint8Array(count), + idx: (x, y) => y * size + x + }; + let state = 123456789; + Object.assign(sim, { + world, + radiusOffsetCache: new Map(), + getPolityCities: () => [{ x: 20, y: 20 }], + getCitiesNear: () => [], + rng: { next: () => { state = (1664525 * state + 1013904223) >>> 0; return state / 4294967296; } } + }); + const picks = Array.from({ length: 20 }, () => sim.findTreasuryCityFoundingSite({ id: 1 })); + assert.ok(new Set(picks).size >= 12, '同等適地から十分に分散して選ばれる必要がある'); +}); + +test('都市老朽化を削除し、人口回復は人口減少幅に応じて働く', () => { + const engineSource = read('engine.js'); + assert.equal('agingStartYears' in context.Settings.city, false); + assert.equal('agingDecayPerYear' in context.Settings.city, false); + assert.equal('agingCapacityPenaltyMax' in context.Settings.city, false); + assert.equal(engineSource.includes('updateCityAging'), false); + assert.equal(engineSource.includes('ageWear'), false); + + const sim = Object.create(context.SimulationClass.prototype); + const stable = sim.cityRecoveryGrowthBonus({ population: 1000, peakPopulation: 1000 }); + const damaged = sim.cityRecoveryGrowthBonus({ population: 500, peakPopulation: 1000 }); + assert.equal(stable, 0); + assert.ok(damaged > stable); +}); diff --git a/tests/simulation.test.cjs b/tests/simulation.test.cjs new file mode 100644 index 0000000..3c9d2b6 --- /dev/null +++ b/tests/simulation.test.cjs @@ -0,0 +1,478 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const root = path.join(__dirname, '..'); +const read = name => fs.readFileSync(path.join(root, name), 'utf8'); +const engine = vm.createContext({ console, Date, performance }); +vm.runInContext(read('config.js') + read('utils.js') + read('engine.js') + + ';globalThis.WorldClass = World;globalThis.SimulationClass = Simulation;globalThis.SimSettings = SimConfig;', engine); + +function fixture() { + const w = Object.create(engine.WorldClass.prototype); + Object.assign(w, { size: 16, count: 256, terrain: new Uint8Array(256), + territoryOwner: new Int32Array(256).fill(-1), polity: new Int32Array(256).fill(-1), + city: new Int32Array(256).fill(-1), tradeRoute: new Uint8Array(256), + control: new Float32Array(256), claim: new Float32Array(256), contested: new Uint8Array(256) }); + w.buildLandNeighbors(); + const sim = Object.create(engine.SimulationClass.prototype); + Object.assign(sim, { world: w, scratchIntA: new Int32Array(256), scratchUintA: new Uint8Array(256), + cities: [], polities: [{ id: 1 }, { id: 2 }], polityById: new Map(), territorialClaims: new Map(), radiusOffsetCache: new Map(), + visitStamp: new Int32Array(256), nextVisitStamp: 1, territoryDebug: { disconnectedTerritoryRemoved: 0 } }); + sim.getPolityById = id => sim.polities.find(p => p.id === id); + const set = (x, y, owner = 1) => { w.territoryOwner[w.idx(x, y)] = owner; w.polity[w.idx(x, y)] = owner; }; + const rect = (x1, y1, x2, y2) => { for (let y = y1; y <= y2; y++) for (let x = x1; x <= x2; x++) set(x, y); }; + return { sim, w, set, rect }; +} + +test('国境の小さな穴・凹み・突起を整える', () => { + const { sim, w, set, rect } = fixture(); + rect(3, 3, 11, 11); + set(7, 7, -1); set(3, 7, -1); set(12, 7); + sim.smoothTerritoryBorders(); + assert.equal(w.polity[w.idx(7, 7)], 1); + assert.equal(w.polity[w.idx(3, 7)], 1); + assert.equal(w.polity[w.idx(12, 7)], -1); + assert.deepEqual(w.polity, w.territoryOwner); +}); + +test('細い連絡路・都市・交易路を保護する', () => { + const { sim, w, set, rect } = fixture(); + rect(2, 4, 5, 7); rect(10, 4, 13, 7); + for (let x = 6; x <= 9; x++) set(x, 5); + set(3, 8); sim.cities.push({ x: 3, y: 8 }); + set(12, 8); w.tradeRoute[w.idx(12, 8)] = 100; + sim.smoothTerritoryBorders(); + for (let x = 6; x <= 9; x++) assert.equal(w.polity[w.idx(x, 5)], 1); + assert.equal(w.polity[w.idx(3, 8)], 1); + assert.equal(w.polity[w.idx(12, 8)], 1); +}); + +test('外国の都市と海岸を保護する', () => { + const { sim, w, set, rect } = fixture(); + rect(3, 3, 11, 11); + set(7, 7, -1); set(6, 7, 2); + sim.cities.push({ x: 6, y: 7 }); + w.terrain[w.idx(12, 8)] = 3; + w.buildLandNeighbors(); + const original = w.terrain.slice(); + sim.smoothTerritoryBorders(); + assert.equal(w.polity[w.idx(6, 7)], 2); + assert.equal(w.polity[w.idx(7, 7)], -1); + assert.deepEqual(w.terrain, original); +}); + +test('国家同士の境界でも小さな突起を整える', () => { + const { sim, w, set, rect } = fixture(); + rect(2, 2, 13, 13); + for (let y = 2; y <= 13; y++) for (let x = 9; x <= 13; x++) set(x, y, 2); + set(8, 7, 2); + sim.smoothTerritoryBorders(); + assert.equal(w.polity[w.idx(8, 7)], 1); + assert.equal(w.polity[w.idx(9, 7)], 2); + assert.equal(sim.territorialClaims.get(w.idx(8, 7)).polityId, 1); +}); + +test('通常更新でも都市に接続されていない領土と保存済みの主張を除去する', () => { + const { sim, w, rect } = fixture(); + rect(2, 2, 5, 5); rect(10, 10, 12, 12); + sim.cities = [{ id: 1, x: 3, y: 3, population: 1000, polityId: 1 }]; + sim.territorialClaims.set(w.idx(11, 11), { polityId: 1, claim: 0.8 }); + sim.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); + assert.equal(w.polity[w.idx(3, 3)], 1); + assert.equal(w.polity[w.idx(11, 11)], -1); + assert.equal(w.control[w.idx(11, 11)], 0); + assert.equal(sim.territorialClaims.has(w.idx(11, 11)), false); + assert.deepEqual(w.polity, w.territoryOwner); +}); + +test('都市の国家切り替え後は中心の所属を修正し、旧国家の残存領土を除去する', () => { + const { sim, w, rect } = fixture(); + rect(2, 2, 5, 5); rect(10, 10, 12, 12); + const left = { id: 1, x: 3, y: 3, population: 1000, polityId: 1 }; + const captured = { id: 2, x: 11, y: 11, population: 1000, polityId: 1 }; + sim.cities = [left, captured]; + sim.polities[0].cityIds = new Set([1, 2]); sim.polities[1].cityIds = new Set(); + sim.territorialClaims.set(w.idx(11, 11), { polityId: 1, claim: 0.8 }); + sim.addCityToPolity(captured, sim.polities[1]); + sim.enforceTerritoryConnectivity({ allowOwnershipChanges: false }); + assert.equal(w.polity[w.idx(11, 11)], 2); + assert.equal(w.polity[w.idx(10, 10)], -1); + assert.equal(w.polity[w.idx(3, 3)], 1); + assert.equal(sim.territorialClaims.has(w.idx(11, 11)), false); + captured.polityId = null; + sim.enforceTerritoryConnectivity(); + assert.equal(w.polity[w.idx(11, 11)], -1); +}); + +test('独立都市併合の平和・武力分岐をNode回帰試験で一元管理する', () => { + const selector = Object.create(engine.SimulationClass.prototype); + selector.rng = { next: () => 0.399999 }; + assert.equal(selector.independentAnnexationMode(), 'peaceful'); + selector.rng.next = () => 0.4; + assert.equal(selector.independentAnnexationMode(), 'military'); + + const run = (cityPolityId, annexationMode) => { + const sim = Object.create(engine.SimulationClass.prototype); + const attacker = { id: 1 }; + const city = { id: 7, x: 2, y: 3, polityId: cityPolityId, population: 1000 }; + Object.assign(sim, { + world: { size: 10, territoryOwner: new Int32Array(100), idx: (x, y) => y * 10 + x }, + year: 0, damageCalls: 0, transferCalls: 0 + }); + sim.world.territoryOwner.fill(-1); + sim.getPolityById = id => id === 1 ? attacker : null; + sim.getCityById = id => id === 7 ? city : null; + sim.campaignEthnicSimilarity = () => 0.5; + sim.applyCityWarDamage = () => { sim.damageCalls++; }; + sim.transferCityToPolity = () => { sim.transferCalls++; city.polityId = attacker.id; }; + sim.applyCampaignTerritory = () => []; + sim.addPolityEvent = () => {}; + sim.recordCampaign = () => {}; + sim.syncTilePopulationCulture = () => {}; + sim.recomputeTerritories = () => {}; + sim.reconcileCityTerritoryOwnership = () => {}; + sim.campaignPressureOnTerritories = () => {}; + sim.resolveCampaignSuccess({ + id: 1, type: 'independent_city_annexation', annexationMode, attackerPolityId: 1, + targetCityId: 7, targetTile: 32, progress: 1, strength: 100, resistance: 50 + }); + return sim; + }; + + const peaceful = run(null, 'peaceful'); + assert.equal(peaceful.transferCalls, 1); + assert.equal(peaceful.damageCalls, 0); + const military = run(null, 'military'); + assert.equal(military.transferCalls, 1); + assert.equal(military.damageCalls, 1); + const owned = run(2, 'peaceful'); + assert.equal(owned.transferCalls, 0); +}); + +function warFixture() { + const { sim } = fixture(); + Object.assign(sim, { wars: [], campaigns: [], nextWar: 1, year: 4, deaths: 0, + rng: { range: () => 1, next: () => 0.99 }, + cities: [{ id: 1, polityId: 1, population: 1000, storedResources: 500, loyalty: 1 }, + { id: 2, polityId: 2, population: 1000, storedResources: 500, loyalty: 1 }] }); + sim.getPolityCities = polity => sim.cities.filter(city => city.polityId === polity.id); + sim.chooseWarGoal = () => 'border_claim'; + sim.addPolityEvent = () => {}; + sim.applyWarSettlement = () => {}; + return sim; +} + +test('new polities receive a founding truce before they can enter a war', () => { + const { sim } = fixture(); + Object.assign(sim, { + year: 96, nextPolity: 3, rng: { range: (min, max) => (min + max) / 2 }, + polities: [], polityById: new Map() + }); + sim.transferCityToPolity = () => true; + sim.claimPolityCoreTerritory = () => []; + sim.ensurePolityHistory = () => ({}); + sim.samplePolityHistory = () => {}; + const polity = sim.createPolity({ id: 7, storedResources: 100 }); + assert.equal(polity.nextWarAllowedWeek, 96 + engine.SimSettings.polity.foundingProtectionYears * 48); + + const warSim = warFixture(); + warSim.polities[0].nextWarAllowedWeek = warSim.year + 1; + assert.equal(warSim.startWar(warSim.polities[0], warSim.polities[1]), null); +}); + +test('captured city claims can absorb adjacent neutralized former territory', () => { + const { sim, w, set } = fixture(); + const center = w.idx(8, 8); + const former = w.idx(9, 8); + const neutral = w.idx(8, 9); + set(8, 8, 1); set(9, 8, 2); set(8, 9, -1); + sim.cities = [{ id: 9, x: 8, y: 8, polityId: 1 }]; + w.city[center] = 9; + sim.getCityById = id => sim.cities.find(city => city.id === id) || null; + sim.recomputeTerritories = () => {}; + sim.markContestedFront = () => {}; + const originalTransfer = sim.transferTerritoryTiles.bind(sim); + let optionsSeen = null; + sim.transferTerritoryTiles = (from, to, tiles, options) => { + optionsSeen = options; + return originalTransfer(from, to, tiles, options); + }; + const changed = sim.applyCampaignTerritory(sim.polities[0], center, 2, 0.8, { + allowForeignTakeover: true, fromPolityId: 2, reason: 'war' + }); + assert.equal(optionsSeen.allowNeutralWithFrom, true); + assert.ok(changed.includes(former)); + assert.ok(changed.includes(neutral)); + assert.equal(w.territoryOwner[former], 1); + assert.equal(w.territoryOwner[neutral], 1); +}); + +test('戦争は年境界以外でも4週ごとに損害を与える', () => { + const sim = warFixture(); + sim.startWar(sim.polities[0], sim.polities[1]); + sim.applyWarPressure = (war, attacker, defender) => sim.applyCityWarDamage(sim.getPolityCities(defender)[0], 0.1); + sim.applyWarExhaustion = () => {}; + sim.recordWarDeaths = loss => { sim.deaths += loss; }; + sim.updateWars(); + assert.equal(sim.cities[0].population, 935); + assert.equal(sim.cities[1].population, 935); + assert.equal(sim.deaths, 130); + sim.year = 5; sim.updateWars(); + assert.equal(sim.deaths, 130); +}); + +test('国家間の戦争開始判定も4週ごとに実行する', () => { + const sim = warFixture(); + sim.rng.next = () => 0; + sim.polityPower = () => 100; + sim.polityDistance = () => 10; + sim.polityInstability = sim.polityAgePressure = sim.polityOverextension = () => 0; + sim.averagePolityLoyalty = () => 1; + sim.maybeStartWars(); + assert.equal(sim.wars.length, 1); + assert.equal(sim.wars[0].started, 4); +}); + +test('戦争は経過年数だけでは終結せず、戦闘停止後に終結して再戦間隔を空ける', () => { + const sim = warFixture(); + const war = sim.startWar(sim.polities[0], sim.polities[1]); + sim.campaigns.push({ type: 'border_war', status: 'active', attackerPolityId: 1, defenderPolityId: 2 }); + sim.year = war.started + 100 * 48; + war.lastActionYear = sim.year; + sim.maybeEndWar(war); + assert.equal(war.ended, null); + war.lastActionYear = sim.year - engine.SimSettings.war.idleEndWeeks; + sim.maybeEndWar(war); + assert.equal(war.ended, sim.year); + assert.equal(sim.campaigns[0].status, 'ceased'); + assert.equal(sim.startWar(sim.polities[0], sim.polities[1]), null); + sim.year += engine.SimSettings.war.cooldownWeeks; + assert.ok(sim.startWar(sim.polities[0], sim.polities[1])); +}); + +test('首都から遠く領域接続もない戦場では攻撃補給が不利になる', () => { + const sim = warFixture(); + const capital = Object.assign(sim.cities[0], { x: 2, y: 2, tradeLinks: new Set() }); + const source = { id: 3, polityId: 1, x: 5, y: 2, population: 500, storedResources: 100, tradeLinks: new Set() }; + const closeTarget = Object.assign(sim.cities[1], { x: 7, y: 2, tradeLinks: new Set() }); + const farTarget = { id: 4, polityId: 2, x: 15, y: 15, population: 500, storedResources: 100, tradeLinks: new Set() }; + sim.cities.push(source, farTarget); + sim.polities[0].centerCityId = capital.id; + for (let x = 2; x <= 5; x++) sim.world.territoryOwner[sim.world.idx(x, 2)] = 1; + sim.warLogisticsCache = new Map(); + const connected = sim.warLogisticsFactor(sim.polities[0], source, closeTarget); + sim.world.territoryOwner[sim.world.idx(4, 2)] = -1; + sim.warLogisticsCache = new Map(); + const disconnected = sim.warLogisticsFactor(sim.polities[0], source, closeTarget); + const distant = sim.warLogisticsFactor(sim.polities[0], capital, farTarget); + assert.ok(connected > disconnected * 2); + assert.ok(connected > distant); +}); + +test('明確な戦果や疲弊でも短期間で戦争を終結する', () => { + for (const reason of ['score', 'exhaustion']) { + const sim = warFixture(); + const war = sim.startWar(sim.polities[0], sim.polities[1]); + sim.year = war.started + engine.SimSettings.war.minDurationWeeks; + if (reason === 'score') war.scoreA = engine.SimSettings.war.decisiveScore; + else war.exhaustionA = 0.8; + sim.maybeEndWar(war); + assert.equal(war.ended, sim.year); + } +}); + +test('地域人口と収容力を10倍にしても人口圧力と食料需要は維持する', () => { + const models = [1, 10].map(scale => { + const fixedDate = class extends Date { static now() { return 42; } }; + const context = vm.createContext({ console, Date: fixedDate, performance }); + const config = read('config.js').replace(/(population: Object.freeze\(\{\s*populationScale: )10/, `$1${scale}`); + vm.runInContext(config + read('utils.js') + read('engine.js') + ';globalThis.Model = Simulation', context); + return new context.Model(32, 5000 * scale); + }); + assert.equal(models[1].world.polity, models[1].world.territoryOwner); + for (let tile = 0; tile < models[0].world.count; tile++) { + assert.ok(Math.abs(models[1].world.population[tile] - models[0].world.population[tile] * 10) < 0.01); + assert.ok(Math.abs(models[1].world.populationCapacity[tile] - models[0].world.populationCapacity[tile] * 10) < 0.001); + assert.ok(Math.abs(models[1].world.populationPressure[tile] - models[0].world.populationPressure[tile]) < 0.001); + } + for (const sim of models) sim.consumeTileResources(); + for (let tile = 0; tile < models[0].world.count; tile++) { + assert.ok(Math.abs(models[1].world.resource[tile] - models[0].world.resource[tile]) < 0.001); + } +}); + +test('領土の中心部は円形の距離で広がる', () => { + const { sim, w } = fixture(); + sim.transferTerritoryTiles = (from, to, tiles) => tiles; + const tiles = sim.claimPolityCoreTerritory({ id: 1 }, { x: 7, y: 7 }, 4); + assert.ok(tiles.includes(w.idx(10, 9))); // 菱形の範囲外だが円の範囲内。 + assert.ok(!tiles.includes(w.idx(10, 10))); +}); + +test('通常のシミュレーションを複数年進めても人口と領土が正常', () => { + const sim = new engine.SimulationClass(32, 6000); + for (let week = 0; week < 480; week++) sim.step(); + assert.equal(sim.year, 480); + assert.deepEqual(sim.world.polity, sim.world.territoryOwner); + const countries = new Set(sim.polities.map(p => p.id)); + for (let tile = 0; tile < sim.world.count; tile++) { + assert.ok(Number.isFinite(sim.world.population[tile])); + assert.ok(sim.world.population[tile] >= 0); + assert.ok(sim.world.polity[tile] === -1 || countries.has(sim.world.polity[tile])); + } +}); + +test('農業圏は都市として登録せず、中心の一タイルだけ登録する', () => { + const sim = new engine.SimulationClass(32, 6000); + const city = sim.createCity(16, 16); + city.population = 200; + sim.cities.push(city); + sim.rebuildIndexes(); + sim.updateCities(); + assert.equal(sim.cities.length, 1); + assert.equal(sim.world.city[sim.world.idx(16, 16)], city.id); + assert.equal([...sim.world.city].filter(id => id >= 0).length, 1); + assert.equal(sim.world.city[sim.world.idx(17, 16)], -1); +}); + +test('全レイヤー・統計・歴史・ツールチップを日本語で描画できる', () => { + const texts = []; + const rectangles = []; + const outlines = []; + const circles = []; + const drawing = new Proxy({ + createImageData: (w, h) => ({ data: new Uint8ClampedArray(w * h * 4) }), + createRadialGradient: () => ({ addColorStop() {} }), + fillText: text => texts.push(text), + fillRect: (...args) => rectangles.push(args), + strokeRect: (...args) => outlines.push(args), + arc: (...args) => circles.push(args) + }, { get: (target, key) => key in target ? target[key] : () => {} }); + const elements = {}; + const element = id => elements[id] ??= { value: '', style: {}, hidden: true, + innerHTML: '', textContent: '', offsetWidth: 200, offsetHeight: 180, + getContext: () => drawing, getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 300 }), + addEventListener() {}, attributes: {}, + getAttribute(name) { return this.attributes[name] ?? null; }, + setAttribute(name, value) { this.attributes[name] = value; }, + querySelectorAll(selector) { + if (selector !== 'canvas') return []; + return [...this.innerHTML.matchAll(/data-event-type="([^"]+)"/g)].map(match => ({ + getAttribute: () => match[1], getContext: () => drawing + })); + } }; + element('viewMode').value = 'polities'; + const ui = vm.createContext({ console, Date, performance, window: { devicePixelRatio: 1 }, + document: { getElementById: element, querySelector: () => element('sim') } }); + vm.runInContext(['config.js', 'utils.js', 'app-state.js', 'engine.js', 'render.js'].map(read).join('\n'), ui); + vm.runInContext('sim = new Simulation(16, 500);', ui); + assert.equal(vm.runInContext('cityRenderRadius({ population: 200 })', ui), 1); + assert.equal(vm.runInContext('cityRenderRadius({ population: 999 })', ui), 1); + assert.equal(vm.runInContext('cityRenderRadius({ population: 1000 })', ui), 2); + assert.equal(vm.runInContext('cityRenderRadius({ population: 4999 })', ui), 2); + assert.equal(vm.runInContext('cityRenderRadius({ population: 5000 })', ui), 3); + assert.equal(vm.runInContext('cityRenderRadius({ population: 9999 })', ui), 3); + assert.equal(vm.runInContext('cityRenderRadius({ population: 10000 })', ui), 4); + vm.runInContext('sim.world.terrain[sim.world.idx(8, 8)] = Terrain.PLAINS; sim.world.terrain[sim.world.idx(9, 8)] = Terrain.PLAINS; sim.world.buildLandNeighbors();', ui); + // 描画済みの凡例とレイヤー連動の簡潔なツールチップを確認。 + for (const mode of ['polities', 'terrain', 'resources', 'ethnicity', 'pressure', 'technology', 'pheromone']) { + element('viewMode').value = mode; + vm.runInContext('render(); setLegend(); hoverState = { x: 8, y: 8, left: 20, top: 20, width: 300, height: 300 }; renderTooltip();', ui); + assert.ok(!/[A-Za-z]{2,}/.test(element('tooltip').innerHTML.replace(/<[^>]*>/g, ''))); + assert.ok((element('tooltip').innerHTML.match(//g) || []).length <= 8); + } + vm.runInContext('setEventLegend(); updateStats(true);', ui); + assert.equal((element('eventLegend').innerHTML.match(//g) || []).length, 0); + assert.ok(element('eventLegend').innerHTML.includes('記号は表示されていません')); + assert.ok(!/Pheromone|Routes|States|pop |parent /.test(element('eventLegend').innerHTML + element('ethnicityList').innerHTML)); + assert.equal(vm.runInContext('formatSimDate(49)', ui), '1年 1月 第2週'); + const summaries = vm.runInContext('Object.entries(graphEventTypes).map(([type, item]) => item.label + item.summary({ outcome: "started", campaignType: "border_war", population: 100, cityId: 1 }))', ui); + for (const summary of summaries) assert.ok(!/[A-Za-z]/.test(summary), summary); + vm.runInContext('renderStateGraph();', ui); + assert.ok(texts.includes('国家の歴史はまだありません')); + vm.runInContext(` + const capital = { id: 1, x: 8, y: 8, population: 1000, storedResources: 40, + loyalty: 0.8, polityId: 1, ethnicityComposition: new Map([[1, 1000]]) }; + const country = { id: 1, color: [100, 180, 120], centerCityId: 1, cityIds: new Set([1]) }; + sim.cities = [capital]; sim.polities = [country]; sim.rebuildIndexes(); + sim.world.city[sim.world.idx(8, 8)] = 1; + sim.world.polity[sim.world.idx(8, 8)] = 1; + sim.year = years(2); + const event = { type: "capitalShift", year: years(1), importance: 3, + data: { oldCenterCityId: 2, newCenterCityId: 1 } }; + sim.polityHistory.set(1, { id: 1, color: country.color, active: true, + founded: 0, ended: null, events: [event], samples: [ + { year: 0, population: 500, cities: 1, avgLoyalty: 0.8, power: 40 }, + { year: years(2), population: 1000, cities: 1, avgLoyalty: 0.8, power: 60 } + ] }); + sim.graphEvents = [event]; + renderStateGraph(); render(); renderTooltip(); + showStateGraphMarkerTooltip(graphState.markers[0], 100, 10); + `, ui); + assert.ok(element('tooltip').innerHTML.includes('首都')); + assert.ok(element('tooltip').innerHTML.includes('都市データ')); + assert.ok(element('tooltip').innerHTML.includes('民族1 100%')); + assert.ok(element('tooltip').innerHTML.includes('制度老朽化')); + assert.ok(element('tooltip').innerHTML.includes('忠誠度上限')); + assert.equal((element('tooltip').innerHTML.match(/国庫<\/b>/g) || []).length, 1); + assert.ok(!/国庫<\/b>[^<]*\//.test(element('tooltip').innerHTML)); + assert.ok(element('tooltip').innerHTML.indexOf('都市データ') < element('tooltip').innerHTML.indexOf('国家データ')); + assert.ok(element('stateGraphTooltip').innerHTML.includes('遷都')); + assert.ok(texts.includes('存続中 1')); + assert.ok(texts.includes('国1')); + // The whole rendered city icon opens the city tooltip, while water still suppresses it. + vm.runInContext('hoverState = { x: 9, y: 8, left: 20, top: 20, width: 300, height: 300 }; renderTooltip()', ui); + assert.ok(element('tooltip').innerHTML.includes('都市1')); + vm.runInContext('sim.world.terrain[sim.world.idx(9, 8)] = Terrain.WATER; renderTooltip()', ui); + assert.equal(element('tooltip').hidden, true); + vm.runInContext('sim.world.terrain[sim.world.idx(9, 8)] = Terrain.PLAINS; hoverState = { x: 8, y: 8, left: 20, top: 20, width: 300, height: 300 }; renderTooltip()', ui); + assert.equal(element('tooltip').hidden, false); + rectangles.length = 0; outlines.length = 0; + vm.runInContext('sim.world.population.fill(0); sim.activePopulationTiles.clear(); sim.world.population[sim.world.idx(7, 8)] = 100; sim.activePopulationTiles.add(sim.world.idx(7, 8)); drawPopulationAndCities("polities")', ui); + assert.equal(rectangles.length, 3); // population tile, city body, and city center + assert.equal(outlines.length, 1); + vm.runInContext(` + const crowded = Array.from({ length: 9 }, (_, index) => ({ + type: "war", year: years(1), importance: 2, + data: { opponentPolityId: 2, outcome: index % 2 ? "ended" : "started" } + })); + sim.polityHistory.get(1).events = crowded; + sim.graphEvents = []; + renderStateGraph(); + `, ui); + const markerPositions = vm.runInContext('graphState.markers.filter(marker => marker.row).map(marker => `${marker.x},${marker.y}`)', ui); + assert.equal(markerPositions.length, 5); + assert.equal(new Set(markerPositions).size, markerPositions.length); + vm.runInContext('graphState.showEventIcons = false; renderStateGraph(); setEventLegend();', ui); + assert.equal(vm.runInContext('graphState.markers.length', ui), 0); + assert.ok(element('eventLegend').innerHTML.includes('非表示')); + vm.runInContext('graphState.showEventIcons = true; renderStateGraph();', ui); + vm.runInContext('sim.campaigns = [{ type: "frontier_colonization", status: "active", targetTile: 10, started: 0, expires: 200, progress: 0.5 }]', ui); + circles.length = 0; + vm.runInContext('drawCampaigns()', ui); + assert.equal(circles.length, 0); + vm.runInContext('sim.campaigns[0].type = "border_war"; drawCampaigns()', ui); + assert.equal(circles.length, 1); + // 凡例には現在のグラフで描かれた種類だけを載せ、地図の記号と混同しない。 + assert.equal((element('eventLegend').innerHTML.match(//g) || []).length, 1); + assert.ok(element('eventLegend').innerHTML.includes('data-event-type="war"')); + assert.ok(!element('eventLegend').innerHTML.includes('遊牧集団')); + assert.ok(!/[●◆▲▼]/.test(element('eventLegend').innerHTML)); + element('historyFilter').value = 'ended'; + vm.runInContext('renderStateGraph()', ui); + assert.ok(element('eventLegend').innerHTML.includes('記号は表示されていません')); + element('historyFilter').value = 'all'; + vm.runInContext('sim.polityHistory.get(1).events = []; sim.graphEvents = [{ type: "nomadTrade", year: years(1), importance: 1 }]; renderStateGraph();', ui); + assert.ok(!element('eventLegend').innerHTML.includes('遊牧集団との交易')); + const html = read('index.html'); + assert.ok(html.includes('lang="ja"')); + assert.ok(html.includes('')); + assert.ok(!/[A-Za-z]/.test(html.replace(/<[^>]*>/g, '').replace(/FPS/g, ''))); + assert.ok(/