diff --git a/assets/prefabs/blocks/waterCupSource.prefab b/assets/prefabs/blocks/waterCupSource.prefab index fa75495b..7db2b93d 100644 --- a/assets/prefabs/blocks/waterCupSource.prefab +++ b/assets/prefabs/blocks/waterCupSource.prefab @@ -4,6 +4,6 @@ }, "InteractionTarget": {}, "UnbreathableBlock": {}, - "WellSource": {}, + "WellBlock": {}, "Network": {} } diff --git a/assets/prefabs/buildings/marketPlace.prefab b/assets/prefabs/buildings/marketPlace.prefab index 24b964d7..323ff28a 100644 --- a/assets/prefabs/buildings/marketPlace.prefab +++ b/assets/prefabs/buildings/marketPlace.prefab @@ -9,9 +9,10 @@ }, "MarketSubscriber" : { "production" : { - "AdditionalFruits:Blueberry": 3, - "AdditionalFruits:Raspberry": 3, - "AdditionalFruits:Strawberry": 3 + "AdditionalFruits:Blueberry": 3, + "AdditionalFruits:Raspberry": 3, + "AdditionalFruits:Strawberry": 3, + "MetalRenegades:emptyCup": 2 }, "productionInterval" : 10000 }, diff --git a/assets/prefabs/buildings/templates/wellTemplate.prefab b/assets/prefabs/buildings/templates/wellTemplate.prefab index 6c6d70cb..cd07d8a1 100644 --- a/assets/prefabs/buildings/templates/wellTemplate.prefab +++ b/assets/prefabs/buildings/templates/wellTemplate.prefab @@ -9,6 +9,7 @@ { "blockType": "CoreAssets:Cobblestone", "region": { "min": [-1, 0, -2], "size": [1, 1, 1]}}, { "blockType": "CoreAssets:Cobblestone", "region": { "min": [1, 0, -2], "size": [1, 1, 1]}}, { "blockType": "CoreAssets:Cobblestone", "region": { "min": [-1, 0, -1], "size": [3, 1, 1]}}, + { "blockType": "CoreAssets:Cobblestone", "region": { "min": [0, 0, -2], "size": [1, 1, 1]}}, { "blockType": "CoreAssets:Cobblestone:engine:stair.BACK", "region": { "min": [0, 1, -1], "size": [1, 1, 1]}}, { "blockType": "CoreAssets:Cobblestone:engine:stair.FRONT", "region": { "min": [0, 1, -3], "size": [1, 1, 1]}}, { "blockType": "CoreAssets:Cobblestone:engine:stair.LEFT", "region": { "min": [-1, 1, -2], "size": [1, 1, 1]}}, @@ -25,7 +26,7 @@ { "blockType": "engine:air", "region": { "min": [0, 2, -3], "size": [1, 2, 1]}}, { "blockType": "engine:air", "region": { "min": [-1, 2, -2], "size": [3, 2, 1]}}, { "blockType": "engine:air", "region": { "min": [0, 2, -1], "size": [1, 2, 1]}}, - { "blockType": "MetalRenegades:waterCupSource", "region": { "min": [0, 0, -2], "size": [1, 2, 1]}} + { "blockType": "MetalRenegades:waterCupSource", "region": { "min": [0, 1, -2], "size": [1, 1, 1]}} ] } } diff --git a/assets/prefabs/buildings/well.prefab b/assets/prefabs/buildings/well.prefab index fb5c7e7d..7e055b8f 100644 --- a/assets/prefabs/buildings/well.prefab +++ b/assets/prefabs/buildings/well.prefab @@ -9,5 +9,9 @@ }, "SimpleSource": { "needType": "WATER" + }, + "WellSource": { + "refillsLeft": 5, + "capacity": 5 } } diff --git a/assets/prefabs/items/emptyCup.prefab b/assets/prefabs/items/emptyCup.prefab new file mode 100644 index 00000000..e9831b27 --- /dev/null +++ b/assets/prefabs/items/emptyCup.prefab @@ -0,0 +1,15 @@ +{ + "parent": "engine:iconItem", + "DisplayName": { + "name": "Empty Cup" + }, + "Item": { + "stackId": "MetalRenegades:emptyCup", + "icon" : "MetalRenegades:emptyCup", + "cooldownTime": 500, + "maxStackSize": 1 + }, + "WaterCup": { + "filled": false + } +} diff --git a/assets/prefabs/items/filledCup.prefab b/assets/prefabs/items/filledCup.prefab new file mode 100644 index 00000000..c57c5de1 --- /dev/null +++ b/assets/prefabs/items/filledCup.prefab @@ -0,0 +1,18 @@ +{ + "parent": "engine:iconItem", + "DisplayName": { + "name": "Filled Cup" + }, + "Item": { + "stackId": "MetalRenegades:filledCup", + "icon" : "MetalRenegades:filledCup", + "cooldownTime": 500, + "maxStackSize": 1 + }, + "Drink": { + "filling": 100 + }, + "WaterCup": { + "filled": true + } +} diff --git a/assets/prefabs/items/waterCup.prefab b/assets/prefabs/items/waterCup.prefab deleted file mode 100644 index 20ac91fa..00000000 --- a/assets/prefabs/items/waterCup.prefab +++ /dev/null @@ -1,15 +0,0 @@ -{ - "parent": "engine:iconItem", - "DisplayName": { - "name": "Water Cup" - }, - "Item": { - "stackId": "MetalRenegades:waterCup", - "icon" : "MetalRenegades:waterCup", - "consumedOnUse": true, - "cooldownTime": 500 - }, - "Drink": { - "filling": 50 - } -} diff --git a/assets/textures/items/emptyCup.png b/assets/textures/items/emptyCup.png new file mode 100644 index 00000000..ea638e08 Binary files /dev/null and b/assets/textures/items/emptyCup.png differ diff --git a/assets/textures/items/waterCup.png b/assets/textures/items/filledCup.png similarity index 100% rename from assets/textures/items/waterCup.png rename to assets/textures/items/filledCup.png diff --git a/deltas/engine/prefabs/player.prefab b/deltas/engine/prefabs/player.prefab index d46ee8fd..29eab88f 100644 --- a/deltas/engine/prefabs/player.prefab +++ b/deltas/engine/prefabs/player.prefab @@ -8,5 +8,8 @@ { "uri": "CoreAssets:pickaxe", "quantity": 1 }, { "uri": "CoreAssets:Torch", "quantity": 10 } ] + }, + "Thirst": { + "maxWaterCapacity": 200 } } diff --git a/src/main/java/org/terasology/metalrenegades/economy/ui/MarketItemBuilder.java b/src/main/java/org/terasology/metalrenegades/economy/ui/MarketItemBuilder.java index 6e16ac66..fc238e7d 100644 --- a/src/main/java/org/terasology/metalrenegades/economy/ui/MarketItemBuilder.java +++ b/src/main/java/org/terasology/metalrenegades/economy/ui/MarketItemBuilder.java @@ -30,6 +30,12 @@ public final class MarketItemBuilder { private static boolean isInitialised = false; private static void initialise() { + details.put("emptyCup", new MarketItem( + "emptyCup", + "A container to hold refreshing water from wells.", + 25 + )); + details.put("waffles", new MarketItem( "waffles", "Hot and fresh waffles.", diff --git a/src/main/java/org/terasology/metalrenegades/interaction/component/WaterCupComponent.java b/src/main/java/org/terasology/metalrenegades/interaction/component/WaterCupComponent.java new file mode 100644 index 00000000..7ac5c857 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/component/WaterCupComponent.java @@ -0,0 +1,17 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.component; + +import org.terasology.entitySystem.Component; + +/** + * Component describing the status of particular water cup. + */ +public class WaterCupComponent implements Component { + + /** + * True if the cup contains water, false otherwise. + */ + public boolean filled; + +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/component/WellBlockComponent.java b/src/main/java/org/terasology/metalrenegades/interaction/component/WellBlockComponent.java new file mode 100644 index 00000000..c4d07f7b --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/component/WellBlockComponent.java @@ -0,0 +1,12 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.component; + +import org.terasology.entitySystem.Component; + +/** + * Indicates a water source block inside of a well entity, which the player interacts with to gather water. + */ +public class WellBlockComponent implements Component { + +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/component/WellSourceComponent.java b/src/main/java/org/terasology/metalrenegades/interaction/component/WellSourceComponent.java index f4f7e8e1..0845b60f 100644 --- a/src/main/java/org/terasology/metalrenegades/interaction/component/WellSourceComponent.java +++ b/src/main/java/org/terasology/metalrenegades/interaction/component/WellSourceComponent.java @@ -1,25 +1,22 @@ -/* - * 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. - */ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.metalrenegades.interaction.component; import org.terasology.entitySystem.Component; /** - * Indicates a water cup source block, to be used inside wells. + * Indicates a water well entity, that the player can drink from. */ public class WellSourceComponent implements Component { + /** + * The number of water refills remaining in this well. Replenishes after a certain period of time. + */ + public int refillsLeft; + + /** + * The maximum number of refills that this well can have. + */ + public int capacity; + } diff --git a/src/main/java/org/terasology/metalrenegades/interaction/events/CupFilledEvent.java b/src/main/java/org/terasology/metalrenegades/interaction/events/CupFilledEvent.java new file mode 100644 index 00000000..33d7e2b1 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/events/CupFilledEvent.java @@ -0,0 +1,36 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.events; + +import org.terasology.entitySystem.entity.EntityRef; +import org.terasology.entitySystem.event.Event; + +/** + * Fires when a cup item is filled from a well. + */ +public class CupFilledEvent implements Event { + + /** + * The entity gathering the water. Assumed to be a player entity. + */ + private final EntityRef gatheringCharacter; + + /** + * The new cup item given to the player upon refilling. + */ + private final EntityRef cupItem; + + public CupFilledEvent(EntityRef gatheringCharacter, EntityRef cupItem) { + this.gatheringCharacter = gatheringCharacter; + this.cupItem = cupItem; + } + + public EntityRef getGatheringCharacter() { + return gatheringCharacter; + } + + public EntityRef getCupItem() { + return cupItem; + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/events/WellDrinkEvent.java b/src/main/java/org/terasology/metalrenegades/interaction/events/WellDrinkEvent.java new file mode 100644 index 00000000..275ec4b3 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/events/WellDrinkEvent.java @@ -0,0 +1,26 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.events; + +import org.terasology.entitySystem.entity.EntityRef; +import org.terasology.entitySystem.event.Event; + +/** + * Fires when a player drinks directly from a well. + */ +public class WellDrinkEvent implements Event { + + /** + * The entity drinking the water. Assumed to be a player entity. + */ + private final EntityRef gatheringCharacter; + + public WellDrinkEvent(EntityRef gatheringCharacter) { + this.gatheringCharacter = gatheringCharacter; + } + + public EntityRef getGatheringCharacter() { + return gatheringCharacter; + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/events/WellRefilledEvent.java b/src/main/java/org/terasology/metalrenegades/interaction/events/WellRefilledEvent.java new file mode 100644 index 00000000..3c80044e --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/events/WellRefilledEvent.java @@ -0,0 +1,11 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.events; + +import org.terasology.entitySystem.event.Event; + +/** + * Fires when a well gains one more refill. + */ +public class WellRefilledEvent implements Event { +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/systems/WellTooltipSystem.java b/src/main/java/org/terasology/metalrenegades/interaction/systems/WellTooltipSystem.java new file mode 100644 index 00000000..7247cec1 --- /dev/null +++ b/src/main/java/org/terasology/metalrenegades/interaction/systems/WellTooltipSystem.java @@ -0,0 +1,73 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.metalrenegades.interaction.systems; + +import org.terasology.entitySystem.entity.EntityManager; +import org.terasology.entitySystem.entity.EntityRef; +import org.terasology.entitySystem.event.ReceiveEvent; +import org.terasology.entitySystem.systems.BaseComponentSystem; +import org.terasology.entitySystem.systems.RegisterMode; +import org.terasology.entitySystem.systems.RegisterSystem; +import org.terasology.logic.nameTags.NameTagComponent; +import org.terasology.metalrenegades.interaction.component.WellSourceComponent; +import org.terasology.metalrenegades.interaction.events.CupFilledEvent; +import org.terasology.metalrenegades.interaction.events.WellDrinkEvent; +import org.terasology.metalrenegades.interaction.events.WellRefilledEvent; +import org.terasology.registry.In; +import org.terasology.rendering.nui.Color; +import org.terasology.world.time.WorldTimeEvent; + +/** + * Adds tooltips to wells about the number of thirst refills available. + */ +@RegisterSystem(value = RegisterMode.AUTHORITY) +public class WellTooltipSystem extends BaseComponentSystem { + + @In + private EntityManager entityManager; + + @ReceiveEvent + public void onWorldTimeEvent(WorldTimeEvent worldTimeEvent, EntityRef entityRef) { + for (EntityRef waterSource : entityManager.getEntitiesWith(WellSourceComponent.class)) { + if (waterSource.hasComponent(NameTagComponent.class)) { + continue; + } + + NameTagComponent nameTagComponent = new NameTagComponent(); + waterSource.addComponent(nameTagComponent); + updateNameTag(waterSource); + } + } + + @ReceiveEvent(components = {WellSourceComponent.class, NameTagComponent.class}) + public void onCupFilled(CupFilledEvent cupFilledEvent, EntityRef wellEntity) { + updateNameTag(wellEntity); + } + + @ReceiveEvent(components = {WellSourceComponent.class, NameTagComponent.class}) + public void onWellDrink(WellDrinkEvent wellDrinkEvent, EntityRef wellEntity) { + updateNameTag(wellEntity); + } + + @ReceiveEvent(components = {WellSourceComponent.class, NameTagComponent.class}) + public void onWellRefill(WellRefilledEvent wellRefilledEvent, EntityRef wellEntity) { + updateNameTag(wellEntity); + } + + /** + * Updates the name tag above a well entity to show the number of remaining water refills. + * + * @param wellEntity The well entity to update. + */ + private void updateNameTag(EntityRef wellEntity) { + WellSourceComponent wellSourceComponent = wellEntity.getComponent(WellSourceComponent.class); + NameTagComponent nameTagComponent = wellEntity.getComponent(NameTagComponent.class); + + nameTagComponent.text = String.format("Drinks: %d/%d", wellSourceComponent.refillsLeft, wellSourceComponent.capacity); + nameTagComponent.textColor = Color.BLUE; + nameTagComponent.yOffset = 2f; + nameTagComponent.scale = 1f; + wellEntity.saveComponent(nameTagComponent); + } + +} diff --git a/src/main/java/org/terasology/metalrenegades/interaction/systems/WellWaterSystem.java b/src/main/java/org/terasology/metalrenegades/interaction/systems/WellWaterSystem.java index 17d1b213..7209a82d 100644 --- a/src/main/java/org/terasology/metalrenegades/interaction/systems/WellWaterSystem.java +++ b/src/main/java/org/terasology/metalrenegades/interaction/systems/WellWaterSystem.java @@ -15,19 +15,36 @@ */ package org.terasology.metalrenegades.interaction.systems; +import org.terasology.dynamicCities.buildings.components.DynParcelRefComponent; +import org.terasology.engine.Time; import org.terasology.entitySystem.entity.EntityManager; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.entitySystem.event.ReceiveEvent; import org.terasology.entitySystem.systems.BaseComponentSystem; import org.terasology.entitySystem.systems.RegisterMode; import org.terasology.entitySystem.systems.RegisterSystem; +import org.terasology.logic.characters.CharacterHeldItemComponent; import org.terasology.logic.common.ActivateEvent; import org.terasology.logic.inventory.events.GiveItemEvent; +import org.terasology.logic.location.LocationComponent; +import org.terasology.math.geom.Rect2i; +import org.terasology.math.geom.Vector3i; +import org.terasology.metalrenegades.interaction.component.WaterCupComponent; +import org.terasology.metalrenegades.interaction.component.WellBlockComponent; import org.terasology.metalrenegades.interaction.component.WellSourceComponent; +import org.terasology.metalrenegades.interaction.events.CupFilledEvent; +import org.terasology.metalrenegades.interaction.events.WellDrinkEvent; +import org.terasology.metalrenegades.interaction.events.WellRefilledEvent; import org.terasology.registry.In; +import org.terasology.thirst.component.ThirstComponent; +import org.terasology.thirst.event.DrinkConsumedEvent; +import org.terasology.world.time.WorldTimeEvent; + +import java.util.stream.StreamSupport; /** - * Tracks water source blocks inside of wells; and gives water cups to players upon interaction. + * Tracks water source blocks inside of wells, fills player's water cups upon interaction, and empties + * water cups upon consumption. */ @RegisterSystem(RegisterMode.AUTHORITY) public class WellWaterSystem extends BaseComponentSystem { @@ -35,16 +52,168 @@ public class WellWaterSystem extends BaseComponentSystem { @In private EntityManager entityManager; - @ReceiveEvent(components = {WellSourceComponent.class}) - public void onActivate(ActivateEvent event, EntityRef target) { + @In + private Time time; + + /** + * The number of world time cycles it takes for all wells to replenish with one refill. + */ + private static final int CYCLES_UNTIL_REFILL = 20; + + private static final String EMPTY_CUP_URI = "MetalRenegades:emptyCup"; + + private static final String FULL_CUP_URI = "MetalRenegades:filledCup"; + + /** + * A timer counting the current number of cycles until all wells are replenished. + */ + private int worldTimeCycles = 0; + + @ReceiveEvent + public void onWorldTimeEvent(WorldTimeEvent worldTimeEvent, EntityRef entityRef) { + if (worldTimeCycles >= CYCLES_UNTIL_REFILL) { + refillWells(); + worldTimeCycles = 0; + } + + worldTimeCycles++; + } + + @ReceiveEvent(components = {WellBlockComponent.class}) + public void onActivate(ActivateEvent event, EntityRef sourceBlock) { EntityRef gatheringCharacter = event.getInstigator(); - EntityRef cupItem = entityManager.create("MetalRenegades:waterCup"); + EntityRef wellEntity = getWellEntity(sourceBlock); + if (!gatheringCharacter.exists() || wellEntity == EntityRef.NULL) { + return; + } + + WellSourceComponent wellSourceComp = wellEntity.getComponent(WellSourceComponent.class); + boolean refillAvailable = useRefill(wellSourceComp); + + if (refillAvailable) { + EntityRef heldItem = gatheringCharacter.getComponent(CharacterHeldItemComponent.class).selectedItem; + + if (heldItem.hasComponent(WaterCupComponent.class)) { + fillWaterCup(gatheringCharacter, heldItem, wellEntity); + } else { + directDrink(gatheringCharacter, wellEntity); + } + } + + wellEntity.saveComponent(wellSourceComp); + } + + @ReceiveEvent(components = {WaterCupComponent.class}) + public void onDrinkConsumed(DrinkConsumedEvent event, EntityRef item) { + EntityRef drinkingCharacter = event.getInstigator(); + EntityRef cupItem = entityManager.create(EMPTY_CUP_URI); - if(!cupItem.exists() || !gatheringCharacter.exists()) { + if (!cupItem.exists() || !drinkingCharacter.exists()) { return; } + item.destroy(); // Replace the full cup with an empty cup. + cupItem.send(new GiveItemEvent(drinkingCharacter)); + } + + /** + * Fills a player's held empty cup with water. This is done by destroying the old cup entity, and giving the + * player a new cup entity. + * + * @param gatheringCharacter The player entity that is collecting water. + * @param oldCup The old cup entity that the player is holding. + * @param wellEntity The well building entity that the water is supplied from. + */ + private void fillWaterCup(EntityRef gatheringCharacter, EntityRef oldCup, EntityRef wellEntity) { + EntityRef cupItem = entityManager.create(FULL_CUP_URI); + oldCup.destroy(); + cupItem.send(new GiveItemEvent(gatheringCharacter)); + wellEntity.send(new CupFilledEvent(gatheringCharacter, cupItem)); + } + + /** + * Replenishes a player's thirst bar back to full capacity. + * + * @param gatheringCharacter The player entity that is collecting water. + * @param wellEntity The well building entity that the water is supplied from. + */ + private void directDrink(EntityRef gatheringCharacter, EntityRef wellEntity) { + ThirstComponent thirst = gatheringCharacter.getComponent(ThirstComponent.class); + thirst.lastCalculatedWater = thirst.maxWaterCapacity; + thirst.lastCalculationTime = time.getGameTimeInMs(); + gatheringCharacter.saveComponent(thirst); + + wellEntity.send(new WellDrinkEvent(gatheringCharacter)); + } + + /** + * Replenish all wells in the game world with one refill. + */ + private void refillWells() { + for (EntityRef waterSource : entityManager.getEntitiesWith(WellSourceComponent.class)) { + WellSourceComponent wellSourceComp = waterSource.getComponent(WellSourceComponent.class); + if (addRefill(wellSourceComp)) { + waterSource.send(new WellRefilledEvent()); + } + } + } + + /** + * Retrieves the well entity associated with a well water source block. + * + * @param sourceBlock The activated source block. + * @return The well entity that contains this block. + */ + private EntityRef getWellEntity(EntityRef sourceBlock) { + LocationComponent blockLocComp = sourceBlock.getComponent(LocationComponent.class); + Vector3i blockLocation = new Vector3i(blockLocComp.getWorldPosition()); + + return StreamSupport.stream(entityManager.getEntitiesWith(WellSourceComponent.class).spliterator(), false) + .filter(wellEntity -> buildingContainsPosition(wellEntity, blockLocation)) + .findFirst() + .orElse(EntityRef.NULL); + } + + /** + * Checks if a building entity contains a particular world location. Used to determine which well entity + * a water source block belongs to. The building area is considered infinite in the y-axis, only x and z + * coordinates are checked. + * + * @param building The dynamic cities building entity to check. + * @param location The world position that will be checked. + * @return True if this building contains the location, false otherwise. + */ + private boolean buildingContainsPosition(EntityRef building, Vector3i location) { + DynParcelRefComponent dynParcelRefComponent = building.getComponent(DynParcelRefComponent.class); + Rect2i parcelRect = dynParcelRefComponent.dynParcel.getShape(); + return parcelRect.contains(location.x, location.z); + } + + /** + * Attempts to use one refill from this well. + * + * @return True if this is successful, false if this well is empty. + */ + public boolean useRefill(WellSourceComponent well) { + if (well.refillsLeft <= 0) { + return false; + } + well.refillsLeft--; + return true; + } + + /** + * Attempts to add one refill to this well. + * + * @return True if this is successful, false if this well is already full. + */ + public boolean addRefill(WellSourceComponent well) { + if (well.refillsLeft >= well.capacity) { + return false; + } + well.refillsLeft++; + return true; } }