Personal Project, Unreal, C++, Only Developer
Descent Into Atlantis is a turn-based dungeon crawler.It is inspired by other games of its genre. Labryinth of Refrain, Etrian Odyssey and Mary Skelter.

Combat System

Descent Into Atlantis uses a variation of the press turn system popularised by Shin Megami Tensei Nocturne. (Hitting an enemy's weakness will give you more turns while hitting an enemy's strength will make you lose your turns.) (The combat moves in a much more fervent pace and was a great choice to create engaging combat.)

                        
// This is called after we have already selected a skill

void UPressTurnManager::ActivateSkill(UCombatEntity* aAttacker, int aCursorPosition, USkillBase* aSkill)
{

	TArray  enemyInCombat    = TArray(combatManager->GetEnemysInCombat());
	TArray  playersInCombat  = TArray(combatManager->GetPlayersInCombat());
	
	TArray entitySkillsAreUsedOn;

	TArray turnReactions;

	FSkillsData skillsData = aSkill->skillData;
	
	if(aAttacker->characterType == ECharactertype::Ally)
	{
		entitySkillsAreUsedOn = skillsData.skillUsage == ESkillUsage::Opponents ? enemyInCombat : playersInCombat;
	}
	else if(aAttacker->characterType == ECharactertype::Enemy)
	{
		entitySkillsAreUsedOn = skillsData.skillUsage == ESkillUsage::Opponents ? playersInCombat : enemyInCombat;
	}
	
	if(skillsData.skillRange == ESkillRange::Single)
	{
		turnReactions.Add(aSkill->UseSkill(aAttacker,entitySkillsAreUsedOn[aCursorPosition]));
	}
	else if (skillsData.skillRange == ESkillRange::Multi)
	{
		for(int i = 0 ; i < entitySkillsAreUsedOn.Num();i++)
		{
		
			turnReactions.Add(aSkill->UseSkill(aAttacker,entitySkillsAreUsedOn[i]));
			
		}
		
	}

	ProcessTurn(turnReactions);
}

-----------------------------------------------------------------------------------------

void UPressTurnManager::ProcessTurn(TArray aAllTurnReactions)
{
	//Null Skills Consume all press turns completely

	for (PressTurnReactions reaction : aAllTurnReactions)
	{
		if (reaction == PressTurnReactions::Null)
		{
			ConsumeTurn(activePressTurns.Num());
			return;
		}
	}
        
        
	//If Dodged Consume two press turns if empowered only take the empowered and the next token
        
	for (PressTurnReactions reaction : aAllTurnReactions)
	{
		if (reaction == PressTurnReactions::Dodge ||
			reaction == PressTurnReactions::Strong )
		{
			ConsumeTurn(2);
			return;
		}
	}
        
	//If weakness is hit correctly then the turn that was used will be empowered
        
	for (PressTurnReactions reaction : aAllTurnReactions)
	{
		if (reaction == PressTurnReactions::Weak || 
			reaction == PressTurnReactions::Pass)
		{
			EmpowerTurn();
			return;
		}
	}
        
        
	//Normal action Consume 1 empowered or normal pressturn
	ConsumeTurn(1);
	//Passing will turn a whole icon into a empowered one but will consume an empowered one if it is

}
---------------------------------------------------------------------------------------------

void UPressTurnManager::ConsumeTurn(int aAmountOfTurnsConsumed)
{
	
	int TurnsRemaining =  (activePressTurns.Num() - 1)  - aAmountOfTurnsConsumed;
	
	for (int i = activePressTurns.Num() - 1; i > TurnsRemaining; i--)
	{
		if(i >= 0)
		{
			inActivePressTurns.Add(activePressTurns[i]);
			activePressTurns.RemoveAt(i);
		}
	}
	turnCounter->SetTurnOrder(activePressTurns.Num(),characterType);
	
	combatManager->TurnFinished();
}
    
-------------------------------------------------------------------------------------------


