package game import ( "symph/parameter" "time" ) // Status is the persistent player effect state granted by items. It survives a // level change and is cleared on restart. Timed effects hold an absolute // deadline, charge effects a count. Adding an effect adds a field here plus a // row in resolvers — the pause/interlude shift and the restart clear are // already funneled through this type type Status struct { BoostUntil time.Time // zero or past = inactive Shield int // charges; each absorbs one negative contact. No expiry } // Shielded reports whether an absorb charge is held func (s Status) Shielded() bool { return s.Shield > 0 } // Boosted reports whether a Boost is active at instant t. Drives the tempo // (deltaAt, evaluated at beat boundaries) and hazard immunity (hitHazard, // evaluated at the contact instant) func (s Status) Boosted(t time.Time) bool { return !s.BoostUntil.IsZero() && t.Before(s.BoostUntil) } // BoostRemaining reports the Boost span left at t; 0 when inactive func (s Status) BoostRemaining(t time.Time) time.Duration { if !s.Boosted(t) { return 0 } return s.BoostUntil.Sub(t) } // shift moves every absolute deadline forward by d, holding the remaining span // across a pause or a level-clear interlude // `at` is the instant the dead span opened. A deadline already lapsed // at `at` is cleared, not shifted — dead time never revives an effect func (s *Status) shift(at time.Time, d time.Duration) { if s.BoostUntil.IsZero() { return } if !s.Boosted(at) { s.BoostUntil = time.Time{} return } s.BoostUntil = s.BoostUntil.Add(d) } // grantShield arms the absorb charge. Charges do not stack: a pickup while // shielded re-arms func (s *Status) grantShield() { s.Shield = parameter.ShieldCharges } // grantBoost re-arms the tempo deadline func (s *Status) grantBoost(now time.Time) { s.BoostUntil = now.Add(parameter.BoostDuration) } // absorb spends one charge; reports whether one was held func (s *Status) absorb() bool { if s.Shield <= 0 { return false } s.Shield-- return true }