mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 08:02:03 +00:00
Compare commits
6 Commits
9fd1764522
...
8fa40769a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fa40769a8 | |||
| 0a7eb0b742 | |||
| 8bd65290fc | |||
| 28eb45338a | |||
| 78ef0cb1f8 | |||
| 8d4078989c |
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Orbitron Project Authors (https://github.com/theleagueof/orbitron), with Reserved Font Name: "Orbitron"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://bhbpb712ouagv"
|
||||
path="res://.godot/imported/Orbitron-Medium.ttf-0d3cd88166f12a3096066c2eef570744.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/fonts/Orbitron-Medium.ttf"
|
||||
dest_files=["res://.godot/imported/Orbitron-Medium.ttf-0d3cd88166f12a3096066c2eef570744.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=3
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
+51
-1
@@ -1,9 +1,10 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://c8kak2l3m4n5"]
|
||||
[gd_scene load_steps=10 format=3 uid="uid://c8kak2l3m4n5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://du7y176h5aaq4" path="res://scripts/HUDController.gd" id="1_hud_controller"]
|
||||
[ext_resource type="Script" path="res://scripts/hud_attitude_indicator.gd" id="2_adi"]
|
||||
[ext_resource type="Script" path="res://scripts/hud_heading_tape.gd" id="3_tape"]
|
||||
[ext_resource type="Script" path="res://scripts/hud_gauge.gd" id="4_gauge"]
|
||||
[ext_resource type="Theme" path="res://themes/hud_theme.tres" id="5_theme"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_hud"]
|
||||
font_size = 32
|
||||
@@ -50,6 +51,7 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme = ExtResource("5_theme")
|
||||
|
||||
[node name="ScoreboardPanel" type="PanelContainer" parent="Control"]
|
||||
layout_mode = 1
|
||||
@@ -118,6 +120,54 @@ label_settings = SubResource("LabelSettings_kickoff")
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ImpactFlash" type="ColorRect" parent="Control"]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.72, 0.9, 1, 0)
|
||||
|
||||
[node name="GoalCelebration" type="Control" parent="Control"]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Tint" type="ColorRect" parent="Control/GoalCelebration"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="Control/GoalCelebration"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="GoalTitle" type="Label" parent="Control/GoalCelebration/Center"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.8)
|
||||
theme_override_constants/shadow_offset_x = 5
|
||||
theme_override_constants/shadow_offset_y = 5
|
||||
theme_override_font_sizes/font_size = 82
|
||||
text = "GOAL!"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ResultOverlay" type="Control" parent="Control"]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="PackedScene" path="res://scenes/arena_base.tscn" id="1_base"]
|
||||
[ext_resource type="Shader" path="res://shaders/sky_asteroid_field.gdshader" id="7_asteroid"]
|
||||
[ext_resource type="PackedScene" path="res://assets/models/nebula_station.glb" id="8_station"]
|
||||
[ext_resource type="PackedScene" path="res://assets/models/nebula_debris.glb" id="9_debris"]
|
||||
[ext_resource type="PackedScene" path="res://assets/models/nebula_planet.glb" id="10_planet"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_asteroid"]
|
||||
shader = ExtResource("7_asteroid")
|
||||
@@ -22,3 +25,30 @@ light_color = Color(1, 0.85, 0.65, 1)
|
||||
light_color = Color(0.55, 0.4, 0.3, 1)
|
||||
|
||||
[node name="Decoration" type="Node3D" parent="."]
|
||||
|
||||
[node name="MiningStationA" parent="Decoration" instance=ExtResource("8_station")]
|
||||
transform = Transform3D(2.1, 0, 0, 0, 2.1, 0, 0, 0, 2.1, 44, 8, -12)
|
||||
rotation = Vector3(0.08, -0.72, 0.02)
|
||||
|
||||
[node name="MiningStationB" parent="Decoration" instance=ExtResource("8_station")]
|
||||
transform = Transform3D(1.45, 0, 0, 0, 1.45, 0, 0, 0, 1.45, -48, 15, 22)
|
||||
rotation = Vector3(-0.06, 1.2, 0.08)
|
||||
|
||||
[node name="AsteroidClusterA" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(2.4, 0, 0, 0, 2.4, 0, 0, 0, 2.4, -35, 12, -25)
|
||||
rotation = Vector3(0.4, 0.9, -0.2)
|
||||
|
||||
[node name="AsteroidClusterB" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(1.7, 0, 0, 0, 1.7, 0, 0, 0, 1.7, 38, 20, 28)
|
||||
rotation = Vector3(-0.25, 2.1, 0.35)
|
||||
|
||||
[node name="AsteroidClusterC" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 18, 7, -48)
|
||||
rotation = Vector3(0.7, -0.5, 0.1)
|
||||
|
||||
[node name="AsteroidClusterD" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, -20, 25, 50)
|
||||
rotation = Vector3(-0.3, -1.1, -0.2)
|
||||
|
||||
[node name="DistantPlanet" parent="Decoration" instance=ExtResource("10_planet")]
|
||||
transform = Transform3D(24, 0, 0, 0, 24, 0, 0, 0, 24, 95, 48, -145)
|
||||
|
||||
@@ -31,6 +31,23 @@ adjustment_enabled = true
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.05
|
||||
adjustment_saturation = 1.08
|
||||
ssao_enabled = true
|
||||
ssao_radius = 2.5
|
||||
ssao_intensity = 2.0
|
||||
ssao_power = 1.5
|
||||
ssao_detail = 0.75
|
||||
ssao_horizon = 0.06
|
||||
ssao_sharpness = 0.98
|
||||
ssao_light_affect = 0.15
|
||||
ssil_enabled = true
|
||||
ssil_radius = 4.0
|
||||
ssil_intensity = 1.25
|
||||
ssil_sharpness = 0.98
|
||||
ssil_normal_rejection = 1.0
|
||||
sdfgi_enabled = true
|
||||
sdfgi_use_occlusion = true
|
||||
sdfgi_bounce_feedback = 0.5
|
||||
sdfgi_read_sky_light = true
|
||||
|
||||
[node name="Arena" type="Node3D"]
|
||||
script = ExtResource("1_iywne")
|
||||
@@ -49,6 +66,44 @@ light_color = Color(0.55, 0.65, 0.85, 1)
|
||||
light_energy = 0.4
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="PitchLights" type="Node3D" parent="."]
|
||||
|
||||
[node name="Team0GoalLight" type="OmniLight3D" parent="PitchLights"]
|
||||
position = Vector3(0, 4.5, 22)
|
||||
light_color = Color(0.48, 0.18, 0.88, 1)
|
||||
light_energy = 5.0
|
||||
light_bake_mode = 1
|
||||
omni_range = 16.0
|
||||
omni_attenuation = 1.35
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="Team1GoalLight" type="OmniLight3D" parent="PitchLights"]
|
||||
position = Vector3(0, 4.5, -22)
|
||||
light_color = Color(0.24, 0.86, 0.42, 1)
|
||||
light_energy = 5.0
|
||||
light_bake_mode = 1
|
||||
omni_range = 16.0
|
||||
omni_attenuation = 1.35
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="MidfieldLightA" type="OmniLight3D" parent="PitchLights"]
|
||||
position = Vector3(-10, 7, 0)
|
||||
light_color = Color(0.38, 0.62, 1, 1)
|
||||
light_energy = 3.5
|
||||
light_bake_mode = 1
|
||||
omni_range = 15.0
|
||||
omni_attenuation = 1.5
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="MidfieldLightB" type="OmniLight3D" parent="PitchLights"]
|
||||
position = Vector3(10, 7, 0)
|
||||
light_color = Color(0.38, 0.62, 1, 1)
|
||||
light_energy = 3.5
|
||||
light_bake_mode = 1
|
||||
omni_range = 15.0
|
||||
omni_attenuation = 1.5
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_base")
|
||||
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/ship_camera.gd" id="1_rig"]
|
||||
[ext_resource type="Shader" path="res://shaders/post_process.gdshader" id="2_post"]
|
||||
|
||||
[sub_resource type="CameraAttributesPractical" id="CameraAttributes_gameplay"]
|
||||
dof_blur_far_enabled = true
|
||||
dof_blur_far_distance = 24.0
|
||||
dof_blur_far_transition = 14.0
|
||||
dof_blur_near_enabled = true
|
||||
dof_blur_near_distance = 1.25
|
||||
dof_blur_near_transition = 0.75
|
||||
dof_blur_amount = 0.08
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_post"]
|
||||
shader = ExtResource("2_post")
|
||||
|
||||
[node name="ShipCameraRig" type="Node3D"]
|
||||
script = ExtResource("1_rig")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
current = true
|
||||
attributes = SubResource("CameraAttributes_gameplay")
|
||||
|
||||
[node name="PostProcess" type="CanvasLayer" parent="."]
|
||||
layer = 0
|
||||
|
||||
[node name="PostFX" type="ColorRect" parent="PostProcess"]
|
||||
material = SubResource("ShaderMaterial_post")
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
@@ -18,6 +18,10 @@ class_name HUDController
|
||||
@onready var result_panel = get_node_or_null("Control/ResultOverlay/Center/ResultPanel")
|
||||
@onready var result_label = get_node_or_null("Control/ResultOverlay/Center/ResultPanel/VBox/ResultLabel")
|
||||
@onready var final_score_label = get_node_or_null("Control/ResultOverlay/Center/ResultPanel/VBox/FinalScoreLabel")
|
||||
@onready var impact_flash = get_node_or_null("Control/ImpactFlash")
|
||||
@onready var goal_celebration = get_node_or_null("Control/GoalCelebration")
|
||||
@onready var goal_tint = get_node_or_null("Control/GoalCelebration/Tint")
|
||||
@onready var goal_title = get_node_or_null("Control/GoalCelebration/Center/GoalTitle")
|
||||
|
||||
@onready var heading_tape: HudHeadingTape = get_node_or_null("Control/Instruments/HeadingTape")
|
||||
@onready var attitude_indicator: HudAttitudeIndicator = get_node_or_null("Control/Instruments/Cluster/Dials/AttitudeIndicator")
|
||||
@@ -28,6 +32,8 @@ class_name HUDController
|
||||
|
||||
var ship: Node
|
||||
var _last_score := {0: 0, 1: 0}
|
||||
var _impact_flash_tween: Tween
|
||||
var _goal_tween: Tween
|
||||
|
||||
func _ready():
|
||||
# Wait one frame to ensure ship is ready and added to group
|
||||
@@ -170,6 +176,51 @@ func _flash_score_label(label: Label) -> void:
|
||||
tween.tween_property(label, "scale", Vector2(1.4, 1.4), 0.1)
|
||||
tween.tween_property(label, "scale", Vector2(1.0, 1.0), 0.2)
|
||||
|
||||
|
||||
func flash_impact(intensity: float) -> void:
|
||||
if not impact_flash or not is_instance_valid(impact_flash):
|
||||
return
|
||||
if _impact_flash_tween and _impact_flash_tween.is_valid():
|
||||
_impact_flash_tween.kill()
|
||||
impact_flash.color = Color(0.72, 0.9, 1.0, lerpf(0.06, 0.24, intensity))
|
||||
impact_flash.visible = true
|
||||
_impact_flash_tween = create_tween()
|
||||
_impact_flash_tween.tween_property(impact_flash, "color:a", 0.0, lerpf(0.08, 0.18, intensity))
|
||||
_impact_flash_tween.tween_callback(func(): impact_flash.visible = false)
|
||||
|
||||
|
||||
func show_goal_celebration(scoring_team: int) -> void:
|
||||
if not goal_celebration or not goal_title or not goal_tint:
|
||||
return
|
||||
if _goal_tween and _goal_tween.is_valid():
|
||||
_goal_tween.kill()
|
||||
var tint: Color = TeamColors.TEAM_COLORS[scoring_team]
|
||||
goal_tint.color = Color(tint.r, tint.g, tint.b, 0.34)
|
||||
goal_title.text = "%s GOAL!" % TeamColors.TEAM_NAMES[scoring_team].to_upper()
|
||||
goal_title.add_theme_color_override("font_color", tint.lightened(0.35))
|
||||
goal_title.scale = Vector2(0.55, 0.55)
|
||||
goal_title.modulate.a = 0.0
|
||||
goal_celebration.visible = true
|
||||
await get_tree().process_frame
|
||||
if not is_instance_valid(goal_title):
|
||||
return
|
||||
goal_title.pivot_offset = goal_title.size / 2.0
|
||||
_goal_tween = create_tween().set_parallel(true).set_ignore_time_scale(true)
|
||||
_goal_tween.tween_property(goal_title, "scale", Vector2.ONE, 0.32).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
_goal_tween.tween_property(goal_title, "modulate:a", 1.0, 0.16)
|
||||
_goal_tween.tween_property(goal_tint, "color:a", 0.12, 0.7).set_delay(0.12)
|
||||
|
||||
|
||||
func hide_goal_celebration() -> void:
|
||||
if not goal_celebration or not is_instance_valid(goal_celebration):
|
||||
return
|
||||
if _goal_tween and _goal_tween.is_valid():
|
||||
_goal_tween.kill()
|
||||
_goal_tween = create_tween().set_parallel(true).set_ignore_time_scale(true)
|
||||
_goal_tween.tween_property(goal_title, "modulate:a", 0.0, 0.16)
|
||||
_goal_tween.tween_property(goal_tint, "color:a", 0.0, 0.2)
|
||||
_goal_tween.chain().tween_callback(func(): goal_celebration.visible = false)
|
||||
|
||||
func _on_overtime_started():
|
||||
if timer_label and is_instance_valid(timer_label):
|
||||
timer_label.text = "OT"
|
||||
|
||||
@@ -17,10 +17,54 @@ extends RigidBody3D
|
||||
const MAX_SPEED := 32.0
|
||||
|
||||
var _boundary: ArenaBoundary
|
||||
var _trail: GPUParticles3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_boundary = get_tree().get_first_node_in_group("arena_boundary")
|
||||
if DisplayServer.get_name() == "headless":
|
||||
# _integrate_forces remains active; only the render-side trail updater is
|
||||
# disabled across the many parallel RL environments.
|
||||
set_physics_process(false)
|
||||
else:
|
||||
_build_trail()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if _trail:
|
||||
var speed_ratio := clampf(linear_velocity.length() / MAX_SPEED, 0.0, 1.0)
|
||||
_trail.emitting = speed_ratio > 0.12
|
||||
_trail.amount_ratio = smoothstep(0.12, 1.0, speed_ratio)
|
||||
|
||||
|
||||
func _build_trail() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.albedo_color = Color(0.55, 0.85, 1.0, 0.42)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(0.35, 0.72, 1.0)
|
||||
mat.emission_energy_multiplier = 1.8
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(0.22, 0.22)
|
||||
quad.material = mat
|
||||
var process := ParticleProcessMaterial.new()
|
||||
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||
process.emission_sphere_radius = 0.35
|
||||
process.gravity = Vector3.ZERO
|
||||
process.scale_min = 0.35
|
||||
process.scale_max = 1.0
|
||||
_trail = GPUParticles3D.new()
|
||||
_trail.name = "BallTrail"
|
||||
_trail.amount = 48
|
||||
_trail.lifetime = 0.48
|
||||
_trail.local_coords = false
|
||||
_trail.process_material = process
|
||||
_trail.draw_pass_1 = quad
|
||||
_trail.visibility_aabb = AABB(Vector3(-18, -18, -18), Vector3(36, 36, 36))
|
||||
_trail.emitting = false
|
||||
add_child(_trail)
|
||||
|
||||
|
||||
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
|
||||
|
||||
@@ -17,6 +17,16 @@ var hud: HUDController
|
||||
var ball: RigidBody3D
|
||||
var ships: Array[Ship] = []
|
||||
var _ship_spawn_transforms := {}
|
||||
var _hit_stop_generation := 0
|
||||
var _hit_stop_active := false
|
||||
var _time_scale_before_hit_stop := 1.0
|
||||
var _camera_rig: ShipCameraRig
|
||||
var _goal_slowmo_active := false
|
||||
var _time_scale_before_goal := 1.0
|
||||
var _goal_in_progress := false
|
||||
|
||||
const GOAL_CELEBRATION_SECONDS := 1.6
|
||||
const GOAL_SLOWMO_SCALE := 0.22
|
||||
|
||||
# Shared by modes that keep score (Match, Spectate); Free Play never
|
||||
# references this or emits a score_changed signal, and HUDController relies
|
||||
@@ -63,16 +73,58 @@ func _on_goal_scored(_conceding_team: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Virtual: immediate, non-presentational consequences at sensor time. Modes
|
||||
# with a score/clock override this so a last-second goal is authoritative
|
||||
# before the cinematic; reset/kickoff remains in _on_goal_scored afterwards.
|
||||
func _on_goal_registered(_conceding_team: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Debounce: a fast ball can re-trigger the goal area before the deferred
|
||||
# reset teleports it away, which would double-count the goal.
|
||||
var _goal_cooldown := false
|
||||
|
||||
func _handle_goal_scored(conceding_team: int) -> void:
|
||||
if _goal_cooldown:
|
||||
if _goal_in_progress:
|
||||
return
|
||||
_goal_cooldown = true
|
||||
get_tree().create_timer(0.5).timeout.connect(func(): _goal_cooldown = false)
|
||||
_on_goal_scored(conceding_team)
|
||||
_goal_in_progress = true
|
||||
_on_goal_registered(conceding_team)
|
||||
await _play_goal_celebration(1 - conceding_team, conceding_team)
|
||||
await _on_goal_scored(conceding_team)
|
||||
_goal_in_progress = false
|
||||
|
||||
|
||||
func _play_goal_celebration(scoring_team: int, conceding_team: int) -> void:
|
||||
# Training/headless modes never spawn a camera or HUD and must not pay a
|
||||
# real-time presentation delay between episodes.
|
||||
if DisplayServer.get_name() == "headless" or not is_instance_valid(_camera_rig):
|
||||
return
|
||||
_restore_hit_stop()
|
||||
_goal_slowmo_active = true
|
||||
_time_scale_before_goal = Engine.time_scale
|
||||
Engine.time_scale = minf(Engine.time_scale, GOAL_SLOWMO_SCALE)
|
||||
var goal_position := Vector3.ZERO
|
||||
for goal in arena.get_goals():
|
||||
if goal.team == conceding_team:
|
||||
goal_position = goal.global_position
|
||||
break
|
||||
_camera_rig.begin_goal_cut(goal_position)
|
||||
if hud:
|
||||
hud.show_goal_celebration(scoring_team)
|
||||
await get_tree().create_timer(GOAL_CELEBRATION_SECONDS, true, false, true).timeout
|
||||
if is_instance_valid(_camera_rig):
|
||||
_camera_rig.end_goal_cut()
|
||||
if hud and is_instance_valid(hud):
|
||||
hud.hide_goal_celebration()
|
||||
# Defensive unwind: impact feedback is suppressed while goal slow-mo owns
|
||||
# time scale, but restore any hit-stop that was already queued this frame.
|
||||
_restore_hit_stop()
|
||||
_restore_goal_slowmo()
|
||||
|
||||
|
||||
func _restore_goal_slowmo() -> void:
|
||||
if not _goal_slowmo_active:
|
||||
return
|
||||
Engine.time_scale = _time_scale_before_goal
|
||||
_goal_slowmo_active = false
|
||||
|
||||
|
||||
func spawn_ball() -> RigidBody3D:
|
||||
@@ -101,7 +153,9 @@ func spawn_ship(team: int, spawn_index: int = 0, controller: ShipController = nu
|
||||
func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||
var rig: ShipCameraRig = CAMERA_RIG_SCENE.instantiate()
|
||||
add_child(rig)
|
||||
_camera_rig = rig
|
||||
rig.target = target
|
||||
rig.impact_feedback.connect(_on_player_impact)
|
||||
# Also wires the scene's static HUD (if any) to the same ship, rather
|
||||
# than letting it guess via the "ship" group.
|
||||
if hud:
|
||||
@@ -109,6 +163,44 @@ func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||
return rig
|
||||
|
||||
|
||||
func _on_player_impact(intensity: float) -> void:
|
||||
if hud:
|
||||
hud.flash_impact(intensity)
|
||||
if not _goal_slowmo_active:
|
||||
_run_hit_stop(intensity)
|
||||
|
||||
|
||||
func _run_hit_stop(intensity: float) -> void:
|
||||
if _goal_slowmo_active:
|
||||
return
|
||||
_hit_stop_generation += 1
|
||||
var generation := _hit_stop_generation
|
||||
if not _hit_stop_active:
|
||||
_time_scale_before_hit_stop = Engine.time_scale
|
||||
_hit_stop_active = true
|
||||
Engine.time_scale = minf(
|
||||
Engine.time_scale, lerpf(0.22, 0.06, clampf(intensity, 0.0, 1.0))
|
||||
)
|
||||
await get_tree().create_timer(
|
||||
lerpf(0.025, 0.065, clampf(intensity, 0.0, 1.0)), true, false, true
|
||||
).timeout
|
||||
if generation == _hit_stop_generation:
|
||||
_restore_hit_stop()
|
||||
|
||||
|
||||
func _restore_hit_stop() -> void:
|
||||
if not _hit_stop_active:
|
||||
return
|
||||
Engine.time_scale = _time_scale_before_hit_stop
|
||||
_hit_stop_active = false
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# A scene change during the unscaled timer must never strand global time.
|
||||
_restore_hit_stop()
|
||||
_restore_goal_slowmo()
|
||||
|
||||
|
||||
# Given an already-resolved (path, reaction_ticks, action_noise) — callers
|
||||
# apply their own GameSettings-override logic first, which differs between
|
||||
# modes (Match lets GameSettings override all three fields, Spectate only
|
||||
|
||||
@@ -26,6 +26,8 @@ const RIM_DEPTH := 0.05
|
||||
|
||||
const NET_SHADER_PATH := "res://shaders/goal_net.gdshader"
|
||||
|
||||
var _goal_burst: GPUParticles3D
|
||||
|
||||
|
||||
func _ready():
|
||||
# Group lets AI controllers and game modes discover goals
|
||||
@@ -34,10 +36,13 @@ func _ready():
|
||||
# headless arenas that never render it. The sensor is unaffected.
|
||||
if DisplayServer.get_name() != "headless":
|
||||
_build_visuals()
|
||||
_build_goal_burst()
|
||||
|
||||
|
||||
func _on_body_entered(body):
|
||||
if body.is_in_group("ball"):
|
||||
if _goal_burst:
|
||||
_goal_burst.restart()
|
||||
goal_scored.emit(team)
|
||||
|
||||
|
||||
@@ -103,6 +108,43 @@ func _build_visuals() -> void:
|
||||
add_child(visuals)
|
||||
|
||||
|
||||
func _build_goal_burst() -> void:
|
||||
var tint: Color = TeamColors.TEAM_COLORS[team % TeamColors.TEAM_COLORS.size()]
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.albedo_color = Color(tint.r, tint.g, tint.b, 0.9)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = tint
|
||||
mat.emission_energy_multiplier = 3.0
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(0.38, 0.8)
|
||||
quad.material = mat
|
||||
var process := ParticleProcessMaterial.new()
|
||||
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
||||
process.emission_box_extents = Vector3(1.7, 0.7, 0.15)
|
||||
process.direction = Vector3(0, 0, -1)
|
||||
process.spread = 48.0
|
||||
process.initial_velocity_min = 8.0
|
||||
process.initial_velocity_max = 18.0
|
||||
process.gravity = Vector3(0, -2.0, 0)
|
||||
process.scale_min = 0.35
|
||||
process.scale_max = 1.2
|
||||
_goal_burst = GPUParticles3D.new()
|
||||
_goal_burst.name = "GoalBurst"
|
||||
_goal_burst.position = Vector3(0, 0, -0.15)
|
||||
_goal_burst.amount = 140
|
||||
_goal_burst.lifetime = 1.1
|
||||
_goal_burst.one_shot = true
|
||||
_goal_burst.explosiveness = 0.92
|
||||
_goal_burst.process_material = process
|
||||
_goal_burst.draw_pass_1 = quad
|
||||
_goal_burst.visibility_aabb = AABB(Vector3(-12, -8, -20), Vector3(24, 16, 24))
|
||||
_goal_burst.emitting = false
|
||||
add_child(_goal_burst)
|
||||
|
||||
|
||||
# Four bars around the mouth. The uprights run the full outer height so each
|
||||
# corner is covered exactly once.
|
||||
func _add_frame_ring(
|
||||
|
||||
@@ -73,7 +73,7 @@ func _draw() -> void:
|
||||
draw_line(Vector2(-half_chord, horizon_y), Vector2(half_chord, horizon_y), LINE_COLOR, 2.0)
|
||||
|
||||
# Pitch ladder: rungs every 10 degrees, labelled, scrolling with pitch.
|
||||
var font := ThemeDB.fallback_font
|
||||
var font := hud_font()
|
||||
for ladder_deg: int in [-30, -20, -10, 10, 20, 30]:
|
||||
var y := horizon_y - ladder_deg * px_per_deg
|
||||
if absf(y) > radius - 12.0:
|
||||
|
||||
@@ -37,7 +37,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
var font := hud_font()
|
||||
var fraction := clampf(_value / max_value, 0.0, 1.0)
|
||||
var reading := value_format % _value
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
var font := hud_font()
|
||||
var center_x := size.x / 2.0
|
||||
var px_per_deg := size.x / VISIBLE_RANGE
|
||||
|
||||
|
||||
@@ -23,3 +23,11 @@ static func angle_delta_deg(from: float, to: float) -> float:
|
||||
|
||||
func _smoothing_weight(delta: float) -> float:
|
||||
return 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
|
||||
|
||||
|
||||
# Procedural draw calls do not automatically consume a Control's theme font;
|
||||
# resolve it explicitly so every instrument uses the same packaged typeface
|
||||
# as ordinary Labels instead of ThemeDB's debug-looking fallback.
|
||||
func hud_font() -> Font:
|
||||
var font := get_theme_default_font()
|
||||
return font if font else ThemeDB.fallback_font
|
||||
|
||||
@@ -37,6 +37,8 @@ var match_timer: Timer
|
||||
var _in_overtime := false
|
||||
var _match_over := false
|
||||
var _last_timer_remaining := -1
|
||||
var _pending_scoring_team := -1
|
||||
var _match_time_after_goal := -1.0
|
||||
|
||||
|
||||
func _get_arena_scene_path() -> String:
|
||||
@@ -79,16 +81,32 @@ func _process(_delta):
|
||||
timer_updated.emit(floori(remaining / 60.0), remaining % 60)
|
||||
|
||||
|
||||
func _on_goal_scored(conceding_team: int) -> void:
|
||||
func _on_goal_registered(conceding_team: int) -> void:
|
||||
if _match_over:
|
||||
return
|
||||
var scoring_team := 1 - conceding_team
|
||||
_record_goal(scoring_team)
|
||||
_pending_scoring_team = 1 - conceding_team
|
||||
_record_goal(_pending_scoring_team)
|
||||
score_changed.emit(score.duplicate())
|
||||
# Commit the score and freeze regulation time at sensor entry. The clock
|
||||
# resumes after the cinematic, so a last-second goal cannot be discarded or
|
||||
# reclassified as overtime while presentation is still running.
|
||||
if match_timer and match_timer.time_left > 0.0:
|
||||
_match_time_after_goal = match_timer.time_left
|
||||
match_timer.stop()
|
||||
|
||||
|
||||
func _on_goal_scored(_conceding_team: int) -> void:
|
||||
if _match_over or _pending_scoring_team < 0:
|
||||
return
|
||||
var scoring_team := _pending_scoring_team
|
||||
_pending_scoring_team = -1
|
||||
if _in_overtime:
|
||||
# Golden goal: the first score after a draw ends the match outright.
|
||||
_end_match(scoring_team)
|
||||
return
|
||||
if match_timer and _match_time_after_goal > 0.0:
|
||||
match_timer.start(_match_time_after_goal)
|
||||
_match_time_after_goal = -1.0
|
||||
await _run_kickoff_countdown()
|
||||
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ signal altitude_changed(altitude: float)
|
||||
signal thrust_changed(thrust_percent: float)
|
||||
signal angular_velocity_changed(angular_speed: float)
|
||||
signal heading_changed(heading_degrees: float)
|
||||
signal ball_contact(intensity: float, world_position: Vector3)
|
||||
|
||||
# Performance optimization - track last emitted values to avoid unnecessary signals
|
||||
var _last_speed: float = -1.0
|
||||
@@ -115,6 +116,12 @@ const ANGULAR_THRESHOLD = 0.01 # rad/s
|
||||
const ATTITUDE_THRESHOLD = 1.0 # degrees
|
||||
const THRUST_THRESHOLD = 1.0 # percent
|
||||
|
||||
var _engine_cores: Array[MeshInstance3D] = []
|
||||
var _engine_plumes: Array[GPUParticles3D] = []
|
||||
var _turbo_flames: Array[GPUParticles3D] = []
|
||||
var _engine_lights: Array[OmniLight3D] = []
|
||||
var _impact_sparks: GPUParticles3D
|
||||
|
||||
|
||||
func _ready():
|
||||
# Add ship to group for instrument discovery
|
||||
@@ -148,6 +155,8 @@ func _ready():
|
||||
set_physics_process(false)
|
||||
else:
|
||||
_build_merged_hull()
|
||||
_build_movement_vfx()
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _apply_team_color() -> void:
|
||||
@@ -197,10 +206,147 @@ func set_controller(new_controller: ShipController) -> void:
|
||||
|
||||
|
||||
func _physics_process(_delta):
|
||||
_update_movement_vfx()
|
||||
if _has_telemetry_listeners():
|
||||
_emit_telemetry_data()
|
||||
|
||||
|
||||
func _build_movement_vfx() -> void:
|
||||
for x in [-0.42, 0.42]:
|
||||
var engine_pos := Vector3(x, -0.05, 1.42)
|
||||
|
||||
var core_mat := _vfx_material(Color(0.35, 0.85, 1.0, 1.0), 1.0)
|
||||
var core_mesh := SphereMesh.new()
|
||||
core_mesh.radius = 0.11
|
||||
core_mesh.height = 0.22
|
||||
core_mesh.material = core_mat
|
||||
var core := MeshInstance3D.new()
|
||||
core.name = "EngineCoreL" if x < 0.0 else "EngineCoreR"
|
||||
core.position = engine_pos
|
||||
core.mesh = core_mesh
|
||||
core.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(core)
|
||||
_engine_cores.append(core)
|
||||
|
||||
var plume := _make_particles(
|
||||
"EnginePlumeL" if x < 0.0 else "EnginePlumeR",
|
||||
Color(0.3, 0.82, 1.0, 0.72), Vector2(0.16, 0.42), 36, 0.24,
|
||||
Vector3(0, 0, 1), 12.0, 2.0, 5.0
|
||||
)
|
||||
plume.position = engine_pos + Vector3(0, 0, 0.08)
|
||||
add_child(plume)
|
||||
_engine_plumes.append(plume)
|
||||
|
||||
var turbo := _make_particles(
|
||||
"TurboFlameL" if x < 0.0 else "TurboFlameR",
|
||||
Color(0.78, 0.34, 1.0, 0.9), Vector2(0.22, 0.72), 48, 0.18,
|
||||
Vector3(0, 0, 1), 7.0, 6.0, 10.0
|
||||
)
|
||||
turbo.position = engine_pos + Vector3(0, 0, 0.12)
|
||||
add_child(turbo)
|
||||
_turbo_flames.append(turbo)
|
||||
|
||||
var light := OmniLight3D.new()
|
||||
light.name = "EngineLightL" if x < 0.0 else "EngineLightR"
|
||||
light.position = engine_pos
|
||||
light.light_color = Color(0.3, 0.75, 1.0)
|
||||
light.omni_range = 3.5
|
||||
light.omni_attenuation = 2.0
|
||||
light.shadow_enabled = false
|
||||
add_child(light)
|
||||
_engine_lights.append(light)
|
||||
|
||||
_impact_sparks = _make_particles(
|
||||
"ImpactSparks", Color(1.0, 0.72, 0.22, 1.0), Vector2(0.09, 0.3),
|
||||
28, 0.32, Vector3.UP, 180.0, 5.0, 12.0
|
||||
)
|
||||
_impact_sparks.one_shot = true
|
||||
_impact_sparks.explosiveness = 1.0
|
||||
_impact_sparks.local_coords = false
|
||||
_impact_sparks.emitting = false
|
||||
add_child(_impact_sparks)
|
||||
|
||||
|
||||
func _vfx_material(color: Color, energy: float) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.albedo_color = color
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(color.r, color.g, color.b)
|
||||
mat.emission_energy_multiplier = energy
|
||||
return mat
|
||||
|
||||
|
||||
func _make_particles(
|
||||
particle_name: String, color: Color, size: Vector2, amount: int,
|
||||
lifetime: float, direction: Vector3, spread: float,
|
||||
velocity_min: float, velocity_max: float
|
||||
) -> GPUParticles3D:
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = size
|
||||
quad.material = _vfx_material(color, 2.2)
|
||||
var process := ParticleProcessMaterial.new()
|
||||
process.direction = direction
|
||||
process.spread = spread
|
||||
process.initial_velocity_min = velocity_min
|
||||
process.initial_velocity_max = velocity_max
|
||||
process.gravity = Vector3.ZERO
|
||||
process.scale_min = 0.35
|
||||
process.scale_max = 1.0
|
||||
var particles := GPUParticles3D.new()
|
||||
particles.name = particle_name
|
||||
particles.amount = amount
|
||||
particles.lifetime = lifetime
|
||||
particles.local_coords = true
|
||||
particles.process_material = process
|
||||
particles.draw_pass_1 = quad
|
||||
particles.visibility_aabb = AABB(Vector3(-3, -3, -1), Vector3(6, 6, 12))
|
||||
particles.emitting = false
|
||||
return particles
|
||||
|
||||
|
||||
func _update_movement_vfx() -> void:
|
||||
if _engine_plumes.is_empty():
|
||||
return
|
||||
# These are the two rear main engines, so lateral/vertical maneuvering jets
|
||||
# must not make them flare. Reverse thrust also comes from separate attitude
|
||||
# jets conceptually; only positive Z drives this rear-facing plume.
|
||||
var thrust := clampf(maxf(_current_action.thrust.z, 0.0), 0.0, 1.0)
|
||||
var turbo := _current_action.turbo and thrust > 0.05
|
||||
for i in _engine_plumes.size():
|
||||
_engine_plumes[i].emitting = thrust > 0.02
|
||||
_engine_plumes[i].amount_ratio = 0.25 + thrust * 0.75
|
||||
_engine_plumes[i].speed_scale = 0.65 + thrust * 0.75
|
||||
_turbo_flames[i].emitting = turbo
|
||||
_engine_lights[i].light_energy = 0.35 + thrust * 2.1 + (2.3 if turbo else 0.0)
|
||||
var core_mat := _engine_cores[i].mesh.material as StandardMaterial3D
|
||||
core_mat.emission_energy_multiplier = 0.65 + thrust * 2.0 + (2.0 if turbo else 0.0)
|
||||
_engine_cores[i].scale = Vector3.ONE * (0.8 + thrust * 0.3 + (0.25 if turbo else 0.0))
|
||||
|
||||
|
||||
func get_speed_ratio() -> float:
|
||||
return clampf(linear_velocity.length() / maxf(max_speed, 0.001), 0.0, 1.0)
|
||||
|
||||
|
||||
func is_turbo_active() -> bool:
|
||||
return _current_action.turbo and _current_action.thrust.z > 0.05
|
||||
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if not body is Ball:
|
||||
return
|
||||
var relative_speed := (linear_velocity - (body as Ball).linear_velocity).length()
|
||||
var intensity := clampf(inverse_lerp(3.0, 24.0, relative_speed), 0.12, 1.0)
|
||||
var contact_pos := (global_position + (body as Ball).global_position) * 0.5
|
||||
if _impact_sparks:
|
||||
_impact_sparks.position = to_local(contact_pos)
|
||||
_impact_sparks.amount_ratio = lerpf(0.25, 1.0, intensity)
|
||||
_impact_sparks.restart()
|
||||
ball_contact.emit(intensity, contact_pos)
|
||||
|
||||
|
||||
# Covers a second/AI ship with no HUD watching it - headless mode is already
|
||||
# handled by disabling _physics_process entirely in _ready.
|
||||
func _has_telemetry_listeners() -> bool:
|
||||
|
||||
+134
-3
@@ -7,6 +7,7 @@ extends Node3D
|
||||
# themselves are camera-free (a headless/AI ship never needs one).
|
||||
|
||||
signal camera_mode_changed(is_ball_cam: bool)
|
||||
signal impact_feedback(intensity: float)
|
||||
|
||||
@export var camera_distance := 8.0
|
||||
@export var camera_height := 3.0
|
||||
@@ -14,22 +15,66 @@ signal camera_mode_changed(is_ball_cam: bool)
|
||||
@export var orbit_smoothing := 6.0 # How fast the camera swings around the ship in ball cam
|
||||
@export var look_smoothing := 20.0 # How fast the camera re-centres its look target
|
||||
@export var min_camera_height := 1.0 # Keeps the camera from dipping through the arena floor
|
||||
@export_group("Speed Feel")
|
||||
@export var base_fov := 70.0
|
||||
@export var speed_fov_add := 11.0
|
||||
@export var turbo_fov_kick := 7.0
|
||||
@export var turbo_distance_kick := 0.9
|
||||
@export var feel_smoothing := 7.0
|
||||
@export_group("Impact Shake")
|
||||
@export var max_shake_offset := 0.32
|
||||
@export var shake_decay := 8.0
|
||||
|
||||
var target: Ship
|
||||
var target: Ship:
|
||||
set(value):
|
||||
if target == value:
|
||||
return
|
||||
if is_instance_valid(target) and target.ball_contact.is_connected(_on_target_ball_contact):
|
||||
target.ball_contact.disconnect(_on_target_ball_contact)
|
||||
target = value
|
||||
_connect_target()
|
||||
var ball_cam_enabled := true
|
||||
|
||||
@onready var camera: Camera3D = $Camera3D
|
||||
@onready var post_material: ShaderMaterial = $PostProcess/PostFX.material as ShaderMaterial
|
||||
|
||||
var _ball: Node3D
|
||||
# Smoothed horizontal direction from ball to ship; the ball-cam orbits along
|
||||
# this so the camera swings smoothly around the ship instead of chasing a
|
||||
# raw position (which whips when ball and ship are close together).
|
||||
var _orbit_dir := Vector3.BACK
|
||||
var _turbo_blend := 0.0
|
||||
var _speed_blend := 0.0
|
||||
var _effective_distance := 8.0
|
||||
var _shake_strength := 0.0
|
||||
var _shake_noise := FastNoiseLite.new()
|
||||
var _shake_time := 0.0
|
||||
var _last_shake_offset := Vector3.ZERO
|
||||
var _goal_cut_active := false
|
||||
var _goal_cut_position := Vector3.ZERO
|
||||
var _goal_cut_look_at := Vector3.ZERO
|
||||
|
||||
|
||||
func _ready():
|
||||
# Group lets the HUD discover the rig for the camera-mode instrument
|
||||
add_to_group("ship_camera")
|
||||
camera.fov = base_fov
|
||||
_effective_distance = camera_distance
|
||||
_shake_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
_shake_noise.frequency = 2.5
|
||||
_connect_target()
|
||||
|
||||
|
||||
func _connect_target() -> void:
|
||||
if not is_inside_tree() or not is_instance_valid(target):
|
||||
return
|
||||
if not target.ball_contact.is_connected(_on_target_ball_contact):
|
||||
target.ball_contact.connect(_on_target_ball_contact)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if is_instance_valid(target) and target.ball_contact.is_connected(_on_target_ball_contact):
|
||||
target.ball_contact.disconnect(_on_target_ball_contact)
|
||||
|
||||
|
||||
func _input(event):
|
||||
@@ -39,8 +84,17 @@ func _input(event):
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
# Camera position smoothing operates on the unshaken chase position. Remove
|
||||
# last frame's presentation-only offset first so shake never accumulates or
|
||||
# exceeds its configured amplitude during a low-time-scale hit-stop.
|
||||
camera.global_position -= _last_shake_offset
|
||||
_last_shake_offset = Vector3.ZERO
|
||||
if not is_instance_valid(target):
|
||||
return
|
||||
if _goal_cut_active:
|
||||
_smooth_look_at(_goal_cut_look_at, delta)
|
||||
return
|
||||
_update_speed_feel(delta)
|
||||
var ball := _get_ball()
|
||||
if ball_cam_enabled and ball:
|
||||
_update_ball_cam(delta, ball)
|
||||
@@ -77,11 +131,12 @@ func _update_ball_cam(delta, ball: Node3D):
|
||||
var swing := _orbit_dir.signed_angle_to(flat.normalized(), Vector3.UP)
|
||||
_orbit_dir = _orbit_dir.rotated(Vector3.UP, swing * t).normalized()
|
||||
|
||||
var camera_target_pos := ship_pos + _orbit_dir * camera_distance + Vector3.UP * camera_height
|
||||
var camera_target_pos := ship_pos + _orbit_dir * _effective_distance + Vector3.UP * camera_height
|
||||
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
|
||||
|
||||
var pos_t := 1.0 - exp(-camera_smoothing * delta)
|
||||
camera.global_position = camera.global_position.lerp(camera_target_pos, pos_t)
|
||||
_apply_shake(delta)
|
||||
|
||||
# Look slightly above the ball's centre; look smoothing is faster than
|
||||
# position smoothing so the ball never drifts out of frame while the
|
||||
@@ -95,11 +150,12 @@ func _update_ship_cam(delta):
|
||||
var ship_forward: Vector3 = -target.global_transform.basis.z
|
||||
|
||||
# Position camera behind and above the ship
|
||||
var camera_target_pos := ship_pos - ship_forward * camera_distance + Vector3.UP * camera_height
|
||||
var camera_target_pos := ship_pos - ship_forward * _effective_distance + Vector3.UP * camera_height
|
||||
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
|
||||
|
||||
var pos_t := 1.0 - exp(-camera_smoothing * delta)
|
||||
camera.global_position = camera.global_position.lerp(camera_target_pos, pos_t)
|
||||
_apply_shake(delta)
|
||||
|
||||
# Look ahead of the ship
|
||||
_smooth_look_at(ship_pos + ship_forward * 10.0, delta)
|
||||
@@ -115,3 +171,78 @@ func _smooth_look_at(point: Vector3, delta: float) -> void:
|
||||
var look_basis := Basis.looking_at(dir, Vector3.UP)
|
||||
var look_t := 1.0 - exp(-look_smoothing * delta)
|
||||
camera.global_basis = camera.global_basis.slerp(look_basis, look_t).orthonormalized()
|
||||
|
||||
|
||||
func _update_speed_feel(delta: float) -> void:
|
||||
var feel_t := 1.0 - exp(-feel_smoothing * delta)
|
||||
var turbo_target := 1.0 if target.is_turbo_active() else 0.0
|
||||
_turbo_blend = lerpf(_turbo_blend, turbo_target, feel_t)
|
||||
_speed_blend = lerpf(_speed_blend, target.get_speed_ratio(), feel_t)
|
||||
var target_fov := base_fov + _speed_blend * speed_fov_add + _turbo_blend * turbo_fov_kick
|
||||
camera.fov = lerpf(camera.fov, target_fov, feel_t)
|
||||
_effective_distance = lerpf(
|
||||
_effective_distance, camera_distance + _turbo_blend * turbo_distance_kick, feel_t
|
||||
)
|
||||
post_material.set_shader_parameter(
|
||||
"motion_blur_amount", _speed_blend * _speed_blend * 0.006 + _turbo_blend * 0.006
|
||||
)
|
||||
post_material.set_shader_parameter("chromatic_aberration", _turbo_blend * 0.007)
|
||||
post_material.set_shader_parameter("vignette_strength", 0.22 + _turbo_blend * 0.16)
|
||||
|
||||
|
||||
func _on_target_ball_contact(intensity: float, _world_position: Vector3) -> void:
|
||||
_shake_strength = maxf(_shake_strength, clampf(intensity, 0.0, 1.0))
|
||||
for device in Input.get_connected_joypads():
|
||||
Input.start_joy_vibration(
|
||||
device, lerpf(0.18, 0.65, intensity), lerpf(0.32, 1.0, intensity),
|
||||
lerpf(0.08, 0.2, intensity)
|
||||
)
|
||||
impact_feedback.emit(intensity)
|
||||
|
||||
|
||||
func _apply_shake(delta: float) -> void:
|
||||
if _shake_strength <= 0.001:
|
||||
_shake_strength = 0.0
|
||||
return
|
||||
_shake_time += delta * 60.0
|
||||
var amplitude := max_shake_offset * _shake_strength * _shake_strength
|
||||
var noise := Vector2(
|
||||
_shake_noise.get_noise_1d(_shake_time),
|
||||
_shake_noise.get_noise_1d(_shake_time + 100.0)
|
||||
).limit_length(1.0) * amplitude
|
||||
_last_shake_offset = camera.global_basis.x * noise.x + camera.global_basis.y * noise.y
|
||||
camera.global_position += _last_shake_offset
|
||||
_shake_strength = move_toward(_shake_strength, 0.0, shake_decay * delta)
|
||||
|
||||
|
||||
func begin_goal_cut(goal_position: Vector3) -> void:
|
||||
_goal_cut_active = true
|
||||
# The hard cut replaces the chase camera's presentation offset entirely;
|
||||
# don't let the next physics frame subtract a pre-cut shake sample from the
|
||||
# new cinematic position.
|
||||
camera.global_position -= _last_shake_offset
|
||||
_last_shake_offset = Vector3.ZERO
|
||||
var inward := Vector3(-goal_position.x, 0.0, -goal_position.z)
|
||||
if inward.length_squared() < 0.01:
|
||||
inward = Vector3.BACK
|
||||
else:
|
||||
inward = inward.normalized()
|
||||
_goal_cut_position = goal_position + inward * 10.5 + Vector3.UP * 5.5
|
||||
_goal_cut_look_at = goal_position + Vector3.UP * 0.8
|
||||
camera.global_position = _goal_cut_position
|
||||
camera.fov = 58.0
|
||||
camera.look_at(_goal_cut_look_at, Vector3.UP)
|
||||
# A hard, stationary cinematic camera must not inherit the scoring frame's
|
||||
# ship-speed smear or turbo chroma. Keep only a deliberate cinematic edge.
|
||||
post_material.set_shader_parameter("motion_blur_amount", 0.0)
|
||||
post_material.set_shader_parameter("chromatic_aberration", 0.0)
|
||||
post_material.set_shader_parameter("vignette_strength", 0.3)
|
||||
|
||||
|
||||
func end_goal_cut() -> void:
|
||||
_goal_cut_active = false
|
||||
_speed_blend = 0.0
|
||||
_turbo_blend = 0.0
|
||||
post_material.set_shader_parameter("motion_blur_amount", 0.0)
|
||||
post_material.set_shader_parameter("chromatic_aberration", 0.0)
|
||||
post_material.set_shader_parameter("vignette_strength", 0.22)
|
||||
|
||||
@@ -31,10 +31,13 @@ func _start() -> void:
|
||||
spawn_camera_rig(ship_a)
|
||||
|
||||
|
||||
func _on_goal_scored(conceding_team: int) -> void:
|
||||
func _on_goal_registered(conceding_team: int) -> void:
|
||||
var scoring_team := 1 - conceding_team
|
||||
_record_goal(scoring_team)
|
||||
score_changed.emit(score.duplicate())
|
||||
|
||||
|
||||
func _on_goal_scored(_conceding_team: int) -> void:
|
||||
reset_ball()
|
||||
reset_ships()
|
||||
|
||||
|
||||
@@ -14,11 +14,6 @@ uniform vec3 deck_color : source_color = vec3(0.085, 0.095, 0.122);
|
||||
// Kept close to deck_color on purpose: per-plate variation is meant to break
|
||||
// up a 24x36 m plane, not to reintroduce visible tiling.
|
||||
uniform vec3 panel_color : source_color = vec3(0.104, 0.116, 0.146);
|
||||
// Stand-in for bounce light. The arena is lit by one dim directional plus low
|
||||
// ambient, so hull facing away from it — the goal bulkheads, the fillet caps
|
||||
// either side of each mouth — otherwise crushed to pure black and the whole
|
||||
// end zone read as a void with a goal floating in it.
|
||||
uniform float hull_fill : hint_range(0.0, 2.0) = 0.55;
|
||||
uniform float panel_variation : hint_range(0.0, 1.0) = 0.4;
|
||||
uniform vec3 line_color : source_color = vec3(0.62, 0.78, 1.0);
|
||||
// Must track TeamColors.TEAM_COLORS in scripts/team_colors.gd — shaders can't
|
||||
@@ -47,9 +42,9 @@ uniform float seam_width = 0.22;
|
||||
uniform float end_zone_strength : hint_range(0.0, 1.0) = 0.12;
|
||||
uniform float panel_size = 4.0;
|
||||
uniform float panel_line_width : hint_range(0.0, 0.5) = 0.05;
|
||||
// Deliberately not a high metallic value: this arena is lit by one dim
|
||||
// directional plus low ambient, and a near-metal deck has almost no diffuse
|
||||
// response, which crushed the plating to black.
|
||||
// Deliberately not a high metallic value: the local pitch lights and SDFGI
|
||||
// need enough diffuse response to reveal the plating instead of turning it
|
||||
// into a dark mirror at grazing angles.
|
||||
uniform float deck_metallic : hint_range(0.0, 1.0) = 0.3;
|
||||
uniform float deck_roughness : hint_range(0.0, 1.0) = 0.34;
|
||||
|
||||
@@ -135,8 +130,7 @@ void fragment() {
|
||||
ALBEDO = mix(albedo, line_color, marks);
|
||||
METALLIC = deck_metallic * (1.0 - marks);
|
||||
ROUGHNESS = clamp(mix(rough, 0.5, marks), 0.05, 1.0);
|
||||
EMISSION = albedo * hull_fill
|
||||
+ line_color * marks * line_emission
|
||||
EMISSION = line_color * marks * line_emission
|
||||
+ seam_color * seam * seam_emission
|
||||
+ zone * 0.015 * flat_deck;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;
|
||||
uniform float motion_blur_amount : hint_range(0.0, 0.03) = 0.0;
|
||||
uniform float chromatic_aberration : hint_range(0.0, 0.02) = 0.0;
|
||||
uniform float vignette_strength : hint_range(0.0, 1.0) = 0.22;
|
||||
|
||||
|
||||
vec4 motion_sample(vec2 uv) {
|
||||
// A restrained centre-directed velocity smear. The camera script scales it
|
||||
// with ship speed/turbo; mipmapped screen reads soften the outer samples.
|
||||
if (motion_blur_amount < 0.0001) {
|
||||
return textureLod(screen_texture, uv, 0.0);
|
||||
}
|
||||
vec2 step_uv = (uv - vec2(0.5)) * motion_blur_amount;
|
||||
vec4 color = textureLod(screen_texture, uv, 0.0) * 0.36;
|
||||
color += textureLod(screen_texture, uv - step_uv * 0.35, 0.5) * 0.25;
|
||||
color += textureLod(screen_texture, uv - step_uv * 0.7, 1.0) * 0.21;
|
||||
color += textureLod(screen_texture, uv - step_uv, 1.5) * 0.18;
|
||||
return color;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec2 radial = UV - vec2(0.5);
|
||||
vec2 chroma = radial * chromatic_aberration;
|
||||
vec4 center = motion_sample(UV);
|
||||
vec3 color = center.rgb;
|
||||
// The branch is driven by one uniform for the whole draw, so non-turbo
|
||||
// frames avoid eight redundant screen taps without causing warp divergence.
|
||||
if (chromatic_aberration > 0.0001) {
|
||||
color.r = motion_sample(UV + chroma).r;
|
||||
color.b = motion_sample(UV - chroma).b;
|
||||
}
|
||||
|
||||
// Aspect-correct vignette, always subtle and tightened slightly on turbo.
|
||||
vec2 aspect_radial = radial;
|
||||
aspect_radial.x *= SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x;
|
||||
float vignette = smoothstep(0.38, 0.78, length(aspect_radial));
|
||||
color *= 1.0 - vignette * vignette_strength;
|
||||
COLOR = vec4(color, 1.0);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cx1lpjodunjhx
|
||||
@@ -0,0 +1,29 @@
|
||||
[gd_resource type="Theme" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="FontFile" path="res://assets/fonts/Orbitron-Medium.ttf" id="1_font"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
||||
bg_color = Color(0.035, 0.05, 0.085, 0.76)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.3, 0.7, 1, 0.38)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 8.0
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("1_font")
|
||||
default_font_size = 14
|
||||
Label/colors/font_color = Color(0.86, 0.94, 1, 1)
|
||||
Label/colors/font_shadow_color = Color(0, 0, 0, 0.72)
|
||||
Label/constants/outline_size = 1
|
||||
Label/constants/shadow_offset_x = 2
|
||||
Label/constants/shadow_offset_y = 2
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_panel")
|
||||
@@ -15,15 +15,15 @@ The training pipeline is built — see `TRAINING.md` (self-play PPO via the vend
|
||||
The largest gap between this and a AAA-feeling product is presentation, not code. Sequenced after the above for pragmatic reasons, but this is the highest impact per hour.
|
||||
|
||||
- [ ] **Audio — there is none.** Zero sound files, zero `AudioStreamPlayer` nodes, no bus layout. Needs: engine hum pitched to throttle, turbo whoosh, ball impacts scaled by collision impulse, wall scrapes, goal explosion, crowd bed, UI clicks, countdown beeps, music. Can be driven off `Ship`'s existing telemetry signals.
|
||||
- [ ] SSAO/SSIL in the arena Environments — cheapest single perceived-quality win available; grounds the ships against the deck and gives the fillets and goal recesses real depth.
|
||||
- [ ] VFX on anything that moves: `EngineGlowL/R` are static meshes that don't respond to throttle. No thruster plume, turbo flame, ball trail, impact sparks or goal burst. `arena_02` is the only scene with any particles at all.
|
||||
- [ ] Impact feedback — screen shake, hit-stop, flash, controller rumble on ball contact.
|
||||
- [ ] Camera feel in `ship_camera.gd` — fixed distance/height/FOV today. Speed-based FOV widening, turbo kick, impact shake.
|
||||
- [ ] Goal celebration sequence: today it's a `print()` and a label scale-pop. Wants an explosion, team-tinted screen flash, camera cut, slow-mo, title card. `HUDController`'s `ResultOverlay` animation is a reasonable template.
|
||||
- [ ] Local lighting / LightmapGI bake — two directional lights and low ambient, no local lights anywhere. `arena_deck.gdshader`'s `hull_fill` uniform is explicitly a "stand-in for bounce light", i.e. the shader is compensating for lighting that isn't there. The arena is fully static, so a bake is viable; retire `hull_fill` afterwards.
|
||||
- [ ] Dress `arena_03` — its `Decoration` node is empty, while `arena_02` has stations, debris, a planet and volumetric dust.
|
||||
- [ ] Custom font + a real `Theme` resource for the HUD. The procedural instruments are well-engineered, but `ThemeDB.fallback_font` at 10-13 px reads as a debug overlay.
|
||||
- [ ] Post-processing beyond glow: DoF, motion blur, vignette, chromatic aberration on turbo.
|
||||
- [x] SSAO/SSIL in the arena Environments — cheapest single perceived-quality win available; grounds the ships against the deck and gives the fillets and goal recesses real depth.
|
||||
- [x] VFX on anything that moves: throttle-reactive engine cores and plumes, distinct turbo flames, a speed-scaled ball trail, contact sparks and team-tinted goal bursts.
|
||||
- [x] Impact feedback — intensity-scaled screen shake, unscaled hit-stop, HUD flash and controller rumble on ball contact.
|
||||
- [x] Camera feel in `ship_camera.gd` — speed-based FOV widening, turbo kick-back and decaying impact shake.
|
||||
- [x] Goal celebration sequence: goal burst, team-tinted flash/title card, dedicated camera cut and slow-motion beat after authoritative score registration and before reset/kickoff.
|
||||
- [x] Local lighting / dynamic GI — four local pitch lights plus SDFGI provide bounce and contact lighting for the runtime-generated arena shell; the shader's `hull_fill` stand-in is retired. (The shell is generated in `_ready`, so it cannot participate in an editor LightmapGI bake.)
|
||||
- [x] Dress `arena_03` — its asteroid field now has mining stations, debris clusters and a distant planet.
|
||||
- [x] Custom font + a real `Theme` resource for the HUD. Packaged Orbitron typography, shared label styling and themed procedural-instrument text replace `ThemeDB.fallback_font`.
|
||||
- [x] Post-processing beyond glow: camera DoF plus speed-scaled motion blur, a cinematic vignette and turbo-driven chromatic aberration.
|
||||
|
||||
## Multiplayer (long term)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user