Run Re-Do
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// One authored boon in the catalog blob: a thin wrapper over the existing stat pipeline —
|
||||
/// <see cref="Target"/>/<see cref="Op"/>/<see cref="Value"/> map 1:1 onto a <see cref="StatModifier"/> row
|
||||
/// (bytes, never enums, on the baked path). <see cref="Id"/> is the stable APPEND-ONLY key the replicated
|
||||
/// <c>BoonOffer</c> options and pick RPC carry. <see cref="Weight"/> is the rarity draw weight
|
||||
/// (common 100 / rare 30 / epic 10). <see cref="ClassMask"/> gates by class: bit0 = Warrior (classId 0),
|
||||
/// bit1 = Ranger (classId 1), 3 = both.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>The baked boon pool (config blob, both worlds, NOT replicated — the AbilityDatabase pattern).</summary>
|
||||
public struct BoonCatalogBlob
|
||||
{
|
||||
public BlobArray<BoonDefBlob> Defs;
|
||||
}
|
||||
|
||||
/// <summary>Singleton component carrying the baked catalog (place ONE BoonCatalogAuthoring in the subscene).</summary>
|
||||
public struct BoonCatalog : IComponentData
|
||||
{
|
||||
public BlobAssetReference<BoonCatalogBlob> Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only bookkeeping for <c>BoonOfferSystem</c>, attached at runtime beside the catalog singleton (the
|
||||
/// RoomFieldState idiom): the RoomEpoch offers were last drawn for — int equality, one offer set per room.
|
||||
/// </summary>
|
||||
public struct BoonOfferState : IComponentData
|
||||
{
|
||||
public int OfferedRoomEpoch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure, deterministic boon selection math — integer-hash only (<see cref="RunMapMath.Hash(uint,uint)"/> chain,
|
||||
/// no RNG state), so an offer is a reproducible function of (runSeed, room, player). EditMode-tested.
|
||||
/// </summary>
|
||||
public static class BoonMath
|
||||
{
|
||||
/// <summary>Class-mask bit for a wire class id (0 = Warrior, 1 = Ranger).</summary>
|
||||
public static byte MaskFor(byte classId) => (byte)(1 << (classId & 1));
|
||||
|
||||
/// <summary>
|
||||
/// Draw 3 DISTINCT, rarity-weighted, class-filtered boon ids from the pool. Deterministic per
|
||||
/// <paramref name="offerSeed"/>. 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.
|
||||
/// </summary>
|
||||
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<byte>();
|
||||
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<byte>(); // 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;
|
||||
}
|
||||
|
||||
/// <summary>Find a def index by its stable id (-1 when absent — callers preserve-and-skip unknown ids).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class BoonCatalogData
|
||||
{
|
||||
/// <summary>Build the default catalog blob (caller owns/disposes the reference).</summary>
|
||||
public static BlobAssetReference<BoonCatalogBlob> BuildDefault(Allocator allocator = Allocator.Persistent)
|
||||
{
|
||||
var builder = new BlobBuilder(Allocator.Temp);
|
||||
ref var root = ref builder.ConstructRoot<BoonCatalogBlob>();
|
||||
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<BoonCatalogBlob>(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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00909311d983d7a43afc195595aff217
|
||||
@@ -0,0 +1,17 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client → server boon pick: <see cref="Index"/> (0/1/2) into the sender's OWN replicated <c>BoonOffer</c>
|
||||
/// options. Server-validated (<c>Pending==1</c>, index in range, <c>RunInfo.Lifecycle==RoomReward</c> — the
|
||||
/// D-F4 gate that stops a grace-timeout straggler pick landing after the Returning-edge strip). UNCONDITIONAL
|
||||
/// wire type, blittable scalar. Declared at Step 3 (wire front-load); consumed by <c>BoonApplySystem</c> from
|
||||
/// Step 10.
|
||||
/// </summary>
|
||||
public struct BoonPickRequest : IRpcCommand
|
||||
{
|
||||
/// <summary>The chosen option slot: 0, 1, or 2.</summary>
|
||||
public byte Index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc9680d4a4c9a334396b60bb97d75b3b
|
||||
@@ -39,6 +39,12 @@ namespace ProjectM.Simulation
|
||||
public static byte AbilityFor(byte classId)
|
||||
=> classId == RangerClass ? (byte)AbilityId.Primary : (byte)AbilityId.WarriorCone;
|
||||
|
||||
/// <summary>The class a Fire-slot ability id implies — the exact inverse of <see cref="AbilityFor"/> (Ranger
|
||||
/// iff Primary). Lets the CLIENT derive the local class from the replicated <see cref="AbilityRef"/> (tracks
|
||||
/// the dev class-switch, unlike the menu's ClassSelection static; PlayerClass itself is server-only).</summary>
|
||||
public static byte ClassForAbility(byte abilityId)
|
||||
=> abilityId == (byte)AbilityId.Primary ? RangerClass : WarriorClass;
|
||||
|
||||
/// <summary>True when a modifier's SourceId is in the reserved class-seed range [ClassSourceId, +ClassSeedCount).</summary>
|
||||
public static bool IsClassSeed(uint sourceId)
|
||||
=> sourceId >= Tuning.ClassSourceId && sourceId < Tuning.ClassSourceId + (uint)ClassSeedCount;
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace ProjectM.Simulation
|
||||
/// enum-codegen hazard that already de-Bursted ProjectileClassificationSystem.
|
||||
/// </summary>
|
||||
[GhostComponent(OwnerSendType = SendToOwnerType.All)]
|
||||
[InternalBufferCapacity(8)]
|
||||
// Capacity 32 (was 8): class seeds + permanent meta rows + equip + up to ~10 run boons must stay chunk-internal
|
||||
// (a chunk-layout hint only — NOT ghost serializer metadata, so this is NOT a re-bake; overflow spills to heap).
|
||||
[InternalBufferCapacity(32)]
|
||||
public struct StatModifier : IBufferElementData
|
||||
{
|
||||
/// <summary>The <see cref="StatTarget"/> this modifier applies to (stored as a byte).</summary>
|
||||
|
||||
@@ -31,5 +31,16 @@ namespace ProjectM.Simulation
|
||||
if (mods[j].SourceId == sourceId) { mods.RemoveAtSwapBack(j); removed++; }
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>Remove every <see cref="StatModifier"/> row whose SourceId lies in [lo, hiExclusive) — the
|
||||
/// run-scoped BOON strip (per-pick distinct ids share the band; one call clears the whole run's boons while
|
||||
/// class/meta/equip bands survive untouched). Idempotent; RemoveAtSwapBack. Returns the count removed.</summary>
|
||||
public static int RemoveBySourceIdRange(DynamicBuffer<StatModifier> mods, uint lo, uint hiExclusive)
|
||||
{
|
||||
int removed = 0;
|
||||
for (int j = mods.Length - 1; j >= 0; j--)
|
||||
if (mods[j].SourceId >= lo && mods[j].SourceId < hiExclusive) { mods.RemoveAtSwapBack(j); removed++; }
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user