Script: q_hob_inc

// q_hob_inc.nss — "Concerning Hobbits" Shire-genealogy quiz (roadmap: concerning-hobbits)
//
// T1 (L5) daily dialogue minigame, journal tag "concern_hob". Odo Proudfoot
// (q_hob_odo), a Hobbiton genealogist, is forever settling a dispute at the
// Ivy Bush over who is cousin to whom. He puts five questions of Shire
// lineage to the PC; answer four of five rightly to win gold, XP and a
// keepsake from the family stores. One attempt per calendar day (UTC), win
// or lose (quest_cd_inc QCD_IsDoneToday / QCD_Stamp).
//
// "The correct answer rotates by season." The bank holds QHOB_COUNT
// questions; a per-season seed (derived from the in-game calendar,
// GetCalendarYear()*4 + season) deterministically picks QHOB_ASKED distinct
// questions (stride offset + k*step mod 16, step odd so coprime with 16) and
// rotates the displayed answer order per question so the correct letter moves
// with the season. Everything is a pure function of (season seed, question
// index), so re-showing a question is idempotent and the whole server shares
// one seasonal dispute.
//
// Odo is script-spawned at waypoint AP_concerninghobbits_1 (Hobbiton) by
// q_hob_spawn, fired from the area OnEnter wrapper q_hob_enter. It no-ops
// gracefully until the admin places the waypoint (see the roadmap item manual_steps)
// and never double-spawns.
//
// Session state (locals on the PC, reset by q_hob_begin):
//   Q_HOB_ACTIVE  1 while a game is running
//   Q_HOB_SEED    the season seed captured at game start
//   Q_HOB_IDX     questions answered so far (0..5)
//   Q_HOB_SCORE   correct answers so far
//   Q_HOB_SLOT    which displayed option (1..4) is correct for the shown q.
//   Q_HOB_LAST    1 if the most recent answer was correct
//   Q_HOB_RESULT  set at game end: 1 = won, 2 = lost
//
// Custom tokens (registered per-display by the StartingConditionals):
//   6420 time until tomorrow          6421 question number / final score
//   6422 question text                6423-6426 answer options 1-4
//   6427 right-answer line            6428 wrong-answer line

#include "quest_cd_inc"

const string QHOB_QUEST  = "concern_hob"; // questcddb key + journal tag
const int    QHOB_COUNT  = 16;            // questions in the bank
const int    QHOB_ASKED  = 5;             // questions per game
const int    QHOB_TO_WIN = 4;             // correct answers needed
const int    QHOB_XP     = 100;           // XP on a win
const int    QHOB_LINES  = 5;             // right/wrong flavor lines each

// ------------------------------------------------------------
// Season seed and "come back tomorrow" countdown.

// In-game season seed: 0..3 season within the year, unique per (year,season)
// so the dispute rotates every season. GetCalendarMonth() is 1..12.
int QHOB_SeasonSeed()
{
    int nMonth  = GetCalendarMonth();          // 1..12
    int nSeason = (nMonth - 1) / 3;            // 0..3
    return GetCalendarYear() * 4 + nSeason;
}

// Seconds until the next UTC midnight (when QCD_IsDoneToday flips back). From
// SQLite so there is no game-clock drift, matching QCD_IsDoneToday's date().
int QHOB_SecsToTomorrow()
{
    sqlquery q = SqlPrepareQueryCampaign(QCD_DB,
        "SELECT CAST(strftime('%s', date('now','+1 day')) AS INTEGER)"
        + " - CAST(strftime('%s','now') AS INTEGER)");
    return SqlStep(q) ? SqlGetInt(q, 0) : 0;
}

// Bank index of the k-th question (k = 0..QHOB_ASKED-1) for season seed nSeed.
// step is odd, hence coprime with 16, so the picked indices are distinct.
int QHOB_QAt(int nSeed, int k)
{
    int nOff  = nSeed % QHOB_COUNT;
    int nStep = 2 * ((nSeed / QHOB_COUNT) % 8) + 1;
    return (nOff + k * nStep) % QHOB_COUNT;
}

// ------------------------------------------------------------
// The question bank. QHOB_Opt(i, 0) is always the correct answer; 1..3 are
// plausible distractors (real Shire names). Display order is rotated at show
// time so the correct letter varies with the season.

