# Creating Custom Enemies

## The Basics

Once you have [created a custom profile](/gtfo-replay/viewer-guides/creating-a-custom-profile.md), you can now customise the enemies as you wish.

Open the datablock file `path_to_your_profile/datablocks/enemy/enemy.js` and remove all enemy definitions after "Unknown" such that you are left with just the following:

```javascript
const { EnemyDatablock } = await require("../../../vanilla/datablocks/enemy/enemy.js", "asl");
const { Identifier } = await require("../../../vanilla/parser/identifier.js", "asl");
const { BigFlyerModel } = await require("../../../vanilla/renderer/enemy/models/bigflyer.js", "asl");
const { FlyerModel } = await require("../../../vanilla/renderer/enemy/models/flyer.js", "asl");
const { HumanoidEnemyModel } = await require("../../../vanilla/renderer/enemy/models/humanoid.js", "asl");
const { SquidModel } = await require("../../../vanilla/renderer/enemy/models/squid.js", "asl");
EnemyDatablock.clear();
const shooterScale = 0.8;
EnemyDatablock.set(Identifier.create("Enemy", 0), {
  name: "Unknown",
  maxHealth: Infinity
});
```

{% hint style="info" %}
`shooterScale` is provided as shooters in-game are slightly smaller than strikers. This is a useful constant to use when scaling shooter models.
{% endhint %}

To create your first custom enemy, open a replay of the rundown you are making the profile for and enable "Show Enemy Info" in the viewer settings.&#x20;

Locate an enemy that you wish to add to the datablocks, enemy info should show its ID:

<figure><img src="/files/wj2i3uKrMEemjVjLfYUg" alt="" width="215"><figcaption><p>Enemy with ID of 24</p></figcaption></figure>

Back in our script we can add this enemy using the following:

<pre class="language-javascript"><code class="lang-javascript"><strong>// Set the ID in the Identifier to 24
</strong><strong>EnemyDatablock.set(Identifier.create("Enemy", 24), {
</strong>  name: "Bob"
});
</code></pre>

{% hint style="info" %}
You do not need to provide a `maxHealth` value, this is there in the vanilla version for backwards compatability with old replays. Newer versions store the health value of an enemy within the replay.
{% endhint %}

<figure><img src="/files/vVsdTFDjvLdHhVtTqtNf" alt="" width="335"><figcaption><p>Enemy after defining in DB</p></figcaption></figure>

The `EnemyDatablock` provides additional customisation for the model of the enemy as well via the `model` property. This accepts a function which should return the model that the enemy should use, providing the enemy spawn information.

In the basic case of a stickfigure, you do not need to worry about any of this and can simply use the built-in `HumanoidEnemyModel` class:

```javascript
EnemyDatablock.set(Identifier.create("Enemy", 24), {
  name: "Bob",
  model: (wrapper, enemy) => {
    // wrapper - THREE JS wrapper used to encapsulte your model and
    //           render things such as Bio Tracker Pings and Enemy Info.
    //           It also provides information such as the current animHandle
    //           and enemy DB information. Many model implementations will use
    //           it, but it is not necessary.
    // enemy - Spawn information about the enemy such as type, hp etc...
    const model = new HumanoidEnemyModel(wrapper);
    return model;
  }
});
```

The `HumanoidEnemyModel` class provides a `applySettings` method which lets you alter how the stick figure looks:

```javascript
EnemyDatablock.set(Identifier.create("Enemy", 24), {
  name: "Bob",
  model: (wrapper, enemy) => {
    const model = new HumanoidEnemyModel(wrapper);
    model.applySettings({
      scale: enemy.scale * shooterScale,
      armScale: {
        x: 0.2,
        y: 0.2,
        z: 0.2
      },
      headScale: {
        x: 0,
        y: 0,
        z: 0
      },
      legScale: {
        x: 1.1,
        y: 1.1,
        z: 1.1
      }
    });
    return model;
  }
});
```

<figure><img src="/files/e0vs5oGnhi9p2gHk9q96" alt="" width="206"><figcaption></figcaption></figure>

{% hint style="warning" %}
Note that we pass `enemy.scale` into the `scale` option for `applySettings`.&#x20;

This is because the game scales enemies dynamically, and the recorder will record the scale the game has provided and store it in `enemy.scale`.&#x20;

We can then apply this scale to our model to better reflect the scale in-game.
{% endhint %}

{% hint style="info" %}
For more information on what can be customised, refer to the docs [here](/gtfo-replay/viewer-guides/datablock-documentation.md).
{% endhint %}

## Custom Model

