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": {
"warrior": "warrior",
"fighter": "warrior",
"crusader": "warrior",
"page": "warrior",
"knight": "warrior",
"spearman": "warrior",
"magician": "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`: 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
- `damagePerAttackPlayedThisTurn`: bonus damage per attack played this turn
- `damagePerDiscardedThisTurn`: bonus damage per card discarded this turn
- `damagePerSkillInHand`: bonus damage per skill card in hand
- `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
- `cardPlayedDamage`: 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
- `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
- `repeatOnKill`: repeat the attack when it kills at least one enemy
- `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
- `drawSkillBlock`: gain block for each Skill 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
- `drawDisabledThisTurn`: disable draw for the rest of the turn
- `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
- `selfVuln`: apply Vulnerable to self
- `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
@@ -91,7 +127,9 @@ The goal is to keep card behavior reusable instead of hardcoding one-off card na
- `powerEffect: "blockPerTurn"`
- `powerEffect: "poisonPerTurn"`
- `powerEffect: "damagePerTurn"`
- `powerEffect: "healPerTurn"`
- `powerEffect: "retainOne"`
- `powerEffect: "keepBlock"`
- `turnStartShiv`: create Shivs at turn start
- `turnStartDraw`: draw 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,
} 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 = {
rogue: [
@@ -38,6 +41,27 @@ const CONTEXT_DECKS = {
'Survivor', 'LeadingStrike', 'BladeDance', 'JavelinAcceleration',
'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 = {
@@ -46,6 +70,11 @@ const ENCOUNTER_SCALE = {
assassin: { hp: 2.25, attack: 1.65 },
thiefmaster: { hp: 2.4, attack: 1.5 },
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) => {
@@ -172,7 +201,7 @@ export function auditCardEfficiency({ runs = 300, seed = 20260701 } = {}) {
const rows = [];
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();
deck[replacementIndex(deck, cards, card)] = id;
const result = simulateDeck(scaledEncounter(data, card.class), deck, runs, seed, id);
@@ -204,7 +233,7 @@ function formatPercent(value) {
export function formatEfficiencyReport(report) {
const lines = [];
lines.push(`도적 카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}`);
lines.push(`카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}`);
lines.push('기준 덱:');
for (const [classId, baseline] of Object.entries(report.baselines)) {
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 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 SECTION_COUNT = 5;
const NORMAL_FIGHTS = 4;
@@ -18,6 +18,8 @@ const BOSS_POOL = ['king_slime', 'slime_boss'];
const JOBS = {
thief: { tier2: 'thief', tier3: 'thiefmaster', tier2Starter: 'DaggerAcceleration', tier3Starter: 'Venom' },
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 = {
@@ -26,12 +28,17 @@ const LINEAGES = {
thiefmaster: ['rogue', 'thief', 'thiefmaster'],
assassin: ['rogue', 'assassin'],
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)];
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;
return JOBS[branch].tier3;
}
@@ -102,11 +109,22 @@ function branchCardValue(card, branch, deck, id) {
value += card.sly ? 5 : 0;
value += (card.discard || 0) * 2 + (card.drawPerDiscarded || 0) * 4;
value += (card.poisonApplicationBurstDamage || 0) * 1.5;
} else {
} else if (branch === 'assassin') {
value += (card.addShiv || 0) * 3 + (card.turnStartShiv || 0) * 8;
value += (card.shivDamageBonus || 0) * 6 + (card.firstShivDamageBonus || 0) * 3;
value += card.shivAoe ? 12 : 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;
value -= copies * (card.kind === 'Power' ? 10 : 3);
@@ -192,7 +210,8 @@ function fight(state, branch, kind, section, rng, options) {
state.turns += result.turns;
if (!result.win) return false;
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;
}
@@ -203,11 +222,13 @@ export function simulateCampaign(branch, rng, {
minimumRewardValue = 10,
} = {}) {
if (!JOBS[branch]) throw new Error(`지원하지 않는 도적 분기: ${branch}`);
const root = JOBS[branch].root || 'rogue';
const maxHp = PLAYER_MAX_HP[root];
const state = {
hp: PLAYER_MAX_HP,
maxHp: PLAYER_MAX_HP,
deck: cardsData.starterDecks.rogue.slice(),
job: 'rogue',
hp: maxHp,
maxHp,
deck: cardsData.starterDecks[root].slice(),
job: root,
turns: 0,
sectionCleared: 0,
diedAt: '',
@@ -306,7 +327,7 @@ function main() {
else if (args[i] === '--scale-step') scaleStep = 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 })));
}
}

View File

@@ -17,6 +17,16 @@ test('도적 전직 시점: 1섹션 Rogue, 2섹션 2차, 3섹션부터 3차', ()
test('3차 직업은 자기 계보 카드만 사용', () => {
assert.deepEqual(playableClassesForJob('thiefmaster'), ['rogue', 'thief', 'thiefmaster']);
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차 이후 더 빠르게 증가', () => {

View File

@@ -99,7 +99,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const card = cards[x.id];
if (!card || card.unplayable || !canPlayCardNow(card, ctx)) return false;
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.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) effectiveCost = 0;
@@ -116,7 +116,7 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
const effectiveCost = (x) => {
const card = cards[x.id];
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.kind === 'Skill') {
if (ctx.nextSkillCostZero === true) cost = 0;
@@ -127,7 +127,48 @@ export function chooseAction(hand, cards, energy, ctx = {}) {
}
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 bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[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 (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 (attacks.length) return bestBy(attacks, dmgEff).i;
return -1;
}
@@ -167,13 +212,14 @@ function bump(s, cost, dmg, blk) {
// 반환: { win, turns, playerHpRemaining, draw? }
export function simulateCombat(data, rng, stats) {
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);
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: startingPlayerHp };
let drawPile = prepareCombatDrawPile(shuffle(starterDeck, rng), cards);
let discard = [];
const exhaust = [];
let hand = [];
const zeroCostCardIdsThisTurn = new Set();
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 blockGainMultiplier = 1;
@@ -200,6 +246,9 @@ export function simulateCombat(data, rng, stats) {
let cardsDrawnThisCombat = 0;
let bonusRewardScreens = 0;
let activeKillReward = 0;
let comboCount = 0;
let holyChargeCount = 0;
let damagePowerStrengthUsed = false;
let energy = 0;
const powers = [];
const mob = monsters.map((m) => ({
@@ -215,6 +264,14 @@ export function simulateCombat(data, rng, stats) {
if (!alive.length) return null;
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) => {
if (!m.intents || m.intents.length === 0) return total;
const expected = m.intents.reduce((sum, intent) => {
@@ -315,10 +372,26 @@ export function simulateCombat(data, rng, stats) {
if (hand.length >= 10) {
discard.push(card);
triggerSly(card);
} else hand.push(card);
} else {
hand.push(card);
triggerDrawNameMatchAutoPlay(card);
}
}
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) {
for (let k = 0; k < n; k++) {
if (hand.length >= 10) discard.push(id);
@@ -380,8 +453,26 @@ export function simulateCombat(data, rng, stats) {
}
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) {
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);
if (c.damagePerOtherHandCard) base += otherHand * c.damagePerOtherHandCard;
if (c.damagePerAttackPlayedThisTurn) base += turnAttackCardsPlayed * c.damagePerAttackPlayedThisTurn;
@@ -433,6 +524,69 @@ export function simulateCombat(data, rng, stats) {
}
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) {
const alive = aliveList();
let dmg = 0;
@@ -461,7 +615,7 @@ export function simulateCombat(data, rng, stats) {
}
const xEnergy = costSpent || 0;
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 bonusHits = (c.otherHandAtLeast && c.bonusHitsWhenOtherHandAtLeast && Math.max(0, hand.length - 1) >= c.otherHandAtLeast)
? c.bonusHitsWhenOtherHandAtLeast : 0;
@@ -476,8 +630,9 @@ export function simulateCombat(data, rng, stats) {
if (!target || !target.alive) return { killed: false, dealt: 0 };
let dealt = amount;
if (target.vuln > 0) dealt = Math.floor(dealt * 1.5);
if (target.weak > 0 && c.attackDamageVsWeakMultiplier && c.attackDamageVsWeakMultiplier > 1) {
dealt = Math.floor(dealt * c.attackDamageVsWeakMultiplier);
const weakMultiplier = Math.max(c.attackDamageVsWeakMultiplier || 1, powerFieldMax('attackDamageVsWeakMultiplier'));
if (target.weak > 0 && weakMultiplier > 1) {
dealt = Math.floor(dealt * weakMultiplier);
}
if (c.pierce === true) {
target.hp -= dealt;
@@ -493,6 +648,10 @@ export function simulateCombat(data, rng, stats) {
target.alive = false;
killed = true;
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
if (c.maxHpOnKill) {
playerMaxHp += c.maxHpOnKill;
pHp += c.maxHpOnKill;
}
}
return { killed, dealt };
};
@@ -534,11 +693,11 @@ export function simulateCombat(data, rng, stats) {
roundKilled = resolveAttackRound();
} 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') {
if (recordStats) powers.push(id);
powers.push(id);
} 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 vulnAmount = c.vuln || 0;
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.thorns) pThorns += c.thorns;
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.kind !== 'Attack' && c.comboGain) gainCombo(c.comboGain);
if (c.removePlayerDebuffs === true) { pWeak = 0; pVuln = 0; }
activeKillReward = c.rewardOnKill || 0;
if (c.intangible) pIntangible += c.intangible;
queueNextTurnEffects(c);
@@ -588,6 +749,28 @@ export function simulateCombat(data, rng, stats) {
}
}
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) {
const target = chooseTarget(aliveList(), 0);
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') {
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.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) {
turns++;
turnAttackCardsPlayed = 0;
@@ -655,7 +901,9 @@ export function simulateCombat(data, rng, stats) {
drawDisabledThisTurn = false;
skillCostReductionThisTurn = 0;
// 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
zeroCostCardIdsThisTurn.clear();
if (nextTurnKeepBlock === true) nextTurnKeepBlock = false;
else if (powers.some((pid) => cards[pid]?.powerEffect === 'keepBlock')) {}
else pBlock = 0;
turnAttackMultiplier = nextTurnAttackMultiplier;
nextTurnAttackMultiplier = 1;
@@ -668,6 +916,7 @@ export function simulateCombat(data, rng, stats) {
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
else if (pc.powerEffect === 'keepBlock') {}
else if (pc.powerEffect === 'poisonPerTurn') {
for (const m of mob) if (m.alive) applyPoisonToMonster(m, pc.value);
} else if (pc.powerEffect === 'damagePerTurn') {
@@ -677,6 +926,8 @@ export function simulateCombat(data, rng, stats) {
m.hp = r.hp; m.block = r.block;
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.turnStartDraw) powerTurnDraw += pc.turnStartDraw;
@@ -701,8 +952,14 @@ export function simulateCombat(data, rng, stats) {
skillCostReductionThisTurn,
handCostZeroThisTurn,
combatCardCostReduction,
zeroCostCardIdsThisTurn,
incomingDamage: data.smartPlayer === true ? expectedIncomingDamage() : 0,
currentBlock: pBlock,
turnAttackCardsPlayed,
cardsDrawnThisCombat,
drawPileCards: drawPile,
discardCards: discard,
exhaustCards: exhaust,
});
if (idx < 0) break;
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 finalCost = c.useAllEnergy === true ? cost : Math.max(0, cost - combatReduction);
energy -= finalCost;
hand.splice(idx, 1);
resolveCardEffects(id, c, finalCost);
if (c.kind === 'Attack' && (data.healOnAttack || 0) > 0) {
pHp = Math.min(playerMaxHp, pHp + data.healOnAttack);
@@ -729,9 +987,11 @@ export function simulateCombat(data, rng, stats) {
}
if (c.kind === 'Attack') turnAttackCardsPlayed++;
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
hand.splice(idx, 1);
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);
if (c.combatCostReductionOnPlay && c.combatCostReductionOnPlay > 0) {
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 beforeHp = pHp;
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;
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) {
m.hp -= pThorns;
if (m.hp <= 0) m.alive = false;

View File

@@ -649,9 +649,10 @@ test("simulateCombat: damagePerAttackPlayedThisTurn scales Finisher", () => {
starterDeck: ["Hit", "Finisher"],
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.turns, 2);
assert.ok((stats.Finisher?.damage || 0) >= 6);
});
test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applied", () => {
@@ -666,9 +667,11 @@ test("simulateCombat: damagePerOtherHandCard and damagePerSkillInHand are applie
starterDeck: ["Skill1", "Skill2", "Blank", "Precise", "Flechettes"],
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.turns, 5);
assert.ok((stats.Precise?.damage || 0) >= 5);
assert.ok((stats.Flechettes?.damage || 0) >= 10);
});
test("simulateCombat: damagePerDiscardedThisTurn and bonusHitsWhenOtherHandAtLeast work", () => {
@@ -1099,6 +1102,90 @@ test("simulateCombat: blockPerDamageDealtThisTurn grants block from damage dealt
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", () => {
const data = {
cards: {
@@ -1124,6 +1211,95 @@ test("simulateCombat: rewardOnKill grants an extra reward screen when an attack
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", () => {
const data = {
cards: {
@@ -1215,3 +1391,102 @@ test("simulateCombat: shivAoe makes Shivs hit all enemies", () => {
assert.equal(r.win, true);
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: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
], 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

@@ -49,6 +49,8 @@ local skillFree = false
local skillRepeat = 0
if self.HandCostZeroThisTurn == true then
cost = 0
elseif self.ZeroCostCardIdsThisTurn ~= nil and self.ZeroCostCardIdsThisTurn[cardId] == true then
cost = 0
elseif c.useAllEnergy == true then
cost = self.Energy
end
@@ -71,6 +73,8 @@ if self.Energy < cost then
end
self.Energy = self.Energy - cost
self.ActiveKillReward = c.rewardOnKill or 0
self.ActiveKillMaxHpGain = c.maxHpOnKill or 0
table.remove(self.Hand, slot)
self:ResolveCardEffects(cardId, slot, c, false, cost)
local function applyCardPlayHooks()
if self:HasPowerField("cardPlayedBlock") == true then
@@ -82,6 +86,8 @@ local function applyCardPlayHooks()
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
@@ -106,16 +112,19 @@ 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
if c.combatCostReductionOnPlay ~= nil and c.combatCostReductionOnPlay > 0 then
if self.CombatCardCostReduction == nil then
self.CombatCardCostReduction = {}
end
self.CombatCardCostReduction[cardId] = (self.CombatCardCostReduction[cardId] or 0) + c.combatCostReductionOnPlay
end
table.remove(self.Hand, slot)
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
@@ -163,8 +172,8 @@ for i = 1, #self.Monsters do
local m = self.Monsters[i]
local active = false
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" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i) .. "/TargetMarker", active and dragActive)
self:SetEntityEnabled("/ui/RunUIGroup/CombatHud/MonsterStatus" .. self:IntStr(i) .. "/TargetMarker/Label", active and dragActive)
end`),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
@@ -177,7 +186,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
self.CardHoverTweenId = 0
end
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
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
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
return
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
local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
@@ -211,7 +220,7 @@ end`, [
return
end
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
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
@@ -288,6 +297,7 @@ if m.block > 0 and pierce ~= true then
m.block = m.block - absorbed
dmg = dmg - absorbed
end
self:ShakeCombatCamera()
m.hp = m.hp - dmg
if dmg > 0 then
local poison = self:AddPowerFieldTotal("attackPoison")
@@ -301,6 +311,13 @@ local killed = false
if m.hp <= 0 then
m.hp = 0
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
end
return killed`, [
@@ -317,6 +334,7 @@ end
if m == nil then
return false
end
self:ShakeCombatCamera()
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot)
@@ -343,6 +361,7 @@ local m = alive[math.random(1, #alive)]
if m == nil then
return false
end
self:ShakeCombatCamera()
m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot)
@@ -368,7 +387,7 @@ local burstEvery = self:AddPowerFieldTotal("poisonApplicationBurstEvery")
local burstDamage = self:AddPowerFieldTotal("poisonApplicationBurstDamage")
if burstEvery ~= nil and burstEvery > 0 and burstDamage ~= nil and burstDamage > 0 then
if (self.PoisonApplicationsThisCombat % burstEvery) == 0 then
self:DealDamageToAllMonsters(burstDamage)
self:DealDamageToAllMonsters(burstDamage, false)
end
end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'target' },
@@ -377,6 +396,17 @@ end`, [
method('DealDamageToAllMonsters', `if self.Monsters == nil then
return false
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
for i = 1, #self.Monsters do
local m = self.Monsters[i]
@@ -412,6 +442,11 @@ end
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
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:CheckCombatEnd()
return killCount > 0`, [
@@ -420,6 +455,13 @@ return killCount > 0`, [
], 0, 'boolean'),
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
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.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat()
@@ -451,12 +493,9 @@ _TimerService:SetTimerOnce(function()
shown = math.floor(shown * self.ActiveAttackDamageVsWeakMultiplier)
end
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.ActiveKillMaxHpGain = 0
self.ActiveAttackDamageVsWeakMultiplier = 1
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat()
self:CheckCombatEnd()
end, 0.35)`, [
@@ -511,10 +550,8 @@ _TimerService:SetTimerOnce(function()
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.ActiveKillMaxHpGain = 0
self.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat()
self:CheckCombatEnd()
@@ -531,11 +568,16 @@ if m.entity ~= nil and isvalid(m.entity) then
local ent = m.entity
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
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
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
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
local absorbed = math.min(self.PlayerBlock, dmg)
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
dmg = 1
end
self:ShakeCombatCamera()
if dmg > 0 then
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
if self:HasRelic("bronzeScales") then
reflect = reflect + 3
@@ -567,7 +621,7 @@ if dmg > 0 then
end
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
self.FirstHpLossDone = true
self:DrawCards(3)
self:DrawCards(3, true)
self:RenderHand(false)
end
end
@@ -588,7 +642,7 @@ if idx == 0 or self.PlayerHp <= 0 then
return
end
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)
_TimerService:SetTimerOnce(function()
local poisonTicks = 1

View File

@@ -71,20 +71,65 @@ if allDeckClose ~= nil and (allDeckClose.ButtonComponent ~= nil or allDeckClose:
self.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)
end
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
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.SpriteGUIRendererComponent ~= nil then
allCard.SpriteGUIRendererComponent.RaycastTarget = true
end
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)
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
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
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(UITouchExitEvent, function() self:SetCardHover(cardPath, false) end)
cardEntity:ConnectEvent(UITouchBeginDragEvent, function(ev) self:OnCardDragBegin(i) end)
@@ -98,13 +143,15 @@ for i = 1, 10 do
end
end
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
rc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)
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(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
@@ -127,13 +174,15 @@ for i = 1, #mapNodeIds do
end
end
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
sc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)
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(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
@@ -150,13 +199,13 @@ if restLeave ~= nil and (restLeave.ButtonComponent ~= nil or restLeave:AddCompon
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
end
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
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
end
end
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
local idx = i
rs:ConnectEvent(UITouchEnterEvent, function()
@@ -170,7 +219,7 @@ for i = 1, 10 do
end
end
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
local idx = i
ps:ConnectEvent(UITouchEnterEvent, function()
@@ -218,7 +267,7 @@ if jcJob ~= nil and (jcJob.ButtonComponent ~= nil or jcJob:AddComponent("ButtonC
end
for i = 1, 3 do
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
jb:ConnectEvent(ButtonClickEvent, function()
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
@@ -243,6 +292,7 @@ self.BlockGainMultiplier = 1
self:ApplyRelics("turnStart")
if self.NextTurnKeepBlock == true then
self.NextTurnKeepBlock = false
elseif self:HasPowerEffect("keepBlock") == true then
else
self.PlayerBlock = 0
end
@@ -258,6 +308,7 @@ self.ActiveAttackDamageVsWeakMultiplier = 1
self.DrawDamageThisTurn = 0
self.DrawPoisonThisTurn = 0
self.ShivAoeThisCombat = false
self.ZeroCostCardIdsThisTurn = {}
self.SkillSlyOnPlayCards = self.SkillSlyOnPlayCards or {}
self.TurnSkillSlyCards = {}
self.EnemyStrengthLossThisTurn = 0
@@ -275,6 +326,7 @@ if self.PlayerPowers ~= nil then
self.Energy = self.Energy + pc.value
elseif pc.powerEffect == "blockPerTurn" then
self.PlayerBlock = self.PlayerBlock + pc.value
elseif pc.powerEffect == "keepBlock" then
elseif pc.powerEffect == "poisonPerTurn" then
if self.Monsters ~= nil then
for j = 1, #self.Monsters do
@@ -288,6 +340,8 @@ if self.PlayerPowers ~= nil then
if self.Monsters ~= nil then
self:PlayAoeFx(pc.fx or pc.image, pc.value or 0)
end
elseif pc.powerEffect == "healPerTurn" then
self.PlayerHp = math.min(self.PlayerMaxHp, self.PlayerHp + pc.value)
end
if pc.turnStartShiv ~= nil then
self:AddCardsToHand("Shiv", pc.turnStartShiv)
@@ -316,7 +370,7 @@ if self.NextTurnAddCards ~= nil then
end
local drawN = 5 + (self.NextTurnDraw or 0) + powerTurnDraw
self.NextTurnDraw = 0
self:DrawCards(drawN)
self:DrawCards(drawN, true)
self:RenderHand(true)
self:RenderCombat()
if powerTurnDiscard > 0 then
@@ -481,8 +535,11 @@ for i = 1, amount do
\t\tself:TriggerSly(cardId)
\telse
\t\ttable.insert(self.Hand, cardId)
\t\tdrewAny = true
\t\ttable.insert(drawnSlots, #self.Hand)
\t\tlocal autoPlayed = self:TriggerDrawnCardAutoPlay(cardId)
\t\tif autoPlayed ~= true then
\t\t\tdrewAny = true
\t\t\ttable.insert(drawnSlots, #self.Hand)
\t\tend
\tend
end
self:RenderPiles()
@@ -495,8 +552,9 @@ if animate == true and #drawnSlots > 0 then
\t\tlocal slot = drawnSlots[i]
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
\tend
end
return drawnCards
end`, [
`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
], 0, 'any'),

View File

@@ -44,7 +44,7 @@ end
self:SetText("/ui/DeckUIGroup/DeckInspectHud/Title", title .. suffix)
self:SetEntityEnabled("/ui/DeckUIGroup/DeckInspectHud/Empty", count <= 0)
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
if pile ~= nil then
cardId = pile[i]
@@ -59,7 +59,7 @@ end`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
{ 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: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
]),
@@ -181,7 +181,6 @@ end
self.DeckInspectKind = ""
self.ClassDeckMode = false
self.ClassDeckClass = ""
self.CodexMode = false
self.DebugCardPickerMode = false
self:RenderClassDeckTabs()
self.DeckAllOpen = true
@@ -212,9 +211,6 @@ local title = "모든 덱"
if self.ClassDeckMode == true then
pile = self.ClassDeckCards or {}
title = self.ClassDeckTitle
if self.CodexMode == true then
title = "카드 도감"
end
if self.DebugCardPickerMode == true then
title = title .. " - 테스트 카드 추가"
end
@@ -227,7 +223,7 @@ self:SetText("/ui/DeckUIGroup/DeckAllHud/Title", title .. " (" .. tostring(count
self:RenderClassDeckTabs()
self:SetEntityEnabled("/ui/DeckUIGroup/DeckAllHud/Empty", count <= 0)
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]
if cardId == nil then
self:SetEntityEnabled(path, false)
@@ -236,7 +232,7 @@ for i = 1, 120 do
self:ApplyAllDeckCardVisual(i, cardId)
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: '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 drawStart = Vector2(-590, 8)
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
\t\tlocal cardId = self.Hand[i]
\t\tif cardId == nil then
@@ -151,7 +151,7 @@ if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
end
local items = {}
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
local tr = e.UITransformComponent
local tx = xs[i]
@@ -195,7 +195,7 @@ self.CardHoverTweenId = eventId`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
{ 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: '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))
end
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
\treturn
end
@@ -246,10 +246,10 @@ local target = Vector2(590, 8)
local duration = 0.18
for i = 1, #cardIds do
\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
\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\t\tlocal sx = 0
\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)
\tfor i = 1, #cardIds do
\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\t\tlocal sx = 0
\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\tfor i = 1, #cardIds do
\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\t\tif self.Hand ~= nil and self.Hand[slot] ~= nil then
\t\t\t\t\te.Enable = true
@@ -313,6 +313,108 @@ if amount < 0 then
end
self.PlayerBlock = self.PlayerBlock + amount
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
return 0
end
@@ -326,7 +428,104 @@ for i = 1, #self.Hand do
end
end
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
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
if self.Hand ~= nil then
otherHand = #self.Hand - 1
@@ -365,6 +564,200 @@ return base2`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'c' },
], 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
self.FightAttackCount = self.FightAttackCount + 1
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
end
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()
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
baseDmg = xEnergy * c.xDamagePerEnergy
end
local total = 0
local hitDamages = {}
local hitN = c.hits or 1
if c.otherHandAtLeast ~= nil and c.bonusHitsWhenOtherHandAtLeast ~= nil then
local otherHand = 0
@@ -505,7 +899,9 @@ if c.kind == "Attack" then
end
end
for h = 1, hitN do
total = total + self:CalcPlayerAttack(baseDmg)
local hitDamage = self:CalcPlayerAttack(baseDmg)
hitDamages[h] = hitDamage
total = total + hitDamage
end
local useAoe = c.aoe == true
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
self.ShivFirstDamageBonusUsed = true
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
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
self:RunAttackSequence(hitDamages, total, useAoe, c.randomTargetEachHit == true, c.pierce == true, c.repeatOnKill == true)
end
if c.block ~= nil then
self:AddCardBlock(c.block)
self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0))
end
if free ~= true then
self:ApplyRelics("cardPlayed")
end
elseif c.kind == "Skill" then
if c.block ~= nil then
self:AddCardBlock(c.block)
self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0))
end
elseif c.kind == "Power" then
if free ~= true then
@@ -597,11 +940,19 @@ if c.selfVuln ~= nil then
self.PlayerVuln = self.PlayerVuln + c.selfVuln
end
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
if c.gainEnergy ~= nil and c.gainEnergy ~= 0 then
self.Energy = self.Energy + c.gainEnergy
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
self.PlayerIntangible = (self.PlayerIntangible or 0) + c.intangible
end
@@ -719,6 +1070,23 @@ if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
end
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
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`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
{ 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
self.PlayerStr = self.PlayerStr + r.value
elseif r.effect == "draw" then
self:DrawCards(r.value)
self:DrawCards(r.value, true)
self:RenderHand(false)
elseif r.effect == "heal" or r.effect == "healOnAttack" or r.effect == "healOnWin" then
self.PlayerHp = self.PlayerHp + r.value
@@ -95,7 +95,7 @@ if self:AddPotion(pid) == true then
self:Toast("물약 획득: " .. p.name)
end`),
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)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local pid = nil
@@ -188,7 +188,7 @@ if self.RunRelics ~= nil then
count = #self.RunRelics
end
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)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
local rid = nil

