From e0413aebc548d5c8b22c96702b12d01f45302554 Mon Sep 17 00:00:00 2001 From: andrei <68240139+andrei8l@users.noreply.github.com> Date: Fri, 22 Apr 2022 06:48:20 +0300 Subject: [PATCH] Rework NPC shop restocking (#56720) * zones: fix get_point_set_loot respect faction parameter and actually only get loot zones check vehicle-bound zones too * zones: fix get_near_zone_type_for_item respect faction parameter and don't return a zone type if it doesn't actually exist * zones: fix load_world_zones * mapgen: place zones on the right z-level * zones: fix zone rotation boundary also reorganize this function so it's less circuitous and not so confusing * mapgen: add support for custom loot zones * zones: fix get_near() for custom loot zones * zones: add LOOT_ITEM_GROUP * npctrade: zone-based restocking place restocked and traded items in nearby zones create fallback zone if necessary consume items in zones proportional to time elapsed since last restock npctrade: shuffle restock destination point set and use the unsorted zone as a spillover fallback npctrade: allow distributing items to vehicle zones * map: restock NPC shops on map load * npc: fully initialize shopkeeper_item_group may fix this UB https://github.com/CleverRaven/Cataclysm-DDA/runs/5997943701?check_suite_focus=true#step:16:5897 * json/mapgen: add loot zones for isolated artisans * json/isolated_artisans: obsolete EOC-based restock * json/isolated_artisans: update shop item list so that each restock is roughly equivalent to the original mapgen loot distribution * json/mapgen: add loot zones for evac shelter * json/mapgen: add loot zones for exodii merchant * json/exodii: obsolete chunk-based restocking --- data/json/effect_on_condition.json | 42 ---- data/json/loot_zones.json | 7 + data/json/mapgen/exodii/exodii_nested.json | 108 +--------- .../isolated_road_home_chunks.json | 67 +++++-- .../mapgen/refugee_center/refugee_center.json | 7 +- .../exodii/exodii_merchant_definitions.json | 7 +- .../npcs/exodii/exodii_merchant_itemlist.json | 12 ++ .../npcs/exodii/exodii_merchant_talk.json | 65 ------ .../isolated_road_item_groups.json | 188 +----------------- .../isolated_road/isolated_road_npcs.json | 22 +- src/activity_item_handling.cpp | 2 +- src/clzones.cpp | 148 ++++++++------ src/clzones.h | 12 +- src/map.cpp | 8 + src/mapgen.cpp | 18 +- src/npc.cpp | 28 ++- src/npc_class.h | 14 +- src/npctrade.cpp | 9 +- src/npctrade_utils.cpp | 154 ++++++++++++++ src/npctrade_utils.h | 15 ++ src/trade_ui.cpp | 24 ++- 21 files changed, 447 insertions(+), 510 deletions(-) create mode 100644 src/npctrade_utils.cpp create mode 100644 src/npctrade_utils.h diff --git a/data/json/effect_on_condition.json b/data/json/effect_on_condition.json index ea843b542f80a..6d87cbf3a89b6 100644 --- a/data/json/effect_on_condition.json +++ b/data/json/effect_on_condition.json @@ -375,47 +375,5 @@ ] }, "false_effect": { "u_message": "The recorder prints: \"NO DATA\" in a short piece of paper", "type": "info" } - }, - { - "type": "effect_on_condition", - "id": "isolated_artisans_gunsmith_refresh", - "eoc_type": "OM_MOVE", - "condition": { - "and": [ - { "u_near_om_location": "isolated_house_farm_gunsmith", "range": 2 }, - { "compare_int": [ { "global_val": "var", "var_name": "jay_dead" }, "!=", { "const": 1 } ] }, - { - "not": { "u_compare_time_since_var": "gunsmith_shop_update", "type": "timer", "context": "artisans", "op": "<", "time": "7 d" } - } - ] - }, - "effect": [ - { "mapgen_update": "jay_shop_update", "om_terrain": "isolated_house_farm_gunsmith" }, - { "u_add_var": "gunsmith_shop_update", "type": "timer", "context": "artisans", "time": true } - ] - }, - { - "type": "effect_on_condition", - "id": "isolated_artisans_blacksmith_refresh", - "eoc_type": "OM_MOVE", - "condition": { - "and": [ - { "u_near_om_location": "isolated_house_farm_blacksmith", "range": 2 }, - { "compare_int": [ { "global_val": "var", "var_name": "cody_dead" }, "!=", { "const": 1 } ] }, - { - "not": { - "u_compare_time_since_var": "blacksmith_shop_update", - "type": "timer", - "context": "artisans", - "op": "<", - "time": "7 d" - } - } - ] - }, - "effect": [ - { "mapgen_update": "cody_shop_update", "om_terrain": "isolated_house_farm_blacksmith" }, - { "u_add_var": "blacksmith_shop_update", "type": "timer", "context": "artisans", "time": true } - ] } ] diff --git a/data/json/loot_zones.json b/data/json/loot_zones.json index 5681b7ca50069..f768e6cfb164c 100644 --- a/data/json/loot_zones.json +++ b/data/json/loot_zones.json @@ -251,6 +251,13 @@ "can_be_personal": true, "description": "Favorite items inside of this zone are ignored by \"sort out loot\" zone-action." }, + { + "id": "LOOT_ITEM_GROUP", + "type": "LOOT_ZONE", + "name": "Loot: item_group", + "can_be_personal": false, + "description": "Destination for items from an item_group" + }, { "id": "NO_AUTO_PICKUP", "type": "LOOT_ZONE", diff --git a/data/json/mapgen/exodii/exodii_nested.json b/data/json/mapgen/exodii/exodii_nested.json index 657cb93bab12c..0e85d1f1ba9da 100644 --- a/data/json/mapgen/exodii/exodii_nested.json +++ b/data/json/mapgen/exodii/exodii_nested.json @@ -10,114 +10,24 @@ "T__c_", "1__cc", "L____", - "2345T", + "1111T", " " ], "place_npcs": [ { "class": "exodii_merchant", "x": 2, "y": 1 } ], + "place_zones": [ + { "type": "LOOT_UNSORTED", "faction": "exodii", "x": [ 0, 0 ], "y": [ 1, 1 ] }, + { "type": "LOOT_BIONICS", "faction": "exodii", "x": [ 0, 0 ], "y": [ 3, 3 ] }, + { "type": "LOOT_ARMOR", "faction": "exodii", "x": [ 1, 1 ], "y": [ 3, 3 ] }, + { "type": "LOOT_AMMO", "faction": "exodii", "x": [ 2, 2 ], "y": [ 3, 3 ] }, + { "type": "LOOT_GUNS", "faction": "exodii", "x": [ 3, 3 ], "y": [ 3, 3 ] } + ], "terrain": { "_": "t_metal_floor", "|": "t_junk_wall", "T": "t_metal_floor" }, "monster": { "T": { "monster": "mon_exodii_turret" } }, "mapping": { - "1": { - "terrain": "t_metal_floor", - "furniture": "f_locker", - "items": [ { "item": "EXODII_Shop_NomadGear_Tier0", "chance": 100 } ] - }, - "2": { - "terrain": "t_metal_floor", - "furniture": "f_locker", - "items": [ { "item": "EXODII_Shop_Guns_Tier0", "chance": 100 } ] - }, - "3": { - "terrain": "t_metal_floor", - "furniture": "f_locker", - "items": [ { "item": "EXODII_Shop_OldArmour", "chance": 100, "repeat": [ 1, 2 ] } ] - }, - "4": { - "terrain": "t_metal_floor", - "furniture": "f_locker", - "items": [ { "item": "EXODII_Shop_NomadGear_Tier0", "chance": 100 } ] - }, - "5": { - "terrain": "t_metal_floor", - "furniture": "f_locker", - "items": [ { "item": "EXODII_Shop_Guns_Tier0", "chance": 100 } ] - }, + "1": { "terrain": "t_metal_floor", "furniture": "f_locker" }, "c": { "terrain": "t_metal_floor", "furniture": "f_metal_table" }, "L": { "terrain": "t_metal_floor", "furniture": "f_exodii_lamp" } } } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "CBM_shop_initial", - "//": "Fill lockers with CBMs from Tier1 and one bonus tier2 item.", - "object": { - "place_items": [ - { "item": "EXODII_CBM_Store_Tier1", "x": 19, "y": 21, "chance": 100, "repeat": [ 2, 4 ] }, - { "item": "EXODII_CBM_Store_Tier1", "x": [ 19, 21 ], "y": 23, "chance": 100, "repeat": [ 4, 6 ] }, - { "item": "EXODII_CBM_Store_Tier2", "x": 22, "y": 23, "chance": 100 } - ], - "faction_owner": [ { "id": "exodii", "x": 19, "y": 21 }, { "id": "exodii", "x": [ 19, 22 ], "y": 23 } ] - } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "tier1_CBM_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back.", - "object": { - "place_items": [ - { "item": "EXODII_CBM_Store_Tier1", "x": 19, "y": 21, "chance": 50, "repeat": [ 1, 2 ] }, - { "item": "EXODII_CBM_Store_Tier1", "x": [ 19, 20 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] }, - { "item": "EXODII_Shop_Tier0", "x": [ 21, 22 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] } - ], - "faction_owner": [ { "id": "exodii", "x": 19, "y": 21 }, { "id": "exodii", "x": [ 19, 22 ], "y": 23 } ] - } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "tier2_CBM_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back. Add some tier 2 stuff now.", - "object": { - "place_items": [ - { "item": "EXODII_CBM_Store_Tier1", "x": 19, "y": 21, "chance": 50, "repeat": [ 1, 2 ] }, - { "item": "EXODII_CBM_Store_Tier2", "x": [ 19, 20 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] }, - { "item": "EXODII_Shop_Tier0", "x": [ 21, 22 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] } - ], - "faction_owner": [ { "id": "exodii", "x": 19, "y": 21 }, { "id": "exodii", "x": [ 19, 22 ], "y": 23 } ] - } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "tier3_CBM_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back. Add some tier 3 stuff now.", - "object": { - "place_items": [ - { "item": "EXODII_CBM_Store_Tier1", "x": 19, "y": 21, "chance": 25 }, - { "item": "EXODII_CBM_Store_Tier3", "x": 19, "y": 21, "chance": 50 }, - { "item": "EXODII_CBM_Store_Tier2", "x": [ 19, 20 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] }, - { "item": "EXODII_Shop_Tier0", "x": [ 21, 22 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] } - ], - "faction_owner": [ { "id": "exodii", "x": 19, "y": 21 }, { "id": "exodii", "x": [ 19, 22 ], "y": 23 } ] - } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "tier4_CBM_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back. Add all the good stuff now.", - "object": { - "place_items": [ - { "item": "EXODII_CBM_Store_Tier1", "x": 19, "y": 21, "chance": 25 }, - { "item": "EXODII_CBM_Store_Tier3", "x": 19, "y": 21, "chance": 50 }, - { "item": "EXODII_CBM_Store_Tier2", "x": 19, "y": 23, "chance": 40 }, - { "item": "EXODII_CBM_Store_Tier4", "x": 20, "y": 23, "chance": 25 }, - { "item": "EXODII_Shop_Tier0", "x": [ 21, 22 ], "y": 23, "chance": 50, "repeat": [ 1, 2 ] } - ], - "faction_owner": [ { "id": "exodii", "x": 19, "y": 21 }, { "id": "exodii", "x": [ 19, 22 ], "y": 23 } ] - } } ] diff --git a/data/json/mapgen/isolated_road/isolated_road_home_chunks.json b/data/json/mapgen/isolated_road/isolated_road_home_chunks.json index 3dda367743fcb..d7fc454a2d0a4 100644 --- a/data/json/mapgen/isolated_road/isolated_road_home_chunks.json +++ b/data/json/mapgen/isolated_road/isolated_road_home_chunks.json @@ -36,16 +36,51 @@ { "monster": "mon_turret_artisans", "x": 8, "y": 13, "chance": 100 }, { "monster": "mon_turret_artisans", "x": 4, "y": 13, "chance": 100 } ], - "place_loot": [ - { "group": "ammo_rifle_common", "x": 2, "y": 7, "chance": 100 }, - { "group": "guns_rifle_milspec", "x": 2, "y": 7, "chance": 100 }, - { "group": "gunsmith_basic_ammo", "x": 3, "y": 10, "chance": 100, "repeat": [ 1, 4 ] }, - { "group": "gunsmith_basic_ammo", "x": 5, "y": 10, "chance": 100, "repeat": [ 1, 4 ] }, - { "group": "gunsmith_basic_ammo", "x": 6, "y": 10, "chance": 100, "repeat": [ 1, 4 ] }, - { "group": "gunsmith_basic_ammo", "x": 7, "y": 10, "chance": 100, "repeat": [ 1, 4 ] }, - { "group": "gunsmith_repack_ammo", "x": 3, "y": 12, "chance": 100, "repeat": [ 3, 6 ] }, - { "group": "gunsmith_repack_ammo", "x": 9, "y": 12, "chance": 100, "repeat": [ 3, 6 ] }, - { "group": "gunsmith_repack_ammo", "x": 9, "y": 11, "chance": 100, "repeat": [ 3, 6 ] } + "place_zones": [ + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 2, 2 ], + "y": [ 7, 7 ], + "filter": "ammo_rifle_common" + }, + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 2, 2 ], + "y": [ 7, 7 ], + "filter": "guns_rifle_milspec" + }, + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 3, 3 ], + "y": [ 10, 10 ], + "filter": "gunsmith_basic_ammo" + }, + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 5, 7 ], + "y": [ 10, 10 ], + "filter": "gunsmith_basic_ammo" + }, + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 3, 3 ], + "y": [ 12, 12 ], + "filter": "gunsmith_repack_ammo" + }, + { + "type": "LOOT_ITEM_GROUP", + "faction": "isolated_artisans", + "x": [ 9, 9 ], + "y": [ 11, 12 ], + "filter": "gunsmith_repack_ammo" + }, + { "type": "LOOT_AMMO", "faction": "isolated_artisans", "x": [ 5, 7 ], "y": [ 10, 10 ] }, + { "type": "LOOT_UNSORTED", "faction": "isolated_artisans", "x": [ 9, 9 ], "y": [ 11, 12 ] } ], "place_npcs": [ { "class": "jay_ruckers", "x": 6, "y": 13 } ], "faction_owner": [ { "id": "isolated_artisans", "x": [ 0, 23 ], "y": [ 0, 23 ] } ] @@ -85,11 +120,13 @@ ], "palettes": [ "artisans" ], "place_monster": [ { "monster": "mon_turret_artisans", "x": 17, "y": 10, "chance": 100 } ], - "place_loot": [ - { "group": "CODY_MILLER_trade", "chance": 100, "x": 12, "y": 8 }, - { "group": "CODY_MILLER_trade", "chance": 100, "x": 12, "y": 9 }, - { "group": "CODY_MILLER_trade", "chance": 100, "x": 12, "y": 10 }, - { "group": "CODY_MILLER_trade", "chance": 100, "x": 12, "y": 11 } + "place_zones": [ + { "type": "LOOT_ARMOR", "faction": "isolated_artisans", "x": [ 12, 12 ], "y": [ 8, 8 ] }, + { "type": "LOOT_CLOTHING", "faction": "isolated_artisans", "x": [ 12, 12 ], "y": [ 9, 9 ] }, + { "type": "LOOT_WEAPONS", "faction": "isolated_artisans", "x": [ 12, 12 ], "y": [ 10, 10 ] }, + { "type": "LOOT_GUNS", "faction": "isolated_artisans", "x": [ 12, 12 ], "y": [ 11, 11 ] }, + { "type": "LOOT_MANUALS", "faction": "isolated_artisans", "x": [ 17, 17 ], "y": [ 7, 7 ] }, + { "type": "LOOT_UNSORTED", "faction": "isolated_artisans", "x": [ 12, 12 ], "y": [ 8, 11 ] } ], "place_item": [ { "item": "blacksmith_prototypes", "chance": 100, "x": 17, "y": 7 } ], "place_npcs": [ { "class": "cody_miller", "x": 16, "y": 7 } ], diff --git a/data/json/mapgen/refugee_center/refugee_center.json b/data/json/mapgen/refugee_center/refugee_center.json index 422a39ecf4dfc..d5968f064681d 100644 --- a/data/json/mapgen/refugee_center/refugee_center.json +++ b/data/json/mapgen/refugee_center/refugee_center.json @@ -203,7 +203,8 @@ { "type": "NPC_INVESTIGATE_ONLY", "faction": "wasteland_scavengers", "x": [ 72, 95 ], "y": [ 0, 23 ] }, { "type": "NPC_NO_INVESTIGATE", "faction": "free_merchants", "x": [ 24, 32 ], "y": [ 3, 20 ] }, { "type": "NPC_NO_INVESTIGATE", "faction": "wasteland_scavengers", "x": [ 24, 32 ], "y": [ 3, 20 ] }, - { "type": "NPC_INVESTIGATE_ONLY", "faction": "lobby_beggars", "x": [ 51, 68 ], "y": [ 21, 23 ] } + { "type": "NPC_INVESTIGATE_ONLY", "faction": "lobby_beggars", "x": [ 51, 68 ], "y": [ 21, 23 ] }, + { "type": "LOOT_UNSORTED", "faction": "free_merchants", "x": [ 72, 72 ], "y": [ 23, 23 ] } ], "items": { "D": { "item": "trash", "chance": 60, "repeat": [ 1, 3 ] }, @@ -286,7 +287,9 @@ { "type": "NPC_INVESTIGATE_ONLY", "faction": "old_guard", "x": [ 29, 47 ], "y": [ 2, 19 ] }, { "type": "NPC_INVESTIGATE_ONLY", "faction": "old_guard", "x": [ 48, 59 ], "y": [ 2, 19 ] }, { "type": "NPC_INVESTIGATE_ONLY", "faction": "old_guard", "x": [ 48, 48 ], "y": [ 0, 0 ] }, - { "type": "NPC_INVESTIGATE_ONLY", "faction": "lobby_beggars", "x": [ 51, 68 ], "y": [ 0, 4 ] } + { "type": "NPC_INVESTIGATE_ONLY", "faction": "lobby_beggars", "x": [ 51, 68 ], "y": [ 0, 4 ] }, + { "type": "LOOT_CURRENCY", "faction": "free_merchants", "x": [ 70, 70 ], "y": [ 1, 1 ] }, + { "type": "LOOT_WOOD", "faction": "free_merchants", "x": [ 71, 71 ], "y": [ 1, 1 ] } ], "items": { "@": { "item": "bed", "chance": 80 }, diff --git a/data/json/npcs/exodii/exodii_merchant_definitions.json b/data/json/npcs/exodii/exodii_merchant_definitions.json index 9f0e430d80023..ac8df84270a77 100644 --- a/data/json/npcs/exodii/exodii_merchant_definitions.json +++ b/data/json/npcs/exodii/exodii_merchant_definitions.json @@ -21,7 +21,12 @@ "bonus_dex": { "rng": [ 0, 6 ] }, "bonus_int": { "rng": [ 0, 2 ] }, "bonus_per": { "rng": [ 0, 4 ] }, - "shopkeeper_item_group": "EXODII_Shop_Tier0", + "shopkeeper_item_group": [ + { "group": "EXODII_basic_trade", "rigid": true }, + { "group": "EXODII_CBM_Store_Tier2", "rigid": true, "trust": 10 }, + { "group": "EXODII_CBM_Store_Tier3", "rigid": true, "trust": 20, "strict": true }, + { "group": "EXODII_CBM_Store_Tier4", "rigid": true, "trust": 40, "strict": true } + ], "carry_override": "NC_EXODII_TYPE_1_carried", "worn_override": "EMPTY_GROUP", "skills": [ diff --git a/data/json/npcs/exodii/exodii_merchant_itemlist.json b/data/json/npcs/exodii/exodii_merchant_itemlist.json index ec809883808ae..3e259f8d53226 100644 --- a/data/json/npcs/exodii/exodii_merchant_itemlist.json +++ b/data/json/npcs/exodii/exodii_merchant_itemlist.json @@ -1,4 +1,16 @@ [ + { + "type": "item_group", + "id": "EXODII_basic_trade", + "subtype": "collection", + "entries": [ + { "group": "EXODII_Shop_Tier0", "count": 1, "prob": 100 }, + { "group": "EXODII_Shop_NomadGear_Tier0", "count": [ 1, 2 ], "prob": 100 }, + { "group": "EXODII_Shop_Guns_Tier0", "count": 1, "prob": 100 }, + { "group": "EXODII_Shop_OldArmour", "count": 1, "prob": 100 }, + { "group": "EXODII_CBM_Store_Tier1", "count": 1, "prob": 100 } + ] + }, { "type": "item_group", "id": "EXODII_Shop_Tier0", diff --git a/data/json/npcs/exodii/exodii_merchant_talk.json b/data/json/npcs/exodii/exodii_merchant_talk.json index 19d78dabd1456..99b0b99a4bd71 100644 --- a/data/json/npcs/exodii/exodii_merchant_talk.json +++ b/data/json/npcs/exodii/exodii_merchant_talk.json @@ -146,71 +146,6 @@ }, "effect": [ { "arithmetic": [ { "u_val": "sold" }, "-=", { "const": 20 } ] }, { "u_add_faction_trust": 1 } ] }, - { - "//": "Every week, add some stuff to the shop based on your current trust.", - "condition": { - "and": [ - { "u_has_faction_trust": 1 }, - { "not": { "u_has_effect": "u_exodii_interaction_timer_long" } }, - { "u_has_trait": "CBM_Interface" } - ] - }, - "effect": [ { "mapgen_update": "tier1_CBM_shop_update", "om_terrain": "exodii_base_x0y2z1" } ] - }, - { - "//": "Every week, add some stuff to the shop based on your current trust.", - "condition": { - "and": [ - { "u_has_faction_trust": 10 }, - { - "u_compare_time_since_var": "u_met_Rubik", - "type": "timer", - "context": "first_meeting", - "op": ">=", - "time": "14 d" - }, - { "not": { "u_has_effect": "u_exodii_interaction_timer_long" } }, - { "u_has_trait": "CBM_Interface" } - ] - }, - "effect": [ { "mapgen_update": "tier2_CBM_shop_update", "om_terrain": "exodii_base_x0y2z1" } ] - }, - { - "//": "Every week, add some stuff to the shop based on your current trust.", - "condition": { - "and": [ - { "u_has_faction_trust": 20 }, - { - "u_compare_time_since_var": "u_met_Rubik", - "type": "timer", - "context": "first_meeting", - "op": ">=", - "time": "21 d" - }, - { "not": { "u_has_effect": "u_exodii_interaction_timer_long" } }, - { "u_has_trait": "CBM_Interface" } - ] - }, - "effect": [ { "mapgen_update": "tier3_CBM_shop_update", "om_terrain": "exodii_base_x0y2z1" } ] - }, - { - "//": "Every week, add some stuff to the shop based on your current trust.", - "condition": { - "and": [ - { "u_has_faction_trust": 40 }, - { - "u_compare_time_since_var": "u_met_Rubik", - "type": "timer", - "context": "first_meeting", - "op": ">=", - "time": "56 d" - }, - { "not": { "u_has_effect": "u_exodii_interaction_timer_long" } }, - { "u_has_trait": "CBM_Interface" } - ] - }, - "effect": [ { "mapgen_update": "tier4_CBM_shop_update", "om_terrain": "exodii_base_x0y2z1" } ] - }, { "//": "Governs the special CBM stock availability and restock, also adds a little extra faction rep.", "condition": { "not": { "u_has_effect": "u_exodii_interaction_timer_long" } }, diff --git a/data/json/npcs/isolated_road/isolated_road_item_groups.json b/data/json/npcs/isolated_road/isolated_road_item_groups.json index 35bf0de1004fd..d75c72674aec0 100644 --- a/data/json/npcs/isolated_road/isolated_road_item_groups.json +++ b/data/json/npcs/isolated_road/isolated_road_item_groups.json @@ -4,12 +4,12 @@ "id": "CODY_MILLER_trade", "subtype": "collection", "entries": [ - { "group": "sca_smith_manuals", "count": [ 0, 1 ], "prob": 100 }, - { "group": "sca_smith_armor", "count": [ 1, 2 ], "prob": 100 }, - { "group": "sca_smith_ranged", "count": [ 0, 1 ], "prob": 100 }, - { "group": "sca_smith_weapons", "count": [ 0, 1 ], "prob": 100 }, - { "group": "sca_smith_accessories", "count": [ 0, 1 ], "prob": 100 }, - { "group": "sca_smith_ammo", "count": [ 0, 2 ], "prob": 100 } + { "group": "sca_smith_manuals", "count": [ 0, 4 ], "prob": 100 }, + { "group": "sca_smith_armor", "count": [ 4, 8 ], "prob": 100 }, + { "group": "sca_smith_ranged", "count": [ 0, 4 ], "prob": 100 }, + { "group": "sca_smith_weapons", "count": [ 0, 4 ], "prob": 100 }, + { "group": "sca_smith_accessories", "count": [ 0, 4 ], "prob": 100 }, + { "group": "sca_smith_ammo", "count": [ 0, 8 ], "prob": 100 } ] }, { @@ -17,8 +17,10 @@ "id": "JAY_RUCKERS_trade", "subtype": "collection", "entries": [ - { "group": "gunsmith_basic_ammo", "count": [ 1, 4 ], "prob": 100 }, - { "group": "gunsmith_repack_ammo", "count": [ 1, 4 ], "prob": 100 } + { "group": "ammo_rifle_common", "count": 1, "prob": 100 }, + { "group": "guns_rifle_milspec", "count": 1, "prob": 100 }, + { "group": "gunsmith_basic_ammo", "count": [ 4, 16 ], "prob": 100 }, + { "group": "gunsmith_repack_ammo", "count": [ 9, 18 ], "prob": 100 } ] }, { @@ -214,175 +216,5 @@ "weight": "3 g", "volume": "1 ml", "flags": [ "TRADER_AVOID" ] - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "jay_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back. Replacing what was there.", - "object": { - "place_nested": [ - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_repack_clear", 30 ], [ "jay_shop_update_repack_add", 30 ] ], - "x": 3, - "y": 12 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_repack_clear", 30 ], [ "jay_shop_update_repack_add", 30 ] ], - "x": 9, - "y": 12 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_repack_clear", 30 ], [ "jay_shop_update_repack_add", 30 ] ], - "x": 9, - "y": 11 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_clear", 30 ], [ "jay_shop_update_add", 30 ] ], - "x": 3, - "y": 10 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_clear", 30 ], [ "jay_shop_update_add", 30 ] ], - "x": 5, - "y": 10 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_clear", 30 ], [ "jay_shop_update_add", 30 ] ], - "x": 6, - "y": 10 - }, - { - "chunks": [ [ "null", 30 ], [ "jay_shop_update_clear", 30 ], [ "jay_shop_update_add", 30 ] ], - "x": 7, - "y": 10 - } - ], - "faction_owner": [ - { "id": "isolated_artisans", "x": [ 5, 7 ], "y": 10 }, - { "id": "isolated_artisans", "x": 9, "y": [ 11, 12 ] }, - { "id": "isolated_artisans", "x": 3, "y": 10 }, - { "id": "isolated_artisans", "x": 3, "y": 12 } - ] - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "jay_shop_update_repack_clear", - "//": "restock a shelf clearing it", - "object": { - "flags": [ "ERASE_ALL_BEFORE_PLACING_TERRAIN" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_floor" }, - "mapping": { - "X": { - "terrain": "t_floor", - "furniture": "f_utility_shelf", - "items": [ { "item": "gunsmith_repack_ammo", "chance": 100, "repeat": [ 3, 6 ] } ] - } - } - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "jay_shop_update_repack_add", - "//": "restock a shelf adding to it", - "object": { - "flags": [ "ALLOW_TERRAIN_UNDER_OTHER_DATA" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_null" }, - "mapping": { "X": { "terrain": "t_null", "items": [ { "item": "gunsmith_repack_ammo", "chance": 100, "repeat": [ 1, 3 ] } ] } } - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "jay_shop_update_clear", - "//": "restock a shelf clearing it", - "object": { - "flags": [ "ERASE_ALL_BEFORE_PLACING_TERRAIN" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_floor" }, - "mapping": { - "X": { - "terrain": "t_floor", - "furniture": "f_utility_shelf", - "items": [ { "item": "gunsmith_basic_ammo", "chance": 100, "repeat": [ 1, 4 ] } ] - } - } - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "jay_shop_update_add", - "//": "restock a shelf adding to it", - "object": { - "flags": [ "ALLOW_TERRAIN_UNDER_OTHER_DATA" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_null" }, - "mapping": { "X": { "terrain": "t_null", "items": [ { "item": "gunsmith_basic_ammo", "chance": 100, "repeat": [ 0, 2 ] } ] } } - } - }, - { - "type": "mapgen", - "method": "json", - "update_mapgen_id": "cody_shop_update", - "//": "Put a bit of extra stuff in the shop every week if you're checking back. Replacing what was there.", - "object": { - "place_nested": [ - { "chunks": [ [ "null", 30 ], [ "cody_shop_update_clear", 30 ], [ "cody_shop_update_add", 30 ] ], "x": 12, "y": 8 }, - { - "chunks": [ [ "null", 30 ], [ "cody_shop_update_clear", 30 ], [ "cody_shop_update_add", 30 ] ], - "x": 12, - "y": 9 - }, - { - "chunks": [ [ "null", 30 ], [ "cody_shop_update_clear", 30 ], [ "cody_shop_update_add", 30 ] ], - "x": 12, - "y": 10 - }, - { - "chunks": [ [ "null", 30 ], [ "cody_shop_update_clear", 30 ], [ "cody_shop_update_add", 30 ] ], - "x": 12, - "y": 11 - } - ], - "faction_owner": [ { "id": "isolated_artisans", "x": 12, "y": [ 8, 11 ] } ] - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "cody_shop_update_clear", - "//": "restock a shelf clearing it", - "object": { - "flags": [ "ERASE_ALL_BEFORE_PLACING_TERRAIN" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_floor" }, - "mapping": { - "X": { "terrain": "t_floor", "furniture": "f_utility_shelf", "items": [ { "item": "CODY_MILLER_trade", "chance": 100 } ] } - } - } - }, - { - "type": "mapgen", - "method": "json", - "nested_mapgen_id": "cody_shop_update_add", - "//": "restock a shelf adding to it", - "object": { - "flags": [ "ALLOW_TERRAIN_UNDER_OTHER_DATA" ], - "mapgensize": [ 1, 1 ], - "rows": [ "X" ], - "terrain": { "X": "t_null" }, - "mapping": { "X": { "terrain": "t_null", "items": [ { "item": "CODY_MILLER_trade", "chance": 100 } ] } } - } } ] diff --git a/data/json/npcs/isolated_road/isolated_road_npcs.json b/data/json/npcs/isolated_road/isolated_road_npcs.json index 588d8cbdc3c92..c2f2d0ddf9b6e 100644 --- a/data/json/npcs/isolated_road/isolated_road_npcs.json +++ b/data/json/npcs/isolated_road/isolated_road_npcs.json @@ -10,14 +10,7 @@ "attitude": 0, "mission": 3, "chat": "TALK_BLACKSMITH_SERVICES", - "faction": "isolated_artisans", - "death_eocs": [ - { - "id": "EOC_DEATH_NPC_CODY", - "eoc_type": "NPC_DEATH", - "effect": [ { "arithmetic": [ { "global_val": "var", "var_name": "cody_dead" }, "=", { "const": 1 } ] } ] - } - ] + "faction": "isolated_artisans" }, { "type": "npc_class", @@ -30,7 +23,7 @@ "bonus_dex": { "rng": [ -1, 1 ] }, "bonus_int": { "rng": [ 1, 3 ] }, "bonus_per": { "rng": [ -1, 1 ] }, - "shopkeeper_item_group": "CODY_MILLER_trade", + "shopkeeper_item_group": [ { "group": "CODY_MILLER_trade", "rigid": true } ], "worn_override": "CODY_MILLER_worn", "carry_override": "CODY_MILLER_carry", "weapon_override": "CODY_MILLER_weild", @@ -56,14 +49,7 @@ "attitude": 0, "mission": 3, "chat": "TALK_GUNSMITH_SERVICES", - "faction": "isolated_artisans", - "death_eocs": [ - { - "id": "EOC_DEATH_NPC_JAY", - "eoc_type": "NPC_DEATH", - "effect": [ { "arithmetic": [ { "global_val": "var", "var_name": "jay_dead" }, "=", { "const": 1 } ] } ] - } - ] + "faction": "isolated_artisans" }, { "type": "npc_class", @@ -76,7 +62,7 @@ "bonus_dex": { "rng": [ -1, 2 ] }, "bonus_int": { "rng": [ 1, 3 ] }, "bonus_per": { "rng": [ 0, 3 ] }, - "shopkeeper_item_group": "JAY_RUCKERS_trade", + "shopkeeper_item_group": [ { "group": "JAY_RUCKERS_trade", "rigid": true } ], "worn_override": "JAY_RUCKERS_worn", "carry_override": "JAY_RUCKERS_carry", "weapon_override": "JAY_RUCKERS_weild", diff --git a/src/activity_item_handling.cpp b/src/activity_item_handling.cpp index 348c029237be1..b72b32764e0c6 100644 --- a/src/activity_item_handling.cpp +++ b/src/activity_item_handling.cpp @@ -2163,7 +2163,7 @@ void activity_on_turn_move_loot( player_activity &act, Character &you ) continue; } - if( id == zone_type_LOOT_CUSTOM && mgr.custom_loot_has( src, &thisitem ) ) { + if( id == zone_type_LOOT_CUSTOM && mgr.custom_loot_has( src, &thisitem, zone_type_LOOT_CUSTOM ) ) { continue; } diff --git a/src/clzones.cpp b/src/clzones.cpp index 17da6df952b21..9821e685c876a 100644 --- a/src/clzones.cpp +++ b/src/clzones.cpp @@ -63,6 +63,7 @@ static const zone_type_id zone_type_LOOT_CUSTOM( "LOOT_CUSTOM" ); static const zone_type_id zone_type_LOOT_DRINK( "LOOT_DRINK" ); static const zone_type_id zone_type_LOOT_FOOD( "LOOT_FOOD" ); static const zone_type_id zone_type_LOOT_IGNORE( "LOOT_IGNORE" ); +static const zone_type_id zone_type_LOOT_ITEM_GROUP( "LOOT_ITEM_GROUP" ); static const zone_type_id zone_type_LOOT_PDRINK( "LOOT_PDRINK" ); static const zone_type_id zone_type_LOOT_PFOOD( "LOOT_PFOOD" ); static const zone_type_id zone_type_LOOT_SEEDS( "LOOT_SEEDS" ); @@ -197,7 +198,7 @@ shared_ptr_fast zone_options::create( const zone_type_id &type ) return make_shared_fast(); } else if( type == zone_type_CONSTRUCTION_BLUEPRINT ) { return make_shared_fast(); - } else if( type == zone_type_LOOT_CUSTOM ) { + } else if( type == zone_type_LOOT_CUSTOM or type == zone_type_LOOT_ITEM_GROUP ) { return make_shared_fast(); } @@ -210,7 +211,7 @@ bool zone_options::is_valid( const zone_type_id &type, const zone_options &optio return dynamic_cast( &options ) != nullptr; } else if( type == zone_type_CONSTRUCTION_BLUEPRINT ) { return dynamic_cast( &options ) != nullptr; - } else if( type == zone_type_LOOT_CUSTOM ) { + } else if( type == zone_type_LOOT_CUSTOM or type == zone_type_LOOT_ITEM_GROUP ) { return dynamic_cast( &options ) != nullptr; } @@ -724,14 +725,13 @@ std::unordered_set zone_manager::get_point_set_loot( const tripoint_ab } std::unordered_set zone_manager::get_point_set_loot( const tripoint_abs_ms &where, - int radius, bool npc_search, const faction_id &/*fac*/ ) const + int radius, bool npc_search, const faction_id &fac ) const { std::unordered_set res; map &here = get_map(); for( const tripoint &elem : here.points_in_radius( here.getlocal( where ), radius ) ) { - const zone_data *zone = get_zone_at( here.getglobal( elem ) ); - // if not a LOOT zone - if( ( !zone ) || ( zone->get_type().str().substr( 0, 4 ) != "LOOT" ) ) { + const zone_data *zone = get_zone_at( here.getglobal( elem ), true, fac ); + if( zone == nullptr ) { continue; } if( npc_search && has( zone_type_NO_NPC_PICKUP, where ) ) { @@ -821,17 +821,31 @@ const zone_data *zone_manager::get_zone_at( const tripoint_abs_ms &where, return nullptr; } -bool zone_manager::custom_loot_has( const tripoint_abs_ms &where, const item *it ) const +bool zone_manager::custom_loot_has( const tripoint_abs_ms &where, const item *it, + const zone_type_id &ztype ) const { - const zone_data *zone = get_zone_at( where, zone_type_LOOT_CUSTOM ); + const zone_data *zone = get_zone_at( where, ztype ); if( !zone || !it ) { return false; } const loot_options &options = dynamic_cast( zone->get_options() ); std::string filter_string = options.get_mark(); - auto z = item_filter_from_string( filter_string ); + if( ztype == zone_type_LOOT_CUSTOM ) { + auto const z = item_filter_from_string( filter_string ); - return z( *it ); + return z( *it ); + } + if( ztype == zone_type_LOOT_ITEM_GROUP ) { + std::set const &gr = + item_group::every_possible_item_from( item_group_id( filter_string ) ); + + return std::any_of( gr.begin(), gr.end(), + [it]( itype const * type ) { + return type->get_id() == it->typeId(); + } ); + } + + return false; } std::unordered_set zone_manager::get_near( const zone_type_id &type, @@ -843,11 +857,8 @@ std::unordered_set zone_manager::get_near( const zone_type_id & for( const tripoint_abs_ms &point : point_set ) { if( point.z() == where.z() ) { if( square_dist( point, where ) <= range ) { - if( it && has( zone_type_LOOT_CUSTOM, point ) ) { - if( custom_loot_has( point, it ) ) { - near_point_set.insert( point ); - } - } else { + if( ( type != zone_type_LOOT_CUSTOM and type != zone_type_LOOT_ITEM_GROUP ) or + ( it != nullptr and custom_loot_has( point, it, type ) ) ) { near_point_set.insert( point ); } } @@ -858,11 +869,8 @@ std::unordered_set zone_manager::get_near( const zone_type_id & for( const tripoint_abs_ms &point : vzone_set ) { if( point.z() == where.z() ) { if( square_dist( point, where ) <= range ) { - if( it && has( zone_type_LOOT_CUSTOM, point ) ) { - if( custom_loot_has( point, it ) ) { - near_point_set.insert( point ); - } - } else { + if( ( type != zone_type_LOOT_CUSTOM and type != zone_type_LOOT_ITEM_GROUP ) or + ( it != nullptr and custom_loot_has( point, it, type ) ) ) { near_point_set.insert( point ); } } @@ -911,37 +919,43 @@ cata::optional zone_manager::get_nearest( const zone_type_id &t } zone_type_id zone_manager::get_near_zone_type_for_item( const item &it, - const tripoint_abs_ms &where, int range ) const + const tripoint_abs_ms &where, int range, const faction_id &fac ) const { const item_category &cat = it.get_category_of_contents(); - if( has_near( zone_type_LOOT_CUSTOM, where, range ) ) { - if( !get_near( zone_type_LOOT_CUSTOM, where, range, &it ).empty() ) { + if( has_near( zone_type_LOOT_CUSTOM, where, range, fac ) ) { + if( !get_near( zone_type_LOOT_CUSTOM, where, range, &it, fac ).empty() ) { return zone_type_LOOT_CUSTOM; } } + if( has_near( zone_type_LOOT_ITEM_GROUP, where, range, fac ) ) { + if( !get_near( zone_type_LOOT_ITEM_GROUP, where, range, &it, fac ).empty() ) { + return zone_type_LOOT_ITEM_GROUP; + } + } if( it.has_flag( STATIC( flag_id( "FIREWOOD" ) ) ) ) { - if( has_near( zone_type_LOOT_WOOD, where, range ) ) { + if( has_near( zone_type_LOOT_WOOD, where, range, fac ) ) { return zone_type_LOOT_WOOD; } } if( it.is_corpse() ) { - if( has_near( zone_type_LOOT_CORPSE, where, range ) ) { + if( has_near( zone_type_LOOT_CORPSE, where, range, fac ) ) { return zone_type_LOOT_CORPSE; } } if( it.typeId() == itype_disassembly ) { - if( has_near( zone_type_zone_disassemble, where, range ) ) { + if( has_near( zone_type_zone_disassemble, where, range, fac ) ) { return zone_type_zone_disassemble; } } cata::optional zone_check_first = cat.priority_zone( it ); - if( zone_check_first && has_near( *zone_check_first, where, range ) ) { + if( zone_check_first && has_near( *zone_check_first, where, range, fac ) ) { return *zone_check_first; } - if( cat.zone() ) { + cata::optional zone_cat = cat.zone(); + if( zone_cat && has_near( *zone_cat, where, range, fac ) ) { return *cat.zone(); } @@ -972,14 +986,14 @@ zone_type_id zone_manager::get_near_zone_type_for_item( const item &it, if( it_food != nullptr ) { if( it_food->get_comestible()->comesttype == "DRINK" ) { - if( perishable && has_near( zone_type_LOOT_PDRINK, where, range ) ) { + if( perishable && has_near( zone_type_LOOT_PDRINK, where, range, fac ) ) { return zone_type_LOOT_PDRINK; - } else if( has_near( zone_type_LOOT_DRINK, where, range ) ) { + } else if( has_near( zone_type_LOOT_DRINK, where, range, fac ) ) { return zone_type_LOOT_DRINK; } } - if( perishable && has_near( zone_type_LOOT_PFOOD, where, range ) ) { + if( perishable && has_near( zone_type_LOOT_PFOOD, where, range, fac ) ) { return zone_type_LOOT_PFOOD; } } @@ -1006,13 +1020,23 @@ std::vector zone_manager::get_zones( const zone_type_id &type, return zones; } -const zone_data *zone_manager::get_zone_at( const tripoint_abs_ms &where ) const +const zone_data *zone_manager::get_zone_at( const tripoint_abs_ms &where, bool loot_only, + const faction_id &fac ) const { + auto const check = [&fac, loot_only, &where]( zone_data const & z ) { + return z.get_faction() == fac and + ( !loot_only || z.get_type().str().substr( 0, 4 ) == "LOOT" ) and + z.has_inside( where ); + }; for( auto it = zones.rbegin(); it != zones.rend(); ++it ) { - const auto &zone = *it; - - if( zone.has_inside( where ) ) { - return &zone; + if( check( *it ) ) { + return &*it; + } + } + auto const vzones = get_map().get_vehicle_zones( get_map().get_abs_sub().z() ); + for( zone_data *it : vzones ) { + if( check( *it ) ) { + return &*it; } } return nullptr; @@ -1163,31 +1187,30 @@ void zone_manager::rotate_zones( map &target_map, const int turns ) if( turns == 0 ) { return; } - const tripoint_abs_ms a_start = target_map.getglobal( tripoint_zero ); - const tripoint_abs_ms a_end = target_map.getglobal( tripoint( 23, 23, 0 ) ); - const point dim( 24, 24 ); + const point dim( SEEX * 2, SEEY * 2 ); for( zone_data &zone : zones ) { - const tripoint_abs_ms z_start = zone.get_start_point(); - const tripoint_abs_ms z_end = zone.get_end_point(); - if( ( a_start.x() <= z_start.x() && a_start.y() <= z_start.y() ) && - ( a_end.x() > z_start.x() && a_end.y() >= z_start.y() ) && - ( a_start.x() <= z_end.x() && a_start.y() <= z_end.y() ) && - ( a_end.x() >= z_end.x() && a_end.y() >= z_end.y() ) ) { - tripoint z_l_start3 = target_map.getlocal( z_start ); - tripoint z_l_end3 = target_map.getlocal( z_end ); + const tripoint a_start( 0, 0, target_map.get_abs_sub().z() ); + const tripoint a_end( SEEX * 2 - 1, SEEY * 2 - 1, a_start.z ); + const tripoint z_start = target_map.getlocal( zone.get_start_point() ); + const tripoint z_end = target_map.getlocal( zone.get_end_point() ); + const inclusive_cuboid boundary( a_start, a_end ); + if( boundary.contains( z_start ) and boundary.contains( z_end ) ) { // don't rotate centered squares - if( z_l_start3.x == z_l_start3.y && z_l_end3.x == z_l_end3.y && z_l_start3.x + z_l_end3.x == 23 ) { + if( z_start.x == z_start.y && z_end.x == z_end.y && + z_start.x + z_end.x == a_end.x ) { continue; } - point z_l_start = z_l_start3.xy().rotate( turns, dim ); - point z_l_end = z_l_end3.xy().rotate( turns, dim ); - point new_z_start = target_map.getabs( z_l_start ); - point new_z_end = target_map.getabs( z_l_end ); - tripoint first = tripoint( std::min( new_z_start.x, new_z_end.x ), - std::min( new_z_start.y, new_z_end.y ), a_start.z() ); - tripoint second = tripoint( std::max( new_z_start.x, new_z_end.x ), - std::max( new_z_start.y, new_z_end.y ), a_end.z() ); - zone.set_position( std::make_pair( first, second ), false ); + point z_l_start = z_start.xy().rotate( turns, dim ); + point z_l_end = z_end.xy().rotate( turns, dim ); + tripoint_abs_ms first = + target_map.getglobal( tripoint( std::min( z_l_start.x, z_l_end.x ), + std::min( z_l_start.y, z_l_end.y ), + z_start.z ) ); + tripoint_abs_ms second = + target_map.getglobal( tripoint( std::max( z_l_start.x, z_l_end.x ), + std::max( z_l_start.y, z_l_end.y ), + z_end.z ) ); + zone.set_position( std::make_pair( first.raw(), second.raw() ), false ); } } } @@ -1386,14 +1409,15 @@ void zone_manager::load_world_zones() read_from_file_optional( savefile, [&]( std::istream & fin ) { JsonIn jsin( fin ); jsin.read( tmp ); - for( auto it = tmp.begin(); it != tmp.end(); ++it ) { + for( auto it = tmp.begin(); it != tmp.end(); ) { const zone_type_id zone_type = it->get_type(); if( !has_type( zone_type ) ) { - tmp.erase( it ); + it = tmp.erase( it ); debugmsg( "Invalid zone type: %s", zone_type.c_str() ); - } - if( it->get_faction() == faction_your_followers ) { - tmp.erase( it ); + } else if( it->get_faction() == faction_your_followers ) { + it = tmp.erase( it ); + } else { + ++it; } } std::copy( tmp.begin(), tmp.end(), std::back_inserter( zones ) ); diff --git a/src/clzones.h b/src/clzones.h index 8515b20fc60f2..1d6bac4c2e19d 100644 --- a/src/clzones.h +++ b/src/clzones.h @@ -218,6 +218,10 @@ class loot_options : public zone_options, public mark_option return mark; } + void set_mark( std::string const &nmark ) { + mark = nmark; + } + bool has_options() const override { return true; } @@ -453,7 +457,8 @@ class zone_manager bool has_near( const zone_type_id &type, const tripoint_abs_ms &where, int range = MAX_DISTANCE, const faction_id &fac = your_fac ) const; bool has_loot_dest_near( const tripoint_abs_ms &where ) const; - bool custom_loot_has( const tripoint_abs_ms &where, const item *it ) const; + bool custom_loot_has( const tripoint_abs_ms &where, const item *it, + const zone_type_id &ztype ) const; std::unordered_set get_near( const zone_type_id &type, const tripoint_abs_ms &where, int range = MAX_DISTANCE, const item *it = nullptr, const faction_id &fac = your_fac ) const; @@ -461,10 +466,11 @@ class zone_manager const zone_type_id &type, const tripoint_abs_ms &where, int range = MAX_DISTANCE, const faction_id &fac = your_fac ) const; zone_type_id get_near_zone_type_for_item( const item &it, const tripoint_abs_ms &where, - int range = MAX_DISTANCE ) const; + int range = MAX_DISTANCE, const faction_id &fac = your_fac ) const; std::vector get_zones( const zone_type_id &type, const tripoint_abs_ms &where, const faction_id &fac = your_fac ) const; - const zone_data *get_zone_at( const tripoint_abs_ms &where ) const; + const zone_data *get_zone_at( const tripoint_abs_ms &where, bool loot_only = false, + const faction_id &fac = your_fac ) const; const zone_data *get_bottom_zone( const tripoint_abs_ms &where, const faction_id &fac = your_fac ) const; cata::optional query_name( const std::string &default_name = "" ) const; diff --git a/src/map.cpp b/src/map.cpp index 33a457dba2c57..dac08f2087592 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -71,6 +71,7 @@ #include "mongroup.h" #include "monster.h" #include "mtype.h" +#include "npc.h" #include "optional.h" #include "options.h" #include "output.h" @@ -7572,6 +7573,13 @@ void map::actualize( const tripoint &grid ) } } + tripoint_abs_sm const sm = abs_sub + grid; + for( auto const &guy : overmap_buffer.get_npcs_near( sm, 0 ) ) { + if( guy->get_location().z() == sm.z() ) { + guy->shop_restock(); + } + } + // the last time we touched the submap, is right now. tmpsub->last_touched = calendar::turn; } diff --git a/src/mapgen.cpp b/src/mapgen.cpp index 767072fcdbf04..2a35e56360744 100644 --- a/src/mapgen.cpp +++ b/src/mapgen.cpp @@ -180,6 +180,9 @@ static const trait_id trait_NPC_STATIC_NPC( "NPC_STATIC_NPC" ); static const vproto_id vehicle_prototype_shopping_cart( "shopping_cart" ); +static const zone_type_id zone_type_LOOT_CUSTOM( "LOOT_CUSTOM" ); +static const zone_type_id zone_type_LOOT_ITEM_GROUP( "LOOT_ITEM_GROUP" ); + #define dbg(x) DebugLog((x),D_MAP_GEN) << __FILE__ << ":" << __LINE__ << ": " static constexpr int MON_RADIUS = 3; @@ -2952,21 +2955,30 @@ class jmapgen_zone : public jmapgen_piece mapgen_value zone_type; mapgen_value faction; std::string name; + std::string filter; jmapgen_zone( const JsonObject &jsi, const std::string &/*context*/ ) : zone_type( jsi.get_member( "type" ) ) , faction( jsi.get_member( "faction" ) ) { if( jsi.has_string( "name" ) ) { name = jsi.get_string( "name" ); } + if( jsi.has_string( "filter" ) ) { + filter = jsi.get_string( "filter" ); + } } void apply( const mapgendata &dat, const jmapgen_int &x, const jmapgen_int &y, const std::string &/*context*/ ) const override { zone_type_id chosen_zone_type = zone_type.get( dat ); faction_id chosen_faction = faction.get( dat ); zone_manager &mgr = zone_manager::get_manager(); - const tripoint start = dat.m.getabs( tripoint( x.val, y.val, 0 ) ); - const tripoint end = dat.m.getabs( tripoint( x.valmax, y.valmax, 0 ) ); - mgr.add( name, chosen_zone_type, chosen_faction, false, true, start, end ); + const tripoint start = dat.m.getabs( tripoint( x.val, y.val, dat.m.get_abs_sub().z() ) ); + const tripoint end = dat.m.getabs( tripoint( x.valmax, y.valmax, dat.m.get_abs_sub().z() ) ); + auto options = zone_options::create( chosen_zone_type ); + if( chosen_zone_type == zone_type_LOOT_CUSTOM or + chosen_zone_type == zone_type_LOOT_ITEM_GROUP ) { + dynamic_cast( &*options )->set_mark( filter ); + } + mgr.add( name, chosen_zone_type, chosen_faction, false, true, start, end, options ); } void check( const std::string &oter_name, const mapgen_parameters ¶meters, diff --git a/src/npc.cpp b/src/npc.cpp index bb2a0555f9e93..8fa26e43babfa 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -54,6 +54,7 @@ #include "mtype.h" #include "mutation.h" #include "npc_class.h" +#include "npctrade_utils.h" #include "npctalk.h" #include "options.h" #include "output.h" @@ -2013,7 +2014,9 @@ int npc::max_willing_to_owe() const void npc::shop_restock() { // NPCs refresh every week, since the last time you checked in - if( ( restock != calendar::turn_zero ) && ( ( calendar::turn - restock ) < 0_days ) ) { + time_duration const elapsed = + restock != calendar::turn_zero ? calendar::turn - restock : 0_days; + if( ( restock != calendar::turn_zero ) && ( elapsed < 0_days ) ) { return; } @@ -2038,10 +2041,8 @@ void npc::shop_restock() return; } - units::volume total_space = volume_capacity(); - if( mission == NPC_MISSION_SHOPKEEP ) { - total_space = units::from_liter( 5000 ); - } + add_fallback_zone( *this ); + consume_items_in_zones( *this, elapsed ); std::list ret; int shop_value = 75000; @@ -2080,13 +2081,12 @@ void npc::shop_restock() if( !value_groups.empty() ) { int count = 0; bool last_item = false; - while( shop_value > 0 && total_space > 0_ml && !last_item ) { + while( shop_value > 0 && !last_item ) { item tmpit = item_group::item_from( random_entry( value_groups ), calendar::turn ); - if( !tmpit.is_null() && total_space >= tmpit.volume() ) { + if( !tmpit.is_null() ) { tmpit.set_owner( *this ); ret.push_back( tmpit ); shop_value -= tmpit.price( true ); - total_space -= tmpit.volume(); count += 1; last_item = count > 10 && one_in( 100 ); } @@ -2101,9 +2101,15 @@ void npc::shop_restock() } } - has_new_items = true; - inv->clear(); - inv->push_back( ret ); + if( mission == NPC_MISSION_SHOPKEEP ) { + distribute_items_to_npc_zones( ret, *this ); + } else { + for( const item &i : ret ) { + i_add( i, true, nullptr, nullptr, true, false ); + } + DebugLog( DebugLevel::D_WARNING, DebugClass::D_GAME ) + << "shop_restock() called on NPC who is not a shopkeeper " << name; + } } int npc::minimum_item_value() const diff --git a/src/npc_class.h b/src/npc_class.h index 924fb4565b3ee..4db9c9d7d4172 100644 --- a/src/npc_class.h +++ b/src/npc_class.h @@ -44,16 +44,16 @@ class distribution }; struct shopkeeper_item_group { - item_group_id id; - int trust; - bool strict; + item_group_id id = item_group_id( "EMPTY_GROUP" ); + int trust = 0; + bool strict = false; // Rigid shopkeeper groups will be processed a single time. Default groups are not rigid, and will be processed until the shopkeeper has no more room or remaining value to populate goods with. - bool rigid; + bool rigid = false; - shopkeeper_item_group() : id( item_group_id( "EMPTY_GROUP" ) ), trust( 0 ), strict( false ) {} - shopkeeper_item_group( const std::string &id, int trust, bool strict ) : - id( item_group_id( id ) ), trust( trust ), strict( strict ) {} + shopkeeper_item_group() = default; + shopkeeper_item_group( const std::string &id, int trust, bool strict, bool rigid = false ) : + id( item_group_id( id ) ), trust( trust ), strict( strict ), rigid( rigid ) {} void deserialize( const JsonObject &jo ); }; diff --git a/src/npctrade.cpp b/src/npctrade.cpp index ba7caa1696354..79e8771c6f909 100644 --- a/src/npctrade.cpp +++ b/src/npctrade.cpp @@ -17,6 +17,7 @@ #include "item_location.h" #include "item_pocket.h" #include "npc.h" +#include "npctrade_utils.h" #include "ret_val.h" #include "skill.h" #include "trade_ui.h" @@ -273,8 +274,12 @@ bool npc_trading::trade( npc &np, int cost, const std::string &deal ) true ); npc_trading::transfer_items( trade_result.items_trader, np, player_character, from_map, false ); // Now move items from escrow to the npc. Keep the weapon wielded. - for( const item &i : escrow ) { - np.i_add( i, true, nullptr, nullptr, true, false ); + if( np.mission == NPC_MISSION_SHOPKEEP ) { + distribute_items_to_npc_zones( escrow, np ); + } else { + for( const item &i : escrow ) { + np.i_add( i, true, nullptr, nullptr, true, false ); + } } for( item_location *loc_ptr : from_map ) { diff --git a/src/npctrade_utils.cpp b/src/npctrade_utils.cpp new file mode 100644 index 0000000000000..66a589d3cecd0 --- /dev/null +++ b/src/npctrade_utils.cpp @@ -0,0 +1,154 @@ +#include "npctrade_utils.h" + +#include +#include + +#include "calendar.h" +#include "clzones.h" +#include "npc.h" +#include "rng.h" +#include "vehicle.h" +#include "vpart_position.h" + +static const zone_type_id zone_type_LOOT_UNSORTED( "LOOT_UNSORTED" ); + +namespace +{ +using consume_cache = std::map; +using consume_queue = std::vector; +using dest_t = std::vector; + +void _consume_item( item_location elem, consume_queue &consumed, consume_cache &cache, npc &guy, + int amount ) +{ + if( !elem->is_owned_by( guy ) ) { + return; + } + std::list const contents = elem->all_items_top( item_pocket::pocket_type::CONTAINER ); + if( contents.empty() ) { + auto it = cache.find( elem->typeId() ); + if( it == cache.end() ) { + it = cache.emplace( elem->typeId(), amount ).first; + } + if( it->second != 0 ) { + consumed.push_back( elem ); + it->second--; + } + } else { + for( item *it : contents ) { + _consume_item( item_location( elem, it ), consumed, cache, guy, amount ); + } + } +} + +dest_t _get_shuffled_point_set( std::unordered_set const &set ) +{ + dest_t ret; + std::copy( set.begin(), set.end(), std::back_inserter( ret ) ); + std::shuffle( ret.begin(), ret.end(), rng_get_engine() ); + return ret; +} + +bool _to_map( item const &it, map &here, tripoint const &dpoint_here ) +{ + bool leftover = true; + if( here.can_put_items_ter_furn( dpoint_here ) and + here.free_volume( dpoint_here ) >= it.volume() ) { + here.add_item_or_charges( dpoint_here, it, false ); + leftover = false; + } + + return leftover; +} + +bool _to_veh( item const &it, cata::optional const vp ) +{ + bool leftover = true; + int const part = static_cast( vp->part_index() ); + if( vp->vehicle().free_volume( part ) >= it.volume() ) { + vp->vehicle().add_item( part, it ); + leftover = false; + } + return leftover; +} + +} // namespace + +void add_fallback_zone( npc &guy ) +{ + zone_manager &zmgr = zone_manager::get_manager(); + tripoint_abs_ms const loc = guy.get_location(); + faction_id const &fac_id = guy.get_fac_id(); + + if( !zmgr.has_near( zone_type_LOOT_UNSORTED, loc, PICKUP_RANGE, fac_id ) ) { + zmgr.add( fallback_name, zone_type_LOOT_UNSORTED, fac_id, false, + true, loc.raw() + tripoint_north_west, loc.raw() + tripoint_south_east ); + DebugLog( DebugLevel::D_WARNING, DebugClass::D_GAME ) + << "Added a fallack loot zone for NPC trader " << guy.name; + } +} + +std::list distribute_items_to_npc_zones( std::list &items, npc &guy ) +{ + zone_manager &zmgr = zone_manager::get_manager(); + map &here = get_map(); + tripoint_abs_ms const loc_abs = guy.get_location(); + faction_id const &fac_id = guy.get_fac_id(); + + std::list leftovers; + dest_t const fallback = _get_shuffled_point_set( + zmgr.get_near( zone_type_LOOT_UNSORTED, loc_abs, PICKUP_RANGE, nullptr, fac_id ) ); + for( item const &it : items ) { + zone_type_id const zid = + zmgr.get_near_zone_type_for_item( it, loc_abs, PICKUP_RANGE, fac_id ); + + dest_t dest = zid.is_valid() ? _get_shuffled_point_set( zmgr.get_near( + zid, loc_abs, PICKUP_RANGE, &it, fac_id ) ) + : dest_t(); + std::copy( fallback.begin(), fallback.end(), std::back_inserter( dest ) ); + + bool leftover = true; + for( tripoint_abs_ms const &dpoint : dest ) { + tripoint const dpoint_here = here.getlocal( dpoint ); + cata::optional const vp = + here.veh_at( dpoint_here ).part_with_feature( "CARGO", false ); + if( vp and vp->vehicle().get_owner() == fac_id ) { + leftover = _to_veh( it, vp ); + } else { + leftover = _to_map( it, here, dpoint_here ); + } + if( !leftover ) { + break; + } + } + if( leftover ) { + leftovers.emplace_back( it ); + } + } + + return leftovers; +} + +void consume_items_in_zones( npc &guy, time_duration const &elapsed ) +{ + std::unordered_set const src = zone_manager::get_manager().get_point_set_loot( + guy.get_location(), PICKUP_RANGE, guy.get_fac_id() ); + + consume_cache cache; + map &here = get_map(); + int constexpr rate = 5; // FIXME: jsonize + + for( tripoint const &pt : src ) { + consume_queue consumed; + std::list stack = + here.items_with( pt, [&guy]( item const & it ) { + return it.is_owned_by( guy ); + } ); + for( item_location &elem : stack ) { + _consume_item( elem, consumed, cache, guy, rate * to_days( elapsed ) ); + } + for( item_location &it : consumed ) { + it.remove_item(); + } + } +} diff --git a/src/npctrade_utils.h b/src/npctrade_utils.h new file mode 100644 index 0000000000000..a84aabe67fc8f --- /dev/null +++ b/src/npctrade_utils.h @@ -0,0 +1,15 @@ +#ifndef CATA_SRC_NPCTRADE_UTILS_H +#define CATA_SRC_NPCTRADE_UTILS_H +#include + +class npc; +class item; +class time_duration; + +constexpr char const *fallback_name = "NPC Shopkeeper unsorted fallback zone"; + +void add_fallback_zone( npc &guy ); +std::list distribute_items_to_npc_zones( std::list &items, npc &guy ); +void consume_items_in_zones( npc &guy, time_duration const &elapsed ); + +#endif // CATA_SRC_NPCTRADE_UTILS_H diff --git a/src/trade_ui.cpp b/src/trade_ui.cpp index 42412a6c489bc..47ab07b39d121 100644 --- a/src/trade_ui.cpp +++ b/src/trade_ui.cpp @@ -6,6 +6,7 @@ #include #include "character.h" +#include "clzones.h" #include "color.h" #include "enums.h" #include "game_constants.h" @@ -13,6 +14,7 @@ #include "item.h" #include "npc.h" #include "npctrade.h" +#include "npctrade_utils.h" #include "output.h" #include "point.h" #include "string_formatter.h" @@ -101,7 +103,27 @@ trade_ui::trade_ui( party_t &you, npc &trader, currency_t cost, std::string titl _panes[_trader]->add_character_items( trader ); if( trader.mission == NPC_MISSION_SHOPKEEP ) { _panes[_trader]->categorize_map_items( true ); - _panes[_trader]->add_nearby_items( PICKUP_RANGE ); + + add_fallback_zone( trader ); + + zone_manager &zmgr = zone_manager::get_manager(); + + // FIXME: migration for traders in old saves - remove after 0.G + zone_data const *const fallback = + zmgr.get_zone_at( trader.get_location(), true, trader.get_fac_id() ); + bool const legacy = fallback != nullptr and fallback->get_name() == fallback_name; + + if( legacy ) { + _panes[_trader]->add_nearby_items( PICKUP_RANGE ); + } else { + std::unordered_set const src = + zmgr.get_point_set_loot( trader.get_location(), PICKUP_RANGE, trader.get_fac_id() ); + + for( tripoint const &pt : src ) { + _panes[_trader]->add_map_items( pt ); + _panes[_trader]->add_vehicle_items( pt ); + } + } } else if( !trader.is_player_ally() ) { _panes[_trader]->add_nearby_items( 1 ); }