MoveLib 3

by Ter13
Accessible. Simple. General.
ID:2860944
 
MoveLib3 implements an array of improvements to BYOND's native pixel movement utilities that will make working with coordinates and implementing common quality of life features much more accessible.

MoveLib3 implements the following behaviors:

* Collision reciprocity - Movers are given the final say in what they collide with, and are informed when they start or stop overlapping on-map atoms or existing within other atoms.

* Vector2 and coord data types - Working with coordinates requires much less boilerplate.

* Step() and Relocate() - force movables to make slides or jumps at any distance and restore their step_size once the movement is finished.

* Angular, vector2, coord, and directional forms for Move(), Step(), and Relocate() - Makes moving things over pixel distances much less of a pain.

* Trace() - a highly configurable collision detection function, which simulates a movement with options to ignore Cross()/Uncross() results and calls, as well as to disable Crossed()/Uncrossed()/Entered()/Exited()/Bump() calls. It can ignore a passed list of atoms, as well as return a /trace datum which allows movements to be stored and finalized after inspection.

* atom/movable/New() override allows passing /coord datums for pixel-perfect on-map initialization without extra arguments.

* bounds_edge() function which allows getting all objects just past the bounding area of an atom in a specified direction.

Check out the Documentation for more information.
Collision Reciprocity
  • atom/movable.collision_flags - One of:
        COLLISION_BASIC (0)
        COLLISION_ADVANCED (1)

      Marks this movable as using basic or advanced collision. Advanced collision causes collision reciprocity functions to be considered during movement.
  • atom/movable.canCross()
        atom/o - The object that we are about to cross
        allow - The value about to be returned by Cross()

      Called by atom.Cross() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.canUncross()
        atom/o - The object that we are about to uncross
        allow - The value about to be returned by Uncross()

      Called by atom.Uncross() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.canEnter()
        atom/o - The object that we are about to enter
        atom/oldloc - The location src is trying to enter from
        allow - The value about to be returned by Enter()

      Called by atom.Enter() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.canExit()
        atom/o - The object that we are about to exit
        atom/newloc - The location src is trying to exit to
        allow - The value about to be returned by Exit()

      Called by atom.Exit() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.Crossing()
        atom/o - The object that we just crossed

      Called by atom.Crossed() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.Uncrossing()
        atom/o - The object that we just uncrossed

      Called by atom.Uncrossed() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.Entering()
        atom/o - The object that we just entered

      Called by atom.Entered() when the movable has advanced collision turned on. See the style guide for more information.
  • atom/movable.Exiting()
        atom/o - The object that we just exited

      Called by atom.Exited() when the movable has advanced collision turned on. See the style guide for more information.


Style guide:

  Collision reciprocity really needs to be baked into the engine by default. Unfortunately, the best we can do is a pretty hacky workaround that allows us to integrate it ourselves. The reason that this is a problem, is because it's brittle. Anywhere we override Cross(), we can break collision reciprocity with that object without meaning to. That's because we have no way to access the return value of the call that preceded a supercall. This means that your Cross()/Uncross()/Enter()/Exit() calls must always supercall, and they must always pass their return value as an additional argument of the supercall. You should build your overrides with this in mind.

//This example breaks reciprocity:
turf
locked_door
var
lock
Cross(mob/m)
if(ismob(m) && m.hasKey(lock))
return 1
return ..()


//This example preserves reciprocity:
turf
locked_door
var
lock

Cross(mob/m,allowing)
if(isnull(allowing))
if(ismob(m) && m.hasKey(lock))
return ..(m,1)
return ..()


  Preserving reciprocity is important for the time being. If and when collision reciprocity is integrated into BYOND properly, this kind of pattern will become unnecessary. If you don't care for this pattern, you can just call the reciprocal yourself with a convenience macro:

//This example preserves reciprocity while breaking the supercall chain:
turf
locked_door
var
lock

Cross(mob/m)
if(ismob(m) && m.hasKey(lock))
return recipCross(m,1)
return ..()


Reciprocal macros:
  recipCross(mover,return_value)
  recipUncross(mover,return_value)
  recipEnter(mover,oldloc,return_value)
  recipExit(mover,newloc,return_value)
  recipCrossed(mover)
  recipUncrossed(mover)
  recipEntered(mover,oldloc)
  recipExited(mover,newloc)
