Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_executable(remote_spell_tests tests/remote_spell.cpp)
target_include_directories(remote_spell_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME remote_spell COMMAND remote_spell_tests)
add_executable(hit_point_tests tests/hit_point.cpp)
target_include_directories(hit_point_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME hit_point COMMAND hit_point_tests)
add_executable(remote_shot_tests tests/remote_shot.cpp)
target_include_directories(remote_shot_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME remote_shot COMMAND remote_shot_tests)
Expand All @@ -115,6 +118,9 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_executable(spell_area_tests tests/spell_area.cpp)
target_include_directories(spell_area_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME spell_area COMMAND spell_area_tests)
add_executable(heal_tests tests/heal.cpp)
target_include_directories(heal_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME heal COMMAND heal_tests)
add_executable(teleport_tests tests/teleport.cpp)
target_include_directories(teleport_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME teleport COMMAND teleport_tests)
Expand Down
12 changes: 12 additions & 0 deletions HeavenClient/Character/Char.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ namespace ms
{
// v83 stretches each frame of an action to delay * (speed + 10) / 16,
// holding the speed to 2..10 (FUN_00453ad1).
if (fixed_action_speed > 0)
return fixed_action_speed;

return std::clamp<int8_t>(get_integer_attackspeed(), 2, 10);
}

Expand Down Expand Up @@ -386,6 +389,7 @@ namespace ms
look.set_action(action);

attacking = true;
fixed_action_speed = 0;
look.set_alerted(5000);
}

Expand All @@ -394,6 +398,7 @@ namespace ms
look.attack(stance);

attacking = true;
fixed_action_speed = 0;
look.set_alerted(5000);
}

Expand All @@ -402,6 +407,7 @@ namespace ms
look.attack(degenerate);

attacking = true;
fixed_action_speed = 0;
look.set_alerted(5000);
}

Expand All @@ -410,6 +416,7 @@ namespace ms
look.attack(degenerate, stance);

attacking = true;
fixed_action_speed = 0;
look.set_alerted(5000);
}

Expand All @@ -418,6 +425,11 @@ namespace ms
look.hold(frame);
}

void Char::fix_action_speed(int8_t speed)
{
fixed_action_speed = speed;
}

void Char::end_attack()
{
look.end_attack();
Expand Down
7 changes: 7 additions & 0 deletions HeavenClient/Character/Char.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ namespace ms
// Hold the attack on this frame until end_attack.
void hold_attack(uint8_t frame);

// Play the action just started at this speed rather than the
// character's own, until the next one. v83 plays the action of a
// skill used without attacking at 6 (0x96d4a6).
void fix_action_speed(int8_t speed);

// End the attack now, held or not.
void end_attack();

Expand Down Expand Up @@ -250,6 +255,8 @@ namespace ms

State state = State::STAND;
bool attacking;
// The speed fixed for the current action, or 0 for the character's own.
int8_t fixed_action_speed = 0;
// The milliseconds of animation time a speed left over from its last
// whole step.
float stance_carry = 0.0f;
Expand Down
5 changes: 3 additions & 2 deletions HeavenClient/Character/Look/BodyDrawInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ namespace ms
} else
{
Stance::Id stance = Stance::by_string(ststr);
int16_t delay = framenode["delay"];
int16_t delay = framenode["delay"].data_type() == nl::node::type::integer
? static_cast<int16_t>(framenode["delay"]) : static_cast<int16_t>(150);

if (delay <= 0)
delay = 100;
delay = 150;

stance_delays[stance][frame] = delay;

Expand Down
20 changes: 6 additions & 14 deletions HeavenClient/Character/Look/BodyDrawInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,12 @@ namespace ms
frame = src["frame"];
move = src["move"];

int16_t sgndelay = src["delay"];

if (sgndelay == 0)
sgndelay = 100;

if (sgndelay > 0)
{
delay = sgndelay;
attackframe = true;
} else if (sgndelay < 0)
{
delay = -sgndelay;
attackframe = false;
}
// v83 takes 150 for a missing delay, but keeps a 0 (FUN_00414d40).
int16_t sgndelay = src["delay"].data_type() == nl::node::type::integer
? static_cast<int16_t>(src["delay"]) : static_cast<int16_t>(150);

delay = sgndelay < 0 ? -sgndelay : sgndelay;
attackframe = sgndelay >= 0;
}

