View Javadoc

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