using Unity.Collections; using Unity.Entities; namespace ProjectM.Simulation { /// /// One authored boon in the catalog blob: a thin wrapper over the existing stat pipeline — /// // map 1:1 onto a row /// (bytes, never enums, on the baked path). is the stable APPEND-ONLY key the replicated /// BoonOffer options and pick RPC carry. is the rarity draw weight /// (common 100 / rare 30 / epic 10). gates by class: bit0 = Warrior (classId 0), /// bit1 = Ranger (classId 1), 3 = both. /// public struct BoonDefBlob { public byte Id; public byte Target; // StatTarget as byte public byte Op; // ModOp as byte public float Value; public byte Weight; public byte ClassMask; public FixedString64Bytes Name; public FixedString128Bytes Desc; } /// The baked boon pool (config blob, both worlds, NOT replicated — the AbilityDatabase pattern). public struct BoonCatalogBlob { public BlobArray Defs; } /// Singleton component carrying the baked catalog (place ONE BoonCatalogAuthoring in the subscene). public struct BoonCatalog : IComponentData { public BlobAssetReference Value; } /// /// Server-only bookkeeping for BoonOfferSystem, attached at runtime beside the catalog singleton (the /// RoomFieldState idiom): the RoomEpoch offers were last drawn for — int equality, one offer set per room. /// public struct BoonOfferState : IComponentData { public int OfferedRoomEpoch; } /// /// Pure, deterministic boon selection math — integer-hash only ( chain, /// no RNG state), so an offer is a reproducible function of (runSeed, room, player). EditMode-tested. /// public static class BoonMath { /// Class-mask bit for a wire class id (0 = Warrior, 1 = Ranger). public static byte MaskFor(byte classId) => (byte)(1 << (classId & 1)); /// /// Draw 3 DISTINCT, rarity-weighted, class-filtered boon ids from the pool. Deterministic per /// . If the class-legal pool has fewer than 3 entries the tail repeats the /// last-drawn candidates (a catalog authoring smell, not a crash). Returns the number of distinct ids. /// public static int PickBoons(uint offerSeed, byte classId, ref BoonCatalogBlob pool, out byte o0, out byte o1, out byte o2) { byte classBit = MaskFor(classId); // Class-legal candidate indices + the total weight. var candidates = new FixedList128Bytes(); int totalWeight = 0; for (int i = 0; i < pool.Defs.Length && candidates.Length < candidates.Capacity; i++) { if ((pool.Defs[i].ClassMask & classBit) == 0) continue; if (pool.Defs[i].Weight == 0) continue; candidates.Add((byte)i); totalWeight += pool.Defs[i].Weight; } o0 = o1 = o2 = 0; if (candidates.Length == 0) return 0; var picked = new FixedList32Bytes(); // picked catalog indices uint salt = 0; while (picked.Length < 3 && picked.Length < candidates.Length) { // Weighted draw with rejection on duplicates (bounded; falls through to a linear fill). uint roll = RunMapMath.Hash(offerSeed, (uint)picked.Length, salt) % (uint)totalWeight; byte drawn = candidates[candidates.Length - 1]; int acc = 0; for (int c = 0; c < candidates.Length; c++) { acc += pool.Defs[candidates[c]].Weight; if (roll < (uint)acc) { drawn = candidates[c]; break; } } bool dup = false; for (int p = 0; p < picked.Length; p++) if (picked[p] == drawn) { dup = true; break; } if (!dup) { picked.Add(drawn); salt = 0; } else if (++salt > 16) { // Rejection budget spent — take the first unpicked candidate (still deterministic). for (int c = 0; c < candidates.Length; c++) { bool used = false; for (int p = 0; p < picked.Length; p++) if (picked[p] == candidates[c]) { used = true; break; } if (!used) { picked.Add(candidates[c]); break; } } salt = 0; } } o0 = picked.Length > 0 ? pool.Defs[picked[0]].Id : (byte)0; o1 = picked.Length > 1 ? pool.Defs[picked[1]].Id : o0; o2 = picked.Length > 2 ? pool.Defs[picked[2]].Id : o1; return picked.Length; } /// Find a def index by its stable id (-1 when absent — callers preserve-and-skip unknown ids). public static int FindDef(ref BoonCatalogBlob pool, byte id) { for (int i = 0; i < pool.Defs.Length; i++) if (pool.Defs[i].Id == id) return i; return -1; } } /// /// The DEFAULT v1 boon table + the blob builder the baker AND EditMode tests share (single source — the /// authoring bakes this table verbatim when its designer-row list is empty). Append-only ids. /// public static class BoonCatalogData { /// Build the default catalog blob (caller owns/disposes the reference). public static BlobAssetReference BuildDefault(Allocator allocator = Allocator.Persistent) { var builder = new BlobBuilder(Allocator.Temp); ref var root = ref builder.ConstructRoot(); var defs = builder.Allocate(ref root.Defs, 12); int i = 0; // id, target, op, value, weight, mask(1=Warrior,2=Ranger,3=both), name, desc defs[i++] = Make(1, StatTarget.Damage, ModOp.PercentAdd, 0.20f, 100, 3, "Honed Edge", "+20% ability damage"); defs[i++] = Make(2, StatTarget.CooldownTicks, ModOp.PercentMult, -0.15f, 100, 3, "Swift Hands", "-15% ability cooldown"); defs[i++] = Make(3, StatTarget.Range, ModOp.PercentAdd, 0.25f, 100, 2, "Long Reach", "+25% projectile range"); defs[i++] = Make(4, StatTarget.MoveSpeed, ModOp.PercentAdd, 0.12f, 100, 3, "Fleet Foot", "+12% move speed"); defs[i++] = Make(5, StatTarget.MaxHealth, ModOp.Flat, 25f, 100, 3, "Iron Constitution", "+25 max health"); defs[i++] = Make(6, StatTarget.MeleeDamage, ModOp.PercentAdd, 0.25f, 100, 1, "Heavy Blows", "+25% melee damage"); defs[i++] = Make(7, StatTarget.MeleeRange, ModOp.PercentAdd, 0.20f, 60, 1, "Extended Haft", "+20% melee reach"); defs[i++] = Make(8, StatTarget.ProjectileSpeed, ModOp.PercentAdd, 0.25f, 60, 2, "Swift Bolts", "+25% projectile speed"); defs[i++] = Make(9, StatTarget.AutoTargetRange, ModOp.PercentAdd, 0.20f, 60, 3, "Keen Instinct", "+20% auto-target range"); defs[i++] = Make(10, StatTarget.CooldownTicks, ModOp.PercentMult, -0.25f, 30, 3, "Berserker's Pace", "-25% ability cooldown"); defs[i++] = Make(11, StatTarget.MaxHealth, ModOp.Flat, 60f, 30, 3, "Titan's Vigor", "+60 max health"); defs[i++] = Make(12, StatTarget.Damage, ModOp.PercentAdd, 0.50f, 10, 3, "Executioner", "+50% ability damage"); var blob = builder.CreateBlobAssetReference(allocator); builder.Dispose(); return blob; } static BoonDefBlob Make(byte id, StatTarget target, ModOp op, float value, byte weight, byte mask, string name, string desc) { return new BoonDefBlob { Id = id, Target = (byte)target, Op = (byte)op, Value = value, Weight = weight, ClassMask = mask, Name = new FixedString64Bytes(name), Desc = new FixedString128Bytes(desc), }; } } }