diff --git a/assets/behaviors/citizen.behavior b/assets/behaviors/citizen.behavior index c5673938..b332987b 100644 --- a/assets/behaviors/citizen.behavior +++ b/assets/behaviors/citizen.behavior @@ -1,5 +1,21 @@ { dynamic: [ + { + guard: { + componentPresent: "MetalRenegades:NearbyCitizenEnemies", + values: ["N enemiesWithinRange nonEmpty"], + child: { + sequence: [ + set_target_to_enemy, + { + lookup: { + tree: "Behaviors:hostile" + } + } + ] + } + } + }, { selector: [ { diff --git a/assets/prefabs/characters/badCitizen.prefab b/assets/prefabs/characters/badCitizen.prefab index 0e30d751..0d891275 100644 --- a/assets/prefabs/characters/badCitizen.prefab +++ b/assets/prefabs/characters/badCitizen.prefab @@ -43,5 +43,8 @@ "Behavior": { "tree": "MetalRenegades:citizen" }, - "Trader": {} + "Trader": {}, + "FactionAlignment": { + "alignment": "BAD" + } } diff --git a/assets/prefabs/characters/baseCitizen.prefab b/assets/prefabs/characters/baseCitizen.prefab index 81558926..19d79f34 100644 --- a/assets/prefabs/characters/baseCitizen.prefab +++ b/assets/prefabs/characters/baseCitizen.prefab @@ -48,7 +48,11 @@ }, "Network": {}, "MinionMove": {}, - "Health": {}, + "Health": { + "destroyEntityOnNoHealth" : true, + "currentHealth": 30, + "maxHealth": 30 + }, "BoxShape": { "extents": [ 1.0, @@ -63,6 +67,8 @@ ] }, "Citizen": {}, + "NearbyCitizenEnemies": {}, + "AttackOnHit": {}, "Inventory": { "privateToOwner": false, "itemSlots": [ diff --git a/assets/prefabs/characters/goodCitizen.prefab b/assets/prefabs/characters/goodCitizen.prefab index 11006a65..3df4a785 100644 --- a/assets/prefabs/characters/goodCitizen.prefab +++ b/assets/prefabs/characters/goodCitizen.prefab @@ -43,5 +43,8 @@ "Behavior": { "tree": "MetalRenegades:citizen" }, - "Trader": {} + "Trader": {}, + "FactionAlignment": { + "alignment": "GOOD" + } } diff --git a/assets/prefabs/characters/gooeyCitizen.prefab b/assets/prefabs/characters/gooeyCitizen.prefab index 3c4e3a90..cdc3c95c 100644 --- a/assets/prefabs/characters/gooeyCitizen.prefab +++ b/assets/prefabs/characters/gooeyCitizen.prefab @@ -43,5 +43,8 @@ "Behavior": { "tree": "MetalRenegades:citizen" }, - "Trader": {} + "Trader": {}, + "FactionAlignment": { + "alignment": "NEUTRAL" + } } diff --git a/src/main/java/org/terasology/metalrenegades/ai/actions/CheckNeedAction.java b/src/main/java/org/terasology/metalrenegades/ai/actions/CheckNeedAction.java index 9d435380..78764073 100644 --- a/src/main/java/org/terasology/metalrenegades/ai/actions/CheckNeedAction.java +++ b/src/main/java/org/terasology/metalrenegades/ai/actions/CheckNeedAction.java @@ -32,6 +32,10 @@ public class CheckNeedAction extends BaseAction { @Override public BehaviorState modify(Actor actor, BehaviorState result) { + if(!actor.hasComponent(NeedsComponent.class)) { + return BehaviorState.FAILURE; + } + CitizenNeed.Type needTypeValue = CitizenNeed.Type.valueOf(needType); NeedsComponent needsComponent = actor.getComponent(NeedsComponent.class); diff --git a/src/main/java/org/terasology/metalrenegades/ai/actions/SetTargetToEnemyAction.java b/src/main/java/org/terasology/metalrenegades/ai/actions/SetTargetToEnemyAction.java new file mode 100644 index 00000000..f9717ab9 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/actions/SetTargetToEnemyAction.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.actions; + +import org.terasology.behaviors.components.FollowComponent; +import org.terasology.logic.behavior.BehaviorAction; +import org.terasology.logic.behavior.core.Actor; +import org.terasology.logic.behavior.core.BaseAction; +import org.terasology.logic.behavior.core.BehaviorState; +import org.terasology.metalrenegades.ai.component.NearbyCitizenEnemiesComponent; + +/** + * Action which sets this agent's move target to the nearest citizen from an enemy faction, as + * defined in {@link NearbyCitizenEnemiesComponent}. + */ +@BehaviorAction(name = "set_target_to_enemy") +public class SetTargetToEnemyAction extends BaseAction { + + @Override + public BehaviorState modify(Actor actor, BehaviorState result) { + if (!actor.hasComponent(NearbyCitizenEnemiesComponent.class)) { + return BehaviorState.SUCCESS; + } + + NearbyCitizenEnemiesComponent enemiesComponent = actor.getComponent(NearbyCitizenEnemiesComponent.class); + FollowComponent followComponent = new FollowComponent(); + + followComponent.entityToFollow = enemiesComponent.closestEnemy; + actor.getEntity().addComponent(followComponent); + + return BehaviorState.SUCCESS; + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/ai/component/FactionAlignmentComponent.java b/src/main/java/org/terasology/metalrenegades/ai/component/FactionAlignmentComponent.java new file mode 100644 index 00000000..055b87aa --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/component/FactionAlignmentComponent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.component; + +import org.terasology.entitySystem.Component; +import org.terasology.metalrenegades.ai.system.FactionAlignmentSystem.Alignment; + +/** + * Defines the faction alignment of a particular character, building, or settlement. + */ +public class FactionAlignmentComponent implements Component { + + public Alignment alignment; + + public FactionAlignmentComponent() { + this.alignment = Alignment.NEUTRAL; + } + + public FactionAlignmentComponent(Alignment alignment) { + this.alignment = alignment; + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/ai/component/NearbyCitizenEnemiesComponent.java b/src/main/java/org/terasology/metalrenegades/ai/component/NearbyCitizenEnemiesComponent.java new file mode 100644 index 00000000..dcfecde7 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/component/NearbyCitizenEnemiesComponent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.component; + +import org.terasology.entitySystem.Component; +import org.terasology.entitySystem.entity.EntityRef; + +import java.util.List; + +/** + * Stores the faction enemies in range of this citizen. + */ +public class NearbyCitizenEnemiesComponent implements Component { + + public float searchRadius = 20f; + + public List enemiesWithinRange; + + public EntityRef closestEnemy; + +} diff --git a/src/main/java/org/terasology/metalrenegades/ai/event/CitizenSpawnedEvent.java b/src/main/java/org/terasology/metalrenegades/ai/event/CitizenSpawnedEvent.java new file mode 100644 index 00000000..974aebc3 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/event/CitizenSpawnedEvent.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.event; + +import org.terasology.entitySystem.event.Event; + +/** + * Fired when a new citizen character is spawned by {@link org.terasology.metalrenegades.ai.system.CitizenSpawnSystem}. + */ +public class CitizenSpawnedEvent implements Event { + +} diff --git a/src/main/java/org/terasology/metalrenegades/ai/system/CitizenSpawnSystem.java b/src/main/java/org/terasology/metalrenegades/ai/system/CitizenSpawnSystem.java index d538bbf5..2fa3a015 100644 --- a/src/main/java/org/terasology/metalrenegades/ai/system/CitizenSpawnSystem.java +++ b/src/main/java/org/terasology/metalrenegades/ai/system/CitizenSpawnSystem.java @@ -19,6 +19,7 @@ import org.terasology.dialogs.components.DialogComponent; import org.terasology.dialogs.components.DialogPage; import org.terasology.dialogs.components.DialogResponse; +import org.terasology.dynamicCities.buildings.components.SettlementRefComponent; import org.terasology.entitySystem.entity.EntityBuilder; import org.terasology.entitySystem.entity.EntityManager; import org.terasology.entitySystem.entity.EntityRef; @@ -32,18 +33,17 @@ import org.terasology.logic.inventory.events.GiveItemEvent; import org.terasology.logic.location.LocationComponent; import org.terasology.metalrenegades.ai.CitizenNeed; -import org.terasology.metalrenegades.ai.component.CitizenComponent; +import org.terasology.metalrenegades.ai.component.FactionAlignmentComponent; import org.terasology.metalrenegades.ai.component.HomeComponent; import org.terasology.metalrenegades.ai.component.NeedsComponent; import org.terasology.metalrenegades.ai.component.PotentialHomeComponent; -import org.terasology.metalrenegades.economy.MarketCitizenComponent; +import org.terasology.metalrenegades.ai.event.CitizenSpawnedEvent; import org.terasology.metalrenegades.economy.TraderComponent; import org.terasology.metalrenegades.economy.actions.ShowTradingScreenAction; +import org.terasology.registry.CoreRegistry; import org.terasology.metalrenegades.minimap.events.AddCharacterToOverlayEvent; import org.terasology.registry.In; - import java.util.ArrayList; -import java.util.Collection; /** * Spawns new citizens inside of available buildings with {@link PotentialHomeComponent}. @@ -60,10 +60,19 @@ public class CitizenSpawnSystem extends BaseComponentSystem implements UpdateSub private EntityManager entityManager; @In - private PrefabManager prefabManager; + private InventoryManager inventoryManager; @In - private InventoryManager inventoryManager; + private FactionAlignmentSystem citizenAlignmentSystem; + + @In + private PrefabManager prefabManager; + + @Override + public void initialise() { + // TODO: Temporary fix for injection malfunction in actions, ideally remove this in the future. + citizenAlignmentSystem = CoreRegistry.get(FactionAlignmentSystem.class); + } @Override public void update(float delta) { @@ -98,12 +107,10 @@ public void update(float delta) { * @return The new citizen entity, or null if spawning is not possible. */ private EntityRef spawnCitizen(EntityRef homeEntity) { - Prefab citizenPrefab = chooseCitizenPrefab(); - if (citizenPrefab == null) { // if no prefab is available. - return null; - } + SettlementRefComponent settlementRefComponent = homeEntity.getComponent(SettlementRefComponent.class); + FactionAlignmentComponent settlementAlignmentComponent = settlementRefComponent.settlement.getComponent(FactionAlignmentComponent.class); - EntityBuilder entityBuilder = entityManager.newBuilder(chooseCitizenPrefab()); + EntityBuilder entityBuilder = entityManager.newBuilder(citizenAlignmentSystem.getPrefab(settlementAlignmentComponent.alignment)); LocationComponent homeLocationComponent = homeEntity.getComponent(LocationComponent.class); LocationComponent citizenLocationComponent = entityBuilder.getComponent(LocationComponent.class); @@ -123,6 +130,7 @@ private EntityRef spawnCitizen(EntityRef homeEntity) { needsComponent.needs.put(CitizenNeed.Type.REST, new CitizenNeed(50, 0.5f, 20, 50)); entityBuilder.saveComponent(needsComponent); + entityBuilder.addComponent(new TraderComponent()); EntityRef entityRef = entityBuilder.build(); @@ -131,25 +139,9 @@ private EntityRef spawnCitizen(EntityRef homeEntity) { setupStartInventory(entityRef); } - return entityRef; - } + entityRef.send(new CitizenSpawnedEvent()); - /** - * Selects a random citizen prefab from a collection of prefabs with {@link CitizenComponent}. - * - * @return A random citizen prefab, or null if none are available. - */ - private Prefab chooseCitizenPrefab() { - Collection citizenList = prefabManager.listPrefabs(CitizenComponent.class); - citizenList.removeIf(prefab -> prefab.hasComponent(MarketCitizenComponent.class)); - - int i = (int) (Math.random() * citizenList.size()); - for (Prefab prefab : citizenList) { - if (i-- <= 0) { - return prefab; - } - } - return null; + return entityRef; } private void setupStartInventory(EntityRef citizen) { diff --git a/src/main/java/org/terasology/metalrenegades/ai/system/FactionAlignmentSystem.java b/src/main/java/org/terasology/metalrenegades/ai/system/FactionAlignmentSystem.java new file mode 100644 index 00000000..3f10e1a1 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/system/FactionAlignmentSystem.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.system; + +import org.terasology.assets.management.AssetManager; +import org.terasology.dynamicCities.settlements.events.SettlementRegisterEvent; +import org.terasology.entitySystem.entity.EntityRef; +import org.terasology.entitySystem.event.ReceiveEvent; +import org.terasology.entitySystem.prefab.Prefab; +import org.terasology.entitySystem.prefab.PrefabManager; +import org.terasology.entitySystem.systems.BaseComponentSystem; +import org.terasology.entitySystem.systems.RegisterMode; +import org.terasology.entitySystem.systems.RegisterSystem; +import org.terasology.metalrenegades.ai.component.FactionAlignmentComponent; +import org.terasology.registry.In; +import org.terasology.registry.Share; + +/** + * Assigns a faction to new settlements on spawn, and stores references to the character prefabs for all faction + * types. + */ +@RegisterSystem(RegisterMode.AUTHORITY) +@Share(value = FactionAlignmentSystem.class) +public class FactionAlignmentSystem extends BaseComponentSystem { + + @In + private AssetManager assetManager; + + @In + private PrefabManager prefabManager; + + /** + * Defines a particular alignment with an associated character prefab. + */ + public enum Alignment { + GOOD("goodCitizen"), + BAD("badCitizen"), + NEUTRAL("gooeyCitizen"); + + private final String assetId; + + Alignment(String assetId) { + this.assetId = assetId; + } + } + + /** + * Returns the prefab associated with a provided faction alignment. + * + * @param alignment The alignment to return a prefab for. + * @return The character prefab for this alignment. + */ + public Prefab getPrefab(Alignment alignment) { + return prefabManager.getPrefab(alignment.assetId); + } + + @ReceiveEvent + public void onSettlementRegisterEvent(SettlementRegisterEvent buildingEntitySpawnedEvent, EntityRef entityRef) { + entityRef.addComponent(new FactionAlignmentComponent(Alignment.values()[(int) (Math.random() * Alignment.values().length)])); + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/ai/system/FactionEnemiesSystem.java b/src/main/java/org/terasology/metalrenegades/ai/system/FactionEnemiesSystem.java new file mode 100644 index 00000000..b7928369 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/ai/system/FactionEnemiesSystem.java @@ -0,0 +1,117 @@ +/* + * Copyright 2018 MovingBlocks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.terasology.metalrenegades.ai.system; + +import com.google.common.collect.Lists; +import org.terasology.entitySystem.entity.EntityManager; +import org.terasology.entitySystem.entity.EntityRef; +import org.terasology.entitySystem.systems.BaseComponentSystem; +import org.terasology.entitySystem.systems.RegisterMode; +import org.terasology.entitySystem.systems.RegisterSystem; +import org.terasology.entitySystem.systems.UpdateSubscriberSystem; +import org.terasology.logic.location.LocationComponent; +import org.terasology.math.geom.Vector3f; +import org.terasology.metalrenegades.ai.component.CitizenComponent; +import org.terasology.metalrenegades.ai.component.FactionAlignmentComponent; +import org.terasology.metalrenegades.ai.component.NearbyCitizenEnemiesComponent; +import org.terasology.registry.In; + +/** + * Tracks nearby faction enemies much like {@link org.terasology.behaviors.system.FindNearbyPlayersSystem}, and stores + * the results in each citizens {@link NearbyCitizenEnemiesComponent}. + */ +@RegisterSystem(value = RegisterMode.AUTHORITY) +public class FactionEnemiesSystem extends BaseComponentSystem implements UpdateSubscriberSystem { + + private static final float ENEMY_CHECK_DELAY = 5; + + private float counter; + + @In + private EntityManager entityManager; + + @Override + public void update(float delta) { + counter+=delta; + + if(counter < ENEMY_CHECK_DELAY) { + return; + } + + counter = 0; + + for (EntityRef entity : entityManager.getEntitiesWith(NearbyCitizenEnemiesComponent.class)) { + checkForEnemies(entity); + } + } + + /** + * Checks for faction enemies nearby a particular citizen. Enemies are defined as follows: + * {@link org.terasology.metalrenegades.ai.system.FactionAlignmentSystem.Alignment#GOOD} citizens are enemies of + * {@link org.terasology.metalrenegades.ai.system.FactionAlignmentSystem.Alignment#BAD} citizens, and vice-versa, + * while {@link org.terasology.metalrenegades.ai.system.FactionAlignmentSystem.Alignment#NEUTRAL} are enemies with + * no other citizens. + * + * Results are stored in {@link NearbyCitizenEnemiesComponent}. + * + * @param citizen The citizen to check enemies for. + */ + private void checkForEnemies(EntityRef citizen) { + if (!citizen.hasComponent(NearbyCitizenEnemiesComponent.class) + || !citizen.hasComponent(FactionAlignmentComponent.class)) { + return; + } + + NearbyCitizenEnemiesComponent enemiesComponent = citizen.getComponent(NearbyCitizenEnemiesComponent.class); + FactionAlignmentComponent alignmentComponent = citizen.getComponent(FactionAlignmentComponent.class); + + enemiesComponent.enemiesWithinRange = Lists.newArrayList(); + enemiesComponent.closestEnemy = EntityRef.NULL; + + float minDistance = Float.MAX_VALUE; + Vector3f actorPosition = citizen.getComponent(LocationComponent.class).getWorldPosition(); + + for (EntityRef otherCitizen : entityManager.getEntitiesWith(CitizenComponent.class)) { + if (!otherCitizen.hasComponent(FactionAlignmentComponent.class) + || otherCitizen.equals(citizen)) { + continue; + } + + FactionAlignmentComponent otherAlignmentComponent = otherCitizen.getComponent(FactionAlignmentComponent.class); + if (otherAlignmentComponent.alignment.equals(alignmentComponent.alignment) // continue if alignments are + || otherAlignmentComponent.alignment.equals(FactionAlignmentSystem.Alignment.NEUTRAL) // the same, or either alignment + || alignmentComponent.alignment.equals(FactionAlignmentSystem.Alignment.NEUTRAL)) { // is neutral. + continue; + } + + LocationComponent otherLocationComponent = otherCitizen.getComponent(LocationComponent.class); + float distanceApart = otherLocationComponent.getWorldPosition().distanceSquared(actorPosition); + + if (distanceApart > enemiesComponent.searchRadius * enemiesComponent.searchRadius) { + continue; + } + + if (distanceApart < minDistance) { + enemiesComponent.closestEnemy = otherCitizen; + minDistance = otherLocationComponent.getWorldPosition().distanceSquared(actorPosition); + } + + enemiesComponent.enemiesWithinRange.add(otherCitizen); + } + + citizen.saveComponent(enemiesComponent); + } +}