Skip to content

Commit

Permalink
feat: weighted random spawns (#1848)
Browse files Browse the repository at this point in the history
Rework of #1802 to fix issues with boss spawns and adding a "weight" to
enable users to balance things.

Credits to @Schiffers for the original PR and idea.
  • Loading branch information
luan authored Dec 9, 2023
1 parent 890db68 commit c778b8e
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 86 deletions.
2 changes: 2 additions & 0 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ onlyPremiumAccount = false
-- NOTE: buyBlessCommandFee will add fee when player buy bless by command (!bless), active changing value between 1 and 100 (fee percent. ex: 3 = 3%, 30 = 30%)
-- NOTE: teleportPlayerToVocationRoom will enable oressa to teleport player to his/her room vocation
-- NOTE: toggleReceiveReward = true, will enable players to choose one of reward exercise weapon by command !reward
-- NOTE: randomMonsterSpawn = true, will enable monsters from the same spawn to be randomized between them, thus making a variable hunt
weatherRain = false
thunderEffect = false
allConsoleLog = false
Expand All @@ -222,6 +223,7 @@ buyAolCommandFee = 0
buyBlessCommandFee = 0
teleportPlayerToVocationRoom = true
toggleReceiveReward = false
randomMonsterSpawn = false

-- Teleport summon
-- Set to true will never remove the summon
Expand Down
1 change: 1 addition & 0 deletions src/config/config_definitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ enum ConfigKey_t : uint16_t {
PVP_RATE_DAMAGE_REDUCTION_PER_LEVEL,
PVP_RATE_DAMAGE_TAKEN_PER_LEVEL,
PZ_LOCKED,
RANDOM_MONSTER_SPAWN,
RATE_ATTACK_SPEED,
RATE_BOSS_ATTACK,
RATE_BOSS_DEFENSE,
Expand Down
1 change: 1 addition & 0 deletions src/config/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ bool ConfigManager::load() {
loadBoolConfig(L, OPTIMIZE_DATABASE, "startupDatabaseOptimization", true);
loadBoolConfig(L, TOGGLE_MAP_CUSTOM, "toggleMapCustom", true);
loadBoolConfig(L, TOGGLE_MAINTAIN_MODE, "toggleMaintainMode", false);
loadBoolConfig(L, RANDOM_MONSTER_SPAWN, "randomMonsterSpawn", false);
loadStringConfig(L, MAINTAIN_MODE_MESSAGE, "maintainModeMessage", "");

loadStringConfig(L, IP, "ip", "127.0.0.1");
Expand Down
260 changes: 183 additions & 77 deletions src/creatures/monsters/spawns/spawn_monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,13 @@ bool SpawnsMonster::loadFromXML(const std::string &filemonstername) {
boostedrate = 2;
}

uint32_t interval = pugi::cast<uint32_t>(childMonsterNode.attribute("spawntime").value()) * 1000 * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN, __FUNCTION__) * boostedrate * eventschedule));
if (interval >= MONSTER_MINSPAWN_INTERVAL && interval <= MONSTER_MAXSPAWN_INTERVAL) {
spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, static_cast<uint32_t>(interval));
} else {
if (interval <= MONSTER_MINSPAWN_INTERVAL) {
g_logger().warn("[SpawnsMonster::loadFromXml] - {} {} spawntime cannot be less than {} seconds, set to {} by default.", nameAttribute.as_string(), pos.toString(), MONSTER_MINSPAWN_INTERVAL / 1000, MONSTER_MINSPAWN_INTERVAL / 1000);
spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, MONSTER_MINSPAWN_INTERVAL);
} else {
g_logger().warn("[SpawnsMonster::loadFromXml] - {} {} spawntime can not be more than {} seconds", nameAttribute.as_string(), pos.toString(), MONSTER_MAXSPAWN_INTERVAL / 1000);
}
pugi::xml_attribute weightAttribute = childMonsterNode.attribute("weight");
uint32_t weight = 1;
if (weightAttribute) {
weight = pugi::cast<uint32_t>(weightAttribute.value());
}

spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, pugi::cast<uint32_t>(childMonsterNode.attribute("spawntime").value()) * 1000, weight);
}
}
}
Expand Down Expand Up @@ -150,8 +146,7 @@ void SpawnMonster::startSpawnMonsterCheck() {
}

