77 Commits

Author SHA1 Message Date
thayol 55e5e8117c exclude dead entities from auto-attack target selection 2025-01-23 11:17:16 +09:00
thayol 15e72a9e10 fix auto-attack when target not in direct vision 2025-01-23 10:49:14 +09:00
thayol 4acd7a2881 fix projectiles colliding with dead entities 2025-01-23 10:36:28 +09:00
thayol afa419e77a add acceleration to shield throw 2025-01-23 00:04:13 +09:00
thayol 441a73355e untangle abilities 2025-01-22 23:41:53 +09:00
thayol 59b5a603a0 generalize buff damage multipliers 2025-01-22 23:33:48 +09:00
thayol 4c76d5dbde restrict casting vision to nearby enemies 2025-01-22 23:21:39 +09:00
thayol 0db1ceeedc fix dead state 2025-01-22 22:52:08 +09:00
thayol 4f8dcebcd1 increase process priority instead of offloading to workers 2025-01-22 15:00:32 +09:00
thayol c4c7c921d7 optimize reporting and serialization for clients 2025-01-22 12:41:55 +09:00
thayol 916bc31356 fix ghostable entities being pushed by ghosted entities 2025-01-22 00:16:07 +09:00
thayol fa2dbb5237 add bbox checks to pathfinding graphs 2025-01-21 23:57:45 +09:00
thayol 8ce1a2266f add bbox checks for pathfinding 2025-01-21 10:02:54 +09:00
thayol 6b8a220f39 fix vision logic and game tick timer 2025-01-20 11:17:35 +09:00
thayol bf38f69071 add vision 2025-01-20 00:05:48 +09:00
thayol 634dde2a3b use auto-incremented IDs instead of UUIDs 2025-01-19 21:43:27 +09:00
thayol e4f1fe19f4 fix projectile colliders in movement 2025-01-19 20:55:16 +09:00
thayol 072204b902 add catching up mechanic for ticks 2025-01-19 18:03:20 +09:00
thayol 04cc3f951e make projectiles use bounding boxes too 2025-01-19 16:11:51 +09:00
thayol e75c0d2944 use bounding boxes to optimize collision detection 2025-01-19 14:24:19 +09:00
thayol 0a4853aff9 add a basic terrain layout 2025-01-19 00:59:17 +09:00
thayol 0b949683a6 add 3D casting indicator 2025-01-18 21:02:04 +09:00
thayol 7824ba976b add stats 2025-01-18 20:10:54 +09:00
thayol 8457312f63 display buffs in the client 2025-01-18 12:00:12 +09:00
thayol 18c3ace616 make minimap dynamic 2025-01-18 11:07:06 +09:00
thayol 7415475cb0 add buffs 2025-01-18 10:49:38 +09:00
thayol ed6394354e fix auto attack range after cast 2025-01-18 10:01:46 +09:00
thayol 8ebae0d866 use ability keys instead of indices 2025-01-18 09:53:50 +09:00
thayol b4162d4e39 add range indicator 2025-01-18 09:42:16 +09:00
thayol 8e95bc141c fix melee attacks 2025-01-17 23:40:33 +09:00
thayol 9345c7af04 rely on stringification instead of state reports 2025-01-17 23:04:38 +09:00
thayol 80ccb92815 add visualRadius 2025-01-17 17:51:00 +09:00
thayol a44693aa5d despawn projectile instead of weird movement 2025-01-17 14:46:27 +09:00
thayol 1a5e811020 add moveCancelable to Ability 2025-01-17 14:43:49 +09:00
thayol 787b48a4df fix projectiles phasing through stuff 2025-01-17 14:01:30 +09:00
thayol 20f8a2f1fe use obstacle-in-path pathfinding 2025-01-17 13:01:47 +09:00
thayol 597aa204de add README 2025-01-14 01:44:21 +09:00
thayol 92e06dedce add minion routing 2025-01-13 22:38:54 +09:00
thayol 9d3fbda494 move entity definitions to templates 2025-01-13 16:53:12 +09:00
thayol ffbc4d9803 inflate ranges by entity radii 2025-01-13 16:17:34 +09:00
thayol 16429a6e1b add dead state 2025-01-13 14:08:10 +09:00
thayol 03bbea4862 fix auto-attack targeting 2025-01-13 11:45:26 +09:00
thayol 49a4d3e924 add visual distinction for teams 2025-01-12 20:38:00 +09:00
thayol ea23aa3174 adjust visual height 2025-01-12 20:05:29 +09:00
thayol 8e861929cb fix pathfinding issues 2025-01-12 19:43:45 +09:00
thayol 6ff950640c extend moveset with attack, halt, stop 2025-01-12 17:03:42 +09:00
thayol 302d2f0618 fix cast times 2025-01-12 14:50:37 +09:00
thayol d9d62d7070 collapse Effect into Ability 2025-01-12 13:58:35 +09:00
thayol d9849f770b fix game loop timer 2025-01-12 10:58:30 +09:00
thayol e0dd7dcaf3 add cast times and cooldowns 2025-01-12 03:30:52 +09:00
thayol 2eb914a680 add homing projectiles 2025-01-12 00:29:11 +09:00
thayol 51b61ab449 add skillshots 2025-01-12 00:11:00 +09:00
thayol 957b09b878 add lane scenario with HP 2025-01-11 21:40:57 +09:00
thayol 462dfe7b9a add tweening 2025-01-11 19:38:40 +09:00
thayol 4aba510ec0 improve position fixing 2025-01-11 18:17:44 +09:00
thayol f1c191f61f fix waypoints going out of bounds 2025-01-10 23:47:58 +09:00
thayol fe4dc8b8bc fix pathfinding for real 2024-12-25 18:12:53 +09:00
thayol 8fe48fb679 fix pathfinding nekimegyafalnak style 2024-12-25 17:19:45 +09:00
thayol 0f8a73911f check bi-directional paths in graph building 2024-12-25 11:19:01 +09:00
thayol 5acc827f7b revert "disable collision to fix pathfinding phasing through walls"
This reverts commit f48a6bf9aa.
2024-12-25 09:37:13 +09:00
thayol 2570f32592 revert "fix some pathfinding problems"
This reverts commit 2a9ef691fe.
2024-12-25 09:37:05 +09:00
thayol 2a9ef691fe fix some pathfinding problems 2024-12-25 09:33:07 +09:00
thayol f48a6bf9aa disable collision to fix pathfinding phasing through walls 2024-12-25 03:55:32 +09:00
thayol 3bb34ed012 fix line instantiation 2024-12-25 03:42:12 +09:00
thayol 227cc1590a fall back to lines when radius is zero 2024-12-25 01:35:12 +09:00
thayol fb6e75e38c add pathfinding times report 2024-12-25 01:15:46 +09:00
thayol 05360208b0 add unoptimized pathfinding 2024-12-25 00:32:33 +09:00
thayol 47aade7b3f add camera movement 2024-12-24 10:23:33 +09:00
thayol 37a77e902c add terrain collision 2024-12-23 18:30:59 +09:00
thayol ae6f4c2847 rename main.js to client.js 2024-12-23 12:18:21 +09:00
thayol ba0d8f606a add client side terrain without collision 2024-12-23 11:57:36 +09:00
thayol 604368b52c add minimap 2024-12-23 10:08:06 +09:00
thayol e23978ea90 ditch THREE raycasting for SAT again 2024-12-23 09:46:26 +09:00
thayol 054d22d01a replace most systems with THREE 2024-12-22 23:52:56 +09:00
thayol 14212afd70 add move speed 2024-12-22 17:00:23 +09:00
thayol 69343821b6 add move command to client 2024-12-22 16:50:54 +09:00
thayol 2957903cb1 Buff Queen AKA "Ez egy fa?"
Because the first placeholder player model
resembled a queen that's been to the gym a
bit too much. Also, before she got her head
and hands, she looked like a tree, legit.
2024-12-21 23:46:32 +09:00
96 changed files with 6827 additions and 1940 deletions
+4
View File
@@ -0,0 +1,4 @@
.git
*Dockerfile*
*docker-compose*
node_modules
-2
View File
@@ -1,2 +0,0 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
+136 -5
View File
@@ -1,5 +1,136 @@
# Godot 4+ specific ignores # Logs
.godot/ logs
/android/ *.log
build/ npm-debug.log*
*.tmp yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Files generated by the app
public/temp
# Flamegraphs
*.0X
+7
View File
@@ -0,0 +1,7 @@
FROM node:current-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY public ./public
COPY src ./src
CMD ["node", "src/index.js"]
+86
View File
@@ -0,0 +1,86 @@
# Instructions Clear
Instructions Clear is a Multiplayer Online Battle Arena (MOBA) where two teams of five players fight for victory.
The players have the ability to upgrade their abilities via their gear earned from quests.
But make sure to prioritize resurrecting your allies first because once you wipe, it's all over.
The game blends some elements of traditional MMORPG dungeons with modern MOBAs.
## Design Pillars
| Power | Reaction | Competition |
| --- | --- | --- |
| Players gradually get stronger as they complete quests. | Quests provide upgrade paths that provide opportunity to adapt. | If not by sheer power or a good build, teams have to outperform the opposing team to win by any means. |
## Audience & Market
The game is designed to be played online.
Elements of the no longer popular MMORPG genre are used to give former players a modern game.
It is aimed at young adults.
## Core Gameplay
The game controls are single unit isometric Real-Time Strategy (RTS) style (point-and-click).
The player can decide the pathing destination via right mouse clicks, combine it with auto-attacking the closest target via left mouse clicks, and cast abilities using the Q, W, and E keys.
Players of the same team collectively deal damage to players of the other team to bring the members' health points to zero. If a team's collective health pool has run out, that team loses.
Quests provide the income of players. Each quest gives the option of changing an ability or cashing out for gold.
Abilities provided by quests can either be a "legendary" (main) ability or a "basic" (minor) ability. A player is limited to one legendary ability per game.
The main ideology behind legendary abilities is the traditional damage, control, support trio.
Gold can be used to resurrect others (in lieu of a specialized ability), and to upgrade ability slots (to scale power).
The game is likely to provide a single level only.
## Mechanics for mechanics
The game concept is mainly dictated by MOBAs with the tried and tested systems, however, there are differences:
* Abilities replace items to reduce the possible combinations and to ease the learning curve.
* Quests replace last-hitting to make income more diverse to enable traditionally low-income roles like position 5 supports to earn equally.
* The win condition is changed to team elimination instead of the destruction of the main structure so that the game is more centered around the already celebrated team fights.
* Equal, generic ability kits replace heroes (or champions) to eliminate the possibility of a "lost draft" at the hero selection phase.
TBD: What are quests?
TBD: How (and if) structures work
TBD: Unlike in the Defense Of The Ancients (DOTA), the core structure (the Ancient) is not a win condition but it provides a safe haven until destroyed.
## Gameplay Balance & Pacing
As all MOBAs, if balancing is not perfect, players will gravitate towards "metas" to win.
TBD: The pacing is so that it doesn't feel like too much preparation but it isn't dragged out.
TBD: The "AFK farming" tactic should be crossed out by giving players incentive to fight.
### Possible Issues
**Corpse camping:** After a successful pick, the enemies would not let the picked player revive.
**Long death timers:** The player can get bored until they get revived.
## Setting & World
TBD: Diagonal lanes or a free-for-all layout?
## Narrative
TBD: Gameplay is king, narrative is going to be an afterthought.
## Business Model
This is a free-to-play game. Since running servers costs money and the server binary is not public, a subscription model can be introduced to spin up more servers on demand instead of waiting in queue for the free servers.
The cosmetic aspect ("skins" on the player model) is supposed to be responsible for the majority of the income.
## Timeline
1. Prototype with working pathing
1. Win condition: full team elimination
1. Basic ability mechanics
1. Ability upgrades (upgrade trees)
1. Quest mechanics
1. Quest type diversification
1. Retrospective to plan the remainder of the timeline
-129
View File
@@ -1,129 +0,0 @@
[preset.0]
name="Windows Desktop"
platform="Windows Desktop"
runnable=false
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/instructions-clear-win-desktop.exe"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
[preset.1]
name="Windows Server"
platform="Windows Desktop"
runnable=false
advanced_options=false
dedicated_server=false
custom_features="dedicated_server"
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/instructions-clear-win-server.exe"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.1.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

Before

Width:  |  Height:  |  Size: 994 B

