1 /*
2 * This file is part of EasyRPG Player.
3 *
4 * EasyRPG Player is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * EasyRPG Player is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 // Headers
19 #include "game_player.h"
20 #include "async_handler.h"
21 #include "game_actor.h"
22 #include "game_map.h"
23 #include "game_message.h"
24 #include "game_party.h"
25 #include "game_system.h"
26 #include "game_screen.h"
27 #include "game_pictures.h"
28 #include "input.h"
29 #include "main_data.h"
30 #include "player.h"
31 #include "util_macro.h"
32 #include "game_switches.h"
33 #include "output.h"
34 #include "rand.h"
35 #include "utils.h"
36 #include <lcf/reader_util.h>
37 #include <lcf/scope_guard.h>
38 #include "scene_battle.h"
39 #include "scene_menu.h"
40 #include <lcf/rpg/savetarget.h>
41 #include <algorithm>
42 #include <cmath>
43
Game_Player()44 Game_Player::Game_Player(): Game_PlayerBase(Player)
45 {
46 SetDirection(lcf::rpg::EventPage::Direction_down);
47 SetMoveSpeed(4);
48 SetAnimationType(lcf::rpg::EventPage::AnimType_non_continuous);
49 }
50
SetSaveData(lcf::rpg::SavePartyLocation save)51 void Game_Player::SetSaveData(lcf::rpg::SavePartyLocation save)
52 {
53 *data() = std::move(save);
54
55 SanitizeData("Party");
56
57 // RPG_RT will always reset the hero graphic on loading a save, even if
58 // a move route changed the graphic.
59 ResetGraphic();
60 }
61
GetSaveData() const62 lcf::rpg::SavePartyLocation Game_Player::GetSaveData() const {
63 return *data();
64 }
65
GetScreenZ(bool apply_shift) const66 int Game_Player::GetScreenZ(bool apply_shift) const {
67 // Player is always slightly above events
68 // (and always on "same layer as hero" obviously)
69 return Game_Character::GetScreenZ(apply_shift) + 1;
70 }
71
ReserveTeleport(int map_id,int x,int y,int direction,TeleportTarget::Type tt)72 void Game_Player::ReserveTeleport(int map_id, int x, int y, int direction, TeleportTarget::Type tt) {
73 teleport_target = TeleportTarget(map_id, x, y, direction, tt);
74
75 FileRequestAsync* request = Game_Map::RequestMap(map_id);
76 request->SetImportantFile(true);
77 request->Start();
78 }
79
ReserveTeleport(const lcf::rpg::SaveTarget & target)80 void Game_Player::ReserveTeleport(const lcf::rpg::SaveTarget& target) {
81 int map_id = target.map_id;
82
83 if (Game_Map::GetMapType(target.map_id) == lcf::rpg::TreeMap::MapType_area) {
84 // Area: Obtain the map the area belongs to
85 map_id = Game_Map::GetParentId(target.map_id);
86 }
87
88 ReserveTeleport(map_id, target.map_x, target.map_y, Down, TeleportTarget::eSkillTeleport);
89
90 if (target.switch_on) {
91 Main_Data::game_switches->Set(target.switch_id, true);
92 Game_Map::SetNeedRefresh(true);
93 }
94 }
95
PerformTeleport()96 void Game_Player::PerformTeleport() {
97 assert(IsPendingTeleport());
98 if (!IsPendingTeleport()) {
99 return;
100 }
101
102 if (teleport_target.GetMapId() <= 0) {
103 Output::Error("Invalid Teleport map id! mapid={} x={} y={} d={}", teleport_target.GetMapId(),
104 teleport_target.GetX(), teleport_target.GetY(), teleport_target.GetDirection());
105 }
106
107 const auto map_changed = (GetMapId() != teleport_target.GetMapId());
108 MoveTo(teleport_target.GetMapId(), teleport_target.GetX(), teleport_target.GetY());
109
110
111 if (teleport_target.GetDirection() >= 0) {
112 SetDirection(teleport_target.GetDirection());
113 UpdateFacing();
114 }
115
116 if (map_changed && teleport_target.GetType() != TeleportTarget::eAsyncQuickTeleport) {
117 Main_Data::game_screen->OnMapChange();
118 Main_Data::game_pictures->OnMapChange();
119 Game_Map::GetInterpreter().OnMapChange();
120 }
121
122 ResetTeleportTarget();
123 }
124
MoveTo(int map_id,int x,int y)125 void Game_Player::MoveTo(int map_id, int x, int y) {
126 const auto map_changed = (GetMapId() != map_id);
127
128 Game_Character::MoveTo(map_id, x, y);
129 SetEncounterSteps(0);
130 SetMenuCalling(false);
131
132 auto* vehicle = GetVehicle();
133 if (vehicle) {
134 // RPG_RT doesn't check the aboard flag for this one
135 vehicle->MoveTo(map_id, x, y);
136 }
137
138 if (map_changed) {
139 // FIXME: Assert map pre-loaded in cache.
140
141 // pan_state does not reset when you change maps.
142 data()->pan_speed = lcf::rpg::SavePartyLocation::kPanSpeedDefault;
143 data()->pan_finish_x = lcf::rpg::SavePartyLocation::kPanXDefault;
144 data()->pan_finish_y = lcf::rpg::SavePartyLocation::kPanYDefault;
145 data()->pan_current_x = lcf::rpg::SavePartyLocation::kPanXDefault;
146 data()->pan_current_y = lcf::rpg::SavePartyLocation::kPanYDefault;
147
148 ResetAnimation();
149
150 auto map = Game_Map::loadMapFile(GetMapId());
151
152 Game_Map::Setup(std::move(map));
153 Game_Map::PlayBgm();
154
155 // This Fixes an RPG_RT bug where the jumping flag doesn't get reset
156 // if you change maps during a jump
157 SetJumping(false);
158 } else {
159 Game_Map::SetPositionX(GetSpriteX() - GetPanX());
160 Game_Map::SetPositionY(GetSpriteY() - GetPanY());
161 }
162
163 ResetGraphic();
164 }
165
MakeWay(int from_x,int from_y,int to_x,int to_y)166 bool Game_Player::MakeWay(int from_x, int from_y, int to_x, int to_y) {
167 if (IsAboard()) {
168 return GetVehicle()->MakeWay(from_x, from_y, to_x, to_y);
169 }
170
171 return Game_Character::MakeWay(from_x, from_y, to_x, to_y);
172 }
173
MoveRouteSetSpriteGraphic(std::string sprite_name,int index)174 void Game_Player::MoveRouteSetSpriteGraphic(std::string sprite_name, int index) {
175 auto* vh = GetVehicle();
176 if (vh) {
177 vh->MoveRouteSetSpriteGraphic(std::move(sprite_name), index);
178 } else {
179 Game_Character::MoveRouteSetSpriteGraphic(std::move(sprite_name), index);
180 }
181 }
182
UpdateScroll(int amount,bool was_jumping)183 void Game_Player::UpdateScroll(int amount, bool was_jumping) {
184 if (IsPanLocked()) {
185 return;
186 }
187
188 auto dx = (GetX() * SCREEN_TILE_SIZE) - Game_Map::GetPositionX() - GetPanX();
189 auto dy = (GetY() * SCREEN_TILE_SIZE) - Game_Map::GetPositionY() - GetPanY();
190
191 const auto w = Game_Map::GetWidth() * SCREEN_TILE_SIZE;
192 const auto h = Game_Map::GetHeight() * SCREEN_TILE_SIZE;
193
194 dx = Utils::PositiveModulo(dx + w / 2, w) - w / 2;
195 dy = Utils::PositiveModulo(dy + h / 2, h) - h / 2;
196
197 const auto sx = Utils::Signum(dx);
198 const auto sy = Utils::Signum(dy);
199
200 if (was_jumping) {
201 const auto jdx = sx * std::abs(GetX() - GetBeginJumpX());
202 const auto jdy = sy * std::abs(GetY() - GetBeginJumpY());
203
204 Game_Map::Scroll(amount * jdx, amount * jdy);
205
206 if (!IsJumping()) {
207 // RPG does this to fix rounding errors?
208 const auto x = SCREEN_TILE_SIZE * Utils::RoundTo<int>(Game_Map::GetPositionX() / static_cast<double>(SCREEN_TILE_SIZE));
209 const auto y = SCREEN_TILE_SIZE * Utils::RoundTo<int>(Game_Map::GetPositionY() / static_cast<double>(SCREEN_TILE_SIZE));
210
211 // RPG_RT does adjust map position, but not panorama!
212 Game_Map::SetPositionX(x, false);
213 Game_Map::SetPositionY(y, false);
214 }
215 return;
216 }
217
218 int move_sx = 0;
219 int move_sy = 0;
220 const auto d = GetDirection();
221 if (sy < 0 && (d == Up || d == UpRight || d == UpLeft)) {
222 move_sy = sy;
223 }
224 if (sy > 0 && (d == Down || d == DownRight || d == DownLeft)) {
225 move_sy = sy;
226 }
227 if (sx > 0 && (d == Right || d == UpRight || d == DownRight)) {
228 move_sx = sx;
229 }
230 if (sx < 0 && (d == Left || d == UpLeft || d == DownLeft)) {
231 move_sx = sx;
232 }
233
234 Game_Map::Scroll(move_sx * amount, move_sy * amount);
235 }
236
UpdateAirship()237 bool Game_Player::UpdateAirship() {
238 auto* vehicle = GetVehicle();
239
240 // RPG_RT doesn't check vehicle, but we have to as we don't have another way to fetch it.
241 // Also in vanilla RPG_RT it's impossible for the hero to fly without the airship.
242 if (vehicle && vehicle->IsFlying()) {
243 if (vehicle->AnimateAscentDescent()) {
244 if (!vehicle->IsFlying()) {
245 // If we landed, them disembark
246 Main_Data::game_player->SetFlying(vehicle->IsFlying());
247 data()->aboard = false;
248 SetFacing(Down);
249 data()->vehicle = 0;
250 SetMoveSpeed(data()->preboard_move_speed);
251
252 Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
253 }
254
255 return true;
256 }
257 }
258 return false;
259 }
260
UpdateNextMovementAction()261 void Game_Player::UpdateNextMovementAction() {
262 if (UpdateAirship()) {
263 return;
264 }
265
266 UpdateMoveRoute(data()->move_route_index, data()->move_route, true);
267
268 if (Game_Map::GetInterpreter().IsRunning()) {
269 SetMenuCalling(false);
270 return;
271 }
272
273 if(IsPaused() || IsMoveRouteOverwritten() || Game_Message::IsMessageActive()) {
274 return;
275 }
276
277 if (IsEncounterCalling()) {
278 SetMenuCalling(false);
279 SetEncounterCalling(false);
280
281 BattleArgs args;
282 if (Game_Map::PrepareEncounter(args)) {
283 Scene::instance->SetRequestedScene(Scene_Battle::Create(std::move(args)));
284 return;
285 }
286 }
287
288 if (IsMenuCalling()) {
289 SetMenuCalling(false);
290
291 ResetAnimation();
292 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
293 Scene::instance->SetRequestedScene(std::make_shared<Scene_Menu>());
294 return;
295 }
296
297 CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false);
298
299 if (Game_Map::IsAnyEventStarting()) {
300 return;
301 }
302
303 int move_dir = -1;
304 switch (Input::dir4) {
305 case 2:
306 move_dir = Down;
307 break;
308 case 4:
309 move_dir = Left;
310 break;
311 case 6:
312 move_dir = Right;
313 break;
314 case 8:
315 move_dir = Up;
316 break;
317 }
318 if (move_dir >= 0) {
319 SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->route_through);
320 Move(move_dir);
321 ResetThrough();
322 if (IsStopping()) {
323 int front_x = Game_Map::XwithDirection(GetX(), GetDirection());
324 int front_y = Game_Map::YwithDirection(GetY(), GetDirection());
325 CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false);
326 }
327 }
328
329 if (IsStopping()) {
330 if (Input::IsTriggered(Input::DECISION)) {
331 if (!GetOnOffVehicle()) {
332 CheckActionEvent();
333 }
334 }
335 return;
336 }
337
338 Main_Data::game_party->IncSteps();
339 if (Main_Data::game_party->ApplyStateDamage()) {
340 Main_Data::game_screen->FlashMapStepDamage();
341 }
342 UpdateEncounterSteps();
343 }
344
UpdateMovement(int amount)345 void Game_Player::UpdateMovement(int amount) {
346 const bool was_jumping = IsJumping();
347
348 Game_Character::UpdateMovement(amount);
349
350 UpdateScroll(amount, was_jumping);
351
352 if (!IsMoveRouteOverwritten() && IsStopping()) {
353 TriggerSet triggers = { lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision };
354 CheckEventTriggerHere(triggers, false);
355 }
356 }
357
Update()358 void Game_Player::Update() {
359 Game_Character::Update();
360
361 if (IsStopping()) {
362 if (data()->boarding) {
363 // Boarding completed
364 data()->aboard = true;
365 data()->boarding = false;
366 // Note: RPG_RT ignores the lock_facing flag here!
367 SetFacing(Left);
368
369 auto* vehicle = GetVehicle();
370 SetMoveSpeed(vehicle->GetMoveSpeed());
371 }
372 if (data()->unboarding) {
373 // Unboarding completed
374 data()->unboarding = false;
375 }
376 }
377
378 auto* vehicle = GetVehicle();
379
380 if (IsAboard() && vehicle) {
381 vehicle->SyncWithRider(this);
382 }
383
384 UpdatePan();
385
386 // ESC-Menu calling
387 if (Main_Data::game_system->GetAllowMenu()
388 && !Game_Message::IsMessageActive()
389 && !Game_Map::GetInterpreter().IsRunning())
390 {
391 if (Input::IsTriggered(Input::CANCEL)) {
392 SetMenuCalling(true);
393 }
394 }
395 }
396
CheckActionEvent()397 bool Game_Player::CheckActionEvent() {
398 if (IsFlying()) {
399 return false;
400 }
401
402 bool result = false;
403 int front_x = Game_Map::XwithDirection(GetX(), GetDirection());
404 int front_y = Game_Map::YwithDirection(GetY(), GetDirection());
405
406 result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true);
407 result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true);
408
409 // Counter tile loop stops only if you talk to an action event.
410 bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true);
411 // RPG_RT allows maximum of 3 counter tiles
412 for (int i = 0; !got_action && i < 3; ++i) {
413 if (!Game_Map::IsCounter(front_x, front_y)) {
414 break;
415 }
416
417 front_x = Game_Map::XwithDirection(front_x, GetDirection());
418 front_y = Game_Map::YwithDirection(front_y, GetDirection());
419
420 got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true);
421 }
422 return result || got_action;
423 }
424
CheckEventTriggerHere(TriggerSet triggers,bool triggered_by_decision_key)425 bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key) {
426 if (InAirship()) {
427 return false;
428 }
429
430 bool result = false;
431
432 for (auto& ev: Game_Map::GetEvents()) {
433 const auto trigger = ev.GetTrigger();
434 if (ev.IsActive()
435 && ev.GetX() == GetX()
436 && ev.GetY() == GetY()
437 && ev.GetLayer() != lcf::rpg::EventPage::Layers_same
438 && trigger >= 0
439 && triggers[trigger]) {
440 SetEncounterCalling(false);
441 result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, true);
442 }
443 }
444 return result;
445 }
446
CheckEventTriggerThere(TriggerSet triggers,int x,int y,bool triggered_by_decision_key)447 bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key) {
448 if (InAirship()) {
449 return false;
450 }
451 bool result = false;
452
453 for (auto& ev : Game_Map::GetEvents()) {
454 const auto trigger = ev.GetTrigger();
455 if (ev.IsActive()
456 && ev.GetX() == x
457 && ev.GetY() == y
458 && ev.GetLayer() == lcf::rpg::EventPage::Layers_same
459 && trigger >= 0
460 && triggers[trigger]) {
461 SetEncounterCalling(false);
462 result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, true);
463 }
464 }
465 return result;
466 }
467
ResetGraphic()468 void Game_Player::ResetGraphic() {
469
470 auto* actor = Main_Data::game_party->GetActor(0);
471 if (actor == nullptr) {
472 SetSpriteGraphic("", 0);
473 SetTransparency(0);
474 return;
475 }
476
477 SetSpriteGraphic(ToString(actor->GetSpriteName()), actor->GetSpriteIndex());
478 SetTransparency(actor->GetSpriteTransparency());
479 }
480
GetOnOffVehicle()481 bool Game_Player::GetOnOffVehicle() {
482 if (IsDirectionDiagonal(GetDirection())) {
483 SetDirection(GetFacing());
484 }
485
486 return IsAboard()
487 ? GetOffVehicle()
488 : GetOnVehicle();
489 }
490
GetOnVehicle()491 bool Game_Player::GetOnVehicle() {
492 assert(!IsDirectionDiagonal(GetDirection()));
493 assert(!IsAboard());
494
495 auto* vehicle = Game_Map::GetVehicle(Game_Vehicle::Airship);
496
497 if (vehicle->IsInPosition(GetX(), GetY()) && IsStopping() && vehicle->IsStopping()) {
498 data()->vehicle = Game_Vehicle::Airship;
499 data()->aboard = true;
500
501 // Note: RPG_RT ignores the lock_facing flag here!
502 SetFacing(Left);
503
504 data()->preboard_move_speed = GetMoveSpeed();
505 SetMoveSpeed(vehicle->GetMoveSpeed());
506 vehicle->StartAscent();
507 Main_Data::game_player->SetFlying(vehicle->IsFlying());
508 } else {
509 const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection());
510 const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection());
511
512 vehicle = Game_Map::GetVehicle(Game_Vehicle::Ship);
513 if (!vehicle->IsInPosition(front_x, front_y)) {
514 vehicle = Game_Map::GetVehicle(Game_Vehicle::Boat);
515 if (!vehicle->IsInPosition(front_x, front_y)) {
516 return false;
517 }
518 }
519
520 if (!Game_Map::CanEmbarkShip(*this, front_x, front_y)) {
521 return false;
522 }
523
524 SetThrough(true);
525 Move(GetDirection());
526 // FIXME: RPG_RT resets through to route_through || not visible?
527 ResetThrough();
528
529 data()->vehicle = vehicle->GetVehicleType();
530 data()->preboard_move_speed = GetMoveSpeed();
531 data()->boarding = true;
532 }
533
534 Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM());
535 Main_Data::game_system->BgmPlay(vehicle->GetBGM());
536 return true;
537 }
538
GetOffVehicle()539 bool Game_Player::GetOffVehicle() {
540 assert(!IsDirectionDiagonal(GetDirection()));
541 assert(IsAboard());
542
543 auto* vehicle = GetVehicle();
544 if (!vehicle) {
545 return false;
546 }
547
548 if (InAirship()) {
549 if (vehicle->IsAscendingOrDescending()) {
550 return false;
551 }
552
553 // Note: RPG_RT ignores the lock_facing flag here!
554 SetFacing(Left);
555 vehicle->StartDescent();
556 return true;
557 }
558
559 const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection());
560 const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection());
561
562 if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) {
563 return false;
564 }
565
566 vehicle->SetDefaultDirection();
567 data()->aboard = false;
568 SetMoveSpeed(data()->preboard_move_speed);
569 data()->unboarding = true;
570
571 SetThrough(true);
572 Move(GetDirection());
573 ResetThrough();
574
575 data()->vehicle = 0;
576 Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
577
578 return true;
579 }
580
ForceGetOffVehicle()581 void Game_Player::ForceGetOffVehicle() {
582 if (!IsAboard()) {
583 return;
584 }
585
586 auto* vehicle = GetVehicle();
587 vehicle->ForceLand();
588 vehicle->SetDefaultDirection();
589
590 data()->flying = false;
591 data()->aboard = false;
592 SetMoveSpeed(data()->preboard_move_speed);
593 data()->unboarding = true;
594 data()->vehicle = 0;
595 Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic());
596 }
597
InVehicle() const598 bool Game_Player::InVehicle() const {
599 return data()->vehicle > 0;
600 }
601
InAirship() const602 bool Game_Player::InAirship() const {
603 return data()->vehicle == Game_Vehicle::Airship;
604 }
605
GetVehicle() const606 Game_Vehicle* Game_Player::GetVehicle() const {
607 return Game_Map::GetVehicle((Game_Vehicle::Type) data()->vehicle);
608 }
609
Move(int dir)610 bool Game_Player::Move(int dir) {
611 if (!IsStopping()) {
612 return true;
613 }
614
615 Game_Character::Move(dir);
616 if (IsStopping()) {
617 return false;
618 }
619
620 if (InAirship()) {
621 return true;
622 }
623
624 int terrain_id = Game_Map::GetTerrainTag(GetX(), GetY());
625 const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, terrain_id);
626 bool red_flash = false;
627
628 if (terrain) {
629 if (terrain->damage != 0) {
630 for (auto hero : Main_Data::game_party->GetActors()) {
631 if (terrain->damage < 0 || !hero->PreventsTerrainDamage()) {
632 if (terrain->damage > 0) {
633 red_flash = true;
634 }
635 hero->ChangeHp(-terrain->damage, false);
636 }
637 }
638 }
639 if (!terrain->on_damage_se || red_flash) {
640 Main_Data::game_system->SePlay(terrain->footstep);
641 }
642 } else {
643 Output::Warning("Player BeginMove: Invalid terrain ID {} at ({}, {})", terrain_id, GetX(), GetY());
644 }
645
646 if (red_flash) {
647 Main_Data::game_screen->FlashMapStepDamage();
648 }
649
650 return true;
651 }
652
IsAboard() const653 bool Game_Player::IsAboard() const {
654 return data()->aboard;
655 }
656
IsBoardingOrUnboarding() const657 bool Game_Player::IsBoardingOrUnboarding() const {
658 return data()->boarding || data()->unboarding;
659 }
660
UpdateEncounterSteps()661 void Game_Player::UpdateEncounterSteps() {
662 if (Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) {
663 return;
664 }
665
666 if(IsFlying()) {
667 return;
668 }
669
670 const auto encounter_rate = Game_Map::GetEncounterRate();
671
672 if (encounter_rate <= 0) {
673 SetEncounterSteps(0);
674 return;
675 }
676
677 int x = GetX();
678 int y = GetY();
679
680 const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, Game_Map::GetTerrainTag(x,y));
681 if (!terrain) {
682 Output::Warning("UpdateEncounterSteps: Invalid terrain at ({}, {})", x, y);
683 return;
684 }
685
686 data()->encounter_steps += terrain->encounter_rate;
687
688 struct Row {
689 int ratio;
690 float pmod;
691 };
692
693 #if 1
694 static constexpr Row enc_table[] = {
695 { 0, 0.0625},
696 { 20, 0.125 },
697 { 40, 0.25 },
698 { 60, 0.5 },
699 { 100, 2.0 },
700 { 140, 4.0 },
701 { 160, 8.0 },
702 { 180, 16.0 },
703 { INT_MAX, 16.0 }
704 };
705 #else
706 //Old versions of RM2k used this table.
707 //Left here for posterity.
708 static constexpr Row enc_table[] = {
709 { 0, 0.5 },
710 { 20, 2.0 / 3.0 },
711 { 50, 5.0 / 6.0 },
712 { 100, 6.0 / 5.0 },
713 { 200, 3.0 / 2.0 },
714 { INT_MAX, 3.0 / 2.0 }
715 };
716 #endif
717 const auto ratio = GetEncounterSteps() / encounter_rate;
718
719 auto& idx = last_encounter_idx;
720 while (ratio > enc_table[idx+1].ratio) {
721 ++idx;
722 }
723 const auto& row = enc_table[idx];
724
725 const auto pmod = row.pmod;
726 const auto p = (1.0f / float(encounter_rate)) * pmod * (float(terrain->encounter_rate) / 100.0f);
727
728 if (!Rand::PercentChance(p)) {
729 return;
730 }
731
732 SetEncounterSteps(0);
733 SetEncounterCalling(true);
734 }
735
SetEncounterSteps(int steps)736 void Game_Player::SetEncounterSteps(int steps) {
737 last_encounter_idx = 0;
738 data()->encounter_steps = steps;
739 }
740
LockPan()741 void Game_Player::LockPan() {
742 data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed;
743 }
744
UnlockPan()745 void Game_Player::UnlockPan() {
746 data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow;
747 }
748
StartPan(int direction,int distance,int speed)749 void Game_Player::StartPan(int direction, int distance, int speed) {
750 distance *= SCREEN_TILE_SIZE;
751
752 if (direction == PanUp) {
753 int new_pan = data()->pan_finish_y + distance;
754 data()->pan_finish_y = new_pan;
755 } else if (direction == PanRight) {
756 int new_pan = data()->pan_finish_x - distance;
757 data()->pan_finish_x = new_pan;
758 } else if (direction == PanDown) {
759 int new_pan = data()->pan_finish_y - distance;
760 data()->pan_finish_y = new_pan;
761 } else if (direction == PanLeft) {
762 int new_pan = data()->pan_finish_x + distance;
763 data()->pan_finish_x = new_pan;
764 }
765
766 data()->pan_speed = 2 << speed;
767 }
768
ResetPan(int speed)769 void Game_Player::ResetPan(int speed) {
770 data()->pan_finish_x = lcf::rpg::SavePartyLocation::kPanXDefault;
771 data()->pan_finish_y = lcf::rpg::SavePartyLocation::kPanYDefault;
772 data()->pan_speed = 2 << speed;
773 }
774
GetPanWait()775 int Game_Player::GetPanWait() {
776 const auto distance = std::max(
777 std::abs(data()->pan_current_x - data()->pan_finish_x),
778 std::abs(data()->pan_current_y - data()->pan_finish_y));
779 const auto speed = data()->pan_speed;
780 assert(speed > 0);
781 return distance / speed + (distance % speed != 0);
782 }
783
UpdatePan()784 void Game_Player::UpdatePan() {
785 if (!IsPanActive())
786 return;
787
788 const int step = data()->pan_speed;
789 const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x;
790 const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y;
791
792 int dx = std::min(step, std::abs(pan_remain_x));
793 dx = pan_remain_x >= 0 ? dx : -dx;
794 int dy = std::min(step, std::abs(pan_remain_y));
795 dy = pan_remain_y >= 0 ? dy : -dy;
796
797 int screen_x = Game_Map::GetPositionX();
798 int screen_y = Game_Map::GetPositionY();
799
800 Game_Map::AddScreenX(screen_x, dx);
801 Game_Map::AddScreenY(screen_y, dy);
802
803 // If we hit the edge of the map before pan finishes.
804 if (dx == 0 && dy == 0) {
805 return;
806 }
807
808 Game_Map::Scroll(dx, dy);
809
810 data()->pan_current_x -= dx;
811 data()->pan_current_y -= dy;
812 }
813
814