Compare commits
11 Commits
55e5e8117c
..
godot
| Author | SHA1 | Date | |
|---|---|---|---|
|
aa3839a02a
|
|||
|
9293b1c96b
|
|||
|
3f1f4b1811
|
|||
|
151a757e3e
|
|||
|
ca63d4594c
|
|||
|
591225996d
|
|||
|
ac49bcee0b
|
|||
|
4e65d2c43b
|
|||
|
be682b2813
|
|||
|
c8c5c08b4d
|
|||
|
3ef5254792
|
@@ -1,4 +0,0 @@
|
|||||||
.git
|
|
||||||
*Dockerfile*
|
|
||||||
*docker-compose*
|
|
||||||
node_modules
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Normalize EOL for all files that Git considers text files.
|
||||||
|
* text=auto eol=lf
|
||||||
@@ -1,136 +1,5 @@
|
|||||||
# Logs
|
# Godot 4+ specific ignores
|
||||||
logs
|
.godot/
|
||||||
*.log
|
/android/
|
||||||
npm-debug.log*
|
build/
|
||||||
yarn-debug.log*
|
*.tmp
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
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"]
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
[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}'"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 994 B |
@@ -0,0 +1,37 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://diptcpjxid3cm"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.799569, 0, 0.0857406, 1)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://chp3rogcgumau"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.054902, 0.431373, 0.129412, 1)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://ccrb46njti2ke"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.270588, 0.596078, 1, 1)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="PlaneMesh" format=3 uid="uid://dwpvym2kc4gd8"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
size = Vector2(10000, 10000)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,38 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 543 B |
@@ -0,0 +1,34 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 95 B |
@@ -0,0 +1,34 @@
|
|||||||
|
[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
|
||||||
|
After Width: | Height: | Size: 95 B |
@@ -0,0 +1,34 @@
|
|||||||
|
[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
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
; 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
|
||||||
@@ -1,648 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 365 B |
@@ -0,0 +1,179 @@
|
|||||||
|
[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"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[gd_scene format=3 uid="uid://b6nq7wjyrroi0"]
|
||||||
|
|
||||||
|
[node name="EmptyNode" type="Node"]
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
[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"]
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
[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"]
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
[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"]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c3ks2lbj65erq
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3vuso52nyr8p
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dhgarroknbyh0
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://tt4s3jkcyqt
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cm5vnqe8f0f2u
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d3hbh4mxk38n6
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bu8fv7b2ndq3
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0u1dn17o73ox
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://xwmho0fyx2rj
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://beydsuuy3og2r
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ciltmpb1wqqpr
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dikih6wn2rwsk
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://20wwhts3wq7t
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://5x0hciokrxcj
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
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)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,860 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default class Team {
|
|
||||||
static neutral = 'neutral'
|
|
||||||
static blue = 'blue'
|
|
||||||
static red = 'red'
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<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>
|
|
||||||