In Progress Double Dragon Mini

The project is currently under development.

MKLIUKANG1

Active member
Double Dragon Mini
Engine:
OpenBOR v.4
Developers: BVD Game
Realease Date: ???
👊 Hey everyone! I want to share the first development updates on my new project built on the OpenBOR v.4 engine. The current working title is Double Dragon Mini (subject to change in the future).
The visuals are heavily inspired by and based on sprites from the cult classic Battletoads & Double Dragon (SNES). In this video, I'm showcasing the starting stage, Rumble City, along with some core gameplay mechanics.
🕹️ Features implemented so far:
  • Billy Lee's Combat System: Basic movement, running, jumping, a powerful running attack, and classic punch combos.
  • Unique Finishers:
    • For the powerhouse Guido — a devastating spinning back kick / spin kick!
    • For the dangerous Linda — a brutal hair-grab attack in the style of old-school entries.
  • Interactive Environment (Stage Gimmick): There are chandeliers hanging from the ceiling in Rumble City. Billy can jump and hang on them, attack while hanging, and if he lands a heavy hit, the chandelier breaks off and crashes to the ground, forcing the player to quickly perform a tactical leap/evasion!
🔥 The project is in active development, and there is still a lot of work to be done on balance, new enemies, movesets, and stages.
👇 Let me know what you think about the chandelier mechanics and finishing moves in the comments below! Any feedback is highly appreciated.


 
Last edited:
DevLog: The Magic of Primitives and the Return of Jimmy
In this update, I’ve focused on the visual effects and expanding the character roster.
  • Jimmy Lee is Ready for Battle: Animations and controls for the second player are fully configured. You can now play through Rumble City in local co-op mode.
  • Lighting via drawbox: To keep the engine light and avoid heavy alpha-channel sprites, I implemented a custom lighting system using engine primitives.
  • Volumetric Light Cone: The light cone is rendered in a loop that draws dozens of horizontal drawbox strips, dynamically adjusting their width. This creates a volumetric "god ray" effect that expands from the lamp to the floor.
  • Retro Charm: I’ve added flicker logic using the rand() function, giving the lights a lively, unstable retro feel.
  • Chandelier Grab Polish: Fixed collision bugs. Now, each player can hang on their own chandelier independently. Additionally, the script instantly stops rendering the light cone the moment a chandelier is broken or destroyed.
 
This type of interaction with the environment is always appreciated.
Since the ground is 2.5D, it would be good if the same was true for the light on the ground which can be deposited in an oval or rounded shape.

Congratulations on your research in programming ;)
 
[Update] Double Dragon Mini: Synchronized Lighting & Dynamic Boss Finishers
Hey everyone! Today I focused on polishing the visual effects, stage atmosphere, and making boss battles much more cinematic. Here is what has been fully implemented in the latest build under OpenBOR v4.0 (Build 7530) constraints:

💡 Synchronized Chandelier Lighting via drawbox
I completely re-engineered the chandelier lighting by combining the volumetric vertical light cone and the floor shadow into a single unified render loop.
  • Flawless Synchronization: Both elements now share the same real-time calculation and flicker logic (rand()), creating a perfectly synchronized, pulsing "live" light effect that adds great retro atmosphere to the stage.
  • Full Interactivity: The script now intercepts ANI_FALL or ANI_DIE on the chandelier, instantly killing the light render so it turns off dynamically the moment the object is broken.

👊 Dynamic Boss Hit Reactions (onpainscript)
Implemented a custom event handler for the Guido Boss via onpainscript.
  • The Logic: Since native properties can be tricky in v4.0, the script safely utilizes openborvariant("lasthit_attacker") to intercept every successful strike from Billy or Jimmy.
  • Combo Variety: Now, instead of standard repetitive hit-stuns, whenever the boss takes damage, the players will randomly execute one of their core finishers: an uppercut, a flying jump kick, a spin kick, or a powerful elbow drop! This makes boss fights way more engaging and unpredictable.

🧠 AI Scripting Experiments (On Hold)
I also spent quite a bit of time trying to write a custom AI script for smarter enemy positioning and cooperative flanking. However, the pathfinding logic started conflicting heavily, turning the code into a messy "spaghetti" pile. I have decided to put the AI overhaul on hold for now to clear my head and approach it later with a fresh perspective.


 
This type of interaction with the environment is always appreciated.
Since the ground is 2.5D, it would be good if the same was true for the light on the ground which can be deposited in an oval or rounded shape.

Congratulations on your research in programming ;)
"Thanks, mate! I completely agree with you on the 2.5D physics. I actually just fixed that — the floor light is now a fully rounded oval and reacts perfectly to the stage depth. Thanks for the tip and for following the project!"
 
Double Dragon Mini:
- Custom Menu
Hey everyone! Here is a fresh update on how the main heroes interact with enemies. This time, I’ve upgraded the combat arsenal of the Lee brothers (Billy and Jimmy) in their fights against Guido.
Changelog & New Features:
  1. Custom Menu: Refreshed the visual style of the game's user interface.
  2. Billy & Jimmy Contextual Moves: The brothers now react to Guido's health level.
    • Guido has high HP: The player grabs him to continue the combo.
    • Guido has low HP: Triggers a flashy and quick finishing kick.
  3. New Enemy: Started integrating the Shadow Boss. At this stage, he can move around and perform a few basic attacks.
 
Double Dragon Mini [OpenBOR] - Custom Scripted Main Menu, Music Player & Memory Optimization
Hello Chronocrash community! I wanted to share a major update regarding my project's UI. I decided to step away from the default OpenBOR main menu and programmed a completely custom scripted interface tailored for 320x240 resolution.
Key Features & Implementation:
  • Native Feature Integration: The default engine menu is entirely bypassed. "Start Game" safely clears script variables and seamlessly triggers character selection (jumptobranch). The native "Options" menu has been fully integrated into the custom UI layout.
  • Cheat Code Input: Added an input buffer checker. Executing the classic cheat code on the main screen dynamically unlocks hidden sub-menus in real-time.
  • Soundtrack Player Menu: A custom-scripted jukebox to stream in-game music. Features a 3-track looping layout (displays previous, current, and next tracks), flashing navigation arrows, and a vertical "blind-fold".
  • BVD_TEAM Gallery (Credits Screen): An interactive developer credits page. Each team member has a dedicated profile box. The UI border and neon glow colors change dynamically (via rgbcolor shifting) based on the active page index.
Engine Optimization & Bug Fixes:
  • Memory Leak Fixed (Sprite Caching): Initially, using loadsprite and free within the logic loop caused severe memory leaks, resulting in missing sprites and black screens after re-entering the menu. I resolved this by implementing an automated global variable caching system (getglobalvar). Sprites are now loaded into RAM only once, ensuring perfect stability.
  • Strict Z-Index Hierarchy: Layered all drawing functions properly, moving the background to layer 1, profile sprites to layer 650, and text rendering to layers 660–670 to prevent any overlap clipping.
  • BGM Execution Fix: Resolved an issue where state_ticks was resetting prematurely in the main loop, allowing music tracks to safely trigger exactly once upon menu initialization.
Any feedback on the visual style would be highly appreciated! :)
 
Looking good!
I have interest in the sound player code
Thanks! Here is the reworked Soundtrack Menu for custom stages.
I hope someone can create a proper guide for this on the forum. I’m not really good at writing tutorials or translating all of this into English myself, and automated translators often mess up technical terms. :) I tried my best to layout everything step-by-step. It should work perfectly.
STEP 1: Setting up the global update script
  1. Go to your data/scripts/ folder. If it doesn't exist, create it.
  2. Inside that folder, create a new text file and name it updated.c.
  3. Paste the following code into it:

C:
// =========================================================================
// CUSTOM SOUNDTRACK PLAYER SCRIPT
// Developed by: MKLIUKANG1
// Optimized for OpenBOR Engine (Resolution: 320x240)
// =========================================================================

#define key_up      openborconstant("FLAG_MOVEUP")
#define key_down    openborconstant("FLAG_MOVEDOWN")
#define key_left    openborconstant("FLAG_MOVELEFT")
#define key_right   openborconstant("FLAG_MOVERIGHT")
#define key_attack  openborconstant("FLAG_ATTACK")
#define key_start   openborconstant("FLAG_START")

