{
  "_about": "Human phrasing vocabulary for the changelog compare page (changelog/compare.html, rendered by gen/pages_versiondiff.py through gen/patchnote.py). One entry per '<Table>.<Field>' seen in a changed row of data/changelog.json or data/version_diffs.json. Each 'tpl' is ONE english sentence with {#} slots, passed to ctx.msgt() so ko/ja can word-order it. Slot order is ALWAYS subject, old, new, unless the entry carries its own 'slots' key. 'tpl_one' is the same sentence written for a subject of exactly one.",

  "_sections": [
    ["combat", "Combat balance"],
    ["skills", "Skills and skill trees"],
    ["items", "Items, gear and crafting"],
    ["monsters", "Monsters, spawns and loot"],
    ["effects", "Effects and traits"],
    ["settlers", "Settlers and recruitment"],
    ["settlement", "Settlement, buildings and research"],
    ["world", "Dungeons and the world"],
    ["economy", "Trade and prices"],
    ["events", "Events and quests"],
    ["rules", "Game rules and alerts"],
    ["text", "Text and wording"],
    ["other", "Other adjustments"]
  ],

  "_table_sections": {
    "AffecterTable": "effects",
    "AlertTable": "rules",
    "BaseAreaTable": "settlement",
    "BuildingTable": "settlement",
    "CampaignStartingTable": "rules",
    "CandidatePresetTable": "settlers",
    "ClanRankTable": "settlers",
    "DecisionCategoryTable": "settlement",
    "DecisionTable": "settlement",
    "DropTable": "monsters",
    "DungeonRoomTable": "world",
    "EmotionBubbleTable": "other",
    "EventChoiceTable": "other",
    "EventNodeTable": "other",
    "EventNpcMappingTable": "other",
    "EventRandomMappingTable": "other",
    "EventTable": "events",
    "FarmingTable": "settlement",
    "GameSystemTable": "rules",
    "HousingRoomTable": "settlement",
    "ItemTable": "items",
    "NPCSpawnConditionTable": "world",
    "OperationTable": "settlement",
    "QuestTable": "events",
    "RecipeTable": "items",
    "RecreationTable": "settlement",
    "ResearchTable": "settlement",
    "SkillTable": "combat",
    "SkillTreeTable": "skills",
    "SpawnGroupTable": "monsters",
    "SpecificStorageTable": "settlement",
    "StatusTable": "effects",
    "StatusValueTable": "combat",
    "TextKeys": "text",
    "TradePoolTable": "economy",
    "TraitTable": "settlers",
    "UnitProfileTable": "other",
    "UnitTable": "combat"
  },

  "_table_nouns": {
    "AffecterTable": "effects",
    "CampaignStartingTable": "scenarios",
    "CandidatePresetTable": "recruit presets",
    "DecisionCategoryTable": "decision categories",
    "DecisionTable": "decisions",
    "DropTable": "loot tables",
    "EmotionBubbleTable": "mood bubbles",
    "EventChoiceTable": "dialogue options",
    "EventNodeTable": "event steps",
    "EventNpcMappingTable": "NPC conversation links",
    "EventRandomMappingTable": "random event slots",
    "FarmingTable": "crops",
    "GameSystemTable": "game constants",
    "NPCSpawnConditionTable": "NPC spawns",
    "RecreationTable": "recreation activities",
    "ResearchTable": "research options",
    "SpecificStorageTable": "storage rules",
    "StatusTable": "stats",
    "TextKeys": "text entries",
    "TradePoolTable": "merchants",
    "UnitProfileTable": "character appearances"
  },

  "_nouns": {
    "background traits": "backgrounds",
    "individual traits": "personal traits"
  },

  "_default": {
    "tpl": "On {#}, {#} has changed from {#} to {#}.",
    "slots": ["subject", "field", "old", "new"],
    "note": "Fallback for any field with no entry above. FOUR slots: the humanised field name is slot 2. Section is decided from _table_sections, not from here. This is the anti-rot measure: an unknown future field still renders a readable sentence instead of raw JSON."
  },

  "_added": {
    "tpl": "Newly added: {#}.",
    "slots": ["subject"],
    "note": "Verbless on purpose so it reads for one entity and for a list of sixty. It carries NO 'section': see _notes 'presence lines follow their own table'."
  },

  "_removed": {
    "tpl": "No longer in the game: {#}.",
    "slots": ["subject"]
  },

  "AffecterTable.AdjustStatsData": {"tpl": "The stat changes applied by {#} have been adjusted from {#} to {#}."},
  "AffecterTable.AdjustStatsPerStackData": {"tpl": "The per-stack stat changes applied by {#} have been adjusted from {#} to {#}."},
  "AffecterTable.Categories": {"tpl": "The categories on {#} have changed from {#} to {#}."},
  "AffecterTable.FloatValues": {"tpl": "The values behind {#} have changed from {#} to {#}."},

  "AlertTable.Condition": {"tpl": "The trigger condition changed for {#}.", "tpl_one": "{#} had its trigger condition changed.", "fold": true, "tpl_count": "The trigger condition changed for {#} alerts.", "tpl_count_one": "{#} had its trigger condition changed."},
  "AlertTable.Type": {"tpl": "The severity of {#} has changed from {#} to {#}."},
  "AlertTable.TypeName": {"tpl": "The severity of {#} has changed from {#} to {#}."},

  "BuildingTable.ConstructionCategory": {"tpl": "The build menu category of {#} has changed from {#} to {#}."},
  "BuildingTable.ConstructionCategoryName": {"tpl": "The build menu category of {#} has changed from {#} to {#}."},
  "BuildingTable.ConstructionRequiredItems": {"tpl": "The materials needed to build {#} have changed from {#} to {#}."},
  "BuildingTable.DefaultTraits": {"tpl": "The effects carried by {#} have changed from {#} to {#}."},
  "BuildingTable.Durability": {"tpl": "The durability of {#} has changed from {#} to {#}."},
  "BuildingTable.OnDeconstructReturnedItems": {"tpl": "The materials returned for dismantling {#} have changed from {#} to {#}."},
  "BuildingTable.OnGatherItemsAndDrops": {"tpl": "The items gathered from {#} have changed from {#} to {#}."},
  "BuildingTable.Quality": {"tpl": "The quality rating of {#} has changed from {#} to {#}."},
  "BuildingTable.StorageCapacity": {"tpl": "The storage capacity of {#} has changed from {#} to {#}."},
  "BuildingTable.SubType": {"tpl": "The building subtype of {#} has changed from {#} to {#}."},
  "BuildingTable.SubTypeName": {"tpl": "The building subtype of {#} has changed from {#} to {#}."},

  "CampaignStartingTable.ManagementDifficultyType": {"tpl": "The management difficulty of {#} has changed from {#} to {#}."},
  "CampaignStartingTable.ManagementDifficultyTypeName": {"tpl": "The management difficulty of {#} has changed from {#} to {#}."},
  "CampaignStartingTable.StartingBuildingsSpawnPos": {"tpl": "The starting positions of the buildings in {#} have changed from {#} to {#}."},
  "CampaignStartingTable.StartingNeutralUnits": {"tpl": "The neutral units present at the start of {#} have changed from {#} to {#}."},

  "CandidatePresetTable.AdditionalEquipments": {"tpl": "The starting equipment for {#} has changed from {#} to {#}."},
  "CandidatePresetTable.IndividualTraits": {"tpl": "The personal traits of {#} have changed from {#} to {#}."},
  "CandidatePresetTable.IntelligenceTalent": {"tpl": "The Intelligence talent of {#} has changed from {#} to {#}."},
  "CandidatePresetTable.IntelligenceTalentName": {"tpl": "The Intelligence talent of {#} has changed from {#} to {#}."},
  "CandidatePresetTable.StrengthTalent": {"tpl": "The Strength talent of {#} has changed from {#} to {#}."},
  "CandidatePresetTable.StrengthTalentName": {"tpl": "The Strength talent of {#} has changed from {#} to {#}."},

  "ClanRankTable.LycanWeight": {"tpl": "The recruit weighting for Lycans at {#} has changed from {#} to {#}.", "note": "SHIM, delete on the engine fix. See _notes 'engine requests' R1: the generic ClanRankTable.<Race>Weight rule in patchnote._DYNAMIC_TPL prints its two values in the wrong order, so removing this race-specific key today would ship seven inverted sentences. The moment R1 lands, delete this entry - the generic rule covers all seven race columns and the twelve level columns, this one covers one."},

  "DecisionTable.Condition": {"tpl": "The offer condition changed for {#}.", "tpl_one": "{#} had its offer condition changed.", "fold": true, "tpl_count": "The offer condition changed for {#} decisions.", "tpl_count_one": "{#} had its offer condition changed."},
  "DecisionTable.Reward": {"tpl": "The reward changed for {#}.", "tpl_one": "{#} had its reward changed.", "fold": true, "tpl_count": "The reward changed for {#} decisions.", "tpl_count_one": "{#} had its reward changed."},

  "DropTable.DropItemOne_Amount": {"tpl": "The amount of the first item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemOne_ItemKey": {"tpl": "The first item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemOne_Percentage": {"tpl": "The chance of drawing the first item from {#} has changed from {#} to {#}."},
  "DropTable.DropItemOne_ProbabilityWeight": {"tpl": "The draw weight of the first item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemTwo_Amount": {"tpl": "The amount of the second item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemTwo_ItemKey": {"tpl": "The second item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemTwo_Percentage": {"tpl": "The chance of drawing the second item from {#} has changed from {#} to {#}."},
  "DropTable.DropItemTwo_ProbabilityWeight": {"tpl": "The draw weight of the second item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemThree_Amount": {"tpl": "The amount of the third item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemThree_ItemKey": {"tpl": "The third item in {#} has changed from {#} to {#}."},
  "DropTable.DropItemThree_Percentage": {"tpl": "The chance of drawing the third item from {#} has changed from {#} to {#}."},
  "DropTable.DropItemThree_ProbabilityWeight": {"tpl": "The draw weight of the third item in {#} has changed from {#} to {#}."},

  "DungeonRoomTable.AvailablePhases": {"tpl": "The phases {#} can appear in have changed from {#} to {#}."},
  "DungeonRoomTable.RoomDecoStyle": {"tpl": "The decoration style of {#} has changed from {#} to {#}."},
  "DungeonRoomTable.RoomDecoStyleName": {"tpl": "The decoration style of {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_1": {"tpl": "The first group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_2": {"tpl": "The second group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_3": {"tpl": "The third group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_4": {"tpl": "The fourth group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_5": {"tpl": "The fifth group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnBuildingList_6": {"tpl": "The sixth group of objects placed in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnItemCategory_1": {"tpl": "The first group of items scattered in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnItemCategory_2": {"tpl": "The second group of items scattered in {#} has changed from {#} to {#}."},
  "DungeonRoomTable.SpawnMonster": {"tpl": "The monsters placed in {#} have changed from {#} to {#}.", "section": "monsters"},
  "DungeonRoomTable.Weight": {"tpl": "The selection weight of {#} has changed from {#} to {#}."},

  "EventChoiceTable.ActiveCondition": {"tpl": "The availability condition changed for {#}.", "tpl_one": "{#} had its availability condition changed.", "fold": true, "tpl_count": "The availability condition changed for {#} dialogue options.", "tpl_count_one": "{#} had its availability condition changed."},
  "EventChoiceTable.OnSelectAction": {"tpl": "The outcome changed for {#}.", "tpl_one": "{#} had its outcome changed.", "fold": true, "tpl_count": "The outcome changed for {#} dialogue options.", "tpl_count_one": "{#} had its outcome changed."},
  "EventChoiceTable.TooltipAction": {"tpl": "The preview changed for {#}.", "tpl_one": "{#} had its preview changed.", "fold": true, "tpl_count": "The preview changed for {#} dialogue options.", "tpl_count_one": "{#} had its preview changed."},

  "EventNodeTable.ChoiceIDs": {"tpl": "The choices offered at {#} have changed from {#} to {#}."},
  "EventNodeTable.IllustKey": {"tpl": "The illustration shown at {#} has changed from {#} to {#}."},
  "EventNodeTable.Type": {"tpl": "The step type of {#} has changed from {#} to {#}."},
  "EventNodeTable.TypeName": {"tpl": "The step type of {#} has changed from {#} to {#}."},

  "EventNpcMappingTable.DefaultEventKey": {"tpl": "The default conversation for {#} has changed from {#} to {#}."},
  "EventNpcMappingTable.LinkedEventKeys": {"tpl": "The events linked to {#} have changed from {#} to {#}."},

  "EventTable.Cooldown": {"tpl": "The cooldown on {#} has changed from {#} to {#} in-game minutes."},
  "EventTable.IsRepeatable": {"tpl": "Whether {#} can happen more than once has changed from {#} to {#}."},
  "EventTable.KeyWord": {"tpl": "The internal keyword changed for {#}.", "tpl_one": "{#} had its internal keyword changed.", "fold": true, "tpl_count": "The internal keyword changed for {#} events.", "tpl_count_one": "{#} had its internal keyword changed."},
  "EventTable.StartCondition": {"tpl": "The trigger condition changed for {#}.", "tpl_one": "{#} had its trigger condition changed.", "fold": true, "tpl_count": "The trigger condition changed for {#} events.", "tpl_count_one": "{#} had its trigger condition changed."},
  "EventTable.TargetUnitCondition": {"tpl": "The targeting condition changed for {#}.", "tpl_one": "{#} had its targeting condition changed.", "fold": true, "tpl_count": "The targeting condition changed for {#} events.", "tpl_count_one": "{#} had its targeting condition changed."},

  "GameSystemTable.FloatValues": {"tpl": "The values behind {#} have changed from {#} to {#}."},
  "GameSystemTable.StringValues": {"tpl": "The stored list changed for {#}.", "tpl_one": "The stored list behind {#} changed.", "fold": true, "tpl_count": "The stored list changed for {#} game constants.", "tpl_count_one": "The stored list behind {#} changed.", "_notes": "GameSystemTable is a key-value table; the recruitment rosters GAMESYSTEM_Recruitment_UniqueExistingNames and _UniqueExistingProfiles are ROW KEYS whose contents (100+ TEXTKEY_PLAYERNAME_* and UNITVISUAL_* keys) live in this StringValues FIELD. Folding the field covers both rosters without a hardcoded row-key list. FloatValues stays value-carrying: those are numeric game constants a reader wants to see."},

  "HousingRoomTable.HousingType": {"tpl": "The room type of {#} has changed from {#} to {#}."},
  "HousingRoomTable.HousingTypeName": {"tpl": "The room type of {#} has changed from {#} to {#}."},

  "ItemTable.Durability": {"tpl": "The durability of {#} has changed from {#} to {#}."},
  "ItemTable.Equipment_AdditionalStatuses": {"tpl": "The bonuses granted by equipping {#} have changed from {#} to {#}."},
  "ItemTable.Equipment_ItemDefaultSkills": {"tpl": "The skills granted by equipping {#} have changed from {#} to {#}."},
  "ItemTable.Equipment_MagicAttackPower": {"tpl": "The magic attack granted by {#} has changed from {#} to {#}."},
  "ItemTable.Equipment_PhysicalAttackPower": {"tpl": "The physical attack granted by {#} has changed from {#} to {#}."},
  "ItemTable.Equipment_MaxHealthTotal": {"tpl": "The health granted by {#} has changed from {#} to {#}."},
  "ItemTable.Equipment_OnEquipAffecters": {"tpl": "The effects applied by equipping {#} have changed from {#} to {#}."},
  "ItemTable.MaxStack": {"tpl": "The stack limit on {#} has changed from {#} to {#}."},
  "ItemTable.RarityName": {"tpl": "The rarity of {#} has changed from {#} to {#}."},
  "ItemTable.RequiredItemsRepair": {"tpl": "The materials needed to repair {#} have changed from {#} to {#}."},
  "ItemTable.StandardPrice": {"tpl": "The base price of {#} has changed from {#} to {#}.", "section": "economy"},
  "ItemTable.Weight": {"tpl": "The weight of {#} has changed from {#} to {#}."},

  "NPCSpawnConditionTable.ProhibitedRecords": {"tpl": "The blocking conditions changed for {#}.", "tpl_one": "{#} had its blocking conditions changed.", "fold": true, "tpl_count": "The blocking conditions changed for {#} NPC spawns.", "tpl_count_one": "{#} had its blocking conditions changed."},
  "NPCSpawnConditionTable.RequiredRecords": {"tpl": "The prerequisites changed for {#}.", "tpl_one": "{#} had its prerequisites changed.", "fold": true, "tpl_count": "The prerequisites changed for {#} NPC spawns.", "tpl_count_one": "{#} had its prerequisites changed."},
  "NPCSpawnConditionTable.SpawnDungeonRoomKey": {"tpl": "The spawn room for {#} has changed from {#} to {#}."},

  "QuestTable.CompleteCondition": {"tpl": "The completion requirement changed for {#}.", "tpl_one": "{#} had its completion requirement changed.", "fold": true, "tpl_count": "The completion requirement changed for {#} quests.", "tpl_count_one": "{#} had its completion requirement changed."},

  "RecipeTable.CraftingWorkloadRequired": {"tpl": "The work needed for {#} has changed from {#} to {#}."},
  "RecipeTable.IngredientAmounts": {"tpl": "The ingredient amounts for {#} have changed from {#} to {#}."},

  "ResearchTable.GridY": {"tpl": "The position of {#} on the research board has changed from {#} to {#}."},
  "ResearchTable.ResearchResult": {"tpl": "The unlock list changed for {#}.", "tpl_one": "{#} had its unlock list changed.", "fold": true, "tpl_count": "The unlock list changed for {#} research options.", "tpl_count_one": "{#} had its unlock list changed."},

  "SkillTable.AreaRadius": {"tpl": "The area of effect of {#} has changed from {#} to {#} tiles."},
  "SkillTable.Cooldown": {"tpl": "The cooldown of {#} has changed from {#} to {#}.", "unit": "s"},
  "SkillTable.Duration": {"tpl": "The duration of {#} has changed from {#} to {#}.", "unit": "s"},
  "SkillTable.EndDelay": {"tpl": "The end delay on {#} has changed from {#} to {#}.", "unit": "s"},
  "SkillTable.EnergyCost": {"tpl": "The energy cost of {#} has changed from {#} to {#}."},
  "SkillTable.FloatValues": {"tpl": "The values behind {#} have changed from {#} to {#}."},
  "SkillTable.Range": {"tpl": "The range of {#} has changed from {#} to {#} tiles."},
  "SkillTable.RequiredEquipment": {"tpl": "The equipment required to use {#} has changed from {#} to {#}.", "section": "skills"},
  "SkillTable.RequiredLevel": {"tpl": "The level required for {#} has changed from {#} to {#}.", "section": "skills"},
  "SkillTable.RequiredSkillPoint": {"tpl": "The skill point cost of {#} has changed from {#} to {#}.", "section": "skills"},
  "SkillTable.StartDelay": {"tpl": "The start delay on {#} has changed from {#} to {#}.", "unit": "s"},

  "SpawnGroupTable.SpawnPool": {"tpl": "The monsters in {#} have changed from {#} to {#}."},
  "SpawnGroupTable.Weight": {"tpl": "The selection weight of {#} has changed from {#} to {#}."},

  "TextKeys.en": {"tpl": "The in-game text of {#} has changed from {#} to {#}."},
  "TextKeys.text": {"tpl": "The in-game text of {#} has changed from {#} to {#}."},

  "TradePoolTable.AppearChancePers": {"tpl": "The chance of each item appearing at {#} has changed from {#} to {#}.", "unit": "%"},
  "TradePoolTable.AvailableItems": {"tpl": "The goods sold at {#} have changed from {#} to {#}."},
  "TradePoolTable.MaxAmounts": {"tpl": "The largest quantity of each item stocked at {#} has changed from {#} to {#}."},
  "TradePoolTable.MinAmounts": {"tpl": "The smallest quantity of each item stocked at {#} has changed from {#} to {#}."},
  "TradePoolTable.MaxDiscountPers": {"tpl": "The upper end of the price adjustment at {#} has changed from {#} to {#}.", "unit": "%"},
  "TradePoolTable.MinDiscountPers": {"tpl": "The lower end of the price adjustment at {#} has changed from {#} to {#}.", "unit": "%"},
  "TradePoolTable.TraderGold": {"tpl": "The gold available at {#} has changed from {#} to {#}."},

  "TraitTable.AdjustStats": {"tpl": "The stat changes granted by {#} have been adjusted from {#} to {#}.", "section": "effects"},
  "TraitTable.AvailableBackgroundTraits": {"tpl": "The backgrounds available to {#} have changed from {#} to {#}."},
  "TraitTable.AvailableIndividualTraits": {"tpl": "The personal traits available to {#} have changed from {#} to {#}."},
  "TraitTable.AvaliableMainSkillTreeTypes": {"tpl": "The main skill trees open to {#} have changed from {#} to {#}.", "section": "skills"},
  "TraitTable.AvaliableSubSkillTreeTypes": {"tpl": "The sub skill trees open to {#} have changed from {#} to {#}.", "section": "skills"},
  "TraitTable.BackgroundTraitWeights": {"tpl": "The weighting of each background for {#} has changed from {#} to {#}."},
  "TraitTable.Categories": {"tpl": "The categories on {#} have changed from {#} to {#}.", "section": "effects"},
  "TraitTable.DefaultEquipments": {"tpl": "The starting equipment for {#} has changed from {#} to {#}."},
  "TraitTable.DefaultItems": {"tpl": "The starting items for {#} have changed from {#} to {#}."},
  "TraitTable.IndividualTraitWeights": {"tpl": "The weighting of each personal trait for {#} has changed from {#} to {#}."},
  "TraitTable.PerceptionPerModifierMax": {"tpl": "The top of the Perception modifier range on {#} has changed from {#} to {#}."},
  "TraitTable.PerceptionPerModifierMin": {"tpl": "The bottom of the Perception modifier range on {#} has changed from {#} to {#}."},
  "TraitTable.RecruitAppearClanRank": {"tpl": "The clan rank from which {#} start appearing among recruits has changed from {#} to {#}.", "tpl_one": "The clan rank from which {#} starts appearing among recruits has changed from {#} to {#}."},
  "TraitTable.SubSkillTreeWeights": {"tpl": "The weighting of each sub skill tree for {#} has changed from {#} to {#}.", "section": "skills"},

  "UnitProfileTable.BodyType": {"tpl": "The body used by {#} has changed from {#} to {#}."},
  "UnitProfileTable.BodyTypeName": {"tpl": "The body used by {#} has changed from {#} to {#}."},
  "UnitProfileTable.EyesType": {"tpl": "The eyes used by {#} have changed from {#} to {#}."},
  "UnitProfileTable.EyesTypeName": {"tpl": "The eyes used by {#} have changed from {#} to {#}."},
  "UnitProfileTable.FacialDecoType": {"tpl": "The facial decoration used by {#} has changed from {#} to {#}."},
  "UnitProfileTable.FacialDecoTypeName": {"tpl": "The facial decoration used by {#} has changed from {#} to {#}."},
  "UnitProfileTable.HairType": {"tpl": "The hair used by {#} has changed from {#} to {#}."},
  "UnitProfileTable.HairTypeName": {"tpl": "The hair used by {#} has changed from {#} to {#}."},

  "UnitTable.AttackSpeed": {"tpl": "The attack speed of {#} has changed from {#} to {#}."},
  "UnitTable.DefaultSkills": {"tpl": "The starting skills of {#} have changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.DefaultTraits": {"tpl": "The starting traits of {#} have changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.FirstKillItemsAndDrops": {"tpl": "The reward for the first kill of {#} has changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.MagicAttackPower": {"tpl": "The magic attack of {#} has changed from {#} to {#}."},
  "UnitTable.MaxHealthTotal": {"tpl": "The health of {#} has changed from {#} to {#}."},
  "UnitTable.MovementSpeed": {"tpl": "The movement speed of {#} has changed from {#} to {#}."},
  "UnitTable.OnDeathItemsAndDrops": {"tpl": "The loot dropped by {#} has changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.PhysicalAttackPower": {"tpl": "The physical attack of {#} has changed from {#} to {#}."},
  "UnitTable.SubFaction": {"tpl": "The sub-faction of {#} has changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.SubFactionName": {"tpl": "The sub-faction of {#} has changed from {#} to {#}.", "section": "monsters"},
  "UnitTable.VisionRange": {"tpl": "The vision range of {#} has changed from {#} to {#} tiles."},

  "AdjustStats": {"tpl": "The stat changes granted by {#} have been adjusted from {#} to {#}."},
  "AdjustStatsData": {"tpl": "The stat changes applied by {#} have been adjusted from {#} to {#}."},
  "AttackSpeed": {"tpl": "The attack speed of {#} has changed from {#} to {#}."},
  "Categories": {"tpl": "The categories on {#} have changed from {#} to {#}."},
  "Condition": {"tpl": "The condition changed for {#}.", "tpl_one": "{#} had its condition changed.", "fold": true, "tpl_count": "The condition changed for {#} entries.", "tpl_count_one": "{#} had its condition changed."},
  "Cooldown": {"tpl": "The cooldown of {#} has changed from {#} to {#}.", "unit": "s"},
  "Durability": {"tpl": "The durability of {#} has changed from {#} to {#}."},
  "Duration": {"tpl": "The duration of {#} has changed from {#} to {#}.", "unit": "s"},
  "FloatValues": {"tpl": "The values behind {#} have changed from {#} to {#}."},
  "MagicAttackPower": {"tpl": "The magic attack of {#} has changed from {#} to {#}."},
  "MaxHealthTotal": {"tpl": "The health of {#} has changed from {#} to {#}."},
  "MaxStack": {"tpl": "The stack limit on {#} has changed from {#} to {#}."},
  "MovementSpeed": {"tpl": "The movement speed of {#} has changed from {#} to {#}."},
  "PhysicalAttackPower": {"tpl": "The physical attack of {#} has changed from {#} to {#}."},
  "Quality": {"tpl": "The quality rating of {#} has changed from {#} to {#}."},
  "Range": {"tpl": "The range of {#} has changed from {#} to {#} tiles."},
  "RarityName": {"tpl": "The rarity of {#} has changed from {#} to {#}."},
  "RequiredLevel": {"tpl": "The level required for {#} has changed from {#} to {#}."},
  "Reward": {"tpl": "The reward changed for {#}.", "tpl_one": "{#} had its reward changed.", "fold": true, "tpl_count": "The reward changed for {#} entries.", "tpl_count_one": "{#} had its reward changed."},
  "StandardPrice": {"tpl": "The base price of {#} has changed from {#} to {#}."},
  "StringValues": {"tpl": "The stored list changed for {#}.", "tpl_one": "The stored list behind {#} changed.", "fold": true, "tpl_count": "The stored list changed for {#} entries.", "tpl_count_one": "The stored list behind {#} changed."},
  "SubTypeName": {"tpl": "The subtype of {#} has changed from {#} to {#}."},
  "TypeName": {"tpl": "The type of {#} has changed from {#} to {#}."},
  "VisionRange": {"tpl": "The vision range of {#} has changed from {#} to {#} tiles."},
  "Weight": {"tpl": "The weight of {#} has changed from {#} to {#}."},

  "_notes": {
    "lookup order": "'<Table>.<Field>' first, then patchnote._DYNAMIC_TPL, then the bare '<Field>' entry (no dot, no 'section' key), then '_default'. The bare entries exist so a known property on a NEW table still reads as a sentence.",

    "slot order": "Every entry except _default, _added and _removed takes exactly three slots in this order: subject, old value, new value. Entries that carry a 'slots' key declare their own order.",

    "tpl vs tpl_one": "'tpl' is written for a subject of MANY ('15 backgrounds'), 'tpl_one' for a subject of exactly one ('Blacksmith'). Only entries whose sentence contains a verb or article that agrees with the SUBJECT SLOT carry both; every other template is worded so the grammatical subject is the property, which reads for one and for sixty alike. The four that need the pair are the ones with a relative clause hanging off the slot (RecruitAppearClanRank 'starts/start appearing', ResearchResult 'unlocks/unlock') and the ones whose lead noun counts with the subject (the condition/conditions family: AlertTable.Condition, DecisionTable.Condition, EventTable.StartCondition, EventChoiceTable.ActiveCondition, bare 'Condition').",

    "grammar rule the templates follow": "The subject slot is NEVER the subject of an inflected verb, unless the entry also supplies tpl_one. The grammatical subject is the property ('The stack limit on {#} has changed...'), so the same sentence reads for one entity and for a collapsed group. Any new template must keep this property or carry both forms, otherwise a group of many produces 'The traits 3 monsters starts with'.",

    "COUNT-ONLY FOLD CONTRACT (key names for the engine)": "This is the owner's decision-1 for this round: any field whose value is a condition / reward / action script / roster (boolean gate, DSL, or a list of keys) renders as a COUNT with NO value on the surface, e.g. 'The trigger condition changed for 12 events.', with the raw before/after reachable only behind the member <details>. The boolean/reward expression NEVER appears as surface prose and NEVER as an unresolved token. The vocabulary keys this file carries for that, on every folded entry: (1) \"fold\": true - the flag that marks the field foldable; (2) \"tpl_count\" - the MANY sentence, its single {#} is the COUNT INTEGER and the noun is written INTO the sentence ('... for {#} events.'); (3) \"tpl_count_one\" - the ONE sentence, its single {#} is the SUBJECT (the one entity's already-rendered link+icon HTML), e.g. '{#} had its trigger condition changed.'. Provided on the SAME entries, deliberately value-free: (4) \"tpl\" / \"tpl_one\" - these carry NO old/new slot, only a subject slot, so EVEN IF the engine has not wired the fold yet, or renders these the ordinary way, no condition/reward/DSL value can reach the surface. That is the belt-and-suspenders split: this file alone guarantees 'no DSL token as prose' (the value slots are simply gone); the engine's fold only adds the collapse from N per-entity lines into one count line. To get the single count line, the engine must group these fields NON-value-keyed (by section+kind+field, ignoring old/new) and fill tpl_count/tpl_count_one; value-keyed grouping (today's behaviour) stays SAFE, it just prints one value-free line per distinct change instead of one 'N events' line. Detect a foldable field by the \"fold\": true flag OR by value shape (contains '&&' '||' or a leading '!', or splits into comma/newline action tokens like ModifyContributionPoint_15, or is a roster of TEXTKEY_/UNITVISUAL_/entity keys) - either is fine, both land on the same templates. Folded fields present in the current diffs: EventTable.StartCondition, EventTable.TargetUnitCondition, AlertTable.Condition, DecisionTable.Condition, DecisionTable.Reward, EventChoiceTable.ActiveCondition, EventChoiceTable.OnSelectAction, EventChoiceTable.TooltipAction, QuestTable.CompleteCondition, ResearchTable.ResearchResult, NPCSpawnConditionTable.ProhibitedRecords, NPCSpawnConditionTable.RequiredRecords, GameSystemTable.StringValues. The bare 'Condition', 'Reward' and 'StringValues' entries carry the same fold+count keys so a future table's same-named field folds with no edit (this is how 'any *Condition / *Reward' is covered without a hardcoded field list). Not folded, on purpose: NPCSpawnConditionTable.SpawnDungeonRoomKey (a single room, renders as a linked room), GameSystemTable.FloatValues and every *.FloatValues (numeric constants a reader wants to read), and every material/equipment list (ConstructionRequiredItems, RequiredItemsRepair, DefaultEquipments, DefaultItems, AdditionalEquipments) - those are real content and keep their 'from X to Y'.",

    "sections are ordered by what a player feels": "_sections is the render order and it leads with combat, skills, items and monsters. patchnote.SECTIONS is BUILT from it, and patchnote.FOLD_SECTIONS forces 'text' and 'other' last whatever this file says, so those two are the folds. Event plumbing (EventChoiceTable, EventNodeTable, EventNpcMappingTable, EventRandomMappingTable) and appearance presets (UnitProfileTable) are mapped to 'other' on purpose: they are the largest row counts in a typical build and the smallest thing a player notices. The 'events' section keeps EventTable and QuestTable, which are the events a player actually meets.",

    "a field's section is its TABLE's section unless it says otherwise": "Most entries carry no 'section' key at all: patchnote falls back to _table_sections, which makes that map the single place a whole table moves. A 'section' key appears ONLY where a field belongs somewhere its table does not (ItemTable.StandardPrice is trade, SkillTable.Required* are the skill tree rather than combat, UnitTable's drop and faction columns are monsters, TraitTable's tree columns are skills). Do not re-add a 'section' that merely repeats the table's own: that is the second copy that drifts.",

    "presence lines follow their own table": "_added and _removed deliberately carry NO 'section'. patchnote._presence_section then falls back to the row's own table, so a build's new items land at the top of Items, its new monsters at the top of Monsters, and its 207 new text keys inside the Text fold. When _added named one shared section, that section became a dump whose biggest line was 'Newly added: 207 text keys' - which is also simply false as a claim about new content: new text is the NAMES of the skills, items and buildings the same build already announces. There is no 'new content' section for that reason; every section leads with its own additions.",

    "nouns are the site's own words": "_table_nouns overrides the noun patchnote.table_noun() derives from a table's NAME, and _nouns overrides the phrase group_noun() composes from a member column plus that noun. Both are vocabulary, not content: an unmapped table or an unmapped phrase falls through to the derivation, which is exactly today's behaviour, so a table the game adds later cannot go missing and a renamed column cannot break a lookup. Every value is a word a player has already seen: AffecterTable rows are 'effects' (site/affecters/index.html h1, never the developer word 'affecters'), DecisionTable rows are 'decisions' (the site's own counts say '28 decisions', '53 decisions'; it is the game's internal word), GameSystemTable is 'game constants', TradePoolTable is 'merchants', CandidatePresetTable is 'recruit presets', ResearchTable is 'research options', TraitTable's Background rows are 'backgrounds'. The rest are plain player-facing descriptions chosen because the table's own name is a developer word and the game has no player name for it at all: EventNodeTable 'event steps', EventChoiceTable 'dialogue options' (never the developer word 'event choices'), EventRandomMappingTable 'random event slots', UnitProfileTable 'character appearances' for the UNITVISUAL_* rows (never 'unit profiles'), TextKeys 'text entries' (never 'text keys'), SpecificStorageTable 'storage rules'. None of them claims a mechanic. THESE MAPS ONLY TAKE EFFECT ONCE THE ENGINE READS THEM (see 'engine requests' R3): today patchnote.table_noun() and group_noun() still derive from the table name, so until R3 is wired the developer nouns 'affecters', 'text keys', 'unit profiles', 'event nodes', 'event choices', 'npcspawn conditions' still reach any count-of-many subject phrase. The data here is correct; the wiring is the engine's.",

    "nouns must survive singularisation": "pages_versiondiff._one() singularises the LAST word of a noun for a count of one, so a noun whose plural is not its last word breaks ('lines of in-game text' would print as '1 lines of in-game text'). TextKeys is therefore 'text entries', which also matches the wording the polish sentence already uses, and StatusTable is 'stats' rather than the site's 'Statuses', whose singular comes out as 'statuse'.",

    "no arrows": "Templates say 'from X to Y'. Not one of them uses an arrow between the old and the new value, matching the register of a real patch note: 79,000 characters of Palworld release notes contain zero of them.",

    "no reason is ever stated": "Templates report the change only. None of them says why, and none should be extended to. There is no bug-fix wording here for the same reason: whether a change fixed something is not in the tables.",

    "no em dash": "build_site strips em dashes after the build, so an em dash in a template produces a catalog key that never matches its i18n entry. Use ' - ' or a comma.",

    "unit": "'unit' is a suffix appended to the old and the new value ('s' for seconds, '%' for percent). Where the value is a distance the word 'tiles' is written into the sentence instead, because it reads better once and not twice.",

    "TextKeys.en and TextKeys.text are the same field": "make_changelog.py names the slot 'en'; make_version_diffs.py names it 'text'. Both hold the English string of a text key, both truncated to 200 characters with a trailing ellipsis by the generator. They therefore carry identical templates. Anything that folds them together is correct.",

    "enum id twins": "Several fields ship as a pair: a numeric id and a '<Field>Name' label (Type/TypeName, SubType/SubTypeName, BodyType/BodyTypeName, ManagementDifficultyType/...Name, StrengthTalent/StrengthTalentName, RoomDecoStyle/...Name, ConstructionCategory/...Name, HousingType/...Name). Both are phrased identically here so either renders. Derive the rule, do not hardcode the list: if field F and field F+'Name' BOTH changed on the same row, the numeric one is internal and only the Name row should get a named line. If the numeric id changed ALONE the label did not move, which means the enum was renumbered and nothing the player sees actually changed (HousingType 8 to 9 on ROOM_MerchantRoom in B.0.3.40 is exactly that).",

    "NULL and sentinel values": "The tables use the string 'NULL' and the list ['NULL'] to mean empty, and [-1.0] as a runtime sentinel. A template says 'from {#} to {#}' regardless, so the renderer must map those to readable words ('nothing', 'none') before filling the slots. Do not add per-field wording for it here.",

    "UnitProfileTable FacialDecoType": "In build 24354931 this is null to 0 on 126 rows and FacialDecoTypeName is null to 'None' on the same 126. That is the column being introduced, not 126 appearance changes. Nothing is dropped: the table is mapped to the 'other' fold, so the rows stay reachable behind a details without claiming a build's worth of visual work.",

    "DropTable percentages are derived": "DropItem*_Percentage is computed from the DropItem*_ProbabilityWeight values of the same row, and the source ships it as a string that already contains '%'. Do not give it a 'unit'. When both the weight and the percentage changed on one row they are the same change reported twice.",

    "EventTable.Cooldown unit": "The events page renders this as 'min', and 1440 is one in-game day (the game runs 64,800 ticks per day). The template therefore says 'in-game minutes'. If that is ever contradicted by the code, fix the template, not the data.",

    "AvailablePhases means phase, not floor": "The wiki already labels this 'Available phases' and Ctx maps a floor to an internal difficulty phase. The template says 'phases' on purpose. Calling them floors would be wrong for regions whose floor to phase map is not one to one.",

    "unclear semantics, phrased conservatively": {
      "AffecterTable.Categories, TraitTable.Categories": "Values seen are NULL, Prefer, InDependent, Lycan. They group traits for some selection or exclusion rule, but the rule itself is not in the tables. The template says only that the categories changed. Do not upgrade this to a claim about what a category does until the code is read.",
      "TraitTable.BackgroundTraitWeights, TraitTable.IndividualTraitWeights, TraitTable.SubSkillTreeWeights, ClanRankTable.LycanWeight, SpawnGroupTable.Weight, DungeonRoomTable.Weight": "These are relative weights inside a weighted draw, not percentages. The templates say 'weighting' and 'selection weight' rather than 'chance' for that reason.",
      "TradePoolTable.MinDiscountPers, TradePoolTable.MaxDiscountPers": "Named 'discount' by the game but the values go negative (-25 at TRADEPOOL_GuildMerchant1), so a negative value is a markup, not a discount. The templates say 'price adjustment', which is true in both directions.",
      "UnitTable.SubFaction": "The units page labels this column 'Sub-faction' and its values mix creature kinds with roles (Insect, NPC, Abyss). What it drives beyond filtering is not established, so the template only reports the move.",
      "ResearchTable.GridY": "A coordinate on the research board layout. It moves a node on screen; it is not a balance change. Phrased as a position so it reads honestly in an expander.",
      "CampaignStartingTable.StartingBuildingsSpawnPos": "Tile coordinates in '(x/y)' form for the scenario's opening layout. Reported as positions, with no claim about what moving them does.",
      "EventTable.KeyWord": "A record string the event writes when it fires, used by later start conditions. 'Keyword recorded by' is the safest reading of it.",
      "AffecterTable 'individual effects'": "AffecterTable TypeName 'Individual' composes to 'individual effects', which is left alone. There are 78 of them against 77 Individual rows in TraitTable, so they LOOK like the effect twins of the personal traits, but the tables do not say so and 'personal trait effects' would be a claim. 'individual effects' leaks no developer noun and asserts nothing.",
      "StatusTable 'stats'": "The 131 STATUS_* rows are the game's stat and resource definitions (Strength, Constitution, WillPower, Intelligence, Agility, Perception and the derived ones). 'stats' is what they are; the site's page title 'Statuses' does not singularise.",
      "TraitTable.PerceptionPerModifierMin, TraitTable.PerceptionPerModifierMax": "The wiki shows these as 'Talent per-modifier ranges' with a Min and a Max column and no percent sign, so the templates say 'the bottom / the top of the range' and carry no unit. The other five talents use the same '<Stat>PerModifierMin/Max' shape and will land on _default until they change and get an entry."
    },

    "fields deliberately absent": "DevComment, DevComment1..3, DevComment_SkillFloatValues, Dev_Description, Dev_Title, DevIndex and DevAlignOrder appear in the diffs (23 table.field pairs) but are Korean-only developer notes and internal ordering. They are not phrased here, deliberately, and they must not reach the page as a line of their own. See 'engine requests' R4: this file cannot suppress them, because patchnote._field_kind matches them before it ever reads this file.",

    "when a new field appears": "It falls to _default and still renders a sentence. That is working as intended, not a bug. Add a named entry only when the field's meaning is known from the tables or the code. Never guess a phrasing to make the fallback go away.",

    "engine requests": {
      "_preface": "Everything below is OUTSIDE this file's reach. It is written here because this file is the vocabulary and these are the six places where the vocabulary cannot land. Ordered by how wrong the page is without them. R1 is a factual inversion, not a wording problem.",

      "R1 patchnote._DYNAMIC_TPL prints its values BACKWARDS": "The five generated-field templates read '{#} now drop {#} instead of {#}', i.e. NEW then OLD, but they declare no 'slots' key, so _slots() gives them the default 3-slot order [subject, old, new] and sentence() fills them old-then-new. Every sentence they produce states the change in reverse. They are invisible today only because this file has an explicit entry for all four DropTable shapes and one for ClanRankTable.LycanWeight; ClanRankTable has SEVEN race weight columns (Human, Lizardman, Lycan, Elf, Dwarf, WhiteDemon, Tal) plus twelve Level*Weight columns, and every one of them except Lycan already falls through to the inverted rule. Fix by adding \"slots\": [\"subject\", \"new\", \"old\"] to all five, or by rewording them to 'from {#} to {#}'. THEN delete the ClanRankTable.LycanWeight entry from this file: it exists only to mask the bug on the one column that moved in this build, and a race name in a key is the seventh hardcoded list of game things in this codebase.",

      "R2 SUPERSEDED by decision-1 count-only fold - do NOT render the scripts as prose": "Earlier rounds asked value_html to RENDER the action/condition scripts nicely (split_actions/render_action from gen/pages_events.py, token-level diff). The owner's decision this round REVERSES that: no real patch note shows boolean gate expressions or reward scripts, and rendering them - even prettified - leaked 1847 DSL tokens as visible prose. So these fields are now COUNT-ONLY FOLDS: see the 'COUNT-ONLY FOLD CONTRACT' note above. The value slots have been removed from every one of these entries (tpl/tpl_one are value-free; tpl_count/tpl_count_one are the count sentences), so the engine must NOT route DecisionTable.Reward, DecisionTable.Condition, EventTable.StartCondition, QuestTable.CompleteCondition, EventChoiceTable.*, AlertTable.Condition, NPCSpawnConditionTable.*, ResearchTable.ResearchResult or GameSystemTable.StringValues through split_actions()/render_action() for surface display. The raw before/after MAY still be shown behind the member <details> for the curious (that is the one place split_actions/render_action is still welcome, since a <details> is not surface prose), but the surface line stays a bare count. The old request to prettify scripts is retired; if any code was written toward it, point it at the fold instead.",

      "R3 read _table_nouns and _nouns from this file": "table_noun() derives a noun from the TABLE NAME, which is a developer name, so the page says 'text keys', 'unit profiles', 'affecters', 'event nodes', 'drops', 'campaign startings', 'game systems', 'npcspawn conditions' and '2 status'. group_noun() then composes a member column in front of it, which is where 'background traits' comes from (the game's word is 'backgrounds'). This file now carries the two override maps: _table_nouns (table -> plural noun) consulted at the top of table_noun(), and _nouns (composed lowercase phrase -> plural noun) consulted on every return path of group_noun(). Both must fall through to the current derivation when a key is absent, which is what keeps them from rotting: a new table or a renamed column simply behaves as it does today. Both are read by pages_versiondiff's lede as well, since it calls the same two functions, so the fix lands on the summary sentence at no extra cost. NOTE: this is the one request that changes nothing until it is wired, and until then 'affecters', 'text keys' and 'background traits' stay on the page.",

      "R4 the DevComment family must not get a line of its own": "_field_kind() matches _DEV_FIELD_RE and returns ('dev', 'other') before it ever consults this file, so nothing here can route it. sentence() then prints 'Internal developer notes changed on 51 event choices.' Those columns are the Korean notes the game's own writers leave for themselves; they are not a player-facing change and they cannot be phrased into one without stating what the developers were thinking, which is the rule this page is built on. Requested: give 'dev' the treatment 'polish' already has, i.e. no named line, members reachable inside the Text fold's expander; or classify it 'drop', which the engine already supports. Either satisfies 'nothing is dropped' better than the current line does, because the current line says nothing a reader can use.",

      "R5 the sub tree clause needs a plural form": "_SUB_LEAD and _SUB_CONT in patchnote.py are indexed [0]/[1] on whether the SUBJECT is one or many, but the noun that needs to agree is the VALUE: 'now takes Stake, Warrior as a sub tree' must read 'as a sub tree' for one tree name and 'as sub trees' for several. _tree_names_html knows the count. Same for _MAIN_FRAG if a main tree list ever carries more than one name. Both fragments must stay one msgt() template, so the plural has to be a second fragment string, not a concatenation.",

      "R6 the entity-key prefix list: ALREADY FIXED mid-run, do not undo": "Not a request. patchnote._RAWKEY_RE was a hand-typed prefix literal and was replaced during this same run by common.raw_key_re() / common.partial_key_re(), which derive the set from the keys the parsed tables actually carry (common.entity_key_prefixes, 29 prefixes). Recorded here because the old literal was measurably wrong and the next agent must not retype one: over data/*Table.json in build 24354931 it missed GAMESYSTEM 451 rows, UNITVISUAL 142, SPAWN 91, ALERT 57, BASEAREAROOM 51, CADIDATEPRESET 35 (the game's own typo), ROOM 34, CAMPAIGNSTARTING 27, EMOTIONBUBBLE 23, NPCSC 9, TRADEPOOL 6, OPERATION 3 and FARMING 2, while three prefixes it DID carry matched nothing at all, the real ones being SPAWN_ not SPAWNGROUP_, ROOM_ not HOUSINGROOM_ and BASEAREAROOM_ not BASEAREA_. The derived set covers all of them and needs no edit when the game adds a table."
    }
  }
}
