diff --git a/app/lib/world/vector.coffee b/app/lib/world/vector.coffee
index 0b826b6c0..913d2b66e 100644
--- a/app/lib/world/vector.coffee
+++ b/app/lib/world/vector.coffee
@@ -5,10 +5,14 @@ class Vector
   for name in ['add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate']
     do (name) ->
       Vector[name] = (a, b, useZ) ->
-        a.copy()[name](b, useZ)
+        a.copy()["#{name}Self"](b, useZ)
+  for name in ['magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared']
+    do (name) ->
+      Vector[name] = (a, b, useZ) ->
+        a[name](b, useZ)
 
   isVector: true
-  apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'rotate']
+  apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'rotate', 'add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate']
 
   constructor: (x=0, y=0, z=0) ->
     return new Vector x, y, z unless @ instanceof Vector
@@ -17,6 +21,74 @@ class Vector
   copy: ->
     new Vector(@x, @y, @z)
 
+
+  # Mutating methods:
+
+  normalizeSelf: (useZ) ->
+    m = @magnitude useZ
+    @divideSelf m, useZ if m > 0
+    @
+
+  normalize: (useZ) ->
+    # Hack to detect when we are in player code so we can avoid mutation
+    (if @__aetherAPIValue? then @copy() else @).normalizeSelf(useZ)
+
+  limitSelf: (max) ->
+    if @magnitude() > max
+      @normalizeSelf()
+      @multiplySelf(max)
+    else
+      @
+
+  limit: (useZ) ->
+    (if @__aetherAPIValue? then @copy() else @).limitSelf(useZ)
+
+  subtractSelf: (other, useZ) ->
+    @x -= other.x
+    @y -= other.y
+    @z -= other.z if useZ
+    @
+
+  subtract: (other, useZ) ->
+    (if @__aetherAPIValue? then @copy() else @).subtractSelf(other, useZ)
+
+  addSelf: (other, useZ) ->
+    @x += other.x
+    @y += other.y
+    @z += other.z if useZ
+    @
+
+  add: (other, useZ) ->
+    (if @__aetherAPIValue? then @copy() else @).addSelf(other, useZ)
+
+  divideSelf: (n, useZ) ->
+    [@x, @y] = [@x / n, @y / n]
+    @z = @z / n if useZ
+    @
+
+  divide: (n, useZ) ->
+    (if @__aetherAPIValue? then @copy() else @).divideSelf(n, useZ)
+
+  multiplySelf: (n, useZ) ->
+    [@x, @y] = [@x * n, @y * n]
+    @z = @z * n if useZ
+    @
+
+  multiply: (n, useZ) ->
+    (if @__aetherAPIValue? then @copy() else @).multiplySelf(n, useZ)
+
+  # Rotate it around the origin
+  # If we ever want to make this also use z: https://en.wikipedia.org/wiki/Axes_conventions
+  rotateSelf: (theta) ->
+    return @ unless theta
+    [@x, @y] = [Math.cos(theta) * @x - Math.sin(theta) * @y, Math.sin(theta) * @x + Math.cos(theta) * @y]
+    @
+
+  rotate: (theta) ->
+    (if @__aetherAPIValue? then @copy() else @).rotateSelf(theta)
+
+  # Non-mutating methods:
+
   magnitude: (useZ) ->
     sum = @x * @x + @y * @y
     sum += @z * @z if useZ
@@ -27,18 +99,6 @@ class Vector
     sum += @z * @z if useZ
     sum
 
-  normalize: (useZ) ->
-    m = @magnitude useZ
-    @divide m, useZ if m > 0
-    @
-
-  limit: (max) ->
-    if @magnitude() > max
-      @normalize()
-      return @multiply(max)
-    else
-      @
-
   heading: ->
     -1 * Math.atan2(-1 * @y, @x)
 
@@ -60,28 +120,6 @@ class Vector
       sum += dz * dz
     sum
 
-  subtract: (other, useZ) ->
-    @x -= other.x
-    @y -= other.y
-    @z -= other.z if useZ
-    @
-
-  add: (other, useZ) ->
-    @x += other.x
-    @y += other.y
-    @z += other.z if useZ
-    @
-
-  divide: (n, useZ) ->
-    [@x, @y] = [@x / n, @y / n]
-    @z = @z / n if useZ
-    @
-
-  multiply: (n, useZ) ->
-    [@x, @y] = [@x * n, @y * n]
-    @z = @z * n if useZ
-    @
-
   dot: (other, useZ) ->
     sum = @x * other.x + @y * other.y
     sum += @z + other.z if useZ
