!cpu 6502
!to "niteflow.prg", cbm

; ==============================================================================
; NITEFLOW - C128 Native Mode Procedural Screensaver
; 40x25 standard VIC-II, 1MHz mode, 6502 strictly
; ==============================================================================

* = $2000       ; Load address: 8192

; ------------------------------------------------------------------------------
; Zero Page Variables (Safe in C128 user space)
; ------------------------------------------------------------------------------
Ptr             = $10 ; Word
PtrBelow        = $12 ; Word
ScrPtr          = $14 ; Word
ColPtr          = $16 ; Word
BufPtr          = $18 ; Word
CurrentMaterial = $1A
TitlePtr        = $1C ; Word
Seed1           = $1E
Seed2           = $1F
FrameCount      = $20
LastJiffy       = $21
JiffyCount      = $22 ; Word
DrawX           = $24
DrawY           = $25
DrawLen         = $26
RandLimit       = $27
RandMask        = $28
TempCounter     = $29
JumpVector      = $2A ; Word
TempX           = $2C ; 6502 workaround for missing PHX/PLX

; ------------------------------------------------------------------------------
; Memory Map Constants
; ------------------------------------------------------------------------------
Buffer          = $2800     ; 1000 byte physics grid buffer (exactly 1000 used)
ScreenRAM       = $0400     ; Standard C128 Bank 0/15 Screen
ColorRAM        = $D800     ; Standard C128 Color RAM

; ==============================================================================
; ENTRY POINT
; ==============================================================================
!zone StartZone {
Start:
    ; Guarantee 40-column mode on C128
    lda $D7             ; $D7 = $00 for 40-col, $80 for 80-col
    bpl .is_40          ; If positive (bit 7 clear), already 40-col
    jsr $FF5F           ; SWAPPER: Toggles active screen to 40-column
.is_40:

    ; Clear screen using Kernal BSOUT
    lda #147
    jsr $FFD2

    ; Hide C128 editor cursor
    jsr $CD9F

    ; Set colors (Black Border & Background)
    lda #0
    sta $D020
    sta $D021

    ; Initialize 16-bit LFSR Seed (must be non-zero)
    lda $D012
    bne .seed1_ok
    lda #$11
.seed1_ok:
    sta Seed1
    
    lda $DC04
    bne .seed2_ok
    lda #$22
.seed2_ok:
    sta Seed2

    jsr NewScene
}

; ==============================================================================
; MAIN SIMULATION LOOP
; ==============================================================================
!zone MainLoopZone {
MainLoop:
    jsr WaitRaster
    inc FrameCount

    jsr CheckTimer
    bcc .continue_scene
    jsr NewScene
.continue_scene:

    jsr UpdatePhysics
    jsr RenderGrid
    jsr DrawTitle

    jmp MainLoop
}

; ==============================================================================
; SYSTEM SYNC AND TIMING
; ==============================================================================
!zone WaitRasterZone {
WaitRaster:
    ; Phase 1: Wait until we are above the target (protects against high-bit lines)
.wait_top:
    lda $D011
    bmi .wait_top       ; Wait until high bit is clear (lines 0-255)
.wait_low:
    lda $D012
    cmp #250
    bcs .wait_low       ; Wait while raster >= 250

    ; Phase 2: Wait until we precisely reach the target line
.wait_reach:
    lda $D012
    cmp #250
    bcc .wait_reach     ; Wait until raster >= 250
    rts
}

!zone CheckTimerZone {
CheckTimer:
    ; Reads delta from Jiffy Clock low byte to prevent wraparound bugs
    lda $A2
    sta TempX           ; Safely latch jiffy to avoid race conditions
    sec
    sbc LastJiffy
    beq .not_done
    
    ; Accumulate delta
    clc
    adc JiffyCount
    sta JiffyCount
    bcc .no_hi
    inc JiffyCount+1
.no_hi:
    lda TempX
    sta LastJiffy

    ; Safe 16-bit compare: Check if JiffyCount >= 1800 ($0708)
    lda JiffyCount+1
    cmp #$07
    bcc .not_done       ; High < 7
    bne .done           ; High > 7 (must be >= 1800)
    lda JiffyCount
    cmp #$08
    bcc .not_done       ; High == 7 but Low < 8
.done:
    sec
    rts
.not_done:
    clc
    rts
}

