structure EnemyBehaviour = struct open GameType fun canWalkAhead (x, y, wallTree, platformTree) = let val ww = Constants.worldWidth val wh = Constants.worldHeight val searchWidth = Constants.enemySize val y = y + Constants.enemySize - 5 val searchHeight = 10 in QuadTree.hasCollisionAt (x, y, searchWidth, searchHeight, 0, 0, ww, wh, ~1, wallTree) orelse QuadTree.hasCollisionAt (x, y, searchWidth, searchHeight, 0, 0, ww, wh, ~1, platformTree) end (* same function takes either wallTree or platformTree and returns true * if standing on tree. * Function is monomorphic in the sense that wallTree and platformTree * are both same type (no generics/parametric polymorphism). * *) fun standingOnArea (enemy, tree) = let val {x = ex, y = ey, ...} = enemy val ey = ey + Constants.enemySize - 1 val width = Constants.enemySize val height = 2 val ww = Constants.worldWidth val wh = Constants.worldHeight in QuadTree.hasCollisionAt (ex, ey, width, height, 0, 0, ww, wh, ~1, tree) end fun getPatrollPatches (enemy: enemy, wallTree, platformTree, acc) = let (* This function is meant to check * if enemy should switch the horizontal direction * if the enemy is patrolling. * * Algorithm: * 1. Check if enemy there is a wall ahead of the enemy * in the direction the enemy is walking. * 1.1. If there is a wall, then invert the direction. * * 2. If there is no wall, check if there is space to * walk ahead on, such that enemy will not fall * if enemy continues to walk. * 2.1. If continuing to walk will cause the enemy to fall, * then invert the direction. * * 3. Else, do not invert direction and simply return given list. * *) val {x, y, xAxis, ...} = enemy in case xAxis of MOVE_LEFT => let (* search to see if there is wall on left side *) val searchStartX = x - Constants.moveEnemyBy val searchWidth = Constants.enemySize val searchHeight = Constants.enemySize - 5 val ww = Constants.worldWidth val wh = Constants.worldHeight val hasWallAhead = QuadTree.hasCollisionAt ( searchStartX , y , searchWidth , searchHeight , 0 , 0 , ww , wh , ~1 , wallTree ) in if hasWallAhead then EnemyPatch.W_X_AXIS MOVE_RIGHT :: acc else (* invert direction if moving further left * will result in falling down *) if canWalkAhead (searchStartX, y, wallTree, platformTree) then acc else EnemyPatch.W_X_AXIS MOVE_RIGHT :: acc end | MOVE_RIGHT => let (* enemy's x field is top left coordinate * but we want to check top * right coordinate, * so add enemySize *) val searchStartX = x + Constants.enemySize + Constants.moveEnemyBy val searchWidth = Constants.enemySize val searchHeight = Constants.enemySize - 5 val ww = Constants.worldWidth val wh = Constants.worldHeight val hasWallAhead = QuadTree.hasCollisionAt ( searchStartX , y , searchWidth , searchHeight , 0 , 0 , ww , wh , ~1 , wallTree ) in if hasWallAhead then EnemyPatch.W_X_AXIS MOVE_LEFT :: acc else (* invert direction if moving further right * will result in falling down *) if canWalkAhead (searchStartX, y, wallTree, platformTree) then acc else EnemyPatch.W_X_AXIS MOVE_LEFT :: acc end | STAY_STILL => acc end (* new pathfinding using quad tree *) fun helpHasVisited (pos, find, visited) = if pos = Vector.length visited then false else let val cur = Vector.sub (visited, pos) in cur = find orelse helpHasVisited (pos + 1, find, visited) end fun hasVisted (find, visited) = helpHasVisited (0, Char.chr find, visited) fun isBetween (p1, check, p2) = check >= p1 andalso check <= p2 fun isReachableFromBelow (prevPlat: platform, currentPlat: platform) = let val {x = prevX, y = prevY, width = prevWidth, ...} = prevPlat val {x = curX, y = curY, width = curWidth, ...} = currentPlat val prevFinishX = prevX + prevWidth val curFinishX = curX + curWidth in (isBetween (prevX, curX, prevFinishX) orelse isBetween (prevX, curFinishX, prevFinishX) andalso prevY + Constants.jumpLimit >= curY) end fun isReachableFromAbove (prevPlat: platform, currentPlat: platform) = let val {x = prevX, y = prevY, width = prevWidth, ...} = prevPlat val {x = curX, y = curY, width = curWidth, ...} = currentPlat val prevFinishX = prevX + prevWidth val curFinishX = curX + curWidth in (isBetween (prevX, curX, prevFinishX) orelse isBetween (prevX, curFinishX, prevFinishX) andalso prevY <= curY) end fun isReachableFromLeft (prevPlat, currentPlat) = (* prev = right/from, current = left/to *) let val {x = prevX, y = prevY, width = prevWidth, ...} = prevPlat val {x = curX, y = curY, width = curWidth, ...} = currentPlat val enemyX = prevX val xDiff = prevX - curX in if xDiff <= Constants.jumpLimit then true else let val enemyApexX = enemyX - Constants.jumpLimit val enemyApexY = prevY + Constants.jumpLimit val curFinishX = curX + curWidth val diffApexY = enemyApexY - curY val diffApexX = enemyApexX - curFinishX in diffApexX <= diffApexY orelse diffApexY <= 0 end end fun isReachableFromRight (prevPlat, currentPlat) = (* prev = left/from, current = right/to *) let val {x = prevX, y = prevY, width = prevWidth, ...} = prevPlat val {x = curX, y = curY, width = curWidth, ...} = currentPlat (* last x coordinate where enemy can fully fit on prevPlat *) val enemyX = prevX + prevWidth - Constants.enemySize val xDiff = curX - prevX in if xDiff <= Constants.jumpLimit then (* platform is possible to jump to without falling *) true else let val enemyApexX = enemyX + Constants.jumpLimit val enemyApexY = prevY + Constants.jumpLimit val diffApexY = enemyApexY - curY val diffApexX = enemyApexX - curX in diffApexY <= 0 orelse diffApexX <= diffApexY end end fun getLeftwardsPath (playerPlatID, currentPlatID, platforms, platformTree, dist, visited) = if playerPlatID = currentPlatID then (dist, [currentPlatID]) else let val chr = Char.chr currentPlatID val visited = Vector.concat [Vector.fromList [chr], visited] val currentPlat = Platform.find (currentPlatID, platforms) val {x, y, width, ...} = currentPlat (* include all platforms we can jump leftwards to, * whether above or below. * *) val searchY = y - Constants.jumpLimit val searchH = Constants.worldHeight - searchY val searchX = 0 val searchW = x val ww = Constants.worldWidth val wh = Constants.worldHeight val leftList = QuadTree.getCollisions (searchX, searchY, searchW, searchH, 0, 0, ww, wh, ~1, platformTree) val (bestDist, bestPath) = helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , leftList , dist , currentPlat , ~1 , [] , visited ) in if bestDist = ~1 then (* invalid *) (~1, []) else (bestDist, currentPlatID :: bestPath) end and helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , lst , dist , prevPlat , bestDist , bestPath , visited ) = case lst of id :: tl => if hasVisted (id, visited) then helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else let val currentPlat = Platform.find (id, platforms) in if isReachableFromLeft (prevPlat, currentPlat) then (* is reachable, so reach *) let (* considering horizontal distance only. * todo: can consider diagonal/vertical distance too * (by pythagoras) * also todo: lFinishX might be to the right of rx * if currentPlat intersects with prevPlat in the y axis * in which case the distance calculated is wrong. * *) val {x = lx, width = lWidth, ...} = currentPlat val {x = rx, width = rWidth, ...} = prevPlat val lFinishX = lx + lWidth val diff = rx - lFinishX val platDist = dist + diff val (newDist, newPath) = getLeftwardsPath (playerPlatID, id, platforms, platformTree, platDist, visited) in if newDist = ~1 then (* newPath is invalid, so reuse old path *) helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else if bestDist = ~1 then (* bestPath is invalid *) helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else if newDist < bestDist then helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) end else (* ignore node and filter out if we cannot reach *) helpGetLeftwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) end | [] => (bestDist, bestPath) fun getRightwardsPath (playerPlatID, currentPlatID, platforms, platformTree, dist, visited) = if playerPlatID = currentPlatID then (dist, [currentPlatID]) else let val chr = Char.chr currentPlatID val visited = Vector.concat [Vector.fromList [chr], visited] val currentPlat = Platform.find (currentPlatID, platforms) val {x, y, width, ...} = currentPlat (* include all platforms we can jump rightwards to, * whether above or below. * Note: Collision list may contain platforms which we can't jump to * as player will eventually fall from jump, * and our query to the QuadTree is a simple rectangular box * which is not the correct shape to model diagonal descent. * Thus, we perform additional filtering in the collision list * to see if the platform is reachable. * *) val searchY = y - Constants.jumpLimit val searchH = Constants.worldHeight - searchY val searchX = x + width val searchW = Constants.worldWidth - searchX val ww = Constants.worldWidth val wh = Constants.worldHeight val rightList = QuadTree.getCollisions (searchX, searchY, searchW, searchH, 0, 0, ww, wh, ~1, platformTree) val (bestDist, bestPath) = helpGetRightwardsPath ( playerPlatID , platforms , platformTree , rightList , dist , currentPlat , ~1 , [] , visited ) in if bestDist = ~1 then (* invalid *) (~1, []) else (bestDist, currentPlatID :: bestPath) end and helpGetRightwardsPath ( playerPlatID , platforms , platformTree , lst , dist , prevPlat , bestDist , bestPath , visited ) = case lst of id :: tl => if hasVisted (id, visited) then helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else let val currentPlat = Platform.find (id, platforms) in if isReachableFromRight (prevPlat, currentPlat) then (* is reachable, so reach *) let (* considering horizontal distance only. * todo: can consider diagonal/vertical distance too * (by pythagoras) *) val {x = cx, ...} = currentPlat val {x = px, ...} = prevPlat val diff = cx - px val platDist = dist + diff val (newDist, newPath) = getRightwardsPath (playerPlatID, id, platforms, platformTree, platDist, visited) in if newDist = ~1 then (* newPath is invalid, so reuse old path *) helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else if bestDist = ~1 then (* bestPath is invalid *) helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else if newDist < bestDist then helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) end else (* ignore node and filter out if we cannot reach *) helpGetRightwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) end | [] => (bestDist, bestPath) fun getUpwardsPath (playerPlatID, currentPlatID, platforms, platformTree, dist, visited) = if playerPlatID = currentPlatID then (dist, [currentPlatID]) else let (* add current node to list of visited nodes *) val chr = Char.chr currentPlatID val visited = Vector.concat [Vector.fromList [chr], visited] val currentPlat = Platform.find (currentPlatID, platforms) val {x, y, width, ...} = currentPlat (* search for platforms directly above current one *) val searchY = y - Constants.jumpLimit val searchH = Constants.jumpLimit (* todo: x/width are placeholder values. * They should define values that let reachable platforms * on the top left/top right be included in the collision list * but they currentl are not. *) val searchX = x val searchW = width val ww = Constants.worldWidth val wh = Constants.worldHeight val upList = QuadTree.getCollisions (searchX, searchY, searchW, searchH, 0, 0, ww, wh, ~1, platformTree) val (bestDist, bestPath) = helpGetUpwardsPath ( playerPlatID , platforms , platformTree , upList , dist , currentPlat , ~1 , [] , visited ) in if bestDist = ~1 then (* invalid; return error value *) (~1, []) else (* is valid, so cons currentPlatID to path *) (bestDist, currentPlatID :: bestPath) end and helpGetUpwardsPath ( playerPlatID , platforms , platformTree , lst , dist , prevPlat , bestDist , bestPath , visited ) = case lst of id :: tl => if hasVisted (id, visited) then helpGetUpwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else let val currentPlat = Platform.find (id, platforms) (* considering vertical distance only. * This is okay because the scan box is a simple square * directly above prev platform which does not care about * top right or top left). *) val {y = cy, ...} = currentPlat val {y = py, ...} = prevPlat val diff = py - cy val platDist = dist + diff val (newDist, newPath) = getUpwardsPath (playerPlatID, id, platforms, platformTree, platDist, visited) in if newDist = ~1 then (* newPath is invalid, so reuse old path *) helpGetUpwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) else if bestDist = ~1 then (* bestPath is invalid *) helpGetUpwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else if newDist < bestDist then helpGetUpwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , newDist , newPath , visited ) else helpGetUpwardsPath ( playerPlatID , platforms , platformTree , tl , dist , prevPlat , bestDist , bestPath , visited ) end | [] => (bestDist, bestPath) (* pathfinding *) fun getHighestPlatform (collisions, platforms, highestY, highestID) = case collisions of id :: tl => let val {y = platY, ...} = Platform.find (id, platforms) in (* platY < highestY is correct because lowest number = highest * in * this case *) if platY < highestY then getHighestPlatform (tl, platforms, platY, id) else getHighestPlatform (tl, platforms, highestY, highestID) end | [] => highestID fun getPlatformBelowPlayer (player, platformTree, platforms) = let val {x, y, ...} = player val searchWidth = Constants.playerSize val searchHeight = Constants.worldHeight - y val ww = Constants.worldWidth val wh = Constants.worldHeight val collisions = QuadTree.getCollisions (x, y, searchWidth, searchHeight, 0, 0, ww, wh, ~1, platformTree) in getHighestPlatform (collisions, platforms, wh, ~1) end fun getPlatformBelowEnemy (enemy: enemy, platformTree, platforms) = let val {x, y, ...} = enemy val searchWidth = Constants.enemySize val searchHeight = Constants.worldHeight - y val ww = Constants.worldWidth val wh = Constants.worldHeight val collisions = QuadTree.getCollisions (x, y, searchWidth, searchHeight, 0, 0, ww, wh, ~1, platformTree) in getHighestPlatform (collisions, platforms, wh, ~1) end fun canJumpOnPID (collisions, pID) = case collisions of id :: tl => (id = pID) orelse canJumpOnPID (tl, pID) | [] => false fun canJumpOnPlatform (player, platforms, enemy: enemy, platformTree) = let val pID = getPlatformBelowPlayer (player, platformTree, platforms) val {x, y, ...} = enemy val eID = getPlatformBelowEnemy (enemy, platformTree, platforms) val (bestDist, bestPath) = if eID > ~1 andalso pID > ~1 then getUpwardsPath (pID, eID, platforms, platformTree, 0, "") else (~1, []) val _ = if bestDist = ~1 then print "no path\n" else let val _ = print "has path:\n" val _ = List.map (fn platID => print ("best path platID: " ^ Int.toString platID ^ "\n")) bestPath in () end val distance = Constants.moveEnemyBy * Constants.jumpLimit val distance = distance div 2 val yDistance = distance val y = y - yDistance + Constants.enemySize val ww = Constants.worldWidth val wh = Constants.worldHeight val mx = x - distance val rightCollisions = QuadTree.getCollisions (x, y, distance, yDistance, 0, 0, ww, wh, ~1, platformTree) val leftCollisions = QuadTree.getCollisions (mx, y, distance, yDistance, 0, 0, ww, wh, ~1, platformTree) in canJumpOnPID (rightCollisions, pID) orelse canJumpOnPID (leftCollisions, pID) end fun getFollowPatches (player: player, enemy, wallTree, platformTree, platforms, acc) = let val {x = px, y = py, ...} = player val {x = ex, y = ey, yAxis = eyAxis, ...} = enemy val xAxis = if px < ex then MOVE_LEFT else MOVE_RIGHT val isOnWall = standingOnArea (enemy, wallTree) val isOnPlatform = standingOnArea (enemy, platformTree) val hasPlatformAbove = canJumpOnPlatform (player, platforms, enemy, platformTree) val shouldJump = (isOnWall orelse isOnPlatform) andalso hasPlatformAbove val yAxis = if ey > py andalso shouldJump then case eyAxis of ON_GROUND => JUMPING 0 | FALLING => JUMPING 0 | _ => eyAxis else eyAxis in EnemyPatch.W_X_AXIS STAY_STILL :: acc end fun getVariantPatches (enemy, walls, wallTree, platforms, platformTree, player, acc) = let open EnemyVariants in case #variant enemy of PATROL_SLIME => getPatrollPatches (enemy, wallTree, platformTree, acc) | FOLLOW_SIME => getFollowPatches (player, enemy, wallTree, platformTree, platforms, acc) end end