void main()
{
    // Check if the game is currently on a custom stage screen
    if (openborvariant("in_level"))
    {
        // Get button inputs for Player 1
        int player_index = 0;
        int newkeys = getplayerproperty(player_index, "newkeys");

        // Load menu global variables from OpenBOR memory
        int state = getglobalvar("menu_state");
        int track = getglobalvar("menu_soundtrack_track");
        int state_ticks = getglobalvar("menu_state_ticks");
        int selection = getglobalvar("menu_selection");
        int m_type = getglobalvar("menu_music_type");

        // Base initializations on first launch
        if(state == NULL()) { state = 2; setglobalvar("menu_state", 2); } // Turn on the player directly (state 2)
        if(track == NULL()) track = 0;
        if(state_ticks == NULL()) state_ticks = 0;
        if(m_type == NULL()) m_type = 0;

        // Interface alignment positions & dimensions
        int cx = 160;
        int sy = 100;
        int screen_w = 320;
        int screen_h = 240;
        int flash_color = rgbcolor(0, 200, 100); // Green neon glow palette
        int current_layer = 0;
        int i, y;

        // ---------- SOUNDTRACK ENGINE (state == 2) ----------
        if (state == 2)
        {
            state_ticks++;

            // Wait a few frames before registering inputs to prevent accidental button double-triggers
            if (state_ticks > 2)
            {
                // Navigation controls through the 8 tracks (Indices 0 to 7)
                if (newkeys & key_up)   { track--; if (track < 0) track = 7; }
                if (newkeys & key_down) { track++; if (track > 7) track = 0; }

                // Audio mode toggle: Original vs Arranged サウンドトラック
                if (newkeys & openborconstant("FLAG_SPECIAL"))
                {
                    if (m_type == 0) m_type = 1; // Switch to Arranged folder
                    else             m_type = 0; // Revert to Original folder
                }

                // Play the selected track upon attack button press
                if (newkeys & key_attack)
                {
                    // Dynamically choose folder prefix path based on selected mode
                    void path_prefix = (m_type == 1) ? "data/music/Arr/" : "data/music/";

                    if (track == 0)      playmusic(path_prefix + "Track_1.ogg", 1);
                    else if (track == 1) playmusic(path_prefix + "Track_2.ogg", 345892);
                    else if (track == 2) playmusic(path_prefix + "Track_3.ogg", 1);
                    else if (track == 3) playmusic(path_prefix + "Track_4.ogg", 1);
                    else if (track == 4) playmusic(path_prefix + "Track_5.ogg", 1);
                    else if (track == 5) playmusic(path_prefix + "Track_6.ogg", 1);
                    else if (track == 6) playmusic(path_prefix + "Track_7.ogg", 1);
                    else if (track == 7) playmusic(path_prefix + "Track_8.ogg", 1);
                }

                // Return or menu button handler
                if (newkeys & key_start)
                {
                    state = 2;
                }
            }

            // =========================================================================
            // UI RENDERING - BACKGROUND & BORDERS
            // =========================================================================
            drawbox(0, 0, screen_w, screen_h, 10, 0x000000, 0);
            drawbox(cx - 91, sy - 36, 220, 136, 95, 0x000000, 1);
            drawbox(cx - 95, sy - 40, 220, 136, 100, 0x14141E, 0);
            drawbox(cx - 94, sy - 39, 218, 1, 105, 0x3A3A4A, 0);
            drawbox(cx - 94, sy - 39, 1, 134, 105, 0x3A3A4A, 0);
            drawbox(cx - 95, sy - 42, 220, 4, 108, flash_color, 1);
            drawbox(cx - 95, sy - 42, 220, 2, 110, flash_color, 0);

            // Title Header Text
            drawstring(cx - 75, sy - 28, 1, "SOUNDTRACK", 600);
         
            // Track Counter Display (E.g. "[ 1 / 8 ]")
            // OpenBOR implicitly converts integers to text strings when using string addition (+)
            int display_track = track + 1;
            drawstring(cx + 40, sy - 28, 1, "[ " + display_track + " / 8 ]", 600);

            // Active Music Mode Badge Graphic
            if (m_type == 1) {
                drawbox(cx - 50, sy - 17, 100, 8, 105, rgbcolor(0, 120, 255), 0);
                drawstring(cx - 42, sy - 17, 0, "MODE: ARRANGED", 600);
            } else {
                drawbox(cx - 50, sy - 17, 100, 8, 105, rgbcolor(10, 132, 0), 0);
                drawstring(cx - 40, sy - 17, 0, "MODE: ORIGINAL", 600);
            }

            // Inner Playlist Box Elements
            drawbox(cx - 90, sy - 6, 210, 100, 101, 0x0D0D14, 0);
            drawbox(cx - 92, sy - 8, 214, 2, 610, flash_color, 0);
            drawbox(cx - 92, sy + 94, 214, 2, 610, flash_color, 0);
            drawbox(cx - 92, sy - 8, 2, 104, 610, flash_color, 0);
            drawbox(cx + 120, sy - 8, 2, 104, 610, flash_color, 0);
            drawbox(cx - 90, sy - 6, 210, 1, 610, 0xFFFFFF, 0);
            drawbox(cx - 90, sy + 93, 210, 1, 610, 0xFFFFFF, 0);
            drawbox(cx - 90, sy - 6, 1, 100, 610, 0xFFFFFF, 0);
            drawbox(cx + 119, sy - 6, 1, 100, 610, 0xFFFFFF, 0);

            // Flashing Scroll Arrows Logic
            if ((state_ticks / 10) % 2 == 0) {
                drawstring(cx + 12, sy - 5, 1, "^", 620);
                drawstring(cx + 12, sy + 84, 1, "v", 620);
            }

            // Looping playlist rendering loop (Renders previous, active, and upcoming track names)
            for (i = 0; i < 3; i++) {
                int target_track = track + (i - 1);
                if (target_track < 0) target_track = 7;
                if (target_track > 7) target_track = 0;

                y = sy + 4 + i * 28;

                if (i == 1) {
                    // Accent Neon Glow Highlight for selected track list item
                    drawbox(cx - 88, y - 2, 206, 22, 112, flash_color, 1);
                    drawbox(cx - 86, y, 202, 18, 114, flash_color, 1);
                    drawbox(cx - 85, y + 1, 200, 16, 116, flash_color, 0);
                    drawbox(cx - 85, y, 200, 1, 118, 0xFFFFFF, 0);
                    drawbox(cx - 85, y + 17, 200, 1, 118, 0xFFFFFF, 0);
                    current_layer = 1; // High contrast text color layer
                } else {
                    // Standard backing plinth for inactive items
                    drawbox(cx - 85, y + 2, 200, 14, 102, 0x13131F, 0);
                    current_layer = 0; // Default text color layer
                }

                // Audio track labeling checks
                if (target_track == 0)      drawstring(cx - 60, y + 4, current_layer, "TRACK 1", 600);
                else if (target_track == 1) drawstring(cx - 60, y + 4, current_layer, "TRACK 2", 600);
                else if (target_track == 2) drawstring(cx - 60, y + 4, current_layer, "TRACK 3", 600);
                else if (target_track == 3) drawstring(cx - 60, y + 4, current_layer, "TRACK 4", 600);
                else if (target_track == 4) drawstring(cx - 60, y + 4, current_layer, "TRACK 5", 600);
                else if (target_track == 5) drawstring(cx - 60, y + 4, current_layer, "TRACK 6", 600);
                else if (target_track == 6) drawstring(cx - 60, y + 4, current_layer, "TRACK 7", 600);
                else if (target_track == 7) drawstring(cx - 60, y + 4, current_layer, "TRACK 8", 600);
            }

            // Lower Info Panel Footer Box
            drawbox(cx - 95, sy + 101, 220, 34, 104, 0x0A0A0F, 0);
            drawbox(cx - 95, sy + 100, 220, 1, 106, 0x252535, 0);
         
            // Centered control layout labels
            drawstring(cx - 35, sy + 104, 1, "SPECIAL: MODE", 600);
            drawstring(cx - 30, sy + 114, 1, "ATTACK: PLAY", 600);
        }

        // Commit and write menu status updates back to engine RAM variables
        setglobalvar("menu_state", state);
        setglobalvar("menu_soundtrack_track", track);
        setglobalvar("menu_state_ticks", state_ticks);
        setglobalvar("menu_selection", selection);
        setglobalvar("menu_music_type", m_type);
    }
}

