package com.cardshifter.client;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.cardshifter.api.both.ChatMessage;
import com.cardshifter.api.incoming.RequestTargetsMessage;
import com.cardshifter.api.incoming.UseAbilityMessage;
import com.cardshifter.api.messages.Message;
import com.cardshifter.api.outgoing.AvailableTargetsMessage;
import com.cardshifter.api.outgoing.CardInfoMessage;
import com.cardshifter.api.outgoing.ClientDisconnectedMessage;
import com.cardshifter.api.outgoing.EntityRemoveMessage;
import com.cardshifter.api.outgoing.GameOverMessage;
import com.cardshifter.api.outgoing.NewGameMessage;
import com.cardshifter.api.outgoing.PlayerMessage;
import com.cardshifter.api.outgoing.ResetAvailableActionsMessage;
import com.cardshifter.api.outgoing.UpdateMessage;
import com.cardshifter.api.outgoing.UsableActionMessage;
import com.cardshifter.api.outgoing.WelcomeMessage;
import com.cardshifter.api.outgoing.ZoneChangeMessage;
import com.cardshifter.api.outgoing.ZoneMessage;
import com.cardshifter.client.views.ActionButton;
import com.cardshifter.client.views.BattlefieldZoneView;
import com.cardshifter.client.views.CardBattlefieldDocumentController;
import com.cardshifter.client.views.CardHandDocumentController;
import com.cardshifter.client.views.CardView;
import com.cardshifter.client.views.PlayerHandZoneView;
import com.cardshifter.client.views.ZoneView;
import com.cardshifter.core.messages.IncomingHandler;
import javafx.scene.input.MouseEvent;
public class GameClientController {
private static final Logger logger = LogManager.getLogger(GameClientController.class);
@FXML private AnchorPane rootPane;
@FXML private Label loginMessage;
@FXML private ListView<String> serverMessages;
@FXML private VBox opponentStatBox;
@FXML private VBox playerStatBox;
@FXML private HBox actionBox;
@FXML private HBox opponentHandPane;
@FXML private Rectangle opponentTargetRectangle;
@FXML private HBox opponentBattlefieldPane;
@FXML private Pane opponentDeckPane;
@FXML private Label opponentDeckLabel;
@FXML private HBox playerHandPane;
@FXML private Rectangle playerTargetRectangle;
@FXML private HBox playerBattlefieldPane;
@FXML private Pane playerDeckPane;
@FXML private Label playerDeckLabel;
@FXML private Label playerName;
@FXML private Label opponentName;
private int gameId;
private int playerIndex;
private int opponentId;
private int opponentHandId;
private int opponentBattlefieldId;
private int opponentDeckId;
private final Map<Integer, Set<Integer>> deckEntityIds = new HashMap<>();
private int playerId;
private int playerHandId;
private int playerBattlefieldId;
private int playerDeckId;
private final Map<String, Integer> playerStatBoxMap = new HashMap<>();
private final Map<String, Integer> opponentStatBoxMap = new HashMap<>();
private final Map<Integer, ZoneView<?>> zoneViewMap = new HashMap<>();
private final List<UsableActionMessage> savedMessages = new ArrayList<>();
private final Set<Integer> chosenTargets = new HashSet<>();
private AvailableTargetsMessage targetInfo;
private Consumer<Message> sender;
private IncomingHandler incomingHandler;
public void acceptConnectionSettings(NewGameMessage message, Consumer<Message> sender) {
// this is passed into this object after it is automatically created by the FXML document
this.playerIndex = message.getPlayerIndex();
this.gameId = message.getGameId();
logger.info(String.format("You are player: %d", this.playerIndex));
this.sender = sender;
this.setUpMessageHandling();
}
private void setUpMessageHandling() {
this.incomingHandler = new IncomingHandler();
//incomingHandler.addHandler("login", LoginMessage.class, handlers::loginMessage);
//incomingHandler.addHandler("chat", ChatMessage.class, handlers::chat);
//incomingHandler.addHandler("startgame", StartGameRequest.class, handlers::play);
}
/**
*
* @return Returns the IncomingHandler for use
*/
public IncomingHandler getIncomingHandler() {
return incomingHandler;
}
public void performIncoming(Message message) {
//functionality not yet complete
getIncomingHandler().perform(message, null);
}
public void createAndSendMessage(UsableActionMessage action) {
if (action.isTargetRequired()) {
this.send(new RequestTargetsMessage(gameId, action.getId(), action.getAction()));
} else {
this.send(new UseAbilityMessage(gameId, action.getId(), action.getAction(), action.getTargetId()));
}
//A new list of actions will be sent back from the server, so it is okay to clear them
this.actionBox.getChildren().clear();
this.clearActiveFromAllCards();
}
private void send(Message message) {
this.sender.accept(message);
}
public void processMessageFromServer(Message message) {
//for diagnostics in client
serverMessages.getItems().add(message.toString());
//for diagnostics in console
System.out.println(message.toString());
//this.performIncoming(message);
if (message instanceof WelcomeMessage) {
Platform.runLater(() -> loginMessage.setText(message.toString()));
} else if (message instanceof PlayerMessage) {
this.processPlayerMessage((PlayerMessage)message);
} else if (message instanceof ZoneMessage) {
this.assignZoneIdForZoneMessage((ZoneMessage)message);
} else if (message instanceof CardInfoMessage) {
this.processCardInfoMessage((CardInfoMessage)message);
} else if (message instanceof UsableActionMessage) {
this.savedMessages.add((UsableActionMessage)message);
this.processUseableActionMessage((UsableActionMessage)message);
} else if (message instanceof UpdateMessage) {
this.processUpdateMessage((UpdateMessage)message);
} else if (message instanceof ZoneChangeMessage) {
this.processZoneChangeMessage((ZoneChangeMessage)message);
} else if (message instanceof EntityRemoveMessage) {
this.processEntityRemoveMessage((EntityRemoveMessage)message);
} else if (message instanceof AvailableTargetsMessage) {
this.processAvailableTargetsMessage((AvailableTargetsMessage)message);
} else if (message instanceof ResetAvailableActionsMessage) {
//this.processResetAvailableActionsMessage((ResetAvailableActionsMessage)message);
this.clearSavedActions();
this.unhighlightPlayerTargetRectangles();
} else if (message instanceof ClientDisconnectedMessage) {
this.processClientDisconnectedMessage((ClientDisconnectedMessage)message);
} else if (message instanceof GameOverMessage) {
this.processGameOverMessage((GameOverMessage)message);
}
}
private void processPlayerMessage(PlayerMessage message) {
if (message.getIndex() == this.playerIndex) {
this.playerId = message.getId();
Platform.runLater(() -> this.playerName.setText(message.getName()));
this.processPlayerMessageForPlayer(message, playerStatBox, playerStatBoxMap);
} else {
this.opponentId = message.getId();
Platform.runLater(() -> this.opponentName.setText(message.getName()));
this.processPlayerMessageForPlayer(message, opponentStatBox, opponentStatBoxMap);
Platform.runLater(() -> this.loginMessage.setText("Opponent Connected"));
}
}
private void processPlayerMessageForPlayer(PlayerMessage message, Pane statBox, Map<String, Integer> playerMap) {
statBox.getChildren().clear();
Map<String, Integer> sortedMap = new TreeMap<>(message.getProperties());
playerMap.putAll(sortedMap);
for (Map.Entry<String, Integer> entry : sortedMap.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
Platform.runLater(() -> {
statBox.getChildren().add(new Label(key));
statBox.getChildren().add(new Label(String.format("%d",value)));
});
}
}
private void assignZoneIdForZoneMessage(ZoneMessage message) {
if (!this.zoneViewMap.containsKey(message.getId())) {
if (message.getName().equals("Battlefield")) {
if(message.getOwner() == this.playerId) {
this.playerBattlefieldId = message.getId();
this.zoneViewMap.put(message.getId(), new BattlefieldZoneView(message.getId(), playerBattlefieldPane));
} else {
this.opponentBattlefieldId = message.getId();
this.zoneViewMap.put(message.getId(), new BattlefieldZoneView(message.getId(), opponentBattlefieldPane));
}
} else if (message.getName().equals("Hand")) {
if (message.getOwner() == this.playerId) {
this.playerHandId = message.getId();
this.zoneViewMap.put(message.getId(), new PlayerHandZoneView(message.getId(), playerHandPane));
} else {
this.opponentHandId = message.getId();
this.zoneViewMap.put(this.opponentHandId, new ZoneView<CardView>(message.getId(), opponentHandPane));
this.createOpponentHand(message);
}
} else if (message.getName().equals("Deck")) {
if (message.getOwner() == this.playerId) {
this.playerDeckId = message.getId();
this.deckEntityIds.put(message.getId(), new HashSet<>());
for (int entity : message.getEntities()) {
this.addCardToDeck(playerDeckId, entity);
}
this.repaintDeckLabels();
this.zoneViewMap.put(message.getId(), new ZoneView<CardView>(message.getId(), playerDeckPane));
} else {
this.opponentDeckId = message.getId();
this.deckEntityIds.put(message.getId(), new HashSet<>());
for (int entity : message.getEntities()) {
this.addCardToDeck(opponentDeckId, entity);
}
this.repaintDeckLabels();
this.zoneViewMap.put(message.getId(), new ZoneView<CardView>(message.getId(), opponentDeckPane));
}
}
}
}
private void processCardInfoMessage(CardInfoMessage message) {
int targetZone = message.getZone();
if (targetZone == opponentBattlefieldId) {
this.addCardToOpponentBattlefieldPane(message);
} else if (targetZone == opponentHandId) {
this.addCardToOpponentHandPane(message);
} else if (targetZone == playerBattlefieldId) {
this.addCardToPlayerBattlefieldPane(message);
} else if (targetZone == playerHandId) {
this.addCardToPlayerHandPane(message);
} else {
logger.warn("No known target Zone for " + message);
}
}
private void addCardToOpponentBattlefieldPane(CardInfoMessage message) {
BattlefieldZoneView opponentBattlefield = getZoneView(opponentBattlefieldId);
CardBattlefieldDocumentController card = new CardBattlefieldDocumentController(message, this);
opponentBattlefield.addPane(message.getId(), card);
}
private void addCardToOpponentHandPane(CardInfoMessage message) {
// this is unused because *KNOWN* cards don't pop up in opponent hand without reason (at least not now)
}
private void addCardToPlayerBattlefieldPane(CardInfoMessage message) {
BattlefieldZoneView playerBattlefield = getZoneView(playerBattlefieldId);
CardBattlefieldDocumentController card = new CardBattlefieldDocumentController(message, this);
playerBattlefield.addPane(message.getId(), card);
}
private void addCardToPlayerHandPane(CardInfoMessage message) {
PlayerHandZoneView playerHand = getZoneView(playerHandId);
CardHandDocumentController card = new CardHandDocumentController(message, this);
playerHand.addPane(message.getId(), card);
}
private void processUseableActionMessage(UsableActionMessage message) {
ZoneView<?> zoneView = getZoneViewForCard(message.getId());
logger.info("Usable message: " + message + " inform zone " + zoneView);
if (zoneView == null) {
this.createActionButton(message);
return;
}
if (message.getAction().equals("Attack")) {
((BattlefieldZoneView)zoneView).setCardCanAttack(message.getId(),message);
} else if (message.getAction().equals("Scrap")) {
zoneView.setCardScrappable(message.getId(), message);
} else {
zoneView.setCardActive(message.getId(), message);
}
}
private void processUpdateMessage(UpdateMessage message) {
if (message.getId() == this.playerId) {
this.processUpdateMessageForPlayer(playerStatBox, message, playerStatBoxMap);
} else if (message.getId() == this.opponentId) {
this.processUpdateMessageForPlayer(opponentStatBox, message, opponentStatBoxMap);
} else {
this.processUpdateMessageForCard(message);
}
}
private void processUpdateMessageForPlayer(Pane statBox, UpdateMessage message, Map<String, Integer> playerMap) {
String key = (String)message.getKey();
Integer value = (Integer)message.getValue();
playerMap.put(key, value);
this.repaintStatBox(statBox, playerMap);
}
private void processUpdateMessageForCard(UpdateMessage message) {
ZoneView<?> zoneView = getZoneViewForCard(message.getId());
if (zoneView != null) {
zoneView.updateCard(message.getId(), message);
}
}
private void processZoneChangeMessage(ZoneChangeMessage message) {
int sourceZoneId = message.getSourceZone();
int destinationZoneId = message.getDestinationZone();
int cardId = message.getEntity();
if (sourceZoneId == opponentDeckId) {
this.removeCardFromDeck(sourceZoneId, cardId);
} else if (sourceZoneId == playerDeckId) {
this.removeCardFromDeck(sourceZoneId, cardId);
}
if (destinationZoneId == opponentHandId) {
this.addCardToOpponentHand(cardId);
}
if (destinationZoneId == opponentDeckId || destinationZoneId == playerDeckId) {
this.addCardToDeck(destinationZoneId, cardId);
}
if (this.zoneViewMap.containsKey(sourceZoneId) && this.zoneViewMap.containsKey(destinationZoneId)) {
if (sourceZoneId == playerHandId) {
PlayerHandZoneView sourceZone = getZoneView(sourceZoneId);
CardHandDocumentController card = sourceZone.getCard(cardId);
CardBattlefieldDocumentController newCard = new CardBattlefieldDocumentController(card.getCard(), this);
ZoneView<?> zoneView = getZoneView(destinationZoneId);
if (zoneView instanceof BattlefieldZoneView) {
BattlefieldZoneView destinationZone = getZoneView(destinationZoneId);
destinationZone.addPane(cardId, newCard);
} else if (zoneView instanceof PlayerHandZoneView) {
// TODO: Card moving from battlefield to hand for example, doesn't happen yet
} else {
// Card moving to deck is handled above
}
}
}
if (zoneViewMap.containsKey(sourceZoneId)) {
ZoneView<?> view = zoneViewMap.get(sourceZoneId);
view.removePane(cardId);
}
}
private void addCardToDeck(int zoneId, int cardId) {
logger.info("Add card to deck " + zoneId + " card " + cardId);
Set<Integer> set = this.deckEntityIds.get(zoneId);
set.add(cardId);
this.repaintDeckLabels();
}
private void processEntityRemoveMessage(EntityRemoveMessage message) {
int entityId = message.getEntity();
for (Entry<Integer, Set<Integer>> deckIdsEntry : this.deckEntityIds.entrySet()) {
int deckId = deckIdsEntry.getKey();
Set<Integer> deckIds = deckIdsEntry.getValue();
if (deckIds.contains(entityId)) {
this.removeCardFromDeck(deckId, message.getEntity());
}
}
ZoneView<?> zoneView = getZoneViewForCard(message.getEntity());
if (zoneView != null) {
zoneView.removePane(message.getEntity());
}
}
private void processAvailableTargetsMessage(AvailableTargetsMessage message) {
this.chosenTargets.clear();
this.targetInfo = message;
if (message.getAction().equals("Attack")) {
ZoneView<?> attackerZoneView = getZoneViewForCard(message.getEntity());
if (attackerZoneView != null) {
((BattlefieldZoneView)attackerZoneView).setCardIsAttacking(message.getEntity());
}
}
for (int i = 0; i < message.getTargets().length; i++) {
int target = message.getTargets()[i];
if (target != this.opponentId) {
ZoneView<?> zoneView = getZoneViewForCard(target);
if (zoneView != null) {
zoneView.setCardTargetable(target);
}
} else {
//Create the target box and action for self/opponent targeting
UsableActionMessage newMessage = new UsableActionMessage(message.getEntity(), message.getAction(), false, target);
this.highlightPlayerTargetRectangle(target, newMessage);
}
}
this.createCancelActionsButton();
if (message.getMax() != message.getMin()) {
// This is an action that can have a variant amount of targets. We need a "Done" button to use it for fewer targets
createActionButton("Done", () -> sendActionWithCurrentTargets(message));
}
}
public boolean addTarget(int id) {
if (chosenTargets.isEmpty() && targetInfo.getMax() == 1) {
// Only one target, perform that action with target now
this.createAndSendMessage(new UsableActionMessage(targetInfo.getEntity(), targetInfo.getAction(), false, id));
return false; // Card should not be selected, because we are sending the action directly
}
if (chosenTargets.size() >= targetInfo.getMax()) {
logger.info("Cannot add more targets");
return false;
}
if (chosenTargets.add(id)) {
return true;
} else {
chosenTargets.remove(id);
return false;
}
}
private void sendActionWithCurrentTargets(AvailableTargetsMessage message) {
this.send(new UseAbilityMessage(gameId, message.getEntity(), message.getAction(), chosenTargets.stream().mapToInt(i -> i).toArray()));
this.actionBox.getChildren().clear();
this.clearActiveFromAllCards();
}
private void processClientDisconnectedMessage(ClientDisconnectedMessage message) {
Platform.runLater(() -> this.loginMessage.setText("Opponent Left"));
}
private void processGameOverMessage(GameOverMessage message) {
Platform.runLater(() -> this.loginMessage.setText("Game Over!"));
}
private void removeCardFromDeck(int zoneId, int cardId) {
Set<Integer> set = this.deckEntityIds.get(zoneId);
set.remove(cardId);
this.repaintDeckLabels();
}
private void createActionButton(UsableActionMessage message) {
createActionButton(message.getAction(), () -> createAndSendMessage(message));
}
private void createCancelActionsButton() {
createActionButton("Cancel", () -> cancelAction());
}
private void createActionButton(String label, Runnable action) {
double paneHeight = actionBox.getHeight();
double paneWidth = actionBox.getWidth();
int maxActions = 8;
double actionWidth = paneWidth / maxActions;
ActionButton actionButton = new ActionButton(label, actionWidth, paneHeight, action);
actionBox.getChildren().add(actionButton);
}
private void clearSavedActions() {
this.savedMessages.clear();
this.actionBox.getChildren().clear();
this.zoneViewMap.get(this.playerBattlefieldId).removeActiveAllCards();
}
public void cancelAction() {
this.clearActiveFromAllCards();
this.unhighlightPlayerTargetRectangles();
this.actionBox.getChildren().clear();
for (UsableActionMessage message : this.savedMessages) {
this.processUseableActionMessage(message);
}
}
private void clearActiveFromAllCards() {
for (ZoneView<?> zoneView : this.zoneViewMap.values()) {
zoneView.removeActiveAllCards();
zoneView.removeScrappableAllCards();
}
}
private void repaintStatBox(Pane statBox, Map<String, Integer> playerMap) {
statBox.getChildren().clear();
for (Map.Entry<String, Integer> entry : playerMap.entrySet()) {
String key = entry.getKey();
statBox.getChildren().add(new Label(key));
int value = entry.getValue();
statBox.getChildren().add(new Label(String.format("%d",value)));
}
}
private void repaintDeckLabels() {
if (this.deckEntityIds.containsKey(opponentDeckId)) {
this.opponentDeckLabel.setText(String.format("%d", this.deckEntityIds.get(opponentDeckId).size()));
}
if (this.deckEntityIds.containsKey(playerDeckId)) {
this.playerDeckLabel.setText(String.format("%d", this.deckEntityIds.get(playerDeckId).size()));
}
}
private void highlightPlayerTargetRectangle(int playerId, UsableActionMessage message) {
if (playerId == this.playerId) {
this.playerTargetRectangle.setVisible(true);
this.playerTargetRectangle.setOnMouseClicked(e -> this.clickToTargetPlayer(e, message));
} else if (playerId == this.opponentId) {
this.opponentTargetRectangle.setVisible(true);
this.opponentTargetRectangle.setOnMouseClicked(e -> this.clickToTargetPlayer(e, message));
}
}
private void unhighlightPlayerTargetRectangles() {
this.playerTargetRectangle.setVisible(false);
this.opponentTargetRectangle.setVisible(false);
}
private void clickToTargetPlayer(MouseEvent e, UsableActionMessage message) {
this.createAndSendMessage(message);
}
private void createOpponentHand(ZoneMessage message) {
for (int i : message.getEntities()) {
this.addCardToOpponentHand(i);
}
}
private void addCardToOpponentHand(int i) {
ZoneView<?> opponentHand = this.zoneViewMap.get(this.opponentHandId);
opponentHand.addSimplePane(i, this.cardForOpponentHand());
}
private Pane cardForOpponentHand() {
double paneHeight = opponentHandPane.getHeight();
double paneWidth = opponentHandPane.getWidth();
int maxCards = 10;
double cardWidth = paneWidth / maxCards;
Pane card = new Pane();
Rectangle cardBack = new Rectangle(0,0,cardWidth,paneHeight);
cardBack.setFill(Color.AQUAMARINE);
card.getChildren().add(cardBack);
return card;
}
public void closeWindow() {
Node source = this.rootPane;
Stage stage = (Stage)source.getScene().getWindow();
stage.close();
}
@SuppressWarnings("unchecked")
private <T extends ZoneView<?>> T getZoneView(int id) {
return (T) this.zoneViewMap.get(id);
}
private ZoneView<?> getZoneViewForCard(int id) {
for (ZoneView<?> zoneView : this.zoneViewMap.values()) {
if (zoneView.contains(id)) {
return zoneView;
}
}
return null;
}
public void closeGame() {
this.send(new ChatMessage(1, "unused", "(Ends game " + gameId + ")"));
// run on window close
}
}