View File

@@ -71,7 +71,7 @@ if opts == nil then
end
self.JobOpts = opts
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]
if o ~= nil then
self:SetEntityEnabled(base, true)

View File

@@ -14,7 +14,7 @@ end
local worldPos = transform.WorldPosition
local screen = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + ${HEAD_OFFSET_Y}))
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
slotEntity.UITransformComponent.anchoredPosition = uipos
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),

View File

@@ -162,7 +162,7 @@ if node ~= nil then
end
end
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
d.Enable = has
if has == true and d.SpriteGUIRendererComponent ~= nil then

View File

@@ -15,7 +15,7 @@ return table.concat(parts, " ")`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
], 0, 'string'),
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]
if m ~= nil and m.alive == true then
self:SetEntityEnabled(base, true)
@@ -80,6 +80,14 @@ if self.PlayerThorns ~= nil and self.PlayerThorns > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "가시" .. tostring(self.PlayerThorns)
end
if self.ComboCount ~= nil and self.ComboCount > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "콤보 " .. tostring(self.ComboCount) .. "/" .. tostring(self:GetComboMax())
end
if self.HolyChargeCount ~= nil and self.HolyChargeCount > 0 then
if pb ~= "" then pb = pb .. " " end
pb = pb .. "홀리 차지 " .. tostring(self.HolyChargeCount) .. "/" .. tostring(self:GetHolyChargeMax())
end
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
local names = {}
for i = 1, #self.PlayerPowers do
@@ -92,13 +100,21 @@ end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Buffs", pb)
self:RenderRun()`),
method('ShowDmgPop', `local slotKey = string.format("%d", math.floor(slot or 0))
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey
if self.DmgPopSlotQueue == nil then
self.DmgPopSlotQueue = {}
end
local popIndex = (self.DmgPopSlotQueue[slotKey] or 0) + 1
if popIndex > 5 then
popIndex = 1
end
self.DmgPopSlotQueue[slotKey] = popIndex
local base = "/ui/RunUIGroup/CombatHud/DmgPop" .. slotKey .. "_" .. self:IntStr(popIndex)
local pop = _EntityService:GetEntityByPath(base)
if pop == nil then
return
end
self.DmgPopSeq = (self.DmgPopSeq or 0) + 1
local popSeq = self.DmgPopSeq
local function showNow()
self:SetEntityEnabled(base, false)
self:SetText(base, "")
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
local shown = tostring(math.max(0, math.floor(amount)))
@@ -106,16 +122,17 @@ if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
end
local digits = {}
local digitPathsToEnable = {}
for i = 1, string.len(shown) do
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
end
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
self:SetEntityEnabled(base .. "/Digit" .. tostring(i), false)
self:SetEntityEnabled(base .. "/Digit" .. self:IntStr(i), false)
end
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitPath = base .. "/Digit" .. tostring(i)
local digitPath = base .. "/Digit" .. self:IntStr(i)
local digitEntity = _EntityService:GetEntityByPath(digitPath)
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
if digits[i] ~= nil then
@@ -124,7 +141,7 @@ for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
if digitEntity.UITransformComponent ~= nil then
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
end
self:SetEntityEnabled(digitPath, true)
table.insert(digitPathsToEnable, digitPath)
else
self:SetEntityEnabled(digitPath, false)
end
@@ -143,7 +160,7 @@ else
popPos = Vector2(sp.x, sp.y + 76)
end
end
if pop ~= nil and pop.UITransformComponent ~= nil then
if pop.UITransformComponent ~= nil then
if popPos ~= nil then
pop.UITransformComponent.anchoredPosition = popPos
else
@@ -151,35 +168,168 @@ if pop ~= nil and pop.UITransformComponent ~= nil then
end
end
self:SetEntityEnabled(base, true)
for i = 1, #digitPathsToEnable do
self:SetEntityEnabled(digitPathsToEnable[i], true)
end
for i = 1, 6 do
_TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then
return
end
local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.UITransformComponent ~= nil then
local cur = p.UITransformComponent.anchoredPosition
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
end
end, 0.045 * i)
local alpha = 1 - (i / 6)
if alpha < 0 then alpha = 0 end
for di = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(di))
if digitEntity ~= nil and digitEntity.Enable == true and digitEntity.SpriteGUIRendererComponent ~= nil then
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, alpha)
end
end
end, 0.05 * i)
end
_TimerService:SetTimerOnce(function()
if self.DmgPopSeq ~= popSeq then
return
end
self:SetEntityEnabled(base, false)
end, 0.48)`, [
end, 0.3)
end
showNow()`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]),
method('ShowPlayerDmgPop', `local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop"
if amount > 0 then
self:SetText(base, "-" .. string.format("%d", amount))
else
self:SetText(base, "막음")
method('ShowPlayerDmgPop', `local shownAmount = math.max(0, math.floor(amount or 0))
if shownAmount <= 0 then
return
end
self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
local function setEnabled(entity, enabled)
if entity == nil then
return
end
entity.Enable = enabled
if entity.SpriteGUIRendererComponent ~= nil then
entity.SpriteGUIRendererComponent.Enable = enabled
end
if entity.TextComponent ~= nil then
entity.TextComponent.Enable = enabled
end
end
self.PlayerDmgPopIndex = (self.PlayerDmgPopIndex or 0) + 1
if self.PlayerDmgPopIndex > 4 then
self.PlayerDmgPopIndex = 1
end
local base = "/ui/RunUIGroup/CombatHud/PlayerPanel/DmgPop_" .. self:IntStr(self.PlayerDmgPopIndex)
local pop = _EntityService:GetEntityByPath(base)
if pop == nil then
return
end
setEnabled(pop, false)
local damageDigitRuids = { ${DAMAGE_DIGIT_RUIDS.map(luaStr).join(', ')} }
local shown = tostring(shownAmount)
if string.len(shown) > ${DAMAGE_POP_MAX_DIGITS} then
shown = string.sub(shown, 1, ${DAMAGE_POP_MAX_DIGITS})
end
local digits = {}
local digitEntitiesToEnable = {}
for i = 1, string.len(shown) do
table.insert(digits, tonumber(string.sub(shown, i, i)) or 0)
end
local totalW = #digits * ${DAMAGE_POP_DIGIT_W} + math.max(0, #digits - 1) * ${DAMAGE_POP_DIGIT_SPACING}
local startX = -totalW / 2 + ${DAMAGE_POP_DIGIT_W} / 2
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(i))
setEnabled(digitEntity, false)
end
for i = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitPath = base .. "/Digit" .. self:IntStr(i)
local digitEntity = _EntityService:GetEntityByPath(digitPath)
if digitEntity ~= nil and digitEntity.SpriteGUIRendererComponent ~= nil then
if digits[i] ~= nil then
digitEntity.SpriteGUIRendererComponent.ImageRUID = damageDigitRuids[digits[i] + 1]
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
if digitEntity.UITransformComponent ~= nil then
digitEntity.UITransformComponent.anchoredPosition = Vector2(startX + (i - 1) * (${DAMAGE_POP_DIGIT_W} + ${DAMAGE_POP_DIGIT_SPACING}), 0)
end
table.insert(digitEntitiesToEnable, digitEntity)
end
end
end
local startPos = Vector2(16, 34)
if pop.UITransformComponent ~= nil then
local cur = pop.UITransformComponent.anchoredPosition
if math.abs(cur.x) < 240 and math.abs(cur.y) < 140 then
startPos = cur
end
pop.UITransformComponent.anchoredPosition = startPos
end
setEnabled(pop, true)
for i = 1, #digitEntitiesToEnable do
setEnabled(digitEntitiesToEnable[i], true)
end
for i = 1, 6 do
_TimerService:SetTimerOnce(function()
local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.Enable == true and p.UITransformComponent ~= nil then
local cur = p.UITransformComponent.anchoredPosition
p.UITransformComponent.anchoredPosition = Vector2(cur.x, cur.y + 7)
end
local alpha = 1 - (i / 6)
if alpha < 0 then alpha = 0 end
for di = 1, ${DAMAGE_POP_MAX_DIGITS} do
local digitEntity = _EntityService:GetEntityByPath(base .. "/Digit" .. self:IntStr(di))
if digitEntity ~= nil and digitEntity.Enable == true and digitEntity.SpriteGUIRendererComponent ~= nil then
digitEntity.SpriteGUIRendererComponent.Color = Color(1, 1, 1, alpha)
end
end
end, 0.05 * i)
end
_TimerService:SetTimerOnce(function()
local p = _EntityService:GetEntityByPath(base)
if p ~= nil and p.UITransformComponent ~= nil then
p.UITransformComponent.anchoredPosition = startPos
end
setEnabled(p, false)
end, 0.3)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('ShakeCombatCamera', `local lp = _UserService.LocalPlayer
local cam = nil
if lp ~= nil then
cam = lp.CameraComponent
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
@@ -276,8 +426,28 @@ 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
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
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' },
@@ -293,4 +463,5 @@ if self.AscensionLevel > 0 then
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
hud.Enable = true
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: '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)
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:OfferReward()
return

View File

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

View File

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

View File

@@ -9,17 +9,7 @@ self.ShopBought = { false, false, false }
for i = 1, 3 do
self.ShopChoices[i] = pool[math.random(1, #pool)]
end
local relicChoices = {}
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.ShopRelic = self.RelicPool[math.random(1, #self.RelicPool)]
self.ShopRelicBought = false
local pkeys = {}
for pid, _ in pairs(self.Potions) do
@@ -34,7 +24,7 @@ self:ShowState("shop")`),
for i = 1, 3 do
local cid = self.ShopChoices[i]
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
self:ApplyCardFace(base, cid)
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
@@ -48,7 +38,6 @@ for i = 1, 3 do
end
local rr = self.Relics[self.ShopRelic]
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/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
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)
end
end
else
self:SetEntityEnabled("/ui/RunUIGroup/ShopHud/Relic", false)
end
local pp = self.Potions[self.ShopPotion]
if pp ~= nil then
@@ -78,9 +65,6 @@ end`),
method('BuyRelic', `if self.ShopRelicBought == true then
return
end
if self.ShopRelic == nil or self.ShopRelic == "" then
return
end
if self.Gold < ${RELIC_PRICE} then
return
end

View File

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

View File

@@ -102,6 +102,217 @@ for i = 1, #lines do
out = out .. lines[i]
end
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
return
end
@@ -112,26 +323,21 @@ local cardId = self.Hand[slot]
if cardId == nil then
return
end
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. tostring(slot))
local tx = 0
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
if e ~= nil and e.UITransformComponent ~= nil then
tx = e.UITransformComponent.anchoredPosition.x
e.UITransformComponent.UIScale = Vector3(1.3, 1.3, 1)
end
local c = self.Cards[cardId]
if c ~= nil then
local tip = self:BuildCardKeywordTooltip(c)
if tip ~= "" then
local tipX = tx + 270
if tx > 180 then tipX = tx - 270 end
if tipX > 760 then tipX = tx - 270 end
if tipX < -760 then tipX = tx + 270 end
self:ShowTooltipAt("키워드", tip, tipX, 90)
local anchor = self:GetTooltipAnchorForPath("/ui/RunUIGroup/CardHand/Card" .. self:IntStr(slot))
self:ShowTooltipAt("키워드", tip, anchor.x, anchor.y)
else
self:HideTooltip()
end
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
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end
@@ -141,9 +347,9 @@ self:HideTooltip()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, At
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'x' },
]),
method('ShowTooltipAt', `self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Name", name)
self:SetText("/ui/RunUIGroup/CombatHud/TooltipBox/Desc", desc)
local e = _EntityService:GetEntityByPath("/ui/RunUIGroup/CombatHud/TooltipBox")
method('ShowTooltipAt', `self:SetText("/ui/ToolTipGroup/TooltipBox/Name", name)
self:SetText("/ui/ToolTipGroup/TooltipBox/Desc", desc)
local e = _EntityService:GetEntityByPath("/ui/ToolTipGroup/TooltipBox")
if e ~= nil then
if e.UITransformComponent ~= nil then
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: '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', 'CardHoverTweenId', '0'),
prop('number', 'DmgPopSeq', '0'),
prop('number', 'CombatCameraShakeSeq', '0'),
prop('any', 'DmgPopSlotQueue'),
prop('number', 'PlayerDmgPopIndex', '0'),
prop('any', 'HpBarOrigins'),
prop('any', 'EndTurnHandler'),
prop('any', 'NewGameHandler'),
prop('any', 'WarriorSelectHandler'),
@@ -75,6 +79,8 @@ function writeCodeblocks() {
prop('any', 'ClassDeckCards'),
prop('string', 'ClassDeckTitle', '""'),
prop('string', 'ClassDeckClass', '""'),
prop('string', 'GridHoverPath', '""'),
prop('boolean', 'GridScrollActive', 'false'),
prop('any', 'SoulUnlocks'),
prop('any', 'SoulShopDef'),
prop('boolean', 'SoulShopBound', 'false'),
@@ -142,10 +148,14 @@ function writeCodeblocks() {
prop('number', 'PoisonApplicationsThisCombat', '0'),
prop('number', 'EnemyStrengthLossThisTurn', '0'),
prop('number', 'ActiveKillReward', '0'),
prop('number', 'ActiveKillMaxHpGain', '0'),
prop('number', 'BonusRewardScreens', '0'),
prop('number', 'FightAttackCount', '0'),
prop('number', 'TurnAttackCardsPlayed', '0'),
prop('number', 'TurnCardsPlayedThisTurn', '0'),
prop('number', 'ComboCount', '0'),
prop('number', 'HolyChargeCount', '0'),
prop('boolean', 'DamagePowerStrengthUsed', 'false'),
prop('number', 'DamageDealtThisTurn', '0'),
prop('number', 'TurnDiscardedCards', '0'),
prop('boolean', 'FirstHpLossDone', 'false'),
@@ -173,6 +183,7 @@ function writeCodeblocks() {
prop('boolean', 'NextSkillCostZero', 'false'),
prop('number', 'NextSkillRepeatCount', '0'),
prop('any', 'NextTurnAddCards'),
prop('any', 'ZeroCostCardIdsThisTurn'),
], [
...bootMethods,
...screensMethods,

View File

@@ -19,18 +19,18 @@ for (const cls of Object.keys(CLASSES)) {
// 전직 옵션
const JOBS = {
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: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce', tier: 2, parent: 'warrior' },
{ id: 'fighter', name: '파이터', desc: '콤보와 다단 공격 특화\n공격으로 콤보를 쌓고\n추가타로 압박', starter: 'ComboAttack', tier: 2, parent: 'warrior' },
{ id: 'page', name: '페이지', desc: '홀리 포스와 방어 특화\n홀리 차지를 쌓아\n공격과 생존 강화', starter: 'HolyCharge', tier: 2, parent: 'warrior' },
{ id: 'spearman', name: '스피어맨', desc: '광역·장기전 계열\n대화재 · 소용돌이\n불의 심장', starter: 'Conflagration', tier: 2, parent: 'warrior' },
],
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: [
{ 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: [
{ 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: [
{ 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 },
];
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}`;
}
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
@@ -165,7 +165,7 @@ for (const id of RELICS.relicPool) {
}
function luaRelicsTable(relics) {
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}`;
}
@@ -175,32 +175,38 @@ for (const [pid, p] of Object.entries(POTIONS.potions)) {
}
function luaPotionsTable(potions) {
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}`;
}
function luaIntentsArray(intents) {
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.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(', ')} }`;
}).join(', ') + ' }';
}
function luaEnemiesTable(enemies) {
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}`;
}
function luaStr(s) {
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) {
const cls = Object.entries(jobs).map(([clsId, list]) => {
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},`;
}).join('\n');
return `self.Jobs = {\n${cls}\n}`;
@@ -217,97 +223,133 @@ function luaClassLineagesTable(lineages) {
}
function luaJobMetaTable(meta) {
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}`;
}
function luaCardsTable(cards) {
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)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${c.damagePerOtherHandCard}`);
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${c.damagePerAttackPlayedThisTurn}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${c.damagePerDiscardedThisTurn}`);
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${c.damagePerSkillInHand}`);
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${c.damagePerCardDrawnThisCombat}`);
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${c.firstCardDamageBonus}`);
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
if (c.attackPoison != null) fields.push(`attackPoison = ${c.attackPoison}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${c.otherHandAtLeast}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${c.bonusHitsWhenOtherHandAtLeast}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.blockGainMultiplier != null) fields.push(`blockGainMultiplier = ${c.blockGainMultiplier}`);
if (c.blockPerDamageDealtThisTurn != null) fields.push(`blockPerDamageDealtThisTurn = ${c.blockPerDamageDealtThisTurn}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.dex != null) fields.push(`dex = ${c.dex}`);
if (c.thorns != null) fields.push(`thorns = ${c.thorns}`);
if (c.cardPlayedBlock != null) fields.push(`cardPlayedBlock = ${c.cardPlayedBlock}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
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 = ${luaNum(c.damage)}`);
if (c.damageFromCurrentBlock != null) fields.push(`damageFromCurrentBlock = ${luaNum(c.damageFromCurrentBlock)}`);
if (c.damagePerCombo != null) fields.push(`damagePerCombo = ${luaNum(c.damagePerCombo)}`);
if (c.damagePerHolyCharge != null) fields.push(`damagePerHolyCharge = ${luaNum(c.damagePerHolyCharge)}`);
if (c.attackDamagePerCombo != null) fields.push(`attackDamagePerCombo = ${luaNum(c.attackDamagePerCombo)}`);
if (c.damageNameMatch != null) fields.push(`damageNameMatch = ${luaStr(c.damageNameMatch)}`);
if (c.damagePerOwnedNameMatch != null) fields.push(`damagePerOwnedNameMatch = ${luaNum(c.damagePerOwnedNameMatch)}`);
if (c.damagePerOtherHandCard != null) fields.push(`damagePerOtherHandCard = ${luaNum(c.damagePerOtherHandCard)}`);
if (c.damagePerAttackPlayedThisTurn != null) fields.push(`damagePerAttackPlayedThisTurn = ${luaNum(c.damagePerAttackPlayedThisTurn)}`);
if (c.damagePerDiscardedThisTurn != null) fields.push(`damagePerDiscardedThisTurn = ${luaNum(c.damagePerDiscardedThisTurn)}`);
if (c.damagePerSkillInHand != null) fields.push(`damagePerSkillInHand = ${luaNum(c.damagePerSkillInHand)}`);
if (c.damagePerCardDrawnThisCombat != null) fields.push(`damagePerCardDrawnThisCombat = ${luaNum(c.damagePerCardDrawnThisCombat)}`);
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${luaNum(c.damagePerTurn)}`);
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${luaNum(c.cardPlayedDamage)}`);
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${luaNum(c.cardPlayedRandomDamage)}`);
if (c.attackPlayedDamage != null) fields.push(`attackPlayedDamage = ${luaNum(c.attackPlayedDamage)}`);
if (c.firstCardDamageBonus != null) fields.push(`firstCardDamageBonus = ${luaNum(c.firstCardDamageBonus)}`);
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${luaNum(c.rewardOnKill)}`);
if (c.maxHpOnKill != null) fields.push(`maxHpOnKill = ${luaNum(c.maxHpOnKill)}`);
if (c.intangible != null) fields.push(`intangible = ${luaNum(c.intangible)}`);
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${luaNum(c.endTurnDexLoss)}`);
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${luaNum(c.poisonPerTurn)}`);
if (c.attackPoison != null) fields.push(`attackPoison = ${luaNum(c.attackPoison)}`);
if (c.otherHandAtLeast != null) fields.push(`otherHandAtLeast = ${luaNum(c.otherHandAtLeast)}`);
if (c.bonusHitsWhenOtherHandAtLeast != null) fields.push(`bonusHitsWhenOtherHandAtLeast = ${luaNum(c.bonusHitsWhenOtherHandAtLeast)}`);
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.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 누락`);
fields.push(`class = ${luaStr(c.class)}`);
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.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.draw != null) fields.push(`draw = ${c.draw}`);
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
if (c.drawDamage != null) fields.push(`drawDamage = ${c.drawDamage}`);
if (c.drawPoison != null) fields.push(`drawPoison = ${c.drawPoison}`);
if (c.heal != null) fields.push(`heal = ${c.heal}`);
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
if (c.poison != null) fields.push(`poison = ${c.poison}`);
if (c.discard != null) fields.push(`discard = ${c.discard}`);
if (c.selfVuln != null) fields.push(`selfVuln = ${luaNum(c.selfVuln)}`);
if (c.draw != null) fields.push(`draw = ${luaNum(c.draw)}`);
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${luaNum(c.drawUntilHandSize)}`);
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${luaNum(c.drawSkillBlock)}`);
if (c.drawDamage != null) fields.push(`drawDamage = ${luaNum(c.drawDamage)}`);
if (c.drawPoison != null) fields.push(`drawPoison = ${luaNum(c.drawPoison)}`);
if (c.exhaustHandNonAttack === true) fields.push('exhaustHandNonAttack = true');
if (c.exhaustHandAll === true) fields.push('exhaustHandAll = true');
if (c.drawPerExhausted != null) fields.push(`drawPerExhausted = ${luaNum(c.drawPerExhausted)}`);
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.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${c.drawPerDiscarded}`);
if (c.addShiv != null) fields.push(`addShiv = ${c.addShiv}`);
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${c.turnStartShiv}`);
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${c.turnStartDraw}`);
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${c.turnStartDiscard}`);
if (c.drawPerDiscarded != null) fields.push(`drawPerDiscarded = ${luaNum(c.drawPerDiscarded)}`);
if (c.addShiv != null) fields.push(`addShiv = ${luaNum(c.addShiv)}`);
if (c.turnStartShiv != null) fields.push(`turnStartShiv = ${luaNum(c.turnStartShiv)}`);
if (c.turnStartDraw != null) fields.push(`turnStartDraw = ${luaNum(c.turnStartDraw)}`);
if (c.turnStartDiscard != null) fields.push(`turnStartDiscard = ${luaNum(c.turnStartDiscard)}`);
if (c.handCostZeroThisTurn === true) fields.push('handCostZeroThisTurn = true');
if (c.drawDisabledThisTurn === true) fields.push('drawDisabledThisTurn = true');
if (c.addShivPerDiscard === true) fields.push('addShivPerDiscard = true');
if (c.useAllEnergy === true) fields.push('useAllEnergy = true');
if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${c.shivDamageBonus}`);
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${c.firstShivDamageBonus}`);
if (c.shivDamageBonus != null) fields.push(`shivDamageBonus = ${luaNum(c.shivDamageBonus)}`);
if (c.firstShivDamageBonus != null) fields.push(`firstShivDamageBonus = ${luaNum(c.firstShivDamageBonus)}`);
if (c.shivRetain === true) fields.push('shivRetain = true');
if (c.shivAoe === true) fields.push('shivAoe = true');
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${c.attackDamageVsWeakMultiplier}`);
if (c.poisonHits != null) fields.push(`poisonHits = ${c.poisonHits}`);
if (c.attackDamageVsWeakMultiplier != null) fields.push(`attackDamageVsWeakMultiplier = ${luaNum(c.attackDamageVsWeakMultiplier)}`);
if (c.poisonHits != null) fields.push(`poisonHits = ${luaNum(c.poisonHits)}`);
if (c.poisonRandomTargets === true) fields.push('poisonRandomTargets = true');
if (c.poisonIfTargetPoisoned === true) fields.push('poisonIfTargetPoisoned = true');
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${c.xDamagePerEnergy}`);
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${c.xWeakPerEnergy}`);
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${c.nextTurnBlock}`);
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${c.nextTurnDraw}`);
if (c.xDamagePerEnergy != null) fields.push(`xDamagePerEnergy = ${luaNum(c.xDamagePerEnergy)}`);
if (c.xWeakPerEnergy != null) fields.push(`xWeakPerEnergy = ${luaNum(c.xWeakPerEnergy)}`);
if (c.nextTurnBlock != null) fields.push(`nextTurnBlock = ${luaNum(c.nextTurnBlock)}`);
if (c.nextTurnDraw != null) fields.push(`nextTurnDraw = ${luaNum(c.nextTurnDraw)}`);
if (c.nextTurnKeepBlock === true) fields.push('nextTurnKeepBlock = true');
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${c.nextTurnAttackMultiplier}`);
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
if (c.nextTurnAttackMultiplier != null) fields.push(`nextTurnAttackMultiplier = ${luaNum(c.nextTurnAttackMultiplier)}`);
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${luaNum(c.nextTurnCopies)}`);
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
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.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.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${c.turnHandSlyCount}`);
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${c.combatCostReductionOnPlay}`);
if (c.turnHandSlyCount != null) fields.push(`turnHandSlyCount = ${luaNum(c.turnHandSlyCount)}`);
if (c.combatCostReductionOnPlay != null) fields.push(`combatCostReductionOnPlay = ${luaNum(c.combatCostReductionOnPlay)}`);
if (c.randomTargetEachHit === true) fields.push('randomTargetEachHit = true');
if (c.repeatOnKill === true) fields.push('repeatOnKill = true');
if (c.affectsAllEnemies === true) fields.push('affectsAllEnemies = true');
if (c.removeEnemyBlock === true) fields.push('removeEnemyBlock = true');
if (c.removeEnemyArtifact === true) fields.push('removeEnemyArtifact = true');
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${c.enemyStrengthLossThisTurn}`);
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${c.extraPoisonTicks}`);
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${c.poisonApplicationBurstEvery}`);
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${c.poisonApplicationBurstDamage}`);
if (c.enemyStrengthLossThisTurn != null) fields.push(`enemyStrengthLossThisTurn = ${luaNum(c.enemyStrengthLossThisTurn)}`);
if (c.extraPoisonTicks != null) fields.push(`extraPoisonTicks = ${luaNum(c.extraPoisonTicks)}`);
if (c.poisonApplicationBurstEvery != null) fields.push(`poisonApplicationBurstEvery = ${luaNum(c.poisonApplicationBurstEvery)}`);
if (c.poisonApplicationBurstDamage != null) fields.push(`poisonApplicationBurstDamage = ${luaNum(c.poisonApplicationBurstDamage)}`);
if (c.innate === true) fields.push('innate = true');
if (c.playableWhenDrawPileEmpty === true) fields.push('playableWhenDrawPileEmpty = 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.curse === true) fields.push('curse = 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.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`;

View File

@@ -17,6 +17,10 @@ const POWER_FIELDS = [
'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe',
'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier',
'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage',
'comboOnAttack', 'comboMax', 'attackDamagePerCombo', 'attackPlayedDamage', 'attackWeak',
'holyChargeOnHolyForce', 'holyChargeMax', 'damageTakenReduction',
'blockOnDamaged', 'strengthOnDamagedOnce',
'drawOnExhaust', 'drawNameMatchAutoPlay',
'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage',
'skillSlyOnPlay', 'endTurnDexLoss',
];
@@ -28,7 +32,7 @@ for (const [id, c] of Object.entries(cards)) {
issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`);
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)`);
}
if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) {

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 2,
"displayOrder": 3,
"pathConstraints": "//",
"revision": 1,
"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
}
}
]
}
}