diff --git a/rogueviz/platformer.cpp b/rogueviz/platformer.cpp new file mode 100644 index 00000000..f966fb23 --- /dev/null +++ b/rogueviz/platformer.cpp @@ -0,0 +1,832 @@ +#include "../rogueviz/rogueviz.h" + +/** \brief Hyperbolic platformer. +The implementation of the platformer from https://twitter.com/ZenoRogue/status/1467233150380089345 +To play, load platformer.lev (e.g. `-load platformer.lev`) +Or create a new map by running with options: + `-noscr -canvas B -geo dbin -platformer` + +Keys: +up/left/right -- move the guy +c -- clear the current room (buggy, does not clear parts of the adjacent rooms) +g -- generate the current room (buggy) +m -- see the map (toggle) +p -- pause (toggle) +z -- screenshot menu +q -- quit +s -- save to platformer.lev +1 -- place a small block under the mouse +2 -- place a big block under the mouse +3 -- delete the block under the mouse + +Have fun! +*/ + +namespace hr { + +namespace platformer { + +/* the size of a block, in pixels */ +constexpr int block_x = 8; +constexpr int block_y = 8; + +/* the size of a room (screen), in blocks */ +constexpr int room_x = 32; +constexpr int room_y = 24; + +/* the size of a room, in pixels */ +constexpr int screen_x = block_x * room_x; +constexpr int screen_y = block_y * room_y; + +/* the size of the area shared between right/left blocks */ +constexpr int lr_margin = 4; + +/* the size of the area shared with the block above */ +constexpr int t_margin = 2; + +/* the size of the area shared with the block below */ +constexpr int b_margin = 2 * t_margin; + +/* the position of margins in pixels */ +constexpr int l_margin_at = lr_margin * block_x / 2; +constexpr int r_margin_at = screen_x - block_x * lr_margin / 2; +constexpr int t_margin_at = t_margin * block_y / 2; +constexpr int b_margin_at = screen_y - b_margin * block_y / 2; + +/* between the margins */ +constexpr int actual_screen_x = r_margin_at - l_margin_at; +constexpr int actual_screen_y = b_margin_at - t_margin_at; + +int game_fps = 300; +bool bottom = 1; + +basic_textureinfo roomtinf; + +hpcshape roomshape; + +constexpr auto yctr = (t_margin_at + b_margin_at) / 2.; +constexpr auto xctr = (l_margin_at + r_margin_at) / 2.; + +double mscale = 100; + +hyperpoint to_hyper(ld x, ld y) { + y -= t_margin_at; + y += actual_screen_y; + x -= xctr; + + y /= mscale; + x /= mscale; + + y += .5; + ld d = x*x + y*y; + x /= d; + y /= d; + y -= 1; + hyperpoint h; + h[0] = -x; h[1] = y; h[2] = 1; + h = spin(-90*degree) * h; + return perspective_to_space(h, 1, gcHyperbolic); + } + +pair from_hyper(hyperpoint h) { + h = spin(+90*degree) * h; h[0] = -h[0]; + h[2] += 1; + h /= h[2]; + + h[1] += 1; + ld d = h[0]*h[0] + h[1]*h[1]; + h /= d; + h[1] -= .5; + + double x = h[0], y = h[1]; + + y *= mscale; + x *= mscale; + + y -= actual_screen_y; + y += t_margin_at; + x += xctr; + + return {x, y}; + } + +void prepare_tinf() { + cgi.bshape(roomshape, PPR::WALL); + + auto add_vertex = [&] (double bx, double by) { + roomtinf.tvertices.push_back(glhr::makevertex(bx / screen_x, (by + (screen_x-screen_y)/2.) / screen_x, 0)); + cgi.hpc.push_back(to_hyper(bx, by)); + }; + + for(int by=t_margin_at; byflags |= POLY_TRIANGLES; + cgi.last->tinf = &roomtinf; + cgi.last->texture_offset = 0; + cgi.finishshape(); + cgi.extra_vertices(); + println(hlog, "room generated at ", roomshape.s, " to ", roomshape.e, " tvertices = ", isize(roomtinf.tvertices)); + } + +bool is_right(struct room *r); +struct room *get_adjacent(struct room *r, int i); + +struct room { + texture::texture_data *room_texture; + cell *where; + char block_at[room_y][room_x]; + + void clear() { + for(int y=0; ymaster->distance; + println(hlog, "ylev = ", ylev); + if(ylev <= 0) + for(int y=room_y-6; y= room_x || y >= room_y) return; + if(b < 8) b = 0; + if((b & 4) && (x&1) != (b&1)) return; + if((b & 4) && (2*(y&1)) != (b&2)) return; + if(y < t_margin && (b&4)) return; + if(y > room_y - b_margin && !(b&4) && b) return; + if(block_at[y][x] == b) return; + + block_at[y][x] = b; + if(b & 4) { + place_block_full(x^1, y, b^1); + place_block_full(x, y^1, b^2); + } + else { + if(block_at[y][x^1] & 4) + place_block_full(x^1, y, 0); + if(block_at[y^1][x] & 4) + place_block_full(x, y^1, 0); + } + + if(x < lr_margin) + get_adjacent(this, 2)->place_block_full(x + room_x - lr_margin, y, b); + if(x >= room_x - lr_margin) + get_adjacent(this, 4)->place_block_full(x - room_x + lr_margin, y, b); + if(y < t_margin) { + if(x < room_x / 2) { + get_adjacent(this, 1)->place_block_full((x - lr_margin/2) * 2 + lr_margin/2, room_y - b_margin + 2*y, b^4); + } + else { + get_adjacent(this, 0)->place_block_full((x - room_x/2) * 2 + lr_margin/2, room_y - b_margin + 2*y, b^4); + } + } + + if(y >= room_y - b_margin) { + bool r = is_right(this); + x -= lr_margin/2; + x /= 2; + x += (r ? room_x/2 : lr_margin/2); + y = y - room_y + b_margin; + y /= 2; + b &= ~3; + get_adjacent(this, 3)->place_block_full(x, y, b ^ 4); + } + } + + void generate() { + clear(); + place_small(0, 0); + place_small(room_x/2-1, 0); + place_small(room_x/2, 0); + place_small(room_x-1, 0); + place_big(0, room_y-2); + place_big(room_x-2, room_y-2); + + /* + for(int x=0; x rooms; + +bool paused; + +room *current_room; + +void switch_to_room(room *r) { + current_room = r; + } + +room *get_room_at(cell *c) { + bool create = !rooms.count(c); + auto& r = rooms[c]; + if(create) { + r.where = c; + r.clear(); + r.initial(); + c->wall = waNone; + c->land = laCanvas; + } + return &r; + } + +bool is_right(room *r) { + cell *c = r->where; + c->cmove(3); + return c->c.spin(3) == 0; + } + +room *get_adjacent(room *r, int i) { + cell *c = r->where; + c->cmove(i); + c = c->cmove(i); + return get_room_at(c); + } + +void switch_to_adjacent_room(int i) { + switch_to_room(get_adjacent(current_room, i)); + } + +string block[8] = { + "11111111", + "12444461", + "13555571", + "13555571", + "13555571", + "13555571", + "14666671", + "11111111" + }; + +const int man_x = 8, man_y = 12; + +string man[man_y] = { + "..1111..", + "..1111..", + "..1111..", + "11111111", + "..1111..", + "..1111..", + "1..11..1", + ".111111.", + "...11...", + "..1111..", + ".11..11.", + "11....11", + }; + +double where_x = screen_x / 2.; +double where_y = screen_y / 2.; + +double get_scale() { + return (actual_screen_y + (where_y - t_margin_at)) / (actual_screen_y * 3/2.); + } + +struct bbox { + int minx, miny, maxx, maxy; + }; + +bbox get_pixel_bbox() { + bbox b; + double d = get_scale(); + b.minx = where_x - man_x * d / 2; + b.maxx = where_x + man_x * d / 2 + 1; + b.miny = where_y - man_y * d / 2; + b.maxy = where_y + man_y * d / 2 + 1; + return b; + } + +bbox pixel_to_block(bbox b) { + b.minx /= block_x; + b.miny /= block_y; + b.maxx = (b.maxx + block_x-1) / block_x; + b.maxy = (b.maxy + block_y-1) / block_y; + if(b.minx < 0) b.minx = 0; + if(b.miny < 0) b.miny = 0; + if(b.maxx >= room_x) b.maxx = 0; + if(b.maxy >= room_y) b.maxy = 0; + return b; + } + +bool conflict_at(double x, double y) { + dynamicval fx(where_x, x); + dynamicval fy(where_y, y); + bbox b = pixel_to_block(get_pixel_bbox()); + for(int xx=b.minx; xxblock_at[yy][xx]) return true; + return false; + } + +static double gtime = 0; + +double vel_x = 0, vel_y = 0; + +static const double grav = 0.1; + +bool map_on = false; + +bool last_mkey = false; + +extern int mousepx, mousepy; + +void game_frame() { + const Uint8 *keystate = SDL12_GetKeyState(NULL); + if(keystate['3']) + current_room->place_block_full(mousepx / block_x, mousepy / block_y, 0); + if(keystate['1']) + current_room->place_block_full(mousepx / block_x, mousepy / block_y, 8); + if(keystate['2']) + current_room->place_block_full(mousepx / block_x, mousepy / block_y, 12); + + if(paused) return; + + double d = get_scale(); + + bool on_floor = false; + + ld modv = 60. / game_fps; + ld moda = modv * modv; + + vel_y += d * grav * moda; + + while(conflict_at(where_x, where_y + vel_y)) { + if(vel_y > 0) on_floor = true; + vel_y /= 2; + if(vel_y < 1e-9) { vel_y = 0; break; } + if(!conflict_at(where_x, where_y + vel_y)) where_y += vel_y; + } + where_y += vel_y; + + double avel_x = vel_x; + while(conflict_at(where_x + vel_x, where_y)) { + vel_x /= 2; + if(vel_x < 1e-9) { vel_x = 0; break; } + if(!conflict_at(where_x + vel_x, where_y)) where_x += vel_x; + } + where_x += vel_x; + vel_x = avel_x; + + hyperpoint h_at = to_hyper(where_x, where_y); + // ld test_x, test_y; + // tie(test_x, test_y) = from_hyper(h_at); + + /*println(hlog, tie(where_x, where_y), " TO ", h_at, " TO ", tie(test_x, test_y)); + exit(1); */ + + hyperpoint h_was = to_hyper(where_x - vel_x, where_y - vel_y); + hyperpoint h_willbe = rgpushxto0(h_at) * MirrorX * MirrorY * gpushxto0(h_at) * h_was; + ld next_x, next_y; + tie(next_x, next_y) = from_hyper(h_willbe); + vel_x = next_x - where_x; + vel_y = next_y - where_y; + + if(on_floor) { + if(keystate[SDLK_LEFT]) vel_x = -d * modv; + else if(keystate[SDLK_RIGHT]) vel_x = d * modv; + else vel_x = 0; + if(keystate[SDLK_UP]) vel_y = -4 * d * modv; + } + else { + if(keystate[SDLK_LEFT]) vel_x += -d * .01 * moda; + else if(keystate[SDLK_RIGHT]) vel_x += d * .01 * moda; + vel_y += d * grav * moda; + } + + if(where_x < l_margin_at) { + where_x += actual_screen_x; + switch_to_adjacent_room(2); + } + if(where_x > r_margin_at) { + where_x -= actual_screen_x; + switch_to_adjacent_room(4); + } + + if(where_y < t_margin_at) { + where_y = (where_y - t_margin_at) * 2 + b_margin_at; + where_x -= l_margin_at; + where_x = 2 * where_x; + if(where_x > actual_screen_x) { + where_x -= actual_screen_x; + switch_to_adjacent_room(0); + } + else + switch_to_adjacent_room(1); + where_x += l_margin_at; + vel_x *= 2; vel_y *= 2; + } + if(where_y > b_margin_at) { + where_x -= l_margin_at; + where_y -= b_margin_at; + where_y /= 2; + where_y += t_margin_at; + if(is_right(current_room)) + where_x += actual_screen_x; + switch_to_adjacent_room(3); + where_x /= 2; + where_x += l_margin_at; + vel_x /= 2; vel_y /= 2; + } + + } + +void room::create_texture() { + if(room_texture) return; + room_texture = new texture::texture_data; + + auto& tex = *room_texture; + + tex.twidth = tex.theight = 256; + + tex.tx = screen_x; + tex.ty = screen_y; + tex.stretched = false; + tex.strx = tex.tx; + tex.stry = tex.ty; + tex.base_x = 0; + tex.base_y = (tex.theight - tex.ty) / 2; + } + +texture::texture_data *sprite_texture; + +void create_sprite_texture() { + if(sprite_texture) return; + sprite_texture = new texture::texture_data; + auto& tex = *sprite_texture; + tex.twidth = tex.theight = 256; + tex.whitetexture(); + for(int y=0; y<256; y++) + for(int x=0; x<256; x++) + tex.get_texture_pixel(x, y) = 0; + for(int y=0; ytexture_id = texture_id; dqi_poly::draw(); } + }; + +basic_textureinfo sprite_vertices; + +template void render_room_objects(room *r, R render_at); + +void render_room(room *r); + +bool draw_room_on_map(cell *c, const shiftmatrix& V) { + hr::addaura(tC0(V), 0xFF00FF00, 0); + if(!rooms.count(c)) { + c->landparam = 0x101010; + get_room_at(c); + return false; + } + auto& r = rooms[c]; + if(!r.room_texture) render_room(&r); + if(!r.room_texture) return false; + + auto& p = queuea (PPR::WALL); + p.V = V; + p.offset = roomshape.s; + p.cnt = roomshape.e - roomshape.s; + p.color = 0xFFFFFFFF; + p.tab = &cgi.ourshape; + p.flags = roomshape.flags; + p.tinf = &roomtinf; + p.offset_texture = 0; + p.texture_id = r.room_texture->textureid; + println(hlog, "offset = ", p.offset, " texture_offset = ", p.offset_texture); + + auto render_at = [&] (GLuint texid, double px0, double py0, double px1, double py1, + double tx0, double ty0, double tx1, double ty1) { + + auto addpoint = [&] (int x, int y) { + curvepoint(to_hyper(x ? px1 : px0, y ? py1 : py0)); + sprite_vertices.tvertices.emplace_back(glhr::makevertex(x ? tx1 : tx0, y ? ty1 : ty0, 1)); + }; + + int tcurvestart = isize(sprite_vertices.tvertices); + + addpoint(0, 0); + addpoint(0, 1); + addpoint(1, 1); + addpoint(0, 0); + addpoint(1, 0); + addpoint(1, 1); + + auto& p = queuea (PPR::MONSTER_BODY); + p.V = V; + p.offset = curvestart; + p.cnt = isize(curvedata) - curvestart; + curvestart = isize(curvedata); + p.color = 0xFFFFFFFF; + p.tab = &curvedata; + p.flags = POLY_TRIANGLES; + p.tinf = &sprite_vertices; + p.offset_texture = tcurvestart; + p.texture_id = texid; + }; + + render_room_objects(&r, render_at); + return true; + } + +void render_room(room *r) { + r->create_texture(); + auto& tex = *(r->room_texture); + tex.whitetexture(); + + auto pb = get_pixel_bbox(); + auto bb = pixel_to_block(pb); + + for(int y=0; yblock_at[ly][lx]; + + if(c == 0) { + if(false && lx >= bb.minx && lx < bb.maxx && ly >= bb.miny && ly < bb.maxy) + tex.get_texture_pixel(x, y + tex.base_y) = 0xFF008000; + else + tex.get_texture_pixel(x, y + tex.base_y) = 0xFF000000; + } + + else { + int ax = x % block_x; + int ay = y % block_y; + if(c & 4) { + ax /= 2; + ay /= 2; + if(c & 1) ax += (block_x/2); + if(c & 2) ay += (block_y/2); + } + char chr = block[ay][ax]; + tex.get_texture_pixel(x, y + tex.base_y) = 0xFFFFFFFF + 0x202020 * (chr - '7'); + } + } + + tex.loadTextureGL(); + } + +template void render_room_objects(room *r, R render_at) { + auto pb = get_pixel_bbox(); + if(r != current_room) return; + render_at(sprite_texture->textureid, pb.minx, pb.miny, pb.maxx, pb.maxy, 0, 0, man_x/256., man_y/256.); + } + +int mousepx, mousepy; + +void draw_room() { + render_room(current_room); + create_sprite_texture(); + flat_model_enabler fme; + auto& tex = *(current_room->room_texture); + + ld tx = tex.tx; + ld ty = tex.ty; + ld scalex = (vid.xres/2) / (current_display->radius * tx); + ld scaley = (vid.yres/2) / (current_display->radius * ty); + ld scale = min(scalex, scaley); + scale *= 4; + + double low = tex.base_y * 1. / tex.theight; + + auto render_at = [&] (GLuint texid, double px0, double py0, double px1, double py1, + double tx0, double ty0, double tx1, double ty1) { + static vector rtver(4); + + ld cx[4] = {1,0,0,1}; + ld cy[4] = {1,1,0,0}; + + for(int i=0; i<4; i++) { + rtver[i].texture[0] = cx[i] ? tx1 : tx0; + rtver[i].texture[1] = cy[i] ? ty1 : ty0; + rtver[i].coords[0] = ((cx[i] ? px1 : px0) - screen_x/2) * scale; + rtver[i].coords[1] = ((cy[i] ? py1 : py0) - screen_y/2) * scale; + rtver[i].coords[2] = 1; + rtver[i].coords[3] = 1; + } + + glhr::be_textured(); + current_display->set_projection(0, false); + glBindTexture(GL_TEXTURE_2D, texid); + glhr::color2(0xFFFFFFFF); + glhr::id_modelview(); + current_display->set_mask(0); + glhr::prepare(rtver); + glhr::set_depthtest(false); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + }; + + render_at(tex.textureid, 0, 0, screen_x, screen_y, 0, low, 1, 1-low); + render_room_objects(current_room, render_at); + + mousepx = (mousex - current_display->xcenter) * 2 / scale / current_display->radius + screen_x/2; + mousepy = (mousey - current_display->ycenter) * 2 / scale / current_display->radius + screen_y/2; + } + +void run() { + clearMessages(); + dialog::init(); + + sprite_vertices.tvertices.clear(); + if(map_on) { + render_room(current_room); + gamescreen(0); + } + else { + draw_room(); + } + + dialog::add_key_action('c', [] { current_room->clear(); }); + dialog::add_key_action('g', [] { current_room->generate(); }); + dialog::add_key_action('m', [] { + map_on = !map_on; + }); + dialog::add_key_action('p', [] { + paused = !paused; + }); + dialog::add_key_action('z', [] { + pushScreen(shot::menu); + }); + dialog::add_key_action('q', [] { exit(0); }); + dialog::add_key_action('s', [] { + mapstream::saveMap("platformer.lev"); + }); + + keyhandler = [] (int sym, int uni) { + if(map_on && paused) + handlePanning(sym, uni); + dialog::handleNavigation(sym, uni); + }; + } + +void add_platf_hooks(); + +void set_sval() { + + ld s_min = 10, s_max = 1200; + + for(int it=0; it<100; it++) { + mscale = sqrt(s_min * s_max); + hyperpoint atop = deparabolic13(to_hyper(0, t_margin_at)); + if(atop[0] < -log(2)/2) s_max = mscale; + else s_min = mscale; + } + } + +void enable() { + + stop_game(); + + set_sval(); + + hyperpoint aleft = deparabolic13(to_hyper(l_margin_at, yctr)); + hyperpoint aright = deparabolic13(to_hyper(r_margin_at, yctr)); + + vid.binary_width = abs(aright[1] - aleft[1]) / log(2); + + start_game(); + cgi.prepare_shapes(); + + current_room = get_room_at(cwt.at); + + prepare_tinf(); + + add_platf_hooks(); + } + +void add_platf_hooks() { + + rogueviz::rv_hook(hooks_prestats, 90, [=] { + + if(nomap) + draw_room(); + else + render_room(current_room); + + return true; + }); + + rogueviz::rv_hook(shmup::hooks_turn, 90, [=] (int d) { + gtime += d; + while(gtime > 1000. / game_fps) { + gtime -= 1000. / game_fps; + game_frame(); + } + return true; + }); + + rogueviz::rv_hook(hooks_drawcell, 90, draw_room_on_map); + + rogueviz::rv_hook(mapstream::hooks_savemap, 100, [] (fhstream& f) { + f.write(66); + for(auto& p: rooms) { + f.write(mapstream::cellids[p.first]); + for(int y=0; y(0); + } + f.write(-1); + f.write(mapstream::cellids[current_room->where]); + f.write(where_x); + f.write(where_y); + f.write(vel_x); + f.write(vel_y); + }); + + pushScreen(run); + } + +auto chk = arg::add3("-platformer", enable) ++ addHook(mapstream::hooks_loadmap, 100, [] (fhstream& f, int id) { + if(id == 66) { + println(hlog, "loading platformer"); + while(true) { + int i = f.get(); + if(i == -1) break; + auto r = get_room_at(mapstream::cellbyid[i]); + for(int y=0; yblock_at[y][x]); + f.get(); + } + int id = f.get(); + current_room = get_room_at(mapstream::cellbyid[id]); + f.read(where_x); + f.read(where_y); + f.read(vel_x); + f.read(vel_y); + add_platf_hooks(); + println(hlog, "done"); + set_sval(); + + cgi.prepare_shapes(); + prepare_tinf(); + } + }); + +} +}