This page documents information about Castlevania
. Many of the tricks demonstrated here are near impossible in real time and documented for the purposes of creating Tool-assisted Speedruns.
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];
}
Note: these tricks tend to point at some kind of horizontal corridor with a fixed height in which the hanging bat can detect the player. Jumping to delay it would then mean going above this corridor to avoid being seen too soon, for example. This is just speculation based on observation.