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
3 changes: 3 additions & 0 deletions HeavenClient/Character/Buff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ namespace ms
case 47: return ECHO_OF_HERO;
case 49: return GHOST_MORPH;
case 51: return CONFUSE;
case 75: return STOP_POTION;
case 76: return STOP_MOTION;
case 77: return FEAR;
case 82: return ENERGY_CHARGE;
case 83: return DASH2;
case 84: return DASH;
Expand Down
6 changes: 6 additions & 0 deletions HeavenClient/Character/Buff.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ namespace ms
CURSE,
SLOW,
SEDUCE,
// A mob's Undead shares Puppet's bit (46); its reason tells them
// apart.
UNDEAD,
STOP_POTION,
STOP_MOTION,
FEAR,
LENGTH
};

Expand Down
5 changes: 5 additions & 0 deletions HeavenClient/Character/Char.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,11 @@ namespace ms
return phobj;
}

const PhysicsObject &Char::get_phobj() const
{
return phobj;
}

void Char::init()
{
CharLook::init();
Expand Down
1 change: 1 addition & 0 deletions HeavenClient/Character/Char.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ namespace ms

// Return a reference to this characters's physics.
PhysicsObject &get_phobj();
const PhysicsObject &get_phobj() const;

// Initialize character effects
static void init();
Expand Down
48 changes: 45 additions & 3 deletions HeavenClient/Character/Diseases.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace ms
namespace disease
{
constexpr int32_t DISPEL = 2311001;
constexpr int32_t UNDEAD = 133;

// Under Stop Motion, the ms the player may stand still between hurts
// (0x94b1d7).
constexpr int64_t STILL_TIME = 2000;

// The mob skill a disease's reason names, and its level.
constexpr int32_t skill(int32_t reason)
Expand All @@ -28,7 +33,8 @@ namespace ms
// The stats a mob skill's disease sets.
constexpr Buffstat::Id STATS[] = {
Buffstat::STUN, Buffstat::POISON, Buffstat::SEAL, Buffstat::DARKNESS, Buffstat::WEAKEN,
Buffstat::CURSE, Buffstat::SLOW, Buffstat::SEDUCE, Buffstat::CONFUSE
Buffstat::CURSE, Buffstat::SLOW, Buffstat::SEDUCE, Buffstat::CONFUSE, Buffstat::UNDEAD,
Buffstat::STOP_POTION, Buffstat::STOP_MOTION, Buffstat::FEAR
};

constexpr bool is_disease(Buffstat::Id stat)
Expand All @@ -40,6 +46,16 @@ namespace ms
return false;
}

// The stat a buff's bit gives for `reason`: bit 46 is Puppet's, or
// Undead's when a mob skill gave it (CTS 46 in both).
constexpr Buffstat::Id given(Buffstat::Id stat, int32_t reason)
{
if (stat == Buffstat::PUPPET && skill(reason) == UNDEAD && level(reason) > 0)
return Buffstat::UNDEAD;

return stat;
}

// Which diseases are on the player, with the n of the ones it
// matters for.
struct State
Expand All @@ -48,7 +64,10 @@ namespace ms
bool sealed = false;
bool weakened = false;
bool confused = false;
bool stop_potion = false;
std::optional<int32_t> seduced;
// Stop Motion's n: the HP standing still costs.
std::optional<int32_t> stop_motion;
};

// One frame's arrow input: x is -1 left or 1 right, y -1 up or 1
Expand Down Expand Up @@ -117,6 +136,21 @@ namespace ms
return can_attack(state) && (!state.sealed || skillid == DISPEL);
}

// Items are refused, without a word, under Stop Potion or a
// seduction (FUN_0094fa45, from the quickslots at 0x94f86c and the
// USE_ITEM sender FUN_00a092fb).
constexpr bool can_use_item(const State &state)
{
return !state.stop_potion && !state.seduced;
}

// What Stop Motion takes from a player with `hp` who stood still:
// its n, but never the last HP (0x94b1d7).
constexpr int32_t stop_motion_damage(int32_t n, int32_t hp)
{
return n < hp - 1 ? n : (hp > 1 ? hp - 1 : 0);
}

// The speed the player walks at: Slow's n in place of their own
// (0x92ef56).
constexpr int32_t walk_speed(int32_t speed, std::optional<int32_t> slow)
Expand All @@ -125,8 +159,8 @@ namespace ms
}

