Merge pull request #160 from MiyazakiTheFalse/codex/add-tutorial-for-first-time-chase-state

Add one-time chase tutorial and optional failure reminder
This commit is contained in:
MiyazakiTheFalse 2026-03-07 17:14:26 +00:00 committed by GitHub
commit 4b8b59c807
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 2 deletions

View File

@ -0,0 +1,16 @@
# Chase tutorial design notes
## Goal
Provide a one-time, lightweight explanation the first time a player enters chase state, then offer an optional reminder if the player repeatedly fails chase re-engagements.
## Rule framing reference
Tutorial text should stay aligned with the chase continuation and safe-hub rules documented in [`docs/chase_boundary_policy.md`](./chase_boundary_policy.md):
- Chasers pursue while chase state is active.
- Safe/scripted hubs (towns, cities, indoors) terminate chase.
- Encounter-eligible terrain is used for re-engagement attempts while chase remains active.
## Runtime behavior
- **First-time tutorial** is queued when chase starts for the first time in a save.
- **Reminder hint** is queued after repeated chase failures (currently two consecutive chase-battle escapes/teleports that continue pressure).
- Messages are shown only when no script is running and no field message box is active.
- Reminder is optional and one-time per save.

View File

@ -1408,6 +1408,8 @@
#define FLAG_SYS_UNLOCKED_TANOBY_RUINS (SYS_FLAGS + 0x49)
#define FLAG_ENABLE_SHIP_NAVEL_ROCK (SYS_FLAGS + 0x4A)
#define FLAG_ENABLE_SHIP_BIRTH_ISLAND (SYS_FLAGS + 0x4B)
#define FLAG_SYS_CHASE_TUTORIAL_SEEN (SYS_FLAGS + 0x4C)
#define FLAG_SYS_CHASE_FAILURE_REMINDER_SEEN (SYS_FLAGS + 0x4D)
// World Map Flags
#define FLAG_WORLD_MAP_PALLET_TOWN (SYS_FLAGS + 0x90)

View File

@ -1030,6 +1030,8 @@ extern const u8 gText_StaminaHudLabel[];
extern const u8 gText_ChaseHudInactive[];
extern const u8 gText_ChaseHudChasersLabel[];
extern const u8 gText_ChaseHudStepsLabel[];
extern const u8 gText_ChaseTutorialIntro[];
extern const u8 gText_ChaseTutorialReminder[];
extern const u8 gText_TrainerCardYen[];
extern const u8 gText_TrainerCardPokedex[];
extern const u8 gText_TrainerCardNull[];

View File

