Automatic Skill Level Detection

This section is created for developers and non-developers who think that he/she has a good (and realistic) idea that might contribute to the OpenKore community.

Moderator: Moderators

Message
Author
EternalHarvest
Developers
Developers
Posts: 1798
Joined: 05 Dec 2008, 05:42
Noob?: Yes

Re: Automatic Skill Level Detection

#11 Post by EternalHarvest »

xlr82xs wrote:Also, does anyone know if the formula in http://calc.irowiki.org/js/functions/magicAttack.js are correct ?
Cause I really don't want to spend all the time porting them over to fit in the plugin system just to find out they are wrong ;)
You may want to compare them with rAthena (or other server software) code. They may differ from official servers though.
I'm still struggling on figuring out how to avoid having to have one useAttackSkill block per level of each skill/spell you know.
Maybe put/hook that into attack logic, at the place where attack skill blocks are processed. For example, guess skill level needed if no skill level is specified; or with block option, say, smartDamage (we already have smartEncore and noSmartHeal).

This is a good idea, and there may be other things where we can use damage and fight predictions. Will read this topic more fully later.

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#12 Post by xlr82xs »

OK, so I've got a bit more of this finished, and I've changed the config syntax around a bit.

Updated version of monsterDB.pl looks like :

Code: Select all

############################
# MonsterDB plugin for OpenKore by Damokles
#
# This software is open source, licensed under the GNU General Public
# License, version 2.
#
# This plugin extends all functions which use 'checkMonsterCondition'.
# Basically these are AttackSkillSlot, equipAuto, AttackComboSlot, monsterSkill.
#
# Following new checks are possible:
#
# target_Element (list)
# target_notElement (list)
# target_Race (list)
# target_notRace (list)
# target_Size (list)
# target_notSize (list)
# target_hpLeft (range)
#
# In equipAuto you have to leave the target_ part,
# this is due some coding inconsistency in the funtions.pl
#
# You can use monsterEquip if you think that equipAuto is to slow.
# It supports the new equip syntax. It is event-driven and is called
# when a monster: is attacked, changes status, changes element
#
# Note: It will check all monsterEquip blocks but it respects priority.
# If you check in the first block for element fire and in the second
# for race Demi-Human and in both you use different arrows but in the
# Demi-Human block you use a bow, it will take the arrows form the first
# matching block and equip the bow since the fire block didn't specified it.
#
#
# Note: monsterEquip will modify your attackEquip_{slot} so don't be surprised
# about having other attackEquips as you set before.
#
# Be careful with right and leftHand those slots will not be checked for
# two-handed weapons that may conflict.
#
# Example:
# monsterEquip {
# 	target_Element Earth
# 	equip_arrow Fire Arrow
# }
#
# For the element names just scroll a bit down and you'll find it.
# You can check for element Lvls too, eg. target_Element Dark4
#
# $Revision: 5549 $
# $Id: monsterDB.pl 5549 2007-03-21 00:55:47Z h4rry_84 $
############################

package monsterDB;

use 5.010;
use strict;
use Plugins;
use Globals;
use Settings;
use Log qw(message warning error debug);
use Misc qw(bulkConfigModify);
use Translation qw(T TF);
use Utils;
use Data::Dumper;

Plugins::register('monsterDB', 'extends Monster infos', \&onUnload);
my $hooks = Plugins::addHooks(
	['checkMonsterCondition', \&extendedCheck, undef],
	['packet_skilluse', \&onPacketSkillUse, undef],
	['packet/skill_use_no_damage', \&onPacketSkillUseNoDamage, undef],
	['packet_attack', \&onPacketAttack, undef],
	['attack_start', \&onAttackStart, undef],
	['changed_status', \&onStatusChange, undef],
);


my @monsterDB;
my @element_lut = qw(Neutral Water Earth Fire Wind Poison Holy Shadow Ghost Undead);
my @race_lut = qw(Formless Undead Brute Plant Insect Fish Demon Demi-Human Angel Dragon);
my @size_lut = qw(Small Medium Large);
my %skillChangeElement = qw(
	NPC_CHANGEWATER Water
	NPC_CHANGEGROUND Earth
	NPC_CHANGEFIRE Fire
	NPC_CHANGEWIND Wind
	NPC_CHANGEPOISON Poison
	NPC_CHANGEHOLY Holy
	NPC_CHANGEDARKNESS Shadow
	NPC_CHANGETELEKINESIS Ghost
);
my %element_modifiers;

my %raw_modifiers;
$raw_modifiers{lvl1} = "
100     100     100     100     100     100     100     100     25      100
100     25      100     150     50      100     75      100     100     100
100     100     100     50      150     100     75      100     100     100
100     50      150     25      100     100     75      100     100     125
100     175     50      100     25      100     75      100     100     100
100     100     125     125     125     0       75      50      100     -25
100     100     100     100     100     100     0       125     100     150
100     100     100     100     100     50      125     0       100     -25
25      100     100     100     100     100     75      75      125     100
100     100     100     100     100     50      100     0       100     0";

$raw_modifiers{lvl2} = "
100     100     100     100     100     100     100     100     25      100
100     0       100     175     25      100     50      75      100     100
100     100     50      25      175     100     50      75      100     100
100     25      175     0       100     100     50      75      100     150
100     175     25      100     0       100     50      75      100     100
100     75      125     125     125     0       50      25      75      -50
100     100     100     100     100     100     -25     150     100     175
100     100     100     100     100     25      150     -25     100     -50
0       75      75      75      75      75      50      50      150     125
100     75      75      75      75      25      125     0       100     0";

$raw_modifiers{lvl3} = "
100     100     100     100     100     100     100     100     0       100
100     -25     100     200     0       100     25      50      100     125
100     100     0       0       200     100     25      50      100     75
100     0       200     -25     100     100     25      50      100     175
100     200     0       100     -25     100     25      50      100     100
100     50      100     100     100     0       25      0       50      -75
100     100     100     100     100     125     -50     175     100     200
100     100     100     100     100     0       175     -50     100     -75
0       50      50      50      50      50      25      25      175     150
100     50      50      50      50      0       150     0       100     0
";

$raw_modifiers{lvl4} = "
100     100     100     100     100     100     100     100     0       100
100     -50     100     200     0       75      0       25      100     150
100     100     -25     0       200     75      0       25      100     50
100     0       200     -50     100     75      0       25      100     200
100     200     0       100     -50     75      0       25      100     100
100     25      75      75      75      0       0       -25     25      -100
100     75      75      75      75      125     -100    200     100     200
100     75      75      75      75      -25     200     -100    100     -100
0       25      25      25      25      25      0       0       200     175
100     25      25      25      25      -25     175     0       100     0
";

for my $tlevel (1 .. 4) {
		my $x;
        foreach (split /^/ , $raw_modifiers{'lvl'.$tlevel}) {
                next unless m/^\w+/;
                my $base = $element_lut[$x++];
                my @emodifiers = ( split );
                for my $i (0 .. $#element_lut) {
                        $element_modifiers{$element_lut[$i],$tlevel}->{$base} = $emodifiers[$i] / 100;
                }
        }
		delete $raw_modifiers{'lvl'.$tlevel};
}
undef %raw_modifiers;

# can be accessed now as $element_modifiers{"target_element"}{"skill_element"} which returns a multiplier

debug ("MonsterDB: Finished init.\n",'monsterDB',2);
loadMonDB(); # Load MonsterDB into Memory

sub onUnload {
	Plugins::delHooks($hooks);
	@monsterDB = undef;
}

