View Javadoc

1   /*
2    *   Firemox is a turn based strategy simulator
3    *   Copyright (C) 2003-2007 Fabrice Daugan
4    *
5    *   This program is free software; you can redistribute it and/or modify it 
6    * under the terms of the GNU General Public License as published by the Free 
7    * Software Foundation; either version 2 of the License, or (at your option) any
8    * later version.
9    *
10   *   This program is distributed in the hope that it will be useful, but WITHOUT 
11   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12   * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
13   * details.
14   *
15   *   You should have received a copy of the GNU General Public License along  
16   * with this program; if not, write to the Free Software Foundation, Inc., 
17   * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18   */
19  package net.sf.firemox.action;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.List;
24  
25  import net.sf.firemox.action.context.ActionContextWrapper;
26  import net.sf.firemox.action.context.ManaCost;
27  import net.sf.firemox.action.handler.ChosenAction;
28  import net.sf.firemox.action.handler.InitAction;
29  import net.sf.firemox.action.handler.RollBackAction;
30  import net.sf.firemox.action.listener.WaitingAbility;
31  import net.sf.firemox.action.listener.WaitingMana;
32  import net.sf.firemox.clickable.ability.Ability;
33  import net.sf.firemox.clickable.mana.Mana;
34  import net.sf.firemox.clickable.target.card.MCard;
35  import net.sf.firemox.clickable.target.player.Player;
36  import net.sf.firemox.event.context.ContextEventListener;
37  import net.sf.firemox.expression.Expression;
38  import net.sf.firemox.expression.ExpressionFactory;
39  import net.sf.firemox.expression.RegisterAccess;
40  import net.sf.firemox.network.ConnectionManager;
41  import net.sf.firemox.network.message.CoreMessageType;
42  import net.sf.firemox.stack.StackManager;
43  import net.sf.firemox.test.TestOn;
44  import net.sf.firemox.token.IdCardColors;
45  import net.sf.firemox.token.IdCommonToken;
46  import net.sf.firemox.token.IdConst;
47  import net.sf.firemox.token.IdTokens;
48  import net.sf.firemox.token.MCommonVars;
49  import net.sf.firemox.tools.Log;
50  import net.sf.firemox.tools.MToolKit;
51  import net.sf.firemox.ui.MagicUIComponents;
52  import net.sf.firemox.ui.i18n.LanguageManager;
53  import net.sf.firemox.zone.MZone;
54  
55  /***
56   * Used to pay mana, remove directly mana from the mana pool <br>
57   * 
58   * @author <a href="mailto:fabdouglas@users.sourceforge.net">Fabrice Daugan </a>
59   * @since 0.54
60   * @since 0.70 'auto use colored mana as colorless mana' AI has been upgraded to
61   *        manage case where only one colored mana can be used as colorless mana
62   * @since 0.71 'auto use colored mana as colorless mana' AI has been upgraded to
63   *        prevent the opponent to be informed you are using this option.
64   * @since 0.85 Mana pay can be aborted.
65   * @see net.sf.firemox.action.Actiontype#PAY_MANA
66   */
67  public class PayMana extends UserAction implements ChosenAction, InitAction,
68  		WaitingMana, WaitingAbility, RollBackAction {
69  
70  	/***
71  	 * If true, mana operation are nor ignored.
72  	 */
73  	public static boolean useMana;
74  
75  	/***
76  	 * The maximum number of mana symbols displayed in context menu.
77  	 */
78  	public static int thresholdColored;
79  
80  	/***
81  	 * This method is invoked when opponent has clicked on manas.
82  	 * 
83  	 * @param data
84  	 *          data sent by opponent.
85  	 */
86  	public static void clickOn(byte[] data) {
87  		// waiting for mana information
88  		if (!(StackManager.actionManager.currentAction instanceof PayMana)) {
89  			Log.fatal("Current action is not 'pay mana' but '"
90  					+ StackManager.actionManager.currentAction.getClass().getName());
91  		}
92  
93  		final int color = data[0];
94  		final PayMana action = (PayMana) StackManager.actionManager.currentAction;
95  		StackManager.actionManager.succeedClickOn(action.controller.getPlayer(
96  				StackManager.currentAbility, null).mana.manaButtons[color]);
97  	}
98  
99  	/***
100 	 * Return Html string corresponding to the given mana pool. Colors with no
101 	 * mana in the mana pool are ignored. If a color appear more than
102 	 * THRESHOLD_COLORED, the <code>{color} x {amount}</code> will be used. If
103 	 * the given code is empty the <code>{0}</code> value will be returned.
104 	 * 
105 	 * @param manaPool
106 	 *          the amount of mana to pay
107 	 * @return Html string corresponding to the given mana pool.
108 	 */
109 	public static String toHtmlString(int[] manaPool) {
110 		String res = null;
111 
112 		if (manaPool[0] != 0) {
113 			res = MToolKit.getHtmlMana(0, manaPool[0]);
114 		}
115 
116 		for (int j = IdCommonToken.COLOR_NAMES.length; j-- > 1;) {
117 			if (manaPool[j] != 0) {
118 				if (res == null) {
119 					res = "";
120 				}
121 				if (manaPool[j] > PayMana.thresholdColored) {
122 					res += MToolKit.getHtmlMana(j, 1) + "x" + manaPool[j];
123 				} else {
124 					res += MToolKit.getHtmlMana(j, manaPool[j]);
125 				}
126 			}
127 		}
128 		if (res == null) {
129 			return " " + MToolKit.getHtmlMana(0, 0);
130 		}
131 		return res;
132 	}
133 
134 	/***
135 	 * The test manager
136 	 */
137 	private TestOn on;
138 
139 	/***
140 	 * The complex expression to use for the right value. Is null if the IdToken
141 	 * number is not a complex expression.
142 	 */
143 	private Expression[] codeExpr = null;
144 
145 	/***
146 	 * The player paying this mana
147 	 */
148 	public TestOn controller;
149 
150 	/***
151 	 * Create an instance of PayMana by reading a file Offset's file must pointing
152 	 * on the first byte of this action <br>
153 	 * <ul>
154 	 * Structure of InputStream : Data[size]
155 	 * <li>the player paying this mana [TestOn]</li>
156 	 * <li>mode IdTokens#MANA_POOL[2]
157 	 * <li>[on [Expression]]</li>
158 	 * <li>COLORLESS [Expression]</li>
159 	 * <li>BLACK [Expression]</li>
160 	 * <li>BLUE [Expression]</li>
161 	 * <li>GREEN [Expression]</li>
162 	 * <li>RED [Expression]</li>
163 	 * <li>WHITE [Expression]</li>
164 	 * </ul>
165 	 * 
166 	 * @param inputFile
167 	 *          file containing this action
168 	 * @throws IOException
169 	 *           if error occurred during the reading process from the specified
170 	 *           input stream
171 	 */
172 	PayMana(InputStream inputFile) throws IOException {
173 		super(inputFile);
174 		controller = TestOn.deserialize(inputFile);
175 		int code0 = MToolKit.readInt16(inputFile);
176 		if (code0 == IdTokens.MANA_POOL) {
177 			on = TestOn.deserialize(inputFile);
178 		} else {
179 			codeExpr = new Expression[IdCommonToken.COLOR_NAMES.length];
180 			for (int i = 0; i < codeExpr.length; i++) {
181 				codeExpr[i] = ExpressionFactory.readNextExpression(inputFile);
182 			}
183 		}
184 	}
185 
186 	public List<Ability> abilitiesOf(MCard card) {
187 		return WaitChosenActionChoice.getInstance().abilitiesOf(card);
188 	}
189 
190 	public List<Ability> advancedAbilitiesOf(MCard card) {
191 		return WaitChosenActionChoice.getInstance().advancedAbilitiesOf(card);
192 	}
193 
194 	public boolean choose(ActionContextWrapper actionContext,
195 			ContextEventListener context, Ability ability) {
196 		if (!useMana) {
197 			return true;
198 		}
199 
200 		final Player controller = this.controller.getPlayer(ability, context, null);
201 		final ManaCost manaContext = (ManaCost) actionContext.actionContext;
202 
203 		if (manaContext.isNullRequired()) {
204 			// no more mana to pay
205 			return true;
206 		}
207 
208 		// highlight the selectable mana
209 		for (int idColor = IdCardColors.CARD_COLOR_NAMES.length; idColor-- > 0;) {
210 			if (controller.mana.getMana(idColor, ability) > 0
211 					&& (manaContext.requiredMana[idColor] > 0 || manaContext.requiredMana[0] > 0)) {
212 				controller.mana.manaButtons[idColor].highLight(null);
213 
214 			} else {
215 				controller.mana.manaButtons[idColor].disHighLight();
216 			}
217 		}
218 		controller.mana.repaint();
219 
220 		// update the ticket
221 		MagicUIComponents.magicForm.moreInfoLbl.setText("<html>"
222 				+ LanguageManager.getString("manatopay",
223 						toHtmlString(manaContext.manaPaid),
224 						toHtmlString(manaContext.requiredMana)));
225 		actionContext.refreshText(ability, context);
226 
227 		// display playable mana source abilities
228 		WaitChosenActionChoice.getInstance().play(controller);
229 
230 		if (controller.isYou() && MCommonVars.autoMana) {
231 			// enougth colored mana?
232 			int remainingColoredMana = 0;
233 			int uniqueColored = -1;
234 
235 			for (int i = IdCardColors.CARD_COLOR_NAMES.length; i-- > 1;) {
236 				if (manaContext.requiredMana[i] > controller.mana.getMana(i, ability)) {
237 					// no enougth colored mana
238 					return false;
239 				}
240 
241 				// calculate remaining colored mana
242 				if (remainingColoredMana != 0) {
243 					uniqueColored = -1;
244 				} else {
245 					uniqueColored = i;
246 				}
247 				remainingColoredMana += controller.mana.getMana(i, ability)
248 						- manaContext.requiredMana[i];
249 			}
250 
251 			if (manaContext.requiredMana[0] <= controller.mana.getMana(0, ability)) {
252 				// we can pay all required colorless mana as normal
253 				validateCode(manaContext.requiredMana, controller);
254 			} else if (manaContext.requiredMana[0] == controller.mana.getMana(0,
255 					ability)
256 					+ remainingColoredMana) {
257 				// we spend all remaining colored mana to pay the required colorless
258 				final int[] validCode = new int[IdCardColors.CARD_COLOR_NAMES.length];
259 				for (int i = validCode.length; i-- > 0;) {
260 					validCode[i] = controller.mana.getMana(i, ability);
261 				}
262 				validateCode(validCode, controller);
263 			} else if (manaContext.requiredMana[0] < controller.mana.getMana(0)
264 					+ remainingColoredMana) {
265 				// we have enough colored mana to pay required colorless mana
266 				if (uniqueColored != -1) {
267 					// only one color can be used as colorless mana
268 					final int[] validCode = new int[6];
269 					validCode[0] = controller.mana.getMana(0, ability);
270 					for (int i = validCode.length; i-- > 1;) {
271 						validCode[i] = manaContext.requiredMana[i];
272 					}
273 					validCode[uniqueColored] += manaContext.requiredMana[0]
274 							- validCode[0];
275 					validateCode(validCode, controller);
276 				} else {
277 					// colored mana will be automatically used as colorless mana
278 					final int[] coloredHand = new int[IdCardColors.CARD_COLOR_NAMES.length];
279 					final MZone hand = controller.zoneManager.hand;
280 					// count all manas used for all cards in the hand
281 					for (int i = hand.getComponentCount(); i-- > 0;) {
282 						final int[] colors = hand.getCard(i).cachedRegisters;
283 						for (int color = coloredHand.length; color-- > 1;) {
284 							if (colors[color] > coloredHand[color - 1]) {
285 								coloredHand[color] = colors[color];
286 							}
287 						}
288 					}
289 
290 					int colorlessToPay = manaContext.requiredMana[0]
291 							- controller.mana.getMana(0, ability);
292 					int totalColored = 0;
293 					final int[] manaPaid = new int[IdCardColors.CARD_COLOR_NAMES.length];
294 					for (int color = manaPaid.length; color-- > 1;) {
295 						manaPaid[color] = manaContext.requiredMana[color];
296 						totalColored = 1 + coloredHand[color];
297 					}
298 					manaPaid[0] = controller.mana.getMana(0, ability);
299 					final int ratioRemove = totalColored < colorlessToPay ? 1
300 							: totalColored / colorlessToPay;
301 
302 					while (colorlessToPay-- > 0) {
303 						int miniColor = -1;
304 						int mini = 0;
305 						for (int color = manaPaid.length; color-- > 1;) {
306 							if ((miniColor == -1 || mini > coloredHand[color])
307 									&& controller.mana.getMana(color, ability) - manaPaid[color] > 0) {
308 								// we setthe nex=t color to remove
309 								miniColor = color;
310 								mini = coloredHand[color];
311 							}
312 						}
313 						if (miniColor == -1) {
314 							throw new InternalError("couldn't find the color to use");
315 						}
316 						coloredHand[miniColor] -= ratioRemove;
317 						manaPaid[miniColor]++;
318 					}
319 					validateCode(manaPaid, controller);
320 				}
321 			}
322 			return false;
323 		}
324 
325 		return false;
326 	}
327 
328 	public boolean clickOn(Ability ability) {
329 		return WaitChosenActionChoice.getInstance().clickOn(ability);
330 	}
331 
332 	public boolean clickOn(Mana mana) {
333 		final Player controller = this.controller.getPlayer(
334 				StackManager.currentAbility, null);
335 		final ManaCost manaContext = (ManaCost) StackManager.actionManager
336 				.getActionContext().actionContext;
337 		// one less colorless mana to pay
338 		if (controller.isYou() && mana.getMana() > 0) {
339 			if (mana.color == 0) {
340 				return manaContext.requiredMana[0] > 0;
341 			}
342 			return manaContext.requiredMana[mana.color] > 0
343 					|| manaContext.requiredMana[0] > 0;
344 		}
345 		return false;
346 	}
347 
348 	/***
349 	 * No generated event. Unset this action as current one.
350 	 * 
351 	 * @param actionContext
352 	 *          the context containing data saved by this action during the
353 	 *          'choose" process.
354 	 * @param ability
355 	 *          is the ability owning this test. The card component of this
356 	 *          ability should correspond to the card owning this test too.
357 	 * @param context
358 	 *          is the context attached to this action.
359 	 */
360 	public void disactivate(ActionContextWrapper actionContext,
361 			ContextEventListener context, Ability ability) {
362 		// mana paid are not restored, this action is simply paused
363 		controller.getPlayer(ability, context, null).mana.disHighLight();
364 	}
365 
366 	/***
367 	 * Called when this action is finished (aborted or completed). No stack
368 	 * operation should be done here.<br>
369 	 * For this action, all mana are dishilighted.
370 	 */
371 	public void finished() {
372 		final Player controller = this.controller.getPlayer(
373 				StackManager.currentAbility, null);
374 		for (Mana mana : controller.mana.manaButtons) {
375 			mana.disHighLight();
376 		}
377 		WaitChosenActionChoice.getInstance().finished();
378 	}
379 
380 	private int[] getCode(Ability ability, ContextEventListener context) {
381 		if (on != null) {
382 			return on.getCard(ability, null).cachedRegisters;
383 		}
384 
385 		final int[] code = new int[codeExpr.length];
386 		for (int i = 6; i-- > 0;) {
387 			final Expression expr = codeExpr[i];
388 			if (expr instanceof RegisterAccess && ((RegisterAccess) expr).isXvalue()) {
389 				code[i] = -1;
390 			} else {
391 				code[i] = codeExpr[i].getValue(ability, null, context);
392 			}
393 		}
394 		return code;
395 	}
396 
397 	@Override
398 	public Actiontype getIdAction() {
399 		return Actiontype.PAY_MANA;
400 	}
401 
402 	public boolean init(ActionContextWrapper actionContext,
403 			ContextEventListener context, Ability ability) {
404 		// Calculate the needed mana only if this not already done.
405 		if (actionContext.actionContext == null) {
406 			actionContext.actionContext = new ManaCost(manaNeeded(ability, context));
407 		}
408 		return true;
409 	}
410 
411 	/***
412 	 * Return the amount of mana needed (constant part only) to play this ability
413 	 * As default, we return an empty number for all manas.
414 	 * 
415 	 * @param ability
416 	 *          is the ability owning this test. The card component of this
417 	 *          ability should correspond to the card owning this test too.
418 	 * @param context
419 	 *          the context of playing ability.
420 	 * @return array of mana needed to play this ability
421 	 */
422 	public int[] manaNeeded(Ability ability, ContextEventListener context) {
423 		if (!useMana) {
424 			return IdConst.EMPTY_CODE;
425 		}
426 
427 		// this action need our manas
428 		if (on != null) {
429 			return on.getCard(ability, null).cachedRegisters;
430 		}
431 		final int[] realCode = new int[codeExpr.length];
432 		for (int i = realCode.length; i-- > 0;) {
433 			realCode[i] = codeExpr[i].getValue(ability, null, context);
434 		}
435 		return realCode;
436 	}
437 
438 	public boolean manualSkip() {
439 		// cancel the ability since required manas are not paid
440 		StackManager.cancel();
441 		return false;
442 	}
443 
444 	public boolean replay(ActionContextWrapper actionContext,
445 			ContextEventListener context, Ability ability) {
446 		assert ((ManaCost) actionContext.actionContext).isNullRequired();
447 		// pay the mana
448 		final int[] manaPaid = ((ManaCost) actionContext.actionContext).manaPaid;
449 		final Player controller = this.controller.getPlayer(ability, context, null);
450 		for (int idColor = controller.mana.manaButtons.length; idColor-- > 0;) {
451 			controller.mana.removeMana(idColor, manaPaid[idColor], ability);
452 		}
453 		return true;
454 	}
455 
456 	/***
457 	 * No generated event. Rollback an action.
458 	 * 
459 	 * @param actionContext
460 	 *          the context containing data saved by this action during the
461 	 *          'choose" process.
462 	 * @param ability
463 	 *          is the ability owning this test. The card component of this
464 	 *          ability should correspond to the card owning this test too.
465 	 * @param context
466 	 *          is the context attached to this action.
467 	 */
468 	public void rollback(ActionContextWrapper actionContext,
469 			ContextEventListener context, Ability ability) {
470 		// restore the mana pool
471 		final Player controller = this.controller.getPlayer(
472 				StackManager.currentAbility, context, null);
473 		((ManaCost) actionContext.actionContext).restoreMana(controller.mana);
474 	}
475 
476 	public boolean succeedClickOn(Ability ability) {
477 		finished();
478 		StackManager.newSpell(ability, ability.isMatching());
479 		return true;
480 	}
481 
482 	public boolean succeedClickOn(Mana mana) {
483 		finished();
484 		final Player controller = this.controller.getPlayer(
485 				StackManager.currentAbility, null);
486 
487 		if (mana.getMana() == 0) {
488 			Log.fatal("Player has not this mana : " + mana);
489 		}
490 
491 		final ManaCost manaCost = (ManaCost) StackManager.actionManager
492 				.getActionContext().actionContext;
493 		if (mana.color == 0) {
494 			if (manaCost.requiredMana[mana.color] > 0
495 					&& controller.mana.getMana(0) > 0) {
496 				manaCost.payMana(0, 0, 1, controller.mana);
497 			} else {
498 				Log.fatal("No colorless mana can be paid this way : " + mana);
499 			}
500 		} else {
501 			if (manaCost.requiredMana[mana.color] > 0) {
502 				// pay a colored mana
503 				manaCost.payMana(mana.color, mana.color, 1, controller.mana);
504 			} else if (manaCost.requiredMana[0] > 0) {
505 				// pay a colorless mana with colored one
506 				manaCost.payMana(0, mana.color, 1, controller.mana);
507 			} else {
508 				Log.fatal("This colored mana is not required : " + mana);
509 			}
510 		}
511 
512 		// send the event to opponent
513 		if (controller.isYou()) {
514 			ConnectionManager.send(CoreMessageType.CLICK_PAY_MANA, (byte) mana.color);
515 		}
516 
517 		// check if there is a required mana to pay
518 		return choose(StackManager.actionManager.getActionContext(), StackManager
519 				.getInstance().getAbilityContext(), StackManager.currentAbility);
520 	}
521 
522 	@Override
523 	public String toHtmlString(Ability ability, ContextEventListener context) {
524 		return toHtmlString(getCode(ability, context));
525 	}
526 
527 	public String toHtmlString(Ability ability, ContextEventListener context,
528 			ActionContextWrapper actionContext) {
529 		final ManaCost manaCost = (ManaCost) actionContext.actionContext;
530 		for (int color = manaCost.requiredMana.length; color-- > 0;) {
531 			if (manaCost.requiredMana[color] > 0) {
532 				// at least one required mana
533 				return toHtmlString(manaCost.manaCost) + ", required "
534 						+ toHtmlString(manaCost.requiredMana);
535 			}
536 		}
537 		// no remaining mana
538 		return toHtmlString(manaCost.manaCost);
539 	}
540 
541 	@Override
542 	public String toString(Ability ability) {
543 		String res = null;
544 
545 		// this action need our manas
546 		final int[] code = getCode(ability, null);
547 
548 		if (code[0] != 0) {
549 			res = "" + code[0];
550 		}
551 
552 		for (int j = IdCommonToken.COLOR_NAMES.length; j-- > 1;) {
553 			if (code[j] != 0) {
554 				if (res == null) {
555 					res = "";
556 				}
557 				res += IdCommonToken.COLOR_NAMES[j] + "x" + code[j] + ",";
558 			}
559 		}
560 		if (res == null) {
561 			return MToolKit.getHtmlMana(0, 0);
562 		}
563 		return res;
564 	}
565 
566 	private void validateCode(int[] valideMana, Player controller) {
567 		for (int i = controller.mana.manaButtons.length; i-- > 0;) {
568 			if (valideMana[i] > 0) {
569 				StackManager.actionManager
570 						.succeedClickOn(controller.mana.manaButtons[i]);
571 				return;
572 			}
573 		}
574 	}
575 }