atom/movable.New():

  atom/movable.New() can take a /coord as its first argument. Provided the atom has no location upon atom/movable/New()'s default behavior, it will be moved into place where the coordinate specifies. If you override New() on a subtype of movable, you will likely want to ensure that you do not cut off this default behavior.

Movement changes:

  Move() now takes several new forms:

    Move(vector2,Dir)
    Move(coord,Dir)

  Passing a coord datum will attempt to end the move with the bottom-left corner of the movable's bounding box at the location the coordinate references. This could either be a slide or a jump, depending on step_size.

  Passing a vector2 datum will attempt to end the move with the bottom-left corner of the movable's bounding box at the location on the movable's current z layer at the location the vector2 references. This could be either a slide or a jump, depending on step_size.

  In both cases, these are absolute coordinates, but they are 0-indexed by default instead of 1-indexed. The bound_x and bound_y values of the movable are taken into account when finding the destination.



  atom/movable.Step() has been added, which is shorthand for Move(), which forces a slide.

    Step(turf/NewLoc,[Dir,step_x,step_y]) - turf form
    Step(coord,[Dir]) - coord form
    Step(vector2,[Dir]) - vector2 form
    Step(direction,[Dir,dist=step_size]) - directional form
    Step(ang=0..360,[face=0,dist=step_siz e]) - angular form

  The various forms for Step() have a rigid structure. If you are using the turf form, you must not use any of the named arguments. Only the NewLoc, Dir,step_x, and step_y arguments are valid in this form.

  If you are using the coord or vector form, only the NewLoc and Dir argument are valid, where NewLoc is the coord or vector2 datum, and Dir is the forced facing direction of the mover following the movement. 0 by default will make the mob face whatever direction they appeared to have moved.

  If you are using the directional form, the direction of movement is the first argument, and the second positional argument, if present, is the forced facing direction. Otherwise this will be 0. You may use the dist named argument to pass a step distance in that direction, which is mover.step_size by default.

  If you are using the angular form, no positional arguments are valid, and the face and dist arguments have been provided instead.

  Note: Step() sets the mover's step_size to 1#INF to ensure that the movement will always be a slide. This can result in bugs if you call another movement function during a movement-derived hook, such as Cross()/Crossed(),Bump(),Enter()/Entered(), or their antonyms. It is best practice to avoid doing this. Once the Step() is resolved, step_size is restored provided it is still 1#INF. This means it is safe to change step_size within a movement-derived hook. If you need to access the mob's real step_size during a move_derived hook, step_size will be unreliable, and you should use __restore_step_size instead to gather that information.




  atom/movable.Relocate() has been added, which is shorthand for Move(), which forces a jump.

    Relocate(turf/ NewLoc,[Dir,step_x,step_y]) - turf form
    Relocate(coord,[Dir]) - coord form
    Relocate(vector2,[Dir]) - vector2 form
    Relocate(direction,[Dir,dist=step_siz e]) - directional form
    Relocate(ang=0..360,[face=0,dist=step _size]) - angular form

  Relocate() is similar to Step(), in that it takes all the same argument structure, but instead of forcing a slice, it forces a jump.

  Note: Step() sets the mover's step_size to 1 to ensure the movement will be a jump where possible. This can result in bugs if you call another movement function during a movement-derived hook. It is best practice to avoid doing this. Once the Step() is resolved, step_size is restored provided it is still 1. This means it is safe to change_step_size to any value but 1 within a movement-derived hook. If you want to change step_size to 1 permanently during a move derived hook, also set __restore_step_size to 1 to ensure it isn't changed back when a Jump() finishes moving the mover. Similarly, this makes step_size unreliable for the real movement speed of the mover. You may wish to store this information in your own variable, or you may have to read __restore_step_size instead.
/vector2 datum

  The /vector2 datum stores an x (float), y (float), variable, and represents an arbitrary point in 2d space. It provides basic arithmetic operations on those points, as well as some convenience functions for getting information from the world using a vector2.

