2026-10-21 20:40:13 +07:00
package gameserver
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"gac-hkrpg-go/internal/common"
pb "gac-hkrpg-go/protocol/proto"
)
type cwEnemyPlan struct {
Route uint32 `json:"route_id"`
Chapter uint32 `json:"chapter_id"`
Section uint32 `json:"section_id"`
Division uint32 `json:"division_id"`
Overclock bool `json:"is_overclock"`
Penalty uint32 `json:"penalty_id"`
Stage uint32 `json:"stage_id"`
Level uint32 `json:"level"`
HardLevelGroup uint32 `json:"hard_level_group"`
EliteGroup uint32 `json:"elite_group"`
ExtraDifficulty uint32 `json:"extra_difficulty"`
Waves [] struct {
ID uint32 `json:"wave_id"`
Formation uint32 `json:"formation_wave_id"`
Count uint32 `json:"monster_count"`
} `json:"waves"`
}
2026-10-28 18:54:43 +07:00
// cwSelectedEnemyBranch resolves the one-based selection stored by the CW
// layer. Encounter layers can offer several enemy plans; choosing the first
// entry implicitly would fight a different branch from the one shown to the
// player.
func cwSelectedEnemyBranch ( layer * pb . MDJBBILMHLC ) ( * pb . KKBGIPCAGGH , error ) {
if layer == nil || len ( layer . BPDABHGFBFO ) == 0 {
return nil , fmt . Errorf ( "CW saved enemy layer has no offered plans" )
}
selection := layer . MOADBALHMFD
if selection == 0 || uint64 ( selection ) > uint64 ( len ( layer . BPDABHGFBFO )) {
return nil , fmt . Errorf ( "CW selected enemy branch %d is outside %d offered plans" , selection , len ( layer . BPDABHGFBFO ))
}
plan := layer . BPDABHGFBFO [ selection - 1 ]
if plan == nil {
return nil , fmt . Errorf ( "CW selected enemy branch %d is empty" , selection )
}
return plan , nil
}
2026-10-21 20:40:13 +07:00
// Only wave parameters are captured. Monster IDs and stars must agree with
// the current run's saved layer, so a reference encounter never replaces it.
func cwBuildNodeEnemy ( catalog * common . CurrencyWarsCatalog , run * pb . MPIMAEMCEFH , stage * pb . BEEFLEJAOCN , node * pb . OHMDHKGAEMM , info * pb . SceneBattleInfo ) error {
raw , err := common . ReadResourceFile ( "resources/CurrencyWarsEnemyPlans.json" )
if err != nil {
return err
}
var resource struct {
Format uint32 `json:"format_version"`
Client string `json:"target_client_version"`
Assembly string `json:"target_assembly_sha256"`
PlanHash string `json:"source_plan_sha256"`
StageHash string `json:"target_stage_capture_sha256"`
Entries [] cwEnemyPlan `json:"entries"`
}
if err = json . Unmarshal ( raw , & resource ); err != nil {
return err
}
if resource . Format != 1 || resource . Client != catalog . ClientVersion || resource . Assembly != catalog . Provenance . GameAssemblySHA256 {
return fmt . Errorf ( "CW enemy plan revision differs" )
}
for _ , digest := range [] string { resource . PlanHash , resource . StageHash } {
value , e := hex . DecodeString ( digest )
if e != nil || len ( value ) != sha256 . Size {
return fmt . Errorf ( "CW enemy plan provenance missing" )
}
}
2026-10-28 18:54:43 +07:00
layer := stage . IMPNAHBCADN . GetMPOBBIPMNNJ ()
plan , err := cwSelectedEnemyBranch ( layer )
if err != nil {
return err
}
2026-10-21 20:40:13 +07:00
var recipe * cwEnemyPlan
for i := range resource . Entries {
r := & resource . Entries [ i ]
2026-10-28 18:54:43 +07:00
if r . Route == stage . EMOHLJHPOPC && r . Chapter == stage . DCAHJELNJNP && r . Section == stage . SectionId && r . Division == run . DivisionId && r . Overclock == run . IsOverlock && r . Stage == node . StageId && r . Penalty == plan . IFIIOIIKJHO {
2026-10-21 20:40:13 +07:00
if recipe != nil {
return fmt . Errorf ( "CW enemy plan duplicated" )
}
recipe = r
}
}
2026-10-28 18:54:43 +07:00
if recipe == nil {
return cwBuildReferenceBossEnemy ( catalog , run , stage , node , info )
2026-10-21 20:40:13 +07:00
}
2026-10-28 18:54:43 +07:00
if recipe . Level == 0 || recipe . HardLevelGroup == 0 || recipe . EliteGroup == 0 || len ( recipe . Waves ) == 0 {
return fmt . Errorf ( "CW enemy plan not captured for route=%d chapter=%d section=%d division=%d overclock=%t stage=%d branch=%d/%d penalty=%d" , stage . EMOHLJHPOPC , stage . DCAHJELNJNP , stage . SectionId , run . DivisionId , run . IsOverlock , node . StageId , layer . MOADBALHMFD , len ( layer . BPDABHGFBFO ), plan . IFIIOIIKJHO )
2026-10-21 20:40:13 +07:00
}
if plan . IFIIOIIKJHO != recipe . Penalty || plan . EPPHOFDAEMI != recipe . ExtraDifficulty || len ( plan . MonsterWaveList ) != len ( recipe . Waves ) {
return fmt . Errorf ( "CW saved enemy plan differs from captured wave schema" )
}
camp , err := catalog . MonsterCamp ( run . LJNGPCKPJLF , layer . LNDKCPBJDKH )
if err != nil {
return err
}
allowed := make ( map [ uint32 ] bool , len ( camp . Season . MonsterList ))
for _ , id := range camp . Season . MonsterList {
allowed [ id ] = true
}
division , err := catalog . CapturedStage ( run . DivisionId )
if err != nil {
return err
}
// Additional difficulty sources are not resolved by this base recipe.
if stage . ANJBLBEOJND != nil {
for _ , delta := range stage . ANJBLBEOJND . PIFNOKAMBLL {
if delta != 0 {
return fmt . Errorf ( "CW modified enemy difficulty requires its own resolver" )
}
}
}
waves := make ([] * pb . SceneMonsterWave , 0 , len ( recipe . Waves ))
for i , config := range recipe . Waves {
current := plan . MonsterWaveList [ i ]
if config . ID == 0 || config . Formation == 0 || current . OCPEPEMNNEK != config . ID || uint64 ( len ( current . PBLBBDBDLEK )) != uint64 ( config . Count ) {
return fmt . Errorf ( "CW current wave identity/count differs" )
}
wave := & pb . SceneMonsterWave { BattleStageId : node . StageId , WaveId : config . ID , WaveParam : & pb . SceneMonsterWaveParam { Level : recipe . Level , HardLevelGroup : recipe . HardLevelGroup , EliteGroup : recipe . EliteGroup , CPHIINKIDEB : config . Formation }}
for _ , monster := range current . PBLBBDBDLEK {
if ! allowed [ monster . MonsterId ] || monster . RoleStar == 0 {
return fmt . Errorf ( "CW saved monster identity/star differs from current season camp" )
}
wave . MonsterList = append ( wave . MonsterList , & pb . SceneMonster { MonsterId : monster . MonsterId , ExtraInfo : & pb . MNDMNDKLKIE { TeleportNice : & pb . MNDMNDKLKIE_EIGMKOJOEEI { EIGMKOJOEEI : & pb . HNEBODEBELG { RoleStar : monster . RoleStar }}}})
}
waves = append ( waves , wave )
}
info . StageId = node . StageId
info . MonsterWaveList = waves
grid := info . EIGMKOJOEEI
grid . IOCKDJCDMAH = & pb . MIPDJDJLGPL { EMOHLJHPOPC : stage . EMOHLJHPOPC , DCAHJELNJNP : stage . DCAHJELNJNP , SectionId : stage . SectionId , CJCCMAACJDB : node . CJCCMAACJDB }
2026-10-28 18:54:43 +07:00
grid . IFIIOIIKJHO = recipe . Penalty
2026-10-21 20:40:13 +07:00
grid . LODNFLMBIEH = division . EnemyHardLevel
grid . BattleWaveId = recipe . Waves [ 0 ]. ID
return nil
}