• entries
    104
  • comments
    101
  • views
    256097

About this blog

Musings of a hobbyist

Entries in this blog

I'm currently attending yet another C64 game creation competition: Forum64.de's Adventure Competition 2015.

I always wondered how the guys/gals at Lucasfilm Games managed to cram Maniac Mansion and Zak McKracken onto a few small disks. So this competition was the perfect opportunity to test drive my ideas.


How they did it

Fortunately today the internet provides all kind of technical info at my finger tips. So it's no problem finding some of the old programmers spilling their beans. They actually created a Lisp like language that allowed them scripting even with a kind of multi threading. Incredible! Obviously the script was compiled to byte code which barely fit into memory. So that's the way I chose to follow.
Fun link of the day: Ron Gilbert on Scumm (http://grumpygamer.com/scumm_notes)


How I faked it

Tables. Tables with pointers to more tables. All verbs are represented by indices; so are items and static objects (items on screen)
Every item/object has a list of verbs to scripts. Item 5 with verb 7? Run this script. No entry for object 14 and verb 9? Show the dreaded "That doesn't work."

And there's action areas with type "room exit" or "script trigger". Room exits are handled directly while script triggers start their attached script.


Where's the beef?

It's in the script! All puzzles are handled by various script actions. A script action can set/clear flags, run a different script depending on a flag state. Characters can be "walked", get or lose an item, branch if a certain combination of items/objects is used, if the controlled character is anybody specific, etc.

This script together with rooms made up of elements (of which some may depend on certain flag states) makes for a quite small memory footprint. At the current state there's 65 flags, 39 rooms, 55 static objects and 18 items making up a total of 30355 bytes (graphics included).

Since that's all pretty abstract, here's a few excerpts.


Action table for a static object (badge reader in elevator)
SOA_BADGE_BUTTON_IN_LIFT !byte PA_EXAMINE, CS_EXAMINE_BADGE_BUTTON !byte PA_LAST_ENTRY_FLAG | PA_USE, CS_USE_BADGE_BUTTON_IN_LIFT
Starts a sequence when a button is used in the elevator, referenced in the table above

CS_USE_BADGE_BUTTON_IN_LIFT !byte CSA_SET_HAPPEN,2 !byte CSA_REDRAW_SCREEN !byte CSA_WAIT,50 !byte CSA_IF_HAPPEN_SET,4,CS_ACCIDENT !byte CSA_CLEAR_HAPPEN,2 !byte CSA_TOGGLE_HAPPEN,3 !byte CSA_LAST_ENTRY_FLAG | CSA_REDRAW_SCREEN
Post Mortem to "Building Blocks"

shot5.png

So I entered the Week of Awesome II competition last week. I simply love this tight deadline competitions. And I couldn't help it again.
The theme came up as "The Toys are alive". Yay.

Usually I don't jump into coding right away to think about the game to be. And as usual I always go for the nearest idea. It's very rare that I try to outwit the theme and do something really creative. And so it was decided, a simple Lemmings style game, with tin soldiers walking about building blocks.

The Good

Level design

The common start is to think of how to keep the level in memory. The base of the level is tile based. I did think of bigger blocks right away, and this worked out fine after a few tries. This led me to the next point pretty fast, the


Level editor

shot6.png

Esp. for these competitions, when I go level based, I need a way to churn out levels very fast. Since this competition lasted for a week I went out of my way to make a decent editor. It was a bit more work in the beginning, but helped me tremendously during the last two days to play with level design and add some more stages. I wouldn't have managed 10 stages without it.


Base Library

For Ludum Dare we have to provide the full source. This makes me hesitate to actually use my home grown engine and I work with a very simple cut down framework. For the GameDev compo I can use my full framework and it does help quite a lot. I've got game assets, GUI and game states from the start, and especially, I'm comfortable with it.


Generated Music

Previous to the last Ludum Dare I was on the search for music generators once more. I encountered cgMusic, which made some really nice songs. It's a bit piano centric, and I feel that some song parts to repeat, but it was good enough for me. Adding a midi to ogg converter I could generate songs in a few minutes.


Early Preview

As suggested I put up an early version. And it was a good idea. I had some very valuable feedback, as there were a few things I didn't even think of. I think I did add all suggestions.


The Bad

Collision system

The sloped tiles were made out as a first issue to tackle. For some reason I thought using polygons would work out nice. Since I have a math library with polygon collision code I wrote polygon creators for the different blocks. Collision with a unit's bounds rectangle (=polygon) worked very nicely.
However problems appeared when I actually implemented the movement system. I had my units move in pixel units. Obviously mixing pixel based movement with "exact" mathematics (think of the slope of a diagonal block) does not work out. Units fell one pixel, ended up inside the block polygon and were stuck.

I ended up adding a simple hack: return the pixel based height of a tile depending on the x position in the tile. Ugly, but worked.


Ugly bug

When I had the first preview out someone mentioned that the game crashes on exit. I despise things like that (Gamemaker games, I'm looking at you). In the end the real bug was a bug in my level block handling. I managed to access out of bounds memory and as usual this crashed at a totally unrelated position. I had the luck that some code changes led to the bug appearing right away after the wrong access. It took a few seconds to fix, but I noticed that due to that bug some stages had broken data saved.

I added a simple hack: On loading levels I auto fixed these broken parts.



The Ugly

Windows 7 auto volume adjustment

Windows 7 does some automatic volume adjusting. If there's only very low volume music, it turns it up. But if the music suddenly gets louder it gets adjusted down. I do understand the reasoning behind this feature, but it doesn't lend itself to gaming too well. I wish there was a way for my game to tell Windows to not mess with my volume.

This feature is really really annoying. I'm not entirely sure how to work around it beside starting to play a loud sound effect at the start.



TL;DR

As final thought I'm pretty happy how the game turned out. It's not my best, but also not my worst. I wish I had more stages, as the first 5 are simple tutorials. At least the last two stages are a bit of a puzzle though.


Soooo, when does the next compo start?

And thus this create a game series ends...

We fix the last few bugs (score was not properly reset on replay, music was stuck on saving the scores). Now the game is complete and can be enjoyed as it was meant to be!

step100.png

Oh joy, Smila (the graphician) actually touched up the game even more for a retail release. It has been out for a few weeks now, but if you didn't know, go here:
Psytronik (for digital download, tape or disk) or to RGCD (cartridge with nifty packaging and stickers).
Or simply marvel in all the nice games that are still being made for older computers.

There you can actually buy a severely enhanced version (more two player game modes, better graphic, gameplay enhancements) as either digital download for emulators, or as a real tape, disk and finally as cartridge!

Thank you for your encouragements throughout, and keep on coding ;)

step100.zip

Previous Step

 

And here's the final bit, the extro part. Never have the player play through your whole game and put "Game Over" there. At the minimum a nice message is required :)

And yes, it's symbolic at 98, since there's always some bugs left to fix. The last two steps will be mostly bug fixes.

Since it's the extro (and not even particularely impressing) I'll refrain to show the screenshot (and the code update) ;)

Just for explanation, to hide the sprite behind some chars there's a priority bit. This is used to hide the main object when it goes behind the hills.

step98.zip

Previous Step Next Step

 

And here's a little gameplay update, the bats. The diagonal movement was too predictable, so now there's more randomness to it.

step97.png

The bat will move in curves. On every end of a curve the new direction will be decided randomly. Two tables are enough, however due to the C64 using two bit complement negative values are annoying to handle. Therefor I went the naive route and simply added code for every case. Ugly, but it works :)

;------------------------------------------------------------
;simply move diagonal
;------------------------------------------------------------
!zone BehaviourBatDiagonal
BehaviourBatDiagonal 
          jsr HandleHitBack 
          beq .NoHitBack 
          
          rts
          
.RandomDir 
          jsr GenerateRandomNumber 
          and #$07 
          sta SPRITE_DIRECTION,x 
          inc SPRITE_DIRECTION,x 
          lda #0 
          sta SPRITE_MOVE_POS,x 
          rts
          
