diff --git a/README.md b/README.md index e00398f..e7dea83 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,39 @@ # Alone -Single-player ASCII roguelike/roguelite focused on surviving, alone, on an island inhabited by animals. +[![Latest Github release](https://img.shields.io/github/release/fabioticconi/alone-rl.svg)](https://github.com/fabioticconi/alone-rl/releases/latest) + +Single-player ASCII roguelike focused on surviving, alone, on an island inhabited by animals. The main inspiration is from [Unreal World](http://unrealworld.fi) and [Wayward](http://www.waywardgame.com), with a much simpler gameplay. It's a real-time game but it defaults to a turn-based modality where the world only advances during player actions, for as long as the player action runs. Pure real-time gameplay can be toggled. -**NB: this is not even in alpha state.** Lurk freely if you like the concept, but *know* this is not playable, -by far. -Keep an eye on the [release](https://github.com/fabioticconi/alone-the-roguelite/releases) area, for the future. +**NB: this is not ready yet.** Keep an eye on the [releases page](https://github.com/fabioticconi/alone-the-roguelite/releases) +for a stable release. ## Controls * **`directional arrows` to move** (hold two together for diagonal movement, eg UP+RIGHT to go north-east) - Move into creatures to attack them (message/combat log coming soon), trees to cut them, boulders to crush them. - For the last two you need proper tools (a cutting weapon for cutting three, not craftable yet, and a blunt weapon - for crushing boulders: you can use a stone for that) - -* **`g` to get the first item** on the ground you are currently positioned on (stones, sticks, corpses, tree trunks.. - there's no inventory limit for now). + Move into creatures to attack them, trees to cut them, boulders to crush them. + For the last two you need proper tools (a cutting weapon for cutting three and a blunt weapon + for crushing boulders). +* **`g` to get an item**. You must move onto it first. There is no inventory limit. + * **`d` to open the Drop screen**. You will be able to choose which item to drop. * **`e` to open the Eat screen**. You can only eat corpses that you have taken from the ground, for now. * **`w` to wear or wield an object** via the Equip screen. Stones and branches will do nicely for now. - + * **`l` to open the Look screen**. Move around to choose a target (if you have Line of Sight) and press **`t`** to throw an equipped weapon in that direction. +* **`c` to open the Craft screen**. A list of recipes is loaded anew from a yaml file and displayed. See + [Crafting](https://github.com/fabioticconi/alone-rl#crafting) for details. + There are also some special commands: * **`Ctrl+SPACE`** to toggle real-time/turn-based behaviour @@ -38,21 +41,19 @@ There are also some special commands: * **`SPACE`** to pause/unpause if you are on real-time mode; if you are on turn-based mode, keep `SPACE` pressed to temporarily run the game in real-time (needed, for example, to recover stamina when you finish it, or regenerate health). - + * **`F1`** removes the speed delay of the player. Useful to test the game without having to suffer the movement delays. Will be removed in the final version. - + * **`F2`** restores the correct player speed. Will be removed in the final version. - + ## Screenshots This is how the game looks when run (**very** preliminary GUI): ![](screenshots/gameplay.gif) -A few wolves are chasing rabbits, while pumas manage to bring a buffalo down. - ### Map ![](screenshots/orig_map.png) @@ -74,6 +75,8 @@ Later the terrain will be configurable by the user. You can also see some rivers (still in-progress, not in the game yet) flowing from high to low places. +Note: this is only temporary. In the final game the terrain will be randomised. + ## Features ### Field of view @@ -85,14 +88,14 @@ Pathfinding is both precise and efficient thanks to an AStar implementation that to plan a course to a target position. What this means for the end user is that the game doesn't cheat. It doesn't magically make creatures see you -if they shouldn't. The other creatures can only do what *you* also can. +if they shouldn't. The other creatures can only do what *you* can also do. -### Simple Ecology Simulation +### Simple Ecology -Creatures don't "pop" or "spawn", they don't just appear when needed but they keep going even when the +Creatures don't "pop" or "spawn", they don't just appear when needed but they *exist*; and they keep going even when the player is not looking. -This is the main difference between Alone and most roguelikes/roguelites. It makes it a simulation game, to an extent. +This is the main difference between `AloneRL` and most roguelikes/roguelites. It makes it a simulation game, to an extent. Different creatures have different set of behaviours: @@ -116,5 +119,8 @@ Escape predators, steal their carcasses if you can, or kill any walking thing an ### Crafting -**Not implemented yet**. It will cover the basic primitive technology of a Neolithic hunter, eg stone knives, spears and axes, +It will cover the basic primitive technology of a Neolithic hunter, eg stone knives, spears and axes, simple bark protection, a shelter, maybe rudimentary pit traps and extraction of parts from dead animals. + +All currently implemented recipes can be seen (and modified, added or removed) in the file +[crafting.yml](data/crafting.yml). diff --git a/alone-rl.iml b/alone-rl.iml index 21b5596..a202107 100644 --- a/alone-rl.iml +++ b/alone-rl.iml @@ -20,10 +20,11 @@ - + - - + + + \ No newline at end of file diff --git a/data/crafting.yml b/data/crafting.yml index 3aa4910..df53828 100644 --- a/data/crafting.yml +++ b/data/crafting.yml @@ -1,41 +1,20 @@ -branch: - name: a branch - #source: - # external: tree +sharp-stone: + sources: stone + tools: stone -tree-trunk: - name: a tree trunk - #source: - # external: tree - tool: axe +small-sharp-stone: + sources: sharp-stone + tools: stone -stone: - name: a stone - #source: - # external: boulder - tool: stone - n: 3 +stone-hammer: + sources: [stone, branch, vine] + tools: stone -sharp-stone: - name: a sharp stone - source: stone - tool: stone +stone-axe: + sources: [sharp-stone, branch, vine] + tools: stone -#small-sharp-stone: -# name: a small, sharp stone -# source: [sharp-stone] -# tool: stone -# n: 2 -# -#stone-hammer: -# name: a stone hammer -# source: [stone, branch] -# -#stone-axe: -# name: a stone axe -# source: [sharp-stone, branch] -# -#stone-spear: -# name: a stone spear -# source: [small-sharp-stone, tree-trunk] +stone-spear: + sources: [small-sharp-stone, trunk, vine] + tools: stone diff --git a/data/items.yml b/data/items.yml index f3ad73a..44e4576 100644 --- a/data/items.yml +++ b/data/items.yml @@ -1,12 +1,140 @@ stone: - name: a stone + name: a round stone + wearable: + where: HANDS + weapon: + damageType: BLUNT + damage: 2 + sprite: + c: 7 + col: + red: 88 + green: 88 + blue: 88 branch: - name: a branch + name: a sturdy branch + wearable: + where: HANDS + weapon: + damageType: BLUNT + damage: 1 + sprite: + c: '/' + col: + red: 141 + green: 104 + blue: 21 + +vine: + name: a thin, flexible branch + sprite: + c: 239 + col: + red: 36 + green: 122 + blue: 7 trunk: - name: a tree trunk + name: a fallen tree + sprite: + c: 22 + col: + red: 141 + green: 104 + blue: 21 boulder: - name: a boulder + name: a big boulder + sprite: + c: '#' + col: + red: 88 + green: 88 + blue: 88 + shadowView: true + obstacle: {} + crushable: {} + +tree: + name: a mature tree + sprite: + c: T + col: + red: 0 + green: 205 + blue: 113 + shadowView: true + obstacle: {} + cuttable: {} + +sharp-stone: + name: a sharp stone + wearable: + where: HANDS + weapon: + damageType: SLASH + damage: 2 + sprite: + c: 4 + col: + red: 88 + green: 88 + blue: 88 + +small-sharp-stone: + name: a small, sharp stone + wearable: + where: HANDS + weapon: + damageType: POINT + damage: 2 + sprite: + c: 17 + col: + red: 88 + green: 88 + blue: 88 + +stone-hammer: + name: a stone hammer + wearable: + where: HANDS + weapon: + damageType: BLUNT + damage: 4 + sprite: + c: 209 + col: + red: 141 + green: 104 + blue: 21 + +stone-axe: + name: a stone axe + wearable: + where: HANDS + weapon: + damageType: SLASH + damage: 4 + sprite: + c: 184 + col: + red: 141 + green: 104 + blue: 21 + +stone-spear: + name: a stone spear + wearable: + where: HANDS + weapon: + damageType: POINT + damage: 4 + sprite: + c: 244 + col: + red: 141 + green: 104 + blue: 21 diff --git a/pom.xml b/pom.xml index a75def4..bfc9bf8 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ com.github.fabioticconi alone-rl - 0.1.1 + 0.2.0 jar AloneRL @@ -74,14 +74,31 @@ 4.12 test + com.fasterxml.jackson.core jackson-databind - 2.3.1 + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + + com.fasterxml.jackson + jackson-bom + 2.9.0 + import + pom + + + + diff --git a/screenshots/gameplay.gif b/screenshots/gameplay.gif index e58f5df..1e0b896 100644 Binary files a/screenshots/gameplay.gif and b/screenshots/gameplay.gif differ diff --git a/src/main/java/com/github/fabioticconi/alone/Main.java b/src/main/java/com/github/fabioticconi/alone/Main.java index f835cb6..426d95c 100644 --- a/src/main/java/com/github/fabioticconi/alone/Main.java +++ b/src/main/java/com/github/fabioticconi/alone/Main.java @@ -25,6 +25,10 @@ import com.artemis.link.EntityLinkManager; import com.artemis.managers.PlayerManager; import com.artemis.utils.BitVector; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import com.github.fabioticconi.alone.behaviours.*; import com.github.fabioticconi.alone.constants.Options; import com.github.fabioticconi.alone.screens.*; @@ -68,6 +72,10 @@ public Main() throws IOException final Properties properties = new Properties(); properties.load(this.getClass().getResourceAsStream("/project.properties")); + final YAMLFactory factory = new YAMLFactory(); + final ObjectMapper mapper = new ObjectMapper(factory).registerModule(new ParameterNamesModule()) + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + final WorldConfiguration config; config = new WorldConfiguration(); // first thing to be loaded @@ -75,6 +83,7 @@ public Main() throws IOException // POJO config.register(new Random()); config.register(properties); + config.register(mapper); // passive systems, one-timers, managers etc config.setSystem(EntityLinkManager.class); config.setSystem(BootstrapSystem.class); @@ -139,12 +148,11 @@ public static void main(final String[] args) throws IOException } /** - * In real-time mode, space means pause/unpause the game, while - * in turn-based mode, space means unpause the game until SPACE is released. + * Makes sure that the game is paused */ public static void pause() { - Main.paused = Main.realtime && !Main.paused; + Main.paused = true; } public void loop() diff --git a/src/main/java/com/github/fabioticconi/alone/components/Armour.java b/src/main/java/com/github/fabioticconi/alone/components/Armour.java index 6ab8334..64850d4 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Armour.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Armour.java @@ -29,6 +29,8 @@ */ public class Armour extends Component { + // TODO we can improve this by using a primitive float array and WeaponType ordinals as indeces. + // which is what EnumMap does internally. We'd avoid autoboxing. public final EnumMap defences; public Armour() diff --git a/src/main/java/com/github/fabioticconi/alone/components/Tree.java b/src/main/java/com/github/fabioticconi/alone/components/Cuttable.java similarity index 95% rename from src/main/java/com/github/fabioticconi/alone/components/Tree.java rename to src/main/java/com/github/fabioticconi/alone/components/Cuttable.java index 34529cc..2dce47b 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Tree.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Cuttable.java @@ -24,6 +24,6 @@ * Author: Fabio Ticconi * Date: 07/10/17 */ -public class Tree extends Component +public class Cuttable extends Component { } diff --git a/src/main/java/com/github/fabioticconi/alone/components/Name.java b/src/main/java/com/github/fabioticconi/alone/components/Name.java index f21c1f2..d61d50a 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Name.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Name.java @@ -27,14 +27,17 @@ public class Name extends Component { public final String name; + public final String tag; public Name() { this.name = ""; + this.tag = ""; } - public Name(final String name) + public Name(final String name, final String tag) { this.name = name; + this.tag = tag; } } diff --git a/src/main/java/com/github/fabioticconi/alone/components/LightBlocker.java b/src/main/java/com/github/fabioticconi/alone/components/Obstacle.java similarity index 86% rename from src/main/java/com/github/fabioticconi/alone/components/LightBlocker.java rename to src/main/java/com/github/fabioticconi/alone/components/Obstacle.java index a539be7..be1736a 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/LightBlocker.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Obstacle.java @@ -24,6 +24,7 @@ * Author: Fabio Ticconi * Date: 10/09/17 */ -public class LightBlocker extends Component +public class Obstacle extends Component { + // TODO eventually, we might need a parameter to say: "this only blocks light, not movement" } diff --git a/src/main/java/com/github/fabioticconi/alone/components/Path.java b/src/main/java/com/github/fabioticconi/alone/components/Path.java index a3e7d6b..706e14a 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Path.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Path.java @@ -31,6 +31,7 @@ public class Path extends Component { public float cooldown; public List steps; + public int i; public Path() { @@ -44,6 +45,7 @@ public Path(final float cooldown, final List steps) public void set(final float cooldown, final List steps) { + this.i = 0; this.cooldown = cooldown; this.steps = steps; } diff --git a/src/main/java/com/github/fabioticconi/alone/components/Sprite.java b/src/main/java/com/github/fabioticconi/alone/components/Sprite.java index 7db6f2d..c1743f4 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Sprite.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Sprite.java @@ -45,6 +45,11 @@ public Sprite(final char c, final Color col) this(c, col, false); } + public Sprite(final char c, final String hexCol) + { + this(c, Color.decode(hexCol), false); + } + public Sprite(final char c, final Color col, final boolean shadowView) { this.c = c; diff --git a/src/main/java/com/github/fabioticconi/alone/components/Wearable.java b/src/main/java/com/github/fabioticconi/alone/components/Wearable.java index e9292e0..55bd1e5 100644 --- a/src/main/java/com/github/fabioticconi/alone/components/Wearable.java +++ b/src/main/java/com/github/fabioticconi/alone/components/Wearable.java @@ -19,6 +19,7 @@ package com.github.fabioticconi.alone.components; import com.artemis.Component; +import com.github.fabioticconi.alone.constants.BodyPart; /** * Author: Fabio Ticconi @@ -26,4 +27,22 @@ */ public class Wearable extends Component { + public BodyPart where; + + public Wearable() + { + + } + + public Wearable(final BodyPart where) + { + this.where = where; + } + + public Wearable set(final BodyPart where) + { + this.where = where; + + return this; + } } diff --git a/src/main/java/com/github/fabioticconi/alone/constants/BodyPart.java b/src/main/java/com/github/fabioticconi/alone/constants/BodyPart.java new file mode 100644 index 0000000..6bb7ed3 --- /dev/null +++ b/src/main/java/com/github/fabioticconi/alone/constants/BodyPart.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 Fabio Ticconi + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package com.github.fabioticconi.alone.constants; + +/** + * Author: Fabio Ticconi + * Date: 12/11/17 + */ +public enum BodyPart +{ + HEAD, + BODY, + HANDS +} diff --git a/src/main/java/com/github/fabioticconi/alone/messages/CraftMsg.java b/src/main/java/com/github/fabioticconi/alone/messages/CraftMsg.java new file mode 100644 index 0000000..33b7413 --- /dev/null +++ b/src/main/java/com/github/fabioticconi/alone/messages/CraftMsg.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Fabio Ticconi + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package com.github.fabioticconi.alone.messages; + +/** + * Author: Fabio Ticconi + * Date: 14/11/17 + */ +public class CraftMsg extends AbstractMessage +{ + @Override + public String format() + { + return String.format("%s successfully %s %s", actor, thirdPerson ? "crafts" : "craft", target.toLowerCase()); + } +} diff --git a/src/main/java/com/github/fabioticconi/alone/screens/AbstractScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/AbstractScreen.java index a5b7228..f5854d5 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/AbstractScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/AbstractScreen.java @@ -21,7 +21,7 @@ import asciiPanel.AsciiPanel; import com.artemis.managers.PlayerManager; import com.artemis.utils.BitVector; -import com.github.fabioticconi.alone.components.Inventory; +import com.github.fabioticconi.alone.utils.Util; import net.mostlyoriginal.api.system.core.PassiveSystem; import java.util.Collections; @@ -36,43 +36,9 @@ */ public abstract class AbstractScreen extends PassiveSystem implements Screen { - PlayerManager pManager; + public static final Letter[] ALL = Letter.values(); - public enum Letter - { - a, - b, - c, - d, - e, - f, - g, - h, - i, - j, - k, - l, - m, - n, - o, - p, - q, - r, - s, - t, - u, - v, - w, - x, - y, - z; - - @Override - public String toString() - { - return name() + ")"; - } - } + PlayerManager pManager; public abstract String header(); @@ -85,14 +51,18 @@ void drawHeader(final AsciiPanel terminal) void drawList(final AsciiPanel terminal, final List list) { + if (list.isEmpty()) + return; + final int maxSize = AbstractScreen.Letter.values().length; - final int size = Math.min(maxSize, list.size()); + final int size = Math.min(maxSize, list.size()); + final int space = Util.clamp(maxSize / size, 1, 4); - for (int i = 0, starty = terminal.getHeightInCharacters() / 2 - size / 2; i < size; i++) + for (int i = 0, starty = terminal.getHeightInCharacters() / 2 - (size * space / 2); i < size; i++) { - final String entry = list.get(i); + final String entry = ALL[i] + " " + list.get(i); - terminal.writeCenter(entry, starty + (size < maxSize / 2 ? i * 2 : i)); + terminal.writeCenter(entry, starty + i * space); } } @@ -115,4 +85,40 @@ int getTargetIndex(final BitVector keys) return -1; } + + public enum Letter + { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + u, + v, + w, + x, + y, + z; + + @Override + public String toString() + { + return name() + ")"; + } + } } diff --git a/src/main/java/com/github/fabioticconi/alone/screens/CraftItemScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/CraftItemScreen.java index 60f2024..fc813e1 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/CraftItemScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/CraftItemScreen.java @@ -19,10 +19,17 @@ package com.github.fabioticconi.alone.screens; import asciiPanel.AsciiPanel; +import com.artemis.ComponentMapper; import com.artemis.utils.BitVector; +import com.github.fabioticconi.alone.components.Inventory; +import com.github.fabioticconi.alone.messages.CannotMsg; +import com.github.fabioticconi.alone.messages.CraftMsg; +import com.github.fabioticconi.alone.systems.CraftSystem; +import com.github.fabioticconi.alone.systems.MessageSystem; import com.github.fabioticconi.alone.systems.ScreenSystem; import java.awt.event.KeyEvent; +import java.util.Arrays; /** * Author: Fabio Ticconi @@ -30,20 +37,42 @@ */ public class CraftItemScreen extends AbstractScreen { + ComponentMapper mInventory; + ScreenSystem screen; + CraftSystem sCraft; + MessageSystem msg; + CraftScreen craftScreen; @Override public String header() { - return "Crafting item:"; + return "Craft " + craftScreen.craftItem.tag + "?"; } @Override public float handleKeys(final BitVector keys) { + final int playerId = pManager.getEntitiesOfPlayer("player").get(0).getId(); + if (keys.get(KeyEvent.VK_ESCAPE)) + screen.select(CraftScreen.class); + else if (keys.get(KeyEvent.VK_ENTER)) + { + System.out.println(craftScreen.craftItem); + final int id = sCraft.craftItem(playerId, craftScreen.craftItem); + if (id >= 0) + { + final Inventory inv = mInventory.get(playerId); + inv.items.add(id); + msg.send(playerId, id, new CraftMsg()); + } + else + msg.send(playerId, id, new CannotMsg("craft")); + screen.select(PlayScreen.class); + } keys.clear(); @@ -53,10 +82,20 @@ public float handleKeys(final BitVector keys) @Override public void display(final AsciiPanel terminal) { + terminal.clear(); + drawHeader(terminal); - terminal.writeCenter(craftScreen.craftItem, terminal.getHeightInCharacters()/2); + int height = terminal.getHeightInCharacters() / 3; + + terminal.writeCenter("Consumes:", height); + terminal.writeCenter(Arrays.toString(craftScreen.craftItem.sources), height + 2); + + height += 8; + + terminal.writeCenter("Tools needed:", height); + terminal.writeCenter(Arrays.toString(craftScreen.craftItem.tools), height + 2); - // TODO + terminal.writeCenter("[ type ENTER to confirm ]", terminal.getHeightInCharacters() - 2); } } diff --git a/src/main/java/com/github/fabioticconi/alone/screens/CraftScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/CraftScreen.java index 8140563..267b119 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/CraftScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/CraftScreen.java @@ -31,6 +31,7 @@ import com.github.fabioticconi.alone.systems.ScreenSystem; import java.awt.event.KeyEvent; +import java.util.List; /** * Author: Fabio Ticconi @@ -49,19 +50,26 @@ public class CraftScreen extends AbstractScreen PlayerManager pManager; - String craftItem = "Test"; + List recipeNames; + CraftSystem.CraftItem craftItem; @Override public float handleKeys(final BitVector keys) { if (keys.get(KeyEvent.VK_ESCAPE)) + { + recipeNames = null; screen.select(PlayScreen.class); + } else { final int pos = getTargetIndex(keys); - if (pos >= 0) + if (pos >= 0 && pos < recipeNames.size()) + { + craftItem = sCraft.getRecipes().get(recipeNames.get(pos)); screen.select(CraftItemScreen.class); + } } keys.clear(); @@ -72,9 +80,14 @@ public float handleKeys(final BitVector keys) @Override public void display(final AsciiPanel terminal) { + if (recipeNames == null) + recipeNames = sCraft.getRecipeNames(); + + terminal.clear(' '); + drawHeader(terminal); - drawList(terminal, sCraft.getRecipeNames()); + drawList(terminal, recipeNames); } @Override diff --git a/src/main/java/com/github/fabioticconi/alone/screens/DropScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/DropScreen.java index 64bc51b..2ec4d09 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/DropScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/DropScreen.java @@ -36,8 +36,6 @@ public float handleKeys(final BitVector keys) if (targetId < 0) return super.handleKeys(keys); - screen.select(PlayScreen.class); - return sAction.act(sItems.drop(playerId, targetId)); } diff --git a/src/main/java/com/github/fabioticconi/alone/screens/EquipScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/EquipScreen.java index 4a5d665..398a724 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/EquipScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/EquipScreen.java @@ -40,8 +40,6 @@ public float handleKeys(final BitVector keys) if (targetId < 0) return super.handleKeys(keys); - screen.select(PlayScreen.class); - return sAction.act(sItems.equip(playerId, targetId)); } diff --git a/src/main/java/com/github/fabioticconi/alone/screens/InventoryScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/InventoryScreen.java index 364c4f6..0791542 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/InventoryScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/InventoryScreen.java @@ -31,10 +31,6 @@ import java.awt.event.KeyEvent; import java.util.ArrayList; -import java.util.Collections; - -import static java.awt.event.KeyEvent.VK_A; -import static java.awt.event.KeyEvent.VK_Z; /** * Author: Fabio Ticconi @@ -81,10 +77,8 @@ public void display(final AsciiPanel terminal) if (!canDraw(itemId)) continue; - elements.add(String.format("%s %s %s", - Letter.values()[i], - mName.get(itemId).name.toLowerCase(), - mEquip.has(itemId) ? " [WORN]" : "")); + elements + .add(String.format("%s %s", mName.get(itemId).name.toLowerCase(), mEquip.has(itemId) ? " [WORN]" : "")); } drawList(terminal, elements); @@ -101,11 +95,21 @@ int getItem(final BitVector keys) if (pos < 0) return -1; - final int playerId = pManager.getEntitiesOfPlayer("player").get(0).getId(); - final Inventory i = mInventory.get(playerId); + final int playerId = pManager.getEntitiesOfPlayer("player").get(0).getId(); + final Inventory inv = mInventory.get(playerId); - if (pos < i.items.size()) - return i.items.get(pos); + for (int i = 0, j = 0, size = inv.items.size(); i < size; i++) + { + final int itemId = inv.items.get(i); + + if (!canDraw(itemId)) + continue; + + if (j == pos) + return itemId; + + j++; + } return -1; } diff --git a/src/main/java/com/github/fabioticconi/alone/screens/LookScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/LookScreen.java index 85af8dd..198d9d3 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/LookScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/LookScreen.java @@ -24,6 +24,7 @@ import com.github.fabioticconi.alone.components.Position; import com.github.fabioticconi.alone.components.Target; import com.github.fabioticconi.alone.components.attributes.Sight; +import com.github.fabioticconi.alone.systems.ItemSystem; import com.github.fabioticconi.alone.systems.MapSystem; import com.github.fabioticconi.alone.systems.ThrowSystem; import com.github.fabioticconi.alone.utils.Coords; @@ -47,6 +48,7 @@ public class LookScreen extends PlayScreen ThrowSystem sThrow; MapSystem map; + ItemSystem sItem; @Override public float handleKeys(final BitVector keys) diff --git a/src/main/java/com/github/fabioticconi/alone/screens/PlayScreen.java b/src/main/java/com/github/fabioticconi/alone/screens/PlayScreen.java index cd00bdf..06a486f 100644 --- a/src/main/java/com/github/fabioticconi/alone/screens/PlayScreen.java +++ b/src/main/java/com/github/fabioticconi/alone/screens/PlayScreen.java @@ -28,9 +28,11 @@ import com.github.fabioticconi.alone.components.attributes.Sight; import com.github.fabioticconi.alone.constants.Cell; import com.github.fabioticconi.alone.constants.Side; +import com.github.fabioticconi.alone.constants.WeaponType; import com.github.fabioticconi.alone.map.SingleGrid; import com.github.fabioticconi.alone.messages.AbstractMessage; import com.github.fabioticconi.alone.messages.CannotMsg; +import com.github.fabioticconi.alone.messages.Msg; import com.github.fabioticconi.alone.systems.*; import com.github.fabioticconi.alone.utils.LongBag; import org.slf4j.Logger; @@ -38,6 +40,7 @@ import java.awt.*; import java.awt.event.KeyEvent; +import java.util.EnumSet; import java.util.Properties; import java.util.Stack; @@ -177,7 +180,7 @@ else if (keys.get(KeyEvent.VK_E)) return 0f; } - else if (keys.get(KeyEvent.VK_L) || keys.get(KeyEvent.VK_T)) + else if (keys.get(KeyEvent.VK_L)) { keys.clear(); @@ -187,6 +190,22 @@ else if (keys.get(KeyEvent.VK_L) || keys.get(KeyEvent.VK_T)) return 0f; } + else if (keys.get(KeyEvent.VK_T)) + { + keys.clear(); + + final int weaponId = sItems.getWeapon(playerId, EnumSet.allOf(WeaponType.class), true); + + if (weaponId < 0) + { + // TODO: it would be cool if we could support proper screen chaining. + // Eg, here we should actually select the EquipScreen AND follow it with a LookScreen. + + msg.send(playerId, new Msg("must equip a weapon first")); + + return 0f; + } + } else if (keys.get(KeyEvent.VK_W)) { keys.clear(); @@ -197,6 +216,16 @@ else if (keys.get(KeyEvent.VK_W)) return 0f; } + else if (keys.get(KeyEvent.VK_C)) + { + keys.clear(); + + Main.pause(); + + screen.select(CraftScreen.class); + + return 0f; + } else if (keys.get(KeyEvent.VK_ESCAPE)) { keys.clear(); @@ -231,12 +260,23 @@ else if (keys.get(KeyEvent.VK_SPACE)) Main.paused = !Main.paused; keys.clear(); + + return 0f; } - else + + keys.clear(); + + if (Main.realtime) { - keys.clear(); + // in real-time mode, SPACE just means pausing (or unpausing) - Main.paused = Main.realtime && !Main.paused; + Main.paused = !Main.paused; + } + else + { + // in turn-based mode, SPACE means "unpause", which we achieve by + // simply setting a player action equal to the world delta. + // this will return world.delta; } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/BootstrapSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/BootstrapSystem.java index d8afc13..c25b4c8 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/BootstrapSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/BootstrapSystem.java @@ -21,7 +21,6 @@ import com.artemis.annotations.Wire; import com.artemis.managers.PlayerManager; import com.artemis.utils.IntBag; -import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLParser; import com.github.fabioticconi.alone.behaviours.*; @@ -52,6 +51,7 @@ public class BootstrapSystem extends PassiveSystem MapSystem sMap; TreeSystem sTree; CrushSystem sCrush; + ItemSystem sItems; MapSystem map; PlayerManager pManager; @@ -87,7 +87,7 @@ protected void initialize() map.obstacles.set(id, x, y); pManager.setPlayer(world.getEntity(id), "player"); edit.create(Inventory.class); - edit.add(new Name("You")); + edit.add(new Name("You", "you")); // add a herd of buffalos int groupId = sGroup.createGroup(); @@ -125,7 +125,7 @@ protected void initialize() edit.create(Alertness.class).value = 0.0f; edit.create(Sprite.class).set('b', Util.BROWN.darker().darker()); // edit.create(Sprite.class).set(Character.forDigit(id, 10), Util.BROWN.darker().darker()); - edit.add(new Name("A buffalo")); + edit.add(new Name("A big buffalo", "buffalo")); map.obstacles.set(id, x, y); } @@ -160,7 +160,7 @@ protected void initialize() edit.create(Position.class).set(x, y); edit.create(Alertness.class).value = 0.0f; edit.create(Sprite.class).set('r', Color.LIGHT_GRAY); - edit.add(new Name("A rabbit")); + edit.add(new Name("A cute rabbit", "rabbit")); map.obstacles.set(id, x, y); } @@ -201,7 +201,7 @@ protected void initialize() edit.create(Alertness.class).value = 0.0f; edit.create(Sprite.class).set('w', Color.DARK_GRAY); // edit.create(Sprite.class).set(Character.forDigit(id, 10), Color.DARK_GRAY); - edit.add(new Name("A wolf")); + edit.add(new Name("A ferocious wolf", "wolf")); map.obstacles.set(id, x, y); } @@ -237,7 +237,7 @@ protected void initialize() edit.create(Alertness.class).value = 0.0f; edit.create(Sprite.class).set('p', Util.BROWN.darker().darker()); // edit.create(Sprite.class).set(Character.forDigit(id, 10), Util.BROWN.darker()); - edit.add(new Name("A puma")); + edit.add(new Name("A strong puma", "puma")); map.obstacles.set(id, x, y); } @@ -276,7 +276,7 @@ protected void initialize() edit.create(Position.class).set(x, y); edit.create(Alertness.class).value = 0.0f; edit.create(Sprite.class).set('f', Color.CYAN.darker()); - edit.add(new Name("A fish")); + edit.add(new Name("A colorful fish", "fish")); map.obstacles.set(id, x, y); } @@ -297,7 +297,7 @@ protected void initialize() if (!map.obstacles.isEmpty(x, y)) continue; - sTree.makeTree(x, y); + sItems.makeItem("tree", x, y); } } } @@ -319,7 +319,7 @@ protected void initialize() if (!map.obstacles.isEmpty(x, y)) continue; - sCrush.makeBoulder(x, y); + sItems.makeItem("boulder", x, y); } } } @@ -341,7 +341,7 @@ protected void initialize() if (!map.items.isEmpty(x, y)) continue; - sCrush.makeStone(x, y); + sItems.makeItem("stone", x, y); } } } @@ -360,19 +360,9 @@ protected void initialize() if (!map.items.isEmpty(x, y)) continue; - switch (r.nextInt(3)) - { - case 0: - id = sTree.makeTrunk(x, y); - map.items.set(id, x, y); - break; - - case 1: - case 2: - id = sTree.makeBranch(x, y); - map.items.set(id, x, y); - break; - } + sItems.makeItem("trunk", x, y); + sItems.makeItem("branch", x, y); + sItems.makeItem("vine", x, y); } } } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/BumpSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/BumpSystem.java index d290ee5..43918a7 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/BumpSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/BumpSystem.java @@ -38,12 +38,13 @@ public class BumpSystem extends PassiveSystem static final Logger log = LoggerFactory.getLogger(BumpSystem.class); ComponentMapper mHealth; - ComponentMapper mTree; + ComponentMapper mCuttable; ComponentMapper mPushable; ComponentMapper mCrushable; ComponentMapper mPos; ComponentMapper mPlayer; ComponentMapper mSight; + ComponentMapper mPath; ActionSystem sAction; AttackSystem sAttack; @@ -53,12 +54,12 @@ public class BumpSystem extends PassiveSystem MovementSystem sMove; MapSystem map; - public float bumpAction(final int entityId, final Side direction) + public float bumpAction(final int actorId, final Side direction) { if (direction.equals(Side.HERE)) return 0f; - final Position p = mPos.get(entityId); + final Position p = mPos.get(actorId); final int newX = p.x + direction.x; final int newY = p.y + direction.y; @@ -72,35 +73,44 @@ public float bumpAction(final int entityId, final Side direction) if (targetId < 0) { - c = sMove.move(entityId, direction); + c = sMove.move(actorId, direction); sAction.act(c); return c.delay; } - if (mPlayer.has(entityId) && mTree.has(targetId)) + // BUMPING! + + // if we were path-moving, now, whatever happens next, we stop + mPath.remove(actorId); + + if (mPlayer.has(actorId) && mCuttable.has(targetId)) { - c = sTree.cut(entityId, targetId); + c = sTree.cut(actorId, targetId); } - // else if (mPlayer.has(entityId) && mPushable.has(targetId)) + // else if (mPlayer.has(actorId) && mPushable.has(targetId)) // { - // c = sPush.push(entityId, targetId); + // c = sPush.push(actorId, targetId); // } - else if (mPlayer.has(entityId) && mCrushable.has(targetId)) + else if (mPlayer.has(actorId) && mCrushable.has(targetId)) { - c = sCrush.crush(entityId, targetId); + c = sCrush.crush(actorId, targetId); } else if (mHealth.has(targetId)) { - c = sAttack.attack(entityId, targetId); + c = sAttack.attack(actorId, targetId); + } + else + { + log.warn("{} bumped into {} but couldn't do anything", actorId, targetId); } return sAction.act(c); } - public float bumpAction(final int entityId, final Position target) + public float bumpAction(final int actorId, final Position target) { - final Position pos = mPos.get(entityId); - final Sight sight = mSight.get(entityId); + final Position pos = mPos.get(actorId); + final Sight sight = mSight.get(actorId); if (pos.equals(target)) { @@ -111,7 +121,7 @@ public float bumpAction(final int entityId, final Position target) if (Coords.distanceChebyshev(pos.x, pos.y, target.x, target.y) == 1) { // it's only one step away, no point calculating line of sight - return bumpAction(entityId, Side.getSide(pos.x, pos.y, target.x, target.y)); + return bumpAction(actorId, Side.getSide(pos.x, pos.y, target.x, target.y)); } // let's give them the opportunity to plan a path even if the creature is at the border of the vision @@ -123,7 +133,7 @@ public float bumpAction(final int entityId, final Position target) // the target position is the same as the entity's position, // or the target is not visible. Either way, we don't move. - log.warn("{} cannot find a path from {} to {}", entityId, pos, target); + log.warn("{} cannot find a path from {} to {}", actorId, pos, target); return 0f; } @@ -131,6 +141,6 @@ public float bumpAction(final int entityId, final Position target) // position 0 is "HERE" final Point p = path[1]; - return bumpAction(entityId, Side.getSide(pos.x, pos.y, p.x, p.y)); + return bumpAction(actorId, Side.getSide(pos.x, pos.y, p.x, p.y)); } } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/CraftSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/CraftSystem.java index a3bd464..cbfb10d 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/CraftSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/CraftSystem.java @@ -18,12 +18,13 @@ package com.github.fabioticconi.alone.systems; -import com.fasterxml.jackson.core.JsonParser; +import com.artemis.ComponentMapper; +import com.artemis.annotations.Wire; +import com.artemis.utils.IntBag; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLParser; -import com.github.fabioticconi.alone.screens.CraftItemScreen; +import com.github.fabioticconi.alone.components.Inventory; +import com.github.fabioticconi.alone.components.Name; import net.mostlyoriginal.api.system.core.PassiveSystem; import java.io.FileInputStream; @@ -37,49 +38,180 @@ */ public class CraftSystem extends PassiveSystem { + ComponentMapper mInventory; + ComponentMapper mName; + + ItemSystem sItems; + + @Wire + ObjectMapper mapper; + HashMap recipes; - public CraftSystem() throws IOException + @Override + protected void initialize() { - loadRecipes(); + try + { + loadRecipes(); + } catch (final IOException e) + { + e.printStackTrace(); + } } public List getRecipeNames() { - if (recipes == null || recipes.isEmpty()) + try + { + loadRecipes(); + + return new ArrayList<>(recipes.keySet()); + } catch (final IOException e) + { + e.printStackTrace(); + return List.of(); + } + } + + public HashMap getRecipes() + { + try + { + loadRecipes(); + } catch (final IOException e) + { + e.printStackTrace(); + } - return new ArrayList<>(recipes.keySet()); + return recipes; } - private void loadRecipes() throws IOException + public void loadRecipes() throws IOException { - // TODO we can actually instantiate the factory and mapper in the Main and inject/Wire them + final InputStream fileStream = new FileInputStream("data/crafting.yml"); - final InputStream fileStream = new FileInputStream("data/crafting.yml"); - final YAMLFactory factory = new YAMLFactory(); - final ObjectMapper mapper = new ObjectMapper(factory); + recipes = mapper.readValue(fileStream, new TypeReference>() + { + }); - recipes = mapper.readValue(fileStream, new TypeReference>(){}); + // reload item templates + sItems.loadTemplates(); for (final Map.Entry entry : recipes.entrySet()) { - System.out.println(entry.getKey() + " | " + entry.getValue()); + final CraftItem temp = entry.getValue(); + temp.tag = entry.getKey(); + + for (final String tempSource : temp.sources) + { + if (!sItems.templates.keySet().contains(tempSource)) + throw new RuntimeException("unknown item in sources field: " + tempSource); + } + + for (final String tempTool : temp.tools) + { + if (!sItems.templates.keySet().contains(tempTool)) + throw new RuntimeException("unknown item in tools field: " + tempTool); + } + } + } + + public int craftItem(final int entityId, final CraftItem itemRecipe) + { + final Inventory inv = mInventory.get(entityId); + + if (inv == null) + return -1; + + final IntBag tempSources = new IntBag(itemRecipe.sources.length); + Arrays.fill(tempSources.getData(), -1); + final IntBag tempTools = new IntBag(itemRecipe.tools.length); + Arrays.fill(tempTools.getData(), -1); + + final int[] data = inv.items.getData(); + for (int i = 0, size = inv.items.size(); i < size; i++) + { + final int itemId = data[i]; + + if (!mName.has(itemId)) + continue; + + final Name name = mName.get(itemId); + + int ii = 0; + final int[] sources = tempSources.getData(); + while (ii < itemRecipe.sources.length) + { + System.out.println("sources " + ii); + System.out.println(name.tag + " | " + itemRecipe.sources[ii]); + System.out.println(sources[ii]); + if (sources[ii] < 0 && name.tag.equals(itemRecipe.sources[ii])) + { + tempSources.set(ii, itemId); + + break; // source items can only be "used" once + } + + ii++; + } + + // if ii == sources.length, then it means that the previous loop completed + // without setting itemId as a "source". So it's OK to evaluate whether it + // can be used as a "tool". + // conversely, if ii < sources.length then itemId is a "source" and we cannot use it + // as tool. + if (ii < sources.length) + continue; + + ii = 0; + final int[] tools = tempTools.getData(); + while (ii < itemRecipe.tools.length) + { + System.out.println("tools " + ii); + System.out.println(name.tag + " | " + itemRecipe.tools[ii]); + System.out.println(tools[ii]); + if (tools[ii] < 0 && name.tag.equals(itemRecipe.tools[ii])) + { + tempTools.set(ii, itemId); + + break; // tool items can only be used once + } + + ii++; + } + } + + if (tempSources.size() < itemRecipe.sources.length || tempTools.size() < itemRecipe.tools.length) + return -1; + + final int id = sItems.makeItem(itemRecipe.tag); + + if (id < 0) + return -1; + + for (final int sourceId : tempSources.getData()) + { + // destroying source items + world.delete(sourceId); + inv.items.removeValue(sourceId); } + + return id; } public static class CraftItem { - public String name; - public String source; - public String tool; - public int n = 1; + public String tag; + public String[] sources; + public String[] tools; @Override public String toString() { - return "CraftItem{" + "name='" + name + '\'' + ", source='" + source + '\'' + ", tool='" + tool + '\'' + - ", n=" + n + '}'; + return "CraftItem{" + "tag='" + tag + '\'' + ", sources=" + Arrays.toString(sources) + ", tools=" + + Arrays.toString(tools) + '}'; } } } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/CrushSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/CrushSystem.java index f2b00a7..41a628e 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/CrushSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/CrushSystem.java @@ -19,8 +19,10 @@ package com.github.fabioticconi.alone.systems; import com.artemis.ComponentMapper; -import com.artemis.EntityEdit; -import com.github.fabioticconi.alone.components.*; +import com.github.fabioticconi.alone.components.Crushable; +import com.github.fabioticconi.alone.components.Position; +import com.github.fabioticconi.alone.components.Speed; +import com.github.fabioticconi.alone.components.Weapon; import com.github.fabioticconi.alone.components.actions.ActionContext; import com.github.fabioticconi.alone.components.attributes.Strength; import com.github.fabioticconi.alone.constants.WeaponType; @@ -29,9 +31,7 @@ import net.mostlyoriginal.api.system.core.PassiveSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import rlforj.math.Point; -import java.awt.Color; import java.util.EnumSet; /** @@ -42,10 +42,10 @@ public class CrushSystem extends PassiveSystem { static final Logger log = LoggerFactory.getLogger(CrushSystem.class); - ComponentMapper mCrushable; + ComponentMapper mCrush; ComponentMapper mSpeed; - ComponentMapper mStrength; - ComponentMapper mPosition; + ComponentMapper mStr; + ComponentMapper mPos; ComponentMapper mWeapon; StaminaSystem sStamina; @@ -64,49 +64,6 @@ public CrushAction crush(final int entityId, final int targetId) return c; } - public int makeStone(final Point p) - { - return makeStone(p.x, p.y); - } - - public int makeStone(final int x, final int y) - { - final int id = world.create(); - - final EntityEdit edit = world.edit(id); - edit.create(Position.class).set(x, y); - edit.create(Sprite.class).set('o', Color.DARK_GRAY.brighter()); - edit.create(Weapon.class).set(WeaponType.BLUNT, 1); - edit.create(Wearable.class); - edit.add(new Name("A stone")); - - map.items.set(id, x, y); - - return id; - } - - public int makeBoulder(final Point p) - { - return makeBoulder(p.x, p.y); - } - - public int makeBoulder(final int x, final int y) - { - final int id = world.create(); - final EntityEdit edit = world.edit(id); - - edit.create(Position.class).set(x, y); - edit.create(Sprite.class).set('#', Color.DARK_GRAY.brighter(), true); - edit.create(LightBlocker.class); - edit.create(Pushable.class); - edit.create(Crushable.class); - edit.add(new Name("A boulder")); - - map.obstacles.set(id, x, y); - - return id; - } - public class CrushAction extends ActionContext { @Override @@ -117,7 +74,7 @@ public boolean tryAction() final int targetId = targets.get(0); - if (targetId < 0 || !mCrushable.has(targetId)) + if (targetId < 0 || !mCrush.has(targetId)) return false; final int hammerId = sItem.getWeapon(actorId, EnumSet.of(WeaponType.BLUNT), true); @@ -142,7 +99,7 @@ public boolean tryAction() // FIXME further adjust delay and cost using the hammer power delay = mSpeed.get(actorId).value; - cost = delay / (mStrength.get(actorId).value + 3f); + cost = delay / (mStr.get(actorId).value + 3f); return true; } @@ -157,7 +114,7 @@ public void doAction() msg.send(actorId, targetId, new CrushMsg()); - final Position p = mPosition.get(targetId); + final Position p = mPos.get(targetId); // from a tree we get a trunk and two branches map.obstacles.del(p.x, p.y); @@ -165,7 +122,7 @@ public void doAction() for (int i = 0; i < 3; i++) { - makeStone(map.getFirstTotallyFree(p.x, p.y, -1)); + sItem.makeItem("stone", p.x, p.y); } // consume a fixed amount of stamina diff --git a/src/main/java/com/github/fabioticconi/alone/systems/DeadSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/DeadSystem.java index bece3eb..4f840c2 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/DeadSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/DeadSystem.java @@ -25,7 +25,7 @@ import com.github.fabioticconi.alone.components.*; import rlforj.math.Point; -import java.awt.Color; +import java.awt.*; /** * Author: Fabio Ticconi @@ -65,7 +65,7 @@ protected void process(final int entityId) edit.create(Sprite.class).set('$', Color.RED.darker().darker(), false); edit.create(Corpse.class); edit.create(Health.class).set(size.value + 3); - edit.add(new Name(name.name + "'s corpse")); + edit.add(new Name(name.name + "'s corpse", "corpse")); map.items.set(corpseId, p2.x, p2.y); } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/ItemSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/ItemSystem.java index d51f467..8629a5d 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/ItemSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/ItemSystem.java @@ -19,6 +19,10 @@ package com.github.fabioticconi.alone.systems; import com.artemis.ComponentMapper; +import com.artemis.EntityEdit; +import com.artemis.annotations.Wire; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.fabioticconi.alone.components.*; import com.github.fabioticconi.alone.components.actions.ActionContext; import com.github.fabioticconi.alone.constants.WeaponType; @@ -26,13 +30,17 @@ import com.github.fabioticconi.alone.messages.DropMsg; import com.github.fabioticconi.alone.messages.EquipMsg; import com.github.fabioticconi.alone.messages.GetMsg; -import com.github.fabioticconi.alone.screens.AbstractScreen; import net.mostlyoriginal.api.system.core.PassiveSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rlforj.math.Point; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; /** * Author: Fabio Ticconi @@ -48,10 +56,140 @@ public class ItemSystem extends PassiveSystem ComponentMapper mEquip; ComponentMapper mWearable; ComponentMapper mArmour; + ComponentMapper mName; + ComponentMapper mObstacle; MessageSystem msg; MapSystem map; + @Wire + ObjectMapper mapper; + + HashMap templates; + + @Override + protected void initialize() + { + try + { + loadTemplates(); + } catch (final IOException e) + { + e.printStackTrace(); + } + } + + public HashMap getTemplates() + { + try + { + loadTemplates(); + } catch (final IOException e) + { + e.printStackTrace(); + } + + return templates; + } + + public void loadTemplates() throws IOException + { + final InputStream fileStream = new FileInputStream("data/items.yml"); + + templates = mapper.readValue(fileStream, new TypeReference>() + { + }); + + for (final Map.Entry entry : templates.entrySet()) + { + final ItemTemplate temp = entry.getValue(); + temp.tag = entry.getKey(); + } + } + + /** + * It instantiates an object of the given type and places at that Point. + * + * @param tag + * @param p + * @return + */ + public int makeItem(final String tag, final Point p) + { + return makeItem(tag, p.x, p.y); + } + + /** + * It instantiates an object of the given type and places at that position. + * + * @param tag + * @param x + * @param y + * @return + */ + public int makeItem(final String tag, final int x, final int y) + { + final int id = makeItem(tag); + + if (id < 0) + return id; + + final Point p = map.getFirstTotallyFree(x, y, -1); + + mPos.create(id).set(p.x, p.y); + + if (mObstacle.has(id)) + map.obstacles.set(id, p.x, p.y); + else + map.items.set(id, p.x, p.y); + + return id; + } + + public int makeItem(final String tag) + { + try + { + loadTemplates(); + + final ItemTemplate template = templates.get(tag); + + if (template == null) + { + log.warn("Item named {} doesn't exist", tag); + return -1; + } + + final int id = world.create(); + + final EntityEdit edit = world.edit(id); + + edit.add(new Name(template.name, tag)); + + // TODO find a way to do this dynamically. Right now I cannot figure out a way + // of deserialising an array of Components, because it's an abstract class that I cannot annotate. + if (template.wearable != null) + edit.add(template.wearable); + if (template.weapon != null) + edit.add(template.weapon); + if (template.sprite != null) + edit.add(template.sprite); + if (template.obstacle != null) + edit.add(template.obstacle); + if (template.crushable != null) + edit.add(template.crushable); + if (template.cuttable != null) + edit.add(template.cuttable); + + return id; + } catch (final IOException e) + { + e.printStackTrace(); + + return -1; + } + } + public GetAction get(final int actorId) { final GetAction a = new GetAction(); @@ -81,12 +219,12 @@ public EquipAction equip(final int actorId, final int targetId) return a; } - int getArmour(final int entityId) + public int getArmour(final int entityId) { return getArmour(entityId, true); } - int getArmour(final int entityId, final boolean onlyEquipped) + public int getArmour(final int entityId, final boolean onlyEquipped) { final Inventory items = mInventory.get(entityId); @@ -115,17 +253,42 @@ int getArmour(final int entityId, final boolean onlyEquipped) return -1; } - int getWeapon(final int entityId) + public int getItem(final int entityId, final String tag, final boolean onlyEquipped) + { + final Inventory items = mInventory.get(entityId); + + if (items == null) + return -1; + + final int[] data = items.items.getData(); + for (int i = 0, size = items.items.size(); i < size; i++) + { + final int itemId = data[i]; + + // we might only want an equipped item + if (!mName.has(itemId) || (onlyEquipped && !mEquip.has(itemId))) + continue; + + final Name name = mName.get(itemId); + + if (name.tag.equals(tag)) + return itemId; + } + + return -1; + } + + public int getWeapon(final int entityId) { return getWeapon(entityId, true); } - int getWeapon(final int entityId, final boolean onlyEquipped) + public int getWeapon(final int entityId, final boolean onlyEquipped) { return getWeapon(entityId, EnumSet.allOf(WeaponType.class), onlyEquipped); } - int getWeapon(final int entityId, final EnumSet weaponTypes, final boolean onlyEquipped) + public int getWeapon(final int entityId, final EnumSet weaponTypes, final boolean onlyEquipped) { final Inventory items = mInventory.get(entityId); @@ -137,13 +300,6 @@ int getWeapon(final int entityId, final EnumSet weaponTypes, final b { final int itemId = data[i]; - if (itemId < 0) - { - // TODO: we could flag inventory as "dirty", and then use a system for periodic cleanup. - - continue; - } - // we might only want an equipped weapon if (!mWeapon.has(itemId) || (onlyEquipped && !mEquip.has(itemId))) continue; @@ -157,6 +313,19 @@ int getWeapon(final int entityId, final EnumSet weaponTypes, final b return -1; } + public static class ItemTemplate + { + public String name; + public String tag; + + public Wearable wearable; + public Weapon weapon; + public Sprite sprite; + public Obstacle obstacle; + public Crushable crushable; + public Cuttable cuttable; + } + public class GetAction extends ActionContext { @Override diff --git a/src/main/java/com/github/fabioticconi/alone/systems/MapSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/MapSystem.java index c4b2905..02def06 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/MapSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/MapSystem.java @@ -18,7 +18,7 @@ package com.github.fabioticconi.alone.systems; import com.artemis.ComponentMapper; -import com.github.fabioticconi.alone.components.LightBlocker; +import com.github.fabioticconi.alone.components.Obstacle; import com.github.fabioticconi.alone.constants.Cell; import com.github.fabioticconi.alone.constants.Options; import com.github.fabioticconi.alone.constants.Side; @@ -30,7 +30,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rlforj.IBoard; -import rlforj.los.*; +import rlforj.los.BresLos; +import rlforj.los.IFovAlgorithm; +import rlforj.los.ILosAlgorithm; +import rlforj.los.ShadowCasting; import rlforj.math.Point; import rlforj.pathfinding.AStar; @@ -54,14 +57,13 @@ public class MapSystem extends PassiveSystem implements IBoard final Cell terrain[][]; /* FOV/LOS stuff */ - final LongBag lastVisited; - final AStar astar; + final LongBag lastVisited; + final AStar astar; final IFovAlgorithm fov; final ILosAlgorithm los; - ComponentMapper mLightBlocker; - final SingleGrid obstacles; final SingleGrid items; + ComponentMapper mObstacle; public MapSystem() throws IOException { @@ -82,6 +84,12 @@ public MapSystem() throws IOException obstacles = new SingleGrid(Options.MAP_SIZE_X, Options.MAP_SIZE_Y); items = new SingleGrid(Options.MAP_SIZE_X, Options.MAP_SIZE_Y); + // TODO: we should make Cell a class, and obviously use a pool. + // This way we can load the cells from a yaml file, with their thresholds, colours, characters etc, + // and then we aren't stuck anymore with a few discrete terrain types but we can colour with a gradient, + // for example. + // movement costs should also be part of this cell. + float value; for (int x = 0; x < Options.MAP_SIZE_X; x++) @@ -416,7 +424,7 @@ public boolean blocksLight(final int x, final int y) // currently no tile blocks light by itself, so if there's no creature // here we know that light passes. - return entityId >= 0 && mLightBlocker.has(entityId); + return entityId >= 0 && mObstacle.has(entityId); } @Override diff --git a/src/main/java/com/github/fabioticconi/alone/systems/MovementSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/MovementSystem.java index 8318f08..2b6da60 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/MovementSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/MovementSystem.java @@ -18,6 +18,7 @@ package com.github.fabioticconi.alone.systems; import com.artemis.ComponentMapper; +import com.github.fabioticconi.alone.components.Path; import com.github.fabioticconi.alone.components.Position; import com.github.fabioticconi.alone.components.Speed; import com.github.fabioticconi.alone.components.Underwater; @@ -38,6 +39,7 @@ public class MovementSystem extends PassiveSystem ComponentMapper mPosition; ComponentMapper mSpeed; ComponentMapper mUnderWater; + ComponentMapper mPath; StaminaSystem sStamina; MapSystem map; @@ -76,6 +78,23 @@ public boolean tryAction() final Cell cell = map.get(x2, y2); + if (mUnderWater.has(actorId)) + { + cost = 0.25f; + delay = speed.value * 0.25f; + + return true; + } + + if (mPath.has(actorId)) + { + // it's a thrown weapon, no cost whatsoever + cost = 0f; + delay = speed.value; + + return true; + } + switch (cell) { case HILL: @@ -93,17 +112,11 @@ public boolean tryAction() break; case WATER: - if (mUnderWater.has(actorId)) - cost = 0.25f; - else - cost = 3f; + cost = 3f; break; case DEEP_WATER: - if (mUnderWater.has(actorId)) - cost = 0.25f; - else - cost = 4f; + cost = 4f; break; default: @@ -123,17 +136,7 @@ public void doAction() final int x2 = p.x + direction.x; final int y2 = p.y + direction.y; - if (map.items.has(actorId, p.x, p.y)) - { - // it's a moving item - map.items.move(p.x, p.y, x2, y2); - - p.x = x2; - p.y = y2; - - // it doesn't have stamina - } - else if (map.obstacles.has(actorId, p.x, p.y) && map.isFree(x2, y2)) + if (map.isFree(x2, y2)) { final int id = map.obstacles.move(p.x, p.y, x2, y2); diff --git a/src/main/java/com/github/fabioticconi/alone/systems/PathSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/PathSystem.java index fa30cb9..2bc6d39 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/PathSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/PathSystem.java @@ -25,6 +25,9 @@ import com.github.fabioticconi.alone.components.Position; import com.github.fabioticconi.alone.components.Speed; import com.github.fabioticconi.alone.constants.Side; +import com.github.fabioticconi.alone.utils.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import rlforj.math.Point; /** @@ -33,14 +36,14 @@ */ public class PathSystem extends DelayedIteratingSystem { + static final Logger log = LoggerFactory.getLogger(PathSystem.class); + ComponentMapper mPath; ComponentMapper mSpeed; ComponentMapper mPos; - MapSystem map; - - MovementSystem sMove; - BumpSystem sBump; + MapSystem map; + BumpSystem sBump; public PathSystem() { @@ -59,34 +62,54 @@ protected void processDelta(final int entityId, final float accumulatedDelta) mPath.get(entityId).cooldown -= accumulatedDelta; } + @Override + protected void removed(final int entityId) + { + // path was removed, so it stopped. Ergo, we place the object on the ground + + final Position pos = mPos.get(entityId); + + final Point p = map.getFirstTotallyFree(pos.x, pos.y, -1); + + map.obstacles.del(pos.x, pos.y); + map.items.set(entityId, p.x, p.y); + } + @Override protected void processExpired(final int entityId) { final Path path = mPath.get(entityId); final Position p = mPos.get(entityId); - final Point newP = path.steps.remove(0); - final Side direction = Side.getSide(p.x, p.y, newP.x, newP.y); + if (path.i >= path.steps.size()) + { + // for some reason the path has ended outside of us. Let's just terminate + mPath.remove(entityId); + + log.warn("{} was moving via a Path but there are no steps left"); + + return; + } - final float wait = sBump.bumpAction(entityId, direction); + final Point p2 = path.steps.get(path.i++); + final Side side = Side.getSide(p.x, p.y, p2.x, p2.y); - // FIXME: if wait is zero, it usually means the bump failed somehow. If that's true than we need - // to stop path-moving. Maybe we should reserve -1 for when the bump fails? + sBump.bumpAction(entityId, side); - // in general, actually, we should stop whenever the bump did not do a movement. This - // requires a more complicated handling which should, I believe, be completely performed by - // BumpSystem. + // bump can remove the Path in case movement fails (eg, there's an obstacle and so we actually bump) + if (!mPath.has(entityId)) + return; - if (path.steps.isEmpty()) + if (path.i == path.steps.size()) { // we arrived! mPath.remove(entityId); } else { - final float speed = mSpeed.get(entityId).value; + final float speed = mSpeed.get(entityId).value * Util.gain((float) path.i / path.steps.size(), 0.25f); - path.cooldown = Math.max(speed, wait); + path.cooldown = Math.max(speed, 0.05f); // let's not go too fast offerDelay(speed); } diff --git a/src/main/java/com/github/fabioticconi/alone/systems/ThrowSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/ThrowSystem.java index e9ae86d..a1d1508 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/ThrowSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/ThrowSystem.java @@ -52,6 +52,7 @@ public class ThrowSystem extends PassiveSystem ComponentMapper mStrength; ComponentMapper mAgility; ComponentMapper mName; + ComponentMapper mEquip; StaminaSystem sStamina; BumpSystem sBump; @@ -151,9 +152,9 @@ public void doAction() final int weaponId = targets.get(0); - final Point newP = path.get(0); + final Point p2 = path.get(0); - if (map.isFree(newP.x, newP.y)) + if (map.isFree(p2.x, p2.y)) { final Inventory inventory = mInventory.get(actorId); @@ -166,17 +167,19 @@ public void doAction() mSpeed.create(weaponId).set(cooldown); mPath.create(weaponId).set(cooldown, path); - mPos.create(weaponId).set(newP.x, newP.y); + mPos.create(weaponId).set(p2.x, p2.y); + + // it's not equipped anymore + mEquip.remove(weaponId); // strength and agility of thrower are passed on to the thrown weapon. // effects: the weapon will hit more likely with high agility, and do more damage with high strength. mStrength.create(weaponId).value = mStrength.get(actorId).value; mAgility.create(weaponId).value = mAgility.get(actorId).value; - // at this point it really happened: the weapon is flying at its new position - // (it's not an obstacle, so there's not risk of someone interrupting it in mid-air) - final Point p2 = map.getFirstTotallyFree(newP.x, newP.y, -1); - map.items.set(weaponId, p2.x, p2.y); + // at this point it really happened: the weapon is flying at its new position. + // it's an obstacle, so it will bump against whatever it finds + map.obstacles.set(weaponId, p2.x, p2.y); } else { @@ -186,7 +189,7 @@ public void doAction() final Position p = mPos.get(actorId); - sBump.bumpAction(actorId, Side.getSide(p.x, p.y, newP.x, newP.y)); + sBump.bumpAction(actorId, Side.getSide(p.x, p.y, p2.x, p2.y)); } sStamina.consume(actorId, cost); diff --git a/src/main/java/com/github/fabioticconi/alone/systems/TreeSystem.java b/src/main/java/com/github/fabioticconi/alone/systems/TreeSystem.java index 24506f8..4e3039a 100644 --- a/src/main/java/com/github/fabioticconi/alone/systems/TreeSystem.java +++ b/src/main/java/com/github/fabioticconi/alone/systems/TreeSystem.java @@ -19,20 +19,18 @@ package com.github.fabioticconi.alone.systems; import com.artemis.ComponentMapper; -import com.artemis.EntityEdit; -import com.github.fabioticconi.alone.components.*; +import com.github.fabioticconi.alone.components.Cuttable; +import com.github.fabioticconi.alone.components.Position; +import com.github.fabioticconi.alone.components.Speed; import com.github.fabioticconi.alone.components.actions.ActionContext; import com.github.fabioticconi.alone.components.attributes.Strength; import com.github.fabioticconi.alone.constants.WeaponType; import com.github.fabioticconi.alone.messages.CannotMsg; import com.github.fabioticconi.alone.messages.CutMsg; -import com.github.fabioticconi.alone.utils.Util; import net.mostlyoriginal.api.system.core.PassiveSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import rlforj.math.Point; -import java.awt.*; import java.util.EnumSet; /** @@ -43,11 +41,10 @@ public class TreeSystem extends PassiveSystem { static final Logger log = LoggerFactory.getLogger(TreeSystem.class); - ComponentMapper mTree; + ComponentMapper mCuttable; ComponentMapper mSpeed; ComponentMapper mStrength; ComponentMapper mPosition; - ComponentMapper mName; StaminaSystem sStamina; ItemSystem sItem; @@ -65,62 +62,6 @@ public CutAction cut(final int entityId, final int treeId) return c; } - public int makeTree(final int x, final int y) - { - final int id = world.create(); - - final EntityEdit edit = world.edit(id); - edit.create(Position.class).set(x, y); - edit.create(Sprite.class).set('T', Color.GREEN.darker(), true); - edit.create(LightBlocker.class); - edit.create(Tree.class); - edit.add(new Name("A tree")); - - map.obstacles.set(id, x, y); - - return id; - } - - public int makeTrunk(final Point p) - { - return makeTrunk(p.x, p.y); - } - - public int makeTrunk(final int x, final int y) - { - final int id = world.create(); - - final EntityEdit edit = world.edit(id); - edit.create(Position.class).set(x, y); - edit.create(Sprite.class).set('-', Util.BROWN.brighter()); - edit.add(new Name("A tree trunk")); - - map.items.set(id, x, y); - - return id; - } - - public int makeBranch(final Point p) - { - return makeBranch(p.x, p.y); - } - - public int makeBranch(final int x, final int y) - { - final int id = world.create(); - - final EntityEdit edit = world.edit(id); - edit.create(Position.class).set(x, y); - edit.create(Sprite.class).set('/', Util.BROWN.brighter()); - edit.create(Weapon.class).set(WeaponType.BLUNT, 1); - edit.create(Wearable.class); - edit.add(new Name("A branch")); - - map.items.set(id, x, y); - - return id; - } - public class CutAction extends ActionContext { @Override @@ -131,7 +72,7 @@ public boolean tryAction() final int treeId = targets.get(0); - if (!mTree.has(treeId)) + if (!mCuttable.has(treeId)) return false; final int axeId = sItem.getWeapon(actorId, EnumSet.of(WeaponType.SLASH), false); @@ -169,9 +110,9 @@ public void doAction() map.obstacles.del(p.x, p.y); world.delete(treeId); - makeTrunk(map.getFirstTotallyFree(p.x, p.y, -1)); - makeBranch(map.getFirstTotallyFree(p.x, p.y, -1)); - makeBranch(map.getFirstTotallyFree(p.x, p.y, -1)); + sItem.makeItem("trunk", p.x, p.y); + sItem.makeItem("branch", p.x, p.y); + sItem.makeItem("vine", p.x, p.y); // consume a fixed amount of stamina sStamina.consume(actorId, cost);