Fraser commited on
Commit
38d460f
·
1 Parent(s): 1f711bf

describe abilities

Browse files
src/lib/components/Battle/ActionViewSelector.svelte CHANGED
@@ -5,6 +5,7 @@
5
  <script lang="ts">
6
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
  import type { BattleState } from '$lib/battle-engine/types';
 
8
 
9
  export let currentView: ActionView = 'main';
10
  export let onViewChange: (view: ActionView) => void;
@@ -95,9 +96,9 @@
95
  </div>
96
  <div class="move-desc" class:disabled={isDisabled}>
97
  {#if battleMove?.effects && battleMove.effects.length > 0}
98
- {battleMove.effects.length} effects • {move.description}
99
  {:else}
100
- {move.description}
101
  {/if}
102
  </div>
103
  {#if battleMove?.flags && battleMove.flags.length > 0}
 
5
  <script lang="ts">
6
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
  import type { BattleState } from '$lib/battle-engine/types';
8
+ import { generateMoveDescription } from '$lib/utils/moveDescriptions';
9
 
10
  export let currentView: ActionView = 'main';
11
  export let onViewChange: (view: ActionView) => void;
 
96
  </div>
97
  <div class="move-desc" class:disabled={isDisabled}>
98
  {#if battleMove?.effects && battleMove.effects.length > 0}
99
+ {battleMove.effects.length} effects • {generateMoveDescription(battleMove || move)}
100
  {:else}
101
+ {generateMoveDescription(battleMove || move)}
102
  {/if}
103
  </div>
104
  {#if battleMove?.flags && battleMove.flags.length > 0}
src/lib/components/Piclets/AbilityDisplay.svelte CHANGED
@@ -1,5 +1,6 @@
1
  <script lang="ts">
2
  import type { SpecialAbility, BattleEffect, Trigger } from '$lib/battle-engine/types';
 
3
 
4
  interface Props {
5
  ability: SpecialAbility;
@@ -124,7 +125,7 @@
124
  <span class="ability-icon">✨</span>
125
  <div class="ability-info">
126
  <h3 class="ability-name">{ability.name}</h3>
127
- <p class="ability-description">{ability.description}</p>
128
  </div>
129
  </div>
130
 
 
1
  <script lang="ts">
2
  import type { SpecialAbility, BattleEffect, Trigger } from '$lib/battle-engine/types';
3
+ import { generateAbilityDescription } from '$lib/utils/moveDescriptions';
4
 
5
  interface Props {
6
  ability: SpecialAbility;
 
125
  <span class="ability-icon">✨</span>
126
  <div class="ability-info">
127
  <h3 class="ability-name">{ability.name}</h3>
128
+ <p class="ability-description">{generateAbilityDescription(ability)}</p>
129
  </div>
130
  </div>
131
 
src/lib/components/Piclets/MoveDisplay.svelte CHANGED
@@ -1,6 +1,7 @@
1
  <script lang="ts">
2
  import type { BattleMove as DBBattleMove } from '$lib/db/schema';
3
  import type { Move, BattleEffect } from '$lib/battle-engine/types';
 
4
 
5
  interface Props {
6
  move: DBBattleMove;
@@ -141,9 +142,6 @@
141
  <span class="power-badge">⚡{move.power}</span>
142
  {/if}
143
  </div>
144
- <div class="move-type-badge" style="background-color: {typeColor}">
145
- {move.type.toUpperCase()}
146
- </div>
147
  </div>
148
  </div>
149
 
@@ -170,7 +168,7 @@
170
  </div>
171
 
172
  <div class="move-description">
173
- {move.description}
174
  </div>
175
 
176
  {#if battleMove?.flags && battleMove.flags.length > 0}
@@ -285,16 +283,6 @@
285
  font-weight: 600;
286
  }
287
 
288
- .move-type-badge {
289
- display: inline-block;
290
- padding: 2px 8px;
291
- border-radius: 12px;
292
- color: white;
293
- font-size: 10px;
294
- font-weight: 600;
295
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
296
- align-self: flex-start;
297
- }
298
 
299
  .move-stats {
300
  display: flex;
 
1
  <script lang="ts">
2
  import type { BattleMove as DBBattleMove } from '$lib/db/schema';
3
  import type { Move, BattleEffect } from '$lib/battle-engine/types';
4
+ import { generateMoveDescription } from '$lib/utils/moveDescriptions';
5
 
6
  interface Props {
7
  move: DBBattleMove;
 
142
  <span class="power-badge">⚡{move.power}</span>
143
  {/if}
144
  </div>
 
 
 
145
  </div>
146
  </div>
147
 
 
168
  </div>
169
 
170
  <div class="move-description">
171
+ {generateMoveDescription(battleMove || move)}
172
  </div>
173
 
174
  {#if battleMove?.flags && battleMove.flags.length > 0}
 
283
  font-weight: 600;
284
  }
285
 
 
 
 
 
 
 
 
 
 
 
286
 
287
  .move-stats {
288
  display: flex;
src/lib/utils/moveDescriptions.ts ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Utility for generating descriptive text for battle moves based on their properties
3
+ */
4
+
5
+ import type { BattleMove } from '$lib/db/schema';
6
+ import type { Move, BattleEffect, StatModification, MoveFlag, SpecialAbility, Trigger } from '$lib/battle-engine/types';
7
+
8
+ /**
9
+ * Generate a natural language description for a move based on its properties
10
+ */
11
+ export function generateMoveDescription(move: BattleMove | Move): string {
12
+ const parts: string[] = [];
13
+
14
+ // Start with power assessment
15
+ if (move.power > 0) {
16
+ const powerDescription = getPowerDescription(move.power);
17
+ parts.push(`A ${powerDescription} ${move.type} attack`);
18
+ } else {
19
+ // Non-damaging move
20
+ parts.push(`A ${move.type} support move`);
21
+ }
22
+
23
+ // Add accuracy information if not perfect
24
+ if (move.accuracy < 100) {
25
+ const accuracyDescription = getAccuracyDescription(move.accuracy);
26
+ parts.push(`with ${accuracyDescription} accuracy`);
27
+ }
28
+
29
+ // Add priority information
30
+ if (move.priority > 0) {
31
+ parts.push(`that strikes with increased priority`);
32
+ } else if (move.priority < 0) {
33
+ parts.push(`that moves with reduced priority`);
34
+ }
35
+
36
+ // Add flag-based descriptions
37
+ if (move.flags.length > 0) {
38
+ const flagDescriptions = move.flags.map(flag => getFlagDescription(flag)).filter(Boolean);
39
+ if (flagDescriptions.length > 0) {
40
+ parts.push(flagDescriptions.join(', '));
41
+ }
42
+ }
43
+
44
+ // Add effects description
45
+ if ('effects' in move && move.effects && move.effects.length > 0) {
46
+ const effectDescriptions = getEffectsDescription(move.effects);
47
+ if (effectDescriptions.length > 0) {
48
+ parts.push(effectDescriptions);
49
+ }
50
+ }
51
+
52
+ // Join all parts together
53
+ let description = parts[0] || 'A mysterious move';
54
+ if (parts.length > 1) {
55
+ const additionalParts = parts.slice(1);
56
+ description += ' ' + additionalParts.join(' ');
57
+ }
58
+
59
+ return description + '.';
60
+ }
61
+
62
+ /**
63
+ * Convert power value to descriptive text
64
+ */
65
+ function getPowerDescription(power: number): string {
66
+ if (power <= 40) return 'weak';
67
+ if (power <= 60) return 'modest';
68
+ if (power <= 80) return 'strong';
69
+ if (power <= 100) return 'powerful';
70
+ if (power <= 120) return 'devastating';
71
+ return 'overwhelming';
72
+ }
73
+
74
+ /**
75
+ * Convert accuracy to descriptive text
76
+ */
77
+ function getAccuracyDescription(accuracy: number): string {
78
+ if (accuracy >= 95) return 'near-perfect';
79
+ if (accuracy >= 85) return 'reliable';
80
+ if (accuracy >= 75) return 'moderate';
81
+ if (accuracy >= 65) return 'shaky';
82
+ return 'unreliable';
83
+ }
84
+
85
+ /**
86
+ * Convert move flag to descriptive text
87
+ */
88
+ function getFlagDescription(flag: MoveFlag): string {
89
+ switch (flag) {
90
+ case 'contact': return 'requiring physical contact';
91
+ case 'bite': return 'using a biting attack';
92
+ case 'punch': return 'delivered as a punch';
93
+ case 'sound': return 'using sound waves';
94
+ case 'explosive': return 'with explosive force';
95
+ case 'draining': return 'that drains energy';
96
+ case 'ground': return 'affecting grounded targets';
97
+ case 'priority': return 'with quick execution';
98
+ case 'lowPriority': return 'with delayed execution';
99
+ case 'charging': return 'requiring preparation';
100
+ case 'recharge': return 'causing exhaustion afterward';
101
+ case 'multiHit': return 'striking multiple times';
102
+ case 'twoTurn': return 'taking two turns to complete';
103
+ case 'sacrifice': return 'at great cost to the user';
104
+ case 'gambling': return 'with unpredictable results';
105
+ case 'reckless': return 'with reckless abandon';
106
+ case 'reflectable': return 'that can be reflected';
107
+ case 'snatchable': return 'that can be stolen';
108
+ case 'copyable': return 'that can be copied';
109
+ case 'protectable': return 'blocked by protection moves';
110
+ case 'bypassProtect': return 'that bypasses protection';
111
+ default: return '';
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Generate description for move effects
117
+ */
118
+ function getEffectsDescription(effects: BattleEffect[]): string {
119
+ const descriptions: string[] = [];
120
+
121
+ for (const effect of effects) {
122
+ const desc = getEffectDescription(effect);
123
+ if (desc) descriptions.push(desc);
124
+ }
125
+
126
+ if (descriptions.length === 0) return '';
127
+ if (descriptions.length === 1) return descriptions[0];
128
+ if (descriptions.length === 2) return descriptions.join(' and ');
129
+
130
+ return descriptions.slice(0, -1).join(', ') + ', and ' + descriptions[descriptions.length - 1];
131
+ }
132
+
133
+ /**
134
+ * Generate description for a single effect
135
+ */
136
+ function getEffectDescription(effect: BattleEffect): string {
137
+ switch (effect.type) {
138
+ case 'damage':
139
+ return getDamageEffectDescription(effect);
140
+ case 'modifyStats':
141
+ return getStatModificationDescription(effect);
142
+ case 'applyStatus':
143
+ return getStatusEffectDescription(effect);
144
+ case 'heal':
145
+ return getHealEffectDescription(effect);
146
+ case 'manipulatePP':
147
+ return getPPEffectDescription(effect);
148
+ case 'fieldEffect':
149
+ return 'affects the battlefield';
150
+ case 'counter':
151
+ return 'retaliates against attacks';
152
+ case 'removeStatus':
153
+ return 'removes status conditions';
154
+ case 'mechanicOverride':
155
+ return 'alters battle mechanics';
156
+ default:
157
+ return '';
158
+ }
159
+ }
160
+
161
+ function getDamageEffectDescription(effect: any): string {
162
+ if (effect.formula === 'recoil') return 'causes recoil damage to the user';
163
+ if (effect.formula === 'drain') return 'restores HP equal to damage dealt';
164
+ if (effect.formula === 'fixed') return 'deals fixed damage';
165
+ if (effect.formula === 'percentage') return 'deals percentage-based damage';
166
+
167
+ if (effect.target === 'self') return 'damages the user';
168
+ return ''; // Standard damage is implied by power
169
+ }
170
+
171
+ function getStatModificationDescription(effect: any): string {
172
+ const target = effect.target === 'self' ? 'the user\'s' : 'the target\'s';
173
+ const stats = Object.keys(effect.stats);
174
+ const modifications = Object.values(effect.stats) as StatModification[];
175
+
176
+ if (stats.length === 1) {
177
+ const statName = stats[0];
178
+ const modification = modifications[0];
179
+ const modDesc = getStatModificationText(modification);
180
+ return `${modDesc} ${target} ${statName}`;
181
+ }
182
+
183
+ return `modifies ${target} battle stats`;
184
+ }
185
+
186
+ function getStatModificationText(modification: StatModification): string {
187
+ switch (modification) {
188
+ case 'increase': return 'raises';
189
+ case 'greatly_increase': return 'sharply raises';
190
+ case 'decrease': return 'lowers';
191
+ case 'greatly_decrease': return 'sharply lowers';
192
+ default: return 'affects';
193
+ }
194
+ }
195
+
196
+ function getStatusEffectDescription(effect: any): string {
197
+ const target = effect.target === 'self' ? 'the user' : 'the target';
198
+ const chance = effect.chance ? ` (${effect.chance}% chance)` : '';
199
+
200
+ switch (effect.status) {
201
+ case 'burn': return `may burn ${target}${chance}`;
202
+ case 'freeze': return `may freeze ${target}${chance}`;
203
+ case 'paralyze': return `may paralyze ${target}${chance}`;
204
+ case 'poison': return `may poison ${target}${chance}`;
205
+ case 'sleep': return `may put ${target} to sleep${chance}`;
206
+ case 'confuse': return `may confuse ${target}${chance}`;
207
+ default: return `may inflict ${effect.status} on ${target}${chance}`;
208
+ }
209
+ }
210
+
211
+ function getHealEffectDescription(effect: any): string {
212
+ const target = effect.target === 'self' ? 'the user' : 'the target';
213
+
214
+ if (effect.amount === 'full') return `fully restores ${target}'s HP`;
215
+ if (effect.amount === 'large') return `greatly restores ${target}'s HP`;
216
+ if (effect.amount === 'medium') return `moderately restores ${target}'s HP`;
217
+ if (effect.amount === 'small') return `slightly restores ${target}'s HP`;
218
+
219
+ return `restores ${target}'s HP`;
220
+ }
221
+
222
+ function getPPEffectDescription(effect: any): string {
223
+ const target = effect.target === 'self' ? 'the user\'s' : 'the target\'s';
224
+
225
+ switch (effect.action) {
226
+ case 'drain': return `reduces ${target} move PP`;
227
+ case 'restore': return `restores ${target} move PP`;
228
+ case 'disable': return `disables ${target} moves`;
229
+ default: return `affects ${target} move usage`;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Generate a natural language description for a special ability based on its properties
235
+ */
236
+ export function generateAbilityDescription(ability: SpecialAbility): string {
237
+ const parts: string[] = [];
238
+
239
+ // Handle abilities with triggers (most common case)
240
+ if (ability.triggers && ability.triggers.length > 0) {
241
+ const triggerDescriptions = ability.triggers.map(trigger => getTriggerDescription(trigger));
242
+ const validTriggers = triggerDescriptions.filter(Boolean);
243
+
244
+ if (validTriggers.length > 0) {
245
+ parts.push(...validTriggers);
246
+ }
247
+ }
248
+
249
+ // Handle abilities with direct effects (less common)
250
+ if (ability.effects && ability.effects.length > 0) {
251
+ const effectDescriptions = getEffectsDescription(ability.effects);
252
+ if (effectDescriptions) {
253
+ parts.push(effectDescriptions);
254
+ }
255
+ }
256
+
257
+ // If no triggers or effects, provide a generic description
258
+ if (parts.length === 0) {
259
+ return `A mysterious ability called "${ability.name}".`;
260
+ }
261
+
262
+ // Combine all parts into a cohesive description
263
+ if (parts.length === 1) {
264
+ return parts[0] + '.';
265
+ }
266
+
267
+ return parts.join(' ') + '.';
268
+ }
269
+
270
+ /**
271
+ * Convert trigger event and effects into descriptive text
272
+ */
273
+ function getTriggerDescription(trigger: Trigger): string {
274
+ const eventText = getTriggerEventText(trigger.event);
275
+ const effectsText = getEffectsDescription(trigger.effects);
276
+ const conditionText = trigger.condition ? getConditionText(trigger.condition) : '';
277
+
278
+ let description = eventText;
279
+
280
+ if (conditionText) {
281
+ description += ` ${conditionText}`;
282
+ }
283
+
284
+ if (effectsText) {
285
+ description += `, ${effectsText}`;
286
+ }
287
+
288
+ return description;
289
+ }
290
+
291
+ /**
292
+ * Convert trigger event to human-readable text
293
+ */
294
+ function getTriggerEventText(event: string): string {
295
+ switch (event) {
296
+ case 'onDamageTaken': return 'When taking damage';
297
+ case 'onDamageDealt': return 'When dealing damage';
298
+ case 'onContactDamage': return 'When hit by contact moves';
299
+ case 'onStatusInflicted': return 'When afflicted with a status condition';
300
+ case 'onStatusMove': return 'When using status moves';
301
+ case 'onStatusMoveTargeted': return 'When targeted by status moves';
302
+ case 'onCriticalHit': return 'When landing critical hits';
303
+ case 'onHPDrained': return 'When HP is drained';
304
+ case 'onKO': return 'When knocked out';
305
+ case 'onSwitchIn': return 'When switching in';
306
+ case 'onSwitchOut': return 'When switching out';
307
+ case 'onWeatherChange': return 'When weather changes';
308
+ case 'beforeMoveUse': return 'Before using moves';
309
+ case 'afterMoveUse': return 'After using moves';
310
+ case 'onLowHP': return 'When HP is low';
311
+ case 'onFullHP': return 'When at full HP';
312
+ case 'endOfTurn': return 'At the end of each turn';
313
+ case 'onOpponentContactMove': return 'When the opponent uses contact moves';
314
+ case 'onStatChange': return 'When stats are changed';
315
+ case 'onTypeChange': return 'When type is changed';
316
+ default: return `When ${event.replace(/^on/, '').toLowerCase()}`;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Convert effect condition to descriptive text
322
+ */
323
+ function getConditionText(condition: string): string {
324
+ switch (condition) {
325
+ case 'always': return '';
326
+ case 'onHit': return 'on hit';
327
+ case 'afterUse': return 'after use';
328
+ case 'onCritical': return 'on critical hits';
329
+ case 'ifLowHp': return 'when HP is low';
330
+ case 'ifHighHp': return 'when HP is high';
331
+ case 'thisTurn': return 'this turn';
332
+ case 'nextTurn': return 'next turn';
333
+ case 'turnAfterNext': return 'the turn after next';
334
+ case 'restOfBattle': return 'for the rest of battle';
335
+ case 'onCharging': return 'while charging';
336
+ case 'afterCharging': return 'after charging';
337
+ case 'ifDamagedThisTurn': return 'if damaged this turn';
338
+ case 'ifNotSuperEffective': return 'against non-super-effective moves';
339
+ case 'ifStatusMove': return 'with status moves';
340
+ case 'ifLucky50': return 'with 50% luck';
341
+ case 'ifUnlucky50': return 'with 50% bad luck';
342
+ case 'whileFrozen': return 'while frozen';
343
+ case 'whenStatusAfflicted': return 'when status is inflicted';
344
+ case 'vsPhysical': return 'against physical moves';
345
+ case 'vsSpecial': return 'against special moves';
346
+ default:
347
+ // Handle type-specific conditions
348
+ if (condition.startsWith('ifMoveType:')) {
349
+ const type = condition.split(':')[1];
350
+ return `with ${type} moves`;
351
+ }
352
+ if (condition.startsWith('ifStatus:')) {
353
+ const status = condition.split(':')[1];
354
+ return `when ${status}`;
355
+ }
356
+ if (condition.startsWith('ifWeather:')) {
357
+ const weather = condition.split(':')[1];
358
+ return `in ${weather} weather`;
359
+ }
360
+ return condition;
361
+ }
362
+ }