Skip to content

Commit a986982

Browse files
StopDragonclaude
andcommitted
feat: Add sword-type enhance stats, Windows multiline, upset 1-20
Major changes: - Windows overlay: DrawTextW multiline support (fixes text truncation) - Battle upset: Expand level diff 1-20 (was 1-3) - Telemetry: Add SwordEnhanceStats for per-sword success/destroy rates - API: Add /api/stats/enhance endpoint - Remove OCR/capture modules (use clipboard-based text reading) - Add Windows compatibility report doc Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c44b4d0 commit a986982

File tree

21 files changed

+1829
-790
lines changed

21 files changed

+1829
-790
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ build/
2121

2222
# Windows
2323
*.exe
24+
25+
# Build binaries
26+
sword-api
27+
sword-macro

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
1. 카카오톡에서 검키우기 채팅방 열기
4747
2. 매크로 실행 → 모드 선택
48-
3. 카카오톡 **메시지 입력칸**에 마우스 올리기 → 3초 후 좌표 자동 저장
48+
3. 카카오톡 **메시지 입력칸 '메시지 입력'**에 마우스 올리기 → 3초 후 좌표 자동 저장
4949
4. 매크로 시작
5050

5151
## 설정

cmd/sword-api/main.go

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ type TelemetryStats struct {
156156
HiddenFoundByName map[string]int `json:"hidden_found_by_name,omitempty"`
157157
UpsetStatsByDiff map[int]*UpsetStat `json:"upset_stats_by_diff,omitempty"`
158158
SwordSaleStats map[string]*SwordSaleStat `json:"sword_sale_stats,omitempty"`
159+
SwordEnhanceStats map[string]*SwordEnhanceStat `json:"sword_enhance_stats,omitempty"`
159160
ItemFarmingStats map[string]*ItemFarmingStat `json:"item_farming_stats,omitempty"`
160161
}
161162

@@ -182,6 +183,14 @@ type SwordSaleStat struct {
182183
Count int `json:"count"`
183184
}
184185

186+
// SwordEnhanceStat 검 종류별 강화 통계
187+
type SwordEnhanceStat struct {
188+
Attempts int `json:"attempts"`
189+
Success int `json:"success"`
190+
Fail int `json:"fail"`
191+
Destroy int `json:"destroy"`
192+
}
193+
185194
// ItemFarmingStat 아이템별 파밍 통계
186195
type ItemFarmingStat struct {
187196
TotalCount int `json:"total_count"`
@@ -224,6 +233,7 @@ type StatsStore struct {
224233
hiddenFoundByName map[string]int
225234
upsetStatsByDiff map[int]*UpsetStat
226235
swordSaleStats map[string]*SwordSaleStat
236+
swordEnhanceStats map[string]*SwordEnhanceStat
227237
itemFarmingStats map[string]*ItemFarmingStat
228238
}
229239

@@ -233,6 +243,7 @@ var stats = &StatsStore{
233243
hiddenFoundByName: make(map[string]int),
234244
upsetStatsByDiff: make(map[int]*UpsetStat),
235245
swordSaleStats: make(map[string]*SwordSaleStat),
246+
swordEnhanceStats: make(map[string]*SwordEnhanceStat),
236247
itemFarmingStats: make(map[string]*ItemFarmingStat),
237248
}
238249

@@ -278,9 +289,27 @@ func getGameData() GameData {
278289
{Level: 15, MinPrice: 30000000, MaxPrice: 40000000, AvgPrice: 35000000},
279290
},
280291
BattleRewards: []BattleReward{
292+
// 레벨 차이가 클수록 승률↓ 보상↑
281293
{LevelDiff: 1, WinRate: 35.0, MinReward: 500, MaxReward: 1500, AvgReward: 1000},
282294
{LevelDiff: 2, WinRate: 20.0, MinReward: 1500, MaxReward: 4000, AvgReward: 2750},
283295
{LevelDiff: 3, WinRate: 10.0, MinReward: 4000, MaxReward: 10000, AvgReward: 7000},
296+
{LevelDiff: 4, WinRate: 5.0, MinReward: 10000, MaxReward: 25000, AvgReward: 17500},
297+
{LevelDiff: 5, WinRate: 3.0, MinReward: 25000, MaxReward: 60000, AvgReward: 42500},
298+
{LevelDiff: 6, WinRate: 2.0, MinReward: 60000, MaxReward: 140000, AvgReward: 100000},
299+
{LevelDiff: 7, WinRate: 1.5, MinReward: 140000, MaxReward: 300000, AvgReward: 220000},
300+
{LevelDiff: 8, WinRate: 1.0, MinReward: 300000, MaxReward: 600000, AvgReward: 450000},
301+
{LevelDiff: 9, WinRate: 0.7, MinReward: 600000, MaxReward: 1200000, AvgReward: 900000},
302+
{LevelDiff: 10, WinRate: 0.5, MinReward: 1200000, MaxReward: 2500000, AvgReward: 1850000},
303+
{LevelDiff: 11, WinRate: 0.35, MinReward: 2500000, MaxReward: 5000000, AvgReward: 3750000},
304+
{LevelDiff: 12, WinRate: 0.25, MinReward: 5000000, MaxReward: 10000000, AvgReward: 7500000},
305+
{LevelDiff: 13, WinRate: 0.18, MinReward: 10000000, MaxReward: 20000000, AvgReward: 15000000},
306+
{LevelDiff: 14, WinRate: 0.12, MinReward: 20000000, MaxReward: 40000000, AvgReward: 30000000},
307+
{LevelDiff: 15, WinRate: 0.08, MinReward: 40000000, MaxReward: 80000000, AvgReward: 60000000},
308+
{LevelDiff: 16, WinRate: 0.05, MinReward: 80000000, MaxReward: 150000000, AvgReward: 115000000},
309+
{LevelDiff: 17, WinRate: 0.03, MinReward: 150000000, MaxReward: 300000000, AvgReward: 225000000},
310+
{LevelDiff: 18, WinRate: 0.02, MinReward: 300000000, MaxReward: 500000000, AvgReward: 400000000},
311+
{LevelDiff: 19, WinRate: 0.01, MinReward: 500000000, MaxReward: 800000000, AvgReward: 650000000},
312+
{LevelDiff: 20, WinRate: 0.005, MinReward: 800000000, MaxReward: 1000000000, AvgReward: 900000000},
284313
},
285314
UpdatedAt: time.Now().Format(time.RFC3339),
286315
}
@@ -404,6 +433,17 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
404433
stats.swordSaleStats[key].Count += stat.Count
405434
}
406435

