Script: merit_db

// merit_db.nss — Merit Award System database helpers
//
// Campaign DB: "meritdb" (SQLite)
// Schema:  players(cdkey PK, name, last_login, bugs, exploits, features, merit_spent)
//
// Merit rates: defect=1pt  exploit=3pts  feature=2pts

const string MERIT_DB            = "meritdb";
const int    MERIT_BUG_VALUE     = 1;
const int    MERIT_EXPLOIT_VALUE = 3;
const int    MERIT_FEATURE_VALUE = 2;

const int MERIT_COST_1 = 5;
const int MERIT_COST_2 = 10;
const int MERIT_COST_3 = 20;
const int MERIT_COST_4 = 35;
const int MERIT_COST_5 = 50;

// ------------------------------------------------------------
// Schema

void Merit_InitDb()
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "CREATE TABLE IF NOT EXISTS players (" +
        "cdkey TEXT PRIMARY KEY," +
        "name TEXT," +
        "last_login TEXT," +
        "bugs INTEGER DEFAULT 0," +
        "exploits INTEGER DEFAULT 0," +
        "features INTEGER DEFAULT 0," +
        "merit_spent INTEGER DEFAULT 0)");
    SqlStep(q);
}

// ------------------------------------------------------------
// Login tracking

void Merit_RecordLogin(object oPC)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "INSERT INTO players(cdkey, name, last_login) VALUES(@k, @n, datetime('now'))" +
        " ON CONFLICT(cdkey) DO UPDATE SET name=excluded.name, last_login=excluded.last_login");
    SqlBindString(q, "@k", GetPCPublicCDKey(oPC));
    SqlBindString(q, "@n", GetPCPlayerName(oPC));
    SqlStep(q);
}

// Sent on login only when the player has at least one contribution.
void Merit_LoginMessage(object oPC)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "SELECT bugs, exploits, features, merit_spent FROM players WHERE cdkey=@k");
    SqlBindString(q, "@k", GetPCPublicCDKey(oPC));
    if (!SqlStep(q)) return;

    int nBugs  = SqlGetInt(q, 0);
    int nExp   = SqlGetInt(q, 1);
    int nFtr   = SqlGetInt(q, 2);
    int nSpent = SqlGetInt(q, 3);

    if (nBugs + nExp + nFtr == 0) return;

    int nEarned = nBugs  * MERIT_BUG_VALUE
                + nExp   * MERIT_EXPLOIT_VALUE
                + nFtr   * MERIT_FEATURE_VALUE;
    int nAvail  = nEarned - nSpent;

    SendMessageToPC(oPC,
        "\n[Merit] Thank you for your contributions to this world!\n" +
        "  Defects reported:    " + IntToString(nBugs) + "\n" +
        "  Exploits reported:   " + IntToString(nExp)  + "\n" +
        "  Features implemented:" + IntToString(nFtr)  + "\n" +
        "Merit balance: " + IntToString(nAvail) + " pts available to spend.\n" +
        "Visit the Keeper of Records in the Prancing Pony to redeem rewards.");
}

// ------------------------------------------------------------
// Awarding

void Merit_AwardBug(string sCdKey)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "UPDATE players SET bugs=bugs+1 WHERE cdkey=@k");
    SqlBindString(q, "@k", sCdKey);
    SqlStep(q);
}

void Merit_AwardExploit(string sCdKey)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "UPDATE players SET exploits=exploits+1 WHERE cdkey=@k");
    SqlBindString(q, "@k", sCdKey);
    SqlStep(q);
}

void Merit_AwardFeature(string sCdKey)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "UPDATE players SET features=features+1 WHERE cdkey=@k");
    SqlBindString(q, "@k", sCdKey);
    SqlStep(q);
}

// ------------------------------------------------------------
// Balance

int Merit_Available(string sCdKey)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "SELECT bugs, exploits, features, merit_spent FROM players WHERE cdkey=@k");
    SqlBindString(q, "@k", sCdKey);
    if (!SqlStep(q)) return 0;
    int nBugs  = SqlGetInt(q, 0);
    int nExp   = SqlGetInt(q, 1);
    int nFtr   = SqlGetInt(q, 2);
    int nSpent = SqlGetInt(q, 3);
    return nBugs  * MERIT_BUG_VALUE
         + nExp   * MERIT_EXPLOIT_VALUE
         + nFtr   * MERIT_FEATURE_VALUE
         - nSpent;
}