// What the player is told when a mob skill gives them a disease
// (FUN_00932da6, StringPool 0xb5a-0xb5f and 0xec2). Slow and Confuse
// say nothing.
// (FUN_00932da6, StringPool 0xb5a-0xb5f, 0xec2, 0xec3 and
// 0x1558-0x155a). Slow and Confuse say nothing.
inline std::string notice(int32_t mobskill)
{
switch (mobskill)
Expand All @@ -138,8 +172,16 @@ namespace ms
case 124: return "You've been cursed, so the amount of EXP you'll receive will decrease.";
case 125: return "You have been poisoned, so your HP will slowly decrease.";
case 128: return "You have been seduced, which disables you from moving.";
case 133: return "You will become an Undead and the effects of your Recovery will diminish by 50%.";
case 134: return "Your current condition will not permit you to use a potion.";
case 135: return "You will receive damage if you become covered with the Shadow of Darkness and cannot move.";
case 136: return "The fear of darkness has overtaken you! Your range of vision has been narrowed.";
default: return "";
}
}

// What the player is told when Stop Motion hurts them (StringPool
// 0x1561, chat type 7).
constexpr const char *STOP_MOTION_HURT = "You receive damage having been covered by the Shadow of Darkness.";
}
}
45 changes: 44 additions & 1 deletion HeavenClient/Character/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ namespace ms
set_chair(0);
nullstate.update_state(*this);
path.reset(current_move(0));
stirred = now_ms();
}

void Player::send_action(KeyAction::Id action, bool down)
Expand All @@ -122,6 +123,16 @@ namespace ms
if (pst && !refused)
pst->send_action(*this, action, down);

// Under Stop Motion, a walking key going down, repeating or up
// (FUN_0094c856, after Confuse swaps them), or a jump, is moving.
bool confused = has_buff(Buffstat::Id::CONFUSE);
bool walks = action == KeyAction::Id::LEFT || action == KeyAction::Id::RIGHT ||
action == (confused ? KeyAction::Id::UP : KeyAction::Id::DOWN);

if (has_buff(Buffstat::Id::STOP_MOTION) && !is_dead() &&
(walks || (action == KeyAction::Id::JUMP && down && !refused)))
stirred = now_ms();

keysdown[action] = down;
}

Expand Down Expand Up @@ -194,7 +205,7 @@ namespace ms
{
if (type == InventoryType::Id::USE && (itemid / 10000 == 228 || itemid / 10000 == 229))
request(SkillBookPacket(slot, itemid), ExclRequest::USE_WAIT);
else if (type == InventoryType::Id::USE)
else if (type == InventoryType::Id::USE && disease::can_use_item(get_diseases()))
request(UseItemPacket(slot, itemid), ExclRequest::USE_WAIT);
else if (type == InventoryType::Id::SETUP && itemid / 10000 == 301)
sit_on_chair(itemid);
Expand Down Expand Up @@ -779,12 +790,44 @@ namespace ms
diseases.weakened = has_buff(Buffstat::Id::WEAKEN);
diseases.confused = has_buff(Buffstat::Id::CONFUSE);

diseases.stop_potion = has_buff(Buffstat::Id::STOP_POTION);

if (has_buff(Buffstat::Id::SEDUCE))
diseases.seduced = get_buff_value(Buffstat::Id::SEDUCE);

if (has_buff(Buffstat::Id::STOP_MOTION))
diseases.stop_motion = get_buff_value(Buffstat::Id::STOP_MOTION);

return diseases;
}

int32_t Player::take_stop_motion()
{
std::optional<int32_t> n = get_diseases().stop_motion;

if (!n || is_dead())
return 0;

int64_t now = now_ms();

// A held walking key repeats, as Windows repeats it.
bool confused = has_buff(Buffstat::Id::CONFUSE);

if (is_held(KeyAction::Id::LEFT) || is_held(KeyAction::Id::RIGHT) ||
is_held(confused ? KeyAction::Id::UP : KeyAction::Id::DOWN))
stirred = now;

if (now - stirred <= disease::STILL_TIME)
return 0;

int32_t damage = disease::stop_motion_damage(*n, stats.get_stat(MapleStat::Id::HP));

if (damage > 0)
stirred = now;

return damage;
}

bool Player::has_buff(Buffstat::Id stat) const
{
return buffs[stat].value > 0;
Expand Down
6 changes: 6 additions & 0 deletions HeavenClient/Character/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ namespace ms

// The diseases on the player.
disease::State get_diseases() const;
// What Stop Motion takes now from a player who stood still too
// long, 0 if nothing (0x94b1d7).
int32_t take_stop_motion();

// Change a skill
void change_skill(int32_t skill_id, int32_t level, int32_t masterlevel, int64_t expiration);
Expand Down Expand Up @@ -327,6 +330,9 @@ namespace ms
ExclRequest excl;

std::map<KeyAction::Id, bool> keysdown;
// When the player last moved under Stop Motion, or entered the map
// (+0x311c).
int64_t stirred = 0;

move_path::Recorder path;

Expand Down
15 changes: 15 additions & 0 deletions HeavenClient/Gameplay/Combat/Combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ namespace ms

result.attacker = player.get_oid();
expect_hits(result, 1);
show_counters(result);
extract_effects(player, move, result, range, reach, shot_area, shot_end);

if (attack.type == Attack::Type::MAGIC)
Expand Down Expand Up @@ -2579,11 +2580,25 @@ namespace ms
mobs.send_attack(result, attack, targets, attack.mobcount);
result.attacker = player.get_oid();
expect_hits(result, 1);
show_counters(result);
extract_effects(player, skill, result, range, 0, range, position);

AttackPacket(result, player.get_position(), position).dispatch();
}

