/proc/roundTZ(var/i) if (i < 0) return -round(-i) return round(i) /proc/angleToDir(var/angle) if (angle >= 360) return angleToDir(angle-360) else if (angle < 0) return angleToDir(angle+360) else if (angle < 22.5 || angle >= 337.5) return NORTH else if (angle < 67.5) return NORTHWEST else if (angle < 112.5) return WEST else if (angle < 157.5) return SOUTHWEST else if (angle < 202.5) return SOUTH else if (angle < 247.5) return SOUTHEAST else if (angle < 292.5) return EAST else return NORTHEAST /obj/projectile name = "projectile" flags = TABLEPASS var/xo var/yo // I have no idea what to do with these. var/target = null var/datum/projectile/proj_data = null //var/obj/o_shooter = null var/list/targets = list() var/dissipation_ticker = 0 // moved this from the datum because why the hell was it on there var/power = 20 // local copy of power for proper dissipation tracking var/implanted = null var/forensic_ID = null var/atom/shooter = null // Who/what fired this? var/mob/mob_shooter = null // We use shooter to avoid self collision, however, the shot may have been initiated through a proxy object. This is for logging. var/travelled = 0 // track distance var/angle // for reference @see setup() animate_movement = 0 var/turf/orig_turf var/datum/light/light var/data = 0 var/was_pointblank = 0 // Adjusts the log entry accordingly. var/was_setup = 0 var/far_border_hit var/incidence = 0 // reflection normal on the current tile (NORTH if projectile came from the north, etc.) var/list/crossing = list() var/list/special_data = list() var/curr_t = 0 var/wx = 0 var/wy = 0 proc/rotateDirection(var/angle) var/oldxo = xo var/oldyo = yo var/sa = sin(angle) var/ca = cos(angle) xo = ca * oldxo - sa * oldyo yo = sa * oldxo + ca * oldyo src.Turn(-angle) proc/launch() proj_data.on_launch(src) if (!disposed) spawn(0) process() proc/process() if (!src.was_setup) src.setup() while (!disposed) do_step() sleep(1) proc/collide(atom/A as mob|obj|turf|area) if (!A) // you never know ok?? return if (disposed) // if disposed is set to true we have been pooled or set for garbage collection and shouldn't process bumps return if (A == shooter) // never collide with the original shooter return // Necessary because the check in human.dm is ineffective (Convair880). var/immunity = check_target_immunity(A) if (immunity) log_shot(src, A, 1) A.visible_message("The projectile bounces off of [A] uselessly!") die() return // Unified check instead of a separate one in every mob type's bullet_act() (Convair880). if (ismob(A)) for (var/obj/item/cloaking_device/S in A.contents) if (S.active) S.deactivate(A) src.visible_message("[A]'s cloak is disrupted!") for (var/obj/item/device/disguiser/D in A.contents) if (D.on) D.disrupt(A) src.visible_message("[A]'s disguiser is disrupted!") // also run the atom's general bullet act var/atom/B = A.bullet_act(src) //If bullet_act returns an atom, do all bad stuff to that atom instead if(istype(B)) A = B // if we made it this far this is a valid bump, run the specific projectile's hit code proj_data.on_hit(A, src.angle, src) //Trigger material on attack. if(proj_data.material) proj_data.material.triggerOnAttack(src, src.shooter, A) if (istype(A,/turf)) // if we hit a turf apparently the bullet is magical and hits every single object in the tile, nice shooting tex for (var/obj/O in A) O.bullet_act(src) if (istype(A, /turf/simulated/wall) && proj_data.icon_turf_hit) var/turf/simulated/wall/W = A if (src.forensic_ID) W.forensic_impacts += src.forensic_ID var/image/impact = image('icons/obj/projectiles.dmi', proj_data.icon_turf_hit) impact.transform = turn(impact.transform, pick(0, 90, 180, 270)) impact.pixel_x += rand(-12,12) impact.pixel_y += rand(-12,12) W.proj_impacts += impact W.update_projectile_image(ticker.round_elapsed_ticks) die() // pool the projectile pooled() name = "projectile" xo = 0 yo = 0 pixel_x = 0 pixel_y = 0 power = 0 dissipation_ticker = 0 travelled = 0 target = null proj_data = null //o_shooter = null shooter = null mob_shooter = null implanted = null forensic_ID = null targets = null angle = 0 was_setup = 0 was_pointblank = 0 data = 0 crossing.len = 0 curr_t = 0 wx = 0 wy = 0 color = null special_data.len = 0 overlays = null transform = null removeMaterial() ..() proc/die() if (proj_data) proj_data.on_end(src) if (src.light) src.light.disable() pool(src) proc/set_icon() if(istype(proj_data)) src.icon = proj_data.icon src.icon_state = proj_data.icon_state if (!proj_data.override_color) src.color = proj_data.color_icon else src.icon = 'icons/obj/projectiles.dmi' src.icon_state = null if (!proj_data.override_color) src.color = "#ffffff" proc/setup() if (src.proj_data == null || (xo == 0 && yo == 0) || proj_data.projectile_speed == 0) die() return name = src.proj_data.name set_icon() orig_turf = get_turf(src) var/len = sqrt(src.xo * src.xo + src.yo * src.yo) if (len == 0) die() src.xo = src.xo / len src.yo = src.yo / len if (src.yo == 0) if (src.xo < 0) src.angle = 90 else src.angle = 270 else if (src.xo == 0) if (src.yo < 0) src.angle = 180 else src.angle = 0 else var/r = 1 src.angle = arccos(src.yo / r) var/anglecheck = arcsin(-src.xo / r) if (anglecheck < 0) src.angle = -src.angle transform = null Turn(-angle) if (!proj_data.precalculated) return var/x32 = 0 var/xs = 1 var/y32 = 0 var/ys = 1 if (xo) x32 = 32 / (proj_data.projectile_speed * xo) if (x32 < 0) xs = -1 x32 = -x32 if (yo) y32 = 32 / (proj_data.projectile_speed * yo) if (y32 < 0) ys = -1 y32 = -y32 var/max_t if (proj_data.dissipation_rate) max_t = proj_data.dissipation_delay + round(proj_data.power / proj_data.dissipation_rate) + 1 else max_t = 500 // why not var/next_x = x32 / 2 var/next_y = y32 / 2 var/ct = 0 var/turf/T = get_turf(src) var/cx = T.x var/cy = T.y while (ct < max_t) if (next_x == 0 && next_y == 0) break if (next_x == 0 || (next_y != 0 && next_y < next_x)) ct = next_y next_y = ct + y32 cy += ys else ct = next_x next_x = ct + x32 cx += xs var/turf/Q = locate(cx, cy, T.z) if (!Q) break crossing += Q crossing[Q] = ct curr_t = 0 src.was_setup = 1 Bump(var/atom/A) src.collide(A) Crossed(var/atom/movable/A) if (!istype(A)) return // can't happen will happen if (!A.CanPass(src, get_step(src, A.dir), 1, 0)) src.collide(A) proc/do_step() if (!loc) die() return src.dissipation_ticker++ // The bullet has expired/decayed. if (src.dissipation_ticker > src.proj_data.dissipation_delay) src.power -= src.proj_data.dissipation_rate if (src.power <= 0) die() return proj_data.tick(src) if (disposed) return var/turf/curr_turf = loc var/dwx = src.proj_data.projectile_speed * src.xo var/dwy = src.proj_data.projectile_speed * src.yo curr_t++ src.travelled += src.proj_data.projectile_speed if (proj_data.precalculated) for (var/i = 1, i < crossing.len, i++) var/turf/T = crossing[i] if (crossing[T] < curr_t) Move(T) if (disposed) return incidence = get_dir(curr_turf, T) curr_turf = T crossing.Cut(1,2) i-- else break wx += dwx wy += dwy dir = 1 if (!proj_data.precalculated) var/trfx = round((wx + 16) / 32) var/trfy = round((wy + 16) / 32) var/turf/Dest = locate(orig_turf.x + trfx, orig_turf.y + trfy, orig_turf.z) if (loc != Dest) Move(Dest) incidence = get_dir(curr_turf, Dest) if (!(incidence in cardinal)) var/txl = wx + 16 % 32 var/tyl = wy + 16 % 32 var/ext = xo < 0 ? (32 - txl) / -xo : txl / xo var/eyt = yo < 0 ? (32 - tyl) / -yo : tyl / yo if (ext < eyt) incidence &= EAST | WEST else incidence &= NORTH | SOUTH incidence = turn(incidence, 180) var/dx = loc.x - orig_turf.x var/dy = loc.y - orig_turf.y var/dpx = dx * 32 var/dpy = dy * 32 pixel_x = wx - dpx pixel_y = wy - dpy datum/projectile // These vars were copied from the an projectile datum. I am not sure which version, probably not 4407. var name = "projectile" icon = 'icons/obj/projectiles.dmi' icon_state = "bullet" // A special note: the icon state, if not a point-symmetric sprite, should face NORTH by default. icon_turf_hit = null // what kinda overlay they puke onto turfs when they hit brightness = 0 color_red = 0 color_green = 0 color_blue = 0 color_icon = "#ffffff" override_color = 0 power = 20 // How much of a punch this has cost = 1 // How much ammo this costs dissipation_rate = 2 // How fast the power goes away dissipation_delay = 10 // How many tiles till it starts to lose power dissipation_ticker = 0 // Tracks how many tiles we moved ks_ratio = 1.0 /* Kill/Stun ratio, when it hits a mob the damage/stun is based upon this and the power eg 1.0 will cause damage = to power while 0.0 would cause just stun = to power */ sname = "stun" // name of the projectile setting, used when you change a guns setting shot_sound = 'sound/weapons/Taser.ogg' // file location for the sound you want it to play shot_number = 0 // How many projectiles should be fired, each will cost the full cost damage_type = D_KINETIC // What is our damage type hit_type = null // For blood system damage - DAMAGE_BLUNT, DAMAGE_CUT and DAMAGE_STAB hit_ground_chance = 0 // With what % do we hit mobs laying down window_pass = 0 // Can we pass windows obj/projectile/master = null silentshot = 0 // standard visible message upon bullet_act implanted // Path of "bullet" left behind in the mob on successful hit disruption = 0 // planned thing to deal with pod electronics / etc zone = null // todo: if fired from a handheld gun, check the targeted zone --- this should be in the goddamn obj caliber = null datum/material/material = null casing = null reagent_payload = null forensic_ID = null precalculated = 1 // Determines the amount of length units the projectile travels each tick // A tile is 32 wide, 32 long, and 32 * sqrt(2) across. // Setting this to 32 will mimic the old behaviour for shots travelling in one of the cardinal directions. var/projectile_speed = 28 // Determines the impact range of the projectile. Should ideally be half the length of the sprite // for line-based stuff (lasers), or the radius for circular projectiles. // If the projectile is irregular (like, a square), try to use the radius of a circle that touches the farthest point // of the edge of a shape in a cardinal direction from the center of symmetry (eg. half the edge length for a square) // This is mostly aesthetic, so the player feels like they are actually hit when the projectile reaches them, not // earlier or later. // For very high speed projectiles, this may be much lower than the suggested amounts. // If 0, no forward checking is done at all. May be useful for stuff like revolver shots, where the bullets are literally // a pixel wide. var/impact_range = 8 // Self-explanatory. var/hits_ghosts = 0 proc hit_ground() return prob(hit_ground_chance) //For future use, ie guns that can change the power settings set_power() return //When it hits a mob or such should anything special happen on_hit(atom/hit, angle, var/obj/projectile/O) return tick(var/obj/projectile/O) return on_launch(var/obj/projectile/O) return on_pointblank(var/obj/projectile/O, var/mob/target) return on_end(var/obj/projectile/O) return // WOO IMPACT RANGES // Meticulously calculated by hand. datum/projectile/laser impact_range = 16 datum/projectile/laser/pred impact_range = 2 datum/projectile/laser/light impact_range = 2 datum/projectile/laser/glitter impact_range = 4 datum/projectile/laser/precursor impact_range = 4 datum/projectile/laser/precursor/sphere impact_range = 16 datum/projectile/laser/mining impact_range = 12 datum/projectile/laser/eyebeams impact_range = 4 datum/projectile/laser/drill impact_range = 0 datum/projectile/laser/drill/cutter impact_range = 0 datum/projectile/fourtymm impact_range = 12 datum/projectile/bfg impact_range = 16 datum/projectile/bullet impact_range = 0 datum/projectile/bullet/autocannon impact_range = 2 datum/projectile/bullet/autocannon/plasma_orb impact_range = 8 datum/projectile/bullet/autocannon/huge impact_range = 8 datum/projectile/bullet/glitch impact_range = 4 // for the gun, not the drone datum/projectile/bullet/glitch/gun impact_range = 16 datum/projectile/bullet/rod impact_range = 16 datum/projectile/bullet/flare/ufo impact_range = 8 datum/projectile/owl impact_range = 16 datum/projectile/disruptor impact_range = 4 datum/projectile/disruptor/high impact_range = 4 datum/projectile/energy_bolt impact_range = 4 datum/projectile/energy_bolt_v impact_range = 4 datum/projectile/energy_bolt_antighost impact_range = 16 hits_ghosts = 1 // do it. datum/projectile/rad_bolt impact_range = 0 datum/projectile/tele_bolt impact_range = 4 datum/projectile/wavegun impact_range = 4 datum/projectile/snowball impact_range = 4 // THIS IS INTENDED FOR POINTBLANKING. /proc/hit_with_projectile(var/S, var/datum/projectile/DATA, var/atom/T) if (!S || !T) return var/times = max(1, DATA.shot_number) for (var/i = 1, i <= times, i++) var/obj/projectile/P = initialize_projectile_ST(S, DATA, T) if (S == T) P.shooter = null P.mob_shooter = S hit_with_existing_projectile(P, T) /proc/hit_with_existing_projectile(var/obj/projectile/P, var/atom/T) if (!P || !T) return if (ismob(T)) var/immunity = check_target_immunity(T) // Point-blank overrides, such as stun bullets (Convair880). if (immunity) log_shot(P, T, 1) T.visible_message("...but the projectile bounces off uselessly!") P.die() return P.proj_data.on_pointblank(P, T) P.collide(T) // The other immunity check is in there (Convair880). /proc/shoot_projectile_ST(var/atom/movable/S, var/datum/projectile/DATA, var/T) if (!S) return if (!isturf(S) && !isturf(S.loc)) return null var/turf/target = get_turf(T) var/obj/projectile/Q = shoot_projectile_relay(S, DATA, target) if (DATA.shot_number > 1) spawn(0) for (var/i = 2, i < DATA.shot_number, i++) sleep(1) shoot_projectile_relay(S, DATA, target) return Q /proc/shoot_projectile_ST_pixel(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy) if (!S) return if (!isturf(S) && !isturf(S.loc)) return null var/turf/target = get_turf(T) var/obj/projectile/Q = shoot_projectile_relay_pixel(S, DATA, target, pox, poy) if (DATA.shot_number > 1) spawn(0) for (var/i = 2, i <= DATA.shot_number, i++) sleep(1) shoot_projectile_relay_pixel(S, DATA, target, pox, poy) return Q /proc/shoot_projectile_ST_pixel_spread(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy, var/spread_angle) if (!S) return if (!isturf(S) && !isturf(S.loc)) return null var/turf/target = get_turf(T) var/obj/projectile/Q = shoot_projectile_relay_pixel_spread(S, DATA, target, pox, poy, spread_angle) if (DATA.shot_number > 1) spawn(0) for (var/i = 2, i <= DATA.shot_number, i++) sleep(1) shoot_projectile_relay_pixel_spread(S, DATA, target, pox, poy, spread_angle) return Q /proc/shoot_projectile_DIR(var/atom/movable/S, var/datum/projectile/DATA, var/dir) if (!S) return if (!isturf(S) && !isturf(S.loc)) return null var/turf/T = get_step(get_turf(S), dir) if (T) return shoot_projectile_ST(S, DATA, T) return null /proc/shoot_projectile_relay(var/atom/movable/S, var/datum/projectile/DATA, var/T) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/obj/projectile/P = initialize_projectile_ST(S, DATA, T) if (P) P.launch() return P /proc/shoot_projectile_relay_pixel(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/obj/projectile/P = initialize_projectile_pixel(S, DATA, T, pox, poy) if (P) P.launch() return P /proc/shoot_projectile_relay_pixel_spread(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy, var/spread_angle) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/obj/projectile/P = initialize_projectile_pixel_spread(S, DATA, T, pox, poy, spread_angle) if (P) P.launch() return P /proc/shoot_projectile_XY(var/atom/movable/S, var/datum/projectile/DATA, var/xo, var/yo) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/obj/projectile/Q = shoot_projectile_XY_relay(S, DATA, xo, yo) if (DATA.shot_number > 1) spawn(0) for (var/i = 2, i <= DATA.shot_number, i++) sleep(1) shoot_projectile_XY_relay(S, DATA, xo, yo) return Q /proc/shoot_projectile_XY_relay(var/atom/movable/S, var/datum/projectile/DATA, var/xo, var/yo) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/obj/projectile/P = initialize_projectile(get_turf(S), DATA, xo, yo, S) if (P) P.launch() return P /proc/initialize_projectile_ST(var/atom/movable/S, var/datum/projectile/DATA, var/T) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/turf/Q1 = get_turf(S) var/turf/Q2 = get_turf(T) return initialize_projectile(Q1, DATA, Q2.x - Q1.x, Q2.y - Q1.y, S) /proc/initialize_projectile_pixel(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy) if (!S) return if (!isturf(S) && !isturf(S.loc)) return var/turf/Q1 = get_turf(S) var/turf/Q2 = get_turf(T) return initialize_projectile(Q1, DATA, (Q2.x - Q1.x) * 32 + pox, (Q2.y - Q1.y) * 32 + poy, S) /proc/initialize_projectile_pixel_spread(var/atom/movable/S, var/datum/projectile/DATA, var/T, var/pox, var/poy, var/spread_angle) var/obj/projectile/P = initialize_projectile_pixel(S, DATA, T, pox, poy) if (P && spread_angle) if (spread_angle < 0) spread_angle = -spread_angle var/spread = rand(spread_angle * 10) / 10 P.rotateDirection(prob(50) ? spread : -spread) return P /proc/initialize_projectile(var/turf/S, var/datum/projectile/DATA, var/xo, var/yo, var/shooter = null) if (!S) return var/obj/projectile/P = unpool(/obj/projectile) if(!P) return P.set_loc(S) P.proj_data = DATA if (narrator_mode) playsound(S, 'sound/vox/shoot.ogg', 50, 1) else if(DATA.shot_sound && shooter) playsound(S, DATA.shot_sound, 50) if (isobj(shooter)) for (var/mob/M in shooter) M << sound(DATA.shot_sound, volume=50) #ifdef DATALOGGER if (game_stats && istype(game_stats)) game_stats.Increment("gunfire") #endif if (DATA.implanted) P.implanted = DATA.implanted P.set_icon() P.shooter = shooter P.name = DATA.name P.setMaterial(DATA.material) P.power = DATA.power if (DATA.brightness) if (!P.light) P.light = new /datum/light/point P.light.attach(P) P.light.set_brightness(DATA.brightness) P.light.set_color(DATA.color_red, DATA.color_green, DATA.color_blue) P.light.enable() P.xo = xo P.yo = yo return P /proc/stun_bullet_hit(var/obj/projectile/P, var/mob/living/M) if (ishuman(M) && M.stat != 2) var/mob/living/carbon/human/H = M H.lying = 1 H.stat = 1 H.stunned = max(H.stunned, 1) else if (istype(M, /mob/living/silicon/robot)) var/mob/living/silicon/robot/R = M R.stunned = max(R.stunned, 5) /proc/shoot_reflected(var/obj/projectile/P, var/obj/reflector) var/obj/projectile/Q = initialize_projectile(get_turf(reflector), P.proj_data, -P.xo, -P.yo, reflector) if (!Q) return null if (ismob(P.shooter)) Q.mob_shooter = P.shooter Q.name = "reflected [Q.name]" Q.launch() return Q /proc/shoot_cardinal_normal_reflected(var/obj/projectile/P, var/obj/reflector) if (!P.incidence || !(P.incidence in cardinal)) return null var/rx = 0 var/ry = 0 var/nx = P.incidence == WEST ? -1 : (P.incidence == EAST ? 1 : 0) var/ny = P.incidence == SOUTH ? -1 : (P.incidence == NORTH ? 1 : 0) var/dn = 2 * (P.xo * nx + P.yo * ny) // incident direction DOT normal * 2 rx = P.xo - dn * nx // r = d - 2 * (d * n) * n ry = P.yo - dn * ny if (rx == ry && rx == 0) logTheThing("debug", null, null, "Marquesas/Reflecting Projectiles: Reflection failed for [P.name] (incidence: [P.incidence], direction: [P.xo];[P.yo]).") return null // unknown error var/obj/projectile/Q = initialize_projectile(get_turf(reflector), P.proj_data, rx, ry, reflector) if (!Q) return null if (ismob(P.shooter)) Q.mob_shooter = P.shooter Q.name = "reflected [Q.name]" Q.launch() return Q