Compare commits
77 Commits
godot
..
55e5e8117c
| Author | SHA1 | Date | |
|---|---|---|---|
|
55e5e8117c
|
|||
|
15e72a9e10
|
|||
|
4acd7a2881
|
|||
|
afa419e77a
|
|||
|
441a73355e
|
|||
|
59b5a603a0
|
|||
|
4c76d5dbde
|
|||
|
0db1ceeedc
|
|||
|
4f8dcebcd1
|
|||
|
c4c7c921d7
|
|||
|
916bc31356
|
|||
|
fa2dbb5237
|
|||
|
8ce1a2266f
|
|||
|
6b8a220f39
|
|||
|
bf38f69071
|
|||
|
634dde2a3b
|
|||
|
e4f1fe19f4
|
|||
|
072204b902
|
|||
|
04cc3f951e
|
|||
|
e75c0d2944
|
|||
|
0a4853aff9
|
|||
|
0b949683a6
|
|||
|
7824ba976b
|
|||
|
8457312f63
|
|||
|
18c3ace616
|
|||
|
7415475cb0
|
|||
|
ed6394354e
|
|||
|
8ebae0d866
|
|||
|
b4162d4e39
|
|||
|
8e95bc141c
|
|||
|
9345c7af04
|
|||
|
80ccb92815
|
|||
|
a44693aa5d
|
|||
|
1a5e811020
|
|||
|
787b48a4df
|
|||
|
20f8a2f1fe
|
|||
|
597aa204de
|
|||
|
92e06dedce
|
|||
|
9d3fbda494
|
|||
|
ffbc4d9803
|
|||
|
16429a6e1b
|
|||
|
03bbea4862
|
|||
|
49a4d3e924
|
|||
|
ea23aa3174
|
|||
|
8e861929cb
|
|||
|
6ff950640c
|
|||
|
302d2f0618
|
|||
|
d9d62d7070
|
|||
|
d9849f770b
|
|||
|
e0dd7dcaf3
|
|||
|
2eb914a680
|
|||
|
51b61ab449
|
|||
|
957b09b878
|
|||
|
462dfe7b9a
|
|||
|
4aba510ec0
|
|||
|
f1c191f61f
|
|||
|
fe4dc8b8bc
|
|||
|
8fe48fb679
|
|||
|
0f8a73911f
|
|||
|
5acc827f7b
|
|||
|
2570f32592
|
|||
|
2a9ef691fe
|
|||
|
f48a6bf9aa
|
|||
|
3bb34ed012
|
|||
|
227cc1590a
|
|||
|
fb6e75e38c
|
|||
|
05360208b0
|
|||
|
47aade7b3f
|
|||
|
37a77e902c
|
|||
|
ae6f4c2847
|
|||
|
ba0d8f606a
|
|||
|
604368b52c
|
|||
|
e23978ea90
|
|||
|
054d22d01a
|
|||
|
14212afd70
|
|||
|
69343821b6
|
|||
|
2957903cb1
|
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
*Dockerfile*
|
||||||
|
*docker-compose*
|
||||||
|
node_modules
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Normalize EOL for all files that Git considers text files.
|
|
||||||
* text=auto eol=lf
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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 +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 |
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[gd_resource type="StandardMaterial3D" format=3 uid="uid://diptcpjxid3cm"]
|
|
||||||
|
|
||||||
[resource]
|
|
||||||
albedo_color = Color(0.799569, 0, 0.0857406, 1)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[gd_resource type="StandardMaterial3D" format=3 uid="uid://chp3rogcgumau"]
|
|
||||||
|
|
||||||
[resource]
|
|
||||||
albedo_color = Color(0.054902, 0.431373, 0.129412, 1)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[gd_resource type="StandardMaterial3D" format=3 uid="uid://ccrb46njti2ke"]
|
|
||||||
|
|
||||||
[resource]
|
|
||||||
albedo_color = Color(0.270588, 0.596078, 1, 1)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[gd_resource type="PlaneMesh" format=3 uid="uid://dwpvym2kc4gd8"]
|
|
||||||
|
|
||||||
[resource]
|
|
||||||
size = Vector2(10000, 10000)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
|
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
|
|
||||||
|
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
|
|
||||||
@@ -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
|
|
||||||
|
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
|
|
||||||
|
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
|
|
||||||
@@ -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
|
|
||||||
|
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
|
|
||||||
|
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
|
|
||||||
|
Before Width: | Height: | Size: 543 B |
@@ -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
|
|
||||||
@@ -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
|
|
||||||
|
Before Width: | Height: | Size: 95 B |
@@ -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
|
|
||||||
|
Before Width: | Height: | Size: 95 B |
@@ -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
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 365 B |
@@ -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"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://b6nq7wjyrroi0"]
|
|
||||||
|
|
||||||
[node name="EmptyNode" type="Node"]
|
|
||||||
@@ -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
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://c3ks2lbj65erq
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://b3vuso52nyr8p
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dhgarroknbyh0
|
|
||||||
@@ -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 +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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://d3hbh4mxk38n6
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bu8fv7b2ndq3
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://c0u1dn17o73ox
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://xwmho0fyx2rj
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://beydsuuy3og2r
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://ciltmpb1wqqpr
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dikih6wn2rwsk
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://20wwhts3wq7t
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://5x0hciokrxcj
|
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class Team {
|
||||||
|
static neutral = 'neutral'
|
||||||
|
static blue = 'blue'
|
||||||
|
static red = 'red'
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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` })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||