Compare commits
16 Commits
fix/tls-ve
...
aa2550cc59
| Author | SHA1 | Date | |
|---|---|---|---|
| aa2550cc59 | |||
| 29c620a9e8 | |||
| 27dc726839 | |||
| 477c299d9f | |||
| f65e8dd9ea | |||
| 84a257f9b9 | |||
| f7c498d7ad | |||
| 7080d7fefc | |||
| 69614f058c | |||
| cc582e1f0e | |||
| c44ace5804 | |||
| ec63ce6701 | |||
| 767d821534 | |||
| 59b6728ce8 | |||
| 4e3e17ced4 | |||
| bb23f2e3a1 |
@@ -19,30 +19,39 @@ jobs:
|
|||||||
git config --global http.https://git.kimchi.sslVerify false
|
git config --global http.https://git.kimchi.sslVerify false
|
||||||
git clone --depth 1 https://${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}@git.kimchi/tas/major_tom.git .
|
git clone --depth 1 https://${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}@git.kimchi/tas/major_tom.git .
|
||||||
|
|
||||||
|
- name: Debug registry auth
|
||||||
|
run: |
|
||||||
|
curl -s -D- http://git.kimchi/v2/ | head -15
|
||||||
|
TOKEN=$(curl -s "http://git.kimchi/v2/token?service=container_registry&scope=repository:tas/major_tom:push,pull" -u "${{
|
||||||
|
secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}" | jq -r .token)
|
||||||
|
echo "Token received: ${TOKEN:0:20}..."
|
||||||
|
curl -s -D- -H "Authorization: Bearer $TOKEN" http://git.kimchi/v2/tas/major_tom/tags/list | head -15
|
||||||
|
|
||||||
- name: Build and push container image
|
- name: Build and push container image
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /etc/containers
|
set -ex
|
||||||
printf '[registries.insecure]\nregistries = ["git.kimchi"]\n' > /etc/containers/registries.conf
|
|
||||||
|
|
||||||
IMAGE_TAG="${{ env.IMAGE }}:sha-${GITHUB_SHA::8}"
|
IMAGE_TAG="${{ env.IMAGE }}:sha-${GITHUB_SHA::8}"
|
||||||
IMAGE_LATEST="${{ env.IMAGE }}:latest"
|
IMAGE_LATEST="${{ env.IMAGE }}:latest"
|
||||||
|
|
||||||
buildah bud --tls-verify=false -f Containerfile -t "$IMAGE_TAG" -t "$IMAGE_LATEST" .
|
buildah bud --tls-verify=false -f Containerfile -t "$IMAGE_TAG" -t "$IMAGE_LATEST" .
|
||||||
buildah login --tls-verify=false "${{ env.REGISTRY }}" -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_PASSWORD }}"
|
|
||||||
buildah push --tls-verify=false "$IMAGE_TAG"
|
CREDS="${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}"
|
||||||
buildah push --tls-verify=false "$IMAGE_LATEST"
|
|
||||||
|
echo "=== buildah push tag ==="
|
||||||
|
buildah push --tls-verify=false --creds "$CREDS" "$IMAGE_TAG"
|
||||||
|
|
||||||
|
echo "=== buildah push latest ==="
|
||||||
|
buildah push --tls-verify=false --creds "$CREDS" "$IMAGE_LATEST"
|
||||||
|
|
||||||
echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV"
|
echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Deploy to k3s
|
- name: Restart deployment on k3s
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.kube
|
mkdir -p ~/.kube
|
||||||
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
|
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
|
||||||
chmod 600 ~/.kube/config
|
chmod 600 ~/.kube/config
|
||||||
|
|
||||||
kubectl set image deployment/${{ env.DEPLOYMENT }} \
|
POD_NAME=$(kubectl get pods -l app=jnr-web -n jnr-web -o jsonpath='{.items[0].metadata.name}')
|
||||||
${{ env.DEPLOYMENT }}="${{ env.IMAGE_TAG }}" \
|
echo "Deleting running pod" "$POD_NAME"
|
||||||
-n ${{ env.NAMESPACE }}
|
kubectl delete pod $POD_NAME -n jnr-web
|
||||||
|
|
||||||
kubectl rollout status deployment/${{ env.DEPLOYMENT }} \
|
|
||||||
-n ${{ env.NAMESPACE }} --timeout=60s
|
|
||||||
17
AGENTS.md
17
AGENTS.md
@@ -256,6 +256,23 @@ incremental progress.
|
|||||||
- Draw layers: BG → entities → FG → particles → HUD
|
- Draw layers: BG → entities → FG → particles → HUD
|
||||||
- Camera transforms world coords to screen coords
|
- Camera transforms world coords to screen coords
|
||||||
|
|
||||||
|
## Remote Git Server
|
||||||
|
|
||||||
|
The remote git server is a Gitea instance. Use the **`tea` CLI** (not `gh`) for all remote
|
||||||
|
operations: pull requests, issues, releases, and repository management.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea pr create --title "..." --description "..." # Create a pull request
|
||||||
|
tea pr list # List open PRs
|
||||||
|
tea issue list # List issues
|
||||||
|
tea issue create --title "..." --description "..."
|
||||||
|
tea pr merge <number> # Merge a PR
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT use `gh` (GitHub CLI) — it will not work with this remote.
|
||||||
|
|
||||||
|
After creating a pull request, always check out the `main` branch (`git checkout main`).
|
||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
- Imperative mood, concise
|
- Imperative mood, concise
|
||||||
- No co-authored-by or AI attribution
|
- No co-authored-by or AI attribution
|
||||||
|
|||||||
@@ -530,6 +530,124 @@ void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir) {
|
|||||||
particle_emit(&dust);
|
particle_emit(&dust);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void particle_emit_hit_sparks(Vec2 pos) {
|
||||||
|
/* Bright orange-white sparks — fast, short-lived, spray outward */
|
||||||
|
ParticleBurst sparks = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 8,
|
||||||
|
.speed_min = 60.0f,
|
||||||
|
.speed_max = 180.0f,
|
||||||
|
.life_min = 0.08f,
|
||||||
|
.life_max = 0.2f,
|
||||||
|
.size_min = 1.0f,
|
||||||
|
.size_max = 2.0f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 3.0f,
|
||||||
|
.gravity_scale = 0.4f,
|
||||||
|
.color = {255, 200, 100, 255}, /* orange-white */
|
||||||
|
.color_vary = true,
|
||||||
|
};
|
||||||
|
particle_emit(&sparks);
|
||||||
|
|
||||||
|
/* Brief white flash at impact point */
|
||||||
|
ParticleBurst flash = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 3,
|
||||||
|
.speed_min = 5.0f,
|
||||||
|
.speed_max = 15.0f,
|
||||||
|
.life_min = 0.03f,
|
||||||
|
.life_max = 0.06f,
|
||||||
|
.size_min = 2.0f,
|
||||||
|
.size_max = 3.5f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 8.0f,
|
||||||
|
.gravity_scale = 0.0f,
|
||||||
|
.color = {255, 255, 230, 255}, /* bright white */
|
||||||
|
.color_vary = false,
|
||||||
|
};
|
||||||
|
particle_emit(&flash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void particle_emit_metal_explosion(Vec2 pos) {
|
||||||
|
/* Metal shrapnel — fast grey/silver chunks flying outward */
|
||||||
|
ParticleBurst shrapnel = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 16,
|
||||||
|
.speed_min = 60.0f,
|
||||||
|
.speed_max = 200.0f,
|
||||||
|
.life_min = 0.2f,
|
||||||
|
.life_max = 0.6f,
|
||||||
|
.size_min = 1.5f,
|
||||||
|
.size_max = 3.5f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 2.0f,
|
||||||
|
.gravity_scale = 0.5f,
|
||||||
|
.color = {180, 180, 190, 255}, /* silver-grey */
|
||||||
|
.color_vary = true,
|
||||||
|
};
|
||||||
|
particle_emit(&shrapnel);
|
||||||
|
|
||||||
|
/* Hot orange sparks — electrical/mechanical innards */
|
||||||
|
ParticleBurst sparks = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 10,
|
||||||
|
.speed_min = 40.0f,
|
||||||
|
.speed_max = 150.0f,
|
||||||
|
.life_min = 0.15f,
|
||||||
|
.life_max = 0.35f,
|
||||||
|
.size_min = 1.0f,
|
||||||
|
.size_max = 2.5f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 2.5f,
|
||||||
|
.gravity_scale = 0.3f,
|
||||||
|
.color = {255, 160, 40, 255}, /* hot orange */
|
||||||
|
.color_vary = true,
|
||||||
|
};
|
||||||
|
particle_emit(&sparks);
|
||||||
|
|
||||||
|
/* Bright white flash at center — brief pop */
|
||||||
|
ParticleBurst flash = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 5,
|
||||||
|
.speed_min = 5.0f,
|
||||||
|
.speed_max = 20.0f,
|
||||||
|
.life_min = 0.04f,
|
||||||
|
.life_max = 0.08f,
|
||||||
|
.size_min = 3.0f,
|
||||||
|
.size_max = 5.0f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 8.0f,
|
||||||
|
.gravity_scale = 0.0f,
|
||||||
|
.color = {255, 240, 200, 255}, /* bright flash */
|
||||||
|
.color_vary = false,
|
||||||
|
};
|
||||||
|
particle_emit(&flash);
|
||||||
|
|
||||||
|
/* Smoke cloud — slower, lingers */
|
||||||
|
ParticleBurst smoke = {
|
||||||
|
.origin = pos,
|
||||||
|
.count = 8,
|
||||||
|
.speed_min = 15.0f,
|
||||||
|
.speed_max = 50.0f,
|
||||||
|
.life_min = 0.3f,
|
||||||
|
.life_max = 0.7f,
|
||||||
|
.size_min = 2.5f,
|
||||||
|
.size_max = 4.5f,
|
||||||
|
.spread = (float)M_PI,
|
||||||
|
.direction = 0,
|
||||||
|
.drag = 3.5f,
|
||||||
|
.gravity_scale = -0.1f, /* floats up */
|
||||||
|
.color = {120, 120, 130, 180}, /* dark smoke */
|
||||||
|
.color_vary = true,
|
||||||
|
};
|
||||||
|
particle_emit(&smoke);
|
||||||
|
}
|
||||||
|
|
||||||
/* Spawn a single dust mote with the given visual properties. */
|
/* Spawn a single dust mote with the given visual properties. */
|
||||||
static void spawn_dust_mote(Vec2 pos, Vec2 vel,
|
static void spawn_dust_mote(Vec2 pos, Vec2 vel,
|
||||||
float life_min, float life_max,
|
float life_min, float life_max,
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir);
|
|||||||
/* Wall slide dust (small puffs while scraping against a wall) */
|
/* Wall slide dust (small puffs while scraping against a wall) */
|
||||||
void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir);
|
void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir);
|
||||||
|
|
||||||
|
/* Metal hit sparks (turret/machine takes non-lethal damage) */
|
||||||
|
void particle_emit_hit_sparks(Vec2 pos);
|
||||||
|
|
||||||
|
/* Metal explosion (turret/machine death — shrapnel + flash) */
|
||||||
|
void particle_emit_metal_explosion(Vec2 pos);
|
||||||
|
|
||||||
/* Ambient atmosphere dust (call each frame for Mars Surface levels).
|
/* Ambient atmosphere dust (call each frame for Mars Surface levels).
|
||||||
* Spawns subtle dust motes around the camera viewport that drift with wind.
|
* Spawns subtle dust motes around the camera viewport that drift with wind.
|
||||||
* cam_pos = camera top-left world position, vp = viewport size in pixels. */
|
* cam_pos = camera top-left world position, vp = viewport size in pixels. */
|
||||||
|
|||||||
@@ -5,9 +5,46 @@
|
|||||||
#include "engine/renderer.h"
|
#include "engine/renderer.h"
|
||||||
#include "engine/particle.h"
|
#include "engine/particle.h"
|
||||||
#include "engine/audio.h"
|
#include "engine/audio.h"
|
||||||
|
#include "config.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
/* ── Shared helpers ───────────────────────────────── */
|
||||||
|
|
||||||
|
/* Kill enemy if it fell past the bottom of the level. */
|
||||||
|
static bool enemy_check_level_bottom(Entity *self, const Tilemap *map,
|
||||||
|
EntityManager *em) {
|
||||||
|
float level_bottom = (float)(map->height * TILE_SIZE);
|
||||||
|
if (self->body.pos.y > level_bottom) {
|
||||||
|
self->flags |= ENTITY_DEAD;
|
||||||
|
self->health = 0;
|
||||||
|
entity_destroy(em, self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for cliff ahead of a ground enemy. Returns true if cliff detected.
|
||||||
|
* Uses a wider lookahead that scales with speed for fast-moving enemies. */
|
||||||
|
static bool enemy_detect_cliff(const Body *body, float patrol_dir,
|
||||||
|
float speed, const Tilemap *map) {
|
||||||
|
if (!body->on_ground) return false;
|
||||||
|
|
||||||
|
/* Scale lookahead with speed: at least 4px, up to speed/10 */
|
||||||
|
float lookahead = speed * 0.1f;
|
||||||
|
if (lookahead < 4.0f) lookahead = 4.0f;
|
||||||
|
|
||||||
|
float check_x = (patrol_dir > 0) ?
|
||||||
|
body->pos.x + body->size.x + lookahead :
|
||||||
|
body->pos.x - lookahead;
|
||||||
|
float check_y = body->pos.y + body->size.y + 4.0f;
|
||||||
|
|
||||||
|
int tx = world_to_tile(check_x);
|
||||||
|
int ty = world_to_tile(check_y);
|
||||||
|
|
||||||
|
return !tilemap_is_solid(map, tx, ty);
|
||||||
|
}
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════════
|
||||||
* GRUNT - ground patrol enemy
|
* GRUNT - ground patrol enemy
|
||||||
* ════════════════════════════════════════════════════ */
|
* ════════════════════════════════════════════════════ */
|
||||||
@@ -20,6 +57,9 @@ static void grunt_update(Entity *self, float dt, const Tilemap *map) {
|
|||||||
|
|
||||||
Body *body = &self->body;
|
Body *body = &self->body;
|
||||||
|
|
||||||
|
/* Kill if fallen off bottom of level */
|
||||||
|
if (enemy_check_level_bottom(self, map, s_grunt_em)) return;
|
||||||
|
|
||||||
/* Death sequence */
|
/* Death sequence */
|
||||||
if (self->flags & ENTITY_DEAD) {
|
if (self->flags & ENTITY_DEAD) {
|
||||||
animation_set(&self->anim, &anim_grunt_death);
|
animation_set(&self->anim, &anim_grunt_death);
|
||||||
@@ -48,19 +88,10 @@ static void grunt_update(Entity *self, float dt, const Tilemap *map) {
|
|||||||
gd->patrol_dir = -gd->patrol_dir;
|
gd->patrol_dir = -gd->patrol_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Turn around at ledge: check if there's ground ahead */
|
/* Turn around at ledge */
|
||||||
if (body->on_ground) {
|
if (enemy_detect_cliff(body, gd->patrol_dir, GRUNT_SPEED, map)) {
|
||||||
float check_x = (gd->patrol_dir > 0) ?
|
gd->patrol_dir = -gd->patrol_dir;
|
||||||
body->pos.x + body->size.x + 2.0f :
|
body->vel.x = 0;
|
||||||
body->pos.x - 2.0f;
|
|
||||||
float check_y = body->pos.y + body->size.y + 4.0f;
|
|
||||||
|
|
||||||
int tx = world_to_tile(check_x);
|
|
||||||
int ty = world_to_tile(check_y);
|
|
||||||
|
|
||||||
if (!tilemap_is_solid(map, tx, ty)) {
|
|
||||||
gd->patrol_dir = -gd->patrol_dir;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation */
|
/* Animation */
|
||||||
@@ -146,12 +177,14 @@ static Entity *find_player(EntityManager *em) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void flyer_update(Entity *self, float dt, const Tilemap *map) {
|
static void flyer_update(Entity *self, float dt, const Tilemap *map) {
|
||||||
(void)map; /* flyers don't collide with tiles */
|
|
||||||
FlyerData *fd = (FlyerData *)self->data;
|
FlyerData *fd = (FlyerData *)self->data;
|
||||||
if (!fd) return;
|
if (!fd) return;
|
||||||
|
|
||||||
Body *body = &self->body;
|
Body *body = &self->body;
|
||||||
|
|
||||||
|
/* Kill if fallen off bottom of level */
|
||||||
|
if (enemy_check_level_bottom(self, map, s_flyer_em)) return;
|
||||||
|
|
||||||
/* Death sequence */
|
/* Death sequence */
|
||||||
if (self->flags & ENTITY_DEAD) {
|
if (self->flags & ENTITY_DEAD) {
|
||||||
animation_set(&self->anim, &anim_flyer_death);
|
animation_set(&self->anim, &anim_flyer_death);
|
||||||
@@ -289,6 +322,9 @@ static void charger_update(Entity *self, float dt, const Tilemap *map) {
|
|||||||
|
|
||||||
Body *body = &self->body;
|
Body *body = &self->body;
|
||||||
|
|
||||||
|
/* Kill if fallen off bottom of level */
|
||||||
|
if (enemy_check_level_bottom(self, map, s_charger_em)) return;
|
||||||
|
|
||||||
/* Death sequence */
|
/* Death sequence */
|
||||||
if (self->flags & ENTITY_DEAD) {
|
if (self->flags & ENTITY_DEAD) {
|
||||||
animation_set(&self->anim, &anim_charger_death);
|
animation_set(&self->anim, &anim_charger_death);
|
||||||
@@ -319,14 +355,10 @@ static void charger_update(Entity *self, float dt, const Tilemap *map) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Reverse at ledge */
|
/* Reverse at ledge */
|
||||||
if (body->on_ground) {
|
if (enemy_detect_cliff(body, cd->patrol_dir,
|
||||||
float cx = (cd->patrol_dir > 0) ?
|
CHARGER_PATROL_SPEED, map)) {
|
||||||
body->pos.x + body->size.x + 2.0f :
|
cd->patrol_dir = -cd->patrol_dir;
|
||||||
body->pos.x - 2.0f;
|
body->vel.x = 0;
|
||||||
float cy = body->pos.y + body->size.y + 4.0f;
|
|
||||||
if (!tilemap_is_solid(map, world_to_tile(cx), world_to_tile(cy))) {
|
|
||||||
cd->patrol_dir = -cd->patrol_dir;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Detect player — horizontal line-of-sight */
|
/* Detect player — horizontal line-of-sight */
|
||||||
@@ -510,10 +542,12 @@ static int count_alive_grunts(EntityManager *em) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void spawner_update(Entity *self, float dt, const Tilemap *map) {
|
static void spawner_update(Entity *self, float dt, const Tilemap *map) {
|
||||||
(void)map;
|
|
||||||
SpawnerData *sd = (SpawnerData *)self->data;
|
SpawnerData *sd = (SpawnerData *)self->data;
|
||||||
if (!sd) return;
|
if (!sd) return;
|
||||||
|
|
||||||
|
/* Kill if fallen off bottom of level */
|
||||||
|
if (enemy_check_level_bottom(self, map, s_spawner_em)) return;
|
||||||
|
|
||||||
/* Death sequence */
|
/* Death sequence */
|
||||||
if (self->flags & ENTITY_DEAD) {
|
if (self->flags & ENTITY_DEAD) {
|
||||||
animation_set(&self->anim, &anim_spawner_death);
|
animation_set(&self->anim, &anim_spawner_death);
|
||||||
|
|||||||
@@ -177,36 +177,45 @@ static Camera *s_active_camera = NULL;
|
|||||||
|
|
||||||
static void damage_entity(Entity *target, int damage) {
|
static void damage_entity(Entity *target, int damage) {
|
||||||
target->health -= damage;
|
target->health -= damage;
|
||||||
|
|
||||||
|
Vec2 center = vec2(
|
||||||
|
target->body.pos.x + target->body.size.x * 0.5f,
|
||||||
|
target->body.pos.y + target->body.size.y * 0.5f
|
||||||
|
);
|
||||||
|
|
||||||
if (target->health <= 0) {
|
if (target->health <= 0) {
|
||||||
target->flags |= ENTITY_DEAD;
|
target->flags |= ENTITY_DEAD;
|
||||||
|
|
||||||
/* Death particles — centered on entity */
|
/* Death particles — turrets get a metal explosion, others a puff */
|
||||||
Vec2 center = vec2(
|
if (target->type == ENT_TURRET || target->type == ENT_LASER_TURRET) {
|
||||||
target->body.pos.x + target->body.size.x * 0.5f,
|
particle_emit_metal_explosion(center);
|
||||||
target->body.pos.y + target->body.size.y * 0.5f
|
|
||||||
);
|
|
||||||
SDL_Color death_color;
|
|
||||||
if (target->type == ENT_ENEMY_GRUNT) {
|
|
||||||
death_color = (SDL_Color){200, 60, 60, 255}; /* red debris */
|
|
||||||
} else if (target->type == ENT_ENEMY_FLYER) {
|
|
||||||
death_color = (SDL_Color){140, 80, 200, 255}; /* purple puff */
|
|
||||||
} else if (target->type == ENT_TURRET || target->type == ENT_LASER_TURRET) {
|
|
||||||
death_color = (SDL_Color){160, 160, 160, 255}; /* metal scraps */
|
|
||||||
} else if (target->type == ENT_ENEMY_CHARGER) {
|
|
||||||
death_color = (SDL_Color){220, 140, 40, 255}; /* orange spark */
|
|
||||||
} else if (target->type == ENT_SPAWNER) {
|
|
||||||
death_color = (SDL_Color){180, 60, 180, 255}; /* purple burst */
|
|
||||||
} else {
|
} else {
|
||||||
death_color = (SDL_Color){200, 200, 200, 255}; /* grey */
|
SDL_Color death_color;
|
||||||
|
if (target->type == ENT_ENEMY_GRUNT) {
|
||||||
|
death_color = (SDL_Color){200, 60, 60, 255};
|
||||||
|
} else if (target->type == ENT_ENEMY_FLYER) {
|
||||||
|
death_color = (SDL_Color){140, 80, 200, 255};
|
||||||
|
} else if (target->type == ENT_ENEMY_CHARGER) {
|
||||||
|
death_color = (SDL_Color){220, 140, 40, 255};
|
||||||
|
} else if (target->type == ENT_SPAWNER) {
|
||||||
|
death_color = (SDL_Color){180, 60, 180, 255};
|
||||||
|
} else {
|
||||||
|
death_color = (SDL_Color){200, 200, 200, 255};
|
||||||
|
}
|
||||||
|
particle_emit_death_puff(center, death_color);
|
||||||
}
|
}
|
||||||
particle_emit_death_puff(center, death_color);
|
|
||||||
|
|
||||||
/* Screen shake on kill */
|
/* Screen shake on kill — stronger for turret explosions */
|
||||||
if (s_active_camera) {
|
if (s_active_camera) {
|
||||||
camera_shake(s_active_camera, 2.0f, 0.15f);
|
float intensity = (target->type == ENT_TURRET ||
|
||||||
|
target->type == ENT_LASER_TURRET) ? 3.5f : 2.0f;
|
||||||
|
camera_shake(s_active_camera, intensity, 0.15f);
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_play_sound_at(s_sfx_enemy_death, 80, center, 0);
|
audio_play_sound_at(s_sfx_enemy_death, 80, center, 0);
|
||||||
|
} else if (target->type == ENT_TURRET || target->type == ENT_LASER_TURRET) {
|
||||||
|
/* Hit marker sparks on non-lethal turret damage */
|
||||||
|
particle_emit_hit_sparks(center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,11 +237,14 @@ static void damage_player(Entity *player, int damage, Entity *source) {
|
|||||||
ppd->inv_timer = PLAYER_INV_TIME;
|
ppd->inv_timer = PLAYER_INV_TIME;
|
||||||
player->flags |= ENTITY_INVINCIBLE;
|
player->flags |= ENTITY_INVINCIBLE;
|
||||||
|
|
||||||
/* Knockback away from source */
|
/* Knockback away from source, scaled by source speed */
|
||||||
if (source) {
|
if (source) {
|
||||||
float knock_dir = (player->body.pos.x < source->body.pos.x) ?
|
float knock_dir = (player->body.pos.x < source->body.pos.x) ?
|
||||||
-1.0f : 1.0f;
|
-1.0f : 1.0f;
|
||||||
player->body.vel.x = knock_dir * 150.0f;
|
float src_speed = fabsf(source->body.vel.x);
|
||||||
|
float knock_str = 150.0f;
|
||||||
|
if (src_speed > knock_str) knock_str = src_speed;
|
||||||
|
player->body.vel.x = knock_dir * knock_str;
|
||||||
player->body.vel.y = -150.0f;
|
player->body.vel.y = -150.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,8 +306,7 @@ static void handle_collisions(EntityManager *em) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Enemy contact damage to player ──── */
|
/* ── Enemy contact damage to player ──── */
|
||||||
if (player && !(player->flags & ENTITY_INVINCIBLE) &&
|
if (player && entity_is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
|
||||||
entity_is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
|
|
||||||
if (physics_overlap(&a->body, &player->body)) {
|
if (physics_overlap(&a->body, &player->body)) {
|
||||||
/* Check if player is stomping (falling onto enemy from above) */
|
/* Check if player is stomping (falling onto enemy from above) */
|
||||||
bool stomping = (player->body.vel.y > 0) &&
|
bool stomping = (player->body.vel.y > 0) &&
|
||||||
@@ -307,8 +318,16 @@ static void handle_collisions(EntityManager *em) {
|
|||||||
damage_entity(a, 2);
|
damage_entity(a, 2);
|
||||||
if (a->flags & ENTITY_DEAD) stats_record_kill();
|
if (a->flags & ENTITY_DEAD) stats_record_kill();
|
||||||
player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f;
|
player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f;
|
||||||
} else {
|
} else if (!(player->flags & ENTITY_INVINCIBLE)) {
|
||||||
damage_player(player, a->damage, a);
|
/* Charger deals extra damage and knockback while charging */
|
||||||
|
int dmg = a->damage;
|
||||||
|
if (a->type == ENT_ENEMY_CHARGER) {
|
||||||
|
ChargerData *cd = (ChargerData *)a->data;
|
||||||
|
if (cd && cd->state == CHARGER_CHARGE) {
|
||||||
|
dmg = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
damage_player(player, dmg, a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,8 +308,8 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
|
|||||||
pd->dash_timer = PLAYER_DASH_DURATION;
|
pd->dash_timer = PLAYER_DASH_DURATION;
|
||||||
pd->dash_dir = vec2(0.0f, 1.0f);
|
pd->dash_dir = vec2(0.0f, 1.0f);
|
||||||
|
|
||||||
/* Brief invincibility during dash */
|
/* Invincibility during dash + short grace period after */
|
||||||
pd->inv_timer = PLAYER_DASH_DURATION;
|
pd->inv_timer = PLAYER_DASH_DURATION + PLAYER_DASH_INV_GRACE;
|
||||||
self->flags |= ENTITY_INVINCIBLE;
|
self->flags |= ENTITY_INVINCIBLE;
|
||||||
|
|
||||||
/* Jetpack burst downward */
|
/* Jetpack burst downward */
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
/* Invincibility after taking damage */
|
/* Invincibility after taking damage */
|
||||||
#define PLAYER_INV_TIME 1.5f /* seconds of invincibility */
|
#define PLAYER_INV_TIME 1.5f /* seconds of invincibility */
|
||||||
|
#define PLAYER_DASH_INV_GRACE 0.15f /* extra invincibility after dash */
|
||||||
|
|
||||||
/* Aim direction (for shooting) */
|
/* Aim direction (for shooting) */
|
||||||
typedef enum AimDir {
|
typedef enum AimDir {
|
||||||
|
|||||||
Reference in New Issue
Block a user