diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ee959f3d1..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- - -# https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/ubuntu-18.04-git/.devcontainer/Dockerfile -FROM ubuntu:18.04 - - ENV DEBIAN_FRONTEND=noninteractive - RUN apt-get update \ - && apt-get -y install --no-install-recommends build-essential \ - gcc-5 g++-5 libtool cmake curl debconf-utils \ - git git-core libio-stringy-perl liblua5.1 \ - liblua5.1-dev libluabind-dev libmysql++ \ - libperl-dev libperl5i-perl libsodium-dev \ - libsodium23 libmysqlclient-dev lua5.1 \ - minizip make mariadb-client \ -# optional, mariadb server - mariadb-server \ -# optional, debugging tools - gdb valgrind \ -# - nano open-vm-tools unzip uuid-dev \ - zlibc wget \ -# # -# # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* -ENV DEBIAN_FRONTEND=dialog -EXPOSE 3306 -EXPOSE 5558 -EXPOSE 5559 -EXPOSE 7000 -EXPOSE 7001 -EXPOSE 7002 -EXPOSE 7003 -EXPOSE 7004 -EXPOSE 7005 -EXPOSE 7778 -EXPOSE 9000 -EXPOSE 9001 -EXPOSE 9080 -EXPOSE 9081 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1723407c3..589bb2ed4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,28 +1,21 @@ // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/ubuntu-18.04-git { - "name": "Ubuntu 18.04 EQEMU", - "dockerFile": "Dockerfile", + "name": "Ubuntu 18.04 EQEMU", + // Moved from dockerfile to image so it builds faster + "image": "eqemu/devcontainer:0.0.2", // Set *default* container specific settings.json values on container create. "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, + "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], + // Add the IDs of extensions you want installed when the container is created. - "extensions": [], - "appPort": [3306, 5558, 5559, 7000, 7001, 7002, 7003, 7004, 7005, 7778, 9000, 9001, 9080, 9081], - // "forwardPorts": [3306, 5558, 5559, 7000, 7001, 7002, 7003, 7004, 7005, 7778, 9000, 9001, 9080, 9081], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", - - // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-in-docker. - // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], - - // Uncomment when using a ptrace-based debugger like C++, Go, and Rust - // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], - - // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. - // "remoteUser": "eqemu" -} \ No newline at end of file + "extensions": ["ms-vscode.cpptools", "ms-azuretools.vscode-docker"], + "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], + "remoteEnv": { + "HOST_PROJECT_PATH": "${localWorkspaceFolder}" + } +} diff --git a/.gitignore b/.gitignore index 89dd521c5..287c07412 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ *.out *.app +.bash_history + # CMake CMakeCache.txt CMakeFiles diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 429b59492..1c6e5c871 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,10 +15,22 @@ "$gcc" ] }, + { + "label": "make clean", + "type": "shell", + "command": "cd build && make clean", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, { "label": "cmake", "type": "shell", - "command": "mkdir -p build && cd build && cmake -DEQEMU_BUILD_LOGIN=ON -DEQEMU_BUILD_LUA=ON -G 'Unix Makefiles' ..", + "command": "mkdir -p build && cd build && rm CMakeCache.txt && cmake -DEQEMU_BUILD_LOGIN=ON -DEQEMU_BUILD_LUA=ON -G 'Unix Makefiles' ..", "group": { "kind": "build", "isDefault": true @@ -38,9 +50,66 @@ } }, { - "label": "zone", + "label": "download maps", "type": "shell", - "command": "cd build/bin && ./zone", + "command": "mkdir -p build/bin && cd build/bin && wget https://codeload.github.com/Akkadius/EQEmuMaps/zip/master -O maps.zip && unzip -o maps.zip && rm ./maps -rf && mv EQEmuMaps-master maps && rm maps.zip", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "download quests", + "type": "shell", + "command": "mkdir -p build/bin && cd build/bin && cd server && git -C ./quests pull 2> /dev/null || git clone https://github.com/ProjectEQ/projecteqquests.git quests", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "download eqemu_config", + "type": "shell", + "command": "mkdir -p build/bin && cd build/bin && wget --no-check-certificate https://raw.githubusercontent.com/Akkadius/EQEmuInstall/master/eqemu_config_docker.json -O eqemu_config.json", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "rebuild database (mariadb must be started)", + "type": "shell", + "command": "mkdir -p build/bin && cd build/bin && docker run -i --rm --privileged -v ${HOST_PROJECT_PATH}/build/bin:/src --network=eqemu -it eqemu/server:0.0.3 bash -c './eqemu_server.pl source_peq_db && ./eqemu_server.pl check_db_updates && ./eqemu_server.pl linux_login_server_setup'", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "zone 7000", + "type": "shell", + "command": "docker stop zone7000 | true && docker network create eqemu | true && docker run -i --rm --name zone7000 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --privileged -v ${HOST_PROJECT_PATH}/build/bin:/src --ulimit core=10000000 --network=eqemu -p 7000:7000/udp -e LD_LIBRARY_PATH=/src/ eqemu/server:0.0.3 gdb -ex run --args ./zone dynamic_zone7000:7000", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "zone 7001", + "type": "shell", + "command": "docker stop zone7001 | true && docker network create eqemu | true && docker run -i --rm --name zone7001 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --privileged -v ${HOST_PROJECT_PATH}/build/bin:/src --ulimit core=10000000 --network=eqemu -p 7001:7001/udp -e LD_LIBRARY_PATH=/src/ eqemu/server:0.0.3 gdb -ex run --args ./zone dynamic_zone7001:7001", "group": { "kind": "test", "isDefault": true @@ -49,20 +118,38 @@ { "label": "loginserver", "type": "shell", - "command": "cd build/bin && ./loginserver", + "command": "docker stop loginserver | true && docker network create eqemu | true && docker run -i --rm --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --privileged -v ${HOST_PROJECT_PATH}/build/bin:/src --ulimit core=10000000 --network=eqemu --name loginserver -p 5999:5999/udp -p 5998:5998/udp -e LD_LIBRARY_PATH=/src/ eqemu/server:0.0.3 gdb -ex run --args ./loginserver", "group": { "kind": "test", "isDefault": true } }, { - "label": "world", + "label": "shared_memory, world", "type": "shell", - "command": "cd build/bin && ./world", + "command": "docker stop sharedmemory | true && docker stop world | true && docker network create eqemu | true && docker run --rm -v ${HOST_PROJECT_PATH}/build/bin:/src --network=eqemu --name sharedmemory eqemu/server:0.0.3 ./shared_memory && docker run --rm -v ${HOST_PROJECT_PATH}/build/bin:/src --ulimit core=10000000 -e LD_LIBRARY_PATH=/src/ --network=eqemu --name world -p 9000:9000 -p 9000:9000/udp -p 9001:9001 -p 9080:9080 eqemu/server:0.0.3 gdb -ex run ./world", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "queryserv", + "type": "shell", + "command": "docker stop queryserv | true && docker run --rm -v ${HOST_PROJECT_PATH}/build/bin:/src --ulimit core=10000000 -e LD_LIBRARY_PATH=/src/ --network=eqemu --name queryserv eqemu/server:0.0.3 gdb -ex run ./queryserv", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "mariadb", + "type": "shell", + "command": "docker stop mariadb | true && cd build/bin && docker network create eqemu | true && docker run --rm -v ${HOST_PROJECT_PATH}/build/bin/db:/bitnami/mariadb -p 3306:3306 -e MARIADB_DATABASE=peq -e MARIADB_USER=eqemu -e MARIADB_PASSWORD=eqemupass -e ALLOW_EMPTY_PASSWORD=yes --name mariadb --network=eqemu bitnami/mariadb:latest", "group": { "kind": "test", "isDefault": true } } ] -} \ No newline at end of file +} diff --git a/common/version.h b/common/version.h index a950edbe0..359c27576 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9150 +#define CURRENT_BINARY_DATABASE_VERSION 9151 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9026 diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 9d0aa9231..ca7a29f25 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -404,6 +404,7 @@ 9148|2020_01_28_corpse_guild_consent_id.sql|SHOW COLUMNS FROM `character_corpses` LIKE 'guild_consent_id'|empty| 9149|2020_02_06_globalloot.sql|SHOW COLUMNS FROM `global_loot` LIKE 'hot_zone'|empty| 9150|2020_02_06_aa_reset_on_death.sql|SHOW COLUMNS FROM `aa_ability` LIKE 'reset_on_death'|empty| +9151|2020_03_05_npc_always_aggro.sql|SHOW COLUMNS FROM `npc_types` LIKE 'always_aggros_foes'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2020_03_05_npc_always_aggro.sql b/utils/sql/git/required/2020_03_05_npc_always_aggro.sql new file mode 100644 index 000000000..d9e6351a3 --- /dev/null +++ b/utils/sql/git/required/2020_03_05_npc_always_aggro.sql @@ -0,0 +1 @@ +ALTER TABLE `npc_types` ADD COLUMN `always_aggros_foes` tinyint(4) NOT NULL DEFAULT 0; diff --git a/zone/aggro.cpp b/zone/aggro.cpp index ff0077353..fac15457e 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -139,7 +139,7 @@ void NPC::DescribeAggro(Client *towho, Mob *mob, bool verbose) { if (RuleB(Aggro, UseLevelAggro)) { - if (GetLevel() < RuleI(Aggro, MinAggroLevel) && mob->GetLevelCon(GetLevel()) == CON_GRAY && GetBodyType() != 3) + if (GetLevel() < RuleI(Aggro, MinAggroLevel) && mob->GetLevelCon(GetLevel()) == CON_GRAY && GetBodyType() != 3 && !AlwaysAggrosFoes()) { towho->Message(Chat::White, "...%s is red to me (basically)", mob->GetName(), dist2, iAggroRange2); return; @@ -147,7 +147,7 @@ void NPC::DescribeAggro(Client *towho, Mob *mob, bool verbose) { } else { - if(GetINT() > RuleI(Aggro, IntAggroThreshold) && mob->GetLevelCon(GetLevel()) == CON_GRAY ) { + if(GetINT() > RuleI(Aggro, IntAggroThreshold) && mob->GetLevelCon(GetLevel()) == CON_GRAY && !AlwaysAggrosFoes()) { towho->Message(Chat::White, "...%s is red to me (basically)", mob->GetName(), dist2, iAggroRange2); return; @@ -318,7 +318,7 @@ bool Mob::CheckWillAggro(Mob *mob) { //old InZone check taken care of above by !mob->CastToClient()->Connected() ( ( GetLevel() >= RuleI(Aggro, MinAggroLevel)) - ||(GetBodyType() == 3) + ||(GetBodyType() == 3) || AlwaysAggrosFoes() ||( mob->IsClient() && mob->CastToClient()->IsSitting() ) ||( mob->GetLevelCon(GetLevel()) != CON_GRAY) @@ -352,6 +352,7 @@ bool Mob::CheckWillAggro(Mob *mob) { //old InZone check taken care of above by !mob->CastToClient()->Connected() ( ( GetINT() <= RuleI(Aggro, IntAggroThreshold) ) + || AlwaysAggrosFoes() ||( mob->IsClient() && mob->CastToClient()->IsSitting() ) ||( mob->GetLevelCon(GetLevel()) != CON_GRAY) @@ -383,6 +384,7 @@ bool Mob::CheckWillAggro(Mob *mob) { LogAggro("Dist^2: [{}]\n", dist2); LogAggro("Range^2: [{}]\n", iAggroRange2); LogAggro("Faction: [{}]\n", fv); + LogAggro("AlwaysAggroFlag: [{}]\n", AlwaysAggrosFoes()); LogAggro("Int: [{}]\n", GetINT()); LogAggro("Con: [{}]\n", GetLevelCon(mob->GetLevel())); diff --git a/zone/beacon.cpp b/zone/beacon.cpp index 81b964290..ac4beaacc 100644 --- a/zone/beacon.cpp +++ b/zone/beacon.cpp @@ -56,7 +56,7 @@ Beacon::Beacon(Mob *at_mob, int lifetime) :Mob ( nullptr, nullptr, 0, 0, 0, INVISIBLE_MAN, 0, BT_NoTarget, 0, 0, 0, 0, 0, at_mob->GetPosition(), 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, EQEmu::TintProfile(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, EQEmu::TintProfile(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false ), remove_timer(lifetime), spell_timer(0) diff --git a/zone/bot.cpp b/zone/bot.cpp index 5579b4bc5..6d14ce740 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -6421,32 +6421,52 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { bool taunt_time = taunt_timer.Check(); bool ca_time = classattack_timer.Check(false); + bool ma_time = monkattack_timer.Check(false); bool ka_time = knightattack_timer.Check(false); - if((taunt_time || ca_time || ka_time) && !IsAttackAllowed(target)) + + if (taunt_time) { + + // Bots without this skill shouldn't be 'checking' on this timer..let's just disable it and avoid the extra IsAttackAllowed() checks + // Note: this is done here instead of NPC::ctor() because taunt skill can be acquired during level ups (the timer is re-enabled in CalcBotStats()) + if (!GetSkill(EQEmu::skills::SkillTaunt)) { + + taunt_timer.Disable(); + return; + } + + if (!IsAttackAllowed(target)) { + return; + } + } + + if ((ca_time || ma_time || ka_time) && !IsAttackAllowed(target)) { return; + } if(ka_time){ - int knightreuse = 1000; + switch(GetClass()){ - case SHADOWKNIGHT: - case SHADOWKNIGHTGM: { + case SHADOWKNIGHT: { CastSpell(SPELL_NPC_HARM_TOUCH, target->GetID()); - knightreuse = (HarmTouchReuseTime * 1000); + knightattack_timer.Start(HarmTouchReuseTime * 1000); + break; } - case PALADIN: - case PALADINGM: { + case PALADIN: { if(GetHPRatio() < 20) { CastSpell(SPELL_LAY_ON_HANDS, GetID()); - knightreuse = (LayOnHandsReuseTime * 1000); + knightattack_timer.Start(LayOnHandsReuseTime * 1000); + } + else { + knightattack_timer.Start(2000); } - else - knightreuse = 2000; break; } + default: { + break; + } } - knightattack_timer.Start(knightreuse); } if(taunting && target && target->IsNPC() && taunt_time) { @@ -6457,8 +6477,66 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } } - if(!ca_time) + if (ma_time) { + switch (GetClass()) { + case MONK: { + int reuse = (MonkSpecialAttack(target, EQEmu::skills::SkillTigerClaw) - 1); + + // Live AA - Technique of Master Wu + int wuchance = itembonuses.DoubleSpecialAttack + spellbonuses.DoubleSpecialAttack + aabonuses.DoubleSpecialAttack; + + if (wuchance) { + const int MonkSPA[5] = { + EQEmu::skills::SkillFlyingKick, + EQEmu::skills::SkillDragonPunch, + EQEmu::skills::SkillEagleStrike, + EQEmu::skills::SkillTigerClaw, + EQEmu::skills::SkillRoundKick + }; + int extra = 0; + // always 1/4 of the double attack chance, 25% at rank 5 (100/4) + while (wuchance > 0) { + if (zone->random.Roll(wuchance)) { + ++extra; + } + else { + break; + } + wuchance /= 4; + } + + Mob* bo = GetBotOwner(); + if (bo && bo->IsClient() && bo->CastToClient()->GetBotOption(Client::booMonkWuMessage)) { + + bo->Message( + GENERIC_EMOTE, + "The spirit of Master Wu fills %s! %s gains %d additional attack(s).", + GetCleanName(), + GetCleanName(), + extra + ); + } + + auto classic = RuleB(Combat, ClassicMasterWu); + while (extra) { + MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : EQEmu::skills::SkillTigerClaw)); + --extra; + } + } + + float HasteModifier = (GetHaste() * 0.01f); + monkattack_timer.Start((reuse * 1000) / HasteModifier); + + break; + } + default: + break;; + } + } + + if (!ca_time) { return; + } float HasteModifier = (GetHaste() * 0.01f); uint16 skill_to_use = -1; @@ -6493,18 +6571,22 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } break; case MONK: - if(GetLevel() >= 30) + if (GetLevel() >= 30) { skill_to_use = EQEmu::skills::SkillFlyingKick; - else if(GetLevel() >= 25) + } + else if (GetLevel() >= 25) { skill_to_use = EQEmu::skills::SkillDragonPunch; - else if(GetLevel() >= 20) + } + else if (GetLevel() >= 20) { skill_to_use = EQEmu::skills::SkillEagleStrike; - else if(GetLevel() >= 10) - skill_to_use = EQEmu::skills::SkillTigerClaw; - else if(GetLevel() >= 5) + } + else if (GetLevel() >= 5) { skill_to_use = EQEmu::skills::SkillRoundKick; - else + } + else { skill_to_use = EQEmu::skills::SkillKick; + } + break; case ROGUE: skill_to_use = EQEmu::skills::SkillBackstab; @@ -6555,19 +6637,54 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { } } - if (skill_to_use == EQEmu::skills::SkillFlyingKick || skill_to_use == EQEmu::skills::SkillDragonPunch || skill_to_use == EQEmu::skills::SkillEagleStrike || skill_to_use == EQEmu::skills::SkillTigerClaw || skill_to_use == EQEmu::skills::SkillRoundKick) { + if ( + skill_to_use == EQEmu::skills::SkillFlyingKick || + skill_to_use == EQEmu::skills::SkillDragonPunch || + skill_to_use == EQEmu::skills::SkillEagleStrike || + skill_to_use == EQEmu::skills::SkillRoundKick + ) { reuse = (MonkSpecialAttack(target, skill_to_use) - 1); - MonkSpecialAttack(target, skill_to_use); - uint32 bDoubleSpecialAttack = (itembonuses.DoubleSpecialAttack + spellbonuses.DoubleSpecialAttack + aabonuses.DoubleSpecialAttack); - if(bDoubleSpecialAttack && (bDoubleSpecialAttack >= 100 || bDoubleSpecialAttack > zone->random.Int(0, 100))) { - int MonkSPA[5] = { EQEmu::skills::SkillFlyingKick, EQEmu::skills::SkillDragonPunch, EQEmu::skills::SkillEagleStrike, EQEmu::skills::SkillTigerClaw, EQEmu::skills::SkillRoundKick }; - MonkSpecialAttack(target, MonkSPA[zone->random.Int(0, 4)]); - int TripleChance = 25; - if (bDoubleSpecialAttack > 100) - TripleChance += (TripleChance * (100 - bDoubleSpecialAttack) / 100); + + // Live AA - Technique of Master Wu + int wuchance = itembonuses.DoubleSpecialAttack + spellbonuses.DoubleSpecialAttack + aabonuses.DoubleSpecialAttack; - if(TripleChance > zone->random.Int(0,100)) - MonkSpecialAttack(target, MonkSPA[zone->random.Int(0, 4)]); + if (wuchance) { + const int MonkSPA[5] = { + EQEmu::skills::SkillFlyingKick, + EQEmu::skills::SkillDragonPunch, + EQEmu::skills::SkillEagleStrike, + EQEmu::skills::SkillTigerClaw, + EQEmu::skills::SkillRoundKick + }; + int extra = 0; + // always 1/4 of the double attack chance, 25% at rank 5 (100/4) + while (wuchance > 0) { + if (zone->random.Roll(wuchance)) { + ++extra; + } + else { + break; + } + wuchance /= 4; + } + + Mob* bo = GetBotOwner(); + if (bo && bo->IsClient() && bo->CastToClient()->GetBotOption(Client::booMonkWuMessage)) { + + bo->Message( + GENERIC_EMOTE, + "The spirit of Master Wu fills %s! %s gains %d additional attack(s).", + GetCleanName(), + GetCleanName(), + extra + ); + } + + auto classic = RuleB(Combat, ClassicMasterWu); + while (extra) { + MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : skill_to_use)); + --extra; + } } reuse *= 1000; @@ -8966,6 +9083,12 @@ void Bot::CalcBotStats(bool showtext) { skills[sindex] = database.GetSkillCap(GetClass(), (EQEmu::skills::SkillType)sindex, GetLevel()); } + taunt_timer.Start(1000); + + if (GetClass() == MONK && GetLevel() >= 10) { + monkattack_timer.Start(1000); + } + LoadAAs(); GenerateSpecialAttacks(); diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index dc9d40aab..9c202fed8 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -3928,6 +3928,16 @@ void bot_command_owner_option(Client *c, const Seperator *sep) "