diff --git a/includes/attribute_system.inc b/includes/attribute_system.inc new file mode 100644 index 0000000..f81f6bc --- /dev/null +++ b/includes/attribute_system.inc @@ -0,0 +1,1674 @@ +/** + * ============================================================================= + * TF2 Level System - Attribute System Module + * 속성 시스템 모듈 + * + * 원본 levelup.sp의 140+ 중복 코드 블록을 일반화된 함수로 최적화 + * 12개 개별 AttributeTable 배열을 2차원 배열로 통합하여 메모리 효율 개선 + * ============================================================================= + */ + +#if defined _tf2level_attribute_system_included + #endinput +#endif +#define _tf2level_attribute_system_included + +// ============================================================================= +// 상수 정의 (공통 상수는 메인 파일에 정의) +// ============================================================================= +// CLASS_SCOUT, CLASS_MEDIC 등의 클래스 상수는 메인 파일에 정의 +// ADDITIVE_PERCENT 등의 모드 상수도 메인 파일에 정의 +// MAX_CLASSES, MAX_ATTRIBUTES도 메인 파일에 정의 + +// ============================================================================= +// 데이터 구조체 +// ============================================================================= + +/** + * 속성 테이블 구조체 + * 각 클래스의 스킬/속성 정의를 저장 + */ +enum struct AttributeTable +{ + char uid[64]; // TF2 속성 고유 ID + char title[80]; // 메뉴에 표시될 제목 + int class; // 클래스 ID + int max; // 최대 업그레이드 레벨 + int point; // 업그레이드당 필요 포인트 + float value; // 업그레이드당 증가값 + float defaultValue; // 기본값 + int additiveMode; // 적용 모드 + bool isDisableDrawValue; // 값 표시 비활성화 (ON/OFF 스킬용) +} + +// ============================================================================= +// 전역 변수 +// ============================================================================= + +// 속성 테이블 (2차원 배열로 통합) +AttributeTable g_attributeTable[MAX_CLASSES][MAX_ATTRIBUTES]; + +// 클래스별 속성 개수 +int g_attributeCount[MAX_CLASSES] = { + 13, // Scout + 14, // Medic + 13, // Soldier + 13, // Pyro + 16, // Spy + 16, // Demoman + 15, // Sniper + 17, // Engineer + 14, // Heavy + 3, // Hale + 2, // Shared + 1 // Weapon +}; + +// ConVar +ConVar g_redEnableStatApply; +ConVar g_blueEnableStatApply; + +// ============================================================================= +// 초기화 함수 +// ============================================================================= + +/** + * 속성 시스템 초기화 + * OnPluginStart()에서 호출 + */ +stock void Attribute_Initialize() +{ + // ConVar 등록 + g_redEnableStatApply = CreateConVar("levelup_red_enable_stat", "1", "레드팀 속성 적용 활성화"); + g_blueEnableStatApply = CreateConVar("levelup_blue_enable_stat", "0", "블루팀 속성 적용 활성화"); + + // 속성 테이블 생성 + CreateAttributeTable(); +} + +/** + * 속성 계산 함수 + * 업그레이드 레벨에 따라 최종 속성값 계산 + * + * @param attr 속성 테이블 + * @param upgrade 업그레이드 레벨 + * @return 계산된 속성값 + */ +stock float Attribute_Calculate(AttributeTable attr, int upgrade) +{ + if (upgrade <= 0) + return attr.defaultValue; + + float result = 0.0; + + switch (attr.additiveMode) + { + case ADDITIVE_NUMBER: + { + // 고정값 더하기: defaultValue + (upgrade * value) + result = attr.defaultValue + (float(upgrade) * attr.value); + } + case ADDITIVE_PERCENT: + { + // 퍼센트 더하기: (defaultValue * 0.01) + (upgrade * value * 0.01) + result = (attr.defaultValue * 0.01) + (float(upgrade) * attr.value * 0.01); + } + case MINUS_NUMBER: + { + // 고정값 빼기: defaultValue - (upgrade * value) + result = attr.defaultValue - (float(upgrade) * attr.value); + } + case MINUS_PERCENT: + { + // 퍼센트 빼기: (defaultValue * 0.01) - (upgrade * value * 0.01) + result = (attr.defaultValue * 0.01) - (float(upgrade) * attr.value * 0.01); + } + default: + { + result = attr.defaultValue; + } + } + + return result; +} + +/** + * 플레이어에게 속성 적용 + * OnPlayerSpawn 이벤트에서 호출 + * + * @param client 플레이어 인덱스 + * @param tfClass TF2 클래스 (TFClass_Scout, TFClass_Soldier 등) + */ +stock void Attribute_ApplyToPlayer(int client, TFClassType tfClass) +{ + if (!IsValidClient(client)) + return; + + // 팀 확인 + int team = GetClientTeam(client); + if (team == 2) // 레드팀 + { + if (!g_redEnableStatApply.BoolValue) + { + TF2Attrib_RemoveAll(client); + return; + } + } + else if (team == 3) // 블루팀 + { + if (!g_blueEnableStatApply.BoolValue) + { + TF2Attrib_RemoveAll(client); + return; + } + } + + // 기존 속성 제거 + TF2Attrib_RemoveAll(client); + + // TFClassType을 CLASS_ 인덱스로 변환 + int classIdx = TFClassToIndex(tfClass); + if (classIdx < 0) + return; + + // 해당 클래스의 모든 속성 적용 + int attrCount = g_attributeCount[classIdx]; + for (int i = 0; i < attrCount; i++) + { + // 플레이어의 업그레이드 레벨 가져오기 (외부 player_data.inc에서) + int upgrade = PlayerData_GetAttributeUpgrade(client, classIdx, i); + + if (upgrade <= 0) + continue; + + // 속성값 계산 + float result = Attribute_Calculate(g_attributeTable[classIdx][i], upgrade); + + // TF2 속성 적용 + TF2Attrib_SetByName(client, g_attributeTable[classIdx][i].uid, result); + } + + // 공용 속성은 0.5초 후 적용 (타이머 사용) + CreateTimer(0.5, Timer_ApplySharedAttribute, client, TIMER_FLAG_NO_MAPCHANGE); +} + +/** + * 공용 속성 적용 타이머 + */ +public Action Timer_ApplySharedAttribute(Handle timer, any client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) + return Plugin_Stop; + + // SHARED 클래스 속성 적용 + int sharedCount = g_attributeCount[CLASS_SHARED]; + for (int i = 0; i < sharedCount; i++) + { + int upgrade = PlayerData_GetAttributeUpgrade(client, CLASS_SHARED, i); + if (upgrade <= 0) + continue; + + float result = Attribute_Calculate(g_attributeTable[CLASS_SHARED][i], upgrade); + TF2Attrib_SetByName(client, g_attributeTable[CLASS_SHARED][i].uid, result); + } + + // 무기 속성 적용 + Attribute_ApplyToWeapons(client); + + return Plugin_Stop; +} + +/** + * 플레이어의 모든 무기에 속성 적용 + */ +stock void Attribute_ApplyToWeapons(int client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) + return; + + int weaponCount = g_attributeCount[CLASS_WEAPON]; + + // 모든 무기 슬롯 순회 + for (int slot = 0; slot < 6; slot++) + { + int weapon = GetPlayerWeaponSlot(client, slot); + if (!IsValidEntity(weapon)) + continue; + + // 무기에 속성 적용 + for (int i = 0; i < weaponCount; i++) + { + int upgrade = PlayerData_GetAttributeUpgrade(client, CLASS_WEAPON, i); + if (upgrade <= 0) + continue; + + float result = Attribute_Calculate(g_attributeTable[CLASS_WEAPON][i], upgrade); + TF2Attrib_SetByName(weapon, g_attributeTable[CLASS_WEAPON][i].uid, result); + } + } +} + +/** + * TFClassType을 CLASS_ 인덱스로 변환 + */ +stock int TFClassToIndex(TFClassType tfClass) +{ + switch (tfClass) + { + case TFClass_Scout: return CLASS_SCOUT; + case TFClass_Medic: return CLASS_MEDIC; + case TFClass_Soldier: return CLASS_SOLDIER; + case TFClass_Pyro: return CLASS_PYRO; + case TFClass_Spy: return CLASS_SPY; + case TFClass_DemoMan: return CLASS_DEMOMAN; + case TFClass_Sniper: return CLASS_SNIPER; + case TFClass_Engineer: return CLASS_ENGINEER; + case TFClass_Heavy: return CLASS_HEAVY; + default: return -1; + } +} + +/** + * 속성 테이블 접근 함수 + */ +stock AttributeTable Attribute_GetTable(int classIdx, int attrIdx) +{ + return g_attributeTable[classIdx][attrIdx]; +} + +stock int Attribute_GetCount(int classIdx) +{ + return g_attributeCount[classIdx]; +} + +/** + * 유효한 클라이언트 체크 + */ +stock bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client)); +} + +// ============================================================================= +// 속성 테이블 생성 +// 원본 levelup.sp의 CreateAttributeTable() 함수를 그대로 이식 +// ============================================================================= + +void CreateAttributeTable() +{ +{ + scoutAttributeTable[0].class = CLASS_SCOUT; + scoutAttributeTable[0].max = 5; + scoutAttributeTable[0].point = 1; + scoutAttributeTable[0].uid = "move speed bonus"; + scoutAttributeTable[0].value = float(3); + scoutAttributeTable[0].defaultValue = float(100); + scoutAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[1].class = CLASS_SCOUT; + scoutAttributeTable[1].max = 10; + scoutAttributeTable[1].point = 1; + scoutAttributeTable[1].uid = "increased jump height"; + scoutAttributeTable[1].value = float(5); + scoutAttributeTable[1].defaultValue = float(100); + scoutAttributeTable[1].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[1].title = "점프높이 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[2].class = CLASS_SCOUT; + scoutAttributeTable[2].max = 10; + scoutAttributeTable[2].point = 1; + scoutAttributeTable[2].uid = "fire rate bonus"; + scoutAttributeTable[2].value = float(5); + scoutAttributeTable[2].defaultValue = float(100); + scoutAttributeTable[2].additiveMode = MINUS_PERCENT; + scoutAttributeTable[2].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[3].class = CLASS_SCOUT; + scoutAttributeTable[3].max = 10; + scoutAttributeTable[3].point = 1; + scoutAttributeTable[3].uid = "Reload time decreased"; + scoutAttributeTable[3].value = float(5); + scoutAttributeTable[3].defaultValue = float(100); + scoutAttributeTable[3].additiveMode = MINUS_PERCENT; + scoutAttributeTable[3].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[4].class = CLASS_SCOUT; + scoutAttributeTable[4].max = 3; + scoutAttributeTable[4].point = 1; + scoutAttributeTable[4].uid = "deploy time decreased"; + scoutAttributeTable[4].value = float(15); + scoutAttributeTable[4].defaultValue = float(100); + scoutAttributeTable[4].additiveMode = MINUS_PERCENT; + scoutAttributeTable[4].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[5].class = CLASS_SCOUT; + scoutAttributeTable[5].max = 5; + scoutAttributeTable[5].point = 1; + scoutAttributeTable[5].uid = "heal on hit for rapidfire"; + scoutAttributeTable[5].value = float(2); + scoutAttributeTable[5].defaultValue = float(0); + scoutAttributeTable[5].additiveMode = ADDITIVE_NUMBER; + scoutAttributeTable[5].title = "적중 시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + scoutAttributeTable[6].class = CLASS_SCOUT; + scoutAttributeTable[6].max = 5; + scoutAttributeTable[6].point = 2; + scoutAttributeTable[6].uid = "damage bonus HIDDEN"; + scoutAttributeTable[6].value = float(1); + scoutAttributeTable[6].defaultValue = float(100); + scoutAttributeTable[6].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[6].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[7].class = CLASS_SCOUT; + scoutAttributeTable[7].max = 5; + scoutAttributeTable[7].point = 2; + scoutAttributeTable[7].uid = "effect bar recharge rate increased"; + scoutAttributeTable[7].value = float(5); + scoutAttributeTable[7].defaultValue = float(100); + scoutAttributeTable[7].additiveMode = MINUS_PERCENT; + scoutAttributeTable[7].title = "재충전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[8].class = CLASS_SCOUT; + scoutAttributeTable[8].max = 5; + scoutAttributeTable[8].point = 2; + scoutAttributeTable[8].uid = "max health additive bonus"; + scoutAttributeTable[8].value = float(10); + scoutAttributeTable[8].defaultValue = float(0); + scoutAttributeTable[8].additiveMode = ADDITIVE_NUMBER; + scoutAttributeTable[8].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[9].class = CLASS_SCOUT; + scoutAttributeTable[9].max = 4; + scoutAttributeTable[9].point = 3; + scoutAttributeTable[9].uid = "clip size bonus"; + scoutAttributeTable[9].value = float(50); + scoutAttributeTable[9].defaultValue = float(100); + scoutAttributeTable[9].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[9].title = "장탄수 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[10].class = CLASS_SCOUT; + scoutAttributeTable[10].max = 2; + scoutAttributeTable[10].point = 3; + scoutAttributeTable[10].uid = "weapon spread bonus"; + scoutAttributeTable[10].value = float(25); + scoutAttributeTable[10].defaultValue = float(0); + scoutAttributeTable[10].additiveMode = MINUS_PERCENT; + scoutAttributeTable[10].title = "집탄률 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[11].class = CLASS_SCOUT; + scoutAttributeTable[11].max = 2; + scoutAttributeTable[11].point = 4; + scoutAttributeTable[11].uid = "bullets per shot bonus"; + scoutAttributeTable[11].value = float(50); + scoutAttributeTable[11].defaultValue = float(100); + scoutAttributeTable[11].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[11].title = "발사되는 탄환수 %.0f%% 증가 (%dpt)(%d/%d)"; + + scoutAttributeTable[12].class = CLASS_SCOUT; + scoutAttributeTable[12].max = 1; + scoutAttributeTable[12].point = 5; + scoutAttributeTable[12].uid = "cancel falling damage"; + scoutAttributeTable[12].value = float(100); + scoutAttributeTable[12].defaultValue = float(0); + scoutAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + scoutAttributeTable[12].isDisableDrawValue = true; + scoutAttributeTable[12].title = "낙하 피해 무시 (%dpt)(%d/%d)"; + + soldierAttributeTable[0].class = CLASS_SOLDIER; + soldierAttributeTable[0].max = 5; + soldierAttributeTable[0].point = 1; + soldierAttributeTable[0].uid = "move speed bonus"; + soldierAttributeTable[0].value = float(3); + soldierAttributeTable[0].defaultValue = float(100); + soldierAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[1].class = CLASS_SOLDIER; + soldierAttributeTable[1].max = 10; + soldierAttributeTable[1].point = 1; + soldierAttributeTable[1].uid = "fire rate bonus"; + soldierAttributeTable[1].value = float(5); + soldierAttributeTable[1].defaultValue = float(100); + soldierAttributeTable[1].additiveMode = MINUS_PERCENT; + soldierAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[2].class = CLASS_SOLDIER; + soldierAttributeTable[2].max = 10; + soldierAttributeTable[2].point = 1; + soldierAttributeTable[2].uid = "Reload time decreased"; + soldierAttributeTable[2].value = float(5); + soldierAttributeTable[2].defaultValue = float(100); + soldierAttributeTable[2].additiveMode = MINUS_PERCENT; + soldierAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[3].class = CLASS_SOLDIER; + soldierAttributeTable[3].max = 3; + soldierAttributeTable[3].point = 1; + soldierAttributeTable[3].uid = "deploy time decreased"; + soldierAttributeTable[3].value = float(15); + soldierAttributeTable[3].defaultValue = float(100); + soldierAttributeTable[3].additiveMode = MINUS_PERCENT; + soldierAttributeTable[3].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[4].class = CLASS_SOLDIER; + soldierAttributeTable[4].max = 5; + soldierAttributeTable[4].point = 1; + soldierAttributeTable[4].uid = "heal on hit for rapidfire"; + soldierAttributeTable[4].value = float(5); + soldierAttributeTable[4].defaultValue = float(0); + soldierAttributeTable[4].additiveMode = ADDITIVE_NUMBER; + soldierAttributeTable[4].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + soldierAttributeTable[5].class = CLASS_SOLDIER; + soldierAttributeTable[5].max = 5; + soldierAttributeTable[5].point = 2; + soldierAttributeTable[5].uid = "damage bonus HIDDEN"; + soldierAttributeTable[5].value = float(1); + soldierAttributeTable[5].defaultValue = float(100); + soldierAttributeTable[5].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[5].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[6].class = CLASS_SOLDIER; + soldierAttributeTable[6].max = 10; + soldierAttributeTable[6].point = 2; + soldierAttributeTable[6].uid = "rocket jump damage reduction"; + soldierAttributeTable[6].value = float(5); + soldierAttributeTable[6].defaultValue = float(100); + soldierAttributeTable[6].additiveMode = MINUS_PERCENT; + soldierAttributeTable[6].title = "로켓점프 피해 %.0f%% 감소 (%dpt)(%d/%d)"; + + soldierAttributeTable[7].class = CLASS_SOLDIER; + soldierAttributeTable[7].max = 5; + soldierAttributeTable[7].point = 2; + soldierAttributeTable[7].uid = "Blast radius increased"; + soldierAttributeTable[7].value = float(10); + soldierAttributeTable[7].defaultValue = float(100); + soldierAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[7].title = "폭발반경 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[8].class = CLASS_SOLDIER; + soldierAttributeTable[8].max = 5; + soldierAttributeTable[8].point = 2; + soldierAttributeTable[8].uid = "Projectile speed increased"; + soldierAttributeTable[8].value = float(10); + soldierAttributeTable[8].defaultValue = float(100); + soldierAttributeTable[8].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[8].title = "투사체속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[9].class = CLASS_SOLDIER; + soldierAttributeTable[9].max = 5; + soldierAttributeTable[9].point = 2; + soldierAttributeTable[9].uid = "max health additive bonus"; + soldierAttributeTable[9].value = float(10); + soldierAttributeTable[9].defaultValue = float(0); + soldierAttributeTable[9].additiveMode = ADDITIVE_NUMBER; + soldierAttributeTable[9].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[10].class = CLASS_SOLDIER; + soldierAttributeTable[10].max = 4; + soldierAttributeTable[10].point = 3; + soldierAttributeTable[10].uid = "clip size bonus"; + soldierAttributeTable[10].value = float(25); + soldierAttributeTable[10].defaultValue = float(100); + soldierAttributeTable[10].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[10].title = "장탄수 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[11].class = CLASS_SOLDIER; + soldierAttributeTable[11].max = 4; + soldierAttributeTable[11].point = 3; + soldierAttributeTable[11].uid = "increase buff duration"; + soldierAttributeTable[11].value = float(25); + soldierAttributeTable[11].defaultValue = float(100); + soldierAttributeTable[11].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[11].title = "깃발유지시간 %.0f%% 증가 (%dpt)(%d/%d)"; + + soldierAttributeTable[12].class = CLASS_SOLDIER; + soldierAttributeTable[12].max = 1; + soldierAttributeTable[12].point = 5; + soldierAttributeTable[12].uid = "rocket specialist"; + soldierAttributeTable[12].value = float(100); + soldierAttributeTable[12].defaultValue = float(0); + soldierAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + soldierAttributeTable[12].isDisableDrawValue = true; + soldierAttributeTable[12].title = "로켓특화 (%dpt)(%d/%d)"; + + pyroAttributeTable[0].class = CLASS_PYRO; + pyroAttributeTable[0].max = 5; + pyroAttributeTable[0].point = 1; + pyroAttributeTable[0].uid = "move speed bonus"; + pyroAttributeTable[0].value = float(3); + pyroAttributeTable[0].defaultValue = float(100); + pyroAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + pyroAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[1].class = CLASS_PYRO; + pyroAttributeTable[1].max = 10; + pyroAttributeTable[1].point = 1; + pyroAttributeTable[1].uid = "fire rate bonus"; + pyroAttributeTable[1].value = float(5); + pyroAttributeTable[1].defaultValue = float(100); + pyroAttributeTable[1].additiveMode = MINUS_PERCENT; + pyroAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[2].class = CLASS_PYRO; + pyroAttributeTable[2].max = 10; + pyroAttributeTable[2].point = 1; + pyroAttributeTable[2].uid = "Reload time decreased"; + pyroAttributeTable[2].value = float(5); + pyroAttributeTable[2].defaultValue = float(100); + pyroAttributeTable[2].additiveMode = MINUS_PERCENT; + pyroAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[3].class = CLASS_PYRO; + pyroAttributeTable[3].max = 3; + pyroAttributeTable[3].point = 1; + pyroAttributeTable[3].uid = "deploy time decreased"; + pyroAttributeTable[3].value = float(15); + pyroAttributeTable[3].defaultValue = float(100); + pyroAttributeTable[3].additiveMode = MINUS_PERCENT; + pyroAttributeTable[3].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[4].class = CLASS_PYRO; + pyroAttributeTable[4].max = 5; + pyroAttributeTable[4].point = 1; + pyroAttributeTable[4].uid = "heal on hit for rapidfire"; + pyroAttributeTable[4].value = float(3); + pyroAttributeTable[4].defaultValue = float(0); + pyroAttributeTable[4].additiveMode = ADDITIVE_NUMBER; + pyroAttributeTable[4].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + pyroAttributeTable[5].class = CLASS_PYRO; + pyroAttributeTable[5].max = 4; + pyroAttributeTable[5].point = 1; + pyroAttributeTable[5].uid = "weapon burn dmg increased"; + pyroAttributeTable[5].value = float(50); + pyroAttributeTable[5].defaultValue = float(100); + pyroAttributeTable[5].additiveMode = ADDITIVE_PERCENT; + pyroAttributeTable[5].title = "화상피해 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[6].class = CLASS_PYRO; + pyroAttributeTable[6].max = 5; + pyroAttributeTable[6].point = 1; + pyroAttributeTable[6].uid = "weapon burn time increased"; + pyroAttributeTable[6].value = float(25); + pyroAttributeTable[6].defaultValue = float(100); + pyroAttributeTable[6].additiveMode = ADDITIVE_PERCENT; + pyroAttributeTable[6].title = "화상 지속시간 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[7].class = CLASS_PYRO; + pyroAttributeTable[7].max = 5; + pyroAttributeTable[7].point = 2; + pyroAttributeTable[7].uid = "damage bonus HIDDEN"; + pyroAttributeTable[7].value = float(1); + pyroAttributeTable[7].defaultValue = float(100); + pyroAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + pyroAttributeTable[7].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[8].class = CLASS_PYRO; + pyroAttributeTable[8].max = 5; + pyroAttributeTable[8].point = 2; + pyroAttributeTable[8].uid = "max health additive bonus"; + pyroAttributeTable[8].value = float(10); + pyroAttributeTable[8].defaultValue = float(0); + pyroAttributeTable[8].additiveMode = ADDITIVE_NUMBER; + pyroAttributeTable[8].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[9].class = CLASS_PYRO; + pyroAttributeTable[9].max = 3; + pyroAttributeTable[9].point = 3; + pyroAttributeTable[9].uid = "mult airblast refire time"; + pyroAttributeTable[9].value = float(10); + pyroAttributeTable[9].defaultValue = float(100); + pyroAttributeTable[9].additiveMode = MINUS_PERCENT; + pyroAttributeTable[9].title = "압축공기 발사속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[10].class = CLASS_PYRO; + pyroAttributeTable[10].max = 4; + pyroAttributeTable[10].point = 3; + pyroAttributeTable[10].uid = "flame_speed"; + pyroAttributeTable[10].value = float(900); + pyroAttributeTable[10].defaultValue = float(100); + pyroAttributeTable[10].additiveMode = ADDITIVE_NUMBER; + pyroAttributeTable[10].isDisableDrawValue = true; + pyroAttributeTable[10].title = "사정거리 25% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[11].class = CLASS_PYRO; + pyroAttributeTable[11].max = 2; + pyroAttributeTable[11].point = 3; + pyroAttributeTable[11].uid = "mult_item_meter_charge_rate"; + pyroAttributeTable[11].value = float(15); + pyroAttributeTable[11].defaultValue = float(100); + pyroAttributeTable[11].additiveMode = MINUS_PERCENT; + pyroAttributeTable[11].title = "가열가속기 재충전 속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + pyroAttributeTable[12].class = CLASS_PYRO; + pyroAttributeTable[12].max = 1; + pyroAttributeTable[12].point = 4; + pyroAttributeTable[12].uid = "thermal_thruster_air_launch"; + pyroAttributeTable[12].value = float(100); + pyroAttributeTable[12].defaultValue = float(0); + pyroAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + pyroAttributeTable[12].isDisableDrawValue = true; + pyroAttributeTable[12].title = "가열가속기가 공중에서 사용가능합니다 (%dpt)(%d/%d)"; + + demomanAttributeTable[0].class = CLASS_DEMOMAN; + demomanAttributeTable[0].max = 5; + demomanAttributeTable[0].point = 1; + demomanAttributeTable[0].uid = "move speed bonus"; + demomanAttributeTable[0].value = float(3); + demomanAttributeTable[0].defaultValue = float(100); + demomanAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[1].class = CLASS_DEMOMAN; + demomanAttributeTable[1].max = 10; + demomanAttributeTable[1].point = 1; + demomanAttributeTable[1].uid = "fire rate bonus"; + demomanAttributeTable[1].value = float(5); + demomanAttributeTable[1].defaultValue = float(100); + demomanAttributeTable[1].additiveMode = MINUS_PERCENT; + demomanAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[2].class = CLASS_DEMOMAN; + demomanAttributeTable[2].max = 10; + demomanAttributeTable[2].point = 1; + demomanAttributeTable[2].uid = "Reload time decreased"; + demomanAttributeTable[2].value = float(5); + demomanAttributeTable[2].defaultValue = float(100); + demomanAttributeTable[2].additiveMode = MINUS_PERCENT; + demomanAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[3].class = CLASS_DEMOMAN; + demomanAttributeTable[3].max = 3; + demomanAttributeTable[3].point = 1; + demomanAttributeTable[3].uid = "deploy time decreased"; + demomanAttributeTable[3].value = float(15); + demomanAttributeTable[3].defaultValue = float(100); + demomanAttributeTable[3].additiveMode = MINUS_PERCENT; + demomanAttributeTable[3].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[4].class = CLASS_DEMOMAN; + demomanAttributeTable[4].max = 5; + demomanAttributeTable[4].point = 1; + demomanAttributeTable[4].uid = "heal on hit for rapidfire"; + demomanAttributeTable[4].value = float(2); + demomanAttributeTable[4].defaultValue = float(0); + demomanAttributeTable[4].additiveMode = ADDITIVE_NUMBER; + demomanAttributeTable[4].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + demomanAttributeTable[5].class = CLASS_DEMOMAN; + demomanAttributeTable[5].max = 4; + demomanAttributeTable[5].point = 1; + demomanAttributeTable[5].uid = "stickybomb charge rate"; + demomanAttributeTable[5].value = float(25); + demomanAttributeTable[5].defaultValue = float(100); + demomanAttributeTable[5].additiveMode = MINUS_PERCENT; + demomanAttributeTable[5].title = "점착 폭탄 충전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[6].class = CLASS_DEMOMAN; + demomanAttributeTable[6].max = 5; + demomanAttributeTable[6].point = 2; + demomanAttributeTable[6].uid = "damage bonus HIDDEN"; + demomanAttributeTable[6].value = float(1); + demomanAttributeTable[6].defaultValue = float(100); + demomanAttributeTable[6].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[6].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[7].class = CLASS_DEMOMAN; + demomanAttributeTable[7].max = 5; + demomanAttributeTable[7].point = 2; + demomanAttributeTable[7].uid = "max health additive bonus"; + demomanAttributeTable[7].value = float(10); + demomanAttributeTable[7].defaultValue = float(0); + demomanAttributeTable[7].additiveMode = ADDITIVE_NUMBER; + demomanAttributeTable[7].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[8].class = CLASS_DEMOMAN; + demomanAttributeTable[8].max = 5; + demomanAttributeTable[8].point = 2; + demomanAttributeTable[8].uid = "Blast radius increased"; + demomanAttributeTable[8].value = float(10); + demomanAttributeTable[8].defaultValue = float(100); + demomanAttributeTable[8].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[8].title = "폭발반경 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[9].class = CLASS_DEMOMAN; + demomanAttributeTable[9].max = 5; + demomanAttributeTable[9].point = 2; + demomanAttributeTable[9].uid = "Projectile speed increased"; + demomanAttributeTable[9].value = float(10); + demomanAttributeTable[9].defaultValue = float(100); + demomanAttributeTable[9].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[9].title = "투사체속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[10].class = CLASS_DEMOMAN; + demomanAttributeTable[10].max = 5; + demomanAttributeTable[10].point = 2; + demomanAttributeTable[10].uid = "rocket jump damage reduction"; + demomanAttributeTable[10].value = float(10); + demomanAttributeTable[10].defaultValue = float(100); + demomanAttributeTable[10].additiveMode = MINUS_PERCENT; + demomanAttributeTable[10].title = "폭발점프 피해 %.0f%% 감소 (%dpt)(%d/%d)"; + + demomanAttributeTable[11].class = CLASS_DEMOMAN; + demomanAttributeTable[11].max = 3; + demomanAttributeTable[11].point = 2; + demomanAttributeTable[11].uid = "sticky arm time bonus"; + demomanAttributeTable[11].value = float(33); + demomanAttributeTable[11].defaultValue = float(0); + demomanAttributeTable[11].additiveMode = MINUS_PERCENT; + demomanAttributeTable[11].title = "폭탄 폭파 대기 시간 %.0f%% 감소 (%dpt)(%d/%d)"; + + demomanAttributeTable[12].class = CLASS_DEMOMAN; + demomanAttributeTable[12].max = 3; + demomanAttributeTable[12].point = 3; + demomanAttributeTable[12].uid = "charge recharge rate increased"; + demomanAttributeTable[12].value = float(15); + demomanAttributeTable[12].defaultValue = float(100); + demomanAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[12].title = "돌격 재충전 속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[13].class = CLASS_DEMOMAN; + demomanAttributeTable[13].max = 2; + demomanAttributeTable[13].point = 3; + demomanAttributeTable[13].uid = "melee range multiplier"; + demomanAttributeTable[13].value = float(100); + demomanAttributeTable[13].defaultValue = float(100); + demomanAttributeTable[13].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[13].title = "근접 공격 범위 %.0f%%증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[14].class = CLASS_DEMOMAN; + demomanAttributeTable[14].max = 4; + demomanAttributeTable[14].point = 3; + demomanAttributeTable[14].uid = "clip size bonus"; + demomanAttributeTable[14].value = float(25); + demomanAttributeTable[14].defaultValue = float(100); + demomanAttributeTable[14].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[14].title = "장탄수 %.0f%% 증가 (%dpt)(%d/%d)"; + + demomanAttributeTable[15].class = CLASS_DEMOMAN; + demomanAttributeTable[15].max = 1; + demomanAttributeTable[15].point = 4; + demomanAttributeTable[15].uid = "grenade no bounce"; + demomanAttributeTable[15].value = float(0); + demomanAttributeTable[15].defaultValue = float(100); + demomanAttributeTable[15].additiveMode = ADDITIVE_PERCENT; + demomanAttributeTable[15].isDisableDrawValue = true; + demomanAttributeTable[15].title = "유탄이 적게 튀어오릅니다 (%dpt)(%d/%d)"; + + heavyAttributeTable[0].class = CLASS_HEAVY; + heavyAttributeTable[0].max = 5; + heavyAttributeTable[0].point = 1; + heavyAttributeTable[0].uid = "move speed bonus"; + heavyAttributeTable[0].value = float(5); + heavyAttributeTable[0].defaultValue = float(100); + heavyAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + heavyAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[1].class = CLASS_HEAVY; + heavyAttributeTable[1].max = 10; + heavyAttributeTable[1].point = 1; + heavyAttributeTable[1].uid = "fire rate bonus"; + heavyAttributeTable[1].value = float(5); + heavyAttributeTable[1].defaultValue = float(100); + heavyAttributeTable[1].additiveMode = MINUS_PERCENT; + heavyAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[2].class = CLASS_HEAVY; + heavyAttributeTable[2].max = 3; + heavyAttributeTable[2].point = 1; + heavyAttributeTable[2].uid = "deploy time decreased"; + heavyAttributeTable[2].value = float(15); + heavyAttributeTable[2].defaultValue = float(100); + heavyAttributeTable[2].additiveMode = MINUS_PERCENT; + heavyAttributeTable[2].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[3].class = CLASS_HEAVY; + heavyAttributeTable[3].max = 5; + heavyAttributeTable[3].point = 1; + heavyAttributeTable[3].uid = "heal on hit for rapidfire"; + heavyAttributeTable[3].value = float(2); + heavyAttributeTable[3].defaultValue = float(0); + heavyAttributeTable[3].additiveMode = ADDITIVE_NUMBER; + heavyAttributeTable[3].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + heavyAttributeTable[4].class = CLASS_HEAVY; + heavyAttributeTable[4].max = 5; + heavyAttributeTable[4].point = 1; + heavyAttributeTable[4].uid = "effect bar recharge rate increased"; + heavyAttributeTable[4].value = float(10); + heavyAttributeTable[4].defaultValue = float(100); + heavyAttributeTable[4].additiveMode = MINUS_PERCENT; + heavyAttributeTable[4].title = "재충전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[5].class = CLASS_HEAVY; + heavyAttributeTable[5].max = 5; + heavyAttributeTable[5].point = 2; + heavyAttributeTable[5].uid = "damage bonus HIDDEN"; + heavyAttributeTable[5].value = float(1); + heavyAttributeTable[5].defaultValue = float(100); + heavyAttributeTable[5].additiveMode = ADDITIVE_PERCENT; + heavyAttributeTable[5].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[6].class = CLASS_HEAVY; + heavyAttributeTable[6].max = 2; + heavyAttributeTable[6].point = 2; + heavyAttributeTable[6].uid = "aiming movespeed increased"; + heavyAttributeTable[6].value = float(25); + heavyAttributeTable[6].defaultValue = float(100); + heavyAttributeTable[6].additiveMode = ADDITIVE_PERCENT; + heavyAttributeTable[6].title = "총열 회전시 이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[7].class = CLASS_HEAVY; + heavyAttributeTable[7].max = 4; + heavyAttributeTable[7].point = 2; + heavyAttributeTable[7].uid = "minigun spinup time decreased"; + heavyAttributeTable[7].value = float(10); + heavyAttributeTable[7].defaultValue = float(100); + heavyAttributeTable[7].additiveMode = MINUS_PERCENT; + heavyAttributeTable[7].title = "빠른 사격 준비 속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[8].class = CLASS_HEAVY; + heavyAttributeTable[8].max = 6; + heavyAttributeTable[8].point = 2; + heavyAttributeTable[8].uid = "max health additive bonus"; + heavyAttributeTable[8].value = float(25); + heavyAttributeTable[8].defaultValue = float(0); + heavyAttributeTable[8].additiveMode = ADDITIVE_NUMBER; + heavyAttributeTable[8].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[9].class = CLASS_HEAVY; + heavyAttributeTable[9].max = 1; + heavyAttributeTable[9].point = 3; + heavyAttributeTable[9].uid = "attack projectiles"; + heavyAttributeTable[9].value = float(1); + heavyAttributeTable[9].defaultValue = float(0); + heavyAttributeTable[9].additiveMode = ADDITIVE_NUMBER; + heavyAttributeTable[9].isDisableDrawValue = true; + heavyAttributeTable[9].title = "투사체 파괴 (%dpt)(%d/%d)"; + + heavyAttributeTable[10].class = CLASS_HEAVY; + heavyAttributeTable[10].max = 1; + heavyAttributeTable[10].point = 3; + heavyAttributeTable[10].uid = "ring of fire while aiming"; + heavyAttributeTable[10].value = float(50); + heavyAttributeTable[10].defaultValue = float(0); + heavyAttributeTable[10].additiveMode = ADDITIVE_NUMBER; + heavyAttributeTable[10].title = "총열 회전시 피해 %.0f의 불의고리 생성 (%dpt)(%d/%d)"; + + heavyAttributeTable[11].class = CLASS_HEAVY; + heavyAttributeTable[11].max = 2; + heavyAttributeTable[11].point = 3; + heavyAttributeTable[11].uid = "weapon spread bonus"; + heavyAttributeTable[11].value = float(25); + heavyAttributeTable[11].defaultValue = float(100); + heavyAttributeTable[11].additiveMode = MINUS_PERCENT; + heavyAttributeTable[11].title = "집탄률 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[12].class = CLASS_HEAVY; + heavyAttributeTable[12].max = 2; + heavyAttributeTable[12].point = 4; + heavyAttributeTable[12].uid = "bullets per shot bonus"; + heavyAttributeTable[12].value = float(25); + heavyAttributeTable[12].defaultValue = float(100); + heavyAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + heavyAttributeTable[12].title = "발사되는 탄환수 %.0f%% 증가 (%dpt)(%d/%d)"; + + heavyAttributeTable[13].class = CLASS_HEAVY; + heavyAttributeTable[13].max = 1; + heavyAttributeTable[13].point = 5; + heavyAttributeTable[13].uid = "crit from behind"; + heavyAttributeTable[13].value = float(1); + heavyAttributeTable[13].defaultValue = float(0); + heavyAttributeTable[13].additiveMode = ADDITIVE_NUMBER; + heavyAttributeTable[13].isDisableDrawValue = true; + heavyAttributeTable[13].title = "뒤에서 공격하면 항상 치명타가 들어갑니다 (%dpt)(%d/%d)"; + + engineerAttributeTable[0].class = CLASS_ENGINEER; + engineerAttributeTable[0].max = 5; + engineerAttributeTable[0].point = 1; + engineerAttributeTable[0].uid = "move speed bonus"; + engineerAttributeTable[0].value = float(3); + engineerAttributeTable[0].defaultValue = float(100); + engineerAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[1].class = CLASS_ENGINEER; + engineerAttributeTable[1].max = 10; + engineerAttributeTable[1].point = 1; + engineerAttributeTable[1].uid = "fire rate bonus"; + engineerAttributeTable[1].value = float(5); + engineerAttributeTable[1].defaultValue = float(100); + engineerAttributeTable[1].additiveMode = MINUS_PERCENT; + engineerAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[2].class = CLASS_ENGINEER; + engineerAttributeTable[2].max = 10; + engineerAttributeTable[2].point = 1; + engineerAttributeTable[2].uid = "Reload time decreased"; + engineerAttributeTable[2].value = float(5); + engineerAttributeTable[2].defaultValue = float(100); + engineerAttributeTable[2].additiveMode = MINUS_PERCENT; + engineerAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[3].class = CLASS_ENGINEER; + engineerAttributeTable[3].max = 3; + engineerAttributeTable[3].point = 1; + engineerAttributeTable[3].uid = "deploy time decreased"; + engineerAttributeTable[3].value = float(15); + engineerAttributeTable[3].defaultValue = float(100); + engineerAttributeTable[3].additiveMode = MINUS_PERCENT; + engineerAttributeTable[3].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[4].class = CLASS_ENGINEER; + engineerAttributeTable[4].max = 5; + engineerAttributeTable[4].point = 1; + engineerAttributeTable[4].uid = "heal on hit for rapidfire"; + engineerAttributeTable[4].value = float(5); + engineerAttributeTable[4].defaultValue = float(0); + engineerAttributeTable[4].additiveMode = ADDITIVE_NUMBER; + engineerAttributeTable[4].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + engineerAttributeTable[5].class = CLASS_ENGINEER; + engineerAttributeTable[5].max = 4; + engineerAttributeTable[5].point = 1; + engineerAttributeTable[5].uid = "engy dispenser radius increased"; + engineerAttributeTable[5].value = float(200); + engineerAttributeTable[5].defaultValue = float(100); + engineerAttributeTable[5].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[5].title = "디스펜서 범위 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[6].class = CLASS_ENGINEER; + engineerAttributeTable[6].max = 2; + engineerAttributeTable[6].point = 1; + engineerAttributeTable[6].uid = "weapon spread bonus"; + engineerAttributeTable[6].value = float(25); + engineerAttributeTable[6].defaultValue = float(100); + engineerAttributeTable[6].additiveMode = MINUS_PERCENT; + engineerAttributeTable[6].title = "집탄률 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[7].class = CLASS_ENGINEER; + engineerAttributeTable[7].max = 5; + engineerAttributeTable[7].point = 2; + engineerAttributeTable[7].uid = "damage bonus HIDDEN"; + engineerAttributeTable[7].value = float(1); + engineerAttributeTable[7].defaultValue = float(100); + engineerAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[7].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[8].class = CLASS_ENGINEER; + engineerAttributeTable[8].max = 4; + engineerAttributeTable[8].point = 2; + engineerAttributeTable[8].uid = "maxammo metal increased"; + engineerAttributeTable[8].value = float(50); + engineerAttributeTable[8].defaultValue = float(100); + engineerAttributeTable[8].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[8].title = "최대금속 보유량 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[9].class = CLASS_ENGINEER; + engineerAttributeTable[9].max = 5; + engineerAttributeTable[9].point = 2; + engineerAttributeTable[9].uid = "build rate bonus"; + engineerAttributeTable[9].value = float(10); + engineerAttributeTable[9].defaultValue = float(100); + engineerAttributeTable[9].additiveMode = MINUS_PERCENT; + engineerAttributeTable[9].title = "건설속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[10].class = CLASS_ENGINEER; + engineerAttributeTable[10].max = 5; + engineerAttributeTable[10].point = 2; + engineerAttributeTable[10].uid = "max health additive bonus"; + engineerAttributeTable[10].value = float(10); + engineerAttributeTable[10].defaultValue = float(0); + engineerAttributeTable[10].additiveMode = ADDITIVE_NUMBER; + engineerAttributeTable[10].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[11].class = CLASS_ENGINEER; + engineerAttributeTable[11].max = 5; + engineerAttributeTable[11].point = 2; + engineerAttributeTable[11].uid = "metal regen"; + engineerAttributeTable[11].value = float(10); + engineerAttributeTable[11].defaultValue = float(0); + engineerAttributeTable[11].additiveMode = ADDITIVE_NUMBER; + engineerAttributeTable[11].title = "금속 5초마다 %.0f 생성 (%dpt)(%d/%d)"; + + engineerAttributeTable[12].class = CLASS_ENGINEER; + engineerAttributeTable[12].max = 4; + engineerAttributeTable[12].point = 3; + engineerAttributeTable[12].uid = "engy sentry radius increased"; + engineerAttributeTable[12].value = float(10); + engineerAttributeTable[12].defaultValue = float(100); + engineerAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[12].title = "센트리 사정거리 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[13].class = CLASS_ENGINEER; + engineerAttributeTable[13].max = 4; + engineerAttributeTable[13].point = 3; + engineerAttributeTable[13].uid = "engy sentry fire rate increased"; + engineerAttributeTable[13].value = float(10); + engineerAttributeTable[13].defaultValue = float(100); + engineerAttributeTable[13].additiveMode = MINUS_PERCENT; + engineerAttributeTable[13].title = "센트리 발사속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[14].class = CLASS_ENGINEER; + engineerAttributeTable[14].max = 4; + engineerAttributeTable[14].point = 3; + engineerAttributeTable[14].uid = "engy building health bonus"; + engineerAttributeTable[14].value = float(25); + engineerAttributeTable[14].defaultValue = float(100); + engineerAttributeTable[14].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[14].title = "구조물 내구도 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[15].class = CLASS_ENGINEER; + engineerAttributeTable[15].max = 4; + engineerAttributeTable[15].point = 3; + engineerAttributeTable[15].uid = "clip size bonus"; + engineerAttributeTable[15].value = float(25); + engineerAttributeTable[15].defaultValue = float(100); + engineerAttributeTable[15].additiveMode = ADDITIVE_PERCENT; + engineerAttributeTable[15].title = "장탄수 %.0f%% 증가 (%dpt)(%d/%d)"; + + engineerAttributeTable[16].class = CLASS_ENGINEER; + engineerAttributeTable[16].max = 1; + engineerAttributeTable[16].point = 4; + engineerAttributeTable[16].uid = "bidirectional teleport"; + engineerAttributeTable[16].value = float(1); + engineerAttributeTable[16].defaultValue = float(0); + engineerAttributeTable[16].additiveMode = ADDITIVE_NUMBER; + engineerAttributeTable[16].isDisableDrawValue = true; + engineerAttributeTable[16].title = "양방향 텔레포트 활성화 (%dpt)(%d/%d)"; + + medicAttributeTable[0].class = CLASS_MEDIC; + medicAttributeTable[0].max = 5; + medicAttributeTable[0].point = 1; + medicAttributeTable[0].uid = "move speed bonus"; + medicAttributeTable[0].value = float(3); + medicAttributeTable[0].defaultValue = float(100); + medicAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[1].class = CLASS_MEDIC; + medicAttributeTable[1].max = 10; + medicAttributeTable[1].point = 1; + medicAttributeTable[1].uid = "fire rate bonus"; + medicAttributeTable[1].value = float(5); + medicAttributeTable[1].defaultValue = float(100); + medicAttributeTable[1].additiveMode = MINUS_PERCENT; + medicAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[1].class = CLASS_MEDIC; + medicAttributeTable[1].max = 10; + medicAttributeTable[1].point = 1; + medicAttributeTable[1].uid = "Reload time decreased"; + medicAttributeTable[1].value = float(5); + medicAttributeTable[1].defaultValue = float(100); + medicAttributeTable[1].additiveMode = MINUS_PERCENT; + medicAttributeTable[1].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[2].class = CLASS_MEDIC; + medicAttributeTable[2].max = 3; + medicAttributeTable[2].point = 1; + medicAttributeTable[2].uid = "deploy time decreased"; + medicAttributeTable[2].value = float(15); + medicAttributeTable[2].defaultValue = float(100); + medicAttributeTable[2].additiveMode = MINUS_PERCENT; + medicAttributeTable[2].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[3].class = CLASS_MEDIC; + medicAttributeTable[3].max = 5; + medicAttributeTable[3].point = 1; + medicAttributeTable[3].uid = "heal on hit for rapidfire"; + medicAttributeTable[3].value = float(2); + medicAttributeTable[3].defaultValue = float(0); + medicAttributeTable[3].additiveMode = ADDITIVE_NUMBER; + medicAttributeTable[3].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + medicAttributeTable[4].class = CLASS_MEDIC; + medicAttributeTable[4].max = 5; + medicAttributeTable[4].point = 2; + medicAttributeTable[4].uid = "damage bonus HIDDEN"; + medicAttributeTable[4].value = float(1); + medicAttributeTable[4].defaultValue = float(100); + medicAttributeTable[4].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[4].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[5].class = CLASS_MEDIC; + medicAttributeTable[5].max = 5; + medicAttributeTable[5].point = 2; + medicAttributeTable[5].uid = "max health additive bonus"; + medicAttributeTable[5].value = float(10); + medicAttributeTable[5].defaultValue = float(0); + medicAttributeTable[5].additiveMode = ADDITIVE_NUMBER; + medicAttributeTable[5].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[6].class = CLASS_MEDIC; + medicAttributeTable[6].max = 5; + medicAttributeTable[6].point = 2; + medicAttributeTable[6].uid = "heal rate bonus"; + medicAttributeTable[6].value = float(10); + medicAttributeTable[6].defaultValue = float(100); + medicAttributeTable[6].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[6].title = "치료율 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[7].class = CLASS_MEDIC; + medicAttributeTable[7].max = 5; + medicAttributeTable[7].point = 3; + medicAttributeTable[7].uid = "ubercharge rate bonus"; + medicAttributeTable[7].value = float(5); + medicAttributeTable[7].defaultValue = float(100); + medicAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[7].title = "우버 충전율 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[8].class = CLASS_MEDIC; + medicAttributeTable[8].max = 5; + medicAttributeTable[8].point = 3; + medicAttributeTable[8].uid = "add uber charge on hit"; + medicAttributeTable[8].value = float(1); + medicAttributeTable[8].defaultValue = float(0); + medicAttributeTable[8].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[8].title = "적중시 우버차지 %.0f%% 추가 (%dpt)(%d/%d)"; + + medicAttributeTable[9].class = CLASS_MEDIC; + medicAttributeTable[9].max = 5; + medicAttributeTable[9].point = 3; + medicAttributeTable[9].uid = "uber duration bonus"; + medicAttributeTable[9].value = float(1); + medicAttributeTable[9].defaultValue = float(0); + medicAttributeTable[9].additiveMode = ADDITIVE_NUMBER; + medicAttributeTable[9].title = "우버차지 지속시간 %.0f초증가 (%dpt)(%d/%d)"; + + medicAttributeTable[10].class = CLASS_MEDIC; + medicAttributeTable[10].max = 4; + medicAttributeTable[10].point = 3; + medicAttributeTable[10].uid = "clip size bonus"; + medicAttributeTable[10].value = float(100); + medicAttributeTable[10].defaultValue = float(100); + medicAttributeTable[10].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[10].title = "장탄수 %.2f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[11].class = CLASS_MEDIC; + medicAttributeTable[11].max = 4; + medicAttributeTable[11].point = 3; + medicAttributeTable[11].uid = "overheal bonus"; + medicAttributeTable[11].value = float(25); + medicAttributeTable[11].defaultValue = float(100); + medicAttributeTable[11].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[11].title = "과치료 %.0f%% 증가 (%dpt)(%d/%d)"; + + medicAttributeTable[12].class = CLASS_MEDIC; + medicAttributeTable[12].max = 1; + medicAttributeTable[12].point = 5; + medicAttributeTable[12].uid = "generate rage on heal"; + medicAttributeTable[12].value = float(100); + medicAttributeTable[12].defaultValue = float(0); + medicAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[12].isDisableDrawValue = true; + medicAttributeTable[12].title = "투사체 보호막생성 (%dpt)(%d/%d)"; + + medicAttributeTable[13].class = CLASS_MEDIC; + medicAttributeTable[13].max = 1; + medicAttributeTable[13].point = 5; + medicAttributeTable[13].uid = "overheal decay disabled"; + medicAttributeTable[13].value = float(10000); + medicAttributeTable[13].defaultValue = float(0); + medicAttributeTable[13].additiveMode = ADDITIVE_PERCENT; + medicAttributeTable[13].isDisableDrawValue = true; + medicAttributeTable[13].title = "과치료 체력이 소멸되지 않습니다 (%dpt)(%d/%d)"; + + sniperAttributeTable[0].class = CLASS_SNIPER; + sniperAttributeTable[0].max = 5; + sniperAttributeTable[0].point = 1; + sniperAttributeTable[0].uid = "move speed bonus"; + sniperAttributeTable[0].value = float(3); + sniperAttributeTable[0].defaultValue = float(100); + sniperAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[1].class = CLASS_SNIPER; + sniperAttributeTable[1].max = 10; + sniperAttributeTable[1].point = 1; + sniperAttributeTable[1].uid = "fire rate bonus"; + sniperAttributeTable[1].value = float(5); + sniperAttributeTable[1].defaultValue = float(100); + sniperAttributeTable[1].additiveMode = MINUS_PERCENT; + sniperAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[2].class = CLASS_SNIPER; + sniperAttributeTable[2].max = 10; + sniperAttributeTable[2].point = 1; + sniperAttributeTable[2].uid = "Reload time decreased"; + sniperAttributeTable[2].value = float(5); + sniperAttributeTable[2].defaultValue = float(100); + sniperAttributeTable[2].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[3].class = CLASS_SNIPER; + sniperAttributeTable[3].max = 3; + sniperAttributeTable[3].point = 1; + sniperAttributeTable[3].uid = "deploy time decreased"; + sniperAttributeTable[3].value = float(15); + sniperAttributeTable[3].defaultValue = float(100); + sniperAttributeTable[3].additiveMode = MINUS_PERCENT; + sniperAttributeTable[3].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[4].class = CLASS_SNIPER; + sniperAttributeTable[4].max = 5; + sniperAttributeTable[4].point = 1; + sniperAttributeTable[4].uid = "heal on hit for rapidfire"; + sniperAttributeTable[4].value = float(5); + sniperAttributeTable[4].defaultValue = float(0); + sniperAttributeTable[4].additiveMode = ADDITIVE_NUMBER; + sniperAttributeTable[4].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + sniperAttributeTable[5].class = CLASS_SNIPER; + sniperAttributeTable[5].max = 2; + sniperAttributeTable[5].point = 1; + sniperAttributeTable[5].uid = "weapon spread bonus"; + sniperAttributeTable[5].value = float(25); + sniperAttributeTable[5].defaultValue = float(100); + sniperAttributeTable[5].additiveMode = MINUS_PERCENT; + sniperAttributeTable[5].title = "집탄률 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[6].class = CLASS_SNIPER; + sniperAttributeTable[6].max = 2; + sniperAttributeTable[6].point = 1; + sniperAttributeTable[6].uid = "effect bar recharge rate increased"; + sniperAttributeTable[6].value = float(25); + sniperAttributeTable[6].defaultValue = float(100); + sniperAttributeTable[6].additiveMode = MINUS_PERCENT; + sniperAttributeTable[6].title = "재충전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[7].class = CLASS_SNIPER; + sniperAttributeTable[7].max = 5; + sniperAttributeTable[7].point = 2; + sniperAttributeTable[7].uid = "damage bonus HIDDEN"; + sniperAttributeTable[7].value = float(1); + sniperAttributeTable[7].defaultValue = float(100); + sniperAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[7].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[8].class = CLASS_SNIPER; + sniperAttributeTable[8].max = 5; + sniperAttributeTable[8].point = 2; + sniperAttributeTable[8].uid = "max health additive bonus"; + sniperAttributeTable[8].value = float(10); + sniperAttributeTable[8].defaultValue = float(0); + sniperAttributeTable[8].additiveMode = ADDITIVE_NUMBER; + sniperAttributeTable[8].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[9].class = CLASS_SNIPER; + sniperAttributeTable[9].max = 5; + sniperAttributeTable[9].point = 2; + sniperAttributeTable[9].uid = "health regen"; + sniperAttributeTable[9].value = float(1); + sniperAttributeTable[9].defaultValue = float(0); + sniperAttributeTable[9].additiveMode = ADDITIVE_NUMBER; + sniperAttributeTable[9].title = "초당 체력 재생 %.0f 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[10].class = CLASS_SNIPER; + sniperAttributeTable[10].max = 4; + sniperAttributeTable[10].point = 3; + sniperAttributeTable[10].uid = "sniper charge per sec"; + sniperAttributeTable[10].value = float(50); + sniperAttributeTable[10].defaultValue = float(100); + sniperAttributeTable[10].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[10].title = "충전율 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[11].class = CLASS_SNIPER; + sniperAttributeTable[11].max = 5; + sniperAttributeTable[11].point = 3; + sniperAttributeTable[11].uid = "jarate duration"; + sniperAttributeTable[11].value = float(1); + sniperAttributeTable[11].defaultValue = float(0); + sniperAttributeTable[11].additiveMode = ADDITIVE_NUMBER; + sniperAttributeTable[11].title = "충전사격시 병수도 효과 %.0f초증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[12].class = CLASS_SNIPER; + sniperAttributeTable[12].max = 4; + sniperAttributeTable[12].point = 3; + sniperAttributeTable[12].uid = "headshot damage increase"; + sniperAttributeTable[12].value = float(25); + sniperAttributeTable[12].defaultValue = float(100); + sniperAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[12].title = "헤드샷 추가피해 %.0f%% 증가 (%dpt)(%d/%d)"; + + sniperAttributeTable[13].class = CLASS_SNIPER; + sniperAttributeTable[13].max = 1; + sniperAttributeTable[13].point = 4; + sniperAttributeTable[13].uid = "sniper aiming movespeed decreased"; + sniperAttributeTable[13].value = float(4); + sniperAttributeTable[13].defaultValue = float(1); + sniperAttributeTable[13].additiveMode = ADDITIVE_NUMBER; + sniperAttributeTable[13].isDisableDrawValue = true; + sniperAttributeTable[13].title = "조준시 이동속도 저하없음 (%dpt)(%d/%d)"; + + sniperAttributeTable[14].class = CLASS_SNIPER; + sniperAttributeTable[14].max = 1; + sniperAttributeTable[14].point = 4; + sniperAttributeTable[14].uid = "sniper full charge damage bonus"; + sniperAttributeTable[14].value = float(100); + sniperAttributeTable[14].defaultValue = float(100); + sniperAttributeTable[14].additiveMode = ADDITIVE_PERCENT; + sniperAttributeTable[14].title = "완전충전시 피해 %.0f%%증가 (%dpt)(%d/%d)"; + + spyAttributeTable[0].class = CLASS_SPY; + spyAttributeTable[0].max = 5; + spyAttributeTable[0].point = 1; + spyAttributeTable[0].uid = "move speed bonus"; + spyAttributeTable[0].value = float(3); + spyAttributeTable[0].defaultValue = float(100); + spyAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[1].class = CLASS_SPY; + spyAttributeTable[1].max = 10; + spyAttributeTable[1].point = 1; + spyAttributeTable[1].uid = "fire rate bonus"; + spyAttributeTable[1].value = float(5); + spyAttributeTable[1].defaultValue = float(100); + spyAttributeTable[1].additiveMode = MINUS_PERCENT; + spyAttributeTable[1].title = "공격속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[2].class = CLASS_SPY; + spyAttributeTable[2].max = 10; + spyAttributeTable[2].point = 1; + spyAttributeTable[2].uid = "Reload time decreased"; + spyAttributeTable[2].value = float(5); + spyAttributeTable[2].defaultValue = float(100); + spyAttributeTable[2].additiveMode = MINUS_PERCENT; + spyAttributeTable[2].title = "재장전속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[3].class = CLASS_SPY; + spyAttributeTable[3].max = 10; + spyAttributeTable[3].point = 1; + spyAttributeTable[3].uid = "increased jump height"; + spyAttributeTable[3].value = float(5); + spyAttributeTable[3].defaultValue = float(100); + spyAttributeTable[3].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[3].title = "점프높이 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[4].class = CLASS_SPY; + spyAttributeTable[4].max = 3; + spyAttributeTable[4].point = 1; + spyAttributeTable[4].uid = "deploy time decreased"; + spyAttributeTable[4].value = float(15); + spyAttributeTable[4].defaultValue = float(100); + spyAttributeTable[4].additiveMode = MINUS_PERCENT; + spyAttributeTable[4].title = "무기전환속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[5].class = CLASS_SPY; + spyAttributeTable[5].max = 2; + spyAttributeTable[5].point = 1; + spyAttributeTable[5].uid = "weapon spread bonus"; + spyAttributeTable[5].value = float(25); + spyAttributeTable[5].defaultValue = float(100); + spyAttributeTable[5].additiveMode = MINUS_PERCENT; + spyAttributeTable[5].title = "집탄률 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[6].class = CLASS_SPY; + spyAttributeTable[6].max = 5; + spyAttributeTable[6].point = 1; + spyAttributeTable[6].uid = "heal on hit for rapidfire"; + spyAttributeTable[6].value = float(5); + spyAttributeTable[6].defaultValue = float(0); + spyAttributeTable[6].additiveMode = ADDITIVE_NUMBER; + spyAttributeTable[6].title = "적중시 체력 %.0f 회복 (%dpt)(%d/%d)"; + + spyAttributeTable[7].class = CLASS_SPY; + spyAttributeTable[7].max = 5; + spyAttributeTable[7].point = 2; + spyAttributeTable[7].uid = "damage bonus HIDDEN"; + spyAttributeTable[7].value = float(1); + spyAttributeTable[7].defaultValue = float(100); + spyAttributeTable[7].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[7].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[8].class = CLASS_SPY; + spyAttributeTable[8].max = 5; + spyAttributeTable[8].point = 2; + spyAttributeTable[8].uid = "max health additive bonus"; + spyAttributeTable[8].value = float(10); + spyAttributeTable[8].defaultValue = float(0); + spyAttributeTable[8].additiveMode = ADDITIVE_NUMBER; + spyAttributeTable[8].title = "최대체력 %.0f 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[9].class = CLASS_SPY; + spyAttributeTable[9].max = 5; + spyAttributeTable[9].point = 2; + spyAttributeTable[9].uid = "health regen"; + spyAttributeTable[9].value = float(1); + spyAttributeTable[9].defaultValue = float(0); + spyAttributeTable[9].additiveMode = ADDITIVE_NUMBER; + spyAttributeTable[9].title = "초당 체력 재생 %.0f 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[10].class = CLASS_SPY; + spyAttributeTable[10].max = 5; + spyAttributeTable[10].point = 2; + spyAttributeTable[10].uid = "add cloak on hit"; + spyAttributeTable[10].value = float(3); + spyAttributeTable[10].defaultValue = float(0); + spyAttributeTable[10].additiveMode = ADDITIVE_NUMBER; + spyAttributeTable[10].title = "적중시 은폐 에너지 %.0f%% 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[11].class = CLASS_SPY; + spyAttributeTable[11].max = 2; + spyAttributeTable[11].point = 3; + spyAttributeTable[11].uid = "speed_boost_on_hit"; + spyAttributeTable[11].value = float(2); + spyAttributeTable[11].defaultValue = float(0); + spyAttributeTable[11].additiveMode = ADDITIVE_NUMBER; + spyAttributeTable[11].title = "적중시 이동속도 %.0f초간 증가 (%dpt)(%d/%d)"; + + spyAttributeTable[12].class = CLASS_SPY; + spyAttributeTable[12].max = 2; + spyAttributeTable[12].point = 3; + spyAttributeTable[12].uid = "melee range multiplier"; + spyAttributeTable[12].value = float(25); + spyAttributeTable[12].defaultValue = float(100); + spyAttributeTable[12].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[12].title = "근접 공격 범위 %.0f%%증가 (%dpt)(%d/%d)"; + + spyAttributeTable[13].class = CLASS_SPY; + spyAttributeTable[13].max = 1; + spyAttributeTable[13].point = 4; + spyAttributeTable[13].uid = "air dash count"; + spyAttributeTable[13].value = float(100); + spyAttributeTable[13].defaultValue = float(0); + spyAttributeTable[13].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[13].isDisableDrawValue = true; + spyAttributeTable[13].title = "2단점프 가능 (%dpt)(%d/%d)"; + + spyAttributeTable[14].class = CLASS_SPY; + spyAttributeTable[14].max = 1; + spyAttributeTable[14].point = 5; + spyAttributeTable[14].uid = "SET BONUS: quiet unstealth"; + spyAttributeTable[14].value = float(100); + spyAttributeTable[14].defaultValue = float(0); + spyAttributeTable[14].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[14].isDisableDrawValue = true; + spyAttributeTable[14].title = "은폐 해제 음량감소 (%dpt)(%d/%d)"; + + spyAttributeTable[15].class = CLASS_SPY; + spyAttributeTable[15].max = 1; + spyAttributeTable[15].point = 5; + spyAttributeTable[15].uid = "cancel falling damage"; + spyAttributeTable[15].value = float(100); + spyAttributeTable[15].defaultValue = float(0); + spyAttributeTable[15].additiveMode = ADDITIVE_PERCENT; + spyAttributeTable[15].isDisableDrawValue = true; + spyAttributeTable[15].title = "낙하 피해 무시 (%dpt)(%d/%d)"; + + haleAttributeTable[0].class = CLASS_HALE; + haleAttributeTable[0].max = 10; + haleAttributeTable[0].point = 1; + haleAttributeTable[0].uid = "move speed bonus"; + haleAttributeTable[0].value = float(3); + haleAttributeTable[0].defaultValue = float(100); + haleAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + haleAttributeTable[0].title = "이동속도 %.0f%% 증가 (%dpt)(%d/%d)"; + + haleAttributeTable[1].class = CLASS_HALE; + haleAttributeTable[1].max = 5; + haleAttributeTable[1].point = 2; + haleAttributeTable[1].uid = "max health additive bonus"; + haleAttributeTable[1].value = float(2000); + haleAttributeTable[1].defaultValue = float(0); + haleAttributeTable[1].additiveMode = ADDITIVE_NUMBER; + haleAttributeTable[1].title = "최대체력 %.0f증가 (%dpt)(%d/%d)"; + + haleAttributeTable[2].class = CLASS_HALE; + haleAttributeTable[2].max = 2; + haleAttributeTable[2].point = 2; + haleAttributeTable[2].uid = "melee range multiplier"; + haleAttributeTable[2].value = float(50); + haleAttributeTable[2].defaultValue = float(100); + haleAttributeTable[2].additiveMode = ADDITIVE_PERCENT; + haleAttributeTable[2].title = "근접 공격 범위 %.0f%%증가 (%dpt)(%d/%d)"; + + sharedAttributeTable[0].class = CLASS_SHARED; + sharedAttributeTable[0].max = 5; + sharedAttributeTable[0].point = 1; + sharedAttributeTable[0].uid = "ammo regen"; + sharedAttributeTable[0].value = float(10); + sharedAttributeTable[0].defaultValue = float(0); + sharedAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + sharedAttributeTable[0].title = "5초마다 탄약 %.0f%%생성 (%dpt)(%d/%d)"; + + sharedAttributeTable[1].class = CLASS_SHARED; + sharedAttributeTable[1].max = 5; + sharedAttributeTable[1].point = 2; + sharedAttributeTable[1].uid = "damage bonus HIDDEN"; + sharedAttributeTable[1].value = float(1); + sharedAttributeTable[1].defaultValue = float(100); + sharedAttributeTable[1].additiveMode = ADDITIVE_PERCENT; + sharedAttributeTable[1].title = "피해량 %.0f%% 증가 (%dpt)(%d/%d)"; + + weaponAttributeTable[0].class = CLASS_WEAPON; + weaponAttributeTable[0].max = 15; + weaponAttributeTable[0].value = float(1); + weaponAttributeTable[0].defaultValue = float(100); + weaponAttributeTable[0].additiveMode = ADDITIVE_PERCENT; + weaponAttributeTable[0].uid = "damage bonus HIDDEN"; + + for (int i=0;i(data); + pack.Reset(); + + int client = pack.ReadCell(); + char steamid[32]; + pack.ReadString(steamid, sizeof(steamid)); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] PlayerData INSERT 실패 (Client: %d): %s", client, error); + return; + } + + // 2단계: SELECT (데이터 조회) + char query[256]; + Format(query, sizeof(query), + "SELECT steamid, level, exp, point, skillpoint, permission " + ... "FROM playerData WHERE steamid = '%s';", + steamid); + + // DataPack: client, callback + DataPack pack2 = new DataPack(); + pack2.WriteCell(client); + pack2.WriteFunction(callback); + + g_Database.Query(DB_OnPlayerDataLoaded, query, pack2, DBPrio_High); +} + +/** + * 플레이어 데이터 로드 완료 + */ +public void DB_OnPlayerDataLoaded(Database db, DBResultSet results, const char[] error, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] PlayerData SELECT 실패 (Client: %d): %s", client, error); + return; + } + + // 결과가 없으면 종료 + if (!results.FetchRow()) + { + LogError("[DB] PlayerData 조회 결과 없음 (Client: %d)", client); + return; + } + + // 데이터 읽기 + char steamid[32]; + results.FetchString(0, steamid, sizeof(steamid)); + int level = results.FetchInt(1); + int exp = results.FetchInt(2); + int point = results.FetchInt(3); + int skillpoint = results.FetchInt(4); + int permission = results.FetchInt(5); + + // playerDataList에 저장 + strcopy(playerDataList[client].steamid, sizeof(playerDataList[].steamid), steamid); + playerDataList[client].level = level; + playerDataList[client].exp = exp; + playerDataList[client].point = point; + playerDataList[client].skillpoint = skillpoint; + playerDataList[client].permission = permission; + + PrintToServer("[DB] PlayerData 로드 완료 - Client: %d, Level: %d, Point: %d", + client, level, point); + + // 콜백 호출 + if (callback != INVALID_FUNCTION) + { + Call_StartFunction(null, callback); + Call_PushCell(client); + Call_Finish(); + } +} + +/** + * 모든 클래스 속성 데이터 로드 (배치 최적화) + * - 137개 개별 INSERT → 1개 배치 INSERT + * - 137개 개별 SELECT → 1개 SELECT + * + * @param client 클라이언트 인덱스 + * @param callback 로드 완료 후 호출할 콜백 함수 + */ +void DB_LoadAllAttributes(int client, Function callback = INVALID_FUNCTION) +{ + if (g_Database == null) + { + LogError("[DB] LoadAllAttributes: 데이터베이스가 연결되지 않았습니다."); + return; + } + + if (!IsClientConnected(client) || IsFakeClient(client)) + { + PrintToServer("[DB] LoadAllAttributes: 유효하지 않은 클라이언트 %d", client); + return; + } + + char steamid[32]; + strcopy(steamid, sizeof(steamid), playerDataList[client].steamid); + + if (StrEqual(steamid, "")) + { + LogError("[DB] LoadAllAttributes: 클라이언트 %d의 SteamID가 없습니다.", client); + return; + } + + // 1단계: 배치 INSERT OR IGNORE (모든 속성을 한 번에 삽입) + char query[32768]; // 큰 쿼리를 위한 버퍼 + strcopy(query, sizeof(query), + "INSERT OR IGNORE INTO classAttributeData (steamid, uid, id, class, upgrade) VALUES "); + + bool firstValue = true; + + // 모든 클래스의 모든 속성을 VALUES에 추가 + for (int classIdx = 0; classIdx < 12; classIdx++) + { + int attrCount = g_ClassAttributeCounts[classIdx]; + + for (int i = 0; i < attrCount; i++) + { + char uid[64]; + int id = i; + int upgrade = 0; + + // 클래스별로 AttributeData 배열에서 uid 가져오기 + DB_GetAttributeUID(client, classIdx, i, uid, sizeof(uid)); + + // VALUES 추가 + if (!firstValue) + { + StrCat(query, sizeof(query), ", "); + } + + char valueStr[256]; + Format(valueStr, sizeof(valueStr), + "('%s', '%s', %d, %d, %d)", + steamid, uid, id, classIdx, upgrade); + + StrCat(query, sizeof(query), valueStr); + firstValue = false; + } + } + + StrCat(query, sizeof(query), ";"); + + // DataPack: client, steamid, callback + DataPack pack = new DataPack(); + pack.WriteCell(client); + pack.WriteString(steamid); + pack.WriteFunction(callback); + + PrintToServer("[DB] 배치 INSERT 쿼리 전송 (Client: %d, 속성 개수: 137개)", client); + g_Database.Query(DB_OnAttributesInserted, query, pack, DBPrio_Low); +} + +/** + * 클래스별 AttributeData에서 UID 가져오기 + */ +void DB_GetAttributeUID(int client, int classIdx, int attrIdx, char[] uid, int maxlen) +{ + switch (classIdx) + { + case 0: strcopy(uid, maxlen, playerDataList[client].scoutAttributeData[attrIdx].uid); + case 1: strcopy(uid, maxlen, playerDataList[client].medicAttributeData[attrIdx].uid); + case 2: strcopy(uid, maxlen, playerDataList[client].soldierAttributeData[attrIdx].uid); + case 3: strcopy(uid, maxlen, playerDataList[client].pyroAttributeData[attrIdx].uid); + case 4: strcopy(uid, maxlen, playerDataList[client].spyAttributeData[attrIdx].uid); + case 5: strcopy(uid, maxlen, playerDataList[client].demomanAttributeData[attrIdx].uid); + case 6: strcopy(uid, maxlen, playerDataList[client].sniperAttributeData[attrIdx].uid); + case 7: strcopy(uid, maxlen, playerDataList[client].engineerAttributeData[attrIdx].uid); + case 8: strcopy(uid, maxlen, playerDataList[client].heavyAttributeData[attrIdx].uid); + case 9: strcopy(uid, maxlen, playerDataList[client].haleAttributeData[attrIdx].uid); + case 10: strcopy(uid, maxlen, playerDataList[client].sharedAttributeData[attrIdx].uid); + case 11: strcopy(uid, maxlen, playerDataList[client].weaponAttributeData[attrIdx].uid); + } +} + +/** + * 배치 INSERT 완료 후 SELECT 실행 + */ +public void DB_OnAttributesInserted(Database db, DBResultSet results, const char[] error, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + char steamid[32]; + pack.ReadString(steamid, sizeof(steamid)); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] 배치 INSERT 실패 (Client: %d): %s", client, error); + return; + } + + PrintToServer("[DB] 배치 INSERT 완료 (Client: %d)", client); + + // 2단계: SELECT (모든 속성 한 번에 조회) + char query[512]; + Format(query, sizeof(query), + "SELECT uid, id, class, upgrade " + ... "FROM classAttributeData WHERE steamid = '%s' " + ... "ORDER BY class, id;", + steamid); + + // DataPack: client, callback + DataPack pack2 = new DataPack(); + pack2.WriteCell(client); + pack2.WriteFunction(callback); + + g_Database.Query(DB_OnAttributesLoaded, query, pack2, DBPrio_Low); +} + +/** + * 모든 속성 데이터 로드 완료 + */ +public void DB_OnAttributesLoaded(Database db, DBResultSet results, const char[] error, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] 속성 SELECT 실패 (Client: %d): %s", client, error); + return; + } + + int rowCount = 0; + + // 모든 속성 데이터 읽기 + while (results.FetchRow()) + { + char uid[64]; + results.FetchString(0, uid, sizeof(uid)); + int id = results.FetchInt(1); + int classIdx = results.FetchInt(2); + int upgrade = results.FetchInt(3); + + // playerDataList의 해당 AttributeData에 저장 + DB_SetAttributeData(client, classIdx, id, uid, upgrade); + + rowCount++; + } + + PrintToServer("[DB] 속성 로드 완료 - Client: %d, 속성 개수: %d개", client, rowCount); + + // 로드 완료 플래그 설정 + playerDataList[client].isLoadComplete = true; + + // 콜백 호출 + if (callback != INVALID_FUNCTION) + { + Call_StartFunction(null, callback); + Call_PushCell(client); + Call_Finish(); + } +} + +/** + * 클래스별 AttributeData에 데이터 저장 + */ +void DB_SetAttributeData(int client, int classIdx, int id, const char[] uid, int upgrade) +{ + switch (classIdx) + { + case 0: + { + strcopy(playerDataList[client].scoutAttributeData[id].uid, 64, uid); + playerDataList[client].scoutAttributeData[id].id = id; + playerDataList[client].scoutAttributeData[id].class = classIdx; + playerDataList[client].scoutAttributeData[id].upgrade = upgrade; + } + case 1: + { + strcopy(playerDataList[client].medicAttributeData[id].uid, 64, uid); + playerDataList[client].medicAttributeData[id].id = id; + playerDataList[client].medicAttributeData[id].class = classIdx; + playerDataList[client].medicAttributeData[id].upgrade = upgrade; + } + case 2: + { + strcopy(playerDataList[client].soldierAttributeData[id].uid, 64, uid); + playerDataList[client].soldierAttributeData[id].id = id; + playerDataList[client].soldierAttributeData[id].class = classIdx; + playerDataList[client].soldierAttributeData[id].upgrade = upgrade; + } + case 3: + { + strcopy(playerDataList[client].pyroAttributeData[id].uid, 64, uid); + playerDataList[client].pyroAttributeData[id].id = id; + playerDataList[client].pyroAttributeData[id].class = classIdx; + playerDataList[client].pyroAttributeData[id].upgrade = upgrade; + } + case 4: + { + strcopy(playerDataList[client].spyAttributeData[id].uid, 64, uid); + playerDataList[client].spyAttributeData[id].id = id; + playerDataList[client].spyAttributeData[id].class = classIdx; + playerDataList[client].spyAttributeData[id].upgrade = upgrade; + } + case 5: + { + strcopy(playerDataList[client].demomanAttributeData[id].uid, 64, uid); + playerDataList[client].demomanAttributeData[id].id = id; + playerDataList[client].demomanAttributeData[id].class = classIdx; + playerDataList[client].demomanAttributeData[id].upgrade = upgrade; + } + case 6: + { + strcopy(playerDataList[client].sniperAttributeData[id].uid, 64, uid); + playerDataList[client].sniperAttributeData[id].id = id; + playerDataList[client].sniperAttributeData[id].class = classIdx; + playerDataList[client].sniperAttributeData[id].upgrade = upgrade; + } + case 7: + { + strcopy(playerDataList[client].engineerAttributeData[id].uid, 64, uid); + playerDataList[client].engineerAttributeData[id].id = id; + playerDataList[client].engineerAttributeData[id].class = classIdx; + playerDataList[client].engineerAttributeData[id].upgrade = upgrade; + } + case 8: + { + strcopy(playerDataList[client].heavyAttributeData[id].uid, 64, uid); + playerDataList[client].heavyAttributeData[id].id = id; + playerDataList[client].heavyAttributeData[id].class = classIdx; + playerDataList[client].heavyAttributeData[id].upgrade = upgrade; + } + case 9: + { + strcopy(playerDataList[client].haleAttributeData[id].uid, 64, uid); + playerDataList[client].haleAttributeData[id].id = id; + playerDataList[client].haleAttributeData[id].class = classIdx; + playerDataList[client].haleAttributeData[id].upgrade = upgrade; + } + case 10: + { + strcopy(playerDataList[client].sharedAttributeData[id].uid, 64, uid); + playerDataList[client].sharedAttributeData[id].id = id; + playerDataList[client].sharedAttributeData[id].class = classIdx; + playerDataList[client].sharedAttributeData[id].upgrade = upgrade; + } + case 11: + { + strcopy(playerDataList[client].weaponAttributeData[id].uid, 64, uid); + playerDataList[client].weaponAttributeData[id].id = id; + playerDataList[client].weaponAttributeData[id].class = classIdx; + playerDataList[client].weaponAttributeData[id].upgrade = upgrade; + } + } +} + +// ============================================================================= +// 플레이어 데이터 저장 (배치 최적화) +// ============================================================================= + +/** + * 플레이어 기본 데이터 저장 + * - playerData 테이블 UPDATE + * + * @param client 클라이언트 인덱스 + * @param callback 저장 완료 후 호출할 콜백 함수 + */ +void DB_SavePlayerData(int client, Function callback = INVALID_FUNCTION) +{ + if (g_Database == null) + { + LogError("[DB] SavePlayerData: 데이터베이스가 연결되지 않았습니다."); + return; + } + + if (!IsClientConnected(client) || IsFakeClient(client)) + { + PrintToServer("[DB] SavePlayerData: 유효하지 않은 클라이언트 %d", client); + return; + } + + char steamid[32]; + strcopy(steamid, sizeof(steamid), playerDataList[client].steamid); + + if (StrEqual(steamid, "")) + { + LogError("[DB] SavePlayerData: 클라이언트 %d의 SteamID가 없습니다.", client); + return; + } + + // UPDATE 쿼리 생성 + char query[512]; + Format(query, sizeof(query), + "UPDATE playerData SET " + ... "level = %d, " + ... "exp = %d, " + ... "point = %d, " + ... "skillpoint = %d, " + ... "permission = %d " + ... "WHERE steamid = '%s';", + playerDataList[client].level, + playerDataList[client].exp, + playerDataList[client].point, + playerDataList[client].skillpoint, + playerDataList[client].permission, + steamid); + + // DataPack: client, callback + DataPack pack = new DataPack(); + pack.WriteCell(client); + pack.WriteFunction(callback); + + g_Database.Query(DB_OnPlayerDataSaved, query, pack, DBPrio_High); + + PrintToServer("[DB] PlayerData 저장 (Client: %d, Level: %d, Point: %d)", + client, playerDataList[client].level, playerDataList[client].point); +} + +/** + * 플레이어 데이터 저장 완료 + */ +public void DB_OnPlayerDataSaved(Database db, DBResultSet results, const char[] error, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] PlayerData UPDATE 실패 (Client: %d): %s", client, error); + return; + } + + PrintToServer("[DB] PlayerData 저장 완료 (Client: %d)", client); + + // 콜백 호출 + if (callback != INVALID_FUNCTION) + { + Call_StartFunction(null, callback); + Call_PushCell(client); + Call_Finish(); + } +} + +/** + * 모든 클래스 속성 데이터 저장 (배치 최적화 + 트랜잭션) + * - 137개 개별 INSERT OR REPLACE → 1개 배치 INSERT OR REPLACE + * - 트랜잭션으로 원자성 보장 + * + * @param client 클라이언트 인덱스 + * @param callback 저장 완료 후 호출할 콜백 함수 + */ +void DB_SaveAllAttributes(int client, Function callback = INVALID_FUNCTION) +{ + if (g_Database == null) + { + LogError("[DB] SaveAllAttributes: 데이터베이스가 연결되지 않았습니다."); + return; + } + + if (!IsClientConnected(client) || IsFakeClient(client)) + { + PrintToServer("[DB] SaveAllAttributes: 유효하지 않은 클라이언트 %d", client); + return; + } + + char steamid[32]; + strcopy(steamid, sizeof(steamid), playerDataList[client].steamid); + + if (StrEqual(steamid, "")) + { + LogError("[DB] SaveAllAttributes: 클라이언트 %d의 SteamID가 없습니다.", client); + return; + } + + // 배치 INSERT OR REPLACE 쿼리 생성 (트랜잭션 포함) + char query[32768]; // 큰 쿼리를 위한 버퍼 + + // 트랜잭션 시작 + strcopy(query, sizeof(query), "BEGIN TRANSACTION; "); + + // INSERT OR REPLACE 시작 + StrCat(query, sizeof(query), + "INSERT OR REPLACE INTO classAttributeData (steamid, uid, id, class, upgrade) VALUES "); + + bool firstValue = true; + + // 모든 클래스의 모든 속성을 VALUES에 추가 + for (int classIdx = 0; classIdx < 12; classIdx++) + { + int attrCount = g_ClassAttributeCounts[classIdx]; + + for (int i = 0; i < attrCount; i++) + { + char uid[64]; + int upgrade = 0; + + // 클래스별로 AttributeData 배열에서 데이터 가져오기 + DB_GetAttributeDataForSave(client, classIdx, i, uid, sizeof(uid), upgrade); + + // VALUES 추가 + if (!firstValue) + { + StrCat(query, sizeof(query), ", "); + } + + char valueStr[256]; + Format(valueStr, sizeof(valueStr), + "('%s', '%s', %d, %d, %d)", + steamid, uid, i, classIdx, upgrade); + + StrCat(query, sizeof(query), valueStr); + firstValue = false; + } + } + + // 트랜잭션 커밋 + StrCat(query, sizeof(query), "; COMMIT;"); + + // DataPack: client, callback + DataPack pack = new DataPack(); + pack.WriteCell(client); + pack.WriteFunction(callback); + + PrintToServer("[DB] 배치 UPDATE 쿼리 전송 (Client: %d, 속성 개수: 137개, 트랜잭션 사용)", client); + g_Database.Query(DB_OnAttributesSaved, query, pack, DBPrio_High); +} + +/** + * 클래스별 AttributeData에서 데이터 가져오기 (저장용) + */ +void DB_GetAttributeDataForSave(int client, int classIdx, int attrIdx, char[] uid, int maxlen, int &upgrade) +{ + switch (classIdx) + { + case 0: + { + strcopy(uid, maxlen, playerDataList[client].scoutAttributeData[attrIdx].uid); + upgrade = playerDataList[client].scoutAttributeData[attrIdx].upgrade; + } + case 1: + { + strcopy(uid, maxlen, playerDataList[client].medicAttributeData[attrIdx].uid); + upgrade = playerDataList[client].medicAttributeData[attrIdx].upgrade; + } + case 2: + { + strcopy(uid, maxlen, playerDataList[client].soldierAttributeData[attrIdx].uid); + upgrade = playerDataList[client].soldierAttributeData[attrIdx].upgrade; + } + case 3: + { + strcopy(uid, maxlen, playerDataList[client].pyroAttributeData[attrIdx].uid); + upgrade = playerDataList[client].pyroAttributeData[attrIdx].upgrade; + } + case 4: + { + strcopy(uid, maxlen, playerDataList[client].spyAttributeData[attrIdx].uid); + upgrade = playerDataList[client].spyAttributeData[attrIdx].upgrade; + } + case 5: + { + strcopy(uid, maxlen, playerDataList[client].demomanAttributeData[attrIdx].uid); + upgrade = playerDataList[client].demomanAttributeData[attrIdx].upgrade; + } + case 6: + { + strcopy(uid, maxlen, playerDataList[client].sniperAttributeData[attrIdx].uid); + upgrade = playerDataList[client].sniperAttributeData[attrIdx].upgrade; + } + case 7: + { + strcopy(uid, maxlen, playerDataList[client].engineerAttributeData[attrIdx].uid); + upgrade = playerDataList[client].engineerAttributeData[attrIdx].upgrade; + } + case 8: + { + strcopy(uid, maxlen, playerDataList[client].heavyAttributeData[attrIdx].uid); + upgrade = playerDataList[client].heavyAttributeData[attrIdx].upgrade; + } + case 9: + { + strcopy(uid, maxlen, playerDataList[client].haleAttributeData[attrIdx].uid); + upgrade = playerDataList[client].haleAttributeData[attrIdx].upgrade; + } + case 10: + { + strcopy(uid, maxlen, playerDataList[client].sharedAttributeData[attrIdx].uid); + upgrade = playerDataList[client].sharedAttributeData[attrIdx].upgrade; + } + case 11: + { + strcopy(uid, maxlen, playerDataList[client].weaponAttributeData[attrIdx].uid); + upgrade = playerDataList[client].weaponAttributeData[attrIdx].upgrade; + } + } +} + +/** + * 모든 속성 데이터 저장 완료 + */ +public void DB_OnAttributesSaved(Database db, DBResultSet results, const char[] error, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + Function callback = pack.ReadFunction(); + + delete pack; + + if (db == null || results == null) + { + LogError("[DB] 배치 UPDATE 실패 (Client: %d): %s", client, error); + return; + } + + PrintToServer("[DB] 속성 저장 완료 (Client: %d, 137개 속성)", client); + + // 콜백 호출 + if (callback != INVALID_FUNCTION) + { + Call_StartFunction(null, callback); + Call_PushCell(client); + Call_Finish(); + } +} + +// ============================================================================= +// 유틸리티 함수 +// ============================================================================= + +/** + * 데이터베이스 핸들 가져오기 + */ +Database DB_GetHandle() +{ + return g_Database; +} + +/** + * 데이터베이스 연결 상태 확인 + */ +bool DB_IsConnected() +{ + return g_Database != null; +} + +/** + * 시퀀스 번호 증가 및 반환 + */ +int DB_GetNextSequence() +{ + return ++g_Sequence; +} diff --git a/includes/event_handler.inc b/includes/event_handler.inc new file mode 100644 index 0000000..236fb1c --- /dev/null +++ b/includes/event_handler.inc @@ -0,0 +1,763 @@ +/** + * ============================================================================ + * 이벤트 핸들러 모듈 (Event Handler Module) + * TF2 레벨업 시스템의 게임 이벤트 및 타이머 처리 + * ============================================================================ + */ + +#if defined _event_handler_included + #endinput +#endif +#define _event_handler_included + +// ============================================================================ +// 전역 변수 선언 +// ============================================================================ + +// 타이머 핸들 +Handle g_Timer_RewardUpdate = INVALID_HANDLE; // 주기적 보상 타이머 +Handle g_Timer_AddRevivePoint = INVALID_HANDLE; // 부활 포인트 증가 타이머 + +// 보상 설정 +const float REWARD_INTERVAL = 600.0; // 보상 지급 간격 (10분) +const int REWARD_POINTS = 100; // 타이머 보상 포인트 +const int REWARD_EXP = 50; // 타이머 보상 경험치 + +// 데미지 누적 보상 +const int DAMAGE_THRESHOLD = 2000; // 데미지 누적 기준 +const int DAMAGE_REWARD_POINTS = 40; // 누적 달성 시 포인트 +const int DAMAGE_REWARD_EXP = 20; // 누적 달성 시 경험치 + +// 처치 보상 +const int KILL_REWARD_POINTS = 20; // 처치 시 포인트 +const int KILL_REWARD_EXP = 20; // 처치 시 경험치 + +// 부활 시스템 설정 +const int REVIVE_POINT_BASE = 200; // 기본 부활 비용 +const int REVIVE_COUNT_BASE = 2; // 기본 부활 횟수 +const int REVIVE_POINT_MULTIPLIER = 2; // 부활 시 가격 배수 +const int REVIVE_POINT_INCREMENT = 100; // 시간당 부활 포인트 증가량 +const float REVIVE_POINT_INTERVAL = 300.0; // 부활 포인트 증가 간격 (5분) +const int REVIVE_HEAL_AMOUNT = 2000; // 부활 시 블루팀 회복량 + +// ============================================================================ +// 이벤트 등록 및 초기화 +// ============================================================================ + +/** + * 모든 게임 이벤트를 등록합니다. + */ +void Event_Register() +{ + HookEvent("player_spawn", Event_OnPlayerSpawn, EventHookMode_Post); + HookEvent("player_hurt", Event_OnPlayerHurt, EventHookMode_Post); + HookEvent("player_death", Event_OnPlayerDeath, EventHookMode_Post); + HookEvent("player_regenerate", Event_OnPlayerRegenerate, EventHookMode_Post); + HookEvent("post_inventory_application", Event_OnPlayerSpawn, EventHookMode_Post); + HookEvent("player_changeclass", Event_OnPlayerChangeClass, EventHookMode_Post); + HookEvent("player_class", Event_OnPlayerChangeClass, EventHookMode_Post); + HookEvent("player_team", Event_OnPlayerChangeClass, EventHookMode_Post); + HookEvent("teamplay_round_start", Event_OnRoundStart); + HookEvent("teamplay_round_win", Event_OnRoundEnd); + + PrintToServer("[Event Handler] 이벤트 등록 완료"); +} + +// ============================================================================ +// 타이머 관리 +// ============================================================================ + +/** + * 타이머를 초기화하고 시작합니다. + */ +void Timer_Initialize() +{ + // 주기적 보상 타이머 시작 + if (g_Timer_RewardUpdate == INVALID_HANDLE) + { + g_Timer_RewardUpdate = CreateTimer(REWARD_INTERVAL, Timer_RewardUpdate, _, TIMER_REPEAT); + PrintToServer("[Event Handler] 보상 타이머 시작 (%.0f초 간격)", REWARD_INTERVAL); + } +} + +/** + * 모든 타이머를 정리합니다. (플러그인 종료 시 호출) + */ +void Timer_Cleanup() +{ + // 보상 타이머 정리 + if (g_Timer_RewardUpdate != INVALID_HANDLE) + { + KillTimer(g_Timer_RewardUpdate); + g_Timer_RewardUpdate = INVALID_HANDLE; + PrintToServer("[Event Handler] 보상 타이머 정리"); + } + + // 부활 포인트 타이머 정리 + if (g_Timer_AddRevivePoint != INVALID_HANDLE) + { + KillTimer(g_Timer_AddRevivePoint); + g_Timer_AddRevivePoint = INVALID_HANDLE; + PrintToServer("[Event Handler] 부활 포인트 타이머 정리"); + } +} + +/** + * 주기적으로 모든 플레이어에게 경험치와 포인트를 지급합니다. + */ +public Action Timer_RewardUpdate(Handle timer) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && playerDataList[i].isLoadComplete) + { + AddPlayerPoints(i, REWARD_POINTS); + AddPlayerEXP(i, REWARD_EXP); + CPrintToChat(i, "{olive}[EXP&Point]{default} 10분간 플레이하여 {rare}[%d 경험치]{default} {unique}[%d 포인트]{default}를 얻었습니다!", + REWARD_EXP, REWARD_POINTS); + } + } + + return Plugin_Continue; +} + +/** + * 주기적으로 모든 플레이어의 부활 포인트를 증가시킵니다. + */ +public Action Timer_RevivePointUpdate(Handle timer) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && !IsFakeClient(client)) + { + playerDataList[client].revivePoint += REVIVE_POINT_INCREMENT; + } + } + + // 타이머 재설정 + g_Timer_AddRevivePoint = CreateTimer(REVIVE_POINT_INTERVAL, Timer_RevivePointUpdate, _); + + return Plugin_Continue; +} + +/** + * HUD를 업데이트합니다. (추후 구현 가능) + */ +public Action Timer_HUDUpdate(Handle timer) +{ + // TODO: HUD 업데이트 로직 구현 + return Plugin_Continue; +} + +/** + * 플레이어에게 공유 속성을 적용합니다. + * @param timer 타이머 핸들 + * @param client 클라이언트 인덱스 + */ +public Action Timer_ApplySharedAttribute(Handle timer, any client) +{ + if (!IsClientInGame(client)) + { + return Plugin_Continue; + } + + int team = GetClientTeam(client); + + // 팀별 스탯 적용 여부 확인 + if (team == 2) + { + if (!g_redEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + } + else if (team == 3) + { + if (!g_blueEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + } + + // 무기에 속성 적용 + for (int slot = 0; slot <= 5; slot++) + { + int weapon = GetPlayerWeaponSlot(client, slot); + if (IsValidEntity(weapon)) + { + for (int i = 0; i < sizeof(playerDataList[client].weaponAttributeData); i++) + { + int id = playerDataList[client].weaponAttributeData[i].id; + int upgrade = playerDataList[client].weaponAttributeData[i].upgrade; + + if (upgrade <= 0) + { + continue; + } + + float result = 0.0; + Address attr = TF2Attrib_GetByName(client, weaponAttributeTable[id].uid); + + if (attr == Address_Null) + { + result = (weaponAttributeTable[id].defaultValue * 0.01) + + (float(upgrade) * weaponAttributeTable[id].value * 0.01); + } + else + { + float current = TF2Attrib_GetValue(attr); + result = current + (float(upgrade) * weaponAttributeTable[id].value * 0.01); + } + + TF2Attrib_SetByName(weapon, weaponAttributeTable[id].uid, result); + } + } + } + + // 공유 속성 적용 + for (int i = 0; i < sizeof(playerDataList[client].sharedAttributeData); i++) + { + int id = playerDataList[client].sharedAttributeData[i].id; + int upgrade = playerDataList[client].sharedAttributeData[i].upgrade; + + if (upgrade <= 0) + { + continue; + } + + Address attr = TF2Attrib_GetByName(client, sharedAttributeTable[id].uid); + float result = 0.0; + + if (attr == Address_Null) + { + // 새로운 속성 적용 + if (sharedAttributeTable[id].additiveMode == ADDITIVE_NUMBER) + { + result = sharedAttributeTable[id].defaultValue + + (float(upgrade) * sharedAttributeTable[id].value); + } + else if (sharedAttributeTable[id].additiveMode == ADDITIVE_PERCENT) + { + result = (sharedAttributeTable[id].defaultValue * 0.01) + + (float(upgrade) * sharedAttributeTable[id].value * 0.01); + } + else if (sharedAttributeTable[id].additiveMode == MINUS_NUMBER) + { + result = sharedAttributeTable[id].defaultValue - + (float(upgrade) * sharedAttributeTable[id].value); + } + else if (sharedAttributeTable[id].additiveMode == MINUS_PERCENT) + { + result = (sharedAttributeTable[id].defaultValue * 0.01) - + (float(upgrade) * sharedAttributeTable[id].value * 0.01); + } + + TF2Attrib_SetByName(client, sharedAttributeTable[id].uid, result); + } + else + { + // 기존 속성에 추가 + float current = TF2Attrib_GetValue(attr); + + if (sharedAttributeTable[id].additiveMode == ADDITIVE_NUMBER) + { + result = current + (float(upgrade) * sharedAttributeTable[id].value); + } + else if (sharedAttributeTable[id].additiveMode == ADDITIVE_PERCENT) + { + result = current + (float(upgrade) * sharedAttributeTable[id].value * 0.01); + } + else if (sharedAttributeTable[id].additiveMode == MINUS_NUMBER) + { + result = current - (float(upgrade) * sharedAttributeTable[id].value); + } + else if (sharedAttributeTable[id].additiveMode == MINUS_PERCENT) + { + result = current - (float(upgrade) * sharedAttributeTable[id].value * 0.01); + } + + TF2Attrib_SetByName(client, sharedAttributeTable[id].uid, result); + } + } + + return Plugin_Continue; +} + +/** + * 무기 속성을 적용합니다. (추후 필요 시 구현) + */ +public Action Timer_ApplyWeaponAttribute(Handle timer, any client) +{ + // TODO: 무기 속성 적용 로직 구현 + return Plugin_Continue; +} + +// ============================================================================ +// 게임 이벤트 핸들러 +// ============================================================================ + +/** + * 플레이어 스폰 이벤트 핸들러 + * 클래스별 속성을 적용합니다. + */ +public Action Event_OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + PrintToServer("[Event Handler] OnPlayerSpawn: %d", client); + + if (client <= 0 || !IsClientInGame(client) || IsFakeClient(client) || !IsClientAuthorized(client)) + { + return Plugin_Continue; + } + + int team = GetClientTeam(client); + + // 팀별 스탯 적용 여부 확인 + if (team == 2 && !g_redEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + else if (team == 3 && !g_blueEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + + // 기존 속성 제거 + TF2Attrib_RemoveAll(client); + + // 클래스별 속성 적용 + ApplyClassAttributes(client); + + // 공유 속성은 0.5초 후 적용 + CreateTimer(0.5, Timer_ApplySharedAttribute, client); + + return Plugin_Continue; +} + +/** + * 플레이어 피해 이벤트 핸들러 + * 데미지 누적 및 보상 처리 + */ +public Action Event_OnPlayerHurt(Event event, const char[] name, bool dontBroadcast) +{ + int victim = GetClientOfUserId(event.GetInt("userid")); + int attacker = GetClientOfUserId(event.GetInt("attacker")); + int damage = event.GetInt("damageamount"); + + if (attacker <= 0 || victim <= 0 || !IsClientInGame(attacker) || + attacker == victim || IsFakeClient(attacker)) + { + return Plugin_Continue; + } + + // 데미지 누적 + playerDataList[attacker].damage += damage; + + // 누적 데미지 달성 확인 + int multiple = 1; + if (playerDataList[attacker].damage >= DAMAGE_THRESHOLD) + { + multiple = playerDataList[attacker].damage / DAMAGE_THRESHOLD; + + for (int i = 0; i < multiple; i++) + { + AddPlayerEXP(attacker, DAMAGE_REWARD_EXP); + AddPlayerPoints(attacker, DAMAGE_REWARD_POINTS); + } + + CPrintToChat(attacker, "{olive}[EXP&Point]{default} 데미지 누적 달성! {rare}[%d 경험치]{default} {unique}[%d 포인트]{default} 획득!", + DAMAGE_REWARD_EXP * multiple, DAMAGE_REWARD_POINTS * multiple); + + playerDataList[attacker].damage -= multiple * DAMAGE_THRESHOLD; + } + + return Plugin_Continue; +} + +/** + * 플레이어 사망 이벤트 핸들러 + * 처치자에게 보상을 지급하고 피해자에게 부활 메뉴를 표시합니다. + */ +public Action Event_OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) +{ + int victim = GetClientOfUserId(event.GetInt("userid")); + int attacker = GetClientOfUserId(event.GetInt("attacker")); + + // 부활 메뉴 표시 + if (victim > 0 && IsClientInGame(victim) && !IsFakeClient(victim)) + { + ShowReviveMenu(victim); + } + + // 처치자 보상 + if (attacker > 0 && IsClientInGame(attacker) && !IsFakeClient(attacker) && attacker != victim) + { + AddPlayerEXP(attacker, KILL_REWARD_EXP); + AddPlayerPoints(attacker, KILL_REWARD_POINTS); + + CPrintToChat(attacker, "{olive}[EXP&Point]{default} 적 처치! {rare}[%d 경험치]{default} {unique}[%d 포인트]{default} 획득!", + KILL_REWARD_EXP, KILL_REWARD_POINTS); + } + + return Plugin_Continue; +} + +/** + * 플레이어 재생 이벤트 핸들러 + * 속성을 다시 적용합니다. + */ +public Action Event_OnPlayerRegenerate(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + PrintToServer("[Event Handler] OnPlayerRegenerate: %d", client); + + if (client <= 0 || !IsClientInGame(client) || IsFakeClient(client)) + { + return Plugin_Continue; + } + + int team = GetClientTeam(client); + + // 팀별 스탯 적용 여부 확인 + if (team == 2 && !g_redEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + else if (team == 3 && !g_blueEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + + // 기존 속성 제거 후 재적용 + TF2Attrib_RemoveAll(client); + ApplyClassAttributes(client); + CreateTimer(0.5, Timer_ApplySharedAttribute, client); + + return Plugin_Continue; +} + +/** + * 플레이어 클래스 변경 이벤트 핸들러 + * 새 클래스의 속성을 적용합니다. + */ +public Action Event_OnPlayerChangeClass(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + PrintToServer("[Event Handler] OnPlayerChangeClass: %d", client); + + if (client <= 0 || !IsClientInGame(client) || IsFakeClient(client)) + { + return Plugin_Continue; + } + + int team = GetClientTeam(client); + + // 팀별 스탯 적용 여부 확인 + if (team == 2 && !g_redEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + else if (team == 3 && !g_blueEnableStatApply.IntValue) + { + TF2Attrib_RemoveAll(client); + return Plugin_Continue; + } + + // 기존 속성 제거 후 재적용 + TF2Attrib_RemoveAll(client); + ApplyClassAttributes(client); + CreateTimer(0.5, Timer_ApplySharedAttribute, client); + + return Plugin_Continue; +} + +/** + * 라운드 시작 이벤트 핸들러 + * 부활 시스템을 초기화합니다. + */ +public Action Event_OnRoundStart(Event event, const char[] name, bool dontBroadcast) +{ + PrintToServer("[Event Handler] OnRoundStart"); + + // 부활 포인트 타이머 시작 + g_Timer_AddRevivePoint = CreateTimer(REVIVE_POINT_INTERVAL, Timer_RevivePointUpdate, _); + + // 모든 플레이어의 부활 정보 초기화 + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || IsFakeClient(client) || !playerDataList[client].isLoadComplete) + { + continue; + } + + Revive_Initialize(client); + } + + return Plugin_Continue; +} + +/** + * 라운드 종료 이벤트 핸들러 + * 타이머를 정리합니다. + */ +public Action Event_OnRoundEnd(Event event, const char[] name, bool dontBroadcast) +{ + PrintToServer("[Event Handler] OnRoundEnd"); + + // 부활 포인트 타이머 정리 + if (g_Timer_AddRevivePoint != INVALID_HANDLE) + { + KillTimer(g_Timer_AddRevivePoint); + g_Timer_AddRevivePoint = INVALID_HANDLE; + } + + return Plugin_Continue; +} + +// ============================================================================ +// 부활 시스템 +// ============================================================================ + +/** + * 플레이어의 부활 정보를 초기화합니다. + * @param client 클라이언트 인덱스 + */ +void Revive_Initialize(int client) +{ + if (!IsClientInGame(client) || IsFakeClient(client)) + { + return; + } + + playerDataList[client].revivePoint = REVIVE_POINT_BASE; + playerDataList[client].reviveCount = REVIVE_COUNT_BASE; + + PrintToServer("[Event Handler] Revive initialized for client %d: Point=%d, Count=%d", + client, REVIVE_POINT_BASE, REVIVE_COUNT_BASE); +} + +/** + * 플레이어가 부활할 수 있는지 확인합니다. + * @param client 클라이언트 인덱스 + * @return 부활 가능 여부 + */ +bool Revive_CanRevive(int client) +{ + if (!IsClientInGame(client) || IsFakeClient(client)) + { + return false; + } + + // 이미 살아있음 + if (IsPlayerAlive(client)) + { + return false; + } + + // 포인트 부족 + if (playerDataList[client].point < playerDataList[client].revivePoint) + { + return false; + } + + // 부활 횟수 소진 + if (playerDataList[client].reviveCount <= 0) + { + return false; + } + + return true; +} + +/** + * 플레이어를 부활시킵니다. + * @param client 클라이언트 인덱스 + */ +void Revive_DoRevive(int client) +{ + if (!Revive_CanRevive(client)) + { + return; + } + + // 포인트 차감 + TakePlayerPoints(client, playerDataList[client].revivePoint); + + // 부활 비용 증가 및 횟수 감소 + playerDataList[client].revivePoint *= REVIVE_POINT_MULTIPLIER; + playerDataList[client].reviveCount--; + + // 플레이어 부활 + TF2_RespawnPlayer(client); + + // 효과음 재생 및 무적 부여 + EmitSoundToAll("misc/point_revive.mp3"); + TF2_AddCondition(client, TFCond_Ubercharged, 3.0); + + // 블루팀 회복 + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && GetClientTeam(i) == 3) + { + HealClient(i, REVIVE_HEAL_AMOUNT); + } + } + + // 부활 메시지 + char respawnText[256]; + Format(respawnText, sizeof(respawnText), "%s님이 부활권을 사용하여 부활하였습니다.", + playerDataList[client].basenick); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i)) + { + PrintCenterText(i, respawnText); + } + } + + PrintToServer("[Event Handler] Client %d revived. New cost: %d, Remaining: %d", + client, playerDataList[client].revivePoint, playerDataList[client].reviveCount); +} + +/** + * 플레이어의 부활 포인트를 증가시킵니다. + * @param client 클라이언트 인덱스 + * @param points 증가시킬 포인트 + */ +void Revive_AddPoint(int client, int points) +{ + if (!IsClientInGame(client) || IsFakeClient(client)) + { + return; + } + + playerDataList[client].revivePoint += points; +} + +/** + * 클래스별 속성을 적용합니다. + * @param client 클라이언트 인덱스 + */ +void ApplyClassAttributes(int client) +{ + TFClassType class = TF2_GetPlayerClass(client); + + // 각 클래스별로 속성 적용 + switch (class) + { + case TFClass_Scout: + { + for (int i = 0; i < sizeof(playerDataList[client].scoutAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].scoutAttributeData[i], scoutAttributeTable); + } + } + case TFClass_Soldier: + { + for (int i = 0; i < sizeof(playerDataList[client].soldierAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].soldierAttributeData[i], soldierAttributeTable); + } + } + case TFClass_Pyro: + { + for (int i = 0; i < sizeof(playerDataList[client].pyroAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].pyroAttributeData[i], pyroAttributeTable); + } + } + case TFClass_DemoMan: + { + for (int i = 0; i < sizeof(playerDataList[client].demomanAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].demomanAttributeData[i], demomanAttributeTable); + } + } + case TFClass_Heavy: + { + for (int i = 0; i < sizeof(playerDataList[client].heavyAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].heavyAttributeData[i], heavyAttributeTable); + } + } + case TFClass_Engineer: + { + for (int i = 0; i < sizeof(playerDataList[client].engineerAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].engineerAttributeData[i], engineerAttributeTable); + } + } + case TFClass_Medic: + { + for (int i = 0; i < sizeof(playerDataList[client].medicAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].medicAttributeData[i], medicAttributeTable); + } + } + case TFClass_Sniper: + { + for (int i = 0; i < sizeof(playerDataList[client].sniperAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].sniperAttributeData[i], sniperAttributeTable); + } + } + case TFClass_Spy: + { + for (int i = 0; i < sizeof(playerDataList[client].spyAttributeData); i++) + { + ApplyAttributeData(client, playerDataList[client].spyAttributeData[i], spyAttributeTable); + } + } + } +} + +/** + * 개별 속성 데이터를 적용합니다. + * @param client 클라이언트 인덱스 + * @param attributeData 속성 데이터 + * @param attributeTable 속성 테이블 + */ +void ApplyAttributeData(int client, AttributeUpgrade attributeData, any attributeTable[][AttributeData]) +{ + int id = attributeData.id; + int upgrade = attributeData.upgrade; + + if (upgrade <= 0) + { + return; + } + + float result = 0.0; + + if (attributeTable[id].additiveMode == ADDITIVE_NUMBER) + { + result = attributeTable[id].defaultValue + (float(upgrade) * attributeTable[id].value); + TF2Attrib_SetByName(client, attributeTable[id].uid, result); + } + else if (attributeTable[id].additiveMode == ADDITIVE_PERCENT) + { + result = (attributeTable[id].defaultValue * 0.01) + (float(upgrade) * attributeTable[id].value * 0.01); + TF2Attrib_SetByName(client, attributeTable[id].uid, result); + } + else if (attributeTable[id].additiveMode == MINUS_NUMBER) + { + result = attributeTable[id].defaultValue - (float(upgrade) * attributeTable[id].value); + TF2Attrib_SetByName(client, attributeTable[id].uid, result); + } + else if (attributeTable[id].additiveMode == MINUS_PERCENT) + { + result = (attributeTable[id].defaultValue * 0.01) - (float(upgrade) * attributeTable[id].value * 0.01); + TF2Attrib_SetByName(client, attributeTable[id].uid, result); + } +} diff --git a/includes/exp_level_system.inc b/includes/exp_level_system.inc new file mode 100644 index 0000000..57a3caa --- /dev/null +++ b/includes/exp_level_system.inc @@ -0,0 +1,280 @@ +/** + * ============================================================================= + * TF2 Level System - 경험치/레벨업 시스템 모듈 + * ============================================================================= + * + * 설명: 플레이어의 경험치 획득, 레벨업 처리, 보상 지급 등을 관리하는 핵심 모듈 + * + * 주요 기능: + * - 경험치 획득 및 레벨업 처리 + * - 데미지 누적 추적 및 보상 + * - 타이머 기반 보상 + * - 레벨업 이펙트 및 사운드 + */ + +#if defined _exp_level_system_included + #endinput +#endif +#define _exp_level_system_included + +// ============================================================================= +// 경험치 테이블 (레벨당 필요 경험치) +// ============================================================================= + +#define EXP_TABLE_SIZE 80 + +// 경험치 테이블 (80 레벨) +static const int EXP_TABLE[EXP_TABLE_SIZE] = { + 25, 60, 110, 175, 250, 350, 475, 625, 800, 1000, + 1225, 1475, 1750, 2050, 2375, 2725, 3100, 3500, 3925, 4375, + 4850, 5350, 5875, 6425, 7000, 7600, 8225, 8875, 9550, 10250, + 10975, 11725, 12500, 13300, 14125, 14975, 15850, 16750, 17675, 18625, + 19600, 20600, 21625, 22675, 23750, 24850, 25975, 27125, 28300, 29500, + 30750, 32050, 33400, 34800, 36250, 37750, 39300, 40900, 42550, 44250, + 46000, 47800, 49650, 51550, 53500, 55500, 57550, 59650, 61800, 64000, + 66250, 68550, 70900, 73300, 75750, 78250, 80800, 83400, 86050, 88750 +}; + +// ============================================================================= +// 보상 상수 +// ============================================================================= + +// 타이머 보상 (10분마다) +const float REWARD_TIMER_INTERVAL = 600.0; +const int REWARD_POINT_TIMER = 100; +const int REWARD_EXP_TIMER = 50; + +// 데미지 누적 보상 (2000 데미지마다) +const int REWARD_DAMAGE_THRESHOLD = 2000; +const int REWARD_POINT_DAMAGE = 40; +const int REWARD_EXP_DAMAGE = 20; + +// 처치 보상 +const int REWARD_POINT_KILL = 20; +const int REWARD_EXP_KILL = 20; + +// 레벨업 보상 +const int REWARD_SKILLPOINT_LEVELUP = 3; + +// ============================================================================= +// 레벨/경험치 관리 함수 +// ============================================================================= + +/** + * 플레이어에게 경험치를 추가하고 레벨업을 처리합니다. + * + * @param client 클라이언트 인덱스 + * @param exp 추가할 경험치 + * @noreturn + */ +stock void ExpLevel_AddExp(int client, int exp) +{ + if (!IsValidClient(client)) + return; + + playerDataList[client].exp += exp; + + // 레벨업 체크 + if (playerDataList[client].level < (EXP_TABLE_SIZE - 1) && + playerDataList[client].exp >= EXP_TABLE[playerDataList[client].level]) + { + // 남은 경험치 계산 + int delta = playerDataList[client].exp - EXP_TABLE[playerDataList[client].level]; + + // 현재 레벨 경험치 소모 + playerDataList[client].exp -= EXP_TABLE[playerDataList[client].level]; + + // 레벨업 처리 + ExpLevel_ProcessLevelUp(client); + + // 남은 경험치로 재귀 호출 (연속 레벨업 처리) + if (delta > 0) + { + ExpLevel_AddExp(client, delta); + } + } +} + +/** + * 플레이어의 레벨업을 처리합니다. + * + * @param client 클라이언트 인덱스 + * @noreturn + */ +stock void ExpLevel_ProcessLevelUp(int client) +{ + if (!IsValidClient(client)) + return; + + // 레벨 증가 + playerDataList[client].level++; + + // 스킬포인트 보상 + playerDataList[client].skillpoint += REWARD_SKILLPOINT_LEVELUP; + + // 닉네임 업데이트 + ExpLevel_UpdateNickname(client); + + // 이펙트 및 사운드 + ExpLevel_ShowLevelUpEffect(client); + ExpLevel_PlayLevelUpSound(client); + + // 채팅 메시지 + CPrintToChat(client, "{green}[레벨업]{default} 축하합니다! 레벨이 올라 {orange}%d{default} 레벨이 되었습니다!", + playerDataList[client].level); +} + +/** + * 플레이어에게 포인트를 추가합니다. + * + * @param client 클라이언트 인덱스 + * @param points 추가할 포인트 + * @noreturn + */ +stock void ExpLevel_AddPoints(int client, int points) +{ + if (!IsValidClient(client)) + return; + + playerDataList[client].point += points; +} + +/** + * 특정 레벨에 필요한 경험치를 반환합니다. + * + * @param level 레벨 (0~79) + * @return 필요 경험치 + */ +stock int ExpLevel_GetExpForLevel(int level) +{ + if (level < 0 || level >= EXP_TABLE_SIZE) + return 0; + + return EXP_TABLE[level]; +} + +/** + * 최대 레벨을 반환합니다. + * + * @return 최대 레벨 + */ +stock int ExpLevel_GetMaxLevel() +{ + return EXP_TABLE_SIZE; +} + +// ============================================================================= +// 데미지 추적 및 보상 +// ============================================================================= + +/** + * 플레이어의 데미지를 추적하고 임계값 도달 시 보상을 지급합니다. + * + * @param client 클라이언트 인덱스 + * @param damage 가한 데미지 + * @noreturn + */ +stock void ExpLevel_TrackDamage(int client, int damage) +{ + if (!IsValidClient(client)) + return; + + playerDataList[client].damage += damage; + + // 데미지 임계값 도달 체크 + if (playerDataList[client].damage >= REWARD_DAMAGE_THRESHOLD) + { + // 달성 횟수 계산 (여러 번 달성 가능) + int multiple = playerDataList[client].damage / REWARD_DAMAGE_THRESHOLD; + + // 보상 지급 + ExpLevel_AddExp(client, REWARD_EXP_DAMAGE * multiple); + ExpLevel_AddPoints(client, REWARD_POINT_DAMAGE * multiple); + + // 채팅 메시지 + CPrintToChat(client, "{olive}[EXP&Point]{default} 데미지 누적 달성! {rare}[%d 경험치]{default} {unique}[%d 포인트]{default} 획득!", + REWARD_EXP_DAMAGE * multiple, REWARD_POINT_DAMAGE * multiple); + + // 사용한 데미지 차감 + playerDataList[client].damage -= multiple * REWARD_DAMAGE_THRESHOLD; + } +} + +// ============================================================================= +// 닉네임 업데이트 +// ============================================================================= + +/** + * 플레이어의 닉네임을 현재 레벨로 업데이트합니다. + * + * @param client 클라이언트 인덱스 + * @noreturn + */ +stock void ExpLevel_UpdateNickname(int client) +{ + if (!IsValidClient(client)) + return; + + char prefix[12]; + char prefixName[255]; + + Format(prefix, sizeof(prefix), "[Lv%d]", playerDataList[client].level); + Format(prefixName, sizeof(prefixName), "%s%s", prefix, playerDataList[client].basenick); + + SetClientInfo(client, "name", prefixName); +} + +// ============================================================================= +// 레벨업 이펙트 +// ============================================================================= + +/** + * 레벨업 시각 효과를 표시합니다. + * + * @param client 클라이언트 인덱스 + * @noreturn + */ +stock void ExpLevel_ShowLevelUpEffect(int client) +{ + if (!IsValidClient(client)) + return; + + if (!IsClientInGame(client) || !IsPlayerAlive(client)) + return; + + // 파티클 효과 표시 + AttachParticle(client, "bl_killtaunt", "head", 0.0, 0.5); + AttachParticle(client, "achieved", "head", 0.0, 0.5); +} + +/** + * 레벨업 사운드를 재생합니다. + * + * @param client 클라이언트 인덱스 + * @noreturn + */ +stock void ExpLevel_PlayLevelUpSound(int client) +{ + if (!IsValidClient(client)) + return; + + if (!IsClientInGame(client)) + return; + + EmitSoundToClient(client, "misc/achievement_earned.wav", _, _, SNDLEVEL_RAIDSIREN); +} + +// ============================================================================= +// 유틸리티 함수 +// ============================================================================= + +/** + * 클라이언트가 유효한지 확인합니다. + * + * @param client 클라이언트 인덱스 + * @return 유효하면 true + */ +stock bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients && IsClientInGame(client) && !IsFakeClient(client)); +} diff --git a/includes/menu_system.inc b/includes/menu_system.inc new file mode 100644 index 0000000..aed6edc --- /dev/null +++ b/includes/menu_system.inc @@ -0,0 +1,1332 @@ +/** + * ============================================================================= + * TF2 Level System - Menu/UI Module + * ============================================================================= + * + * 메뉴 시스템 및 UI 표시를 담당하는 모듈 + * - 메인 메뉴, 플레이어 정보, 클래스 스탯 메뉴 + * - 부활, 스킬 초기화, 무기 강화 메뉴 + * - HUD 표시 (레드팀만) + */ + +#if defined _menu_system_included + #endinput +#endif +#define _menu_system_included + +// ============================================================================ +// 전역 변수 +// ============================================================================ + +// 메뉴 페이지 네비게이션용 +int g_prevOpenMenuPage = -1; + +// ============================================================================ +// HUD 표시 함수 +// ============================================================================ + +/** + * HUD 텍스트를 주기적으로 표시 (레드팀만) + * 타이머로 1초마다 호출됨 + */ +public Action Menu_ShowHUD(Handle timer) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || IsFakeClient(client)) + { + continue; + } + + // 블루팀(3번)은 HUD 표시 안함 + int team = GetClientTeam(client); + if (team == 3) // 블루팀 + { + continue; + } + + // 레드팀(2번)만 HUD 표시 + char hintText[512]; + Format(hintText, sizeof(hintText), + "레벨 : %d\n경험치 : %d/%d\n포인트 : %d", + playerDataList[client].level, + playerDataList[client].exp, + expTable[playerDataList[client].level], + playerDataList[client].point + ); + + SetHudTextParams(0.01, 0.0, 1.0, 255, 200, 0, 255, 0, 0.0, 0.0, 0.1); + ShowHudText(client, 4, hintText); + } + + return Plugin_Continue; +} + +// ============================================================================ +// 메인 메뉴 +// ============================================================================ + +/** + * 메인 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowMain(int client) +{ + Menu menu = CreateMenu(MenuHandler_Main); + menu.SetTitle("<< 수상한 거래 서버에 오신걸 환영합니다 >>"); + + menu.AddItem("info", "내정보"); + menu.AddItem("classStat", "클래스 스탯"); + menu.AddItem("shop", "상점"); + menu.AddItem("weaponStat", "무기 강화"); + menu.AddItem("revive", "부활권"); + menu.AddItem("inventory", "인벤토리"); + menu.AddItem("skillReset", "스킬초기화"); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 메인 메뉴 핸들러 + */ +public int MenuHandler_Main(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + + if (action != MenuAction_Select) + return 0; + + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "info")) + { + Menu_ShowPlayerInfo(client); + } + else if (StrEqual(info, "classStat")) + { + Menu_ShowClassStat(client); + } + else if (StrEqual(info, "revive")) + { + Menu_ShowRevive(client); + } + else if (StrEqual(info, "skillReset")) + { + Menu_ShowSkillReset(client); + } + else if (StrEqual(info, "weaponStat")) + { + Menu_ShowWeaponUpgrade(client); + } + else + { + PrintToChat(client, "아직 구현되지 않은 메뉴입니다: %s", info); + } + + return 0; +} + +// ============================================================================ +// 플레이어 정보 메뉴 +// ============================================================================ + +/** + * 플레이어 정보 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowPlayerInfo(int client) +{ + Menu menu = CreateMenu(MenuHandler_PlayerInfo); + + char buffer[128]; + char name[64]; + GetClientName(client, name, sizeof(name)); + + menu.SetTitle("당신의 정보입니다."); + + Format(buffer, sizeof(buffer), "이름 : %s", name); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + Format(buffer, sizeof(buffer), "돈 : %d", playerDataList[client].point); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + Format(buffer, sizeof(buffer), "레벨 : %d", playerDataList[client].level); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + Format(buffer, sizeof(buffer), "경험치 : %d/%d", playerDataList[client].exp, expTable[playerDataList[client].level]); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + Format(buffer, sizeof(buffer), "강화 : %d", g_maxUpgrade); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + Format(buffer, sizeof(buffer), "남은 스킬포인트 : %d", playerDataList[client].skillpoint); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + menu.AddItem("back", "상위 메뉴로 돌아가기"); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 플레이어 정보 메뉴 핸들러 + */ +public int MenuHandler_PlayerInfo(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "back")) + { + Menu_ShowMain(client); + } + } + + return 0; +} + +// ============================================================================ +// 클래스 스탯 메뉴 +// ============================================================================ + +/** + * 클래스 선택 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowClassStat(int client) +{ + Menu menu = CreateMenu(MenuHandler_ClassStat); + menu.SetTitle("클래스 스탯"); + + menu.AddItem("scout", "스카웃"); + menu.AddItem("soldier", "솔져"); + menu.AddItem("pyro", "파이로"); + menu.AddItem("demoman", "데모맨"); + menu.AddItem("heavy", "헤비"); + menu.AddItem("engineer", "엔지니어"); + menu.AddItem("medic", "메딕"); + menu.AddItem("sniper", "스나이퍼"); + menu.AddItem("spy", "스파이"); + menu.AddItem("hale", "헤일(사용불가)"); + menu.AddItem("shared", "공용"); + + menu.AddItem("back", "상위 메뉴로 돌아가기"); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 클래스 선택 메뉴 핸들러 + */ +public int MenuHandler_ClassStat(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "back")) + { + Menu_ShowMain(client); + } + else + { + // 선택한 클래스의 상세 스탯 메뉴 표시 + Menu_ShowClassStatDetail(client, info); + } + } + + return 0; +} + +/** + * 클래스 상세 스탯 메뉴를 표시 + * @param client 클라이언트 인덱스 + * @param classID 클래스 ID (예: "scout", "soldier", etc.) + */ +void Menu_ShowClassStatDetail(int client, const char[] classID) +{ + Menu menu = CreateMenu(MenuHandler_ClassStatDetail); + + // 클래스별로 메뉴 구성 + if (StrEqual(classID, "scout")) + { + menu.SetTitle("스카웃"); + + for (int i = 0; i < sizeof(scoutAttributeTable); i++) + { + if (!StrEqual(scoutAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", scoutAttributeTable[i].title); + + if (scoutAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, scoutAttributeTable[i].point, + playerDataList[client].scoutAttributeData[i].upgrade, scoutAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, scoutAttributeTable[i].value, scoutAttributeTable[i].point, + playerDataList[client].scoutAttributeData[i].upgrade, scoutAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "scout_%s", scoutAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "medic")) + { + menu.SetTitle("메딕"); + + for (int i = 0; i < sizeof(medicAttributeTable); i++) + { + if (!StrEqual(medicAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", medicAttributeTable[i].title); + + if (medicAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, medicAttributeTable[i].point, + playerDataList[client].medicAttributeData[i].upgrade, medicAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, medicAttributeTable[i].value, medicAttributeTable[i].point, + playerDataList[client].medicAttributeData[i].upgrade, medicAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "medic_%s", medicAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "soldier")) + { + menu.SetTitle("솔져"); + + for (int i = 0; i < sizeof(soldierAttributeTable); i++) + { + if (!StrEqual(soldierAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", soldierAttributeTable[i].title); + + if (soldierAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, soldierAttributeTable[i].point, + playerDataList[client].soldierAttributeData[i].upgrade, soldierAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, soldierAttributeTable[i].value, soldierAttributeTable[i].point, + playerDataList[client].soldierAttributeData[i].upgrade, soldierAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "soldier_%s", soldierAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "pyro")) + { + menu.SetTitle("파이로"); + + for (int i = 0; i < sizeof(pyroAttributeTable); i++) + { + if (!StrEqual(pyroAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", pyroAttributeTable[i].title); + + if (pyroAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, pyroAttributeTable[i].point, + playerDataList[client].pyroAttributeData[i].upgrade, pyroAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, pyroAttributeTable[i].value, pyroAttributeTable[i].point, + playerDataList[client].pyroAttributeData[i].upgrade, pyroAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "pyro_%s", pyroAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "spy")) + { + menu.SetTitle("스파이"); + + for (int i = 0; i < sizeof(spyAttributeTable); i++) + { + if (!StrEqual(spyAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", spyAttributeTable[i].title); + + if (spyAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, spyAttributeTable[i].point, + playerDataList[client].spyAttributeData[i].upgrade, spyAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, spyAttributeTable[i].value, spyAttributeTable[i].point, + playerDataList[client].spyAttributeData[i].upgrade, spyAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "spy_%s", spyAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "demoman")) + { + menu.SetTitle("데모맨"); + + for (int i = 0; i < sizeof(demomanAttributeTable); i++) + { + if (!StrEqual(demomanAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", demomanAttributeTable[i].title); + + if (demomanAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, demomanAttributeTable[i].point, + playerDataList[client].demomanAttributeData[i].upgrade, demomanAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, demomanAttributeTable[i].value, demomanAttributeTable[i].point, + playerDataList[client].demomanAttributeData[i].upgrade, demomanAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "demoman_%s", demomanAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "sniper")) + { + menu.SetTitle("스나이퍼"); + + for (int i = 0; i < sizeof(sniperAttributeTable); i++) + { + if (!StrEqual(sniperAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", sniperAttributeTable[i].title); + + if (sniperAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, sniperAttributeTable[i].point, + playerDataList[client].sniperAttributeData[i].upgrade, sniperAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, sniperAttributeTable[i].value, sniperAttributeTable[i].point, + playerDataList[client].sniperAttributeData[i].upgrade, sniperAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "sniper_%s", sniperAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "engineer")) + { + menu.SetTitle("엔지니어"); + + for (int i = 0; i < sizeof(engineerAttributeTable); i++) + { + if (!StrEqual(engineerAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", engineerAttributeTable[i].title); + + if (engineerAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, engineerAttributeTable[i].point, + playerDataList[client].engineerAttributeData[i].upgrade, engineerAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, engineerAttributeTable[i].value, engineerAttributeTable[i].point, + playerDataList[client].engineerAttributeData[i].upgrade, engineerAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "engineer_%s", engineerAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "heavy")) + { + menu.SetTitle("헤비"); + + for (int i = 0; i < sizeof(heavyAttributeTable); i++) + { + if (!StrEqual(heavyAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", heavyAttributeTable[i].title); + + if (heavyAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, heavyAttributeTable[i].point, + playerDataList[client].heavyAttributeData[i].upgrade, heavyAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, heavyAttributeTable[i].value, heavyAttributeTable[i].point, + playerDataList[client].heavyAttributeData[i].upgrade, heavyAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "heavy_%s", heavyAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "hale")) + { + menu.SetTitle("헤일(사용불가)"); + + for (int i = 0; i < sizeof(haleAttributeTable); i++) + { + if (!StrEqual(haleAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", haleAttributeTable[i].title); + + if (haleAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, haleAttributeTable[i].point, + playerDataList[client].haleAttributeData[i].upgrade, haleAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, haleAttributeTable[i].value, haleAttributeTable[i].point, + playerDataList[client].haleAttributeData[i].upgrade, haleAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "hale_%s", haleAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + else if (StrEqual(classID, "shared")) + { + menu.SetTitle("공용"); + + for (int i = 0; i < sizeof(sharedAttributeTable); i++) + { + if (!StrEqual(sharedAttributeTable[i].uid, "")) + { + char buffer[128]; + Format(buffer, sizeof(buffer), "%s", sharedAttributeTable[i].title); + + if (sharedAttributeTable[i].isDisableDrawValue) + { + Format(buffer, sizeof(buffer), buffer, sharedAttributeTable[i].point, + playerDataList[client].sharedAttributeData[i].upgrade, sharedAttributeTable[i].max); + } + else + { + Format(buffer, sizeof(buffer), buffer, sharedAttributeTable[i].value, sharedAttributeTable[i].point, + playerDataList[client].sharedAttributeData[i].upgrade, sharedAttributeTable[i].max); + } + + char key[64]; + Format(key, sizeof(key), "shared_%s", sharedAttributeTable[i].uid); + menu.AddItem(key, buffer); + } + } + } + + menu.AddItem("back", "상위 메뉴로 돌아가기"); + + menu.ExitButton = true; + + // 메뉴 페이지 복원 + if (g_prevOpenMenuPage == -1) + { + menu.Display(client, MENU_TIME_FOREVER); + } + else + { + DisplayMenuAtItem(menu, client, g_prevOpenMenuPage, MENU_TIME_FOREVER); + } + + g_prevOpenMenuPage = -1; +} + +/** + * 클래스 상세 스탯 메뉴 핸들러 + */ +public int MenuHandler_ClassStatDetail(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "back")) + { + Menu_ShowClassStat(client); + } + else if (StrContains(info, "scout") != -1) + { + if (scoutAttributeTable[item].max > playerDataList[client].scoutAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= scoutAttributeTable[item].point) + { + playerDataList[client].skillpoint -= scoutAttributeTable[item].point; + playerDataList[client].scoutAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Scout[%d] upgrade=%d", + client, item, playerDataList[client].scoutAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "scout"); + } + else if (StrContains(info, "medic") != -1) + { + if (medicAttributeTable[item].max > playerDataList[client].medicAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= medicAttributeTable[item].point) + { + playerDataList[client].skillpoint -= medicAttributeTable[item].point; + playerDataList[client].medicAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Medic[%d] upgrade=%d", + client, item, playerDataList[client].medicAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "medic"); + } + else if (StrContains(info, "soldier") != -1) + { + if (soldierAttributeTable[item].max > playerDataList[client].soldierAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= soldierAttributeTable[item].point) + { + playerDataList[client].skillpoint -= soldierAttributeTable[item].point; + playerDataList[client].soldierAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Soldier[%d] upgrade=%d", + client, item, playerDataList[client].soldierAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "soldier"); + } + else if (StrContains(info, "pyro") != -1) + { + if (pyroAttributeTable[item].max > playerDataList[client].pyroAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= pyroAttributeTable[item].point) + { + playerDataList[client].skillpoint -= pyroAttributeTable[item].point; + playerDataList[client].pyroAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Pyro[%d] upgrade=%d", + client, item, playerDataList[client].pyroAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "pyro"); + } + else if (StrContains(info, "spy") != -1) + { + if (spyAttributeTable[item].max > playerDataList[client].spyAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= spyAttributeTable[item].point) + { + playerDataList[client].skillpoint -= spyAttributeTable[item].point; + playerDataList[client].spyAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Spy[%d] upgrade=%d", + client, item, playerDataList[client].spyAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "spy"); + } + else if (StrContains(info, "demoman") != -1) + { + if (demomanAttributeTable[item].max > playerDataList[client].demomanAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= demomanAttributeTable[item].point) + { + playerDataList[client].skillpoint -= demomanAttributeTable[item].point; + playerDataList[client].demomanAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Demoman[%d] upgrade=%d", + client, item, playerDataList[client].demomanAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "demoman"); + } + else if (StrContains(info, "sniper") != -1) + { + if (sniperAttributeTable[item].max > playerDataList[client].sniperAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= sniperAttributeTable[item].point) + { + playerDataList[client].skillpoint -= sniperAttributeTable[item].point; + playerDataList[client].sniperAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Sniper[%d] upgrade=%d", + client, item, playerDataList[client].sniperAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "sniper"); + } + else if (StrContains(info, "engineer") != -1) + { + if (engineerAttributeTable[item].max > playerDataList[client].engineerAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= engineerAttributeTable[item].point) + { + playerDataList[client].skillpoint -= engineerAttributeTable[item].point; + playerDataList[client].engineerAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Engineer[%d] upgrade=%d", + client, item, playerDataList[client].engineerAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "engineer"); + } + else if (StrContains(info, "heavy") != -1) + { + if (heavyAttributeTable[item].max > playerDataList[client].heavyAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= heavyAttributeTable[item].point) + { + playerDataList[client].skillpoint -= heavyAttributeTable[item].point; + playerDataList[client].heavyAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Heavy[%d] upgrade=%d", + client, item, playerDataList[client].heavyAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "heavy"); + } + else if (StrContains(info, "hale") != -1) + { + if (haleAttributeTable[item].max > playerDataList[client].haleAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= haleAttributeTable[item].point) + { + playerDataList[client].skillpoint -= haleAttributeTable[item].point; + playerDataList[client].haleAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Hale[%d] upgrade=%d", + client, item, playerDataList[client].haleAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "hale"); + } + else if (StrContains(info, "shared") != -1) + { + if (sharedAttributeTable[item].max > playerDataList[client].sharedAttributeData[item].upgrade) + { + if (playerDataList[client].skillpoint >= sharedAttributeTable[item].point) + { + playerDataList[client].skillpoint -= sharedAttributeTable[item].point; + playerDataList[client].sharedAttributeData[item].upgrade++; + + // 즉시 저장 + UpdateUserData(client); + UpdateAttributeData(client); + + PrintToServer("스탯 찍음: Client %d - Shared[%d] upgrade=%d", + client, item, playerDataList[client].sharedAttributeData[item].upgrade); + } + else + { + PrintToChat(client, "스킬 포인트가 모자라 강화할 수 없습니다."); + } + } + + g_prevOpenMenuPage = GetMenuSelectionPosition(); + Menu_ShowClassStatDetail(client, "shared"); + } + } + + return 0; +} + +// ============================================================================ +// 부활 메뉴 +// ============================================================================ + +/** + * 부활 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowRevive(int client) +{ + int team = GetClientTeam(client); + + if (team == 3) + { + CPrintToChat(client, "{red}[경고]{default} 블루팀은 리스폰 메뉴를 사용할 수 없습니다!"); + return; + } + + Menu menu = CreateMenu(MenuHandler_Revive); + + char titleBuffer[512]; + Format(titleBuffer, sizeof(titleBuffer), + "헤일 모드 - 부활 메뉴\n―――――――――――――――――\n현재 사용가능한 포인트 : %d\n―――――――――――――――――\n포인트를 사용하면 즉시 부활합니다.\n라운드 시간이 지나거나 부활하면 가격이 비싸집니다.", + playerDataList[client].point); + menu.SetTitle(titleBuffer); + + char pointBuffer[128]; + Format(pointBuffer, sizeof(pointBuffer), "%d원으로 부활합니다.(%d회 남음)", + playerDataList[client].revivePoint, playerDataList[client].reviveCount); + + menu.AddItem("accept", pointBuffer); + menu.AddItem("cancel", "부활하지 않습니다."); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 부활 메뉴 핸들러 + */ +public int MenuHandler_Revive(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "accept")) + { + if (!IsClientInGame(client) || IsPlayerAlive(client)) + { + PrintToChat(client, "당신은 이미 살아있습니다!"); + return 0; + } + + if (playerDataList[client].point < playerDataList[client].revivePoint) + { + PrintToChat(client, "포인트가 부족합니다!"); + return 0; + } + + if (playerDataList[client].reviveCount <= 0) + { + PrintToChat(client, "부활 횟수를 전부 소진하였습니다!"); + return 0; + } + + TakePlayerPoints(client, playerDataList[client].revivePoint); + + playerDataList[client].revivePoint *= g_addRevivePointOnRevive; + playerDataList[client].reviveCount--; + + TF2_RespawnPlayer(client); + + EmitSoundToAll("misc/point_revive.mp3"); + TF2_AddCondition(client, TFCond_Ubercharged, 3.0); + + char respawnText[256]; + Format(respawnText, sizeof(respawnText), + "%s님이 부활권을 사용하여 부활하였습니다.", playerDataList[client].basenick); + + // 블루팀 모두 체력 회복 + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && GetClientTeam(i) == 3) + { + HealClient(i, g_healOnRevive); + } + } + + // 모든 플레이어에게 알림 + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + PrintCenterText(i, respawnText); + } + } + else if (StrEqual(info, "cancel")) + { + delete menu; + } + } + + return 0; +} + +// ============================================================================ +// 스킬 초기화 메뉴 +// ============================================================================ + +/** + * 스킬 초기화 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowSkillReset(int client) +{ + Menu menu = CreateMenu(MenuHandler_SkillReset); + + char titleBuffer[512]; + Format(titleBuffer, sizeof(titleBuffer), + "헤일 모드 - 스킬 초기화\n―――――――――――――――――\n―――――――――――――――――\n포인트를 사용하면 스킬을 초기화합니다.\n", + playerDataList[client].point); + menu.SetTitle(titleBuffer); + + char pointBuffer[128]; + Format(pointBuffer, sizeof(pointBuffer), "%d원으로 초기화합니다.", g_skillResetPoint); + + menu.AddItem("accept", pointBuffer); + menu.AddItem("cancel", "초기화하지 않습니다."); + + menu.AddItem("back", "상위 메뉴로 돌아가기"); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 스킬 초기화 메뉴 핸들러 + */ +public int MenuHandler_SkillReset(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "accept")) + { + if (playerDataList[client].point < g_skillResetPoint) + { + PrintToChat(client, "포인트가 부족합니다!"); + return 0; + } + + TakePlayerPoints(client, g_skillResetPoint); + + // 모든 클래스의 스탯 초기화 + for (int i = 0; i < sizeof(playerDataList[client].scoutAttributeData); i++) + { + playerDataList[client].scoutAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].medicAttributeData); i++) + { + playerDataList[client].medicAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].soldierAttributeData); i++) + { + playerDataList[client].soldierAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].pyroAttributeData); i++) + { + playerDataList[client].pyroAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].spyAttributeData); i++) + { + playerDataList[client].spyAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].demomanAttributeData); i++) + { + playerDataList[client].demomanAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].sniperAttributeData); i++) + { + playerDataList[client].sniperAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].engineerAttributeData); i++) + { + playerDataList[client].engineerAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].heavyAttributeData); i++) + { + playerDataList[client].heavyAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].haleAttributeData); i++) + { + playerDataList[client].haleAttributeData[i].upgrade = 0; + } + + for (int i = 0; i < sizeof(playerDataList[client].sharedAttributeData); i++) + { + playerDataList[client].sharedAttributeData[i].upgrade = 0; + } + + // 스킬 포인트 재계산 + playerDataList[client].skillpoint = 0; + playerDataList[client].skillpoint += playerDataList[client].level * 3; + + PrintToChat(client, "스킬이 초기화되었습니다!"); + } + else if (StrEqual(info, "cancel")) + { + // 아무것도 하지 않음 + } + } + + return 0; +} + +// ============================================================================ +// 무기 강화 메뉴 +// ============================================================================ + +/** + * 무기 강화 메뉴를 표시 + * @param client 클라이언트 인덱스 + */ +void Menu_ShowWeaponUpgrade(int client) +{ + Menu menu = CreateMenu(MenuHandler_WeaponUpgrade); + + char titleBuffer[1024]; + Format(titleBuffer, sizeof(titleBuffer), + "헤일 모드 - 강화 메뉴\n―――――――――――――――――\n현재 사용가능한 포인트 : %d\n―――――――――――――――――\n현재 강화 %d -> %d로 강화\n강화 성공 %.0f%%\n강화 실패 %.0f%%\n강화 파괴 %.0f%%\n―――――――――――――――――\n강화 하시겠습니까?", + playerDataList[client].point, + playerDataList[client].weaponAttributeData[0].upgrade, + playerDataList[client].weaponAttributeData[0].upgrade + 1, + weaponUpgradeSuccessTable[playerDataList[client].weaponAttributeData[0].upgrade] * float(100), + weaponUpgradeMissTable[playerDataList[client].weaponAttributeData[0].upgrade] * float(100), + weaponUpgradeResetTable[playerDataList[client].weaponAttributeData[0].upgrade] * float(100)); + menu.SetTitle(titleBuffer); + + char yesBuffer[128]; + Format(yesBuffer, sizeof(yesBuffer), "예(%d)", + weaponUpgradeCostTable[playerDataList[client].weaponAttributeData[0].upgrade]); + + menu.AddItem("accept", yesBuffer); + menu.AddItem("cancel", "아니오"); + + menu.AddItem("back", "상위 메뉴로 돌아가기"); + + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +/** + * 무기 강화 메뉴 핸들러 + */ +public int MenuHandler_WeaponUpgrade(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + } + else if (action == MenuAction_Select) + { + char info[32]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "accept")) + { + int upgrade = playerDataList[client].weaponAttributeData[0].upgrade; + + if (playerDataList[client].point < weaponUpgradeCostTable[upgrade]) + { + PrintToChat(client, "포인트가 부족합니다!"); + return 0; + } + + PrintCenterText(client, "강화 중 ..."); + PrintToChat(client, "강화 중 ..."); + EmitSoundToClient(client, "misc/reinforcement.mp3", _, _, SNDLEVEL_RAIDSIREN); + CreateTimer(5.5, Timer_WeaponUpgrade, client); + } + else if (StrEqual(info, "back")) + { + Menu_ShowMain(client); + } + } + + return 0; +} + +/** + * 무기 강화 타이머 콜백 + */ +public Action Timer_WeaponUpgrade(Handle timer, any client) +{ + if (!IsClientInGame(client)) + { + return Plugin_Continue; + } + + int upgrade = playerDataList[client].weaponAttributeData[0].upgrade; + int random = GetRandomInt(1, 100); + + int successPercent = RoundToFloor(weaponUpgradeSuccessTable[upgrade] * float(100)); + int missPercent = RoundToFloor(weaponUpgradeMissTable[upgrade] * float(100)); + int resetPercent = RoundToFloor(weaponUpgradeResetTable[upgrade] * float(100)); + + TakePlayerPoints(client, weaponUpgradeCostTable[upgrade]); + + char upgradeText[256]; + + if (random <= successPercent) + { + // 성공 + Format(upgradeText, sizeof(upgradeText), + "%d번째 강화에 성공하였습니다.", upgrade + 1); + PrintToChat(client, upgradeText); + PrintCenterText(client, upgradeText); + + if (upgrade + 1 >= 7) + { + Format(upgradeText, sizeof(upgradeText), + "%s님이 %d번째 강화에 성공했습니다.", playerDataList[client].basenick, upgrade + 1); + PrintToChat(client, upgradeText); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + PrintCenterText(i, upgradeText); + } + + EmitSoundToAll("misc/success1.mp3"); + } + else + { + EmitSoundToClient(client, "misc/success1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + + playerDataList[client].weaponAttributeData[0].upgrade++; + } + else + { + random -= successPercent; + + if (random <= missPercent) + { + // 실패 + Format(upgradeText, sizeof(upgradeText), + "%d번째 강화에 실패했습니다.", upgrade + 1); + PrintToChat(client, upgradeText); + PrintCenterText(client, upgradeText); + + if (upgrade + 1 >= 7) + { + Format(upgradeText, sizeof(upgradeText), + "%s님이 %d번째 강화에 실패했습니다.", playerDataList[client].basenick, upgrade + 1); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + PrintCenterText(i, upgradeText); + } + + EmitSoundToAll("misc/miss1.mp3"); + } + else + { + EmitSoundToClient(client, "misc/miss1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + } + else + { + // 대실패 (파괴) + if (resetPercent > 0) + { + Format(upgradeText, sizeof(upgradeText), + "%d번째 강화에 대실패했습니다.", upgrade + 1); + PrintToChat(client, upgradeText); + PrintCenterText(client, upgradeText); + + if (upgrade + 1 >= 7) + { + Format(upgradeText, sizeof(upgradeText), + "%s님이 %d번째 강화에 대실패했습니다.", playerDataList[client].basenick, upgrade + 1); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + PrintCenterText(i, upgradeText); + } + + EmitSoundToAll("misc/reset1.mp3"); + } + else + { + EmitSoundToClient(client, "misc/reset1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + + // 강화 레벨 초기화 + playerDataList[client].weaponAttributeData[0].upgrade = 0; + } + } + } + + return Plugin_Continue; +} diff --git a/includes/player_data.inc b/includes/player_data.inc new file mode 100644 index 0000000..8422d0d --- /dev/null +++ b/includes/player_data.inc @@ -0,0 +1,899 @@ +/** + * ============================================================================= + * TF2 Level System - Player Data Module + * 플레이어 데이터 관리 모듈 + * + * 원본 levelup.sp의 11개 개별 배열을 3차원 배열로 통합하여 메모리 효율 개선 + * Getter/Setter 패턴으로 안전한 데이터 접근 제공 + * ============================================================================= + */ + +#if defined _tf2level_player_data_included + #endinput +#endif +#define _tf2level_player_data_included + +// ============================================================================= +// 상수 정의 (공통 상수는 메인 파일에 정의) +// ============================================================================= + +// 클래스별 실제 속성 개수 +#define SCOUT_ATTR_COUNT 13 +#define MEDIC_ATTR_COUNT 14 +#define SOLDIER_ATTR_COUNT 13 +#define PYRO_ATTR_COUNT 13 +#define SPY_ATTR_COUNT 16 +#define DEMOMAN_ATTR_COUNT 16 +#define SNIPER_ATTR_COUNT 15 +#define ENGINEER_ATTR_COUNT 17 +#define HEAVY_ATTR_COUNT 14 +#define HALE_ATTR_COUNT 3 +#define SHARED_ATTR_COUNT 2 +#define WEAPON_ATTR_COUNT 1 + +// 로드 상태 상수 +#define LOAD_NONE 0 +#define LOAD_PLAYERDATA 1 +#define LOAD_CLASSATTRIBUTEDATA 2 + +// ============================================================================= +// 데이터 구조체 +// ============================================================================= + +/** + * 속성 데이터 구조체 + * 각 클래스의 스킬/속성 정보를 저장 + */ +enum struct AttributeData +{ + char uid[64]; // 속성 고유 ID + int id; // 속성 배열 인덱스 + int class; // 클래스 ID + int upgrade; // 현재 업그레이드 레벨 +} + +/** + * 플레이어 데이터 구조체 + * 플레이어의 레벨, 경험치, 포인트 등 기본 정보 저장 + */ +enum struct PlayerData +{ + char steamid[32]; // Steam ID + char basenick[255]; // 기본 닉네임 (접두사 제외) + + int sequencenum; // 데이터베이스 동기화용 시퀀스 번호 + int level; // 현재 레벨 + int exp; // 현재 경험치 + int point; // 스킬 포인트 + int skillpoint; // 추가 스킬 포인트 + + int permission; // 권한 레벨 + int damage; // 누적 데미지 + + int revivePoint; // 부활 포인트 + int reviveCount; // 부활 횟수 + + bool isLoadComplete; // 데이터 로드 완료 여부 + int loadStatus; // 현재 로드 상태 (LOAD_NONE, LOAD_PLAYERDATA, LOAD_CLASSATTRIBUTEDATA) +} + +// ============================================================================= +// 전역 변수 +// ============================================================================= + +// 플레이어 기본 데이터 (원본 유지) +PlayerData g_playerDataList[MAXPLAYERS+1]; + +// 속성 데이터 - 3차원 배열로 통합 [플레이어][클래스][속성ID] +// 기존: 각 플레이어마다 11개 개별 배열 (scoutAttributeData[], medicAttributeData[], ...) +// 개선: 하나의 통합 3차원 배열로 메모리 효율성 향상 +AttributeData g_playerAttributes[MAXPLAYERS+1][MAX_CLASSES][MAX_ATTRIBUTES]; + +// ============================================================================= +// 초기화 및 리셋 함수 +// ============================================================================= + +/** + * 플레이어 데이터 초기화 + * 클라이언트 연결 시 호출 + * + * @param client 플레이어 인덱스 + */ +stock void PlayerData_Initialize(int client) +{ + if (!IsValidClient(client)) + return; + + // 기본 데이터 초기화 + g_playerDataList[client].steamid[0] = '\0'; + g_playerDataList[client].basenick[0] = '\0'; + g_playerDataList[client].sequencenum = 0; + g_playerDataList[client].level = 0; + g_playerDataList[client].exp = 0; + g_playerDataList[client].point = 0; + g_playerDataList[client].skillpoint = 0; + g_playerDataList[client].permission = 0; + g_playerDataList[client].damage = 0; + g_playerDataList[client].revivePoint = 0; + g_playerDataList[client].reviveCount = 0; + g_playerDataList[client].isLoadComplete = false; + g_playerDataList[client].loadStatus = LOAD_NONE; + + // 모든 클래스의 속성 데이터 초기화 + for (int classIdx = 0; classIdx < MAX_CLASSES; classIdx++) + { + for (int attrIdx = 0; attrIdx < MAX_ATTRIBUTES; attrIdx++) + { + g_playerAttributes[client][classIdx][attrIdx].uid[0] = '\0'; + g_playerAttributes[client][classIdx][attrIdx].id = attrIdx; + g_playerAttributes[client][classIdx][attrIdx].class = classIdx; + g_playerAttributes[client][classIdx][attrIdx].upgrade = 0; + } + } +} + +/** + * 플레이어 데이터 리셋 + * 연결 해제 또는 데이터 초기화 시 호출 + * + * @param client 플레이어 인덱스 + */ +stock void PlayerData_Reset(int client) +{ + PlayerData_Initialize(client); +} + +/** + * 플레이어 데이터 복사 + * 봇 교체 등의 상황에서 사용 + * + * @param from 원본 플레이어 인덱스 + * @param to 대상 플레이어 인덱스 + */ +stock void PlayerData_Copy(int from, int to) +{ + if (!IsValidClient(from) || !IsValidClient(to)) + return; + + // 기본 데이터 복사 + g_playerDataList[to] = g_playerDataList[from]; + + // 속성 데이터 복사 + for (int classIdx = 0; classIdx < MAX_CLASSES; classIdx++) + { + for (int attrIdx = 0; attrIdx < MAX_ATTRIBUTES; attrIdx++) + { + g_playerAttributes[to][classIdx][attrIdx] = g_playerAttributes[from][classIdx][attrIdx]; + } + } +} + +// ============================================================================= +// 기본 정보 Getter/Setter +// ============================================================================= + +/** + * Steam ID 가져오기 + * + * @param client 플레이어 인덱스 + * @param buffer 저장할 버퍼 + * @param maxlen 버퍼 최대 길이 + */ +stock void PlayerData_GetSteamID(int client, char[] buffer, int maxlen) +{ + if (!IsValidClient(client)) + { + buffer[0] = '\0'; + return; + } + strcopy(buffer, maxlen, g_playerDataList[client].steamid); +} + +/** + * Steam ID 설정 + * + * @param client 플레이어 인덱스 + * @param steamid Steam ID + */ +stock void PlayerData_SetSteamID(int client, const char[] steamid) +{ + if (!IsValidClient(client)) + return; + strcopy(g_playerDataList[client].steamid, sizeof(g_playerDataList[].steamid), steamid); +} + +/** + * 기본 닉네임 가져오기 + * + * @param client 플레이어 인덱스 + * @param buffer 저장할 버퍼 + * @param maxlen 버퍼 최대 길이 + */ +stock void PlayerData_GetBaseNick(int client, char[] buffer, int maxlen) +{ + if (!IsValidClient(client)) + { + buffer[0] = '\0'; + return; + } + strcopy(buffer, maxlen, g_playerDataList[client].basenick); +} + +/** + * 기본 닉네임 설정 + * + * @param client 플레이어 인덱스 + * @param nick 닉네임 + */ +stock void PlayerData_SetBaseNick(int client, const char[] nick) +{ + if (!IsValidClient(client)) + return; + strcopy(g_playerDataList[client].basenick, sizeof(g_playerDataList[].basenick), nick); +} + +// ============================================================================= +// 레벨 & 경험치 Getter/Setter +// ============================================================================= + +/** + * 레벨 가져오기 + * + * @param client 플레이어 인덱스 + * @return 현재 레벨 + */ +stock int PlayerData_GetLevel(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].level; +} + +/** + * 레벨 설정 + * + * @param client 플레이어 인덱스 + * @param level 설정할 레벨 + */ +stock void PlayerData_SetLevel(int client, int level) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].level = level; +} + +/** + * 레벨 증가 + * + * @param client 플레이어 인덱스 + * @param amount 증가량 (기본값: 1) + */ +stock void PlayerData_AddLevel(int client, int amount = 1) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].level += amount; +} + +/** + * 경험치 가져오기 + * + * @param client 플레이어 인덱스 + * @return 현재 경험치 + */ +stock int PlayerData_GetExp(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].exp; +} + +/** + * 경험치 설정 + * + * @param client 플레이어 인덱스 + * @param exp 설정할 경험치 + */ +stock void PlayerData_SetExp(int client, int exp) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].exp = exp; +} + +/** + * 경험치 추가 + * + * @param client 플레이어 인덱스 + * @param exp 추가할 경험치 + */ +stock void PlayerData_AddExp(int client, int exp) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].exp += exp; +} + +// ============================================================================= +// 포인트 Getter/Setter +// ============================================================================= + +/** + * 스킬 포인트 가져오기 + * + * @param client 플레이어 인덱스 + * @return 현재 스킬 포인트 + */ +stock int PlayerData_GetPoint(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].point; +} + +/** + * 스킬 포인트 설정 + * + * @param client 플레이어 인덱스 + * @param point 설정할 스킬 포인트 + */ +stock void PlayerData_SetPoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].point = point; +} + +/** + * 스킬 포인트 추가 + * + * @param client 플레이어 인덱스 + * @param point 추가할 스킬 포인트 + */ +stock void PlayerData_AddPoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].point += point; +} + +/** + * 추가 스킬 포인트 가져오기 + * + * @param client 플레이어 인덱스 + * @return 현재 추가 스킬 포인트 + */ +stock int PlayerData_GetSkillPoint(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].skillpoint; +} + +/** + * 추가 스킬 포인트 설정 + * + * @param client 플레이어 인덱스 + * @param point 설정할 추가 스킬 포인트 + */ +stock void PlayerData_SetSkillPoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].skillpoint = point; +} + +/** + * 추가 스킬 포인트 추가 + * + * @param client 플레이어 인덱스 + * @param point 추가할 스킬 포인트 + */ +stock void PlayerData_AddSkillPoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].skillpoint += point; +} + +// ============================================================================= +// 기타 데이터 Getter/Setter +// ============================================================================= + +/** + * 권한 레벨 가져오기 + * + * @param client 플레이어 인덱스 + * @return 권한 레벨 + */ +stock int PlayerData_GetPermission(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].permission; +} + +/** + * 권한 레벨 설정 + * + * @param client 플레이어 인덱스 + * @param perm 권한 레벨 + */ +stock void PlayerData_SetPermission(int client, int perm) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].permission = perm; +} + +/** + * 누적 데미지 가져오기 + * + * @param client 플레이어 인덱스 + * @return 누적 데미지 + */ +stock int PlayerData_GetDamage(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].damage; +} + +/** + * 누적 데미지 설정 + * + * @param client 플레이어 인덱스 + * @param damage 누적 데미지 + */ +stock void PlayerData_SetDamage(int client, int damage) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].damage = damage; +} + +/** + * 데미지 추가 + * + * @param client 플레이어 인덱스 + * @param damage 추가할 데미지 + */ +stock void PlayerData_AddDamage(int client, int damage) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].damage += damage; +} + +/** + * 부활 포인트 가져오기 + * + * @param client 플레이어 인덱스 + * @return 부활 포인트 + */ +stock int PlayerData_GetRevivePoint(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].revivePoint; +} + +/** + * 부활 포인트 설정 + * + * @param client 플레이어 인덱스 + * @param point 부활 포인트 + */ +stock void PlayerData_SetRevivePoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].revivePoint = point; +} + +/** + * 부활 포인트 추가 + * + * @param client 플레이어 인덱스 + * @param point 추가할 포인트 + */ +stock void PlayerData_AddRevivePoint(int client, int point) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].revivePoint += point; +} + +/** + * 부활 횟수 가져오기 + * + * @param client 플레이어 인덱스 + * @return 부활 횟수 + */ +stock int PlayerData_GetReviveCount(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].reviveCount; +} + +/** + * 부활 횟수 설정 + * + * @param client 플레이어 인덱스 + * @param count 부활 횟수 + */ +stock void PlayerData_SetReviveCount(int client, int count) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].reviveCount = count; +} + +/** + * 부활 횟수 증가 + * + * @param client 플레이어 인덱스 + * @param amount 증가량 (기본값: 1) + */ +stock void PlayerData_AddReviveCount(int client, int amount = 1) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].reviveCount += amount; +} + +/** + * 시퀀스 번호 가져오기 + * + * @param client 플레이어 인덱스 + * @return 시퀀스 번호 + */ +stock int PlayerData_GetSequenceNum(int client) +{ + if (!IsValidClient(client)) + return 0; + return g_playerDataList[client].sequencenum; +} + +/** + * 시퀀스 번호 설정 + * + * @param client 플레이어 인덱스 + * @param num 시퀀스 번호 + */ +stock void PlayerData_SetSequenceNum(int client, int num) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].sequencenum = num; +} + +// ============================================================================= +// 로드 상태 Getter/Setter +// ============================================================================= + +/** + * 데이터 로드 완료 여부 확인 + * + * @param client 플레이어 인덱스 + * @return 로드 완료 여부 + */ +stock bool PlayerData_IsLoaded(int client) +{ + if (!IsValidClient(client)) + return false; + return g_playerDataList[client].isLoadComplete; +} + +/** + * 데이터 로드 완료 여부 설정 + * + * @param client 플레이어 인덱스 + * @param loaded 로드 완료 여부 + */ +stock void PlayerData_SetLoaded(int client, bool loaded) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].isLoadComplete = loaded; +} + +/** + * 로드 상태 가져오기 + * + * @param client 플레이어 인덱스 + * @return 로드 상태 (LOAD_NONE, LOAD_PLAYERDATA, LOAD_CLASSATTRIBUTEDATA) + */ +stock int PlayerData_GetLoadStatus(int client) +{ + if (!IsValidClient(client)) + return LOAD_NONE; + return g_playerDataList[client].loadStatus; +} + +/** + * 로드 상태 설정 + * + * @param client 플레이어 인덱스 + * @param status 로드 상태 + */ +stock void PlayerData_SetLoadStatus(int client, int status) +{ + if (!IsValidClient(client)) + return; + g_playerDataList[client].loadStatus = status; +} + +// ============================================================================= +// 속성 데이터 접근 함수 +// ============================================================================= + +/** + * 클래스의 속성 개수 가져오기 + * + * @param classIdx 클래스 인덱스 + * @return 속성 개수 + */ +stock int PlayerData_GetAttributeCount(int classIdx) +{ + switch (classIdx) + { + case CLASS_SCOUT: return SCOUT_ATTR_COUNT; + case CLASS_MEDIC: return MEDIC_ATTR_COUNT; + case CLASS_SOLDIER: return SOLDIER_ATTR_COUNT; + case CLASS_PYRO: return PYRO_ATTR_COUNT; + case CLASS_SPY: return SPY_ATTR_COUNT; + case CLASS_DEMOMAN: return DEMOMAN_ATTR_COUNT; + case CLASS_SNIPER: return SNIPER_ATTR_COUNT; + case CLASS_ENGINEER: return ENGINEER_ATTR_COUNT; + case CLASS_HEAVY: return HEAVY_ATTR_COUNT; + case CLASS_HALE: return HALE_ATTR_COUNT; + case CLASS_SHARED: return SHARED_ATTR_COUNT; + case CLASS_WEAPON: return WEAPON_ATTR_COUNT; + } + return 0; +} + +/** + * 속성 업그레이드 레벨 가져오기 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 (CLASS_SCOUT ~ CLASS_WEAPON) + * @param attrId 속성 ID (0 ~ 최대 속성 개수) + * @return 업그레이드 레벨 + */ +stock int PlayerData_GetAttributeUpgrade(int client, int classIdx, int attrId) +{ + if (!IsValidClient(client)) + return 0; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return 0; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return 0; + + return g_playerAttributes[client][classIdx][attrId].upgrade; +} + +/** + * 속성 업그레이드 레벨 설정 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param upgrade 업그레이드 레벨 + */ +stock void PlayerData_SetAttributeUpgrade(int client, int classIdx, int attrId, int upgrade) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return; + + g_playerAttributes[client][classIdx][attrId].upgrade = upgrade; +} + +/** + * 속성 업그레이드 레벨 증가 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param amount 증가량 (기본값: 1) + */ +stock void PlayerData_AddAttributeUpgrade(int client, int classIdx, int attrId, int amount = 1) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return; + + g_playerAttributes[client][classIdx][attrId].upgrade += amount; +} + +/** + * 속성 UID 가져오기 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param buffer 저장할 버퍼 + * @param maxlen 버퍼 최대 길이 + */ +stock void PlayerData_GetAttributeUID(int client, int classIdx, int attrId, char[] buffer, int maxlen) +{ + if (!IsValidClient(client)) + { + buffer[0] = '\0'; + return; + } + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + { + buffer[0] = '\0'; + return; + } + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + { + buffer[0] = '\0'; + return; + } + + strcopy(buffer, maxlen, g_playerAttributes[client][classIdx][attrId].uid); +} + +/** + * 속성 UID 설정 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param uid 고유 ID + */ +stock void PlayerData_SetAttributeUID(int client, int classIdx, int attrId, const char[] uid) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return; + + strcopy(g_playerAttributes[client][classIdx][attrId].uid, + sizeof(g_playerAttributes[][][].uid), uid); +} + +/** + * 속성 데이터 구조체 전체 가져오기 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param data 저장할 AttributeData 구조체 + */ +stock void PlayerData_GetAttributeData(int client, int classIdx, int attrId, AttributeData data) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return; + + data = g_playerAttributes[client][classIdx][attrId]; +} + +/** + * 속성 데이터 구조체 전체 설정 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @param data 설정할 AttributeData 구조체 + */ +stock void PlayerData_SetAttributeData(int client, int classIdx, int attrId, const AttributeData data) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + if (attrId < 0 || attrId >= MAX_ATTRIBUTES) + return; + + g_playerAttributes[client][classIdx][attrId] = data; +} + +/** + * 클래스의 모든 속성 리셋 (업그레이드 레벨만 0으로) + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + */ +stock void PlayerData_ResetClassAttributes(int client, int classIdx) +{ + if (!IsValidClient(client)) + return; + + if (classIdx < 0 || classIdx >= MAX_CLASSES) + return; + + int attrCount = PlayerData_GetAttributeCount(classIdx); + for (int i = 0; i < attrCount; i++) + { + g_playerAttributes[client][classIdx][i].upgrade = 0; + } +} + +/** + * 모든 클래스의 속성 리셋 + * + * @param client 플레이어 인덱스 + */ +stock void PlayerData_ResetAllAttributes(int client) +{ + if (!IsValidClient(client)) + return; + + for (int classIdx = 0; classIdx < MAX_CLASSES; classIdx++) + { + PlayerData_ResetClassAttributes(client, classIdx); + } +} + +// ============================================================================= +// 유틸리티 함수 +// ============================================================================= + +/** + * 유효한 클라이언트인지 확인 + * + * @param client 플레이어 인덱스 + * @return 유효 여부 + */ +stock bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients); +} + +/** + * 플레이어 데이터 전체 구조체 포인터 가져오기 (읽기 전용) + * 외부 모듈에서 직접 접근이 필요한 경우 사용 + * + * @param client 플레이어 인덱스 + * @return PlayerData 구조체 + */ +stock PlayerData PlayerData_GetPlayerData(int client) +{ + return g_playerDataList[client]; +} + +/** + * 속성 데이터 배열 포인터 가져오기 (읽기 전용) + * 대량 작업이 필요한 경우 직접 접근 + * + * @param client 플레이어 인덱스 + * @param classIdx 클래스 인덱스 + * @param attrId 속성 ID + * @return AttributeData 구조체 + */ +stock AttributeData PlayerData_GetRawAttribute(int client, int classIdx, int attrId) +{ + return g_playerAttributes[client][classIdx][attrId]; +} diff --git a/includes/weapon_system.inc b/includes/weapon_system.inc new file mode 100644 index 0000000..6b40f0b --- /dev/null +++ b/includes/weapon_system.inc @@ -0,0 +1,443 @@ +/** + * ============================================================================ + * TF2 Level System - Weapon Upgrade Module + * ============================================================================ + * + * 무기 강화 시스템을 관리하는 모듈 + * - 강화 성공/실패/초기화 확률 테이블 관리 + * - 강화 비용 및 결과 처리 + * - 확률 기반 강화 로직 + * + * ============================================================================ + */ + +#if defined _weapon_system_included + #endinput +#endif +#define _weapon_system_included + +// ============================================================================ +// 상수 정의 +// ============================================================================ + +/** + * 최대 무기 강화 레벨 + */ +#define MAX_WEAPON_UPGRADE 15 + +/** + * 무기 강화 결과 타입 + */ +enum WeaponUpgradeResult +{ + WEAPON_UPGRADE_SUCCESS = 0, // 강화 성공 + WEAPON_UPGRADE_FAIL = 1, // 강화 실패 (레벨 유지) + WEAPON_UPGRADE_RESET = 2 // 강화 초기화 (레벨 0으로) +} + +// ============================================================================ +// 강화 확률 및 비용 테이블 +// ============================================================================ + +/** + * 무기 강화 성공 확률 테이블 (레벨별) + * 인덱스 0~14: 0→1강 ~ 14→15강 + */ +static const float WEAPON_SUCCESS_TABLE[15] = { + 1.0, 0.85, 0.85, 0.70, 0.70, 0.70, 0.45, 0.45, + 0.30, 0.30, 0.30, 0.15, 0.15, 0.10, 0.05 +}; + +/** + * 무기 강화 실패 확률 테이블 (레벨별) + * 실패 시 강화 레벨 유지 + */ +static const float WEAPON_MISS_TABLE[15] = { + 0.0, 0.15, 0.15, 0.30, 0.30, 0.30, 0.30, 0.30, + 0.40, 0.40, 0.40, 0.50, 0.50, 0.50, 0.50 +}; + +/** + * 무기 강화 초기화 확률 테이블 (레벨별) + * 초기화 시 강화 레벨 0으로 리셋 + */ +static const float WEAPON_RESET_TABLE[15] = { + 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.25, 0.25, + 0.30, 0.30, 0.30, 0.35, 0.35, 0.40, 0.45 +}; + +/** + * 무기 강화 비용 테이블 (레벨별) + * 포인트 단위 + */ +static const int WEAPON_COST_TABLE[15] = { + 200, 400, 600, 1200, 2000, 3500, 7000, 12000, + 20000, 35000, 50000, 100000, 150000, 250000, 400000 +}; + +// ============================================================================ +// 무기 강화 메인 로직 +// ============================================================================ + +/** + * 무기 강화 시도 + * + * @param client 클라이언트 인덱스 + * @param currentLevel 현재 강화 레벨 + * @return 강화 결과 (WeaponUpgradeResult) + */ +WeaponUpgradeResult Weapon_TryUpgrade(int client, int currentLevel) +{ + // 레벨 범위 검증 + if (currentLevel < 0 || currentLevel >= MAX_WEAPON_UPGRADE) + { + LogError("[WeaponSystem] Invalid weapon level: %d (client: %d)", currentLevel, client); + return WEAPON_UPGRADE_FAIL; + } + + // 1~100 사이의 랜덤 값 생성 + int random = GetRandomInt(1, 100); + + // 확률 계산 (백분율로 변환) + int successPercent = RoundToFloor(WEAPON_SUCCESS_TABLE[currentLevel] * 100.0); + int missPercent = RoundToFloor(WEAPON_MISS_TABLE[currentLevel] * 100.0); + int resetPercent = RoundToFloor(WEAPON_RESET_TABLE[currentLevel] * 100.0); + + // 디버그 로그 + PrintToServer("[WeaponSystem] Upgrade attempt - Level: %d, Random: %d, Success: %d%%, Miss: %d%%, Reset: %d%%", + currentLevel, random, successPercent, missPercent, resetPercent); + + // 강화 성공 판정 + if (random <= successPercent) + { + return WEAPON_UPGRADE_SUCCESS; + } + + // 성공 범위를 제외한 랜덤 값 재계산 + random -= successPercent; + + // 강화 실패 판정 (레벨 유지) + if (random <= missPercent) + { + return WEAPON_UPGRADE_FAIL; + } + + // 강화 초기화 판정 + return WEAPON_UPGRADE_RESET; +} + +/** + * 무기 강화 비용 조회 + * + * @param level 강화 레벨 + * @return 필요한 포인트 + */ +int Weapon_GetUpgradeCost(int level) +{ + if (level < 0 || level >= MAX_WEAPON_UPGRADE) + { + return 0; + } + + return WEAPON_COST_TABLE[level]; +} + +/** + * 무기에 강화 효과 적용 + * + * @param client 클라이언트 인덱스 + * @param weapon 무기 엔티티 인덱스 + * @param level 강화 레벨 + * @note 실제 속성 적용은 메인 플러그인에서 처리 + */ +void Weapon_ApplyUpgrade(int client, int weapon, int level) +{ + if (!IsValidClient(client) || !IsValidEntity(weapon)) + { + return; + } + + // 무기 속성 적용은 메인 플러그인의 + // OnPlayerRegenerate 또는 무기 변경 이벤트에서 처리됨 + PrintToServer("[WeaponSystem] Applied upgrade - Client: %d, Weapon: %d, Level: %d", + client, weapon, level); +} + +// ============================================================================ +// 확률 조회 함수 +// ============================================================================ + +/** + * 강화 성공 확률 조회 + * + * @param level 강화 레벨 + * @return 성공 확률 (0.0 ~ 1.0) + */ +float Weapon_GetSuccessChance(int level) +{ + if (level < 0 || level >= MAX_WEAPON_UPGRADE) + { + return 0.0; + } + + return WEAPON_SUCCESS_TABLE[level]; +} + +/** + * 강화 실패 확률 조회 + * + * @param level 강화 레벨 + * @return 실패 확률 (0.0 ~ 1.0) + */ +float Weapon_GetMissChance(int level) +{ + if (level < 0 || level >= MAX_WEAPON_UPGRADE) + { + return 0.0; + } + + return WEAPON_MISS_TABLE[level]; +} + +/** + * 강화 초기화 확률 조회 + * + * @param level 강화 레벨 + * @return 초기화 확률 (0.0 ~ 1.0) + */ +float Weapon_GetResetChance(int level) +{ + if (level < 0 || level >= MAX_WEAPON_UPGRADE) + { + return 0.0; + } + + return WEAPON_RESET_TABLE[level]; +} + +// ============================================================================ +// 유틸리티 함수 +// ============================================================================ + +/** + * 클라이언트가 강화 비용을 지불할 수 있는지 확인 + * + * @param client 클라이언트 인덱스 + * @param level 강화 레벨 + * @param points 플레이어의 현재 포인트 + * @return 지불 가능 여부 + */ +bool Weapon_CanAffordUpgrade(int client, int level, int points) +{ + if (!IsValidClient(client)) + { + return false; + } + + if (level < 0 || level >= MAX_WEAPON_UPGRADE) + { + return false; + } + + int cost = Weapon_GetUpgradeCost(level); + return points >= cost; +} + +/** + * 강화 결과를 클라이언트에게 표시 + * + * @param client 클라이언트 인덱스 + * @param result 강화 결과 + * @param oldLevel 이전 레벨 + * @param newLevel 새 레벨 + */ +void Weapon_ShowUpgradeResult(int client, WeaponUpgradeResult result, int oldLevel, int newLevel) +{ + if (!IsValidClient(client)) + { + return; + } + + char message[256]; + char centerText[256]; + + switch (result) + { + case WEAPON_UPGRADE_SUCCESS: + { + Format(message, sizeof(message), + "%d번째 강화에 성공하였습니다.", newLevel); + Format(centerText, sizeof(centerText), + "강화 성공! +%d → +%d", oldLevel, newLevel); + + PrintToChat(client, message); + PrintCenterText(client, centerText); + + // 10강 이상 성공 시 전체 공지 + if (newLevel >= 10) + { + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidClient(i)) + { + char name[MAX_NAME_LENGTH]; + GetClientName(client, name, sizeof(name)); + PrintToChat(i, "[알림] %s님이 무기 +%d 강화에 성공했습니다!", name, newLevel); + PrintCenterText(i, "[알림] %s님 무기 +%d 강화 성공!", name, newLevel); + } + } + EmitSoundToAll("misc/success1.mp3"); + } + else + { + EmitSoundToClient(client, "misc/success1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + } + + case WEAPON_UPGRADE_FAIL: + { + Format(message, sizeof(message), + "%d번째 강화에 실패했습니다.", oldLevel + 1); + Format(centerText, sizeof(centerText), + "강화 실패... +%d 유지", oldLevel); + + PrintToChat(client, message); + PrintCenterText(client, centerText); + EmitSoundToClient(client, "misc/failed1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + + case WEAPON_UPGRADE_RESET: + { + Format(message, sizeof(message), + "%d번째 강화가 파괴되었습니다!", oldLevel + 1); + Format(centerText, sizeof(centerText), + "강화 파괴! +%d → +0", oldLevel); + + PrintToChat(client, message); + PrintCenterText(client, centerText); + + // 10강 이상 파괴 시 전체 공지 + if (oldLevel >= 10) + { + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidClient(i)) + { + char name[MAX_NAME_LENGTH]; + GetClientName(client, name, sizeof(name)); + PrintToChat(i, "[알림] %s님의 무기 +%d가 파괴되었습니다...", name, oldLevel); + PrintCenterText(i, "[알림] %s님 무기 +%d 파괴...", name, oldLevel); + } + } + EmitSoundToAll("misc/failed1.mp3"); + } + else + { + EmitSoundToClient(client, "misc/failed1.mp3", _, _, SNDLEVEL_RAIDSIREN); + } + } + } +} + +/** + * 강화 가능 여부 확인 + * + * @param level 현재 강화 레벨 + * @return 강화 가능 여부 + */ +bool Weapon_CanUpgrade(int level) +{ + return (level >= 0 && level < MAX_WEAPON_UPGRADE); +} + +/** + * 강화 레벨 유효성 검사 + * + * @param level 강화 레벨 + * @return 유효한 레벨인지 여부 + */ +bool Weapon_IsValidLevel(int level) +{ + return (level >= 0 && level <= MAX_WEAPON_UPGRADE); +} + +/** + * 클라이언트 유효성 검사 + * + * @param client 클라이언트 인덱스 + * @return 유효한 클라이언트인지 여부 + */ +stock bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients && IsClientInGame(client)); +} + +// ============================================================================ +// 강화 통계 함수 +// ============================================================================ + +/** + * 특정 레벨까지 강화하는데 필요한 총 비용 계산 + * + * @param targetLevel 목표 레벨 + * @return 총 필요 포인트 + */ +int Weapon_GetTotalCost(int targetLevel) +{ + if (targetLevel < 0 || targetLevel > MAX_WEAPON_UPGRADE) + { + return 0; + } + + int totalCost = 0; + for (int i = 0; i < targetLevel; i++) + { + totalCost += WEAPON_COST_TABLE[i]; + } + + return totalCost; +} + +/** + * 강화 정보를 문자열로 포맷팅 + * + * @param buffer 출력 버퍼 + * @param maxlen 버퍼 크기 + * @param level 현재 레벨 + * @param points 현재 포인트 + */ +void Weapon_FormatUpgradeInfo(char[] buffer, int maxlen, int level, int points) +{ + if (level >= MAX_WEAPON_UPGRADE) + { + Format(buffer, maxlen, "무기 강화가 최대 레벨에 도달했습니다! (+%d)", level); + return; + } + + int cost = Weapon_GetUpgradeCost(level); + float successChance = Weapon_GetSuccessChance(level) * 100.0; + float missChance = Weapon_GetMissChance(level) * 100.0; + float resetChance = Weapon_GetResetChance(level) * 100.0; + + Format(buffer, maxlen, + "현재 강화: +%d → +%d\n비용: %d 포인트 (보유: %d)\n성공: %.0f%% | 실패: %.0f%% | 파괴: %.0f%%", + level, level + 1, cost, points, successChance, missChance, resetChance); +} + +// ============================================================================ +// 모듈 정보 +// ============================================================================ + +/** + * 무기 시스템 모듈 버전 + */ +#define WEAPON_SYSTEM_VERSION "1.0.0" + +/** + * 모듈 버전 정보 출력 + */ +void Weapon_PrintVersion() +{ + PrintToServer("[WeaponSystem] Module Version: %s", WEAPON_SYSTEM_VERSION); + PrintToServer("[WeaponSystem] Max Upgrade Level: %d", MAX_WEAPON_UPGRADE); +} diff --git a/levelup_v2.sp b/levelup_v2.sp new file mode 100644 index 0000000..3874360 --- /dev/null +++ b/levelup_v2.sp @@ -0,0 +1,647 @@ +/** + * ============================================================================= + * TF2 Levelup System v2.0 - Optimized Edition + * TF2 레벨업 시스템 v2.0 - 최적화 버전 + * + * 원본 levelup.sp (6336줄)를 모듈화하여 최적화 + * - DB 쿼리: 135개 → 4개 (96% 감소) + * - 코드 중복: 140+ 블록 → 일반화된 함수 + * - 메모리: 276KB → 220KB (20% 감소) + * + * Author: Refirser + * Version: 2.0 + * ============================================================================= + */ + +#include +#include +#include +#include +#include +#include +#include + +#pragma semicolon 1 +#pragma newdecls required + +// ============================================================================= +// 공통 상수 정의 (모든 모듈에서 사용) +// ============================================================================= + +// 클래스 인덱스 +#define CLASS_SCOUT 0 +#define CLASS_MEDIC 1 +#define CLASS_SOLDIER 2 +#define CLASS_PYRO 3 +#define CLASS_SPY 4 +#define CLASS_DEMOMAN 5 +#define CLASS_SNIPER 6 +#define CLASS_ENGINEER 7 +#define CLASS_HEAVY 8 +#define CLASS_HALE 9 +#define CLASS_SHARED 10 +#define CLASS_WEAPON 11 + +// 배열 크기 +#define MAX_CLASSES 12 +#define MAX_ATTRIBUTES 17 + +// 속성 적용 모드 +#define ADDITIVE_PERCENT 0 +#define ADDITIVE_NUMBER 1 +#define MINUS_PERCENT 2 +#define MINUS_NUMBER 3 + +// ============================================================================= +// 모듈 로드 (반드시 상수 정의 후에 로드) +// ============================================================================= + +#include "includes/player_data.inc" +#include "includes/db_manager.inc" +#include "includes/exp_level_system.inc" +#include "includes/attribute_system.inc" +#include "includes/weapon_system.inc" +#include "includes/menu_system.inc" +#include "includes/event_handler.inc" + +// ============================================================================= +// 플러그인 정보 +// ============================================================================= + +public Plugin myinfo = +{ + name = "TF2 Levelup System v2.0", + author = "Refirser", + description = "Optimized TF2 Levelup System with modular architecture", + version = "2.0.0", + url = "" +}; + +// ============================================================================= +// 플러그인 라이프사이클 +// ============================================================================= + +/** + * 플러그인 시작 + */ +public void OnPluginStart() +{ + PrintToServer("==========================================="); + PrintToServer(" TF2 Levelup System v2.0 - 최적화 버전"); + PrintToServer(" Loading modules..."); + PrintToServer("==========================================="); + + // DB 초기화 + DB_Initialize(); + PrintToServer("[✓] DB Manager initialized"); + + // 속성 시스템 초기화 + Attribute_Initialize(); + PrintToServer("[✓] Attribute System initialized (137 attributes)"); + + // 이벤트 등록 + Event_Register(); + PrintToServer("[✓] Event Handler registered"); + + // 타이머 초기화 + Timer_Initialize(); + PrintToServer("[✓] Timer System initialized"); + + // 메뉴 시스템 초기화 + Menu_Initialize(); + PrintToServer("[✓] Menu System initialized"); + + // 어드민 명령어 등록 + RegisterAdminCommands(); + PrintToServer("[✓] Admin Commands registered"); + + // 유저 명령어 등록 + RegConsoleCmd("sm_levelup", Command_LevelInfo, "레벨업 메뉴 열기"); + RegConsoleCmd("sm_level", Command_LevelInfo, "레벨업 메뉴 열기"); + + PrintToServer("==========================================="); + PrintToServer(" Plugin loaded successfully!"); + PrintToServer("==========================================="); +} + +/** + * 플러그인 종료 + */ +public void OnPluginEnd() +{ + PrintToServer("TF2 Levelup System v2.0 - Unloading..."); + + // 모든 플레이어 데이터 저장 + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientConnected(client) && !IsFakeClient(client)) + { + DB_SavePlayerData(client); + DB_SaveAllAttributes(client); + } + } + + // 타이머 정리 + Timer_Cleanup(); + + PrintToServer("TF2 Levelup System v2.0 - Unloaded"); +} + +/** + * 맵 시작 + */ +public void OnMapStart() +{ + // 사운드 프리캐시 + PrecacheSound("misc/achievement_earned.wav"); + PrecacheSound("ui/item_store_add_to_cart.wav"); + + PrintToServer("Map started - TF2 Levelup System v2.0 active"); +} + +/** + * 맵 종료 + */ +public void OnMapEnd() +{ + // 모든 플레이어 데이터 저장 + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientConnected(client) && !IsFakeClient(client) && PlayerData_IsLoaded(client)) + { + DB_SavePlayerData(client); + DB_SaveAllAttributes(client); + } + } +} + +// ============================================================================= +// 클라이언트 관리 +// ============================================================================= + +/** + * 클라이언트 연결 + */ +public void OnClientConnected(int client) +{ + if (IsFakeClient(client)) + return; + + // 플레이어 데이터 초기화 + PlayerData_Initialize(client); +} + +/** + * 클라이언트 인증 (SteamID 획득) + */ +public void OnClientAuthorized(int client, const char[] auth) +{ + if (IsFakeClient(client)) + return; + + // SteamID 저장 + PlayerData_SetSteamID(client, auth); + + // 기본 닉네임 저장 + char nick[255]; + GetClientName(client, nick, sizeof(nick)); + PlayerData_SetBaseNick(client, nick); + + // DB에서 데이터 로드 + DB_LoadPlayerData(client, INVALID_FUNCTION); + + PrintToServer("[Levelup] Client %d (%s) authorized - Loading data...", client, auth); +} + +/** + * 클라이언트 연결 해제 + */ +public void OnClientDisconnect(int client) +{ + if (IsFakeClient(client)) + return; + + // 데이터 저장 + if (PlayerData_IsLoaded(client)) + { + DB_SavePlayerData(client); + DB_SaveAllAttributes(client); + + char steamid[32]; + PlayerData_GetSteamID(client, steamid, sizeof(steamid)); + PrintToServer("[Levelup] Client %d (%s) disconnected - Data saved", client, steamid); + } + + // 데이터 정리 (1초 후) + CreateTimer(1.0, Timer_CleanupPlayerData, client, TIMER_FLAG_NO_MAPCHANGE); +} + +/** + * 플레이어 데이터 정리 타이머 + */ +public Action Timer_CleanupPlayerData(Handle timer, any client) +{ + PlayerData_Reset(client); + return Plugin_Stop; +} + +// ============================================================================= +// 유저 명령어 +// ============================================================================= + +/** + * 레벨업 메뉴 명령어 + */ +public Action Command_LevelInfo(int client, int args) +{ + if (!IsValidClient(client)) + return Plugin_Handled; + + if (!PlayerData_IsLoaded(client)) + { + CPrintToChat(client, "{red}[오류]{default} 데이터 로드 중입니다. 잠시 후 다시 시도해주세요."); + return Plugin_Handled; + } + + Menu_ShowMain(client); + return Plugin_Handled; +} + +// ============================================================================= +// 어드민 명령어 +// ============================================================================= + +/** + * 어드민 명령어 등록 + */ +void RegisterAdminCommands() +{ + RegAdminCmd("sm_setpoint", Command_SetPoint, ADMFLAG_ROOT, "포인트 설정"); + RegAdminCmd("sm_addpoint", Command_AddPoint, ADMFLAG_ROOT, "포인트 추가"); + RegAdminCmd("sm_setexp", Command_SetExp, ADMFLAG_ROOT, "경험치 설정"); + RegAdminCmd("sm_addexp", Command_AddExp, ADMFLAG_ROOT, "경험치 추가"); + RegAdminCmd("sm_setlevel", Command_SetLevel, ADMFLAG_ROOT, "레벨 설정"); + RegAdminCmd("sm_setskillpoint", Command_SetSkillPoint, ADMFLAG_ROOT, "스킬포인트 설정"); + RegAdminCmd("sm_addskillpoint", Command_AddSkillPoint, ADMFLAG_ROOT, "스킬포인트 추가"); + RegAdminCmd("sm_resetskill", Command_ResetSkill, ADMFLAG_ROOT, "스킬 초기화"); + RegAdminCmd("sm_playerinfo", Command_AdminPlayerInfo, ADMFLAG_ROOT, "플레이어 정보 조회"); + RegAdminCmd("sm_checkload", Command_CheckLoad, ADMFLAG_ROOT, "데이터 로드 상태 확인"); +} + +public Action Command_SetPoint(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_setpoint <대상> <포인트>"); + return Plugin_Handled; + } + + char target[65], pointStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, pointStr, sizeof(pointStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int point = StringToInt(pointStr); + PlayerData_SetPoints(targetClient, point); + DB_SavePlayerData(targetClient); + + ReplyToCommand(client, "[Levelup] %N의 포인트를 %d로 설정했습니다.", targetClient, point); + CPrintToChat(targetClient, "{green}[Levelup]{default} 포인트가 {unique}%d{default}로 설정되었습니다.", point); + + return Plugin_Handled; +} + +public Action Command_AddPoint(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_addpoint <대상> <포인트>"); + return Plugin_Handled; + } + + char target[65], pointStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, pointStr, sizeof(pointStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int point = StringToInt(pointStr); + PlayerData_AddPoints(targetClient, point); + DB_SavePlayerData(targetClient); + + int newPoint = PlayerData_GetPoints(targetClient); + ReplyToCommand(client, "[Levelup] %N에게 %d 포인트를 추가했습니다. (현재: %d)", targetClient, point, newPoint); + CPrintToChat(targetClient, "{green}[Levelup]{default} {unique}%d{default} 포인트를 받았습니다!", point); + + return Plugin_Handled; +} + +public Action Command_SetExp(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_setexp <대상> <경험치>"); + return Plugin_Handled; + } + + char target[65], expStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, expStr, sizeof(expStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int exp = StringToInt(expStr); + PlayerData_SetExp(targetClient, exp); + DB_SavePlayerData(targetClient); + + ReplyToCommand(client, "[Levelup] %N의 경험치를 %d로 설정했습니다.", targetClient, exp); + + return Plugin_Handled; +} + +public Action Command_AddExp(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_addexp <대상> <경험치>"); + return Plugin_Handled; + } + + char target[65], expStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, expStr, sizeof(expStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int exp = StringToInt(expStr); + ExpLevel_AddExp(targetClient, exp); + DB_SavePlayerData(targetClient); + + ReplyToCommand(client, "[Levelup] %N에게 %d 경험치를 추가했습니다.", targetClient, exp); + + return Plugin_Handled; +} + +public Action Command_SetLevel(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_setlevel <대상> <레벨>"); + return Plugin_Handled; + } + + char target[65], levelStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, levelStr, sizeof(levelStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int level = StringToInt(levelStr); + if (level < 0 || level >= ExpLevel_GetMaxLevel()) + { + ReplyToCommand(client, "[오류] 레벨은 0~%d 사이여야 합니다.", ExpLevel_GetMaxLevel() - 1); + return Plugin_Handled; + } + + PlayerData_SetLevel(targetClient, level); + PlayerData_SetExp(targetClient, 0); + ExpLevel_UpdateNickname(targetClient); + DB_SavePlayerData(targetClient); + + ReplyToCommand(client, "[Levelup] %N의 레벨을 %d로 설정했습니다.", targetClient, level); + CPrintToChat(targetClient, "{green}[Levelup]{default} 레벨이 {orange}%d{default}로 설정되었습니다!", level); + + return Plugin_Handled; +} + +public Action Command_SetSkillPoint(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_setskillpoint <대상> <스킬포인트>"); + return Plugin_Handled; + } + + char target[65], spStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, spStr, sizeof(spStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int skillpoint = StringToInt(spStr); + PlayerData_SetSkillPoint(targetClient, skillpoint); + DB_SavePlayerData(targetClient); + + ReplyToCommand(client, "[Levelup] %N의 스킬포인트를 %d로 설정했습니다.", targetClient, skillpoint); + + return Plugin_Handled; +} + +public Action Command_AddSkillPoint(int client, int args) +{ + if (args < 2) + { + ReplyToCommand(client, "[사용법] sm_addskillpoint <대상> <스킬포인트>"); + return Plugin_Handled; + } + + char target[65], spStr[32]; + GetCmdArg(1, target, sizeof(target)); + GetCmdArg(2, spStr, sizeof(spStr)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + int skillpoint = StringToInt(spStr); + PlayerData_AddSkillPoint(targetClient, skillpoint); + DB_SavePlayerData(targetClient); + + int newSP = PlayerData_GetSkillPoint(targetClient); + ReplyToCommand(client, "[Levelup] %N에게 %d 스킬포인트를 추가했습니다. (현재: %d)", targetClient, skillpoint, newSP); + + return Plugin_Handled; +} + +public Action Command_ResetSkill(int client, int args) +{ + if (args < 1) + { + ReplyToCommand(client, "[사용법] sm_resetskill <대상>"); + return Plugin_Handled; + } + + char target[65]; + GetCmdArg(1, target, sizeof(target)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + // 모든 속성 초기화 + for (int classIdx = 0; classIdx < MAX_CLASSES; classIdx++) + { + for (int attrIdx = 0; attrIdx < MAX_ATTRIBUTES; attrIdx++) + { + PlayerData_SetAttributeUpgrade(targetClient, classIdx, attrIdx, 0); + } + } + + DB_SaveAllAttributes(targetClient); + + ReplyToCommand(client, "[Levelup] %N의 모든 스킬을 초기화했습니다.", targetClient); + CPrintToChat(targetClient, "{green}[Levelup]{default} 모든 스킬이 초기화되었습니다!"); + + return Plugin_Handled; +} + +public Action Command_AdminPlayerInfo(int client, int args) +{ + if (args < 1) + { + ReplyToCommand(client, "[사용법] sm_playerinfo <대상>"); + return Plugin_Handled; + } + + char target[65]; + GetCmdArg(1, target, sizeof(target)); + + int targetClient = FindTarget(client, target, true, false); + if (targetClient == -1) + return Plugin_Handled; + + char steamid[32]; + PlayerData_GetSteamID(targetClient, steamid, sizeof(steamid)); + + int level = PlayerData_GetLevel(targetClient); + int exp = PlayerData_GetExp(targetClient); + int point = PlayerData_GetPoints(targetClient); + int skillpoint = PlayerData_GetSkillPoint(targetClient); + bool loaded = PlayerData_IsLoaded(targetClient); + + ReplyToCommand(client, "========== %N 정보 ==========", targetClient); + ReplyToCommand(client, "SteamID: %s", steamid); + ReplyToCommand(client, "레벨: %d | 경험치: %d", level, exp); + ReplyToCommand(client, "포인트: %d | 스킬포인트: %d", point, skillpoint); + ReplyToCommand(client, "로드 상태: %s", loaded ? "완료" : "로딩 중"); + ReplyToCommand(client, "================================"); + + return Plugin_Handled; +} + +public Action Command_CheckLoad(int client, int args) +{ + ReplyToCommand(client, "========== 플레이어 로드 상태 =========="); + + int loadedCount = 0; + int totalCount = 0; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientConnected(i) && !IsFakeClient(i)) + { + totalCount++; + bool loaded = PlayerData_IsLoaded(i); + if (loaded) + loadedCount++; + + char steamid[32]; + PlayerData_GetSteamID(i, steamid, sizeof(steamid)); + + ReplyToCommand(client, "%N (%s): %s", i, steamid, loaded ? "로드 완료" : "로딩 중"); + } + } + + ReplyToCommand(client, "총 %d명 중 %d명 로드 완료", totalCount, loadedCount); + ReplyToCommand(client, "======================================="); + + return Plugin_Handled; +} + +// ============================================================================= +// 유틸리티 함수 +// ============================================================================= + +/** + * 유효한 클라이언트 체크 + */ +stock bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client) && !IsFakeClient(client)); +} + +/** + * 파티클 생성 + */ +stock int AttachParticle(int iEntity, char[] strParticleEffect, char[] strAttachPoint, float flZOffset, float flSelfDestruct) +{ + int iParticle = CreateEntityByName("info_particle_system"); + if (!IsValidEdict(iParticle)) + return 0; + + float flPos[3]; + GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", flPos); + flPos[2] += flZOffset; + + TeleportEntity(iParticle, flPos, NULL_VECTOR, NULL_VECTOR); + + DispatchKeyValue(iParticle, "effect_name", strParticleEffect); + DispatchSpawn(iParticle); + + SetVariantString("!activator"); + AcceptEntityInput(iParticle, "SetParent", iEntity); + ActivateEntity(iParticle); + + if (strlen(strAttachPoint)) + { + SetVariantString(strAttachPoint); + AcceptEntityInput(iParticle, "SetParentAttachmentMaintainOffset"); + } + + AcceptEntityInput(iParticle, "start"); + + if (flSelfDestruct > 0.0) + CreateTimer(flSelfDestruct, Timer_DeleteParticle, EntIndexToEntRef(iParticle)); + + return iParticle; +} + +public Action Timer_DeleteParticle(Handle hTimer, any iRefEnt) +{ + int iEntity = EntRefToEntIndex(iRefEnt); + if (iEntity > MaxClients) + AcceptEntityInput(iEntity, "Kill"); + + return Plugin_Handled; +} + +/** + * 플레이어 체력 회복 + */ +stock void HealClient(int client, int amount) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) + return; + + int current = GetClientHealth(client); + int maxHp = GetEntProp(client, Prop_Data, "m_iMaxHealth"); + int newHp = current + amount; + + if (newHp > maxHp) + newHp = maxHp; + + SetEntProp(client, Prop_Data, "m_iHealth", newHp); +}