code:
// 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 ADVENTURE_VERSION_FIELD "version"

#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"

#define CURRENT_ADVENTURE_VERSION 1

/// All possible adventures in raw form
GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)

/// Loads all adventures from DB
/proc/load_adventures()
	. = list()
	if(!SSdbcore.Connect())
		GLOB.explorer_drone_adventure_db_entries = .
		return
	var/datum/db_query/Query = SSdbcore.NewQuery("SELECT id,adventure_data,uploader,timestamp,approved FROM [format_table_name("text_adventures")]")
	if(!Query.Execute())
		qdel(Query)
		return
	while(Query.NextRow())
		var/datum/adventure_db_entry/entry = new()
		entry.id = Query.item[1]
		entry.raw_json = Query.item[2]
		entry.uploader = Query.item[3]
		entry.timestamp = Query.item[4]
		entry.approved = Query.item[5]
		entry.extract_metadata()
		. += entry
	qdel(Query)
	GLOB.explorer_drone_adventure_db_entries = .

/datum/adventure_db_entry
	/// db id or null for freshly created adventures
	var/id
	/// actual adventure json string
	var/raw_json
	/// ckey of last change user.
	var/uploader
	/// Time of last change.
	var/timestamp
	/// Unapproved adventures won't be used for exploration sites.
	var/approved = FALSE
	/// Was the adventure used for exploration site this round.
	var/placed = FALSE

	//Variables below are extracted from the JSON

	/// json version
	var/version
	/// adventure name
	var/name
	/// required site traits to use this adventure
	var/list/required_site_traits

/// Check if the adventure usable for given exploration site traits
/datum/adventure_db_entry/proc/valid_for_use(list/site_traits)
	if(!raw_json || version != CURRENT_ADVENTURE_VERSION || placed)
		return FALSE
	if(required_site_traits && length(required_site_traits - site_traits) != 0)
		return FALSE
	return TRUE

/// Updates this entry from db, if possible.
/datum/adventure_db_entry/proc/refresh()
	if(id)
		//Check if our timestamp is fresh, if not update local and stop
		var/datum/db_query/SelectQuery = SSdbcore.NewQuery("SELECT adventure_data,uploader,timestamp,approved FROM [format_table_name("text_adventures")] WHERE id = :id",list("id" = id))
		if(!SelectQuery.warn_execute() || !SelectQuery.NextRow())
			qdel(SelectQuery)
			return
		raw_json = SelectQuery.item[1]
		uploader = SelectQuery.item[2]
		timestamp = SelectQuery.item[3]
		approved = SelectQuery.item[4]
		extract_metadata()
		qdel(SelectQuery)
		return
	// No ID, nothing to be done.

/// Pushes this entry changes to DB
/datum/adventure_db_entry/proc/save()
	if(id)
		//We're up to date, update db instead
		var/datum/db_query/UpdateQuery = SSdbcore.NewQuery("UPDATE [format_table_name("text_adventures")] SET adventure_data = :adventure_data,uploader = :uploader,approved = :approved WHERE id = :id AND timestamp < NOW()",
		list("id" = id, "adventure_data" = raw_json, "uploader" = usr.ckey, "approved" = approved))
		UpdateQuery.warn_execute()
		qdel(UpdateQuery)
	else
		// Create new entry
		var/datum/db_query/InsertQuery = SSdbcore.NewQuery("INSERT INTO [format_table_name("text_adventures")] (adventure_data, uploader) VALUES (:raw_json, :uploader)", list("raw_json" = raw_json, "uploader" = usr.ckey))
		if(!InsertQuery.warn_execute())
			qdel(InsertQuery)
			return FALSE
		id = InsertQuery.last_insert_id
		qdel(InsertQuery)
	refresh()

/// Deletes the local AND db entry.
/datum/adventure_db_entry/proc/remove()
	if(id)
		var/datum/db_query/DelQuery = SSdbcore.NewQuery("DELETE FROM [format_table_name("text_adventures")] WHERE id = :id", list("id" = id))
		if(!DelQuery.warn_execute())
			qdel(DelQuery)
			return FALSE
		log_admin("[key_name(usr)] deleted text adventure with id : [id], name : [name]")
		qdel(DelQuery)
	GLOB.explorer_drone_adventure_db_entries -= src
	qdel(src)
	return TRUE

/// Extracts fields that are used by adventure browser / generation before instantiating
/datum/adventure_db_entry/proc/extract_metadata()
	if(!raw_json)
		CRASH("Trying to extract metadata from empty adventure")
	var/list/json_data = json_decode(raw_json)
	if(!islist(json_data))
		CRASH("Invalid JSON for adventure with db id:[id]")
	version = json_data[ADVENTURE_VERSION_FIELD] || 0
	name = json_data[ADVENTURE_NAME_FIELD]
	required_site_traits = json_data[ADVENTURE_REQUIRED_SITE_TRAITS_FIELD]

