16 Commits

Author SHA1 Message Date
89f2e2e149 Merge pull request '데미지 팝 경로 정수화 및 카드 툴팁 호버 UI 정리' (#116) from codex/grid-hover-tooltip-fix into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/116
2026-07-13 02:00:35 +09:00
1029459f23 Fix damage pop UI paths and card hover tooltip support 2026-07-13 01:59:11 +09:00
11cc7abb27 Merge pull request '덱 그리드 툴팁 호버 처리 수정' (#115) from codex/grid-hover-tooltip-fix into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/115
2026-07-11 23:19:30 +09:00
84381947be Fix deck grid tooltip hover handling 2026-07-11 23:15:58 +09:00
2928e2b45f Merge pull request '타격 시퀀스와 카메라 쉐이크 타이밍 정리' (#114) from codex/attack-sequence-feel-fix into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/114
2026-07-10 00:38:07 +09:00
9bc71c0e32 Improve attack hit timing and camera shake 2026-07-10 00:35:50 +09:00
701769fcd7 Merge pull request '데미지 팝업 순차 표시 및 전투 UI 반영 정리' (#112) from codex/dmg-pop-sequence-fix into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/112
2026-07-09 22:48:39 +09:00
a7a6b2123a Fix damage popup sequencing 2026-07-08 02:35:01 +09:00
fc37d81350 Merge pull request '전사·도적 전직 카드 풀 및 성장 밸런스 조정' (#111) from codex/warrior-card-pool-rebalance into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/111
2026-07-05 20:02:04 +09:00
d5931b1822 balance: 도적 계열 성장 곡선 조정 2026-07-05 04:03:13 +09:00
fe92b8bbb8 balance: 전사 카드를 전직 계열별로 재배치 2026-07-05 03:47:28 +09:00
f89cd1ad63 Merge pull request '메이플 전사 전직 스킬 카드 및 연계 기믹 구현' (#110) from codex/maple-warrior-skill-cards into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/110
2026-07-04 02:29:12 +09:00
758ebd19f2 balance: 전사 계열 5섹션 성장 곡선 조정 2026-07-04 02:22:17 +09:00
90494232bc feat: 메이플 전사 전직 카드와 연계 기믹 추가 2026-07-04 02:15:01 +09:00
ecadf3606e Merge pull request '워리어 카드 풀 및 공용 전투 효과 구현' (#109) from codex/warrior-card-effects-fixes into main
Reviewed-on: https://gitea.gahusb.synology.me/gahusb/maplecontest/pulls/109
2026-07-04 01:40:00 +09:00
4b559ca7fa feat: 워리어 카드와 공용 전투 효과 구현 2026-07-03 23:07:41 +09:00
37 changed files with 26003 additions and 5289 deletions

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,9 @@
"classToFrame": { "classToFrame": {
"warrior": "warrior", "warrior": "warrior",
"fighter": "warrior", "fighter": "warrior",
"crusader": "warrior",
"page": "warrior", "page": "warrior",
"knight": "warrior",
"spearman": "warrior", "spearman": "warrior",
"magician": "magician", "magician": "magician",
"firepoison": "magician", "firepoison": "magician",

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -6,15 +6,25 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
## Damage ## Damage
- `damage`: base attack damage - `damage`: base attack damage
- `damageFromCurrentBlock`: add current block times this value to attack damage
- `damageNameMatch`: substring to match against owned card names
- `damagePerOwnedNameMatch`: bonus damage per owned card whose name matches `damageNameMatch`
- `damagePerOtherHandCard`: bonus damage per other card in hand - `damagePerOtherHandCard`: bonus damage per other card in hand
- `damagePerAttackPlayedThisTurn`: bonus damage per attack played this turn - `damagePerAttackPlayedThisTurn`: bonus damage per attack played this turn
- `damagePerDiscardedThisTurn`: bonus damage per card discarded this turn - `damagePerDiscardedThisTurn`: bonus damage per card discarded this turn
- `damagePerSkillInHand`: bonus damage per skill card in hand - `damagePerSkillInHand`: bonus damage per skill card in hand
- `damagePerCardDrawnThisCombat`: bonus damage per card drawn this combat - `damagePerCardDrawnThisCombat`: bonus damage per card drawn this combat
- `damagePerCombo`: bonus base damage per current Combo
- `damagePerHolyCharge`: bonus base damage per current Holy Charge
- `attackDamagePerCombo`: Power field that adds base damage per current Combo to all Attacks
- `attackPlayedDamage`: Power field that deals extra damage after an Attack card is played
- `damagePerTurn`: damage applied at turn start - `damagePerTurn`: damage applied at turn start
- `cardPlayedDamage`: damage when the card is played - `cardPlayedDamage`: damage when the card is played
- `cardPlayedRandomDamage`: random damage when the card is played - `cardPlayedRandomDamage`: random damage when the card is played
- `drawOnExhaust`: draw when a card is exhausted
- `rewardOnKill`: gain bonus reward screens when the card kills - `rewardOnKill`: gain bonus reward screens when the card kills
- `maxHpOnKill`: gain max HP when the attack kills
- `drawNameMatchAutoPlay`: auto-play drawn cards whose names contain this substring
- `randomTargetEachHit`: choose a random alive enemy for each hit - `randomTargetEachHit`: choose a random alive enemy for each hit
- `repeatOnKill`: repeat the attack when it kills at least one enemy - `repeatOnKill`: repeat the attack when it kills at least one enemy
- `firstCardDamageBonus`: bonus damage for the first card played this turn - `firstCardDamageBonus`: bonus damage for the first card played this turn
@@ -39,6 +49,17 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
- `drawUntilHandSize`: draw until hand reaches a target size - `drawUntilHandSize`: draw until hand reaches a target size
- `drawSkillBlock`: gain block for each Skill drawn - `drawSkillBlock`: gain block for each Skill drawn
- `drawPoison`: apply poison when a card is drawn - `drawPoison`: apply poison when a card is drawn
- `exhaustHandNonAttack`: exhaust every non-Attack card in hand
- `exhaustHandAll`: exhaust every card in hand
- `drawPerExhausted`: draw cards equal to exhausted cards
- `blockPerExhaustedCard`: gain block for each card exhausted by the current effect
- `addRandomCardCount`: add random cards to hand
- `addRandomCardPerExhausted`: add random cards equal to exhausted cards
- `addRandomCardKind`: filter random added cards by kind
- `addRandomCardSameClass`: restrict random added cards to the source card class
- `addedCardsCostZeroThisTurn`: cards added by this effect cost 0 this turn
- `playTopDrawPileCount`: play cards from the top of the draw pile
- `playTopDrawPileCountPerEnergy`: play cards from the top of the draw pile per energy spent
- `handCostZeroThisTurn`: make hand cards cost 0 this turn - `handCostZeroThisTurn`: make hand cards cost 0 this turn
- `drawDisabledThisTurn`: disable draw for the rest of the turn - `drawDisabledThisTurn`: disable draw for the rest of the turn
- `heal`: heal immediately - `heal`: heal immediately
@@ -48,6 +69,21 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
- `thorns`: gain Thorns - `thorns`: gain Thorns
- `selfVuln`: apply Vulnerable to self - `selfVuln`: apply Vulnerable to self
- `extraPoisonTicks`: add extra poison ticks at enemy turn start - `extraPoisonTicks`: add extra poison ticks at enemy turn start
- `comboGain`: gain Combo when the card resolves; Attack cards gain it after dealing damage
- `comboOnAttack`: Power field that gains Combo whenever an Attack card is played
- `comboMax`: Power field that raises the maximum Combo above the default 5
- `attackWeak`: Power field that applies Weak after an Attack card is played
- `removePlayerDebuffs`: remove player Weak and Vulnerable
- `holyForce`: marks a card as Holy Force for Holy Charge Power triggers
- `holyChargeGain`: gain Holy Charge directly
- `holyChargeOnHolyForce`: Power field that gains Holy Charge after a Holy Force card
- `holyChargeMax`: Power field that raises the maximum Holy Charge above the default 3
- `blockPerHolyCharge`: gain additional block per current Holy Charge
- `healPerHolyCharge`: heal additional HP per current Holy Charge
- `holyChargeSpendAll`: consume all Holy Charge after resolving the card
- `damageTakenReduction`: Power field that reduces incoming damage; total reduction is capped at 75%
- `blockOnDamaged`: Power field that grants block after taking HP damage
- `strengthOnDamagedOnce`: Power field that grants Strength on the first HP damage each combat
## Status ## Status
@@ -91,7 +127,9 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
- `powerEffect: "blockPerTurn"` - `powerEffect: "blockPerTurn"`
- `powerEffect: "poisonPerTurn"` - `powerEffect: "poisonPerTurn"`
- `powerEffect: "damagePerTurn"` - `powerEffect: "damagePerTurn"`
- `powerEffect: "healPerTurn"`
- `powerEffect: "retainOne"` - `powerEffect: "retainOne"`
- `powerEffect: "keepBlock"`
- `turnStartShiv`: create Shivs at turn start - `turnStartShiv`: create Shivs at turn start
- `turnStartDraw`: draw cards at turn start - `turnStartDraw`: draw cards at turn start
- `turnStartDiscard`: discard cards at turn start - `turnStartDiscard`: discard cards at turn start

View File

@@ -0,0 +1,164 @@
# Card UI Standardization
## Goal
Make every card-shaped UI use the same input structure so hover tooltip,
click, drag, and future interactions behave the same way everywhere.
Target surfaces:
- hand cards
- reward cards
- shop cards
- draw pile / discard pile / exhaust pile viewer cards
- all deck / class deck / codex cards
## Standard Card Entity Shape
Each visible card should have one root entity that owns input.
```text
CardRoot
- UITransformComponent
- SpriteGUIRendererComponent
- ButtonComponent
- UITouchReceiveComponent
- Cost
- Name
- Desc
- Art
```
Rules:
1. `CardRoot` covers the full card rect.
2. `CardRoot` owns both click and hover.
3. Child entities are display only.
4. Child entities must not become the primary input target.
## Required Component Rules
### Card root
- `UITransformComponent`
- `SpriteGUIRendererComponent`
- `ButtonComponent`
- `UITouchReceiveComponent`
Recommended sprite settings:
- `RaycastTarget: true`
- card frame sprite on root
### Card children
- `Cost`, `Name`, `Desc`: text only
- `Art`: sprite only
Recommended child sprite settings:
- `RaycastTarget: false`
That keeps hover/click ownership on the root card entity.
## Path Rules
Use the same path naming pattern everywhere:
- hand: `/ui/.../CardHand/Card1`
- reward: `/ui/.../RewardHud/Reward1`
- shop: `/ui/.../ShopHud/Card1`
- inspect: `/ui/.../DeckInspectHud/Grid/Card1`
- all deck: `/ui/.../DeckAllHud/Grid/Card1`
The important part is that each list has a stable root card path ending in a
numeric slot. That lets controller code resolve hover target -> slot -> card id
without special cases.
## Controller Rules
Once UI is standardized, controller logic should follow this shape:
1. Resolve `path`
2. Resolve `slot` from `path`
3. Resolve `cardId` from current backing list
4. Show tooltip from `cardId`
Preferred shared methods:
- `GetHoveredCardId(path)`
- `HoverCardByPath(path)`
- `UnhoverCardByPath(path)`
The hand should just become one caller of the same shared path-based hover
logic rather than having its own special tooltip flow.
## Safe Rollout Order
Do not standardize every card surface at once.
### Step 1
Document current structures for:
- `RewardHud/Reward1`
- `ShopHud/Card1`
- `DeckInspectHud/Grid/Card1`
- `DeckAllHud/Grid/Card1`
### Step 2
Make `DeckInspectHud/Grid/CardN` match `RewardHud/RewardN` component structure.
### Step 3
Verify game boot and UI load before changing controller hover bindings.
### Step 4
Make `DeckAllHud/Grid/CardN` match the same structure.
### Step 5
After both surfaces are stable, bind hover events in controller code.
## Validation Checklist
Before testing hover behavior:
- game boots with no `LEA-3015 CannotLoad`
- `DefaultGroup.ui` loads cleanly
- card roots exist at expected paths
- root card sprite has `RaycastTarget: true`
- child art sprite has `RaycastTarget: false`
- root has both `ButtonComponent` and `UITouchReceiveComponent`
After binding hover:
- hand tooltip still works
- reward tooltip still works
- shop tooltip still works
- inspect tooltip works
- all deck tooltip works
- no duplicated tooltip flicker
- no drag regression on hand cards
## What Not To Do
- do not hand-edit `ui/DefaultGroup.ui`
- do not change multiple card surfaces in one shot
- do not mix root-input cards with child-input cards
- do not add hover logic before confirming the target card entity actually
receives input
## Recommended Next Change
The next implementation step should be:
1. diff `DeckInspectHud/Grid/Card1` against `RewardHud/Reward1`
2. make only `DeckInspectHud` root card structure match
3. regenerate UI
4. boot test
5. then bind hover for inspect cards only
That keeps the blast radius small.

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@ import {
simulateCombat, simulateCombat,
} from './sim-balance.mjs'; } from './sim-balance.mjs';
const ROGUE_CLASSES = new Set(['rogue', 'thief', 'thiefmaster', 'assassin', 'hermit']); const AUDITED_CLASSES = new Set([
'rogue', 'thief', 'thiefmaster', 'assassin', 'hermit',
'warrior', 'fighter', 'crusader', 'page', 'knight',
]);
const CONTEXT_DECKS = { const CONTEXT_DECKS = {
rogue: [ rogue: [
@@ -38,6 +41,27 @@ const CONTEXT_DECKS = {
'Survivor', 'LeadingStrike', 'BladeDance', 'JavelinAcceleration', 'Survivor', 'LeadingStrike', 'BladeDance', 'JavelinAcceleration',
'JavelinMastery', 'TripleThrow', 'SpiritJavelin', 'SkilledJavelin', 'JavelinMastery', 'TripleThrow', 'SpiritJavelin', 'SkilledJavelin',
], ],
warrior: [
'Strike', 'Strike', 'Strike', 'Strike',
'Defend', 'Defend', 'Defend', 'Defend',
'Bash', 'SlashBlast', 'IronBody', 'WarriorMastery',
],
fighter: [
'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend',
'Bash', 'SlashBlast', 'ComboAttack', 'Brandish', 'WeaponMastery', 'FlashSlash',
],
crusader: [
'Strike', 'Strike', 'Defend', 'Defend', 'Bash', 'SlashBlast',
'ComboAttack', 'Brandish', 'WeaponMastery', 'BraveSlash', 'ComboSynergy', 'Rush',
],
page: [
'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend',
'Bash', 'SlashBlast', 'HolyCharge', 'DivineSwing', 'PageOrder', 'PageStance',
],
knight: [
'Strike', 'Strike', 'Defend', 'Defend', 'Bash', 'SlashBlast',
'HolyCharge', 'DivineSwing', 'PageOrder', 'DivineCharge', 'KnightRush', 'Restoration',
],
}; };
const ENCOUNTER_SCALE = { const ENCOUNTER_SCALE = {
@@ -46,6 +70,11 @@ const ENCOUNTER_SCALE = {
assassin: { hp: 2.25, attack: 1.65 }, assassin: { hp: 2.25, attack: 1.65 },
thiefmaster: { hp: 2.4, attack: 1.5 }, thiefmaster: { hp: 2.4, attack: 1.5 },
hermit: { hp: 2.6, attack: 1.65 }, hermit: { hp: 2.6, attack: 1.65 },
warrior: { hp: 1.9, attack: 1.5 },
fighter: { hp: 2.2, attack: 1.6 },
crusader: { hp: 2.6, attack: 1.7 },
page: { hp: 2.2, attack: 1.6 },
knight: { hp: 2.6, attack: 1.7 },
}; };
const median = (values) => { const median = (values) => {
@@ -172,7 +201,7 @@ export function auditCardEfficiency({ runs = 300, seed = 20260701 } = {}) {
const rows = []; const rows = [];
for (const [id, card] of Object.entries(cards)) { for (const [id, card] of Object.entries(cards)) {
if (!ROGUE_CLASSES.has(card.class)) continue; if (!AUDITED_CLASSES.has(card.class)) continue;
const deck = CONTEXT_DECKS[card.class].slice(); const deck = CONTEXT_DECKS[card.class].slice();
deck[replacementIndex(deck, cards, card)] = id; deck[replacementIndex(deck, cards, card)] = id;
const result = simulateDeck(scaledEncounter(data, card.class), deck, runs, seed, id); const result = simulateDeck(scaledEncounter(data, card.class), deck, runs, seed, id);
@@ -204,7 +233,7 @@ function formatPercent(value) {
export function formatEfficiencyReport(report) { export function formatEfficiencyReport(report) {
const lines = []; const lines = [];
lines.push(`도적 카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}`); lines.push(`카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}`);
lines.push('기준 덱:'); lines.push('기준 덱:');
for (const [classId, baseline] of Object.entries(report.baselines)) { for (const [classId, baseline] of Object.entries(report.baselines)) {
lines.push(` ${classId}: 승률 ${formatPercent(baseline.winRate)}, 평균 ${baseline.avgTurns.toFixed(2)}턴, 승리 HP ${baseline.avgHpOnWin.toFixed(1)}`); lines.push(` ${classId}: 승률 ${formatPercent(baseline.winRate)}, 평균 ${baseline.avgTurns.toFixed(2)}턴, 승리 HP ${baseline.avgHpOnWin.toFixed(1)}`);

View File

@@ -6,7 +6,7 @@ const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8')); const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
const relicsData = JSON.parse(readFileSync('data/relics.json', 'utf8')); const relicsData = JSON.parse(readFileSync('data/relics.json', 'utf8'));
const PLAYER_MAX_HP = 70; const PLAYER_MAX_HP = { rogue: 70, warrior: 80 };
const REST_HEAL = 30; const REST_HEAL = 30;
const SECTION_COUNT = 5; const SECTION_COUNT = 5;
const NORMAL_FIGHTS = 4; const NORMAL_FIGHTS = 4;
@@ -18,6 +18,8 @@ const BOSS_POOL = ['king_slime', 'slime_boss'];
const JOBS = { const JOBS = {
thief: { tier2: 'thief', tier3: 'thiefmaster', tier2Starter: 'DaggerAcceleration', tier3Starter: 'Venom' }, thief: { tier2: 'thief', tier3: 'thiefmaster', tier2Starter: 'DaggerAcceleration', tier3Starter: 'Venom' },
assassin: { tier2: 'assassin', tier3: 'hermit', tier2Starter: 'JavelinAcceleration', tier3Starter: 'SpiritJavelin' }, assassin: { tier2: 'assassin', tier3: 'hermit', tier2Starter: 'JavelinAcceleration', tier3Starter: 'SpiritJavelin' },
fighter: { root: 'warrior', tier2: 'fighter', tier3: 'crusader', tier2Starter: 'ComboAttack', tier3Starter: 'ComboSynergy' },
page: { root: 'warrior', tier2: 'page', tier3: 'knight', tier2Starter: 'HolyCharge', tier3Starter: 'DivineCharge' },
}; };
const LINEAGES = { const LINEAGES = {
@@ -26,12 +28,17 @@ const LINEAGES = {
thiefmaster: ['rogue', 'thief', 'thiefmaster'], thiefmaster: ['rogue', 'thief', 'thiefmaster'],
assassin: ['rogue', 'assassin'], assassin: ['rogue', 'assassin'],
hermit: ['rogue', 'assassin', 'hermit'], hermit: ['rogue', 'assassin', 'hermit'],
warrior: ['warrior'],
fighter: ['warrior', 'fighter'],
crusader: ['warrior', 'fighter', 'crusader'],
page: ['warrior', 'page'],
knight: ['warrior', 'page', 'knight'],
}; };
const pick = (rng, values) => values[Math.floor(rng() * values.length)]; const pick = (rng, values) => values[Math.floor(rng() * values.length)];
export function campaignJobAtSection(branch, section) { export function campaignJobAtSection(branch, section) {
if (section <= 1) return 'rogue'; if (section <= 1) return JOBS[branch].root || 'rogue';
if (section === 2) return JOBS[branch].tier2; if (section === 2) return JOBS[branch].tier2;
return JOBS[branch].tier3; return JOBS[branch].tier3;
} }
@@ -102,11 +109,22 @@ function branchCardValue(card, branch, deck, id) {
value += card.sly ? 5 : 0; value += card.sly ? 5 : 0;
value += (card.discard || 0) * 2 + (card.drawPerDiscarded || 0) * 4; value += (card.discard || 0) * 2 + (card.drawPerDiscarded || 0) * 4;
value += (card.poisonApplicationBurstDamage || 0) * 1.5; value += (card.poisonApplicationBurstDamage || 0) * 1.5;
} else { } else if (branch === 'assassin') {
value += (card.addShiv || 0) * 3 + (card.turnStartShiv || 0) * 8; value += (card.addShiv || 0) * 3 + (card.turnStartShiv || 0) * 8;
value += (card.shivDamageBonus || 0) * 6 + (card.firstShivDamageBonus || 0) * 3; value += (card.shivDamageBonus || 0) * 6 + (card.firstShivDamageBonus || 0) * 3;
value += card.shivAoe ? 12 : 0; value += card.shivAoe ? 12 : 0;
value += card.shivRetain ? 5 : 0; value += card.shivRetain ? 5 : 0;
} else if (branch === 'fighter') {
value += (card.hits || 1) * 1.5;
value += (card.comboGain || 0) * 5 + (card.comboOnAttack || 0) * 10;
value += (card.damagePerCombo || 0) * 8 + (card.attackDamagePerCombo || 0) * 12;
value += (card.attackPlayedDamage || 0) * 5;
} else if (branch === 'page') {
value += (card.block || 0) * 0.5 + (card.cardPlayedBlock || 0) * 7;
value += (card.holyChargeOnHolyForce || 0) * 12 + (card.damagePerHolyCharge || 0) * 7;
value += (card.blockPerHolyCharge || 0) * 6 + (card.healPerHolyCharge || 0) * 3;
value += (card.damageTakenReduction || 0) * 40;
value += (card.blockOnDamaged || 0) * 3 + (card.strengthOnDamagedOnce || 0) * 5;
} }
const copies = deck.filter((cardId) => cardId === id).length; const copies = deck.filter((cardId) => cardId === id).length;
value -= copies * (card.kind === 'Power' ? 10 : 3); value -= copies * (card.kind === 'Power' ? 10 : 3);
@@ -192,7 +210,8 @@ function fight(state, branch, kind, section, rng, options) {
state.turns += result.turns; state.turns += result.turns;
if (!result.win) return false; if (!result.win) return false;
healFromRelics(state, 'combatEnd'); healFromRelics(state, 'combatEnd');
if (kind !== 'boss') offerReward(state.job, branch, state.deck, rng, options.minimumRewardValue); const rewardStrategy = state.job === 'warrior' || state.job === 'rogue' ? state.job : branch;
if (kind !== 'boss') offerReward(state.job, rewardStrategy, state.deck, rng, options.minimumRewardValue);
return true; return true;
} }
@@ -203,11 +222,13 @@ export function simulateCampaign(branch, rng, {
minimumRewardValue = 10, minimumRewardValue = 10,
} = {}) { } = {}) {
if (!JOBS[branch]) throw new Error(`지원하지 않는 도적 분기: ${branch}`); if (!JOBS[branch]) throw new Error(`지원하지 않는 도적 분기: ${branch}`);
const root = JOBS[branch].root || 'rogue';
const maxHp = PLAYER_MAX_HP[root];
const state = { const state = {
hp: PLAYER_MAX_HP, hp: maxHp,
maxHp: PLAYER_MAX_HP, maxHp,
deck: cardsData.starterDecks.rogue.slice(), deck: cardsData.starterDecks[root].slice(),
job: 'rogue', job: root,
turns: 0, turns: 0,
sectionCleared: 0, sectionCleared: 0,
diedAt: '', diedAt: '',
@@ -306,7 +327,7 @@ function main() {
else if (args[i] === '--scale-step') scaleStep = Number.parseFloat(args[++i]); else if (args[i] === '--scale-step') scaleStep = Number.parseFloat(args[++i]);
else if (args[i] === '--reward-min') minimumRewardValue = Number.parseFloat(args[++i]); else if (args[i] === '--reward-min') minimumRewardValue = Number.parseFloat(args[++i]);
} }
for (const branch of ['thief', 'assassin']) { for (const branch of ['thief', 'assassin', 'fighter', 'page']) {
console.log(formatCampaignReport(runCampaignBatch(branch, runs, seed, { restHeal, sectionHeal, scaleStep, minimumRewardValue }))); console.log(formatCampaignReport(runCampaignBatch(branch, runs, seed, { restHeal, sectionHeal, scaleStep, minimumRewardValue })));
} }
} }

View File

@@ -17,6 +17,16 @@ test('도적 전직 시점: 1섹션 Rogue, 2섹션 2차, 3섹션부터 3차', ()
test('3차 직업은 자기 계보 카드만 사용', () => { test('3차 직업은 자기 계보 카드만 사용', () => {
assert.deepEqual(playableClassesForJob('thiefmaster'), ['rogue', 'thief', 'thiefmaster']); assert.deepEqual(playableClassesForJob('thiefmaster'), ['rogue', 'thief', 'thiefmaster']);
assert.deepEqual(playableClassesForJob('hermit'), ['rogue', 'assassin', 'hermit']); assert.deepEqual(playableClassesForJob('hermit'), ['rogue', 'assassin', 'hermit']);
assert.deepEqual(playableClassesForJob('crusader'), ['warrior', 'fighter', 'crusader']);
assert.deepEqual(playableClassesForJob('knight'), ['warrior', 'page', 'knight']);
});
test('전사 전직 시점: 1섹션 Warrior, 2섹션 2차, 3섹션부터 3차', () => {
assert.equal(campaignJobAtSection('fighter', 1), 'warrior');
assert.equal(campaignJobAtSection('fighter', 2), 'fighter');
assert.equal(campaignJobAtSection('fighter', 3), 'crusader');
assert.equal(campaignJobAtSection('page', 2), 'page');
assert.equal(campaignJobAtSection('page', 5), 'knight');
}); });
test('섹션 난이도는 3차 이후 더 빠르게 증가', () => { test('섹션 난이도는 3차 이후 더 빠르게 증가', () => {

View File

@@ -99,7 +99,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const card = cards[x.id]; const card = cards[x.id];
if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false; if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false;
let effectiveCost = card.cost || 0; let effectiveCost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) effectiveCost = 0; if (ctx.handCostZeroThisTurn === true || ctx.zeroCostCardIdsThisTurn?.has(x.id) === true) effectiveCost = 0;
else if (card.useAllEnergy === true) effectiveCost = 1; else if (card.useAllEnergy === true) effectiveCost = 1;
else if (card.kind === 'Skill') { else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) effectiveCost = 0; if (ctx.nextSkillCostZero === true) effectiveCost = 0;
@@ -116,7 +116,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const effectiveCost = (x) => { const effectiveCost = (x) => {
const card = cards[x.id]; const card = cards[x.id];
let cost = card.cost || 0; let cost = card.cost || 0;
if (ctx.handCostZeroThisTurn === true) cost = 0; if (ctx.handCostZeroThisTurn === true || ctx.zeroCostCardIdsThisTurn?.has(x.id) === true) cost = 0;
else if (card.useAllEnergy === true) cost = 1; else if (card.useAllEnergy === true) cost = 1;
else if (card.kind === 'Skill') { else if (card.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) cost = 0; if (ctx.nextSkillCostZero === true) cost = 0;
@@ -127,7 +127,48 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
} }
return cost; return cost;
}; };
const dmgEff = (x) => (cards[x.id].damage || 0) / Math.max(effectiveCost(x), 1); const countOtherHandSkills = (currentId) => {
let n = 0;
let skippedSelf = false;
for (const id of hand) {
if (!skippedSelf && id === currentId) {
skippedSelf = true;
continue;
}
if (cards[id]?.kind === 'Skill') n++;
}
return n;
};
const countOwnedNameMatches = (match) => {
if (!match) return 0;
let n = 0;
for (const id of hand) {
const name = cards[id]?.name || '';
if (name.includes(match)) n++;
}
for (const pile of [ctx.drawPileCards || [], ctx.discardCards || [], ctx.exhaustCards || []]) {
for (const id of pile) {
const name = cards[id]?.name || '';
if (name.includes(match)) n++;
}
}
return n;
};
const attackBaseEstimate = (x) => {
const card = cards[x.id];
let base = card.damage || 0;
base += (ctx.currentBlock || 0) * (card.damageFromCurrentBlock || 0);
if (card.damageNameMatch && card.damagePerOwnedNameMatch) {
base += countOwnedNameMatches(card.damageNameMatch) * card.damagePerOwnedNameMatch;
}
base += Math.max(0, hand.length - 1) * (card.damagePerOtherHandCard || 0);
base += (ctx.turnAttackCardsPlayed || 0) * (card.damagePerAttackPlayedThisTurn || 0);
base += countOtherHandSkills(x.id) * (card.damagePerSkillInHand || 0);
base += (ctx.cardsDrawnThisCombat || 0) * (card.damagePerCardDrawnThisCombat || 0);
if (base < 0) base = 0;
return base;
};
const dmgEff = (x) => attackBaseEstimate(x) / Math.max(effectiveCost(x), 1);
const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(x), 1); const blkEff = (x) => (cards[x.id].block || 0) / Math.max(effectiveCost(x), 1);
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0]; const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
if ((ctx.incomingDamage || 0) > (ctx.currentBlock || 0)) { if ((ctx.incomingDamage || 0) > (ctx.currentBlock || 0)) {
@@ -144,8 +185,12 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
} }
} }
if (powers.length) return powers[0].i; if (powers.length) return powers[0].i;
if (attacks.length) return bestBy(attacks, dmgEff).i; if (attacks.length) {
const bestAttack = bestBy(attacks, dmgEff);
if (bestAttack && dmgEff(bestAttack) > 0) return bestAttack.i;
}
if (skills.length) return bestBy(skills, blkEff).i; if (skills.length) return bestBy(skills, blkEff).i;
if (attacks.length) return bestBy(attacks, dmgEff).i;
return -1; return -1;
} }
@@ -167,13 +212,14 @@ function bump(s, cost, dmg, blk) {
// 반환: { win, turns, playerHpRemaining, draw? } // 반환: { win, turns, playerHpRemaining, draw? }
export function simulateCombat(data, rng, stats) { export function simulateCombat(data, rng, stats) {
const { cards, starterDeck, monsters } = data; const { cards, starterDeck, monsters } = data;
const playerMaxHp = data.playerMaxHp || PLAYER_HP; let playerMaxHp = data.playerMaxHp || PLAYER_HP;
const startingPlayerHp = Math.min(data.playerHp ?? playerMaxHp, playerMaxHp); const startingPlayerHp = Math.min(data.playerHp ?? playerMaxHp, playerMaxHp);
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: startingPlayerHp }; if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: startingPlayerHp };
let drawPile = prepareCombatDrawPile(shuffle(starterDeck, rng), cards); let drawPile = prepareCombatDrawPile(shuffle(starterDeck, rng), cards);
let discard = []; let discard = [];
const exhaust = []; const exhaust = [];
let hand = []; let hand = [];
const zeroCostCardIdsThisTurn = new Set();
let pHp = startingPlayerHp, pBlock = data.playerStartBlock || 0; let pHp = startingPlayerHp, pBlock = data.playerStartBlock || 0;
let pStr = data.playerStrength || 0, pDex = 0, pThorns = data.playerThorns || 0, pWeak = 0, pVuln = 0, pIntangible = 0; let pStr = data.playerStrength || 0, pDex = 0, pThorns = data.playerThorns || 0, pWeak = 0, pVuln = 0, pIntangible = 0;
let blockGainMultiplier = 1; let blockGainMultiplier = 1;
@@ -200,6 +246,9 @@ export function simulateCombat(data, rng, stats) {
let cardsDrawnThisCombat = 0; let cardsDrawnThisCombat = 0;
let bonusRewardScreens = 0; let bonusRewardScreens = 0;
let activeKillReward = 0; let activeKillReward = 0;
let comboCount = 0;
let holyChargeCount = 0;
let damagePowerStrengthUsed = false;
let energy = 0; let energy = 0;
const powers = []; const powers = [];
const mob = monsters.map((m) => ({ const mob = monsters.map((m) => ({
@@ -215,6 +264,14 @@ export function simulateCombat(data, rng, stats) {
if (!alive.length) return null; if (!alive.length) return null;
return alive[Math.floor(rng() * alive.length)]; return alive[Math.floor(rng() * alive.length)];
}; };
const randomCardPool = (sourceCard) => Object.entries(cards)
.filter(([, rc]) => {
if (!rc || rc.token === true || rc.curse === true || rc.unplayable === true) return false;
if (sourceCard.addRandomCardKind && rc.kind !== sourceCard.addRandomCardKind) return false;
if (sourceCard.addRandomCardSameClass === true && rc.class !== sourceCard.class) return false;
return true;
})
.map(([id]) => id);
const expectedIncomingDamage = () => mob.filter((m) => m.alive).reduce((total, m) => { const expectedIncomingDamage = () => mob.filter((m) => m.alive).reduce((total, m) => {
if (!m.intents || m.intents.length === 0) return total; if (!m.intents || m.intents.length === 0) return total;
const expected = m.intents.reduce((sum, intent) => { const expected = m.intents.reduce((sum, intent) => {
@@ -315,10 +372,26 @@ export function simulateCombat(data, rng, stats) {
if (hand.length >= 10) { if (hand.length >= 10) {
discard.push(card); discard.push(card);
triggerSly(card); triggerSly(card);
} else hand.push(card); } else {
hand.push(card);
triggerDrawNameMatchAutoPlay(card);
}
} }
return drawn; return drawn;
} }
function triggerDrawNameMatchAutoPlay(drawnId) {
const drawnCard = cards[drawnId];
const drawnName = drawnCard?.name || '';
if (!drawnName) return;
for (const pid of powers) {
const pc = cards[pid];
if (!pc?.drawNameMatchAutoPlay || !drawnName.includes(pc.drawNameMatchAutoPlay)) continue;
const idx = hand.indexOf(drawnId);
if (idx < 0) continue;
hand.splice(idx, 1);
autoPlayCardFromEffect(drawnId, 0);
}
}
function addCardsToHand(id, n) { function addCardsToHand(id, n) {
for (let k = 0; k < n; k++) { for (let k = 0; k < n; k++) {
if (hand.length >= 10) discard.push(id); if (hand.length >= 10) discard.push(id);
@@ -380,8 +453,26 @@ export function simulateCombat(data, rng, stats) {
} }
return n; return n;
} }
function countOwnedNameMatches(match) {
if (!match) return 0;
let n = 0;
for (const pile of [hand, drawPile, discard, exhaust]) {
for (const id of pile) {
const name = cards[id]?.name || '';
if (name.includes(match)) n++;
}
}
return n;
}
function attackBaseForCard(id, c) { function attackBaseForCard(id, c) {
let base = c.damage || 0; let base = c.damage || 0;
if (c.damageNameMatch && c.damagePerOwnedNameMatch) {
base += countOwnedNameMatches(c.damageNameMatch) * c.damagePerOwnedNameMatch;
}
if (c.damageFromCurrentBlock) base += pBlock * c.damageFromCurrentBlock;
const comboScale = (c.damagePerCombo || 0) + powerFieldTotal('attackDamagePerCombo');
if (comboScale) base += comboCount * comboScale;
if (c.damagePerHolyCharge) base += holyChargeCount * c.damagePerHolyCharge;
const otherHand = Math.max(0, hand.length - 1); const otherHand = Math.max(0, hand.length - 1);
if (c.damagePerOtherHandCard) base += otherHand * c.damagePerOtherHandCard; if (c.damagePerOtherHandCard) base += otherHand * c.damagePerOtherHandCard;
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn; if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
@@ -433,6 +524,69 @@ export function simulateCombat(data, rng, stats) {
} }
return total; return total;
} }
function powerFieldMax(field) {
let best = 0;
for (const pid of powers) best = Math.max(best, cards[pid]?.[field] || 0);
return best;
}
function comboMax() {
return Math.max(5, powerFieldMax('comboMax'));
}
function gainCombo(amount) {
if (amount > 0) comboCount = Math.min(comboMax(), comboCount + amount);
}
function holyChargeMax() {
return Math.max(3, powerFieldMax('holyChargeMax'));
}
function gainHolyCharge(amount) {
if (amount > 0) holyChargeCount = Math.min(holyChargeMax(), holyChargeCount + amount);
}
function triggerExhaust(count = 1) {
const drawOnExhaust = powerFieldTotal('drawOnExhaust');
if (drawOnExhaust > 0 && count > 0) draw(drawOnExhaust * count);
}
function addRandomCardsFromEffect(sourceCard, count) {
if (!count || count <= 0) return [];
const pool = randomCardPool(sourceCard);
if (!pool.length) return [];
const added = [];
for (let i = 0; i < count; i++) {
const id = pool[Math.floor(rng() * pool.length)];
if (!id) continue;
addCardsToHand(id, 1);
if (sourceCard.addedCardsCostZeroThisTurn === true) zeroCostCardIdsThisTurn.add(id);
added.push(id);
}
return added;
}
function exhaustHandNonAttackEffects(c) {
if (c.exhaustHandNonAttack !== true || hand.length === 0) return 0;
let exhaustedCount = 0;
for (let i = hand.length - 1; i >= 0; i--) {
const id = hand[i];
const hc = cards[id];
if (hc?.kind === 'Attack') continue;
hand.splice(i, 1);
exhaust.push(id);
exhaustedCount++;
}
if (exhaustedCount > 0) {
if (c.blockPerExhaustedCard) addBlock(exhaustedCount * c.blockPerExhaustedCard);
triggerExhaust(exhaustedCount);
}
return exhaustedCount;
}
function exhaustHandAllEffects(c) {
if (c.exhaustHandAll !== true || hand.length === 0) return 0;
let exhaustedCount = 0;
while (hand.length > 0) {
const id = hand.pop();
exhaust.push(id);
exhaustedCount++;
}
if (exhaustedCount > 0) triggerExhaust(exhaustedCount);
return exhaustedCount;
}
function resolveCardEffects(id, c, costSpent, recordStats = true) { function resolveCardEffects(id, c, costSpent, recordStats = true) {
const alive = aliveList(); const alive = aliveList();
let dmg = 0; let dmg = 0;
@@ -461,7 +615,7 @@ export function simulateCombat(data, rng, stats) {
} }
const xEnergy = costSpent || 0; const xEnergy = costSpent || 0;
if (c.kind === 'Attack') { if (c.kind === 'Attack') {
if (alive.length && (c.damage || c.xDamagePerEnergy)) { if (alive.length && (c.damage != null || c.xDamagePerEnergy != null || c.damageFromCurrentBlock != null)) {
const baseDamage = c.xDamagePerEnergy ? xEnergy * c.xDamagePerEnergy : attackBaseForCard(id, c); const baseDamage = c.xDamagePerEnergy ? xEnergy * c.xDamagePerEnergy : attackBaseForCard(id, c);
const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast) const bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
? c.bonusHitsWhenOtherHandAtLeast : 0; ? c.bonusHitsWhenOtherHandAtLeast : 0;
@@ -476,8 +630,9 @@ export function simulateCombat(data, rng, stats) {
if (!target || !target.alive) return { killed: false, dealt: 0 }; if (!target || !target.alive) return { killed: false, dealt: 0 };
let dealt = amount; let dealt = amount;
if (target.vuln > 0) dealt = Math.floor(dealt * 1.5); if (target.vuln > 0) dealt = Math.floor(dealt * 1.5);
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) { const weakMultiplier = Math.max(c.attackDamageVsWeakMultiplier || 1, powerFieldMax('attackDamageVsWeakMultiplier'));
dealt = Math.floor(dealt * c.attackDamageVsWeakMultiplier); if (target.weak > 0 && weakMultiplier > 1) {
dealt = Math.floor(dealt * weakMultiplier);
} }
if (c.pierce === true) { if (c.pierce === true) {
target.hp -= dealt; target.hp -= dealt;
@@ -493,6 +648,10 @@ export function simulateCombat(data, rng, stats) {
target.alive = false; target.alive = false;
killed = true; killed = true;
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill; if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
if (c.maxHpOnKill) {
playerMaxHp += c.maxHpOnKill;
pHp += c.maxHpOnKill;
}
} }
return { killed, dealt }; return { killed, dealt };
}; };
@@ -534,11 +693,11 @@ export function simulateCombat(data, rng, stats) {
roundKilled = resolveAttackRound(); roundKilled = resolveAttackRound();
} while (c.repeatOnKill === true && roundKilled === true && countAliveMonsters() > 0); } while (c.repeatOnKill === true && roundKilled === true && countAliveMonsters() > 0);
} }
if (c.block) blockGained = addBlock(c.block); if (c.block) blockGained = addBlock(c.block + holyChargeCount * (c.blockPerHolyCharge || 0));
} else if (c.kind === 'Power') { } else if (c.kind === 'Power') {
if (recordStats) powers.push(id); powers.push(id);
} else { } else {
if (c.block) blockGained = addBlock(c.block); if (c.block) blockGained = addBlock(c.block + holyChargeCount * (c.blockPerHolyCharge || 0));
const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy; const weakAmount = (c.weak || 0) + (c.xWeakPerEnergy || 0) * xEnergy;
const vulnAmount = c.vuln || 0; const vulnAmount = c.vuln || 0;
if ((weakAmount || vulnAmount || c.poison || c.removeEnemyBlock || c.removeEnemyArtifact || c.enemyStrengthLossThisTurn) && alive.length) { if ((weakAmount || vulnAmount || c.poison || c.removeEnemyBlock || c.removeEnemyArtifact || c.enemyStrengthLossThisTurn) && alive.length) {
@@ -570,8 +729,10 @@ export function simulateCombat(data, rng, stats) {
if (c.dex) pDex += c.dex; if (c.dex) pDex += c.dex;
if (c.thorns) pThorns += c.thorns; if (c.thorns) pThorns += c.thorns;
if (c.selfVuln) pVuln += c.selfVuln; if (c.selfVuln) pVuln += c.selfVuln;
if (c.heal) pHp = Math.min(pHp + c.heal, playerMaxHp); if (c.heal) pHp = Math.min(pHp + c.heal + holyChargeCount * (c.healPerHolyCharge || 0), playerMaxHp);
if (c.gainEnergy) energy += c.gainEnergy; if (c.gainEnergy) energy += c.gainEnergy;
if (c.kind !== 'Attack' && c.comboGain) gainCombo(c.comboGain);
if (c.removePlayerDebuffs === true) { pWeak = 0; pVuln = 0; }
activeKillReward = c.rewardOnKill || 0; activeKillReward = c.rewardOnKill || 0;
if (c.intangible) pIntangible += c.intangible; if (c.intangible) pIntangible += c.intangible;
queueNextTurnEffects(c); queueNextTurnEffects(c);
@@ -588,6 +749,28 @@ export function simulateCombat(data, rng, stats) {
} }
} }
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv); if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
const exhaustedCount = exhaustHandNonAttackEffects(c);
const exhaustedAllCount = exhaustHandAllEffects(c);
const totalExhausted = exhaustedCount + exhaustedAllCount;
if (exhaustedCount > 0 && c.blockPerExhaustedCard) {
blockGained += exhaustedCount * c.blockPerExhaustedCard;
}
if (c.drawPerExhausted && totalExhausted > 0) {
draw(totalExhausted * c.drawPerExhausted);
}
if (c.addRandomCardCount) addRandomCardsFromEffect(c, c.addRandomCardCount);
if (c.addRandomCardPerExhausted) {
if (totalExhausted > 0) addRandomCardsFromEffect(c, totalExhausted * c.addRandomCardPerExhausted);
}
const topPlayCount = (c.playTopDrawPileCount || 0) + ((c.playTopDrawPileCountPerEnergy || 0) * xEnergy);
if (topPlayCount > 0) {
for (let i = 0; i < topPlayCount; i++) {
if (drawPile.length <= 0) break;
const topId = drawPile.pop();
if (!topId) break;
autoPlayCardFromEffect(topId, 0);
}
}
if (c.cardPlayedDamage && alive.length) { if (c.cardPlayedDamage && alive.length) {
const target = chooseTarget(aliveList(), 0); const target = chooseTarget(aliveList(), 0);
if (target && target.alive) { if (target && target.alive) {
@@ -609,6 +792,28 @@ export function simulateCombat(data, rng, stats) {
} }
} }
} }
if (c.kind === 'Attack') {
gainCombo((c.comboGain || 0) + powerFieldTotal('comboOnAttack'));
const extraDamage = powerFieldTotal('attackPlayedDamage');
if (extraDamage > 0) {
const target = chooseTarget(aliveList(), extraDamage);
if (target) {
const r = applyDamage(target.hp, target.block, extraDamage);
target.hp = r.hp; target.block = r.block;
damageDealtThisTurn += extraDamage;
if (target.hp <= 0) target.alive = false;
}
}
const attackWeak = powerFieldTotal('attackWeak');
if (attackWeak > 0) {
const target = chooseTarget(aliveList(), 0);
if (target) applyMonsterWeak(target, attackWeak);
}
}
let holyGain = c.holyChargeGain || 0;
if (c.holyForce === true) holyGain += powerFieldTotal('holyChargeOnHolyForce');
gainHolyCharge(holyGain);
if (c.holyChargeSpendAll === true) holyChargeCount = 0;
if (c.blockPerDamageDealtThisTurn && c.blockPerDamageDealtThisTurn > 0 && c.kind !== 'Power') { if (c.blockPerDamageDealtThisTurn && c.blockPerDamageDealtThisTurn > 0 && c.kind !== 'Power') {
blockGained += addBlock(Math.max(0, damageDealtThisTurn * c.blockPerDamageDealtThisTurn)); blockGained += addBlock(Math.max(0, damageDealtThisTurn * c.blockPerDamageDealtThisTurn));
} }
@@ -639,7 +844,48 @@ export function simulateCombat(data, rng, stats) {
if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded); if (c.addShivPerDiscard === true) addCardsToHand('Shiv', discarded);
if (c.drawPerDiscarded) draw(discarded * c.drawPerDiscarded); if (c.drawPerDiscarded) draw(discarded * c.drawPerDiscarded);
} }
function autoPlayCardFromEffect(id, energySpent = 0) {
const c = cards[id];
if (!c) return false;
const skillFree = c.kind === 'Skill' && c.useAllEnergy !== true && nextSkillCostZero === true;
const skillRepeat = c.kind === 'Skill' && (nextSkillRepeatCount || 0) > 0 ? nextSkillRepeatCount : 0;
activeKillReward = c.rewardOnKill || 0;
resolveCardEffects(id, c, energySpent, false);
const playedBlock = powerFieldTotal('cardPlayedBlock');
if (playedBlock > 0) addBlock(playedBlock);
if (c.cardPlayedDamage && aliveList().length) {
const target = chooseTarget(aliveList(), 0);
if (target && target.alive) {
target.hp -= c.cardPlayedDamage;
damageDealtThisTurn += c.cardPlayedDamage;
if (target.hp <= 0) target.alive = false;
}
}
if (c.cardPlayedRandomDamage && aliveList().length) {
const target = randomAliveMonster();
if (target) {
target.hp -= c.cardPlayedRandomDamage;
damageDealtThisTurn += c.cardPlayedRandomDamage;
if (target.hp <= 0) target.alive = false;
}
}
if (skillRepeat > 0) {
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
for (let r = 0; r < skillRepeat; r++) {
resolveCardEffects(id, c, energySpent, false);
if (playedBlock > 0) addBlock(playedBlock);
}
}
if (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) {
exhaust.push(id);
triggerExhaust(1);
} else if (c.kind !== 'Power') {
discard.push(id);
}
return true;
}
while (turns < MAX_TURNS) { while (turns < MAX_TURNS) {
turns++; turns++;
turnAttackCardsPlayed = 0; turnAttackCardsPlayed = 0;
@@ -655,7 +901,9 @@ export function simulateCombat(data, rng, stats) {
drawDisabledThisTurn = false; drawDisabledThisTurn = false;
skillCostReductionThisTurn = 0; skillCostReductionThisTurn = 0;
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워) // 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
zeroCostCardIdsThisTurn.clear();
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false; if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
else if (powers.some((pid) => cards[pid]?.powerEffect === 'keepBlock')) {}
else pBlock = 0; else pBlock = 0;
turnAttackMultiplier = nextTurnAttackMultiplier; turnAttackMultiplier = nextTurnAttackMultiplier;
nextTurnAttackMultiplier = 1; nextTurnAttackMultiplier = 1;
@@ -668,6 +916,7 @@ export function simulateCombat(data, rng, stats) {
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value; if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value; else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value; else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
else if (pc.powerEffect === 'keepBlock') {}
else if (pc.powerEffect === 'poisonPerTurn') { else if (pc.powerEffect === 'poisonPerTurn') {
for (const m of mob) if (m.alive) applyPoisonToMonster(m, pc.value); for (const m of mob) if (m.alive) applyPoisonToMonster(m, pc.value);
} else if (pc.powerEffect === 'damagePerTurn') { } else if (pc.powerEffect === 'damagePerTurn') {
@@ -677,6 +926,8 @@ export function simulateCombat(data, rng, stats) {
m.hp = r.hp; m.block = r.block; m.hp = r.hp; m.block = r.block;
if (m.hp <= 0) m.alive = false; if (m.hp <= 0) m.alive = false;
} }
} else if (pc.powerEffect === 'healPerTurn') {
pHp = Math.min(playerMaxHp, pHp + (pc.value || 0));
} }
if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv); if (pc.turnStartShiv) addCardsToHand('Shiv', pc.turnStartShiv);
if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw; if (pc.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
@@ -701,8 +952,14 @@ export function simulateCombat(data, rng, stats) {
skillCostReductionThisTurn, skillCostReductionThisTurn,
handCostZeroThisTurn, handCostZeroThisTurn,
combatCardCostReduction, combatCardCostReduction,
zeroCostCardIdsThisTurn,
incomingDamage: data.smartPlayer === true ? expectedIncomingDamage() : 0, incomingDamage: data.smartPlayer === true ? expectedIncomingDamage() : 0,
currentBlock: pBlock, currentBlock: pBlock,
turnAttackCardsPlayed,
cardsDrawnThisCombat,
drawPileCards: drawPile,
discardCards: discard,
exhaustCards: exhaust,
}); });
if (idx < 0) break; if (idx < 0) break;
const id = hand[idx], c = cards[id]; const id = hand[idx], c = cards[id];
@@ -714,6 +971,7 @@ export function simulateCombat(data, rng, stats) {
const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost))); const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)));
const finalCost = c.useAllEnergy === true ? cost : Math.max(0, cost - combatReduction); const finalCost = c.useAllEnergy === true ? cost : Math.max(0, cost - combatReduction);
energy -= finalCost; energy -= finalCost;
hand.splice(idx, 1);
resolveCardEffects(id, c, finalCost); resolveCardEffects(id, c, finalCost);
if (c.kind === 'Attack' && (data.healOnAttack || 0) > 0) { if (c.kind === 'Attack' && (data.healOnAttack || 0) > 0) {
pHp = Math.min(playerMaxHp, pHp + data.healOnAttack); pHp = Math.min(playerMaxHp, pHp + data.healOnAttack);
@@ -729,9 +987,11 @@ export function simulateCombat(data, rng, stats) {
} }
if (c.kind === 'Attack') turnAttackCardsPlayed++; if (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false; if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
hand.splice(idx, 1);
queueSelectedReserve(c); queueSelectedReserve(c);
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id); if (c.exhaust === true || String(c.desc || '').includes('소멸.')) {
exhaust.push(id);
triggerExhaust(1);
}
else if (c.kind !== 'Power') discard.push(id); else if (c.kind !== 'Power') discard.push(id);
if (c.combatCostReductionOnPlay && c.combatCostReductionOnPlay > 0) { if (c.combatCostReductionOnPlay && c.combatCostReductionOnPlay > 0) {
combatCardCostReduction[id] = (combatCardCostReduction[id] || 0) + c.combatCostReductionOnPlay; combatCardCostReduction[id] = (combatCardCostReduction[id] || 0) + c.combatCostReductionOnPlay;
@@ -781,8 +1041,18 @@ export function simulateCombat(data, rng, stats) {
const atk = calcEnemyAttack(it.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn); const atk = calcEnemyAttack(it.value, m.str, m.weak, pVuln, enemyStrengthLossThisTurn);
const beforeHp = pHp; const beforeHp = pHp;
let incoming = atk; let incoming = atk;
const reduction = Math.min(0.75, powerFieldTotal('damageTakenReduction'));
if (reduction > 0) incoming = Math.floor(incoming * (1 - reduction));
if (pIntangible > 0 && incoming > 1) incoming = 1; if (pIntangible > 0 && incoming > 1) incoming = 1;
const r = applyDamage(pHp, pBlock, incoming); pHp = r.hp; pBlock = r.block; const r = applyDamage(pHp, pBlock, incoming); pHp = r.hp; pBlock = r.block;
if (beforeHp > pHp) {
const reactiveBlock = powerFieldTotal('blockOnDamaged');
if (reactiveBlock > 0) addBlock(reactiveBlock);
if (!damagePowerStrengthUsed) {
const reactiveStrength = powerFieldTotal('strengthOnDamagedOnce');
if (reactiveStrength > 0) { pStr += reactiveStrength; damagePowerStrengthUsed = true; }
}
}
if (beforeHp > pHp && pThorns > 0) { if (beforeHp > pHp && pThorns > 0) {
m.hp -= pThorns; m.hp -= pThorns;
if (m.hp <= 0) m.alive = false; if (m.hp <= 0) m.alive = false;

View File

@@ -649,9 +649,10 @@ test("simulateCombat: damagePerAttackPlayedThisTurn scales Finisher", () => {
starterDeck: ["Hit", "Finisher"], starterDeck: ["Hit", "Finisher"],
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }], monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }],
}; };
const r = simulateCombat(data, () => 0); const stats = {};
const r = simulateCombat(data, () => 0, stats);
assert.equal(r.win, true); assert.equal(r.win, true);
assert.equal(r.turns, 2); assert.ok((stats.Finisher?.damage || 0) >= 6);
}); });
test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applied", () => { test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applied", () => {
@@ -666,9 +667,11 @@ test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applie
starterDeck: ["Skill1", "Skill2", "Blank", "Precise", "Flechettes"], starterDeck: ["Skill1", "Skill2", "Blank", "Precise", "Flechettes"],
monsters: [{ name: "Dummy", maxHp: 21, intents: [{ kind: "Attack", value: 0 }] }], monsters: [{ name: "Dummy", maxHp: 21, intents: [{ kind: "Attack", value: 0 }] }],
}; };
const r = simulateCombat(data, () => 0); const stats = {};
const r = simulateCombat(data, () => 0, stats);
assert.equal(r.win, true); assert.equal(r.win, true);
assert.equal(r.turns, 5); assert.ok((stats.Precise?.damage || 0) >= 5);
assert.ok((stats.Flechettes?.damage || 0) >= 10);
}); });
test("simulateCombat: damagePerDiscardedThisTurn and bonusHitsWhenOtherHandAtLeast work", () => { test("simulateCombat: damagePerDiscardedThisTurn and bonusHitsWhenOtherHandAtLeast work", () => {
@@ -1099,6 +1102,90 @@ test("simulateCombat: blockPerDamageDealtThisTurn grants block from damage dealt
assert.equal(r.win, true); assert.equal(r.win, true);
}); });
test("simulateCombat: damageFromCurrentBlock uses current block as attack damage", () => {
const data = {
cards: {
Guard: { name: "Guard", cost: 1, kind: "Skill", block: 5 },
BodySlam: { name: "BodySlam", cost: 1, kind: "Attack", damageFromCurrentBlock: 1 },
},
starterDeck: ["Guard", "BodySlam"],
monsters: [{ name: "Dummy", maxHp: 5, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.turns, 1);
});
test("simulateCombat: damagePerOwnedNameMatch counts matching owned cards across combat piles", () => {
const data = {
cards: {
Strike1: { name: "타격", cost: 99, kind: "Attack", damage: 0 },
Strike2: { name: "타격", cost: 99, kind: "Attack", damage: 0 },
Perfected: { name: "완벽한 타격", cost: 0, kind: "Attack", damage: 6, damageNameMatch: "타격", damagePerOwnedNameMatch: 2 },
},
starterDeck: ["Strike1", "Strike2", "Perfected"],
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.win, true);
assert.ok((stats.Perfected?.damage || 0) >= 12);
});
test("simulateCombat: exhaustHandNonAttack exhausts only non-attacks and grants block per exhausted card", () => {
const data = {
cards: {
SecondWind: { name: "기사회생", cost: 0, kind: "Skill", exhaustHandNonAttack: true, blockPerExhaustedCard: 5 },
Guard1: { name: "수비1", cost: 99, kind: "Skill", block: 0 },
Guard2: { name: "수비2", cost: 99, kind: "Skill", block: 0 },
Hit: { name: "타격", cost: 99, kind: "Attack", damage: 1 },
},
starterDeck: ["Guard1", "Guard2", "Hit", "SecondWind"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 10 }] }],
};
const stats = {};
simulateCombat(data, () => 0.999999, stats);
assert.ok((stats.SecondWind?.block || 0) >= 10);
});
test("simulateCombat: drawOnExhaust draws when cards are exhausted", () => {
const data = {
cards: {
Embrace: { name: "어둠의 포옹", cost: 0, kind: "Power", drawOnExhaust: 1 },
Burn: { name: "소각", cost: 0, kind: "Skill", exhaust: true },
Hit: { name: "타격", cost: 0, kind: "Attack", damage: 6 },
},
starterDeck: ["Embrace", "Burn", "Hit"],
monsters: [{ name: "Dummy", maxHp: 6, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.turns, 1);
});
test("simulateCombat: keepBlock power preserves block across turns", () => {
const data = {
cards: {
Barricade: { name: "바리케이드", cost: 0, kind: "Power", powerEffect: "keepBlock", value: 0 },
Guard: { name: "수비", cost: 0, kind: "Skill", block: 5 },
Pass: { name: "대기", cost: 99, kind: "Skill", block: 0 },
},
starterDeck: ["Barricade", "Guard", "Pass"],
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 3 }, { kind: "Attack", value: 3 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.draw, true);
assert.equal(r.playerHpRemaining, 80);
});
test("chooseAction: damageFromCurrentBlock values attack using current block", () => {
const cards = {
Guard: { name: "Guard", cost: 1, kind: "Skill", block: 5 },
BodySlam: { name: "BodySlam", cost: 1, kind: "Attack", damageFromCurrentBlock: 1 },
};
assert.equal(chooseAction(["BodySlam", "Guard"], cards, 1, { currentBlock: 6 }), 0);
});
test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play", () => { test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play", () => {
const data = { const data = {
cards: { cards: {
@@ -1124,6 +1211,95 @@ test("simulateCombat: rewardOnKill grants an extra reward screen when an attack
assert.equal(r.bonusRewardScreens, 1); assert.equal(r.bonusRewardScreens, 1);
}); });
test("simulateCombat: maxHpOnKill increases max hp and heals when attack kills", () => {
const data = {
cards: {
Feed: { name: "포식", cost: 1, kind: "Attack", damage: 10, maxHpOnKill: 3 },
},
starterDeck: ["Feed"],
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
playerHp: 50,
playerMaxHp: 80,
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.playerHpRemaining, 53);
});
test("simulateCombat: drawNameMatchAutoPlay auto-plays matching drawn cards", () => {
const data = {
cards: {
Hellraiser: { name: "지옥검무", cost: 0, kind: "Power", drawNameMatchAutoPlay: "타격" },
Strike: { name: "강타격", cost: 99, kind: "Attack", damage: 9 },
Pass1: { name: "대기1", cost: 99, kind: "Skill" },
Pass2: { name: "대기2", cost: 99, kind: "Skill" },
Pass3: { name: "대기3", cost: 99, kind: "Skill" },
Pass4: { name: "대기4", cost: 99, kind: "Skill" },
},
starterDeck: ["Hellraiser", "Pass1", "Pass2", "Pass3", "Pass4", "Strike"],
monsters: [{ name: "Dummy", maxHp: 9, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: addRandomCardCount can add same-class attack as zero-cost this turn", () => {
const data = {
cards: {
InfernalBlade: {
name: "지옥검",
cost: 0,
kind: "Skill",
addRandomCardCount: 1,
addRandomCardKind: "Attack",
addRandomCardSameClass: true,
addedCardsCostZeroThisTurn: true,
class: "warrior",
},
BigHit: { name: "큰 일격", cost: 2, kind: "Attack", damage: 12, class: "warrior" },
OffClass: { name: "외부 공격", cost: 0, kind: "Attack", damage: 1, class: "rogue" },
},
starterDeck: ["InfernalBlade"],
monsters: [{ name: "Dummy", maxHp: 12, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: drawPerExhausted draws for each exhausted card", () => {
const data = {
cards: {
Stoke: { name: "화력 증폭", cost: 0, kind: "Skill", exhaustHandAll: true, drawPerExhausted: 1 },
Filler1: { name: "채우기1", cost: 99, kind: "Skill" },
Filler2: { name: "채우기2", cost: 99, kind: "Skill" },
Hit: { name: "일격", cost: 0, kind: "Attack", damage: 8 },
},
starterDeck: ["Stoke", "Filler1", "Filler2", "Hit"],
monsters: [{ name: "Dummy", maxHp: 8, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: playTopDrawPileCountPerEnergy auto-plays top draw pile cards", () => {
const data = {
cards: {
Cascade: { name: "연쇄", cost: 0, kind: "Skill", useAllEnergy: true, playTopDrawPileCountPerEnergy: 1, innate: true },
Filler1: { name: "준비1", cost: 99, kind: "Skill", innate: true },
Filler2: { name: "준비2", cost: 99, kind: "Skill", innate: true },
Filler3: { name: "준비3", cost: 99, kind: "Skill", innate: true },
Filler4: { name: "준비4", cost: 99, kind: "Skill", innate: true },
Hit1: { name: "타격1", cost: 99, kind: "Attack", damage: 6 },
Hit2: { name: "타격2", cost: 99, kind: "Attack", damage: 6 },
Hit3: { name: "타격3", cost: 99, kind: "Attack", damage: 6 },
},
starterDeck: ["Cascade", "Filler1", "Filler2", "Filler3", "Filler4", "Hit1", "Hit2", "Hit3"],
monsters: [{ name: "Dummy", maxHp: 18, intents: [{ kind: "Attack", value: 0 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
});
test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => { test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => {
const data = { const data = {
cards: { cards: {
@@ -1215,3 +1391,102 @@ test("simulateCombat: shivAoe makes Shivs hit all enemies", () => {
assert.equal(r.win, true); assert.equal(r.win, true);
assert.equal(r.turns, 1); assert.equal(r.turns, 1);
}); });
test("simulateCombat: comboGain and damagePerCombo scale repeated attacks", () => {
const data = {
cards: {
Brandish: { name: "브랜디쉬", cost: 1, kind: "Attack", damage: 2, hits: 2, comboGain: 1, damagePerCombo: 1 },
},
starterDeck: ["Brandish"],
monsters: [{ name: "Dummy", maxHp: 14, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.win, true);
assert.ok(stats.Brandish.damage > stats.Brandish.plays * 4);
});
test("simulateCombat: comboMax power raises the combo cap", () => {
const data = {
cards: {
ComboSynergy: { name: "콤보 시너지", cost: 0, kind: "Power", comboMax: 8, comboOnAttack: 2, attackDamagePerCombo: 1, innate: true },
Hit: { name: "연속 베기", cost: 0, kind: "Attack", damage: 1 },
},
starterDeck: ["ComboSynergy", "Hit"],
monsters: [{ name: "Dummy", maxHp: 40, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.win, true);
assert.ok(stats.Hit.damage > stats.Hit.plays);
});
test("simulateCombat: healPerTurn power restores hp at turn start", () => {
const data = {
cards: {
Recovery: { name: "셀프 리커버리", cost: 0, kind: "Power", powerEffect: "healPerTurn", value: 3 },
Hit: { name: "마무리", cost: 1, kind: "Attack", damage: 1 },
},
starterDeck: ["Recovery", "Hit"],
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 2 }] }],
playerHp: 70,
playerMaxHp: 80,
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.ok(r.playerHpRemaining >= 69);
});
test("simulateCombat: Holy Charge scales repeated Holy Force attacks", () => {
const data = {
cards: {
ChargeStrike: { name: "차지 타격", cost: 1, kind: "Attack", damage: 2, holyChargeGain: 1, damagePerHolyCharge: 1 },
},
starterDeck: ["ChargeStrike"],
monsters: [{ name: "Dummy", maxHp: 14, intents: [{ kind: "Attack", value: 0 }] }],
};
const stats = {};
const r = simulateCombat(data, () => 0.999999, stats);
assert.equal(r.win, true);
assert.ok(stats.ChargeStrike.damage > stats.ChargeStrike.plays * 2);
});
test("simulateCombat: damageTakenReduction lowers incoming HP damage", () => {
const data = {
cards: {
Achilles: { name: "아킬레스", cost: 3, kind: "Power", damageTakenReduction: 0.25, innate: true },
Wait1: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait2: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait3: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait4: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Finish: { name: "마무리", cost: 3, kind: "Power", cardPlayedDamage: 2 },
},
starterDeck: ["Finish", "Achilles", "Wait1", "Wait2", "Wait3", "Wait4"],
monsters: [{ name: "Dummy", maxHp: 2, intents: [{ kind: "Attack", value: 10 }] }],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.playerHpRemaining, 73);
});
test("simulateCombat: blockOnDamaged protects against later attackers", () => {
const data = {
cards: {
Armor: { name: "블레싱 아머", cost: 3, kind: "Power", blockOnDamaged: 6, strengthOnDamagedOnce: 2, innate: true },
Wait1: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait2: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait3: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Wait4: { name: "대기", cost: 99, kind: "Status", unplayable: true, innate: true },
Finish1: { name: "마무리", cost: 3, kind: "Power", cardPlayedDamage: 1 },
Finish2: { name: "마무리", cost: 3, kind: "Power", cardPlayedDamage: 1 },
},
starterDeck: ["Finish1", "Finish2", "Armor", "Wait1", "Wait2", "Wait3", "Wait4"],
monsters: [
{ name: "A", maxHp: 1, intents: [{ kind: "Attack", value: 5 }] },
{ name: "B", maxHp: 1, intents: [{ kind: "Attack", value: 5 }] },
],
};
const r = simulateCombat(data, () => 0.999999);
assert.equal(r.win, true);
assert.equal(r.playerHpRemaining, 70);
});

View File

@@ -77,4 +77,7 @@ end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
], 2), ], 2),
method('IntStr', `return string.format("%d", math.floor((n or 0) + 0.00001))`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'n' },
], 0, 'string'),
]; ];

View File

@@ -38,7 +38,7 @@ if c == nil then
return return
end end
if c.unplayable == true then if c.unplayable == true then
self:Toast("사용할 수 없는 카드입니다") self:Toast("사용할 수 없는 카드입니다")
return return
end end
if self:CanPlayCardNow(c) ~= true then if self:CanPlayCardNow(c) ~= true then
@@ -49,6 +49,8 @@ local skillFree = false
local skillRepeat = 0 local skillRepeat = 0
if self.HandCostZeroThisTurn == true then if self.HandCostZeroThisTurn == true then
cost = 0 cost = 0
elseif self.ZeroCostCardIdsThisTurn ~= nil and self.ZeroCostCardIdsThisTurn[cardId] == true then
cost = 0
elseif c.useAllEnergy == true then elseif c.useAllEnergy == true then
cost = self.Energy cost = self.Energy
end end
@@ -71,6 +73,8 @@ if self.Energy < cost then
end end
self.Energy = self.Energy - cost self.Energy = self.Energy - cost
self.ActiveKillReward = c.rewardOnKill or 0 self.ActiveKillReward = c.rewardOnKill or 0
self.ActiveKillMaxHpGain = c.maxHpOnKill or 0
table.remove(self.Hand, slot)
self:ResolveCardEffects(cardId, slot, c, false, cost) self:ResolveCardEffects(cardId, slot, c, false, cost)
local function applyCardPlayHooks() local function applyCardPlayHooks()
if self:HasPowerField("cardPlayedBlock") == true then if self:HasPowerField("cardPlayedBlock") == true then
@@ -82,6 +86,8 @@ local function applyCardPlayHooks()
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage) self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
end end
self:ApplyAttackCardPlayHooks(c)
self:ApplyHolyForceCardPlayHooks(c)
end end
applyCardPlayHooks() applyCardPlayHooks()
if skillRepeat > 0 then if skillRepeat > 0 then
@@ -106,16 +112,19 @@ end
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
self.ActiveKillReward = 0 self.ActiveKillReward = 0
end end
if self.ActiveKillMaxHpGain ~= nil and self.ActiveKillMaxHpGain <= 0 then
self.ActiveKillMaxHpGain = 0
end
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
if self.CombatCardCostReduction == nil then if self.CombatCardCostReduction == nil then
self.CombatCardCostReduction = {} self.CombatCardCostReduction = {}
end end
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
end end
table.remove(self.Hand, slot)
if c.exhaust == true then if c.exhaust == true then
if self.ExhaustPile == nil then self.ExhaustPile = {} end if self.ExhaustPile == nil then self.ExhaustPile = {} end
table.insert(self.ExhaustPile, cardId) table.insert(self.ExhaustPile, cardId)
self:TriggerExhaustEffects(1)
elseif c.kind ~= "Power" then elseif c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId) table.insert(self.DiscardPile, cardId)
end end
@@ -163,8 +172,8 @@ for i = 1, #self.Monsters do
local m = self.Monsters[i] local m = self.Monsters[i]
local active = false local active = false
if m ~= nil and m.alive == true and i == shownTarget then active = true end if m ~= nil and m.alive == true and i == shownTarget then active = true end
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker", active and dragActive) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i) .. "/TargetMarker", active and dragActive)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) .. "/TargetMarker/Label", active and dragActive) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i) .. "/TargetMarker/Label", active and dragActive)
end`), end`),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return return
@@ -177,7 +186,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
self.CardHoverTweenId = 0 self.CardHoverTweenId = 0
end end
for i = 1, 10 do for i = 1, 10 do
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i)) local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(i))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.UIScale = Vector3(1, 1, 1)
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0) e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
@@ -189,7 +198,7 @@ self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection
method('OnCardDrag', `if self.DragSlot ~= slot then method('OnCardDrag', `if self.DragSlot ~= slot then
return return
end end
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
local ui = _UILogic:ScreenToUIPosition(touchPoint) local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360) e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
@@ -211,7 +220,7 @@ end`, [
return return
end end
self.DragSlot = 0 self.DragSlot = 0
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0) e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.UIScale = Vector3(1, 1, 1)
@@ -288,6 +297,7 @@ if m.block > 0 and pierce ~= true then
m.block = m.block - absorbed m.block = m.block - absorbed
dmg = dmg - absorbed dmg = dmg - absorbed
end end
self:ShakeCombatCamera()
m.hp = m.hp - dmg m.hp = m.hp - dmg
if dmg > 0 then if dmg > 0 then
local poison = self:AddPowerFieldTotal("attackPoison") local poison = self:AddPowerFieldTotal("attackPoison")
@@ -301,6 +311,13 @@ local killed = false
if m.hp <= 0 then if m.hp <= 0 then
m.hp = 0 m.hp = 0
self:KillMonster(m.slot) self:KillMonster(m.slot)
if self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + self.ActiveKillReward
end
if self.ActiveKillMaxHpGain ~= nil and self.ActiveKillMaxHpGain > 0 then
self.PlayerMaxHp = self.PlayerMaxHp + self.ActiveKillMaxHpGain
self.PlayerHp = self.PlayerHp + self.ActiveKillMaxHpGain
end
killed = true killed = true
end end
return killed`, [ return killed`, [
@@ -317,6 +334,7 @@ end
if m == nil then if m == nil then
return false return false
end end
self:ShakeCombatCamera()
m.hp = m.hp - amount m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount) self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot) self:MonsterHitMotion(m.slot)
@@ -343,6 +361,7 @@ local m = alive[math.random(1, #alive)]
if m == nil then if m == nil then
return false return false
end end
self:ShakeCombatCamera()
m.hp = m.hp - amount m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount) self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot) self:MonsterHitMotion(m.slot)
@@ -368,7 +387,7 @@ local burstEvery = self:AddPowerFieldTotal("poisonApplicationBurstEvery")
local burstDamage = self:AddPowerFieldTotal("poisonApplicationBurstDamage") local burstDamage = self:AddPowerFieldTotal("poisonApplicationBurstDamage")
if burstEvery ~= nil and burstEvery > 0 and burstDamage ~= nil and burstDamage > 0 then if burstEvery ~= nil and burstEvery > 0 and burstDamage ~= nil and burstDamage > 0 then
if (self.PoisonApplicationsThisCombat % burstEvery) == 0 then if (self.PoisonApplicationsThisCombat % burstEvery) == 0 then
self:DealDamageToAllMonsters(burstDamage) self:DealDamageToAllMonsters(burstDamage, false)
end end
end`, [ end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'target' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'target' },
@@ -377,6 +396,17 @@ end`, [
method('DealDamageToAllMonsters', `if self.Monsters == nil then method('DealDamageToAllMonsters', `if self.Monsters == nil then
return false return false
end end
local anyAlive = false
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
anyAlive = true
break
end
end
if anyAlive == true and isAttack == true then
self:ShakeCombatCamera()
end
local killCount = 0 local killCount = 0
for i = 1, #self.Monsters do for i = 1, #self.Monsters do
local m = self.Monsters[i] local m = self.Monsters[i]
@@ -412,6 +442,11 @@ end
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward) self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
end end
if killCount > 0 and self.ActiveKillMaxHpGain ~= nil and self.ActiveKillMaxHpGain > 0 then
local gain = killCount * self.ActiveKillMaxHpGain
self.PlayerMaxHp = self.PlayerMaxHp + gain
self.PlayerHp = self.PlayerHp + gain
end
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd() self:CheckCombatEnd()
return killCount > 0`, [ return killCount > 0`, [
@@ -420,6 +455,13 @@ return killCount > 0`, [
], 0, 'boolean'), ], 0, 'boolean'),
method('PlayAttackFx', `local m = self.Monsters[targetIndex] method('PlayAttackFx', `local m = self.Monsters[targetIndex]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
local shown = damage
if m ~= nil and m.alive == true and m.vuln > 0 then
shown = math.floor(damage * 1.5)
end
if m ~= nil and m.alive == true and m.weak > 0 and self.ActiveAttackDamageVsWeakMultiplier ~= nil and self.ActiveAttackDamageVsWeakMultiplier > 1 then
shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier)
end
self:DealDamageToTarget(damage, pierce) self:DealDamageToTarget(damage, pierce)
self.ActiveAttackDamageVsWeakMultiplier = 1 self.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat() self:RenderCombat()
@@ -451,12 +493,9 @@ _TimerService:SetTimerOnce(function()
shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier) shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier)
end end
local killed = self:DealDamageToTarget(damage, pierce) local killed = self:DealDamageToTarget(damage, pierce)
if killed == true and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + self.ActiveKillReward
end
self.ActiveKillReward = 0 self.ActiveKillReward = 0
self.ActiveKillMaxHpGain = 0
self.ActiveAttackDamageVsWeakMultiplier = 1 self.ActiveAttackDamageVsWeakMultiplier = 1
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd() self:CheckCombatEnd()
end, 0.35)`, [ end, 0.35)`, [
@@ -511,10 +550,8 @@ _TimerService:SetTimerOnce(function()
end end
end end
end end
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
end
self.ActiveKillReward = 0 self.ActiveKillReward = 0
self.ActiveKillMaxHpGain = 0
self.ActiveAttackDamageVsWeakMultiplier = 1 self.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd() self:CheckCombatEnd()
@@ -531,11 +568,16 @@ if m.entity ~= nil and isvalid(m.entity) then
local ent = m.entity local ent = m.entity
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4) _TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
end end
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot), false) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(slot), false)
for i = 1, #self.Monsters do for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then self.TargetIndex = i; break end if self.Monsters[i].alive == true then self.TargetIndex = i; break end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('DealDamageToPlayer', `local dmg = amount method('DealDamageToPlayer', `local dmg = amount
local reduction = self:AddPowerFieldTotal("damageTakenReduction")
if reduction ~= nil and reduction > 0 then
reduction = math.min(0.75, reduction)
dmg = math.floor(dmg * (1 - reduction))
end
if self.PlayerBlock > 0 then if self.PlayerBlock > 0 then
local absorbed = math.min(self.PlayerBlock, dmg) local absorbed = math.min(self.PlayerBlock, dmg)
self.PlayerBlock = self.PlayerBlock - absorbed self.PlayerBlock = self.PlayerBlock - absorbed
@@ -544,8 +586,20 @@ end
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
dmg = 1 dmg = 1
end end
self:ShakeCombatCamera()
if dmg > 0 then if dmg > 0 then
self.PlayerHp = self.PlayerHp - dmg self.PlayerHp = self.PlayerHp - dmg
local reactiveBlock = self:AddPowerFieldTotal("blockOnDamaged")
if reactiveBlock ~= nil and reactiveBlock > 0 then
self:AddCardBlock(reactiveBlock)
end
if self.DamagePowerStrengthUsed ~= true then
local reactiveStrength = self:AddPowerFieldTotal("strengthOnDamagedOnce")
if reactiveStrength ~= nil and reactiveStrength > 0 then
self.PlayerStr = self.PlayerStr + reactiveStrength
self.DamagePowerStrengthUsed = true
end
end
local reflect = self.PlayerThorns or 0 local reflect = self.PlayerThorns or 0
if self:HasRelic("bronzeScales") then if self:HasRelic("bronzeScales") then
reflect = reflect + 3 reflect = reflect + 3
@@ -567,7 +621,7 @@ if dmg > 0 then
end end
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
self.FirstHpLossDone = true self.FirstHpLossDone = true
self:DrawCards(3) self:DrawCards(3, true)
self:RenderHand(false) self:RenderHand(false)
end end
end end
@@ -588,7 +642,7 @@ if idx == 0 or self.PlayerHp <= 0 then
return return
end end
local m = self.Monsters[idx] local m = self.Monsters[idx]
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(idx) local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(idx)
self:SetEntityEnabled(base .. "/ActFrame", true) self:SetEntityEnabled(base .. "/ActFrame", true)
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
local poisonTicks = 1 local poisonTicks = 1

View File

@@ -68,23 +68,68 @@ if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:
allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler) allDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)
self.AllDeckCloseHandler = nil self.AllDeckCloseHandler = nil
end end
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end) self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
end end
self:BindClassDeckTabs() self:BindClassDeckTabs()
local allDeckGrid = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Grid")
if allDeckGrid ~= nil and (allDeckGrid.UITouchReceiveComponent ~= nil or allDeckGrid:AddComponent("UITouchReceiveComponent") ~= nil) then
allDeckGrid:ConnectEvent(UITouchEnterEvent, function(ev) self.GridScrollActive = false; self:HoverGridCard("/ui/DeckUIGroup/DeckAllHud/Grid", ev.TouchPoint) end)
allDeckGrid:ConnectEvent(UITouchDownEvent, function(ev) self.GridScrollActive = false; self:HoverGridCard("/ui/DeckUIGroup/DeckAllHud/Grid", ev.TouchPoint) end)
allDeckGrid:ConnectEvent(UITouchDragEvent, function(ev) self.GridScrollActive = true; self:UnhoverGridCard("/ui/DeckUIGroup/DeckAllHud/Grid") end)
allDeckGrid:ConnectEvent(UITouchEndDragEvent, function() self.GridScrollActive = false end)
allDeckGrid:ConnectEvent(UITouchExitEvent, function() self.GridScrollActive = false; self:UnhoverGridCard("/ui/DeckUIGroup/DeckAllHud/Grid") end)
end
for i = 1, 120 do for i = 1, 120 do
local allCard = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(i)) local allCard = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. self:IntStr(i))
if allCard ~= nil and (allCard.ButtonComponent ~= nil or allCard:AddComponent("ButtonComponent") ~= nil) then if allCard ~= nil and (allCard.ButtonComponent ~= nil or allCard:AddComponent("ButtonComponent") ~= nil) then
if allCard.SpriteGUIRendererComponent ~= nil then if allCard.SpriteGUIRendererComponent ~= nil then
allCard.SpriteGUIRendererComponent.RaycastTarget = true allCard.SpriteGUIRendererComponent.RaycastTarget = true
end end
local slot = i local slot = i
local cardPath = "/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. self:IntStr(i)
allCard:ConnectEvent(ButtonStateChangeEvent, function(ev)
local state = nil
if ev ~= nil then
state = ev.CurrentState or ev.ButtonState or ev.State
end
if state == ButtonState.Highlighted or state == ButtonState.Pressed or tostring(state) == "1" or tostring(state) == "2" then
self:HoverCardByPath(cardPath)
else
self:UnhoverCardByPath(cardPath)
end
end)
allCard:ConnectEvent(ButtonClickEvent, function() self:OnAllDeckCardButton(slot) end) allCard:ConnectEvent(ButtonClickEvent, function() self:OnAllDeckCardButton(slot) end)
end end
end end
local inspectGrid = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud/Grid")
if inspectGrid ~= nil and (inspectGrid.UITouchReceiveComponent ~= nil or inspectGrid:AddComponent("UITouchReceiveComponent") ~= nil) then
inspectGrid:ConnectEvent(UITouchEnterEvent, function(ev) self.GridScrollActive = false; self:HoverGridCard("/ui/DeckUIGroup/DeckInspectHud/Grid", ev.TouchPoint) end)
inspectGrid:ConnectEvent(UITouchDownEvent, function(ev) self.GridScrollActive = false; self:HoverGridCard("/ui/DeckUIGroup/DeckInspectHud/Grid", ev.TouchPoint) end)
inspectGrid:ConnectEvent(UITouchDragEvent, function(ev) self.GridScrollActive = true; self:UnhoverGridCard("/ui/DeckUIGroup/DeckInspectHud/Grid") end)
inspectGrid:ConnectEvent(UITouchEndDragEvent, function() self.GridScrollActive = false end)
inspectGrid:ConnectEvent(UITouchExitEvent, function() self.GridScrollActive = false; self:UnhoverGridCard("/ui/DeckUIGroup/DeckInspectHud/Grid") end)
end
for i = 1, 60 do
local inspectCard = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. self:IntStr(i))
if inspectCard ~= nil and (inspectCard.ButtonComponent ~= nil or inspectCard:AddComponent("ButtonComponent") ~= nil) then
local cardPath = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. self:IntStr(i)
inspectCard:ConnectEvent(ButtonStateChangeEvent, function(ev)
local state = nil
if ev ~= nil then
state = ev.CurrentState or ev.ButtonState or ev.State
end
if state == ButtonState.Highlighted or state == ButtonState.Pressed or tostring(state) == "1" or tostring(state) == "2" then
self:HoverCardByPath(cardPath)
else
self:UnhoverCardByPath(cardPath)
end
end)
end
end
for i = 1, 10 do for i = 1, 10 do
local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i)) local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(i))
if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then if cardEntity ~= nil and cardEntity.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/CardHand/Card" .. tostring(i) local cardPath = "/ui/RunUIGroup/CardHand/Card" .. self:IntStr(i)
cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) cardEntity:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) cardEntity:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end) cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
@@ -98,13 +143,15 @@ for i = 1, 10 do
end end
end end
for i = 1, 3 do for i = 1, 3 do
local rc = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Reward" .. tostring(i)) local rc = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud/Reward" .. self:IntStr(i))
if rc ~= nil and (rc.ButtonComponent ~= nil or rc:AddComponent("ButtonComponent") ~= nil) then if rc ~= nil and (rc.ButtonComponent ~= nil or rc:AddComponent("ButtonComponent") ~= nil) then
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end) rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
if rc.UITouchReceiveComponent ~= nil then if rc.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/RewardHud/Reward" .. tostring(i) local cardPath = "/ui/RunUIGroup/RewardHud/Reward" .. self:IntStr(i)
rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) rc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) rc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
rc:ConnectEvent(UITouchEnterEvent, function() self:HoverCardByPath(cardPath) end)
rc:ConnectEvent(UITouchExitEvent, function() self:UnhoverCardByPath(cardPath) end)
end end
end end
end end
@@ -127,13 +174,15 @@ for i = 1, #mapNodeIds do
end end
end end
for i = 1, 3 do for i = 1, 3 do
local sc = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Card" .. tostring(i)) local sc = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Card" .. self:IntStr(i))
if sc ~= nil and (sc.ButtonComponent ~= nil or sc:AddComponent("ButtonComponent") ~= nil) then if sc ~= nil and (sc.ButtonComponent ~= nil or sc:AddComponent("ButtonComponent") ~= nil) then
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end) sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
if sc.UITouchReceiveComponent ~= nil then if sc.UITouchReceiveComponent ~= nil then
local cardPath = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i) local cardPath = "/ui/RunUIGroup/ShopHud/Card" .. self:IntStr(i)
sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end) sc:ConnectEvent(UITouchEnterEvent, function() self:SetCardHover(cardPath, true) end)
sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end) sc:ConnectEvent(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
sc:ConnectEvent(UITouchEnterEvent, function() self:HoverCardByPath(cardPath) end)
sc:ConnectEvent(UITouchExitEvent, function() self:UnhoverCardByPath(cardPath) end)
end end
end end
end end
@@ -150,13 +199,13 @@ if restLeave ~= nil and (restLeave.ButtonComponent ~= nil or restLeave:AddCompon
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end end
for i = 1, ${MAX_MONSTERS} do for i = 1, ${MAX_MONSTERS} do
local ms = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i)) local ms = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i))
if ms ~= nil and (ms.ButtonComponent ~= nil or ms:AddComponent("ButtonComponent") ~= nil) then if ms ~= nil and (ms.ButtonComponent ~= nil or ms:AddComponent("ButtonComponent") ~= nil) then
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end) ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
end end
end end
for i = 1, 10 do for i = 1, 10 do
local rs = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i)) local rs = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. self:IntStr(i))
if rs ~= nil and rs.UITouchReceiveComponent ~= nil then if rs ~= nil and rs.UITouchReceiveComponent ~= nil then
local idx = i local idx = i
rs:ConnectEvent(UITouchEnterEvent, function() rs:ConnectEvent(UITouchEnterEvent, function()
@@ -170,7 +219,7 @@ for i = 1, 10 do
end end
end end
for i = 1, 5 do for i = 1, 5 do
local ps = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i)) local ps = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. self:IntStr(i))
if ps ~= nil and ps.UITouchReceiveComponent ~= nil then if ps ~= nil and ps.UITouchReceiveComponent ~= nil then
local idx = i local idx = i
ps:ConnectEvent(UITouchEnterEvent, function() ps:ConnectEvent(UITouchEnterEvent, function()
@@ -218,7 +267,7 @@ if jcJob ~= nil and (jcJob.ButtonComponent ~= nil or jcJob:AddComponent("ButtonC
end end
for i = 1, 3 do for i = 1, 3 do
local slotIdx = i local slotIdx = i
local jb = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i)) local jb = _EntityService:GetEntityByPath("/ui/SelectUIGroup/JobSelectHud/Job_slot" .. self:IntStr(i))
if jb ~= nil and (jb.ButtonComponent ~= nil or jb:AddComponent("ButtonComponent") ~= nil) then if jb ~= nil and (jb.ButtonComponent ~= nil or jb:AddComponent("ButtonComponent") ~= nil) then
jb:ConnectEvent(ButtonClickEvent, function() jb:ConnectEvent(ButtonClickEvent, function()
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
@@ -243,6 +292,7 @@ self.BlockGainMultiplier = 1
self:ApplyRelics("turnStart") self:ApplyRelics("turnStart")
if self.NextTurnKeepBlock == true then if self.NextTurnKeepBlock == true then
self.NextTurnKeepBlock = false self.NextTurnKeepBlock = false
elseif self:HasPowerEffect("keepBlock") == true then
else else
self.PlayerBlock = 0 self.PlayerBlock = 0
end end
@@ -258,6 +308,7 @@ self.ActiveAttackDamageVsWeakMultiplier = 1
self.DrawDamageThisTurn = 0 self.DrawDamageThisTurn = 0
self.DrawPoisonThisTurn = 0 self.DrawPoisonThisTurn = 0
self.ShivAoeThisCombat = false self.ShivAoeThisCombat = false
self.ZeroCostCardIdsThisTurn = {}
self.SkillSlyOnPlayCards = self.SkillSlyOnPlayCards or {} self.SkillSlyOnPlayCards = self.SkillSlyOnPlayCards or {}
self.TurnSkillSlyCards = {} self.TurnSkillSlyCards = {}
self.EnemyStrengthLossThisTurn = 0 self.EnemyStrengthLossThisTurn = 0
@@ -275,6 +326,7 @@ if self.PlayerPowers ~= nil then
self.Energy = self.Energy + pc.value self.Energy = self.Energy + pc.value
elseif pc.powerEffect == "blockPerTurn" then elseif pc.powerEffect == "blockPerTurn" then
self.PlayerBlock = self.PlayerBlock + pc.value self.PlayerBlock = self.PlayerBlock + pc.value
elseif pc.powerEffect == "keepBlock" then
elseif pc.powerEffect == "poisonPerTurn" then elseif pc.powerEffect == "poisonPerTurn" then
if self.Monsters ~= nil then if self.Monsters ~= nil then
for j = 1, #self.Monsters do for j = 1, #self.Monsters do
@@ -288,6 +340,8 @@ if self.PlayerPowers ~= nil then
if self.Monsters ~= nil then if self.Monsters ~= nil then
self:PlayAoeFx(pc.fx or pc.image, pc.value or 0) self:PlayAoeFx(pc.fx or pc.image, pc.value or 0)
end end
elseif pc.powerEffect == "healPerTurn" then
self.PlayerHp = math.min(self.PlayerMaxHp, self.PlayerHp + pc.value)
end end
if pc.turnStartShiv ~= nil then if pc.turnStartShiv ~= nil then
self:AddCardsToHand("Shiv", pc.turnStartShiv) self:AddCardsToHand("Shiv", pc.turnStartShiv)
@@ -316,7 +370,7 @@ if self.NextTurnAddCards ~= nil then
end end
local drawN = 5 + (self.NextTurnDraw or 0) + powerTurnDraw local drawN = 5 + (self.NextTurnDraw or 0) + powerTurnDraw
self.NextTurnDraw = 0 self.NextTurnDraw = 0
self:DrawCards(drawN) self:DrawCards(drawN, true)
self:RenderHand(true) self:RenderHand(true)
self:RenderCombat() self:RenderCombat()
if powerTurnDiscard > 0 then if powerTurnDiscard > 0 then
@@ -481,8 +535,11 @@ for i = 1, amount do
\t\tself:TriggerSly(cardId) \t\tself:TriggerSly(cardId)
\telse \telse
\t\ttable.insert(self.Hand, cardId) \t\ttable.insert(self.Hand, cardId)
\t\tdrewAny = true \t\tlocal autoPlayed = self:TriggerDrawnCardAutoPlay(cardId)
\t\ttable.insert(drawnSlots, #self.Hand) \t\tif autoPlayed ~= true then
\t\t\tdrewAny = true
\t\t\ttable.insert(drawnSlots, #self.Hand)
\t\tend
\tend \tend
end end
self:RenderPiles() self:RenderPiles()
@@ -495,8 +552,9 @@ if animate == true and #drawnSlots > 0 then
\t\tlocal slot = drawnSlots[i] \t\tlocal slot = drawnSlots[i]
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045) \t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
\tend \tend
end
return drawnCards return drawnCards
end`, [ `, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
], 0, 'any'), ], 0, 'any'),

View File

@@ -44,7 +44,7 @@ end
self:SetText("/ui/DeckUIGroup/DeckInspectHud/Title", title .. suffix) self:SetText("/ui/DeckUIGroup/DeckInspectHud/Title", title .. suffix)
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud/Empty", count <= 0) self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud/Empty", count <= 0)
for i = 1, 60 do for i = 1, 60 do
local path = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(i) local path = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. self:IntStr(i)
local cardId = nil local cardId = nil
if pile ~= nil then if pile ~= nil then
cardId = pile[i] cardId = pile[i]
@@ -59,7 +59,7 @@ end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
]), ]),
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [ method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckInspectHud/Grid/Card" .. self:IntStr(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]), ]),
@@ -181,7 +181,6 @@ end
self.DeckInspectKind = "" self.DeckInspectKind = ""
self.ClassDeckMode = false self.ClassDeckMode = false
self.ClassDeckClass = "" self.ClassDeckClass = ""
self.CodexMode = false
self.DebugCardPickerMode = false self.DebugCardPickerMode = false
self:RenderClassDeckTabs() self:RenderClassDeckTabs()
self.DeckAllOpen = true self.DeckAllOpen = true
@@ -212,9 +211,6 @@ local title = "모든 덱"
if self.ClassDeckMode == true then if self.ClassDeckMode == true then
pile = self.ClassDeckCards or {} pile = self.ClassDeckCards or {}
title = self.ClassDeckTitle title = self.ClassDeckTitle
if self.CodexMode == true then
title = "카드 도감"
end
if self.DebugCardPickerMode == true then if self.DebugCardPickerMode == true then
title = title .. " - 테스트 카드 추가" title = title .. " - 테스트 카드 추가"
end end
@@ -227,7 +223,7 @@ self:SetText("/ui/DeckUIGroup/DeckAllHud/Title", title .. " (" .. tostring(count
self:RenderClassDeckTabs() self:RenderClassDeckTabs()
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0) self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0)
for i = 1, 120 do for i = 1, 120 do
local path = "/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(i) local path = "/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. self:IntStr(i)
local cardId = pile[i] local cardId = pile[i]
if cardId == nil then if cardId == nil then
self:SetEntityEnabled(path, false) self:SetEntityEnabled(path, false)
@@ -236,7 +232,7 @@ for i = 1, 120 do
self:ApplyAllDeckCardVisual(i, cardId) self:ApplyAllDeckCardVisual(i, cardId)
end end
end`), end`),
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [ method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DeckUIGroup/DeckAllHud/Grid/Card" .. self:IntStr(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]), ]),

View File

@@ -56,7 +56,7 @@ if n > 8 then spacing = math.floor(1400 / n) end
local startX = -((n - 1) * spacing) / 2 local startX = -((n - 1) * spacing) / 2
local drawStart = Vector2(-590, 8) local drawStart = Vector2(-590, 8)
for i = 1, 10 do for i = 1, 10 do
\tlocal cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(i)) \tlocal cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(i))
\tif cardEntity ~= nil then \tif cardEntity ~= nil then
\t\tlocal cardId = self.Hand[i] \t\tlocal cardId = self.Hand[i]
\t\tif cardId == nil then \t\tif cardId == nil then
@@ -151,7 +151,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
end end
local items = {} local items = {}
for i = 1, count do for i = 1, count do
local e = _EntityService:GetEntityByPath(prefix .. tostring(i)) local e = _EntityService:GetEntityByPath(prefix .. self:IntStr(i))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
local tr = e.UITransformComponent local tr = e.UITransformComponent
local tx = xs[i] local tx = xs[i]
@@ -195,7 +195,7 @@ self.CardHoverTweenId = eventId`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hover' },
]), ]),
method('ApplyCardVisual', `self:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. tostring(slot), cardId)`, [ method('ApplyCardVisual', `self:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]), ]),
@@ -217,7 +217,7 @@ if math.abs(n - math.floor(n)) < 0.00001 then
return string.format("%d", math.floor(n)) return string.format("%d", math.floor(n))
end end
return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'), return tostring(n)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }], 0, 'string'),
method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if cardEntity == nil or cardEntity.UITransformComponent == nil then if cardEntity == nil or cardEntity.UITransformComponent == nil then
\treturn \treturn
end end
@@ -246,10 +246,10 @@ local target = Vector2(590, 8)
local duration = 0.18 local duration = 0.18
for i = 1, #cardIds do for i = 1, #cardIds do
\tlocal slot = slots[i] or i \tlocal slot = slots[i] or i
\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) \tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
\tif e ~= nil then \tif e ~= nil then
\t\te.Enable = true \t\te.Enable = true
\t\tself:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. tostring(slot), cardIds[i]) \t\tself:ApplyCardFace("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot), cardIds[i])
\t\tif e.UITransformComponent ~= nil then \t\tif e.UITransformComponent ~= nil then
\t\t\tlocal sx = 0 \t\t\tlocal sx = 0
\t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end \t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end
@@ -266,7 +266,7 @@ eventId = _TimerService:SetTimerRepeat(function()
\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseIn, t) \tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseIn, t)
\tfor i = 1, #cardIds do \tfor i = 1, #cardIds do
\t\tlocal slot = slots[i] or i \t\tlocal slot = slots[i] or i
\t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) \t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
\t\tif e ~= nil and e.UITransformComponent ~= nil then \t\tif e ~= nil and e.UITransformComponent ~= nil then
\t\t\tlocal sx = 0 \t\t\tlocal sx = 0
\t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end \t\t\tif startXs ~= nil and startXs[i] ~= nil then sx = startXs[i] else sx = self:GetHandSlotX(slot) end
@@ -281,7 +281,7 @@ eventId = _TimerService:SetTimerRepeat(function()
\t\t_TimerService:ClearTimer(eventId) \t\t_TimerService:ClearTimer(eventId)
\t\tfor i = 1, #cardIds do \t\tfor i = 1, #cardIds do
\t\t\tlocal slot = slots[i] or i \t\t\tlocal slot = slots[i] or i
\t\t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) \t\t\tlocal e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
\t\t\tif e ~= nil then \t\t\tif e ~= nil then
\t\t\t\tif self.Hand ~= nil and self.Hand[slot] ~= nil then \t\t\t\tif self.Hand ~= nil and self.Hand[slot] ~= nil then
\t\t\t\t\te.Enable = true \t\t\t\t\te.Enable = true
@@ -313,6 +313,108 @@ if amount < 0 then
end end
self.PlayerBlock = self.PlayerBlock + amount self.PlayerBlock = self.PlayerBlock + amount
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
method('RunAttackSequence', `if hitDamages == nil or #hitDamages <= 0 then
return
end
local function countAliveMonsters()
local n = 0
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
n = n + 1
end
end
end
return n
end
local function randomAliveMonsterIndex()
local alive = {}
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
table.insert(alive, mi)
end
end
end
if #alive <= 0 then
return 0
end
return alive[math.random(1, #alive)]
end
self.FxBusy = true
local hitIndex = 1
local lockedTargetIndex = self.TargetIndex or 0
local function finishSequence()
self.FxBusy = false
self.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat()
self:CheckCombatEnd()
end
local function step()
if self.CombatOver == true then
finishSequence()
return
end
if countAliveMonsters() <= 0 then
finishSequence()
return
end
if hitIndex == 1 then
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + (roundPlannedDamage or 0)
end
local roundKilled = false
if useAoe == true then
roundKilled = self:DealDamageToAllMonsters(hitDamages[hitIndex] or 0, true)
else
local targetIdx = lockedTargetIndex
if randomTargetEachHit == true then
targetIdx = randomAliveMonsterIndex()
end
if targetIdx ~= nil and targetIdx > 0 then
if randomTargetEachHit ~= true then
local lockedMonster = nil
if self.Monsters ~= nil then
lockedMonster = self.Monsters[targetIdx]
end
if lockedMonster == nil or lockedMonster.alive ~= true then
finishSequence()
return
end
end
local prev = self.TargetIndex
self.TargetIndex = targetIdx
roundKilled = self:DealDamageToTarget(hitDamages[hitIndex] or 0, pierce == true)
self.TargetIndex = prev
end
end
self:RenderCombat()
self:CheckCombatEnd()
hitIndex = hitIndex + 1
if self.CombatOver == true then
finishSequence()
return
end
if hitIndex <= #hitDamages then
_TimerService:SetTimerOnce(step, 0.2)
return
end
if repeatOnKill == true and roundKilled == true and countAliveMonsters() > 0 then
hitIndex = 1
_TimerService:SetTimerOnce(step, 0.2)
return
end
finishSequence()
end
step()`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hitDamages' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'roundPlannedDamage' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'useAoe' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'randomTargetEachHit' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'repeatOnKill' },
]),
method('CountOtherHandSkills', `if self.Hand == nil then method('CountOtherHandSkills', `if self.Hand == nil then
return 0 return 0
end end
@@ -326,7 +428,104 @@ for i = 1, #self.Hand do
end end
end end
return n`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'), return n`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }], 0, 'number'),
method('CountOwnedNameMatches', `if match == nil or match == "" then
return 0
end
local n = 0
local function countPile(pile)
if pile == nil then return end
for i = 1, #pile do
local c2 = self.Cards[pile[i]]
local name = ""
if c2 ~= nil and c2.name ~= nil then name = c2.name end
if string.find(name, match, 1, true) ~= nil then
n = n + 1
end
end
end
countPile(self.Hand)
countPile(self.DrawPile)
countPile(self.DiscardPile)
countPile(self.ExhaustPile)
return n`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'match' }], 0, 'number'),
method('MaxPowerField', `local best = 0
if self.PlayerPowers == nil then
return best
end
for i = 1, #self.PlayerPowers do
local powerCard = self.Cards[self.PlayerPowers[i]]
if powerCard ~= nil and powerCard[field] ~= nil and powerCard[field] > best then
best = powerCard[field]
end
end
return best`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'field' }], 0, 'number'),
method('GetComboMax', `local comboMax = 5
local powerMax = self:MaxPowerField("comboMax")
if powerMax ~= nil and powerMax > comboMax then
comboMax = powerMax
end
return comboMax`, [], 0, 'number'),
method('GainCombo', `if amount == nil or amount <= 0 then
return
end
self.ComboCount = math.min(self:GetComboMax(), (self.ComboCount or 0) + amount)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('GetHolyChargeMax', `local chargeMax = 3
local powerMax = self:MaxPowerField("holyChargeMax")
if powerMax ~= nil and powerMax > chargeMax then
chargeMax = powerMax
end
return chargeMax`, [], 0, 'number'),
method('GainHolyCharge', `if amount == nil or amount <= 0 then
return
end
self.HolyChargeCount = math.min(self:GetHolyChargeMax(), (self.HolyChargeCount or 0) + amount)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('ApplyHolyForceCardPlayHooks', `if c == nil then
return
end
local gain = c.holyChargeGain or 0
if c.holyForce == true then
gain = gain + self:AddPowerFieldTotal("holyChargeOnHolyForce")
end
self:GainHolyCharge(gain)
if c.holyChargeSpendAll == true then
self.HolyChargeCount = 0
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]),
method('ApplyAttackCardPlayHooks', `if c == nil or c.kind ~= "Attack" then
return
end
local comboGain = c.comboGain or 0
comboGain = comboGain + self:AddPowerFieldTotal("comboOnAttack")
self:GainCombo(comboGain)
local extraDamage = self:AddPowerFieldTotal("attackPlayedDamage")
if extraDamage ~= nil and extraDamage > 0 then
self:DealDirectDamageToTarget(extraDamage)
end
local weakAmount = self:AddPowerFieldTotal("attackWeak")
if weakAmount ~= nil and weakAmount > 0 and self.Monsters ~= nil then
local target = self.Monsters[self.TargetIndex]
if target ~= nil and target.alive == true then
if target.artifact ~= nil and target.artifact > 0 then
target.artifact = target.artifact - 1
else
target.weak = (target.weak or 0) + weakAmount
end
end
end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }]),
method('AttackBaseForCard', `local base2 = c.damage or 0 method('AttackBaseForCard', `local base2 = c.damage or 0
if c.damageNameMatch ~= nil and c.damagePerOwnedNameMatch ~= nil then
base2 = base2 + self:CountOwnedNameMatches(c.damageNameMatch) * c.damagePerOwnedNameMatch
end
if c.damageFromCurrentBlock ~= nil and c.damageFromCurrentBlock ~= 0 then
base2 = base2 + (self.PlayerBlock or 0) * c.damageFromCurrentBlock
end
local comboScale = c.damagePerCombo or 0
comboScale = comboScale + self:AddPowerFieldTotal("attackDamagePerCombo")
if comboScale ~= 0 then
base2 = base2 + (self.ComboCount or 0) * comboScale
end
if c.damagePerHolyCharge ~= nil and c.damagePerHolyCharge ~= 0 then
base2 = base2 + (self.HolyChargeCount or 0) * c.damagePerHolyCharge
end
local otherHand = 0 local otherHand = 0
if self.Hand ~= nil then if self.Hand ~= nil then
otherHand = #self.Hand - 1 otherHand = #self.Hand - 1
@@ -365,6 +564,200 @@ return base2`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
], 0, 'number'), ], 0, 'number'),
method('TriggerExhaustEffects', `if count == nil or count <= 0 then
return
end
local drawOnExhaust = self:AddPowerFieldTotal("drawOnExhaust")
if drawOnExhaust ~= nil and drawOnExhaust > 0 then
self:DrawCards(drawOnExhaust * count, true)
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'count' }]),
method('MarkCardCostZeroThisTurn', `if cardId == nil or cardId == "" then
return
end
if self.ZeroCostCardIdsThisTurn == nil then
self.ZeroCostCardIdsThisTurn = {}
end
self.ZeroCostCardIdsThisTurn[cardId] = true`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }]),
method('AutoPlayCardId', `local c = self.Cards[cardId]
if c == nil then
return false
end
local spent = energySpent or 0
local skillFree = false
local skillRepeat = 0
if c.kind == "Skill" and c.useAllEnergy ~= true and self.NextSkillCostZero == true then
skillFree = true
end
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
skillRepeat = self.NextSkillRepeatCount
end
self.ActiveKillReward = c.rewardOnKill or 0
self.ActiveKillMaxHpGain = c.maxHpOnKill or 0
self:ResolveCardEffects(cardId, 0, c, false, spent)
local function applyCardPlayHooks()
if self:HasPowerField("cardPlayedBlock") == true then
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
end
if c.cardPlayedDamage ~= nil and c.cardPlayedDamage > 0 then
self:DealDirectDamageToTarget(c.cardPlayedDamage)
end
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
end
self:ApplyAttackCardPlayHooks(c)
self:ApplyHolyForceCardPlayHooks(c)
end
applyCardPlayHooks()
if skillRepeat > 0 then
local remaining = (self.NextSkillRepeatCount or 0) - skillRepeat
if remaining < 0 then remaining = 0 end
self.NextSkillRepeatCount = remaining
for i = 1, skillRepeat do
self:ResolveCardEffects(cardId, 0, c, false, spent)
applyCardPlayHooks()
end
end
if c.kind == "Attack" then
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
end
if skillFree == true and c.nextSkillCostZero ~= true then
self.NextSkillCostZero = false
end
if c.exhaust == true then
if self.ExhaustPile == nil then self.ExhaustPile = {} end
table.insert(self.ExhaustPile, cardId)
self:TriggerExhaustEffects(1)
elseif c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId)
end
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
self.ActiveKillReward = 0
end
if self.ActiveKillMaxHpGain ~= nil and self.ActiveKillMaxHpGain <= 0 then
self.ActiveKillMaxHpGain = 0
end
return true`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'energySpent' },
], 0, 'boolean'),
method('TriggerDrawnCardAutoPlay', `if cardId == nil or cardId == "" or self.Hand == nil or self.PlayerPowers == nil then
return false
end
local c = self.Cards[cardId]
if c == nil or c.name == nil or c.name == "" then
return false
end
for i = 1, #self.PlayerPowers do
local powerCard = self.Cards[self.PlayerPowers[i]]
if powerCard ~= nil and powerCard.drawNameMatchAutoPlay ~= nil and powerCard.drawNameMatchAutoPlay ~= "" then
if string.find(c.name, powerCard.drawNameMatchAutoPlay, 1, true) ~= nil then
local foundSlot = 0
for hi = 1, #self.Hand do
if self.Hand[hi] == cardId then
foundSlot = hi
break
end
end
if foundSlot <= 0 then
return false
end
table.remove(self.Hand, foundSlot)
self:AutoPlayCardId(cardId, 0)
return true
end
end
end
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }], 0, 'boolean'),
method('PlayTopDrawPileCards', `if c == nil or self.DrawPile == nil then
return 0
end
local count = c.playTopDrawPileCount or 0
if c.playTopDrawPileCountPerEnergy ~= nil and c.playTopDrawPileCountPerEnergy > 0 then
count = count + ((energySpent or 0) * c.playTopDrawPileCountPerEnergy)
end
if count <= 0 then
return 0
end
local played = 0
for i = 1, count do
if #self.DrawPile <= 0 then
break
end
local topCardId = table.remove(self.DrawPile)
if topCardId ~= nil then
self:AutoPlayCardId(topCardId, 0)
played = played + 1
end
end
return played`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'energySpent' },
], 0, 'number'),
method('AddRandomCardsFromEffect', `if c == nil or count == nil or count <= 0 then
return 0
end
local pool = {}
for id, rc in pairs(self.Cards) do
if rc ~= nil and rc.token ~= true and rc.curse ~= true and rc.unplayable ~= true then
local ok = true
if c.addRandomCardKind ~= nil and rc.kind ~= c.addRandomCardKind then ok = false end
if c.addRandomCardSameClass == true and rc.class ~= c.class then ok = false end
if ok == true then table.insert(pool, id) end
end
end
if #pool <= 0 then
return 0
end
local added = 0
for i = 1, count do
local cardId2 = pool[math.random(1, #pool)]
if cardId2 ~= nil then
self:AddCardsToHand(cardId2, 1)
if c.addedCardsCostZeroThisTurn == true then
self:MarkCardCostZeroThisTurn(cardId2)
end
added = added + 1
end
end
return added`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'count' },
], 0, 'number'),
method('ExhaustHandNonAttack', `if c == nil or c.exhaustHandNonAttack ~= true or self.Hand == nil or #self.Hand <= 0 then
return 0
end
local exhausted = 0
for i = #self.Hand, 1, -1 do
local cardId2 = self.Hand[i]
local hc = self.Cards[cardId2]
if hc == nil or hc.kind ~= "Attack" then
table.remove(self.Hand, i)
if self.ExhaustPile == nil then self.ExhaustPile = {} end
table.insert(self.ExhaustPile, cardId2)
exhausted = exhausted + 1
end
end
if exhausted > 0 then
if c.blockPerExhaustedCard ~= nil and c.blockPerExhaustedCard > 0 then
self:AddCardBlock(exhausted * c.blockPerExhaustedCard)
end
self:TriggerExhaustEffects(exhausted)
end
return exhausted`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'number'),
method('ExhaustHandAll', `if c == nil or c.exhaustHandAll ~= true or self.Hand == nil or #self.Hand <= 0 then
return 0
end
local exhausted = 0
while #self.Hand > 0 do
local cardId2 = table.remove(self.Hand)
if self.ExhaustPile == nil then self.ExhaustPile = {} end
table.insert(self.ExhaustPile, cardId2)
exhausted = exhausted + 1
end
if exhausted > 0 then
self:TriggerExhaustEffects(exhausted)
end
return exhausted`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'number'),
method('CalcPlayerAttack', `local base2 = base method('CalcPlayerAttack', `local base2 = base
self.FightAttackCount = self.FightAttackCount + 1 self.FightAttackCount = self.FightAttackCount + 1
if self.FightAttackCount == 1 and self:HasRelic("akabeko") then if self.FightAttackCount == 1 and self:HasRelic("akabeko") then
@@ -485,14 +878,15 @@ if c.xWeakPerEnergy ~= nil and c.xWeakPerEnergy > 0 then
weakAmount = weakAmount + xEnergy * c.xWeakPerEnergy weakAmount = weakAmount + xEnergy * c.xWeakPerEnergy
end end
if c.kind == "Attack" then if c.kind == "Attack" then
if c.damage ~= nil or c.xDamagePerEnergy ~= nil then if c.damage ~= nil or c.xDamagePerEnergy ~= nil or c.damageFromCurrentBlock ~= nil then
self:PlayerAttackMotion() self:PlayerAttackMotion()
local baseDmg = self:AttackBaseForCard(slot, c) local baseDmg = self:AttackBaseForCard(slot, c)
self.ActiveAttackDamageVsWeakMultiplier = c.attackDamageVsWeakMultiplier or 1 self.ActiveAttackDamageVsWeakMultiplier = math.max(c.attackDamageVsWeakMultiplier or 1, self:MaxPowerField("attackDamageVsWeakMultiplier"))
if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then if c.xDamagePerEnergy ~= nil and c.xDamagePerEnergy > 0 then
baseDmg = xEnergy * c.xDamagePerEnergy baseDmg = xEnergy * c.xDamagePerEnergy
end end
local total = 0 local total = 0
local hitDamages = {}
local hitN = c.hits or 1 local hitN = c.hits or 1
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
local otherHand = 0 local otherHand = 0
@@ -505,7 +899,9 @@ if c.kind == "Attack" then
end end
end end
for h = 1, hitN do for h = 1, hitN do
total = total + self:CalcPlayerAttack(baseDmg) local hitDamage = self:CalcPlayerAttack(baseDmg)
hitDamages[h] = hitDamage
total = total + hitDamage
end end
local useAoe = c.aoe == true local useAoe = c.aoe == true
if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then if c.class == "shiv" and (self.ShivAoeThisCombat == true or self:HasPowerField("shivAoe") == true) then
@@ -514,70 +910,17 @@ if c.kind == "Attack" then
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
self.ShivFirstDamageBonusUsed = true self.ShivFirstDamageBonusUsed = true
end end
local function countAliveMonsters() self:RunAttackSequence(hitDamages, total, useAoe, c.randomTargetEachHit == true, c.pierce == true, c.repeatOnKill == true)
local n = 0
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then n = n + 1 end
end
end
return n
end
local function randomAliveMonsterIndex()
local alive = {}
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
table.insert(alive, mi)
end
end
end
if #alive <= 0 then
return 0
end
return alive[math.random(1, #alive)]
end
local function resolveAttackRound()
local roundKilled = false
if useAoe == true then
local killed = self:DealDamageToAllMonsters(total, true)
if killed == true then roundKilled = true end
elseif c.randomTargetEachHit == true then
for h = 1, hitN do
local targetIdx = randomAliveMonsterIndex()
if targetIdx ~= nil and targetIdx > 0 then
local prev = self.TargetIndex
self.TargetIndex = targetIdx
local killed = self:DealDamageToTarget(math.floor(total / hitN), c.pierce == true)
self.TargetIndex = prev
if killed == true then roundKilled = true end
end
end
else
local killed = self:DealDamageToTarget(total, c.pierce == true)
if killed == true then roundKilled = true end
end
return roundKilled
end
local totalDamage = 0
local roundKilled = false
repeat
roundKilled = resolveAttackRound()
totalDamage = totalDamage + total
until c.repeatOnKill ~= true or roundKilled ~= true or countAliveMonsters() <= 0
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + totalDamage
end end
if c.block ~= nil then if c.block ~= nil then
self:AddCardBlock(c.block) self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0))
end end
if free ~= true then if free ~= true then
self:ApplyRelics("cardPlayed") self:ApplyRelics("cardPlayed")
end end
elseif c.kind == "Skill" then elseif c.kind == "Skill" then
if c.block ~= nil then if c.block ~= nil then
self:AddCardBlock(c.block) self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0))
end end
elseif c.kind == "Power" then elseif c.kind == "Power" then
if free ~= true then if free ~= true then
@@ -597,11 +940,19 @@ if c.selfVuln ~= nil then
self.PlayerVuln = self.PlayerVuln + c.selfVuln self.PlayerVuln = self.PlayerVuln + c.selfVuln
end end
if c.heal ~= nil then if c.heal ~= nil then
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp) local healAmount = c.heal + (self.HolyChargeCount or 0) * (c.healPerHolyCharge or 0)
self.PlayerHp = math.min(self.PlayerHp + healAmount, self.PlayerMaxHp)
end end
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
self.Energy = self.Energy + c.gainEnergy self.Energy = self.Energy + c.gainEnergy
end end
if c.kind ~= "Attack" and c.comboGain ~= nil and c.comboGain > 0 then
self:GainCombo(c.comboGain)
end
if c.removePlayerDebuffs == true then
self.PlayerWeak = 0
self.PlayerVuln = 0
end
if c.intangible ~= nil and c.intangible > 0 then if c.intangible ~= nil and c.intangible > 0 then
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
end end
@@ -719,6 +1070,23 @@ if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
end end
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
self:AddCardsToHand("Shiv", c.addShiv) self:AddCardsToHand("Shiv", c.addShiv)
end
local exhaustedNonAttack = self:ExhaustHandNonAttack(c)
local exhaustedAll = self:ExhaustHandAll(c)
local totalExhausted = exhaustedNonAttack + exhaustedAll
if c.drawPerExhausted ~= nil and c.drawPerExhausted > 0 and totalExhausted > 0 then
self:DrawCards(totalExhausted * c.drawPerExhausted, true)
end
if c.addRandomCardCount ~= nil and c.addRandomCardCount > 0 then
self:AddRandomCardsFromEffect(c, c.addRandomCardCount)
end
if c.addRandomCardPerExhausted ~= nil and c.addRandomCardPerExhausted > 0 then
if totalExhausted > 0 then
self:AddRandomCardsFromEffect(c, totalExhausted * c.addRandomCardPerExhausted)
end
end
if (c.playTopDrawPileCount ~= nil and c.playTopDrawPileCount > 0) or (c.playTopDrawPileCountPerEnergy ~= nil and c.playTopDrawPileCountPerEnergy > 0) then
self:PlayTopDrawPileCards(c, xEnergy)
end`, [ end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },

View File

@@ -25,7 +25,7 @@ for i = 1, #self.RunRelics do
elseif r.effect == "strength" then elseif r.effect == "strength" then
self.PlayerStr = self.PlayerStr + r.value self.PlayerStr = self.PlayerStr + r.value
elseif r.effect == "draw" then elseif r.effect == "draw" then
self:DrawCards(r.value) self:DrawCards(r.value, true)
self:RenderHand(false) self:RenderHand(false)
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
self.PlayerHp = self.PlayerHp + r.value self.PlayerHp = self.PlayerHp + r.value
@@ -95,7 +95,7 @@ if self:AddPotion(pid) == true then
self:Toast("물약 획득: " .. p.name) self:Toast("물약 획득: " .. p.name)
end`), end`),
method('RenderPotions', `for i = 1, 5 do method('RenderPotions', `for i = 1, 5 do
local base = "/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. tostring(i) local base = "/ui/RunUIGroup/CombatHud/TopBar/PotionSlot" .. self:IntStr(i)
local e = _EntityService:GetEntityByPath(base) local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local pid = nil local pid = nil
@@ -188,7 +188,7 @@ if self.RunRelics ~= nil then
count = #self.RunRelics count = #self.RunRelics
end end
for i = 1, 10 do for i = 1, 10 do
local base = "/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. tostring(i) local base = "/ui/RunUIGroup/CombatHud/TopBar/RelicSlot" .. self:IntStr(i)
local e = _EntityService:GetEntityByPath(base) local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local rid = nil local rid = nil

View File

@@ -71,7 +71,7 @@ if opts == nil then
end end
self.JobOpts = opts self.JobOpts = opts
for i = 1, 3 do for i = 1, 3 do
local base = "/ui/SelectUIGroup/JobSelectHud/Job_slot" .. tostring(i) local base = "/ui/SelectUIGroup/JobSelectHud/Job_slot" .. self:IntStr(i)
local o = opts[i] local o = opts[i]
if o ~= nil then if o ~= nil then
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)

View File

@@ -14,7 +14,7 @@ end
local worldPos = transform.WorldPosition local worldPos = transform.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + ${HEAD_OFFSET_Y})) local screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + ${HEAD_OFFSET_Y}))
local uipos = _UILogic:ScreenToUIPosition(screen) local uipos = _UILogic:ScreenToUIPosition(screen)
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(slot)) local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(slot))
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
slotEntity.UITransformComponent.anchoredPosition = uipos slotEntity.UITransformComponent.anchoredPosition = uipos
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),

