Adds explorer drones / adventures. (#57851)

A new side-job for cargo. Prepare and launch exploration drones at distant sites to encounter weird adventures, collect loot and expand the station cargo network. Adventures - the main content type here, can be written by anyone without any knowledge of programming. The purpose here is creating an way of interesting way of delivering lore (and adding some new job content/cargo expansion method).

Co-authored-by: tralezab <40974010+tralezab@users.noreply.github.com>
Co-authored-by: EOBGames <58124831+EOBGames@users.noreply.github.com>
Co-authored-by: Fikou <piotrbryla@onet.pl>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
This commit is contained in:
AnturK
2021-03-26 11:38:39 +02:00
committed by GitHub
co-authored by tralezab EOBGames Fikou Aleksej Komarov
parent b08ff4f9ec
commit 8b1ffd1e49
59 changed files with 23108 additions and 18909 deletions
+11
View File
@@ -0,0 +1,11 @@
Future improvements:
Better names/descriptions for ships/trading/planets
More fluff lines for each site type.
Adventure-Maker : styling aaaaah.
Adventure-Maker : Consider parsing define hard-configured dm file for constants
Typelists for event/site stuff to reduce costs
Figure out some way to cache adventure prototypes / defer loading somehow so it scales better with a lot of adventure files.
Unit test for json loading?
Mk II drones with more tool slots/other buffs researchable at R&D ?
Could use more scan conditions
Exit Node - Random Node
+422
View File
@@ -0,0 +1,422 @@
// json field definitions bit verbose but i've had it with the typos
#define ADVENTURE_NAME_FIELD "adventure_name"
#define ADVENTURE_STARTING_NODE_FIELD "starting_node"
#define ADVENTURE_REQUIRED_SITE_TRAITS_FIELD "required_site_traits"
#define ADVENTURE_SCAN_BAND_MODS_FIELD "scan_band_mods"
#define ADVENTURE_LOOT_FIELD "loot_categories"
#define ADVENTURE_STARTING_QUALITIES_FIELD "starting_qualities"
#define ADVENTURE_DEEP_SCAN_DESCRIPTION "deep_scan_description"
#define ADVENTURE_NODES_FIELD "nodes"
#define ADVENTURE_TRIGGERS_FIELD "triggers"
#define NODE_NAME_FIELD "name"
#define NODE_DESCRIPTION_FIELD "description"
#define NODE_IMAGE_FIELD "image"
#define NODE_RAW_IMAGE_FIELD "raw_image"
#define NODE_CHOICES_FIELD "choices"
#define NODE_ON_ENTER_EFFECTS_FIELD "on_enter_effects"
#define NODE_ON_EXIT_EFFECTS_FIELD "on_exit_effects"
#define CHOICE_KEY_FIELD "key"
#define CHOICE_NAME_FIELD "name"
#define CHOICE_ON_SELECTION_EFFECT_FIELD "on_selection_effects"
#define CHOICE_REQUIREMENTS_FIELD "requirements"
#define CHOICE_EXIT_NODE_FIELD "exit_node"
#define CHOICE_DELAY_FIELD "delay"
#define CHOICE_DELAY_MESSAGE_FIELD "delay_message"
#define EFFECT_TYPE_FIELD "effect_type"
#define EFFECT_QUALITY_FIELD "quality"
#define EFFECT_VALUE_FIELD "value"
#define EFFECT_VALUE_VALUE_TYPE_FIELD "value_type"
#define TRIGGER_NAME_FIELD "name"
#define TRIGGER_REQUIREMENTS_FIELD "requirements"
#define TRIGGER_ON_TRIGGER_EFFECTS_FIELD "on_trigger_effects"
#define TRIGGER_TARGET_NODE_FIELD "target_node"
#define REQ_GROUP_REQUIREMENTS_FIELD "requirements"
#define REQ_GROUP_GROUP_TYPE_FIELD "group_type"
#define REQ_QUALITY_FIELD "quality"
#define REQ_VALUE_FIELD "value"
#define REQ_OPERATOR_FIELD "operator"
GLOBAL_LIST_EMPTY(explorer_drone_adventures)
/proc/load_adventures()
. = list()
for(var/filename in flist(ADVENTURE_DIR))
var/datum/adventure/adventure = try_loading_adventure(filename)
if(adventure)
. += adventure
/proc/try_loading_adventure(filename)
var/list/json_data = json_load(ADVENTURE_DIR+filename)
if(!islist(json_data))
CRASH("Invalid JSON in adventure file [filename]")
//Basic validation of required fields, don't even bother loading if they are missing.
var/static/list/required_fields = list(ADVENTURE_NAME_FIELD,ADVENTURE_STARTING_NODE_FIELD,ADVENTURE_NODES_FIELD)
for(var/field in required_fields)
if(!json_data[field])
CRASH("Adventure file [filename] missing [field] value")
var/datum/adventure/loaded_adventure = new
//load properties
loaded_adventure.starting_node = json_data[ADVENTURE_STARTING_NODE_FIELD]
loaded_adventure.name = json_data[ADVENTURE_NAME_FIELD]
loaded_adventure.required_site_traits = json_data[ADVENTURE_REQUIRED_SITE_TRAITS_FIELD]
loaded_adventure.band_modifiers = json_data[ADVENTURE_SCAN_BAND_MODS_FIELD]
loaded_adventure.loot_categories = json_data[ADVENTURE_LOOT_FIELD]
loaded_adventure.starting_qualities = json_data[ADVENTURE_STARTING_QUALITIES_FIELD]
loaded_adventure.deep_scan_description = json_data[ADVENTURE_DEEP_SCAN_DESCRIPTION]
for(var/list/node_data in json_data[ADVENTURE_NODES_FIELD])
var/datum/adventure_node/node = try_loading_node(node_data,filename)
if(node)
if(loaded_adventure.nodes[node.id])
CRASH("Duplicate [node.id] node in [filename] adventure")
loaded_adventure.nodes[node.id] = node
loaded_adventure.triggers = json_data[ADVENTURE_TRIGGERS_FIELD]
if(!loaded_adventure.validate())
CRASH("Validation failed for [filename] adventure")
return loaded_adventure
/proc/try_loading_node(node_data,adventure_filename)
if(!islist(node_data))
CRASH("Invalid adventure node data in [adventure_filename] adventure.")
var/datum/adventure_node/fresh_node = new
fresh_node.id = node_data[NODE_NAME_FIELD]
fresh_node.description = node_data[NODE_DESCRIPTION_FIELD]
fresh_node.image_name = node_data[NODE_IMAGE_FIELD]
fresh_node.raw_image = node_data[NODE_RAW_IMAGE_FIELD]
fresh_node.choices = list()
for(var/list/choice_data in node_data[NODE_CHOICES_FIELD])
fresh_node.choices[choice_data[CHOICE_KEY_FIELD]] = choice_data
fresh_node.on_enter_effects = node_data[NODE_ON_ENTER_EFFECTS_FIELD]
fresh_node.on_exit_effects = node_data[NODE_ON_EXIT_EFFECTS_FIELD]
return fresh_node
/// text adventure instance, holds data about nodes/choices/etc and of current play state.
/datum/adventure
/// Adventure name, this organization only, not visible to users
var/name
/// Node the adventure will start at
var/starting_node
/// Required site traits for the adventure to appear
var/list/required_site_traits = list()
/// Modifiers to band scan values
var/list/band_modifiers = list()
/// Loot table ids used as reward for finishing the adventure succesfully.
var/list/loot_categories = list()
/// Nodes for this adventure, represent single scene.
var/list/nodes = list()
/// Triggers for this adventure, checked after quality changes to cause instantenous results
var/list/triggers = list()
/// List of starting quality values, these will be set before first node is encountered.
var/list/starting_qualities = list()
///Keeps track firing of triggers until stop state to prevent loops
var/list/trigger_loop_safety = list()
/// Opional description shown after site deep scan
var/deep_scan_description
// State tracking variables
/// Current active adventure node
var/datum/adventure_node/current_node
/// Last other node than this one. Used by GO_BACK_NODE
var/previous_node_id
/// Assoc list of quality name = value
var/list/qualities
/// Was this adventure placed on generated exploration site already.
var/placed = FALSE
/// Basic sanity checks to ensure broken adventures are not used.
/datum/adventure/proc/validate()
///Check all nodes have choices
for(var/node_id in nodes)
var/datum/adventure_node/node = nodes[node_id]
if(!length(node.choices))
return FALSE
return TRUE
/datum/adventure/proc/start_adventure()
initialize_qualities()
previous_node_id = starting_node
navigate_to_node(starting_node)
/// Finish adventure
/datum/adventure/proc/end_adventure(result)
SEND_SIGNAL(src,COMSIG_ADVENTURE_FINISHED,result)
/datum/adventure/proc/initialize_qualities()
qualities = starting_qualities || list()
SEND_SIGNAL(src,COMSIG_ADVENTURE_QUALITY_INIT,qualities)
/datum/adventure/proc/end_delay()
return
/datum/adventure/proc/navigate_to_node(node_id)
if(current_node)
if(current_node.on_exit(src)) //Trigger on exit caused node change <- I don't really see much use for this so might want to warn about it ?
return
if(current_node.id != previous_node_id)
previous_node_id = current_node.id
if(handle_special_nodes(node_id))
return
if(!nodes[node_id])
stack_trace("Invalid adventure node navigation from node [current_node.id]")
current_node = nodes[node_id]
current_node.on_enter(src)
/// Handles special node ID's
/datum/adventure/proc/handle_special_nodes(node_id)
switch(node_id)
if(FAIL_NODE)
end_adventure(ADVENTURE_RESULT_DAMAGE)
return TRUE
if(FAIL_DEATH_NODE)
end_adventure(ADVENTURE_RESULT_DEATH)
return TRUE
if(WIN_NODE)
end_adventure(ADVENTURE_RESULT_SUCCESS)
return TRUE
if(GO_BACK_NODE)
if(previous_node_id)
navigate_to_node(previous_node_id)
return TRUE
else
return FALSE
else
return FALSE
/datum/adventure/proc/select_choice(choice_id)
if(!current_node || !islist(current_node.choices[choice_id]))
return
var/list/choice_data = current_node.choices[choice_id]
if(!check_requirements(choice_data[CHOICE_REQUIREMENTS_FIELD]))
return
if(choice_data[CHOICE_ON_SELECTION_EFFECT_FIELD])
if(apply_adventure_effect(choice_data[CHOICE_ON_SELECTION_EFFECT_FIELD],src))
return //Trigger forced node change.
var/exit_id = choice_data[CHOICE_EXIT_NODE_FIELD]
if(!exit_id)
CRASH("No exit node for choice [choice_id] in adventure [name]")
if(choice_data[CHOICE_DELAY_FIELD])
var/delay_message = choice_data[CHOICE_DELAY_MESSAGE_FIELD]
var/delay_time = choice_data[CHOICE_DELAY_FIELD]
if(!isnum(delay_time))
CRASH("Invalid delay in adventure [name]")
SEND_SIGNAL(src,COMSIG_ADVENTURE_DELAY_START,delay_time,delay_message)
addtimer(CALLBACK(src,.proc/finish_delay,exit_id),delay_time)
return
navigate_to_node(exit_id)
/datum/adventure/proc/finish_delay(exit_id)
navigate_to_node(exit_id)
SEND_SIGNAL(src,COMSIG_ADVENTURE_DELAY_END)
/datum/adventure/ui_data(mob/user)
. = ..()
.["description"] = current_node?.description
.["image"] = current_node?.image_name
.["raw_image"] = current_node?.raw_image
.["choices"] = current_node?.get_available_choices(src)
/datum/adventure_node
/// Unique identifier for this node
var/id
/// The actual displayed text
var/description
/// Preset image name, exclusive with raw_image
var/image_name
/// Image in base64 form. Exclusive with image_name
var/raw_image
/// All possible choices from this node, associative list of choice_id -> choice_data
var/list/choices
/// Effects fired when navigating to this node.
var/list/on_enter_effects
/// Effects fired when leaving this node.
var/list/on_exit_effects
/// Pauses adventure for this long after the choice
var/delay
/// This will show when the delay is happening.
var/delay_message
/datum/adventure_node/proc/on_enter(datum/adventure/context)
if(on_enter_effects)
if(context.apply_adventure_effect(on_enter_effects))
return TRUE
/datum/adventure_node/proc/on_exit(datum/adventure/context)
if(on_exit_effects)
if(context.apply_adventure_effect(on_exit_effects))
return TRUE
/datum/adventure_node/proc/get_available_choices(datum/adventure/context)
. = list()
for(var/choice_key in choices)
var/list/choice_data = choices[choice_key]
if(context.check_requirements(choice_data[CHOICE_REQUIREMENTS_FIELD]))
. += list(list("key" = choice_key,"text" = choice_data[CHOICE_NAME_FIELD]))
///Applies changes encoded in effect data and processes triggers, returns TRUE if the change forced node change.
/datum/adventure/proc/apply_adventure_effect(list/effect_data,process_triggers=TRUE)
if(!islist(effect_data))
CRASH("Invalid effect data [json_encode(effect_data)] in adventure [name]")
for(var/list/effect_group in effect_data)
var/effect_keyword = effect_group[EFFECT_TYPE_FIELD]
var/list/quality_name = effect_group[EFFECT_QUALITY_FIELD]
var/value = process_adventure_value(effect_group[EFFECT_VALUE_FIELD])
switch(effect_keyword)
if(ADVENTURE_EFFECT_TYPE_REMOVE) //remove quality doesn't care about value for now
qualities -= quality_name
if(ADVENTURE_EFFECT_TYPE_ADD)
if(!isnum(value))
CRASH("Invalid add quality effect value in effect [json_encode(effect_data)] in adventure [name]")
if(!qualities[quality_name])
qualities[quality_name] = 0
qualities[quality_name] += value
if(ADVENTURE_EFFECT_TYPE_SET)
qualities[quality_name] = value
else
CRASH("Invalid effect keyword in effect [json_encode(effect_data)] in adventure [name]")
///Check Triggers
if(process_triggers)
for(var/list/trigger_data in triggers)
if(!check_requirements(trigger_data[TRIGGER_REQUIREMENTS_FIELD]))
continue
if(LAZYACCESS(trigger_loop_safety,trigger_data[TRIGGER_NAME_FIELD]))
stack_trace("Loop in trigger processing detected in adventure [name]")
continue
LAZYADD(trigger_loop_safety,trigger_data[TRIGGER_NAME_FIELD])
if(trigger_data[TRIGGER_ON_TRIGGER_EFFECTS_FIELD])
apply_adventure_effect(trigger_data[TRIGGER_ON_TRIGGER_EFFECTS_FIELD],FALSE) //Let's keep this simple
if(trigger_data[TRIGGER_TARGET_NODE_FIELD])
navigate_to_node(trigger_data[TRIGGER_TARGET_NODE_FIELD])
return TRUE
//We're out of trigger processing
LAZYCLEARLIST(trigger_loop_safety)
return FALSE
/// Extracts raw value from special value objects
/datum/adventure/proc/process_adventure_value(raw_value)
if(islist(raw_value))
var/list/value_as_list = raw_value
switch(value_as_list[EFFECT_VALUE_VALUE_TYPE_FIELD])
if(ADVENTURE_QUALITY_TYPE_RANDOM)
return rand(value_as_list[ADVENTURE_RANDOM_QUALITY_LOW_FIELD],value_as_list[ADVENTURE_RANDOM_QUALITY_HIGH_FIELD])
else
CRASH("Invalid special value type in adventure [name]")
else
return raw_value
/// Checks if current qualities satisfy passed in requirements
/datum/adventure/proc/check_requirements(raw_requirements)
if(!islist(raw_requirements))
return TRUE
var/list/req_groups = raw_requirements
// Top level list - can contain either req groups or single requirements and is AND type group
for(var/list/group_data in req_groups)
if(group_data[REQ_GROUP_REQUIREMENTS_FIELD]) //It's a group
if(!check_requirement_group(group_data))
return FALSE
else //It's a single requirement
if(!check_single_requirement(group_data))
return FALSE
return TRUE
/// Recursively validates group requirements.
/datum/adventure/proc/check_requirement_group(raw_group_data)
if(!islist(raw_group_data))
CRASH("Invalid group requirement in adventure [name]")
var/list/group_data = raw_group_data
var/group_type = group_data[REQ_GROUP_GROUP_TYPE_FIELD]
var/list/group_elements = group_data[REQ_GROUP_REQUIREMENTS_FIELD]
switch(group_type)
if("OR") //Just one out of subgroups/reqs need to be true for this to return true
for(var/list/subgroup_data in group_elements)
if(subgroup_data[REQ_GROUP_REQUIREMENTS_FIELD]) //It's a group
if(check_requirement_group(subgroup_data))
return TRUE
else //It's a single requirement
if(check_single_requirement(subgroup_data))
return TRUE
return FALSE
if("AND") //All subgroups/reqs need to be true for this to return true
for(var/list/subgroup_data in group_elements)
if(subgroup_data[REQ_GROUP_REQUIREMENTS_FIELD]) //It's a group
if(!check_requirement_group(subgroup_data))
return FALSE
else //It's a single requirement
if(!check_single_requirement(subgroup_data))
return FALSE
return TRUE
else
CRASH("Invalid requirement group in adventure [name]")
//Checks if unit requirement {"quality": "a","op": "==","value": "something"} is met.
/datum/adventure/proc/check_single_requirement(raw_requirement)
var/qkey = raw_requirement[REQ_QUALITY_FIELD]
var/qval = raw_requirement[REQ_VALUE_FIELD]
switch(raw_requirement[REQ_OPERATOR_FIELD])
if("==")
return qualities[qkey] == qval
if("!=")
return qualities[qkey] != qval
if(">")
return qualities[qkey] > qval
if(">=")
return qualities[qkey] >= qval
if("<=")
return qualities[qkey] <= qval
if("<")
return qualities[qkey] < qval
if("exists")
return qkey in qualities
#undef ADVENTURE_NAME_FIELD
#undef ADVENTURE_STARTING_NODE_FIELD
#undef ADVENTURE_REQUIRED_SITE_TRAITS_FIELD
#undef ADVENTURE_SCAN_BAND_MODS_FIELD
#undef ADVENTURE_LOOT_FIELD
#undef ADVENTURE_STARTING_QUALITIES_FIELD
#undef ADVENTURE_DEEP_SCAN_DESCRIPTION
#undef ADVENTURE_NODES_FIELD
#undef ADVENTURE_TRIGGERS_FIELD
#undef NODE_NAME_FIELD
#undef NODE_DESCRIPTION_FIELD
#undef NODE_IMAGE_FIELD
#undef NODE_RAW_IMAGE_FIELD
#undef NODE_CHOICES_FIELD
#undef NODE_ON_ENTER_EFFECTS_FIELD
#undef NODE_ON_EXIT_EFFECTS_FIELD
#undef CHOICE_KEY_FIELD
#undef CHOICE_NAME_FIELD
#undef CHOICE_ON_SELECTION_EFFECT_FIELD
#undef CHOICE_REQUIREMENTS_FIELD
#undef CHOICE_EXIT_NODE_FIELD
#undef CHOICE_DELAY_FIELD
#undef CHOICE_DELAY_MESSAGE_FIELD
#undef EFFECT_TYPE_FIELD
#undef EFFECT_QUALITY_FIELD
#undef EFFECT_VALUE_FIELD
#undef EFFECT_VALUE_VALUE_TYPE_FIELD
#undef TRIGGER_NAME_FIELD
#undef TRIGGER_REQUIREMENTS_FIELD
#undef TRIGGER_ON_TRIGGER_EFFECTS_FIELD
#undef TRIGGER_TARGET_NODE_FIELD
#undef REQ_GROUP_REQUIREMENTS_FIELD
#undef REQ_GROUP_GROUP_TYPE_FIELD
#undef REQ_QUALITY_FIELD
#undef REQ_VALUE_FIELD
#undef REQ_OPERATOR_FIELD
@@ -0,0 +1,172 @@
/obj/machinery/computer/exodrone_control_console
name = "exploration drone control console"
desc = "control eploration drones from intersteller distances. Communication lag not included."
//Currently controlled drone
var/obj/item/exodrone/controlled_drone
/// Have we lost contact with the drone without disconnecting. Unset on user confirmation.
var/signal_lost = FALSE
/obj/machinery/computer/exodrone_control_console/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ExodroneConsole", name)
ui.open()
/obj/machinery/computer/exodrone_control_console/proc/start_drone_control(obj/item/exodrone/drone)
if(!drone.controlled)//Only one controller per drone at once to make it saner
///End control if we had previous drone
end_drone_control()
controlled_drone = drone
controlled_drone.controlled = TRUE
RegisterSignal(controlled_drone,COMSIG_PARENT_QDELETING,.proc/drone_destroyed)
RegisterSignal(controlled_drone,COMSIG_EXODRONE_STATUS_CHANGED,.proc/on_exodrone_status_changed)
update_icon()
/obj/machinery/computer/exodrone_control_console/proc/on_exodrone_status_changed()
SIGNAL_HANDLER
//Notify we need human action and switch screeb icon to alert.
playsound(src,'sound/machines/ping.ogg',30,FALSE)
update_icon()
/obj/machinery/computer/exodrone_control_console/proc/drone_destroyed()
SIGNAL_HANDLER
signal_lost = TRUE
end_drone_control()
/obj/machinery/computer/exodrone_control_console/proc/end_drone_control()
if(controlled_drone)
controlled_drone.controlled = FALSE
UnregisterSignal(controlled_drone,list(COMSIG_PARENT_QDELETING,COMSIG_EXODRONE_STATUS_CHANGED))
controlled_drone = null
update_icon()
/obj/machinery/computer/exodrone_control_console/Destroy()
. = ..()
end_drone_control()
/obj/machinery/computer/exodrone_control_console/ui_static_data(mob/user)
. = ..()
.["all_tools"] = GLOB.exodrone_tool_metadata
.["all_bands"] = GLOB.exoscanner_bands
/obj/machinery/computer/exodrone_control_console/ui_data(mob/user)
. = ..()
.["signal_lost"] = signal_lost
.["drone"] = controlled_drone
if(controlled_drone)
.["drone_status"] = controlled_drone.drone_status
.["drone_name"] = controlled_drone.name
.["drone_integrity"] = controlled_drone.obj_integrity
.["drone_max_integrity"] = controlled_drone.max_integrity
.["drone_log"] = controlled_drone.drone_log
.["configurable"] = controlled_drone.drone_status == EXODRONE_IDLE
.["cargo"] = controlled_drone.get_cargo_data()
.["drone_travel_coefficent"] = controlled_drone.get_travel_coeff()
var/travel_error = controlled_drone.travel_error()
.["can_travel"] = !travel_error
.["travel_error"] = travel_error ? travel_error : ""
switch(controlled_drone.drone_status) //could move this down to drone.ui_data()
if(EXODRONE_IDLE)
.["sites"] = build_exploration_site_ui_data()
.["site"] = null
if(EXODRONE_TRAVEL)
.["travel_time"] = controlled_drone.travel_time
.["travel_time_left"] = timeleft(controlled_drone.travel_timer_id)
if(EXODRONE_BUSY)
.["wait_time_left"] = controlled_drone.busy_time_left()
.["wait_message"] = controlled_drone.busy_message
if(EXODRONE_EXPLORATION)
.["sites"] = build_exploration_site_ui_data()
.["site"] = controlled_drone.location.site_data(exploration=TRUE)
.["event"] = controlled_drone.current_event_ui_data
if(EXODRONE_ADVENTURE)
.["adventure_data"] = controlled_drone.get_adventure_data()
else
var/list/exodrones = list()
for(var/obj/item/exodrone/drone in GLOB.exodrones)
exodrones += list(list("name"=drone.name,"controlled"=drone.controlled,"description"=drone.ui_description(),"ref"=ref(drone)))
.["all_drones"] = exodrones
/obj/machinery/computer/exodrone_control_console/update_overlays()
/// Show alert screen if the drone is in a mode that requires decisionmaking
if(controlled_drone && (controlled_drone.drone_status == EXODRONE_IDLE || controlled_drone.drone_status == EXODRONE_EXPLORATION || controlled_drone.drone_status == EXODRONE_ADVENTURE))
icon_screen = "alert:2"
else
icon_screen = initial(icon_screen)
. = ..()
/obj/machinery/computer/exodrone_control_console/ui_act(action, list/params)
. = ..()
if(.)
return
switch(action)
if("select_drone")
var/obj/item/exodrone/selected = locate(params["drone_ref"]) in GLOB.exodrones
if(selected)
start_drone_control(selected)
return TRUE
if("end_control")
end_drone_control()
return TRUE
if("confirm_signal_lost")
signal_lost = FALSE
return TRUE
if("self_destruct")
qdel(controlled_drone) //var will be nulled in signal response
return TRUE
if("add_tool")
if(controlled_drone && controlled_drone.drone_status == EXODRONE_IDLE)
controlled_drone.add_tool(params["tool_type"])
return TRUE
if("remove_tool")
if(controlled_drone && controlled_drone.drone_status == EXODRONE_IDLE)
controlled_drone.remove_tool(params["tool_type"])
return TRUE
if("start_travel")
if(controlled_drone && !controlled_drone.travel_error())
var/datum/exploration_site/target_site
if(params["target_site"])
target_site = locate(params["target_site"]) in GLOB.exploration_sites
if(!target_site)
return TRUE
controlled_drone.launch_for(target_site)
return TRUE
if("explore")
if(controlled_drone && controlled_drone.drone_status == EXODRONE_EXPLORATION)
controlled_drone.explore_site()
return TRUE
if("explore_event")
if(controlled_drone && controlled_drone.drone_status == EXODRONE_EXPLORATION)
var/datum/exploration_event/chosen_event = locate(params["target_event"]) in controlled_drone.location.events
if(chosen_event)
controlled_drone.explore_site(chosen_event)
if("adventure_choice")
if(controlled_drone && controlled_drone.drone_status == EXODRONE_ADVENTURE)
controlled_drone.current_adventure?.select_choice(params["choice"])
return TRUE
if("start_event")
if(controlled_drone && controlled_drone.current_event_ui_data)
var/datum/exploration_event/simple/chosen_event = locate(controlled_drone.current_event_ui_data["ref"]) in controlled_drone.location.events
if(chosen_event)
chosen_event.fire(controlled_drone)
return TRUE
if("skip_event")
if(controlled_drone && controlled_drone.current_event_ui_data)
var/datum/exploration_event/simple/chosen_event = locate(controlled_drone.current_event_ui_data["ref"]) in controlled_drone.location.events
if(chosen_event.skippable)
chosen_event.end(controlled_drone)
return TRUE
if("jettison")
if(controlled_drone)
var/obj/thing_to_jettison = locate(params["target_ref"]) in controlled_drone.contents
if(thing_to_jettison)
controlled_drone.drone_log("Jettisoned [thing_to_jettison]")
if(controlled_drone.drone_status == EXODRONE_IDLE)
thing_to_jettison.forceMove(controlled_drone.drop_location())
else
qdel(thing_to_jettison) //this might need some limitations
return TRUE
/obj/machinery/computer/exodrone_control_console/ui_assets(mob/user)
return list(get_asset_datum(/datum/asset/simple/adventure)) //preset screens
+452
View File
@@ -0,0 +1,452 @@
/// 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 300
#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 = "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()
. = ..()
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
GLOB.exodrones += src
/// Cargo storage
var/datum/component/storage/storage = AddComponent(/datum/component/storage/concrete)
storage.cant_hold = GLOB.blacklisted_cargo_types
storage.max_w_class = WEIGHT_CLASS_NORMAL
storage.max_items = EXODRONE_CARGO_SLOTS
/obj/item/exodrone/Destroy()
. = ..()
GLOB.exodrones -= src
/// Description for drone listing, describes location and current status
/obj/item/exodrone/proc/ui_description()
if(location)
switch(drone_status)
if(EXODRONE_TRAVEL)
return "Traveling back to station."
else
return "Exploring [location.display_name()]"
else
switch(drone_status)
if(EXODRONE_TRAVEL)
return "Traveling to exploration site."
else
return "Idle."
/// 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/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()
var/datum/component/storage/storage = GetComponent(/datum/component/storage/concrete)
storage.max_items = 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() > 1)
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()
var/obj/structure/closet/supplypod/pod = new
pod.bluespace = TRUE
new /obj/effect/pod_landingzone(random_spot, pod, src)
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)
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/resolve_adventure)
RegisterSignal(current_adventure,COMSIG_ADVENTURE_QUALITY_INIT,.proc/add_tool_qualities)
RegisterSignal(current_adventure,COMSIG_ADVENTURE_DELAY_START,.proc/adventure_delay_start)
RegisterSignal(current_adventure,COMSIG_ADVENTURE_DELAY_END,.proc/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)
drone_log.Insert(1,message)
if(length(drone_log) > EXODRONE_LOG_SIZE)
drone_log.Cut(EXODRONE_LOG_SIZE)
/obj/item/exodrone/proc/has_tool(tool_type)
return tools.Find(tool_type)
/// Exploration drone launcher
/obj/machinery/exodrone_launcher
name = "exploration drone launcher"
icon = 'icons/obj/exploration.dmi'
icon_state = "launcher"
/// Loaded fuel pellet.
var/obj/item/fuel_pellet/fuel_canister
/obj/machinery/exodrone_launcher/Initialize()
. = ..()
GLOB.exodrone_launchers += src
/obj/machinery/exodrone_launcher/attackby(obj/item/I, mob/living/user, params)
if(istype(I, /obj/item/fuel_pellet))
if(fuel_canister)
to_chat(user, "<span class='warning'>There's already a fuel tank inside [src]!</span>")
return TRUE
if(!user.transferItemToLoc(I, src))
return
fuel_canister = I
update_icon()
return TRUE
else if(istype(I,/obj/item/exodrone) && user.transferItemToLoc(I, drop_location()))
return TRUE
else
return ..()
/obj/machinery/exodrone_launcher/crowbar_act(mob/living/user, obj/item/I)
. = ..()
if(fuel_canister)
to_chat(user, "<span class='notie'>You remove the fuel tank from [src].</span>")
fuel_canister.forceMove(drop_location())
fuel_canister = null
/obj/machinery/exodrone_launcher/Destroy()
. = ..()
GLOB.exodrone_launchers -= src
/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"
/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
/obj/machinery/exodrone_launcher/proc/fuel_up(obj/item/exodrone/drone)
drone.travel_cost_coeff = get_fuel_coefficent()
fuel_canister.use()
/obj/machinery/exodrone_launcher/proc/launch_effect()
playsound(src,'sound/effects/podwoosh.ogg',50, FALSE)
do_smoke(1,get_turf(src))
/obj/machinery/exodrone_launcher/handle_atom_del(atom/A)
if(A == 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 = "compressed fuel pellet for long-distance flight"
icon = 'icons/obj/exploration.dmi'
icon_state = "fuel_basic"
var/fuel_type = FUEL_BASIC
var/uses = 5
/obj/item/fuel_pellet/use()
uses--
if(uses < 0)
qdel(src)
/obj/item/fuel_pellet/advanced
fuel_type = FUEL_ADVANCED
icon_state = "fuel_advanced"
/obj/item/fuel_pellet/exotic
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
@@ -0,0 +1,88 @@
/// Exploration event
/datum/exploration_event
/// These types will be ignored in event creation
var/root_abstract_type = /datum/exploration_event
///This name will show up in exploration list if it's repeatable
var/name = "Something interesting"
/// encountered at least once
var/visited = FALSE
/// Modifies site scan results by these
var/band_values
/// This will be added to site description, mind this will most likely reveal presence of this event early if set.
var/site_description_mod
/// message logged when first encountering the event.
var/discovery_log
/// Exploration site required_traits for this event to show up
var/required_site_traits
/// If these site traits are present the event won't show up
var/blacklisted_site_traits
/// Optional description that will be added to site description when point scan is completed.
var/point_scan_description
/// Optional description that will be added to site description when point scan is completed.
var/deep_scan_description
/// Main event functionality, called when exploring randomly/revisiting.
/datum/exploration_event/proc/encounter(obj/item/exodrone/drone)
SHOULD_CALL_PARENT(TRUE)
if(!visited)
var/log = get_discovery_message(drone)
if(log)
drone.drone_log(log)
visited = TRUE
/// Override this if you need to modify discovery message
/datum/exploration_event/proc/get_discovery_message(obj/item/exodrone/drone)
return discovery_log
/// Should this event show up on site exploration list.
/datum/exploration_event/proc/is_targetable()
return FALSE
/// Simple events, not a full fledged adventure, consist only of single encounter screen
/datum/exploration_event/simple
root_abstract_type = /datum/exploration_event/simple
var/ui_image = "default"
/// Show ignore button.
var/skippable = TRUE
/// Ignore button text
var/ignore_text = "Ignore"
/// Action text, can be further parametrized in get_action_text()
var/action_text = "encounter"
/// Description, can be further parametrized in get_description()
var/description = "You encounter a bug."
/// On exploration, only display our information with the act/ignore options
/datum/exploration_event/simple/encounter(obj/item/exodrone/drone)
. = ..()
drone.current_event_ui_data = build_ui_event(drone)
/// After choosing not to ignore the event, THIS IS DONE AFTER UNKNOWN DELAY SO YOU NEED TO VALIDATE IF ACTION IS POSSIBLE AGAIN
/datum/exploration_event/simple/proc/fire(obj/item/exodrone/drone)
return
/// Ends simple event and cleans up display data
/datum/exploration_event/simple/proc/end(obj/item/exodrone/drone)
drone.current_event_ui_data = null
/// Description shown below image
/datum/exploration_event/simple/proc/get_description(obj/item/exodrone/drone)
return description
/// Text on the act button
/datum/exploration_event/simple/proc/get_action_text(obj/item/exodrone/drone)
return action_text
/// Button to act disabled or not
/datum/exploration_event/simple/proc/action_enabled(obj/item/exodrone/drone)
return TRUE
/// Creates ui data for displaying the event
/datum/exploration_event/simple/proc/build_ui_event(obj/item/exodrone/drone)
. = list()
.["image"] = ui_image
.["description"] = get_description(drone)
.["action_enabled"] = action_enabled(drone)
.["action_text"] = get_action_text(drone)
.["skippable"] = skippable
.["ignore_text"] = ignore_text
.["ref"] = ref(src)
@@ -0,0 +1,9 @@
/// Adventure wrapper event
/datum/exploration_event/adventure
discovery_log = "Encountered something unexpected"
var/datum/adventure/adventure
root_abstract_type = /datum/exploration_event/adventure
/datum/exploration_event/adventure/encounter(obj/item/exodrone/drone)
. = ..()
drone.start_adventure(adventure)
@@ -0,0 +1,138 @@
/// Danger event - unskippable, if you have appriopriate tool you can mitigate damage.
/datum/exploration_event/simple/danger
root_abstract_type = /datum/exploration_event/simple/danger
description = "You encounter a giant error."
var/required_tool = EXODRONE_TOOL_LASER
var/has_tool_action_text = "Fight"
var/no_tool_action_text = "Endure"
var/has_tool_description = ""
var/no_tool_description = ""
var/avoid_log = "Escaped unharmed from danger."
var/damage = 30
skippable = FALSE
/datum/exploration_event/simple/danger/get_description(obj/item/exodrone/drone)
. = ..()
var/list/desc_parts = list(.)
desc_parts += can_escape_danger(drone) ? has_tool_description : no_tool_description
return desc_parts.Join("\n")
/datum/exploration_event/simple/danger/get_action_text(obj/item/exodrone/drone)
return can_escape_danger(drone) ? has_tool_action_text : no_tool_action_text
/datum/exploration_event/simple/danger/proc/can_escape_danger(obj/item/exodrone/drone)
return !required_tool || drone.has_tool(required_tool)
/datum/exploration_event/simple/danger/fire(obj/item/exodrone/drone)
if(can_escape_danger(drone))
drone.drone_log(avoid_log)
else
drone.damage(damage)
end(drone)
/// Danger events
/datum/exploration_event/simple/danger/carp
name = "space carp attack"
required_site_traits = list(EXPLORATION_SITE_SPACE)
blacklisted_site_traits = list(EXPLORATION_SITE_CIVILIZED)
deep_scan_description = "You detect damage patterns to the site hinting at a presence of space carp."
description = "You are ambushed by a solitary space carp!"
has_tool_action_text = "Fight"
no_tool_action_text = "Escape!"
has_tool_description = "You charge your laser to fend it off."
no_tool_description = "Unfortunately you have no weaponry so the only option is flight."
avoid_log = "Defeated a space carp."
/// They get everywhere
/datum/exploration_event/simple/danger/carp/surface_variety
required_site_traits = list(EXPLORATION_SITE_SURFACE)
/datum/exploration_event/simple/danger/assistant
name = "assistant attack"
required_site_traits = list(EXPLORATION_SITE_STATION)
deep_scan_description = "Detected mask usage coefficent suggests a sizeable crowd of undersirables on the site."
description = "You encounter a shaggy creature dressed in gray! It's a deranged assistant!"
has_tool_action_text = "Fight"
no_tool_action_text = "Escape!"
has_tool_description = "You charge your laser to fend it off."
no_tool_description = "Unfortunately you have no weaponry so the only option is flight."
avoid_log = "Defeated an assistant."
/datum/exploration_event/simple/danger/collapse
name = "collapse"
required_site_traits = list(EXPLORATION_SITE_RUINS)
required_tool = EXODRONE_TOOL_DRILL
deep_scan_description = "The architecture of the site is unstable, caution advised."
description = "A damaged ceiling gives up as you search an unexplored passage! You're trapped by the debris."
has_tool_action_text = "Dig out"
no_tool_action_text = "Squeeze."
has_tool_description = "You can use your drill to get out."
no_tool_description = "You'll have to scrape a few parts to get out without any tools."
avoid_log = "Dug out of collapsed passage."
/datum/exploration_event/simple/danger/loose_wires
name = "loose wires"
required_site_traits = list(EXPLORATION_SITE_TECHNOLOGY)
required_tool = EXODRONE_TOOL_MULTITOOL
deep_scan_description = "Damaged wiring detected on site."
description = "You hear a loud snap behind you! A stack of sparking high-voltage wires is blocking you way out."
has_tool_action_text = "Disable power"
no_tool_action_text = "Get fried."
has_tool_description = "You can try to use your multitool to shut down power to escape."
no_tool_description = "You'll have to risk frying your electronics getting out."
avoid_log = "Escaped loose wire."
/datum/exploration_event/simple/danger/cosmic_rays
name = "cosmic ray burst"
required_site_traits = list(EXPLORATION_SITE_SURFACE)
required_tool = EXODRONE_TOOL_MULTITOOL
deep_scan_description = "Site is exposed to space radiation. Using self-diagnostic multiool attachment advised."
description = "Drone feed suddenly goes haywire! It seems that the drone got hit by extremely rare cosmic ray burst! You'll have to wait for signal to be restored."
has_tool_description = "Multitool extension self-diagnostic attachement should deal with most of the damage automatically."
no_tool_description = "Nothing more to be done than wait and asses the damage."
has_tool_action_text = "Wait"
no_tool_action_text = "Wait"
avoid_log = "Prevented cosmic ray damage with multitool"
/datum/exploration_event/simple/danger/alien_sentry
name = "alien security measure"
required_site_traits = list(EXPLORATION_SITE_ALIEN)
required_tool = EXODRONE_TOOL_TRANSLATOR
deep_scan_description = "Automated security measures of unknown origin detected on site."
description = "A dangerous looking machine slides out the floor and start flashing strange glyphs while emitting high-pitched sound."
has_tool_description = "Your translator recognizes the glyphs as security hail and suggests identyfing yourself as guest."
no_tool_description = "The machine start shooting soon after."
has_tool_action_text = "Identify yourself"
no_tool_action_text = "Escape"
avoid_log = "Avoided alien security"
/datum/exploration_event/simple/danger/beast
name = "alien encounter"
required_site_traits = list(EXPLORATION_SITE_HABITABLE)
blacklisted_site_traits = list(EXPLORATION_SITE_CIVILIZED)
required_tool = EXODRONE_TOOL_LASER
deep_scan_description = "Dangerous fauna detected on site."
description = "You encounter BEAST. It prepares to strike."
has_tool_action_text = "Fight"
no_tool_action_text = "Escape"
has_tool_description = "You ready your laser."
no_tool_description = "Time to run."
avoid_log = "Defeated BEAST"
/datum/exploration_event/simple/danger/beast/New()
. = ..()
var/beast_name = pick_list(EXODRONE_FILE,"alien_fauna")
description = replacetext(description,"BEAST",beast_name)
avoid_log = replacetext(avoid_log,"BEAST",beast_name)
/datum/exploration_event/simple/danger/rad
name = "irradiated section"
required_site_traits = list(EXPLORATION_SITE_SHIP)
required_tool = EXODRONE_TOOL_MULTITOOL
deep_scan_description = "Sections of the vessel are irradiated."
description = "You enter a nondescript ship section."
has_tool_action_text = "Detour"
no_tool_action_text = "Escape and mitigate damage."
has_tool_description = "Your multitool suddenly screams in warning! Section ahead is irradiated, you'll have to go around"
no_tool_description = "Suddenly the drone reports significant damage, it seems this section was heavily irradiated."
avoid_log = "Avoided irradiated section"
@@ -0,0 +1,6 @@
/// Just a message in the log nothing more
/datum/exploration_event/fluff
name = "fluff event"
/datum/exploration_event/fluff/get_discovery_message(obj/item/exodrone/drone)
return pick_list(EXODRONE_FILE,drone.location.fluff_type)
@@ -0,0 +1,296 @@
/// Simple event type that checks if you have a tool and after a retrieval delay adds loot to drone.
/datum/exploration_event/simple/resource
name = "Retrievable resource"
root_abstract_type = /datum/exploration_event/simple/resource
discovery_log = "Encountered recoverable resource"
action_text = "Extract"
/// Tool type required to recover this resource
var/required_tool
/// What you get out of it, either /obj path or adventure_loot_generator id
var/loot_type = /obj/item/trash/chips
/// Message logged on success
var/success_log = "Retrieved something"
/// Description shown when you don't have the tool
var/no_tool_description = "You can't retrieve it without a tool"
/// Description shown when you have the necessary tool
var/has_tool_description = "You can get it out with that tool."
var/delay = 30 SECONDS
var/delay_message = "Recovering resource..."
/// How many times can this be extracted
var/amount = 1
/// Description shown below image
/datum/exploration_event/simple/resource/get_description(obj/item/exodrone/drone)
. = ..()
var/list/desc_list = list(.)
if(!required_tool || drone.has_tool(required_tool))
desc_list += has_tool_description
else
desc_list += no_tool_description
return desc_list.Join("\n")
/datum/exploration_event/simple/resource/action_enabled(obj/item/exodrone/drone)
return (amount > 0) && (!required_tool || drone.has_tool(required_tool))
/datum/exploration_event/simple/resource/fire(obj/item/exodrone/drone)
if(!action_enabled(drone)) //someone used it up or we lost the tool while we were looking at ui
end()
return
amount--
if(delay > 0)
drone.set_busy(delay_message,delay)
addtimer(CALLBACK(src,.proc/delay_finished,WEAKREF(drone)),delay)
else
finish_event(drone)
/datum/exploration_event/simple/resource/is_targetable()
return visited && amount > 0 ///Can go back if something is left.
/datum/exploration_event/simple/resource/proc/delay_finished(datum/weakref/drone_ref)
var/obj/item/exodrone/drone = drone_ref.resolve()
if(QDELETED(drone)) //drone blown up in the meantime
return
drone.unset_busy(EXODRONE_EXPLORATION)
finish_event(drone)
/datum/exploration_event/simple/resource/proc/finish_event(obj/item/exodrone/drone)
drone.drone_log(success_log)
dispense_loot(drone)
end(drone)
/datum/exploration_event/simple/resource/proc/dispense_loot(obj/item/exodrone/drone)
if(ispath(loot_type,/datum/adventure_loot_generator))
var/datum/adventure_loot_generator/generator = new loot_type
generator.transfer_loot(drone)
else
var/obj/loot = new loot_type()
drone.try_transfer(loot)
/// Resource Events
// All
/datum/exploration_event/simple/resource/concealed_cache
name = "Concealed Cache"
band_values = list(EXOSCANNER_BAND_DENSITY=1)
required_tool = EXODRONE_TOOL_WELDER
discovery_log = "Discovered concealed and locked cache."
description = "You spot a cleverly hidden metal container."
no_tool_description = "You see no way to open it without a welder."
has_tool_description = "You can try to open it with your welder"
action_text = "Weld open"
delay_message = "Welding open the cache..."
loot_type = /datum/adventure_loot_generator/maintenance
// EXPLORATION_SITE_RUINS 2/2
/datum/exploration_event/simple/resource/remnants
name = "dessicated corpse"
required_site_traits = list(EXPLORATION_SITE_RUINS)
required_tool = EXODRONE_TOOL_MULTITOOL
discovery_log = "You discovered a corpse of a humanoid."
description = "You find a dessicated corpose of a humanoid, it's too damaged to identify. A locked briefcase is lying nearby."
no_tool_description = "You can't open it without a multiool"
has_tool_description = "You can try to hack it open"
action_text = "Hack open"
delay_message = "Hacking..."
loot_type = /datum/adventure_loot_generator/simple/cash
/datum/exploration_event/simple/resource/gunfight
name = "gunfight leftovers"
required_site_traits = list(EXPLORATION_SITE_RUINS)
required_tool = EXODRONE_TOOL_DRILL
discovery_log = "You discovered a site of some past gunfight."
description = "You find a site full of gun casing and scorched with laser marks. You notice something under rubble nearby."
no_tool_description = "You can't get to it without a drill"
action_text = "Remove rubble"
delay_message = "Drilling..."
loot_type = /datum/adventure_loot_generator/simple/weapons
// EXPLORATION_SITE_TECHNOLOGY 2/2
/datum/exploration_event/simple/resource/maint_room
name = "locked maintenance room"
required_site_traits = list(EXPLORATION_SITE_TECHNOLOGY,EXPLORATION_SITE_STATION)
required_tool = EXODRONE_TOOL_MULTITOOL
discovery_log = "You discovered a locked maintenance room."
success_log = "Retrieved contents of maintenance room."
description = "You discover a locked maintenance room. You can see marks of something being moved often from it nearby."
no_tool_description = "You can't open it without a multitool"
action_text = "Hack"
delay_message = "Hacking..."
loot_type = /datum/adventure_loot_generator/maintenance
amount = 3
/datum/exploration_event/simple/resource/storage
name = "storage room"
required_site_traits = list(EXPLORATION_SITE_TECHNOLOGY,EXPLORATION_SITE_STATION)
required_tool = EXODRONE_TOOL_TRANSLATOR
discovery_log = "You discovered a storage room full of crates."
success_log = "Used translated manifest to find a crate with double bottom."
description = "You find a storage room full of empty crates. There's a manifest in some obscure language pinned near the entrance."
no_tool_description = "You can only see empty crates, and can't understand the manifest without a translator."
action_text = "Translate"
delay_message = "Translating manifest..."
loot_type = /datum/adventure_loot_generator/simple/drugs
// EXPLORATION_SITE_ALIEN 2/2
/datum/exploration_event/simple/resource/alien_tools
name = "alien sarcophagus"
required_site_traits = list(EXPLORATION_SITE_ALIEN)
band_values = list(EXOSCANNER_BAND_TECH=1,EXOSCANNER_BAND_RADIATION=1)
required_tool = EXODRONE_TOOL_TRANSLATOR
discovery_log = "Discovered a alien sarcophagus covered in unknown glyphs"
success_log = "Retrieved contents of alien sarcophagus"
description = "You find an giant sarcophagus of alien origin covered in unknown script."
no_tool_description = "You see no way to open the sarcophagus or translate the glyphs without a tool."
has_tool_description = "You translate the glyphs and find a description of a hidden mechanism for unlocking the tomb."
delay_message = "Opening..."
action_text = "Open"
loot_type = /obj/item/scalpel/alien
/datum/exploration_event/simple/resource/pod
name = "alien biopod"
required_site_traits = list(EXPLORATION_SITE_ALIEN)
band_values = list(EXOSCANNER_BAND_LIFE=1)
required_tool = EXODRONE_TOOL_LASER
discovery_log = "Discovered an alien pod."
success_log = "Retrieved contents of the alien pod"
description = "You encounter an alien biomachinery full of sacks containing some lifeform."
no_tool_description = "You can't open them without precise laser."
has_tool_description = "You can try to cut one open with a laser."
delay_message = "Opening..."
action_text = "Open"
loot_type = /datum/adventure_loot_generator/pet
// EXPLORATION_SITE_SHIP 2/2
/datum/exploration_event/simple/resource/fuel_storage
name = "fuel storage"
required_site_traits = list(EXPLORATION_SITE_SHIP)
band_values = list(EXOSCANNER_BAND_PLASMA=1)
required_tool = EXODRONE_TOOL_MULTITOOL
discovery_log = "Discovered ship fuel storage."
description = "You find the ship fuel storage. Unfortunately it's locked with electronic lock."
success_log = "Retrieved fuel from storage."
no_tool_description = "You'll need multitool to open it."
delay_message = "Opening..."
action_text = "Open"
loot_type = /obj/item/fuel_pellet/exotic
/datum/exploration_event/simple/resource/navigation
name = "navigation systems"
required_site_traits = list(EXPLORATION_SITE_SHIP)
required_tool = EXODRONE_TOOL_TRANSLATOR
discovery_log = "Discovered ship navigation systems."
description = "You find the ship navigation systems. With proper tools you can retrieve any data stored here."
success_log = "Retrieved shipping data from navigation systems."
no_tool_description = "You'll need a translator to decipher the data."
delay_message = "Retrieving data..."
action_text = "Retrieve data"
loot_type = /datum/adventure_loot_generator/cargo
// EXPLORATION_SITE_HABITABLE 2/2
/datum/exploration_event/simple/resource/unknown_microbiome
name = "unknown microbiome"
required_site_traits = list(EXPLORATION_SITE_HABITABLE)
required_tool = EXODRONE_TOOL_TRANSLATOR
discovery_log = "Discovered a isolated microbiome."
description = "You discover a giant fungus colony."
success_log = "Retrieved samples of the fungus for future study."
no_tool_description = "With a laser tool you could slice off a sample for study."
delay_message = "Taking samples..."
action_text = "Take sample"
loot_type = /obj/item/petri_dish/random
/datum/exploration_event/simple/resource/tcg_nerd
name = "creepy stranger"
required_site_traits = list(EXPLORATION_SITE_HABITABLE)
band_values = list(EXOSCANNER_BAND_LIFE=1)
required_tool = EXODRONE_TOOL_TRANSLATOR
discovery_log = "Met a creepy stranger."
description = "You meet an inhabitant of this site. Smelling horribly and clearly agitated about something."
no_tool_description = "You have no idea what it wants from you without a translator."
has_tool_description = "Your best translation is that it wants to share its hobby with you. "
success_log = "Recieved a gift from a stranger."
delay_message = "Enduring..."
action_text = "Accept gift."
loot_type = /obj/item/cardpack/series_one
// EXPLORATION_SITE_SPACE 2/2
/datum/exploration_event/simple/resource/comms_satellite
name = "derelict comms satellite"
required_site_traits = list(EXPLORATION_SITE_SPACE)
required_tool = EXODRONE_TOOL_MULTITOOL
discovery_log = "You discovered a derelict communication satellite."
description = "You discover a derelict communication satellite. Its encryption module seem intact and can be retrieved."
no_tool_description = "You'll need a multiool to crack open the lock."
success_log = "Retrieved encryption keys from derelict satellite"
delay_message = "Hacking..."
action_text = "Hack lock"
loot_type = /obj/item/encryptionkey/heads/captain
/datum/exploration_event/simple/resource/welded_locker
name = "welded locker"
required_site_traits = list(EXPLORATION_SITE_SPACE)
required_tool = EXODRONE_TOOL_WELDER
discovery_log = "You discovered a welded shut locker."
description = "You discover a welded shut locker floating through space. What could be inside ?"
success_log = "Retrieved bones of unfortunate spaceman from a welded locker."
delay_message = "Welding open..."
action_text = "Weld open"
loot_type = /obj/item/bodypart/head
/datum/exploration_event/simple/resource/welded_locker/dispense_loot(obj/item/exodrone/drone)
var/mob/living/carbon/human/head_species_source = new
head_species_source.set_species(/datum/species/skeleton)
head_species_source.real_name = "spaced locker victim"
var/obj/item/bodypart/head/skeleton_head = new
skeleton_head.update_limb(FALSE,head_species_source)
qdel(head_species_source)
drone.try_transfer(skeleton_head)
// EXPLORATION_SITE_SURFACE 2/2
/datum/exploration_event/simple/resource/plasma_deposit
name = "Raw Plasma Deposit"
required_site_traits = list(EXPLORATION_SITE_SURFACE)
band_values = list(EXOSCANNER_BAND_PLASMA=3)
required_tool = EXODRONE_TOOL_DRILL
discovery_log = "Discovered a sizeable plasma deposit"
success_log = "Extracted plasma."
description = "You locate a rich surface deposit of plasma."
no_tool_description = "You'll need to come back with a drill to mine it."
has_tool_description = ""
action_text = "Mine"
delay_message = "Mining..."
loot_type = /obj/item/stack/sheet/mineral/plasma/thirty
/obj/item/stack/sheet/mineral/plasma/thirty
amount = 30
/datum/exploration_event/simple/resource/mineral_deposit
name = "MATERIAL Deposit"
required_site_traits = list(EXPLORATION_SITE_SURFACE)
band_values = list(EXOSCANNER_BAND_DENSITY=3)
required_tool = EXODRONE_TOOL_DRILL
discovery_log = "Discovered a sizeable MATRIAL deposit"
success_log = "Extracted MATERIAL."
description = "You locate a rich surface deposit of MATERIAL."
no_tool_description = "You'll need to come back with a drill to mine it."
has_tool_description = ""
action_text = "Mine"
delay_message = "Mining..."
var/static/list/possible_materials = list(/datum/material/silver,/datum/material/bananium,/datum/material/pizza) //only add materials with sheet type here
var/loot_amount = 30
var/chosen_material_type
/datum/exploration_event/simple/resource/mineral_deposit/New()
. = ..()
chosen_material_type = pick(possible_materials)
var/datum/material/chosen_mat = GET_MATERIAL_REF(chosen_material_type)
name = "[chosen_mat.name] Deposit"
discovery_log = "Discovered a sizeable [chosen_mat.name] deposit"
success_log = "Extracted [chosen_mat.name]."
description = "You locate a rich surface deposit of [chosen_mat.name]."
/datum/exploration_event/simple/resource/mineral_deposit/dispense_loot(obj/item/exodrone/drone)
var/datum/material/chosen_mat = GET_MATERIAL_REF(chosen_material_type)
var/obj/loot = new chosen_mat.sheet_type(loot_amount)
drone.try_transfer(loot)
@@ -0,0 +1,126 @@
/// Trader events - If drone is loaded with X exchanges it for Y, might require translator tool.
/datum/exploration_event/simple/trader
root_abstract_type = /datum/exploration_event/simple/trader
action_text = "Trade"
/// Obj path we'll take or list of paths ,one path will be picked from it at init
var/required_path
/// Obj path we'll give out or list of paths ,one path will be picked from it at init
var/traded_path
//How many times we'll allow the trade
var/amount = 1
var/requires_translator = TRUE
/datum/exploration_event/simple/trader/New()
. = ..()
if(islist(required_path))
required_path = pick(required_path)
if(islist(traded_path))
traded_path = pick(traded_path)
/datum/exploration_event/simple/trader/get_discovery_message(obj/item/exodrone/drone)
if(requires_translator && !drone.has_tool(EXODRONE_TOOL_TRANSLATOR))
return "You encountered [name] but could not understand what they want without a translator."
var/obj/want = required_path
var/obj/gives = traded_path
return "Encountered [name] willing to trade [initial(want.name)] for [initial(gives.name)]"
/datum/exploration_event/simple/trader/get_description(obj/item/exodrone/drone)
if(requires_translator && !drone.has_tool(EXODRONE_TOOL_TRANSLATOR))
return "You encounter [name] but cannot understand what they want without a translator."
var/obj/want = required_path
var/obj/gives = traded_path
return "You encounter [name] willing to trade [initial(want.name)] for [initial(gives.name)] [amount > 1 ? "[amount] times":""]."
/datum/exploration_event/simple/trader/is_targetable()
return visited && (amount > 0)
/datum/exploration_event/simple/trader/action_enabled(obj/item/exodrone/drone)
var/obj/trade_good = locate(required_path) in drone.contents
return (amount > 0) && trade_good && (!requires_translator || drone.has_tool(EXODRONE_TOOL_TRANSLATOR))
/datum/exploration_event/simple/trader/fire(obj/item/exodrone/drone)
if(!action_enabled(drone))
end(drone)
return
amount--
trade(drone)
end(drone)
/datum/exploration_event/simple/trader/proc/trade(obj/item/exodrone/drone)
var/obj/trade_good = locate(required_path) in drone.contents
var/obj/loot = new traded_path()
drone.drone_log("Traded [trade_good] for [loot]")
qdel(trade_good)
drone.try_transfer(loot)
/// Trade events
/datum/exploration_event/simple/trader/vendor_ai
name = "sentient drug vending machine"
required_site_traits = list(EXPLORATION_SITE_TECHNOLOGY)
band_values = list(EXOSCANNER_BAND_TECH=2)
requires_translator = FALSE
required_path = /obj/item/stock_parts/cell/high
traded_path = /obj/item/storage/pill_bottle/happy
amount = 3
/datum/exploration_event/simple/trader/farmer_market
name = "farmer's market"
deep_scan_description = "You detect a spot with unusal concentraction of edibles on the site."
required_site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_SURFACE)
band_values = list(EXOSCANNER_BAND_LIFE=2)
required_path = /obj/item/stock_parts/manipulator/nano
traded_path = list(/obj/item/seeds/tomato/killer,/obj/item/seeds/orange_3d,/obj/item/seeds/firelemon,/obj/item/seeds/gatfruit)
amount = 1
/datum/exploration_event/simple/trader/fish
name = "interstellar fish trader"
requires_translator = FALSE
deep_scan_description = "You spot gian \"FRESH FISH\" sign on the site."
required_site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_SURFACE)
band_values = list(EXOSCANNER_BAND_LIFE=2)
required_path = /obj/item/stock_parts/cell/high
traded_path = /obj/item/storage/fish_case/random
amount = 3
/datum/exploration_event/simple/trader/shady_merchant
name = "shady merchant"
requires_translator = FALSE
required_site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_CIVILIZED)
band_values = list(EXOSCANNER_BAND_LIFE=1)
required_path = list(/obj/item/organ/heart,/obj/item/organ/liver,/obj/item/organ/stomach,/obj/item/organ/eyes)
traded_path = list(/obj/item/implanter/explosive)
amount = 1
/datum/exploration_event/simple/trader/surplus
name = "military surplus trader"
deep_scan_description = "You decrypt a transmission advertising military surplus sale on the site."
required_site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_CIVILIZED)
band_values = list(EXOSCANNER_BAND_LIFE=1)
required_path = list(/obj/item/clothing/suit/armor,/obj/item/clothing/shoes/jackboots)
traded_path = /obj/item/gun/energy/laser/retro/old
amount = 3
/datum/exploration_event/simple/trader/flame_card
name = "id card artisan"
deep_scan_description = "You spy a adveristment for an id card customization workshop."
required_site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_CIVILIZED)
band_values = list(EXOSCANNER_BAND_TECH=1)
required_path = list(/obj/item/card/id) //If you trade a better card for worse that's on you
traded_path = null
requires_translator = FALSE
amount = 1
var/static/list/possible_card_states = list("card_flames","card_carp","card_rainbow")
/datum/exploration_event/simple/trader/flame_card/get_discovery_message(obj/item/exodrone/drone)
return "Encountered [name] willing to customize any id card you bring them."
/datum/exploration_event/simple/trader/flame_card/get_description(obj/item/exodrone/drone)
return "You encounter local craftsman willing to improve an id card for you free of charge."
/datum/exploration_event/simple/trader/flame_card/trade(obj/item/exodrone/drone)
var/obj/item/card/id/card = locate(required_path) in drone.contents
card.icon_state = pick(possible_card_states)
card.update_icon() //Refresh cached helper image
drone.drone_log("Let artisan work on [card.name].")
@@ -0,0 +1,265 @@
/// All exploration site instances
GLOBAL_LIST_EMPTY(exploration_sites)
// Band is general distance group. Cost of scanning bands increasly exponentialy.
/proc/generate_exploration_sites()
var/band = GLOB.exoscanner_controller.wide_scan_band
var/site_count = 1+rand(band-1,band+1)
var/site_types = subtypesof(/datum/exploration_site) //cache?
for(var/i in 1 to site_count)
var/site_type = pick(site_types)
var/datum/exploration_site/fresh_site = new site_type(band)
GLOB.exploration_sites += fresh_site
GLOB.exoscanner_controller.wide_scan_band += 1
/// Exploration site, drone travel destination representing interesting zone for exploration.
/datum/exploration_site
/// Name displayed after scanning/exploring
var/name
/// Description shown after scanning/exploring
var/description
/// How far is it, affects travel time/cost.
var/distance = 1
/// Coordinates in Station coordinate system - don't ask if station rotates
var/coordinates
/// Was the point scan done or a drone arrived on the site. Affects displayed name/description
var/revealed = FALSE
/// Was point scan of this site completed.
var/point_scan_complete = FALSE
/// Was deep scan of this site completed.
var/deep_scan_complete = FALSE
/// Contains baseline site bands at define time. Events bands will be added to this list as part of event generation.
var/list/band_info = list()
/// List of event instances represting thing to be found around this exploration site.
var/list/events = list()
/// These are used to determine events/adventures possible for this site
var/site_traits = list()
/// Key for strings file fluff events
var/fluff_type = "fluff_generic"
/// List of scan conditions for this site - scan conditions are singletons
var/list/datum/scan_condition/scan_conditions
/datum/exploration_site/New(band)
. = ..()
distance = max(band+pick(-1,0,1,2),1)
coordinates = ":[rand(0,360)]°,𝑏:[rand(0,90)]°" // and 𝑏 are symbols for longitude/inclination in made-up station centric coordinate system.
generate_events()
generate_scan_conditions()
/datum/exploration_site/proc/generate_events()
/// Try to find aventure first since they're the meat of the system.
var/datum/exploration_event/adventure = generate_adventure(site_traits)
if(adventure)
add_event(adventure)
/// Fill other events
/// Baseline weights for each event root type
var/static/list/base_weights = list(
/datum/exploration_event/fluff = 2,
/datum/exploration_event/simple/danger = 2,
/datum/exploration_event/simple/trader = 1,
/datum/exploration_event/simple/resource = 1
)
/// Weight mods scaled by distance, resources are more easily found on farther sites
var/static/list/distance_modifiers = list(
/datum/exploration_event/simple/trader = 0.3,
/datum/exploration_event/simple/resource = 0.3
)
var/list/category_weights = base_weights.Copy()
for(var/modifier in distance_modifiers)
category_weights[modifier] += distance*distance_modifiers[modifier]
var/min_events_amount = CEILING(0.4*distance+0.2,1)
for(var/i in 1 to rand(min_events_amount,min_events_amount+2))
var/chosen_category = pickweight(category_weights)
var/datum/exploration_event/event = generate_event(site_traits,chosen_category)
if(event)
add_event(event)
/datum/exploration_site/proc/generate_scan_conditions()
var/condition_count = pick(3;0,2;1,1;2) //scale this with distance maybe ?
var/list/possible_conditions = GLOB.scan_conditions.Copy()
for(var/i in 1 to condition_count)
LAZYADD(scan_conditions,pick_n_take(possible_conditions))
/datum/exploration_site/proc/generate_adventure(site_traits)
var/list/possible_adventures = list()
for(var/datum/adventure/adventure_candidate in GLOB.explorer_drone_adventures)
if(adventure_candidate.placed || (adventure_candidate.required_site_traits && length(adventure_candidate.required_site_traits - site_traits) != 0))
continue
possible_adventures += adventure_candidate
if(!length(possible_adventures))
return
var/datum/adventure/chosen_adventure = pick(possible_adventures)
chosen_adventure.placed = TRUE
var/datum/exploration_event/adventure/adventure_event = new
adventure_event.adventure = chosen_adventure
adventure_event.band_values = chosen_adventure.band_modifiers
return adventure_event
/datum/exploration_site/proc/generate_event(site_traits,event_root_type)
/// List of exploration event requirements indexed by type, .[/datum/exploration_site/a] = list("required"=list(trait),"blacklisted"=list(other_trait))
var/static/exploration_event_requirements_cache = list()
if(!length(exploration_event_requirements_cache))
exploration_event_requirements_cache = build_exploration_event_requirements_cache()
var/list/viable_events = list()
for(var/event_type in exploration_event_requirements_cache)
var/list/required_traits = exploration_event_requirements_cache[event_type]["required"]
var/list/blacklisted_traits = exploration_event_requirements_cache[event_type]["blacklisted"]
if(!ispath(event_type,event_root_type))
continue
if(required_traits && length(required_traits - site_traits) != 0)
continue
if(blacklisted_traits && length(required_traits & blacklisted_traits) != 0)
continue
viable_events += event_type
if(!length(viable_events))
return
var/chosen_type = pick(viable_events)
return new chosen_type()
/datum/exploration_site/proc/build_exploration_event_requirements_cache()
. = list()
for(var/event_type in subtypesof(/datum/exploration_event))
var/datum/exploration_event/event = event_type
if(initial(event.root_abstract_type) == event_type)
continue
event = new event_type
.[event_type] = list("required" = event.required_site_traits,"blacklisted" = event.blacklisted_site_traits)
//Should be no event refs,GC'd naturally
/datum/exploration_site/proc/add_event(datum/exploration_event/event)
events += event
/// Add up event band values to ours
for(var/band in event.band_values)
if(band_info[band])
band_info[band] += event.band_values[band]
else
band_info[band] = event.band_values[band]
return
/datum/exploration_site/proc/on_drone_arrival(obj/item/exodrone/drone)
var/was_known_before = revealed
reveal()
if(!was_known_before)
drone.drone_log("Discovered [name] at [coordinates].")
else
drone.drone_log("Arrived at [display_name()].")
/datum/exploration_site/proc/reveal()
revealed = TRUE
/datum/exploration_site/proc/display_name()
return revealed ? name : "Anomaly"
/datum/exploration_site/proc/display_description()
if(!revealed)
return "No Data"
var/list/descriptions = list(description)
for(var/datum/exploration_event/event in events)
if(deep_scan_complete && event.deep_scan_description)
descriptions += event.deep_scan_description
else if(point_scan_complete && event.point_scan_description)
descriptions += event.point_scan_description
return descriptions.Join("\n")
/// Data for ui_data, exploration
/datum/exploration_site/proc/site_data(exploration=FALSE)
. = list()
.["ref"] = ref(src)
.["name"] = display_name()
.["coordinates"] = coordinates
.["description"] = display_description()
.["distance"] = distance
.["revealed"] = revealed
.["point_scan_complete"] = point_scan_complete
.["deep_scan_complete"] = deep_scan_complete
.["band_info"] = point_scan_complete ? band_info : list() //This loses order so when you iterate bands ui side use all_bands
if(exploration)
var/list/event_data = list()
for(var/datum/exploration_event/event in events)
if(event.visited && event.is_targetable())
event_data += list(list("name"=event.name,"ref"=ref(event)))
.["events"] = event_data
/// Helper proc for exploration site listings in ui.
/proc/build_exploration_site_ui_data()
. = list()
for(var/datum/exploration_site/site in GLOB.exploration_sites)
. += list(site.site_data())
/// Sites
/datum/exploration_site/abandoned_refueling_station
name = "abandoned refueling station"
description = "old shuttle refueling station drifting through the void."
band_info = list(EXOSCANNER_BAND_TECH = 1)
site_traits = list(EXPLORATION_SITE_RUINS,EXPLORATION_SITE_TECHNOLOGY,EXPLORATION_SITE_STATION)
/datum/exploration_site/trader_post
name = "unregistered trading station"
description = "Weak radio transmission advertises this place as RANDOMIZED_NAME"
band_info = list(EXOSCANNER_BAND_TECH = 1, EXOSCANNER_BAND_LIFE = 1)
site_traits = list(EXPLORATION_SITE_TECHNOLOGY,EXPLORATION_SITE_STATION,EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_CIVILIZED)
fluff_type = "fluff_trading"
/datum/exploration_site/trader_post/New(band)
. = ..()
var/chosen_name = pick_list(EXODRONE_FILE,"trading_station_names")
name = "\"[chosen_name]\" trading station"
description = replacetext(description,"RANDOMIZED_NAME",chosen_name)
/datum/exploration_site/cargo_wreck
name = "interstellar cargo ship wreckage"
description = "wreckage of long-range cargo shuttle"
band_info = list(EXOSCANNER_BAND_TECH = 1, EXOSCANNER_BAND_DENSITY = 1)
site_traits = list(EXPLORATION_SITE_SHIP,EXPLORATION_SITE_TECHNOLOGY)
/datum/exploration_site/alien_spaceship
name = "ancient alien spaceship"
description = "a gigantic spaceship of unknown origin, it doesnt respond to your hails but does not prevent you boarding either"
band_info = list(EXOSCANNER_BAND_TECH = 1, EXOSCANNER_BAND_RADIATION = 1)
site_traits = list(EXPLORATION_SITE_SHIP,EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_ALIEN)
/datum/exploration_site/uncharted_planet
name = "uncharted planet"
description = "planet missing from nanotrasen starcharts."
band_info = list(EXOSCANNER_BAND_LIFE = 3)
site_traits = list(EXPLORATION_SITE_SURFACE)
/datum/exploration_site/uncharted_planet/New(band)
/// Planet Type, Atmosphere
var/list/planet_info = pick_list(EXODRONE_FILE,"planet_types")
name = planet_info["name"]
description = planet_info["description"]
if(planet_info["habitable"])
site_traits += EXPLORATION_SITE_HABITABLE
if(planet_info["civilized"])
site_traits += EXPLORATION_SITE_CIVILIZED
if(planet_info["tech"])
site_traits += EXPLORATION_SITE_TECHNOLOGY
. = ..()
/datum/exploration_site/alien_ruins
name = "alien ruins"
description = "alien ruins on small moon surface."
site_traits = list(EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_SURFACE,EXPLORATION_SITE_ALIEN,EXPLORATION_SITE_RUINS)
fluff_type = "fluff_ruins"
/datum/exploration_site/asteroid_belt
name = "asteroid belt"
description = "dense asteroid belt"
site_traits = list(EXPLORATION_SITE_SURFACE)
fluff_type = "fluff_space"
/datum/exploration_site/spacemine
name = "mining facility"
description = "abandoned mining facility attached to ore-heavy asteroid"
band_info = list(EXOSCANNER_BAND_PLASMA = 3)
site_traits = list(EXPLORATION_SITE_RUINS,EXPLORATION_SITE_HABITABLE,EXPLORATION_SITE_SURFACE)
fluff_type = "fluff_ruins"
/datum/exploration_site/junkyard
name = "space junk field"
description = "a giant cluster of space junk."
band_info = list(EXOSCANNER_BAND_DENSITY = 3)
site_traits = list(EXPLORATION_SITE_TECHNOLOGY,EXPLORATION_SITE_SPACE)
fluff_type = "fluff_space"
+187
View File
@@ -0,0 +1,187 @@
GLOBAL_LIST_INIT(adventure_loot_generator_index,generate_generator_index())
/// Creates generator__id => type map.
/proc/generate_generator_index()
. = list()
for(var/type in typesof(/datum/adventure_loot_generator))
var/datum/adventure_loot_generator/generator = type
if(!initial(generator.id))
continue
.[initial(generator.id)] = type
/// Adventure loot category identified by ID
/datum/adventure_loot_generator
var/id
/datum/adventure_loot_generator/proc/generate()
return
/// Helper to transfer loot while respecting cargo space
/datum/adventure_loot_generator/proc/transfer_loot(obj/item/exodrone/drone)
for(var/obj/loot in generate())
drone.try_transfer(loot)
/// Uses manintenance loot generators
/datum/adventure_loot_generator/maintenance
id = "maint"
var/amount = 1
/datum/adventure_loot_generator/maintenance/generate()
var/list/all_loot = list()
for(var/i in 1 to amount)
var/lootspawn = pickweight(GLOB.maintenance_loot)
while(islist(lootspawn))
lootspawn = pickweight(lootspawn)
var/atom/movable/loot = new lootspawn()
all_loot += loot
return all_loot
/// Unlocks special cargo crates
/datum/adventure_loot_generator/cargo
id = "trade_contract"
var/static/list/unlockable_packs = list(/datum/supply_pack/exploration/scrapyard,/datum/supply_pack/exploration/catering,/datum/supply_pack/exploration/shrubbery)
/datum/adventure_loot_generator/cargo/generate()
var/list/still_locked_packs = list()
for(var/pack_type in unlockable_packs)
var/datum/supply_pack/pack_singleton = SSshuttle.supply_packs[pack_type]
if(!pack_singleton.special_enabled)
still_locked_packs += pack_type
if(!length(still_locked_packs)) // Just give out some cash instead.
var/datum/adventure_loot_generator/simple/cash/replacement = new
return replacement.generate()
var/chosen_pack_type = pick(still_locked_packs)
return new /obj/item/trade_chip(null,chosen_pack_type)
/// Just picks and instatiates the path from the list
/datum/adventure_loot_generator/simple
var/loot_list
/datum/adventure_loot_generator/simple/generate()
var/loot_type = pick(loot_list)
return list(new loot_type())
/// Unique exploration-only rewards - this is contextless
/datum/adventure_loot_generator/simple/unique
id = "unique"
loot_list = list(/obj/item/clothing/glasses/geist_gazers,/obj/item/clothing/glasses/psych,/obj/item/firelance)
/// Valuables
/datum/adventure_loot_generator/simple/cash
id = "cash"
loot_list = list(/obj/item/storage/bag/money,/obj/item/antique,/obj/item/stack/spacecash/c1000,/obj/item/holochip/thousand)
/// Drugs
/datum/adventure_loot_generator/simple/drugs
id = "drugs"
loot_list = list(/obj/item/storage/pill_bottle/happy,/obj/item/storage/pill_bottle/lsd,/obj/item/storage/pill_bottle/penacid,/obj/item/storage/pill_bottle/stimulant)
/// Rare minerals/materials
/datum/adventure_loot_generator/simple/materials
id = "materials"
loot_list = list(/obj/item/stack/sheet/iron/fifty,/obj/item/stack/sheet/plasteel/twenty)
/// Assorted weaponry
/datum/adventure_loot_generator/simple/weapons
id = "weapons"
loot_list = list(/obj/item/gun/energy/laser,/obj/item/melee/baton/loaded)
/// Pets and pet accesories in carriers
/datum/adventure_loot_generator/pet
id = "pets"
var/carrier_type = /obj/item/pet_carrier/biopod
var/list/possible_pets = list(/mob/living/simple_animal/pet/cat/space,/mob/living/simple_animal/pet/dog/corgi,/mob/living/simple_animal/pet/penguin/baby,/mob/living/simple_animal/pet/dog/pug)
/datum/adventure_loot_generator/pet/generate()
var/obj/item/pet_carrier/carrier = new carrier_type()
var/chosen_pet_type = pick(possible_pets)
var/mob/living/simple_animal/pet/pet = new chosen_pet_type()
carrier.add_occupant(pet)
return carrier
/obj/item/antique
name = "antique"
desc = "Valuable and completly incomprehensible."
icon = 'icons/obj/exploration.dmi'
icon_state = "antique"
/// Supply pack unlocker chip
/obj/item/trade_chip
name = "trade contract chip"
desc = "Uses the station's cargo network to contact a black market supplier, allowing the purchase of a new crate type at cargo console."
icon = 'icons/obj/exploration.dmi'
icon_state = "trade_chip"
/// Supply pack type enabled by this chip
var/unlocked_pack_type
/obj/item/trade_chip/Initialize(mapload, pack_type)
. = ..()
if(pack_type)
unlocked_pack_type = pack_type
var/datum/supply_pack/typed_pack_type = pack_type
name += "- [initial(typed_pack_type.name)]"
/obj/item/trade_chip/proc/try_to_unlock_contract(mob/user)
var/datum/supply_pack/pack_singleton = SSshuttle.supply_packs[unlocked_pack_type]
if(!unlocked_pack_type || !pack_singleton || !pack_singleton.special)
to_chat(user,"<span class='danger'>This chip is invalid!</span>")
return
pack_singleton.special_enabled = TRUE
to_chat(user,"<span class='notice'>Contract accepted into nanotrasen supply database.</span>")
qdel(src)
/// Two handed fire lance. Melts wall after short windup.
/obj/item/firelance
name = "fire lance"
desc = "Melts everything in front of you. Takes a while to start and operate."
icon = 'icons/obj/exploration.dmi'
icon_state = "firelance"
inhand_icon_state = "firelance"
righthand_file = 'icons/mob/inhands/misc/firelance_righthand.dmi'
lefthand_file = 'icons/mob/inhands/misc/firelance_lefthand.dmi'
var/windup_time = 10 SECONDS
var/melt_range = 3
var/charge_per_use = 200
var/obj/item/stock_parts/cell/cell
/obj/item/firelance/Initialize()
. = ..()
cell = new /obj/item/stock_parts/cell(src)
AddComponent(/datum/component/two_handed)
/obj/item/firelance/attack(mob/living/M, mob/living/user, params)
if(!user.combat_mode)
return
. = ..()
/obj/item/firelance/get_cell()
return cell
/obj/item/firelance/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
if(!HAS_TRAIT(src,TRAIT_WIELDED))
to_chat(user,"<span class='notice'>You need to wield [src] in two hands before you can fire it.</span>")
return
if(LAZYACCESS(user.do_afters, "firelance"))
return
if(!cell.use(charge_per_use))
to_chat(user,"<span class='warning'>[src] battery ran dry!</span>")
ADD_TRAIT(user,TRAIT_IMMOBILIZED,src)
to_chat(user,"<span class='notice'>You begin to charge [src]</span>")
inhand_icon_state = "firelance_charging"
user.update_inv_hands()
if(do_after(user,windup_time,interaction_key="firelance",extra_checks = CALLBACK(src, .proc/windup_checks)))
var/turf/start_turf = get_turf(user)
var/turf/last_turf = get_ranged_target_turf(start_turf,user.dir,melt_range)
start_turf.Beam(last_turf,icon_state="solar_beam",time=1 SECONDS)
for(var/turf/turf_to_melt in getline(start_turf,last_turf))
if(turf_to_melt.density)
turf_to_melt.Melt()
inhand_icon_state = initial(inhand_icon_state)
user.update_inv_hands()
REMOVE_TRAIT(user,TRAIT_IMMOBILIZED,src)
/// Additional windup checks
/obj/item/firelance/proc/windup_checks()
return HAS_TRAIT(src,TRAIT_WIELDED)
@@ -0,0 +1,367 @@
GLOBAL_DATUM_INIT(exoscanner_controller,/datum/scanner_controller,new)
/// List of scanned distances
GLOBAL_LIST_INIT(exoscanner_bands,list(EXOSCANNER_BAND_PLASMA=0,EXOSCANNER_BAND_LIFE=0,EXOSCANNER_BAND_TECH=0,EXOSCANNER_BAND_RADIATION=0,EXOSCANNER_BAND_DENSITY=0))
/// Scan condition instances
GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
/proc/init_scan_conditions()
. = list()
for(var/type in subtypesof(/datum/scan_condition))
. += new type
#define MAX_SCAN_DISTANCE 10
#define WIDE_SCAN_COST(BAND, SCAN_POWER) (((BAND*BAND)/(SCAN_POWER))*2*60*10)
#define BASE_POINT_SCAN_TIME 5 MINUTES
#define BASE_DEEP_SCAN_TIME 5 MINUTES
/// Represents scan in progress, only one globally for now, todo later split per z or allow partial dish swarm usage
/datum/exoscan
/// Scan type wide/point/deep
var/scan_type
/// The scan power this scan was started with, if scanner swarm power falls below this value it will be interrupted
var/scan_power = 0
/// Target site for point/band scans
var/datum/exploration_site/target
/// End of scan timer id
var/scan_timer
/datum/exoscan/New(scan_type,datum/exploration_site/target)
src.scan_type = scan_type
src.target = target
var/scan_time = 0
switch(scan_type)
if(EXOSCAN_WIDE)
scan_power = length(GLOB.exoscanner_controller.tracked_dishes)
scan_time = WIDE_SCAN_COST(GLOB.exoscanner_controller.wide_scan_band,scan_power)
if(EXOSCAN_POINT)
scan_power = GLOB.exoscanner_controller.get_scan_power(target)
scan_time = BASE_POINT_SCAN_TIME/scan_power
if(EXOSCAN_DEEP)
scan_power = GLOB.exoscanner_controller.get_scan_power(target)
scan_time = (BASE_DEEP_SCAN_TIME*target.distance)/scan_power
scan_timer = addtimer(CALLBACK(src,.proc/resolve_scan),scan_time,TIMER_STOPPABLE)
/// Short description for in progress scan
/datum/exoscan/proc/ui_description()
switch(scan_type)
if(EXOSCAN_WIDE)
return "Wide: Scanning sphere starting 1 AU from the station."
if(EXOSCAN_POINT)
return "Point scan of [target.display_name()]"
if(EXOSCAN_DEEP)
return "Deep scan of [target.display_name()]"
/datum/exoscan/proc/resolve_scan()
switch(scan_type)
if(EXOSCAN_WIDE)
generate_exploration_sites()
if(EXOSCAN_POINT)
target.reveal()
target.point_scan_complete = TRUE
if(EXOSCAN_DEEP)
target.reveal()
target.deep_scan_complete = TRUE
qdel(src)
/datum/exoscan/proc/stop()
SEND_SIGNAL(src,COMSIG_EXOSCAN_INTERRUPTED)
qdel(src)
/datum/exoscan/Destroy(force, ...)
. = ..()
deltimer(scan_timer)
/obj/machinery/computer/exoscanner_control
name = "Scanner Array Control Console"
/// If scan was interrupted show a popup until dismissed.
var/failed_popup = FALSE
/// Site we're configuring targeted scans for.
var/datum/exploration_site/selected_site
/obj/machinery/computer/exoscanner_control/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ExoscannerConsole", name)
ui.open()
/obj/machinery/computer/exoscanner_control/ui_data(mob/user)
. = ..()
.["failed"] = failed_popup
.["selected_site"] = selected_site && ref(selected_site)
var/scan_power = 0
if(selected_site)
.["site_data"] = selected_site.site_data()
.["scan_power"] = scan_power = GLOB.exoscanner_controller.get_scan_power(selected_site)
.["point_scan_eta"] = scan_power > 0 ? BASE_POINT_SCAN_TIME/scan_power : 0
.["deep_scan_eta"] = scan_power > 0 ? (BASE_DEEP_SCAN_TIME*selected_site.distance)/scan_power : 0
var/list/condition_descriptions = list()
for(var/datum/scan_condition/condition in selected_site.scan_conditions)
condition_descriptions += condition.description
.["scan_conditions"] = condition_descriptions
else
.["scan_power"] = scan_power = length(GLOB.exoscanner_controller.tracked_dishes)
.["wide_scan_eta"] = scan_power > 0 ? WIDE_SCAN_COST(GLOB.exoscanner_controller.wide_scan_band,scan_power) : 0
.["possible_sites"] = build_exploration_site_ui_data()
.["scan_conditions"] = null
.["scan_in_progress"] = !!GLOB.exoscanner_controller.current_scan
if(GLOB.exoscanner_controller.current_scan) //Display scan in progress info
.["scan_time"] = timeleft(GLOB.exoscanner_controller.current_scan.scan_timer)
.["current_scan_power"] = GLOB.exoscanner_controller.current_scan.scan_power
.["scan_description"] = GLOB.exoscanner_controller.current_scan.ui_description()
/obj/machinery/computer/exoscanner_control/ui_static_data(mob/user)
. = ..()
.["all_bands"] = GLOB.exoscanner_bands
/obj/machinery/computer/exoscanner_control/ui_act(action, list/params)
. = ..()
if(.)
return
switch(action)
if("select_site")
if(params["site_ref"])
var/datum/exploration_site/site = locate(params["site_ref"]) in GLOB.exploration_sites
if(site)
selected_site = site
else
selected_site = null
return TRUE
if("stop_scan")
stop_current_scan()
return TRUE
if("start_wide_scan")
start_wide_scan()
return TRUE
if("start_point_scan")
start_point_scan()
return TRUE
if("start_deep_scan")
start_deep_scan()
return TRUE
if("confirm_fail")
failed_popup = FALSE
return TRUE
/obj/machinery/computer/exoscanner_control/proc/stop_current_scan()
if(GLOB.exoscanner_controller.current_scan)
GLOB.exoscanner_controller.current_scan.stop()
/obj/machinery/computer/exoscanner_control/proc/start_wide_scan(radius)
if(GLOB.exoscanner_controller.current_scan)
return
if(GLOB.exoscanner_controller.wide_scan_band > MAX_SCAN_DISTANCE)
return
create_scan(EXOSCAN_WIDE)
/obj/machinery/computer/exoscanner_control/proc/start_point_scan()
if(GLOB.exoscanner_controller.current_scan || !selected_site || selected_site.point_scan_complete)
return
create_scan(EXOSCAN_POINT,selected_site)
/obj/machinery/computer/exoscanner_control/proc/start_deep_scan()
if(GLOB.exoscanner_controller.current_scan || !selected_site || selected_site.deep_scan_complete)
return
create_scan(EXOSCAN_DEEP,selected_site)
/obj/machinery/computer/exoscanner_control/proc/create_scan(scan_type,target)
var/datum/exoscan/scan = GLOB.exoscanner_controller.create_scan(scan_type,target)
if(scan)
RegisterSignal(scan, COMSIG_EXOSCAN_INTERRUPTED, .proc/scan_failed)
/obj/machinery/computer/exoscanner_control/proc/scan_failed()
SIGNAL_HANDLER
failed_popup = TRUE
SStgui.update_uis(src)
/obj/machinery/computer/exoscanner_control/Initialize()
..()
return INITIALIZE_HINT_LATELOAD
/obj/machinery/computer/exoscanner_control/LateInitialize()
. = ..()
AddComponent(/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/exploration_scan), \
config_mode = EXPERIMENT_CONFIG_UI, \
config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE)
/obj/machinery/exoscanner
name = "Scanner array"
icon = 'icons/obj/exploration.dmi'
icon_state = "scanner_off"
desc = "Sophisticated scanning array. Easily influenced by enviroment."
idle_power_usage = 0
active_power_usage = 500
/obj/machinery/exoscanner/Initialize()
. = ..()
RegisterSignal(GLOB.exoscanner_controller,list(COMSIG_EXOSCAN_STARTED,COMSIG_EXOSCAN_FINISHED),.proc/scan_change)
update_readiness()
/obj/machinery/exoscanner/proc/scan_change()
SIGNAL_HANDLER
if(GLOB.exoscanner_controller.current_scan)
use_power = ACTIVE_POWER_USE
else
use_power = IDLE_POWER_USE
update_icon_state()
/obj/machinery/exoscanner/Destroy()
. = ..()
GLOB.exoscanner_controller.deactivate_scanner(src)
/obj/machinery/exoscanner/proc/is_ready()
return anchored && is_operational
/obj/machinery/exoscanner/proc/update_readiness()
if(is_ready())
GLOB.exoscanner_controller.activate_scanner(src)
else
GLOB.exoscanner_controller.deactivate_scanner(src)
update_icon_state()
/obj/machinery/exoscanner/update_icon_state()
. = ..()
if(is_ready())
if(GLOB.exoscanner_controller.current_scan)
icon_state = "scanner_on"
else
icon_state = "scanner_ready"
else
icon_state = "scanner_off"
/obj/machinery/exoscanner/wrench_act(mob/living/user, obj/item/I)
..()
default_unfasten_wrench(user, I, 10)
return TRUE
/obj/machinery/exoscanner/set_anchored(anchorvalue)
. = ..()
update_readiness()
/obj/machinery/exoscanner/on_set_is_operational(old_value)
. = ..()
update_readiness()
///Helper datum to calculate and store scanning power and track in progress scans
/datum/scanner_controller
/// List of dishes in working condition.
var/list/tracked_dishes = list()
/// Scan currently in progress if any.
var/datum/exoscan/current_scan
/// Band for the next wide scan. Increased after successful completion of wide scan.
var/wide_scan_band = 1
/// Current scan power keyed by site
var/list/scan_power_cache = list()
/datum/scanner_controller/proc/create_scan(scan_type,datum/exploration_site/target)
if(current_scan)
return
if(length(GLOB.exoscanner_controller.tracked_dishes) <= 0 || (target && GLOB.exoscanner_controller.get_scan_power(target) <= 0))
return
current_scan = new(scan_type,target)
RegisterSignal(current_scan,COMSIG_PARENT_QDELETING,.proc/cleanup_current_scan)
SEND_SIGNAL(src,COMSIG_EXOSCAN_STARTED,current_scan)
return current_scan
/datum/scanner_controller/proc/cleanup_current_scan()
current_scan = null
SEND_SIGNAL(src,COMSIG_EXOSCAN_FINISHED,current_scan)
/datum/scanner_controller/proc/activate_scanner(obj/machinery/exoscanner/scanner)
if(scanner in tracked_dishes)
return
tracked_dishes += scanner
update_scan_power()
/datum/scanner_controller/proc/deactivate_scanner(obj/machinery/exoscanner/scanner)
if(!(scanner in tracked_dishes))
return
tracked_dishes -= scanner
update_scan_power()
/datum/scanner_controller/proc/update_scan_power()
scan_power_cache = list()
if(current_scan) //Check if we need to interrupt current scan.
var/current_power = length(tracked_dishes)
if(current_scan.target)
current_power = get_scan_power(current_scan.target)
if(current_scan.scan_power > current_power)
current_scan.stop("Scan swarm power reduced")
/datum/scanner_controller/proc/get_scan_power(datum/exploration_site/target)
if(!scan_power_cache[target])
scan_power_cache[target] = calculate_scan_power(target.scan_conditions)
return scan_power_cache[target]
/datum/scanner_controller/proc/calculate_scan_power(conditions)
. = 0
for(var/obj/machinery/exoscanner/dish in tracked_dishes)
var/effective_power = 1
for(var/datum/scan_condition/condition in conditions)
effective_power *= condition.check_dish(dish)
if(!effective_power) //Don't bother continuing if it's zero
break
. += effective_power
/// Scan condition, these require some specific setup for the dish to count for the scan power for the given site
/datum/scan_condition
var/name
var/description
/// Returns power multiplier of the dish depending on condition.
/datum/scan_condition/proc/check_dish(obj/machinery/exoscanner/dish)
return 1
/datum/scan_condition/nebula
name = "Nebula"
description = "Site is within a unusually dense nebula, to reduce scanner noise position dishes at least 15 tiles apart"
var/distance = 15
/datum/scan_condition/nebula/check_dish(obj/machinery/exoscanner/dish)
for(var/obj/machinery/exoscanner/other_dish in GLOB.exoscanner_controller.tracked_dishes)
if(dish != other_dish && dish.z == other_dish.z && get_dist(dish,other_dish) < distance)
return 0
return 1
/datum/scan_condition/pulsar
name = "Pulsar"
description = "Pulsar near the site requires dishes to be shielded from electomagnetic noise, ensure no other machines are working near the dish."
var/distance = 2
/datum/scan_condition/pulsar/check_dish(obj/machinery/exoscanner/dish)
for(var/obj/machinery/some_machine in range(distance,dish))
if(some_machine != dish && some_machine.is_operational)
return 0
return 1
/datum/scan_condition/asteroid_belt
name = "Asteroid Belt"
description = "An asteroid belt is obscuring the direct line of sight from the station to the site, ensure the dishes are placed outside of station z level."
/datum/scan_condition/asteroid_belt/check_dish(obj/machinery/exoscanner/dish)
var/turf/dish_turf = get_turf(dish)
return is_station_level(dish_turf.z) ? 0 : 1
/datum/scan_condition/black_hole
name = "Black Hole"
description = "Background black hole requires you to focus the scan point precisely, ensure the dishes isolated from rest of the station with at least 6 walls around them."
/datum/scan_condition/black_hole/check_dish(obj/machinery/exoscanner/dish)
var/wall_count = 0
for(var/turf/turf_in_dish_range in range(1,get_turf(dish)))
if(turf_in_dish_range.density)
wall_count += 1
return wall_count > 6 ? 1 : 0
/datum/scan_condition/easy
name = "Easy Scan"
description = "This site is very easy to scan, all dish power is doubled."
/datum/scan_condition/easy/check_dish(obj/machinery/exoscanner/dish)
return 2