string QHOB_Q(int i)
{
    switch (i)
    {
        case 0:  return "Bilbo Baggins's mother, a famous Took, was named -- who?";
        case 1:  return "Frodo's mother, who drowned with his father in the Brandywine, was named -- who?";
        case 2:  return "And Frodo's father, who drowned with her, was -- who?";
        case 3:  return "Old Bilbo named which young cousin his heir, and left him Bag End?";
        case 4:  return "Otho Sackville-Baggins, who so coveted Bag End, was wed to which sharp-tongued lady?";
        case 5:  return "Otho and Lobelia's son -- later nicknamed 'Pimple' -- was named -- who?";
        case 6:  return "Samwise Gamgee's father, the old gardener of Bagshot Row, was better known as -- what?";
        case 7:  return "Whom did Samwise Gamgee marry, there in Hobbiton?";
        case 8:  return "Meriadoc Brandybuck's father, the Master of Buckland, was -- who?";
        case 9:  return "Peregrin Took's father, the Thain of the Shire, was -- who?";
        case 10: return "The Old Took, who lived to a hundred and thirty, was named -- who?";
        case 11: return "'Bullroarer' Took, who clubbed a goblin-king's head clean off, was rightly named -- who?";
        case 12: return "Before Bag End, young Frodo was raised at Brandy Hall -- seat of which family?";
        case 13: return "'Fatty' Bolger, left to keep up Frodo's ruse at Crickhollow, was rightly named -- who?";
        case 14: return "The great smials at Tuckborough -- the Great Smials -- are the seat of which family?";
        case 15: return "Bilbo's grand farewell party marked his 'eleventy-first' birthday. That is -- how old?";
    }
    return "";
}

string QHOB_Opt(int i, int j)
{
    switch (i * 4 + j)
    {
        // 0 — Belladonna Took
        case 0:  return "Belladonna Took.";
        case 1:  return "Mirabella Took.";
        case 2:  return "Donnamira Took.";
        case 3:  return "Primula Brandybuck.";
        // 1 — Primula Brandybuck
        case 4:  return "Primula Brandybuck.";
        case 5:  return "Esmeralda Took.";
        case 6:  return "Peony Baggins.";
        case 7:  return "Menegilda Goold.";
        // 2 — Drogo Baggins
        case 8:  return "Drogo Baggins.";
        case 9:  return "Dudo Baggins.";
        case 10: return "Fosco Baggins.";
        case 11: return "Otho Sackville-Baggins.";
        // 3 — Frodo Baggins
        case 12: return "Frodo Baggins.";
        case 13: return "Lotho Sackville-Baggins.";
        case 14: return "Meriadoc Brandybuck.";
        case 15: return "Folco Boffin.";
        // 4 — Lobelia
        case 16: return "Lobelia Bracegirdle.";
        case 17: return "Lalia Clayhanger.";
        case 18: return "Camellia Sackville.";
        case 19: return "Peony Baggins.";
        // 5 — Lotho
        case 20: return "Lotho Sackville-Baggins.";
        case 21: return "Ted Sandyman.";
        case 22: return "Sancho Proudfoot.";
        case 23: return "Otho the Younger.";
        // 6 — the Gaffer
        case 24: return "the Gaffer -- Hamfast Gamgee.";
        case 25: return "Andwise 'Andy' Roper.";
        case 26: return "Daddy Twofoot.";
        case 27: return "Halfred of Overhill.";
        // 7 — Rosie Cotton
        case 28: return "Rosie Cotton.";
        case 29: return "Marigold Gamgee.";
        case 30: return "Pretty Poppy Chubb.";
        case 31: return "Daisy Boffin.";
        // 8 — Saradoc Brandybuck
        case 32: return "Saradoc Brandybuck.";
        case 33: return "Rorimac Brandybuck.";
        case 34: return "Gorbadoc Brandybuck.";
        case 35: return "Merimac Brandybuck.";
        // 9 — Paladin Took
        case 36: return "Paladin Took.";
        case 37: return "Ferumbras Took.";
        case 38: return "Adalgrim Took.";
        case 39: return "Fortinbras Took.";
        // 10 — Gerontius Took
        case 40: return "Gerontius Took.";
        case 41: return "Isengrim Took.";
        case 42: return "Isumbras Took.";
        case 43: return "Bandobras Took.";
        // 11 — Bandobras Took
        case 44: return "Bandobras Took.";
        case 45: return "Gerontius Took.";
        case 46: return "Isengrim Took.";
        case 47: return "Ferumbras Took.";
        // 12 — Brandybuck
        case 48: return "The Brandybucks.";
        case 49: return "The Tooks.";
        case 50: return "The Bolgers.";
        case 51: return "The Boffins.";
        // 13 — Fredegar Bolger
        case 52: return "Fredegar Bolger.";
        case 53: return "Folco Boffin.";
        case 54: return "Odo Proudfoot.";
        case 55: return "Milo Burrows.";
        // 14 — Took
        case 56: return "The Tooks.";
        case 57: return "The Brandybucks.";
        case 58: return "The Bagginses.";
        case 59: return "The Bolgers.";
        // 15 — 111
        case 60: return "A hundred and eleven.";
        case 61: return "A hundred exactly.";
        case 62: return "A hundred and twenty-one.";
        case 63: return "A hundred and thirty-one.";
    }
    return "";
}

