--[[ Uncle Broadsword API v1.1.0 Created by Ohmato/Arabsalmon THINGS TO DO (Please list any bugs/suggestions here): -- Expose sword colliders (ON HOLD - WAITING FOR STANDARDIZATION) ]] local colliders = require("colliders") local rng = require("rng") local pm = require("playerManager") local blockutils = require("blocks/blockutils") local unclebroadsword = {} unclebroadsword.debugMode = false -- Graphics/Audio Loading -------------------------------------------------------------------- local sfx = { drop = pm.registerSound(CHARACTER_UNCLEBROADSWORD, "ub_drop.wav"), knife = pm.registerSound(CHARACTER_UNCLEBROADSWORD, "ub_knife.wav"), lunge = pm.registerSound(CHARACTER_UNCLEBROADSWORD, "ub_lunge.wav"), swipe = pm.registerSound(CHARACTER_UNCLEBROADSWORD, "ub_swipe.wav"), charging = pm.registerSound(CHARACTER_UNCLEBROADSWORD, "ub_charging.wav") } local deathFX_left = pm.registerGraphic(CHARACTER_UNCLEBROADSWORD, "dead_left.png") local deathFX_right = pm.registerGraphic(CHARACTER_UNCLEBROADSWORD, "dead_right.png") local afterimage = pm.registerGraphic(CHARACTER_UNCLEBROADSWORD, "afterimage.png") local statue_img = pm.registerGraphic(CHARACTER_UNCLEBROADSWORD, "statue_img.png") local itembox = pm.registerGraphic(CHARACTER_UNCLEBROADSWORD, "itembox.png") local afterimagepos = {} -- Positions for stall-and-fall afterimages local charge_sfxobj = nil -- SFX channel for charging sound effect Graphics.registerCharacterHUD(CHARACTER_UNCLEBROADSWORD, Graphics.HUD_ITEMBOX, nil, { reserveBox = itembox, reserveBox2P = itembox }) -- Attack logic ------------------------------------------------------------------------------ unclebroadsword.swordCollider = nil -- Sword hitbox local ATKSTATE = { -- Possible sword swipe states COOLDOWN = -6, -- Recovery period, cannot swing sword CHARGED = -5, -- Ready to release charged lunge CHARGING = -4, -- Currently charging lunge STALLED = -3, -- Can't move after stall-and-fall PAUSE2 = -2, -- Pause after upward swipe PAUSE1 = -1, -- Pause after downward swipe NONE = 0, -- Ready to attack SWIPE1 = 1, -- Upward swipe SWIPE2 = 2, -- Downward swipe LUNGE_COMBO = 3, -- Combo lunging after two swipes LUNGE_CHARGE = 4, -- Performing charged lunge UPWARDSTAB = 5, -- Stabbing upward (in air) STALLANDFALL = 6, -- Stall-and-fall STATUEFALL = 7 -- Falling as statue } unclebroadsword.attackState = ATKSTATE.NONE -- Current attack state local queue_state = ATKSTATE.NONE -- Next attack state to assign if already in the middle of one local attack_timer = 0 -- Timer for managing attack state local can_aerial = true -- Can an aerial swipe be performed? local can_stallnfall = true -- Can a stall-and-fall be performed? local charge_timer = 0 -- Timer for charging lunge local charge_fraction = 0 -- Fraction of the charge meter filled before a charged lunge local doublejump_ready = true -- Is a double jump available? local aerial_atk_stall = false -- Should the player stall in the air when swiping? -- Attack configuration ---------------------------------------------------------------------- local DURATION = { -- Frame durations for attack state [ATKSTATE.CHARGING] = 70, -- Time required to fully charge a lunge [ATKSTATE.STALLED] = 24, -- Time after a stall-and-fall during which input is blocked [ATKSTATE.PAUSE1] = 12, -- Pause length between swipes [ATKSTATE.PAUSE2] = 12, [ATKSTATE.COOLDOWN] = 3, -- Time required after attacking before you can attack again [ATKSTATE.SWIPE1] = 2, -- (x3) [ATKSTATE.SWIPE2] = 2, -- (x3) [ATKSTATE.LUNGE_COMBO] = 30, -- Time required to perform a full combo lunge [ATKSTATE.LUNGE_CHARGE] = 40 -- Time required to perform a full charged lunge } local LUNGEDIST_COMBO = 256 -- Distance travelled during a combo lunge local LUNGEDIST_CHARGE = 192 -- Distance travelled during a charged lunge local LUNGELAG_COMBO = 10 -- Frames of end lag after a combo lunge local LUNGELAG_CHARGE = 25 -- Frames of end lag after a charged lunge local CHARGING_SOUND_DURATION = 44 -- Duration of "charging" sound effect ===== DO NOT CHANGE ===== local FALLSPEED = 40 -- Maximum speed of stall-and-fall local AFTERIMAGE_COUNT = 5 -- How many afterimages does the stall-and-falla leave? local SWIPE_WIDTH = 38 -- Width of swipe hitbox local SWIPE_HEIGHT = 28 -- Height of swipe hitbox local LUNGE_WIDTH = 40 -- Width of lunge hitbox local LUNGE_HEIGHT = 20 -- Height of lunge hitbox local UDSTAB_WIDTH = 16 -- Width of up/downstab hitbox local UDSTAB_HEIGHT = 20 -- Height of up/downstab hitbox -- Player states ----------------------------------------------------------------------------- local function ducking_ext() -- Ignore if not holding down if not player.downKeyPressing then return false end -- Check if dead if player:mem(0x13e, FIELD_WORD) > 0 then return false end -- Check if submerged and standing on the ground if (player:mem(0x34, FIELD_WORD) ~= 0 or player:mem(0x36, FIELD_WORD) ~= 0) and not player:isGroundTouching() then return false end -- Check if on a slope and not underwater if player:mem(0x48, FIELD_WORD) ~= 0 and player:mem(0x36, FIELD_WORD) == 0 then return false end -- Check if sliding, climbing, holding an item, in statue form, or spinjumping if player:mem(0x3c, FIELD_WORD) ~= 0 or player:mem(0x40, FIELD_WORD) ~= 0 or player:mem(0x154, FIELD_WORD) > 0 or player:mem(0x4a, FIELD_WORD) == -1 or player:mem(0x50, FIELD_WORD) ~= 0 then return false end -- Check if in a forced animation if player.forcedState == 1 -- Powering up or player.forcedState == 2 -- Powering down or player.forcedState == 3 -- Entering pipe or player.forcedState == 4 -- Getting fire flower or player.forcedState == 7 -- Entering door or player.forcedState == 41 -- Getting ice flower or player.forcedState == 500 then return false end return true end local function ducking() -- In ducking state if player.powerup == PLAYER_SMALL then return ducking_ext() else return (player:mem(0x12e, FIELD_WORD) ~= 0) end end local function submerged() -- In quicksand or water return (player:mem(0x34, FIELD_WORD) ~= 0 or player:mem(0x36, FIELD_WORD) ~= 0) end local function sliding() -- Sliding down slope return (player:mem(0x3c, FIELD_WORD) ~= 0) end local function climbing() -- Climbing ladder/fence return (player:mem(0x40, FIELD_WORD) ~= 0) end local function spinjumping() -- Spinjumping return (player:mem(0x50, FIELD_WORD) ~= 0) end local function mounted() -- On a mount return (player:mem(0x108, FIELD_WORD) > 0) end local function holding() -- Holding an object return (player:mem(0x154, FIELD_WORD) > 0) end local function grounded() -- Touching solid ground return player:isGroundTouching(); end local function pickingup() -- Currently picking something up return (player:mem(0x26, FIELD_WORD) > 0) end local function standing() -- On the ground, not ducking, not on a mount, and not sliding return (not ducking() and grounded() and player:mem(0x108, FIELD_WORD) == 0 and player:mem(0x3c, FIELD_WORD) == 0) end local function airborne() -- In the air and not ducking return not (grounded() or submerged() or climbing()) end local function inforcedanim() -- In a forced animation return (player.forcedState ~= 0) end local function statued() -- In tanooki statue form return (player:mem(0x4a, FIELD_WORD) == -1) end local function warping() return (player.forcedState == 3) end local is_hurt = false -- Has the player just been hurt? local knockback = true -- Should knockback be delivered? -- Entity typing ----------------------------------------------------------------------------- local BLOCK_PLAYERSWITCH = {622,623,624,625,631,639,641,643,645,647,649,651,653,655,657,659,661,663} local BLOCK_BRICK = {4,60,188,226,293, 90} local BLOCK_BOUNCY = {55, 682} local NPC_BONUS = {10,33,88,103,138,152,240,248,251,252,253,258,274} local NPC_POWERUP = {9,14,34,90,153,169,170,182,183,184,185,186,187,188,249,250,254,264,273,277,287,287,293} local NPC_CARRYABLETOOL = {22,26,31,45,49,92,139,140,141,142,143,144,145,146,147,154,155,156,157,158,159,179,195,241,278} -- Does not include Mouser's bomb local NPC_BOUNCY = {26} local function isType(id, idtable) for _, val in ipairs(idtable) do if id == val then return true end end return false end -- PLAYER_SMALL frame management ------------------------------------------------------------- local holding_frames = {9,8,9,10} -- Frames to display when holding an item local holding_frame_timer = 1 -- Frame timer for holding frames local holding_frame_index = 1 -- Current holding frame to display local walking_frames = {2,1,2,3} -- Frames to display when walking/running local walking_frame_timer = 1 -- Frame timer for walking/running frames local walking_frame_index = 1 -- Current walking/running frame to display local running_durations = {1,2,2} -- Frame durations to use when approaching running speed local running_duration_index = 1 -- Current frame duration to use when running ---------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------- INPUT MANAGEMENT -------------------------------- local bufferedInputs = {"jump", "altJump"} local inputBuffer = {} local function BlockMovement() -------------------------------------------------------------------- Block arrow movement and jumping --player.upKeyPressing = false if is_hurt or (not ducking() and not airborne()) then player.downKeyPressing = false end for k,v in ipairs(bufferedInputs) do inputBuffer[v] = player.keys[v] end player.leftKeyPressing = false player.rightKeyPressing = false player.jumpKeyPressing = false player.altJumpKeyPressing = false player.altRunKeyPressing = false end local function RemoveInputBuffer() for k,v in ipairs(bufferedInputs) do if inputBuffer[v] then if player.keys[v] then player.keys[v] = KEYS_UP player[v .. "KeyPressing"] = false else inputBuffer[v] = false end end end end function unclebroadsword.getAttackState() return unclebroadsword.attackState end local function SetCooldown() ---------------------------------------------------------------------- Sets cooldown state unclebroadsword.attackState = ATKSTATE.COOLDOWN attack_timer = DURATION[ATKSTATE.COOLDOWN] end function unclebroadsword.onKeyDown(keycode, playerIndex) if player.character == CHARACTER_UNCLEBROADSWORD and not is_hurt and not inforcedanim() then -- If pressing the attack key if keycode == KEY_X and not (is_hurt or statued()) then -- Prevent attacking when submerged, sliding, climbing, spinjumping, mounted, holding, or picking up something if inforcedanim() or submerged() or sliding() or climbing() or spinjumping() or mounted() or holding() or pickingup() then return end -- Prevent attacking in air if you already have if airborne() and not can_aerial then return end -- Alter attack combo state if unclebroadsword.attackState == ATKSTATE.NONE then unclebroadsword.attackState = ATKSTATE.SWIPE1 -- Swipe upward attack_timer = DURATION[ATKSTATE.SWIPE1]*3 Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.swipe)), 0) elseif unclebroadsword.attackState == ATKSTATE.SWIPE1 and attack_timer <= DURATION[ATKSTATE.SWIPE1]*3/2 then queue_state = ATKSTATE.SWIPE2 -- Queue up second swipe if during first swipe elseif unclebroadsword.attackState == ATKSTATE.PAUSE1 then unclebroadsword.attackState = ATKSTATE.SWIPE2 -- Swipe downward attack_timer = DURATION[ATKSTATE.SWIPE2]*3 Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.swipe)), 0) elseif unclebroadsword.attackState == ATKSTATE.SWIPE2 and attack_timer <= DURATION[ATKSTATE.SWIPE2]*3/2 and not ducking() and player.powerup > PLAYER_SMALL then queue_state = ATKSTATE.LUNGE_COMBO -- Queue up combo lunge if during second swipe elseif unclebroadsword.attackState == ATKSTATE.PAUSE2 and not ducking() and player.powerup > PLAYER_SMALL then unclebroadsword.attackState = ATKSTATE.LUNGE_COMBO -- Combo lunge forward attack_timer = DURATION[ATKSTATE.LUNGE_COMBO] + LUNGELAG_COMBO Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.lunge)), 0) end -- Stall-and-fall elseif keycode == KEY_DOWN and player.powerup > PLAYER_SMALL and airborne() and can_stallnfall then unclebroadsword.attackState = ATKSTATE.STALLANDFALL can_stallnfall = false -- Cancel charge when moving elseif (keycode == KEY_LEFT or keycode == KEY_RIGHT) and (unclebroadsword.attackState == ATKSTATE.CHARGING or unclebroadsword.attackState == ATKSTATE.CHARGED) then SetCooldown() -- Falling statue elseif player.powerup == PLAYER_TANOOKIE and keycode == KEY_RUN and not grounded() and not mounted() then unclebroadsword.attackState = ATKSTATE.STATUEFALL can_stallnfall = false -- Double jump elseif (player.powerup == PLAYER_LEAF or player.powerup == PLAYER_TANOOKIE) and (keycode == KEY_JUMP or keycode == KEY_SPINJUMP) and doublejump_ready and player:mem(0x60, FIELD_WORD) == -1 then colliders.bounceResponse(player, nil, false) doublejump_ready = false if keycode == KEY_JUMP then playSFX(1) player:mem(0x50, FIELD_WORD, 0) else playSFX(33) player:mem(0x50, FIELD_WORD, -1) end -- Spawn visual effects for i = 1,6 do local star = Animation.spawn(80, player.x + player.width/2 + player.speedX, player.y + player.height/2 + player.speedY) local angle = rng.random(2)*math.pi star.speedX = math.cos(angle)*rng.random(0.2,1.2) star.speedY = math.sin(angle)*rng.random(0.2,1.2) end end end end function unclebroadsword.onInputUpdate() if player.character == CHARACTER_UNCLEBROADSWORD then pm.winStateCheck() -- Block movement if attacking or hurt if is_hurt or (unclebroadsword.attackState >= ATKSTATE.STALLED and unclebroadsword.attackState <= ATKSTATE.LUNGE_CHARGE and unclebroadsword.attackState ~= ATKSTATE.NONE) or unclebroadsword.attackState == ATKSTATE.STALLANDFALL or player.forcedState > 0 then BlockMovement() else RemoveInputBuffer() end if unclebroadsword.attackState == ATKSTATE.STALLED then player.downKeyPressing = true end -- Prevent fire/ice projectiles from firing if player.powerup > PLAYER_BIG then player:mem(0x160, FIELD_WORD,1) end -- Disable tail swipe/flying if player.powerup == PLAYER_LEAF or player.powerup == PLAYER_TANOOKIE then player:mem(0x164, FIELD_WORD, -2) -- Re-enable ducking if ducking_ext() then player:mem(0x12e, FIELD_WORD, -1) end else player:mem(0x164, FIELD_WORD, 0) end player:mem(0x168, FIELD_FLOAT, 0) player:mem(0x174, FIELD_WORD, 0) -- Force statue form if unclebroadsword.attackState == ATKSTATE.STATUEFALL then player.altRunKeyPressing = true end -- Prevent movement while ducking if player.powerup == PLAYER_SMALL then if ducking_ext() then player.leftKeyPressing = false player.rightKeyPressing = false -- Alter hitbox player.character = CHARACTER_MARIO player:getCurrentPlayerSetting().hitboxHeight = 44 player.character = CHARACTER_UNCLEBROADSWORD else player.character = CHARACTER_MARIO player:getCurrentPlayerSetting().hitboxHeight = 54 player.character = CHARACTER_UNCLEBROADSWORD end end end end ---------------------------------------------------------------------------------------------- PER-FRAME LOGIC --------------------------------- local function CheckHurtState() ------------------------------------------------------------------- Check if player is being hurt and knocked back -- Check if no longer hurt if is_hurt and player:mem(0x140, FIELD_WORD) < 80 then is_hurt = false end -- If powering down and not invincible blinking if player.forcedState == 2 and player:mem(0x140, FIELD_WORD) == 0 then -- If not on a mount and not holding anything if player:mem(0x108, FIELD_WORD) == 0 and player:mem(0x154, FIELD_WORD) <= 0 then is_hurt = true end -- Knockback if is_hurt then -- Stop climbing player:mem(0x40, FIELD_WORD, 0) -- Set knockback if knockback then unclebroadsword.attackState = ATKSTATE.NONE attack_timer = 0 player.speedX = -player:mem(0x106, FIELD_WORD)*3 player.speedY = -7 else knockback = true end end -- Cancel power down player.forcedState = 0 player.powerup = PLAYER_SMALL -- Set invincibility timer player:mem(0x140, FIELD_WORD, 120) end -- Change death animation if player:mem(0x13e, FIELD_WORD) > 0 then local gfx = Animation.get(159)[1] if gfx then gfx.speedX = -2*player:mem(0x106, FIELD_WORD) end if player:mem(0x106, FIELD_WORD) == 1 then Graphics.sprites.effect[159].img = pm.getGraphic(CHARACTER_UNCLEBROADSWORD, deathFX_right) else Graphics.sprites.effect[159].img = pm.getGraphic(CHARACTER_UNCLEBROADSWORD, deathFX_left) end end end local function UpdateAttackState() ---------------------------------------------------------------- Advance attack state timers -- Should the state be changed? if attack_timer <= 0 then -- Pause after swiping if unclebroadsword.attackState == ATKSTATE.SWIPE1 then -- Check if the next attack has been queued if queue_state == ATKSTATE.SWIPE2 then unclebroadsword.attackState = ATKSTATE.SWIPE2 -- Swipe downward attack_timer = DURATION[ATKSTATE.SWIPE2]*3 - 1 Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.swipe)), 0) queue_state = ATKSTATE.NONE else unclebroadsword.attackState = ATKSTATE.PAUSE1 attack_timer = DURATION[ATKSTATE.PAUSE1] end elseif unclebroadsword.attackState == ATKSTATE.SWIPE2 then -- Check if the next attack has been queued if queue_state == ATKSTATE.LUNGE_COMBO then unclebroadsword.attackState = ATKSTATE.LUNGE_COMBO -- Combo lunge forward attack_timer = DURATION[ATKSTATE.LUNGE_COMBO] - 1 + LUNGELAG_COMBO Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.lunge)), 0) queue_state = ATKSTATE.NONE else unclebroadsword.attackState = ATKSTATE.PAUSE2 attack_timer = DURATION[ATKSTATE.PAUSE2] end -- Cooldown after pausing elseif unclebroadsword.attackState == ATKSTATE.PAUSE1 or unclebroadsword.attackState == ATKSTATE.PAUSE2 or unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO or unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE or unclebroadsword.attackState == ATKSTATE.STALLED then unclebroadsword.attackState = ATKSTATE.COOLDOWN -- Skip cooldown if ducking if not ducking() then attack_timer = DURATION[ATKSTATE.COOLDOWN] end -- Prevent further aerials can_aerial = false elseif unclebroadsword.attackState == ATKSTATE.COOLDOWN then unclebroadsword.attackState = ATKSTATE.NONE end -- Check if charging if player.powerup > PLAYER_SMALL and standing() then -- Holding charge button and don't move if player.runKeyPressing then if unclebroadsword.attackState == ATKSTATE.COOLDOWN then unclebroadsword.attackState = ATKSTATE.CHARGING elseif unclebroadsword.attackState == ATKSTATE.CHARGING then charge_timer = charge_timer + 1 if charge_timer >= DURATION[ATKSTATE.CHARGING] then unclebroadsword.attackState = ATKSTATE.CHARGED Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.knife)), 0) elseif charge_timer == DURATION[ATKSTATE.CHARGING] - CHARGING_SOUND_DURATION then charge_sfxobj = Audio.SfxPlayObj(Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.charging)), 0) end end else -- Lunge forward if charge_timer >= DURATION[ATKSTATE.CHARGING] - CHARGING_SOUND_DURATION then unclebroadsword.attackState = ATKSTATE.LUNGE_CHARGE attack_timer = DURATION[ATKSTATE.LUNGE_CHARGE] + LUNGELAG_CHARGE Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.lunge)), 0) charge_fraction = charge_timer/DURATION[ATKSTATE.CHARGING] -- Cancel the charge elseif unclebroadsword.attackState == ATKSTATE.CHARGING then SetCooldown() end -- Stop playing the charging sound if charge_sfxobj and charge_sfxobj:IsPlaying() then charge_sfxobj:Stop() end -- Reset charge timer charge_timer = 0 end end if (unclebroadsword.attackState == ATKSTATE.CHARGING or unclebroadsword.attackState == ATKSTATE.CHARGED) and (not standing() or statued() or player.leftKeyPressing or player.rightKeyPressing) then -- Cancel the charge if not standing on the ground SetCooldown() charge_timer = 0 if charge_sfxobj and charge_sfxobj:IsPlaying() then charge_sfxobj:Stop() end end -- Decrement timer else attack_timer = attack_timer - 1 end -- Check for upward stabbing if airborne() then if unclebroadsword.attackState == ATKSTATE.NONE or unclebroadsword.attackState == ATKSTATE.UPWARDSTAB then if player.upKeyPressing then unclebroadsword.attackState = ATKSTATE.UPWARDSTAB else unclebroadsword.attackState = ATKSTATE.NONE end end end -- Landing on the ground or bouncing off of a spring if grounded() or player:mem(0x11c,FIELD_WORD) == 55 or player:mem(0x11c,FIELD_WORD) == 49 then -- Refresh aerial attack state can_aerial = true can_stallnfall = true doublejump_ready = true if unclebroadsword.attackState == ATKSTATE.UPWARDSTAB then unclebroadsword.attackState = ATKSTATE.NONE elseif unclebroadsword.attackState == ATKSTATE.STALLANDFALL or unclebroadsword.attackState == ATKSTATE.STATUEFALL then -- Kick up some dust local smoke = Animation.spawn(10, player.x - 24, player.y + player.height - 16) smoke.speedX = -1 smoke = Animation.spawn(10, player.x + player.width - 8, player.y + player.height - 16) smoke.speedX = 1 -- Shake the screen a bit Audio.SfxPlayCh(-1, Audio.SfxOpen(pm.getSound(CHARACTER_UNCLEBROADSWORD, sfx.drop)), 0) if unclebroadsword.attackState == ATKSTATE.STATUEFALL then Defines.earthquake = 15 else Defines.earthquake = 3 end -- Stall player after hitting ground unclebroadsword.attackState = ATKSTATE.STALLED attack_timer = DURATION[ATKSTATE.STALLED] end end -- Reset if downstabbing onto a note block if unclebroadsword.attackState == ATKSTATE.STALLANDFALL and player.speedY < 0 then check = colliders.Box(player.x, player.y + player.height, player.width, UDSTAB_HEIGHT) if colliders.collideBlock(check, 55) then unclebroadsword.attackState = ATKSTATE.NONE end end -- Cancel state if necessary if submerged() or sliding() or climbing() or (statued() and mounted())--[[or mounted() or holding() or pickingup() or warping()--]] then unclebroadsword.attackState = ATKSTATE.NONE if(statued() and mounted()) then player:mem(0x4a, FIELD_WORD, 0) end can_aerial = true can_stallnfall = true doublejump_ready = true end end local function AttackPhysics() -------------------------------------------------------------------- Set player speed and sword hitbox -- If in air while attacking, don't fall if player.forcedState ~= 0 then return end if aerial_atk_stall or unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then player.speedY = 0 player.y = player.y - Defines.player_grav end if unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE and player:mem(0x48, FIELD_WORD) ~= 0 then player.y = player.y - 1 end if unclebroadsword.attackState < ATKSTATE.PAUSE2 or unclebroadsword.attackState > ATKSTATE.LUNGE_CHARGE or unclebroadsword.attackState == ATKSTATE.NONE then aerial_atk_stall = false; end -- Boost player forward slightly when swiping in the air if airborne() then if unclebroadsword.attackState == ATKSTATE.SWIPE1 or unclebroadsword.attackState == ATKSTATE.SWIPE2 then --player.speedX = player:mem(0x106, FIELD_WORD)*1 player.speedY = math.min(player.speedY, 1) elseif unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO then aerial_atk_stall = true end end -- Lunging speeds if unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO or unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then -- Maximum time duration local tmax = DURATION[unclebroadsword.attackState] -- Time parameter; Maximum distance traveled local t = 0; local xmax = 0; -- Set parameters if unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO then t = attack_timer - LUNGELAG_COMBO if(attack_timer > tmax) then player.speedY = 0 end xmax = LUNGEDIST_COMBO elseif unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then t = attack_timer - LUNGELAG_CHARGE xmax = LUNGEDIST_CHARGE*charge_fraction*charge_fraction end if t < 0 then t = 0 end if airborne() then can_stallnfall = true end -- Set speed player.speedX = player.direction*2*xmax/tmax/tmax*t end if mounted() or statued() then can_stallnfall = false end -- Lose momentum when swiping if grounded() then if unclebroadsword.attackState == ATKSTATE.SWIPE1 or unclebroadsword.attackState == ATKSTATE.SWIPE2 then if math.abs(player.speedX) < 0.02 then player.speedX = 0 else player.speedX = player.speedX - player:mem(0x106, FIELD_WORD)*0.02 end end end -- Toss boomerang if player.powerup == PLAYER_HAMMER then if (unclebroadsword.attackState == ATKSTATE.SWIPE2 and attack_timer == DURATION[ATKSTATE.SWIPE2]*2) then local n = NPC.spawn(436, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width/2, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height/2, player.section, false, true) n:mem(0x12E, FIELD_WORD, 1) n:mem(0x13A, FIELD_WORD, player.idx) n.direction = player.direction end end -- Stall-and-fall if (unclebroadsword.attackState == ATKSTATE.STALLANDFALL or unclebroadsword.attackState == ATKSTATE.STATUEFALL) and player.speedY > 0 then -- Set physics Defines.gravity = FALLSPEED player.speedY = player.speedY + 1.5 player.speedX = 0 player.downKeyPressing = false if unclebroadsword.attackState == ATKSTATE.STATUEFALL then player:mem(0x4a, FIELD_WORD, -1) end -- Remember positions if #afterimagepos < AFTERIMAGE_COUNT then afterimagepos[#afterimagepos + 1] = {x = player.x, y = player.y, state = unclebroadsword.attackState} else for i = 1, #afterimagepos-1 do afterimagepos[i] = afterimagepos[i+1] end afterimagepos[#afterimagepos] = {x = player.x, y = player.y, state = unclebroadsword.attackState} end else Defines.gravity = nil if #afterimagepos > 0 then table.remove(afterimagepos, 1) end end -- Set sword hitbox if unclebroadsword.attackState > ATKSTATE.NONE then if unclebroadsword.swordCollider == nil then unclebroadsword.swordCollider = colliders.Box(0,0,0,0) end if unclebroadsword.attackState == ATKSTATE.SWIPE1 or unclebroadsword.attackState == ATKSTATE.SWIPE2 then unclebroadsword.swordCollider.x = player.x - SWIPE_WIDTH + ( player:mem(0x106, FIELD_WORD) + 1 )*( player.width + SWIPE_WIDTH )/2 + player.speedX unclebroadsword.swordCollider.y = player.y + player.height/2 - SWIPE_HEIGHT/2 unclebroadsword.swordCollider.width = SWIPE_WIDTH unclebroadsword.swordCollider.height = SWIPE_HEIGHT elseif unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO or unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then unclebroadsword.swordCollider.x = player.x - LUNGE_WIDTH + ( player:mem(0x106, FIELD_WORD) + 1 )*( player.width + LUNGE_WIDTH )/2 + player.speedX unclebroadsword.swordCollider.y = player.y + player.height/2 - LUNGE_HEIGHT/2 + 6 unclebroadsword.swordCollider.width = LUNGE_WIDTH unclebroadsword.swordCollider.height = LUNGE_HEIGHT elseif unclebroadsword.attackState == ATKSTATE.UPWARDSTAB then unclebroadsword.swordCollider.x = player.x + player.width/2 - UDSTAB_WIDTH/2 + player.speedX unclebroadsword.swordCollider.y = player.y - UDSTAB_HEIGHT + player.speedY unclebroadsword.swordCollider.width = UDSTAB_WIDTH unclebroadsword.swordCollider.height = UDSTAB_HEIGHT elseif unclebroadsword.attackState == ATKSTATE.STALLANDFALL then unclebroadsword.swordCollider.x = player.x + player.width/2 - UDSTAB_WIDTH/2 + player.speedX unclebroadsword.swordCollider.y = player.y + player.height + player.speedY unclebroadsword.swordCollider.width = UDSTAB_WIDTH unclebroadsword.swordCollider.height = UDSTAB_HEIGHT elseif unclebroadsword.attackState == ATKSTATE.STATUEFALL then unclebroadsword.swordCollider.x = player.x + player.speedX unclebroadsword.swordCollider.y = player.y + player.height + player.speedY unclebroadsword.swordCollider.width = player.width unclebroadsword.swordCollider.height = player.speedY end else unclebroadsword.swordCollider = nil end end local function PerformSwordBlockCollisions() ------------------------------------------------------ Detect interactions of sword/tanooki statue with blocks -- Block collisions for _,block in pairs(Block.getIntersecting(unclebroadsword.swordCollider.x, unclebroadsword.swordCollider.y, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height)) do -- If block is visible if block:mem(0x1c, FIELD_WORD) == 0 and block:mem(0x5a, FIELD_WORD) == 0 then -- Not bouncing from noteblocks (can get stuck) if isType(block.id, BLOCK_BOUNCY) and block.contentID == 0 then if unclebroadsword.attackState == ATKSTATE.STALLANDFALL then unclebroadsword.attackState = ATKSTATE.NONE end end -- Destroy bricks if isType(block.id, BLOCK_BRICK) and block.contentID == 0 then block:remove(true) if unclebroadsword.attackState == ATKSTATE.STALLANDFALL then colliders.bounceResponse(player) end -- Melting ice blocks with fire sword elseif block.id == 1151 then if player.powerup == PLAYER_ICE then blockutils.spawnNPC(block) Animation.spawn(10, block.x, block.y) playSFX(16) block:remove() end elseif block.id == 620 or block.id == 621 or block.id == 669 then if player.powerup == PLAYER_FIREFLOWER then -- Frozen coin if block.id == 620 then NPC.spawn(10, block.x, block.y, player.section) end -- Frozen muncher if block.id == 621 then Block.spawn(109, block.x, block.y) end -- Ice block container if block.id == 669 then blockutils.spawnNPC(block) end Animation.spawn(10, block.x, block.y) playSFX(16) block:remove() end -- Ignore player blocks elseif not isType(block.id, BLOCK_PLAYERSWITCH) then if unclebroadsword.attackState >= ATKSTATE.STALLANDFALL then block:hit(true, player) -- Bounce if it's a SMW turn block or it had something inside if (block.id == 90 or block.contentID ~= 0) and unclebroadsword.attackState == ATKSTATE.STALLANDFALL then colliders.bounceResponse(player) unclebroadsword.attackState = ATKSTATE.STALLED attack_timer = DURATION[ATKSTATE.STALLED] end else -- When lunging, only hit the block on the first frames if not ((unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO or unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE) and attack_timer < DURATION[unclebroadsword.attackState]) then block:hit() end end end end end end local function PerformSwordNPCCollisions() -------------------------------------------------------- Detect interactions of sword with NPCs -- NPC collisions if player.forcedState ~= 0 then return end for _,npc in ipairs(NPC.getIntersecting(unclebroadsword.swordCollider.x, unclebroadsword.swordCollider.y, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height)) do -- Check if hittable and not a generator if not (npc.invincibleToSword or npc.friendly or npc.isHidden) and npc:mem(0x64, FIELD_WORD) == 0 then -- Prevent infinite bounce on springboards on stall-and-fall if isType(npc.id, NPC_BOUNCY) then if unclebroadsword.attackState == ATKSTATE.STALLANDFALL then unclebroadsword.attackState = ATKSTATE.NONE end end -- Catch powerups if isType(npc.id, NPC_POWERUP) or isType(npc.id, NPC_BONUS) then if unclebroadsword.attackState < ATKSTATE.UPWARDSTAB then npc.x = player.x + player.width/2 - npc.width/2 npc.y = player.y + player.height/2 - npc.height/2 end -- Frozen enemy elseif npc.id == 263 then -- If swiping, only destroy on the first attack frame if (unclebroadsword.attackState == ATKSTATE.SWIPE1 or unclebroadsword.attackState == ATKSTATE.SWIPE2) and attack_timer == 3*DURATION[unclebroadsword.attackState] - 1 then npc:harm(HARM_TYPE_SWORD) elseif unclebroadsword.attackState >= ATKSTATE.LUNGE_COMBO then npc:harm(HARM_TYPE_SWORD) if unclebroadsword.attackState == ATKSTATE.STALLANDFALL then colliders.bounceResponse(player) end end else -- Freeze enemy if you have the ice flower and it can be frozen if player.powerup == PLAYER_ICE and not NPC.config[npc.id].noiceball then npc:harm(HARM_TYPE_EXT_ICE) -- Ignore if a tool, an egg, or Mouser's bomb elseif not (npc.id == 96 or npc.id == 134 or isType(npc.id, NPC_CARRYABLETOOL)) then npc:harm(HARM_TYPE_SWORD) end -- Bounce if a boss character and stall-and-fall if unclebroadsword.attackState >= ATKSTATE.STALLANDFALL and npc:mem(0x148, FIELD_FLOAT) ~= 0 then colliders.bounceResponse(player) if unclebroadsword.attackState == ATKSTATE.STATUEFALL then player.forcedState = 500 end SetCooldown() end end -- Stall if swiping in the air if airborne() and not inforcedanim() and (unclebroadsword.attackState == ATKSTATE.SWIPE1 or unclebroadsword.attackState == ATKSTATE.SWIPE2) then aerial_atk_stall = true; end end end end local function SpawnAttackGFX() ------------------------------------------------------------------- Display visual effects for fire/ice sword -- Generate smoke puffs if (unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE or unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO) and grounded() then local x = math.random(player.x + player.width/2, player.x + player.width/2*(1 - player:mem(0x106, FIELD_WORD))) local y = player.y + player.height + rng.random(-2,2) - 4 Animation.spawn(74, x, y) end -- Show effect for fire/ice powerups if unclebroadsword.swordCollider and ((unclebroadsword.attackState >= ATKSTATE.SWIPE1 and unclebroadsword.attackState <= ATKSTATE.LUNGE_CHARGE) or unclebroadsword.attackState == ATKSTATE.STALLANDFALL) then if player.powerup == PLAYER_FIREFLOWER then local fire = Animation.spawn( 12, rng.random(unclebroadsword.swordCollider.x, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width) - 14, rng.random(unclebroadsword.swordCollider.y, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height) - 44 ) fire.speedX = 0; fire.speedY = 0 elseif player.powerup == PLAYER_ICE then if not (unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE or unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO) or attack_timer%3 == 0 then local star = Animation.spawn( 80, rng.random(unclebroadsword.swordCollider.x, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width) - 8, rng.random(unclebroadsword.swordCollider.y, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height) - 8 ) star.speedX = 0; star.speedY = 0 end end end end local function PrintAttackState(x, y) ------------------------------------------------------------- Print name of current attack state and charge meter reading local msg = "" if unclebroadsword.attackState == ATKSTATE.COOLDOWN then msg = "cooldown" elseif unclebroadsword.attackState == ATKSTATE.NONE then msg = "none" elseif unclebroadsword.attackState == ATKSTATE.SWIPE1 then msg = "swipe1" elseif unclebroadsword.attackState == ATKSTATE.PAUSE1 then msg = "pause1" elseif unclebroadsword.attackState == ATKSTATE.SWIPE2 then msg = "swipe2" elseif unclebroadsword.attackState == ATKSTATE.PAUSE2 then msg = "pause2" elseif unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO then msg = "lunge combo" elseif unclebroadsword.attackState == ATKSTATE.CHARGING then msg = "charging" elseif unclebroadsword.attackState == ATKSTATE.CHARGED then msg = "charged" elseif unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then msg = "lunge charge" elseif unclebroadsword.attackState == ATKSTATE.UPWARDSTAB then msg = "upward stab" elseif unclebroadsword.attackState == ATKSTATE.STALLANDFALL then msg = "stall-and-fall" elseif unclebroadsword.attackState == ATKSTATE.STATUEFALL then msg = "falling statue" elseif unclebroadsword.attackState == ATKSTATE.STALLED then msg = "stalled" end Text.print(msg, x, y) Text.print("Charge: "..tostring(charge_timer), x, y + 20) Text.print("Aerial: "..tostring(can_aerial), x, y + 40) end function unclebroadsword.onTick() if player.character == CHARACTER_UNCLEBROADSWORD and player:mem(0x13E,FIELD_WORD) == 0 then -- Check for hurt state CheckHurtState() -- Update current attacking state UpdateAttackState() -- Change player speed when attacking AttackPhysics() -- Perform hit detection for sword if unclebroadsword.swordCollider then PerformSwordBlockCollisions() PerformSwordNPCCollisions() end -- Draw visual effects SpawnAttackGFX() if unclebroadsword.debugMode then PrintAttackState(20, 500) end end end ---------------------------------------------------------------------------------------------- RENDERING --------------------------------------- local function SetFrame(frame) -------------------------------------------------------------------- Set player frame player:mem(0x114, FIELD_WORD, frame) end local function ShowSmallFrames() ------------------------------------------------------------------ PLAYER_SMALL frame management if ducking_ext()and not mounted() then SetFrame(7) -- Ducking elseif sliding() then SetFrame(24) -- Sliding down slope elseif player:mem(0x58, FIELD_WORD) ~= 0 then -- Changing direction SetFrame(6) if holding() then SetFrame(10) end elseif holding() then -- Holding an item if grounded() then if player.speedX == 0 then SetFrame(8) holding_frame_timer = 1 holding_frame_index = 1 else if math.abs(player.speedX) < Defines.player_walkspeed then if holding_frame_timer > 5 then holding_frame_timer = 1 holding_frame_index = holding_frame_index + 1 end else if holding_frames[holding_frame_index] == 9 then if holding_frame_timer > 2 then holding_frame_timer = 1 holding_frame_index = holding_frame_index + 1 end else if holding_frame_timer > 3 then holding_frame_timer = 1 holding_frame_index = holding_frame_index + 1 end end end if holding_frame_index > 4 then holding_frame_index = 1 end SetFrame(holding_frames[holding_frame_index]) holding_frame_timer = holding_frame_timer + 1 end else SetFrame(10) end else if grounded() then -- Walking/running if player.speedX == 0 then walking_frame_timer = 1 walking_frame_index = 1 running_duration_index = 1 else if math.abs(player.speedX) < Defines.player_walkspeed then if walking_frame_timer > 5 then walking_frame_timer = 1 walking_frame_index = walking_frame_index + 1 end elseif math.abs(player.speedX) < Defines.player_walkspeed + 1.5 then if walking_frames[walking_frame_index] == 2 then if walking_frame_timer > 2 then walking_frame_timer = 1 walking_frame_index = walking_frame_index + 1 end else if walking_frame_timer > 3 then walking_frame_timer = 1 walking_frame_index = walking_frame_index + 1 end end else if walking_frame_timer > running_durations[running_duration_index] then walking_frame_timer = 1 walking_frame_index = walking_frame_index + 1 running_duration_index = running_duration_index + 1 end if running_duration_index > 3 then running_duration_index = 1 end end if walking_frame_index > 4 then walking_frame_index = 1 end SetFrame(walking_frames[walking_frame_index]) walking_frame_timer = walking_frame_timer + 1 end end end end local function ShowAttackFrames() ----------------------------------------------------------------- Render frames when attacking -- Sword swipe/lunge/stab frames frames if unclebroadsword.attackState == ATKSTATE.SWIPE1 then local offset = math.floor(attack_timer / DURATION[ATKSTATE.SWIPE1]) if ducking() then SetFrame(29 - offset) elseif grounded() then SetFrame(20 - offset) else SetFrame(35 - offset) end elseif unclebroadsword.attackState == ATKSTATE.PAUSE1 then if ducking() then SetFrame(7) elseif grounded() then SetFrame(21) else SetFrame(36) end elseif unclebroadsword.attackState == ATKSTATE.SWIPE2 then local offset = math.floor(attack_timer / DURATION[ATKSTATE.SWIPE2]) if ducking() then SetFrame(27 + offset) elseif grounded() then SetFrame(19 + offset) else SetFrame(33 + offset) end elseif unclebroadsword.attackState == ATKSTATE.PAUSE2 then if ducking() then SetFrame(7) elseif grounded() then SetFrame(17) else SetFrame(32) end elseif unclebroadsword.attackState == ATKSTATE.LUNGE_COMBO then SetFrame(39) elseif unclebroadsword.attackState == ATKSTATE.CHARGING then SetFrame(45) elseif unclebroadsword.attackState == ATKSTATE.CHARGED then if math.floor(os.clock()*12)%2 == 0 then SetFrame(46) else SetFrame(45) end elseif unclebroadsword.attackState == ATKSTATE.LUNGE_CHARGE then local max_speed = 2*LUNGEDIST_CHARGE/DURATION[ATKSTATE.LUNGE_CHARGE] if math.abs(player.speedX) < max_speed/6 then SetFrame(39) elseif math.abs(player.speedX) < max_speed/3 then SetFrame(38) else SetFrame(37) end elseif unclebroadsword.attackState == ATKSTATE.STALLANDFALL then SetFrame(47) elseif unclebroadsword.attackState == ATKSTATE.UPWARDSTAB then if spinjumping() then if player:mem(0x114, FIELD_WORD) == 13 then SetFrame(14) elseif player:mem(0x114, FIELD_WORD) == 15 then SetFrame(16) else SetFrame(48) end else SetFrame(48) end end end function unclebroadsword.onDraw() if player.character == CHARACTER_UNCLEBROADSWORD then -- Render afterimages for stall-and-fall if(player:mem(0x13E,FIELD_WORD) == 0) then for i,pos in ipairs(afterimagepos) do if pos.state == ATKSTATE.STALLANDFALL then Graphics.draw {x = pos.x - 12, y = pos.y - 20, type = RTYPE_IMAGE, image = pm.getGraphic(CHARACTER_UNCLEBROADSWORD, afterimage), isSceneCoordinates = true, opacity = 0.7*i/(#afterimagepos + 1), priority = -26, sourceX = ( player:mem(0x106, FIELD_WORD) + 1 )/2*100, sourceY = player.powerup*100 - 100, sourceWidth = 100, sourceHeight = 100} elseif pos.state == ATKSTATE.STATUEFALL then Graphics.draw {x = pos.x - 8, y = pos.y - 26, type = RTYPE_IMAGE, image = pm.getGraphic(CHARACTER_UNCLEBROADSWORD, statue_img), isSceneCoordinates = true, opacity = 0.7*i/(#afterimagepos + 1), priority = -26} end end end if(player:mem(0x13E,FIELD_WORD) == 0) then if player.powerup == PLAYER_SMALL then ShowSmallFrames() end if unclebroadsword.attackState ~= ATKSTATE.NONE and unclebroadsword.attackState ~= ATKSTATE.COOLDOWN then ShowAttackFrames() elseif is_hurt then SetFrame(49) end end end end ---------------------------------------------------------------------------------------------- CHARACTER MANAGEMENT ---------------------------- function unclebroadsword.onInitAPI() registerEvent(unclebroadsword, "onKeyDown", "onKeyDown") registerEvent(unclebroadsword, "onInputUpdate", "onInputUpdate") registerEvent(unclebroadsword, "onNPCKill", "onNPCKill") registerEvent(unclebroadsword, "onTick", "onTick") registerEvent(unclebroadsword, "onDraw", "onDraw") if unclebroadsword.debugMode then registerEvent(unclebroadsword, "onDraw", "DebugDraw") end end function unclebroadsword.initCharacter() -- Prevent from dropping rupees Defines.kill_drop_link_rupeeID1 = 0 Defines.kill_drop_link_rupeeID2 = 0 Defines.kill_drop_link_rupeeID3 = 0 -- Slightly nerf speed Defines.player_runspeed = 5 end function unclebroadsword.cleanupCharacter() -- Reset death effect Graphics.sprites.effect[3].img = nil -- Reset maximum player fall/run speed Defines.gravity = nil Defines.player_runspeed = nil -- Reset rupee drop Defines.kill_drop_link_rupeeID1 = nil Defines.kill_drop_link_rupeeID2 = nil Defines.kill_drop_link_rupeeID3 = nil end ---------------------------------------------------------------------------------------------- DEBUG FUNCTIONS --------------------------------- function DrawRect(x1, y1, x2, y2, clr) ------------------------------------------------------------ OpenGL box drawing Graphics.glDraw {vertexCoords = {x1,y1,x2,y1,x2,y1,x2,y2,x2,y2,x1,y2,x1,y2,x1,y1}, sceneCoords=true, color = clr, primitive = Graphics.GL_LINES} end function unclebroadsword.DebugDraw() -------------------------------------------------------------- Render hitboxes if player.character == CHARACTER_UNCLEBROADSWORD then local playerbox = colliders.getHitbox(player) DrawRect(playerbox.x, playerbox.y, playerbox.x + playerbox.width, playerbox.y + playerbox.height, {0,1,0}) if unclebroadsword.swordCollider ~= nil then DrawRect(unclebroadsword.swordCollider.x, unclebroadsword.swordCollider.y, unclebroadsword.swordCollider.x + unclebroadsword.swordCollider.width, unclebroadsword.swordCollider.y + unclebroadsword.swordCollider.height, {1,0,0}) end end end function unclebroadsword.stopGroundPound() if unclebroadsword.attackState == ATKSTATE.STALLANDFALL then unclebroadsword.attackState = ATKSTATE.NONE end end return unclebroadsword