; ==============================================================================
; SCENE GENERATOR
; ==============================================================================
!zone NewSceneZone {
NewScene:
    ; 1. Clear Grid Buffer (Exactly 1000 bytes)
    lda #0
    ldy #0
.clr_buf:
    sta Buffer, y
    sta Buffer+256, y
    sta Buffer+512, y
    cpy #232
    bcs .skip_last          ; Do not overwrite beyond 1000 bytes!
    sta Buffer+768, y
.skip_last:
    iny
    bne .clr_buf

    ; 2. Solid Floor (Row 24 = cells 960-999)
    lda #1
    ldy #0
.floor:
    sta Buffer+960, y
    iny
    cpy #40
    bne .floor

    ; 3. Pick Material (Sand or Water)
    lda #2
    ldx #3
    jsr RandomRange
    cmp #0
    beq .is_sand

    ; Setup Water
    lda #3
    sta CurrentMaterial
    lda #<TitleWater
    sta TitlePtr
    lda #>TitleWater
    sta TitlePtr+1
    ; Random water color
    lda #3
    ldx #3
    jsr RandomRange
    tax
    lda Pal_Water, x
    sta ColorTable+3
    jmp .do_walls

.is_sand:
    lda #2
    sta CurrentMaterial
    lda #<TitleSand
    sta TitlePtr
    lda #>TitleSand
    sta TitlePtr+1
    ; Random sand color
    lda #3
    ldx #3
    jsr RandomRange
    tax
    lda Pal_Sand, x
    sta ColorTable+2

.do_walls:
    ; Random wall color
    lda #7
    ldx #7
    jsr RandomRange
    tax
    lda Pal_Wall, x
    sta ColorTable+1

    ; 4. Generate 4 to 7 Obstacles
    lda #4
    ldx #7
    jsr RandomRange
    clc
    adc #4
    sta TempCounter
.obs_loop:
    jsr GenRandomObstacle
    dec TempCounter
    bne .obs_loop

    ; 5. Generate 1 to 3 Sources
    lda #3
    ldx #3
    jsr RandomRange
    clc
    adc #1
    sta TempCounter
.src_loop:
    ; Source X = 5 to 34
    lda #30
    ldx #31
    jsr RandomRange
    clc
    adc #5
    tax
    lda #4
    sta Buffer+40, x    ; Place at Row 1 (offset 40)
    dec TempCounter
    bne .src_loop

    ; 6. Reset Timer
    lda #0
    sta JiffyCount
    sta JiffyCount+1
    lda $A2
    sta LastJiffy
    
    rts
}

; ==============================================================================
; PHYSICS ENGINE
; ==============================================================================
!zone UpdatePhysicsZone {
UpdatePhysics:
    ldx #23                 ; Process from row 23 up to 1
.row_loop:
    ; Set Ptr to Current Row, PtrBelow to Row+1
    lda RowOffsetLow, x
    sta Ptr
    lda RowOffsetHigh, x
    clc
    adc #>Buffer
    sta Ptr+1

    inx                     
    lda RowOffsetLow, x
    sta PtrBelow
    lda RowOffsetHigh, x
    clc
    adc #>Buffer
    sta PtrBelow+1
    dex                     

    ; Alternate X scan direction to prevent directional bias
    lda FrameCount
    and #1
    bne .scan_right_to_left

.scan_left_to_right:
    ldy #0
.col_lr_loop:
    jsr ProcessCell
    iny
    cpy #40
    bne .col_lr_loop
    jmp .next_row

.scan_right_to_left:
    ldy #39
.col_rl_loop:
    jsr ProcessCell
    dey
    cpy #255
    bne .col_rl_loop

.next_row:
    dex
    cpx #0
    beq .done_physics
    jmp .row_loop           ; Use JMP to guarantee branch reach
.done_physics:
    rts
}

