localplayer.cpp

main 670 lines · 19.6 KB Raw
Alex Alex Commit Client Upload Part 2 19/10/2026 00:06
1/*
2 * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22
23#include "localplayer.h"
24#include "map.h"
25#include "game.h"
26#include "tile.h"
27#include <framework/core/eventdispatcher.h>
28#include <framework/graphics/graphics.h>
29#include <framework/util/extras.h>
30
31LocalPlayer::LocalPlayer()
32{
33 m_states = 0;
34 m_vocation = 0;
35 m_blessings = Otc::BlessingNone;
36 m_walkLockExpiration = 0;
37
38 m_skillsLevel.resize(Otc::LastSkill + 1, 0);
39 m_skillsBaseLevel.resize(Otc::LastSkill + 1, 0);
40 m_skillsLevelPercent.resize(Otc::LastSkill + 1, 0);
41
42 m_health = -1;
43 m_maxHealth = -1;
44 m_freeCapacity = -1;
45 m_experience = -1;
46 m_level = -1;
47 m_levelPercent = -1;
48 m_mana = -1;
49 m_maxMana = -1;
50 m_magicLevel = -1;
51 m_magicLevelPercent = -1;
52 m_baseMagicLevel = -1;
53 m_soul = -1;
54 m_stamina = -1;
55 m_baseSpeed = -1;
56 m_regenerationTime = -1;
57 m_offlineTrainingTime = -1;
58 m_totalCapacity = -1;
59}
60
61void LocalPlayer::draw(const Point& dest, bool animate, LightView* lightView)
62{
63 Creature::draw(dest, animate, lightView);
64}
65
66
67void LocalPlayer::lockWalk(int millis)
68{
69 m_walkLockExpiration = std::max<int>(m_walkLockExpiration, (ticks_t) g_clock.millis() + millis);
70}
71
72bool LocalPlayer::canWalk(Otc::Direction direction, bool ignoreLock)
73{
74 // cannot walk while locked
75 if ((m_walkLockExpiration != 0 && g_clock.millis() < m_walkLockExpiration) && !ignoreLock)
76 return false;
77
78 // paralyzed
79 if (m_speed == 0)
80 return false;
81
82 // last walk is not done yet
83 if (m_walking && (m_walkTimer.ticksElapsed() < getStepDuration()) && !isAutoWalking() && !isServerWalking())
84 return false;
85
86 auto tile = g_map.getTile(getPrewalkingPosition(true));
87 if (isPreWalking() && (!m_lastPrewalkDone || (tile && tile->isBlocking())))
88 return false;
89
90 // cannot walk while already walking
91 if ((m_walking && !isAutoWalking() && !isServerWalking()) && (!isPreWalking() || !m_lastPrewalkDone))
92 return false;
93
94 // Without new walking limit only to 1 prewalk
95 if (!m_preWalking.empty() && !g_game.getFeature(Otc::GameNewWalking))
96 return false;
97
98 // Limit pre walking steps
99 if (m_preWalking.size() >= g_game.getMaxPreWalkingSteps()) { // max 3 extra steps
100 if (m_walkTimer.ticksElapsed() >= getStepDuration() + 300)
101 return true;
102 return false;
103 }
104
105 if (!m_preWalking.empty()) { // disallow diagonal extented prewalking walking
106 auto dir = m_position.getDirectionFromPosition(m_preWalking.back());
107 if ((dir == Otc::NorthWest || dir == Otc::NorthEast || dir == Otc::SouthWest || dir == Otc::SouthEast)) {
108 return false;
109 }
110 if (!g_map.getTile(getPrewalkingPosition())->isWalkable())
111 return false;
112 }
113
114 return true;
115}
116
117void LocalPlayer::walk(const Position& oldPos, const Position& newPos)
118{
119 if (g_extras.debugWalking) {
120 g_logger.info(stdext::format("[%i] LocalPlayer::walk", (int)g_clock.millis()));
121 }
122
123 m_lastAutoWalkRetries = 0;
124 // a prewalk was going on
125 if (isPreWalking()) {
126 for (auto it = m_preWalking.begin(); it != m_preWalking.end(); ++it) {
127 if (*it == newPos) {
128 m_preWalking.erase(m_preWalking.begin(), ++it);
129 if (!isPreWalking()) // reset pre walking
130 updateWalk();
131 return;
132 }
133 }
134 if (g_extras.debugWalking) {
135 g_logger.info(stdext::format("[%i] LocalPlayer::walk invalid prewalk", (int)g_clock.millis()));
136 }
137
138 // invalid pre walk
139 m_preWalking.clear();
140 m_serverWalking = true;
141 if (m_serverWalkEndEvent)
142 m_serverWalkEndEvent->cancel();
143
144 Creature::walk(oldPos, newPos);
145 } else { // no prewalk was going on, this must be an server side automated walk
146 if (g_extras.debugWalking) {
147 g_logger.info(stdext::format("[%i] LocalPlayer::walk server walk", (int)g_clock.millis()));
148 }
149
150 m_serverWalking = true;
151 if (m_serverWalkEndEvent)
152 m_serverWalkEndEvent->cancel();
153 m_lastAutoWalkRetries = 0;
154
155 Creature::walk(oldPos, newPos);
156 }
157}
158
159void LocalPlayer::preWalk(Otc::Direction direction)
160{
161 Position startPos = m_position;
162 if (!m_preWalking.empty())
163 startPos = m_preWalking.back();
164 Position newPos = startPos.translatedToDirection(direction);
165
166 if (m_serverWalkEndEvent)
167 m_serverWalkEndEvent->cancel();
168
169 m_lastPrewalkDone = false;
170 m_preWalking.push_back(newPos);
171 if (m_preWalking.size() > 1)
172 g_map.requestVisibleTilesCacheUpdate();
173
174 Creature::walk(startPos, newPos);
175}
176
177void LocalPlayer::cancelNewWalk(Otc::Direction dir)
178{
179 if (g_extras.debugWalking) {
180 g_logger.info(stdext::format("[%i] cancelWalk", (int)g_clock.millis()));
181 }
182
183 bool clearedPrewalk = !m_preWalking.empty();
184
185 m_preWalking.clear();
186 g_map.requestVisibleTilesCacheUpdate();
187
188 if (clearedPrewalk) {
189 stopWalk();
190 }
191
192 m_idleTimer.restart();
193
194 if (retryAutoWalk()) return;
195
196 if (!g_game.isIgnoringServerDirection() || !g_game.getFeature(Otc::GameNewWalking)) {
197 setDirection(dir);
198 }
199 callLuaField("onCancelWalk", dir);
200}
201
202bool LocalPlayer::predictiveCancelWalk(const Position& pos, uint32_t predictionId, Otc::Direction dir)
203{
204 if (g_extras.debugPredictiveWalking) {
205 g_logger.info(stdext::format("[%i] predictiveCancelWalk: %i - %i", (int)g_clock.millis(), predictionId, (int)m_preWalking.size()));
206 }
207
208 m_walkMatrix.update(pos, predictionId); // for debugging, not used
209
210 for (auto it = m_preWalking.begin(); it != m_preWalking.end(); ++it) {
211 if (*it != pos) {
212 continue;
213 }
214 if (g_extras.debugPredictiveWalking) {
215 g_logger.info(stdext::format("[%i] predictiveCancelWalk: canceling walk", (int)g_clock.millis()));
216 }
217
218 if (it == m_preWalking.begin()) {
219 cancelNewWalk(dir);
220 return true;
221 }
222 while (it != m_preWalking.end()) {
223 it = m_preWalking.erase(it);
224 }
225
226 m_walkTimer.restart();
227 m_walkTimer.adjust(-(getStepDuration(true) + 50));
228 updateWalk();
229 g_map.requestVisibleTilesCacheUpdate();
230
231 return true;
232 }
233
234 return false;
235}
236
237bool LocalPlayer::retryAutoWalk()
238{
239 if (m_autoWalkDestination.isValid()) {
240 g_game.stop();
241 auto self = asLocalPlayer();
242
243 if (m_lastAutoWalkRetries <= 3) {
244 if (m_autoWalkContinueEvent)
245 m_autoWalkContinueEvent->cancel();
246 m_autoWalkContinueEvent = g_dispatcher.scheduleEvent(std::bind(&LocalPlayer::autoWalk, asLocalPlayer(), m_autoWalkDestination, true), 200);
247 self->m_lastAutoWalkRetries += 1;
248 return true;
249 } else {
250 self->m_autoWalkDestination = Position();
251 }
252 }
253 return false;
254}
255
256
257bool LocalPlayer::autoWalk(Position destination, bool retry)
258{
259 // reset state
260 m_autoWalkDestination = Position();
261 m_lastAutoWalkPosition = Position();
262 if (m_autoWalkContinueEvent)
263 m_autoWalkContinueEvent->cancel();
264 m_autoWalkContinueEvent = nullptr;
265
266 if (!retry)
267 m_lastAutoWalkRetries = 0;
268
269 if (destination == getPrewalkingPosition())
270 return true;
271
272 m_autoWalkDestination = destination;
273 auto self(asLocalPlayer());
274 g_map.findPathAsync(getPrewalkingPosition(), destination, [self](PathFindResult_ptr result) {
275 if (self->m_autoWalkDestination != result->destination)
276 return;
277 if (g_extras.debugWalking) {
278 g_logger.info(stdext::format("Async path search finished with complexity %i/50000", result->complexity));
279 }
280
281 if (result->status != Otc::PathFindResultOk) {
282 if (self->m_lastAutoWalkRetries > 0 && self->m_lastAutoWalkRetries <= 3) { // try again in 300, 700, 1200 ms if canceled by server
283 self->m_autoWalkContinueEvent = g_dispatcher.scheduleEvent(std::bind(&LocalPlayer::autoWalk, self, result->destination, true), 200 + self->m_lastAutoWalkRetries * 100);
284 return;
285 }
286 self->m_autoWalkDestination = Position();
287 self->callLuaField("onAutoWalkFail", result->status);
288 return;
289 }
290
291 if (!g_game.getFeature(Otc::GameNewWalking) && result->path.size() > 127)
292 result->path.resize(127);
293 else if (result->path.size() > 4095)
294 result->path.resize(4095);
295
296 if (result->path.empty()) {
297 self->m_autoWalkDestination = Position();
298 self->callLuaField("onAutoWalkFail", result->status);
299 return;
300 }
301
302 auto finalAutowalkPos = self->getPrewalkingPosition().translatedToDirections(result->path).back();
303 if (self->m_autoWalkDestination != finalAutowalkPos) {
304 self->m_lastAutoWalkPosition = finalAutowalkPos;
305 }
306
307 g_game.autoWalk(result->path, result->start);
308 });
309
310 if (!retry)
311 lockWalk();
312 return true;
313}
314
315void LocalPlayer::cancelWalk(Otc::Direction direction)
316{
317 if (g_game.getFeature(Otc::GameNewWalking)) {
318 return;
319 }
320
321 return cancelNewWalk(direction);
322}
323
324void LocalPlayer::stopAutoWalk()
325{
326 m_autoWalkDestination = Position();
327 m_lastAutoWalkPosition = Position();
328
329 if (m_autoWalkContinueEvent) {
330 m_autoWalkContinueEvent->cancel();
331 m_autoWalkContinueEvent = nullptr;
332 }
333}
334
335void LocalPlayer::stopWalk() {
336 if (g_extras.debugWalking) {
337 g_logger.info(stdext::format("[%i] stopWalk", (int)g_clock.millis()));
338 }
339
340 Creature::stopWalk(); // will call terminateWalk
341
342 m_preWalking.clear();
343}
344
345void LocalPlayer::updateWalkOffset(uint8 totalPixelsWalked, bool inNextFrame)
346{
347 // pre walks offsets are calculated in the oposite direction
348 if(isPreWalking()) {
349 Point& walkOffset = inNextFrame ? m_walkOffsetInNextFrame : m_walkOffset;
350 walkOffset = Point(0,0);
351 if(m_walkDirection == Otc::North || m_walkDirection == Otc::NorthEast || m_walkDirection == Otc::NorthWest)
352 walkOffset.y = -totalPixelsWalked;
353 else if(m_walkDirection == Otc::South || m_walkDirection == Otc::SouthEast || m_walkDirection == Otc::SouthWest)
354 walkOffset.y = totalPixelsWalked;
355
356 if(m_walkDirection == Otc::East || m_walkDirection == Otc::NorthEast || m_walkDirection == Otc::SouthEast)
357 walkOffset.x = totalPixelsWalked;
358 else if(m_walkDirection == Otc::West || m_walkDirection == Otc::NorthWest || m_walkDirection == Otc::SouthWest)
359 walkOffset.x = -totalPixelsWalked;
360 } else
361 Creature::updateWalkOffset(totalPixelsWalked, inNextFrame);
362}
363
364void LocalPlayer::updateWalk()
365{
366 if (!m_walking)
367 return;
368
369 Creature::updateWalk();
370
371 // terminate walk only when client and server side walk are completed
372 if (m_walking && m_walkTimer.ticksElapsed() >= getStepDuration()) {
373 m_lastPrewalkDone = true;
374 if (!isPreWalking()) {
375 terminateWalk();
376 }
377 }
378}
379
380void LocalPlayer::terminateWalk()
381{
382 if (g_extras.debugWalking) {
383 g_logger.info(stdext::format("[%i] terminateWalk", (int)g_clock.millis()));
384 }
385
386 Creature::terminateWalk();
387 m_idleTimer.restart();
388 m_preWalking.clear();
389 m_walking = false;
390
391 if(m_serverWalking) {
392 if(m_serverWalkEndEvent)
393 m_serverWalkEndEvent->cancel();
394 auto self = asLocalPlayer();
395 m_serverWalkEndEvent = g_dispatcher.scheduleEvent([self] {
396 self->m_serverWalking = false;
397 }, 100);
398 }
399
400 callLuaField("onWalkFinish");
401}
402
403void LocalPlayer::onAppear()
404{
405 Creature::onAppear();
406
407 /* Does not seem to be needed anymore
408 // on teleports lock the walk
409 if(!m_oldPosition.isInRange(m_position,1,1))
410 lockWalk();
411 */
412}
413
414void LocalPlayer::onPositionChange(const Position& newPos, const Position& oldPos)
415{
416 Creature::onPositionChange(newPos, oldPos);
417
418 if(newPos == m_autoWalkDestination)
419 stopAutoWalk();
420 else if(m_autoWalkDestination.isValid() && newPos == m_lastAutoWalkPosition)
421 autoWalk(m_autoWalkDestination);
422
423 m_walkMatrix.updatePosition(newPos);
424}
425
426void LocalPlayer::turn(Otc::Direction direction)
427{
428 Creature::setDirection(direction);
429 callLuaField("onTurn", direction);
430}
431
432void LocalPlayer::setStates(int states)
433{
434 if(m_states != states) {
435 int oldStates = m_states;
436 m_states = states;
437
438 callLuaField("onStatesChange", states, oldStates);
439 }
440}
441
442void LocalPlayer::setSkill(uint8_t skill, int level, int levelPercent)
443{
444 if(skill >= m_skillsLevel.size()) {
445 m_skillsLevel.resize(skill + 1, 0);
446 m_skillsBaseLevel.resize(skill + 1, 0);
447 m_skillsLevelPercent.resize(skill + 1, 0);
448 }
449
450 int oldLevel = m_skillsLevel[skill];
451 int oldLevelPercent = m_skillsLevelPercent[skill];
452
453 if(level != oldLevel || levelPercent != oldLevelPercent) {
454 m_skillsLevel[skill] = level;
455 m_skillsLevelPercent[skill] = levelPercent;
456
457 callLuaField("onSkillChange", skill, level, levelPercent, oldLevel, oldLevelPercent);
458 }
459}
460
461void LocalPlayer::setBaseSkill(uint8_t skill, int baseLevel)
462{
463 if (skill >= m_skillsLevel.size()) {
464 m_skillsLevel.resize(skill + 1, 0);
465 m_skillsBaseLevel.resize(skill + 1, 0);
466 m_skillsLevelPercent.resize(skill + 1, 0);
467 }
468
469 int oldBaseLevel = m_skillsBaseLevel[skill];
470 if(baseLevel != oldBaseLevel) {
471 m_skillsBaseLevel[skill] = baseLevel;
472
473 callLuaField("onBaseSkillChange", skill, baseLevel, oldBaseLevel);
474 }
475}
476
477void LocalPlayer::setHealth(double health, double maxHealth)
478{
479 if(m_health != health || m_maxHealth != maxHealth) {
480 double oldHealth = m_health;
481 double oldMaxHealth = m_maxHealth;
482 m_health = health;
483 m_maxHealth = maxHealth;
484
485 callLuaField("onHealthChange", health, maxHealth, oldHealth, oldMaxHealth);
486
487 // cannot walk while dying
488 if(health == 0) {
489 if(isPreWalking())
490 stopWalk();
491 lockWalk();
492 }
493 }
494}
495
496void LocalPlayer::setFreeCapacity(double freeCapacity)
497{
498 if(m_freeCapacity != freeCapacity) {
499 double oldFreeCapacity = m_freeCapacity;
500 m_freeCapacity = freeCapacity;
501
502 callLuaField("onFreeCapacityChange", freeCapacity, oldFreeCapacity);
503 }
504}
505
506void LocalPlayer::setTotalCapacity(double totalCapacity)
507{
508 if(m_totalCapacity != totalCapacity) {
509 double oldTotalCapacity = m_totalCapacity;
510 m_totalCapacity = totalCapacity;
511
512 callLuaField("onTotalCapacityChange", totalCapacity, oldTotalCapacity);
513 }
514}
515
516void LocalPlayer::setExperience(double experience)
517{
518 if(m_experience != experience) {
519 double oldExperience = m_experience;
520 m_experience = experience;
521
522 callLuaField("onExperienceChange", experience, oldExperience);
523 }
524}
525
526void LocalPlayer::setLevel(double level, double levelPercent)
527{
528 if(m_level != level || m_levelPercent != levelPercent) {
529 double oldLevel = m_level;
530 double oldLevelPercent = m_levelPercent;
531 m_level = level;
532 m_levelPercent = levelPercent;
533
534 callLuaField("onLevelChange", level, levelPercent, oldLevel, oldLevelPercent);
535 }
536}
537
538void LocalPlayer::setMana(double mana, double maxMana)
539{
540 if(m_mana != mana || m_maxMana != maxMana) {
541 double oldMana = m_mana;
542 double oldMaxMana = m_maxMana;
543 m_mana = mana;
544 m_maxMana = maxMana;
545
546 callLuaField("onManaChange", mana, maxMana, oldMana, oldMaxMana);
547 }
548}
549
550void LocalPlayer::setMagicLevel(double magicLevel, double magicLevelPercent)
551{
552 if(m_magicLevel != magicLevel || m_magicLevelPercent != magicLevelPercent) {
553 double oldMagicLevel = m_magicLevel;
554 double oldMagicLevelPercent = m_magicLevelPercent;
555 m_magicLevel = magicLevel;
556 m_magicLevelPercent = magicLevelPercent;
557
558 callLuaField("onMagicLevelChange", magicLevel, magicLevelPercent, oldMagicLevel, oldMagicLevelPercent);
559 }
560}
561
562void LocalPlayer::setBaseMagicLevel(double baseMagicLevel)
563{
564 if(m_baseMagicLevel != baseMagicLevel) {
565 double oldBaseMagicLevel = m_baseMagicLevel;
566 m_baseMagicLevel = baseMagicLevel;
567
568 callLuaField("onBaseMagicLevelChange", baseMagicLevel, oldBaseMagicLevel);
569 }
570}
571
572void LocalPlayer::setSoul(double soul)
573{
574 if(m_soul != soul) {
575 double oldSoul = m_soul;
576 m_soul = soul;
577
578 callLuaField("onSoulChange", soul, oldSoul);
579 }
580}
581
582void LocalPlayer::setStamina(double stamina)
583{
584 if(m_stamina != stamina) {
585 double oldStamina = m_stamina;
586 m_stamina = stamina;
587
588 callLuaField("onStaminaChange", stamina, oldStamina);
589 }
590}
591
592void LocalPlayer::setInventoryItem(Otc::InventorySlot inventory, const ItemPtr& item)
593{
594 if(inventory >= Otc::LastInventorySlot) {
595 g_logger.traceError("invalid slot");
596 return;
597 }
598
599 if(m_inventoryItems[inventory] != item) {
600 ItemPtr oldItem = m_inventoryItems[inventory];
601 m_inventoryItems[inventory] = item;
602
603 callLuaField("onInventoryChange", inventory, item, oldItem);
604 }
605}
606
607void LocalPlayer::setVocation(int vocation)
608{
609 if(m_vocation != vocation) {
610 int oldVocation = m_vocation;
611 m_vocation = vocation;
612
613 callLuaField("onVocationChange", vocation, oldVocation);
614 }
615}
616
617void LocalPlayer::setPremium(bool premium)
618{
619 if(m_premium != premium) {
620 m_premium = premium;
621
622 callLuaField("onPremiumChange", premium);
623 }
624}
625
626void LocalPlayer::setRegenerationTime(double regenerationTime)
627{
628 if(m_regenerationTime != regenerationTime) {
629 double oldRegenerationTime = m_regenerationTime;
630 m_regenerationTime = regenerationTime;
631
632 callLuaField("onRegenerationChange", regenerationTime, oldRegenerationTime);
633 }
634}
635
636void LocalPlayer::setOfflineTrainingTime(double offlineTrainingTime)
637{
638 if(m_offlineTrainingTime != offlineTrainingTime) {
639 double oldOfflineTrainingTime = m_offlineTrainingTime;
640 m_offlineTrainingTime = offlineTrainingTime;
641
642 callLuaField("onOfflineTrainingChange", offlineTrainingTime, oldOfflineTrainingTime);
643 }
644}
645
646void LocalPlayer::setSpells(const std::vector<int>& spells)
647{
648 if(m_spells != spells) {
649 std::vector<int> oldSpells = m_spells;
650 m_spells = spells;
651
652 callLuaField("onSpellsChange", spells, oldSpells);
653 }
654}
655
656void LocalPlayer::setBlessings(int blessings)
657{
658 if(blessings != m_blessings) {
659 int oldBlessings = m_blessings;
660 m_blessings = blessings;
661
662 callLuaField("onBlessingsChange", blessings, oldBlessings);
663 }
664}
665
666bool LocalPlayer::hasSight(const Position& pos)
667{
668 return m_position.isInRange(pos, g_map.getAwareRange().left - 1, g_map.getAwareRange().top - 1);
669}
670
Top