.NoHitBack 
          lda DELAYED_GENERIC_COUNTER 
          and #$03 
          bne .NoAnimUpdate 
          inc SPRITE_ANIM_POS,x 
          lda SPRITE_ANIM_POS,x 
          and #$03 
          sta SPRITE_ANIM_POS,x 
          tay 
          lda BAT_ANIMATION,y 
          sta SPRITE_POINTER_BASE,x 
          
.NoAnimUpdate 
          lda SPRITE_DIRECTION,x 
          beq .RandomDir 
          cmp #1 
          beq .MoveCCWWN 
          cmp #2 
          beq .MoveCCWSW 
          cmp #3 
          beq .MoveCCWES 
          cmp #4 
          beq .MoveCCWNE 
          cmp #5 
          bne + 
          
          jmp .MoveCWWS
          
+ 
          cmp #6 
          bne + 
          
          jmp .MoveCWNW
          
+ 
          cmp #7 
          bne + 
          
          jmp .MoveCWEN
          
+ 
          cmp #8
          bne + 
          
          jmp .MoveCWSE
          
+ 
.NextStep 
          inc SPRITE_MOVE_POS,x 
          lda SPRITE_MOVE_POS,x 
          cmp #16 
          bne + 
          
          lda #0 
          sta SPRITE_DIRECTION,x 
          sta SPRITE_MOVE_POS,x
          
+ 
          rts 
          
.MoveCCWWN 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveUp 
          
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveRight 
          jmp .NextStep
          
.MoveCCWSW 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveLeft 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveUp 
          jmp .NextStep
          
.MoveCCWES 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveDown 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveLeft 
          jmp .NextStep
          
.MoveCCWNE 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveRight 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveDown 
          jmp .NextStep
          
.MoveCWWS 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveDown 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveRight 
          jmp .NextStep
          
.MoveCWNW 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveLeft 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveDown 
          jmp .NextStep
          
.MoveCWEN 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveUp 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveLeft 
          jmp .NextStep
          
.MoveCWSE 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE,y 
          sta PARAM3 
          jsr .TryMoveRight 
          ldy SPRITE_MOVE_POS,x 
          lda PATH_CURVE_R,y 
          sta PARAM3 
          jsr .TryMoveUp 
          jmp .NextStep
          
.Blocked 
          lda #0 
          sta SPRITE_DIRECTION,x 
          rts
          
.TryMoveUp 
          beq + 
          jsr ObjectMoveUpBlocking 
          beq .Blocked 
          
          dec PARAM3 
          jmp .TryMoveUp 
          
+ 
          rts 
          
.TryMoveDown 
          beq + 
          jsr ObjectMoveDownBlocking 
          beq .Blocked 
          dec PARAM3 
          jmp .TryMoveDown 
          
+ 
          rts 
          
.TryMoveLeft 
          beq + 
          jsr ObjectMoveLeftBlocking 
          beq .Blocked 
          
          dec PARAM3 
          jmp .TryMoveLeft 
          
+ 
          rts 
          
.TryMoveRight 
          beq + 
          jsr ObjectMoveRightBlocking 
          beq .Blocked 
          
          dec PARAM3 
          jmp .TryMoveRight 
          
+ 
          rts



The table are simple delta updates per frame, one being the reverse of the other:

PATH_CURVE 
          !byte 0 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1
          
PATH_CURVE_R 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 1 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 0 
          !byte 1 
          !byte 0 
          !byte 0


The other thing is a little fix for a bug I found with the vanishing bats for the last portal stages. The bats could appear outside the playing area. The fix are two border values for left and right which will be set to be farther inside the screen if a portal level is run:

          ;set default 
          lda #10 
          sta SPAWN_LEFT_BORDER 
          lda #30 
          sta SPAWN_RIGHT_BORDER 
          
          ;adjust spawn border on portal level 
          lda LEVEL_CONFIG 
          and #$04 
          beq + 
          
          lda #15 
          sta SPAWN_LEFT_BORDER 
          lda #25 
          sta SPAWN_RIGHT_BORDER
          
+


...and adjust the spawn code inside BehaviourBatVanishing:

          ;position diagonal above/below player 
          lda SPRITE_CHAR_POS_X 
          cmp #SPAWN_LEFT_BORDER 
          
          ;was #10 
          bcc .SpawnOnRight 
          cmp #SPAWN_RIGHT_BORDER 
          
          ;was #30 
          bcs .SpawnOnLeft


step97.zip

Previous Step Next Step

 

And a nice little update, Richard Bayliss added sounds effects. Now there's a SFX mode, toggle in the title screen with left/right.

step96.png

The effects are integrated in the player code as separate "songs". So we add a variable SFX_MODE and check it's value when we want to play an effect or start the music:

No music in title when SFX mode enabled:

          ;initialise music player 
          ldx #0 
          ldy #0 
          lda SFX_MODE 
          bne + 
          
          lda #MUSIC_TITLE_TUNE 
          jsr MUSIC_PLAYER


Play an effect is similar:

          lda SFX_MODE 
          beq + 
          
          lda #MUSIC_PICKUP 
          jsr MUSIC_PLAYER
          
+ 

 

To toggle sfx mode move the joystick left/right in the title screen and display its state:

          ;switch through music/sfx mode 
          lda #$04 
          bit JOYSTICK_PORT_II 
          bne .NotLeftPressed 
          
          lda LEFT_RELEASED 
          beq .LeftPressed 
          
          lda SFX_MODE 
          eor #$01 
          sta SFX_MODE 
          jsr DisplaySfxMode 
          
          lda SFX_MODE 
          beq + 
          
          lda #MUSIC_PICKUP 
          jmp ++
          
+ 
          lda #MUSIC_TITLE_TUNE
++ 
          jsr MUSIC_PLAYER 
          lda #0 
          jmp .LeftPressed
          
.NotLeftPressed 
          lda #1
          
.LeftPressed 
          sta LEFT_RELEASED 
          lda #$08 
          bit JOYSTICK_PORT_II 
          bne .NotRightPressed 
          
          lda RIGHT_RELEASED 
          beq .RightPressed 
          
          lda SFX_MODE 
          eor #$01 
          sta SFX_MODE 
          jsr DisplaySfxMode 
          
          lda SFX_MODE 
          beq + 
          
          lda #MUSIC_PICKUP 
          jmp ++
          
+ 
          lda #MUSIC_TITLE_TUNE
++ 
          jsr MUSIC_PLAYER 
          lda #0 
          jmp .RightPressed
          
.NotRightPressed 
          lda #1
          
.RightPressed 
          sta RIGHT_RELEASED 
          
!zone DisplaySfxMode
DisplaySfxMode 
          lda SFX_MODE 
          bne + 
          
          lda #TEXT_MUSIC 
          jmp .DisplaySfxMode
          
+ 
          lda #TEXT_SFX
          
.DisplaySfxMode 
          sta ZEROPAGE_POINTER_1 + 1 
          lda #34 
          sta PARAM1 
          lda #24 
          sta PARAM2 
          jmp DisplayText

Due to technical limitations in the player code there are not too many sounds, but there are enough to make it worthwile.

step96.zip

Previous Step Next Step

 

Now a little update that adds a change that was long overdue: Zombies do not wake up all of a sudden, but peek out of the ground before. Now players should be able to escape if they're keeping an eye out.

step95.png


We change the .WakeUp part of BehaviourZombie to show up, look left/right a few times and only then rise, like good zombies do:

          ;only animate head to warn player 
          inc SPRITE_MOVE_POS,x 
          lda SPRITE_MOVE_POS,x 
          cmp #20 
          beq .ReallyWakeUp 
          
          and #$07 
          bne ++ 
          
          ;show head 
          lda SPRITE_DIRECTION,x 
          eor #1 
          sta SPRITE_DIRECTION,x 
          lda #SPRITE_ZOMBIE_COLLAPSE_R_2 
          clc 
          adc SPRITE_DIRECTION,x 
          sta SPRITE_POINTER_BASE,x
          
++ 
          rts 
.ReallyWakeUp


Also, having the spawn animation playing but then "appearing" underground is awkward the start state of zombies is now fully alive. Which is simply a change in the TYPE_START_STATE table.

That was simple now :)

step95.zip

Previous Step Next Step

 