{% hint style="info" %}
This requires good knowledge of JavaScript and how ASL works, I recommend you read the [prerequisite knowledge](/gtfo-replay/viewer-guides/prerequisite-knowledge.md#async-script-loader-asl) page before continuing.
{% endhint %}

Sometimes you might want to stray away from just stick figures and instead provide your own model.&#x20;

This section will provide an example in the context of creating a model for the "Bloody Mass" enemies from [Duo Trials](https://thunderstore.io/c/gtfo/p/ProjectZaero/Duo_Trials/):

<figure><img src="/files/8PtR6awyMZisRe3wRkHe" alt="" width="230"><figcaption><p>Bloody Mass from Duo Trials</p></figcaption></figure>

This can be achieved by creating a custom enemy model class. First lets create a new file called `bloodymass.js` in the root of your profile:

<figure><img src="/files/WjbYZ1c33aSfrCVfgUdL" alt=""><figcaption></figcaption></figure>

### Extending the Existing Stick Figures

Lets open this file in your favourite text editor to start creating our custom model. I want this model to extend the existing stick figure model since I wish to use the underlying bone structure and animations, however you can choose to make the model completely from scratch as well.

First lets import `HumanoidEnemyModel` from the vanilla profile:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");
```

We can then export a new class extending it:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
}
```

When an enemy model is generated, they are all passed an optional `EnemyModelWrapper` object. This wrapper manages the model and contains information such as `EnemyDatablock` data for the type of enemy and its animation handle.&#x20;

Furthermore, you can assign a target object as the spot where the bio tracker ping icon will render. `HumanoidEnemyModel` uses this to place the bio ping on the hip bone of the enemy:

```javascript
// @Class HumanoidEnemyModel
constructor(wrapper) {
    super();  
    this.wrapper = wrapper;
    this.wrapper.tagTarget = this.visual.joints.spine1;
    // ...
}
```

For this reason, we need to pass the wrapper object to it in our class inheritance:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
        super(wrapper);
    }
}
```

We now have our extended model, lets use it for our enemy!&#x20;

{% hint style="info" %}
If you have not setup a custom datablock for an enemy, please do so now [here](/gtfo-replay/viewer-guides/creating-custom-enemies.md#the-basics).
{% endhint %}

Back in our enemy datablock definition, we can now import our new model and use it:

```javascript
// ** path_to_profile/datablocks/enemy.js **

// Import our new model
const { BloodMassModel } = await require("../../bloodymass.js", "asl");

// Assign the model to our datablock
EnemyDatablock.set(Identifier.create("Enemy", 24), {
  name: "Blood Mass",
  model: (wrapper, enemy) => {
    const model = new BloodMassModel(wrapper);
    return model;
  }
});
```

<figure><img src="/files/MvxmkgrlsV4DriqjxULu" alt="" width="177"><figcaption><p>Result in Viewer</p></figcaption></figure>

### Creating our own renderer

Great! We have now successfully extended the humanoid model, it currently will act and behave the same but we can change that.

Back in our `bloodymass.js` script we can override the render function for the model:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
        super(wrapper);
    }
	
    render(dt, time, enemy, anim) {
    	
    }
};
```

Back in the viewer, your enemy should now dissappear as we are no longer rendering it (our render function does nothing)

Instead you should notice that all your enemies our at spawn (position 0,0):

<figure><img src="/files/QbWrCGnqxIMf8cCF1a7E" alt="" width="375"><figcaption><p>Note that due to culling, you need to keep the original enemy locations in camera view to see this.</p></figcaption></figure>

Since we are not updating any of the objects during rendering, the root parent object also does not move. We can change this using the following:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
    	super(wrapper);
    }
	
    render(dt, time, enemy, anim) {
        this.root.position.copy(enemy.position);
    }
};
```

<figure><img src="/files/Ti433axSmOLSzULQPFTQ" alt="" width="238"><figcaption><p>By updating the root, the enemy now renders in the right location</p></figcaption></figure>

Instead of doing this, however, we are going to use the animations provided by `HumanoidEnemyModel` . We can do this by calling its `animate` function:

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
    	super(wrapper);
    }
	
    render(dt, time, enemy, anim) {
        this.animate(dt, time, enemy, anim);
    }
};
```

The `animate` function does more than just move the root of the model, it also animates the model's bones for the enemy limbs.

Next lets add an early return to skip rendering if the enemy is no longer visible (This accounts for culling):

```javascript
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
    	super(wrapper);
    }
	
    render(dt, time, enemy, anim) {
        if (!this.isVisible()) return;
    
        this.animate(dt, time, enemy, anim);
    }
};
```