!zone ProcessCellZone {
ProcessCell:
    ; Evaluates Buffer cell at Y
    lda (Ptr), y
    beq .done               ; Skip Empty
    bmi .done               ; Skip already MOVED ($80 set)
    cmp #1
    beq .done               ; Skip Wall

    cmp #4
    bne .do_particle
    
    ; Process Source
    tya:pha:txa:pha
    jsr DoSource
    pla:tax:pla:tay
    rts

.do_particle:
    tya:pha:txa:pha
    lda CurrentMaterial     ; Guarantee A is original particle type
    cmp #2
    beq .call_sand
    jsr DoWater
    jmp .end_particle
.call_sand:
    jsr DoSand
.end_particle:
    pla:tax:pla:tay
.done:
    rts
}

; ------------------------------------------------------------------------------
; Source Logic
; ------------------------------------------------------------------------------
!zone DoSourceZone {
DoSource:
    ; Spawns material directly below if empty
    lda (PtrBelow), y
    bne .try_shift
    lda CurrentMaterial
    ora #$80                ; Pre-set MOVED flag for spawn
    sta (PtrBelow), y

.try_shift:
    ; 1 in 32 chance to shift left/right dynamically
    jsr RandomByte
    and #$1F
    bne .done
    
    jsr RandomByte
    and #1
    beq .move_left

.move_right:
    cpy #38
    bcs .done
    iny
    lda (Ptr), y
    bne .fail_right         ; Target cell must be empty!
    dey
    lda #0
    sta (Ptr), y            ; Clear old
    iny
    lda #$84                ; SOURCE | MOVED flag 
    sta (Ptr), y            ; Write new
    rts
.fail_right:
    dey
    rts

.move_left:
    cpy #1
    bcc .done
    dey
    lda (Ptr), y
    bne .fail_left          ; Target cell must be empty!
    iny
    lda #0
    sta (Ptr), y            ; Clear old
    dey
    lda #$84
    sta (Ptr), y            ; Write new
    rts
.fail_left:
    iny
.done:
    rts
}

; ------------------------------------------------------------------------------
; Sand Logic
; ------------------------------------------------------------------------------
!zone DoSandZone {
DoSand:
    ; 1. Check straight down
    lda (PtrBelow), y
    bne .blocked_down
    
    lda CurrentMaterial
    ora #$80
    sta (PtrBelow), y
    lda #0
    sta (Ptr), y
    rts

.blocked_down:
    ; 2. Randomly pick Down-Left or Down-Right first
    jsr RandomByte
    and #1
    bne .try_right_first
    
    jsr CheckDownLeft
    bcc .done
    jsr CheckDownRight
    rts

.try_right_first:
    jsr CheckDownRight
    bcc .done
    jsr CheckDownLeft
.done:
    rts
}

; ------------------------------------------------------------------------------
; Water Logic
; ------------------------------------------------------------------------------
!zone DoWaterZone {
DoWater:
    ; Same initial logic as Sand
    lda (PtrBelow), y
    bne .blocked_down
    
    lda CurrentMaterial
    ora #$80
    sta (PtrBelow), y
    lda #0
    sta (Ptr), y
    rts

.blocked_down:
    jsr RandomByte
    and #1
    bne .try_right_first
    
    jsr CheckDownLeft
    bcc .done_water
    jsr CheckDownRight
    bcc .done_water
    jmp .try_horizontal

.try_right_first:
    jsr CheckDownRight
    bcc .done_water
    jsr CheckDownLeft
    bcc .done_water

.try_horizontal:
    jsr RandomByte
    and #1
    bne .scan_right_first
    
    jsr SearchLeft
    bcc .done_water
    jsr SearchRight
    bcc .done_water
    jmp .normal_spread
    
.scan_right_first:
    jsr SearchRight
    bcc .done_water
    jsr SearchLeft
    bcc .done_water
    
.normal_spread:
    ; Fallback lateral spread if no immediate drops
    jsr RandomByte
    and #1
    bne .spread_right
    jsr CheckLeft
    bcc .done_water
    jsr CheckRight
    rts
.spread_right:
    jsr CheckRight
    bcc .done_water
    jsr CheckLeft
.done_water:
    rts
}