436+
// 검 강화 통계
437+
for name, stat := range payload.Stats.SwordEnhanceStats {
438+
if stats.swordEnhanceStats[name] == nil {
439+
stats.swordEnhanceStats[name] = &SwordEnhanceStat{}
440+
}
441+
stats.swordEnhanceStats[name].Attempts += stat.Attempts
442+
stats.swordEnhanceStats[name].Success += stat.Success
443+
stats.swordEnhanceStats[name].Fail += stat.Fail
444+
stats.swordEnhanceStats[name].Destroy += stat.Destroy
445+
}
446+
407447
// 아이템 파밍 통계
408448
for name, stat := range payload.Stats.ItemFarmingStats {
409449
if stats.itemFarmingStats[name] == nil {
@@ -583,11 +623,12 @@ func handleUpsetStats(w http.ResponseWriter, r *http.Request) {
583623
stats.mu.RLock()
584624
defer stats.mu.RUnlock()
585625

586-
// 이론 승률
626+
// 이론 승률 (레벨 차이 1-20)
587627
theoryRates := map[int]float64{
588-
1: 35.0,
589-
2: 20.0,
590-
3: 10.0,
628+
1: 35.0, 2: 20.0, 3: 10.0, 4: 5.0, 5: 3.0,
629+
6: 2.0, 7: 1.5, 8: 1.0, 9: 0.7, 10: 0.5,
630+
11: 0.35, 12: 0.25, 13: 0.18, 14: 0.12, 15: 0.08,
631+
16: 0.05, 17: 0.03, 18: 0.02, 19: 0.01, 20: 0.005,
591632
}
592633

593634
type DiffStat struct {
@@ -599,7 +640,7 @@ func handleUpsetStats(w http.ResponseWriter, r *http.Request) {
599640
}
600641

601642
byDiff := make(map[string]DiffStat)
602-
for diff := 1; diff <= 3; diff++ {
643+
for diff := 1; diff <= 20; diff++ {
603644
stat := stats.upsetStatsByDiff[diff]
604645
winRate := 0.0
605646
attempts := 0
@@ -664,6 +705,51 @@ func handleItemStats(w http.ResponseWriter, r *http.Request) {
664705
})
665706
}
666707

708+
// 검 종류별 강화 성공률
709+
func handleEnhanceStats(w http.ResponseWriter, r *http.Request) {
710+
w.Header().Set("Content-Type", "application/json")
711+
w.Header().Set("Access-Control-Allow-Origin", "*")
712+
713+
stats.mu.RLock()
714+
defer stats.mu.RUnlock()
715+
716+
type EnhanceEntry struct {
717+
Name string `json:"name"`
718+
Attempts int `json:"attempts"`
719+
Success int `json:"success"`
720+
Fail int `json:"fail"`
721+
Destroy int `json:"destroy"`
722+
SuccessRate float64 `json:"success_rate"`
723+
DestroyRate float64 `json:"destroy_rate"`
724+
}
725+
726+
var swords []EnhanceEntry
727+
for name, stat := range stats.swordEnhanceStats {
728+
successRate := 0.0
729+
destroyRate := 0.0
730+
if stat.Attempts > 0 {
731+
successRate = float64(stat.Success) / float64(stat.Attempts) * 100
732+
destroyRate = float64(stat.Destroy) / float64(stat.Attempts) * 100
733+
}
734+
swords = append(swords, EnhanceEntry{
735+
Name: name,
736+
Attempts: stat.Attempts,
737+
Success: stat.Success,
738+
Fail: stat.Fail,
739+
Destroy: stat.Destroy,
740+
SuccessRate: successRate,
741+
DestroyRate: destroyRate,
742+
})
743+
}
744+
745+
json.NewEncoder(w).Encode(map[string]interface{}{
746+
"total_attempts": stats.enhanceAttempts,
747+
"total_success": stats.enhanceSuccess,
748+
"total_destroy": stats.enhanceDestroy,
749+
"swords": swords,
750+
})
751+
}
752+
667753
func generateSignature(sessionID, period string) string {
668754
h := sha256.Sum256([]byte(sessionID + period + getAppSecret()))
669755
return hex.EncodeToString(h[:])[:16]
@@ -714,6 +800,9 @@ func validateTelemetryPayload(p *TelemetryPayload) error {
714800
if len(p.Stats.SwordSaleStats) > maxMapEntries {
715801
return fmt.Errorf("sword_sale_stats too many entries")
716802
}
803+
if len(p.Stats.SwordEnhanceStats) > maxMapEntries {
804+
return fmt.Errorf("sword_enhance_stats too many entries")
805+
}
717806
if len(p.Stats.ItemFarmingStats) > maxMapEntries {
718807
return fmt.Errorf("item_farming_stats too many entries")
719808
}
@@ -734,6 +823,11 @@ func validateTelemetryPayload(p *TelemetryPayload) error {
734823
return fmt.Errorf("item name too long: %s", name)
735824
}
736825
}
826+
for name := range p.Stats.SwordEnhanceStats {
827+
if len(name) > maxSwordNameLen {
828+
return fmt.Errorf("enhance sword name too long: %s", name)
829+
}
830+
}
737831

738832
return nil
739833
}
@@ -769,9 +863,9 @@ func validateStatValues(s *TelemetryStats) error {
769863
}
770864
}
771865

772-
// 역배 레벨차 검증
866+
// 역배 레벨차 검증 (1-20 허용)
773867
for diff, stat := range s.UpsetStatsByDiff {
774-
if diff < 1 || diff > 10 {
868+
if diff < 1 || diff > 20 {
775869
return fmt.Errorf("invalid upset level diff: %d", diff)
776870
}
777871
if stat != nil && (stat.Attempts < 0 || stat.Wins < 0 || stat.GoldEarned < 0) {
@@ -799,6 +893,7 @@ func main() {
799893
http.HandleFunc("/api/stats/hidden", handleHiddenStats)
800894
http.HandleFunc("/api/stats/upset", handleUpsetStats)
801895
http.HandleFunc("/api/stats/items", handleItemStats)
896+
http.HandleFunc("/api/stats/enhance", handleEnhanceStats)
802897

803898
log.Printf("🚀 Sword API 서버 시작 (포트: %s)", port)
804899
log.Printf(" /api/game-data - 게임 데이터 조회")
@@ -808,6 +903,7 @@ func main() {
808903
log.Printf(" /api/stats/hidden - 히든 검 출현 확률 (v2)")
809904
log.Printf(" /api/stats/upset - 역배 실측 승률 (v2)")
810905
log.Printf(" /api/stats/items - 아이템 파밍 통계 (v2)")
906+
log.Printf(" /api/stats/enhance - 검 종류별 강화 성공률 (v2)")
811907

812908
if err := http.ListenAndServe(":"+port, nil); err != nil {
813909
log.Fatal(err)

cmd/sword-macro/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"os/signal"
7+
"runtime"
78
"syscall"
89

910
"github.com/StopDragon/sword-macro-ai/internal/config"
@@ -12,6 +13,11 @@ import (
1213
"github.com/StopDragon/sword-macro-ai/internal/telemetry"
1314
)
1415

16+
func init() {
17+
// macOS에서 Cocoa UI를 사용하려면 메인 스레드 고정 필요
18+
runtime.LockOSThread()
19+
}
20+
1521
const VERSION = "2.0.0"
1622

1723
func main() {

0 commit comments

Comments
 (0)