// // IMapCharacterController.cpp // WattsenglishFoodApp // // Created by Katarzyna Kalinowska-Górska on 22/03/2020. // #include "IMapCharacterController.h" #include "IMapCharacter.h" #include "AniMapUtils.h" #include "AniGeometryUtils.h" #include "AniScalingUtils.h" #include "AniTutorialMapControl.h" // TODO all these, or almost all of these, should go in a subclass basic/move/default MapCharactercontroller IMapCharacterController::~IMapCharacterController(){ detachFromCharacter(); } void IMapCharacterController::attachToCharacter(IMapCharacter* p_mapCharacter){ detachFromCharacter(); m_mapCharacter = p_mapCharacter; m_mapCharacter->setController(this); } void IMapCharacterController::detachFromCharacter(){ if(m_mapCharacter != nullptr){ m_mapCharacter->clearController(); m_mapCharacter = nullptr; } } void IMapCharacterController::approachAdventureObject(IMapAdventureObject* p_advObject, AniMapUtils::TileData tile){ // cocos2d::log("approach ad!"); if(m_characterDisabled){ return; } if(p_advObject == nullptr){ return; } if(m_approachedAdventureObject == p_advObject){ // cocos2d::log("returning though\n"); return; } m_approachedAdventureObject = p_advObject; auto startTile = getCurrentCharacterTile(); auto potentialDestTiles = p_advObject->getEntryTiles(); //find the closer entry point auto path = AniMapUtils::getInstance().calculatePathToClosestFromTileSet(startTile, potentialDestTiles, m_mapController->getMap(), m_mapController->getTileLayer(), true); // and move the character there moveCharacterStartOnPath(path); } AniMapUtils::TileData IMapCharacterController::getCurrentCharacterTile(){ auto map = m_mapController->getMap(); return AniMapUtils::getInstance().translateXYPointToColRow(m_mapCharacter->getPosition(), map->getTileSize().width, map->getTileSize().height,map->getMapSize().width, map->getMapSize().height); } void IMapCharacterController::onEnter(){ updateLastUserActivityTime(); } void IMapCharacterController::update(float dt){ if(m_characterDisabled){ return; } auto characterState = m_mapCharacter->getCharacterState(); if(characterState == IMapCharacter::JUMPING || characterState == IMapCharacter::FALLING || characterState == IMapCharacter::SLIDING){ m_mapController->centerCameraOnNode(m_mapCharacter); adjustCharacterDepth(); instantTilesReveal(); return; } if(m_mapCharacter->isBusy()){ return; } auto AniMapUtils = AniMapUtils::getInstance(); auto map = m_mapController->getMap(); bool isSteveMoving = m_mapCharacter->isInFingerMoveState(); bool doneMovingHorizontally = true, doneMovingVertically = true; if(isSteveMoving){ updatePhaseMoveCharacter(dt, doneMovingHorizontally, doneMovingVertically); m_mapController->centerCameraOnNode(m_mapCharacter); } if(m_mapCharacter->getCharacterState() != IMapCharacter::IDLE){ adjustCharacterDepth(); } if(isSteveMoving && doneMovingHorizontally && doneMovingVertically){ updatePhaseCheckCharacterStopped(dt); } // the character might become busy if started interaction with an adv object if(m_currentScreenTouchPoint.x != -1 && !m_mapCharacter->isBusy()){ //TODO add blocking!!!!! when e.g. waiting for steve to approach an object like a slide auto touchLocationOnMap = AniMapUtils.translateScreenPositionToMapPosition(m_currentScreenTouchPoint, m_mapController->getCameraPosition()); auto rowCol = AniMapUtils.translateXYPointToColRow(touchLocationOnMap, map->getTileSize().width, map->getTileSize().height,map->getMapSize().width, map->getMapSize().height); auto callback = [&](){ }; // cocos2d::log("update, touched tile: %d x %d\n", rowCol.col,rowCol.row); // if(AniMapUtils::getInstance().isTileAccessible(m_mapController->getMap(), m_mapController->getTileLayer(), rowCol.col, rowCol.row)){ if(m_mapCharacter->isInFingerMoveState() && m_approachedAdventureObject == nullptr){//todo check if this does not generate bugs if(m_currentlyScheduledPath.size() == 0 || currentDestination() != rowCol){ //TODO dedup auto advObjectsSet = m_mapController->getAdventureObjectsAtTile(rowCol); if(advObjectsSet.size() > 0){ auto advObject = pickAdventureObject(advObjectsSet); approachAdventureObject(advObject, rowCol); } else { // cocos2d::log("THIS IS HAPPENING, tile: %d %d\n", rowCol.col, rowCol.row); moveCharacterToTile(rowCol, callback); } } // else { // moveCharacterToTile(rowCol, callback); // } } } if(!m_mapCharacter->isBusy() && !m_mapCharacter->isInFingerMoveState()){ auto now = cocos2d::utils::getTimeInMilliseconds(); if((now - m_lastUserActivityTime)/1000.0 > m_mapCharacter->getTimeToGetBored()){ if(m_lastUserActivityTime > 0){ //if -1, it means init state - don't trigger character bored m_mapCharacter->setBored(); } updateLastUserActivityTime(); } } } void IMapCharacterController::updatePhaseMoveCharacter(float p_dt, bool& doneMovingHorizontally, bool& doneMovingVertically){ auto characterPosition = m_mapCharacter->getPosition(); doneMovingHorizontally = true; if(m_currentMoveDestination.x > characterPosition.x){ auto newX = m_mapCharacter->getPositionX() + m_mapCharacter->getDeltaMove()*m_currentCharacterSpeedMultiplier; if(newX >= m_currentMoveDestination.x){ newX = m_currentMoveDestination.x; } else { doneMovingHorizontally = false; } m_mapCharacter->setPositionX(newX); m_mapCharacter->faceRight(); } else if(m_currentMoveDestination.x < characterPosition.x){ auto newX = m_mapCharacter->getPositionX() - m_mapCharacter->getDeltaMove()*m_currentCharacterSpeedMultiplier; if(newX <= m_currentMoveDestination.x){ newX = m_currentMoveDestination.x; } else { doneMovingHorizontally = false; } m_mapCharacter->setPositionX(newX); m_mapCharacter->faceLeft(); } doneMovingVertically = true; if(m_currentMoveDestination.y > characterPosition.y){ auto newY = characterPosition.y + m_mapCharacter->getDeltaMove()*m_currentCharacterSpeedMultiplier; if(newY >= m_currentMoveDestination.y){ newY = m_currentMoveDestination.y; } else { doneMovingVertically = false; } m_mapCharacter->setPositionY(newY); } else if(m_currentMoveDestination.y < characterPosition.y){ auto newY = characterPosition.y - m_mapCharacter->getDeltaMove()*m_currentCharacterSpeedMultiplier; if(newY <= m_currentMoveDestination.y){ newY = m_currentMoveDestination.y; } else { doneMovingVertically = false; } m_mapCharacter->setPositionY(newY); } } void IMapCharacterController::updatePhaseCheckCharacterStopped(float p_dt){ if(m_currentlyScheduledPath.size() == 0){ stopMovingCharacter(); interactWithAdventureObjects(); if(m_currentOnDestinationTileReachedCallback != nullptr){ m_currentOnDestinationTileReachedCallback(); //todo would be nice to pass the dst tile data m_currentOnDestinationTileReachedCallback = nullptr; } //TODO not sure if this is ok } else { auto mapEvent = new AniTutorialMapControl::CharacterChangeTileEvent(m_mapCharacter->getPosition(), getCurrentCharacterTile()); m_mapControl->mapEvent(this, mapEvent); delete mapEvent; if(m_moveDestinationTileIndexOnPath != -1 && m_moveDestinationTileIndexOnPath < m_currentlyScheduledPath.size()-1){ m_moveDestinationTileIndexOnPath++; auto nextTile = m_currentlyScheduledPath[m_moveDestinationTileIndexOnPath]; moveCharacterByOneTile(nextTile.col, nextTile.row); //todo handle errors } else { stopMovingCharacter(); // cocos2d::log("STOPPING AND INTERACTING!\n"); interactWithAdventureObjects(); if(m_currentOnDestinationTileReachedCallback != nullptr){ m_currentOnDestinationTileReachedCallback(); //todo would be nice to pass the dst tile data m_currentOnDestinationTileReachedCallback = nullptr; } } } } void IMapCharacterController::moveCharacterToTouchedTile(const cocos2d::Point& touchLocation, const cocos2d::Point& mapPosition) { if(m_characterDisabled || m_characterMovementDisabled){ return; } auto map = m_mapController->getMap(); auto AniMapUtils = AniMapUtils::getInstance(); auto touchLocationOnMap = AniMapUtils.translateScreenPositionToMapPosition(touchLocation, mapPosition); auto rowCol = AniMapUtils.translateXYPointToColRow(touchLocationOnMap, map->getTileSize().width, map->getTileSize().height, map->getMapSize().width,map->getMapSize().height); if(m_currentlyScheduledPath.size() > 0 && currentDestination() == rowCol){ // if the same touched tile as before, do not calculate the path again and do not check for adventure objects // cocos2d::log("TT: %d %d, same as previous, SKIPPING\n", rowCol.col, rowCol.row); return; } auto advObjectsSet = m_mapController->getAdventureObjectsAtTile(rowCol); if(advObjectsSet.size() > 0){ auto advObject = pickAdventureObject(advObjectsSet); approachAdventureObject(advObject, rowCol); } else { // cocos2d::log("moving to tile %d %d\n", rowCol.col, rowCol.row); moveCharacterToTile(rowCol, [&](){ }); } } void IMapCharacterController::interactWithAdventureObjects(){ if(m_characterDisabled){ return; } auto advObjects = m_mapController->getAdventureObjectsAtMapPoint(m_mapCharacter->getPosition()); if(std::find(advObjects.begin(), advObjects.end(), m_approachedAdventureObject) != advObjects.end()){ interactWithAdventureObjects(std::vector{m_approachedAdventureObject}); } else { interactWithAdventureObjects(advObjects); } m_approachedAdventureObject = nullptr; } // warning: we are ssuming here that the tile is accessible, e.g. belongs to a valid path (currentlyScheduledPath) void IMapCharacterController::moveCharacterByOneTile(int p_toCol, int p_toRow) { // cocos2d::log("move by one tile: %d %d\n", p_toCol, p_toRow); if(m_characterDisabled || m_characterMovementDisabled){ return; } updateLastUserActivityTime(); auto AniMapUtils = AniMapUtils::getInstance(); auto toTile = AniMapUtils::TileData(p_toCol, p_toRow); auto currentCharacterLocation = getCurrentCharacterTile(); if(AniMapUtils.areTilesNeighbours(currentCharacterLocation, toTile, m_mapController->getMap(), m_mapController->getTileLayer())){ m_currentMoveDestination = AniMapUtils.getTileMiddlePosition(m_mapController->getMap(), p_toCol, p_toRow); m_mapCharacter->setWalking(true); onTileChange(AniMapUtils::TileData(p_toCol, p_toRow), AniMapUtils::TileData(p_toCol, p_toRow)); } } //TODO uporzadkowac void IMapCharacterController::moveCharacterToTile(AniMapUtils::TileData toTile, std::function callback){ if(m_characterDisabled || m_characterMovementDisabled){ return; } m_currentOnDestinationTileReachedCallback = callback; auto map = m_mapController->getMap(); auto tileLayer = m_mapController->getTileLayer(); auto AniMapUtils = AniMapUtils::getInstance(); if(!AniMapUtils.isTileAccessible(map, tileLayer, toTile.col, toTile.row)){//todo check for dups auto closestAccessibleTile = AniMapUtils.findClosestAccessibleTile(toTile, map, tileLayer); if(closestAccessibleTile.row != -1){ moveCharacterToTile(closestAccessibleTile, callback); } return; } // once in a while, perform a full path check instead of optimization - in order to avoid steve going in a slalom, but taking the shorter path instead // if(m_tilePathAddCounter < 128){ //todo 128 as a constant ^ // m_tilePathAddCounter++; if(m_currentlyScheduledPath.size() > 0){ // our tile is already on the path, let's just cut the rest of the path and carry on for(int i = m_moveDestinationTileIndexOnPath; i <= m_currentlyScheduledPath.size()-1; ++i){ auto tile = m_currentlyScheduledPath[i]; if(toTile == tile){ int erasedTilesCount = (int)m_currentlyScheduledPath.size() - 2*i - 2; if(erasedTilesCount > 0){ m_currentlyScheduledPath.erase(m_currentlyScheduledPath.begin()+i+1,m_currentlyScheduledPath.end()-i-1); if(i == m_moveDestinationTileIndexOnPath){ stopMovingCharacter(); if(m_currentOnDestinationTileReachedCallback != nullptr){ m_currentOnDestinationTileReachedCallback(); //todo would be nice to pass the dst tile data m_currentOnDestinationTileReachedCallback = nullptr; } } } return; // our tile is a neighbour of either the last tile on the path - so add it, or a mid tile - remove the tiles further and add it } else if(AniMapUtils.areTilesNeighbours(toTile, tile, m_mapController->getMap(), m_mapController->getTileLayer()) && (i == m_currentlyScheduledPath.size()-1 || toTile != m_currentlyScheduledPath[i+1])){ // add toTile to the path after current tile, removing all path tiles from i+1 if(i < m_currentlyScheduledPath.size()-1){ m_currentlyScheduledPath.erase(m_currentlyScheduledPath.begin() + i+1,m_currentlyScheduledPath.end()); } // cocos2d::log("modifying path, pushing tile: %d %d\n", toTile.col, toTile.row); m_currentlyScheduledPath.push_back(toTile); return; } } } // } auto path = AniMapUtils.calculatePathOnTmxMap(getCurrentCharacterTile(), toTile, map, tileLayer, true, true); // cocos2d::log("new path to til: %d x %d size: %d\n", toTile.col, toTile.row, path.size()); moveCharacterStartOnPath(path); m_tilePathAddCounter = 0; } void IMapCharacterController::moveCharacterStartOnPath(std::vector p_path){ if(m_characterDisabled || m_characterMovementDisabled){ return; } // stopMovingCharacter(); if(p_path.size() > 0){ // cocos2d::log("bul\n"); m_currentlyScheduledPath = p_path; //todo cancel previous first if already scheduled., check if path length valid. // this.tilePathAddCounter = 0; m_moveDestinationTileIndexOnPath = 1; auto nextTile = p_path[m_moveDestinationTileIndexOnPath]; m_mapCharacter->getController()->moveCharacterByOneTile(nextTile.col, nextTile.row); //todo handle errors } else if(!m_mapCharacter->isInFingerMoveState()){ // stopMovingCharacter(); interactWithAdventureObjects(); if(m_currentOnDestinationTileReachedCallback != nullptr){ m_currentOnDestinationTileReachedCallback(); m_currentOnDestinationTileReachedCallback = nullptr; } } } void IMapCharacterController::stopMovingCharacter() { // cocos2d::log("STOP MOVING CHARACTER CALLED\n"); m_currentCharacterSpeedMultiplier = 1; m_mapCharacter->setWalking(false, true); m_moveDestinationTile.col = m_moveDestinationTile.row = -1; m_currentlyScheduledPath.clear(); m_tilePathAddCounter = 0; m_moveDestinationTileIndexOnPath = -1; } void IMapCharacterController::setDefaultCharacterPosture() { m_mapCharacter->adjustDefaultImage(); } void IMapCharacterController::adjustCharacterDepth() { int newDepth = m_mapController->getBackgroundDepth() + 1; auto characterState = m_mapCharacter->getCharacterState(); if(characterState == IMapCharacter::FALLING || characterState == IMapCharacter::JUMPING){ //TODO or climbing? // if(m_mapCharacter->isFalling() || m_mapCharacter->isClimbing() || m_mapCharacter->isJumping()) { //TODO isclimbing newDepth = m_mapController->getMaxObjectDepth()+10; } else { //get all the map objects in steve's area auto allObjects = m_mapController->getMapImageObjectsBoundByRect(m_mapCharacter->getBoundingBox()); for(int i = 0; i < allObjects.size(); ++i){ auto object = allObjects.at(i); if (object->alwaysOnTop()){ continue;; } else if ((m_mapCharacter->getBoundingBox().getMinY() < object->getBoundingBox().getMinY()) || object->mergeWithBackground()) { newDepth = MAX(newDepth, object->getLocalZOrder()+1); } } } m_mapCharacter->getParent()->reorderChild(m_mapCharacter, newDepth); } void IMapCharacterController::instantTilesReveal(bool allTheWayUp){ auto characterBB = m_mapCharacter->getBoundingBox(); auto revealRangeX = m_tileRevealRange.x/cocos2d::Director::getInstance()->getContentScaleFactor(); auto revealRangeY = m_tileRevealRange.y/cocos2d::Director::getInstance()->getContentScaleFactor(); if(allTheWayUp){ auto beginning = m_mapCharacter->getParent()->getBoundingBox().size.height; auto step = -revealRangeY/2; for(int i = beginning; i > characterBB.getMidY(); i += step){ m_mapController->revealTilesInRadius(cocos2d::Point(characterBB.getMidX(), i), revealRangeX, revealRangeY); // TODO dedup } } m_mapController->revealTilesInRadius(cocos2d::Point(characterBB.getMidX(), characterBB.getMidY()), revealRangeX, revealRangeY); } void IMapCharacterController::updateLastUserActivityTime() { m_lastUserActivityTime = cocos2d::utils::getTimeInMilliseconds(); } void IMapCharacterController::disableCharacter(){ // TODO make sure he stays at a reachable tile. m_characterDisabled = true; if(m_mapCharacter->isInFingerMoveState()){ stopMovingCharacter(); } else if(m_mapCharacter->getCharacterState() == IMapCharacter::DANCING){ m_mapCharacter->stopDancing(); // } else if(m_mapCharacter->isJumping() || m_mapCharacter->isFalling() climbing sliding) {//TODO //// m_mapCharacter->scheduleOnce([&](){ //// //// });s } else if(m_mapCharacter->isLaughing()){ m_mapCharacter->stopLaughing(); } else { m_mapCharacter->pause(); m_mapCharacter->stopAllActions(); } } void IMapCharacterController::enableCharacter() { m_characterDisabled = false; m_mapCharacter->resume(); } void IMapCharacterController::disableCharacterMovement(){ m_characterMovementDisabled = true; if(m_mapCharacter->isInFingerMoveState()){ stopMovingCharacter(); } } void IMapCharacterController::enableCharacterMovement(){ m_characterMovementDisabled = false; }