Files
Bubberstation/code/modules/explorer_drone/exodrone.dm
MrMelbert 5261efb67f Re-refactors batons / Refactors attack chain force modifiers (#90809)
## About The Pull Request

Melee attack chain now has a list passed along with it,
`attack_modifiers`, which you can stick force modifiers to change the
resulting attack

This is basically a soft implementation of damage packets until a more
definitive pr, but one that only applies to item attack chain, and not
unarmed attacks.

This change was done to facilitate a baton refactor - batons no longer
hack together their own attack chain, and are now integrated straight
into the real attack chain. This refactor itself was done because batons
don't send any attack signals, which has been annoying in the past (for
swing combat).

## Changelog

🆑 Melbert
refactor: Batons have been refactored again. Baton stuns now properly
count as an attack, when before it was a nothing. Report any oddities,
particularly in regards to harmbatonning vs normal batonning.
refactor: The method of adjusting item damage mid-attack has been
refactored - some affected items include the Nullblade and knives.
Report any strange happenings with damage numbers.
refactor: A few objects have been moved to the new interaction chain -
records consoles, mawed crucible, alien weeds and space vines, hedges,
restaurant portals, and some mobs - to name a few.
fix: Spears only deal bonus damage against secure lockers, not all
closet types (including crates)
/🆑
2025-05-19 13:32:12 +10:00

487 lines
16 KiB
Plaintext

