fix: exclude automatic hold judgments from special-start timing bonus
Publish GitHub Pages output / publish (push) Successful in 40s
Publish GitHub Pages output / publish (push) Successful in 40s
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -85,6 +85,8 @@ export type ExactChartNote = {
|
||||
timeSeconds: number;
|
||||
type: 'regular' | 'mid';
|
||||
isFlick: boolean;
|
||||
/** 롱노트 종료 자동 판정. 점수 가중치는 regular와 같지만 밀어치기는 불가능하다. */
|
||||
isHoldEnd?: boolean;
|
||||
};
|
||||
|
||||
export type ExactSongChart = {
|
||||
|
||||
@@ -1204,7 +1204,7 @@
|
||||
{/if}
|
||||
<div class="score-summary"><span>스킬 미발동 기준</span><strong>{result.baselineScore.toLocaleString('ko-KR')}</strong></div>
|
||||
</div>
|
||||
<p class="probability-guide">두 최고점은 모두 PERFECT 기준입니다. 밀어치기 적용은 스페셜 시작과 같은 시각의 노트를 발동 이후에 처리하여 새 스페셜 효과를 받는 경우, 미적용은 해당 노트가 새 효과를 받지 않는 경우입니다. 일부만 적용되면 두 점수 사이가 됩니다. 예상 점수·그래프·달성 확률은 밀어치기 적용 기준입니다.</p>
|
||||
<p class="probability-guide">두 최고점은 모두 PERFECT 기준입니다. 밀어치기 적용은 스페셜 시작과 같은 시각의 수동 판정 노트를 발동 이후에 처리하여 새 스페셜 효과를 받는 경우, 미적용은 해당 노트가 새 효과를 받지 않는 경우입니다. 롱노트 중간·마지막 자동 판정은 밀어치기가 불가능하므로 두 경우 모두 시작 경계의 새 스페셜 효과를 받지 않습니다. 일부 수동 판정만 밀어치기하면 두 점수 사이가 됩니다. 예상 점수·그래프·달성 확률은 밀어치기 적용 기준입니다.</p>
|
||||
{#if !simulatedResult && (mode === 'analysis' || achievementProbabilities)}
|
||||
<div class="probability-grid">
|
||||
<div><span>최고점 100%</span><strong>{probabilityLabel(achievementProbabilities?.maximum)}</strong></div>
|
||||
|
||||
Reference in New Issue
Block a user