Ancient Story / Journal

Ancient Story

Ancient Story is a Unity RPG prototype about exploration, terrain readability, quests, loot feedback, and UI that feels embedded in the world. The first project tab is the Journal: a quest book where text appears in stages, the quill follows the currently written letter, and instant reveal safely resets TextMesh Pro state instead of leaving hidden text behind.

Project overview

A readable RPG prototype built around world interaction

Ancient Story is being shaped as a practical Unity project: systems are added through small, testable gameplay slices, then polished into portfolio-ready features. Recent work covers player visibility behind trees, terrain updates, loot screen presentation, arc selection, absorption feedback, and the quest journal described below.

The Journal is the first visible Ancient Story tab because it ties together data, animation, audio feedback, TextMesh Pro measurement, and a handcrafted presentation layer. It is both a gameplay screen and a technical case study for UI state.

Generated Ancient Story journal UI concept
Generated concept image for the Ancient Story Journal: parchment pages, quest markers, quill motion, and diegetic UI panels.
Structure

Keep selection logic separate from reveal logic

`JournalScreen` is intentionally thin. It creates quest cards, handles hover and click events, tracks the selected card, and decides whether the book should play the reveal sequence or show the current quest instantly. The animation details stay inside `JournalBookComponent`.

private void SelectQuest(QuestDefinition quest)
{
    var selectedQuestView = FindQuestView(quest);

    if (m_selectedQuestView == selectedQuestView)
    {
        m_journalBookComponent.SetDataInstant(quest);
        return;
    }

    m_journalBookComponent.SetData(quest);
    m_selectedQuestView.Hide();
    m_selectedQuestView = selectedQuestView;
    m_selectedQuestView.Show();
}
Journal data flow diagram
The screen selects a quest, the book builds the reveal sequence, TMP reports visible letters, and the quill receives target positions.
Reveal sequence

The book presents quest data in readable stages

`JournalBookComponent` fills title, descriptions, progress data, and memorise text, then starts a DOTween sequence. The quill first moves to the title, the writing audio loop starts, and each text block is revealed in order. The progress icon fades in together with the progress text.

private void PlayTextRevealSequence()
{
    var startPosition = GetQuilRevealStartPosition();

    m_textRevealSequence = DOTween.Sequence()
        .AppendCallback(() => m_quilContainer.TakeQuill(startPosition))
        .AppendInterval(m_quilContainer.TakeDurationSeconds)
        .AppendCallback(PlayRandomTextRevealLoop)
        .Append(RevealText(m_title, m_longRevealDelaySeconds))
        .AppendCallback(StopTextRevealLoop)
        .AppendCallback(PlayRandomTextRevealLoop)
        .Append(RevealText(m_shortDescription, m_longRevealDelaySeconds, false));
}
TextMesh Pro

Reveal only visible characters

The typewriter does not animate raw string length. It collects only TMP characters where `isVisible` is true. Spaces and newline characters still exist in the rendered text, but they do not consume reveal time. This removes the small pauses that were visible on fast text speeds.

private static List<int> GetVisibleCharacterIndices(TMP_Text textUI)
{
    var textInfo = textUI.textInfo;
    var indices = new List<int>(textInfo.characterCount);

    for (var i = 0; i < textInfo.characterCount; i++)
    {
        if (textInfo.characterInfo[i].isVisible)
        {
            indices.Add(i);
        }
    }

    return indices;
}

The quill position is also derived from TMP character info. The current implementation uses the center of the character bottom edge, transformed into world space.

Typewriter and quill movement diagram
Visible characters drive both text reveal timing and quill target updates.
Quill movement

Follow a target instead of restarting a tween per letter

Restarting a DOMove for every written letter made the quill feel stiff on fast text. `SetQuilPosition` now only updates a target position. While the quill is taken by the journal, `Update` moves it toward that target with `Vector3.SmoothDamp`. A small random fluctuation keeps the movement from feeling perfectly mechanical.

public void SetQuilPosition(Vector3 position)
{
    if (!m_isFollowingQuilTarget)
    {
        m_rectTransform.DOKill();
        m_quilFollowVelocity = Vector3.zero;
    }

    m_quilTargetPosition = position + GetFollowFluctuation();
    m_isFollowingQuilTarget = true;
}
Instant reveal

Reset TMP visibility before killing the sequence

The hardest bug was caused by `maxVisibleCharacters`. If a typewriter tween was killed while a TMP field had `maxVisibleCharacters = 0`, later measurement could see an empty visible text state. `SetDataInstant` now shows all text before killing the sequence, assigns the new quest data, refreshes wrapped text, and shows all text again.

public void SetDataInstant(QuestDefinition questDefinition)
{
    ShowAllTextInstantly();
    KillTextRevealSequence();

    m_title.text = questDefinition.Title;
    m_shortDescription.text = questDefinition.ShortDescription;
    m_longDescription.text = questDefinition.LongDescription;

    m_memoriseTextWrap.RefreshFromSettings();
    m_longTextWrap.RefreshFromSettings();

    ShowAllTextInstantly();
    StopTextRevealLoop();
}

Final rule: if `maxVisibleCharacters` is used for animation, completion, kill, instant reveal, and TMP measurement must restore full visibility deliberately.

TMP state reset diagram
Instant reveal is a state reset, not only a visual shortcut.