Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions src/pages/public/Chart/components/ChartCanvas/ChartCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function ChartCanvas() {
const totalMeasures = useGameStore((s) => s.timeline.totalMeasures);
const beatsPerMeasure = useGameStore((s) => s.timeline.beatsPerMeasure);
const hiSpeed = useGameStore((s) => s.hiSpeed);
const alwaysKeepHiSpeed = useGameStore((s) => s.alwaysKeepHiSpeed);
const slideRotation = useGameStore((s) => s.slideRotation);
const mirrorMode = useGameStore((s) => s.mirrorMode);
const judgmentLineDesign = useGameStore((s) => s.judgmentLineDesign);
Expand Down Expand Up @@ -171,6 +172,8 @@ export function ChartCanvas() {
renderer.setPinkSlideStart(state.pinkSlideStart);
renderer.setHighlightExNotes(state.highlightExNotes);
renderer.setNormalColorBreakSlide(state.normalColorBreakSlide);
renderer.setAlwaysKeepHiSpeed(state.alwaysKeepHiSpeed);
renderer.setPlaybackSpeed(state.playbackSpeed);

const handleResize = () => {
renderer.resize();
Expand Down Expand Up @@ -209,6 +212,20 @@ export function ChartCanvas() {
}
}, [hiSpeed, renderFrame]);

useEffect(() => {
if (rendererRef.current) {
rendererRef.current.setAlwaysKeepHiSpeed(alwaysKeepHiSpeed);
renderFrame(playbackTimeRef.current);
}
}, [alwaysKeepHiSpeed, renderFrame]);

useEffect(() => {
if (rendererRef.current) {
rendererRef.current.setPlaybackSpeed(playbackSpeed);
renderFrame(playbackTimeRef.current);
}
}, [playbackSpeed, renderFrame]);

useEffect(() => {
if (rendererRef.current) {
rendererRef.current.setSlideRotation(slideRotation);
Expand Down
12 changes: 10 additions & 2 deletions src/pages/public/Chart/components/Controls/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ export function PlaybackControls({ onToggleFullscreen, isFullscreen }: PlaybackC

export function Controls() {
const {
hiSpeed, slideRotation, mirrorMode, playbackSpeed, rawSimaiText,
hiSpeed, alwaysKeepHiSpeed, slideRotation, mirrorMode, playbackSpeed, rawSimaiText,
judgmentLineDesign, pinkSlideStart, highlightExNotes, normalColorBreakSlide,
musicOffset, musicVolume, soundOffset, selectedDifficulty, availableDifficulties, chartData,
setHiSpeed, setSlideRotation, setMirrorMode, setPlaybackSpeed,
setHiSpeed, setAlwaysKeepHiSpeed, setSlideRotation, setMirrorMode, setPlaybackSpeed,
setJudgmentLineDesign, setPinkSlideStart, setHighlightExNotes, setNormalColorBreakSlide,
setChartData, setMusicOffset, setMusicVolume, setSoundOffset, setSelectedDifficulty,
} = useGameStore(useShallow((state) => state));
Expand Down Expand Up @@ -363,6 +363,14 @@ export function Controls() {
onChange={(e) => setNormalColorBreakSlide(e.currentTarget.checked)}
/>
</Group>

<Group justify="space-between">
<Text size="sm">保持谱面流速</Text>
<Switch
checked={alwaysKeepHiSpeed}
onChange={(e) => setAlwaysKeepHiSpeed(e.currentTarget.checked)}
/>
</Group>
</Stack>
</Collapse>
</Card>
Expand Down
6 changes: 5 additions & 1 deletion src/pages/public/Chart/renderers/BaseRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ export abstract class BaseRenderer {
}

protected getApproachTimeMs(): number {
return this.context.baseApproachTimeMs / this.context.hiSpeed;
if(this.context.config.alwaysKeepHiSpeed) {
return this.context.baseApproachTimeMs / (this.context.hiSpeed / this.context.config.playbackSpeed);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getApproachTimeMs uses a nested division (baseApproachTimeMs / (hiSpeed / playbackSpeed)). This is mathematically equivalent to (baseApproachTimeMs * playbackSpeed) / hiSpeed and can be rewritten to reduce operations and make the intent (scale approach window by playback speed) clearer.

Suggested change
return this.context.baseApproachTimeMs / (this.context.hiSpeed / this.context.config.playbackSpeed);
return (this.context.baseApproachTimeMs * this.context.config.playbackSpeed) / this.context.hiSpeed;

Copilot uses AI. Check for mistakes.
} else {
return this.context.baseApproachTimeMs / this.context.hiSpeed;
}
}

protected distanceToCenter(x: number, y: number): number {
Expand Down
14 changes: 14 additions & 0 deletions src/pages/public/Chart/renderers/MainRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class MainRenderer {

private config: RendererConfig = {
hiSpeed: HI_SPEED_DEFAULT * HI_SPEED_CONVERSION_FACTOR,
alwaysKeepHiSpeed: false,
playbackSpeed: 1.0,
mirrorMode: 'none',
highlightExNotes: false,
normalColorBreakSlide: false,
Expand Down Expand Up @@ -164,6 +166,18 @@ export class MainRenderer {
}
}

setAlwaysKeepHiSpeed(alwaysKeepHighSpeed: boolean): void {
this.config.alwaysKeepHiSpeed = alwaysKeepHighSpeed;
this.updateRenderersContext();
}

setPlaybackSpeed(playbackSpeed: number): void {
if (playbackSpeed >= 0.1 && playbackSpeed <= 1.0) {
this.config.playbackSpeed = playbackSpeed;
this.updateRenderersContext();
}
Comment on lines +175 to +178
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPlaybackSpeed silently ignores values outside [0.1, 1.0]. For a setter API this can be surprising (caller thinks the value applied when it didn’t). Consider clamping to the valid range (like the store does) or throwing/logging to make invalid inputs explicit.

Suggested change
if (playbackSpeed >= 0.1 && playbackSpeed <= 1.0) {
this.config.playbackSpeed = playbackSpeed;
this.updateRenderersContext();
}
if (playbackSpeed < 0.1 || playbackSpeed > 1.0) {
console.warn(
`MainRenderer.setPlaybackSpeed: playbackSpeed ${playbackSpeed} is out of range [0.1, 1.0]; ignoring value.`,
);
return;
}
this.config.playbackSpeed = playbackSpeed;
this.updateRenderersContext();

Copilot uses AI. Check for mistakes.
}

setBpm(bpm: number): void {
this.bpm = bpm;
}
Expand Down
14 changes: 10 additions & 4 deletions src/pages/public/Chart/stores/useGameStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface GameState {
availableDifficulties: AvailableDifficulties;
/** 流速 */
hiSpeed: number;
/** 保持固定流速 */
alwaysKeepHiSpeed: boolean;
/** 滑条起始 Note 是否旋转 */
slideRotation: boolean;
/** 镜像设置 */
Expand Down Expand Up @@ -102,6 +104,8 @@ interface GameActions {
setAvailableDifficulties: (difficulties: AvailableDifficulties) => void;
/** 设置流速 */
setHiSpeed: (speed: number) => void;
/** 设置保持固定流速 */
setAlwaysKeepHiSpeed: (enabled: boolean) => void;
/** 设置滑条起始 Note 是否旋转 */
setSlideRotation: (enabled: boolean) => void;
/** 设置镜像模式 */
Expand Down Expand Up @@ -165,6 +169,7 @@ const initialState: GameState = {
selectedDifficulty: null,
availableDifficulties: {},
hiSpeed: 6,
alwaysKeepHiSpeed: false,
slideRotation: true,
mirrorMode: 'none',
judgmentLineDesign: 'simple',
Expand Down Expand Up @@ -217,7 +222,7 @@ function getDivisorAt(chartData: Chart | null, time: number, forStepping: boolea
}

const events = chartData.divisorEvents;

// 二分查找最后一个 timing <= time 的事件
let left = 0;
let right = events.length - 1;
Expand All @@ -234,7 +239,7 @@ function getDivisorAt(chartData: Chart | null, time: number, forStepping: boolea
}

const divisor = result >= 0 ? events[result].divisor : 4;

// 步进时限制最大分拍
return forStepping ? Math.min(divisor, MAX_STEP_DIVISOR) : divisor;
}
Expand Down Expand Up @@ -301,7 +306,7 @@ export const useGameStore = create<GameStore>()(
const state = get();
const { beatsPerMeasure, totalMeasures } = state.timeline;
const time = playbackTimeRef.current;

// 从 playbackTimeRef 同步当前位置
const measure = Math.floor(time / beatsPerMeasure);
const clampedMeasure = Math.max(0, Math.min(measure, totalMeasures - 1));
Expand Down Expand Up @@ -452,7 +457,7 @@ export const useGameStore = create<GameStore>()(

setChartData: (chart: Chart | null) => {
playbackTimeRef.current = 0;

set((state) => ({
chartData: chart,
isPlaying: false,
Expand All @@ -477,6 +482,7 @@ export const useGameStore = create<GameStore>()(
set({ availableDifficulties: difficulties }),

setHiSpeed: (speed: number) => set({ hiSpeed: Math.max(3, Math.min(9, speed)) }),
setAlwaysKeepHiSpeed: (enabled: boolean) => set({ alwaysKeepHiSpeed: enabled }),
setSlideRotation: (enabled: boolean) => set({ slideRotation: enabled }),
setMirrorMode: (mode: MirrorMode) => set({ mirrorMode: mode }),
setJudgmentLineDesign: (design: JudgmentLineDesign) => set({ judgmentLineDesign: design }),
Expand Down
4 changes: 4 additions & 0 deletions src/pages/public/Chart/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ export interface Point2D {
export interface RendererConfig {
/** 谱面流速:3-9 */
hiSpeed: number;
/** 保持固定流速 */
alwaysKeepHiSpeed: boolean;
/** 播放速度:0.1-1.0 */
playbackSpeed: number;
/** 镜像模式:上下反、左右反、全反 */
mirrorMode: MirrorMode;
/** 是否高亮保护套 Note */
Expand Down