package storage import ( "math/rand/v2" "slices" "github.com/denisovdennis/autohero/internal/model" ) // FilterCapOfferableQuests drops quest templates whose id is in taken, then shuffles the rest // deterministically from seed and returns at most limit entries. If limit <= 0, returns all offerable quests (still filtered). func FilterCapOfferableQuests(all []model.Quest, taken map[int64]struct{}, limit int, seed int64) []model.Quest { var offer []model.Quest for _, q := range all { if _, skip := taken[q.ID]; skip { continue } offer = append(offer, q) } if len(offer) == 0 { return offer } if limit <= 0 || len(offer) <= limit { return offer } shuffled := slices.Clone(offer) rng := rand.New(rand.NewPCG(uint64(seed), uint64(seed>>32)^0x9e3779b97f4a7c15)) for i := len(shuffled) - 1; i > 0; i-- { j := rng.IntN(i + 1) shuffled[i], shuffled[j] = shuffled[j], shuffled[i] } return shuffled[:limit] } // questOfferDrySpellSalt mixes into the RNG seed so dry-spell draws are independent of quest shuffle draws. const questOfferDrySpellSalt int64 = 0x8BADF00D // QuestOfferDrySpellThisPeriod reports whether this hero/NPC/time bucket is a "dry spell" (no offers shown). // Deterministic: same inputs always yield the same result. dryChance in [0,1]; 0 disables, 1 always dry. func QuestOfferDrySpellThisPeriod(npcID, heroID, timeBucket int64, dryChance float64) bool { if dryChance <= 0 { return false } if dryChance >= 1 { return true } seed := npcID ^ heroID ^ timeBucket ^ questOfferDrySpellSalt rng := rand.New(rand.NewPCG(uint64(seed), uint64(seed>>32)^0x9e3779b97f4a7c15)) return rng.Float64() < dryChance }