View File

@@ -162,7 +162,7 @@ if node ~= nil then
end end
end end
for k = 1, 3 do for k = 1, 3 do
local d = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k)) local d = _EntityService:GetEntityByPath("/ui/RunUIGroup/MapHud/Dot_" .. dotId .. "_" .. self:IntStr(k))
if d ~= nil then if d ~= nil then
d.Enable = has d.Enable = has
if has == true and d.SpriteGUIRendererComponent ~= nil then if has == true and d.SpriteGUIRendererComponent ~= nil then

View File

@@ -1,296 +1,467 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs'; import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs'; import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs'; import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const renderMethods = [ export const renderMethods = [
method('BuffsLabel', `local parts = {} method('BuffsLabel', `local parts = {}
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
return table.concat(parts, " ")`, [ return table.concat(parts, " ")`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
], 0, 'string'), ], 0, 'string'),
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. tostring(i) local base = "/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i)
local m = self.Monsters[i] local m = self.Monsters[i]
if m ~= nil and m.alive == true then if m ~= nil and m.alive == true then
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", m.name) self:SetText(base .. "/Name", m.name)
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp)) self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
local intent = m.intents[m.intentIdx] local intent = m.intents[m.intentIdx]
local t = "" local t = ""
if intent ~= nil then if intent ~= nil then
if intent.kind == "Attack" then if intent.kind == "Attack" then
local atk = intent.value + m.str local atk = intent.value + m.str
if m.weak > 0 then atk = math.floor(atk * 0.75) end if m.weak > 0 then atk = math.floor(atk * 0.75) end
if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end if self.PlayerVuln > 0 then atk = math.floor(atk * 1.5) end
t = "공격 " .. tostring(atk) t = "공격 " .. tostring(atk)
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value)
elseif intent.kind == "Debuff" then elseif intent.kind == "Debuff" then
if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여" if intent.effect == "weak" then t = "약화 " .. tostring(intent.value) .. " 부여"
else t = "취약 " .. tostring(intent.value) .. " 부여" end else t = "취약 " .. tostring(intent.value) .. " 부여" end
elseif intent.kind == "AddCard" then elseif intent.kind == "AddCard" then
t = "저주 카드 추가" t = "저주 카드 추가"
end end
end end
self:SetText(base .. "/Intent", t) self:SetText(base .. "/Intent", t)
local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0 local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end if dragActive == true then shownTarget = self.DragTargetIndex end
self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive) self:SetEntityEnabled(base .. "/TargetMarker", i == shownTarget and dragActive)
self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive) self:SetEntityEnabled(base .. "/TargetMarker/Label", i == shownTarget and dragActive)
local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent") local intentEntity = _EntityService:GetEntityByPath(base .. "/Intent")
if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then if intentEntity ~= nil and intentEntity.TextComponent ~= nil and intent ~= nil then
if intent.kind == "Attack" then if intent.kind == "Attack" then
intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1) intentEntity.TextComponent.FontColor = Color(1, 0.45, 0.35, 1)
elseif intent.kind == "Debuff" then elseif intent.kind == "Debuff" then
intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1) intentEntity.TextComponent.FontColor = Color(0.8, 0.5, 1, 1)
elseif intent.kind == "AddCard" then elseif intent.kind == "AddCard" then
intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1) intentEntity.TextComponent.FontColor = Color(0.6, 0.85, 0.4, 1)
else else
intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1) intentEntity.TextComponent.FontColor = Color(0.5, 0.75, 1, 1)
end end
end end
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W}) self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0) self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block)) self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0)) self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
else else
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
end end
end end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp)) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/HpText", string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220) self:SetHpBar("/ui/RunUIGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock)) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0) local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then if self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "불가침" .. tostring(self.PlayerIntangible) pb = pb .. "불가침" .. tostring(self.PlayerIntangible)
end end
if self.PlayerDex ~= nil and self.PlayerDex > 0 then if self.PlayerDex ~= nil and self.PlayerDex > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "민첩+" .. tostring(self.PlayerDex) pb = pb .. "민첩+" .. tostring(self.PlayerDex)
end end
if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
if pb ~= "" then pb = pb .. " " end if pb ~= "" then pb = pb .. " " end
pb = pb .. "가시" .. tostring(self.PlayerThorns) pb = pb .. "가시" .. tostring(self.PlayerThorns)
end end
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then if self.ComboCount ~= nil and self.ComboCount > 0 then
local names = {} if pb ~= "" then pb = pb .. " " end
for i = 1, #self.PlayerPowers do pb = pb .. "콤보 " .. tostring(self.ComboCount) .. "/" .. tostring(self:GetComboMax())
local pc = self.Cards[self.PlayerPowers[i]] end
if pc ~= nil then table.insert(names, pc.name) end if self.HolyChargeCount ~= nil and self.HolyChargeCount > 0 then
end if pb ~= "" then pb = pb .. " " end
if pb ~= "" then pb = pb .. " · " end pb = pb .. "홀리 차지 " .. tostring(self.HolyChargeCount) .. "/" .. tostring(self:GetHolyChargeMax())
pb = pb .. table.concat(names, " ") end
end if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb) local names = {}
self:RenderRun()`), for i = 1, #self.PlayerPowers do
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0)) local pc = self.Cards[self.PlayerPowers[i]]
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey if pc ~= nil then table.insert(names, pc.name) end
local pop = _EntityService:GetEntityByPath(base) end
if pop == nil then if pb ~= "" then pb = pb .. " · " end
return pb = pb .. table.concat(names, " ")
end end
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1 self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb)
local popSeq = self.DmgPopSeq self:RenderRun()`),
self:SetText(base, "") method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} } if self.DmgPopSlotQueue == nil then
local shown = tostring(math.max(0, math.floor(amount))) self.DmgPopSlotQueue = {}
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then end
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS}) local popIndex = (self.DmgPopSlotQueue[slotKey] or 0) + 1
end if popIndex > 5 then
local digits = {} popIndex = 1
for i = 1, string.len(shown) do end
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0) self.DmgPopSlotQueue[slotKey] = popIndex
end local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey .. "_" .. self:IntStr(popIndex)
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING} local pop = _EntityService:GetEntityByPath(base)
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2 if pop == nil then
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do return
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false) end
end local function showNow()
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do self:SetEntityEnabled(base, false)
local digitPath = base .. "/Digit" .. tostring(i) self:SetText(base, "")
local digitEntity = _EntityService:GetEntityByPath(digitPath) local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then local shown = tostring(math.max(0, math.floor(amount)))
if digits[i] ~= nil then if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1] shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1) end
if digitEntity.UITransformComponent ~= nil then local digits = {}
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0) local digitPathsToEnable = {}
end for i = 1, string.len(shown) do
self:SetEntityEnabled(digitPath, true) table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
else end
self:SetEntityEnabled(digitPath, false) local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
end local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
end for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
end self:SetEntityEnabled(base .. "/Digit" .. self:IntStr(i), false)
local popPos = nil end
local m = self.Monsters[slot] for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then local digitPath = base .. "/Digit" .. self:IntStr(i)
local wp = m.entity.TransformComponent.WorldPosition local digitEntity = _EntityService:GetEntityByPath(digitPath)
local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45})) if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
popPos = _UILogic:ScreenToUIPosition(screen) if digits[i] ~= nil then
else digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey) digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then if digitEntity.UITransformComponent ~= nil then
local sp = slotEntity.UITransformComponent.anchoredPosition digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
popPos = Vector2(sp.x, sp.y + 76) end
end table.insert(digitPathsToEnable, digitPath)
end else
if pop ~= nil and pop.UITransformComponent ~= nil then self:SetEntityEnabled(digitPath, false)
if popPos ~= nil then end
pop.UITransformComponent.anchoredPosition = popPos end
else end
pop.UITransformComponent.anchoredPosition = Vector2(0, 120) local popPos = nil
end local m = self.Monsters[slot]
end if m ~= nil and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
self:SetEntityEnabled(base, true) local wp = m.entity.TransformComponent.WorldPosition
for i = 1, 6 do local screen = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + ${HEAD_OFFSET_Y + 0.45}))
_TimerService:SetTimerOnce(function() popPos = _UILogic:ScreenToUIPosition(screen)
if self.DmgPopSeq ~= popSeq then else
return local slotEntity = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/MonsterStatus" .. slotKey)
end if slotEntity ~= nil and slotEntity.UITransformComponent ~= nil then
local p = _EntityService:GetEntityByPath(base) local sp = slotEntity.UITransformComponent.anchoredPosition
if p ~= nil and p.UITransformComponent ~= nil then popPos = Vector2(sp.x, sp.y + 76)
local cur = p.UITransformComponent.anchoredPosition end
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7) end
end if pop.UITransformComponent ~= nil then
end, 0.045 * i) if popPos ~= nil then
end pop.UITransformComponent.anchoredPosition = popPos
_TimerService:SetTimerOnce(function() else
if self.DmgPopSeq ~= popSeq then pop.UITransformComponent.anchoredPosition = Vector2(0, 120)
return end
end end
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, true)
end, 0.48)`, [ for i = 1, #digitPathsToEnable do
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, self:SetEntityEnabled(digitPathsToEnable[i], true)
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, end
]), for i = 1, 6 do
method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop" _TimerService:SetTimerOnce(function()
if amount > 0 then local p = _EntityService:GetEntityByPath(base)
self:SetText(base, "-" .. string.format("%d", amount)) if p ~= nil and p.UITransformComponent ~= nil then
else local cur = p.UITransformComponent.anchoredPosition
self:SetText(base, "막음") p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
end end
self:SetEntityEnabled(base, true) local alpha = 1 - (i / 6)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), if alpha < 0 then alpha = 0 end
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer for di = 1, ${DAMAGE_POP_MAX_DIGITS} do
if lp == nil then local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(di))
return if digitEntity ~= nil and digitEntity.Enable == true and digitEntity.SpriteGUIRendererComponent ~= nil then
end digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, alpha)
if lp.StateComponent == nil then end
return end
end end, 0.05 * i)
pcall(function() lp.StateComponent:ChangeState("ATTACK") end) end
_TimerService:SetTimerOnce(function() _TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then self:SetEntityEnabled(base, false)
pcall(function() lp.StateComponent:ChangeState("IDLE") end) end, 0.3)
end end
end, 0.5)`), showNow()`, [
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
if lp == nil then { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
return ]),
end method('ShowPlayerDmgPop', `local shownAmount = math.max(0, math.floor(amount or 0))
if lp.StateComponent ~= nil then if shownAmount <= 0 then
pcall(function() lp.StateComponent:ChangeState("HIT") end) return
end end
local tr = lp.TransformComponent local function setEnabled(entity, enabled)
if tr == nil then if entity == nil then
return return
end end
local p = tr.Position entity.Enable = enabled
tr.Position = Vector3(p.x - 0.15, p.y, p.z) if entity.SpriteGUIRendererComponent ~= nil then
_TimerService:SetTimerOnce(function() entity.SpriteGUIRendererComponent.Enable = enabled
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then end
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z) if entity.TextComponent ~= nil then
end entity.TextComponent.Enable = enabled
end, 0.15)`), end
method('MonsterLunge', `local m = self.Monsters[idx] end
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then self.PlayerDmgPopIndex = (self.PlayerDmgPopIndex or 0) + 1
return if self.PlayerDmgPopIndex > 4 then
end self.PlayerDmgPopIndex = 1
if m.motionBusy == true then end
return local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop_" .. self:IntStr(self.PlayerDmgPopIndex)
end local pop = _EntityService:GetEntityByPath(base)
m.motionBusy = true if pop == nil then
local e = m.entity return
local tr = e.TransformComponent end
if tr == nil then setEnabled(pop, false)
m.motionBusy = false local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
return local shown = tostring(shownAmount)
end if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
local p = tr.Position shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
tr.Position = Vector3(p.x - 0.35, p.y, p.z) end
_TimerService:SetTimerOnce(function() local digits = {}
if isvalid(e) and e.TransformComponent ~= nil then local digitEntitiesToEnable = {}
e.TransformComponent.Position = Vector3(p.x, p.y, p.z) for i = 1, string.len(shown) do
end table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
m.motionBusy = false end
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]), local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
method('MonsterHitMotion', `local m = self.Monsters[slot] local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
return local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(i))
end setEnabled(digitEntity, false)
local e = m.entity end
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
e.SpriteRendererComponent.SpriteRUID = m.hitClip local digitPath = base .. "/Digit" .. self:IntStr(i)
_TimerService:SetTimerOnce(function() local digitEntity = _EntityService:GetEntityByPath(digitPath)
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.standClip if digits[i] ~= nil then
end digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
end, 0.5) digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
else if digitEntity.UITransformComponent ~= nil then
if m.motionBusy == true then digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
return end
end table.insert(digitEntitiesToEnable, digitEntity)
m.motionBusy = true end
local tr = e.TransformComponent end
if tr == nil then end
m.motionBusy = false local startPos = Vector2(16, 34)
return if pop.UITransformComponent ~= nil then
end local cur = pop.UITransformComponent.anchoredPosition
local p = tr.Position if math.abs(cur.x) < 240 and math.abs(cur.y) < 140 then
local seq = { 0.12, -0.12, 0 } startPos = cur
for i = 1, #seq do end
local dx = seq[i] pop.UITransformComponent.anchoredPosition = startPos
_TimerService:SetTimerOnce(function() end
if isvalid(e) and e.TransformComponent ~= nil then setEnabled(pop, true)
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z) for i = 1, #digitEntitiesToEnable do
end setEnabled(digitEntitiesToEnable[i], true)
if i == #seq then end
m.motionBusy = false for i = 1, 6 do
end _TimerService:SetTimerOnce(function()
end, 0.06 * i) local p = _EntityService:GetEntityByPath(base)
end if p ~= nil and p.Enable == true and p.UITransformComponent ~= nil then
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), local cur = p.UITransformComponent.anchoredPosition
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path) p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
if e == nil or e.UITransformComponent == nil then end
return local alpha = 1 - (i / 6)
end if alpha < 0 then alpha = 0 end
local ratio = 0 for di = 1, ${DAMAGE_POP_MAX_DIGITS} do
if maxHp > 0 then ratio = hp / maxHp end local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(di))
if ratio < 0 then ratio = 0 end if digitEntity ~= nil and digitEntity.Enable == true and digitEntity.SpriteGUIRendererComponent ~= nil then
local w = width * ratio digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, alpha)
e.UITransformComponent.RectSize = Vector2(w, 14)`, [ end
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, end
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' }, end, 0.05 * i)
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' }, end
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' }, _TimerService:SetTimerOnce(function()
]), local p = _EntityService:GetEntityByPath(base)
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then if p ~= nil and p.UITransformComponent ~= nil then
self.TargetIndex = slot p.UITransformComponent.anchoredPosition = startPos
self:RenderCombat() end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), setEnabled(p, false)
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층" end, 0.3)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
if self.AscensionLevel > 0 then method('ShakeCombatCamera', `local lp = _UserService.LocalPlayer
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel) local cam = nil
end if lp ~= nil then
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText) cam = lp.CameraComponent
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`), end
if cam == nil then
cam = _CameraService:GetCurrentCameraComponent()
end
if cam == nil then
return
end
self.CombatCameraShakeSeq = (self.CombatCameraShakeSeq or 0) + 1
local seq = self.CombatCameraShakeSeq
local baseCameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
cam.CameraOffset = baseCameraOffset
local offsets = {
Vector2(-0.22, 0),
Vector2(0.2, 0),
Vector2(-0.12, 0),
Vector2(0.06, 0),
Vector2(0, 0),
}
for i = 1, #offsets do
local delta = offsets[i]
_TimerService:SetTimerOnce(function()
local lp2 = _UserService.LocalPlayer
local cam2 = nil
if lp2 ~= nil then
cam2 = lp2.CameraComponent
end
if cam2 == nil then
cam2 = _CameraService:GetCurrentCameraComponent()
end
if cam2 == nil then
return
end
if seq ~= self.CombatCameraShakeSeq then
return
end
cam2.CameraOffset = Vector2(baseCameraOffset.x + delta.x, baseCameraOffset.y + delta.y)
end, 0.028 * (i - 1))
end`, []),
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.StateComponent == nil then
return
end
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
end
end, 0.5)`),
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("HIT") end)
end
local tr = lp.TransformComponent
if tr == nil then
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
end, 0.15)`),
method('MonsterLunge', `local m = self.Monsters[idx]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
if m.motionBusy == true then
return
end
m.motionBusy = true
local e = m.entity
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
m.motionBusy = false
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
method('MonsterHitMotion', `local m = self.Monsters[slot]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
local e = m.entity
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.hitClip
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.standClip
end
end, 0.5)
else
if m.motionBusy == true then
return
end
m.motionBusy = true
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
local seq = { 0.12, -0.12, 0 }
for i = 1, #seq do
local dx = seq[i]
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
end
if i == #seq then
m.motionBusy = false
end
end, 0.06 * i)
end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
if e == nil or e.UITransformComponent == nil then
return
end
local ratio = 0
if maxHp > 0 then ratio = hp / maxHp end
if ratio < 0 then ratio = 0 end
if ratio > 1 then ratio = 1 end
if self.HpBarOrigins == nil then
self.HpBarOrigins = {}
end
local t = e.UITransformComponent
local origin = self.HpBarOrigins[path]
if origin == nil then
local pivotX = 0
if t.Pivot ~= nil then
pivotX = t.Pivot.x or 0
end
origin = {
left = t.anchoredPosition.x - (t.RectSize.x * pivotX),
y = t.anchoredPosition.y,
height = t.RectSize.y,
pivotX = pivotX,
}
self.HpBarOrigins[path] = origin
end
local w = width * ratio
t.RectSize = Vector2(w, origin.height or 14)
t.anchoredPosition = Vector2(origin.left + (w * (origin.pivotX or 0)), origin.y or t.anchoredPosition.y)`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'width' },
]),
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
self.TargetIndex = slot
self:RenderCombat()
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('RenderRun', `local floorText = "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층"
if self.AscensionLevel > 0 then
floorText = floorText .. " · 승천" .. string.format("%d", self.AscensionLevel)
end
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Floor", floorText)
self:SetText("/ui/RunUIGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`),
];