; ------------------------------------------------------------------------------
; Deep Water Searches (Up to 4 cells horizontal lookahead for drops)
; ------------------------------------------------------------------------------
!zone SearchLeftZone {
SearchLeft:
    sty TempCounter
    ldx #4
.sl_loop:
    cpy #0
    beq .sl_fail
    dey
    lda (Ptr), y
    bne .sl_fail            ; Blocked by wall/material
    lda (PtrBelow), y
    beq .sl_found           ; Found a drop hole!
    dex
    bne .sl_loop
.sl_fail:
    ldy TempCounter
    sec
    rts
.sl_found:
    ldy TempCounter
    dey
    lda CurrentMaterial
    ora #$80
    sta (Ptr), y
    iny
    lda #0
    sta (Ptr), y
    clc
    rts
}

!zone SearchRightZone {
SearchRight:
    sty TempCounter
    ldx #4
.sr_loop:
    cpy #39
    beq .sr_fail
    iny
    lda (Ptr), y
    bne .sr_fail            ; Blocked by wall/material
    lda (PtrBelow), y
    beq .sr_found           ; Found a drop hole!
    dex
    bne .sr_loop
.sr_fail:
    ldy TempCounter
    sec
    rts
.sr_found:
    ldy TempCounter
    iny
    lda CurrentMaterial
    ora #$80
    sta (Ptr), y
    dey
    lda #0
    sta (Ptr), y
    clc
    rts
}

; ------------------------------------------------------------------------------
; Directional Movement Checks
; Carry Clear = Success, Carry Set = Blocked
; ------------------------------------------------------------------------------
!zone CheckDownLeftZone {
CheckDownLeft:
    cpy #0
    beq .cdl_fail
    dey
    lda (PtrBelow), y
    bne .cdl_restore
    lda CurrentMaterial
    ora #$80
    sta (PtrBelow), y
    iny
    lda #0
    sta (Ptr), y
    clc
    rts
.cdl_restore:
    iny
.cdl_fail:
    sec
    rts
}

!zone CheckDownRightZone {
CheckDownRight:
    cpy #39
    beq .cdr_fail
    iny
    lda (PtrBelow), y
    bne .cdr_restore
    lda CurrentMaterial
    ora #$80
    sta (PtrBelow), y
    dey
    lda #0
    sta (Ptr), y
    clc
    rts
.cdr_restore:
    dey
.cdr_fail:
    sec
    rts
}

!zone CheckLeftZone {
CheckLeft:
    cpy #0
    beq .cl_fail
    dey
    lda (Ptr), y
    bne .cl_restore
    lda CurrentMaterial
    ora #$80
    sta (Ptr), y
    iny
    lda #0
    sta (Ptr), y
    clc
    rts
.cl_restore:
    iny
.cl_fail:
    sec
    rts
}

!zone CheckRightZone {
CheckRight:
    cpy #39
    beq .cr_fail
    iny
    lda (Ptr), y
    bne .cr_restore
    lda CurrentMaterial
    ora #$80
    sta (Ptr), y
    dey
    lda #0
    sta (Ptr), y
    clc
    rts
.cr_restore:
    dey
.cr_fail:
    sec
    rts
}

; ==============================================================================
; RENDER ENGINE
; ==============================================================================
!zone RenderGridZone {
RenderGrid:
    lda #<ScreenRAM
    sta ScrPtr
    lda #>ScreenRAM
    sta ScrPtr+1
    
    lda #<ColorRAM
    sta ColPtr
    lda #>ColorRAM
    sta ColPtr+1
    
    lda #<Buffer
    sta BufPtr
    lda #>Buffer
    sta BufPtr+1

    ; Render exactly 1000 cells (3 full pages + 232 bytes)
    ldx #3
    ldy #0
.page_loop:
    jsr RenderCell
    iny
    bne .page_loop
    inc ScrPtr+1
    inc ColPtr+1
    inc BufPtr+1
    dex
    bne .page_loop

.partial_loop:
    jsr RenderCell
    iny
    cpy #232
    bne .partial_loop
    rts
}