operators supported:
  • + - * / Arithmetic operators return a new vector
  • += -= *= /= Assignment operators modify the left operand's x, y values
  • ~= ~! Equivalence operators check if x and y are the same on two vectors



vector2 procs
  • Magnitude() returns the length of a line segment between 0,0 and x,y
  • Normalize() returns a new vector with a segment length of 1, but along the same orientation as the source vector
  • Dot(vector2) returns the dot product of two vectors
  • Cross(vector2) returns the cross product of two vectors
  • Bounds(z,[w=1,h=1,x=0,y=0]) returns any objects within the specified bounding region. x,y args offset the position of the vector's x,y values. w,h sets the size of the box used to find overlapping objects in the world.
  • Locate(z) returns the turf at the vector's position on the specified z layer.
  • getDir(atom|vector2|coord) returns the manhattan direction between this vector and the center of the passed atom, or the vector or coord passed as a target.
  • getDirection(atom|vector2|coord) returns the smoothed direction between this vector and the center of the passed atom, or the vector or coord passed as a target. Direction is angular, and along a 45 degree arc segment offset by 22.5 degrees from 0.
  • getDist(atom|vector2|coord) returns the manhattan distance between this vector and the center of the passed atom or the vector or coord passed as the target. This assumes that we are always on the same Z layer as the target.
  • getDistance(atom|vector2|coord) returns real distance between this vector and the center of the passed atom or the vector or coord passed as the target. This assumes that we are always on the same Z layer as the target.
  • getAngle(atom|vector2|coord) returns the angle between this vector and the center of the passed atom or the vector or coord passed as the target. This assumes that we are always on the same Z layer as the target.
  • coord(z) creates a new coordinate using the vector's x and y values, and the passed z value.


isvector2() is a shorthand for istype(v,/vector2) that's included in the library.

You can get a vector2 from any atom by calling atom.vector2(anchor)

anchor may be one of:
  NORTH (1)
  SOUTH (2)
  EAST (4)
  WEST (8)
  NORTHEAST (5)
  NORTHWEST (9)
  SOUTHEAST (6)
  SOUTHWEST (10)
  CENTER (0)

The anchored position will result in a vector referencing one of the nine points along the left, center, right, top, middle, and bottom anchor points of the atom's bounding box.
/coord datum

  The coord datum is similar to the vector2 datum, but also contains a z coordinate. z coordinates ignore arithmetic, which is how they are distinct from a vector3.

operators supported:
  • + - * / Arithmetic operators return a new vector
  • += -= *= /= Assignment operators modify the left operand's x, y values
  • ~= ~! Equivalence operators check if x, y, and z are the same on two coords
  • [] indexing is used to pull coordinate conversions out of coords.


conversions:
  coord[1],coord["absolute_x"],coord["abs_x"]: x
  coord[2],coord["absolute_y"],coord["abs_y"]: y
  coord[3],coord["z"]: z
  coord["x"],coord["tile_x"]: round(x / TILE_WIDTH) + 1
  coord["y"],coord["tile_y"]: round(y / TILE_HEIGHT) + 1
  coord["step_x"]: x - round(x / TILE_WIDTH) * TILE_WIDTH
  coord["step_y"]: y - round(y / TILE_HEIGHT) * TILE_HEIGHT

coord procs
  • getClamped([x=0,y=0,w=1,h=1]) clamps this coord between world max coordinates for the given bounding box
  • Step() moves this coord along the distance that would be attempted by a movable's step with the same args
  • getStep() like step, but returns a new coord instead of modifying this one
  • Bounds([w=1,h=1,x=0,y=0]) returns any objects within the specified bounding region. x,y args offset the position of the vector's x,y values. w,h sets the size of the box used to find overlapping objects in the world.
  • Locate() returns the turf at the coord's position.
  • getDir(atom|vector2|coord) returns the manhattan direction between this coord and the center of the passed atom, or the vector or coord passed as a target.
  • getDirection(atom|vector2|coord) returns the smoothed direction between this coord and the center of the passed atom, or the vector or coord passed as a target. Direction is angular, and along a 45 degree arc segment offset by 22.5 degrees from 0.
  • getDist(atom|vector2|coord) returns the manhattan distance between this coord and the center of the passed atom or the vector or coord passed as the target. If the target is a vector, they are assumed to be on the same Z layer. Otherwise, if not on the same Z layer, distance is infinite.
  • getDistance(atom|vector2|coord) returns real distance between this vector and the center of the passed atom or the vector or coord passed as the target. If the target is a vector, they are assumed to be on the same Z layer. Otherwise, if not on the same Z layer, distance is infinite.
  • getAngle(atom|vector2|coord) returns the angle between this coord and the center of the passed atom or the vector or coord passed as the target. This assumes that we are always on the same Z layer as the target.
  • vector2() creates a new vector2 using the coord's x and y values.