View File

@@ -34,7 +34,7 @@ local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
if hud ~= nil then if hud ~= nil then
hud.Enable = true hud.Enable = true
end`), end`),
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/RunUIGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [ method('ApplyRewardVisual', `self:ApplyCardFace("/ui/RunUIGroup/RewardHud/Reward" .. self:IntStr(slot), cardId)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]), ]),
@@ -47,7 +47,7 @@ if slot ~= 0 and self.RewardChoices ~= nil then
table.insert(self.RunDeck, id) table.insert(self.RunDeck, id)
end end
end end
if self.BonusRewardScreens ~= nil and self.BonusRewardScreens > 0 then if self.BonusRewardScreens ~= nil and self.BonusRewardScreens > 0 and slot ~= 0 then
self.BonusRewardScreens = self.BonusRewardScreens - 1 self.BonusRewardScreens = self.BonusRewardScreens - 1
self:OfferReward() self:OfferReward()
return return

View File

@@ -64,7 +64,7 @@ end, 0.2)`),
self:KickCombatCamera() self:KickCombatCamera()
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/Result", false) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/Result", false)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/PotionMenu", false)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false) self:SetEntityEnabled("/ui/ToolTipGroup/TooltipBox", false)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/DiscardPrompt", false) self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/DiscardPrompt", false)
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())
self.MaxEnergy = 3 self.MaxEnergy = 3
@@ -95,13 +95,20 @@ self.PlayerVuln = 0
self.PlayerIntangible = 0 self.PlayerIntangible = 0
self.BonusRewardScreens = 0 self.BonusRewardScreens = 0
self.ActiveKillReward = 0 self.ActiveKillReward = 0
self.ActiveKillMaxHpGain = 0
self.PlayerPowers = {} self.PlayerPowers = {}
self.FightAttackCount = 0 self.FightAttackCount = 0
self.TurnAttackCardsPlayed = 0 self.TurnAttackCardsPlayed = 0
self.TurnDiscardedCards = 0 self.TurnDiscardedCards = 0
self.TurnCardsPlayedThisTurn = 0 self.TurnCardsPlayedThisTurn = 0
self.ComboCount = 0
self.HolyChargeCount = 0
self.DamagePowerStrengthUsed = false
self.DamageDealtThisTurn = 0 self.DamageDealtThisTurn = 0
self.DmgPopSeq = 0 self.DmgPopSeq = 0
self.CombatCameraShakeSeq = 0
self.DmgPopSlotQueue = {}
self.PlayerDmgPopIndex = 0
self.FirstHpLossDone = false self.FirstHpLossDone = false
self.ClayBlockNext = 0 self.ClayBlockNext = 0
self.DiscardSelectRemaining = 0 self.DiscardSelectRemaining = 0
@@ -118,6 +125,7 @@ self.TurnAttackMultiplier = 1
self.NextTurnSelectPrompt = "" self.NextTurnSelectPrompt = ""
self.NextTurnSelectCopies = 0 self.NextTurnSelectCopies = 0
self.NextTurnAddCards = {} self.NextTurnAddCards = {}
self.ZeroCostCardIdsThisTurn = {}
self.CombatOver = false self.CombatOver = false
self.DiscardPile = {} self.DiscardPile = {}
self.ExhaustPile = {} self.ExhaustPile = {}

