package { public class WorkerAIController implements IObjectController { private static const DESTINATION_THRESHOLD:Number = 0.125; private static const IDLE_MIN:int = 60 * 1; private static const IDLE_MAX:int = 60 * 5; private static const LOS_UPDATE_INTERVAL:int = 10; private static const TASK_ARRIVE:int = 0; private static const TASK_MORNING_WORK:int = 1; private static const TASK_AFTERNOON_WORK:int = 2; private static const TASK_REFRIGERATE_LUNCH:int = 3; private static const TASK_RETRIEVE_LUNCH:int = 4; private static const TASK_DRINK_COFFEE:int = 5; private static const TASK_USE_RESTROOM:int = 6; private static const TASK_CALL_MAINTENANCE:int = 7; private static const TASK_LEAVE:int = 8; public var worker:WorkerModel; private var gameStateModel:GameStateModel; private var pathToDestination:Vector.; private var idleTicks:int; private var nextLOSUpdate:int; private var stashedSandwich:Boolean; private var finishedLunch:Boolean; private var drankCoffee:Boolean; private var enteredRestroom:Boolean; private var currentTask:int; private var ticksLeftOnTask:int; private var afternoon:Boolean; private static var losDelay:int = 0; private var pathfindingFailedLastUpdate:Boolean; public function WorkerAIController(worker:WorkerModel, gameStateModel:GameStateModel) { this.worker = worker; this.gameStateModel = gameStateModel; pathToDestination = null; idleTicks = IDLE_MIN + Math.random() * (IDLE_MAX - IDLE_MIN); nextLOSUpdate = losDelay++ % LOS_UPDATE_INTERVAL + 1; currentTask = TASK_ARRIVE; ticksLeftOnTask = Math.random() * 60 * 2; stashedSandwich = false; finishedLunch = false; afternoon = false; } private function bestPathToDestination(destination:Vector2):Vector. { var openNodes:Vector., visitedNodes:Vector.; var origin:Vector2 = new Vector2(Math.round(worker.position.x), Math.round(worker.position.z)); const neighborOffsets:Vector. = new [{x: -1, z: 0}, {x: 1, z: 0}, {x: 0, z: -1}, {x: 0, z: 1}]; var neighborPosition:Vector2; var visited:Boolean, open:Boolean; var currentNode:Object, newNode:Object; if (origin.x == destination.x && origin.z == destination.z) { return new Vector.(); } openNodes = new Vector.(); openNodes.push({position: origin.clone(), previous: null, distanceToGo: Math.abs(destination.x - origin.x) + Math.abs(destination.z - origin.z)}); visitedNodes = new Vector.(); while (openNodes.length > 0) { currentNode = openNodes.shift(); if (currentNode.position.x == destination.x && currentNode.position.z == destination.z) { var result:Vector. = new Vector.(); var reverseNodeIndexMap:Vector. = new Vector.(gameStateModel.level.sizeX * gameStateModel.level.sizeZ); for (var visitedNodeIndex:int = 0; visitedNodeIndex < visitedNodes.length; visitedNodeIndex++) { reverseNodeIndexMap[visitedNodes[visitedNodeIndex].position.z * gameStateModel.level.sizeX + visitedNodes[visitedNodeIndex].position.x] = visitedNodeIndex; } do { result.unshift(currentNode.position); currentNode = visitedNodes[reverseNodeIndexMap[currentNode.previous.z * gameStateModel.level.sizeX + currentNode.previous.x]]; } while (currentNode.previous != null); return result; } visitedNodes.push(currentNode); for each (var neighborOffset:Object in neighborOffsets) { var nodeIndex:int; neighborPosition = new Vector2(currentNode.position.x + neighborOffset.x, currentNode.position.z + neighborOffset.z); if (neighborPosition.x < 0 || neighborPosition.x >= gameStateModel.level.sizeX || neighborPosition.z < 0 || neighborPosition.z >= gameStateModel.level.sizeZ) { continue; } if (gameStateModel.isSolidTileAtPosition(neighborPosition.x, neighborPosition.z)) { continue; } visited = false; for each (var visitedNode:Object in visitedNodes) { if (visitedNode.position.x == neighborPosition.x && visitedNode.position.z == neighborPosition.z) { visited = true; break; } } if (visited) {continue;} open = false; for each (var openNode:Object in openNodes) { if (openNode.position.x == neighborPosition.x && openNode.position.z == neighborPosition.z) { open = true; break; } } if (open) {continue;} newNode = {position: neighborPosition, previous: currentNode.position, distanceToGo: Math.abs(destination.x - neighborPosition.x) + Math.abs(destination.z - neighborPosition.z)}; for (nodeIndex = 0; nodeIndex < openNodes.length; nodeIndex++) { if (newNode.distanceToGo < openNodes[nodeIndex].distanceToGo) { break; } } openNodes.splice(nodeIndex, 0, newNode); } } return null; } private static function linesIntersect(p0:Vector2, p1:Vector2, p2:Vector2, p3:Vector2):Boolean { return (p1.subtract(p0).cross(p2.subtract(p0)) > 0) != (p1.subtract(p0).cross(p3.subtract(p0)) > 0) && (p3.subtract(p2).cross(p0.subtract(p2)) > 0) != (p3.subtract(p2).cross(p1.subtract(p2)) > 0); } private static function lineIntersectsTile(p0:Vector2, p1:Vector2, tileX:int, tileZ:int):Boolean { return linesIntersect(p0, p1, new Vector2(tileX - 0.5, tileZ - 0.5), new Vector2(tileX + 0.5, tileZ - 0.5)) || linesIntersect(p0, p1, new Vector2(tileX - 0.5, tileZ - 0.5), new Vector2(tileX - 0.5, tileZ + 0.5)) || linesIntersect(p0, p1, new Vector2(tileX - 0.5, tileZ + 0.5), new Vector2(tileX + 0.5, tileZ + 0.5)) || linesIntersect(p0, p1, new Vector2(tileX + 0.5, tileZ - 0.5), new Vector2(tileX + 0.5, tileZ + 0.5)); } private function updateWorkersInLineOfSight():void { var spottedWorker:WorkerModel; var tileMinX:int, tileMinZ:int, tileMaxX:int, tileMaxZ:int; worker.workerIDsInLineOfSight.splice(0, worker.workerIDsInLineOfSight.length); for each (var object:GameObjectModel in gameStateModel.objects) { if (object is WorkerModel && object != worker) { var occluded:Boolean = false; spottedWorker = object as WorkerModel; if (worker.facing == Direction.NORTH && spottedWorker.position.z > worker.position.z || worker.facing == Direction.EAST && spottedWorker.position.x < worker.position.x || worker.facing == Direction.SOUTH && spottedWorker.position.z < worker.position.z || worker.facing == Direction.WEST && spottedWorker.position.x > worker.position.x) { continue; } tileMinX = Math.round(Math.min(worker.position.x, spottedWorker.position.x)); tileMinZ = Math.round(Math.min(worker.position.z, spottedWorker.position.z)); tileMaxX = Math.round(Math.max(worker.position.x, spottedWorker.position.x)); tileMaxZ = Math.round(Math.max(worker.position.z, spottedWorker.position.z)); for (var tileIndexZ:int = tileMinZ; tileIndexZ <= tileMaxZ && !occluded; tileIndexZ++) { for (var tileIndexX:int = tileMinX; tileIndexX <= tileMaxX && !occluded; tileIndexX++) { if (gameStateModel.isSolidTileAtPosition(tileIndexX, tileIndexZ) && lineIntersectsTile(worker.position, spottedWorker.position, tileIndexX, tileIndexZ)) { occluded = true; } } } if (!occluded) { worker.workerIDsInLineOfSight.push(spottedWorker.workerID); } } } } public function update():void { var tileX:int, tileZ:int; var reachedDestination:Boolean = false; if (pathToDestination != null && pathToDestination.length > 0) { var atNodeX:Boolean, atNodeZ:Boolean; atNodeX = true; if (worker.position.x < pathToDestination[0].x - DESTINATION_THRESHOLD) { atNodeX = false; worker.startMoving(Direction.EAST); } else { worker.stopMoving(Direction.EAST); } if (worker.position.x > pathToDestination[0].x + DESTINATION_THRESHOLD) { atNodeX = false; worker.startMoving(Direction.WEST); } else { worker.stopMoving(Direction.WEST); } atNodeZ = true; if (worker.position.z < pathToDestination[0].z - DESTINATION_THRESHOLD) { atNodeZ = false; worker.startMoving(Direction.SOUTH); } else { worker.stopMoving(Direction.SOUTH); } if (worker.position.z > pathToDestination[0].z + DESTINATION_THRESHOLD) { atNodeZ = false; worker.startMoving(Direction.NORTH); } else { worker.stopMoving(Direction.NORTH); } if (atNodeX && atNodeZ) { pathToDestination.shift(); if (pathToDestination.length == 0) { reachedDestination = true; } } } if (pathToDestination == null || pathToDestination.length == 0) { if (reachedDestination || pathfindingFailedLastUpdate) { switch (currentTask) { case TASK_REFRIGERATE_LUNCH: if (stashedSandwich) { break; } var fridge:FridgeModel = gameStateModel.tiles[gameStateModel.fridgePosition.z * gameStateModel.level.sizeX + gameStateModel.fridgePosition.x] as FridgeModel; worker.hasSandwich = false; fridge.hasSandwich = true; stashedSandwich = true; break; case TASK_DRINK_COFFEE: if (drankCoffee) { break; } var coffeePot:CoffeePotModel = gameStateModel.doodads[gameStateModel.coffeePotPosition.z * gameStateModel.level.sizeX + gameStateModel.coffeePotPosition.x] as CoffeePotModel; if (coffeePot.fullnessLevel > 0) { coffeePot.fullnessLevel--; if (coffeePot.fullnessLevel == 0) { coffeePot.fullnessLevel = CoffeePotModel.FULLNESS_MAX; } } else { worker.showEmote(WorkerModel.EMOTE_CRY); gameStateModel.player.showEmote(WorkerModel.EMOTE_DEVIL); coffeePot.fullnessLevel = CoffeePotModel.FULLNESS_MAX; } Embeds.soundForType(SoundModel.SOUND_CoffeePour).play(); drankCoffee = true; break; case TASK_RETRIEVE_LUNCH: if (finishedLunch) { break; } fridge = gameStateModel.tiles[gameStateModel.fridgePosition.z * gameStateModel.level.sizeX + gameStateModel.fridgePosition.x] as FridgeModel; if (!fridge.hasSandwich) { worker.showEmote(WorkerModel.EMOTE_CRY); gameStateModel.player.showEmote(WorkerModel.EMOTE_DEVIL); } fridge.hasSandwich = false; finishedLunch = true; break; case TASK_USE_RESTROOM: if (enteredRestroom) { break; } worker.inVisibleOffice = false; ticksLeftOnTask = Math.random() * 120 + 240; enteredRestroom = true; break; case TASK_LEAVE: if (worker.inVisibleOffice) { Embeds.soundForType(SoundModel.SOUND_ElevatorDing).play(); } worker.inVisibleOffice = false; break; } } pathfindingFailedLastUpdate = false; ticksLeftOnTask--; if (ticksLeftOnTask <= 0) { switch (currentTask) { case TASK_ARRIVE: worker.inVisibleOffice = true; Embeds.soundForType(SoundModel.SOUND_ElevatorDing).play(); if (worker.hasSandwich) { currentTask = TASK_REFRIGERATE_LUNCH; } else { currentTask = TASK_MORNING_WORK; } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_REFRIGERATE_LUNCH: if (stashedSandwich) { currentTask = TASK_MORNING_WORK; } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_MORNING_WORK: if (gameStateModel.timeOfDay > 0.4) { // After 12 PM if (stashedSandwich) { currentTask = TASK_RETRIEVE_LUNCH; } else { afternoon = true; currentTask = TASK_AFTERNOON_WORK; } } else { randomForTask = Math.random(); if (randomForTask < 0.2) { drankCoffee = false; currentTask = TASK_DRINK_COFFEE; } else if (randomForTask < 0.25) { enteredRestroom = false; currentTask = TASK_USE_RESTROOM; } } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_RETRIEVE_LUNCH: if (finishedLunch) { afternoon = true; currentTask = TASK_AFTERNOON_WORK; } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_AFTERNOON_WORK: var randomForTask:Number; if (gameStateModel.timeOfDay > 0.9) { // After 5 PM currentTask = TASK_LEAVE; } else { randomForTask = Math.random(); if (randomForTask < 0.2) { drankCoffee = false; currentTask = TASK_DRINK_COFFEE; } else if (randomForTask < 0.25) { enteredRestroom = false; currentTask = TASK_USE_RESTROOM; } } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_DRINK_COFFEE: if (drankCoffee) { currentTask = afternoon ? TASK_AFTERNOON_WORK : TASK_MORNING_WORK; } ticksLeftOnTask = Math.random() * 60 + 60; break; case TASK_USE_RESTROOM: if (enteredRestroom) { worker.inVisibleOffice = true; currentTask = afternoon ? TASK_AFTERNOON_WORK : TASK_MORNING_WORK;; } ticksLeftOnTask = Math.random() * 60 + 60; break; } } idleTicks--; if (idleTicks <= 0) { var destination:Vector2; var destinationCandidates:Vector.; switch (currentTask) { case TASK_MORNING_WORK: case TASK_AFTERNOON_WORK: destinationCandidates = gameStateModel.validOfficeTilesForWorker(worker.workerID); destination = destinationCandidates[int(Math.random() * destinationCandidates.length)]; break; case TASK_REFRIGERATE_LUNCH: case TASK_RETRIEVE_LUNCH: destination = gameStateModel.fridgePosition.clone(); switch (gameStateModel.tileTypeAtPosition(destination.x, destination.z)) { case TileModel.TILE_FridgeE: destination.x++; break; case TileModel.TILE_FridgeS: destination.z++; break; } break; case TASK_DRINK_COFFEE: destination = gameStateModel.coffeePotPosition.clone(); switch (gameStateModel.doodadTypeAtPosition(destination.x, destination.z)) { case TileModel.TILE_CoffeemakerE: destination.x++; break; case TileModel.TILE_CoffeemakerS: destination.z++; break; } break; case TASK_USE_RESTROOM: switch (worker.gender) { case Gender.FEMALE: destination = gameStateModel.womensRoomPosition.clone(); break; case Gender.MALE: destination = gameStateModel.mensRoomPosition.clone(); break; } destination.x++; break; case TASK_CALL_MAINTENANCE: case TASK_LEAVE: destinationCandidates = gameStateModel.elevatorPositions; destination = destinationCandidates[int(Math.random() * destinationCandidates.length)].clone(); destination.z++; break; } if (destination != null) { pathToDestination = bestPathToDestination(destination); if (pathToDestination == null || pathToDestination.length == 0) { pathfindingFailedLastUpdate = true; } } idleTicks = IDLE_MIN + Math.random() * (IDLE_MAX - IDLE_MIN); } } nextLOSUpdate--; if (nextLOSUpdate <= 0) { updateWorkersInLineOfSight(); } tileX = Math.round(worker.position.x); tileZ = Math.round(worker.position.z); if (tileX >= 0 && tileX < gameStateModel.level.sizeX && tileZ >= 0 && tileZ < gameStateModel.level.sizeZ) { worker.ownerOfOccupiedTile = gameStateModel.level.officeOwners[tileZ * gameStateModel.level.sizeX + tileX]; } } } }