!zone RenderCellZone {
RenderCell:
    lda (BufPtr), y
    and #$7F                ; Strip MOVED flag
    sta (BufPtr), y         ; Write cleaned state back to buffer
    
    stx TempX               ; Use Zero Page TempX instead of C02 PHX
    tax                     ; A -> X (Contains Material Type)
    lda CharTable, x
    sta (ScrPtr), y
    lda ColorTable, x
    sta (ColPtr), y
    ldx TempX               ; Restore X instead of PLX
    rts
}

!zone DrawTitleZone {
DrawTitle:
    ldy #0
.loop:
    lda (TitlePtr), y
    beq .done
    sta ScreenRAM, y
    lda #1                  ; White text
    sta ColorRAM, y
    iny
    jmp .loop
.done:
    rts
}

; ==============================================================================
; PROCEDURAL OBSTACLE GENERATION
; ==============================================================================
!zone GenRandomObstacleZone {
GenRandomObstacle:
    lda #5
    ldx #7
    jsr RandomRange
    asl
    tax
    lda ObsJumpTable, x
    sta JumpVector
    lda ObsJumpTable+1, x
    sta JumpVector+1
    jmp (JumpVector)
}

ObsJumpTable:
    !word DoGenHoriz
    !word DoGenVert
    !word DoGenRampL
    !word DoGenRampR
    !word DoGenBowl

!zone DoGenHorizZone {
DoGenHoriz:
    lda #30:ldx #31:jsr RandomRange:clc:adc #5:sta DrawX
    lda #15:ldx #15:jsr RandomRange:clc:adc #5:sta DrawY
    lda #10:ldx #15:jsr RandomRange:clc:adc #5:sta DrawLen
.loop:
    jsr PutWall
    inc DrawX
    dec DrawLen
    bne .loop
    rts
}

!zone DoGenVertZone {
DoGenVert:
    lda #34:ldx #63:jsr RandomRange:clc:adc #3:sta DrawX
    lda #10:ldx #15:jsr RandomRange:clc:adc #5:sta DrawY
    lda #8:ldx #7:jsr RandomRange:clc:adc #3:sta DrawLen
.loop:
    jsr PutWall
    inc DrawY
    dec DrawLen
    bne .loop
    rts
}

!zone DoGenRampLZone {
DoGenRampL:
    lda #30:ldx #31:jsr RandomRange:clc:adc #3:sta DrawX
    lda #12:ldx #15:jsr RandomRange:clc:adc #5:sta DrawY
    lda #8:ldx #7:jsr RandomRange:clc:adc #4:sta DrawLen
.loop:
    jsr PutWall
    inc DrawX
    inc DrawY
    dec DrawLen
    bne .loop
    rts
}

!zone DoGenRampRZone {
DoGenRampR:
    lda #30:ldx #31:jsr RandomRange:clc:adc #5:sta DrawX
    lda #12:ldx #15:jsr RandomRange:clc:adc #10:sta DrawY
    lda #8:ldx #7:jsr RandomRange:clc:adc #4:sta DrawLen
.loop:
    jsr PutWall
    inc DrawX
    dec DrawY
    dec DrawLen
    bne .loop
    rts
}

