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:
+7
-1
@@ -1,7 +1,13 @@
|
||||
---
|
||||
updatedAt: 2026-09-28T00:00:00Z
|
||||
updatedAt: 2026-09-29T00:00:00Z
|
||||
---
|
||||
|
||||
## 2026년 9월 29일
|
||||
|
||||
### 밀어치기 자동 판정 제외
|
||||
|
||||
`FIX` 롱노트 중간·마지막 자동 판정을 밀어치기 대상에서 제외했습니다. 스페셜 시작과 정확히 겹친 자동 판정은 새 스페셜 효과를 받지 않으며, 최고점·최적 배치·달성 확률에 동일하게 반영합니다.
|
||||
|
||||
## 2026년 9월 28일
|
||||
|
||||
### 도메인 이전 안내 제거
|
||||
|
||||
@@ -125,6 +125,7 @@ function flattenEvents(chartData) {
|
||||
const block = Array.isArray(rawBlock) ? rawBlock : [rawBlock];
|
||||
return block.map((note, index) => ({
|
||||
...note,
|
||||
isHoldEnd: index > 0 && index === block.length - 1,
|
||||
// 롱노트 안쪽 판정은 점 모양이어도 0.1배다. 시작·끝의 일반 판정만 1배다.
|
||||
type: (index === 0 || index === block.length - 1) &&
|
||||
note.note_type !== 'mid' &&
|
||||
@@ -141,13 +142,15 @@ function flattenEvents(chartData) {
|
||||
...pink.map((note) => ({
|
||||
...note,
|
||||
type: note.note_type ?? 'regular',
|
||||
is_flick: !isHoldEndFlick(note)
|
||||
is_flick: !isHoldEndFlick(note),
|
||||
isHoldEnd: isHoldEndFlick(note)
|
||||
}))
|
||||
]
|
||||
.map((note) => ({
|
||||
timeSeconds: note.time_seconds,
|
||||
type: note.type === 'mid' ? 'mid' : 'regular',
|
||||
isFlick: note.is_flick === true
|
||||
isFlick: note.is_flick === true,
|
||||
...(note.isHoldEnd ? { isHoldEnd: true } : {})
|
||||
}))
|
||||
.filter((note) => Number.isFinite(note.timeSeconds))
|
||||
.sort((left, right) => left.timeSeconds - right.timeSeconds || (left.type === 'mid' ? 1 : -1));
|
||||
|
||||
@@ -156,7 +156,7 @@ export function parseSusTimeline(text) {
|
||||
for (let beat = (Math.floor(start.beat * 2 + 1e-9) + 1) / 2; beat < end.beat - 1e-9; beat += 0.5) {
|
||||
notes.push({ timeSeconds: secondsAt(beat), type: 'mid', isFlick: false });
|
||||
}
|
||||
notes.push({ timeSeconds: secondsAt(end.beat), type: 'regular', isFlick: false });
|
||||
notes.push({ timeSeconds: secondsAt(end.beat), type: 'regular', isFlick: false, isHoldEnd: true });
|
||||
}
|
||||
// Visible relay ticks are score judgements even if the editor stream contains an unmatched visual segment.
|
||||
for (const relay of visibleRelays) notes.push({ timeSeconds: secondsAt(relay.beat), type: 'mid', isFlick: false });
|
||||
|
||||
@@ -34,7 +34,7 @@ test('끝 플릭을 중복 집계하지 않고 중간 판정을 곡의 반박
|
||||
{ timeSeconds:0.5,type:'mid',isFlick:false },
|
||||
{ timeSeconds:0.75,type:'mid',isFlick:false },
|
||||
{ timeSeconds:1,type:'mid',isFlick:false },
|
||||
{ timeSeconds:1.25,type:'regular',isFlick:false },
|
||||
{ timeSeconds:1.25,type:'regular',isFlick:false,isHoldEnd:true },
|
||||
{ timeSeconds:2,type:'regular',isFlick:true },
|
||||
{ timeSeconds:3.75,type:'regular',isFlick:false }
|
||||
]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user