View File

@@ -49,9 +49,9 @@ end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], N
method('ShowMainMenu', `self.SelectedClass = "" method('ShowMainMenu', `self.SelectedClass = ""
self:RenderAscension() self:RenderAscension()
self:ShowState("menu") self:ShowState("menu")
self:SetText("/ui/DefaultGroup/MainMenu/Title", "메이플 덱 어드벤처") self:SetText("/ui/DefaultGroup/MainMenu/Title", "Maple Deck Adventure")
self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "캐릭터를 고르고 덱을 만들어 모험을 시작하세요") self:SetText("/ui/DefaultGroup/MainMenu/Subtitle", "Choose your character and begin your run")
self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "새 게임") self:SetText("/ui/DefaultGroup/MainMenu/NewGameButton", "New Game")
self:BindMenuButtons()`), self:BindMenuButtons()`),
method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton") method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton")
if buttonEntity ~= nil and (buttonEntity.ButtonComponent ~= nil or buttonEntity:AddComponent("ButtonComponent") ~= nil) then if buttonEntity ~= nil and (buttonEntity.ButtonComponent ~= nil or buttonEntity:AddComponent("ButtonComponent") ~= nil) then
@@ -136,22 +136,28 @@ self:BindLobbyButtons()
self:BindMenuButtons() self:BindMenuButtons()
self:GoLobbyMap()`), self:GoLobbyMap()`),
method('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0 method('RenderSoulLabel', `local soulPoints = self.SoulPoints or 0
self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "영혼 " .. string.format("%d", soulPoints)) self:SetText("/ui/LobbyUIGroup/LobbyHud/SoulLabel", "Soul " .. string.format("%d", soulPoints))
self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "영혼 " .. string.format("%d", soulPoints))`), self:SetText("/ui/LobbyUIGroup/SoulShopHud/Souls", "Soul " .. string.format("%d", soulPoints))`),
method('BindLobbyButtons', `if self.LobbyBound == true then method('BindLobbyButtons', `if self.LobbyBound == true then
return return
end end
self.LobbyBound = true self.LobbyBound = true
local function bindClick(path, handler) local ascMinus = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/LobbyHud/AscMinus")
local entity = _EntityService:GetEntityByPath(path) if ascMinus ~= nil and (ascMinus.ButtonComponent ~= nil or ascMinus:AddComponent("ButtonComponent") ~= nil) then
if entity ~= nil and (entity.ButtonComponent ~= nil or entity:AddComponent("ButtonComponent") ~= nil) then ascMinus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(-1) end)
entity:ConnectEvent(ButtonClickEvent, handler)
end
end end
bindClick("/ui/LobbyUIGroup/LobbyHud/AscMinus", function() self:AdjustAscension(-1) end) local ascPlus = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/LobbyHud/AscPlus")
bindClick("/ui/LobbyUIGroup/LobbyHud/AscPlus", function() self:AdjustAscension(1) end) if ascPlus ~= nil and (ascPlus.ButtonComponent ~= nil or ascPlus:AddComponent("ButtonComponent") ~= nil) then
bindClick("/ui/LobbyUIGroup/BoardHud/Close", function() self:CloseBoard() end) ascPlus:ConnectEvent(ButtonClickEvent, function() self:AdjustAscension(1) end)
bindClick("/ui/LobbyUIGroup/SoulShopHud/Close", function() self:CloseSoulShop() end)`), end
local boardClose = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/BoardHud/Close")
if boardClose ~= nil and (boardClose.ButtonComponent ~= nil or boardClose:AddComponent("ButtonComponent") ~= nil) then
boardClose:ConnectEvent(ButtonClickEvent, function() self:CloseBoard() end)
end
local soulClose = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/SoulShopHud/Close")
if soulClose ~= nil and (soulClose.ButtonComponent ~= nil or soulClose:AddComponent("ButtonComponent") ~= nil) then
soulClose:ConnectEvent(ButtonClickEvent, function() self:CloseSoulShop() end)
end`),
method('ShowCodex', `self.CodexMode = true method('ShowCodex', `self.CodexMode = true
self.ClassDeckMode = true self.ClassDeckMode = true
local close = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close") local close = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/Close")

