Модель предметов по базе ремейка: узлы, деревья, знание
Протокол: - ItemData читает таблицу items/items.csv (218 предметов ремейка) и отдает числа: урон, дальность, темп, прочность, сколько ресурса за удар. - ItemType: поля rid, researchScrap, techTreeBench, techTreeScrap, techTreeParent, admin. Урон по частям тела делится как в ремейке: голова вдвое, руки и ноги четыре пятых. Число в исходнике главнее таблицы. - ItemRegistry: 218 предметов с рецептами, ценой исследования и веткой дерева. Новые вещи: salvage-тесак, бронедвери, гаражи, взрывчатка, компоненты, еда, мусор, верстаки и печи. - NodeType: узлы с запасом (дерево 500/750/1000, камень 750, металл 500, сера 200) и меткой: крестик на дереве до двух, горячая точка на руде до трех. - TechTree: ветки верстаков с предками, 125/250/500 скрапа. - Новые пакеты: Dig, BreakProgress, CraftStart, CraftUpdate, ResearchStart, ResearchUpdate, TechTreeUnlock, KnownRecipes, TechTreeState, ContainerOpen, ContainerTake, LearnBlueprint. Сервер: - Knowledge: что игрок знает - рецепты и открытые узлы деревьев. - Crafter: проверка рецепта, уровня верстака и ингредиентов; время крафта по формуле плана. - ResearchTable: предмет плюс скрап, десять секунд, предмет превращается в чертёж; чертёж изучается и открывает рецепт. - ResourceNode: запас, метка, последний удар отдает остаток. - JunkPile: две-четыре бочки (серая - скрап и мусор, синяя - компонент, красная - нефть и топливо) и ящик с едой; пустая мусорка исчезает через 60-180 секунд. - RoadNetwork: сетка дорог и кольцо, точки спавна мусорки; генератор кладет асфальт и ровняет дорогу по ширине. - Проверка досягаемости вынесена из changeBlock, игрок получил benchLevel. - Сессия отвечает на новые пакеты, при входе шлет известные рецепты и открытые узлы. Тесты: протокол 24 + 10, сервер 74 (было 57). Форк собирается чисто.
This commit is contained in:
@@ -47,7 +47,9 @@ Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files"
|
||||
$suites = @(
|
||||
'net.rustnotmc.server.ServerTest',
|
||||
'net.rustnotmc.server.world.WorldTest',
|
||||
'net.rustnotmc.server.world.NodeTest',
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.craft.CraftTest',
|
||||
'net.rustnotmc.server.PlayTest'
|
||||
)
|
||||
foreach ($suite in $suites) {
|
||||
|
||||
@@ -9,12 +9,19 @@ import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.rustnotmc.protocol.Buf;
|
||||
import net.rustnotmc.protocol.Blocks;
|
||||
import net.rustnotmc.protocol.ChunkFormat;
|
||||
import net.rustnotmc.protocol.Frame;
|
||||
import net.rustnotmc.protocol.FrameDecoder;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.Packet;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.protocol.ProtocolException;
|
||||
import net.rustnotmc.protocol.TechTree;
|
||||
import net.rustnotmc.server.craft.Crafter;
|
||||
import net.rustnotmc.server.craft.Knowledge;
|
||||
import net.rustnotmc.server.craft.ResearchTable;
|
||||
|
||||
/**
|
||||
* One connection, and the handshake that has to finish before it means
|
||||
@@ -207,6 +214,21 @@ public final class ClientSession implements Runnable {
|
||||
case Packets.BLOCK_CHANGE:
|
||||
onBlockChange(frame);
|
||||
return true;
|
||||
case Packets.DIG:
|
||||
onDig(frame);
|
||||
return true;
|
||||
case Packets.CRAFT_START:
|
||||
onCraftStart(frame);
|
||||
return true;
|
||||
case Packets.RESEARCH_START:
|
||||
onResearchStart(frame);
|
||||
return true;
|
||||
case Packets.TECH_TREE_UNLOCK:
|
||||
onTechTreeUnlock(frame);
|
||||
return true;
|
||||
case Packets.LEARN_BLUEPRINT:
|
||||
onLearnBlueprint(frame);
|
||||
return true;
|
||||
case Packets.PLAYER_MOVE:
|
||||
onPlayerMove(frame);
|
||||
return true;
|
||||
@@ -444,6 +466,12 @@ public final class ClientSession implements Runnable {
|
||||
// what the player is carrying comes with the world: the server owns
|
||||
// the bag, so the client is told what is in it
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
// and what the player knows, so that the client can grey out the
|
||||
// recipes that are not known yet
|
||||
send(Packets.KNOWN_RECIPES,
|
||||
new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
|
||||
send(Packets.TECH_TREE_STATE,
|
||||
new Packets.TechTreeState(this.player.knowledge().unlockedNodes()));
|
||||
this.player.streamChunks(this.server.world());
|
||||
}
|
||||
|
||||
@@ -494,11 +522,156 @@ public final class ClientSession implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Where the player is now. */
|
||||
private void onPlayerMove(Frame frame) {
|
||||
/**
|
||||
* The player is digging.
|
||||
*
|
||||
* The client says where it is looking and whether the button is down. The
|
||||
* server decides what happens there: whether the player is close enough,
|
||||
* whether it is allowed to break that, and how far the work has come.
|
||||
*
|
||||
* The nodes of the world are not blocks, so digging at one of them is
|
||||
* answered by the node and not by the world. That comes with the nodes
|
||||
* themselves; until then a dig at a block breaks it in the creative mode
|
||||
* and does nothing in the survival one.
|
||||
*/
|
||||
private void onDig(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.Dig dig = Packets.Dig.read(new Buf(frame.body()));
|
||||
|
||||
if (!dig.start()) {
|
||||
// the button came up: the cracks go away
|
||||
send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
|
||||
return;
|
||||
}
|
||||
if (!this.player.canReach(dig.x, dig.y, dig.z)) {
|
||||
send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
|
||||
return;
|
||||
}
|
||||
if (this.player.mayBreakBlocks()) {
|
||||
if (this.server.world().setBlock(dig.x, dig.y, dig.z, Blocks.AIR)) {
|
||||
this.server.broadcastBlockChange(dig.x, dig.y, dig.z, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* The player wants to make something.
|
||||
*
|
||||
* The server checks the recipe, the workbench and the ingredients, and
|
||||
* says no otherwise.
|
||||
*/
|
||||
private void onCraftStart(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.CraftStart start = Packets.CraftStart.read(new Buf(frame.body()));
|
||||
ItemType type = ItemRegistry.byId(start.item);
|
||||
int bench = this.player.benchLevel();
|
||||
String problem = Crafter.whyNot(type, this.player.knowledge(), bench,
|
||||
this.player.inventory());
|
||||
if (problem != null) {
|
||||
send(Packets.CRAFT_UPDATE, new Packets.CraftUpdate(start.item, 0, 0, problem));
|
||||
return;
|
||||
}
|
||||
int seconds = Crafter.seconds(type, bench);
|
||||
this.player.startCraft(start.item, seconds);
|
||||
send(Packets.CRAFT_UPDATE, new Packets.CraftUpdate(start.item, start.count,
|
||||
seconds, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* The player wants to research an item.
|
||||
*
|
||||
* The table takes the scrap at once and the item when the work is done.
|
||||
*/
|
||||
private void onResearchStart(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.ResearchStart start = Packets.ResearchStart.read(new Buf(frame.body()));
|
||||
ResearchTable table = this.player.research();
|
||||
String problem = table.start(start.item, this.player.inventory());
|
||||
send(Packets.RESEARCH_UPDATE, new Packets.ResearchUpdate(start.item,
|
||||
table.secondsLeft(), problem == null ? "" : problem));
|
||||
if (problem == null) {
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player wants to open a node of the tree of a workbench.
|
||||
*
|
||||
* The branch has to be open and there has to be enough scrap. The scrap
|
||||
* is taken and the node opens, and the client is told what the player
|
||||
* knows now.
|
||||
*/
|
||||
private void onTechTreeUnlock(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.TechTreeUnlock unlock = Packets.TechTreeUnlock.read(new Buf(frame.body()));
|
||||
Knowledge knowledge = this.player.knowledge();
|
||||
String problem = knowledge.reasonCannotUnlock(unlock.item);
|
||||
if (problem == null) {
|
||||
int cost = TechTree.costOf(unlock.item);
|
||||
if (this.player.inventory().count(ItemRegistry.SCRAP) < cost) {
|
||||
problem = "you are short of scrap";
|
||||
} else {
|
||||
this.player.inventory().take(ItemRegistry.SCRAP, cost);
|
||||
knowledge.unlock(unlock.item);
|
||||
}
|
||||
}
|
||||
if (problem != null) {
|
||||
this.server.log("tree: " + problem + " (" + this.account + ")");
|
||||
}
|
||||
send(Packets.TECH_TREE_STATE, new Packets.TechTreeState(knowledge.unlockedNodes()));
|
||||
send(Packets.KNOWN_RECIPES, new Packets.KnownRecipes(knowledge.knownRecipes()));
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
}
|
||||
|
||||
/**
|
||||
* The player learned a blueprint.
|
||||
*
|
||||
* The blueprint is used up and the recipe opens for good.
|
||||
*/
|
||||
private void onLearnBlueprint(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.LearnBlueprint learn = Packets.LearnBlueprint.read(new Buf(frame.body()));
|
||||
if (!ResearchTable.learn(this.player.inventory(), this.player.knowledge(),
|
||||
learn.item)) {
|
||||
return;
|
||||
}
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
send(Packets.KNOWN_RECIPES,
|
||||
new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* How the research of a player is going.
|
||||
*
|
||||
* The work goes on even when the player walks away from the table.
|
||||
*/
|
||||
void onResearchProgress(int item, int secondsLeft, boolean done) {
|
||||
if (this.player == null) {
|
||||
return;
|
||||
}
|
||||
send(Packets.RESEARCH_UPDATE, new Packets.ResearchUpdate(item, secondsLeft, ""));
|
||||
if (done) {
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
send(Packets.KNOWN_RECIPES,
|
||||
new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Where the player is now. */
|
||||
private void onPlayerMove(Frame frame) { if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.PlayerMove move = Packets.PlayerMove.read(new Buf(frame.body()));
|
||||
|
||||
// a position that is not a number would spread through the whole
|
||||
|
||||
@@ -5,7 +5,10 @@ import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import net.rustnotmc.protocol.Blocks;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.server.craft.Knowledge;
|
||||
import net.rustnotmc.server.craft.ResearchTable;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
import net.rustnotmc.server.world.Chunk;
|
||||
import net.rustnotmc.server.world.ChunkPos;
|
||||
@@ -53,6 +56,16 @@ public final class Player {
|
||||
/** What the player is carrying. The server owns it. */
|
||||
private final Inventory inventory;
|
||||
|
||||
/** What the player knows: the recipes and the nodes of the trees. */
|
||||
private final Knowledge knowledge;
|
||||
|
||||
/** The research table the player is working at, if any. */
|
||||
private final ResearchTable research;
|
||||
|
||||
/** What the player is making, and how much of it is left to do. */
|
||||
private int craftItem = ItemRegistry.NOTHING;
|
||||
private int craftSecondsLeft;
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn) {
|
||||
this(session, playerId, entityId, account, spawn,
|
||||
@@ -69,6 +82,8 @@ public final class Player {
|
||||
this.y = spawn[1];
|
||||
this.z = spawn[2];
|
||||
this.inventory = inventory;
|
||||
this.knowledge = new Knowledge();
|
||||
this.research = new ResearchTable();
|
||||
}
|
||||
|
||||
/** What the player is carrying. */
|
||||
@@ -76,6 +91,38 @@ public final class Player {
|
||||
return this.inventory;
|
||||
}
|
||||
|
||||
/** What the player knows. */
|
||||
public Knowledge knowledge() {
|
||||
return this.knowledge;
|
||||
}
|
||||
|
||||
/** The research table the player is working at. */
|
||||
public ResearchTable research() {
|
||||
return this.research;
|
||||
}
|
||||
|
||||
/** What the player is making, or nothing. */
|
||||
public int craftItem() {
|
||||
return this.craftItem;
|
||||
}
|
||||
|
||||
/** How many seconds are left of the work, or zero. */
|
||||
public int craftSecondsLeft() {
|
||||
return this.craftSecondsLeft;
|
||||
}
|
||||
|
||||
/** Starts a craft. */
|
||||
public void startCraft(int item, int seconds) {
|
||||
this.craftItem = item;
|
||||
this.craftSecondsLeft = seconds;
|
||||
}
|
||||
|
||||
/** Stops the craft that is going on. */
|
||||
public void stopCraft() {
|
||||
this.craftItem = ItemRegistry.NOTHING;
|
||||
this.craftSecondsLeft = 0;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- getters
|
||||
|
||||
public ClientSession session() {
|
||||
@@ -266,15 +313,35 @@ public final class Player {
|
||||
if (!Blocks.isValid(id)) {
|
||||
return false;
|
||||
}
|
||||
double dx = (x + 0.5D) - this.x;
|
||||
double dy = (y + 0.5D) - this.y;
|
||||
double dz = (z + 0.5D) - this.z;
|
||||
if (dx * dx + dy * dy + dz * dz > REACH * REACH) {
|
||||
if (!canReach(x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
return world.setBlock(x, y, z, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player is close enough to have plausibly touched a block.
|
||||
*
|
||||
* Without this, a client can rewrite the world from the other side of it.
|
||||
*/
|
||||
public boolean canReach(int x, int y, int z) {
|
||||
double dx = (x + 0.5D) - this.x;
|
||||
double dy = (y + 0.5D) - this.y;
|
||||
double dz = (z + 0.5D) - this.z;
|
||||
return dx * dx + dy * dy + dz * dz <= REACH * REACH;
|
||||
}
|
||||
|
||||
/**
|
||||
* The level of the workbench the player is standing next to.
|
||||
*
|
||||
* Zero means there is none, and then only the things that need no
|
||||
* workbench can be made. The workbenches that are put down in the world
|
||||
* come with the places of the world; until then this is always zero.
|
||||
*/
|
||||
public int benchLevel() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Floor division that works for negative numbers, unlike a cast. */
|
||||
private static int floor(double value) {
|
||||
int truncated = (int) value;
|
||||
|
||||
@@ -321,8 +321,22 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
/** One server tick. The place the world will live. */
|
||||
private void tickOnce() {
|
||||
/**
|
||||
* Lets the research of a player go on.
|
||||
*
|
||||
* The work is measured in seconds, so it is looked at once a second.
|
||||
*/
|
||||
private void tickResearch(Player player) {
|
||||
net.rustnotmc.server.craft.ResearchTable table = player.research();
|
||||
if (!table.busy()) {
|
||||
return;
|
||||
}
|
||||
int item = table.item();
|
||||
boolean done = table.tick(player.inventory());
|
||||
player.session().onResearchProgress(item, table.secondsLeft(), done);
|
||||
}
|
||||
|
||||
/** One server tick. The place the world will live. */ private void tickOnce() {
|
||||
this.ticks.incrementAndGet();
|
||||
this.world.tick();
|
||||
// chunks nobody has walked near are dropped once a second; the limit
|
||||
@@ -330,6 +344,9 @@ public final class Server {
|
||||
// notice, not often enough to be right
|
||||
if (this.ticks.get() % 20L == 0L) {
|
||||
this.world.evictIdle();
|
||||
for (Player player : this.players.values()) {
|
||||
tickResearch(player);
|
||||
}
|
||||
}
|
||||
if ((this.ticks.get() & 0x3FFFL) == 0L) {
|
||||
this.tokens.sweep();
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.rustnotmc.server.craft;
|
||||
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.Recipe;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
|
||||
/**
|
||||
* Making things.
|
||||
*
|
||||
* Three things have to be true before a thing is made: the player knows the
|
||||
* recipe, there is a workbench of the right level within reach, and the
|
||||
* ingredients are in the bag. The server checks all three and says no
|
||||
* otherwise, because a client that decides for itself can be made to make
|
||||
* anything.
|
||||
*/
|
||||
public final class Crafter {
|
||||
|
||||
private Crafter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Why the item cannot be made, or null when it can.
|
||||
*
|
||||
* The reasons are words a player can read.
|
||||
*/
|
||||
public static String whyNot(ItemType type, Knowledge knowledge, int benchLevel,
|
||||
Inventory inventory) {
|
||||
if (type == null) {
|
||||
return "there is no such thing";
|
||||
}
|
||||
if (type.recipe == null) {
|
||||
return "it is not made, it is found";
|
||||
}
|
||||
if (type.admin) {
|
||||
return "only the administrators of the server may have that";
|
||||
}
|
||||
if (!knowledge.knows(type.id)) {
|
||||
return "you do not know how to make that";
|
||||
}
|
||||
if (type.bench > benchLevel) {
|
||||
return "you need a workbench of level " + type.bench;
|
||||
}
|
||||
for (int i = 0; i < type.recipe.items.length; i++) {
|
||||
int needed = type.recipe.counts[i];
|
||||
if (inventory.count(type.recipe.items[i]) < needed) {
|
||||
return "you are short of " + nameOf(type.recipe.items[i]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the item, if it can be made.
|
||||
*
|
||||
* The ingredients are taken out of the bag and the result is put in. When
|
||||
* the bag is full the work is not done: the ingredients stay where they
|
||||
* are.
|
||||
*/
|
||||
public static boolean craft(ItemType type, int count, Knowledge knowledge,
|
||||
int benchLevel, Inventory inventory) {
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (whyNot(type, knowledge, benchLevel, inventory) != null) {
|
||||
return false;
|
||||
}
|
||||
Recipe recipe = type.recipe;
|
||||
for (int i = 0; i < recipe.items.length; i++) {
|
||||
inventory.take(recipe.items[i], recipe.counts[i] * count);
|
||||
}
|
||||
if (!inventory.add(net.rustnotmc.protocol.ItemStack.of(type.id, count))) {
|
||||
// The bag is full: give the ingredients back rather than eat them.
|
||||
for (int i = 0; i < recipe.items.length; i++) {
|
||||
inventory.add(net.rustnotmc.protocol.ItemStack.of(recipe.items[i],
|
||||
recipe.counts[i] * count));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the work takes, in seconds.
|
||||
*
|
||||
* A workbench above the one the recipe asks for makes it faster: half as
|
||||
* long for the first level above, three quarters off for the second, and
|
||||
* no more after that.
|
||||
*/
|
||||
public static int seconds(ItemType type, int benchLevel) {
|
||||
if (type == null || type.recipe == null) {
|
||||
return 0;
|
||||
}
|
||||
int required = type.bench;
|
||||
int levels = benchLevel - required;
|
||||
if (levels < 0) {
|
||||
levels = 0;
|
||||
}
|
||||
if (levels > 2) {
|
||||
levels = 2;
|
||||
}
|
||||
double factor = Math.pow(0.5D, levels);
|
||||
int seconds = (int) Math.floor(type.recipe.seconds * factor);
|
||||
return seconds < 1 ? 1 : seconds;
|
||||
}
|
||||
|
||||
private static String nameOf(int item) {
|
||||
ItemType type = ItemRegistry.byId(item);
|
||||
return type == null ? ("item " + item) : type.name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package net.rustnotmc.server.craft;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.TechTree;
|
||||
|
||||
/**
|
||||
* What a player knows.
|
||||
*
|
||||
* There are two kinds of knowledge, and they are kept apart on purpose:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the <b>recipes</b> the player can make. A recipe that is known by
|
||||
* default is known from the start; anything else has to be researched,
|
||||
* either at the research table or by opening a node in the tree of a
|
||||
* workbench.
|
||||
* <li>the <b>nodes</b> the player has opened in the trees. This is what the
|
||||
* tree of a workbench is: a set of things that were paid for with scrap.
|
||||
* </ul>
|
||||
*
|
||||
* The knowledge belongs to the player and not to the item, so two players can
|
||||
* know different things at the same moment.
|
||||
*/
|
||||
public final class Knowledge {
|
||||
|
||||
private final Set<Integer> learned = new HashSet<Integer>();
|
||||
private final Set<Integer> unlocked = new HashSet<Integer>();
|
||||
|
||||
/**
|
||||
* Whether the player can make the item.
|
||||
*
|
||||
* An item that is known by default needs nothing. Everything else has to
|
||||
* have been researched, and the research is what the table or the tree
|
||||
* gives.
|
||||
*/
|
||||
public boolean knows(int item) {
|
||||
ItemType type = ItemRegistry.byId(item);
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
if (type.knownByDefault) {
|
||||
return true;
|
||||
}
|
||||
if (this.unlocked.contains(Integer.valueOf(item))) {
|
||||
return true;
|
||||
}
|
||||
return this.learned.contains(Integer.valueOf(item));
|
||||
}
|
||||
|
||||
/** The player learned a recipe, from a blueprint. */
|
||||
public void learn(int item) {
|
||||
this.learned.add(Integer.valueOf(item));
|
||||
}
|
||||
|
||||
/** Whether the player has opened this node of a tree. */
|
||||
public boolean isUnlocked(int item) {
|
||||
return this.unlocked.contains(Integer.valueOf(item));
|
||||
}
|
||||
|
||||
/** The player opened a node of a tree. */
|
||||
public void unlock(int item) {
|
||||
this.unlocked.add(Integer.valueOf(item));
|
||||
}
|
||||
|
||||
/** What a blueprint of this item would open. */
|
||||
public void learnFromBlueprint(int item) {
|
||||
learn(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the node can be opened, and what it costs.
|
||||
*
|
||||
* The cost is zero when the node cannot be opened at all: there is no
|
||||
* node for an item that is in no tree, and a node whose parent is still
|
||||
* closed is not for sale either.
|
||||
*/
|
||||
public boolean canUnlock(int item) {
|
||||
return reasonCannotUnlock(item) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Why the node cannot be opened, or null when it can.
|
||||
*
|
||||
* The reasons are words a player can read: "no such node", "the branch is
|
||||
* not open yet".
|
||||
*/
|
||||
public String reasonCannotUnlock(int item) {
|
||||
TechTree.Node node = TechTree.forItem(item);
|
||||
if (node == null) {
|
||||
return "there is no such node";
|
||||
}
|
||||
if (isUnlocked(item)) {
|
||||
return "it is already open";
|
||||
}
|
||||
if (!TechTree.canOpen(item, this.unlocked)) {
|
||||
return "the branch is not open yet";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The recipes the player knows, for the client to draw. */
|
||||
public int[] knownRecipes() {
|
||||
List<Integer> out = new ArrayList<Integer>();
|
||||
for (ItemType type : ItemRegistry.all()) {
|
||||
if (knows(type.id)) {
|
||||
out.add(Integer.valueOf(type.id));
|
||||
}
|
||||
}
|
||||
int[] ids = new int[out.size()];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
ids[i] = out.get(i).intValue();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The nodes the player has opened, for the client to draw. */
|
||||
public int[] unlockedNodes() {
|
||||
int[] ids = new int[this.unlocked.size()];
|
||||
int i = 0;
|
||||
for (Integer id : this.unlocked) {
|
||||
ids[i++] = id.intValue();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The number of recipes the player knows. */
|
||||
public int knownCount() {
|
||||
return knownRecipes().length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package net.rustnotmc.server.craft;
|
||||
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
|
||||
/**
|
||||
* The research table.
|
||||
*
|
||||
* Researching an item means taking it apart to find out what it is made of,
|
||||
* so the player has to have the item and the scrap to pay for the table. The
|
||||
* item does not survive: it turns into a blueprint, which is the same item
|
||||
* with a mark on it, and a blueprint can be learned, or given away.
|
||||
*
|
||||
* The work takes ten seconds, and it goes on even when the player walks away
|
||||
* from the table.
|
||||
*/
|
||||
public final class ResearchTable {
|
||||
|
||||
/** How long the work takes, in seconds. */
|
||||
public static final int SECONDS = 10;
|
||||
|
||||
/** The mark that turns a stack into a blueprint. */
|
||||
public static final String BLUEPRINT = "blueprint";
|
||||
|
||||
private int item = ItemRegistry.NOTHING;
|
||||
private int secondsLeft;
|
||||
|
||||
/** What is being researched, or nothing. */
|
||||
public int item() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
/** How much is left of the work, in seconds. */
|
||||
public int secondsLeft() {
|
||||
return this.secondsLeft;
|
||||
}
|
||||
|
||||
/** True when something is being researched. */
|
||||
public boolean busy() {
|
||||
return this.item != ItemRegistry.NOTHING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the work.
|
||||
*
|
||||
* The player has to have the item and the scrap. The scrap is taken at
|
||||
* once; the item is taken when the work is done, so that a player who
|
||||
* walks away does not lose the item for nothing.
|
||||
*
|
||||
* Returns the reason it did not start, or null when it did.
|
||||
*/
|
||||
public String start(int item, Inventory inventory) {
|
||||
if (busy()) {
|
||||
return "the table is busy";
|
||||
}
|
||||
ItemType type = ItemRegistry.byId(item);
|
||||
if (type == null || type.recipe == null) {
|
||||
return "there is nothing to find out about that";
|
||||
}
|
||||
if (type.knownByDefault) {
|
||||
return "you already know how to make that";
|
||||
}
|
||||
if (type.researchScrap <= 0) {
|
||||
return "that cannot be researched";
|
||||
}
|
||||
if (inventory.count(item) <= 0) {
|
||||
return "you do not have that";
|
||||
}
|
||||
if (inventory.count(ItemRegistry.SCRAP) < type.researchScrap) {
|
||||
return "you are short of scrap";
|
||||
}
|
||||
inventory.take(ItemRegistry.SCRAP, type.researchScrap);
|
||||
this.item = item;
|
||||
this.secondsLeft = SECONDS;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets the work go on.
|
||||
*
|
||||
* Returns true when the research finished, which is when the item turns
|
||||
* into a blueprint in the bag.
|
||||
*/
|
||||
public boolean tick(Inventory inventory) {
|
||||
if (!busy()) {
|
||||
return false;
|
||||
}
|
||||
this.secondsLeft--;
|
||||
if (this.secondsLeft > 0) {
|
||||
return false;
|
||||
}
|
||||
int done = this.item;
|
||||
this.item = ItemRegistry.NOTHING;
|
||||
this.secondsLeft = 0;
|
||||
if (inventory.count(done) <= 0) {
|
||||
return false;
|
||||
}
|
||||
inventory.take(done, 1);
|
||||
ItemStack blueprint = ItemStack.of(done, 1);
|
||||
blueprint.set(BLUEPRINT, "1");
|
||||
inventory.add(blueprint);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a stack is a blueprint.
|
||||
*
|
||||
* A blueprint is the item with a mark on it, so the client draws the
|
||||
* blueprint texture and the item on top of it.
|
||||
*/
|
||||
public static boolean isBlueprint(ItemStack stack) {
|
||||
return stack != null && "1".equals(stack.get(BLUEPRINT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Learns a blueprint: the recipe opens and the blueprint is used up.
|
||||
*
|
||||
* Returns false when there was no blueprint of that item.
|
||||
*/
|
||||
public static boolean learn(Inventory inventory, Knowledge knowledge, int item) {
|
||||
for (int i = 0; i < Inventory.SLOTS; i++) {
|
||||
ItemStack stack = inventory.get(i);
|
||||
if (stack.item != item || !isBlueprint(stack)) {
|
||||
continue;
|
||||
}
|
||||
if (stack.count <= 1) {
|
||||
inventory.set(i, ItemStack.EMPTY);
|
||||
} else {
|
||||
stack.count--;
|
||||
}
|
||||
knowledge.learn(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package net.rustnotmc.server.world;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
|
||||
/**
|
||||
* A heap of rubbish at the side of the road.
|
||||
*
|
||||
* It is the first place where scrap comes from, and the only one that is
|
||||
* there before the monuments are. A heap is two to four barrels and, now and
|
||||
* then, a box of food. The barrels are broken, the box is looked into, and
|
||||
* when there is nothing left in it the heap goes away after a while and shows
|
||||
* up again somewhere else along the road.
|
||||
*
|
||||
* The barrels are the ones of the original game, with the names of the colours
|
||||
* they are painted in:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the <b>grey</b> one holds scrap and one piece of rubbish;
|
||||
* <li>the <b>blue</b> one holds a part;
|
||||
* <li>the <b>red</b> one is a barrel of fuel: crude oil and low grade fuel.
|
||||
* </ul>
|
||||
*/
|
||||
public final class JunkPile {
|
||||
|
||||
/** A barrel with scrap and rubbish in it. */
|
||||
public static final int GREY = 0;
|
||||
|
||||
/** A barrel with parts in it. */
|
||||
public static final int BLUE = 1;
|
||||
|
||||
/** A barrel of fuel. */
|
||||
public static final int RED = 2;
|
||||
|
||||
/** How long the heap stays after it is empty, at least. */
|
||||
public static final int MIN_LIFE = 60;
|
||||
|
||||
/** How long the heap stays after it is empty, at most. */
|
||||
public static final int MAX_LIFE = 180;
|
||||
|
||||
/** The rubbish a grey barrel can give. */
|
||||
private static final int[] RUBBISH = {
|
||||
ItemRegistry.ROAD_SIGNS, ItemRegistry.TARP, ItemRegistry.ROPE,
|
||||
ItemRegistry.EMPTY_PROPANE_TANK, ItemRegistry.SEWING_KIT,
|
||||
ItemRegistry.BANDANA_MASK, ItemRegistry.TANK_TOP, ItemRegistry.SHORTS,
|
||||
ItemRegistry.T_SHIRT, ItemRegistry.PANTS, ItemRegistry.LEATHER_GLOVES,
|
||||
ItemRegistry.BASEBALL_CAP, ItemRegistry.BOONIE_HAT, ItemRegistry.HIDE_PANTS,
|
||||
ItemRegistry.HIDE_BOOTS, ItemRegistry.HIDE_SKIRT, ItemRegistry.HIDE_CHEST,
|
||||
ItemRegistry.HIDE_PONCHO, ItemRegistry.ROADSIGN_JACKET, ItemRegistry.ROADSIGN_KILT,
|
||||
ItemRegistry.ROADSIGN_GLOVES,
|
||||
};
|
||||
|
||||
/** The parts a blue barrel can give. */
|
||||
private static final int[] PARTS = {
|
||||
ItemRegistry.METAL_BLADE, ItemRegistry.METAL_PIPE, ItemRegistry.GEARS,
|
||||
ItemRegistry.METAL_SPRING, ItemRegistry.SHEET_METAL, ItemRegistry.ROAD_SIGNS,
|
||||
ItemRegistry.ELECTRIC_FUSE, ItemRegistry.ROPE, ItemRegistry.TARP,
|
||||
ItemRegistry.SEWING_KIT, ItemRegistry.EMPTY_PROPANE_TANK,
|
||||
ItemRegistry.SEMI_AUTO_BODY, ItemRegistry.TECH_TRASH,
|
||||
};
|
||||
|
||||
/** What a box of food can hold. */
|
||||
private static final int[] FOOD = {
|
||||
ItemRegistry.APPLE, ItemRegistry.BLUEBERRIES, ItemRegistry.BLACK_RASPBERRIES,
|
||||
ItemRegistry.POTATO, ItemRegistry.CHOCOLATE_BAR, ItemRegistry.CAN_OF_BEANS,
|
||||
ItemRegistry.CAN_OF_TUNA, ItemRegistry.GRANOLA_BAR, ItemRegistry.CORN,
|
||||
ItemRegistry.PUMPKIN, ItemRegistry.MUSHROOM,
|
||||
};
|
||||
|
||||
/** What a box of food can hold instead of food. */
|
||||
private static final int[] DRINK = {
|
||||
ItemRegistry.SMALL_WATER_BOTTLE, ItemRegistry.WATER_JUG,
|
||||
};
|
||||
|
||||
/** One barrel of the heap. */
|
||||
public static final class Barrel {
|
||||
|
||||
public final int kind;
|
||||
public final int x;
|
||||
public final int y;
|
||||
public final int z;
|
||||
private boolean broken;
|
||||
|
||||
Barrel(int kind, int x, int y, int z) {
|
||||
this.kind = kind;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public boolean isBroken() {
|
||||
return this.broken;
|
||||
}
|
||||
|
||||
void broken() {
|
||||
this.broken = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** One box of food of the heap. */
|
||||
public static final class FoodBox {
|
||||
|
||||
public final int x;
|
||||
public final int y;
|
||||
public final int z;
|
||||
private final List<ItemStack> content = new ArrayList<ItemStack>();
|
||||
|
||||
FoodBox(int x, int y, int z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/** What is in the box. */
|
||||
public List<ItemStack> content() {
|
||||
return Collections.unmodifiableList(this.content);
|
||||
}
|
||||
|
||||
boolean empty() {
|
||||
return this.content.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final List<Barrel> barrels = new ArrayList<Barrel>();
|
||||
private final List<FoodBox> boxes = new ArrayList<FoodBox>();
|
||||
private int despawnIn = -1;
|
||||
|
||||
/**
|
||||
* Makes a heap.
|
||||
*
|
||||
* Two to four barrels and, from time to time, a box of food. The barrels
|
||||
* stand in a row along the road, and the box is next to them.
|
||||
*/
|
||||
public JunkPile(int x, int y, int z, Random random) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
|
||||
int count = 2 + random.nextInt(3);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int kind = random.nextInt(3);
|
||||
this.barrels.add(new Barrel(kind, x + i * 2, y, z));
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
FoodBox box = new FoodBox(x, y, z + 2);
|
||||
int slots = 1 + random.nextInt(2);
|
||||
for (int i = 0; i < slots; i++) {
|
||||
int item;
|
||||
if (random.nextInt(4) == 0) {
|
||||
item = DRINK[random.nextInt(DRINK.length)];
|
||||
} else {
|
||||
item = FOOD[random.nextInt(FOOD.length)];
|
||||
}
|
||||
box.content.add(ItemStack.of(item, 1));
|
||||
}
|
||||
this.boxes.add(box);
|
||||
}
|
||||
}
|
||||
|
||||
public int x() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public int y() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public int z() {
|
||||
return this.z;
|
||||
}
|
||||
|
||||
/** The barrels of the heap. */
|
||||
public List<Barrel> barrels() {
|
||||
return Collections.unmodifiableList(this.barrels);
|
||||
}
|
||||
|
||||
/** The boxes of food of the heap. */
|
||||
public List<FoodBox> boxes() {
|
||||
return Collections.unmodifiableList(this.boxes);
|
||||
}
|
||||
|
||||
/** True when there is nothing left in the heap. */
|
||||
public boolean isEmpty() {
|
||||
for (int i = 0; i < this.barrels.size(); i++) {
|
||||
if (!this.barrels.get(i).isBroken()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < this.boxes.size(); i++) {
|
||||
if (!this.boxes.get(i).empty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a barrel and gives what is in it.
|
||||
*
|
||||
* Returns an empty list when the barrel is already broken or is not one
|
||||
* of this heap.
|
||||
*/
|
||||
public List<ItemStack> breakBarrel(Barrel barrel, Random random) {
|
||||
if (barrel == null || barrel.isBroken()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
boolean mine = false;
|
||||
for (int i = 0; i < this.barrels.size(); i++) {
|
||||
if (this.barrels.get(i) == barrel) {
|
||||
mine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mine) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
barrel.broken();
|
||||
List<ItemStack> loot = new ArrayList<ItemStack>();
|
||||
if (barrel.kind == GREY) {
|
||||
int scrap = 1 + random.nextInt(15);
|
||||
loot.add(ItemStack.of(ItemRegistry.SCRAP, scrap));
|
||||
loot.add(ItemStack.of(RUBBISH[random.nextInt(RUBBISH.length)], 1));
|
||||
} else if (barrel.kind == BLUE) {
|
||||
loot.add(ItemStack.of(PARTS[random.nextInt(PARTS.length)], 1));
|
||||
} else {
|
||||
loot.add(ItemStack.of(ItemRegistry.CRUDE_OIL, 10 + random.nextInt(11)));
|
||||
loot.add(ItemStack.of(ItemRegistry.LOW_GRADE_FUEL, 5 + random.nextInt(11)));
|
||||
}
|
||||
if (isEmpty()) {
|
||||
scheduleDespawn(random);
|
||||
}
|
||||
return loot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes something out of a box of food.
|
||||
*
|
||||
* Returns null when the slot is empty or the box is not one of this heap.
|
||||
*/
|
||||
public ItemStack takeFromBox(FoodBox box, int slot) {
|
||||
return takeFromBox(box, slot, new Random());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes something out of a box of food.
|
||||
*
|
||||
* The random is only used to say when the heap goes away once it is empty.
|
||||
*/
|
||||
public ItemStack takeFromBox(FoodBox box, int slot, Random random) {
|
||||
if (box == null || slot < 0 || slot >= box.content.size()) {
|
||||
return null;
|
||||
}
|
||||
ItemStack taken = box.content.remove(slot);
|
||||
if (isEmpty()) {
|
||||
scheduleDespawn(random);
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Says when the heap goes away.
|
||||
*
|
||||
* It does not go at once: the heap stays a little while, and then it is
|
||||
* gone and another one shows up somewhere else.
|
||||
*/
|
||||
public void scheduleDespawn(Random random) {
|
||||
this.despawnIn = MIN_LIFE + random.nextInt(MAX_LIFE - MIN_LIFE + 1);
|
||||
}
|
||||
|
||||
/** How many seconds are left before the heap goes, or -1. */
|
||||
public int despawnIn() {
|
||||
return this.despawnIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets the heap go.
|
||||
*
|
||||
* Returns true when the heap is gone.
|
||||
*/
|
||||
public boolean tick() {
|
||||
if (this.despawnIn < 0) {
|
||||
return false;
|
||||
}
|
||||
this.despawnIn--;
|
||||
return this.despawnIn <= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package net.rustnotmc.server.world;
|
||||
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.NodeType;
|
||||
import net.rustnotmc.protocol.ResourceClass;
|
||||
|
||||
/**
|
||||
* A thing in the world that can be worn down.
|
||||
*
|
||||
* A node has a pool of resource in it, and the pool is also its health. A
|
||||
* swing of the right tool takes some of it out; the last swing gives what is
|
||||
* left, so a node always gives exactly its pool.
|
||||
*
|
||||
* A node can have a mark: a cross on a tree, a hot spot on an outcrop. A
|
||||
* swing that hits the mark does more, and every swing that follows it does a
|
||||
* little more still, up to a limit. A swing that misses resets the bonus.
|
||||
*/
|
||||
public final class ResourceNode {
|
||||
|
||||
private final NodeType type;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private int pool;
|
||||
private int markStreak;
|
||||
|
||||
public ResourceNode(NodeType type, int x, int y, int z) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.pool = type.pool;
|
||||
}
|
||||
|
||||
public NodeType type() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public int x() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public int y() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public int z() {
|
||||
return this.z;
|
||||
}
|
||||
|
||||
/** How much is left in it. */
|
||||
public int pool() {
|
||||
return this.pool;
|
||||
}
|
||||
|
||||
/** True when there is nothing left in it. */
|
||||
public boolean isEmpty() {
|
||||
return this.pool <= 0;
|
||||
}
|
||||
|
||||
/** How many marked swings in a row there were. */
|
||||
public int markStreak() {
|
||||
return this.markStreak;
|
||||
}
|
||||
|
||||
/**
|
||||
* A swing of a tool.
|
||||
*
|
||||
* Returns how much was taken out, or zero when the tool cannot take
|
||||
* anything out of this node: a pickaxe does not cut a tree and a sword
|
||||
* does not mine.
|
||||
*/
|
||||
public int hit(ItemType tool, boolean marked) {
|
||||
if (tool == null || isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
ItemType.Gather gather = tool.gather(this.type.resource);
|
||||
if (gather == null || gather.damage <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int damage = gather.damage;
|
||||
if (marked && this.type.hasMarker()) {
|
||||
this.markStreak++;
|
||||
damage = this.type.swingDamage(damage, this.markStreak);
|
||||
} else {
|
||||
this.markStreak = 0;
|
||||
}
|
||||
int taken = damage < this.pool ? damage : this.pool;
|
||||
this.pool -= taken;
|
||||
return taken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResourceNode[" + this.type.name + " at " + this.x + ", " + this.y + ", "
|
||||
+ this.z + " " + this.pool + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.rustnotmc.server.world;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The roads of the world.
|
||||
*
|
||||
* A grid of straight roads crosses the island and a ring goes around it. The
|
||||
* roads are where the junk piles stand, so the network also knows the points
|
||||
* where a heap may appear.
|
||||
*
|
||||
* Nothing here depends on a random number: the roads are the same in every
|
||||
* world of the same size, because a road that moved between two runs would
|
||||
* leave the save files full of holes.
|
||||
*/
|
||||
public final class RoadNetwork {
|
||||
|
||||
/** How many blocks to the side of the middle of a road. */
|
||||
public static final int HALF_WIDTH = 3;
|
||||
|
||||
/** How far apart the straight roads are. */
|
||||
private final int spacing;
|
||||
|
||||
/** The radius of the ring around the island. */
|
||||
private final int ringRadius;
|
||||
|
||||
/** The size of the world. */
|
||||
private final int size;
|
||||
|
||||
public RoadNetwork(int size) {
|
||||
this.size = size;
|
||||
this.spacing = Math.max(256, size / 4);
|
||||
this.ringRadius = Math.max(128, size / 3);
|
||||
}
|
||||
|
||||
/** How far apart the straight roads are. */
|
||||
public int spacing() {
|
||||
return this.spacing;
|
||||
}
|
||||
|
||||
/** The radius of the ring. */
|
||||
public int ringRadius() {
|
||||
return this.ringRadius;
|
||||
}
|
||||
|
||||
/** The middle of the world. */
|
||||
public int center() {
|
||||
return this.size / 2;
|
||||
}
|
||||
|
||||
/** The middle of the straight road that is nearest to the column. */
|
||||
public int centerLine(int value) {
|
||||
return (int) Math.round(value / (double) this.spacing) * this.spacing;
|
||||
}
|
||||
|
||||
/** True when the column is on a straight road. */
|
||||
public boolean onStraightRoad(int x, int z) {
|
||||
return Math.abs(x - centerLine(x)) <= HALF_WIDTH
|
||||
|| Math.abs(z - centerLine(z)) <= HALF_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* The point of the ring that is nearest to the column, or null when the
|
||||
* column is not on the ring.
|
||||
*/
|
||||
public int[] onRing(int x, int z) {
|
||||
int cx = center();
|
||||
int cz = center();
|
||||
double dx = x - cx;
|
||||
double dz = z - cz;
|
||||
double distance = Math.sqrt(dx * dx + dz * dz);
|
||||
if (distance < 0.001D) {
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(distance - this.ringRadius) > HALF_WIDTH) {
|
||||
return null;
|
||||
}
|
||||
double angle = Math.atan2(dz, dx);
|
||||
int px = cx + (int) Math.round(Math.cos(angle) * this.ringRadius);
|
||||
int pz = cz + (int) Math.round(Math.sin(angle) * this.ringRadius);
|
||||
return new int[]{px, pz};
|
||||
}
|
||||
|
||||
/** True when the column is on any road. */
|
||||
public boolean onRoad(int x, int z) {
|
||||
return onStraightRoad(x, z) || onRing(x, z) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The points along the roads where a junk pile may stand.
|
||||
*
|
||||
* The points are given as columns; the height of the ground there is the
|
||||
* business of the generator.
|
||||
*/
|
||||
public List<int[]> spawnPoints() {
|
||||
List<int[]> points = new ArrayList<int[]>();
|
||||
int half = this.size / 2;
|
||||
int step = 64;
|
||||
for (int v = -half; v <= half; v += step) {
|
||||
points.add(new int[]{v, center()});
|
||||
points.add(new int[]{center(), v});
|
||||
}
|
||||
// and a ring of points along the ring road
|
||||
int around = Math.max(8, this.ringRadius * 2 / step);
|
||||
for (int i = 0; i < around; i++) {
|
||||
double angle = 2.0D * Math.PI * i / around;
|
||||
points.add(new int[]{center() + (int) Math.round(Math.cos(angle) * this.ringRadius),
|
||||
center() + (int) Math.round(Math.sin(angle) * this.ringRadius)});
|
||||
}
|
||||
return Collections.unmodifiableList(points);
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,26 @@ import net.rustnotmc.protocol.ChunkFormat;
|
||||
public final class WorldGenerator {
|
||||
|
||||
private final long seed;
|
||||
private final RoadNetwork roads;
|
||||
|
||||
public WorldGenerator(long seed) {
|
||||
this(seed, null);
|
||||
}
|
||||
|
||||
public WorldGenerator(long seed, RoadNetwork roads) {
|
||||
this.seed = seed;
|
||||
this.roads = roads;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return this.seed;
|
||||
}
|
||||
|
||||
/** The roads of this world, or null when it has none. */
|
||||
public RoadNetwork roads() {
|
||||
return this.roads;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hash of one coordinate, and nothing else.
|
||||
*
|
||||
@@ -89,6 +100,40 @@ public final class WorldGenerator {
|
||||
|
||||
/** The height of solid ground at a block column. */
|
||||
public int heightAt(int blockX, int blockZ) {
|
||||
if (this.roads != null) {
|
||||
int roadHeight = roadHeightAt(blockX, blockZ);
|
||||
if (roadHeight >= 0) {
|
||||
return roadHeight;
|
||||
}
|
||||
}
|
||||
return terrainHeightAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* The height of a road at a column, or -1 when the column is not on one.
|
||||
*
|
||||
* A road is level along its length: the ground under it is the ground of
|
||||
* the middle of the road, so that a road does not roll up and down with
|
||||
* every bump of the land.
|
||||
*/
|
||||
private int roadHeightAt(int blockX, int blockZ) {
|
||||
int middleX = this.roads.centerLine(blockX);
|
||||
int middleZ = this.roads.centerLine(blockZ);
|
||||
if (Math.abs(blockX - middleX) <= RoadNetwork.HALF_WIDTH) {
|
||||
return terrainHeightAt(middleX, blockZ);
|
||||
}
|
||||
if (Math.abs(blockZ - middleZ) <= RoadNetwork.HALF_WIDTH) {
|
||||
return terrainHeightAt(blockX, middleZ);
|
||||
}
|
||||
int[] ring = this.roads.onRing(blockX, blockZ);
|
||||
if (ring != null) {
|
||||
return terrainHeightAt(ring[0], ring[1]);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** The height of the land itself, with no road on it. */
|
||||
private int terrainHeightAt(int blockX, int blockZ) {
|
||||
double base = fBm(blockX / 160.0D, blockZ / 160.0D, 4);
|
||||
double detail = fBm(blockX / 32.0D, blockZ / 32.0D, 2) * 0.35D;
|
||||
int h = (int) Math.round(62.0D + base * 26.0D + detail * 6.0D);
|
||||
@@ -116,6 +161,7 @@ public final class WorldGenerator {
|
||||
int worldX = baseX + x;
|
||||
int worldZ = baseZ + z;
|
||||
int height = heightAt(worldX, worldZ);
|
||||
boolean road = this.roads != null && this.roads.onRoad(worldX, worldZ);
|
||||
|
||||
// a column: dirt under the surface, grass on top, stone below
|
||||
for (int y = 0; y <= height; y++) {
|
||||
@@ -123,7 +169,7 @@ public final class WorldGenerator {
|
||||
if (y == 0) {
|
||||
id = Blocks.STONE;
|
||||
} else if (y == height) {
|
||||
id = Blocks.GRASS;
|
||||
id = road ? Blocks.ASPHALT : Blocks.GRASS;
|
||||
} else if (y > height - 4) {
|
||||
id = Blocks.DIRT;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package net.rustnotmc.server.craft;
|
||||
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
|
||||
/**
|
||||
* Tests what a player knows and what a player can make.
|
||||
*
|
||||
* The knowledge belongs to the player, the workbench is a place in the world
|
||||
* and the ingredients are in the bag, and all three have to agree before
|
||||
* anything is made.
|
||||
*/
|
||||
public final class CraftTest {
|
||||
|
||||
private static int checks;
|
||||
private static int failed;
|
||||
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||
|
||||
public static void main(String[] args) {
|
||||
run("the known things are known from the start", new Throwing() {
|
||||
public void go() { testKnownByDefault(); }
|
||||
});
|
||||
run("the rest has to be researched", new Throwing() {
|
||||
public void go() { testUnknownByDefault(); }
|
||||
});
|
||||
run("a branch has to be walked", new Throwing() {
|
||||
public void go() { testTreeBranch(); }
|
||||
});
|
||||
run("opening a node costs scrap", new Throwing() {
|
||||
public void go() { testTreeCost(); }
|
||||
});
|
||||
run("a thing is made out of what is in the bag", new Throwing() {
|
||||
public void go() { testCraft(); }
|
||||
});
|
||||
run("the workbench has to be there", new Throwing() {
|
||||
public void go() { testBenchGate(); }
|
||||
});
|
||||
run("a better workbench makes it faster", new Throwing() {
|
||||
public void go() { testCraftTime(); }
|
||||
});
|
||||
run("researching turns the thing into a blueprint", new Throwing() {
|
||||
public void go() { testResearch(); }
|
||||
});
|
||||
run("a blueprint is learned", new Throwing() {
|
||||
public void go() { testLearnBlueprint(); }
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
for (int i = 0; i < failures.size(); i++) {
|
||||
System.out.println(" FAIL " + failures.get(i));
|
||||
}
|
||||
System.out.println();
|
||||
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||
if (failed != 0) {
|
||||
System.out.println("FAILED");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("OK");
|
||||
}
|
||||
|
||||
private interface Throwing {
|
||||
void go() throws Exception;
|
||||
}
|
||||
|
||||
private static void run(String name, Throwing body) {
|
||||
checks++;
|
||||
int before = failed;
|
||||
try {
|
||||
body.go();
|
||||
} catch (Throwable t) {
|
||||
failed++;
|
||||
failures.add(name + ": threw " + t);
|
||||
t.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (failed == before) {
|
||||
System.out.println(" ok " + name);
|
||||
} else {
|
||||
System.out.println(" FAIL " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void eq(Object what, Object expected, Object actual) {
|
||||
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||
failed++;
|
||||
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void yes(String what, boolean condition) {
|
||||
if (!condition) {
|
||||
failed++;
|
||||
failures.add(what);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testKnownByDefault() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
yes("the rock is known", knowledge.knows(ItemRegistry.ROCK));
|
||||
yes("the stone hatchet is known", knowledge.knows(ItemRegistry.STONE_HATCHET));
|
||||
yes("the workbench is known", knowledge.knows(ItemRegistry.WORKBENCH_1));
|
||||
yes("there is more than a handful of known things", knowledge.knownCount() > 20);
|
||||
}
|
||||
|
||||
private static void testUnknownByDefault() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
yes("the hatchet is not known", !knowledge.knows(ItemRegistry.HATCHET));
|
||||
yes("the salvaged sword is not known", !knowledge.knows(ItemRegistry.SALVAGED_SWORD));
|
||||
knowledge.learn(ItemRegistry.HATCHET);
|
||||
yes("after the research the hatchet is known", knowledge.knows(ItemRegistry.HATCHET));
|
||||
}
|
||||
|
||||
private static void testTreeBranch() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
yes("the root of the branch can be opened",
|
||||
knowledge.canUnlock(ItemRegistry.SALVAGED_HAMMER));
|
||||
yes("the next one cannot", !knowledge.canUnlock(ItemRegistry.HATCHET));
|
||||
yes("and it says why",
|
||||
knowledge.reasonCannotUnlock(ItemRegistry.HATCHET) != null);
|
||||
knowledge.unlock(ItemRegistry.SALVAGED_HAMMER);
|
||||
yes("with the hammer the hatchet opens",
|
||||
knowledge.canUnlock(ItemRegistry.HATCHET));
|
||||
knowledge.unlock(ItemRegistry.HATCHET);
|
||||
yes("with the hatchet the sword opens",
|
||||
knowledge.canUnlock(ItemRegistry.SALVAGED_SWORD));
|
||||
yes("an opened node knows the recipe", knowledge.knows(ItemRegistry.HATCHET));
|
||||
}
|
||||
|
||||
private static void testTreeCost() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
knowledge.unlock(ItemRegistry.SALVAGED_HAMMER);
|
||||
eq("the node costs a hundred twenty five", Integer.valueOf(125),
|
||||
Integer.valueOf(net.rustnotmc.protocol.TechTree
|
||||
.costOf(ItemRegistry.HATCHET)));
|
||||
eq("the nodes that are open are told to the client", Integer.valueOf(1),
|
||||
Integer.valueOf(knowledge.unlockedNodes().length));
|
||||
}
|
||||
|
||||
private static void testCraft() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
Inventory inventory = new Inventory();
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
knowledge.learn(ItemRegistry.HATCHET);
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 100));
|
||||
inventory.add(ItemStack.of(ItemRegistry.METAL, 75));
|
||||
|
||||
yes("without a workbench it cannot be made",
|
||||
Crafter.whyNot(hatchet, knowledge, 0, inventory) != null);
|
||||
|
||||
yes("it can be made at the first workbench",
|
||||
Crafter.whyNot(hatchet, knowledge, 1, inventory) == null);
|
||||
yes("the craft works", Crafter.craft(hatchet, 1, knowledge, 1, inventory));
|
||||
eq("the hatchet is in the bag", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
|
||||
eq("the wood is gone", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
eq("the metal is gone", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.METAL)));
|
||||
}
|
||||
|
||||
private static void testBenchGate() {
|
||||
Knowledge knowledge = new Knowledge();
|
||||
Inventory inventory = new Inventory();
|
||||
ItemType pickaxe = ItemRegistry.require(ItemRegistry.PICKAXE);
|
||||
knowledge.learn(ItemRegistry.PICKAXE);
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 100));
|
||||
inventory.add(ItemStack.of(ItemRegistry.METAL, 125));
|
||||
|
||||
yes("the pickaxe wants the first workbench",
|
||||
Crafter.whyNot(pickaxe, knowledge, 0, inventory) != null);
|
||||
yes("and it is refused without it",
|
||||
!Crafter.craft(pickaxe, 1, knowledge, 0, inventory));
|
||||
eq("nothing was made", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.PICKAXE)));
|
||||
yes("at the workbench it goes through",
|
||||
Crafter.craft(pickaxe, 1, knowledge, 1, inventory));
|
||||
|
||||
// and without the knowledge it is refused as well
|
||||
Knowledge empty = new Knowledge();
|
||||
eq("the rock is short of nothing", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.ROCK)));
|
||||
yes("an unknown recipe is refused",
|
||||
Crafter.whyNot(pickaxe, empty, 1, inventory) != null);
|
||||
}
|
||||
|
||||
private static void testCraftTime() {
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
eq("at the workbench it wants thirty seconds", Integer.valueOf(30),
|
||||
Integer.valueOf(Crafter.seconds(hatchet, 1)));
|
||||
eq("one level above is half", Integer.valueOf(15),
|
||||
Integer.valueOf(Crafter.seconds(hatchet, 2)));
|
||||
eq("two levels above is a quarter", Integer.valueOf(7),
|
||||
Integer.valueOf(Crafter.seconds(hatchet, 3)));
|
||||
eq("three levels above is no faster", Integer.valueOf(7),
|
||||
Integer.valueOf(Crafter.seconds(hatchet, 4)));
|
||||
}
|
||||
|
||||
private static void testResearch() {
|
||||
Inventory inventory = new Inventory();
|
||||
inventory.add(ItemStack.of(ItemRegistry.HATCHET, 1));
|
||||
inventory.add(ItemStack.of(ItemRegistry.SCRAP, 30));
|
||||
|
||||
ResearchTable table = new ResearchTable();
|
||||
String problem = table.start(ItemRegistry.HATCHET, inventory);
|
||||
yes("the research starts: " + problem, problem == null);
|
||||
eq("the scrap is taken at once", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.SCRAP)));
|
||||
eq("the item is still there while the work goes on", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
|
||||
yes("the table is busy", table.busy());
|
||||
|
||||
boolean done = false;
|
||||
for (int i = 0; i < 20 && !done; i++) {
|
||||
done = table.tick(inventory);
|
||||
}
|
||||
yes("the work finishes", done);
|
||||
eq("the item is a blueprint now", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
|
||||
yes("and it is a blueprint", ResearchTable.isBlueprint(inventory.get(0)));
|
||||
yes("the plain item is not a blueprint",
|
||||
!ResearchTable.isBlueprint(ItemStack.of(ItemRegistry.HATCHET, 1)));
|
||||
}
|
||||
|
||||
private static void testLearnBlueprint() {
|
||||
Inventory inventory = new Inventory();
|
||||
ItemStack blueprint = ItemStack.of(ItemRegistry.HATCHET, 1);
|
||||
blueprint.set(ResearchTable.BLUEPRINT, "1");
|
||||
inventory.add(blueprint);
|
||||
|
||||
Knowledge knowledge = new Knowledge();
|
||||
yes("the hatchet is not known yet", !knowledge.knows(ItemRegistry.HATCHET));
|
||||
yes("the blueprint is learned", ResearchTable.learn(inventory, knowledge,
|
||||
ItemRegistry.HATCHET));
|
||||
yes("and now it is known", knowledge.knows(ItemRegistry.HATCHET));
|
||||
eq("the blueprint is used up", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package net.rustnotmc.server.world;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.rustnotmc.protocol.Blocks;
|
||||
import net.rustnotmc.protocol.ChunkFormat;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.NodeType;
|
||||
|
||||
/**
|
||||
* Tests the things in the world that can be worn down: the nodes of resource,
|
||||
* the heaps of rubbish and the roads they stand by.
|
||||
*/
|
||||
public final class NodeTest {
|
||||
|
||||
private static int checks;
|
||||
private static int failed;
|
||||
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||
|
||||
public static void main(String[] args) {
|
||||
run("a node is worn down by the right tool", new Throwing() {
|
||||
public void go() { testGather(); }
|
||||
});
|
||||
run("the last swing gives what is left", new Throwing() {
|
||||
public void go() { testLastSwing(); }
|
||||
});
|
||||
run("the wrong tool takes nothing", new Throwing() {
|
||||
public void go() { testWrongTool(); }
|
||||
});
|
||||
run("the mark makes a swing stronger", new Throwing() {
|
||||
public void go() { testMark(); }
|
||||
});
|
||||
run("a heap of rubbish gives scrap", new Throwing() {
|
||||
public void go() { testJunkPile(); }
|
||||
});
|
||||
run("a heap goes away when it is empty", new Throwing() {
|
||||
public void go() { testJunkPileLife(); }
|
||||
});
|
||||
run("the roads are where they should be", new Throwing() {
|
||||
public void go() { testRoads(); }
|
||||
});
|
||||
run("a road is level along its length", new Throwing() {
|
||||
public void go() { testRoadIsLevel(); }
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
for (int i = 0; i < failures.size(); i++) {
|
||||
System.out.println(" FAIL " + failures.get(i));
|
||||
}
|
||||
System.out.println();
|
||||
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||
if (failed != 0) {
|
||||
System.out.println("FAILED");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("OK");
|
||||
}
|
||||
|
||||
private interface Throwing {
|
||||
void go() throws Exception;
|
||||
}
|
||||
|
||||
private static void run(String name, Throwing body) {
|
||||
checks++;
|
||||
int before = failed;
|
||||
try {
|
||||
body.go();
|
||||
} catch (Throwable t) {
|
||||
failed++;
|
||||
failures.add(name + ": threw " + t);
|
||||
t.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (failed == before) {
|
||||
System.out.println(" ok " + name);
|
||||
} else {
|
||||
System.out.println(" FAIL " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void eq(Object what, Object expected, Object actual) {
|
||||
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||
failed++;
|
||||
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void yes(String what, boolean condition) {
|
||||
if (!condition) {
|
||||
failed++;
|
||||
failures.add(what);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testGather() {
|
||||
ResourceNode node = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
int taken = node.hit(hatchet, false);
|
||||
eq("the hatchet takes eighty five wood", Integer.valueOf(85), Integer.valueOf(taken));
|
||||
eq("the node has less in it", Integer.valueOf(750 - 85),
|
||||
Integer.valueOf(node.pool()));
|
||||
}
|
||||
|
||||
private static void testLastSwing() {
|
||||
ResourceNode node = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
int total = 0;
|
||||
for (int i = 0; i < 20 && !node.isEmpty(); i++) {
|
||||
total += node.hit(hatchet, false);
|
||||
}
|
||||
eq("the node gives exactly its pool", Integer.valueOf(750), Integer.valueOf(total));
|
||||
yes("and then it is gone", node.isEmpty());
|
||||
eq("an empty node gives nothing", Integer.valueOf(0),
|
||||
Integer.valueOf(node.hit(hatchet, false)));
|
||||
}
|
||||
|
||||
private static void testWrongTool() {
|
||||
ResourceNode tree = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
|
||||
ItemType pickaxe = ItemRegistry.require(ItemRegistry.PICKAXE);
|
||||
eq("a pickaxe does not cut", Integer.valueOf(0),
|
||||
Integer.valueOf(tree.hit(pickaxe, false)));
|
||||
|
||||
ResourceNode ore = new ResourceNode(NodeType.byId(NodeType.STONE), 0, 64, 0);
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
eq("an axe does not mine", Integer.valueOf(0),
|
||||
Integer.valueOf(ore.hit(hatchet, false)));
|
||||
}
|
||||
|
||||
private static void testMark() {
|
||||
ResourceNode tree = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
|
||||
int plain = tree.hit(hatchet, false);
|
||||
ResourceNode marked = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
|
||||
int onMark = marked.hit(hatchet, true);
|
||||
yes("a swing on the cross takes more", onMark > plain);
|
||||
eq("and the streak is kept", Integer.valueOf(1),
|
||||
Integer.valueOf(marked.markStreak()));
|
||||
marked.hit(hatchet, false);
|
||||
eq("a swing that misses resets it", Integer.valueOf(0),
|
||||
Integer.valueOf(marked.markStreak()));
|
||||
}
|
||||
|
||||
private static void testJunkPile() {
|
||||
Random random = new Random(7);
|
||||
JunkPile heap = new JunkPile(10, 64, 10, random);
|
||||
List<JunkPile.Barrel> barrels = heap.barrels();
|
||||
yes("a heap has two to four barrels", barrels.size() >= 2 && barrels.size() <= 4);
|
||||
|
||||
int scrap = 0;
|
||||
for (int i = 0; i < barrels.size(); i++) {
|
||||
List<ItemStack> loot = heap.breakBarrel(barrels.get(i), random);
|
||||
for (int j = 0; j < loot.size(); j++) {
|
||||
if (loot.get(j).item == ItemRegistry.SCRAP) {
|
||||
scrap += loot.get(j).count;
|
||||
}
|
||||
}
|
||||
}
|
||||
yes("the grey barrels give scrap", scrap > 0);
|
||||
yes("a broken barrel stays broken", barrels.get(0).isBroken());
|
||||
yes("breaking it twice gives nothing",
|
||||
heap.breakBarrel(barrels.get(0), random).isEmpty());
|
||||
}
|
||||
|
||||
private static void testJunkPileLife() {
|
||||
Random random = new Random(11);
|
||||
JunkPile heap = new JunkPile(10, 64, 10, random);
|
||||
List<JunkPile.Barrel> barrels = heap.barrels();
|
||||
for (int i = 0; i < barrels.size(); i++) {
|
||||
heap.breakBarrel(barrels.get(i), random);
|
||||
}
|
||||
// the box of food has to be emptied as well
|
||||
List<JunkPile.FoodBox> boxes = heap.boxes();
|
||||
for (int i = 0; i < boxes.size(); i++) {
|
||||
JunkPile.FoodBox box = boxes.get(i);
|
||||
while (heap.takeFromBox(box, 0, random) != null) {
|
||||
// keep taking until the box is empty
|
||||
}
|
||||
}
|
||||
yes("the heap is empty", heap.isEmpty());
|
||||
yes("and it is going away", heap.despawnIn() > 0);
|
||||
boolean gone = false;
|
||||
for (int i = 0; i < 200 && !gone; i++) {
|
||||
gone = heap.tick();
|
||||
}
|
||||
yes("and then it is gone", gone);
|
||||
}
|
||||
|
||||
private static void testRoads() {
|
||||
RoadNetwork roads = new RoadNetwork(4000);
|
||||
yes("the middle of the world is on a road", roads.onRoad(2000, 2000));
|
||||
yes("the middle of a straight road is on a road", roads.onRoad(2000, 2007));
|
||||
yes("far away from a road is not on a road", !roads.onRoad(2500, 2500));
|
||||
yes("there are points where a heap may stand", roads.spawnPoints().size() > 10);
|
||||
}
|
||||
|
||||
private static void testRoadIsLevel() {
|
||||
RoadNetwork roads = new RoadNetwork(4000);
|
||||
WorldGenerator generator = new WorldGenerator(1234L, roads);
|
||||
// a stretch of road that does not cross another one
|
||||
int height = generator.heightAt(2000, 2400);
|
||||
eq("the road is level across it", Integer.valueOf(height),
|
||||
Integer.valueOf(generator.heightAt(2000 + 3, 2400)));
|
||||
eq("and level across it the other way", Integer.valueOf(height),
|
||||
Integer.valueOf(generator.heightAt(2000, 2400 + 3)));
|
||||
|
||||
// and the surface of a road is asphalt
|
||||
Chunk chunk = new Chunk(new ChunkPos(125, 125));
|
||||
generator.generate(chunk);
|
||||
byte[] blocks = chunk.snapshot();
|
||||
int top = -1;
|
||||
for (int y = Chunk.HEIGHT - 1; y >= 0; y--) {
|
||||
if (blocks[ChunkFormat.index(0, y, 0)] != Blocks.AIR) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eq("the top of the road is asphalt", Integer.valueOf(Blocks.ASPHALT),
|
||||
Integer.valueOf(blocks[ChunkFormat.index(0, top, 0)]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user