!zone DoGenBowlZone {
DoGenBowl:
    lda #25:ldx #31:jsr RandomRange:clc:adc #5:sta DrawX
    lda #12:ldx #15:jsr RandomRange:clc:adc #8:sta DrawY
    
    lda DrawX:pha
    lda DrawY:pha
    lda #6:sta DrawLen
.loop1:
    jsr PutWall
    inc DrawX
    dec DrawLen
    bne .loop1
    
    pla:sec:sbc #3:sta DrawY
    pla:sta DrawX
    lda #4:sta DrawLen
.loop2:
    jsr PutWall
    inc DrawY
    dec DrawLen
    bne .loop2
    
    lda DrawX:clc:adc #5:sta DrawX
    lda DrawY:sec:sbc #4:sta DrawY
    lda #4:sta DrawLen
.loop3:
    jsr PutWall
    inc DrawY
    dec DrawLen
    bne .loop3
    rts
}

!zone PutWallZone {
PutWall:
    ; Absolute safe bounds checker - protects rows 0 & 1 and columns outside 0-39
    lda DrawX
    bmi .skip               ; Reject if < 0
    cmp #40
    bcs .skip               ; Reject if >= 40
    lda DrawY
    cmp #2
    bcc .skip               ; Reject if < 2 (protects Title and Sources)
    cmp #24
    bcs .skip               ; Reject if >= 24

    ; Calculate robust address
    ldy DrawY
    lda RowOffsetLow, y
    clc
    adc DrawX
    sta BufPtr
    lda RowOffsetHigh, y
    adc #>Buffer
    sta BufPtr+1

    ldy #0
    lda #1                  ; WALL material
    sta (BufPtr), y
.skip:
    rts
}

; ==============================================================================
; UTILITY & DATA
; ==============================================================================
!zone RandomByteZone {
RandomByte:
    ; Proven 16-bit Galois LFSR for 6502 (Tap mask $B400 for 16, 14, 13, 11)
    lda Seed2
    lsr
    sta Seed2
    lda Seed1
    ror
    sta Seed1
    bcc .no_eor
    lda Seed2
    eor #$B4
    sta Seed2
.no_eor:
    lda Seed1
    eor Seed2
    rts
}

!zone RandomRangeZone {
RandomRange:
    ; Input: A = Limit, X = Mask (Closest power of 2 minus 1)
    sta RandLimit
    stx RandMask
.retry:
    jsr RandomByte
    and RandMask
    cmp RandLimit
    bcs .retry
    rts
}

; Lookup Tables ----------------------------------------------------------------
RowOffsetLow:
    !byte <(0*40), <(1*40), <(2*40), <(3*40), <(4*40), <(5*40), <(6*40), <(7*40)
    !byte <(8*40), <(9*40), <(10*40),<(11*40),<(12*40),<(13*40),<(14*40),<(15*40)
    !byte <(16*40),<(17*40),<(18*40),<(19*40),<(20*40),<(21*40),<(22*40),<(23*40)
    !byte <(24*40)

RowOffsetHigh:
    !byte >(0*40), >(1*40), >(2*40), >(3*40), >(4*40), >(5*40), >(6*40), >(7*40)
    !byte >(8*40), >(9*40), >(10*40),>(11*40),>(12*40),>(13*40),>(14*40),>(15*40)
    !byte >(16*40),>(17*40),>(18*40),>(19*40),>(20*40),>(21*40),>(22*40),>(23*40)
    !byte >(24*40)

CharTable:
    !byte 32    ; 0: Empty (Space)
    !byte 160   ; 1: Wall (Solid Block)
    !byte 46    ; 2: Sand (Period / Grain)
    !byte 247   ; 3: Water (Waves)
    !byte 209   ; 4: Source (Circle/Ball)

ColorTable:
    !byte 0     ; 0: Empty (Black)
    !byte 5     ; 1: Wall (Overwritten dynamically)
    !byte 7     ; 2: Sand (Overwritten dynamically)
    !byte 14    ; 3: Water (Overwritten dynamically)
    !byte 1     ; 4: Source (White)

Pal_Wall:
    !byte 1, 3, 4, 5, 6, 10, 13
Pal_Sand:
    !byte 7, 8, 1
Pal_Water:
    !byte 3, 14, 6

TitleSand:
    !text "     N I T E F L O W // S A N D         ", 0
TitleWater:
    !text "    N I T E F L O W // W A T E R        ", 0