SpawnMonster::~SpawnMonster() {
for (const auto &it : spawnedMonsterMap) {
std::shared_ptr<Monster> monster = it.second;
for (const auto &[_, monster] : spawnedMonsterMap) {
monster->setSpawnMonster(nullptr);
}
}
Expand All @@ -170,72 +165,100 @@ bool SpawnMonster::isInSpawnMonsterZone(const Position &pos) {
return SpawnsMonster::isInZone(centerPos, radius, pos);
}

bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, const std::shared_ptr<MonsterType> monsterType, const Position &pos, Direction dir, bool startup /*= false*/) {
bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr<MonsterType> monsterType, bool startup /*= false*/) {
if (spawnedMonsterMap.contains(spawnMonsterId)) {
return false;
}
auto monster = std::make_shared<Monster>(monsterType);
if (startup) {
// No need to send out events to the surrounding since there is no one out there to listen!
if (!g_game().internalPlaceCreature(monster, pos, true)) {
if (!g_game().internalPlaceCreature(monster, sb.pos, true)) {
return false;
}
} else {
if (!g_game().placeCreature(monster, pos, false, true)) {
if (!g_game().placeCreature(monster, sb.pos, false, true)) {
return false;
}
}

monster->setDirection(dir);
monster->setDirection(sb.direction);
monster->setSpawnMonster(this);
monster->setMasterPos(pos);
monster->setMasterPos(sb.pos);

spawnedMonsterMap.insert(spawned_pair(spawnMonsterId, monster));
spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME();
g_events().eventMonsterOnSpawn(monster, pos);
g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, pos);
spawnedMonsterMap[spawnMonsterId] = monster;
sb.lastSpawn = OTSYS_TIME();
g_events().eventMonsterOnSpawn(monster, sb.pos);
g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, sb.pos);
return true;
}

void SpawnMonster::startup() {
for (const auto &it : spawnMonsterMap) {
uint32_t spawnMonsterId = it.first;
const spawnBlock_t &sb = it.second;
spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction, true);
void SpawnMonster::startup(bool delayed) {
if (g_configManager().getBoolean(RANDOM_MONSTER_SPAWN, __FUNCTION__)) {
for (auto it = spawnMonsterMap.begin(); it != spawnMonsterMap.end(); ++it) {
auto &[spawnMonsterId, sb] = *it;
for (auto &[monsterType, weight] : sb.monsterTypes) {
if (monsterType->isBoss()) {
continue;
}
for (auto otherIt = std::next(it); otherIt != spawnMonsterMap.end(); ++otherIt) {
auto &[id, otherSb] = *otherIt;
if (id == spawnMonsterId) {
continue;
}
if (otherSb.hasBoss()) {
continue;
}
if (otherSb.monsterTypes.contains(monsterType)) {
weight += otherSb.monsterTypes[monsterType];
}
otherSb.monsterTypes.emplace(monsterType, weight);
sb.monsterTypes.emplace(monsterType, weight);
}
}
}
}
for (auto &[spawnMonsterId, sb] : spawnMonsterMap) {
const auto &mType = sb.getMonsterType();
if (!mType) {
continue;
}
if (delayed) {
g_dispatcher().addEvent(std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, mType, 0, true), "SpawnMonster::startup");
} else {
scheduleSpawn(spawnMonsterId, sb, mType, 0, true);
}
}
}