void UPressTurnManager::EmpowerTurn()
{
	int ActivePositionTurn = activePressTurns.Num() - 1;
        
	if (activePressTurns[ActivePositionTurn]->isEmpowered == false)
	{
		activePressTurns[ActivePositionTurn]->isEmpowered = true;
		turnCounter->SetEmpowerTurnIcon(ActivePositionTurn);
		combatManager->TurnFinished();
	}
	else
	{
		ConsumeTurn(1);
	}
}
                        
                    
                        
    skillFactory = NewObject();
    
    skillFactory->InitializeDatabase(dataTablesSkills);

    if(dataTables.Contains(EDataTableTypes::Enemys) &&
       dataTables.Contains(EDataTableTypes::EnemyGroups))
    {
        if(dataTables[EDataTableTypes::Enemys] != nullptr
            && dataTables[EDataTableTypes::EnemyGroups] != nullptr)
        {
            enemyFactory = NewObject();
            enemyFactory->InitializeDatabase(dataTables[EDataTableTypes::Enemys],
                dataTables[EDataTableTypes::EnemyGroups]);
        }
    }

    combatManager = NewObject();
    combatManager->Initialize(this,world);

    if(dataTables.Contains(EDataTableTypes::Party) &&
       !dataTablesClasses.IsEmpty())
    {
        if(dataTables[EDataTableTypes::Party] != nullptr)
        {
            partyManager = NewObject();
            partyManager->InitializeDataTable(skillFactory,dataTables[EDataTableTypes::Party], dataTablesClasses,combatManager);
        }
    }

    if(dataTables.Contains(EDataTableTypes::Tutorial))
    {
        if(dataTables[EDataTableTypes::Tutorial] != nullptr)
        {
            tutorialManager = NewObject();
            tutorialManager->InitializeDatabase(dataTables[EDataTableTypes::Tutorial]);
        }
    }
    if(dataTables.Contains(EDataTableTypes::Floor)
        && dataTables.Contains(EDataTableTypes::FloorEvent))
    {
        if(dataTables[EDataTableTypes::Floor] != nullptr
            &&dataTables[EDataTableTypes::FloorEvent] != nullptr)
        {
            floorFactory = NewObject();
            floorFactory->InitializeDatabase(dataTables[EDataTableTypes::Floor],dataTables[EDataTableTypes::FloorEvent]);
            floorEventManager = NewObject();
            floorEventManager->Initialize( this,floorFactory,combatManager);
        }
    }

    if(dataTables.Contains(EDataTableTypes::Dialogue))
    {
        if(dataTables[EDataTableTypes::Dialogue] != nullptr)
        {
            dialogueFactory = NewObject();
            dialogueFactory->InitializeDatabase(dataTables[EDataTableTypes::Dialogue]);
        }
    }

-----------------------------------------------------------------------------------------

void UFloorFactory::InitializeDatabase(UDataTable* aFloorDatabase,UDataTable* aFloorEnemyDatabase)
{
	UDataTable* datatable = aFloorDatabase;
	for(int i = 0 ; i < datatable->GetRowMap().Num(); i ++)
	{
		floorData.Add(*datatable->FindRow(FName(FString::FromInt(i)),FString("Searching for Floors"),true));

		UFloorBase* floorBase = NewObject();
		floorBase->floorData = floorData[i];
		
		floorDictionary.Add(floorData[i].floorIdentifier,floorBase);
	}

	UDataTable* datatable2 = aFloorEnemyDatabase;
	for(int i = 0 ; i < datatable2->GetRowMap().Num(); i ++)
	{
		floorEnemyData.Add(*datatable2->FindRow(FName(FString::FromInt(i)),FString("Searching for Floors Events"),true));
	}

	for(int i = 0 ; i < floorEnemyData.Num(); i++)
	{
		floorDictionary[floorEnemyData[i].floorIdentifier]->floorEventData.Add(floorEnemyData[i].positionInGrid,floorEnemyData[i]);
	}
}
-----------------------------------------------------------------------------------------

void UPartyManager::InitializeDataTable (USkillFactory* aSkillFactory,UDataTable* aDataTable, TMap aClassDataTable,UCombatManager* aCombatManager)
{
	skillFactory = aSkillFactory;

	UDataTable* datatable = aDataTable;
	for(int i = 0 ; i < datatable->GetRowMap().Num(); i ++)
	{
		playerEntityData.Add(*datatable->FindRow(FName(FString::FromInt(i)),FString("Searching for seres"),true));
	}

	
	for(int i = 0;i < playerEntityData.Num();i++)
	{
		//if we dont get back the correct information from the datatable
		EDataTableClasses classTable = playerEntityData[i].DataTableClass;
		UPlayerCombatEntity* PlayerCombatEntity = NewObject();

		PlayerCombatEntity->SetPlayerEntity(playerEntityData[i]);
		PlayerCombatEntity->SetTacticsEntity(aSkillFactory);
		PlayerCombatEntity->SetPlayerClass(aClassDataTable[classTable]);
		PlayerCombatEntity->SetTacticsEvents(aCombatManager);
		

		playerCombatEntity.Add(PlayerCombatEntity);
		playerCombatEntityInfo.Add(classTable,PlayerCombatEntity);
	}
}
                        
                    

Data Driven Design

The game is designed to be fully data driven. The enemies, dialogue, floor, and tutorials are all data driven.
to see how the data is setup click here

Unreal Editor Tools and Dungeon Construction

