diff --git a/assets/prefabs/gameEntity.prefab b/assets/prefabs/gameEntity.prefab index 7eb7ce75..0479eb5f 100644 --- a/assets/prefabs/gameEntity.prefab +++ b/assets/prefabs/gameEntity.prefab @@ -1,8 +1,13 @@ { "LASConfig" : { - "maxTeamSizeDifference" : 3 + "maxTeamSizeDifference": 3, + "minTeamSize": 1 }, "CurrencyStorage": { "amount": 100 + }, + // this component is required for the PhaseSystem phase change events to be broadcasted + "Network": { + "replicateMode": "ALWAYS" } } diff --git a/src/main/java/org/terasology/module/lightandshadow/LASUtils.java b/src/main/java/org/terasology/module/lightandshadow/LASUtils.java index 5affd5bd..1dc5a09d 100644 --- a/src/main/java/org/terasology/module/lightandshadow/LASUtils.java +++ b/src/main/java/org/terasology/module/lightandshadow/LASUtils.java @@ -13,6 +13,13 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.unicode.EnclosedAlphanumerics; +import org.terasology.engine.input.InputSystem; +import org.terasology.input.Input; +import org.terasology.nui.Color; +import org.terasology.nui.FontColor; + import static java.util.Collections.singletonMap; public final class LASUtils { @@ -222,4 +229,34 @@ public static String pixel(int x, int y, int r) { .findAny() .orElse("engine:air"); } + + /** + * Get a formatted representation of the primary {@link Input} associated with the given button binding. + * + * If the display name of the primary bound key is a single character this representation will be the encircled + * character. Otherwise the full display name is used. The bound key will be printed in yellow. + * + * If no key binding was found the text "n/a" in red color is returned. + * + * @param button the URI of a bindable button + * @return a formatted text to be used as representation for the player + */ + //TODO: put this in a common place? Duplicated in Dialogs, EventualSkills, and InGameHelp + public static String getActivationKey(InputSystem inputSystem, SimpleUri button) { + return inputSystem.getInputsForBindButton(button).stream() + .findFirst() + .map(Input::getDisplayName) + .map(key -> { + if (key.length() == 1) { + // print the key in yellow within a circle + int off = key.charAt(0) - 'A'; + char code = (char) (EnclosedAlphanumerics.CIRCLED_LATIN_CAPITAL_LETTER_A + off); + return String.valueOf(code); + } else { + return key; + } + }) + .map(key -> FontColor.getColored(key, Color.yellow)) + .orElse(FontColor.getColored("n/a", Color.red)); + } } diff --git a/src/main/java/org/terasology/module/lightandshadow/components/LASConfigComponent.java b/src/main/java/org/terasology/module/lightandshadow/components/LASConfigComponent.java index b2622a24..79d48d6d 100644 --- a/src/main/java/org/terasology/module/lightandshadow/components/LASConfigComponent.java +++ b/src/main/java/org/terasology/module/lightandshadow/components/LASConfigComponent.java @@ -6,9 +6,11 @@ public class LASConfigComponent implements Component { public int maxTeamSizeDifference; + public int minTeamSize; @Override public void copyFrom(LASConfigComponent other) { this.maxTeamSizeDifference = other.maxTeamSizeDifference; + this.minTeamSize = other.minTeamSize; } } diff --git a/src/main/java/org/terasology/module/lightandshadow/components/LASTeamStatsComponent.java b/src/main/java/org/terasology/module/lightandshadow/components/LASTeamStatsComponent.java new file mode 100644 index 00000000..34d31017 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/components/LASTeamStatsComponent.java @@ -0,0 +1,20 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.components; + +import org.terasology.gestalt.entitysystem.component.Component; + +public class LASTeamStatsComponent implements Component { + public int redTeamSize; + public int blackTeamSize; + public int whiteTeamSize; + + @Override + public void copyFrom(LASTeamStatsComponent other) { + this.blackTeamSize = other.blackTeamSize; + this.redTeamSize = other.redTeamSize; + this.whiteTeamSize = other.whiteTeamSize; + } + +} diff --git a/src/main/java/org/terasology/module/lightandshadow/events/ActivateBarrierEvent.java b/src/main/java/org/terasology/module/lightandshadow/events/ActivateBarrierEvent.java deleted file mode 100644 index 999570ff..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/events/ActivateBarrierEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.events; - -import org.terasology.gestalt.entitysystem.event.Event; - -/** - * Trigger event to activate the pregame barriers. - */ -public class ActivateBarrierEvent implements Event { -} diff --git a/src/main/java/org/terasology/module/lightandshadow/events/DelayedDeactivateBarrierEvent.java b/src/main/java/org/terasology/module/lightandshadow/events/DelayedDeactivateBarrierEvent.java deleted file mode 100644 index 3f504234..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/events/DelayedDeactivateBarrierEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.events; - -import org.terasology.engine.network.ServerEvent; -import org.terasology.gestalt.entitysystem.event.Event; - -/** - * Trigger event to deactivate the pregame barriers. - */ -@ServerEvent -public class DelayedDeactivateBarrierEvent implements Event { - private int delay; - - public DelayedDeactivateBarrierEvent(int delay) { - this.delay = delay; - } - - public int getDelay() { - return delay; - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/events/GameStartMessageEvent.java b/src/main/java/org/terasology/module/lightandshadow/events/GameStartMessageEvent.java deleted file mode 100644 index ef543514..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/events/GameStartMessageEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.events; - -import org.terasology.engine.network.OwnerEvent; -import org.terasology.gestalt.entitysystem.event.Event; - -/** - * Notification event that indicates the pregame phase has started so that the pregame message can be displayed. - */ -@OwnerEvent -public class GameStartMessageEvent implements Event { -} diff --git a/src/main/java/org/terasology/module/lightandshadow/events/PlayerExitedArenaEvent.java b/src/main/java/org/terasology/module/lightandshadow/events/PlayerExitedArenaEvent.java new file mode 100644 index 00000000..bfa70896 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/events/PlayerExitedArenaEvent.java @@ -0,0 +1,19 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.events; + +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.gestalt.entitysystem.event.Event; + +public class PlayerExitedArenaEvent implements Event { + private final EntityRef player; + + public PlayerExitedArenaEvent(EntityRef player) { + this.player = player; + } + + public EntityRef getPlayer() { + return player; + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/events/PregameEvent.java b/src/main/java/org/terasology/module/lightandshadow/events/PregameEvent.java deleted file mode 100644 index 9be90421..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/events/PregameEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.events; - -import org.terasology.gestalt.entitysystem.event.Event; - -/** - * Notification event that indicates the pregame phase has started. - */ -public class PregameEvent implements Event { -} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseEndedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseEndedEvent.java new file mode 100644 index 00000000..88b0fc7f --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseEndedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnCountdownPhaseEndedEvent implements Event { + + public OnCountdownPhaseEndedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseStartedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseStartedEvent.java new file mode 100644 index 00000000..4cab7196 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnCountdownPhaseStartedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnCountdownPhaseStartedEvent implements Event { + + public OnCountdownPhaseStartedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseEndedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseEndedEvent.java new file mode 100644 index 00000000..e1ff344e --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseEndedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnIdlePhaseEndedEvent implements Event { + + public OnIdlePhaseEndedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseStartedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseStartedEvent.java new file mode 100644 index 00000000..e847a0e0 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnIdlePhaseStartedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnIdlePhaseStartedEvent implements Event { + + public OnIdlePhaseStartedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseEndedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseEndedEvent.java new file mode 100644 index 00000000..dff174cf --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseEndedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnInGamePhaseEndedEvent implements Event { + + public OnInGamePhaseEndedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseStartedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseStartedEvent.java new file mode 100644 index 00000000..5474ad52 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnInGamePhaseStartedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnInGamePhaseStartedEvent implements Event { + + public OnInGamePhaseStartedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseEndedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseEndedEvent.java new file mode 100644 index 00000000..94f31caa --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseEndedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnPostGamePhaseEndedEvent implements Event { + + public OnPostGamePhaseEndedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseStartedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseStartedEvent.java new file mode 100644 index 00000000..e060411a --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnPostGamePhaseStartedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnPostGamePhaseStartedEvent implements Event { + + public OnPostGamePhaseStartedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseEndedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseEndedEvent.java new file mode 100644 index 00000000..c0bca413 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseEndedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnPreGamePhaseEndedEvent implements Event { + + public OnPreGamePhaseEndedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseStartedEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseStartedEvent.java new file mode 100644 index 00000000..4c939c02 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/OnPreGamePhaseStartedEvent.java @@ -0,0 +1,16 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.network.BroadcastEvent; +import org.terasology.gestalt.entitysystem.event.Event; + +// TODO: Would it make more sense to have a single "OnPhaseTransition" with old and new phase here? + +@BroadcastEvent +public class OnPreGamePhaseStartedEvent implements Event { + + public OnPreGamePhaseStartedEvent() { + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/Phase.java b/src/main/java/org/terasology/module/lightandshadow/phases/Phase.java new file mode 100644 index 00000000..0a981c46 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/Phase.java @@ -0,0 +1,24 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.gestalt.entitysystem.event.Event; + +import java.util.function.Supplier; + +public enum Phase { + IDLE(OnIdlePhaseStartedEvent::new, OnIdlePhaseEndedEvent::new), + PRE_GAME(OnPreGamePhaseStartedEvent::new, OnPreGamePhaseEndedEvent::new), + COUNTDOWN(OnCountdownPhaseStartedEvent::new, OnCountdownPhaseEndedEvent::new), + IN_GAME(OnInGamePhaseStartedEvent::new, OnInGamePhaseEndedEvent::new), + POST_GAME(OnPostGamePhaseStartedEvent::new, OnPostGamePhaseEndedEvent::new); + + public final Supplier endEvent; + public final Supplier startEvent; + + Phase(Supplier startEvent, Supplier endEvent) { + this.startEvent = startEvent; + this.endEvent = endEvent; + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapter.java b/src/main/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapter.java new file mode 100644 index 00000000..44194085 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapter.java @@ -0,0 +1,36 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.engine.logic.console.commandSystem.adapter.ParameterAdapter; +import org.terasology.engine.logic.console.commandSystem.adapter.ParameterAdapterManager; + +/** + * Adapter to allow direct parsing of {@link Phase} arguments in the in-game console. + *

+ * If this adapter is registered with the {@link ParameterAdapterManager}, a command can accept a {@link Phase} as input parameter without + * the need to manually parse it. + *

{@code
+ *      public String myPhaseCommand(
+ *             @Sender EntityRef client,
+ *             @CommandParam("phase") Phase phase) { ... }
+ * }
+ * + * This adapter can parse exact matches that conform to {@link Phase#valueOf(String)}. + * It is case-insensitive, e.g., it compensates lower-case characters. + */ +public class PhaseParameterAdapter implements ParameterAdapter { + + @Override + public Phase parse(String raw) { + // TODO: Make the user aware of the valid values. Needs some adjustments in CommandParameter. + // "Unknown phase ''. Valid values are: " + return Phase.valueOf(raw.toUpperCase()); + } + + @Override + public String convertToString(Phase value) { + return value.toString(); + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/SwitchToPhaseEvent.java b/src/main/java/org/terasology/module/lightandshadow/phases/SwitchToPhaseEvent.java new file mode 100644 index 00000000..639994c9 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/SwitchToPhaseEvent.java @@ -0,0 +1,16 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.terasology.gestalt.entitysystem.event.Event; + +public class SwitchToPhaseEvent implements Event { + public Phase targetPhase; + + public SwitchToPhaseEvent() { } + + public SwitchToPhaseEvent(Phase targetPhase) { + this.targetPhase = targetPhase; + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystem.java b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystem.java new file mode 100644 index 00000000..28975541 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystem.java @@ -0,0 +1,70 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases.authority; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.logic.console.commandSystem.adapter.ParameterAdapterManager; +import org.terasology.engine.logic.console.commandSystem.annotations.Command; +import org.terasology.engine.logic.console.commandSystem.annotations.CommandParam; +import org.terasology.engine.logic.console.commandSystem.annotations.Sender; +import org.terasology.engine.logic.permission.PermissionManager; +import org.terasology.engine.registry.In; +import org.terasology.module.inventory.components.ItemCommands; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.PhaseParameterAdapter; + +@RegisterSystem(RegisterMode.ALWAYS) +public class PhaseCommandsSystem extends BaseComponentSystem { + + private static final Logger logger = LoggerFactory.getLogger(ItemCommands.class); + + @In + PhaseSystem phaseSystem; + + @In + ParameterAdapterManager parameterAdapterManager; + + @Override + public void initialise() { + // globally register the adapter for `Phase` values. Required for `forcePhase` command. + parameterAdapterManager.registerAdapter(Phase.class, new PhaseParameterAdapter()); + } + + @Command(shortDescription = "Forces a phase transition to a specified target phase", + helpText = "Puts the Light & Shadow World into a desired phase", + runOnServer = true, + requiredPermission = PermissionManager.CHEAT_PERMISSION) + public String forcePhase( + @Sender EntityRef client, + @CommandParam("targetPhase") Phase targetPhase) { + + switch (targetPhase) { + case IDLE: + case PRE_GAME: + case COUNTDOWN: + case IN_GAME: + case POST_GAME: + break; + default: + // this case can only occur if we add a new phase and (deliberately or not) don't add it here. + // TODO(skaldarnar): I'd like to remove this altogether, as I think this is just maintenance overhead and a potential + // pitfall in the case we're adding a new phase. Less code is better code. + StringBuilder errorMsg = new StringBuilder(targetPhase + " is not a valid phase, please choose one of the following:"); + for (Phase validPhase : Phase.values()) { + errorMsg.append(" ").append(validPhase.toString()); + } + logger.error(errorMsg.toString()); + return errorMsg.toString(); + } + + Phase currentPhase = phaseSystem.getCurrentPhase(); + phaseSystem.transitionPhase(currentPhase, targetPhase); + return "Initiated phase transition from " + currentPhase + " to " + targetPhase; + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseComponent.java b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseComponent.java new file mode 100644 index 00000000..75ed839f --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseComponent.java @@ -0,0 +1,36 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases.authority; + +import org.terasology.gestalt.entitysystem.component.Component; +import org.terasology.module.lightandshadow.phases.Phase; + +/** + * Read-only component holding the current state of a Light and Shadow game. + */ +public class PhaseComponent implements Component { + + private Phase currentPhase; + + PhaseComponent() { + this.currentPhase = Phase.IDLE; + } + + public Phase getCurrentPhase() { + return currentPhase; + } + + /** + * Package-private setter to only allow the authority system to update the game state. + * @param newPhase the new active game phase + */ + void setCurrentPhase(Phase newPhase) { + this.currentPhase = newPhase; + } + + @Override + public void copyFrom(PhaseComponent other) { + this.currentPhase = other.currentPhase; + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseSystem.java b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseSystem.java new file mode 100644 index 00000000..592cf0f2 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/phases/authority/PhaseSystem.java @@ -0,0 +1,81 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases.authority; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.registry.In; +import org.terasology.engine.registry.Share; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import org.terasology.module.lightandshadow.phases.OnIdlePhaseEndedEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; +import org.terasology.module.lightandshadow.systems.GameEntitySystem; + +/** + * Provides an entity that keeps track of game state information. + */ +@RegisterSystem(RegisterMode.AUTHORITY) +@Share(value = PhaseSystem.class) +public class PhaseSystem extends BaseComponentSystem { + + private static final Logger logger = LoggerFactory.getLogger(PhaseSystem.class); + + @In + GameEntitySystem gameEntitySystem; + + @Override + public void postBegin() { + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + gameEntity.addOrSaveComponent(new PhaseComponent()); + logger.info("hello dog"); + logger.debug("Initializing PhaseSystem with game entity:\n{}", gameEntity.toFullDescription()); + } + + // TODO: What happens here if there's multiple components present that implement PhaseComponent? + public Phase getCurrentPhase() { + return gameEntitySystem.getGameEntity().getComponent(PhaseComponent.class).getCurrentPhase(); + } + + void transitionPhase(Phase from, Phase to) { + logger.debug("Transitioning phases from " + from + " to " + to); + endPhase(from); + startPhase(to); + } + + @ReceiveEvent(components = PhaseComponent.class) + public void switchToPhase(SwitchToPhaseEvent event, EntityRef entity) { + transitionPhase(getCurrentPhase(), event.targetPhase); + } + + private void assertInPhase(Phase expectedPhase, EntityRef gameEntity) { + PhaseComponent phase = gameEntity.getComponent(PhaseComponent.class); + + if (phase == null || phase.getCurrentPhase() != expectedPhase) { + String message = "LightAndShadow/PhaseSystem: Attempting to end idle phase without being in `" + expectedPhase + "` phase."; + String entityDump = gameEntity.toFullDescription(); + throw new RuntimeException(message + "\n" + entityDump); + } + gameEntity.send(new OnIdlePhaseEndedEvent()); + } + + private void endPhase(Phase endingPhase) { + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + assertInPhase(endingPhase, gameEntity); + gameEntity.send(endingPhase.endEvent.get()); + } + + private void startPhase(Phase startingPhase) { + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + gameEntity.updateComponent(PhaseComponent.class, phaseComponent -> { + phaseComponent.setCurrentPhase(startingPhase); + return phaseComponent; + }); + gameEntity.send(startingPhase.startEvent.get()); + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/AttackSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/AttackSystem.java index db27040e..d32dba82 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/AttackSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/AttackSystem.java @@ -5,6 +5,8 @@ import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.event.EventPriority; +import org.terasology.engine.entitySystem.event.Priority; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; import org.terasology.engine.entitySystem.systems.RegisterMode; import org.terasology.engine.entitySystem.systems.RegisterSystem; @@ -17,13 +19,17 @@ import org.terasology.gestalt.entitysystem.event.ReceiveEvent; import org.terasology.lightandshadowresources.components.FlagComponent; import org.terasology.lightandshadowresources.components.LASTeamComponent; +import org.terasology.module.health.events.BeforeDamagedEvent; +import org.terasology.module.inventory.events.InventorySlotChangedEvent; import org.terasology.module.lightandshadow.components.FlagDropOnActivateComponent; import org.terasology.module.lightandshadow.components.HasFlagComponent; +import org.terasology.module.lightandshadow.components.InvulnerableComponent; import org.terasology.module.lightandshadow.events.DropFlagEvent; import org.terasology.module.lightandshadow.events.OnFlagDropEvent; import org.terasology.module.lightandshadow.events.OnFlagPickupEvent; import org.terasology.module.lightandshadow.events.ReturnFlagEvent; -import org.terasology.module.inventory.events.InventorySlotChangedEvent; +import org.terasology.module.lightandshadow.phases.OnInGamePhaseStartedEvent; +import org.terasology.module.lightandshadow.phases.OnPreGamePhaseStartedEvent; @RegisterSystem(RegisterMode.AUTHORITY) public class AttackSystem extends BaseComponentSystem { @@ -71,6 +77,22 @@ private void dropFlagOnPlayerAttack(ActivateEvent event, EntityRef targetPlayer) } } + @ReceiveEvent + public void onPregameStart(OnPreGamePhaseStartedEvent event, EntityRef entity) { + entity.addComponent(new InvulnerableComponent()); + } + + @ReceiveEvent + public void onGameStart(OnInGamePhaseStartedEvent event, EntityRef entity) { + entity.removeComponent(InvulnerableComponent.class); + } + + @Priority(EventPriority.PRIORITY_HIGH) + @ReceiveEvent(components = InvulnerableComponent.class) + public void preventFriendlyFire(BeforeDamagedEvent event, EntityRef entity) { + event.consume(); + } + /** * Checks if player picks up flag of the same team. * If so, moves flag back to base, otherwise adds particle emitter and HasFlagComponent to player diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientGameOverSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientGameOverSystem.java deleted file mode 100644 index 8a67486e..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ClientGameOverSystem.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.engine.entitySystem.entity.EntityManager; -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.logic.players.LocalPlayer; -import org.terasology.engine.logic.players.PlayerCharacterComponent; -import org.terasology.engine.logic.players.PlayerUtil; -import org.terasology.engine.network.ClientComponent; -import org.terasology.engine.registry.In; -import org.terasology.engine.registry.Share; -import org.terasology.engine.rendering.nui.NUIManager; -import org.terasology.engine.rendering.nui.layers.ingame.DeathScreen; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.lightandshadowresources.components.LASTeamComponent; -import org.terasology.module.lightandshadow.LASUtils; -import org.terasology.module.lightandshadow.components.PlayerStatisticsComponent; -import org.terasology.module.lightandshadow.events.GameOverEvent; -import org.terasology.module.lightandshadow.events.RestartRequestEvent; -import org.terasology.nui.WidgetUtil; -import org.terasology.nui.layouts.miglayout.MigLayout; -import org.terasology.nui.widgets.UIButton; -import org.terasology.nui.widgets.UILabel; - -import java.util.Timer; -import java.util.TimerTask; - -/** - * Displays game over screen for all clients. - */ -@RegisterSystem(RegisterMode.CLIENT) -@Share(value = ClientGameOverSystem.class) -public class ClientGameOverSystem extends BaseComponentSystem { - - private static Timer timer; - - private DeathScreen deathScreen; - - @In - private NUIManager nuiManager; - @In - private LocalPlayer localPlayer; - @In - private EntityManager entityManager; - - @Override - public void initialise() { - deathScreen = nuiManager.createScreen(LASUtils.DEATH_SCREEN, DeathScreen.class); - } - - /** - * System to show game over screen once a team achieves goal score. - * - * @param event the GameOverEvent event which stores the winning team and the final scores of both teams. - * @param entity the entity about each player connected to the game. TODO: needs more details/clarification - */ - @ReceiveEvent - public void onGameOver(GameOverEvent event, EntityRef entity) { - if (localPlayer.getClientEntity().equals(entity)) { - nuiManager.removeOverlay(LASUtils.ONLINE_PLAYERS_OVERLAY); - nuiManager.pushScreen(deathScreen); - addPlayerStatisticsInfo(deathScreen); - addFlagInfo(deathScreen, event); - UILabel gameOverResult = deathScreen.find("gameOverResult", UILabel.class); - - UIButton restartButton = deathScreen.find("restart", UIButton.class); - UILabel countDown = deathScreen.find("timer", UILabel.class); - if (restartButton != null && countDown != null) { - timer = new Timer(); - timer.scheduleAtFixedRate(new TimerTask() { - int timePeriod = 10; - public void run() { - countDown.setText(Integer.toString(timePeriod--)); - if (timePeriod < 0) { - countDown.setText(" "); - restartButton.setEnabled(true); - timer.cancel(); - } - } - }, 0, 1000); - restartButton.setVisible(true); - restartButton.setEnabled(false); - } - - WidgetUtil.trySubscribe(deathScreen, "restart", widget -> triggerRestart()); - if (gameOverResult != null) { - if (event.winningTeam.equals(localPlayer.getCharacterEntity().getComponent(LASTeamComponent.class).team)) { - gameOverResult.setText("Victory"); - gameOverResult.setFamily("win"); - } else { - gameOverResult.setText("Defeat"); - gameOverResult.setFamily("lose"); - } - } - } - } - - public void addPlayerStatisticsInfo(DeathScreen deathScreen) { - MigLayout spadesTeamMigLayout = deathScreen.find("spadesTeamPlayerStatistics", MigLayout.class); - MigLayout heartsTeamMigLayout = deathScreen.find("heartsTeamPlayerStatistics", MigLayout.class); - spadesTeamMigLayout.removeAllWidgets(); - heartsTeamMigLayout.removeAllWidgets(); - if (spadesTeamMigLayout != null && heartsTeamMigLayout != null) { - Iterable characters = entityManager.getEntitiesWith(PlayerCharacterComponent.class, - LASTeamComponent.class); - - for (EntityRef character : characters) { - EntityRef client = character.getOwner(); - ClientComponent clientComponent = client.getComponent(ClientComponent.class); - String playerTeam = character.getComponent(LASTeamComponent.class).team; - PlayerStatisticsComponent playerStatisticsComponent = - character.getComponent(PlayerStatisticsComponent.class); - MigLayout migLayout = (playerTeam.equals("black") ? spadesTeamMigLayout : heartsTeamMigLayout); - if (!playerTeam.equals("white")) { - addInfoToTeamMigLayout(migLayout, clientComponent, playerStatisticsComponent); - } - } - } - } - - private void addInfoToTeamMigLayout(MigLayout migLayout, ClientComponent clientComponent, - PlayerStatisticsComponent playerStatisticsComponent) { - migLayout.addWidget(new UILabel(PlayerUtil.getColoredPlayerName(clientComponent.clientInfo)), - new MigLayout.CCHint()); - migLayout.addWidget(new UILabel(String.valueOf(playerStatisticsComponent.kills)), new MigLayout.CCHint()); - migLayout.addWidget(new UILabel(String.valueOf(playerStatisticsComponent.deaths)), new MigLayout.CCHint("wrap" - )); - playerStatisticsComponent.kills = 0; - playerStatisticsComponent.deaths = 0; - } - - private void addFlagInfo(DeathScreen deathScreen, GameOverEvent event) { - addTeamScore(deathScreen, "spadesTeamScore", event.blackTeamScore); - addTeamScore(deathScreen, "heartsTeamScore", event.redTeamScore); - addGoalScore(deathScreen, "spadesGoalScore"); - addGoalScore(deathScreen, "heartsGoalScore"); - } - - public void addTeamScore(DeathScreen deathScreen, String teamUILabelId, int finalScore) { - UILabel teamScore = deathScreen.find(teamUILabelId, UILabel.class); - teamScore.setText(String.valueOf(finalScore)); - } - - public void addGoalScore(DeathScreen deathScreen, String teamUILabelID) { - UILabel goalScore = deathScreen.find(teamUILabelID, UILabel.class); - goalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); - } - - private void triggerRestart() { - localPlayer.getClientEntity().send(new RestartRequestEvent()); - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientInformationSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientInformationSystem.java new file mode 100644 index 00000000..150b786c --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/systems/ClientInformationSystem.java @@ -0,0 +1,352 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.module.lightandshadow.systems; + +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.event.EventPriority; +import org.terasology.engine.entitySystem.event.Priority; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.input.InputSystem; +import org.terasology.engine.logic.players.LocalPlayer; +import org.terasology.engine.logic.players.PlayerCharacterComponent; +import org.terasology.engine.logic.players.PlayerUtil; +import org.terasology.engine.logic.players.event.LocalPlayerInitializedEvent; +import org.terasology.engine.logic.players.event.OnPlayerSpawnedEvent; +import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.registry.In; +import org.terasology.engine.rendering.nui.NUIManager; +import org.terasology.engine.rendering.nui.layers.ingame.DeathScreen; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import org.terasology.lightandshadowresources.components.LASTeamComponent; +import org.terasology.module.lightandshadow.LASUtils; +import org.terasology.module.lightandshadow.TapButton; +import org.terasology.module.lightandshadow.components.LASConfigComponent; +import org.terasology.module.lightandshadow.components.PlayerStatisticsComponent; +import org.terasology.module.lightandshadow.events.ClientRestartEvent; +import org.terasology.module.lightandshadow.events.GameOverEvent; +import org.terasology.module.lightandshadow.events.RestartRequestEvent; +import org.terasology.module.lightandshadow.events.ScoreUpdateFromServerEvent; +import org.terasology.module.lightandshadow.events.TimerEvent; +import org.terasology.module.lightandshadow.phases.OnPreGamePhaseEndedEvent; +import org.terasology.module.lightandshadow.phases.OnPreGamePhaseStartedEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; +import org.terasology.module.lightandshadow.phases.authority.PhaseSystem; +import org.terasology.notifications.events.ExpireNotificationEvent; +import org.terasology.notifications.events.ShowNotificationEvent; +import org.terasology.notifications.model.Notification; +import org.terasology.notify.ui.DialogNotificationOverlay; +import org.terasology.nui.ControlWidget; +import org.terasology.nui.UILayout; +import org.terasology.nui.WidgetUtil; +import org.terasology.nui.databinding.ReadOnlyBinding; +import org.terasology.nui.layouts.miglayout.MigLayout; +import org.terasology.nui.widgets.UIButton; +import org.terasology.nui.widgets.UILabel; + +import java.util.Timer; +import java.util.TimerTask; + +@RegisterSystem(RegisterMode.CLIENT) +public class ClientInformationSystem extends BaseComponentSystem { + private static final String WAIT_NOTIFICATION_ID = "LightAndShadow:waitForPlayers"; + private static final String STATS_NOTIFICATION_ID = "LightAndShadow:firstTimeStatistics"; + + public static final ResourceUrn ASSET_URI = new ResourceUrn("LightAndShadow:Timer"); + + private static final int COUNTDOWN_IN_SECONDS = 30; + + private static Timer timer; + + @In + InputSystem inputSystem; + + @In + private EntityManager entityManager; + @In + private NUIManager nuiManager; + @In + private GameEntitySystem gameEntitySystem; + @In + private PhaseSystem phaseSystem; + + @In + private LocalPlayer localPlayer; + + private DeathScreen statisticsScreen; + private DeathScreen deathScreen; + private int redScore = 0; + private int blackScore = 0; + private boolean statsScreenIsOpen; + private boolean isExpired; + + private DialogNotificationOverlay window; + + @Override + public void postBegin() { + // Sets score screen bindings + ControlWidget scoreScreen = nuiManager.getHUD().getHUDElement("LightAndShadow:ScoreHud"); + UILabel blackScoreArea = scoreScreen.find("blackScoreArea", UILabel.class); + blackScoreArea.bindText(new ReadOnlyBinding() { + @Override + public String get() { + return String.valueOf(blackScore); + } + }); + UILabel redScoreArea = scoreScreen.find("redScoreArea", UILabel.class); + redScoreArea.bindText(new ReadOnlyBinding() { + @Override + public String get() { + return String.valueOf(redScore); + } + }); + UILabel blackGoalScore = scoreScreen.find("blackGoalScore", UILabel.class); + blackGoalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); + UILabel redGoalScore = scoreScreen.find("redGoalScore", UILabel.class); + redGoalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); + } + + @Override + public void initialise() { + window = nuiManager.addOverlay(ASSET_URI, DialogNotificationOverlay.class); + nuiManager.getHUD().addHUDElement("ScoreHud"); + deathScreen = nuiManager.createScreen(LASUtils.DEATH_SCREEN, DeathScreen.class); + statisticsScreen = nuiManager.createScreen("LightAndShadow:statisticsScreen", DeathScreen.class); + addGoalScore(statisticsScreen, "spadesGoalScore"); + addGoalScore(statisticsScreen, "heartsGoalScore"); + addTeamScore(statisticsScreen, "heartsTeamScore", redScore); + addTeamScore(statisticsScreen, "spadesTeamScore", blackScore); + } + + @Override + public void shutdown() { + nuiManager.closeScreen(window); + } + + @ReceiveEvent + public void onPregameStart(OnPreGamePhaseStartedEvent event, EntityRef entity) { + notifyOnTooFewPlayers(); + } + + @ReceiveEvent + public void onSpawnDuringPregame(OnPlayerSpawnedEvent event, EntityRef entity) { + if (phaseSystem.getCurrentPhase() == Phase.PRE_GAME) { + notifyOnTooFewPlayers(); + } + } + + @ReceiveEvent + public void onPregameEnd(OnPreGamePhaseEndedEvent event, EntityRef entity) { + localPlayer.getClientEntity().send(new ExpireNotificationEvent(WAIT_NOTIFICATION_ID)); + } + + @ReceiveEvent + public void onScoreUpdate(ScoreUpdateFromServerEvent event, EntityRef entity) { + String team = event.team; + if (team.equals(LASUtils.RED_TEAM)) { + redScore = event.score; + } + if (team.equals(LASUtils.BLACK_TEAM)) { + blackScore = event.score; + } + } + + @Priority(EventPriority.PRIORITY_CRITICAL) + @ReceiveEvent + public void onTab(TapButton event, EntityRef entity) { + if (event.isDown()) { + if (localPlayer.getClientEntity().equals(entity) && !statsScreenIsOpen) { + if (!isExpired) { + entity.send(new ExpireNotificationEvent(STATS_NOTIFICATION_ID)); + isExpired = true; + } + addTeamScore(statisticsScreen, "spadesTeamScore", blackScore); + addTeamScore(statisticsScreen, "heartsTeamScore", redScore); + addPlayerStatisticsInfo(statisticsScreen); + nuiManager.pushScreen("LightAndShadow:statisticsScreen"); + statsScreenIsOpen = true; + event.consume(); + } + } else { + statsScreenIsOpen = false; + nuiManager.closeScreen("LightAndShadow:statisticsScreen"); + } + } + + @ReceiveEvent + public void onLocalPlayerInitialized(LocalPlayerInitializedEvent event, EntityRef entity) { + Notification notification = new Notification(STATS_NOTIFICATION_ID, + "The Numbers Game", + "Hold " + LASUtils.getActivationKey(inputSystem, new SimpleUri("LightAndShadow:statistics")) + " to see statistics", + "engine:items#blueBook"); + localPlayer.getClientEntity().send(new ShowNotificationEvent(notification)); + } + + /** + * System to show game over screen once a team achieves goal score. + * + * @param event the GameOverEvent event which stores the winning team and the final scores of both teams. + * @param entity the entity about each player connected to the game. TODO: needs more details/clarification + */ + @ReceiveEvent + public void onGameOver(GameOverEvent event, EntityRef entity) { + if (localPlayer.getClientEntity().equals(entity)) { + nuiManager.removeOverlay(LASUtils.ONLINE_PLAYERS_OVERLAY); + nuiManager.pushScreen(deathScreen); + addPlayerStatisticsInfo(deathScreen); + addFlagInfo(deathScreen, event); + UILabel gameOverResult = deathScreen.find("gameOverResult", UILabel.class); + + UIButton restartButton = deathScreen.find("restart", UIButton.class); + UILabel countDown = deathScreen.find("timer", UILabel.class); + if (restartButton != null && countDown != null) { + timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask() { + int timePeriod = 10; + public void run() { + countDown.setText(Integer.toString(timePeriod--)); + if (timePeriod < 0) { + countDown.setText(" "); + restartButton.setEnabled(true); + timer.cancel(); + } + } + }, 0, 1000); + restartButton.setVisible(true); + restartButton.setEnabled(false); + } + + WidgetUtil.trySubscribe(deathScreen, "restart", widget -> triggerRestart()); + if (gameOverResult != null) { + if (event.winningTeam.equals(localPlayer.getCharacterEntity().getComponent(LASTeamComponent.class).team)) { + gameOverResult.setText("Victory"); + gameOverResult.setFamily("win"); + } else { + gameOverResult.setText("Defeat"); + gameOverResult.setFamily("lose"); + } + } + } + } + + @ReceiveEvent + public void onClientRestart(ClientRestartEvent event, EntityRef clientEntity) { + if (localPlayer.getClientEntity().equals(clientEntity)) { + if (nuiManager.isOpen(LASUtils.DEATH_SCREEN)) { + DeathScreen deathScreen = (DeathScreen) nuiManager.getScreen(LASUtils.DEATH_SCREEN); + UILayout migLayout = deathScreen.find("playerStatistics", UILayout.class); + if (migLayout != null) { + migLayout.removeAllWidgets(); + } + nuiManager.closeScreen(LASUtils.DEATH_SCREEN); + } + } + } + + @ReceiveEvent + public void updateTimer(TimerEvent event, EntityRef entity) { + if (localPlayer.getClientEntity().equals(entity)) { + timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask() { + int timePeriod = COUNTDOWN_IN_SECONDS; + boolean addNotification; + public void run() { + if (phaseSystem.getCurrentPhase() != Phase.COUNTDOWN) { + if (!addNotification) { + window.removeNotification("The game starts in " + (timePeriod + 1) + " seconds."); + } + timer.cancel(); + } + if (addNotification) { + entity.send(new ExpireNotificationEvent(WAIT_NOTIFICATION_ID)); + window.addNotification("The game starts in " + timePeriod-- + " seconds."); + addNotification = false; + } else { + entity.send(new ExpireNotificationEvent(WAIT_NOTIFICATION_ID)); + window.removeNotification("The game starts in " + (timePeriod + 1) + " seconds."); + addNotification = true; + } + if (timePeriod < 0) { + window.removeNotification("The game starts in " + (timePeriod + 1) + " seconds."); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.IN_GAME)); + timer.cancel(); + } + } + }, 0, 500); + } + } + + public void notifyOnTooFewPlayers() { + Notification notification = new Notification(WAIT_NOTIFICATION_ID, + "Too Few Players", + "Each team needs at least " + + gameEntitySystem.getGameEntity().getComponent(LASConfigComponent.class).minTeamSize + " player(s)", + "engine:icons#halfGreenHeart"); + localPlayer.getClientEntity().send(new ShowNotificationEvent(notification)); + } + + @ReceiveEvent + public void removeTooFewPlayersNotification(OnPreGamePhaseEndedEvent event, EntityRef entity) { + entity.send(new ExpireNotificationEvent(WAIT_NOTIFICATION_ID)); + } + + public void addPlayerStatisticsInfo(DeathScreen deathScreen) { + MigLayout spadesTeamMigLayout = deathScreen.find("spadesTeamPlayerStatistics", MigLayout.class); + MigLayout heartsTeamMigLayout = deathScreen.find("heartsTeamPlayerStatistics", MigLayout.class); + spadesTeamMigLayout.removeAllWidgets(); + heartsTeamMigLayout.removeAllWidgets(); + if (spadesTeamMigLayout != null && heartsTeamMigLayout != null) { + Iterable characters = entityManager.getEntitiesWith(PlayerCharacterComponent.class, + LASTeamComponent.class); + + for (EntityRef character : characters) { + EntityRef client = character.getOwner(); + ClientComponent clientComponent = client.getComponent(ClientComponent.class); + String playerTeam = character.getComponent(LASTeamComponent.class).team; + PlayerStatisticsComponent playerStatisticsComponent = + character.getComponent(PlayerStatisticsComponent.class); + MigLayout migLayout = (playerTeam.equals("black") ? spadesTeamMigLayout : heartsTeamMigLayout); + if (!playerTeam.equals("white")) { + addInfoToTeamMigLayout(migLayout, clientComponent, playerStatisticsComponent); + } + } + } + } + + private void addInfoToTeamMigLayout(MigLayout migLayout, ClientComponent clientComponent, + PlayerStatisticsComponent playerStatisticsComponent) { + migLayout.addWidget(new UILabel(PlayerUtil.getColoredPlayerName(clientComponent.clientInfo)), + new MigLayout.CCHint()); + migLayout.addWidget(new UILabel(String.valueOf(playerStatisticsComponent.kills)), new MigLayout.CCHint()); + migLayout.addWidget(new UILabel(String.valueOf(playerStatisticsComponent.deaths)), new MigLayout.CCHint("wrap" + )); + playerStatisticsComponent.kills = 0; + playerStatisticsComponent.deaths = 0; + } + + private void addFlagInfo(DeathScreen deathScreen, GameOverEvent event) { + addTeamScore(deathScreen, "spadesTeamScore", event.blackTeamScore); + addTeamScore(deathScreen, "heartsTeamScore", event.redTeamScore); + addGoalScore(deathScreen, "spadesGoalScore"); + addGoalScore(deathScreen, "heartsGoalScore"); + } + + public void addTeamScore(DeathScreen deathScreen, String teamUILabelId, int finalScore) { + UILabel teamScore = deathScreen.find(teamUILabelId, UILabel.class); + teamScore.setText(String.valueOf(finalScore)); + } + + public void addGoalScore(DeathScreen deathScreen, String teamUILabelID) { + UILabel goalScore = deathScreen.find(teamUILabelID, UILabel.class); + goalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); + } + + private void triggerRestart() { + localPlayer.getClientEntity().send(new RestartRequestEvent()); + } +} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientPregameSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientPregameSystem.java deleted file mode 100644 index 84d216f6..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ClientPregameSystem.java +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.economy.components.AllowShopScreenComponent; -import org.terasology.economy.ui.MarketUiClientSystem; -import org.terasology.engine.core.SimpleUri; -import org.terasology.engine.entitySystem.entity.EntityManager; -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.entity.lifecycleEvents.OnAddedComponent; -import org.terasology.engine.entitySystem.event.EventPriority; -import org.terasology.engine.entitySystem.event.Priority; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.input.InputSystem; -import org.terasology.engine.logic.players.LocalPlayer; -import org.terasology.engine.network.ClientComponent; -import org.terasology.engine.registry.In; -import org.terasology.engine.rendering.nui.NUIManager; -import org.terasology.engine.unicode.EnclosedAlphanumerics; -import org.terasology.gestalt.assets.ResourceUrn; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.input.ButtonState; -import org.terasology.input.Input; -import org.terasology.module.inventory.input.InventoryButton; -import org.terasology.module.lightandshadow.events.GameStartMessageEvent; -import org.terasology.module.lightandshadow.events.TimerEvent; -import org.terasology.notifications.events.ExpireNotificationEvent; -import org.terasology.notifications.events.ShowNotificationEvent; -import org.terasology.notifications.model.Notification; -import org.terasology.notify.ui.DialogNotificationOverlay; -import org.terasology.nui.Color; -import org.terasology.nui.FontColor; - -import java.util.Timer; -import java.util.TimerTask; - -@RegisterSystem(RegisterMode.CLIENT) -public class ClientPregameSystem extends BaseComponentSystem { - private static final String NOTIFICATION_ID = "LightAndShadow:firstTimeShop"; - - public static final ResourceUrn ASSET_URI = new ResourceUrn("LightAndShadow:Timer"); - - private static final String PREGAME_MESSAGE = "The game start's as soon as there is \n at least one player in each team."; - - private static final int COUNTDOWN_IN_SECONDS = 30; - - private static Timer timer; - - @In - InputSystem inputSystem; - - @In - private EntityManager entityManager; - @In - private NUIManager nuiManager; - - @In - private LocalPlayer localPlayer; - - private DialogNotificationOverlay window; - - @Override - public void initialise() { - window = nuiManager.addOverlay(ASSET_URI, DialogNotificationOverlay.class); - } - - @Override - public void shutdown() { - nuiManager.closeScreen(window); - } - - @ReceiveEvent - public void onPregameStart(GameStartMessageEvent event, EntityRef entity) { - if (localPlayer.getClientEntity().equals(entity)) { - window.addNotification(PREGAME_MESSAGE); - entity.upsertComponent(AllowShopScreenComponent.class, c -> c.orElse(new AllowShopScreenComponent())); - } - } - - @ReceiveEvent - public void updateTimer(TimerEvent event, EntityRef entity) { - if (localPlayer.getClientEntity().equals(entity)) { - timer = new Timer(); - timer.scheduleAtFixedRate(new TimerTask() { - int timePeriod = COUNTDOWN_IN_SECONDS; - boolean addNotification; - public void run() { - if (addNotification) { - window.removeNotification(PREGAME_MESSAGE); - window.addNotification("The game starts in " + timePeriod-- + " seconds."); - addNotification = false; - } else { - window.removeNotification(PREGAME_MESSAGE); - window.removeNotification("The game starts in " + (timePeriod + 1) + " seconds."); - addNotification = true; - } - if (timePeriod < 0) { - window.removeNotification("The game starts in " + (timePeriod + 1) + " seconds."); - timer.cancel(); - } - } - }, 0, 500); - } - } - - /** - * Handles the button event if in-game shop is enabled. - * Needs to have a higher priority than {@link MarketUiClientSystem#onToggleInventory(InventoryButton, EntityRef)} - * to receive the {@link InventoryButton} event before it is consumed. - * - * @param event the help button event. - * @param entity the entity to display the help screen to. - */ - @Priority(EventPriority.PRIORITY_CRITICAL) - @ReceiveEvent(components = {ClientComponent.class, AllowShopScreenComponent.class}) - public void onInGameShopButton(InventoryButton event, EntityRef entity) { - if (event.getState() == ButtonState.DOWN) { - entity.send(new ExpireNotificationEvent(NOTIFICATION_ID)); - } - } - - /** - * Get a formatted representation of the primary {@link Input} associated with the given button binding. - * - * If the display name of the primary bound key is a single character this representation will be the encircled - * character. Otherwise the full display name is used. The bound key will be printed in yellow. - * - * If no key binding was found the text "n/a" in red color is returned. - * - * @param button the URI of a bindable button - * @return a formatted text to be used as representation for the player - */ - //TODO: put this in a common place? Duplicated in Dialogs, EventualSkills, and InGameHelp - private String getActivationKey(SimpleUri button) { - return inputSystem.getInputsForBindButton(button).stream() - .findFirst() - .map(Input::getDisplayName) - .map(key -> { - if (key.length() == 1) { - // print the key in yellow within a circle - int off = key.charAt(0) - 'A'; - char code = (char) (EnclosedAlphanumerics.CIRCLED_LATIN_CAPITAL_LETTER_A + off); - return String.valueOf(code); - } else { - return key; - } - }) - .map(key -> FontColor.getColored(key, Color.yellow)) - .orElse(FontColor.getColored("n/a", Color.red)); - } - - @ReceiveEvent(components = AllowShopScreenComponent.class) - public void onShopComponentAdded(OnAddedComponent event, EntityRef entity) { - Notification notification = new Notification(NOTIFICATION_ID, - "Shut Up and Take My Money!", - "Press " + getActivationKey(new SimpleUri("Inventory:inventory")) + " to buy items", - "Economy:GoldCoin"); - localPlayer.getClientEntity().send(new ShowNotificationEvent(notification)); - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientRestartSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientRestartSystem.java deleted file mode 100644 index e61ae1a9..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ClientRestartSystem.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.logic.players.LocalPlayer; -import org.terasology.engine.registry.In; -import org.terasology.engine.rendering.nui.NUIManager; -import org.terasology.engine.rendering.nui.layers.ingame.DeathScreen; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.module.lightandshadow.LASUtils; -import org.terasology.module.lightandshadow.events.ClientRestartEvent; -import org.terasology.nui.UILayout; - -/** - * System to close game over screen once restart is complete. - */ - -@RegisterSystem(RegisterMode.CLIENT) -public class ClientRestartSystem extends BaseComponentSystem { - @In - LocalPlayer localPlayer; - @In - NUIManager nuiManager; - - @ReceiveEvent - public void onClientRestart(ClientRestartEvent event, EntityRef clientEntity) { - if (localPlayer.getClientEntity().equals(clientEntity)) { - if (nuiManager.isOpen(LASUtils.DEATH_SCREEN)) { - DeathScreen deathScreen = (DeathScreen) nuiManager.getScreen(LASUtils.DEATH_SCREEN); - UILayout migLayout = deathScreen.find("playerStatistics", UILayout.class); - if (migLayout != null) { - migLayout.removeAllWidgets(); - } - nuiManager.closeScreen(LASUtils.DEATH_SCREEN); - } - } - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientScoreSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientScoreSystem.java deleted file mode 100644 index b0d66fe2..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ClientScoreSystem.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.registry.In; -import org.terasology.engine.rendering.nui.NUIManager; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.module.lightandshadow.LASUtils; -import org.terasology.module.lightandshadow.events.ScoreUpdateFromServerEvent; -import org.terasology.nui.ControlWidget; -import org.terasology.nui.databinding.ReadOnlyBinding; -import org.terasology.nui.widgets.UILabel; - -@RegisterSystem(RegisterMode.CLIENT) -public class ClientScoreSystem extends BaseComponentSystem { - @In - private NUIManager nuiManager; - - private int redScore = 0; - private int blackScore = 0; - - @Override - public void postBegin() { - // Sets score screen bindings - ControlWidget scoreScreen = nuiManager.getHUD().getHUDElement("LightAndShadow:ScoreHud"); - UILabel blackScoreArea = scoreScreen.find("blackScoreArea", UILabel.class); - blackScoreArea.bindText(new ReadOnlyBinding() { - @Override - public String get() { - return String.valueOf(blackScore); - } - }); - UILabel redScoreArea = scoreScreen.find("redScoreArea", UILabel.class); - redScoreArea.bindText(new ReadOnlyBinding() { - @Override - public String get() { - return String.valueOf(redScore); - } - }); - UILabel blackGoalScore = scoreScreen.find("blackGoalScore", UILabel.class); - blackGoalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); - UILabel redGoalScore = scoreScreen.find("redGoalScore", UILabel.class); - redGoalScore.setText(Integer.toString(LASUtils.GOAL_SCORE)); - } - - @Override - public void initialise() { - // Displays score UI to client on game start - nuiManager.getHUD().addHUDElement("ScoreHud"); - } - - @ReceiveEvent - public void onScoreUpdateFromServer(ScoreUpdateFromServerEvent event, EntityRef entity) { - String team = event.team; - if (team.equals(LASUtils.RED_TEAM)) { - redScore = event.score; - return; - } - if (team.equals(LASUtils.BLACK_TEAM)) { - blackScore = event.score; - return; - } - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientShopSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientShopSystem.java new file mode 100644 index 00000000..04c647e0 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/systems/ClientShopSystem.java @@ -0,0 +1,76 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.module.lightandshadow.systems; + +import org.terasology.economy.components.AllowShopScreenComponent; +import org.terasology.economy.ui.MarketUiClientSystem; +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.lifecycleEvents.OnAddedComponent; +import org.terasology.engine.entitySystem.event.EventPriority; +import org.terasology.engine.entitySystem.event.Priority; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.input.InputSystem; +import org.terasology.engine.logic.players.LocalPlayer; +import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.registry.In; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import org.terasology.input.ButtonState; +import org.terasology.module.inventory.input.InventoryButton; +import org.terasology.module.lightandshadow.LASUtils; +import org.terasology.module.lightandshadow.events.PlayerExitedArenaEvent; +import org.terasology.module.lightandshadow.phases.OnPreGamePhaseStartedEvent; +import org.terasology.notifications.events.ExpireNotificationEvent; +import org.terasology.notifications.events.ShowNotificationEvent; +import org.terasology.notifications.model.Notification; + +import java.util.Timer; +import java.util.TimerTask; + +@RegisterSystem(RegisterMode.CLIENT) +public class ClientShopSystem extends BaseComponentSystem { + private static final String SHOP_NOTIFICATION_ID = "LightAndShadow:firstTimeShop"; + + @In + InputSystem inputSystem; + + @In + private LocalPlayer localPlayer; + + @ReceiveEvent + public void onPregameStart(OnPreGamePhaseStartedEvent event, EntityRef entity) { + entity.upsertComponent(AllowShopScreenComponent.class, c -> c.orElse(new AllowShopScreenComponent())); + } + + @ReceiveEvent + public void onArenaExit(PlayerExitedArenaEvent event, EntityRef entity) { + entity.removeComponent(AllowShopScreenComponent.class); + } + + /** + * Handles the button event if in-game shop is enabled. + * Needs to have a higher priority than {@link MarketUiClientSystem#onToggleInventory(InventoryButton, EntityRef)} + * to receive the {@link InventoryButton} event before it is consumed. + * + * @param event the help button event. + * @param entity the entity to display the help screen to. + */ + @Priority(EventPriority.PRIORITY_CRITICAL) + @ReceiveEvent(components = {ClientComponent.class, AllowShopScreenComponent.class}) + public void onInGameShopButton(InventoryButton event, EntityRef entity) { + if (event.getState() == ButtonState.DOWN) { + entity.send(new ExpireNotificationEvent(SHOP_NOTIFICATION_ID)); + } + } + + @ReceiveEvent(components = AllowShopScreenComponent.class) + public void onShopComponentAdded(OnAddedComponent event, EntityRef entity) { + Notification notification = new Notification(SHOP_NOTIFICATION_ID, + "Shut Up and Take My Money!", + "Press " + LASUtils.getActivationKey(inputSystem, new SimpleUri("Inventory:inventory")) + " to buy items", + "Economy:GoldCoin"); + localPlayer.getClientEntity().send(new ShowNotificationEvent(notification)); + } +} \ No newline at end of file diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ClientStatisticsSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ClientStatisticsSystem.java deleted file mode 100644 index 3c5758f0..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ClientStatisticsSystem.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.engine.core.SimpleUri; -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.event.EventPriority; -import org.terasology.engine.entitySystem.event.Priority; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.input.InputSystem; -import org.terasology.engine.logic.players.LocalPlayer; -import org.terasology.engine.logic.players.event.LocalPlayerInitializedEvent; -import org.terasology.engine.registry.In; -import org.terasology.engine.rendering.nui.NUIManager; -import org.terasology.engine.rendering.nui.layers.ingame.DeathScreen; -import org.terasology.engine.unicode.EnclosedAlphanumerics; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.input.Input; -import org.terasology.module.lightandshadow.LASUtils; -import org.terasology.module.lightandshadow.events.ScoreUpdateFromServerEvent; -import org.terasology.module.lightandshadow.TapButton; -import org.terasology.notifications.events.ExpireNotificationEvent; -import org.terasology.notifications.events.ShowNotificationEvent; -import org.terasology.notifications.model.Notification; -import org.terasology.nui.Color; -import org.terasology.nui.FontColor; - -/** - * Displays statistics screen when required. - */ -@RegisterSystem(RegisterMode.CLIENT) -public class ClientStatisticsSystem extends BaseComponentSystem { - private static final String NOTIFICATION_ID = "LightAndShadow:firstTimeStatistics"; - - @In - InputSystem inputSystem; - @In - private NUIManager nuiManager; - @In - private LocalPlayer localPlayer; - @In - private ClientGameOverSystem clientGameOverSystem; - private DeathScreen statisticsScreen; - private int redScore = 0; - private int blackScore = 0; - private boolean isOpen; - private boolean isExpired; - - @Override - public void initialise() { - statisticsScreen = nuiManager.createScreen("LightAndShadow:statisticsScreen", DeathScreen.class); - clientGameOverSystem.addGoalScore(statisticsScreen, "spadesGoalScore"); - clientGameOverSystem.addGoalScore(statisticsScreen, "heartsGoalScore"); - clientGameOverSystem.addTeamScore(statisticsScreen, "heartsTeamScore", redScore); - clientGameOverSystem.addTeamScore(statisticsScreen, "spadesTeamScore", blackScore); - - } - @ReceiveEvent - public void onScoreUpdate(ScoreUpdateFromServerEvent event, EntityRef entity) { - String team = event.team; - if (team.equals(LASUtils.RED_TEAM)) { - redScore = event.score; - } - if (team.equals(LASUtils.BLACK_TEAM)) { - blackScore = event.score; - } - } - - @Priority(EventPriority.PRIORITY_CRITICAL) - @ReceiveEvent - public void onTab(TapButton event, EntityRef entity) { - if (event.isDown()) { - if (localPlayer.getClientEntity().equals(entity) && !isOpen) { - if (!isExpired) { - entity.send(new ExpireNotificationEvent(NOTIFICATION_ID)); - isExpired = true; - } - clientGameOverSystem.addTeamScore(statisticsScreen, "spadesTeamScore", blackScore); - clientGameOverSystem.addTeamScore(statisticsScreen, "heartsTeamScore", redScore); - clientGameOverSystem.addPlayerStatisticsInfo(statisticsScreen); - nuiManager.pushScreen("LightAndShadow:statisticsScreen"); - isOpen = true; - event.consume(); - } - } else { - isOpen = false; - nuiManager.closeScreen("LightAndShadow:statisticsScreen"); - } - } - - /** - * Get a formatted representation of the primary {@link Input} associated with the given button binding. - * - * If the display name of the primary bound key is a single character this representation will be the encircled - * character. Otherwise the full display name is used. The bound key will be printed in yellow. - * - * If no key binding was found the text "n/a" in red color is returned. - * - * @param button the URI of a bindable button - * @return a formatted text to be used as representation for the player - */ - //TODO: put this in a common place? Duplicated in Dialogs, EventualSkills, and InGameHelp - private String getActivationKey(SimpleUri button) { - return inputSystem.getInputsForBindButton(button).stream() - .findFirst() - .map(Input::getDisplayName) - .map(key -> { - if (key.length() == 1) { - // print the key in yellow within a circle - int off = key.charAt(0) - 'A'; - char code = (char) (EnclosedAlphanumerics.CIRCLED_LATIN_CAPITAL_LETTER_A + off); - return String.valueOf(code); - } else { - return key; - } - }) - .map(key -> FontColor.getColored(key, Color.yellow)) - .orElse(FontColor.getColored("n/a", Color.red)); - } - - @ReceiveEvent - public void onLocalPlayerInitialized(LocalPlayerInitializedEvent event, EntityRef entity) { - Notification notification = new Notification(NOTIFICATION_ID, - "The Numbers Game", - "Hold " + getActivationKey(new SimpleUri("LightAndShadow:statistics")) + " to see statistics", - "engine:items#blueBook"); - localPlayer.getClientEntity().send(new ShowNotificationEvent(notification)); - } -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/GameEntitySystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/GameEntitySystem.java index 9148db46..f7890308 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/GameEntitySystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/GameEntitySystem.java @@ -12,9 +12,17 @@ import org.terasology.engine.entitySystem.entity.EntityRef; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.logic.console.commandSystem.annotations.Command; +import org.terasology.engine.logic.console.commandSystem.annotations.CommandParam; +import org.terasology.engine.logic.console.commandSystem.annotations.Sender; +import org.terasology.engine.logic.permission.PermissionManager; +import org.terasology.engine.logic.players.PlayerCharacterComponent; import org.terasology.engine.registry.Share; +import org.terasology.lightandshadowresources.components.LASTeamComponent; +import org.terasology.module.lightandshadow.LASUtils; import org.terasology.module.lightandshadow.components.LASConfigComponent; import org.terasology.engine.registry.In; +import org.terasology.module.lightandshadow.components.LASTeamStatsComponent; /** * Provides an entity that keeps track of game state information. @@ -43,5 +51,43 @@ public EntityRef getGameEntity() { } return gameEntity; } + + /** + * Updates the team statistics stored in the LASTeamStatsComponent of the GameEntity + */ + public void updateTeamStats() { + LASTeamStatsComponent teamStats = new LASTeamStatsComponent(); + Iterable characters = entityManager.getEntitiesWith(PlayerCharacterComponent.class, LASTeamComponent.class); + for (EntityRef character : characters) { + if (character.getComponent(LASTeamComponent.class).team.equals(LASUtils.BLACK_TEAM)) { + teamStats.blackTeamSize++; + } else if (character.getComponent(LASTeamComponent.class).team.equals(LASUtils.RED_TEAM)) { + teamStats.redTeamSize++; + } else { + logger.debug("Found character {} with white team", character.getId()); + teamStats.whiteTeamSize++; + } + } + + gameEntity.addOrSaveComponent(teamStats); + } + + @Command(shortDescription = "Set the maximum team size difference", helpText = "Set maxTeamSizeDifference", runOnServer = true, + requiredPermission = PermissionManager.CHEAT_PERMISSION) + public String setMaxTeamSizeDifference(@Sender EntityRef client, @CommandParam("difference") int difference) { + LASConfigComponent lasconfig = gameEntity.getComponent(LASConfigComponent.class); + lasconfig.maxTeamSizeDifference = difference; + gameEntity.saveComponent(lasconfig); + return "The max team size difference is set to " + difference; + } + + @Command(shortDescription = "Set the minimum team size", helpText = "Set minTeamSize", runOnServer = true, + requiredPermission = PermissionManager.CHEAT_PERMISSION) + public String setMinTeamSize(@Sender EntityRef client, @CommandParam("size") int size) { + LASConfigComponent lasconfig = gameEntity.getComponent(LASConfigComponent.class); + lasconfig.minTeamSize = size; + gameEntity.saveComponent(lasconfig); + return "The min team size is set to " + size; + } } diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/LASSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/LASSystem.java index 773d2c9a..cf765b92 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/LASSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/LASSystem.java @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 package org.terasology.module.lightandshadow.systems; +import org.joml.Vector3f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.terasology.economy.components.CurrencyStorageComponent; import org.terasology.economy.events.WalletUpdatedEvent; import org.terasology.engine.entitySystem.entity.EntityRef; @@ -9,13 +12,20 @@ import org.terasology.engine.entitySystem.event.Priority; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.logic.characters.CharacterTeleportEvent; import org.terasology.engine.logic.players.event.OnPlayerSpawnedEvent; +import org.terasology.engine.network.ClientComponent; import org.terasology.engine.registry.In; import org.terasology.engine.world.sun.CelestialSystem; import org.terasology.gestalt.entitysystem.event.ReceiveEvent; import org.terasology.module.inventory.components.InventoryComponent; import org.terasology.module.inventory.events.RemoveItemAction; import org.terasology.module.inventory.systems.InventoryManager; +import org.terasology.module.lightandshadow.LASUtils; +import org.terasology.module.lightandshadow.events.PlayerExitedArenaEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; +import org.terasology.module.lightandshadow.phases.authority.PhaseSystem; @RegisterSystem public class LASSystem extends BaseComponentSystem { @@ -25,6 +35,12 @@ public class LASSystem extends BaseComponentSystem { private CelestialSystem celestialSystem; @In private GameEntitySystem gameEntitySystem; + @In + private PhaseSystem phaseSystem; + @In + private TeamSystem teamSystem; + + private static final Logger logger = LoggerFactory.getLogger(LASSystem.class); /** * Gives an empty inventory to a player in the lobby to prevent fight's in the lobby and gives the player some funds. @@ -39,6 +55,29 @@ public void onPlayerSpawn(OnPlayerSpawnedEvent event, EntityRef player, Inventor EntityRef gameEntity = gameEntitySystem.getGameEntity(); player.getComponent(CurrencyStorageComponent.class).amount = gameEntity.getComponent(CurrencyStorageComponent.class).amount; player.send(new WalletUpdatedEvent(gameEntity.getComponent(CurrencyStorageComponent.class).amount)); + player.send(new CharacterTeleportEvent(new Vector3f(LASUtils.FLOATING_PLATFORM_POSITION).add(0, 1, 0))); + } + + /** + * Follow-up actions necessary when a player leaves the arena, either by porting back to the platform + * or by disconnecting from the server. + * These action are required to verify the current game state and update the phase accordingly if necessary. + */ + @ReceiveEvent + public void onArenaExit(PlayerExitedArenaEvent event, EntityRef player) { + // if in game phase: verify that game start condition still met + Phase currentPhase = phaseSystem.getCurrentPhase(); + if ((currentPhase == Phase.IN_GAME || currentPhase == Phase.COUNTDOWN) && !teamSystem.isMinSizeTeams()) { + logger.debug("Starting condition no longer met, switching from phase {} to PRE_GAME", currentPhase); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.PRE_GAME)); + } + + // if the player teleporting was the last one still in the arena + // switch back to idle phase + if (teamSystem.getTeamSize(LASUtils.BLACK_TEAM) == 0 && teamSystem.getTeamSize(LASUtils.RED_TEAM) == 0) { + logger.debug("No players left in arena, switching to IDLE phase"); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.IDLE)); + } } @Override diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/MagicDomeSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/MagicDomeSystem.java index 0a3f3883..561a7f88 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/MagicDomeSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/MagicDomeSystem.java @@ -13,23 +13,23 @@ import org.terasology.engine.logic.characters.CharacterImpulseEvent; import org.terasology.engine.logic.characters.CharacterMoveInputEvent; import org.terasology.engine.logic.delay.DelayManager; -import org.terasology.engine.logic.delay.DelayedActionTriggeredEvent; import org.terasology.engine.logic.location.LocationComponent; import org.terasology.engine.registry.In; import org.terasology.gestalt.entitysystem.event.ReceiveEvent; import org.terasology.lightandshadowresources.components.LASTeamComponent; import org.terasology.module.lightandshadow.LASUtils; -import org.terasology.module.lightandshadow.components.InvulnerableComponent; import org.terasology.module.lightandshadow.components.MagicDome; -import org.terasology.module.lightandshadow.events.ActivateBarrierEvent; -import org.terasology.module.lightandshadow.events.DelayedDeactivateBarrierEvent; +import org.terasology.module.lightandshadow.phases.OnInGamePhaseStartedEvent; +import org.terasology.module.lightandshadow.phases.OnPreGamePhaseStartedEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.authority.PhaseSystem; @RegisterSystem(RegisterMode.AUTHORITY) public class MagicDomeSystem extends BaseComponentSystem { - private static final String DEACTIVATE_BARRIERS_ACTION = "LightAndShadow:deactivateBarriers"; private static final int PREGAME_ZONE_RADIUS = 20; +; @In - DelayManager delayManager; + PhaseSystem phaseSystem; @In private EntityManager entityManager; @@ -38,38 +38,46 @@ public class MagicDomeSystem extends BaseComponentSystem { private EntityRef blackBarrier = EntityRef.NULL; + @ReceiveEvent + public void onPregameStart(OnPreGamePhaseStartedEvent event, EntityRef entity) { + activateBarriers(); + } + + @ReceiveEvent + public void onGameStart(OnInGamePhaseStartedEvent event, EntityRef entity) { + deactivateBarriers(); + } + /** * Activates the barriers for the pregame regions corresponding to both the teams only in the beginning when * the barriers haven't been created yet. - * - * @param event - * @param entity */ - @ReceiveEvent - public void activateBarriers(ActivateBarrierEvent event, EntityRef entity) { + private void activateBarriers() { if (redBarrier == EntityRef.NULL && blackBarrier == EntityRef.NULL) { redBarrier = createBarrier("lightAndShadowResources:magicDome", LASUtils.CENTER_RED_BASE_POSITION, "red"); blackBarrier = createBarrier("lightAndShadowResources:magicDome", LASUtils.CENTER_BLACK_BASE_POSITION, "black"); } } - @ReceiveEvent - public void delayedDeactivateBarriers(DelayedDeactivateBarrierEvent event, EntityRef entity) { - delayManager.addDelayedAction(entity, DEACTIVATE_BARRIERS_ACTION, event.getDelay()); + public void deactivateBarriers() { + redBarrier.destroy(); + blackBarrier.destroy(); } - @ReceiveEvent - public void deactivateBarriers(DelayedActionTriggeredEvent event, EntityRef entity) { - if (event.getActionId().equals(DEACTIVATE_BARRIERS_ACTION)) { - redBarrier.destroy(); - blackBarrier.destroy(); - removePlayerInvulnerableComponents(); + + @ReceiveEvent(components = {LocationComponent.class, LASTeamComponent.class}) + public void onCharacterMovement(CharacterMoveInputEvent moveInputEvent, EntityRef player, LocationComponent loc) { + String team = player.getComponent(LASTeamComponent.class).team; + if (team.equals(LASUtils.WHITE_TEAM)) { + // spectators should not be affected by the barriers + return; } - } + if (! (phaseSystem.getCurrentPhase().equals(Phase.PRE_GAME) || phaseSystem.getCurrentPhase().equals(Phase.COUNTDOWN))) { + // barriers should only be active during pre-game and countdown phase + return; + } - @ReceiveEvent(components = LocationComponent.class) - public void onCharacterMovement(CharacterMoveInputEvent moveInputEvent, EntityRef player, LocationComponent loc) { Vector3f pos = new Vector3f(loc.getWorldPosition(new Vector3f())); for (EntityRef domeEntity : entityManager.getEntitiesWith(MagicDome.class, LocationComponent.class)) { @@ -110,11 +118,4 @@ public boolean isAllowedInsideBarrier(EntityRef player, EntityRef barrier) { String playerTeam = player.getComponent(LASTeamComponent.class).team; return barrierTeam.equals(playerTeam); } - - private void removePlayerInvulnerableComponents() { - Iterable players = entityManager.getEntitiesWith(InvulnerableComponent.class); - for (EntityRef player : players) { - player.removeComponent(InvulnerableComponent.class); - } - } } diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/PregameSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/PregameSystem.java deleted file mode 100644 index cb091405..00000000 --- a/src/main/java/org/terasology/module/lightandshadow/systems/PregameSystem.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.module.lightandshadow.systems; - -import org.terasology.engine.entitySystem.entity.EntityManager; -import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.entitySystem.event.EventPriority; -import org.terasology.engine.entitySystem.event.Priority; -import org.terasology.engine.entitySystem.prefab.Prefab; -import org.terasology.engine.entitySystem.systems.BaseComponentSystem; -import org.terasology.engine.entitySystem.systems.RegisterMode; -import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.engine.registry.In; -import org.terasology.engine.utilities.Assets; -import org.terasology.gestalt.entitysystem.event.ReceiveEvent; -import org.terasology.module.health.events.BeforeDamagedEvent; -import org.terasology.module.inventory.components.StartingInventoryComponent; -import org.terasology.module.lightandshadow.components.InvulnerableComponent; -import org.terasology.module.lightandshadow.events.ActivateBarrierEvent; -import org.terasology.module.lightandshadow.events.PregameEvent; - -import java.util.Optional; - -@RegisterSystem(RegisterMode.AUTHORITY) -public class PregameSystem extends BaseComponentSystem { - Optional prefab = Assets.getPrefab("inventory"); - StartingInventoryComponent startingInventory = prefab.get().getComponent(StartingInventoryComponent.class); - - @In - private EntityManager entityManager; - - @ReceiveEvent - public void onPregameStart(PregameEvent event, EntityRef entity) { - entity.send(new ActivateBarrierEvent()); - entity.addComponent(new InvulnerableComponent()); - } - - @Priority(EventPriority.PRIORITY_HIGH) - @ReceiveEvent(components = InvulnerableComponent.class) - public void preventFriendlyFire(BeforeDamagedEvent event, EntityRef entity) { - event.consume(); - } - -} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/RestartSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/RestartSystem.java index cb3846e1..98da2557 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/RestartSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/RestartSystem.java @@ -20,6 +20,8 @@ import org.terasology.module.lightandshadow.LASUtils; import org.terasology.module.lightandshadow.events.ClientRestartEvent; import org.terasology.module.lightandshadow.events.RestartRequestEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; /** * System to invoke restart of a game round. @@ -52,5 +54,6 @@ public void onRestartRequest(RestartRequestEvent event, EntityRef clientEntity, sub(LASUtils.getTeleportDestination(LASUtils.getOppositionTeam(team)), new Vector3f())), 0)); client.send(new ClientRestartEvent()); } + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.PRE_GAME)); } } diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/ScoreSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/ScoreSystem.java index 9782e3ee..6468048e 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/ScoreSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/ScoreSystem.java @@ -23,6 +23,8 @@ import org.terasology.module.lightandshadow.events.RestartRequestEvent; import org.terasology.module.lightandshadow.events.ReturnFlagEvent; import org.terasology.module.lightandshadow.events.ScoreUpdateFromServerEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; /** * System responsible for calculating and providing score. @@ -34,6 +36,8 @@ public class ScoreSystem extends BaseComponentSystem { private InventoryManager inventoryManager; @In private EntityManager entityManager; + @In + GameEntitySystem gameEntitySystem; private int redScore; private int blackScore; @@ -87,10 +91,12 @@ private void checkAndResetGameOnScore(ActivateEvent event, EntityRef entity) { if (redScore >= LASUtils.GOAL_SCORE) { resetLevel(); sendEventToClients(new GameOverEvent(LASUtils.RED_TEAM, blackScore, redScore)); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.POST_GAME)); } if (blackScore >= LASUtils.GOAL_SCORE) { resetLevel(); sendEventToClients(new GameOverEvent(LASUtils.BLACK_TEAM, blackScore, redScore)); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.POST_GAME)); } } } diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/TeamSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/TeamSystem.java new file mode 100644 index 00000000..7bace3e9 --- /dev/null +++ b/src/main/java/org/terasology/module/lightandshadow/systems/TeamSystem.java @@ -0,0 +1,95 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.systems; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.lifecycleEvents.OnChangedComponent; +import org.terasology.engine.entitySystem.event.EventPriority; +import org.terasology.engine.entitySystem.event.Priority; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.registry.In; +import org.terasology.engine.registry.Share; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; +import org.terasology.lightandshadowresources.components.LASTeamComponent; +import org.terasology.module.lightandshadow.LASUtils; +import org.terasology.module.lightandshadow.components.LASConfigComponent; +import org.terasology.module.lightandshadow.components.LASTeamStatsComponent; +import org.terasology.module.lightandshadow.events.PlayerExitedArenaEvent; + +@RegisterSystem(RegisterMode.AUTHORITY) +@Share(value = TeamSystem.class) +public class TeamSystem extends BaseComponentSystem { + @In + GameEntitySystem gameEntitySystem; + + private static final Logger logger = LoggerFactory.getLogger(TeamSystem.class); + + @ReceiveEvent(components = LASTeamComponent.class) + public void onTeamChange(OnChangedComponent event, EntityRef entity) { + logger.debug("Player {} switched to team {}", + entity.getOwner().getComponent(ClientComponent.class).clientInfo, + entity.getComponent(LASTeamComponent.class).team); + } + + @Priority(EventPriority.PRIORITY_HIGH) + @ReceiveEvent(components = LASTeamComponent.class) + public void onArenaExit(PlayerExitedArenaEvent event, EntityRef player) { + // players exiting the arena (e.g. by teleporting to platform or disconnecting from the game) + // should be removed from the playing teams + LASTeamComponent lasTeamComponent = player.getComponent(LASTeamComponent.class); + lasTeamComponent.team = LASUtils.WHITE_TEAM; + player.addOrSaveComponent(lasTeamComponent); + } + + public boolean isBalancedTeams(String targetTeam) { + gameEntitySystem.updateTeamStats(); + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + LASTeamStatsComponent teamStats = gameEntity.getComponent(LASTeamStatsComponent.class); + int maxTeamSizeDifference = gameEntity.getComponent(LASConfigComponent.class).maxTeamSizeDifference; + int currentTeamSizeDiff = teamStats.blackTeamSize - teamStats.redTeamSize; + int postTeleportTeamSizeDiff = targetTeam.equals(LASUtils.BLACK_TEAM) ? currentTeamSizeDiff + 1 : currentTeamSizeDiff - 1; + + return Math.abs(postTeleportTeamSizeDiff) <= maxTeamSizeDifference; + } + + public boolean isMinSizeTeams() { + gameEntitySystem.updateTeamStats(); + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + LASTeamStatsComponent teamStats = gameEntity.getComponent(LASTeamStatsComponent.class); + int minTeamSize = gameEntity.getComponent(LASConfigComponent.class).minTeamSize; + + return teamStats.redTeamSize >= minTeamSize && teamStats.blackTeamSize >= minTeamSize; + } + + public boolean setPlayerTeamToTeam(EntityRef player, String team) { + if (!isBalancedTeams(team)) { + return false; + } + LASTeamComponent playerTeamComponent = new LASTeamComponent(team); + player.addOrSaveComponent(playerTeamComponent); + return true; + } + + public int getTeamSize(String team) { + gameEntitySystem.updateTeamStats(); + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + LASTeamStatsComponent teamStats = gameEntity.getComponent(LASTeamStatsComponent.class); + switch (team) { + case LASUtils.BLACK_TEAM: + return teamStats.blackTeamSize; + case LASUtils.RED_TEAM: + return teamStats.redTeamSize; + case LASUtils.WHITE_TEAM: + return teamStats.whiteTeamSize; + } + logger.error("Was requested to get team size for non-existant team {}", team); + return 0; + } + +} diff --git a/src/main/java/org/terasology/module/lightandshadow/systems/TeleporterSystem.java b/src/main/java/org/terasology/module/lightandshadow/systems/TeleporterSystem.java index 7117779d..8dffad51 100644 --- a/src/main/java/org/terasology/module/lightandshadow/systems/TeleporterSystem.java +++ b/src/main/java/org/terasology/module/lightandshadow/systems/TeleporterSystem.java @@ -4,6 +4,8 @@ import org.joml.Random; import org.joml.Vector3f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; import org.terasology.engine.entitySystem.prefab.Prefab; @@ -14,12 +16,11 @@ import org.terasology.engine.logic.chat.ChatMessageEvent; import org.terasology.engine.logic.common.ActivateEvent; import org.terasology.engine.logic.console.commandSystem.annotations.Command; -import org.terasology.engine.logic.console.commandSystem.annotations.CommandParam; import org.terasology.engine.logic.console.commandSystem.annotations.Sender; import org.terasology.engine.logic.permission.PermissionManager; -import org.terasology.engine.logic.players.PlayerCharacterComponent; import org.terasology.engine.logic.players.SetDirectionEvent; import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.network.events.BeforeDisconnectEvent; import org.terasology.engine.registry.In; import org.terasology.engine.utilities.Assets; import org.terasology.gestalt.entitysystem.event.Event; @@ -30,17 +31,18 @@ import org.terasology.module.inventory.events.RequestInventoryEvent; import org.terasology.module.lightandshadow.LASUtils; import org.terasology.module.lightandshadow.components.LASConfigComponent; -import org.terasology.module.lightandshadow.events.DelayedDeactivateBarrierEvent; -import org.terasology.module.lightandshadow.events.GameStartMessageEvent; -import org.terasology.module.lightandshadow.events.PregameEvent; +import org.terasology.module.lightandshadow.events.PlayerExitedArenaEvent; import org.terasology.module.lightandshadow.events.TimerEvent; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.phases.SwitchToPhaseEvent; +import org.terasology.module.lightandshadow.phases.authority.PhaseSystem; import java.util.Optional; import java.util.function.Supplier; /** - * Teleports players to play arena once they chose their team. - * It also sends events to change players skins and hud based on team they have chosen. + * Teleports players to play arena once they chose their team. It also sends events to change players skins and hud based on team they have + * chosen. * * @see ClientSkinSystem */ @@ -50,95 +52,65 @@ public class TeleporterSystem extends BaseComponentSystem { EntityManager entityManager; @In GameEntitySystem gameEntitySystem; + @In + PhaseSystem phaseSystem; + @In + TeamSystem teamSystem; + + private static final Logger logger = LoggerFactory.getLogger(TeleporterSystem.class); Optional prefab = Assets.getPrefab("inventory"); StartingInventoryComponent startingInventory = prefab.get().getComponent(StartingInventoryComponent.class); - private boolean gameStart; - private final Random random = new Random(); - @Command(shortDescription = "Set the maximum team size difference", helpText = "Set maxTeamSizeDifference", runOnServer = true, - requiredPermission = PermissionManager.CHEAT_PERMISSION) - public String setMaxTeamSizeDifference(@Sender EntityRef client, @CommandParam("difference") int difference) { - EntityRef gameEntity = gameEntitySystem.getGameEntity(); - LASConfigComponent lasconfig = gameEntity.getComponent(LASConfigComponent.class); - lasconfig.maxTeamSizeDifference = difference; - gameEntity.saveComponent(lasconfig); - return "The max team size difference is set to " + difference; - } - /** - * Depending on which teleporter the player chooses, they are set to that team - * and teleported to that base + * Depending on which teleporter the player chooses, they are set to that team and teleported to that base Assumption: there are two + * teleporters, one for the red, one for the black team * * @param event * @param entity */ @ReceiveEvent(components = SetTeamOnActivateComponent.class) - public void onActivate(ActivateEvent event, EntityRef entity) { + public void onTeleportActivation(ActivateEvent event, EntityRef entity) { EntityRef player = event.getInstigator(); - if (isProperTeamSize(entity, player)) { - String team = setPlayerTeamToTeleporterTeam(player, entity); - handlePlayerTeleport(player, team); - } - } - - private boolean isProperTeamSize(EntityRef teleporter, EntityRef player) { - EntityRef gameEntity = gameEntitySystem.getGameEntity(); - int oppositeTeamCount = 0; - int teleporterTeamCount = 0; - int maxTeamSizeDifference = gameEntity.getComponent(LASConfigComponent.class).maxTeamSizeDifference; - String teleporterTeam = teleporter.getComponent(LASTeamComponent.class).team; - Iterable characters = entityManager.getEntitiesWith(PlayerCharacterComponent.class, - LASTeamComponent.class); - - for (EntityRef character : characters) { - String otherPlayerTeam = character.getComponent(LASTeamComponent.class).team; - if (teleporterTeam.equals(otherPlayerTeam)) { - teleporterTeamCount++; - } else if (!otherPlayerTeam.equals(LASUtils.WHITE_TEAM)) { - oppositeTeamCount++; - } - } - if (teleporterTeamCount - oppositeTeamCount < maxTeamSizeDifference) { - if (teleporterTeamCount >= 0 && oppositeTeamCount >= 1 && !gameStart) { - sendEventToClients(TimerEvent::new); - player.send(new DelayedDeactivateBarrierEvent(30000)); - gameStart = true; - } - return true; + String targetTeam = entity.getComponent(LASTeamComponent.class).team; + if (targetTeam.equals(LASUtils.WHITE_TEAM)) { + // teleporters are (currently) expected to only target black or red team + logger.debug("Player {} attempted to join white team via teleporter", player.getId()); + event.consume(); } else { - if (maxTeamSizeDifference == 1) { - player.getOwner().send(new ChatMessageEvent( - "The " + teleporterTeam + " team has " + maxTeamSizeDifference + " player more, " + - "so please join the " + LASUtils.getOppositionTeam(teleporterTeam) + " team.", EntityRef.NULL)); + if (teamSystem.setPlayerTeamToTeam(player, targetTeam)) { + logger.debug("Player {} successfully added to team {}", player, targetTeam); + handlePlayerTeleport(player, targetTeam); } else { + EntityRef gameEntity = gameEntitySystem.getGameEntity(); + LASConfigComponent config = gameEntity.getComponent(LASConfigComponent.class); player.getOwner().send(new ChatMessageEvent( - "The " + teleporterTeam + " team has " + maxTeamSizeDifference + " players more, " + - "so please join the " + LASUtils.getOppositionTeam(teleporterTeam) + " team.", EntityRef.NULL)); + "The " + targetTeam + " team has " + config.maxTeamSizeDifference + " player(s) more, " + + "please join the " + LASUtils.getOppositionTeam(targetTeam) + " team.", EntityRef.NULL)); } - return false; } } - private String setPlayerTeamToTeleporterTeam(EntityRef player, EntityRef teleporter) { - LASTeamComponent teleporterTeamComponent = teleporter.getComponent(LASTeamComponent.class); - LASTeamComponent playerTeamComponent = player.getComponent(LASTeamComponent.class); - playerTeamComponent.team = teleporterTeamComponent.team; - player.saveComponent(playerTeamComponent); - return playerTeamComponent.team; - } - private void handlePlayerTeleport(EntityRef player, String team) { + // when the first player joins the game, switch to pre-game phase + if (phaseSystem.getCurrentPhase() == Phase.IDLE) { + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.PRE_GAME)); + } + Vector3f randomVector = new Vector3f(-1 + random.nextInt(3), 0, -1 + random.nextInt(3)); - player.send(new PregameEvent()); player.send(new CharacterTeleportEvent(randomVector.add(LASUtils.getTeleportDestination(team)))); player.send(new SetDirectionEvent(LASUtils.getYaw(LASUtils.getTeleportDestination(team). sub(LASUtils.getTeleportDestination(LASUtils.getOppositionTeam(team)), new Vector3f())), 0)); player.addOrSaveComponent(startingInventory); player.send(new RequestInventoryEvent(startingInventory.items)); - sendEventToClients(GameStartMessageEvent::new); + + // check game start condition + if (teamSystem.isMinSizeTeams() && phaseSystem.getCurrentPhase() == Phase.PRE_GAME) { + sendEventToClients(TimerEvent::new); + gameEntitySystem.getGameEntity().send(new SwitchToPhaseEvent(Phase.COUNTDOWN)); + } } private void sendEventToClients(Supplier eventSupplier) { @@ -147,4 +119,27 @@ private void sendEventToClients(Supplier eventSupplier) { client.send(eventSupplier.get()); } } + + @Command(shortDescription = "Teleport player back to platform", helpText = "Platform Teleport", runOnServer = true, + requiredPermission = PermissionManager.CHEAT_PERMISSION) + public String teleportToPlatform(@Sender EntityRef sender) { + ClientComponent clientComp = sender.getComponent(ClientComponent.class); + LASTeamComponent senderTeam = clientComp.character.getComponent(LASTeamComponent.class); + if (senderTeam != null && (senderTeam.team.equals(LASUtils.RED_TEAM) || senderTeam.team.equals(LASUtils.BLACK_TEAM))) { + // spectators (white team) are not relevant for arena exit actions + clientComp.character.send(new PlayerExitedArenaEvent(sender)); + } + clientComp.character.send(new CharacterTeleportEvent(new Vector3f(LASUtils.FLOATING_PLATFORM_POSITION).add(0, 1, 0))); + return "Teleporting you to the platform."; + } + + @ReceiveEvent(components = ClientComponent.class) + public void onPlayerDisconnect(BeforeDisconnectEvent event, EntityRef entity) { + ClientComponent clientComp = entity.getComponent(ClientComponent.class); + LASTeamComponent senderTeam = clientComp.character.getComponent(LASTeamComponent.class); + if (senderTeam != null && (senderTeam.team.equals(LASUtils.RED_TEAM) || senderTeam.team.equals(LASUtils.BLACK_TEAM))) { + // spectators (white team) are not relevant for arena exit actions + teleportToPlatform(entity); + } + } } diff --git a/src/test/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapterTest.java b/src/test/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapterTest.java new file mode 100644 index 00000000..febbf5b5 --- /dev/null +++ b/src/test/java/org/terasology/module/lightandshadow/phases/PhaseParameterAdapterTest.java @@ -0,0 +1,40 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PhaseParameterAdapterTest { + + static PhaseParameterAdapter adapter; + + @BeforeAll + static void setup() { + // the adapter is a stateless component and can therefore safely be reused. + adapter = new PhaseParameterAdapter(); + } + + @Test + void testInvalidInput() { + assertThrows(IllegalArgumentException.class, () -> adapter.parse("foobar")); + } + + @ParameterizedTest + @EnumSource(Phase.class) + void testExactInput(Phase phase) { + assertEquals(phase, adapter.parse(phase.toString())); + } + + @ParameterizedTest + @EnumSource(Phase.class) + void testLowercaseInput(Phase phase) { + assertEquals(phase, adapter.parse(phase.toString().toLowerCase())); + } +} diff --git a/src/test/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystemTest.java b/src/test/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystemTest.java new file mode 100644 index 00000000..657f7672 --- /dev/null +++ b/src/test/java/org/terasology/module/lightandshadow/phases/authority/PhaseCommandsSystemTest.java @@ -0,0 +1,61 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.module.lightandshadow.phases.authority; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.internal.PojoEntityManager; +import org.terasology.module.lightandshadow.phases.Phase; +import org.terasology.module.lightandshadow.systems.GameEntitySystem; + +import java.util.stream.Stream; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PhaseCommandsSystemTest { + PhaseCommandsSystem phaseCommandsSystem; + + @BeforeEach + void setUp() { + phaseCommandsSystem = new PhaseCommandsSystem(); + EntityRef gameEntity = new PojoEntityManager().create(); + gameEntity.saveComponent(new PhaseComponent()); + createMockPhaseSystem(gameEntity); + } + + private void createMockPhaseSystem(EntityRef gameEntity) { + phaseCommandsSystem.phaseSystem = mock(PhaseSystem.class); + phaseCommandsSystem.phaseSystem.gameEntitySystem = mock(GameEntitySystem.class); + when(phaseCommandsSystem.phaseSystem.gameEntitySystem.getGameEntity()).thenReturn(gameEntity); + } + + static Stream phaseProvider() { + Stream.Builder argumentsBuilder = Stream.builder(); + for (Phase fromPhase : Phase.values()) { + for (Phase toPhase : Phase.values()) { + argumentsBuilder.add(Arguments.of(fromPhase, toPhase)); + } + } + + return argumentsBuilder.build(); + } + + @ParameterizedTest + @MethodSource("phaseProvider") + void testForcePhaseSuccessful(Phase currentPhase, Phase targetPhase) { + // Arrange + when(phaseCommandsSystem.phaseSystem.getCurrentPhase()).thenReturn(currentPhase); + // Act + phaseCommandsSystem.forcePhase(EntityRef.NULL, targetPhase); + // Assert + verify(phaseCommandsSystem.phaseSystem, times(1)).getCurrentPhase(); + verify(phaseCommandsSystem.phaseSystem, times(1)).transitionPhase(currentPhase, targetPhase); + } +}