package common import "fmt" // The action processor applies each crossed level's properties, capacity and // shop weights together with this plan. It must not credit an EXP purchase // separately from its gold/HP debit or persist only part of the level effects. type CurrencyWarsExperiencePlan struct { Level uint32 Exp uint32 Crossed []CurrencyWarsPlayerLevel } // PlanBaseExperience uses the captured thresholds before run modifiers. // levelLimit is the resolved run limit, not a fallback to a normal-mode cap. // Residual EXP at that limit is retained in the returned plan; callers must // apply a separately established cap/reset policy before encoding the Client // counter. This function does not guess that policy or spend any currency. func (c *CurrencyWarsCatalog) PlanBaseExperience(level, exp, gain, levelLimit uint32) (CurrencyWarsExperiencePlan, error) { var empty CurrencyWarsExperiencePlan if c == nil || level == 0 || levelLimit < level { return empty, fmt.Errorf("CW experience level/limit is invalid") } if _, err := c.PlayerLevel(levelLimit); err != nil { return empty, fmt.Errorf("CW experience limit: %w", err) } current, err := c.PlayerLevel(level) if err != nil { return empty, err } if level < levelLimit && (current.LevelUpExp == 0 || exp >= current.LevelUpExp) { return empty, fmt.Errorf("CW initial experience counter is not normalized") } plan := CurrencyWarsExperiencePlan{Level: level} remaining := uint64(exp) + uint64(gain) for plan.Level < levelLimit { if current.LevelUpExp == 0 { return empty, fmt.Errorf("CW level %d has no positive EXP threshold", plan.Level) } if remaining < uint64(current.LevelUpExp) { break } remaining -= uint64(current.LevelUpExp) plan.Level++ current, err = c.PlayerLevel(plan.Level) if err != nil { return empty, err } plan.Crossed = append(plan.Crossed, current) } if remaining > uint64(^uint32(0)) { return empty, fmt.Errorf("CW experience counter overflows protocol uint32") } plan.Exp = uint32(remaining) return plan, nil }