Builds / 1.15.91
Update 1.15.91
Compared with 1.15.88 · published Fri, 16 Oct 2026 02:01:12 GMT · live channel
Highlights
- Auto-upgrading of heroes and the castle now pauses while you have the hero or castle detail screen open, so it no longer changes levels under you while you upgrade manually.
- Auto-upgrades are saved to the server less often and in a safer order, which should cut down on sync conflicts and error popups.
Changes
- Hero and castle auto-upgrade now pauses for up to 60 seconds after you open the hero detail screen or the Castle Info popup, and resumes when you close it.
AutoLevelHold,HeroDetailPopup,CastleDetailPopup - Auto-upgrades now save in batches. A batch is sent only once no other server requests are in flight and at least 30 seconds have passed since the last save (previously a fixed 15 seconds).
HeroAutoController,CastleAutoController - Any other action that talks to the server now saves pending auto-upgrades first, so the server sees your levels before the new action. Hero and castle upgrade requests themselves are excluded.
BaseService,HeroAutoController,CastleAutoController - Pending auto-upgrades are now saved immediately when the game goes to the background or the tab is hidden.
HeroAutoController,CastleAutoController
Fixes
- Failed saves from auto-upgrades no longer show an error popup. Errors now appear only for upgrades you made by hand.
HeroItemRenderer,CastleItemRenderer - Failed auto-upgrade saves no longer reset the multi-level selection on the hero list.
HeroItemRenderer - Saving hero level-ups now skips heroes that are no longer in your roster. This appears to prevent failed saves caused by stale hero entries.
HeroItemRenderer
Technical
- Added a
shouldAutoFlush/shouldAutoBuy/isAutoLevelSaveUrlhelper set, plus a before-mutation hook list and pending-request counter onBaseService. - The auto controllers now use
markDirtyAutoandhasPendingon the item renderers instead of the debounced manual path. They also gainedflushNowandmaybeFlushmethods. - The Fishing Hunt page code grew substantially (about 43 KB to 109 KB) and the Fishing flow bundle grew by about 30 KB. All 14 top-down fishing hazard icons were removed (ghost, gust, ice, log, monster, rapid, reef, rock, storm, swell, thunder, vine, wave and the generic hazard icon). There are no matching text changes, so the player-facing effect is unclear.
FishingHuntPage,FishingFlow - Several other chunks were rebuilt with no or negligible size change.
- Version bumped to 1.15.91.
VersionManager
AI summary (claude-sonnet-5-5) written from the diffs below. Check the evidence before quoting it.
Assets 25
| File | Change | Size | Preview |
|---|---|---|---|
assets/chunks/ArcheryGamePopup.js |
changed | 27.9 KB → 27.9 KB | |
assets/chunks/ConquestPopup.js |
changed | 47.2 KB → 47.2 KB | |
assets/chunks/EventHubPopup.js |
changed | 4.8 KB → 4.8 KB | |
assets/chunks/FishingFlow.js |
changed | 880.1 KB → 911.0 KB | |
assets/chunks/FishingHuntPage.js |
changed | 43.6 KB → 108.8 KB | |
assets/chunks/FishingMiniWindow.js |
changed | 12.4 KB → 12.4 KB | |
assets/chunks/FriendPopup.js |
changed | 40.4 KB → 40.4 KB | |
assets/chunks/GuildWarBattlePopupV3.js |
changed | 113.9 KB → 113.9 KB | |
assets/chunks/MiniAscentFlow.js |
changed | 42.4 KB → 42.4 KB | |
assets/chunks/index.js |
changed | 1.7 KB → 1.7 KB | |
assets/chunks/vendor-pixi.js |
changed | 644.3 KB → 644.3 KB | |
assets/images/fishing/topdown/hazard_icons/ghost.webp |
removed | 13.1 KB → gone | |
assets/images/fishing/topdown/hazard_icons/gust.webp |
removed | 11.9 KB → gone | |
assets/images/fishing/topdown/hazard_icons/hazard.webp |
removed | 6.4 KB → gone | |
assets/images/fishing/topdown/hazard_icons/ice.webp |
removed | 8.9 KB → gone | |
assets/images/fishing/topdown/hazard_icons/log.webp |
removed | 11.4 KB → gone | |
assets/images/fishing/topdown/hazard_icons/monster.webp |
removed | 10.6 KB → gone | |
assets/images/fishing/topdown/hazard_icons/rapid.webp |
removed | 11.3 KB → gone | |
assets/images/fishing/topdown/hazard_icons/reef.webp |
removed | 12.4 KB → gone | |
assets/images/fishing/topdown/hazard_icons/rock.webp |
removed | 9.5 KB → gone | |
assets/images/fishing/topdown/hazard_icons/storm.webp |
removed | 9.6 KB → gone | |
assets/images/fishing/topdown/hazard_icons/swell.webp |
removed | 7.9 KB → gone | |
assets/images/fishing/topdown/hazard_icons/thunder.webp |
removed | 7.2 KB → gone | |
assets/images/fishing/topdown/hazard_icons/vine.webp |
removed | 13.5 KB → gone | |
assets/images/fishing/topdown/hazard_icons/wave.webp |
removed | 12.0 KB → gone |
Code 31 changes in 10 classes
Minified client code with short names blanked for diffing. Class names are real.
- AutoLevelHold 26
- BaseService 19
- CastleAutoController 16
- HeroItemRenderer 14
- HeroAutoController 12
- CastleItemRenderer 9
- HeroDetailPopup 3
- CastleDetailPopup 3
- VersionManager 1
- (module) 1
AutoLevelHold 2 hunks, 26 statements
- e(Dy,"maxAffordableLevel"),e(Py,"planEvenLevels"),e(xy,"planInOrder"); + function By(t,s,i,h){ + return t&&0===h&&s-i>=3e4} + function Uy(t,s){ + return!t&&0===s} + function Hy(t){ + const s=t.split("?")[0]; + return s.endsWith("/hero/levelUp")||s.endsWith("/castle/upgrade")} + e(Dy,"maxAffordableLevel"),e(Py,"planEvenLevels"),e(xy,"planInOrder"),e(By,"shouldAutoFlush"),e(Uy,"shouldAutoBuy"),e(Hy,"isAutoLevelSaveUrl");
- n(this,"frameCount",0),n(this,"frameRegistered",!1),n(this,"_enabled",!1),n(this,"lastFlushAt",0),n(this,"loadPromise",null),n(this,"touched",!1)} + n(this,"until",new Map)} + hold(s,i=Date.now()){ + this.until.set(s,i+t.HOLD_MS)} + release(t){ + this.until.delete(t)} + active(t=Date.now()){ + let s=!1; + return this.until.forEach((i,h)=>{ + t<i?s=!0:this.until.delete(h)} + ),s} + } + ; + e($y,"AutoLevelHold"),n($y,"HOLD_MS",6e4); + const Fy=new $y,Wy=class t{ + constructor(){ + n(this,"frameCount",0),n(this,"frameRegistered",!1),n(this,"_enabled",!1),n(this,"lastFlushAt",0),n(this,"loadPromise",null),n(this,"touched",!1),Zi.addBeforeMutation(t=>{ + !Hy(t)&&yy.hasPending&&(this.lastFlushAt=Date.now(),yy.cleanup())} + )}
BaseService 3 hunks, 19 statements
+ static get pendingMutations(){ + return t.pendingMutationCount} + static addBeforeMutation(s){ + t.beforeMutationHooks.includes(s)||t.beforeMutationHooks.push(s)}
- const s=t.mutationChain.then(a,a); - t.mutationChain=s.then(()=>{ + if(!t.runningBeforeMutation&&t.beforeMutationHooks.length>0){ + t.runningBeforeMutation=!0; + try{ + for(const i of t.beforeMutationHooks)i(s)} + catch(c){ + } + finally{ + t.runningBeforeMutation=!1} + } + t.pendingMutationCount++; + const i=e(()=>{ + t.pendingMutationCount=Math.max(0,t.pendingMutationCount-1)} + ,"settled"),h=t.mutationChain.then(a,a); + h.then(i,i),t.mutationChain=h.then(()=>{
- e(Ji,"BaseService"),n(Ji,"pendingRequests",new Map),n(Ji,"DUPLICATE_THRESHOLD",700),n(Ji,"REQUEST_TIMEOUT_MS",15e3),n(Ji,"mutationChain",Promise.resolve()),n(Ji,"READONLY_EXCEPTIONS",["getHeroInfo"]); + e(Ji,"BaseService"),n(Ji,"pendingRequests",new Map),n(Ji,"DUPLICATE_THRESHOLD",700),n(Ji,"REQUEST_TIMEOUT_MS",15e3),n(Ji,"mutationChain",Promise.resolve()),n(Ji,"pendingMutationCount",0),n(Ji,"beforeMutationHooks",[]),n(Ji,"runningBeforeMutation",!1),n(Ji,"READONLY_EXCEPTIONS",["getHeroInfo"]);
CastleAutoController 4 hunks, 16 statements
- n(this,"frameCount",0),n(this,"frameRegistered",!1),n(this,"_enabled",!1),n(this,"loadPromise",null),n(this,"touched",!1),n(this,"lastFlushAt",0)} + n(this,"frameCount",0),n(this,"frameRegistered",!1),n(this,"_enabled",!1),n(this,"loadPromise",null),n(this,"touched",!1),n(this,"lastFlushAt",0),Zi.addBeforeMutation(t=>{ + !Hy(t)&&jZ.hasPending&&(this.lastFlushAt=Date.now(),jZ.cleanup())} + )}
- if(!this._enabled)return; - if(!lo.isIdleBattle)return; - if(lo.pausedAt>0)return; - if(!B2.getInstance().getPlayer())return; + this._enabled&&lo.isIdleBattle&&(lo.pausedAt>0||W2.getInstance().getPlayer()&&Uy(Fy.active(),Zi.pendingMutations)&&(this.buy(),this.maybeFlush()))} + flushNow(){ + jZ.hasPending&&(this.lastFlushAt=Date.now(),jZ.cleanup())} + maybeFlush(){ + const t=Date.now(); + By(jZ.hasPending,t,this.lastFlushAt,Zi.pendingMutations)&&(this.lastFlushAt=t,jZ.cleanup())} + buy(){
- if(r>0){ - Oo.gold-=r,rd.getInstance().updateGold(),p0.getInstance().setData(),VZ.markDirtyAndSchedule(); - const s=Date.now(); - 0===this.lastFlushAt&&(this.lastFlushAt=s),s-this.lastFlushAt>=t.FLUSH_MS&&(this.lastFlushAt=s,VZ.cleanup())} - } + o>0&&(Oo.gold-=o,rd.getInstance().updateGold(),m0.getInstance().setData(),jZ.markDirtyAuto())}
- e(KZ,"CastleAutoController"),n(KZ,"instance"),n(KZ,"TICK_FRAMES",180),n(KZ,"UNLOCK_WAVE",100),n(KZ,"FLUSH_MS",15e3); + e(qZ,"CastleAutoController"),n(qZ,"instance"),n(qZ,"TICK_FRAMES",180),n(qZ,"UNLOCK_WAVE",100);
HeroItemRenderer 6 hunks, 14 statements
+ static get hasPending(){ + return t.isDirty} + static markDirtyAuto(){ + t.isDirty=!0,-1===t.numReviveSnapshot&&(t.numReviveSnapshot=Oo.numRevive)}
- t.isDirty=!0,-1===t.numReviveSnapshot&&(t.numReviveSnapshot=Oo.numRevive),null!==t.updateTimer&&clearTimeout(t.updateTimer),t.updateTimer=setTimeout(()=>{ + t.isDirty=!0,t.manualDirty=!0,-1===t.numReviveSnapshot&&(t.numReviveSnapshot=Oo.numRevive),null!==t.updateTimer&&clearTimeout(t.updateTimer),t.updateTimer=setTimeout(()=>{
- if(0===t.heroTargetLevels.size)return void(t.numReviveSnapshot=-1); + const s=t.manualDirty; + if(t.manualDirty=!1,0===t.heroTargetLevels.size)return void(t.numReviveSnapshot=-1);
- const i=[],h=[]; - s.forEach((t,s)=>{ - i.push(s),h.push(t)} - ); + const h=[],e=[],n=new Set(Xt.getInstance().list.filter(t=>t&&t.id>0).map(t=>t.id)); + if(i.forEach((t,s)=>{ + n.has(s)&&(h.push(s),e.push(t))} + ),0===h.length)return;
- return t.success&&(Oo.setUserInfo(t.body),rd.getInstance().updateAll(),_N.getInstance().setData()),void Ar.getInstance().showError(o)} - Oo.setUserInfo(o.body),rd.getInstance().updateGold(),_N.getInstance().isMultiLevel=!1,_N.getInstance().setMultiLevel(!1)} + return t.success&&(Oo.setUserInfo(t.body),rd.getInstance().updateAll(),EN.getInstance().setData()),void(s&&Ar.getInstance().showError(a))} + Oo.setUserInfo(a.body),rd.getInstance().updateGold(),s&&(EN.getInstance().isMultiLevel=!1,EN.getInstance().setMultiLevel(!1))}
- e(Ry,"HeroItemRenderer"),n(Ry,"updateTimer",null),n(Ry,"DEBOUNCE_DELAY",3e3),n(Ry,"isDirty",!1),n(Ry,"heroTargetLevels",new Map),n(Ry,"numReviveSnapshot",-1),n(Ry,"autoCloseCounter",0),n(Ry,"AUTO_CLOSE_SECONDS",5),n(Ry,"lastCounterUpdateTime",0); + e(Ry,"HeroItemRenderer"),n(Ry,"updateTimer",null),n(Ry,"DEBOUNCE_DELAY",3e3),n(Ry,"isDirty",!1),n(Ry,"heroTargetLevels",new Map),n(Ry,"numReviveSnapshot",-1),n(Ry,"manualDirty",!1),n(Ry,"autoCloseCounter",0),n(Ry,"AUTO_CLOSE_SECONDS",5),n(Ry,"lastCounterUpdateTime",0);
HeroAutoController 3 hunks, 12 statements
- if(!this._enabled)return; - if(!t.isUnlocked)return; - if(!lo.isIdleBattle)return; - if(lo.pausedAt>0)return; - if(!B2.getInstance().getPlayer())return; + this._enabled&&t.isUnlocked&&lo.isIdleBattle&&(lo.pausedAt>0||W2.getInstance().getPlayer()&&Uy(Fy.active(),Zi.pendingMutations)&&(this.buy(),this.maybeFlush()))} + flushNow(){ + yy.hasPending&&(this.lastFlushAt=Date.now(),yy.cleanup())} + maybeFlush(){ + const t=Date.now(); + By(yy.hasPending,t,this.lastFlushAt,Zi.pendingMutations)&&(this.lastFlushAt=t,yy.cleanup())} + buy(){
- if(a<=0)return; - Oo.gold-=a,rd.getInstance().updateGold(),l&&Ii.getInstance().recalculate(),yy.markDirtyAndSchedule(); - const c=Date.now(); - 0===this.lastFlushAt&&(this.lastFlushAt=c),c-this.lastFlushAt>=t.FLUSH_MS&&(this.lastFlushAt=c,yy.cleanup()),_N.getInstance().refreshLevels(),pR.getInstance().onConditionChanged()} + r<=0||(Oo.gold-=r,rd.getInstance().updateGold(),a&&Ii.getInstance().recalculate(),yy.markDirtyAuto(),EN.getInstance().refreshLevels(),pR.getInstance().onConditionChanged())}
- e(By,"HeroAutoController"),n(By,"instance"),n(By,"TICK_FRAMES",180),n(By,"FLUSH_MS",15e3),n(By,"UNLOCK_WAVE",1e3); + e(Wy,"HeroAutoController"),n(Wy,"instance"),n(Wy,"TICK_FRAMES",180),n(Wy,"UNLOCK_WAVE",1e3);
CastleItemRenderer 5 hunks, 9 statements
+ static get hasPending(){ + return t.isDirty} + static markDirtyAuto(){ + t.isDirty=!0}
- t.isDirty=!0,null!==t.updateTimer&&clearTimeout(t.updateTimer),t.updateTimer=setTimeout(()=>{ + t.isDirty=!0,t.manualDirty=!0,null!==t.updateTimer&&clearTimeout(t.updateTimer),t.updateTimer=setTimeout(()=>{
- if(!t.isDirty)return; + const s=t.manualDirty; + if(t.manualDirty=!1,!t.isDirty)return;
- return t.success&&(Oo.setUserInfo(t.body),rd.getInstance().updateAll(),p0.getInstance().setData(),FZ.getInstance().setData()),void Ar.getInstance().showError(e)} + return t.success&&(Oo.setUserInfo(t.body),rd.getInstance().updateAll(),m0.getInstance().setData(),zZ.getInstance().setData()),void(s&&Ar.getInstance().showError(n))}
- e(WZ,"CastleItemRenderer"),n(WZ,"updateTimer",null),n(WZ,"DEBOUNCE_DELAY",3e3),n(WZ,"isDirty",!1); + e(XZ,"CastleItemRenderer"),n(XZ,"updateTimer",null),n(XZ,"DEBOUNCE_DELAY",3e3),n(XZ,"isDirty",!1),n(XZ,"manualDirty",!1);
HeroDetailPopup 3 hunks, 3 statements
- if(1===s){ + if(Fy.hold("heroDetail"),1===s){
- this.tapTrans.visible=h,!h&&this.tapTrans.selected?this.onTapDetailClick():this.tapTrans.selected&&this.updateTransInfo(),this.applyTabButtonVisibility(),this.applyTabResourceMode(),this.updateLayout(),this.updateDraggableContainer(),this.show2()} + this.tapTrans.visible=h,!h&&this.tapTrans.selected?this.onTapDetailClick():this.tapTrans.selected&&this.updateTransInfo(),this.applyTabButtonVisibility(),this.applyTabResourceMode(),this.updateLayout(),this.updateDraggableContainer(),"list"!==this.mode&&"roster"!==this.mode||Fy.hold("heroDetail"),this.show2()}
- this.enhanceTimer&&this.syncEnhanceToServer(),this.multiEnhanceContainer.visible=!1,super.hide()} + this.enhanceTimer&&this.syncEnhanceToServer(),this.multiEnhanceContainer.visible=!1,Fy.release("heroDetail"),super.hide()}
CastleDetailPopup 3 hunks, 3 statements
- this.title=Nr.getText("GAME_86","성 정보"),this.lblName.text=Nr.getText("GAME_530","발키온 성"),this.btnEnhance.text=Nr.getText("GAME_66","강화"),this.btnReset.text=Nr.getText("COMMON_368","초기화"),this.enhanceTimer||this.syncInFlight||this.resetEnhance(),this.multiEnhanceContainer.visible=!1,t||(t=js.getInstance().castleVO),this.vo=t,this.updateCastleInfo(),this.updateLayout(),super.show(),pR.getInstance().registerPopupButtonGetter(t=>"btnEnhance"===t&&this.btnEnhance.visible?{ + this.title=Nr.getText("GAME_86","성 정보"),this.lblName.text=Nr.getText("GAME_530","발키온 성"),this.btnEnhance.text=Nr.getText("GAME_66","강화"),this.btnReset.text=Nr.getText("COMMON_368","초기화"),this.enhanceTimer||this.syncInFlight||this.resetEnhance(),this.multiEnhanceContainer.visible=!1,t||(t=js.getInstance().castleVO),this.vo=t,this.updateCastleInfo(),this.updateLayout(),super.show(),Fy.hold("castleDetail"),pR.getInstance().registerPopupButtonGetter(t=>"btnEnhance"===t&&this.btnEnhance.visible?{
- this.enhanceTimer&&(clearTimeout(this.enhanceTimer),this.enhanceTimer=null,this.syncEnhanceToServer()),this.multiEnhanceContainer.visible=!1,pR.getInstance().unregisterPopupButtonGetter(),super.hide()} + this.enhanceTimer&&(clearTimeout(this.enhanceTimer),this.enhanceTimer=null,this.syncEnhanceToServer()),this.multiEnhanceContainer.visible=!1,pR.getInstance().unregisterPopupButtonGetter(),Fy.release("castleDetail"),super.hide()}
- if(1===s){ + if(Fy.hold("castleDetail"),1===s){
VersionManager 1 hunks, 1 statements
- e(Z,"VersionManager"),n(Z,"gameId","ef2"),n(Z,"appVersion","1.15.88"); + e(Z,"VersionManager"),n(Z,"gameId","ef2"),n(Z,"appVersion","1.15.91");
(module) 1 hunks, 1 statements
- "hidden"===document.visibilityState?C0.cleanup():"visible"===document.visibilityState&&Yi.getInstance().checkDateRollover()} + "hidden"===document.visibilityState?(G0.cleanup(),Vy.getInstance().flushNow(),QZ.getInstance().flushNow()):"visible"===document.visibilityState&&Yi.getInstance().checkDateRollover()}