/// Creates new adventure instance
/datum/adventure_db_entry/proc/create_adventure()
	if(version != CURRENT_ADVENTURE_VERSION)
		CRASH("Trying to instance outdated adventure version")
	return try_loading_adventure()

/// Parses adventure JSON and returns /datum/adventure instance on success
/datum/adventure_db_entry/proc/try_loading_adventure()
	var/list/json_data = json_decode(raw_json)
	if(!islist(json_data))
		CRASH("Invalid JSON in adventure with id:[id], name:[name]")

	//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 id:[id], name:[name] 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)
		if(node)
			if(loaded_adventure.nodes[node.id])
				CRASH("Duplicate [node.id] node in id:[id], name:[name] adventure")
			loaded_adventure.nodes[node.id] = node
	loaded_adventure.triggers = json_data[ADVENTURE_TRIGGERS_FIELD]
	if(!loaded_adventure.validate())
		CRASH("Validation failed for id:[id], name:[name] adventure")
	return loaded_adventure

/datum/adventure_db_entry/proc/try_loading_node(node_data)
	if(!islist(node_data))
		CRASH("Invalid adventure node data in id:[id], name:[name] 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
	/// Delayed state properties. If not null, means adventure is in delayed action state and will contain list(delay_time,delay_message)
	var/list/delayed_action

/// 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/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)
		delayed_action = list(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)
	delayed_action = null
	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


code:
/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.get_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

repo_name: AnalWerewolf/sundown

license: mit


code:
/// 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(mapload)
	. = ..()
	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 = podspawn(list(
		"target" = random_spot,
	))
	forceMove(pod)
	return random_spot

/// Tries to find landing pad, starting with the one we launched from.
/obj/item/exodrone/proc/find_landing_pad()
	var/obj/machinery/exodrone_launcher/landing_pad = last_pad?.resolve()
	if(landing_pad)
		return landing_pad
	for(var/obj/machinery/exodrone_launcher/other_pad in GLOB.exodrone_launchers)
		return other_pad

/// encounters random or specificed event for the current site.
/obj/item/exodrone/proc/explore_site(datum/exploration_event/specific_event)
	if(!specific_event) //encounter random event
		var/list/events_to_encounter = list()
		for(var/datum/exploration_event/event in location.events)
			if(event.visited)
				continue
			events_to_encounter += event
		if(!length(events_to_encounter))
			drone_log("It seems there's nothing interesting left around [location.name].")
			return
		var/datum/exploration_event/encountered_event = pick(events_to_encounter)
		encountered_event.encounter(src)
	else if(specific_event.is_targetable())
		specific_event.encounter(src)

/obj/item/exodrone/proc/get_adventure_data()
	var/list/data = current_adventure?.ui_data()
	data["description"] = updateKeywords(data["description"])
	var/list/choices = data["choices"]
	for(var/list/choice in choices)
		choice["text"] = updateKeywords(choice["text"])
	return data

///Replaces $$SITE_NAME with site name and $$QualityName with quality values
/obj/item/exodrone/proc/updateKeywords(text)
	_regex_context = src
	var/static/regex/keywordRegex = regex(@"\$\$(\S*)","g")
	. = keywordRegex.Replace(text,/obj/item/exodrone/proc/replace_keyword)
	_regex_context = null

/// This is called with src = regex datum, so don't try to access any instance variables directly here.
/obj/item/exodrone/proc/replace_keyword(match,g1)
	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(mapload)
	. = ..()
	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_warning("There's already a fuel tank inside [src]!"))
			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_notice("You remove the fuel tank from [src]."))
		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
	name = "advanced fuel pellet"
	fuel_type = FUEL_ADVANCED
	icon_state = "fuel_advanced"

/obj/item/fuel_pellet/exotic
	name = "exotic fuel pellet"
	fuel_type = FUEL_EXOTIC
	icon_state = "fuel_exotic"

#undef EXODRONE_LOG_SIZE
#undef EXODRONE_CARGO_SLOTS
#undef FUEL_BASIC
#undef BASIC_FUEL_TIME_COST
#undef FUEL_ADVANCED
#undef ADVANCED_FUEL_TIME_COST
#undef FUEL_EXOTIC
#undef EXOTIC_FUEL_TIME_COST


code:
/// 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)


code:
/// 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)


code:
/// 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"


code:
/// 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)


code:
/// 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

/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)


code:
/// 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].")


code:
/// 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 = pick_weight(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_db_entry/entry in GLOB.explorer_drone_adventure_db_entries)
		if(entry.valid_for_use(site_traits))
			possible_adventures += entry
	if(!length(possible_adventures))
		return
	var/datum/adventure_db_entry/chosen_db_entry = pick(possible_adventures)
	var/datum/adventure/chosen_adventure = chosen_db_entry.create_adventure()
	chosen_db_entry.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"