Now we have a real two player coop mode. It's not both players playing by themselves, but true teamwork. Sam needs to capture, and only then Dean can shoot enemies.

step94.png

To makes things easier we add a new flag to check if two player mode is active (TWO_PLAYER_MODE_ACTIVE):

          ;set two player mode active flag 
          lda #0 
          sta TWO_PLAYER_MODE_ACTIVE 
          lda GAME_MODE 
          cmp #GT_COOP 
          bne + 
          
          inc TWO_PLAYER_MODE_ACTIVE
          
+


Remove the flag if one of the player dies on his last life:

.OtherPlayerStillAlive 
          ;remove 2 player active flag 
          lda #0 
          sta TWO_PLAYER_MODE_ACTIVE 
          jsr RemoveObject 
          rts


If Sam is firing away and the enemy would be hurt, we bail out if our flag is set. The flag set means having a value of 0. So beq actually checks if it is not set:

          ;Sam needs to keep pressed 
          jsr RedrawForceBeam 
          
          ldy SPRITE_HELD 
          dey 
          ldx SPRITE_ACTIVE,y 
          lda IS_TYPE_ENEMY,x 
          cmp #1 
          bne .NormalHurtByForce 
          
          ;in 2p mode? 
          lda TWO_PLAYER_MODE_ACTIVE 
          beq .NormalHurtByForce 
          
          ;no further action 
          jmp .NoEnemyHeld 
          
.NormalHurtByForce


In Deans shot routine we check if the enemy is held, if not bail out:

.EnemyHit 
          ;enemy hit! 
          ;is two player enemy? 
          ldy SPRITE_ACTIVE,x 
          lda IS_TYPE_ENEMY,y 
          cmp #1 
          bne .HitEnemy 
          
          ;in 2p mode? 
          lda TWO_PLAYER_MODE_ACTIVE 
          beq .HitEnemy 
          
          ;is the player held? 
          ldy SPRITE_HELD 
          dey 
          sty PARAM1 
          cpx PARAM1 
          beq .HitEnemy 
          
          ;enemy would be hit, but is not held 
          jmp .ShotDone 
          
.HitEnemy 


Simple addon, but bound to get complicated :)

step94.zip

Previous Step Next Step

 

An addon to make the other bosses a bit stronger against Sam. Now you need to re-grab the boss after every hit you place.
Previously Sam only had to stand there and keep fire pressed. Hardly a challenge :)

step93.png

First of all, a new enemy type is added. Normal enemies stay type 1, bosses are now type 3.

Most changes are in the FireSam routine. Add in the PLAYER_FIRE_RELEASED check:

.FireSam 
          ldy PLAYER_JOYSTICK_PORT,x 
          lda JOYSTICK_PORT_II,y 
          and #$10 
          beq + 
          
          ;not fire pressed 
          lda #1 
          sta PLAYER_FIRE_RELEASED,x 
          jmp .SamNotFirePushed 
          
+ 
          lda #1 
          sta PLAYER_FIRE_PRESSED_TIME,x 
          stx PARAM6 
          lda SPRITE_HELD 
          bne .NoFireReleasedCheck 
          
          jsr SamUseForce 
          beq .NoEnemyHeld 
          
          ldx CURRENT_INDEX 
          lda PLAYER_FIRE_RELEASED,x 
          bne + 
          
          jmp .SamNotFirePushed
          
+ 
          lda #0 
          sta PLAYER_FIRE_RELEASED,x
          
.NoFireReleasedCheck



In the enemy hurt routine we check if a boss was hurt (by checking the type). If it is, release the enemy from the force grip:

          ;enemy was hurt 
          ldy SPRITE_HELD 
          dey 
          lda SPRITE_ACTIVE,y 
          tay 
          lda IS_TYPE_ENEMY,y 
          cmp #3 
          
          ;if boss, auto-release 
          bne + 
          
          lda #0 
          sta PLAYER_FIRE_RELEASED,x 
          jmp .SamNotFirePushed 
          
+

Have fun!

step93.zip

Previous Step Next Step

 

Poor Sam was left out again. Now he can kill the boss too.

step92.png

Since the boss is a special beast you wouldn't want Sam just to stand there and kill him without any reaction.

We add a new variable BOSS_HELD, similar to the SPRITE_HELD value.
So if SAM hurts the enemy, and it's the boss, the boss is released from Sam's grip:

          dec SPRITE_HP,x 
          beq .EnemyKilled 
          
          ;enemy was hurt 
          lda BOSS_HELD 
          beq .EnemyWasHurt 
          
          ;release if end boss 
          jmp .SamNotFirePushed



BOSS_HELD is set to 1 if Sam has the boss in his force grip. We check if the sprite caught is the last boss or one of his parts:

.EnemyHit 
          ;enemy hit! 
          stx SPRITE_HELD 
          ldy SPRITE_HELD 
          inc SPRITE_HELD 
          lda SPRITE_ACTIVE,y 
          cmp #TYPE_BOSS7 
          beq .HoldingBoss 
          
          cmp #TYPE_BOSS_PART 
          beq .HoldingBoss 
          
          jmp .NotHoldingBoss 
          
.HoldingBoss 
          sty BOSS_HELD 
          inc BOSS_HELD
          
.NotHoldingBoss


Therefore we also need to clear the bit in case the enemy or Sam is killed:

!zone KillEnemy
KillEnemy 
          ;is the enemy currently held? 
          ldy SPRITE_HELD 
          dey 
          sty PARAM4 
          cpx PARAM4 
          bne .WasNotHeld 
          
          lda #0 
          sta SPRITE_HELD 
          sta BOSS_HELD


Obviously the boss should not move when being caught, so in BehaviourBoss7 we add an early bail out:

.NoHitBack 
          lda BOSS_HELD 
          beq + 
          rts
          
+


The step also adds a few bug fixes, as in the boss not auto-moving the bats he spawned (as if they were body parts).

Have fun!

step92.zip

Previous Step Next Step

 

Aaaand the torso gets to fight back too, not only sit put.


step91.png


Time to reuse existing code again. The torso will spit out bats just like the last two bossed did. SPRITE_MODE_POS is used to stop the attacking mode and revert back to movement.

A contains the number of boss parts killed (so 4 = 2 legs plus 2 arms):

          cmp #4 
          bne + 
          
          ;attack with bats 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM1 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #4 
          sta PARAM2 
          inc PARAM2 
          stx PARAM10 
          jsr GenerateRandomNumber 
          and #$01 
          beq .NoBatLeft 
          
          jsr FindEmptySpriteSlot 
          beq ++ 
          
          lda #TYPE_BAT_ATTACKING 
          sta PARAM3 
          jsr SpawnObject 
          lda #0 
          sta SPRITE_DIRECTION,x
          
.NoBatLeft 
          jsr GenerateRandomNumber 
          and #$01 
          beq .NoBatRight 
          
          jsr FindEmptySpriteSlot 
          beq ++ 
          
          jsr SpawnObject 
          lda #1 
          sta SPRITE_DIRECTION,x
          
++ 
.NoBatRight 
          ldx CURRENT_INDEX 
          lda SPRITE_MODE_POS,x 
          cmp #20 
          bne +++ 
          
          dec SPRITE_STATE,x 
          lda #0 
          sta SPRITE_MODE_POS,x
          
+++ 
          rts
+



That's all there is for this step :)

step91.zip

Previous Step Next Step

 

And the boss got a bit more lively (visually), it was quite stiff previously. Moving body parts and the head screams (also visually) when getting hurt.

step90.png


The boss helper code is enhanced by a routine doing circling movements:

          jsr GenerateRandomNumber
          and #$03
          bne .DoY

          inc SPRITE_MOVE_POS,x
          lda SPRITE_MOVE_POS,x
          and #$0f
          sta SPRITE_MOVE_POS,x
          ldy SPRITE_MOVE_POS,x
          lda BOSS_DELTA_TABLE_X,y
          beq .DoY
          sta PARAM1

          bmi .Left

.Right
          jsr MoveSpriteRight
          dec PARAM1
          bne .Right
          jmp .DoY

.Left
          jsr MoveSpriteLeft
          inc PARAM1
          bne .Left