STEP 2: Creating the automatic music swapping script for stages
Now we need to make regular stages respond to the menu_music_type variable that we toggle inside the player.
  1. Inside your data/scripts/ folder, create a new text file and name it level_music.c.
  2. Paste the following code into it (it automatically checks the current level ID and loads the corresponding arranged track):
C:
void main()
{
// Execute this check only once when the stage loads
if (!getglobalvar("level_music_done"))
{
setglobalvar("level_music_done", 1);
int m_type = getglobalvar("menu_music_type");
if (m_type == NULL()) m_type = 0;
// If the player selected ARRANGED mode in the jukebox
if (m_type == 1)
{
int current_lvl = openborvariant("current_level");
void found_track = NULL();
// Match your stages according to their order in levels.txt (starting from 0)
if (current_lvl == 0)      found_track = "Track_1.ogg";        // The very first stage in levels.txt
else if (current_lvl == 1) found_track = "Track_2.ogg";        // The second stage in the list
else if (current_lvl == 2) found_track = "Track_3.ogg"; // The third stage, and so on...
// If a track is assigned, override the original and play the arranged version
if (found_track)
{
playmusic("data/music/Arr/" + found_track, 1);
}
}
}
}
STEP 3: Hooking up the player and the music script to your stages
The last thing to do is to declare the created music script directly inside your stage structure.
  1. Open your stage configuration file: data/levels.txt.
  2. Find your game set section and attach the level_music.c script to all playable levels using the levelscript parameter:
Code:
Hook up the automatic music swapper as a global script for all levels
levelscript data/scripts/level_music.c
set             My_Custom_Mod
lives           3
credits 3

1. Your menu stage (where the Soundtrack Jukebox interface will render)
z               160 200
file data/levels/menu.txt # This stage index is ID 0

2. Regular stages of your game
z               160 200
file            data/levels/test.txt   # This stage index is ID 1
Once you launch the game, you will immediately load into the test stage menu.txt. Thanks to the updated.c file, a beautiful custom jukebox UI will render directly on top of the empty background screen.
You can press the SPECIAL button to toggle between ORIGINAL and ARRANGED modes, and the ATTACK button will play the highlighted songs. As soon as you transition to an actual gameplay stage, the level_music.c script will automatically inherit the settings from the jukebox and fire up the correct version of the soundtrack!


 
Back
Top Bottom