diff --git a/data/json/npcs/factions.json b/data/json/npcs/factions.json
index 20eacecdf3e4d..f545e00befff4 100644
--- a/data/json/npcs/factions.json
+++ b/data/json/npcs/factions.json
@@ -9,6 +9,7 @@
"size": 1,
"power": 100,
"fac_food_supply": { "calories": 0, "vitamins": { } },
+ "consumes_food": true,
"wealth": 0,
"relations": {
"your_followers": {
@@ -60,7 +61,9 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { "iron": 800, "calcium": 800, "vitC": 600 } },
+ "//": "90 days worth of food for 10 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "//2": "Expect to see these values a lot.",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 100000000,
"currency": "RobofacCoin",
"price_rules": [ { "group": "NC_ROBOFAC_gear", "markup": 2 } ],
@@ -74,6 +77,7 @@
"defends your space": true,
"knows your voice": true
},
+ "robofac_auxiliaries": { "share my stuff": true },
"marloss": { "kill on sight": true }
},
"epilogues": [
@@ -91,7 +95,8 @@
"known_by_u": false,
"size": 70,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "No food supply, they eat off of robofac's",
+ "fac_food_supply": { "calories": 0, "vitamins": { } },
"wealth": 75000,
"currency": "RobofacCoin",
"relations": {
@@ -117,7 +122,8 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 201600, "vitamins": { } },
+ "//": "FIXME: No camp! One character spawns at the refugee center but can't eat out of their stores",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 100000000,
"relations": {
"old_guard": {
@@ -155,7 +161,9 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "(only) 30 days worth of food for 100 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "//2": "Their canonical starvation is not yet implemented.",
+ "fac_food_supply": { "calories": 9000000000, "vitamins": { "iron": 288000, "calcium": 288000, "vitC": 288000 } },
"wealth": 75000000,
"currency": "FMCNote",
"price_rules": [
@@ -201,6 +209,7 @@
"kill on sight": false,
"watch your back": true,
"share my stuff": false,
+ "share public goods": true,
"guard your stuff": true,
"lets you in": false,
"defends your space": false,
@@ -241,7 +250,8 @@
"known_by_u": false,
"size": 5,
"power": 0,
- "fac_food_supply": { "calories": 1152, "vitamins": { } },
+ "//": "No camp, on purpose! These guys will literally starve.",
+ "fac_food_supply": { "calories": 0, "vitamins": { } },
"wealth": 100,
"relations": {
"lobby_beggars": {
@@ -301,7 +311,7 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 10000000,
"currency": "FMCNote",
"price_rules": [ { "item": "money_strap_FMCNote", "fixed_adj": 0 }, { "item": "money_bundle_FMCNote", "fixed_adj": 0 } ],
@@ -331,7 +341,8 @@
"known_by_u": false,
"size": 2,
"power": 5,
- "fac_food_supply": { "calories": 4000, "vitamins": { } },
+ "//": "180 days worth of food for 2 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 1080000000, "vitamins": { "iron": 34560, "calcium": 34560, "vitC": 34560 } },
"wealth": 100000,
"currency": "FMCNote",
"price_rules": [ { "item": "money_strap_FMCNote", "fixed_adj": 0 }, { "item": "money_bundle_FMCNote", "fixed_adj": 0 } ],
@@ -361,7 +372,8 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 172800, "vitamins": { } },
+ "//": "No camp, on purpose! The pilgrim group should probably be full marloss, and not need to eat?",
+ "fac_food_supply": { "calories": 0, "vitamins": { } },
"wealth": 25000,
"relations": {
"marloss": {
@@ -406,7 +418,8 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 172800, "vitamins": { } },
+ "//": "90 days worth of food for 10 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 2500000,
"relations": {
"lobby_beggars": { "knows your voice": true },
@@ -430,7 +443,7 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 230400, "vitamins": { } },
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 45000000,
"relations": {
"hells_raiders": {
@@ -506,7 +519,8 @@
"known_by_u": false,
"size": 1,
"power": 10,
- "fac_food_supply": { "calories": 2304, "vitamins": { } },
+ "//": "90 days worth of food for 1 person, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 270000000, "vitamins": { "iron": 8640, "calcium": 8640, "vitC": 8640 } },
"wealth": 0,
"relations": {
"apis_hive": {
@@ -534,7 +548,8 @@
"size": 7,
"power": 10,
"currency": "signed_chit",
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "FOUR camps.",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 75000,
"relations": {
"free_merchants": {
@@ -607,7 +622,7 @@
"size": 25,
"power": 20,
"currency": "icon",
- "fac_food_supply": { "calories": 87500, "vitamins": { } },
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 82500,
"relations": {
"gods_community": {
@@ -699,7 +714,8 @@
"size": 1,
"power": 1,
"currency": "fur",
- "fac_food_supply": { "calories": 87500, "vitamins": { } },
+ "//": "90 days worth of food for 1 person, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 270000000, "vitamins": { "iron": 8640, "calcium": 8640, "vitC": 8640 } },
"wealth": 82500,
"relations": {
"free_merchants": {
@@ -781,7 +797,8 @@
"known_by_u": false,
"size": 3,
"power": 3,
- "fac_food_supply": { "calories": 100, "vitamins": { } },
+ "//": "FIXME: Currently no camps! Spawns via % chance in a bit of nested mapgen, then moves to an existing lighthouse (with no way to know which lighthouse during mapgen!). Will probably need some dynamic camp placement to support.",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 20000,
"relations": {
"free_merchants": { "knows your voice": true },
@@ -800,7 +817,8 @@
"known_by_u": true,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 230400, "vitamins": { } },
+ "//": "7 days worth of food for 10 people, at 3000kcal/day and *half* RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 210000000, "vitamins": { "iron": 3360, "calcium": 3360, "vitC": 3360 } },
"wealth": 45000000,
"relations": {
"hells_raiders": {
@@ -832,7 +850,7 @@
"mon_faction": "exodii",
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 75000000,
"price_rules": [ { "group": "games_board_all", "premium": 2.5 } ],
"relations": {
@@ -915,7 +933,7 @@
"known_by_u": false,
"size": 100,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 25000000,
"currency": "FlatCoin",
"price_rules": [
diff --git a/data/json/obsoletion_and_migration_0.I/camp_placement.json b/data/json/obsoletion_and_migration_0.I/camp_placement.json
new file mode 100644
index 0000000000000..1f651412d5505
--- /dev/null
+++ b/data/json/obsoletion_and_migration_0.I/camp_placement.json
@@ -0,0 +1,62 @@
+[
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Refugee Center", "overmap_terrain": "evac_center_13", "faction": "free_merchants" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Exodii", "overmap_terrain": "exodii_base_x1y2z1", "faction": "exodii" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "???", "overmap_terrain": "hive", "faction": "apis_hive" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Lapin", "overmap_terrain": "cabin_lapin", "faction": "lapin" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Holdouts", "overmap_terrain": "prison_island_1_6", "faction": "prisoners" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "New England Church Retreat", "overmap_terrain": "godco_5", "faction": "gods_community" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Tacoma", "overmap_terrain": "ranch_camp_67", "faction": "tacoma_commune" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Hub 01", "overmap_terrain": "robofachq_surface_entrance", "faction": "robofac" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Cody & Jay", "overmap_terrain": "isolated_house_farm_gunsmith", "faction": "isolated_artisans" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Cabin", "overmap_terrain": "cabin_isherwood", "faction": "isherwood_family" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Isherwood Stables", "overmap_terrain": "horse_farm_isherwood_6", "faction": "isherwood_family" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Isherwood Outcropping", "overmap_terrain": "farm_isherwood_2", "faction": "isherwood_family" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Isherwood Farms", "overmap_terrain": "dairy_farm_isherwood_SW", "faction": "isherwood_family" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Merchant", "overmap_terrain": "bunker_shop_b", "faction": "wasteland_scavengers" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "The Great Library", "overmap_terrain": "campus_media_0_0_0", "faction": "the_great_library" }
+ }
+]
diff --git a/data/json/overmap/overmap_special/alien.json b/data/json/overmap/overmap_special/alien.json
index 4433a138a3518..073e18bfc7bdc 100644
--- a/data/json/overmap/overmap_special/alien.json
+++ b/data/json/overmap/overmap_special/alien.json
@@ -57,7 +57,7 @@
{ "point": [ 2, 1, 1 ], "overmap": "exodii_base_x2y1z1_north" },
{ "point": [ 3, 1, 1 ], "overmap": "exodii_base_x3y1z1_north" },
{ "point": [ 0, 2, 1 ], "overmap": "exodii_base_x0y2z1_north" },
- { "point": [ 1, 2, 1 ], "overmap": "exodii_base_x1y2z1_north" },
+ { "point": [ 1, 2, 1 ], "overmap": "exodii_base_x1y2z1_north", "camp": "exodii", "camp_name": "Exodii" },
{ "point": [ 2, 2, 1 ], "overmap": "exodii_base_x2y2z1_north" },
{ "point": [ 3, 2, 1 ], "overmap": "exodii_base_x3y2z1_north" },
{ "point": [ 0, 3, 1 ], "overmap": "exodii_base_x0y3z1_north" },
diff --git a/data/json/overmap/overmap_special/campus.json b/data/json/overmap/overmap_special/campus.json
index a66a32bd7486f..3abdeb3a8557a 100644
--- a/data/json/overmap/overmap_special/campus.json
+++ b/data/json/overmap/overmap_special/campus.json
@@ -63,7 +63,12 @@
{ "point": [ 10, 6, 3 ], "overmap": "campus_commons_1_2_3_north" },
{ "point": [ 9, 7, 3 ], "overmap": "campus_commons_0_3_3_north" },
{ "point": [ 10, 7, 3 ], "overmap": "campus_commons_1_3_3_north" },
- { "point": [ 12, 10, 0 ], "overmap": "campus_media_0_0_0_north" },
+ {
+ "point": [ 12, 10, 0 ],
+ "overmap": "campus_media_0_0_0_north",
+ "camp": "the_great_library",
+ "camp_name": "The Great Library"
+ },
{ "point": [ 12, 11, 0 ], "overmap": "campus_media_0_1_0_north" },
{ "point": [ 13, 10, 0 ], "overmap": "campus_media_1_0_0_north" },
{ "point": [ 13, 11, 0 ], "overmap": "campus_media_1_1_0_north" },
diff --git a/data/json/overmap/overmap_special/specials.json b/data/json/overmap/overmap_special/specials.json
index a6ddd0c2b13c4..52a272737e0f5 100644
--- a/data/json/overmap/overmap_special/specials.json
+++ b/data/json/overmap/overmap_special/specials.json
@@ -443,7 +443,7 @@
{ "point": [ 0, 1, 0 ], "overmap": "hive_edge_01_north" },
{ "point": [ 0, 2, 0 ], "overmap": "hive_edge_02_north" },
{ "point": [ 1, 0, 0 ], "overmap": "hive_edge_10_north" },
- { "point": [ 1, 1, 0 ], "overmap": "hive_north" },
+ { "point": [ 1, 1, 0 ], "overmap": "hive_north", "camp": "apis_hive", "camp_name": "???" },
{ "point": [ 1, 2, 0 ], "overmap": "hive_edge_12_north" },
{ "point": [ 2, 0, 0 ], "overmap": "hive_edge_20_north" },
{ "point": [ 2, 1, 0 ], "overmap": "hive_edge_21_north" },
@@ -716,7 +716,7 @@
"type": "overmap_special",
"id": "Cabin_Lapin",
"overmaps": [
- { "point": [ 0, 0, 0 ], "overmap": "cabin_lapin_north" },
+ { "point": [ 0, 0, 0 ], "overmap": "cabin_lapin_north", "camp": "lapin", "camp_name": "Lapin" },
{ "point": [ 0, 0, 1 ], "overmap": "cabin_roof_lapin_north" },
{ "point": [ 1, 1, 0 ], "overmap": "field_graboid" }
],
@@ -1339,7 +1339,7 @@
{ "point": [ 2, 0, 0 ], "overmap": "prison_island_1_3_north" },
{ "point": [ 3, 0, 0 ], "overmap": "prison_island_1_4_north" },
{ "point": [ 4, 0, 0 ], "overmap": "prison_island_1_5_north" },
- { "point": [ 0, 1, 0 ], "overmap": "prison_island_1_6_north" },
+ { "point": [ 0, 1, 0 ], "overmap": "prison_island_1_6_north", "camp": "prisoners", "camp_name": "Holdouts" },
{ "point": [ 1, 1, 0 ], "overmap": "prison_island_1_7_north" },
{ "point": [ 2, 1, 0 ], "overmap": "prison_island_1_8_north" },
{ "point": [ 3, 1, 0 ], "overmap": "prison_island_1_9_north" },
@@ -2080,7 +2080,12 @@
{ "point": [ 9, 6, 0 ], "overmap": "evac_center_10_north" },
{ "point": [ 5, 7, 0 ], "overmap": "evac_center_11_north" },
{ "point": [ 6, 7, 0 ], "overmap": "evac_center_12_north" },
- { "point": [ 7, 7, 0 ], "overmap": "evac_center_13_north" },
+ {
+ "point": [ 7, 7, 0 ],
+ "overmap": "evac_center_13_north",
+ "camp": "free_merchants",
+ "camp_name": "Refugee Center"
+ },
{ "point": [ 8, 7, 0 ], "overmap": "evac_center_14_north" },
{ "point": [ 9, 7, 0 ], "overmap": "evac_center_15_north" },
{ "point": [ 5, 8, 0 ], "overmap": "evac_center_16_north" },
@@ -2436,7 +2441,12 @@
{ "point": [ -2, 4, 0 ], "overmap": "godco_6_north" },
{ "point": [ -3, 4, 0 ], "overmap": "forest_thick" },
{ "point": [ -4, 4, 0 ], "overmap": "forest_thick" },
- { "point": [ -1, 4, 0 ], "overmap": "godco_5_north" },
+ {
+ "point": [ -1, 4, 0 ],
+ "overmap": "godco_5_north",
+ "camp": "gods_community",
+ "camp_name": "New England Church Retreat"
+ },
{ "point": [ 0, 4, 0 ], "overmap": "godco_4_north" },
{ "point": [ 1, 4, 0 ], "overmap": "forest_thick" },
{ "point": [ 2, 4, 0 ], "overmap": "forest_thick" },
@@ -2680,7 +2690,7 @@
{ "point": [ 1, 7, 1 ], "overmap": "ranch_camp_65_roof_north" },
{ "point": [ 2, 7, 0 ], "overmap": "ranch_camp_66_north" },
{ "point": [ 2, 7, 1 ], "overmap": "ranch_camp_66_roof_north" },
- { "point": [ 3, 7, 0 ], "overmap": "ranch_camp_67_north" },
+ { "point": [ 3, 7, 0 ], "overmap": "ranch_camp_67_north", "camp": "tacoma_commune", "camp_name": "Tacoma" },
{ "point": [ 3, 7, 1 ], "overmap": "ranch_camp_67_roof_north" },
{ "point": [ 4, 7, 0 ], "overmap": "ranch_camp_68_north" },
{ "point": [ 4, 7, 1 ], "overmap": "ranch_camp_68_roof_north" },
@@ -3583,7 +3593,12 @@
{ "point": [ 2, -1, 0 ], "overmap": "robofachq_surface_road2_north" },
{ "point": [ 3, -1, 0 ], "overmap": "robofachq_surface_road3_north" },
{ "point": [ 0, 0, 0 ], "overmap": "robofachq_surface_parking_north" },
- { "point": [ 1, 0, 0 ], "overmap": "robofachq_surface_entrance_north" },
+ {
+ "point": [ 1, 0, 0 ],
+ "overmap": "robofachq_surface_entrance_north",
+ "camp": "robofac",
+ "camp_name": "Hub 01"
+ },
{ "point": [ 2, 0, 0 ], "overmap": "robofachq_surface_car_entrance_north" },
{ "point": [ 3, 0, 0 ], "overmap": "robofachq_surface_a3_north" },
{ "point": [ 0, 1, 0 ], "overmap": "robofachq_surface_b0_north" },
@@ -3685,7 +3700,12 @@
"id": "isolated_road",
"overmaps": [
{ "point": [ 0, 0, 0 ], "overmap": "isolated_road_field_0" },
- { "point": [ 1, 0, 0 ], "overmap": "isolated_house_farm_gunsmith" },
+ {
+ "point": [ 1, 0, 0 ],
+ "overmap": "isolated_house_farm_gunsmith",
+ "camp": "isolated_artisans",
+ "camp_name": "Cody & Jay"
+ },
{ "point": [ 0, 1, 0 ], "overmap": "isolated_house_farm_blacksmith" },
{ "point": [ 1, 0, 1 ], "overmap": "isolated_house_farm_roof_gunsmith" },
{ "point": [ 0, 1, 1 ], "overmap": "isolated_house_farm_roof_blacksmith" },
@@ -5487,7 +5507,7 @@
{ "point": [ 1, 4, 0 ], "overmap": "special_forest" },
{ "point": [ 2, 4, 0 ], "overmap": "special_forest_thick" },
{ "point": [ 3, 4, 0 ], "overmap": "special_forest" },
- { "point": [ 4, 4, 0 ], "overmap": "cabin_isherwood_north" },
+ { "point": [ 4, 4, 0 ], "overmap": "cabin_isherwood_north", "camp": "isherwood_family", "camp_name": "Cabin" },
{ "point": [ 4, 4, 1 ], "overmap": "cabin_lake_roof_north" },
{ "point": [ 5, 4, 0 ], "overmap": "special_forest" },
{ "point": [ 6, 4, 0 ], "overmap": "special_forest" },
@@ -5622,7 +5642,12 @@
{ "point": [ 14, 8, 0 ], "overmap": "special_field" },
{ "point": [ 15, 8, 0 ], "overmap": "special_field" },
{ "point": [ 16, 8, 0 ], "overmap": "horse_farm_isherwood_5_north" },
- { "point": [ 17, 8, 0 ], "overmap": "horse_farm_isherwood_6_north" },
+ {
+ "point": [ 17, 8, 0 ],
+ "overmap": "horse_farm_isherwood_6_north",
+ "camp": "isherwood_family",
+ "camp_name": "Isherwood Stables"
+ },
{ "point": [ 18, 8, 0 ], "overmap": "horse_farm_isherwood_7_north" },
{ "point": [ 18, 8, 1 ], "overmap": "horse_farm_isherwood_7_hayloft_north" },
{ "point": [ 18, 8, 2 ], "overmap": "horse_farm_isherwood_7_roof_north" },
@@ -5924,7 +5949,12 @@
{ "point": [ 10, 18, 0 ], "overmap": "farm_isherwood_3_north" },
{ "point": [ 10, 18, 2 ], "overmap": "farm_isherwood_3_roof_north" },
{ "point": [ 10, 18, 1 ], "overmap": "farm_isherwood_3_hayloft_north" },
- { "point": [ 11, 18, 0 ], "overmap": "farm_isherwood_2_north" },
+ {
+ "point": [ 11, 18, 0 ],
+ "overmap": "farm_isherwood_2_north",
+ "camp": "isherwood_family",
+ "camp_name": "Isherwood Outcropping"
+ },
{ "point": [ 11, 18, -1 ], "overmap": "farm_isherwood_2_cellar_north" },
{ "point": [ 11, 18, 1 ], "overmap": "farm_isherwood_2_roof_north" },
{ "point": [ 12, 18, 0 ], "overmap": "farm_isherwood_1_north" },
@@ -6064,7 +6094,12 @@
{ "point": [ 23, 22, 0 ], "overmap": "rural_road_ns" },
{ "point": [ 24, 22, 0 ], "overmap": "smokehouse_north" },
{ "point": [ 24, 22, 1 ], "overmap": "smokehouse_roof_north" },
- { "point": [ 25, 22, 0 ], "overmap": "dairy_farm_isherwood_SW_north" },
+ {
+ "point": [ 25, 22, 0 ],
+ "overmap": "dairy_farm_isherwood_SW_north",
+ "camp": "isherwood_family",
+ "camp_name": "Isherwood Farms"
+ },
{ "point": [ 25, 22, 1 ], "overmap": "dairy_farm_isherwood_SW_roof_north" },
{ "point": [ 26, 22, 0 ], "overmap": "dairy_farm_isherwood_SE_north" },
{ "point": [ 26, 22, 1 ], "overmap": "dairy_farm_isherwood_SE_roof_north" },
@@ -6946,7 +6981,7 @@
"type": "overmap_special",
"id": "Occupied Chem Lab",
"overmaps": [
- { "point": [ 0, 0, 0 ], "overmap": "chemical_lab_ocu_north" },
+ { "point": [ 0, 0, 0 ], "overmap": "chemical_lab_ocu_north", "camp": "wasteland_scavengers", "camp_name": "Chemist" },
{ "point": [ 0, 0, 1 ], "overmap": "chemical_lab_roof_ocu_north" },
{ "point": [ -1, -1, 0 ], "locations": [ "land", "road" ] },
{ "point": [ -1, 0, 0 ], "locations": [ "land", "road" ] }
@@ -6965,7 +7000,7 @@
"type": "overmap_special",
"id": "Occupied Scrap Yard",
"overmaps": [
- { "point": [ 0, 0, 0 ], "overmap": "smallscrapyard_ocu_north" },
+ { "point": [ 0, 0, 0 ], "overmap": "smallscrapyard_ocu_north", "camp": "wasteland_scavengers", "camp_name": "Scrapper" },
{ "point": [ 0, -1, 0 ], "locations": [ "land", "road" ] }
],
"connections": [ { "point": [ 0, -1, 0 ], "terrain": "road", "connection": "local_road", "from": [ 0, 0, 0 ] } ],
@@ -6982,7 +7017,12 @@
{ "point": [ 0, 0, 0 ], "overmap": "lumbermill_0_0_ocu_north" },
{ "point": [ 1, 0, 0 ], "overmap": "lumbermill_1_0_ocu_north" },
{ "point": [ 0, 1, 0 ], "overmap": "lumbermill_0_1_ocu_north" },
- { "point": [ 1, 1, 0 ], "overmap": "lumbermill_1_1_ocu_north" },
+ {
+ "point": [ 1, 1, 0 ],
+ "overmap": "lumbermill_1_1_ocu_north",
+ "camp": "wasteland_scavengers",
+ "camp_name": "Lumbermill (survivor base)"
+ },
{ "point": [ 0, 0, 1 ], "overmap": "lumbermill_0_0_roof_north" },
{ "point": [ 1, 0, 1 ], "overmap": "lumbermill_1_0_roof_north" },
{ "point": [ 0, 1, 1 ], "overmap": "lumbermill_0_1_roof_north" },
@@ -7061,7 +7101,12 @@
"id": "bunker shop",
"overmaps": [
{ "point": [ 0, 0, 0 ], "overmap": "bunker_shop_g_south" },
- { "point": [ 0, 0, -1 ], "overmap": "bunker_shop_b_south" }
+ {
+ "point": [ 0, 0, -1 ],
+ "overmap": "bunker_shop_b_south",
+ "camp": "wasteland_scavengers",
+ "camp_name": "Merchant"
+ }
],
"connections": [ { "point": [ 0, -1, 0 ], "terrain": "road", "existing": true, "connection": "local_road", "from": [ 0, 0, 0 ] } ],
"locations": [ "field", "forest_without_trail" ],
diff --git a/data/json/recipes/basecamps/special_hardcoded.json b/data/json/recipes/basecamps/special_hardcoded.json
new file mode 100644
index 0000000000000..0c37fdd91f8a1
--- /dev/null
+++ b/data/json/recipes/basecamps/special_hardcoded.json
@@ -0,0 +1,41 @@
+[
+ {
+ "//": "This file contains a special NPC-only basecamp which doesn't place anything, equivalent to the barebones camp. It is placed during mapgen, and should not show up as a valid player option in regular gameplay. The entire purpose is to provide 'water_well' so NPCs don't dehydrate to death.",
+ "//2": "Has hardcoded references in place (overmap.cpp) and overmap::migrate_camps.",
+ "type": "recipe",
+ "activity_level": "NO_EXERCISE",
+ "result": "faction_base_bare_bones_NPC_camp_0",
+ "description": "NPC-only. If you see this it's a bug",
+ "category": "CC_BUILDING",
+ "subcategory": "CSC_BUILDING_BASES",
+ "skill_used": "fabrication",
+ "autolearn": false,
+ "never_learn": true,
+ "time": "1 h",
+ "construction_blueprint": "fbbb",
+ "blueprint_provides": [
+ { "id": "gathering" },
+ { "id": "fbbb_crafting_recipes_basic" },
+ { "id": "fbbb_crafting_recipes_cooking" },
+ { "id": "fbbb_crafting_recipes_crafting" },
+ { "id": "fbbb_crafting_recipes_custom" },
+ { "id": "fbbb" },
+ { "id": "firewood" },
+ { "id": "foraging" },
+ { "id": "sorting" },
+ { "id": "logging" },
+ { "id": "trapping" },
+ { "id": "hunting" },
+ { "id": "kitchen" },
+ { "id": "relaying" },
+ { "id": "walls" },
+ { "id": "recruiting" },
+ { "id": "scouting" },
+ { "id": "water_well" },
+ { "id": "patrolling" }
+ ],
+ "blueprint_requires": [ { "id": "not_an_upgrade" } ],
+ "blueprint_name": "NPC-only. If you see this it's a bug",
+ "check_blueprint_needs": false
+ }
+]
diff --git a/data/mods/Aftershock/maps/overmap_specials.json b/data/mods/Aftershock/maps/overmap_specials.json
index 0c9801053d85b..a693bc18e471c 100644
--- a/data/mods/Aftershock/maps/overmap_specials.json
+++ b/data/mods/Aftershock/maps/overmap_specials.json
@@ -2,7 +2,7 @@
{
"type": "overmap_special",
"id": "prepnet_orchard",
- "overmaps": [ { "point": [ 0, 0, 0 ], "overmap": "prepnet_orchard_north" } ],
+ "overmaps": [ { "point": [ 0, 0, 0 ], "overmap": "prepnet_orchard_north", "camp": "Prepnet_Phyle", "camp_name": "PrepNet" } ],
"connections": [ { "point": [ 0, -1, 0 ], "terrain": "road" } ],
"locations": [ "land" ],
"city_distance": [ 5, -1 ],
@@ -189,7 +189,12 @@
"overmaps": [
{ "point": [ 0, -1, -9 ], "overmap": "augustmoon_docking_arm_north" },
{ "point": [ -1, 0, -9 ], "overmap": "augustmoon_bar_north" },
- { "point": [ 0, 0, -9 ], "overmap": "augustmoon_main_concourse1_north" },
+ {
+ "point": [ 0, 0, -9 ],
+ "overmap": "augustmoon_main_concourse1_north",
+ "camp": "UICA",
+ "camp_name": "Port Augustmoon"
+ },
{ "point": [ 1, 0, -9 ], "overmap": "augustmoon_solar_array1_north" },
{ "point": [ 0, 1, -9 ], "overmap": "augustmoon_main_concourse2_north" },
{ "point": [ 1, 1, -9 ], "overmap": "augustmoon_solar_array2_north" },
diff --git a/data/mods/Aftershock/migration/camp_placement.json b/data/mods/Aftershock/migration/camp_placement.json
new file mode 100644
index 0000000000000..1b08651b6ab90
--- /dev/null
+++ b/data/mods/Aftershock/migration/camp_placement.json
@@ -0,0 +1,11 @@
+[
+ {
+ "//": "Remove all entries after 0.I",
+ "type": "camp_migration",
+ "camp_migrations": { "name": "PrepNet", "overmap_terrain": "prepnet_orchard", "faction": "Prepnet_Phyle" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Port Augustmoon", "overmap_terrain": "augustmoon_main_concourse1", "faction": "UICA" }
+ }
+]
diff --git a/data/mods/Aftershock/npcs/factions.json b/data/mods/Aftershock/npcs/factions.json
index 2e9d84f8e8fa5..1138ac98d4982 100644
--- a/data/mods/Aftershock/npcs/factions.json
+++ b/data/mods/Aftershock/npcs/factions.json
@@ -10,7 +10,8 @@
"size": 15,
"power": 20,
"currency": "crypto_coin",
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "90 days worth of food for 1 person, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 270000000, "vitamins": { "iron": 8640, "calcium": 8640, "vitC": 8640 } },
"wealth": 75000,
"relations": {
"free_merchants": {
@@ -91,7 +92,8 @@
{ "item": "UICA_1000d", "fixed_adj": 0 },
{ "item": "UICA_10000d", "fixed_adj": 0 }
],
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "Being an interplantary authority with effectively unlimited resources to draw upon, these numbers are here only as a technical limit. Vitamins are capped at ~90% of the max for a 32-bit integer and calories scaled to match.",
+ "fac_food_supply": { "calories": 62500000000000, "vitamins": { "iron": 2000000000, "calcium": 2000000000, "vitC": 2000000000 } },
"wealth": 75000,
"relations": {
"free_merchants": {
@@ -172,7 +174,8 @@
"size": 100,
"power": 100,
"currency": "",
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "Doesn't actually have NPCs, so no camp or food supply is necessary.",
+ "fac_food_supply": { "calories": 0, "vitamins": { } },
"wealth": 75000,
"relations": {
"free_merchants": {
@@ -252,7 +255,8 @@
"known_by_u": false,
"size": 15,
"power": 20,
- "fac_food_supply": { "calories": 1200, "vitamins": { } },
+ "//": "Doesn't actually have *spawned* NPCs, so no camp or food supply is necessary.",
+ "fac_food_supply": { "calories": 0, "vitamins": { } },
"wealth": 7500,
"relations": {
"free_merchants": {
diff --git a/data/mods/DinoMod/NPC/NC_BO_BARONYX.json b/data/mods/DinoMod/NPC/NC_BO_BARONYX.json
index b4fbdfd569b6f..ab2b9f7c38835 100644
--- a/data/mods/DinoMod/NPC/NC_BO_BARONYX.json
+++ b/data/mods/DinoMod/NPC/NC_BO_BARONYX.json
@@ -286,7 +286,8 @@
"size": 25,
"power": 20,
"currency": "icon",
- "fac_food_supply": { "calories": 87500, "vitamins": { } },
+ "//2": "90 days worth of food for 10 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 82500,
"relations": {
"free_merchants": {
diff --git a/data/mods/Magiclysm/npc/factions.json b/data/mods/Magiclysm/npc/factions.json
index 1965000672f1a..f799a8d4495aa 100644
--- a/data/mods/Magiclysm/npc/factions.json
+++ b/data/mods/Magiclysm/npc/factions.json
@@ -8,7 +8,8 @@
"known_by_u": false,
"size": 5,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "90 days worth of food for 1 person, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 270000000, "vitamins": { "iron": 8640, "calcium": 8640, "vitC": 8640 } },
"wealth": 100000,
"currency": "embued_coin",
"relations": {
@@ -69,7 +70,8 @@
"known_by_u": false,
"size": 50,
"power": 50,
- "fac_food_supply": { "calories": 11520, "vitamins": { } },
+ "//": "90 days worth of food for 1 person, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 270000000, "vitamins": { "iron": 8640, "calcium": 8640, "vitC": 8640 } },
"wealth": 1000,
"relations": {
"lobby_beggars": {
@@ -130,7 +132,8 @@
"known_by_u": false,
"size": 5,
"power": 100,
- "fac_food_supply": { "calories": 115200, "vitamins": { } },
+ "//": "90 days worth of food for 10 people, at 3000kcal/day and full RDA of iron/calcium/vitC",
+ "fac_food_supply": { "calories": 2700000000, "vitamins": { "iron": 86400, "calcium": 86400, "vitC": 86400 } },
"wealth": 75000000,
"currency": "denarius",
"relations": {
diff --git a/data/mods/Magiclysm/obsolete/camp_placement.json b/data/mods/Magiclysm/obsolete/camp_placement.json
new file mode 100644
index 0000000000000..2497399f684ab
--- /dev/null
+++ b/data/mods/Magiclysm/obsolete/camp_placement.json
@@ -0,0 +1,15 @@
+[
+ {
+ "//": "Remove all entries after 0.I",
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Forge of Wonders", "overmap_terrain": "forge_3A", "faction": "forge_lords" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Healer's respite", "overmap_terrain": "magic_shop", "faction": "healers_grey" }
+ },
+ {
+ "type": "camp_migration",
+ "camp_migrations": { "name": "Old Wizard", "overmap_terrain": "lake_retreat_z3", "faction": "wizards_ancient" }
+ }
+]
diff --git a/data/mods/Magiclysm/worldgen/forge_of_wonders.json b/data/mods/Magiclysm/worldgen/forge_of_wonders.json
index 0ea79c1867ba3..0bb65f5dc25a7 100644
--- a/data/mods/Magiclysm/worldgen/forge_of_wonders.json
+++ b/data/mods/Magiclysm/worldgen/forge_of_wonders.json
@@ -184,7 +184,7 @@
{ "point": [ 0, 0, 1 ], "overmap": "forge_1A_roof_north" },
{ "point": [ 1, 0, 0 ], "overmap": "forge_2A_north" },
{ "point": [ 1, 0, 1 ], "overmap": "forge_2A_roof_north" },
- { "point": [ 2, 0, 0 ], "overmap": "forge_3A_north" },
+ { "point": [ 2, 0, 0 ], "overmap": "forge_3A_north", "camp": "forge_lords", "camp_name": "Forge of Wonders" },
{ "point": [ 2, 0, 1 ], "overmap": "forge_3A_roof_north" },
{ "point": [ 3, 0, 0 ], "overmap": "forge_4A_north" },
{ "point": [ 3, 0, 1 ], "overmap": "forge_4A_roof_north" },
diff --git a/data/mods/Magiclysm/worldgen/multitile_city_buildings.json b/data/mods/Magiclysm/worldgen/multitile_city_buildings.json
index 3767a55c23ed3..059167bfc9f1c 100644
--- a/data/mods/Magiclysm/worldgen/multitile_city_buildings.json
+++ b/data/mods/Magiclysm/worldgen/multitile_city_buildings.json
@@ -4,7 +4,7 @@
"id": "magic_shop",
"locations": [ "land" ],
"overmaps": [
- { "point": [ 0, 0, 0 ], "overmap": "magic_shop_north" },
+ { "point": [ 0, 0, 0 ], "overmap": "magic_shop_north", "camp": "healers_grey", "camp_name": "Healer's respite" },
{ "point": [ 0, 0, 1 ], "overmap": "magic_shop_2ndfloor_north" },
{ "point": [ 0, 0, 2 ], "overmap": "magic_shop_roof_north" }
]
diff --git a/data/mods/Magiclysm/worldgen/overmap_specials.json b/data/mods/Magiclysm/worldgen/overmap_specials.json
index 1e014dec03d87..9baf26748c8e0 100644
--- a/data/mods/Magiclysm/worldgen/overmap_specials.json
+++ b/data/mods/Magiclysm/worldgen/overmap_specials.json
@@ -101,7 +101,12 @@
{ "point": [ 3, 3, 0 ], "overmap": "lake_retreat_ground_north" },
{ "point": [ 3, 3, 1 ], "overmap": "lake_retreat_z1_north" },
{ "point": [ 3, 3, 2 ], "overmap": "lake_retreat_z2_north" },
- { "point": [ 3, 3, 3 ], "overmap": "lake_retreat_z3_north" },
+ {
+ "point": [ 3, 3, 3 ],
+ "overmap": "lake_retreat_z3_north",
+ "camp": "wizards_ancient",
+ "camp_name": "Old Wizard"
+ },
{ "point": [ 3, 3, 4 ], "overmap": "lake_retreat_z4_north" },
{ "point": [ 4, 3, 0 ], "overmap": "forest" },
{ "point": [ 5, 3, 0 ], "overmap": "lake_shore" },
diff --git a/doc/FACTIONS.md b/doc/FACTIONS.md
index 4f83adae3128b..29b78ddade7cf 100644
--- a/doc/FACTIONS.md
+++ b/doc/FACTIONS.md
@@ -14,6 +14,7 @@ An NPC faction looks like this:
"size": 100,
"power": 100,
"fac_food_supply": { "calories": 115200, "vitamins": { "iron": 800, "calcium": 800, "vitC": 600 } },
+ "consumes_food": true,
"lone_wolf_faction": true,
"wealth": 75000000,
"currency": "FMCNote",
@@ -66,8 +67,10 @@ Field | Meaning
`"known_by_u"` | boolean, whether the player has met members of the faction. Can be changed in play. Unknown factions will not be displayed in the faction menu.
`"size"` | integer, an approximate count of the members of the faction. Has no effect in play currently.
`"power"` | integer, an approximation of the faction's power. Has no effect in play currently.
-`"fac_food_supply"` | integer, the number of calories (not kilocalories!) available to the faction. Has no effect in play currently.
-`"vitamins"` | array, *units* of vitamins available to this faction. This is not the same as RDA, see [the vitamins doc](VITAMIN.md) for more details. Has no effect in play currently.
+`"fac_food_supply"` | object, mandatory. The overall food supply of the faction, including the `calories` and `vitamins`.
+`"calories"` | integer, the number of calories (not kilocalories!) available to the faction.
+`"vitamins"` | array, *units* of vitamins available to this faction. This is not the same as RDA, see [the vitamins doc](VITAMIN.md) for more details.
+`"consumes_food"` | bool, optional, defaults to false. Controls whether characters eating from the fac_food_supply actually removes food from it. Note that eating is controlled by the external optional "NO_NPC_FOOD". consumes_food only controls whether the eaten food is removed from storage (whether the amount of food available goes down!)
`"wealth"` | integer, number of post-apocalyptic currency in cents that that faction has to purchase stuff. Serves as an upper limit on the amount of items restocked by a NPC of this faction with a defined shopkeeper_item_group (see NPCs.md)
`"currency"` | string, the item `"id"` of the faction's preferred currency. Faction shopkeeps will trade faction current at 100% value, for both selling and buying.
`"price_rules"` | array, allows defining `premium`, `markup`, `price` and/or `fixed_adj` for an `item`/`category`/`group`.
`premium` is a price multiplier that applies to both sides.
`markup` is only used when an NPC is selling to the avatar and defaults to `1`.
`price` replaces the item's `price_postapoc`.
`fixed_adj` is used instead of adjustment based on social skill and intelligence stat and can be used to define secondary currencies.
Lower entries override higher ones. For conditionals, the avatar is used as alpha and the evaluating npc as beta
@@ -101,10 +104,9 @@ Flag | Meaning
---------------------- | --
`"kill on sight"` | Members of the acting faction are always hostile to members of the object faction.
`"watch your back"` | Members of the acting faction will treat attacks on members of the object faction as attacks on themselves.
-`"share my stuff"` | Members of the acting faction will not object if members of the object faction take items owned by the acting faction.
+`"share my stuff"` | Members of the acting faction will not object if members of the object faction take items owned by the acting faction. Camps of the acting faction will allow members of the object faction to eat from their food stores (including water access).
+`"share public goods"` | Camps of the acting faction will allow members of the object faction to drink from camp water wells. Irrelevant if the relationship already allows "share my stuff".
`"guard your stuff"` | Members of the acting faction will object if someone takes items owned by the object faction.
-`"lets you in"` | Members of the acting faction will not object if a member of the object faction enters territory controlled by the acting faction.
-`"defends your space"` | Members of the acting faction will become hostile if someone enters territory controlled by the object faction.
+`"lets you in"` | Members of the acting faction will not object if a member of the object faction enters territory controlled by the acting faction. No current gameplay effect.
+`"defends your space"` | Members of the acting faction will become hostile if someone enters territory controlled by the object faction. No current gameplay effect.
`"knows your voice"` | Members of the acting faction will not comment on speech by members of the object faction.
-
-So far, only `"kill on sight"`, `"knows your voice"`, and `"watch your back"` have been implemented.
diff --git a/doc/OVERMAP.md b/doc/OVERMAP.md
index fe06ff2f74eb4..d8c4162ac3531 100644
--- a/doc/OVERMAP.md
+++ b/doc/OVERMAP.md
@@ -423,7 +423,7 @@ Depending on the subtype, there are further relevant fields:
"overmaps": [
{ "point": [ 0, 0, 0 ], "overmap": "campground_1a_north", "locations": [ "forest_edge" ] },
{ "point": [ 1, 0, 0 ], "overmap": "campground_1b_north" },
- { "point": [ 0, 1, 0 ], "overmap": "campground_2a_north" },
+ { "point": [ 0, 1, 0 ], "overmap": "campground_2a_north", "camp": "isherwood_family", "camp_name": "Campground camp" },
{ "point": [ 1, 1, 0 ], "overmap": "campground_2b_north" }
],
"connections": [ { "point": [ 1, -1, 0 ], "terrain": "road", "connection": "local_road", "from": [ 1, 0, 0 ] } ],
@@ -444,6 +444,8 @@ Depending on the subtype, there are further relevant fields:
| `point` | `[ x, y, z]` of the overmap terrain within the special. |
| `overmap` | Id of the `overmap_terrain` to place at the location. If ommited no overmap_terrain is placed but the point will still be checked for valid locations when deciding if placement is valid. |
| `locations` | List of `overmap_location` ids that this overmap terrain may be placed on. Overrides the specials overall `locations` field. |
+| `camp` | Will make a NPC-owned camp spawn here when given a value. The entered value is the ID of the faction that owns this camp. |
+| `camp_name` | Name that will be displayed on the overmap for the camp. |
### Connections
diff --git a/lang/string_extractor/parser.py b/lang/string_extractor/parser.py
index 4275e4c2a3eff..24ff1470e1237 100644
--- a/lang/string_extractor/parser.py
+++ b/lang/string_extractor/parser.py
@@ -61,6 +61,7 @@
from .parsers.movement_mode import parse_movement_mode
from .parsers.mutation_category import parse_mutation_category
from .parsers.overmap_land_use_code import parse_overmap_land_use_code
+from .parsers.overmap_special import parse_overmap_special
from .parsers.practice import parse_practice
from .parsers.scenario import parse_scenario
from .parsers.shop_blacklist import parse_shopkeeper_blacklist
@@ -114,6 +115,7 @@ def dummy_parser(json, origin):
"body_part": parse_body_part,
"book": parse_generic,
"butchery_requirement": dummy_parser,
+ "camp_migration": dummy_parser,
"character_mod": parse_character_mod,
"charge_removal_blacklist": dummy_parser,
"city": parse_city,
@@ -193,7 +195,7 @@ def dummy_parser(json, origin):
"overmap_connection": dummy_parser,
"overmap_land_use_code": parse_overmap_land_use_code,
"overmap_location": dummy_parser,
- "overmap_special": dummy_parser,
+ "overmap_special": parse_overmap_special,
"overmap_special_migration": dummy_parser,
"overmap_terrain": parse_overmap_terrain,
"palette": parse_palette,
diff --git a/lang/string_extractor/parsers/overmap_special.py b/lang/string_extractor/parsers/overmap_special.py
new file mode 100644
index 0000000000000..57e3cc6eb5c53
--- /dev/null
+++ b/lang/string_extractor/parsers/overmap_special.py
@@ -0,0 +1,8 @@
+from ..write_text import write_text
+
+
+def parse_overmap_special(json, origin):
+ for overmap in json["overmaps"]:
+ if "camp_name" in overmap:
+ write_text(overmap["camp_name"], origin,
+ comment="Name of NPC faction camp")
diff --git a/src/basecamp.cpp b/src/basecamp.cpp
index 210a8889f7180..62734492c8b94 100644
--- a/src/basecamp.cpp
+++ b/src/basecamp.cpp
@@ -187,9 +187,12 @@ void basecamp::add_expansion( const std::string &bldg, const tripoint_abs_omt &n
update_resources( bldg );
}
-void basecamp::define_camp( const tripoint_abs_omt &p, const std::string_view camp_type )
+void basecamp::define_camp( const tripoint_abs_omt &p, const std::string_view camp_type,
+ bool player_founded )
{
- query_new_name( true );
+ if( player_founded ) {
+ query_new_name( true );
+ }
omt_pos = p;
const oter_id &omt_ref = overmap_buffer.ter( omt_pos );
// purging the regions guarantees all entries will start with faction_base_
@@ -205,9 +208,11 @@ void basecamp::define_camp( const tripoint_abs_omt &p, const std::string_view ca
e.pos = omt_pos;
expansions[base_camps::base_dir] = e;
const std::string direction = oter_get_rotation_string( omt_ref );
- const oter_id bcid( direction.empty() ? "faction_base_camp_0" : "faction_base_camp_new_0" +
- direction );
- overmap_buffer.ter_set( omt_pos, bcid );
+ if( player_founded ) {
+ const oter_id bcid( direction.empty() ? "faction_base_camp_0" : "faction_base_camp_new_0" +
+ direction );
+ overmap_buffer.ter_set( omt_pos, bcid );
+ }
update_provides( base_camps::faction_encode_abs( e, 0 ),
expansions[base_camps::base_dir] );
} else {
@@ -319,6 +324,24 @@ bool basecamp::has_water() const
return has_provides( "water_well" ) || has_provides( "fbmh_well_north" );
}
+bool basecamp::allowed_access_by( Character &guy, bool water_request ) const
+{
+ // The owner can always access their own camp.
+ if( fac() == guy.get_faction() ) {
+ return true;
+ }
+ // Sharing stuff also means sharing access.
+ if( fac()->has_relationship( guy.get_faction()->id, npc_factions::share_my_stuff ) ) {
+ return true;
+ }
+ // Some factions will share access to infinite water sources, but not food
+ if( water_request &&
+ fac()->has_relationship( guy.get_faction()->id, npc_factions::share_public_goods ) ) {
+ return true;
+ }
+ return false;
+}
+
std::vector basecamp::available_upgrades( const point &dir )
{
std::vector ret_data;
@@ -429,6 +452,7 @@ void basecamp::update_resources( const std::string &bldg )
void basecamp::update_provides( const std::string &bldg, expansion_data &e_data )
{
if( !recipe_id( bldg ).is_valid() ) {
+ debugmsg( "Invalid basecamp recipe %s", bldg );
return;
}
@@ -808,14 +832,8 @@ void basecamp::unload_camp_map()
void basecamp::set_owner( faction_id new_owner )
{
- for( const std::pair fac : g->faction_manager_ptr->all() ) {
- if( fac.first == new_owner ) {
- owner = new_owner;
- return;
- }
- }
- //Fallthrough, id must be invalid
- debugmsg( "Could not find matching faction for new owner's faction_id!" );
+ // Absolutely no safety checks, factions don't exist until you've encountered them but we sometimes set the owner before that
+ owner = new_owner;
}
faction_id basecamp::get_owner()
diff --git a/src/basecamp.h b/src/basecamp.h
index debbeb9ae1576..dce150046b19d 100644
--- a/src/basecamp.h
+++ b/src/basecamp.h
@@ -210,7 +210,8 @@ class basecamp
void add_expansion( const std::string &terrain, const tripoint_abs_omt &new_pos );
void add_expansion( const std::string &bldg, const tripoint_abs_omt &new_pos,
const point &dir );
- void define_camp( const tripoint_abs_omt &p, std::string_view camp_type );
+ void define_camp( const tripoint_abs_omt &p, std::string_view camp_type,
+ bool player_founded = true );
std::string expansion_tab( const point &dir ) const;
// check whether the point is the part of camp
@@ -273,6 +274,7 @@ class basecamp
/// Changes the faction opinion for you by @ref change, returns opinion
int camp_morale( int change = 0 ) const;
+ bool allowed_access_by( Character &guy, bool water_request = false ) const;
// recipes, gathering, and craft support functions
// from a direction
std::map recipe_deck( const point &dir ) const;
diff --git a/src/character.cpp b/src/character.cpp
index c51123d74d729..e0afa867e1d68 100644
--- a/src/character.cpp
+++ b/src/character.cpp
@@ -4983,9 +4983,7 @@ float Character::activity_level() const
bool Character::needs_food() const
{
- // Before the mechanism for non-player faction NPCs to obtain food is set up, it is unreasonable to require them to consume food.
- return ( !get_option( "NO_NPC_FOOD" ) && get_faction() == get_avatar().get_faction() ) ||
- !is_npc();
+ return !( is_npc() && get_option( "NO_NPC_FOOD" ) );
}
void Character::update_needs( int rate_multiplier )
diff --git a/src/condition.cpp b/src/condition.cpp
index 986a25f7f0050..3b26b8b8d8cdd 100644
--- a/src/condition.cpp
+++ b/src/condition.cpp
@@ -1770,7 +1770,17 @@ conditional_t::func f_math( const JsonObject &jo, const std::string_view member
conditional_t::func f_u_has_camp()
{
return []( dialogue const & ) {
- return !get_player_character().camps.empty();
+ for( const tripoint_abs_omt &camp_tripoint : get_player_character().camps ) {
+ std::optional camp = overmap_buffer.find_camp( camp_tripoint.xy() );
+ if( !camp ) {
+ continue;
+ }
+ basecamp *bcp = *camp;
+ if( bcp->get_owner() == get_player_character().get_faction()->id ) {
+ return true;
+ }
+ }
+ return false;
};
}
diff --git a/src/faction.cpp b/src/faction.cpp
index 7a8e691efc2d6..ebe431d5be72e 100644
--- a/src/faction.cpp
+++ b/src/faction.cpp
@@ -132,6 +132,7 @@ faction_template::faction_template( const JsonObject &jsobj )
, wealth( jsobj.get_int( "wealth" ) )
{
jsobj.get_member( "description" ).read( desc );
+ optional( jsobj, false, "consumes_food", consumes_food, false );
optional( jsobj, false, "price_rules", price_rules, faction_price_rules_reader {} );
jsobj.read( "fac_food_supply", food_supply, true );
if( jsobj.has_string( "currency" ) ) {
@@ -1052,6 +1053,10 @@ void faction_manager::display() const
continue;
}
basecamp *temp_camp = *p;
+ if( temp_camp->get_owner() != player_character.get_faction()->id ) {
+ // Don't display NPC camps as ours
+ continue;
+ }
camps.push_back( temp_camp );
}
lore.clear();
diff --git a/src/faction.h b/src/faction.h
index 464918d46b379..5d99dbd5d4350 100644
--- a/src/faction.h
+++ b/src/faction.h
@@ -50,6 +50,7 @@ enum relationship : int {
kill_on_sight,
watch_your_back,
share_my_stuff,
+ share_public_goods,
guard_your_stuff,
lets_you_in,
defend_your_space,
@@ -62,6 +63,7 @@ const std::unordered_map relation_strs = { {
{ "kill on sight", kill_on_sight },
{ "watch your back", watch_your_back },
{ "share my stuff", share_my_stuff },
+ { "share public goods", share_public_goods },
{ "guard your stuff", guard_your_stuff },
{ "lets you in", lets_you_in },
{ "defends your space", defend_your_space },
@@ -114,6 +116,7 @@ class faction_template
int size; // How big is our sphere of influence?
int power; // General measure of our power
nutrients food_supply; //Total nutritional value held
+ bool consumes_food; //Whether this faction actually draws down the food_supply when eating from it
int wealth; //Total trade currency
bool lone_wolf_faction; // is this a faction for just one person?
itype_id currency; // id of the faction currency
diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp
index 53b0819afc1fc..df4c93a03c1d7 100644
--- a/src/faction_camp.cpp
+++ b/src/faction_camp.cpp
@@ -2165,6 +2165,10 @@ void basecamp::abandon_camp()
for( npc_ptr &guy : get_npcs_assigned() ) {
talk_function::stop_guard( *guy );
}
+ // We must send this message early, before the name is erased.
+ add_msg( m_info, _( "You abandon %s." ), name );
+ std::set &known_camps = get_player_character().camps;
+ known_camps.erase( omt_pos );
overmap_buffer.remove_camp( *this );
map &here = get_map();
const tripoint sm_pos = omt_to_sm_copy( omt_pos.raw() );
@@ -2172,7 +2176,6 @@ void basecamp::abandon_camp()
// We cannot use bb_pos here, because bb_pos may be {0,0,0} if you haven't examined the bulletin board on camp ever.
// here.remove_submap_camp( here.getlocal( bb_pos ) );
here.remove_submap_camp( here.bub_from_abs( ms_pos ) );
- add_msg( m_info, _( "You abandon %s." ), name );
}
void basecamp::scan_pseudo_items()
@@ -5527,7 +5530,7 @@ nutrients basecamp::camp_food_supply( nutrients &change )
double percent_consumed = std::abs( static_cast( change.calories ) ) /
fac()->food_supply.calories;
consumed = fac()->food_supply;
- if( std::abs( change.calories ) > fac()->food_supply.calories ) {
+ if( std::abs( change.calories ) > fac()->food_supply.calories && fac()->consumes_food ) {
//Whoops, we don't have enough food. Empty the larder! No crumb shall go un-eaten!
fac()->food_supply += change;
faction *yours = get_player_character().get_faction();
@@ -5540,8 +5543,10 @@ nutrients basecamp::camp_food_supply( nutrients &change )
return consumed;
}
consumed *= percent_consumed;
- // Subtraction since we use the absolute value of change's calories to get the percent
- fac()->food_supply -= consumed;
+ if( fac()->consumes_food ) {
+ // Subtraction since we use the absolute value of change's calories to get the percent
+ fac()->food_supply -= consumed;
+ }
return consumed;
}
fac()->food_supply += change;
@@ -5578,6 +5583,10 @@ void basecamp::feed_workers( const std::vector bcp = overmap_buffer.find_camp( omt );
if( bcp ) {
basecamp *temp_camp = *bcp;
+ if( !temp_camp->allowed_access_by( you ) ) {
+ you.add_msg_if_player( _( "You don't run this camp, the board is useless to you." ) );
+ return;
+ }
+
temp_camp->validate_bb_pos( here.getglobal( examp ) );
temp_camp->validate_assignees();
temp_camp->validate_sort_points();
diff --git a/src/init.cpp b/src/init.cpp
index beda2c8d1ba87..59cf34507468f 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -410,6 +410,7 @@ void DynamicDataLoader::initialize()
add( "attack_vector", &attack_vector::load_attack_vectors );
add( "effect_type", &load_effect_type );
add( "oter_id_migration", &overmap::load_oter_id_migration );
+ add( "camp_migration", &overmap::load_oter_id_camp_migration );
add( "overmap_terrain", &overmap_terrains::load );
add( "construction_category", &construction_categories::load );
add( "construction_group", &construction_groups::load );
@@ -613,6 +614,7 @@ void DynamicDataLoader::unload_data()
overmap_special_migration::reset();
overmap_terrains::reset();
overmap::reset_oter_id_migrations();
+ overmap::reset_oter_id_camp_migrations();
profession::reset();
profession_blacklist::reset();
proficiency::reset();
diff --git a/src/map.cpp b/src/map.cpp
index 0e23500e40c6e..fef25c1fdd78d 100644
--- a/src/map.cpp
+++ b/src/map.cpp
@@ -6878,12 +6878,15 @@ basecamp map::hoist_submap_camp( const tripoint &p )
return pcamp ? *pcamp : basecamp();
}
-void map::add_camp( const tripoint_abs_omt &omt_pos, const std::string &name )
+void map::add_camp( const tripoint_abs_omt &omt_pos, const std::string &name, bool need_validate )
{
basecamp temp_camp = basecamp( name, omt_pos );
overmap_buffer.add_camp( temp_camp );
get_player_character().camps.insert( omt_pos );
- g->validate_camps();
+ // Mapgen-spawned camps never need or want to validate, since they'll always be on newly generated OMTs
+ if( need_validate ) {
+ g->validate_camps();
+ }
}
void map::update_submaps_with_active_items()
diff --git a/src/map.h b/src/map.h
index 56b6b7c2bb0fe..7bd3cfd82bfad 100644
--- a/src/map.h
+++ b/src/map.h
@@ -1798,7 +1798,8 @@ class map
computer *add_computer( const tripoint &p, const std::string &name, int security );
// Camps
- void add_camp( const tripoint_abs_omt &omt_pos, const std::string &name );
+ void add_camp( const tripoint_abs_omt &omt_pos, const std::string &name,
+ bool need_validate = true );
void remove_submap_camp( const tripoint & );
void remove_submap_camp( const tripoint_bub_ms & );
basecamp hoist_submap_camp( const tripoint &p );
diff --git a/src/npcmove.cpp b/src/npcmove.cpp
index 680270e61334c..0146b30965724 100644
--- a/src/npcmove.cpp
+++ b/src/npcmove.cpp
@@ -4555,9 +4555,6 @@ static float rate_food( const item &it, int want_nutr, int want_quench )
bool npc::consume_food_from_camp()
{
- if( !is_player_ally() ) {
- return false;
- }
Character &player_character = get_player_character();
std::optional potential_bc;
for( const tripoint_abs_omt &camp_pos : player_character.camps ) {
@@ -4572,46 +4569,28 @@ bool npc::consume_food_from_camp()
return false;
}
basecamp *bcp = *potential_bc;
- if( get_thirst() > 40 && bcp->has_water() ) {
+
+ // Handle water
+ if( get_thirst() > 40 && bcp->has_water() && bcp->allowed_access_by( *this, true ) ) {
complain_about( "camp_water_thanks", 1_hours,
chat_snippets().snip_camp_water_thanks.translated(), false );
// TODO: Stop skipping the stomach for this, actually put the water in there.
set_thirst( 0 );
return true;
}
- faction *yours = player_character.get_faction();
+ // Handle food
int current_kcals = get_stored_kcal() + stomach.get_calories() + guts.get_calories();
int kcal_threshold = get_healthy_kcal() * 19 / 20;
- if( get_hunger() > 0 && current_kcals < kcal_threshold ) {
+ if( get_hunger() > 0 && current_kcals < kcal_threshold && bcp->allowed_access_by( *this ) ) {
// Try to eat a bit more than the bare minimum so that we're not eating every 5 minutes
// but also don't try to eat a week's worth of food in one sitting
int desired_kcals = std::min( static_cast( base_metabolic_rate ), std::max( 0,
kcal_threshold + 100 - current_kcals ) );
- int kcals_to_eat = std::min( desired_kcals, yours->food_supply.kcal() );
+ int kcals_to_eat = std::min( desired_kcals, bcp->get_owner()->food_supply.kcal() );
if( kcals_to_eat > 0 ) {
- // We need food and there's some available, so let's eat it
- complain_about( "camp_food_thanks", 1_hours,
- chat_snippets().snip_camp_food_thanks.translated(), false );
-
- // Make a fake food object here to feed the NPC with, since camp calories are abstracted away
-
- // Fill up the stomach to "full" (half of capacity) but no further, to avoid NPCs vomiting
- // or becoming engorged
- units::volume filling_vol = std::max( 0_ml, stomach.capacity( *this ) / 2 - stomach.contains() );
-
- // Returns the actual amount of calories and vitamins taken from the camp's larder.
- nutrients nutr = bcp->camp_food_supply( -kcals_to_eat );
-
- stomach.ingest( food_summary{
- 0_ml,
- filling_vol,
- nutr
- } );
- // Ensure our hunger is satisfied so we don't try to eat again immediately.
- // update_stomach() usually takes care of that but it's only called once every 10 seconds for NPCs
- set_hunger( -1 );
+ bcp->feed_workers( *this, bcp->camp_food_supply( -kcals_to_eat ) );
return true;
} else {
@@ -4636,7 +4615,7 @@ bool npc::consume_food()
const std::vector- inv_food = cache_get_items_with( "is_food", &item::is_food );
if( inv_food.empty() ) {
- if( !is_player_ally() ) {
+ if( !needs_food() ) {
// TODO: Remove this and let player "exploit" hungry NPCs
set_hunger( 0 );
set_thirst( 0 );
diff --git a/src/npctalk_funcs.cpp b/src/npctalk_funcs.cpp
index 020e6c63747af..6cf962bc8d202 100644
--- a/src/npctalk_funcs.cpp
+++ b/src/npctalk_funcs.cpp
@@ -358,6 +358,9 @@ void talk_function::goto_location( npc &p )
if( elem == p.global_omt_location() ) {
continue;
}
+ if( !overmap_buffer.seen( elem ) ) {
+ continue;
+ }
std::optional camp = overmap_buffer.find_camp( elem.xy() );
if( !camp ) {
continue;
diff --git a/src/omdata.h b/src/omdata.h
index 9675a92d3135b..04f9460765312 100644
--- a/src/omdata.h
+++ b/src/omdata.h
@@ -500,6 +500,8 @@ struct overmap_special_terrain : overmap_special_locations {
const std::set & );
oter_str_id terrain;
std::set flags;
+ std::optional camp_owner;
+ translation camp_name;
void deserialize( const JsonObject &om );
};
diff --git a/src/overmap.cpp b/src/overmap.cpp
index 15e728d96740c..0691d94b68d75 100644
--- a/src/overmap.cpp
+++ b/src/overmap.cpp
@@ -1103,6 +1103,8 @@ void overmap_special_terrain::deserialize( const JsonObject &om )
{
om.read( "point", p );
om.read( "overmap", terrain );
+ om.read( "camp", camp_owner );
+ om.read( "camp_name", camp_name );
om.read( "flags", flags );
om.read( "locations", locations );
}
@@ -1313,6 +1315,18 @@ struct fixed_overmap_special_data : overmap_special_data {
points.insert( pos );
}
+ if( elem.camp_owner.has_value() ) {
+ if( !elem.camp_owner.value().is_valid() ) {
+ debugmsg( "In %s, camp at %s has invalid owner %s", context, pos.to_string(),
+ elem.camp_owner.value().c_str() );
+ }
+ if( elem.camp_name.empty() ) {
+ debugmsg( "In %s, camp was defined but missing a camp_name.", context );
+ }
+ } else if( !elem.camp_name.empty() ) {
+ debugmsg( "In %s, camp_name defined but no owner. Invalid name is discarded.", context );
+ }
+
if( elem.locations.empty() ) {
debugmsg( "In %s, no location is defined for point %s or the "
"overall special.", context, pos.to_string() );
@@ -1419,6 +1433,21 @@ struct fixed_overmap_special_data : overmap_special_data {
result.omts_used.push_back( location );
const oter_id tid = elem.terrain->get_rotated( dir );
om.ter_set( location, tid );
+ if( elem.camp_owner.has_value() ) {
+ // This always results in z=0, but pos() doesn't return z-level information...
+ tripoint_abs_omt camp_loc = {project_combine( om.pos(), location.xy() ), 0};
+ get_map().add_camp( camp_loc, "faction_camp", false );
+ std::optional bcp = overmap_buffer.find_camp( camp_loc.xy() );
+ if( !bcp ) {
+ debugmsg( "Camp placement during special generation failed at %s", camp_loc.to_string() );
+ } else {
+ basecamp *temp_camp = *bcp;
+ temp_camp->set_owner( elem.camp_owner.value() );
+ temp_camp->set_name( elem.camp_name.translated() );
+ // FIXME? Camp types are raw strings! Not ideal.
+ temp_camp->define_camp( camp_loc, "faction_base_bare_bones_NPC_camp_0", false );
+ }
+ }
if( blob ) {
for( int x = -2; x <= 2; x++ ) {
for( int y = -2; y <= 2; y++ ) {
@@ -3943,6 +3972,7 @@ void overmap::clear_connections_out()
}
static std::map oter_id_migrations;
+static std::map> camp_migration_map;
void overmap::load_oter_id_migration( const JsonObject &jo )
{
@@ -3951,11 +3981,34 @@ void overmap::load_oter_id_migration( const JsonObject &jo )
}
}
+void overmap::load_oter_id_camp_migration( const JsonObject &jo )
+{
+ std::string name;
+ oter_type_str_id oter;
+ faction_id owner;
+ JsonObject jsobj = jo.get_object( "camp_migrations" );
+ jsobj.read( "name", name );
+ jsobj.read( "overmap_terrain", oter );
+ jsobj.read( "faction", owner );
+ camp_migration_map.emplace( oter,
+ std::pair( translation::to_translation( name ), owner ) );
+}
+
+void overmap::reset_oter_id_camp_migrations()
+{
+ camp_migration_map.clear();
+}
+
void overmap::reset_oter_id_migrations()
{
oter_id_migrations.clear();
}
+bool overmap::oter_id_should_have_camp( const oter_type_str_id &oter )
+{
+ return camp_migration_map.count( oter ) > 0;
+}
+
bool overmap::is_oter_id_obsolete( const std::string &oterid )
{
return oter_id_migrations.count( oterid ) > 0;
@@ -3978,6 +4031,28 @@ void overmap::migrate_oter_ids( const std::unordered_map &points ) const
+{
+ for( const tripoint_abs_omt &point : points ) {
+ std::optional bcp = overmap_buffer.find_camp( point.xy() );
+ if( bcp ) {
+ continue; // Already a camp nearby, can't put down another one
+ }
+ get_map().add_camp( point, "faction_camp", false );
+ bcp = overmap_buffer.find_camp( point.xy() );
+ if( !bcp ) {
+ debugmsg( "Camp placement failed during migration?!" );
+ continue;
+ }
+ basecamp *temp_camp = *bcp;
+ const oter_type_str_id &keyvalue = ter( project_remain
+ ( point ).remainder_tripoint )->get_type_id();
+ temp_camp->set_owner( camp_migration_map[keyvalue].second );
+ temp_camp->set_name( camp_migration_map[keyvalue].first.translated() );
+ temp_camp->define_camp( point, "faction_base_bare_bones_NPC_camp_0", false );
+ }
+}
+
oter_id overmap::get_or_migrate_oter( const std::string &oterid )
{
auto migration = oter_id_migrations.find( oterid );
diff --git a/src/overmap.h b/src/overmap.h
index 149ba7b0884dd..0195d36eed3d2 100644
--- a/src/overmap.h
+++ b/src/overmap.h
@@ -549,9 +549,13 @@ class overmap
void load_legacy_monstergroups( const JsonArray &jsin );
void save_monster_groups( JsonOut &jo ) const;
public:
+ static void load_oter_id_camp_migration( const JsonObject &jo );
static void load_oter_id_migration( const JsonObject &jo );
+ static void reset_oter_id_camp_migrations();
static void reset_oter_id_migrations();
+ static bool oter_id_should_have_camp( const oter_type_str_id &oter );
static bool is_oter_id_obsolete( const std::string &oterid );
+ void migrate_camps( const std::vector &points ) const;
void migrate_oter_ids( const std::unordered_map &points );
oter_id get_or_migrate_oter( const std::string &oterid );
};
diff --git a/src/savegame.cpp b/src/savegame.cpp
index 9ab7dc6f55fdb..07c2698feb514 100644
--- a/src/savegame.cpp
+++ b/src/savegame.cpp
@@ -417,6 +417,7 @@ void overmap::unserialize( const JsonObject &jsobj )
// Extract layers first so predecessor deduplication can happen.
if( jsobj.has_member( "layers" ) ) {
std::unordered_map oter_id_migrations;
+ std::vector camps_to_place;
JsonArray layers_json = jsobj.get_array( "layers" );
for( int z = 0; z < OVERMAP_LAYERS; ++z ) {
@@ -445,6 +446,11 @@ void overmap::unserialize( const JsonObject &jsobj )
debugmsg( "Loaded invalid oter_id '%s'", tmp_ter.c_str() );
tmp_otid = oter_omt_obsolete;
}
+ if( oter_id_should_have_camp( oter_str_id( tmp_ter )->get_type_id() ) ) {
+ for( int p = i; p < i + count; p++ ) {
+ camps_to_place.emplace_back( project_combine( pos(), tripoint_om_omt( p, j, z - OVERMAP_DEPTH ) ) );
+ }
+ }
}
count--;
layer[z].terrain[i][j] = tmp_otid;
@@ -452,6 +458,7 @@ void overmap::unserialize( const JsonObject &jsobj )
}
}
migrate_oter_ids( oter_id_migrations );
+ migrate_camps( camps_to_place );
}
for( JsonMember om_member : jsobj ) {
const std::string name = om_member.name();