Files
GS13NG/code/game/objects/items/summon.dm
2022-04-06 18:41:31 -03:00

540 lines
15 KiB
Plaintext

/// doing nothing/orbiting idly
#define STATE_IDLE 0
/// performing reset animation
#define STATE_RESET 1
/// performing attack animation
#define STATE_ATTACK 2
/// performing animation between attacks
#define STATE_RECOVER 3
/**
* Simple summon weapon code in this file
*
* tl;dr latch onto target, repeatedly proc attacks, animate using transforms,
* no real hitboxes/collisions, think of /datum/component/orbit-adjacent
*/
/obj/item/summon
name = "a horrifying mistake"
desc = "Why does this exist?"
/// datum type
var/host_type
/// number of summons
var/summon_count = 6
/// how long it takes for a "stack" to fall off by itself
var/stack_duration = 5 SECONDS
/// our summon weapon host
var/datum/summon_weapon_host/host
/// range summons will chase to
var/range = 7
/// are we a ranged weapon?
var/melee_only = TRUE
/obj/item/summon/Initialize(mapload)
. = ..()
if(host_type)
host = new host_type(src, summon_count, range)
/obj/item/summon/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
if(!host)
return
if(!proximity_flag && melee_only)
return
Target(target)
/obj/item/summon/dropped(mob/user, silent)
. = ..()
addtimer(CALLBACK(src, .proc/check_activation), 0, TIMER_UNIQUE)
/obj/item/summon/equipped(mob/user, slot)
. = ..()
addtimer(CALLBACK(src, .proc/check_activation), 0, TIMER_UNIQUE)
/obj/item/summon/proc/check_activation()
if(!host)
return
if(!isliving(loc))
host.SetMaster(null)
var/mob/living/L = loc
if(!istype(L))
return
if(!L.is_holding(src))
host.SetMaster(src)
host.Suppress()
host.SetMaster(L)
host.Wake()
/obj/item/summon/proc/Target(atom/victim)
if(!host?.CheckTarget(victim))
return
host.AutoTarget(victim, stack_duration)
/obj/item/summon/sword
name = "spectral blade"
desc = "An eldritch blade that summons phantasms to attack one's enemies."
icon = 'icons/obj/items_and_weapons.dmi'
icon_state = "spectral"
item_state = "spectral"
lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
host_type = /datum/summon_weapon_host/sword
force = 15
sharpness = SHARP_EDGED
/**
* Serves as the master datum for summon weapons
*/
/datum/summon_weapon_host
/// master atom
var/atom/master
/// suppressed?
var/active = TRUE
/// actual projectiles
var/list/datum/summon_weapon/controlled
/// active projectiles - refreshing a projectile reorders the list, so if they all have the same stack durations, you can trust the list to have last-refreshed at [1]
var/list/datum/summon_weapon/attacking
/// idle projectiles
var/list/datum/summon_weapon/idle
/// projectile type
var/weapon_type
/// default stack time
var/stack_time = 5 SECONDS
/// range
var/range = 7
/datum/summon_weapon_host/New(atom/master, count, range)
src.master = master
src.range = range
controlled = list()
attacking = list()
idle = list()
Create(count)
/datum/summon_weapon_host/Destroy()
QDEL_LIST(controlled)
master = null
return ..()
/datum/summon_weapon_host/proc/SetMaster(atom/master, reset_on_failure = TRUE)
var/changed = src.master != master
src.master = master
if(changed)
for(var/datum/summon_weapon/weapon as anything in idle)
weapon.Reset()
if(!master && reset_on_failure)
for(var/datum/summon_weapon/weapon as anything in attacking)
weapon.Reset()
/datum/summon_weapon_host/proc/Create(count)
if(!weapon_type)
return
for(var/i in 1 to min(count, clamp(20 - controlled.len - count, 0, 20)))
var/datum/summon_weapon/weapon = new weapon_type
Associate(weapon)
/datum/summon_weapon_host/proc/Associate(datum/summon_weapon/linking)
if(linking.host && linking.host != src)
linking.host.Disassociate(linking)
linking.host = src
controlled |= linking
linking.Reset()
/datum/summon_weapon_host/proc/Disassociate(datum/summon_weapon/unlinking, reset = TRUE, autodel)
if(unlinking.host == src)
unlinking.host = null
controlled -= unlinking
if(reset)
unlinking.Reset(del_no_host = autodel)
idle -= unlinking
attacking -= unlinking
/datum/summon_weapon_host/proc/AutoTarget(atom/victim, duration = stack_time)
if(!active)
return
var/datum/summon_weapon/weapon = (idle.len && idle[1]) || (attacking.len && attacking[1])
if(!weapon)
return
if(!CheckTarget(victim))
return
weapon.Target(victim)
if(duration)
weapon.ResetIn(duration)
/datum/summon_weapon_host/proc/OnTarget(datum/summon_weapon/weapon, atom/victim)
attacking -= weapon
idle -= weapon
attacking |= weapon
/datum/summon_weapon_host/proc/OnReset(datum/summon_weapon/weapon, atom/victim)
attacking -= weapon
idle |= weapon
/datum/summon_weapon_host/proc/CheckTarget(atom/victim)
if(isitem(victim))
return FALSE
if(QDELETED(victim))
return FALSE
if(victim == master)
return FALSE
if(isliving(victim))
var/mob/living/L = victim
if(L.stat == DEAD)
return FALSE
return TRUE
if(isobj(victim))
var/obj/O = victim
return (O.obj_flags & CAN_BE_HIT)
return FALSE
/datum/summon_weapon_host/proc/Suppress()
active = FALSE
for(var/datum/summon_weapon/weapon as anything in controlled)
weapon.Reset()
/datum/summon_weapon_host/proc/Wake()
active = TRUE
for(var/datum/summon_weapon/weapon as anything in controlled)
weapon.Reset()
/datum/summon_weapon_host/sword
weapon_type = /datum/summon_weapon/sword
/**
* A singular summoned object
*
* How summon weapons work:
*
* Reset() - makes it go back to its master.
* Target() - locks onto a target for a duration
*
* The biggest challenge is synchronizing animations.
* Variables keep track of when things tick, but,
* animations are client-timed, and not server-timed
*
* Animations:
* The weapon can only track its "intended" angle and dist
* "Current" pixel x/y are always calculated relative to a target from the current orbiting atom the physical effect is on
* There's 3 animations,
* MoveTo(location, angle, dist, rotation)
* Orbit(location)
* Rotate(degrees)
*
* And an non-animation that just snaps it to a location,
* HardReset(location)
*/
/datum/summon_weapon
/// name
var/name = "summoned weapon"
/// host
var/datum/summon_weapon_host/host
/// icon file
var/icon = 'icons/effects/summon.dmi'
/// icon state
var/icon_state
/// mutable_appearance to use, will skip making from icon/icon state if so
var/mutable_appearance/appearance
/// the actual effect
var/atom/movable/summon_weapon_effect/atom
/// currently locked attack target
var/atom/victim
/// current angle from victim - clockwise from 0. null if not attacking.
var/angle
/// current distance from victim - pixels
var/dist
/// current rotation - angles clockwise from north
var/rotation
/// rand dist to rotate during reattack phase
var/angle_vary = 45
/// orbit distance from victim - pixels
var/orbit_dist = 72
/// orbit distance variation from victim
var/orbit_dist_vary = 24
/// attack delay in deciseconds - this is time spent between attacks
var/attack_speed = 1.5
/// attack length in deciseconds - this is the attack animation speed in total
var/attack_length = 1.5
/// attack damage
var/attack_damage = 5
/// reset animation duration
var/reset_speed = 2
/// attack damtype
var/attack_type = BRUTE
/// attack sound
var/attack_sound = list(
'sound/weapons/bladeslice.ogg',
'sound/weapons/bladesliceb.ogg'
)
/// attack verb
var/attack_verb = list(
"rended",
"pierced",
"penetrated",
"sliced"
)
/// current state
var/state = STATE_IDLE
/// animation locked until
var/animation_lock
/// animation lock timer
var/animation_timerid
/// reset timerid
var/reset_timerid
/datum/summon_weapon/New(mutable_appearance/appearance_override)
if(appearance_override)
appearance = appearance_override
Setup()
attack_verb = typelist(NAMEOF(src, attack_verb), attack_verb)
attack_sound = typelist(NAMEOF(src, attack_sound), attack_sound)
/datum/summon_weapon/Destroy()
host.Disassociate(src, autodel = FALSE)
QDEL_NULL(atom)
QDEL_NULL(appearance)
return ..()
/datum/summon_weapon/proc/Setup()
atom = new
if(!appearance)
GenerateAppearance()
atom.appearance = appearance
atom.moveToNullspace()
if(host)
Reset()
/datum/summon_weapon/proc/GenerateAppearance()
if(!appearance)
appearance = new
appearance.icon = icon
appearance.icon_state = icon_state
appearance.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
appearance.opacity = FALSE
appearance.plane = GAME_PLANE
appearance.layer = ABOVE_MOB_LAYER
appearance.appearance_flags = KEEP_TOGETHER
appearance.overlays = list(
emissive_appearance(icon, icon_state)
)
/datum/summon_weapon/proc/Reset(immediate = FALSE, del_no_host = TRUE)
angle = null
victim = null
if(reset_timerid)
deltimer(reset_timerid)
reset_timerid = null
host?.OnReset(src)
atom.Release()
state = STATE_RESET
if(!host)
if(del_no_host)
qdel(src)
return
HardReset(null)
atom.moveToNullspace()
return
if(immediate)
if(animation_timerid)
deltimer(animation_timerid)
Act()
else
Wake()
/datum/summon_weapon/proc/ResetIn(ds)
reset_timerid = addtimer(CALLBACK(src, .proc/Reset), ds, TIMER_STOPPABLE)
/datum/summon_weapon/proc/Target(atom/victim)
if(!istype(victim) || !isturf(victim.loc) || (host && !host.CheckTarget(victim)))
Reset()
return
src.victim = victim
host.OnTarget(src, victim)
state = STATE_ATTACK
Wake()
/datum/summon_weapon/proc/Wake()
if(!animation_timerid)
Act()
/datum/summon_weapon/proc/AnimationLock(duration)
if(animation_timerid)
deltimer(animation_timerid)
animation_timerid = addtimer(CALLBACK(src, .proc/Act), duration, TIMER_CLIENT_TIME | TIMER_STOPPABLE)
/datum/summon_weapon/proc/Act()
animation_timerid = null
switch(state)
if(STATE_IDLE)
return
if(STATE_ATTACK)
if(!isturf(victim.loc) || (host && !host.CheckTarget(victim)))
Reset(TRUE)
return
state = STATE_RECOVER
// register hit at the halfway mark
// we can do better math to approximate when the attack will hit but i'm too tired to bother
addtimer(CALLBACK(src, .proc/Hit, victim), attack_length / 2, TIMER_CLIENT_TIME)
// we need to approximate our incoming angle - again, better math exists but why bother
var/incoming_angle = angle
if(isturf(atom.loc) && (atom.loc != victim.loc))
incoming_angle = Get_Angle(atom.loc, victim.loc)
// pierce through target
// we do not want to turn while doing this so we pierce through them visually
incoming_angle += 180
var/outgoing_angle = SIMPLIFY_DEGREES(incoming_angle)
AnimationLock(MoveTo(victim, null, outgoing_angle, orbit_dist + rand(-orbit_dist_vary, orbit_dist_vary), outgoing_angle, attack_length))
if(STATE_RESET)
state = STATE_IDLE
if(!host || !host.active || !get_turf(host.master))
atom.moveToNullspace()
src.angle = null
src.dist = null
src.rotation = null
return
var/reset_angle = rand(0, 360)
AnimationLock(MoveTo(host.master, null, reset_angle, 30, 90, reset_speed))
addtimer(CALLBACK(src, .proc/Orbit, host.master, reset_angle, 30, 3 SECONDS), reset_speed, TIMER_CLIENT_TIME)
if(STATE_RECOVER)
state = STATE_ATTACK
AnimationLock(Rotate(rand(-angle_vary, angle_vary), attack_speed, null))
/datum/summon_weapon/proc/Hit(atom/victim)
if(!isobj(victim) && !isliving(victim))
return FALSE
if(isliving(victim))
var/mob/living/L = victim
L.apply_damage(attack_damage, attack_type)
playsound(victim, pick(attack_sound), 75)
else if(isobj(victim))
var/obj/O = victim
O.take_damage(attack_damage, attack_type)
return TRUE
/**
* relative to defaults to current location
*/
/datum/summon_weapon/proc/MoveTo(atom/destination, atom/relative_to, angle = 0, dist = 64, rotation = 180, time)
. = time
// construct final transform
var/matrix/dest = ConstructMatrix(angle, dist, rotation)
// move to
atom.Lock(destination)
// get relative first positions
relative_to = get_turf(relative_to || atom.locked)
destination = get_turf(destination)
// if none, move to immediately and end
if(!relative_to)
atom.transform = dest
src.angle = angle
src.dist = dist
src.rotation = rotation
// end animations
animate(atom, time = 0, flags = ANIMATION_END_NOW)
return 0
// grab source
var/rel_x = (destination.x - relative_to.x) * world.icon_size + src.dist * sin(src.angle)
var/rel_y = (destination.y - relative_to.y) * world.icon_size + src.dist * cos(src.angle)
// construct source matrix
var/matrix/source = new
source.Turn((relative_to == get_turf(atom.locked))? src.rotation : Get_Angle(relative_to, destination))
source.Translate(rel_x, rel_y)
// set vars
src.angle = angle
src.dist = dist
src.rotation = rotation
// animate
atom.transform = source
animate(atom, transform = dest, time, FALSE, LINEAR_EASING, ANIMATION_LINEAR_TRANSFORM | ANIMATION_END_NOW)
/**
* rotation defaults to facing towards locked atom
*/
/datum/summon_weapon/proc/Rotate(degrees, time, rotation)
. = time
if(!dist)
return 0
var/matrix/M = ConstructMatrix(angle + degrees, dist, rotation || src.rotation)
if(rotation)
src.rotation = rotation
angle += degrees
animate(atom, transform = M, time, FALSE, LINEAR_EASING, ANIMATION_END_NOW | ANIMATION_LINEAR_TRANSFORM)
/datum/summon_weapon/proc/Orbit(atom/destination, initial_degrees = rand(0, 360), dist, speed)
. = 0
atom.Lock(destination)
animate(atom, 0, FALSE, flags = ANIMATION_END_NOW)
atom.transform = ConstructMatrix(initial_degrees, dist, 90)
atom.SpinAnimation(speed, parallel = FALSE, segments = 10)
// we can't predict dist/angle anymre because clienttime vs servertime.
// well, we can, but, let's not be bothered with timeofday math eh.
dist = 0
angle = 0
/datum/summon_weapon/proc/ConstructMatrix(angle = 0, dist = 64, rotation = 0)
var/matrix/M = new
M.Turn(rotation)
M.Translate(0, dist)
M.Turn(angle)
return M
/datum/summon_weapon/proc/HardReset(atom/snap_to)
if(animation_timerid)
deltimer(animation_timerid)
atom.Release()
atom.forceMove(snap_to)
atom.transform = null
/datum/summon_weapon/sword
name = "spectral blade"
icon_state = "sword"
attack_damage = 5
attack_speed = 1.5
attack_length = 1.5
/atom/movable/summon_weapon_effect
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
plane = GAME_PLANE
layer = ABOVE_MOB_LAYER
opacity = FALSE
density = FALSE
/// locked atom
var/atom/locked
/atom/movable/summon_weapon_effect/Destroy()
Release()
return ..()
/atom/movable/summon_weapon_effect/proc/Lock(atom/target)
if(locked == target)
return
if(locked)
Release()
if(!target)
return
locked = target
forceMove(locked.loc)
if(ismovable(locked))
RegisterSignal(locked, COMSIG_MOVABLE_MOVED, .proc/Update)
/atom/movable/summon_weapon_effect/proc/Release()
if(ismovable(locked))
UnregisterSignal(locked, COMSIG_MOVABLE_MOVED)
locked = null
/atom/movable/summon_weapon_effect/proc/Update()
if(!locked)
return
if(loc != locked.loc)
forceMove(locked.loc)
#undef STATE_IDLE
#undef STATE_ATTACK
#undef STATE_RECOVER
#undef STATE_RESET