Roster: [html_encode(ckeys.Join(", "))]
Claimed encounters: [length(settlement_claims)]
Hull: [html_encode(team.ship.name)]
" + lines += "| Band | Total | " + for(var/family in zone_reputation_families()) + lines += "[family] | " + lines += "
|---|---|---|
| [metric_zone_name(band)] | [zone_totals["[band]"]] | " + for(var/family in zone_reputation_families()) + lines += "[families[family]] | " + lines += "
[html_encode("[trader]")]: [trader_favor[trader]]
" + lines += "[html_encode("[sku]")]: [favor_unique_purchases[sku]]
" + return lines.Join("") + +ADMIN_VERB(zone_crew_reputation, R_ADMIN|R_DEBUG, "Zones: Crew Reputation", "Inspect, set family contributions or reset a continuing crew's shared record.", ADMIN_CATEGORY_DEBUG) + var/datum/voidcrew_crew_record/record = voidcrew_admin_pick_crew_record(user) + if(!record) + return + var/action = tgui_input_list(user, "Crew #[record.crew_id]", "Crew Reputation", list("Inspect", "Set family", "Reset record")) + if(!action || QDELETED(record) || !check_rights_for(user, R_ADMIN|R_DEBUG)) + return + if(action == "Inspect") + var/datum/browser/popup = new(user.mob, "crewreputation", "Crew Reputation", 760, 600) + popup.set_content(record.admin_zone_report()) + popup.open() + return + var/family + var/band + var/amount + if(action == "Set family") + band = voidcrew_admin_pick_zone_band(user, "Crew Reputation") + if(isnull(band) || QDELETED(record)) + return + family = tgui_input_list(user, "Which family?", "Crew Reputation", zone_reputation_families()) + if(!family || QDELETED(record)) + return + var/list/families = record.zone_families["[band]"] + var/cap = band == ZONE_GREEN ? ZONE_REPUTATION_GREEN_FAMILY_CAP : ZONE_REPUTATION_DANGER_FAMILY_CAP + amount = tgui_input_number(user, "Lifetime contribution (0-[cap]):", "Crew Reputation", families[family], cap, 0) + if(isnull(amount)) + return + else if(tgui_alert(user, "Reset Crew #[record.crew_id]'s zone contributions, trader favor and unique purchase counts? Membership and spent encounter claims stay intact.", "Reset Crew Record", list("Reset", "Cancel")) != "Reset") + return + // Every prompt yields; check rights and registry membership again before touching the ledger. + if(QDELETED(record) || !(record in GLOB.voidcrew_crew_records) || !check_rights_for(user, R_ADMIN|R_DEBUG)) + return + var/list/before = record.zone_totals.Copy() + var/changed = action == "Set family" ? record.admin_set_zone_family(family, band, amount) : record.admin_reset_zone_record() + if(!changed) + return + var/operation = action == "Set family" ? "set [metric_zone_name(band)]/[family] to [amount]" : "reset balances and purchase counts" + message_admins("[key_name_admin(user)] [operation] for crew #[record.crew_id].") + log_admin("[key_name(user)] [operation] for crew #[record.crew_id]; zone totals [json_encode(before)] -> [json_encode(record.zone_totals)].") + record_metric(METRIC_MISSION, "zone_record_admin_changed", ckey = metric_ckey(user.mob), subject = "crew_[record.crew_id]", details = list("crew_id" = record.crew_id, "operation" = operation, "totals_before" = before, "totals_after" = record.zone_totals.Copy())) + BLACKBOX_LOG_ADMIN_VERB("Crew Reputation") + ADMIN_VERB(give_trade_vouchers, R_ADMIN|R_DEBUG, "Trade: Give Vouchers", "Put a stack of trade vouchers in a player's hands.", ADMIN_CATEGORY_DEBUG) var/mob/living/target = voidcrew_admin_pick_player(user, "Give Trade Vouchers") if(!target) @@ -701,29 +808,6 @@ ADMIN_VERB(reveal_rare_ruin, R_ADMIN|R_DEBUG, "Trade: Reveal Rare Ruin", "Spawn log_admin("[key_name(user)] revealed rare ruin '[template.name]'.") BLACKBOX_LOG_ADMIN_VERB("Reveal Rare Ruin") -ADMIN_VERB(spawn_gun_blueprint, R_ADMIN|R_DEBUG, "Trade: Spawn Blueprint", "Spawn a specific weapons-bench gun blueprint at your feet.", ADMIN_CATEGORY_DEBUG) - var/turf/drop_turf = get_turf(user.mob) - if(!drop_turf) - to_chat(user, span_warning("You need a physical location to spawn a blueprint at.")) - return - var/list/choices = list() - for(var/obj/item/blueprint/blueprint_type as anything in subtypesof(/obj/item/blueprint)) - if(blueprint_type == /obj/item/blueprint/gun) - continue - choices[initial(blueprint_type.name)] = blueprint_type - var/choice = tgui_input_list(user, "Which blueprint?", "Spawn Blueprint", sort_list(choices)) - if(!choice) - return - var/blueprint_type = choices[choice] - var/obj/item/blueprint/blueprint = new blueprint_type(drop_turf) - var/mob/living/admin_mob = user.mob - if(isliving(admin_mob)) - admin_mob.put_in_hands(blueprint) - to_chat(user, span_notice("[blueprint] spawned.")) - message_admins("[key_name_admin(user)] spawned blueprint '[blueprint]'.") - log_admin("[key_name(user)] spawned blueprint '[blueprint]'.") - BLACKBOX_LOG_ADMIN_VERB("Spawn Gun Blueprint") - // ===== OVERMAP / GAS ECONOMY ===== ADMIN_VERB(spawn_gas_nebula, R_ADMIN|R_DEBUG, "Overmap: Spawn Nebula", "Spawn a gas nebula on the overmap, under a ship for instant ram scoop testing, or on an empty square.", ADMIN_CATEGORY_DEBUG) diff --git a/voidcrew/modules/ambient_npcs/ambient_subsystem.dm b/voidcrew/modules/ambient_npcs/ambient_subsystem.dm index 397eecaafcf..76c4ad07c88 100644 --- a/voidcrew/modules/ambient_npcs/ambient_subsystem.dm +++ b/voidcrew/modules/ambient_npcs/ambient_subsystem.dm @@ -800,6 +800,9 @@ SUBSYSTEM_DEF(ambient_npcs) for(var/turf/tile as anything in floor) if(passages[tile] || (locate(/obj/machinery/door) in tile)) continue + // Nobody stands about in the checkpoint lobby (outpost_checkpoint.dm) + if(istype(tile.loc, /area/voidcrew/trader_outpost/checkpoint)) + continue var/skip = FALSE for(var/cdir in GLOB.cardinals) var/turf/next = get_step(tile, cdir) @@ -1088,6 +1091,8 @@ SUBSYSTEM_DEF(ambient_npcs) npc.role = type npc.set_place(place) npc.fade_in() + // Off the lift on the lobby side of the weapons checkpoint: through it first (outpost_checkpoint_npcs.dm) + ambient_cross_checkpoint_on_arrival(npc, place?.outpost()) return npc /** diff --git a/voidcrew/modules/ambient_npcs/outpost_patrons.dm b/voidcrew/modules/ambient_npcs/outpost_patrons.dm index 6b12874386b..78f33a7124c 100644 --- a/voidcrew/modules/ambient_npcs/outpost_patrons.dm +++ b/voidcrew/modules/ambient_npcs/outpost_patrons.dm @@ -179,6 +179,9 @@ /proc/ambient_outpost_tile_avoided(turf/tile, datum/ambient_place/place, counter_business = FALSE) if(!tile || istype(tile, /turf/open/water) || ambient_by_a_door(tile)) return TRUE + // The checkpoint lobby is for passing through (outpost_checkpoint.dm) + if(istype(tile.loc, /area/voidcrew/trader_outpost/checkpoint)) + return TRUE var/datum/ambient_place/outpost/outpost_place = place var/obj/structure/overmap/trader_outpost/outpost = istype(outpost_place) ? outpost_place.outpost() : null for(var/turf/alcove as anything in outpost?.lobby_alcove_turfs) diff --git a/voidcrew/modules/ambient_npcs/outpost_workers.dm b/voidcrew/modules/ambient_npcs/outpost_workers.dm index e6e43087ae4..fec79e7269c 100644 --- a/voidcrew/modules/ambient_npcs/outpost_workers.dm +++ b/voidcrew/modules/ambient_npcs/outpost_workers.dm @@ -858,9 +858,11 @@ var/mob/living/basic/ambient_npc/drunk = anchor() if(!drunk || drunk.fading || drunk.stat != CONSCIOUS || !istype(drunk.activity, /datum/ambient_activity/leave)) return AMBIENT_STEP_DONE - // They can manage the last few steps to the lift; the barback stays off it + // They can manage the last few steps to the lift; the barback stays off it, and on this side of the checkpoint var/mob/living/basic/ambient_npc/outpost/barback = doer var/obj/structure/overmap/trader_outpost/outpost = istype(barback) ? barback.get_outpost() : null + if(length(outpost?.checkpoints) && !outpost.is_secured_turf(get_turf(drunk))) + return AMBIENT_STEP_DONE for(var/turf/alcove as anything in outpost?.lobby_alcove_turfs) if(alcove.z == drunk.z && get_dist(alcove, drunk) <= 2) return AMBIENT_STEP_DONE @@ -950,9 +952,10 @@ return ..() /** - * Unloading the convoy: sealed crates from beside the lift to the loading area in front of the - * depot's counter, a few trips over about a minute and a half. The crates are part of the look: - * they go when they are set down, and the shelves are the convoy's business, not theirs. + * Unloading the convoy: sealed crates from beside the lift (just inside the weapons checkpoint, where + * there is one) to the loading area in front of the depot's counter, a few trips over about a + * minute and a half. The crates are part of the look: they go when they are set down, and the + * shelves are the convoy's business, not theirs. */ /datum/ambient_activity/convoy_unload name = "unloading the convoy" @@ -961,7 +964,7 @@ var/trips_left = WORKER_CONVOY_TRIPS /// A crate in their arms var/carrying = FALSE - /// Beside the lift + /// Beside the lift, or just inside the weapons checkpoint var/turf/pickup /// In front of the counter, out of the customers' way var/turf/dropoff @@ -982,10 +985,17 @@ if(!trader) return FALSE var/list/lift_spots = list() - for(var/turf/alcove as anything in outpost.lobby_alcove_turfs) - var/turf/spot = ambient_waiting_spot(doer, alcove, 2, 3, failed_spots, loiter = FALSE) - if(spot) + // Past a weapons checkpoint the crates are handed through it, and picked up on this side + var/datum/outpost_checkpoint/checkpoint = outpost.lobby_checkpoint(doer) + if(checkpoint?.secured_turf) + var/turf/spot = ambient_waiting_spot(doer, checkpoint.secured_turf, 2, 3, failed_spots, loiter = FALSE) + if(spot && outpost.is_secured_turf(spot)) lift_spots += spot + else + for(var/turf/alcove as anything in outpost.lobby_alcove_turfs) + var/turf/spot = ambient_waiting_spot(doer, alcove, 2, 3, failed_spots, loiter = FALSE) + if(spot) + lift_spots += spot pickup = ambient_nearest_tile(doer, lift_spots) dropoff = ambient_waiting_spot(doer, trader, 3, 4, failed_spots, loiter = FALSE) if(!pickup || !dropoff) diff --git a/voidcrew/modules/ambient_npcs/planet_peddler.dm b/voidcrew/modules/ambient_npcs/planet_peddler.dm index 1411c751398..e1c78d6472d 100644 --- a/voidcrew/modules/ambient_npcs/planet_peddler.dm +++ b/voidcrew/modules/ambient_npcs/planet_peddler.dm @@ -210,7 +210,7 @@ /datum/shop_sku/peddler/odd/lighter item_path = /obj/item/lighter - price_credits = 80 + price_credits = 50 /datum/shop_sku/peddler/odd/whiskey item_path = /obj/item/reagent_containers/cup/glass/bottle/whiskey @@ -816,18 +816,7 @@ for(var/i in 1 to count) var/turf/where = ambient_free_turf_near(landing, 2) || landing var/raider_type = i == 3 ? /mob/living/basic/trooper/pirate/faction/grey/ranged : pick_weight(raiders - /mob/living/basic/trooper/pirate/faction/grey/ranged) - var/mob/living/raider = new raider_type(where) - // Not the planet's own: it comes for the caravan, and is kept near it - var/list/kept = list() - for(var/faction_token in raider.faction) - if(!findtext("[faction_token]", "planetary_")) - kept += faction_token - raider.faction = kept - raider.AddComponent(/datum/component/leash, doer, AMBIENT_AMBUSH_LEASH) - raider.AddElement(/datum/element/relay_attackers) - RegisterSignal(raider, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_pack_attacked)) - if(bait) - raider.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, bait) + var/mob/living/raider = spawn_raider(raider_type, where, bait) var/datum/weakref/ref = WEAKREF(raider) pack += ref pack_refs += ref @@ -837,6 +826,25 @@ SSplanet_mobs.total_managed_mobs += count return count +/// Ready each ambusher against the caravan site before applying its profile. +/datum/ambient_activity/caravan_ambush/proc/spawn_raider(raider_type, turf/where, mob/living/bait) + var/datum/ambient_place/site/site = doer.place + var/datum/npc_spawn_context/context = new(site, site.band, source_turf = get_turf(doer)) + var/mob/living/raider = spawn_npc_in_zone(raider_type, where, context, FALSE) + // Not the planet's own: it comes for the caravan, and is kept near it. + var/list/kept = list() + for(var/faction_token in raider.faction) + if(!findtext("[faction_token]", "planetary_")) + kept += faction_token + raider.faction = kept + raider.AddComponent(/datum/component/leash, doer, AMBIENT_AMBUSH_LEASH) + raider.AddElement(/datum/element/relay_attackers) + RegisterSignal(raider, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_pack_attacked)) + if(bait) + raider.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, bait) + context.finalize() + return raider + /// Someone hit the pack: their crew gets the helpers' price /datum/ambient_activity/caravan_ambush/proc/on_pack_attacked(datum/source, atom/attacker, attack_flags) SIGNAL_HANDLER diff --git a/voidcrew/modules/ambient_npcs/recruit.dm b/voidcrew/modules/ambient_npcs/recruit.dm index 89ce467fd36..e4b9a61554d 100644 --- a/voidcrew/modules/ambient_npcs/recruit.dm +++ b/voidcrew/modules/ambient_npcs/recruit.dm @@ -420,6 +420,7 @@ GLOBAL_LIST_EMPTY(ambient_ship_places) stray_state = STRAY_WILD REMOVE_TRAIT(src, TRAIT_SPACEWALK, STRAY_TRAIT) return FALSE + exempt_npc_zone() on_joined(ship) speak_context("accepted", leader, force = TRUE) log_game("AMBIENT: [src] agreed to join the [ship.name], asked by [key_name(leader)] at [AREACOORD(src)]") diff --git a/voidcrew/modules/anomalies/core_rewards.dm b/voidcrew/modules/anomalies/core_rewards.dm new file mode 100644 index 00000000000..2336f9338b7 --- /dev/null +++ b/voidcrew/modules/anomalies/core_rewards.dm @@ -0,0 +1,77 @@ +#define PLANET_ICE_DEMON_CORE_CAP 2 + +/obj/structure/overmap/planet + /// Shared by both demon variants, retained when the surface unloads or relocates. + var/ice_demon_cores_emitted = 0 + +/// Remember the source at spawn, before a demon can be carried to another site. +/datum/component/ice_demon_core_reward + var/datum/weakref/source_planet_ref + /// Preloaded surfaces get their overmap owner after their mapped mobs initialize. + var/turf/pending_spawn_turf + var/core_type + var/drop_chance + /// A failed first roll is final too; revival cannot buy another attempt. + var/attempted = FALSE + +/datum/component/ice_demon_core_reward/Initialize(core_type, drop_chance) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.core_type = core_type + src.drop_chance = drop_chance + var/turf/spawn_turf = get_turf(parent) + bind_source(spawn_turf) + if(!source_planet_ref && !SSovermap.initialized) + pending_spawn_turf = spawn_turf + RegisterSignal(SSovermap, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(on_overmap_initialized)) + +/datum/component/ice_demon_core_reward/proc/bind_source(turf/spawn_turf) + var/datum/map_footprint/footprint = map_region_for_turf(spawn_turf) + if(istype(footprint) && istype(footprint.owner, /obj/structure/overmap/planet)) + source_planet_ref = WEAKREF(footprint.owner) + +/datum/component/ice_demon_core_reward/proc/on_overmap_initialized(datum/source) + SIGNAL_HANDLER + UnregisterSignal(SSovermap, COMSIG_SUBSYSTEM_POST_INITIALIZE) + bind_source(pending_spawn_turf) + pending_spawn_turf = null + +/datum/component/ice_demon_core_reward/RegisterWithParent() + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_death)) + +/datum/component/ice_demon_core_reward/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_LIVING_DEATH) + +/datum/component/ice_demon_core_reward/Destroy(force) + UnregisterSignal(SSovermap, COMSIG_SUBSYSTEM_POST_INITIALIZE) + pending_spawn_turf = null + source_planet_ref = null + return ..() + +/datum/component/ice_demon_core_reward/proc/on_death(mob/living/source, gibbed) + SIGNAL_HANDLER + if(attempted) + return + attempted = TRUE + if(!prob(drop_chance) || pending_spawn_turf) + return + var/obj/structure/overmap/planet/source_planet = source_planet_ref?.resolve() + // A deleted source cannot turn a transported demon into an unrestricted reward. + if(source_planet_ref && (!source_planet || source_planet.ice_demon_cores_emitted >= PLANET_ICE_DEMON_CORE_CAP)) + return + var/atom/destination = source.drop_location() + if(!destination) + return + var/obj/item/core = new core_type(destination) + if(!QDELETED(core) && source_planet) + source_planet.ice_demon_cores_emitted++ + +/mob/living/basic/mining/ice_demon/Initialize(mapload) + . = ..() + AddComponent(/datum/component/ice_demon_core_reward, /obj/item/raw_anomaly_core/bluespace, 5) + +/mob/living/simple_animal/hostile/asteroid/old_demon/Initialize(mapload) + . = ..() + AddComponent(/datum/component/ice_demon_core_reward, /obj/item/assembly/signaler/anomaly/bluespace, 20) + +#undef PLANET_ICE_DEMON_CORE_CAP diff --git a/voidcrew/modules/antag_ruins/ascension_mech.dm b/voidcrew/modules/antag_ruins/ascension_mech.dm index d31fe93b958..b8fab656eb6 100644 --- a/voidcrew/modules/antag_ruins/ascension_mech.dm +++ b/voidcrew/modules/antag_ruins/ascension_mech.dm @@ -330,6 +330,7 @@ /mob/living/basic/vestige_warframe/Initialize(mapload) . = ..() + npc_red_weapon_drop = npc_ladder_red_drop() add_traits(list(TRAIT_NOFIRE, TRAIT_NOBREATH, TRAIT_RESISTHIGHPRESSURE, TRAIT_RESISTLOWPRESSURE), INNATE_TRAIT) AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY) @@ -627,9 +628,8 @@ /** * The drops. * - * Same shape as the Matriarch's: one guaranteed piece so the kill is always worth - * something, then a jackpot rolled from a pool where both entries hand you a piece of - * what just beat you. The capstone itself is paid out by the ascension run, not here. + * The blade is rolled once at construction. The utility prize and ascension + * capstone keep their separate reward paths. */ /mob/living/basic/vestige_warframe/proc/drop_the_match() if(loot_dropped) @@ -638,7 +638,8 @@ if(isnull(spot)) return loot_dropped = TRUE - new /obj/item/sparring_blade(spot) + if(npc_red_weapon_drop) + new /obj/item/sparring_blade(spot) var/static/list/jackpot_pool = list( /obj/item/warframe_contact_plate, /obj/item/warframe_actuator, diff --git a/voidcrew/modules/antag_ruins/theme_cult.dm b/voidcrew/modules/antag_ruins/theme_cult.dm index 55ab06aed55..42ff4681059 100644 --- a/voidcrew/modules/antag_ruins/theme_cult.dm +++ b/voidcrew/modules/antag_ruins/theme_cult.dm @@ -238,9 +238,11 @@ paid = TRUE votive.forceMove(get_turf(user)) votive.anchored = TRUE + votive.get_npc_spawn_context() running = TRUE for(var/index in 1 to 3) - var/mob/living/basic/carp/vestige_clot/clot = new(pick_n_take(places)) + var/datum/npc_spawn_context/context = new(votive) + var/mob/living/basic/carp/vestige_clot/clot = spawn_npc_in_zone(/mob/living/basic/carp/vestige_clot, pick_n_take(places), context) clots += clot RegisterSignal(clot, COMSIG_LIVING_DEATH, PROC_REF(clot_died)) refresh_tracker() diff --git a/voidcrew/modules/antag_ruins/theme_dragon.dm b/voidcrew/modules/antag_ruins/theme_dragon.dm index cf5c9ede3b7..5983795e341 100644 --- a/voidcrew/modules/antag_ruins/theme_dragon.dm +++ b/voidcrew/modules/antag_ruins/theme_dragon.dm @@ -423,6 +423,7 @@ /// Wakes the watch: the egg warms, the ground is chosen, and the first tide is heralded /obj/structure/vestige_dragon_egg/proc/begin_assault(mob/living/user) + get_npc_spawn_context() assault_underway = TRUE START_PROCESSING(SSobj, src) color = "#d8905a" // the cold gives way @@ -459,9 +460,12 @@ return wave_lost = FALSE stage++ + get_npc_spawn_context() for(var/i in 1 to 2) - var/mob/living/basic/carp/vestige_brood/hunter = new(wave_perch) + var/datum/npc_spawn_context/context = new(src) + var/mob/living/basic/carp/vestige_brood/hunter = spawn_npc_in_zone(/mob/living/basic/carp/vestige_brood, wave_perch, context, FALSE) enlist(hunter, egg_bound = (i % 2 == 0)) + context.finalize() visible_message(span_boldwarning("The brood pours from the announced approach and turns toward [src]!")) playsound(src, 'sound/effects/magic/wand_teleport.ogg', 70, TRUE) var/datum/vestige_trial/broodwatch/trial = get_bound_trial() @@ -651,6 +655,7 @@ * hunts, and tamed to the keeper by the constructor's tamer argument. */ /mob/living/basic/carp/pet/vestige_hatchling + npc_zone_exempt = TRUE name = "dragonet" desc = "A carp the colour of banked embers, fresh from a shell it had no business fitting inside. It is extremely certain it is a dragon. Nothing so far has dared correct it." gender = NEUTER diff --git a/voidcrew/modules/antag_ruins/theme_spider.dm b/voidcrew/modules/antag_ruins/theme_spider.dm index 6bd2cee52e6..c19801ceb4f 100644 --- a/voidcrew/modules/antag_ruins/theme_spider.dm +++ b/voidcrew/modules/antag_ruins/theme_spider.dm @@ -1125,7 +1125,9 @@ next_send_at = world.time + VESTIGE_TREMOR_RESPITE return next_send_at = world.time + VESTIGE_TREMOR_RESPITE // floor, so an instant kill can't machine-gun the night - var/mob/living/basic/vestige_silk_thief/filcher = new(pick(perches)) + line.get_npc_spawn_context() + var/datum/npc_spawn_context/context = new(line) + var/mob/living/basic/vestige_silk_thief/filcher = spawn_npc_in_zone(/mob/living/basic/vestige_silk_thief, pick(perches), context, FALSE) thieves[filcher] = line line.pinged = FALSE // every new thief must announce its own first bite register_loan(filcher) @@ -1134,6 +1136,7 @@ // The lifespan rides the THIEF, not the trial. Orphans always clean themselves up addtimer(CALLBACK(filcher, TYPE_PROC_REF(/mob/living/basic/vestige_silk_thief, dissolve)), VESTIGE_TREMOR_THIEF_LIFESPAN) filcher.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, line) + context.finalize() if(isliving(keeper)) to_chat(keeper, span_warning("The web shivers. Something small is moving near your line to the [dir2text(get_dir(keeper, line)) || "very spot you stand on"].")) playsound(keeper, 'sound/effects/snap.ogg', 25, TRUE) diff --git a/voidcrew/modules/antag_ruins/theme_xeno.dm b/voidcrew/modules/antag_ruins/theme_xeno.dm index 8c0e4abef47..b2957c75f53 100644 --- a/voidcrew/modules/antag_ruins/theme_xeno.dm +++ b/voidcrew/modules/antag_ruins/theme_xeno.dm @@ -402,6 +402,7 @@ /// Begins the season: the shell warms and the first work-gang is heralded /obj/structure/vestige_comb_egg/proc/begin_season(mob/living/user) + get_npc_spawn_context() assault_underway = TRUE START_PROCESSING(SSobj, src) color = "#e0c47f" // the warmth takes @@ -436,12 +437,15 @@ visible_message(span_warning("The approach has closed. Clear it and touch the shell to call the gang again.")) return squad_lost = FALSE + get_npc_spawn_context() var/list/gang_sizes = list(1, 2, 2, 3) var/squad_size = gang_sizes[squads_landed + 1] for(var/i in 1 to squad_size) - var/mob/living/basic/hivebot/vestige_comb_chewer/vermin = new(squad_perch) + var/datum/npc_spawn_context/context = new(src) + var/mob/living/basic/hivebot/vestige_comb_chewer/vermin = spawn_npc_in_zone(/mob/living/basic/hivebot/vestige_comb_chewer, squad_perch, context, FALSE) do_sparks(3, TRUE, vermin) enlist(vermin, egg_bound = TRUE) + context.finalize() squads_landed++ visible_message(span_boldwarning("The salvage gang clatters from the announced approach, cutters spinning!")) var/datum/vestige_trial/warm_season/trial = get_bound_trial() diff --git a/voidcrew/modules/bounties/bounty_activities.dm b/voidcrew/modules/bounties/bounty_activities.dm index ebe45351ea1..894fd6f1026 100644 --- a/voidcrew/modules/bounties/bounty_activities.dm +++ b/voidcrew/modules/bounties/bounty_activities.dm @@ -1310,6 +1310,9 @@ return entry[2] var/list/floor = list() for(var/turf/tile as anything in bounty_outpost_public_floor(outpost)) + // Not the weapons checkpoint's lobby or chambers (outpost_checkpoint.dm): they blend in past it + if(istype(tile.loc, /area/voidcrew/trader_outpost/checkpoint)) + continue floor[tile] = TRUE if(!length(floor)) floor = null diff --git a/voidcrew/modules/bounties/bounty_admin.dm b/voidcrew/modules/bounties/bounty_admin.dm index b8bb991cd77..721a17dfc7e 100644 --- a/voidcrew/modules/bounties/bounty_admin.dm +++ b/voidcrew/modules/bounties/bounty_admin.dm @@ -868,7 +868,8 @@ ADMIN_VERB(bounty_panel, R_ADMIN, "Bounty Panel", "Spawn, post, give out, list a var/datum/criminal_bounty/posting = wanted.posting() if(posting && (posting.placement_kind != BOUNTY_PLACEMENT_TRADER_OUTPOST || posting.outpost_site?.resolve() != outpost)) posting = null - var/mob/living/basic/bounty_criminal/decoy/decoy = new(decoy_spot(center)) + var/datum/npc_spawn_context/context = wanted.bounty_profile_context() + var/mob/living/basic/bounty_criminal/decoy/decoy = spawn_npc_in_zone(/mob/living/basic/bounty_criminal/decoy, decoy_spot(center), context, FALSE) decoy.outpost_become(wanted, decoy_record, posting, outpost, bounty_outpost_display_name(outpost), bounty_outpost_display_desc(outpost), bounty_outpost_outfit(outpost)) if(posting) posting.decoys += WEAKREF(decoy) diff --git a/voidcrew/modules/bounties/bounty_ai_normal.dm b/voidcrew/modules/bounties/bounty_ai_normal.dm index 4290542b1ff..70aa025207e 100644 --- a/voidcrew/modules/bounties/bounty_ai_normal.dm +++ b/voidcrew/modules/bounties/bounty_ai_normal.dm @@ -349,6 +349,7 @@ if(HAS_TRAIT(src, TRAIT_BOUNTY_SURRENDERED) || stat == DEAD) return ADD_TRAIT(src, TRAIT_BOUNTY_SURRENDERED, BOUNTY_TRAIT) + body_note_state() ai_stop_aiming() ai_suspend_activity() ai_set_mode(BOUNTY_AI_CALM) diff --git a/voidcrew/modules/bounties/bounty_boss.dm b/voidcrew/modules/bounties/bounty_boss.dm index 2d66ba67a9a..94d389fabdf 100644 --- a/voidcrew/modules/bounties/bounty_boss.dm +++ b/voidcrew/modules/bounties/bounty_boss.dm @@ -191,7 +191,7 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) if(attack_sound) playsound(target, attack_sound, 50, TRUE) target.visible_message(span_danger("[src] [attack_verb_continuous] [target]!")) - boss_hit(target, rand(melee_damage_lower, melee_damage_upper), melee_damage_type, MELEE, 0, armour_penetration) + boss_hit(target, rand(melee_damage_lower, melee_damage_upper), melee_damage_type, MELEE, 0, armour_penetration, TRUE) SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, TRUE) if(!ignore_cooldown) changeNext_move(melee_attack_cooldown) @@ -473,8 +473,11 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) if(!length(table) || !count) return FALSE var/wanted = table[clamp(count, 1, length(table))] + var/raw_health = wanted + wanted *= npc_zone_health_factor if(wanted <= maxHealth) return FALSE + npc_zone_original_max_health = raw_health var/share = maxHealth > 0 ? health / maxHealth : 1 setMaxHealth(wanted) if(boss_tired) @@ -553,7 +556,8 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) * One hit from an ability on `victim`: `damage` of `damage_type` against `armour_flag` armour, and * a knockdown. A mech takes it on its hull (H2). */ -/mob/living/basic/bounty_criminal/boss/proc/boss_hit(atom/victim, damage, damage_type = BRUTE, armour_flag = MELEE, knockdown = 0, armour_penetration = 0) +/mob/living/basic/bounty_criminal/boss/proc/boss_hit(atom/victim, damage, damage_type = BRUTE, armour_flag = MELEE, knockdown = 0, armour_penetration = 0, already_scaled = FALSE) + damage = npc_zone_damage(damage, src, already_scaled) if(ismecha(victim)) var/obj/vehicle/sealed/mecha/mech = victim mech.take_damage(damage, damage_type, armour_flag, TRUE, get_dir(mech, src), armour_penetration) @@ -1160,6 +1164,7 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) var/list/turfs /// The boss whose fire it is, for who it may hurt var/datum/weakref/boss_ref + var/damage_factor = 1 /obj/effect/bounty_boss_fire_pool/Initialize(mapload, list/fire_turfs, lifetime, mob/living/basic/bounty_criminal/boss/boss, landing_damage = 0) . = ..() @@ -1174,6 +1179,7 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) if(!length(turfs)) return INITIALIZE_HINT_QDEL boss_ref = boss ? WEAKREF(boss) : null + damage_factor = boss ? boss.get_npc_zone_damage_factor() : 1 // Invisible, but the light comes from here: sit it where the fire is. var/turf/middle = turfs[round((length(turfs) + 1) / 2)] if(middle != loc) @@ -1205,13 +1211,13 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) continue if(ismecha(thing)) var/obj/vehicle/sealed/mecha/mech = thing - mech.take_damage(max(damage, BOUNTY_BOSS_BURNING_DAMAGE), BURN, FIRE, FALSE) + mech.take_damage(npc_zone_damage(max(damage, BOUNTY_BOSS_BURNING_DAMAGE), damage_factor), BURN, FIRE, FALSE) continue var/mob/living/victim = thing if(damage > 0) - victim.apply_damage(damage, BURN, blocked = victim.run_armor_check(null, FIRE, silent = TRUE), spread_damage = TRUE) + victim.apply_damage(npc_zone_damage(damage, damage_factor), BURN, blocked = victim.run_armor_check(null, FIRE, silent = TRUE), spread_damage = TRUE) to_chat(victim, span_userdanger("You're caught in the flames!")) - victim.apply_status_effect(/datum/status_effect/bounty_boss_burning) + victim.apply_status_effect(/datum/status_effect/bounty_boss_burning, damage_factor) /obj/effect/bounty_boss_fire_pool/proc/burn_out() qdel(src) @@ -1229,6 +1235,15 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) alert_type = null /// The flames on them var/mutable_appearance/flames + var/damage_factor = 1 + +/datum/status_effect/bounty_boss_burning/on_creation(mob/living/new_owner, factor = 1) + damage_factor = factor + return ..() + +/datum/status_effect/bounty_boss_burning/refresh(mob/living/new_owner, factor = 1) + damage_factor = factor + return ..() /datum/status_effect/bounty_boss_burning/on_apply() if(owner.stat != CONSCIOUS || HAS_TRAIT(owner, TRAIT_NOFIRE)) @@ -1241,7 +1256,7 @@ GLOBAL_LIST_EMPTY(bounty_boss_barricades) if(owner.stat != CONSCIOUS) qdel(src) return - owner.apply_damage(BOUNTY_BOSS_BURNING_DAMAGE, BURN, blocked = owner.run_armor_check(null, FIRE, silent = TRUE), spread_damage = TRUE) + owner.apply_damage(npc_zone_damage(BOUNTY_BOSS_BURNING_DAMAGE, damage_factor), BURN, blocked = owner.run_armor_check(null, FIRE, silent = TRUE), spread_damage = TRUE) /datum/status_effect/bounty_boss_burning/on_remove() if(flames) diff --git a/voidcrew/modules/bounties/bounty_boss_kits.dm b/voidcrew/modules/bounties/bounty_boss_kits.dm index 2b70bce6227..22bfcd86bb2 100644 --- a/voidcrew/modules/bounties/bounty_boss_kits.dm +++ b/voidcrew/modules/bounties/bounty_boss_kits.dm @@ -673,6 +673,7 @@ /// Who it catches, and how hard var/radius = BOUNTY_DEMOLITIONIST_GRENADE_RADIUS var/damage = BOUNTY_DEMOLITIONIST_GRENADE_DAMAGE + var/damage_factor = 1 var/knockdown = BOUNTY_DEMOLITIONIST_GRENADE_KNOCKDOWN /// What it breaks, and how hard var/object_radius = BOUNTY_DEMOLITIONIST_GRENADE_OBJECT_RADIUS @@ -688,6 +689,7 @@ return INITIALIZE_HINT_QDEL GLOB.bounty_boss_explosives += src boss_ref = boss ? WEAKREF(boss) : null + damage_factor = boss ? boss.get_npc_zone_damage_factor() : 1 structure_tally = list(BOUNTY_BOSS_ABILITY_STRUCTURE_CAP) ring = new(get_turf(src), fuse, COLOR_RED, "target_circle") ring.transform = matrix() * (radius * 2 + 1) @@ -729,12 +731,12 @@ if(!(isliving(victim) || ismecha(victim)) || !may_hit(victim, boss) || !bounty_boss_clear_line(here, get_turf(victim))) continue if(boss) - boss.boss_hit(victim, damage, BRUTE, BOMB, knockdown) + boss.boss_hit(victim, npc_zone_damage(damage, damage_factor), BRUTE, BOMB, knockdown, already_scaled = TRUE) else if(ismecha(victim)) - victim.take_damage(damage, BRUTE, BOMB, FALSE) + victim.take_damage(npc_zone_damage(damage, damage_factor), BRUTE, BOMB, FALSE) else var/mob/living/person = victim - person.apply_damage(damage, BRUTE, BODY_ZONE_CHEST, person.run_armor_check(BODY_ZONE_CHEST, BOMB, silent = TRUE)) + person.apply_damage(npc_zone_damage(damage, damage_factor), BRUTE, BODY_ZONE_CHEST, person.run_armor_check(BODY_ZONE_CHEST, BOMB, silent = TRUE)) if(knockdown) person.Knockdown(knockdown) qdel(src) @@ -841,7 +843,7 @@ /mob/living/basic/bounty_criminal/boss/ghost/proc/boss_cut(mob/living/victim) if(QDELETED(victim) || victim.stat == DEAD) return - victim.apply_status_effect(/datum/status_effect/bounty_boss_bleed) + victim.apply_status_effect(/datum/status_effect/bounty_boss_bleed, get_npc_zone_damage_factor()) boss_cuts++ if(boss_cuts >= BOUNTY_GHOST_CUTS_BEFORE_BREAKOFF) boss_cuts = 0 @@ -1022,7 +1024,7 @@ boss.boss_hit(victim, BOUNTY_GHOST_DASH_DAMAGE, BRUTE, MELEE, 0, BOUNTY_GHOST_DASH_AP) return var/mob/living/person = victim - person.apply_damage(BOUNTY_GHOST_DASH_DAMAGE, BRUTE, BODY_ZONE_CHEST, person.run_armor_check(BODY_ZONE_CHEST, MELEE, armour_penetration = BOUNTY_GHOST_DASH_AP, silent = TRUE), sharpness = SHARP_EDGED) + person.apply_damage(npc_zone_damage(BOUNTY_GHOST_DASH_DAMAGE, boss), BRUTE, BODY_ZONE_CHEST, person.run_armor_check(BODY_ZONE_CHEST, MELEE, armour_penetration = BOUNTY_GHOST_DASH_AP, silent = TRUE), sharpness = SHARP_EDGED) boss.boss_cut(person) /// Cloak: the air around it shimmers, then it fades to a faint distortion that half of all hits miss @@ -1060,11 +1062,17 @@ alert_type = null /// Cuts bleeding var/cuts = 1 + var/damage_factor = 1 + +/datum/status_effect/bounty_boss_bleed/on_creation(mob/living/new_owner, factor = 1) + damage_factor = factor + return ..() /datum/status_effect/bounty_boss_bleed/on_apply() return owner.stat == CONSCIOUS -/datum/status_effect/bounty_boss_bleed/refresh(effect, ...) +/datum/status_effect/bounty_boss_bleed/refresh(mob/living/new_owner, factor = 1) + damage_factor = factor . = ..() cuts++ @@ -1073,7 +1081,7 @@ qdel(src) return var/rate = min(cuts * BOUNTY_GHOST_BLEED_PER_CUT, BOUNTY_GHOST_BLEED_CAP) - owner.apply_damage(rate, BRUTE, spread_damage = TRUE) + owner.apply_damage(npc_zone_damage(rate, damage_factor), BRUTE, spread_damage = TRUE) owner.bleed(rate) // ===== HEAVY ===== diff --git a/voidcrew/modules/bounties/bounty_criminal.dm b/voidcrew/modules/bounties/bounty_criminal.dm index e4ef0fc148d..e8f95e0702f 100644 --- a/voidcrew/modules/bounties/bounty_criminal.dm +++ b/voidcrew/modules/bounties/bounty_criminal.dm @@ -163,8 +163,11 @@ if(!where) return null var/criminal_type = bounty_criminal_type(record, posting) - var/mob/living/basic/bounty_criminal/criminal = new criminal_type(where) + var/atom/site = posting?.site_ref?.resolve() + var/datum/npc_spawn_context/context = new(site || where, null, "ordinary", source_turf = site ? get_turf(site) : where) + var/mob/living/basic/bounty_criminal/criminal = spawn_npc_in_zone(criminal_type, where, context, FALSE) criminal.body_setup(record, posting) + context.finalize() return criminal /// The criminal type for `record` wanted on `posting`: meek, normal, or the mini-boss of its kit. A mini-boss never goes to a trader outpost; a normal criminal stands in (D-A8). @@ -275,9 +278,24 @@ /mob/living/basic/bounty_criminal/proc/body_match(mob/living/basic/bounty_criminal/other) if(QDELETED(other) || other == src) return - maxHealth = other.maxHealth - setBruteLoss(other.getBruteLoss()) + // Copy raw stats before finalizing the inherited profile, never its scaled pool. + var/datum/npc_spawn_context/context = npc_spawn_context + if(!context || context.source_id != other.npc_spawn_context?.source_id) + context = other.bounty_profile_context() + if(npc_zone_applied) + UnregisterSignal(src, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) + melee_damage_lower = npc_zone_original_melee_lower + melee_damage_upper = npc_zone_original_melee_upper + npc_zone_applied = FALSE + npc_zone_health_factor = 1 + npc_zone_damage_factor = 1 + npc_spawn_context = context + maxHealth = other.npc_zone_applied ? other.npc_zone_original_max_health : other.maxHealth + var/health_share = other.maxHealth > 0 ? other.health / other.maxHealth : 1 + setBruteLoss(maxHealth * (1 - health_share)) updatehealth() + context.bodies |= WEAKREF(src) + context.finalize() set_varspeed(other.speed) damage_coeff[STAMINA] = other.damage_coeff[STAMINA] stamina_regen_time = other.stamina_regen_time @@ -287,6 +305,11 @@ max_stamina_slowdown = other.max_stamina_slowdown make_stamina_slowable() +/// Decoys use the source's selected role before construction, including named boss bodies. +/mob/living/basic/bounty_criminal/proc/bounty_profile_context() + var/role = npc_zone_role || (npc_zone_boss_type(type) ? "boss" : npc_spawn_context?.role || "ordinary") + return new /datum/npc_spawn_context(src, null, role) + // ===== THE SITE ===== /** @@ -570,6 +593,8 @@ /// Records its capture state in worst_state if it is as bad or worse: once downed or dead, it never gets better (spec 12.1) /mob/living/basic/bounty_criminal/proc/body_note_state() var/state = capture_state() + if(state != BOUNTY_STATE_FREE) + posting()?.board_note_work_zone(src) if(bounty_state_rank(state) <= bounty_state_rank(worst_state)) worst_state = state diff --git a/voidcrew/modules/bounties/bounty_kingpin.dm b/voidcrew/modules/bounties/bounty_kingpin.dm index 75378fd52dc..29ab4fe6b61 100644 --- a/voidcrew/modules/bounties/bounty_kingpin.dm +++ b/voidcrew/modules/bounties/bounty_kingpin.dm @@ -715,6 +715,7 @@ GLOBAL_LIST_EMPTY(bounty_kingpin_marks) return kingpin_cancel_aim() ADD_TRAIT(src, TRAIT_BOUNTY_SURRENDERED, BOUNTY_TRAIT) + body_note_state() visible_message(span_notice("[src] puts [p_their()] hands up."), vision_distance = BOUNTY_KINGPIN_SIGHT) INVOKE_ASYNC(src, TYPE_PROC_REF(/mob/living, bounty_say), "surrender", null, TRUE) @@ -1236,7 +1237,9 @@ GLOBAL_LIST_EMPTY(bounty_kingpin_marks) /datum/bounty_kingpin_crew/proc/spawn_goon(turf/post, weapon = BOUNTY_GOON_WEAPON_PISTOL) if(!post) return null - var/mob/living/basic/bounty_kingpin_goon/goon = new(post) + var/mob/living/basic/bounty_criminal/kingpin/kingpin = kingpin_ref?.resolve() + var/datum/npc_spawn_context/context = new(kingpin || post, null, "ordinary", source_turf = post) + var/mob/living/basic/bounty_kingpin_goon/goon = spawn_npc_in_zone(/mob/living/basic/bounty_kingpin_goon, post, context, FALSE) goon.goon_weapon = weapon goon.goon_post = post goon.goon_rounds = bounty_kingpin_goon_mag(weapon) @@ -1255,6 +1258,7 @@ GLOBAL_LIST_EMPTY(bounty_kingpin_marks) goon.goon_post_dir = facing || SOUTH goon.setDir(goon.goon_post_dir) add_goon(goon) + context.finalize() return goon /// Makes `goon` one of the crew @@ -2469,13 +2473,15 @@ GLOBAL_LIST_EMPTY(bounty_kingpin_marks) /proc/bounty_kingpin_setup(turf/seat, list/posts, datum/bounty_record/record) if(!seat) return null - var/mob/living/basic/bounty_criminal/kingpin/kingpin = new(seat) + var/datum/npc_spawn_context/context = new(seat, null, "boss") + var/mob/living/basic/bounty_criminal/kingpin/kingpin = spawn_npc_in_zone(/mob/living/basic/bounty_criminal/kingpin, seat, context, FALSE) kingpin.kingpin_crew = new /datum/bounty_kingpin_crew(kingpin, seat) kingpin.body_setup(record, null) var/obj/structure/overmap/trader_outpost/outpost = get_trader_outpost_for_turf(seat) if(outpost) kingpin.body_set_site(outpost) kingpin.kingpin_sit() + context.finalize() for(var/list/post as anything in posts) kingpin.kingpin_crew.crew_posts += list(post.Copy()) kingpin.kingpin_crew.spawn_goon(post[1], post[2]) @@ -2934,3 +2940,7 @@ SUBSYSTEM_DEF(bounty_kingpin) if(!posting) return panel.refuse(user, "The kingpin couldn't be posted.") return "post the kingpin bounty on [posting.record?.name || "nobody"] at [posting.board_site_name || "the black market"]" + +/// His lounge faction stays neutral; his explicit boss encounter is still profiled. +/mob/living/basic/bounty_criminal/kingpin/is_npc_zone_exempt() + return npc_zone_exempt || npc_spawn_context?.exempt || client || mind diff --git a/voidcrew/modules/bounties/bounty_lair.dm b/voidcrew/modules/bounties/bounty_lair.dm index 35e63a6382f..8feb24f1dfc 100644 --- a/voidcrew/modules/bounties/bounty_lair.dm +++ b/voidcrew/modules/bounties/bounty_lair.dm @@ -77,6 +77,12 @@ /datum/criminal_bounty/kill_only/board_arm() return FALSE +/datum/criminal_bounty/kill_only/board_reputation_family() + return ZONE_FAMILY_LAIR + +/datum/criminal_bounty/kill_only/board_reputation_difficulty() + return MISSION_DIFFICULTY_HARD + /// Its site is fixed, so a lost site or a lost trophy ends it instead of moving it /datum/criminal_bounty/kill_only/relist(reason) if(!is_open()) @@ -185,6 +191,7 @@ /datum/criminal_bounty/kill_only/proc/kill_publish(obj/structure/overmap/site, zone, vouchers, lifetime) placement_kind = SScriminal_bounties.board_kind_of_site(site) || BOUNTY_PLACEMENT_RUIN board_zone = zone + record.board_work_zone = zone value = SScriminal_bounties.board_value_for(record.base_value, zone) board_vouchers = vouchers board_posted_at = world.time @@ -302,6 +309,13 @@ lair_kind = null return ..() +/// The existing atomic claim closes the bounty before returning its successful pay. +/datum/criminal_bounty/kill_only/lair/board_claim(obj/structure/overmap/ship/ship, obj/machinery/mission_pad/pad, atom/movable/target, state, mob/user) + var/prize_zone = board_zone + . = ..() + if(islist(.) && !QDELETED(ship)) + ship.award_guarded_prize("lair", prize_zone, pad, type) + /// Its clock also waits while anyone is inside the lair: a crew deep in it never loses the bounty to the clock /datum/criminal_bounty/kill_only/lair/board_clock_held(now = world.time) if(..()) @@ -538,7 +552,10 @@ if(!ispath(boss_type, /mob/living)) log_mapping("BOUNTY LAIR: [name] has a boss landmark with no mob type at [AREACOORD(home)]") continue - lair_adopt_boss(new boss_type(home)) + var/datum/npc_spawn_context/context = new(src, null, "boss") + var/mob/living/boss = spawn_npc_in_zone(boss_type, home, context, FALSE) + lair_adopt_boss(boss) + context.finalize() for(var/obj/effect/landmark/bounty_lair_gatekeeper/mark as anything in keeper_marks) if(QDELETED(mark)) @@ -551,7 +568,10 @@ if(!ispath(keeper_type, /mob/living)) log_mapping("BOUNTY LAIR: [name] has a gatekeeper landmark with no mob type at [AREACOORD(home)]") continue - lair_add_gatekeeper(new keeper_type(home)) + var/datum/npc_spawn_context/context = new(src, null, "boss") + var/mob/living/keeper = spawn_npc_in_zone(keeper_type, home, context, FALSE) + lair_add_gatekeeper(keeper) + context.finalize() lair_leash_goons() lair_goon_timer = addtimer(CALLBACK(src, PROC_REF(lair_leash_goons)), BOUNTY_LAIR_GOON_LEASH_DELAY, TIMER_STOPPABLE) diff --git a/voidcrew/modules/bounties/bounty_lair_mafia.dm b/voidcrew/modules/bounties/bounty_lair_mafia.dm index 3c2913c6004..796e4eb45f9 100644 --- a/voidcrew/modules/bounties/bounty_lair_mafia.dm +++ b/voidcrew/modules/bounties/bounty_lair_mafia.dm @@ -573,7 +573,7 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) for(var/mob/living/pilot in suit.occupants) if(rocket_volley) rocket_volley[REF(pilot)] = TRUE - return suit.take_damage(BOUNTY_MECH_ROCKET_EXOSUIT_DAMAGE, BRUTE, BULLET, TRUE, get_dir(suit, src)) + return suit.take_damage(npc_zone_damage(BOUNTY_MECH_ROCKET_EXOSUIT_DAMAGE, npc_zone_damage_factor), BRUTE, BULLET, TRUE, get_dir(suit, src)) /obj/projectile/bullet/bounty_mafia_rocket/on_hit(atom/target, blocked = 0, pierce_hit) . = ..() @@ -614,7 +614,7 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) if(rocket_volley) rocket_volley[key] = TRUE var/blocked = victim.run_armor_check(BODY_ZONE_CHEST, BULLET, silent = TRUE) - victim.apply_damage(damage, BRUTE, BODY_ZONE_CHEST, blocked) + victim.apply_damage(npc_zone_damage(damage, npc_zone_damage_factor, npc_zone_damage_scaled), BRUTE, BODY_ZONE_CHEST, blocked) victim.Knockdown(BOUNTY_MECH_ROCKET_KNOCKDOWN) to_chat(victim, span_userdanger("A rocket bursts right at your feet!")) . += victim @@ -1112,14 +1112,11 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) /** * The club's goons. On P11's map every room has one of these; its room (the marker's area) fixes the crew - * (bounty_mafia_room_crew()). Anywhere else it rolls 3-5 from the zone table like any other theme. Goons + * (bounty_mafia_room_crew()). Each marker rolls 1-2 from its room's weighted roster. Goons * only spawn on tiles of the marker's own room. */ /obj/effect/zone_mobs/mafia name = "zone mob spawner (mafia)" - count_green = list(3, 4) - count_yellow = list(3, 5) - count_red = list(4, 5) scatter_radius = 3 mobs_green = list( /mob/living/basic/trooper/russian/mafia = 4, @@ -1143,10 +1140,11 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) if(!crew) return ..() var/list/open_turfs = get_scatter_turfs() - for(var/mob_path in crew) - for(var/i in 1 to crew[mob_path]) - var/turf/spot = length(open_turfs) ? pick_n_take(open_turfs) : get_turf(src) - new mob_path(spot) + for(var/i in 1 to rand(1, 2)) + var/mob_path = pick_weight(crew) + var/turf/spot = length(open_turfs) ? pick_n_take(open_turfs) : get_turf(src) + var/datum/npc_spawn_context/context = new(src, zone_type, "ordinary") + spawn_npc_in_zone(mob_path, spot, context) /obj/effect/zone_mobs/mafia/get_scatter_turfs() var/area/room = get_area(src) @@ -1516,8 +1514,11 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) return FALSE var/list/table = mech_health_table() var/wanted = table[clamp(count, 1, length(table))] + var/raw_health = wanted + wanted *= npc_zone_health_factor if(wanted <= maxHealth) return FALSE + npc_zone_original_max_health = raw_health setMaxHealth(wanted) updatehealth() if(announce && count > 1) @@ -1595,8 +1596,7 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) mech_engaged = list() mech_grudge = list() adjust_health(-getBruteLoss()) - setMaxHealth(BOUNTY_MECH_HEALTH_1) - updatehealth() + set_npc_zone_health(BOUNTY_MECH_HEALTH_1) ai_controller?.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) visible_message(span_notice("[src] vents steam as its repair arms fold away. It looks as good as new.")) @@ -1877,7 +1877,8 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) var/obj/structure/bounty_mafia_wreck/wreck = new(spot) wreck.setDir(dir) var/turf/exit = mech_exit_turf(spot) - var/mob/living/basic/bounty_lair_boss/mafia_don/don = new(exit || spot) + var/datum/npc_spawn_context/context = new(src, null, "boss") + var/mob/living/basic/bounty_lair_boss/mafia_don/don = spawn_npc_in_zone(/mob/living/basic/bounty_lair_boss/mafia_don, exit || spot, context, FALSE) don.posting_ref = posting_ref don.don_wreck_ref = WEAKREF(wreck) // Whoever broke his machine, he knows @@ -1891,6 +1892,7 @@ GLOBAL_LIST_EMPTY(bounty_mafia_rooms) don.real_name = posting.record.name SEND_SIGNAL(src, COMSIG_BOUNTY_MAFIA_DON_EJECTED, don) don.don_on_eject() + context.finalize() return don /// The open tile beside `wreck_spot`, in its arena, farthest from the hunters diff --git a/voidcrew/modules/bounties/bounty_outpost.dm b/voidcrew/modules/bounties/bounty_outpost.dm index 0f7f8ccb82b..0773a31fac1 100644 --- a/voidcrew/modules/bounties/bounty_outpost.dm +++ b/voidcrew/modules/bounties/bounty_outpost.dm @@ -207,7 +207,8 @@ GLOBAL_LIST_INIT(bounty_outpost_trader_react_lines, list( // If a line ever names a crime, it names the same one for everybody if(decoy_record) decoy_record.crime = record.crime - var/mob/living/basic/bounty_criminal/decoy/decoy = new(spot) + var/datum/npc_spawn_context/context = fugitive.bounty_profile_context() + var/mob/living/basic/bounty_criminal/decoy/decoy = spawn_npc_in_zone(/mob/living/basic/bounty_criminal/decoy, spot, context, FALSE) var/atom/anchor = (length(free_stools) && prob(50)) ? pick_n_take(free_stools) : null decoy.outpost_become(fugitive, decoy_record, src, outpost, display_name, display_desc, outfit, anchor) decoys += WEAKREF(decoy) @@ -330,6 +331,10 @@ GLOBAL_LIST_INIT(bounty_outpost_trader_react_lines, list( return list() var/list/floor = bounty_outpost_public_floor(outpost) floor -= outpost.lobby_alcove_turfs + // Nobody blends in at the checkpoint (outpost_checkpoint.dm): the hunt is past it + for(var/turf/checkpoint_tile as anything in floor.Copy()) + if(istype(checkpoint_tile.loc, /area/voidcrew/trader_outpost/checkpoint)) + floor -= checkpoint_tile var/turf/near_turf = get_turf(near) if(near_turf) floor -= near_turf diff --git a/voidcrew/modules/bounties/bounty_posting.dm b/voidcrew/modules/bounties/bounty_posting.dm index 5b3ebf319cd..8876ae3f231 100644 --- a/voidcrew/modules/bounties/bounty_posting.dm +++ b/voidcrew/modules/bounties/bounty_posting.dm @@ -414,6 +414,8 @@ site_ref = null // Whoever turns up at the next site is a fresh sighting board_worst_state = BOUNTY_STATE_FREE + if(record) + record.board_work_zone = null for(var/obj/structure/overmap/ship/hunter as anything in hunter_ships()) hunter.remove_waypoint(board_waypoint_key()) hunter.ship_notify("WANTED: [record?.name] [reason || "slipped away"].","MISSION CONTROL", SHIP_NOTIFY_WARNING, 'voidcrew/sound/notify2.ogg', 50) diff --git a/voidcrew/modules/bounties/bounty_turn_in.dm b/voidcrew/modules/bounties/bounty_turn_in.dm index 9f6dd6c2e9a..3fcbfe55af8 100644 --- a/voidcrew/modules/bounties/bounty_turn_in.dm +++ b/voidcrew/modules/bounties/bounty_turn_in.dm @@ -35,6 +35,42 @@ // ===== TURNING IN ===== +/// Bind field work before the criminal or its proof can be carried to another band. +/datum/criminal_bounty/proc/board_note_work_zone(mob/living/basic/bounty_criminal/target) + if(!is_open() || !record || record.board_work_zone || target != criminal() || record.status != BOUNTY_RECORD_WANTED) + return + var/band = get_zone_type_for_player_turf(get_turf(target)) + if(band in list(ZONE_GREEN, ZONE_YELLOW, ZONE_RED)) + record.board_work_zone = band + +/datum/criminal_bounty/proc/board_reputation_family() + return ZONE_FAMILY_CRIMINAL_BOUNTY + +/datum/criminal_bounty/proc/board_reputation_difficulty() + if(record?.archetype == BOUNTY_ARCHETYPE_BOSS) + return MISSION_DIFFICULTY_HARD + switch(record?.tier) + if(BOUNTY_TIER_PETTY) + return MISSION_DIFFICULTY_EASY + if(BOUNTY_TIER_WANTED) + return MISSION_DIFFICULTY_MEDIUM + if(BOUNTY_TIER_MOST_WANTED) + return MISSION_DIFFICULTY_HARD + return null + +/// Called only by the successful paying claim, before detachment, closing signals or rewards. +/datum/criminal_bounty/proc/board_claim_zone_work(obj/structure/overmap/ship/ship) + if(!record || record.board_reputation_claimed || record.status != BOUNTY_RECORD_WANTED \ + || !(record.board_work_zone in list(ZONE_GREEN, ZONE_YELLOW, ZONE_RED))) + return 0 + var/difficulty = board_reputation_difficulty() + var/datum/voidcrew_crew_record/crew = ship.get_crew_record() + if(!difficulty || QDELETED(crew)) + return 0 + var/family = board_reputation_family() + record.board_reputation_claimed = TRUE + return crew.claim_zone_work(family, record.board_work_zone, difficulty, "bounty:[record.id]:[family]") + /** * `ship` turns this bounty in at `pad`: the criminal (or its proof of death) on the pad's own tile, * the pad bolted and aboard. Returns list(credits, vouchers) paid, or why not (a short phrase). @@ -127,6 +163,8 @@ // Closing lets go of the record and the hunters, so everything read from them is read now var/poster_text = (live && share >= 100 && claimed?.tier >= BOUNTY_TIER_WANTED) ? board_poster_text(ship) : null var/list/already_told = most_wanted ? hunter_ships() : null + if(pay[1] > 0 || pay[2] > 0) + board_claim_zone_work(ship) // Take it: the criminal is frozen for the beam and stops being this bounty's, so closing the bounty doesn't remove it if(criminal) diff --git a/voidcrew/modules/bounties/bounty_types.dm b/voidcrew/modules/bounties/bounty_types.dm index f98ca75c327..6af790269ea 100644 --- a/voidcrew/modules/bounties/bounty_types.dm +++ b/voidcrew/modules/bounties/bounty_types.dm @@ -86,6 +86,10 @@ GLOBAL_LIST_EMPTY(bounty_prisoner_pool) var/status = BOUNTY_RECORD_WANTED /// world.time the record was made var/created_at = 0 + /// First capture/death band (or fixed lair band), retained across body/proof changes. + var/board_work_zone + /// Shared by every posting/body alias, including after a prison escape. + var/board_reputation_claimed = FALSE /// Their mugshot, a base64 PNG built once and cached here (bounty_record_mugshot()) var/mugshot /// How they looked when the mugshot was taken, if that differs from how they look now (a trader-outpost fugitive has changed their hair or clothes). P1 fills it (make_old_look()); the mugshot uses it when set. diff --git a/voidcrew/modules/bounties/bounty_weapons.dm b/voidcrew/modules/bounties/bounty_weapons.dm index b173c94c03b..d4f4fac0902 100644 --- a/voidcrew/modules/bounties/bounty_weapons.dm +++ b/voidcrew/modules/bounties/bounty_weapons.dm @@ -192,8 +192,7 @@ GLOBAL_LIST_EMPTY(bounty_real_weapons) /// Puts its hand numbers on `fighter`'s own blow. The blow's timing stays the fighter's. /datum/bounty_real_weapon/proc/apply_melee(mob/living/basic/fighter) read_melee() - fighter.melee_damage_lower = force_low - fighter.melee_damage_upper = force_high + fighter.set_npc_zone_melee(force_low, force_high) fighter.melee_damage_type = damage_type fighter.sharpness = sharpness fighter.wound_bonus = wound_bonus diff --git a/voidcrew/modules/captain_management/captain_management.dm b/voidcrew/modules/captain_management/captain_management.dm index 3d1aa301718..98032b29c07 100644 --- a/voidcrew/modules/captain_management/captain_management.dm +++ b/voidcrew/modules/captain_management/captain_management.dm @@ -135,6 +135,8 @@ data["is_captain"] = ship.is_ship_captain(captain) // Crew list + var/datum/mind/crew_leader = ship.get_crew_record()?.leader + data["crew_leader"] = crew_leader?.current?.real_name || crew_leader?.name || "None" data["crew"] = list() if(ship.ship_team) for(var/datum/mind/member in ship.ship_team.members) @@ -353,21 +355,19 @@ if(!ship || QDELETED(ship)) to_chat(player, span_warning("The ship no longer exists!")) return + return accept_crew_invite(player) - // Check if player is already on a ship team - remove them first - for(var/datum/team/voidcrew/old_team in player.mind.ship_teams) - if(old_team != ship.ship_team) - old_team.remove_member(player.mind) - - // Add player to ship team - if(ship.ship_team) - ship.ship_team.add_member(player.mind) - ship.manifest += player.real_name +/// Final invitation transaction, revalidated after the player's prompt. +/datum/captain_management_ui/proc/accept_crew_invite(mob/living/player) + if(QDELETED(ship) || !ship.is_ship_captain(captain) || !player?.mind) + return FALSE + if(!ship.enlist_crewmember(player)) + return FALSE // Remember the captain's invitation across respawns until the password changes // or the captain resets join access. - if(ckey) - ship.password_cleared_ckeys[ckey] = TRUE + if(player.ckey) + ship.password_cleared_ckeys[player.ckey] = TRUE // Humans can pick up a ship headset. A silicon's radio is built in, so point it at // this ship's channel the way a silicon crew spawn does. @@ -382,6 +382,7 @@ to_chat(player, span_notice("You have joined the crew of [ship.name]!")) ship.ship_notify("[player.real_name] has joined the crew.", "CREW UPDATE", SHIP_NOTIFY_NOTICE, 'voidcrew/sound/notify.ogg', 50) log_game("[key_name(captain)] invited [key_name(player)] to ship [ship.name]") + return TRUE // ===== CREW MANAGEMENT ===== @@ -395,7 +396,7 @@ // Remove from ship team if(ship.ship_team) - ship.ship_team.remove_member(target) + target.leave_reputation_crew() // Remove from manifest ship.manifest -= kicked_name diff --git a/voidcrew/modules/cargo/anomaly_packs.dm b/voidcrew/modules/cargo/anomaly_packs.dm new file mode 100644 index 00000000000..86a28ed4be7 --- /dev/null +++ b/voidcrew/modules/cargo/anomaly_packs.dm @@ -0,0 +1,32 @@ +/** + * Raw anomaly cores are off the cargo catalogue. + * + * The anomaly core cap is lifted in this fork (voidcrew/modules/research/edits/ + * _experiments.dm), so a 2000 cr crate per core made them a routine order. + * Boffin at the Quartermain Depot is now the only store that sells raw cores, + * on a small rotating shelf, with anomaly charts as the other way to hunt one + * (shop_catalog_outfitter_vendors.dm, anomaly_charts.dm). + * + * SSshuttle only registers packs with a non-null `contains`. Overriding the var + * to null does not work for these: upstream sets `contains = list(...)`, and a + * list default is built at New() time, after the null has been applied, so the + * pack comes back with its contents. Clearing it in New() drops each crate from + * every console without touching the upstream definitions. + */ +GLOBAL_LIST_INIT(voidcrew_removed_supply_packs, typecacheof(list( + /datum/supply_pack/science/raw_flux_anomaly, + /datum/supply_pack/science/raw_hallucination_anomaly, + /datum/supply_pack/science/raw_grav_anomaly, + /datum/supply_pack/science/raw_vortex_anomaly, + /datum/supply_pack/science/raw_ectoplasm_anomaly, + /datum/supply_pack/science/raw_bluespace_anomaly, + /datum/supply_pack/science/raw_pyro_anomaly, + /datum/supply_pack/science/raw_bioscrambler_anomaly, + /datum/supply_pack/science/raw_dimensional_anomaly, +))) + +// Chains onto the upstream New. +/datum/supply_pack/New() + . = ..() + if(GLOB.voidcrew_removed_supply_packs[type]) + contains = null diff --git a/voidcrew/modules/cargo/bank_machine.dm b/voidcrew/modules/cargo/bank_machine.dm index 0022d5d9edd..176b205208f 100644 --- a/voidcrew/modules/cargo/bank_machine.dm +++ b/voidcrew/modules/cargo/bank_machine.dm @@ -84,7 +84,7 @@ balloon_alert(user, "no ship account connected") return FALSE // Join-password clearance also covers former lives; banking requires the current mind. - if(!user.mind || !(user.mind in ship.ship_team?.members)) + if(!ship.is_ship_crew(user)) balloon_alert(user, "ship crew only") return FALSE if(card.registered_account == synced_bank_account) diff --git a/voidcrew/modules/cargo/exports.dm b/voidcrew/modules/cargo/exports.dm index 5f93b893cf9..08954e677aa 100644 --- a/voidcrew/modules/cargo/exports.dm +++ b/voidcrew/modules/cargo/exports.dm @@ -93,7 +93,7 @@ // ========== NANITE TECHNOLOGY ========== /datum/export/nanite_disk - cost = 200 + cost = 0 unit_name = "nanite program disk" export_types = list(/obj/item/disk/nanite_program) diff --git a/voidcrew/modules/cargo/shipping/_shuttle_loan.dm b/voidcrew/modules/cargo/shipping/_shuttle_loan.dm index 408ab0144ae..4c65362bace 100644 --- a/voidcrew/modules/cargo/shipping/_shuttle_loan.dm +++ b/voidcrew/modules/cargo/shipping/_shuttle_loan.dm @@ -194,12 +194,30 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( var/thanks_msg = "Your shuttle will return with our cargo. Payment has been transferred." /// Description for logging var/logging_desc = "generic loan" + /// One captured host band for all hostile cargo in this delivery. + var/datum/npc_spawn_context/npc_source /// Spawns items on the shuttle when it arrives /datum/voidcrew_shuttle_loan/proc/spawn_items(datum/voidcrew_cargo_shuttle/shuttle) SHOULD_CALL_PARENT(FALSE) CRASH("Unimplemented spawn_items() on [src.type].") +/// Loan deliveries use packs without passing through the order catalog. +/datum/voidcrew_shuttle_loan/proc/spawn_supply_pack(datum/supply_pack/pack, turf/destination) + var/obj/structure/closet/crate/delivery = pack.generate(destination) + for(var/obj/item/item in delivery.get_all_contents()) + if(is_ladder_restricted_production(item.type)) + qdel(item) + return delivery + +/// The cargo bay can be off-map; its host ship supplies the encounter band. +/datum/voidcrew_shuttle_loan/proc/spawn_hostile(mob_type, turf/location, datum/voidcrew_cargo_shuttle/shuttle) + if(!npc_source) + var/turf/source_turf = get_turf(shuttle.target_ship) + npc_source = new(shuttle, SSovermap_zones.get_zone_type_anywhere(source_turf) || ZONE_GREEN, source_turf = source_turf) + var/datum/npc_spawn_context/context = new(shuttle, npc_source.zone_type, "ordinary", source_turf = npc_source.source_turf) + return spawn_npc_in_zone(mob_type, location, context) + /// Department resupply - free supplies, no bonus /datum/voidcrew_shuttle_loan/department_resupply sender = "Nanotrasen Supply Division" @@ -221,7 +239,7 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( for(var/crate_type in crate_types) var/datum/supply_pack/pack = SSshuttle.supply_packs[crate_type] if(pack && length(cargo_turfs)) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) /// Salvage recovery - mixed loot and danger /datum/voidcrew_shuttle_loan/salvage_recovery @@ -343,7 +361,7 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( // Medical supplies var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/medical/supplies] if(pack) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) var/list/extra_supplies = list( /obj/item/storage/medkit/regular, @@ -425,19 +443,19 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( // Spawn special ops crate var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/imports/specialops] if(pack && length(cargo_turfs)) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) // Spawn syndicate infiltrators for(var/i in 1 to 2) if(!length(cargo_turfs)) break - new /mob/living/basic/trooper/syndicate/ranged/infiltrator(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/trooper/syndicate/ranged/infiltrator, pick_n_take(cargo_turfs), shuttle) // Chance for more if(prob(75) && length(cargo_turfs)) - new /mob/living/basic/trooper/syndicate/ranged/infiltrator(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/trooper/syndicate/ranged/infiltrator, pick_n_take(cargo_turfs), shuttle) if(prob(50) && length(cargo_turfs)) - new /mob/living/basic/trooper/syndicate/ranged/infiltrator(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/trooper/syndicate/ranged/infiltrator, pick_n_take(cargo_turfs), shuttle) /// Lots of bees - Toxic bees and beekeeping equipment /datum/voidcrew_shuttle_loan/lots_of_bees @@ -456,7 +474,7 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( // Beekeeping crate var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/organic/hydroponics/beekeeping_fullkit] if(pack && length(cargo_turfs)) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) // Corpses from the incident var/list/corpse_types = list( @@ -470,12 +488,6 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( break new corpse_type(pick_n_take(cargo_turfs)) - // Weapons from the fight - if(length(cargo_turfs)) - new /obj/item/gun/ballistic/automatic/pistol/no_mag(pick_n_take(cargo_turfs)) - if(length(cargo_turfs)) - new /obj/item/gun/ballistic/automatic/pistol/m1911/no_mag(pick_n_take(cargo_turfs)) - // Beekeeping stuff for(var/i in 1 to 3) if(!length(cargo_turfs)) @@ -492,7 +504,7 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( for(var/i in 1 to 8) if(!length(cargo_turfs)) break - new /mob/living/basic/bee/toxin(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/bee/toxin, pick_n_take(cargo_turfs), shuttle) // Blood and bug guts for(var/i in 1 to 5) @@ -564,21 +576,21 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( // Party supplies var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/service/party] if(pack && length(cargo_turfs)) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) // The Russians if(length(cargo_turfs)) - new /mob/living/basic/trooper/russian(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/trooper/russian, pick_n_take(cargo_turfs), shuttle) if(length(cargo_turfs)) - new /mob/living/basic/trooper/russian/ranged(pick_n_take(cargo_turfs)) // This one drops a mateba + spawn_hostile(/mob/living/basic/trooper/russian/ranged, pick_n_take(cargo_turfs), shuttle) // This one drops a mateba if(length(cargo_turfs)) - new /mob/living/basic/bear/russian(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/bear/russian, pick_n_take(cargo_turfs), shuttle) // Chance for more if(prob(75) && length(cargo_turfs)) - new /mob/living/basic/trooper/russian(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/trooper/russian, pick_n_take(cargo_turfs), shuttle) if(prob(50) && length(cargo_turfs)) - new /mob/living/basic/bear/russian(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/bear/russian, pick_n_take(cargo_turfs), shuttle) /// Spider gift - Giant spiders from the Spider Clan /datum/voidcrew_shuttle_loan/spider_gift @@ -597,17 +609,17 @@ GLOBAL_LIST_INIT(voidcrew_shuttle_loans, list( // Special ops crate (the "gift") var/datum/supply_pack/pack = SSshuttle.supply_packs[/datum/supply_pack/imports/specialops] if(pack && length(cargo_turfs)) - pack.generate(pick_n_take(cargo_turfs)) + spawn_supply_pack(pack, pick_n_take(cargo_turfs)) // THE SPIDERS if(length(cargo_turfs)) - new /mob/living/basic/spider/giant(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/spider/giant, pick_n_take(cargo_turfs), shuttle) if(length(cargo_turfs)) - new /mob/living/basic/spider/giant(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/spider/giant, pick_n_take(cargo_turfs), shuttle) if(length(cargo_turfs)) - new /mob/living/basic/spider/giant/nurse(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/spider/giant/nurse, pick_n_take(cargo_turfs), shuttle) if(prob(50) && length(cargo_turfs)) - new /mob/living/basic/spider/giant/hunter(pick_n_take(cargo_turfs)) + spawn_hostile(/mob/living/basic/spider/giant/hunter, pick_n_take(cargo_turfs), shuttle) // Victim remains if(length(cargo_turfs)) diff --git a/voidcrew/modules/cargo/shipping/cargo_console.dm b/voidcrew/modules/cargo/shipping/cargo_console.dm index a0ad90a3db6..88421e64c5c 100644 --- a/voidcrew/modules/cargo/shipping/cargo_console.dm +++ b/voidcrew/modules/cargo/shipping/cargo_console.dm @@ -209,7 +209,7 @@ data["supplies"] = list() for(var/pack_id in SSshuttle.supply_packs) var/datum/supply_pack/pack = SSshuttle.supply_packs[pack_id] - if(!data["supplies"][pack.group]) + if(can_order_pack(pack) && !data["supplies"][pack.group]) data["supplies"][pack.group] = list( "name" = pack.group, "packs" = get_packs_data(pack.group), @@ -223,7 +223,28 @@ /obj/machinery/computer/voidcrew_cargo/proc/can_order_pack(datum/supply_pack/pack) if(!pack) return FALSE - if((pack.hidden && !(obj_flags & EMAGGED)) || (pack.special && !pack.special_enabled) || pack.drop_pod_only) + var/static/list/forbidden_packs = typecacheof(list( + /datum/supply_pack/security, + /datum/supply_pack/goody/mars_single, + /datum/supply_pack/goody/ballistic_single, + /datum/supply_pack/goody/disabler_single, + /datum/supply_pack/goody/energy_single, + /datum/supply_pack/goody/laser_single, + /datum/supply_pack/goody/thermal_single, + /datum/supply_pack/goody/double_barrel, + /datum/supply_pack/goody/hell_single, + /datum/supply_pack/imports/mafia, + /datum/supply_pack/imports/abandoned, + /datum/supply_pack/imports/russian, + )) + if(is_type_in_typecache(pack, forbidden_packs)) + return FALSE + for(var/item_type in pack.contains) + if(is_ladder_restricted_production(item_type)) + return FALSE + // The materials market files hidden custom packs into this same cart. + var/material_order = istype(pack, /datum/supply_pack/custom/minerals) && valid_material_order_contents(pack.contains) + if((pack.hidden && !material_order && !(obj_flags & EMAGGED)) || (pack.special && !pack.special_enabled) || pack.drop_pod_only) return FALSE return !pack.contraband || contraband @@ -511,6 +532,12 @@ say("Error: No orders in cart.") return TRUE + // Outpost freight reserves payment during dispatch, before buy(). + for(var/datum/supply_order/order as anything in checkout_list) + if(!can_order_pack(order.pack)) + say("Order unavailable.") + return TRUE + // Check if we have enough credits for the order var/total_cost = get_cart_total() // A siphon on the account freezes ordering: shipping the balance out diff --git a/voidcrew/modules/cargo/shipping/cargo_shuttle.dm b/voidcrew/modules/cargo/shipping/cargo_shuttle.dm index 6e775ef6352..3be0a71505d 100644 --- a/voidcrew/modules/cargo/shipping/cargo_shuttle.dm +++ b/voidcrew/modules/cargo/shipping/cargo_shuttle.dm @@ -84,18 +84,24 @@ * Returns TRUE if accepted successfully */ /datum/voidcrew_cargo_shuttle/proc/accept_loan() - if(!pending_loan) + if(!pending_loan || loan_accepted) return FALSE if(state != CARGO_SHUTTLE_AWAY) return FALSE + var/datum/bank_account/receiving_account = linked_console?.bank_account_holder?.synced_bank_account + if(pending_loan.bonus_credits > 0 && QDELETED(receiving_account)) + return FALSE + loan_accepted = TRUE // Add bonus credits to the linked bank account - if(linked_console?.bank_account_holder?.synced_bank_account && pending_loan.bonus_credits > 0) - linked_console.bank_account_holder.synced_bank_account.adjust_money(pending_loan.bonus_credits) + if(pending_loan.bonus_credits > 0) + if(!receiving_account.adjust_money(pending_loan.bonus_credits)) + loan_accepted = FALSE + return FALSE record_transaction("loan", pending_loan.logging_desc, 1, pending_loan.bonus_credits) - metric_cargo_loan(pending_loan, linked_console.bank_account_holder.synced_bank_account, usr) + metric_cargo_loan(pending_loan, receiving_account, usr) // Announce acceptance if(target_ship) diff --git a/voidcrew/modules/cargo/shipping/purchasing.dm b/voidcrew/modules/cargo/shipping/purchasing.dm index 71696dc0e6f..2c2ed7c1ac3 100644 --- a/voidcrew/modules/cargo/shipping/purchasing.dm +++ b/voidcrew/modules/cargo/shipping/purchasing.dm @@ -31,6 +31,8 @@ // Iterate a copy: paid orders leave checkout_list inside the loop. for(var/datum/supply_order/spawning_order as anything in checkout_list.Copy()) + if(!can_order_pack(spawning_order.pack)) + continue // The cart quote is also the debit. Material stock is checked in full before // payment and consumed only after payment, never partially at a pooled price. if(!spawning_order.settle_ship_order(bank_account_holder.synced_bank_account)) diff --git a/voidcrew/modules/colosseum/colosseum_controller.dm b/voidcrew/modules/colosseum/colosseum_controller.dm index cd20cd9065a..ff55e0ba644 100644 --- a/voidcrew/modules/colosseum/colosseum_controller.dm +++ b/voidcrew/modules/colosseum/colosseum_controller.dm @@ -853,11 +853,11 @@ return var/static/list/weapon_weights = list( /obj/item/spear = 25, - /obj/item/knife/combat = 20, + /obj/item/knife/combat/survival = 20, /obj/item/melee/baseball_bat = 20, /obj/item/shield/riot = 10, /obj/item/gun/ballistic/shotgun/doublebarrel = 10, - /obj/item/gun/energy/laser = 10, + /obj/item/gun/ballistic/automatic/pistol/m1911 = 10, /obj/item/restraints/legcuffs/beartrap = 5, ) var/obj/structure/closet/crate/weapon_crate = new() diff --git a/voidcrew/modules/colosseum/colosseum_modifiers.dm b/voidcrew/modules/colosseum/colosseum_modifiers.dm index b58a5b36fbb..49cca25abf5 100644 --- a/voidcrew/modules/colosseum/colosseum_modifiers.dm +++ b/voidcrew/modules/colosseum/colosseum_modifiers.dm @@ -108,8 +108,10 @@ if(QDELETED(spot)) continue var/beast_type = pick_weight(beast_weights) - var/mob/living/beast = new beast_type(spot) + var/datum/npc_spawn_context/context = new(controller.site, role = "exempt") + var/mob/living/beast = spawn_npc_in_zone(beast_type, spot, context, FALSE) beast.faction = list("colosseum") // beasts side with beasts, and nobody else + context.finalize() beasts += beast RegisterSignal(beast, COMSIG_LIVING_DEATH, PROC_REF(on_beast_death)) RegisterSignal(beast, COMSIG_QDELETING, PROC_REF(on_beast_gone)) diff --git a/voidcrew/modules/colosseum/colosseum_shop.dm b/voidcrew/modules/colosseum/colosseum_shop.dm index af3662fe185..5438fccda88 100644 --- a/voidcrew/modules/colosseum/colosseum_shop.dm +++ b/voidcrew/modules/colosseum/colosseum_shop.dm @@ -108,7 +108,7 @@ /datum/shop_sku/colosseum/pit_satchel category = "Pit Kit" item_path = /obj/item/storage/medkit/pit_doctor - price_credits = 1500 + price_credits = 1000 stock_min = 2 stock_max = 3 @@ -122,7 +122,7 @@ /datum/shop_sku/colosseum/rotating/sandstriders category = "Panoply" item_path = /obj/item/clothing/shoes/sandal/sandstrider - price_credits = 2400 + price_credits = 800 /datum/shop_sku/colosseum/rotating/parmula category = "Armaments" @@ -151,8 +151,8 @@ /datum/shop_sku/colosseum/rare/laurel category = "Panoply" item_path = /obj/item/clothing/head/costume/crown/laurel - price_vouchers = 2 - price_credits = 4500 + price_vouchers = 0 + price_credits = 2000 // ========================================================================= // THE LANISTA diff --git a/voidcrew/modules/crew_management/admin_crew_panel.dm b/voidcrew/modules/crew_management/admin_crew_panel.dm index b2097acc1f3..488d5ba12b3 100644 --- a/voidcrew/modules/crew_management/admin_crew_panel.dm +++ b/voidcrew/modules/crew_management/admin_crew_panel.dm @@ -185,7 +185,7 @@ ADMIN_VERB(manage_ship_crews, R_ADMIN, "Manage Ship Crews", "View and manage eve // team.remove_member() already retires their Ship Management button for this hull if(ship.claimed_captain == target) ship.claimed_captain = null - ship.ship_team?.remove_member(target) + target.leave_reputation_crew() ship.manifest -= kicked_name if(target.current) diff --git a/voidcrew/modules/cyberware/chrome_cradle.dm b/voidcrew/modules/cyberware/chrome_cradle.dm index 9304e582ed8..904bb0d0899 100644 --- a/voidcrew/modules/cyberware/chrome_cradle.dm +++ b/voidcrew/modules/cyberware/chrome_cradle.dm @@ -236,10 +236,14 @@ */ /obj/machinery/chrome_cradle/proc/begin_install(obj/item/organ/ware) var/mob/living/carbon/patient = occupant + if(!istype(patient) || QDELETED(ware)) + return + var/datum/component/cyberware/chrome = ware.GetComponent(/datum/component/cyberware) + if(!chrome || !cyberware_insert_check(ware, patient)) + return if(ware.loc != src && !patient.transferItemToLoc(ware, src)) balloon_alert(patient, "[ware.name] is stuck to you!") return - var/datum/component/cyberware/chrome = ware.GetComponent(/datum/component/cyberware) busy = TRUE busy_action = "install" busy_ware = ware @@ -282,8 +286,18 @@ var/datum/component/cyberware/chrome = ware.GetComponent(/datum/component/cyberware) if(!chrome) return + // Recheck the final set after the sequence delay, while every incumbent is + // still installed. An intervening install or lost Governor must not evict it. + if(!cyberware_insert_check(ware, patient)) + tray |= ware + balloon_alert(patient, "install refused!") + playsound(src, 'sound/machines/scanner/scanbuzz.ogg', 40, TRUE) + wake_patient(patient) + SStgui.update_uis(src) + return // Same-slot incumbents come out into the tray first, never the floor. - for(var/obj/item/organ/incumbent in ware.cyberware_get_incumbents(patient)) + var/list/incumbents = ware.cyberware_get_incumbents(patient) + for(var/obj/item/organ/incumbent in incumbents) incumbent.Remove(patient, special = TRUE) incumbent.forceMove(src) tray += incumbent @@ -291,10 +305,16 @@ tray -= ware chrome.grant_install_context(patient) if(!ware.Insert(patient)) - tray += ware + chrome.clear_install_context() + // Preserve the old installation even if a later insertion gate refuses. + for(var/obj/item/organ/incumbent in incumbents) + if(!QDELETED(incumbent) && incumbent.Insert(patient, special = TRUE)) + tray -= incumbent + tray |= ware balloon_alert(patient, "install refused!") playsound(src, 'sound/machines/scanner/scanbuzz.ogg', 40, TRUE) wake_patient(patient) + SStgui.update_uis(src) return do_sparks(3, FALSE, src) patient.flash_act(visual = 1) @@ -644,11 +664,9 @@ projected_capacity += chrome.capacity_bonus for(var/obj/item/organ/incumbent in selected_ware.cyberware_get_incumbents(patient)) evicts += incumbent.name + projected_load -= get_installed_capacity_cost(incumbent) var/datum/component/cyberware/incumbent_chrome = incumbent.GetComponent(/datum/component/cyberware) - if(!incumbent_chrome) - continue - projected_load -= incumbent_chrome.chrome_load - projected_capacity -= incumbent_chrome.capacity_bonus + projected_capacity -= incumbent_chrome?.capacity_bonus return list( "load" = projected_load, "capacity" = projected_capacity, diff --git a/voidcrew/modules/cyberware/cradle_ui.dm b/voidcrew/modules/cyberware/cradle_ui.dm index 889222f0bfd..7ad8beda7ad 100644 --- a/voidcrew/modules/cyberware/cradle_ui.dm +++ b/voidcrew/modules/cyberware/cradle_ui.dm @@ -18,7 +18,7 @@ * somewhere even before it gets a home. */ GLOBAL_LIST_INIT(cyberware_ui_groups, list( - list("id" = "cortex", "name" = "Frontal Cortex", "region" = "head", "slots" = list(ORGAN_SLOT_CYBERWARE_GOVERNOR, ORGAN_SLOT_CYBERWARE_RIGGER, ORGAN_SLOT_CYBERWARE_REGISTRY)), + list("id" = "cortex", "name" = "Frontal Cortex", "region" = "head", "slots" = list(ORGAN_SLOT_CYBERWARE_RIGGER, ORGAN_SLOT_CYBERWARE_REGISTRY)), list("id" = "ocular", "name" = "Ocular System", "region" = "head", "slots" = list(ORGAN_SLOT_EYES)), list("id" = "aural", "name" = "Auditory System", "region" = "head", "slots" = list(ORGAN_SLOT_CYBERWARE_EARS, ORGAN_SLOT_CYBERWARE_LARYNX)), list("id" = "os", "name" = "Operating System", "region" = "torso", "slots" = list(ORGAN_SLOT_CYBERWARE_OS)), diff --git a/voidcrew/modules/cyberware/cyberware_base.dm b/voidcrew/modules/cyberware/cyberware_base.dm index 921b8d4eb16..367f077f491 100644 --- a/voidcrew/modules/cyberware/cyberware_base.dm +++ b/voidcrew/modules/cyberware/cyberware_base.dm @@ -74,7 +74,6 @@ /obj/item/organ/cyberimp/cyberware/examine(mob/user) . = ..() - . += span_notice("Neural load: [chrome_load][chrome_capacity_bonus ? ", grants +[chrome_capacity_bonus] chrome capacity" : ""]. Tier [tier] chrome, install at a Chrome Cradle or through organ-manipulation surgery.") . += cyberware_emp_examine_line() /obj/item/organ/cyberimp/cyberware/Insert(mob/living/carbon/receiver, special = FALSE, movement_flags) @@ -151,8 +150,6 @@ /obj/item/organ/eyes/robotic/cyberware/examine(mob/user) . = ..() - . += span_notice("Neural load: [chrome_load]. Tier [tier] chrome, install at a Chrome Cradle or through organ-manipulation surgery.") - . += span_notice("Diagnostic bus: [chrome_scan_resolution >= CYBERWARE_SCAN_ITEMIZED ? "reads a body's chrome piece by piece" : "counts a body's chrome signatures, but can't name them"].") . += cyberware_emp_examine_line() /obj/item/organ/eyes/robotic/cyberware/Insert(mob/living/carbon/receiver, special = FALSE, movement_flags) @@ -223,7 +220,6 @@ /obj/item/organ/cyberimp/arm/toolkit/cyberware/examine(mob/user) . = ..() - . += span_notice("Neural load: [chrome_load]. Tier [tier] chrome, install at a Chrome Cradle or through organ-manipulation surgery.") . += cyberware_emp_examine_line() /obj/item/organ/cyberimp/arm/toolkit/cyberware/Insert(mob/living/carbon/receiver, special = FALSE, movement_flags) @@ -250,6 +246,9 @@ // (Fixer's Fingers picking a tool off the radial, the Rockjaw drill, blades, // launchers) lights the bearer up on the way out and on the way back in. /obj/item/organ/cyberimp/arm/toolkit/cyberware/Extend(obj/item/augment) + var/datum/component/cyberware/chrome = GetComponent(/datum/component/cyberware) + if(!chrome?.is_running()) + return . = ..() // The parent assigns active_item before it knows whether a hand is free // (augments_arms.dm), so "did it deploy" is "did it leave us", not "is it set". @@ -297,24 +296,10 @@ /proc/cyberware_insert_check(obj/item/organ/ware, mob/living/carbon/target, silent = FALSE, mob/feedback_to) if(!iscarbon(target)) return FALSE - var/datum/component/cyberware/chrome = ware.GetComponent(/datum/component/cyberware) - if(!chrome) + if(installed_capacity_check(target, list(ware), ware.cyberware_get_incumbents(target), silent)) return TRUE - var/projected_load = get_chrome_load(target) + chrome.chrome_load - var/projected_capacity = get_chrome_capacity(target) + chrome.capacity_bonus - for(var/obj/item/organ/incumbent in ware.cyberware_get_incumbents(target)) - var/datum/component/cyberware/incumbent_chrome = incumbent.GetComponent(/datum/component/cyberware) - if(!incumbent_chrome) - continue - projected_load -= incumbent_chrome.chrome_load - projected_capacity -= incumbent_chrome.capacity_bonus - if(projected_load <= projected_capacity) - return TRUE - if(!silent) - target.balloon_alert(target, "no neural headroom!") - to_chat(target, span_warning("Your nervous system is already maxed out. [ware] needs [projected_load - projected_capacity] more chrome capacity.")) - if(feedback_to && feedback_to != target) - to_chat(feedback_to, span_warning("[target]'s nervous system can't take [ware], [projected_load - projected_capacity] over capacity.")) + if(!silent && feedback_to && feedback_to != target) + feedback_to.balloon_alert(feedback_to, "no neural headroom!") return FALSE /** @@ -381,7 +366,7 @@ /// weakness was always in the design and never legible on the item itself; /// now that it actually bites (BAL-4), it says so where people read it. /proc/cyberware_emp_examine_line() - return span_notice("Electromagnetic pulses scramble chrome. An EMP knocks this offline for up to [DisplayTimeText(CYBERWARE_EMP_DOWNTIME)] and everything it was doing for you stops until it reboots; a Chrome Cradle tune-up clears it early.") + return span_notice("The shielding is thin and worn.") // ---- Ink bus ----------------------------------------------------------- @@ -458,11 +443,10 @@ . = ..() if(!.) return - if(!organ?.owner) - return FALSE - if(organ.organ_flags & ORGAN_FAILING) + var/datum/component/cyberware/chrome = organ?.GetComponent(/datum/component/cyberware) + if(!chrome?.is_running()) if(feedback) - organ.owner.balloon_alert(organ.owner, "chrome offline!") + organ?.owner?.balloon_alert(organ.owner, "chrome offline!") return FALSE return TRUE diff --git a/voidcrew/modules/cyberware/cyberware_component.dm b/voidcrew/modules/cyberware/cyberware_component.dm index 56456fb7f8d..36aa2c2692d 100644 --- a/voidcrew/modules/cyberware/cyberware_component.dm +++ b/voidcrew/modules/cyberware/cyberware_component.dm @@ -26,7 +26,7 @@ /// CYBERWARE_TIER_*, drives accent colours and Splice's bedside manner. var/tier = CYBERWARE_TIER_1 /// Chrome capacity this ware ADDS to its bearer while installed, the - /// Overclock Governor hook. Load 0 + bonus 6 is a Governor. + /// Governor Delete hook. Load 0 + bonus 15 is a Governor. var/capacity_bonus = 0 /// TRUE while the bearer is over capacity and this ware is browned out. var/browned_out = FALSE @@ -186,9 +186,10 @@ */ /datum/component/cyberware/proc/settle_passives(mob/living/carbon/bearer) var/obj/item/organ/ware = parent - var/installed_here = !QDELETED(ware) && bearer && ware.owner == bearer - var/ware_running = !browned_out && !emp_down && !(ware.organ_flags & ORGAN_FAILING) - var/should_be_online = installed_here && ware_running + var/should_be_online = bearer && ware.owner == bearer && is_running() + if(!should_be_online && istype(ware, /obj/item/organ/cyberimp/arm/toolkit/cyberware)) + var/obj/item/organ/cyberimp/arm/toolkit/cyberware/arm = ware + arm.Retract() if(passives_online == !!should_be_online) return passives_online = !!should_be_online @@ -201,7 +202,12 @@ /// settle (see the registration comment in on_implanted()). /datum/component/cyberware/proc/on_owner_life(mob/living/carbon/source, seconds_per_tick, times_fired) SIGNAL_HANDLER - settle_passives(source) + update_failing() + +/// Read our own failure sources: organ healing can clear the shared failing bit mid-reboot. +/datum/component/cyberware/proc/is_running() + var/obj/item/organ/ware = parent + return !QDELETED(ware) && ware.owner && !browned_out && !emp_down && ware.damage < ware.maxHealth && !(ware.organ_flags & ORGAN_FAILING) // ---- Install context --------------------------------------------------- // Cyberware refuses Insert() outside a legit context: organ-manipulation @@ -243,20 +249,86 @@ if(organ.GetComponent(/datum/component/cyberware)) . += organ -/// Chrome load of a single organ; 0 for anything that isn't chrome. +/// Compatibility helper for callers quoting an organ's installed load. /proc/get_organ_chrome_load(obj/item/organ/ware) - var/datum/component/cyberware/chrome = ware?.GetComponent(/datum/component/cyberware) - return chrome ? chrome.chrome_load : 0 + return get_installed_capacity_cost(ware) + +/// All non-chrome costs. Ordinary anatomy is free; chrome retains its component's cost. +GLOBAL_LIST_INIT(installed_capacity_costs, list( + "implant" = 1, + "robotic_organ" = 1, + "bioware" = 1, + "prosthetic_limb" = 0, +)) -/// The target's total neural load, summed live off the installed organs. +/proc/get_installed_capacity_cost(datum/installed) + if(QDELETED(installed)) + return 0 + if(isorgan(installed)) + var/obj/item/organ/organ = installed + var/datum/component/cyberware/chrome = organ.GetComponent(/datum/component/cyberware) + if(chrome) + return chrome.chrome_load + return (IS_ROBOTIC_ORGAN(organ) || istype(organ, /obj/item/organ/cyberimp)) ? GLOB.installed_capacity_costs["robotic_organ"] : 0 + if(istype(installed, /obj/item/implant)) + return GLOB.installed_capacity_costs["implant"] + if(istype(installed, /datum/status_effect/bioware)) + return GLOB.installed_capacity_costs["bioware"] + if(istype(installed, /obj/item/bodypart)) + return GLOB.installed_capacity_costs["prosthetic_limb"] + if(istype(installed, /datum/nanite_program)) + var/datum/nanite_program/program = installed + return program.capacity_cost() + return 0 + +/// A set of installed objects, so chrome never also counts as a generic robotic organ. +/proc/get_installed_capacity_items(mob/living/carbon/target) + var/list/installed = list() + if(!iscarbon(target)) + return installed + installed |= target.organs + if(target.implants) + installed |= target.implants + installed |= target.bodyparts + for(var/datum/status_effect/bioware/enhancement in target.status_effects) + installed |= enhancement + var/datum/component/nanites/nanites = target.GetComponent(/datum/component/nanites) + if(nanites && !QDELETED(nanites)) + installed |= nanites.programs + return installed + +/// Check the complete resulting body before mutating it. Removal and load-neutral recovery stay possible. +/proc/installed_capacity_check(mob/living/carbon/target, list/additions, list/removals, silent = FALSE) + if(!iscarbon(target)) + return TRUE + var/list/projected = get_installed_capacity_items(target) + if(removals) + projected -= removals + if(additions) + projected |= additions + var/load = 0 + var/capacity = CYBERWARE_BASE_CAPACITY + var/addition_cost = 0 + for(var/datum/item in additions) + addition_cost += get_installed_capacity_cost(item) + for(var/datum/item in projected) + load += get_installed_capacity_cost(item) + if(isorgan(item)) + var/datum/component/cyberware/chrome = item.GetComponent(/datum/component/cyberware) + capacity += chrome?.capacity_bonus + if(load <= capacity || (!addition_cost && load <= get_chrome_load(target) && capacity >= get_chrome_capacity(target))) + return TRUE + if(!silent) + target.balloon_alert(target, "no neural headroom!") + return FALSE + +/// The target's total installed load, including disabled programs. /proc/get_chrome_load(mob/living/carbon/target) . = 0 if(!iscarbon(target)) return - for(var/obj/item/organ/organ in target.organs) - var/datum/component/cyberware/chrome = organ.GetComponent(/datum/component/cyberware) - if(chrome) - . += chrome.chrome_load + for(var/datum/item in get_installed_capacity_items(target)) + . += get_installed_capacity_cost(item) /// The target's chrome capacity: the body's baseline plus every installed /// ware's capacity bonus (the Governor hook). @@ -291,9 +363,9 @@ if(!iscarbon(target) || QDELETED(target)) return var/list/installed = get_installed_cyberware(target) - if(!length(installed)) - return var/over = get_chrome_load(target) > get_chrome_capacity(target) + var/datum/component/nanites/nanites = target.GetComponent(/datum/component/nanites) + nanites?.settle_capacity(over) var/newly_tripped = FALSE var/any_recovered = FALSE for(var/obj/item/organ/ware as anything in installed) @@ -306,7 +378,7 @@ any_recovered = TRUE if(newly_tripped) target.balloon_alert(target, "chrome brownout!") - to_chat(target, span_boldwarning("Your chrome browns out. Too much load on the wetware. Shed some ware to bring it back.")) + to_chat(target, span_boldwarning("Your chrome goes dark.")) do_sparks(2, TRUE, target) else if(any_recovered) target.balloon_alert(target, "chrome back online") diff --git a/voidcrew/modules/cyberware/ripperdoc_catalog.dm b/voidcrew/modules/cyberware/ripperdoc_catalog.dm index b7b379946ef..eba6111bed5 100644 --- a/voidcrew/modules/cyberware/ripperdoc_catalog.dm +++ b/voidcrew/modules/cyberware/ripperdoc_catalog.dm @@ -3,10 +3,8 @@ * * The Undertow's chrome parlor: a vendor stall (own stock, own voice, own * ledger) fronted by Splice, who sits behind the counter in the neon annex off - * the Dregs. He sells cyberware and nothing else, the whole roster, tiered up - * the voucher ladder, with the flagships always on the shelf so the chase is - * plannable. Ammunition for the arm weapons is parlor-only: the return-visit - * hook. + * the Dregs. He sells utility chrome, limited melee implants and ammunition + * for salvaged gun arms. * * Balance shape mirrors Vex's counter: credits buy the street and pro-utility * ware, vouchers gate everything that changes how you fight. Splice's intake @@ -92,11 +90,9 @@ /datum/shop_sku/ripperdoc/atlas, /datum/shop_sku/ripperdoc/gorilla, /datum/shop_sku/ripperdoc/monowire, - /datum/shop_sku/ripperdoc/ronin, /datum/shop_sku/ripperdoc/rigger, /datum/shop_sku/ripperdoc/slabskin, /datum/shop_sku/ripperdoc/mantis, - /datum/shop_sku/ripperdoc/bunker_buster, /datum/shop_sku/ripperdoc/ghostskin, /datum/shop_sku/ripperdoc/lazarus, // ---- Legend Chrome (the chase, always listed) ---- @@ -152,11 +148,6 @@ "Sawbones does organs. I do upgrades. We don't compete. We share a supplier.", "The mirror's there for after. Everyone looks. Take your time.", "The bar watches installs through that window. Dram charges them for the seat.", - "EMP will drop your chrome for a few seconds. It comes back. You might not. Plan around it.", - "Capacity's not a suggestion. Your spine carries twenty points of load. After that, things brown out.", - "Trade-ins get you half list on the street grades. Military chrome I don't buy back. Nobody sells it anyway.", - "A courier once asked me to hide two kilos in his chest. The cavity's one slot. Read the tag.", - "Borging shreds every implant in the body. Cloning leaves them on the corpse. Bring the corpse.", "I don't do discounts. I do work that doesn't need discounts.", ), TRADER_LINE_RESTOCK = list( @@ -176,7 +167,7 @@ /datum/shop_sku/ripperdoc/chromatic_dermis category = "Street Chrome" - desc = "Programmable circuit-tattoos under the skin. They flare when you take a hit, strobe when your chrome fires, and go dim when you're starving. Pure show, and worth every credit." + desc = "Programmable circuit tattoos beneath the skin." item_path = /obj/item/organ/cyberimp/cyberware/chromatic_dermis price_credits = 400 stock_min = 2 @@ -184,7 +175,7 @@ /datum/shop_sku/ripperdoc/gastro category = "Street Chrome" - desc = "A furnace where your stomach was. Eats anything that qualifies as food, and it's generous about qualifying. Immune to whatever the Dregs calls a special." + desc = "A reinforced synthetic stomach with an industrial lining." item_path = /obj/item/organ/cyberimp/cyberware/gastro price_credits = 400 stock_min = 2 @@ -199,7 +190,7 @@ /datum/shop_sku/ripperdoc/nightshade category = "Street Chrome" - desc = "Sees in the dark, and doesn't blind you when someone pops a flash. The printed thermals can't say that. Diagnostic bus is stock, so you'll read how much chrome the other guy is carrying, but never what." + desc = "A pair of chrome low-light optics." item_path = /obj/item/organ/eyes/robotic/cyberware/nightshade price_credits = 600 stock_min = 1 @@ -208,7 +199,7 @@ /datum/shop_sku/ripperdoc/scrapper category = "Street Chrome" name = "Scrapper's Knuckles (pair)" - desc = "Reinforced knuckle plating for both hands. Hits people harder, hits machines and doors a lot harder. Sold as a set; install one side at a time." + desc = "A cased pair of reinforced knuckle plates." item_path = /obj/item/storage/case/cyberware/scrapper price_credits = 800 stock_min = 1 @@ -230,7 +221,7 @@ /datum/shop_sku/ripperdoc/cargo_cavity category = "Street Chrome" - desc = "One sealed slot behind the sternum. Scanners skip it and pat-downs miss it. I don't ask what goes in." + desc = "A concealed storage cavity behind the sternum." item_path = /obj/item/organ/cyberimp/cyberware/cargo_cavity price_credits = 1000 stock_min = 1 @@ -288,7 +279,7 @@ /datum/shop_sku/ripperdoc/icepick category = "Pro Chrome" - desc = "A data spike for the doors and turrets that won't open or stand down when asked. Outpost defenses ignore it, before you get ideas." + desc = "A chrome forearm with a retractable data spike." item_path = /obj/item/organ/cyberimp/arm/toolkit/cyberware/icepick price_vouchers = 1 @@ -327,58 +318,93 @@ // ========================================================================= /datum/shop_sku/ripperdoc/atlas + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" item_path = /obj/item/organ/cyberimp/cyberware/atlas price_vouchers = 2 /datum/shop_sku/ripperdoc/gorilla + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" name = "Gorilla Arms (pair)" - desc = "A pair of chrome forearms that punch through people, doors, and rock in that order. Sold cased; install one side at a time." + desc = "A cased pair of heavy chrome forearms." item_path = /obj/item/cyberware_pair_case/gorilla_arms price_vouchers = 3 + stock_min = 1 + stock_max = 1 + restock_interval = INFINITY + fixed_price = TRUE + contract_reward = FALSE /datum/shop_sku/ripperdoc/monowire + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" item_path = /obj/item/organ/cyberimp/arm/toolkit/cyberware/monowire price_vouchers = 3 + stock_min = 1 + stock_max = 1 + restock_interval = INFINITY + fixed_price = TRUE + contract_reward = FALSE /datum/shop_sku/ripperdoc/ronin + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" - desc = "A submachine gun that folds into your forearm. Feeds a proprietary caliber only I sell. See the Ammunition shelf. Nobody is disarming you of it." + desc = "A submachine gun folded into a chrome forearm." item_path = /obj/item/organ/cyberimp/arm/toolkit/cyberware/ronin price_vouchers = 3 + stock_min = 0 + stock_max = 0 + restock_interval = INFINITY + fixed_price = TRUE + contract_reward = FALSE /datum/shop_sku/ripperdoc/rigger + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" - desc = "Skull jack. Opens your ship's helm wherever you're standing on it: engineering, a corridor, medbay with your hands full. It borrows a real console, so a hull with no helm left gives you nothing, and you're walking, not running, the whole time you're flying." + desc = "A skull jack with a ship's control interface." item_path = /obj/item/organ/cyberimp/cyberware/rigger price_vouchers = 3 /datum/shop_sku/ripperdoc/slabskin + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" item_path = /obj/item/organ/cyberimp/cyberware/slabskin price_vouchers = 3 /datum/shop_sku/ripperdoc/mantis + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" name = "Mantis Blades (pair)" - desc = "A pair of folded blades where your forearms were. The lunge is the selling point; the reaction you get is free. Sold cased, install one side at a time." + desc = "A cased pair of folding forearm blades." item_path = /obj/item/cyberware_pair_case/mantis_blades price_vouchers = 4 + stock_min = 1 + stock_max = 1 + restock_interval = INFINITY + fixed_price = TRUE + contract_reward = FALSE /datum/shop_sku/ripperdoc/bunker_buster + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" - desc = "A two-shot rocket pod built into the forearm. Reloads are parlor-only. See the Ammunition shelf. Shaped charge, so it won't open your hull. Or theirs. Just whoever's standing in it." + desc = "A rocket pod built into a chrome forearm." item_path = /obj/item/organ/cyberimp/arm/toolkit/cyberware/bunker_buster price_vouchers = 4 + stock_min = 0 + stock_max = 0 + restock_interval = INFINITY + fixed_price = TRUE + contract_reward = FALSE /datum/shop_sku/ripperdoc/ghostskin + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" item_path = /obj/item/organ/cyberimp/cyberware/ghostskin price_vouchers = 4 /datum/shop_sku/ripperdoc/lazarus + zone_permission = ZONE_PERMISSION_ELITE category = "Military Chrome" item_path = /obj/item/organ/cyberimp/cyberware/lazarus price_vouchers = 4 @@ -388,14 +414,16 @@ // ========================================================================= /datum/shop_sku/ripperdoc/governor_delete + zone_permission = ZONE_PERMISSION_ELITE category = "Legend Chrome" - desc = "Firmware surgery. Your capacity governor goes back in believing your spine can carry six more points of load, and it holds. Everything else on this shelf needs it." + desc = "A factory neural governor with its limiter firmware stripped out." item_path = /obj/item/organ/cyberimp/cyberware/governor_delete price_vouchers = 5 stock_min = 1 stock_max = 1 /datum/shop_sku/ripperdoc/redline + zone_permission = ZONE_PERMISSION_ELITE category = "Legend Chrome" item_path = /obj/item/organ/cyberimp/cyberware/redline price_vouchers = 6 @@ -403,8 +431,9 @@ stock_max = 1 /datum/shop_sku/ripperdoc/cascade + zone_permission = ZONE_PERMISSION_ELITE category = "Legend Chrome" - desc = "Sandevistan-class reflex lattice. Eight seconds where everyone else may as well be furniture, then a crash that puts you on the floor. The most expensive thing I've ever bolted into a spine." + desc = "A Sandevistan reflex lattice in a sealed surgical case." item_path = /obj/item/organ/cyberimp/cyberware/cascade price_vouchers = 8 stock_min = 1 @@ -415,6 +444,7 @@ // ========================================================================= /datum/shop_sku/ripperdoc/ronin_mag + zone_permission = ZONE_PERMISSION_ELITE category = "Ammunition" name = "Ronin magazine" desc = "Proprietary caliber for the Popup Ronin. Nowhere else stocks it, which is the point." @@ -424,6 +454,7 @@ stock_max = 6 /datum/shop_sku/ripperdoc/buster_rockets + zone_permission = ZONE_PERMISSION_ELITE category = "Ammunition" name = "Bunker Buster rockets" desc = "A matched pair of shaped micro-rockets for the Bunker Buster pod. Handle them carefully. They're meant to go off at the far end." diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index 856c4306d2c..aa1c1ec9356 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -576,10 +576,10 @@ // ========================================================================= /** - * # Governor Delete (T4, head, governor slot, load 0, +6 capacity) + * # Governor Delete (T4, chest OS, load 0, +15 capacity) * * The enabler: a neural governor with its limiter firmware deleted, worth - * six points of chrome capacity and nothing else. Deliberately built on the + * fifteen points of capacity and nothing else. Deliberately built on the * generic chrome base rather than tg's brain-implant subtype, the brain * base answers EMP with a 200/severity stun, which on a PvP server would * turn a one-voucher grenade into an execution button. Here it browns out @@ -587,13 +587,13 @@ */ /obj/item/organ/cyberimp/cyberware/governor_delete name = "\improper Governor Delete" - desc = "A factory neural governor with the governing part surgically removed. It tells your nervous system it has six more points of headroom than it really does, and your nervous system believes it." + desc = "A factory neural governor with its limiter firmware stripped out." icon_state = "governor_delete" - zone = BODY_ZONE_HEAD - slot = ORGAN_SLOT_CYBERWARE_GOVERNOR + zone = BODY_ZONE_CHEST + slot = ORGAN_SLOT_CYBERWARE_OS w_class = WEIGHT_CLASS_SMALL chrome_load = 0 - chrome_capacity_bonus = 6 + chrome_capacity_bonus = 15 tier = CYBERWARE_TIER_4 // ========================================================================= @@ -818,14 +818,13 @@ /datum/action/cooldown/cyberware/piledriver_leap/proc/crush_victim(mob/living/jumper, mob/living/victim) if(QDELETED(victim) || QDELETED(jumper)) return - var/blocked = victim.run_armor_check(attack_flag = MELEE) if(iscarbon(victim)) var/mob/living/carbon/carbon_victim = victim - if(prob(CYBERWARE_PILEDRIVER_CRUSH_SPREAD_CHANCE)) + if(should_spread_crush_damage()) carbon_victim.apply_damage( CYBERWARE_PILEDRIVER_CRUSH_DAMAGE, BRUTE, - blocked = blocked, + blocked = 0, // Forced damage bypasses armor, including nanite armor accounting. forced = TRUE, spread_damage = TRUE, attack_direction = jumper.dir, @@ -839,7 +838,7 @@ victim.apply_damage( CYBERWARE_PILEDRIVER_CRUSH_DAMAGE, BRUTE, - blocked = blocked, + blocked = 0, forced = TRUE, attack_direction = jumper.dir, ) @@ -853,6 +852,9 @@ ) log_combat(jumper, victim, "crushed with a Meteor Piledriver landing") +/datum/action/cooldown/cyberware/piledriver_leap/proc/should_spread_crush_damage() + return prob(CYBERWARE_PILEDRIVER_CRUSH_SPREAD_CHANCE) + #undef CYBERWARE_CASCADE_WINDOW #undef CYBERWARE_CASCADE_CRASH #undef CYBERWARE_CASCADE_DODGE_CHANCE diff --git a/voidcrew/modules/cyberware/ware_military_body.dm b/voidcrew/modules/cyberware/ware_military_body.dm index 8bd9840fb15..66cd60b4e75 100644 --- a/voidcrew/modules/cyberware/ware_military_body.dm +++ b/voidcrew/modules/cyberware/ware_military_body.dm @@ -294,6 +294,8 @@ tier = CYBERWARE_TIER_3 /// TRUE during the one-second windup. var/jolting = FALSE + /// Changes on every windup/cancellation; old callbacks cannot spend a new installation's charge. + var/jolt_generation = 0 /// The once-per-six-minutes clock. COOLDOWN_DECLARE(jolt_cooldown) @@ -312,6 +314,12 @@ . = ..() UnregisterSignal(organ_owner, COMSIG_MOB_STATCHANGE) jolting = FALSE + jolt_generation++ + +/obj/item/organ/cyberimp/cyberware/lazarus/chrome_passives_off(mob/living/carbon/bearer) + . = ..() + jolting = FALSE + jolt_generation++ /** * Signal proc for [COMSIG_MOB_STATCHANGE]: fires the windup on the @@ -322,7 +330,8 @@ SIGNAL_HANDLER if(jolting) return - if(organ_flags & ORGAN_FAILING) + var/datum/component/cyberware/chrome = GetComponent(/datum/component/cyberware) + if(source != owner || !chrome?.is_running()) return if(new_stat == CONSCIOUS || new_stat == DEAD) return @@ -331,7 +340,7 @@ if(!COOLDOWN_FINISHED(src, jolt_cooldown)) return jolting = TRUE - COOLDOWN_START(src, jolt_cooldown, CYBERWARE_LAZARUS_COOLDOWN) + jolt_generation++ source.visible_message( span_boldwarning("[source] seizes, something under [source.p_their()] ribs whines up to full charge!"), span_userdanger("The Lazarus node screams up to charge..."), @@ -339,20 +348,26 @@ playsound(source, 'sound/machines/defib/defib_charge.ogg', 75, TRUE) source.set_jitter_if_lower(4 SECONDS) source.do_jitter_animation(200) - addtimer(CALLBACK(src, PROC_REF(jolt)), CYBERWARE_LAZARUS_WINDUP) + addtimer(CALLBACK(src, PROC_REF(jolt), jolt_generation, WEAKREF(source)), CYBERWARE_LAZARUS_WINDUP) /// The discharge: heal to 30%, clear the floor out from under every stun, /// and stand the patient up with a thunderclap. Skipped if they died (or /// recovered) during the windup, that second belongs to the attacker. -/obj/item/organ/cyberimp/cyberware/lazarus/proc/jolt() +/obj/item/organ/cyberimp/cyberware/lazarus/proc/jolt(windup_generation, datum/weakref/intended_patient) + if(!jolting || windup_generation != jolt_generation) + return jolting = FALSE - var/mob/living/carbon/patient = owner - if(QDELETED(src) || QDELETED(patient)) + var/mob/living/carbon/patient = intended_patient?.resolve() + if(QDELETED(src) || QDELETED(patient) || patient != owner) return - if(organ_flags & ORGAN_FAILING) + var/datum/component/cyberware/chrome = GetComponent(/datum/component/cyberware) + if(!chrome?.is_running()) return if(patient.stat == DEAD || patient.stat == CONSCIOUS) return + if(!COOLDOWN_FINISHED(src, jolt_cooldown)) + return + COOLDOWN_START(src, jolt_cooldown, CYBERWARE_LAZARUS_COOLDOWN) var/needed = (patient.maxHealth * CYBERWARE_LAZARUS_HEAL_TO) - patient.health if(needed > 0) var/oxy_heal = min(needed, patient.getOxyLoss()) diff --git a/voidcrew/modules/derelict_outposts/derelict_themes.dm b/voidcrew/modules/derelict_outposts/derelict_themes.dm index 22a7fd5b41c..4d4175bc53c 100644 --- a/voidcrew/modules/derelict_outposts/derelict_themes.dm +++ b/voidcrew/modules/derelict_outposts/derelict_themes.dm @@ -720,7 +720,7 @@ // The boss first, at its anchor var/turf/boss_tile = (boss_anchor && hostile_can_start(boss_anchor, generator)) ? boss_anchor : pick_hostile_tile(legal, spawned, boss_anchor, FALSE) if(boss_tile) - add_hostile(theme, theme.boss_type, boss_tile, red ? DERELICT_BOSS_HEALTH_MULT_RED : DERELICT_BOSS_HEALTH_MULT_YELLOW, spawned) + add_hostile(theme, theme.boss_type, boss_tile, "boss", spawned) else log_mapping("Derelict outpost [site.true_name]: no room for its [theme.name] boss.") // Then the roster, taking the anchors in turn so every cluster is guarded @@ -741,7 +741,7 @@ if(!tile) log_mapping("Derelict outpost [site.true_name]: no room for a [hostile_type].") continue - add_hostile(theme, hostile_type, tile, red ? DERELICT_HEALTH_MULT_RED : DERELICT_HEALTH_MULT_YELLOW, spawned) + add_hostile(theme, hostile_type, tile, "ordinary", spawned) /** * A start tile for one hostile from `legal`, spaced DERELICT_HOSTILE_SPACING from the others (1 if nothing fits). @@ -772,19 +772,13 @@ return null /// Spawns one hostile of `hostile_type` on `tile`, readies, scales and leashes it. Returns it, or null. -/datum/derelict_scene/proc/add_hostile(datum/derelict_theme/theme, hostile_type, turf/tile, multiplier, list/mob/living/spawned) - var/mob/living/hostile = new hostile_type(tile) +/datum/derelict_scene/proc/add_hostile(datum/derelict_theme/theme, hostile_type, turf/tile, role, list/mob/living/spawned) + var/datum/npc_spawn_context/context = new(site, site.derelict_band, role) + var/mob/living/hostile = spawn_npc_in_zone(hostile_type, tile, context, FALSE) if(QDELETED(hostile)) - // A mi-go can come out as its rare variant, which takes its place on the tile - hostile = null - for(var/mob/living/replacement in tile) - if(istype(replacement, hostile_type) && !QDELETED(replacement) && !(replacement in spawned)) - hostile = replacement - break - if(!hostile) - return null + return null theme.prepare_hostile(hostile) - scale_npc_ship_pirate_health(hostile, multiplier) + context.finalize() hostile.AddComponent(/datum/component/derelict_leash, site) site.derelict_hostiles += WEAKREF(hostile) spawned += hostile diff --git a/voidcrew/modules/dynamic_events/events/anomalies.dm b/voidcrew/modules/dynamic_events/events/anomalies.dm index 4fe5da0002a..e62e181b613 100644 --- a/voidcrew/modules/dynamic_events/events/anomalies.dm +++ b/voidcrew/modules/dynamic_events/events/anomalies.dm @@ -7,7 +7,7 @@ * * ALL OF THESE ARE ADMIN-ONLY. Every control below is weight 0 / max_occurrences 0, and * none of them roll naturally. Anomalies now generate on planet surfaces instead, see - * voidcrew/datums/mapgen/planet_anomalies.dm and the ZONE_PLANET_ANOMALY_BUDGET_* defines. + * voidcrew/datums/mapgen/planet_anomalies.dm and the ZONE_PLANET_ANOMALY_CHANCE_* defines. * * The reason is that a shuttle is the worst possible venue for one. A ship has a single * route between compartments, so an anomaly spawned in a corridor is not a hazard the diff --git a/voidcrew/modules/dynamic_events/events/portal_storm.dm b/voidcrew/modules/dynamic_events/events/portal_storm.dm index 65a1ce934b2..98b221070a6 100644 --- a/voidcrew/modules/dynamic_events/events/portal_storm.dm +++ b/voidcrew/modules/dynamic_events/events/portal_storm.dm @@ -81,6 +81,9 @@ var/next_pulse = 0 /// Whether the final pulse may spend one spawn slot on a boss. var/boss_pending = FALSE + /// Pinned when the event is set up, before later pulses or ship movement. + var/npc_spawn_zone + var/turf/npc_source_turf /// Builds plane-correct portal appearances without resolving or retaining turfs. /datum/round_event/voidcrew/portal_storm/setup() @@ -88,6 +91,8 @@ kill() return + npc_source_turf = get_turf(target_ship) + npc_spawn_zone = SSovermap_zones.get_zone_type_anywhere(npc_source_turf) || ZONE_GREEN storm_appearances = list() for(var/offset in 0 to SSmapping.max_plane_offset) var/mutable_appearance/storm = mutable_appearance('icons/obj/machines/engine/energy_ball.dmi', "energy_ball_fast", FLY_LAYER) @@ -159,10 +164,12 @@ spawn_slots_remaining-- var/mob_type + var/role = "ordinary" if(boss_pending && pulses_remaining == 1) boss_pending = FALSE if(length(target_ship.get_event_crew()) >= 5) mob_type = pick_weight(boss_types) + role = "boss" else mob_type = pick_weight(hostile_types) if(!mob_type) @@ -171,7 +178,8 @@ var/turf/open/floor/spawn_turf = target_ship.get_random_open_ship_turf() if(!spawn_turf || !target_ship.is_aboard(spawn_turf)) continue - var/mob/living/new_hostile = new mob_type(spawn_turf) + var/datum/npc_spawn_context/context = new(target_ship, npc_spawn_zone, role, source_turf = npc_source_turf) + var/mob/living/new_hostile = spawn_npc_in_zone(mob_type, spawn_turf, context) if(QDELETED(new_hostile)) continue spawned_mobs++ diff --git a/voidcrew/modules/dynamic_events/events/vent_clog.dm b/voidcrew/modules/dynamic_events/events/vent_clog.dm index ec5cdb534c8..45d864416c4 100644 --- a/voidcrew/modules/dynamic_events/events/vent_clog.dm +++ b/voidcrew/modules/dynamic_events/events/vent_clog.dm @@ -74,12 +74,15 @@ var/list/filth_spawn_types = list() /// Whether this datum currently has signals registered on vent. var/signals_registered = FALSE + /// Capture the owning ship's band, including while docked or in transit. + var/datum/npc_spawn_context/npc_source /datum/round_event/voidcrew/vent_clog/setup() if(!target_valid()) finish_event() return + npc_source = new(target_ship, SSovermap_zones.get_zone_type_anywhere(get_turf(target_ship)) || ZONE_GREEN) vent = get_vent() if(QDELETED(vent) || !target_ship.is_aboard(vent)) finish_event() @@ -205,7 +208,10 @@ if(length(living_mobs) >= min(maximum_spawns, crew_scaled_cap)) return - var/mob/living/new_mob = new spawned_mob(vent_loc) + var/datum/npc_spawn_context/context = new(target_ship, npc_source.zone_type, source_turf = npc_source.source_turf) + var/mob/living/new_mob = spawn_npc_in_zone(spawned_mob, vent_loc, context) + if(QDELETED(new_mob)) + return living_mobs += WEAKREF(new_mob) vent.visible_message(span_warning("[new_mob] crawls out of [vent]!")) diff --git a/voidcrew/modules/lich/lich_abilities.dm b/voidcrew/modules/lich/lich_abilities.dm index 07c2494cdb2..ba082eb5ac5 100644 --- a/voidcrew/modules/lich/lich_abilities.dm +++ b/voidcrew/modules/lich/lich_abilities.dm @@ -250,6 +250,7 @@ /mob/living/basic/skeleton/templar, ) summon_radius = 2 + npc_summon_role = "ordinary" summon_amount = 2 summon_respects_density = TRUE // Permanent. The cap plus the mob's death cleanup is what keeps this safe, not a @@ -298,7 +299,6 @@ qdel(risen) return - ilthuun.register_summon(risen) // FACTION_LICH is what makes the ward machines read this thing as its layer's // garrison rather than as somebody's pet, so the gates open when the hall is // actually clear. FACTION_SKELETON is kept so the /mob/living/basic/skeleton types @@ -309,6 +309,7 @@ if(risen_turf) new /obj/effect/temp_visual/small_smoke/halfsecond(risen_turf) playsound(risen_turf, 'sound/effects/magic/RATTLEMEBONES.ogg', 45, vary = TRUE) + ilthuun.register_summon(risen) // ===== DESTRUCTION ===== @@ -452,6 +453,8 @@ var/burn_interval = 1 SECONDS /// Burn applied per tick to anyone standing on an affected tile. var/burn_per_tick = 5 + /// Captured at cast time; the patch never depends on a surviving caster for damage. + var/npc_damage_factor = 1 /// The live patch's ground visuals, so his deletion can snuff them early. var/list/obj/effect/temp_visual/lich_grave_dirt/active_dirt = list() /// Stoppable id of the pending ignition, null outside the telegraph window. @@ -481,6 +484,7 @@ var/turf/center = get_turf(cast_on) if(isnull(center)) return + npc_damage_factor = isliving(owner) ? npc_zone_damage(1, owner) : 1 if(owner) owner.visible_message(span_boldwarning("Ilthuun drives a hand downward, and the ground around [cast_on] darkens into turned earth.")) @@ -538,7 +542,7 @@ for(var/mob/living/victim in tile) if(!can_burn(victim)) continue - victim.adjustFireLoss(burn_per_tick) + victim.adjustFireLoss(npc_zone_damage(burn_per_tick, npc_damage_factor)) to_chat(victim, span_danger("The grave-dirt sears you from below!")) // The first application lands at ignition, so reschedule only while the NEXT one // would still fall inside the window: 7 ticks total, not 8. @@ -660,11 +664,14 @@ var/turf/spot = get_step(cast_on, pick_n_take(directions)) if(isnull(spot) || spot.is_blocked_turf(exclude_mobs = TRUE)) continue - var/mob/living/basic/lich/mirror_image/copy = new(spot) + ilthuun.get_npc_spawn_context() + var/datum/npc_spawn_context/context = new(ilthuun, role = "ordinary") + var/mob/living/basic/lich/mirror_image/copy = spawn_npc_in_zone(/mob/living/basic/lich/mirror_image, spot, context, FALSE) ilthuun.register_summon(copy) copies += copy RegisterSignals(copy, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(lost_copy)) QDEL_IN(copy, image_lifespan) + context.finalize() placed++ if(!placed) @@ -833,7 +840,7 @@ // mend_the_dead() in lich_spells.dm. victim.heal_overall_damage(brute = 10, burn = 10, forced = TRUE) return - victim.adjustToxLoss(8, forced = TRUE) + victim.adjustToxLoss(npc_zone_damage(8, npc_zone_damage_factor), forced = TRUE) to_chat(victim, span_danger("Something starts rotting where the bolt went in.")) #undef VERDIGRIS_TINT diff --git a/voidcrew/modules/lich/lich_loot.dm b/voidcrew/modules/lich/lich_loot.dm index 516e9ffcfc1..45e055f31d0 100644 --- a/voidcrew/modules/lich/lich_loot.dm +++ b/voidcrew/modules/lich/lich_loot.dm @@ -157,6 +157,8 @@ GLOBAL_LIST_INIT(lich_hoard_contents, list( var/list/turf/remaining = scatter.Copy() for(var/hoard_type as anything in GLOB.lich_hoard_contents) + if(hoard_type == /obj/item/melee/verdigris_staff && (!istype(drop_near, /mob/living/basic/lich) || !drop_near:npc_red_weapon_drop)) + continue if(!length(remaining)) remaining = scatter.Copy() new hoard_type(pick_n_take(remaining)) @@ -671,7 +673,8 @@ GLOBAL_LIST_INIT(lich_hoard_contents, list( refuse_draught() return - var/came_back = drinker.heal_and_revive( + var/came_back = drinker.heal_and_revive_from( + "phylactery", LICH_PHYLACTERY_REVIVE_TO, span_boldwarning("[drinker] jerks upright in a wash of green light, and somewhere a jar breaks."), ) diff --git a/voidcrew/modules/lich/lich_mob.dm b/voidcrew/modules/lich/lich_mob.dm index 028ef65ff41..9940f0a45de 100644 --- a/voidcrew/modules/lich/lich_mob.dm +++ b/voidcrew/modules/lich/lich_mob.dm @@ -406,8 +406,12 @@ /mob/living/basic/lich/proc/register_summon(mob/living/summon) if(QDELETED(summon) || (summon in live_summons)) return + get_npc_spawn_context() + if(!summon.npc_spawn_context) + summon.npc_spawn_context = new /datum/npc_spawn_context(src, role = "ordinary") live_summons += summon RegisterSignals(summon, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_summon_lost)) + summon.finalize_npc_zone(summon.npc_spawn_context) /mob/living/basic/lich/proc/on_summon_lost(mob/living/source) SIGNAL_HANDLER @@ -620,7 +624,7 @@ SIGNAL_HANDLER if(attack_flags & (ATTACKER_STAMINA_ATTACK|ATTACKER_SHOVING)) return - attacker.adjustFireLoss(15) + attacker.adjustFireLoss(npc_zone_damage(15, src)) to_chat(attacker, span_warning("The image bursts, and something cold pours down your arm!")) /mob/living/basic/lich/mirror_image/examine(mob/user) diff --git a/voidcrew/modules/lich/lich_site.dm b/voidcrew/modules/lich/lich_site.dm index 22503b2c570..2205c7d2ae1 100644 --- a/voidcrew/modules/lich/lich_site.dm +++ b/voidcrew/modules/lich/lich_site.dm @@ -331,7 +331,8 @@ GLOBAL_DATUM(lich_lair, /obj/structure/overmap/space_ruin/lich_lair) if(!found_lich && !spent) var/turf/spawn_turf = boss_spawn_turf || get_random_layer_turf(/area/ruin/space/has_grav/powered/lich_lair/sanctum) if(spawn_turf) - found_lich = new /mob/living/basic/lich(spawn_turf) + var/datum/npc_spawn_context/context = new(src, role = "boss") + found_lich = spawn_npc_in_zone(/mob/living/basic/lich, spawn_turf, context) log_mapping("LICH: template placed no lich. Spawned one at ([spawn_turf.x], [spawn_turf.y]).") else log_mapping("LICH: no lich, no boss_spawn landmark and no sanctum turf. The raid has no boss.") @@ -350,6 +351,8 @@ GLOBAL_DATUM(lich_lair, /obj/structure/overmap/space_ruin/lich_lair) /obj/structure/overmap/space_ruin/lich_lair/proc/bind_lich(mob/living/boss) if(QDELETED(boss)) return + if(!boss.npc_spawn_context) + boss.finalize_npc_zone(new /datum/npc_spawn_context(src, role = "boss")) lich_ref = WEAKREF(boss) RegisterSignal(boss, COMSIG_LIVING_DEATH, PROC_REF(on_boss_death), override = TRUE) diff --git a/voidcrew/modules/lich/lich_spells.dm b/voidcrew/modules/lich/lich_spells.dm index 3ad47797a0a..096c68d95f2 100644 --- a/voidcrew/modules/lich/lich_spells.dm +++ b/voidcrew/modules/lich/lich_spells.dm @@ -439,11 +439,14 @@ var/list/mob/living/basic/verdigris_mirage/fresh_copies = list() while(length(fresh_copies) < VERDIGRIS_MIRAGE_COUNT && length(free_steps) > 1) var/turf/mirage_turf = pick_n_take(free_steps) - var/mob/living/basic/verdigris_mirage/mirage = new(mirage_turf, caster) + caster.get_npc_spawn_context() + var/datum/npc_spawn_context/context = new(caster) + var/mob/living/basic/verdigris_mirage/mirage = spawn_npc_in_zone(/mob/living/basic/verdigris_mirage, mirage_turf, context, FALSE, list(caster)) mirages += mirage fresh_copies += mirage RegisterSignals(mirage, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_mirage_lost)) QDEL_IN(mirage, VERDIGRIS_MIRAGE_LIFESPAN) + context.finalize() if(!length(fresh_copies)) to_chat(caster, span_warning("The grave-light gathers, but there's no room for it.")) @@ -633,7 +636,7 @@ return stung = TRUE - attacker.adjustBruteLoss(VERDIGRIS_MIRAGE_STING) + attacker.adjustBruteLoss(npc_zone_damage(VERDIGRIS_MIRAGE_STING, src)) to_chat(attacker, span_userdanger("The mirage bursts, spraying green light through your arm!")) new /obj/effect/temp_visual/small_smoke/halfsecond(get_turf(src)) QDEL_IN(src, 0) diff --git a/voidcrew/modules/loot/abandoned_crate.dm b/voidcrew/modules/loot/abandoned_crate.dm index 8638ac4c9eb..1a2e6e7d60d 100644 --- a/voidcrew/modules/loot/abandoned_crate.dm +++ b/voidcrew/modules/loot/abandoned_crate.dm @@ -1,59 +1,15 @@ /** - * # Abandoned crate loot - * - * Replaces tg's `spawn_loot()` (code/modules/mining/abandoned_crates.dm), - * which was a 1-100 switch that paid a plushie, a balloon, kitty ears or a - * banhammer-and-bwoink-mines gag roughly seven times in ten. That table is - * written for a station round where the crate is a bit of maintenance - * furniture. Out here a crate is a two-jump trip and a bomb you have to - * out-think, so it pays like one. - * - * WHAT THIS IS NOT: a zone cache. The nine themed caches - * (voidcrew/modules/loot/themes/) are the other channel and they stay the - * better one, because they are guarded, band-scaled and hold the authored - * uniques. This crate is the flat one: a shipping container nobody came back - * for, identical odds in green space and in red. The lock is what it charges - * instead of a zone (see the header block on the constants in - * voidcrew/_DEFINES/loot.dm for why that trade is the whole design). - * - * The content split follows from that. A themed cache is a PURPOSEFUL - * stockpile: an armory holds an armory's guns, a lab holds a lab's science, a - * pirate hoard holds treasure. Freight is COMMERCIAL CARGO, which is to say - * anything a hauler moves: a pallet of plasteel, a case of medkits, a crate of - * surplus rifles, a bale of winter coats. Breadth is the identity, not a - * subject. - * - * That extends to the guns, and it is the main thing keeping this table off - * the armory theme's toes. The armory pays the SEC-ISSUE ladder (vest and - * helmet, disabler, seclite, baton, riot line). Freight pays what turns up in - * a container: surplus bolt-actions, hunting doubles, a riot pump, a Makarov, - * a detective's .38, an uzi, a tommygun. Civilian, surplus and criminal - * hardware rather than anybody's service weapon, and every ballistic brings a - * spare reload via GLOB.loot_gun_spare_ammo so a gun roll is never dead. - * - * Still excluded, all four the other channel's job: cyberware, authored - * uniques, themed weapon SETS, and five-figure cash. - * - * Tuned against the outpost shelves, which are the certainty channel to this - * crate's gamble (plasteel x20 = 900cr, jetpack = 2100cr, RCD = 2400cr, the - * dearest general SKU = 3600cr). A cracked crate averages a couple of hundred - * credits of loose cash, about one gun, and 5-7 items a crew would otherwise - * have flown to a trader for. It is a good crate. It is not a round-winning - * one. + * Deca-code freight keeps its lock and 5-7 draws. One draw is a weapon from + * its spawn-zone profile; the others retain the nonweapon freight tables. + * Freight never pays advanced guns, cyberware or authored unique weapons. */ -/// Freight bulk: consumables, kit and the surplus long gun off the bottom of -/// the manifest. A crate that paid nothing but these would still be worth -/// cracking, which is the bar this tier has to clear. +/// Nonweapon commercial cargo, drawn without replacement within each tier. GLOBAL_LIST_INIT(abandoned_crate_common, list( /obj/item/storage/medkit/regular = 8, /obj/item/stack/sheet/iron/fifty = 8, - // the container gun: war-surplus, one shot at a time, and it arrives with - // a box of 310 to feed it - /obj/item/gun/ballistic/rifle/boltaction/surplus = 7, /obj/item/stack/sheet/glass/fifty = 7, /obj/item/tank/internals/oxygen = 7, - /obj/item/knife/combat = 6, /obj/item/reagent_containers/hypospray/medipen = 6, /obj/item/storage/box/donkpockets = 6, /obj/item/food/rationpack = 5, @@ -75,19 +31,12 @@ GLOBAL_LIST_INIT(abandoned_crate_uncommon, list( /obj/item/stack/sheet/plasteel/twenty = 8, /obj/item/tank/jetpack/oxygen = 7, /obj/item/storage/medkit/advanced = 7, - /obj/item/gun/ballistic/shotgun/riot = 6, /obj/item/clothing/suit/armor/vest = 6, /obj/item/stack/spacecash/c500 = 6, /obj/item/stack/sheet/mineral/plasma/thirty = 6, - /obj/item/gun/ballistic/automatic/pistol = 5, - /obj/item/gun/ballistic/revolver/c38/detective = 5, - /obj/item/gun/ballistic/shotgun/doublebarrel = 5, /obj/item/stock_parts/power_store/cell/super = 5, /obj/item/stack/sheet/mineral/titanium/fifty = 5, /obj/item/reagent_containers/hypospray/medipen/survival = 5, - // somebody was moving these in a crate marked FARM EQUIPMENT - /obj/item/gun/ballistic/automatic/mini_uzi = 4, - /obj/item/gun/ballistic/automatic/tommygun = 4, /obj/item/storage/backpack/duffelbag/med = 4, /obj/item/survivalcapsule = 4, /obj/item/wormhole_jaunter = 4, @@ -97,48 +46,151 @@ GLOBAL_LIST_INIT(abandoned_crate_uncommon, list( /// The top of freight, and deliberately the top of nothing else. Gear a crew /// reorganizes an afternoon around, never gear it reorganizes the round -/// around: no uniques, no chrome, no five-figure cash. The blueprint is the -/// one long-odds reach into the weapons-bench pipeline, at the tier the themed -/// caches already put a mid blueprint on. +/// around: no uniques, no chrome, no five-figure cash. GLOBAL_LIST_INIT(abandoned_crate_prime, list( /obj/item/stack/sheet/plasteel/fifty = 7, /obj/item/ship_parts/combat = 7, /obj/item/construction/rcd/loaded = 6, - /obj/item/gun/ballistic/shotgun/automatic/combat = 6, - /obj/item/gun/energy/e_gun = 5, - /obj/item/gun/ballistic/automatic/pistol/deagle = 5, /obj/item/stock_parts/power_store/cell/hyper = 5, /obj/item/defibrillator/compact = 5, /obj/item/stack/spacecash/c1000 = 5, /obj/item/stack/sheet/mineral/uranium/fifty = 5, /obj/item/stack/sheet/mineral/gold/fifty = 5, - /obj/item/gun/energy/laser = 4, - /obj/item/gun/ballistic/rifle/boltaction = 4, /obj/item/storage/medkit/surgery = 4, /obj/item/stack/sheet/mineral/diamond/five = 4, /obj/item/clothing/glasses/night = 4, - /obj/item/blueprint/gun/wt550 = 3, // a loaded bluespace RPED: ten each of tier-3 parts, enough to re-fit // every machine on a hull in one pass. Tier 4 exists a line above this one // in part_replacer.dm if this plays too weak /obj/item/storage/part_replacer/bluespace/tier3 = 3, )) +GLOBAL_LIST_INIT(abandoned_crate_guns_green, list( + /obj/item/gun/ballistic/rifle/boltaction/surplus, + /obj/item/gun/ballistic/automatic/pistol, + /obj/item/gun/ballistic/revolver/c38/detective, + /obj/item/gun/ballistic/shotgun/doublebarrel, +)) + +GLOBAL_LIST_INIT(abandoned_crate_guns_yellow, list( + /obj/item/gun/ballistic/shotgun/riot, + /obj/item/gun/ballistic/automatic/tommygun, + /obj/item/gun/ballistic/automatic/mini_uzi, +)) + +GLOBAL_LIST_INIT(abandoned_crate_melee_green, list( + /obj/item/knife/combat/survival, + /obj/item/spear, + /obj/item/hatchet, + /obj/item/melee/baseball_bat, +)) + +GLOBAL_LIST_INIT(abandoned_crate_melee_yellow, list( + /obj/item/knife/combat, + /obj/item/claymore/shortsword, + /obj/item/melee/baseball_bat/ablative, + /obj/item/fireaxe, + /obj/item/melee/cleric_mace, + /obj/item/melee/sabre, +)) + +GLOBAL_LIST_INIT(abandoned_crate_melee_red, list( + /obj/item/claymore, + /obj/item/claymore/cutlass, + /obj/item/katana, + /obj/item/mjollnir, + /obj/item/melee/energy/sword/saber, + /obj/item/melee/energy/sword/pirate, +)) + /** - * Fires once, and only from the correct-code branch of attack_hand(). A crate - * that gets blown, cut or emagged never reaches here, which is why the table - * can afford to be this good. - * - * Draws WITHOUT replacement, so one crate never pays the same line twice, and - * an exhausted tier walks down rather than wasting the draw (see - * draw_loot_from_tier in zone_loot.dm). There is no UNIQUE tier here; the - * odds never select one, and the walk-down handles the missing shelf anyway. + * One Bernoulli gun roll, then a class roll in thousandths. Freight folds + * the gun ladder's red class into yellow (940/60, 450/550, 150/850). + * Melee retains all three classes: 900/99/1, 350/640/10, 100/450/450. + * Explicit rolls let tests enumerate the exact distribution without sampling. */ +/proc/freight_weapon_pool(zone_type, gun_roll = rand(1, 100), class_roll = rand(1, 1000)) + var/gun_chance = 8 + var/gun_green = 940 + var/melee_green = 900 + var/melee_through_yellow = 999 + switch(zone_type) + if(ZONE_YELLOW) + gun_chance = 19 + gun_green = 450 + melee_green = 350 + melee_through_yellow = 990 + if(ZONE_RED) + gun_chance = 33 + gun_green = 150 + melee_green = 100 + melee_through_yellow = 550 + if(gun_roll <= gun_chance) + return class_roll <= gun_green ? GLOB.abandoned_crate_guns_green : GLOB.abandoned_crate_guns_yellow + if(class_roll <= melee_green) + return GLOB.abandoned_crate_melee_green + if(class_roll <= melee_through_yellow) + return GLOB.abandoned_crate_melee_yellow + return GLOB.abandoned_crate_melee_red + +/obj/structure/closet/crate/secure/loot + /// Resolved once at the origin; moving the crate cannot improve it. + var/loot_zone + var/turf/spawn_turf + +/obj/structure/closet/crate/secure/loot/Initialize(mapload) + spawn_turf = spawned_loot ? null : get_turf(src) + return ..() + +/obj/structure/closet/crate/secure/loot/LateInitialize() + . = ..() + if(!spawned_loot) + new /datum/zone_resolver(spawn_turf, CALLBACK(src, PROC_REF(on_freight_zone_resolved))) + +/obj/structure/closet/crate/secure/loot/proc/on_freight_zone_resolved(zone_type) + if(QDELETED(src) || spawned_loot || !isnull(loot_zone) || isnull(zone_type)) + return + loot_zone = zone_type + spawn_turf = null + +/// The same latch covers correct-code payout, forced opening and rebuilds. +/obj/structure/closet/crate/secure/loot/proc/consume_freight_loot() + spawned_loot = TRUE + spawn_turf = null + +/obj/structure/closet/crate/secure/loot/dump_contents() + // All opening, break, cut and anti-tamper explosion paths dump contents. + // Only the correct-code branch calls spawn_loot() before reaching this. + consume_freight_loot() + return ..() + +/obj/structure/closet/crate/secure/loot/emag_act(mob/user, obj/item/card/emag/emag_card) + consume_freight_loot() + return ..() + +/obj/structure/closet/crate/secure/loot/Destroy() + consume_freight_loot() + return ..() + +/// Replaces upstream's gag table; only a correct code requests this payout. /obj/structure/closet/crate/secure/loot/spawn_loot() + if(QDELETED(src) || spawned_loot) + return + // Claim before any item constructor can yield or re-enter this proc. + spawned_loot = TRUE + if(isnull(loot_zone)) + loot_zone = SSovermap_zones.get_zone_type_anywhere(spawn_turf) + if(isnull(loot_zone)) + loot_zone = ZONE_GREEN + consume_freight_loot() + var/weapon_path = pick(freight_weapon_pool(loot_zone)) + new weapon_path(src) + var/spare_ammo = GLOB.loot_gun_spare_ammo[weapon_path] + if(spare_ammo) + new spare_ammo(src) var/list/pools = list( "[LOOT_TIER_COMMON]" = GLOB.abandoned_crate_common.Copy(), "[LOOT_TIER_UNCOMMON]" = GLOB.abandoned_crate_uncommon.Copy(), "[LOOT_TIER_PRIME]" = GLOB.abandoned_crate_prime.Copy(), ) - roll_loot_draws(src, pools, rand(ABANDONED_CRATE_DRAWS_MIN, ABANDONED_CRATE_DRAWS_MAX), ABANDONED_CRATE_ODDS) - spawned_loot = TRUE + roll_loot_draws(src, pools, rand(ABANDONED_CRATE_DRAWS_MIN, ABANDONED_CRATE_DRAWS_MAX) - 1, ABANDONED_CRATE_ODDS) diff --git a/voidcrew/modules/loot/loot_guns.dm b/voidcrew/modules/loot/loot_guns.dm index be0a9e4eb6b..6c3401cfb48 100644 --- a/voidcrew/modules/loot/loot_guns.dm +++ b/voidcrew/modules/loot/loot_guns.dm @@ -33,20 +33,98 @@ /obj/item/gun/energy/temperature/unrestricted pin = /obj/item/firing_pin -/** - * The armory contraband spawner is scattered through a dozen ruins, and its two - * `/contraband` pistols roll a 1-in-10 clown pin on spawn: usually a gun the - * finder simply cannot fire, and on the super-ultra roll one that detonates in - * their hands. Same guns, same odds, without the coin flip. - */ +// Incidental contraband rolls cannot bypass the guarded weapon ladder. /obj/effect/spawner/random/contraband/armory loot = list( - /obj/item/gun/ballistic/automatic/pistol = 80, - /obj/item/gun/ballistic/shotgun/automatic/combat = 50, - /obj/item/storage/box/syndie_kit/throwing_weapons = 30, - /obj/item/grenade/clusterbuster/teargas = 20, - /obj/item/grenade/clusterbuster = 20, - /obj/item/gun/ballistic/automatic/pistol/deagle, - /obj/item/gun/ballistic/revolver/mateba = 9, - /obj/item/gun/ballistic/revolver/reverse/mateba = 1, + /obj/item/knife/combat/survival = 1, + /obj/item/spear = 1, + /obj/item/hatchet = 1, + /obj/item/melee/baseball_bat = 1, ) + +/// Main entries have equal weight; theme-specific retained variants have weight one. +GLOBAL_LIST_INIT(loot_gun_pools, list( + LOOT_GUN_POOL_ARMORY = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/rifle/boltaction/surplus = 10, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/energy/laser = 10, + /obj/item/gun/energy/e_gun = 10, + /obj/item/gun/ballistic/automatic/wt550 = 10, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/energy/laser/carbine = 10, + /obj/item/gun/energy/lasercannon/unrestricted = 10, + /obj/item/gun/energy/laser/scatter = 10, + /obj/item/gun/ballistic/rifle/sniper_rifle = 10, + /obj/item/gun/ballistic/shotgun/automatic/combat = 10, + ), + ), + LOOT_GUN_POOL_SYNDICATE = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/automatic/pistol = 10, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/c20r/unrestricted = 10, + /obj/item/gun/ballistic/automatic/mini_uzi = 10, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/ballistic/automatic/l6_saw/unrestricted = 10, + /obj/item/gun/energy/xray = 10, + /obj/item/gun/energy/e_gun/nuclear = 10, + ), + ), + LOOT_GUN_POOL_PLUNDER = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/revolver/c38/detective = 10, + /obj/item/gun/ballistic/shotgun/doublebarrel = 10, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/revolver = 10, + /obj/item/gun/ballistic/automatic/tommygun = 10, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/ballistic/shotgun/bulldog/unrestricted = 10, + /obj/item/gun/energy/laser/hellgun = 10, + ), + ), + LOOT_GUN_POOL_FREIGHT = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/rifle/boltaction/surplus = 10, + /obj/item/gun/ballistic/automatic/pistol = 10, + /obj/item/gun/ballistic/revolver/c38/detective = 10, + /obj/item/gun/ballistic/shotgun/doublebarrel = 10, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/shotgun/riot = 10, + /obj/item/gun/ballistic/automatic/tommygun = 10, + /obj/item/gun/ballistic/automatic/mini_uzi = 10, + ), + ), +)) + +GLOBAL_LIST_INIT(loot_melee_pools, list( + LOOT_WEAPON_GREEN = list( + /obj/item/knife/combat/survival = 10, + /obj/item/spear = 10, + /obj/item/hatchet = 10, + /obj/item/melee/baseball_bat = 10, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/knife/combat = 10, + /obj/item/claymore/shortsword = 10, + /obj/item/melee/baseball_bat/ablative = 10, + /obj/item/fireaxe = 10, + /obj/item/melee/cleric_mace = 10, + /obj/item/melee/sabre = 10, + ), + LOOT_WEAPON_RED = list( + /obj/item/claymore = 10, + /obj/item/claymore/cutlass = 10, + /obj/item/katana = 10, + /obj/item/mjollnir = 10, + /obj/item/melee/energy/sword/saber = 10, + /obj/item/melee/energy/sword/pirate = 10, + ), +)) diff --git a/voidcrew/modules/loot/themes/_theme.dm b/voidcrew/modules/loot/themes/_theme.dm index 033fe3b5be3..e6900d346fc 100644 --- a/voidcrew/modules/loot/themes/_theme.dm +++ b/voidcrew/modules/loot/themes/_theme.dm @@ -1,59 +1,24 @@ /** - * # Loot themes + * Each theme owns four nonweapon tiers plus retained weapon variants. + * A cache replaces one draw with the shared gun-or-melee selector. The + * spawn zone changes draw counts and odds; every weapon class remains + * reachable everywhere. Utility tools and control gear keep their sources. * - * One datum per loot theme (armory, plunder, ...), owning everything the - * theme hands out and everything defending it: the four tier tables - * consumed by /obj/structure/closet/crate/zone_loot (zone_loot.dm), plus, - * by file colocation, the theme's crate subtype and the guard-marker - * subtypes (/obj/effect/zone_mobs) that watch over it. - * - * A theme is ONE pool split by how good the item is, not by where you are. - * Overmap zone never selects a table. It only decides how many draws a - * cache gets and how the odds lean across the four tiers (see zone_loot.dm). - * Every band can reach every tier; deep space just reaches the top of it far - * more often. This mirrors how zones already scale planet ore, fauna and - * weather (voidcrew/_DEFINES/overmap_zones.dm): amounts and odds, not kinds. - * - * Why a datum instead of vars on the crate subtype (the old shape): the - * cache weights are hand-tuned against outpost shop prices ("the gamble - * channel and the certainty channel stay on one curve") and against the - * theme's own guard tables. Keeping tables, guards and the SKU - * cross-reference list in one file per theme makes that curve a same-screen - * edit instead of a comment maintained across three files, and gives the - * loot audit unit test (code/modules/unit_tests/voidcrew_loot.dm) a single - * registry to walk. - * - * CYBERWARE (2026-08-05): the parlor roster rides these tables the same way - * the gun blueprints do. The shop is the certainty channel, the caches are - * the gamble channel, and both sit on one curve. Chrome enters at the tier - * that matches what Splice charges for it - * (voidcrew/modules/cyberware/ripperdoc_catalog.dm): - * - Street chrome (400-2000cr) -> common / uncommon - * - Pro chrome (2800-3600cr, 1-2 vouchers) -> uncommon / prime - * - Military chrome (2-4 vouchers) -> the long tail of prime, weight 2-3 - * Two exclusions, both deliberate: Legend chrome (Cascade, Redline, Governor - * Delete) is the authored chase and is always on Splice's shelf, and the - * Piledriver is the black market's contract-only reward, neither drops. The - * Skyhook Wrist is the mirror of that: it has no SKU anywhere, so a cache is - * the only place it exists (expedition prime). - * - * A found piece is not a worn piece. Chrome still installs only at a Chrome - * Cradle or through organ-manipulation surgery, and it still costs capacity, - * so a cache pays the hardware and never the seat in the chair. - * - * HARD RULE (design): no megafauna anywhere in the zone system, not in - * guard tables, not as bosses, never as generic cache guards in new maps. - * Megafauna (including this fork's /mob/living/basic/boss tier, see - * ismegafauna()) belong to planet apex spawns and to the handful of - * purpose-built boss-arena ruins enumerated in the unit test's whitelist - * (blood-drunk shrines, the hierophant arena, the wendigo cave, ...). - * "Harder versions of normal mobs" (elites, ancients, captains) are the - * ceiling everywhere else; the unit test enforces this for the tables and - * for every shipped ruin .dmm. + * Weapon extras have weight one against weight ten per main entry. Chrome + * weapons belong here too; other implants remain in the nonweapon tiers. + * Guards stay colocated with their theme. Megafauna never enter these + * guard tables: only the purpose-built arena ruins may place them. */ /datum/loot_theme /// Debug/audit label var/name + /// Per-container gun percentages, indexed by ZONE_GREEN/YELLOW/RED. + var/list/gun_chance + /// Main gun family; each class also accepts the theme's retained variants. + var/gun_pool = LOOT_GUN_POOL_ARMORY + /// Class -> weighted extra weapon paths, separate from all nonweapon draws. + var/list/gun_extras + var/list/melee_extras /// Weighted tier tables (typepath -> weight). Bread and butter; what a /// cache pays most of the time in any band. var/list/loot_common @@ -75,6 +40,10 @@ /// audit unit test can walk the curve. var/list/theme_skus +/// Returns a disposable pool. Freight never inherits themed weapon extras. +/datum/loot_theme/proc/get_weapon_pool(gun, weapon_class, freight = FALSE) + return get_cache_weapon_pool(gun, weapon_class, freight ? LOOT_GUN_POOL_FREIGHT : gun_pool, freight ? null : (gun ? gun_extras : melee_extras)) + /// theme typepath -> singleton instance; built once at world init GLOBAL_LIST_INIT(loot_themes, init_loot_themes()) diff --git a/voidcrew/modules/loot/themes/armory.dm b/voidcrew/modules/loot/themes/armory.dm index a1e3e4c100a..9a0af19267b 100644 --- a/voidcrew/modules/loot/themes/armory.dm +++ b/voidcrew/modules/loot/themes/armory.dm @@ -15,6 +15,118 @@ /datum/loot_theme/armory name = "armory" + gun_chance = LOOT_GUN_CHANCE_ARMORY + gun_pool = LOOT_GUN_POOL_ARMORY + gun_extras = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/automatic/pistol/m1911 = 1, + /obj/item/gun/ballistic/automatic/pistol/m1911/no_mag = 1, + /obj/item/gun/ballistic/automatic/pistol/no_mag = 1, + /obj/item/gun/ballistic/automatic/pistol/toy = 1, + /obj/item/gun/ballistic/automatic/toy = 1, + /obj/item/gun/ballistic/bow = 1, + /obj/item/gun/ballistic/revolver/c38 = 1, + /obj/item/gun/ballistic/revolver/nagant = 1, + /obj/item/gun/ballistic/revolver/russian = 1, + /obj/item/gun/ballistic/revolver/russian/soul = 1, + /obj/item/gun/ballistic/rifle/boltaction/pipegun = 1, + /obj/item/gun/ballistic/rifle/boltaction/pipegun/pistol = 1, + /obj/item/gun/ballistic/shotgun/doublebarrel/breacherslug = 1, + /obj/item/gun/ballistic/shotgun/doublebarrel/slugs = 1, + /obj/item/gun/ballistic/shotgun/musket = 1, + /obj/item/gun/ballistic/shotgun/toy = 1, + /obj/item/gun/ballistic/shotgun/toy/crossbow = 1, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/ar = 1, + /obj/item/gun/ballistic/automatic/pistol/aps = 1, + /obj/item/gun/ballistic/automatic/pistol/deagle = 1, + /obj/item/gun/ballistic/automatic/pistol/deagle/camo = 1, + /obj/item/gun/ballistic/automatic/pistol/deagle/gold = 1, + /obj/item/gun/ballistic/automatic/smartgun = 1, + /obj/item/gun/ballistic/revolver/golden = 1, + /obj/item/gun/ballistic/rifle/boltaction = 1, + /obj/item/gun/ballistic/rifle/boltaction/donkrifle = 1, + /obj/item/gun/ballistic/rifle/boltaction/pipegun/prime = 1, + /obj/item/gun/ballistic/rifle/boltaction/prime = 1, + /obj/item/gun/ballistic/rifle/rebarxbow/forced = 1, + /obj/item/gun/ballistic/shotgun = 1, + /obj/item/gun/ballistic/shotgun/lethal = 1, + /obj/item/gun/ballistic/shotgun/riot = 1, + /obj/item/gun/energy/e_gun/dragnet = 1, + /obj/item/gun/energy/e_gun/old = 1, + /obj/item/gun/energy/laser/luxurypaywall = 1, + /obj/item/gun/energy/laser/retro/old = 1, + /obj/item/gun/syringe/rapidsyringe = 1, + /obj/item/gun/syringe/syndicate = 1, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/ballistic/automatic/m90/unrestricted = 1, + /obj/item/gun/energy/e_gun/hos = 1, + /obj/item/gun/energy/e_gun/nuclear = 1, + /obj/item/gun/energy/laser/carbine/cybersun/unrestricted = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_GREEN = list( + /obj/item/bambostaff = 1, + /obj/item/knife/combat/bone = 1, + /obj/item/knife/envy = 1, + /obj/item/lead_pipe = 1, + /obj/item/spear/bamboospear = 1, + /obj/item/spear/bonespear = 1, + /obj/item/spear/military = 1, + /obj/item/storage/case/cyberware/scrapper = 1, + /obj/item/tailclub = 1, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/chainsaw = 1, + /obj/item/claymore/cutlass/old = 1, + /obj/item/claymore/weak = 1, + /obj/item/crowbar/large/twenty_force = 1, + /obj/item/fireaxe/boardingaxe = 1, + /obj/item/fireaxe/boneaxe = 1, + /obj/item/melee/parsnip_sabre = 1, + /obj/item/nullrod/claymore = 1, + /obj/item/nullrod/claymore/chainsaw_sword = 1, + /obj/item/nullrod/claymore/darkblade = 1, + /obj/item/nullrod/claymore/glowing = 1, + /obj/item/nullrod/claymore/katana = 1, + /obj/item/nullrod/claymore/multiverse = 1, + /obj/item/nullrod/claymore/saber = 1, + /obj/item/nullrod/claymore/saber/pirate = 1, + /obj/item/nullrod/claymore/saber/red = 1, + /obj/item/nullrod/hypertool = 1, + /obj/item/nullrod/vibro = 1, + /obj/item/nullrod/vibro/spellblade = 1, + /obj/item/nullrod/vibro/talking = 1, + /obj/item/pen/edagger = 1, + /obj/item/shield/buckler = 1, + /obj/item/shield/buckler/parmula = 1, + /obj/item/shield/kite = 1, + /obj/item/shield/riot = 1, + /obj/item/shield/riot/phalanx_buckler = 1, + /obj/item/shield/riot/tele = 1, + /obj/item/shield/roman = 1, + /obj/item/shovel/serrated = 1, + /obj/item/spear/grey_tide = 1, + /obj/item/switchblade = 1, + ), + LOOT_WEAPON_RED = list( + /obj/item/clothing/gloves/knock_knock = 1, + /obj/item/cursed_katana = 1, + /obj/item/energy_katana = 1, + /obj/item/house_edge = 1, + /obj/item/melee/energy/sword/saber/blue = 1, + /obj/item/melee/energy/sword/saber/purple = 1, + /obj/item/melee/energy/sword/saber/red = 1, + /obj/item/melee/greykingsword = 1, + /obj/item/shield/energy = 1, + /obj/item/storage/toolbox/mechanical/old/clean = 1, + /obj/item/veilrender = 1, + /obj/item/veilrender/vealrender = 1, + ), + ) guard_themes = list( /obj/effect/zone_mobs/robot, /obj/effect/zone_mobs/robot/boss, @@ -35,7 +147,6 @@ /obj/item/clothing/suit/armor/vest = 10, /obj/item/clothing/head/helmet/sec = 10, /obj/item/gun/energy/disabler = 7, - /obj/item/gun/ballistic/rifle/boltaction/surplus = 7, /obj/item/flashlight/seclite = 7, /obj/item/grenade/flashbang = 6, /obj/item/clothing/glasses/hud/security/sunglasses = 5, @@ -44,44 +155,22 @@ ) loot_uncommon = list( /obj/item/clothing/suit/armor/bulletproof = 10, - /obj/item/gun/ballistic/automatic/pistol/m1911 = 8, /obj/item/ammo_box/magazine/m45 = 8, - /obj/item/gun/ballistic/shotgun/riot = 6, - /obj/item/gun/ballistic/automatic/ar = 6, - /obj/item/gun/ballistic/automatic/pistol/deagle = 5, - /obj/item/shield/riot = 6, /obj/item/clothing/suit/armor/vest/marine = 5, /obj/item/clothing/gloves/combat = 5, /obj/item/clothing/shoes/combat = 5, - /obj/item/gun/ballistic/rifle/boltaction = 4, /obj/item/clothing/under/syndicate/tacticool = 3, /obj/item/ship_parts/combat = 7, - // issue chrome: subdermal impact weave, and the knuckle set cased as - // a matched pair (both halves, one per arm) + // Issue chrome: subdermal impact weave. /obj/item/organ/cyberimp/cyberware/dermal_mesh = 4, - /obj/item/storage/case/cyberware/scrapper = 3, ) loot_prime = list( /obj/item/clothing/suit/armor/riot = 9, /obj/item/clothing/head/helmet/toggleable/riot = 8, - /obj/item/gun/energy/laser = 8, - /obj/item/gun/ballistic/shotgun/automatic/combat = 6, - /obj/item/gun/energy/laser/scatter = 6, - // finished blueprint guns, prime only: the garrison's own issued - // hardware, not the schematics the shops sell - /obj/item/gun/ballistic/automatic/wt550 = 4, - /obj/item/gun/energy/laser/carbine = 4, /obj/item/clothing/suit/armor/laserproof = 6, /obj/item/clothing/head/helmet/marine = 6, /obj/item/storage/belt/military/assault = 5, - /obj/item/gun/energy/e_gun = 4, - /obj/item/gun/ballistic/rifle/boltaction/prime = 3, - /obj/item/shield/riot/tele = 3, /obj/item/clothing/suit/armor/heavy = 2, - // the deepest slice of the theme, previously reachable only through - // a sealed cache: now the long tail of prime, open to any band - /obj/item/gun/ballistic/rifle/sniper_rifle = 3, - /obj/item/gun/energy/lasercannon/unrestricted = 3, // the back room's chrome, all voucher-grade at the parlor: ceramic // plate, the targeting link, and the truss that keeps limbs attached /obj/item/organ/cyberimp/cyberware/slabskin = 3, @@ -95,8 +184,6 @@ /obj/item/clothing/mask/whistle/sergeants = 3, /obj/item/handloaders_vise = 3, /obj/item/clothing/glasses/sunglasses/marksmans_cant = 2, - /obj/item/shield/riot/phalanx_buckler = 2, - /obj/item/clothing/gloves/knock_knock = 1, /obj/item/garrison_standard = 1, ) diff --git a/voidcrew/modules/loot/themes/expedition.dm b/voidcrew/modules/loot/themes/expedition.dm index 2d6e8bde8bf..5dfa9fbb1a7 100644 --- a/voidcrew/modules/loot/themes/expedition.dm +++ b/voidcrew/modules/loot/themes/expedition.dm @@ -17,6 +17,20 @@ /datum/loot_theme/expedition name = "expedition" + gun_chance = LOOT_GUN_CHANCE_EXPEDITION + gun_pool = LOOT_GUN_POOL_ARMORY + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/rifle/boltaction = 1, + /obj/item/gun/ballistic/rifle/boltaction/prime = 1, + /obj/item/gun/ballistic/shotgun/riot = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/kinetic_crusher = 1, + ), + ) guard_themes = list( /obj/effect/zone_mobs/wildlife, /obj/effect/zone_mobs/wildlife/boss, @@ -31,8 +45,6 @@ ) loot_common = list( /obj/item/clothing/suit/hooded/explorer = 10, - /obj/item/knife/combat/survival = 9, - /obj/item/gun/ballistic/rifle/boltaction/surplus = 8, /obj/item/gps/mining = 8, /obj/item/flashlight/flare = 8, /obj/item/stack/marker_beacon/thirty = 6, @@ -50,9 +62,6 @@ /obj/item/pickaxe/diamond = 7, /obj/item/t_scanner/adv_mining_scanner/lesser = 6, /obj/item/wormhole_jaunter = 6, - // wildlife stoppers: the KA is a mining tool first, these are not - /obj/item/gun/ballistic/shotgun/riot = 6, - /obj/item/gun/ballistic/rifle/boltaction = 5, /obj/item/survivalcapsule = 5, /obj/item/clothing/suit/hooded/cloak/goliath = 4, /obj/item/gun/energy/plasmacutter = 4, @@ -67,14 +76,8 @@ /obj/item/clothing/shoes/bhop = 7, /obj/item/resonator = 7, /obj/item/gun/energy/plasmacutter/adv = 6, - /obj/item/gun/ballistic/shotgun/automatic/combat = 6, - /obj/item/gun/energy/laser = 5, - /obj/item/kinetic_crusher = 5, /obj/item/clothing/glasses/heat = 4, /obj/item/clothing/suit/hooded/cloak/drake = 2, - // the deepest slice of the theme, previously reachable only through - // a sealed cache: now the long tail of prime, open to any band - /obj/item/gun/ballistic/rifle/boltaction/prime = 5, // top-end survey chrome: eyes that pulse ore through rock, the wrist // winch that exists nowhere else, and four tiles of standing jump /obj/item/organ/eyes/robotic/cyberware/prospector = 3, @@ -128,9 +131,6 @@ /// tendril chest, the cache they guard is the prize. /obj/effect/zone_mobs/wildlife/boss name = "zone mob spawner (wildlife boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( /mob/living/basic/mining/goliath = 1, ) diff --git a/voidcrew/modules/loot/themes/industrial.dm b/voidcrew/modules/loot/themes/industrial.dm index 09dfcb318a6..b9d65961ebb 100644 --- a/voidcrew/modules/loot/themes/industrial.dm +++ b/voidcrew/modules/loot/themes/industrial.dm @@ -16,6 +16,20 @@ /datum/loot_theme/industrial name = "industrial" + gun_chance = LOOT_GUN_CHANCE_INDUSTRIAL + gun_pool = LOOT_GUN_POOL_ARMORY + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/proto/unrestricted = 1, + /obj/item/gun/energy/ionrifle = 1, + /obj/item/gun/energy/ionrifle/carbine = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/cyberware_pair_case/gorilla_arms = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/robot, /obj/effect/zone_mobs/robot/boss) theme_skus = list( /datum/shop_sku/skunk/capacitor, @@ -39,8 +53,6 @@ /obj/item/stock_parts/matter_bin/adv = 6, /obj/item/storage/toolbox/mechanical = 5, /obj/item/weldingtool/largetank = 5, - // the line's own prototype SMG, never finished, still fires - /obj/item/gun/ballistic/automatic/proto/unrestricted = 5, /obj/item/stock_parts/power_store/cell/high = 4, /obj/item/stack/sheet/plasteel/twenty = 4, ) @@ -54,9 +66,6 @@ /obj/item/stock_parts/matter_bin/super = 6, /obj/item/stock_parts/power_store/cell/super = 5, /obj/item/storage/part_replacer = 4, - // asset-denial kit, and the answer to this theme's own hivebot - // guards: ion weapons wreck automation - /obj/item/gun/energy/ionrifle/carbine = 6, /obj/item/circuitboard/machine/autolathe = 4, /obj/item/flamethrower/full = 3, /obj/item/stack/sheet/mineral/gold/fifty = 3, @@ -75,14 +84,11 @@ /obj/item/stock_parts/matter_bin/bluespace = 6, /obj/item/stack/sheet/mineral/diamond/five = 6, /obj/item/stock_parts/power_store/cell/hyper = 5, - /obj/item/gun/energy/ionrifle = 6, /obj/item/stack/sheet/bluespace_crystal = 4, /obj/item/construction/rcd = 3, /obj/item/stack/sheet/mineral/diamond/fifty = 2, - // hot-work and heavy-lift chrome: a body-wide coolant loop, and the - // cased myomer arm pair (both halves, one per arm) + // Hot-work chrome: a body-wide coolant loop. /obj/item/organ/cyberimp/cyberware/coolant = 3, - /obj/item/cyberware_pair_case/gorilla_arms = 2, ) // One-of-a-kind authored prizes, drawn as the fourth tier from any // band (weight = how shallow the item used to sit: 3 was reachable @@ -123,9 +129,6 @@ /obj/effect/zone_mobs/robot/boss name = "zone mob spawner (robot boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 2) mobs_green = list( /mob/living/basic/hivebot/strong = 1, ) diff --git a/voidcrew/modules/loot/themes/medical.dm b/voidcrew/modules/loot/themes/medical.dm index 6674e250886..42af4e43537 100644 --- a/voidcrew/modules/loot/themes/medical.dm +++ b/voidcrew/modules/loot/themes/medical.dm @@ -16,6 +16,14 @@ /datum/loot_theme/medical name = "medical" + gun_chance = LOOT_GUN_CHANCE_MEDICAL + gun_pool = LOOT_GUN_POOL_ARMORY + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/shotgun/riot = 1, + /obj/item/gun/energy/e_gun/mini = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/undead, /obj/effect/zone_mobs/undead/boss) theme_skus = list( /datum/shop_sku/black_market/tactical_medkit, @@ -40,9 +48,6 @@ /obj/item/storage/pill_bottle/multiver = 5, /obj/item/healthanalyzer = 4, /obj/item/reagent_containers/cup/bottle/salglu_solution = 4, - // the ward's own enforcement locker: a quarantine that stopped - // answering was a quarantine somebody had to hold by force - /obj/item/gun/energy/e_gun/mini = 5, // cheapest thing on the parlor's shelf and the one piece of chrome a // clinic would actually stock: it replaces a stomach /obj/item/organ/cyberimp/cyberware/gastro = 4, @@ -59,8 +64,6 @@ /obj/item/reagent_containers/cup/bottle/atropine = 5, /obj/item/storage/pill_bottle/penacid = 5, /obj/item/defibrillator = 4, - /obj/item/gun/energy/laser = 5, - /obj/item/gun/ballistic/shotgun/riot = 4, /obj/item/healthanalyzer/advanced = 4, /obj/item/storage/medkit/tactical_lite = 4, /obj/item/organ/eyes/robotic/basic = 3, @@ -76,8 +79,6 @@ /obj/item/reagent_containers/hypospray/medipen/atropine = 7, /obj/item/reagent_containers/hypospray/medipen/penthrite = 6, /obj/item/defibrillator/compact = 6, - /obj/item/gun/ballistic/shotgun/automatic/combat = 5, - /obj/item/gun/energy/laser/scatter = 4, /obj/item/storage/medkit/tactical = 5, /obj/item/reagent_containers/hypospray/medipen/survival = 5, /obj/item/organ/heart/cybernetic = 4, diff --git a/voidcrew/modules/loot/themes/occult.dm b/voidcrew/modules/loot/themes/occult.dm index 0188b8b4325..67dad562b7a 100644 --- a/voidcrew/modules/loot/themes/occult.dm +++ b/voidcrew/modules/loot/themes/occult.dm @@ -25,6 +25,21 @@ /datum/loot_theme/occult name = "occult" + gun_chance = LOOT_GUN_CHANCE_OCCULT + gun_pool = LOOT_GUN_POOL_PLUNDER + gun_extras = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/rifle/boltaction/surplus = 1, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/energy/laser/retro = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/knife/bloodletter = 1, + ), + ) guard_themes = list( /obj/effect/zone_mobs/cult, /obj/effect/zone_mobs/cult/boss, @@ -41,8 +56,6 @@ /obj/item/reagent_containers/cup/glass/bottle/holywater = 6, /obj/item/book/bible = 6, /obj/item/coin/gold = 5, - // the grave-robber's rifle: old, ugly, still kills what walks - /obj/item/gun/ballistic/rifle/boltaction/surplus = 6, /obj/item/toy/cards/deck/tarot = 5, /obj/item/reagent_containers/cup/glass/bottle/wine = 4, ) @@ -50,7 +63,6 @@ /obj/item/statuebust = 8, /obj/item/flashlight/lantern = 7, /obj/item/ectoplasm = 6, - /obj/item/gun/energy/laser/retro = 6, /obj/item/clothing/suit/chaplainsuit/bishoprobe = 5, /obj/item/clothing/head/chaplain/bishopmitre = 5, /obj/item/toy/cards/deck/tarot/haunted = 4, @@ -65,17 +77,12 @@ /obj/item/stack/sheet/mineral/gold/fifty = 6, /obj/item/stack/sheet/mineral/silver/fifty = 6, /obj/item/clothing/suit/armor/riot/knight = 5, - /obj/item/gun/energy/laser/hellgun = 5, - /obj/item/knife/bloodletter = 3, /obj/item/soulstone/anybody/purified = 3, // the deepest slice of the theme, previously reachable only through // a sealed cache: now the long tail of prime, open to any band /obj/item/book/granter/action/spell/sacredflame = 2, - /obj/item/gun/magic/staff/chaos = 2, - /obj/item/mjollnir = 2, /obj/item/book/granter/action/spell/fireball = 1, /obj/item/book/granter/action/spell/random = 1, - /obj/item/his_grace = 1, ) // One-of-a-kind authored prizes, drawn as the fourth tier from any // band (weight = how shallow the item used to sit: 3 was reachable @@ -114,9 +121,6 @@ /obj/effect/zone_mobs/cult/boss name = "zone mob spawner (cult boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( /mob/living/basic/construct/artificer/hostile = 1, ) @@ -147,9 +151,6 @@ /obj/effect/zone_mobs/undead/boss name = "zone mob spawner (undead boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( /mob/living/basic/skeleton/templar = 1, ) diff --git a/voidcrew/modules/loot/themes/plunder.dm b/voidcrew/modules/loot/themes/plunder.dm index 7dee57694e0..ea2a1067b3d 100644 --- a/voidcrew/modules/loot/themes/plunder.dm +++ b/voidcrew/modules/loot/themes/plunder.dm @@ -13,11 +13,32 @@ /datum/loot_theme/plunder name = "plunder" + gun_chance = LOOT_GUN_CHANCE_PLUNDER + gun_pool = LOOT_GUN_POOL_PLUNDER + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/mini_uzi = 1, + /obj/item/gun/ballistic/automatic/pistol/deagle = 1, + /obj/item/gun/ballistic/revolver/mateba = 1, + /obj/item/gun/ballistic/shotgun/musket/no_quarter = 1, + /obj/item/gun/magic/hook/marlinspike = 1, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/ballistic/shotgun/automatic/combat = 1, + /obj/item/organ/cyberimp/arm/toolkit/cyberware/bunker_buster = 1, + /obj/item/organ/cyberimp/arm/toolkit/cyberware/ronin = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/claymore/cutlass/parley = 1, + /obj/item/cyberware_pair_case/mantis_blades = 1, + /obj/item/heave_ho = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/pirate, /obj/effect/zone_mobs/pirate/boss) theme_skus = list( /datum/shop_sku/black_market/rotating/mateba, - /datum/shop_sku/black_market/bulldog_blueprint, - /datum/shop_sku/outfitter/carbine_blueprint, /datum/shop_sku/ripperdoc/gecko, /datum/shop_sku/ripperdoc/mantis, /datum/shop_sku/ripperdoc/ronin, @@ -31,23 +52,16 @@ /obj/item/clothing/glasses/eyepatch = 6, /obj/item/clothing/head/costume/pirate/bandana = 6, /obj/item/coin/gold/doubloon = 5, - /obj/item/gun/ballistic/revolver/c38/detective = 5, - /obj/item/knife/combat = 4, /obj/item/toy/cards/deck = 4, ) loot_uncommon = list( /obj/item/stack/spacecash/c500 = 10, - /obj/item/gun/ballistic/shotgun/doublebarrel = 7, /obj/item/storage/box/lethalshot = 7, - /obj/item/gun/ballistic/revolver = 6, - /obj/item/gun/ballistic/automatic/tommygun = 6, - /obj/item/gun/ballistic/automatic/mini_uzi = 5, /obj/item/ammo_box/a357 = 6, /obj/item/stack/sheet/mineral/gold = 5, /obj/item/clothing/head/costume/pirate/armored = 4, /obj/item/clothing/suit/costume/pirate/armored = 4, /obj/item/reagent_containers/cup/glass/bottle/rum/aged = 4, - /obj/item/blueprint/gun/laser_carbine = 3, /obj/item/ship_parts/combat = 7, // boarder's palms: your hands clamp shut on your gun when you go // down, and tables stop being furniture @@ -55,26 +69,11 @@ ) loot_prime = list( /obj/item/stack/spacecash/c1000 = 9, - /obj/item/gun/ballistic/revolver/mateba = 5, - /obj/item/gun/ballistic/shotgun/automatic/combat = 5, - /obj/item/gun/ballistic/automatic/pistol/deagle = 5, - // finished blueprint gun, prime only: boarding hardware nobody - // filed a schematic for - /obj/item/gun/ballistic/shotgun/bulldog/unrestricted = 4, - /obj/item/melee/energy/sword/pirate = 4, /obj/item/stack/sheet/mineral/diamond = 4, /obj/item/reagent_containers/cup/glass/bottle/absinthe/premium = 4, /obj/item/clothing/suit/costume/pirate/captain/armored = 3, /obj/item/clothing/head/costume/pirate/captain = 3, - /obj/item/blueprint/gun/bulldog = 3, /obj/item/stack/spacecash/c10000 = 2, - // the captain's cut, all of it voucher-grade chrome: the cased blade - // pair, the arm gun nobody can disarm you of, and the rocket pod. - // Both weapon pieces are parlor-fed. The crate pays what's loaded in - // them and no more, so reloads still mean a trip to Splice - /obj/item/cyberware_pair_case/mantis_blades = 3, - /obj/item/organ/cyberimp/arm/toolkit/cyberware/ronin = 3, - /obj/item/organ/cyberimp/arm/toolkit/cyberware/bunker_buster = 2, ) // One-of-a-kind authored prizes, drawn as the fourth tier from any // band (weight = how shallow the item used to sit: 3 was reachable @@ -83,10 +82,6 @@ /obj/item/reagent_containers/cup/glass/bottle/bottomless_ration = 3, /obj/item/toy/cards/deck/cheats = 3, /obj/item/clothing/glasses/eyepatch/fences_eye = 2, - /obj/item/gun/magic/hook/marlinspike = 2, - /obj/item/claymore/cutlass/parley = 1, - /obj/item/gun/ballistic/shotgun/musket/no_quarter = 1, - /obj/item/heave_ho = 1, ) /obj/structure/closet/crate/zone_loot/plunder @@ -121,9 +116,6 @@ /// The pirate setpiece: a captain holding the vault, a proper boss in the red. /obj/effect/zone_mobs/pirate/boss name = "zone mob spawner (pirate boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( /mob/living/basic/trooper/pirate/faction/grey/captain = 1, /mob/living/basic/trooper/pirate/faction/skeleton/captain = 1, diff --git a/voidcrew/modules/loot/themes/research.dm b/voidcrew/modules/loot/themes/research.dm index d3d145dd4c2..dffb832ac37 100644 --- a/voidcrew/modules/loot/themes/research.dm +++ b/voidcrew/modules/loot/themes/research.dm @@ -12,9 +12,28 @@ /datum/loot_theme/research name = "research" + gun_chance = LOOT_GUN_CHANCE_RESEARCH + gun_pool = LOOT_GUN_POOL_SYNDICATE + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/energy/e_gun/mini = 1, + /obj/item/gun/energy/laser/retro = 1, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/energy/event_horizon = 1, + /obj/item/gun/energy/laser/carbine = 1, + /obj/item/gun/energy/lasercannon/unrestricted = 1, + /obj/item/gun/energy/recharge/ebow = 1, + /obj/item/gun/energy/temperature/unrestricted = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/singularityhammer = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/bug, /obj/effect/zone_mobs/bug/boss) theme_skus = list( - /datum/shop_sku/outfitter/carbine_blueprint, /datum/shop_sku/ripperdoc/doppler, /datum/shop_sku/ripperdoc/rigger, ) @@ -27,8 +46,6 @@ /obj/item/petri_dish = 6, /obj/item/experi_scanner = 5, /obj/item/research_notes/loot/tiny = 5, - // containment sidearm: every lab that kept specimens kept one - /obj/item/gun/energy/e_gun/mini = 5, /obj/item/stack/ore/bluespace_crystal/artificial = 4, ) loot_uncommon = list( @@ -40,11 +57,7 @@ /obj/item/slime_extract/gold = 5, /obj/item/clothing/glasses/night = 5, /obj/item/research_notes/loot/small = 5, - /obj/item/gun/energy/laser/retro = 6, - /obj/item/gun/energy/recharge/ebow = 4, /obj/item/reagent_containers/cup/beaker/noreact = 4, - /obj/item/raw_anomaly_core/random = 4, - /obj/item/blueprint/gun/laser_carbine = 3, /obj/item/ship_parts/science = 7, // lab hardware worn instead of carried: a sonar transceiver socketed // into the bone behind the ear, reading movement through walls @@ -52,6 +65,7 @@ ) loot_prime = list( /obj/item/stack/ore/bluespace_crystal/refined = 10, + /obj/item/raw_anomaly_core/random = 4, /obj/item/stock_parts/capacitor/super = 8, /obj/item/stock_parts/micro_laser/ultra = 8, /obj/item/stock_parts/matter_bin/super = 7, @@ -59,10 +73,6 @@ /obj/item/slime_extract/adamantine = 5, /obj/item/research_notes/loot/medium = 5, /obj/item/clothing/shoes/bhop = 4, - /obj/item/gun/energy/xray = 4, - // finished blueprint gun, prime only: the annex built its own - /obj/item/gun/energy/laser/carbine = 4, - /obj/item/gun/energy/temperature/unrestricted = 4, /obj/item/reagent_containers/cup/beaker/bluespace = 3, /obj/item/assembly/signaler/anomaly/grav = 3, /obj/item/storage/backpack/holding = 2, @@ -70,11 +80,8 @@ // a sealed cache: now the long tail of prime, open to any band /obj/item/stock_parts/matter_bin/bluespace = 6, /obj/item/research_notes/loot/big = 4, - /obj/item/gun/energy/lasercannon/unrestricted = 3, /obj/item/book/granter/action/spell/charge = 2, - /obj/item/gun/magic/wand/polymorph = 2, /obj/item/research_notes/loot/genius = 2, - /obj/item/singularityhammer = 2, // the annex's own interface work: a skull jack that flies a ship from // anywhere aboard it, as long as the hull still has a helm console /obj/item/organ/cyberimp/cyberware/rigger = 2, @@ -116,9 +123,6 @@ /obj/effect/zone_mobs/bug/boss name = "zone mob spawner (bug boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( /mob/living/basic/spider/giant/hunter = 1, ) diff --git a/voidcrew/modules/loot/themes/syndicate.dm b/voidcrew/modules/loot/themes/syndicate.dm index e63595fc913..1f63090058e 100644 --- a/voidcrew/modules/loot/themes/syndicate.dm +++ b/voidcrew/modules/loot/themes/syndicate.dm @@ -6,11 +6,6 @@ // TODO: review/balance-pass all four tiers: first-draft weights and // contents, never playtested. Tune weights against the black-market voucher // prices in shop_catalog_black_market.dm. -// Weapon blueprints are seeded across both channels: the outfitter sells the -// c20r/wt550/carbine schematics (uncommon-tier), the black market sells the -// l6_saw/sniper/bulldog three (prime-tier), the same blueprints ride these -// cache tables at matching tiers. Craft them from the recipe anywhere, once -// you have the part and a firing pin (modules/weapons_bench/blueprint.dm). // Cyberware rides the same two channels: the parlor's smuggling cavity and // door spike sit at uncommon, and the three voucher-only pieces an operative // would carry (cloak weave, wire, reflex shunt) sit at the bottom of prime. @@ -18,20 +13,33 @@ /datum/loot_theme/syndicate name = "syndicate" + gun_chance = LOOT_GUN_CHANCE_SYNDICATE + gun_pool = LOOT_GUN_POOL_SYNDICATE + gun_extras = list( + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/pistol/deagle = 1, + /obj/item/gun/ballistic/revolver = 1, + /obj/item/gun/ballistic/revolver/c38/housecall = 1, + ), + LOOT_WEAPON_RED = list( + /obj/item/gun/ballistic/rifle/sniper_rifle = 1, + /obj/item/gun/ballistic/shotgun/automatic/combat = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/knife/understudy = 1, + /obj/item/organ/cyberimp/arm/toolkit/cyberware/monowire = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/syndicate, /obj/effect/zone_mobs/syndicate/boss) theme_skus = list( /datum/shop_sku/black_market/emag, /datum/shop_sku/black_market/tactical_medkit, - /datum/shop_sku/black_market/saw_blueprint, - /datum/shop_sku/black_market/sniper_blueprint, - /datum/shop_sku/black_market/bulldog_blueprint, /datum/shop_sku/black_market/mystery_cache, /datum/shop_sku/black_market/mystery_cache_rare, /datum/shop_sku/black_market/rare/energy_sword, /datum/shop_sku/general/rotating/freight_crate, - /datum/shop_sku/outfitter/smg_blueprint, - /datum/shop_sku/outfitter/wt550_blueprint, - /datum/shop_sku/outfitter/carbine_blueprint, /datum/shop_sku/ripperdoc/cargo_cavity, /datum/shop_sku/ripperdoc/icepick, /datum/shop_sku/ripperdoc/slipwire, @@ -41,9 +49,7 @@ loot_common = list( /obj/item/soap/syndie = 10, /obj/item/storage/fancy/cigarettes/cigpack_syndicate = 10, - /obj/item/knife/combat = 8, /obj/item/ammo_box/magazine/m9mm = 8, - /obj/item/gun/ballistic/automatic/pistol = 7, /obj/item/suppressor = 6, /obj/item/encryptionkey/syndicate = 6, /obj/item/grenade/empgrenade = 5, @@ -51,16 +57,10 @@ ) loot_uncommon = list( /obj/item/grenade/c4 = 6, - /obj/item/gun/ballistic/automatic/mini_uzi = 6, /obj/item/ammo_box/a357 = 5, - /obj/item/gun/ballistic/automatic/pistol/deagle = 4, - /obj/item/gun/ballistic/revolver = 4, /obj/item/card/id/advanced/chameleon = 4, /obj/item/clothing/shoes/chameleon/noslip = 4, /obj/item/clothing/glasses/thermal/syndi = 3, - /obj/item/blueprint/gun/c20r = 4, - /obj/item/blueprint/gun/wt550 = 3, - /obj/item/blueprint/gun/laser_carbine = 3, /obj/item/ship_parts/combat = 8, // courier chrome: a shielded rib compartment scanners read past, and // a wrist spike for door motors and turret IFF @@ -68,25 +68,11 @@ /obj/item/organ/cyberimp/arm/toolkit/cyberware/icepick = 3, ) loot_prime = list( - /obj/item/gun/ballistic/shotgun/automatic/combat = 5, - /obj/item/melee/energy/sword/saber = 4, - // finished blueprint gun, prime only: an operative's issued weapon, - // not the schematic the black market fences - /obj/item/gun/ballistic/automatic/c20r/unrestricted = 4, /obj/item/pen/sleepy = 4, /obj/item/grenade/syndieminibomb = 3, /obj/item/card/emag = 2, - /obj/item/blueprint/gun/l6_saw = 3, - /obj/item/blueprint/gun/sniper_rifle = 3, - /obj/item/blueprint/gun/bulldog = 3, - // the deepest slice of the theme, previously reachable only through - // a sealed cache: now the long tail of prime, open to any band - /obj/item/gun/ballistic/automatic/l6_saw/unrestricted = 3, - /obj/item/gun/ballistic/rifle/sniper_rifle = 3, - // operative chrome, 2-4 vouchers over the counter: the reflex shunt, - // the wire, and the refraction weave that drops the moment you swing + // Operative chrome: the reflex shunt and refraction weave. /obj/item/organ/cyberimp/cyberware/slipwire = 3, - /obj/item/organ/cyberimp/arm/toolkit/cyberware/monowire = 3, /obj/item/organ/cyberimp/cyberware/ghostskin = 2, ) // One-of-a-kind authored prizes, drawn as the fourth tier from any @@ -95,10 +81,8 @@ loot_uniques = list( /obj/item/clothing/gloves/courier = 3, /obj/item/radio/listening_coin = 3, - /obj/item/gun/ballistic/revolver/c38/housecall = 2, /obj/item/static_cuff = 2, /obj/item/clothing/suit/hooded/cloak/second_shadow = 1, - /obj/item/knife/understudy = 1, ) /obj/structure/closet/crate/zone_loot/syndicate @@ -108,25 +92,15 @@ base_icon_state = "syndicrate" theme = /datum/loot_theme/syndicate -/// The black market's premium cache SKU (shop_catalog_black_market.dm). Same -/// tables as any other syndicate cache. It is simply packed fuller, which is -/// what the extra vouchers buy. -/obj/structure/closet/crate/zone_loot/syndicate/reinforced - name = "reinforced syndicate cache" - desc = "A matte-black drop crate in an extra layer of plating. Whatever's inside, somebody thought it was worth the postage." - bonus_draws = 2 - /// Syndicate holdouts: operatives left guarding whatever the base was for. /obj/effect/zone_mobs/syndicate name = "zone mob spawner (syndicate)" mobs_green = list( - /mob/living/basic/trooper/syndicate/melee = 10, - /mob/living/basic/trooper/syndicate/melee/sword = 4, + /mob/living/basic/trooper/syndicate/melee = 14, ) mobs_yellow = list( /mob/living/basic/trooper/syndicate/melee/sword = 8, - /mob/living/basic/trooper/syndicate/ranged = 8, - /mob/living/basic/trooper/syndicate/ranged/smg = 4, + /mob/living/basic/trooper/syndicate/ranged = 12, ) mobs_red = list( /mob/living/basic/trooper/syndicate/ranged/smg = 8, @@ -137,15 +111,11 @@ /obj/effect/zone_mobs/syndicate/boss name = "zone mob spawner (syndicate boss)" - count_green = list(1, 1) - count_yellow = list(1, 1) - count_red = list(1, 1) mobs_green = list( - /mob/living/basic/trooper/syndicate/melee/sword = 1, + /mob/living/basic/trooper/syndicate/melee = 1, ) mobs_yellow = list( - /mob/living/basic/trooper/syndicate/ranged/shotgun = 1, - /mob/living/basic/trooper/syndicate/ranged/smg = 1, + /mob/living/basic/trooper/syndicate/ranged = 2, ) mobs_red = list( /mob/living/basic/trooper/syndicate/ranged/smg/space/stormtrooper = 1, diff --git a/voidcrew/modules/loot/themes/wardrobe.dm b/voidcrew/modules/loot/themes/wardrobe.dm index d3d014b91be..e71f26a4b9e 100644 --- a/voidcrew/modules/loot/themes/wardrobe.dm +++ b/voidcrew/modules/loot/themes/wardrobe.dm @@ -13,6 +13,21 @@ /datum/loot_theme/wardrobe name = "wardrobe" + gun_chance = LOOT_GUN_CHANCE_WARDROBE + gun_pool = LOOT_GUN_POOL_PLUNDER + gun_extras = list( + LOOT_WEAPON_GREEN = list( + /obj/item/gun/ballistic/automatic/pistol/m1911 = 1, + ), + LOOT_WEAPON_YELLOW = list( + /obj/item/gun/ballistic/automatic/pistol/deagle = 1, + ), + ) + melee_extras = list( + LOOT_WEAPON_RED = list( + /obj/item/veilrender/honkrender = 1, + ), + ) guard_themes = list(/obj/effect/zone_mobs/undead, /obj/effect/zone_mobs/undead/boss) theme_skus = list( /datum/shop_sku/ripperdoc/chromatic_dermis, @@ -30,8 +45,6 @@ /obj/item/clothing/suit/hooded/wintercoat = 5, /obj/item/clothing/suit/costume/hawaiian = 4, /obj/item/clothing/neck/scarf/red = 4, - // what was in the coat pocket when the liner went down - /obj/item/gun/ballistic/revolver/c38/detective = 5, // somebody's unused parlor appointment: tattoo ink you wear under // the skin, colour and pattern set at a Cradle /obj/item/organ/cyberimp/cyberware/chromatic_dermis = 4, @@ -44,8 +57,6 @@ /obj/item/clothing/under/costume/soviet = 6, /obj/item/clothing/suit/costume/judgerobe = 5, /obj/item/clothing/gloves/tackler/combat = 5, - /obj/item/gun/ballistic/automatic/pistol/m1911 = 5, - /obj/item/gun/ballistic/automatic/tommygun = 4, /obj/item/clothing/under/rank/prisoner = 4, /obj/item/clothing/mask/gas/sechailer/swat = 4, /obj/item/ship_parts/trade = 6, @@ -58,15 +69,12 @@ /obj/item/clothing/suit/armor/vest/warden/alt = 8, /obj/item/clothing/head/helmet/knight = 7, /obj/item/clothing/head/cowboy/black/syndicate = 6, - /obj/item/gun/ballistic/automatic/pistol/deagle = 5, // the helmet is the suit's hood, it comes with it. never rolled alone /obj/item/clothing/suit/hooded/berserker = 4, // the deepest slice of the theme, previously reachable only through // a sealed cache: now the long tail of prime, open to any band /obj/item/clothing/shoes/clown_shoes/banana_shoes/combat = 2, - /obj/item/gun/magic/staff/honk = 2, /obj/item/megaphone/clown = 2, - /obj/item/veilrender/honkrender = 1, ) // One-of-a-kind authored prizes, drawn as the fourth tier from any // band (weight = how shallow the item used to sit: 3 was reachable diff --git a/voidcrew/modules/loot/uniques/medical.dm b/voidcrew/modules/loot/uniques/medical.dm index f8dfd616c5e..9623cdc8675 100644 --- a/voidcrew/modules/loot/uniques/medical.dm +++ b/voidcrew/modules/loot/uniques/medical.dm @@ -537,7 +537,7 @@ patient.updatehealth() patient.set_heartattack(FALSE) patient.grab_ghost() - patient.revive() + patient.revive_from("meridian_heart") patient.emote("gasp") patient.set_jitter_if_lower(200 SECONDS) SEND_SIGNAL(patient, COMSIG_LIVING_MINOR_SHOCK) @@ -729,7 +729,7 @@ target.grab_ghost(force = TRUE) target.fully_heal(HEAL_ALL) // also cures husk, unconditionally - see /mob/living/proc/fully_heal - target.revive(force_grab_ghost = TRUE) + target.revive_from("lazarus_line", force_grab_ghost = TRUE) if(target.stat == DEAD) to_chat(user, span_warning("[target] convulses, then lies still. It didn't take.")) return FALSE diff --git a/voidcrew/modules/loot/uniques/plunder.dm b/voidcrew/modules/loot/uniques/plunder.dm index 7cf16af4e20..4ffe5e85d4f 100644 --- a/voidcrew/modules/loot/uniques/plunder.dm +++ b/voidcrew/modules/loot/uniques/plunder.dm @@ -761,19 +761,19 @@ // // CHANGED: the reload used to take a full minute and demand a table, and its // ammunition existed nowhere in the game. Reloading is now a flat 5 seconds -// anywhere, and the gun arrives with a bandolier of cartridges and the -// schematic to make more. +// anywhere, and the gun arrives with a bandolier of craftable cartridges. // ========================================================================= #define NO_QUARTER_RELOAD_TIME (5 SECONDS) #define NO_QUARTER_THROW_RANGE 6 #define NO_QUARTER_BANDOLIER_SHELLS 12 +#define NO_QUARTER_CALIBER "heavy cartridge" /// One shot: breaches a normal wall outright, or throws a person six tiles. /// Five seconds to load the next cartridge. /obj/item/gun/ballistic/shotgun/musket/no_quarter name = "\"No Quarter\"" - desc = "A flintlock hand cannon dressed up as a boltloading musket. One shot: it punches a hole in a normal wall, or throws a person 6 tiles. Loading the next cartridge takes 5 seconds." + desc = "A flintlock hand cannon dressed up as a boltloading musket." icon = 'voidcrew/modules/loot/icons/uniques.dmi' icon_state = "no_quarter" // base musket sets inhand_icon_state = "donk_musket", override so the @@ -792,8 +792,8 @@ /** * The gun is worthless without cartridges and there is nowhere else in the - * game to get them, so it never arrives alone: a full bandolier and the - * schematic for more land wherever the gun did (in the cache it rolled in, or + * game to find them, so it never arrives alone: a full bandolier + * lands wherever the gun did (in the cache it rolled in, or * on the floor next to an admin who spawned it). */ /obj/item/gun/ballistic/shotgun/musket/no_quarter/Initialize(mapload) @@ -807,11 +807,6 @@ kit_location = get_turf(src) if(kit_location) new /obj/item/storage/belt/bandolier/no_quarter(kit_location) - new /obj/item/blueprint/no_quarter_cartridge(kit_location) - -/obj/item/gun/ballistic/shotgun/musket/no_quarter/examine(mob/user) - . = ..() - . += span_notice("Loading a cartridge takes 5 seconds and can be done anywhere.") /// Slows the normal ammo-loading flow down to a five-second job, once that's /// done, the real (unmodified) chambering logic takes over. @@ -834,17 +829,29 @@ /obj/item/ammo_box/magazine/internal/shot/single/no_quarter name = "\"No Quarter\" chamber" ammo_type = /obj/item/ammo_casing/shotgun/no_quarter + caliber = NO_QUARTER_CALIBER -/// "Gunpowder and a metal slug, twisted shut", the one cartridge type -/// No Quarter feeds on. Subtypes the real shotgun slug casing (icon/caliber/ -/// materials inherited verbatim) and just swaps the projectile it fires. +/// The hand cannon also retains its ordinary shotgun loads. +/obj/item/ammo_box/magazine/internal/shot/single/no_quarter/give_round(obj/item/ammo_casing/new_round) + if(new_round?.caliber != CALIBER_SHOTGUN) + return ..() + var/obj/item/ammo_casing/current_round = get_round() + if(current_round?.loaded_projectile) + return FALSE + if(current_round) + current_round.forceMove(get_turf(src)) + stored_ammo += new_round + new_round.forceMove(src) + return TRUE + +/// A paper cartridge for the hand cannon, with the ordinary slug's icon and materials. /obj/item/ammo_casing/shotgun/no_quarter name = "heavy cartridge" - desc = "A paper cartridge bulging with gunpowder and a single lead slug, twisted shut. Only \"No Quarter\" takes them." + desc = "A paper cartridge bulging with gunpowder and a single lead slug, twisted shut." + caliber = NO_QUARTER_CALIBER projectile_type = /obj/projectile/bullet/shotgun_slug/no_quarter -/// The bandolier the gun turns up with. Twelve cartridges, then you make -/// your own off the schematic. +/// The bandolier the gun turns up with. Twelve cartridges, then you craft your own. /obj/item/storage/belt/bandolier/no_quarter name = "hand cannon bandolier" desc = "A worn leather bandolier of heavy cartridges, cut wide for a hand cannon's bore." @@ -853,15 +860,7 @@ for(var/i in 1 to NO_QUARTER_BANDOLIER_SHELLS) new /obj/item/ammo_casing/shotgun/no_quarter(src) -/// Carrying this puts the cartridge recipe in your crafting menu; an outpost -/// neural imprinter can burn it in for the round instead. -/obj/item/blueprint/no_quarter_cartridge - name = "weapon schematic (heavy cartridge)" - schematic_name = "heavy cartridge" - recipe_type = /datum/crafting_recipe/blueprint/no_quarter_cartridge - tier = BLUEPRINT_TIER_RED - -/datum/crafting_recipe/blueprint/no_quarter_cartridge +/datum/crafting_recipe/no_quarter_cartridge name = "Heavy Cartridge" result = /obj/item/ammo_casing/shotgun/no_quarter category = CAT_WEAPON_AMMO @@ -899,6 +898,7 @@ wall_target.dismantle_wall(devastated = TRUE) #undef NO_QUARTER_BANDOLIER_SHELLS +#undef NO_QUARTER_CALIBER #undef NO_QUARTER_THROW_RANGE #undef NO_QUARTER_RELOAD_TIME diff --git a/voidcrew/modules/loot/uniques/research.dm b/voidcrew/modules/loot/uniques/research.dm index bff0bf2ee50..44a9a7ec5d7 100644 --- a/voidcrew/modules/loot/uniques/research.dm +++ b/voidcrew/modules/loot/uniques/research.dm @@ -640,7 +640,7 @@ user.setOxyLoss(snap["oxy"]) user.setStaminaLoss(snap["stamina"]) if(user.stat == DEAD) - user.revive(NONE) + user.revive_from("temporal_rewind", NONE) user.visible_message(span_warning("[user]'s wrist brace flares and cracks in half, and suddenly [user.p_theyre()] standing somewhere else!"), \ span_userdanger("The last ten seconds unwind. You're back where you were, and the splint is scrap.")) diff --git a/voidcrew/modules/loot/zone_loot.dm b/voidcrew/modules/loot/zone_loot.dm index d4af25276ec..9fcf87af226 100644 --- a/voidcrew/modules/loot/zone_loot.dm +++ b/voidcrew/modules/loot/zone_loot.dm @@ -1,7 +1,7 @@ /** * # Zone-Aware Loot Cache * - * One generic container that rolls its contents when it spawns. Every cache + * One generic container that rolls its contents on first opening or break. Every cache * of a theme draws from the SAME four-tier pool no matter where it sits; the * overmap zone decides only how many draws it gets and how the odds lean * across those tiers. A green cache can pay the top of its theme, it just @@ -29,8 +29,8 @@ * (themes/




