diff --git a/cardinal/component/building.go b/cardinal/component/building.go index e6972f7..64fe052 100644 --- a/cardinal/component/building.go +++ b/cardinal/component/building.go @@ -30,6 +30,7 @@ func GetAllBuildingTypes() []BuildingType { } type Building struct { + TileID int `json:"tileID"` Level int `json:"level"` Type BuildingType `json:"type"` Farming *Farming `json:"farming,omitempty"` @@ -43,6 +44,7 @@ func (Building) Name() string { } type BuildingConstants struct { + TileID int UnitLimit int StorageCapacity int Resources []Resource @@ -53,6 +55,7 @@ type BuildingConstants struct { var BuildingConfigs = map[BuildingType]BuildingConstants{ Main: { + TileID: constants.MainTileID, Resources: []Resource{}, UnitLimit: constants.MainUnitLimit, StorageCapacity: constants.MainStorageCapacity, @@ -100,6 +103,7 @@ var BuildingConfigs = map[BuildingType]BuildingConstants{ {Type: Wood, Amount: constants.ShipyardEffectRaftResourceWood}, {Type: Fish, Amount: constants.ShipyardEffectRaftResourceFish}, }, + BuildingTimeSeconds: constants.ShipyardEffectRaftBuildSeconds, }, TileType: CoastlineTile, }, @@ -128,6 +132,7 @@ func GetBuilding(buildingType BuildingType) (Building, error) { switch buildingType { case Main: return Building{ + TileID: config.TileID, Level: 1, Type: buildingType, UnitLimit: config.UnitLimit, @@ -153,14 +158,9 @@ func GetBuilding(buildingType BuildingType) (Building, error) { }, nil case Shipyard: return Building{ - Level: 1, - Type: buildingType, - Effect: &Effect{ - Type: config.Effect.Type, - Amount: 0, - Capacity: config.Effect.Capacity, - Resources: config.Effect.Resources, - }, + Level: 1, + Type: buildingType, + Effect: config.Effect, }, nil case Warehouse: return Building{ diff --git a/cardinal/component/effect.go b/cardinal/component/effect.go index 9038581..6350ecc 100644 --- a/cardinal/component/effect.go +++ b/cardinal/component/effect.go @@ -1,5 +1,7 @@ package component +import "oceanus-shard/constants" + type EffectType string const ( @@ -12,11 +14,22 @@ func GetAllEffectTypes() []EffectType { } } +func GetCapacityByEffectType(effectType EffectType) int { + switch effectType { + case Raft: + return constants.ShipyardEffectRaftCapacity + default: + return 0 + } +} + type Effect struct { - Type EffectType `json:"type"` - Amount int `json:"amount"` - Capacity int `json:"capacity"` - Resources []Resource `json:"resources,omitempty"` + Type EffectType `json:"type"` + Amount int `json:"amount"` + Capacity int `json:"capacity"` + Resources []Resource `json:"resources,omitempty"` + BuildingTimeSeconds int `json:"buildingTimeSeconds,omitempty"` + BuildingTimeStartedAt uint64 `json:"buildingTimeStartedAt,omitempty"` } func (Effect) Name() string { diff --git a/cardinal/constants/buildings.go b/cardinal/constants/buildings.go index 7302d55..2c3840f 100644 --- a/cardinal/constants/buildings.go +++ b/cardinal/constants/buildings.go @@ -1,6 +1,7 @@ package constants const ( + MainTileID = 22 MainUnitLimit = 5 // MainStorageCapacity = 10 MainStorageCapacity = 100 @@ -31,6 +32,7 @@ const ( ShipyardEffectRaftCapacity = 1 ShipyardEffectRaftResourceWood = 10 ShipyardEffectRaftResourceFish = 20 + ShipyardEffectRaftBuildSeconds = 60 ) const ( diff --git a/cardinal/go.mod b/cardinal/go.mod index 1f17b4c..8b14f26 100644 --- a/cardinal/go.mod +++ b/cardinal/go.mod @@ -4,7 +4,6 @@ go 1.22.1 require ( github.com/rs/zerolog v1.32.0 - gotest.tools/v3 v3.5.1 pkg.world.dev/world-engine/cardinal v1.5.1 ) @@ -113,6 +112,7 @@ require ( gopkg.in/DataDog/dd-trace-go.v1 v1.63.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect pkg.world.dev/world-engine/rift v1.1.0-beta.0.20240402214846-de1fc179818a // indirect pkg.world.dev/world-engine/sign v1.0.1-beta // indirect ) diff --git a/cardinal/main.go b/cardinal/main.go index e15fc8c..247377f 100644 --- a/cardinal/main.go +++ b/cardinal/main.go @@ -47,18 +47,14 @@ func MustInitWorld(w *cardinal.World) { // NOTE: You must register your transactions here for it to be executed. Must( cardinal.RegisterMessage[msg.CreatePlayerMsg, msg.CreatePlayerResult](w, "create-player"), - cardinal.RegisterMessage[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply](w, "attack-player"), cardinal.RegisterMessage[msg.CreateBuildingMsg, msg.CreateBuildingResult](w, "create-building"), cardinal.RegisterMessage[msg.DeleteBuildingMsg, msg.DeleteBuildingResult](w, "delete-building"), + cardinal.RegisterMessage[msg.CreateEffectMsg, msg.CreateEffectResult](w, "create-effect"), ) // Register queries // NOTE: You must register your queries here for it to be accessible. Must( - cardinal.RegisterQuery[ - query.PlayerHealthRequest, - query.PlayerHealthResponse, - ](w, "player-health", query.PlayerHealth), cardinal.RegisterQuery[ query.MapStateRequest, query.MapStateResponse, @@ -78,11 +74,12 @@ func MustInitWorld(w *cardinal.World) { // For example, you may want to run the attack system before the regen system // so that the player's HP is subtracted (and player killed if it reaches 0) before HP is regenerated. Must(cardinal.RegisterSystems(w, - system.AttackSystem, system.FarmingSystem, system.PlayerSpawnerSystem, system.CreateBuildingSystem, system.DeleteBuildingSystem, + system.CreateEffectSystem, + system.EffectsSpawnerSystem, )) } diff --git a/cardinal/msg/attack_player.go b/cardinal/msg/attack_player.go deleted file mode 100644 index 95aa945..0000000 --- a/cardinal/msg/attack_player.go +++ /dev/null @@ -1,9 +0,0 @@ -package msg - -type AttackPlayerMsg struct { - TargetNickname string `json:"target"` -} - -type AttackPlayerMsgReply struct { - Damage int `json:"damage"` -} diff --git a/cardinal/msg/create_effect.go b/cardinal/msg/create_effect.go new file mode 100644 index 0000000..78c5aec --- /dev/null +++ b/cardinal/msg/create_effect.go @@ -0,0 +1,10 @@ +package msg + +type CreateEffectMsg struct { + EffectType string `json:"effectType"` + TileIndex int `json:"tileIndex"` +} + +type CreateEffectResult struct { + Success bool `json:"success"` +} diff --git a/cardinal/query/player_health.go b/cardinal/query/player_health.go deleted file mode 100644 index f5a4698..0000000 --- a/cardinal/query/player_health.go +++ /dev/null @@ -1,58 +0,0 @@ -package query - -import ( - "fmt" - - "pkg.world.dev/world-engine/cardinal/search/filter" - "pkg.world.dev/world-engine/cardinal/types" - - comp "oceanus-shard/component" - - "pkg.world.dev/world-engine/cardinal" -) - -type PlayerHealthRequest struct { - Nickname string -} - -type PlayerHealthResponse struct { - HP int -} - -func PlayerHealth(world cardinal.WorldContext, req *PlayerHealthRequest) (*PlayerHealthResponse, error) { - var playerHealth *comp.Health - var err error - searchErr := cardinal.NewSearch().Entity( - filter.Exact(filter.Component[comp.Player](), filter.Component[comp.Health]())). - Each(world, func(id types.EntityID) bool { - var player *comp.Player - player, err = cardinal.GetComponent[comp.Player](world, id) - if err != nil { - return false - } - - // Terminates the search if the player is found - if player.Nickname == req.Nickname { - playerHealth, err = cardinal.GetComponent[comp.Health](world, id) - if err != nil { - return false - } - return false - } - - // Continue searching if the player is not the target player - return true - }) - if searchErr != nil { - return nil, searchErr - } - if err != nil { - return nil, err - } - - if playerHealth == nil { - return nil, fmt.Errorf("player %s does not exist", req.Nickname) - } - - return &PlayerHealthResponse{HP: playerHealth.HP}, nil -} diff --git a/cardinal/system/attack.go b/cardinal/system/attack.go deleted file mode 100644 index 3d27f72..0000000 --- a/cardinal/system/attack.go +++ /dev/null @@ -1,38 +0,0 @@ -package system - -import ( - "fmt" - - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/search/filter" - - comp "oceanus-shard/component" - "oceanus-shard/msg" -) - -const AttackDamage = 10 - -// AttackSystem inflict damage to player's HP based on `AttackPlayer` transactions. -// This provides an example of a system that modifies the component of an entity. -func AttackSystem(world cardinal.WorldContext) error { - return cardinal.EachMessage[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply]( - world, - func(attack cardinal.TxData[msg.AttackPlayerMsg]) (msg.AttackPlayerMsgReply, error) { - playerID, playerHealth, err := QueryComponent[comp.Health]( - world, - attack.Msg.TargetNickname, - filter.Component[comp.Player](), - filter.Component[comp.Health](), - ) - if err != nil { - return msg.AttackPlayerMsgReply{}, fmt.Errorf("failed to inflict damage: %w", err) - } - - playerHealth.HP -= AttackDamage - if err := cardinal.SetComponent[comp.Health](world, playerID, playerHealth); err != nil { - return msg.AttackPlayerMsgReply{}, fmt.Errorf("failed to inflict damage: %w", err) - } - - return msg.AttackPlayerMsgReply{Damage: AttackDamage}, nil - }) -} diff --git a/cardinal/system/create_building.go b/cardinal/system/create_building.go index fc32d3e..6f2b6f0 100644 --- a/cardinal/system/create_building.go +++ b/cardinal/system/create_building.go @@ -35,18 +35,20 @@ func CreateBuildingSystem(world cardinal.WorldContext) error { fmt.Errorf("failed to create building, this building doesn't fit this tiletype") } - if err := SubtractResourcesToBuild(world, building, request.Tx.PersonaTag); err != nil { - return msg.CreateBuildingResult{Success: false}, err - } - tile := &(*playerMap.Tiles)[request.Msg.TileIndex] if tile.Building == nil { + building.TileID = request.Msg.TileIndex tile.Building = &building } else { return msg.CreateBuildingResult{Success: false}, fmt.Errorf("failed to create building, this tile already have another building") } + resourcesForBuild := comp.BuildingConfigs[building.Type].Resources + if err := SubtractResources(world, resourcesForBuild, request.Tx.PersonaTag); err != nil { + return msg.CreateBuildingResult{Success: false}, err + } + player, _ := cardinal.GetComponent[comp.Player](world, mapEntityID) if err := cardinal.SetComponent(world, mapEntityID, playerMap); err != nil { @@ -55,14 +57,7 @@ func CreateBuildingSystem(world cardinal.WorldContext) error { buildingEntityID, _ := cardinal.Create(world, player, - comp.Building{ - Level: building.Level, - Type: building.Type, - Farming: building.Farming, - Effect: building.Effect, - UnitLimit: building.UnitLimit, - StorageCapacity: building.StorageCapacity, - }, + building, ) if building.Farming != nil { @@ -83,7 +78,7 @@ func CreateBuildingSystem(world cardinal.WorldContext) error { }) } -func SubtractResourcesToBuild(world cardinal.WorldContext, building comp.Building, personaTag string) error { +func SubtractResources(world cardinal.WorldContext, resources []comp.Resource, personaTag string) error { playerResourcesEntityID, playerResources, _ := QueryComponent[comp.PlayerResources]( world, personaTag, @@ -91,8 +86,7 @@ func SubtractResourcesToBuild(world cardinal.WorldContext, building comp.Buildin filter.Component[comp.PlayerResources](), ) - resourcesToBuild := comp.BuildingConfigs[building.Type].Resources - for _, resource := range resourcesToBuild { + for _, resource := range resources { var playerResource *comp.Resource var err error if playerResource, err = GetResourceByType(playerResources, resource.Type); err != nil { diff --git a/cardinal/system/create_effect.go b/cardinal/system/create_effect.go new file mode 100644 index 0000000..04ddb5d --- /dev/null +++ b/cardinal/system/create_effect.go @@ -0,0 +1,89 @@ +package system + +import ( + "errors" + "fmt" + + "pkg.world.dev/world-engine/cardinal" + "pkg.world.dev/world-engine/cardinal/search/filter" + + comp "oceanus-shard/component" + "oceanus-shard/msg" +) + +// CreateEffectSystem spawns effects. +func CreateEffectSystem(world cardinal.WorldContext) error { + return cardinal.EachMessage[msg.CreateEffectMsg, msg.CreateEffectResult](world, + func(request cardinal.TxData[msg.CreateEffectMsg]) (msg.CreateEffectResult, error) { + playerMapEntityID, playerMap, _ := QueryComponent[comp.TileMap]( + world, + request.Tx.PersonaTag, + filter.Component[comp.Player](), + filter.Component[comp.TileMap](), + ) + + playerBuildingsEntityIDs, playerBuildingsWithEffect, _ := QueryAllComponents[comp.Building]( + world, + request.Tx.PersonaTag, + filter.Component[comp.Player](), + filter.Component[comp.Building](), + filter.Component[comp.Effect](), + ) + + if playerMap == nil { + return msg.CreateEffectResult{Success: false}, + fmt.Errorf("failed to create effect, this player did not have tilemap") + } + + tileMapPlayer, _ := cardinal.GetComponent[comp.Player](world, playerMapEntityID) + if tileMapPlayer.Nickname != request.Tx.PersonaTag { + return msg.CreateEffectResult{Success: false}, fmt.Errorf("can't use another player map") + } + + if request.Msg.TileIndex < 0 || request.Msg.TileIndex >= len(*playerMap.Tiles) { + return msg.CreateEffectResult{Success: false}, fmt.Errorf("index of tiles out of range") + } + + tile := &(*playerMap.Tiles)[request.Msg.TileIndex] + if tile.Building == nil { + return msg.CreateEffectResult{Success: false}, + fmt.Errorf("failed to create effect, this tile didn't have buildings") + } + + if tile.Building.Effect == nil { + return msg.CreateEffectResult{Success: false}, + fmt.Errorf("failed to create effect, this building didn't have effect") + } + + building, i, err := FindBuildingByTileID(playerBuildingsWithEffect, tile.Building.TileID) + if err != nil { + return msg.CreateEffectResult{Success: false}, err + } + + effectComponent, _ := cardinal.GetComponent[comp.Effect](world, playerBuildingsEntityIDs[i]) + effectComponent.BuildingTimeStartedAt = world.Timestamp() + resourcesForEffect := comp.BuildingConfigs[building.Type].Effect.Resources + if err := SubtractResources(world, resourcesForEffect, request.Tx.PersonaTag); err != nil { + return msg.CreateEffectResult{Success: false}, err + } + if err := cardinal.SetComponent(world, playerBuildingsEntityIDs[i], effectComponent); err != nil { + return msg.CreateEffectResult{Success: false}, fmt.Errorf("failed to create effect: %w", err) + } + + tile.Building.Effect = effectComponent + if err := cardinal.SetComponent(world, playerMapEntityID, playerMap); err != nil { + return msg.CreateEffectResult{Success: false}, fmt.Errorf("failed to create effect: %w", err) + } + + return msg.CreateEffectResult{Success: true}, nil + }) +} + +func FindBuildingByTileID(buildings []*comp.Building, tileID int) (*comp.Building, int, error) { + for index, building := range buildings { + if building.TileID == tileID { + return building, index, nil + } + } + return nil, -1, errors.New("building with given tileID not found") +} diff --git a/cardinal/system/delete_building.go b/cardinal/system/delete_building.go index a4e85b2..fc07fe4 100644 --- a/cardinal/system/delete_building.go +++ b/cardinal/system/delete_building.go @@ -22,7 +22,7 @@ func DeleteBuildingSystem(world cardinal.WorldContext) error { filter.Component[comp.TileMap](), ) - buildingsEntityIDs, buildingComponents, _ := QueryAllComponents[comp.Building]( + buildingsEntityIDs, playerBuildings, _ := QueryAllComponents[comp.Building]( world, request.Tx.PersonaTag, filter.Component[comp.Player](), @@ -50,8 +50,8 @@ func DeleteBuildingSystem(world cardinal.WorldContext) error { return msg.DeleteBuildingResult{Success: false}, fmt.Errorf("failed to delete building, this tile didn't have buildings") } - for i, building := range buildingComponents { - if building.Type == tile.Building.Type { + for i, building := range playerBuildings { + if building.TileID == tile.Building.TileID { if err := cardinal.Remove(world, buildingsEntityIDs[i]); err != nil { return msg.DeleteBuildingResult{Success: false}, fmt.Errorf("failed to delete building entity: %w", err) } diff --git a/cardinal/system/effects_spawner.go b/cardinal/system/effects_spawner.go new file mode 100644 index 0000000..b609c2a --- /dev/null +++ b/cardinal/system/effects_spawner.go @@ -0,0 +1,94 @@ +package system + +import ( + "time" + + "pkg.world.dev/world-engine/cardinal" + "pkg.world.dev/world-engine/cardinal/search/filter" + "pkg.world.dev/world-engine/cardinal/types" + + comp "oceanus-shard/component" + "oceanus-shard/constants" +) + +func EffectsSpawnerSystem(world cardinal.WorldContext) error { + return cardinal.NewSearch().Entity( + filter.Contains(filter.Component[comp.Building](), filter.Component[comp.Effect]())). + Each( + world, + func(id types.EntityID) bool { + playerComponent, _ := cardinal.GetComponent[comp.Player](world, id) + effectComponent, _ := cardinal.GetComponent[comp.Effect](world, id) + buildingComponent, _ := cardinal.GetComponent[comp.Building](world, id) + + if effectComponent.BuildingTimeStartedAt == 0 { + return true + } + + playerMapEntityID, playerMap, _ := QueryComponent[comp.TileMap]( + world, + playerComponent.Nickname, + filter.Component[comp.Player](), + filter.Component[comp.PlayerResources](), + ) + + playerResourcesEntityID, playerResources, _ := QueryComponent[comp.PlayerResources]( + world, + playerComponent.Nickname, + filter.Component[comp.Player](), + filter.Component[comp.PlayerResources](), + ) + + previousEffectAmount := effectComponent.Amount + endBuildingTime := effectComponent.BuildingTimeStartedAt + + uint64(time.Duration(effectComponent.BuildingTimeSeconds)*time.Second/time.Millisecond) + + if world.Timestamp() < endBuildingTime { + return true + } + + effectComponent.Amount = min( + constants.ShipyardEffectRaftCapacity, + effectComponent.Amount+1, + ) + effectComponent.BuildingTimeStartedAt = 0 + buildingComponent.Effect = effectComponent + tile := &(*playerMap.Tiles)[buildingComponent.TileID] + tile.Building = buildingComponent + + for _, playerResourcesEffect := range playerResources.Effects { + if playerResourcesEffect.Type != effectComponent.Type { + continue + } + + totalEffectsAmount := playerResourcesEffect.Amount + effectComponent.Amount - previousEffectAmount + playerResources.Effects = UpdateEffectsAmount(playerResources.Effects, effectComponent.Type, totalEffectsAmount) + if err := cardinal.SetComponent(world, playerResourcesEntityID, playerResources); err != nil { + return true + } + } + + if err := cardinal.SetComponent(world, id, effectComponent); err != nil { + return true + } + if err := cardinal.SetComponent(world, id, buildingComponent); err != nil { + return true + } + if err := cardinal.SetComponent(world, playerMapEntityID, playerMap); err != nil { + return true + } + + return true + }, + ) +} + +func UpdateEffectsAmount(effects []comp.Effect, effectType comp.EffectType, newAmount int) []comp.Effect { + for i := range effects { + if effects[i].Type == effectType { + effects[i].Amount = newAmount + return effects + } + } + return effects +} diff --git a/cardinal/system/player_spawner.go b/cardinal/system/player_spawner.go index 1371c65..42136ea 100644 --- a/cardinal/system/player_spawner.go +++ b/cardinal/system/player_spawner.go @@ -64,8 +64,9 @@ func PlayerSpawnerSystem(world cardinal.WorldContext) error { effects := make([]comp.Effect, len(comp.GetAllEffectTypes())) for i, effectType := range comp.GetAllEffectTypes() { effects[i] = comp.Effect{ - Type: effectType, - Amount: 0, + Type: effectType, + Amount: 0, + Capacity: comp.GetCapacityByEffectType(effectType), } } @@ -82,18 +83,10 @@ func PlayerSpawnerSystem(world cardinal.WorldContext) error { }, ) - var building, _ = comp.GetBuilding(comp.Main) - + var mainBuilding, _ = comp.GetBuilding(comp.Main) mainBuildingID, err := cardinal.Create(world, playerComponent, - comp.Building{ - Level: building.Level, - Type: building.Type, - Farming: building.Farming, - Effect: building.Effect, - UnitLimit: building.UnitLimit, - StorageCapacity: building.StorageCapacity, - }, + mainBuilding, ) if err != nil { diff --git a/cardinal/system/regen.go b/cardinal/system/regen.go deleted file mode 100644 index b5d68aa..0000000 --- a/cardinal/system/regen.go +++ /dev/null @@ -1,27 +0,0 @@ -package system - -import ( - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/search/filter" - "pkg.world.dev/world-engine/cardinal/types" - - comp "oceanus-shard/component" -) - -// RegenSystem replenishes the player's HP at every tick. -// This provides an example of a system that doesn't rely on a transaction to update a component. -func RegenSystem(world cardinal.WorldContext) error { - return cardinal.NewSearch().Entity( - filter.Exact(filter.Component[comp.Player](), filter.Component[comp.Health]())). - Each(world, func(id types.EntityID) bool { - health, err := cardinal.GetComponent[comp.Health](world, id) - if err != nil { - return true - } - health.HP++ - if err := cardinal.SetComponent[comp.Health](world, id, health); err != nil { - return true - } - return true - }) -} diff --git a/cardinal/system_test.go b/cardinal/system_test.go deleted file mode 100644 index 6c6ef06..0000000 --- a/cardinal/system_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "testing" - - "gotest.tools/v3/assert" - - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/receipt" - "pkg.world.dev/world-engine/cardinal/search/filter" - "pkg.world.dev/world-engine/cardinal/types" - - "oceanus-shard/component" - "oceanus-shard/msg" -) - -const ( - attackMsgName = "game.attack-player" - createMsgName = "game.create-player" -) - -// TestSystem_AttackSystem_ErrorWhenTargetDoesNotExist ensures the attack message results in an error when the given -// target does not exist. Note, message errors are stored in receipts; they are NOT returned from the relevant system. -func TestSystem_AttackSystem_ErrorWhenTargetDoesNotExist(t *testing.T) { - tf := cardinal.NewTestFixture(t, nil) - MustInitWorld(tf.World) - - txHash := tf.AddTransaction(getAttackMsgID(t, tf.World), msg.AttackPlayerMsg{ - TargetNickname: "does-not-exist", - }) - - tf.DoTick() - - gotReceipt := getReceiptFromPastTick(t, tf.World, txHash) - if len(gotReceipt.Errs) == 0 { - t.Fatal("expected error when target does not exist") - } -} - -// TestSystem_PlayerSpawnerSystem_CanCreatePlayer ensures the CreatePlayer message can be used to create a new player -// with the default amount of health. cardinal.NewSearch is used to find the newly created player. -func TestSystem_PlayerSpawnerSystem_CanCreatePlayer(t *testing.T) { - tf := cardinal.NewTestFixture(t, nil) - MustInitWorld(tf.World) - - const nickname = "jeff" - createTxHash := tf.AddTransaction(getCreateMsgID(t, tf.World), msg.CreatePlayerMsg{ - Nickname: nickname, - }) - tf.DoTick() - - // Make sure the player creation was successful - createReceipt := getReceiptFromPastTick(t, tf.World, createTxHash) - if errs := createReceipt.Errs; len(errs) > 0 { - t.Fatalf("expected 0 errors when creating a player, got %v", errs) - } - - // Make sure the newly created player has 100 health - wCtx := cardinal.NewReadOnlyWorldContext(tf.World) - // This search demonstrates the use of a "Where" clause, which limits the search results to only the entity IDs - // that end up returning true from the anonymous function. In this case, we're looking for a specific nickname. - acc := make([]types.EntityID, 0) - err := cardinal.NewSearch().Entity(filter.All()).Each(wCtx, func(id types.EntityID) bool { - player, err := cardinal.GetComponent[component.Player](wCtx, id) - if err != nil { - t.Fatalf("failed to get player component: %v", err) - } - if player.Nickname == nickname { - acc = append(acc, id) - return false - } - return true - }) - assert.NilError(t, err) - assert.Equal(t, len(acc), 1) - id := acc[0] - - health, err := cardinal.GetComponent[component.Health](wCtx, id) - if err != nil { - t.Fatalf("failed to find entity ID: %v", err) - } - if health.HP != 100 { - t.Fatalf("a newly created player should have 100 health; got %v", health.HP) - } -} - -// TestSystem_AttackSystem_AttackingTargetReducesTheirHealth ensures an attack message can find an existing target the -// reduce the target's health. -func TestSystem_AttackSystem_AttackingTargetReducesTheirHealth(t *testing.T) { - tf := cardinal.NewTestFixture(t, nil) - MustInitWorld(tf.World) - - const target = "jeff" - - // Create an initial player - _ = tf.AddTransaction(getCreateMsgID(t, tf.World), msg.CreatePlayerMsg{ - Nickname: target, - }) - tf.DoTick() - - // Attack the player - attackTxHash := tf.AddTransaction(getAttackMsgID(t, tf.World), msg.AttackPlayerMsg{ - TargetNickname: target, - }) - tf.DoTick() - - // Make sure attack was successful - attackReceipt := getReceiptFromPastTick(t, tf.World, attackTxHash) - if errs := attackReceipt.Errs; len(errs) > 0 { - t.Fatalf("expected no errors when attacking a player; got %v", errs) - } - - // Find the attacked player and check their health. - wCtx := cardinal.NewReadOnlyWorldContext(tf.World) - var found bool - // This search demonstrates the "Each" pattern. Every entity ID is considered, and as long as the anonymous - // function return true, the search will continue. - searchErr := cardinal.NewSearch().Entity(filter.All()).Each(wCtx, func(id types.EntityID) bool { - player, err := cardinal.GetComponent[component.Player](wCtx, id) - if err != nil { - t.Fatalf("failed to get player component for %v", id) - } - if player.Nickname != target { - return true - } - // The player's nickname matches the target. This is the player we care about. - found = true - health, err := cardinal.GetComponent[component.Health](wCtx, id) - if err != nil { - t.Fatalf("failed to get health component for %v", id) - } - // The target started with 100 HP, -10 for the attack, +1 for regen - if health.HP != 91 { - t.Fatalf("attack target should end up with 91 hp, got %v", health.HP) - } - - return false - }) - if searchErr != nil { - t.Fatalf("error when performing search: %v", searchErr) - } - if !found { - t.Fatalf("failed to find target %q", target) - } -} - -func getCreateMsgID(t *testing.T, world *cardinal.World) types.MessageID { - return getMsgID(t, world, createMsgName) -} - -func getAttackMsgID(t *testing.T, world *cardinal.World) types.MessageID { - return getMsgID(t, world, attackMsgName) -} - -func getMsgID(t *testing.T, world *cardinal.World, fullName string) types.MessageID { - msg, ok := world.GetMessageByFullName(fullName) - if !ok { - t.Fatalf("failed to get %q message", fullName) - } - return msg.ID() -} - -// getReceiptFromPastTick search past ticks for a txHash that matches the given txHash. An error will be returned if -// the txHash cannot be found in Cardinal's history. -func getReceiptFromPastTick(t *testing.T, world *cardinal.World, txHash types.TxHash) receipt.Receipt { - tick := world.CurrentTick() - for { - tick-- - receipts, err := world.GetTransactionReceiptsForTick(tick) - if err != nil { - t.Fatal(err) - } - for _, r := range receipts { - if r.TxHash == txHash { - return r - } - } - } -}