View File

@@ -9,17 +9,7 @@ self.ShopBought = { false, false, false }
for i = 1, 3 do for i = 1, 3 do
self.ShopChoices[i] = pool[math.random(1, #pool)] self.ShopChoices[i] = pool[math.random(1, #pool)]
end end
local relicChoices = {} self.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
for i = 1, #self.RelicPool do
if self:HasRelic(self.RelicPool[i]) == false then
table.insert(relicChoices, self.RelicPool[i])
end
end
if #relicChoices > 0 then
self.ShopRelic = relicChoices[math.random(1, #relicChoices)]
else
self.ShopRelic = ""
end
self.ShopRelicBought = false self.ShopRelicBought = false
local pkeys = {} local pkeys = {}
for pid, _ in pairs(self.Potions) do for pid, _ in pairs(self.Potions) do
@@ -34,7 +24,7 @@ self:ShowState("shop")`),
for i = 1, 3 do for i = 1, 3 do
local cid = self.ShopChoices[i] local cid = self.ShopChoices[i]
local c = self.Cards[cid] local c = self.Cards[cid]
local base = "/ui/RunUIGroup/ShopHud/Card" .. tostring(i) local base = "/ui/RunUIGroup/ShopHud/Card" .. self:IntStr(i)
if c ~= nil then if c ~= nil then
self:ApplyCardFace(base, cid) self:ApplyCardFace(base, cid)
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소") self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
@@ -48,7 +38,6 @@ for i = 1, 3 do
end end
local rr = self.Relics[self.ShopRelic] local rr = self.Relics[self.ShopRelic]
if rr ~= nil then if rr ~= nil then
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud/Relic", true)
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc) self:SetText("/ui/RunUIGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
self:SetText("/ui/RunUIGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소") self:SetText("/ui/RunUIGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
local re = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic") local re = _EntityService:GetEntityByPath("/ui/RunUIGroup/ShopHud/Relic")
@@ -59,8 +48,6 @@ if rr ~= nil then
re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1) re.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)
end end
end end
else
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud/Relic", false)
end end
local pp = self.Potions[self.ShopPotion] local pp = self.Potions[self.ShopPotion]
if pp ~= nil then if pp ~= nil then
@@ -78,9 +65,6 @@ end`),
method('BuyRelic', `if self.ShopRelicBought == true then method('BuyRelic', `if self.ShopRelicBought == true then
return return
end end
if self.ShopRelic == nil or self.ShopRelic == "" then
return
end
if self.Gold < ${RELIC_PRICE} then if self.Gold < ${RELIC_PRICE} then
return return
end end

View File

@@ -41,43 +41,59 @@ if lp ~= nil then
end end
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]), self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]),
method('BuySoulUnlock', `local d = nil method('BuySoulUnlock', `local d = nil
if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end if self.SoulShopDef ~= nil then
if d == nil then return end d = self.SoulShopDef[slot]
if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then end
self:Toast("이미 보유 중입니다") if d == nil then
return return
end end
if (self.SoulPoints or 0) < d.cost then local unlockKey = d.key
self:Toast("영혼이 부족합니다") local unlockCost = d.cost
local unlockName = d.name or "Unlock"
if unlockKey == nil or unlockCost == nil then
return return
end end
self.SoulPoints = self.SoulPoints - d.cost if self.SoulUnlocks ~= nil and self.SoulUnlocks[unlockKey] == true then
if self.SoulUnlocks == nil then self.SoulUnlocks = {} end self:Toast("Already owned")
self.SoulUnlocks[d.key] = true return
end
if (self.SoulPoints or 0) < unlockCost then
self:Toast("Not enough soul")
return
end
self.SoulPoints = self.SoulPoints - unlockCost
if self.SoulUnlocks == nil then
self.SoulUnlocks = {}
end
self.SoulUnlocks[unlockKey] = true
local lp = _UserService.LocalPlayer local lp = _UserService.LocalPlayer
if lp ~= nil then if lp ~= nil then
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId) self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
end end
self:Toast(d.name .. " 해금!") self:Toast(unlockName .. " unlocked")
self:RenderSoulLabel() self:RenderSoulLabel()
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]), self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
method('RenderSoulShop', `local defs = self.SoulShopDef or {} method('RenderSoulShop', `local defs = self.SoulShopDef or {}
for i = 1, 4 do for i = 1, 4 do
local base = "/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i) local base = "/ui/LobbyUIGroup/SoulShopHud/Item" .. self:IntStr(i)
local d = defs[i] local d = defs[i]
if d == nil then if d == nil then
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
else else
local itemName = d.name or "Unlock"
local itemDesc = d.desc or ""
local itemKey = d.key
local itemCost = d.cost or 0
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", d.name) self:SetText(base .. "/Name", itemName)
self:SetText(base .. "/Desc", d.desc) self:SetText(base .. "/Desc", itemDesc)
local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true local owned = itemKey ~= nil and self.SoulUnlocks ~= nil and self.SoulUnlocks[itemKey] == true
if owned then if owned then
self:SetText(base .. "/Status", "보유 중") self:SetText(base .. "/Status", "Owned")
elseif (self.SoulPoints or 0) >= d.cost then elseif (self.SoulPoints or 0) >= itemCost then
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매") self:SetText(base .. "/Status", tostring(itemCost) .. " soul · buy")
else else
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족") self:SetText(base .. "/Status", tostring(itemCost) .. " soul · low")
end end
end end
end`), end`),
@@ -87,7 +103,7 @@ end
self.SoulShopBound = true self.SoulShopBound = true
for i = 1, 4 do for i = 1, 4 do
local idx = i local idx = i
local e = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/SoulShopHud/Item" .. tostring(i)) local e = _EntityService:GetEntityByPath("/ui/LobbyUIGroup/SoulShopHud/Item" .. self:IntStr(i))
if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then if e ~= nil and (e.ButtonComponent ~= nil or e:AddComponent("ButtonComponent") ~= nil) then
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end) e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
end end

