├── README.md ├── audio ├── audio_replacements.json └── metalBoss.ogg ├── mod.json ├── rawdata ├── DEZ │ ├── Layout │ │ └── 2.bin │ └── Object Pos │ │ └── 2.bin └── rominjections.json ├── scripts ├── credits.lemon ├── main.lemon └── shc.lemon └── sprites ├── boss.png ├── boss_hit.png ├── bossname.png ├── explosion.bmp ├── explosion.png ├── markers.png ├── metalsonic.json └── spikebomb.png /README.md: -------------------------------------------------------------------------------- 1 | # Vs Metal Sonic in Sonic 3 A.I.R 2 | Adds Metal Sonic as a boss fight to Sonic 3 Angel Island Revisited. 3 | There is no explanation as to why Metal Sonic is here, but that will come when my level mod releases (eventually). 4 | He replaces Death Ball in DEZ2, because Death Ball is annoying. 5 | There are changes to the arena, such as the removal of gravity flipping teleporters (This is because they aren't a mechanic used in the fight.) 6 | 7 | Credits: 8 | 9 | -New Boss Theme arranged by Cosmic 10 | 11 | -Old Boss Theme 16-Bit Remix made by JustKam 12 | 13 | -Metal Sonic Sprites made by TannerTH25 and TheIrishSpriter 14 | 15 | -Additional sprite tweaks (such as laser fire animation and facing forward) done by Benkoopa 16 | 17 | -SEGA for creating Metal Sonic 18 | -------------------------------------------------------------------------------- /audio/audio_replacements.json: -------------------------------------------------------------------------------- 1 | { 2 | //Boss Theme, made by me 3 | "metalsonic_boss": {"File": "metalBoss.ogg", "Name": "Vs Metal Sonic", "Type": "Music", "LoopStart": "70119"}, 4 | } 5 | -------------------------------------------------------------------------------- /audio/metalBoss.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/audio/metalBoss.ogg -------------------------------------------------------------------------------- /mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "Metadata": 3 | { 4 | "Name": "Metal Sonic in Sonic The Hedgehog 3", 5 | "Author": "Cosmic", 6 | "Description": "Adds Metal Sonic as a custom boss in DEZ2, replacing Death Ball.", 7 | "URL": "", 8 | "ModVersion": "3.1.1", 9 | "GameVersion": "24.02.02.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rawdata/DEZ/Layout/2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/rawdata/DEZ/Layout/2.bin -------------------------------------------------------------------------------- /rawdata/DEZ/Object Pos/2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/rawdata/DEZ/Object Pos/2.bin -------------------------------------------------------------------------------- /rawdata/rominjections.json: -------------------------------------------------------------------------------- 1 | { 2 | "dez2_layout": {"File": "DEZ/Layout/2.bin"}, 3 | "dez2_objects": {"File": "DEZ/Object Pos/2.bin"} 4 | } 5 | -------------------------------------------------------------------------------- /scripts/credits.lemon: -------------------------------------------------------------------------------- 1 | //More will be added to this in future 2 | 3 | //# address-hook(0x32000a) 4 | function void Credits.MetalSonic() 5 | { 6 | if (allocDynamicObjectStd() && objA0.value32 % 20 == 0) 7 | { 8 | spawnChildObject(0x32000C, 0x00, System.randRange(-16, 16), System.randRange(-16, 16)) 9 | } 10 | objA0.velocity.y = cos_s16((objA0.value32 % (0x648))*24)/8 11 | DrawObject() 12 | UpdateMovementStraightSimple() 13 | ++objA0.value32 14 | } 15 | 16 | //# address-hook(0x32000c) 17 | function void Credits.Explosion() 18 | { 19 | DrawObject() 20 | objA0.animation.sprite++ 21 | objA0.state++ 22 | if objA0.state == 12 23 | UnloadObject() 24 | } 25 | 26 | //# address-hook(0x05f0e2) end(0x05f104) 27 | function void fn05f0e2() 28 | { 29 | base.fn05f0e2() 30 | if (allocDynamicObjectStd()) 31 | { 32 | //Spawn Metal Sonic 33 | objA1.update_address = 0x32000a 34 | objA1.position.x.u16 = 140 35 | objA1.position.y.u16 = 90 36 | } 37 | } -------------------------------------------------------------------------------- /scripts/main.lemon: -------------------------------------------------------------------------------- 1 | //include shc 2 | include credits 3 | 4 | #if GAMEAPP < 0x24070100 5 | function s32 System.randRange(s32 min, s32 max) 6 | { 7 | return (System.rand() + min) % max 8 | } 9 | #endif 10 | 11 | global float MetalSonic.scale 12 | global float jetScale 13 | global u8 jetScaleDir 14 | 15 | global s16 MetalSonic.reticleX 16 | 17 | //Visual flags system 18 | global u16 MetalSonic.visualFlags = 0 19 | constant u16 VFLAG.SPARKING = 0x01 20 | constant u16 VFLAG.JETFLICKER = 0x02 // Not actually needed for now 21 | 22 | /* 23 | objA0.base_state - Metal Sonic 24 | 0x00 - Intro - Boring and basic, but will be improved on in the full release of the level mod, currently named Cosmic Revamp. 25 | 0x02 - Idle 26 | 0x04 - Jet attack 27 | 0x06 - Going down into idle (like 0, but it draws the bossbar) 28 | 0x08 - Laser shots above player 29 | 0x0a - BG spike bomb attack 30 | */ 31 | 32 | /* 33 | Object List 34 | 35 | 0x320000 - Metal Sonic 36 | 0x320002 - Laser projectile 37 | 0x320004 - Spike ball 38 | 0x320006 - Spike Ball explosion/Telegraph 39 | 0x320008 - Extra hitbox thing 40 | 0x32000A - Bad Ending Metal 41 | 0x32000C - Bad Ending explosion 42 | */ 43 | 44 | global u32 MetalSonic.screenboundLeft 45 | global u32 MetalSonic.screenboundRight 46 | global u32 MetalSonic.screenboundBottom 47 | 48 | function void DrawBossHealthBar(u8 boss.id, u8 bar.health, u8 bar.max) 49 | { 50 | base.DrawBossHealthBar(boss.id, bar.health, bar.max) 51 | } 52 | 53 | function s32 basiclerp(s32 val1, s32 val2, s32 div) 54 | { 55 | return val1 + ((val2-val1)/div) 56 | } 57 | 58 | function void updateScreenBounds() 59 | { 60 | //System.writeDisplayLine("Updating Screen Bounds") 61 | if (move_area.left == MetalSonic.screenboundLeft && move_area.right == MetalSonic.screenboundRight && move_area.bottom.current == MetalSonic.screenboundBottom) 62 | player1.camera_lock = 1 63 | move_area.left = basiclerp(move_area.left, MetalSonic.screenboundLeft, 32) 64 | move_area.right = basiclerp(move_area.right, MetalSonic.screenboundRight, 32) 65 | move_area.bottom.target = basiclerp(move_area.bottom.target, MetalSonic.screenboundBottom, 24) 66 | if (camera.position.x.u16 >= MetalSonic.screenboundLeft && camera.position.x.u16 + getScreenWidth() <= MetalSonic.screenboundRight && camera.position.y.u16 - 224 < MetalSonic.screenboundBottom) 67 | player1.camera_lock = 1 68 | else 69 | player1.camera_lock = 0 70 | } 71 | 72 | function void MoveScreenBounds(u32 left, u32 right) 73 | { 74 | //System.writeDisplayLine(stringformat("Screen Bounds moved to left: 0x%04x, right: 0x%04x", left, right)) 75 | MetalSonic.screenboundLeft = left 76 | MetalSonic.screenboundRight = right 77 | } 78 | 79 | function void MoveScreenBounds(u32 left, u32 right, u32 bottom) 80 | { 81 | //System.writeDisplayLine(stringformat("Screen Bounds moved to left: 0x%04x, right: 0x%04x, bottom: 0x%04x", left, right, bottom)) 82 | MetalSonic.screenboundLeft = left 83 | MetalSonic.screenboundRight = right 84 | MetalSonic.screenboundBottom = bottom 85 | } 86 | 87 | //# address-hook(0x07f0d2) end(0x07f0d8) 88 | function void fn07f0d2() 89 | { 90 | MetalSonic.Init() 91 | return 92 | 93 | base.fn07f0d2() 94 | } 95 | 96 | function u64 Standalone.getModdedSoundKey(u64 soundKey, u8 sfxId, u8 soundRegType) 97 | { 98 | if (sfxId == MUSIC_MAINBOSS && global.zone_act == 0x0b01) 99 | { 100 | return "metalsonic_boss" 101 | } 102 | return base.Standalone.getModdedSoundKey(soundKey, sfxId, soundRegType) 103 | } 104 | 105 | function void MetalSonic.Init() 106 | { 107 | if(allocDynamicObjectStd()) 108 | { 109 | MoveScreenBounds(0x3450, 0x3450, level.bossarea.bottom - 10) 110 | boss.remaining_hits = 8 111 | playMusic(MUSIC_MAINBOSS) 112 | objA0.update_address = 0x320000 113 | objA0.sprite_attributes = (0x06bc) 114 | objA0.render_flags = render_flag.WORLD| render_flag.VISIBLE 115 | objA0.sprite_priority = 0x100 116 | 117 | objA0.box_size.x = 14 118 | objA0.box_size.y = 24 119 | 120 | objA0.collision_attributes = collision.size.12x20 121 | objA0.hitbox_extends.x = 14 122 | objA0.hitbox_extends.y = 24 123 | 124 | objA0.position.x.u16 = 0x34F8 125 | objA0.position.y.u16 = 0x0279 126 | 127 | objA0.value2f = 0 128 | objA0.base_state = 0 129 | objA0.value32 = 0 130 | objA0.value39 = 0 131 | objA0.render_flags &= ~SPRITE_FLAG_FLIP_X 132 | MetalSonic.visualFlags = 0 133 | objA0.value26 = 0 134 | MetalSonic.scale = 1.0f 135 | 136 | spawnChildObject(0x320008, 0x00, 0, 0) 137 | } 138 | } 139 | 140 | //# address-hook(0x320000) 141 | function void MetalSonic.Update() 142 | { 143 | updateScreenBounds() 144 | if (!objA0.value39) 145 | { 146 | if Mods.isModActive("Bossbar") && (objA0.base_state != 0 || objA0.base_state == 0 && objA0.value32 >= 15) 147 | { 148 | DrawBossHealthBar(0xf5, boss.remaining_hits, 8) 149 | } 150 | MetalSonic.StateMachine() 151 | MetalSonic.HealthUpdater() 152 | } 153 | else 154 | { 155 | if (allocDynamicObjectStd() && level.framecounter % 7 == 0) 156 | { 157 | spawnChildObject(0x320006, 0x00, System.randRange(-16, 16), System.randRange(-16, 16)) 158 | } 159 | if (level.framecounter % 3 == 0) 160 | playSound(0xb4) 161 | if (objA0.value32 == 30) 162 | { 163 | objA0.velocity.y = -0x300 164 | } 165 | if (objA0.value32 > 30) 166 | { 167 | MoveWithGravity20() 168 | } 169 | if (objA0.position.y.u16 > camera.position.y.u16 + 224 + 32) 170 | { 171 | objA0.update_address = 0x085668 172 | objA0.position.y.u16 += 24 173 | objA0.flags2a |= 0x80 174 | u8[0xfffffab8] |= 0x01 175 | objA0.countdown_callback = 0x07f210 176 | } 177 | ++objA0.value32 178 | } 179 | if (objA0.position.y.u16 < camera.position.y.u16 + 224 + 32) 180 | { 181 | if (objA0.base_state != 0x00 && objA0.base_state != 0x0a && !objA0.value39) 182 | { 183 | Enemy.DrawDynamicObject() 184 | } 185 | else 186 | { 187 | DrawObject() 188 | } 189 | } 190 | } 191 | 192 | function void MetalSonic.StateMachine() 193 | { 194 | if (objA0.base_state == 0x00) 195 | { 196 | objA0.position.y.u16 += 2 197 | if (objA0.value32 == 45) 198 | { 199 | objA0.base_state = 0x02 200 | objA0.value32 = 1 201 | } 202 | ++objA0.value32 203 | } 204 | if (objA0.base_state == 0x02) 205 | { 206 | objA0.velocity.y = cos_s16((objA0.value32 % (0x648))*24)/8 207 | UpdateMovementStraightSimple() 208 | A1 = 0xffffb000 209 | if (objA0.position.x.u16 > objA1.position.x.u16) 210 | { 211 | objA0.render_flags |= SPRITE_FLAG_FLIP_X 212 | } 213 | else 214 | { 215 | objA0.render_flags &= ~SPRITE_FLAG_FLIP_X 216 | } 217 | 218 | if (objA0.value32 >= 40) 219 | { 220 | if (objA0.value2f == 0 || objA0.value2f == 2) 221 | objA0.position.y.u16 -= 2 222 | else 223 | objA0.position.x.u16 -= 8 224 | MetalSonic.visualFlags |= VFLAG.SPARKING 225 | if ((objA0.value2f == 0 || objA0.value2f == 2) && objA0.value32 == 175) 226 | { 227 | objA0.collision_attributes = collision.size.14x14 228 | MetalSonic.visualFlags &= ~VFLAG.SPARKING 229 | objA0.state = 0 230 | objA0.value26 = 0 231 | objA0.base_state = 0x04 232 | objA0.value32 = 0 233 | objA0.position.x.u16 = 0x33d9 234 | objA0.position.y.u16 = 0x032c 235 | objA0.value2f = 1 236 | } 237 | else if (objA0.value2f == 1 && objA0.value32 == 60) 238 | { 239 | objA0.position.x.u16 -= 4 240 | MetalSonic.visualFlags &= ~VFLAG.SPARKING 241 | objA0.state = 0 242 | objA0.value26 = 0 243 | objA0.base_state = 0x08 244 | objA0.value32 = 0 245 | objA0.value2f = 2 246 | } 247 | } 248 | ++objA0.value32 249 | } 250 | if(objA0.base_state == 0x04) 251 | { 252 | objA0.velocity.y = cos_s16((objA0.value32 % (0x648))*48)/2 253 | UpdateMovementStraightSimple() 254 | if (objA0.value32 == 1 || objA0.value32 == 110) 255 | { 256 | playSound(0x8e) 257 | } 258 | 259 | if (objA0.value32 == 30 || objA0.value32 == 140) 260 | { 261 | playSound(0xa2) 262 | } 263 | if (objA0.value32 > 30 && objA0.value32 < 77) 264 | { 265 | objA0.position.x.u16 += 15 266 | objA0.render_flags &= ~SPRITE_FLAG_FLIP_X 267 | } 268 | else if (objA0.value32 > 140) 269 | { 270 | objA0.position.x.u16 -= 15 271 | objA0.render_flags |= SPRITE_FLAG_FLIP_X 272 | } 273 | if (objA0.value32 == 350) 274 | { 275 | objA0.base_state = 0x06 276 | objA0.value32 = 0 277 | objA0.position.x.u16 = 0x34F8 278 | objA0.position.y.u16 = 0x0279 279 | objA0.collision_attributes = collision.size.12x20 280 | } 281 | ++objA0.value32 282 | } 283 | if (objA0.base_state == 0x06) 284 | { 285 | objA0.position.y.u16 += 2 286 | if (objA0.value32 == 45) 287 | { 288 | objA0.base_state = 0x02 289 | objA0.value32 = 0 290 | } 291 | ++objA0.value32 292 | } 293 | if (objA0.base_state == 0x08) 294 | { 295 | ++objA0.value32 296 | A1 = 0xffffb000 297 | 298 | if (objA0.value32 < 600) 299 | { 300 | if (objA0.value32 % 75 == 0) 301 | { 302 | MetalSonic.SpawnLaserBlast() 303 | } 304 | objA0.position.y.u16 = basiclerp(objA0.position.y.u16, objA1.position.y.u16, 15) 305 | } 306 | else if (objA0.value32 == 645) 307 | { 308 | MetalSonic.visualFlags &= ~VFLAG.SPARKING 309 | objA0.collision_attributes &= ~collision.flag.THREAT 310 | objA0.state = 0 311 | objA0.base_state = 0x0a 312 | objA0.value32 = 0 313 | objA0.value2f = 0 314 | playSound(0x60) 315 | } 316 | } 317 | if (objA0.base_state == 0x0a) 318 | { 319 | if (objA0.value32 < 120) 320 | { 321 | objA0.position.x.u16 = basiclerp(objA0.position.x.u16, 0x3437 + getScreenWidth()/2, 8) 322 | objA0.position.y.u16 = basiclerp(objA0.position.y.u16, 0x02A1, 24) 323 | objA0.value3e = 150 324 | } 325 | else if (objA0.value32 < 150*6) 326 | { 327 | if (objA0.value3e == 149) 328 | MetalSonic.Reticle() 329 | else if (objA0.value3e == 0) 330 | { 331 | MetalSonic.SpikeAttack() 332 | if (objA0.value32 < 150*6 - 60) 333 | objA0.value3e = 150 334 | } 335 | objA0.value3e-- 336 | } 337 | else if (objA0.value32 < 150*6 + 90) 338 | { 339 | objA0.position.y.u16 -= 2 340 | } 341 | else if (objA0.value32 == 150*6 + 90) 342 | { 343 | objA0.position.x.u16 = 0x34F8 344 | objA0.position.y.u16 = 0x0279 345 | objA0.base_state = 0x06 346 | objA0.value32 = 0 347 | } 348 | 349 | if (MetalSonic.scale > 0.5f && objA0.value32 < 150*6 - 120) 350 | { 351 | MetalSonic.scale -= 0.01f 352 | } 353 | if (objA0.value32 > 150*6 - 15 && MetalSonic.scale < 1.0f) 354 | { 355 | MetalSonic.scale += 0.01f 356 | } 357 | ++objA0.value32 358 | objA0.velocity.y = cos_s16((objA0.value32 % (0x648))*24)/16 359 | UpdateMovementStraightSimple() 360 | } 361 | if (objA0.base_state == 0x0c) 362 | { 363 | if (MetalSonic.scale < 1.0f) 364 | { 365 | MetalSonic.scale += 0.05f 366 | } 367 | ++objA0.value32 368 | } 369 | 370 | if (objA0.base_state != 0x0a && objA0.base_state != 0x0c) 371 | { 372 | MetalSonic.scale = 1.0f 373 | } 374 | } 375 | 376 | function void MetalSonic.SpawnLaserBlast() 377 | { 378 | if (allocDynamicObjectStd()) 379 | { 380 | objA0.value3a = 6 381 | playSound(0x54) 382 | spawnChildObject(0x320002, 0x00, 0, 0) 383 | } 384 | } 385 | 386 | //# address-hook(0x320002) 387 | function void MetalSonic.LaserBlast() 388 | { 389 | if (objA0.base_state == 0x00) 390 | { 391 | objA0.sprite_attributes = (sprite_attribute.PRIORITY | 0x06bc) 392 | objA0.render_flags = render_flag.WORLD| render_flag.VISIBLE 393 | 394 | objA0.box_size.x = 16 395 | objA0.box_size.y = 16 396 | 397 | objA0.collision_attributes = collision.size.14x14|collision.flag.THREAT 398 | objA0.hitbox_extends.x = 16 399 | objA0.hitbox_extends.y = 16 400 | 401 | objA0.velocity.x = 0x400 402 | objA0.base_state = 0x02 403 | } 404 | else if (objA0.base_state == 0x02) 405 | { 406 | Enemy.DrawDynamicObject() 407 | 408 | UpdateMovementStraightSimple() 409 | ++objA0.state 410 | if (!(objA0.render_flags & render_flag.VISIBLE)) 411 | { 412 | UnloadObject() 413 | } 414 | } 415 | } 416 | 417 | function void MetalSonic.Reticle() 418 | { 419 | if (allocDynamicObjectStd()) 420 | { 421 | playSound(0x9d) 422 | objA1.update_address = 0x320006 423 | objA1.subtype2c = 0x02 424 | objA1.render_flags = render_flag.WORLD| render_flag.VISIBLE 425 | A2 = 0xffffb000 426 | objA1.position.x.u16 = u16[A2 + 0x10] 427 | objA1.position.y.u16 = 0x032F 428 | } 429 | } 430 | 431 | function void MetalSonic.SpikeAttack() 432 | { 433 | playSound(0x98) 434 | if(allocDynamicObjectStd()) 435 | { 436 | objA1.update_address = 0x320004 437 | objA1.position.y.u16 = 0x0248 438 | } 439 | } 440 | 441 | //# address-hook(0x320004) 442 | function void MetalSonic.SpikeBall() 443 | { 444 | if (objA0.base_state == 0x00) 445 | { 446 | playSound(0x51) 447 | objA0.position.x.u16 = MetalSonic.reticleX 448 | objA0.collision_attributes = collision.size.8x8|collision.flag.THREAT 449 | 450 | objA0.render_flags |= render_flag.WORLD 451 | objA0.velocity.y = 6 452 | objA0.base_state = 0x02 453 | } 454 | else if (objA0.base_state == 0x02) 455 | { 456 | if (objA0.position.y.u16 >= 0x033c) 457 | { 458 | if (allocDynamicObjectStd()) 459 | { 460 | spawnChildObject(0x320006, 0x00, 0, 0) 461 | playSound(0xb4) 462 | objA1.render_flags |= render_flag.WORLD 463 | } 464 | UnloadObject() 465 | } 466 | if !Game.getSetting(SETTING_SMOOTH_ROTATION) 467 | { 468 | ++objA0.value2f 469 | if (objA0.value2f > 3) 470 | { 471 | objA0.value32 += 0x10 472 | objA0.value2f = 0 473 | } 474 | } 475 | else 476 | { 477 | objA0.value32 += 0x05 478 | } 479 | MoveWithGravity20() 480 | 481 | Enemy.DrawDynamicObject() 482 | } 483 | } 484 | 485 | //# address-hook(0x320006) 486 | function void MetalSonic.VisualObject() 487 | { 488 | objA0.sprite_attributes = (sprite_attribute.PRIORITY | 0x06bc) 489 | objA0.render_flags |= render_flag.WORLD 490 | if (objA0.subtype2c == 0x00) 491 | { 492 | MetalSonic.ProjectileExplosion() 493 | } 494 | else if (objA0.subtype2c == 0x02) 495 | { 496 | MetalSonic.TelegraphObject() 497 | } 498 | DrawObject() 499 | objA0.animation.sprite++ 500 | objA0.state++ 501 | } 502 | 503 | //# address-hook(0x320008) 504 | function void MetalSonic.AdditionalHitbox() 505 | { 506 | A1 = 0xffffb128 507 | if (objA1.base_state == 0x04) 508 | { 509 | objA0.collision_attributes = collision.size.20x16|collision.flag.THREAT 510 | } 511 | else 512 | { 513 | objA0.collision_attributes = collision.size.12x20 514 | } 515 | if (objA1.value39) 516 | objA0.collision_attributes &= ~collision.flag.THREAT 517 | 518 | if (objA0.collision_attributes & collision.flag.THREAT) 519 | Enemy.DrawDynamicObject() 520 | 521 | MoveWithParent() 522 | } 523 | 524 | function void MetalSonic.ProjectileExplosion() 525 | { 526 | if (objA0.state == 24) 527 | UnloadObject() 528 | } 529 | 530 | function void MetalSonic.TelegraphObject() 531 | { 532 | if (objA0.state < 120) 533 | { 534 | A1 = 0xffffb000 535 | objA0.position.x.u16 = objA1.position.x.u16 536 | MetalSonic.reticleX = objA0.position.x.u16 537 | } 538 | 539 | if (objA0.state == 150) 540 | UnloadObject() 541 | } 542 | 543 | function void MetalSonic.HealthUpdater() 544 | { 545 | if (objA0.collision_attributes != 0) 546 | return 547 | 548 | if (boss.remaining_hits == 0) 549 | { 550 | // whatever code you wanna run for when the last hit is landed 551 | //spawnChildObject(0x083d84, 0x00, 0, 0) 552 | /* 553 | if (_equal()) 554 | { 555 | u8[A1 + 0x2c] = 0x04 556 | } 557 | */ 558 | MoveScreenBounds(level.bossarea.left, level.bossarea.right, level.bossarea.bottom) 559 | objA0.value32 = 0 560 | objA0.value39 = 1 561 | AddScoreForBossEnemy() 562 | return 563 | } 564 | 565 | if (objA0.state == 0) 566 | { 567 | objA0.state = 0x20 568 | playSound(0x6e) 569 | ++u8[A1 + 0x29] 570 | objA0.flags2a |= 0x40 571 | } 572 | --objA0.state 573 | if (objA0.state == 0) 574 | { 575 | objA0.flags2a &= ~0x40 576 | u8[A0 + 0x28] = u8[A0 + 0x25] 577 | } 578 | } 579 | 580 | //This function probably looks like a mess 581 | function bool Standalone.onWriteToSpriteTable(s16 px, s16 py, u16 renderQueue) 582 | { 583 | if (objA0.update_address == 0x320000) 584 | { 585 | u64 key 586 | u32 MetalSonic.renderQueue = 0x8fff 587 | 588 | //BG attack? Use lower renderqueue 589 | MetalSonic.renderQueue = (objA0.base_state == 0x0c) ? 0x3000 : 0x8fff 590 | 591 | if (objA0.base_state != 0x04 && objA0.base_state != 0x0a) 592 | { 593 | key = "metalsonic_idle" 594 | } 595 | else if (objA0.base_state == 0x04) 596 | { 597 | key = stringformat("metalsonic_attack_0x0%d", 1 + (level.framecounter.low & 0x01)) 598 | } 599 | else if (objA0.base_state == 0x0a) 600 | { 601 | key = "metalsonic_facingforward" 602 | } 603 | 604 | if (objA0.value26 > 0) 605 | { 606 | key = stringformat("metalsonic_attack_0x0%d", objA0.value26 - 1) 607 | } 608 | 609 | if (MetalSonic.visualFlags & VFLAG.SPARKING) 610 | { 611 | key = stringformat("metalsonic_spark_0x0%d", level.framecounter.low & 0x01) 612 | if (level.framecounter.low & 0x01) 613 | { 614 | Renderer.drawCustomSprite("metalsonic_sparkeffect", px, py, 0x00, SPRITE_FLAG_PRIO | render_flag.WORLD, MetalSonic.renderQueue + 1) 615 | playSound(0x5c) 616 | } 617 | } 618 | 619 | if (objA0.value3a) 620 | { 621 | if (level.framecounter % 4 == 0) 622 | objA0.value3a-- 623 | key = stringformat("metalsonic_lasershot_0x0%d", objA0.value3a <= 4) 624 | } 625 | 626 | //Hit? Use hit flash sprites 627 | if (objA0.state && (level.framecounter.low & 0x01)) 628 | { 629 | if (key != "metalsonic_attack_0x00" && key != "metalsonic_attack_0x01" && key != "metalsonic_facingforward" && key != "metalsonic_spark_0x00" && key != "metalsonic_spark_0x01") // This check is quite big, but it ensures that the correct frames get the right sprite key. 630 | { 631 | key = stringformat("%s_hit", key) 632 | } 633 | } 634 | 635 | //Dead? Use the dead sprite 636 | if (objA0.value39 && objA0.value32 > 30) 637 | { 638 | key = "metalsonic_dead" 639 | } 640 | 641 | if (MetalSonic.scale < 0.5f) 642 | { 643 | px = getScreenWidth() / 2 644 | } 645 | 646 | float maxScale = (MetalSonic.scale < 0.5f) ? 0.45f : 0.9f 647 | float minScale = (MetalSonic.scale < 0.5f) ? 0.25f : 0.5f 648 | 649 | if ((objA0.value26 < 2 && objA0.base_state != 0x04) && !objA0.value39) 650 | { 651 | s16 dx = (objA0.render_flags & SPRITE_FLAG_FLIP_X ? 9:-9) 652 | if (key == "metalsonic_facingforward") 653 | { 654 | dx = 0 655 | } 656 | if jetScale < minScale 657 | jetScaleDir = 1 658 | else if jetScale > maxScale 659 | jetScaleDir = 0 660 | 661 | if jetScaleDir == 0 662 | { 663 | jetScale -= (MetalSonic.visualFlags & VFLAG.JETFLICKER && objA0.base_state == 0x0c) ? 0.025f : 0.075f 664 | } 665 | else if jetScaleDir == 1 666 | { 667 | jetScale += (MetalSonic.visualFlags & VFLAG.JETFLICKER && objA0.base_state == 0x0c) ? 0.025f : 0.075f 668 | } 669 | SpriteHandle jetSpr = Renderer.addSpriteHandle("jet_0x03", px + dx, py, MetalSonic.renderQueue - 1) 670 | jetSpr.setBlendMode(3) 671 | jetSpr.setScale(MetalSonic.scale - jetScale) 672 | } 673 | 674 | SpriteHandle metalSpr = Renderer.addSpriteHandle(key, px, py, MetalSonic.renderQueue) 675 | metalSpr.setFlags(0x40) 676 | metalSpr.setScale(MetalSonic.scale) 677 | metalSpr.setFlipX(objA0.render_flags & SPRITE_FLAG_FLIP_X) 678 | return true 679 | } 680 | if (objA0.update_address == 0x320002) 681 | { 682 | SpriteHandle laserSprite = Renderer.addSpriteHandle("laser_0x03", objA0.position.x.u16, objA0.position.y.u16, 0x4005) 683 | laserSprite.setScale(jetScale) 684 | laserSprite.setFlags(0x20) 685 | laserSprite.setBlendMode(3) 686 | } 687 | if (objA0.update_address == 0x320004) 688 | { 689 | Renderer.drawCustomSprite("metalsonic_spikeball", px, py, 0, SPRITE_FLAG_PRIO | render_flag.WORLD, renderQueue, objA0.value32, 0xff) 690 | return true 691 | } 692 | if (objA0.update_address == 0x320006) 693 | { 694 | if (objA0.subtype2c == 0x00) 695 | { 696 | Renderer.drawCustomSprite(stringformat("boss_explosion_0x0%d", (objA0.animation.sprite / 4) % 6), px, py, 0, SPRITE_FLAG_PRIO | render_flag.WORLD, renderQueue) 697 | } 698 | else if (objA0.subtype2c == 0x02) 699 | { 700 | Renderer.drawCustomSprite(stringformat("hitmarker_0x0%d", (objA0.animation.sprite / 4) % 2), px, py, 0, SPRITE_FLAG_PRIO | render_flag.WORLD, renderQueue) 701 | } 702 | return true 703 | } 704 | if (objA0.update_address == 0x320008) 705 | { 706 | return true 707 | } 708 | if (objA0.update_address == 0x32000A) 709 | { 710 | px = objA0.position.x.u16 711 | py = objA0.position.y.u16 712 | 713 | float maxScale = 0.9f 714 | float minScale = 0.5f 715 | 716 | if ((objA0.value26 < 2 && objA0.base_state != 0x04) && !objA0.value39) 717 | { 718 | if jetScale < minScale 719 | jetScaleDir = 1 720 | else if jetScale > maxScale 721 | jetScaleDir = 0 722 | 723 | if jetScaleDir == 0 724 | { 725 | jetScale -= (MetalSonic.visualFlags & VFLAG.JETFLICKER && objA0.base_state == 0x0c) ? 0.025f : 0.075f 726 | } 727 | else if jetScaleDir == 1 728 | { 729 | jetScale += (MetalSonic.visualFlags & VFLAG.JETFLICKER && objA0.base_state == 0x0c) ? 0.025f : 0.075f 730 | } 731 | SpriteHandle jetSpr = Renderer.addSpriteHandle("jet_0x03", px - 9, py, 0x9ffe) 732 | jetSpr.setBlendMode(3) 733 | jetSpr.setScale(MetalSonic.scale - jetScale) 734 | } 735 | 736 | SpriteHandle metalSpr = Renderer.addSpriteHandle("metalsonic_idle", px, py, 0x9fff) 737 | metalSpr.setFlags(0x40) 738 | return true 739 | } 740 | if (objA0.update_address == 0x32000C) 741 | { 742 | px = objA0.position.x.u16 743 | py = objA0.position.y.u16 744 | renderQueue = 0x9fff 745 | Renderer.drawCustomSprite(stringformat("boss_explosion_0x0%d", (objA0.animation.sprite / 4) % 6), px, py, 0, SPRITE_FLAG_PRIO | render_flag.WORLD, renderQueue) 746 | return true 747 | } 748 | return base.Standalone.onWriteToSpriteTable(px, py, renderQueue) 749 | } 750 | 751 | //# address-hook(0x085ca4) end(0x085d68) 752 | function void CheckForBossStart() 753 | { 754 | if global.zone_act != 0x0b01 755 | { 756 | base.CheckForBossStart() 757 | return 758 | } 759 | // Condition 1: Countdown (can be zero from start) 760 | if ((u8[A0 + 0x27] & 0x01) == 0) 761 | { 762 | --objA0.countdown_value 763 | if (objA0.countdown_value < 0) 764 | { 765 | u8[A0 + 0x27] |= 0x01 766 | level.default_music.u8 = objA0.value32 767 | playMusic(objA0.value32) 768 | } 769 | } 770 | 771 | // Condition 2: Camera Y position 772 | if ((u8[A0 + 0x27] & 0x02) == 0) 773 | { 774 | bool fulfilled = false 775 | if (u8[A0 + 0x27] & 0x80) 776 | { 777 | fulfilled = (camera.position.y.u16 <= level.bossarea.bottom + 0x60) 778 | } 779 | else 780 | { 781 | fulfilled = (camera.position.y.u16 >= level.bossarea.top) 782 | level.vertical_wrap = camera.position.y.u16 // To be overwritten if condition fulfilled 783 | } 784 | 785 | if (fulfilled) 786 | { 787 | u8[A0 + 0x27] |= 0x02 788 | level.vertical_wrap = level.bossarea.top 789 | move_area.bottom.target = level.bossarea.bottom 790 | } 791 | } 792 | 793 | // Condition 3: Camera X position 794 | if ((u8[A0 + 0x27] & 0x04) == 0) 795 | { 796 | bool fulfilled = false 797 | if (u8[A0 + 0x27] & 0x40) 798 | { 799 | fulfilled = (camera.position.x.u16 > level.bossarea.right) 800 | move_area.right = camera.position.x.u16 801 | } 802 | else 803 | { 804 | fulfilled = (camera.position.x.u16 >= level.bossarea.left) 805 | move_area.left = camera.position.x.u16 806 | } 807 | 808 | if (fulfilled) 809 | { 810 | u8[A0 + 0x27] |= 0x04 811 | move_area.left = level.bossarea.left 812 | move_area.right = level.bossarea.right 813 | } 814 | } 815 | 816 | // All conditions must be fulfilled to pass, and actually start the boss fight 817 | if ((u8[A0 + 0x27] & 0x07) != 0x07) 818 | return 819 | 820 | u8[A0 + 0x27] = 0 821 | u16[A0 + 0x1c] = 0 822 | objA0.value32 = 0 823 | 824 | A1 = objA0.countdown_callback 825 | call A1 826 | } 827 | -------------------------------------------------------------------------------- /scripts/shc.lemon: -------------------------------------------------------------------------------- 1 | global s16 shc_metalAnimCounter 2 | global u16 shc_metalOpacity 3 | 4 | function bool SHCSplash.shouldShowUpAfterStartup() 5 | { 6 | return true 7 | } 8 | 9 | function void SHCSplash.showSplashScreen() 10 | { 11 | s16 opacity = 0 12 | shc_metalOpacity = opacity 13 | u16 controllerPressed = 0 14 | 15 | // Wait briefly in a black screen 16 | SHCSplash.setPalette(0) 17 | for (u8 frame = 0; frame < 30; ++frame) 18 | { 19 | Renderer.resetSprites() 20 | yieldExecution() 21 | } 22 | 23 | // Fade in 24 | shc_metalOpacity += 7 25 | for (; opacity <= 0x100; opacity += 8) 26 | { 27 | if (shc_metalOpacity < 0xff - 8) 28 | { 29 | shc_metalOpacity += 8 30 | } 31 | MetalSonic.SHCRender() 32 | SHCSplash.setPalette(opacity) 33 | Renderer.resetSprites() 34 | Renderer.drawCustomSprite("shc_splash_frame_02", getScreenWidth() / 2, getScreenHeight() / 2, 0, 0, 0xa000) 35 | 36 | yieldExecution() 37 | } 38 | 39 | constant array ANIMATION_DATA = // Animation data from Mania version of the splash screen 40 | { 41 | // Sprite number, duration 42 | 2, 8, // Original duration: 64 - but shortened here to accommodate for the fade-in 43 | 1, 1, 44 | 0, 1, 45 | 1, 1, 46 | 2, 1, 47 | 1, 1, 48 | 0, 2, 49 | 2, 1, 50 | 0, 2, 51 | 1, 1, 52 | 2, 3, 53 | 0, 1, 54 | 2, 4, 55 | 1, 1, 56 | 0, 3, 57 | 1, 2, 58 | 0, 3, 59 | 1, 2, 60 | 2, 4, 61 | 1, 2, 62 | 0, 3, 63 | 2, 4, 64 | 0, 64, 65 | 2, 4, 66 | 1, 2, 67 | 0, 3, 68 | 2, 2, 69 | 0, 3, 70 | 1, 2, 71 | 2, 2, 72 | 0, 3, 73 | 2, 2, 74 | 0, 3, 75 | 2, 2, 76 | 1, 2, 77 | 0, 3, 78 | 1, 2, 79 | 0, 64, 80 | 2, 2, 81 | 0, 3, 82 | 1, 2, 83 | 2, 2, 84 | 0, 3, 85 | 2, 2, 86 | 1, 2, 87 | 0, 3, 88 | 1, 2, 89 | 2, 2, 90 | 1, 2, 91 | 2, 2, 92 | 1, 2, 93 | 0, 3, 94 | 2, 2, 95 | 0, 3, 96 | 1, 2, 97 | 2, 8 // Original duration: 360 - but shortened here, as that was certainly only meant to effectively stop the animation here 98 | } 99 | 100 | Audio.playAudio("shc_splash") 101 | 102 | // Show the animation 103 | u8 animationStep = 0 104 | u8 animationCounter = 0 105 | while (animationStep * 2 < ANIMATION_DATA.length()) 106 | { 107 | MetalSonic.SHCRender() 108 | controllerPressed |= Input.getController(0) 109 | if (controllerPressed != 0) 110 | break 111 | 112 | u8 currentStepSprite = ANIMATION_DATA[animationStep * 2] 113 | u8 currentStepDuration = ANIMATION_DATA[animationStep * 2 + 1] 114 | Renderer.resetSprites() 115 | Renderer.drawCustomSprite(stringformat("shc_splash_frame_%02d", currentStepSprite), getScreenWidth() / 2, getScreenHeight() / 2, 0, 0, 0xa000) 116 | 117 | ++animationCounter 118 | if (animationCounter >= currentStepDuration) 119 | { 120 | ++animationStep 121 | animationCounter = 0 122 | } 123 | 124 | yieldExecution() 125 | } 126 | 127 | shc_metalOpacity -= 7 128 | // Fade out 129 | for (; opacity >= 0; opacity -= (controllerPressed != 0) ? 0x10 : 8) 130 | { 131 | if (shc_metalOpacity > 0) 132 | { 133 | shc_metalOpacity -= (controllerPressed != 0) ? 15 : 7 134 | } 135 | MetalSonic.SHCRender() 136 | controllerPressed |= Input.getController(0) 137 | 138 | SHCSplash.setPalette(opacity) 139 | Renderer.resetSprites() 140 | Renderer.drawCustomSprite("shc_splash_frame_02", getScreenWidth() / 2, getScreenHeight() / 2, 0, 0, 0xa000) 141 | 142 | yieldExecution() 143 | } 144 | } 145 | 146 | function void MetalSonic.SHCRender() 147 | { 148 | float maxScale = 0.9f 149 | float minScale = 0.5f 150 | 151 | u16 shcMetalSonic_posX = 190 152 | u16 shcMetalSonic_posY = 170 + cos_s16((shc_metalAnimCounter % (0x648))*24)/48 153 | 154 | Renderer.drawCustomSprite("metalsonic_facingforward", shcMetalSonic_posX, shcMetalSonic_posY, 0, 0, 0xa002, 0, shc_metalOpacity) 155 | 156 | if jetScale < minScale 157 | jetScaleDir = 1 158 | else if jetScale > maxScale 159 | jetScaleDir = 0 160 | 161 | if jetScaleDir == 0 162 | { 163 | jetScale -= 0.075f 164 | } 165 | else if jetScaleDir == 1 166 | { 167 | jetScale += 0.075f 168 | } 169 | Renderer.drawSpriteTinted("jet_0x03", shcMetalSonic_posX, shcMetalSonic_posY, 0, 0, 0xa001, 0, -shc_metalOpacity, jetScale) 170 | SpriteHandle jetSpr = Renderer.addSpriteHandle("jet_0x03", shcMetalSonic_posX, shcMetalSonic_posY, 0xa001) 171 | jetSpr.setBlendMode(3) 172 | jetSpr.setScale(jetScale) 173 | ++shc_metalAnimCounter 174 | 175 | jetSpr.setOpacity(shc_metalOpacity) 176 | } -------------------------------------------------------------------------------- /sprites/boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/boss.png -------------------------------------------------------------------------------- /sprites/boss_hit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/boss_hit.png -------------------------------------------------------------------------------- /sprites/bossname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/bossname.png -------------------------------------------------------------------------------- /sprites/explosion.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/explosion.bmp -------------------------------------------------------------------------------- /sprites/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/explosion.png -------------------------------------------------------------------------------- /sprites/markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/markers.png -------------------------------------------------------------------------------- /sprites/metalsonic.json: -------------------------------------------------------------------------------- 1 | { 2 | //Metal Sonic sprites made by TannerTH25 and TheIrishSpriter 3 | "metalsonic_idle": { "File": "boss.png", "Rect": "0,0,27,47", "Center": "12,22" }, 4 | "metalsonic_attack_0x00": { "File": "boss.png", "Rect": "28,0,45,23", "Center": "22,11" }, 5 | "metalsonic_attack_0x01": { "File": "boss.png", "Rect": "28,27,48,28", "Center": "24,14" }, 6 | "metalsonic_attack_0x02": { "File": "boss.png", "Rect": "0,56,47,32", "Center": "23,16" }, 7 | "metalsonic_dead": { "File": "boss.png", "Rect": "48,56,31,38", "Center": "15,19" }, 8 | "metalsonic_idle_hit": { "File": "boss_hit.png", "Rect": "0,0,27,47", "Center": "12,22" }, 9 | "metalsonic_attack_0x00_hit": { "File": "boss_hit.png", "Rect": "28,0,45,23", "Center": "22,11" }, 10 | "metalsonic_attack_0x01_hit": { "File": "boss_hit.png", "Rect": "28,27,48,28", "Center": "24,14" }, 11 | "metalsonic_attack_0x02_hit": { "File": "boss_hit.png", "Rect": "0,56,47,32", "Center": "23,16" }, 12 | "metalsonic_spark_0x00": { "File": "boss.png", "Rect": "178,1,32,49", "Center": "16,24" }, 13 | "metalsonic_spark_0x01": { "File": "boss.png", "Rect": "211,1,32,49", "Center": "16,24" }, 14 | "metalsonic_sparkeffect": { "File": "boss.png", "Rect": "244,1,48,49", "Center": "24,24" }, 15 | //Laser Shooting and Facing Forward done by Benkoopa 16 | "metalsonic_lasershot_0x00": { "File": "boss.png", "Rect": "209,59,30,47", "Center": "15,23" }, 17 | "metalsonic_lasershot_0x01": { "File": "boss.png", "Rect": "248,57,32,49", "Center": "16,24" }, 18 | "metalsonic_lasershot_0x00_hit": { "File": "boss.png", "Rect": "209,59,30,47", "Center": "15,23" }, 19 | "metalsonic_lasershot_0x01_hit": { "File": "boss.png", "Rect": "248,57,32,49", "Center": "16,24" }, 20 | "metalsonic_facingforward": { "File": "boss.png", "Rect": "147,1,30,45", "Center": "15,22" }, 21 | //Jet Sprites 22 | "jet_0x00": { "File": "boss.png", "Rect": "74,0,10,10", "Center": "5,5" }, 23 | "jet_0x01": { "File": "boss.png", "Rect": "77,11,16,16", "Center": "8,8" }, 24 | "jet_0x02": { "File": "boss.png", "Rect": "77,28,24,24", "Center": "12,12" }, 25 | "jet_0x03": { "File": "boss.png", "Rect": "80,53,32,32", "Center": "16,16" }, 26 | //Laser sprites, recoloured jets 27 | "laser_0x00": { "File": "boss.png", "Rect": "85,0,10,10", "Center": "5,5" }, 28 | "laser_0x01": { "File": "boss.png", "Rect": "94,11,16,16", "Center": "8,8" }, 29 | "laser_0x02": { "File": "boss.png", "Rect": "102,28,24,24", "Center": "12,12" }, 30 | "laser_0x03": { "File": "boss.png", "Rect": "113,53,32,32", "Center": "16,16" }, 31 | //Bossbar name 32 | "bossf5": { "File": "bossname.png", "Rect": "0,0,0,0", "Center": "0,0" }, 33 | //hitmarkers 34 | "hitmarker_0x00": { "File": "markers.png", "Rect": "0,0,20,20", "Center": "10,10" }, 35 | "hitmarker_0x01": { "File": "markers.png", "Rect": "20,0,20,20", "Center": "10,10" }, 36 | //Spikeball 37 | "metalsonic_spikeball": { "File": "spikebomb.png", "Rect": "0,0,15,15", "Center": "7,7" }, 38 | //Explosions for the spikeballs, bmp from UML Demo 3 39 | "boss_explosion_0x00": { "File": "explosion.png", "Rect": "0,1,16,16", "Center": "8,8" }, 40 | "boss_explosion_0x01": { "File": "explosion.png", "Rect": "17,1,24,24", "Center": "12,12" }, 41 | "boss_explosion_0x02": { "File": "explosion.png", "Rect": "42,1,24,24", "Center": "12,12" }, 42 | "boss_explosion_0x03": { "File": "explosion.png", "Rect": "67,1,24,24", "Center": "12,12" }, 43 | "boss_explosion_0x04": { "File": "explosion.png", "Rect": "92,1,24,24", "Center": "12,13" }, 44 | "boss_explosion_0x05": { "File": "explosion.png", "Rect": "117,0,24,16", "Center": "12,10" } 45 | } 46 | -------------------------------------------------------------------------------- /sprites/spikebomb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmicPikachu001/s3air-vsmetalsonic/07bf95f2a439f51600101f9e2fd7764915224807/sprites/spikebomb.png --------------------------------------------------------------------------------