mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-09 16:12:17 +00:00
340 lines
13 KiB
Plaintext
340 lines
13 KiB
Plaintext
GLOBAL_LIST_EMPTY(all_portal_masters)
|
|
|
|
/*
|
|
|
|
Portal map effects allow a mapper to join two distant places together, while looking somewhat seamlessly connected.
|
|
This can allow for very strange PoIs that twist and turn in what appear to be physically impossible ways.
|
|
|
|
Portals do have some specific requirements when mapping them in;
|
|
- There must by one, and only one `/obj/effect/map_effect/portal/master` for each side of a portal.
|
|
- Both sides need to have matching `portal_id`s in order to link to each other.
|
|
- Each side must face opposite directions, e.g. if side A faces SOUTH, side B must face NORTH.
|
|
- Clarification on the above - you will be moved in the direction that the portal faces.
|
|
If Side A faces south, you will be moved south. Dirs are 1/2/4/8, 1: NORTH, 2: SOUTH, 4: EAST, 8: WEST.
|
|
To further explain: If your cave entrance is on the NORTH side of the map on ENTRY side, and SOUTH side on EXIT side:
|
|
You will need to set the ENTRY side's dir to 2, IE SOUTH, as that's the direction you will moving coming FROM the EXIT side.
|
|
IE: Directions should be set based on the direction of travel.
|
|
- Each side must have the same orientation, e.g. horizontal on both sides, or vertical on both sides.
|
|
- Portals can be made to be longer than 1x1 with `/obj/effect/map_effect/portal/line`s,
|
|
but both sides must have the same length.
|
|
- If portal lines are added, they must form a straight line and be next to a portal master or another portal line.
|
|
- If portal lines are used, both portal masters should be in the same relative position among the lines.
|
|
E.g. both being on the left most side on a horizontal row.
|
|
|
|
Portals also have some limitations to be aware of when mapping. Some of these are not an issue if you're trying to make an 'obvious' portal;
|
|
- The objects seen through portals are purely visual, which has many implications,
|
|
such as simple_mob AIs being blind to mobs on the other side of portals.
|
|
- Objects on the other side of a portal can be interacted with if the interaction has no range limitation,
|
|
or the distance between the two portal sides happens to be less than the interaction max range. Examine will probably work,
|
|
while picking up an item that appears to be next to you will fail.
|
|
- Sounds currently are not carried across portals.
|
|
- Mismatched lighting between each portal end can make the portal look obvious.
|
|
- Portals look weird when observing as a ghost, or otherwise when able to see through walls. Meson vision will also spoil the illusion.
|
|
- Walls that change icons based on neightboring walls can give away that a portal is nearby if both sides don't have a similar transition.
|
|
- Projectiles that pass through portals will generally work as intended, however aiming and firing upon someone on the other side of a portal
|
|
will likely be weird due to the click targeting the real position of the thing clicked instead of the apparent position.
|
|
Thrown objects suffer a similar fate.
|
|
- The tiles that are visually shown across a portal are determined based on visibility at the time of portal initialization,
|
|
and currently don't update, meaning that opacity changes are not reflected, e.g. a wall is deconstructed, or an airlock is opened.
|
|
- There is currently a small but somewhat noticable pause in mob movement when moving across a portal,
|
|
as a result of the mob's glide animation being inturrupted by a teleport.
|
|
- Gas is not transferred through portals, and ZAS is oblivious to them.
|
|
|
|
A lot of those limitations can potentially be solved with some more work. Otherwise, portals work best in static environments like Points of Interest,
|
|
when portals are shortly lived, or when portals are made to be obvious with special effects.
|
|
*/
|
|
|
|
/obj/effect/map_effect/portal
|
|
name = "portal subtype"
|
|
invisibility = INVISIBILITY_NONE
|
|
opacity = TRUE
|
|
plane = TURF_PLANE
|
|
layer = ABOVE_TURF_LAYER
|
|
appearance_flags = NONE
|
|
|
|
var/obj/effect/map_effect/portal/counterpart = null // The portal line or master that this is connected to, on the 'other side'.
|
|
|
|
// Information used to apply `pixel_[x|y]` offsets so that the visuals line up.
|
|
// Set automatically by `calculate_dimensions()`.
|
|
var/total_height = 0 // Measured in tiles.
|
|
var/total_width = 0
|
|
|
|
var/portal_distance_x = 0 // How far the portal is from the left edge, in tiles.
|
|
var/portal_distance_y = 0 // How far the portal is from the top edge.
|
|
|
|
/obj/effect/map_effect/portal/Destroy()
|
|
vis_contents = null
|
|
if(counterpart)
|
|
counterpart.counterpart = null // Disconnect our counterpart from us
|
|
counterpart = null // Now disconnect us from them.
|
|
return ..()
|
|
|
|
// Called when something touches the portal, and usually teleports them to the other side.
|
|
/obj/effect/map_effect/portal/Crossed(atom/movable/AM)
|
|
..()
|
|
if(!AM)
|
|
return
|
|
if(!counterpart)
|
|
return
|
|
|
|
go_through_portal(AM)
|
|
|
|
|
|
/obj/effect/map_effect/portal/proc/go_through_portal(atom/movable/AM)
|
|
// TODO: Find a way to fake the glide or something.
|
|
if(isliving(AM))
|
|
var/mob/living/L = AM
|
|
if(L.pulling)
|
|
var/atom/movable/pulled = L.pulling
|
|
L.stop_pulling()
|
|
// For some reason, trying to put the pulled object behind the person makes the drag stop and it doesn't even move to the other side.
|
|
// pulled.forceMove(get_turf(counterpart))
|
|
pulled.forceMove(counterpart.get_focused_turf())
|
|
L.forceMove(counterpart.get_focused_turf())
|
|
L.continue_pulling(pulled)
|
|
else
|
|
L.forceMove(counterpart.get_focused_turf())
|
|
else
|
|
AM.forceMove(counterpart.get_focused_turf())
|
|
|
|
// 'Focused turf' is the turf directly in front of a portal,
|
|
// and it is used both as the destination when crossing, as well as the PoV for visuals.
|
|
/obj/effect/map_effect/portal/proc/get_focused_turf()
|
|
return get_step(get_turf(src), dir)
|
|
|
|
// Determines the size of the block of turfs inside `vis_contents`, and where the portal is in relation to that.
|
|
/obj/effect/map_effect/portal/proc/calculate_dimensions()
|
|
var/highest_x = 0
|
|
var/lowest_x = 0
|
|
|
|
var/highest_y = 0
|
|
var/lowest_y = 0
|
|
|
|
// First pass is for finding the top right corner.
|
|
for(var/turf/T as anything in vis_contents)
|
|
if(T.x > highest_x)
|
|
highest_x = T.x
|
|
if(T.y > highest_y)
|
|
highest_y = T.y
|
|
|
|
lowest_x = highest_x
|
|
lowest_y = highest_y
|
|
|
|
// Second one is for the bottom left corner.
|
|
for(var/turf/T as anything in vis_contents)
|
|
if(T.x < lowest_x)
|
|
lowest_x = T.x
|
|
if(T.y < lowest_y)
|
|
lowest_y = T.y
|
|
|
|
// Now calculate the dimensions.
|
|
total_width = (highest_x - lowest_x) + 1
|
|
total_height = (highest_y - lowest_y) + 1
|
|
|
|
// Find how far the portal is from the edges.
|
|
var/turf/focused_T = counterpart.get_focused_turf()
|
|
portal_distance_x = lowest_x - focused_T.x
|
|
portal_distance_y = lowest_y - focused_T.y
|
|
|
|
|
|
// Portal masters manage everything else involving portals.
|
|
// This is the base type. Use `/side_a` or `/side_b` with matching IDs for actual portals.
|
|
/obj/effect/map_effect/portal/master
|
|
name = "portal master"
|
|
show_messages = TRUE // So portals can hear and see, and relay to the other side.
|
|
var/portal_id = "test" // For a portal to be made, both the A and B sides need to share the same ID value.
|
|
var/list/portal_lines = list()
|
|
|
|
/obj/effect/map_effect/portal/master/Initialize(mapload)
|
|
GLOB.all_portal_masters += src
|
|
find_lines()
|
|
..()
|
|
return INITIALIZE_HINT_LATELOAD
|
|
|
|
/obj/effect/map_effect/portal/master/LateInitialize()
|
|
find_counterparts()
|
|
make_visuals()
|
|
apply_offset()
|
|
|
|
/obj/effect/map_effect/portal/master/Destroy()
|
|
GLOB.all_portal_masters -= src
|
|
for(var/thing in portal_lines)
|
|
qdel(thing)
|
|
return ..()
|
|
|
|
/obj/effect/map_effect/portal/master/proc/find_lines()
|
|
var/list/dirs_to_search = list( turn(dir, 90), turn(dir, -90) )
|
|
|
|
for(var/dir_to_search in dirs_to_search)
|
|
var/turf/current_T = get_turf(src)
|
|
while(current_T)
|
|
current_T = get_step(current_T, dir_to_search)
|
|
var/obj/effect/map_effect/portal/line/line = locate() in current_T
|
|
if(line)
|
|
portal_lines += line
|
|
line.my_master = src
|
|
else
|
|
break
|
|
|
|
// Connects both sides of a portal together.
|
|
/obj/effect/map_effect/portal/master/proc/find_counterparts()
|
|
for(var/obj/effect/map_effect/portal/master/M as anything in GLOB.all_portal_masters)
|
|
if(M == src)
|
|
continue
|
|
if(M.counterpart)
|
|
continue
|
|
|
|
if(M.portal_id == src.portal_id)
|
|
counterpart = M
|
|
M.counterpart = src
|
|
if(portal_lines.len)
|
|
for(var/i = 1 to portal_lines.len)
|
|
var/obj/effect/map_effect/portal/line/our_line = portal_lines[i]
|
|
var/obj/effect/map_effect/portal/line/their_line = M.portal_lines[i]
|
|
our_line.counterpart = their_line
|
|
their_line.counterpart = our_line
|
|
break
|
|
|
|
if(!counterpart)
|
|
stack_trace("Portal master [type] ([x],[y],[z]) could not find another portal master with a matching portal_id ([portal_id]).")
|
|
|
|
/obj/effect/map_effect/portal/master/proc/make_visuals()
|
|
var/list/observed_turfs = list()
|
|
for(var/obj/effect/map_effect/portal/P as anything in portal_lines + src)
|
|
P.name = null
|
|
P.icon_state = null
|
|
|
|
if(!P.counterpart)
|
|
return
|
|
|
|
var/turf/T = P.counterpart.get_focused_turf()
|
|
P.vis_contents += T
|
|
|
|
var/list/things = dview(world.view, T)
|
|
for(var/turf/turf in things)
|
|
if(get_dir(turf, T) & P.dir)
|
|
if(turf in observed_turfs) // Avoid showing the same turf twice or more for improved performance.
|
|
continue
|
|
|
|
P.vis_contents += turf
|
|
observed_turfs += turf
|
|
|
|
P.calculate_dimensions()
|
|
|
|
// Shifts the portal's pixels in order to line up properly, as BYOND offsets the sprite when it holds multiple turfs inside `vis_contents`.
|
|
// This undos the shift that BYOND did.
|
|
/obj/effect/map_effect/portal/master/proc/apply_offset()
|
|
for(var/obj/effect/map_effect/portal/P as anything in portal_lines + src)
|
|
|
|
P.pixel_x = WORLD_ICON_SIZE * P.portal_distance_x
|
|
P.pixel_y = WORLD_ICON_SIZE * P.portal_distance_y
|
|
|
|
// Allows portals to transfer emotes.
|
|
// Only portal masters do this to avoid flooding the other side with duplicate messages.
|
|
/obj/effect/map_effect/portal/master/see_emote(mob/M, text)
|
|
if(!counterpart)
|
|
return
|
|
var/turf/T = counterpart.get_focused_turf()
|
|
var/list/in_range = get_mobs_and_objs_in_view_fast(T, world.view, 0)
|
|
var/list/mobs_to_relay = in_range["mobs"]
|
|
|
|
for(var/mob/mob as anything in mobs_to_relay)
|
|
var/rendered = span_message("[text]")
|
|
mob.show_message(rendered)
|
|
|
|
..()
|
|
|
|
// Allows portals to transfer visible messages.
|
|
/obj/effect/map_effect/portal/master/show_message(msg, type, alt, alt_type)
|
|
if(!counterpart)
|
|
return
|
|
var/rendered = span_message("[msg]")
|
|
var/turf/T = counterpart.get_focused_turf()
|
|
var/list/in_range = get_mobs_and_objs_in_view_fast(T, world.view, 0)
|
|
var/list/mobs_to_relay = in_range["mobs"]
|
|
|
|
for(var/mob/mob as anything in mobs_to_relay)
|
|
mob.show_message(rendered)
|
|
|
|
..()
|
|
|
|
// Allows portals to transfer speech.
|
|
/obj/effect/map_effect/portal/master/hear_talk(mob/M, list/message_pieces, verb)
|
|
if(!counterpart)
|
|
return
|
|
var/turf/T = counterpart.get_focused_turf()
|
|
var/list/in_range = get_mobs_and_objs_in_view_fast(T, world.view, 0)
|
|
var/list/mobs_to_relay = in_range["mobs"]
|
|
|
|
for(var/mob/mob as anything in mobs_to_relay)
|
|
var/list/combined = mob.combine_message(message_pieces, verb, M)
|
|
var/message = combined["formatted"]
|
|
var/name_used = M.GetVoice()
|
|
var/rendered = null
|
|
rendered = span_game(span_say("[span_name(name_used)] [message]"))
|
|
mob.show_message(rendered, 2)
|
|
|
|
..()
|
|
|
|
// Returns the position that an atom that's hopefully on the other side of the portal would be if it were really there.
|
|
// Z levels not taken into account.
|
|
/obj/effect/map_effect/portal/master/proc/get_apparent_position(atom/A)
|
|
if(!counterpart)
|
|
return null
|
|
|
|
var/turf/true_turf = get_turf(A)
|
|
var/obj/effect/map_effect/portal/master/other_master = counterpart
|
|
|
|
var/in_vis_contents = FALSE
|
|
for(var/obj/effect/map_effect/portal/P as anything in other_master.portal_lines + other_master)
|
|
if(P in true_turf.vis_locs)
|
|
in_vis_contents = TRUE
|
|
break
|
|
|
|
if(!in_vis_contents)
|
|
return null // Not in vision of the other portal.
|
|
|
|
var/turf/their_focus = counterpart.get_focused_turf()
|
|
var/turf/our_focus = get_focused_turf()
|
|
|
|
var/relative_x = (true_turf.x - our_focus.x)
|
|
relative_x += SIGN(relative_x)
|
|
var/relative_y = (true_turf.y - our_focus.y)
|
|
relative_y += SIGN(relative_y)
|
|
|
|
return new /datum/position(their_focus.x + relative_x, their_focus.y + relative_y, our_focus.z)
|
|
|
|
|
|
/obj/effect/map_effect/portal/master/side_a
|
|
name = "portal master A"
|
|
icon_state = "portal_side_a"
|
|
// color = "#00FF00"
|
|
|
|
/obj/effect/map_effect/portal/master/side_b
|
|
name = "portal master B"
|
|
icon_state = "portal_side_b"
|
|
// color = "#FF0000"
|
|
|
|
|
|
|
|
// Portal lines extend out from the sides of portal masters,
|
|
// They let portals be longer than 1x1.
|
|
// Both sides MUST be the same length, meaning if side A is 1x3, side B must also be 1x3.
|
|
/obj/effect/map_effect/portal/line
|
|
name = "portal line"
|
|
var/obj/effect/map_effect/portal/master/my_master = null
|
|
|
|
/obj/effect/map_effect/portal/line/Destroy()
|
|
if(my_master)
|
|
my_master.portal_lines -= src
|
|
my_master = null
|
|
return ..()
|
|
|
|
/obj/effect/map_effect/portal/line/side_a
|
|
name = "portal line A"
|
|
icon_state = "portal_line_side_a"
|
|
|
|
/obj/effect/map_effect/portal/line/side_b
|
|
name = "portal line B"
|
|
icon_state = "portal_line_side_b"
|