Downloads containing KeenJazz.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: Commander Keen Lark Mutator N/A Download file

File preview

#pragma name "Commander Keen"
#pragma description "Replaces Jazz and Spaz with Commander Keen while preserving their gameplay."
// Source sheet: Commander Keen Sprite Sheet by Yorpy, supplied by the user.

#pragma require "KeenSprites.png"

namespace KeenJazz {
    const string SPRITE_FILE = "KeenSprites.png";
    const float SIZE_MULTIPLIER = 1.0f;
    const uint CELL_SIZE = 128;
    const uint ROW_COUNT = 26;
    const float FOOT_Y = 48.0f; // atlas baseline 112 minus cell center 64
    const uint SPIN_ANGLE_STEP = 64; // one full turn every 16 ticks
    const uint RUN_FRAME_TICKS = 5; // hold each running pose briefly

    enum Pose {
        STAND, RUN, CROUCH, SHOOT, SHOOT_FLASH, AIM_UP, JUMP, FALL,
        HURT, DIE, CORPSE, SKID, IDLE_SHRUG, IDLE_BOOK, DIZZY,
        POLE, HOVER, AIRBOARD, HANG_RIGHT, HANG_LEFT, SWIM_RIGHT,
        SWIM_LEFT, SPIN, POGO, BLASTER, BLASTER_FADE
    }

    uint firstAnim = 0;
    int customSlot = -1;
    float sizeScale = 1.0f;
    float jazzFoot = 0.0f;
    float spazFoot = 0.0f;
    bool attemptedLoad = false;

    void load() {
        if (attemptedLoad) return;
        attemptedLoad = true;

        jjCOLORMAP sheet(SPRITE_FILE);
        if (sheet.width != CELL_SIZE * 12 || sheet.height != CELL_SIZE * ROW_COUNT) {
            jjPrint("Commander Keen: missing or incorrect KeenSprites.png. Copy it beside Jazz2.exe.");
            return;
        }

        int slot = -1;
        for (int i = 0; i < 256; ++i) {
            if (jjAnimSets[ANIM::CUSTOM[i]].firstAnim == 0) {
                slot = i;
                break;
            }
        }
        if (slot < 0) {
            jjPrint("Commander Keen: an unused custom animation slot is required.");
            return;
        }

        if (jjAnimSets[ANIM::JAZZ].firstAnim == 0)
            jjAnimSets[ANIM::JAZZ].load();
        if (jjAnimSets[ANIM::SPAZ].firstAnim == 0)
            jjAnimSets[ANIM::SPAZ].load();

        jjAnimSets[ANIM::CUSTOM[slot]].load(sheet, CELL_SIZE, CELL_SIZE);
        firstAnim = jjAnimSets[ANIM::CUSTOM[slot]].firstAnim;
        if (firstAnim == 0) return;
        customSlot = slot;

        const uint jazzStand = jjAnimations[
            jjAnimSets[ANIM::JAZZ].firstAnim + RABBIT::STAND
        ].firstFrame;
        const uint spazStand = jjAnimations[
            jjAnimSets[ANIM::SPAZ].firstAnim + RABBIT::STAND
        ].firstFrame;
        const uint keenStand = jjAnimations[firstAnim + STAND].firstFrame;
        sizeScale = float(jjAnimFrames[jazzStand].height)
                  / float(jjAnimFrames[keenStand].height);
        jazzFoot = float(int(jjAnimFrames[jazzStand].hotSpotY)
                       - int(jjAnimFrames[jazzStand].coldSpotY));
        spazFoot = float(int(jjAnimFrames[spazStand].hotSpotY)
                       - int(jjAnimFrames[spazStand].coldSpotY));

        for (uint animationID = 0; animationID < ROW_COUNT; ++animationID) {
            jjANIMATION@ animation = jjAnimations[firstAnim + animationID];
            for (uint frameID = 0; frameID < animation.frameCount; ++frameID) {
                jjANIMFRAME@ frame = jjAnimFrames[animation.firstFrame + frameID];
                frame.coldSpotY = int16(int(frame.hotSpotY) - int(FOOT_Y));
            }
        }
    }

    void reload() {
        if (customSlot >= 0 && firstAnim != 0
            && jjAnimSets[ANIM::CUSTOM[customSlot]].firstAnim == firstAnim) return;
        firstAnim = 0;
        customSlot = -1;
        attemptedLoad = false;
        load();
    }

