Compare commits

..

56 Commits

Author SHA1 Message Date
356897045f Merge pull request 'chore: add flake check workflow' (#21) from workflow-init into master
All checks were successful
flake-check / build (push) Successful in 46s
Reviewed-on: #21
2025-08-27 21:08:53 +00:00
1e1f8cebe8 chore: add flake check workflow
All checks were successful
flake-check / build (pull_request) Successful in 48s
2025-08-27 23:05:32 +02:00
b21ad67cfc chore: add gdformat check to flake 2025-08-26 21:51:38 +02:00
4fffa8784a chore: remove version from image name 2025-08-26 20:23:43 +02:00
dc3f8c94f8 Merge pull request 'Add minimal main menu' (#19) from 15-main-menu into master
Reviewed-on: #19
2025-08-23 19:06:03 +00:00
f06bf17757 game: add minimal main menu 2025-08-23 21:04:26 +02:00
d74831df56 chore: gdformat bleed changes 2025-08-23 20:52:29 +02:00
2bd83504f2 game: remove raycast added to enemy group by mistake 2025-08-23 20:51:13 +02:00
3f21aef4eb game: make blead deal damage 2025-08-22 20:56:06 +02:00
6fff2dd9a3 game: add bleed 2025-08-22 20:50:11 +02:00
0dada63709 assets: add bleed icon sprite 2025-08-22 20:50:04 +02:00
d862974747 assets: add puddle sprite 2025-08-22 15:21:45 +02:00
f53d91a9eb game: cache some calculations for enemies 2025-08-22 14:12:42 +02:00
d57a59e9fe game: make some changes to water shader 2025-08-22 10:19:55 +02:00
950d177936 game: add enemy mods 2025-08-22 07:49:02 +02:00
40d6162b95 game: add enemy modifier class 2025-08-22 06:27:51 +02:00
7311b52fad game: add name-generator to enemies 2025-08-22 06:03:11 +02:00
b2129d9aa6 game: correct slime sprite naming 2025-08-22 05:43:15 +02:00
892b37572f chore: format slime changes 2025-08-21 23:56:49 +02:00
7aedea8206 game: add slime enemy 2025-08-21 23:56:18 +02:00
7cff7b62e9 assets: update slime assets 2025-08-21 23:56:10 +02:00
0d8356957d assets: add grayscale slime 2025-08-21 23:20:41 +02:00
9044338662 assets: add small slime 2025-08-21 23:01:33 +02:00
0d8846fb83 game: add name to enemies 2025-08-21 22:27:27 +02:00
76cb23b5db chore: add version to slopvivors_docker output 2025-08-21 20:38:44 +02:00
45b3b1c53e chore: use version from project in flake 2025-08-21 20:20:01 +02:00
1d75a850bf game: add weapon mod system 2025-08-21 19:49:07 +02:00
3e12c386c2 game: use shader instead of sprite for xp orbs 2025-08-21 10:59:50 +02:00
9240413da7 chore: clean up old debug prints 2025-08-21 10:43:50 +02:00
66822cbf9e chore: update flake.lock 2025-08-21 10:24:01 +02:00
f166f138a3 chore: update README again 2025-08-21 10:17:55 +02:00
699b758afb chore: update README 2025-08-21 10:12:50 +02:00
85d9486d2f chore: add alias for pushing image 2025-08-21 10:07:49 +02:00
294c88a830 chore: add docker image to flake 2025-08-21 09:53:23 +02:00
a2dfc2d65d chore: add web server package 2025-08-21 09:26:42 +02:00
a1cc8042ea chore: update .gdignore 2025-08-21 09:26:18 +02:00
5e8301a04c chore: add web-files package to flake 2025-08-21 08:24:23 +02:00
36dfcee36d chore: add web export preset 2025-08-21 07:44:32 +02:00
0046865101 game: add rat enemy 2025-08-21 07:09:42 +02:00
f860ddd669 assets: add small bat 2025-08-21 06:47:07 +02:00
80d00d0c26 game: fix warning in enemy base 2025-08-21 06:26:44 +02:00
f0f717e00e game: improve enemy-manager 2025-08-21 06:11:09 +02:00
83e2067fea game: change rat base hp 2025-08-21 06:10:24 +02:00
a0d121fadc game: add weapon active ability 2025-08-21 05:52:40 +02:00
8f02850a73 game: add shader to indicate low hp 2025-08-21 02:10:23 +02:00
632643a130 game: use shader for enemy on-hit visual 2025-08-21 00:58:58 +02:00
c8340efc95 Merge pull request 'game: add weapon base and sword' (#13) from rework-attack into master
Reviewed-on: #13
2025-08-20 22:40:04 +00:00
8bd7bfa406 game: add weapon base and sword 2025-08-21 00:33:59 +02:00
156da4898e game: adjust max collision for pathfind shape 2025-08-20 22:37:27 +02:00
ac8ffdcd96 game: tweak enemy movement 2025-08-20 19:55:46 +02:00
56278de1d7 game: add spawn rate to debug ui 2025-08-20 19:41:13 +02:00
7136b07de5 game: add base enemy and rat 2025-08-20 19:32:21 +02:00
4a900e99bd assets: add small rat sprite 2025-08-20 19:31:33 +02:00
051406cf40 game: remove old debug prints 2025-08-20 09:46:53 +02:00
98e7753014 Merge pull request 'Add XP bar' (#9) from 1-xp-bar into master
Reviewed-on: #9
2025-08-20 06:43:57 +00:00
f58fe398cf game: add xp bar 2025-08-20 08:42:55 +02:00
95 changed files with 2019 additions and 228 deletions

View File

@@ -1,3 +1,4 @@
README.md
flake.nix
flake.lock
result

16
.github/workflows/flake-check.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: flake-check
on:
push:
branches: master
pull_request:
branches:
- master
jobs:
build:
runs-on: [ ubuntu-latest, homelab ]
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- run: nix flake check

View File

@@ -2,20 +2,40 @@
Not AI-slop, but just regular slop survivors game.
# Build
## Run
Build using nix:
Can be run easily with either docker, or nix.
### Web-version using docker
Run the following command to run a web-server using docker.
In your browser, navigate to http://localhost:8080 to play.
```console
$ nix build .#\slopvivors
$ docker run --rm -it -p 8080:8080 git.t-juice.club/torjus/slopvivors:latest
```
# Development
### Linux version using nix
Desktop version can be run using nix flake.
```console
$ nix run git+https://git.t-juice.club/torjus/slopvivors#slopvivors
```
## Development
Use nix to setup development environment. Contains godot and other useful tools.
```console
$ nix develop
$ godot project.godot
```shell
nix develop
godot project.godot
# slop-format runs gdformat on gdscript files, run before commit
slop-format
# slop-push pushes image with latest-tag to git.t-juice.club
slop-push
```

BIN
animation/generic_anims.res Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
shader_type canvas_item;
uniform vec4 base_color : source_color = vec4(1.0, 0.2, 0.2, 1.0); // tint color
void fragment() {
vec4 tex = texture(TEXTURE, UV);
COLOR = vec4(base_color.rgb * tex.r, tex.a);
}

View File

@@ -0,0 +1 @@
uid://cf48pgfl308o3

View File

@@ -0,0 +1,8 @@
shader_type canvas_item;
uniform float flash_amount : hint_range(0.0, 1.0) = 0.0; // 0 = normal, 1 = full white
void fragment() {
vec4 tex_color = texture(TEXTURE, UV);
COLOR = mix(tex_color, vec4(1.0, 1.0, 1.0, tex_color.a), flash_amount);
}

View File

@@ -0,0 +1 @@
uid://p7evv1wldgsa

View File

@@ -0,0 +1,39 @@
shader_type canvas_item;
uniform sampler2D screen_texture : hint_screen_texture;
uniform float damage_amount : hint_range(0.0, 1.0) = 0.0;
uniform float noise_strength : hint_range(0.0, 1.0) = 0.3;
uniform float pulse_strength : hint_range(0.0, 1.0) = 0.2;
uniform float time;
// TODO: uniform vec2 player_uv; // use this to calculate player offset to camera
float random(vec2 uv) {
return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void fragment() {
// Distance from screen center
vec2 center = vec2(0.5, 0.5);
float dist = distance(SCREEN_UV, center);
float edge_factor = smoothstep(0.4, 0.7, dist); // stronger at edges
float edge_factor_gray = smoothstep(0.0, 1.0-damage_amount, dist);
// Noise flicker
float n = random(SCREEN_UV * 200.0 + time * 2.0);
float noise = (n - 0.5) * noise_strength;
// Pulsing intensity
float pulse = sin(time * 5.0) * pulse_strength;
// Final intensity
float final_amount = damage_amount * edge_factor + noise + pulse;
final_amount = clamp(final_amount, 0.0, 1.0);
vec4 base = texture(screen_texture, SCREEN_UV);
float gray = dot(base.rgb, vec3(0.299, 0.587, 0.114));
base.rgb = mix(base.rgb, vec3(gray), edge_factor_gray * damage_amount);
vec4 red_tint = vec4(1.0, 0.0, 0.0, 1.0);
COLOR = mix(base, red_tint, final_amount);
}

View File

@@ -0,0 +1 @@
uid://dnwiwj1ak1bu5

View File

@@ -3,19 +3,28 @@ uniform vec4 water_color : source_color = vec4(0.0, 0.0, 1.0, 1.0);
uniform sampler2D noise_tex; // noise texture
uniform vec2 noise_speed = vec2(0.05, 0.01);
uniform float noise_strength = 0.2; // how much to lighten/darken
uniform float pixel_size = 0.005;
void fragment() {
vec2 uv = UV;
vec4 base_tex = texture(TEXTURE, uv);
float is_water = step(0.8, 1.0 - distance(base_tex.rgb, water_color.rgb));
vec2 snapped_uv = floor(SCREEN_UV / pixel_size) * pixel_size;
// Scroll UVs for noise animation
vec2 noise_uv = SCREEN_UV + noise_speed * TIME;
//vec2 noise_uv = SCREEN_UV + noise_speed * TIME;
vec2 noise_uv = snapped_uv + noise_speed * TIME;
vec4 noise_sample = texture(noise_tex, fract(noise_uv));
float n = noise_sample.r;
n = pow(n, 3.0) * 4.0;
n = clamp(n,0.0, 1.0);
vec4 noisy_color = water_color.rgba;
noisy_color += vec4(0.1* (noise_sample.r - 0.5), 0.05 * (noise_sample.r - 0.5), 0.0, 1.0);
// Use noise (0..1) to brighten/darken the water color
float brightness = (noise_sample.r - 0.5) * 2.0 * noise_strength;
vec4 animated_water = water_color + vec4(vec3(brightness), 0.0);
float brightness = (n - 0.5) * 4.0 * noise_strength;
vec4 animated_water = noisy_color + vec4(vec3(brightness), 0.0);
COLOR = mix(base_tex, animated_water, is_water);

View File

@@ -0,0 +1,24 @@
shader_type canvas_item;
uniform vec4 orb_color : source_color = vec4(0.2, 1.0, 0.5, 1.0); // base orb color
uniform float glow_strength : hint_range(0.0, 2.0) = 1.2;
uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
void fragment() {
// Centered UV (0,0 in middle, -1..1 range)
vec2 uv = (UV - vec2(0.5)) * 2.0;
float dist = length(uv);
// Soft circular mask
float circle = smoothstep(0.8, 0.0, dist);
// Glow falloff
float glow = smoothstep(0.5, 0.0, dist) * glow_strength;
// Pulsing
float pulse = 0.5 + 0.5 * sin(TIME * pulse_speed);
vec3 color = orb_color.rgb * (circle + glow * pulse);
COLOR = vec4(color, circle); // alpha fades at edges
}

View File

@@ -0,0 +1 @@
uid://ckjyt0mt0ngk

BIN
assets/sprites/puddle_1.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/puddle_1.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c5t4it4if0s6g"
path="res://.godot/imported/puddle_1.png-28d62b8b3cc5647c5219303699bcce62.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/puddle_1.png"
dest_files=["res://.godot/imported/puddle_1.png-28d62b8b3cc5647c5219303699bcce62.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/rat_small.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/rat_small.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://in5id1p5bxux"
path="res://.godot/imported/rat_small.png-00253d8b77772b9b3ad68d57407664f2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/rat_small.png"
dest_files=["res://.godot/imported/rat_small.png-00253d8b77772b9b3ad68d57407664f2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/slime_small grayscale.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/slime_small grayscale.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ctob5me7woid8"
path="res://.godot/imported/slime_small grayscale.png-b5ea2536903e774dce2837ff19f6ad76.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/slime_small grayscale.png"
dest_files=["res://.godot/imported/slime_small grayscale.png-b5ea2536903e774dce2837ff19f6ad76.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/slime_small.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/slime_small.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bfj2h2jp1w60p"
path="res://.godot/imported/slime_small.png-a01d407ba84c2dd70ce8b2ef5c142cae.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/slime_small.png"
dest_files=["res://.godot/imported/slime_small.png-a01d407ba84c2dd70ce8b2ef5c142cae.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/small_bat.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/small_bat.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chtjfq3bmonhj"
path="res://.godot/imported/small_bat.png-8cac10440ad9b69c99d6007471ff8d06.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/small_bat.png"
dest_files=["res://.godot/imported/small_bat.png-8cac10440ad9b69c99d6007471ff8d06.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/small_bleed_icon.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/small_bleed_icon.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c856sh6vk5lqa"
path="res://.godot/imported/small_bleed_icon.png-472f2c7cb608bb835a947b6c2d78acf7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/small_bleed_icon.png"
dest_files=["res://.godot/imported/small_bleed_icon.png-472f2c7cb608bb835a947b6c2d78acf7.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

View File

@@ -3,7 +3,7 @@
name="Linux"
platform="Linux"
runnable=true
advanced_options=false
advanced_options=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -39,3 +39,47 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
[preset.1]
name="Web"
platform="Web"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=""
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.1.options]
custom_template/debug=""
custom_template/release=""
variant/extensions_support=false
variant/thread_support=false
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=true
progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1)

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1755186698,
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=",
"lastModified": 1755615617,
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
"type": "github"
},
"original": {

139
flake.nix
View File

@@ -30,23 +30,56 @@
{
default = pkgs.mkShell {
packages = with pkgs; [
godot
gdtoolkit_4
krita
gimp
pixelorama
aseprite
gdtoolkit_4
gimp
godot
krita
pixelorama
skopeo
];
shellHook = ''
alias slop-format='find . -name "*.gd" | xargs gdformat'
alias slop-push='nix build .#\slopvivors_docker && skopeo copy docker-archive:result docker://git.t-juice.club/torjus/slopvivors:latest'
'';
};
}
);
checks = forAllSystems (
{ pkgs }:
{
gdformat = pkgs.stdenvNoCC.mkDerivation {
name = "gdformat-check";
src = pkgs.lib.sources.sourceFilesBySuffices (pkgs.lib.cleanSource ./.) [ ".gd" ];
nativeBuildInputs = with pkgs; [
gdtoolkit_4
];
dontBuild = true;
doCheck = true;
checkPhase = ''
export HOME=$(mktemp -d)
find . -name "*.gd" -print0 | xargs -0 gdformat --check
echo "All .gd files are properly formatted"
'';
installPhase = "mkdir $out";
};
}
);
packages = forAllSystems (
{ pkgs }:
let
project_file = builtins.readFile ./project.godot;
version = builtins.head (builtins.match ".*version=\"([0-9.]+)\".*" project_file);
in
{
slopvivors = pkgs.stdenv.mkDerivation {
pname = "slopvivors";
version = "0.1.0";
version = version;
src = ./.;
strictDeps = true;
@@ -68,13 +101,105 @@
runHook postBuild
'';
installPhase = ''
find .
install -D -m 755 -t $out/libexec ./build/slopvivors
install -D -m 644 -t $out/libexec ./build/slopvivors.pck
install -d -m 755 $out/bin
ln -s $out/libexec/slopvivors $out/bin/slopvivors
'';
};
slopvivors_web_files = pkgs.stdenv.mkDerivation {
pname = "slopvivors-web-files";
version = version;
src = ./.;
strictDeps = true;
nativeBuildInputs = with pkgs; [
godot
godot-export-templates-bin
];
buildPhase = ''
runHook preBuild
export HOME=$(mktemp -d)
mkdir -p $HOME/.local/share/godot/
ln -s ${pkgs.godot-export-templates-bin}/share/godot/export_templates "$HOME/.local/share/godot/"
mkdir -p build
godot --headless --export-release Web ./build/slopvivors
runHook postBuild
'';
installPhase = ''
mkdir -p "$out"
cp ./build/* "$out"
mv ./build/slopvivors "$out"/index.html
'';
};
slopvivors_web = pkgs.buildGoModule {
pname = "slopvivors-web";
version = version;
vendorHash = null;
src = self.packages.${pkgs.system}.slopvivors_web_files;
nativeBuildInputs = with pkgs; [
go
];
prePatch = ''
mkdir -p slopvivors_web_files
cp "${self.packages.${pkgs.system}.slopvivors_web_files}"/* slopvivors_web_files
cat > main.go <<EOF
package main
import (
"embed"
"fmt"
"net/http"
"io/fs"
)
//go:embed slopvivors_web_files/*
var webFiles embed.FS
func main() {
rootDir, err := fs.Sub(webFiles, "slopvivors_web_files")
if err != nil {
panic(err)
}
http.Handle("/", http.FileServer(http.FS(rootDir)))
fmt.Printf("Serving on :8080\n")
http.ListenAndServe(":8080", nil)
}
EOF
'';
postPatch = ''
go mod init slopvivors-web
'';
};
slopvivors_docker = pkgs.dockerTools.buildLayeredImage {
name = "slopvivors-docker";
tag = version;
created = "now";
contents = [
pkgs.busybox
pkgs.caddy
self.packages.${pkgs.system}.slopvivors_web_files
];
config = {
Cmd = [
"caddy"
"file-server"
"-a"
"--listen"
":8080"
];
WorkingDir = "${self.packages.${pkgs.system}.slopvivors_web_files}/";
};
};
}
);
};

View File

@@ -10,14 +10,21 @@ signal sig_debug_enemy_god_mode(value: bool)
signal sig_debug_stats_set(key: String, value: String)
@warning_ignore("unused_signal")
signal sig_stop_spawning(value: bool)
@warning_ignore("unused_signal")
signal sig_set_spawn_rate(value: float)
@warning_ignore("unused_signal")
signal sig_on_player_hp_change(hp: float, max_hp: float)
@warning_ignore("unused_signal")
signal sig_toggle_low_hp_shader(toggled_on: float)
const GROUP_ENEMY = "enemy"
const GROUP_DAMAGEABLE = "damagable"
const GROUP_PLAYER = "player"
const GROUP_XP_ORB = "xp_orb"
const GROUP_PICKUP = "pickup"
const GROUP_PROJ_MANAGER = "proj_manager"
enum ModRarity { LEGENDARY, EPIC, RARE, NORMAL }
enum Rarity { LEGENDARY, EPIC, RARE, NORMAL }
const placeholder_tex = preload("res://assets/sprites/64x64_placeholder.tres")
@@ -25,7 +32,7 @@ var MOD_CHOICES = [
{
"internal_name": "flat_health_small",
"name": "+10 Health",
"rarity": ModRarity.NORMAL,
"rarity": Rarity.NORMAL,
"tex": placeholder_tex,
"description": "Adds 10 flat health.",
"weight": 100,
@@ -36,7 +43,7 @@ var MOD_CHOICES = [
{
"internal_name": "flat_health_med",
"name": "+25 Health",
"rarity": ModRarity.RARE,
"rarity": Rarity.RARE,
"tex": placeholder_tex,
"description": "Adds 25 flat health.",
"weight": 50,
@@ -47,7 +54,7 @@ var MOD_CHOICES = [
{
"internal_name": "flat_health_large",
"name": "+50 Health",
"rarity": ModRarity.EPIC,
"rarity": Rarity.EPIC,
"tex": placeholder_tex,
"description": "Adds 50 flat health.",
"weight": 10,
@@ -58,7 +65,7 @@ var MOD_CHOICES = [
{
"internal_name": "flat_crit_small",
"name": "2.5% More Critical Chance",
"rarity": ModRarity.RARE,
"rarity": Rarity.RARE,
"tex": placeholder_tex,
"description": "Gives 2.5% more base critical hit chance",
"weight": 50,
@@ -69,7 +76,7 @@ var MOD_CHOICES = [
{
"internal_name": "flat_crit_large",
"name": "5% More Critical Chance",
"rarity": ModRarity.EPIC,
"rarity": Rarity.EPIC,
"tex": placeholder_tex,
"description": "Gives 5% more base critical hit chance",
"weight": 10,
@@ -80,6 +87,17 @@ var MOD_CHOICES = [
]
func rarity_to_color(rarity: Rarity) -> Color:
match rarity:
Rarity.RARE:
return Color.YELLOW
Rarity.EPIC:
return Color.BLUE_VIOLET
Rarity.LEGENDARY:
return Color.DARK_ORANGE
return Color.WHITE
func _draw_random_choice(fortune: float = 1.0) -> Dictionary:
var total_weight: int = 0
for choice in MOD_CHOICES:
@@ -102,7 +120,6 @@ func draw_random_mod(fortune: float = 1.0) -> PlayerStatsModifier:
mod.description = choice["description"]
mod.internal_name = choice["internal_name"]
mod.tex = choice["tex"]
print_debug("gc: %s" % mod.tex)
mod.title = choice["name"]
return mod

View File

@@ -11,6 +11,7 @@ config_version=5
[application]
config/name="Slopvivors"
config/version="0.2.0"
run/main_scene="uid://bjg50n7aab3ng"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"
@@ -33,6 +34,7 @@ enemy="Group containing all enemies"
damagable="Can be damaged using take_damage"
xp_orb="Experience orbs"
pickup="Items that can be picked up"
proj_manager="Parent node for all projectiles"
[input]
@@ -66,6 +68,11 @@ click={
"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(90, 11),"global_position":Vector2(99, 59),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
active={
"deadzone": 0.2,
"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
[layer_names]

View File

@@ -1,102 +0,0 @@
extends Node2D
@export var default_attack_time: float = 0.5
@export var player: Player = null
@onready var trigger_area: Area2D = $TriggerArea
@onready var trigger_collision: CollisionShape2D = $TriggerArea/TriggerCollision
@onready var timer: Timer = $Timer
@onready var attack_path: Path2D = $AttackPath
@onready var path_follow_2d: PathFollow2D = $AttackPath/PathFollow2D
@onready var attack_area: Area2D = $AttackPath/PathFollow2D/Sprite2D/AttackArea
var base_damage: float = 5.0
var current_target: Node2D = null
var current_progress: float = 0.0
var damaged_this_attack: Array = []
var is_attacking: bool = false
func _ready() -> void:
attack_path.visible = false
func _process(delta: float) -> void:
if timer.is_stopped() and current_progress == 0.0:
timer.start()
if current_progress > 0.95:
reset_attack()
if (
current_target
and is_instance_valid(current_target)
and not current_target.is_queued_for_deletion()
):
track_target(current_target)
is_attacking = true
attack_path.visible = true
if is_attacking:
# Do attack animation
current_progress += delta / default_attack_time
current_progress = clampf(current_progress, 0.0, 1.0)
path_follow_2d.progress_ratio = current_progress
func reset_attack() -> void:
current_target = null
attack_path.visible = false
current_progress = 0.0
damaged_this_attack = []
is_attacking = false
position = Vector2.ZERO
rotation = 0.0
func set_target(body: Node2D):
current_target = body
is_attacking = true
func track_target(body: Node2D):
var mid_distance = attack_path.curve.get_baked_length() / 2
var mid_point: Vector2 = attack_path.curve.sample_baked(mid_distance)
var offset = body.global_position - to_global(mid_point)
var desired_dir = (body.global_position - to_global(mid_point)).normalized()
var start_point_global = attack_path.to_global(attack_path.curve.sample_baked(0))
var end_point_global = attack_path.to_global(
attack_path.curve.sample_baked(attack_path.curve.get_baked_length())
)
var curve_dir = (start_point_global - end_point_global).normalized()
var angle_diff = curve_dir.angle_to(desired_dir)
if rotation == 0.0:
rotation = curve_dir.angle_to(desired_dir)
position += offset
func _on_timer_timeout() -> void:
if current_target:
if trigger_area.has_overlapping_areas():
if current_target not in trigger_area.get_overlapping_bodies():
current_target = null
return
if trigger_area.has_overlapping_bodies():
for body in trigger_area.get_overlapping_bodies():
if body.is_in_group(GlobalConst.GROUP_ENEMY):
set_target(body)
func _on_attack_area_body_entered(body: Node2D) -> void:
if not attack_path.visible:
return
if body in damaged_this_attack:
return
if body.is_in_group(GlobalConst.GROUP_ENEMY) and body.is_in_group(GlobalConst.GROUP_DAMAGEABLE):
var crit_chance = player.player_stats.get_final("crit_chance", player.modifiers)
var damage_dealt = base_damage
var is_crit = randf() >= 1 - crit_chance
if is_crit:
damage_dealt *= player.player_stats.get_final("crit_multiplier", player.modifiers)
body.take_damage(damage_dealt, is_crit)
damaged_this_attack.append(body)

View File

@@ -1 +0,0 @@
uid://db326gu8abue5

View File

@@ -1,53 +0,0 @@
[gd_scene load_steps=6 format=3 uid="uid://cdojqe2m4kxx1"]
[ext_resource type="Texture2D" uid="uid://dycw7c3484dir" path="res://assets/sprites/sword.png" id="1_3fwwl"]
[ext_resource type="Script" uid="uid://db326gu8abue5" path="res://scenes/attacks/attack_sword.gd" id="1_frsqi"]
[sub_resource type="Curve2D" id="Curve2D_frsqi"]
bake_interval = 2.0
_data = {
"points": PackedVector2Array(0, 0, 0, 0, 0, 0, 0, 0, 25, 10, 200, 0)
}
point_count = 2
[sub_resource type="RectangleShape2D" id="RectangleShape2D_frsqi"]
size = Vector2(13.9997, 46.999)
[sub_resource type="CircleShape2D" id="CircleShape2D_3fwwl"]
radius = 267.002
[node name="AttackSword" type="Node2D"]
script = ExtResource("1_frsqi")
[node name="AttackPath" type="Path2D" parent="."]
curve = SubResource("Curve2D_frsqi")
[node name="PathFollow2D" type="PathFollow2D" parent="AttackPath"]
loop = false
[node name="Sprite2D" type="Sprite2D" parent="AttackPath/PathFollow2D"]
position = Vector2(0.322462, -0.946582)
rotation = 0.328329
texture = ExtResource("1_3fwwl")
[node name="AttackArea" type="Area2D" parent="AttackPath/PathFollow2D/Sprite2D"]
collision_layer = 0
collision_mask = 2
[node name="AttackCollision" type="CollisionShape2D" parent="AttackPath/PathFollow2D/Sprite2D/AttackArea"]
position = Vector2(-0.0328934, -4.50646)
shape = SubResource("RectangleShape2D_frsqi")
[node name="TriggerArea" type="Area2D" parent="."]
visible = false
collision_layer = 0
collision_mask = 2
[node name="TriggerCollision" type="CollisionShape2D" parent="TriggerArea"]
shape = SubResource("CircleShape2D_3fwwl")
[node name="Timer" type="Timer" parent="."]
one_shot = true
[connection signal="body_entered" from="AttackPath/PathFollow2D/Sprite2D/AttackArea" to="." method="_on_attack_area_body_entered"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View File

@@ -0,0 +1,8 @@
class_name EnemyEffectBase
extends Resource
var enemy: EnemyBase
func apply(enemy: EnemyBase) -> void:
push_error("%s does not implement apply" % self)

View File

@@ -0,0 +1 @@
uid://dg3yshxb4vdiu

View File

@@ -0,0 +1,33 @@
class_name EnemyEffectBleed
extends EnemyEffectBase
var damage: float
var duration: float
var tick_rate: float = 1.0
var _timer: Timer
var _remaining_ticks: int = 5
var _enemy: EnemyBase
const PUDDLE = preload("res://scenes/puddle.tscn")
func _init(enemy: EnemyBase, bleed_damage: float, duration: float):
damage = bleed_damage
_timer = Timer.new()
func apply(enemy: EnemyBase) -> void:
enemy.effects.append(self)
while _remaining_ticks > 0:
enemy.take_damage(damage, false)
_remaining_ticks -= 1
await enemy.get_tree().create_timer(1.0, false, true, false).timeout
var p = PUDDLE.instantiate()
enemy.get_parent().add_child(p)
p.global_position = enemy.global_position
enemy.effects.erase(self)
static func _is_bleeding(enemy: EnemyBase) -> bool:
return false

View File

@@ -0,0 +1 @@
uid://b2ptwltm211t3

View File

@@ -9,9 +9,9 @@ extends CharacterBody2D
@onready var contact_damage_cd: Timer = $ContactDamageCD
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var move_speed: float
var health: float
var max_health: float
var move_speed: float = default_move_speed
var health: float = default_max_health
var max_health: float = default_max_health
var god_mode: bool = false
var is_dead: bool = false

View File

@@ -0,0 +1,210 @@
class_name EnemyBase
extends CharacterBody2D
@export var move_speed: float = 100
@export var max_health: float = 10.0
@export var default_contact_damage: float = 0.0
@export var target_distance: float = 6.0
@export var path_update_interval: float = 1.5
@export var xp_dropped: float = 5.0
@export var enemy_rarity: GlobalConst.Rarity = GlobalConst.Rarity.NORMAL
var modifiers: Array[EnemyMod] = []
@onready var target_cast: RayCast2D = $TargetCast
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var contact_damage_cd: Timer = $ContactDamageCD
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
@onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D
@onready var shape_cast_2d: ShapeCast2D = $ShapeCast2D
@onready var sprite_2d: Sprite2D = $Sprite2D
@onready var label: Label = $HBoxContainer/Label
@onready var effect_container: HBoxContainer = $HBoxContainer/EffectContainer
var player: Player
var enemy_name: String
var target: Node2D
var god_mode: bool = false
var is_dead: bool = false
var health: float
var effects: Array[EnemyEffectBase]
var _path_update_timer: float = 0.0
var _compute_cache: KeyedCache = KeyedCache.new()
var _effects_visible = []
func _ready() -> void:
enemy_name = _gen_name()
match enemy_rarity:
GlobalConst.Rarity.NORMAL:
label.visible = false
GlobalConst.Rarity.RARE:
var mods = EnemyModPool.get_random_mods(2)
label.visible = true
modifiers = mods
GlobalConst.Rarity.EPIC:
var mods = EnemyModPool.get_random_mods(4)
label.visible = true
modifiers = mods
if modifiers.size() > 0:
enemy_name += " the %s" % modifiers.pick_random().adjective
label.add_theme_color_override("font_color", GlobalConst.rarity_to_color(enemy_rarity))
label.text = enemy_name
health = get_calculated("max_health")
shape_cast_2d.shape.radius = collision_shape_2d.shape.radius
shape_cast_2d.enabled = false
sprite_2d.material = sprite_2d.material.duplicate()
_find_player()
func _find_player():
if not player:
player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
target = player
func _gen_name() -> String:
return "Unnamed enemy"
func _process(delta: float) -> void:
for effect in effects:
if effect in _effects_visible:
continue
var effect_sprite = Sprite2D.new()
effect_sprite.texture = preload("res://assets/sprites/small_bleed_icon.png")
effect_container.add_child(effect_sprite)
_effects_visible.append(effect)
func _physics_process(delta: float) -> void:
if not target:
return
do_movement(delta)
check_contact_damage()
func do_movement(delta: float) -> void:
if not target:
return
if global_position.distance_to(target.global_position) < target_distance:
return
_path_update_timer -= delta + randf()
if _has_direct_path():
shape_cast_2d.enabled = false
_do_simple_movement()
else:
_do_nav_agent_movement()
if velocity.x < 0:
sprite_2d.flip_h = true
else:
sprite_2d.flip_h = false
func _has_direct_path():
target_cast.target_position = to_local(target.global_position)
target_cast.enabled = true
return not target_cast.is_colliding()
func _do_simple_movement():
var direction = global_position.direction_to(target.global_position)
var distance = global_position.distance_to(target.global_position)
if distance > 4:
velocity = direction * get_calculated("move_speed")
move_and_slide()
func _do_nav_agent_movement():
if _path_update_timer <= 0.0:
_path_update_timer = path_update_interval
nav_agent.target_position = target.global_position
if nav_agent.is_navigation_finished():
return
var next_point = nav_agent.get_next_path_position()
var direction = (next_point - global_position).normalized()
shape_cast_2d.target_position = to_local(next_point)
shape_cast_2d.enabled = true
if shape_cast_2d.is_colliding():
direction = direction.bounce(shape_cast_2d.get_collision_normal(0)).normalized()
velocity = direction * get_calculated("move_speed")
move_and_slide()
func check_contact_damage():
if default_contact_damage == 0.0:
return
if global_position.distance_to(target.global_position) > target_distance + 2:
return
deal_contact_damage()
func deal_contact_damage():
if target.is_in_group("damagable"):
if contact_damage_cd.is_stopped():
target.take_damage(default_contact_damage)
contact_damage_cd.start()
func take_damage(value: float, is_crit: bool = false):
if god_mode:
return
health -= value
var dm = preload("res://scenes/damage_numbers.tscn").instantiate()
dm.damage_taken = value
dm.critical_damage = is_crit
animation_player.play("generic_anims/take_damage")
add_child(dm)
if health <= 0:
die()
func cheer():
target_distance = 2
if not animation_player.is_playing():
animation_player.play("generic_anims/cheer")
func die():
if is_dead:
return
is_dead = true
drop_xp_orb()
target = null
velocity = Vector2.ZERO
animation_player.play("generic_anims/die")
func drop_xp_orb() -> void:
var orb: XPOrb = preload("res://scenes/xp_orb.tscn").instantiate()
orb.value = xp_dropped * (1 + (modifiers.size() * 3))
orb.position = position
get_parent().call_deferred("add_child", orb)
func _on_animation_player_animation_finished(anim_name: StringName) -> void:
if is_dead:
queue_free()
func get_calculated(key: String) -> Variant:
# set max move speed to players move speed
var compute_func = func():
if key == "move_speed":
return clampf(
EnemyMod.get_calculated(self, key),
0,
player.player_stats.get_final("move_speed", player.modifiers)
)
return EnemyMod.get_calculated(self, key)
return _compute_cache.get_or_compute(key, compute_func)
func has_property(key: String) -> bool:
var cache_key = "prop_%s" % key
var compute_func = func(): return get(key) != null
return _compute_cache.get_or_compute(key, compute_func)

View File

@@ -0,0 +1 @@
uid://dxn17u7ltuibw

View File

@@ -0,0 +1,114 @@
[gd_scene load_steps=10 format=3 uid="uid://b7vq8xspnlyeu"]
[ext_resource type="Script" uid="uid://dxn17u7ltuibw" path="res://scenes/enemies/enemy_base.gd" id="1_qty17"]
[ext_resource type="AnimationLibrary" uid="uid://dos4y853hq1gu" path="res://animation/generic_anims.res" id="2_pkqou"]
[ext_resource type="Shader" uid="uid://p7evv1wldgsa" path="res://assets/shaders/hit_flash.gdshader" id="2_satqt"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_qa0nx"]
shader = ExtResource("2_satqt")
shader_parameter/flash_amount = 0.0
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_pkqou"]
size = Vector2(32, 32)
[sub_resource type="CircleShape2D" id="CircleShape2D_satqt"]
radius = 4.0
[sub_resource type="Animation" id="Animation_satqt"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Sprite2D:position")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector2(0, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Sprite2D:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Sprite2D:material:shader_parameter/flash_amount")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [0.0]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_qa0nx"]
_data = {
&"RESET": SubResource("Animation_satqt")
}
[sub_resource type="CircleShape2D" id="CircleShape2D_pkqou"]
[node name="EnemyBase" type="CharacterBody2D" groups=["damagable", "enemy"]]
collision_layer = 2
collision_mask = 3
script = ExtResource("1_qty17")
[node name="Sprite2D" type="Sprite2D" parent="."]
material = SubResource("ShaderMaterial_qa0nx")
texture = SubResource("PlaceholderTexture2D_pkqou")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_satqt")
[node name="TargetCast" type="RayCast2D" parent="."]
enabled = false
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
libraries = {
&"": SubResource("AnimationLibrary_qa0nx"),
&"generic_anims": ExtResource("2_pkqou")
}
[node name="ContactDamageCD" type="Timer" parent="."]
wait_time = 0.5
[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."]
[node name="ShapeCast2D" type="ShapeCast2D" parent="."]
shape = SubResource("CircleShape2D_pkqou")
max_results = 2
[node name="HBoxContainer" type="HBoxContainer" parent="."]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -53.0
offset_right = 53.0
offset_bottom = 40.0
grow_horizontal = 2
grow_vertical = 2
[node name="EffectContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_vertical = 4
[node name="Label" type="Label" parent="HBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 9
horizontal_alignment = 1
[connection signal="animation_finished" from="AnimationPlayer" to="." method="_on_animation_player_animation_finished"]

View File

@@ -0,0 +1,32 @@
extends EnemyBase
const NAME_PREFIXES: Array[String] = [
"Chi",
"Sque",
"Flit",
"Bat",
"Kla",
]
const NAME_ROOTS: Array[String] = [
"ter",
"tch",
"reek",
"p",
"nap",
]
const NAME_SUFFIXES: Array[String] = [
"y",
"a",
"et",
"ik",
"in",
]
func _gen_name() -> String:
return (
"%s%s%s"
% [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()]
)

View File

@@ -0,0 +1 @@
uid://bfhmjpjwdh25o

View File

@@ -0,0 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://du4t4vydra4rm"]
[ext_resource type="PackedScene" uid="uid://b7vq8xspnlyeu" path="res://scenes/enemies/enemy_base.tscn" id="1_4r40l"]
[ext_resource type="Script" uid="uid://bfhmjpjwdh25o" path="res://scenes/enemies/enemy_bat.gd" id="2_1jj4k"]
[ext_resource type="Texture2D" uid="uid://chtjfq3bmonhj" path="res://assets/sprites/small_bat.png" id="2_608ut"]
[node name="EnemyBat" instance=ExtResource("1_4r40l")]
collision_mask = 2
script = ExtResource("2_1jj4k")
move_speed = 175.0
max_health = 6.0
default_contact_damage = 5.0
target_distance = 2.0
xp_dropped = 8.0
[node name="Sprite2D" parent="." index="0"]
texture = ExtResource("2_608ut")
[node name="TargetCast" parent="." index="2"]
collision_mask = 0
collide_with_bodies = false
[node name="ShapeCast2D" parent="." index="6"]
visible = false

View File

@@ -0,0 +1,34 @@
class_name EnemyMod
extends Resource
enum ModType { ADDITIVE, MULTIPLICATIVE, ABSOLUTE, BOOL }
@export var mod_name: String
@export var mod_property: String
@export var mod_value: float
@export var mod_value_bool: bool = false
@export var mod_type: ModType = ModType.MULTIPLICATIVE
var adjective: String
static func get_calculated(enemy: EnemyBase, key: String) -> Variant:
assert(
enemy.has_property(key),
"tried to calculate property '%s' where base value does not exist on %s" % [key, enemy]
)
var base_value = enemy.get(key)
var add = 0.0
var mul = 1.0
for mod in enemy.modifiers:
if mod.mod_property == key:
match mod.mod_type:
ModType.ADDITIVE:
add += mod.mod_value
ModType.MULTIPLICATIVE:
mul *= mod.mod_value
ModType.ABSOLUTE:
return mod.mod_value
ModType.BOOL:
return mod.mod_value_bool
return (base_value + add) * mul

View File

@@ -0,0 +1 @@
uid://d2mvgwmsmcxp4

View File

@@ -0,0 +1,58 @@
class_name EnemyModPool
extends Resource
const MAX_HEALTH_MOD: Dictionary = {
"mod_name": "Extra health",
"mod_property": "max_health",
"mod_value": 1.0,
"mod_value_bool": false,
"mod_type": EnemyMod.ModType.MULTIPLICATIVE,
"mod_adjectives":
[
"Sturdy",
"Bulky",
"Ironclad",
"Unyielding",
"Stonehide",
"Thicc",
"Chunky",
"Pudgy",
"Chonky",
"Double-stuffed"
],
}
const MOD_POOL = [
{
"mod_name": "Extra movement speed",
"mod_property": "move_speed",
"mod_value": 1.25,
"mod_value_bool": false,
"mod_type": EnemyMod.ModType.MULTIPLICATIVE,
"mod_adjectives": ["Swift", "Nimble", "Fleet", "Spry", "Skittering", "Zoomy", "Zippy"]
}
]
static func get_random_mods(count: int) -> Array[EnemyMod]:
var mods: Array[EnemyMod] = []
# always include extra health mod
var hp_mod = _dict_to_mod(MAX_HEALTH_MOD)
hp_mod.mod_value *= 5 * count
mods.append(hp_mod)
for i in count:
var mod_data = MOD_POOL.pick_random()
var mod = _dict_to_mod(mod_data)
mods.append(mod)
return mods
static func _dict_to_mod(d: Dictionary) -> EnemyMod:
var mod: EnemyMod = EnemyMod.new()
mod.mod_name = d["mod_name"]
mod.mod_property = d["mod_property"]
mod.mod_value = d["mod_value"]
mod.mod_value_bool = d["mod_value_bool"]
mod.mod_type = d["mod_type"]
mod.adjective = d["mod_adjectives"].pick_random()
return mod

View File

@@ -0,0 +1 @@
uid://bl11yfav37ecs

View File

@@ -0,0 +1,29 @@
extends EnemyBase
const NAME_PREFIXES: Array[String] = [
"Sk",
"Sn",
"Gr",
"R",
"Kr",
"V",
"Vr",
]
const NAME_ROOTS: Array[String] = [
"itch",
"abble",
"itter",
"usk",
"arp",
"uzzle",
]
const NAME_SUFFIXES: Array[String] = ["y", "er", "o", "ok", "in"]
func _gen_name() -> String:
return (
"%s%s%s"
% [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()]
)

View File

@@ -0,0 +1 @@
uid://ddvxp8eada0mw

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=4 format=3 uid="uid://c7jaqltfjhn5n"]
[ext_resource type="PackedScene" uid="uid://b7vq8xspnlyeu" path="res://scenes/enemies/enemy_base.tscn" id="1_o1awd"]
[ext_resource type="Texture2D" uid="uid://in5id1p5bxux" path="res://assets/sprites/rat_small.png" id="2_gscll"]
[ext_resource type="Script" uid="uid://ddvxp8eada0mw" path="res://scenes/enemies/enemy_rat.gd" id="2_sari4"]
[node name="EnemyRat" instance=ExtResource("1_o1awd")]
script = ExtResource("2_sari4")
default_contact_damage = 5.0
[node name="Sprite2D" parent="." index="0"]
texture = ExtResource("2_gscll")

View File

@@ -0,0 +1,60 @@
class_name EnemySlimeSmall
extends EnemyBase
@export var color: Color = Color.CHARTREUSE
@onready var disabled_sprite: Sprite2D = $Sprite2D
@onready var base_sprite: Sprite2D = $Sprite/BaseSprite
var shader = preload("res://assets/shaders/base_color_tint.gdshader")
var shader_material: ShaderMaterial
const NAME_PREFIXES: Array[String] = [
"Gl",
"Sl",
"Bl",
"Pl",
"Dr",
"Go",
"Glo",
"Blo",
]
const NAME_ROOTS: Array[String] = [
"op",
"ub",
"ooz",
"ump",
"ibble",
"ug",
"urp",
"lop",
]
const NAME_SUFFIXES: Array[String] = [
"y",
"o",
"let",
"kin",
"ish",
"oo",
]
func _ready() -> void:
shader_material = ShaderMaterial.new()
disabled_sprite.visible = false
set_color(color)
shader_material.shader = shader
base_sprite.material = shader_material
super._ready()
func set_color(new_color: Color) -> void:
shader_material.set_shader_parameter("base_color", new_color)
func _gen_name() -> String:
return (
"%s%s%s"
% [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()]
)

View File

@@ -0,0 +1 @@
uid://c5wgwy5itcnkk

View File

@@ -0,0 +1,41 @@
[gd_scene load_steps=9 format=3 uid="uid://cjikar11hbp3b"]
[ext_resource type="PackedScene" uid="uid://b7vq8xspnlyeu" path="res://scenes/enemies/enemy_base.tscn" id="1_o6v6p"]
[ext_resource type="Script" uid="uid://c5wgwy5itcnkk" path="res://scenes/enemies/enemy_slime_small.gd" id="2_2bnvq"]
[ext_resource type="Shader" uid="uid://cf48pgfl308o3" path="res://assets/shaders/base_color_tint.gdshader" id="3_2yvgl"]
[ext_resource type="Texture2D" uid="uid://ctob5me7woid8" path="res://assets/sprites/slime_small grayscale.png" id="3_iaao1"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_daihb"]
shader = ExtResource("3_2yvgl")
shader_parameter/base_color = Color(3.46541e-06, 0.969973, 0.260278, 1)
[sub_resource type="AtlasTexture" id="AtlasTexture_2yvgl"]
atlas = ExtResource("3_iaao1")
region = Rect2(0, 15, 16, 15)
[sub_resource type="AtlasTexture" id="AtlasTexture_daihb"]
atlas = ExtResource("3_iaao1")
region = Rect2(0, 30, 16, 15)
[sub_resource type="AtlasTexture" id="AtlasTexture_wu20c"]
atlas = ExtResource("3_iaao1")
region = Rect2(0, 45, 16, 15)
[node name="EnemySlimeSmall" instance=ExtResource("1_o6v6p")]
script = ExtResource("2_2bnvq")
color = Color(0.498039, 1, 0, 1)
[node name="Sprite" type="Node2D" parent="." index="0"]
[node name="BaseSprite" type="Sprite2D" parent="Sprite" index="0"]
material = SubResource("ShaderMaterial_daihb")
texture = SubResource("AtlasTexture_2yvgl")
[node name="Shading" type="Sprite2D" parent="Sprite" index="1"]
texture = SubResource("AtlasTexture_daihb")
[node name="Highlights" type="Sprite2D" parent="Sprite" index="2"]
texture = SubResource("AtlasTexture_wu20c")
[node name="Sprite2D" parent="." index="1"]
visible = false

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=8 format=3 uid="uid://bjg50n7aab3ng"]
[gd_scene load_steps=9 format=3 uid="uid://bjg50n7aab3ng"]
[ext_resource type="Script" uid="uid://brb4ssksmtq8k" path="res://scenes/main.gd" id="1_jyhfs"]
[ext_resource type="PackedScene" uid="uid://4xha2nhf8fya" path="res://scenes/test_level.tscn" id="1_o5qli"]
@@ -7,6 +7,7 @@
[ext_resource type="PackedScene" uid="uid://dy73qrxcgrwg3" path="res://scenes/managers/enemy_manager.tscn" id="5_tbgi4"]
[ext_resource type="PackedScene" uid="uid://bbev8m5g0p3a3" path="res://scenes/pickups/pickup_magnet.tscn" id="6_tefeu"]
[ext_resource type="PackedScene" uid="uid://cr8gj1dlloamp" path="res://scenes/pickups/pickup_hp.tscn" id="7_o6xl0"]
[ext_resource type="PackedScene" uid="uid://c2o1lpm4pimpr" path="res://scenes/managers/projectile_manager.tscn" id="8_tipki"]
[node name="Main" type="Node2D"]
script = ExtResource("1_jyhfs")
@@ -19,16 +20,20 @@ script = ExtResource("1_jyhfs")
zoom = Vector2(2, 2)
process_callback = 0
[node name="EnemyManager" parent="." node_paths=PackedStringArray("target", "camera") instance=ExtResource("5_tbgi4")]
spawn_rate = 1.5
target = NodePath("../Player")
camera = NodePath("../MainCamera")
[node name="Player" parent="." node_paths=PackedStringArray("camera", "main_ui") instance=ExtResource("2_0wfyh")]
position = Vector2(1057, 798)
camera = NodePath("../MainCamera")
main_ui = NodePath("../MainUI")
[node name="EnemyManager" parent="." node_paths=PackedStringArray("target") instance=ExtResource("5_tbgi4")]
target = NodePath("../Player")
[node name="PickupMagnet" parent="." instance=ExtResource("6_tefeu")]
position = Vector2(1697, 414)
[node name="PickupHP" parent="." instance=ExtResource("7_o6xl0")]
position = Vector2(1678, 939)
[node name="ProjectileManager" parent="." instance=ExtResource("8_tipki")]

19
scenes/main_menu.gd Normal file
View File

@@ -0,0 +1,19 @@
extends Control
const MAIN = preload("res://scenes/main.tscn")
func _ready() -> void:
pass
func _on_new_game_button_pressed() -> void:
get_tree().change_scene_to_packed(MAIN)
func _on_options_button_pressed() -> void:
pass # Replace with function body.
func _on_exit_game_button_pressed() -> void:
get_tree().quit()

1
scenes/main_menu.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://sd158y3mdmkt

69
scenes/main_menu.tscn Normal file
View File

@@ -0,0 +1,69 @@
[gd_scene load_steps=4 format=3 uid="uid://cynet50emve6c"]
[ext_resource type="Script" uid="uid://sd158y3mdmkt" path="res://scenes/main_menu.gd" id="1_l6cm7"]
[sub_resource type="Gradient" id="Gradient_vue75"]
colors = PackedColorArray(0.252028, 0.252028, 0.252028, 1, 0.25098, 0.25098, 0.25098, 1)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_l6cm7"]
gradient = SubResource("Gradient_vue75")
[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_l6cm7")
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = SubResource("GradientTexture1D_l6cm7")
expand_mode = 2
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -20.0
offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="NewGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "New game"
[node name="OptionsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Options"
[node name="ExitGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Exit game"
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/NewGameButton" to="." method="_on_new_game_button_pressed"]
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/ExitGameButton" to="." method="_on_exit_game_button_pressed"]

View File

@@ -4,30 +4,114 @@ extends Node2D
@export var max_enemies: int
@export var spawn_rate: float
@export var target: CollisionObject2D
@export var camera: Camera2D
@onready var timer: Timer = $Timer
var enemy_scene = preload("res://scenes/enemies/enemy.tscn")
const ENEMY_RAT = preload("res://scenes/enemies/enemy_rat.tscn")
const ENEMY_BAT = preload("res://scenes/enemies/enemy_bat.tscn")
const ENEMY_SLIME_SMALL = preload("res://scenes/enemies/enemy_slime_small.tscn")
const SLIME_COLOR_VARIATIONS: Array[Color] = [Color.CHARTREUSE, Color.FUCHSIA, Color.DARK_ORANGE]
var _elapsed_time: float = 0.0
func _ready() -> void:
timer.wait_time = spawn_rate / 1
timer.wait_time = 1 / spawn_rate
timer.start()
GlobalConst.sig_stop_spawning.connect(_on_stop_spawning)
GlobalConst.sig_set_spawn_rate.connect(_on_set_spawn_rate)
func _physics_process(delta: float) -> void:
_elapsed_time += delta
func _on_timer_timeout() -> void:
_on_set_spawn_rate(1.0 + (_elapsed_time / 60.0) ** 2)
var enemies = get_tree().get_nodes_in_group(GlobalConst.GROUP_ENEMY)
GlobalConst.sig_debug_stats_set.emit("enemy_count", "%s" % len(enemies))
var next_enemy: PackedScene
match randi() % 3:
0:
next_enemy = ENEMY_BAT
1:
next_enemy = ENEMY_RAT
2:
next_enemy = ENEMY_SLIME_SMALL
if len(enemies) < max_enemies:
var new_enemy = enemy_scene.instantiate()
new_enemy.position = target.position + Vector2(50, 50)
var new_enemy = next_enemy.instantiate()
new_enemy.position = _get_spawn_pos()
new_enemy.target = target
if randf() < 0.1:
new_enemy.enemy_rarity = GlobalConst.Rarity.RARE
if randf() < 0.1:
new_enemy.enemy_rarity = GlobalConst.Rarity.EPIC
if is_instance_of(new_enemy, EnemySlimeSmall):
var slime_color: Color = SLIME_COLOR_VARIATIONS.pick_random()
new_enemy.color = slime_color
add_child(new_enemy)
func _get_spawn_pos() -> Vector2:
var rect = _get_camera_rect()
var side = randi() % 4
var margin = 50
var pos: Vector2
match side:
0: # Top
pos = Vector2(
randf_range(rect.position.x, rect.position.x + rect.size.x),
rect.position.y - margin
)
1: # Bottom
pos = Vector2(
randf_range(rect.position.x, rect.position.x + rect.size.x),
rect.position.y + rect.size.y + margin
)
2: # Left
pos = Vector2(
rect.position.x - margin,
randf_range(rect.position.y, rect.position.y + rect.size.y)
)
3: # Right
pos = Vector2(
rect.position.x + rect.size.x + margin,
randf_range(rect.position.y, rect.position.y + rect.size.y)
)
if !_is_pos_valid(pos):
pos = _get_spawn_pos()
return pos
func _is_pos_valid(pos: Vector2) -> bool:
var space = get_world_2d().direct_space_state
var parameters = PhysicsPointQueryParameters2D.new()
parameters.collide_with_areas = false
parameters.collide_with_bodies = true
parameters.collision_mask = 1
parameters.position = pos
var result = space.intersect_point(parameters, 1)
return result.size() == 0
func _get_camera_rect() -> Rect2:
var viewport_size = camera.get_viewport_rect().size
var half_size = viewport_size * 0.5
var top_left = camera.global_position - half_size
return Rect2(top_left, viewport_size)
func _on_stop_spawning(val: bool):
if val:
timer.stop()
elif timer.is_stopped():
timer.start()
func _on_set_spawn_rate(val: float):
timer.stop()
timer.wait_time = 1.0 / val
timer.start()

View File

@@ -4,7 +4,7 @@
[node name="EnemyManager" type="Node2D"]
script = ExtResource("1_tfsap")
max_enemies = 50
max_enemies = 500
spawn_rate = 1.0
[node name="Timer" type="Timer" parent="."]

View File

@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://c2o1lpm4pimpr"]
[node name="ProjectileManager" type="Node2D"]

View File

@@ -75,3 +75,7 @@ func update_debug_stats() -> void:
func _on_time_scale_slider_value_changed(value: float) -> void:
Engine.time_scale = value
func _on_spawn_rate_value_changed(value: float) -> void:
GlobalConst.sig_set_spawn_rate.emit(value)

View File

@@ -13,19 +13,18 @@ signal lvlup_picked(mod: PlayerStatsModifier)
func _ready() -> void:
match mod.rarity:
GlobalConst.ModRarity.NORMAL:
GlobalConst.Rarity.NORMAL:
upgrade_name.add_theme_color_override("font_color", Color.WHITE)
GlobalConst.ModRarity.RARE:
GlobalConst.Rarity.RARE:
upgrade_name.add_theme_color_override("font_color", Color.DODGER_BLUE)
GlobalConst.ModRarity.EPIC:
GlobalConst.Rarity.EPIC:
upgrade_name.add_theme_color_override("font_color", Color.DARK_ORCHID)
GlobalConst.ModRarity.LEGENDARY:
GlobalConst.Rarity.LEGENDARY:
upgrade_name.add_theme_color_override("font_color", Color.DARK_ORANGE)
upgrade_name.text = mod.title
upgrade_description.text = mod.description
upgrade_tex.texture = mod.tex
print_debug("tex: %s" % mod.tex)
func _on_pick_button_pressed() -> void:

View File

@@ -5,9 +5,34 @@ extends Control
@onready var player_ui: PlayerUI = $CanvasLayer/PlayerUI
@onready var debug_ui: DebugUI = $CanvasLayer/DebugUI
@onready var level_up_ui: LevelUpUI = $CanvasLayer/LevelUpUI
@onready var low_hp_indicator: ColorRect = $CanvasLayer/LowHPIndicator
var elapsed_time: float
func _ready() -> void:
pause_ui.visible = false
player_ui.visible = true
level_up_ui.visible = false
low_hp_indicator.visible = false
GlobalConst.sig_on_player_hp_change.connect(_on_player_hp_change)
GlobalConst.sig_toggle_low_hp_shader.connect(_on_toggle_low_hp_shader)
func _process(delta: float) -> void:
elapsed_time += delta
if low_hp_indicator.visible:
low_hp_indicator.material.set_shader_parameter("time", elapsed_time)
func _on_player_hp_change(hp: float, max_hp: float):
var percent_hp = hp / max_hp * 100
if percent_hp < 40:
low_hp_indicator.visible = true
low_hp_indicator.material.set_shader_parameter("damage_amount", 1 - (percent_hp / 100))
else:
low_hp_indicator.visible = false
func _on_toggle_low_hp_shader(toggled_on: bool):
low_hp_indicator.visible = !low_hp_indicator.visible

View File

@@ -1,11 +1,19 @@
[gd_scene load_steps=6 format=3 uid="uid://b18uib08hvdpq"]
[gd_scene load_steps=8 format=3 uid="uid://b18uib08hvdpq"]
[ext_resource type="Script" uid="uid://dcxc70fvu7kl2" path="res://scenes/managers/ui/main_ui.gd" id="1_3a826"]
[ext_resource type="Script" uid="uid://sjnxf0hj3egp" path="res://scenes/managers/ui/pause_ui.gd" id="1_lke1m"]
[ext_resource type="Shader" uid="uid://dnwiwj1ak1bu5" path="res://assets/shaders/screen_low_hp.gdshader" id="2_lcbsd"]
[ext_resource type="Script" uid="uid://dbq74tvxtpfjc" path="res://scenes/managers/ui/player_ui.gd" id="3_gaipe"]
[ext_resource type="Script" uid="uid://d2o6tqnqg2o25" path="res://scenes/managers/ui/debug_ui.gd" id="4_217l8"]
[ext_resource type="PackedScene" uid="uid://isg7vt4l7eem" path="res://scenes/managers/ui/level_up_ui.tscn" id="5_cfhdr"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_cg7ol"]
shader = ExtResource("2_lcbsd")
shader_parameter/damage_amount = 0.5
shader_parameter/noise_strength = 0.2
shader_parameter/pulse_strength = 0.05
shader_parameter/time = 0.0
[node name="MainUI" type="Control"]
layout_mode = 3
anchors_preset = 15
@@ -17,6 +25,14 @@ script = ExtResource("1_3a826")
[node name="CanvasLayer" type="CanvasLayer" parent="."]
[node name="LowHPIndicator" type="ColorRect" parent="CanvasLayer"]
material = SubResource("ShaderMaterial_cg7ol")
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="PauseUI" type="Control" parent="CanvasLayer"]
layout_mode = 3
anchors_preset = 15
@@ -84,10 +100,13 @@ offset_bottom = 100.0
[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/PlayerUI/CenterContainer"]
layout_mode = 2
[node name="ProgressBar" type="ProgressBar" parent="CanvasLayer/PlayerUI/CenterContainer/VBoxContainer"]
[node name="HPBar" type="ProgressBar" parent="CanvasLayer/PlayerUI/CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
[node name="XPBar" type="ProgressBar" parent="CanvasLayer/PlayerUI/CenterContainer/VBoxContainer"]
layout_mode = 2
[node name="ElapsedLabel" type="Label" parent="CanvasLayer/PlayerUI/CenterContainer/VBoxContainer"]
layout_mode = 2
text = "10:00:00"
@@ -151,6 +170,27 @@ max_value = 1.0
step = 0.1
value = 1.0
[node name="MarginContainer2" type="MarginContainer" parent="CanvasLayer/DebugUI/PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/margin_left = 6
theme_override_constants/margin_right = 6
[node name="HBoxContainer" type="HBoxContainer" parent="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/MarginContainer2"]
layout_mode = 2
[node name="Label" type="Label" parent="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/MarginContainer2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Spawn rate"
[node name="SpawnRate" type="HSlider" parent="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/MarginContainer2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
max_value = 10.0
step = 0.1
value = 1.0
[node name="StatsContainer" type="PanelContainer" parent="CanvasLayer/DebugUI"]
layout_mode = 0
offset_left = 938.0
@@ -173,3 +213,4 @@ layout_mode = 2
[connection signal="toggled" from="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/EnemyGodModeCheck" to="CanvasLayer/DebugUI" method="_on_enemy_god_mode_check_toggled"]
[connection signal="toggled" from="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/StatsCheck" to="CanvasLayer/DebugUI" method="_on_stats_check_toggled"]
[connection signal="value_changed" from="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/MarginContainer/HBoxContainer/TimeScaleSlider" to="CanvasLayer/DebugUI" method="_on_time_scale_slider_value_changed"]
[connection signal="value_changed" from="CanvasLayer/DebugUI/PanelContainer/VBoxContainer/MarginContainer2/HBoxContainer/SpawnRate" to="CanvasLayer/DebugUI" method="_on_spawn_rate_value_changed"]

View File

@@ -1,7 +1,9 @@
class_name PlayerUI
extends Control
@onready var hp_bar: ProgressBar = $CenterContainer/VBoxContainer/ProgressBar
@onready var hp_bar: ProgressBar = $CenterContainer/VBoxContainer/HPBar
@onready var xp_bar: ProgressBar = $CenterContainer/VBoxContainer/XPBar
@onready var elapsed_label: Label = $CenterContainer/VBoxContainer/ElapsedLabel
const ANIM_SPEED = 4.0
@@ -9,7 +11,8 @@ const TRESHOLD = 1
var wanted_hp_value: float
var wanted_hp_max: float
var wanted_xp_value: float
var wanted_xp_max: float
var player: Player
@@ -18,6 +21,11 @@ func _ready() -> void:
if player:
hp_bar.value = player.player_stats.current_health
hp_bar.max_value = player.player_stats.max_health
xp_bar.value = player.player_stats.current_xp
xp_bar.max_value = player.player_stats.xp_required_for_level()
await get_tree().create_timer(0.5).timeout
update_hp()
update_xp()
func update_hp():
@@ -29,19 +37,42 @@ func update_hp():
wanted_hp_max = max_hp
func update_xp():
if not player:
push_error("cant update xp, no player found")
var current_xp = player.player_stats.get_final("current_xp", player.modifiers)
var max_xp = player.player_stats.xp_required_for_level()
wanted_xp_value = current_xp
wanted_xp_max = max_xp
func _process(delta: float) -> void:
update_hp_bar(delta)
update_xp_bar(delta)
func update_hp_bar(delta: float):
if wanted_hp_max != hp_bar.max_value:
print_debug("wanted: %f-%f" % [wanted_hp_max, hp_bar.max_value])
hp_bar.max_value = lerpf(hp_bar.max_value, wanted_hp_max, ANIM_SPEED * delta)
if abs(hp_bar.max_value - wanted_hp_max) < TRESHOLD:
hp_bar.max_value = wanted_hp_max
if wanted_hp_value != hp_bar.value:
print_debug("current: %f-%f" % [wanted_hp_value, hp_bar.value])
hp_bar.value = lerpf(hp_bar.value, wanted_hp_value, ANIM_SPEED * delta)
if abs(hp_bar.value - wanted_hp_value) < TRESHOLD:
hp_bar.value = wanted_hp_value
func update_xp_bar(delta: float):
if wanted_xp_max != xp_bar.max_value:
xp_bar.max_value = lerpf(xp_bar.max_value, wanted_xp_max, ANIM_SPEED * delta)
if abs(xp_bar.max_value - wanted_xp_max) < TRESHOLD:
xp_bar.max_value = wanted_xp_max
if wanted_xp_value != xp_bar.value:
xp_bar.value = lerpf(xp_bar.value, wanted_xp_value, ANIM_SPEED * delta)
if abs(xp_bar.value - wanted_xp_value) < TRESHOLD:
xp_bar.value = wanted_xp_value
func set_elapsed_time(value: float):
elapsed_label.text = format_time(value)

View File

@@ -16,7 +16,7 @@ func pickup() -> void:
mod.title = "Magnet"
mod.tex = GlobalConst.placeholder_tex
mod.internal_name = "magnet"
mod.rarity = GlobalConst.ModRarity.RARE
mod.rarity = GlobalConst.Rarity.RARE
mod.stat_name = "pickup_radius"
mod.value = 5000.0
mod.type = PlayerStatsModifier.ModifierType.ABSOLUTE

View File

@@ -1,9 +1,13 @@
class_name Player
extends CharacterBody2D
const WEAPON_SWORD = preload("res://scenes/weapons/weapon_sword.tscn")
@export var camera: Camera2D
@export var main_ui: MainUI
var weapon: WeaponBase
@onready var sprite_2d: Sprite2D = $Sprite2D
var player_stats: PlayerStats = PlayerStats.new()
@@ -13,10 +17,18 @@ var death_anim_done: bool = false
var god_mode: bool = false
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("active"):
weapon.do_active()
func _ready() -> void:
camera.position = global_position
main_ui.player_ui.update_hp()
GlobalConst.sig_debug_god_mode.connect(toggle_god_mode)
if not weapon:
weapon = WEAPON_SWORD.instantiate()
add_child(weapon)
func _physics_process(delta: float) -> void:
@@ -51,6 +63,7 @@ func take_damage(value: float) -> void:
dm.damage_taken = value
dm.player_damage = false
add_child(dm)
GlobalConst.sig_on_player_hp_change.emit(current_hp, max_hp)
main_ui.player_ui.update_hp()
if player_stats.current_health <= 0:
die()
@@ -59,7 +72,7 @@ func take_damage(value: float) -> void:
func die():
dead = true
remove_from_group("damagable")
get_tree().call_group("enemy", "cheer_anim")
get_tree().call_group("enemy", "cheer")
GlobalConst.sig_stop_spawning.emit(true)
sprite_2d.z_index += 10
get_taunted()
@@ -75,18 +88,16 @@ func death_animation(delta: float):
func get_taunted():
var taunting_enemies: Array[Enemy] = []
var taunting_enemies: Array[EnemyBase] = []
for body in get_tree().get_nodes_in_group(GlobalConst.GROUP_ENEMY):
if global_position.distance_to(body.global_position) < 500.0:
if global_position.distance_to(body.global_position) < 1000.0:
taunting_enemies.append(body)
for i in range(taunting_enemies.size()):
var angle = (TAU / taunting_enemies.size()) * i
var target_pos = Vector2(cos(angle), sin(angle)) * 50
var new_target = StaticBody2D.new()
var collision = CollisionShape2D.new()
var new_target = Marker2D.new()
new_target.position = target_pos
add_child(new_target)
new_target.add_child(collision)
taunting_enemies[i].target = new_target
@@ -96,11 +107,12 @@ func toggle_god_mode(value: bool):
func add_xp(amount: float) -> void:
player_stats.current_xp += amount
main_ui.player_ui.update_xp()
_check_level_up()
func _check_level_up() -> void:
var required_for_level = 25.0
var required_for_level = player_stats.xp_required_for_level()
if player_stats.current_xp >= required_for_level:
player_stats.current_level += 1
player_stats.current_xp -= required_for_level

View File

@@ -1,8 +1,7 @@
[gd_scene load_steps=6 format=3 uid="uid://ca2so8fm3q8fe"]
[gd_scene load_steps=5 format=3 uid="uid://ca2so8fm3q8fe"]
[ext_resource type="Texture2D" uid="uid://5x5wimok8uw2" path="res://assets/sprites/roguelikeChar_transparent.png" id="1_3vyb7"]
[ext_resource type="Script" uid="uid://cvqaxckx4num3" path="res://scenes/player.gd" id="1_g2els"]
[ext_resource type="PackedScene" uid="uid://cdojqe2m4kxx1" path="res://scenes/attacks/attack_sword.tscn" id="3_qhqgy"]
[sub_resource type="CircleShape2D" id="CircleShape2D_3vyb7"]
radius = 8.0
@@ -22,9 +21,6 @@ region_rect = Rect2(0, 104, 16, 14)
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_3vyb7")
[node name="AttackSword" parent="." node_paths=PackedStringArray("player") instance=ExtResource("3_qhqgy")]
player = NodePath("..")
[node name="PickupArea" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 24

View File

@@ -28,4 +28,4 @@ func get_final(stat: String, modifiers: Array[PlayerStatsModifier]) -> Variant:
func xp_required_for_level() -> float:
return 100 * (100 * (1.5 ** (current_level - 1)))
return floorf(100 * (1.5 ** (current_level - 1)))

View File

@@ -8,6 +8,6 @@ var value: Variant
var type: ModifierType = ModifierType.ADDITIVE
var internal_name: String
var title: String
var rarity: GlobalConst.ModRarity
var rarity: GlobalConst.Rarity
var tex: Texture2D
var description: String

22
scenes/puddle.gd Normal file
View File

@@ -0,0 +1,22 @@
class_name Puddle
extends Node2D
@export var color: Color = Color.CRIMSON
@onready var base: Sprite2D = $Base
func _ready() -> void:
var player: Player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
var shader = preload("res://assets/shaders/base_color_tint.gdshader")
var shader_material: ShaderMaterial
shader_material = ShaderMaterial.new()
shader_material.set_shader_parameter("base_color", color)
shader_material.shader = shader
base.material = shader_material
match randi() % 4:
1:
rotation_degrees = 90
2:
rotation_degrees = 180
3:
rotation_degrees = -90

1
scenes/puddle.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://cds5aqq2mqmpe

34
scenes/puddle.tscn Normal file
View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=8 format=3 uid="uid://dcka1mmlgeoj7"]
[ext_resource type="Shader" uid="uid://cf48pgfl308o3" path="res://assets/shaders/base_color_tint.gdshader" id="1_qlq6d"]
[ext_resource type="Script" uid="uid://cds5aqq2mqmpe" path="res://scenes/puddle.gd" id="1_s8xbh"]
[ext_resource type="Texture2D" uid="uid://c5t4it4if0s6g" path="res://assets/sprites/puddle_1.png" id="2_s8xbh"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_i64cl"]
shader = ExtResource("1_qlq6d")
shader_parameter/base_color = Color(1, 0.2, 0.2, 1)
[sub_resource type="AtlasTexture" id="AtlasTexture_h42jt"]
atlas = ExtResource("2_s8xbh")
region = Rect2(0, 0, 16, 13)
[sub_resource type="AtlasTexture" id="AtlasTexture_16dvh"]
atlas = ExtResource("2_s8xbh")
region = Rect2(32, 0, 16, 13)
[sub_resource type="AtlasTexture" id="AtlasTexture_5nuq4"]
atlas = ExtResource("2_s8xbh")
region = Rect2(16, 0, 16, 13)
[node name="Puddle" type="Node2D"]
script = ExtResource("1_s8xbh")
[node name="Base" type="Sprite2D" parent="."]
material = SubResource("ShaderMaterial_i64cl")
texture = SubResource("AtlasTexture_h42jt")
[node name="Highlights" type="Sprite2D" parent="."]
texture = SubResource("AtlasTexture_16dvh")
[node name="Shading" type="Sprite2D" parent="."]
texture = SubResource("AtlasTexture_5nuq4")

View File

@@ -1,4 +1,4 @@
[gd_resource type="TileSet" load_steps=8 format=3 uid="uid://c3clgitssvdlg"]
[gd_resource type="TileSet" load_steps=9 format=3 uid="uid://c3clgitssvdlg"]
[ext_resource type="Texture2D" uid="uid://2xgqhe7u6lfq" path="res://assets/sprites/roguelikeSheet_transparent.png" id="1_kkifh"]
[ext_resource type="Shader" uid="uid://clpp23h7fpamt" path="res://assets/shaders/water2.gdshader" id="1_n3s2j"]
@@ -26,6 +26,12 @@ shader_parameter/noise_tex = SubResource("NoiseTexture2D_m3ddo")
shader_parameter/noise_speed = Vector2(0.03, 0.01)
shader_parameter/noise_strength = 0.05
[sub_resource type="NavigationPolygon" id="NavigationPolygon_n3s2j"]
vertices = PackedVector2Array(8, 8, -8, 8, -8, -8, 8, -8)
polygons = Array[PackedInt32Array]([PackedInt32Array(0, 1, 2, 3)])
outlines = Array[PackedVector2Array]([PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)])
agent_radius = 0.0
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_s7nh2"]
texture = ExtResource("1_kkifh")
separation = Vector2i(1, 1)
@@ -74,10 +80,15 @@ separation = Vector2i(1, 1)
4:0/0/terrains_peering_bit/top_right_corner = 0
5:0/0 = 0
5:0/0/terrain_set = 0
5:0/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:0/0 = 0
6:0/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:0/0 = 0
7:0/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:0/0 = 0
8:0/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:0/0 = 0
9:0/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:0/0 = 0
11:0/0 = 0
12:0/0 = 0
@@ -197,10 +208,15 @@ separation = Vector2i(1, 1)
5:1/0/terrains_peering_bit/top_left_corner = 0
5:1/0/terrains_peering_bit/top_side = 0
5:1/0/terrains_peering_bit/top_right_corner = 0
5:1/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:1/0 = 0
6:1/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:1/0 = 0
7:1/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:1/0 = 0
8:1/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:1/0 = 0
9:1/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:1/0 = 0
11:1/0 = 0
12:1/0 = 0
@@ -311,10 +327,15 @@ separation = Vector2i(1, 1)
4:2/0/terrains_peering_bit/top_side = 1
4:2/0/terrains_peering_bit/top_right_corner = 0
5:2/0 = 0
5:2/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:2/0 = 0
6:2/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:2/0 = 0
7:2/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:2/0 = 0
8:2/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:2/0 = 0
9:2/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:2/0 = 0
11:2/0 = 0
12:2/0 = 0
@@ -368,10 +389,15 @@ separation = Vector2i(1, 1)
3:3/0 = 0
4:3/0 = 0
5:3/0 = 0
5:3/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:3/0 = 0
6:3/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:3/0 = 0
7:3/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:3/0 = 0
8:3/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:3/0 = 0
9:3/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:3/0 = 0
11:3/0 = 0
12:3/0 = 0
@@ -425,10 +451,15 @@ separation = Vector2i(1, 1)
3:4/0 = 0
4:4/0 = 0
5:4/0 = 0
5:4/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:4/0 = 0
6:4/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:4/0 = 0
7:4/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:4/0 = 0
8:4/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:4/0 = 0
9:4/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:4/0 = 0
11:4/0 = 0
12:4/0 = 0
@@ -482,10 +513,15 @@ separation = Vector2i(1, 1)
3:5/0 = 0
4:5/0 = 0
5:5/0 = 0
5:5/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
6:5/0 = 0
6:5/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
7:5/0 = 0
7:5/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
8:5/0 = 0
8:5/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
9:5/0 = 0
9:5/0/navigation_layer_0/polygon = SubResource("NavigationPolygon_n3s2j")
10:5/0 = 0
11:5/0 = 0
12:5/0 = 0
@@ -1965,5 +2001,6 @@ terrain_set_0/terrain_0/name = "Grass"
terrain_set_0/terrain_0/color = Color(0.5, 0.34375, 0.25, 1)
terrain_set_0/terrain_1/name = "Water"
terrain_set_0/terrain_1/color = Color(0.5, 0.4375, 0.25, 1)
navigation_layer_0/layers = 1
sources/0 = SubResource("TileSetAtlasSource_s7nh2")
pattern_0 = SubResource("TileMapPattern_beh8d")

View File

@@ -0,0 +1,17 @@
class_name KeyedCache
extends Resource
var cache = {}
func get_or_compute(key: String, compute_func: Callable):
if key in cache:
return cache["key"]
var value = compute_func.call()
cache["key"] = value
return value
func invalidate_key(key: String):
cache.erase(key)

View File

@@ -0,0 +1 @@
uid://bbshyok4m8nq3

View File

@@ -0,0 +1,109 @@
class_name WeaponBase
extends Node2D
enum WeaponTag {
CAN_RETURN,
CAN_BLEED,
CAN_CHAIN,
CAN_FORK,
}
@export var attack_cd: float
@export var attack_damage: float
@export var attack_aoe: float
@export var attack_duration: float
@export var attack_range: float
@export var attack_crit_chance: float = 0.05
@export var return_chance: float = 0.0
@export var bleed_chance: float = 0.0
@export var bleed_duration: float = 5.0
@export var chain_chance: float = 0.0
@export var tags: Array[String] = []
@export var modifiers: Array[WeaponModBase] = []
@onready var active_cd_timer: Timer = $ActiveCDTimer
var _player: Player
func _ready() -> void:
_player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
func _on_attack_cd_timer_timeout() -> void:
do_attack()
func do_attack() -> void:
push_error("%s does not implement do_attack" % self)
func do_active() -> void:
if not active_cd_timer.is_stopped():
return
active_cd_timer.start()
_do_active()
func add_mod(mod: WeaponModBase) -> void:
modifiers.append(mod)
func _do_active() -> void:
push_error("%s does not implement do_active" % self)
func find_target_in_radius() -> EnemyBase:
var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
var shape := CircleShape2D.new()
shape.radius = attack_range
var query := PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = Transform2D(0, global_position)
query.collision_mask = 2
query.collide_with_bodies = true
var results := space_state.intersect_shape(query)
if len(results) < 1:
return null
var closest: PhysicsBody2D = results[0]["collider"]
for r in results:
var c: PhysicsBody2D = r["collider"]
if not c.is_in_group(GlobalConst.GROUP_ENEMY):
continue
if (
c.global_position.distance_to(global_position)
< closest.global_position.distance_to(global_position)
):
closest = c
return closest
func get_calculated(key: String) -> Variant:
return WeaponModBase.get_calculated(self, key)
func has_property(key: String) -> bool:
for prop in get_property_list():
if prop.name == key:
return true
return false
func did_crit() -> bool:
var weapon_crit = get_calculated("attack_crit_chance")
var player_crit = _player.player_stats.get_final("crit_chance", _player.modifiers)
return randf() >= 1 - weapon_crit + player_crit
func did_bleed() -> bool:
return randf() >= 1 - bleed_chance
func base_damage() -> Array[Variant]:
var damage = get_calculated("attack_damage")
var is_crit := did_crit()
if is_crit:
damage *= _player.player_stats.get_final("crit_multiplier", _player.modifiers)
return [damage, is_crit]

View File

@@ -0,0 +1 @@
uid://d6nwfhyethdw

View File

@@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3 uid="uid://i8mtky2req41"]
[ext_resource type="Script" uid="uid://d6nwfhyethdw" path="res://scenes/weapons/weapon_base.gd" id="1_v4xn6"]
[node name="WeaponBase" type="Node2D"]
script = ExtResource("1_v4xn6")
[node name="AttackCDTimer" type="Timer" parent="."]
autostart = true
[node name="ActiveCDTimer" type="Timer" parent="."]
one_shot = true
[connection signal="timeout" from="AttackCDTimer" to="." method="_on_attack_cd_timer_timeout"]

View File

@@ -0,0 +1,36 @@
class_name WeaponModBase
extends Resource
enum ModType { ADDITIVE, MULTIPLICATIVE, ABSOLUTE, BOOL }
@export var name: String = "Unnamed mod"
@export var description: String = ""
@export var icon: Texture2D
@export var mod_property: String
@export var mod_value: float
@export var mod_value_bool: bool = false
@export var mod_type: ModType = ModType.MULTIPLICATIVE
@export var mod_extra_tags: Array[WeaponBase.WeaponTag] = []
static func get_calculated(weapon: WeaponBase, key: String) -> Variant:
assert(
weapon.has_property(key),
"tried calculate property '%s' where base value does not exist on %s" % [key, weapon]
)
var base_value = weapon.get(key)
var add = 0.0
var mul = 1.0
for mod in weapon.modifiers:
if mod.mod_property == key:
match mod.mod_type:
ModType.ADDITIVE:
add += mod.mod_value
ModType.MULTIPLICATIVE:
mul *= mod.mod_value
ModType.ABSOLUTE:
return mod.mod_value
ModType.BOOL:
return mod.mod_value_bool
return (base_value + add) * mul

View File

@@ -0,0 +1 @@
uid://b1cg1bhnt6xk4

View File

@@ -0,0 +1,56 @@
class_name WeaponSword
extends WeaponBase
const WEAPON_SWORD_PROJECTILE = preload("res://scenes/weapons/weapon_sword_projectile.tscn")
@onready var targeting_range: Area2D = $TargetingRange
@onready var targeting_range_shape: CollisionShape2D = $TargetingRange/CollisionShape2D
signal projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase)
func _ready() -> void:
bleed_chance = 0.2
targeting_range_shape.shape.radius = attack_range
projectile_hit.connect(_on_projectile_hit)
super._ready()
func do_attack() -> void:
var target: EnemyBase = find_target_in_radius()
if not target:
return
var projectile = WEAPON_SWORD_PROJECTILE.instantiate()
projectile.target = target
projectile.on_hit_sig = projectile_hit
add_child(projectile)
func _do_active() -> void:
var radius = targeting_range_shape.shape.radius
var count = 15
for i in count:
var angle = TAU * float(i) / float(count)
var target_pos = Vector2(cos(angle), sin(angle)) * radius
var new_target := Marker2D.new()
new_target.global_position = global_position + target_pos
var projectile = WEAPON_SWORD_PROJECTILE.instantiate()
projectile.damage_mult = 3.0
projectile.target = new_target
projectile.on_hit_sig = projectile_hit
add_child(projectile)
func deal_damage(enemy: EnemyBase, damage_mult: float):
var damage_and_crit = base_damage()
# TODO: Fix crit value
enemy.take_damage(damage_and_crit[0], damage_and_crit[1])
if did_bleed():
var bleed = EnemyEffectBleed.new(enemy, damage_and_crit[0], bleed_duration)
bleed.apply(enemy)
func _on_projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase, damage_mult: float):
deal_damage(enemy, damage_mult)

View File

@@ -0,0 +1 @@
uid://b072d866r4usq

View File

@@ -0,0 +1,22 @@
[gd_scene load_steps=4 format=3 uid="uid://dfikvj27k01tu"]
[ext_resource type="PackedScene" uid="uid://i8mtky2req41" path="res://scenes/weapons/weapon_base.tscn" id="1_2dti5"]
[ext_resource type="Script" uid="uid://b072d866r4usq" path="res://scenes/weapons/weapon_sword.gd" id="2_ruf80"]
[sub_resource type="CircleShape2D" id="CircleShape2D_ruf80"]
[node name="WeaponSword" instance=ExtResource("1_2dti5")]
script = ExtResource("2_ruf80")
attack_cd = 1.0
attack_damage = 5.0
attack_aoe = 1.0
attack_duration = 1.0
attack_range = 150.0
[node name="TargetingRange" type="Area2D" parent="." index="1"]
collision_layer = 0
collision_mask = 2
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="TargetingRange" index="0"]
shape = SubResource("CircleShape2D_ruf80")

View File

@@ -0,0 +1,41 @@
class_name WeaponSwordProjectile
extends Node2D
@export var speed: float = 500.0
@export var range: float = 200.0
@export var target: Node2D
@export var damage_mult: float = 1.0
@export var bleed_chance: float = 0.2
@export var on_hit_sig: Signal
var _direction: Vector2
var _traveled: float = 0.0
var _already_hit: Array[PhysicsBody2D] = []
func _ready() -> void:
_direction = global_position.direction_to(target.global_position).normalized()
_traveled = 0.0
rotation = _direction.angle()
if _direction.x < 0:
rotation += PI
var proj_manager = get_tree().get_first_node_in_group(GlobalConst.GROUP_PROJ_MANAGER)
if not proj_manager:
return
reparent(proj_manager, true)
func _physics_process(delta: float) -> void:
var step = _direction * speed * delta
global_position += step
_traveled += step.length()
if _traveled >= range:
queue_free()
func _on_area_2d_body_entered(body: Node2D) -> void:
if body in _already_hit:
return
on_hit_sig.emit(self, body, damage_mult)
_already_hit.append(body)

View File

@@ -0,0 +1 @@
uid://c40iaqdubwl0p

View File

@@ -0,0 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://bv5f47x3ishmf"]
[ext_resource type="Script" uid="uid://c40iaqdubwl0p" path="res://scenes/weapons/weapon_sword_projectile.gd" id="1_asuu4"]
[ext_resource type="Texture2D" uid="uid://dycw7c3484dir" path="res://assets/sprites/sword.png" id="2_pxap4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ygc1t"]
size = Vector2(16, 46)
[node name="WeaponSwordProjectile" type="Node2D"]
script = ExtResource("1_asuu4")
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("2_pxap4")
[node name="Area2D" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 2
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2(0, -5)
shape = SubResource("RectangleShape2D_ygc1t")
[connection signal="body_entered" from="Area2D" to="." method="_on_area_2d_body_entered"]

View File

@@ -1,17 +1,31 @@
[gd_scene load_steps=4 format=3 uid="uid://du4dqne3o0d5b"]
[gd_scene load_steps=7 format=3 uid="uid://du4dqne3o0d5b"]
[ext_resource type="Script" uid="uid://bx43us4qd2vpf" path="res://scenes/xp_orb.gd" id="1_me6n8"]
[ext_resource type="Texture2D" uid="uid://c0g255217x10l" path="res://assets/sprites/xp_orb_small.png" id="2_me6n8"]
[ext_resource type="Shader" uid="uid://ckjyt0mt0ngk" path="res://assets/shaders/xp_orb.gdshader" id="2_me6n8"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_pkphv"]
shader = ExtResource("2_me6n8")
shader_parameter/orb_color = Color(0.2, 1, 0.5, 1)
shader_parameter/glow_strength = 1.0
shader_parameter/pulse_speed = 2.0
[sub_resource type="Gradient" id="Gradient_me6n8"]
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_hluwh"]
gradient = SubResource("Gradient_me6n8")
width = 12
height = 12
[sub_resource type="CircleShape2D" id="CircleShape2D_me6n8"]
radius = 5.0
radius = 4.0
[node name="XPOrb" type="Node2D" groups=["pickup", "xp_orb"]]
script = ExtResource("1_me6n8")
[node name="Sprite2D" type="Sprite2D" parent="."]
position = Vector2(-1, 1)
texture = ExtResource("2_me6n8")
material = SubResource("ShaderMaterial_pkphv")
texture = SubResource("GradientTexture2D_hluwh")
[node name="Area2D" type="Area2D" parent="."]
collision_layer = 9