Skip to content

Commit

Permalink
feat: Implement fall damage and rework damage system (#342)
Browse files Browse the repository at this point in the history
The vertical distance travelled while not on the ground (as reported by the
client) is now measured, and is used to damage the player when "on ground"
becomes true again. This causes a sound effect to be played to all players.
If the player's health goes to zero, the appropriate death message is selected
based on the `minecraft:damage_type` registry.

Primitive health regeneration is also added such that there is a way to recover
from this damage besides dying.
  • Loading branch information
meyfa authored Feb 16, 2025
1 parent 69f90e5 commit fc07c9f
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 23 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The following features are already working:
- [X] configuration via server.properties
- [X] whitelist (persistent; stored in whitelist.json)
- [X] extremely basic block/player collision and entity physics
- [X] fall damage, death, and respawning

Note that blocks with multiple states, orientations, or interactive blocks require large amounts of specialized code
to make them behave properly, which is way beyond the scope of this project.
Expand Down
24 changes: 5 additions & 19 deletions src/commands/kill.cob
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ PROCEDURE DIVISION.
COPY DD-CLIENTS.
COPY DD-PLAYERS.
COPY DD-SERVER-PROPERTIES.
01 ENTITY-EVENT-DEATH BINARY-CHAR UNSIGNED VALUE 3.
*> as close to infinity as we can get
01 DAMAGE-AMOUNT FLOAT-SHORT VALUE 3.40282346638528859811704183484516925E+38.
01 DAMAGE-TYPE BINARY-LONG.
01 BUFFER PIC X(255).
01 PLAYER-ID BINARY-LONG UNSIGNED.
01 BYTE-COUNT BINARY-LONG UNSIGNED.
01 OTHER-PLAYER-ID BINARY-LONG UNSIGNED.
LINKAGE SECTION.
COPY DD-CALLBACK-COMMAND-EXECUTE.

Expand All @@ -63,22 +63,8 @@ PROCEDURE DIVISION.
END-IF
END-IF

MOVE 0 TO PLAYER-HEALTH(PLAYER-ID)
CALL "SendPacket-SetHealth" USING PLAYER-CLIENT(PLAYER-ID) PLAYER-HEALTH(PLAYER-ID) PLAYER-FOOD-LEVEL(PLAYER-ID) PLAYER-SATURATION(PLAYER-ID)

*> Play the death sound and animation to all players (including the dying player).
*> For this to have any effect, the player must have 0 health. For the dying player, this is already the case.
*> For all others, it will be handled by "set entity metadata" in the main loop.
PERFORM VARYING OTHER-PLAYER-ID FROM 1 BY 1 UNTIL OTHER-PLAYER-ID > MAX-PLAYERS
IF PLAYER-CLIENT(OTHER-PLAYER-ID) NOT = 0
CALL "SendPacket-EntityEvent" USING PLAYER-CLIENT(OTHER-PLAYER-ID) PLAYER-ID ENTITY-EVENT-DEATH
END-IF
END-PERFORM

INITIALIZE BUFFER
STRING FUNCTION TRIM(PLAYER-NAME(PLAYER-ID)) " was killed" INTO BUFFER
MOVE FUNCTION STORED-CHAR-LENGTH(BUFFER) TO BYTE-COUNT
CALL "BroadcastChatMessage" USING BUFFER BYTE-COUNT OMITTED
CALL "Registries-Get-EntryId" USING "minecraft:damage_type" "minecraft:generic_kill" DAMAGE-TYPE
CALL "Players-Damage" USING PLAYER-ID DAMAGE-AMOUNT DAMAGE-TYPE

INITIALIZE BUFFER
STRING "Killed " FUNCTION TRIM(PLAYER-NAME(PLAYER-ID)) INTO BUFFER
Expand Down
3 changes: 3 additions & 0 deletions src/copybooks/DD-PLAYERS.cpy
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
03 PLAYER-CLIENT BINARY-LONG UNSIGNED.
03 PLAYER-UUID PIC X(16).
03 PLAYER-NAME PIC X(16).
*> Amount of time (ticks) the player has been in the world, resets when the player leaves
03 PLAYER-WORLD-TIME BINARY-LONG-LONG.
*> Survival: 0, Creative: 1, Adventure: 2, Spectator: 3
03 PLAYER-GAMEMODE BINARY-CHAR UNSIGNED.
03 PLAYER-POSITION.
Expand All @@ -25,6 +27,7 @@
04 PLAYER-VELOCITY-Z FLOAT-LONG.
03 PLAYER-ON-GROUND BINARY-CHAR UNSIGNED.
03 PLAYER-AGAINST-WALL BINARY-CHAR UNSIGNED.
03 PLAYER-FALL-DISTANCE FLOAT-SHORT.
03 PLAYER-HEALTH FLOAT-SHORT.
03 PLAYER-FOOD-LEVEL BINARY-LONG UNSIGNED.
03 PLAYER-SATURATION FLOAT-SHORT.
Expand Down
50 changes: 50 additions & 0 deletions src/packets/clientbound/play/entity-sound.cob
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. SendPacket-EntitySound.

DATA DIVISION.
WORKING-STORAGE SECTION.
COPY DD-PACKET REPLACING IDENTIFIER BY "play/clientbound/minecraft:sound_entity".
*> buffer used to store the packet data
01 PAYLOAD PIC X(1024).
01 PAYLOADPOS BINARY-LONG UNSIGNED.
01 PAYLOADLEN BINARY-LONG UNSIGNED.
*> temporary variables
01 TEMP-INT32 BINARY-LONG.
01 TEMP-FLOAT FLOAT-SHORT.
01 SEED BINARY-LONG-LONG.
LINKAGE SECTION.
01 LK-CLIENT BINARY-LONG UNSIGNED.
01 LK-ENTITY-ID BINARY-LONG UNSIGNED.
01 LK-SOUND-ID BINARY-LONG UNSIGNED.
01 LK-VOLUME FLOAT-SHORT.

PROCEDURE DIVISION USING LK-CLIENT LK-ENTITY-ID LK-SOUND-ID LK-VOLUME.
COPY PROC-PACKET-INIT.

MOVE 1 TO PAYLOADPOS

COMPUTE TEMP-INT32 = LK-SOUND-ID + 1
CALL "Encode-VarInt" USING TEMP-INT32 PAYLOAD PAYLOADPOS

*> category: https://gist.github.com/konwboj/7c0c380d3923443e9d55
*> 7 = player
*> TODO Can't we get this somewhere else?!
MOVE 7 TO TEMP-INT32
CALL "Encode-VarInt" USING TEMP-INT32 PAYLOAD PAYLOADPOS

CALL "Encode-VarInt" USING LK-ENTITY-ID PAYLOAD PAYLOADPOS

CALL "Encode-Float" USING LK-VOLUME PAYLOAD PAYLOADPOS

*> TODO pitch
MOVE 1.0 TO TEMP-FLOAT
CALL "Encode-Float" USING TEMP-FLOAT PAYLOAD PAYLOADPOS

*> TODO How can we generate a proper seed?
CALL "Encode-Long" USING SEED PAYLOAD PAYLOADPOS

COMPUTE PAYLOADLEN = PAYLOADPOS - 1
CALL "SendPacket" USING LK-CLIENT PACKET-ID PAYLOAD PAYLOADLEN
GOBACK.

END PROGRAM SendPacket-EntitySound.
1 change: 1 addition & 0 deletions src/packets/serverbound/play/client-command.cob
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ PROCEDURE DIVISION USING LK-CLIENT LK-BUFFER LK-OFFSET.
INITIALIZE PLAYER-VELOCITY(PLAYER-ID)
MOVE 1 TO PLAYER-ON-GROUND(PLAYER-ID)
MOVE 0 TO PLAYER-AGAINST-WALL(PLAYER-ID)
MOVE 0 TO PLAYER-FALL-DISTANCE(PLAYER-ID)
MOVE 0 TO PLAYER-SNEAKING(PLAYER-ID)
MOVE 0 TO PLAYER-FLYING(PLAYER-ID)
MOVE 0 TO PLAYER-HOTBAR(PLAYER-ID)
Expand Down
222 changes: 218 additions & 4 deletions src/players.cob
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ PROCEDURE DIVISION USING LK-PLAYER-ID LK-FAILURE.
MOVE 8 TO NAME-LEN
CALL "NbtEncode-Byte" USING NBT-ENCODER-STATE NBT-BUFFER TAG-NAME NAME-LEN PLAYER-ON-GROUND(LK-PLAYER-ID)

*> fall distance ("FallDistance")
MOVE "FallDistance" TO TAG-NAME
MOVE 12 TO NAME-LEN
CALL "NbtEncode-Float" USING NBT-ENCODER-STATE NBT-BUFFER TAG-NAME NAME-LEN PLAYER-FALL-DISTANCE(LK-PLAYER-ID)

*> health ("Health")
MOVE "Health" TO TAG-NAME
MOVE 6 TO NAME-LEN
Expand Down Expand Up @@ -316,6 +321,9 @@ PROCEDURE DIVISION USING LK-PLAYER-ID LK-PLAYER-UUID LK-FAILURE.
WHEN "OnGround"
CALL "NbtDecode-Byte" USING NBT-DECODER-STATE NBT-BUFFER PLAYER-ON-GROUND(LK-PLAYER-ID)

WHEN "FallDistance"
CALL "NbtDecode-Float" USING NBT-DECODER-STATE NBT-BUFFER PLAYER-FALL-DISTANCE(LK-PLAYER-ID)

WHEN "Health"
CALL "NbtDecode-Float" USING NBT-DECODER-STATE NBT-BUFFER PLAYER-HEALTH(LK-PLAYER-ID)

Expand Down Expand Up @@ -599,6 +607,9 @@ PROGRAM-ID. Players-HandleMove.
DATA DIVISION.
WORKING-STORAGE SECTION.
COPY DD-PLAYERS.
78 FALL-DAMAGE-THRESHOLD VALUE 3.0.
01 FALL-DAMAGE-AMOUNT FLOAT-SHORT.
01 FALL-DAMAGE-TYPE BINARY-LONG.
LINKAGE SECTION.
01 LK-PLAYER BINARY-LONG UNSIGNED.
01 LK-POSITION.
Expand All @@ -613,19 +624,222 @@ LINKAGE SECTION.
02 LK-AGAINST-WALL BINARY-CHAR UNSIGNED.

PROCEDURE DIVISION USING LK-PLAYER OPTIONAL LK-POSITION OPTIONAL LK-ROTATION OPTIONAL LK-FLAGS.
IF LK-FLAGS IS NOT OMITTED
IF PLAYER-FLYING(LK-PLAYER) = 0
EVALUATE TRUE
WHEN PLAYER-ON-GROUND(LK-PLAYER) NOT = 0 AND LK-ON-GROUND = 0
PERFORM HandleLeaveGround
WHEN PLAYER-ON-GROUND(LK-PLAYER) = 0 AND LK-ON-GROUND NOT = 0
PERFORM HandleHitGround
END-EVALUATE
ELSE
MOVE 0 TO PLAYER-FALL-DISTANCE(LK-PLAYER)
END-IF

MOVE LK-ON-GROUND TO PLAYER-ON-GROUND(LK-PLAYER)
MOVE LK-AGAINST-WALL TO PLAYER-AGAINST-WALL(LK-PLAYER)
END-IF

IF LK-POSITION IS NOT OMITTED
IF PLAYER-ON-GROUND(LK-PLAYER) = 0
COMPUTE PLAYER-FALL-DISTANCE(LK-PLAYER) = FUNCTION MAX(0, PLAYER-FALL-DISTANCE(LK-PLAYER) + PLAYER-Y(LK-PLAYER) - LK-POSITION-Y)
END-IF

MOVE LK-POSITION TO PLAYER-POSITION(LK-PLAYER)
END-IF

IF LK-ROTATION IS NOT OMITTED
MOVE LK-ROTATION TO PLAYER-ROTATION(LK-PLAYER)
END-IF

IF LK-FLAGS IS NOT OMITTED
MOVE LK-ON-GROUND TO PLAYER-ON-GROUND(LK-PLAYER)
MOVE LK-AGAINST-WALL TO PLAYER-AGAINST-WALL(LK-PLAYER)
GOBACK.

HandleLeaveGround.
MOVE 0 TO PLAYER-FALL-DISTANCE(LK-PLAYER)
.

HandleHitGround.
COMPUTE FALL-DAMAGE-AMOUNT = PLAYER-FALL-DISTANCE(LK-PLAYER) - FALL-DAMAGE-THRESHOLD

*> TODO avoid hardcoding the invulnerability period here
IF FALL-DAMAGE-AMOUNT <= 0 OR PLAYER-WORLD-TIME(LK-PLAYER) < 20
MOVE 0 TO PLAYER-FALL-DISTANCE(LK-PLAYER)
EXIT PARAGRAPH
END-IF

GOBACK.
CALL "Registries-Get-EntryId" USING "minecraft:damage_type" "minecraft:fall" FALL-DAMAGE-TYPE
CALL "Players-Damage" USING LK-PLAYER FALL-DAMAGE-AMOUNT FALL-DAMAGE-TYPE

MOVE 0 TO PLAYER-FALL-DISTANCE(LK-PLAYER)
.

END PROGRAM Players-HandleMove.

*> --- Players-Damage ---
*> Inflict damage on a player, taking into account the player's gamemode and the type of damage
*> from the "minecraft:damage_type" registry.
IDENTIFICATION DIVISION.
PROGRAM-ID. Players-Damage.

DATA DIVISION.
WORKING-STORAGE SECTION.
COPY DD-PLAYERS.
COPY DD-CLIENTS.
COPY DD-CLIENT-STATES.
COPY DD-SERVER-PROPERTIES.
01 ENTITY-EVENT-DEATH BINARY-CHAR UNSIGNED VALUE 3.
01 OTHER-CLIENT-ID BINARY-LONG UNSIGNED.
01 BUFFER PIC X(255).
01 BYTE-COUNT BINARY-LONG UNSIGNED.
01 SOUND-ID BINARY-LONG.
01 SOUND-VOLUME FLOAT-SHORT.
*> Whether the following data was initialized
01 DATA-INIT BINARY-CHAR UNSIGNED VALUE 0.
*> IDs of damage types for discerning the death message
01 TYPE-FALL BINARY-LONG UNSIGNED.
01 TYPE-GENERIC_KILL BINARY-LONG UNSIGNED.
01 TYPE-OUT_OF_WORLD BINARY-LONG UNSIGNED.
*> IDs of damage sounds
01 SOUND-HURT BINARY-LONG UNSIGNED.
01 SOUND-SMALL_FALL BINARY-LONG UNSIGNED.
01 SOUND-BIG_FALL BINARY-LONG UNSIGNED.
LINKAGE SECTION.
01 LK-PLAYER BINARY-LONG UNSIGNED.
01 LK-AMOUNT FLOAT-SHORT.
01 LK-DAMAGE-TYPE BINARY-LONG UNSIGNED.

PROCEDURE DIVISION USING LK-PLAYER LK-AMOUNT LK-DAMAGE-TYPE.
IF DATA-INIT = 0
PERFORM InitData
MOVE 1 TO DATA-INIT
END-IF

*> Creative mode players are immune to all damage except the "/kill" command and void damage.
IF PLAYER-GAMEMODE(LK-PLAYER) = 1 AND NOT (LK-DAMAGE-TYPE = TYPE-GENERIC_KILL OR TYPE-OUT_OF_WORLD)
GOBACK
END-IF

*> Ignore already dead players.
IF PLAYER-HEALTH(LK-PLAYER) <= 0
GOBACK
END-IF

COMPUTE PLAYER-HEALTH(LK-PLAYER) = FUNCTION MAX(0, PLAYER-HEALTH(LK-PLAYER) - LK-AMOUNT)

CALL "SendPacket-SetHealth" USING PLAYER-CLIENT(LK-PLAYER) PLAYER-HEALTH(LK-PLAYER)
PLAYER-FOOD-LEVEL(LK-PLAYER) PLAYER-SATURATION(LK-PLAYER)

IF PLAYER-HEALTH(LK-PLAYER) > 0
*> The player is not dead, so the sound won't be played via the entity event packet.
PERFORM PickDamageSound
PERFORM VARYING OTHER-CLIENT-ID FROM 1 BY 1 UNTIL OTHER-CLIENT-ID > MAX-CLIENTS
IF CLIENT-PRESENT(OTHER-CLIENT-ID) NOT = 0 AND CLIENT-STATE(OTHER-CLIENT-ID) = CLIENT-STATE-PLAY
CALL "SendPacket-EntitySound" USING OTHER-CLIENT-ID LK-PLAYER SOUND-ID SOUND-VOLUME
END-IF
END-PERFORM

GOBACK
END-IF

*> Play the death sound and animation to all players (including the dying player).
*> For this to have any effect, the player must have 0 health. For the dying player, this is already the case.
*> For all others, it will be handled by "set entity metadata" in the main loop.
PERFORM VARYING OTHER-CLIENT-ID FROM 1 BY 1 UNTIL OTHER-CLIENT-ID > MAX-CLIENTS
IF CLIENT-PRESENT(OTHER-CLIENT-ID) NOT = 0 AND CLIENT-STATE(OTHER-CLIENT-ID) = CLIENT-STATE-PLAY
CALL "SendPacket-EntityEvent" USING OTHER-CLIENT-ID LK-PLAYER ENTITY-EVENT-DEATH
END-IF
END-PERFORM

PERFORM BuildDeathMessage

MOVE FUNCTION STORED-CHAR-LENGTH(BUFFER) TO BYTE-COUNT
CALL "BroadcastChatMessage" USING BUFFER BYTE-COUNT OMITTED

GOBACK.

BuildDeathMessage.
INITIALIZE BUFFER

EVALUATE LK-DAMAGE-TYPE
WHEN TYPE-FALL
STRING FUNCTION TRIM(PLAYER-NAME(LK-PLAYER)) " fell from a high place" INTO BUFFER
WHEN TYPE-GENERIC_KILL
STRING FUNCTION TRIM(PLAYER-NAME(LK-PLAYER)) " was killed" INTO BUFFER
WHEN TYPE-OUT_OF_WORLD
STRING FUNCTION TRIM(PLAYER-NAME(LK-PLAYER)) " fell out of the world" INTO BUFFER
WHEN OTHER
STRING FUNCTION TRIM(PLAYER-NAME(LK-PLAYER)) " died" INTO BUFFER
END-EVALUATE
.

PickDamageSound.
MOVE SOUND-HURT TO SOUND-ID
MOVE 1.0 TO SOUND-VOLUME

EVALUATE LK-DAMAGE-TYPE
WHEN TYPE-FALL
IF LK-AMOUNT <= 4.0
MOVE SOUND-SMALL_FALL TO SOUND-ID
ELSE
MOVE SOUND-BIG_FALL TO SOUND-ID
END-IF
END-EVALUATE
.

InitData.
CALL "Registries-Get-EntryId" USING "minecraft:damage_type" "minecraft:fall" TYPE-FALL
CALL "Registries-Get-EntryId" USING "minecraft:damage_type" "minecraft:generic_kill" TYPE-GENERIC_KILL
CALL "Registries-Get-EntryId" USING "minecraft:damage_type" "minecraft:out_of_world" TYPE-OUT_OF_WORLD

CALL "Registries-Get-EntryId" USING "minecraft:sound_event" "minecraft:entity.player.hurt" SOUND-HURT
CALL "Registries-Get-EntryId" USING "minecraft:sound_event" "minecraft:entity.player.small_fall" SOUND-SMALL_FALL
CALL "Registries-Get-EntryId" USING "minecraft:sound_event" "minecraft:entity.player.big_fall" SOUND-BIG_FALL
.

END PROGRAM Players-Damage.

*> --- Players-Tick ---
*> Called every tick to update player state.
IDENTIFICATION DIVISION.
PROGRAM-ID. Players-Tick.

DATA DIVISION.
WORKING-STORAGE SECTION.
COPY DD-PLAYERS.
COPY DD-CLIENTS.
COPY DD-CLIENT-STATES.
COPY DD-SERVER-PROPERTIES.
01 TIMER BINARY-LONG-LONG UNSIGNED VALUE 0.
01 CLIENT-ID BINARY-LONG UNSIGNED.
01 PLAYER-ID BINARY-LONG UNSIGNED.

PROCEDURE DIVISION.
*> TODO Replace this timer with per-player logic
ADD 1 TO TIMER

*> Only tick players in the play state, as ticking may send packets that are not valid in other states.
PERFORM VARYING CLIENT-ID FROM 1 BY 1 UNTIL CLIENT-ID > MAX-CLIENTS
IF CLIENT-PRESENT(CLIENT-ID) NOT = 0 AND CLIENT-STATE(CLIENT-ID) = CLIENT-STATE-PLAY
MOVE CLIENT-PLAYER(CLIENT-ID) TO PLAYER-ID
PERFORM TickPlayer
END-IF
END-PERFORM

GOBACK.

TickPlayer.
ADD 1 TO PLAYER-WORLD-TIME(PLAYER-ID)

*> If the player is dead, do not update their health.
IF PLAYER-HEALTH(PLAYER-ID) <= 0
EXIT PARAGRAPH
END-IF

*> TODO Implement a proper health system. For now, we simply heal 1/2 heart per second on a global timer.
IF FUNCTION MOD(TIMER, 20) = 0
COMPUTE PLAYER-HEALTH(PLAYER-ID) = FUNCTION MIN(20, PLAYER-HEALTH(PLAYER-ID) + 1)
CALL "SendPacket-SetHealth" USING CLIENT-ID PLAYER-HEALTH(PLAYER-ID) PLAYER-FOOD-LEVEL(PLAYER-ID) PLAYER-SATURATION(PLAYER-ID)
END-IF
.

END PROGRAM Players-Tick.
1 change: 1 addition & 0 deletions src/server.cob
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ ServerLoop.

GameLoop.
CALL "World-Tick"
CALL "Players-Tick"
.

ConsoleInput.
Expand Down

0 comments on commit fc07c9f

Please sign in to comment.