← 게임 포트폴리오MONSTER TAMING / ENGINEERING기술문서구현 근거
IMPLEMENTATION EVIDENCE

설명을 뒷받침하는
구현과 검증 범위.

2026.09.11 소스 스냅샷 · 프로젝트 상대 경로와 원문 줄 번호를 함께 제공합니다. 전체 저장소가 아닌 핵심 범위 발췌입니다.

검증 기록은 범위별로 읽습니다

기록확인한 범위해석의 경계
44종 · 2026.09.07제작 자료의 Catalog·Draft 편입 대조제작 도구 적용 규모. 작업 시간 개선율이나 아트 원작 제작 수가 아닙니다.
270/270 · 2026.08.29 가이드9개 테마 × 10개 난이도 × 3개 시드의 생성 검증 집계해당 입력 조합의 기록. 모든 시드·밸런스·재미를 보장하지 않습니다.
EditMode 126/126 · PlayMode 3/3기존 Hex 검증 집계집계와 모든 개별 assertion의 대응은 여기서 제공하지 않습니다. Writer·저장 실패 주입의 통과 수로 사용하지 않습니다.
아래 선택 소스생산 검증·비용 지도·후보 저장·정산 가드의 구현소스 경로 확인이며 실기기·장시간·오류 주입 실행 결과는 아닙니다.

집계가 수록된 팀 발표 원본 ↗ · 팀 QA와 JIRA 집계는 협업 기록입니다.

추가 검증 과제: Writer 실패 지점별 복구, 플랫폼별 저장 중단·복구, 병력 수별 프레임 시간과 비용 지도 캐시 적중률.

팀의 제작 편차를 공통 생산 흐름으로

설계와 실행 흐름 읽기 ↗

생산 전 검증과 변경 범위 확보