void Merit_Spend(string sCdKey, int nCost)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "UPDATE players SET merit_spent=merit_spent+@c WHERE cdkey=@k");
    SqlBindInt(q, "@c", nCost);
    SqlBindString(q, "@k", sCdKey);
    SqlStep(q);
}

// ------------------------------------------------------------
// NPC conversation tokens (5020-5027)
// Call from reply action scripts; tokens are set before the next entry renders.

void Merit_SetNpcTokens(object oPC)
{
    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "SELECT bugs, exploits, features, merit_spent FROM players WHERE cdkey=@k");
    SqlBindString(q, "@k", GetPCPublicCDKey(oPC));

    int nBugs  = 0;
    int nExp   = 0;
    int nFtr   = 0;
    int nSpent = 0;
    if (SqlStep(q))
    {
        nBugs  = SqlGetInt(q, 0);
        nExp   = SqlGetInt(q, 1);
        nFtr   = SqlGetInt(q, 2);
        nSpent = SqlGetInt(q, 3);
    }

    int nBugPts = nBugs  * MERIT_BUG_VALUE;
    int nExpPts = nExp   * MERIT_EXPLOIT_VALUE;
    int nFtrPts = nFtr   * MERIT_FEATURE_VALUE;
    int nEarned = nBugPts + nExpPts + nFtrPts;
    int nAvail  = nEarned - nSpent;

    SetCustomToken(5020, IntToString(nBugs));
    SetCustomToken(5021, IntToString(nExp));
    SetCustomToken(5022, IntToString(nFtr));
    SetCustomToken(5023, IntToString(nBugPts));
    SetCustomToken(5024, IntToString(nExpPts));
    SetCustomToken(5025, IntToString(nFtrPts));
    SetCustomToken(5026, IntToString(nEarned));
    SetCustomToken(5027, IntToString(nAvail));
}

// ------------------------------------------------------------
// DM emote-wand player list (tokens 5001-5010)
// Call from reply action scripts; tokens are set before E_PLAYER_LIST renders.

void Merit_BuildPage(object oDM)
{
    int nOff = GetLocalInt(oDM, "merit_page_off");

    sqlquery qCount = SqlPrepareQueryCampaign(MERIT_DB, "SELECT COUNT(*) FROM players");
    int nTotal = 0;
    if (SqlStep(qCount)) nTotal = SqlGetInt(qCount, 0);
    SetLocalInt(oDM, "merit_page_total", nTotal);

    int nPages = (nTotal + 8) / 9;
    if (nPages == 0) nPages = 1;
    int nPage = nOff / 9 + 1;
    SetCustomToken(5010, "Page " + IntToString(nPage) + " of " + IntToString(nPages));

    int i;
    for (i = 0; i < 9; i++)
    {
        DeleteLocalString(oDM, "merit_slot_" + IntToString(i) + "_cdkey");
        DeleteLocalString(oDM, "merit_slot_" + IntToString(i) + "_name");
        SetCustomToken(5001 + i, "(empty)");
    }

    sqlquery q = SqlPrepareQueryCampaign(MERIT_DB,
        "SELECT cdkey, name, bugs, exploits, features, merit_spent" +
        " FROM players ORDER BY last_login DESC LIMIT 9 OFFSET @off");
    SqlBindInt(q, "@off", nOff);

    i = 0;
    while (SqlStep(q) && i < 9)
    {
        string sCdKey  = SqlGetString(q, 0);
        string sName   = SqlGetString(q, 1);
        int nBugs      = SqlGetInt(q, 2);
        int nExp       = SqlGetInt(q, 3);
        int nFtr       = SqlGetInt(q, 4);
        int nSpent     = SqlGetInt(q, 5);
        int nAvail     = nBugs  * MERIT_BUG_VALUE
                       + nExp   * MERIT_EXPLOIT_VALUE
                       + nFtr   * MERIT_FEATURE_VALUE
                       - nSpent;

        SetLocalString(oDM, "merit_slot_" + IntToString(i) + "_cdkey", sCdKey);
        SetLocalString(oDM, "merit_slot_" + IntToString(i) + "_name",  sName);

        string sLabel = sName
            + " [D:" + IntToString(nBugs)
            + " E:" + IntToString(nExp)
            + " F:" + IntToString(nFtr)
            + " bal:" + IntToString(nAvail) + "]";
        SetCustomToken(5001 + i, sLabel);
        i++;
    }
}