Skip to content

Instantly share code, notes, and snippets.

@dermotbalson
Created October 19, 2014 15:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dermotbalson/0111292e68f6168badd6 to your computer and use it in GitHub Desktop.
Save dermotbalson/0111292e68f6168badd6 to your computer and use it in GitHub Desktop.
Dungeon 1.05 alpha
--# Main
--Dungeon ver 1.05 alpha
--navigation
--touch left 1/3 of screen to turn left, right 1/3 of screen to turn right
--touch top 1/3 of screen to move forward, bottom 1/3 of screen to go backwards
--double tap left or right to turn 90 degrees
--touch centre of screen to stop
--touching multiple times speeds up
--BUGS
--the navigation could probably be improved
displayMode(FULLSCREEN)
supportedOrientations(LANDSCAPE_ANY)
mapp={}
function setup()
Fx.Enabled(false)
FPS=60
Test=""
SetupMap()
AI.init(M.width,M.height,mapp)
player=Player(M.startPos,M.width,mapp,M.walkAccel,M.maxSpeed)
SetupScene()
SetupModels(mapp)
joyMove=JoyStick{radius=100,centre=vec2(105,110)}
joyAim=JoyStick{radius=100,centre=vec2(WIDTH-105,110)}
--ShowInitialMessages()
--ShowHelp()
fill(255)
fontSize(12)
end
function SetupScene()
--"tall" is the height of the camera (sorry about variable names)
tall=M.startPos.y
--get the light range
range=M.range
--set the flicker as % of total light radius
flicker=0.3
--this is added to current position to set the camera direction
look=vec3(0,tall,-1000)
lookDist=1000 --this seems to duplicate the look var, need to investigate
--height of the light we are holding
lightHeight=vec3(0,M.height*0.25,0)
defaultAim=vec2(WIDTH/2,HEIGHT/2)
aimPoint=defaultAim
--moving,aiming={},{}
Fx.Play("hell")
end
function ShowInitialMessages()
Messages.Add{text="Wha...?",5}
Messages.Add{text="What am I doing here?",5}
Messages.Add{text="Where am I?",5}
Messages.Add{text="Wait - this feels like a light",5}
blackout=ElapsedTime+20
Messages.Add{text="",5} --pause messages for 5 sec
end
function ShowHelp()
Messages.Add{text="Touch left or right of screen to turn\na tap turns 5 degrees\na little swipe turns 45 degrees\na bigger swipe turns 90 degrees",
length=10,size=32,colour=color(255,255,0),position=vec2(WIDTH/2,HEIGHT/3)}
Messages.Add{text="Tap the top of the screen to walk forward\n(and the bottom to go backwards)\nTap the middle to stop turning or moving",
length=10,size=32,colour=color(255,255,0),position=vec2(WIDTH/2,HEIGHT/3)}
Messages.Add{text="Tap the yellow dot (lower right)\nto turn the map on and off",
length=5,size=32,colour=color(255,255,0),position=vec2(WIDTH/2,HEIGHT/3)}
end
function SetupMap()
M=ReadMap() --see Map tab
--set up scene
--pass texture, texture scale (0.05 means reduce image by 95%), tile width, and wall height
S=Scene("Dropbox:3D-walls",0.05,M.width,M.height)
mapp={} --table which will hold the position of all the walls for collision avoidance
for i=1,M.size.x do mapp[i]={} end --initialise
--add walls to mesh
--add rooms
for i=1,#M.rooms do
for j,w in pairs(M.rooms[i].walls) do
S:AddSideWall(w,mapp)
end
end
--add corridors
for i=1,#M.corr do
for j,w in pairs(M.corr[i].walls) do
S:AddSideWall(w,mapp)
end
end
--images
--these are added differently, see Billboard class
images={}
for u,i in pairs(M.images) do
images[#images+1]=Billboard(i,M.width,M.height,mapp)
end
--create separate "scene" for floor/ceiling because it has different texture
F=Scene("Dropbox:3D-gravel1s",0.05,M.width,M.height)
F:AddFloorCeiling(M.floor)
--create map we can show on screen
MapScale=6 --scale
MapTrans=150 --transparency
MapImg=image(M.size.x*MapScale,M.size.y*MapScale)
setContext(MapImg)
rectMode(CENTER)
pushStyle()
fill(255,255,255,MapTrans)
for i=1,M.size.x do
for j=1,M.size.y do
if mapp[i][j]==1 then
rect(i*MapScale,j*MapScale,MapScale,MapScale)
end
end
end
popStyle()
setContext()
--set position of map button
mapButtonRadius=25
mapButton=vec2(WIDTH-mapButtonRadius-5,mapButtonRadius+5)
mapShowing=false
--create map button
mapButtonImg=image(mapButtonRadius*2,mapButtonRadius*2)
setContext(mapButtonImg)
pushStyle()
fill(255,255,0,100)
ellipse(mapButtonImg.width/2,mapButtonImg.height/2,mapButtonRadius*2)
popStyle()
setContext()
end
function draw()
background(150)
FPS=0.9*FPS+0.1/DeltaTime --FPS is geometric average
if moving then player:Moving(moving) end
if aiming then ChangeAim(aiming) end
if shooting then
player:Shoot()
shooting=false
end
player:draw()
--set flicker radius using noise
local flickerRange=range*(1+flicker*noise(ElapsedTime))
--set up camera
SetupCamera()
--draw all the walls, roof and floor
S:draw(player.position+lightHeight,flickerRange) --pass light position and flicker level
F:draw(player.position+lightHeight,flickerRange)
--draw billboard images (they are rotated first)
for a,i in pairs(images) do
i:draw(player.position,flickerRange)
end
--draw 3D models
for i,d in pairs(AI.objects) do
if d.model.m and not d.model.m[1].shader then ConfigureModel(d.model) end
d:draw(player.position,flickerRange)
end
DrawMessages()
Fx.Update()
if player.weapon then sprite(M.crosshairs,aimPoint.x,aimPoint.y) end
joyMove:draw()
joyAim:draw()
end
function DrawMessages()
--draw FPS on screen
ortho()
viewMatrix(matrix())
fill(255,255,0)
text("FPS="..math.floor(FPS),50,50)
if Test~="" then text("Test="..Test,300,25) end
--draw map button
sprite(mapButtonImg,mapButton.x,mapButton.y)
DrawMap(player.position)
--draw messages
if blackout then
if blackout>ElapsedTime then
pushStyle()
fill(0)
rect(WIDTH/2,HEIGHT/2,WIDTH+2,HEIGHT+2)
popStyle()
else blackout=nil return end
end
Messages.draw()
end
function SetupCamera()
--camera pos
perspective()
--local yy=player.position.y
--if speed>0 then yy=yy+math.sin(ElapsedTime/4)*3 end
local p=player.position
camera(p.x,p.y,p.z,p.x+look.x,look.y,p.z+look.z)
end
function ConfigureModel(d)
local m=d.m
for i=1,#m do --add lighting to each mesh
m[i]:setColors(m[i].settings.Kd) -- or color(255))
m[i].shader=shader(TileShader.vertexShader,TileShader.fragmentShader)
if m[i].settings.map then
m[i].texture=OBJ.imgPrefix..m[i].settings.map
m[i].shader.hasTexture=true
else m[i].shader.hasTexture=false
end
end
end
function PlayerTile(v)
return vec2(math.floor(v.x/M.width+1),math.floor(-v.z/M.width+1))
end
--handles player movement
function touched(t)
if t.state==ENDED then
player:Stop()
if t.id==movingID then moving=nil end
if t.id==aimingID then aiming=nil end
--moving,aiming=nil,nil
aimPoint=defaultAim
if vec2(t.x,t.y):dist(mapButton)<mapButtonRadius then
mapShowing=not mapShowing
print(FPS)
end
else
--check for joystick
local v=joyAim:touched(t)
if v then
aiming=v
aimingID=t.id
end
v=joyMove:touched(t)
if aiming and v then
shooting=true
moving=false
else
moving=v
movingID=t.id
end
end
end
function ChangeAim(v)
aimPoint=vec2(WIDTH*(v.x/2+0.5),HEIGHT*(v.y/2+0.5))
end
function DrawMap(p)
if not mapShowing then return end
local margin=8
local x,y=WIDTH-MapImg.width-margin,mapButtonRadius*2+margin
sprite(MapImg,x+MapImg.width/2,y+MapImg.height/2)
local a=PlayerTile(p)
pushStyle()
fill(255,255,0,MapTrans*2)
ellipse(x+(p.x/M.width+0.5)*MapScale,y-(p.z/M.width-0.5)*MapScale,MapScale)
popStyle()
end
--# Map
--Dungeon map
--this map is organised as a table
--the map is a rectangular grid of squares
--image here: http://i1303.photobucket.com/albums/ag142/ignatz_mouse/dungeon_zpsc89352fc.png
--Thinking I would have to cull (ie ony draw stuff directly around the player), I organised the map
--to have corridors between all the rooms. This made it easier to say "if you are in room X, also draw
--corridors "Y and Z". It proved unnecessary - so far.
--anyway, that is why I have organised the walls by room, and separately for rooms and corridors
--LOCATION OF IMAGES REQUIRED
--https://www.dropbox.com/sh/i7stxdfcnnh8azx/AAByAiTG7oswE7nczwZj4pxUa?dl=0
function ReadMap()
local map={}
map.width=10 --number of pixels per square (not a good variable name!)
map.height=20 --height of walls in pixels
map.size=vec2(82,114) --number of tiles wide and deep
map.startPos=vec3(74,10,10) --starting tile --74,10
map.walkAccel=0.1 --speed change (pixels/sec) when you touch screen
map.maxSpeed=0.3
map.range=100 --range of light, in pixels
map.rooms,map.corr={},{} --rooms and corridors stored separately
--load the map coordinates
local m,c=map.rooms,map.corr
for i=1,23 do m[i]={} end
for i=1,30 do c[i]={} end
--the rooms and corr tables have provision for wall and neighbour settings
--I'm only using wall settings, as explained above
--room 1
--each wall is stored in a vec4 = (x1,z1,x2,z2) in tile units
--no need for a y value, we know each wall goes from 0 to map.height
m[1].walls={vec4(64,11,75,11),vec4(75,11,75,4),vec4(61,4,75,4),vec4(61,10,61,4)}
--m[1].neighbours={m.corr1} --this was where I was going to specify neighbours
m[2].walls={vec4(41,10,56,10),vec4(56,13,56,10),
vec4(40,19,40,13),vec4(40,19,53,19),vec4(56,19,56,16)}
m[3].walls={vec4(21,16,21,1),vec4(21,1,30,1),vec4(30,10,30,1),vec4(30,20,30,13),
vec4(21,20,30,20),vec4(21,20,21,19)}
m[4].walls={vec4(32,21,34,21),vec4(32,27,32,21),vec4(32,36,32,30),
vec4(38,21,40,21),vec4(40,24,40,21),vec4(40,36,40,27),vec4(35,36,40,36)}
m[5].walls={vec4(13,33,13,21),vec4(16,21,22,21),vec4(22,27,22,21),
vec4(22,39,22,30),vec4(13,39,22,39),vec4(13,39,13,36)}
m[6].walls={vec4(8,28,11,28),vec4(11,28,11,21),vec4(5,21,11,21),vec4(5,28,5,21),vec4(5,28,6,28)}
m[7].walls={vec4(2,39,2,30),vec4(2,30,6,30),vec4(8,30,11,30),
vec4(11,33,11,30),vec4(11,39,11,36),vec4(2,39,11,39)}
m[8].walls={vec4(50,39,50,30),vec4(50,30,59,30),vec4(59,45,59,30),
vec4(50,45,59,45),vec4(50,45,50,42)}
m[9].walls={vec4(23,48,23,47),vec4(23,48,29,48),vec4(32,48,37,48),
vec4(37,48,37,41),vec4(35,41,37,41),vec4(23,41,32,41),vec4(23,44,23,41)}
m[10].walls={vec4(10,56,16,56),vec4(10,56,10,41),vec4(10,41,19,41),vec4(19,44,19,41),vec4(19,56,19,47)}
m[11].walls={vec4(34,68,34,62),vec4(34,68,39,68),vec4(34,59,34,52),
vec4(34,52,48,52),vec4(48,59,48,52),vec4(48,68,48,62),vec4(43,68,48,68)}
m[12].walls={vec4(61,56,61,50),vec4(61,56,66,56),vec4(69,56,72,56),
vec4(72,56,72,41),vec4(67,41,72,41),vec4(61,41,64,41),vec4(61,47,61,41)}
m[13].walls={vec4(13,85,19,85),vec4(13,85,13,76),vec4(13,76,16,76),
vec4(19,76,27,76),vec4(27,85,27,76),vec4(21,85,27,85)}
m[14].walls={vec4(29,91,29,81),vec4(29,81,32,81),vec4(35,81,39,81),
vec4(43,81,45,81),vec4(45,87,45,81),vec4(37,91,45,91),vec4(29,91,34,91)}
m[15].walls={vec4(50,93,50,91),vec4(50,93,58,93),vec4(50,87,50,84),vec4(50,84,55,84),vec4(59,90,59,84)}
m[16].walls={vec4(50,76,50,64),vec4(50,64,61,64),vec4(61,70,61,64),
vec4(61,79,61,73),vec4(59,79,61,79),vec4(51,79,55,79)}
m[17].walls={vec4(74,99,74,93),vec4(74,99,82,99),vec4(82,99,82,84),vec4(74,84,82,84),vec4(74,90,74,84)}
m[18].walls={vec4(50,105,50,99),vec4(50,105,59,105),vec4(59,105,59,96),vec4(51,96,59,96)}
m[19].walls={vec4(28,114,28,107),vec4(28,107,34,107),vec4(28,114,34,114),
vec4(34,114,34,110),vec4(34,108,34,107)}
m[20].walls={vec4(36,111,36,110),vec4(36,111,48,111),vec4(48,111,48,101),
vec4(45,101,48,101),vec4(36,101,42,101),vec4(36,108,36,101)}
m[21].walls={vec4(63,114,63,104),vec4(63,114,77,114),vec4(77,114,77,104),
vec4(69,104,77,104),vec4(63,104,66,104)}
m[22].walls={vec4(23,69,23,65),vec4(23,69,27,69),vec4(29,69,30,69),
vec4(30,69,30,65),vec4(26,65,30,65),vec4(23,65,24,65)}
m[23].walls={vec4(21,63,24,63),vec4(26,63,27,63),vec4(27,63,27,58),vec4(21,58,27,58),vec4(21,60,21,58)}
--corridors now
c[1].walls={vec4(56,13,61, 13),vec4(61,13,61, 10),vec4(56,16,64, 16),vec4(64,16,64, 11)}
c[2].walls={vec4(38,13,40, 13),vec4(38,21,38, 13),vec4(34,21,34, 13),
vec4(30,13,34, 13),vec4(30,10,41, 10)}
c[3].walls={vec4(16,19,21,19),vec4(16,21,16,19),vec4(13,16,21,16),vec4(13,21,13,16)}
c[4].walls={vec4(53,24,53,19),vec4(56,24,56,19),vec4(40,24,53,24),
vec4(56,24,67,24),vec4(40,27,64,27),vec4(64,41,64,27),vec4(67,41,67,24)}
c[5].walls={vec4(8,30,8,28),vec4(6,30,6,28)}
c[6].walls={vec4(22,27,32,27),vec4(22,30,32,30)}
c[7].walls={vec4(32,41,32,36),vec4(35,41,35,36)}
c[8].walls={vec4(19,47,23,47),vec4(19,44,23,44)}
c[9].walls={vec4(29,62,29,48),vec4(32,59,32,48),vec4(29,62,34,62),vec4(32,59,34,59)}
c[10].walls={vec4(48,59,53,59),vec4(48,62,56,62),vec4(56,62,56,50),
vec4(53,59,53,50),vec4(56,50,61,50),vec4(45,50,53,50),vec4(45,50,45,39),
vec4(45,39,50,39),vec4(48,42,50,42),vec4(48,47,48,42),vec4(48,47,61,47)}
c[11].walls={vec4(66,70,66,56),vec4(69,77,69,56),vec4(66,78,66,73),vec4(61,73,66,73),vec4(61,70,66,70)}
c[12].walls={vec4(66,90,66,78),vec4(69,90,69,79)}
c[13].walls={vec4(69,93,74,93),vec4(69,90,74,90)}
c[14].walls={vec4(58,93,66,93),vec4(59,90,66,90)}
c[15].walls={vec4(45,91,50,91),vec4(45,87,50,87)}
c[16].walls={vec4(19,88,19,85),vec4(21,88,21,85)}
c[17].walls={vec4(43,79,51,79),vec4(43,76,50,76)}
c[18].walls={vec4(16,76,16,56),vec4(19,60,19,56),vec4(19,70,19,63),vec4(19,76,19,73)}
c[19].walls={vec4(19,63,21,63),vec4(19,60,21,60)}
c[20].walls={vec4(19,73,32,73),vec4(19,70,27,70),vec4(29,70,35,70),vec4(35,81,35,70),vec4(32,81,32,73)}
c[21].walls={vec4(39,81,39,68),vec4(43,81,43,79),vec4(43,76,43,68)}
c[22].walls={vec4(34,99,34,91),vec4(34,99,42,99),vec4(37,96,50,96),vec4(45,99,50,99),vec4(37,91,42,96)}
c[23].walls={vec4(42,101,42,99),vec4(45,101,45,99)}
c[24].walls={vec4(34,110,36,110),vec4(34,108,36,108)}
c[25].walls={vec4(66,104,66,93),vec4(69,104,69,93)}
c[26].walls={vec4(55,84,55,79),vec4(59,84,59,79)}
c[27].walls={vec4(69,79,72,79),vec4(69,77,72,77)}
c[28].walls={vec4(24,65,24,63),vec4(26,65,26,63)}
c[29].walls={vec4(11,36,13,36),vec4(11,33,13,33)}
c[30].walls={vec4(27,70,27,69),vec4(29,70,29,69)}
--floor coords, wil be used for roof too
map.floor=vec4(2,1,82,114)
--set of images to be billboarded
--table contains image, x, y, z, s
--where x,z are tile positions, y is fraction of map.height
--and s is ambient lighting, 0 for none, 1 for bright, this property makes them glow in the dark
map.images={
-- {readImage("Dropbox:Gargoyle"),53,0.5,16,0.2},
-- {readImage("Dropbox:Gargoyle"),53,0.5,14,0.2},
-- {readImage("Dropbox:Buddha1"),34,0.5,23,0.2},
-- {readImage("Dropbox:Buddha2"),36,0.5,11,0.4}
}
map.crosshairs=CreateCrossHairs(100)
return map
end
function MapToPixel(v) --x,z are tile units, y is pixels
return vec3((v.x-0.5)*M.width,v.y,-(v.z-0.5)*M.width)
end
--modelName,AI_Object,position,rotate,markAsOccupied
function SetupModels(m)
AI.AddObject{modelName="Spider",object=AI.Spider,position=vec3(62,0,5),speed=M.maxSpeed/2,ambient=0.25} --62,5
AI.AddObject{modelName="Zombie",object=AI.Zombie,position=vec3(42,0,15),speed=M.maxSpeed/2}
AI.AddObject{modelName="Revolver",object=AI.Revolver,position=vec3(69,4,9),rotation=vec3(0,90,0)}
AI.AddObject{modelName="Cage3",object=nil,position=vec3(74,0,10),rotation=vec3(0,-90,0),
markAsOccupied={vec2(74,9)}}
end
function CreateCrossHairs(w)
local i=image(w,w)
setContext(i)
stroke(255,255,0,200)
strokeWidth(1)
line(w/2,0,w/2,w)
line(0,w/2,w,w/2)
fill(0,0,0,0)
--ellipse(w/2,w/2,w)
ellipse(w/2,w/2,w*2/3)
setContext()
return i
end
--# Player
--Player
Player=class()
function Player:init(initialPosition,tileWidth,map,walkAccel,maxSpeed)
--self.position=initialPosition
self.tileWidth=tileWidth
self.map=map
self.health=100
self.maxSpeed=maxSpeed
self.walkAccel=walkAccel
--starting position
--deduct 0.5 from x and z so we start in the middle of the specified tile
--concert from tile units to pixels
--z is made negative, if you don't know why, you should go read up on 3D to avoid great confusion
self.position=vec3((initialPosition.x-0.5)*self.tileWidth,
initialPosition.y,
-(initialPosition.z-0.5)*self.tileWidth)
--player settings
self.angle=0 --player orientation
self.turnAngle=1 --how much player turns when screen is touched
--..the speed with which angle changes
self.angleChange=1
self.velocity=vec3(0,0,0)
self.speed=0
end
function Player:draw()
self:Move()
if self.weapon then
if self.weapon.model.name=="Revolver" then
local x,z=1.3*self.deltaV.x,1.3*self.deltaV.z
local pos=self.position+vec3(x,-2.5,z)
local rot=vec3(0,90-self.angle,0)
self.weapon:SetPosRot(pos,rot)
end
end
self:DrawCrossHairs()
end
function Player:DrawCrossHairs()
end
function Player:ChangeVel()
local a=math.rad(self.angle)
local s,c=math.sin(a),math.cos(a)
self.velocity.x,self.velocity.z=self.speed*s,-self.speed*c
self.deltaV=vec3(s,0,-c)
look.x,look.z=lookDist*s,-lookDist*c
end
--check if the square the player is in contains a wall
function Player:Move()
local v=self.position+self.velocity
local a=PlayerTile(v)
if self.map[a.x][a.y]==1 then
self.speed=0
else
self.position=self.position+self.velocity
end
end
function Player:Stop()
self.speed=0
self.velocity=vec3(0,0,0)
end
function Player:Collect(object)
Messages.Add{text=object.model.name.." collected"}
self.weapon=object
end
function Player:Moving(v) --v is vec2
--Test=v.x..", "..v.y
self.angle=self.angle+v.x*self.turnAngle
--Test=Test.." = "..self.angle
self.speed=math.min(self.maxSpeed,self.speed+v.y*self.walkAccel)
self:ChangeVel()
end
function Player:Shoot()
if self.weapon then self.weapon:Shoot() end
end
--# Scene
--main class for handling map wals, floor, roof
Scene=class()
function Scene:init(tex,s,w,h) --tex is image, s is image scaling, w,h are tile size and wall height
self.m=mesh()
if type(tex)=="string" then tex=readImage(tex) end
self.iw,self.ih=tex.width,tex.height
self.width,self.height=w,h
self.v,self.t={},{}
self.s=s or 1
self.m.texture=tex
self.m.shader=shader(TileShader.vertexShader,TileShader.fragmentShader)
self.m.shader.hasTexture=true
self.m.shader.ambient=0
end
--v1,v2,v3,v4 are positions of the four wall corners
function Scene:AddWall(v1,v2,v3,v4)
--figure out tex coords, ie which way the wall is pointing
local d=v2-v1
local dx,dy
if d.x~=0 then dx=d.x elseif d.y~=0 then dx=d.y else dx=d.z end
d=v3-v2
if d.x~=0 then dy=d.x elseif d.y~=0 then dy=d.y else dy=d.z end
--next bit is important for tiling, calculate texture upper limit as width of mesh / size of scaled image
local tx1,tx2=0,math.abs(dx)/self.iw/self.s
local ty1,ty2=0,math.abs(dy)/self.ih/self.s
--vertices
local n=#self.v
self.v[n+1],self.v[n+2],self.v[n+3],self.v[n+4],self.v[n+5],self.v[n+6]=v1,v2,v3,v3,v4,v1
--add to existing mesh vertices which are stored in a "buffer"
local b=self.m:buffer("position")
b:resize(n+6)
for i=1,#self.v do b[i]=self.v[i] end
--now the texture mappings
local n=#self.t
self.t[n+1],self.t[n+2],self.t[n+3],self.t[n+4],self.t[n+5],self.t[n+6]=
vec2(tx1,ty1),vec2(tx2,ty1),vec2(tx2,ty2),vec2(tx2,ty2),vec2(tx1,ty2),vec2(tx1,ty1)
--add to texture buffer
local b=self.m:buffer("texCoord")
b:resize(n+6)
for i=1,#self.t do b[i]=self.t[i] end
self.m:setColors(color(255))
end
--calculates corner vecs for a wall, given a vec4 containing (x1,z1,x2,z2)
function Scene:AddSideWall(v,m) --m is mapp table listing tiles containing walls
local x1,y1,z1=(v.x-0.5)*self.width,0,-(v.y-0.5)*self.width
local x2,y2,z2=(v.z-0.5)*self.width,self.height,-(v.w-0.5)*self.width
self:AddWall(vec3(x1,y1,z1),vec3(x2,y1,z2),vec3(x2,y2,z2),vec3(x1,y2,z1))
--update mapp table to show these tiles are occupied
if m then
for i=math.min(v.x,v.z),math.max(v.x,v.z) do
m[i][v.y]=1
end
for i=math.min(v.y,v.w),math.max(v.y,v.w) do
m[v.x][i]=1
end
end
end
--simpified version of AddSideWall, only this wall is horizontal
function Scene:AddFloorCeiling(v)
local x1,z1=(v.x-0.5)*self.width,-(v.y-0.5)*self.width
local x2,z2=(v.z-0.5)*self.width,-(v.w-0.5)*self.width
self:AddWall(vec3(x1,0,z1),vec3(x2,0,z1),vec3(x2,0,z2),vec3(x1,0,z2))
self:AddWall(vec3(x1,self.height,z1),vec3(x2,self.height,z1),vec3(x2,self.height,z2),vec3(x1,self.height,z2))
end
--not used
function Scene:AddLight(v)
self.m.shader.light=vec3(v.x*self.width,v.y*self.height,v.z*self.width)
end
function Scene:draw(p,r)
self.m.shader.pos=p
self.m.shader.range=r
self.m.shader.mModel = modelMatrix()
self.m:draw()
end
--# Billboard
--handles billboarded images
Billboard=class()
--t is table of image,x,y,z in tile units, then ambient light fraction
--w,h are map.width,map.height, m is mapp table listing all the things the player can't walk through
--we will add the images to this table
function Billboard:init(t,w,h,m)
self.m=mesh()
self.width,self.height=w,h
m[t[2]][t[4]]=1 --mark this tie as occupied
local tex --for image
if type(t[1])=="string" then tex=readImage(t[1]) else tex=t[1] end
--set position in centre of tile
self.x,self.y,self.z=(t[2]-0.5)*self.width,t[3]*self.height,-(t[4]-0.5)*self.width
self.angle=0 --for rotating to face the player
self.count=0 --on;y rotate every few frames, use this counter
--add image to mesh
self.m:addRect(0,0,tex.width*self.y/tex.height,self.y)
self.m.texture=tex
--billboards have their own slightly modified shader to handle transparent pixels
self.m.shader=shader(TransTileShader.vertexShader,TransTileShader.fragmentShader)
self.m.shader.lightbase=t[5] --set ambient light
end
function Billboard:draw(p,r) --p is player position, r is current light range
self.m.shader.pos=p
self.m.shader.range=r
--adjust rotation every 10 frames
self.count=self.count+1
if self.count%10==0 then
--rotate to face player
local dx,dz=self.x-p.x,self.z-p.z
self.angle=math.deg(math.atan(dx/-dz))
end
pushMatrix()
translate(self.x,self.y/2,self.z)
rotate(-self.angle,0,1,0)
self.m.shader.mModel = modelMatrix()
self.m:draw()
popMatrix()
end
--# Models
--Models
Models={
{name="Box",scale=3,mat={0.5,0,0,0,0,0.5,0,0,0,0,0.5,0,0,2,0,1},
url="https://gist.githubusercontent.com/dermotbalson/039e6dd23ea9168a4d04/raw/72719b58b0b072444574df1b0b593808b84756b9/gistfile1.txt"},
{name="Medical1",scale=0.08,mat={-0.8,0,0,0,0,0.8,0,0,-0,0,-0.8,0,0,0,0,1},
url="https://gist.githubusercontent.com/dermotbalson/6939d1bb89181c371263/raw/6c6badc36b50fbe99c20f80e0879069a08d5391d/gistfile1.txt"},
{name="Zombie",scale=0.1,mat={-1,0,0,0,0,1,0,0,-0,0,-1,0,0,-11,0,1},
url="https://gist.githubusercontent.com/dermotbalson/50ed2828d29bccdf13a0/raw/44bae47c689f43f5b0a0b857706a852bfe4b7f53/gistfile1.txt"},
{name="Gate",scale=0.1, mat={-0,0,-1,0,0,1,0,0,1,0,-0,0,0,0,0,1},
url="https://gist.githubusercontent.com/dermotbalson/f7ca1f4047ace425f2c8/raw/3f1ce2d07840cd32b73cdb12732ffb859cfbd66a/gistfile1.txt"},
{name="Gate2",scale=0.075,mat={-0,-0.5,-0,0,-0,0,-0.5,0,0.5,0,-0,0,0,7,0,1},
url="https://gist.githubusercontent.com/dermotbalson/ddbfdce7684b7985d2f0/raw/6b207ce2e992e69a4133630224822eb72d2ad55c/gistfile1.txt"},
{name="Cage3",scale=3,mat=nil,
url="https://gist.githubusercontent.com/dermotbalson/23091122d62ce9d28bb1/raw/6b23dbe862e54f04d2bb4984b00baf4057533bf8/gistfile1.txt"},
{name="Spider",scale=0.1,mat={-1,0,-0,0,0,1,0,0,0,0,-1,0,0,0,-1,1},
url="https://gist.githubusercontent.com/dermotbalson/a7d1cc67e37460b90707/raw/12c35c72d3242f6376241020d0e808d06d324c32/gistfile1.txt"},
{name="Swarm",scale=3,mat=nil,
url="https://gist.githubusercontent.com/dermotbalson/03f901b5da76cf01e5f2/raw/b27a5da4d24b2a4d002d24be860e3f510d34dff4/gistfile1.txt"},
{name="Revolver",scale=0.02,mat={-0,0,-0.5,0,0,0.5,0,0,0.5,0,-0,0,0,1,0,1},
url="https://gist.githubusercontent.com/dermotbalson/f10527becdc7b9a3d550/raw/1714828f4d67c7885f53a8c0e61c590370742e99/gistfile1.txt"},
{name="SpyEye5",scale=1,mat={1,0,0,0,0,1,0,0,0,0,1,0,0,8,0,1},
url="https://gist.githubusercontent.com/dermotbalson/7a821074cdd149812798/raw/2705147580b8d2f596127268b0067333e9a9061d/gistfile1.txt"},
{name="MiniGun",scale=.2,mat=nil,
url="https://gist.githubusercontent.com/dermotbalson/579227d5eb6bc28bc90e/raw/898bebaa30cba2e1e52dce0473e838bdaad93dfb/gistfile1.txt"},
{name="Barrel10",scale=4,mat={0.75,0,0,0,0,0.75,0,0,0,0,0.75,0,0,0,0,1},
url="https://gist.githubusercontent.com/dermotbalson/68e2c8318266dd152f8b/raw/6f6bbf158e72f9764211ba5f00a6bf0e49fc52b0/gistfile1.txt"},
{name="Container",scale=1.2,mat=nil,
url="https://gist.githubusercontent.com/dermotbalson/d71797058ef2487a9632/raw/4c3e7d102b29e1fa35efe75e5aab3198900f9df8/gistfile1.txt"},
{name="Wraith",scale=2,mat={1,0,0,0,0,1,0,0,0,0,1,0,0,-2,0,1},
url="https://gist.githubusercontent.com/dermotbalson/d1d6a73c0b79b9960e54/raw/3fdfcf14728615c6c317c7effafb3219e0764d25/gistfile1.txt"},
{name="Swarm2",scale=1,
url="https://gist.githubusercontent.com/dermotbalson/b432e0601c1805a77b8f/raw/8cbc3bbe8a331350f71fa3167de969c8ec894b3e/gistfile1.txt"},
{name="Leaper",scale=1,
url="https://gist.githubusercontent.com/dermotbalson/ecf548e68dc88f108dbc/raw/31ab54f0fe2c6b1ecbdad28d56ae5f5a5f55113d/gistfile1.txt"},
{name="Antlion",scale=0.05,
url="https://gist.githubusercontent.com/dermotbalson/664eda75c8c0160d1276/raw/0d81abb6b68d6ffbf085e3e027caf9ded6055d18/gistfile1.txt"}
}
function LoadModel(m)
return OBJ(m.name,m.url,m.scale,m.mat)
end
--Object class
O={}
function O.LoadModel(p)
local mod={}
for i=1,#p do
mod[i]=OBJ(p[i][1],p[i][2],p[i][3])
end
return mod
end
--OBJ library
OBJ=class()
OBJ.DataPrefix="cfg_"
OBJ.imgPrefix="Documents:z3D"
function OBJ:init(name,url,scale,mat)
self.name=name
self.callback=callback
self.scale=scale or 1
if mat then self.modelmatrix=matrix(unpack(mat)) end
self.centre=vec3(0,0,0)
self.data=readGlobalData(OBJ.DataPrefix..name)
if self.data then self:ProcessData()
else http.request(url,function(d) self:DownloadData(d) end) end
end
function OBJ:DownloadData(data)
if data~=nil and string.find(data,"[obj]") then
saveGlobalData(OBJ.DataPrefix..self.name,data)
self.data=data
self:ProcessData()
else print("Error loading data for "..self.name) return end
end
function OBJ:ProcessData()
self.mtl={}
self.m={}
local p, v, tx, t, np, n={},{},{},{},{},{}
local s=self.data
local mname
local section="mtl"
for line in s:gmatch("[^\r\n]+") do
line=OBJ.trim(line)
if string.find(line,"%[obj%]")~=nil then section="obj" mname=nil end
--material definition section
if section=="mtl" then
if string.find(line,"newmtl") then
mname=OBJ.GetValue(line)
--print(mname)
self.mtl[mname]={}
else
local code=string.sub(line,1,2)
if code=="Ka" then --ambient
self.mtl[mname].Ka=OBJ.GetColor(line)
--print(mname,"Ka",OBJ.mtl[mname].Ka[1],OBJ.mtl[mname].Ka[2],OBJ.mtl[mname].Ka[3])
elseif code=="Kd" then --diffuse
self.mtl[mname].Kd=OBJ.GetColor(line)
--print(mname,"Kd",OBJ.mtl[mname].Kd[1],OBJ.mtl[mname].Kd[2],OBJ.mtl[mname].Kd[3])
elseif code=="Ks" then --specular
self.mtl[mname].Ks=OBJ.GetColor(line)
--print(mname,"Ks",OBJ.mtl[mname].Ks[1],OBJ.mtl[mname].Ks[2],OBJ.mtl[mname].Ks[3])
elseif code=="Ns" then --specular exponent
self.mtl[mname].Ns=OBJ.GetValue(line)
--print(mname,"Ns",OBJ.mtl[mname].Ns)
elseif code=="ill" then --illumination code
self.mtl[mname].illum=OBJ.GetValue(line)
--print(mname,"illum",OBJ.mtl[mname].illum)
elseif code=="ma" then --texture map name
local u=OBJ.split(OBJ.GetValue(line)," ")
if string.find(u[1],"%.") then
self.mtl[mname].map=string.sub(u[1],1,string.find(u[1],"%.")-1)
else
self.mtl[mname].map=u[1]
end
self.mtl[mname].path=u[2]
--print(mname,line,"\n",OBJ.mtl[mname].map,"\n",OBJ.mtl[mname].path)
end
end
--data section
elseif section=="obj" then
--read in groups of data into separate meshes
local code=string.sub(line,1,2)
--look for material settings, a separate mesh is used for each
if string.find(line,"usemtl") then
if mname then
local m=mesh()
if self.scale~=1 then for i=1,#v do v[i]=v[i]*self.scale end end
m.vertices=v
if #t>0 then m.texCoords=t end
if #n>0 then m.normals=n end
if self.mtl[mname] then m.settings=self.mtl[mname] end
self.m[#self.m+1]=m
end
mname=OBJ.GetValue(line)
v,t,n={},{},{}
--print(mname)
end
if code=="v " then --point position
p[#p+1]=OBJ.GetVec3(line)
elseif code=="vn" then --point normal
np[#np+1]=OBJ.GetVec3(line)
elseif code=="vt" then --texture co-ord
tx[#tx+1]=OBJ.GetVec2(line)
elseif code=="f " then --vertex
local pts,ptex,pnorm=OBJ.GetList(line)
if #pts==3 then
for i=1,3 do v[#v+1]=p[tonumber(pts[i])] end
if ptex then for i=1,3 do t[#t+1]=tx[tonumber(ptex[i])] end end
if pnorm then for i=1,3 do n[#n+1]=np[tonumber(pnorm[i])] end end
elseif #pts==4 then
for i=1,3 do v[#v+1]=p[tonumber(pts[i])] end
if ptex then for i=1,3 do t[#t+1]=tx[tonumber(ptex[i])] end end
if pnorm then for i=1,3 do n[#n+1]=np[tonumber(pnorm[i])] end end
v[#v+1]=p[tonumber(pts[3])]
if ptex then t[#t+1]=tx[tonumber(ptex[3])] end
if pnorm then n[#n+1]=np[tonumber(pnorm[3])] end
v[#v+1]=p[tonumber(pts[4])]
if ptex then t[#t+1]=tx[tonumber(ptex[4])] end
if pnorm then n[#n+1]=np[tonumber(pnorm[4])] end
v[#v+1]=p[tonumber(pts[1])]
if ptex then t[#t+1]=tx[tonumber(ptex[1])] end
if pnorm then n[#n+1]=np[tonumber(pnorm[1])] end
elseif #pts>4 then
local cx,cy,cz=0,0,0
local ttx,tty=0,0
local nx,ny,nz=0,0,0
for i=1,#pts do
local u=p[tonumber(pts[i])] cx,cy,cz=cx+u.x,cy+u.y,cz+u.z
if ptex then local u=tx[tonumber(ptex[i])] ttx,tty=ttx+u.x,tty+u.y end
--if pnorm then local u=p[tonumber(pnorm[i])] nx,ny,nz=nx+u.x,ny+u.y,nz+u.z end
end
local cp=vec3(cx/#pts,cy/#pts,cz/#pts)
if ptex then ct=vec2(ttx/#pts,tty/#pts) end
--if pnorm then cn=vec3(nx/#pts,ny/#pts,nz/#pts):normalize() end
local j
for i=1,#pts do
if i<#pts then j=i+1 else j=1 end
v[#v+1]=p[tonumber(pts[i])]
if ptex then t[#t+1]=tx[tonumber(ptex[i])] end
--if pnorm then n[#n+1]=np[tonumber(pnorm[i])] end
v[#v+1]=p[tonumber(pts[j])]
if ptex then t[#t+1]=tx[tonumber(ptex[j])] end
--if pnorm then n[#n+1]=np[tonumber(pnorm[j])] end
v[#v+1]=cp
if ptex then t[#t+1]=ct end
--if pnorm then n[#n+1]=np[tonumber(cn)] end
end
end
end
end
end
local m=mesh()
if self.scale~=1 then
for i=1,#v do v[i]=v[i]*self.scale end
end
m.vertices=v
if #t>0 then m.texCoords=t end
if #n>0 then m.normals=n end
m.settings=self.mtl[mname]
self.m[#self.m+1]=m
self:GetStats()
--download images if not stored locally
self.MissingImages={}
for i,O in pairs(self.mtl) do
if O.map then
local y=readImage(OBJ.imgPrefix..O.map)
if not y then self.MissingImages[#self.MissingImages+1]={O.map,O.path} end
end
end
if #self.MissingImages>0 then self:LoadImages() end
end
function OBJ:LoadImages()
print("downloading"..self.MissingImages[1][1])
http.request(self.MissingImages[1][2],function(d) self:StoreImage(d) end)
end
function OBJ:StoreImage(d)
--print("saving"..self.MissingImages[1][1])
saveImage(OBJ.imgPrefix..self.MissingImages[1][1],d)
table.remove(self.MissingImages,1)
if #self.MissingImages~=0 then self:LoadImages() end
end
function OBJ:DeleteData()
saveGlobalData(OBJ.DataPrefix..self.name,nil)
for i,O in pairs(self.mtl) do
if O.map then
print("deleting "..OBJ.imgPrefix..O.map)
local y=saveImage(OBJ.imgPrefix..O.map,nil)
end
end
end
function OBJ.ListStoredData()
local t=listGlobalData()
for i,u in pairs(t) do
if string.find(u,"cfg_") then print(u) saveGlobalData(u,nil) end
end
--local t=listGlobalData()
--local a=table.concat(t,",")
--print(a)
end
function OBJ.GetColor(s)
local s1=string.find(s," ")
local s2=string.find(s," ",s1+1)
local s3=string.find(s," ",s2+1)
return color(string.sub(s,s1+1,s2-1)*255,string.sub(s,s2+1,s3-1)*255,string.sub(s,s3+1,string.len(s))*255)
end
function OBJ.GetVec3(s)
local s1=string.find(s," ")
local s2=string.find(s," ",s1+1)
local s3=string.find(s," ",s2+1)
return vec3(math.floor(string.sub(s,s1+1,s2-1)*100)/100,
math.floor(string.sub(s,s2+1,s3-1)*100)/100,
math.floor(string.sub(s,s3+1,string.len(s))*100)/100)
end
function OBJ.GetVec2(s)
local s1=string.find(s," ")
local s2=string.find(s," ",s1+1)
local s3=string.find(s," ",s2+1)
if s3 then
return vec3(math.floor(string.sub(s,s1+1,s2-1)*100)/100,
math.floor(string.sub(s,s2+1,s3-1)*100)/100)
else
return vec2(math.floor(string.sub(s,s1+1,s2-1)*100)/100,
math.floor(string.sub(s,s2+1,string.len(s))*100)/100)
end
end
function OBJ:draw(v,rot,p,r,m,a)
pushMatrix()
if m then v,rot=m(v,rot,p,r) end
translate(v.x,v.y,v.z)
if type(rot)=="number" and rot~=0 then rotate(rot,0,1,0)
else
for i,v in pairs(rot) do
rotate(v.x,v.y,v.z,v.w)
end
end
for i=1,#self.m do
self.m[i].shader.mModel=modelMatrix() --part of lighting
self.m[i].shader.pos=p
self.m[i].shader.range=r
self.m[i]:draw()
end
popMatrix()
end
function OBJ.GetValue(s)
return string.sub(s,string.find(s," ")+1,string.len(s))
end
function OBJ.trim(s)
while string.find(s," ") do s = string.gsub(s," "," ") end
return s:match'^()%s*$' and '' or s:match'^%s*(.*%S)'
end
function OBJ.split(s,sep)
sep=sep or "/"
local p={}
local pattern = string.format("([^%s]+)", sep)
string.gsub(s,pattern, function(c) p[#p+1] = c end)
return p
end
function OBJ.GetList(s)
local p,t,n={},{},{}
--for word in s:gmatch("%w+") do table.insert(p, word) end
p=OBJ.split(s," ")
table.remove(p,1)
for i=1,#p do
local a=OBJ.split(p[i])
if #a==1 then
p[i]=math.abs(a[1])
elseif #a==2 then
p[i]=math.abs(a[1])
t[i]=math.abs(a[2])
elseif #a==3 then
p[i]=math.abs(a[1])
t[i]=math.abs(a[2])
n[i]=math.abs(a[3])
end
end
return p,t,n
end
function OBJ:GetStats()
local x,y,z,n=0,0,0,0
local minx,maxx,miny,maxy,minz,maxz=999,-999,999,-999,999,-999
for i=1,#self.m do
local vv=self.m[i].vertices
local nn=0
for j=1,#vv do
local v=vv[i]
if v~=nil then
x,y,z=x+v.x,y+v.y,z+v.z
nn=nn+1
if v.x<minx then minx=v.x end
if v.x>maxx then maxx=v.x end
if v.y<miny then miny=v.y end
if v.y>maxy then maxy=v.y end
if v.z<minz then minz=v.z end
if v.z>maxz then maxz=v.z end
end
end
n=n+nn
end
self.centre=vec3(x/n,y/n,z/n)
self.minvert=vec3(minx,miny,minz)
self.maxvert=vec3(maxx,maxy,maxz)
self.size=vec3(maxx-minx,maxy-miny,maxz-minz)
end
--# Fx
--Fx
Fx={}
Fx.file="Dropbox:Dungeon"
Fx.On=true
Fx.clips={
["hell"]={start=0,finish=153,volume=0.33},
["littlegirl"]={start=155,finish=177,volume=0.6},
["spider chatter"]={start=179,finish=184,volume=0.1},
["zombie bite"]={start=186,finish=189},
["brains"]={start=191,finish=199},
["brains 2"]={start=201,finish=204},
["shotgun fire"]={start=206,finish=208},
["zombie moan"]={start=211,finish=218}
}
Fx.theme="hell"
Fx.phaseTime=0.5 --phase in/out time
--Fx.status=nil
function Fx.Enabled(s)
Fx.On=s
end
function Fx.Play(clip)
if not Fx.On then return end
if not music.name then
music(Fx.file,false,0,0)
music.paused=true
end
local f=Fx.clips[clip] --get details for new clip
--if the theme has been interrupted, remember where it stopped
if clip~=Fx.theme and Fx.currentClip==Fx.theme then
Fx.clips[Fx.theme].offset=music.currentTime
end
f.volume=f.volume or 1
f.pan=f.pan or 0
f.offset=f.offset or 0
music.currentTime=f.start+f.offset
music.volume=f.volume
Fx.startTime=ElapsedTime
Fx.endOfPhaseInTime=Fx.startTime+Fx.phaseTime
Fx.endTime=ElapsedTime+f.finish-f.start-f.offset
Fx.startOfPhaseOutTime=Fx.endTime-Fx.phaseTime
Fx.currentClip=clip
-- Fx.status="PhaseIn"
music.paused=false
end
function Fx.Stop()
music.paused=true
if Fx.currentClip==Fx.theme then Fx.clips[Fx.theme].offset=0 end
Fx.Play(Fx.theme)
end
function Fx.Update()
if not Fx.currentClip then return end
local v=Fx.clips[Fx.currentClip].volume
if ElapsedTime<Fx.endOfPhaseInTime then
music.volume=v*(ElapsedTime-Fx.startTime)/Fx.phaseTime
elseif ElapsedTime>Fx.startOfPhaseOutTime then
if ElapsedTime>Fx.endTime then Fx.Stop()
else
music.volume=v*(Fx.endTime-ElapsedTime)/Fx.phaseTime
end
end
end
--# AI
--AI
--manages interactive object behaviour
--each object has a class
AI={}
AI.objects={}
function AI.init(tileSize,wallHeight,dungeonMap) --copy of internal map of dungeon
AI.tileSize=tileSize
AI.wallHeight=wallHeight
AI.map=dungeonMap
AI.collisionDistance=tileSize
end
function AI.AddObject(arg)
arg.rotation=arg.rotation or vec3(0,0,0)
mods=mods or {}
if mods[arg.modelName]==nil then
for j,mod in pairs(Models) do
if mod.name==arg.modelName then
mods[arg.modelName]=LoadModel(mod)
break
end
end
end
arg.object=arg.object or AI.Base
arg.model=mods[arg.modelName]
table.insert(AI.objects,arg.object(arg))
--add to collision map if requested
if arg.markAsOccupied then
if type(arg.markAsOccupied)=="boolean" then
AI.map[arg.position.x][arg.position.z]=1
--print(vec2(arg.position.x,arg.position.z))
elseif type(arg.markAsOccupied)=="table" then
for i,c in pairs(arg.markAsOccupied) do
AI.map[c.x][c.y]=1
-- print(vec2(c.x,c.y))
end
end
end
end
---------------------------------------
--class template - all classes must derive from this
---------------------------------------
AI.Base=class()
--must have at least model, position, rotation
function AI.Base:init(arg) --position is vec3 tile based, rotation is vec3
self.model=arg.model
--convert to pixels
self.pos=vec3((arg.position.x-0.5)*AI.tileSize,arg.position.y,-(arg.position.z-0.5)*AI.tileSize)
--store initial rotations as a matrix
if arg.rotation then
self.rotation=arg.rotation
self:GetMatrix()
end
self.speed=arg.speed or 0
self.ambient=arg.ambient or 0
--for _,m in pairs(self.model.m) do m.shader.ambient=self.ambient end
self.ignoreVisibility=false
self:setup(arg) --object specific setup, if you want to do anything extra, create this function
end
function AI.Base:setup(arg)
--put your code in here
end
function AI.Base:SetPosRot(pos,rot)
self.pos=pos
if rot then
self.rotation=rot
self:GetMatrix()
end
end
function AI.Base:draw(playerPos,lightRange)
pushMatrix()
--<---move here
translate(self.pos.x,self.pos.y,self.pos.z)
if self.model.modelmatrix then applyMatrix(self.model.modelmatrix) end
applyMatrix(self.rotationMatrix)
--<---rotate here
for _,m in pairs(self.model.m) do
m.shader.mModel=modelMatrix()
m.shader.pos=playerPos
m.shader.range=lightRange
m:draw()
end
popMatrix()
end
function AI.Base:collide(playerPos,detectionRange)
if self:HasCollided(playerPos) then return true else return false end
end
function AI.Base:Shoot() end --for objects which shoot
function AI.Base:Shot() end --for when player shoots object
function AI.Base:HasCollided(playerPos)
local d=vec2(playerPos.x,playerPos.z):dist(vec2(self.pos.x,self.pos.z))
if d<AI.collisionDistance then return true else return false end
end
function AI.Base:PlayerDetected(playerPos,detectionRange,ignoreVisibility)
if self.pos:dist(playerPos)<detectionRange then
if ignoreVisibility then return true
else
--get fractional tile positions
local selfTile=vec2(self.pos.x,-self.pos.z)/AI.tileSize
local playerTile=vec2(playerPos.x,-playerPos.z)/AI.tileSize
--print(selfTile,playerTile)
local distance=selfTile:dist(playerTile)
--pprint(distance)
if distance==0 then return true end
local difference=playerTile-selfTile
--step through the distance between them, checking if the squares are filled
local stepFraction=math.max(0.01,0.1/distance)
for f=stepFraction,1,stepFraction do
local p=selfTile+difference*f
local x,y=math.floor(p.x+1),math.floor(p.y+1)
if AI.map[x][y]==1 then return false end
end
return true
end
end
end
function AI.Base:Move(playerPos)
local d,f=playerPos:dist(self.pos),0
if d>0 then f=math.min(1,self.speed/d) end
self.pos.x=self.pos.x+f*(playerPos.x-self.pos.x)
self.pos.z=self.pos.z+f*(playerPos.z-self.pos.z)
self:Orient(playerPos)
end
function AI.Base:Orient(playerPos)
self.rotation.y=self:AngleToPlayer(playerPos)
self:GetMatrix()
end
function AI.Base:DrawModel(playerPos,lightRange,rotation)
pushMatrix()
local x,y,z=self.pos.x,self.pos.y,self.pos.z
translate(x,y,z)
if self.model.modelmatrix then applyMatrix(self.model.modelmatrix) end
applyMatrix(self.rotationMatrix)
if rotation then
if rotation.x~=0 then rotate(rotation.x,1,0,0) end
--if rotation.y~=0 then rotate(rotation.y,0,1,0) end
if rotation.z~=0 then rotate(rotation.z,0,0,1) end
if rotation.y~=0 then rotate(rotation.y,0,1,0) end
end
for _,m in pairs(self.model.m) do
m.shader.mModel=modelMatrix() --part of lighting
m.shader.pos=playerPos
m.shader.range=lightRange
m.shader.ambient=self.ambient
m:draw()
end
popMatrix()
end
--TO DO bug when angle is exactly 90, faces wrong way
function AI.Base:AngleToPlayer(playerPos)
local diff=playerPos-self.pos
if diff.x==0 then
if diff.z<0 then return 0 else return 180 end
else
local a=math.deg(math.atan(diff.x/diff.z))
if diff.z>0 then a=a+180 end
return a
end
end
function AI.Base:GetMatrix()
local r=self.rotation
resetMatrix()
pushMatrix()
rotate(r.x,1,0,0) rotate(r.z,0,0,1) rotate(r.y,0,1,0)
local m=modelMatrix()
self.rotationMatrix=matrix(m[1],m[2],m[3],m[4],m[5],m[6],m[7],m[8],m[9],m[10],
m[11],m[12],m[13],m[14],m[15],m[16])
popMatrix()
end
-------------------------------------------
-- Zombie class
-------------------------------------------
AI.Zombie=class(AI.Base)
function AI.Zombie:setup(arg)
self.speed=arg.speed or 0
end
function AI.Zombie:collide(playerPos,detectionRange)
if self:HasCollided(playerPos) then
--TO DO program damage
elseif self:PlayerDetected(playerPos,detectionRange,self.ignoreVisibility) then
if not self.seen then
Fx.Play("brains")
self.seen=true
end
self:Move(playerPos)
end
end
function AI.Zombie:draw(playerPos,lightRange)
self:collide(playerPos,lightRange)
self.pos.y=self.pos.y+0.15*noise(ElapsedTime) --make zombie bounce a little
self:DrawModel(playerPos,lightRange)
end
-------------------------------------------
-- Spider class
-------------------------------------------
AI.Spider=class(AI.Base)
function AI.Spider:setup(arg)
self.speed=arg.speed or 0
self.range=arg.range or 0.5 --of light radius, sight is poor
self.floorY=self.pos.y --store floor height
self.pos.y=AI.wallHeight*0.8 --position in mid air
end
function AI.Spider:collide(playerPos,detectionRange)
if self:HasCollided(playerPos) then
--TO DO program damage
elseif self:PlayerDetected(playerPos,detectionRange,self.ignoreVisibility) then
if not self.seen then
Fx.Play("spider chatter")
--Messages.Add{text="Spider alert!"}
self.seen=true
end
if self.pos.y>self.floorY then
self.pos.y=self.pos.y-0.1
self:Orient(playerPos)
else self:Move(playerPos) end
end
end
function AI.Spider:draw(playerPos,lightRange)
self:collide(playerPos,lightRange)
local rotation=vec3(0,0,9*noise(ElapsedTime))
self.pos.x=self.pos.x+0.05*noise(ElapsedTime)
self.pos.y=self.pos.y+0.05*noise(ElapsedTime/2)
self:DrawModel(playerPos,lightRange,rotation)
end
-------------------------------------------
-- Revolver class
-------------------------------------------
AI.Revolver=class(AI.Base)
function AI.Revolver:setup()
local p=readImage("Tyrian Remastered:Flame 1")
self.flame=image(p.width,p.height*3)
setContext(self.flame)
sprite(p,0,0,p.width,p.height*3)
setContext()
self.pauseBetweenShots=0.5
self.lastShotTime=0
end
function AI.Revolver:draw(playerPos,lightRange)
if not self.collected and self:HasCollided(playerPos) then
self.collected=true
player:Collect(self)
end
AI.Base.draw(self,playerPos,lightRange)
end
function AI.Revolver:Shoot()
if self.lastShotTime and ElapsedTime<self.lastShotTime+self.pauseBetweenShots then return end
self.lastShotTime=ElapsedTime
Messages.Add{text="Bang!",0.5}
end
---------------------------------------
--shared generic functions for use by all objects
---------------------------------------
--# Joystick
JoyStick = class()
--Note all the options you can set below. Pass them through in a named table
function JoyStick:init(t)
t = t or {}
self.radius = t.radius or 100 --size of joystick on screen
self.centre = t.centre or self.radius * vec2(1,1) + vec2(5,15)
end
function JoyStick:draw()
pushStyle()
fill(213, 213, 207, 50)
ellipse(self.centre.x,self.centre.y,2*self.radius)
popStyle()
end
function JoyStick:touched(t)
if t.state~=ENDED then
local v=vec2(t.x,t.y)
if v:dist(self.centre)<=self.radius then
local w=v-self.centre
return vec2(w.x/self.radius,w.y/self.radius)
end
else return nil
end
end
--# Shaders
--Shaders
--this shader used by the walls, floor, roof
TileShader = {
vertexShader = [[
uniform mat4 modelViewProjection;
uniform mat4 mModel;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;
void main()
{
vColor = color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
vPosition = mModel * position; //needed to set light intensity
}
]],
fragmentShader = [[
precision highp float;
uniform lowp sampler2D texture;
uniform lowp float range; //light range
uniform lowp vec3 pos; //position of player
uniform lowp float ambient;
uniform bool hasTexture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition; //position of current pixel
float nonAmbient=1.0-ambient;
void main()
{
float f=ambient+nonAmbient*max(0.,1.0-length( pos - vPosition.xyz ) / range);
//next is the magic line of code that tiles the image across areas of any size
//it is multiplied by the light intensity calculated above
lowp vec4 col = f * vColor;
if (hasTexture) col = f*texture2D( texture, vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));
col.a=1.0;
gl_FragColor =col;
}
]]
}
--this shader is used by the images
--the only difference is that transparent pixels are discarded
TransTileShader = {
vertexShader = [[
uniform mat4 modelViewProjection;
uniform mat4 mModel;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;
void main()
{
vColor = color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
vPosition = mModel * position;
}
]],
fragmentShader = [[
precision highp float;
uniform lowp sampler2D texture;
uniform lowp float range;
uniform lowp float lightbase;
uniform lowp vec3 pos;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;
void main()
{
lowp vec4 col = texture2D( texture, vTexCoord );
if (col.a==0.0) discard; //this is the only difference
else {
//lightbase below is the ambient light of this object, gives it a glow if you want one
float f=max(0.,1.0-length( pos - vPosition.xyz ) / range)+lightbase;
col = col*f;
col.a=1.0;
gl_FragColor=col;
}
}
]]
}
--# Messages
--messages
Messages={}
Messages.text={}
Messages.defaultLength=5
Messages.defaultColour=color(255)
Messages.defaultSize=64
--Messages.defaultPosition=vec2(WIDTH/2, HEIGHT/2)
Messages.defaultFont="Noteworthy-Bold"
function Messages.Add(settings) --messageText,durationInSeconds=Messages.defaultLength,size=64,colour)
Messages.text[#Messages.text+1]={
["message"]=settings.text,
["length"]=settings.length or Messages.defaultLength,
["size"]=settings.size or Messages.defaultSize,
["colour"]=settings.colour or Messages.defaultColour,
["position"]=settings.position or vec2(WIDTH/2, HEIGHT/2), --Messages.defaultPosition,
["font"]=settings.font or Messages.defaultFont
}
end
function Messages.draw()
if #Messages.text==0 then return end
m=Messages.text[1]
if not m.endTime then
m.endTime=ElapsedTime+m.length
elseif ElapsedTime>m.endTime then
table.remove(Messages.text,1)
return
end
Messages.WriteMessage(m)
end
function Messages.WriteMessage(m)
pushStyle()
font(m.font)
textWrapWidth(WIDTH*.75)
fontSize(m.size)
m.colour.a=(m.endTime-ElapsedTime)/m.length*255
fill(m.colour)
textMode(CENTER)
--text(m.message,m.position.x,m.position.y)
text(m.message,WIDTH/2,HEIGHT/2)
popStyle()
end
--# Test
--Test
--[[
displayMode(STANDARD)
function setup()
parameter.integer("n",0,1000,0)
end
--]
--[
function draw()
background(0)
if tt then n=n+1 end
end
function touched(t)
if t.state==BEGAN then tt=true
elseif t.state==ENDED then
tt=nil
n=0
return
end
end
--]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment