SUBSYSTEM_DEF(throwing) name = "Throwing" priority = FIRE_PRIORITY_THROWING wait = 0.25 // scale up to 40 fps subsystem_flags = SS_NO_INIT runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/list/currentrun var/list/processing = list() /datum/controller/subsystem/throwing/stat_entry() return ..() + " P:[processing.len]" /datum/controller/subsystem/throwing/fire(resumed = 0) if (!resumed) src.currentrun = processing.Copy() //cache for sanic speed (lists are references anyways) var/list/currentrun = src.currentrun while(length(currentrun)) var/atom/movable/AM = currentrun[currentrun.len] var/datum/thrownthing/TT = currentrun[AM] currentrun.len-- if (QDELETED(AM) || QDELETED(TT)) processing -= AM if (MC_TICK_CHECK) return continue TT.tick() if (MC_TICK_CHECK) return currentrun = null // todo: the landing stack kinda sucks ass and needs to be unit tested and rewritten /datum/thrownthing //! important stuff /// thing we threw var/atom/movable/thrownthing /// flags var/throw_flags = NONE /// things we impacted already. associative list for speed. var/list/impacted /// thing we originally were thrown at var/atom/target /// turf our original target was on at original time of throw var/turf/target_turf /// turf we initially started from var/turf/initial_turf /// initial byond get_dir from thrown thing to target var/init_dir /// max tiles we can travel var/maxrange /// tiles to move per decisecond var/speed /// what threw us var/atom/thrower /// how hard were we thrown? var/force /// original throw resist var/resist /// callback to call when we hit something. called with (hit atom, thrownthing datum) var/datum/callback/on_hit /// callback to call when we land. will not be called if we do not land on anything. called with (landed atom, thrownthing datum) var/datum/callback/on_land /// paused? var/paused = FALSE //! processing vars /// have we started yet var/started = FALSE /// are we done var/finished = FALSE /// world.time we started var/start_time /// tiles we travelled var/dist_travelled = 0 /// how long we've been paused for var/delayed_time = 0 /// last world.time we moved var/last_move = 0 /// dx of original throw target var/dist_x /// dy of original throw target var/dist_y /// x dir var/dx /// y dir var/dy /// current targeted angle var/current_angle /// tracks diagonal error so we move in a relatively "raycasted" (shittily) path var/diagonal_error /// are we purely diagonal? var/pure_diagonal /// What did we hit that's causing us to land? var/atom/landing_from_impact //! fluff shit players use to kill each other /// zone selected by user at time of throw var/target_zone /// damage multiplier var/damage_multiplier = 1 /datum/thrownthing/New(atom/movable/AM, atom/target, range, speed, flags, atom/thrower, datum/callback/on_hit, datum/callback/on_land, force) src.thrownthing = AM if(!target_atom(target)) qdel(src) return // todo: multiz throws src.maxrange = range src.speed = speed src.throw_flags = flags src.thrower = thrower src.on_hit = on_hit src.on_land = on_land src.force = force src.resist = AM.throw_resist impacted = list() /datum/thrownthing/Destroy() . = ..() if(!finished) terminate(TRUE) if(thrownthing) SSthrowing.processing -= thrownthing thrownthing.throwing = null thrownthing = null landing_from_impact = null target = null thrower = null on_hit = null on_land = null initial_turf = null target_turf = null impacted = null return ..() /datum/thrownthing/proc/target_atom(atom/target) src.target = target var/turf/T = get_turf(target) var/atom/movable/AM = thrownthing if(!T) CRASH("tried to throw something at something that wasn't in the game world.") target_turf = T T = get_turf(AM) if(!T) CRASH("tried to throw something that wasn't in the game world at something") if(!initial_turf) initial_turf = T init_dir = get_dir(thrownthing, target) dist_x = abs(target_turf.x - AM.x) dist_y = abs(target_turf.y - AM.y) current_angle = arctan(target_turf.y - AM.y, target_turf.x - AM.x) dx = (target_turf.x > AM.x)? EAST : WEST dy = (target_turf.y > AM.y)? NORTH : SOUTH if(dist_x == dist_y) pure_diagonal = TRUE else if(dist_x <= dist_y) // standardization: i honestly don't understand why we do this but we do this, thanks MSO var/olddist_x = dist_x var/olddx = dx dist_x = dist_y dist_y = olddist_x dx = dy dy = olddx diagonal_error = dist_x / 2 - dist_y return TRUE /datum/thrownthing/proc/tick() SHOULD_NOT_SLEEP(TRUE) var/atom/movable/AM = thrownthing // if throwing got cancelled maybe like, don't if(!isturf(AM.loc) || !AM.throwing) terminate() return if(paused) delayed_time += world.time - last_move return //calculate how many tiles to move, making up for any missed ticks. var/tilestomove = CEILING(min(((((world.time+world.tick_lag) - start_time + delayed_time) * speed) - (dist_travelled ? dist_travelled : -1)), speed*MAX_TICKS_TO_MAKE_UP) * (world.tick_lag * SSthrowing.wait), 1) // catch anything in our tile //? experimental: removed in favor for cross/bump hooks /* if(dist_travelled) var/atom/to_hit = scan_for_impact(get_turf(AM)) while(to_hit) impact(to_hit) if(finished) return to_hit = scan_for_impact(get_turf(AM)) */ var/atom/stepping last_move = world.time while(tilestomove-- > 0) // if we have gravity we can end, else keep going if(AM.has_gravity()) if(dist_travelled >= maxrange || AM.loc == target_turf) land() return else if(dist_travelled >= MAX_THROWING_DIST) terminate() return // if we havne't reached target yet if(dist_travelled <= max(dist_x, dist_y)) // home in stepping = get_step(AM, get_dir(AM, target_turf)) else // just go init dir, diagonal error solves the rest stepping = get_step(AM, init_dir) // solve diagonal error if(!pure_diagonal) // checks for tile before so we don't raycast past it if(diagonal_error >= 0 && (max(dist_x, dist_y) - dist_travelled != 1)) stepping = get_step(AM, dx) diagonal_error += (diagonal_error < 0)? (dist_x / 2) : -dist_y // if we're out of tiles.. don't run out of the map if(!stepping) land() return // move AM.Move(stepping, get_dir(AM, stepping)) // atom somehow got deleted if(QDELETED(AM)) terminate() return // we hit something during move if(finished) return // tick up dist moved ++dist_travelled /** * hook for making us impact things on bump (we cross them) */ /datum/thrownthing/proc/bump_into(atom/A) // if you sleep, eat shit set waitfor = FALSE if(!can_hit(A, TRUE)) return impact(A) /** * hook for making us impact thing on cross (they cross us) */ /datum/thrownthing/proc/crossed_by(atom/movable/AM) // if you sleep, eat shit set waitfor = FALSE if(!can_hit(AM)) return impact(AM) /datum/thrownthing/proc/scan_for_impact(turf/T) RETURN_TYPE(/atom) var/atom/highest for(var/atom/thing as anything in T) if(!can_hit(thing)) continue if(!highest || highest.layer < thing.layer) highest = thing return highest /datum/thrownthing/proc/can_hit(atom/A, bumping) if((throw_flags & THROW_AT_OVERHAND) && (A != target) && A.check_pass_flags_self(ATOM_PASS_OVERHEAD_THROW)) return FALSE if(A == thrownthing) return FALSE if(A == thrower) return FALSE if(impacted[A]) return FALSE if(!bumping && thrownthing.CanPass(A, get_turf(thrownthing))) return FALSE return TRUE /** * quickstart - immediately tick the first tick */ /datum/thrownthing/proc/quickstart() if(throw_flags & THROW_AT_QUICKSTARTED) return throw_flags |= THROW_AT_QUICKSTARTED tick(1) /** * start - register to subsystem */ /datum/thrownthing/proc/start() if(started) CRASH("double start") start_time = world.time started = TRUE SSthrowing.processing[thrownthing] = src if(SSthrowing.state == SS_PAUSED && length(SSthrowing.currentrun)) SSthrowing.currentrun[thrownthing] = src if(!(throw_flags & THROW_AT_NO_AUTO_QUICKSTART)) quickstart() /** * handle impacting an atom * return TRUE if we should end the throw, FALSE to pierce */ /datum/thrownthing/proc/impact(atom/impacting, in_land) impacted[impacting] = TRUE var/op_return = thrownthing._throw_do_hit(impacting, src) if(op_return & COMPONENT_THROW_HIT_TERMINATE) terminate() return on_hit?.InvokeAsync(impacting, src) if(!(op_return & COMPONENT_THROW_HIT_PIERCE) && !in_land) landing_from_impact = impacting land(get_turf(thrownthing)) landing_from_impact = null return // we are piercing. move again. tick(1) /** * land on something and terminate the throw */ /datum/thrownthing/proc/land(atom/landing = get_turf(thrownthing)) // todo: need to rewrite to consider qdel's for object + us maybe? // nothing to land on if(!landing) terminate() return // hit our target if we haven't already if(!impacted[target] && (target in get_turf(landing)) && target.is_throw_explicit_targetable(src)) impact(target, TRUE) // we got terminated already if(finished) return // land thrownthing._throw_finalize(landing, src) on_land?.InvokeAsync(landing, src) // halt terminate() /** * terminate the throw. * when called, immediately erases the throw from the atom and stops it. */ /datum/thrownthing/proc/terminate(in_qdel) // todo: shitcode, rewrite if(QDELETED(src)) return finished = TRUE thrownthing.throwing = null if(!QDELETED(thrownthing)) // move addtimer(CALLBACK(thrownthing, TYPE_PROC_REF(/atom/movable, newtonian_move), init_dir), 1) addtimer(CALLBACK(thrownthing, TYPE_PROC_REF(/atom/movable, fall)), 1) if(in_qdel) return qdel(src) /** * should we skip damage entirely? */ /datum/thrownthing/proc/is_pacifistic() return throw_flags & THROW_AT_IS_GENTLE /** * get damage scaling - default handling * * @params * * target - (optional) thing being hit */ /datum/thrownthing/proc/get_damage_multiplier(atom/target) if(!resist) return MAX_THROWING_DAMAGE_MULTIPLIER . = damage_multiplier if(thrownthing.movable_flags & MOVABLE_NO_THROW_DAMAGE_SCALING) return if(throw_flags & THROW_AT_NO_SCALE_DAMAGE) return // todo: this should be damage_multipliers and damage_classifier based // for now, snowflake. if(ismob(thrownthing) && isobj(target)) . *= 2.5 // :trol: // multiplier = force > resist? (force / resist) ** (p * 0.1) : 1 / (force / resist) ** (p * 0.1) if(isnull(force)) . *= speed > resist? (speed / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) : 1 / (speed / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) . *= force > resist? (force / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) : 1 / (force / resist) ** (thrownthing.throw_damage_scaling_exponential * 0.1) /** * get damage tier - default handling */ /datum/thrownthing/proc/get_damage_tier(atom/target) if(isitem(thrownthing)) var/obj/item/thing = thrownthing return thing.damage_tier if(iscarbon(thrownthing)) return 4 // :trol: return 2.5 /** * Get angle we're flying */ /datum/thrownthing/proc/get_current_angle() return current_angle /** * simulated thrownthing datums * doesn't register to subsystem * immediately hits and deletes on start */ /datum/thrownthing/emulated /datum/thrownthing/emulated/start() return // you must manually quickstart /datum/thrownthing/emulated/quickstart() if(throw_flags & THROW_AT_QUICKSTARTED) return throw_flags |= THROW_AT_QUICKSTARTED process_hit() /datum/thrownthing/emulated/proc/process_hit() // hit without landing impact(target, TRUE) // gtfo terminate()