feat(rust): chat titles rank twenty-three conditions, and admins name the categories (PLAN_REDESIGNS §5, D172-D175, D209)
- Schema: fourteen title columns on rust_player_wipe_stats (the two best_* distances move by GREATEST, the rest are sums), rust_weapon_kills (kills by weapon prefab name) and rust_title_categories (the admin's title per category). purge.sql drops the two tables. - Ingest: player.tally's new fields, in one statement per frame, none for an older plugin's frame. - Titles: 23 categories plus playtime, each naming where it is read from (a sum, a MAX, npc_kills - animal_kills in signed arithmetic, or a named list). The bow/melee/blade/revolver/wood/ore/plants lists are one file, applied when a title is read. "NPC kills" ranks human NPCs only (D209). - A rule's text may be empty, meaning the category's title: the rule's own, then the admin's, then the default. Typed-only-markup is still refused. A rename forgets every server's cached answer. - Admin API: GET /admin/rust/title-categories and PUT /admin/rust/title-categories/:stat (empty text resets); the server list returns them too. Swagger fragment and routes.manifest.json regenerated (63 routes, against the pinned core). - Screen: every category in the rule form, the category's title as a placeholder, and a Category titles section with Save and Reset. The weapon lists are unverified until the rig probe runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -13,6 +13,7 @@ const core = require('../../core')
|
||||
const EVENTS = 'rust_events'
|
||||
const STATS = 'rust_player_wipe_stats'
|
||||
const GATHER = 'rust_gather_totals'
|
||||
const WEAPONS = 'rust_weapon_kills'
|
||||
const PLAYERS = 'rust_players'
|
||||
const WIPES = 'rust_wipes'
|
||||
const PRESENCE = 'rust_presence'
|
||||
@@ -132,6 +133,78 @@ async function addStats({ serverId, wipeId, steamId }, deltas = {}) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The tally's counts for the chat-title conditions (PLAN_REDESIGNS §5.1): the
|
||||
* wire field each is, and the column it adds to. All sums.
|
||||
*/
|
||||
const TITLE_SUMS = {
|
||||
animalKills: 'animal_kills',
|
||||
headshots: 'headshots',
|
||||
apcKills: 'apc_kills',
|
||||
heliKills: 'heli_kills',
|
||||
built: 'built',
|
||||
repaired: 'repaired',
|
||||
healed: 'healed',
|
||||
rockets: 'rockets',
|
||||
explosives: 'explosives',
|
||||
missions: 'missions',
|
||||
}
|
||||
|
||||
/** The two *best* fields and their columns: the longest single kill, never a sum. */
|
||||
const TITLE_BESTS = { bestPvpM: 'best_pvp_m', bestPveM: 'best_pve_m' }
|
||||
|
||||
/** Item categories the plugin sends under `crafted`, and the column each counts in. */
|
||||
const CRAFTED = { attire: 'crafted_attire', weapon: 'crafted_weapons' }
|
||||
|
||||
/**
|
||||
* Adds one tally's title counts to the player's row for the wipe. A sum adds;
|
||||
* a best is GREATEST of the stored and the sent, because a tally carries its
|
||||
* interval's maximum and two maxima added are not a distance. Nothing is
|
||||
* written when the tally carried none of them, so an older plugin's frame
|
||||
* costs no statement.
|
||||
*/
|
||||
async function addTitleStats({ serverId, wipeId, steamId }, frame = {}) {
|
||||
if (!serverId || !steamId) return
|
||||
|
||||
const sums = {}
|
||||
for (const [field, col] of Object.entries(TITLE_SUMS)) sums[col] = Math.max(0, Math.floor(Number(frame[field]) || 0))
|
||||
|
||||
const crafted = frame.crafted && typeof frame.crafted === 'object' ? frame.crafted : {}
|
||||
for (const [category, col] of Object.entries(CRAFTED)) sums[col] = Math.max(0, Math.floor(Number(crafted[category]) || 0))
|
||||
|
||||
const bests = {}
|
||||
for (const [field, col] of Object.entries(TITLE_BESTS)) {
|
||||
const m = Number(frame[field])
|
||||
bests[col] = Number.isFinite(m) && m > 0 ? Math.min(Math.round(m * 10) / 10, 999999.9) : 0
|
||||
}
|
||||
|
||||
if (![...Object.values(sums), ...Object.values(bests)].some((v) => v > 0)) return
|
||||
|
||||
const cols = [...Object.keys(sums), ...Object.keys(bests)]
|
||||
|
||||
await core.query(
|
||||
`INSERT INTO ${STATS} (server_id, wipe_id, steam_id, ${cols.join(', ')}, last_seen)
|
||||
VALUES (?, ?, ?, ${cols.map(() => '?').join(', ')}, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
${Object.keys(sums).map((c) => `${c} = ${c} + VALUES(${c})`).join(',\n ')},
|
||||
${Object.keys(bests).map((c) => `${c} = GREATEST(${c}, VALUES(${c}))`).join(',\n ')},
|
||||
last_seen = CURRENT_TIMESTAMP`,
|
||||
[serverId, wipeId || '', steamId, ...Object.values(sums), ...Object.values(bests)],
|
||||
)
|
||||
}
|
||||
|
||||
/** One weapon's credited kills (§5.3), added as a delta like a gathered resource. */
|
||||
async function addWeaponKills({ serverId, wipeId, steamId }, weapon, kills) {
|
||||
if (!serverId || !steamId || !weapon || !(kills > 0)) return
|
||||
|
||||
await core.query(
|
||||
`INSERT INTO ${WEAPONS} (server_id, wipe_id, steam_id, weapon, kills)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE kills = kills + VALUES(kills)`,
|
||||
[serverId, wipeId || '', steamId, String(weapon).slice(0, 64), kills],
|
||||
)
|
||||
}
|
||||
|
||||
async function addGathered({ serverId, wipeId, steamId }, resource, amount) {
|
||||
if (!serverId || !steamId || !resource || !(amount > 0)) return
|
||||
|
||||
@@ -312,6 +385,8 @@ module.exports = {
|
||||
touchPlayer,
|
||||
addStats,
|
||||
addGathered,
|
||||
addTitleStats,
|
||||
addWeaponKills,
|
||||
replacePresence,
|
||||
pruneEvents,
|
||||
recentEvents,
|
||||
|
||||
Reference in New Issue
Block a user