fix: exclude automatic hold judgments from special-start timing bonus
Publish GitHub Pages output / publish (push) Successful in 40s

This commit is contained in:
2026-09-29 10:25:51 +09:00
parent eaf4aeadc5
commit 5783072b12
826 changed files with 859 additions and 828 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { effectiveActiveProbability } from './active-probability.ts';
import { calculateExactScore, finalNoteScore } from './score.ts';
import { calculateExactScore, finalNoteScore, specialScoreSupportForNote } from './score.ts';
import { passiveScoreSupportByCard } from './passive-support.ts';
import { effectiveCooldownSeconds } from './timeline.ts';
import { COOLDOWN_REDUCTIONS } from './types.ts';
@@ -68,7 +68,7 @@ function baseScores(chart: ExactSongChart, perfectNoteScore: number): Float64Arr
function makeProfiles(order: readonly OptimizerCard[], chart: ExactSongChart): Map<string, CandidateProfile> {
const profiles = new Map<string, CandidateProfile>();
const noteSupport = Float64Array.from(chart.notes, (note) => specialRate(order, chart, note.timeSeconds, 'scoreUpPercent'));
const noteSupport = Float64Array.from(chart.notes, (note) => specialScoreSupportForNote(order, chart, note));
const passiveSupport = passiveScoreSupportByCard(order);
order.forEach((card, cardIndex) => {
+2 -1
View File
@@ -1,4 +1,5 @@
import { effectiveActiveProbability } from './active-probability.ts';
import { specialScoreSupportForNote } from './score.ts';
import { passiveScoreSupportByCard } from './passive-support.ts';
import { effectiveCooldownSeconds } from './timeline.ts';
import type {
@@ -76,7 +77,7 @@ function prepareModel(
scoreNotes(chart).forEach((note, noteIndex) => {
const candidates: NoteCandidate[] = [];
const specialSupport = specialRate(cards, chart, note.timeSeconds, 'scoreUpPercent');
const specialSupport = specialScoreSupportForNote(cards, chart, note);
cards.forEach((card, cardIndex) => {
const reduction = reductions[card.id] ?? 0;
const interval = effectiveCooldownSeconds(card.active.cooldownSeconds, reduction);
+7 -2
View File
@@ -94,6 +94,10 @@ function activationRateBonusAt(
: 0), 0);
}
export function specialScoreSupportForNote(cards: readonly OptimizerCard[], chart: ExactSongChart, note: ExactSongChart['notes'][number]): number {
return specialSupportAt(cards, chart, note.timeSeconds, note.type === 'mid' || note.isHoldEnd === true);
}
function activeCandidates(
cardsInOrder: readonly OptimizerCard[],
reductions: Readonly<Record<string, CooldownReduction>>,
@@ -160,11 +164,12 @@ export function calculateExactScore(
? Math.ceil(options.perfectNoteScore * 0.1)
: options.perfectNoteScore * (note.isFlick ? 1.05 : 1);
const rawBaselineScore = baseNoteScore * (1 + comboBonus(combo));
const candidates = activeCandidates(cardsInOrder, reductions, chart, note.timeSeconds, options.excludedActiveWindows);
const automatic = note.type === 'mid' || note.isHoldEnd === true;
const candidates = activeCandidates(cardsInOrder, reductions, chart, note.timeSeconds, options.excludedActiveWindows, automatic);
const baselineScore = finalNoteScore(rawBaselineScore, directBonus);
const expectedScore = finalNoteScore(rawBaselineScore * (1 + expectedStrongestBoost(candidates)), directBonus);
const theoreticalMaxScore = finalNoteScore(rawBaselineScore * (1 + (candidates[0]?.boost ?? 0)), directBonus);
if (chart.specialStartSeconds.some((start) => start != null && Math.abs(start - note.timeSeconds) < 1e-9)) {
if (!automatic && chart.specialStartSeconds.some((start) => start != null && Math.abs(start - note.timeSeconds) < 1e-9)) {
const withoutStart = activeCandidates(cardsInOrder, reductions, chart, note.timeSeconds, options.excludedActiveWindows, true);
specialStartBonus += theoreticalMaxScore - finalNoteScore(rawBaselineScore * (1 + (withoutStart[0]?.boost ?? 0)), directBonus);
}
+15 -1
View File
@@ -24,6 +24,20 @@ test('PSYCHO Hard 첨부 편성의 밀어치기 적용 및 미적용 점수를
const rows = [[30,11,110,14,115], [27,9,125,10,160], [15,6,110,12,135], [19,7,110,10,160], [23,8,120,11,145]];
const cards: OptimizerCard[] = rows.map(([cooldownSeconds,durationSeconds,scoreUpPercent,sd,sb],i) => ({id: String(i),name: String(i),active:{cooldownSeconds,durationSeconds,scoreUpPercent},special:{durationSeconds:sd,scoreUpPercent:sb},scoreSupportPercent:57}));
const result = calculateExactScore(cards, {'0':4,'1':12,'2':12,'3':12,'4':12}, chart, {perfectNoteScore:396815/chart.scoreRatio,directSongScoreBonusPercent:10});
assert.equal(result.theoreticalMaxScore, 4067011);
assert.equal(result.theoreticalMaxScore, 4060353);
assert.equal(result.withoutSpecialStartBonusScore, 4043015);
});
test('자동 중간·종료 판정은 경계 스페셜을 받지 않고 수동 노트만 밀어치기로 증가한다', () => {
const card: OptimizerCard = {id:'a',name:'a',active:{cooldownSeconds:1,durationSeconds:10,scoreUpPercent:100,probabilityPercent:100},special:{durationSeconds:2,scoreUpPercent:100}};
const chart: ExactSongChart = {songId:'automatic',difficulty:'hard',durationSeconds:5,scoreRatio:1,specialStartSeconds:[2],notes:[
{timeSeconds:2,type:'mid',isFlick:false},
{timeSeconds:2,type:'regular',isFlick:false,isHoldEnd:true},
{timeSeconds:2,type:'regular',isFlick:false},
{timeSeconds:2.5,type:'regular',isFlick:false,isHoldEnd:true}
]};
const result=calculateExactScore([card],{},chart,{perfectNoteScore:100});
assert.deepEqual(result.notes.map(n=>n.theoreticalMaxScore),[20,200,300,300]);
assert.equal(result.theoreticalMaxScore,820);
assert.equal(result.withoutSpecialStartBonusScore,720);
});
+2
View File
@@ -85,6 +85,8 @@ export type ExactChartNote = {
timeSeconds: number;
type: 'regular' | 'mid';
isFlick: boolean;
/** 롱노트 종료 자동 판정. 점수 가중치는 regular와 같지만 밀어치기는 불가능하다. */
isHoldEnd?: boolean;
};
export type ExactSongChart = {