/** Use the `Knockback3.add(..)` functions to apply 3D knockback to units. Using angles: import Knockback3 ... let u = ... Knockback3.add(u, 1000., 0 .fromDeg(), 45 .fromDeg()) Using a target position: let caster = GetSpellAbilityUnit() let target = getSpellTargetPos() Knockback3.add(caster, target, 500.) Configure this system by overriding the @configurable globals and setting the static class members: Knockback3.gravity = 25. By default, this package allows units to collide with destructables and destroy them. To disable this feature, make the global destroyDestructableSpeedThreshold a very high value: Knockback3.destroyDestructableSpeedThreshold = 999999999. This value defaults to 300. */ package Knockback3 import TerrainUtils import MapBounds import LinkedListModule import HashMap import Objects /** A vec3 -> real function. */ public interface TerrainZProvider function get(vec3 where) returns real /** A unit -> bool function. */ public interface UnitFilter function get(unit which) returns bool /** A vec3 -> vec3 closure. */ public interface WallHitTransform function apply(vec3 vel) returns vec3 /** If configured, provides an alternative source for getting terrain z-height. This can be useful for avoiding a desync in some cases. */ @configurable TerrainZProvider HEIGHTMAP_PROVIDER = (vec3 w) -> w.getTerrainZ() /** Controls the behavior when hitting a wall at low altitude. Default behavior is to bounce/reverse off the wall, applying the same restitution as in bouncing off the ground. Override this value if you want to prevent bouncing, reflect on planes, etc. */ @configurable WallHitTransform WALLHIT_TRANSFORM = (vec3 vel) -> ( vel.rotate(vec3(0., 0., 1.), PI) * Knockback3.restitutionCoefficientGround ) /** The square rect size used for finding destructables. */ @configurable let DESTRUCTABLE_ENUM_SIZE = 130. /** If enabled, units have their move-speed changed while airborne. Warning: this is not a lock-safe form of crowd control. */ @configurable let USE_MOVE_SPEED_MODIFIERS = true /** If enabled, units have their prop window changed while airborne. Warning: this is not a lock-safe form of crowd control. */ @configurable let USE_PROP_WINDOW_MODIFIERS = true /** Units must match this filter, or Knockback3 takes no effect. */ @configurable UnitFilter UNIT_FILTER = (unit u) -> not u.isType(UNIT_TYPE_FLYING) public class Knockback3 use LinkedListModule /** Fraction of velocity retained after colliding with ground/destructable. */ static var restitutionCoefficientGround = .2 static var restitutionCoefficientDestructable = .3 /** Ratio. */ static var frictionCoefficientGround = .15 /** In units per second squared. */ static var gravity = 90. /** If a unit is not falling faster than this, it will not bounce. Units per second. */ static var elasticityThreshold = -300. /** If a unit's vertical component is not greater than this, it will not become airborne. Units per second. */ static var airborneThreshold = 150. /** For the purposes of friction, distinguish a unit which is airborne versus one which is sliding. Height in units. */ static var isAirborneThreshold = 5. /** Below this speed, sliding units will stop. */ static var minimumSlideSpeed = 30. /** Above this speed, sliding units will spawn a dust effect. */ static var frictionFxThreshold = 180. static var frictionFxPath = Objects.impaleTargetDust /** Above this speed, destructables hit will be destroyed. */ static var destroyDestructableSpeedThreshold = 300. /** Below this height, destructables hit may be destroyed. */ static var destroyDestructableHeightThreshold = 150. /** Apply a knockback vector to unit u. `velocity` is initial speed in units per second. `groundAngle` and `airAngle` are the direction and trajectory parameters, respectively. Example: */ static function add(unit u, real velocity, angle groundAngle, angle airAngle) if not UNIT_FILTER.get(u) return let instVel = velocity * ANIMATION_PERIOD let v = ZERO3.polarProject(instVel, groundAngle, airAngle) if unitNodes.has(u) unitNodes.get(u).del += v else let knockback = new Knockback3() unitNodes.put(u, knockback) knockback.u = u knockback.del = v if size == 1 clock.startPeriodic(ANIMATION_PERIOD, function tick) if knockback.u.addAbility(HEIGHT_ENABLER) knockback.u.removeAbility(HEIGHT_ENABLER) /** Apply a knockback vector to unit u. If the unit is stopped, this vector will throw the unit to the position at vec2 `target`. Along the XY-plane, the unit will move `groundSpeed` units per second. Note that the more vertical angle will always be used from the pair of possible trajectories. Example: */ static function add(unit u, vec2 target, real groundSpeed) let t = (target - u.getPos()).length() / groundSpeed let theta = u.getPos().angleTo(target) let velZ = gravity * t / 2. / ANIMATION_PERIOD let vel = theta.toVec(groundSpeed).withZ(velZ) let speed = vel.length() add(u, speed, theta, vec2(groundSpeed, vel.z).getAngle()) /** Setter for the knockback vector on unit u. If the unit is not already tracked, this has the same behavior as `add`. */ static function setVel(unit u, real velocity, angle groundAngle, angle airAngle) if unitNodes.has(u) let knockback = unitNodes.get(u) let instVel = velocity * ANIMATION_PERIOD knockback.del = ZERO3.polarProject(instVel, groundAngle, airAngle) else add(u, velocity, groundAngle, airAngle) /** Getter for the knockback vector on unit u. If the unit is not already tracked, returns (0, 0, 0). */ static function getVel(unit u) returns vec3 return unitNodes.has(u) ? unitNodes.get(u).del : ZERO3 /** Stop tracking unit u. If the unit is middair it will simply stop moving. If the unit is already untracked, nothing happens. */ static function forget(unit u) if unitNodes.has(u) destroy unitNodes.get(u) // Instance Variables private unit u private vec3 del private static function tickNearGround(Knockback3 knockback, vec3 newPos3, vec3 pos3, real velXySquared) if ( newPos3.toVec2().isTerrainWalkable() or (newPos3.z > HEIGHTMAP_PROVIDER.get(newPos3) and knockback.del.z > 0.) or (not pos3.toVec2().isTerrainWalkable()) ) knockback.u.setXY(newPos3) if knockback.del.z <= isAirborneThreshold knockback.del *= (1. - frictionCoefficientGround) if velXySquared > frictionFxThreshold * frictionFxThreshold * ANIMATION_PERIOD addEffect(frictionFxPath, pos3).destr() if USE_MOVE_SPEED_MODIFIERS knockback.u.setMoveSpeed(knockback.u.getDefaultMovespeed()) if USE_PROP_WINDOW_MODIFIERS knockback.u.setPropWindow(knockback.u.getDefaultPropWindow() * bj_DEGTORAD) else knockback.del = WALLHIT_TRANSFORM.apply(knockback.del) if knockback.del.z < elasticityThreshold * ANIMATION_PERIOD knockback.del.z = knockback.del.z*-1.*restitutionCoefficientGround if knockback.del.z > airborneThreshold * ANIMATION_PERIOD knockback.u.setFlyHeight(pos3.z + knockback.del.z, 0.) knockback.del.z = knockback.del.z - gravity * ANIMATION_PERIOD private static function tickAboveGround(Knockback3 knockback, vec3 newPos3, vec3 pos3) knockback.del.z -= gravity * ANIMATION_PERIOD let heightDifference = HEIGHTMAP_PROVIDER.get(newPos3) - HEIGHTMAP_PROVIDER.get(pos3) knockback.u..setFlyHeight(newPos3.z - heightDifference, 0.) ..setXY(newPos3) if USE_MOVE_SPEED_MODIFIERS knockback.u.setMoveSpeed(0.) if USE_PROP_WINDOW_MODIFIERS knockback.u.setPropWindow(angle(0.)) private static bool hitDestructable private static let destructableRect = Rect(-1 * DESTRUCTABLE_ENUM_SIZE, -1 * DESTRUCTABLE_ENUM_SIZE, DESTRUCTABLE_ENUM_SIZE, DESTRUCTABLE_ENUM_SIZE) private static function tickTryDestructable(Knockback3 knockback, vec3 newPos3) hitDestructable = false MoveRectTo(destructableRect, newPos3.x, newPos3.y) EnumDestructablesInRect(destructableRect, null) -> let des = GetEnumDestructable() if des.getLife() > 0. des.kill() hitDestructable = true if hitDestructable knockback.del *= restitutionCoefficientDestructable private static constant unitNodes = new HashMap() ondestroy unitNodes.remove(this.u) if USE_MOVE_SPEED_MODIFIERS this.u.setMoveSpeed(this.u.getDefaultMovespeed()) if USE_PROP_WINDOW_MODIFIERS this.u.setPropWindow(this.u.getDefaultPropWindow() * bj_DEGTORAD) private static let clock = CreateTimer() private static function tick() for knockback from staticItr() let pos3 = knockback.u.getPos3Fly() var newPos3 = pos3 + knockback.del if not newPos3.inPlayable() newPos3.x = pos3.x newPos3.y = pos3.y knockback.del = ZERO2.withZ(knockback.del.z) let velXySquared = knockback.del.toVec2().lengthSq() if pos3.z < isAirborneThreshold tickNearGround(knockback, newPos3, pos3, velXySquared) else tickAboveGround(knockback, newPos3, pos3) if velXySquared > destroyDestructableSpeedThreshold * ANIMATION_PERIOD and pos3.z < destroyDestructableHeightThreshold tickTryDestructable(knockback, newPos3) let isVelxyLow = velXySquared < (minimumSlideSpeed * minimumSlideSpeed) * ANIMATION_PERIOD let isVelzLow = knockback.del.z.abs() < -1. * elasticityThreshold * ANIMATION_PERIOD let isAirborne = pos3.z > isAirborneThreshold if isVelxyLow and isVelzLow and not isAirborne knockback.u.setFlyHeight(0., 0.) destroy knockback if size == 0 clock.pause()