Descent Into Atlantis is constructed using information set in a DataTable. (Each node of the floor tells us which cardinal direction is currently free. This information is used when generating the level to decide where walls should be placed)

                        
						
						
						

void AFloorManager::Initialize(ADesentIntoAtlantisGameModeBase* aGameModeBase)
{
	cardinalPositions.Add(ECardinalNodeDirections::Up,    FVector2D(-1,0));
	cardinalPositions.Add(ECardinalNodeDirections::Down,  FVector2D(1,0));
	cardinalPositions.Add(ECardinalNodeDirections::Left,  FVector2D(0,-1));
	cardinalPositions.Add(ECardinalNodeDirections::Right, FVector2D(0,1));
	gameModeBase = aGameModeBase;
}

-----------------------------------------------------------------------------------------
//Floor base is created and set by Data in a DataTable
void AFloorManager::SpawnFloor(UFloorBase* aFloorBase)
{
	if(aFloorBase == nullptr)
	{
		return;
	}

	aFloorBase->Initialize();

	AFloorNode* Object = nullptr;
	int AmountOfFloorNodes = aFloorBase->GridDimensionX * aFloorBase->GridDimensionY;

	floorNodes.Init(Object,AmountOfFloorNodes);
	currentFloor = aFloorBase;

	CreateGrid(aFloorBase);
	SetFloorNodeNeightbors(floorNodes);
	
}
-----------------------------------------------------------------------------------------

void AFloorManager::CreateGrid(UFloorBase* aFloor)
{
	UFloorBase* tempfloor = aFloor;
	for (int x = 0; x < tempfloor->GridDimensionX; x++)
	{
		for (int y = 0; y < tempfloor->GridDimensionY; y++)
		{
			int LevelIndex = aFloor->GetIndex(x, y);
			//If there is no node then continue
			if (tempfloor->floorData.floorBlueprint[LevelIndex] == (short)ECardinalNodeDirections::Empty)
			{
				continue;
			}

			SpawnFloorNode(x , y,LevelIndex );
			floorNodes[LevelIndex]->SetWalkableDirections(aFloor->floorData.floorBlueprint[LevelIndex]);
			FVector2D positionInGrid = FVector2D(x,y);
			if(aFloor->floorEventData.Contains(positionInGrid))
			{
				floorNodes[LevelIndex]->hasFloorEvent = true;
				floorNodes[LevelIndex]->floorEventHasBeenTriggeredEvent = gameModeBase->floorEventManager->EventHasBeenTriggered;
				SpawnFloorEnemyPawn(positionInGrid);
			}
		}
	}
	

}

---------------------------------------------------------------------------------------------

void AFloorManager::CreateFloor(EFloorIdentifier aFloorIdentifier)
{
	floorDictionary = gameModeBase->floorFactory->floorDictionary;
	
	if(floorDictionary[aFloorIdentifier] != nullptr)
	{
		SpawnFloor(floorDictionary[aFloorIdentifier]);
		SetPlayerPosition(floorDictionary[aFloorIdentifier]->floorData.startPosition);
	}
	
}

    
-------------------------------------------------------------------------------------------


void AFloorManager::SpawnFloorNode(int aRow, int aColumn, int aIndex)
{
	//Setting new Positon
	FVector PositionOffset;
	PositionOffset.Set(200 * aRow, 200 * aColumn, 0);
	FVector ActorFinalSpawnPoint = GetActorLocation() + PositionOffset ;
	
	FVector2D PositionInGrid;
	PositionInGrid.Set(aRow,aColumn);
	
	//Rotation
	FRotator rotator = GetActorRotation();
	
	//Spawn
	AFloorNode* floorNode;


	floorNode = Cast(GetWorld()->SpawnActor(floorNodeReference, ActorFinalSpawnPoint, rotator));
	floorNode->SetPositionInGrid(PositionInGrid);

	floorNodes[aIndex] = floorNode;
}
-------------------------------------------------------------------------------------------
void AFloorManager::SetFloorNodeNeightbors(TArray aFloorNodes)
{
	if(aFloorNodes.Num() == 0)
	{
		return;
	}

	for(int i = 0 ; i < aFloorNodes.Num();i++)
	{
		AFloorNode* mainNode = aFloorNodes[i];

		// In situations where the node was removed at creation
		if(mainNode != nullptr)
		{
			TArray walkableDirections = mainNode->walkableDirections;
		
			for (ECardinalNodeDirections direction : walkableDirections)
			{
				AFloorNode* neightborNode = GetNodeInDirection(mainNode->positionInGrid, direction);

				//In situations where the neightbor in that direction doesnt exist
				if(neightborNode != nullptr)
				{
					mainNode->nodeNeighbors.Add(direction,neightborNode);
				}
			}
		}
	}
}

                        
                    

Gallery