.DoY
          jsr GenerateRandomNumber
          and #$03
          bne .Done
          inc SPRITE_MOVE_POS_Y,x
          lda SPRITE_MOVE_POS_Y,x
          and #$0f
          sta SPRITE_MOVE_POS_Y,x
          ldy SPRITE_MOVE_POS_Y,x
          lda BOSS_DELTA_TABLE_Y,y
          beq .Done
          sta PARAM1

          bmi .Up

.Down
          jsr MoveSpriteDown
          dec PARAM1
          bne .Down
          jmp .Done

.Up
          jsr MoveSpriteUp
          inc PARAM1
          bne .Up

.Done
          rts


Plus a rather simple swinging table:

BOSS_DELTA_TABLE_X
          !byte 0, 1, 0, 1, 1, 0, 1, 0
BOSS_DELTA_TABLE_Y
          !byte 0, $ff, 0, $ff, $ff, 0, $ff, 0
          !byte 0, 1, 0, 1, 1, 0, 1, 0

 


To make the boss' head scream we adjust its hurt routine:

 

;------------------------------------------------------------
;hit behaviour getting hurt
;------------------------------------------------------------
!zone HitBehaviourBoss7
HitBehaviourBoss7
          lda #SPRITE_BOSS_HEAD_HURT
          sta SPRITE_POINTER_BASE,x
          jmp HitBehaviourHurt

 

step90.zip


Previous Step Next Step

 

 

Now you can kill the last part. Beware, it will fight back though!



step89.png


All kind of changes are added to the boss 7 behaviour routine, as it now gets a new state. First of all, handle getting hit

;------------------------------------------------------------
;boss #7
;state = 0, 128 -> random movements
;state = 129 -> attack with beams
;------------------------------------------------------------
!zone BehaviourBoss7
BehaviourBoss7
BOSS_MOVE_SPEED = 1 
          lda SPRITE_HITBACK,x 
          beq .NoHitBack 
          
          dec SPRITE_HITBACK,x 
          ldy SPRITE_HITBACK,x 
          lda BOSS_FLASH_TABLE,y 
          sta VIC_SPRITE_COLOR,x 
          cpy #0 
          bne .NoHitBack 
          
          ;make vulnerable again 
          lda #0 
          sta SPRITE_STATE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
.NoHitBack 
          lda SPRITE_STATE,x 
          beq .RandomMovements 
          
          cmp #1 
          beq + 
          
          cmp #129 
          beq + 
          
          jmp .RandomMovements 
          
+ 


In AttackWithBeams we add a special case when all body parts have been destroyed:

          lda BOSS_PARTS_KILLED 
          cmp #5 
          bne + 
          
          jmp FinalAttack 
+ 


FinalAttack is the meat of the new part. Shooting a beam and rotating it over the screen! (Subtly reusing existing beam helper routines)

!zone FinalAttack
.BeamStep1 
          ;left arm 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM3 
          lda #0 
          sta PARAM1 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithYPosH 
          ldy #1 
          jsr CheckIsPlayerCollidingWithYPosH 
          
          ldy #BEAM_TYPE_DARK 
          lda BEAM_CHAR_H,y 
          sta PARAM1 
          lda #1 
          sta PARAM2 
          lda #0 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #2 
          sta PARAM5 
          jsr DrawBeamHSegment 
          rts 
          
.BeamStep1End 
          ;remove beam 
          lda #0 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          lda #39 
          sta PARAM5 
          jsr RestoreBeamHSegment 
          jsr RedrawItems 
          rts
          
.BeamStep2 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM2 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM1 
          ldy #0 
          jsr CheckIsPlayerCollidingWithDiagonalLLUR 
          ldy #1 
          jsr CheckIsPlayerCollidingWithDiagonalLLUR 
          ldy #3 
          lda BEAM_CHAR_NESW,y 
          sta PARAM1 
          lda #1 
          sta PARAM2 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          jsr DrawBeamDiagonalLLUR 
          rts 
          
.BeamStep2End 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          jsr RestoreBeamDiagonalLLUR 
          jsr RedrawItems 
          rts
          
FinalAttack 
          ;mode 5, 6 = left 
          ; 7, 8 = diagonal left down 
          ; 9, 10 = down 
          ; 11, 12 = diagonal right down 
          ; 13, 14 = right 
          lda SPRITE_MODE_POS,x 
          cmp #5 
          bne + 
          
          jmp .BeamStep1
          
+ 
          cmp #6 
          beq .BeamStep1End 
          cmp #7 
          beq .BeamStep2 
          cmp #8 
          beq .BeamStep2End 
          cmp #9 
          beq .BeamStep3 
          cmp #10 
          beq .BeamStep3End 
          cmp #11 
          beq .BeamStep4 
          cmp #12 
          bne + 
          
          jmp .BeamStep4End
          
+ 
          cmp #13 
          bne + 
          
          jmp .BeamStep5
          
+ 
          cmp #14 
          bne + 
          
          ; 
.BeamStep5End 
          jmp .BeamStep1End
          
+ 
          cmp #15 
          bne + 
          
          lda #0 
          sta SPRITE_MODE_POS,x 
          sta SPRITE_STATE,x
          
+ 
          rts
          
.BeamStep3 
          ;does player hit beam? 
          ldy #0 
          jsr CheckIsPlayerCollidingWithBeamV 
          ldy #1 
          jsr CheckIsPlayerCollidingWithBeamV 
          
          ldy #BEAM_TYPE_DARK 
          lda BEAM_CHAR_H,y 
          sta PARAM1 
          lda BEAM_CHAR_V,y 
          sta PARAM2 
          lda #1 
          sta PARAM3 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM4 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM5 
          stx PARAM6 
          jsr DrawBeamV 
          rts 
          
.BeamStep3End 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM4 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM5 
          jsr RestoreBeamHV 
          jsr RedrawItems 
          rts 
          
.BeamStep4 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM1 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithDiagonalULLR 
          ldy #1 
          jsr CheckIsPlayerCollidingWithDiagonalULLR 
          
          ldy #3 
          lda BEAM_CHAR_NWSE,y 
          sta PARAM1 
          lda #1 
          sta PARAM2 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          jsr DrawBeamDiagonalULLR 
          rts 
          
.BeamStep4End 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          jsr RestoreBeamDiagonalULLR 
          jsr RedrawItems 
          rts
          
.BeamStep5 
          ;right arm 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM1 
          lda #39 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithYPosH 
          ldy #1 
          jsr CheckIsPlayerCollidingWithYPosH 
          
          ldy #BEAM_TYPE_DARK 
          lda BEAM_CHAR_H,y 
          sta PARAM1 
          lda #1 
          sta PARAM2 
          lda SPRITE_CHAR_POS_X,x 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          sta PARAM4 
          lda #39 
          sta PARAM5 
          jsr DrawBeamHSegment 
          rts


Have fun and look out!

step89.zip

Previous Step Next Step

 

A C64 Game - Step 88

And finally, here's the big boss. Expect him to put up quite a fight once he is completed. Note that currently you cannot kill the last part.

step88.png

First of all, the boss is not just simply there, it is entering with a few flashes. We reuse Dean's shot flash code for this. To mark the final boss entry we use a new bit in the level config byte:

;final boss intro 
          lda LEVEL_CONFIG 
          and #$08 
          beq ++ 
          
          jsr HandleFinalBossIntro 
          jmp .NotDoneYet
          
++



The boss is compiled of several sprites which we create once the flashes are done. Also, the boss bit is removed to avoid restarting the boss intro.

!zone HandleFinalBossIntro
HandleFinalBossIntro 
          inc FINAL_INTRO_TIMER_DELAY 
          lda FINAL_INTRO_TIMER_DELAY 
          and #$03 
          beq ++ 
          rts
          
++ 
          inc FINAL_INTRO_TIMER 
          lda FINAL_INTRO_TIMER 
          cmp #5 
          beq .Flash 
          cmp #8 
          beq .Flash 
          cmp #10 
          beq .Flash 
          cmp #11 
          beq .SpawnBoss 
          rts 
          
.Flash 
          ;use dean's shot flash 
          lda #5 
          sta PLAYER_RELOAD_FLASH_POS 
          lda #1 
          sta VIC_BACKGROUND_COLOR 
          rts 
          
