From 5c67d941b9b183824a0a52f10f2556cb6a62e74c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 19:17:48 +0000 Subject: [PATCH 1/2] Add flags for ineligible characters and green-con mobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 11% cap asterisk (*) already appears in the XP/kill cell. Two more flags now appear in the same cell and in the kills/time columns: † — character is too far below the highest party member to receive XP (level-spread eligibility check) ‡ — mob cons green to the highest party member, so no XP is awarded The eligible flag is threaded through split.js → award.js → kills.js so the UI can distinguish "zero XP because ineligible" from "zero XP because green con" without re-implementing eligibility logic in the DOM. --- src/award.js | 3 +++ src/kills.js | 3 +++ src/split.js | 13 ++++++++++--- src/ui.js | 23 +++++++++++++++++------ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/award.js b/src/award.js index 0022981..d4f0ed9 100644 --- a/src/award.js +++ b/src/award.js @@ -21,6 +21,8 @@ const PER_MOB_CAP = 0.11; * @property {number} share proportional share of the party total (0-1) * @property {number} xp XP this character receives for the kill (capped) * @property {boolean} capApplied true if the 11% per-mob cap clamped this slice + * @property {boolean} eligible false when the character is too far below the + * group's highest level to receive any XP */ /** @@ -54,6 +56,7 @@ export function awardXp(party, mobLevel, zem) { share: a.share, xp: capApplied ? cap : uncapped, capApplied, + eligible: a.eligible, }); }), ), diff --git a/src/kills.js b/src/kills.js index c3df5a3..887fa6e 100644 --- a/src/kills.js +++ b/src/kills.js @@ -15,6 +15,8 @@ import { awardXp } from "./award.js"; * @property {number} remaining XP still needed to reach the next level * @property {number} kills kills needed (Infinity if xpPerKill is 0) * @property {boolean} capApplied true if the 11% per-mob cap clamped xpPerKill + * @property {boolean} eligible false when the character is too far below the + * group's highest level to receive any XP */ /** @@ -50,6 +52,7 @@ export function killsToNextLevel(party, mobLevel, zem) { remaining, kills, capApplied: a.capApplied, + eligible: a.eligible, }); }); diff --git a/src/split.js b/src/split.js index f54baa0..33f4ede 100644 --- a/src/split.js +++ b/src/split.js @@ -26,6 +26,8 @@ import { groupXpEligibility } from "./eligibility.js"; * @typedef {Object} Allocation * @property {object} character the character this share belongs to * @property {number} share fraction of the XP pool (0-1); shares sum to 1 + * @property {boolean} eligible false when the character is too far below the + * group's highest level to receive any XP (groupXpEligibility check) */ /** @@ -46,15 +48,20 @@ export function splitXp(party) { } const { characters, penaltiesInEffect, maxLevel } = party; - const weights = characters.map((c) => { - if (!groupXpEligibility(c.level, maxLevel)) return 0; + const eligible = characters.map((c) => groupXpEligibility(c.level, maxLevel)); + const weights = characters.map((c, i) => { + if (!eligible[i]) return 0; return penaltiesInEffect ? c.xpToNextLevel : totalXpToLevel(c.level, 1); }); const total = weights.reduce((sum, w) => sum + w, 0); return Object.freeze( characters.map((c, i) => - Object.freeze({ character: c, share: weights[i] / total }), + Object.freeze({ + character: c, + share: weights[i] / total, + eligible: eligible[i], + }), ), ); } diff --git a/src/ui.js b/src/ui.js index e292316..dc27914 100644 --- a/src/ui.js +++ b/src/ui.js @@ -165,6 +165,13 @@ function refresh() { if (player.capApplied) { r.gk.appendChild(el("span", { class: "cap-flag" }, " *")); r.gk.title = "11% per-mob cap applied — excess XP is lost"; + } else if (!player.eligible) { + r.gk.appendChild(el("span", { class: "cap-flag" }, " †")); + r.gk.title = + "Character is too far below the highest party member to receive XP"; + } else if (player.xpPerKill === 0) { + r.gk.appendChild(el("span", { class: "cap-flag" }, " ‡")); + r.gk.title = "Mob cons green to the highest party member — no XP awarded"; } else { r.gk.title = ""; } @@ -177,12 +184,16 @@ function refresh() { r.xr.textContent = fmtNum(player.remaining) + " XP"; r.xc.textContent = fmtNum(player.character.xpToNextLevel) + " XP"; - r.kl.textContent = Number.isFinite(player.kills) - ? fmtNum(player.kills) - : dash; - r.tm.textContent = Number.isFinite(player.kills) - ? fmtMins(player.kills * state.enc.minutesPerKill) - : dash; + if (Number.isFinite(player.kills)) { + r.kl.textContent = fmtNum(player.kills); + r.tm.textContent = fmtMins(player.kills * state.enc.minutesPerKill); + } else if (!player.eligible) { + r.kl.textContent = "—†"; + r.tm.textContent = "—†"; + } else { + r.kl.textContent = "—‡"; + r.tm.textContent = "—‡"; + } }); // Totals row. From fa1417005f893269248c9dc8f4f371ce8e025cd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 19:23:04 +0000 Subject: [PATCH 2/2] Expand section 1 examples to show the squared result --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fad4421..e7f9679 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ baseMobXp = mobLevel^2 * ZEM - *A Froglok* — a level 3 mob in **Innothule Swamp**: ``` - baseMobXp = 3^2 * ZEM + baseMobXp = 3^2 * ZEM = 9 * ZEM ``` - *Lord Bob* — a level 65 mob in **Velk's Labyrinth** (Velketor's Labyrinth): ``` - baseMobXp = 65^2 * ZEM + baseMobXp = 65^2 * ZEM = 4,225 * ZEM ``` These are the base values before the group bonus, consider modifier, and 11%