[MIRROR] Adds explorer drones / adventures. (#4424)

* Adds explorer drones / adventures.

* Update persistence.dm

* Update asset_list_items.dm

* MAP RESET

Co-authored-by: AnturK <AnturK@users.noreply.github.com>
Co-authored-by: Gandalf <jzo123@hotmail.com>
This commit is contained in:
SkyratBot
2021-03-26 15:49:44 +00:00
committed by GitHub
co-authored by AnturK Gandalf
parent 3a8d59e815
commit 49836e4fac
58 changed files with 23118 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