Repository navigation
Release/1.39.0 - #3322
Merged
Merged
Release/1.39.0#3322
Conversation
Release/1.38.0 to dev
v200490 TableCSV 에서 8등급 합성 행이 열리고(8,*,4,10000) Ultimate 아이템이 실제로 들어오면서, 가짜 G9 행을 주입하고 '8,Aura,1,0' 치환을 전제하던 SynthesizingIntoAGradeTheSheetsAddNeedsNoCodeChange 가 깨졌다. 주입 없이 공개 시트로 Aura/Grimoire/FullCostume 8→9 합성을 검증하는 Theory 로 바꾸고, 성공률은 열림(>0)만 단언한 뒤 결정성을 위해 100% 로 고정한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Update table CSVs (2026-09-29) — v200490 G9 Ultimate
StageSheet(EventDungeonStageSheet 상속 포함), AdventureBossFloorSheet, InfiniteTowerFloorSheet 의 hp/atk/def/cri/hit/spd 보정값을 TryParseInt 로 읽고 있어서, int32 를 넘는 값은 에러 없이 continue 로 빠졌다. 몬스터가 의도보다 약해지는 쪽이다. StatModifier.Value 는 이미 long 이라 파서만 TryParseLong 으로 바꾼다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: 적 초기 스탯 보정값을 long 으로 파싱
이벤트성 수치 조정(보상 증가·비용 할인)을 원본 시트를 덮어쓰고 되돌리는 대신 블록 구간 [start_block, end_block) 동안만 적용되는 행으로 표현한다. 첫 적용 대상은 Aura/Rune/Costume 소환의 등급 보장 개수. - 행 파싱은 절대 throw 하지 않고, 깨진 행은 invalid 로 남겨 매칭에서 제외한다 - target 은 문자열로만 매칭해서 구버전 노드가 모르는 target 도 읽을 수 있다 - 시트가 패치된 적 없으면 TryGetPatchedSheet 로 기존 동작 그대로 (재실행 동일) - 부스트는 이미 있는 보장만 조정하고 새로 만들지 않으며, 최소 1회 일반 뽑기를 남긴다 - PatchTableSheet(Compressed) 에서 ValidateCsv 로 깨진 스케줄을 패치 시점에 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
등급·레벨 구간별로 배율이 다른 조정(예: 룬 강화 lv1~50 ×0.6, lv51~100 ×0.65)을 id 마다 행을 쓰지 않고 구간 하나로 표현한다. 구분자를 "-" 대신 "~" 로 한 건 스프레드시트가 "1-12" 를 날짜로 바꿔버리는 걸 피하기 위해서다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
로테이션 이벤트 E1(합성 재료 할인)을 SynthesizeSheet 를 덮어쓰지 않고 BoostScheduleSheet 행으로 기간 한정 적용한다. - target SYNTHESIZE_REQUIRED_COUNT, target_id = 재료 등급(grade_id). 구간(1~4)으로 등급 티어별 배율을 표현한다 - item_sub_type 은 구분하지 않는다. 기획이 같은 등급이면 Aura/Grimoire/FullCostume 동일 배율을 원하므로 target 하나로 충분하고, 구분이 필요해지면 target 을 추가한다 - required_count 를 쓰는 곳은 SynthesizeSimulator.Simulate 한 곳뿐이라 거기서 GetRequiredCount 로 적용한다. Synthesize.Execute 가 TryGetPatchedSheet 로 행을 찾아 InputData.RequiredCountBoost 로 넘긴다 (기본 null, 기존 호출부 그대로 컴파일·동작) - 내림 후 최소 1로 바닥을 둔다: 할인이 합성을 공짜로 만들거나 0 나눗셈으로 터지지 않게. 시트 값이 0 이하이면 부스트 없이 원래대로 실패하게 그대로 둔다 - 시트 미패치·매칭 없음이면 소모 재료·난수 소비·결과가 이전과 동일 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
월드보스 보상(룬·크리스탈·서클)을 BoostScheduleSheet 행으로 블록 구간 동안 조정한다. 기념일 "월드보스 보상 x2" 같은 이벤트를 원본 보상 시트를 고치지 않고 걸기 위함. - target 을 보상 출처별로 나눈다: WORLD_BOSS_BATTLE_REWARD(Raid 전투 보상), WORLD_BOSS_KILL_REWARD(Raid 안의 처치 보상 + ClaimWordBossKillReward), WORLD_BOSS_RANK_REWARD(ClaimRaidReward 등급 보상). target_id 는 boss_id(900001 등). 처치·등급 보상은 지급(청구) 블록 기준이라 이벤트 전에 쌓아 둔 미청구분도 이벤트 중 청구하면 부스트된다. 전투 보상만 걸고 싶을 때를 위해 출처별로 분리했다. - 뽑기 횟수가 아니라 뽑힌 결과를 스케일한다. 룬 개수 루프·SetRune 은 그대로라 IRandom 소비가 부스트 유무와 무관하게 동일하고, 같은 액션에서 뒤이어 뽑는 보상도 바뀌지 않는다. MUL 2 는 뽑힌 룬 종류별 개수·크리스탈·서클을 각각 정확히 2배로 만든다. ADD 는 각 수량에 따로 더해진다(룬은 뽑힌 종류마다). 0 이 된 수량은 지급하지 않고, 원래 0 인 보상(전투 보상의 크리스탈 등)을 새로 만들지 않는다. - WorldBossHelper.CalculateReward, RaidSimulator 생성자, SetWorldBossKillReward 에 기본값 null 인 선택 인자로 넘긴다. 클라이언트가 같은 값을 넘겨야 재시뮬레이션 결과가 맞는다. - 시트가 패치된 적 없거나 매칭 행이 없으면 기존과 동일(재실행 동일). - 기여도 보상(ClaimWorldBossReward)과 랭킹 보상(WorldBossRankingRewardSheet, 체인 밖 지급)은 이번 범위에서 제외. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
로테이션 이벤트 E3(룬 강화 비용 할인)를 BoostScheduleSheet 행으로 표현한다.
크리스탈과 룬스톤을 서로 다른 곡선으로 깎을 수 있게 target 을 둘로 나눴다.
- RUNE_ENHANCEMENT_CRYSTAL_COST / RUNE_ENHANCEMENT_RUNE_STONE_COST
- target_id = 그 시도가 도달하려는 룬 레벨(현재 레벨 + 1). 모든 룬 id 에 공통.
실패한 시도는 같은 레벨을 다시 시도하므로 같은 할인율로 다시 과금된다.
룬 id 별 제한은 요구가 없어 넣지 않았다(target_id 하나로는 레벨과 id 를
동시에 담을 수 없음).
- 시도마다 RuneHelper.TryEnhancement 안에서 레벨별로 행을 찾는다. 그래서
1~50 / 51~100 같은 구간이 tryCount 여러 번에 걸친 강화에도 그대로 맞는다.
- NCG 와 성공률은 건드리지 않는다. 난수 소비 횟수·순서도 그대로다.
- 결과는 [1, base] 로 묶는다.
- 최소 1: 4 × 0.2 = 0.8 → 0 처럼 내림 때문에 공짜가 되는 것을 막는다.
공짜 강화는 RuneCostSheet 에서 직접 정할 일이다. base 가 0 이면 0 유지.
- 최대 base(할인 전용): 인상 요구가 없고, 잘못 넣은 행이 시도 합계를
int 범위 밖으로 밀어 음수로 감기면 과금이 통째로 건너뛰어지기 때문.
- 시트가 패치된 적 없거나 매칭 행이 없으면 같은 RuneCostData 인스턴스를
그대로 쓴다 → 재실행 결과 동일.
- 새 파라미터는 모두 선택적(기본 null)이라 클라이언트 등 기존 호출부는
그대로 컴파일·동작한다. 클라이언트가 비용을 표시할 때 쓰도록
ApplyEnhancementCostBoost(cost, level, sheet, blockIndex) 를 공개한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
어드벤처보스 탐험·소탕 보상을 BoostScheduleSheet 행으로 기간 한정 조정한다. - ADVENTURE_BOSS_FLOOR_REWARD: 층 클리어마다 AdventureBossFloorSheet 보상에서 뽑는 1개 항목의 수량. ExploreAdventureBoss·SweepAdventureBoss 둘 다 적용. - ADVENTURE_BOSS_FIRST_CLEAR_REWARD: ExploreAdventureBoss 의 첫 클리어 보상 (AdventureBossFloorFirstRewardSheet) 각 항목의 수량. - target_id 는 층 번호(floor, 1~20). 층 row id 가 아니다. 결정: - 이미 뽑힌 항목의 최종 수량만 Row.Apply 로 조정한다. 뽑기 순서·횟수가 그대로라 점수(보상 뒤에 뽑는 random)까지 부스트 유무와 무관하게 동일하다. - 수량 0 항목은 건드리지 않고(보상을 만들지 않음), 결과는 최소 1 로 둔다 (0 지급은 AddItem/MintAsset 에서 throw). - GetWaveRewards 는 바꾸지 않았다: 탐험에서 웨이브 보상은 시뮬레이터에만 넘어가고 인벤토리에 지급되지 않는다. - ClaimAdventureBossReward(현상금·기여 보상, NCG 포함)는 범위 밖. 시즌 종료 후 청구 시점 블록을 기준으로 할지 등 기획 결정이 필요하다. 탐험·소탕 보상에는 NCG 가 없다. - 시트가 패치된 적 없으면 TryGetPatchedSheet 로 기존 동작 그대로. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
모험 스테이지(HackAndSlash, HackAndSlashSweep) 클리어 보상을 BoostScheduleSheet 행으로 블록 구간 동안 조정한다. 대상은 두 가지: - STAGE_ITEM_REWARD: 재료 아이템 보상. target_id = StageSheet 스테이지 id - STAGE_FAV_REWARD: 크리스탈·룬스톤 등 FAV 보상. target_id = StageSheet 스테이지 id 결정 사항 - 뽑기가 끝난 뒤 수량만 조정한다. 난수 호출 횟수·순서는 부스트가 있어도 그대로라 같은 전투가 같은 결과로 재생된다(경험치·클리어 여부 동일). - 클리어 1회 단위, 서로 다른 아이템 id 단위로 n → Apply(n). MUL 2 = 뽑힌 아이템이 정확히 2배, ADD 1 = 종류별 1개 더, 1 미만 배율은 감소. DropItemMax 는 뽑기 횟수만 제한하므로 넘을 수 있다. FAV 는 클리어 1회의 티커별 수량에 적용하고, 0 이 되면 민트하지 않는다. - 부스트는 뽑힌 보상만 조정하고 새로 만들지 않는다(뽑히지 않은/0 으로 뽑힌 FAV 는 그대로). 늘어난 재료도 일반 보상과 같아서 수집 퀘스트 진행에 반영된다. - 아이템은 클리어당 뽑힌 수의 10배(MaxItemRewardBoostFactor)로 상한. 값 오타의 과지급을 10배로 묶고, 클리어당 생성 객체 수를 DropItemMax 의 10배로 묶는다. - 소탕은 클리어마다 같은 규칙으로 적용해 HackAndSlash N 회와 지급 총량이 같다. 다만 최대 ~2,400 클리어라 늘어난 수량을 단위마다 객체로 만들면 AddItem(매번 정렬)이 배율에 비례해 블록 평가 비용을 키운다. 그래서 SplitItemRewardBoost 로 추가분을 아이템 id 별 개수로만 모아 id 당 AddItem(item, count) 한 번으로 넣는다. 부스트가 없으면 기존 UpdateInventory 경로 그대로. - 클라이언트 재생: HackAndSlashSweep.GetRewardItems(5인자) 와 StageSimulator.GetWaveRewards 는 추가분까지 객체로 펼친 목록을 돌려주며, id 별 개수가 실제 지급과 같다. - 이벤트 던전(EventDungeonBattle/Sweep)은 대상이 아니다. 스테이지 id 공간이 달라서 필요하면 별도 target 으로 추가한다. - 클라이언트가 같은 시드로 보상을 재생하므로 GetWaveRewards / GetFavWaveRewards / StageSimulator 생성자 / HackAndSlashSweep.GetRewardItems 에 선택 인자로 받는다. 시트가 패치된 적 없으면(TryGetPatchedSheet false) 기존과 바이트 단위로 동일. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
기획(재호) 확인: 룬 강화 비용 할인은 룬 소환으로 얻을 수 있는 룬에만 적용한다. - target 이름을 SUMMON_RUNE_ENHANCEMENT_CRYSTAL_COST / SUMMON_RUNE_ENHANCEMENT_RUNE_STONE_COST 로 바꿔 범위가 이름에 드러나게 했다 (PR 미머지라 변경 가능). target_id 는 여전히 도달 레벨. - "소환 룬" = RuneSummonSheet 어느 그룹이든 recipe id 로 등장하는 룬(비율 무관). id 목록을 박지 않고 실행 시점의 시트에서 유도하므로 소환 풀 변경을 따라간다. - RuneHelper.ResolveEnhancementCostBoost 가 판정한다. 활성 행이 하나라도 있을 때만 RuneSummonSheet 를 읽으므로(지연 로더) 부스트 없는 경로는 상태 읽기까지 이전과 동일하다. 시트가 없거나 못 읽으면 소환 룬 없음 → 할인 없음, throw 없음 (TryGetSheet). - TryEnhancement / ApplyEnhancementCostBoost 시그니처는 그대로이고, 여기에 넘길 시트는 Resolve 결과여야 한다고 문서화했다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TryEnhancement / ApplyEnhancementCostBoost 가 패치된 BoostScheduleSheet 를 그대로 받으면 소환 룬 판정을 건너뛸 수 있었다. 클라이언트가 그렇게 쓰면 비소환 룬에 할인가를 보여 주고, 실제 tx 는 NotEnoughFungibleAssetValueException 으로 실패한다. - RuneEnhancementCostBoost(sealed, 생성자 internal) 를 도입했다. 인스턴스는 RuneHelper.ResolveEnhancementCostBoost 만 만들 수 있고, 클라이언트는 별도 asmdef 라 직접 만들 수 없다. - TryEnhancement(..., RuneEnhancementCostBoost? boost = null, long blockIndex = 0) 와 ApplyEnhancementCostBoost 는 이 타입만 받는다. 시트를 직접 받는 공개 경로는 없다. 5인자 호출부는 그대로 컴파일되고 부스트 없는 경로도 동일하다. - Targets 문서: "소환 룬" 은 RuneSummonSheet 어느 행의 recipe 든 등장하는 룬이며, 비율이나 그 그룹의 현재 판매 여부와 무관하다. - 테스트: 인라인 RuneSummonSheet 로 비율 0 recipe 와 두 번째 그룹에만 있는 룬을 추가했다. "첫 그룹만" / "비율 > 0 만" 변이를 각각 잡는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
STAGE_ITEM_REWARD 의 MUL 값이 1 보다 크면, 클리어 1회에 1개만 뽑힌 아이템은 최소 2개를 준다(max(floor(1 × value), 2)). 대부분의 스테이지가 아이템을 클리어당 1개씩 떨궈서 MUL 1.5 같은 소수 배율이 아무 효과 없던 문제를 기획(재호) 승인으로 보정한다. - 2개 이상 뽑힌 아이템은 기존 내림 그대로(MUL 1.2 × 2 = 2). 일반적인 "최소 +1" 이 아니다. - MUL ≤ 1, ADD, FAV(STAGE_FAV_REWARD) 는 변경 없음. 10배 상한도 그대로. - 계산을 StageSimulator.GetBoostedItemRewardCount 한 곳에 두고 SplitItemRewardBoost 가 쓰므로 ApplyItemRewardBoost, HackAndSlashSweep.GetRewardItems(5·6인자) 가 모두 같은 규칙을 따른다. HackAndSlash·소탕·클라 재생 결과가 일치한다. - 난수 소비는 여전히 변하지 않는다(뽑은 뒤 수량만 조정). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"~" 단독, "*~5", int 를 넘는 끝값, 전각 물결표(IME) 는 무효로, "1~1" 과 끝값 주변 공백은 유효로 읽히는 것을 테스트로 고정한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RuneEnhancementCostBoost 가 시트만 들고 있어서, 소환 룬으로 해석한 부스트를 다른(비소환) 룬의 비용에 넘겨도 할인이 적용됐다. 룬 선택이 바뀌어도 부스트를 캐시해 두는 UI 라면 래퍼 타입을 둔 의미가 없어진다. - RuneEnhancementCostBoost 에 해석 시점의 RuneId 를 저장한다(Resolve 가 설정). - ApplyEnhancementCostBoost(cost, runeId, targetLevel, boost, blockIndex): runeId 가 boost.RuneId 와 다르면 부스트를 무시한다. RuneCostData 에는 룬 id 가 없어서 파라미터로 받는다. - TryEnhancement 는 costRow.RuneId 를 넘긴다. id 가 다르면 throw 하지 않고 무시해 할인 전 비용을 보여 준다(클라이언트 미리보기 경로). 온체인 RuneEnhancement 는 항상 같은 id 로 해석하므로 동작이 같다. - 클래스 문서: 부스트는 해석한 룬만 할인한다는 보장을 명시했다. - 테스트: 10001 로 해석한 부스트를 10036 비용에 넘기면 할인되지 않는다 (TryEnhancement, ApplyEnhancementCostBoost 각각). id 검사를 빼는 변이를 잡는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runeId 와 targetLevel 이 둘 다 int 라 순서가 바뀌어도 컴파일된다. 내부 호출처를 이름으로 넘겨 실수를 막는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: BoostScheduleSheet 로 소환 보장 개수를 기간 한정 조정
feat: BoostScheduleSheet 로 합성 재료 수량 조정
…g/boost-rune-enhancement # Conflicts: # Lib9c/TableData/BoostScheduleSheet.cs
…reward # Conflicts: # Lib9c/TableData/BoostScheduleSheet.cs
…oss-reward # Conflicts: # Lib9c/TableData/BoostScheduleSheet.cs
…reward # Conflicts: # Lib9c/TableData/BoostScheduleSheet.cs
ci: feature/* 브랜치도 dev NuGet 패키지 발행
- target 마다 무엇을 바꾸는지, target_id 가 가리키는 것·시트, 보통 쓰는 op, 값 처리(내림·상하한), 적용 범위 밖·선배포 조건, 예시 행을 정적 데이터로 둔다. 액션은 읽지 않는 참고용 - 백오피스 등 도구가 이걸 그대로 보여줘, 레버를 추가·변경할 때 설명도 같은 PR 에서 바뀐다 - Applied 는 Infos 에서 뽑아 레버 목록을 한 곳에서만 관리 - 모든 Targets 상수가 정확히 한 번 설명되고, 예시 행이 검증을 통과하는지 테스트 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 월드보스 boss_id≠raid id, 공통 RowRules, TargetIdMeaning 개명 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: BoostScheduleSheet 로 소환 룬 강화 비용 조정
feat: BoostScheduleSheet 로 월드보스 보상 조정
…ward feat: BoostScheduleSheet 로 어드벤처보스 보상 조정
feat: BoostScheduleSheet 로 스테이지 보상 조정
…g/boost-target-descriptions
feat: BoostScheduleSheet 레버별 작성 안내(Targets.Infos, RowRules)
- target_id = 망치 아이템 id(600301~600308) — 등급별 차등을 구간 행으로 표현 - ItemEnhancement.GetHammerExp(sheet, hammerId, boost): 64비트 내림, [원래, 원래×3] 으로 묶어 증가 전용 (공용 Row.Apply(int) 는 int.MaxValue 에서 잘려 초월 망치 5억에 못 쓰므로 Row.Apply(long) 추가) - 부스트된 exp 는 장비에 그대로 저장 — 정상적으로 그만큼 모은 장비와 같아져 다음 강화·재료 가치가 어긋나지 않는다 - 레버 안내(Infos): 구클라는 미리보기 레벨·NCG 비용을 낮게 보여 표시만큼 NCG 를 가진 아바타가 실패 → 클라 선배포 - 시트 미패치 체인은 기존과 동일(TryGetPatchedSheet) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…axAbsoluteValue 근거·cref 명시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: BoostScheduleSheet 로 강화 망치 exp 조정 (ENHANCEMENT_HAMMER_EXP)
feat: BoostScheduleSheet — 기간 한정 보상·비용 배율 (레버 14종)
* fix(boost): cap world boss and adventure boss reward boosts at 5x A BoostScheduleSheet row for WORLD_BOSS_BATTLE_REWARD, WORLD_BOSS_KILL_REWARD, WORLD_BOSS_RANK_REWARD, ADVENTURE_BOSS_FLOOR_REWARD or ADVENTURE_BOSS_FIRST_CLEAR_REWARD had no upper bound. A mistyped value (e.g. a block index pasted into the value cell) made ClaimRaidReward add several int.MaxValue circle counts to one inventory slot, overflowing it to a negative count. Each boosted amount is now capped at 5 times the drawn amount, ADD included, computed in 64 bits (WorldBossHelper.MaxWorldBossRewardBoostFactor, AdventureBossHelper.MaxAdventureBossRewardBoostFactor). Lower bounds are unchanged (world boss 0, adventure boss 1) and an amount of 0 is still never boosted. Also make SummonHelperTest.GetSummonRecipeIdsWithGradeGuarantee_AppliesBoost fail when the boost is dropped: assert the last 7 summons are all grade 4+ and that the unboosted run with the same seed is not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: document constants next to the boost reward caps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
500 사이클 릴리즈 브랜치
release/1.39.0을 main 에 머지합니다.🤖 Generated with Claude Code