sub loadMonDB {
	@monsterDB = undef;
	my @temp;
	debug ("MonsterDB: Loading DataBase\n",'monsterDB',2);
	my $file = Settings::getTableFilename('monsterDB.txt');
	error ("MonsterDB: cannot load $file\n",'monsterDB',0) unless (-r $file);
	{ open my $fp, '<', $file; @temp = <$fp> }
	my $i = 0;
	foreach my $line (@temp) {
		next unless ($line =~ /(\d{4})\s+(\d+)\s+(\d)\s+(\d)\s+(\d+)/);
		$monsterDB[(int($1) - 1000)] = [$2,$3,$4,$5];
		$i++;
	}
	message TF("%d monsters in database\n", $i), 'monsterDB';
}

sub extendedCheck {
	my (undef, $args) = @_;
	
	return 0 if !$args->{monster} || $args->{monster}->{nameID} eq '';

	my $monsterInfo = $monsterDB[(int($args->{monster}->{nameID}) - 1000)];

	if (!defined $monsterInfo) {
		debug("monsterDB: Monster {$args->{monster}->{name}} not found\n", 'monsterDB', 2);
		return 0;
	} #return if monster is not in DB


	my $element = $element_lut[($monsterInfo->[3] % 10)];
	my $element_lvl = int($monsterInfo->[3] / 20);
	my $race = $race_lut[$monsterInfo->[2]];
	my $size = $size_lut[$monsterInfo->[1]];
	my $skillBlock;
	($skillBlock = $args->{prefix}) =~ s/_target//;

	if ($args->{monster}->{element} && $args->{monster}->{element} ne '') {
		$element = $args->{monster}->{element};
		debug("monsterDB: Monster $args->{monster}->{name} has changed element to $args->{monster}->{element}\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_STONECURSE, BODYSTATE_STONECURSE_ING')) {
		$element = 'Earth';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is petrified changing element to Earth\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_FREEZING')) {
		$element = 'Water';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is frozen changing element to Water\n", 'monsterDB', 3);
	}

	if ($config{$args->{prefix} . '_Element'}
	&& (!existsInList($config{$args->{prefix} . '_Element'},$element)
		&& !existsInList($config{$args->{prefix} . '_Element'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notElement'}
	&& (existsInList($config{$args->{prefix} . '_notElement'},$element)
		|| existsInList($config{$args->{prefix} . '_notElement'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Race'}
	&& !existsInList($config{$args->{prefix} . '_Race'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notRace'}
	&& existsInList($config{$args->{prefix} . '_notRace'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Size'}
	&& !existsInList($config{$args->{prefix} . '_Size'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notSize'}
	&& existsInList($config{$args->{prefix} . '_notSize'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_hpLeft'}
	&& !inRange(($monsterInfo->[0] + $args->{monster}->{deltaHp}),$config{$args->{prefix} . '_hpLeft'})) {
	return $args->{return} = 0;
	}
	
	my $matkstatus = int((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3)) + (($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))/abs((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))*2));
	my $matkav = $char->{attack_magic_max} + $matkstatus;
	my $skillLevel = $config{$skillBlock.'_lvl'};
	my $powerMultiplier = 1;
	if ($char->{'statuses'}->{'EFST_MAGICPOWER'}) {
		$powerMultiplier += $char->{'skills'}->{'HW_MAGICPOWER'}->{'lv'} * 0.05;
		debug("I have Magic Power active, which makes my modifier $powerMultiplier\n", 'monsterDB', 1);
	}
	my $elementalMultiplier = 1;
	if ($config{$skillBlock.'_damageType'}) {
		$elementalMultiplier = $element_modifiers{$element,$element_lvl}{$config{$skillBlock.'_damageType'}};
		debug("$config{$skillBlock.'_damageType'} on a $element$element_lvl gives a multiplier of $elementalMultiplier\n", 'monsterDB', 1);
	}
	my $formula = $config{$skillBlock.'_damageFormula'};
	$formula =~ s/mATK/\$matkav/;
	$formula =~ s/sLVL/\$skillLevel/;
	$formula = int(eval($formula));
	$formula *= $powerMultiplier;
	$formula *= $elementalMultiplier;

	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterInfo->[0] + $args->{monster}->{deltaHp}),'>= '.$formula)) {
		debug("Rejected $config{$skillBlock} with estimated damage : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 0;
	}
	
	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterInfo->[0] + $args->{monster}->{deltaHp}),'< '.$formula)) {
		debug("I think my damage will be around : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 1;
	}

	return 1;
}

sub onPacketSkillUse { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{disp}) if $_[1]->{disp} }

sub onPacketSkillUseNoDmg {
	my (undef,$args) = @_;
	return 1 unless $monsters{$args->{targetID}} && $monsters{$args->{targetID}}{nameID};
	if (
		$args->{targetID} eq $args->{sourceID} && $args->{targetID} ne $accountID
		&& $skillChangeElement{$args->{skillID}}
	) {
		$monsters{$args->{targetID}}{element} = $skillChangeElement{$args->{skillID}};
		monsterEquip($monsters{$args->{targetID}});
		return 1;
	}
}

sub onPacketAttack { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{msg}) if $_[1]->{msg} }

sub monsterHp {
	my ($monster, $message) = @_;
	return 1 unless $monster && $monster->{nameID};
	
	return 1 unless my $monsterInfo = $monsterDB[(int($monster->{nameID}) - 1000)];
	$$message =~ s~(?=\n)~TF(" (HP: %d/%d)", $monsterInfo->[0] + $monster->{deltaHp}, $monsterInfo->[0])~se;
}

sub onAttackStart {
	my (undef,$args) = @_;
	monsterEquip($monsters{$args->{ID}});
}

sub onStatusChange {
	my (undef, $args) = @_;

	return unless $args->{changed};
	my $actor = $args->{actor};
	return unless (UNIVERSAL::isa($actor, 'Actor::Monster'));
	my $index = binFind(\@ai_seq, 'attack');
	return unless defined $index;
	return unless $ai_seq_args[$index]->{target} == $actor->{ID};
	monsterEquip($actor);
}

sub monsterEquip {
	my $monster = shift;
	return unless $monster;
	my %equip_list;

	my %args = ('monster' => $monster);
	my $slot;

	for (my $i=0;exists $config{"monsterEquip_$i"};$i++) {
		$args{prefix} = "monsterEquip_${i}_target";
		if (extendedCheck(undef,\%args)) {
			foreach $slot (%equipSlot_lut) {
				if ($config{"monsterEquip_${i}_equip_$slot"}
				&& !$equip_list{"attackEquip_$slot"}
				&& defined Actor::Item::get($config{"monsterEquip_${i}_equip_$slot"})) {
					$equip_list{"attackEquip_$slot"} = $config{"monsterEquip_${i}_equip_$slot"};
					debug "monsterDB: using ".$config{"monsterEquip_${i}_equip_$slot"}."\n",'monsterDB';
				}
			}
		}
	}
	foreach (keys %equip_list) {
		$config{$_} = $equip_list{$_};
	}
	Actor::Item::scanConfigAndEquip('attackEquip');
}

1;
and the Cold Bolt config section for an example. Pay attention to the Cold Bolt level 10 block, this is a fallback block which ensures that the highest level of Cold Bolt that you know (even if it is less than 10) gets cast if you can not one hit kill the target with a lower level.

Code: Select all

attackSkillSlot Cold Bolt {
	lvl 1
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 2
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 3
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 4
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 5
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 6
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 7
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 8
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 9
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
	damageType Water
	damageFormula mATK * sLVL
}

attackSkillSlot Cold Bolt {
	lvl 10
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Fire
}
Please note, there will of course be some issues doing that with Lightning Bolt once you get to Jupitel Thunder (you'll have to actually modify the config block at that point, so it checks JT instead of just using your highest level Lightning Bolt)

Also, I'm currently assuming that you aren't selecting skills like Storm Gust by potential damage, because I have made it assume that your Mystic Power / Amplify Power damage bonus applies over all the skill's damage, not just the first hit.

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#13 Post by xlr82xs »

EternalHarvest wrote: Maybe put/hook that into attack logic, at the place where attack skill blocks are processed. For example, guess skill level needed if no skill level is specified; or with block option, say, smartDamage (we already have smartEncore and noSmartHeal).
Potentially I could do something like implement a Fallthrough option that tells it that if you can't do it at max level of this skill (and potentially additional conditions are met) then switch to skill "x" instead, so if you can't one hit kill with Lightning Bolt, but have Jupitel Thunder it would cast JT instead. If I can get that working, (I should just be able to queue the fallthrough skill into the top of the AI stack) it will alleviate the need for the multiple config blocks per skill, but until then, I think you need to be able to do something like :

Code: Select all

attackSkillSlot Lightning Bolt {
	lvl 1
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * sLVL
}

attackSkillSlot Lightning Bolt {
	lvl 2
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * sLVL
}

attackSkillSlot Jupitel Thunder {
	lvl 1
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * (sLVL + 2)
}

attackSkillSlot Jupitel Thunder {
	lvl 2
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * (sLVL + 2)
}

attackSkillSlot Jupitel Thunder {
	lvl 3
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * (sLVL + 2)
}

attackSkillSlot Jupitel Thunder {
	lvl 4
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
	damageFormula mATK * (sLVL + 2)
}

attackSkillSlot Jupitel Thunder {
	lvl 5
	dist 9
	whenStatusInactive Action Delay
	sp > 10
	notInTown 1
	target_Element Water
}
I will also need to implement a flag that lets you select between using maximum possible damage, average possible damage or least possible damage during the calculations.

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#14 Post by xlr82xs »

What are people's thoughts to requiring an additional dependancy or two for this functionality ?

mob_db.txt as used by the servers use a bitmask to determine the capabilities of the monsters. I COULD turn this into a bunch of boolean variables in monsterDB.txt using the updateMonsterDB.pl script, or (more efficiently I think) we could use the enum package.
eAthena wrote: Bit Legend:
-------------------------------------------------------------------------------

MD_CANMOVE | 0x0001 | 1
MD_LOOTER | 0x0002 | 2
MD_AGGRESSIVE | 0x0004 | 4
MD_ASSIST | 0x0008 | 8
MD_CASTSENSOR_IDLE | 0x0010 | 16
MD_BOSS | 0x0020 | 32
MD_PLANT | 0x0040 | 64
MD_CANATTACK | 0x0080 | 128
MD_DETECTOR | 0x0100 | 256
MD_CASTSENSOR_CHASE | 0x0200 | 512
MD_CHANGECHASE | 0x0400 | 1024
MD_ANGRY | 0x0800 | 2048
MD_CHANGETARGET_MELEE | 0x1000 | 4096
MD_CHANGETARGET_CHASE | 0x2000 | 8192
MD_TARGETWEAK | 0x4000 | 16384
MD_RANDOMTARGET | 0x8000 | 32768 (not implemented)

Explanation for modes:
-------------------------------------------------------------------------------

CanMove: Enables the mob to move/chase characters.

CanAttack: Enables the mob to attack/retaliate when you are within attack
range. Note that this only enables them to use normal attacks, skills are
always allowed.

Looter: The mob will loot up nearby items on the ground when it's on idle state.

Aggressive: normal aggressive mob, will look for a close-by player to attack.

Assist: When a nearby mob of the same class attacks, assist types will join them.

Cast Sensor Idle: Will go after characters who start casting on them if idle
or walking (without a target).

Cast Sensor Chase: Will go after characters who start casting on them if idle
or chasing other players (they switch chase targets)

Boss: Special flag which makes mobs immune to certain status changes and skills.

Plant: Always receives 1 damage from attacks.

Detector: Enables mob to detect and attack characters who are in hiding/cloak.

ChangeChase: Allows chasing mobs to switch targets if another player happens
to be within attack range (handy on ranged attackers, for example)

Angry: These mobs are "hyper-active". Apart from "chase"/"attack", they have
the states "follow"/"angry". Once hit, they stop using these states and use
the normal ones. The new states are used to determine a different skill-set
for their "before attacked" and "after attacked" states. Also, when
"following", they automatically switch to whoever character is closest.

Change Target Melee: Enables a mob to switch targets when attacked while
attacking someone else.

Change Target Chase: Enables a mob to switch targets when attacked while
chasing another character.

Target Weak: Allows aggressive monsters to only be aggressive against
characters that are five levels below it's own level.
For example, a monster of level 104 will not pick fights with a level 99.

Random Target: Picks a new random target in range on each attack / skill.
(not implemented)
Using the scorpion as a test example with the enum package :

Code: Select all

#!/usr/bin/perl

use 5.010;
use strict;
use warnings;
use Data::Dumper;

use enum qw(BITMASK:MD_ CANMOVE LOOTER AGGRESSIVE ASSIST CASTSENSOR_IDLE BOSS PLANT CANATTACK DETECTOR CASTSENSOR_CHASE CHANGECHASE ANGRY CHANGETARGET_MELEE CHANGETARGET_CHASE TARGETWEAK RANDOMTARGET);

my $test = 0x3195;

if ($test & MD_CANMOVE) {
	print "Enables the mob to move/chase characters.\n";
}

if ($test & MD_CANATTACK) {
	print "Enables the mob to attack/retaliate when you are within attack range. Note that this only enables them to use normal attacks, skills are always allowed.\n";
}

if ($test & MD_LOOTER) {
	print "The mob will loot up nearby items on the ground when it's on idle state.\n";
}

if ($test & MD_AGGRESSIVE) {
	print "Normal aggressive mob, will look for a close-by player to attack.\n";
}

if ($test & MD_ASSIST) {
	print "When a nearby mob of the same class attacks, assist types will join them.\n";
}

if ($test & MD_CASTSENSOR_IDLE) {
	print "Will go after characters who start casting on them if idle or walking (without a target).\n";
}

if ($test & MD_CASTSENSOR_CHASE) {
	print "Will go after characters who start casting on them if idle or chasing other players (they switch chase targets)\n";
}

if ($test & MD_BOSS) {
	print "Special flag which makes mobs immune to certain status changes and skills.\n";
}

if ($test & MD_PLANT) {
	print "Always receives 1 damage from attacks.\n";
}

if ($test & MD_DETECTOR) {
	print "Enables mob to detect and attack characters who are in hiding/cloak.\n";
}

if ($test & MD_CHANGECHASE) {
	print "Allows chasing mobs to switch targets if another player happens to be within attack range (handy on ranged attackers, for example)\n";
}

if ($test & MD_ANGRY) {
	print "These mobs are \"hyper-active\". Apart from \"chase\"/\"attack\", they have the states \"follow\"/\"angry\". Once hit, they stop using these states and use the normal ones. The new states are used to determine a different skill-set for their \"before attacked\" and \"after attacked\" states. Also, when \"following\", they automatically switch to whoever character is closest.\n";
}

if ($test & MD_CHANGETARGET_MELEE) {
	print "Enables a mob to switch targets when attacked while attacking someone else.\n";
}

if ($test & MD_CHANGETARGET_CHASE) {
	print "Enables a mob to switch targets when attacked while chasing another character.\n";
}

if ($test & MD_TARGETWEAK) {
	print "Allows aggressive monsters to only be aggressive against characters that are five levels below it's own level. For example, a monster of level 104 will not pick fights with a level 99.\n";
}

if ($test & MD_RANDOMTARGET) {
	print "Picks a new random target in range on each attack / skill.\n";
}


xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#15 Post by xlr82xs »

OK, more housekeeping stuff.

I've changed update_MonsterDB.pl so it's actually readable, and it pulls in the MDEF and Mode of mobs.
Thinking about this we could/should probably just skip the whole monsterDB.txt thing and read straight from mob_db.txt at some point (if I or someone else ever extends this past skill use)

Code: Select all

#!/usr/bin/perl -w

############################
# eAthena/Cronus to MonsterDB monster table converter by iMikeLance
############################
use utf8;
use strict;
use warnings;

# Renewal mon_db: https://cronusemulator.svn.sourceforge.net/svnroot/cronusemulator/Server/branches/Renewal/db/mob_db.txt
# Pre-renewal mon_db: https://cronusemulator.svn.sourceforge.net/svnroot/cronusemulator/Server/trunk/db/mob_db.txt

open DB, "<", "mob_db.txt" or die "Are you sure that mob_db.txt exists? Internal error: ".$!;
open EXT, ">", "monsterDB.txt" or die $!;

while (<DB>) {
	next unless m/^(\d{4}),/;
	my ($ID, $Sprite_Name, $kROName, $iROName, $LV, $HP, $SP, $EXP, $JEXP, $Range1, $ATK1, $ATK2, $DEF, $MDEF, $STR, $AGI, $VIT, $INT, $DEX, $LUK, $Range2, $Range3, $Scale, $Race, $Element, $Mode, $Speed, $aDelay, $aMotion, $dMotion, $MEXP, $ExpPer, $MVP1id, $MVP1per, $MVP2id, $MVP2per, $MVP3id, $MVP3per, $Drop1id, $Drop1per, $Drop2id, $Drop2per, $Drop3id, $Drop3per, $Drop4id, $Drop4per, $Drop5id, $Drop5per, $Drop6id, $Drop6per, $Drop7id, $Drop7per, $Drop8id, $Drop8per, $Drop9id, $Drop9per, $DropCardid, $DropCardper) = split /,/;
	$Mode =~ s/0x//;
	print EXT "$ID $HP $Scale $Race $Element $Mode $MDEF\n";
}

close(DB);
close(EXT);

print "Finished !\n";

franibaflo
Human
Human
Posts: 35
Joined: 06 May 2012, 10:11
Noob?: Yes

Re: Automatic Skill Level Detection

#16 Post by franibaflo »

xlr82xs wrote:What are people's thoughts to requiring an additional dependancy or two for this functionality ?
An added functionality is always welcome ^^,

I'm glad that this project is moving at a really decent phase. Keep it up guys! Cheers to xlr82xs and EternalHarvest! :D

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#17 Post by xlr82xs »

Infact.

Here is the initial change of monsterDB.pl to use mob_db.txt instead of monsterDB.txt

There may be a couple of bugs, and certainly it's not as efficient as it could be.

Code: Select all

############################
# MonsterDB plugin for OpenKore by Damokles
#
# This software is open source, licensed under the GNU General Public
# License, version 2.
#
# This plugin extends all functions which use 'checkMonsterCondition'.
# Basically these are AttackSkillSlot, equipAuto, AttackComboSlot, monsterSkill.
#
# Following new checks are possible:
#
# target_Element (list)
# target_notElement (list)
# target_Race (list)
# target_notRace (list)
# target_Size (list)
# target_notSize (list)
# target_hpLeft (range)
#
# In equipAuto you have to leave the target_ part,
# this is due some coding inconsistency in the funtions.pl
#
# You can use monsterEquip if you think that equipAuto is to slow.
# It supports the new equip syntax. It is event-driven and is called
# when a monster: is attacked, changes status, changes element
#
# Note: It will check all monsterEquip blocks but it respects priority.
# If you check in the first block for element fire and in the second
# for race Demi-Human and in both you use different arrows but in the
# Demi-Human block you use a bow, it will take the arrows form the first
# matching block and equip the bow since the fire block didn't specified it.
#
#
# Note: monsterEquip will modify your attackEquip_{slot} so don't be surprised
# about having other attackEquips as you set before.
#
# Be careful with right and leftHand those slots will not be checked for
# two-handed weapons that may conflict.
#
# Example:
# monsterEquip {
# 	target_Element Earth
# 	equip_arrow Fire Arrow
# }
#
# For the element names just scroll a bit down and you'll find it.
# You can check for element Lvls too, eg. target_Element Dark4
#
# $Revision: 5549 $
# $Id: monsterDB.pl 5549 2007-03-21 00:55:47Z h4rry_84 $
############################

package monsterDB;

use 5.010;
use strict;
use Plugins;
use Globals;
use Settings;
use Log qw(message warning error debug);
use Misc qw(bulkConfigModify);
use Translation qw(T TF);
use Utils;
use enum qw(BITMASK:MD_ CANMOVE LOOTER AGGRESSIVE ASSIST CASTSENSOR_IDLE BOSS PLANT CANATTACK DETECTOR CASTSENSOR_CHASE CHANGECHASE ANGRY CHANGETARGET_MELEE CHANGETARGET_CHASE TARGETWEAK RANDOMTARGET);

Plugins::register('monsterDB', 'extends Monster infos', \&onUnload);
my $hooks = Plugins::addHooks(
	['checkMonsterCondition', \&extendedCheck, undef],
	['packet_skilluse', \&onPacketSkillUse, undef],
	['packet/skill_use_no_damage', \&onPacketSkillUseNoDamage, undef],
	['packet_attack', \&onPacketAttack, undef],
	['attack_start', \&onAttackStart, undef],
	['changed_status', \&onStatusChange, undef],
);


my %monsterDB;
my @element_lut = qw(Neutral Water Earth Fire Wind Poison Holy Shadow Ghost Undead);
my @race_lut = qw(Formless Undead Brute Plant Insect Fish Demon Demi-Human Angel Dragon);
my @size_lut = qw(Small Medium Large);
my %skillChangeElement = qw(
	NPC_CHANGEWATER Water
	NPC_CHANGEGROUND Earth
	NPC_CHANGEFIRE Fire
	NPC_CHANGEWIND Wind
	NPC_CHANGEPOISON Poison
	NPC_CHANGEHOLY Holy
	NPC_CHANGEDARKNESS Shadow
	NPC_CHANGETELEKINESIS Ghost
);

my %element_modifiers;

my %raw_modifiers;
$raw_modifiers{lvl1} = "
100     100     100     100     100     100     100     100     25      100
100     25      100     150     50      100     75      100     100     100
100     100     100     50      150     100     75      100     100     100
100     50      150     25      100     100     75      100     100     125
100     175     50      100     25      100     75      100     100     100
100     100     125     125     125     0       75      50      100     -25
100     100     100     100     100     100     0       125     100     150
100     100     100     100     100     50      125     0       100     -25
25      100     100     100     100     100     75      75      125     100
100     100     100     100     100     50      100     0       100     0";

$raw_modifiers{lvl2} = "
100     100     100     100     100     100     100     100     25      100
100     0       100     175     25      100     50      75      100     100
100     100     50      25      175     100     50      75      100     100
100     25      175     0       100     100     50      75      100     150
100     175     25      100     0       100     50      75      100     100
100     75      125     125     125     0       50      25      75      -50
100     100     100     100     100     100     -25     150     100     175
100     100     100     100     100     25      150     -25     100     -50
0       75      75      75      75      75      50      50      150     125
100     75      75      75      75      25      125     0       100     0";

$raw_modifiers{lvl3} = "
100     100     100     100     100     100     100     100     0       100
100     -25     100     200     0       100     25      50      100     125
100     100     0       0       200     100     25      50      100     75
100     0       200     -25     100     100     25      50      100     175
100     200     0       100     -25     100     25      50      100     100
100     50      100     100     100     0       25      0       50      -75
100     100     100     100     100     125     -50     175     100     200
100     100     100     100     100     0       175     -50     100     -75
0       50      50      50      50      50      25      25      175     150
100     50      50      50      50      0       150     0       100     0
";

$raw_modifiers{lvl4} = "
100     100     100     100     100     100     100     100     0       100
100     -50     100     200     0       75      0       25      100     150
100     100     -25     0       200     75      0       25      100     50
100     0       200     -50     100     75      0       25      100     200
100     200     0       100     -50     75      0       25      100     100
100     25      75      75      75      0       0       -25     25      -100
100     75      75      75      75      125     -100    200     100     200
100     75      75      75      75      -25     200     -100    100     -100
0       25      25      25      25      25      0       0       200     175
100     25      25      25      25      -25     175     0       100     0
";

for my $tlevel (1 .. 4) {
		my $x;
        foreach (split /^/ , $raw_modifiers{'lvl'.$tlevel}) {
                next unless m/^\w+/;
                my $base = $element_lut[$x++];
                my @emodifiers = ( split );
                for my $i (0 .. $#element_lut) {
                        $element_modifiers{$element_lut[$i],$tlevel}->{$base} = $emodifiers[$i] / 100;
                }
        }
		delete $raw_modifiers{'lvl'.$tlevel};
}
undef %raw_modifiers;

# can be accessed now as $element_modifiers{"target_element"}{"skill_element"} which returns a multiplier

debug ("MonsterDB: Finished init.\n",'monsterDB',2);
loadMonDB(); # Load MonsterDB into Memory

sub onUnload {
	Plugins::delHooks($hooks);
	%monsterDB = undef;
}

sub loadMonDB {
	%monsterDB = undef;
	debug("MonsterDB: Loading Database\n", 'monsterDB', 2);
	my $file = Settings::getTableFilename('mob_db.txt');
	error("MonsterDB: can't load $file\n", 'monsterDB', 0) unless (-r $file);
	open my $fh, "<", $file;
	my $i = 0;
	while (<$fh>) {
		next unless m/^(\d{4}),/;
		my ($ID, $Sprite_Name, $kROName, $iROName, $LV, $HP, $SP, $EXP, $JEXP, $Range1, $ATK1, $ATK2, $DEF, $MDEF, $STR, $AGI, $VIT, $INT, $DEX, $LUK, $Range2, $Range3, $Scale, $Race, $Element, $Mode, $Speed, $aDelay, $aMotion, $dMotion, $MEXP, $ExpPer, $MVP1id, $MVP1per, $MVP2id, $MVP2per, $MVP3id, $MVP3per, $Drop1id, $Drop1per, $Drop2id, $Drop2per, $Drop3id, $Drop3per, $Drop4id, $Drop4per, $Drop5id, $Drop5per, $Drop6id, $Drop6per, $Drop7id, $Drop7per, $Drop8id, $Drop8per, $Drop9id, $Drop9per, $DropCardid, $DropCardper) = split /,/;
		$monsterDB{$ID}{HP} = $HP;
		$monsterDB{$ID}{mDEF} = $MDEF;
		$monsterDB{$ID}{element} = $element_lut[($Element % 10)];
		$monsterDB{$ID}{elementLevel} = int($Element / 20);
		$monsterDB{$ID}{race} = $race_lut[$Race];
		$monsterDB{$ID}{size} = $size_lut[$Scale];
		$monsterDB{$ID}{mode} = $Mode;
		$i++;
	}
	close $fh;
	message TF("%d monsters in database\n", $i), 'monsterDB';		
}

sub extendedCheck {
	my (undef, $args) = @_;
	
	return 0 if !$args->{monster} || $args->{monster}->{nameID} eq '';

	if (!defined $monsterDB{int($args->{monster}->{nameID})}) {
		debug("monsterDB: Monster {$args->{monster}->{name}} not found\n", 'monsterDB', 2);
		return 0;
	} #return if monster is not in DB

	my $ID = int($args->{monster}->{nameID});
	my $element = $monsterDB{$ID}{element};
	my $element_lvl = $monsterDB{$ID}{elementLevel};
	my $race = $monsterDB{$ID}{race};
	my $size = $monsterDB{$ID}{size};
	my $skillBlock;
	($skillBlock = $args->{prefix}) =~ s/_target//;

	if ($args->{monster}->{element} && $args->{monster}->{element} ne '') {
		$element = $args->{monster}->{element};
		debug("monsterDB: Monster $args->{monster}->{name} has changed element to $args->{monster}->{element}\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_STONECURSE, BODYSTATE_STONECURSE_ING')) {
		$element = 'Earth';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is petrified changing element to Earth\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_FREEZING')) {
		$element = 'Water';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is frozen changing element to Water\n", 'monsterDB', 3);
	}

	if ($config{$args->{prefix} . '_Element'}
	&& (!existsInList($config{$args->{prefix} . '_Element'},$element)
		&& !existsInList($config{$args->{prefix} . '_Element'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notElement'}
	&& (existsInList($config{$args->{prefix} . '_notElement'},$element)
		|| existsInList($config{$args->{prefix} . '_notElement'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Race'}
	&& !existsInList($config{$args->{prefix} . '_Race'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notRace'}
	&& existsInList($config{$args->{prefix} . '_notRace'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Size'}
	&& !existsInList($config{$args->{prefix} . '_Size'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notSize'}
	&& existsInList($config{$args->{prefix} . '_notSize'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_hpLeft'}
	&& !inRange(($monsterDB{$ID}->{HP} + $args->{monster}->{deltaHp}),$config{$args->{prefix} . '_hpLeft'})) {
	return $args->{return} = 0;
	}
		
	my $matkstatus = int((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3)) + (($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))/abs((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))*2));
	my $matkav = $char->{attack_magic_max} + $matkstatus;
	my $skillLevel = $config{$skillBlock.'_lvl'};
	my $powerMultiplier = 1;
	if ($char->{'statuses'}->{'EFST_MAGICPOWER'}) {
		$powerMultiplier += $char->{'skills'}->{'HW_MAGICPOWER'}->{'lv'} * 0.05;
		debug("I have Magic Power active, which makes my modifier $powerMultiplier\n", 'monsterDB', 1);
	}
	my $elementalMultiplier = 1;
	if ($config{$skillBlock.'_damageType'}) {
		$elementalMultiplier = $element_modifiers{$element,$element_lvl}{$config{$skillBlock.'_damageType'}};
		debug("$config{$skillBlock.'_damageType'} on a $element$element_lvl gives a multiplier of $elementalMultiplier\n", 'monsterDB', 1);
	}
	my $formula = $config{$skillBlock.'_damageFormula'};
	$formula =~ s/mATK/\$matkav/;
	$formula =~ s/sLVL/\$skillLevel/;
	$formula = int(eval($formula));
	$formula *= $powerMultiplier;
	$formula *= $elementalMultiplier;
	
	if (!($monsterDB{$ID}{mode} & MD_CANMOVE)) {
	   debug("The target is immovable\n", 'monsterDB', 1);
	}

	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterDB{$ID}{HP} + $args->{monster}->{deltaHp}),'>= '.$formula)) {
		debug("Rejected $config{$skillBlock} with estimated damage : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 0;
	}
	
	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterDB{$ID}{HP} + $args->{monster}->{deltaHp}),'< '.$formula)) {
		debug("I think my damage will be around : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 1;
	}

	return 1;
}

sub onPacketSkillUse { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{disp}) if $_[1]->{disp} }

sub onPacketSkillUseNoDmg {
	my (undef,$args) = @_;
	return 1 unless $monsters{$args->{targetID}} && $monsters{$args->{targetID}}{nameID};
	if (
		$args->{targetID} eq $args->{sourceID} && $args->{targetID} ne $accountID
		&& $skillChangeElement{$args->{skillID}}
	) {
		$monsters{$args->{targetID}}{element} = $skillChangeElement{$args->{skillID}};
		monsterEquip($monsters{$args->{targetID}});
		return 1;
	}
}

sub onPacketAttack { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{msg}) if $_[1]->{msg} }

sub monsterHp {
	my ($monster, $message) = @_;
	return 1 unless $monster && $monster->{nameID};
	my $ID = int($monster->{nameID});
	return 1 unless my $monsterInfo = $monsterDB{$ID};
	$$message =~ s~(?=\n)~TF(" (HP: %d/%d)", $monsterDB{$ID}{HP} + $monster->{deltaHp}, $monsterDB{$ID}{HP})~se;
}

sub onAttackStart {
	my (undef,$args) = @_;
	monsterEquip($monsters{$args->{ID}});
}

sub onStatusChange {
	my (undef, $args) = @_;

	return unless $args->{changed};
	my $actor = $args->{actor};
	return unless (UNIVERSAL::isa($actor, 'Actor::Monster'));
	my $index = binFind(\@ai_seq, 'attack');
	return unless defined $index;
	return unless $ai_seq_args[$index]->{target} == $actor->{ID};
	monsterEquip($actor);
}

sub monsterEquip {
	my $monster = shift;
	return unless $monster;
	my %equip_list;

	my %args = ('monster' => $monster);
	my $slot;

	for (my $i=0;exists $config{"monsterEquip_$i"};$i++) {
		$args{prefix} = "monsterEquip_${i}_target";
		if (extendedCheck(undef,\%args)) {
			foreach $slot (%equipSlot_lut) {
				if ($config{"monsterEquip_${i}_equip_$slot"}
				&& !$equip_list{"attackEquip_$slot"}
				&& defined Actor::Item::get($config{"monsterEquip_${i}_equip_$slot"})) {
					$equip_list{"attackEquip_$slot"} = $config{"monsterEquip_${i}_equip_$slot"};
					debug "monsterDB: using ".$config{"monsterEquip_${i}_equip_$slot"}."\n",'monsterDB';
				}
			}
		}
	}
	foreach (keys %equip_list) {
		$config{$_} = $equip_list{$_};
	}
	Actor::Item::scanConfigAndEquip('attackEquip');
}

1;

Last edited by xlr82xs on 18 Sep 2013, 23:01, edited 1 time in total.

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#18 Post by xlr82xs »

Also, if anyone wants to know a bit more about the bitmask thing, there is this.

There is a really spammy bit of debug code implemented in the above that tells you if you're trying to cast on something that can't move, I'll eventually put in the correct checks so you can specify for that in the config block.

And maybe tonight, maybe on the weekend I'll take a look at hooking earlier into the combat cycle so we can use smart blocks.
If I do that, I'm going to probably rename the project, as it's not really a check extension anymore ;)

Also, can anyone think of a reason that Damokles would have originally been using an array instead of a hash ?
I like hashes and hashes of hashes (as you can probably tell), but if there is a good reason I can re-factor to use arrays, I just don't think they are as maintainable.

Also, I haven't noticed any significant delays in loading (or memory use, but I have to admit my development machine is maybe a little higher specced than the average user) using a HoH generator and reading directly from mob_db, if anyone has comments on this, please let me know.

Code: Select all

if (($monsterDB{$ID}{mode} & MD_CANMOVE) == 0) {
	print "Mob is immovable.\n";
}
or

Code: Select all

if (!($monsterDB{$ID}{mode} & MD_CANMOVE)) {
	print "Mob is immovable.\n";
}
I prefer the second style, but I've been told my code is a little hard to follow sometimes (especially if you look at the old CVS commit logs)

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#19 Post by xlr82xs »

franibaflo wrote: Finally the monster database was successfully loaded (I can't believe that I missed out regarding where to put the monsterDB.txt - so stupid of me). For the mob_db.txt can you check the link below. I think it's more updated since the last monster number is 2380 instead of 2125 from cronus.
https://code.google.com/p/rathena/sourc ... xt?r=15394
Excellent, sorry sometimes I don't actually bother reading all the replies (that's VCL's job damnit) to threads.
At a glance https://rathena.googlecode.com/svn-hist ... mob_db.txt does seem newer, but still accurate to iRO renewal.

With the newest code, you can just put that mob_db.txt in your tables folder, completely ignore the update_monsterDB.pl script, and it will get loaded and parsed by monsterDB.pl

This means that if anyone wants to add in checks for things based on the target's VIT (for mobs only of course) or the target's level or whatever you don't need to recreate monsterDB.txt anymore, you just grab the fields you want from the mob_db.txt (I even got rid of that crappy regular expression (this actually needs more testing, i'm not sure if any versions of mob_db.txt will ever have the string "," anywhere other than as a separator, I wouldn't think so, but I'm not sure)).

There are some examples above for pulling values out of the monster modes bitmasks aswell (well, converting the representation into a bitmask and then accessing it).

xlr82xs
Developers
Developers
Posts: 51
Joined: 04 Sep 2013, 19:54
Noob?: No

Re: Automatic Skill Level Detection

#20 Post by xlr82xs »

Ooops, made a mistake in the logic when I pulled the Mode variable from mob_db.txt (I forgot that it would come across as a string value, not a hex value).
Fixed now

Code: Select all

############################
# MonsterDB plugin for OpenKore by Damokles
#
# This software is open source, licensed under the GNU General Public
# License, version 2.
#
# This plugin extends all functions which use 'checkMonsterCondition'.
# Basically these are AttackSkillSlot, equipAuto, AttackComboSlot, monsterSkill.
#
# Following new checks are possible:
#
# target_Element (list)
# target_notElement (list)
# target_Race (list)
# target_notRace (list)
# target_Size (list)
# target_notSize (list)
# target_hpLeft (range)
#
# In equipAuto you have to leave the target_ part,
# this is due some coding inconsistency in the funtions.pl
#
# You can use monsterEquip if you think that equipAuto is to slow.
# It supports the new equip syntax. It is event-driven and is called
# when a monster: is attacked, changes status, changes element
#
# Note: It will check all monsterEquip blocks but it respects priority.
# If you check in the first block for element fire and in the second
# for race Demi-Human and in both you use different arrows but in the
# Demi-Human block you use a bow, it will take the arrows form the first
# matching block and equip the bow since the fire block didn't specified it.
#
#
# Note: monsterEquip will modify your attackEquip_{slot} so don't be surprised
# about having other attackEquips as you set before.
#
# Be careful with right and leftHand those slots will not be checked for
# two-handed weapons that may conflict.
#
# Example:
# monsterEquip {
# 	target_Element Earth
# 	equip_arrow Fire Arrow
# }
#
# For the element names just scroll a bit down and you'll find it.
# You can check for element Lvls too, eg. target_Element Dark4
#
# $Revision: 5549 $
# $Id: monsterDB.pl 5549 2007-03-21 00:55:47Z h4rry_84 $
############################

package monsterDB;

use 5.010;
use strict;
use Plugins;
use Globals;
use Settings;
use Log qw(message warning error debug);
use Misc qw(bulkConfigModify);
use Translation qw(T TF);
use Utils;
use enum qw(BITMASK:MD_ CANMOVE LOOTER AGGRESSIVE ASSIST CASTSENSOR_IDLE BOSS PLANT CANATTACK DETECTOR CASTSENSOR_CHASE CHANGECHASE ANGRY CHANGETARGET_MELEE CHANGETARGET_CHASE TARGETWEAK RANDOMTARGET);

Plugins::register('monsterDB', 'extends Monster infos', \&onUnload);
my $hooks = Plugins::addHooks(
	['checkMonsterCondition', \&extendedCheck, undef],
	['packet_skilluse', \&onPacketSkillUse, undef],
	['packet/skill_use_no_damage', \&onPacketSkillUseNoDamage, undef],
	['packet_attack', \&onPacketAttack, undef],
	['attack_start', \&onAttackStart, undef],
	['changed_status', \&onStatusChange, undef],
);


my %monsterDB;
my @element_lut = qw(Neutral Water Earth Fire Wind Poison Holy Shadow Ghost Undead);
my @race_lut = qw(Formless Undead Brute Plant Insect Fish Demon Demi-Human Angel Dragon);
my @size_lut = qw(Small Medium Large);
my %skillChangeElement = qw(
	NPC_CHANGEWATER Water
	NPC_CHANGEGROUND Earth
	NPC_CHANGEFIRE Fire
	NPC_CHANGEWIND Wind
	NPC_CHANGEPOISON Poison
	NPC_CHANGEHOLY Holy
	NPC_CHANGEDARKNESS Shadow
	NPC_CHANGETELEKINESIS Ghost
);

my %element_modifiers;

my %raw_modifiers;
$raw_modifiers{lvl1} = "
100     100     100     100     100     100     100     100     25      100
100     25      100     150     50      100     75      100     100     100
100     100     100     50      150     100     75      100     100     100
100     50      150     25      100     100     75      100     100     125
100     175     50      100     25      100     75      100     100     100
100     100     125     125     125     0       75      50      100     -25
100     100     100     100     100     100     0       125     100     150
100     100     100     100     100     50      125     0       100     -25
25      100     100     100     100     100     75      75      125     100
100     100     100     100     100     50      100     0       100     0";

$raw_modifiers{lvl2} = "
100     100     100     100     100     100     100     100     25      100
100     0       100     175     25      100     50      75      100     100
100     100     50      25      175     100     50      75      100     100
100     25      175     0       100     100     50      75      100     150
100     175     25      100     0       100     50      75      100     100
100     75      125     125     125     0       50      25      75      -50
100     100     100     100     100     100     -25     150     100     175
100     100     100     100     100     25      150     -25     100     -50
0       75      75      75      75      75      50      50      150     125
100     75      75      75      75      25      125     0       100     0";

$raw_modifiers{lvl3} = "
100     100     100     100     100     100     100     100     0       100
100     -25     100     200     0       100     25      50      100     125
100     100     0       0       200     100     25      50      100     75
100     0       200     -25     100     100     25      50      100     175
100     200     0       100     -25     100     25      50      100     100
100     50      100     100     100     0       25      0       50      -75
100     100     100     100     100     125     -50     175     100     200
100     100     100     100     100     0       175     -50     100     -75
0       50      50      50      50      50      25      25      175     150
100     50      50      50      50      0       150     0       100     0
";

$raw_modifiers{lvl4} = "
100     100     100     100     100     100     100     100     0       100
100     -50     100     200     0       75      0       25      100     150
100     100     -25     0       200     75      0       25      100     50
100     0       200     -50     100     75      0       25      100     200
100     200     0       100     -50     75      0       25      100     100
100     25      75      75      75      0       0       -25     25      -100
100     75      75      75      75      125     -100    200     100     200
100     75      75      75      75      -25     200     -100    100     -100
0       25      25      25      25      25      0       0       200     175
100     25      25      25      25      -25     175     0       100     0
";

for my $tlevel (1 .. 4) {
		my $x;
        foreach (split /^/ , $raw_modifiers{'lvl'.$tlevel}) {
                next unless m/^\w+/;
                my $base = $element_lut[$x++];
                my @emodifiers = ( split );
                for my $i (0 .. $#element_lut) {
                        $element_modifiers{$element_lut[$i],$tlevel}->{$base} = $emodifiers[$i] / 100;
                }
        }
		delete $raw_modifiers{'lvl'.$tlevel};
}
undef %raw_modifiers;

# can be accessed now as $element_modifiers{"target_element"}{"skill_element"} which returns a multiplier

debug ("MonsterDB: Finished init.\n",'monsterDB',2);
loadMonDB(); # Load MonsterDB into Memory

sub onUnload {
	Plugins::delHooks($hooks);
	%monsterDB = undef;
}

sub loadMonDB {
	%monsterDB = undef;
	debug("MonsterDB: Loading Database\n", 'monsterDB', 2);
	my $file = Settings::getTableFilename('mob_db.txt');
	error("MonsterDB: can't load $file\n", 'monsterDB', 0) unless (-r $file);
	open my $fh, "<", $file;
	my $i = 0;
	while (<$fh>) {
		next unless m/^(\d{4}),/;
		my ($ID, $Sprite_Name, $kROName, $iROName, $LV, $HP, $SP, $EXP, $JEXP, $Range1, $ATK1, $ATK2, $DEF, $MDEF, $STR, $AGI, $VIT, $INT, $DEX, $LUK, $Range2, $Range3, $Scale, $Race, $Element, $Mode, $Speed, $aDelay, $aMotion, $dMotion, $MEXP, $ExpPer, $MVP1id, $MVP1per, $MVP2id, $MVP2per, $MVP3id, $MVP3per, $Drop1id, $Drop1per, $Drop2id, $Drop2per, $Drop3id, $Drop3per, $Drop4id, $Drop4per, $Drop5id, $Drop5per, $Drop6id, $Drop6per, $Drop7id, $Drop7per, $Drop8id, $Drop8per, $Drop9id, $Drop9per, $DropCardid, $DropCardper) = split /,/;
		$monsterDB{$ID}{HP} = $HP;
		$monsterDB{$ID}{mDEF} = $MDEF;
		$monsterDB{$ID}{element} = $element_lut[($Element % 10)];
		$monsterDB{$ID}{elementLevel} = int($Element / 20);
		$monsterDB{$ID}{race} = $race_lut[$Race];
		$monsterDB{$ID}{size} = $size_lut[$Scale];
		$monsterDB{$ID}{mode} = hex($Mode);
		$i++;
	}
	close $fh;
	message TF("%d monsters in database\n", $i), 'monsterDB';		
}

sub extendedCheck {
	my (undef, $args) = @_;
	
	return 0 if !$args->{monster} || $args->{monster}->{nameID} eq '';

	if (!defined $monsterDB{int($args->{monster}->{nameID})}) {
		debug("monsterDB: Monster {$args->{monster}->{name}} not found\n", 'monsterDB', 2);
		return 0;
	} #return if monster is not in DB

	my $ID = int($args->{monster}->{nameID});
	my $element = $monsterDB{$ID}{element};
	my $element_lvl = $monsterDB{$ID}{elementLevel};
	my $race = $monsterDB{$ID}{race};
	my $size = $monsterDB{$ID}{size};
	my $skillBlock;
	($skillBlock = $args->{prefix}) =~ s/_target//;

	if ($args->{monster}->{element} && $args->{monster}->{element} ne '') {
		$element = $args->{monster}->{element};
		debug("monsterDB: Monster $args->{monster}->{name} has changed element to $args->{monster}->{element}\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_STONECURSE, BODYSTATE_STONECURSE_ING')) {
		$element = 'Earth';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is petrified changing element to Earth\n", 'monsterDB', 3);
	}

	if ($args->{monster}->statusActive('BODYSTATE_FREEZING')) {
		$element = 'Water';
		$element_lvl = 1;
		debug("monsterDB: Monster $args->{monster}->{name} is frozen changing element to Water\n", 'monsterDB', 3);
	}

	if ($config{$args->{prefix} . '_Element'}
	&& (!existsInList($config{$args->{prefix} . '_Element'},$element)
		&& !existsInList($config{$args->{prefix} . '_Element'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notElement'}
	&& (existsInList($config{$args->{prefix} . '_notElement'},$element)
		|| existsInList($config{$args->{prefix} . '_notElement'},$element.$element_lvl))) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Race'}
	&& !existsInList($config{$args->{prefix} . '_Race'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notRace'}
	&& existsInList($config{$args->{prefix} . '_notRace'},$race)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_Size'}
	&& !existsInList($config{$args->{prefix} . '_Size'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_notSize'}
	&& existsInList($config{$args->{prefix} . '_notSize'},$size)) {
	return $args->{return} = 0;
	}

	if ($config{$args->{prefix} . '_hpLeft'}
	&& !inRange(($monsterDB{$ID}->{HP} + $args->{monster}->{deltaHp}),$config{$args->{prefix} . '_hpLeft'})) {
	return $args->{return} = 0;
	}
	
	if ($config{$args->{prefix} . '_notImmovable'} && (!($monsterDB{$ID}{mode} & MD_CANMOVE))) {
			debug("Will not cast $config{$skillBlock} on an immovable $args->{monster}\n", 'monsterDB', 1);
			return $args->{return} = 0;
	}
			
	my $matkstatus = int((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3)) + (($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))/abs((($char->{lv} / 4) + ($char->{int} + $char->{int_bonus}) + (($char->{int} + $char->{int_bonus}) / 2) + (($char->{dex} + $char->{dex_bonus}) / 5) + (($char->{luk} + $char->{luk_bonus}) / 3))*2));
	my $matkav = $char->{attack_magic_max} + $matkstatus;
	my $skillLevel = $config{$skillBlock.'_lvl'};
	my $powerMultiplier = 1;
	if ($char->{'statuses'}->{'EFST_MAGICPOWER'}) {
		$powerMultiplier += $char->{'skills'}->{'HW_MAGICPOWER'}->{'lv'} * 0.05;
		debug("I have Magic Power active, which makes my modifier $powerMultiplier\n", 'monsterDB', 1);
	}
	my $elementalMultiplier = 1;
	if ($config{$skillBlock.'_damageType'}) {
		$elementalMultiplier = $element_modifiers{$element,$element_lvl}{$config{$skillBlock.'_damageType'}};
		debug("$config{$skillBlock.'_damageType'} on a $element$element_lvl gives a multiplier of $elementalMultiplier\n", 'monsterDB', 1);
	}
	my $formula = $config{$skillBlock.'_damageFormula'};
	$formula =~ s/mATK/\(\$matkav - \$monsterDB\{\$ID\}\{mDEF\}\)/;
	$formula =~ s/sLVL/\$skillLevel/;
	$formula = int(eval($formula));
	$formula *= $powerMultiplier;
	$formula *= $elementalMultiplier;
	
	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterDB{$ID}{HP} + $args->{monster}->{deltaHp}),'>= '.$formula)) {
		debug("Rejected $config{$skillBlock} with estimated damage : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 0;
	}
	
	if ($config{$skillBlock.'_damageFormula'}
	&& inRange(($monsterDB{$ID}{HP} + $args->{monster}->{deltaHp}),'< '.$formula)) {
		debug("I think my damage will be around : $formula using skill level $skillLevel\n", 'monsterDB', 1);
		return $args->{return} = 1;
	}

	return 1;
}

sub onPacketSkillUse { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{disp}) if $_[1]->{disp} }

sub onPacketSkillUseNoDmg {
	my (undef,$args) = @_;
	return 1 unless $monsters{$args->{targetID}} && $monsters{$args->{targetID}}{nameID};
	if (
		$args->{targetID} eq $args->{sourceID} && $args->{targetID} ne $accountID
		&& $skillChangeElement{$args->{skillID}}
	) {
		$monsters{$args->{targetID}}{element} = $skillChangeElement{$args->{skillID}};
		monsterEquip($monsters{$args->{targetID}});
		return 1;
	}
}

sub onPacketAttack { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{msg}) if $_[1]->{msg} }

sub monsterHp {
	my ($monster, $message) = @_;
	return 1 unless $monster && $monster->{nameID};
	my $ID = int($monster->{nameID});
	return 1 unless my $monsterInfo = $monsterDB{$ID};
	$$message =~ s~(?=\n)~TF(" (HP: %d/%d)", $monsterDB{$ID}{HP} + $monster->{deltaHp}, $monsterDB{$ID}{HP})~se;
}

sub onAttackStart {
	my (undef,$args) = @_;
	monsterEquip($monsters{$args->{ID}});
}

sub onStatusChange {
	my (undef, $args) = @_;

	return unless $args->{changed};
	my $actor = $args->{actor};
	return unless (UNIVERSAL::isa($actor, 'Actor::Monster'));
	my $index = binFind(\@ai_seq, 'attack');
	return unless defined $index;
	return unless $ai_seq_args[$index]->{target} == $actor->{ID};
	monsterEquip($actor);
}

sub monsterEquip {
	my $monster = shift;
	return unless $monster;
	my %equip_list;

	my %args = ('monster' => $monster);
	my $slot;

	for (my $i=0;exists $config{"monsterEquip_$i"};$i++) {
		$args{prefix} = "monsterEquip_${i}_target";
		if (extendedCheck(undef,\%args)) {
			foreach $slot (%equipSlot_lut) {
				if ($config{"monsterEquip_${i}_equip_$slot"}
				&& !$equip_list{"attackEquip_$slot"}
				&& defined Actor::Item::get($config{"monsterEquip_${i}_equip_$slot"})) {
					$equip_list{"attackEquip_$slot"} = $config{"monsterEquip_${i}_equip_$slot"};
					debug "monsterDB: using ".$config{"monsterEquip_${i}_equip_$slot"}."\n",'monsterDB';
				}
			}
		}
	}
	foreach (keys %equip_list) {
		$config{$_} = $equip_list{$_};
	}
	Actor::Item::scanConfigAndEquip('attackEquip');
}

1;
I have also implemented the target_notImmovable config option, set it to 1 and you won't cast on immovable objects. Will implement target_notPlant at some point later.

Post Reply