BodyAction()
Expand Down
3 changes: 2 additions & 1 deletion HeavenClient/Character/Look/CharLook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,9 @@ namespace ms
}
} else
{
// A frame of no delay passes at once.
uint16_t delay = action->get_delay();
uint16_t delta = delay - stelapsed;
uint16_t delta = delay > stelapsed ? delay - stelapsed : 0;

if (timestep >= delta)
{
Expand Down
3 changes: 2 additions & 1 deletion HeavenClient/Data/SkillData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ namespace ms
4111004, 4121003, 4221003, 5121002, 11101004, 13101005, 14101006, 15111007, 21100004, 21110004,
21120006
};
// Heal has its own case (0x96a69d), but casts as these do (0x95571f).
static const std::unordered_set<int32_t> spells =
{
2001004, 2001005,
Expand All @@ -199,7 +200,7 @@ namespace ms
2201004, 2201005,
2211002, 2211003, 2211006,
2221001, 2221003, 2221006, 2221007,
2301005,
2301002, 2301005,
2311004,
2321001, 2321007, 2321008,
12001003,
Expand Down
4 changes: 4 additions & 0 deletions HeavenClient/Gameplay/Combat/Attack.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ namespace ms
int32_t mastery = 0;
int32_t amplify = 100;
uint8_t element = 0;
// Heal's `hp` percent, which it hurts the undead with in place of a
// magic attack, and how many it reaches, mobs and party members.
int32_t heal = 0;
int32_t healed = 0;

uint8_t hitcount = 0;
uint8_t mobcount = 0;
Expand Down
131 changes: 99 additions & 32 deletions HeavenClient/Gameplay/Combat/Combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "ChainLightning.h"
#include "DamageLayout.h"
#include "Heal.h"
#include "HitTiming.h"
#include "PointBlank.h"
#include "Prepare.h"
Expand Down Expand Up @@ -312,9 +313,9 @@ namespace ms
return false;

move.apply_useeffects(player);
move.apply_actions(player, Attack::Type::MAGIC);
std::string action = move.apply_actions(player, Attack::Type::MAGIC);
player.fix_action_speed(6);

std::string action = move.get_action(player);
uint16_t lock = action.empty() ? 0 : player.get_action_lock(action);
player.request(UseSkillPacket(moveid, level, lock, *reach), ExclRequest::SKILL_WAIT);

Expand Down Expand Up @@ -359,55 +360,114 @@ namespace ms

if (kinds & skill_use::PARTY)
{
const Party &party = player.get_party();
members = find_party(skill, range, reach.party);

if (!party.exists())
if (!skill_use::sends(skill, reach.party))
return std::nullopt;
}

if (kinds & skill_use::MOBS)
reach.mobs = find_targets(mobs.get_mobs(), range, position, skill_use::mob_limit(stats.mobcount, members),
true, false);

return reach;
}

int32_t Combat::find_party(int32_t skill, Rectangle<int16_t> range, uint8_t &mask) const
{
const Party &party = player.get_party();
int32_t members = 0;

if (!party.exists())
{
mask = skill_use::ALONE;
members = 1;
}

for (const PartyMember &member : party.get_members())
{
bool self = member.id == player.get_oid();
bool dead = false;

if (self)
{
reach.party = skill_use::ALONE;
members = 1;
dead = player.is_dead();
}
else
{
Optional<OtherChar> other = chars.get_char(member.id);

if (!other || !range.contains(other->get_position()))
continue;

for (const PartyMember &member : party.get_members())
dead = other->is_dead();
}

if (skill_use::reaches(skill, self, dead))
{
bool self = member.id == player.get_oid();
bool dead = false;
mask |= skill_use::party_bit(member.slot);
members++;
}
}

if (self)
{
dead = player.is_dead();
}
else
{
Optional<OtherChar> other = chars.get_char(member.id);
return members;
}

if (!other || !range.contains(other->get_position()))
continue;
int32_t Combat::request_heal()
{
int32_t level = player.get_skills().get_level(heal::ID);
const SkillData::Stats &stats = SkillData::get(heal::ID).get_stats(level);
Rectangle<int16_t> range = place_range(player.get_position(), stats.range, 1.0f, !player.getflip());

dead = other->is_dead();
}
UseSkillPacket::Reach reach;
int32_t members = find_party(heal::ID, range, reach.party);

if (skill_use::reaches(skill, self, dead))
{
reach.party |= skill_use::party_bit(member.slot);
members++;
}
}
player.request(UseSkillPacket(heal::ID, level, 0, reach), ExclRequest::SKILL_WAIT);

if (!skill_use::sends(skill, reach.party))
return std::nullopt;
return members;
}