Assets/ProjectMT/Editor/MonsterMaker/MonsterMakerAssetWriter.cs · L57–112
원본 파일 식별값 · SHA-2567f7d5192f627f22d5f93d617fbfcef5d79746323ab9dea8bd462450cee686680
57        public static MonsterMakerWriteResult BuildAndRegister(58            MonsterMakerDraft draft,59            MonsterCatalog catalog = null,60            MonsterRarityCatalog rarityCatalog = null)61        {62            draft?.EditorSyncActiveAttackAuthoring();63            var preflight = MonsterMakerValidator.Validate(draft);64            if (preflight.HasErrors)65            {66                throw new InvalidOperationException(BuildIssueText(preflight.Issues));67            }68 69            catalog ??= AssetDatabase.LoadAssetAtPath<MonsterCatalog>(MonsterCatalogPath);70            rarityCatalog ??= AssetDatabase.LoadAssetAtPath<MonsterRarityCatalog>(MonsterRarityCatalogPath);71            if (catalog == null || rarityCatalog == null)72            {73                throw new InvalidOperationException("MonsterCatalog 또는 MonsterRarityCatalog을 찾을 수 없습니다.");74            }75 76            var paths = BuildPaths(draft.MonsterId);77            var generatesUniquePassive = draft.UsePassiveSkill &&78                                         draft.RarityPassiveSkill is GenericMonsterPassiveSkill;79            var generatesAttackActive = draft.UseActiveSkill &&80                                        draft.Rarity >= MonsterRarity.Legendary &&81                                        draft.HasActiveProfile;82            var passivePath = BuildPassivePath(draft.MonsterId);83            var extraPaths = new List<string>();84            if (generatesUniquePassive) extraPaths.Add(passivePath);85            if (generatesAttackActive) extraPaths.Add(BuildActivePath(draft.MonsterId));86            var outputPaths = paths.Concat(extraPaths).ToArray();87            var dataFolder = DataRoot + "/" + draft.MonsterId;88            var artFolder = ArtRoot + "/" + draft.MonsterId;89            var catalogPath = RequirePersistentAssetPath(catalog, "MonsterCatalog");90            var rarityCatalogPath = RequirePersistentAssetPath(rarityCatalog, "MonsterRarityCatalog");91            ValidateProductionDraftOwnership(draft, catalogPath, rarityCatalogPath);92            var writesProductionAiCatalog =93                string.Equals(catalogPath, MonsterCatalogPath, StringComparison.OrdinalIgnoreCase) &&94                string.Equals(rarityCatalogPath, MonsterRarityCatalogPath, StringComparison.OrdinalIgnoreCase);95            var transactionPaths = outputPaths.Concat(new[] { catalogPath, rarityCatalogPath });96            if (writesProductionAiCatalog)97            {98                transactionPaths = transactionPaths.Concat(new[]99                {100                    CastleRaidAIProfileCatalogPath,101                    MainBattleAIProfileCatalogPath102                });103            }104            var transaction = MonsterMakerWriteTransaction.Capture(105                transactionPaths,106                new[] { dataFolder, artFolder });107 108            try109            {110                EnsureFolder("Assets/ProjectMT/02_Shared/Unit/Data", "Monsters");111                EnsureFolder(DataRoot, draft.MonsterId);112                EnsureFolder("Assets/ProjectMT/05_Art", "Monsters");

생성물·Catalog·GUID 검증과 복구

Assets/ProjectMT/Editor/MonsterMaker/MonsterMakerAssetWriter.cs · L415–485
원본 파일 식별값 · SHA-2567f7d5192f627f22d5f93d617fbfcef5d79746323ab9dea8bd462450cee686680
415            definition = AssetDatabase.LoadAssetAtPath<MonsterDefinition>(paths[0]);416            var outputValidation = MonsterDefinitionValidator.Validate(definition, true);417            if (outputValidation.HasErrors)418            {419                throw new InvalidOperationException(BuildRuntimeIssueText(outputValidation.Issues));420            }421 422            RegisterLast(catalog, rarityCatalog, definition, draft, uniquePassive, attackActive);423            SaveAssetsIfDirty(catalog, rarityCatalog, castleRaidAiCatalog, mainBattleAiCatalog);424            AssetDatabase.Refresh();425 426            if (!catalog.TryGet(draft.MonsterId, out var registered) || registered != definition)427            {428                throw new InvalidOperationException("생성물 검증 뒤 MonsterCatalog 등록을 확인하지 못했습니다.");429            }430 431            if (!rarityCatalog.TryGetRarity(draft.MonsterId, out var rarity) || rarity != draft.Rarity)432            {433                throw new InvalidOperationException("생성물 검증 뒤 MonsterRarityCatalog 등록을 확인하지 못했습니다.");434            }435 436            var assignedAiProfile = castleRaidAiCatalog?.Resolve(draft.MonsterId);437            if (castleRaidAiCatalog != null && (assignedAiProfile == null ||438                assignedAiProfile.Pattern != draft.CastleRaidAiPattern))439            {440                throw new InvalidOperationException("생성물 검증 뒤 Hex Castle Raid AI Profile 등록을 확인하지 못했습니다.");441            }442 443            if (mainBattleAiCatalog != null &&444                (!mainBattleAiCatalog.TryResolve(draft.MonsterId, out var mainBattleProfile) ||445                 mainBattleProfile.Role != draft.MainBattleRole ||446                 mainBattleProfile.TargetPriority != draft.MainBattleTargetPriority))447            {448                throw new InvalidOperationException("생성물 검증 뒤 MainBattle AI Profile 등록을 확인하지 못했습니다.");449            }450 451            var guidAfter = outputPaths.ToDictionary(path => path, AssetDatabase.AssetPathToGUID);452            for (var index = 0; index < outputPaths.Length; index++)453            {454                var before = guidBefore[outputPaths[index]];455                if (!string.IsNullOrEmpty(before) && !string.Equals(before, guidAfter[outputPaths[index]], StringComparison.Ordinal))456                {457                    throw new InvalidOperationException("기존 Asset GUID가 변경되었습니다: " + outputPaths[index]);458                }459            }460 461                var result = new MonsterMakerWriteResult(462                    definition,463                    updatedExisting,464                    outputPaths,465                    guidBefore,466                    guidAfter,467                    outputValidation);468                transaction.Commit();469                return result;470            }471            catch (Exception buildException)472            {473                try474                {475                    transaction.Rollback();476                }477                catch (Exception rollbackException)478                {479                    throw new AggregateException(480                        "Monster Maker 생성과 원상복구가 모두 실패했습니다.",481                        buildException,482                        rollbackException);483                }484 485                throw;

부술 수 있는 성벽까지 경로 비용으로

설계와 실행 흐름 읽기 ↗

공유 비용 지도의 키와 무효화

Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L209–255
원본 파일 식별값 · SHA-256310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6
209        public void Invalidate()210        {211            fields.Clear();212        }213 214        public bool TryResolveRoute(215            HexCoordinates start,216            HexCastleAssaultRoutePolicy policy,217            int expectedDefenseLayer,218            float damagePerSecond,219            float moveSpeed,220            int topologyVersion,221            out HexCastleAssaultRoutePlan plan)222        {223            plan = null;224            if (!cells.ContainsKey(start) || palaceFootprint.Contains(start))225            {226                return false;227            }228 229            var damageBand = Mathf.Max(1, Mathf.RoundToInt(Mathf.Max(1f, damagePerSecond) / 10f));230            var speedBand = Mathf.Max(1, Mathf.RoundToInt(Mathf.Max(0.1f, moveSpeed) * 4f));231            var key = new FieldKey(232                policy,233                Mathf.Max(0, expectedDefenseLayer),234                damageBand,235                speedBand,236                topologyVersion);237            if (!fields.TryGetValue(key, out var field))238            {239                field = BuildReverseField(240                    policy,241                    key.ExpectedDefenseLayer,242                    damageBand * 10f,243                    speedBand / 4f);244                fields.Add(key, field);245            }246 247            if (!field.ContainsKey(start))248            {249                return false;250            }251 252            var path = ReconstructPath(253                start,254                field,255                policy,

역방향 비용 지도 생성

Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L402–451
원본 파일 식별값 · SHA-256310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6
402        private Dictionary<HexCoordinates, float> BuildReverseField(403            HexCastleAssaultRoutePolicy policy,404            int expectedDefenseLayer,405            float damagePerSecond,406            float moveSpeed)407        {408            var result = new Dictionary<HexCoordinates, float>();409            var heap = new MinimumHeap();410            foreach (var goal in palaceApproaches)411            {412                if (!CanUseCell(goal, expectedDefenseLayer))413                {414                    continue;415                }416 417                result[goal] = 0f;418                heap.Push(new QueueNode(goal, 0f));419            }420 421            while (heap.Count > 0)422            {423                var current = heap.Pop();424                if (!result.TryGetValue(current.Coordinates, out var currentCost) ||425                    current.Cost > currentCost + 0.0001f)426                {427                    continue;428                }429 430                var entryCost = ResolveEntryCost(431                    current.Coordinates,432                    policy,433                    expectedDefenseLayer,434                    damagePerSecond,435                    moveSpeed);436                if (float.IsPositiveInfinity(entryCost))437                {438                    continue;439                }440 441                for (var direction = 0; direction < HexCoordinates.Directions.Length; direction++)442                {443                    var predecessor = current.Coordinates.Neighbor(direction);444                    if (!CanUseCell(predecessor, expectedDefenseLayer) || palaceFootprint.Contains(predecessor))445                    {446                        continue;447                    }448 449                    var candidate = currentCost + entryCost;450                    if (result.TryGetValue(predecessor, out var known) && candidate >= known - 0.0001f)451                    {

이동 시간과 파괴 비용

Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L540–561
원본 파일 식별값 · SHA-256310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6
540        private float ResolveEntryCost(541            HexCoordinates coordinates,542            HexCastleAssaultRoutePolicy policy,543            int expectedDefenseLayer,544            float damagePerSecond,545            float moveSpeed)546        {547            if (!CanUseCell(coordinates, expectedDefenseLayer))548            {549                return float.PositiveInfinity;550            }551 552            var travel = cellTravelDistance / Mathf.Max(0.1f, moveSpeed);553            var cell = cells[coordinates];554            if (!cell.IsBlocked)555            {556                return travel;557            }558 559            var destruction = cell.CurrentHealth / Mathf.Max(1f, damagePerSecond);560            return travel + destruction * ResolveDestructionWeight(cell, policy);561        }

저장이 성공한 상태만 플레이에 확정

설계와 실행 흐름 읽기 ↗

후보 저장 성공 후 현재 상태 확정

Assets/ProjectMT/02_Shared/GameData/GameDataService.cs · L93–142
원본 파일 식별값 · SHA-256289e29fff5da12fa5efa143b119a0157a9cb2bdaaf486e8e95d85d65409b28ad
93        private async Task<(bool Success, CommanderSkillSummonReceipt Receipt)> ApplyAndSaveCoreAsync(GameProgressChange change)94        {95            var notifyChanged = false;96            CommanderSkillSummonReceipt receipt = null;97            await gate.WaitAsync();98            try99            {100                if (!IsLoaded)101                {102                    ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, false);103                    return (false, null);104                }105 106                var candidate = current.Clone(107                    commanderSkillBalanceConfig,108                    commanderSkillSummonConfig); // 원본 보존 후 변경 검증109                if (!candidate.TryApply(110                        change,111                        commanderGrowthConfig,112                        itemCatalog,113                        equipmentBalanceConfig,114                        commanderSkillBalanceConfig,115                        commanderSkillSummonConfig))116                {117                    ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, false);118                    return (false, null);119                }120 121                try { await saveService.SaveAsync(candidate); } // 저장 성공을 먼저 확인122                catch123                {124                    try { ProjectMT.Shared.Audio.SfxEvents.Play2D(ProjectMT.Shared.Audio.SfxEvents.SaveError); }125                    catch (Exception soundError) { UnityEngine.Debug.LogException(soundError); }126                    throw;127                }128                current = candidate; // 성공한 후보만 확정129                receipt = candidate.CommanderSkillSummonReceipt;130                notifyChanged = !change.SuppressChangedNotification;131            }132            finally133            {134                gate.Release();135            }136 137            ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, true);138            if (notifyChanged)139            {140                Changed?.Invoke();141            }142 

정산 재진입 가드와 저장 결과 처리

Assets/ProjectMT/04_Contents/00_Framework/Runtime/ContentFlow.cs · L456–520
원본 파일 식별값 · SHA-2569e3e4e9ee67b4340e81e6da28c3f488a01ef52f9bdb831fc892b4b9a73ea0f26
456        private async Task TrySaveAndFinishAsync(ActiveRun run)457        {458            if (run == null || !ReferenceEquals(activeRun, run) || Phase != ContentFlowPhase.Finishing ||459                run.PendingChange == null || Interlocked.Exchange(ref run.SettlementInFlight, 1) != 0)460            {461                return;462            }463 464            run.CanRetry = false;465            ShowSaving();466 467            var saved = false;468            try469            {470                saved = await progress.TryApplyAndSaveAsync(run.PendingChange);471            }472            catch (Exception exception)473            {474                Debug.LogException(exception);475            }476            finally477            {478                Interlocked.Exchange(ref run.SettlementInFlight, 0);479            }480 481            if (!ReferenceEquals(activeRun, run))482            {483                return;484            }485 486            if (!saved)487            {488                Debug.LogError($"Content progress could not be saved. Content={run.Definition.ContentId}");489                run.CanRetry = true;490                ShowSaveFailed(() => RetrySave(run));491                return;492            }493 494            HideFinishFeedback();495            PlayResultSound(run);496            if (run.PendingResultPresentation != null && resultView != null)497            {498                try499                {500                    await resultView.ShowAsync(run.PendingResultPresentation);501                }502                catch (Exception exception)503                {504                    Debug.LogException(exception); // 결과 표현 실패가 복귀를 막지 않음505                }506 507                if (!ReferenceEquals(activeRun, run))508                {509                    return;510                }511            }512 513            if (run.PendingPresentation != null)514            {515                try516                {517                    rewardPresentation?.PlayConfirmed(run.PendingPresentation); // 저장 성공 뒤에만 화면 연출 허용518                }519                catch (Exception exception)520                {