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