Creating new monsters is much more involved than most other content and consequently knowing the basics of creating content will go a long ways. Getting started we’ll want to create a new folder inside of the mods/mymod/monster/ directory and in that new subfolder create a new .ce file so it looks like mods/mymod/monster/gold_slime/gold_slime.ce. Even though monsters are more complex they still have their own content template to help simplify the creation.
The Monster
template has several parameters which you can see below.
Monster
(name, *, tuning=None, isPersistent=False, weight=0, isParagon=False, layer=Layer.Active, placementType=MonsterPlacementType.OnGround, biomes=[], spawnOffset=Vector())¶Sets up the content entity data of a Monster. The majority of this data can come from the provided tuning data.
Parameters: |
|
---|
The content file contains the standard components such as hasPhysics
and animations
.
In addition, we have to provide information and logic for the monster’s attacks as well as the rest of the monster’s AI.
New monster attacks can be added by through the template’s Monster.attack()
.
This sets up a core.template.monster.MonsterAttack
with the provided parameters.
Afterwards MonsterAttack.customize
class decorator can be used to finish creating the attack.
The parameters provided to Monster.attack()
are set as class attributes on the customized class.
Most of these are to change default class core.monster.MonsterMeleeAttack
attributes that most attacks derive from.
With this customized class you can override any functions that core.monster.MonsterMeleeAttack
has to provide your own custom logic.
In the example below the BasicOrtomiAttack
overrides core.monster.MonsterMeleeAttack.perform()
and handleTimeStep()
.
Note
Whenever using callbacks for delayed events be sure to ensure the monster is still alive using core.monster.MonsterMeleeAttack.shouldStopAttack()
.
basicAttack = monster.attack(
hitFrames = attack.indices(3, 4, 5, 6),
power = MonsterTuning.ORTOMI.MELEE_POWER,
powerLevel = MonsterTuning.ORTOMI.MELEE_POWER_LEVEL,
detectionArea = Rect(0, -3, 32, 24),
cooldownTime = 700,
recoverTime = 900,
sound = "mods/core/audio/sfx/monster/ortomi_bite.ogg",
soundDelay = 300
)
@basicAttack.customize
class BasicOrtomiAttack(MonsterMeleeAttack):
def perform(self, monster):
MonsterMeleeAttack.perform(self, monster)
self.jumped = False
def handleTimeStep(self, physics, frameTime):
isFinished = MonsterMeleeAttack.handleTimeStep(self, physics, frameTime)
if not self.jumped and self.monster.entity.animation.getPlayTime() >= 600:
direction = -1 if self.monster.entity.render.getFlipX() else 1
force = Vector(5.0 * direction, -1.5)
physics.applyForce(force)
self.jumped = True
return isFinished
Below are the full details of the core.monster.MonsterMeleeAttack
class.
MonsterMeleeAttack
¶hitFrames = []
power
¶Power of the attack. Defaults to 0.
powerLevel
¶Amount of power to add multiplied by the monster’s level. Defaults to 0.
statusEffect
¶The status effect to apply to any hit targets. Defaults to None.
attackType
¶Defaults to AttackType.Melee.
damageType
¶Defaults to DamageType.Physical.
detectionArea
¶The area around the monster that this attack is considered to be in range. Defaults to Rect().
cooldownTime
¶Amount of time (ms) elapsed after for this attack to be performed again. Defaults to 0.
recoverTime
¶Amount of time (ms) the monster requires to recover after performing the attack. Defaults to 0.
animation
¶Defaults to “attack”.
sound
¶Sound to play when performing the attack. Defaults to “”.
soundDelay
¶Amount of time (ms) to delay playing the sound. Defaults to 0.
isPhysical
¶Defaults to True.
getPower
(level)¶Calculates the final power of the attack.
Parameters: | level (int) – The current level of the monster performing the attack. |
---|---|
Returns: | The power of the attack. |
Return type: | int |
update
(frameTime)¶Called every frame to update the attack. This is typically used to update timers and the likes.
Parameters: | frameTime (int) – The amount of time (ms) elapsed since the last update. |
---|
shouldPerform
(monster)¶Determines whether or not this attack should be performed. Default logic checks to see if any enemies are within range.
Parameters: | monster (BaseMonster) – The instance of the monster that would be performing the attack. |
---|---|
Returns: | Whether this attack should be performed or not. |
Return type: | bool |
inRange
(monster)¶Checks if there are any enemies within the defined range of this attack.
Parameters: | monster (BaseMonster) – The instance of the monster that would be performing the attack. |
---|---|
Returns: | Whether enemies are within range or not. |
Return type: | bool |
perform
(monster, animation='')¶Called when the attack is being performed by the monster.
Parameters: |
|
---|
updateAttackTime
(physics, frameTime)¶Updates the amount of time the attack has been being performed and will play the attack’s sound effect after the specified sound delay.
Parameters: |
|
---|
handleTimeStep
(physics, frameTime)¶Called every physics step at a fixed time and determining when the attack is complete.
Parameters: |
|
---|---|
Returns: | Whether the attack animation is complete or not. |
Return type: | bool |
handleIsAlive
(entity, wasAlive, isAlive)¶Used to watch is the monster’s alive state changes. If so then it disconnects the timestep listener allowing the attack to discontinue.
Parameters: |
|
---|
checkHit
()¶Attempts to hit any enemies within the attack range.
Returns: | The enemies that were hit by the attack. |
---|---|
Return type: | list of Entity |
handleRecoverTimeStep
(physics, frameTime)¶Responsible for updating the recovery timer while the monster is recovering from performing this attack. It automatically stops once the recovery timer has expired.
Parameters: |
|
---|
shouldStopAttack
()¶Determines if this attack should be discontinued based off of the monsters current state. This should be called immediately for any callbacks. Especially for those that have some side-effect such as creating an entity.
Returns: | Whether the monster should stop the attack or not. |
---|---|
Return type: | bool |
onFinish
()¶Called when the attack has finished being performed.
cleanup
()¶Called when the attack has fully concluded including recovery.
All of the logic that drives the monster behavior lives within the monster core.
This monster core is a python instance that derives from core.monster.BaseMonster
which is stored inside of the MonsterComponent at entity.monster.core
.
There is a great deal of default base logic that can help simplify the creation of more basic monsters.
Even with more simple monsters you will often want to change its behavior some.
The monsters’ logic is driven by a state machine. At any given moment a monster can only be doing one thing.
While in one state that state can determine that the monster should switch to another state.
Such as when the monster is roaming about and spots a player it can switch from the roaming state to the hostile state.
When changing to a state through BaseMonster.changeState()
that state’s initializer is called. Afterwards, every frame the state updater is called while updating the monster until the monster changes state once more.
Both state initializers and handlers are split between daytime and nighttime.
The current state initializers can be found in BaseMonster.initializers
and the handlers can be found in BaseMonster.handlers
.
When the time of day changes the current set of initializers and handlers are swapped out for the others.
These are all in BaseMonster.dayInitializers
, BaseMonster.dayHandlers
, BaseMonster.nightInitializers
and BaseMonster.nightHandlers
.
After creation the initializers and handlers can be changed by using BaseMonster.updateInitializer()
and BaseMonster.updateHandler()
respectively.
All initializers and handlers are expected to have the following signatures.
BaseMonster
¶__init__
(entity, dayInitializers=None, dayHandlers=None, nightInitializers=None, nightHandlers=None)¶setup
()¶Called after the monster has been fully initialized and added to the world. Allows for additional setup after creation.
changeState
(state)¶Called when the monster’s state is being changed. State changes are disallowed when the monster is in MonsterState.Death.
update
(frameTime)¶Called every frame updating the monster. This includes updating the monster’s actions checking to see if they should be performed.
Parameters: | frameTime (int) – The amount of time (ms) elapsed since the last update. |
---|
updateInitializer
(state, callback)¶Changes the provided state’s initializer to the provided callback. This changes occurs for both day and night initializers.
Parameters: |
|
---|
updateHandler
(state, callback)¶Changes the provided state’s handler to the provided callback. This changes occurs for both day and night handlers.
Parameters: |
|
---|
attachToSpawner
(spawner)¶Attaches the monster to the specified spawner. This results in the monster becoming hostile towards any players that attack the spawner.
Parameters: | spawner (Entity) – The entity of the spawner to attach the monster to. |
---|
guardPoint
(point)¶Changes the monster’s roaming state to patrol the specified area not straying too far from it.
Parameters: | point (Vector) – The position the monster should guard. |
---|
guardSpawner
(spawner)¶Changes the monster’s roaming state to patrol around the spawner and not stray too far from it.
Parameters: | spawner (Entity) – The spawner the monster should guard. |
---|
performAction
(action)¶Sets the provided action as the action the monster is currently performing and changes the monster’s state to MonsterState.Action
.
Parameters: | action – The action the monster should perform. |
---|
attemptAction
()¶All available actions are checked to see if any should be performed. If any are found then the action is performed.
surveyArea
()¶Checks the nearby area using detectionArea
to see if there are any enemies nearby that the monster should become hostile towards. If an enemy is found the the monster’s state is changed to MonsterState.Hostile
.
getLifePercentage
()¶Retrieves the percentage of health the monster is at.
Returns: | The health percentage ranging from 0-100. |
---|---|
Return type: | int |
addAffix
(affixKey)¶Adds the affix belonging to the given key to the monster.
Parameters: | affixKey (str) – The key that the desired affix is registered under. |
---|
playAnimation
(name, animation=None)¶Plays the specified animation on the monster.
Parameters: |
|
---|
idle
(animation=None)¶Plays the ‘idle’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
move
(animation=None)¶Plays the ‘move’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
jump
(animation=None)¶Plays the ‘jump’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
fall
(animation=None)¶Plays the ‘fall’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
land
(animation=None)¶Plays the ‘land’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
death
(animation=None)¶Plays the ‘death’ animation on the monster if it has it.
Parameters: | animation (AnimationComponent) – The animation component belonging to the monster. If not provided then it will be automatically retrieved. |
---|
Paragons are elite versions of monsters that are given extra abilities and prove to be a greater challenge.
Paragon monsters derive from core.monster.ParagonMonster
which itself inherits from core.monster.BaseMonster
.
Paragon monsters are automatically given the extra abilities known as affixes.
Unlike other monsters though, Paragon monsters persist between play sessions keeping their stats and affixes.
New affixes can be registered to the monster system via MonsterSystem.registerAffix()
which should be done during mod registration.
Whenever an affix is being added to a monster a new instance of that affix is created.
The affix’s constructor is responsible for hooking up the affix to the monster in whichever way it needs.
Some affixes act as a new attack while some simply use a game timer.
The affix constructor is expected to have the following signature.
Animals are basically monsters with a few behavioral tweaks. Namely the creature is made to be passive and not have any attacks. Additionally ‘hostile = False’ should be provided to the Monster template. Animals are treated differently by the conflict system in that they spawn more often during the day and in less hostile areas.
Below is a full example of a monster’s content file.
from core.monster import BaseMonster, MonsterMeleeAttack
from core.template.animation import Frame, Frames
from core.template.monster import Monster
from core.tuning.monster import MonsterTuning
from siege.util import PixelVector, Rect, Vector
monster = Monster(name="Ortomi", tuning=MonsterTuning.ORTOMI)
monster.hasPhysics(
body = Rect(-7, -17, 13, 35),
friction = Vector(0.1, 0),
groundFriction = Vector(0.1, 0)
)
lick = monster.getSpriteFrames(
frames = [
Frame(122, 226),
Frame(122, 170),
Frame(122, 114),
Frame(122, 58),
Frame(122, 2),
Frame(2, 226),
Frame(2, 170),
Frame(2, 114),
Frame(2, 58),
Frame(2, 2),
],
size = PixelVector(120, 56),
origin = PixelVector(29, 40)
)
run = monster.getSpriteFrames(
frames = [
Frame(242, 225),
Frame(242, 182),
Frame(242, 139),
Frame(242, 96),
Frame(242, 53),
],
size = PixelVector(40, 43),
origin = PixelVector(25, 26)
)
death = monster.getSpriteFrames(Frame(282, 135, size=(39, 41), origin=(29, 24)))
jump = monster.getSpriteFrames(
frames = [
Frame(282, 53),
Frame(267, 268),
],
size = PixelVector(39, 41),
origin = PixelVector(29, 21)
)
attack = monster.getSpriteFrames(
frames = [
Frame(242, 2),
Frame(214, 282),
Frame(161, 282),
Frame(108, 282),
Frame(55, 282),
Frame(2, 282),
],
size = PixelVector(53, 51),
origin = PixelVector(30, 34)
)
idle = monster.getSpriteFrames(Frame(282, 94, size=(39, 41), origin=(29, 24)))
monster.animations(
start = 'idle',
idle = Frames(idle()),
move = Frames(run(), time=100),
jump = Frames(jump(), time=100),
fall = Frames(jump(2), time=100),
attack = (
Frames(attack(1), time=150),
Frames(attack(2), time=300),
Frames(attack(3, 4, 5, 6), time=100)
),
lick = (
Frames(lick(1, 2), time=300),
Frames(lick(3, 4, 5, 6, 7, 8, 4), time=100),
Frames(lick(9, 10, 9), time=60),
),
death = Frames(idle() + death() + idle() + death(), time=200),
disableLooping = ['idle', 'attack', 'jump', 'death', 'lick']
)
lickAttack = monster.attack(
hitFrames = lick.indices(3, 4, 5, 6, 7, 8),
power = MonsterTuning.ORTOMI.LICK_POWER,
powerLevel = MonsterTuning.ORTOMI.LICK_POWER_LEVEL,
detectionArea = Rect(0, -14, 50, 40),
recoverTime = 850,
cooldownTime = 1600,
sound = "mods/core/audio/sfx/monster/ortomi_lick.ogg",
)
basicAttack = monster.attack(
hitFrames = attack.indices(3, 4, 5, 6),
power = MonsterTuning.ORTOMI.MELEE_POWER,
powerLevel = MonsterTuning.ORTOMI.MELEE_POWER_LEVEL,
detectionArea = Rect(0, -3, 32, 24),
cooldownTime = 700,
recoverTime = 900,
sound = "mods/core/audio/sfx/monster/ortomi_bite.ogg",
soundDelay = 300
)
@lickAttack.customize
class LickAttack(MonsterMeleeAttack):
def perform(self, monster, animation='lick'):
MonsterMeleeAttack.perform(self, monster, animation)
@basicAttack.customize
class BasicOrtomiAttack(MonsterMeleeAttack):
def perform(self, monster):
MonsterMeleeAttack.perform(self, monster)
self.jumped = False
def handleTimeStep(self, physics, frameTime):
isFinished = MonsterMeleeAttack.handleTimeStep(self, physics, frameTime)
if not self.jumped and self.monster.entity.animation.getPlayTime() >= 600:
direction = -1 if self.monster.entity.render.getFlipX() else 1
force = Vector(5.0 * direction, -1.5)
physics.applyForce(force)
self.jumped = True
return isFinished
@monster.customize
class OrtomiMonster(BaseMonster):
def __init__(self, entity, component):
BaseMonster.__init__(self, entity)
self.actions.append(BasicOrtomiAttack())
self.actions.append(LickAttack())
def update(self, frameTime):
BaseMonster.update(self, frameTime)
@staticmethod
def create(entity, component):
return OrtomiMonster(entity, component)