-37
View File
@@ -1,37 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rf3iei6hroc0"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
-4
View File
@@ -1,4 +0,0 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://diptcpjxid3cm"]
[resource]
albedo_color = Color(0.799569, 0, 0.0857406, 1)
-4
View File
@@ -1,4 +0,0 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://chp3rogcgumau"]
[resource]
albedo_color = Color(0.054902, 0.431373, 0.129412, 1)
-4
View File
@@ -1,4 +0,0 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://ccrb46njti2ke"]
[resource]
albedo_color = Color(0.270588, 0.596078, 1, 1)
-4
View File
@@ -1,4 +0,0 @@
[gd_resource type="PlaneMesh" format=3 uid="uid://dwpvym2kc4gd8"]
[resource]
size = Vector2(10000, 10000)
-34
View File
@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bk722l8idpuov"
path="res://.godot/imported/blue.png-2ebebcb60a2786b51bf8c57bf6c35d93.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://models/blue.png"
dest_files=["res://.godot/imported/blue.png-2ebebcb60a2786b51bf8c57bf6c35d93.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dmry88lccca6v"
path="res://.godot/imported/generic-bam-placeholder.gltf-9c6b218e010af71bbfc5e7c09f9a3001.scn"
[deps]
source_file="res://models/generic-bam-placeholder.gltf"
dest_files=["res://.godot/imported/generic-bam-placeholder.gltf-9c6b218e010af71bbfc5e7c09f9a3001.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
gltf/naming_version=1
gltf/embedded_image_handling=1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://en0thvrt6r6o"
path.s3tc="res://.godot/imported/generic-bam-placeholder_0.png-4b26c694cab47cc035250c044ea2f819.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "c16cd153f9c4e58f92155c7fdbacab46"
}
[deps]
source_file="res://models/generic-bam-placeholder_0.png"
dest_files=["res://.godot/imported/generic-bam-placeholder_0.png-4b26c694cab47cc035250c044ea2f819.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://daj38bahjfuwa"
path.s3tc="res://.godot/imported/generic-bam-placeholder_1.png-a28949c8b47cd079d7024f9873bd59ec.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "ef862aea6838e1d435e71bdbfb00179b"
}
[deps]
source_file="res://models/generic-bam-placeholder_1.png"
dest_files=["res://.godot/imported/generic-bam-placeholder_1.png-a28949c8b47cd079d7024f9873bd59ec.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://k6f3fdlnbmjp"
path="res://.godot/imported/generic-player-placeholder-red.gltf-ea859f06b8e003c98de2a38069feee97.scn"
[deps]
source_file="res://models/generic-player-placeholder-red.gltf"
dest_files=["res://.godot/imported/generic-player-placeholder-red.gltf-ea859f06b8e003c98de2a38069feee97.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
gltf/naming_version=1
gltf/embedded_image_handling=1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cpeerw3suw6mu"
path.s3tc="res://.godot/imported/generic-player-placeholder-red_0.png-2c10651b9fcf9a6312237a1dc764d53d.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "ab6be8d52f7b2c547d41a85e90c891ca"
}
[deps]
source_file="res://models/generic-player-placeholder-red_0.png"
dest_files=["res://.godot/imported/generic-player-placeholder-red_0.png-2c10651b9fcf9a6312237a1dc764d53d.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vjlacypkdltq"
path.s3tc="res://.godot/imported/generic-player-placeholder-red_1.png-dee3ab7ddc7582b49a9fd5adc139df00.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "ef862aea6838e1d435e71bdbfb00179b"
}
[deps]
source_file="res://models/generic-player-placeholder-red_1.png"
dest_files=["res://.godot/imported/generic-player-placeholder-red_1.png-dee3ab7ddc7582b49a9fd5adc139df00.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dy6gkeak06s17"
path="res://.godot/imported/generic-player-placeholder.gltf-d04d5db451019c4676ca4f90b38fb57a.scn"
[deps]
source_file="res://models/generic-player-placeholder.gltf"
dest_files=["res://.godot/imported/generic-player-placeholder.gltf-d04d5db451019c4676ca4f90b38fb57a.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
gltf/naming_version=1
gltf/embedded_image_handling=1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://o47cpbwqjsjs"
path.s3tc="res://.godot/imported/generic-player-placeholder_0.png-1bbbfa59d7787bfd0a359a8b57d9b3b8.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "952985ffaa2c5afc37983b4f7510c250"
}
[deps]
source_file="res://models/generic-player-placeholder_0.png"
dest_files=["res://.godot/imported/generic-player-placeholder_0.png-1bbbfa59d7787bfd0a359a8b57d9b3b8.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,38 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://voq5cycx1mal"
path.s3tc="res://.godot/imported/generic-player-placeholder_1.png-28f369353b1e252b6560224aa6109668.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "ef862aea6838e1d435e71bdbfb00179b"
}
[deps]
source_file="res://models/generic-player-placeholder_1.png"
dest_files=["res://.godot/imported/generic-player-placeholder_1.png-28f369353b1e252b6560224aa6109668.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

-34
View File
@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bq1dptt2cbowv"
path="res://.godot/imported/neutral.png-80876cec7814f9bf29f7275049231f7f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://models/neutral.png"
dest_files=["res://.godot/imported/neutral.png-80876cec7814f9bf29f7275049231f7f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
-34
View File
@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2njhdyxocn3m"
path="res://.godot/imported/notblue.png-11c566d53917b3b6ae77697a670d1436.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://models/notblue.png"
dest_files=["res://.godot/imported/notblue.png-11c566d53917b3b6ae77697a670d1436.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

-34
View File
@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ds63oxmg2i260"
path="res://.godot/imported/red.png-ff7cef4bec9b12c12344685a7dc6e028.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://models/red.png"
dest_files=["res://.godot/imported/red.png-ff7cef4bec9b12c12344685a7dc6e028.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

-34
View File
@@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bninyy5txs3yf"
path="res://.godot/imported/white.png-251d22fbf66643dc5f64509f2be910f4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://models/white.png"
dest_files=["res://.godot/imported/white.png-251d22fbf66643dc5f64509f2be910f4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+1075
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "instructions-clear",
"version": "1.0.0",
"main": "src/index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Thayol",
"license": "UNLICENSED",
"description": "",
"dependencies": {
"@tweenjs/tween.js": "^25.0.0",
"sat": "^0.9.0",
"stats.js": "^0.17.0",
"three": "^0.171.0",
"websocket-express": "^3.1.2"
}
}
-99
View File
@@ -1,99 +0,0 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Instructions Clear"
run/main_scene="res://scenes/empty.tscn"
config/features=PackedStringArray("4.4", "Mobile")
run/max_fps=120
boot_splash/show_image=false
config/icon="res://icon.svg"
run/size/borderless=false
run/size/viewport_height=720
run/size/viewport_width=1280
[autoload]
MultiplayerManager="*res://scripts/multiplayer/multiplayer_manager.gd"
Main="*res://scripts/main.gd"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/vsync/vsync_mode=0
[input]
move_forward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_back={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
mouse_capture={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(163, 13),"global_position":Vector2(172, 59),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
mouse_release={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
walk={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
primary_interact={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(138, 4),"global_position":Vector2(147, 50),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
secondary_interact={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(159, 23),"global_position":Vector2(168, 69),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
[rendering]
rendering_device/fallback_to_vulkan=false
rendering_device/fallback_to_d3d12=false
lights_and_shadows/directional_shadow/size.mobile=4096
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3
lights_and_shadows/directional_shadow/soft_shadow_filter_quality.mobile=3
lights_and_shadows/positional_shadow/soft_shadow_filter_quality=3
lights_and_shadows/positional_shadow/soft_shadow_filter_quality.mobile=3
anti_aliasing/quality/msaa_2d=1
anti_aliasing/quality/msaa_3d=1
environment/defaults/default_clear_color=Color(0, 0, 0, 1)
anti_aliasing/quality/use_debanding=true
lights_and_shadows/positional_shadow/atlas_size.mobile=4096
+648
View File
@@ -0,0 +1,648 @@
import * as THREE from 'three'
import { Tween } from '@tweenjs/tween.js'
import Stats from 'stats.js'
const global = (0,eval)('this')
const scene = new THREE.Scene()
const raycaster = new THREE.Raycaster()
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
const backgroundColor = new THREE.Color().setHex(0x112233)
scene.background = backgroundColor
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(render)
const cameraOffsetX = 0
const cameraOffsetY = -13.5
const cameraOffsetZ = 20
camera.position.set(cameraOffsetX, cameraOffsetY, cameraOffsetZ)
camera.rotation.set((34 / 180) * Math.PI, 0, 0)
camera.zoom += 0.2
camera.updateProjectionMatrix()
camera.layers.enable(1)
camera.layers.enable(2)
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc })
const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 })
const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 })
// const bboxMaterial = new THREE.MeshToonMaterial({ color: 0xffd700, transparent: true, opacity: 0.2 })
const opacity = 0.3
const teamMaterials = {
blue: new THREE.MeshToonMaterial({ color: 0x4444ff }),
blueTransparent: new THREE.MeshToonMaterial({ color: 0x4444ff, transparent: true, opacity }),
neutral: new THREE.MeshToonMaterial({ color: 0xcccccc }),
neutralTransparent: new THREE.MeshToonMaterial({ color: 0xcccccc, transparent: true, opacity }),
red: new THREE.MeshToonMaterial({ color: 0xff4444 }),
redTransparent: new THREE.MeshToonMaterial({ color: 0xff4444, transparent: true, opacity }),
projectile: new THREE.MeshToonMaterial({ color: 0xff00ff, transparent: true, opacity }),
range: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 2 }),
}
// TODO: draw lines of path for minimap camera
const minimapCameraZ = 10
const minimapCamera = new THREE.OrthographicCamera(-10, 10, 10, -10)
const minimapRenderer = new THREE.WebGLRenderer()
minimapRenderer.setSize(300, 300)
minimapRenderer.setAnimationLoop(minimapRender)
minimapCamera.position.set(10, 10, 10)
const entities = {}
const projectiles = {}
const positionTweens = {}
const terrains = {}
var state = { abilities: [], entities: [], terrains: [], projectiles: [] }
global.entities = entities
global.projectiles = projectiles
global.terrains = terrains
global.state = state
const geometry = new THREE.PlaneGeometry(0, 0)
const material = new THREE.MeshToonMaterial({ color: 0x115011 })
const ground = new THREE.Mesh(geometry, material)
scene.add(ground)
const ambientLight = new THREE.AmbientLight(0x404040, 10)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5)
directionalLight.position.set(-0.5, -0.05, 1)
directionalLight.power = 3000
scene.add(directionalLight)
global.THREE = THREE
global.renderer = renderer
global.camera = camera
global.scene = scene
var tweenDuration = 1
const keysDown = {}
const mouse = {}
var stats = new Stats()
stats.showPanel(0)
function render() {
stats.begin()
cameraMovement()
Object.values(positionTweens).forEach((tween) => tween.update()) // TODO: clean up tweens
renderer.render(scene, camera)
stats.end()
}
function minimapRender() {
minimapRenderer.render(scene, minimapCamera)
}
var cameraLocked = true
function followCamera() {
const entity = entities[playerId]
if (entity == null) { return }
const distanceX = Math.abs((entity.position.x + cameraOffsetX) - camera.position.x)
const distanceY = Math.abs((entity.position.y + cameraOffsetY) - camera.position.y)
camera.position.z = cameraOffsetZ
if (distanceX > 0.01) {
if (entity.position.x + cameraOffsetX > camera.position.x) {
camera.position.x += cameraSpeed * distanceX
}
if (entity.position.x + cameraOffsetX < camera.position.x) {
camera.position.x -= cameraSpeed * distanceX
}
}
else if (distanceX != 0) {
camera.position.x = entity.position.x + cameraOffsetX
}
if (distanceY > 0.01) {
if (entity.position.y + cameraOffsetY > camera.position.y) {
camera.position.y += cameraSpeed * distanceY
}
if (entity.position.y + cameraOffsetY < camera.position.y) {
camera.position.y -= cameraSpeed * distanceY
}
}
else if (distanceY != 0) {
camera.position.y = entity.position.y + cameraOffsetY
}
}
const cameraSpeed = 0.03
function cameraMovement() {
if (cameraLocked) {
followCamera()
return
}
if (keysDown.ArrowLeft) { camera.position.x -= cameraSpeed }
else if (keysDown.ArrowRight) { camera.position.x += cameraSpeed }
if (keysDown.ArrowUp) { camera.position.y += cameraSpeed }
else if (keysDown.ArrowDown) { camera.position.y -= cameraSpeed }
if (keysDown.Space) {
camera.position.set(entities[playerId].position.x + cameraOffsetX, entities[playerId].position.y + cameraOffsetY, cameraOffsetZ)
}
}
function raycastToGround() {
const canvas = renderer.domElement
raycaster.setFromCamera(new THREE.Vector2((mouse.x / canvas.clientWidth) * 2 - 1, (mouse.y / canvas.clientHeight) * -2 + 1), camera)
const intersect = raycaster.intersectObject(ground).at(0)?.point
if (intersect != null) {
return {
x: Math.round(intersect.x * 100),
y: Math.round(intersect.y * 100),
}
}
return null
}
var websocket = null
global.websocket = null
var timerId = null
var playerId = null
function connectWebSocket() {
websocket = new WebSocket(`ws://${window.location.hostname}:1280/ws`)
global.websocket = websocket
websocket.onerror = () => websocket.close()
websocket.onopen = () => {
document.getElementById('connection').innerHTML = 'open'
clearInterval(timerId)
websocket.send(JSON.stringify({ action: 'join', id: playerId }))
}
websocket.onclose = () => {
websocket = null
document.getElementById('connection').innerHTML = 'closed'
timerId = setInterval(() => {
if (websocket == null) {
connectWebSocket()
}
}, 2000)
}
websocket.onmessage = (event) => {
state.byteSize = new Blob([event.data]).size
const stateUpdates = JSON.parse(event.data)
if (stateUpdates.tickRate != null) {
tweenDuration = 1000 / stateUpdates.tickRate
}
if (stateUpdates.width != null && stateUpdates.height != null) {
state.width = stateUpdates.width
state.height = stateUpdates.height
minimapCamera.top = state.height / 200
minimapCamera.right = state.width / 200
minimapCamera.bottom = -state.height / 200
minimapCamera.left = -state.width / 200
minimapCamera.updateProjectionMatrix()
minimapCamera.position.set(state.width / 200, state.height / 200, minimapCameraZ)
const size = 300
const wide = state.width > state.height
minimapRenderer.setSize(
wide ? size : (state.width / state.height) * size,
wide ? (state.height / state.width) * size : size,
)
}
for (const [key, value] of Object.entries(stateUpdates)) {
if (!['abilities', 'terrains', 'entities', 'projectiles', 'width', 'height'].includes(key)) {
state[key] = value
}
}
if (stateUpdates.abilities != null) {
const ids = stateUpdates.abilities.map((it) => it.id)
state.abilities = state.abilities.filter((it) => ids.includes(it.id))
for (const ability of stateUpdates.abilities ?? []) {
const index = state?.abilities?.findIndex((it) => it.id == ability.id)
if (index > -1) {
state.abilities[index] = {...state.abilities[index], ...ability}
}
else {
state.abilities.push(ability)
}
}
}
if (stateUpdates.entities != null) {
const ids = stateUpdates.entities.map((it) => it.id)
state.entities = state.entities.filter((it) => ids.includes(it.id))
for (const entity of stateUpdates.entities ?? []) {
const index = state?.entities?.findIndex((it) => it.id == entity.id)
if (index > -1) {
state.entities[index] = {...state.entities[index], ...entity}
}
else {
state.entities.push(entity)
}
}
}
if (stateUpdates.terrains != null) {
const ids = stateUpdates.terrains.map((it) => it.id)
state.terrains = state.terrains.filter((it) => ids.includes(it.id))
for (const terrain of stateUpdates.terrains ?? []) {
const index = state?.terrains?.findIndex((it) => it.id == terrain.id)
if (index > -1) {
state.terrains[index] = {...state.terrains[index], ...terrain}
}
else {
state.terrains.push(terrain)
}
}
}
if (stateUpdates.projectiles != null) {
const ids = stateUpdates.projectiles.map((it) => it.id)
state.projectiles = state.projectiles.filter((it) => ids.includes(it.id))
for (const projectile of stateUpdates.projectiles) {
const index = state?.projectiles?.findIndex((it) => it.id == projectile.id)
if (index > -1) {
state.projectiles[index] = {...state.projectiles[index], ...projectile}
}
else {
state.projectiles.push(projectile)
}
}
}
if (state.width != null && state.height != null && (ground.geometry.attributes.width != state.width || ground.geometry.attributes.height != state.height)) {
ground.geometry = new THREE.PlaneGeometry(state.width / 100, state.height / 100)
ground.position.set(state.width / 200, state.height / 200, 0)
}
for (const e of Object.values(entities)) {
e.userData.flaggedForRemoval = true
}
for (const e of state.entities ?? []) {
let entity
if (e.id in entities) {
entity = entities[e.id]
}
else {
const entityMaterial = teamMaterials[e.team]
entity = new THREE.Mesh(new THREE.CylinderGeometry(e.visualRadius / 100, e.visualRadius / 100, e.height / 50), entityMaterial)
entity.rotation.x = Math.PI / 2
entity.userData.type = 'entity'
entity.userData.id = e.id
entity.position.set(e.position.x / 100, e.position.y / 100, e.height / 100)
scene.add(entity)
const hpMargin = 0.4
const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 }))
maxHp.position.set(0, (e.height / 100) + hpMargin, 0)
maxHp.scale.set(1.5, 0.2, 1)
maxHp.layers.set(1)
entity.add(maxHp)
const hp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0x77ff77 }))
hp.position.set(0, 0, 0)
hp.scale.set(1, 1, 1)
hp.layers.set(1)
maxHp.add(hp)
const teamMaterial = teamMaterials[`${e.team}Transparent`]
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.radius) / 100, (e.radius) / 100, 1), teamMaterial)
const teamMarkerSize = 4000
teamMarker.scale.y = e.height / teamMarkerSize
teamMarker.position.y = (e.height / (teamMarkerSize * 2)) - (e.height / 100)
teamMarker.position.y += 0.01
teamMarker.layers.set(1)
entity.add(teamMarker)
const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 })
const buffMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.visualRadius + 10) / 100, (e.visualRadius + 10) / 100, 1), buffMaterial)
const buffMarkerSize = 400
buffMarker.scale.y = e.height / buffMarkerSize
buffMarker.layers.set(1)
buffMarker.visible = false
entity.add(buffMarker)
const rotationBase = new THREE.Object3D()
entity.add(rotationBase)
const castingMaterial = new THREE.MeshToonMaterial({ color: 0x10dde0, transparent: true, opacity: 0.4 })
const castingMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.height * 0.9) / 100, (e.height * 0.9) / 100, 1), castingMaterial)
const castingMarkerSize = 800
castingMarker.rotation.z = Math.PI / 2
castingMarker.position.x = (e.radius) / 100
castingMarker.scale.y = e.height / castingMarkerSize
castingMarker.layers.set(1)
buffMarker.visible = false
rotationBase.add(castingMarker)
const rangeMaterial = teamMaterials['range']
// const rangeSize = e.visionRange ?? 0
const rangeSize = (state.abilities.find((it) => it.id == e.abilities?.a)?.range ?? 0) + e.radius
const rangeMarker = new THREE.Mesh(new THREE.CylinderGeometry((rangeSize) / 100, (rangeSize) / 100, 1), rangeMaterial)
const rangeMarkerSize = 5000
rangeMarker.scale.y = e.height / rangeMarkerSize
rangeMarker.position.y = (e.height / (rangeMarkerSize * 2)) - (e.height / 100)
rangeMarker.layers.set(1)
rangeMarker.visible = false
entity.add(rangeMarker)
entities[e.id] = entity
}
entity.children.at(0).visible = !e.dead
entity.children.at(1).visible = !e.dead
entity.children.at(2).visible = e.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now
let z = e.height / 100
if (e.dead) {
entity.rotation.x = 0
entity.position.z = 0
z = 0
}
else {
entity.rotation.x = Math.PI / 2
entity.position.z = e.height / 100
}
entity.userData.flaggedForRemoval = false
entity.children.at(3).rotation.y = e.rotation
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z }, tweenDuration).start()
const hp = entity.children.at(0).children.at(0)
const percentageHp = e.health / e.maxHealth
hp.scale.x = percentageHp
hp.position.x = -(1 - percentageHp) / 2
// entity.children.at(4).visible = e.id == playerId
entity.children.at(3).children.at(0).visible = e.casting != null
}
for (const e of Object.values(entities)) {
if (e.userData.flaggedForRemoval) {
scene.remove(e)
delete entities[e.userData.id]
delete positionTweens[e.userData.id]
}
}
for (const p of Object.values(projectiles)) {
p.userData.flaggedForRemoval = true
}
for (const p of state.projectiles ?? []) {
let projectile
if (p.id in projectiles) {
projectile = projectiles[p.id]
}
else {
projectile = new THREE.Mesh(new THREE.SphereGeometry(p.visualRadius / 100), projectileMaterial)
projectile.userData.type = 'projectile'
projectile.userData.id = p.id
projectile.position.set(p.position.x / 100, p.position.y / 100, p.height / 100)
projectile.layers.set(2)
scene.add(projectile)
projectile.rotation.x = Math.PI / 2 // needed for the team marker...
const teamMaterial = teamMaterials[`${p.team}Transparent`] ?? teamMaterials['projectile']
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((p.radius) / 100, (p.radius) / 100, 1), teamMaterial)
const teamMarkerSize = 4000
teamMarker.scale.y = p.height / teamMarkerSize
teamMarker.position.y = (p.height / (teamMarkerSize * 2)) - (p.height / 100)
teamMarker.position.y += 0.01
teamMarker.layers.set(2)
projectile.add(teamMarker)
projectiles[p.id] = projectile
}
projectile.userData.flaggedForRemoval = false
positionTweens[projectile.id] = new Tween(projectile.position).to({ x: p.position.x / 100, y: p.position.y / 100, z: p.height / 100 }, tweenDuration).start()
}
for (const p of Object.values(projectiles)) {
if (p.userData.flaggedForRemoval) {
scene.remove(p)
delete projectiles[p.userData.id]
delete positionTweens[p.userData.id]
}
}
for (const t of state.terrains ?? []) {
let terrain
if (t.id in terrains) {
terrain = terrains[t.id]
}
else {
const vertices = t.relativeVertices
const shape = new THREE.Shape()
shape.moveTo(vertices.at(0).x / 100, vertices.at(0).y / 100)
vertices.slice(1).forEach((v) => shape.lineTo(v.x / 100, v.y / 100))
terrain = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { bevelEnabled: false, depth: t.collision ? 0.5 : 0.35 }), t.collision ? terrainMaterial : passableTerrainMaterial)
terrain.userData.type = 'terrain'
terrain.userData.id = t.id
scene.add(terrain)
terrains[t.id] = terrain
// // TODO: bboxes aren't tracked and can leak memory
// const bboxValues = Object.values(t.bbox)
// if (bboxValues.length >= 4) {
// const width = (bboxValues[1] - bboxValues[3]) / 100
// const height = (bboxValues[0] - bboxValues[2]) / 100
// const bbox = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.2), bboxMaterial)
// bbox.position.set((bboxValues[3] / 100) + (width / 2), (bboxValues[2] / 100) + (height / 2), 0)
// bbox.layers.set(1)
// scene.add(bbox)
// }
}
terrain.position.set(t.position.x / 100, t.position.y / 100, 0)
}
if (playerId != null) {
const player = state.entities.find((e) => e.id == playerId)
if (player != null) {
const playerAbilities = player.abilities
let abilitiesHTML = ''
let i = 0
for (const [abilityKey, _abilityId] of Object.entries(playerAbilities)) {
i++
const abilityKeyText = abilityKey.toUpperCase()
const abilityTemplate = `<div id="ability-${i}" class="ability">${abilityKeyText}<div id="ability-${i}-cooldown" class="cooldown"></div><div id="ability-${i}-cooldown-text" class="cooldown-text"></div></div>`
abilitiesHTML += abilityTemplate
}
if (document.getElementById(`abilities`).innerHTML != abilitiesHTML) {
document.getElementById(`abilities`).innerHTML = abilitiesHTML
}
let abilityIndex = 0
for (const [_abilityKey, abilityId] of Object.entries(playerAbilities)) {
abilityIndex++
const ability = state.abilities.find((it) => it.id == abilityId)
const lastCast = player.cooldowns[ability.id] ?? -Infinity
const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0
const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick
let cssPercentage = '100%'
let text = ''
if (remainingCooldown > 0) {
const cooldownPercentage = 1 - (remainingCooldown / cooldownDuration)
cssPercentage = `${Math.round(100 * cooldownPercentage)}%`
if (remainingCooldown / state.tickRate <= 5) {
text = `${(Math.round(10 * remainingCooldown / state.tickRate) / 10).toFixed(1)}`
}
else {
text = `${Math.round(remainingCooldown / state.tickRate)}`
}
}
if (player.casting?.ability == ability.id) {
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)` // triangle
}
else {
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(0 ${cssPercentage}, 100% ${cssPercentage}, 100% 100%, 0 100%)`
}
document.getElementById(`ability-${abilityIndex}-cooldown-text`).innerHTML = text
}
let buffs = ``
player.buffs.forEach((b) => {
buffs += `<div class="buff"><div class="buff-body">${state.buffs.find((it) => it.id == b.id).name}</div></div>`
})
if (document.getElementById('buffs').innerHTML != buffs) {
document.getElementById('buffs').innerHTML = buffs
}
let castIndicatorDisplay = 'none'
if (player.casting != null) {
castIndicatorDisplay = 'block'
const ability = state.abilities.find((it) => it.id == player.casting.ability)
if (ability != null) {
const castDuration = (ability.castTime * state.tickRate) ?? 0
const remainingCastTime = (player.casting.timestamp + castDuration) - state.currentTick
let cssPercentage = '100%'
if (remainingCastTime > 0) {
const castPercentage = 1 - (remainingCastTime / castDuration)
cssPercentage = `${Math.round(100 * castPercentage)}%`
}
document.getElementById('cast_indicator_progress').style.clipPath = `polygon(0 0, ${cssPercentage} 0, ${cssPercentage} 100%, 0% 100%)`
document.getElementById('cast_indicator_name').innerHTML = ability.name ?? ''
}
}
document.getElementById('cast_indicator').style.display = castIndicatorDisplay
}
}
document.getElementById('state').innerHTML = JSON.stringify(stateUpdates, null, 2)
}
}
window.addEventListener('load', () => {
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
playerId = params.id
if (playerId == null) {
playerId = prompt('Player ID:')
}
connectWebSocket()
const canvas = renderer.domElement
canvas.classList.add('canvas')
window.addEventListener('mousedown', (event) => {
const intersect = raycastToGround()
if (intersect != null) {
const { x, y } = intersect
if (event.button == 0) {
websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y }))
}
if (event.button == 2) {
websocket.send(JSON.stringify({ action: 'move', id: playerId, x, y }))
}
}
})
window.addEventListener('keydown', (event) => {
const intersect = raycastToGround()
if (intersect != null) {
const { x, y } = intersect
if (event.code == 'KeyA') {
websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y }))
}
if (event.code == 'KeyX') {
websocket.send(JSON.stringify({ action: 'cast', slot: 'a', id: playerId, x, y }))
}
if (event.code == 'KeyS') {
websocket.send(JSON.stringify({ action: 'stop', id: playerId }))
}
if (event.code == 'KeyH') {
websocket.send(JSON.stringify({ action: 'halt', id: playerId }))
}
const alreadyBound = ['A', 'X', 'S', 'H']
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach((letter) => {
if (alreadyBound.includes(letter)) { return }
if (event.code == `Key${letter}`) {
websocket.send(JSON.stringify({ action: 'cast', slot: letter.toLowerCase(), id: playerId, x, y }))
}
})
}
})
window.addEventListener('wheel', (event) => {
if (event.deltaY < 0) {
camera.zoom += 0.2
if (camera.zoom > 3) {
camera.zoom = 3
}
camera.updateProjectionMatrix()
}
if (event.deltaY > 0) {
camera.zoom -= 0.2
if (camera.zoom < 1) {
camera.zoom = 1
}
camera.updateProjectionMatrix()
}
})
window.addEventListener('resize', (event) => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
window.addEventListener('contextmenu', (event) => event.preventDefault())
window.addEventListener('keydown', (event) => keysDown[event.code] = true)
window.addEventListener('keyup', (event) => keysDown[event.code] = false)
window.addEventListener('keydown', (event) => {
if (event.code == 'Space') {
cameraLocked = !cameraLocked
}
})
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX
mouse.y = event.clientY
})
document.body.appendChild(canvas)
const minimap = minimapRenderer.domElement
minimap.classList.add('minimap')
document.body.appendChild(minimap)
document.body.appendChild(stats.dom)
})
+190
View File
@@ -0,0 +1,190 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="author" content="Thayol">
<script type="importmap">
{
"imports": {
"three": "/three/build/three.module.js",
"three/addons/": "/three/examples/jsm/",
"@tweenjs/tween.js": "/@tweenjs/tween.js/dist/tween.esm.js",
"stats.js": "/stats.js/src/Stats.js"
}
}
</script>
<style>
* {
box-sizing: border-box;
}
html {
font-family: sans-serif;
}
html, body {
margin: 0;
padding: 0;
user-select: none;
}
.debug-panel {
display: none;
font-size: 0.8rem;
position: fixed;
opacity: 0.2;
overflow-y: scroll;
inset: 0 0 290px auto;
border-bottom-left-radius: 10px;
padding: 10px 10px 20px 20px;
background-color: white;
border: 5px solid gray;
border-top: none;
border-right: none;
width: 300px;
transition-duration: 0.2s;
transition-property: opacity;
}
.debug-panel:hover {
opacity: 1;
}
.minimap {
position: fixed;
inset: auto 0 0 auto;
border: 5px solid gray;
border-top-left-radius: 10px;
border-bottom: none;
border-right: none;
}
.abilities {
display: none;
position: fixed;
gap: 10px;
padding: 15px;
padding-bottom: 10px;
inset: auto 0 0 0;
width: fit-content;
margin: auto;
border: 5px solid gray;
background-color: black;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom: none;
}
.abilities:has(.ability) {
display: flex;
}
.ability {
position: relative;
flex: 1 0 0;
border: 1px solid white;
width: 75px;
height: 75px;
color: white;
}
.cooldown {
position: absolute;
top: 0;
left: 0;
width: 73px;
height: 73px;
background-color: grey;
opacity: 0.4;
}
.cooldown-text {
position: absolute;
top: 0;
left: 0;
width: 73px;
height: 73px;
line-height: 73px;
text-align: center;
color: white;
font-family: monospace;
}
.cast-indicator-wrapper {
display: none;
position: fixed;
inset: auto 0 30%;
width: 400px;
margin: auto;
}
.cast-indicator-progress {
position: absolute;
background-color: #edd9ff;
width: calc(100% - 4px);
height: calc(100% - 4px);
}
.cast-indicator-name {
text-align: center;
color: white;
text-shadow: 1px 1px 2px black, 0 0 1em dimgray, 0 0 0.2em dimgray;
}
.cast-indicator-bar {
position: relative;
background-color: dimgray;
width: 100%;
height: 20px;
padding: 2px;
}
.buffs {
position: fixed;
display: flex;
gap: 10px;
inset: auto 0 120px calc(50vw - 165px);
width: fit-content;
}
.buff {
flex: 1 0 0;
width: 30px;
height: 30px;
background-color: black;
/* border: 1px solid gray; */
border-right: 1px solid gray;
color: white;
overflow: hidden;
}
.buff:hover {
overflow: visible;
z-index: 3;
}
.buff-body {
border: 1px solid gray;
padding: 5px;
background-color: black;
width: fit-content;
height: 100%;
}
</style>
</head>
<body>
<div class="debug-panel">
<p>Connection: <span id="connection"></span></p>
<pre id="state"></pre>
</div>
<div id="cast_indicator" class="cast-indicator-wrapper">
<div id="cast_indicator_name" class="cast-indicator-name"></div>
<div class="cast-indicator-bar">
<div id="cast_indicator_progress" class="cast-indicator-progress"></div>
</div>
</div>
<div id="abilities" class="abilities">
</div>
<div id="buffs" class="buffs"></div>
<script type="module" src="client.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

