From a0f27fe3784e3ba696b3b26239b5257ac2eddc63 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Tue, 11 Mar 2025 18:57:19 -0700 Subject: [PATCH] Add graze --- client/danmaku!/Board.tscn | 9 ++ client/danmaku!/network_manager.gd | 9 +- client/danmaku!/player.gd | 20 +++-- client/danmaku!/player.tscn | 10 ++- client/test-graze.png | Bin 0 -> 978 bytes server/main.go | 133 ++++++++++++++++++----------- 6 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 client/test-graze.png diff --git a/client/danmaku!/Board.tscn b/client/danmaku!/Board.tscn index a34a461..5541662 100644 --- a/client/danmaku!/Board.tscn +++ b/client/danmaku!/Board.tscn @@ -41,3 +41,12 @@ script = ExtResource("4_ubrrh") unique_name_in_owner = true script = ExtResource("2_b2dol") player = NodePath("../Player") + +[node name="GrazeLabel" type="RichTextLabel" parent="."] +unique_name_in_owner = true +z_index = 5 +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +grow_horizontal = 2 +fit_content = true diff --git a/client/danmaku!/network_manager.gd b/client/danmaku!/network_manager.gd index 8221654..9bcbe4a 100644 --- a/client/danmaku!/network_manager.gd +++ b/client/danmaku!/network_manager.gd @@ -32,11 +32,13 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): if data["forcePlayerPos"]: player.set_position_data( Vector2( - float(data["playerPos"]["x"]), - float(data["playerPos"]["y"]) + float(data["playerHitPos"]["x"]), + float(data["playerHitPos"]["y"]) ), - float(data["playerPos"]["radius_multiplier"]) + float(data["playerHitPos"]["radius_multiplier"]), + float(data["playerGrazePos"]["radius_multiplier"]) ) + %GrazeLabel.text = "Graze: " + str(data["graze"]) # Handle player death if there is an ongoing death timer if int(data["deathTimer"]) > 0: @@ -61,6 +63,7 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): # Reimplemented from ScalableSprite2D here atm var scale_ratio = ((b["radius_multiplier"] * 2) * Globals.SERVER_SIZE.x) / bullet.texture.get_width() bullet.scale = Vector2(scale_ratio, scale_ratio) + bullet.z_index = 4 %BulletManager.add_child(bullet) %BulletManager.bullets.append(bullet) diff --git a/client/danmaku!/player.gd b/client/danmaku!/player.gd index fde37fb..16465c6 100644 --- a/client/danmaku!/player.gd +++ b/client/danmaku!/player.gd @@ -3,10 +3,10 @@ extends Node2D @export var speed = 80 var velocity := Vector2.ZERO -var collision: DanmakuCircle = DanmakuCircle.new() +var hurt_collision: DanmakuCircle = DanmakuCircle.new() +var graze_collision: DanmakuCircle = DanmakuCircle.new() -# This is temporary, it should be defined per-sprite when I get to the skin system -const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18 +const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18 # This is temporary, it should be defined per-sprite when I get to the skin system var alive: bool = true @@ -31,16 +31,22 @@ func _physics_process(delta: float): var attempted_position := position + (velocity * delta) attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE) - set_position_data(attempted_position, null) + set_position_data(attempted_position, null, null) -func set_position_data(pos: Vector2, hurtcircle_scale_multiplier): +func set_position_data(pos: Vector2, hurtcircle_scale_multiplier, grazecircle_scale_multiplier): position = pos - collision.set_position(pos.x, pos.y) + hurt_collision.set_position(pos.x, pos.y) + graze_collision.set_position(pos.x, pos.y) if hurtcircle_scale_multiplier: - collision.set_radius(Globals.SERVER_SIZE.x*hurtcircle_scale_multiplier) + hurt_collision.set_radius(Globals.SERVER_SIZE.x*hurtcircle_scale_multiplier) $HurtcircleSprite.scale_sprite(hurtcircle_scale_multiplier*2) + if grazecircle_scale_multiplier: + graze_collision.set_radius(Globals.SERVER_SIZE.x*grazecircle_scale_multiplier) + $GrazecircleSprite.scale_sprite(grazecircle_scale_multiplier*2) + + func kill(): if alive == false: return diff --git a/client/danmaku!/player.tscn b/client/danmaku!/player.tscn index 2017661..8b362ef 100644 --- a/client/danmaku!/player.tscn +++ b/client/danmaku!/player.tscn @@ -1,22 +1,30 @@ -[gd_scene load_steps=6 format=3 uid="uid://cd3tqt7hr5pqs"] +[gd_scene load_steps=7 format=3 uid="uid://cd3tqt7hr5pqs"] [ext_resource type="Script" uid="uid://bhwiun72wpk6e" path="res://danmaku!/player.gd" id="1_r7xhp"] [ext_resource type="Texture2D" uid="uid://bs3fntlmlqpt2" path="res://icon.svg" id="2_04s0l"] [ext_resource type="Texture2D" uid="uid://c3deywcu4du2b" path="res://test-collision.png" id="3_gf44i"] [ext_resource type="Script" uid="uid://v6jris184o8u" path="res://danmaku!/ScalableSprite2D.gd" id="3_u0x7w"] +[ext_resource type="Texture2D" uid="uid://brcxly3s7d1jt" path="res://test-graze.png" id="5_273wv"] [ext_resource type="AudioStream" uid="uid://c5n7x6q67tp78" path="res://test-death-noise.mp3" id="5_poktv"] [node name="Player" type="Node2D"] script = ExtResource("1_r7xhp") [node name="BodySprite" type="Sprite2D" parent="."] +z_index = 1 texture = ExtResource("2_04s0l") script = ExtResource("3_u0x7w") [node name="HurtcircleSprite" type="Sprite2D" parent="."] +z_index = 3 texture = ExtResource("3_gf44i") script = ExtResource("3_u0x7w") +[node name="GrazecircleSprite" type="Sprite2D" parent="."] +z_index = 2 +texture = ExtResource("5_273wv") +script = ExtResource("3_u0x7w") + [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] stream = ExtResource("5_poktv") volume_db = -20.0 diff --git a/client/test-graze.png b/client/test-graze.png new file mode 100644 index 0000000000000000000000000000000000000000..ba84253bb267e4e9258ac5e173df733e31ae8b24 GIT binary patch literal 978 zcmV;@117ab3qoD# zt=-h8aiv(-g5pke<;FMAQYZ+eo4U}2AZQg)zb_^sH_gn=WNzmslL>^GoXN?Y-^oel zU#8z{Cf$2^wfCgTscydQ4N!lpicUz~euz?u&I*n#+Q$L^(3bg>;e-c{dV7$)a>#>3 zbsa$cX=H9^0s1+}ii%2b%z3DMNB)a^D7V;DPjwwI185pDu>@z#;^aR<4VVg5gx(VJ z?Hc;yQl$;FB`5_)0D7L`7fnJW1hG_Ax)1P2(NH@Kfg?5`e(X5p6!|*f473*j|H0Wx zWI{OM5=`1h{x-@q-#K786?g(mTbD<{^Y|zSoIss5H2B=7v6$*eo7Z3y846R`5I6;` zi)iS5p>_fXFdQz!%w6PPE_2HwU_Ets0!xN<7d&N!5dg)gE^|cyzVf2WWLX6Gww-i6 zmjmY8*8A)s$uw{GmgxWvhdJbHcBK}T$wo#!^z{ye6MwL2%F&n)^aGR`0l`mHO&LDM z^JrG(fRkwa+!#*~dsAd3v%V8t!6ebh%ZwO2BU$i=ZW##!IT(Y!YhLjXIjcW*vz zBwI%@t2rQ9kh+E|2mGnqP=fk`SvlZmf&g_*7RUJO|BEQ;b`NRn1w z#+JH=06zf3mYM@jrw$ZgzhG7lP}_iZl2sARIf4)MnD{i7>q`;L1Nyc2jUl&WPyoDO zRv$nCKf21*S9+E2j&@*5Ufn#e{*oy-W(LhiKf(#T8R5TGoWbAao3>t%%w zfX6o5LSM$>3=0XK{Zd)3t=)n5?xY$C()JzL?_4KLuCfR)xo=@oOlrkA;8y|g!h~?` z`#K=|A13(c#WuXU;qqckGHK&BY#RBu2<`TbfFq2t-6sG%h$To#dSeOOD{#cqBi?CH z0Pwpw-87ps%K88uq zA}oZrYXRpTnvCY>X`8jET=}~L;0I{z*ngk@2O&AHAEC3aJpcdz07*qoM6N<$g1{EQ ARsaA1 literal 0 HcmV?d00001 diff --git a/server/main.go b/server/main.go index 8c61d88..5ded290 100644 --- a/server/main.go +++ b/server/main.go @@ -13,7 +13,6 @@ import ( "database/sql" "encoding/json" "math" - "math/rand" "slices" "github.com/heroiclabs/nakama-common/runtime" @@ -32,22 +31,26 @@ const ( ) const ( - STAGE_WIDTH float64 = 90.0 - STAGE_HEIGHT float64 = 160.0 - BULLET_KILL_BUFFER_WIDTH float64 = 16.0 - PLAYER_COL_RADIUS_MULTIPLIER float64 = 0.01 - PLAYER_DEATH_TIMER_MAX int = 180 + STAGE_WIDTH float64 = 90.0 + STAGE_HEIGHT float64 = 160.0 + BULLET_KILL_BUFFER_WIDTH float64 = 16.0 + PLAYER_HIT_COL_RADIUS_MULTIPLIER float64 = 0.01 + PLAYER_GRAZE_COL_RADIUS_MULTIPLIER float64 = 0.04 + PLAYER_DEATH_TIMER_MAX int = 180 + GRAZE_ADDITION_MULTIPLIER int = 1000 ) // Interface for registering match handlers type BattleRoyaleMatch struct{} type PlayerStageState struct { - col *C.Circle + hitCol *C.Circle + grazeCol *C.Circle bullets []*C.Bullet updatePlayerPos bool health int graze int + score int deathTimer int } @@ -58,10 +61,12 @@ type PlayerUpdate struct { type GameTickUpdate struct { Tick int64 `json:"tick"` - PlayerPos map[string]interface{} `json:"playerPos"` + PlayerHitPos map[string]interface{} `json:"playerHitPos"` + PlayerGrazePos map[string]interface{} `json:"playerGrazePos"` NewBullets []map[string]interface{} `json:"newBullets"` ForcePlayerPos bool `json:"forcePlayerPos"` DeathTimer int `json:"deathTimer"` + Graze int `json:"graze"` } type PresenceState struct { // present time! hahahahahahahah! @@ -115,7 +120,8 @@ func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{ presence: presences[i], stageState: PlayerStageState{ - col: C.new_circle(C.double(STAGE_WIDTH*0.5), C.double(STAGE_HEIGHT-STAGE_HEIGHT*0.1), C.double(STAGE_WIDTH*PLAYER_COL_RADIUS_MULTIPLIER)), + hitCol: C.new_circle(C.double(STAGE_WIDTH*0.5), C.double(STAGE_HEIGHT-STAGE_HEIGHT*0.1), C.double(STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER)), + grazeCol: C.new_circle(C.double(STAGE_WIDTH*0.5), C.double(STAGE_HEIGHT-STAGE_HEIGHT*0.1), C.double(STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER)), bullets: []*C.Bullet{}, updatePlayerPos: true, health: 3, @@ -143,7 +149,8 @@ func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logge for _, bullet := range playerState.stageState.bullets { C.destroy_bullet(bullet) } - C.destroy_circle(playerState.stageState.col) + C.destroy_circle(playerState.stageState.hitCol) + C.destroy_circle(playerState.stageState.grazeCol) delete(lobbyState.presences, sessionID) } } @@ -215,8 +222,11 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger update.X = math.Max(0, math.Min(update.X, STAGE_WIDTH)) update.Y = math.Max(0, math.Min(update.Y, STAGE_HEIGHT)) - lobbyState.presences[msg.GetSessionId()].stageState.col.x = C.double(update.X) - lobbyState.presences[msg.GetSessionId()].stageState.col.y = C.double(update.Y) + lobbyState.presences[msg.GetSessionId()].stageState.hitCol.x = C.double(update.X) + lobbyState.presences[msg.GetSessionId()].stageState.hitCol.y = C.double(update.Y) + + lobbyState.presences[msg.GetSessionId()].stageState.grazeCol.x = C.double(update.X) + lobbyState.presences[msg.GetSessionId()].stageState.grazeCol.y = C.double(update.Y) if clampedX || clampedY { lobbyState.presences[msg.GetSessionId()].stageState.updatePlayerPos = true @@ -240,65 +250,90 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger } if v.stageState.deathTimer == 0 { // If the player's death timer has run out, reset them. 0 is a special deathTimer tick that indicates reset to the clients. - v.stageState.col.x = C.double(STAGE_WIDTH * 0.5) - v.stageState.col.y = C.double(STAGE_HEIGHT - STAGE_HEIGHT*0.1) + v.stageState.hitCol.x = C.double(STAGE_WIDTH * 0.5) + v.stageState.hitCol.y = C.double(STAGE_HEIGHT - STAGE_HEIGHT*0.1) + + v.stageState.grazeCol.x = C.double(STAGE_WIDTH * 0.5) + v.stageState.grazeCol.y = C.double(STAGE_HEIGHT - STAGE_HEIGHT*0.1) + v.stageState.updatePlayerPos = true - } else { // If the player is alive, check if the player collided with a bullet and kill them if so + } else if v.stageState.deathTimer == -1 { // If the player is alive, check if the player collided with a bullet and kill them if so if slices.ContainsFunc(v.stageState.bullets, func(b *C.Bullet) bool { - return bool(C.bullet_collides_with(b, C.int64_t(tick), v.stageState.col)) + return bool(C.bullet_collides_with(b, C.int64_t(tick), v.stageState.hitCol)) }) { v.stageState.deathTimer = PLAYER_DEATH_TIMER_MAX + } else if slices.ContainsFunc(v.stageState.bullets, func(b *C.Bullet) bool { // Otherwise, check the graze col and increment the graze and score + return bool(C.bullet_collides_with(b, C.int64_t(tick), v.stageState.grazeCol)) + }) { + v.stageState.graze += GRAZE_ADDITION_MULTIPLIER } } var newBulletsToBroadcast = []map[string]interface{}{} - // Test bullet spawning, only when player is alive - if tick%10 == 0 && v.stageState.deathTimer == -1 { - velx := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate) - vely := (rand.Float64() * STAGE_WIDTH) / float64(lobbyState.tickRate) - radius_multiplier := 0.01 + rand.Float64()*(0.1-0.01) - vel_x_sign := 2*rand.Intn(2) - 1 - vel_y_sign := 2*rand.Intn(2) - 1 + if tick%30 == 0 && v.stageState.deathTimer == -1 { + numBullets := 20 + spreadAngle := 60.0 // Spread in degrees + startAngle := 90 - (spreadAngle / 2) + bulletSpeed := STAGE_WIDTH / float64(lobbyState.tickRate) / 3 + bulletRadiusMult := 0.03 - bullet := C.new_bullet( - C.uint8_t(BULLET_LINEAR), - C.int64_t(tick), - C.double(STAGE_WIDTH*rand.Float64()), - C.double(STAGE_HEIGHT*rand.Float64()), - C.double(radius_multiplier*STAGE_WIDTH), - C.double(float64(vel_x_sign)*velx), - C.double(float64(vel_y_sign)*vely), - ) + // Define a single spawn point near the top of the screen + spawnX := STAGE_WIDTH / 2 // Centered horizontally + spawnY := STAGE_HEIGHT * 0.1 // 10% from the top - v.stageState.bullets = append(v.stageState.bullets, bullet) + for i := 0; i < numBullets; i++ { + angle := startAngle + (spreadAngle/float64(numBullets-1))*float64(i) + angleRad := angle * (math.Pi / 180.0) - var x, y C.double - C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y) + velx := bulletSpeed * math.Cos(angleRad) + vely := bulletSpeed * math.Sin(angleRad) - bulletData := map[string]interface{}{ - "class": BULLET_LINEAR, - "tick": tick, - "x": float64(x), - "y": float64(y), - "radius_multiplier": float64(radius_multiplier), - "vel_x": float64(vel_x_sign) * velx, - "vel_y": float64(vel_y_sign) * vely, + bullet := C.new_bullet( + C.uint8_t(BULLET_LINEAR), + C.int64_t(tick), + C.double(spawnX), // Fixed X start position + C.double(spawnY), // Fixed Y start position + C.double(bulletRadiusMult*STAGE_WIDTH), // Fixed radius + C.double(velx), + C.double(vely), + ) + + v.stageState.bullets = append(v.stageState.bullets, bullet) + + var x, y C.double + C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y) + + bulletData := map[string]interface{}{ + "class": BULLET_LINEAR, + "tick": tick, + "x": float64(x), + "y": float64(y), + "radius_multiplier": bulletRadiusMult, + "vel_x": velx, + "vel_y": vely, + } + + newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData) } - - newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData) } var tickData = GameTickUpdate{ Tick: tick, - PlayerPos: map[string]interface{}{ - "x": v.stageState.col.x, - "y": v.stageState.col.y, - "radius_multiplier": PLAYER_COL_RADIUS_MULTIPLIER, + PlayerHitPos: map[string]interface{}{ + "x": v.stageState.hitCol.x, + "y": v.stageState.hitCol.y, + "radius_multiplier": PLAYER_HIT_COL_RADIUS_MULTIPLIER, + }, + PlayerGrazePos: map[string]interface{}{ + "x": v.stageState.grazeCol.x, + "y": v.stageState.grazeCol.y, + "radius_multiplier": PLAYER_GRAZE_COL_RADIUS_MULTIPLIER, }, NewBullets: newBulletsToBroadcast, ForcePlayerPos: v.stageState.updatePlayerPos, DeathTimer: v.stageState.deathTimer, + Graze: v.stageState.graze, } v.stageState.updatePlayerPos = false