var FrameCounter = byte at RAM[0x1A],
CurrentStage = byte at RAM[0x28],
SpecialItemCounter = byte at RAM[0x7B],
CurrentSpecialWeapon = byte at RAM[0x15B],
MultiplierSpawnCounter = byte at RAM[0x79],
SpecialWeaponOnScreen = byte at RAM[0x72],
WhipLength012 = RAM[0x70],
NumberOfHearts = RAM[0x71],
CurrentMultiplier = RAM[0x64],
CurrentRandomBonusId = RAM[0x6F]
function SpawnItem_WhenEnemyDies: # Is at $1E09A:
var ai_type = this->ObjectAItype
if((ai_type < 32 AND ai_type ≠ 11) OR (ai_type ≥ 40))
{
if((FrameCounter MOD 16) = 2)
return this->SpawnSpecialItem();
}
if(ai_type = 10 OR ai_type = 19)
{
return this->BecomeBonusItem(4); # unconditionally a large heart
}
var default_item ≔ 0; # random bonus
if(ai_type = 3 OR ai_type = 8 OR ai_type = 12 OR ai_type = 14 OR ai_type = 18)
{
default_item ≔ 4; # a large heart instead of random bonus
# (but it can still become a multiplier instead of large heart)
}
if( (FrameCounter MOD 8) ≠ 0)
ai_type ≔ 0x30 # just disappears after a while, no bonus!
else
ai_type ≔ 0x31 # will become a bonus item.
this->BecomeBonusItem(default_item, ai_type);
function GetCurrentLevel:
return floor((CurrentStage - 1) / 3)
function SpawnSpecialItem:
var special_table ≔ Array(0..5, 0..3)
{
{10,15,11,0}, # level 0(boss:bat): rosary, watch, firebomb, <none>
{11,10,0,9}, # level 1(boss:medusa): firebomb, rosary, <none>, boomerang
{10,10,14,0}, # level 2(boss:mummies): rosary, rosary, amphora, <none>
{10,14,13,0}, # level 3(boss:frankenstein): rosary, amphora, axe, <none>
{0,15,14,8}, # level 4(boss:death): <none>, watch, amphora, dagger
{9,15,10,0} # level 5(boss:dracula): boomerang, watch, rosary, <none>
}
# Note: SpecialItemCounter is a global variable that is never ever referenced
# anywhere else but in this function. It is a 2-bit counter.
do {
var special_item ≔ special_table[ GetCurrentLevel ] [ (++SpecialItemCounter) MOD 4 ];
} while(special_item = CurrentWeapon);
this->BecomeBonusItem (special_item)
function BecomeBonusItem(bonustype, ai_type = 0x31):
# This function does not really change the item yet.
# It only assigns the AI to be used by this object.
this->AItype ≔ ai_type
this->BonusType ≔ bonustype
function BecomeActor(actorid):
# This function creates the actual object from the given id.
# implementation is omitted here, it is not relevant.
var TranslateBonusIdToActorId = Array(0..15)
{0x0F,0x0E,0x05,0x0C,0x42,0x10,0x23,0x00,0x17,0x43,0x0A,0x44,0x2F,0x18,0x45,0x46};
function RunObjectAI_30:
if(--this->ObjectMultiPurposeCounter) return;
this->AItype ≔ 0x32;
function RunObjectAI_32:
this->StopExisting();
function RunObjectAI_2F:
# Common AI code for bonus actors. Omitted here.
function RunObjectAI_31:
if(--this->ObjectMultiPurposeCounter) return;
this->AItype ≔ 0x2F;
switch(this->BonusType MOD 16)
{
default:
if(SpecialWeaponOnScreen OR (CurrentWeapon = this->BonusType))
return this->BecomeTinyRandomBonusA();
SpecialWeaponOnScreen ≔ 0xFF;
# fallthrough:
case 5: # beef
case 6: # red sphere
case 7: # unknown (ghost?)
# Keep this decision of "bonus type", and become the actual object.
return this->BecomeActor(TranslateBonusIdToActorId[this->BonusType]);
case 1: # bonus sac
this->BecomeActor(0x0E); # become a sac
this->PaletteIndex ≔ this->BonusType SHR 4;
return;
case 12: # multiplier
# Try becoming a multiplier
MultiplierSpawnCounter ≔ 0;
var tmp ≔ CurrentMultiplier;
if(tmp < 2)
{
this->BonusType ≔ 12;
tmp ≔ tmp + BonusTypeToActorTypeTransTable[this->BonusType];
return this->BecomeActor(tmp);
}
# passthru
case 0: # small heart or a sac of some value
return this->BecomeCompletelyRandomBonus();
}
function BecomeCompletelyRandomBonus:
# It is unclear where MultiplierSpawnCounter is incremented.
# It happens at $E65D, but the circumstances in which that
# code is executed are not clear. It seems to occur whenever
# a special weapon delivers damage, though. In the case of
# the fire bomb, for enemies that are not immediately killed
# from damage, it can mean that the counter is incremented
# by ~50 times at once.
if(MultiplierSpawnCounter ≥ 10)
{
# Try becoming a multiplier
MultiplierSpawnCounter = 0;
var tmp ≔ CurrentMultiplier;
if(tmp < 2)
{
this->BonusType ≔ 12;
tmp ≔ tmp + BonusTypeToActorTypeTransTable[this->BonusType];
return this->BecomeActor(tmp);
}
}
if(NOT SpecialWeaponOnScreen)
{
if(NumberOfHearts < 8)
{
if(NumberOfHearts ≥ 4 AND WhipLength012 < 1)
return this->BecomeWhipUpgrade();
}
else if(WhipLength012 < 2)
return this->BecomeWhipUpgrade();
}
return this->BecomeTinyRandomBonusA();
function BecomeTinyRandomBonusA:
this->BonusType ≔ CurrentRandomBonusId MOD 2; # 0 = heart, 1 = sac
return this->BecomeActor(TranslateBonusIdToActorId[this->BonusType]);
function BecomeWhipUpgrade:
SpecialWeaponOnScreen ≔ 0xFF;
this->BonusType ≔ 3; # whip upgrade
return this->BecomeActor(TranslateBonusIdToActorId[this->BonusType]);
# This code is executed non-stop in the main-loop of the program.
# Each time NMI resumes, it returns to this loop.
# It is responsible for permutating the CurrentRandomBonusId variable.
# If you want to influence the outcome of this loop before the next NMI,
# you need to influence the number of CPU cycles spent in the previous NMI.
function MainLoop:
var RandomTable ≔ Array(0..15)
{
0x33,0xBB,0x3F,0x80,
0x2E,0xA9,0x61,0x87,
0xAD,0xC3,0xB2,0xC8,
0x7C,0x25,0x48,0x7A
};
loop_forever:
{
var tmp ≔ (CurrentRandomBonusId OR FrameCounter) AND 15;
CurrentRandomBonusId ≔ RandomTable[tmp];
}