void SpawnMonster::checkSpawnMonster() {
checkSpawnMonsterEvent = 0;
if (checkSpawnMonsterEvent == 0) {
return;
}

checkSpawnMonsterEvent = 0;
cleanup();

uint32_t spawnMonsterCount = 0;

for (auto &it : spawnMonsterMap) {
uint32_t spawnMonsterId = it.first;
if (spawnedMonsterMap.find(spawnMonsterId) != spawnedMonsterMap.end()) {
for (auto &[spawnMonsterId, sb] : spawnMonsterMap) {
if (spawnedMonsterMap.contains(spawnMonsterId)) {
continue;
}

spawnBlock_t &sb = it.second;
if (!sb.monsterType->canSpawn(sb.pos)) {
const auto &mType = sb.getMonsterType();
if (!mType) {
continue;
}
if (!mType->canSpawn(sb.pos) || (mType->info.isBlockable && findPlayer(sb.pos))) {
sb.lastSpawn = OTSYS_TIME();
continue;
}
if (OTSYS_TIME() < sb.lastSpawn + sb.interval) {
continue;
}

if (OTSYS_TIME() >= sb.lastSpawn + sb.interval) {
if (sb.monsterType->info.isBlockable && findPlayer(sb.pos)) {
sb.lastSpawn = OTSYS_TIME();
continue;
}

if (sb.monsterType->info.isBlockable) {
spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction);
} else {
scheduleSpawn(spawnMonsterId, sb, 3 * NONBLOCKABLE_SPAWN_MONSTER_INTERVAL);
}

if (++spawnMonsterCount >= static_cast<uint32_t>(g_configManager().getNumber(RATE_SPAWN, __FUNCTION__))) {
break;
}
if (mType->info.isBlockable) {
spawnMonster(spawnMonsterId, sb, mType, true);
} else {
scheduleSpawn(spawnMonsterId, sb, mType, 3 * NONBLOCKABLE_SPAWN_MONSTER_INTERVAL);
}
}

Expand All @@ -244,30 +267,29 @@ void SpawnMonster::checkSpawnMonster() {
}
}

void SpawnMonster::scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, uint16_t interval) {
void SpawnMonster::scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr<MonsterType> mType, uint16_t interval, bool startup /*= false*/) {
if (interval <= 0) {
spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction);
spawnMonster(spawnMonsterId, sb, mType, startup);
} else {
g_game().addMagicEffect(sb.pos, CONST_ME_TELEPORT);
g_dispatcher().scheduleEvent(1400, std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, interval - NONBLOCKABLE_SPAWN_MONSTER_INTERVAL), "SpawnMonster::scheduleSpawn");
g_dispatcher().scheduleEvent(NONBLOCKABLE_SPAWN_MONSTER_INTERVAL, std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, mType, interval - NONBLOCKABLE_SPAWN_MONSTER_INTERVAL, startup), "SpawnMonster::scheduleSpawn");
}
}

void SpawnMonster::cleanup() {
auto it = spawnedMonsterMap.begin();
while (it != spawnedMonsterMap.end()) {
uint32_t spawnMonsterId = it->first;
std::shared_ptr<Monster> monster = it->second;
if (!monster || monster->isRemoved()) {
spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME();
it = spawnedMonsterMap.erase(it);
} else {
++it;
std::vector<uint32_t> removeList;
for (const auto &[spawnMonsterId, monster] : spawnedMonsterMap) {
if (monster == nullptr || monster->isRemoved()) {
removeList.push_back(spawnMonsterId);
}
}
for (const auto &spawnMonsterId : removeList) {
spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME();
spawnedMonsterMap.erase(spawnMonsterId);
}
}

bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval) {
bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval, uint32_t weight /*= 1*/) {
std::string variant = "";
for (const auto &zone : Zone::getZones(pos)) {
if (!zone->getMonsterVariant().empty()) {
Expand All @@ -281,34 +303,77 @@ bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Dire
return false;
}

this->interval = std::min(this->interval, scheduleInterval);

spawnBlock_t sb;
sb.monsterType = monsterType;
sb.pos = pos;
sb.direction = dir;
sb.interval = scheduleInterval;
sb.lastSpawn = 0;
uint32_t eventschedule = g_eventsScheduler().getSpawnMonsterSchedule();
std::string boostedMonster = g_game().getBoostedMonsterName();
int32_t boostedrate = 1;
if (name == boostedMonster) {
boostedrate = 2;
}
// eventschedule is a whole percentage, so we need to multiply by 100 to match the order of magnitude of the other values
scheduleInterval = scheduleInterval * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN, __FUNCTION__) * boostedrate * eventschedule));
if (scheduleInterval < MONSTER_MINSPAWN_INTERVAL) {
g_logger().warn("[SpawnsMonster::addMonster] - {} {} spawntime cannot be less than {} seconds, set to {} by default.", name, pos.toString(), MONSTER_MINSPAWN_INTERVAL / 1000, MONSTER_MINSPAWN_INTERVAL / 1000);
scheduleInterval = MONSTER_MINSPAWN_INTERVAL;
} else if (scheduleInterval > MONSTER_MAXSPAWN_INTERVAL) {
g_logger().warn("[SpawnsMonster::addMonster] - {} {} spawntime can not be more than {} seconds, set to {} by default", name, pos.toString(), MONSTER_MAXSPAWN_INTERVAL / 1000, MONSTER_MAXSPAWN_INTERVAL / 1000);
scheduleInterval = MONSTER_MAXSPAWN_INTERVAL;
}
this->interval = std::gcd(this->interval, scheduleInterval);

