Initial commit
This commit is contained in:
63
server/build.gradle
Normal file
63
server/build.gradle
Normal file
@@ -0,0 +1,63 @@
|
||||
apply plugin: 'application'
|
||||
|
||||
|
||||
java.sourceCompatibility = 21
|
||||
java.targetCompatibility = 21
|
||||
if (JavaVersion.current().isJava9Compatible()) {
|
||||
compileJava.options.release.set(21)
|
||||
}
|
||||
|
||||
mainClassName = 'com.ghost.technic.server.ServerLauncher'
|
||||
application.setMainClass(mainClassName)
|
||||
eclipse.project.name = appName + '-server'
|
||||
|
||||
dependencies {
|
||||
implementation 'org.projectlombok:lombok:1.18.38'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||
|
||||
testImplementation 'org.projectlombok:lombok:1.18.38'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.18'
|
||||
annotationProcessor 'ch.qos.logback:logback-classic:1.5.18'
|
||||
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.5.18'
|
||||
testAnnotationProcessor 'ch.qos.logback:logback-classic:1.5.18'
|
||||
|
||||
testImplementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
// testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
|
||||
// testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
|
||||
testImplementation 'org.junit.platform:junit-platform-launcher:1.13.1'
|
||||
testImplementation 'org.junit.platform:junit-platform-engine:1.13.1'
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveBaseName.set(appName)
|
||||
// the duplicatesStrategy matters starting in Gradle 7.0; this setting works.
|
||||
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
||||
dependsOn configurations.runtimeClasspath
|
||||
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
// these "exclude" lines remove some unnecessary duplicate files in the output JAR.
|
||||
exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
|
||||
dependencies {
|
||||
exclude('META-INF/INDEX.LIST', 'META-INF/maven/**')
|
||||
}
|
||||
// setting the manifest makes the JAR runnable.
|
||||
manifest {
|
||||
attributes 'Main-Class': project.mainClassName
|
||||
}
|
||||
// this last step may help on some OSes that need extra instruction to make runnable JARs.
|
||||
doLast {
|
||||
file(archiveFile).setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Equivalent to the jar task; here for compatibility with gdx-setup.
|
||||
task dist(dependsOn: [jar]) {
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
163
server/src/main/java/com/ghost/technic/server/Player.java
Normal file
163
server/src/main/java/com/ghost/technic/server/Player.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package com.ghost.technic.server;
|
||||
|
||||
import com.ghost.technic.server.board.PlayArea;
|
||||
import com.ghost.technic.server.card.impl.ArchonIdentityCard;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightEffect;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class Player {
|
||||
private String name;
|
||||
private int amber;
|
||||
private int keysForged;
|
||||
@Builder.Default
|
||||
private PlayArea playArea = new PlayArea();
|
||||
@Builder.Default
|
||||
private LinkedList<PlayableCard> deck = new LinkedList<>();
|
||||
@Builder.Default
|
||||
private LinkedList<PlayableCard> discardPile = new LinkedList<>();
|
||||
@Builder.Default
|
||||
private List<PlayableCard> hand = new ArrayList<>();
|
||||
@Builder.Default
|
||||
private List<PlayableCard> archives = new ArrayList<>();
|
||||
private ArchonIdentityCard archonIdentityCard;
|
||||
private House activeHouse;
|
||||
|
||||
public Player(Player player) {
|
||||
this.name = player.getName();
|
||||
this.amber = player.getAmber();
|
||||
this.keysForged = player.getKeysForged();
|
||||
this.playArea = player.getPlayArea();
|
||||
this.deck = player.getDeck();
|
||||
this.discardPile = player.getDiscardPile();
|
||||
this.hand = player.getHand();
|
||||
this.archives = player.getArchives();
|
||||
this.archonIdentityCard = player.getArchonIdentityCard();
|
||||
this.activeHouse = player.getActiveHouse();
|
||||
}
|
||||
|
||||
public Set<House> getAvailableHouses() {
|
||||
var results = new HashSet<House>();
|
||||
results.addAll(archonIdentityCard.getHouses());
|
||||
results.addAll(playArea.getAllUsableCardsHouse());
|
||||
return results;
|
||||
}
|
||||
|
||||
public void drawCards(int count) {
|
||||
for (int i = 0; i < count && !deck.isEmpty(); i++) {
|
||||
hand.add(deck.remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
public void drawCardsToSix() {
|
||||
// while (hand.size() < 6) {
|
||||
while (hand.size() < 6 && !deck.isEmpty()) {
|
||||
drawCards(1);
|
||||
}
|
||||
}
|
||||
|
||||
public void forgeKey(int keyCost) {
|
||||
if (amber >= keyCost) {
|
||||
amber -= keyCost;
|
||||
keysForged++;
|
||||
log.info("{} forges a key at cost {}! Keys forged: {}", name, keyCost, keysForged);
|
||||
} else {
|
||||
log.info("{} cannot afford a key (needs {}, has {})", name, keyCost, amber);
|
||||
}
|
||||
}
|
||||
|
||||
public House chooseHouse(Set<House> house) {
|
||||
// Placeholder for UI, use override for testing
|
||||
log.info("{} chooses house: {}", name, activeHouse);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void pickUpArchives() {
|
||||
if (!archives.isEmpty()) {
|
||||
hand.addAll(archives);
|
||||
archives.clear();
|
||||
log.info("{} picked up archives.", name);
|
||||
}
|
||||
}
|
||||
|
||||
public void readyCards() {
|
||||
for (CreatureCard creatureCard : playArea.getBattleline().getCreatures()) {
|
||||
creatureCard.setReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayableCard takeTopCardOfDeck() {
|
||||
if (!deck.isEmpty()) {
|
||||
var topCard = deck.getFirst();
|
||||
deck.remove(topCard);
|
||||
return topCard;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void fightWithCreature(CreatureCard creature, CreatureCard target) {
|
||||
if (!creature.isReady()) return;
|
||||
creature.beforeFight();
|
||||
target.beforeFight();
|
||||
|
||||
log.info(name + "'s " + creature.getName() + " fights " + target.getName());
|
||||
|
||||
// target.takeDamage(creature.);
|
||||
// creature.takeDamage(target.getPower());
|
||||
|
||||
creature.afterFight();
|
||||
target.afterFight();
|
||||
|
||||
creature.setReady(false);
|
||||
removeDestroyedCreatures();
|
||||
}
|
||||
|
||||
public void reapWithCreature(CreatureCard creature, EventBus eventBus) {
|
||||
if (!creature.isReady()) {
|
||||
log.info("Creature is exhausted, cannot reap");
|
||||
return;
|
||||
}
|
||||
creature.beforeReap();
|
||||
// Perform the actual reap logic (creature gains 1 amber, exhausts, etc.)
|
||||
creature.reap(this, eventBus);
|
||||
amber++;
|
||||
log.info("{} reaps with {} and gains 1 amber.", name, creature.getName());
|
||||
creature.afterReap();
|
||||
creature.setReady(false);
|
||||
}
|
||||
|
||||
public List<BeforeFightEffect> chooseBeforeFightOrder(List<BeforeFightEffect> effects) {
|
||||
// 🔧 TEMP: Default to resolving in order as-is (assault first, then hazardous)
|
||||
// Later: prompt human or AI to choose order
|
||||
return effects;
|
||||
}
|
||||
|
||||
public CreatureCard chooseCreature(List<CreatureCard> options, String reason) {
|
||||
// Placeholder for UI, use override for testing
|
||||
log.info("[chooseCreature] {} selects creature for: {}", getName(), reason);
|
||||
return options.stream().findFirst().orElse(null); // pick first for now
|
||||
}
|
||||
|
||||
public void removeDestroyedCreatures() {
|
||||
playArea.getBattleline().getCreatures().removeIf(card -> {
|
||||
if (card instanceof CreatureCard creature && creature.isDestroyed()) {
|
||||
log.info("{}'s {} is removed from battleline.", name, creature.getName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ghost.technic.server;
|
||||
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
/** Launches the server application. */
|
||||
public class ServerLauncher {
|
||||
public static void main(String[] args) {
|
||||
EventBus eventBus = new EventBus();
|
||||
GameState gameState = new GameState(eventBus);
|
||||
gameState.setup();
|
||||
gameState.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ghost.technic.server.board;
|
||||
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.*;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Battleline {
|
||||
private LinkedList<CreatureCard> creatures = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Adds a creature to the specified flank (LEFT or RIGHT).
|
||||
*/
|
||||
public void addToFlank(CreatureCard creature, Flank preferredFlank) {
|
||||
if (creatures.isEmpty()) {
|
||||
creatures.add(creature);
|
||||
} else {
|
||||
switch (preferredFlank) {
|
||||
case LEFT -> creatures.addFirst(creature);
|
||||
case RIGHT -> creatures.addLast(creature);
|
||||
default -> throw new IllegalArgumentException("Unknown flank: " + preferredFlank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the immediate neighbors of the given creature in the battleline.
|
||||
*/
|
||||
public List<CreatureCard> getNeighbors(CreatureCard creature) {
|
||||
int index = creatures.indexOf(creature);
|
||||
List<CreatureCard> neighbors = new ArrayList<>();
|
||||
|
||||
if (index > 0) {
|
||||
neighbors.add(creatures.get(index - 1));
|
||||
}
|
||||
if (index >= 0 && index < creatures.size() - 1) {
|
||||
neighbors.add(creatures.get(index + 1));
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the creature is at the left or right flank.
|
||||
* First creature is considered both left and right flank.
|
||||
*/
|
||||
public Set<Flank> getEffectiveFlanks(CreatureCard creature) {
|
||||
Set<Flank> flanks = new HashSet<>();
|
||||
int index = creatures.indexOf(creature);
|
||||
|
||||
if (index == 0) {
|
||||
flanks.add(Flank.LEFT);
|
||||
}
|
||||
if (index == creatures.size() - 1) {
|
||||
flanks.add(Flank.RIGHT);
|
||||
}
|
||||
|
||||
// If it’s the only creature, it’s left, right and center
|
||||
if (creatures.size() == 1 && index == 0) {
|
||||
flanks.add(Flank.LEFT);
|
||||
flanks.add(Flank.CENTER);
|
||||
flanks.add(Flank.RIGHT);
|
||||
}
|
||||
|
||||
return flanks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ghost.technic.server.board;
|
||||
|
||||
public enum Flank {
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ghost.technic.server.board;
|
||||
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.ArtifactCard;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class PlayArea {
|
||||
private Battleline battleline = new Battleline();
|
||||
private List<ArtifactCard> artifactCards = new ArrayList<>();
|
||||
|
||||
public Set<House> getAllUsableCardsHouse() {
|
||||
var results = new HashSet<House>();
|
||||
var creatures = getBattleline().getCreatures().stream().map(PlayableCard::getHouse).collect(Collectors.toSet());
|
||||
var artifact = artifactCards.stream().map(PlayableCard::getHouse).collect(Collectors.toSet());
|
||||
results.addAll(creatures);
|
||||
results.addAll(artifact);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
26
server/src/main/java/com/ghost/technic/server/card/Card.java
Normal file
26
server/src/main/java/com/ghost/technic/server/card/Card.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.misc.CardType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@ToString(onlyExplicitlyIncluded = true)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public abstract class Card {
|
||||
|
||||
@ToString.Include
|
||||
@EqualsAndHashCode.Include
|
||||
private final UUID id = UUID.randomUUID();
|
||||
|
||||
@ToString.Include
|
||||
@EqualsAndHashCode.Include
|
||||
private String name;
|
||||
|
||||
private CardType cardType;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
|
||||
public interface ActivatedAbility {
|
||||
void activate(GameState gameState, Player controller, PlayableCard source);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public interface CardAbility {
|
||||
void register(GameActionService gameActionService, Player controller, PlayableCard source);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
|
||||
public interface ConstantAbility {
|
||||
|
||||
void apply();
|
||||
void remove();
|
||||
|
||||
default boolean canReap(CreatureCard creature, Player actingPlayer, GameState gameState) {
|
||||
return true; // by default, allow reaping
|
||||
}
|
||||
|
||||
default boolean canFight(CreatureCard creature, Player actingPlayer, GameState gameState) {
|
||||
return true; // by default, allow reaping
|
||||
}
|
||||
|
||||
default int keyCostModifier(Player forPlayer, GameState gameState) {
|
||||
return 0; // By default, no change
|
||||
}
|
||||
|
||||
default int armorBonus(CreatureCard target, GameState state) {
|
||||
return 0; // By default, no change
|
||||
}
|
||||
|
||||
default boolean blanksTextBox(PlayableCard card) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
|
||||
public interface ConstraintEffect {
|
||||
boolean canReap(CreatureCard creature, Player controller, GameState gameState);
|
||||
boolean canFight(CreatureCard creature, Player controller, GameState gameState);
|
||||
boolean canUse(CreatureCard creature, Player controller, GameState gameState);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
public interface StartOfOpponentTurnAbility extends StartOfTurnAbility {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public interface StartOfTurnAbility extends CardAbility {
|
||||
void onStartOfTurn(GameActionService gameActionService, Player activePlayer, PlayableCard source);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ghost.technic.server.card.effect;
|
||||
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class StaticEffectData {
|
||||
private final CreatureCard joya;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.event.AmberChangeType;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class AllusionsOfGrandeurAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService service, Player controller, PlayableCard source) {
|
||||
Player opponent = service.getGameState().getOpponentOf(controller);
|
||||
House chosen = controller.chooseHouse(opponent.getArchonIdentityCard().getHouses());
|
||||
|
||||
service.getGameState().getEventBus().subscribe(EventType.HOUSE_CHOSEN, event -> {
|
||||
Player player = event.get("player");
|
||||
House house = event.get("house");
|
||||
|
||||
if (player.equals(opponent) && house != chosen) {
|
||||
service.adjustAmber(controller, 3, AmberChangeType.GAINED, source);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
|
||||
public class BarristerJoyaAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof CreatureCard joya)) return;
|
||||
|
||||
BarristerJoyaConstantAbility effect = new BarristerJoyaConstantAbility(joya);
|
||||
effect.apply();
|
||||
gameActionService.getGameState().registerStaticEffect(joya, effect);
|
||||
|
||||
gameActionService.getGameState().getEventBus().subscribe(EventType.CREATURE_DESTROYED, event -> {
|
||||
CreatureCard destroyed = event.get("creature");
|
||||
if (destroyed.equals(joya)) {
|
||||
effect.remove();
|
||||
gameActionService.getGameState().unregisterStaticEffect(joya);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BarristerJoyaConstantAbility implements ConstantAbility {
|
||||
|
||||
private final CreatureCard joya;
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
log.info("[apply] Barrister Joya’s effect active: enemy creatures cannot reap");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
log.info("[remove] Barrister Joya’s effect removed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canReap(CreatureCard creature, Player actingPlayer, GameState gameState) {
|
||||
Player joyaOwner = gameState.getOwnerOf(joya);
|
||||
Player creatureOwner = gameState.getOwnerOf(creature);
|
||||
|
||||
if (creatureOwner == null) {
|
||||
log.warn("[canReap] No owner found for creature: {}", creature.getName());
|
||||
}
|
||||
|
||||
return creatureOwner == null || creatureOwner.equals(joyaOwner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return joya.getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class BlindingLightAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
House chosen = controller.chooseHouse(House.getAllHouses());
|
||||
log.info("{} chooses {} for Blinding Light", controller.getName(), chosen);
|
||||
|
||||
List<CreatureCard> targets = gameActionService.getGameState().getAllCreatures().stream()
|
||||
.filter(c -> c.getHouse() == chosen)
|
||||
.toList();
|
||||
|
||||
for (CreatureCard creature : targets) {
|
||||
creature.getCounters().addStun(); // assumes a stun counter is supported
|
||||
log.info("{} is stunned by Blinding Light", creature.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class BulwarkAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof CreatureCard bulwark)) return;
|
||||
|
||||
BulwarkConstantAbility effect = new BulwarkConstantAbility(
|
||||
bulwark,
|
||||
controller.getPlayArea().getBattleline()
|
||||
);
|
||||
|
||||
gameActionService.getGameState().registerStaticEffect(bulwark, effect);
|
||||
|
||||
log.info("Registered Bulwark ability for {}", bulwark.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.board.Battleline;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BulwarkConstantAbility implements ConstantAbility {
|
||||
|
||||
private final CreatureCard bulwark;
|
||||
private final Battleline battleline;
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
log.info("[apply] Bulwark’s effect active: +2 armor to neighbours");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
log.info("[remove] Bulwark’s effect active: +2 armor to neighbours");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int armorBonus(CreatureCard target, GameState state) {
|
||||
if (!battleline.getCreatures().contains(bulwark)) return 0;
|
||||
List<CreatureCard> neighbors = battleline.getNeighbors(bulwark);
|
||||
return neighbors.contains(target) ? 2 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.AmberChangeType;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import com.ghost.technic.server.game.event.UsageType;
|
||||
import com.ghost.technic.server.game.event.UsedEventData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public class DexusTriggeredAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof CreatureCard dexus)) return;
|
||||
|
||||
GameState gameState = gameActionService.getGameState();
|
||||
|
||||
// gameState.getEventBus().subscribe(EventType.CREATURE_ENTERED_PLAY, event -> {
|
||||
gameState.getEventBus().subscribe(EventType.USED, event -> {
|
||||
UsedEventData usedData = event.get("usedEventData");
|
||||
PlayableCard playedCard = usedData.getCard();
|
||||
var usedType = usedData.getUsageType();
|
||||
|
||||
if (usedType.equals(UsageType.PLAY)) {
|
||||
Player playedBy = event.get("player");
|
||||
Set<Flank> flanks = event.get("flanks");
|
||||
|
||||
if (playedBy.equals(gameState.getOwnerOf(dexus))) return;
|
||||
|
||||
if (flanks.contains(Flank.RIGHT)) {
|
||||
gameActionService.adjustAmber(playedBy, -1, AmberChangeType.LOST, dexus);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.StartOfTurnAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class DirectorZYXAbility implements StartOfTurnAbility {
|
||||
|
||||
@Override
|
||||
public void onStartOfTurn(GameActionService gameActionService, Player activePlayer, PlayableCard source) {
|
||||
gameActionService.archiveCard(activePlayer.takeTopCardOfDeck(), activePlayer);
|
||||
log.info("Director of Z.Y.X. archives top card of {}", activePlayer.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
// no-op: triggered through GameTurnManager
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class MurmookAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService game, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof CreatureCard murmook)) return;
|
||||
|
||||
MurmookEffect effect = new MurmookEffect(controller);
|
||||
effect.apply();
|
||||
game.getGameState().registerStaticEffect(murmook, effect);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class MurmookEffect implements ConstantAbility {
|
||||
|
||||
private final Player controller;
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
log.info("Murmook effect active: opponent's keys cost +1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
log.info("Murmook effect removed.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int keyCostModifier(Player forPlayer, GameState state) {
|
||||
Player opponent = state.getOpponentOf(controller);
|
||||
return forPlayer.equals(opponent) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import com.ghost.technic.server.game.event.UsageType;
|
||||
import com.ghost.technic.server.game.event.UsedEventData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class MusthicMurmookAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService game, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof CreatureCard musthicMurmook)) return;
|
||||
|
||||
// Key cost static effect
|
||||
MusthicMurmookEffect effect = new MusthicMurmookEffect();
|
||||
effect.apply();
|
||||
game.getGameState().registerStaticEffect(musthicMurmook, effect);
|
||||
|
||||
game.getGameState().getEventBus().subscribe(EventType.USED, event -> {
|
||||
UsedEventData usedData = event.get("usedEventData");
|
||||
PlayableCard playedCard = usedData.getCard();
|
||||
var usedType = usedData.getUsageType();
|
||||
|
||||
if (playedCard.equals(musthicMurmook) && usedType.equals(UsageType.PLAY)) {
|
||||
List<CreatureCard> choices = game.getGameState().getAllCreaturesInPlay();
|
||||
CreatureCard target = controller.chooseCreature(choices, "Musthic Murmook: Deal 4 damage");
|
||||
|
||||
if (target != null) {
|
||||
game.dealDamage(target, 4);
|
||||
log.info("Musthic Murmook deals 4 damage to {}", target.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class MusthicMurmookEffect implements ConstantAbility {
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
log.info("Musthic Murmook effect active: everyone’s keys cost +1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
log.info("Musthic Murmook effect removed.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int keyCostModifier(Player forPlayer, GameState state) {
|
||||
return 1; // Affects both players
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class PersistenceHuntingAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
House chosen = controller.chooseHouse(House.getAllHouses());
|
||||
log.info("{} chooses {} for Persistence Hunting", controller.getName(), chosen);
|
||||
|
||||
Player opponent = gameActionService.getGameState().getOpponentOf(controller);
|
||||
List<CreatureCard> targets = opponent.getPlayArea().getBattleline().getCreatures().stream()
|
||||
.filter(creature -> creature.getHouse() == chosen)
|
||||
.toList();
|
||||
|
||||
for (CreatureCard target : targets) {
|
||||
target.setReady(false); // Exhaust
|
||||
log.info("{} is exhausted by Persistence Hunting", target.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightAbility;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightEffect;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class RainceFuryheartAbility implements CardAbility, BeforeFightAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService game, Player controller, PlayableCard source) {
|
||||
// Maybe static effects or triggers — not needed here
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<BeforeFightEffect> getBeforeFightEffect(CreatureCard self, CreatureCard opponent, GameActionService service) {
|
||||
return Optional.of(new BeforeFightEffect(
|
||||
"Exalt the creature " + self.getName() + " fights",
|
||||
() -> {
|
||||
log.info("{} Exalting {}",self.getName(), opponent.getName());
|
||||
opponent.getCounters().addAmber(1); // or however you represent exalt
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ScreeyanAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService service, Player controller, PlayableCard source) {
|
||||
service.getEventBus().subscribe(EventType.TURN_ENDS, event -> {
|
||||
Player current = event.get("player");
|
||||
Player opponent = service.getGameState().getOpponentOf(current);
|
||||
|
||||
PlayableCard discardedCard = service.discordTopCardOfDeck(opponent);
|
||||
if (discardedCard != null) {
|
||||
House discardedHouse = discardedCard.getHouse();
|
||||
log.info("[Screeyan] {} discarded. {} may not choose {} next turn.",
|
||||
discardedCard.getName(), opponent.getName(), discardedHouse);
|
||||
|
||||
service.getGameState().restrictHouseNextTurn(opponent, discardedHouse);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.StartOfOpponentTurnAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class SibylWaimareAbility implements StartOfOpponentTurnAbility {
|
||||
|
||||
@Override
|
||||
public void onStartOfTurn(GameActionService gameActionService, Player activePlayer, PlayableCard source) {
|
||||
Player opponent = gameActionService.getGameState().getOwnerOf(source);
|
||||
var discardedCard = gameActionService.discordTopCardOfDeck(activePlayer);
|
||||
|
||||
if (discardedCard instanceof PlayableCard playCard) {
|
||||
House house = playCard.getHouse();
|
||||
List<CreatureCard> creatures = activePlayer.getPlayArea().getBattleline().getCreatures();
|
||||
for (CreatureCard c : creatures) {
|
||||
if (c.getHouse() == house) {
|
||||
c.setReady(false); // exhaust
|
||||
log.info("Sibyl Waimare exhausts {}", c.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(GameActionService gameActionService, Player controller, PlayableCard source) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.impl.UpgradeCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class WayOfTheBearAbility implements CardAbility {
|
||||
|
||||
@Override
|
||||
public void register(GameActionService game, Player controller, PlayableCard source) {
|
||||
if (!(source instanceof UpgradeCard upgrade)) return;
|
||||
|
||||
CreatureCard target = findAttachedCreature(game.getGameState(), upgrade);
|
||||
if (target == null) return;
|
||||
|
||||
var effect = new WayOfTheBearEffect(target);
|
||||
effect.apply();
|
||||
|
||||
game.getGameState().registerStaticEffect(upgrade, effect);
|
||||
}
|
||||
|
||||
private CreatureCard findAttachedCreature(GameState gameState, UpgradeCard upgrade) {
|
||||
for (Player player : List.of(gameState.getPlayerOne(), gameState.getPlayerTwo())) {
|
||||
for (CreatureCard creature : player.getPlayArea().getBattleline().getCreatures()) {
|
||||
if (creature.getUpgrades().contains(upgrade)) return creature;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ghost.technic.server.card.effect.impl;
|
||||
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.card.keyword.Keyword;
|
||||
import com.ghost.technic.server.card.keyword.KeywordAbility;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class WayOfTheBearEffect implements ConstantAbility {
|
||||
|
||||
private final CreatureCard target;
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
target.getKeywords().add(new Keyword(KeywordAbility.ASSAULT, 2));
|
||||
log.info("[Way of the Bear] {} gains Assault 2", target.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
target.getKeywords().removeIf(k -> k.getKeywordAbility() == KeywordAbility.ASSAULT && k.getValue() == 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ghost.technic.server.card.impl;
|
||||
|
||||
import com.ghost.technic.server.card.Card;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
@SuperBuilder
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArchonIdentityCard extends Card {
|
||||
private Set<House> houses;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BeforeFightAbility {
|
||||
Optional<BeforeFightEffect> getBeforeFightEffect(CreatureCard self, CreatureCard opponent, GameActionService service);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
public record BeforeFightEffect(String description, Runnable action) {}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class Keyword {
|
||||
|
||||
private KeywordAbility keywordAbility;
|
||||
private int value;
|
||||
|
||||
public Keyword(KeywordAbility keywordAbility) {
|
||||
this.keywordAbility = keywordAbility;
|
||||
this.value = 0;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return keywordAbility.hasValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return hasValue() ? keywordAbility.name() + " (" + value + ")" : keywordAbility.name();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
public enum KeywordAbility {
|
||||
ALPHA,
|
||||
ASSAULT, // has value
|
||||
DEPLOY,
|
||||
ELUSIVE,
|
||||
HAZARDOUS, // has value
|
||||
INVULNERABLE,
|
||||
OMEGA,
|
||||
POISON,
|
||||
SKIRMISH,
|
||||
SPLASH_ATTACK, // has value
|
||||
TAUNT,
|
||||
TREACHERY,
|
||||
VERSATILE;
|
||||
|
||||
public boolean hasValue() {
|
||||
return switch (this) {
|
||||
case ASSAULT, HAZARDOUS, SPLASH_ATTACK -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public interface KeywordEffect {
|
||||
default void onFight(GameActionService service, CreatureCard attacker, CreatureCard defender) {}
|
||||
default void onReap(GameActionService service, CreatureCard creature) {}
|
||||
default void onPlay(GameActionService service, PlayableCard card, Player controller) {}
|
||||
default void onDestroyed(GameActionService service, CreatureCard creature) {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ghost.technic.server.card.keyword;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.impl.*;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class KeywordEffectRegistry {
|
||||
private final Map<KeywordAbility, KeywordEffect> registry = new EnumMap<>(KeywordAbility.class);
|
||||
|
||||
public KeywordEffectRegistry() {
|
||||
registry.put(KeywordAbility.SKIRMISH, new SkirmishEffect());
|
||||
registry.put(KeywordAbility.ELUSIVE, new ElusiveEffect());
|
||||
registry.put(KeywordAbility.POISON, new PoisonEffect());
|
||||
registry.put(KeywordAbility.ALPHA, new AlphaEffect());
|
||||
// registry.put(KeywordAbility.DESTROYED, new DestroyedEffect());
|
||||
// registry.put(KeywordAbility.SCRAP, new ScrapEffect());
|
||||
// registry.put(KeywordAbility.OMNI, new OmniEffect());
|
||||
registry.put(KeywordAbility.DEPLOY, new DeployEffect());
|
||||
}
|
||||
|
||||
public Optional<KeywordEffect> getEffect(KeywordAbility keywordAbility) {
|
||||
return Optional.ofNullable(registry.get(keywordAbility));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class AlphaEffect implements KeywordEffect {
|
||||
// @Override
|
||||
// public void onPlay(GameActionService service, PlayableCard card, Player controller) {
|
||||
// if (!service.isFirstCardPlayedThisTurn(card)) {
|
||||
// throw new IllegalStateException("Alpha cards must be the first card played on your turn.");
|
||||
// }
|
||||
// }
|
||||
@Override
|
||||
public void onPlay(GameActionService service, PlayableCard card, Player controller) {
|
||||
if (!service.isFirstCardPlayedThisTurn(card)) {
|
||||
service.flagRuleViolation(card, "Alpha cards must be the first card played on your turn.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
|
||||
public class DeployEffect implements KeywordEffect {
|
||||
// Deploy allows placement anywhere on the battleline (not just flanks)
|
||||
// Usually checked during placement, not as a triggered effect
|
||||
// GameActionService should consult card.hasKeyword(DEPLOY) when positioning
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class DestroyedEffect implements KeywordEffect {
|
||||
@Override
|
||||
public void onDestroyed(GameActionService service, CreatureCard creature) {
|
||||
// Trigger destroyed abilities
|
||||
service.resolveDestroyedAbilities(creature);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class ElusiveEffect implements KeywordEffect {
|
||||
@Override
|
||||
public void onFight(GameActionService service, CreatureCard attacker, CreatureCard defender) {
|
||||
// First time attacked, Elusive prevents all damage to defender
|
||||
if (service.isFirstAttackAgainst(defender)) {
|
||||
service.preventDamageTo(defender);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
|
||||
public class OmniEffect implements KeywordEffect {
|
||||
// Omni is more of a tag that allows abilities to be used regardless of house
|
||||
// Effects using this should check card.hasKeyword(OMNI) before checking house
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class PoisonEffect implements KeywordEffect {
|
||||
@Override
|
||||
public void onFight(GameActionService service, CreatureCard attacker, CreatureCard defender) {
|
||||
// Any damage dealt by this creature destroys the other
|
||||
service.markAsPoisonous(attacker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class ScrapEffect implements KeywordEffect {
|
||||
@Override
|
||||
public void onPlay(GameActionService service, PlayableCard card, Player controller) {
|
||||
if (!service.isPlayedFromDiscard(card)) {
|
||||
throw new IllegalStateException("Scrap effects only trigger when played from discard.");
|
||||
}
|
||||
service.resolveScrapEffect(card);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ghost.technic.server.card.keyword.impl;
|
||||
|
||||
import com.ghost.technic.server.card.keyword.KeywordEffect;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
|
||||
public class SkirmishEffect implements KeywordEffect {
|
||||
@Override
|
||||
public void onFight(GameActionService service, CreatureCard attacker, CreatureCard defender) {
|
||||
// Skirmish: This creature takes no damage when it is used to fight
|
||||
service.ignoreRetaliatoryDamage(attacker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
@Data
|
||||
public class BonusIcons {
|
||||
private LinkedList<IBonusIcon> bonusIcons;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
public enum CardType {
|
||||
ARCHON_IDENTITY,
|
||||
CREATURE,
|
||||
ARTIFACT,
|
||||
ACTION,
|
||||
UPGRADE,
|
||||
TIDE,
|
||||
PROPHECY,
|
||||
ARCHON_POWER
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public enum House {
|
||||
BROBNAR,
|
||||
DIS,
|
||||
EKWIDON,
|
||||
GEISTOID,
|
||||
LOGOS,
|
||||
MARS,
|
||||
REDEMPTION,
|
||||
SANCTUM,
|
||||
SAURIAN,
|
||||
SHADOWS,
|
||||
SKYBORN,
|
||||
STAR_ALLIANCE,
|
||||
UNFATHOMABLE,
|
||||
UNTAMED;
|
||||
|
||||
public static Set<House> getAllHouses() {
|
||||
return Set.of(values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class HouseBonusIcon implements IBonusIcon {
|
||||
private final House house;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
public interface IBonusIcon {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ghost.technic.server.card.misc;
|
||||
|
||||
public enum StandardBonusIcon implements IBonusIcon {
|
||||
ÆMBER,
|
||||
CAPTURE,
|
||||
DAMAGE,
|
||||
DRAW,
|
||||
DISCARD
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ghost.technic.server.card.playable;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.Card;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.misc.BonusIcons;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.game.event.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@SuperBuilder
|
||||
@ToString(callSuper = true, onlyExplicitlyIncluded = true)
|
||||
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
|
||||
public abstract class PlayableCard extends Card {
|
||||
@ToString.Include
|
||||
private House house;
|
||||
private BonusIcons bonusIcons;
|
||||
@Builder.Default
|
||||
private List<CardAbility> abilities = new ArrayList<>();
|
||||
|
||||
public void play(Player player, EventBus eventBus) {
|
||||
publishUsedEvent(player, UsageType.PLAY, eventBus);
|
||||
}
|
||||
|
||||
public void publishUsedEvent(Player player, UsageType usageType, EventBus eventBus) {
|
||||
UsedEventData usedData = new UsedEventData(this, usageType);
|
||||
Map<String, Object> payload = Map.of("usedEventData", usedData);
|
||||
GameEvent usedEvent = new GameEvent(EventType.USED, payload);
|
||||
eventBus.publish(usedEvent);
|
||||
log.info("{} {} {}", player.getName(), usageType, getName());
|
||||
}
|
||||
|
||||
public void addAbility(CardAbility ability) {
|
||||
abilities.add(ability);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ghost.technic.server.card.playable.impl;
|
||||
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActionCard extends PlayableCard {
|
||||
// No additional fields for now
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ghost.technic.server.card.playable.impl;
|
||||
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
|
||||
public class UpgradeCard extends PlayableCard {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ghost.technic.server.card.playable.usable;
|
||||
|
||||
public interface Usable {
|
||||
boolean isReady();
|
||||
void setReady(boolean ready);
|
||||
|
||||
default void exhaust() {
|
||||
setReady(false);
|
||||
}
|
||||
|
||||
default void ready() {
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
// Cards are played exhausted by default
|
||||
default void enterPlay() {
|
||||
exhaust(); // enters play exhausted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ghost.technic.server.card.playable.usable.impl;
|
||||
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.Usable;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArtifactCard extends PlayableCard implements Usable {
|
||||
private boolean ready;
|
||||
@Builder.Default
|
||||
private Counters counters = new Counters();
|
||||
private List<String> traits;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ghost.technic.server.card.playable.usable.impl;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.keyword.Keyword;
|
||||
import com.ghost.technic.server.card.keyword.KeywordAbility;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.impl.UpgradeCard;
|
||||
import com.ghost.technic.server.card.playable.usable.Usable;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@SuperBuilder
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
|
||||
public class CreatureCard extends PlayableCard implements Usable {
|
||||
private int power;
|
||||
private int armor;
|
||||
private boolean ready;
|
||||
private List<String> traits;
|
||||
@Builder.Default
|
||||
private Counters counters = new Counters();
|
||||
@Builder.Default
|
||||
private Set<Keyword> keywords = new HashSet<>();
|
||||
@Builder.Default
|
||||
private List<UpgradeCard> upgrades = new ArrayList<>();
|
||||
|
||||
public void play(Player player, Set<Flank> flanks, EventBus eventBus) {
|
||||
publishUsedEvent(player, UsageType.PLAY, flanks, eventBus);
|
||||
}
|
||||
|
||||
public void publishUsedEvent(Player player, UsageType usageType, Set<Flank> flanks, EventBus eventBus) {
|
||||
UsedEventData usedData = new UsedEventData(this, usageType);
|
||||
Map<String, Object> payload = Map.of(
|
||||
"usedEventData", usedData,
|
||||
"flanks", flanks,
|
||||
"player", player);
|
||||
GameEvent usedEvent = new GameEvent(EventType.USED, payload);
|
||||
eventBus.publish(usedEvent);
|
||||
log.info("{} {} {}", player.getName(), usageType, getName());
|
||||
}
|
||||
|
||||
public int getDamage() {
|
||||
return counters.getDmg();
|
||||
}
|
||||
|
||||
public void takeDamage(int amount) {
|
||||
counters.addDmg(amount);
|
||||
}
|
||||
|
||||
public boolean isDestroyed() {
|
||||
return counters.getDmg() >= power;
|
||||
}
|
||||
|
||||
public void beforeFight() {
|
||||
log.info("{} triggers before fight effects.", getName());
|
||||
}
|
||||
|
||||
public void afterFight() {
|
||||
log.info("{} triggers after fight effects.", getName());
|
||||
}
|
||||
|
||||
public void beforeReap() {
|
||||
log.info("{} triggers before reap effects.", getName());
|
||||
}
|
||||
|
||||
public void fight(Player player, EventBus eventBus) {
|
||||
publishUsedEvent(player, UsageType.FIGHT, eventBus);
|
||||
}
|
||||
|
||||
public void reap(Player player, EventBus eventBus) {
|
||||
publishUsedEvent(player, UsageType.REAP, eventBus);
|
||||
}
|
||||
|
||||
public void afterReap() {
|
||||
log.info("{} triggers after reap effects.", getName());
|
||||
}
|
||||
|
||||
public boolean hasKeyword(KeywordAbility keywordAbility) {
|
||||
return keywords.stream().anyMatch(k -> k.getKeywordAbility() == keywordAbility);
|
||||
}
|
||||
|
||||
public Optional<Keyword> getKeyword(KeywordAbility keywordAbility) {
|
||||
return keywords.stream().filter(k -> k.getKeywordAbility() == keywordAbility).findFirst();
|
||||
}
|
||||
|
||||
public int getEffectiveArmor(GameState state) {
|
||||
return armor + state.getArmorBuffs(this);
|
||||
}
|
||||
|
||||
public int getKeywordValue(KeywordAbility ability) {
|
||||
return keywords.stream()
|
||||
.filter(k -> k.getKeywordAbility() == ability)
|
||||
.map(Keyword::getValue)
|
||||
.findFirst()
|
||||
.orElse(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ghost.technic.server.counter;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class Counter {
|
||||
private CounterType counterType;
|
||||
private int numberOfCounter;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ghost.technic.server.counter;
|
||||
|
||||
public enum CounterType {
|
||||
ÆMBER,
|
||||
DAMAGE,
|
||||
WARD,
|
||||
ENRAGE,
|
||||
POWER,
|
||||
STUN,
|
||||
DOOM,
|
||||
TIME,
|
||||
FUSE,
|
||||
YEA,
|
||||
NAY,
|
||||
GLORY,
|
||||
AWAKENING,
|
||||
HATCH,
|
||||
DEPTH,
|
||||
GROWTH,
|
||||
SCHEME,
|
||||
WISDOM,
|
||||
KNOWLEDGE,
|
||||
WARRANT,
|
||||
PAINT,
|
||||
MINERALIZE,
|
||||
TRADE
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ghost.technic.server.counter;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class Counters {
|
||||
|
||||
private Set<Counter> counters = new HashSet<>();
|
||||
|
||||
public int getDmg() {
|
||||
return counters.stream()
|
||||
.filter(counter -> counter.getCounterType().equals(CounterType.DAMAGE))
|
||||
.findFirst()
|
||||
.map(Counter::getNumberOfCounter)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
public void addDmg(int dmg) {
|
||||
var dmgToken = getToken(CounterType.DAMAGE);
|
||||
if (dmgToken != null) {
|
||||
var newDmg = dmgToken.getNumberOfCounter() + dmg;
|
||||
dmgToken.setNumberOfCounter(newDmg);
|
||||
counters.add(dmgToken);
|
||||
} else {
|
||||
var dmgTokens = Counter.builder()
|
||||
.counterType(CounterType.DAMAGE)
|
||||
.numberOfCounter(dmg)
|
||||
.build();
|
||||
counters.add(dmgTokens);
|
||||
}
|
||||
}
|
||||
|
||||
public int getAmber() {
|
||||
return counters.stream()
|
||||
.filter(counter -> counter.getCounterType().equals(CounterType.ÆMBER))
|
||||
.findFirst()
|
||||
.map(Counter::getNumberOfCounter)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
public void addAmber(int amber) {
|
||||
var ambers = getToken(CounterType.ÆMBER);
|
||||
if (ambers != null) {
|
||||
var newAmbers = ambers.getNumberOfCounter() + amber;
|
||||
ambers.setNumberOfCounter(newAmbers);
|
||||
counters.add(ambers);
|
||||
} else {
|
||||
var amberTokens = Counter.builder()
|
||||
.counterType(CounterType.ÆMBER)
|
||||
.numberOfCounter(amber)
|
||||
.build();
|
||||
counters.add(amberTokens);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStun() {
|
||||
var stun = getToken(CounterType.STUN);
|
||||
if (stun == null) {
|
||||
counters.add(Counter.builder().counterType(CounterType.STUN).build());
|
||||
}
|
||||
}
|
||||
|
||||
private Counter getToken(CounterType counterType) {
|
||||
return counters.stream()
|
||||
.filter(counter -> counter.getCounterType().equals(counterType))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
package com.ghost.technic.server.game;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Battleline;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightAbility;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightEffect;
|
||||
import com.ghost.technic.server.card.keyword.KeywordAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.impl.UpgradeCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.event.*;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class GameActionService {
|
||||
|
||||
private final GameState gameState;
|
||||
|
||||
public void forgeKey(Player player) {
|
||||
int baseCost = 6;
|
||||
int modifier = calculateKeyCostModifier(player);
|
||||
int totalCost = baseCost + modifier;
|
||||
|
||||
if (player.getAmber() >= totalCost) {
|
||||
player.setAmber(player.getAmber() - totalCost);
|
||||
player.setKeysForged(player.getKeysForged() + 1);
|
||||
|
||||
log.info("{} forges a key for {} æmber! Keys forged: {}", player.getName(), totalCost, player.getKeysForged());
|
||||
|
||||
getEventBus().publish(new GameEvent(EventType.KEY_FORGED, Map.of(
|
||||
"player", player,
|
||||
"cost", totalCost
|
||||
)));
|
||||
} else {
|
||||
log.info("{} could not forge a key (needs {}, has {})", player.getName(), totalCost, player.getAmber());
|
||||
}
|
||||
}
|
||||
|
||||
public int calculateKeyCostModifier(Player player) {
|
||||
int modifier = 0;
|
||||
for (ConstantAbility effect : gameState.getStaticEffects().values()) {
|
||||
modifier += effect.keyCostModifier(player, gameState);
|
||||
}
|
||||
return modifier;
|
||||
}
|
||||
|
||||
public void reap(CreatureCard creature, Player controller) {
|
||||
// Check if the creature is allowed to be used at all (Rule of Six, exhausted, constant ability etc.)
|
||||
if (!gameState.canUseCard(creature) || !gameState.canReap(creature, controller)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let the player reflect the reap on their state (gain amber, etc.)
|
||||
controller.reapWithCreature(creature, getEventBus());
|
||||
}
|
||||
|
||||
public void fight(CreatureCard attacker, CreatureCard defender, Player attackerControllerPlayer) {
|
||||
if (!gameState.canUseCard(attacker) | !gameState.canFight(attacker, attackerControllerPlayer)) {
|
||||
log.info("{} cannot be used (exhausted or rule of six)", attacker.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
attacker.setReady(false);
|
||||
log.info("{} fights {}", attacker.getName(), defender.getName());
|
||||
|
||||
if (beforeFight(attacker, defender)) {
|
||||
actualFight(attacker, defender);
|
||||
afterFight(attacker, defender);
|
||||
}
|
||||
|
||||
attacker.fight(attackerControllerPlayer, getEventBus()); // TODO: The timing on this needs to be double checked
|
||||
}
|
||||
|
||||
private boolean beforeFight(CreatureCard attacker, CreatureCard defender) {
|
||||
List<BeforeFightEffect> effects = new ArrayList<>();
|
||||
|
||||
// ASSAULT
|
||||
int assault = attacker.getKeywordValue(KeywordAbility.ASSAULT);
|
||||
if (assault > 0) {
|
||||
effects.add(new BeforeFightEffect("Assault from " + attacker.getName(), () ->
|
||||
dealDamage(defender, assault)
|
||||
));
|
||||
}
|
||||
|
||||
// HAZARDOUS
|
||||
int hazardous = defender.getKeywordValue(KeywordAbility.HAZARDOUS);
|
||||
if (hazardous > 0) {
|
||||
effects.add(new BeforeFightEffect("Hazardous from " + defender.getName(), () ->
|
||||
dealDamage(attacker, hazardous)
|
||||
));
|
||||
}
|
||||
|
||||
// BEFORE FIGHT ABILITIES — attacker
|
||||
for (CardAbility ability : attacker.getAbilities()) {
|
||||
if (ability instanceof BeforeFightAbility bf) {
|
||||
bf.getBeforeFightEffect(attacker, defender, this).ifPresent(effects::add);
|
||||
}
|
||||
}
|
||||
|
||||
// BEFORE FIGHT ABILITIES — defender
|
||||
for (CardAbility ability : defender.getAbilities()) {
|
||||
if (ability instanceof BeforeFightAbility bf) {
|
||||
bf.getBeforeFightEffect(defender, attacker, this).ifPresent(effects::add);
|
||||
}
|
||||
}
|
||||
|
||||
if (effects.isEmpty()) return true;
|
||||
|
||||
List<BeforeFightEffect> ordered = gameState.getActivePlayer().chooseBeforeFightOrder(effects);
|
||||
|
||||
for (BeforeFightEffect effect : ordered) {
|
||||
log.info("[Before Fight] Resolving: {}", effect.description());
|
||||
effect.action().run();
|
||||
|
||||
if (attacker.isDestroyed() || defender.isDestroyed()) {
|
||||
log.info("[Before Fight] One or both creatures destroyed. Fight canceled.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void actualFight(CreatureCard attacker, CreatureCard defender) {
|
||||
log.info("[FIGHT] {} ↔ {}", attacker.getName(), defender.getName());
|
||||
|
||||
// Attacker damages defender
|
||||
dealDamage(defender, attacker.getPower());
|
||||
|
||||
// Defender damages attacker unless SKIRMISH
|
||||
if (!attacker.hasKeyword(KeywordAbility.SKIRMISH)) {
|
||||
dealDamage(attacker, defender.getPower());
|
||||
} else {
|
||||
log.info("[SKIRMISH] {} avoids return damage", attacker.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void afterFight(CreatureCard attacker, CreatureCard defender) {
|
||||
// Placeholder for “After Fight” effects (triggers, events, etc.)
|
||||
// You can add card effect handling or publish AFTER_FIGHT events here
|
||||
log.info("[AFTER FIGHT] Resolving aftermath of fight.");
|
||||
}
|
||||
|
||||
public void dealDamage(CreatureCard creature, int amount) {
|
||||
log.info("Dealing {} dmg to {}", amount, creature.getName());
|
||||
if (creature.getArmor() != 0) {
|
||||
damagingCreatureWithArmor(creature, amount);
|
||||
} else {
|
||||
damageDealt(creature, amount);
|
||||
}
|
||||
}
|
||||
|
||||
private void damagingCreatureWithArmor(CreatureCard creature, int amount) {
|
||||
int armor = creature.getArmor();
|
||||
int difference = armor - amount;
|
||||
|
||||
if (difference >= 0) {
|
||||
// All damage prevented
|
||||
log.info("{}'s armor prevents all {} dmg ({} Armor remaining)", creature.getName(), amount, difference);
|
||||
creature.setArmor(difference);
|
||||
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.DAMAGE_PREVENTED, Map.of(
|
||||
"creature", creature,
|
||||
"amount", amount
|
||||
)));
|
||||
} else {
|
||||
// Some damage goes through
|
||||
int overflow = amount - armor;
|
||||
|
||||
log.info("{}'s armor prevents {} dmg (0 Armor remaining)", creature.getName(), armor);
|
||||
creature.setArmor(0);
|
||||
|
||||
if (armor > 0) {
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.DAMAGE_PREVENTED, Map.of(
|
||||
"creature", creature,
|
||||
"amount", armor
|
||||
)));
|
||||
}
|
||||
|
||||
damageDealt(creature, overflow);
|
||||
}
|
||||
}
|
||||
|
||||
private void damageDealt(CreatureCard creature, int amount) {
|
||||
creature.takeDamage(amount);
|
||||
log.info("{} takes {} dmg (total: {})", creature.getName(), amount, creature.getDamage());
|
||||
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.DAMAGE_TAKEN, Map.of(
|
||||
"creature", creature,
|
||||
"amount", amount
|
||||
)));
|
||||
}
|
||||
|
||||
public void destroyCreature(CreatureCard creature) {
|
||||
Player owner = gameState.getOwnerOf(creature);
|
||||
if (owner == null) {
|
||||
log.warn("Attempted to destroy creature with unknown owner: {}", creature.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from battleline
|
||||
owner.getPlayArea().getBattleline().getCreatures().remove(creature);
|
||||
|
||||
// Move to discard pile
|
||||
owner.getDiscardPile().add(creature);
|
||||
log.info("{} is destroyed and moved to {}'s discard pile", creature.getName(), owner.getName());
|
||||
|
||||
// Remove any static effects associated with the creature
|
||||
if (gameState.getStaticEffects().containsKey(creature)) {
|
||||
log.info("Removing static effect associated with {}", creature.getName());
|
||||
gameState.getStaticEffects().remove(creature);
|
||||
}
|
||||
|
||||
// Fire destroy event
|
||||
GameEvent destroyEvent = new GameEvent(EventType.CREATURE_DESTROYED, Map.of(
|
||||
"creature", creature,
|
||||
"player", owner
|
||||
));
|
||||
getEventBus().publish(destroyEvent);
|
||||
}
|
||||
|
||||
public void adjustAmber(Player player, int delta, AmberChangeType type, PlayableCard source) {
|
||||
int original = player.getAmber();
|
||||
int newValue = Math.max(0, original + delta); // Ensure no negative amber
|
||||
int actualDelta = newValue - original;
|
||||
|
||||
if (actualDelta == 0) return;
|
||||
|
||||
player.setAmber(newValue);
|
||||
log.info("{} {} {} amber ({} → {}) from {}",
|
||||
player.getName(),
|
||||
(actualDelta > 0 ? "gains" : "loses"),
|
||||
Math.abs(actualDelta),
|
||||
original, newValue,
|
||||
source.getName()
|
||||
);
|
||||
|
||||
AmberChangeEventData eventData = new AmberChangeEventData(player, Math.abs(actualDelta), type, source);
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.AMBER_CHANGED, Map.of(
|
||||
"amberEventData", eventData
|
||||
)));
|
||||
}
|
||||
|
||||
public void playCard(PlayableCard card, Player controller) {
|
||||
if (card instanceof CreatureCard creature) {
|
||||
if (creature.hasKeyword(KeywordAbility.ALPHA) && !isFirstCardPlayedThisTurn(card)) {
|
||||
flagRuleViolation(card, "Alpha cards must be the first card played this turn.");
|
||||
}
|
||||
|
||||
if (creature.hasKeyword(KeywordAbility.OMEGA)) {
|
||||
markOmegaCardPlayed(card);
|
||||
}
|
||||
|
||||
if (creature.hasKeyword(KeywordAbility.DEPLOY)) {
|
||||
allowCustomBattlelinePlacement(creature);
|
||||
}
|
||||
}
|
||||
|
||||
// Register all triggered/static abilities
|
||||
for (CardAbility ability : card.getAbilities()) {
|
||||
ability.register(this, controller, card);
|
||||
}
|
||||
|
||||
card.play(controller, getEventBus());
|
||||
}
|
||||
|
||||
public void playCreature(Player player, CreatureCard creature, Flank preferredFlank) {
|
||||
// Check if the creature is allowed to be used at all (Rule of Six, exhausted, constant ability etc.)
|
||||
if (!gameState.canUseCard(creature)) {
|
||||
return;
|
||||
}
|
||||
Battleline battleline = player.getPlayArea().getBattleline();
|
||||
|
||||
// Add creature to the appropriate side
|
||||
battleline.addToFlank(creature, preferredFlank);
|
||||
|
||||
// Compute effective flank(s)
|
||||
Set<Flank> effectiveFlanks = battleline.getEffectiveFlanks(creature);
|
||||
|
||||
// Publish CREATURE_ENTERED_PLAY event
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.CREATURE_ENTERED_PLAY, Map.of(
|
||||
"creature", creature,
|
||||
"player", player,
|
||||
"flanks", effectiveFlanks,
|
||||
"gameState", gameState
|
||||
)));
|
||||
|
||||
// Register all triggered/static abilities
|
||||
for (CardAbility ability : creature.getAbilities()) {
|
||||
ability.register(this, player, creature);
|
||||
}
|
||||
|
||||
creature.play(player, effectiveFlanks, getEventBus());
|
||||
creature.setReady(false);
|
||||
}
|
||||
|
||||
public void playUpgrade(Player player, UpgradeCard upgrade, CreatureCard target) {
|
||||
// Check rule-of-six or usage limits if applicable
|
||||
if (!gameState.canUseCard(upgrade)) {
|
||||
log.info("Upgrade {} cannot be used due to Rule of Six.", upgrade.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the upgrade to the target creature
|
||||
target.getUpgrades().add(upgrade);
|
||||
upgrade.play(player, getEventBus());
|
||||
gameState.setOwner(upgrade, player);
|
||||
|
||||
// Register upgrade abilities
|
||||
for (CardAbility ability : upgrade.getAbilities()) {
|
||||
ability.register(this, player, upgrade);
|
||||
}
|
||||
|
||||
log.info("{} attaches upgrade {} to {}", player.getName(), upgrade.getName(), target.getName());
|
||||
}
|
||||
|
||||
public void archiveCard(PlayableCard card, Player player) {
|
||||
if (card != null) {
|
||||
player.getArchives().add(card);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayableCard discordTopCardOfDeck(Player player) {
|
||||
return discardCard(player.takeTopCardOfDeck(), player);
|
||||
}
|
||||
|
||||
public PlayableCard discardCard(PlayableCard card, Player player) {
|
||||
if (card != null) {
|
||||
player.getDiscardPile().add(card);
|
||||
return card;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void chooseActiveHouse(Player player) {
|
||||
Set<House> identityHouses = player.getAvailableHouses();
|
||||
Set<House> restricted = gameState.getRestrictedHouses(player);
|
||||
|
||||
Set<House> valid = new HashSet<>(identityHouses);
|
||||
valid.removeAll(restricted);
|
||||
|
||||
House chosen = player.chooseHouse(valid); // Delegate UI/input to player
|
||||
log.info("{} chooses house: {}", player.getName(), chosen);
|
||||
|
||||
gameState.getEventBus().publish(new GameEvent(EventType.HOUSE_CHOSEN, Map.of(
|
||||
"player", player,
|
||||
"house", chosen
|
||||
)));
|
||||
gameState.clearHouseRestrictions(player);
|
||||
}
|
||||
|
||||
public House chhoseAHouse() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public EventBus getEventBus() {
|
||||
return gameState.getEventBus();
|
||||
}
|
||||
|
||||
// Placeholder method signatures
|
||||
public void ignoreRetaliatoryDamage(CreatureCard attacker) {
|
||||
}
|
||||
|
||||
public boolean isFirstAttackAgainst(CreatureCard defender) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void preventDamageTo(CreatureCard creature) {
|
||||
}
|
||||
|
||||
public void markAsPoisonous(CreatureCard creature) {
|
||||
}
|
||||
|
||||
public void dealHazardousDamage(CreatureCard from, CreatureCard to, int amount) {
|
||||
}
|
||||
|
||||
public void dealAssaultDamage(CreatureCard from, CreatureCard to, int amount) {
|
||||
}
|
||||
|
||||
public void dealSplashDamage(CreatureCard from, CreatureCard primaryTarget, int splashAmount) {
|
||||
}
|
||||
|
||||
public boolean isFirstCardPlayedThisTurn(PlayableCard card) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void flagRuleViolation(PlayableCard card, String reason) {
|
||||
System.out.println("[RULE] " + card.getName() + ": " + reason);
|
||||
}
|
||||
|
||||
public void markOmegaCardPlayed(PlayableCard card) {
|
||||
}
|
||||
|
||||
public void allowCustomBattlelinePlacement(CreatureCard creature) {
|
||||
}
|
||||
|
||||
public boolean isPlayedFromDiscard(PlayableCard card) {
|
||||
// Check discard context or play source
|
||||
return false;
|
||||
}
|
||||
|
||||
public void resolveDestroyedAbilities(CreatureCard creature) {
|
||||
// Trigger destroyed abilities from effects or cards
|
||||
}
|
||||
|
||||
public void resolveScrapEffect(PlayableCard card) {
|
||||
// Trigger scrap ability from card text or linked ability
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.ghost.technic.server.game;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.ConstantAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import com.ghost.technic.server.game.event.UsedEventData;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GameState {
|
||||
|
||||
private Player playerOne;
|
||||
private Player playerTwo;
|
||||
private Player activePlayer;
|
||||
private int turnCount = 0;
|
||||
private Map<PlayableCard, ConstantAbility> staticEffects = new HashMap<>();
|
||||
private Map<PlayableCard, Player> cardOwnership = new HashMap<>();
|
||||
private Map<String, Integer> ruleOfSixCounter = new HashMap<>();
|
||||
private final EventBus eventBus;
|
||||
private final Map<Player, Set<House>> nextTurnHouseRestrictions = new HashMap<>();
|
||||
private final Map<Player, House> forcedHouseChoiceNextTurn = new HashMap<>();
|
||||
private GameTurnManager turnManager;
|
||||
|
||||
public void setup() {
|
||||
playerOne = Player.builder().name("Archon A").build();
|
||||
playerTwo = Player.builder().name("Archon B").build();
|
||||
playerOne.drawCards(7);
|
||||
playerTwo.drawCards(6);
|
||||
activePlayer = playerOne; // Could be randomized
|
||||
}
|
||||
|
||||
// Assign players (optional setter if needed)
|
||||
public void setPlayers(Player playerOne, Player playerTwo) {
|
||||
this.playerOne = playerOne;
|
||||
this.playerTwo = playerTwo;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while (!isGameOver()) {
|
||||
turnManager.takeTurn(activePlayer);
|
||||
}
|
||||
log.info("Game over! Winner: {}", getWinner().getName());
|
||||
}
|
||||
|
||||
// public void takeTurn(Player player) {
|
||||
// log.info("\nTurn {}: {}", turnCount + 1, player.getName());
|
||||
// player.forgeKey();
|
||||
// player.chooseHouse();
|
||||
// player.pickUpArchives();
|
||||
// player.playCards();
|
||||
// player.removeDestroyedCreatures();
|
||||
// player.readyCards();
|
||||
// player.drawCardsToSix();
|
||||
// }
|
||||
|
||||
public void switchActivePlayer() {
|
||||
activePlayer = (activePlayer == playerOne) ? playerTwo : playerOne;
|
||||
}
|
||||
|
||||
public boolean isGameOver() {
|
||||
return playerOne.getKeysForged() >= 3 || playerTwo.getKeysForged() >= 3;
|
||||
}
|
||||
|
||||
public Player getWinner() {
|
||||
return playerOne.getKeysForged() >= 3 ? playerOne : playerTwo;
|
||||
}
|
||||
|
||||
|
||||
public void registerStaticEffect(PlayableCard card, ConstantAbility effect) {
|
||||
staticEffects.put(card, effect);
|
||||
}
|
||||
|
||||
public void unregisterStaticEffect(PlayableCard card) {
|
||||
staticEffects.remove(card);
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
staticEffects.values().forEach(ConstantAbility::remove);
|
||||
staticEffects.clear();
|
||||
}
|
||||
|
||||
public void trackRuleOfSix() {
|
||||
eventBus.subscribe(EventType.USED, event -> {
|
||||
UsedEventData usedData = event.get("usedEventData");
|
||||
PlayableCard card = usedData.getCard();
|
||||
String cardName = card.getName();
|
||||
int count = ruleOfSixCounter.getOrDefault(cardName, 0);
|
||||
ruleOfSixCounter.put(cardName, count + 1);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean canReap(CreatureCard creature, Player actingPlayer) {
|
||||
log.info("{} use {} to reap", actingPlayer.getName(), creature.getName());
|
||||
for (ConstantAbility effect : staticEffects.values()) {
|
||||
if (!effect.canReap(creature, actingPlayer, this)) {
|
||||
log.info("{} cannot reap due to {}'s constant ability.", creature.getName(), effect);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canFight(CreatureCard creature, Player actingPlayer) {
|
||||
log.info("{} use {} to fight", actingPlayer.getName(), creature.getName());
|
||||
for (ConstantAbility effect : staticEffects.values()) {
|
||||
if (!effect.canFight(creature, actingPlayer, this)) {
|
||||
log.info("{} cannot fight due to {}'s constant ability.", creature.getName(), effect);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setOwner(PlayableCard card, Player player) {
|
||||
cardOwnership.put(card, player);
|
||||
}
|
||||
|
||||
public Player getOwnerOf(PlayableCard card) {
|
||||
return cardOwnership.get(card);
|
||||
}
|
||||
|
||||
boolean canUseCard(PlayableCard card) {
|
||||
if (!(ruleOfSixCounter.getOrDefault(card.getName(), 0) < 6)) {
|
||||
log.info("{} cannot be used due to Rule of Six.", card.getName());
|
||||
return false;
|
||||
}
|
||||
;
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<CreatureCard> getAllCreaturesInPlay() {
|
||||
List<CreatureCard> all = new ArrayList<>();
|
||||
if (playerOne != null) {
|
||||
all.addAll(playerOne.getPlayArea().getBattleline().getCreatures());
|
||||
}
|
||||
if (playerTwo != null) {
|
||||
all.addAll(playerTwo.getPlayArea().getBattleline().getCreatures());
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
public Player getOpponentOf(Player controller) {
|
||||
if (controller.equals(playerOne)) {
|
||||
return playerTwo;
|
||||
} else {
|
||||
return playerOne;
|
||||
}
|
||||
}
|
||||
|
||||
public int getArmorBuffs(CreatureCard creature) {
|
||||
return staticEffects.entrySet().stream()
|
||||
.filter(entry -> !isTextBoxBlanked(entry.getKey()))
|
||||
.map(Map.Entry::getValue)
|
||||
.mapToInt(effect -> effect.armorBonus(creature, this))
|
||||
.sum();
|
||||
}
|
||||
|
||||
public boolean isTextBoxBlanked(PlayableCard card) {
|
||||
return staticEffects.values().stream()
|
||||
.anyMatch(effect -> effect.blanksTextBox(card));
|
||||
}
|
||||
|
||||
public List<CreatureCard> getAllCreatures() {
|
||||
var results = new ArrayList<CreatureCard>();
|
||||
var playerOneCreatures = playerOne.getPlayArea().getBattleline().getCreatures();
|
||||
var playerTwoCreatures = playerTwo.getPlayArea().getBattleline().getCreatures();
|
||||
results.addAll(playerOneCreatures);
|
||||
results.addAll(playerTwoCreatures);
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
public void restrictHouseNextTurn(Player player, House house) {
|
||||
nextTurnHouseRestrictions.computeIfAbsent(player, k -> new HashSet<>()).add(house);
|
||||
}
|
||||
|
||||
public Set<House> getRestrictedHouses(Player player) {
|
||||
return nextTurnHouseRestrictions.getOrDefault(player, Set.of());
|
||||
}
|
||||
|
||||
public void clearHouseRestrictions(Player player) {
|
||||
nextTurnHouseRestrictions.remove(player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ghost.technic.server.game;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.CardAbility;
|
||||
import com.ghost.technic.server.card.effect.StartOfOpponentTurnAbility;
|
||||
import com.ghost.technic.server.card.effect.StartOfTurnAbility;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import com.ghost.technic.server.game.event.GameEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GameTurnManager {
|
||||
|
||||
private final GameState gameState;
|
||||
private final GameActionService gameActionService;
|
||||
|
||||
public void takeTurn(Player player) {
|
||||
log.info("== Turn {}: {} ==", gameState.getTurnCount() + 1, player.getName());
|
||||
|
||||
// “Start of turn” effect(s) resolve.
|
||||
handleStartOfTurnAbilities(player);
|
||||
|
||||
// Forge a key step
|
||||
gameActionService.forgeKey(player);
|
||||
|
||||
// Choose which house will be the active house for this turn.
|
||||
gameActionService.chooseActiveHouse(player);
|
||||
// player.chooseHouse();
|
||||
|
||||
// “After you choose a house” effects resolve.
|
||||
|
||||
// STEP 3: PLAY, DISCARD, OR USE CARDS
|
||||
|
||||
player.readyCards();
|
||||
|
||||
player.drawCardsToSix();
|
||||
|
||||
// “End of turn” effects resolve.
|
||||
}
|
||||
|
||||
public void endOfTurn(Player player) {
|
||||
var endOfTurnGameEvent = new GameEvent(EventType.TURN_ENDS , Map.of("player", player));
|
||||
gameState.getEventBus().publish(endOfTurnGameEvent);
|
||||
}
|
||||
|
||||
public void handleStartOfTurnAbilities(Player activePlayer) {
|
||||
List<CreatureCard> allCreatures = gameState.getAllCreaturesInPlay();
|
||||
|
||||
for (CreatureCard creature : allCreatures) {
|
||||
for (CardAbility ability : creature.getAbilities()) {
|
||||
if (ability instanceof StartOfTurnAbility startOfTurnAbility) {
|
||||
boolean isOwnedByActivePlayer = gameState.getOwnerOf(creature).equals(activePlayer);
|
||||
boolean isOpponentAbility = !isOwnedByActivePlayer;
|
||||
|
||||
// Only trigger relevant abilities
|
||||
if ((startOfTurnAbility instanceof StartOfOpponentTurnAbility && isOpponentAbility) ||
|
||||
(!(startOfTurnAbility instanceof StartOfOpponentTurnAbility) && isOwnedByActivePlayer)) {
|
||||
startOfTurnAbility.onStartOfTurn(gameActionService, activePlayer, creature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class AmberChangeEventData {
|
||||
private final Player player;
|
||||
private final int amount;
|
||||
private final AmberChangeType changeType;
|
||||
private final PlayableCard source;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
public enum AmberChangeType {
|
||||
GAINED,
|
||||
LOST,
|
||||
CAPTURED,
|
||||
STOLEN
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class AmberEventData {
|
||||
Player player;
|
||||
int amountChanged; // use negative for loss, positive for gain
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class EventBus {
|
||||
|
||||
private final Map<EventType, List<EventListener>> listeners = new EnumMap<>(EventType.class);
|
||||
|
||||
public void subscribe(EventType type, EventListener listener) {
|
||||
listeners.computeIfAbsent(type, k -> new ArrayList<>()).add(listener);
|
||||
}
|
||||
|
||||
public void unsubscribe(EventType type, EventListener listener) {
|
||||
if (listeners.containsKey(type)) {
|
||||
listeners.get(type).remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void publish(GameEvent event) {
|
||||
List<EventListener> eventListeners = listeners.get(event.getType());
|
||||
if (eventListeners != null) {
|
||||
for (EventListener listener : new ArrayList<>(eventListeners)) {
|
||||
listener.handle(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface EventListener {
|
||||
void handle(GameEvent event);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
public enum EventType {
|
||||
TURN_STARTED,
|
||||
TURN_ENDS,
|
||||
CREATURE_ENTERED_PLAY,
|
||||
CREATURE_DESTROYED,
|
||||
HOUSE_CHOSEN,
|
||||
USED, // unified event for PLAY, REAP, FIGHT, ACTION, etc.
|
||||
CARD_DISCARDED,
|
||||
CARD_DRAWN,
|
||||
CARD_ARCHIVED,
|
||||
DAMAGE_TAKEN,
|
||||
DAMAGE_PREVENTED,
|
||||
KEY_FORGED,
|
||||
AMBER_CHANGED
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class GameEvent {
|
||||
|
||||
private final EventType type;
|
||||
private final Map<String, Object> payload;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(String key) {
|
||||
return (T) payload.get(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
|
||||
public interface TriggeredAbility {
|
||||
void register(EventBus eventBus, CreatureCard source, GameState gameState);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
public enum UsageType {
|
||||
REAP,
|
||||
FIGHT,
|
||||
ACTION,
|
||||
OMNI,
|
||||
PLAY,
|
||||
UNSTUN
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ghost.technic.server.game.event;
|
||||
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class UsedEventData {
|
||||
private final PlayableCard card;
|
||||
private final UsageType usageType;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.card.effect.impl.AllusionsOfGrandeurAbility;
|
||||
import com.ghost.technic.server.card.impl.ArchonIdentityCard;
|
||||
import com.ghost.technic.server.card.playable.impl.ActionCard;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class AllusionsOfGrandeurTest {
|
||||
|
||||
GameActionService actionService;
|
||||
GameState gameState;
|
||||
Player alice;
|
||||
Player bob;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameState = new GameState(new EventBus());
|
||||
actionService = new GameActionService(gameState);
|
||||
|
||||
alice = Player.builder().name("Alice").archonIdentityCard(ArchonIdentityCard.builder().houses(Set.of(House.BROBNAR, House.SHADOWS, House.UNTAMED)).build()).build();
|
||||
|
||||
// Alice picks House Sanctum, if opponent does not pick Sanctum, Alice gains 3 amber
|
||||
alice = new Player(alice) {
|
||||
@Override
|
||||
public House chooseHouse(Set<House> house) {
|
||||
return House.SANCTUM;
|
||||
}
|
||||
};
|
||||
|
||||
bob = Player.builder().name("Bob").archonIdentityCard(ArchonIdentityCard.builder().houses(Set.of(House.SANCTUM, House.LOGOS, House.MARS)).build()).deck(new LinkedList<>(List.of(CreatureCard.builder().name("Some Mars Creature").house(House.MARS).build()))).build();
|
||||
|
||||
bob = new Player(bob) {
|
||||
@Override
|
||||
public House chooseHouse(Set<House> house) {
|
||||
return House.MARS;
|
||||
}
|
||||
};
|
||||
|
||||
gameState.setPlayers(alice, bob);
|
||||
gameState.setActivePlayer(alice);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGainAmberIfOpponentDoesNotChooseDeclaredHouse() {
|
||||
// Alice plays Allusions of Grandeur and picks BROBNAR
|
||||
ActionCard allusions = ActionCard.builder().name("Allusions of Grandeur").house(House.UNFATHOMABLE).abilities(List.of(new AllusionsOfGrandeurAbility())).build();
|
||||
|
||||
actionService.playCard(allusions, alice);
|
||||
assertEquals(0, alice.getAmber());
|
||||
|
||||
// Bob chooses a different house (LOGOS)
|
||||
gameState.setActivePlayer(bob);
|
||||
bob.setAmber(0);
|
||||
actionService.chooseActiveHouse(bob);
|
||||
|
||||
assertEquals(3, alice.getAmber(), "Alice should gain 3 amber because Bob did not choose BROBNAR");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.effect.impl.BarristerJoyaAbility;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import com.ghost.technic.server.game.event.GameEvent;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class BarristerJoyaTest {
|
||||
|
||||
GameActionService gameActionService;
|
||||
Player you;
|
||||
Player opponent;
|
||||
CreatureCard joya;
|
||||
CreatureCard enemyBadPenny;
|
||||
CreatureCard enemyRadPenny;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameActionService = new GameActionService(new GameState(new EventBus()));
|
||||
you = Player.builder().name("You").build();
|
||||
opponent = Player.builder().name("Opponent").build();
|
||||
|
||||
joya = CreatureCard.builder()
|
||||
.name("Barrister Joya")
|
||||
.house(House.SANCTUM)
|
||||
.power(4)
|
||||
.armor(2)
|
||||
.abilities(List.of(new BarristerJoyaAbility()))
|
||||
.build();
|
||||
|
||||
enemyBadPenny = CreatureCard.builder()
|
||||
.name("Bad Penny")
|
||||
.house(House.SHADOWS)
|
||||
.power(1)
|
||||
.armor(0)
|
||||
.build();
|
||||
|
||||
enemyRadPenny = CreatureCard.builder()
|
||||
.name("Rad Penny")
|
||||
.house(House.SHADOWS)
|
||||
.power(1)
|
||||
.armor(0)
|
||||
.build();
|
||||
|
||||
GameState state = gameActionService.getGameState();
|
||||
state.setOwner(joya, you);
|
||||
state.setOwner(enemyBadPenny, opponent);
|
||||
state.setOwner(enemyRadPenny, opponent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJoyaBlocksEnemyReapButNotFriendly() {
|
||||
var friendlyYurk = CreatureCard.builder()
|
||||
.name("Yurk")
|
||||
.house(House.DIS)
|
||||
.power(4)
|
||||
.armor(0)
|
||||
.build();
|
||||
|
||||
gameActionService.getGameState().setOwner(friendlyYurk, you);
|
||||
|
||||
gameActionService.playCreature(you, joya, Flank.LEFT);
|
||||
gameActionService.playCreature(you, friendlyYurk, Flank.RIGHT);
|
||||
gameActionService.playCreature(opponent, enemyBadPenny, Flank.RIGHT);
|
||||
|
||||
enemyBadPenny.setReady(true);
|
||||
gameActionService.reap(enemyBadPenny, opponent);
|
||||
assertEquals(0, opponent.getAmber(), "Enemy should NOT reap while Joya is in play");
|
||||
|
||||
friendlyYurk.setReady(true);
|
||||
gameActionService.reap(friendlyYurk, you);
|
||||
assertEquals(1, you.getAmber(), "Friendly should reap normally");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnemyPlayedAfterJoyaCannotReap() {
|
||||
gameActionService.playCreature(you, joya, Flank.LEFT);
|
||||
gameActionService.playCreature(opponent, enemyBadPenny, Flank.RIGHT);
|
||||
|
||||
enemyBadPenny.setReady(true);
|
||||
gameActionService.reap(enemyBadPenny, opponent);
|
||||
assertEquals(0, opponent.getAmber(), "Enemy creature played after Joya should not reap");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleEnemyCreaturesCannotReap() {
|
||||
gameActionService.playCreature(you, joya, Flank.LEFT);
|
||||
gameActionService.playCreature(opponent, enemyBadPenny, Flank.LEFT);
|
||||
gameActionService.playCreature(opponent, enemyRadPenny, Flank.RIGHT);
|
||||
|
||||
enemyBadPenny.setReady(true);
|
||||
enemyRadPenny.setReady(true);
|
||||
|
||||
gameActionService.reap(enemyBadPenny, opponent);
|
||||
gameActionService.reap(enemyRadPenny, opponent);
|
||||
assertEquals(0, opponent.getAmber(), "No enemy should reap while Joya is active");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnemyCanReapAfterJoyaDestroyed() {
|
||||
gameActionService.playCreature(you, joya, Flank.LEFT);
|
||||
gameActionService.playCreature(opponent, enemyBadPenny, Flank.RIGHT);
|
||||
|
||||
// Destroy Joya
|
||||
GameEvent destroyEvent = new GameEvent(EventType.CREATURE_DESTROYED, Map.of("creature", joya));
|
||||
gameActionService.getEventBus().publish(destroyEvent);
|
||||
|
||||
enemyBadPenny.setReady(true);
|
||||
gameActionService.reap(enemyBadPenny, opponent);
|
||||
assertEquals(1, opponent.getAmber(), "Enemy should reap after Joya is destroyed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.BulwarkAbility;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import lombok.Data;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@Data
|
||||
class BulwarkTest {
|
||||
|
||||
private Player player;
|
||||
private CreatureCard bulwark;
|
||||
private CreatureCard leftNeighbor;
|
||||
private CreatureCard rightNeighbor;
|
||||
private GameActionService gameActionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameActionService = new GameActionService(new GameState(new EventBus()));
|
||||
|
||||
player = Player.builder().name("TestPlayer").build();
|
||||
gameActionService.getGameState().setPlayers(player, Player.builder().name("Opponent").build());
|
||||
|
||||
bulwark = CreatureCard.builder()
|
||||
.name("Bulwark")
|
||||
.house(House.SANCTUM)
|
||||
.power(4)
|
||||
.armor(2)
|
||||
.traits(List.of("Human", "Knight"))
|
||||
.abilities(List.of(new BulwarkAbility()))
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
leftNeighbor = CreatureCard.builder()
|
||||
.name("Lefty")
|
||||
.house(House.BROBNAR)
|
||||
.power(3)
|
||||
.armor(1)
|
||||
.traits(List.of("Beast"))
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
rightNeighbor = CreatureCard.builder()
|
||||
.name("Righty")
|
||||
.house(House.BROBNAR)
|
||||
.power(3)
|
||||
.armor(1)
|
||||
.traits(List.of("Beast"))
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
|
||||
// Add cards to battleline: Lefty - Bulwark - Righty
|
||||
gameActionService.playCreature(player, leftNeighbor, Flank.LEFT);
|
||||
gameActionService.playCreature(player, bulwark, Flank.RIGHT);
|
||||
gameActionService.playCreature(player, rightNeighbor, Flank.RIGHT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBulwarkGrantsArmorToNeighbors() {
|
||||
assertEquals(3, leftNeighbor.getEffectiveArmor(gameActionService.getGameState()), "Left neighbor should have +2 armor");
|
||||
assertEquals(3, rightNeighbor.getEffectiveArmor(gameActionService.getGameState()), "Right neighbor should have +2 armor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBulwarkStaticEffectRemovedOnDestroy() {
|
||||
// BulwarkAbility should have already subscribed during setup
|
||||
gameActionService.getGameState().setOwner(bulwark, player);
|
||||
gameActionService.destroyCreature(bulwark);
|
||||
|
||||
// Ensure Bulwark's effect has been unregistered
|
||||
assertFalse(gameActionService.getGameState().getStaticEffects().containsKey(bulwark),
|
||||
"Bulwark's static effect should be removed from game state");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.DexusTriggeredAbility;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DexusTest {
|
||||
|
||||
GameActionService gameActionService;
|
||||
Player alice; // controller of Dexus
|
||||
Player bob; // opponent
|
||||
CreatureCard dexus;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameActionService = new GameActionService(new GameState(new EventBus()));
|
||||
alice = Player.builder().name("Alice").build();
|
||||
bob = Player.builder().name("Bob").build();
|
||||
|
||||
dexus = CreatureCard.builder()
|
||||
.name("Dexus")
|
||||
.house(House.DIS)
|
||||
.power(3)
|
||||
.armor(1)
|
||||
.abilities(List.of(new DexusTriggeredAbility()))
|
||||
.build();
|
||||
|
||||
gameActionService.getGameState().setPlayers(alice, bob);
|
||||
gameActionService.getGameState().setOwner(dexus, alice);
|
||||
gameActionService.playCreature(alice, dexus, Flank.LEFT);
|
||||
}
|
||||
|
||||
private CreatureCard makeCreature(String name, House house) {
|
||||
return CreatureCard.builder()
|
||||
.name(name)
|
||||
.house(house)
|
||||
.power(2)
|
||||
.armor(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void friendlyCreaturePlayedOnRightFlank_shouldNotLoseAmber() {
|
||||
CreatureCard friendly = makeCreature("Friendly Minion", House.DIS);
|
||||
gameActionService.getGameState().setOwner(friendly, alice);
|
||||
alice.setAmber(3);
|
||||
|
||||
gameActionService.playCreature(alice, friendly, Flank.RIGHT);
|
||||
|
||||
assertEquals(3, alice.getAmber(), "Alice should not lose amber when playing to right flank");
|
||||
}
|
||||
|
||||
@Test
|
||||
void friendlyCreaturePlayedOnLeftFlank_shouldNotLoseAmber() {
|
||||
CreatureCard friendly = makeCreature("Friendly Minion", House.DIS);
|
||||
gameActionService.getGameState().setOwner(friendly, alice);
|
||||
alice.setAmber(3);
|
||||
|
||||
gameActionService.playCreature(alice, friendly, Flank.LEFT);
|
||||
|
||||
assertEquals(3, alice.getAmber(), "Alice should not lose amber when playing to left flank");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enemyCreaturePlayedOnRightFlank_shouldLoseAmber() {
|
||||
CreatureCard enemy = makeCreature("Enemy Minion", House.SANCTUM);
|
||||
gameActionService.getGameState().setOwner(enemy, bob);
|
||||
bob.setAmber(2);
|
||||
|
||||
gameActionService.playCreature(bob, enemy, Flank.RIGHT);
|
||||
|
||||
assertEquals(1, bob.getAmber(), "Bob should lose 1 amber for playing to right flank");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enemyCreaturePlayedOnLeftFlank_shouldLoseAmberDueToBeingTheOnlyOne() {
|
||||
CreatureCard enemy = makeCreature("Enemy Minion", House.SANCTUM);
|
||||
gameActionService.getGameState().setOwner(enemy, bob);
|
||||
bob.setAmber(2);
|
||||
|
||||
gameActionService.playCreature(bob, enemy, Flank.LEFT);
|
||||
|
||||
assertEquals(1, bob.getAmber(), "Bob should lose amber even for playing to left flank because there's no other creature");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enemyPlaysTwoCreaturesToRightFlank_shouldLoseAmberTwice() {
|
||||
CreatureCard e1 = makeCreature("Righty1", House.SANCTUM);
|
||||
CreatureCard e2 = makeCreature("Righty2", House.SANCTUM);
|
||||
gameActionService.getGameState().setOwner(e1, bob);
|
||||
gameActionService.getGameState().setOwner(e2, bob);
|
||||
bob.setAmber(3);
|
||||
|
||||
gameActionService.playCreature(bob, e1, Flank.RIGHT);
|
||||
assertEquals(2, bob.getAmber(), "Bob should lose 1 amber for first creature being played");
|
||||
|
||||
gameActionService.playCreature(bob, e2, Flank.RIGHT);
|
||||
assertEquals(1, bob.getAmber(), "Bob should lose second amber for a creature being played on the right flank");
|
||||
}
|
||||
|
||||
@Test
|
||||
void enemyPlaysTwoCreaturesToLeftFlank_shouldLoseAmberForTheFirstCreature() {
|
||||
CreatureCard e1 = makeCreature("Lefty1", House.SANCTUM);
|
||||
CreatureCard e2 = makeCreature("Lefty2", House.SANCTUM);
|
||||
gameActionService.getGameState().setOwner(e1, bob);
|
||||
gameActionService.getGameState().setOwner(e2, bob);
|
||||
bob.setAmber(3);
|
||||
|
||||
gameActionService.playCreature(bob, e1, Flank.LEFT);
|
||||
assertEquals(2, bob.getAmber(), "Bob should lose 1 amber due to it being the only creature");
|
||||
|
||||
gameActionService.playCreature(bob, e2, Flank.LEFT);
|
||||
assertEquals(2, bob.getAmber(), "Bob should not lose amber for left-flank plays");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.DirectorZYXAbility;
|
||||
import com.ghost.technic.server.card.impl.ArchonIdentityCard;
|
||||
import com.ghost.technic.server.game.GameTurnManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DirectorZYXTest {
|
||||
|
||||
GameActionService gameActionService;
|
||||
GameState gameState;
|
||||
Player player;
|
||||
CreatureCard topCreatureCard;
|
||||
CreatureCard secondCreatureCard;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameState = new GameState(new EventBus());
|
||||
gameActionService = new GameActionService(gameState);
|
||||
|
||||
ArchonIdentityCard archonIdentityCard = ArchonIdentityCard.builder().houses(Set.of(House.SANCTUM, House.LOGOS, House.UNFATHOMABLE)).build();
|
||||
player = Player.builder().name("Z.Y.X. Player").archonIdentityCard(archonIdentityCard).build();
|
||||
topCreatureCard = CreatureCard.builder().name("Top card").build();
|
||||
secondCreatureCard = CreatureCard.builder().name("Second card").build();
|
||||
player.getDeck().addAll(List.of(topCreatureCard, secondCreatureCard));
|
||||
|
||||
gameState.setPlayers(player, Player.builder().name("Opponent").build());
|
||||
gameState.setOwner(player.getDeck().getFirst(), player);
|
||||
|
||||
CreatureCard director = CreatureCard.builder()
|
||||
.name("Director of Z.Y.X.")
|
||||
.house(House.LOGOS)
|
||||
.power(3)
|
||||
.abilities(List.of(new DirectorZYXAbility()))
|
||||
.build();
|
||||
|
||||
gameState.setOwner(director, player);
|
||||
gameActionService.playCreature(player, director, Flank.LEFT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDirectorZYXArchivesTopCard() {
|
||||
int before = player.getArchives().size();
|
||||
|
||||
GameTurnManager turnManager = new GameTurnManager(gameState, gameActionService);
|
||||
turnManager.handleStartOfTurnAbilities(player);
|
||||
|
||||
assertEquals(before + 1, player.getArchives().size(), "Should archive one card at start of turn");
|
||||
assertEquals(topCreatureCard, player.getArchives().getFirst());
|
||||
assertEquals(secondCreatureCard, player.getDeck().getFirst());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.MurmookAbility;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class MurmookTest {
|
||||
|
||||
GameActionService actionService;
|
||||
GameState gameState;
|
||||
Player alice;
|
||||
Player bob;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
gameState = new GameState(new EventBus());
|
||||
actionService = new GameActionService(gameState);
|
||||
alice = Player.builder().name("Alice").build();
|
||||
bob = Player.builder().name("Bob").build();
|
||||
gameState.setPlayers(alice, bob);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMurmookIncreasesOpponentKeyCost() {
|
||||
CreatureCard murmook = CreatureCard.builder()
|
||||
.name("Murmook")
|
||||
.power(3)
|
||||
.armor(0)
|
||||
.abilities(List.of(new MurmookAbility()))
|
||||
.build();
|
||||
|
||||
gameState.setOwner(murmook, alice);
|
||||
actionService.playCreature(alice, murmook, Flank.LEFT);
|
||||
|
||||
assertEquals(1, actionService.calculateKeyCostModifier(bob), "Bob's key cost should be +1");
|
||||
assertEquals(0, actionService.calculateKeyCostModifier(alice), "Alice's key cost should be unaffected");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.MusthicMurmookAbility;
|
||||
import com.ghost.technic.server.card.keyword.Keyword;
|
||||
import com.ghost.technic.server.card.keyword.KeywordAbility;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class MusthicMurmookTest {
|
||||
|
||||
private GameActionService actionService;
|
||||
private GameState gameState;
|
||||
private Player alice;
|
||||
private Player bob;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
gameState = new GameState(new EventBus());
|
||||
actionService = new GameActionService(gameState);
|
||||
bob = Player.builder().name("Bob").build();
|
||||
gameState.setPlayers(alice, bob);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMusthicMurmookAffectsKeyCostAndDeals4Damage() {
|
||||
CreatureCard briarGrubbling = CreatureCard.builder()
|
||||
.name("Briar Grubbling")
|
||||
.power(2)
|
||||
.armor(0)
|
||||
.traits(List.of("Insect", "Beast"))
|
||||
.counters(new Counters())
|
||||
.keywords(Set.of(new Keyword(KeywordAbility.HAZARDOUS, 5)))
|
||||
.build();
|
||||
|
||||
CreatureCard musthicMurmook = CreatureCard.builder()
|
||||
.name("Musthic Murmook")
|
||||
.power(3)
|
||||
.armor(0)
|
||||
.abilities(List.of(new MusthicMurmookAbility()))
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
alice = Player.builder().name("Alice").build();
|
||||
|
||||
alice = new Player(alice) {
|
||||
@Override
|
||||
public CreatureCard chooseCreature(List<CreatureCard> options, String reason) {
|
||||
return briarGrubbling; // target own creature for test
|
||||
}
|
||||
};
|
||||
|
||||
gameState.setOwner(briarGrubbling, alice);
|
||||
gameState.setOwner(musthicMurmook, alice);
|
||||
|
||||
actionService.playCreature(alice, briarGrubbling, Flank.LEFT);
|
||||
actionService.playCreature(alice, musthicMurmook, Flank.RIGHT); // should trigger 4 dmg on ally
|
||||
|
||||
assertEquals(4, briarGrubbling.getDamage(), "Briar Grubbling should have taken 4 damage from play effect");
|
||||
assertEquals(1, actionService.calculateKeyCostModifier(alice));
|
||||
assertEquals(1, actionService.calculateKeyCostModifier(bob));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.RainceFuryheartAbility;
|
||||
import com.ghost.technic.server.card.effect.impl.WayOfTheBearAbility;
|
||||
import com.ghost.technic.server.card.keyword.BeforeFightEffect;
|
||||
import com.ghost.technic.server.card.keyword.Keyword;
|
||||
import com.ghost.technic.server.card.keyword.KeywordAbility;
|
||||
import com.ghost.technic.server.card.playable.impl.UpgradeCard;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RainceFuryheartAndWayOfTheBearBeforeFightTest {
|
||||
|
||||
GameActionService service;
|
||||
Player active;
|
||||
Player defenderPlayer;
|
||||
CreatureCard raince;
|
||||
CreatureCard defender;
|
||||
UpgradeCard wayOfTheBear;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new GameActionService(new GameState(new EventBus()));
|
||||
|
||||
defenderPlayer = Player.builder().name("Bob").build();
|
||||
|
||||
// Defender: 2 power, Hazardous 2
|
||||
defender = CreatureCard.builder()
|
||||
.name("Hazard Goblin")
|
||||
.power(2)
|
||||
.armor(0)
|
||||
.keywords(new HashSet<>(Set.of(new Keyword(KeywordAbility.HAZARDOUS, 3))))
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
raince = CreatureCard.builder()
|
||||
.name("Raince Furyheart")
|
||||
.power(3)
|
||||
.armor(0)
|
||||
.keywords(new HashSet<>(Set.of(new Keyword(KeywordAbility.SKIRMISH))))
|
||||
.counters(new Counters())
|
||||
.abilities(List.of(new RainceFuryheartAbility()))
|
||||
.build();
|
||||
|
||||
wayOfTheBear = UpgradeCard.builder()
|
||||
.name("Way of the Bear")
|
||||
.abilities(List.of(new WayOfTheBearAbility()))
|
||||
.build();
|
||||
|
||||
service.getGameState().setOwner(raince, active);
|
||||
service.getGameState().setOwner(defender, defenderPlayer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEffectOrder_RainceThenAssaultThenHazardous() {
|
||||
|
||||
active = Player.builder().name("Alice").build();
|
||||
|
||||
active = new Player(active) {
|
||||
@Override
|
||||
public List<BeforeFightEffect> chooseBeforeFightOrder(List<BeforeFightEffect> effects) {
|
||||
return List.of(
|
||||
desc(effects, "Exalt the creature Raince Furyheart fights"),
|
||||
desc(effects, "Assault from Raince Furyheart"),
|
||||
desc(effects, "Hazardous from Hazard Goblin")
|
||||
);
|
||||
}
|
||||
};
|
||||
service.playCreature(active, raince, Flank.LEFT);
|
||||
service.getGameState().setPlayers(active, defenderPlayer);
|
||||
service.getGameState().setActivePlayer(active);
|
||||
service.playUpgrade(active, wayOfTheBear, raince);
|
||||
|
||||
assertTrue(raince.hasKeyword(KeywordAbility.ASSAULT), "Raince should have Assault after Way of the Bear");
|
||||
|
||||
service.fight(raince, defender, active);
|
||||
|
||||
assertEquals(1, defender.getCounters().getAmber(), "Defender should be exalted");
|
||||
assertTrue(defender.isDestroyed(), "Defender should be destroyed by Assault");
|
||||
assertFalse(raince.isDestroyed(), "Raince should survive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEffectOrder_HazardousFirst() {
|
||||
|
||||
active = Player.builder().name("Alice").build();
|
||||
|
||||
active = new Player(active) {
|
||||
@Override
|
||||
public List<BeforeFightEffect> chooseBeforeFightOrder(List<BeforeFightEffect> effects) {
|
||||
return List.of(
|
||||
desc(effects, "Hazardous from Hazard Goblin"),
|
||||
desc(effects, "Assault from Raince Furyheart"),
|
||||
desc(effects, "Exalt the creature Raince Furyheart fights")
|
||||
);
|
||||
}
|
||||
};
|
||||
service.playCreature(active, raince, Flank.LEFT);
|
||||
service.getGameState().setPlayers(active, defenderPlayer);
|
||||
service.getGameState().setActivePlayer(active);
|
||||
service.playUpgrade(active, wayOfTheBear, raince);
|
||||
service.fight(raince, defender, active);
|
||||
|
||||
assertEquals(0, defender.getCounters().getAmber(), "Defender should NOT be exalted if Raince dies first");
|
||||
assertTrue(raince.isDestroyed(), "Raince should be destroyed by Hazardous before fight");
|
||||
assertFalse(defender.isDestroyed(), "Defender should survive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEffectOrder_AssaultThenHazardousThenRaince() {
|
||||
|
||||
active = Player.builder().name("Alice").build();
|
||||
|
||||
active = new Player(active) {
|
||||
@Override
|
||||
public List<BeforeFightEffect> chooseBeforeFightOrder(List<BeforeFightEffect> effects) {
|
||||
return List.of(
|
||||
desc(effects, "Assault from Raince Furyheart"),
|
||||
desc(effects, "Hazardous from Hazard Goblin"),
|
||||
desc(effects, "Exalt the creature Raince Furyheart fights")
|
||||
);
|
||||
}
|
||||
};
|
||||
service.playCreature(active, raince, Flank.LEFT);
|
||||
service.getGameState().setPlayers(active, defenderPlayer);
|
||||
service.getGameState().setActivePlayer(active);
|
||||
service.playUpgrade(active, wayOfTheBear, raince);
|
||||
service.fight(raince, defender, active);
|
||||
|
||||
assertEquals(0, defender.getCounters().getAmber(), "Exalt should not have resolved because assault would have killed defender first");
|
||||
assertTrue(defender.isDestroyed(), "Defender should die from Assault");
|
||||
assertFalse(raince.isDestroyed(), "Raince would survive because the assault would kill defender first");
|
||||
}
|
||||
|
||||
private BeforeFightEffect desc(List<BeforeFightEffect> list, String text) {
|
||||
return list.stream()
|
||||
.filter(e -> e.description().equals(text))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("Missing effect: " + text));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.effect.impl.ScreeyanAbility;
|
||||
import com.ghost.technic.server.card.impl.ArchonIdentityCard;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.GameTurnManager;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ScreeyanTest {
|
||||
|
||||
GameActionService actionService;
|
||||
GameTurnManager turnManager;
|
||||
// GameState gameState;
|
||||
Player alice;
|
||||
Player bob;
|
||||
|
||||
@Test
|
||||
void testScreeyanPreventsOpponentFromChoosingDiscardedHouse() {
|
||||
// Setup players
|
||||
alice = Player.builder()
|
||||
.name("Alice")
|
||||
.archonIdentityCard(ArchonIdentityCard.builder().houses(Set.of(House.BROBNAR, House.SHADOWS, House.UNTAMED)).build())
|
||||
.build();
|
||||
|
||||
bob = Player.builder()
|
||||
.name("Bob")
|
||||
.archonIdentityCard(ArchonIdentityCard.builder().houses(Set.of(House.SANCTUM, House.LOGOS, House.MARS)).build())
|
||||
.deck(new LinkedList<>(List.of(
|
||||
CreatureCard.builder().name("Some Mars Creature").house(House.MARS).build()
|
||||
)))
|
||||
.build();
|
||||
|
||||
var screeyan = CreatureCard.builder()
|
||||
.name("Screeyan")
|
||||
.power(5)
|
||||
.house(House.DIS)
|
||||
.abilities(List.of(new ScreeyanAbility()))
|
||||
.build();
|
||||
|
||||
GameActionService actionService = new GameActionService(new GameState(new EventBus()));
|
||||
GameTurnManager turnManager = new GameTurnManager(actionService.getGameState(), actionService);
|
||||
|
||||
actionService.getGameState().setPlayers(alice, bob);
|
||||
|
||||
actionService.getGameState().setActivePlayer(alice);
|
||||
actionService.playCreature(alice, screeyan, Flank.LEFT);
|
||||
|
||||
// Start Bob’s turn
|
||||
turnManager.endOfTurn(alice);
|
||||
// turnManager.takeTurn(bob);
|
||||
|
||||
// Assert MARS is blocked
|
||||
assertEquals(actionService.getGameState().getRestrictedHouses(bob), Set.of(House.MARS));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ghost.technic.server.card;
|
||||
|
||||
import com.ghost.technic.server.card.effect.impl.SibylWaimareAbility;
|
||||
import com.ghost.technic.server.game.GameTurnManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SibylWaimareTest {
|
||||
|
||||
GameActionService gameActionService;
|
||||
GameState gameState;
|
||||
Player opponent;
|
||||
Player controller;
|
||||
CreatureCard topCreatureCard;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameState = new GameState(new EventBus());
|
||||
gameActionService = new GameActionService(gameState);
|
||||
|
||||
controller = Player.builder().name("Waimare Controller").build();
|
||||
opponent = Player.builder().name("Target Player").build();
|
||||
|
||||
gameState.setPlayers(controller, opponent);
|
||||
|
||||
// Creature that will be exhausted
|
||||
CreatureCard brobnarCreature = CreatureCard.builder()
|
||||
.name("Brobnar Bruiser")
|
||||
.house(House.BROBNAR)
|
||||
.power(4)
|
||||
.armor(1)
|
||||
.ready(true)
|
||||
.build();
|
||||
|
||||
opponent.getPlayArea().getBattleline().addToFlank(brobnarCreature, Flank.LEFT);
|
||||
gameState.setOwner(brobnarCreature, opponent);
|
||||
|
||||
// Deck top card is BROBNAR
|
||||
topCreatureCard = CreatureCard.builder().name("Top card").house(House.BROBNAR).build();
|
||||
opponent.getDeck().add(topCreatureCard);
|
||||
gameState.setOwner(topCreatureCard, opponent);
|
||||
|
||||
// Sibyl Waimare in play
|
||||
CreatureCard sibyl = CreatureCard.builder()
|
||||
.name("Sibyl Waimare")
|
||||
.house(House.DIS)
|
||||
.power(4)
|
||||
.armor(2)
|
||||
.abilities(List.of(new SibylWaimareAbility()))
|
||||
.build();
|
||||
|
||||
controller.getPlayArea().getBattleline().addToFlank(sibyl, Flank.RIGHT);
|
||||
gameState.setOwner(sibyl, controller);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSibylWaimareExhaustsOpponentCreaturesOfDiscardedHouse() {
|
||||
assertTrue(opponent.getPlayArea().getBattleline().getCreatures().getFirst().isReady());
|
||||
|
||||
GameTurnManager turnManager = new GameTurnManager(gameState, gameActionService);
|
||||
turnManager.handleStartOfTurnAbilities(opponent);
|
||||
|
||||
CreatureCard brobnarCreature = opponent.getPlayArea().getBattleline().getCreatures().getFirst();
|
||||
assertFalse(brobnarCreature.isReady(), "Creature of discarded house should be exhausted");
|
||||
assertEquals(topCreatureCard, opponent.getDiscardPile().getFirst(), "Top card should be discarded");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.ghost.technic.server.game;
|
||||
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.counter.Counters;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Slf4j
|
||||
public class DealingDamageTest {
|
||||
|
||||
private GameActionService gameActionService;
|
||||
private CreatureCard creature;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameActionService = new GameActionService(new GameState(new EventBus()));
|
||||
|
||||
creature = CreatureCard.builder()
|
||||
.name("Sacro-Beast")
|
||||
.house(House.UNTAMED)
|
||||
.traits(List.of("Mutant", "Beast"))
|
||||
.power(5)
|
||||
.armor(2)
|
||||
.counters(new Counters())
|
||||
.build();
|
||||
|
||||
// log.info(creature.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFullArmorAbsorbsAllDamage() {
|
||||
AtomicBoolean preventedTriggered = new AtomicBoolean(false);
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.DAMAGE_PREVENTED, event -> {
|
||||
CreatureCard c = event.get("creature");
|
||||
int amount = event.get("amount");
|
||||
assertEquals("Sacro-Beast", c.getName());
|
||||
assertEquals(2, amount);
|
||||
preventedTriggered.set(true);
|
||||
});
|
||||
|
||||
gameActionService.dealDamage(creature, 2);
|
||||
|
||||
assertEquals(0, creature.getCounters().getDmg(), "No damage should be on the creature");
|
||||
assertEquals(0, creature.getArmor(), "All Armor should be consumed");
|
||||
assertTrue(preventedTriggered.get(), "DAMAGE_PREVENTED event should be triggered");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialArmorPartialDamage() {
|
||||
AtomicInteger damageTaken = new AtomicInteger();
|
||||
AtomicInteger damagePrevented = new AtomicInteger();
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.DAMAGE_PREVENTED, event -> {
|
||||
damagePrevented.set(event.get("amount"));
|
||||
});
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.DAMAGE_TAKEN, event -> {
|
||||
damageTaken.set(event.get("amount"));
|
||||
});
|
||||
|
||||
gameActionService.dealDamage(creature, 4); // 2 absorbed, 2 to damage
|
||||
|
||||
assertEquals(2, creature.getCounters().getDmg(), "Creature should have 2 damage");
|
||||
assertEquals(0, creature.getArmor(), "Armor should be reduced to 0");
|
||||
|
||||
assertEquals(2, damagePrevented.get(), "2 damage should be prevented");
|
||||
assertEquals(2, damageTaken.get(), "2 damage should be taken");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoArmorTakesAllDamage() {
|
||||
AtomicInteger damageTaken = new AtomicInteger();
|
||||
|
||||
creature.setArmor(0); // no Armor
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.DAMAGE_TAKEN, event -> {
|
||||
damageTaken.set(event.get("amount"));
|
||||
});
|
||||
|
||||
gameActionService.dealDamage(creature, 3);
|
||||
|
||||
assertEquals(3, creature.getCounters().getDmg(), "All 3 damage should be applied");
|
||||
assertEquals(3, damageTaken.get(), "DAMAGE_TAKEN should show full amount");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZeroDamageStillFiresPreventIfArmor() {
|
||||
AtomicBoolean preventedTriggered = new AtomicBoolean(false);
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.DAMAGE_PREVENTED, event -> {
|
||||
preventedTriggered.set(true);
|
||||
assertEquals(0, (int) event.get("amount"));
|
||||
});
|
||||
|
||||
gameActionService.dealDamage(creature, 0);
|
||||
|
||||
assertEquals(2, creature.getArmor(), "Armor should remain untouched");
|
||||
assertEquals(0, creature.getCounters().getDmg(), "No damage should be dealt");
|
||||
assertTrue(preventedTriggered.get(), "Prevented event should still fire with zero dmg");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ghost.technic.server.game;
|
||||
|
||||
import com.ghost.technic.server.Player;
|
||||
import com.ghost.technic.server.board.Flank;
|
||||
import com.ghost.technic.server.card.playable.PlayableCard;
|
||||
import com.ghost.technic.server.game.event.UsageType;
|
||||
import com.ghost.technic.server.game.event.UsedEventData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ghost.technic.server.card.misc.House;
|
||||
import com.ghost.technic.server.card.playable.usable.impl.CreatureCard;
|
||||
import com.ghost.technic.server.game.GameActionService;
|
||||
import com.ghost.technic.server.game.GameState;
|
||||
import com.ghost.technic.server.game.event.EventBus;
|
||||
import com.ghost.technic.server.game.event.EventType;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@Slf4j
|
||||
class ReapTest {
|
||||
|
||||
private GameActionService gameActionService;
|
||||
private Player player;
|
||||
private CreatureCard creature;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gameActionService = new GameActionService(new GameState(new EventBus()));
|
||||
gameActionService.getGameState().trackRuleOfSix(); // make sure listener is registered
|
||||
|
||||
player = Player.builder().name("Test Player").build();
|
||||
creature = CreatureCard.builder().name("Test Creature").house(House.LOGOS).build();
|
||||
creature.setReady(true); // simulate creature is ready
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReapGainsAmberAndExhaustsCreature() {
|
||||
gameActionService.reap(creature, player);
|
||||
|
||||
assertEquals(1, player.getAmber(), "Player should gain 1 amber after reaping");
|
||||
assertFalse(creature.isReady(), "Creature should be exhausted (not ready) after reaping");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReapPublishesReapedEvent() {
|
||||
AtomicBoolean reapEventTriggered = new AtomicBoolean(false);
|
||||
|
||||
gameActionService.getEventBus().subscribe(EventType.USED, event -> {
|
||||
UsedEventData usedData = event.get("usedEventData");
|
||||
PlayableCard card = usedData.getCard();
|
||||
UsageType usage = usedData.getUsageType();
|
||||
|
||||
if (usage.equals(UsageType.REAP)) {
|
||||
reapEventTriggered.set(true);
|
||||
}
|
||||
log.info("Card used: {}, Usage type: {}", card.getName(), usage);
|
||||
});
|
||||
|
||||
gameActionService.reap(creature, player);
|
||||
|
||||
assertTrue(reapEventTriggered.get(), "REAPED event should have been published");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCannotReapIfExhausted() {
|
||||
creature.setReady(false); // simulate exhausted
|
||||
|
||||
gameActionService.reap(creature, player);
|
||||
|
||||
assertEquals(0, player.getAmber(), "No amber should be gained if creature is exhausted");
|
||||
assertFalse(creature.isReady(), "Creature should remain exhausted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReapLimitedByRuleOfSix() {
|
||||
gameActionService.playCreature(player, creature, Flank.RIGHT);
|
||||
|
||||
// Attempt to reap 6 times, forcefully readying the creature each time, only 5 successful reap should fire
|
||||
for (int i = 0; i < 6; i++) {
|
||||
creature.setReady(true);
|
||||
gameActionService.reap(creature, player);
|
||||
}
|
||||
|
||||
assertTrue(creature.isReady(), "Creature should not exhaust since it was played once and reaped 5 times from rule of six");
|
||||
assertEquals(5, player.getAmber(), "Player should gain 5 amber after 5 legal reaps");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user