/// How many lines of log we keep
#define EXODRONE_LOG_SIZE 15
/// Size of drone storage shared between loot and tools.
#define EXODRONE_CARGO_SLOTS 6
// Fuel types and travel time per unit of distance on that fuel.
#define FUEL_BASIC "basic"
#define BASIC_FUEL_TIME_COST 250
#define FUEL_ADVANCED "advanced"
#define ADVANCED_FUEL_TIME_COST 200
#define FUEL_EXOTIC "exotic"
#define EXOTIC_FUEL_TIME_COST 100
/// All exodrones.
GLOBAL_LIST_EMPTY(exodrones)
/// All exodrone launchers.
GLOBAL_LIST_EMPTY(exodrone_launchers)
/// Exploration drone
/obj/item/exodrone
name = "exploration drone"
desc = "A long range, semi-autonomous exploration drone."
icon = 'icons/obj/exploration.dmi'
icon_state = "drone"
w_class = WEIGHT_CLASS_BULKY
/// Current drone status, see code\__DEFINES\adventure.dm
var/drone_status = EXODRONE_IDLE
/// Are we currently controlled by remote terminal, blocks other terminals from interacting with this drone.
var/controlled = FALSE
/// Site we're currently at, null means station.
var/datum/exploration_site/location
/// Site we're currently travelling to, null means going back to station - check drone status if you want to check if traveling or idle
var/datum/exploration_site/travel_target
/// Total travel time to our current target
var/travel_time
/// Id of travel timer
var/travel_timer_id
/// Message that will show up on busy screen
var/busy_message = "Doing something..."
/// When we entered busy state
var/busy_start_time
/// How long will busy state last
var/busy_duration
// Our current adventure if any.
var/datum/adventure/current_adventure
// Our current simple event ui data if any
var/list/current_event_ui_data
/// Pad we've launched from, we'll try to land on this one first when coming back if it still exists.
var/datum/weakref/last_pad
/// Log of recent events
var/list/drone_log = list()
/// List of tools, EXODRONE_TOOL_WELDER etc
var/list/tools = list()
// Current travel cost per 1 distance in deciseconds
var/travel_cost_coeff = BASIC_FUEL_TIME_COST
/// Repeated drone name counter
var/static/name_counter = list()
/// Used to provide source to the regex replacement function. DO NOT MODIFY DIRECTLY
var/static/obj/item/exodrone/_regex_context
/obj/item/exodrone/Initialize(mapload)
. = ..()
if(name == /obj/item/exodrone::name)
name = pick(strings(EXODRONE_FILE,"probe_names"))
if(name_counter[name])
name_counter[name]++
name = "[name] \Roman[name_counter[name]]"
else
name_counter[name] = 1
else
name = name
GLOB.exodrones += src
// Cargo storage
create_storage(max_slots = EXODRONE_CARGO_SLOTS, canthold = GLOB.blacklisted_cargo_types)
/obj/item/exodrone/Destroy()
. = ..()
GLOB.exodrones -= src
/// Description for drone listing, describes location and current status
/obj/item/exodrone/proc/ui_description()
switch(drone_status)
if(EXODRONE_TRAVEL)
return travel_target ? "Traveling to [travel_target.display_name()]." : "Traveling back to station."
if(EXODRONE_EXPLORATION, EXODRONE_ADVENTURE, EXODRONE_BUSY)
return "Exploring [location?.display_name() || "ERROR"]." // better safe than sorry.
if(EXODRONE_IDLE)
return "Idle."
// Searches for blacklisted items hidden inside containers
/obj/item/exodrone/proc/check_blacklist()
for(var/obj/item/contained_item in contents)
if(contained_item.contents)
for(var/subcontained_object in contained_item.get_all_contents())
if(is_type_in_typecache(subcontained_object, GLOB.blacklisted_cargo_types))
return FALSE
return TRUE
/// Starts travel for site, does not validate if it's possible
/obj/item/exodrone/proc/launch_for(datum/exploration_site/target_site)
if(!location) //We're launching from station, fuel up
var/obj/machinery/exodrone_launcher/pad = locate() in loc
pad.fuel_up(src)
pad.launch_effect()
last_pad = WEAKREF(pad)
drone_log("Launched from [pad.name] and set course for [target_site.display_name()]")
else
drone_log("Launched from [location.display_name()] and set course for [target_site ? target_site.display_name() : station_name()]")
set_status(EXODRONE_TRAVEL)
moveToNullspace()
var/distance_to_travel = target_site ? target_site.distance : location.distance //If we're going home distance is distance of our current location
if(location && target_site) //Traveling site to site is faster (don't think too hard on 3d space logistics here)
distance_to_travel = max(abs(target_site.distance - location.distance),1)
travel_target = target_site
travel_time = travel_cost_coeff*distance_to_travel
travel_timer_id = addtimer(CALLBACK(src, PROC_REF(finish_travel)),travel_time,TIMER_STOPPABLE)
/// Travel cleanup
/obj/item/exodrone/proc/finish_travel()
location = travel_target
travel_timer_id = null
travel_time = null
if(location)//We're arriving at exploration site
location.on_drone_arrival(src)
set_status(EXODRONE_EXPLORATION)
else
var/obj/machinery/exodrone_launcher = find_landing_pad()
if(exodrone_launcher)
forceMove(get_turf(exodrone_launcher))
drone_log("Arrived at [station_name()]. Landing at [exodrone_launcher].")
else
var/turf/drop_zone = drop_somewhere_on_station()
drone_log("Arrived at [station_name()]. Emergency landing at [drop_zone.loc.name].")
set_status(EXODRONE_IDLE)
/obj/item/exodrone/proc/set_status(new_status)
SEND_SIGNAL(src,COMSIG_EXODRONE_STATUS_CHANGED)
drone_status = new_status
/// Cargo space left
/obj/item/exodrone/proc/space_left()
return EXODRONE_CARGO_SLOTS - length(contents) - length(tools)
/// Adds drone tool and resizes storage.
/obj/item/exodrone/proc/add_tool(tool_type)
if(space_left() > 0 && (tool_type in GLOB.exodrone_tool_metadata))
tools += tool_type
update_storage_size()
/// Removes drone tool and resizes storage.
/obj/item/exodrone/proc/remove_tool(tool_type)
tools -= tool_type
update_storage_size()
/// Resizes storage component depending on slots used by tools.
/obj/item/exodrone/proc/update_storage_size()
atom_storage.max_slots = EXODRONE_CARGO_SLOTS - length(tools)
/// Builds ui data for drone storage.
/obj/item/exodrone/proc/get_cargo_data()
. = list()
for(var/tool in tools)
. += list(list("type"="tool","name"=tool))
for(var/obj/cargo in contents)
. += list(list("type"="cargo","name"=cargo.name, "ref"=ref(cargo)))
for(var/_ in 1 to space_left())
. += list(list("type"="empty","name"="Free space"))
/// Tries to add loot to drone cargo while respecting space left
/obj/item/exodrone/proc/try_transfer(obj/loot, delete_on_failure=TRUE)
if(space_left())
loot.forceMove(src)
drone_log("Acquired [loot.name].")
else
drone_log("Abandoned [loot.name] due to lack of space.")
if(delete_on_failure)
qdel(loot)
/// Crashes the drone somewhere random if there's no launchpad to be found.
/obj/item/exodrone/proc/drop_somewhere_on_station()
var/turf/random_spot = get_safe_random_station_turf_equal_weight()
var/obj/structure/closet/supplypod/pod = podspawn(list(
"target" = random_spot,
))
forceMove(pod)
return random_spot
/// Tries to find landing pad, starting with the one we launched from.
/obj/item/exodrone/proc/find_landing_pad()
var/obj/machinery/exodrone_launcher/landing_pad = last_pad?.resolve()
if(landing_pad)
return landing_pad
for(var/obj/machinery/exodrone_launcher/other_pad in GLOB.exodrone_launchers)
return other_pad
/// encounters random or specificed event for the current site.
/obj/item/exodrone/proc/explore_site(datum/exploration_event/specific_event)
if(!specific_event) //encounter random event
var/list/events_to_encounter = list()
for(var/datum/exploration_event/event in location.events)
if(event.visited)
continue
events_to_encounter += event
if(!length(events_to_encounter))
drone_log("It seems there's nothing interesting left around [location.name].")
return
var/datum/exploration_event/encountered_event = pick(events_to_encounter)
encountered_event.encounter(src)
else if(specific_event.is_targetable())
specific_event.encounter(src)
/obj/item/exodrone/proc/get_adventure_data()
var/list/data = current_adventure?.ui_data()
data["description"] = updateKeywords(data["description"])
var/list/choices = data["choices"]
for(var/list/choice in choices)
choice["text"] = updateKeywords(choice["text"])
return data
///Replaces $$SITE_NAME with site name and $$QualityName with quality values
/obj/item/exodrone/proc/updateKeywords(text)
_regex_context = src
var/static/regex/keywordRegex = regex(@"\$\$(\S*)","g")
. = keywordRegex.Replace(text, /obj/item/exodrone/proc/replace_keyword)
_regex_context = null
/// This is called with src = regex datum, so don't try to access any instance variables directly here.
/obj/item/exodrone/proc/replace_keyword(match,g1)
REGEX_REPLACE_HANDLER
switch(g1)
if("SITE_NAME")
return _regex_context.location.display_name()
else
if(_regex_context.current_adventure.qualities[g1])
return "[_regex_context.current_adventure.qualities[g1]]"
else
return ""
/obj/item/exodrone/proc/start_adventure(datum/adventure/adventure)
current_adventure = adventure
RegisterSignal(current_adventure,COMSIG_ADVENTURE_FINISHED, PROC_REF(resolve_adventure))
RegisterSignal(current_adventure,COMSIG_ADVENTURE_QUALITY_INIT, PROC_REF(add_tool_qualities))
RegisterSignal(current_adventure,COMSIG_ADVENTURE_DELAY_START, PROC_REF(adventure_delay_start))
RegisterSignal(current_adventure,COMSIG_ADVENTURE_DELAY_END, PROC_REF(adventure_delay_end))
set_status(EXODRONE_ADVENTURE)
current_adventure.start_adventure()
/// Handles finishing adventure
/obj/item/exodrone/proc/resolve_adventure(datum/source,result)
SIGNAL_HANDLER
switch(result)
if(ADVENTURE_RESULT_SUCCESS)
award_adventure_loot()
UnregisterSignal(current_adventure,list(COMSIG_ADVENTURE_FINISHED,COMSIG_ADVENTURE_QUALITY_INIT,COMSIG_ADVENTURE_DELAY_START,COMSIG_ADVENTURE_DELAY_END))
current_adventure = null
set_status(EXODRONE_EXPLORATION)
return
if(ADVENTURE_RESULT_DAMAGE)
damage(max_integrity*0.5) //Half health lost
if(!QDELETED(src)) // Don't bother if we just blown up from the damage
UnregisterSignal(current_adventure,list(COMSIG_ADVENTURE_FINISHED,COMSIG_ADVENTURE_QUALITY_INIT,COMSIG_ADVENTURE_DELAY_START,COMSIG_ADVENTURE_DELAY_END))
current_adventure = null
set_status(EXODRONE_EXPLORATION)
return
if(ADVENTURE_RESULT_DEATH)
qdel(src)
/// Adds loot from current adventure to the drone
/obj/item/exodrone/proc/award_adventure_loot()
if(length(current_adventure.loot_categories))
var/generator_type = GLOB.adventure_loot_generator_index[pick(current_adventure.loot_categories)]
if(!generator_type)
return //Could probably warn but i suppose this is up to adventure creator.
var/datum/adventure_loot_generator/generator = new generator_type
generator.transfer_loot(src)
/// Applies adventure qualities based on our tools
/obj/item/exodrone/proc/add_tool_qualities(datum/source,list/quality_list)
SIGNAL_HANDLER
for(var/tool in tools)
quality_list[tool] = 1
/obj/item/exodrone/proc/adventure_delay_start(datum/source, delay_time,delay_message)
SIGNAL_HANDLER
set_busy(delay_message,delay_time)
/obj/item/exodrone/proc/adventure_delay_end(datum/source)
SIGNAL_HANDLER
unset_busy(EXODRONE_ADVENTURE)
/// Enters busy mode for a given duration.
/obj/item/exodrone/proc/set_busy(message,duration)
if(message)
busy_message = message
busy_start_time = world.time
busy_duration = duration
set_status(EXODRONE_BUSY)
/// Resets busy status
/obj/item/exodrone/proc/unset_busy(new_status)
busy_message = initial(busy_message)
busy_start_time = null
busy_duration = null
set_status(new_status)
/obj/item/exodrone/proc/busy_time_left()
return busy_duration - (world.time - busy_start_time)
/// Returns failure message or FALSE if we're ready to travel
/obj/item/exodrone/proc/travel_error()
/// We're home and on ready pad or exploring and out of any events/adventures
switch(drone_status)
if(EXODRONE_IDLE)
var/obj/machinery/exodrone_launcher/pad = locate() in loc
if(!pad)
return "No launcher"
if(!pad.fuel_canister)
return "No fuel in launcher"
if(pad.fuel_canister.uses <= 0)
return "Launcher fuel used up"
return FALSE
if(EXODRONE_EXPLORATION)
if(current_event_ui_data)
return "Busy"
return FALSE
else
return ""
/// Deals damage in adventures/events.
/obj/item/exodrone/proc/damage(amount)
take_damage(amount)
drone_log("Sustained [amount] damage.")
/obj/item/exodrone/proc/drone_log(message)
if(length(drone_log) > EXODRONE_LOG_SIZE)
drone_log = list()
drone_log.Insert(1,message)
/obj/item/exodrone/proc/has_tool(tool_type)
return tools.Find(tool_type)
/// Exploration drone launcher
/obj/machinery/exodrone_launcher
name = "exploration drone launcher"
desc = "A launch pad designed to send exploration drones into the great beyond."
icon = 'icons/obj/exploration.dmi'
icon_state = "launcher"
/// Loaded fuel pellet.
var/obj/item/fuel_pellet/fuel_canister
/obj/machinery/exodrone_launcher/Initialize(mapload)
. = ..()
GLOB.exodrone_launchers += src
/obj/machinery/exodrone_launcher/examine(user)
. = ..()
if(fuel_canister)
. += span_notice("You can remove the [fuel_canister] with a <b>prying tool</b>.")
/obj/machinery/exodrone_launcher/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers)
if(istype(weapon, /obj/item/fuel_pellet))
if(fuel_canister)
to_chat(user, span_warning("There's already fuel loaded inside [src]!"))
return TRUE
if(!user.transferItemToLoc(weapon, src))
return
fuel_canister = weapon
update_icon()
return TRUE
if(istype(weapon, /obj/item/exodrone) && user.transferItemToLoc(weapon, drop_location()))
return TRUE
return ..()
/obj/machinery/exodrone_launcher/crowbar_act(mob/living/user, obj/item/crowbar)
if(!fuel_canister)
return
to_chat(user, span_notice("You remove [fuel_canister] from [src]."))
fuel_canister.forceMove(drop_location())
fuel_canister = null
update_icon()
return TRUE
/obj/machinery/exodrone_launcher/Destroy()
GLOB.exodrone_launchers -= src
QDEL_NULL(fuel_canister)
return ..()
/obj/machinery/exodrone_launcher/update_overlays()
. = ..()
if(fuel_canister && fuel_canister.uses > 0)
switch(fuel_canister.fuel_type)
if(FUEL_BASIC)
. += "launchpad_fuel_basic"
if(FUEL_ADVANCED)
. += "launchpad_fuel_advanced"
if(FUEL_EXOTIC)
. += "launchpad_fuel_exotic"
/*
* Gets the fuel travel coefficient for what type of fuel is within the launcher.
*/
/obj/machinery/exodrone_launcher/proc/get_fuel_coefficent()
if(!fuel_canister)
return
switch(fuel_canister.fuel_type)
if(FUEL_BASIC)
return BASIC_FUEL_TIME_COST
if(FUEL_ADVANCED)
return ADVANCED_FUEL_TIME_COST
if(FUEL_EXOTIC)
return EXOTIC_FUEL_TIME_COST
/*
* Use up some of the fuel within the launcher to power the drone.
*
* drone - the drone that's being fuelled by our launcher.
*/
/obj/machinery/exodrone_launcher/proc/fuel_up(obj/item/exodrone/drone)
drone.travel_cost_coeff = get_fuel_coefficent()
fuel_canister.use()
update_icon()
/*
* Plays an effect on the pad, with a sound effect to boot.
*/
/obj/machinery/exodrone_launcher/proc/launch_effect()
playsound(src,'sound/effects/podwoosh.ogg',50, FALSE)
do_smoke(1, holder = src, location = get_turf(src))
/obj/machinery/exodrone_launcher/Exited(atom/movable/gone, direction)
. = ..()
if(gone == fuel_canister)
fuel_canister = null
update_icon()
/obj/item/exodrone/proc/get_travel_coeff()
switch(drone_status)
if(EXODRONE_IDLE)
var/obj/machinery/exodrone_launcher/pad = locate() in loc
if(pad && pad.fuel_canister)
return pad.get_fuel_coefficent()
else
return travel_cost_coeff
else
return travel_cost_coeff
/obj/item/fuel_pellet
name = "standard fuel pellet"
desc = "A compressed fuel pellet for long-distance drone flight."
icon = 'icons/obj/exploration.dmi'
icon_state = "fuel_basic"
/// The type of fuel this pellet has within.
var/fuel_type = FUEL_BASIC
/// The amount of uses left in this fuel pellet.
var/uses = 5
/obj/item/fuel_pellet/use(used)
uses--
if(uses <= 0)
qdel(src)
/obj/item/fuel_pellet/advanced
name = "advanced fuel pellet"
fuel_type = FUEL_ADVANCED
icon_state = "fuel_advanced"
/obj/item/fuel_pellet/exotic
name = "exotic fuel pellet"
fuel_type = FUEL_EXOTIC
icon_state = "fuel_exotic"
#undef EXODRONE_LOG_SIZE
#undef EXODRONE_CARGO_SLOTS
#undef FUEL_BASIC
#undef BASIC_FUEL_TIME_COST
#undef FUEL_ADVANCED
#undef ADVANCED_FUEL_TIME_COST
#undef FUEL_EXOTIC
#undef EXOTIC_FUEL_TIME_COST