.SpawnBoss 
          ;disable intro flag 
          lda LEVEL_CONFIG 
          and #$f7 
          sta LEVEL_CONFIG 
          
          ;spawn boss 
          lda #0 
          sta BOSS_PARTS_KILLED 
          lda #19 
          sta PARAM1 
          lda #6 
          sta PARAM2 
          lda #TYPE_BOSS7 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          
          stx PARAM10 
          
          ;torso 
          lda #19 
          sta PARAM1 
          lda #10 
          sta PARAM2 
          lda #TYPE_BOSS5 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          
          lda #TYPE_BOSS_PART 
          sta SPRITE_ACTIVE,x 
          lda #128 
          sta SPRITE_STATE,x 
          jsr MoveSpriteUp 
          jsr MoveSpriteUp 
          lda PARAM10 
          sta SPRITE_VALUE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
          ;left arm 
          lda #17 
          sta PARAM1 
          lda #9 
          sta PARAM2 
          lda #TYPE_BOSS3 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          lda #TYPE_BOSS_PART 
          sta SPRITE_ACTIVE,x 
          lda #0 
          sta SPRITE_STATE,x 
          lda PARAM10 
          sta SPRITE_VALUE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
          ;right arm 
          lda #21 
          sta PARAM1 
          lda #9 
          sta PARAM2 
          lda #TYPE_BOSS4 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          lda #TYPE_BOSS_PART 
          sta SPRITE_ACTIVE,x 
          lda #0 
          sta SPRITE_STATE,x 
          lda PARAM10 
          sta SPRITE_VALUE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
          ;left foot 
          lda #18 
          sta PARAM1 
          lda #13 
          sta PARAM2 
          lda #TYPE_BOSS2 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          lda #TYPE_BOSS_PART 
          sta SPRITE_ACTIVE,x 
          lda #0 
          sta SPRITE_STATE,x 
          lda PARAM10 
          sta SPRITE_VALUE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
          ;right foot 
          lda #20 
          sta PARAM1 
          lda #13 
          sta PARAM2 
          lda #TYPE_BOSS 
          sta PARAM3 
          jsr FindEmptySpriteSlot 
          jsr SpawnObject 
          lda #TYPE_BOSS_PART 
          sta SPRITE_ACTIVE,x 
          lda #0 
          sta SPRITE_STATE,x 
          lda PARAM10 
          sta SPRITE_VALUE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          jmp .Flash
          


Also, the boss is required to be killed in several steps. First the limbs, followed by torso and finally the head. This is done by setting states accordingly (remember, states >= 128 mark invincibility) increasing the killed body part count and making other parts vulnerable:

          cpy #TYPE_BOSS_PART 
          bne ++ 
          
          inc BOSS_PARTS_KILLED 
          lda BOSS_PARTS_KILLED 
          cmp #5 
          beq .MakeBossHeadVulnerable 
          cmp #4 
          bne ++ 
          
          ;make boss torso vulnerable 
          ldy #1 
          
- 
          lda SPRITE_ACTIVE,y 
          cmp #TYPE_BOSS_PART 
          beq + 
          
          iny 
          bne - 
          
+ 
          lda #0 
          sta SPRITE_STATE,y 
          jmp ++ 
          
.MakeBossHeadVulnerable 
          ldy #1 
          
- 
          lda SPRITE_ACTIVE,y 
          cmp #TYPE_BOSS7 
          beq + 
          
          iny 
          bne - 
          
+ 
          lda #0 
          sta SPRITE_STATE,y
          
++


Since the boss object is controlled by the head but bigger with all parts attached the possible movement area needs to be limited (the boss body should not move outside the screen). Therefore a little check is added to all movement routines:

          lda SPRITE_CHAR_POS_X,x 
          cmp MOVE_BORDER_LEFT 
          beq .BlockedLeft



..and similar to the other three directions.

The bosses limbs are shooting beams horizontally and diagonally. These require a few changes to the existing collision check routines to allow for partial beams:

;------------------------------------------------------------
;check player vs. beam H
; YPos in PARAM3
; player index in y
;------------------------------------------------------------
!zone CheckIsPlayerCollidingWithYPosH
CheckIsPlayerCollidingWithYPosH 
          lda SPRITE_ACTIVE,y 
          bne .PlayerIsActive
          
.PlayerNotActive 
          rts 
          
.PlayerIsActive 
          cmp #TYPE_PLAYER_DEAN 
          beq + 
          cmp #TYPE_PLAYER_SAM 
          beq + 
          rts 
          
+ 
          lda SPRITE_STATE,y 
          cmp #128 
          bcs .PlayerNotActive 
          
          lda SPRITE_CHAR_POS_X,y 
          cmp PARAM1 
          bcc .PlayerNotActive 
          cmp PARAM2 
          bcs .PlayerNotActive 
          
          ;compare char positions in y 
          lda PARAM3 
          cmp SPRITE_CHAR_POS_Y,y 
          beq .PlayerHit 
          
          clc 
          adc #1 
          cmp SPRITE_CHAR_POS_Y,y 
          beq .PlayerHit 
          
          sec 
          sbc #2 
          cmp SPRITE_CHAR_POS_Y,y 
          beq .PlayerHit 
          
          ;not hit 
          rts 
          
.PlayerHit 
          ;player killed 
          jmp KillPlayer
          
;------------------------------------------------------------
;check player vs. diagonal beam
; X start in PARAM1
; Y start in PARAM2
; player index in y
;------------------------------------------------------------
!zone CheckIsPlayerCollidingWithDiagonalLLUR
CheckIsPlayerCollidingWithDiagonalLLUR 
          lda SPRITE_ACTIVE,y 
          bne .PlayerIsActive
          
.PlayerNotActive 
          rts 
          
.PlayerIsActive 
          lda SPRITE_STATE,y 
          cmp #128 
          bcs .PlayerNotActive 
          
          ;compare char positions in x 
          lda PARAM1 
          sec 
          sbc SPRITE_CHAR_POS_X,y 
          bpl .PositiveX 
          
          ;player is to the right 
          rts
          
.PositiveX 
          sta PARAM3 
          lda PARAM2 
          sec 
          sbc SPRITE_CHAR_POS_Y,y 
          bpl .PositiveY 
          
          lda SPRITE_CHAR_POS_Y,y 
          sec 
          sbc PARAM2
          
.PositiveY 
          sta PARAM4 
          lda PARAM3 
          cmp PARAM4 
          beq .PlayerHit 
          
          lda PARAM3 
          sec 
          sbc PARAM4 
          bpl .PositiveDelta 
          
          lda PARAM4 
          sec 
          sbc PARAM3
          
.PositiveDelta 
          cmp #1 
          beq .PlayerHit 
          
          ;not hit 
          rts 
          
.PlayerHit 
          ;player killed 
          jmp KillPlayer
          
;------------------------------------------------------------
;check player vs. diagonal beam
; X start in PARAM1
; Y start in PARAM2
; player index in y
;------------------------------------------------------------
!zone CheckIsPlayerCollidingWithDiagonalULLR
CheckIsPlayerCollidingWithDiagonalULLR 
          lda SPRITE_ACTIVE,y 
          bne .PlayerIsActive
          
.PlayerNotActive 
          rts 
          
.PlayerIsActive 
          lda SPRITE_STATE,y 
          cmp #128 
          bcs .PlayerNotActive 
          
          ;compare char positions in x 
          lda PARAM1 
          sec 
          sbc SPRITE_CHAR_POS_X,y 
          bpl .PlayerNotActive 
          
          ;player is to the right 
          lda SPRITE_CHAR_POS_X,y 
          sec 
          sbc PARAM1 
          sta PARAM3 
          lda PARAM2 
          sec 
          sbc SPRITE_CHAR_POS_Y,y 
          bpl .PositiveY 
          
          lda SPRITE_CHAR_POS_Y,y 
          sec 
          sbc PARAM2
          
.PositiveY 
          sta PARAM4 
          lda PARAM3 
          cmp PARAM4 
          beq .PlayerHit 
          
          lda PARAM3 
          sec 
          sbc PARAM4 
          bpl .PositiveDelta 
          
          lda PARAM4 
          sec 
          sbc PARAM3
          
