feat: prestige reads current LoC; tiered CI; v0.1.6
Prestige loop
- prestigeGain + canPrestige now read state.layers[primary].amount instead
of .best. The threshold is "do you currently hold N LoC", not "have you
ever held N LoC", and the gain reflects what's actually being traded.
- resetLayer wipes best and total too. Each prestige is a fresh slate;
nothing rides through to bias the next run's gain calculation.
- PrestigeButton tooltip: "current/threshold" line and explanatory copy
now reference current upstream amount.
CI
- Tiered gating via job-level `if:`. Non-main branch pushes run only the
pre-build set (lint, secrets-scan, vuln-scan, sast). Pull requests AND
main pushes additionally run test + build-images + image-scan. Main
pushes alone run the Harbor push + cosign + per-package git tags.
- `needs:` cascades skipped upstream jobs to dependents, so we don't
duplicate the condition on every downstream job.
Both package versions bumped to 0.1.6; patch notes entry added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,25 @@
|
||||
name: CI
|
||||
|
||||
# Tiered CI:
|
||||
# - non-main push: pre-build (lint, secrets-scan, vuln-scan, sast) only.
|
||||
# Fast feedback while iterating on a feature branch.
|
||||
# - pull_request: above + test + build-images + image-scan.
|
||||
# Heavy verification before code lands.
|
||||
# - push to main: above + push to Harbor + cosign + per-package git tags.
|
||||
# Same set as PRs, plus the one job that actually publishes.
|
||||
#
|
||||
# Job-level `if:` is the only gate — `needs:` cascades a skipped upstream
|
||||
# job to its dependents automatically, so we don't have to repeat conditions
|
||||
# on every downstream job.
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# PR-or-main-push tier.
|
||||
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
@@ -210,6 +224,8 @@ jobs:
|
||||
path: semgrep.sarif
|
||||
|
||||
build-images:
|
||||
# PR-or-main-push tier. Skipped on non-main feature-branch pushes.
|
||||
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, lint, secrets-scan, vuln-scan, sast]
|
||||
steps:
|
||||
@@ -254,6 +270,8 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
image-scan:
|
||||
# PR-or-main-push tier. Skipped on non-main feature-branch pushes.
|
||||
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-images
|
||||
steps:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@teh-riehl/client",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -24,7 +24,8 @@ interface Props {
|
||||
*
|
||||
* Disabled when the player doesn't meet the threshold. Hover for a full
|
||||
* read-out: what gets reset, what gets gained, and where the threshold sits
|
||||
* relative to the player's current best.
|
||||
* relative to the player's CURRENT upstream amount (the resource that's
|
||||
* actually being traded).
|
||||
*/
|
||||
export function PrestigeButton({ layer }: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -37,7 +38,7 @@ export function PrestigeButton({ layer }: Props) {
|
||||
: 0,
|
||||
);
|
||||
const primaryId = layer.branches[0];
|
||||
const primaryBest = useAppSelector((s) => (primaryId ? (s.layers[primaryId]?.best ?? 0) : 0));
|
||||
const primaryAmount = useAppSelector((s) => (primaryId ? (s.layers[primaryId]?.amount ?? 0) : 0));
|
||||
|
||||
if (!layer.reset) return null;
|
||||
|
||||
@@ -81,7 +82,7 @@ export function PrestigeButton({ layer }: Props) {
|
||||
<Row label="gain" value={`+${formatNumber(gain)} ${symbol}`.trim()} accent={can} />
|
||||
<Row
|
||||
label="threshold"
|
||||
value={`${formatNumber(primaryBest)} / ${formatNumber(requirement)} ${primarySymbol}`.trim()}
|
||||
value={`${formatNumber(primaryAmount)} / ${formatNumber(requirement)} ${primarySymbol}`.trim()}
|
||||
accent={can}
|
||||
/>
|
||||
{Math.abs(requirement - baseReq) > 1e-9 && (
|
||||
@@ -92,7 +93,7 @@ export function PrestigeButton({ layer }: Props) {
|
||||
/>
|
||||
)}
|
||||
<p className="text-comment leading-snug">
|
||||
// Gain scales with your best {primaryName} this run.
|
||||
// Gain scales with your current {primaryName}. Best + total reset on prestige.
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -69,16 +69,24 @@ const layersSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
/** Reset a layer (used during prestige). */
|
||||
/**
|
||||
* Reset a layer (used during prestige). Wipes amount, best, total,
|
||||
* and buyables back to zero. Milestones and achievements persist —
|
||||
* they're permanent "you reached this once" markers, not run-state.
|
||||
*
|
||||
* `keepUpgrades` lets a milestone effect (e.g. "Ten in a Row") opt
|
||||
* into keeping owned upgrades through the reset. Off by default.
|
||||
*/
|
||||
resetLayer(state, action: PayloadAction<{ layerId: string; keepUpgrades?: boolean }>) {
|
||||
const layer = state[action.payload.layerId];
|
||||
if (!layer) return;
|
||||
layer.amount = 0;
|
||||
layer.best = 0;
|
||||
layer.total = 0;
|
||||
layer.buyables = {};
|
||||
if (!action.payload.keepUpgrades) {
|
||||
layer.upgrades = [];
|
||||
}
|
||||
// milestones and achievements persist across normal resets
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,24 +22,30 @@ export function effectiveRequirement(layer: Layer, state: RootState): number {
|
||||
* Resolve the prestige currency the player would gain right now.
|
||||
* Returns `null` when the layer has no reset config (i.e. not prestigeable).
|
||||
*
|
||||
* When a requirement multiplier is active, we divide the formula input by
|
||||
* that multiplier so a `prestigePoints(req, pow)` formula stays in sync
|
||||
* with the effective gate — the player isn't surprised by a "can prestige
|
||||
* but gain is 0" window.
|
||||
* Reads CURRENT amount (not best/total) so the gain is what the player is
|
||||
* actually trading away this prestige — not a high-water mark that survived
|
||||
* a previous run. Pairs with resetLayer() wiping best+total so each run is
|
||||
* a clean slate.
|
||||
*
|
||||
* When a requirement multiplier is active (e.g. force-push), we divide the
|
||||
* formula input by that multiplier so a `prestigePoints(req, pow)` formula
|
||||
* stays in sync with the effective gate — the player isn't surprised by a
|
||||
* "can prestige but gain is 0" window.
|
||||
*/
|
||||
export function prestigeGain(layer: Layer, state: RootState): number | null {
|
||||
if (!layer.reset) return null;
|
||||
const ctx = buildEvalContext(state.layers, state.game.elapsedMs);
|
||||
const primary = layer.branches[0];
|
||||
if (!primary) return 0;
|
||||
const best = state.layers[primary]?.best ?? 0;
|
||||
const amount = state.layers[primary]?.amount ?? 0;
|
||||
const reqMult = crossLayerMultiplier(layer.id, 'requirement', undefined, ctx);
|
||||
return evalFormula(layer.reset.gainFormula, best / reqMult, ctx);
|
||||
return evalFormula(layer.reset.gainFormula, amount / reqMult, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player meets the requirement to perform a reset right now.
|
||||
* Currently checks the primary upstream layer's amount against requirement.
|
||||
* Whether the player can perform the reset right now. Compares CURRENT
|
||||
* upstream amount (the resource the player is about to trade) against the
|
||||
* effective requirement — not the upstream layer's best.
|
||||
*/
|
||||
export function canPrestige(layer: Layer, state: RootState): boolean {
|
||||
if (!layer.reset) return false;
|
||||
@@ -48,7 +54,7 @@ export function canPrestige(layer: Layer, state: RootState): boolean {
|
||||
const requirement = effectiveRequirement(layer, state);
|
||||
const primary = layer.branches[0];
|
||||
if (!primary) return false;
|
||||
return (state.layers[primary]?.best ?? 0) >= requirement;
|
||||
return (state.layers[primary]?.amount ?? 0) >= requirement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,17 @@ export interface PatchNote {
|
||||
* Vite/tsc can resolve them without bundler magic. Newest entry first.
|
||||
*/
|
||||
export const patchNotes: ReadonlyArray<PatchNote> = [
|
||||
{
|
||||
version: '0.1.6',
|
||||
date: '2026-05-17',
|
||||
body: [
|
||||
'## v0.1.6 — Prestige loop, fresh slate',
|
||||
'',
|
||||
'- **Commit gain now scales with your CURRENT Lines of Code, not your best.** The threshold tooltip shows current/threshold, and pushing past your old best no longer matters once you prestige. Each run is its own trade.',
|
||||
"- **`best` and `total` reset on prestige.** Past v0.1.5 builds let those high-water marks ride through resets — they don't any more. A fresh prestige is a fresh slate, the way the design intended.",
|
||||
'- **CI is tiered** so feature branches get fast feedback: non-main pushes run only lint / secrets-scan / vuln-scan / SAST. PRs add unit + integration tests, image builds, and image scans. Main pushes add the Harbor push + cosign + per-package git tags. (Infra-only — no gameplay changes from the CI side.)',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
version: '0.1.5',
|
||||
date: '2026-05-17',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@teh-riehl/server",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
|
||||
Reference in New Issue
Block a user