code:
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 = pick_weight(GLOB.maintenance_loot)
		while(islist(lootspawn))
			lootspawn = pick_weight(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/security/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_danger("This chip is invalid!"))
		return
	pack_singleton.special_enabled = TRUE
	to_chat(user,span_notice("Contract accepted into nanotrasen supply database."))
	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(mapload)
	. = ..()
	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_notice("You need to wield [src] in two hands before you can fire it."))
		return
	if(LAZYACCESS(user.do_afters, "firelance"))
		return
	if(!cell.use(charge_per_use))
		to_chat(user,span_warning("[src] battery ran dry!"))
		return
	ADD_TRAIT(user,TRAIT_IMMOBILIZED,src)
	to_chat(user,span_notice("You begin to charge [src]"))
	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 get_line(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)


code:
/// Admin adventure manager
/datum/adventure_browser
	var/datum/adventure/temp_adventure
	var/feedback_message

/datum/adventure_browser/ui_interact(mob/user, datum/tgui/ui)
	. = ..()
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "AdventureBrowser")
		ui.open()

/datum/adventure_browser/ui_state(mob/user)
	return GLOB.admin_state

/// Handles finishing adventure
/datum/adventure_browser/proc/resolve_adventure(datum/source,result)
	SIGNAL_HANDLER
	feedback_message = "Adventure ended with result : [result]"
	QDEL_NULL(temp_adventure)

/datum/adventure_browser/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	. = ..()
	if(.)
		return
	feedback_message = ""
	var/mob/user = usr
	switch(action)
		if("create")
			var/datum/adventure_db_entry/new_entry = new
			new_entry.name = "New Adventure"
			new_entry.uploader = user.ckey
			GLOB.explorer_drone_adventure_db_entries += new_entry
			return TRUE
		if("delete")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			if(!target.remove())
				feedback_message = "Failed to remove adventure"
			return TRUE
		if("approve")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			target.approved = !target.approved
			return TRUE
		if("play")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			if(temp_adventure)
				QDEL_NULL(temp_adventure)
			temp_adventure = target.create_adventure()
			if(!temp_adventure)
				feedback_message = "Instantiating adventure failed. Check runtime logs for details."
				return TRUE
			RegisterSignal(temp_adventure,COMSIG_ADVENTURE_FINISHED,.proc/resolve_adventure)
			temp_adventure.start_adventure()
			feedback_message = "Adventure started"
			return TRUE
		if("adventure_choice")
			temp_adventure?.select_choice(params["choice"])
			return TRUE
		if("end_play")
			if(temp_adventure)
				QDEL_NULL(temp_adventure)
			feedback_message = "Adventure stopped"
			return TRUE
		if("refresh")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			target.refresh()
			return TRUE
		if("save")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			target.save()
			return TRUE
		if("download")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			var/temp_file = file("data/AdventureDownloadTempFile")
			fdel(temp_file)
			WRITE_FILE(temp_file, target.raw_json)
			user << ftp(temp_file,"[target.name].json")
			return TRUE
		if("upload")
			var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
			if(!target)
				return
			var/source_json = input(user,"Select adventure JSON file.","Adventure Upload") as file|null
			if(!source_json)
				return
			var/raw_json = file2text(source_json)
			var/json = json_decode(raw_json)
			if(!json)
				feedback_message = "Decoding JSON failed."
				return
			//do basic validation here
			target.raw_json = raw_json
			target.extract_metadata()
			return TRUE

/datum/adventure_browser/ui_data(mob/user)
	. = ..()
	var/list/adventure_data = list()
	for(var/datum/adventure_db_entry/db_entry in GLOB.explorer_drone_adventure_db_entries)
		adventure_data += list(list(
			"ref" = ref(db_entry),
			"id" = db_entry.id,
			"name" = db_entry.name,
			"version" = db_entry.version,
			"uploader" = db_entry.uploader,
			"timestamp" = db_entry.timestamp,
			"approved" = db_entry.approved,
			"json_status" = db_entry.raw_json ? "Valid JSON" : "Empty"
		))
	.["adventures"] = adventure_data
	.["feedback_message"] = feedback_message
	.["play_mode"] = !!temp_adventure
	.["adventure_data"] = temp_adventure?.ui_data(user)
	if(temp_adventure?.delayed_action)
		.["delay_time"] = temp_adventure.delayed_action[1]
		.["delay_message"] = temp_adventure.delayed_action[2]
	else
		.["delay_time"] = 0
		.["delay_message"] = ""

/datum/adventure_browser/ui_close(mob/user)
	. = ..()
	qdel(src)

/datum/adventure_browser/ui_assets(mob/user)
	return list(get_asset_datum(/datum/asset/simple/adventure))

/datum/adventure_browser/Destroy(force, ...)
	. = ..()
	QDEL_NULL(temp_adventure)

/client/proc/adventure_manager()
	set category = "Debug"
	set name = "Adventure Manager"

	if(!check_rights(R_DEBUG))
		return
	var/datum/adventure_browser/browser = new()
	browser.ui_interact(usr)