// Set answer-option token 6423..6426 for displayed slot nSlot (1..4) of the
// question the PC is on, and return its text. Called one-per-reply from
// q_hob_c_o1..o4 -- setting all four in one loop leaves only the first-written
// token resolving in the reply list in-engine (the riddle-game lesson), so the
// three distractor replies would render empty and be suppressed.
//
// Displayed slot s (0-based) holds data option j where (j + nRot) % 4 == s,
// i.e. j = (s - nRot + 4) % 4. So the correct option (j == 0) lands on slot
// nRot, i.e. Q_HOB_SLOT = nRot + 1 (see q_hob_c_show).
string QHOB_ShowOpt(object oPC, int nSlot)
{
    int nSeed = GetLocalInt(oPC, "Q_HOB_SEED");
    int nIdx  = GetLocalInt(oPC, "Q_HOB_IDX");
    int nQ    = QHOB_QAt(nSeed, nIdx);
    int nRot  = (nSeed + nIdx) % 4;
    int s     = nSlot - 1;                 // 0-based displayed slot
    int j     = (s - nRot + 4) % 4;        // which data option lands here
    string sOpt = QHOB_Opt(nQ, j);
    SetCustomToken(6423 + s, sOpt);
    return sOpt;
}

// ------------------------------------------------------------
// Odo's between-question patter.

string QHOB_RightLine(int k)
{
    switch (k % QHOB_LINES)
    {
        case 0: return "*Odo scratches a tick in the margin.* Just so, just so! You've studied your family-books, I'll grant you.";
        case 1: return "Right as rain! There's a head for lineage. The Ivy Bush could learn a thing from you.";
        case 2: return "Correct, and no mistake. *He dabs sand across the wet ink.* On we go.";
        case 3: return "Aye, that's the one! Half of Hobbiton gets that wrong, you know.";
        case 4: return "Quite right. *He nods, whiskers bobbing.* A friend of the family-books, plainly.";
    }
    return "";
}

string QHOB_WrongLine(int k)
{
    switch (k % QHOB_LINES)
    {
        case 0: return "*Odo winces and shakes his head.* No, no -- a common muddle, that. Mark it and move on.";
        case 1: return "Bless me, no. You've crossed your Bagginses with your Boffins there. Next!";
        case 2: return "Wrong, I'm afraid. The Ivy Bush would have your ears for that one. On we go.";
        case 3: return "*He sighs.* Not so. Half the Shire says the same and half the Shire is wrong.";
        case 4: return "Ah, no. A tidy guess, but the family-book says otherwise. Next question.";
    }
    return "";
}

// ------------------------------------------------------------
// Reward on a win: modest T1 payout -- 40-100 gp, QHOB_XP, and a keepsake
// from the family stores. A perfect 5/5 upgrades the keepsake to the Greater
// Ring of Scholars. Both prizes are existing module blueprints (verified in
// unpacked/), so they always resolve.

void QHOB_PayOut(object oPC, int nScore)
{
    GiveGoldToCreature(oPC, 40 + Random(61));
    GiveXPToCreature(oPC, QHOB_XP);
    if (nScore >= QHOB_ASKED)
        CreateItemOnObject("grscholar", oPC, 1);   // perfect 5/5 bonus
    else
        CreateItemOnObject("witchbud", oPC, 1);     // a pouch of pipe-weed
}

// ------------------------------------------------------------
// Core answer handler, shared by q_hob_a1..a4. nSlot = the displayed option
// (1..4) chosen. On the fifth answer the game is finalized here -- stamp,
// journal, payout -- so escape-closing on the result line cannot dodge the
// daily stamp or lose a paid-out prize.

void QHOB_Answer(object oPC, int nSlot)
{
    if (!GetLocalInt(oPC, "Q_HOB_ACTIVE")) return;
    int nIdx = GetLocalInt(oPC, "Q_HOB_IDX");
    if (nIdx >= QHOB_ASKED) return;

    int nRight = (nSlot == GetLocalInt(oPC, "Q_HOB_SLOT"));
    SetLocalInt(oPC, "Q_HOB_LAST", nRight);
    if (nRight)
        SetLocalInt(oPC, "Q_HOB_SCORE", GetLocalInt(oPC, "Q_HOB_SCORE") + 1);

    nIdx++;
    SetLocalInt(oPC, "Q_HOB_IDX", nIdx);
    if (nIdx < QHOB_ASKED) return;

    // Fifth answer -- game over. One attempt per day, win or lose.
    DeleteLocalInt(oPC, "Q_HOB_ACTIVE");
    int nScore = GetLocalInt(oPC, "Q_HOB_SCORE");
    int nWon   = (nScore >= QHOB_TO_WIN);
    SetLocalInt(oPC, "Q_HOB_RESULT", nWon ? 1 : 2);

    if (QCD_IsDoneToday(oPC, QHOB_QUEST)) return; // paranoid double-pay guard
    QCD_Stamp(oPC, QHOB_QUEST);
    AddJournalQuestEntry(QHOB_QUEST, 2, oPC, FALSE, FALSE, TRUE);
    if (nWon)
        QHOB_PayOut(oPC, nScore);
}