Skip to content

Commit f0f6c44

Browse files
committed
test: cover formula profile combined reward
1 parent 8b51727 commit f0f6c44

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

internal/minimal/shared_world_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16089,6 +16089,132 @@ func TestNewGameSessionFactoryAppliesRegisteredProfileDefaultPracticeMobDropRewa
1608916089
}
1609016090
}
1609116091

16092+
func TestNewGameSessionFactoryAppliesFormulaOnlyRegisteredProfileCombinedDeathReward(t *testing.T) {
16093+
const profile = "formula_reward_profile_runtime"
16094+
if !worldruntime.RegisterStaticActorCombatProfile(profile, worldruntime.StaticActorCombatProfileDefaults{
16095+
MaxHP: 4,
16096+
AttackValue: 3,
16097+
DefenseValue: 1,
16098+
RespawnDelay: worldruntime.PracticeMobBootstrapRespawnDelay,
16099+
DeathReward: worldruntime.StaticActorDeathReward{Experience: 12, Gold: 13, DropVnums: []uint32{27004}},
16100+
}) {
16101+
t.Fatalf("expected formula-only reward profile %q to be accepted", profile)
16102+
}
16103+
t.Cleanup(func() { worldruntime.UnregisterStaticActorCombatProfileForTest(profile) })
16104+
16105+
store := loginticket.NewFileStore(t.TempDir())
16106+
actor := worldruntime.StaticEntity{
16107+
Entity: worldruntime.Entity{ID: 0x0105022F, Kind: worldruntime.EntityKindStaticActor, VID: 0x0105022F, Name: "FormulaRewardProfileMob"},
16108+
Position: worldruntime.NewPosition(bootstrapMapIndex, 1200, 2200),
16109+
RaceNum: 20350,
16110+
CombatProfile: profile,
16111+
CombatKind: profile,
16112+
SpawnGroupRef: "practice.formula_reward_profile_mob",
16113+
}
16114+
killer := peerVisibilityCharacter("FormulaRewardKiller", 0x0103012F, 0x0204012F, 1100, 2100, 0, 101, 201)
16115+
killer.Points[bootstrapExperiencePointType] = 30
16116+
killer.Gold = 40
16117+
issuePeerTicket(t, store, "formula-reward-killer", 0x2f2f2f2f, killer)
16118+
16119+
accounts := accountstore.NewFileStore(t.TempDir())
16120+
if err := accounts.Save(accountstore.Account{Login: "formula-reward-killer", Empire: killer.Empire, Characters: []loginticket.Character{killer}}); err != nil {
16121+
t.Fatalf("seed formula reward killer account: %v", err)
16122+
}
16123+
currentTime := time.Unix(1_700_000_730, 0)
16124+
runtime, err := newGameRuntimeWithAccountStore(config.Service{LegacyAddr: ":13000", PublicAddr: "127.0.0.1"}, store, accounts)
16125+
if err != nil {
16126+
t.Fatalf("new game runtime: %v", err)
16127+
}
16128+
runtime.now = func() time.Time { return currentTime }
16129+
if _, ok := runtime.sharedWorld.registerStaticActor(actor.Entity.ID, actor.Entity.Name, actor.Position.MapIndex, actor.Position.X, actor.Position.Y, actor.RaceNum, "", "", actor.CombatKind, actor.SpawnGroupRef, worldruntime.StaticActorDeathReward{}); !ok {
16130+
t.Fatal("expected formula-only registered-profile reward mob registration to succeed")
16131+
}
16132+
16133+
flow, _ := enterGameWithLoginTicket(t, runtime.SessionFactory(), "formula-reward-killer", 0x2f2f2f2f)
16134+
defer closeSessionFlow(t, flow)
16135+
targetVID := uint32(actor.Entity.ID)
16136+
if selectOut, err := flow.HandleClientFrame(decodeSingleFrame(t, combatproto.EncodeClientTarget(combatproto.ClientTargetPacket{TargetVID: targetVID}))); err != nil || len(selectOut) != 1 {
16137+
t.Fatalf("expected target selection before formula reward kill to succeed with one frame, got frames=%d err=%v", len(selectOut), err)
16138+
}
16139+
16140+
firstHit, err := flow.HandleClientFrame(decodeSingleFrame(t, combatproto.EncodeClientAttack(combatproto.ClientAttackPacket{AttackType: combatproto.ClientAttackTypeNormal, TargetVID: targetVID})))
16141+
if err != nil {
16142+
t.Fatalf("unexpected formula reward first attack error: %v", err)
16143+
}
16144+
if len(firstHit) != 1 {
16145+
t.Fatalf("expected first formula reward hit to return one HP refresh, got %d frames", len(firstHit))
16146+
}
16147+
firstRefresh, err := combatproto.DecodeServerTarget(decodeSingleFrame(t, firstHit[0]))
16148+
if err != nil {
16149+
t.Fatalf("decode first formula reward HP refresh: %v", err)
16150+
}
16151+
if firstRefresh.TargetVID != targetVID || firstRefresh.HPPercent != 50 {
16152+
t.Fatalf("expected formula-only attack/defense profile to report 50%% HP after first hit, got %+v", firstRefresh)
16153+
}
16154+
16155+
currentTime = currentTime.Add(bootstrapNormalAttackCadenceWindow)
16156+
killOut, err := flow.HandleClientFrame(decodeSingleFrame(t, combatproto.EncodeClientAttack(combatproto.ClientAttackPacket{AttackType: combatproto.ClientAttackTypeNormal, TargetVID: targetVID})))
16157+
if err != nil {
16158+
t.Fatalf("unexpected formula reward killing attack error: %v", err)
16159+
}
16160+
if len(killOut) != 6 {
16161+
t.Fatalf("expected killing hit to return dead, clear target, experience, gold, ground-add, and ownership frames, got %d", len(killOut))
16162+
}
16163+
if _, err := worldproto.DecodeDead(decodeSingleFrame(t, killOut[0])); err != nil {
16164+
t.Fatalf("decode formula reward killing hit dead frame: %v", err)
16165+
}
16166+
clearTarget, err := combatproto.DecodeServerTarget(decodeSingleFrame(t, killOut[1]))
16167+
if err != nil {
16168+
t.Fatalf("decode formula reward clear target frame: %v", err)
16169+
}
16170+
if clearTarget.TargetVID != 0 || clearTarget.HPPercent != 0 {
16171+
t.Fatalf("expected formula reward killing hit to clear target, got %+v", clearTarget)
16172+
}
16173+
experienceChange, err := worldproto.DecodePlayerPointChange(decodeSingleFrame(t, killOut[2]))
16174+
if err != nil {
16175+
t.Fatalf("decode formula reward experience point change: %v", err)
16176+
}
16177+
if experienceChange.VID != killer.VID || experienceChange.Type != bootstrapExperiencePointType || experienceChange.Amount != 12 || experienceChange.Value != 42 {
16178+
t.Fatalf("unexpected formula reward experience point change: %+v", experienceChange)
16179+
}
16180+
goldChange, err := worldproto.DecodePlayerPointChange(decodeSingleFrame(t, killOut[3]))
16181+
if err != nil {
16182+
t.Fatalf("decode formula reward gold point change: %v", err)
16183+
}
16184+
if goldChange.VID != killer.VID || goldChange.Type != bootstrapGoldPointType || goldChange.Amount != 13 || goldChange.Value != 53 {
16185+
t.Fatalf("unexpected formula reward gold point change: %+v", goldChange)
16186+
}
16187+
ground, err := itemproto.DecodeGroundAdd(decodeSingleFrame(t, killOut[4]))
16188+
if err != nil {
16189+
t.Fatalf("decode formula reward ground add: %v", err)
16190+
}
16191+
expectedVID := bootstrapRewardGroundItemVID(killer, 27004, 0)
16192+
if ground.VID != expectedVID || ground.Vnum != 27004 || ground.X != killer.X || ground.Y != killer.Y {
16193+
t.Fatalf("unexpected formula reward ground add: %+v want vid=%d", ground, expectedVID)
16194+
}
16195+
ownership, err := itemproto.DecodeOwnership(decodeSingleFrame(t, killOut[5]))
16196+
if err != nil {
16197+
t.Fatalf("decode formula reward ownership: %v", err)
16198+
}
16199+
if ownership != (itemproto.OwnershipPacket{VID: expectedVID, OwnerName: killer.Name}) {
16200+
t.Fatalf("unexpected formula reward ownership: %+v", ownership)
16201+
}
16202+
if !runtime.sharedWorld.GroundItemExists(expectedVID) {
16203+
t.Fatalf("expected formula reward ground item %d to be registered", expectedVID)
16204+
}
16205+
16206+
account, err := accounts.Load("formula-reward-killer")
16207+
if err != nil {
16208+
t.Fatalf("load formula rewarded account: %v", err)
16209+
}
16210+
if got := account.Characters[0].Points[bootstrapExperiencePointType]; got != 42 {
16211+
t.Fatalf("expected formula rewarded account experience point 42 after mob death, got %d", got)
16212+
}
16213+
if got := account.Characters[0].Gold; got != 53 {
16214+
t.Fatalf("expected formula rewarded account gold 53 after mob death, got %d", got)
16215+
}
16216+
}
16217+
1609216218
func TestNewGameSessionFactoryPrefersExplicitDeathRewardOverRegisteredProfileDefault(t *testing.T) {
1609316219
const profile = "explicit_reward_profile_runtime"
1609416220
if !worldruntime.RegisterStaticActorCombatProfile(profile, worldruntime.StaticActorCombatProfileDefaults{

spec/protocol/non-player-reward-bootstrap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Registered bootstrap combat profiles may carry a validated death-reward default
4646
Runtime coverage freezes that a spawn-backed actor using such a registered profile can apply the profile's default EXP/gold/drop descriptor on the accepted killing hit.
4747
That includes a drop-only profile default case where the killing hit emits the normal death/clear frames followed by one `ITEM_GROUND_ADD` / `ITEM_OWNERSHIP` pair and registers the deterministic reward ground item.
4848
That runtime coverage now also includes formula-only registered profiles whose `damage_per_normal_attack` is canonicalized from `attack_value - defense_value`, so the profile can both drive deterministic HP mutation and apply its reward descriptor on the same death edge.
49+
A formula-only registered profile may carry a combined descriptor as well; the killing hit still uses the formula-derived HP mutation first, then emits death/clear, scalar EXP/gold point changes, and deterministic owned drop frames in the normal reward ordering.
4950
If the authored spawn-group snapshot also carries an explicit reward descriptor, that authored descriptor wins over the profile-level default for the current actor life; profile defaults are only the fallback for spawn-backed actors whose live/authored snapshot has no explicit reward descriptor.
5051
That profile-level default still does **not** make standalone runtime static actors reward-bearing: without a non-empty `spawn_group_ref` or explicit authored live snapshot descriptor, the shared-world death attempt returns a rewardless descriptor.
5152

0 commit comments

Comments
 (0)