iscoord() is a shorthand for istype(v,/coord) that's included in the library.

You can get a coord from any atom by calling atom.coord(anchor)

anchor may be one of:
  NORTH (1)
  SOUTH (2)
  EAST (4)
  WEST (8)
  NORTHEAST (5)
  NORTHWEST (9)
  SOUTHEAST (6)
  SOUTHWEST (10)
  CENTER (0)

The anchored position will result in a coord referencing one of the nine points along the left, center, right, top, middle, and bottom anchor points of the atom's bounding box.
atom/movable.Trace()

  Trace() is a handy little function that acts quite a lot like a Step() or a Relocate() call. But it can return a /trace datum with information about the movement, and can be passed a list of atoms it will ignore collision with, or it can be passed a series of flags that will customize how it functions.

Flags:
  TRACE_SLIDE (1) If on, the movement will act like Slide()
  TRACE_JUMP (2) If on, the movement will act like Relocate() --If both are off, it will just act like Move(), using step_size to determine jumping or sliding.

  TRACE_MOVE (4) If on, the movement will finalize immediately.

  TRACE_IGNORE_CROSS (8) Cross failures won't stop this movement, and atom.Cross()/Enter() won't be called by the mover.
  TRACE_IGNORE_UNCROSS (16) Uncross failures won't stop this movement, and atom.Uncross()/Exit() won't be called by the mover.

  TRACE_DISABLE_CROSSED (32) The mover won't call Crossed()/Entered() on finalization.
  TRACE_DISABLE_UNCROSSED (64) The mover won't call Uncrossed()/Exited() on finalization.
  TRACE_DISABLE_BUMP (128) The mover won't call Bump() on finalization.

  TRACE_RESULTS (256) If on, Trace() returns a /trace datum containing information about the attempted movement. If off, Trace() returns whatever Move() would have (the distance it's possible to move, or 0 on failure)

Trace() Forms:
  Trace(turf/ NewLoc,[Dir,step_x,step_y,flags=TRACE_MOVE,ignore=null]) - turf form
  Trace(coord,[Dir,flags=TRACE_MOVE,ignore=null]) - coord form
  Trace(vector2,[Dir,flags=TRACE_MOVE,ignore=null]) - vector2 form
  Trace(direction,[Dir,dist=step_size,flags=TRACE_M OVE,ignore=null]) - directional form
  Trace(ang=0..360,[face=0,dist=step_size,flags=TRA CE_MOVE,ignore=null]) - angular form


/trace datum
  Trace may return the number of pixels that it was possible to move, 0, or a /trace datum. The trace datum contains a list of variables that will provide information about the movement.

  atom/movable/mover - The mover that the trace was run on

  coord/old_loc - The mover's coords when the trace was started
  coord/new_loc - Where the mover attempted to trace a movement to
  coord/loc - The mover's coords when the trace completed or failed

  list/crossing - any atoms that would be crossed over during the trace
  list/uncrossing - any atoms that would be crossed out of during the trace
  atom/collider - the atom that caused the trace to fail

  result - what the traced movement would have returned (the number of pixels moved)

  Finalize() - This immediately places the mover at the final position of the trace, and calls the Crossed()/Uncrossed()/Entered()/Exited()/Bump() hooks for any crossing or uncrossing atoms in the correct sequence with the correct arguments, as though the movement were really happening. If the trace has already been finalized, nothing happens. As the trace already did the required calls to Cross()/Uncross()/Enter()/Exit() have already been simulated by the Trace(), there is no need to call them again here.