.PositiveDelta 
          cmp #1 
          beq .PlayerHit 
          
          ;not hit 
          rts 
          
.PlayerHit 
          ;player killed 
          jmp KillPlayer



All the beams are controlled by the boss head. Attack states, firing and collision checks:

;------------------------------------------------------------
;boss #7
;state = 128 -> random movements
;state = 129 -> attack with beams
;------------------------------------------------------------
!zone BehaviourBoss7
BehaviourBoss7
BOSS_MOVE_SPEED = 1 
          lda SPRITE_STATE,x 
          beq .RandomMovements 
          cmp #129 
          bne + 
          
          jmp .AttackWithBeams
          
+
.RandomMovements 
          inc SPRITE_MODE_POS,x 
          bne + 
          
          ;attack mode 
          lda #129 
          sta SPRITE_STATE,x 
          rts
          
+ 
          lda SPRITE_MOVE_POS,x 
          bne .DoMove 
          
          ;find new random dir 
          lda #25 
          sta SPRITE_MOVE_POS,x 
          jsr GenerateRandomNumber 
          and #$01 
          sta SPRITE_DIRECTION,x 
          jsr GenerateRandomNumber 
          and #$01 
          sta SPRITE_DIRECTION_Y,x
          
.DoMove 
          dec SPRITE_MOVE_POS,x 
          lda #12 
          sta MOVE_BORDER_LEFT 
          lda #39 - 12 
          sta MOVE_BORDER_RIGHT 
          lda #4 
          sta MOVE_BORDER_TOP 
          lda #12 
          sta MOVE_BORDER_BOTTOM 
          lda SPRITE_DIRECTION,x 
          beq ++ 
          
          jsr ObjectMoveLeftBlocking 
          beq .DoMoveY 
          
          ;move other parts 
          lda #5 
          sta PARAM10
- 
          inx 
          jsr ObjectMoveLeft 
          dec PARAM10 
          bne - 
          jmp .DoMoveY
          
++ 
          jsr ObjectMoveRightBlocking 
          beq .DoMoveY 
          
          ;move other parts 
          lda #5 
          sta PARAM10
- 
          inx 
          jsr ObjectMoveRight 
          dec PARAM10 
          bne - 
          
.DoMoveY 
          ldx CURRENT_INDEX 
          lda SPRITE_DIRECTION_Y,x 
          beq ++ 
          
          jsr ObjectMoveUpBlocking 
          beq .DoMoveDone 
          
          ;move other parts 
          lda #5 
          sta PARAM10
- 
          inx 
          jsr ObjectMoveUp 
          dec PARAM10 
          bne - 
          
          jmp .DoMoveDone
          
++ 
          jsr ObjectMoveDownBlocking 
          beq .DoMoveDone 
          
          ;move other parts 
          lda #5 
          sta PARAM10
- 
          inx 
          jsr ObjectMoveDown 
          dec PARAM10 
          bne -
          
.DoMoveDone 
          lda #0 
          sta MOVE_BORDER_LEFT 
          sta MOVE_BORDER_TOP 
          lda #39 
          sta MOVE_BORDER_RIGHT 
          lda #23 
          sta MOVE_BORDER_BOTTOM 
          rts
          
.AttackWithBeams 
          inc SPRITE_MOVE_POS,x 
          lda SPRITE_MOVE_POS,x 
          and #$03 
          beq + 
          
          rts
          
+ 
          inc SPRITE_MODE_POS,x 
          lda SPRITE_MODE_POS,x 
          cmp #5 
          bcs + 
          jmp .BeamNotDangerous
          
+ 
          cmp #12 
          bcc + 
          jmp .BeamNotDangerous
          
+ 
          ;does player hit beam? 
          ;modify x to point to arm object 
          ;TODO - only check left/right segment! 
          lda SPRITE_ACTIVE + 2,x 
          beq ++ 
          
          ;left arm 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #3 
          sta PARAM3 
          lda #0 
          sta PARAM1 
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #2 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithYPosH 
          ldy #1 
          jsr CheckIsPlayerCollidingWithYPosH
          
++ 
          lda SPRITE_ACTIVE + 3,x 
          beq ++ 
          
          ;right arm 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #3 
          sta PARAM3 
          lda SPRITE_CHAR_POS_X,x 
          clc 
          adc #2 
          sta PARAM1 
          lda #39 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithYPosH 
          ldy #1 
          jsr CheckIsPlayerCollidingWithYPosH
          
++ 
          lda SPRITE_ACTIVE + 4,x 
          beq ++ 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM2 
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #1 
          sta PARAM1 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithDiagonalLLUR 
          ldy #1 
          jsr CheckIsPlayerCollidingWithDiagonalLLUR
          
++ 
          lda SPRITE_ACTIVE + 5,x 
          beq ++ 
          
          lda SPRITE_CHAR_POS_X,x 
          clc 
          adc #2 
          sta PARAM1 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM2 
          
          ldy #0 
          jsr CheckIsPlayerCollidingWithDiagonalULLR 
          ldy #1 
          jsr CheckIsPlayerCollidingWithDiagonalULLR
          
++ 
.BeamNotDangerous 
          lda SPRITE_MODE_POS,x 
          cmp #5 
          beq .BeamStep1 
          cmp #6 
          beq .BeamStep2 
          cmp #7 
          beq .BeamStep3 
          cmp #8 
          beq .BeamStep4 
          cmp #9 
          beq .BeamStep3 
          cmp #10 
          beq .BeamStep4 
          cmp #11 
          beq .BeamStep3 
          cmp #12 
          bne + 
          
          lda #128 
          sta SPRITE_STATE,x 
          lda #0 
          sta SPRITE_MODE_POS,x 
          
          ;remove beam 
          lda #0 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #3 
          sta PARAM4 
          lda #39 
          sta PARAM5 
          jsr RestoreBeamHSegment 
          
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #1 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM4 
          jsr RestoreBeamDiagonalLLUR 
          
          lda SPRITE_CHAR_POS_X,x 
          clc 
          adc #2 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM4 
          jsr RestoreBeamDiagonalULLR 
          
+ 
          rts 
          
.BeamStep1 
          ;beam 
          ldy #BEAM_TYPE_DARK 
          jmp .HandleBeam 
          
.BeamStep2 
          ;beam 
          ldy #BEAM_TYPE_MEDIUM 
          jmp .HandleBeam 
          
.BeamStep3 
          ;beam 
          ldy #BEAM_TYPE_LIGHT 
          jmp .HandleBeam 
          
.BeamStep4 
          ;beam 
          ldy #BEAM_TYPE_LIGHT2 
          jmp .HandleBeam 
          
.HandleBeam 
          ;PARAM1 = beam h char 
          ;PARAM2 = beam color 
          ;PARAM3 = x char pos 
          ;PARAM4 = y char pos 
          ;PARAM5 = x end pos 
          tya 
          pha 
          lda BEAM_CHAR_H,y 
          sta PARAM1 
          lda BEAM_COLOR,y 
          sta PARAM2 
          
          ;left arm 
          lda SPRITE_ACTIVE + 2,x 
          beq ++ 
          lda #0 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #3 
          sta PARAM4 
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #2 
          sta PARAM5 
          jsr DrawBeamHSegment
          
++ 
          lda SPRITE_ACTIVE + 3,x 
          beq ++ 
          lda SPRITE_CHAR_POS_X,x 
          clc 
          adc #3 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #3 
          sta PARAM4 
          lda #39 
          sta PARAM5 
          jsr DrawBeamHSegment 
          
++ 
          ;diagonal left 
          pla 
          pha 
          tay 
          lda SPRITE_ACTIVE + 4,x 
          beq ++ 
          
          lda BEAM_CHAR_NESW,y 
          sta PARAM1 
          lda SPRITE_CHAR_POS_X,x 
          sec 
          sbc #1 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM4 
          jsr DrawBeamDiagonalLLUR 
          
