first commit

This commit is contained in:
Valera 2025-06-11 20:19:35 +07:00
commit 35790cbd34
107 changed files with 3400 additions and 0 deletions

100
.editorconfig Normal file
View File

@ -0,0 +1,100 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.{cs,razor}]
indent_style = tab
indent_size = 4
tab_size = 4
# New line preferences
end_of_line = crlf
insert_final_newline = true
#### C# Coding Conventions ####
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Code-block preferences
csharp_prefer_braces = true:silent
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = no_change
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = true
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = true
csharp_space_between_parentheses = control_flow_statements
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# This file describes files and paths that should not be tracked by Git version control
# https://git-scm.com/docs/gitignore
# Auto-generated code editor files
.vs/*
.vscode/*
*.csproj
obj
bin
Properties/*
code/obj/*
code/Properties/*
# Auto-generated asset related files
.sbox/*
*.generated.*
*.*_c
!*.shader_c
*.los
*.vpk
*launchSettings.json
*.sln
*idea
# Exported / standalone games
Exports/

View File

@ -0,0 +1,22 @@
"Layer0"
{
"shader" "shaders/complex.shader"
"g_flAmbientOcclusionDirectDiffuse" "0.000000"
"g_flAmbientOcclusionDirectSpecular" "0.000000"
"TextureAmbientOcclusion" "materials/default/default_ao.tga"
"g_flModelTintAmount" "1.000000"
"g_vColorTint" "[1.000000 1.000000 1.000000 0.000000]"
"TextureColor" "materials/default/default_color.tga"
"g_flFadeExponent" "1.000000"
"g_bFogEnabled" "1"
"g_flDirectionalLightmapMinZ" "0.050000"
"g_flDirectionalLightmapStrength" "1.000000"
"g_flMetalness" "0.000000"
"TextureNormal" "materials/default/default_normal.tga"
"TextureRoughness" "materials/default/default_rough.tga"
"g_nScaleTexCoordUByModelScaleAxis" "0"
"g_nScaleTexCoordVByModelScaleAxis" "0"
"g_vTexCoordOffset" "[0.000 0.000]"
"g_vTexCoordScale" "[1.000 1.000]"
"g_vTexCoordScrollSpeed" "[0.000 0.000]"
}

View File

@ -0,0 +1,363 @@
{
"RootObject": {
"__guid": "bbc29007-9ae0-455d-9bde-62bae3dd88e9",
"__version": 1,
"Flags": 0,
"Name": "metal_impact",
"Position": "0,0,0",
"Rotation": "0,0,0,1",
"Scale": "1,1,1",
"Tags": "particles",
"Enabled": true,
"NetworkMode": 2,
"NetworkInterpolation": true,
"NetworkOrphaned": 0,
"OwnerTransfer": 1,
"Components": [
{
"__type": "Sandbox.ParticleEffect",
"__guid": "4e3d505f-af05-4c8f-85ee-c5a264ff23b6",
"__enabled": true,
"__version": 1,
"Alpha": {
"Type": "Curve",
"Evaluation": "Life",
"CurveA": [
{
"x": 0,
"y": 1,
"in": 0,
"out": 0,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"CurveB": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"Constants": "1,0,0,0"
},
"ApplyAlpha": false,
"ApplyColor": true,
"ApplyRotation": true,
"ApplyShape": true,
"Bounce": 1,
"Brightness": 1,
"Bumpiness": 0,
"Collision": false,
"CollisionIgnore": "player,vehicle",
"CollisionPrefab": [],
"CollisionPrefabAlign": false,
"CollisionPrefabChance": 1,
"CollisionPrefabRotation": 0,
"CollisionRadius": 1,
"Damping": 0,
"DieOnCollisionChance": 0,
"FollowerPrefab": [],
"FollowerPrefabChance": 1,
"FollowerPrefabKill": true,
"Force": true,
"ForceDirection": "0,0,-300",
"ForceScale": 1,
"ForceSpace": "World",
"Friction": 1,
"Gradient": {
"Type": "Constant",
"Evaluation": "Particle",
"GradientA": {
"blend": "Linear",
"color": [
{
"t": 0,
"c": "0.79581,0.8093,0,1"
},
{
"t": 0.33,
"c": "1,0.93333,0,1"
},
{
"t": 0.66,
"c": "1,0.64706,0,1"
},
{
"t": 1,
"c": "1,1,0,1"
},
{
"t": 1,
"c": "1,0.55,0,1"
}
],
"alpha": [
{
"t": 0,
"a": 1
},
{
"t": 1,
"a": 1
}
]
},
"GradientB": {
"blend": "Linear",
"color": [
{
"t": 0.5,
"c": "1,1,1,1"
}
],
"alpha": []
},
"ConstantA": "1,1,1,1",
"ConstantB": "1,0,0,1"
},
"Lifetime": {
"Type": "Range",
"Evaluation": "Particle",
"CurveA": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"CurveB": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"Constants": "0.1,2,0,0"
},
"MaxParticles": 500,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"OnParticleCreated": null,
"OnParticleDestroyed": null,
"OrbitalForce": {
"X": 0,
"Y": 0,
"Z": 0
},
"OrbitalPull": 0,
"PerParticleTimeScale": 1,
"Pitch": 0,
"PreWarm": 0,
"PushStrength": 0,
"Roll": {
"Type": "Range",
"Evaluation": "Particle",
"CurveA": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"CurveB": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"Constants": "0,360,0,0"
},
"Scale": {
"Type": "Curve",
"Evaluation": "Life",
"CurveA": [
{
"x": 0,
"y": 0,
"in": -9.333329,
"out": 9.333329,
"mode": "Mirrored"
},
{
"x": 0.12527578,
"y": 1,
"in": -0.056075174,
"out": 0.056075174,
"mode": "Mirrored"
},
{
"x": 0.36042944,
"y": 0.40260965,
"in": 1.4467248,
"out": -1.4467248,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": -0.39285707,
"out": 0.39285707,
"mode": "Mirrored"
}
],
"CurveB": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"Constants": "1,0,0,0"
},
"SequenceId": 0,
"SequenceSpeed": 1,
"SequenceTime": 1,
"SheetSequence": false,
"Space": "World",
"StartDelay": 0,
"StartVelocity": {
"Type": "Range",
"Evaluation": "Particle",
"CurveA": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"CurveB": [
{
"x": 0.5,
"y": 0.5,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"Constants": "10,70,0,0"
},
"Stretch": 0,
"TimeScale": 1,
"Timing": "GameTime",
"Tint": "1,1,1,1",
"UsePrefabFeature": false,
"Yaw": 0
},
{
"__type": "Sandbox.ParticleSpriteRenderer",
"__guid": "53350178-5acf-40f4-82a7-fc9cf56f9209",
"__enabled": true,
"Additive": false,
"Alignment": "LookAtCamera",
"BlurAmount": 1,
"BlurOpacity": 0.61,
"BlurSpacing": 1,
"DepthFeather": 0,
"FaceVelocity": false,
"FogStrength": 1,
"LeadingTrail": true,
"Lighting": true,
"MotionBlur": true,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"Opaque": false,
"Pivot": "0.5,0.5",
"RenderOptions": {
"GameLayer": true,
"OverlayLayer": false,
"BloomLayer": false,
"AfterUILayer": false
},
"RotationOffset": 0,
"Scale": 1,
"Shadows": false,
"SortMode": "Unsorted",
"Texture": "materials/particles/shapes/spark2.vtex"
},
{
"__type": "Sandbox.ParticleConeEmitter",
"__guid": "e190fd95-5525-4287-9587-ad1a46170627",
"__enabled": true,
"Burst": 50,
"CenterBias": 0,
"CenterBiasVelocity": 0,
"ConeAngle": 30,
"ConeFar": 1,
"ConeNear": 1,
"Delay": 0,
"DestroyOnEnd": true,
"Duration": 1,
"InVolume": false,
"Loop": false,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"OnEdge": false,
"Rate": 0,
"RateOverDistance": 0,
"VelocityMultiplier": 1,
"VelocityRandom": 0
}
],
"Children": [],
"__properties": {
"NetworkInterpolation": true,
"TimeScale": 1,
"WantsSystemScene": true,
"Metadata": {},
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
},
"__variables": []
},
"ResourceVersion": 2,
"ShowInMenu": false,
"MenuPath": null,
"MenuIcon": null,
"DontBreakAsTemplate": false,
"__references": [],
"__version": 2
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,59 @@
{
"UI": false,
"Volume": "1",
"Pitch": "0.8 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/collisions/car_heavy_6.vsnd",
"sounds/collisions/car_heavy_5.vsnd",
"sounds/collisions/car_heavy_4.vsnd",
"sounds/collisions/car_heavy_3.vsnd",
"sounds/collisions/car_heavy_2.vsnd",
"sounds/collisions/car_heavy_1.vsnd",
"sounds/collisions/car_heavy_7.vsnd",
"sounds/collisions/car_heavy_8.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 8500,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,55 @@
{
"UI": false,
"Volume": "1",
"Pitch": "0.8 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/collisions/car_light_4.vsnd",
"sounds/collisions/car_light_3.vsnd",
"sounds/collisions/car_light_2.vsnd",
"sounds/collisions/car_light_1.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 8500,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,53 @@
{
"UI": false,
"Volume": "1",
"Pitch": "0.8 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/collisions/metal_scrape_1.vsnd",
"sounds/collisions/metal_scrape_2.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 8500,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "music",
"Id": "ee991a4a-8016-40e5-93e6-cbf0d964a401"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,139 @@
{
"Parameters": {
"Pitch": 1,
"Volume": 1,
"FadeDist": 1500,
"RedlineFrequency": 55,
"RedlineStrength": 0.2,
"WobbleFrequency": 25,
"WobbleStrength": 0.13
},
"Layers": {
"rpm_high": {
"AudioPath": "sounds/streams/pickup/high.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
0,
1
],
[
"RpmFraction",
0,
1,
"Pitch",
0.49,
1.05
],
[
"RpmFraction",
0.2,
0.75,
"Volume",
0,
1
]
]
},
"idle": {
"AudioPath": "sounds/streams/pickup/idle.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
1,
0
],
[
"RpmFraction",
0,
0.3,
"Volume",
1,
0
],
[
"RpmFraction",
0,
0.38,
"Pitch",
1,
2
]
]
},
"rpm_low": {
"AudioPath": "sounds/streams/pickup/low.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
0,
1
],
[
"RpmFraction",
0,
1,
"Pitch",
0.75,
1.59
],
[
"RpmFraction",
0.3,
0.85,
"Volume",
1,
0
]
]
},
"throttle_low": {
"AudioPath": "sounds/streams/pickup/throttle_low.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0.35,
1,
"Volume",
0.85,
0
],
[
"RpmFraction",
0,
1,
"Pitch",
0.63,
1.33
],
[
"RpmFraction",
0,
0.2,
"Volume",
0,
1
]
]
}
},
"Parent": null,
"Throttle": 0,
"RpmFraction": 0,
"IsRedlining": false,
"__references": [],
"__version": 0
}

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,8 @@
{
"loop": true,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@ -0,0 +1,174 @@
{
"Parameters": {
"Pitch": 1,
"Volume": 1,
"FadeDist": 1500,
"RedlineFrequency": 55,
"RedlineStrength": 0.2,
"WobbleFrequency": 25,
"WobbleStrength": 0.13
},
"Layers": {
"mid": {
"AudioPath": "sounds/streams/v8_c63/acc_4130_adpcm.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
0.1,
1.1
],
[
"RpmFraction",
0,
1,
"Pitch",
0.71,
1.26
],
[
"RpmFraction",
0.79,
1,
"Volume",
1,
0
],
[
"RpmFraction",
0.05,
0.5,
"Volume",
0,
1.1
]
]
},
"throttle_low": {
"AudioPath": "sounds/streams/v8_c63/dec_3951_adpcm.vsnd",
"UseRedline": true,
"Controllers": [
[
"RpmFraction",
0,
1,
"Pitch",
0.74,
1.3
],
[
"Throttle",
0,
1,
"Volume",
1,
0.1
],
[
"RpmFraction",
0,
0.25,
"Volume",
0,
0.8
]
]
},
"low": {
"AudioPath": "sounds/streams/v8_c63/acc_2197_adpcm.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
0,
1.05
],
[
"RpmFraction",
0,
1,
"Pitch",
0.9,
1.59
],
[
"RpmFraction",
0.2,
0.75,
"Volume",
1,
0
]
]
},
"idle": {
"AudioPath": "sounds/streams/v8_c63/acc_1560_adpcm.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
1,
0
],
[
"RpmFraction",
0,
0.4,
"Volume",
1.2,
0
],
[
"RpmFraction",
0,
0.4,
"Pitch",
1,
2
]
]
},
"high": {
"AudioPath": "sounds/streams/v8_c63/acc_6803_adpcm.vsnd",
"UseRedline": true,
"Controllers": [
[
"Throttle",
0,
1,
"Volume",
0.35,
1
],
[
"RpmFraction",
0.5,
1,
"Volume",
0,
1.15
],
[
"RpmFraction",
0,
1,
"Pitch",
0.57,
1
]
]
}
},
"Paused": false,
"__references": [],
"__version": 0
}

View File

@ -0,0 +1,54 @@
{
"UI": false,
"Volume": "1",
"Pitch": "1 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/suspension/compress_heavy_3.vsnd",
"sounds/suspension/compress_heavy_2.vsnd",
"sounds/suspension/compress_heavy_1.vsnd"
],
"Occlusion": false,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 8500,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,55 @@
{
"UI": false,
"Volume": "1",
"Pitch": "1",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/suspension/compress_truck_4.vsnd",
"sounds/suspension/compress_truck_3.vsnd",
"sounds/suspension/compress_truck_2.vsnd",
"sounds/suspension/compress_truck_1.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 15000,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "unknown",
"Id": "00000000-0000-0000-0000-000000000000"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,54 @@
{
"UI": false,
"Volume": "1",
"Pitch": "1 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/suspension/pneumatic_down_3.vsnd",
"sounds/suspension/pneumatic_down_2.vsnd",
"sounds/suspension/pneumatic_down_1.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 7000,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,54 @@
{
"UI": false,
"Volume": "1",
"Pitch": "1 1.2",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/suspension/pneumatic_up_3.vsnd",
"sounds/suspension/pneumatic_up_2.vsnd",
"sounds/suspension/pneumatic_up_1.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 7000,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,54 @@
{
"UI": false,
"Volume": "1",
"Pitch": "1 1.1",
"Decibels": 70,
"SelectionMode": "Random",
"Sounds": [
"sounds/suspension/stress_3.vsnd",
"sounds/suspension/stress_2.vsnd",
"sounds/suspension/stress_1.vsnd"
],
"Occlusion": true,
"AirAbsorption": true,
"Transmission": true,
"OcclusionRadius": 64,
"DistanceAttenuation": true,
"Distance": 8500,
"Falloff": [
{
"x": 0,
"y": 1,
"in": 0,
"out": -1.8,
"mode": "Mirrored"
},
{
"x": 0.05,
"y": 0.22,
"in": 3.5,
"out": -3.5,
"mode": "Mirrored"
},
{
"x": 0.2,
"y": 0.04,
"in": 0.16,
"out": -0.16,
"mode": "Mirrored"
},
{
"x": 1,
"y": 0,
"in": 0,
"out": 0,
"mode": "Mirrored"
}
],
"DefaultMixer": {
"Name": "game",
"Id": "37db45ef-34ec-4948-b1d7-4509bc8e97e2"
},
"__references": [],
"__version": 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,11 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase
{
//[Property, Feature( "Input" ), InputAction] string ThrottleInput { get; set; } = "Forward";
[Property, Feature( "Input" )] internal InputResolver Input { get; set; } = new();
[Property, Feature( "Input" )] public GameObject Driver { get => Input.Driver; set => Input.Driver = value; }
}

View File

@ -0,0 +1,43 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase
{
private Vector3 linForce;
private Vector3 angForce;
private void PhysicsSimulate()
{
var drag = AngularDrag;
var mass = Body.Mass;
var angVel = Body.AngularVelocity;
linForce.x = 0;
linForce.y = 0;
linForce.z = 0;
angForce.x = angVel.x * drag.x * mass;
angForce.y = angVel.y * drag.y * mass;
angForce.z = angVel.z * drag.z * mass;
if ( Wheels.Count > 0 )
{
Vector3 vehVel = Body.Velocity;
Vector3 vehAngVel = Body.AngularVelocity;
var dt = Time.Delta;
foreach ( var v in Wheels )
v.DoPhysics( this, ref vehVel, ref vehAngVel, ref linForce, ref angForce, in dt );
Body.Velocity = vehVel;
Body.AngularVelocity = vehAngVel;
}
Body.ApplyForce( linForce );
Body.ApplyTorque( angForce );
}
}

View File

@ -0,0 +1,62 @@
using Sandbox;
using Sandbox.Audio;
using System;
using System.Text.Json.Serialization;
using VeloX.Audio;
using static Sandbox.Utility.Noise;
using static VeloX.EngineStream;
namespace VeloX;
public abstract partial class VeloXBase : Component, Component.ICollisionListener
{
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionHeavySound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/compress_heavy.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionDownSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/pneumatic_down.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionUpSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/pneumatic_up.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SoftCollisionSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/car_light.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent HardCollisionSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/car_heavy.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent VehicleScrapeSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/metal_scrape.sound" );
[Property, Feature( "Effects" ), Group( "Particle" )]
public GameObject MetalImpactEffect { get; protected set; } = GameObject.GetPrefab( "effects/metal_impact.prefab" );
void ICollisionListener.OnCollisionStart( Collision collision )
{
var speed = MathF.Abs( collision.Contact.NormalSpeed );
var surfaceNormal = collision.Contact.Normal;
if ( speed < 100 )
return;
var isHardHit = speed > 300;
var volume = Math.Clamp( speed / 400f, 0, 1 );
var sound = Sound.Play( SoftCollisionSound, WorldPosition );
sound.Volume = volume;
var a = MetalImpactEffect.Clone( new CloneConfig() { StartEnabled = true, Transform = new( collision.Contact.Point, surfaceNormal.Cross( surfaceNormal ).EulerAngles ) } );
a.Components.Get<ParticleConeEmitter>().Burst = speed / 2;
if ( isHardHit )
{
var hardSound = Sound.Play( HardCollisionSound, WorldPosition );
hardSound.Volume = volume;
}
else if ( surfaceNormal.Dot( -collision.Contact.Speed.Normal ) < 0.5f )
{
var scrapSound = Sound.Play( VehicleScrapeSound, WorldPosition );
scrapSound.Volume = 0.4f;
}
}
}

View File

@ -0,0 +1,20 @@
using Sandbox;
using System.Collections.Generic;
namespace VeloX;
public abstract partial class VeloXBase
{
[Property] public List<VeloXWheel> Wheels { get; set; }
[Property] public TagSet WheelIgnoredTags { get; set; }
public List<VeloXWheel> FindWheels() => [.. Components.GetAll<VeloXWheel>()];
[Button]
public void SetupWheels()
{
Wheels = FindWheels();
}
}

38
Code/Base/VeloXBase.cs Normal file
View File

@ -0,0 +1,38 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase : Component
{
[Sync] public EngineState EngineState { get; set; }
[Sync] public WaterState WaterState { get; set; }
[Sync] public bool IsEngineOnFire { get; set; }
[Sync, Range( 0, 1 ), Property] public float Brake { get; set; }
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property] public float Throttle { get; set; }
[Property] public Vector3 AngularDrag { get; set; } = new( -0.1f, -0.1f, -3 );
[Property] public float Mass { get; set; } = 900;
[Property, Group( "Components" )] public Rigidbody Body { get; protected set; }
[Property, Group( "Components" )] public Collider Collider { get; protected set; }
[Sync] public Angles SteerAngle { get; set; }
public Vector3 LocalVelocity;
public float ForwardSpeed;
public float TotalSpeed;
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
Body.PhysicsBody.Mass = Mass;
PhysicsSimulate();
}
}

View File

@ -0,0 +1,74 @@
using Sandbox;
namespace VeloX;
public partial class VeloXWheel : Component
{
protected override void DrawGizmos()
{
if ( !Gizmo.IsSelected )
return;
Gizmo.Draw.IgnoreDepth = true;
//
// Suspension length
//
{
var suspensionStart = Vector3.Zero;
var suspensionEnd = Vector3.Zero + Vector3.Down * SuspensionLength;
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineThickness = 0.25f;
Gizmo.Draw.Line( suspensionStart, suspensionEnd );
Gizmo.Draw.Line( suspensionStart + Vector3.Forward, suspensionStart + Vector3.Backward );
Gizmo.Draw.Line( suspensionEnd + Vector3.Forward, suspensionEnd + Vector3.Backward );
}
var widthOffset = Vector3.Right * Width * 0.5f;
//
// Wheel radius
//
{
Gizmo.Draw.LineThickness = 0.5f;
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius, Radius, 16 );
}
//
// Wheel width
//
{
var circlePosition = Vector3.Zero;
Gizmo.Draw.LineThickness = 0.25f;
Gizmo.Draw.Color = Color.White;
for ( float i = 0; i < 16; i++ )
{
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius;
Gizmo.Draw.Line( new Line( pos - widthOffset, pos + widthOffset ) );
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius;
Gizmo.Draw.Line( pos - widthOffset, pos2 + widthOffset );
}
}
////
//// Forward direction
////
//{
// var arrowStart = Vector3.Forward * Radius;
// var arrowEnd = arrowStart + Vector3.Forward * 8f;
// Gizmo.Draw.Color = Color.Red;
// Gizmo.Draw.Arrow( arrowStart, arrowEnd, 4, 1 );
//}
}
}

View File

@ -0,0 +1,18 @@
using Sandbox;
namespace VeloX;
public partial class VeloXWheel
{
[Property] float BrakePowerMax { get; set; } = 3000;
[Property, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
[Property, Group( "Traction" )] float ForwardTractionMax { get; set; } = 2600;
[Property, Group( "Traction" )] float SideTractionMultiplier { get; set; } = 20;
[Property, Group( "Traction" )] float SideTractionMaxAng { get; set; } = 25;
[Property, Group( "Traction" )] float SideTractionMax { get; set; } = 2400;
[Property, Group( "Traction" )] float SideTractionMin { get; set; } = 800;
}

View File

@ -0,0 +1,240 @@
using Sandbox;
using System;
using System.Runtime.Intrinsics.Arm;
using System.Text.RegularExpressions;
namespace VeloX;
[Group( "VeloX" )]
[Title( "VeloX - Wheel" )]
public partial class VeloXWheel : Component
{
[Property] public float Radius { get; set; } = 15;
[Property] public float Width { get; set; } = 6;
[Sync] public float SideSlip { get; private set; }
[Sync] public float ForwardSlip { get; private set; }
[Sync, Range( 0, 1 )] public float BrakePower { get; set; }
[Sync] public float Torque { get; set; }
[Sync, Range( 0, 1 )] public float Brake { get; set; }
[Property] public bool IsFront { get; protected set; }
[Property] public float SteerMultiplier { get; set; }
public float Spin { get; private set; }
public float RPM { get => angularVelocity * 60f / MathF.Tau; set => angularVelocity = value / (60 / MathF.Tau); }
internal float DistributionFactor { get; set; }
private Vector3 StartPos { get; set; }
private static Rotation CylinderOffset => Rotation.FromRoll( 90 );
public SceneTraceResult Trace { get; private set; }
public bool IsOnGround => Trace.Hit;
private float lastSpringOffset;
private Vector2 tractionCycle;
private float angularVelocity;
private float lastFraction;
private RealTimeUntil expandSoundCD;
private RealTimeUntil contractSoundCD;
protected override void OnAwake()
{
base.OnAwake();
if ( StartPos.IsNearZeroLength )
StartPos = LocalPosition;
}
private static float TractionRamp( float slipAngle, float sideTractionMaxAng, float sideTractionMax, float sideTractionMin )
{
sideTractionMaxAng /= 90; // Convert max slip angle to the 0 - 1 range
var x = (slipAngle - sideTractionMaxAng) / (1 - sideTractionMaxAng);
return slipAngle < sideTractionMaxAng ? sideTractionMax : (sideTractionMax * (1 - x)) + (sideTractionMin * x);
}
private void DoSuspensionSounds( VeloXBase vehicle, float change )
{
if ( change > 0.1f && expandSoundCD )
{
expandSoundCD = 0.3f;
var sound = Sound.Play( vehicle.SuspensionUpSound, WorldPosition );
sound.Volume = Math.Clamp( Math.Abs( change ) * 5f, 0, 0.5f );
}
if ( change < -0.1f && contractSoundCD )
{
contractSoundCD = 0.3f;
change = MathF.Abs( change );
var sound = Sound.Play( change > 0.3f ? vehicle.SuspensionHeavySound : vehicle.SuspensionDownSound, WorldPosition );
sound.Volume = Math.Clamp( change * 5f, 0, 1 );
}
}
internal void Update( VeloXBase vehicle, in float dt )
{
UpdateVisuals( vehicle, dt );
if ( !IsOnGround )
{
angularVelocity += (Torque / 20) * dt;
angularVelocity = MathX.Approach( angularVelocity, 0, dt * 4 );
}
}
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
//Rotate the wheel around the axle axis
Spin = (Spin - MathX.RadianToDegree( angularVelocity ) * dt) % 360;
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, Spin );
}
public void DoPhysics( VeloXBase vehicle, ref Vector3 vehVel, ref Vector3 vehAngVel, ref Vector3 outLin, ref Vector3 outAng, in float dt )
{
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );
var fw = ang.Forward;
var rt = ang.Right;
var up = ang.Up;
var maxLen = SuspensionLength;
var endPos = pos + ang.Down * maxLen;
Trace = Scene.Trace.IgnoreGameObjectHierarchy( vehicle.GameObject )
.FromTo( pos, endPos )
.Cylinder( Width, Radius )
.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
.UseRenderMeshes( false )
.UseHitPosition( false )
.WithoutTags( vehicle.WheelIgnoredTags )
.Run();
var fraction = Trace.Fraction;
var contactPos = pos - maxLen * fraction * up;
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
DoSuspensionSounds( vehicle, fraction - lastFraction );
lastFraction = fraction;
if ( !IsOnGround )
{
SideSlip = 0;
ForwardSlip = 0;
return;
}
var normal = Trace.Normal;
var vel = vehicle.Body.GetVelocityAtPoint( pos );
// Split that velocity among our local directions
var velF = fw.Dot( vel );
var velR = rt.Dot( vel );
var velU = normal.Dot( vel );
var absVelR = Math.Abs( velR );
//Make forward forces be perpendicular to the surface normal
fw = normal.Cross( rt );
//Suspension spring force &damping
var offset = maxLen - (fraction * maxLen);
var springForce = (offset * SpringStrength);
var damperForce = (lastSpringOffset - offset) * SpringDamper;
lastSpringOffset = offset;
var force = (springForce - damperForce) * up.Dot( normal ) * normal;
//If the suspension spring is going to be fully compressed on the next frame...
if ( velU < 0 && offset + Math.Abs( velU * dt ) > SuspensionLength )
{
var (linearVel, angularVel) = vehicle.Body.PhysicsBody.CalculateVelocityOffset( (-velU / dt) * normal, pos );
vehVel += linearVel;
vehAngVel += angularVel;
}
//Rolling resistance
force += 0.05f * -velF * fw;
//Brake and torque forces
var surfaceGrip = 1;
var maxTraction = ForwardTractionMax * surfaceGrip * 1;
//Grip loss logic
var brakeForce = MathX.Clamp( -velF, -Brake, Brake ) * BrakePowerMax * surfaceGrip;
var forwardForce = Torque + brakeForce;
var signForwardForce = forwardForce > 0 ? 1 : (forwardForce < 0 ? -1 : 0);
// Given an amount of sideways slippage( up to the max.traction )
// and the forward force, calculate how much grip we are losing.
tractionCycle.x = Math.Min( absVelR, maxTraction );
tractionCycle.y = forwardForce;
var gripLoss = Math.Max( tractionCycle.Length - maxTraction, 0 );
// Reduce the forward force by the amount of grip we lost,
// but still allow some amount of brake force to apply regardless.
forwardForce += -(gripLoss * signForwardForce) + MathX.Clamp( brakeForce * 0.5f, -maxTraction, maxTraction );
force += fw * forwardForce;
// Get how fast the wheel would be spinning if it had never lost grip
var groundAngularVelocity = MathF.Tau * (velF / (Radius * MathF.Tau));
// Add our grip loss to our spin velocity
var _angvel = groundAngularVelocity + gripLoss * (Torque > 0 ? 1 : (Torque < 0 ? -1 : 0));
// Smoothly match our current angular velocity to the angular velocity affected by grip loss
angularVelocity = MathX.Approach( angularVelocity, _angvel, dt * 200 );
ForwardSlip = groundAngularVelocity - angularVelocity;
// Calculate side slip angle
var slipAngle = MathF.Atan2( velR, MathF.Abs( velF ) ) / MathF.PI * 2;
SideSlip = slipAngle * MathX.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 ) * 2;
//Sideways traction ramp
slipAngle = MathF.Abs( slipAngle * slipAngle );
maxTraction = TractionRamp( slipAngle, SideTractionMaxAng, SideTractionMax, SideTractionMin );
var sideForce = -rt.Dot( vel * SideTractionMultiplier );
// Reduce sideways traction force as the wheel slips forward
sideForce *= 1 - Math.Clamp( MathF.Abs( gripLoss ) * 0.1f, 0, 1 ) * 0.9f;
// Apply sideways traction force
force += Math.Clamp( sideForce, -maxTraction, maxTraction ) * surfaceGrip * rt;
force += velR * SideTractionMultiplier * -0.1f * rt;
//Apply the forces at the axle / ground contact position
vehicle.Body.ApplyForceAt( pos, force / dt );
}
}

358
Code/Car/VeloXCar.Engine.cs Normal file
View File

@ -0,0 +1,358 @@
using Sandbox;
using System;
using System.Collections.Generic;
namespace VeloX;
public partial class VeloXCar
{
[Property, Feature( "Engine" )] public EngineStream Stream { get; set; }
public EngineStreamPlayer StreamPlayer { get; set; }
[Property, Feature( "Engine" )] public float MinRPM { get; set; } = 800;
[Property, Feature( "Engine" )] public float MaxRPM { get; set; } = 7000;
[Property, Feature( "Engine" ), Range( -1, 1 )]
public float PowerDistribution
{
get => powerDistribution; set
{
powerDistribution = value;
UpdatePowerDistribution();
}
}
[Property, Feature( "Engine" )] public float FlyWheelMass { get; set; } = 80f;
[Property, Feature( "Engine" )] public float FlyWheelRadius { get; set; } = 0.5f;
[Property, Feature( "Engine" )] public float FlywheelFriction { get; set; } = -6000;
[Property, Feature( "Engine" )] public float FlywheelTorque { get; set; } = 20000;
[Property, Feature( "Engine" )] public float EngineBrakeTorque { get; set; } = 2000;
[Property, Feature( "Engine" )]
public Dictionary<int, float> Gears { get; set; } = new()
{
[-1] = 2.5f,
[0] = 0f,
[1] = 2.8f,
[2] = 1.7f,
[3] = 1.2f,
[4] = 0.9f,
[5] = 0.75f,
[6] = 0.7f
};
[Property, Feature( "Engine" )] public float DifferentialRatio { get; set; } = 1f;
[Property, Feature( "Engine" ), Range( 0, 1 )] public float TransmissionEfficiency { get; set; } = 0.8f;
[Property, Feature( "Engine" )] private float MinRPMTorque { get; set; } = 5000f;
[Property, Feature( "Engine" )] private float MaxRPMTorque { get; set; } = 8000f;
[Sync] public int Gear { get; set; } = 0;
[Sync] public float Clutch { get; set; } = 1;
[Sync( SyncFlags.Interpolate )] public float EngineRPM { get; set; }
public float RPMPercent => (EngineRPM - MinRPM) / MaxRPM;
private const float TAU = MathF.Tau;
private int MinGear { get; set; }
private int MaxGear { get; set; }
[Sync] public bool IsRedlining { get; private set; }
private float flywheelVelocity;
private TimeUntil switchCD = 0;
private float groundedCount;
private float burnout;
private float frontBrake;
private float rearBrake;
private float availableFrontTorque;
private float availableRearTorque;
private float avgSideSlip;
private float avgPoweredRPM;
private float avgForwardSlip;
private float inputThrottle, inputBrake;
private bool inputHandbrake;
private float transmissionRPM;
private float powerDistribution;
public float FlywheelRPM
{
get => flywheelVelocity * 60 / TAU;
set
{
flywheelVelocity = value * TAU / 60; EngineRPM = value;
}
}
private void UpdateGearList()
{
int minGear = 0;
int maxGear = 0;
foreach ( var (gear, ratio) in Gears )
{
if ( gear < minGear )
minGear = gear;
if ( gear > maxGear )
maxGear = gear;
if ( minGear != 0 || maxGear != 0 )
{
SwitchGear( 0, false );
}
}
MinGear = minGear;
MaxGear = maxGear;
}
public void SwitchGear( int index, bool cooldown = true )
{
if ( Gear == index ) return;
index = Math.Clamp( index, MinGear, MaxGear );
if ( index == 0 || !cooldown )
switchCD = 0;
else
switchCD = 0.3f;
Clutch = 1;
Gear = index;
}
public float TransmissionToEngineRPM( int gear ) => avgPoweredRPM * Gears[gear] * DifferentialRatio * 60 / TAU;
public float GetTransmissionMaxRPM( int gear ) => FlywheelRPM / Gears[gear] / DifferentialRatio;
private void UpdatePowerDistribution()
{
if ( Wheels is null ) return;
int frontCount = 0, rearCount = 0;
foreach ( var wheel in Wheels )
{
if ( wheel.IsFront )
frontCount++;
else
rearCount++;
}
float frontDistribution = 0.5f + PowerDistribution * 0.5f;
float rearDistribution = 1 - frontDistribution;
frontDistribution /= frontCount;
rearDistribution /= rearCount;
foreach ( var wheel in Wheels )
if ( wheel.IsFront )
wheel.DistributionFactor = frontDistribution;
else
wheel.DistributionFactor = rearDistribution;
}
private void EngineAccelerate( float torque, float dt )
{
var inertia = 0.5f * FlyWheelMass * FlyWheelRadius * FlyWheelRadius;
var angularAcceleration = torque / inertia;
flywheelVelocity += angularAcceleration * dt;
}
private float GetTransmissionTorque( int gear, float minTorque, float maxTorque )
{
var torque = FlywheelRPM.Remap( MinRPM, MaxRPM, minTorque, maxTorque, true );
torque *= (1 - Clutch);
torque = torque * Gears[gear] * DifferentialRatio * TransmissionEfficiency;
return gear == -1 ? -torque : torque;
}
private void AutoGearSwitch()
{
if ( ForwardSpeed < 100 && Input.Down( "Backward" ) )
{
SwitchGear( -1, false );
return;
}
var currentGear = Gear;
if ( currentGear < 0 && ForwardSpeed < -100 )
return;
if ( Math.Abs( avgForwardSlip ) > 10 )
return;
var gear = Math.Clamp( currentGear, 1, MaxGear );
float minRPM = MinRPM, maxRPM = MaxRPM;
maxRPM *= 0.98f;
float gearRPM;
for ( int i = 1; i <= MaxGear; i++ )
{
gearRPM = TransmissionToEngineRPM( i );
if ( (i == 1 && gearRPM < minRPM) || (gearRPM > minRPM && gearRPM < maxRPM) )
{
gear = i;
break;
}
}
var threshold = minRPM + (maxRPM - minRPM) * (0.5 - Throttle * 0.3);
if ( gear < currentGear && gear > currentGear - 2 && EngineRPM > threshold )
return;
SwitchGear( gear );
}
private float EngineClutch( float dt )
{
if ( !switchCD )
{
inputThrottle = 0;
return 0;
}
if ( inputHandbrake )
return 1;
var absForwardSpeed = Math.Abs( ForwardSpeed );
if ( groundedCount < 1 && absForwardSpeed > 30 )
return 1;
if ( ForwardSpeed < -50 && inputBrake > 0 && Gear < 0 )
return 1;
if ( absForwardSpeed > 200 )
return 0;
return inputThrottle > 0.1f ? 0 : 1;
}
private void EngineThink( float dt )
{
inputThrottle = Input.Down( "Forward" ) ? 1 : 0;
inputBrake = Input.Down( "Backward" ) ? 1 : 0;
inputHandbrake = Input.Down( "Jump" );
if ( burnout > 0 )
{
SwitchGear( 1, false );
if ( inputThrottle < 0.1f || inputBrake < 0.1f )
burnout = 0;
}
else
AutoGearSwitch();
if ( Gear < 0 )
(inputBrake, inputThrottle) = (inputThrottle, inputBrake);
var rpm = FlywheelRPM;
var clutch = EngineClutch( dt );
if ( inputThrottle > 0.1 && inputBrake > 0.1 && Math.Abs( ForwardSpeed ) < 50 )
{
burnout = MathX.Approach( burnout, 1, dt * 2 );
Clutch = 0;
}
else if ( inputHandbrake )
{
frontBrake = 0f;
rearBrake = 0.5f;
Clutch = 1;
clutch = 1;
}
else
{
if ( (Gear == -1 || Gear == 1) && inputThrottle < 0.05f && inputBrake < 0.1f && groundedCount > 1 && rpm < MinRPM * 1.2f )
inputBrake = 0.2f;
frontBrake = inputBrake * 0.5f;
rearBrake = inputBrake * 0.5f;
}
clutch = MathX.Approach( Clutch, clutch, dt * ((Gear < 2 && inputThrottle > 0.1f) ? 6 : 2) );
Clutch = clutch;
var isRedlining = false;
transmissionRPM = 0;
if ( Gear != 0 )
{
transmissionRPM = TransmissionToEngineRPM( Gear );
transmissionRPM = Gear < 0 ? -transmissionRPM : transmissionRPM;
rpm = (rpm * clutch) + (MathF.Max( 0, transmissionRPM ) * (1 - clutch));
}
var throttle = Throttle;
var gearTorque = GetTransmissionTorque( Gear, MinRPMTorque, MaxRPMTorque );
var availableTorque = gearTorque * throttle;
if ( transmissionRPM < 0 )
{
availableTorque += gearTorque * 2;
}
else
{
var engineBrakeTorque = GetTransmissionTorque( Gear, EngineBrakeTorque, EngineBrakeTorque );
availableTorque -= engineBrakeTorque * (1 - throttle) * 0.5f;
}
var maxRPM = MaxRPM;
if ( rpm < MinRPM )
{
rpm = MinRPM;
}
else if ( rpm > maxRPM )
{
if ( rpm > maxRPM * 1.2f )
availableTorque = 0;
rpm = maxRPM;
if ( Gear != MaxGear || groundedCount < Wheels.Count )
isRedlining = true;
}
FlywheelRPM = Math.Clamp( rpm, 0, maxRPM );
if ( burnout > 0 )
availableTorque += availableTorque * burnout * 0.1f;
var front = 0.5f + PowerDistribution * 0.5f;
var rear = 1 - front;
availableFrontTorque = availableTorque * front;
availableRearTorque = availableTorque * rear;
throttle = MathX.Approach( throttle, inputThrottle, dt * 4 );
EngineAccelerate( FlywheelFriction + FlywheelTorque * throttle, dt );
Throttle = throttle;
IsRedlining = (isRedlining && inputThrottle > 0);
}
public void StreamEngineUpdate()
{
}
}

View File

@ -0,0 +1,46 @@
using Sandbox;
using System;
using System.Threading;
namespace VeloX;
public partial class VeloXCar
{
public static float ExpDecay( float a, float b, float decay, float dt ) => b + (a - b) * MathF.Exp( -decay * dt );
[Property, Feature( "Steer" )] public float SteerConeMaxSpeed { get; set; } = 1800;
[Property, Feature( "Steer" )] public float SteerConeMaxAngle { get; set; } = 0.25f;
[Property, Feature( "Steer" )] public float SteerConeChangeRate { get; set; } = 8;
[Property, Feature( "Steer" )] public float CounterSteer { get; set; } = 0.1f;
[Property, Feature( "Steer" )] public float MaxSteerAngle { get; set; } = 35f;
[Sync] public float Steering { get; private set; }
private float jTurnMultiplier;
private float inputSteer;
private void UpdateSteering( float dt )
{
var inputSteer = Input.AnalogMove.y;
var absInputSteer = Math.Abs( inputSteer );
var sideSlip = Math.Clamp( avgSideSlip, -1, 1 );
var steerConeFactor = Math.Clamp( TotalSpeed / SteerConeMaxSpeed, 0, 1 );
var steerCone = 1 - steerConeFactor * (1 - SteerConeMaxAngle);
steerCone = Math.Clamp( steerCone, Math.Abs( sideSlip ), 1 );
inputSteer = ExpDecay( this.inputSteer, inputSteer * steerCone, SteerConeChangeRate, dt );
this.inputSteer = inputSteer;
var counterSteer = sideSlip * steerConeFactor * (1 - absInputSteer);
counterSteer = Math.Clamp( counterSteer, -1, 1 ) * CounterSteer;
inputSteer = Math.Clamp( inputSteer + counterSteer, -1, 1 );
Steering = inputSteer;
SteerAngle = new( 0, inputSteer * MaxSteerAngle, 0 );
if ( ForwardSpeed < -100 )
jTurnMultiplier = 0.5f;
else
jTurnMultiplier = ExpDecay( jTurnMultiplier, 1, 2, dt );
}
}

View File

@ -0,0 +1,46 @@
namespace VeloX;
public partial class VeloXCar
{
private void WheelThink( in float dt )
{
var maxRPM = GetTransmissionMaxRPM( Gear );
var frontTorque = availableFrontTorque;
var rearTorque = availableRearTorque;
groundedCount = 0;
float avgRPM = 0, totalSideSlip = 0, totalForwardSlip = 0;
foreach ( var w in Wheels )
{
w.Update( this, dt );
totalSideSlip += w.SideSlip;
totalForwardSlip += w.ForwardSlip;
var rpm = w.RPM;
avgRPM += rpm * w.DistributionFactor;
w.Torque = w.DistributionFactor * (w.IsFront ? frontTorque : rearTorque);
w.Brake = w.IsFront ? frontBrake : rearBrake;
if ( inputHandbrake && !w.IsFront )
w.RPM = 0;
if ( rpm > maxRPM )
w.RPM = maxRPM;
if ( w.IsOnGround )
groundedCount++;
}
avgPoweredRPM = avgRPM;
avgSideSlip = totalSideSlip / Wheels.Count;
avgForwardSlip = totalForwardSlip / Wheels.Count;
}
}

60
Code/Car/VeloXCar.cs Normal file
View File

@ -0,0 +1,60 @@
using Sandbox;
using System;
namespace VeloX;
[Group( "VeloX" )]
[Title( "VeloX - Car" )]
public partial class VeloXCar : VeloXBase
{
protected override void OnAwake()
{
base.OnAwake();
StreamPlayer = new( Stream );
}
protected override void OnStart()
{
base.OnStart();
if ( !IsProxy )
{
UpdateGearList();
UpdatePowerDistribution();
}
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( StreamPlayer is not null )
{
StreamPlayer.Throttle = Throttle;
StreamPlayer.RPMPercent = RPMPercent;
StreamPlayer.EngineState = EngineState;
StreamPlayer.IsRedlining = IsRedlining;
if ( Driver.IsValid() && Driver.IsProxy )
StreamPlayer.Update( Time.Delta, Scene.Camera.WorldPosition );
else
StreamPlayer.Update( Time.Delta, WorldPosition );
}
}
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
base.OnFixedUpdate();
var dt = Time.Delta;
EngineThink( dt );
WheelThink( dt );
UpdateSteering( dt );
Brake = Math.Clamp( frontBrake + rearBrake + (Input.Down( "Jump" ) ? 1 : 0), 0, 1 );
}
}

9
Code/Enum/EngineState.cs Normal file
View File

@ -0,0 +1,9 @@
namespace VeloX;
public enum EngineState
{
Off,
Starting,
Running,
ShuttingDown,
}

9
Code/Enum/WaterState.cs Normal file
View File

@ -0,0 +1,9 @@
namespace VeloX;
public enum WaterState
{
NotOnWater, // 0: Не на воде
PartialContact, // 1: Хотя бы одна точка плавучести на воде
HalfSubmerged, // 2: Хотя бы половина точек плавучести на воде
FullySubmerged // 3: Полностью погружён
}

View File

@ -0,0 +1,47 @@
using Sandbox;
namespace VeloX;
public class InputResolver
{
public GameObject Driver { get; internal set; }
private bool IsDriverActive => Driver.IsValid();
public Vector2 MouseDelta => IsDriverActive ? Input.MouseDelta : default;
public Vector2 MouseWheel => IsDriverActive ? Input.MouseWheel : default;
public Angles AnalogLook => IsDriverActive ? Input.AnalogLook : default;
public Vector3 AnalogMove => IsDriverActive ? Input.AnalogMove : default;
public bool Down( string action )
{
return IsDriverActive && Input.Down( action );
}
public bool Pressed( string action )
{
return IsDriverActive && Input.Pressed( action );
}
public bool Released( string action )
{
return IsDriverActive && Input.Released( action );
}
public void TriggerHaptics( float leftMotor, float rightMotor, float leftTrigger = 0f, float rightTrigger = 0f, int duration = 500 )
{
if ( IsDriverActive )
{
Input.TriggerHaptics( leftMotor, rightMotor, leftTrigger, rightTrigger, duration );
}
}
public void StopAllHaptics()
{
if ( IsDriverActive )
{
Input.StopAllHaptics();
}
}
}

View File

@ -0,0 +1,87 @@
using Sandbox;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace VeloX.Audio;
[JsonConverter( typeof( ControllerConverter ) )]
public sealed class Controller
{
public enum InputTypes
{
Throttle,
RpmFraction,
}
public enum OutputTypes
{
Volume,
Pitch,
}
public required InputTypes InputParameter { get; set; }
[Property] public ValueRange InputRange { get; set; }
public required OutputTypes OutputParameter { get; set; }
public ValueRange OutputRange { get; set; }
}
public record struct ValueRange
{
public ValueRange( float inputMin, float inputMax )
{
Min = inputMin;
Max = inputMax;
}
[Range( 0, 1 )] public float Min { get; set; }
[Range( 0, 1 )] public float Max { get; set; }
public readonly float Remap( float value, float newMin, float newMax )
{
var normalized = (value - Min) / (Max - Min);
return newMin + normalized * (newMax - newMin);
}
};
public sealed class ControllerConverter : JsonConverter<Controller>
{
public override Controller Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
if ( reader.TokenType != JsonTokenType.StartArray )
throw new JsonException( "Expected array for Controller" );
reader.Read();
string inputParam = reader.GetString() ?? string.Empty;
reader.Read();
float inputMin = reader.GetSingle();
reader.Read();
float inputMax = reader.GetSingle();
reader.Read();
string outputParam = reader.GetString() ?? string.Empty;
reader.Read();
float outputMin = reader.GetSingle();
reader.Read();
float outputMax = reader.GetSingle();
reader.Read(); // End of array
return new Controller
{
InputParameter = Enum.Parse<Controller.InputTypes>( inputParam, true ),
InputRange = new ValueRange( inputMin, inputMax ),
OutputParameter = Enum.Parse<Controller.OutputTypes>( outputParam, true ),
OutputRange = new ValueRange( outputMin, outputMax )
};
}
public override void Write( Utf8JsonWriter writer, Controller value, JsonSerializerOptions options )
{
writer.WriteStartArray();
writer.WriteStringValue( value.InputParameter.ToString() );
writer.WriteNumberValue( value.InputRange.Min );
writer.WriteNumberValue( value.InputRange.Max );
writer.WriteStringValue( value.OutputParameter.ToString() );
writer.WriteNumberValue( value.OutputRange.Min );
writer.WriteNumberValue( value.OutputRange.Max );
writer.WriteEndArray();
}
}

View File

@ -0,0 +1,35 @@
using Sandbox;
using System.Collections.Generic;
using VeloX.Audio;
namespace VeloX;
[GameResource( "Engine Stream", "engstr", "Engine Sound", Category = "VeloX", Icon = "time_to_leave" )]
public sealed class EngineStream : GameResource
{
public sealed class Layer
{
[Property,
Title( "Sound File" ),
Description( "Sound asset for this layer" )]
public SoundFile AudioPath { get; set; }
[Property, Title( "Use Redline Effect" ),
Description( "Enable RPM-based vibration effect" )]
public bool UseRedline { get; set; } = true;
[Property, Title( "Controllers" ),
Description( "Audio parameter controllers" )]
public List<Controller> Controllers { get; set; }
[Property, Title( "Is Muted" )]
public bool IsMuted { get; set; }
}
[Property, Title( "Stream Parameters" ),
Description( "Global engine sound parameters" )]
public StreamParameters Parameters { get; set; }
[Property, Title( "Sound Layers" ),
Description( "Individual sound layers" )]
public Dictionary<string, Layer> Layers { get; set; }
}

View File

@ -0,0 +1,119 @@
using Sandbox;
using Sandbox.Audio;
using System;
using System.Collections.Generic;
using System.IO;
using VeloX.Audio;
using static VeloX.EngineStream;
namespace VeloX;
public class EngineStreamPlayer( EngineStream stream ) : IDisposable
{
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
public EngineStream Stream { get; set; } = stream;
public EngineState EngineState { get; set; }
public bool EngineSoundPaused => EngineState != EngineState.Running;
public float Throttle { get; set; }
public bool IsRedlining { get; set; }
public float RPMPercent { get; set; }
private float _wobbleTime;
public readonly Dictionary<Layer, SoundHandle> EngineSounds = [];
public void Update( float deltaTime, Vector3 position, bool isLocal = false )
{
var globalPitch = 1.0f;
// Gear wobble effect
if ( _wobbleTime > 0 )
{
_wobbleTime -= deltaTime * (0.1f + Throttle);
globalPitch += MathF.Cos( _wobbleTime * Stream.Parameters.WobbleFrequency ) * _wobbleTime * (1 - _wobbleTime) * Stream.Parameters.WobbleStrength;
}
globalPitch *= Stream.Parameters.Pitch;
// Redline effect
var redlineVolume = 1.0f;
if ( IsRedlining )
{
redlineVolume = 1 - Stream.Parameters.RedlineStrength +
MathF.Cos( RealTime.Now * Stream.Parameters.RedlineFrequency ) *
Stream.Parameters.RedlineStrength;
}
// Process layers
foreach ( var (id, layer) in Stream.Layers )
{
EngineSounds.TryGetValue( layer, out var channel );
if ( !channel.IsValid() )
{
channel = Sound.PlayFile( layer.AudioPath );
EngineSounds[layer] = channel;
}
if ( channel.Paused && (EngineSoundPaused || layer.IsMuted) )
continue;
// Reset controller outputs
float layerVolume = 1.0f;
float layerPitch = 1.0f;
// Apply all controllers
foreach ( var controller in layer.Controllers )
{
var inputValue = controller.InputParameter switch
{
Controller.InputTypes.Throttle => Throttle,
Controller.InputTypes.RpmFraction => RPMPercent,
_ => 0.0f
};
var normalized = Math.Clamp( inputValue, controller.InputRange.Min, controller.InputRange.Max );
var outputValue = controller.InputRange.Remap(
normalized,
controller.OutputRange.Min,
controller.OutputRange.Max
);
// Apply to correct parameter
switch ( controller.OutputParameter )
{
case Controller.OutputTypes.Volume:
layerVolume *= outputValue;
break;
case Controller.OutputTypes.Pitch:
layerPitch *= outputValue;
break;
}
}
// Apply redline effect if needed
layerVolume *= layer.UseRedline ? redlineVolume : 1.0f;
layerPitch *= globalPitch;
// Update audio channel
channel.Pitch = layerPitch;
channel.Volume = layerVolume * Stream.Parameters.Volume;
channel.Position = position;
channel.ListenLocal = isLocal;
channel.Paused = EngineSoundPaused || layer.IsMuted;
channel.TargetMixer = EngineMixer;
}
}
public void Dispose()
{
foreach ( var item in EngineSounds )
{
item.Value?.Stop();
item.Value?.Dispose();
}
}
}

View File

@ -0,0 +1,24 @@
using Sandbox;
namespace VeloX;
public sealed class StreamParameters
{
[Property, Range( 0.1f, 2.0f ), Description( "Base pitch multiplier" )]
public float Pitch { get; set; } = 1.0f;
[Property, Range( 0.0f, 2.0f ), Description( "Base volume level" )]
public float Volume { get; set; } = 1.0f;
[Property, Range( 1f, 100f ), Description( "Vibration frequency at max RPM" )]
public float RedlineFrequency { get; set; } = 55.0f;
[Property, Range( 0.0f, 1.0f ), Description( "Vibration intensity at max RPM" )]
public float RedlineStrength { get; set; } = 0.2f;
[Property, Range( 1f, 50f ), Description( "Idle vibration frequency" )]
public float WobbleFrequency { get; set; } = 25.0f;
[Property, Range( 0.0f, 0.5f ), Description( "Idle vibration intensity" )]
public float WobbleStrength { get; set; } = 0.13f;
}

View File

@ -0,0 +1,78 @@

using Sandbox;
namespace VeloX;
public static class PhysicsExtensions
{
/// <summary>
/// Calculates the linear and angular velocities on the center of mass for an offset impulse.
/// </summary>
/// <param name="physObj">The physics object</param>
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
/// <param name="position">The location of the impulse in world coordinates</param>
/// <returns>
/// Vector1: Linear velocity from the impulse (World frame)
/// Vector2: Angular velocity from the impulse (Local frame)
/// </returns>
public static (Vector3 LinearVelocity, Vector3 AngularVelocity) CalculateVelocityOffset( this PhysicsBody physObj, Vector3 impulse, Vector3 position )
{
if ( !physObj.IsValid() || !physObj.MotionEnabled )
return (Vector3.Zero, Vector3.Zero);
Vector3 linearVelocity = impulse / physObj.Mass;
Vector3 centerOfMass = physObj.MassCenter;
Vector3 relativePosition = position - centerOfMass;
Vector3 torque = relativePosition.Cross( impulse );
Rotation bodyRotation = physObj.Transform.Rotation;
Vector3 localTorque = bodyRotation.Inverse * torque;
Vector3 localInverseInertia = physObj.Inertia.Inverse;
Vector3 localAngularVelocity = new(
localTorque.x * localInverseInertia.x,
localTorque.y * localInverseInertia.y,
localTorque.z * localInverseInertia.z );
return (linearVelocity, localAngularVelocity);
}
/// <summary>
/// Calculates the linear and angular impulses on the object's center of mass for an offset impulse.
/// </summary>
/// <param name="physObj">The physics object</param>
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
/// <param name="position">The location of the impulse in world coordinates</param>
/// <returns>
/// Vector1: Linear impulse on center of mass (World frame)
/// Vector2: Angular impulse on center of mass (Local frame)
/// </returns>
public static (Vector3 LinearImpulse, Vector3 AngularImpulse) CalculateForceOffset(
this PhysicsBody physObj,
Vector3 impulse,
Vector3 position )
{
if ( !physObj.IsValid() || !physObj.MotionEnabled )
{
return (Vector3.Zero, Vector3.Zero);
}
// 1. Linear impulse is the same as the input impulse (conservation of momentum)
Vector3 linearImpulse = impulse;
// 2. Calculate angular impulse (torque) from the offset force
// τ = r × F (cross product of position relative to COM and force)
Vector3 centerOfMass = physObj.MassCenter;
Vector3 relativePosition = position - centerOfMass;
Vector3 worldAngularImpulse = relativePosition.Cross( impulse );
// Convert angular impulse to local space (since we'll use it with LocalInertia)
Rotation bodyRotation = physObj.Transform.Rotation;
Vector3 localAngularImpulse = bodyRotation.Inverse * worldAngularImpulse;
return (linearImpulse, localAngularImpulse);
}
}

4
Code/velox.csproj.user Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View File

@ -0,0 +1,30 @@
using Editor;
using Sandbox;
using VeloX.Audio;
namespace VeloX;
[CustomEditor( typeof( Controller ) )]
public class ControllerWidget : ControlObjectWidget
{
public override bool SupportsMultiEdit => false;
public override bool IsWideMode => true;
public ControllerWidget( SerializedProperty property ) : base( property, true )
{
Layout = Layout.Column();
SerializedObject.TryGetProperty( nameof( Controller.InputParameter ), out var inputType );
Layout.Add( Create( inputType ) );
SerializedObject.TryGetProperty( nameof( Controller.InputRange ), out var inputRange );
Layout.Add( Create( inputRange ) );
SerializedObject.TryGetProperty( nameof( Controller.OutputParameter ), out var outputType );
Layout.Add( Create( outputType ) );
SerializedObject.TryGetProperty( nameof( Controller.OutputRange ), out var outputRange );
Layout.Add( Create( outputRange ) );
}
protected override void OnPaint() { }
}

View File

@ -0,0 +1,144 @@
using Editor;
using Sandbox;
using static Editor.Inspectors.AssetInspector;
using static VeloX.EngineStream;
namespace VeloX;
[Dock( "Editor", "Engine Stream Editor", "local_fire_department" )]
[CanEdit( "asset:engstr" )]
internal sealed class EngineStreamEditor : Widget, IAssetInspector
{
public EngineStreamEditor( Widget parent ) : base( parent, false )
{
CreateEditor();
}
public static EngineStreamEditor Instance { get; private set; }
private EngineStream ActiveStream;
private SerializedObject StreamSO;
private ScrollArea StreamTabs;
//[EditorEvent.Hotload]
//internal void Reload()
//{
// if ( Instance.IsValid() )
// CreateEditor();
//}
//[Menu( "Editor", "VeloX/Engine Stream Editor", "time_to_leave" )]
internal static void Open()
{
Instance = new EngineStreamEditor();
}
internal EngineStreamEditor()
{
Size = new Vector2( 1300, 700 );
CreateEditor();
Show();
}
private class LayerObject
{
public Layer Layer { get; set; }
public string Id { get; set; }
}
private void CreateEditor()
{
WindowTitle = "Engine Stream Editor";
Layout = Layout.Column();
//Canvas = new( null )
//{
// Layout = Layout,
//};
CreateHeader();
var b = Layout.AddRow();
CreateTabs( b );
CreateRightMenu( b );
}
private void CreateHeader()
{
var a = Layout.AddRow();
a.Margin = new Sandbox.UI.Margin( 8, 12, 8, 16 );
a.Alignment = TextFlag.Left;
a.Spacing = 12;
var fileButton = a.Add( new Button( "Open File" ), 0 );
fileButton.Clicked = () =>
{
var picker = AssetPicker.Create( this,
AssetType.FromType( typeof( EngineStream ) ),
new() { EnableCloud = true, SeparateCloudTab = false }
);
picker.OnAssetPicked += asset => CreateStreamEditor( asset[0].LoadResource<EngineStream>() );
picker.Show();
};
}
Layout RightLayout;
SimulatedEngineWidget SimulatedEngineWidget;
private void CreateRightMenu( Layout layout )
{
RightLayout = layout.AddColumn( 1 );
RightLayout.Alignment = TextFlag.Top;
RebuildRightMenu();
}
private void RebuildRightMenu()
{
RightLayout.Clear( true );
RightLayout.Spacing = 12;
var layoutTop = RightLayout.AddColumn();
SimulatedEngineWidget = new SimulatedEngineWidget( null, ActiveStream );
layoutTop.Add( SimulatedEngineWidget );
if ( StreamSO is not null )
layoutTop.Add( new ParametersWidget( StreamSO.GetProperty( nameof( EngineStream.Parameters ) ) ) );
}
private void CreateTabs( Layout layout )
{
StreamTabs = layout.Add( new ScrollArea( this ), 5 );
StreamTabs.Canvas = new Widget( StreamTabs )
{
Layout = Layout.Column(),
VerticalSizeMode = (SizeMode)3,
};
StreamTabs.Canvas.Layout.Margin = new Sandbox.UI.Margin( 8, 8, 16, 8 );
StreamTabs.Canvas.Layout.Alignment = TextFlag.Top;
}
public void RebuildLayers()
{
StreamTabs.Canvas.Layout.Clear( true );
StreamSO = ActiveStream.GetSerialized();
if ( !StreamSO.TryGetProperty( nameof( EngineStream.Layers ), out var layers ) )
return;
StreamTabs.Canvas.Layout.Add( new LayersEditorWidget( layers, ActiveStream, SimulatedEngineWidget ) );
}
private void CreateStreamEditor( EngineStream stream )
{
ActiveStream = stream;
StreamSO = ActiveStream.GetSerialized();
RebuildRightMenu();
RebuildLayers();
}
public void SetAsset( Asset asset )
{
CreateStreamEditor( asset.LoadResource<EngineStream>() );
}
}

View File

@ -0,0 +1,154 @@

using Editor;
using Sandbox;
using System.Numerics;
using VeloX.Audio;
using static VeloX.EngineStream;
namespace VeloX;
internal class LayerObjectWidget : ControlObjectWidget
{
private SerializedProperty Index;
private SerializedCollection Controllers;
private class ControllerWidget : ControlObjectWidget
{
public ControllerWidget( LayerObjectWidget parent, SerializedProperty property ) : base( property, true )
{
PaintBackground = false;
Layout = Layout.Row();
HorizontalSizeMode = (SizeMode)3;
//SetStyles( "background-color: #123; color: white; font-weight: 600;" );
SerializedObject.TryGetProperty( nameof( Controller.InputParameter ), out var inputType );
Layout.Add( EnumControlWidget.Create( inputType ), 1 );
SerializedObject.TryGetProperty( nameof( Controller.InputRange ), out var inputRange );
Layout.Add( ValueRangeWidget.Create( inputRange ), 12 );
SerializedObject.TryGetProperty( nameof( Controller.OutputParameter ), out var outputType );
Layout.Add( EnumControlWidget.Create( outputType ), 1 );
SerializedObject.TryGetProperty( nameof( Controller.OutputRange ), out var outputRange );
Layout.Add( ValueRangeWidget.Create( outputRange ), 12 );
var removeButton = new IconButton( "clear", () => parent.RemoveEntry( property ) ) { ToolTip = "Remove", Background = Theme.ControlBackground, FixedWidth = Theme.RowHeight, FixedHeight = Theme.RowHeight };
Layout.Add( removeButton );
}
}
void RemoveEntry( SerializedProperty index )
{
Controllers.Remove( index );
Rebuild();
}
private SerializedCollection layers;
SimulatedEngineWidget SimulatedEngine;
public LayerObjectWidget( SerializedProperty property, SerializedProperty index, SerializedCollection layers, SimulatedEngineWidget SimulatedEngine ) : base( property, true )
{
this.SimulatedEngine = SimulatedEngine;
SetStyles( "background-color: #;" );
this.layers = layers;
Index = index;
Layout = Layout.Column();
Layout.SizeConstraint = SizeConstraint.SetMaximumSize;
Layout.Alignment = TextFlag.Top;
Layout.Margin = 4;
Layout.Spacing = 4;
Rebuild();
}
protected override void PaintUnder()
{
base.PaintUnder();
if ( SimulatedEngine is null )
return;
if ( SimulatedEngine.Player is null )
return;
var layer = SimulatedEngine.Player.Stream.Layers[Index.GetValue<string>()];
if ( SimulatedEngine.Player.EngineSounds.TryGetValue( layer, out var handle ) )
{
Paint.SetBrush( Color.Gray );
Rect localRect = base.LocalRect;
var newVolume = localRect.Width * handle.Volume;
Paint.DrawRect( new( 0, 0, newVolume, localRect.Height ) );
Update();
}
}
public void Rebuild()
{
using var _ = SuspendUpdates.For( this );
Layout.Clear( true );
VerticalSizeMode = SizeMode.CanShrink;
var layout = Layout.AddRow();
layout.Alignment = TextFlag.Top;
var id = layout.Add(
StringControlWidget.Create( Index ), 1
).HorizontalSizeMode = SizeMode.Flexible;
layout.Add(
ResourceControlWidget.Create( SerializedObject.GetProperty( nameof( Layer.AudioPath ) ) ), 3
).HorizontalSizeMode = SizeMode.Flexible;
var ctlrs = Layout.AddColumn();
ctlrs.Spacing = 2;
ctlrs.Margin = 4;
var hints = ctlrs.AddRow();
hints.Add( new Label( "Input Type" ), 3 );
hints.Add( new Label( "Input Min" ), 2 );
hints.Add( new Label( "Input Max" ), 2 );
hints.Add( new Label( "Output Type" ), 3 );
hints.Add( new Label( "Output Min" ), 2 );
hints.Add( new Label( "Output Max" ), 2 );
if ( !SerializedObject.TryGetProperty( nameof( Layer.Controllers ), out var constrollers ) )
return;
if ( !constrollers.TryGetAsObject( out var so ) || so is not SerializedCollection sc )
return;
Controllers = sc;
Controllers.OnEntryAdded = Rebuild;
Controllers.OnEntryRemoved = Rebuild;
foreach ( var controller in sc )
{
ctlrs.Add( new ControllerWidget( this, controller ) );
}
var footer = ctlrs.AddRow();
var controls = footer.AddRow();
controls.Alignment = TextFlag.Left;
controls.Spacing = 4;
var useRedline = SerializedObject.GetProperty( nameof( Layer.UseRedline ) );
controls.Add( new BoolControlWidget( useRedline ) );
controls.Add( new Label( useRedline.Description ) );
var isMuted = SerializedObject.GetProperty( nameof( Layer.IsMuted ) );
controls.Add( new BoolControlWidget( isMuted ) );
controls.Add( new Label( isMuted.DisplayName ) );
var cont = footer.AddRow();
cont.Spacing = 4;
cont.Alignment = TextFlag.Right;
cont.Add( new Button( "Add Controller" ) ).Clicked = () =>
{
Controllers.Add( null );
};
cont.Add( new Button( "Remove Layer" ) ).Clicked = () =>
{
layers.RemoveAt( Index.GetValue<string>() );
};
}
}

View File

@ -0,0 +1,49 @@

using Editor;
using Sandbox;
using System.IO;
namespace VeloX;
internal sealed class LayersEditorWidget : ControlObjectWidget
{
public EngineStream Stream;
SimulatedEngineWidget SimulatedEngineWidget;
public LayersEditorWidget( SerializedProperty property, EngineStream stream, SimulatedEngineWidget SimulatedEngine ) : base( property, true )
{
SimulatedEngineWidget = SimulatedEngine;
PaintBackground = false;
Stream = stream;
Layout = Layout.Column();
Layout.SizeConstraint = SizeConstraint.SetMaximumSize;
Layout.Spacing = 12;
SetStyles( "background-color:rgba(0,0,0,0);" );
if ( SerializedObject is not SerializedCollection sc )
return;
sc.OnEntryAdded += Rebuild;
sc.OnEntryRemoved += Rebuild;
Rebuild();
}
private void Rebuild()
{
Layout.Clear( true );
if ( SerializedObject is not SerializedCollection sc )
return;
foreach ( var t in sc )
{
var id = t.GetKey();
Layout.Add( new LayerObjectWidget( t, id, sc, SimulatedEngineWidget ) );
}
Layout.Add( new Button( "Add Layer" ) ).Clicked = () =>
{
sc.Add( "", null );
};
}
}

Some files were not shown because too many files have changed in this diff Show More