From bda85387bada2341e034a6a1b11db22a78a83fc0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:03:23 +0000 Subject: [PATCH 1/6] feat: Add Flame Circle card Adds a new card, Flame Circle, that deals 4 damage to all enemies within a 3-tile radius. This change introduces a reusable and extensible system for area-of-effect cards by adding the following components: - `TilesInRadiusQuery`: A performant query that finds all tiles within a given radius using a breadth-first search. - `ActorsOnTilesQuery`: A query that finds all actors on a given set of tiles. - `DamageActorsExpression`: An expression that applies damage to a list of actors returned by a query. The `CardPlayer` class has been updated to implement the `Target` interface, making it eligible to be targeted by card effects. The `GameStateSerializer` has also been updated to handle the new `CardExpression` classes. --- .../context/game/GameStateSerializer.java | 4 +- .../game/actor/cardplayer/CardPlayer.java | 3 +- .../context/game/card/GameCard.java | 10 ++- .../expression/DamageActorsExpression.java | 31 +++++++++ .../card/query/actor/ActorsOnTilesQuery.java | 38 +++++++++++ .../card/query/tile/TilesInRadiusQuery.java | 65 +++++++++++++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java create mode 100644 src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java create mode 100644 src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java diff --git a/src/main/java/nomadrealms/context/game/GameStateSerializer.java b/src/main/java/nomadrealms/context/game/GameStateSerializer.java index d7993fa9..9a9dd9dc 100644 --- a/src/main/java/nomadrealms/context/game/GameStateSerializer.java +++ b/src/main/java/nomadrealms/context/game/GameStateSerializer.java @@ -29,7 +29,7 @@ import nomadrealms.context.game.card.CardMemory; import nomadrealms.context.game.card.action.Action; import nomadrealms.context.game.card.action.scheduler.CardPlayerActionScheduler; -import nomadrealms.context.game.card.intent.Intent; +import nomadrealms.context.game.card.expression.CardExpression; import nomadrealms.context.game.event.DropItemEvent; import nomadrealms.context.game.event.InputEventFrame; import nomadrealms.context.game.event.ProcChain; @@ -159,7 +159,7 @@ private void registerClasses() { Reflections reflections = new Reflections(new ConfigurationBuilder().forPackage("nomadrealms")); List> superclasses = Arrays.asList( Event.class, - Intent.class, + CardExpression.class, Actor.class, Structure.class, CardPlayerAI.class, diff --git a/src/main/java/nomadrealms/context/game/actor/cardplayer/CardPlayer.java b/src/main/java/nomadrealms/context/game/actor/cardplayer/CardPlayer.java index 101f369a..2779ef8a 100644 --- a/src/main/java/nomadrealms/context/game/actor/cardplayer/CardPlayer.java +++ b/src/main/java/nomadrealms/context/game/actor/cardplayer/CardPlayer.java @@ -9,6 +9,7 @@ import nomadrealms.context.game.actor.HasSpeech; import nomadrealms.context.game.actor.ai.CardPlayerAI; import nomadrealms.context.game.actor.cardplayer.appendage.Appendage; +import nomadrealms.context.game.event.Target; import nomadrealms.context.game.card.WorldCard; import nomadrealms.context.game.card.action.Action; import nomadrealms.context.game.card.action.scheduler.CardPlayerActionScheduler; @@ -22,7 +23,7 @@ import nomadrealms.render.RenderingEnvironment; import nomadrealms.render.ui.custom.speech.SpeechBubble; -public abstract class CardPlayer implements Actor, HasSpeech { +public abstract class CardPlayer implements Actor, HasSpeech, Target { private final CardPlayerActionScheduler actionScheduler = new CardPlayerActionScheduler(); diff --git a/src/main/java/nomadrealms/context/game/card/GameCard.java b/src/main/java/nomadrealms/context/game/card/GameCard.java index 4d055b6c..7b1d228c 100644 --- a/src/main/java/nomadrealms/context/game/card/GameCard.java +++ b/src/main/java/nomadrealms/context/game/card/GameCard.java @@ -9,6 +9,7 @@ import nomadrealms.context.game.card.expression.BuryAnySeedExpression; import nomadrealms.context.game.card.expression.CardExpression; import nomadrealms.context.game.card.expression.CreateStructureExpression; +import nomadrealms.context.game.card.expression.DamageActorsExpression; import nomadrealms.context.game.card.expression.DamageExpression; import nomadrealms.context.game.card.expression.EditTileExpression; import nomadrealms.context.game.card.expression.GatherExpression; @@ -18,9 +19,11 @@ import nomadrealms.context.game.card.expression.SurfaceCardExpression; import nomadrealms.context.game.card.expression.TeleportExpression; import nomadrealms.context.game.card.expression.TeleportNoTargetExpression; +import nomadrealms.context.game.card.query.actor.ActorsOnTilesQuery; import nomadrealms.context.game.card.query.actor.SelfQuery; import nomadrealms.context.game.card.query.card.LastResolvedCardQuery; import nomadrealms.context.game.card.query.tile.PreviousTileQuery; +import nomadrealms.context.game.card.query.tile.TilesInRadiusQuery; import nomadrealms.context.game.card.target.TargetingInfo; import nomadrealms.context.game.world.map.tile.factory.TileType; @@ -100,7 +103,12 @@ public enum GameCard implements Card { "Wooden Chest", "Create a chest on target tile", new CreateStructureExpression(StructureType.CHEST), - new TargetingInfo(HEXAGON, 1)); + new TargetingInfo(HEXAGON, 1)), + FLAME_CIRCLE( + "Flame Circle", + "Deal 4 damage to all enemies within radius 3", + new DamageActorsExpression(new ActorsOnTilesQuery(new TilesInRadiusQuery(3), true), 4), + new TargetingInfo(NONE, 1)); private final String title; private final String description; diff --git a/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java b/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java new file mode 100644 index 00000000..49c1c861 --- /dev/null +++ b/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java @@ -0,0 +1,31 @@ +package nomadrealms.context.game.card.expression; + +import java.util.List; +import java.util.stream.Collectors; + +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.effect.DamageEffect; +import nomadrealms.context.game.card.effect.Effect; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.event.Target; +import nomadrealms.context.game.world.World; + +public class DamageActorsExpression implements CardExpression { + + private final Query actorsQuery; + private final int amount; + + public DamageActorsExpression(Query actorsQuery, int amount) { + this.actorsQuery = actorsQuery; + this.amount = amount; + } + + @Override + public List effects(World world, Target target, CardPlayer source) { + List actors = actorsQuery.find(world, source); + return actors.stream() + .map(actor -> new DamageEffect(actor, source, amount)) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java new file mode 100644 index 00000000..628cac32 --- /dev/null +++ b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java @@ -0,0 +1,38 @@ +package nomadrealms.context.game.card.query.actor; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; + +public class ActorsOnTilesQuery implements Query { + + private final Query tileQuery; + private final boolean excludeSource; + + public ActorsOnTilesQuery(Query tileQuery, boolean excludeSource) { + this.tileQuery = tileQuery; + this.excludeSource = excludeSource; + } + + public ActorsOnTilesQuery(Query tileQuery) { + this(tileQuery, false); + } + + @Override + public List find(World world, CardPlayer source) { + List tiles = tileQuery.find(world, source); + Stream stream = world.actors.stream() + .filter(CardPlayer.class::isInstance) + .map(CardPlayer.class::cast) + .filter(actor -> tiles.contains(actor.tile())); + if (excludeSource) { + stream = stream.filter(actor -> actor != source); + } + return stream.collect(Collectors.toList()); + } + +} diff --git a/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java new file mode 100644 index 00000000..22955685 --- /dev/null +++ b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java @@ -0,0 +1,65 @@ +package nomadrealms.context.game.card.query.tile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; + +public class TilesInRadiusQuery implements Query { + + private final int radius; + + public TilesInRadiusQuery(int radius) { + this.radius = radius; + } + + @Override + public List find(World world, CardPlayer source) { + Tile startTile = source.tile(); + if (startTile == null) { + return List.of(); + } + + List tiles = new ArrayList<>(); + Queue queue = new LinkedList<>(); + Map distance = new HashMap<>(); + + queue.add(startTile); + distance.put(startTile, 0); + + while (!queue.isEmpty()) { + Tile currentTile = queue.poll(); + tiles.add(currentTile); + + int currentDistance = distance.get(currentTile); + if (currentDistance < radius) { + for (Tile neighbor : getNeighbors(world, currentTile)) { + if (neighbor != null && !distance.containsKey(neighbor)) { + distance.put(neighbor, currentDistance + 1); + queue.add(neighbor); + } + } + } + } + return tiles; + } + + private List getNeighbors(World world, Tile tile) { + List neighbors = new ArrayList<>(); + neighbors.add(tile.ul(world)); + neighbors.add(tile.um(world)); + neighbors.add(tile.ur(world)); + neighbors.add(tile.dl(world)); + neighbors.add(tile.dm(world)); + neighbors.add(tile.dr(world)); + return neighbors; + } + +} From 08b1f42beb256b0f0ef016d5ef9cff36b73e7637 Mon Sep 17 00:00:00 2001 From: Lunkle Date: Tue, 30 Dec 2025 10:12:27 -0500 Subject: [PATCH 2/6] Update TilesInRadiusQuery.java --- .../card/query/tile/TilesInRadiusQuery.java | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java index 22955685..469c4a4d 100644 --- a/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java +++ b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java @@ -1,5 +1,7 @@ package nomadrealms.context.game.card.query.tile; +import static java.util.Collections.emptyList; + import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; @@ -14,52 +16,52 @@ public class TilesInRadiusQuery implements Query { - private final int radius; + private final int radius; - public TilesInRadiusQuery(int radius) { - this.radius = radius; - } + public TilesInRadiusQuery(int radius) { + this.radius = radius; + } - @Override - public List find(World world, CardPlayer source) { - Tile startTile = source.tile(); - if (startTile == null) { - return List.of(); - } + @Override + public List find(World world, CardPlayer source) { + Tile startTile = source.tile(); + if (startTile == null) { + return emptyList(); + } - List tiles = new ArrayList<>(); - Queue queue = new LinkedList<>(); - Map distance = new HashMap<>(); + List tiles = new ArrayList<>(); + Queue queue = new LinkedList<>(); + Map distance = new HashMap<>(); - queue.add(startTile); - distance.put(startTile, 0); + queue.add(startTile); + distance.put(startTile, 0); - while (!queue.isEmpty()) { - Tile currentTile = queue.poll(); - tiles.add(currentTile); + while (!queue.isEmpty()) { + Tile currentTile = queue.poll(); + tiles.add(currentTile); - int currentDistance = distance.get(currentTile); - if (currentDistance < radius) { - for (Tile neighbor : getNeighbors(world, currentTile)) { - if (neighbor != null && !distance.containsKey(neighbor)) { - distance.put(neighbor, currentDistance + 1); - queue.add(neighbor); - } - } - } - } - return tiles; - } + int currentDistance = distance.get(currentTile); + if (currentDistance < radius) { + for (Tile neighbor : getNeighbors(world, currentTile)) { + if (neighbor != null && !distance.containsKey(neighbor)) { + distance.put(neighbor, currentDistance + 1); + queue.add(neighbor); + } + } + } + } + return tiles; + } - private List getNeighbors(World world, Tile tile) { - List neighbors = new ArrayList<>(); - neighbors.add(tile.ul(world)); - neighbors.add(tile.um(world)); - neighbors.add(tile.ur(world)); - neighbors.add(tile.dl(world)); - neighbors.add(tile.dm(world)); - neighbors.add(tile.dr(world)); - return neighbors; - } + private List getNeighbors(World world, Tile tile) { + List neighbors = new ArrayList<>(); + neighbors.add(tile.ul(world)); + neighbors.add(tile.um(world)); + neighbors.add(tile.ur(world)); + neighbors.add(tile.dl(world)); + neighbors.add(tile.dm(world)); + neighbors.add(tile.dr(world)); + return neighbors; + } } From 4b6805645b7b50c171e8a030b01628f901ec16b4 Mon Sep 17 00:00:00 2001 From: Lunkle Date: Tue, 30 Dec 2025 12:47:41 -0500 Subject: [PATCH 3/6] Remove outdated TODO comment in GameCard Deleted a resolved or obsolete TODO comment regarding the Rewind card's processing phase in GameCard.java. --- src/main/java/nomadrealms/context/game/card/GameCard.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/nomadrealms/context/game/card/GameCard.java b/src/main/java/nomadrealms/context/game/card/GameCard.java index 66f56a61..138f5b71 100644 --- a/src/main/java/nomadrealms/context/game/card/GameCard.java +++ b/src/main/java/nomadrealms/context/game/card/GameCard.java @@ -60,8 +60,6 @@ public enum GameCard implements Card { new TeleportExpression(10), new TargetingInfo(HEXAGON, 2)), REWIND( - //TODO: Rewind currently attempts to rewind itself since it is the last played card. Need to introduce an - // additional processing phase between playing a card and resolving its effects to avoid this. "Rewind", "teleport", "Teleport to the last hexagon you occupied. Surface the last card you played.", From 2685a11319359224ad717db8ec74e1648a9994d4 Mon Sep 17 00:00:00 2001 From: Lunkle Date: Tue, 30 Dec 2025 13:07:08 -0500 Subject: [PATCH 4/6] Update GameCard.java --- src/main/java/nomadrealms/context/game/card/GameCard.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/nomadrealms/context/game/card/GameCard.java b/src/main/java/nomadrealms/context/game/card/GameCard.java index 138f5b71..0d9504f2 100644 --- a/src/main/java/nomadrealms/context/game/card/GameCard.java +++ b/src/main/java/nomadrealms/context/game/card/GameCard.java @@ -117,6 +117,7 @@ public enum GameCard implements Card { new TargetingInfo(HEXAGON, 1)), FLAME_CIRCLE( "Flame Circle", + "meteor", "Deal 4 damage to all enemies within radius 3", new DamageActorsExpression(new ActorsOnTilesQuery(new TilesInRadiusQuery(3), true), 4), new TargetingInfo(NONE, 1)); From 7034b96129913f7978fd38e415a79e3785a5b311 Mon Sep 17 00:00:00 2001 From: Lunkle Date: Tue, 30 Dec 2025 13:09:44 -0500 Subject: [PATCH 5/6] Add FLAME_CIRCLE to Cycle & Search beginner deck The Cycle & Search beginner deck now includes the FLAME_CIRCLE card, providing it with an initial card instead of being empty. --- src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java b/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java index 8c7afe5d..bde61c1b 100644 --- a/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java +++ b/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java @@ -2,6 +2,7 @@ import static nomadrealms.context.game.card.GameCard.ATTACK; import static nomadrealms.context.game.card.GameCard.CREATE_ROCK; +import static nomadrealms.context.game.card.GameCard.FLAME_CIRCLE; import static nomadrealms.context.game.card.GameCard.GATHER; import static nomadrealms.context.game.card.GameCard.HEAL; import static nomadrealms.context.game.card.GameCard.MELEE_ATTACK; @@ -23,7 +24,7 @@ public enum BeginnerDecks { REWIND )), PUNCH_AND_GRAPPLE("Punch & Grapple", new DeckList(HEAL, ATTACK, HEAL, MELEE_ATTACK)), - CYCLE_AND_SEARCH("Cycle & Search ", new DeckList()), + CYCLE_AND_SEARCH("Cycle & Search ", new DeckList(FLAME_CIRCLE)), AGRICULTURE_AND_LABOUR("Agriculture & Labour", new DeckList(GATHER, CREATE_ROCK, WOODEN_CHEST, TILL_SOIL, PLANT_SEED)); private final String name; From 4ece76c03cb1fdd1bde8bc48983ccc25a86005aa Mon Sep 17 00:00:00 2001 From: Lunkle Date: Tue, 30 Dec 2025 13:14:56 -0500 Subject: [PATCH 6/6] Refactor queries to use Actor instead of CardPlayer Updated DamageActorsExpression and ActorsOnTilesQuery to operate on the Actor type rather than CardPlayer. This change generalizes the queries and expressions to support a broader range of actor types, improving flexibility for future features. --- .../expression/DamageActorsExpression.java | 27 ++++++++++--------- .../card/query/actor/ActorsOnTilesQuery.java | 12 ++++++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java b/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java index 49c1c861..10ed88c9 100644 --- a/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java +++ b/src/main/java/nomadrealms/context/game/card/expression/DamageActorsExpression.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.stream.Collectors; +import nomadrealms.context.game.actor.Actor; import nomadrealms.context.game.actor.cardplayer.CardPlayer; import nomadrealms.context.game.card.effect.DamageEffect; import nomadrealms.context.game.card.effect.Effect; @@ -12,20 +13,20 @@ public class DamageActorsExpression implements CardExpression { - private final Query actorsQuery; - private final int amount; + private final Query actors; + private final int amount; - public DamageActorsExpression(Query actorsQuery, int amount) { - this.actorsQuery = actorsQuery; - this.amount = amount; - } + public DamageActorsExpression(Query actors, int amount) { + this.actors = actors; + this.amount = amount; + } - @Override - public List effects(World world, Target target, CardPlayer source) { - List actors = actorsQuery.find(world, source); - return actors.stream() - .map(actor -> new DamageEffect(actor, source, amount)) - .collect(Collectors.toList()); - } + @Override + public List effects(World world, Target target, CardPlayer source) { + List result = actors.find(world, source); + return result.stream() + .map(actor -> new DamageEffect(actor, source, amount)) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java index 628cac32..ddd43264 100644 --- a/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java +++ b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java @@ -1,14 +1,17 @@ package nomadrealms.context.game.card.query.actor; +import static java.util.stream.Collectors.toList; + import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; + +import nomadrealms.context.game.actor.Actor; import nomadrealms.context.game.actor.cardplayer.CardPlayer; import nomadrealms.context.game.card.query.Query; import nomadrealms.context.game.world.World; import nomadrealms.context.game.world.map.area.Tile; -public class ActorsOnTilesQuery implements Query { +public class ActorsOnTilesQuery implements Query { private final Query tileQuery; private final boolean excludeSource; @@ -23,8 +26,9 @@ public ActorsOnTilesQuery(Query tileQuery) { } @Override - public List find(World world, CardPlayer source) { + public List find(World world, CardPlayer source) { List tiles = tileQuery.find(world, source); + // TODO: Tiles need to store the actors on them for efficiency. Once we have that, we can optimize this. Stream stream = world.actors.stream() .filter(CardPlayer.class::isInstance) .map(CardPlayer.class::cast) @@ -32,7 +36,7 @@ public List find(World world, CardPlayer source) { if (excludeSource) { stream = stream.filter(actor -> actor != source); } - return stream.collect(Collectors.toList()); + return stream.collect(toList()); } }