    class BlasterVisual : jjBEHAVIORINTERFACE {
        void onBehave(jjOBJ@ obj) {
            obj.behave(BEHAVIOR::BULLET, false);
            if (!obj.isActive) return;

            if (obj.state == STATE::EXPLODE) {
                obj.draw();
                return;
            }

            const jjANIMATION@ animation = jjAnimations[firstAnim + BLASTER];
            const uint frame = (uint(jjGameTicks) / 4) % animation.frameCount;
            const uint originalFrame = obj.curFrame;
            obj.curFrame = animation.firstFrame + frame;
            obj.draw();
            obj.curFrame = originalFrame;
        }
    }

    void setupBlaster() {
        if (firstAnim == 0) return;
        jjObjectPresets[OBJECT::BLASTERBULLET].killAnim = firstAnim + BLASTER_FADE;
        jjObjectPresets[OBJECT::BLASTERBULLETPU].killAnim = firstAnim + BLASTER_FADE;
        jjObjectPresets[OBJECT::BLASTERBULLET].behavior = BlasterVisual();
        jjObjectPresets[OBJECT::BLASTERBULLETPU].behavior = BlasterVisual();
    }

    uint choose(int rabbitAnim) {
        switch (rabbitAnim) {
            case RABBIT::STAND:
                return STAND;

            case RABBIT::PUSH:
            case RABBIT::REV1:
            case RABBIT::REV2:
            case RABBIT::REV3:
            case RABBIT::RUN1:
            case RABBIT::RUN2:
            case RABBIT::RUN3:
                return RUN;

            case RABBIT::SKID1:
            case RABBIT::SKID2:
            case RABBIT::SKID3:
                return SKID;

            case RABBIT::DIVE:
            case RABBIT::DIVEFIREQUIT:
            case RABBIT::DIVEFIRERIGHT:
            case RABBIT::DIVEUP:
            case RABBIT::BUTTSTOMPLAND:
            case RABBIT::FALLLAND:
            case RABBIT::LIFTLAND:
                return CROUCH;

            case RABBIT::FIRE:
            case RABBIT::QUIT:
            case RABBIT::JUMPFIRERIGHT:
                return SHOOT;

            case RABBIT::FIREUP:
            case RABBIT::FIREUPQUIT:
            case RABBIT::JUMPFIREQUIT:
            case RABBIT::LOOKUP:
            case RABBIT::LIFT:
                return AIM_UP;

            case RABBIT::JUMPING1:
            case RABBIT::JUMPING2:
            case RABBIT::JUMPING3:
            case RABBIT::RIGHTJUMP:
            case RABBIT::RIGHTFALL:
            case RABBIT::ROLLING:
            case RABBIT::LIFTJUMP:
                return JUMP;

            case RABBIT::STATIONARYJUMPSTART:
            case RABBIT::STATIONARYJUMP:
            case RABBIT::STATIONARYJUMPEND:
                return POGO;

            case RABBIT::FALLBUTTSTOMP:
                return POGO;

            case RABBIT::SPRING:
                return POGO;

            case RABBIT::FALL:
                return FALL;

            case RABBIT::AIRBOARD:
            case RABBIT::AIRBOARDTURN:
                return AIRBOARD;

            case RABBIT::EARBRACHIATE:
            case RABBIT::HANGFIREQUIT:
            case RABBIT::HANGFIREREST:
            case RABBIT::HANGFIREUP:
            case RABBIT::HANGIDLE1:
            case RABBIT::HANGIDLE2:
            case RABBIT::HANGINGFIREQUIT:
            case RABBIT::HANGINGFIRERIGHT:
            case RABBIT::SWINGINGVINE:
                return HANG_RIGHT;

            case RABBIT::SWIMDOWN:
            case RABBIT::SWIMRIGHT:
            case RABBIT::SWIMTURN1:
            case RABBIT::SWIMTURN2:
            case RABBIT::SWIMUP:
                return SWIM_RIGHT;

            case RABBIT::HPOLE:
            case RABBIT::VPOLE:
                return POLE;

            case RABBIT::HURT:
                return HURT;
            case RABBIT::DIE:
                return DIE;
            case RABBIT::CORPSE:
                return CORPSE;

            case RABBIT::LOOPY:
            case RABBIT::STONED:
                return DIZZY;

            case RABBIT::HELICOPTER:
            case RABBIT::HELICOPTERFIREQUIT:
            case RABBIT::HELICOPTERFIRERIGHT:
            case RABBIT::FROG:
                return HOVER;

            case RABBIT::TELEPORT:
            case RABBIT::TELEPORTFALL:
            case RABBIT::TELEPORTFALLING:
            case RABBIT::TELEPORTFALLTELEPORT:
            case RABBIT::TELEPORTSTAND:
                return IDLE_BOOK;

            case RABBIT::ENDOFLEVEL:
            case RABBIT::IDLE1:
            case RABBIT::IDLE3:
                return IDLE_SHRUG;

            case RABBIT::LEDGEWIGGLE:
                return STAND;

            case RABBIT::IDLE2:
            case RABBIT::IDLE4:
            case RABBIT::IDLE5:
                return IDLE_BOOK;
        }
        return STAND;
    }