++ 
          pla 
          tay 
          lda SPRITE_ACTIVE + 5,x 
          beq ++ 
          
          lda BEAM_CHAR_NWSE,y 
          sta PARAM1 
          lda SPRITE_CHAR_POS_X,x 
          clc 
          adc #2 
          sta PARAM3 
          lda SPRITE_CHAR_POS_Y,x 
          clc 
          adc #8 
          sta PARAM4 
          jsr DrawBeamDiagonalULLR 
          
++ 
          rts


Firing the beams (and restoring background) is handled by new sub routines:

;PARAM1 = beam h char
;PARAM2 = beam color
;PARAM3 = x char pos
;PARAM4 = y char pos
!zone DrawBeamDiagonalLLUR
DrawBeamDiagonalLLUR
.NextLine 
          ldy PARAM4 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc 
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          
          ;left 
          ldy PARAM3 
          lda PARAM1 
          sta (ZEROPAGE_POINTER_1),y 
          lda PARAM2 
          sta (ZEROPAGE_POINTER_2),y 
          inc PARAM4 
          lda PARAM4 
          cmp #22 
          beq .LowerPartDone 
          
          ;left border reached? 
          lda PARAM3 
          beq .LowerPartDone 
          
          dec PARAM3 
          jmp .NextLine
          
.LowerPartDone 
          rts 
          
          ;PARAM3 = x char pos
          ;PARAM4 = y char pos
!zone RestoreBeamDiagonalLLUR
RestoreBeamDiagonalLLUR
.NextLine 
          ldy PARAM4 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          sta ZEROPAGE_POINTER_3 
          sta ZEROPAGE_POINTER_4 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc 
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          sec 
          sbc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_3 + 1 
          sec 
          sbc #( ( SCREEN_BACK_CHAR - SCREEN_BACK_COLOR ) >> 8 ) 
          sta ZEROPAGE_POINTER_4 + 1 
          
          ;left 
          ldy PARAM3 
          lda (ZEROPAGE_POINTER_3),y 
          sta (ZEROPAGE_POINTER_1),y 
          lda (ZEROPAGE_POINTER_4),y 
          sta (ZEROPAGE_POINTER_2),y 
          inc PARAM4 
          lda PARAM4 
          cmp #22 
          beq .LowerPartDone 
          
          ;left border reached? 
          lda PARAM3 
          beq .LowerPartDone 
          
          dec PARAM3 
          jmp .NextLine
          
.LowerPartDone 
          rts
          
          ;PARAM1 = beam h char
          ;PARAM2 = beam color
          ;PARAM3 = x char pos
          ;PARAM4 = y char pos
!zone DrawBeamDiagonalULLR
DrawBeamDiagonalULLR
.NextLine 
          ldy PARAM4 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc 
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          ldy PARAM3 
          lda PARAM1 
          sta (ZEROPAGE_POINTER_1),y 
          lda PARAM2 
          sta (ZEROPAGE_POINTER_2),y 
          inc PARAM4 
          lda PARAM4 
          cmp #22 
          beq .LowerPartDone 
          
          ;left border reached? 
          lda PARAM3 
          cmp #39 
          beq .LowerPartDone 
          
          inc PARAM3 
          jmp .NextLine
          
.LowerPartDone 
          rts 
          
          ;PARAM3 = x char pos
          ;PARAM4 = y char pos
!zone RestoreBeamDiagonalULLR
RestoreBeamDiagonalULLR
.NextLine 
          ldy PARAM4 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          sta ZEROPAGE_POINTER_3 
          sta ZEROPAGE_POINTER_4 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc 
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          sec 
          sbc #( ( SCREEN_COLOR - SCREEN_BACK_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_3 + 1 
          sec 
          sbc #( ( SCREEN_BACK_CHAR - SCREEN_BACK_COLOR ) >> 8 ) 
          sta ZEROPAGE_POINTER_4 + 1 
          
          ldy PARAM3 
          lda (ZEROPAGE_POINTER_3),y 
          sta (ZEROPAGE_POINTER_1),y 
          lda (ZEROPAGE_POINTER_4),y 
          sta (ZEROPAGE_POINTER_2),y 
          inc PARAM4 
          lda PARAM4 
          cmp #22 
          beq .LowerPartDone 
          
          ;left border reached? 
          lda PARAM3 
          cmp #39 
          beq .LowerPartDone 
          
          inc PARAM3 
          jmp .NextLine
          
.LowerPartDone 
          rts



The behaviour of the boss parts is surprisingly simple, just react on being hit:

;------------------------------------------------------------
;boss helper
;------------------------------------------------------------
!zone BehaviourBossHelper
BehaviourBossHelper 
          lda SPRITE_HITBACK,x 
          beq .NoHitBack 
          
          dec SPRITE_HITBACK,x 
          ldy SPRITE_HITBACK,x 
          lda BOSS_FLASH_TABLE,y 
          sta VIC_SPRITE_COLOR,x 
          cpy #0 
          bne .NoHitBack 
          
          ;make vulnerable again 
          lda #0 
          sta SPRITE_STATE,x 
          lda #2 
          sta VIC_SPRITE_COLOR,x 
          
.NoHitBack 
          rts



Have fun!

 

step88.zip

Previous Step Next Step

 

And something different for a change: Added road side stones to the story pages to make it look a bit neater.

Looks better in motion :)

step86.png

It's actually pretty simple: We add a stone in front and one in the back. Store positions and update them every frame with different deltas.

Start with the position value variables:

MOVE_STONES 
          !byte 0
          
MOVE_STONE_POS_BACK 
          !byte 0
          
MOVE_STONE_POS_FRONT 
          !byte 0


...and the actual code.

STONE_BACK_DISTANCE = 20
STONE_FRONT_DISTANCE = 24 
          lda MOVE_STONES 
          bne + 
          jmp .NoStones
          
+ 

          ;background
          ldy #19 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          ldy MOVE_STONE_POS_BACK
          
- 
          lda #32 
          sta (ZEROPAGE_POINTER_1),y 
          tya
          clc 
          adc #STONE_BACK_DISTANCE 
          tay
          cpy #39 
          bcc - 
          
          lda MOVE_STONE_POS_BACK 
          clc
          adc #1
          
- 
          cmp #STONE_BACK_DISTANCE 
          bcc + 
          sec 
          sbc #STONE_BACK_DISTANCE 
          jmp - 
          
+
          sta MOVE_STONE_POS_BACK 
          ldy MOVE_STONE_POS_BACK
          
-
          lda #149
          sta (ZEROPAGE_POINTER_1),y 
          lda #8 
          sta (ZEROPAGE_POINTER_2),y 
          tya
          clc
          adc #STONE_BACK_DISTANCE
          tay
          cpy
          #39
          bcc - 
          
          ;foreground
          ldy #22
          lda SCREEN_LINE_OFFSET_TABLE_LO,y
          sta ZEROPAGE_POINTER_1 
          sta ZEROPAGE_POINTER_2 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          clc
          adc #( ( SCREEN_COLOR - SCREEN_CHAR ) >> 8 ) 
          sta ZEROPAGE_POINTER_2 + 1 
          
          ldy MOVE_STONE_POS_FRONT
          
-
          lda #32
          sta (ZEROPAGE_POINTER_1),y 
          tya 
          clc
          adc #STONE_FRONT_DISTANCE 
          tay
          cpy #39
          bcc - 
          
          lda MOVE_STONE_POS_FRONT 
          clc
          adc #2
          
-
          cmp #STONE_FRONT_DISTANCE
          bcc +
          sec
          sbc #STONE_FRONT_DISTANCE 
          jmp -
          
+
          sta MOVE_STONE_POS_FRONT 
          ldy MOVE_STONE_POS_FRONT
          
-
          lda #148
          sta (ZEROPAGE_POINTER_1),y 
          lda #9
          sta (ZEROPAGE_POINTER_2),y
          tya 
          clc
          adc #STONE_FRONT_DISTANCE 
          tay
          cpy #39
          bcc - 
          
.NoStones 


step86.zip

Previous Step Next Step

 

And here's a new extra for Sam. It works similar to the super bullet. For every demon blood picked Sam can destroy an enemy with one touch.

step83.png


The code is quite similar to the super bullet. Add a new counter variable (DEMON_BLOOD), a new item image, make it possible for the item to spawn as well.