void Combat::show_counters(const AttackResult &result)
{
bool magic = result.type == Attack::Type::MAGIC;

for (const auto &line : result.damagelines)
{
int32_t damage = mobs.counter(line.first, magic);

if (damage > 0)
player.show_damage(damage);
}
}

void Combat::clear_grenades()
{
grenades.clear();
Expand Down
5 changes: 5 additions & 0 deletions HeavenClient/Gameplay/Combat/Combat.h
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ namespace ms
// part as its target is hit (`staggered`) or all at once.
void draw_chain(const Char &user, const SpecialMove &move, const AttackResult &result, bool staggered);

// Each target with a counter hurts the player back; v83 shows its
// own roll over the head and sends nothing (FUN_00953595): the
// server takes the HP.
void show_counters(const AttackResult &result);

// Tell the targets how many hits are coming (sign 1) or cancel them (sign -1),
// so a mob the server kills waits for the hits before dying.
void expect_hits(const AttackResult &result, int32_t sign);
Expand Down
50 changes: 46 additions & 4 deletions HeavenClient/Gameplay/MapleMap/MapMists.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,28 @@ namespace ms
{
constexpr int16_t FADE = 1000;

nl::node mob_skill_level(int32_t skillid, uint8_t level)
{
return nl::nx::skill["MobSkill.img"][std::to_string(skillid)]["level"][std::to_string(level)];
}

// A cash item's mist draws its item's tile (StringPool 0x12eb), a
// hurting mob skill's its level's, and a player's skill's its own.
nl::node tile_node(int32_t type, int32_t skillid, uint8_t level)
{
if (type == 3)
return nl::nx::skill["MobSkill.img"][string_format::extend_id(skillid, 3)]["level"][std::to_string(level)]["tile"];
if (type == mist::ITEM)
return nl::nx::item["Cash"]["0528.img"][string_format::extend_id(skillid, 8)]["tile"];

if (mist::hurts(skillid))
return mob_skill_level(skillid, level)["tile"];

return nl::nx::skill[string_format::extend_id(skillid / 10000, 3) + ".img"]["skill"]
[string_format::extend_id(skillid, 7)]["tile"];
}
}

void MapMists::spawn(int32_t oid, int32_t type, int32_t skillid, uint8_t level, int32_t left, int32_t top,
int32_t right, int32_t bottom)
void MapMists::spawn(int32_t oid, int32_t type, int32_t owner, int32_t skillid, uint8_t level, int16_t delay,
int32_t left, int32_t top, int32_t right, int32_t bottom, int32_t element)
{
nl::node src = tile_node(type, skillid, level);

Expand All @@ -71,7 +81,11 @@ namespace ms
std::stable_sort(layout.begin(), layout.end(),
[](const MistCloud &a, const MistCloud &b) { return a.z < b.z; });

int32_t damage = mist::hurts(skillid) ? static_cast<int32_t>(mob_skill_level(skillid, level)["x"]) : 0;

Mist mist;
mist.area = { type, owner, skillid, level, left, top, right, bottom, element, damage, delay <= 0 };
mist.wait = delay * 100;

for (const MistCloud &cloud : layout)
mist.clouds.push_back({ tiles[cloud.tile], Point<int16_t>(cloud.x, cloud.y), cloud.flip });
Expand All @@ -96,6 +110,12 @@ namespace ms
for (Cloud &cloud : mist.clouds)
cloud.animation.update();

if (!mist.area.started)
{
mist.wait -= Constants::TIMESTEP;
mist.area.started = mist.wait <= 0;
}

if (mist.fading >= 0)
{
mist.fading -= Constants::TIMESTEP;
Expand Down Expand Up @@ -129,4 +149,26 @@ namespace ms
{
mists.clear();
}

bool MapMists::hurt_at(Point<int16_t> position, mist::Hurt &hurt) const
{
return mist::hurt_at(areas(), position.x(), position.y(), hurt);
}

bool MapMists::shelters(Point<int16_t> position, const std::function<bool(int32_t)> &ally) const
{
return mist::shelters(areas(), position.x(), position.y(), ally);
}

// A fading mist is gone to the client, which removes its area at once.
std::vector<mist::Area> MapMists::areas() const
{
std::vector<mist::Area> areas;

for (const auto &iter : mists)
if (iter.second.fading < 0)
areas.push_back(iter.second.area);

return areas;
}
}
Loading
Loading