View File

@@ -102,6 +102,217 @@ for i = 1, #lines do
out = out .. lines[i] out = out .. lines[i]
end end
return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'), return out`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' }], 0, 'string'),
method('GetHoveredCardId', `if path == nil or path == "" then
return nil
end
if string.find(path, "/ui/RunUIGroup/CardHand/Card", 1, true) == 1 then
if self.Hand == nil then
return nil
end
local slot = tonumber(string.match(path, "Card(%d+)")) or 0
return self.Hand[slot]
end
if string.find(path, "/ui/RunUIGroup/RewardHud/Reward", 1, true) == 1 then
if self.RewardCards == nil then
return nil
end
local slot = tonumber(string.match(path, "Reward(%d+)")) or 0
return self.RewardCards[slot]
end
if string.find(path, "/ui/RunUIGroup/ShopHud/Card", 1, true) == 1 then
if self.ShopCards == nil then
return nil
end
local slot = tonumber(string.match(path, "Card(%d+)")) or 0
return self.ShopCards[slot]
end
if string.find(path, "/ui/DeckUIGroup/DeckInspectHud/Grid/Card", 1, true) == 1 then
local pile = nil
if self.DeckInspectKind == "discard" then
pile = self.DiscardPile
elseif self.DeckInspectKind == "exhaust" then
pile = self.ExhaustPile
else
pile = self.DrawPile
end
if pile == nil then
return nil
end
local slot = tonumber(string.match(path, "Card(%d+)")) or 0
return pile[slot]
end
if string.find(path, "/ui/DeckUIGroup/DeckAllHud/Grid/Card", 1, true) == 1 then
local pile = self.RunDeck
if self.ClassDeckMode == true then
pile = self.ClassDeckCards
elseif self.CodexMode == true then
pile = self.CodexCards
end
if pile == nil then
return nil
end
local slot = tonumber(string.match(path, "Card(%d+)")) or 0
return pile[slot]
end
return nil`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }], 0, 'string'),
method('GetGridCardPathAtPoint', `if gridPath == nil or gridPath == "" or touchPoint == nil then
return nil
end
local prefix = nil
local maxSlots = 0
if gridPath == "/ui/DeckUIGroup/DeckAllHud/Grid" then
prefix = "/ui/DeckUIGroup/DeckAllHud/Grid/Card"
maxSlots = 120
elseif gridPath == "/ui/DeckUIGroup/DeckInspectHud/Grid" then
prefix = "/ui/DeckUIGroup/DeckInspectHud/Grid/Card"
maxSlots = 60
else
return nil
end
local uiPos = _UILogic:ScreenToUIPosition(touchPoint)
local bestPath = nil
local bestDist = 999999
for i = 1, maxSlots do
local path = prefix .. self:IntStr(i)
local cardEntity = _EntityService:GetEntityByPath(path)
if cardEntity ~= nil and cardEntity.Enable == true and cardEntity.UITransformComponent ~= nil then
local pos = self:GetUiPathPosition(path)
local dx = uiPos.x - pos.x
local dy = uiPos.y - pos.y
if math.abs(dx) <= 90 and math.abs(dy) <= 125 then
local dist = dx * dx + dy * dy
if bestPath == nil or dist < bestDist then
bestPath = path
bestDist = dist
end
end
end
end
return bestPath`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'gridPath' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
], 0, 'string'),
method('GetUiPathPosition', `if path == nil or path == "" then
return Vector2(0, 0)
end
local sumX = 0
local sumY = 0
local cur = path
while cur ~= nil and cur ~= "" do
local e = _EntityService:GetEntityByPath(cur)
if e ~= nil and e.UITransformComponent ~= nil then
local pos = e.UITransformComponent.anchoredPosition
if pos ~= nil then
sumX = sumX + (pos.x or 0)
sumY = sumY + (pos.y or 0)
end
end
local slash = string.match(cur, "^.*()/")
if slash == nil or slash <= 1 then
break
end
cur = string.sub(cur, 1, slash - 1)
if cur == "/ui" then
break
end
end
return Vector2(sumX, sumY)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }], 0, 'any'),
method('GetTooltipAnchorForPath', `local pos = self:GetUiPathPosition(path)
local x = pos.x
local y = pos.y
local sideOffset = 230
local verticalOffset = 0
if string.find(path, "/ui/RunUIGroup/CardHand/Card", 1, true) == 1 then
verticalOffset = 90
elseif string.find(path, "/ui/RunUIGroup/RewardHud/Reward", 1, true) == 1 then
verticalOffset = 30
elseif string.find(path, "/ui/RunUIGroup/ShopHud/Card", 1, true) == 1 then
verticalOffset = 20
else
verticalOffset = 0
end
local tipX = x + sideOffset
if x > 180 then
tipX = x - sideOffset
end
if tipX > 760 then
tipX = x - sideOffset
end
if tipX < -760 then
tipX = x + sideOffset
end
local tipY = y + verticalOffset
if tipY > 430 then
tipY = 430
end
if tipY < -430 then
tipY = -430
end
return Vector2(tipX, tipY)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }], 0, 'any'),
method('HoverCardByPath', `if path == nil or path == "" then
return
end
if self.DragSlot ~= nil and self.DragSlot > 0 and string.find(path, "/ui/RunUIGroup/CardHand/Card", 1, true) == 1 then
return
end
if self.GridScrollActive == true and (string.find(path, "/ui/DeckUIGroup/DeckAllHud/Grid/Card", 1, true) == 1 or string.find(path, "/ui/DeckUIGroup/DeckInspectHud/Grid/Card", 1, true) == 1) then
return
end
local cardId = self:GetHoveredCardId(path)
if cardId == nil or self.Cards == nil then
self:HideTooltip()
return
end
local c = self.Cards[cardId]
if c == nil then
self:HideTooltip()
return
end
local tip = self:BuildCardKeywordTooltip(c)
if tip == nil or tip == "" then
self:HideTooltip()
return
end
local e = _EntityService:GetEntityByPath(path)
if e ~= nil and e.UITransformComponent ~= nil then
if string.find(path, "/ui/RunUIGroup/CardHand/Card", 1, true) == 1 then
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
end
end
local anchor = self:GetTooltipAnchorForPath(path)
self:ShowTooltipAt("키워드", tip, anchor.x, anchor.y)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }]),
method('HoverGridCard', `if gridPath == nil or touchPoint == nil then
return
end
local path = self:GetGridCardPathAtPoint(gridPath, touchPoint)
local lastPath = self.GridHoverPath
if lastPath == path then
return
end
if lastPath ~= nil and lastPath ~= "" then
self:UnhoverCardByPath(lastPath)
end
self.GridHoverPath = path or ""
if path ~= nil and path ~= "" then
self:HoverCardByPath(path)
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'gridPath' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('UnhoverGridCard', `local path = self.GridHoverPath
self.GridHoverPath = ""
if path ~= nil and path ~= "" then
self:UnhoverCardByPath(path)
else
self:HideTooltip()
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'gridPath' }]),
method('UnhoverCardByPath', `if path ~= nil and string.find(path, "/ui/RunUIGroup/CardHand/Card", 1, true) == 1 then
local e = _EntityService:GetEntityByPath(path)
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end
end
self:HideTooltip()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }]),
method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then method('HoverCard', `if self.DragSlot ~= nil and self.DragSlot > 0 then
return return
end end
@@ -112,26 +323,21 @@ local cardId = self.Hand[slot]
if cardId == nil then if cardId == nil then
return return
end end
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
local tx = 0
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
tx = e.UITransformComponent.anchoredPosition.x
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1) e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
end end
local c = self.Cards[cardId] local c = self.Cards[cardId]
if c ~= nil then if c ~= nil then
local tip = self:BuildCardKeywordTooltip(c) local tip = self:BuildCardKeywordTooltip(c)
if tip ~= "" then if tip ~= "" then
local tipX = tx + 270 local anchor = self:GetTooltipAnchorForPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if tx > 180 then tipX = tx - 270 end self:ShowTooltipAt("키워드", tip, anchor.x, anchor.y)
if tipX > 760 then tipX = tx - 270 end
if tipX < -760 then tipX = tx + 270 end
self:ShowTooltipAt("키워드", tip, tipX, 90)
else else
self:HideTooltip() self:HideTooltip()
end end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot)) method('UnhoverCard', `local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if e ~= nil and e.UITransformComponent ~= nil then if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1) e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end end
@@ -141,9 +347,9 @@ self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, At
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
]), ]),
method('ShowTooltipAt', `self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Name", name) method('ShowTooltipAt', `self:SetText("/ui/ToolTipGroup/TooltipBox/Name", name)
self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Desc", desc) self:SetText("/ui/ToolTipGroup/TooltipBox/Desc", desc)
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TooltipBox") local e = _EntityService:GetEntityByPath("/ui/ToolTipGroup/TooltipBox")
if e ~= nil then if e ~= nil then
if e.UITransformComponent ~= nil then if e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(x, y) e.UITransformComponent.anchoredPosition = Vector2(x, y)
@@ -155,5 +361,5 @@ end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'y' },
]), ]),
method('HideTooltip', `self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/TooltipBox", false)`), method('HideTooltip', `self:SetEntityEnabled("/ui/ToolTipGroup/TooltipBox", false)`),
]; ];

