Compare commits

..

9 Commits

29 changed files with 601 additions and 192 deletions

BIN
assets/coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

35
assets/coin.png.import Normal file
View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgo2frvws2o6f"
path.s3tc="res://.godot/imported/coin.png-f04b9cd408b88aba3ab0966b4da32df0.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/coin.png"
dest_files=["res://.godot/imported/coin.png-f04b9cd408b88aba3ab0966b4da32df0.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View File

@ -3,7 +3,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dw3x3h3f34sy3"
path.s3tc="res://.godot/imported/coin_flower.png-f7e515b96b6729484a68167e84f6e510.s3tc.ctex"
path.bptc="res://.godot/imported/coin_flower.png-f7e515b96b6729484a68167e84f6e510.bptc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
@ -12,12 +12,12 @@ metadata={
[deps]
source_file="res://assets/coin_flower.png"
dest_files=["res://.godot/imported/coin_flower.png-f7e515b96b6729484a68167e84f6e510.s3tc.ctex"]
dest_files=["res://.godot/imported/coin_flower.png-f7e515b96b6729484a68167e84f6e510.bptc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/high_quality=true
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0

2
assets/sfx/LICENSES Normal file
View File

@ -0,0 +1,2 @@
land.wav - https://opengameart.org/content/3-item-sounds - CC-BY 3.0
coinsplash.ogg - https://opengameart.org/content/coin-splash - No Rights Reserved

BIN
assets/sfx/coinsplash.ogg Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dkxv7wbq8s1gs"
path="res://.godot/imported/coinsplash.ogg-f4cc7a136a244c66d0c3ed8863b3a3e6.oggvorbisstr"
[deps]
source_file="res://assets/sfx/coinsplash.ogg"
dest_files=["res://.godot/imported/coinsplash.ogg-f4cc7a136a244c66d0c3ed8863b3a3e6.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

BIN
assets/sfx/land.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dy3ngeohp1uqf"
path="res://.godot/imported/land.wav-6f339f0cb8abb44d119c317df4f4b636.sample"
[deps]
source_file="res://assets/sfx/land.wav"
dest_files=["res://.godot/imported/land.wav-6f339f0cb8abb44d119c317df4f4b636.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0

13
data/coin.tres Normal file
View File

@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="InventoryItem" load_steps=3 format=3 uid="uid://dbxrlw5ggh67j"]
[ext_resource type="Texture2D" uid="uid://cb6qv3c0iojfl" path="res://icon.svg" id="1_eakc4"]
[ext_resource type="Script" path="res://src/lib/inventory_item.gd" id="2_lrh23"]
[resource]
script = ExtResource("2_lrh23")
icon = ExtResource("1_eakc4")
name = "Coin"
id = &"coin"
stackable = true
stack_limit = 100
sells_for = 1

View File

@ -9,4 +9,5 @@ icon = ExtResource("1_vrj0d")
name = "Coin Flower"
id = &"coin_flower"
stackable = true
stack_limit = 8
stack_limit = 9
sells_for = 2

View File

@ -1,6 +1,7 @@
[gd_resource type="Resource" script_class="InventoryItem" load_steps=4 format=3 uid="uid://cmeif37pci2ek"]
[gd_resource type="Resource" script_class="InventoryItem" load_steps=5 format=3 uid="uid://cmeif37pci2ek"]
[ext_resource type="Texture2D" uid="uid://cb6qv3c0iojfl" path="res://icon.svg" id="1_g8c3q"]
[ext_resource type="PackedScene" uid="uid://tdsbo3e5ic86" path="res://src/ingame/water_bomb.tscn" id="1_t62t2"]
[ext_resource type="PackedScene" uid="uid://ba2mut58elwrh" path="res://assets/water-bomb.glb" id="2_5cxkh"]
[ext_resource type="Script" path="res://src/lib/inventory_item.gd" id="2_pe2p8"]
@ -8,6 +9,9 @@
script = ExtResource("2_pe2p8")
icon = ExtResource("1_g8c3q")
model = ExtResource("2_5cxkh")
bomb = ExtResource("1_t62t2")
name = "Water Bomb"
id = &"water_bomb"
count = 0
stackable = false
stack_limit = 8
sells_for = 0

View File

@ -5,11 +5,11 @@ bus/1/name = &"Music"
bus/1/solo = false
bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = 0.0
bus/1/volume_db = -4.25003
bus/1/send = &"Master"
bus/2/name = &"SoundEffects"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = -4.00569
bus/2/volume_db = -10.0692
bus/2/send = &"Master"

View File

@ -28,6 +28,7 @@ window/stretch/aspect="expand"
spawn_points=""
voids=""
sell_boxes=""
[input]

158
src/ingame/bomb.gd Normal file
View File

@ -0,0 +1,158 @@
class_name Bomb extends Node3D
# TODO: have those in item db instead ?
@export var _splash_small_sound: AudioStreamPlayer3D
@export var _splash_small_quiet_sound: AudioStreamPlayer3D
@export var _sell_sound: AudioStreamPlayer3D
@export var _model: Node3D
@export var _splash_particles: GPUParticles3D
@export var _sell_particles: GPUParticles3D
@export var _picking_area: Area3D
@export var body: RigidBody3D
@export var billboard := false
## Initials to construct from
@export var item_id := &"ID"
@export var item_count := 0
## Will be constructed as { item_id, item_count }, but could be edited after
## Use set_item_bundle() if it's dynamically assigned, after adding to the scene
@export var item_bundle: Dictionary
## Effect of splhashing.
@export_enum("none", "water") var splash_function := "none"
## no longer exists and shouldn't be considered, but not ready to be freed yet
var is_dead := false
## something to ignore
var sender_id: int
var sender_body: PhysicsBody3D
var _in_splash_range := {}
func _ready() -> void:
body.process_mode = Node.PROCESS_MODE_DISABLED
func _enter_tree() -> void:
if item_bundle.is_empty():
item_bundle = {
"item_id": item_id,
"count": item_count
}
@rpc("authority", "call_local", "reliable")
func set_item_bundle(p_item_bundle: Dictionary) -> void:
item_bundle = p_item_bundle
if _model.has_method("reflect_bundle"):
_model.reflect_bundle(item_bundle)
func _process(delta: float) -> void:
# spin around, no need to replicate this
if not billboard:
_model.basis = _model.basis.rotated(Vector3(1, 0, 0), -((TAU*2) * delta))
_model.basis = _model.basis.orthonormalized()
if not is_multiplayer_authority():
return
# TODO: interpolate for non-authority clients
# or, do we *really* care about synchronicity? could simulate it independently.
global_position = body.global_position
@rpc("authority", "call_local", "reliable")
func _disable_body() -> void:
# not using synchronizer for this because it ends up enabling processing
# on other clients
(func() -> void: body.process_mode = Node.PROCESS_MODE_DISABLED).call_deferred()
_picking_area.collision_layer = 0
func _on_body_entered(p_body: Node3D) -> void:
if p_body == sender_body:
return
if p_body.is_in_group("voids"):
queue_free()
return
if p_body.is_in_group("sell_boxes"):
var item := GameState.fetch().INVENTORY_ITEM_DB[item_bundle["item_id"]] as InventoryItem
if item.sells_for != 0:
get_node("/root/Main").add_inventory_item.rpc(&"coin", item.sells_for * item_bundle["count"])
if _sell_sound != null:
_sell_sound.play()
if _sell_particles != null:
_sell_particles.amount = item.sells_for * item_bundle["count"]
_sell_particles.emitting = true
is_dead = true
_model.hide()
_disable_body.rpc()
await _sell_particles.finished
queue_free()
return
if splash_function == "water":
for area: Area3D in _in_splash_range:
area.get_parent_node_3d().water.rpc_id(1, sender_id)
if _splash_small_sound != null and sender_id != 0:
_splash_small_sound.play()
if _splash_small_quiet_sound:
_splash_small_quiet_sound.play()
if _splash_particles:
_splash_particles.emitting = true
is_dead = true
_model.hide()
_disable_body.rpc()
if _splash_small_sound != null and sender_id != 0:
await _splash_small_sound.finished
if _splash_small_quiet_sound != null and _splash_small_quiet_sound.playing:
await _splash_small_quiet_sound.finished
queue_free()
func _on_splash_area_entered(area: Area3D) -> void:
_in_splash_range[area] = true
func _on_splash_area_exited(area: Area3D) -> void:
_in_splash_range.erase(area)
@rpc("authority", "call_local", "reliable")
func set_global_pos(new: Vector3) -> void:
global_position = new
body.global_position = new
body.reset_physics_interpolation()
@rpc("any_peer", "call_local", "reliable")
func get_picked_up() -> void:
if is_multiplayer_authority():
queue_free()
func mark_interactive() -> void:
# TODO: whatever
if _model.scene_file_path == "res://assets/water-bomb.glb":
for submodel in _model.get_children():
(submodel as MeshInstance3D).mesh.surface_get_material(0).next_pass.set("shader_parameter/color", Color.WHITE)
elif _model.scene_file_path == "res://src/ingame/quad_viewmodel.tscn":
_model.mesh.material_override.set("shader_parameter/outline_color", Color.WHITE)
func mark_non_interactive() -> void:
if _model.scene_file_path == "res://assets/water-bomb.glb":
for submodel in _model.get_children():
(submodel as MeshInstance3D).mesh.surface_get_material(0).next_pass.set("shader_parameter/color", Color(1, 1, 1, 0))
elif _model.scene_file_path == "res://src/ingame/quad_viewmodel.tscn":
_model.mesh.material_override.set("shader_parameter/outline_color", Color.BLACK)

View File

@ -86,7 +86,7 @@ func _input(event: InputEvent) -> void:
func _process(_delta: float) -> void:
# this is fine but should this be done in response to a signal instead?
_coin_label.text = "COINS: %d" % GameState.fetch().coins
_coin_label.text = "COIN: %d" % GameState.get_item_count(&"coin")
func _activate_chat() -> void:
@ -138,8 +138,8 @@ func _submit_chat_message(text: String) -> void:
@rpc("authority", "call_local", "reliable", 1)
func _add_chat_message(username: String, text: String) -> void:
_chat_history.add_child(_make_chat_message(username, text))
_chat_history_inactive.add_child(_make_chat_message(username, text))
_chat_history.add_child(_make_chat_message(username, text, false))
_chat_history_inactive.add_child(_make_chat_message(username, text, true))
func _on_chat_history_scroll_changed(history: ScrollContainer) -> void:
@ -147,11 +147,24 @@ func _on_chat_history_scroll_changed(history: ScrollContainer) -> void:
history.scroll_vertical = int(history.get_v_scroll_bar().max_value)
func _make_chat_message(username: String, text: String) -> RichTextLabel:
func _fade_inactive_chat_message(p_label: RichTextLabel) -> void:
var start := Time.get_ticks_msec()
while true:
var now := Time.get_ticks_msec()
if now - start >= 10000: break
p_label.add_theme_color_override("default_color", Color(0, 0, 0, 1.0 - (now - start) / 10000.0 ))
await get_tree().process_frame
p_label.queue_free()
func _make_chat_message(username: String, text: String, inactive: bool) -> RichTextLabel:
var label := RichTextLabel.new()
label.bbcode_enabled = true
label.fit_content = true
label.append_text("[color=red]%s[/color] %s" % [username.replace("[", "[lb]"), text.replace("[", "[lb]")])
if inactive:
label.add_theme_color_override("default_color", Color.BLACK)
_fade_inactive_chat_message(label)
return label

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=13 format=3 uid="uid://oyvhcwq60v2"]
[gd_scene load_steps=16 format=3 uid="uid://oyvhcwq60v2"]
[ext_resource type="Script" path="res://src/ingame/ingame.gd" id="1_akuuj"]
[ext_resource type="PackedScene" uid="uid://cs8c570bxh6u" path="res://src/ingame/player.tscn" id="2_w1gjc"]
@ -32,6 +32,17 @@ sky = SubResource("Sky_ygvd3")
ambient_light_color = Color(0, 0.164706, 0.278431, 1)
ambient_light_energy = 2.0
[sub_resource type="BoxShape3D" id="BoxShape3D_3215c"]
size = Vector3(3, 1, 3)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vx5ut"]
transparency = 1
albedo_color = Color(1, 1, 0, 0.698039)
[sub_resource type="BoxMesh" id="BoxMesh_gygr8"]
material = SubResource("StandardMaterial3D_vx5ut")
size = Vector3(3, 1, 3)
[node name="Ingame" type="Node3D" node_paths=PackedStringArray("_soundtrack", "_players", "_chat_panel", "_chat_input", "_chat_history_scroll", "_chat_history", "_chat_panel_inactive", "_chat_history_inactive", "_chat_history_scroll_inactive", "_coin_label")]
script = ExtResource("1_akuuj")
_player_scene = ExtResource("2_w1gjc")
@ -144,7 +155,7 @@ vertical_scroll_mode = 3
[node name="ChatHistory" type="VBoxContainer" parent="UI/ChatPanelInactive/ChatHistoryScroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
size_flags_vertical = 10
[node name="ComboTimer" type="Timer" parent="UI"]
wait_time = 3.0
@ -303,6 +314,17 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.15702, 1.04855, 5.88574)
[node name="SpawnPoint3" type="Marker3D" parent="SpawnPoints" groups=["spawn_points"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.43229, 0.627689, 7.20138)
[node name="SellBox" type="StaticBody3D" parent="." groups=["sell_boxes"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0)
collision_layer = 0
collision_mask = 4
[node name="CollisionShape3D" type="CollisionShape3D" parent="SellBox"]
shape = SubResource("BoxShape3D_3215c")
[node name="MeshInstance3D" type="MeshInstance3D" parent="SellBox"]
mesh = SubResource("BoxMesh_gygr8")
[connection signal="text_submitted" from="UI/ChatPanel/ChatInput" to="." method="_on_chat_message_submitted"]
[connection signal="pressed" from="UI/ChatPanel/ChatSendButton" to="." method="_on_chat_message_submitted"]
[connection signal="timeout" from="UI/ComboTimer" to="UI" method="_on_combo_timer_timeout"]

162
src/ingame/item_bomb.tscn Normal file
View File

@ -0,0 +1,162 @@
[gd_scene load_steps=17 format=3 uid="uid://bpvbnlhpth4w7"]
[ext_resource type="Script" path="res://src/ingame/bomb.gd" id="1_27lur"]
[ext_resource type="PackedScene" uid="uid://dxb5f3il2h1ur" path="res://src/ingame/quad_viewmodel.tscn" id="2_fy6j6"]
[ext_resource type="AudioStream" uid="uid://dy3ngeohp1uqf" path="res://assets/sfx/land.wav" id="3_1vo65"]
[ext_resource type="AudioStream" uid="uid://dkxv7wbq8s1gs" path="res://assets/sfx/coinsplash.ogg" id="4_fyu6c"]
[ext_resource type="Texture2D" uid="uid://dgo2frvws2o6f" path="res://assets/coin.png" id="5_om8wy"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_0ebrr"]
properties/0/path = NodePath(".:position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:is_dead")
properties/1/spawn = true
properties/1/replication_mode = 2
properties/2/path = NodePath(".:rotation")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath(".:item_bundle")
properties/3/spawn = true
properties/3/replication_mode = 2
properties/4/path = NodePath("Model:visible")
properties/4/spawn = true
properties/4/replication_mode = 2
properties/5/path = NodePath("SellParticles:emitting")
properties/5/spawn = true
properties/5/replication_mode = 2
properties/6/path = NodePath("DropSound:playing")
properties/6/spawn = true
properties/6/replication_mode = 2
properties/7/path = NodePath("SellSound:playing")
properties/7/spawn = true
properties/7/replication_mode = 2
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_v7dnr"]
albedo_color = Color(0, 1, 1, 1)
roughness = 0.2
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_rjbgm"]
direction = Vector3(0, 1, 0)
initial_velocity_min = 5.0
initial_velocity_max = 5.0
gravity = Vector3(0, -8, 0)
collision_mode = 2
[sub_resource type="SphereMesh" id="SphereMesh_bhdh4"]
radius = 0.1
height = 0.2
[sub_resource type="SphereShape3D" id="SphereShape3D_6c830"]
radius = 0.2
[sub_resource type="SphereShape3D" id="SphereShape3D_y6453"]
radius = 1.5
[sub_resource type="Gradient" id="Gradient_qoklw"]
offsets = PackedFloat32Array(0.0454545, 0.836364, 0.981818)
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_fdtmr"]
gradient = SubResource("Gradient_qoklw")
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_yncjw"]
lifetime_randomness = 0.2
inherit_velocity_ratio = 0.1
direction = Vector3(0, 1, 0)
spread = 180.0
initial_velocity_max = 3.0
gravity = Vector3(0, 0, 0)
linear_accel_min = -6.9
linear_accel_max = -3.45
color_ramp = SubResource("GradientTexture1D_fdtmr")
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ooyci"]
transparency = 1
shading_mode = 0
vertex_color_use_as_albedo = true
albedo_texture = ExtResource("5_om8wy")
[sub_resource type="QuadMesh" id="QuadMesh_4d5bh"]
material = SubResource("StandardMaterial3D_ooyci")
size = Vector2(0.25, 0.25)
[node name="ItemBomb" type="Node3D" node_paths=PackedStringArray("_splash_small_sound", "_sell_sound", "_model", "_sell_particles", "_picking_area", "body")]
script = ExtResource("1_27lur")
_splash_small_sound = NodePath("DropSound")
_sell_sound = NodePath("SellSound")
_model = NodePath("Model")
_sell_particles = NodePath("SellParticles")
_picking_area = NodePath("PickingArea")
body = NodePath("RigidBody3D")
billboard = true
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_0ebrr")
[node name="Model" parent="." instance=ExtResource("2_fy6j6")]
[node name="SplashParticles" type="GPUParticles3D" parent="."]
visible = false
material_override = SubResource("StandardMaterial3D_v7dnr")
emitting = false
amount = 32
lifetime = 2.0
one_shot = true
explosiveness = 1.0
randomness = 1.0
process_material = SubResource("ParticleProcessMaterial_rjbgm")
draw_pass_1 = SubResource("SphereMesh_bhdh4")
[node name="RigidBody3D" type="RigidBody3D" parent="."]
top_level = true
collision_layer = 4
collision_mask = 3
contact_monitor = true
max_contacts_reported = 1
linear_damp_mode = 1
linear_damp = 0.3
[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D"]
shape = SubResource("SphereShape3D_6c830")
[node name="SplashArea" type="Area3D" parent="RigidBody3D"]
collision_layer = 0
collision_mask = 8
monitorable = false
[node name="CollisionShape3D" type="CollisionShape3D" parent="RigidBody3D/SplashArea"]
shape = SubResource("SphereShape3D_y6453")
[node name="PickingArea" type="Area3D" parent="."]
collision_layer = 16
collision_mask = 0
monitoring = false
[node name="CollisionShape3D" type="CollisionShape3D" parent="PickingArea"]
shape = SubResource("SphereShape3D_6c830")
[node name="DropSound" type="AudioStreamPlayer3D" parent="."]
stream = ExtResource("3_1vo65")
volume_db = 10.0
bus = &"SoundEffects"
[node name="SellSound" type="AudioStreamPlayer3D" parent="."]
stream = ExtResource("4_fyu6c")
volume_db = 2.0
bus = &"SoundEffects"
[node name="SellParticles" type="GPUParticles3D" parent="."]
emitting = false
amount = 32
lifetime = 2.0
one_shot = true
speed_scale = 0.5
explosiveness = 1.0
randomness = 0.4
process_material = SubResource("ParticleProcessMaterial_yncjw")
draw_pass_1 = SubResource("QuadMesh_4d5bh")
[connection signal="body_entered" from="RigidBody3D" to="." method="_on_body_entered"]
[connection signal="area_entered" from="RigidBody3D/SplashArea" to="." method="_on_splash_area_entered"]
[connection signal="area_exited" from="RigidBody3D/SplashArea" to="." method="_on_splash_area_exited"]

View File

@ -3,13 +3,13 @@
[ext_resource type="Script" path="res://src/ingame/pipe.gd" id="1_ckmpn"]
[ext_resource type="PackedScene" uid="uid://tdsbo3e5ic86" path="res://src/ingame/water_bomb.tscn" id="2_3sfu2"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bgwg2"]
cull_mode = 2
albedo_color = Color(0.0313726, 1, 1, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ecwtt"]
albedo_color = Color(0, 1, 1, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_g3bb5"]
lightmap_size_hint = Vector2i(12, 12)
material = SubResource("StandardMaterial3D_bgwg2")
[sub_resource type="SphereMesh" id="SphereMesh_504u5"]
material = SubResource("StandardMaterial3D_ecwtt")
height = 0.001
rings = 1
[node name="Pipe" type="Node3D" node_paths=PackedStringArray("_projectile_holder", "_production_timer")]
script = ExtResource("1_ckmpn")
@ -22,12 +22,12 @@ _spawnable_scenes = PackedStringArray("res://src/ingame/water_bomb.tscn")
spawn_path = NodePath("../ProjectileHolder")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("PlaneMesh_g3bb5")
mesh = SubResource("SphereMesh_504u5")
[node name="ProjectileHolder" type="Node" parent="."]
[node name="DropTimer" type="Timer" parent="."]
wait_time = 2.0
wait_time = 2.5
autostart = true
[connection signal="timeout" from="DropTimer" to="." method="_on_drop_timer_timeout"]

View File

@ -1,11 +1,8 @@
class_name Player extends CharacterBody3D
# TODO: inherit from StairStepper, not the other way around ? Probably by also having base Actor class.
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
@export var _projectile_scene: PackedScene
@export var _projectile_holder: Node
@export var _projectile_point: Marker3D
@export var _camera_pivot: Node3D
@ -25,7 +22,13 @@ var _projectile_speed := 12.0
var _interaction_selection: Node3D
var controls_disabled := false
var held_thing := { "item_id": &"empty_hand", "count": 0 }
@export var held_thing := { "item_id": &"empty_hand", "count": 0 }
## Whether picking input is pressed right now
var _picking := false
## Used to limit picking rate, potentially upgradable
var _last_picked := 0
var _picks_per_second := 3
# What the others see.
@ -92,7 +95,7 @@ func _physics_process(delta: float) -> void:
if velocity.length() > _max_speed:
velocity = velocity.clampf(0, _max_speed)
call("stair_step_up", direction)
$StairStepper.stair_step_up(direction)
if move_and_slide() and is_on_floor():
for idx in range(get_slide_collision_count()):
@ -103,7 +106,7 @@ func _physics_process(delta: float) -> void:
global_position = spawn_point.global_position
reset_physics_interpolation()
call("stair_step_down")
$StairStepper.stair_step_down()
@rpc("any_peer", "call_local", "reliable")
@ -134,6 +137,7 @@ func hold_thing(p_bundle: Dictionary) -> void:
mesh.surface_set_material(0, material)
submodel.mesh = mesh
@rpc("any_peer", "call_local", "reliable")
func throw_thing() -> void:
held_thing = { "item_id": &"empty_hand", "count": 0 }
@ -156,19 +160,16 @@ func _unhandled_input(event: InputEvent) -> void:
return
if event.is_action_pressed("pick"):
if _interaction_selection != null:
if empty_handed():
_interaction_selection.owner.get_picked_up.rpc()
hold_thing.rpc(_interaction_selection.owner.item_bundle)
elif GameState.are_bundles_stackable(held_thing, _interaction_selection.owner.item_bundle):
_interaction_selection.owner.get_picked_up.rpc()
var bundle := GameState.combine_bundles(held_thing, _interaction_selection.owner.item_bundle)
hold_thing.rpc(bundle)
_picking = true
if event.is_action_released("pick"):
_picking = false
if event.is_action_pressed("fire") and not empty_handed():
var new_projectile: Node3D = _projectile_scene.instantiate()
var item := GameState.fetch().INVENTORY_ITEM_DB[held_thing["item_id"]] as InventoryItem
var new_projectile: Node3D = item.bomb.instantiate()
_projectile_holder.add_child(new_projectile, true)
_set_projectile_authority.rpc(new_projectile.get_path(), id)
new_projectile.set_item_bundle.rpc(held_thing)
new_projectile.set_global_pos.rpc(_projectile_point.global_position)
new_projectile.rotation = rotation
new_projectile.sender_body = self
@ -190,10 +191,23 @@ func _set_projectile_authority(path: NodePath, authority_id: int) -> void:
func _process_input() -> void:
if controls_disabled:
if controls_disabled or id != multiplayer.get_unique_id():
input_dir = Vector2.ZERO
input_jumped = false
return
input_dir = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_backward")
input_jumped = Input.is_action_just_pressed("jump")
if _picking and (Time.get_ticks_msec() - _last_picked) >= (1000.0 / _picks_per_second) \
and _interaction_selection != null:
if empty_handed():
_last_picked = Time.get_ticks_msec()
_interaction_selection.owner.get_picked_up.rpc()
hold_thing.rpc(_interaction_selection.owner.item_bundle)
elif GameState.are_bundles_stackable(held_thing, _interaction_selection.owner.item_bundle):
_last_picked = Time.get_ticks_msec()
_interaction_selection.owner.get_picked_up.rpc()
var bundle := GameState.combine_bundles(held_thing, _interaction_selection.owner.item_bundle)
hold_thing.rpc(bundle)
#_line_of_sight.force_shapecast_update()

View File

@ -1,7 +1,7 @@
[gd_scene load_steps=8 format=3 uid="uid://cs8c570bxh6u"]
[ext_resource type="Script" path="res://src/ingame/player.gd" id="1_r8lgj"]
[ext_resource type="Script" path="res://src/lib/player_character.gd" id="1_sba4x"]
[ext_resource type="PackedScene" uid="uid://tdsbo3e5ic86" path="res://src/ingame/water_bomb.tscn" id="2_naek4"]
[ext_resource type="AudioStream" uid="uid://3dlhs18w1fa2" path="res://assets/sfx/boom.wav" id="3_u2hxa"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_2xotl"]
@ -23,6 +23,9 @@ properties/4/replication_mode = 2
properties/5/path = NodePath("ShotSound:playing")
properties/5/spawn = true
properties/5/replication_mode = 2
properties/6/path = NodePath(".:held_thing")
properties/6/spawn = true
properties/6/replication_mode = 2
[sub_resource type="CapsuleMesh" id="CapsuleMesh_hi8jw"]
@ -32,8 +35,7 @@ properties/5/replication_mode = 2
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_projectile_holder", "_projectile_point", "_camera_pivot", "_camera", "_line_of_sight", "_shot_sound")]
collision_layer = 2
script = ExtResource("1_sba4x")
_projectile_scene = ExtResource("2_naek4")
script = ExtResource("1_r8lgj")
_projectile_holder = NodePath("ProjectileHolder")
_projectile_point = NodePath("ProjectilePoint")
_camera_pivot = NodePath("CameraPivot")
@ -41,11 +43,14 @@ _camera = NodePath("CameraPivot/Camera3D")
_line_of_sight = NodePath("CameraPivot/LineOfSight")
_shot_sound = NodePath("ShotSound")
[node name="StairStepper" type="Node" parent="."]
script = ExtResource("1_sba4x")
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_2xotl")
[node name="ProjectileSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://src/ingame/water_bomb.tscn")
_spawnable_scenes = PackedStringArray("res://src/ingame/water_bomb.tscn", "res://src/ingame/item_bomb.tscn")
spawn_path = NodePath("../ProjectileHolder")
[node name="ProjectileHolder" type="Node" parent="."]

View File

@ -1,9 +1,13 @@
extends Node3D
@export var mesh: MeshInstance3D
@export var count_label: Label3D
func reflect_bundle(p_bundle: Dictionary) -> void:
var item = GameState.fetch().INVENTORY_ITEM_DB[p_bundle["item_id"]]
$Sprite3D.texture = item.icon
mesh.material_override.set("shader_parameter/albedo_texture", item.icon)
if item.stackable:
$CountLabel.text = str(p_bundle["count"]) + "/" + str(item.stack_limit)
count_label.text = str(p_bundle["count"]) + "\n=\n" + str(item.stack_limit)
else:
$CountLabel.text = str(p_bundle["count"])
count_label.text = str(p_bundle["count"])

View File

@ -1,19 +1,40 @@
[gd_scene load_steps=3 format=3 uid="uid://dxb5f3il2h1ur"]
[gd_scene load_steps=6 format=3 uid="uid://dxb5f3il2h1ur"]
[ext_resource type="Script" path="res://src/ingame/quad_viewmodel.gd" id="1_l2s4x"]
[ext_resource type="Texture2D" uid="uid://dw3x3h3f34sy3" path="res://assets/coin_flower.png" id="2_7fi8t"]
[ext_resource type="Texture2D" uid="uid://dgo2frvws2o6f" path="res://assets/coin.png" id="2_k1r4m"]
[ext_resource type="Shader" path="res://assets/shaders/interactivity_outline2.gdshader" id="2_xpghy"]
[node name="QuadViewmodel" type="Node3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.179242, 0)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_5wlqs"]
resource_local_to_scene = true
render_priority = 0
shader = ExtResource("2_xpghy")
shader_parameter/width = 1.0
shader_parameter/outline_color = Color(0, 0, 0, 1)
shader_parameter/albedo_texture = ExtResource("2_k1r4m")
[sub_resource type="QuadMesh" id="QuadMesh_dxt6h"]
size = Vector2(0.5, 0.5)
[node name="QuadViewmodel" type="Node3D" node_paths=PackedStringArray("mesh", "count_label")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.309578, 0)
script = ExtResource("1_l2s4x")
mesh = NodePath("Mesh")
count_label = NodePath("CountLabel")
[node name="CountLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.181474, 0)
billboard = 1
text = "3"
[node name="Sprite3D" type="Sprite3D" parent="."]
pixel_size = 0.015
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0502154, 0)
billboard = 1
texture_filter = 0
texture = ExtResource("2_7fi8t")
render_priority = 2
outline_render_priority = 1
text = "3
=
9"
font_size = 30
outline_size = 10
line_spacing = -22.0
[node name="Mesh" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0495689, 0)
material_override = SubResource("ShaderMaterial_5wlqs")
mesh = SubResource("QuadMesh_dxt6h")

View File

@ -19,10 +19,10 @@ func _ready() -> void:
GameState.fetch().player_data[_id].water_combo_update.connect(
func(current: int):
if current == 0:
$Combo.hide()
_combo_label.hide()
else:
$Combo.show()
$Combo.text = "+" + str(current) + "!"
_combo_label.show()
_combo_label.text = "+" + str(current) + "!"
_last_combo_time = Time.get_ticks_msec()
_combo_timer.start()
)
@ -30,4 +30,4 @@ func _ready() -> void:
func _on_combo_timer_timeout() -> void:
GameState.fetch().player_data[_id].reset_water_combo()
$Combo.hide()
_combo_label.hide()

View File

@ -1,95 +0,0 @@
extends Node3D
@export var _splash_small_sound: AudioStreamPlayer3D
@export var _splash_small_quiet_sound: AudioStreamPlayer3D
@export var _model: Node3D
@export var _splash_particles: GPUParticles3D
@export var _picking_area: Area3D
@export var body: RigidBody3D
## no longer exists and shouldn't be considered, but not ready to be freed yet
@export var is_dead := false
const item_bundle := { "item_id": &"water_bomb", "count": 1 }
var _in_splash_range := {}
## something to ignore
var sender_id: int
var sender_body: PhysicsBody3D
func _ready() -> void:
body.process_mode = Node.PROCESS_MODE_DISABLED
func _process(delta: float) -> void:
# spin around, no need to replicate this
_model.basis = _model.basis.rotated(Vector3(1, 0, 0), -((TAU*2) * delta))
_model.basis = _model.basis.orthonormalized()
if not is_multiplayer_authority():
return
global_position = body.global_position
@rpc("authority", "call_local", "reliable")
func _disable_body() -> void:
# not using synchronizer for this because it ends up enabling processing
# on other clients
(func() -> void: body.process_mode = Node.PROCESS_MODE_DISABLED).call_deferred()
_picking_area.collision_layer = 0
func _on_body_entered(p_body: Node3D) -> void:
if p_body == sender_body:
return
if p_body.is_in_group("voids"):
queue_free()
return
for area: Area3D in _in_splash_range:
area.get_parent_node_3d().water.rpc_id(1, sender_id)
_splash_small_sound.play()
_splash_small_quiet_sound.play()
_splash_particles.emitting = true
is_dead = true
_model.hide()
_disable_body.rpc()
await _splash_small_sound.finished
if _splash_small_quiet_sound.playing:
await _splash_small_quiet_sound.finished
queue_free()
func _on_splash_area_entered(area: Area3D) -> void:
_in_splash_range[area] = true
func _on_splash_area_exited(area: Area3D) -> void:
_in_splash_range.erase(area)
@rpc("authority", "call_local", "reliable")
func set_global_pos(new: Vector3) -> void:
global_position = new
body.global_position = new
body.reset_physics_interpolation()
@rpc("any_peer", "call_local", "reliable")
func get_picked_up() -> void:
if is_multiplayer_authority():
queue_free()
func mark_interactive() -> void:
for submodel in _model.get_children():
(submodel as MeshInstance3D).mesh.surface_get_material(0).next_pass.set("shader_parameter/color", Color.WHITE)
func mark_non_interactive() -> void:
for submodel in _model.get_children():
(submodel as MeshInstance3D).mesh.surface_get_material(0).next_pass.set("shader_parameter/color", Color(1, 1, 1, 0))

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=11 format=3 uid="uid://tdsbo3e5ic86"]
[ext_resource type="Script" path="res://src/ingame/water_bomb.gd" id="1_lk5fq"]
[ext_resource type="Script" path="res://src/ingame/bomb.gd" id="1_lk5fq"]
[ext_resource type="AudioStream" uid="uid://dtjpv2b74g24m" path="res://assets/sfx/splash-small.wav" id="2_0wk8g"]
[ext_resource type="PackedScene" uid="uid://ba2mut58elwrh" path="res://assets/water-bomb.glb" id="2_v2imr"]
[ext_resource type="AudioStream" uid="uid://blgrl2wl05feq" path="res://assets/sfx/splash-small-quiet.wav" id="3_hgy7l"]
@ -57,6 +57,9 @@ _model = NodePath("Model")
_splash_particles = NodePath("SplashParticles")
_picking_area = NodePath("PickingArea")
body = NodePath("RigidBody3D")
item_id = &"water_bomb"
item_count = 1
splash_function = "water"
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_0ebrr")

View File

@ -1,10 +1,11 @@
class_name GameState extends Resource
# TODO: build dynamically?
const INVENTORY_ITEM_DB = {
&"meat": preload("res://data/meat.tres"),
&"water_bomb": preload("res://data/water_bomb.tres"),
&"coin_flower": preload("res://data/coin_flower.tres"),
var INVENTORY_ITEM_DB := {
&"meat": load("res://data/meat.tres"),
&"water_bomb": load("res://data/water_bomb.tres"),
&"coin_flower": load("res://data/coin_flower.tres"),
&"coin": load("res://data/coin.tres"),
}
## keys are multiplayer ID ints, values are PlayerData
@ -12,7 +13,6 @@ const INVENTORY_ITEM_DB = {
## keys are InventoryItem resource IDs (from db),
## values are { "item": InventoryItem, "count": int }
@export var inventory := {}
@export var coins: int # TODO: might it make sense to have even them as items?
static var _instance := GameState.new()
@ -25,6 +25,13 @@ static func fetch() -> GameState:
return _instance
static func get_item_count(p_item_id: StringName) -> int:
if GameState.fetch().inventory.has(p_item_id):
return GameState.fetch().inventory[p_item_id]["count"]
else:
return 0
# TODO: better place for those?
static func are_bundles_stackable(p_a_bundle: Dictionary, p_b_bundle: Dictionary) -> bool:
var item = GameState.fetch().INVENTORY_ITEM_DB[p_a_bundle["item_id"]]

View File

@ -2,8 +2,11 @@ class_name InventoryItem extends Resource
@export var icon: Texture2D = preload("res://icon.svg")
@export var model: PackedScene
@export var bomb: PackedScene = preload("res://src/ingame/item_bomb.tscn")
@export var name := "NAME"
@export var id := &"ID"
@export var stackable := false
@export var stack_limit := 8
@export var sells_for := 0

View File

@ -1,4 +1,4 @@
class_name StairStepper extends Player
class_name StairStepper extends Node
# MIT License
#
# Copyright (c) 2024 JKWall
@ -53,31 +53,30 @@ var was_grounded: bool = true
var is_grounded: bool = true
func _physics_process(delta: float) -> void:
func _physics_process(_delta: float) -> void:
was_grounded = is_grounded
is_grounded = is_on_floor()
super._physics_process(delta)
is_grounded = owner.is_on_floor()
# Function: Handle walking down stairs
func stair_step_down():
if is_on_floor():
if owner.is_on_floor():
return
# If we're falling from a step
if velocity.y <= 0 and was_grounded:
if owner.velocity.y <= 0 and was_grounded:
# Initialize body test variables
var body_test_result = PhysicsTestMotionResult3D.new()
var body_test_params = PhysicsTestMotionParameters3D.new()
body_test_params.from = self.global_transform ## We get the player's current global_transform
body_test_params.from = owner.global_transform ## We get the player's current global_transform
body_test_params.motion = Vector3(0, MAX_STEP_DOWN, 0) ## We project the player downward
if PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result):
if PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result):
# Enters if a collision is detected by body_test_motion
# Get distance to step and move player downward by that much
position.y += body_test_result.get_travel().y
apply_floor_snap()
owner.position.y += body_test_result.get_travel().y
owner.apply_floor_snap()
is_grounded = true
@ -90,13 +89,13 @@ func stair_step_up(wish_dir: Vector3):
var body_test_params = PhysicsTestMotionParameters3D.new()
var body_test_result = PhysicsTestMotionResult3D.new()
var test_transform = global_transform ## Storing current global_transform for testing
var test_transform = owner.global_transform ## Storing current global_transform for testing
var distance = wish_dir * 0.1 ## Distance forward we want to check
body_test_params.from = self.global_transform ## Self as origin point
body_test_params.from = owner.global_transform ## Self as origin point
body_test_params.motion = distance ## Go forward by current distance
# Pre-check: Are we colliding?
if !PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result):
if !PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result):
## If we don't collide, return
return
@ -108,13 +107,13 @@ func stair_step_up(wish_dir: Vector3):
var step_up = MAX_STEP_UP * Vector3.UP
body_test_params.from = test_transform
body_test_params.motion = step_up
PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result)
PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result)
test_transform = test_transform.translated(body_test_result.get_travel())
# 3. Move test_transform forward by remaining distance
body_test_params.from = test_transform
body_test_params.motion = remainder
PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result)
PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result)
test_transform = test_transform.translated(body_test_result.get_travel())
# 3.5 Project remaining along wall normal (if any)
@ -129,7 +128,7 @@ func stair_step_up(wish_dir: Vector3):
body_test_params.from = test_transform
body_test_params.motion = remainder * projected_vector
PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result)
PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result)
test_transform = test_transform.translated(body_test_result.get_travel())
# 4. Move test_transform down onto step
@ -137,21 +136,21 @@ func stair_step_up(wish_dir: Vector3):
body_test_params.motion = MAX_STEP_UP * -Vector3.UP
# Return if no collision
if !PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result):
if !PhysicsServer3D.body_test_motion(owner.get_rid(), body_test_params, body_test_result):
return
test_transform = test_transform.translated(body_test_result.get_travel())
# 5. Check floor normal for un-walkable slope
var surface_normal = body_test_result.get_collision_normal()
var temp_floor_max_angle = floor_max_angle + deg_to_rad(20)
var temp_floor_max_angle = owner.floor_max_angle + deg_to_rad(20)
if (snappedf(surface_normal.angle_to(Vector3.UP), 0.001) > temp_floor_max_angle):
return
# 6. Move player up
var global_pos = global_position
var global_pos = owner.global_position
#var step_up_dist = test_transform.origin.y - global_pos.y
global_pos.y = test_transform.origin.y
global_position = global_pos
owner.global_position = global_pos

View File

@ -35,8 +35,6 @@ func _on_player_joined(id: int) -> void:
_register_player.rpc_id(id, username)
if _game_started:
set_coins.rpc_id(id, GameState.fetch().coins)
for item_id in GameState.fetch().inventory:
set_inventory_item_count.rpc_id(id, item_id, GameState.fetch().inventory[item_id].count)
@ -180,27 +178,23 @@ func _on_chat_message_submitted(text := "") -> void:
_chat_input.clear()
@rpc("authority", "call_local", "reliable")
func set_coins(value: int) -> void:
GameState.fetch().coins = value
## Make sure it exists and all.
func prepate_inventory_idem(item_id: StringName) -> void:
if not GameState.fetch().inventory.has(item_id):
GameState.fetch().inventory[item_id] = {
"item": GameState.INVENTORY_ITEM_DB[item_id],
"item": GameState.fetch().INVENTORY_ITEM_DB[item_id],
"count": 0,
}
@rpc("authority", "call_local", "reliable")
# TODO: made any_peer to have access from projectiles
@rpc("any_peer", "call_local", "reliable")
func add_inventory_item(item_id: StringName, amount: int) -> void:
prepate_inventory_idem(item_id)
GameState.fetch().inventory[item_id]["count"] += amount
@rpc("authority", "call_local", "reliable")
@rpc("any_peer", "call_local", "reliable")
func remove_inventory_item(item_id: StringName, amount: int) -> void:
assert(GameState.fetch().inventory.has(item_id))
@ -211,7 +205,7 @@ func remove_inventory_item(item_id: StringName, amount: int) -> void:
GameState.fetch().inventory.erase(item_id)
@rpc("authority", "call_local", "reliable")
@rpc("any_peer", "call_local", "reliable")
func set_inventory_item_count(item_id: StringName, value: int) -> void:
prepate_inventory_idem(item_id)
GameState.fetch().inventory[item_id]["count"] = value