1DOLLY 72APPLICATION bhop3REQUIRES HOST runtime@04REQUIRES HOST display@05REQUIRES HOST input@06REQUIRES HOST audio@07REQUIRES HOST touch@08REQUIRES HOST download@09REQUIRES HOST http@010REQUIRES HOST snapshot@011REQUIRES HOST threads@012REQUIRES HOST upload@01314FROM /v0.1.4/demos/slopyard/Dollyfile-gamedev-sdk 4efb41ab327e8a4e041e63025f514d2dd57c446804cf8b76d3c6d8b5ee287e2a15INSTALL /v0.1.4/demos/javascript/Dollyfile-janis dd50b79294be6df6edae90f83d70e3a5e3cfeecb04694785e90e73791b102d5716INSTALL /v0.1.4/demos/pi/Dollyfile-pi-dolly 164864aff1fee96a12245be17b4259dd6553d125aced558822e131292b407ab117INSTALL /v0.1.4/demos/sdl2/Dollyfile-sdl2 b9abe5f4c12b3ba281f42a3e6cf4967d6947a2ae23457b4dbecfddcde3020a9b18REQUIRES TOOL cc19REQUIRES TOOL make20REQUIRES TOOL c++21REQUIRES TOOL pi22REQUIRES TOOL download23REQUIRES TOOL tar24REQUIRES TOOL rm25REQUIRES HEADER cpp26REQUIRES HEADER quickjs-runner27REQUIRES HEADER sdl228REQUIRES LIB dolly-js29REQUIRES LIB quickjs30REQUIRES LIB SDL23132SOURCE /v0.1.4/dist/static/bhop/source.tar 0446cbeb662cd144fac5cd12973093bf92db6c0295b4a55dd71a0aa283df4c3c /tmp/bhop-source.tar33SOURCE /v0.1.4/dist/static/default/stb_truetype.h ecd30b05e0dd4fea3a13c26810dd9e1992dc379049482c393d5a19e6b5090aab /tmp/bhop-stb/stb_truetype.h34SLOP tar -xf /tmp/bhop-source.tar -C /3536FILE /usr/src/dolly/bhop/bhop-movement.h37    #ifndef DOLLY_BHOP_MOVEMENT_H38    #define DOLLY_BHOP_MOVEMENT_H39    #include <math.h>40    #include <string.h>41    42    #define BH_STEP 0.01f43    #define BH_RADIUS 16.0f44    #define BH_HEIGHT 72.0f45    #define BH_GRAVITY 800.0f46    #define BH_RUN_SPEED 250.0f47    48    typedef struct { float low[3], high[3]; } bh_box;49    typedef struct {50      float position[3], velocity[3];51      int ground, jump_queue, jumps;52    } bh_player;53    typedef struct { float fraction, normal[3]; int box; } bh_trace;54    55    static float bh_speed(const bh_player *player) {56      return hypotf(player->velocity[0], player->velocity[2]);57    }58    59    // Original implementation of GoldSrc's projection-limited acceleration rule.60    // Only the air projection is capped at 30; the acceleration uses full wishspeed.61    static void bh_accelerate(bh_player *player, float yaw, float forward, float side, int air) {62      float length = hypotf(forward, side);63      if (length == 0) return;64      float direction[3] = {65        (sinf(yaw) * forward + cosf(yaw) * side) / length, 0,66        (-cosf(yaw) * forward + sinf(yaw) * side) / length67      };68      float projection = player->velocity[0] * direction[0] + player->velocity[2] * direction[2];69      float available = (air ? 30.0f : BH_RUN_SPEED) - projection;70      float acceleration = fminf(available, (air ? 10.0f : 5.0f) * BH_RUN_SPEED * BH_STEP);71      if (acceleration <= 0) return;72      player->velocity[0] += direction[0] * acceleration;73      player->velocity[2] += direction[2] * acceleration;74    }75    76    static bh_trace bh_sweep(const float position[3], const float delta[3],77                             const bh_box *boxes, int count) {78      bh_trace best = {.fraction = 1, .box = -1};79      for (int box = 0; box < count; ++box) {80        if (boxes[box].low[0] >= boxes[box].high[0] || boxes[box].low[1] >= boxes[box].high[1] ||81            boxes[box].low[2] >= boxes[box].high[2]) continue;82        float enter = -INFINITY, leave = INFINITY, normal[3] = {0};83        int intersects = 1;84        for (int axis = 0; axis < 3; ++axis) {85          float low = boxes[box].low[axis] - (axis == 1 ? BH_HEIGHT : BH_RADIUS);86          float high = boxes[box].high[axis] + (axis == 1 ? 0 : BH_RADIUS);87          if (fabsf(delta[axis]) < 1e-8f) {88            if (position[axis] <= low || position[axis] >= high) { intersects = 0; break; }89            continue;90          }91          float first = (low - position[axis]) / delta[axis];92          float last = (high - position[axis]) / delta[axis];93          float sign = -1;94          if (first > last) { float swap = first; first = last; last = swap; sign = 1; }95          if (first > enter) {96            enter = first;97            memset(normal, 0, sizeof(normal));98            normal[axis] = sign;99          }100          leave = fminf(leave, last);101          if (enter > leave) { intersects = 0; break; }102        }103        if (intersects && enter >= 0 && enter < best.fraction && leave >= 0) {104          best.fraction = enter;105          best.box = box;106          memcpy(best.normal, normal, sizeof(normal));107        }108      }109      return best;110    }111    112    static void bh_slide(bh_player *player, const bh_box *boxes, int count) {113      float remaining = BH_STEP;114      for (int bump = 0; bump < 4 && remaining > 0; ++bump) {115        float delta[3];116        for (int axis = 0; axis < 3; ++axis) delta[axis] = player->velocity[axis] * remaining;117        bh_trace hit = bh_sweep(player->position, delta, boxes, count);118        for (int axis = 0; axis < 3; ++axis)119          player->position[axis] += delta[axis] * hit.fraction + hit.normal[axis] * 0.01f;120        if (hit.box < 0) break;121        remaining *= 1 - hit.fraction;122        float inward = 0;123        for (int axis = 0; axis < 3; ++axis) inward += player->velocity[axis] * hit.normal[axis];124        if (inward < 0) for (int axis = 0; axis < 3; ++axis)125          player->velocity[axis] -= inward * hit.normal[axis];126      }127    }128    129    static void bh_ground(bh_player *player, const bh_box *boxes, int count) {130      player->ground = -1;131      if (player->velocity[1] > 0) return;132      const float down[3] = {0, -2, 0};133      bh_trace floor = bh_sweep(player->position, down, boxes, count);134      if (floor.box >= 0 && floor.normal[1] > 0.7f) {135        player->position[1] -= 2 * floor.fraction - 0.01f;136        player->velocity[1] = 0;137        player->ground = floor.box;138      }139    }140    141    static void bh_tick(bh_player *player, float yaw, float forward, float side,142                        const bh_box *boxes, int count) {143      bh_ground(player, boxes, count);144      if (player->jump_queue > 0 && player->ground >= 0) {145        player->velocity[1] = sqrtf(2 * BH_GRAVITY * 45);146        player->ground = -1;147        player->jump_queue = 0;148        ++player->jumps;149      }150      if (player->jump_queue > 0) --player->jump_queue;151      if (player->ground >= 0) {152        float speed = bh_speed(player);153        if (speed > 0) {154          float retained = fmaxf(0, speed - fmaxf(75, speed) * 4 * BH_STEP) / speed;155          player->velocity[0] *= retained;156          player->velocity[2] *= retained;157        }158      }159      bh_accelerate(player, yaw, forward, side, player->ground < 0);160      if (player->ground >= 0 && bh_speed(player) > BH_RUN_SPEED) {161        float scale = BH_RUN_SPEED / bh_speed(player);162        player->velocity[0] *= scale;163        player->velocity[2] *= scale;164      }165      player->velocity[1] -= BH_GRAVITY * BH_STEP / 2;166      bh_slide(player, boxes, count);167      player->velocity[1] -= BH_GRAVITY * BH_STEP / 2;168      bh_ground(player, boxes, count);169    }170    #endif171172FILE /usr/src/dolly/bhop/bhop-course.h173    #ifndef DOLLY_BHOP_COURSE_H174    #define DOLLY_BHOP_COURSE_H175    #include "bhop-movement.h"176    177    enum { BH_PADS = 33, BH_PAD_DELAY = 10, BH_PAD_RETURN = 200 };178    typedef struct {179      bh_box pads[BH_PADS];180      int cooldown[BH_PADS], collapses;181    } bh_course;182    183    static int bh_checkpoint(int pad) { return pad >= 0 && pad < BH_PADS && pad % 8 == 0; }184    static void bh_course_init(bh_course *course) {185      const float x[BH_PADS] = {0, 0, 48, 110, 160, 180, 140, 70, 0,186        -60, -130, -180, -160, -100, -20, 60, 120,187        180, 220, 190, 120, 40, -40, -120, -180,188        -200, -150, -80, 0, 80, 150, 190, 180};189      const float top[BH_PADS] = {0, 0, 0, 16, 16, 32, 32, 48, 48,190        32, 16, 0, -16, -16, 0, 0, 0,191        0, 16, 16, 32, 32, 48, 48, 64,192        64, 80, 80, 96, 96, 112, 112, 128};193      memset(course, 0, sizeof(*course));194      float z = 0;195      for (int i = 0; i < BH_PADS; ++i) {196        int section = i ? (i - 1) / 8 : 0;197        if (i) z -= i == 1 ? 400 : 220 + section * 10;198        float width = i == 0 ? 512 : bh_checkpoint(i) ? 224 : 88 - section * 8;199        float depth = bh_checkpoint(i) ? (i == 0 ? 512 : 256) : width;200        course->pads[i] = (bh_box){{x[i] - width / 2, top[i] - 32, z - depth / 2},201          {x[i] + width / 2, top[i], z + depth / 2}};202      }203    }204    static void bh_course_tick(bh_course *course, bh_player *player, float yaw, float forward, float side) {205      bh_box solid[BH_PADS];206      memcpy(solid, course->pads, sizeof(solid));207      for (int i = 0; i < BH_PADS; ++i) {208        if (course->cooldown[i] && --course->cooldown[i] == BH_PAD_RETURN) ++course->collapses;209        if (course->cooldown[i] > 0 && course->cooldown[i] <= BH_PAD_RETURN)210          solid[i].high[1] = solid[i].low[1];211      }212      bh_tick(player, yaw, forward, side, solid, BH_PADS);213      int pad = player->ground;214      if (pad >= 0 && !bh_checkpoint(pad) && !course->cooldown[pad])215        course->cooldown[pad] = BH_PAD_DELAY + BH_PAD_RETURN;216    }217    #endif218219FILE /usr/src/dolly/bhop/bhop-check.c220    #include "bhop-course.h"221    #include <stdio.h>222    223    #define CHECK(condition) do { if (!(condition)) { \224      fprintf(stderr, "bhop movement check failed at line %d: %s\n", __LINE__, #condition); return 1; \225    } } while (0)226    227    int main(void) {228      bh_player player = {.velocity = {0, 0, -300}};229      bh_accelerate(&player, 0, 1, 0, 1);230      CHECK(player.velocity[2] == -300);231      bh_accelerate(&player, 0, 0, 1, 1);232      CHECK(player.velocity[0] == 25 && player.velocity[2] == -300);233      bh_accelerate(&player, 0, 0, 1, 1);234      CHECK(player.velocity[0] == 30 && bh_speed(&player) > 300);235      player = (bh_player){0};236      bh_accelerate(&player, 0, 1, 1, 1);237      CHECK(fabsf(bh_speed(&player) - 25) < 0.001f);238    239      bh_player strafe = {.velocity = {0, 0, -300}}, held = strafe;240      for (int tick = 0; tick < 100; ++tick) {241        float yaw = atan2f(strafe.velocity[2], strafe.velocity[0]) + acosf(5 / bh_speed(&strafe));242        bh_accelerate(&strafe, yaw, 0, 1, 1);243        bh_accelerate(&held, 0, 0, 1, 1);244      }245      CHECK(bh_speed(&strafe) > 420 && bh_speed(&held) < 302);246      printf("bhop: synchronized strafe %.3f u/s; fixed-direction strafe %.3f u/s\n",247        bh_speed(&strafe), bh_speed(&held));248    249      const bh_box floor = {{-10000, -100, -10000}, {10000, 0, 10000}};250      player = (bh_player){.position = {0, 0.01f, 0}, .velocity = {100, 0, 0}};251      bh_tick(&player, 0, 0, 0, &floor, 1);252      CHECK(fabsf(player.velocity[0] - 96) < 0.001f && player.ground == 0);253      player = (bh_player){.position = {0, 0.01f, 0}, .velocity = {600, 0, 0}, .jump_queue = 2};254      float peak = 0;255      for (int tick = 0; tick < 65; ++tick) {256        bh_tick(&player, 0, 0, 0, &floor, 1);257        peak = fmaxf(peak, player.position[1]);258        CHECK(fabsf(player.velocity[0] - 600) < 0.001f);259      }260      CHECK(peak > 44.9f && peak < 45.1f && player.jumps == 1);261      while (player.ground < 0) bh_tick(&player, 0, 0, 0, &floor, 1);262      player.jump_queue = 2;263      bh_tick(&player, 0, 0, 0, &floor, 1);264      CHECK(player.jumps == 2 && player.ground == -1 && player.velocity[0] == 600);265      for (int tick = 0; tick < 100; ++tick) bh_tick(&player, 0, 0, 0, &floor, 1);266      CHECK(player.jumps == 2 && player.ground == 0 && bh_speed(&player) < 250);267    268      const bh_box wall = {{100, -100, -100}, {110, 200, 100}};269      player = (bh_player){.position = {60, 10, 0}, .velocity = {5000, 0, -100}};270      bh_slide(&player, &wall, 1);271      CHECK(player.position[0] < 84 && player.position[0] > 83.9f);272      CHECK(player.velocity[0] == 0 && player.velocity[2] == -100);273      CHECK(fabsf(player.position[2] + 1) < 0.001f);274      puts("bhop: projection cap, full-wish acceleration, diagonal input, friction, 45-unit jump, landing momentum and swept wall sliding passed");275    276      bh_course course;277      bh_course_init(&course);278      bh_box pad = course.pads[1];279      player = (bh_player){.position = {(pad.low[0] + pad.high[0]) / 2, pad.high[1] + 0.01f,280        (pad.low[2] + pad.high[2]) / 2}};281      bh_course_tick(&course, &player, 0, 0, 0);282      CHECK(course.cooldown[1] == BH_PAD_DELAY + BH_PAD_RETURN && player.ground == 1);283      for (int i = 1; i < BH_PAD_DELAY; ++i) {284        bh_course_tick(&course, &player, 0, 0, 0);285        CHECK(player.ground == 1);286      }287      bh_course_tick(&course, &player, 0, 0, 0);288      CHECK(player.ground == -1 && course.collapses == 1);289      for (int i = 0; i < BH_PAD_RETURN; ++i) bh_course_tick(&course, &player, 0, 0, 0);290      CHECK(course.cooldown[1] == 0 && player.position[1] < pad.low[1]);291      player = (bh_player){.position = {0, 0.01f, 0}};292      for (int i = 0; i < 250; ++i) bh_course_tick(&course, &player, 0, 0, 0);293      CHECK(player.ground == 0 && course.cooldown[0] == 0);294      bh_course_init(&course);295      player = (bh_player){.position = {0, 0.01f, -400}};296      bh_course_tick(&course, &player, 0, 0, 0);297      player.jump_queue = 2;298      for (int i = 0; i < BH_PAD_DELAY + 1; ++i) bh_course_tick(&course, &player, 0, 0, 0);299      CHECK(player.jumps == 1 && player.position[1] > 20 && course.collapses == 1);300      puts("bhop: 100ms collapse, 2s return, safe checkpoints and immediate jumping passed");301    302      float fastest = 0;303      for (int next = 1; next < BH_PADS; ++next) {304        bh_course_init(&course);305        bh_box start = course.pads[next - 1], end = course.pads[next];306        float sx = (start.low[0] + start.high[0]) / 2, sz = (start.low[2] + start.high[2]) / 2;307        float ex = (end.low[0] + end.high[0]) / 2, ez = (end.low[2] + end.high[2]) / 2;308        // Check each gap independently with real swept physics, not just the ballistic formula.309        // Checkpoints provide a run-up; ordinary pads must be crossed without stopping.310        if (bh_checkpoint(next - 1)) sz = start.low[2] + 8;311        float rise = end.high[1] - start.high[1];312        float flight = (sqrtf(2 * BH_GRAVITY * 45) + sqrtf(2 * BH_GRAVITY * (45 - rise))) / BH_GRAVITY;313        float vx = (ex - sx) / flight, vz = (ez - sz) / flight;314        float speed = hypotf(vx, vz);315        fastest = fmaxf(fastest, speed);316        CHECK(speed < 440 && (!bh_checkpoint(next - 1) || speed < BH_RUN_SPEED));317        player = (bh_player){.position = {sx, start.high[1] + 0.01f, sz},318          .velocity = {vx, 0, vz}, .jump_queue = 2};319        int landed = -1;320        for (int tick = 0; tick < 100 && landed < 0; ++tick) {321          bh_course_tick(&course, &player, 0, 0, 0);322          if (player.ground >= 0) landed = player.ground;323        }324        CHECK(landed == next);325      }326      printf("bhop: all 32 individual gaps reachable, maximum center-landing speed %.1f u/s\n", fastest);327      return 0;328    }329330FILE /usr/src/dolly/bhop/bhop.c331    #define _POSIX_C_SOURCE 200809L332    #include "bhop-course.h"333    #include "agent/input.h"334    #include <dolly/raylib.h>335    #include <raymath.h>336    #include <rlgl.h>337    #include <errno.h>338    #include <stdio.h>339    #include <stdlib.h>340    #include <time.h>341    342    enum { PLATFORM_COUNT = BH_PADS };343    static const Color SKY = {20, 28, 38, 255}, INK = {231, 235, 234, 255};344    static const Color ACCENT = {255, 185, 82, 255}, CYAN = {84, 218, 205, 255};345    static const Color MUTED = {145, 159, 172, 255};346    static const char *record_path = "/workspace/bhop-foundry-record.txt";347    348    typedef struct {349      bh_player player;350      bh_course course;351      int forward, backward, left, right, turn_left, turn_right, looking, quit;352      int checkpoint, furthest, deaths, finished, practice, ticks, jumps, air_ticks, gain_ticks;353      float yaw, pitch, elapsed, best, peak_speed, landing_speed;354    } game;355    356    static double seconds(void) {357      struct timespec now;358      return clock_gettime(CLOCK_MONOTONIC, &now) == 0359        ? now.tv_sec + now.tv_nsec / 1e9 : 0;360    }361    static Color blend(Color from, Color to, float amount) {362      amount = Clamp(amount, 0, 1);363      return (Color){from.r + (to.r - from.r) * amount, from.g + (to.g - from.g) * amount,364        from.b + (to.b - from.b) * amount, 255};365    }366    static Vector3 center(bh_box box) {367      return (Vector3){(box.low[0] + box.high[0]) / 2, (box.low[1] + box.high[1]) / 2,368        (box.low[2] + box.high[2]) / 2};369    }370    static int is_checkpoint(int index) { return bh_checkpoint(index); }371    static void clear_keys(game *state) {372      state->forward = state->backward = state->left = state->right = 0;373      state->turn_left = state->turn_right = 0;374      state->player.jump_queue = 0;375    }376    static void respawn(game *state, int restart) {377      if (restart) {378        state->checkpoint = state->furthest = state->deaths = state->finished = 0;379        state->practice = 0;380        state->ticks = state->jumps = state->air_ticks = state->gain_ticks = 0;381        state->elapsed = state->peak_speed = state->landing_speed = 0;382      }383      memset(state->course.cooldown, 0, sizeof(state->course.cooldown));384      if (restart) state->course.collapses = 0;385      bh_box pad = state->course.pads[state->checkpoint];386      state->player = (bh_player){.position = {(pad.low[0] + pad.high[0]) / 2,387        pad.high[1] + 0.01f, (pad.low[2] + pad.high[2]) / 2 + (state->checkpoint == 0 ? 115 : 40)},388        .ground = state->checkpoint};389      Vector3 here = center(pad), next = center(state->course.pads[state->checkpoint + (state->checkpoint < BH_PADS - 1)]);390      state->yaw = atan2f(next.x - here.x, here.z - next.z);391      state->pitch = -0.06f;392      clear_keys(state);393    }394    static void tick(game *state) {395      if (!state->looking || state->finished) return;396      state->yaw += (state->turn_right - state->turn_left) * BH_STEP * 1.6f;397      int grounded = state->player.ground >= 0;398      int jumps = state->player.jumps;399      float before = bh_speed(&state->player);400      bh_course_tick(&state->course, &state->player, state->yaw, state->forward - state->backward,401        state->right - state->left);402      state->jumps += state->player.jumps - jumps;403      ++state->ticks;404      float speed = bh_speed(&state->player);405      state->peak_speed = fmaxf(state->peak_speed, speed);406      if (!grounded && (state->left || state->right || state->forward || state->backward)) {407        ++state->air_ticks;408        if (speed > before + 0.001f) ++state->gain_ticks;409      }410      if (state->player.position[2] < -180 || state->elapsed > 0) state->elapsed += BH_STEP;411      int pad = state->player.ground;412      if (!grounded && pad >= 0) state->landing_speed = speed;413      if (pad > state->furthest) state->furthest = pad;414      if (pad > state->checkpoint && is_checkpoint(pad)) state->checkpoint = pad;415      if (pad == PLATFORM_COUNT - 1 && !state->finished) {416        state->finished = 1;417        if (!state->practice && (!state->best || state->elapsed < state->best)) {418          state->best = state->elapsed;419          FILE *record = fopen(record_path, "w");420          if (record) { fprintf(record, "%.3f\n", state->best); fclose(record); }421        }422      }423      if (state->player.position[1] < -200) { ++state->deaths; respawn(state, 0); }424    }425    static void input(game *state, const dolly_input_event *event) {426      if (event->type == DOLLY_INPUT_EVENT_POINTER_CAPTURE) {427        state->looking = event->action != 0;428        clear_keys(state);429      } else if (event->type == DOLLY_INPUT_EVENT_POINTER_MOTION && state->looking) {430        state->yaw += event->x * 0.0000011f;431        state->pitch = Clamp(state->pitch - event->y * 0.0000011f, -1.48f, 1.48f);432      } else if (event->type == DOLLY_INPUT_EVENT_FOCUS && !event->action) {433        state->looking = 0;434        clear_keys(state);435      } else if (event->type == DOLLY_INPUT_EVENT_DROPPED) {436        clear_keys(state);437      } else if (event->type == DOLLY_INPUT_EVENT_SCROLL && state->looking && event->y != 0) {438        state->player.jump_queue = 2;439      } else if (event->type == DOLLY_INPUT_EVENT_KEY) {440        int down = event->action != DOLLY_KEY_ACTION_RELEASE;441        if (dolly_raylib_code_is(event, "KeyW")) state->forward = down;442        if (dolly_raylib_code_is(event, "KeyS")) state->backward = down;443        if (dolly_raylib_code_is(event, "KeyA")) state->left = down;444        if (dolly_raylib_code_is(event, "KeyD")) state->right = down;445        if (dolly_raylib_code_is(event, "ArrowLeft")) state->turn_left = down;446        if (dolly_raylib_code_is(event, "ArrowRight")) state->turn_right = down;447        if (event->action == DOLLY_KEY_ACTION_PRESS) {448          if (dolly_raylib_code_is(event, "Space") && state->looking) state->player.jump_queue = 2;449          if (dolly_raylib_code_is(event, "KeyR")) respawn(state, 1);450          if (dolly_raylib_code_is(event, "KeyF")) { ++state->deaths; respawn(state, 0); }451          if (dolly_raylib_code_is(event, "KeyQ")) state->quit = 1;452          const char *sections[] = {"Digit1", "Digit2", "Digit3", "Digit4"};453          for (int i = 0; i < 4; ++i) if (dolly_raylib_code_is(event, sections[i])) {454            state->checkpoint = state->furthest = i * 8;455            state->finished = 0;456            state->practice = 1;457            state->elapsed = 0;458            respawn(state, 0);459          }460        }461      }462    }463    static void label(Font font, const char *message, float x, float y, float size, Color color) {464      DrawTextEx(font, message, (Vector2){x, y}, size, 0, color);465    }466    static void shaded_cube(Vector3 middle, Vector3 size, Color color, float fog) {467      static const int faces[6][4] = {{0, 4, 6, 2}, {1, 3, 7, 5}, {0, 1, 5, 4},468        {2, 6, 7, 3}, {0, 2, 3, 1}, {4, 5, 7, 6}};469      static const float light[] = {0.68f, 0.85f, 0.5f, 1, 0.72f, 0.9f};470      Vector3 corner[8];471      for (int i = 0; i < 8; ++i) corner[i] = (Vector3){middle.x + (i & 1 ? 0.5f : -0.5f) * size.x,472        middle.y + (i & 2 ? 0.5f : -0.5f) * size.y, middle.z + (i & 4 ? 0.5f : -0.5f) * size.z};473      for (int i = 0; i < 6; ++i) {474        Color shade = blend((Color){color.r * light[i], color.g * light[i], color.b * light[i], 255}, SKY, fog);475        DrawTriangle3D(corner[faces[i][0]], corner[faces[i][1]], corner[faces[i][2]], shade);476        DrawTriangle3D(corner[faces[i][0]], corner[faces[i][2]], corner[faces[i][3]], shade);477      }478    }479    static void platform(bh_box box, int index, int cooldown, float fade) {480      int hidden = cooldown > 0 && cooldown <= BH_PAD_RETURN;481      if (hidden) {482        int falling = BH_PAD_RETURN - cooldown;483        if (falling > 24) return;484        float drop = falling * falling * 0.65f;485        box.low[1] -= drop;486        box.high[1] -= drop;487        fade = fmaxf(fade, falling / 26.0f);488      }489      Vector3 middle = center(box);490      Color rim = blend(is_checkpoint(index) ? CYAN : cooldown ? (Color){255, 89, 58, 255} : ACCENT, SKY, fade);491      shaded_cube(middle, (Vector3){box.high[0] - box.low[0], box.high[1] - box.low[1], box.high[2] - box.low[2]},492        (Color){132, 144, 153, 255}, fade);493      DrawCube((Vector3){middle.x, box.high[1] - 3, middle.z},494        box.high[0] - box.low[0] + 1, 6, box.high[2] - box.low[2] + 1, rim);495      float y = box.high[1] + 0.07f;496      int columns = (int)ceilf((box.high[0] - box.low[0]) / 32);497      int rows = (int)ceilf((box.high[2] - box.low[2]) / 32);498      for (int row = 0; row < rows; ++row) for (int column = 0; column < columns; ++column) {499        float left = box.low[0] + column * 32, right = fminf(left + 32, box.high[0]);500        float back = box.low[2] + row * 32, front = fminf(back + 32, box.high[2]);501        Color tile = blend((row + column) & 1 ? (Color){190, 198, 201, 255} : (Color){215, 221, 220, 255}, SKY, fade);502        DrawTriangle3D((Vector3){left, y, back}, (Vector3){left, y, front}, (Vector3){right, y, front}, tile);503        DrawTriangle3D((Vector3){left, y, back}, (Vector3){right, y, front}, (Vector3){right, y, back}, tile);504      }505      if (is_checkpoint(index)) {506        DrawCube((Vector3){middle.x, box.low[1] - 116, middle.z}, 60, 232, 60,507          blend((Color){43, 59, 73, 255}, SKY, fade));508        for (int side = -1; side <= 1; side += 2) {509          float x = side < 0 ? box.low[0] - 8 : box.high[0] + 8;510          DrawCube((Vector3){x, box.high[1] + 24, middle.z + 30}, 5, 5, 130, rim);511          for (int end = -1; end <= 1; end += 2)512            DrawCube((Vector3){x, box.high[1] + 12, middle.z + 30 + end * 62}, 5, 24, 5, rim);513        }514      }515    }516    static void structure(Vector3 eye, Vector3 middle, Vector3 size, Color color) {517      float distance = Vector3Distance(eye, middle);518      if (distance < 1900) shaded_cube(middle, size, color, Clamp((distance - 550) / 1450, 0, 0.95f));519    }520    static void industrial_yard(const game *state, Vector3 eye) {521      const Color steel = {65, 82, 92, 255}, concrete = {101, 115, 120, 255};522      const Color rust = {128, 73, 46, 255}, blue = {40, 89, 117, 255};523      for (int i = 0; i < BH_PADS; i += 2) {524        Vector3 pad = center(state->course.pads[i]);525        if (fabsf(pad.z - eye.z) > 1800) continue;526        int section = i / 8;527        float floor = -250;528        structure(eye, (Vector3){0, floor, pad.z}, (Vector3){1280, 12, 480}, (Color){31, 46, 55, 255});529        for (int side = -1; side <= 1; side += 2) {530          float x = side * 520;531          structure(eye, (Vector3){x, 100, pad.z}, (Vector3){24, 700, 32}, steel);532          structure(eye, (Vector3){x - side * 12, 85, pad.z + 17}, (Vector3){5, 95, 2}, ACCENT);533          structure(eye, (Vector3){side * 625, -135, pad.z}, (Vector3){245, 210, 465}, section == 0 ? blue : concrete);534          if (section == 0) {535            // Stacked ribbed shipping containers and open sky at the loading yard.536            structure(eye, (Vector3){side * 660, 50, pad.z - 75}, (Vector3){270, 165, 300}, i % 4 ? rust : blue);537            for (int rib = 0; rib < 6; ++rib)538              structure(eye, (Vector3){side * 519, 50, pad.z - 200 + rib * 50}, (Vector3){8, 150, 7}, steel);539          } else if (section == 1) {540            // Tall windowed turbine hall, suspended ducts and overhead beams.541            structure(eye, (Vector3){side * 550, 260, pad.z}, (Vector3){32, 340, 460}, concrete);542            for (int window = 0; window < 3; ++window)543              structure(eye, (Vector3){side * 530, 285, pad.z - 150 + window * 150},544                (Vector3){3, 92, 105}, (Color){74, 136, 154, 255});545            structure(eye, (Vector3){side * 345, 330, pad.z}, (Vector3){68, 62, 475}, steel);546          } else {547            // Original reactor/silo shapes; low polygon counts suit the software renderer.548            float distance = Vector3Distance(eye, (Vector3){side * 710, 0, pad.z});549            if (distance < 1700) {550              Color tank = blend(section == 2 ? blue : concrete, SKY, Clamp((distance - 450) / 1450, 0, 0.94f));551              DrawCylinder((Vector3){side * 710, -220, pad.z}, 125, 145, section == 2 ? 590 : 790, 12, tank);552              DrawCylinder((Vector3){side * 710, section == 2 ? 220 : 400, pad.z}, 149, 149, 18, 12,553                blend(section == 2 ? CYAN : ACCENT, SKY, Clamp(distance / 2000, 0, 0.9f)));554            }555            structure(eye, (Vector3){side * 460, -170, pad.z}, (Vector3){14, 20, 470}, section == 2 ? CYAN : ACCENT);556          }557        }558        if (section == 1 || i % 4 == 0) {559          structure(eye, (Vector3){0, 425, pad.z}, (Vector3){1080, 24, 32}, steel);560          structure(eye, (Vector3){0, 409, pad.z}, (Vector3){240, 4, 18}, (Color){233, 219, 168, 255});561        }562        if (is_checkpoint(i)) {563          structure(eye, (Vector3){pad.x, pad.y + 194, pad.z - 100}, (Vector3){310, 46, 24}, blue);564          for (int side = -1; side <= 1; side += 2)565            structure(eye, (Vector3){pad.x + side * 152, pad.y + 90, pad.z - 100}, (Vector3){8, 220, 8}, CYAN);566        }567      }568    }569    static void draw(game *state, Font font, int width, int height) {570      BeginDrawing();571      ClearBackground(SKY);572      DrawRectangleGradientV(0, 0, width, height, (Color){12, 18, 27, 255}, SKY);573      Vector3 eye = {state->player.position[0], state->player.position[1] + 64, state->player.position[2]};574      Camera3D camera = {.position = eye, .target = {eye.x + sinf(state->yaw) * cosf(state->pitch),575        eye.y + sinf(state->pitch), eye.z - cosf(state->yaw) * cosf(state->pitch)},576        .up = {0, 1, 0}, .fovy = 74, .projection = CAMERA_PERSPECTIVE};577      BeginMode3D(camera);578      industrial_yard(state, eye);579      for (int i = 0; i < PLATFORM_COUNT; ++i) {580        float distance = Vector3Distance(eye, center(state->course.pads[i]));581        if (distance < 1900) platform(state->course.pads[i], i, state->course.cooldown[i],582          Clamp((distance - 450) / 1500, 0, 0.92f));583      }584      EndMode3D();585    586      DrawRectangle(0, 0, width, 62, (Color){12, 18, 25, 255});587      label(font, "A I R T I M E", 18, 10, 25, INK);588      label(font, "FOUNDRY / STRAFE TRIALS", 20, 39, 11, ACCENT);589      const char *sections[] = {"01 / LOADING YARD", "02 / TURBINE HALL", "03 / REACTOR", "04 / SILO RUN", "FINISH"};590      label(font, sections[state->checkpoint / 8], width / 2 - 95, 14, 14, CYAN);591      for (int i = 0; i < BH_PADS; ++i) {592        Color color = i <= state->furthest ? CYAN : is_checkpoint(i) ? INK : MUTED;593        DrawRectangle(width / 2 - 96 + i * 6, 43, 3, is_checkpoint(i) ? 8 : 4, color);594      }595      char text[160];596      if (state->practice) snprintf(text, sizeof(text), "%05.2f  /  PRACTICE", state->elapsed);597      else snprintf(text, sizeof(text), "%05.2f  /  BEST %s", state->elapsed, state->best > 0 ? TextFormat("%.2f", state->best) : "--");598      label(font, text, width - 230, 17, 18, INK);599      snprintf(text, sizeof(text), "PAD %02d/%02d    FALLS %d", state->furthest, PLATFORM_COUNT - 1, state->deaths);600      label(font, text, width - 230, 41, 11, MUTED);601    602      DrawRectangle(0, height - 96, width, 96, (Color){12, 18, 25, 255});603      float speed = bh_speed(&state->player);604      snprintf(text, sizeof(text), "%03.0f", speed);605      Color speed_color = speed > 300 ? CYAN : ACCENT;606      label(font, text, width / 2 - 50, height - 86, 40, speed_color);607      label(font, "UNITS / SECOND", width / 2 - 47, height - 43, 11, MUTED);608      DrawRectangle(20, height - 16, width - 40, 3, (Color){45, 58, 69, 255});609      DrawRectangle(20, height - 16, (width - 40) * fminf(speed / 800, 1), 3, speed_color);610      snprintf(text, sizeof(text), "%s  |  SYNC %.0f%%", state->player.ground < 0 ? "AIR" : "GROUND",611        state->air_ticks ? 100.0f * state->gain_ticks / state->air_ticks : 0);612      label(font, text, 20, height - 77, 13, INK);613      snprintf(text, sizeof(text), "LAND %.0f   PEAK %.0f", state->landing_speed, state->peak_speed);614      label(font, text, 20, height - 53, 11, MUTED);615      label(font, "WASD   SPACE / WHEEL JUMP", width - 222, height - 77, 11, INK);616      label(font, "R RESTART   F CHECKPOINT   Q EXIT", width - 222, height - 53, 10, MUTED);617      label(font, "1-4 PRACTICE A SECTION", width - 222, height - 33, 10, MUTED);618    619      DrawLine(width / 2 - 5, height / 2, width / 2 + 5, height / 2, INK);620      DrawLine(width / 2, height / 2 - 5, width / 2, height / 2 + 5, INK);621      int ground = state->player.ground;622      if (ground >= 0 && !is_checkpoint(ground)) {623        label(font, "JUMP!", width / 2 - 25, height / 2 + 25, 16, ACCENT);624        DrawRectangle(width / 2 - 32, height / 2 + 47,625          64 * (state->course.cooldown[ground] - BH_PAD_RETURN) / BH_PAD_DELAY, 3, ACCENT);626      }627      if (!state->looking || state->finished) {628        int panel_width = width > 540 ? 500 : width - 30, x = (width - panel_width) / 2;629        DrawRectangle(x, height / 2 - 74, panel_width, 178, (Color){12, 18, 25, 255});630        DrawRectangle(x, height / 2 - 74, panel_width, 2, ACCENT);631        label(font, state->finished ? "COURSE COMPLETE" : "CLICK TO TAKE THE MOUSE", x + 22, height / 2 - 54, 23, INK);632        label(font, "W to launch. In air, release W and strafe A / D", x + 22, height / 2 - 17, 12, MUTED);633        label(font, "while turning the mouse in the same direction.", x + 22, height / 2 + 3, 12, MUTED);634        label(font, "Yellow pads drop on touch. Cyan decks are safe.", x + 22, height / 2 + 26, 12, INK);635        label(font, state->finished ? "R to race again. Q returns to Slop." :636          "Space or either wheel direction jumps. Esc releases.", x + 22, height / 2 + 65, 12, ACCENT);637      }638    }639    static int usage(const char *name, int status) {640      fprintf(status ? stderr : stdout, "usage: %s [--frames COUNT]\n"641        "Click once for mouse look; Escape releases. WASD moves; Space and wheel up/down jump.\n"642        "Foundry: 32 jumps through the yard, turbine hall, reactor and silos.\n"643        "Yellow pads collapse 100ms after landing and return after 2s; cyan checkpoints stay solid.\n"644        "1-4 practices a section without recording a best time; R begins a full run.\n"645        "R restarts; F returns to a checkpoint; Q exits. Release W in air and match A/D with mouse turn.\n", name);646      return status;647    }648    static void agent_input(void *context, const dolly_input_event *event) { input(context, event); }649    static int agent_frame(game *state, Font font) {650      static unsigned char pixels[960 * 540 * 4];651      draw(state, font, 960, 540);652      EndDrawing();653      rlCopyFramebuffer(0, 0, 960, 540, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, pixels);654      return bh_agent_frame(pixels);655    }656    int main(int argc, char **argv) {657      unsigned long limit = 0;658      if (argc == 2 && strcmp(argv[1], "--help") == 0) return usage(argv[0], 0);659      if (argc == 3 && strcmp(argv[1], "--frames") == 0) {660        char *end;661        errno = 0;662        limit = strtoul(argv[2], &end, 10);663        if (errno || argv[2][0] < '0' || argv[2][0] > '9' || *end || !limit) return usage(argv[0], 2);664      } else if (argc != 1) return usage(argv[0], 2);665      game state = {0};666      const int agent = bh_agent_open(agent_input, &state);667      dolly_raylib graphics = {0};668      if (agent) {669        graphics.surface.width = 960; graphics.surface.height = 540;670        // The agent's viewer shows the frames: raylib renders to memory.671        SetConfigFlags(FLAG_WINDOW_HIDDEN);672        InitWindow(960, 540, "Dolly Airtime");673        if (!IsWindowReady()) return 1;674        SetTraceLogLevel(LOG_WARNING);675      } else if (dolly_raylib_open_sized(&graphics, "Dolly Airtime", 960, 540) != 0) return 1;676      // A crosshair asks for the click that lets the page capture the pointer.677      if (!agent && (dolly_input_set_pointer_relative(graphics.input, 1) != 0 ||678                     dolly_raylib_set_cursor(&graphics, DOLLY_DISPLAY_CURSOR_CROSSHAIR) != 0)) {679        dolly_raylib_close(&graphics);680        fputs("bhop: this runtime does not support captured mouse input\n", stderr);681        return 1;682      }683      Font font = LoadFontEx("/usr/share/fonts/IosevkaTerm-SemiBold.ttf", 32, NULL, 0);684      if (!IsFontValid(font)) { if (agent) CloseWindow(); else dolly_raylib_close(&graphics); return 1; }685      SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);686      rlSetClipPlanes(1, 2400);687      bh_course_init(&state.course);688      respawn(&state, 1);689      FILE *record = fopen(record_path, "r");690      if (record) { if (fscanf(record, "%f", &state.best) != 1 || !isfinite(state.best) || state.best < 0) state.best = 0; fclose(record); }691      double previous = seconds(), accumulator = 0;692      unsigned long frames = 0;693      int result = 0;694      while (!state.quit && (!limit || frames < limit)) {695        if (agent) {696          const struct timespec interval = {0, 8000000}; nanosleep(&interval, NULL);697          if (bh_agent_poll()) break;698        } else if (frames && dolly_raylib_wait_frame(&graphics, 1000) < 0) { result = 1; break; }699        dolly_input_event event;700        for (int i = 0; !agent && i < 256; ++i) {701          int status = dolly_raylib_next_event(&graphics, &event, 0);702          if (status < 0) { result = 1; state.quit = 1; break; }703          if (!status) break;704          input(&state, &event);705        }706        if (state.quit) break;707        double now = seconds();708        accumulator += now >= previous ? fmin(now - previous, 0.1) : 0;709        previous = now;710        int drawn = 0;711        for (int i = 0; accumulator >= BH_STEP && i < 10; ++i, accumulator -= BH_STEP) {712          if (agent) bh_agent_tick();713          tick(&state); drawn = 0;714          if (agent && bh_agent_capture_due()) {715            if (!agent_frame(&state, font)) { result = 1; break; }716            drawn = 1;717          }718        }719        if (result) break;720        if (agent) {721          if (!drawn && !agent_frame(&state, font)) { result = 1; break; }722        } else {723          draw(&state, font, graphics.surface.width, graphics.surface.height);724          if (dolly_raylib_end_frame(&graphics) != 0) { result = 1; break; }725        }726        ++frames;727      }728      UnloadFont(font);729      if (agent) { bh_agent_close(); CloseWindow(); } else dolly_raylib_close(&graphics);730      printf("bhop: ticks=%d jumps=%d falls=%d pad=%d peak=%.3f speed=%.3f collapses=%d\n",731        state.ticks, state.jumps, state.deaths, state.furthest, state.peak_speed, bh_speed(&state.player), state.course.collapses);732      return result;733    }734735FILE /usr/src/dolly/bhop/bhop.mk736    .PHONY: all check737    all: /usr/bin/bhop738    /usr/bin/bhop: /usr/src/dolly/bhop/bhop.c /usr/src/dolly/bhop/bhop-movement.h /usr/src/dolly/bhop/bhop-course.h /usr/src/dolly/bhop/agent/input.c /usr/src/dolly/bhop/agent/input.h /usr/src/dolly/bhop/agent/timeline.h /usr/src/dolly/game-agent/control.h /usr/lib/libdolly-raylib.a /usr/lib/libraylib.a /usr/lib/libm.a739    	cc -std=c17 -O2 -fno-builtin $< /usr/src/dolly/bhop/agent/input.c -o $@ -ldolly-raylib -lraylib -lm740    check:741    	@output=/tmp/bhop-check.$$$$; status=0; cc -std=c17 -O2 -fno-builtin /usr/src/dolly/bhop/bhop-check.c -o "$$output" -lm && "$$output" || status=$$?; rm -f "$$output"; exit "$$status"742743SLOP make -f /usr/src/dolly/bhop/bhop.mk all check744745SLOP c++ -O1 -std=c++11 -I/usr/include/SDL2 -I/tmp/bhop-stb /usr/src/dolly/game-agent/viewer.cpp -o /usr/bin/bhop-viewer -lSDL2 -lm746SLOP cc -std=gnu11 -I/usr/include/dolly -DEMSCRIPTEN=1 -D_GNU_SOURCE -DQUICKJS_NG_BUILD -DNDEBUG -funsigned-char /usr/src/dolly/bhop/agent/launch.c -ldolly-js -lquickjs -o /usr/bin/bhop-agent747SLOP rm -rf /tmp/bhop-source.tar /tmp/bhop-stb748749EXPORTS TOOL bhop750EXPORTS TOOL bhop-agent751EXPORTS TOOL bhop-viewer752EXPORTS FOLDER bhop-agent-source /usr/src/dolly/bhop/agent753EXPORTS FOLDER game-agent-source /usr/src/dolly/game-agent754EXPORTS FOLDER game-input-source /usr/src/dolly/rts755756FILE /etc/dolly/init.slop757    if test -f "$HOME/.dollyrc"; then758      /bin/foreground /bin/slop -e "$HOME/.dollyrc"759    fi760    /bin/foreground -i /usr/bin/bhop-agent761    printf '\nDolly: image entry exited; entering the recovery Slop shell.\n'762    /bin/foreground -i /bin/slop763764FILE /home/dolly/.dollyrc765    printf '\033[33mDOLLY / AIRTIME: FOUNDRY\033[0m\n'766    printf 'Tab hides the agent UI; backtick toggles human/agent controls; Enter opens a prompt.\n'767    printf 'Settings connects OpenRouter or local Codex. Review attempts exports captured frames.\n'768    printf 'WASD moves. Space and scroll up/down jump. R restarts; F returns to a checkpoint.\n'769    printf 'Release W in air, then strafe A/D while turning the mouse in the same direction.\n'770    printf '32 jumps: yard, turbine hall, reactor, silos. Yellow pads collapse; cyan checkpoints are safe.\n'771    printf '1-4 practices a section without saving a best time; R starts a full run.\n'772    printf 'Edit /usr/src/dolly/bhop/bhop.c; make -f /usr/src/dolly/bhop/bhop.mk all check.\n\n'773774ENTRY /bin/foreground -i /bin/slop /etc/dolly/init.slop775