-179
View File
@@ -1,179 +0,0 @@
[gd_scene load_steps=21 format=3 uid="uid://dvqj0souma3mh"]
[ext_resource type="Script" path="res://scripts/runner.gd" id="1_hjhpa"]
[ext_resource type="Script" path="res://scripts/multiplayer/input.gd" id="2_ktv5u"]
[ext_resource type="Material" uid="uid://diptcpjxid3cm" path="res://materials/chaser.tres" id="3_tvy4p"]
[ext_resource type="Script" path="res://scripts/states/state_machine.gd" id="4_ttxqy"]
[ext_resource type="Script" path="res://scripts/states/idle.gd" id="5_vepvv"]
[ext_resource type="Script" path="res://scripts/states/run.gd" id="6_fllo7"]
[ext_resource type="Script" path="res://scripts/states/fall.gd" id="7_0e04j"]
[ext_resource type="Script" path="res://scripts/states/dead.gd" id="9_8je4a"]
[ext_resource type="Script" path="res://scripts/states/attack.gd" id="9_nqccg"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_ukf45"]
properties/0/path = NodePath(".:player_id")
properties/0/spawn = true
properties/0/replication_mode = 2
properties/1/path = NodePath(".:server_position")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:server_rotation")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath(".:dead")
properties/3/spawn = true
properties/3/replication_mode = 1
properties/4/path = NodePath("RotationBase/AttackHitbox:visible")
properties/4/spawn = true
properties/4/replication_mode = 1
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1agtp"]
properties/0/path = NodePath("Input:direction")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath("Input:walk")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath("Input:primary_interact")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath("Input:secondary_interact")
properties/3/spawn = true
properties/3/replication_mode = 1
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_j6tb3"]
radius = 0.3
height = 1.8
[sub_resource type="CapsuleMesh" id="CapsuleMesh_di3a0"]
radius = 0.3
height = 1.8
[sub_resource type="PrismMesh" id="PrismMesh_fcj1v"]
[sub_resource type="SphereMesh" id="SphereMesh_tudvv"]
[sub_resource type="SphereMesh" id="SphereMesh_1gltg"]
[sub_resource type="BoxShape3D" id="BoxShape3D_rsamr"]
size = Vector3(1, 1.75, 1.5)
[sub_resource type="BoxMesh" id="BoxMesh_phaav"]
size = Vector3(1, 1.75, 1.5)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ssauw"]
albedo_color = Color(1, 0, 0, 1)
[sub_resource type="SphereShape3D" id="SphereShape3D_wsx1k"]
[node name="Chaser" type="CharacterBody3D" node_paths=PackedStringArray("state_machine")]
script = ExtResource("1_hjhpa")
state_machine = NodePath("StateMachine")
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_ukf45")
[node name="Input" type="MultiplayerSynchronizer" parent="."]
unique_name_in_owner = true
replication_config = SubResource("SceneReplicationConfig_1agtp")
script = ExtResource("2_ktv5u")
[node name="CameraPivot" type="Node3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="Collider" type="CollisionShape3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
shape = SubResource("CapsuleShape3D_j6tb3")
[node name="RotationBase" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="Skin" type="Node3D" parent="RotationBase"]
unique_name_in_owner = true
[node name="MainBody" type="MeshInstance3D" parent="RotationBase/Skin"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
mesh = SubResource("CapsuleMesh_di3a0")
skeleton = NodePath("../../..")
surface_material_override/0 = ExtResource("3_tvy4p")
[node name="Beak" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.35, 0, 0, 0, -0.105655, 0.0906308, 0, -0.226577, -0.0422618, 0, 0.45, -0.3)
mesh = SubResource("PrismMesh_fcj1v")
[node name="RightEye" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0.1, 0.6, -0.25)
mesh = SubResource("SphereMesh_tudvv")
[node name="LeftEye" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, -0.1, 0.6, -0.25)
mesh = SubResource("SphereMesh_1gltg")
[node name="AttackHitbox" type="Area3D" parent="RotationBase"]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -0.75)
visible = false
monitorable = false
[node name="AttackCollider" type="CollisionShape3D" parent="RotationBase/AttackHitbox"]
shape = SubResource("BoxShape3D_rsamr")
[node name="AttackVisualBox" type="MeshInstance3D" parent="RotationBase/AttackHitbox"]
transparency = 0.8
mesh = SubResource("BoxMesh_phaav")
surface_material_override/0 = SubResource("StandardMaterial3D_ssauw")
[node name="AttackTimer" type="Timer" parent="RotationBase/AttackHitbox"]
unique_name_in_owner = true
process_callback = 0
wait_time = 0.35
one_shot = true
[node name="AttackCooldown" type="Timer" parent="RotationBase/AttackHitbox"]
unique_name_in_owner = true
process_callback = 0
wait_time = 2.0
one_shot = true
autostart = true
[node name="FloatingCamera" type="Node" parent="."]
[node name="CameraPlatform" type="Node3D" parent="FloatingCamera"]
unique_name_in_owner = true
[node name="CameraSpringArm" type="SpringArm3D" parent="FloatingCamera/CameraPlatform"]
shape = SubResource("SphereShape3D_wsx1k")
spring_length = 3.5
[node name="Camera" type="Camera3D" parent="FloatingCamera/CameraPlatform/CameraSpringArm"]
unique_name_in_owner = true
[node name="StateMachine" type="Node" parent="." node_paths=PackedStringArray("current_state")]
script = ExtResource("4_ttxqy")
current_state = NodePath("Idle")
[node name="Idle" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("5_vepvv")
[node name="Run" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("6_fllo7")
[node name="Fall" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("7_0e04j")
[node name="Dead" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("9_8je4a")
[node name="Attack" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("9_nqccg")
[connection signal="delta_synchronized" from="Sync" to="." method="_on_sync_delta_synchronized"]
[connection signal="body_entered" from="RotationBase/AttackHitbox" to="StateMachine/Attack" method="_on_attack_body_entered"]
[connection signal="body_exited" from="RotationBase/AttackHitbox" to="StateMachine/Attack" method="_on_attack_body_exited"]
-3
View File
@@ -1,3 +0,0 @@
[gd_scene format=3 uid="uid://b6nq7wjyrroi0"]
[node name="EmptyNode" type="Node"]
-154
View File
@@ -1,154 +0,0 @@
[gd_scene load_steps=18 format=3 uid="uid://cw0ho53ruh87m"]
[sub_resource type="BoxShape3D" id="BoxShape3D_e1dyx"]
size = Vector3(16, 0.1, 20)
[sub_resource type="BoxMesh" id="BoxMesh_xfekw"]
[sub_resource type="BoxShape3D" id="BoxShape3D_cnq3u"]
size = Vector3(16, 0.1, 16)
[sub_resource type="BoxShape3D" id="BoxShape3D_dwa2s"]
size = Vector3(4, 0.1, 4)
[sub_resource type="BoxMesh" id="BoxMesh_lt54m"]
[sub_resource type="BoxShape3D" id="BoxShape3D_fgmeq"]
size = Vector3(9.479, 0.1, 4)
[sub_resource type="BoxMesh" id="BoxMesh_thq07"]
[sub_resource type="BoxShape3D" id="BoxShape3D_xn166"]
size = Vector3(16, 0.1, 20)
[sub_resource type="BoxMesh" id="BoxMesh_intbb"]
[sub_resource type="BoxShape3D" id="BoxShape3D_e0hgo"]
size = Vector3(1, 10, 20)
[sub_resource type="BoxMesh" id="BoxMesh_qljsj"]
[sub_resource type="BoxShape3D" id="BoxShape3D_0reu4"]
size = Vector3(1, 10, 20)
[sub_resource type="BoxMesh" id="BoxMesh_8hngc"]
[sub_resource type="BoxShape3D" id="BoxShape3D_yaygu"]
size = Vector3(16, 10, 1)
[sub_resource type="BoxMesh" id="BoxMesh_hwi1p"]
[sub_resource type="BoxShape3D" id="BoxShape3D_ktywj"]
size = Vector3(16, 10, 1)
[sub_resource type="BoxMesh" id="BoxMesh_lm401"]
[node name="House" type="Node3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0137916, -4.76837e-07, 0.0149183)
[node name="FirstFloor" type="StaticBody3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="FirstFloor"]
shape = SubResource("BoxShape3D_e1dyx")
[node name="MeshInstance3D" type="MeshInstance3D" parent="FirstFloor"]
transform = Transform3D(16, 0, 0, 0, 0.1, 0, 0, 0, 20, 0, 0, 0)
mesh = SubResource("BoxMesh_xfekw")
[node name="SecondFloor" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="SecondFloor"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -2)
shape = SubResource("BoxShape3D_cnq3u")
[node name="MeshInstance3D" type="MeshInstance3D" parent="SecondFloor"]
transform = Transform3D(16, 0, 0, 0, 0.1, 0, 0, 0, 16, 0, 0, -2)
mesh = SubResource("BoxMesh_xfekw")
[node name="CollisionShape3D2" type="CollisionShape3D" parent="SecondFloor"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 8)
shape = SubResource("BoxShape3D_dwa2s")
[node name="MeshInstance3D2" type="MeshInstance3D" parent="SecondFloor"]
transform = Transform3D(4, 0, 0, 0, 0.1, 0, 0, 0, 4, 6, 0, 8)
mesh = SubResource("BoxMesh_lt54m")
[node name="Stairs" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 8)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Stairs"]
transform = Transform3D(0.849893, -0.526956, 0, 0.526956, 0.849893, 0, 0, 0, 1, 0, 2.51, 0)
shape = SubResource("BoxShape3D_fgmeq")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Stairs"]
transform = Transform3D(8.05613, -0.0526956, 0, 4.99501, 0.0849893, 0, 0, 0, 4, 0, 2.51, 0)
mesh = SubResource("BoxMesh_thq07")
[node name="Ceiling" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ceiling"]
shape = SubResource("BoxShape3D_xn166")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ceiling"]
transform = Transform3D(16, 0, 0, 0, 0.1, 0, 0, 0, 20, 0, 0, 0)
mesh = SubResource("BoxMesh_intbb")
[node name="Wall1" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Wall1"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
shape = SubResource("BoxShape3D_e0hgo")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Wall1"]
transform = Transform3D(1, 0, 0, 0, 10, 0, 0, 0, 20, 0, 5, 0)
mesh = SubResource("BoxMesh_qljsj")
[node name="Wall2" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 0, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Wall2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
shape = SubResource("BoxShape3D_0reu4")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Wall2"]
transform = Transform3D(1, 0, 0, 0, 10, 0, 0, 0, 20, 0, 5, 0)
mesh = SubResource("BoxMesh_8hngc")
[node name="Wall3" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -10)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Wall3"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
shape = SubResource("BoxShape3D_yaygu")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Wall3"]
transform = Transform3D(16, 0, 0, 0, 10, 0, 0, 0, 1, 0, 5, 0)
mesh = SubResource("BoxMesh_hwi1p")
[node name="Wall4" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 10)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Wall4"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
shape = SubResource("BoxShape3D_ktywj")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Wall4"]
transform = Transform3D(16, 0, 0, 0, 10, 0, 0, 0, 1, 0, 5, 0)
mesh = SubResource("BoxMesh_lm401")
[node name="Lights" type="Node3D" parent="."]
[node name="FirstFloorLight" type="OmniLight3D" parent="Lights"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0)
light_energy = 0.5
shadow_enabled = true
omni_range = 40.0
[node name="SecondFloorLight" type="OmniLight3D" parent="Lights"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9, 0)
light_energy = 0.5
shadow_enabled = true
omni_range = 40.0
-101
View File
@@ -1,101 +0,0 @@
[gd_scene load_steps=10 format=3 uid="uid://d22gcvp7p2sfr"]
[ext_resource type="Script" path="res://scripts/in_game.gd" id="1_0lma2"]
[ext_resource type="PlaneMesh" uid="uid://dwpvym2kc4gd8" path="res://meshes/ground.tres" id="1_7j0qh"]
[ext_resource type="Material" uid="uid://chp3rogcgumau" path="res://materials/ground.tres" id="2_f8uto"]
[sub_resource type="Environment" id="Environment_2c67a"]
background_mode = 1
ambient_light_source = 2
ambient_light_color = Color(1, 1, 1, 1)
ambient_light_energy = 0.15
tonemap_mode = 3
sdfgi_read_sky_light = false
glow_enabled = true
glow_normalized = true
volumetric_fog_enabled = true
volumetric_fog_albedo = Color(0.381703, 0.381703, 0.381703, 1)
volumetric_fog_emission = Color(0.314009, 2.89988e-06, 1.52815e-06, 1)
volumetric_fog_emission_energy = 0.25
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_1l61b"]
[sub_resource type="BoxMesh" id="BoxMesh_kcty4"]
[sub_resource type="BoxShape3D" id="BoxShape3D_6koow"]
[sub_resource type="BoxShape3D" id="BoxShape3D_y3ciy"]
[sub_resource type="BoxMesh" id="BoxMesh_2wvq0"]
[node name="InGame" type="Node3D"]
script = ExtResource("1_0lma2")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_2c67a")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.258819, 0.965926, 0, -0.965926, 0.258819, 0, 10.0494, 0)
[node name="ChaserSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/chaser.tscn")
spawn_path = NodePath("../Chasers")
[node name="RunnerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://scenes/runner.tscn")
spawn_path = NodePath("../Runners")
[node name="Ground" type="StaticBody3D" parent="."]
[node name="GroundCollider" type="CollisionShape3D" parent="Ground"]
shape = SubResource("WorldBoundaryShape3D_1l61b")
[node name="GroundMesh" type="MeshInstance3D" parent="Ground"]
mesh = ExtResource("1_7j0qh")
surface_material_override/0 = ExtResource("2_f8uto")
[node name="Chasers" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="Runners" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="StatsOverlay" type="CanvasLayer" parent="."]
[node name="StatsContainer" type="Control" parent="StatsOverlay"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="StatsLabel" type="RichTextLabel" parent="StatsOverlay/StatsContainer"]
unique_name_in_owner = true
layout_mode = 0
offset_right = 200.0
offset_bottom = 50.0
[node name="PingTimer" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 0.5
autostart = true
[node name="StaticBody3D" type="StaticBody3D" parent="."]
transform = Transform3D(0.96321, 0, 0.268752, 0, 1, 0, -0.268752, 0, 0.96321, 3, 0.5, 0)
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D"]
mesh = SubResource("BoxMesh_kcty4")
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
shape = SubResource("BoxShape3D_6koow")
[node name="MainObjective" type="StaticBody3D" parent="."]
[node name="CollisionShape3D" type="CollisionShape3D" parent="MainObjective"]
shape = SubResource("BoxShape3D_y3ciy")
[node name="MeshInstance3D" type="MeshInstance3D" parent="MainObjective"]
mesh = SubResource("BoxMesh_2wvq0")
[connection signal="timeout" from="PingTimer" to="." method="_on_ping_timer_timeout"]
-85
View File
@@ -1,85 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://qiew7wf7r8c4"]
[ext_resource type="Script" path="res://scripts/main_menu.gd" id="1_ivhyr"]
[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_ivhyr")
[node name="Panel" type="Panel" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Background" type="ColorRect" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.156863, 0.156863, 0.305882, 1)
[node name="Center" type="CenterContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VerticalMenu" type="VBoxContainer" parent="Panel/Center"]
layout_mode = 2
[node name="IpLabel" type="RichTextLabel" parent="Panel/Center/VerticalMenu"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
size_flags_vertical = 4
bbcode_enabled = true
text = "[center]Server Address[/center]"
scroll_active = false
[node name="IpTextBox" type="TextEdit" parent="Panel/Center/VerticalMenu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_vertical = 4
placeholder_text = "localhost (default)"
[node name="JoinButtonSpacer" type="Control" parent="Panel/Center/VerticalMenu"]
custom_minimum_size = Vector2(0, 15)
layout_mode = 2
[node name="JoinLabel" type="RichTextLabel" parent="Panel/Center/VerticalMenu"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
size_flags_vertical = 4
bbcode_enabled = true
text = "[center]Join as[/center]"
scroll_active = false
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/Center/VerticalMenu"]
layout_mode = 2
[node name="JoinAsRunnerButton" type="Button" parent="Panel/Center/VerticalMenu/HBoxContainer"]
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
text = "Runner"
text_overrun_behavior = 3
[node name="JoinAsChaserButton" type="Button" parent="Panel/Center/VerticalMenu/HBoxContainer"]
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
text = "Chaser"
text_overrun_behavior = 3
[connection signal="pressed" from="Panel/Center/VerticalMenu/HBoxContainer/JoinAsRunnerButton" to="." method="_on_join_as_runner_button_pressed"]
[connection signal="pressed" from="Panel/Center/VerticalMenu/HBoxContainer/JoinAsChaserButton" to="." method="_on_join_as_chaser_button_pressed"]
-138
View File
@@ -1,138 +0,0 @@
[gd_scene load_steps=18 format=3 uid="uid://8esyynmieyog"]
[ext_resource type="Script" path="res://scripts/runner.gd" id="1_d63rt"]
[ext_resource type="Script" path="res://scripts/multiplayer/input.gd" id="2_xmliy"]
[ext_resource type="Material" uid="uid://ccrb46njti2ke" path="res://materials/runner.tres" id="3_6c0ro"]
[ext_resource type="Script" path="res://scripts/states/state_machine.gd" id="4_40cmc"]
[ext_resource type="Script" path="res://scripts/states/idle.gd" id="5_hq6tn"]
[ext_resource type="Script" path="res://scripts/states/run.gd" id="6_1teax"]
[ext_resource type="Script" path="res://scripts/states/fall.gd" id="7_jfat4"]
[ext_resource type="Script" path="res://scripts/states/walk.gd" id="8_phh70"]
[ext_resource type="Script" path="res://scripts/states/dead.gd" id="9_bw4yb"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_ukf45"]
properties/0/path = NodePath(".:player_id")
properties/0/spawn = true
properties/0/replication_mode = 2
properties/1/path = NodePath(".:server_position")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:server_rotation")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath(".:dead")
properties/3/spawn = true
properties/3/replication_mode = 1
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1agtp"]
properties/0/path = NodePath("Input:direction")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath("Input:walk")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath("Input:primary_interact")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath("Input:secondary_interact")
properties/3/spawn = true
properties/3/replication_mode = 1
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_j6tb3"]
radius = 0.3
height = 1.8
[sub_resource type="CapsuleMesh" id="CapsuleMesh_di3a0"]
radius = 0.3
height = 1.8
[sub_resource type="PrismMesh" id="PrismMesh_fcj1v"]
[sub_resource type="SphereMesh" id="SphereMesh_tudvv"]
[sub_resource type="SphereMesh" id="SphereMesh_1gltg"]
[sub_resource type="SphereShape3D" id="SphereShape3D_wsx1k"]
[node name="Runner" type="CharacterBody3D" node_paths=PackedStringArray("state_machine")]
script = ExtResource("1_d63rt")
state_machine = NodePath("StateMachine")
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_ukf45")
[node name="Input" type="MultiplayerSynchronizer" parent="."]
unique_name_in_owner = true
replication_config = SubResource("SceneReplicationConfig_1agtp")
script = ExtResource("2_xmliy")
[node name="CameraPivot" type="Node3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
[node name="Collider" type="CollisionShape3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
shape = SubResource("CapsuleShape3D_j6tb3")
[node name="RotationBase" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="Skin" type="Node3D" parent="RotationBase"]
unique_name_in_owner = true
[node name="MainBody" type="MeshInstance3D" parent="RotationBase/Skin"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
mesh = SubResource("CapsuleMesh_di3a0")
skeleton = NodePath("../../..")
surface_material_override/0 = ExtResource("3_6c0ro")
[node name="Beak" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.35, 0, 0, 0, -0.105655, 0.0906308, 0, -0.226577, -0.0422618, 0, 0.45, -0.3)
mesh = SubResource("PrismMesh_fcj1v")
[node name="RightEye" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0.1, 0.6, -0.25)
mesh = SubResource("SphereMesh_tudvv")
[node name="LeftEye" type="MeshInstance3D" parent="RotationBase/Skin/MainBody"]
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, -0.1, 0.6, -0.25)
mesh = SubResource("SphereMesh_1gltg")
[node name="FloatingCamera" type="Node" parent="."]
[node name="CameraPlatform" type="Node3D" parent="FloatingCamera"]
unique_name_in_owner = true
[node name="CameraSpringArm" type="SpringArm3D" parent="FloatingCamera/CameraPlatform"]
shape = SubResource("SphereShape3D_wsx1k")
spring_length = 3.5
[node name="Camera" type="Camera3D" parent="FloatingCamera/CameraPlatform/CameraSpringArm"]
unique_name_in_owner = true
[node name="StateMachine" type="Node" parent="." node_paths=PackedStringArray("current_state")]
script = ExtResource("4_40cmc")
current_state = NodePath("Idle")
[node name="Idle" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("5_hq6tn")
[node name="Run" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("6_1teax")
[node name="Fall" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("7_jfat4")
[node name="Walk" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("8_phh70")
[node name="Dead" type="Node" parent="StateMachine"]
unique_name_in_owner = true
script = ExtResource("9_bw4yb")
[connection signal="delta_synchronized" from="Sync" to="." method="_on_sync_delta_synchronized"]
-55
View File
@@ -1,55 +0,0 @@
extends Node3D
const statsTemplate := "FPS: %d\nPing: %d ms"
var runner_scene := preload("res://scenes/runner.tscn")
var chaser_scene := preload("res://scenes/chaser.tscn")
var _ping := 0.0
@onready var _chasers_node: Node3D = %Chasers
@onready var _runners_node: Node3D = %Runners
func spawn_player(player_id: int, runner: bool) -> void:
if not multiplayer.is_server(): return
var player = runner_scene.instantiate() if runner else chaser_scene.instantiate()
player.player_id = player_id
player.name = str(player_id)
if runner:
_runners_node.add_child(player)
else:
_chasers_node.add_child(player)
func despawn_player(player_id: int) -> void:
if not multiplayer.is_server(): return
var player_id_str := str(player_id)
for runner_or_chaser in _runners_node.get_children() + _chasers_node.get_children():
if runner_or_chaser.name == player_id_str:
runner_or_chaser.queue_free()
func _process(_delta: float) -> void:
if DisplayServer.get_name() == "headless": return
%StatsLabel.text = statsTemplate % [
Engine.get_frames_per_second(),
_ping,
]
func _get_timestamp() -> int:
return floor(Time.get_unix_time_from_system() * 1000)
func _on_ping_timer_timeout() -> void:
if multiplayer.is_server():
%PingTimer.stop()
return
_ping_call.rpc_id(1, _get_timestamp())
@rpc("any_peer", "call_remote", "unreliable", 99)
func _ping_call(timestamp: int) -> void:
if multiplayer.is_server():
_ping_call.rpc_id(multiplayer.get_remote_sender_id(), timestamp)
return
_ping = _get_timestamp() - timestamp
-1
View File
@@ -1 +0,0 @@
uid://c3ks2lbj65erq
-10
View File
@@ -1,10 +0,0 @@
extends Node
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
var gravity_vector: Vector3 = ProjectSettings.get_setting("physics/3d/default_gravity_vector")
var physics_tickrate = Engine.get_physics_ticks_per_second()
var terminal_velocity: float = 150.0
var gravity_velocity: Vector3 = (gravity_vector * gravity) / (physics_tickrate as float)
func is_server_or_predicting(player_id: int, client_prediction: bool = false):
return multiplayer.is_server() or (multiplayer.get_unique_id() == player_id and client_prediction)
-1
View File
@@ -1 +0,0 @@
uid://b3vuso52nyr8p
-9
View File
@@ -1,9 +0,0 @@
extends Control
func _on_join_as_runner_button_pressed() -> void:
MultiplayerManager.connect_to_ip(true, %IpTextBox.text)
get_tree().change_scene_to_file("res://scenes/in_game.tscn")
func _on_join_as_chaser_button_pressed() -> void:
MultiplayerManager.connect_to_ip(false, %IpTextBox.text)
get_tree().change_scene_to_file("res://scenes/in_game.tscn")
-1
View File
@@ -1 +0,0 @@
uid://dhgarroknbyh0
-28
View File
@@ -1,28 +0,0 @@
extends MultiplayerSynchronizer
@export var direction: Vector2 = Vector2.ZERO
@export var walk: bool = false
@export var primary_interact: bool = false
@export var secondary_interact: bool = false
@onready var _camera_pivot: Node3D = %CameraPivot
func _ready() -> void:
if multiplayer.is_server() or get_multiplayer_authority() != multiplayer.get_unique_id():
set_process(false)
set_physics_process(false)
func _physics_process(_delta: float) -> void:
if get_multiplayer_authority() != multiplayer.get_unique_id(): return
var directional_input := Input.get_vector("move_right", "move_left", "move_forward", "move_back")
var camera_adjusted: Vector3 = (directional_input.x * _camera_pivot.global_basis.z) + (directional_input.y * _camera_pivot.global_basis.x)
direction = Vector2(camera_adjusted.x, camera_adjusted.z).rotated(PI / 2.0).normalized()
walk = Input.is_action_pressed("walk")
func _input(_event: InputEvent) -> void:
if get_multiplayer_authority() != multiplayer.get_unique_id(): return
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED: return
primary_interact = Input.is_action_pressed("primary_interact")
secondary_interact = Input.is_action_pressed("secondary_interact")
-1
View File
@@ -1 +0,0 @@
uid://tt4s3jkcyqt
@@ -1,70 +0,0 @@
extends Node
const PORT = 1280
const IP_ADDRESS = "127.0.0.1"
const MAX_CLIENTS = 5
var in_game_scene := preload("res://scenes/in_game.tscn")
var main_menu_scene := preload("res://scenes/main_menu.tscn")
var runner_client_selection := true
var runner_dict := {}
func _ready() -> void:
set_process(false)
set_physics_process(false)
if OS.has_feature("dedicated_server"):
get_tree().change_scene_to_packed.call_deferred(in_game_scene)
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_connect)
multiplayer.peer_disconnected.connect(_on_disconnect)
else:
get_tree().change_scene_to_packed.call_deferred(main_menu_scene)
func connect_to_ip(runner: bool = true, ip: String = "") -> void:
if ip.is_empty(): ip = IP_ADDRESS
runner_client_selection = runner
print("Connecting to: %s" % ip)
var peer = ENetMultiplayerPeer.new()
peer.create_client(ip, PORT)
multiplayer.multiplayer_peer = peer
multiplayer.connected_to_server.connect(_on_connect_client)
multiplayer.connection_failed.connect(_on_disconnect_client)
multiplayer.server_disconnected.connect(_on_server_closed_client)
func _on_connect(id: int) -> void:
print("Client ID #%s connected" % id)
func _on_disconnect(id: int) -> void:
print("Client ID #%s disconnected" % id)
if not multiplayer.is_server(): return
get_tree().get_current_scene().despawn_player(id)
func _on_connect_client() -> void:
request_is_runner.rpc_id(1, runner_client_selection)
print("[%s] Connected to server" % multiplayer.get_unique_id())
func _on_disconnect_client() -> void:
print("[%s] Disconnected" % multiplayer.get_unique_id())
func _on_server_closed_client() -> void:
print("[%s] Server closed" % multiplayer.get_unique_id())
@rpc("any_peer", "call_remote", "reliable")
func request_is_runner(runner: bool) -> void:
if not multiplayer.is_server(): return
var id := multiplayer.get_remote_sender_id()
runner_dict[id] = runner
print("Runner Dict: %s" % runner_dict)
get_tree().get_current_scene().spawn_player(id, runner)
@@ -1 +0,0 @@
uid://cm5vnqe8f0f2u
-122
View File
@@ -1,122 +0,0 @@
extends CharacterBody3D
@export var player_id := 69:
set(id):
player_id = id
%Input.set_multiplayer_authority(id)
@export var state_machine: Node
@export_group("Camera")
@export_range(0.0, 1.0) var mouse_sensitivity := 0.2
@export var camera_follow_speed := 20.0
@export var smooth_camera := false
@export_group("Movement")
@export var run_speed := 8.0
@export var walk_speed := 4.0
@export var acceleration := 45.0
@export var air_deceleration := 5.0
@export var rotation_speed := 20.0
@export_group("Client Smoothing")
@export var client_smoothing := false
@export var client_smoothing_speed := 10.0
@export var client_smoothing_rotation_speed := 25.0
@export var client_prediction := false
@export var client_prediction_tolerance := 0.3
@export_group("Server variables")
@export var server_position := Vector3.ZERO
@export var server_rotation := Vector3.ZERO
@export var player := true
@export var dead := false
@onready var _camera_pivot: Node3D = %CameraPivot
@onready var _camera_platform: Node3D = %CameraPlatform
@onready var _camera: Node3D = %Camera
@onready var rotation_base: Node3D = %RotationBase
@onready var collider: Node3D = %Collider
var camera_input_direction := Vector2.ZERO
var last_direction := Vector3.FORWARD
var predicted_position := Vector3.ZERO
var predicted_rotation := Vector3.ZERO
func _ready() -> void:
state_machine.set_subject(self)
if multiplayer.is_server(): return
if multiplayer.get_unique_id() == player_id:
_camera.make_current()
else:
_camera.clear_current(false)
func _input(event: InputEvent) -> void:
if multiplayer.get_unique_id() != player_id: return
if event.is_action_pressed("mouse_capture"):
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
elif event.is_action_pressed("mouse_release"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
if event is InputEventMouseMotion:
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
camera_input_direction += event.screen_relative * mouse_sensitivity
func _process(delta: float) -> void:
if multiplayer.get_unique_id() != player_id: return
_camera_platform.global_rotation = _camera_pivot.global_rotation
if smooth_camera:
_camera_platform.global_position = lerp(_camera_platform.global_position, _camera_pivot.global_position, camera_follow_speed * delta)
else:
_camera_platform.global_position = _camera_pivot.global_position
func _physics_process(delta: float) -> void:
state_machine.process_physics(delta)
if multiplayer.get_unique_id() == player_id:
_camera_pivot.rotation.x = clamp(
_camera_pivot.rotation.x - camera_input_direction.y * delta,
-PI / 3.0,
PI / 6.0,
)
_camera_pivot.rotation.y -= camera_input_direction.x * delta
camera_input_direction = Vector2.ZERO
if not multiplayer.is_server():
if multiplayer.get_unique_id() == player_id and client_prediction:
predicted_position = position
predicted_rotation = rotation_base.global_rotation
if multiplayer.get_unique_id() != player_id or not client_prediction:
if client_smoothing:
position = lerp(position, server_position, client_smoothing_speed * delta)
rotation_base.global_rotation.x = server_rotation.x
rotation_base.global_rotation.y = lerp_angle(rotation_base.global_rotation.y, server_rotation.y, client_smoothing_rotation_speed * delta)
rotation_base.global_rotation.z = server_rotation.z
else:
position = server_position
rotation_base.global_rotation = server_rotation
#
if multiplayer.is_server():
server_position = position
server_rotation = rotation_base.global_rotation
func _on_sync_delta_synchronized() -> void:
if client_prediction and server_position.distance_to(predicted_position) > client_prediction_tolerance:
print("%v VS %v" % [server_position, predicted_position])
position = server_position
func apply_input_velocity(delta: float, input_direction: Vector2, speed: float, acc: float) -> void:
var target_velocity = Vector3(input_direction.x * speed, velocity.y, input_direction.y * speed)
velocity = velocity.move_toward(target_velocity, acc * delta) + Main.gravity_velocity
move_and_slide()
if input_direction.length() >= 0.1:
last_direction = Vector3(input_direction.x, 0, input_direction.y)
var target_angle := Vector3.FORWARD.signed_angle_to(last_direction, Vector3.UP)
rotation_base.global_rotation.y = lerp_angle(rotation_base.rotation.y, target_angle, rotation_speed * delta)
-1
View File
@@ -1 +0,0 @@
uid://d3hbh4mxk38n6
-42
View File
@@ -1,42 +0,0 @@
extends State
var hit = false
var players_in_area = {}
func enter() -> void:
super()
if has_node("%AttackHitbox"): %AttackHitbox.visible = true
if has_node("%AttackTimer"): %AttackTimer.start()
if has_node("%AttackCooldown"): %AttackCooldown.start()
hit = false
func exit() -> void:
super()
if has_node("%AttackHitbox"): %AttackHitbox.visible = false
func process_physics(delta: float) -> State:
if not Main.is_server_or_predicting(subject.player_id, subject.client_prediction): return
var input_direction = %Input.direction if subject.is_on_floor() else Vector2.ZERO
subject.apply_input_velocity(delta, input_direction, subject.run_speed, subject.acceleration if subject.is_on_floor() else subject.air_deceleration)
if players_in_area.size() > 0:
var hit_player = players_in_area[players_in_area.keys()[0]]
hit_player.dead = true
hit = true
if has_node("%AttackTimer") and %AttackTimer.time_left > 0 and not hit: return
if not subject.is_on_floor(): return %Fall
if input_direction.length() < 0.05: return %Idle
if has_node("%Walk") and %Input.walk: return %Walk
return %Run
func _on_attack_body_entered(body: Node3D) -> void:
if body == subject: return
if not "player" in body or not body.player: return
if body.dead: return
players_in_area[body.name] = body
func _on_attack_body_exited(body: Node3D) -> void:
players_in_area.erase(body.name)
-1
View File
@@ -1 +0,0 @@
uid://bu8fv7b2ndq3
-17
View File
@@ -1,17 +0,0 @@
extends State
func enter() -> void:
super()
subject.rotation_base.global_rotation.z = PI / 2
subject.collider.disabled = true
func exit() -> void:
super()
subject.rotation_base.global_rotation.z = 0
subject.collider.disabled = false
func process_physics(_delta: float) -> State:
if not multiplayer.is_server(): return
if not subject.dead: return %Idle
return
-1
View File
@@ -1 +0,0 @@
uid://c0u1dn17o73ox
-12
View File
@@ -1,12 +0,0 @@
extends State
func process_physics(delta: float) -> State:
if not Main.is_server_or_predicting(subject.player_id, subject.client_prediction): return
subject.apply_input_velocity(delta, Vector2.ZERO, 0.0, subject.air_deceleration)
if not subject.is_on_floor(): return
if has_node("%Dead") and subject.dead: return %Dead
if %Input.direction.length() < 0.05: return %Idle
if has_node("%Walk") and %Input.walk: return %Walk
return %Run
-1
View File
@@ -1 +0,0 @@
uid://xwmho0fyx2rj
-14
View File
@@ -1,14 +0,0 @@
extends State
func process_physics(delta: float) -> State:
if not Main.is_server_or_predicting(subject.player_id, subject.client_prediction): return
var input_direction = %Input.direction
subject.apply_input_velocity(delta, Vector2.ZERO, 0.0, subject.acceleration)
if not subject.is_on_floor(): return %Fall
if has_node("%Dead") and subject.dead: return %Dead
if has_node("%Attack") and %Input.primary_interact and (not has_node("%AttackCooldown") or %AttackCooldown.time_left == 0): return %Attack
if input_direction.length() < 0.05: return
if has_node("%Walk") and %Input.walk: return %Walk
return %Run
-1
View File
@@ -1 +0,0 @@
uid://beydsuuy3og2r
-14
View File
@@ -1,14 +0,0 @@
extends State
func process_physics(delta: float) -> State:
if not Main.is_server_or_predicting(subject.player_id, subject.client_prediction): return
var input_direction = %Input.direction
subject.apply_input_velocity(delta, input_direction, subject.run_speed, subject.acceleration)
if not subject.is_on_floor(): return %Fall
if has_node("%Dead") and subject.dead: return %Dead
if has_node("%Attack") and %Input.primary_interact and (not has_node("%AttackCooldown") or %AttackCooldown.time_left == 0): return %Attack
if input_direction.length() < 0.05: return %Idle
if has_node("%Walk") and %Input.walk: return %Walk
return
-1
View File
@@ -1 +0,0 @@
uid://ciltmpb1wqqpr
-20
View File
@@ -1,20 +0,0 @@
class_name State
extends Node
var subject
func enter() -> void:
print("Entering state: %s" % name)
pass
func exit() -> void:
pass
func process_input(_event: InputEvent) -> State:
return null
func process_frame(_delta: float) -> State:
return null
func process_physics(_delta: float) -> State:
return null
-1
View File
@@ -1 +0,0 @@
uid://dikih6wn2rwsk
-36
View File
@@ -1,36 +0,0 @@
extends Node
@export var current_state: State
var subject: Node
func set_subject(new_subject: Node) -> void:
subject = new_subject
for state in get_children():
state.subject = subject
func _change_state(new_state: State) -> void:
if current_state: current_state.exit()
current_state = new_state
current_state.enter()
func process_physics(delta: float) -> void:
if not current_state or not current_state.subject: return
var new_state = current_state.process_physics(delta)
if new_state:
_change_state(new_state)
func process_input(event: InputEvent) -> void:
if not current_state or not current_state.subject: return
var new_state = current_state.process_input(event)
if new_state:
_change_state(new_state)
func process_frame(delta: float) -> void:
if not current_state or not current_state.subject: return
var new_state = current_state.process_frame(delta)
if new_state:
_change_state(new_state)
-1
View File
@@ -1 +0,0 @@
uid://20wwhts3wq7t
-14
View File
@@ -1,14 +0,0 @@
extends State
func process_physics(delta: float) -> State:
if not Main.is_server_or_predicting(subject.player_id, subject.client_prediction): return
var input_direction = %Input.direction
subject.apply_input_velocity(delta, input_direction, subject.walk_speed, subject.acceleration)
if not subject.is_on_floor(): return %Fall
if has_node("%Dead") and subject.dead: return %Dead
if has_node("%Attack") and %Input.primary_interact and (not has_node("%AttackCooldown") or %AttackCooldown.time_left == 0): return %Attack
if input_direction.length() < 0.05: return %Idle
if not %Input.walk: return %Run
return
-1
View File
@@ -1 +0,0 @@
uid://5x0hciokrxcj
+320
View File
@@ -0,0 +1,320 @@
import Buff from './buff.js'
import Projectile from './projectile.js'
// Three classes: Blade, Armor, Charm
export default class Ability {
id = `ability-${Ability.nextId()}`
static nextId() { return this.#nextUniqueId++ }
static #nextUniqueId = 0
name = 'Ability'
castTime = null
cooldown = 0
damage = 0
moveCancelable = false
radius = 1
range = 0
speed = 1000
#effect = null
get effect() { return this.#effect ?? Ability.noEffect }
set effect(value) { this.#effect = value }
constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value)
}
static get noEffect() { return function noEffect() {} }
static blink = new Ability({
id: 'blink',
name: 'Blink',
cooldown: 10,
range: 475,
effect: function blinkEffect(caster, cursor) {
const ability = this
const direction = cursor.clone().sub(caster.position)
const realRange = ability.range + caster.radius
if (direction.length() > realRange) {
direction.normalize().multiplyScalar(realRange)
}
const destination = caster.position.clone().add(direction)
caster.teleport(destination)
caster.cooldown(ability.id)
},
})
static circleOfResurrection = new Ability({
id: 'circle_of_resurrection',
name: 'Circle of Resurrection',
castTime: 0.5,
cooldown: 100,
duration: 3,
moveCancelable: true,
radius: 300,
range: 300,
effect: function circleOfResurrectionEffect(caster, cursor) {
const ability = this
caster.haltAction()
const direction = cursor.clone().sub(caster.position)
if (direction.length() > ability.range) {
direction.normalize().multiplyScalar(ability.range)
}
const destination = caster.position.clone().add(direction)
const team = caster.team
const currentTick = caster.game?.currentTick ?? 0
const duration = caster.game?.secToTick(ability.duration) ?? 0
const despawnAfter = currentTick + duration
const casterPosition = caster.position.clone()
const circleOfResurrectionLogic = function castingVisionLogic(projectile) {
const currentTick = projectile.game?.currentTick ?? 0
if (casterPosition.distanceTo(caster.position) > 1) {
projectile.despawn()
}
if (currentTick > despawnAfter) {
const entities = projectile.game?.entities ?? []
const pos = projectile.position
projectile.despawn()
const nearbyDeadTeammates = entities.filter((it) => it.dead && it.team == team && it.distanceTo(pos) <= ability.radius)
const closestDeadTeammate = nearbyDeadTeammates.reduce((e1, e2) => (e1?.distanceTo(pos) ?? Infinity) < e2.distanceTo(pos) ? e1 : e2, null)
if (closestDeadTeammate != null) {
closestDeadTeammate.revive(closestDeadTeammate.maxHealth / 4)
caster.cooldown(ability.id)
}
}
}
const projectile = new Projectile({
logic: circleOfResurrectionLogic,
owner: caster.id,
position: destination,
radius: ability.radius,
visualRadius: 0,
})
caster.game?.spawnProjectile(projectile)
if (caster.casting != null) {
caster.forceCast(Ability.circleOfResurrectionChannel.id, destination)
}
},
})
static circleOfResurrectionChannel = new Ability({
id: 'channel:circle_of_resurrection',
name: 'Channeling: Circle of Resurrection',
castTime: 3,
moveCancelable: true,
})
static control = new Ability({
id: 'control',
name: 'Control',
castTime: 1,
cooldown: 5,
effect: function controlEffect(caster, cursor) { },
})
static expose = new Ability({
id: 'expose',
name: 'Expose',
castTime: 0.25,
cooldown: 6,
radius: 80,
range: 1200,
speed: 1700,
visualRadius: 50,
effect: function exposeEffect(caster, cursor) {
const ability = this
const exposeCollision = function exposeCollision(projectile, collidingEntity) {
if (projectile.game == null) { return }
if (collidingEntity == null) { return }
if (collidingEntity.team == caster.id) { return }
if (collidingEntity.team == (caster.team ?? 'unknown')) { return }
collidingEntity.applyBuff(Buff.exposed.id, caster.id)
projectile.despawn()
}
const projectile = new Projectile({
onCollide: exposeCollision,
owner: caster.id,
position: caster.position.clone(),
radius: ability.radius,
speed: ability.speed,
visualRadius: ability.visualRadius,
})
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius))
caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id)
},
})
static meleeAttack = new Ability({
id: 'melee_attack',
name: 'Melee Attack',
castTime: (1.4 * 0.22),
cooldown: 1.4,
moveCancelable: true,
damage: 60,
radius: 5,
range: 100,
effect: function meleeAttackEffect(caster, targetId) {
const ability = this
const target = caster.game?.entities.find((it) => it.id == targetId)
if (target == null) { return }
target.damage(ability.damage, caster)
caster.cooldown(ability.id)
},
})
static rangedAttack = new Ability({
id: 'ranged_attack',
name: 'Ranged Attack',
castTime: (1.6 * 0.18839),
cooldown: 1.6,
damage: 60,
moveCancelable: true,
radius: 5,
range: 500,
speed: 2000,
effect: function rangedAttackEffect(caster, targetId) {
const ability = this
const target = caster.game?.entities.find((it) => it.id == targetId)
if (target == null) { return }
const rangedAttackAfter = function rangedAttackAfter() {
target.damage(ability.damage, caster)
}
const projectile = new Projectile({
after: rangedAttackAfter,
homingTarget: target,
owner: caster.id,
position: caster.position.clone(),
radius: ability.radius,
speed: ability.speed,
})
caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id)
},
})
static shieldThrow = new Ability({
id: 'shield_throw',
name: 'Shield Throw',
castTime: 0.25,
cooldown: 5,
damage: 90,
deceleratePerTick: 90,
radius: 110,
range: 1025,
speed: 2400,
effect: function shieldThrowEffect(caster, cursor) {
const ability = this
const amount = ability.damage
let onTheWayBack = false
let collided = new Set()
const shieldThrowCollision = function shieldThrowCollision(projectile, collidingEntity) {
if (collidingEntity == null) { return }
if (collidingEntity.id == caster.id) { return }
if (caster.team == null || collidingEntity.team == null || collidingEntity.team != caster.team) { return }
const entityId = collidingEntity.id
if (!collided.has(entityId)) {
collidingEntity.heal(amount, caster)
collided.add(entityId)
}
}
const accelerateLogic = function accelerateLogic(projectile) {
if (onTheWayBack) {
projectile.speed += ability.deceleratePerTick
}
else {
if (projectile.speed - ability.deceleratePerTick >= ability.deceleratePerTick) {
projectile.speed = projectile.speed - ability.deceleratePerTick
}
}
}
const shieldThrowSecondAfter = function shieldThrowSecondAfter(projectile, homingTarget) {
caster.heal(amount, caster)
caster.heal(amount, caster) // NOTE: duplicated on purpose
}
const shieldThrowFirstAfter = function shieldThrowFirstAfter(projectile, homingTarget) {
projectile.destination = null
projectile.homingTarget = caster
onTheWayBack = true
collided.clear()
projectile.after = shieldThrowSecondAfter
}
const projectile = new Projectile({
after: shieldThrowFirstAfter,
logic: accelerateLogic,
onCollide: shieldThrowCollision,
owner: caster.id,
position: caster.position.clone(),
radius: ability.radius,
speed: ability.speed,
visionRange: ability.radius * 1.5,
})
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius))
caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id)
},
})
static straightShot = new Ability({
id: 'straight_shot',
name: 'Straight Shot',
castTime: 0.25,
cooldown: 1,
damage: 83,
radius: 60,
range: 1200,
visualRadius: 20,
speed: 2000,
effect: function straightShotEffect(caster, cursor) {
const ability = this
const straightShotCollision = function straightShotCollision(projectile, collidingEntity) {
if (projectile.game == null) { return }
if (collidingEntity == null) { return }
if (collidingEntity.id == caster.id) { return }
if (collidingEntity.team == (caster.team ?? 'unknown')) { return }
collidingEntity.damage(ability.damage, caster)
projectile.despawn()
}
const projectile = new Projectile({
onCollide: straightShotCollision,
owner: caster.id,
position: caster.position.clone(),
radius: ability.radius,
speed: ability.speed,
visualRadius: ability.visualRadius,
})
projectile.destination = caster.position.clone().add(cursor.clone().sub(caster.position).normalize().multiplyScalar(ability.range + caster.radius))
caster.game?.spawnProjectile(projectile)
caster.cooldown(ability.id)
},
})
}
+28
View File
@@ -0,0 +1,28 @@
export default class Buff {
id = `ability-${Buff.nextId()}`
static nextId() { return this.#nextUniqueId++ }
static #nextUniqueId = 0
name = 'Buff'
damageMultiplier = null
duration = 0
#effect = null
get effect() { return this.#effect ?? Buff.noEffect }
set effect(value) { this.#effect = value }
static get noEffect() { return function noEffect() {} }
constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value)
}
static exposed = new Buff({
id: 'exposed',
name: 'Exposed',
duration: 4,
onHitMultiplier: 3,
})
}
+860
View File
@@ -0,0 +1,860 @@
import { Vector2 } from 'three'
import Buff from './buff.js'
import Pathfind from './pathfind.js'
import Projectile from './projectile.js'
import SAT from 'sat'
import SATX from './satx.js'
import Team from './team.js'
export default class Entity {
id = `entity-${Entity.nextId()}`
static nextId() { return this.#nextUniqueId++ }
static #nextUniqueId = 0
abilities = {}
buffs = []
casting = null
cooldowns = {}
dead = false
ghosting = false
health = null
height = 40
maxHealth = 1
position = null
radius = 0
rotation = 0
speed = 400
team = Team.neutral
visionRange = 900
visualRadius = null
#collision = true
#ghostable = true
#attacking = false
#bbox = new Float32Array(4)
#colliders = []
#entitiesInVision = []
#projectilesInVision = []
#pathfindingCooldown = 0
#pathfindingObstacleLimit = null
#dest = null
#game = null
#logic = null
#moving = false
#path = []
#noPathfindingUntil = 0
#spawnPosition = new Vector2()
static bbox(x, y, radius) {
return new Float32Array([y + radius, x + radius, y - radius, x - radius])
}
static collider(x, y, radius) {
return new SAT.Circle(new SAT.Vector(x, y), radius)
}
// deliberate code duplication for performance
static tunnelCollider(fromX, fromY, toX, toY, radius) {
if (radius <= 0) {
return SATX.line(fromX, fromY, toX, toY)
}
const sides = new Float32Array(5)
sides[0] = toX - fromX
sides[1] = toY - fromY
sides[4] = Math.hypot(sides[0], sides[1])
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
sides[3] = (sides[0] / sides[4]) * radius
return new SAT.Polygon(new SAT.Vector(fromX - sides[2], fromY - sides[3]), [
new SAT.Vector(),
new SAT.Vector(sides[0], sides[1]),
new SAT.Vector(sides[0] + (2 * sides[2]), sides[1] + (2 * sides[3])),
new SAT.Vector(2 * sides[2], 2 * sides[3]),
])
}
// deliberate code duplication for performance
static tunnelVertices(fromX, fromY, toX, toY, radius) {
const sides = new Float32Array(5)
sides[0] = toX - fromX
sides[1] = toY - fromY
sides[4] = Math.hypot(sides[0], sides[1])
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
sides[3] = (sides[0] / sides[4]) * radius
return [
new Vector2(fromX - sides[2], fromY - sides[3]),
new Vector2(fromX - sides[2] + sides[0], fromY - sides[3] + sides[1]),
new Vector2(fromX + sides[2] + sides[0], fromY + sides[3] + sides[1]),
new Vector2(fromX + sides[2], fromY + sides[3]),
]
}
// deliberate code duplication for performance
static tunnelBbox(fromX, fromY, toX, toY, radius) {
if (radius <= 0) {
return new Float32Array([
Math.max(fromY, toY),
Math.max(fromX, toX),
Math.min(fromY, toY),
Math.min(fromX, toX),
])
}
const sides = new Float32Array(5)
sides[0] = toX - fromX
sides[1] = toY - fromY
sides[4] = Math.hypot(sides[0], sides[1])
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
sides[3] = (sides[0] / sides[4]) * radius
const offsetX = fromX + sides[0]
const x1 = fromX - sides[2]
const x2 = fromX + sides[2]
const x3 = offsetX - sides[2]
const x4 = offsetX + sides[2]
const offsetY = fromY + sides[1]
const y1 = fromY - sides[3]
const y2 = fromY + sides[3]
const y3 = offsetY - sides[3]
const y4 = offsetY + sides[3]
return new Float32Array([
Math.max(y1, y2, y3, y4),
Math.max(x1, x2, x3, x4),
Math.min(y1, y2, y3, y4),
Math.min(x1, x2, x3, x4),
])
}
constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value)
if (this.position == null) {
this.position = this.#spawnPosition.clone()
}
if (this.health == null) {
this.health = this.maxHealth
}
if (this.visualRadius == null) {
this.visualRadius = this.radius
}
this.#calculateCollider()
}
get attacking() { return this.#attacking }
get bbox() { return this.#bbox }
get collision() { return this.#collision }
get destination() { return this.#dest }
get entitiesInVision() { return this.#entitiesInVision }
get game() { return this.#game }
get ghostable() { return this.#ghostable }
get logic() { return this.#logic }
get pathfindingCooldown() { return this.#pathfindingCooldown }
get pathfindingObstacleLimit() { return this.#pathfindingObstacleLimit }
get projectilesInVision() { return this.#projectilesInVision }
get spawnPosition() { return this.#spawnPosition }
get x() { return this.position.x }
get y() { return this.position.y }
set bbox(value) { this.#bbox = value }
set collision(value) { this.#collision = value }
set destination(value) { this.#dest = value }
set game(value) { this.#game = value }
set ghostable(value) { this.#ghostable = value }
set logic(value) { this.#logic = value }
set pathfindingCooldown(value) { this.#pathfindingCooldown = value }
set pathfindingObstacleLimit(value) { this.#pathfindingObstacleLimit = value }
set spawnPosition(value) { this.#spawnPosition = value }
set x(value) { this.position.x = value }
set y(value) { this.position.y = value }
attackAction(cursor) {
if (this.dead) { return }
this.moveAction(cursor, true)
}
// TODO: buffer skill inputs
castAction(slot, cursor, halt = false) {
if (this.dead) { return }
const ability = this.ability(slot)
if (ability == null) { return }
if (this.casting != null) {
const abilityBeingCasted = this.game?.abilities.find((it) => it.id == this.casting.ability)
if (abilityBeingCasted != null && abilityBeingCasted.id == ability.id) {
return false
}
return false
}
if (halt) {
this.#moving = false
}
const targetPosition = (cursor instanceof Vector2) ? cursor : this.game?.entities.find((it) => it.id == cursor)?.position
if (targetPosition instanceof Vector2) {
this.rotation = targetPosition.clone().sub(this.position).angle()
}
const cooldown = this.game?.secToTick(ability.cooldown) ?? 0
const lastCast = this.cooldowns[ability.id]
const timestamp = this.game?.currentTick ?? 0
if (lastCast != null && lastCast + cooldown > timestamp) {
return false
}
if (ability.castTime == null) {
this.#castingVision()
ability.effect(this, cursor)
return true
}
this.casting = { ability: ability.id, cursor, timestamp }
return true
}
haltAction() {
if (this.dead) { return }
this.#moving = false
}
moveAction(cursor, attack = false) {
if (this.dead) { return }
if (this.casting != null && this.game?.abilities.find((it) => it.id == this.casting.ability)?.moveCancelable) {
if (!attack && !(this.casting != null && this.casting.ability == this.abilities[0])) {
this.casting = null
}
}
this.#attacking = attack
this.#moving = true
this.#dest = cursor.clone()
}
stopAction() {
if (this.dead) { return }
this.casting = null
this.#moving = true
this.#attacking = false
}
// --- Actions above --- //
ability(slot) {
if (this.abilities[slot] != null) {
return this.game?.abilities.find((it) => it.id == this.abilities[slot])
}
}
adjustWaypoint(waypoint, direction) {
return SATX.clamp(
waypoint.clone().add(direction.clone().multiplyScalar(this.radius)),
this.game?.width,
this.game?.height,
this.radius,
)
}
applyBuff(id, sourceId = null) {
const index = this.buffs.findIndex((it) => it.id == id)
const source = sourceId ?? this.id
const timestamp = this.game?.currentTick ?? 0
if (index > -1) {
this.buffs[index].timestamp = timestamp
this.buffs[index].source = source
}
else {
this.buffs.push({ id, source, timestamp })
}
}
collidables() {
return this.customBboxCollidables(this.bbox)
}
collider() {
return this.#colliders.at(0)
}
colliders() {
return this.#colliders
}
cooldown(id) {
this.cooldowns[id] = this.game?.currentTick ?? 0
}
closestTargetTo(cursor, range, targetAllies = false) {
const entities = this.game?.entities
if (entities == null) { return }
const targetsInRange = targetAllies
? entities.filter((it) => !it.dead && this.team == it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
: entities.filter((it) => !it.dead && this.team != it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
if (targetsInRange.length < 1) { return }
const absoluteClosestTarget = targetsInRange.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
const entityIdsInDirectVision = this.#entitiesInVision
if (entityIdsInDirectVision.includes(absoluteClosestTarget.id)) {
return absoluteClosestTarget
}
const visibleEntityIds = this.visibleEntities()
const visibleEntitiesInRange = targetsInRange.filter((it) => visibleEntityIds.includes(it.id))
return visibleEntitiesInRange.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
}
customBboxCollidables(bbox) {
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
return entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
}
// TODO: add shielding logic
damage(amount, source = null) {
if (this.dead) { return }
let customMultipliers = 0
if (this.hasBuff(Buff.exposed.id)) {
const buff = this.getBuff(Buff.exposed.id)
if (buff.source == source.id) {
customMultipliers += (buff.onHitMultiplier - 1)
this.removeBuff(Buff.exposed.id)
}
}
const damageMultiplerBuffs = source.buffs.map((it) => it.getBuff).filter((it) => it != null && it.damageMultiplier != null)
const buffPassiveDamageMultiplier = damageMultiplerBuffs.reduce((it) => it.damageMultiplier - 1, 0)
const damageMultipler = 1 + buffPassiveDamageMultiplier + customMultipliers
const damage = amount * damageMultipler
this.health = Math.min(Math.max(0, this.health - damage), this.maxHealth)
}
despawn() {
this.game?.despawn(this)
}
distanceTo(cursor) {
return this.position.distanceTo(cursor)
}
forceCast(abilityId, cursor) {
if (this.dead) { return }
const ability = this.game?.abilities.find((it) => it.id == abilityId)
if (ability == null) { return }
const targetPosition = (cursor instanceof Vector2) ? cursor : this.game?.entities.find((it) => it.id == cursor)?.position
if (targetPosition instanceof Vector2) {
this.rotation = targetPosition.clone().sub(this.position).angle()
}
const timestamp = this.game?.currentTick ?? 0
if (ability.castTime == null) {
this.#castingVision()
ability.effect(this, cursor)
}
else {
this.casting = { ability: ability.id, cursor, timestamp }
}
}
futureCollidables(futurePosition) {
return this.customBboxCollidables(new Float32Array([
futurePosition.y + this.radius,
futurePosition.x + this.radius,
futurePosition.y - this.radius,
futurePosition.x - this.radius,
]))
}
getBuff(id) {
if (this.dead) { return }
const entityBuff = this.buffs.find((it) => it.id == id)
if (entityBuff == null) { return }
const buffDefinition = this.game?.buffs.find((it) => it.id == entityBuff.id)
if (buffDefinition == null) { return }
return { ...buffDefinition, ...entityBuff }
}
hasBuff(id) {
if (this.dead) { return false }
return this.buffs.some((it) => it.id == id) && this.game?.buffs.some((it) => it.id == id)
}
heal(amount, _source) {
if (this.dead) { return }
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
}
fixPosition() {
const fixedPosition = this.fixFuturePosition(this.position)
if (this.position.equals(fixedPosition)) { return }
this.setPosition(fixedPosition)
}
fixFuturePosition(futurePosition) {
const maxX = this.game?.width ?? Infinity
const maxY = this.game?.height ?? Infinity
const radius = this.radius
if (!this.willCollide(futurePosition)) {
return SATX.clamp(futurePosition, maxX, maxY, radius)
}
let direction = new Vector2(0, 5)
let multiplier = 1
const rotationSlices = 16
const origin = new Vector2()
for (let limit = 1; limit <= 10000; limit++) {
const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices
const offset = direction.clone().rotateAround(origin, rads).multiplyScalar(multiplier)
const position = SATX.clamp(futurePosition.clone().add(offset), maxX, maxY, radius)
if (!this.willCollide(position)) {
return position
}
if (limit % rotationSlices == 0) {
multiplier++
}
}
console.error(`Can't fix position ([${futurePosition.x}, ${futurePosition.y}]) of entity ID: ${this.id}`)
}
isColliding() {
const collidables = this.collidables()
if (collidables.length < 1) {
return false
}
const colliders = collidables.map((it) => it.colliders()).flat()
const collider = this.collider()
return colliders.some((it) => SATX.collideObject(collider, it))
}
isInLineOfSight(destination, position = this.position) {
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return true }
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
return !colliders.some((it) => SATX.collideObject(collider, it))
}
isInLineOfVision(destination) {
const bbox = Entity.tunnelBbox(this.position.x, this.position.y, destination.x, destination.y, 0)
const terrains = this.game?.terrains ?? []
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return true }
const posCollider = Entity.collider(this.position.x, this.position.y, 0)
const posBbox = Entity.bbox(this.position.x, this.position.y, 0)
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c))))
const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0)
return !colliders.some((it) => SATX.collideObject(collider, it))
}
obstaclesInStraightPath(destination, position = this.position) {
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return [] }
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
return bboxCheckedObstacles.filter((obstacle) => obstacle.colliders().some((it) => SATX.collideObject(collider, it)))
}
removeBuff(id) {
if (this.dead) { return }
this.buffs = this.buffs.filter((it) => it.id != id)
}
respawn() {
this.setPosition(this.#spawnPosition)
this.health = this.maxHealth
this.dead = false
}
revive(startingHealth = null) {
this.dead = false
const health = (startingHealth ?? this.maxHealth)
this.health = Math.max(0, Math.min(health, this.maxHealth))
this.#calculateCollider()
this.#calculateVision()
}
setPosition(vector) {
this.position.copy(vector)
this.#calculateCollider()
}
teleport(cursor) {
this.setPosition(this.fixFuturePosition(cursor))
}
unadjustedWaypoints() {
const numberOfWaypoints = 8
const margin = 1
const enclosingRegularPolygonRadius = SATX.enclosingRegularPolygonRadius(numberOfWaypoints)
const radius = this.radius * enclosingRegularPolygonRadius + margin
const baseWaypoint = new Vector2(radius, 0)
const waypoints = []
const origin = new Vector2
const unitOfRotation = (Math.PI * 2 / numberOfWaypoints)
for (let i = 0; i < numberOfWaypoints; i++) {
waypoints.push(baseWaypoint.clone().rotateAround(origin, unitOfRotation * i))
}
return waypoints.map((w) => [
w.clone().add(this.position),
w.clone().normalize().multiplyScalar(enclosingRegularPolygonRadius),
])
}
update() {
this.#calculateVision()
this.#checkHealth()
if (!this.dead) {
this.#cast()
this.#move()
this.#tickBuffs()
this.fixPosition()
}
if (this.#logic != null) {
this.#logic()
}
}
visibleEntities() {
return this.game?.visibleEntities(this.team)
}
willCollide(futurePosition) {
const collidables = this.futureCollidables(futurePosition)
if (collidables.length < 1) {
return false
}
const colliders = collidables.map((it) => it.colliders()).flat()
const collider = Entity.collider(futurePosition.x, futurePosition.y, this.radius)
return colliders.some((it) => SATX.collideObject(collider, it))
}
#calculateBbox() {
this.bbox[0] = this.position.y + this.radius
this.bbox[1] = this.position.x + this.radius
this.bbox[2] = this.position.y - this.radius
this.bbox[3] = this.position.x - this.radius
}
#calculateCollider() {
this.#calculateBbox()
this.#colliders = [Entity.collider(this.position.x, this.position.y, this.radius)]
}
#calculateVision() {
if (this.dead) {
this.#entitiesInVision = [this.id]
this.#projectilesInVision = []
return
}
const entities = this.game?.entities ?? []
const projectiles = this.game?.projectiles ?? []
const entitiesInVisionRange = entities.filter((it) => it.id != this.id && it.distanceTo(this.position) <= this.visionRange + it.radius)
const entitiesInLineOfSight = entitiesInVisionRange.filter((it) => this.isInLineOfVision(it.position))
const projectilesInVisionRange = projectiles.filter((it) => this.position.distanceTo(it.position) <= this.visionRange + it.radius)
const projectilesInLineOfSight = projectilesInVisionRange.filter((it) => it.visibleThroughTerrain || this.isInLineOfVision(it.position))
this.#entitiesInVision = entitiesInLineOfSight.concat([this]).map((it) => it.id)
this.#projectilesInVision = projectilesInLineOfSight.map((it) => it.id)
}
#cast() {
if (this.casting == null) {
return false
}
const ability = this.game?.abilities.find((it) => it.id == this.casting.ability)
if (ability == null) {
return
}
const castTime = this.game?.secToTick(ability.castTime) ?? 0
const castStart = this.casting.timestamp
const timestamp = this.game?.currentTick ?? 0
if (castStart + castTime > timestamp) {
return false
}
ability.effect(this, this.casting.cursor)
if (this.casting.ability == ability.id) {
this.casting = null
}
this.#castingVision()
return true
}
#castingVision() {
const enemyTeam = this.team == Team.blue ? Team.red : (this.team == Team.red ? Team.blue : null)
if (enemyTeam == null) {
return // only blue/red teams have casting vision
}
const enemiesNearby = (this.game?.entities ?? []).some((it) => !it.dead && it.team == enemyTeam && it.distanceTo(this.position) <= (it.visionRange + this.radius))
if (enemiesNearby) {
const radius = 300
const duration = this.game?.secToTick(2) ?? 0
if (duration <= 0) { return }
const currentTick = this.game?.currentTick ?? 0
const despawnAfter = currentTick + duration
const castingVisionLogic = function castingVisionLogic(projectile) {
const currentTick = projectile.game?.currentTick ?? 0
if (currentTick > despawnAfter) {
projectile.despawn()
}
}
const projectile = new Projectile({
logic: castingVisionLogic,
owner: this.id,
position: this.position.clone(),
visionRange: radius,
})
this.game?.spawnProjectile(projectile)
}
}
#checkHealth() {
if (!this.dead && this.health <= 0) {
this.dead = true
this.buffs = []
}
else if (this.dead && this.health > 0) {
this.health = 0
}
}
#move(distanceTraveled = 0) {
if (this.casting != null) { return false }
const currentTick = this.game?.currentTick ?? 0
if (this.#attacking) {
const cursor = this.#dest ?? this.position
const basicAttack = this.ability('a')
if (basicAttack != null) {
const target = this.closestTargetTo(cursor, 500)
if (target != null && this.distanceTo(target.position) < basicAttack.range + this.radius + target.radius) {
const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0
const lastCast = this.cooldowns[basicAttack.id]
if (lastCast != null && lastCast + cooldown > currentTick) { return false }
this.castAction('a', target.id, false)
return true
}
}
}
if (!this.#moving || this.#dest == null) { return false }
const fixedDest = this.fixFuturePosition(this.#dest)
const pathfinding = this.#noPathfindingUntil <= currentTick
const obstacles = new Map()
let pathGotObstructed = false
if (pathfinding && this.#path.length > 0) {
const sectionDest = this.#path.at(0)
const sectionObstacles = this.obstaclesInStraightPath(sectionDest)
if (sectionObstacles.length > 0) {
pathGotObstructed = true
for (const obstacle of sectionObstacles) {
if (!obstacles.has(obstacle.id)) {
obstacles.set(obstacle.id, obstacle)
}
}
}
}
if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) {
const lineOfSight = this.isInLineOfSight(fixedDest)
if (lineOfSight) {
this.#path = [fixedDest]
}
}
if (pathfinding && (pathGotObstructed || this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
const start = SATX.vectorToFloat32Array(this.position)
const goal = SATX.vectorToFloat32Array(fixedDest)
const obstacleWaypoints = new Map()
const obstacleColliders = new Map()
const obstacleBboxes = new Map()
const initialObstaclesMargin = this.radius + 20
const initialObstacles = this.customBboxCollidables(new Float32Array([
this.position.y + initialObstaclesMargin,
this.position.x + initialObstaclesMargin,
this.position.y - initialObstaclesMargin,
this.position.x - initialObstaclesMargin,
]))
for (const obstacle of initialObstacles) {
if (!obstacles.has(obstacle.id)) {
obstacles.set(obstacle.id, obstacle)
}
}
for (let failsafe = 0; failsafe <= (this.pathfindingObstacleLimit ?? 1000); failsafe++) {
if (failsafe >= 10) { console.error('Failsafe is reached!!!'); process.exit(0) }
const obstaclesArray = Array.from(obstacles.values())
for (const obstacle of obstaclesArray) {
if (!obstacleWaypoints.has(obstacle.id)) {
const waypoint = obstacle.unadjustedWaypoints().map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))
const bbox = obstacle.bbox
const colliders = obstacle.colliders()
obstacleWaypoints.set(obstacle.id, waypoint)
obstacleColliders.set(obstacle.id, colliders)
obstacleBboxes.set(obstacle.id, bbox)
}
}
const waypoints = [
start,
goal,
...Array.from(obstacleWaypoints.values()).flat()
]
const bboxesSize = obstacleBboxes.size * 5
const bboxes = new Float32Array(bboxesSize)
let i = 0
for (const obstacle of obstacleBboxes.values()) {
bboxes[i] = obstacle[0]
bboxes[i + 1] = obstacle[1]
bboxes[i + 2] = obstacle[2]
bboxes[i + 3] = obstacle[3]
bboxes[i + 4] = Math.floor(i / 5)
i += 5
}
const colliders = Array.from(obstacleColliders.values())
const graph = Pathfind.buildGraph(waypoints, bboxes, colliders, this.radius)
const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
if (path.length == 0) {
// WARNING: This unsets the destination because if an unreachable spot is clicked,
// pathfinding cycles all obstacles forever. A possible alternative could
// be setting a pathfinding timeout, but then moveAction must reset that!
this.#dest = null
break
}
let obstacleInPath = false
let lastSection = this.position
for (const section of path) {
const sectionObstacles = this.obstaclesInStraightPath(section, lastSection)
if (sectionObstacles.length > 0) {
obstacleInPath = true
for (const obstacle of sectionObstacles) {
if (!obstacles.has(obstacle.id)) {
obstacles.set(obstacle.id, obstacle)
}
}
}
lastSection = section
}
this.#path = path
if (!obstacleInPath) {
break
}
}
}
if (pathfinding && this.pathfindingCooldown > 0) {
this.#noPathfindingUntil = currentTick + (this.game?.secToTick(this.pathfindingCooldown) ?? 0)
}
if (this.#path.length > 0) {
const speed = (this.speed / (this.game?.tickRate ?? 1)) - distanceTraveled
const destination = this.#path.at(0)
const difference = destination.clone().sub(this.position)
const distance = difference.length()
const direction = difference.clone().normalize()
const stepTaken = this.position.clone().add(direction.multiplyScalar(speed))
const position = distance <= speed ? destination : stepTaken
const rotation = direction.angle()
this.rotation = rotation
if (!this.willCollide(position)) {
this.setPosition(position)
}
if (this.position.equals(destination)) {
this.#path = this.#path.slice(1)
if (this.#path.length > 0) {
this.#move(distance)
}
else {
this.#dest = null
this.#moving = false
}
}
}
}
#tickBuff(index) {
if (this.buffs[index] == null) { return }
const buff = this.getBuff(this.buffs[index].id)
const duration = this.game?.secToTick(buff.duration) ?? 0
const currentTick = this.game?.currentTick ?? 0
if (buff.timestamp + duration < currentTick) {
this.removeBuff(buff.id)
}
}
#tickBuffs() {
this.buffs.forEach((_v, i) => this.#tickBuff(i))
}
}
+208
View File
@@ -0,0 +1,208 @@
import { EventEmitter } from 'node:events'
import { Vector2 } from 'three'
import Ability from './ability.js'
import Buff from './buff.js'
import Entity from './entity.js'
import Projectile from './projectile.js'
import Terrain from './terrain.js'
export default class Game {
id = crypto.randomUUID()
abilities = Object.values({...Ability})
buffs = Object.values({...Buff})
currentTick = 0
entities = []
height = 0
projectiles = []
terrains = []
tickRate = 30
width = 0
#gameLoopIntervalId = null
#logic = null
#nextTickAt = 0
#startTimestamp = 0
#subscriptions = new Map()
#tickBudget = 1000 / this.tickRate
get logic() { return this.#logic }
get tickBudget() { return this.#tickBudget }
get subscriptions() { return this.#subscriptions }
set logic(value) { this.#logic = value }
action(id, options) {
const entity = this.entities.find((it) => it.id == id)
if (entity == null) {
console.error({ error: 'Invalid ID' })
return
}
if (options.action == 'attack') { entity.attackAction(new Vector2(options.x, options.y)) }
if (options.action == 'cast') { entity.castAction(options.slot, new Vector2(options.x, options.y)) }
if (options.action == 'halt') { entity.haltAction() }
if (options.action == 'stop') { entity.stopAction() }
if (options.action == 'move') { entity.moveAction(new Vector2(options.x, options.y)) }
}
addTerrain(terrain) {
this.terrains.push(terrain)
}
despawn(object) {
if (object instanceof Entity) { this.despawnEntity(object) }
else if (object instanceof Terrain) { this.removeTerrain(object) }
else if (object instanceof Projectile) { this.despawnProjectile(object) }
else { console.error({ error: { reason: 'Can\'t despawn object', object } }) }
}
despawnEntity(entity) {
this.entities = this.entities.filter((e) => e.id != entity.id)
entity.game = null
}
despawnProjectile(projectile) {
this.projectiles = this.projectiles.filter((p) => p.id != projectile.id)
projectile.game = null
}
joinReport() {
return {
id: this.id,
height: this.height,
width: this.width,
currentTick: this.currentTick,
abilities: this.abilities,
buffs: this.buffs,
terrains: this.terrains,
tickRate: this.tickRate,
}
}
removeTerrain(terrain) {
this.terrains = this.terrains.filter((t) => t.id != terrain.id)
}
secToTick(sec) {
return Math.floor(this.tickRate * sec)
}
spawn(object) {
if (object instanceof Entity) { this.spawnEntity(object) }
else if (object instanceof Terrain) { this.addTerrain(object) }
else if (object instanceof Projectile) { this.spawnProjectile(object) }
else { console.error({ error: { reason: 'Can\'t spawn object', object } }) }
}
spawnEntity(entity) {
this.entities.push(entity)
entity.game = this
}
spawnProjectile(projectile) {
this.projectiles.push(projectile)
projectile.game = this
}
start() {
if (this.#gameLoopIntervalId != null) { return }
this.#startTimestamp = performance.now() + (this.currentTick * this.tickBudget)
console.info(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`)
this.#gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5)
}
stop() {
if (this.#gameLoopIntervalId == null) { return }
clearInterval(this.#gameLoopIntervalId)
this.#gameLoopIntervalId = null
console.info(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`)
}
subscription(websocket, id) {
return function builtSubscription() {
const game = this
const entity = game.entities.find((it) => it.id == id)
if (entity == null) { return }
const team = entity.team
const state = game.visionByTeam(team)
state.currentTick = game.currentTick
websocket.send(JSON.stringify(state))
}
}
update() {
for (const subscription of this.#subscriptions.values()) {
subscription()
}
const callUpdate = function callUpdate(object) { object.update() }
this.entities.forEach(callUpdate)
this.projectiles.forEach(callUpdate)
if (this.#logic != null) {
this.#logic()
}
this.currentTick++
}
visibleEntities(team) {
const visionSources = this.visionSources(team)
return Array.from(new Set(visionSources.map((it) => it.entitiesInVision).flat()))
}
visibleProjectiles(team) {
const visionSources = this.visionSources(team)
return Array.from(new Set(visionSources.map((it) => it.projectilesInVision).flat()))
}
visionSources(team) {
const entityVisionSources = this.entities.filter((it) => it.team == team)
const projectileVisionSources = this.projectiles.filter((it) => it.visionRange > 0 && (it.team == null || it.team == team))
return entityVisionSources.concat(projectileVisionSources)
}
visionByTeam(team) {
const visionSources = this.visionSources(team)
const visibleEntities = new Set(visionSources.map((it) => it.entitiesInVision).flat())
const visibleProjectiles = new Set(visionSources.map((it) => it.projectilesInVision).flat())
return {
entities: this.entities.filter((it) => visibleEntities.has(it.id)),
projectiles: this.projectiles.filter((it) => visibleProjectiles.has(it.id)),
}
}
#gameLoop() {
if (this.#nextTickAt != null) {
const tickBudget = this.#tickBudget
const nextTickAt = this.#nextTickAt
this.#nextTickAt = null
let start = 0
while (start < nextTickAt) { start = performance.now() }
const before = performance.now()
this.update()
const after = performance.now()
const taken = (after - before)
const useAbsoluteBehind = true
const absoluteBehind = Math.max(0, (start - this.#startTimestamp) - ((this.currentTick) * tickBudget))
this.#nextTickAt = start + tickBudget - (useAbsoluteBehind ? absoluteBehind : prevBehind)
if (after - before > tickBudget) {
const behindNotice = absoluteBehind > 0.1 ? `(Was already behind ${absoluteBehind.toFixed(1)} ms)` : ``
console.warn(`Can't keep up! A tick took ${taken.toFixed(1)} ms / ${tickBudget.toFixed(1)} ms. ${behindNotice}`)
}
}
}
#gameLoopCall() {
this.#gameLoop()
}
}
+56
View File
@@ -0,0 +1,56 @@
import { Dungeon } from './level.js'
import { WebSocketExpress } from 'websocket-express'
import express from 'express'
import Game from './game.js'
import os from 'node:os'
try {
// WARNING: process.nice can undermine dependencies?
os.setPriority(process.pid, os.constants.priority.PRIORITY_HIGHEST)
}
catch (error) {
console.warn('Could not adjust process priority on startup.')
}
const app = new WebSocketExpress()
const port = 1280
const game = new Game()
app.use(express.urlencoded({ extended: true }))
app.use('/three/', express.static('node_modules/three'))
app.use('/@tweenjs/', express.static('node_modules/@tweenjs'))
app.use('/stats.js/', express.static('node_modules/stats.js'))
app.use('/', express.static('public'))
app.use('/tools/', express.static('tools'))
app.ws('/ws', async (req, res) => {
const websocket = await res.accept()
websocket.on('message', (rawData) => {
const message = JSON.parse(rawData)
console.log(message)
if (message.action == 'join') {
const id = message.id
const connectionId = crypto.randomUUID()
websocket.send(JSON.stringify(game.joinReport()))
const subscription = game.subscription(websocket, id).bind(game)
game.subscriptions.set(connectionId, subscription)
websocket.on('close', () => {
console.log({ event: 'disconnected', id })
game.subscriptions.delete(connectionId)
})
return
}
game.action(message.id, message)
})
})
app.listen(port, () => {
console.info(`Server started! Visit http://localhost:${port}`)
Dungeon.scenario(game)
})
+2282
View File
File diff suppressed because it is too large Load Diff
+222
View File
@@ -0,0 +1,222 @@
import Entity from './entity.js'
import PriorityQueue from './priority-queue.js'
import SATX from './satx.js'
export default class Pathfind {
static precision = 0.01
static multiplier = 1000000 // (1 / this.precision) * 10^expected_digit_count / 10
static key2(a, b) {
return `${a},${b}`
}
// Fowler-Noll-Vo hash prime and offset basis for small keyspaces
static floatKey4(a, b, c, d) {
const prime = 16777619
let result = 2166136261
result ^= Math.floor(a * Pathfind.multiplier)
result *= prime
result ^= Math.floor(b * Pathfind.multiplier)
result *= prime
result ^= Math.floor(c * Pathfind.multiplier)
result *= prime
result ^= Math.floor(d * Pathfind.multiplier)
result *= prime
return result
}
static uniqueWaypoints(waypoints) {
const included = new Set()
const uniqueWaypoints = []
for (const waypoint of waypoints) {
const key = Pathfind.key2(waypoint[0], waypoint[1])
if (!included.has(key)) {
included.add(key)
uniqueWaypoints.push(waypoint)
}
}
return uniqueWaypoints
}
static shortestPath(graph, start, goal) {
const queue = new PriorityQueue((a, b) => a[1] < b[1])
const visited = new Map()
queue.push([[start], 0])
while (!queue.isEmpty()) {
const [path, cost] = queue.pop()
const waypoint = path.at(-1)
if (Math.abs(waypoint[0] - goal[0]) < Pathfind.precision && Math.abs(waypoint[1] - goal[1]) < Pathfind.precision) {
path.shift()
return path
}
const waypointKey = Pathfind.key2(waypoint[0], waypoint[1])
if (!visited.has(waypointKey) || visited.get(waypointKey) > cost) {
visited.set(waypointKey, cost)
for (let i = 0; i < graph.length; i += 5) {
if (Math.abs(waypoint[0] - graph[i]) > Pathfind.precision || Math.abs(waypoint[1] - graph[i + 1]) > Pathfind.precision) {
continue // waypoint and graph.from aren't the same (so graph.to isn't a neighbor)
}
const nextKey = Pathfind.key2(graph[i + 2], graph[i + 3])
if (!visited.has(nextKey) || visited.get(nextKey) > cost + graph[i + 4]) {
const next = new Float32Array(2)
next[0] = graph[i + 2]
next[1] = graph[i + 3]
queue.push([[...path, next], cost + graph[i + 4]])
}
}
}
}
return []
}
static buildGraph(waypoints, bboxes, obstacles, radius) {
const filteredWaypoints = []
const checked = new Set()
if (radius > 0) {
for (const waypoint of waypoints) {
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius)
const bboxCheckedObstacles = []
for (let i = 0; i < bboxes.length; i += 5) {
if (bbox[0] <= bboxes[i + 2]) { continue }
if (bbox[1] <= bboxes[i + 3]) { continue }
if (bbox[2] >= bboxes[i]) { continue }
if (bbox[3] >= bboxes[i + 1]) { continue }
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
}
if (bboxCheckedObstacles.length > 0) {
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
const colliding = bboxCheckedObstacles.flat().some((it) => SATX.collideObject(collider, it))
if (colliding) {
continue
}
}
filteredWaypoints.push(waypoint)
}
}
const mergedWaypoints = new Float32Array(filteredWaypoints.length * 2)
let mergedWaypointsIndex = 0
for (const waypoint of filteredWaypoints) {
mergedWaypoints[mergedWaypointsIndex] = waypoint[0]
mergedWaypoints[mergedWaypointsIndex + 1] = waypoint[1]
mergedWaypointsIndex += 2
}
const nodes = []
for (let i = 0; i < mergedWaypoints.length; i += 2) {
for (let j = 0; j < mergedWaypoints.length; j += 2) {
if (i == j) {
continue
}
if (Math.abs(mergedWaypoints[i] - mergedWaypoints[j]) < Pathfind.precision && Math.abs(mergedWaypoints[i + 1] - mergedWaypoints[j + 1]) < Pathfind.precision) {
continue
}
const key = Pathfind.floatKey4(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1])
if (checked.has(key)) {
continue
}
checked.add(key)
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
const bbox = Entity.tunnelBbox(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
const bboxCheckedObstacles = []
for (let i = 0; i < bboxes.length; i += 5) {
if (bbox[0] <= bboxes[i + 2]) { continue }
if (bbox[1] <= bboxes[i + 3]) { continue }
if (bbox[2] >= bboxes[i]) { continue }
if (bbox[3] >= bboxes[i + 1]) { continue }
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
}
if (bboxCheckedObstacles.length > 0) {
const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
const colliding = bboxCheckedObstacles.some((it) => it.some((c) => SATX.collideObject(tunnel, c)))
if (colliding) {
continue
}
}
const node = new Float32Array(5)
node[0] = mergedWaypoints[i]
node[1] = mergedWaypoints[i + 1]
node[2] = mergedWaypoints[j]
node[3] = mergedWaypoints[j + 1]
node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1])
nodes.push(node)
const reverseNode = new Float32Array(5)
reverseNode[0] = mergedWaypoints[j]
reverseNode[1] = mergedWaypoints[j + 1]
reverseNode[2] = mergedWaypoints[i]
reverseNode[3] = mergedWaypoints[i + 1]
reverseNode[4] = node[4] // distance is the same, copying is less expensive
nodes.push(reverseNode)
}
}
const graph = new Float32Array(nodes.length * 5)
let graphIndex = 0
for (const node of nodes) {
graph[graphIndex] = node[0]
graph[graphIndex + 1] = node[1]
graph[graphIndex + 2] = node[2]
graph[graphIndex + 3] = node[3]
graph[graphIndex + 4] = node[4]
graphIndex += 5
}
// const niceGraph = []
// for (let i = 0; i < graph.length / 5; i += 5) {
// niceGraph.push({
// from: [graph[i], graph[i + 1]],
// to: [graph[i + 2], graph[i + 3]],
// distance: graph[i + 4],
// })
// }
// console.log(niceGraph)
return graph
}
static formatFloat32Array(array, columns = 2, text = false) {
const formatted = []
let columnWidth = 0
for (let i = 0; i < array.length; i += columns) {
const row = []
for (let j = i; j < i + columns; j++) {
if (text) {
row.push(`${array[j]}`)
if (`${array[j]}`.length > columnWidth) {
columnWidth = `${array[j]}`.length
}
}
else {
row.push(array[j])
}
}
formatted.push(row)
}
if (text) {
return formatted.map((row) => row.map((v) => v.padEnd(columnWidth, ' ')).join(' | ')).join('\n')
}
return formatted
}
}
+78
View File
@@ -0,0 +1,78 @@
const top = 0;
const parent = i => ((i + 1) >>> 1) - 1;
const left = i => (i << 1) + 1;
const right = i => (i + 1) << 1;
export default class PriorityQueue {
#heap
#comparator
constructor(comparator = (a, b) => a > b) {
this.#heap = []
this.#comparator = comparator
}
get length() { return this.#heap.length }
isEmpty() {
return this.length < 1
}
peek() {
return this.#heap[top]
}
push(...values) {
values.forEach(value => {
this.#heap.push(value)
this.#siftUp();
});
return this.length;
}
pop() {
const poppedValue = this.peek()
const bottom = this.length - 1
if (bottom > top) {
this.#swap(top, bottom)
}
this.#heap.pop()
this.#siftDown()
return poppedValue
}
replace(value) {
const replacedValue = this.peek()
this.#heap[top] = value
this.#siftDown()
return replacedValue
}
#greater(i, j) {
return this.#comparator(this.#heap[i], this.#heap[j])
}
#swap(i, j) {
[this.#heap[i], this.#heap[j]] = [this.#heap[j], this.#heap[i]]
}
#siftUp() {
let node = this.length - 1
while (node > top && this.#greater(node, parent(node))) {
this.#swap(node, parent(node))
node = parent(node)
}
}
#siftDown() {
let node = top;
while (
(left(node) < this.length && this.#greater(left(node), node)) ||
(right(node) < this.length && this.#greater(right(node), node))
) {
let maxChild = (right(node) < this.length && this.#greater(right(node), left(node))) ? right(node) : left(node)
this.#swap(node, maxChild)
node = maxChild
}
}
}
+169
View File
@@ -0,0 +1,169 @@
import { Vector2 } from 'three'
import Entity from './entity.js'
import SAT from 'sat'
import SATX from './satx.js'
export default class Projectile {
id = `projectile-${Projectile.nextId()}`
static nextId() { return this.#nextUniqueId++ }
static #nextUniqueId = 0
height = 50
owner = null
position = new Vector2()
radius = 0
speed = 1000
team = null
visibleThroughTerrain = true
visionRange = 0
visualRadius = null
#after = null
#bbox = new Float32Array(4)
#dest = null
#entitiesInVision = []
#game = null
#homingTarget = null
#logic = null
#onCollide = null
#projectilesInVision = []
get after() { return this.#after }
get bbox() { return this.#bbox }
get entitiesInVision() { return this.#entitiesInVision }
get game() { return this.#game }
get homingTarget() { return this.#homingTarget }
get logic() { return this.#logic }
get onCollide() { return this.#onCollide }
get projectilesInVision() { return this.#projectilesInVision }
set after(value) { this.#after = value }
set bbox(value) { this.#bbox = value }
set destination(value) { this.#dest = value }
set game(value) { this.#game = value }
set homingTarget(value) { this.#homingTarget = value }
set logic(value) { this.#logic = value }
set onCollide(value) { this.#onCollide = value }
get destination() {
return this.#dest ?? this.#homingTarget?.position
}
constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value)
if (this.visualRadius == null) {
this.visualRadius = this.radius
}
}
collider() {
return new SAT.Circle(new SAT.Vector(this.position.x, this.position.y), this.radius)
}
despawn() {
this.game?.despawn(this)
}
isInLineOfVision(destination) {
const bbox = Entity.tunnelBbox(this.position.x, this.position.y, destination.x, destination.y, 0)
const terrains = this.game?.terrains ?? []
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length < 1) { return true }
const posCollider = Entity.collider(this.position.x, this.position.y, 0)
const posBbox = Entity.bbox(this.position.x, this.position.y, 0)
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c))))
const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0)
return !colliders.some((it) => SATX.collideObject(collider, it))
}
setPosition(vector) {
this.position.copy(vector)
this.#calculateBbox()
}
update() {
this.#calculateVision()
this.#move()
this.#checkStationaryCollisions()
this.#checkIfArrived()
if (this.#logic != null) {
this.#logic(this)
}
}
#calculateBbox() {
this.bbox[0] = this.position.y + this.radius
this.bbox[1] = this.position.x + this.radius
this.bbox[2] = this.position.y - this.radius
this.bbox[3] = this.position.x - this.radius
}
#calculateVision() {
const entities = this.game?.entities ?? []
const projectiles = this.game?.projectiles ?? []
const entitiesInVisionRange = entities.filter((it) => it.id != this.id && it.distanceTo(this.position) <= this.visionRange + it.radius)
const entitiesInLineOfSight = entitiesInVisionRange.filter((it) => this.isInLineOfVision(it.position))
const projectilesInVisionRange = projectiles.filter((it) => this.position.distanceTo(it.position) <= this.visionRange + it.radius)
const projectilesInLineOfSight = projectilesInVisionRange.filter((it) => it.visibleThroughTerrain || this.isInLineOfVision(it.position))
this.#entitiesInVision = entitiesInLineOfSight.concat([this]).map((it) => it.id)
this.#projectilesInVision = projectilesInLineOfSight.map((it) => it.id)
}
#checkIfArrived() {
if (this.destination == null) { return }
if (!this.position.equals(this.destination)) { return }
if (this.#after != null) {
this.#after(this, this.#homingTarget)
}
if (this.destination == null) { return }
if (!this.position.equals(this.destination)) { return }
this.despawn()
}
#checkStationaryCollisions() {
if (this.#onCollide == null) { return }
const bbox = this.bbox
const entitiesAndTerrains = this.game?.entities ?? []
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length > 0) {
const collider = this.collider()
const colliding = bboxCheckedObstacles.filter((it) => it.colliders().some((c) => SATX.collideObject(collider, c)))
colliding.forEach((it) => this.#onCollide(this, it))
}
}
#move() {
if (this.destination == null) { return }
const speed = (this.speed / (this.game?.tickRate ?? 1))
const prevPos = this.position.clone()
if (this.position.distanceTo(this.destination) < speed) {
this.setPosition(this.destination)
}
else {
const step = this.destination.clone().sub(this.position).normalize().multiplyScalar(speed)
this.position.add(step)
}
if (this.#onCollide != null) {
const bbox = Entity.tunnelBbox(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius)
const entitiesAndTerrains = this.game?.entities ?? []
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && SATX.bboxCheck(bbox, it.bbox))
if (bboxCheckedObstacles.length > 0) {
const collider = Entity.tunnelCollider(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius)
const colliding = bboxCheckedObstacles.filter((it) => it.colliders().some((c) => SATX.collideObject(collider, c)))
colliding.sort((a, b) => a.distanceTo(prevPos) > b.distanceTo(prevPos)).forEach((it) => this.#onCollide(this, it))
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
import { Vector2 } from 'three'
import SAT from 'sat'
export default class SATX {
static bboxCheck(bbox1, bbox2) {
if (bbox1[0] <= bbox2[2]) { return false }
if (bbox1[1] <= bbox2[3]) { return false }
if (bbox1[2] >= bbox2[0]) { return false }
if (bbox1[3] >= bbox2[1]) { return false }
return true
}
static clamp(vectorOrObject, maxX = Infinity, maxY = Infinity, radius = 0) {
let modified = null
if (vectorOrObject instanceof Vector2) {
modified = vectorOrObject.clone()
}
else if (vectorOrObject instanceof SAT.Vector) {
modified = new SAT.Vector(vectorOrObject.x, vectorOrObject.y)
}
else {
modified = { x: vectorOrObject.x, y: vectorOrObject.y }
}
modified.x = Math.min(Math.max(radius, vectorOrObject.x), (maxX ?? Infinity) - radius)
modified.y = Math.min(Math.max(radius, vectorOrObject.y), (maxY ?? Infinity) - radius)
return modified
}
static collideObject(collider1, collider2, result = null) {
if (collider1 instanceof SAT.Circle && collider2 instanceof SAT.Circle) {
return SAT.testCircleCircle(collider1, collider2, result)
}
if (collider1 instanceof SAT.Circle && collider2 instanceof SAT.Polygon) {
return SAT.testCirclePolygon(collider1, collider2, result)
}
if (collider1 instanceof SAT.Polygon && collider2 instanceof SAT.Circle) {
return SAT.testPolygonCircle(collider1, collider2, result)
}
if (collider1 instanceof SAT.Polygon && collider2 instanceof SAT.Polygon) {
return SAT.testPolygonPolygon(collider1, collider2, result)
}
return false
}
static enclosingRegularPolygonRadius(numberOfVertices) {
return 1 / Math.cos(Math.PI / numberOfVertices)
}
static line(fromX, fromY, toX, toY) {
return new SAT.Polygon(new SAT.Vector(fromX, fromY), [new SAT.Vector(), new SAT.Vector(toX - fromX, toY - fromY)])
}
static satPolygonToVectors(polygon) {
const position = new Vector2(polygon.pos.x, polygon.pos.y)
return polygon.points.map((p) => new Vector2(p.x, p.y).add(position))
}
static vectorToFloat32Array(vector) {
const array = new Float32Array(2)
array[0] = vector.x
array[1] = vector.y
return array
}
static float32ArrayToVector(array) {
return new Vector2(array[0], array[1])
}
static float32ArrayWithIndexToVector(array, index) {
return new Vector2(array[index], array[index + 1])
}
}
+5
View File
@@ -0,0 +1,5 @@
export default class Team {
static neutral = 'neutral'
static blue = 'blue'
static red = 'red'
}
+116
View File
@@ -0,0 +1,116 @@
import { Vector2 } from 'three'
import Ability from './ability.js'
import Team from './team.js'
export default class Template {
static basilisk(overrides) {
return {
abilities: {},
height: 100,
logic: this.#basiliskLogic,
radius: 180,
speed: 230,
visualRadius: 170,
maxHealth: 3000,
...overrides,
}
}
static minion(team, options = {}) {
return {
abilities: { a: options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id },
height: options.ranged ? 40 : 38,
logic: this.#minionLogic(options.route, (team != Team.blue)),
maxHealth: options.ranged ? 300 : 450,
pathfindingCooldown: 0.2,
pathfindingObstacleLimit: 0,
position: options.route?.at(0) ?? options.position ?? new Vector2(0, 0),
radius: 48,
speed: 325,
team,
visionRange: 1200,
visualRadius: options.ranged ? 36 : 38,
}
}
static player(overrides) {
return {
abilities: {
a: Ability.rangedAttack.id,
q: Ability.straightShot.id,
w: Ability.expose.id,
e: Ability.control.id,
r: Ability.shieldThrow.id,
d: Ability.circleOfResurrection.id,
f: Ability.blink.id,
},
height: 80,
logic: this.#playerLogic,
maxHealth: 600,
pathfindingObstacleLimit: 3,
radius: 65,
spawnPosition: new Vector2(500, 150),
visionRange: 1350,
visualRadius: 40,
...overrides,
}
}
static #basiliskLogic() {
const entity = this
return
}
static #minionLogic(route = [], odd = false) {
const checkpointSize = 300
const recalculateDestRadius = 50
const aggroRadius = 500
const memory = {}
return function builtMinionLogic() {
const entity = this
if (entity.dead) { entity.despawn() }
const currentTick = entity.game?.currentTick ?? 0
const minionResponseTime = Math.floor(0.1 * (entity.game?.tickRate ?? 1))
if (!(currentTick % minionResponseTime == 0 && Math.floor(currentTick / minionResponseTime) % 2 == (odd ? 1 : 0))) {
return
}
const target = entity.closestTargetTo(entity.position, aggroRadius)
if (target != null) {
entity.ghosting = false
entity.attackAction(target.position)
}
if ((route.length > 0 || entity.attacking) && target == null) {
const routeIndex = memory.routeCheckpoint ?? 0
const goal = route[routeIndex].clone()
if (goal instanceof Vector2) {
if (entity.distanceTo(goal) < checkpointSize) {
if (routeIndex + 1 < route.length) {
memory.routeCheckpoint = routeIndex + 1
}
}
if ((entity.destination?.distanceTo(entity.position) ?? 0) < recalculateDestRadius) {
entity.ghosting = true
entity.moveAction(goal)
}
}
if (entity.position.equals(route.at(-1))) {
entity.despawn()
}
}
}
}
static #playerLogic() {
const entity = this
// if (entity.dead) {
// entity.respawn()
// }
}
}
+120
View File
@@ -0,0 +1,120 @@
import { Shape, ShapeUtils, Vector2 } from 'three'
import SAT from 'sat'
export default class Terrain {
id = `terrain-${Terrain.nextId()}`
static nextId() { return this.#nextUniqueId++ }
static #nextUniqueId = 0
bbox = new Float32Array(4)
collision = true
ghostable = false
position = new Vector2()
relativeVertices = []
#colliders = []
#vertices = []
#unadjustedWaypoints = []
constructor(vertices, collision = null) {
this.#vertices = vertices.map((v) => new Vector2(v.x, v.y))
if (ShapeUtils.isClockWise(this.#vertices)) {
this.#vertices.reverse()
}
if (collision != null) {
this.collision = collision
}
this.#calculateColliders()
this.#calculatePosition()
this.#calculateRelativeVertices()
this.#calculateUnadjustedWaypoints()
this.#calculateBbox()
}
get vertices() { return this.#vertices }
get dead() { return false }
static waypointsForSide(fromVertex, toVertex, isClockwise = false) {
const from = isClockwise ? toVertex : fromVertex
const to = isClockwise ? fromVertex : toVertex
const origin = new Vector2()
const sideNormal = to.clone().sub(from).clone().normalize()
const margin = sideNormal.clone().rotateAround(origin, -3 * Math.PI / 4)
const offset = margin.clone().multiplyScalar(Math.SQRT2)
const inverseMargin = sideNormal.clone().negate().rotateAround(origin, 3 * Math.PI / 4)
const inverseOffset = inverseMargin.clone().multiplyScalar(Math.SQRT2)
return [
[margin.clone().add(from), offset],
[inverseMargin.clone().add(to), inverseOffset],
]
}
colliders() { return this.#colliders }
unadjustedWaypoints() { return this.#unadjustedWaypoints }
#shape() {
const complexShape = new Shape()
complexShape.moveTo(this.#vertices.at(0).x, this.#vertices.at(0).y)
this.#vertices.slice(1).forEach((v) => complexShape.lineTo(v.x, v.y))
return complexShape
}
#calculateBbox() {
const firstVertex = this.vertices.at(0)
if (firstVertex != null) {
this.bbox[0] = firstVertex.y
this.bbox[1] = firstVertex.x
this.bbox[2] = firstVertex.y
this.bbox[3] = firstVertex.x
}
this.vertices.forEach((v) => {
if (v.y > this.bbox[0]) {
this.bbox[0] = v.y
}
if (v.x > this.bbox[1]) {
this.bbox[1] = v.x
}
if (v.y < this.bbox[2]) {
this.bbox[2] = v.y
}
if (v.x < this.bbox[3]) {
this.bbox[3] = v.x
}
})
}
#calculateColliders() {
const points = this.#shape().extractPoints(16)
const indicesToPolygon = (indices) => {
const satPoints = [
new SAT.Vector(...points.shape[indices[0]].toArray()),
new SAT.Vector(...points.shape[indices[1]].clone().sub(points.shape[indices[0]]).toArray()),
new SAT.Vector(...points.shape[indices[2]].clone().sub(points.shape[indices[0]]).toArray()),
]
return new SAT.Polygon(satPoints[0], [new SAT.Vector(), satPoints[1], satPoints[2]])
}
this.#colliders = ShapeUtils.triangulateShape(points.shape, points.holes).map(indicesToPolygon)
}
#calculatePosition() {
this.position = this.#vertices.reduce(((sum, v) => sum.add(v)), new Vector2()).divideScalar(this.#vertices.length)
}
#calculateRelativeVertices() {
this.relativeVertices = this.#vertices.map((v) => v.clone().sub(this.position))
}
#calculateUnadjustedWaypoints() {
this.#unadjustedWaypoints = this.#vertices.map((v, i, arr) => Terrain.waypointsForSide(v, i + 1 < arr.length ? arr[i + 1] : arr[0])).flat()
}
}
+23
View File
@@ -0,0 +1,23 @@
import WebSocket from 'ws'
const numberOfClients = 10
const url = 'ws://localhost:1280/ws'
for (let i = 1; i <= numberOfClients; i++) {
const id = `${i}`
const websocket = new WebSocket(url)
websocket.onerror = () => websocket.close()
websocket.onopen = () => {
websocket.send(JSON.stringify({ action: 'join', id }))
console.log({ client: id, event: 'joined' })
}
websocket.onclose = () => {
console.log({ client: id, event: 'disconnected' })
}
websocket.onmessage = (event) => {
const byteSize = new Blob([event.data]).size
// console.log({ client: id, received: `${byteSize} B of data` })
}
}
+92
View File
@@ -0,0 +1,92 @@
<html>
<head>
<title>Terrain Creator</title>
<style>
* {
box-sizing: border-box;
}
html, body {
background-color: black;
font-family: sans-serif;
}
html {
padding: 0;
}
body {
margin: 0;
}
#map {
background-color: white;
background-image: url('./background.png');
background-size: cover;
}
.point {
position: absolute;
border-radius: 50%;
margin-top: -5px;
margin-left: -5px;
width: 10px;
height: 10px;
background-color: red;
border: 1px solid white;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var width = null
var height = null
var scale = null
var points = []
window.addEventListener('load', () => {
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
width = params.width
height = params.height
scale = params.scale
if (width == null) {
width = prompt('Width: ')
}
if (height == null) {
height = prompt('Height: ')
}
if (scale == null) {
scale = prompt('Scale: ')
}
const map = document.getElementById('map')
map.style.width = `${width / scale}px`
map.style.height = `${height / scale}px`
map.addEventListener('contextmenu', (event) => event.preventDefault())
map.addEventListener('mousedown', (event) => {
if (event.button == 2) {
console.log(`\n\n[\n` + points.map((p) => ` new Vector2(${p.x}, ${p.y}),`).join(`\n`) + `\n],\n`)
points = []
map.innerHTML = ''
return
}
if (event.button == 0) {
const x = Math.floor(event.pageX * scale)
const y = Math.floor(height - (event.pageY * scale))
points.push({ x, y })
const point = document.createElement('div')
point.classList.add('point')
point.style.left = event.pageX
point.style.top = event.pageY
map.appendChild(point)
return
}
})
})
</script>
</body>
</html>