@ -16,11 +16,13 @@
#include "strings.h"
#include "wild_encounter.h"
#include "battle_anim.h"
#include "event_data.h"
#include "field_message_box.h"
#include "script.h"
#include "overworld_hud.h"
#include "palette.h"
#include "script.h"
#include "sound.h"
#include "constants/battle.h"
#include "constants/flags.h"
#include "constants/map_types.h"
#define STAMINA_LEVEL_MIN 1
@ -46,6 +48,7 @@
#define CHASE_STEPS_MIN 30
#define CHASE_STEPS_MAX 140
#define CHASE_FAILURE_REMINDER_THRESHOLD 2
typedef enum
{
CHASE_END_FEEDBACK_NONE,
@ -64,9 +67,15 @@ static EWRAM_DATA u16 sChaseStepsRemaining = 0;
static EWRAM_DATA u8 sChaseReengageStepCountdown = 0;
static EWRAM_DATA bool8 sPendingWildFirstMovePriority = FALSE;
static EWRAM_DATA bool8 sBattleUsesWildFirstMovePriority = FALSE;
static EWRAM_DATA bool8 sPendingChaseTutorialMessage = FALSE;
static EWRAM_DATA bool8 sPendingChaseReminderMessage = FALSE;
static EWRAM_DATA u8 sConsecutiveChaseFailures = 0;
static EWRAM_DATA u8 sPendingChaseEndFeedback = CHASE_END_FEEDBACK_NONE;
static bool8 IsMapTypeChaseCompatible(u8 mapType);
static void QueueChaseTutorialIfNeeded(void);
static void QueueChaseFailureReminderIfNeeded(void);
static void TryShowPendingChaseMessage(void);
static u8 GetStaminaLevel(void)
{
@ -155,6 +164,47 @@ static bool8 IsCurrentAreaValidForActiveChase(void)
return TRUE;
}
static void QueueChaseTutorialIfNeeded(void)
{
if (FlagGet(FLAG_SYS_CHASE_TUTORIAL_SEEN))
return;
FlagSet(FLAG_SYS_CHASE_TUTORIAL_SEEN);
sPendingChaseTutorialMessage = TRUE;
}
static void QueueChaseFailureReminderIfNeeded(void)
{
if (FlagGet(FLAG_SYS_CHASE_FAILURE_REMINDER_SEEN))
return;
if (sConsecutiveChaseFailures < CHASE_FAILURE_REMINDER_THRESHOLD)
return;
FlagSet(FLAG_SYS_CHASE_FAILURE_REMINDER_SEEN);
sPendingChaseReminderMessage = TRUE;
}
static void TryShowPendingChaseMessage(void)
{
const u8 *message = NULL;
if (ScriptContext_IsEnabled() || !IsFieldMessageBoxHidden())
return;
if (sPendingChaseTutorialMessage)
{
message = gText_ChaseTutorialIntro;
sPendingChaseTutorialMessage = FALSE;
}
else if (sPendingChaseReminderMessage)
{
message = gText_ChaseTutorialReminder;
sPendingChaseReminderMessage = FALSE;
}
if (message != NULL)
ShowFieldMessage(message);
static void EndChase(bool8 shouldQueueFeedback, u8 feedbackType)
static bool8 ShouldSuppressChaseStartFeedback(void)
{
@ -203,6 +253,8 @@ static void StartChase(u8 initialChasers, u16 initialSteps)
sActiveChasers = initialChasers;
sChaseStepsRemaining = initialSteps;
sChaseReengageStepCountdown = CHASE_REENGAGE_COUNTDOWN_MIN;
sConsecutiveChaseFailures = 0;
QueueChaseTutorialIfNeeded();
if (!wasActive && ChaseStamina_IsChaseActive())
TryShowChaseStartFeedback();
@ -384,6 +436,8 @@ void ChaseStamina_UpdateOverworldFrame(bool8 tookStep)
}
}
TryShowPendingChaseMessage();
if (!tookStep)
return;
@ -502,6 +556,9 @@ void ChaseStamina_OnWildBattleEnded(u8 battleOutcome, u32 battleTypeFlags)
if (sChaseStepsRemaining < chaseLength)
sChaseStepsRemaining = chaseLength;
RollChaseReengageStepCountdown();
if (sConsecutiveChaseFailures < 255)
sConsecutiveChaseFailures++;
QueueChaseFailureReminderIfNeeded();
break;
}
@ -510,6 +567,7 @@ void ChaseStamina_OnWildBattleEnded(u8 battleOutcome, u32 battleTypeFlags)
{
if (sActiveChasers != 0)
sActiveChasers--;
sConsecutiveChaseFailures = 0;
if (sActiveChasers == 0)
EndChase(TRUE, CHASE_END_FEEDBACK_ENDED);
break;
@ -523,11 +581,15 @@ void ChaseStamina_OnWildBattleEnded(u8 battleOutcome, u32 battleTypeFlags)
case B_OUTCOME_MON_TELEPORTED:
case B_OUTCOME_LINK_BATTLE_RAN:
// Forced-end and escape outcomes clear the chase to keep its state deterministic.
sConsecutiveChaseFailures = 0;
EndChase();
EndChase(FALSE, CHASE_END_FEEDBACK_NONE);
break;
default:
// Any unknown outcome is treated as a forced end to avoid stale chase state.
sConsecutiveChaseFailures = 0;
EndChase();
EndChase(FALSE, CHASE_END_FEEDBACK_NONE);
break;
}

View File

@ -855,6 +855,17 @@ const u8 gText_StaminaHudLabel[] = _("STA ");
const u8 gText_ChaseHudInactive[] = _("CHASE: OFF");
const u8 gText_ChaseHudChasersLabel[] = _("CHASERS ");
const u8 gText_ChaseHudStepsLabel[] = _("STEPS ");
const u8 gText_ChaseTutorialIntro[] = _("You're being chased!\p"
"Chasers pursue you while CHASE is active.\n"
"Reach a safe hub (town/city/indoors)\n"
"to end it.\p"
"If an encounter starts before the timer\n"
"runs out, the chase can re-engage.\n"
"Keep moving!");
const u8 gText_ChaseTutorialReminder[] = _("Hint: if you keep getting caught,\n"
"head for a safe hub to reset pressure.\n"
"On routes, avoid long grass/water\n"
"until you're ready to re-engage.");
const u8 gText_TrainerCardYen[] = _("¥");
const u8 gText_TrainerCardPokedex[] = _("POKéDEX");
const u8 gText_TrainerCardNull[] = _("");