std::vector<int32_t> Combat::find_undead(Rectangle<int16_t> range, uint8_t count) const
{
MapObjects *objs = mobs.get_mobs();
std::multimap<uint32_t, int32_t> candidates;

for (auto &mmo : *objs)
{
const Mob *mob = static_cast<const Mob *>(mmo.second.get());

if (mob && mob->is_alive() && mob->is_undead() && mob->is_in_range(range))
candidates.emplace(objs->entry_order(mob->get_oid()), mob->get_oid());
}

if (kinds & skill_use::MOBS)
reach.mobs = find_targets(mobs.get_mobs(), range, position, skill_use::mob_limit(stats.mobcount, members),
true, false);
std::vector<int32_t> targets;

return reach;
for (auto &candidate : candidates)
{
if (targets.size() >= count)
break;

targets.push_back(candidate.second);
}

return targets;
}

void Combat::apply_move(const SpecialMove &move, int32_t charge, bool held)
{
if (move.is_attack())
{
// Heal asks the server first, as a skill that doesn't attack.
int32_t healed = 0;

if (move.get_id() == heal::ID)
{
if (!player.can_request(ExclRequest::SKILL_WAIT))
return;

healed = request_heal();
}

Attack attack = player.prepare_attack(move.is_skill() ? move.get_id() : 0);
attack.charge = charge;
Stance::Id whack = Stance::Id::NONE;
Expand Down Expand Up @@ -527,6 +587,13 @@ namespace ms
}
}
}
} else if (attack.skill == heal::ID)
{
// It hurts fewer mobs the more of the party it heals, and each
// less the more it reaches.
range = place_range(origin, heal::area(attack.range), 1.0f, attack.toleft);
mob_targets = find_undead(range, skill_use::mob_limit(mobcount, healed));
attack.healed = healed + static_cast<int32_t>(mob_targets.size());
} else
{
mob_targets = find_targets(mob_objs, range, origin, mobcount, true, closest_first);
Expand Down
12 changes: 12 additions & 0 deletions HeavenClient/Gameplay/Combat/Combat.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ namespace ms
// to raise.
std::optional<UseSkillPacket::Reach> find_reach(int32_t skill, int32_t level);

// The party members a skill reaches in `range`, a bit for each in
// `mask`, and how many (0x96db3f).
int32_t find_party(int32_t skill, Rectangle<int16_t> range, uint8_t &mask) const;

// Tell the server whom in the party Heal heals (0x96a69d), and how
// many.
int32_t request_heal();

// The undead in `range` that Heal hurts, at most `count`, in the
// order they entered the map (FUN_00678760).
std::vector<int32_t> find_undead(Rectangle<int16_t> range, uint8_t count) const;

// Use the move. A held skill hits with its `charge`, and its shots
// while held keep the body's action (`held`).
void apply_move(const SpecialMove &move, int32_t charge = 0, bool held = false);
Expand Down
Loading
Loading