Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# MoeNotes deploy image: Go semi-SSR gateway + Next.js 16 standalone server + Haneoka WASM deck engine
# (pinned to ournotes-team-builder 4ed6e0061f51 with patches/haneoka-rewards.patch; also backward-compatible
# (pinned to ournotes-team-builder 7c167c95e858 with patches/haneoka-rewards.patch; also backward-compatible
# with CI release artifact mode when MOENOTES_BUILD_MODE is unset).
# Set MOENOTES_BUILD_MODE=ssr in production to serve live semi-SSR HTML + persistent disk cache + Markdown routing.

Expand Down
626 changes: 439 additions & 187 deletions patches/haneoka-rewards.patch

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions src/components/deck/DeckResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default function DeckResult(props: DeckResultProps) {
</> : optimality?.proven && rateProven ? <div className="dr-proven"><Icon name="check" className="h-[18px] w-[18px] shrink-0" />
<ProofLink locale={locale} anchor="proven" label={tr("proven")} />{elapsed !== null && <small>{tr("elapsed", { n: (elapsed / 1000).toFixed(1) })}</small>}</div>
: <div className="dr-unproven"><Icon name="info" className="h-[18px] w-[18px] shrink-0" />
<ProofLink locale={locale} anchor="unproven" label={tr(job.status === "done" && job.stopped ? "stoppedUnproven" : result?.exitReason === "timeLimit" ? "timedOutUnproven" : "unproven")} />
<ProofLink locale={locale} anchor="unproven" label={tr(job.status === "done" && job.stopped ? "stoppedUnproven" : result?.exitReason === "timeLimit" ? "timedOutUnproven" : result?.exitReason === "sampled" ? "sampledUnproven" : "unproven")} />
{elapsed !== null && <small>{tr("elapsed", { n: (elapsed / 1000).toFixed(1) })}</small>}</div>}
{!rating && optimality && !optimality.proven && optimality.upperBound !== null && optimality.lowerBound !== null && <p className="dr-bounds">
{tr("bounds", { best: formatValue(optimality.lowerBound / unit, metric, locale), limit: formatValue(optimality.upperBound / unit, metric, locale) })}
Expand Down Expand Up @@ -356,6 +356,7 @@ function SkillOrders(props: DeckResultProps & { team: DeckTeam; aggregation: Dec
const tr = (key: string, values?: Record<string, string | number>) => t(locale, `deckWorkspace.solver.${key}`, values);
const hasTimingExtrema = team.timing !== undefined && (
team.timing.best > team.timing.score
|| team.timing.guideAvailable === false
|| (team.timing.optimal !== undefined && team.timing.worst !== undefined && team.timing.optimal > team.timing.worst)
);
const activeTiming = props.final && hasTimingExtrema ? team.timing! : null;
Expand Down Expand Up @@ -391,8 +392,9 @@ function OrderTimingPanel({ locale, timing, catalog }: { locale: AppLocale; timi
const tr = (key: string, values?: Record<string, string | number>) => t(locale, `deckWorkspace.solver.${key}`, values);
const windows = timing.windows.filter(win => win.steps.length > 0);
const hasFullExtrema = timing.worst !== undefined && timing.optimal !== undefined;
const planDelta = hasFullExtrema ? timing.best - timing.worst! : timing.best - timing.score;
const canGuide = timing.best > timing.score && windows.length > 0;
const planDelta = timing.best - timing.score;
const guideBlocked = timing.guideAvailable === false;
const canGuide = !guideBlocked && timing.best > timing.score && windows.length > 0;
const memberName = (id: number) => catalog.members.find(card => card.id === id)?.title ?? String(id);
return <div className="dr-timing">
{hasFullExtrema ? (
Expand All @@ -410,7 +412,7 @@ function OrderTimingPanel({ locale, timing, catalog }: { locale: AppLocale; timi
<dd>{formatValue(timing.optimal!, "score", locale)}</dd>
</div>
<div>
<dt>{tr("timingFramePlanScore")}</dt>
<dt>{tr("timingFramePlanScore", { ms: timing.advanceMs })}</dt>
<dd>
<span>{formatValue(timing.best, "score", locale)}</span>
{planDelta !== 0 && (
Expand All @@ -424,14 +426,19 @@ function OrderTimingPanel({ locale, timing, catalog }: { locale: AppLocale; timi
<div className="dr-timing-ceiling">
<span>{tr("timingBest")}</span>
<strong>{formatValue(timing.best, "score", locale)}</strong>
<em className="dr-timing-delta">+{formatValue(planDelta, "score", locale)}</em>
{planDelta !== 0 && (
<em className="dr-timing-delta">{planDelta > 0 ? "+" : ""}{formatValue(planDelta, "score", locale)}</em>
)}
</div>
<span className="dr-timing-advance">
<Icon name="fastRewind" className="h-4 w-4" />
{tr("timingAdvance", { ms: timing.advanceMs })}
</span>
</div>
)}
{guideBlocked && (
<p className="dr-muted mt-2 mb-0 text-xs">{tr("timingGuideUnavailable")}</p>
)}
{canGuide && (
<details className="dr-timing-guide">
<summary className="dr-timing-guide-summary md3-state">
Expand Down
61 changes: 58 additions & 3 deletions src/components/deck/DeckTeamGrowthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ function formatDelta(locale: AppLocale, value: number, fractionDigits = 1): stri
}).format(value);
}

type GrowthCategoryFilter = "all" | "level" | "awake" | "rank" | "skill";

function scenarioCategory(action: DeckGrowthScenario["action"]): Exclude<GrowthCategoryFilter, "all"> | "full" {
switch (action) {
case "level":
case "levelCap":
return "level";
case "awake":
case "awakeCap":
return "awake";
case "rank":
case "rankCap":
return "rank";
case "liveSkillLevel":
case "liveSkillCap":
case "gekisouSkillLevel":
case "gekisouSkillCap":
return "skill";
case "full":
return "full";
}
}

function actionLabel(
tr: (key: string, values?: Record<string, string | number>) => string,
scenario: DeckGrowthScenario,
Expand All @@ -40,12 +63,20 @@ function actionLabel(
return tr("actionLevelCap", values);
case "awake":
return tr("actionAwake", values);
case "awakeCap":
return tr("actionAwakeCap", values);
case "rank":
return tr("actionRank", values);
case "rankCap":
return tr("actionRankCap", values);
case "liveSkillLevel":
return tr("actionLiveSkill", values);
case "liveSkillCap":
return tr("actionLiveSkillCap", values);
case "gekisouSkillLevel":
return tr("actionGekisouSkill", values);
case "gekisouSkillCap":
return tr("actionGekisouSkillCap", values);
case "full":
return tr("actionFull");
}
Expand All @@ -63,7 +94,8 @@ export default function DeckTeamGrowthPanel({
}: DeckTeamGrowthPanelProps) {
const tr = (key: string, values?: Record<string, string | number>) =>
t(locale, `deckWorkspace.growth.${key}`, values);
const [scope, setScope] = useState<"step" | "all">("step");
const [scope, setScope] = useState<"step" | "cap" | "all">("step");
const [category, setCategory] = useState<GrowthCategoryFilter>("all");

const counts = useMemo(() => {
if (!report) return null;
Expand All @@ -81,10 +113,16 @@ export default function DeckTeamGrowthPanel({
const visibleScenarios = useMemo(() => {
if (!report) return [];
return report.scenarios.filter((scenario) => {
if (scope === "step" && scenario.action === "full") return false;
const resolvedTarget = scenario.target ?? (scenario.action === "full" ? "full" : scenario.action.endsWith("Cap") ? "cap" : "step");
if (scope === "step" && resolvedTarget !== "step" && scenario.action !== "levelCap") return false;
if (scope === "cap" && resolvedTarget !== "cap") return false;
if (category !== "all") {
const cat = scenarioCategory(scenario.action);
if (cat !== category) return false;
}
return scenario.primaryGain > 1e-6 || scenario.scoreGain > 1e-6 || scenario.powerGain > 0;
});
}, [report, scope]);
}, [report, scope, category]);

return (
<section className="mt-4 rounded-2xl border border-outline-variant/60 bg-surface-container-low p-3.5">
Expand All @@ -108,6 +146,7 @@ export default function DeckTeamGrowthPanel({
onChange={setScope}
options={[
{ value: "step", label: tr("scopeStep") },
{ value: "cap", label: tr("scopeCap") },
{ value: "all", label: tr("scopeAll") },
]}
/>
Expand All @@ -124,6 +163,22 @@ export default function DeckTeamGrowthPanel({
</div>
</div>

{report?.status === "ok" && report.scenarios.length > 0 && (
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
{(["all", "level", "awake", "rank", "skill"] as const).map((cat) => (
<Chip
key={cat}
size="small"
clickable
color={category === cat ? "primary" : "default"}
variant={category === cat ? "filled" : "outlined"}
label={tr(`category${cat[0]!.toUpperCase()}${cat.slice(1)}`)}
onClick={() => setCategory(cat)}
/>
))}
</div>
)}

{report && report.status !== "ok" && (
<p className="mt-2.5 mb-0 text-xs text-error">
{report.message === "growth-unsupported-runtime" ? tr("unavailable") : (report.message ?? tr("unavailable"))}
Expand Down
Loading
Loading