local enet = require"enet" local json = require"common.dkjson" local utf8 = require"utf8" local SERVER_HOSTNAME = "ubq323.website" if os.getenv"HEXEMU_LOCAL" then SERVER_HOSTNAME = "localhost" end local PLAYER_SIZE = require"common.constants".PLAYER_SIZE local local_player = nil local drawing = require"drawing" local coords = require"common.coords" local Pos = coords.Pos local camera = require"camera".Camera:make() local ChunkC = require"chunk".ChunkC local util = require"util" local Map = require"common.map".Map local movement = require"movement" local msgbox = require"msgbox" local drawing2 = require"drawing2" -- local pprint=require"common.pprint" -- pprint.setup{show_all=true} local SCENE = {} local help_text = [[ controls: wasd: move shift: sprint left mouse: place right mouse: destroy mousewheel: zoom in/out F1: show/hide this help F3: show/hide debug enter: toggle chat]] math.randomseed(os.time()) local map = Map:make() local host,peer local selected_tile = 9 -- normal: regular gameplay -- chat: chat box is open -- more modes may come local ui_mode = "normal" _G.debugmode = false local show_controls = false local this_chatmsg = "" local chatmsg_text = love.graphics.newText(love.graphics.getFont()) function SCENE.keypressed(key,scancode,isrepeat) if ui_mode == "normal" then if key == "f3" then _G.debugmode = not _G.debugmode end if key == "f1" then show_controls = not show_controls end for i = 1,9 do if key == tostring(i) then selected_tile = i end end if key == "return" then ui_mode = "chat" this_chatmsg = "" end elseif ui_mode == "chat" then if key == "return" then ui_mode = "normal" if this_chatmsg:sub(1,3) == "/tp" then local x,y = this_chatmsg:match("/tp (%S+) (%S+)") if x then x,y = tonumber(x),tonumber(y) local_player.pos.x=x local_player.pos.y=y local_player.pos_dirty=true end else peer:send(json.encode{t="chat",msg=this_chatmsg}) end -- msgbox.add("[me] "..this_chatmsg) elseif key == "escape" then ui_mode = "normal" elseif key == "backspace" then local boffs = utf8.offset(this_chatmsg,-1) if boffs then this_chatmsg = this_chatmsg:sub(1,boffs-1) end end end end function SCENE.textinput(text) if ui_mode == "chat" then this_chatmsg = this_chatmsg..text end end local function draw_player(pl,islocal) love.graphics.setColor(pl.color) love.graphics.circle("fill",pl.pos.x,pl.pos.y, PLAYER_SIZE) if islocal then love.graphics.setLineWidth(0.01) love.graphics.setColor(0.5,0,0) love.graphics.circle("line",pl.pos.x,pl.pos.y,PLAYER_SIZE) end if pl.username then -- think of a better way to unscale but keep transformation love.graphics.push() love.graphics.translate(pl.pos.x, pl.pos.y) love.graphics.scale(1/camera.zoom) util.print_good(pl.username, 0, 0) love.graphics.pop() end end local remote_players = {} local function update_local_player(pl,dt) local SPEED = 8*math.sqrt(3) -- 8 hexagonheights per second if love.keyboard.isDown("lshift") or love.keyboard.isScancodeDown'kpenter' then SPEED = SPEED*2 end local function kd(codes) for _,code in ipairs(codes) do if love.keyboard.isScancodeDown(code) then return 1 end end return 0 end local dx = kd{"d","kp6"}-kd{"a","kp4"} local dy = kd{"s","kp5"}-kd{"w","kp8"} if dx == 0 and dy == 0 then return end if dx ~= 0 and dy ~= 0 then -- 60degrees direction, to follow hex grid -- instead of 45degrees diagonal dx = dx * 0.5 dy = dy * (math.sqrt(3)/2) end local try_pos = Pos:make( pl.pos.x + SPEED*dt*dx, pl.pos.y + SPEED*dt*dy ) pl.pos = movement.collide_with_terrain(pl.pos,try_pos,map) -- pl.pos = try_pos pl.pos_dirty = true end local function sync_local_player(pl) -- send updated info about local player to server if pl.pos_dirty then peer:send(json.encode{t="ppos",x=pl.pos.x,y=pl.pos.y},1) pl.pos_dirty = false end end local function send_settile(hpos,tile) peer:send(json.encode{t="settile",q=hpos.q,r=hpos.r,tile=tile}) end function SCENE.wheelmoved(dx,dy) camera.zoom = camera.zoom * (1.15 ^ dy) camera.zoom = math.max(2.5,math.min(50,camera.zoom)) end local function handle_net() -- handle network packets repeat local ev = host:service() if ev and ev.type == "receive" then -- print(ev.data) local j = json.decode(ev.data) local op = j.t -- if op ~= "chunk" then print(ev.channel,ev.data) end if op == "join" then local pl = j.pl remote_players[pl.id] = { pos=coords.Pos:make(pl.x,pl.y), color=pl.color, id=pl.id, username = pl.username, } msgbox.add(pl.username.." joined") elseif op == "leave" then local id = j.id msgbox.add(remote_players[id].username.." left") remote_players[id]=nil elseif op == "move" then local id,x,y = j.id,j.x,j.y remote_players[id].pos.x = x remote_players[id].pos.y = y elseif op == "you" then local pl = j.pl local_player = { pos=coords.Pos:make(pl.x,pl.y), color=pl.color, id=pl.id, username=pl.username, inv = {}, } elseif op == "chunk" then local ch = ChunkC:from_packet_data(j) map:add_chunk(ch) elseif op == "give" then local old_count = local_player.inv[j.tile] or 0 local_player.inv[j.tile] = old_count + 1 elseif op == "settile" then local h = coords.Hex:make(j.q,j.r) map:set_at(h,j.tile) elseif op == "chat" then local msg,from = j.msg,j.from msgbox.add("["..tostring(from).."] "..msg) end end until not ev end local function update(dt) msgbox.update(dt) if ui_mode == "normal" then -- movement update_local_player(local_player,dt) sync_local_player(local_player) -- mouse input (place/mine) local msx,msy = love.mouse.getPosition() local mh = camera:screen_to_world(Pos:make(msx,msy)):to_hex():round() local inv = local_player.inv if map:at(mh) == 0 and love.mouse.isDown(1) then local old_count = inv[selected_tile] or 0 if old_count > 0 then map:set_at(mh,selected_tile) send_settile(mh,selected_tile) inv[selected_tile] = old_count - 1 end elseif map:at(mh) ~= 0 and love.mouse.isDown(2) then map:set_at(mh,0) send_settile(mh,0) end end -- load and unload chunks local player_cp = local_player.pos:to_hex():containing_chunk() -- load chunks near to player (within 3x3 square) for _,cp in ipairs(player_cp:neighborhood()) do if map:chunk(cp) == nil then map:mark_chunk_loading(cp) peer:send(json.encode{t="reqchunk",u=cp.u,v=cp.v}) end end -- unload chunks not near player -- todo maybe: instead of immedately unloading chunks when we -- move away, instead have some kind of 'last near' time, so -- that if the player is moving back and forth, we don't -- repeatedly unload and reload a given chunk. local to_remove = {} for cp in map:iter_chunks() do local d = player_cp:orth_dist(cp) if d > 1 then map:remove_chunk(cp) end end handle_net() end local function draw() love.graphics.clear(1,1,1) love.graphics.origin() camera.pos = local_player.pos camera:apply_trans() -- drawing.draw_map(camera,map) drawing2.draw_map(camera,map) draw_player(local_player,true) for _,pl in pairs(remote_players) do draw_player(pl) end -- mouse position (resp. screen, world, hex) local sm = Pos:make(love.mouse.getPosition()) local wm = camera:screen_to_world(sm) local hm = wm:to_hex() -- draw reticle local hmr = hm:round() local mouse_tile = map:at(hmr) if mouse_tile then local col = {0.5,0.5,0.5} if mouse_tile == 0 then col = {0,0,0} end local c = hmr:to_pos() local verts = {} for i=0,5 do local angle = math.pi*2*(i+0.5)/6 table.insert(verts, c.x+math.cos(angle)) table.insert(verts, c.y+math.sin(angle)) end love.graphics.setColor(col) love.graphics.setLineWidth(0.05) love.graphics.polygon("line",verts) end love.graphics.origin() -- selected tile (temp) util.print_good(tostring(selected_tile), "center",10) local W,H = love.graphics.getDimensions() if _G.debugmode and local_player then util.print_good(table.concat({ "ms "..tostring(sm), "mw "..tostring(wm), "mh "..tostring(hm).." "..tostring(hm:round()), "", "pw "..tostring(local_player.pos), "ph "..tostring(local_player.pos:to_hex()).." " ..tostring(local_player.pos:to_hex():round()), "", "voob "..tostring(camera.zoom), "", "fps "..tostring(love.timer.getFPS()), "ping "..tostring(peer:round_trip_time()), "", "res"..W.."x"..H, },"\n"),10,10) end local inv_rows = {'inventory:'} for t=1,9 do local c = local_player.inv[t] or 0 local s = t..': '..util.tile_names[t]..' x'..c if selected_tile == t then s = s .. ' <--' end table.insert(inv_rows, s) end util.print_good(table.concat(inv_rows,'\n'),"end",0) if show_controls then util.print_good(help_text,"center","center") end msgbox.draw() if ui_mode ~= "normal" then util.print_good(ui_mode, -20,10) end if ui_mode == "chat" then chatmsg_text:set("- "..this_chatmsg) local tw,th = chatmsg_text:getDimensions() local y = H-th-30 love.graphics.setColor(0,0,0,0.8) love.graphics.rectangle("fill",0,y,W,th) love.graphics.setColor(1,1,1) love.graphics.draw(chatmsg_text,0,y) love.graphics.setColor(0.8,0.8,0.8) love.graphics.line(tw,y,tw,y+th) end end local connected = false local sent_hshake = false local username = nil function SCENE.update(dt) if connected then return update(dt) else handle_net() if peer:state() == "connected" and not sent_hshake then peer:send(json.encode{t='handshake',username=username}) sent_hshake = true elseif peer:state() == "connected" and local_player then connected = true msgbox.add("connected to "..SERVER_HOSTNAME..":8473") msgbox.add("press F1 for controls help") end end end function SCENE.draw() if connected then return draw() else love.graphics.clear(1,1,1) love.graphics.print("connecting...",10,10) end end function SCENE.load(_username) love.keyboard.setKeyRepeat(true) -- require"profile".start(10,io.open("./trace","w")) host = enet.host_create() peer = host:connect(SERVER_HOSTNAME..":8473",2) username = _username end function SCENE.quit() -- require"profile".stop() peer:disconnect() host:flush() end return SCENE