    void draw(jjPLAYERDRAW &inout drawing) {
        jjPLAYER@ player = drawing.player;
        if (player is null) return;
        if (player.charCurr != CHAR::JAZZ && player.charCurr != CHAR::SPAZ) return;
        load();
        if (firstAnim == 0) return;

        const uint rabbitBase = jjAnimSets[player.setID].firstAnim;
        if (rabbitBase == 0 || player.curAnim < rabbitBase) return;
        const int rabbitAnim = int(player.curAnim) - int(rabbitBase);
        uint pose = choose(rabbitAnim);

        if (pose == SHOOT && drawing.gunFlash)
            pose = SHOOT_FLASH;

        bool faceLeft = player.direction < 0;
        if (player.xSpeed < -0.25f)
            faceLeft = true;
        else if (player.xSpeed > 0.25f)
            faceLeft = false;
        if (pose == HANG_RIGHT) {
            if (player.xSpeed < 0.0f)
                faceLeft = true;
            else if (player.xSpeed > 0.0f)
                faceLeft = false;
            if (faceLeft)
                pose = HANG_LEFT;
        }
        if (pose == SWIM_RIGHT) {
            if (player.xSpeed < 0.0f)
                faceLeft = true;
            else if (player.xSpeed > 0.0f)
                faceLeft = false;
            if (faceLeft)
                pose = SWIM_LEFT;
        }
        jjANIMATION@ target = jjAnimations[firstAnim + pose];
        if (target.frameCount == 0) return;
        const uint sourceCount = jjAnimations[player.curAnim].frameCount;
        uint sourceFrame = player.frameID;
        uint frame = 0;
        if (sourceCount > 1) {
            if (sourceFrame >= sourceCount) sourceFrame = sourceCount - 1;
            if (pose == DIE || pose == SKID)
                frame = sourceFrame * (target.frameCount - 1) / (sourceCount - 1);
            else
                frame = sourceFrame * target.frameCount / sourceCount;
        }
        if (pose == RUN)
            frame = (uint(jjGameTicks) / RUN_FRAME_TICKS) % target.frameCount;

        const float scale = sizeScale * SIZE_MULTIPLIER;
        const float stableFoot = player.charCurr == CHAR::SPAZ ? spazFoot : jazzFoot;
        const bool spinning = pose == SPIN;
        if (spinning)
            drawing.angle = int((uint(jjGameTicks) * SPIN_ANGLE_STEP) & 1023);
        if (!spinning && drawing.angle == 0)
            drawing.yOffset += (stableFoot - FOOT_Y * scale) * drawing.yScale;

        drawing.curFrame = target.firstFrame + frame;
        float xMagnitude = drawing.xScale;
        if (xMagnitude < 0) xMagnitude = -xMagnitude;
        drawing.xScale = (faceLeft ? -xMagnitude : xMagnitude) * scale;
        drawing.yScale *= scale;
        if (drawing.spriteMode == SPRITE::PLAYER) {
            drawing.spriteMode = SPRITE::NORMAL;
            drawing.spriteParam = 0;
        } else if (drawing.spriteMode == SPRITE::TRANSLUCENTPLAYER) {
            drawing.spriteMode = SPRITE::TRANSLUCENT;
            drawing.spriteParam = 0;
        }
        drawing.gunFlash = false;
        drawing.trail = false;
    }
}

void onLevelLoad() {
    KeenJazz::load();
    KeenJazz::setupBlaster();
}

void onLevelReload() {
    KeenJazz::reload();
    KeenJazz::setupBlaster();
}

void onPlayerDraw(jjPLAYERDRAW &inout drawing) {
    KeenJazz::draw(drawing);
}