public JudgeResult?UserPressed(time_t position) { // This check just makes sure that we can process ticks. // If there are no state ticks, there should never be score ticks left. if (HasStateTicks) { Debug.Assert(HasScoreTicks); } else { SpawnKeyBeam?.Invoke(Label, JudgeKind.Passive, false); return(null); } switch (m_state) { case JudgeState.Idle: { var nextStateTick = NextStateTick; time_t difference = position - (nextStateTick.Position + JudgementOffset); time_t absDifference = Math.Abs((double)difference); if (nextStateTick.State == JudgeState.ChipAwaitPress && absDifference <= m_chipMissRadius) { var scoreTick = NextScoreTick; Debug.Assert(scoreTick.Kind == TickKind.Chip); Debug.Assert(scoreTick.Position == nextStateTick.Position); // `difference` applies to both ticks, don't recalculate JudgeResult result; if (absDifference <= m_chipPerfectRadius) { result = new JudgeResult(difference, JudgeKind.Perfect); } else if (absDifference <= m_chipCriticalRadius) { result = new JudgeResult(difference, JudgeKind.Critical); } else if (absDifference <= m_chipNearRadius) { result = new JudgeResult(difference, JudgeKind.Near); } // TODO(local): Is this how we want to handle misses? else { result = new JudgeResult(difference, JudgeKind.Bad); } OnTickProcessed?.Invoke(scoreTick.Entity, position, result, difference < 0); OnChipPressed?.Invoke(position, scoreTick.Entity); SpawnKeyBeam?.Invoke(scoreTick.Entity.Lane, result.Kind, difference < 0); AdvanceStateTick(); AdvanceScoreTick(); // state stays idle after a chip press, chips are instantaneous IsBeingPlayed = true; return(result); } else if (nextStateTick.State == JudgeState.HoldAwaitPress && absDifference <= m_holdActivateRadius) { OnHoldPressed?.Invoke(position, nextStateTick.Entity); AdvanceStateTick(); // No need to advance a score tick, we haven't judged anything // state is `hold on` because ofc we pressed the hold! m_state = JudgeState.HoldOn; m_currentStateTick = nextStateTick; IsBeingPlayed = true; } // do nothing when pressed otherwise else { SpawnKeyBeam?.Invoke(Label, JudgeKind.Passive, false); } } break; case JudgeState.HoldOff: { OnHoldPressed?.Invoke(position, m_currentStateTick.Entity); m_state = JudgeState.HoldOn; IsBeingPlayed = true; } break; case JudgeState.HoldOn: throw new InvalidOperationException(); } return(null); }