GoAdventure – Enemies
Hey. So, today I added enemies to the game. Now, you still cannot fight them. We’ll get there.
SQL
So, I made the enemies table in the database. The entries are id, name and level. The id is serial and used as primary key. The name is varchar(255), must be unique and not null. This avoids creating 2 enemies with the same name. We all know that enemies evolve into better version. Maybe, I should add a Demon Lord Slime… (maybe). And level is an integer that is not null with a default of 1. Don’t want to fight an immortal enemy.
CREATE TABLE enemies (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
level INTEGER NOT NULL DEFAULT 1
);
To populate the table, I just created an sql file for it so that it automatically populate on database creation. I am not sure how game companies create enemies and add them to game. But this way is easier for me right now.
INSERT INTO enemies (name, level) VALUES
('Slime', 1),
('Green Slime', 2),
('Orange Slime', 3),
('Red Slime', 4),
('Black Slime', 5),
('Goblin', 6),
('Hobgoblin', 7),
('Goblin Chief', 8),
('Goblin General', 9),
('Goblin King', 10);
I know. Not very original for enemies but still. It’s the most common enemies that pop to mind when you think adventure game.
Go
For the model in Go, I create a Enemy struct that contains id, name, level, health and stats. The health is a struct with current health and max health. Got to make sure the enemy can die. No fun if you can’t kill it. The stats is a struct with strength and defense. Now, the max health, strength and defense are calculated on initialization based on the enemy level. I used the same way as I did for the player’s stat to calculate them. The health is ((level – 1) * 10) + 50. Whereas strength and defense is ((level – 1) * 2) + 5. The starting health, defense and strength are 50, 5 and 5 points lower compared to the player. Hopefully, this is enough so the player doesn’t die too quickly. I might added critical hits and stuff later. I also added the basic get functions for the enemy and take damage function. Because take damage enemy.
type Enemy struct {
id int32
name string
level int32
health health
stat stat
}
func InitEnemy(id, level int32, name string) (*Enemy, error) {
if id < 1 {
return nil, errors.New("invalid enemy id")
}
if level < 1 {
return nil, errors.New("invalid enemy level")
}
if name == "" {
return nil, errors.New("invalid enemy name")
}
healthAmount := ((level - 1) * 10) + 50
statAmount := ((level - 1) * 2) + 5
enemy := Enemy{
id: id,
name: name,
level: level,
health: health{
currentHealth: healthAmount,
maxHealth: healthAmount,
},
stat: stat{
strength: statAmount,
defense: statAmount,
},
}
return &enemy, nil
}
// Get Functions
func (e *Enemy) GetID() int32 {
return e.id
}
func (e *Enemy) GetName() string {
return e.name
}
func (e *Enemy) GetLevel() int32 {
return e.level
}
func (e *Enemy) GetStrength() int32 {
return e.stat.strength
}
func (e *Enemy) GetDefense() int32 {
return e.stat.defense
}
func (e *Enemy) GetCurrentHealth() int32 {
return e.health.currentHealth
}
func (e *Enemy) GetMaxHealth() int32 {
return e.health.maxHealth
}
// Action Function
func (e *Enemy) TakeDamage(amount int32) (bool, error) {
if amount < 1 {
return false, errors.New("invalid damage amount")
}
e.health.currentHealth -= amount
if e.health.currentHealth <= 0 {
return true, nil
}
return false, nil
}
Now, I had to modify the locations to add a minimum level and max level for each non-town locations. The min and max are to limit the enemies that spawn at that location. Nobody wants to have a level 100 enemy right next to the starting town. Poor level 1 noob. Time to power level I guess. And I used the hasStore to check if it’s a town. I set it up that all towns will have a store.
type Location struct {
id int32
name string
description string
minLevel int32
maxLevel int32
hasStore bool
canTeleport bool
directions []*LocationDirection
}
Now, for spawning enemies at each location. I used a randomizer that has a 75% chance to spawn a monster. During the initialization, it goes through all the enemies and check if that monster will spawn. I used another randomizer to see how many of that enemy will spawn. Because fighting only 1 slime is not fun. Got to destroy millions of them. I still need to work on a way to make them respawn after a set amount of time. So, that we don’t get bored.
// map[locationID]map[enemyID]quantity
func InitializeEnemiesLocation(enemies map[int32]*models.Enemy, locations map[int32]*models.Location) (map[int32]map[int32]int32, error) {
var enemiesLocation = make(map[int32]map[int32]int32)
for _, location := range locations {
// Check if no store == not town
if !location.HasStore() {
var enemiesList = make(map[int32]int32)
// Go through enemy list
for _, enemy := range enemies {
// Check if enemy level is valid for location
if enemy.GetLevel() >= location.GetMinLevel() && enemy.GetLevel() <= location.GetMaxLevel() {
// Randomize if enemy spawn
randNum := rand.IntN(100) + 1
if randNum >= 25 {
// Randomize enemy quantity
amountNum := rand.IntN(5) + 1
enemiesList[enemy.GetID()] = int32(amountNum)
}
}
}
// Add enemiesList to enemiesLocation
enemiesLocation[location.GetID()] = enemiesList
}
}
return enemiesLocation, nil
}
I also modified the look command to include enemies when in a non-town location. I wonder if I should add surprise battles when you move between zones.
func look() {
fmt.Println("\nYou look around...")
fmt.Printf("You are currently in %s.\n", Assets.Locations[Assets.Player.GetLocation()].GetName())
fmt.Println(Assets.Locations[Assets.Player.GetLocation()].GetDescription())
directions := Assets.Locations[Assets.Player.GetLocation()].GetDirections()
for _, direction := range directions {
fmt.Printf("You see %s to the %s.\n", Assets.Locations[direction.GetLocationID()].GetName(), direction.GetDirection())
}
if Assets.Locations[Assets.Player.GetLocation()].HasStore() {
fmt.Println("You see a store in the corner.")
} else {
fmt.Println("Enemies:")
if len(Assets.EnemiesLocation[Assets.Player.GetLocation()]) != 0 {
for enemyID, quantity := range Assets.EnemiesLocation[Assets.Player.GetLocation()] {
fmt.Printf("You see %d %s.\n", quantity, Assets.Enemies[enemyID].GetName())
}
} else {
fmt.Println("You don't see any enemies.")
if generateJoke() {
fmt.Println("When is the last time you took a bath...")
}
}
}
}
That’s it for today. See you in the next one.
God bless.

Thanks for sharing man!