From 4632c85d71978175cb07230011826b2332bd2480 Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Fri, 11 Feb 2022 21:04:21 -0800 Subject: [PATCH] silly proof of concept item (#15456) * should have listened to putnam * why am i doing this * that * lmao * more * it compiles * it compiles * fixes * FUNNY GLOW SWORD * more.. * unterminated text * wthat wasn't working * it's coming together * woo * what have i done * fin. * eh --- code/game/objects/items/summon.dm | 539 ++++++++++++++++++++++++++++++ icons/effects/summon.dmi | Bin 0 -> 815 bytes tgstation.dme | 1 + 3 files changed, 540 insertions(+) create mode 100644 code/game/objects/items/summon.dm create mode 100644 icons/effects/summon.dmi diff --git a/code/game/objects/items/summon.dm b/code/game/objects/items/summon.dm new file mode 100644 index 0000000000..93846d10ab --- /dev/null +++ b/code/game/objects/items/summon.dm @@ -0,0 +1,539 @@ +/// 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() + . = ..() + 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 diff --git a/icons/effects/summon.dmi b/icons/effects/summon.dmi new file mode 100644 index 0000000000000000000000000000000000000000..fefd79d845b882f96932b0759d39acb314a58b4a GIT binary patch literal 815 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=l~o}TB`&GO$wiq3C7Jno3=9=> zg2M`mO22;zF8KKMiI%sn*10q1gExd4Tr__0Nawtd=E(pc$L!IgGahGs&TBLlba@MT z2Z`p0PU$-RxjSge4vS!;?k*soXQ##GE}`OJ#hj4Pk3TJ2LLUV!So7f3^FR<~^Tq;TGq$)2Az)O*ki3Pdd1EZbGg$2lqq<1`F%>TPGZybqAyeE!)8TU7_M^oX>JIUL4R{zC5+%BJ9k67amA1L^ zaYol>rW{2v1G~95PdFZa$=C6C$CPsq*EC$sDuu{BWZl5LL6CcfpO{(Eg&)tY9W<}5 z-+gMev75?+b>+=dg8piBeS57_`ZdV1YtdhO$>MmS4+-mAKR#}sq`7U*r ztDMX~fAwg3WZ1GyDW)I0a_f>AtDLv|eInTBpZ5Ak!S!F~3Iavk!(G2?39)UT8?S5U zImr5K>IuR%TJqSea=`@ZpBZYAK(5S{A%{&Q_($(8`+6g zkq_Rke)8*;{QQ`R`&#=a8LYU&$I$SwK;qSTwWG`qi{HsBhjPxYjR&!5$ESxDt5 z^P%kxyjFAhzW)B}JVCahSNs6qfn(eW>gTe~DWM4fRmXB{ literal 0 HcmV?d00001 diff --git a/tgstation.dme b/tgstation.dme index 38e9409074..3d6516437e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1174,6 +1174,7 @@ #include "code\game\objects\items\singularityhammer.dm" #include "code\game\objects\items\spear.dm" #include "code\game\objects\items\stunbaton.dm" +#include "code\game\objects\items\summon.dm" #include "code\game\objects\items\taster.dm" #include "code\game\objects\items\teleportation.dm" #include "code\game\objects\items\teleprod.dm"