Script: q_bard_inc
// q_bard_inc.nss -- "Tales That Live Forever", Bard class-line I
// (roadmap: bard-line-early)
//
// PREFIX NOTE: this line uses q_bard_* rather than the obvious q_brd_*. The
// module already ships the boss-respawn tracker under the bare brd_* prefix
// (brd_db, brd_open_*, brd_vis_*, brd_sign.dlg -- see CLAUDE-boss-tracker.md),
// and q_brd_* sitting next to it in a flat single-directory source tree is an
// invitation to grep the wrong family. q_bard_* is unambiguous and every
// resref stays inside the 16-character limit.
//
// The early nodes (L1 / L8 / L15) of the Bard line. Lindir of the Hall of Fire
// keeps the Rivendell Upper Halls, where the singing does not really stop, and
// teaches exactly one thing: a song that is only ever sung by the one who made
// it dies with them. Only a Bard (GetLevelByClass(CLASS_TYPE_BARD, oPC) >= 1)
// is taken as a student; other folk get a seat by the fire and the third verse.
//
// The arc, one bounded first chunk. The greater disciplines of the line -- the
// naming-songs, the lament that stops a fight, the making of a true name -- are
// LATER content, NOT built here; Lindir only names them:
// * Node 1 (L1): Learn the opening phrase of the Lay of the Second Spring.
// Reward: the Chorister's Torc (q_bard_torc), a modest
// +1 CHA, Bard-only amulet.
// * Node 2 (L8): Lindir hands over the Unfinished Lay (q_bard_lay) -- a leaf
// of vellum that breaks off mid-line -- to be carried until an
// ending happens to the bearer.
// * Node 3 (L15): The missing line is spoken; Lindir has the finished Lay cut
// into the Rapier of the Deathless Song (q_bard_rap), a +2
// Bard-only rapier. Line I done, the greater forms named for a
// later day.
//
// LEVEL GATES ARE BARD CLASS LEVELS, not total hit dice: every node uses
// GetLevelByClass(CLASS_TYPE_BARD, oPC), so a multiclass cannot buy the line's
// rewards with levels taken elsewhere. (Matches the tightened paladin and monk
// lines, not the older GetHitDice ones.)
//
// Rewards are deliberately bard-legal: an amulet and a rapier, both weapons and
// gear a Bard is proficient with, so nothing on the line sits unusable in the
// pack. Use Limitation: Class Bard is iprp_classes row 1, verified against
// cloakofthebard.uti.json rather than guessed, and consistent with the confirmed
// Cleric 2 / Druid 3 / Monk 5 / Paladin 6 / Ranger 7 rows.
//
// Persistence: campaign DB "bardlinedb", per-character (oPC-keyed
// Get/SetCampaignInt -- relog- and restart-safe, mirrors the monk line).
// Key "song_stage": 0 none / 1 taught / 2 lay carried / 3 line done.
// One-off: the stage only advances, so rewards are never farmable.
//
// Lindir is script-spawned at the admin-placed waypoint AP_bardlineearly_1
// (Rivendell Upper Halls, rivendellupperha) by q_bard_spawn, fired from the area
// OnEnter wrapper q_bard_enter. Everything no-ops gracefully until the waypoint
// exists (see the roadmap item manual_steps) and never double-spawns.
// All engine calls used here are base NWScript builtins -- no framework include.
const string BRD_DB = "bardlinedb"; // shared campaign DB
const string BRD_STAGEKEY = "song_stage"; // per-character stage int
const string BRD_QUEST = "bard_lay"; // journal category tag
const string BRD_MSTR = "q_bard_mstr"; // giver blueprint + tag
const string BRD_WP_TAG = "AP_bardlineearly_1"; // admin-placed waypoint
const int BRD_LVL_NODE2 = 8; // return for the Lay at Bard 8
const int BRD_LVL_NODE3 = 15; // return to give the ending at Bard 15
// ------------------------------------------------------------
// Stage accessors (persistent, per character).
int BRD_GetStage(object oPC)
{
return GetCampaignInt(BRD_DB, BRD_STAGEKEY, oPC);
}
void BRD_SetStage(object oPC, int nStage)
{
SetCampaignInt(BRD_DB, BRD_STAGEKEY, nStage, oPC);
}
// The line's level currency: Bard class levels only.
int BRD_BardLevel(object oPC)
{
return GetLevelByClass(CLASS_TYPE_BARD, oPC);
}
// TRUE if oPC has at least one level of Bard -- the line's entry gate.
int BRD_IsBard(object oPC)
{
return BRD_BardLevel(oPC) >= 1;
}
// ------------------------------------------------------------
// Node action handlers (called from the conversation reply Scripts, so the
// reward fires the instant the reply is chosen -- escape-closing the following
// line cannot dodge a stage advance or a handed-out prize).
// Node 1: be taken as a student. Bard-gated, one-way (stage 0 -> 1).
void BRD_TakeLesson(object oPC)
{
if (!BRD_IsBard(oPC)) return;
if (BRD_GetStage(oPC) != 0) return;
BRD_SetStage(oPC, 1);
AddJournalQuestEntry(BRD_QUEST, 1, oPC, FALSE, FALSE, TRUE);
CreateItemOnObject("q_bard_torc", oPC, 1); // Chorister's Torc (+1 CHA, Bard)
}
// Node 2: receive the Unfinished Lay (stage 1 -> 2). Level gate is enforced by
// the dialogue StartingConditional; guarded here too against a stale node.
void BRD_TakeLay(object oPC)
{
if (BRD_GetStage(oPC) != 1) return;
if (BRD_BardLevel(oPC) < BRD_LVL_NODE2) return;
BRD_SetStage(oPC, 2);
AddJournalQuestEntry(BRD_QUEST, 2, oPC, FALSE, FALSE, TRUE);
if (!GetIsObjectValid(GetItemPossessedBy(oPC, "q_bard_lay")))
CreateItemOnObject("q_bard_lay", oPC, 1); // The Unfinished Lay (plot)
}
// Node 3: the finished Lay is cut into the Rapier of the Deathless Song
// (stage 2 -> 3). Consumes the vellum if the PC still carries it; the reward is
// given regardless so a lost page cannot brick the line at the last step.
void BRD_TakeEnding(object oPC)
{
if (BRD_GetStage(oPC) != 2) return;
if (BRD_BardLevel(oPC) < BRD_LVL_NODE3) return;
BRD_SetStage(oPC, 3);
object oLay = GetItemPossessedBy(oPC, "q_bard_lay");
if (GetIsObjectValid(oLay)) DestroyObject(oLay);
AddJournalQuestEntry(BRD_QUEST, 3, oPC, FALSE, FALSE, TRUE);
CreateItemOnObject("q_bard_rap", oPC, 1); // Rapier of the Deathless Song
}
// ------------------------------------------------------------
// Spawn Lindir at the admin-placed waypoint. Graceful no-op until the waypoint
// exists; never double-spawns (module-wide tag guard).
void BRD_SpawnMaster()
{
if (GetIsObjectValid(GetObjectByTag(BRD_MSTR))) return;
object oWP = GetWaypointByTag(BRD_WP_TAG);
if (!GetIsObjectValid(oWP)) return;
CreateObject(OBJECT_TYPE_CREATURE, BRD_MSTR, GetLocation(oWP));
}