BYTEPATH++ Attacks and Seeker
Fable 5
Summary
Built out BYTEPATH++'s combat slice: the four attacks (normal/shotgun/dart/pulse), a full sound pass with the F3 sound-tuner port, a codebase-modernization sweep onto Anchor framework systems, the seven-enemy .rogue roster port — then the session's two biggest decisions: that roster was removed as "forced" in favor of a single SNKRX-style seeker horde enemy, and (post-seal addendum) the expedition-map design arrived — THE TOWER: phase 1 became a vertical ascent whose stacked bands persist and become the descent, with destruction seeding the descent's difficulty. design.md fully revised.
Attack roster decision (normal, shotgun, dart + existing pulse):
- Proposed 4 attacks covering the major delivery types from
reference/archetypes.md§1: normal (direct projectile baseline), shotgun (volume fire, most distinct of shotgun/spread/triple), dart (unlocks the DoT subsystem), mines (placed/zoning), with shield (aura) consciously cut as its own future subsystem. - Owner: "We already have area, let's do normal, shotgun and dart" — mines deferred.
- All values ported faithfully from BYTEPATH.rogue main.moon (shoot L1634+, set_attack L1798+): normal 0.2s cd/white/250 speed; shotgun 0.8s/red/12 projectiles 400-800 speed ±30° cone with shotgun_decel (0.25-0.5s decelerate-and-die); dart 0.3s/green/250 speed.
- Infrastructure:
player:set_attack(name),player:shoot()restructured into shared muzzle juice + per-attack branches, keys 1-4 select attacks (pipeline debug moved to F5-F8),dot_multiplierstat added.
Dart DoT subsystem:
dartentity (main.moon L3144-3159): sticks to the enemy at hit offset, drawn in a newpre_effectslist rendered UNDER enemies (source draw order L186), dies with parent.- Collision branch: dart projectiles do NO direct damage — spawn dart +
enemy_light_hit(1.15 scale bounce vs 1.35). enemy_update_dots: darts stack per-enemy; every 0.5s × dot_multiplier all dart damages sum into oneenemy_hit.- Dart embed: sensor-begin fires with projectile center ~4px outside the enemy edge, so the stick point advances
dart_embed_depth(6, then tuned to 4) px along travel direction.
Camera shake migration to the framework system:
- First request: directional spring shake on normal/dart fire, stronger on shotgun "similar to the explosion in SNKRX-update... detonator or the one that spawns Big Rock" — identified as the GEOMANCER rock-slam recipe (snkrx/prop.lua L498-499: shake_push 8 + shake_shake 5/0.2); detonator uses shake_trauma instead.
- Owner course-correct: "do not add a new system, remove the old shake system and use the shake system that is built into Anchor itself."
- Removed the ported sampled_shake/camera_shake system (~65 lines); added static
main_camera = camera_new()used only formain_camera.shake; world draw wrapped in camera_attach/detach; camera_update at end of update() so same-frame pushes integrate. - Values: normal/dart
shake_push(shake, attack_r, 3)(SNKRX-update projectile default); shotgun push 8 + shake_shake(5, 0.2); pulse's original camera\shake 2, 0.25 → shake_shake(2, 0.25).
Codebase modernization review (owner-requested audit of old ported systems vs Anchor framework):
- Found 4 real duplications + 1 judgment call; all approved and executed:
- kd_spring → framework spring: same semi-implicit Euler integrator, parameterization convertible EXACTLY — k=(2πf)², d=4π(1-b)f inverts to f=√100/2π≈1.5915, b=0.5 for the original k=100/d=10.
juice_spring()helper; feel digit-identical. - layer_push_pivot → deleted: the engine's layer_push IS a pivot when drawing in local coords (arena idiom); all 9 draw methods converted to -w/2 local coordinates.
- get_enemies_in_area → query_entities_aabb (same centered-box convention; the old center+corners test undercounted true overlaps).
- real_time → engine
timeglobal (verified in anchor.c: game_time += PHYSICS_RATE unscaled while time_scale only scales dt handed to Lua). - Input layer: bind('shoot','mouse:1')/bind('flip','mouse:2')/attack_1-4 via input_down/input_pressed; dev-tool keys (F1, F3, F5-F8, tuner mouse) deliberately stay raw.
lerp_dt conversion + the 120Hz discovery:
- Owner: "Any mouse lerp should use the new functions for lerping which use p, t values... The new lerp functions take in dt which makes them framerate independent."
- Converted mouse tracking to
math.lerp_dt(0.9, 1/60, dt, ...), seeker chase velocitylerp_dt(0.625, 1/60, ...)(derived from the original applyForce on LOVE meter-30 mass ≈ 0.0267), aim lerp_angle_dt(0.9, 1/60). - Initially put the mouse lerp on unscaled_dt (original per-frame lerps didn't slow during slow()); owner rejected: "everything should just use dt" — reverted.
- Debugging a "stalled" seeker telegraph revealed the engine steps at 120Hz (PHYSICS_RATE = 1/120): verify's 300 frames = only 2.5 game-seconds. Per-frame constants from 60fps sources apply twice per second — the slice's mouse lerp had been unintentionally ~2x tighter than .rogue's. Verify extended to 600 frames.
Sound system + F3 sound tuner port:
- AUDIO section ported from SNKRX-update/snkrx/init.lua: sound_tuning pcall-load,
sfx(handle, volume, pitch)with per-sound DSP lookup + 5% pitch jitter, volumes/volumes_defaults/volumes_apply_overrides diff pipeline. sound_tuner.lua(new root file): the SNKRX-update F3 two-tier tuner's LOGIC (off→browser→editor, path→volume-key mapping with _variants stripping, auto-replay 0.5s with live sound_handle_set_dsp retune, diff-based saves to root sound_tuning.lua/volume_tuning.lua) rebuilt in the BYTEPATH window language: F1-tuner chrome, mouse-driven rows + sliders (bits 1-16 / sr_div 1-32 integer-snapped, volume 0-2, RMB reset), Space/S/Esc/PgUp/PgDn accelerators. Game PAUSES while open (update early-out); mutual exclusion with F1. Engine bindings verified in Anchor3 anchor.c (sound_list, sound_play_handle, sound_handle_set_dsp, sound_get_path, file_write_string). Portability catches: no globalunpackin Lua 5.4; vormgevers.ttf has no em-dash/middle-dot glyphs (ASCII only in window text).- Role column added later ("add on the side which attack/effect the sound belongs to"):
sound_rolestable keyed by volume key, shown dim in browser rows + editor title bar; browser widened to 430. - Volume-key map switched from path-keyed to HANDLE-keyed so the same file loaded twice serves two roles with independent volume rows (engine sound_load doesn't cache — each load is a fresh entry).
Sound picks (all from E:/sound packs, mostly Sci-Fi Gun Sounds PRO):
- Normal fire: Gun 01_1/2/3 random. Shotgun: one of Gun 10_1/2/4 + one of Gun 22_1/2/3/4 together (10_3, 10_5, 22_5 culled by ear). Dart fire: Weapon 06 Shoot 1/3. Pulse: Sci-Fi Explosion 09+11+23 together, per-layer volume keys.
- Enemy hit iterations: 2_1 vs 2_5 A/B toggle on F1 → settled on a LAYERED MIX of Hit_Generic 1_5 + 2_5 (F1 panel extended to option sets: 2_1 / 2_5 / 1_5+2_5).
- Enemy death: Light_Enemy_Impact_1/2 (RPG Magic SFX 2) tried, replaced by Hit_Generic 1_2/1_3 random. DoT tick: PM_GENERIC_Miss01 tried, replaced by Hit_Generic 2_6 (silent flag on enemy_hit so ticks don't stack the hit sound). Dart attach: cancel3__pyu-blip.ogg.
- Projectile death (wall/decel fizzle): the hit-mix files loaded a SECOND time as proj_death_a/b at 0.5 baseline — separate tuner rows via the handle-keyed map.
- Shooter fire = player gun files re-loaded at 0.5; exploder boom = pulse layers re-loaded at 0.4 + slight shake — both later pitched UP to random 1.5-1.8 for enemy/player differentiation (removed with the roster).
- BYTEPATH 2018 sound research (owner request): full inventory of libraries/sound.lua — game_shoot_1 (all projectiles except Flame), game_shoot_2 (Blast/shotgun ONLY), laser/lightning 1-5/flame/explosion (vol 0.3)/enemy_die (pitch 1.1-1.3)/hurt 1-3/item/key + console/menu/keystroke_1-28 + AIRGLOW Memory Bank soundtrack. Notable: game_enemy_hit.ogg exists but is NEVER played — shipped BYTEPATH had no per-hit sound. BYTEPATH.rogue confirmed to have ZERO sounds.
F1 panel rework:
- "remove the previous settings on the F1 panel since we already settled the shader visuals" — the 12 shader knobs deleted; settled values baked into a
shader_paramstable pushed every frame (uniforms have no GLSL defaults); tuner_export.txt kept as record. - Panel became a small
debugwindow holding the enemy-hit sound picker (options fromhit_sound_options, active filled white, click previews the set).
Enemy roster port (built, then removed):
- Pass 1 infrastructure: enemy_projectile (3x3 red sensor rect + shotgun_decel mod later), explosion_effect, block patrol restored (the static block was slice-only), on_death hook in enemy_hit, enemy sensor tag for player hits.
- Seeker (.rogue L2648): three-phase seek/telegraph/lunge with mm-chained lunges + death burst; chase force translated to lerp_dt (0.625/60Hz, derived from LOVE mass); later resized 9x5, sped up (chase 100, lunge 230), and given the SNKRX steering blend (seek+wander+separate + robotic velocity-match pull lerp_dt(0.9, 0.3)) per "kinda robotic like it is now but mixed with that behavior"; lunge sound RPG3_WindMagic_LightImpact01 (charge-up sound Charge 2_1/2/3 tried and removed).
- Shooter (L2535, 18x7 after +1px), Exploder (L2430, fixed 12x12 collider under a growing 8→16 visual, kill-before-timer = no explosion), Shotgunner (L2018, proximity-gated 96px burst with 0.6s white telegraph, weak 0.1x drift chase), Spawner (L1893, spin-in, 10s child waves of {shotgunner, seeker}, offscreen-left cull deviation), Rocks (BYTEPATH 2018 Rock/BigRock adapted to SQUARES per owner — "The rocks should be squares and break down into smaller squares... fits this game better"; big 16x16/300hp splits into 4 small 8x8/100hp moving opposite).
- Roster discussion: measured the built four against the plan's spread; recommended Spawner (tank + priority-target), Shotgunner (proximity-priced threat = the greed pillar), flagged the "hunt" as a spawn-policy concept rather than an enemy species.
THE PIVOT — SNKRX-style single enemy:
- Owner: "most of these enemies feel a bit forced and will not be necessary to make the game fun, we can probably get away with just making a very SNKRX-like Seeker."
- Built
chaser= SNKRX-update's snkrx/seeker.lua behavior verbatim: 14x6 body, speed random(50,75), steering_seek + steering_wander(64,32,16) + steering_separate(16) force-limited 200 → apply_force → velocity clamp → lerp_angle_dt(0.99, 0.1) heading; damping 3; spawn spring-pop (0.75 @ freq 3 bounce 0.7) + 0.15s flash; hp mapped to 50 then raised to 100 per owner ("50 is too low"); enemy↔enemy collision enabled for horde jostle; spawns INSIDE the arena (key C), 5-pack on boot. - "This feels much better, remove all other enemies (including rock) except Chaser and let's build the game off this alone" — all seven roster classes deleted (~750 lines) plus enemy_projectile/explosion_effect/their sounds/keys/verify spawns; main.lua header records the removal with re-port pointer.
- Renamed chaser → seeker at session end (the name freed by the roster removal); header disambiguates it from the removed .rogue Seeker.
Design process Q&A: confirmed the full design.md was read (recounted the two-phase arc, two-channel principle, forge/crypt/descent, tree structure so the owner could check); recommended folding session design decisions into design.md at decision time + an eventual plan.md (renderer/PLAN.md pattern) so session startup = design.md + plan.md + memory. Concurrent sessions: git worktrees are the standard answer (BYTEPATH++ turned out to already be a git repo, no remote); honest caveat that the single-file main.lua makes parallel code work conflict-prone — prefer sequential sessions for main.lua, parallelism only with disjoint file ownership (docs/reference/modules), and end overlapping sessions one at a time (a327ex-site push race).
THE TOWER — expedition-map design (post-seal addendum, 2026-07-13):
- Owner's proposal: since phase 2 is a descent, phase 1 becomes an ASCENT — a vertical map in the lineage of the pre-.rogue prototype (identified on D:/ as SHOOTRX: side walls, block rows descending), but an actual explorable map, not a shmup scroll. Descent = Downwell in spirit without the gravity constraint. Plus a possible LEGACY system from the design-log ideas: the 8-12 expedition maps vertically stack and become the descent — "things we destroyed or didn't destroy... come back to bite us or not."
- Analysis: traced the legacy idea to NDA log 17's V2 "Rehearsal" (coverage = intel, "you know the world exactly as well as you chose to rehearse it") — the tower fixes V2's fixed-world replayability risk since it regenerates per arc. Committed recommendations: tower height = slots GRANTED not spent (early forge = more fog, not less game — makes early declaration braver); one expedition = one band (each launch hoisted to the next band, expeditions stay disposable lives); coverage becomes descent intel (expeditions triple-pay: dust, haul, scouting — kills the phase-1-as-chore risk).
- Two problems leaned on: the bite must cut both ways (destruction can't be pure-good in both phases or "destroy everything" is dominant) and the empty-band problem (a strip-mined band is boring on descent). Initial proposal: scars/guardians trade + recolonization + a collapse front chasing the descent.
- Owner's simpler solution — SEEDS: destroying structures yields dust AND a chance to drop a per-band, permanent, inert seed; each seed raises that band's enemy population in the descent. Dust income ↔ later difficulty coupled roughly linearly but noisily — the PoE-league pattern ("juice the map, eat the consequences"). Solves both problems in one rule: farmed bands are the densest, never empty; greed is priced twice. Collapse front demoted to a pacing-only open dial.
- Extraction redesign: owner rejected extract-by-returning-to-elevator ("backtracking... rehearsing the descent takes away from it"). Solved with forward one-way extraction gates scattered ahead along the route — using any gate banks the run; the commitment moment is walking PAST a gate, and phase 1 contains ZERO downward movement (the descent is the first time the player ever moves down). Extract early = the band's top stays fog.
- Geometry: the spine and the wings — bands a few screens wide, net-upward spine = progress, sideways wings = greed (denser destructibles, better haul); "how much horizontal" becomes a generation dial (wing width/density per band archetype) instead of a design commitment; coverage % = wings swept.
- design.md fully revised (title → Ascent/Forge/Descent, overview, two-channel + third-ledger seeds, combat's seeker-substrate decision recorded, Phase 1 rewritten as The Ascent, descent-through-your-own-history, open dials incl. the seed curve as the central knob, next design work = band generation/seed tuning/descent assembly). Owner: "This is in a sense the expedition map design work itself, which it seems already came to me naturally (a good sign)."
Memory: project_bytepath_plus_plus.md updated with the modernization sweep, the 120Hz warning, the combat-slice state, the enemy pivot, and the full tower revision + lineage (V2-Rehearsal, SHOOTRX).
Let's start with BYTEPATH++. Let's implement 4 attacks, one for each major type. Which ones do you think they would be?
I'll read the BYTEPATH++ design doc first to ground this in what's already settled.
Read (E:\a327ex\BYTEPATH++\design.md)
1 # BYTEPATH++ — Design — Expeditions / Forge / Descent
2
3 Written 2026-07-11. Name locked: **BYTEPATH++** (all caps). Lore-free by decision —
4 the game stands on gameplay alone; any lore (BYTEPATH-universe or otherwise) gets
5 figured out later. The previously developed Technochrist worldbuilding is shelved at
6 `E:/a327ex/private/technochrist-game-lore.md` and is NOT assumed anywhere below.
7 Design lineage: BYTEPATH (visual style + big tree + name), BYTEPATH.rogue (combat +
8 CRT shaders + rosters), Roguelite Design Analysis 2 (structure lessons — Summit/B-Side
9 phase purity, Tab's use-or-bank tension, Heirloom's restraint economy, and the
10 commitment-weight thesis: "a decision the player must live with is the cheapest drama
11 a roguelite can buy, and the market barely stocks it").
12
13 ---
14
15 ## Design pillars
16
17 1. **Commitment over optionality.** The game trains follow-through, never rerolling.
18 No respec, no rollback, no null choices. The two phases sit at opposite ends of the
19 commitment-weight axis (phase 1 light per-decision, phase 2 locked/critical) and
20 crossing between them is the game's central event.
21 2. **Anti-idler, hard rules:** no wall-clock or offline progression of any kind; a
22 defined ending; bounded NG+ (no infinite prestige); exactly one AFK-capable build
23 archetype, and only as an earned endgame expression (a deep tree keystone).
24 3. **Meaningful actions.** Moment-to-moment play must contain real decisions:
25 route scarcity (you can never take everything), readable-but-learnable world
26 (knowledge = mastery), and execution that stays load-bearing (dodge-centric,
27 low-HP combat). No vacuum-simulator scavenging, no number-goes-up-for-free.
28 4. **Ends properly.** One arc = expeditions → forge → descent → ending. The game is
29 proud to end.
30
... [200 more lines]
Read (D:\2020-2021 code\old\BYTEPATH.rogue\todo)
1 300 level variants
2 modules window
3 sounds
4 trailer + steam page
5
6 *Seeker 100 - 1000
7 *Shooter 150 - 1500
8 *Block 200 - 2000
9 *Exploder 200 - 2000
10 *Gyro 400 - 4000
11 *Barrager 400 - 4000
12 *Host 900 - 9000
13 *Shotgunner 200 - 2000
14 *Orbitter 200 - 2000
15 *Spawner 800 - 8000
16 *Moving Bar 1000 - 10000
17 *Enemy Area
18
19 *Normal
20 *Shotgun - 12 fast moving but decelerating projectiles shot at the same time, high cooldown
21 *Cannon - high damage and high cooldown single projectile
22 *Dart - damage over time projectile, normal cooldown
23 *Shield - area damage over time that increases with each shot and decreases the bigger it is, reaching an equilibrium
24 *Spread - many low damage projectiles shot at random angles, low cooldown
25 *Triple - three projectiles shot at the same time, normal cooldown
26 *Pulse - high area damage, high cooldown
27 *Mines - mines that explode when near enemies, high area damage, high mine production cooldown
28
29 *Pierce - pierces through enemy, +10% damage per pierce
30 *Finale - every 4th projectile deals double damage
... [62 more lines]
Bash (ls "E:/a327ex/BYTEPATH++")
anchor/
assets/
design.md
main.lua
reference/
run.bat
tuner_export.txt
Bash (wc -l "E:/a327ex/BYTEPATH++/main.lua"; ls "E:/a327ex/BYTEPATH++/reference")
1258 E:/a327ex/BYTEPATH++/main.lua
archetypes.md
source-catalogs.md
Read (E:\a327ex\BYTEPATH++\main.lua)
1 --[[
2 BYTEPATH++ — first playable slice.
3
4 Player moves as the mouse cursor and fires the pulse attack (the area attack)
5 against a static block enemy that takes damage and dies. All juice is a
6 faithful port of BYTEPATH.rogue (D:/2020-2021 code/old/BYTEPATH.rogue,
7 main.moon + common.moon) — per-event effects, timings, and constants are
8 copied from source, with line references noted throughout. Names are
9 translated from the old PascalCase convention to snake_case.
10
11 Render pipeline (main.moon L125-220): game world at 480x270 -> displacement
12 shader pass (UV warp from a displacement map fed by ambient/death glitch
13 blocks) -> nearest 3x upscale -> CRT shader (playscii framebuffer_f_crt) at
14 window resolution -> flash overlay.
15
16 Deliberate deviations from the original, per owner instruction:
17 - the block enemy is stationary (original patrols horizontally)
18 - no sounds (verified: the original has none; its todo lists sounds as
19 unfinished)
20 - player death just refills hp (arena/run flow is future work)
21 - skipped: mod-list HUD, cycle-timer HUD bar, constructs (inactive or
22 absent in this slice)
23 ]]
24
25 require('anchor')({
26 width = 480,
27 height = 270,
28 title = 'BYTEPATH++',
29 scale = 3,
30 filter = 'rough',
... [1229 more lines]
The pulse attack (the area type) is already in the slice. Before answering, let me check the two reference docs — archetypes.md sounds like it might already define the major types.
Read (E:\a327ex\BYTEPATH++\reference\archetypes.md)
1 # Archetype Analysis — what BYTEPATH++ builds from its two parents
2
3 Written 2026-07-11, from reference/source-catalogs.md. Purpose: extract the
4 recurring archetypes across BYTEPATH.rogue (2020) and BYTEPATH (2018), judge them
5 against BYTEPATH++'s design (design.md: expeditions/forge/descent, finite tree,
6 anchor keystones, one AFK archetype, commitment pillars), and propose the build
7 roster. PROPOSALS ONLY — owner picks. ⭐ = recommended for v1 combat.
8
9 ────────────────────────────────────────────────────────────────────────────────
10 ## 1. Attack shapes (union of 9 + 17 attacks → 13 shapes)
11 ────────────────────────────────────────────────────────────────────────────────
12
13 | Shape | .rogue | BYTEPATH | Note |
14 |---|---|---|---|
15 | ⭐ single | normal | Neutral/Rapid | baseline; Rapid = the aspd variant |
16 | ⭐ heavy single | cannon | (2x dmg node builds) | slow big hit |
17 | ⭐ cone burst | shotgun | Blast | 12-shot decelerating wave |
18 | ⭐ fast scatter | spread | Spread/Rapid | dps hose, low aim demand |
19 | multi-parallel | (double mod) | Double/Triple | more of the same shot |
20 | ⭐ radial | pulse | Spin (rotating) | the area statement; pulse is ours already |
21 | ⭐ attached DoT | dart | — | .rogue-unique; pairs with the DoT stat line |
22 | ⭐ aura | shield | — | .rogue-unique; the shots-grow-a-field idea is special |
23 | ⭐ mines/zoning | mines | (drop_mines proc) | placed threat |
24 | homing | — | Homing | aim-forgiveness; tension with cursor skill thesis |
25 | chain | — | Lightning | auto-target adjacency; same tension as homing |
26 | on-hit split | (fork mod) | 2Split/4Split | .rogue treats it as a modifier — better |
27 | beam | — | Laser | expensive to build well; strong identity though |
28 | stream | — | Flame | ammo-hungry hose; distinct feel |
29 | directional cover | (back mod) | Back/Side | .rogue treats it as a modifier — better |
30 | wall-bounce | (ricochet mod) | Bounce | modifier in .rogue — better |
... [147 more lines]
Read (E:\a327ex\BYTEPATH++\reference\source-catalogs.md)
1 # Source Catalogs — BYTEPATH.rogue (2020) & BYTEPATH (2018)
2
3 Compiled 2026-07-11, verified against source (not design notes/todos).
4 Sources: `D:/2020-2021 code/old/BYTEPATH.rogue/` (main.moon + common.moon) and
5 `D:/2020-2021 code/old/BYTEPATH/` (the shipped-game copy — objects/Player.lua 2396
6 lines; the `2017-2018 code` copies are earlier snapshots).
7
8 ═══════════════════════════════════════════════════════════════════════════════
9 # PART 1 — BYTEPATH.rogue (2020)
10 ═══════════════════════════════════════════════════════════════════════════════
11
12 Cursor-is-the-ship arena shooter with a fake-OS meta shell. One player entity, no
13 characters/classes. Combat = attack (1 equipped) × passives (drafted) × enemies.
14
15 ## Attacks (9) — one equipped; auto-fire while LMB held; RMB flips facing
16
17 | Attack | CD | Color | Behavior (shoot, main.moon L1633+) |
18 |---|---|---|---|
19 | normal | 0.2 | white | single projectile, 250 speed |
20 | shotgun | 0.8 | red | 12 projectiles, 400-800 speed, ±30° cone, decelerate & die |
21 | cannon | 0.4 | red | single 350-speed projectile, 2x damage, trail |
22 | dart | 0.3 | green | projectile attaches a DoT dart (per-enemy stack ticks every 0.5s) |
23 | shield | 0.35 | green | no projectile: shots grow a square aura (+5 target, decays to
24 | | | | equilibrium); aura AoE-ticks enemies every 0.25s for 25 |
25 | spread | 0.08 | yellow | 1 projectile ±15° random, 0.75x damage, very fast fire |
26 | triple | 0.16 | yellow | 3 projectiles −15°/0°/+15°, 0.5x damage (desc says 60°, code 15°) |
27 | pulse | 0.8 | blue | 8 radial decelerating projectiles 0.5x + bracket effect + 100x100
28 | | | | area hit at 50 + flash + shake |
29 | mines | 0.8 | blue | slow 4x4 spinning mine, decelerates 1.6-3.2s |
30
... [176 more lines]
Good timing for this question — the archetypes doc already sorted the 9 .rogue attacks into shape families, and pulse (the area statement) is already implemented in the slice. So the question is really: which 4 of the remaining 8 attacks best cover the remaining major delivery types?
Here's how the 9 .rogue attacks group into types:
| Type | Attacks in it | Status |
|---|---|---|
| Radial area burst | pulse | ✅ done |
| Direct projectile | normal, cannon | — |
| Volume fire (multi-projectile) | shotgun, spread, triple | — |
| Damage over time | dart | — |
| Aura (area-over-time, no projectile) | shield | — |
| Placed/zoning | mines | — |
That's five open types for four slots, so one gets cut. My pick:
Normal — direct projectile. The baseline everything else is measured against, and the canvas the 20 projectile modifiers will read clearest on. Cheapest of all: the
projectileclass already exists; this mostly adds aimed fire alongattack_r(the RMB facing flip is already wired). Cannon is the same delivery type with different numbers, so it doesn't earn a type slot.Shotgun — volume fire. The most mechanically distinct of the trio: 12 projectiles, 400–800 speed, ±30° cone, decelerate-and-die — and the decelerating-projectile path already exists (
pulse_decelis exactly that mechanic, just with random angles). Spread and triple are just fire-rate/count variations on normal, so shotgun is the one that represents the family.Dart — DoT. This is the slot that pays infrastructure dividends: it forces the DoT subsystem into existence (attached dart entity, per-enemy stacks, 0.5s tick), which the entire dot stat family plus decay/amplify/contagion/decaying_shot all depend on later. Without a DoT attack, one of the five stat lines has nothing to attach to.
Mines — placed/zoning. The only attack that lives in the world independent of your aim, which makes it feel like a genuinely different verb rather than a projectile variant. It exercises proximity-trigger + area explosion (reusing
get_enemies_in_areaand the pulse effect vocabulary), and it's the foundation the minefield on-kill/on-cycle procs will stand on.
The cut is shield, and I'd argue for it consciously: the archetypes doc already flags the aura as its own subsystem ("the shots-grow-a-field idea is special"), and it's the most bespoke of the eight — a persistent growing/decaying field with equilibrium math and its own tick loop shares almost nothing with the projectile pipeline. It deserves its own pass rather than being the fourth item in a batch. Dart covers the "damage over time" axis in the meantime.
One alternative worth naming: cannon instead of normal, if you'd rather each attack feel maximally distinct (heavy slow hit vs the shotgun's wave vs dart vs mines). But I think normal earns its slot precisely by being the neutral reference — you want it in the game early so every other attack and every modifier has something to be compared against.
If this roster looks right, my next step is a design brief for the four (dart's DoT subsystem and mines' lifecycle are the two parts with actual design decisions in them — the other two are mostly parameter variations on existing code).
We already have area, let's do normal, shotgun and dart.
Right — normal, shotgun, and dart it is. Let me read the exact source behavior in BYTEPATH.rogue before writing the design brief, since the port is supposed to be faithful.
Grep (shotgun|dart|Dart)
551: when "shotgunner"
1174: @darts = {}
1182: if #@darts > 0
1187: for dart in *@darts
1188: dmg += dart.damage
1490: if @attack == "shotgun"
1543: attacks = {"normal", "shotgun", "cannon", "dart", "shield", "spread", "triple", "pulse", "mines"}
1570: attacks = {"normal", "shotgun", "cannon", "dart", "shield", "spread", "triple", "pulse", "mines"}
1651: mods = {damage_multiplier: @damage_multiplier, pierce: @pierce, finale: (@finale_counter == 5), ricochet: @ricochet, fork: @fork, spawn: @spawn, dart: (@attack == "dart"), pulse: (@attack == "pulse"), mines: (@attack == "mines"), explosion: @explosion, scatter: @scatter, telekinesis: @telekinesis, nova_spawn: @nova_spawn, decaying_shot: @decaying_shot, spark: @spark}
1669: when "shotgun"
1671: mods.shotgun_decel = true
1696: when "dart"
1803: when "shotgun"
1809: when "dart"
2078: table.insert projectiles, EnemyProjectile(@x + @w*cos(@r), @y + @w*sin(@r), rng\float(250, 400)*remap(@mm, 1, 10, 1, 2), @r + rng\float(-pi/8, pi/8), red, {shotgun_decel: true})
2802: if @mods.shotgun_decel
3095: if @mods.dart
3096: table.insert pre_effects, Dart(@x, @y, @r, @w, @h, @color, @damage, other)
3099: if not @mods.dart
3144:class Dart
3147: table.insert @parent.darts, @
3171: if @mods.shotgun_decel
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
1630 when "autocannon"
1631 g.setColor red
1632 g.rectangle "fill", c.x - 3, c.y - 3, 6, 6
1633
1634 shoot: =>
1635 if @attack == "spread"
1636 @sx, @sy = 1.2, 1.2
1637 @timer\tween 0.1, @, {sx: 1, sy: 1}, linear, (-> @sx, @sy = 1, 1)
1638 table.insert effects, ShootEffect(@x + @attack_m*@w/2, @y, 0, @color, @, 6)
1639 table.insert effects, RectangleParticle(@x + @attack_m*@w/2, @y, rng\float(50, 200), @attack_r + rng\float(-math.pi/4, math.pi/4))
1640 else
1641 @sx, @sy = 1.5, 1.5
1642 @timer\tween 0.1, @, {sx: 1, sy: 1}, linear, (-> @sx, @sy = 1, 1)
1643 table.insert effects, ShootEffect(@x + @attack_m*@w/2, @y, 0, @color, @)
1644 for i = 1, 4 do table.insert effects, RectangleParticle(@x + @attack_m*@w/2, @y, rng\float(50, 200), @attack_r + rng\float(-math.pi/4, math.pi/4))
1645
1646 if @finale then @finale_counter += 1
1647 if @nova then @nova_counter += 1
1648 if @barrage then @barrage_counter += 1
1649 if @supershot then @supershot_counter += 1
1650
1651 mods = {damage_multiplier: @damage_multiplier, pierce: @pierce, finale: (@finale_counter == 5), ricochet: @ricochet, fork: @fork, spawn: @spawn, dart: (@attack == "dart"), pulse: (@attack == "pulse"), mines: (@attack == "mines"), explosion: @explosion, scatter: @scatter, telekinesis: @telekinesis, nova_spawn: @nova_spawn, decaying_shot: @decaying_shot, spark: @spark}
1652
1653 if @supershot_counter >= 8
1654 @supershot_counter = 0
1655 mods.supershot = true
1656
1657 switch @attack
1658 when "normal"
1659 if @double
... [170 more lines]
Now the Dart entity, the projectile's dart/shotgun_decel handling, and the enemy-side dart stack logic:
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
3080 g.setColor white
3081
3082 on_collision_enter: (other, contact) =>
3083 if other.__class == Block or other.__class == Shooter or other.__class == Exploder or other.__class == Barrager or other.__class == Gyro or other.__class == Host or other.__class == HostOrbitter or other.__class == Seeker or other.__class == MovingBar or other.__class == Shotgunner or other.__class == Spawner or other.__class == Orbitter
3084 if any(@enemies_hit, other.id) then return
3085
3086 if @mods.pierce then @damage *= 1.1
3087 if @mods.fork
3088 mods = copy @mods
3089 mods.fork = false
3090 timer\after 0.01, (->
3091 table.insert projectiles, Projectile(other.x, other.y, @v, @r - math.pi/4, @color, mods, {other.id})
3092 table.insert projectiles, Projectile(other.x, other.y, @v, @r + math.pi/4, @color, mods, {other.id})
3093 table.insert effects, ProjectileHitEffect(other.x, other.y, nil, @color, other)
3094 )
3095 if @mods.dart
3096 table.insert pre_effects, Dart(@x, @y, @r, @w, @h, @color, @damage, other)
3097 other\light_hit!
3098 if not @mods.pierce then @dead = true
3099 if not @mods.dart
3100 other\hit @damage, @
3101 table.insert effects, ProjectileHitEffect(@x, @y, nil, (other and other.target_color) or red, other)
3102 if @mods.explosion
3103 camera\shake 1, 0.2
3104 table.insert effects, ExplosionEffect(@x, @y, 20*player.area_multiplier)
3105 for enemy in *get_enemies_in_area(@x, @y, 50*player.area_multiplier, 50*player.area_multiplier)
3106 enemy\hit 0.5*@damage, @
3107 if player.area then timer\every 0.25*player.dot_multiplier, (-> enemy\hit 0.5*@damage/5, @), 5
3108 if @mods.decaying_shot
3109 table.insert effects, DecayArea(@x, @y, 20*player.area_multiplier)
... [85 more lines]
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
1165 g.setColor white[1], white[2], white[3], 0.1
1166 g.rectangle "fill", @x - 5, @y - 2, @w + 10, @h + 4
1167 g.setColor white
1168 @info_text\draw!
1169
1170 class Collider
1171 new: =>
1172 @id = rng\uid!
1173 @sx, @sy = 1, 1
1174 @darts = {}
1175 @dot_timer = 0
1176 @dot_cd = 0.5
1177
1178 update: (dt) =>
1179 if @body
1180 @x, @y = @body\getPosition!
1181
1182 if #@darts > 0
1183 @dot_timer += dt
1184 if @dot_timer > @dot_cd*player.dot_multiplier
1185 @dot_timer = 0
1186 dmg = 0
1187 for dart in *@darts
1188 dmg += dart.damage
1189 @\hit dmg
1190
1191 draw: =>
1192 if @body
1193 switch @shape
1194 when "rectangle"
... [10 more lines]
A few more details — light_hit, pre_effects, and the projectile's shotgun_decel branch:
Grep (light_hit|pre_effects)
134: pre_effects = {}
168: update_objects pre_effects, dt
186: draw_objects pre_effects
1249: light_hit: =>
3096: table.insert pre_effects, Dart(@x, @y, @r, @w, @h, @color, @damage, other)
3097: other\light_hit!
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
2764 class Projectile extends Collider
2765 new: (@x, @y, @v, @r, @color=white, @mods, @enemies_hit={}) =>
2766 super!
2767 @timer = Timer!
2768
2769 @damage = 25*(@mods.damage_multiplier or 1)
2770 if @mods.finale then @damage *= 2
2771 if @mods.ricochet then @ricochet_amount = 2
2772 if @mods.scatter then @scatter_amount = @mods.scatter_amount or 3
2773 if @mods.spawn
2774 if @mods.mines
2775 @timer\every rng\float(0.5, 1), (->
2776 mods = copy @mods
2777 mods.damage_multiplier = 0.5*@mods.damage_multiplier
2778 mods.spawn = false
2779 table.insert effects, ProjectileHitEffect(@x, @y, nil, @color)
2780 table.insert projectiles, Projectile(@x, @y, @v, @r - math.pi/2, @color, mods)
2781 table.insert projectiles, Projectile(@x, @y, @v, @r + math.pi/2, @color, mods)
2782 ), 3
2783 else
2784 @timer\every rng\float(0.5, 1), (->
2785 mods = copy @mods
2786 mods.damage_multiplier = 0.5*@mods.damage_multiplier
2787 mods.spawn = false
2788 table.insert effects, ProjectileHitEffect(@x, @y, nil, @color)
2789 table.insert projectiles, Projectile(@x, @y, @v, @r - math.pi/2, @color, mods)
2790 table.insert projectiles, Projectile(@x, @y, @v, @r + math.pi/2, @color, mods)
2791 )
2792
2793 if @mods.mines then @\set_rectangle 4, 4
... [30 more lines]
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
1243 destroy: =>
1244 if @body
1245 @fixture\destroy!
1246 @body\destroy!
1247 @fixture, @body = nil, nil
1248
1249 light_hit: =>
1250 @sx, @sy = 1.15, 1.15
1251 @hit_flash = true
1252 @timer\tween 0.1, @, {sx: 1, sy: 1}, linear, (->
1253 @sx, @sy = 1, 1
1254 @hit_flash = false
1255 ), "hit"
1256
1257 hit: (damage, projectile) =>
1258 if @dead then return
1259
1260 if @__class == MovingBar
1261 @sx, @sy = 1.05, 1.05
1262 else
1263 @sx, @sy = 1.35, 1.35
1264 @hit_flash = true
1265 @timer\tween 0.1, @, {sx: 1, sy: 1}, linear, (->
1266 @sx, @sy = 1, 1
1267 @hit_flash = false
1268 ), "hit"
1269
1270 @hp -= damage
1271 if @hp <= 0
1272 @dead = true
One last check — where pre_effects sits in the draw order:
Read (D:\2020-2021 code\old\BYTEPATH.rogue\main.moon)
160
161 update = (dt) ->
162 aesthetic\send "time", time
163 world\update dt
164 update_objects displacements, dt
165 update_objects players, dt
166 update_objects projectiles, dt
167 update_objects enemies, dt
168 update_objects pre_effects, dt
169 update_objects effects, dt
170 update_objects post_effects, dt
171 update_objects ui, dt
172 update_objects ui_effects, dt
173
174 draw = ->
175 g.setCanvas displacement_canvas
176 g.clear!
177 g.setColor 0.5, 0.5, 0.5, 1
178 g.rectangle "fill", 0, 0, gw, gh
179 draw_objects displacements
180 g.setCanvas!
181
182 g.setCanvas game_canvas
183 g.clear!
184 g.setColor white
185 camera\attach!
186 draw_objects pre_effects
187 draw_objects projectiles
188 draw_objects enemies
189 draw_objects effects
... [10 more lines]
I've got everything I need from the source. Here's the design brief.
🔒 Only the summary of this log is public. Private because this is BYTEPATH++.