Enemy Architecture
Relevant source files
The following files were used as context for generating this wiki page:- src/entities/enemies/base/EnemyBase.js
- src/config/enemiesData.js
- src/controllers/EnemyManager.js
- src/entities/enemies/systems/EnemySkillSystem.js
- src/config/mapData.js
- src/entities/enemies/useable/Slime.js
- src/entities/enemies/useable/Canine.js
- src/entities/enemies/useable/Bat.js
- src/systems/PortalConditionManager.js
- src/utils/SaveSlotManager.js
The enemy architecture in Souless is a data-driven system designed for modularity and ease of extension. It revolves around a central configuration file, enemiesData.js, which defines the properties of all enemies, from simple slimes to complex multi-phase bosses. A base class, EnemyBase, provides the core functionality, which is then managed and spawned by the EnemyManager based on map-specific settings.
This architecture separates enemy data (stats, assets, AI behavior) from the core logic, allowing developers to create new enemy types with minimal code. Complex behaviors and attacks are handled by dedicated subsystems like EnemyController, BossController, and the powerful EnemySkillSystem, which interprets skill definitions from the configuration file to execute attacks.
Core Components
The enemy system is composed of several key classes and configuration files that work together.
| Component | File Path | Description |
|---|---|---|
| Enemy Data | src/config/enemiesData.js | A central JSON-like object containing all definitions for every enemy type. |
| Base Class | src/entities/enemies/base/EnemyBase.js | The foundational class for all enemies, handling physics, state, health, and lifecycle. |
| Concrete Classes | src/entities/enemies/useable/*.js | Simple classes like Slime.js that extend EnemyBase for a specific enemy type. |
| Manager | src/controllers/EnemyManager.js | Manages the spawning, updating, and removal of enemies within a game scene. |
| AI Controllers | src/entities/enemies/systems/ | EnemyController.js and BossController.js handle the AI logic for normal and boss enemies. |
| Skill System | src/entities/enemies/systems/EnemySkillSystem.js | Executes simple and complex skills, including sequenced hitboxes and movement. |
This diagram illustrates the relationship between the main components of the enemy architecture.
Data-Driven Configuration
All enemy characteristics are defined in a single configuration file, enemiesData.js. This allows for easy tweaking and creation of new enemies without changing game logic. Each enemy is an object keyed by its type name (e.g., slime, assassin_boss).
Sources: src/config/enemiesData.js
Enemy Data Structure
The structure for each enemy contains all necessary information for its creation and behavior.
| Property | Description | Example (from slime) |
|---|---|---|
type | The category of the enemy, e.g., 'normal' or 'boss'. | normal |
sprite | Defines the spritesheet dimensions, scale, and orientation. | { frameWidth: 64, frameHeight: 64, scale: 1.5 } |
physics | Sets the physics body size, offset, and world collision. | { width: 20, height: 20, collideWorldBounds: true } |
stats | Core gameplay stats like HP, speed, patrol range, and EXP reward. | { maxHP: 20, speed: { min: 40, max: 40 } } |
assets | Paths to the spritesheet files for different states (idle, hit, death). | { idle: '/assets/enemy/slime/Slime_Spiked_Idle.png' } |
animations | Defines the frame ranges and rates for each animation. | { idle: { start: 0, end: 3, frameRate: 6, repeat: -1 } } |
ai | Configures the AI behavior, including detection range and attack details. | { type: 'patrol', detectRange: 180, attack: { ... } } |
An example configuration for a simple "slime" enemy:
// src/config/enemiesData.js:3-36
slime: {
type: 'normal',
sprite: {
frameWidth: 64,
frameHeight: 64,
scale: 1.5,
flipX: false,
},
physics: {
width: 20,
height: 20,
collideWorldBounds: true,
offsetX: 22,
offsetY: 30,
},
stats: {
maxHP: 20,
speed: { min: 40, max: 40 },
patrolRange: { min: 150, max: 150 },
expReward: 10,
damageCooldown: 300,
},
assets: {
idle: '/assets/enemy/slime/Slime_Spiked_Idle.png',
hit: '/assets/enemy/slime/Slime_Spiked_Hit.png',
death: '/assets/enemy/slime/Slime_Spiked_Death.png',
attack: '/assets/enemy/slime/Slime_Spiked_Hit.png',
},
animations: {
idle: { start: 0, end: 3, frameRate: 6, repeat: -1 },
hit: { start: 0, end: 2, frameRate: 10, repeat: 0 },
death: { start: 0, end: 4, frameRate: 8, repeat: 0 },
attack: { start: 0, end: 3, frameRate: 10, repeat: 0 },
},
ai: {
type: 'patrol',
detectRange: 180,
attack: {
range: 60,
damage: 1,
cooldown: 1800,
hitDelay: 200,
},
},
},
Class Structure
EnemyBase
EnemyBase is the abstract base class from which all enemies inherit. It is initialized with a scene, position, and enemyType. The constructor fetches the corresponding data from EnemiesData and sets up the sprite, physics, stats, and AI systems.
Sources: [src/entities/enemies/base/EnemyBase.js:12-140]
This class diagram shows the core components of EnemyBase and its relationship with its subsystems.
Concrete Enemy Classes
Specific enemy classes like Slime, Bat, and Canine are very lightweight. They simply extend EnemyBase and pass their specific enemyType to the super constructor. They also contain a static preload method that calls the parent's preload to load the correct assets.
// src/entities/enemies/useable/Slime.js:3-14
export default class Slime extends EnemyBase {
constructor(scene, x, y) {
super(scene, x, y, 'slime');
// HP바 depth 설정
if (scene.mapConfig?.depths?.enemy) {
this.hpBar.setDepth(scene.mapConfig.depths.enemy + 1);
}
}
static preload(scene) {
EnemyBase.preload(scene, 'slime');
}
}
This inheritance structure allows for rapid development of new enemy types.
Spawning and Management
The EnemyManager is responsible for populating maps with enemies. It is instantiated in a game scene and uses the mapConfig to determine which enemy types to spawn, their quantity, and their respawn rate.
Sources: src/controllers/EnemyManager.js:10-25, [src/config/mapData.js:40-47]
The spawning process follows this logic:
- Initial Spawn: When
createInitial()is called, the manager spawns theinitialCountof enemies defined in the map's configuration. - Respawn Loop: In its
updatemethod, the manager checks if the current enemy count is belowmaxCountand if therespawnIntervalhas passed. - Spawn Location: If conditions are met, it calls
spawnRandomEnemyNearPlayer(), which attempts to spawn a new enemy at a random location that is at leastminPlayerDistanceaway from the player. - Enemy Creation: A random enemy type is selected from the map's
typesarray, and a new instance of the corresponding class (e.g.,Slime,Bat) is created.
This flowchart shows the spawning logic within EnemyManager.
AI and Skill System
AI Controllers
Enemy AI is managed by a controller class, instantiated inside EnemyBase based on the ai.type field in enemiesData.js.
EnemyController: Used forpatrolandaggressiveAI types. Manages basic states like patrolling a defined range, detecting and moving toward the player, and initiating attacks when in range.BossController: Used for thebossAI type. It extends the basic logic with more complex behaviors, such as different movement ranges (walkRange,runRange) and managing multi-phase encounters. It triggers phase changes based on HP thresholds.
Sources: [src/entities/enemies/base/EnemyBase.js:179-192]
Skill System (EnemySkillSystem)
The EnemySkillSystem is a sophisticated component that executes attacks and abilities. It is initialized with the skills object from the enemy's data in enemiesData.js. This system allows for complex attacks with multiple hitboxes, movement, and special effects, all defined declaratively.
Key features include:
- Skill Registration: Parses the
skillsobject and createsSkillinstances. hitboxSequence: A powerful feature that allows a single skill to trigger a sequence of hitboxes with different timings, sizes, damage, and effects. This is used for combo attacks or abilities with multiple stages.- Skill Types: Supports various skill types like
melee,movement,instant, andaoe. - Movement Integration: Skills of type
movementor those with amovementproperty in theirhitboxSequencecan move the enemy or the hitbox itself.
An example of a complex skill definition for the assassin_boss:
// src/config/enemiesData.js:241-274
shadowDash: {
type: 'movement',
animation: 'dash',
range: 350,
cooldown: 6000,
hitDelay: 100,
duration: 300,
priority: 1,
turnDelay: 200,
hitboxSequence: [
{
delay: 0,
hitbox: {
width: 80,
height: 150,
offsetX: 45,
offsetY: 40,
duration: 100,
},
damage: 0.2,
knockback: { x: 50, y: 0 },
},
],
impactEffect: 'fire_knight_w_skill',
hitstop: 'light',
targetType: 'multi',
movement: {
type: 'dash',
speed: 500,
duration: 500,
afterimage: true,
afterimageCount: 3,
},
},
The following sequence diagram shows how an enemy uses a skill on the player.
Sources: src/entities/enemies/systems/EnemySkillSystem.js:178-200, 290-313, [src/entities/enemies/base/EnemyBase.js:457-476]
Lifecycle: Damage and Death
The lifecycle of an enemy is handled within EnemyBase.
- Damage: The
takeDamage(amount)method is called when an enemy is hit. It reduces HP, updates the HP bar, and checks if HP has fallen below zero. If the enemy survives, it plays a "hit" animation and enters a briefisBeingHitstate. - Boss Phases: For bosses with multiple phases (
maxPhase > 1), if HP drops to zero, theonPhaseChange()method is called instead ofdie(). This resets the enemy's HP to the max for the new phase and updates the UI. - Death: When a non-boss or a final-phase boss's HP reaches zero,
isDeadis set totrue. TheplayDeath()animation is triggered. - Soul Spawn: Upon completion of the death animation,
spawnSoul()is called, which creates a soul entity for the player to collect. - Cleanup: The
destroy()method is called, which removes the enemy's sprite and HP bar. It also grants experience to the player and records the kill viaKillTracker.
This flowchart illustrates the enemy death process.
Conclusion
The enemy architecture in Souless is robust, scalable, and developer-friendly. By centralizing enemy definitions in enemiesData.js, the system allows for the rapid creation and balancing of diverse enemies. The separation of concerns between EnemyBase (core state), EnemyManager (spawning), AI controllers (behavior), and EnemySkillSystem (actions) creates a clean and maintainable codebase. This modular, data-driven approach is a cornerstone of the game's design, enabling rich and varied enemy encounters.