@@ -89,7 +127,7 @@ class Vector
 
   # Not the strict projection, the other isn't converted to a unit vector first.
   projectOnto: (other, useZ) ->
-    other.copy().multiply(@dot(other, useZ), useZ)
+    other.copy().multiplySelf(@dot(other, useZ), useZ)
 
   isZero: (useZ) ->
     result = @x is 0 and @y is 0
@@ -101,13 +139,6 @@ class Vector
     result = result and @z is other.z if useZ
     result
 
-  # Rotate it around the origin
-  # If we ever want to make this also use z: https://en.wikipedia.org/wiki/Axes_conventions
-  rotate: (theta) ->
-    return @ unless theta
-    [@x, @y] = [Math.cos(theta) * @x - Math.sin(theta) * @y, Math.sin(theta) * @x + Math.cos(theta) * @y]
-    @
-
   invalid: () ->
     return (@x is Infinity) || isNaN(@x) || @y is Infinity || isNaN(@y) || @z is Infinity || isNaN(@z)
 
diff --git a/test/app/lib/world/vector.spec.coffee b/test/app/lib/world/vector.spec.coffee
index ac5d017d1..b12c3f0e9 100644
--- a/test/app/lib/world/vector.spec.coffee
+++ b/test/app/lib/world/vector.spec.coffee
@@ -27,3 +27,57 @@ describe 'Vector', ->
     v2 = v.copy()
     v2.rotate -0.0000001 * Math.PI
     expect(v.distance v2).toBeCloseTo 0
+
+  it 'has class methods equivalent to the instance methods', ->
+    expectEquivalentMethods = (method, arg) ->
+      v = new Vector 7, 7
+      classResult = Vector[method](v, arg)
+      instanceResult = v[method](arg)
+      expect(classResult).toEqual instanceResult
+
+    expectEquivalentMethods 'add', new Vector 1, 1
+    expectEquivalentMethods 'subtract', new Vector 3, 3
+    expectEquivalentMethods 'multiply', 4
+    expectEquivalentMethods 'divide', 2
+    expectEquivalentMethods 'limit', 3
+    expectEquivalentMethods 'normalize'
+    expectEquivalentMethods 'rotate', 0.3
+    expectEquivalentMethods 'magnitude'
+    expectEquivalentMethods 'heading'
+    expectEquivalentMethods 'distance', new Vector 2, 2
+    expectEquivalentMethods 'distanceSquared', new Vector 4, 4
+    expectEquivalentMethods 'dot', new Vector 3, 3
+    expectEquivalentMethods 'equals', new Vector 7, 7
+    expectEquivalentMethods 'copy'
+
+  it "doesn't mutate when in player code", ->
+    expectNoMutation = (fn) ->
+      v = new Vector 5, 5
+      # player code detection hack depends on this property being != null
+      v.__aetherAPIValue = {}
+      v2 = fn v
+      expect(v.x).toEqual 5
+      expect(v).not.toBe v2
+
+    expectNoMutation (v) -> v.normalize()
+    expectNoMutation (v) -> v.limit 2
+    expectNoMutation (v) -> v.subtract new Vector 2, 2
+    expectNoMutation (v) -> v.add new Vector 2, 2
+    expectNoMutation (v) -> v.divide 2
+    expectNoMutation (v) -> v.multiply 2
+    expectNoMutation (v) -> v.rotate 0.5
+
+  it 'mutates when not in player code', ->
+    expectMutation = (fn) ->
+      v = new Vector 5, 5
+      v2 = fn v
+      expect(v.x).not.toEqual 5
+      expect(v).toBe v2
+
+    expectMutation (v) -> v.normalize()
+    expectMutation (v) -> v.limit 2
+    expectMutation (v) -> v.subtract new Vector 2, 2
+    expectMutation (v) -> v.add new Vector 2, 2
+    expectMutation (v) -> v.divide 2
+    expectMutation (v) -> v.multiply 2
+    expectMutation (v) -> v.rotate 0.5