View File

@@ -36,6 +36,10 @@ function writeCodeblocks() {
prop('number', 'TweenEventId', '0'), prop('number', 'TweenEventId', '0'),
prop('number', 'CardHoverTweenId', '0'), prop('number', 'CardHoverTweenId', '0'),
prop('number', 'DmgPopSeq', '0'), prop('number', 'DmgPopSeq', '0'),
prop('number', 'CombatCameraShakeSeq', '0'),
prop('any', 'DmgPopSlotQueue'),
prop('number', 'PlayerDmgPopIndex', '0'),
prop('any', 'HpBarOrigins'),
prop('any', 'EndTurnHandler'), prop('any', 'EndTurnHandler'),
prop('any', 'NewGameHandler'), prop('any', 'NewGameHandler'),
prop('any', 'WarriorSelectHandler'), prop('any', 'WarriorSelectHandler'),
@@ -75,6 +79,8 @@ function writeCodeblocks() {
prop('any', 'ClassDeckCards'), prop('any', 'ClassDeckCards'),
prop('string', 'ClassDeckTitle', '""'), prop('string', 'ClassDeckTitle', '""'),
prop('string', 'ClassDeckClass', '""'), prop('string', 'ClassDeckClass', '""'),
prop('string', 'GridHoverPath', '""'),
prop('boolean', 'GridScrollActive', 'false'),
prop('any', 'SoulUnlocks'), prop('any', 'SoulUnlocks'),
prop('any', 'SoulShopDef'), prop('any', 'SoulShopDef'),
prop('boolean', 'SoulShopBound', 'false'), prop('boolean', 'SoulShopBound', 'false'),
@@ -142,10 +148,14 @@ function writeCodeblocks() {
prop('number', 'PoisonApplicationsThisCombat', '0'), prop('number', 'PoisonApplicationsThisCombat', '0'),
prop('number', 'EnemyStrengthLossThisTurn', '0'), prop('number', 'EnemyStrengthLossThisTurn', '0'),
prop('number', 'ActiveKillReward', '0'), prop('number', 'ActiveKillReward', '0'),
prop('number', 'ActiveKillMaxHpGain', '0'),
prop('number', 'BonusRewardScreens', '0'), prop('number', 'BonusRewardScreens', '0'),
prop('number', 'FightAttackCount', '0'), prop('number', 'FightAttackCount', '0'),
prop('number', 'TurnAttackCardsPlayed', '0'), prop('number', 'TurnAttackCardsPlayed', '0'),
prop('number', 'TurnCardsPlayedThisTurn', '0'), prop('number', 'TurnCardsPlayedThisTurn', '0'),
prop('number', 'ComboCount', '0'),
prop('number', 'HolyChargeCount', '0'),
prop('boolean', 'DamagePowerStrengthUsed', 'false'),
prop('number', 'DamageDealtThisTurn', '0'), prop('number', 'DamageDealtThisTurn', '0'),
prop('number', 'TurnDiscardedCards', '0'), prop('number', 'TurnDiscardedCards', '0'),
prop('boolean', 'FirstHpLossDone', 'false'), prop('boolean', 'FirstHpLossDone', 'false'),
@@ -173,6 +183,7 @@ function writeCodeblocks() {
prop('boolean', 'NextSkillCostZero', 'false'), prop('boolean', 'NextSkillCostZero', 'false'),
prop('number', 'NextSkillRepeatCount', '0'), prop('number', 'NextSkillRepeatCount', '0'),
prop('any', 'NextTurnAddCards'), prop('any', 'NextTurnAddCards'),
prop('any', 'ZeroCostCardIdsThisTurn'),
], [ ], [
...bootMethods, ...bootMethods,
...screensMethods, ...screensMethods,

View File

@@ -19,18 +19,18 @@ for (const cls of Object.keys(CLASSES)) {
// 전직 옵션 // 전직 옵션
const JOBS = { const JOBS = {
warrior: [ warrior: [
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack', tier: 2, parent: 'warrior' }, { id: 'fighter', name: '파이터', desc: '콤보와 다단 공격 특화\n공격으로 콤보를 쌓고\n추가타로 압박', starter: 'ComboAttack', tier: 2, parent: 'warrior' },
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge', tier: 2, parent: 'warrior' }, { id: 'page', name: '페이지', desc: '홀리 포스와 방어 특화\n홀리 차지를 쌓아\n공격과 생존 강화', starter: 'HolyCharge', tier: 2, parent: 'warrior' },
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce', tier: 2, parent: 'warrior' }, { id: 'spearman', name: '스피어맨', desc: '광역·장기전 계열\n대화재 · 소용돌이\n불의 심장', starter: 'Conflagration', tier: 2, parent: 'warrior' },
], ],
fighter: [ fighter: [
{ id: 'crusader', name: '크루세이더', desc: 'Fighter의 3차 전직\n콤보 압박과 화력 심화\n파이터 카드 계승', starter: '', tier: 3, parent: 'fighter' }, { id: 'crusader', name: '크루세이더', desc: '파이터의 3차 전직\n콤보 상한과 연계 피해 강화\n파이터 카드 계승', starter: 'ComboSynergy', tier: 3, parent: 'fighter' },
], ],
page: [ page: [
{ id: 'knight', name: '나이트', desc: 'Page의 3차 전직\n방어와 차지 운영 심화\n페이지 카드 계승', starter: '', tier: 3, parent: 'page' }, { id: 'knight', name: '나이트', desc: '페이지의 3차 전직\n홀리 차지를 공격·방어·회복으로 전환\n페이지 카드 계승', starter: 'DivineCharge', tier: 3, parent: 'page' },
], ],
spearman: [ spearman: [
{ id: 'berserker', name: '버서커', desc: 'Spearman의 3차 전직\n관통과 생존 운영 심화\n스피어맨 카드 계승', starter: '', tier: 3, parent: 'spearman' }, { id: 'berserker', name: '버서커', desc: '스피어맨의 3차 전직\n아이언클래드 장기전 풀 계승\n전사 카드 사용', starter: '', tier: 3, parent: 'spearman' },
], ],
magician: [ magician: [
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow', tier: 2, parent: 'magician' }, { id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow', tier: 2, parent: 'magician' },
@@ -100,7 +100,7 @@ const SOUL_UNLOCKS = [
{ key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 }, { key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 },
]; ];
function luaSoulShopTable(unlocks) { function luaSoulShopTable(unlocks) {
const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n'); const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${luaNum(u.cost)} },`).join('\n');
return `self.SoulShopDef = {\n${items}\n}`; return `self.SoulShopDef = {\n${items}\n}`;
} }
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
@@ -165,7 +165,7 @@ for (const id of RELICS.relicPool) {
} }
function luaRelicsTable(relics) { function luaRelicsTable(relics) {
const lines = Object.entries(relics).map(([id, r]) => const lines = Object.entries(relics).map(([id, r]) =>
`\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${r.value}, icon = ${luaStr(r.icon || '')} },`); `\t${id} = { name = ${luaStr(r.name)}, desc = ${luaStr(r.desc)}, hook = ${luaStr(r.hook)}, effect = ${luaStr(r.effect)}, value = ${luaNum(r.value)}, icon = ${luaStr(r.icon || '')} },`);
return `self.Relics = {\n${lines.join('\n')}\n}`; return `self.Relics = {\n${lines.join('\n')}\n}`;
} }
@@ -175,32 +175,38 @@ for (const [pid, p] of Object.entries(POTIONS.potions)) {
} }
function luaPotionsTable(potions) { function luaPotionsTable(potions) {
const lines = Object.entries(potions).map(([id, p]) => const lines = Object.entries(potions).map(([id, p]) =>
`\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${p.value}, icon = ${luaStr(p.icon || '')} },`); `\t${id} = { name = ${luaStr(p.name)}, desc = ${luaStr(p.desc)}, effect = ${luaStr(p.effect)}, value = ${luaNum(p.value)}, icon = ${luaStr(p.icon || '')} },`);
return `self.Potions = {\n${lines.join('\n')}\n}`; return `self.Potions = {\n${lines.join('\n')}\n}`;
} }
function luaIntentsArray(intents) { function luaIntentsArray(intents) {
return '{ ' + intents.map((it) => { return '{ ' + intents.map((it) => {
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value != null ? it.value : 0}`]; const fields = [`kind = ${luaStr(it.kind)}`, `value = ${luaNum(it.value != null ? it.value : 0)}`];
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`); if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
if (it.card != null) fields.push(`card = ${luaStr(it.card)}`); if (it.card != null) fields.push(`card = ${luaStr(it.card)}`);
if (it.count != null) fields.push(`count = ${it.count}`); if (it.count != null) fields.push(`count = ${luaNum(it.count)}`);
return `{ ${fields.join(', ')} }`; return `{ ${fields.join(', ')} }`;
}).join(', ') + ' }'; }).join(', ') + ' }';
} }
function luaEnemiesTable(enemies) { function luaEnemiesTable(enemies) {
const lines = Object.entries(enemies).map(([id, e]) => const lines = Object.entries(enemies).map(([id, e]) =>
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`); `\t${id} = { name = ${luaStr(e.name)}, maxHp = ${luaNum(e.maxHp)}, intents = ${luaIntentsArray(e.intents)} },`);
return `self.Enemies = {\n${lines.join('\n')}\n}`; return `self.Enemies = {\n${lines.join('\n')}\n}`;
} }
function luaStr(s) { function luaStr(s) {
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"'; return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
} }
function luaNum(n) {
const v = Number(n);
if (!Number.isFinite(v)) return '0';
if (Number.isInteger(v)) return String(v);
return String(v);
}
function luaJobsTable(jobs) { function luaJobsTable(jobs) {
const cls = Object.entries(jobs).map(([clsId, list]) => { const cls = Object.entries(jobs).map(([clsId, list]) => {
const items = list.map((j) => const items = list.map((j) =>
`\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)}, tier = ${j.tier ?? 2}, parent = ${luaStr(j.parent ?? clsId)} },`).join('\n'); `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)}, tier = ${luaNum(j.tier ?? 2)}, parent = ${luaStr(j.parent ?? clsId)} },`).join('\n');
return `\t${clsId} = {\n${items}\n\t},`; return `\t${clsId} = {\n${items}\n\t},`;
}).join('\n'); }).join('\n');
return `self.Jobs = {\n${cls}\n}`; return `self.Jobs = {\n${cls}\n}`;
@@ -217,97 +223,133 @@ function luaClassLineagesTable(lineages) {
} }
function luaJobMetaTable(meta) { function luaJobMetaTable(meta) {
const rows = Object.entries(meta).map(([jobId, entry]) => const rows = Object.entries(meta).map(([jobId, entry]) =>
`\t${jobId} = { name = ${luaStr(entry.name)}, starter = ${luaStr(entry.starter)}, tier = ${entry.tier}, parent = ${luaStr(entry.parent)}, sourceClass = ${luaStr(entry.sourceClass)} },`); `\t${jobId} = { name = ${luaStr(entry.name)}, starter = ${luaStr(entry.starter)}, tier = ${luaNum(entry.tier)}, parent = ${luaStr(entry.parent)}, sourceClass = ${luaStr(entry.sourceClass)} },`);
return `self.JobMeta = {\n${rows.join('\n')}\n}`; return `self.JobMeta = {\n${rows.join('\n')}\n}`;
} }
function luaCardsTable(cards) { function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => { const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`]; const fields = [`name = ${luaStr(c.name)}`, `cost = ${luaNum(c.cost)}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`); if (c.damage != null) fields.push(`damage = ${luaNum(c.damage)}`);
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${c.damagePerOtherHandCard}`); if (c.damageFromCurrentBlock != null) fields.push(`damageFromCurrentBlock = ${luaNum(c.damageFromCurrentBlock)}`);
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`); if (c.damagePerCombo != null) fields.push(`damagePerCombo = ${luaNum(c.damagePerCombo)}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`); if (c.damagePerHolyCharge != null) fields.push(`damagePerHolyCharge = ${luaNum(c.damagePerHolyCharge)}`);
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`); if (c.attackDamagePerCombo != null) fields.push(`attackDamagePerCombo = ${luaNum(c.attackDamagePerCombo)}`);
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${c.damagePerCardDrawnThisCombat}`); if (c.damageNameMatch != null) fields.push(`damageNameMatch = ${luaStr(c.damageNameMatch)}`);
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`); if (c.damagePerOwnedNameMatch != null) fields.push(`damagePerOwnedNameMatch = ${luaNum(c.damagePerOwnedNameMatch)}`);
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`); if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${luaNum(c.damagePerOtherHandCard)}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`); if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${luaNum(c.damagePerAttackPlayedThisTurn)}`);
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${c.firstCardDamageBonus}`); if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${luaNum(c.damagePerDiscardedThisTurn)}`);
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`); if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${luaNum(c.damagePerSkillInHand)}`);
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`); if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${luaNum(c.damagePerCardDrawnThisCombat)}`);
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`); if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${luaNum(c.damagePerTurn)}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`); if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${luaNum(c.cardPlayedDamage)}`);
if (c.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`); if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${luaNum(c.cardPlayedRandomDamage)}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`); if (c.attackPlayedDamage != null) fields.push(`attackPlayedDamage = ${luaNum(c.attackPlayedDamage)}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`); if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${luaNum(c.firstCardDamageBonus)}`);
if (c.block != null) fields.push(`block = ${c.block}`); if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${luaNum(c.rewardOnKill)}`);
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`); if (c.maxHpOnKill != null) fields.push(`maxHpOnKill = ${luaNum(c.maxHpOnKill)}`);
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${c.blockPerDamageDealtThisTurn}`); if (c.intangible != null) fields.push(`intangible = ${luaNum(c.intangible)}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`); if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${luaNum(c.endTurnDexLoss)}`);
if (c.dex != null) fields.push(`dex = ${c.dex}`); if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${luaNum(c.poisonPerTurn)}`);
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`); if (c.attackPoison != null) fields.push(`attackPoison = ${luaNum(c.attackPoison)}`);
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${c.cardPlayedBlock}`); if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${luaNum(c.otherHandAtLeast)}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`); if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${luaNum(c.bonusHitsWhenOtherHandAtLeast)}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`); if (c.block != null) fields.push(`block = ${luaNum(c.block)}`);
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${luaNum(c.blockGainMultiplier)}`);
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${luaNum(c.blockPerDamageDealtThisTurn)}`);
if (c.strength != null) fields.push(`strength = ${luaNum(c.strength)}`);
if (c.dex != null) fields.push(`dex = ${luaNum(c.dex)}`);
if (c.thorns != null) fields.push(`thorns = ${luaNum(c.thorns)}`);
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${luaNum(c.cardPlayedBlock)}`);
if (c.comboOnAttack != null) fields.push(`comboOnAttack = ${luaNum(c.comboOnAttack)}`);
if (c.comboMax != null) fields.push(`comboMax = ${luaNum(c.comboMax)}`);
if (c.attackWeak != null) fields.push(`attackWeak = ${luaNum(c.attackWeak)}`);
if (c.holyChargeOnHolyForce != null) fields.push(`holyChargeOnHolyForce = ${luaNum(c.holyChargeOnHolyForce)}`);
if (c.holyChargeMax != null) fields.push(`holyChargeMax = ${luaNum(c.holyChargeMax)}`);
if (c.damageTakenReduction != null) fields.push(`damageTakenReduction = ${luaNum(c.damageTakenReduction)}`);
if (c.blockOnDamaged != null) fields.push(`blockOnDamaged = ${luaNum(c.blockOnDamaged)}`);
if (c.strengthOnDamagedOnce != null) fields.push(`strengthOnDamagedOnce = ${luaNum(c.strengthOnDamagedOnce)}`);
if (c.drawOnExhaust != null) fields.push(`drawOnExhaust = ${luaNum(c.drawOnExhaust)}`);
if (c.drawNameMatchAutoPlay != null) fields.push(`drawNameMatchAutoPlay = ${luaStr(c.drawNameMatchAutoPlay)}`);
if (c.weak != null) fields.push(`weak = ${luaNum(c.weak)}`);
if (c.vuln != null) fields.push(`vuln = ${luaNum(c.vuln)}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`); if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`); if (c.value != null) fields.push(`value = ${luaNum(c.value)}`);
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`); if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
fields.push(`class = ${luaStr(c.class)}`); fields.push(`class = ${luaStr(c.class)}`);
fields.push(`rarity = ${luaStr(c.rarity)}`); fields.push(`rarity = ${luaStr(c.rarity)}`);
if (c.hits != null) fields.push(`hits = ${c.hits}`); if (c.hits != null) fields.push(`hits = ${luaNum(c.hits)}`);
if (c.pierce === true) fields.push('pierce = true'); if (c.pierce === true) fields.push('pierce = true');
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`); if (c.selfVuln != null) fields.push(`selfVuln = ${luaNum(c.selfVuln)}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`); if (c.draw != null) fields.push(`draw = ${luaNum(c.draw)}`);
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`); if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${luaNum(c.drawUntilHandSize)}`);
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`); if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${luaNum(c.drawSkillBlock)}`);
if (c.drawDamage != null) fields.push(`drawDamage = ${c.drawDamage}`); if (c.drawDamage != null) fields.push(`drawDamage = ${luaNum(c.drawDamage)}`);
if (c.drawPoison != null) fields.push(`drawPoison = ${c.drawPoison}`); if (c.drawPoison != null) fields.push(`drawPoison = ${luaNum(c.drawPoison)}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`); if (c.exhaustHandNonAttack === true) fields.push('exhaustHandNonAttack = true');
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`); if (c.exhaustHandAll === true) fields.push('exhaustHandAll = true');
if (c.poison != null) fields.push(`poison = ${c.poison}`); if (c.drawPerExhausted != null) fields.push(`drawPerExhausted = ${luaNum(c.drawPerExhausted)}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`); if (c.blockPerExhaustedCard != null) fields.push(`blockPerExhaustedCard = ${luaNum(c.blockPerExhaustedCard)}`);
if (c.addRandomCardCount != null) fields.push(`addRandomCardCount = ${luaNum(c.addRandomCardCount)}`);
if (c.addRandomCardPerExhausted != null) fields.push(`addRandomCardPerExhausted = ${luaNum(c.addRandomCardPerExhausted)}`);
if (c.addRandomCardKind != null) fields.push(`addRandomCardKind = ${luaStr(c.addRandomCardKind)}`);
if (c.addRandomCardSameClass === true) fields.push('addRandomCardSameClass = true');
if (c.addedCardsCostZeroThisTurn === true) fields.push('addedCardsCostZeroThisTurn = true');
if (c.playTopDrawPileCount != null) fields.push(`playTopDrawPileCount = ${luaNum(c.playTopDrawPileCount)}`);
if (c.playTopDrawPileCountPerEnergy != null) fields.push(`playTopDrawPileCountPerEnergy = ${luaNum(c.playTopDrawPileCountPerEnergy)}`);
if (c.heal != null) fields.push(`heal = ${luaNum(c.heal)}`);
if (c.healPerHolyCharge != null) fields.push(`healPerHolyCharge = ${luaNum(c.healPerHolyCharge)}`);
if (c.gainEnergy != null) fields.push(`gainEnergy = ${luaNum(c.gainEnergy)}`);
if (c.comboGain != null) fields.push(`comboGain = ${luaNum(c.comboGain)}`);
if (c.removePlayerDebuffs === true) fields.push('removePlayerDebuffs = true');
if (c.holyForce === true) fields.push('holyForce = true');
if (c.holyChargeGain != null) fields.push(`holyChargeGain = ${luaNum(c.holyChargeGain)}`);
if (c.blockPerHolyCharge != null) fields.push(`blockPerHolyCharge = ${luaNum(c.blockPerHolyCharge)}`);
if (c.holyChargeSpendAll === true) fields.push('holyChargeSpendAll = true');
if (c.poison != null) fields.push(`poison = ${luaNum(c.poison)}`);
if (c.discard != null) fields.push(`discard = ${luaNum(c.discard)}`);
if (c.discardAll === true) fields.push('discardAll = true'); if (c.discardAll === true) fields.push('discardAll = true');
if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${c.drawPerDiscarded}`); if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${luaNum(c.drawPerDiscarded)}`);
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`); if (c.addShiv != null) fields.push(`addShiv = ${luaNum(c.addShiv)}`);
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`); if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${luaNum(c.turnStartShiv)}`);
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`); if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${luaNum(c.turnStartDraw)}`);
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`); if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${luaNum(c.turnStartDiscard)}`);
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true'); if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true');
if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true'); if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true');
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true'); if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
if (c.useAllEnergy === true) fields.push('useAllEnergy = true'); if (c.useAllEnergy === true) fields.push('useAllEnergy = true');
if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${c.shivDamageBonus}`); if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${luaNum(c.shivDamageBonus)}`);
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${c.firstShivDamageBonus}`); if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${luaNum(c.firstShivDamageBonus)}`);
if (c.shivRetain === true) fields.push('shivRetain = true'); if (c.shivRetain === true) fields.push('shivRetain = true');
if (c.shivAoe === true) fields.push('shivAoe = true'); if (c.shivAoe === true) fields.push('shivAoe = true');
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`); if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${luaNum(c.attackDamageVsWeakMultiplier)}`);
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`); if (c.poisonHits != null) fields.push(`poisonHits = ${luaNum(c.poisonHits)}`);
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true'); if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true'); if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true');
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`); if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${luaNum(c.xDamagePerEnergy)}`);
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`); if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${luaNum(c.xWeakPerEnergy)}`);
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`); if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${luaNum(c.nextTurnBlock)}`);
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`); if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${luaNum(c.nextTurnDraw)}`);
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true'); if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${c.nextTurnAttackMultiplier}`); if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${luaNum(c.nextTurnAttackMultiplier)}`);
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`); if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${luaNum(c.nextTurnCopies)}`);
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true'); if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`); if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`); if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${luaNum(c.nextSkillRepeatCount)}`);
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true'); if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`); if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${luaNum(c.skillCostReductionThisTurn)}`);
if (c.skillSlyOnPlay === true) fields.push('skillSlyOnPlay = true'); if (c.skillSlyOnPlay === true) fields.push('skillSlyOnPlay = true');
if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${c.turnHandSlyCount}`); if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${luaNum(c.turnHandSlyCount)}`);
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${c.combatCostReductionOnPlay}`); if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${luaNum(c.combatCostReductionOnPlay)}`);
if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true'); if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true');
if (c.repeatOnKill === true) fields.push('repeatOnKill = true'); if (c.repeatOnKill === true) fields.push('repeatOnKill = true');
if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true'); if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true');
if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true'); if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true');
if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true'); if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true');
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${c.enemyStrengthLossThisTurn}`); if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${luaNum(c.enemyStrengthLossThisTurn)}`);
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${c.extraPoisonTicks}`); if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${luaNum(c.extraPoisonTicks)}`);
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${c.poisonApplicationBurstEvery}`); if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${luaNum(c.poisonApplicationBurstEvery)}`);
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${c.poisonApplicationBurstDamage}`); if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${luaNum(c.poisonApplicationBurstDamage)}`);
if (c.innate === true) fields.push('innate = true'); if (c.innate === true) fields.push('innate = true');
if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true'); if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = true');
if (c.sly === true) fields.push('sly = true'); if (c.sly === true) fields.push('sly = true');
@@ -317,7 +359,7 @@ function luaCardsTable(cards) {
if (c.unplayable === true) fields.push('unplayable = true'); if (c.unplayable === true) fields.push('unplayable = true');
if (c.curse === true) fields.push('curse = true'); if (c.curse === true) fields.push('curse = true');
if (c.token === true) fields.push('token = true'); if (c.token === true) fields.push('token = true');
if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${c.endTurnDamage}`); if (c.endTurnDamage != null) fields.push(`endTurnDamage = ${luaNum(c.endTurnDamage)}`);
if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`); if (c.fx != null) fields.push(`fx = ${luaStr(c.fx)}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`); if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`; return `\t${id} = { ${fields.join(', ')} },`;

View File

@@ -17,6 +17,10 @@ const POWER_FIELDS = [
'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe', 'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe',
'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier', 'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier',
'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage', 'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage',
'comboOnAttack', 'comboMax', 'attackDamagePerCombo', 'attackPlayedDamage', 'attackWeak',
'holyChargeOnHolyForce', 'holyChargeMax', 'damageTakenReduction',
'blockOnDamaged', 'strengthOnDamagedOnce',
'drawOnExhaust', 'drawNameMatchAutoPlay',
'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage', 'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage',
'skillSlyOnPlay', 'endTurnDexLoss', 'skillSlyOnPlay', 'endTurnDexLoss',
]; ];
@@ -28,7 +32,7 @@ for (const [id, c] of Object.entries(cards)) {
issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`); issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`);
continue; continue;
} }
if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null) { if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null && c.damageFromCurrentBlock == null) {
issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`); issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`);
} }
if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) { if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) {

View File

@@ -24,7 +24,7 @@
"enable": true, "enable": true,
"visible": true, "visible": true,
"localize": true, "localize": true,
"displayOrder": 4, "displayOrder": 6,
"pathConstraints": "//", "pathConstraints": "//",
"revision": 1, "revision": 1,
"origin": { "origin": {
@@ -1539,7 +1539,7 @@
{ {
"id": "0e200005-0000-4000-8000-00000e200005", "id": "0e200005-0000-4000-8000-00000e200005",
"path": "/ui/DeckUIGroup/DeckAllHud/Grid", "path": "/ui/DeckUIGroup/DeckAllHud/Grid",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent", "componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent,MOD.Core.UITouchReceiveComponent",
"jsonString": { "jsonString": {
"name": "Grid", "name": "Grid",
"path": "/ui/DeckUIGroup/DeckAllHud/Grid", "path": "/ui/DeckUIGroup/DeckAllHud/Grid",
@@ -1728,6 +1728,10 @@
"UseScroll": true, "UseScroll": true,
"VerticalScrollBarDirection": 1, "VerticalScrollBarDirection": 1,
"Enable": true "Enable": true
},
{
"@type": "MOD.Core.UITouchReceiveComponent",
"Enable": true
} }
], ],
"@version": 1 "@version": 1
@@ -106038,7 +106042,7 @@
{ {
"id": "0e100005-0000-4000-8000-00000e100005", "id": "0e100005-0000-4000-8000-00000e100005",
"path": "/ui/DeckUIGroup/DeckInspectHud/Grid", "path": "/ui/DeckUIGroup/DeckInspectHud/Grid",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent", "componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ScrollLayoutGroupComponent,MOD.Core.UITouchReceiveComponent",
"jsonString": { "jsonString": {
"name": "Grid", "name": "Grid",
"path": "/ui/DeckUIGroup/DeckInspectHud/Grid", "path": "/ui/DeckUIGroup/DeckInspectHud/Grid",
@@ -106227,6 +106231,10 @@
"UseScroll": true, "UseScroll": true,
"VerticalScrollBarDirection": 1, "VerticalScrollBarDirection": 1,
"Enable": true "Enable": true
},
{
"@type": "MOD.Core.UITouchReceiveComponent",
"Enable": true
} }
], ],
"@version": 1 "@version": 1

View File

@@ -24,7 +24,7 @@
"enable": true, "enable": true,
"visible": true, "visible": true,
"localize": true, "localize": true,
"displayOrder": 5, "displayOrder": 1,
"pathConstraints": "//", "pathConstraints": "//",
"revision": 1, "revision": 1,
"origin": { "origin": {

View File

@@ -24,7 +24,7 @@
"enable": true, "enable": true,
"visible": true, "visible": true,
"localize": true, "localize": true,
"displayOrder": 1, "displayOrder": 2,
"pathConstraints": "//", "pathConstraints": "//",
"revision": 1, "revision": 1,
"origin": { "origin": {

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"enable": true, "enable": true,
"visible": true, "visible": true,
"localize": true, "localize": true,
"displayOrder": 2, "displayOrder": 3,
"pathConstraints": "//", "pathConstraints": "//",
"revision": 1, "revision": 1,
"origin": { "origin": {

626
ui/ToolTipGroup.ui Normal file
View File

@@ -0,0 +1,626 @@
{
"Id": "",
"GameId": "",
"EntryKey": "ui://8208cc46-b682-444c-91a0-906bd01d39e1",
"ContentType": "x-mod/ui",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Binary",
"Entities": [
{
"id": "8208cc46-b682-444c-91a0-906bd01d39e1",
"path": "/ui/ToolTipGroup",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.UIGroupComponent,MOD.Core.CanvasGroupComponent",
"jsonString": {
"name": "ToolTipGroup",
"path": "/ui/ToolTipGroup",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 7,
"pathConstraints": "//",
"revision": 1,
"origin": {
"type": "Model",
"entry_id": "uigroup",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uigroup",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"AlignmentOption": 15,
"AnchorsMax": {
"x": 1.0,
"y": 1.0
},
"AnchorsMin": {
"x": 0.0,
"y": 0.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 0.0,
"y": 0.0
},
"OffsetMin": {
"x": 0.0,
"y": 0.0
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 1920.0,
"y": 1080.0
},
"UIMode": 1,
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": 0.0
},
"Position": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.UIGroupComponent",
"DefaultShow": true,
"GroupOrder": 7,
"GroupType": 2,
"Enable": true
},
{
"@type": "MOD.Core.CanvasGroupComponent",
"Enable": true
}
],
"@version": 1
}
},
{
"id": "0cb0014a-0000-4000-8000-00000cb0014a",
"path": "/ui/ToolTipGroup/TooltipBox",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent",
"jsonString": {
"name": "TooltipBox",
"path": "/ui/ToolTipGroup/TooltipBox",
"nameEditable": true,
"enable": false,
"visible": true,
"localize": true,
"displayOrder": 0,
"pathConstraints": "///",
"revision": 1,
"origin": {
"type": "Model",
"entry_id": "UISprite",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uisprite",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 0,
"AnchorsMax": {
"x": 0.5,
"y": 0.5
},
"AnchorsMin": {
"x": 0.5,
"y": 0.5
},
"MobileOnly": false,
"OffsetMax": {
"x": 179.9999,
"y": 475.0001
},
"OffsetMin": {
"x": -180.0001,
"y": 325.0001
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 360.0,
"y": 150.0
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": -0.0001,
"y": 400.0001
},
"Rotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"Position": {
"x": -0.0001,
"y": 400.0001,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": -1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"ZRotation": 0.0,
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": ""
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 0.04,
"g": 0.05,
"b": 0.08,
"a": 0.96
},
"DropShadow": false,
"DropShadowAngle": 30.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 32.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": false,
"Type": 1,
"Enable": true
}
],
"@version": 1
}
},
{
"id": "0cb0014b-0000-4000-8000-00000cb0014b",
"path": "/ui/ToolTipGroup/TooltipBox/Name",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
"jsonString": {
"name": "Name",
"path": "/ui/ToolTipGroup/TooltipBox/Name",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 0,
"pathConstraints": "////",
"revision": 1,
"origin": {
"type": "Model",
"entry_id": "UIText",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uitext",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 0,
"AnchorsMax": {
"x": 0.5,
"y": 0.5
},
"AnchorsMin": {
"x": 0.5,
"y": 0.5
},
"MobileOnly": false,
"OffsetMax": {
"x": 166.0,
"y": 66.0
},
"OffsetMin": {
"x": -166.0,
"y": 38.0
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 332.0,
"y": 28.0
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": 52.0
},
"Position": {
"x": 0.0,
"y": 52.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": ""
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.0
},
"DropShadow": false,
"DropShadowAngle": 30.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 32.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": false,
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.TextComponent",
"Alignment": 4,
"Bold": true,
"DropShadow": false,
"DropShadowAngle": 30.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 32.0,
"Font": 0,
"FontColor": {
"r": 0.94,
"g": 0.74,
"b": 0.26,
"a": 1.0
},
"FontSize": 19,
"MaxSize": 19,
"MinSize": 8,
"OutlineColor": {
"r": 0.08,
"g": 0.08,
"b": 0.08,
"a": 1.0
},
"OutlineDistance": {
"x": 1.0,
"y": -1.0
},
"OutlineWidth": 1.0,
"Overflow": 0,
"OverrideSorting": false,
"Padding": {
"left": 0,
"right": 0,
"top": 0,
"bottom": 0
},
"SizeFit": false,
"Text": "",
"UseOutLine": true,
"Enable": true
}
],
"@version": 1
}
},
{
"id": "0cb0014c-0000-4000-8000-00000cb0014c",
"path": "/ui/ToolTipGroup/TooltipBox/Desc",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
"jsonString": {
"name": "Desc",
"path": "/ui/ToolTipGroup/TooltipBox/Desc",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 1,
"pathConstraints": "////",
"revision": 1,
"origin": {
"type": "Model",
"entry_id": "UIText",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uitext",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 0,
"AnchorsMax": {
"x": 0.5,
"y": 0.5
},
"AnchorsMin": {
"x": 0.5,
"y": 0.5
},
"MobileOnly": false,
"OffsetMax": {
"x": 166.0,
"y": 33.0
},
"OffsetMin": {
"x": -166.0,
"y": -69.0
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 332.0,
"y": 102.0
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": -18.0
},
"Position": {
"x": 0.0,
"y": -18.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": ""
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.0
},
"DropShadow": false,
"DropShadowAngle": 30.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 32.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": false,
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.TextComponent",
"Alignment": 0,
"Bold": false,
"DropShadow": false,
"DropShadowAngle": 30.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 32.0,
"Font": 0,
"FontColor": {
"r": 0.92,
"g": 0.92,
"b": 0.95,
"a": 1.0
},
"FontSize": 15,
"MaxSize": 15,
"MinSize": 8,
"OutlineColor": {
"r": 0.08,
"g": 0.08,
"b": 0.08,
"a": 1.0
},
"OutlineDistance": {
"x": 1.0,
"y": -1.0
},
"OutlineWidth": 1.0,
"Overflow": 0,
"OverrideSorting": false,
"Padding": {
"left": 0,
"right": 0,
"top": 0,
"bottom": 0
},
"SizeFit": false,
"Text": "",
"UseOutLine": true,
"Enable": true
}
],
"@version": 1
}
}
]
}
}