Files
Bubberstation/code/game/objects/structures/safe.dm
SkyratBot 1dd5011776 [MIRROR] Adds pathmaps, refactors pathfinding a bit [MDB IGNORE] (#24414)
* Adds pathmaps, refactors pathfinding a bit (#78684)

## About The Pull Request

Implements /datum/pathfind/sssp, which generates /datum/path_map

/datum/path_maps allow us to very efficently generate paths to any turf
they contain from their central point.

We're effectively running the single source shortest paths algorithm.
We expand from the center turf, adding turfs as they're found, and then
processing them in order of addition.
As we go, we remember what turf "found" us first. Reversing this chain
gives us the shortest possible path from the center turf to any turf in
its range (or the inverse).

This isn't all that useful on its own, outside of a few niche cases
(Like if we wanted to get the farthest reachable turf from the center)
but if we could reuse the map more then once, we'd be able to swarm
to/from a point very easily.

Reuse is a bit troublesome, reqiures a timeout system and a way to
compare different movables trying to get paths.
I've implemented it tho. I've refactored CanAStarPass to take a datum,
/datum/can_pass_info. This is built from a movable and a list of access,
and copies all the properties that would impact pathfinding over onto
itself.

There is one case where we don't do this, pathing over openspace
requires checking if we'd fall through the openspace, and the proc for
that takes an atom.
So instead we use the weakref to the owner that we hold onto, and hold
copies of all the values that would impact the check on the datum.

When someone requests a swarmed path their pass info is compared with
the pass info of all other path_maps centered on their target turf. If
it matches and their requested timeout isn't too short, we just reuse
the map.

Timeout is a tricky thing because the longer a map exists the more out
of date it gets.
I've added a few age defines that let you modulate your level of risk
here. We default to only allowing maps that are currently
being generated, or finished generating in our tick.
Hopefully this prevents falling into trouble, but consumers will need to
allow "failed" movements.

As a part of this datumized pass info, I've refactored pathfinding to
use access lists, rather then id cards directly. This also avoids some
dumbass harddel oppertunities, and prevents an idcard from changing mid
path.

Did a few things to the zPass procs, they took args that they did NOT
need, and I thought it'd be better to yeet em.

If you'd all like I could undo the caching/can_pass_info stuff if you'd
all like. I think it's useful generally because it avoids stuff changing
mid pathfind attempt, but if it's too clunky I could nuke it.

Oh also I added optional args to jps that constricts how it handles
diagonals. I've used this to fix bot paths.

## Why It's Good For The Game

Much of this is redundant currently. I'm adding it because it could have
saved hugglebippers, and because I get the feeling it'll be useful for
"grouping" mobs like bees and such.
We're doing more basic mob work currently and I want to provide extra
tools for that work.

https://github.com/tgstation/tgstation/assets/58055496/66aca1f9-c6e7-4173-9c38-c40516d6d853

## Changelog
🆑
add: Adds swarmed pathfinding, trading accuracy for potential
optimization of used correctly
fix: Bots will no longer take diagonal paths, preventing weirdo looking
path visuals
refactor: Refactored bits of pathfinding code, hopefully easier to add
new pathfinding strategies now
/🆑

* Adds pathmaps, refactors pathfinding a bit

---------

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
2023-10-18 03:31:21 -04:00

252 lines
7.4 KiB
Plaintext

/*
CONTAINS:
SAFES
FLOOR SAFES
*/
/// Chance for a sound clue
#define SOUND_CHANCE 10
/// Explosion number threshold for opening safe
#define BROKEN_THRESHOLD 3
//SAFES
/obj/structure/safe
name = "safe"
desc = "A huge chunk of metal with a dial embedded in it. Fine print on the dial reads \"Scarborough Arms - 2 tumbler safe, guaranteed thermite resistant, explosion resistant, and assistant resistant.\""
icon = 'icons/obj/structures.dmi'
icon_state = "safe"
anchored = TRUE
density = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_UI_INTERACT
/// The maximum combined w_class of stuff in the safe
var/maxspace = 24
/// The amount of tumblers that will be generated
var/number_of_tumblers = 2
/// Whether the safe is open or not
var/open = FALSE
/// Whether the safe is locked or not
var/locked = TRUE
/// The position the dial is pointing to
var/dial = 0
/// The list of tumbler dial positions that need to be hit
var/list/tumblers = list()
/// The index in the tumblers list of the tumbler dial position that needs to be hit
var/current_tumbler_index = 1
/// The combined w_class of everything in the safe
var/space = 0
/// Tough, but breakable if explosion counts reaches set value
var/explosion_count = 0
/obj/structure/safe/Initialize(mapload)
. = ..()
// Combination generation
for(var/iterating in 1 to number_of_tumblers)
tumblers.Add(rand(0, 99))
if(!mapload)
return
// Put as many items on our turf inside as possible
for(var/obj/item/inserting_item in loc)
if(space >= maxspace)
return
if(inserting_item.w_class + space <= maxspace)
space += inserting_item.w_class
inserting_item.forceMove(src)
/obj/structure/safe/update_icon_state()
icon_state = "[initial(icon_state)][open ? "-open" : null]"
return ..()
/obj/structure/safe/attackby(obj/item/attacking_item, mob/user, params)
if(open)
. = TRUE //no afterattack
if(attacking_item.w_class + space <= maxspace)
if(!user.transferItemToLoc(attacking_item, src))
to_chat(user, span_warning("\The [attacking_item] is stuck to your hand, you cannot put it in the safe!"))
return
space += attacking_item.w_class
to_chat(user, span_notice("You put [attacking_item] in [src]."))
else
to_chat(user, span_warning("[attacking_item] won't fit in [src]."))
else
if(istype(attacking_item, /obj/item/clothing/neck/stethoscope))
attack_hand(user)
return
else
to_chat(user, span_warning("You can't put [attacking_item] into the safe while it is closed!"))
return
/obj/structure/safe/blob_act(obj/structure/blob/B)
return
/obj/structure/safe/ex_act(severity, target)
if(((severity == EXPLODE_HEAVY && target == src) || severity == EXPLODE_DEVASTATE) && explosion_count < BROKEN_THRESHOLD)
explosion_count++
switch(explosion_count)
if(1)
desc = initial(desc) + "\nIt looks a little banged up."
if(2)
desc = initial(desc) + "\nIt's pretty heavily damaged."
if(3)
desc = initial(desc) + "\nThe lock seems to be broken."
return TRUE
return FALSE
/obj/structure/safe/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/simple/safe),
)
/obj/structure/safe/ui_state(mob/user)
return GLOB.physical_state
/obj/structure/safe/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Safe", name)
ui.open()
/obj/structure/safe/ui_data(mob/user)
var/list/data = list()
data["dial"] = dial
data["open"] = open
data["locked"] = locked
data["broken"] = check_broken()
if(open)
var/list/contents_names = list()
data["contents"] = contents_names
for(var/obj/jewel in contents)
contents_names[++contents_names.len] = list("name" = jewel.name, "sprite" = jewel.icon_state)
user << browse_rsc(icon(jewel.icon, jewel.icon_state), "[jewel.icon_state].png")
return data
/obj/structure/safe/ui_act(action, params)
. = ..()
if(.)
return
if(!ishuman(usr))
return
var/mob/living/carbon/human/user = usr
if(!user.can_perform_action(src))
return
var/canhear = FALSE
if(user.is_holding_item_of_type(/obj/item/clothing/neck/stethoscope))
canhear = TRUE
switch(action)
if("open")
if(!check_unlocked() && !open && !broken)
to_chat(user, span_warning("You cannot open [src], as its lock is engaged!"))
return
to_chat(user, span_notice("You [open ? "close" : "open"] [src]."))
open = !open
update_appearance()
return TRUE
if("turnright")
if(open)
return
if(broken)
to_chat(user, span_warning("The dial will not turn, as the mechanism is destroyed!"))
return
var/ticks = text2num(params["num"])
for(var/iterate in 1 to ticks)
dial = WRAP(dial - 1, 0, 100)
var/invalid_turn = current_tumbler_index % 2 == 0 || current_tumbler_index > number_of_tumblers
if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
current_tumbler_index = 1
if(!invalid_turn && dial == tumblers[current_tumbler_index])
notify_user(user, canhear, list("tink", "krink", "plink"), ticks, iterate)
current_tumbler_index++
else
notify_user(user, canhear, list("clack", "scrape", "clank"), ticks, iterate)
check_unlocked()
return TRUE
if("turnleft")
if(open)
return
if(broken)
to_chat(user, span_warning("The dial will not turn, as the mechanism is destroyed!"))
return
var/ticks = text2num(params["num"])
for(var/iterate in 1 to ticks)
dial = WRAP(dial + 1, 0, 100)
var/invalid_turn = current_tumbler_index % 2 != 0 || current_tumbler_index > number_of_tumblers
if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
current_tumbler_index = 1
if(!invalid_turn && dial == tumblers[current_tumbler_index])
notify_user(user, canhear, list("tonk", "krunk", "plunk"), ticks, iterate)
current_tumbler_index++
else
notify_user(user, canhear, list("click", "chink", "clink"), ticks, iterate)
check_unlocked()
return TRUE
if("retrieve")
if(!open)
return
var/index = text2num(params["index"])
if(!index)
return
var/obj/item/retrieved_item = contents[index]
if(!retrieved_item || !in_range(src, user))
return
user.put_in_hands(retrieved_item)
space -= retrieved_item.w_class
return TRUE
/**
* Checks if safe is considered in a broken state for force-opening the safe
*/
/obj/structure/safe/proc/check_broken()
return broken || explosion_count >= BROKEN_THRESHOLD
/**
* Called every dial turn to determine whether the safe should unlock or not.
*/
/obj/structure/safe/proc/check_unlocked()
if(check_broken())
return TRUE
if(current_tumbler_index > number_of_tumblers)
locked = FALSE
visible_message(span_boldnotice("[pick("Spring", "Sprang", "Sproing", "Clunk", "Krunk")]!"))
return TRUE
locked = TRUE
return FALSE
/**
* Called every dial turn to provide feedback if possible.
*/
/obj/structure/safe/proc/notify_user(user, canhear, sounds, total_ticks, current_tick)
if(!canhear)
return
if(current_tick == 2)
to_chat(user, "<span class='italics'>The sounds from [src] are too fast and blend together.</span>")
if(total_ticks == 1 || prob(SOUND_CHANCE))
balloon_alert(user, pick(sounds))
//FLOOR SAFES
/obj/structure/safe/floor
name = "floor safe"
icon_state = "floorsafe"
density = FALSE
layer = LOW_OBJ_LAYER
/obj/structure/safe/floor/Initialize(mapload)
. = ..()
AddElement(/datum/element/undertile)
#undef SOUND_CHANCE
#undef BROKEN_THRESHOLD