### Adding the Spikes!

Now lets start creating those spikes from the original model. To do so, I'm going to reuse the tentacle models provided which were originally used for flyer tendrils.

First lets import the required dependencies:

```javascript
const { Mesh, MeshPhongMaterial, Vector3, Euler } = await require("three", "esm");
const { loadGLTFGeometry } = await require("../vanilla/library/modelloader.js", "asl");

const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
    // ...
};
```

From `three` we need:

* [`Mesh`](https://threejs.org/docs/#api/en/objects/Mesh) - An object used for rendering polygons
* [`MeshPhongMaterial`](https://threejs.org/docs/?q=meshpho#api/en/materials/MeshPhongMaterial) - A built in material&#x20;
* [`Vector3` ](https://threejs.org/docs/?q=Vector3#api/en/math/Vector3)- Vector3 math
* [`Euler` ](https://threejs.org/docs/?q=eul#api/en/math/Euler)- Euler angle rotation math

We also need the `loadGLTFGeometry` function from vanilla which is used to load [BufferGeometries](https://threejs.org/docs/#api/en/core/BufferGeometry) of models from GLTF files.

Now we can load the spike model and attach it to a bone on our animated model:

```javascript
const { Mesh, MeshPhongMaterial, Vector3, Euler } = await require("three", "esm");
const { loadGLTFGeometry } = await require("../vanilla/library/modelloader.js", "asl");
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");
exports.BloodMassModel = class extends HumanoidEnemyModel {
    constructor(wrapper) {
        super(wrapper);
        
        // Create our material
        const material = new MeshPhongMaterial({ color: 0xff0000 });
        
        // Load our model
        loadGLTFGeometry("../js3party/models/spike.glb").then(model => {
        
            // Once the model is loaded, create a mesh for it, assigning our material
            this.tendril = new Mesh(model, material);
            
            // Add the tendril to the `hip` joint of the visual skeleton
            this.visual.joints.hip.add(this.tendril);
        
        // Catch any errors during the load process
        }).catch(error => { throw module.error(error, "Failed to load spike model.") });
    }

    render(dt, time, enemy, anim) {
        if (!this.isVisible()) return;
        
        this.animate(dt, time, enemy, anim);
    }
};
```

<figure><img src="/files/01gQXtKTkM24QVQCyK2m" alt="" width="130"><figcaption><p>Rendered Spike</p></figcaption></figure>

We have now successfully added a spike to the `hip` bone of our stick figure!

### Animating the Spikes

Now lets say we want to have the spike sway a little. We can achieve this in our `render` function by applying a `sin` and `cos` wave to our spikes rotation.

In our render function, we are provided with a `time` variable which represents the current time in the replay in milliseconds. Animations are much easier to handle in seconds, so we will first convert the time into seconds:

```javascript
render(dt, time, enemy, anim) {
    if (!this.isVisible()) return;
    
    this.animate(dt, time, enemy, anim);
    
    // Convert from milliseconds to seconds
    time = time / 1000;
}
```

{% hint style="info" %}
Note that we do the conversion after calling `animate` as that function expects the time to still be in milliseconds.
{% endhint %}

We can now set the rotation of the tendril based on a `sin` or `cos` wave in respect to time:

```javascript
render(dt, time, enemy, anim) {
    if (!this.isVisible()) return;
    
    this.animate(dt, time, enemy, anim);

    // Convert from milliseconds to seconds
    time = time / 1000;

    // If our tendril does not exist then it still has not loaded yet - early return
    if (this.tendril === undefined) return;

    // Apply sin and cos rotations to our spike
    // - angles are in radians!
    this.tendril.rotation.set(
        Math.sin(time), 
        Math.cos(time), 
        Math.sin(time)
    );
}
```

With this, we should be able to see our spike sway accordingly:

<figure><img src="/files/c1oLk3wQaOfhrYvCMByv" alt="" width="375"><figcaption><p>Spike swaying</p></figcaption></figure>

We can adjust the math equations to produce the sort of animation we want - Feel free to mess around with it.

By repeating the above to produce more spikes we can generate our bloody mass model:

<figure><img src="/files/fSMFvQUxBaBPaHeV068a" alt="" width="296"><figcaption><p>Full bloody mass model</p></figcaption></figure>

The full source code can be found below:

```javascript
const { Mesh, MeshPhongMaterial, Vector3, Euler } = await require("three", "esm");
const { loadGLTFGeometry } = await require("../vanilla/library/modelloader.js", "asl");
const { HumanoidEnemyModel } = await require("../vanilla/renderer/enemy/models/humanoid.js", "asl");

exports.BloodMassModel = class extends HumanoidEnemyModel {
  constructor(wrapper) {
    super(wrapper);
    this.tendrilMaterial = new MeshPhongMaterial({ color: 0xff0000 });

    this.tendrils = [];
    this.originalRotations = [];
    this.randomOffsets = [];
    
    loadGLTFGeometry("../js3party/models/spike.glb").then(model => {
      const tendrilsPerPart = 8;

      const partsToAttachTo = ["hip", "spine1", "spine2"];
      for (let j = 0; j < partsToAttachTo.length; j++) {
        const jointname = partsToAttachTo[j];
        const randOffset = Math.random();
        for (let i = 0; i < tendrilsPerPart; i++) {
          const rotAngle = i * (360/tendrilsPerPart);
          const tendril = new Mesh(model, this.tendrilMaterial);
          this.visual.joints[jointname].add(tendril);
          tendril.scale.set(0.1, 0.3, 0.1);
          tendril.rotation.set(Math.deg2rad * 60, Math.deg2rad * rotAngle + randOffset, 0, "YXZ");
          const fwd = new Vector3(0, 0.08, 0);
          tendril.position.add(fwd.applyQuaternion(tendril.quaternion));
          tendril.position.add(new Vector3(0, (j * 0.15), 0));

          this.tendrils.push(tendril);
          this.originalRotations.push(tendril.rotation.clone());
          this.randomOffsets.push(Math.random());
        }

        // to have them point downwards
        for (let i = 0; i < tendrilsPerPart; i++) {
          const rotAngle = i * (360/tendrilsPerPart);
          const tendril = new Mesh(model, this.tendrilMaterial);
          this.visual.joints[jointname].add(tendril);
          tendril.scale.set(0.1, 0.3, 0.1);
          tendril.rotation.set(Math.deg2rad * 110, Math.deg2rad * rotAngle + randOffset, 0, "YXZ");
          const fwd = new Vector3(0, 0.08, 0);
          tendril.position.add(fwd.applyQuaternion(tendril.quaternion));
          tendril.position.add(new Vector3(0, (j * 0.15), 0));

          this.tendrils.push(tendril);
          this.originalRotations.push(tendril.rotation.clone());
          this.randomOffsets.push(Math.random());
        }

        const upspikeTendril = new Mesh(model, this.tendrilMaterial);
        this.visual.joints[jointname].add(upspikeTendril);
        upspikeTendril.scale.set(0.1, 0.3, 0.1);
        upspikeTendril.position.add(new Vector3(0, (j * 0.15) + 0.08, 0));
        this.tendrils.push(upspikeTendril);
        this.originalRotations.push(upspikeTendril.rotation.clone());
        this.randomOffsets.push(Math.random());

        const downspikeTendril = new Mesh(model, this.tendrilMaterial);
        this.visual.joints[jointname].add(downspikeTendril);
        downspikeTendril.scale.set(0.1, 0.3, 0.1);
        downspikeTendril.position.add(new Vector3(0, (j * 0.15) + 0.08, 0));
        downspikeTendril.rotation.set(Math.deg2rad * 180, 0, 0)
        this.tendrils.push(downspikeTendril);
        this.originalRotations.push(downspikeTendril.rotation.clone());
        this.randomOffsets.push(Math.random());
      }

    });

    // for gc prevention
    this.temp = new Euler();

  }

  applySettings(settings) {
    super.applySettings(settings);
	
    if (this.settings?.color !== undefined) this.tendrilMaterial.color.set(this.settings.color);
  }

  render(dt, time, enemy, anim) {
    if (!this.isVisible()) return;
    
    // speed up the animation while enemy is asleep
    if (anim.state === "Hibernate") time *= 0.2;
    
    this.animate(dt, time, enemy, anim); // animate the underlying stick figure
    
    // small amounts of wriggling to the tendrils
    for (let i = 0; i < this.tendrils.length; ++i) {
      const tendril = this.tendrils[i];
      const rot = this.originalRotations[i];
      const offset = this.randomOffsets[i];
      this.temp.copy(rot);
      this.temp.x += 5 * Math.sin((time * offset + offset * 2000) / 1000) * Math.deg2rad;
      this.temp.y += 10 * Math.sin((time * offset + offset * 4000) / 1000) * Math.deg2rad;
      this.temp.z += 20 * Math.sin((time * offset + offset * 3000) / 1000) * Math.deg2rad;
      tendril.rotation.copy(this.temp);
    }

  }
};
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://randomuserhi.gitbook.io/gtfo-replay/viewer-guides/creating-custom-enemies.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