At the item pickup we add this. If the player is Sam (means x = 1) increase the counter.

.EffectDemonBlood 
          cpx #1
          bne .DeanDoesNotUseForce 
          
          inc DEMON_BLOOD 
          jmp .RemoveItem 



At the hurt enemy part we add this little snippet. If we have a DEMON_BLOOD, decrease the count and kill the enemy right away.

          lda DEMON_BLOOD 
          beq + 
          dec DEMON_BLOOD 
          jmp .EnemyKilled
          
+ 
          dec SPRITE_HP,x 
          bne .EnemyWasHurt 
          
          jmp .EnemyKilled


Have fun!

step83.zip

Previous Step Next Step

 

A new pickup! And it shines :)

Sometimes one of those wondrous bullets of Samuel Colt will be dropped. Those can kill any enemy with one shot, so use them well!

step81.png

The changes are quite simple. In the PickupItem routine we add a handler for those new items:

          cmp #ITEM_SUPER_BULLET 
          beq .EffectSuperBullet

 

We make sure only Dean can use it and increase the super bullet counter:

.EffectSuperBullet 
          cpx #1
          beq .SamDoesNotUseBullets 
          
          inc SUPER_BULLET

 

The super bullet shot will flash red instead of white, so we add:

;red flash for super bullet 
          lda SUPER_BULLET 
          beq + 
          
          lda #2
          jmp ++ 
          
+ 
          lda #1
++ 
          sta VIC_BACKGROUND_COLOR

 

and finally, at the enemy shot routine we add

          lda SUPER_BULLET 
          beq + 
          
          ;directly kill enemy 
          dec SUPER_BULLET 
          jmp .EnemyKilled 
          
+ 
          lda SPRITE_HP,x 
          dec SPRITE_HP,x 
          lda SPRITE_HP,x 
          beq .EnemyKilled

Done!

BTW, the super bullet also works on bosses!
 

step81.zip


Previous Step Next Step

 

Did you notice the huge door in the background? It's about time it opens!

step80.png

That's actually pretty simple. At the jump-to-next level code we add a check. Whether the open door animation is shown depends on a bit in the LEVEL_CONFIG byte.

GoToNextLevel 
          lda LEVEL_CONFIG 
          and #$04 
          beq .NoDoorAnim 
          jsr DoorAnim
          
.NoDoorAnim



The actual animation is straight forward. The door location is hard coded, and we simply copy the characters one step to the outer side and replace the innermost characters with empty space.

Notice that we use a method which is not possible that easy in todays code: We have a local code loop until the door is completely opened.
Since there are no events to be handled, messages to be retrieved or other OS specific code required, we can do that.

;------------------------------------------------------------
;open door animation
;------------------------------------------------------------
!zone DoorAnim
DoorAnim 
          lda #0 
          sta LEVEL_DONE_DELAY
          
.DoorAnimLoop 
          jsr WaitFrame 
          inc LEVEL_DONE_DELAY 
          lda LEVEL_DONE_DELAY 
          and #$07 
          bne .DoorAnimLoop 
          
          ;open door (16,11) 
          lda #11 
          sta PARAM2 
- 
          ldy PARAM2 
          lda SCREEN_LINE_OFFSET_TABLE_LO,y 
          sta ZEROPAGE_POINTER_1 
          lda SCREEN_LINE_OFFSET_TABLE_HI,y 
          sta ZEROPAGE_POINTER_1 + 1 
          ldy #17 
          lda (ZEROPAGE_POINTER_1),y 
          dey 
          sta (ZEROPAGE_POINTER_1),y 
          ldy #18 
          lda (ZEROPAGE_POINTER_1),y 
          dey 
          sta (ZEROPAGE_POINTER_1),y 
          ldy #19 
          lda (ZEROPAGE_POINTER_1),y 
          dey 
          sta (ZEROPAGE_POINTER_1),y 
          
          lda #32 
          ldy #19 
          sta (ZEROPAGE_POINTER_1),y 
          ldy #22 
          lda (ZEROPAGE_POINTER_1),y 
          iny 
          sta (ZEROPAGE_POINTER_1),y 
          ldy #21 
          lda (ZEROPAGE_POINTER_1),y 
          iny 
          sta (ZEROPAGE_POINTER_1),y 
          ldy #20 
          lda (ZEROPAGE_POINTER_1),y 
          iny 
          sta (ZEROPAGE_POINTER_1),y 
          lda #32 
          ldy #20 
          sta (ZEROPAGE_POINTER_1),y 
          
          inc PARAM2 
          lda PARAM2 
          cmp #21 
          bne - 
          
          ; 
          lda LEVEL_DONE_DELAY 
          lsr 
          lsr 
          lsr 
          cmp #4 
          bne .DoorAnimLoop 
          
          ;door is fully open now
- 
          jsr WaitFrame 
          inc LEVEL_DONE_DELAY 
          lda LEVEL_DONE_DELAY 
          cmp #200 
          bne - 
          
          rts


Also, since I'm going on vacation this tutorial takes a break until next year. Thanks for your support so far, have fun!
 

step80.zip


Previous Step Next Step

 

Almost a new feature: For the final level range the enemies spawn in waves. In this level, after beating the wolfmen two more waves of other enemies come in. And all without any more memory in the level data!

step79.png

The key is in the previously used SPAWN_SPOT_SPAWN_COUNT. We use the upper 4 bits to contain the wave the spawn spots is part of. This limits us to 15 enemies per spawn spot which we were never even close to.

We add a new counter named NUMBER_DELAYED_SPAWN_SPOTS_ALIVE. This contains the number of spawn spots that are currently not active (future waves). SPAWN_SPOT_LEVEL holds the wave number (in the upper for bits, that's why we add 16)

The ProcessSpawnSpots routine is enhanced thusly:

          lda NUMBER_ENEMIES_ALIVE
          ora NUMBER_SPAWN_SPOTS_ALIVE
          bne .NoDelayedSpawnSpots

          lda NUMBER_DELAYED_SPAWN_SPOTS_ALIVE
          beq .NoDelayedSpawnSpots

          ;undelay them now
          lda SPAWN_SPOT_LEVEL
          clc
          adc #16
          sta SPAWN_SPOT_LEVEL

          ;check all spots
          ldx #0
-
          lda SPAWN_SPOT_ACTIVE,x
          beq +

          lda SPAWN_SPOT_SPAWN_COUNT,x
          and #$f0
          cmp SPAWN_SPOT_LEVEL
          bne +

          ;undelay now
          lda SPAWN_SPOT_SPAWN_COUNT,x
          and #$0f
          sta SPAWN_SPOT_SPAWN_COUNT,x

          dec NUMBER_DELAYED_SPAWN_SPOTS_ALIVE
          inc NUMBER_SPAWN_SPOTS_ALIVE

+
          inx
          cpx #SPAWN_SPOT_COUNT
          bne -
.NoDelayedSpawnSpots


Obviously in the spawn spot update loop we need to skip any spawn spots where the upper four bits are set:

          lda SPAWN_SPOT_SPAWN_COUNT,x
          and #$f0
          bne .NextSpawnSpot


Also, during level buildup we need to increment the correct counter depending on the upper four bits again:

          ;count
          iny
          lda (ZEROPAGE_POINTER_1),y
          sta SPAWN_SPOT_SPAWN_COUNT,x

          ;upper 4 bits set? then it's a delayed spawn spot!
          and #$f0
          bne +
          inc NUMBER_SPAWN_SPOTS_ALIVE
          jmp ++
          
+
          inc NUMBER_DELAYED_SPAWN_SPOTS_ALIVE
++


Have fun surviving!

step79.zip

Previous Step Next Step

 

A C64 Game - Step 78

Added and streamlined all story pages. Now we have locations (mentioned) and a crude "story arc". Plus a little animation on the last two bosses. There's no higher meaning behind the locations, I just wandered about USA with Google maps ;)

Showing no code this time, since it's merely added text to existing tables.

The aforementioned animation is a simple xor call on the sprite pointer:

          lda SPRITE_POINTER_BASE,x
          eor #1
          sta SPRITE_POINTER_BASE,x



step78.png

step78.zip

Previous Step Next Step