spawnBlock_t* sb = nullptr;
uint32_t spawnMonsterId = spawnMonsterMap.size() + 1;
spawnMonsterMap[spawnMonsterId] = sb;
for (auto &[id, maybeSb] : spawnMonsterMap) {
if (maybeSb.pos == pos) {
sb = &maybeSb;
spawnMonsterId = id;
break;
}
}
if (sb) {
if (sb->monsterTypes.contains(monsterType)) {
g_logger().error("[SpawnMonster] Monster {} already exists in spawn block at {}", name, pos.toString());
return false;
}
if (monsterType->isBoss() && sb->monsterTypes.size() > 0) {
g_logger().error("[SpawnMonster] Boss monster {} has been added to spawn block with other monsters. This is not allowed.", name);
return false;
}
if (sb->hasBoss()) {
g_logger().error("[SpawnMonster] Monster {} has been added to spawn block with a boss. This is not allowed.", name);
return false;
}
}
if (!sb) {
sb = &spawnMonsterMap.emplace(spawnMonsterId, spawnBlock_t()).first->second;
}
sb->monsterTypes.emplace(monsterType, weight);
sb->pos = pos;
sb->direction = dir;
sb->interval = scheduleInterval;
sb->lastSpawn = 0;
return true;
}

void SpawnMonster::removeMonster(std::shared_ptr<Monster> monster) {
for (auto it = spawnedMonsterMap.begin(), end = spawnedMonsterMap.end(); it != end; ++it) {
if (it->second == monster) {
spawnedMonsterMap.erase(it);
uint32_t spawnMonsterId = 0;
for (const auto &[id, m] : spawnedMonsterMap) {
if (m == monster) {
spawnMonsterId = id;
break;
}
}
spawnedMonsterMap.erase(spawnMonsterId);
}

void SpawnMonster::setMonsterVariant(const std::string &variant) {
for (auto &it : spawnMonsterMap) {
auto variantName = variant + it.second.monsterType->typeName;
auto variantType = g_monsters().getMonsterType(variantName, false);
it.second.monsterType = variantType ? variantType : it.second.monsterType;
std::unordered_map<std::shared_ptr<MonsterType>, uint32_t> monsterTypes;
for (const auto &[monsterType, weight] : it.second.monsterTypes) {
auto variantName = variant + monsterType->typeName;
auto variantType = g_monsters().getMonsterType(variantName, false);
monsterTypes.emplace(variantType, weight);
}
it.second.monsterTypes = monsterTypes;
}
}

Expand All @@ -318,3 +383,44 @@ void SpawnMonster::stopEvent() {
checkSpawnMonsterEvent = 0;
}
}

std::shared_ptr<MonsterType> spawnBlock_t::getMonsterType() const {
if (monsterTypes.empty()) {
return nullptr;
}
uint32_t totalWeight = 0;
for (const auto &[mType, weight] : monsterTypes) {
if (!mType) {
continue;
}
if (mType->isBoss()) {
if (monsterTypes.size() > 1) {
g_logger().warn("[SpawnMonster] Boss monster {} has been added to spawn block with other monsters. This is not allowed.", mType->name);
}
return mType;
}
totalWeight += weight;
}
uint32_t randomWeight = uniform_random(0, totalWeight - 1);
// order monsters by weight DESC
std::vector<std::pair<std::shared_ptr<MonsterType>, uint32_t>> orderedMonsterTypes(monsterTypes.begin(), monsterTypes.end());
std::sort(orderedMonsterTypes.begin(), orderedMonsterTypes.end(), [](const auto &a, const auto &b) {
return a.second > b.second;
});
for (const auto &[mType, weight] : orderedMonsterTypes) {
if (randomWeight < weight) {
return mType;
}
randomWeight -= weight;
}
return nullptr;
}

bool spawnBlock_t::hasBoss() const {
for (const auto &[monsterType, weight] : monsterTypes) {
if (monsterType->isBoss()) {
return true;
}
}
return false;
}
Loading

0 comments on commit c778b8e

Please sign in to comment.