[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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
#define ADVENTURE_DIR "[global.config.directory]/adventures/"
//Special preset nodes
/// Victory node - Get loot and exit
#define WIN_NODE "WIN"
/// Failure node - No loot, get damaged and exit.
#define FAIL_NODE "FAIL"
/// Failure node - No loot and drone blown up.
#define FAIL_DEATH_NODE "FAIL_DEATH"
/// Return node - navigates to previous adventure node.
#define GO_BACK_NODE "GO BACK"
//Adventure results
#define ADVENTURE_RESULT_SUCCESS "success"
#define ADVENTURE_RESULT_DAMAGE "damage"
#define ADVENTURE_RESULT_DEATH "death"
// Exploration drone states
/// Drone is stationside - allow changing tools and such.
#define EXODRONE_IDLE "idle"
/// Drone is traveling from or to the exploration site
#define EXODRONE_TRAVEL "travel"
/// Drone is in adventure/event caused timeout
#define EXODRONE_BUSY "busy"
/// Drone is at exploration site either idle or in simple event
#define EXODRONE_EXPLORATION "exploration"
/// Drone is currently playing an adventure
#define EXODRONE_ADVENTURE "adventure"
// Scanner bands, use these to guess what's in the site and prepare drone accordingly.
#define EXOSCANNER_BAND_PLASMA "Plasma absorption band"
#define EXOSCANNER_BAND_LIFE "Hydrocarbons/Molecular oxygen"
#define EXOSCANNER_BAND_TECH "Narrow-band radio waves"
#define EXOSCANNER_BAND_RADIATION "Exotic Radiation"
#define EXOSCANNER_BAND_DENSITY "Increased Density"
// Exodrone tools
#define EXODRONE_TOOL_WELDER "welder"
#define EXODRONE_TOOL_TRANSLATOR "translator"
#define EXODRONE_TOOL_LASER "laser"
#define EXODRONE_TOOL_MULTITOOL "multitool"
#define EXODRONE_TOOL_DRILL "drill"
GLOBAL_LIST_INIT(exodrone_tool_metadata,list(
EXODRONE_TOOL_WELDER = list("description"="A heavy duty welder.","icon"="burn"),
EXODRONE_TOOL_TRANSLATOR = list("description"="Powerful translation and data recording software.","icon"="language"),
EXODRONE_TOOL_LASER = list("description"="Multipurpose tool suitable for combat and precision cutting.","icon"="bolt"),
EXODRONE_TOOL_MULTITOOL = list("description"="Multipurpose tool for electronics manipulation. Comes with suite of radiation and radiowave sensors.","icon"="broadcast-tower"),
EXODRONE_TOOL_DRILL = list("description"="Heavy duty drill useful for mining.","icon"="screwdriver")
))
// Site traits
/// Some kind of ruined interior
#define EXPLORATION_SITE_RUINS "ruins"
/// Power, wires and machinery present.
#define EXPLORATION_SITE_TECHNOLOGY "technology present"
/// It's a space station
#define EXPLORATION_SITE_STATION "space station"
/// It's ancient alien site
#define EXPLORATION_SITE_ALIEN "alien"
/// Carbon-based life-forms can live here
#define EXPLORATION_SITE_HABITABLE "habitable"
/// Site is in space
#define EXPLORATION_SITE_SPACE "in space"
/// Site is located on planet/moon/whatever surface
#define EXPLORATION_SITE_SURFACE "on surface"
/// Site is a space ship
#define EXPLORATION_SITE_SHIP "spaceship"
/// Site is civilized and populated, trading stations,cities etc. Lack of this trait means it's wilderness
#define EXPLORATION_SITE_CIVILIZED "civilized"
/// Scan types
// Wide scan, untargeted scan only reveals interest points. Cost increases exponentially with each firing. No scan conditions.
#define EXOSCAN_WIDE "wide"
// Point scan, reveals name/description and general band information. Flat cost. Affected by scan conditions of the site
#define EXOSCAN_POINT "point"
// Deep scan, reveals event scan texts. Linear cost increase with distance. Affected by scan conditions of the site.
#define EXOSCAN_DEEP "deep"
/// Adventure Effect Types
//completely removes the quality
#define ADVENTURE_EFFECT_TYPE_REMOVE "Remove"
//adds/substracts value from quality
#define ADVENTURE_EFFECT_TYPE_ADD "Add"
//sets quality to specific value
#define ADVENTURE_EFFECT_TYPE_SET "Set"
/// Adventure Effect Value Types
/// rolls value between low and high inclusive
#define ADVENTURE_QUALITY_TYPE_RANDOM "random"
#define ADVENTURE_RANDOM_QUALITY_LOW_FIELD "low"
#define ADVENTURE_RANDOM_QUALITY_HIGH_FIELD "high"
+24
View File
@@ -1148,3 +1148,27 @@
///from /obj/item/assembly/proc/pulsed()
#define COMSIG_ASSEMBLY_PULSED "assembly_pulsed"
/// Exoprobe adventure finished: (result) result is ADVENTURE_RESULT_??? values
#define COMSIG_ADVENTURE_FINISHED "adventure_done"
/// Sent on initial adventure qualities generation from /datum/adventure/proc/initialize_qualities(): (list/quality_list)
#define COMSIG_ADVENTURE_QUALITY_INIT "adventure_quality_init"
/// Sent on adventure node delay start: (delay_time, delay_message)
#define COMSIG_ADVENTURE_DELAY_START "adventure_delay_start"
/// Sent on adventure delay finish: ()
#define COMSIG_ADVENTURE_DELAY_END "adventure_delay_end"
/// Exoprobe status changed : ()
#define COMSIG_EXODRONE_STATUS_CHANGED "exodrone_status_changed"
// Scanner controller signals
/// Sent on begingging of new scan : (datum/exoscan/new_scan)
#define COMSIG_EXOSCAN_STARTED "exoscan_started"
/// Sent on successful finish of exoscan: (datum/exoscan/finished_scan)
#define COMSIG_EXOSCAN_FINISHED "exoscan_finished"
// Exosca signals
/// Sent on exoscan failure/manual interruption: ()
#define COMSIG_EXOSCAN_INTERRUPTED "exoscan_interrupted"
+1
View File
@@ -442,6 +442,7 @@ GLOBAL_LIST_INIT(pda_styles, sortList(list(MONO, VT, ORBITRON, SHARE)))
#define FLESH_SCAR_FILE "wounds/flesh_scar_desc.json"
#define BONE_SCAR_FILE "wounds/bone_scar_desc.json"
#define SCAR_LOC_FILE "wounds/scar_loc.json"
#define EXODRONE_FILE "exodrone.json"
//Fullscreen overlay resolution in tiles.
#define FULLSCREEN_OVERLAY_RESOLUTION_X 15
+2
View File
@@ -344,6 +344,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_PLANT_WILDMUTATE "wildmutation"
/// If you hit an APC with exposed internals with this item it will try to shock you
#define TRAIT_APC_SHOCKING "apc_shocking"
///Properly wielded two handed item
#define TRAIT_WIELDED "wielded"
//quirk traits
#define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance"
@@ -34,6 +34,8 @@ SUBSYSTEM_DEF(persistence)
LoadRandomizedRecipes()
LoadPaintings()
LoadPanicBunker() //SKYRAT EDIT ADDITION - PANICBUNKER
GLOB.explorer_drone_adventures = load_adventures()
return ..()
/datum/controller/subsystem/persistence/proc/LoadPoly()
+3 -1
View File
@@ -17,8 +17,8 @@
var/icon_wielded = FALSE /// The icon that will be used when wielded
var/obj/item/offhand/offhand_item = null /// Reference to the offhand created for the item
var/sharpened_increase = 0 /// The amount of increase recived from sharpening the item
/**
* Two Handed component
*
* vars:
@@ -152,6 +152,7 @@
if(SEND_SIGNAL(parent, COMSIG_TWOHANDED_WIELD, user) & COMPONENT_TWOHANDED_BLOCK_WIELD)
return // blocked wield from item
wielded = TRUE
ADD_TRAIT(parent,TRAIT_WIELDED,src)
RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, .proc/on_swap_hands)
// update item stats and name
@@ -198,6 +199,7 @@
wielded = FALSE
UnregisterSignal(user, COMSIG_MOB_SWAP_HANDS)
SEND_SIGNAL(parent, COMSIG_TWOHANDED_UNWIELD, user)
REMOVE_TRAIT(parent,TRAIT_WIELDED,src)
// update item stats
var/obj/item/parent_item = parent
@@ -546,3 +546,11 @@
/obj/item/circuitboard/computer/mining_shuttle/common
name = "Lavaland Shuttle (Computer Board)"
build_path = /obj/machinery/computer/shuttle/mining/common
/obj/item/circuitboard/computer/exoscanner_console
name = "Scanner Array Control Console (Computer Board)"
build_path = /obj/machinery/computer/exoscanner_control
/obj/item/circuitboard/computer/exodrone_console
name = "Exploration odrone control console (Computer Board)"
build_path = /obj/machinery/computer/exodrone_control_console
@@ -1330,3 +1330,19 @@
req_components = list(
/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/scanning_module = 4)
/obj/item/circuitboard/machine/exoscanner
name = "Exoscanner (Machine Board)"
icon_state = "science"
build_path = /obj/machinery/exoscanner
req_components = list(
/obj/item/stock_parts/micro_laser = 4,
/obj/item/stock_parts/scanning_module = 4)
/obj/item/circuitboard/machine/exodrone_launcher
name = "Exploration Drone Launcher (Machine Board)"
icon_state = "science"
build_path = /obj/machinery/exodrone_launcher
req_components = list(
/obj/item/stock_parts/micro_laser = 4,
/obj/item/stock_parts/scanning_module = 4)
+5 -1
View File
@@ -11,7 +11,8 @@
/obj/item/holochip/Initialize(mapload, amount)
. = ..()
credits = amount
if(amount)
credits = amount
update_appearance()
/obj/item/holochip/examine(mob/user)
@@ -121,3 +122,6 @@
if(prob(wipe_chance))
visible_message("<span class='warning'>[src] fizzles and disappears!</span>")
qdel(src) //rip cash
/obj/item/holochip/thousand
credits = 1000
+10 -2
View File
@@ -6,6 +6,7 @@
name = "pet carrier"
desc = "A big white-and-blue pet carrier. Good for carrying <s>meat to the chef</s> cute animals around."
icon = 'icons/obj/pet_carrier.dmi'
base_icon_state = "pet_carrier"
icon_state = "pet_carrier_open"
inhand_icon_state = "pet_carrier"
lefthand_file = 'icons/mob/inhands/items_lefthand.dmi'
@@ -146,13 +147,13 @@
if(open)
icon_state = initial(icon_state)
return ..()
icon_state = "pet_carrier_[!occupants.len ? "closed" : "occupied"]"
icon_state = "[base_icon_state]_[!occupants.len ? "closed" : "occupied"]"
return ..()
/obj/item/pet_carrier/update_overlays()
. = ..()
if(!open)
. += "[locked ? "" : "un"]locked"
. += "[base_icon_state]_[locked ? "" : "un"]locked"
/obj/item/pet_carrier/MouseDrop(atom/over_atom)
. = ..()
@@ -196,4 +197,11 @@
occupant_weight -= occupant.mob_size
occupant.setDir(SOUTH)
/obj/item/pet_carrier/biopod
name = "biopod"
desc = "Alien device used for undescribable purpose. Or carrying pets."
base_icon_state = "biopod"
icon_state = "biopod_open"
inhand_icon_state = "biopod"
#undef pet_carrier_full
@@ -166,6 +166,13 @@
)
parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css')
/datum/asset/simple/namespaced/tgfont
assets = list(
"tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
"tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
)
parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
/datum/asset/spritesheet/chat
name = "chat"
@@ -520,6 +527,14 @@
Insert(id, fish_icon, fish_icon_state)
..()
/datum/asset/simple/adventure
assets = list(
"default" = 'icons/UI_Icons/adventure/default.png',
"grue" = 'icons/UI_Icons/adventure/grue.png',
"signal_lost" ='icons/UI_Icons/adventure/signal_lost.png',
"trade" = 'icons/UI_Icons/adventure/trade.png',
)
/datum/asset/simple/inventory
assets = list(
"inventory-glasses.png" = 'icons/UI_Icons/inventory/glasses.png',
@@ -148,3 +148,27 @@ GLOBAL_LIST_INIT(gas_recipe_meta, gas_recipes_list())
energy_release = 2900000
requirements = list(/datum/gas/antinoblium = 5, /datum/gas/zauker = 20, /datum/gas/bz = 7.5)
products = list(/obj/item/stack/sheet/mineral/zaukerite = 2)
/datum/gas_recipe/crystallizer/fuel_pellet
id = "fuel_basic"
name = "standard fuel pellet"
reaction_type = ENDOTHERMIC_REACTION
energy_release = 6000000
requirements = list(/datum/gas/oxygen = 50, /datum/gas/plasma = 100)
products = list(/obj/item/fuel_pellet = 1)
/datum/gas_recipe/crystallizer/fuel_pellet_advanced
id = "fuel_advanced"
name = "advanced fuel pellet"
reaction_type = ENDOTHERMIC_REACTION
energy_release = 6000000
requirements = list(/datum/gas/tritium = 100, /datum/gas/hydrogen = 100)
products = list(/obj/item/fuel_pellet/advanced = 1)
/datum/gas_recipe/crystallizer/fuel_pellet_exotic
id = "fuel_exotic"
name = "exotic fuel pellet"
reaction_type = ENDOTHERMIC_REACTION
energy_release = 6000000
requirements = list(/datum/gas/hypernoblium = 100, /datum/gas/stimulum = 100)
products = list(/obj/item/fuel_pellet/exotic = 1)
+4
View File
@@ -0,0 +1,4 @@
/datum/export/antique
cost = CARGO_CRATE_VALUE*10
unit_name = "antique"
export_types = list(/obj/item/antique)
+8
View File
@@ -43,6 +43,14 @@
QDEL_NULL(radio)
return ..()
/obj/machinery/computer/cargo/attacked_by(obj/item/I, mob/living/user)
if(istype(I,/obj/item/trade_chip))
var/obj/item/trade_chip/contract = I
contract.try_to_unlock_contract(user)
return TRUE
else
return ..()
/obj/machinery/computer/cargo/proc/get_export_categories()
. = EXPORT_CARGO
if(contraband)
+56
View File
@@ -2480,6 +2480,13 @@
/obj/item/book/random)
crate_type = /obj/structure/closet/crate/wooden
/datum/supply_pack/misc/exploration_drone
name = "Exploration Drone"
desc = "A replacement long-range exploration drone."
cost = CARGO_CRATE_VALUE * 5
contains = list(/obj/item/exodrone)
crate_name = "exodrone crate"
/datum/supply_pack/misc/paper
name = "Bureaucracy Crate"
desc = "High stacks of papers on your desk Are a big problem - make it Pea-sized with these bureaucratic supplies! Contains six pens, some camera film, hand labeler supplies, a paper bin, a carbon paper bin, three folders, a laser pointer, two clipboards and two stamps."//that was too forced
@@ -2825,4 +2832,53 @@
/obj/item/vending_refill/wardrobe/det_wardrobe,
/obj/item/vending_refill/wardrobe/law_wardrobe)
crate_name = "security department supply crate"
<<<<<<< HEAD
*/
=======
/// Exploration drone unlockables ///
/datum/supply_pack/exploration
special = TRUE
group = "Outsourced"
/datum/supply_pack/exploration/scrapyard
name = "Scrapyard Crate"
desc = "Outsourced crate containing various junk."
cost = CARGO_CRATE_VALUE * 5
contains = list(/obj/item/relic,
/obj/item/broken_bottle,
/obj/item/pickaxe/rusted)
crate_name = "scrapyard crate"
/datum/supply_pack/exploration/catering
name = "Catering Crate"
desc = "No cook? No problem! Food quality may vary depending on provider."
cost = CARGO_CRATE_VALUE * 5
contains = list(/obj/item/food/sandwich,
/obj/item/food/sandwich,
/obj/item/food/sandwich,
/obj/item/food/sandwich,
/obj/item/food/sandwich)
crate_name = "outsourced food crate"
/datum/supply_pack/exploration/catering/fill(obj/structure/closet/crate/C)
. = ..()
if(prob(30))
for(var/obj/item/food/F in C)
F.name = "spoiled [F.name]"
F.foodtypes |= GROSS
F.MakeEdible()
/datum/supply_pack/exploration/shrubbery
name = "Shrubbery Crate"
desc = "Crate full of hedge shrubs."
cost = CARGO_CRATE_VALUE * 5
crate_name = "shrubbery crate"
var/shrub_amount = 8
/datum/supply_pack/exploration/shrubbery/fill(obj/structure/closet/crate/C)
for(var/i in 1 to shrub_amount)
new /obj/item/grown/shrub(C)
>>>>>>> 8b1ffd1e49a (Adds explorer drones / adventures. (#57851))
+12
View File
@@ -454,6 +454,18 @@
inhand_icon_state = "redglasses"
glass_colour_type = /datum/client_colour/glass_colour/red
/obj/item/clothing/glasses/geist_gazers
name = "geist gazers"
icon_state = "geist_gazers"
worn_icon_state = "geist_gazers"
glass_colour_type = /datum/client_colour/glass_colour/green
/obj/item/clothing/glasses/psych
name = "psych glasses"
icon_state = "psych_glasses"
worn_icon_state = "psych_glasses"
glass_colour_type = /datum/client_colour/glass_colour/red
/obj/item/clothing/glasses/godeye
name = "eye of god"
desc = "A strange eye, said to have been torn from an omniscient creature that used to roam the wastes."
@@ -0,0 +1,78 @@
/datum/experiment/exploration_scan
name = "Exploration Experiment"
description = "An experiment requiring drone exploration to progress"
exp_tag = "Exploration"
performance_hint = "Find a site with specific characteristics and perform the scan."
// Site type, scan needs to be of that type
var/required_site_type
// Condition type, scan needs to be of site with this condition
var/required_condition
// Required scan type
var/required_scan_type = EXOSCAN_POINT
/datum/experiment/exploration_scan/is_complete()
for(var/datum/exploration_site/site in GLOB.exploration_sites)
switch(required_scan_type)
if(EXOSCAN_DEEP)
if(!site.deep_scan_complete)
continue
if(EXOSCAN_POINT)
if(!site.point_scan_complete)
continue
if(required_site_type && !istype(site,required_site_type))
continue
if(required_condition && !(locate(required_condition) in site.scan_conditions))
continue
return TRUE
return FALSE
/datum/experiment/exploration_scan/perform_experiment_actions(datum/component/experiment_handler/experiment_handler)
return is_complete()
/datum/experiment/exploration_scan/actionable(datum/component/experiment_handler/experiment_handler)
return !is_complete()
/datum/experiment/exploration_scan/asteroid_belt
name = "Scan Asteroid Belt"
description = "We're looking for a site to test our asteroid blasting caps. Perform point scan of one."
required_site_type = /datum/exploration_site/asteroid_belt
required_scan_type = EXOSCAN_POINT
/datum/experiment/exploration_scan/black_hole
name = "Deep scan a black hole"
description = "We require more research data on black holes, perform deep scan of a system affected by one."
required_condition = /datum/scan_condition/black_hole
required_scan_type = EXOSCAN_DEEP
/datum/experiment/exploration_scan/random
name = "Random Exoscan Experiment"
description = "We need scan data of specific site type"
/// If not null the required_site_type will be picked from this list
var/list/possible_random_conditions
/// If not null the required_condition will be picked from this list
var/list/possible_random_site_types
/datum/experiment/exploration_scan/random/New()
. = ..()
if(length(possible_random_site_types))
required_site_type = pick(possible_random_site_types)
if(length(possible_random_conditions))
required_condition = pick(possible_random_conditions)
var/list/name_parts = list()
name_parts += "[required_scan_type] scan of"
if(required_site_type)
var/datum/exploration_site/site = required_site_type
name_parts += initial(site.name)
else
name_parts += "site"
if(required_condition)
var/datum/scan_condition/condition = required_condition
name_parts += "affected by \the [initial(condition.name)]"
name = capitalize(name_parts.Join(""))
description = name
/datum/experiment/exploration_scan/random/condition
possible_random_conditions = list(/datum/scan_condition/asteroid_belt,/datum/scan_condition/black_hole,/datum/scan_condition/nebula,/datum/scan_condition/pulsar)
/datum/experiment/exploration_scan/random/site_type
possible_random_site_types = list(/datum/exploration_site/asteroid_belt,/datum/exploration_site/uncharted_planet,/datum/exploration_site/junkyard)
+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
@@ -305,3 +305,19 @@
id = "bounty_pad_control"
build_path = /obj/item/circuitboard/computer/bountypad
category = list("Computer Boards")
/datum/design/board/exoscanner_console
name = "Computer Design (Scanner Array Control Console)"
desc = "Allows for the construction of circuit boards used to build a new scanner array control console."
id = "exoscanner_console"
build_type = IMPRINTER
build_path = /obj/item/circuitboard/computer/exoscanner_console
category = list("Computer Boards")
/datum/design/board/exodrone_console
name = "Computer Design (Exploration Drone Control Console)"
desc = "Allows for the construction of circuit boards used to build a new exploration drone control console."
id = "exodrone_console"
build_type = IMPRINTER
build_path = /obj/item/circuitboard/computer/exodrone_console
category = list("Computer Boards")
@@ -756,3 +756,19 @@
build_path = /obj/item/circuitboard/machine/crystallizer
category = list ("Engineering Machinery")
departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
/datum/design/board/exoscanner
name = "Machine Design (Scanner Array)"
desc = "The circuit board for scanner array."
id = "exoscanner"
build_path = /obj/item/circuitboard/machine/exoscanner
category = list ("Engineering Machinery")
departmental_flags = DEPARTMENTAL_FLAG_CARGO
/datum/design/board/exodrone_launcher
name = "Machine Design (Exploration Drone Launcher)"
desc = "The circuit board for exodrone launcher."
id = "exodrone_launcher"
build_path = /obj/item/circuitboard/machine/exodrone_launcher
category = list ("Engineering Machinery")
departmental_flags = DEPARTMENTAL_FLAG_CARGO
@@ -213,6 +213,7 @@
prereq_ids = list("bluespace_travel", "practical_bluespace", "high_efficiency")
design_ids = list("bluespace_matter_bin", "femto_mani", "bluespacebodybag", "triphasic_scanning", "quantum_keycard", "wormholeprojector", "swapper", "bluespace_electrolite")
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000)
discount_experiments = list(/datum/experiment/exploration_scan/random/condition)
/datum/techweb_node/advanced_bluespace
id = "bluespace_storage"
@@ -289,6 +290,14 @@
design_ids = list("mmi_posi")
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
/datum/techweb_node/exodrone_tech
id = "exodrone"
display_name = "Exploration Drone Research"
description = "Technology for exploring far away locations."
prereq_ids = list("robotics")
design_ids = list("exodrone_console","exoscanner_console","exoscanner","exodrone_launcher")
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
/datum/techweb_node/neural_programming
id = "neural_programming"
display_name = "Neural Programming"
@@ -44,3 +44,19 @@
sample = deposited_sample
to_chat(user, "<span class='notice'>You deposit a sample into [src].</span>")
update_appearance()
/// Petri dish with random sample already in it.
/obj/item/petri_dish/random
var/static/list/possible_samples = list(
list(CELL_LINE_TABLE_CORGI, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5),
list(CELL_LINE_TABLE_SNAKE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5),
list(CELL_LINE_TABLE_COCKROACH, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 7),
list(CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
)
/obj/item/petri_dish/random/Initialize()
. = ..()
var/list/chosen = pick(possible_samples)
sample = new
sample.GenerateSample(chosen[1],chosen[2],chosen[3],chosen[4])
update_appearance()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+73
View File
@@ -0,0 +1,73 @@
{
"probe_names": [
"Voyager",
"Luna",
"Cassini",
"Galileo",
"Mariner",
"Sojourner",
"Pioneer",
"Rosetta",
"Juno",
"Stardust",
"Vega",
"Venera"
],
"trading_station_names": [
"Big Bill's used cars",
"Jims Space Emporium",
"The Galactic Market",
"Turners Ship & Trade",
"Rocketing Deals",
"Cornerstore at the End of the Universe",
"Crazy Redd's Crazy Deals"
],
"planet_types": [
{
"name": "barren planet",
"description": "A barren planet with very thin atmosphere. Nothing but rocks.",
"habitable": false,
"civilized": false,
"tech": false
},
{
"name": "jungle world",
"description": "A planet covered in thick jungle. You detect numerous fauna and flora readings.",
"habitable": true,
"civilized": false,
"tech": false
},
{
"name": "robot moon",
"description": "A tiny rock covered with factories and automatons who seem to ignore your presence for the most part.",
"habitable": true,
"civilized": true,
"tech": true
}
],
"alien_fauna": [
"xenomorph hunter",
"basilisk",
"very angry cow",
"unknown beast"
],
"fluff_generic": [
"You find nothing interesting.",
"This place is emptier than assisstant's credit account.",
"You find a huge pile of boredom."
],
"fluff_space": [
"You detect stardrive activation readings nearby. By time time you arrive at the spot there's nothing there but space dust.",
"You pass by a small field of space debris. Nothing but useless junk."
],
"fluff_trading": [
"You encounter a maintenance drone busy at work. It ignores you completely.",
"You pass by security patrol. They give you suspicious stares.",
"You pass by a drunked local. Good thing smell is not conducted through the drone feed."
],
"fluff_ruins": [
"You encounter a broken statue. You can't tell what was it's original shape.",
"You have to backtrack from a collapsed passage.",
"You pass through a corridor full of cracked tiles."
]
}
+15
View File
@@ -27,6 +27,7 @@
#include "code\__DEFINES\acid.dm"
#include "code\__DEFINES\actionspeed_modification.dm"
#include "code\__DEFINES\admin.dm"
#include "code\__DEFINES\adventure.dm"
#include "code\__DEFINES\ai.dm"
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\aquarium.dm"
@@ -1929,6 +1930,7 @@
#include "code\modules\cargo\bounties\slime.dm"
#include "code\modules\cargo\bounties\special.dm"
#include "code\modules\cargo\bounties\virus.dm"
#include "code\modules\cargo\exports\antiques.dm"
#include "code\modules\cargo\exports\civilain_bounty.dm"
#include "code\modules\cargo\exports\gear.dm"
#include "code\modules\cargo\exports\large_objects.dm"
@@ -2140,6 +2142,7 @@
#include "code\modules\experisci\experiment\physical_experiments.dm"
#include "code\modules\experisci\experiment\handlers\experiment_handler.dm"
#include "code\modules\experisci\experiment\types\experiment.dm"
#include "code\modules\experisci\experiment\types\exploration.dm"
#include "code\modules\experisci\experiment\types\explosive.dm"
#include "code\modules\experisci\experiment\types\physical_experiment.dm"
#include "code\modules\experisci\experiment\types\random_scanning.dm"
@@ -2148,6 +2151,18 @@
#include "code\modules\experisci\experiment\types\scanning_plants.dm"
#include "code\modules\experisci\experiment\types\scanning_points.dm"
#include "code\modules\experisci\experiment\types\scanning_vatgrown.dm"
#include "code\modules\explorer_drone\adventure.dm"
#include "code\modules\explorer_drone\control_console.dm"
#include "code\modules\explorer_drone\exodrone.dm"
#include "code\modules\explorer_drone\exploration_site.dm"
#include "code\modules\explorer_drone\loot.dm"
#include "code\modules\explorer_drone\scanner_array.dm"
#include "code\modules\explorer_drone\exploration_events\_exploration_event.dm"
#include "code\modules\explorer_drone\exploration_events\adventure.dm"
#include "code\modules\explorer_drone\exploration_events\danger.dm"
#include "code\modules\explorer_drone\exploration_events\fluff.dm"
#include "code\modules\explorer_drone\exploration_events\resource.dm"
#include "code\modules\explorer_drone\exploration_events\trader.dm"
#include "code\modules\fields\fields.dm"
#include "code\modules\fields\gravity.dm"
#include "code\modules\fields\peaceborg_dampener.dm"
+3 -8
View File
@@ -1,4 +1,5 @@
import { toFixed } from 'common/math';
import { formatTime } from '../format';
import { Component } from 'inferno';
// AnimatedNumber Copypaste
@@ -57,13 +58,7 @@ export class TimeDisplay extends Component {
if (!isSafeNumber(val)) {
return this.state.value || null;
}
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60)).padStart(2, "0");
const minutes = toFixed(Math.floor((val/(10*60)) % 60)).padStart(2, "0");
const hours = toFixed(Math.floor((val/(10*60*60)) % 24)).padStart(2, "0");
const formattedValue = `${hours}:${minutes}:${seconds}`;
return formattedValue;
return formatTime(val);
}
}
+27
View File
@@ -169,3 +169,30 @@ export const formatSiBaseTenUnit = (
);
return finalString.trim();
};
/**
* Formats decisecond count into HH::MM::SS display by default
* "short" format does not pad and adds hms suffixes
*/
export const formatTime = (val, formatType) => {
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60));
const minutes = toFixed(Math.floor((val/(10*60)) % 60));
const hours = toFixed(Math.floor((val/(10*60*60)) % 24));
switch (formatType) {
case "short": {
const hours_truncated = hours > 0 ? `${hours}h` : "";
const minutes_truncated = minutes > 0 ? `${minutes}m` : "";
const seconds_truncated = seconds > 0 ? `${seconds}s` : "";
return `${hours_truncated}${minutes_truncated}${seconds_truncated}`;
}
default: {
const seconds_padded = seconds.padStart(2, "0");
const minutes_padded = minutes.padStart(2, "0");
const hours_padded = hours.padStart(2, "0");
return `${hours_padded}:${minutes_padded}:${seconds_padded}`;
}
}
};
@@ -0,0 +1,857 @@
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Box, Button, Dimmer, Icon, LabeledList, Modal, ProgressBar, Section, Stack } from '../components';
import { Window } from '../layouts';
import { resolveAsset } from '../assets';
import { formatTime } from '../format';
import { capitalize } from 'common/string';
import nt_logo from '../assets/bg-nanotrasen.svg';
import { Fragment } from 'inferno';
type ExplorationEventData = {
name: string,
ref: string
}
type FullEventData = {
image: string,
description: string,
action_enabled: boolean,
action_text: string,
skippable: boolean,
ignore_text: string,
ref: string
}
type ChoiceData = {
key: string,
text: string
}
type AdventureData = {
description: string,
image: string,
raw_image: string,
choices: Array<ChoiceData>
}
type SiteData = {
name: string,
ref: string,
description: string,
coordinates: string,
distance: number,
band_info: Record<string, number>,
revealed: boolean,
point_scan_complete: boolean,
deep_scan_complete: boolean,
events: Array<ExplorationEventData>
}
enum DroneStatusEnum {
Idle = "idle",
Travel = "travel",
Exploration = "exploration",
Adventure = "adventure",
Busy = "busy"
}
enum CargoType {
Tool = "tool",
Cargo = "cargo",
Empty = "empty"
}
type CargoData = {
type: CargoType,
name: string
}
type DroneBasicData = {
name: string,
description: string,
controlled: boolean,
ref: string,
}
type ExodroneConsoleData = {
signal_lost: boolean,
drone: boolean,
all_drones?: Array<DroneBasicData>
drone_status?: DroneStatusEnum,
drone_name?: string,
drone_integrity?: number,
drone_max_integrity?: number,
drone_travel_coefficent?: number,
drone_log?: Array<string>,
configurable?: boolean,
cargo?: Array<CargoData>,
can_travel?: boolean,
travel_error: string,
sites?: Array<SiteData>,
site?: SiteData,
travel_time?: number,
travel_time_left?: number,
wait_time_left?: number,
wait_message?: string,
event?: FullEventData,
adventure_data?: AdventureData,
// ui_static_data
all_tools: Record<string, ToolData>,
all_bands: Record<string, string>
}
type ToolData = {
description: string,
icon: string
}
export const ExodroneConsole = (props, context) => {
const { data } = useBackend<ExodroneConsoleData>(context);
const {
signal_lost,
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
return (
<Window width={650} height={500}>
{!!signal_lost && <SignalLostModal />}
{!!choosingTools && <ToolSelectionModal />}
<Window.Content>
<ExodroneConsoleContent />
</Window.Content>
</Window>
);
};
const SignalLostModal = (props, context) => {
const { act } = useBackend(context);
return (
<Modal
backgroundColor="red"
textAlign="center"
width={30}
height={22}
p={0}
style={{ "border-radius": "5%" }}>
<img src={nt_logo} width={64} height={64} />
<Box
backgroundColor="black"
textColor="red"
fontSize={2}
style={{ "border-radius": "-10%" }}>
CONNECTION LOST
</Box>
<Box p={2} italic>
Connection to exploration drone interrupted.
Please contact nearest Nanotrasen Exploration Division
representative for further instructions.
</Box>
<Icon
name="exclamation-triangle"
textColor="black"
size={5} />
<Box>
<Button
content="Confirm"
color="danger"
style={{ "border": "1px solid black" }}
onClick={() => act("confirm_signal_lost")} />
</Box>
</Modal>
);
};
const DroneSelectionSection = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
all_drones,
} = data;
return (
<Section scrollable fill title="Exploration Drone Listing">
<Stack vertical>
{all_drones.map(drone => (
<Fragment key={drone.ref}>
<Stack.Item grow>
<Stack fill>
<Stack.Item basis={10} fontFamily="monospace" fontSize="18px">
{drone.name}
</Stack.Item>
<Stack.Divider />
<Stack.Item fontFamily="monospace" mt={0.8}>
{drone.description}
</Stack.Item>
<Stack.Item grow />
<Stack.Divider mr={1} />
<Stack.Item ml={0}>
{drone.controlled && (
"Controlled by another console."
) || (
<Button
content="Assume Control"
icon="plug"
onClick={() => act("select_drone", { "drone_ref": drone.ref })} />
)}
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Divider />
</Fragment>
))}
</Stack>
</Section>
);
};
const ToolSelectionModal = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
all_tools = {},
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
const toolData = Object.keys(all_tools);
return (
<Modal>
<Stack fill vertical pr={2}>
<Stack.Item>
Select Tool:
</Stack.Item>
<Stack.Item>
<Stack textAlign="center">
{!!toolData && toolData.map(tool_name => (
<Stack.Item key={tool_name}>
<Button
onClick={() => {
setChoosingTools(false);
act("add_tool", { tool_type: tool_name });
}}
width={6}
height={6}
tooltip={all_tools[tool_name].description}>
<Stack vertical>
<Stack.Item>
{capitalize(tool_name)}
</Stack.Item>
<Stack.Item ml={2.5}>
<Icon name={all_tools[tool_name].icon} size={3} />
</Stack.Item>
</Stack>
</Button>
</Stack.Item>
)) || (
<Stack.Item>
<Button
content="Back" />
</Stack.Item>
)}
</Stack>
</Stack.Item>
</Stack>
</Modal>
);
};
const EquipmentBox = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
configurable,
all_tools = {},
} = data;
const cargo = props.cargo;
const boxContents = cargo => {
switch (cargo.type) {
case "tool": // Tool icon+Remove button if configurable
return (
<Stack direction="column">
<Stack.Item grow>
<Button
height={4.7}
width={4.7}
tooltip={capitalize(cargo.name)}
tooltipPosition="right"
color="transparent">
<Icon
color="white"
name={all_tools[cargo.name].icon}
size={3}
pl={1.5}
pt={2} />
</Button>
</Stack.Item>
{!!configurable && (
<Stack.Item mt={-9.4} textAlign="right">
<Button
onClick={() => act("remove_tool", { tool_type: cargo.name })}
color="danger"
icon="minus"
tooltipPosition="right"
tooltip="Remove Tool" />
</Stack.Item>
)}
</Stack>
);
case "cargo":// Jettison button.
return (
<Stack direction="column">
<Stack.Item>
<Button
mt={0}
height={4.7}
width={4.7}
tooltip={capitalize(cargo.name)}
tooltipPosition="right"
color="transparent">
<Icon
color="white"
name="box"
size={3}
pl={2.2}
pt={2} />
</Button>
</Stack.Item>
<Stack.Item mt={-9.4} textAlign="right">
<Button
onClick={() => act("jettison", { target_ref: cargo.ref })}
color="danger"
icon="minus"
tooltipPosition="right"
tooltip={`Jettison ${cargo.name}`} />
</Stack.Item>
</Stack>
);
case "empty":
return "";
}
};
return (
<Box
width={5}
height={5}
style={{ border: '2px solid black' }}
textAlign="center">
{boxContents(cargo)}
</Box>
);
};
const EquipmentGrid = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
cargo,
configurable,
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
return (
<Stack vertical fill>
<Stack.Item grow>
<Section fill title="Controls">
<Stack vertical textAlign="center">
<Stack.Item>
<Button
fluid
icon="plug"
content="Disconnect"
onClick={() => act('end_control')} />
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<Button.Confirm
fluid
icon="bomb"
content="Self-Destruct"
color="bad"
onClick={() => act('self_destruct')} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Cargo">
<Stack.Item>
{!!configurable && (
<Button
fluid
color="average"
icon="wrench"
content="Install Tool"
onClick={() => setChoosingTools(true)} />
)}
</Stack.Item>
<Stack.Item>
<Stack wrap="wrap" width={10}>
{cargo.map(cargo_element => (
<EquipmentBox
key={cargo_element.name}
cargo={cargo_element} />
))}
</Stack>
</Stack.Item>
</Section>
</Stack.Item>
</Stack>
);
};
const DroneStatus = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_integrity,
drone_max_integrity,
} = data;
return (
<Stack ml={-40}>
<Stack.Item color="label" mt={0.2}>
Integrity:
</Stack.Item>
<Stack.Item grow>
<ProgressBar
width="200px"
ranges={{
good: [0.7 * drone_max_integrity, drone_max_integrity],
average: [0.4 * drone_max_integrity, 0.7 * drone_max_integrity],
bad: [-Infinity, 0.4 * drone_max_integrity],
}}
value={drone_integrity}
maxValue={drone_max_integrity} />
</Stack.Item>
</Stack>
);
};
const NoSiteDimmer = () => {
return (
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="red"
name="map"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="red">
No Destinations.
</Stack.Item>
<Stack.Item basis={0} color="red">
(Use the Scanner Array Console to find new locations.)
</Stack.Item>
</Stack>
</Dimmer>
);
};
const TravelTargetSelectionScreen = (props, context) => {
// List of sites and eta travel times to each
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
sites,
site,
can_travel,
travel_error,
drone_travel_coefficent,
all_bands,
drone_status,
} = data;
const travel_cost = target_site => {
if (site) {
return Math.max(Math.abs(site.distance - target_site.distance), 1)
* drone_travel_coefficent;
}
else {
return target_site.distance * drone_travel_coefficent;
}
};
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
const [
TravelDimmerShown,
setTravelDimmerShown,
] = useLocalState(context, 'TravelDimmerShown', false);
const travel_to = ref => {
setTravelDimmerShown(false);
act("start_travel", { "target_site": ref });
};
const non_empty_bands = (dest : SiteData) => {
const band_check = (s: string) => dest.band_info[s] !== undefined
&& dest.band_info[s] !== 0;
return Object.keys(all_bands).filter(band_check);
};
const valid_destinations = !!sites && sites.filter(destination => (
!site || destination.ref !== site.ref
));
return (
drone_status === "travel" && (
<TravelDimmer />
) || (
<Section
title="Travel Destinations"
fill
scrollable
buttons={
<>
{props.showCancelButton && (
<Button
ml={5}
mr={0}
content="Cancel"
onClick={() => setTravelDimmerShown(false)} />
)}
<Box mt={props.showCancelButton && -3.5}>
<DroneStatus />
</Box>
</>
}>
{((sites && !sites.length) && !choosingTools) && (
<NoSiteDimmer />
)}
{site && (
<Section
mt={1}
title="Home"
buttons={
<Box>
ETA: {formatTime(site.distance * drone_travel_coefficent, "short")}
<Button
ml={1}
content={can_travel ? "Launch!" : travel_error}
onClick={() => travel_to(null)}
disabled={!can_travel} />
</Box>
}
/>
)}
{valid_destinations.map(destination => (
<Section
key={destination.ref}
title={destination.name}
buttons={
<>
ETA: {formatTime(travel_cost(destination), "short")}
<Button
ml={1}
content={can_travel ? "Launch!" : travel_error}
onClick={() => travel_to(destination.ref)}
disabled={!can_travel} />
</>
}>
<LabeledList>
<LabeledList.Item label="Location">
{destination.coordinates}
</LabeledList.Item>
<LabeledList.Item label="Description">
{destination.description}
</LabeledList.Item>
<LabeledList.Divider />
{non_empty_bands(destination).map(band => (
<LabeledList.Item
key={band}
label={band}>
{destination.band_info[band]}
</LabeledList.Item>
))}
</LabeledList>
</Section>
))}
</Section>
)
);
};
const TravelDimmer = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
travel_time,
travel_time_left,
} = data;
return (
<Section fill>
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="yellow"
name="route"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="yellow">
Travel Time: {formatTime(travel_time_left)}
</Stack.Item>
</Stack>
</Dimmer>
</Section>
);
};
const TimeoutScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
wait_time_left,
wait_message,
} = data;
return (
<Section fill>
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="green"
name="cog"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="green">
{wait_message} ({formatTime(wait_time_left)})
</Stack.Item>
</Stack>
</Dimmer>
</Section>
);
};
const ExplorationScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
site,
event,
sites,
} = data;
const [
TravelDimmerShown,
setTravelDimmerShown,
] = useLocalState(context, 'TravelDimmerShown', false);
if (TravelDimmerShown) {
return (<TravelTargetSelectionScreen showCancelButton />);
}
return (
<Section
fill
title="Exploration"
buttons={
<DroneStatus />
}>
<Stack vertical fill>
<Stack.Item grow>
<LabeledList>
<LabeledList.Item label="Site">{site.name}</LabeledList.Item>
<LabeledList.Item label="Location">{site.coordinates}</LabeledList.Item>
<LabeledList.Item label="Description">{site.description}</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item align="center" grow>
<Button
content="Explore!"
onClick={() => act("explore")} />
</Stack.Item>
{site.events.map(e => (
<Stack.Item
align="center"
key={site.ref}
grow>
<Button
content={capitalize(e.name)}
onClick={() => act("explore_event", { target_event: e.ref })} />
</Stack.Item>))}
<Stack.Item align="center" grow>
<Button
content="Travel"
onClick={() => setTravelDimmerShown(true)} />
</Stack.Item>
</Stack>
</Section>
);
};
const EventScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_status,
event,
} = data;
return (
<Section
fill
title="Exploration"
buttons={
<DroneStatus />
}>
{(drone_status && drone_status === "busy") && (
<TimeoutScreen />
)}
<Stack vertical fill textAlign="center">
<Stack.Item>
<Stack fill>
<Stack.Item>
<img src={resolveAsset(event.image)}
height="125px"
width="250px"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Stack.Item>
<Stack.Item >
<BlockQuote
style={{ "white-space": "pre-wrap" }}>
{event.description}
</BlockQuote>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Stack vertical fill >
<Stack.Item grow />
<Stack.Item grow>
<Button
content={event.action_text}
disabled={!event.action_enabled}
onClick={() => act("start_event")} />
</Stack.Item>
{!!event.skippable && (
<Stack.Item mt={2}>
<Button
content={event.ignore_text}
onClick={() => act("skip_event")} />
</Stack.Item>
)}
<Stack.Item grow />
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const AdventureScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
adventure_data,
} = data;
const rawData = adventure_data.raw_image;
const imgSource = rawData ? rawData : resolveAsset(adventure_data.image);
return (
<Section
fill
title="Exploration"
buttons={<DroneStatus />}>
<Stack>
<Stack.Item>
<BlockQuote style={{ "white-space": "pre-wrap" }}>{adventure_data.description}</BlockQuote>
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<img
src={imgSource}
height="100px"
width="200px"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Stack vertical>
<Stack.Divider />
<Stack.Item grow />
{!!adventure_data.choices && adventure_data.choices.map(choice => (
<Stack.Item key={choice.key}>
<Button
fluid
content={choice.text}
textAlign="center"
onClick={() => act('adventure_choice', { choice: choice.key })} />
</Stack.Item>
))}
<Stack.Item grow />
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const DroneScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_status,
event,
} = data;
switch (drone_status) {
case "busy":
return <TimeoutScreen />;
case "idle":
case "travel":
return <TravelTargetSelectionScreen />;
case "adventure":
return <AdventureScreen />;
case "exploration":
if (event) {
return <EventScreen />;
}
else {
return <ExplorationScreen />;
}
}
};
const ExodroneConsoleContent = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone,
drone_name,
drone_log,
} = data;
if (!drone) {
return <DroneSelectionSection />;
}
return (
<Stack fill vertical>
<Stack.Item grow>
<Stack vertical fill grow={2}>
<Stack.Item grow>
<Stack fill>
<Stack.Item>
<EquipmentGrid />
</Stack.Item>
<Stack.Item grow basis={0}>
<DroneScreen />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item height={10}>
<Section title="Drone Log" fill scrollable>
<LabeledList>
{drone_log.map((log_line, ix) => (
<LabeledList.Item key={log_line} label={`Entry ${ix + 1}`}>
{log_line}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
);
};
@@ -0,0 +1,298 @@
import { useBackend } from '../backend';
import { BlockQuote, Box, Button, Flex, Icon, Modal, Section, LabeledList, NoticeBox, Stack } from '../components';
import { Window } from '../layouts';
import { formatTime } from '../format';
type SiteData = {
name: string,
ref: string,
description: string,
distance: number,
band_info: Record<string, string>,
revealed: boolean,
}
type ScanData = {
scan_power: number,
point_scan_eta: number,
deep_scan_eta: number,
point_scan_complete: boolean,
deep_scan_complete: boolean
site_data: SiteData
}
const ScanFailedModal = (props, context) => {
const { act, data } = useBackend(context);
return (
<Modal>
<Flex direction="column">
<Flex.Item>
<Box color="bad">SCAN FAILURE!</Box>
</Flex.Item>
<Flex.Item>
<Button
content="Confirm"
onClick={() => act("confirm_fail")} />
</Flex.Item>
</Flex>
</Modal>);
};
const ScanSelectionSection = (props, context) => {
const { act, data } = useBackend<ScanData>(context);
const {
scan_power,
point_scan_eta,
deep_scan_eta,
point_scan_complete,
deep_scan_complete,
site_data,
} = data;
const site = site_data;
const point_cost = scan_power > 0 ? formatTime(point_scan_eta, "short") : "∞";
const deep_cost = scan_power > 0 ? formatTime(deep_scan_eta, "short") : "∞";
const scan_availible = !point_scan_complete || !deep_scan_complete;
return (
<Stack vertical fill>
<Stack.Item grow>
<Section
fill
title="Site Data"
buttons={
<Button
content="Back"
onClick={() => act("select_site", { "site_ref": null })} />
}>
<LabeledList>
<LabeledList.Item label="Name">{site.name}</LabeledList.Item>
<LabeledList.Item label="Description">
{site.revealed ? site.description : "No Data"}
</LabeledList.Item>
<LabeledList.Item label="Distance">{site.distance}</LabeledList.Item>
<LabeledList.Divider />
<LabeledList.Item label="Spectrography Data" />
<LabeledList.Divider />
{Object.keys(site.band_info).map(band => (
<LabeledList.Item
key={band}
label={band}>
{site.band_info[band]}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
{scan_availible && (
<Stack.Item>
<Section fill title="Scans">
{!point_scan_complete && (
<Section title="Point Scan">
<BlockQuote>
Point scan performs rudimentary scan of
the site, revealing its general characteristics.
</BlockQuote>
<Box>
<Button
content="Scan"
disabled={scan_power <= 0}
onClick={() => act("start_point_scan")} />
<Box
inline
pl={3}>
Estimated Time: {point_cost}.
</Box>
</Box>
</Section>
)}
{!deep_scan_complete && (
<Section title="Deep Scan">
<BlockQuote>
Deep scan performs full scan
of the site, revealing all details.
</BlockQuote>
<Box>
<Button
content="Scan"
disabled={scan_power <= 0}
onClick={() => act("start_deep_scan")} />
<Box
inline
pl={3}>
Estimated Time: {deep_cost}.
</Box>
</Box>
</Section>
)}
</Section>
</Stack.Item>
)}
</Stack>
);
};
type ScanInProgressData = {
scan_time: number,
scan_power: number,
scan_description: string,
}
const ScanInProgressModal = (props, context) => {
const { act, data } = useBackend<ScanInProgressData>(context);
const {
scan_time,
scan_power,
scan_description,
} = data;
return (
<Modal ml={1}>
<NoticeBox>Scan in Progress!</NoticeBox>
<Box color="danger" />
<LabeledList>
<LabeledList.Item
label="Scan summary">
{scan_description}
</LabeledList.Item>
<LabeledList.Item
label="Time left">
{formatTime(scan_time)}
</LabeledList.Item>
<LabeledList.Item
label="Scanning array power">
{scan_power}
</LabeledList.Item>
<LabeledList.Item
label="Emergency Stop">
<Button.Confirm
content="STOP SCAN"
color="red"
icon="times"
onClick={() => act("stop_scan")} />
</LabeledList.Item>
</LabeledList>
</Modal>
);
};
type ExoscannerConsoleData = {
scan_in_progress: boolean,
scan_power: number,
possible_sites: Array<SiteData>,
wide_scan_eta: number,
selected_site: string,
failed: boolean,
scan_conditions: Array<string>,
}
export const ExoscannerConsole = (props, context) => {
const { act, data } = useBackend<ExoscannerConsoleData>(context);
const {
scan_in_progress,
scan_power,
possible_sites = [],
wide_scan_eta,
selected_site,
failed,
scan_conditions = [],
} = data;
const can_start_wide_scan = scan_power > 0;
return (
<Window>
{!!scan_in_progress && (
<ScanInProgressModal />
)}
{!!failed && (
<ScanFailedModal />
)}
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<Section fill title="Available array power">
<Stack>
<Stack.Item grow>
{scan_power > 0 && (
<>
<Box pr={1} inline fontSize={2}>{scan_power}</Box>
<Icon
name="satellite-dish"
size={3} />
</>
) || (
"No properly configured scanner arrays detected."
)}
</Stack.Item>
</Stack>
<Section title="Special Scan Condtions">
{scan_conditions && scan_conditions.map(condition => (
<NoticeBox
key={condition}
warning>
{condition}
</NoticeBox>
))}
</Section>
</Section>
</Stack.Item>
{!!selected_site && (
<Stack.Item grow>
<ScanSelectionSection site_ref={selected_site} />
</Stack.Item>
)}
{!selected_site && (
<>
<Stack.Item>
<Section fill title="Configure Wide Scan">
<Stack>
<Stack.Item>
<BlockQuote>
Broad spectrum scan looking for
anything not matching known start charts.
</BlockQuote>
</Stack.Item>
<Stack.Item>
Cost estimate: {scan_power > 0 ? formatTime(wide_scan_eta, "short") : "∞ minutes"}
</Stack.Item>
<Stack.Item>
<Button
mt={2}
content="Scan"
disabled={!can_start_wide_scan}
onClick={() => act("start_wide_scan")} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section
fill
title="Configure Targeted Scans"
scrollable
buttons={
<Button
content="View Experiments"
onClick={() => act("open_experiments")}
icon="tasks" />
}>
<Stack vertical>
{possible_sites.map(site => (
<Stack.Item key={site.ref}>
<Button
content={site.name}
onClick={() => act("select_site", { "site_ref": site.ref })} />
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</>
)}
</Stack>
</Window.Content>
</Window>
);
};