Script: q_rid_inc
// q_rid_inc.nss — The Riddle Game shared helpers (roadmap: riddle-game)
//
// T1 weekly dialogue minigame, journal tag "riddle_game". A whispering
// wretch (q_rid_wretch — strongly implied to be, but never named, Gollum)
// squats in Bree Cave and plays the old game: seven riddles, answered by
// dialogue choice. Five or more right of seven wins a prize from its hoard;
// fewer wins nothing but taunting. One attempt per calendar week
// (quest_cd_inc QCD_IsDoneThisWeek — resets at the start of the ISO week,
// UTC, matching the "weekly" intent; win or lose, the week is stamped).
//
// The wretch is script-spawned at waypoint AP_riddlegame_1 (Bree Cave) by
// q_rid_spawn, fired from the area's OnEnter wrapper q_rid_enter. It no-ops
// gracefully until the admin places the waypoint, and never double-spawns.
// The placed CR-1029 Gollum boss (gollum.utc, Roll of the Fallen) is a
// completely separate blueprint and is untouched.
//
// Weekly riddle set: the bank holds QRID_COUNT riddles; each ISO week a
// deterministic seed (strftime('%Y%W') as an integer, so the whole server
// shares one weekly set) picks QRID_ASKED distinct riddles via a
// stride-through-the-bank walk (offset + k*step mod 16, step odd so it is
// coprime with 16), and rotates the displayed answer order per riddle so
// the correct letter moves around. A mid-game relog just clears the session
// locals — the game restarts from riddle one (same weekly set); the weekly
// stamp is only written when the seventh answer lands.
//
// Session state (locals on the PC, cleared/reset by q_rid_begin):
// Q_RID_ACTIVE 1 while a game is running
// Q_RID_SEED the week seed captured at game start
// Q_RID_IDX riddles answered so far (0..7)
// Q_RID_SCORE correct answers so far
// Q_RID_SLOT which displayed option (1..4) is correct for the shown riddle
// Q_RID_LAST 1 if the most recent answer was correct
// Q_RID_RESULT set at game end: 1 = won, 2 = lost
//
// Custom tokens (registered per-display by the StartingConditionals):
// 6360 time until the week turns 6361 riddle number / final score
// 6362 riddle verse 6363-6366 answer options 1-4
// 6367 right-answer quip 6368 wrong-answer taunt
#include "quest_cd_inc"
const string QRID_QUEST = "riddle_game"; // questcddb key + journal tag
const int QRID_COUNT = 16; // riddles in the bank
const int QRID_ASKED = 7; // riddles per game
const int QRID_TO_WIN = 5; // correct answers needed
const int QRID_XP = 250; // XP on a win
const int QRID_QUIPS = 5; // right/wrong flavor lines each
// ------------------------------------------------------------
// Weekly seed and week-turn countdown (both from SQLite so there is no
// game-clock drift; %Y%W matches QCD_IsDoneThisWeek's %Y-%W week).
int QRID_WeekSeed()
{
sqlquery q = SqlPrepareQueryCampaign(QCD_DB,
"SELECT CAST(strftime('%Y%W','now') AS INTEGER)");
return SqlStep(q) ? SqlGetInt(q, 0) : 0;
}
// Seconds until the next ISO week begins (Monday 00:00 UTC). 'weekday 1'
// after '+1 day' always lands on the NEXT Monday, even on a Monday.
int QRID_SecsToNextWeek()
{
sqlquery q = SqlPrepareQueryCampaign(QCD_DB,
"SELECT CAST(strftime('%s', date('now','+1 day','weekday 1')) AS INTEGER)"
+ " - CAST(strftime('%s','now') AS INTEGER)");
return SqlStep(q) ? SqlGetInt(q, 0) : 0;
}
// Bank index of the k-th riddle (k = 0..6) of the week seeded nSeed.
// step is odd, hence coprime with 16, so the 7 indices are distinct.
int QRID_RiddleAt(int nSeed, int k)
{
int nOff = nSeed % QRID_COUNT;
int nStep = 2 * ((nSeed / QRID_COUNT) % 8) + 1;
return (nOff + k * nStep) % QRID_COUNT;
}
// ------------------------------------------------------------
// The riddle bank. Classic Hobbit-style riddles (the old chestnuts
// reworded, plus originals in the same voice). QRID_Opt(i, 0) is always
// the correct answer; 1..3 are distractors. Display order is rotated at
// show time so the correct letter varies.
string QRID_Verse(int i)
{
switch (i)
{
case 0: return "Thirty white ponies stand on a hill of red: first they champ, then they stamp, then they stand still instead.";
case 1: return "It cannot be seen and cannot be felt, cannot be heard and cannot be smelt. It lies behind the stars and under the hills; every empty hole it fills.";
case 2: return "No hinge has it, nor key, nor lid, yet inside it a golden treasure is hid.";
case 3: return "It devours all things: bird and beast, tree and flower; it gnaws iron and bites steel, and grinds the tall mountains down to meal.";
case 4: return "It lives without breathing, cold as the grave; never thirsty, yet always drinking; clad all in mail that never clinks.";
case 5: return "Without a voice it cries, without wings it flutters; without teeth it bites, without a mouth it mutters.";
case 6: return "Its roots no eye has ever seen; it stands taller than the tallest trees. Up and up and up it goes, and yet it never grows.";
case 7: return "An eye set in a blue face beheld an eye in a green face. 'That eye is like this eye,' said the first, 'only set in a low place, and not in a high place.'";
case 8: return "Round it is, yet never whole; lighter than a whisper, heavier than a hill of gold. Most itself upon a finger, most dangerous in a pocket.";
case 9: return "All day it follows at your heel like a faithful hound; at evening it grows monstrous tall; it dies the moment the candle does.";
case 10: return "It belongs to you and to no one else, precious -- yet other people use it far more often than you do.";
case 11: return "Tall when it is young, short when it is old; all its life it wears a crown of fire, and one small breath is its death.";
case 12: return "It runs but never walks, it murmurs but never talks; it has a bed but never sleeps, it has a mouth but never eats.";
case 13: return "Speak its name aloud, even once, and it is gone.";
case 14: return "You make them with every step and carry them on every road, yet you always leave every one of them behind you.";
case 15: return "It is always coming but never arrives; and when at last you meet it, it has already changed its name.";
}
return "";
}
string QRID_Opt(int i, int j)
{
switch (i * 4 + j)
{
// 0 — teeth
case 0: return "Teeth in a mouth.";
case 1: return "The horses of the Rohirrim.";
case 2: return "Millstones grinding.";
case 3: return "Dwarves at supper.";
// 1 — the dark
case 4: return "The dark.";
case 5: return "A wraith.";
case 6: return "The cold wind off the mountains.";
case 7: return "Fear.";
// 2 — an egg
case 8: return "An egg.";
case 9: return "A dragon's hoard.";
case 10: return "A barrow-mound.";
case 11: return "A casket of mithril.";
// 3 — time
case 12: return "Time.";
case 13: return "A dragon.";
case 14: return "The Great Enemy.";
case 15: return "Rust.";
// 4 — a fish
case 16: return "A fish.";
case 17: return "A barrow-wight.";
case 18: return "An icicle.";
case 19: return "A drowned knight.";
// 5 — the wind
case 20: return "The wind.";
case 21: return "A ghost.";
case 22: return "A guilty conscience.";
case 23: return "A swarm of midges.";
// 6 — a mountain
case 24: return "A mountain.";
case 25: return "A mallorn of the Golden Wood.";
case 26: return "A tower of Gondor.";
case 27: return "An Ent.";
// 7 — sun on the daisies
case 28: return "The sun shining on the daisies.";
case 29: return "The moon mirrored in a pool.";
case 30: return "A dragon eyeing an emerald.";
case 31: return "Two elves staring at one another.";
// 8 — a ring
case 32: return "A ring.";
case 33: return "A gold coin.";
case 34: return "A crown.";
case 35: return "A promise.";
// 9 — a shadow
case 36: return "Your shadow.";
case 37: return "A hungry dog.";
case 38: return "Regret.";
case 39: return "A Black Rider.";
// 10 — your name
case 40: return "Your name.";
case 41: return "Your gold.";
case 42: return "Your pipe.";
case 43: return "Your grave.";
// 11 — a candle
case 44: return "A candle.";
case 45: return "A king.";
case 46: return "A pine tree.";
case 47: return "A dragon on its hoard.";
// 12 — a river
case 48: return "A river.";
case 49: return "A serpent.";
case 50: return "An orc-scout.";
case 51: return "A rumor.";
// 13 — silence
case 52: return "Silence.";
case 53: return "A secret.";
case 54: return "The dark.";
case 55: return "A promise.";
// 14 — footprints
case 56: return "Footprints.";
case 57: return "Memories.";
case 58: return "Debts.";
case 59: return "Ponies.";
// 15 — tomorrow
case 60: return "Tomorrow.";
case 61: return "The post.";
case 62: return "Winter.";
case 63: return "Death.";
}
return "";
}
// Set the answer-option custom token (6363..6366) for one displayed slot
// (nSlot = 1..4) of the riddle the PC is currently on, and return its text.
//
// This is called from a per-reply StartingConditional (q_rid_c_o1..o4), one
// SetCustomToken per call, immediately before that reply is rendered. The
// earlier design set all four tokens in a single loop inside q_rid_c_show;
// in-engine only the first-written token survived to the reply list, so the
// three distractor replies rendered empty text and NWN suppressed them —
// leaving only the correct answer. Setting each slot's token from its own
// reply conditional matches how the entry tokens (6361/6362) reliably resolve.
//
// Slot/rotation mapping mirrors q_rid_c_show: displayed slot index 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_RID_SLOT = nRot+1.
string QRID_ShowOpt(object oPC, int nSlot)
{
int nSeed = GetLocalInt(oPC, "Q_RID_SEED");
int nIdx = GetLocalInt(oPC, "Q_RID_IDX");
int nRid = QRID_RiddleAt(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 = QRID_Opt(nRid, j);
SetCustomToken(6363 + s, sOpt);
return sOpt;
}
// ------------------------------------------------------------
// Gollum-ism flavor pools for the between-riddle patter.
string QRID_RightQuip(int k)
{
switch (k % QRID_QUIPS)
{
case 0: return "Yesss... it knows that one. Everyone knows that one, precious. The next is harder, oh yes, much harder. Gollum.";
case 1: return "*It hisses and bites its long fingers.* Right, right! Clever fat one with its clever answers. We hates it, precious, we hates it forever!";
case 2: return "Gollum! Lucky guess, lucky guess! Luck runs out, precious, it always runs out. Sss...";
case 3: return "*Its pale eyes narrow to slits.* Correct... Someone told it! Someone whispered, yes! No matter, no matter. The next one has teeth.";
case 4: return "Right again, curse it and splash it! Old riddles, worn riddles, too easy for sneaky pocketses. The next one comes from deep under the roots, precious.";
}
return "";
}
string QRID_WrongTaunt(int k)
{
switch (k % QRID_QUIPS)
{
case 0: return "*It rocks back and forth, choking with wet laughter.* Wrong! Wrong! It doesn't know, precious, it doesn't know! Gollum, gollum!";
case 1: return "Sss, stupid! Even the fishes know that one, and fishes don't talk. *It grins with too many teeth.* Next riddle, yes?";
case 2: return "*It drums its long fingers on the stone, delighted.* No, no, NO. Not even close, precious. The dark eats another point, yes it does.";
case 3: return "Wrong answers taste the sweetest, gollum! It guesses like a troll at a tea party. Shall we go on? Oh yes, we shall.";
case 4: return "*It whispers to itself.* It missed, precious, it missed... maybe it isn't so clever after all, no, not clever at all. On we goes.";
}
return "";
}
// ------------------------------------------------------------
// Reward on a win: 100-300 gp, QRID_XP, and one random minor treasure from
// the hoard (all existing blueprints, verified; nothing above trinket tier
// — the game is T1 and open to level 1).
void QRID_PayOut(object oPC)
{
GiveGoldToCreature(oPC, 100 + Random(201));
GiveXPToCreature(oPC, QRID_XP);
string sPrize;
switch (d8())
{
case 1: sPrize = "nw_it_mpotion003"; break; // Potion of Cure Critical Wounds
case 2: sPrize = "nw_it_mring009"; break; // Ring of Cyan
case 3: sPrize = "nw_it_mring010"; break; // Ring of Crimson
case 4: sPrize = "nw_it_mring011"; break; // Ring of Jade
case 5: sPrize = "nw_it_gem013"; break; // Alexandrite
case 6: sPrize = "nw_it_mring001"; break; // Ring of Protection +1
case 7: sPrize = "nw_it_mring013"; break; // Ring of Scholars
default: sPrize = "nw_it_mring006"; break; // Ring of Clear Thought +1
}
CreateItemOnObject(sPrize, oPC, 1);
}
// ------------------------------------------------------------
// Core answer handler, shared by q_rid_a1..a4. nSlot = the displayed
// option (1..4) the player chose. On the seventh answer the game is
// finalized right here — stamp, journal, payout — so escape-closing the
// dialogue on the result line can never dodge the weekly stamp or lose a
// paid-out prize.
void QRID_Answer(object oPC, int nSlot)
{
if (!GetLocalInt(oPC, "Q_RID_ACTIVE")) return;
int nIdx = GetLocalInt(oPC, "Q_RID_IDX");
if (nIdx >= QRID_ASKED) return;
int nRight = (nSlot == GetLocalInt(oPC, "Q_RID_SLOT"));
SetLocalInt(oPC, "Q_RID_LAST", nRight);
if (nRight)
SetLocalInt(oPC, "Q_RID_SCORE", GetLocalInt(oPC, "Q_RID_SCORE") + 1);
nIdx++;
SetLocalInt(oPC, "Q_RID_IDX", nIdx);
if (nIdx < QRID_ASKED) return;
// Seventh answer — game over. One attempt per week, win or lose.
DeleteLocalInt(oPC, "Q_RID_ACTIVE");
int nScore = GetLocalInt(oPC, "Q_RID_SCORE");
int nWon = (nScore >= QRID_TO_WIN);
SetLocalInt(oPC, "Q_RID_RESULT", nWon ? 1 : 2);
if (QCD_IsDoneThisWeek(oPC, QRID_QUEST)) return; // paranoid double-pay guard
QCD_Stamp(oPC, QRID_QUEST);
AddJournalQuestEntry(QRID_QUEST, 2, oPC, FALSE, FALSE, TRUE);
if (nWon)
QRID_PayOut(oPC);
}