Броня в семи местах и три зоны урона
- В реестр заведены 46 предметов одежды ремейка: слот, слой, какие части тела закрывает и сколько снимает из каждого вида вреда. Данные выгружены в items.csv (extract-items.py), в коде у каждого предмета armor(...). - Семь мест: голова, лицо, грудь (2 слоя), ноги (2 слоя), руки, стопы, полный костюм. Пояс не делали - в ремейке его нет, есть только быстрая панель. - Inventory сервера: семь мест по два слоя, вещи надеваются и снимаются. - Damage: зона урона по точке попадания (голова верхние 0.4, ноги нижние 0.4), множители голова 2, тело 1, ноги 0.75; одежда снимает вред только там, где закрывает. - Пакет Attack (цель, точка попадания, рука) и клиентский патч, который его шлёт. У игрока появилась жизнь. - В items.csv добавлены колонки armor_slot, armor_layer, armor_zones. Тесты: протокол 24 + 10, сервер 85. Форк собирается чисто, 3279 классов.
This commit is contained in:
@@ -52,6 +52,7 @@ $suites = @(
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.craft.CraftTest',
|
||||
'net.rustnotmc.server.hitbox.HitboxTest',
|
||||
'net.rustnotmc.server.combat.DamageTest',
|
||||
'net.rustnotmc.server.PlayTest'
|
||||
)
|
||||
foreach ($suite in $suites) {
|
||||
|
||||
@@ -232,6 +232,9 @@ public final class ClientSession implements Runnable {
|
||||
case Packets.HITBOXES_RELOAD:
|
||||
onHitboxesReload(frame);
|
||||
return true;
|
||||
case Packets.ATTACK:
|
||||
onAttack(frame);
|
||||
return true;
|
||||
case Packets.PLAYER_MOVE:
|
||||
onPlayerMove(frame);
|
||||
return true;
|
||||
@@ -637,6 +640,44 @@ public final class ClientSession implements Runnable {
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
}
|
||||
|
||||
/**
|
||||
* The client hits something.
|
||||
*
|
||||
* The place of the hit decides how much it hurts, and the clothing of the
|
||||
* one who is hit takes a part of it away. The client never decides any of
|
||||
* that: it says where it hit, and the server counts.
|
||||
*/
|
||||
private void onAttack(Frame frame) {
|
||||
if (this.state != SessionState.PLAYING || this.player == null) {
|
||||
return;
|
||||
}
|
||||
Packets.Attack attack = Packets.Attack.read(new Buf(frame.body()));
|
||||
Player target = this.server.playerByEntity(attack.target);
|
||||
if (target == null || target == this.player) {
|
||||
return;
|
||||
}
|
||||
net.rustnotmc.protocol.ItemType weapon = this.player.inventory()
|
||||
.inHand().type();
|
||||
if (weapon == null) {
|
||||
return;
|
||||
}
|
||||
float base = weapon.damage(net.rustnotmc.protocol.ItemType.TORSO);
|
||||
if (base <= 0) {
|
||||
return;
|
||||
}
|
||||
String zone = net.rustnotmc.server.combat.Damage.zoneOf(attack.y, target.y(),
|
||||
net.rustnotmc.server.Player.HEIGHT);
|
||||
float hurt = net.rustnotmc.server.combat.Damage.apply(base, zone,
|
||||
target.inventory().armor(),
|
||||
net.rustnotmc.protocol.ItemType.HARM_MELEE);
|
||||
if (target.damage(hurt)) {
|
||||
this.server.log(this.account + " killed " + target.account());
|
||||
} else {
|
||||
this.server.log(this.account + " hit " + target.account() + " for "
|
||||
+ (int) hurt + " (" + zone + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client wants the box file read again.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,12 @@ public final class Player {
|
||||
/** How far a player may reach to change a block, in blocks. */
|
||||
public static final double REACH = 5.0D;
|
||||
|
||||
/** How much life a player has, and how much a full bar holds. */
|
||||
public static final float MAX_HEALTH = 100.0F;
|
||||
|
||||
/** How tall a player is, in blocks. */
|
||||
public static final float HEIGHT = 1.8F;
|
||||
|
||||
/** How far a player may see, in chunks, unless it asks for less. */
|
||||
public static final int DEFAULT_VIEW_DISTANCE = 6;
|
||||
|
||||
@@ -56,6 +62,9 @@ public final class Player {
|
||||
/** What the player is carrying. The server owns it. */
|
||||
private final Inventory inventory;
|
||||
|
||||
/** How much life the player has left. */
|
||||
private float health = MAX_HEALTH;
|
||||
|
||||
/** What the player knows: the recipes and the nodes of the trees. */
|
||||
private final Knowledge knowledge;
|
||||
|
||||
@@ -66,6 +75,32 @@ public final class Player {
|
||||
private int craftItem = ItemRegistry.NOTHING;
|
||||
private int craftSecondsLeft;
|
||||
|
||||
/** How much life the player has left. */
|
||||
public float health() {
|
||||
return this.health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hurts the player.
|
||||
*
|
||||
* Returns true when the hit was the last one.
|
||||
*/
|
||||
public boolean damage(float amount) {
|
||||
if (amount <= 0.0F) {
|
||||
return false;
|
||||
}
|
||||
this.health -= amount;
|
||||
if (this.health < 0.0F) {
|
||||
this.health = 0.0F;
|
||||
}
|
||||
return this.health <= 0.0F;
|
||||
}
|
||||
|
||||
/** Gives the player their life back. */
|
||||
public void heal(float amount) {
|
||||
this.health = Math.min(MAX_HEALTH, this.health + amount);
|
||||
}
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn) {
|
||||
this(session, playerId, entityId, account, spawn,
|
||||
|
||||
@@ -125,6 +125,21 @@ public final class Server {
|
||||
return this.players;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player with that entity id, or null.
|
||||
*
|
||||
* The client only knows the entity id of what it sees, so that is the
|
||||
* number a hit comes with.
|
||||
*/
|
||||
public Player playerByEntity(int entityId) {
|
||||
for (Player player : this.players.values()) {
|
||||
if (player.entityId() == entityId) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** How far a new player sees, in chunks. */
|
||||
public int viewDistance() {
|
||||
return this.viewDistance;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package net.rustnotmc.server.combat;
|
||||
|
||||
import net.rustnotmc.protocol.Hitbox;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
|
||||
/**
|
||||
* How much a hit hurts.
|
||||
*
|
||||
* A hit lands somewhere on the target, and where it landed decides how much
|
||||
* it hurts: a hit on the head hurts twice as much as a hit on the body, and a
|
||||
* hit on the legs hurts a little less. That is the rule of the original game,
|
||||
* and the head is the only place where it is written down.
|
||||
*
|
||||
* The clothing takes a part of the harm away, but only for the parts of the
|
||||
* body it covers: a hat does nothing for a hit in the chest.
|
||||
*/
|
||||
public final class Damage {
|
||||
|
||||
/** How much a hit on the head hurts, against a hit on the body. */
|
||||
public static final float HEAD_MULTIPLIER = 2.0F;
|
||||
|
||||
/** How much a hit on the legs hurts, against a hit on the body. */
|
||||
public static final float LEGS_MULTIPLIER = 0.75F;
|
||||
|
||||
/** How tall the head of a player is, in blocks. */
|
||||
public static final float HEAD_HEIGHT = 0.4F;
|
||||
|
||||
/** How tall the legs of a player are, in blocks. */
|
||||
public static final float LEGS_HEIGHT = 0.4F;
|
||||
|
||||
private Damage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a hit landed.
|
||||
*
|
||||
* @param hitY where the hit landed
|
||||
* @param feetY where the feet of the target are
|
||||
* @param height how tall the target is
|
||||
*/
|
||||
public static String zoneOf(double hitY, double feetY, double height) {
|
||||
double above = hitY - feetY;
|
||||
if (above < 0.0D) {
|
||||
above = 0.0D;
|
||||
}
|
||||
if (above > height) {
|
||||
above = height;
|
||||
}
|
||||
if (above >= height - HEAD_HEIGHT) {
|
||||
return Hitbox.PART_HEAD;
|
||||
}
|
||||
if (above < LEGS_HEIGHT) {
|
||||
return Hitbox.PART_LEGS;
|
||||
}
|
||||
return Hitbox.PART_BODY;
|
||||
}
|
||||
|
||||
/** How much a hit on that zone hurts, before the clothing. */
|
||||
public static float multiplier(String zone) {
|
||||
if (Hitbox.PART_HEAD.equals(zone)) {
|
||||
return HEAD_MULTIPLIER;
|
||||
}
|
||||
if (Hitbox.PART_LEGS.equals(zone)) {
|
||||
return LEGS_MULTIPLIER;
|
||||
}
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much of a kind of harm the clothing takes away.
|
||||
*
|
||||
* The pieces that cover the zone are added up, and the sum never goes
|
||||
* past a whole.
|
||||
*/
|
||||
public static float protection(ItemStack[][] armor, String zone, int harm) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < armor.length; i++) {
|
||||
for (int layer = 0; layer < armor[i].length; layer++) {
|
||||
ItemStack stack = armor[i][layer];
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ItemType type = stack.type();
|
||||
if (type == null || type.armorSlot == ItemType.ARMOR_NONE) {
|
||||
continue;
|
||||
}
|
||||
if (!covers(type, zone)) {
|
||||
continue;
|
||||
}
|
||||
if (harm < 0 || harm >= type.protection.length) {
|
||||
continue;
|
||||
}
|
||||
total += type.protection[harm];
|
||||
}
|
||||
}
|
||||
if (total > 100) {
|
||||
total = 100;
|
||||
}
|
||||
if (total < 0) {
|
||||
total = 0;
|
||||
}
|
||||
return total / 100.0F;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much damage a hit does.
|
||||
*
|
||||
* The base damage of the weapon, times the zone, minus what the clothing
|
||||
* takes away.
|
||||
*/
|
||||
public static float apply(float base, String zone, ItemStack[][] armor, int harm) {
|
||||
float withZone = base * multiplier(zone);
|
||||
float taken = protection(armor, zone, harm);
|
||||
return withZone * (1.0F - taken);
|
||||
}
|
||||
|
||||
/** True when a piece of clothing covers that zone. */
|
||||
public static boolean covers(ItemType type, String zone) {
|
||||
if (type.coveredZones == 0) {
|
||||
return false;
|
||||
}
|
||||
if (Hitbox.PART_HEAD.equals(zone)) {
|
||||
return (type.coveredZones & Hitbox.ZONE_HEAD) != 0;
|
||||
}
|
||||
if (Hitbox.PART_LEGS.equals(zone)) {
|
||||
return (type.coveredZones & Hitbox.ZONE_LEGS) != 0;
|
||||
}
|
||||
return (type.coveredZones & Hitbox.ZONE_BODY) != 0;
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,24 @@ public final class Inventory {
|
||||
private final ItemStack[] slots = new ItemStack[SLOTS];
|
||||
private int held;
|
||||
|
||||
/**
|
||||
* The clothing the player wears.
|
||||
*
|
||||
* Seven places, as in the original game: the head, the face, the chest,
|
||||
* the legs, the hands, the feet and the whole body. The chest and the legs
|
||||
* have two layers, the clothes under and the armour over.
|
||||
*/
|
||||
private final ItemStack[][] armor =
|
||||
new ItemStack[net.rustnotmc.protocol.ItemType.ARMOR_SLOTS][2];
|
||||
|
||||
public Inventory() {
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
this.slots[i] = ItemStack.EMPTY;
|
||||
}
|
||||
for (int i = 0; i < this.armor.length; i++) {
|
||||
this.armor[i][0] = ItemStack.EMPTY;
|
||||
this.armor[i][1] = ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +170,69 @@ public final class Inventory {
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
out[i] = Packets.Slot.of(get(i));
|
||||
}
|
||||
return new Packets.Inventory(out, this.held);
|
||||
return new Packets.Inventory(out, this.held, armorToPacket());
|
||||
}
|
||||
|
||||
/**
|
||||
* The clothing as the client is told about it.
|
||||
*
|
||||
* Seven places, two layers each, so fourteen stacks.
|
||||
*/
|
||||
private Packets.Slot[] armorToPacket() {
|
||||
Packets.Slot[] out = new Packets.Slot[this.armor.length * 2];
|
||||
for (int i = 0; i < this.armor.length; i++) {
|
||||
for (int layer = 0; layer < 2; layer++) {
|
||||
out[i * 2 + layer] = Packets.Slot.of(this.armor[i][layer]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The piece of clothing in a slot, or nothing.
|
||||
*
|
||||
* The layer is zero for the clothes under and one for the armour over.
|
||||
*/
|
||||
public ItemStack armor(int slot, int layer) {
|
||||
if (slot < 0 || slot >= this.armor.length || layer < 0 || layer > 1) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
ItemStack stack = this.armor[slot][layer];
|
||||
return stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** Puts a piece of clothing in a slot. */
|
||||
public void setArmor(int slot, int layer, ItemStack stack) {
|
||||
if (slot < 0 || slot >= this.armor.length || layer < 0 || layer > 1) {
|
||||
return;
|
||||
}
|
||||
this.armor[slot][layer] = stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** Every piece of clothing, slot by slot and layer by layer. */
|
||||
public ItemStack[][] armor() {
|
||||
return this.armor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a piece of clothing on, if it fits where it goes.
|
||||
*
|
||||
* Returns the piece that was in the way, or nothing.
|
||||
*/
|
||||
public ItemStack wear(int item) {
|
||||
net.rustnotmc.protocol.ItemType type =
|
||||
net.rustnotmc.protocol.ItemRegistry.byId(item);
|
||||
if (type == null || type.armorSlot == net.rustnotmc.protocol.ItemType.ARMOR_NONE) {
|
||||
return null;
|
||||
}
|
||||
int slot = type.armorSlot - 1;
|
||||
int layer = type.armorLayer;
|
||||
if (slot < 0 || slot >= this.armor.length) {
|
||||
return null;
|
||||
}
|
||||
ItemStack was = armor(slot, layer);
|
||||
setArmor(slot, layer, ItemStack.of(item, 1));
|
||||
return was.isEmpty() ? null : was;
|
||||
}
|
||||
|
||||
/** The slots that are not empty, for a look at what a player carries. */
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.rustnotmc.server.combat;
|
||||
|
||||
import net.rustnotmc.protocol.Hitbox;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
|
||||
/**
|
||||
* Tests how much a hit hurts.
|
||||
*
|
||||
* The rules are the ones of the original game: the head is worth twice the
|
||||
* body, the legs a little less, and a piece of clothing only takes a part of
|
||||
* the harm away where it covers the body.
|
||||
*/
|
||||
public final class DamageTest {
|
||||
|
||||
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 hit on the head is worth more", new Throwing() {
|
||||
public void go() { testZones(); }
|
||||
});
|
||||
run("the multipliers are the ones of the original", new Throwing() {
|
||||
public void go() { testMultipliers(); }
|
||||
});
|
||||
run("the clothing only helps where it covers", new Throwing() {
|
||||
public void go() { testProtection(); }
|
||||
});
|
||||
run("the clothing is worn in seven places", new Throwing() {
|
||||
public void go() { testSlots(); }
|
||||
});
|
||||
run("the clothes come out of the remake's table", new Throwing() {
|
||||
public void go() { testTable(); }
|
||||
});
|
||||
|
||||
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 near(Object what, double expected, double actual) {
|
||||
if (Math.abs(expected - actual) > 0.0001D) {
|
||||
failed++;
|
||||
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testZones() {
|
||||
// a player of 1.8 blocks: the head is the top 0.4, the legs the bottom
|
||||
eq("a hit on the top of the head", Hitbox.PART_HEAD,
|
||||
Damage.zoneOf(1.75D, 0.0D, 1.8D));
|
||||
eq("a hit on the face", Hitbox.PART_HEAD,
|
||||
Damage.zoneOf(1.45D, 0.0D, 1.8D));
|
||||
eq("a hit on the chest", Hitbox.PART_BODY,
|
||||
Damage.zoneOf(1.0D, 0.0D, 1.8D));
|
||||
eq("a hit on the belly", Hitbox.PART_BODY,
|
||||
Damage.zoneOf(0.5D, 0.0D, 1.8D));
|
||||
eq("a hit on the legs", Hitbox.PART_LEGS,
|
||||
Damage.zoneOf(0.3D, 0.0D, 1.8D));
|
||||
eq("a hit below the feet", Hitbox.PART_LEGS,
|
||||
Damage.zoneOf(-1.0D, 0.0D, 1.8D));
|
||||
}
|
||||
|
||||
private static void testMultipliers() {
|
||||
near("the head", 2.0D, Damage.multiplier(Hitbox.PART_HEAD));
|
||||
near("the body", 1.0D, Damage.multiplier(Hitbox.PART_BODY));
|
||||
near("the legs", 0.75D, Damage.multiplier(Hitbox.PART_LEGS));
|
||||
}
|
||||
|
||||
private static void testProtection() {
|
||||
// a mask covers the head and takes away half of a bullet
|
||||
ItemType mask = ItemRegistry.require(ItemRegistry.METALFACEMASK);
|
||||
yes("the mask is a piece of clothing", mask.armorSlot == ItemType.ARMOR_HEAD);
|
||||
yes("the mask covers the head", Damage.covers(mask, Hitbox.PART_HEAD));
|
||||
yes("the mask does not cover the body", !Damage.covers(mask, Hitbox.PART_BODY));
|
||||
|
||||
Inventory inventory = new Inventory();
|
||||
inventory.setArmor(mask.armorSlot - 1, mask.armorLayer, ItemStack.of(mask.id, 1));
|
||||
near("a bullet in the head is half taken", 0.5D,
|
||||
Damage.protection(inventory.armor(), Hitbox.PART_HEAD,
|
||||
ItemType.HARM_PROJECTILE));
|
||||
near("a bullet in the body is not taken", 0.0D,
|
||||
Damage.protection(inventory.armor(), Hitbox.PART_BODY,
|
||||
ItemType.HARM_PROJECTILE));
|
||||
near("and a melee hit in the head is mostly taken", 0.7D,
|
||||
Damage.protection(inventory.armor(), Hitbox.PART_HEAD,
|
||||
ItemType.HARM_MELEE));
|
||||
}
|
||||
|
||||
private static void testSlots() {
|
||||
Inventory inventory = new Inventory();
|
||||
eq("seven places", Integer.valueOf(7),
|
||||
Integer.valueOf(inventory.armor().length));
|
||||
eq("two layers each", Integer.valueOf(2),
|
||||
Integer.valueOf(inventory.armor()[0].length));
|
||||
|
||||
ItemType helmet = ItemRegistry.require(ItemRegistry.BUCKETHELMET);
|
||||
ItemStack was = inventory.wear(helmet.id);
|
||||
yes("nothing was in the way", was == null);
|
||||
eq("the helmet is on the head", Integer.valueOf(helmet.id),
|
||||
Integer.valueOf(inventory.armor(0, 0).item));
|
||||
|
||||
ItemType jacket = ItemRegistry.require(ItemRegistry.METALCHEST);
|
||||
inventory.wear(jacket.id);
|
||||
eq("the chest armour is over the clothes", Integer.valueOf(jacket.id),
|
||||
Integer.valueOf(inventory.armor(2, 1).item));
|
||||
eq("and the clothes stay under", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.armor(2, 0).item));
|
||||
}
|
||||
|
||||
private static void testTable() {
|
||||
// the numbers of the clothes come from the table of the remake
|
||||
ItemType mask = ItemRegistry.require(ItemRegistry.METALFACEMASK);
|
||||
eq("the mask takes away fifty of a bullet", Integer.valueOf(50),
|
||||
Integer.valueOf(mask.protection[ItemType.HARM_PROJECTILE]));
|
||||
eq("and seventy of a blow", Integer.valueOf(70),
|
||||
Integer.valueOf(mask.protection[ItemType.HARM_MELEE]));
|
||||
|
||||
ItemType suit = ItemRegistry.require(ItemRegistry.HAZMATSUIT);
|
||||
eq("the hazmat suit is a whole body", Integer.valueOf(ItemType.ARMOR_FULL_BODY),
|
||||
Integer.valueOf(suit.armorSlot));
|
||||
yes("it covers everything", Damage.covers(suit, Hitbox.PART_HEAD)
|
||||
&& Damage.covers(suit, Hitbox.PART_BODY)
|
||||
&& Damage.covers(suit, Hitbox.PART_LEGS));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user